01 基础知识
简单来说,不可变类是以final关键字定义的类,形如:
final class ClassName{
}
我们熟悉的final关键字通常是用来定义常量的。用final声明的方法和变量有如下的特点:
1. 以final声明的方法不允许覆盖。
2. 以final声明的变量不允许更改。
而利用final,我们可以设计出一种特殊的“只读” 的“不可变类 (immutable class)”。当
创建了一个“不可变的类”的对象之后,此对象的属性不可改,而且也无
法从此类派生出新子类。JDK中的 String 类就是一个不可变类的实例。
02 一个神奇的例子
观察下面的例子:
public class ExplorationJDKSource {
public static void main(String[] args) {
System.out.println(new A());
}
}
//类A没有定义任何的成员
class A {
}
运行代码,会得到以下的输出:
A@4eec7777
为什么会出现这样的情况呢?其原因是默认情况下,当一个对象被直接打印为字符串时,会调用该对象的`toString()`
方法来获取字符串表示形式。
在这个例子中,类A
没有定义任何成员,因此它继承了默认的`toString()`
方法实现。默认的`toString()`
方法会返回一个由类名和对象的哈希码组成的字符串。因此,输出的字符串"A@4eec7777"中的"A"是类名,"4eec7777"是对象的哈希码。
反编译编译后的.class文件,会看到以下的输出:
PS E:\ProgrammingFiles\Java\CompilerLab\out\production\CompilerLab> javap -c .\ExplorationJDKSource.class
Compiled from "ExplorationJDKSource.java"
public class ExplorationJDKSource {
public ExplorationJDKSource();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
public ExplorationJDKSource();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #13 // class A
6: dup
7: invokespecial #15 // Method A."<init>":()V
10: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
13: return
}
从反编译的输出可以看到,代码调用的实际上是 JDK 中的 `public void println(Object x) ` 这个方法,查看源码如下:
public void println(Object x) {
String s = String.valueOf(x);
if (getClass() == PrintStream.class) {
// need to apply String.valueOf again since first invocation
// might return null
writeln(String.valueOf(s));
} else {
synchronized (this) {
print(s);
newLine();
}
}
}
继续查看 valueOf 函数,看到其源码如下:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
继续查看其中的 toString 函数:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
继续查看hashCode:
@IntrinsicCandidate
public native int hashCode();
此时发现,该方法是一个本地方法,由JVM开发者提供具体实现,本地方法暂不做讨论。至此,这个例子为什么输出一串奇怪的字符串就大致清楚了。
03 修改一下例子
下面,把之前的例子稍加修改,此时的输出就完全不同:
public class ExplorationJDKSource {
public static void main(String[] args) {
System.out.println(new A());
}
}
//类A没有定义任何的成员
class A {
@Override
public String toString() {
return "This is class A";
}
}
此时,运行会输出“This is class A”。为啥呢?因为在修改后的代码中,我们重写了toString方法来返回我们想要的字符串表示形式,而不再去调用默认的方法。