本篇文章的主要内容为Java的继承。

主要内容包括:

  • 三个概念:类、超类、子类,这是继承与被继承的主体。
  • Object:所有类的超类。
  • 泛型数组列表:关于泛型这个类的详细内容后面再总结,放在这里是因为它的参数是一个类型,实现了动态数组。
  • 继承设计技巧:《Java核心技术卷》中的忠告。

类、超类、子类

关键字extends表示继承。格式为:

1
2
3
class Manager extands Employee{
添加方法和域
}

超类就是已存在的类,这里是Employee。子类就是新类,这里是Manager。

通用的方法应该放进超类,特殊用途的方法应该放进子类。

override & super

覆盖(override):超类中的方法对子类不适用的时候,就应该适用覆盖。(比如Employee的getSalary() 就是月薪,而Manager的getSalary() 就是月薪+奖金)。

super关键字调用超类方法。比如我们要覆盖Employee的getSalary() ,实现Manager的getSalary() 。那么我们首先要获得月薪,月薪这个概念定义在超类中,就可以适用super关键字来先去调用超类方法,来找到月薪这个域。

综合覆盖和super,如果覆盖Employee的getSalary() ,实现Manager的getSalary() ,可以写为:

1
2
3
4
5
6
7
//覆盖override
public double getSalary(){
//baseSalary是定义在超类中的域,通过super关键字调用超类方法获得
double baseSalary = super.getSalary();
//bonus是在子类中新增的域
return baseSalary + bonus;
}

注意 :子类中可以新增域、方法、或者覆盖超类方法,但是绝对不能删除继承的任何域和方法。

super关键字调用超类构造器。

子类有些在超类中已有的域,可以通过super关键字调用超类构造器。(这些域在超类中,子类是不能直接访问的,很麻烦)

调用语法 :

1
2
3
4
5
6
// 子类构造器
public Manager(String n,double s,int year){
//调用超类构造器,注意必须放在子类构造器的第一句
super(n,s,year);
bonus = 0;
}

对比:

this关键字的作用:1.引用隐式参数 2.调用该类的其他构造器

super关键字的作用:1.调用超类方法 2.调用超类构造器

多态

什么时候应该继承? 符合“is-a”原则的时候。

多态 : 这里指的是对象变量是多态的。即一个对象变量既可以引用一个类的对象,也可以引用这个类的子类的对象。

例如

1
2
3
4
5
6
7
Employee e
e = new Employee(...); //ok
e = new Manager(...); // ok too

//注意
// 1. 不能将超类的引用赋予子类变量
Manager m = new Employee(...); //ERROR ! 因为不是所有的雇员都是经理,如果调用经理的特别方法,可能会报错。

动态绑定

下面是调用对象的执行过程的流程:

  1. 编译器查看对象的声明类型和方法名。

  2. 查看调用方法时的参数类型,进行重载解析。

  3. 如果方法是private、static、final类型的,那么就是静态绑定。如果调用的方法依赖隐式参数的实际类型,在运行的时候绑定,就是动态绑定

    就是说,Employee.getSalary() 和 Manager.getSalary() ,就是在执行时动态绑定。

  4. 动态绑定调用方法的时候,虚拟机一定是调用与隐式参数所引用对象的实际类型最合适的那个类的方法。

    就是说, Manager.getSalary(),会先去 Manager类里面找getSalary()方法,要是没有再去Employee类(Manager的超类)里面找getSalary()方法,再没有再去Employee的超类里找…….

final类和方法(阻止继承)

不允许扩展的类就是final类。

1
2
3
final class Executive extends Manager{
...
}

同样,也可以把类里面的方法声明为final,这样子类就不能覆盖这个方法。(final类中的所有方法自动成为final方法)

使用final的场景 : 确保它们不会在子类中改变语义。

抽象类和方法(abstract关键字)

抽象类不能实例化。

抽象方法充当占位角色,它的具体实现在子类中。

包含一个及以上抽象方法的类必须被声明为抽象类。

即 : 一个抽象类(包含n >= 1个抽象方法)—-扩展—>子类(这个子类可以实现抽象方法,也可以不实现、继续扩展。)

