[java复习] 继承

概要

这一章的复习主要涵盖 Object 的源码解析继承的性质继承初始化。这里整理的都是个人觉得比较重要重新认真分析的东西,不一定全部为基础,如果有不准确的地方,还望大佬从评论区指出,感激不尽。

文章中的所有的自定义的测试都在自己的 java-review 仓库中,本文对应的链接:

根类 Object

  • java 中采用的是单继承,其中 java.lang.Object 是所有类的根类,包括数组
  • 自定义一个类的时候,其实已经继承 Object 这个类,拥有 Object 类内部的默认权限以上的所有方法

registerNatives()

1
2
3
4
5
6
7
8
// java 12 source

private static native void registerNatives();

static {
registerNatives();
}
}
  • java 底层由 c++ 实现,registerNatives() 的作用就是将 java 类中方法注册为 native 方法,是 java 类方法底层方法 建立映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// openjdk 6

static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}

getClass()

1
2
3
4
// java 12 source

/* Return the runtime class of this Object */
public final native Class<?> getClass();
  • Object # getClass 方法使用了 final 进行锁定,不能够被子类重写 @Override
  • 返回这个类对应的 Class 对象实例的引用,类的所有的实例返回的都是同一个 Class 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// test

private static void classOfObject() {
Object obj1 = new Object();
Object obj2 = new Object();
Class clazz1 = obj1.getClass();
Class clazz2 = obj2.getClass();

System.out.println("obj1 == obj2: " + (obj1 == obj2));
System.out.println("lazz1 == clazz2: " + (clazz1 == clazz2));
System.out.println("clazz1.equals(clazz2): " + clazz1.equals(clazz2));

/* Output:

obj1 == obj2: false
lazz1 == clazz2: true
clazz1.equals(clazz2): true
*///:~
}

hashCode()

1
2
3
4
// java 12 source

/* Return a hash value for the Object. */
public native int hashCode();
  • Object # hashCode 同样是的 native 方法,用来获取对象的 Hash 值,可以进行重写

equals(Object obj)

1
2
3
4
5
6
// java 12 source

/* Indicates whether some other object is "equal to" this one. */
public boolean equals(Object obj) {
return (this == obj);
}

Object # equals 方法的作用是判断两个对象是否相等,正确的 equals()hashCode() 的关系应该为

  • 两个不同的对象的 hashCode() 的值相同,但 equals() 两个对象必须不同
  • 两个对象的 hashCode() 不同 equals() 比较两个对象

因此,像 Hash 容器就先进行比较两个对象的 Hash 值,如果相同,再通过 equals() 来比较两个对象

正确的 equals()方法必须满足下列 5 个条件

  1. 自反性。对任意 xx.equals(x) 一定返回 true
  2. 对称性。对任意 xy,如果 y.equals(x) 返回 true,则 x.equals(y) 也返回 true.
  3. 传递性。对任意 x、y、z,如果有 x.equals(y) 返回 turey.equals(z) 返回true,则 x.equals(z) 一定返回 true
  4. 一致性。对任意 xy,如果对象中用于等价比较的信息没有改变,那么无论调用 x.equals(y) 多少次,返回的结果应该保持一致,要么一直是 true,要么一直是 false.
  5. 对任何不是 null 的,x.equals(null) 一定返回 false.

clone()

1
2
3
4
// java 12 source

/* Creates and returns a copy of this object. */
protected native Object clone() throws CloneNotSupportedException;
  • Object # clone 方法用来返回一个当前的拷贝,是一个 protect native 方法,所以 Object 对象实例以及没有重写 Object # clone 的类,都是没有权限直接调用 clone() 方法的

测试直接调用 Object # clone()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// test

