【JavaCore · I】第五章_继承

381 阅读10分钟

5.1_类、超类和子类

5.1.1_定义子类

5.1.2_覆盖方法

超类中的有些方法对子类 Manager 并不一定适用。具体来说, Manager 类中的getSalary方法应该返回薪水和奖金的总和。

public double getSalary() { 
    return salary + bonus; // won't work 
}

注:在 Manager 类的 getSalary() 方法中并不能够直接地访问 salary 域。只有 Employee 类的方法才能够访问 private 部分

public double getSalary() {
    double baseSalary = getSalary();// still won't work 
    return baseSalary + bonus; 
} 

问题:因为 Manager 类 也有一个 getSalary() 方法(就是正在实现的这个方法),所以这条语句将会导致无限次地调用 自己,直到整个程序崩溃为止。

解决方案:我们希望调用超类 Employee 中的 getSalary() 方法, 而不是当前类的这个方法。可使用特定的关键字 super。

public double getSalary() { 
    double baseSalary = super.getSalary();  // Good
    return baseSalary + bonus; 
} 

super 和 this 类似吗?

super 不是一个对象的引用,不能将 super 赋给另一个对象变量, 它只是一个指示编译器调用超类方法的特殊关键字。

5.1.3_子类构造器

public Manager(String name, double salary, int year, int month, int day) { 
  super(name, salary, year, month, day); 
  bonus = 0; 
} 
  • 使用super调用构造器的语句必须是子类构造器的第一条语句。
  • 如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数) 的构造器。
  • 如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则报错。

1. this 与 super 的作用

this

  • 引用隐式参数
  • 是调用该类其他的构造器

super

  • 调用超类的方法
  • 调用超类的构造器

调用构造器的语句只能作为另一个构造器的第一条语句出现。

2. 多态前瞻

  • 多态(polymorphism)。 :一个对象变量可以指示多种实际类型的现象。
  • 动态绑定(dynamic binding):在运行时能够自动地选择调用哪个方法的现象称。
Manager boss = new Manager("Carl Cracker", 800001987, 12, 15); 

boss.setBonus(5000); 
Employee[] staff = new Employee[3]; 

staff[0] = boss; 
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1 ); 
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15); 

for (Employee e : staff) 
    System.out.println(e.getName() + " " + e.getSalary()); 
Carl Cracker 85000.0 
Harry Hacker 50000.0 
Tommy Tester 40000.0 
  • 当 e 引用 Employee 对象时,e.getSalary()调用的是 Employee 类中的 getSalary 方法;
  • 当 e 引用 Manager 对象时,e.getSalary()调用的是 Manager类中的 getSalary 方法。
  • 虚拟机知道 e 实际引用的对象类型,因此能够正确地调用相应的方法。

5.1.4_继承层次

Java 不支持多重继承。

5.1.5_多态

在Java中,对象变量是多态的。先看一段例子:

Manager boss = new Manager(...);
Employee[] staff = new Employee[3]; 
staff[0] = boss; 

变量stafflO]boss引用同一个对象。但编译器将staff[0]看成 Employee对象。请判断以下调用的合法性。

boss.setBouns(3000);     // OK
staff[0].setBouns(3000); // Bad

这是因为setBounds()Manager的方法。再看一段代码

Magager m = staff[1];   // Bad

m有可能引用一个不是经理的 Employee 对象导致调用setBounds()时出现Runtime错误,第一个例子就说明了问题。

5.1.6_理解方法调用

1. 方法的覆盖

  • 覆盖的定义:子类定义了一个与超类签名相同的方法。
    • 签名:方法名、参数列表。
  • 覆盖的条件:要保证返回值类型的兼容性。
    • 兼容:允许覆盖方法的类型为原返回类型的子类型。
    public Employee getBuddy() {...}
    public Manager getBuddy() {...} // OK
    
    • getBuddy()具有可协变的返回值类型

警告:在覆盖时,子类不能把超类的方法的可见性降低。

2. 静态绑定

3. 动态绑定

5.1.7_阻止继承:final 类和方法

1. final 类

不允许扩展的类成为 final 类。

  • final 类的所有方法被声明为final方法。
  • final 类的域并不绝对是声明为final