使用abstract的场景 : 某类是一个高度抽象的类(比如Person类),会扩展出很多子类(比如Student类和Employee类),子类中会实现一些方法(比如getDes()描述这个人的职业)而这个类无法描述(Person类没有职业),就可以在Person类定义 public abstract String getDes(),表示会在子类中实现。

普通类也可以被继承,普通方法也可以被重写,为什么非得用抽象类和抽象方法呢?

使用抽象类相当于多了一重编译器的校验,使用抽象类实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的,但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。

访问修饰符

  1. private : 仅对本类可见。常用于标记类中的域。
  2. public : 对所有类可见。常用于标记类中的方法。
  3. protect : 对本包和所有子类可见。谨慎使用,不建议标记域(子类可以随便派生,就可以在子类中修改这个域,违背了数据封装原则)。
  4. 默认 : 对本包可见。不受欢迎的写法。

Object:所有类的超类

Object是所有类的超类,因此可以使用Object类型的变量引用任何类型的对象。

Object obj = new Manager(...)

当然,这里的obj只能作为值的持有者,想要操作还得转换。

Object有很多方法很实用,下面来看看。

equals()

Object类中的equals()方法用于检测一个对象的引用是否等于另一个对象引用。

实际上,两个对象是否相等的定义,需要讨论。

如果使用equals方法成立,那么他们当然一定相等。

有的时候不需要这么严格,只需要比较两个对象的状态是否相等就可以了。

getClass()

接着上面的写,判断两个对象相等,只需要比较两个对象的部分状态(比如ID)是否相等。

getClass()方法用于返回一个对象所属类型。

如果同一个人类型了 ,再比较其域的状态就可以判断相等

1
2
//B是被比较的那个对象
return ID.equals(B.ID)

相等测试与继承

上面的方法比较两个对象是否相等,建立在两个对象属于一个类。

如果两个对象不是一个类呢(是子类)?

A.equals(B) ?

下面是一个完美的比较方法:

  1. 显示参数命名为B,后面把它转化成一个叫做b的变量。

  2. 看A(或者this)是否引用同一个对象。

    if(this == B) return true;

  3. 检测B是否是null

    if(B == null) return false;

  4. 比较this和B是否是一个类

    if(getClass != B.getClass()) return false;

  5. 将B转化成 相应类的类型变量

    className b = (className) B

  6. 对域进行比较。==比较基本域,equals比较对象域。

    return field1 == b.field1 && field2 == b.field2 ....

泛型数组列表(ArrayList)

ArrayList :一个采用类型参数的泛型类。

声明方式 :

ArrayList<AAA> aaa = new ArrayList<>();

目的:实现动态数组,解决Java代码运行时动态更改数组的问题。

泛型数组列表的方法

  1. aaa.add(i,xxx)

  2. aaa.size() 。返回实际元素数目。若返回为n, 使用aaa.add(n,xxx)就是在尾部添加元素。

  3. aaa.get(i) add.set(i,xxx) 。访问第i个元素。

  4. aaa.remove(i)

  5. for (AAA a : aaa){

    ​ 对a做点什么

    } // 遍历

请注意:泛型数组列表有这么多api,所以很方便使用。但是由于其类型参数不允许是基本类型(是类型参数),所以效率比较低。应该在小型数组中使用。

参数数量可变的方法

参数数量不一定非要是固定的,通过 “ … ”方法可以实现可变参数

例如Java源码中的printf方法,如下定义:

1
2
3
4
5
public class PrintStream{
public PrintStream printf(String fmt,Object... args){
return format(fmt,args);
}
}

省略号 … 是Java代码的一部分,它表明这个方法可以接受任意数量的对象。(除fmt参数之外)

再举一个例子 : 找出任意个数值的最大值 :

1
2
3
4
5
6
public static double max ( double... values){
double largest = Double.NEGATIVE_INFINITY;
for(double v : values) {
if(v > largest) largest = v;
}
}

通过上面两个例子可以看出 :

对于方法实现者来说 ,XXX… 类型参数和 XXX[]完全一样。

继承的设计技巧

这里的建议来自《Java核心技术卷》。

  1. 将公共操作和域放在超类。
  2. 不要使用受保护的域。
  3. 使用继承实现“is-a”关系(他应该是想说,除非是is-a关系,否则不要使用继承)
  4. 除非所有继承的方法都有意义,否则不要使用继承。
  5. 在覆盖(override)方法时,不要改变预取的行为。