private static void SuperCloneTest() {
SuperClone obj = new SuperClone();
try {
System.out.println(obj == obj.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}

/*

java.lang.CloneNotSupportedException: com.example.review._extends._Object$SuperClone
at java.base/java.lang.Object.clone(Native Method)
at com.example.review._extends._Object$SuperClone.clone(_Object.java:93)
at com.example.review._extends._Object.SuperCloneTest(_Object.java:65)
at com.example.review._extends._Object.main(_Object.java:87)
*///:~
}

Object # clone 内部并没有完整实现,需要子类重写这个方法才能进行使用,直接调用会抛出 java.lang.CloneNotSupportedException 异常

toString()

1
2
3
4
5
6
// java 12 source

/* Returns a string representation of the object. */
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • Object # toString() 默认返回 ClassName + @ + 十六进制的 Hash 值

String 环境中的对象实例,会直接调用调用对象实例的 toString() 方法拼接字符串,这一部分在前面写的 字符串 一文中有详细的介绍

wait()

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
30
31
// java 12 source

/* Causes the current thread to wait until it is awakened, typically by being notified or interrupted */
public final void wait() throws InterruptedException {
wait(0L);
}

/* Causes the current thread to wait until it is awakened, typically
* by being notified or interrupted, or until a
* certain amount of real time has elapsed. */
public final native void wait(long timeoutMillis) throws InterruptedException;

/* Causes the current thread to wait until it is awakened, typically
* by being notified or interrupted or until a
* certain amount of real time has elapsed. */
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
if (timeoutMillis < 0) {
throw new IllegalArgumentException("timeoutMillis value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}

if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
timeoutMillis++;
}

wait(timeoutMillis);
}
  1. Object # wait 会释放掉当前线程持有的锁所以 synchronized 方法在 wait 状态是可以为被其他线程调用的,而 sleep()yield() 都不会释放锁,详细的部分会放到线程一部分里面来讲 再留一个坑

  2. Object # wait 方法的作用是让调用当前对象方法的线程进入等待状态,都为 final 不能被修改

    • wait(long timeoutMillis) 方法
    • wait(long timeoutMillis, int nanos) 方法指定超时时间为 timeoutMillis 毫秒 + nanos 毫微秒 (0 <= nanos <= 999999)

毫微秒 为单位
$$ TimeOut = 1000000 * timeoutMillis + nanos $$

notify()

1
2
3
4
5
6
7
// java 12 source

/* Wakes up a single thread that is waiting on this object's monitor. */
public final native void notify();

/* Wakes up all threads that are waiting on this object's monitor. */
public final native void notifyAll();
  • Object # notify 方法的作用就是将通过调用 obj.wait() 进入 wait 状态的线程唤醒
    • notify() 只会随机唤醒一个线程
    • notifyAll() 会唤醒在这个对象上的等待线程中的一个

测试不在同步环境中中使用 wait() 和 notify()

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
30
31
32
33
// test

private static void notifyTest() {
_Object obj = new _Object();
log("notify test", '-');
obj.notify();
}

/*
测试 wait() 方法不在同步中使用
wait() 方法必须在 synchronized 环境中使用,synchronized 关键字 或者 synchronized(obj) 同步块

*/
private static void waitTest() {
_Object obj = new _Object();
log("wait test", '-');
try {
obj.wait(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log("wait() end...");

/*

---------- wait test ----------
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.base/java.lang.Object.wait(Native Method)
at com.example.review._extends._Object.waitTest(_Object.java:87)
at com.example.review._extends._Object.waitAndNotifyTest(_Object.java:148)
at com.example.review._extends._Object.main(_Object.java:159)
*///:~
}

不管是 notify() 还是 wait() 方法都必须在同步环境中使用

  • synchronized
  • synchronized(obj) {}

否则就会抛出 java.lang.IllegalMonitorStateException 异常

测试同步环境中使用 wait() 和 notify()

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// test

private static void waitSyncTest() {
_Object obj = new _Object();
log("wait Sync test", '>', 1);
new Thread() {
@Override
public void run() {
try {
for (int i = 1; i <= 5; i++) {
sleep(1000);
log("sleep " + i + " s");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
log("another thread -----> notify obj thread", -1);
synchronized (obj) {
obj.notify();
}
}
}.start();

synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log("quit wait status", '<', -1);

/*

>>>>>>>>>> wait Sync test >>>>>>>>>>

sleep 1 s
sleep 2 s
sleep 3 s
sleep 4 s
sleep 5 s

another thread -----> notify obj thread
*///:~
}
  1. obj # wait 方法让主线程进入 wait 状态,
  2. 主线程进入 wait状态前,new 一个新的线程并启动,**5** 秒后调用 obj # notify 方法,唤醒在 obj 对象上等待的主线程

finalize() [已废弃]

1
2
3
4
5
// java 12 source

/* @deprecated The finalization mechanism is inherently problematic */
@Deprecated(since="9")
protected void finalize() throws Throwable { }
  • Object # finalize 方法在方法要被回收的时候被执行

这里使用了 WeakReference 弱引用来进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// test

private static void finalizeTest() {
FinalizeTest finalizeTest = new FinalizeTest();
WeakReference<FinalizeTest> weaks = new WeakReference<>(finalizeTest);
System.out.println("---------- ref is not null ---------- ");
System.gc();
System.out.println("---------- ref is null ---------- ");
finalizeTest = null;
System.gc();

/* Output:

---------- ref is not null ----------
---------- ref is null ----------
FinalizeTest, hash = 34801522
*///:~
}

WeakReference 弱引用引用的类型如果没有其他指向了该应用的强引用,进行垃圾回收的时候该对象就会被回收,此时就会回调对象的 finalize() 方法

权限控制

子类会继承父类的属性和方法,子类是否可见 取决于父类的权限设置

Permission class package child class any class
public + + + +
default + + +
protected + +
private +

如果子类的方法重写 @Override 了父类的方法,那么 子类中该方法的访问级别 不允许低于父类的访问级别

这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足 里氏替换原则

权限控制测试

层次结构

1
2
3
4
5
6
7
8
9
permission

├─ pack1
│ CurPackTest.java
│ Permission.java

└─ pack2
ChildPerm.java
OtherPackTest.java

ps: 使用了 //! 注释掉的为保存的语句,也就是没有权限的测试

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// java 12 source

/* 1. class 同一个类中测试 */
package permission.pack1;

public class Permission {

public int pubVal;
int defVal;
protected int proVal;
private int priVal;

private void test() {
this.pubVal = 1;
this.proVal = 1;
this.defVal = 1;
this.priVal = 1;
}

}


/* 2. package 同一个包中的测试 */
package permission.pack1;

public class CurPackTest {

public void test() {
Permission permission = new Permission();
permission.pubVal = 1;
permission.defVal = 1;
permission.proVal = 1;
//! permission.priVal = 1; // private param can be used
}

}

/* 3. child 子类中的测试 */
package permission.pack2;

public class ChildPerm extends Permission {

public void setParentParam(int i) {
super.pubVal = i;
//! super.defVal = i; // child class cannot use default param
//! super.priVal = i; // child class cannot use private param
super.proVal = i;
}

// static 静态方法(例如 main) 只能通过对象实例的引用来访问
public static void staticMethodTest() {
Permission permission = new ChildPerm();
permission.pubVal = 1;
//! permission.proVal = 1; // protected cannot be used
//! permission.defVal = 1; // default cannot be used
//! permission.priVal = 1; // private cannot be used
}

}


/* 4. any class 不同包任意类中的测试 */
package permission.pack2;

public class OtherPackTest {

public void test(){
Permission permission = new Permission();
permission.pubVal = 1;
//! permission.defVal = 1; // default cannot be used
//! permission.proVal = 1; // protected cannot be used
//! permission.priVal = 1; // private cannot be used
}

}

通过对象实例访问

  1. 相同包下 通过对象引用 访问拥有包权限以上的属性
    • public
    • protected
    • default
  1. 不同包下 只能访问
    • public

通过自身和继承的属性访问

  1. 当前类中 拥有全部的权限
    • public
    • protected
    • default
  1. 导出类中通过对象实例 的访问基本一致,区别在于 protected 继承权限修饰符 (即子类的内部)
    • 子类中可以直接通过 super. 关键字来访问父类的继承权限的成员和方法,并 不是通过对象实例引用来访问!

继承类的初始化

这一部分在 {post_link 初始化 } 一文中有详细的解释,这里简单说明一下类初始化顺序

  1. 按照从 父类-> 子类,按照声明顺序从上-> 下,初始化父类和子类的中的 静态 成员/域
  2. 按照从 父类-> 子类,按照声明顺序从上-> 下,初始化父类和子类的中的 非静态 成员/域
  3. 按照从 父类-> 子类,调用构造器完成舒适化

初始化的位置

  1. 声明的位置
  2. 构造器中
  3. 调用之前(惰性初始化)
  4. 使用实例初始化

方法和成员变量的继承

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// test

/* 测试类 - 父类 */
public class SuperClass {

public int value = 0;

void printValues() {
System.out.println("super class printValues:");
System.out.println(" value: " + value);
System.out.println("this.value: " + this.value);
}

void method() {
System.out.println("super method()");
}

public void perm() {
System.out.println("super perm()");
}

}

/* 测试类 - 子类 */
public class SubClass extends SuperClass {

protected int value = 1; // 继承类中的成员变量没有权限要求

void printValues() {
System.out.println("sub class printValues:");
System.out.println(" value: " + value);
System.out.println(" this.value: " + this.value);
System.out.println("super.value: " + super.value);
System.out.println();

System.out.println("sub ----------> super printValues:");
super.printValues();
}

void method() {
System.out.println("sub method()");
System.out.println();

System.out.println("sub ----------> super method:");
super.method();
}

//! protected void perm() {} // 重写方法的权限不能低于被重写的方法的权限

}

为了区分和容易记住,将

  • 父类的 value 置为 0
  • 子类的 value 置为 1

方法的覆盖(重写)

方法覆盖的规范

  1. 权限控制。 当子类没有权限继承到父类的方法的时候,子类的同名方法就相当于另外增加了一个方法
    • private 父类方法私有
    • default 默认权限且不在同一个包内时,子类对其不可见
  1. final 使用了 final 修饰的方法是不能被子类重写的

  2. 异常。 子类重写方法的抛出异常类型 必须是父类抛出的异常类型的子类

  3. 协变返回类型。 子类重写的方法返回类型,可以是 父类方法返回类型的子类

重写方法的调用

首先让 父类引用 指向一个 子类实例对象,测试的两种情况

  1. 直接通过这个父类引用去调用重写的方法
  2. 将引用向下转型指子类型的引用,再去调用重写方法

ps: 在子类的方法中通过 super 又调用了父类的方法

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
// test

private static void extendsSuperAndSubMethod() {
SuperClass superClass = new SubClass();
System.out.println("------------ superClass.method() ------------");
superClass.method();
System.out.println();

System.out.println("------------ ((SubClass)superClass).method() ------------");
((SubClass)superClass).method();

/* Output:

------------ superClass.method() ------------
sub method()

sub ----------> super method:
super method()

------------ ((SubClass)superClass).method() ------------
sub method()

sub ----------> super method:
super method()
*///:~
}

不管是父类引用,还是子类引用,通过引用去调用被重写的这个方法,被调用的 总是子类重写的方法

成员变量的

成员变量不同于方法的覆盖重写,后面的测试中可以大体看的出来,被

继承的成员变量调用

首先让 父类引用 指向一个 子类实例对象,测试的三种情况

  1. 类内部调用成员变量(因为上面已经总结过重写的方法默认调用的为子类方法,这里就不分开测了)
    • value 不加任何修饰
    • this.value 通过 this 调用
    • super.value 通过 super 调用父类的成员变量
    • super.printValue() 调用父类的 printValue 方法,打印父类的上面的两种调用的结果
  1. 通过父类引用调用同名的成员变量
  2. 通过向下转型为子类引用,调用同名的成员变量
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
30
// test

private static void extendsSuperAndSubMember() {
SuperClass superClass = new SubClass();
System.out.println("------------ method call remember ------------");
superClass.printValues();

System.out.println();
System.out.println("------------ instance call remember ------------");
System.out.println(" superClass.value: " + superClass.value);
System.out.println("((SubClass) superClass).value: " +((SubClass) superClass).value);

/* Output:

------------ method call remember ------------
sub class printValues:
value: 1
this.value: 1
super.value: 0

sub ----------> super printValues:
super class printValues:
value: 0
this.value: 0

------------ instance call remember ------------
superClass.value: 0
((SubClass) superClass).value: 1
*///:~
}

从上面的结果可以看出,通过引用不同的调用 父类和子类的重名的成员变量重写的方法 的效果是不同的

  • 父类类型的引用调用的是 父类的成员变量
  • 子类类型的引用调用的是 子类的成员变量

另外 static 属性也是如此,大概是和类的 Class 对象绑定在一起?