内联:如果一个方法没有被覆盖、很简短,编译器则对其进行优化处理。

  • 例如:e.getName() -> e.name

5.1.8_强制类型转换

超类对象不能赋值给子类变量。反之,则可。

Manager boss = (Manager) staff[1];  // Error:ClassCastException异常

在转换之前可做一下判断:

if(staff[1] instanceof Manager)
  Manager boss = (Manager) staff[1];

总结:

  • 只有在继承层次内才可类型转换。
  • 在超类转换成子类之前,应使用instanceof做判断。

5.1.9_抽象类

扩展抽象类可以有两种选择。

  • 在抽象类中定义部分抽象类方法或不定义抽象类方法,这样子类也必须标记为抽象类;
  • 抽象类定义定义全部的抽象方法,子类则为非抽象类。

综述:

  • 抽象类可不定义抽象方法。
  • 抽象类不能被实例化。

5.1.9_受保护访问

  • private:仅对本类可见。
  • protected:对本包和所有子类可见。
  • 默认:对本包可见。
  • public:对所有类可见。

5.2_Object:所有类的超类

  • 在子类中定义equals方法时,首先应该调用超类的equals方法。

5.2.2_相等测试与继承

boolean equals(Object otherObject):比较两个对象是否相等,

  • 如果两个对象指向同一块存储区域,方法返回 true;
  • 否则方法返回 false。
  • 在自定义的类中,应该覆盖这个方法。

Java规范要求equals(Object other)方法应具有以下特性:

  • 自反性:对于任何非空引用 x, x.equals(?0应该返回 truec 2 )
  • 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true, x.equals(y) 也应该返 回 true
  • 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返N true, y.equals(z)返回 true, x.equals(z) 也应该返回 true。
  • 一致性: 如果 x 和 y 引用的对象没有发生变化,反复调用 x.eqimIS(y) 应该返回同样 的结果。
  • 对于任意非空引用 x, x.equals(null) 应该返回 false,

java.util.Arrays

static Boolean equals(type[]a, type[] b)

  • 如果两个数组长度相同, 并且在对应的位置上数据元素也均相同, 将返回 true

static boolean equals(Object a, Object b)

  • 如果 a 和 b 都为 null, 返回 true ;
  • 如果只有其中之一为 null, 则返回 false ;
  • 否则返回 a.equals(b)0

5.2.3_hashCode 方法

散列码:是由对象导出的一个整型值。

String s = "Ok"; 
StringBuilder sb = new StringBuilder(s); 
String t = new String("Ok"); 
StringBuilder tb = new StringBuilder(t);
  • 字符串st拥有相同的散列码, 这是因为字符串的散列码是由内容导出的。
  • 而字符串缓冲sbtb却有着不同的散列码,这是因为在StringBuffer类的散列码是由Object类的默认 hashCode 方法导出的对象存储地址。

需要组合多个散列值时,可以调用Object.hash并提供多个参数。

  • 比如:Objects,hash(name, salary, hireDay);

5.2.4_toString()方法

Object类定义了toString方法,用来打印输出对象所属的类名和散列码。

  • System.out.println(System.out)->java.io.PrintStream@2f6684

打印一维数组:String s = Arrays.toString(luckyNumbers);
打印多维数组:Arrays.deepToString

5.3_泛型数组列表ArrayList

1. capacity与size的区别

  • capacity:拥有保存元素的容量潜力。
  • size:数组列表中包含的实际元素数目。

2. trimToSize

trimToSize()方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。

  • 一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块,所

3. void ensureCapacity(int capacity)

这个函数可以对低层数组扩容,在适当的时机,利用这个函数,将会使我们写出来的程序性能得到提升。

public static void main(String[] args) {
    final int N=1000000;
    Object obj=new Object();
    ArrayList list1=new ArrayList();
    long start=System.currentTimeMillis();
    for(int i=0;i<N;i++){
      list1.add(obj);
    }
    System.out.println(System.currentTimeMillis()-start);
    
    ArrayList list2=new ArrayList();
    long start2=System.currentTimeMillis();
    list2.ensureCapacity(N);  // 显式的对低层数组进行扩容
    for(int i=0;i<N;i++){
      list2.add(obj);
    }
    System.out.println(System.currentTimeMillis()-start2);
}

