第5章 继承
1. 父类和子类
扩展父类时,仅需要指出子类与父类的不同之处,也可以通过重写(动态(单)分派:根据调用者的实际类型决定调用的方法)覆盖父类方法。
子类可以继承父类的私有域,但是只能通过父类方法访问此私有域,若本身覆盖了父类的访问方法,则通过关键字 super 表示调用的是父类方法。 super 两个用途:1、调用父类方法 2、调用父类构造器
public class father { private int age = 40; String name = "jj"; public int getAge() { return age; } } class son extends father{ public int getAge() { return age; // error 字段 father.age 不可视 // return super.getAge(); } public String getName(){ return name; } }
在子类中可以增加域,增加/覆盖方法,但绝对不能删除继承的域和方法。
2. 多态
静态类型为父类的对象引用既可以引用本身的对象,也可以引用子类对象。反之不成立。
class Father{}; class Son extends Father{}; Father father = new Son(); // Yes Son son = new Father(); // No
(JVM)在确定方法调用的版本时:
非虚方法(invokespecial, invokestatic指令调用的方法,例如静态方法,私有方法,父类方法,实例初始化方法 以及 final修饰的方法)编译期可知,运行期不可变,所以编译器确定类型,在类加载的解析阶段就会把符号引用转化为直接引用,称为静态调用(解析)。
虚方法(invokevirtual等指令)在运行期才会确定调用方法的版本(根据接受者的实际类型)此时称为(分派)。
分派分为静态分派和动态分派:
- 静态分派:依据接受者和方法参数静态类型确定调用方法的版本,属于多分派。静态分派发生在编译期
- 动态分派:依据接受者实际类型确定调用方法的版本,属于单分派。动态分派发生在执行期
invokevirtual指令执行过程:
- 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
- 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。
3. 方法表
每次调用方法都要进行搜索,为此,虚拟机预先为每个类创建了方法表,列出了方法的签名和实际调用的方法。
4. 阻止继承
不允许扩展的类称为 final 类,类中方法声明为 final 表示子类不能覆盖这个方法。若将一个类声明为 final ,类中方法自动成为 final 方法, 但不包括域。
5. 抽象类
包含一个或多个抽象方法的类本身必须声明是抽象的,当然,抽象类也可以包含具体的方法和具体的数据。子类继承抽象类后,除非定义全部的抽象方法,否则,依然需要声明为抽象类。
类即使不包含抽象方法,也可以声明为抽象类。
抽象类不能实例化。但可以定义一个抽象类的对象引用,引用非抽象的子类实例。
public abstract class person{ public int age; public abstract int getAge(){} } public student extends person{ public int getAge(){ return age; } } person p = new person(); // error person p = new student(); // true
6. 访问修饰符
- private 仅对本类可见
- protect 对本包和所有子类可见
- public 对所有类可见
- default (默认)对包可见,不需要修饰符
7. equals
Object 类的equals()方法判断两个对象引用是否指向同一个对象。
public boolean equals (Object x){ return this == x; } // java 中 == 判断两个引用是否指向同一对象
8. hash code
hashCode 方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
此外,equals() 与 hashCode() /*必须结果统一(有误)*/,如果 x.equals(y) 返回 true, 则 x.hashCode() 和 y.hashCode() 必须相同。 (2018-03-30)
- 如果两个对象equals比较返回true,则两个对象的hashCode必须相同
- 如果equals返回false,两个对象的hashCode可以相同,例如 HashMap。
9. toString
Object类定义了 toString 方法, 用来打印输出对象所属的类名和散列码。
10. 对象包装器与自动装箱
Integer、Long、Float、Double、Short、Byte、Character、Void、Boolean
包装器类是不可变的,同时,包装器类修饰关键字为final。
ArrayList<Integer> list not ArrayList<int> list ,尖括号中的参数类型不允许是基本类型。
自动装箱:list.add(3); ==> list.add(Integer.valueOf(3));
自动拆箱:int a = list.get(i); ==> int a = list.get(i).intValue();
Integer a = 1000; Integer b = 1000; System.out.printlen(a==b); // return false Integer a = 100; Integer b = 100; System.out.printlen(a==b); // return true
介于 -128~127 之间的 short 和 int 被包装到固定的对象中,即指向同一个对象。
11. Class类
在程序运行期间,java运行时系统始终为所有对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。保存这些信息的类称为Class。
12. 继承的技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 除非所有继承的方法都有意义,否则不要使用继承
13. 多继承
Java不允许类的多继承,是为了避免菱形继承的问题。设想两个类定义了签名(方法名和入参及入参顺序)相同的方法,那么同时继承这两个类,调用这个方法的时候,编译期犯迷糊了,到底想要哪个父类的方法呢。在C++中,使用虚继承解决这个问题,Java秉持简单的原则,放弃了类的多继承。
那Java中有多继承吗?
有的,Java中接口是可以多继承的。
那会产生菱形问题吗?
会也不会,在Java8之前,接口只能定义方法,具体的实现交给实现类完成,如果多个接口定义了签名相同的方法,没有关系,反正只是定义而已,实现类只需要实现一次即可。而在Java8中,接口可以定义default方法,即在接口中定义方法实现。这种情况就跟类的多继承一样,会出现菱形继承问题了。
我们引申一下,为什么Java8需要引入default方法机制呢?
试想一下,一个接口有千万的实现类,或者打成jar包,提供给别人使用。突然有一天,接口添加了一个新方法,所有的实现类都需要实现这个方法,这将是灾难性的。而default方法正如其名,是一个默认方法,接口添加后,实现类可以不实现。不实现则使用接口的默认实现。