0%

Java中的继承

前言

本文主要针对一些比较模糊的问题进行总结:

  1. 引用是父类,实例是子类
  2. super和this

父类和子类的引用情况

类型转换

  父类的引用可以指向子类的对象(向上转型),子类引用不能指向父类对象。这个很好理解,子类实例化会执行父类的构造器,此时父类的属性已经在内存中了,使用父类的引用可以访问到父类的属性信息;但是父类实例化不会执行子类的构造器,因此内存中不会有子类的属性信息,使用子类的引用无法访问到子类的成员信息。这里有个口诀对于对象的类型:编译看左边,运行看右边,静态全看左

1
2
3
Father f = new Son(); // ok
Son s = new Father(); // 编译不通过
Son ss = (Son)new Father(); // 编译通过,运行时抛出ClassCastException(类型转换异常)

  这里比较特殊的是,父类引用指向子类的对象,该引用只能调用父类定义的成员变量和成员方法(因为编译看左边),而在运行中,调用同名的成员变量会调用父类的成员变量,如果调用同名的成员方法(即重写的方法),则会调用子类的方法。这里很奇怪,既然运行看右边,为什么我访问的不是子类对象中同名的成员变量呢?

  这和Java的运行机制有关,Java中的成员变量、静态成员方法、private以及final方法都是静态绑定的,所谓静态绑定是指在编译期绑定,这时候引用f已经和父类的成员变量绑定了;而普通的成员方法是动态绑定的,也就是运行时绑定,此时引用f在调用方法的时候是访问的是子类方法表的地址。也就是说,Java的多态是指在运行时多态,只是指普通成员方法的多态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Father {
public String name = "父亲";

public String getName() {
return name;
}
}

public class Son extends Father{

public String name = "儿子";
public String name2 = "儿子小名"

@Override
public String getName() {
return name;
}
}

public static void main(String[] args) {
Father f = new Son();
System.out.println(f.name); // 父亲
System.out.println(f.getName()); //儿子
}

  这里补充一下:虽然引用f无法直接调用子类的成员变量和父类没有的成员方法,但是在运行时,这些属性仍然是开辟了空间的,但是无法访问(主要是因为编译不过)。这里给出一个验证,我们知道反射是典型的在运行时可以操作对象的方式,这里通过反射在运行时强行去找f的成员变量,可以看到实际上f在运行时是一个子类的实例对象,通过反射可以访问到父类没有的变量或者方法(代码只写了变量),但是因为静态绑定了,所以直接通过f.name显示的是父亲的name

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
Father f = new Son();
try {
Field field = f.getClass().getField("name");
Field field2 = f.getClass().getField("name2");
System.out.println(f.name); // 父亲
System.out.println(f.getName()); // 儿子
String name = (String)field.get(f); // 儿子
String name2 = (String)field2.get(f); // 儿子小名
} catch (Exception e) {
e.printStackTrace();
}
}

Super和This

  大家都知道两点:

  1. 子类初始化会调用父类的构造器
  2. 子类中使用super可以调用父类的变量和方法(除了private修饰的)

  但是这里要强调一下,子类实例化不会创建父类的实例,调用父类的构造器只是在内存中开辟了父类属性的空间,也就是说和this不同,super并不指代一个对象。如果调用super.hashcode()就会发现结果和this.hashcode()的值一样,因为根本就没有父类对象,调用父类的hashcode和子类的hashcode得到的都是子类的hashcode,从这可以推导得出,就算是在父类的构造器中调用this.hashcode()得到的也是子类的hashcode

  总结一下就是:super只是一个标志,不指向任何对象,它能调用父类的构造器和方法,并不是说指向了父类对象,因为根本没有父类对象被创建。

  另外,hashcode方法是运行时调用的,因此上述的结论和引用是不是子类类型并无关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 重写Son和Father
public class Father {
public Father() {
System.out.println("父类this.hashcode(): " + this.hashCode());
}
}

public class Son extends Father{
public Son() {
System.out.println("子类this.hashcode(): " + this.hashCode());
System.out.println("子类super.hashcode(): " + super.hashCode());
}
}

// 验证
public static void main(String[] args) {
Son s = new Son();
System.out.println("===========");
Father f = new Son();
}

// 结果
父类this.hashcode(): 1554874502
子类this.hashcode(): 1554874502
子类super.hashcode(): 1554874502
===========
父类this.hashcode(): 1846274136
子类this.hashcode(): 1846274136
子类super.hashcode(): 1846274136

总结

  Java继承主要就是要处理两个问题:一、什么东西是静态绑定的;二、子类实例化,会不会实例化一个父类对象?懂了这两个问题,所有问题都能推导出来。