第2段的效率显然要比第1段高很多

  • 原因是第一段如果没有一次性扩到想要的最大容量的话,它就会在添加元素的过程中,一点一点的进行扩容。

4. ArrayList快速转X[]数组

X[] a = new [list.size()]; 
list.toArray(a)

5.3.2_类型化与原始数组列表的兼容性

5.4_对象包装器与自动装箱

  • 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
  • 对象包装器类还是final, 因此不能定义它们的子类。

由于每个值分别包装在对象中, 所以 ArrayList<lnteger> 的效率远远低于 int[ ] 数 组。 因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。

自动装箱

执行list.add(3)时,其实是编译器隐式执行了list.add(Integer.value0f(3))

自动拆箱

当将一个Integer对象赋给一个int值时, 将会自动地拆箱

int n = list.get(i); ->
int n = list.get(i).intValue(); 

如下代码,编译器将自动地插人一条对象拆箱的指令, 然后进行自增计算,最后再将结果装箱

Integer n = 3; 
n++; 

包装类对象的==

Integer a = 1000; 
Integer b = 1000; 
print(a = b);   // false

自动装箱规范要求 boolean、byte、char 127, 介于 -128 ~ 127 之间的 short 和 int 类型数值被包装到固定的对象中。

注意点

1. 包装器类null值的引用

由于包装器类引用可以为 null, 所以自动装箱有可能会抛出一个 NullPointerException 异常:

Integer n = null; 
System.out.println(2 * n); // Throws NullPointerException

2. 使用不同类型的包装类进行运算

如果在一个条件表达式中混合使用 Integer 和 Double 类型, Integer 值就会拆箱, 提升为 double, 再装箱为 Double。

Integer n = 1 ; 
Double x = 2.0; 
System.out.println(true ? n : x); // Prints 1.0 

装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码 时, 插人必要的方法调用。虚拟机只是执行这些字节码。

3. 包装类对象的不可变性

public static void triple(Integer x) {
  x = 3*x;  // won't work 
}

Integer对象是不可变的:包含在包装器中的内容不会改变。

5.5_参数数量可变的方法

Java 5 以前,每个Java方法都有固定数量的参数。

5.6_枚举类

public enum Size { 
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
    
    private String abbreviation;
    private Size(String abbreviation) { 
        this.abbreviation = abbreviation; 
    } 
    public String getAbbreviation() { return abbreviation; }
}

1. 如何比较枚举类型的值

不需要调用equals, 而直接使用==

2. Enum 类的地位

所有的枚举类型都是Enum类的子类。

Enum类的静态方法

1. Enum 类的toString方法

该方法返回枚举常量的名称。

  • Size.SMALL.toString()将返回字符串"SMALL"

2. Enum 类的valueOf方法

toString 的逆方法是静态方法 valueOf。

  • Size s = Enum.valueOf(Size.class, "SMALL")将 s 设置成 Size.SMALL。

3. values

返回一个包含全部枚举值的数组。

  • Size[] values = Size.values(); 返回包含元素`Size.SMALL ... Size.EXTRA_LARGE 的数组。

4. ordinal

返冋 enum 声明中枚举常量的位置, 位置从 0 开始计数。

  • Size.MEDIUM.ordinal() 返回 1。

5. int compareTo(E other)

  • 如果枚举常量出现在 Other 之前,则返回一个负值;
  • 如果 this=other,则返回 0;
  • 否则,返回正值。
  • 枚举常量的出现次序在 enum 声明中给出。

5.7_反射

5.8_继承的设计技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护的域
  3. 使用继承实现“ is-a” 关系
  4. 除非所有继承的方法都有意义, 否则不要使用继承
  5. 在覆盖方法时, 不要改变预期的行为
  6. 使用多态, 而非类型信息
  • 无论什么时候,对于下面这种形式的代码
    if (x is of type1) 
      action1(x) 
    else if (x is of type2) 
      action2(x); 
    
  • 应为这个相同概念的action1/2定义一个方法,并将其放置在两个类的超类或接口中;调用x.action()以便使用多态性提供的动态分派机制执行相应的动作。
  • 使用多态方法或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
  1. 不要过多地使用反射
    • 只有在运行时才发现错误并导致异常。