Java 三大特性

  Java 中有三个非常重要的特性,即封装、继承、多态
  那么什么是封装?什么是继承?什么又是多态呢?
  跟随这篇文章来详细的了解一下吧!

什么是封装?

  在面向对象编程方法中,封装(英语:Encapsulation)是指,一种将抽象)性函数接口的实现细节部分包装、隐藏起来的方法。同时,它也是一种防止外界调用端,去访问对象内部实现细节的手段,这个手段是由编程语言本身来提供的。

  适当的封装,可以将对象使用接口的程序实现部分隐藏起来,不让用户看到,同时确保用户无法任意更改对象内部的重要数据。
  因此,封装可以让代码更容易理解与维护,也加强了代码的安全性。
  在 Java 中,可以在类中使用private修饰符将变量封装起来,若不提供该变量的setter方法,一旦通过该类new了一个对象,外部将无法修改这个变量。
  换而言之,Java 可以选择性的将部分不想给外部展示的变量包装、隐藏起来,防止外界调用。
  当然,此种做法并不绝对有效,因为通过反射机制还是可以破开这层封装,取到相关变量,然而一般并不会这么做。

继承

什么是继承?

  继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别 B “继承自”另一个类别 A,就把这个 B 称为“ A 的子类”,而把 A 称为“ B 的父类”,也可以称“ A 是 B 的超类”。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为 m 子类追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类的行为在编译期就已经决定,无法在运行期扩展。
  有些编程语言支持多重继承,即一个子类可以同时有多个父类,比如 C++编程语言;而在有些编程语言中,一个子类只能继承自一个父类别,比如 Java 编程语言,这时可以通过实现接口)来实现与多重继承相似的效果。

  在 Java 中,利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和变量。在此基础上, 还可以添加一些新的方法和变量,以满足新的需求。
  通过继承,可以达到代码复用与扩展的效果。

定义父子类

  在 Java 中的继承通过使用关键字extends表示。
  下面通过一个例子来了解一下:
  首先定义一个父类Worker类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Worker {
public String name;
public String age;
private double salalary;

public Worker() {
}

public Worker(String name, String age) {
this.name = name;
this.age = age;
}
//getter、setter方法省略
}

  这个Worker类初始化时可以设置姓名,年龄,并且还有一个设置薪水的方法。
  然后使用extends关键字来继承该类,定义子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Manager extends Worker {
public double bonus;

public Manager(String name, String age) {
super(name, age);
}

@Override
public void setSalalary(double salalary) {
super.setSalalary(salalary);
}

public void setBonus(double bonus) {
this.bonus = bonus;
}

public double getBonus() {
return bonus;
}
}

  与Worker不同的时,该类新增了一个奖金的成员变量bonus,且可以通过setBonus方法设置其值。
  当然,setBonus方法在Worker类中并没有定义,所以属于Worker类的对象不能使用它。
  对于Worker类其他方法,尽管在Manager类中没有显式地定义getNamegetAge等方法, 但属于Manager类的对象却可以使用它们,这是因为 Manager 类自动地继承了父类Worker中的这些方法。
  同样的, 子类Manager还继承了父类Worker成员变量nameagesalalary,所以现在子类Manager拥有 4 个成员变量。
  子类比超类拥有的功能更加丰富。

覆盖父类方法

  在子类中,有时需要重写父类的方法,为什么要这么做呢?
  因为父类中的有些方法对子类Manager并不一定适用。
  比如经理的薪水肯定比普通工人高,毕竟还加上了奖金呀!
  因此,需要提供一个新的方法来覆盖(override)父类中的这个方法,如下:

1
2
3
4
@Override
public double getSalalary() {
return super.getSalalary() + this.bonus;
}

  尽管每个Manager对象都拥有一个名为salary的成员变量, 但在Manager类的getSalary方法中并不能够直接地访问父类Worker的成员变量salary,因为父类的salaryprivate修饰符修饰了。
  那么该如何去访问呢?
  我们知道,父类的setter方法能够访问私有部分,父类提供了这个方法,那么在子类中怎么使用这个方法?
  使用super.加上方法名即可。
  当然,对于父类的其他修饰符(不能为private哦)修饰的成员变量,还可以通过super.加上变量名的方式获取。

子类的构造器

  对子类的构造器代码:

1
2
3
public Manager(String name, String age) {
super(name, age);
}

  其中super(name, age);,代表调用父类的有参构造器。如果不写,则代表调用父类默认的无参构造器,此时若父类没有无参构造器,编译器将报错。
  当然,在子类构造器中,若不写super(...)调用父类有参构造器,则可以使用this(...)调用本类的其他有参或无参构造器,其他构造器中必有super(...)调用父类的构造器。

继承中代码的执行顺序

  ① 父类静态变量和静态代码块(按照声明顺序);
  ② 子类静态变量和静态代码块(按照声明顺序);
  ③ 父类成员变量和代码块(按照声明顺序);
  ④ 父类构造器;
  ⑤ 子类成员变量和代码块(按照声明顺序);
  ⑥ 子类构造器。

继承层次的划分

  继承并不仅限于一个层次。 例如, 可以由Person类派生Teacher类、Student类等等, 由一个公共父类派生出来的所有类的集合被称为继承层次, 如图 :

  在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链。
  通常,一个祖先类可以拥有多个子孙继承链。
  例如, 可以由Teacher类派生出子类MathTeacher类或ChineseTeacher类, 它们与Student类没有任何关系(有可能它们彼此也没关系),必要的话,可以将这个过程一直延续下去。

不可继承

  有时候,可能希望阻止人们利用某个类定义子类。这种不允许扩展的类被称为最终(final)类,也叫不可继承类,在定义类的时候使用了final修饰符修饰的类就是最终类。
  示例如下:

1
public final class DemoFinalClass{}

  对类中的特定方法也可以声明为final,此时子类就不能覆盖这个方法。

注意哦:final类中的所有方法会自动地成为final方法,但其成员变量并不会自动被final修饰。

所有类的父类:Object 类

  在 Java 中,Object类是所有类的始祖,除了八大基本类型不是对象,所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
  换而言之, Java 中每个类都是扩展自Object类。
  那么思考一个问题,是不是每个类都需要向下面这么做呢?

1
public class Employee extends Object{}

  其实并不需要哦,因为如果没有明确地指出父类,Object类就被认为是这个类的父类。虽说 Java 是单继承,但你总会继承一个没有父类的类,这个类就默认继承Object类;
  通过Object类型的变量引用任何类型的对象,如下代码:

1
Object obj = new Person('zhangsan", 18);

  那我们如何访问Person类的变量或方法?肯定不能通过Object的变量来调用,它只是各种对象的通用持有者。
  因此,如果想对具体的对象(如Person类)进行具体的操作,必须进行相应的类型转换后再使用:

1
2
3
4
Person p = (Person) obj;
p.name;
p.age;
p.run();

toString 方法

  当我们使用System.out.println()打印一个对象时,默认使用的就是该方法,其返回的是当前对象的类名+@+对象哈希值的十六进制
  其源码如下:

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

  一般我们都会对对象的toString方法进行重写,方便开发。

hashCode 方法

  该方法默认返回一个逻辑地址的 10 进制表示,源码如下:

1
public native int hashCode();

equals 方法

  该方法指示其他某个对象是否与此对象相等,类似于==,源码如下:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

  有时候我们需要对该方法进行重写,比如下面的Person对象:

1
2
3
4
5
6
7
8
9
10
public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}
//getter、setter方法省略
}

  如果不重写它equals方法,对于下面语句输出的为false:

1
2
3
Person p1 = new Person("zhangsan",18);
Person p2 = new Person("zhangsan",18);
System.out.println(p1.equals(p2));

  因为此时Person类使用的是Object类的equals方法,比较的是p1p2的内存地址,这自然是不行的,18 岁的张三同学可是同一个人。
  因此,我们应该重写此方法,只要一个人姓名相同同时年龄(这里理解为出生日期更好)也相同(就是同一个人):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null) {
return false;
}
if (obj instanceof Person) {
Person p = (Person) obj;
return this.name == p.name && this.age == p.age;
}
return false;
}

  这里加了一个判断,如obj instanceof Person可以防止其他类型数据干扰,而obj == nullobj == this可以提高程序的效率。
  在 IDE 中,一般可以快速生成equals方法:

1
2
3
4
5
6
7
8
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}

  别人提供的代码比自己写优雅多了,getClass() != o.getClass()使用了反射技术,判断o是否为Person类型,等效于obj instanceof Person。而对于Objects类,可以防止空指针异常。

扩展——为什么要求重写 equals 方法时必须重写 hashCode 方法

  若我们只重写Person类的equals方法,而不重写其hashCode方法,它们相等嘛?
:相等,但不建议这么做,规定要求重写类的equals方法时必须重写其hashCode方法,因为若在HashMap中存储时是按对象的哈希值存储的。

Objects 类

  Objects类是 JDK 7 添加的一个工具类,提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),一般用于:

  • 计算对象的hashCode——hashCode方法
  • 返回对象的字符串表示形式——toString方法
  • 比较两个对象——equals方法

  Objects类是如何防止空指针异常呢?
  举个栗子,如果不使用Objects类:

1
2
3
String s1 = "abc";
String s2 = null;
boolean b = s1.equals(s2);

  如果执行上面的代码对低版本 JDK 可能会报空指针异常,而使用Objects类则不会有这个问题:

1
2
3
String s1 = "abc";
String s2 = null;
boolean b = Objects.equals(s1,s2);

什么是多态?

  在编程语言类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口)。 [1]#cite_note-1)多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。[2]#cite_note-Luca-2)

  计算机程序运行时,相同的消息可能会送给多个不同的类别之对象),而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。

  简单来说,所谓多态意指定义一个统一的接口,然后由不同的对象去分别实现(重写接口方法)。
  因此,相同的消息给予不同的对象会引发不同的动作

  多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定。

  即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

  因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

Java 中的多态

  Java 的多态是什么呢?其实就是一种能力——同一个行为具有不同的表现形式;换句话说就是,执行一段代码,Java 在运行时能根据对象的不同产生不同的结果。

  有一个用来判断是否应该设计为继承关系的简单规则,这就是is-a规则,它表明子类的每个对象也是父类的对象。
  例如,每个经理都是雇员,因此,将Manager类设计为 Worker类的子类是显而易见的,反之不然,并不是每一名员工都是经理。
  is-a规则的另一种表述法是置换法则。它表明程序中出现父类对象的任何地方都可以用子类对象置换。
  例如,可以将一个子类的对象赋给父类变量:

1
2
Worker w1 = new Worker();
Worker w2 = new Manager();

  在 Java 中,对象变量是多态的。 一个Worker变量既可以引用一个Worker类对象, 也可以引用一个Worker类的任何一个子类的对象。但是,不能将父类的引用赋给子类变量,下面的代码是错误的,并不是每一名员工都是经理:

1
Manager m1 = new Worker();

  多态有时也称作动态绑定、后期绑定或运行时绑定。通过多态,我们可以使用父类引用调用子类的方法。

什么是绑定?

  将一个方法调用同一个方法主体关联起来的操作被称作绑定。绑定分为 2 种:

  • 前期绑定:若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。
  • 后期绑定:就是在运行时根据对象的类型进行绑定,亦称作动态绑定或运行时绑定。

  如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。

1
Animal a = new Dog();

  这里,创建了一个Dog对象,并把得到的引用立即赋值给Animal的变量a,这样做看似错误(将一种类型赋值给另一种类型);但实际上是没问题的,因为通过继承,Dog就是一种Animal。因此,编译器认可这条语句,也就不会产出错误信息。
  假设你通过变量a调用了一个方法:a.run();
  你可能再次认为调用的是Animalrun();因为这毕竟是一个Animal引用,那么编译器是怎样知道去做其他的事情呢?
  由于后期绑定(多态),还是正确调用了子类Dog.run()方法。

接口多态:完全解耦

  只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。
  若你想要将这个方法应用于不在此继承结构中的某个类,那是不可能做到的。
  而接口可以在很大程度上放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。

不想动态绑定怎么做?

  Java 中除了static方法和final方法(private方法某种意义上也属于final方法)之外,其他所有的方法都是后期绑定。
  这意味着,通常情况下,不必判定是否应该进行后期绑定——因为它会自动发生。
  “关闭”动态绑定可以通过将某个方法声明为final,因为final修饰的方法不能被重写,因此它可以告诉编译器不要对该方法进行动态绑定,使该方法调用生成更有效的代码。

强制类型转换

  将一个类型强制转换成另外一个类型的过程被称为类型转换。对基本类型:

1
2
double x = 3.405;
int y = (int) x;

  这样可以将浮点型x的值强制转换成整数类型, 舍弃了小数部分。
  就像有时候需要将浮点型数值转换成整型数值一样,有时也可能需要将某个类的对象引用转换成另外一个类的对象引用。一般这么做:

1
2
Worker w = new Worker();
Manager m = (Manager)w;

  进行对象类型转换的唯一原因是: 在暂时忽视对象的实际类型之后,使用对象的全部功能。
  大多数情况并不需要将Worker对象转换成Manager对象, 两个类的对象都能够正确地调用getSalary()方法, 这是因为实现多态性的动态绑定机制能够自动地找到相应的方法。
  只有在使用Manager类中特有的变量或方法时才需要进行类型转换,如该类的getBonus()方法,因为父类Worker肯定没有。。。

参考

  • 维基百科
  • Cay S. Horstmann. Java 核心技术卷一 [M]. 机械工业出版社,2016
0%