是否完全掌握 Java 的引用?』我认为是 Java 基础是否入门的重要标志,甚至没有之一。

相较于其它的 Java 基础问题,其它问题都是语法层面的熟悉度、熟练度的问题,基本是不用动脑子思考的,直接怼就完事了。只有『Java 的引用』在概念上需要反复揣摩思考,才能彻底掌握。

Java 的引用的难以理解和掌握,原因在于『它一脉相承与 C 语言的指针』,而 C 语言的指针是很多 C 程序员的学习历程中的必备槽点。

# 故事从 C 的指针变量说起

这里我们将 C 语言的指针变量只是为了讲清楚『引用概念』的来龙去脉,以及为大家找到一个概念上的参照物。为了不给大家引入新的学习负担,相关的 C 语言的具体语法我们这里少讲、不讲,以讲道理、讲概念为主。

# 变量

『变量』是所有的编程语言中都会涉及到的最最基本的概念。有的书为了便于零基础的人学习,会举一个很贴切的例子:变量就像一个『盒子』,为一个变量赋值,就如同你往这个盒子中存放东西。

之所以用盒子来类比变量,是因为一个变量意味着内存中的一块内存空间,为变量赋值,就是为这块内存赋值,在这块内存上填入数据。而我们常说的『变量名』实际上就是这块内存的一个『代号』(你可以这么通俗地类比)。

# 强类型语言:变量的类型

C(以及 C++、Java 等)语言都是『强类型』语言。所谓的强类型语言的标志就是『变量是有类型的』,也就是说,变量的类型和变量中所存储的值必须一致。

以上面类比的『盒子』为例,这相当于说,这是一个放苹果的盒子,那么,无论未来这个盒子中放的是一个大苹果,还是一个小苹果,放的是一个红苹果,还是一个绿苹果,放的是一个进口苹果,还是一个本地苹果,这都是次要问题,最最基本一个问题是:是这个盒子里必须放苹果!放橘子、橙子、梨子、香蕉都不行。

int i = 'a'; // 此处会报语法错误

以具体的代码为例,上述代码就是错误的:变量 i 的类型是 int 类型,那么变量 i 中必须存放整型的数据,例如:10,9527,10086 等,而你将一个字符 'a' 赋值给它,那么值的类型和变量的类型不一样,这就是一个语法错误。

# 特殊类型的变量:指针变量

上面说到过,变量就代表着一块内存空间,变量名就相当于是这块内存空间的代号。除了通过变量名能让你找到那块内存,进而操作它之外,还有一样东西能让你找到这块内存,进而操作它:内存的地址

道理很简单。你想,现实生活中,不考虑同名的情况,你除了用『人名』找得到一个人之外,你如果有这个人的『身份证号』或者说『手机号』,那么,你一样也能找得到这个人,进而做后续的操作。

上面例子中的『人名』就相当于是变量名,『身份证号』/『手机号』就相当于是变量的地址,而『』就相当于是变量背后的那块实实在在的用于存储数据的内存空间。

看到这里你会发现一个问题:变量背后的那块内存的地址也是数据!地址本身也就是一串数字! 我们完全完全可以将这串数字也存储于一个变量中。

例如,现实生活中,我们完全可以将一个人的身份证号/手机号,抄在一张便签纸上!

『此处欠图一张,后续补』

结合上面所讲到的变量类型的概念,在 C 语言中,这种专门用于存放内存的地址的变量就是『指针变量』。

简单来说,结合变量的类型和指针变量两个概念一起考虑,类似于以下情况:

  • 有的便签纸上只允许写汉字,至于是什么汉字内容都可以,但必须是汉字;

  • 有的便签纸上只允许写英语,至于是什么英语内容都可以,但必须是英语;

  • 有些便签纸上只允许写拼音,至于是什么拼音内容都可以,但必须是拼音;

  • 而有一种/一类便签纸,它上面只允许写人的手机号,至于是谁的手机号都可以,但必须是手机号。

# 一个容易忽视的问题:一个地址,两个指针变量

现实中,我们完全可以做这样的一件事情:在两张『便签纸』上抄写同一个人的『手机号』。

更具体更形象的说法可以是这样,我们先将这个人的手机号抄写在一张变签纸上,让后再拿出第二张便签纸,将第一张便签纸上的内容原样抄写一遍。

这是生活中很正常的操作,看到这里大家一定会觉得这个操作好像没什么值得说道的地方呀?!

如果你能接受上述现实世界中的试试,那么你一定可以理解代码中的这种情况:有两个指针类型的变量,它们俩记录的都是同一块内存的地址。

别说,两个便签纸/变量记录同一个手机号/内存地址,就是有百八十个的也是合情合理又合法。

# 回到 Java 语言

# Java 语言中的内存地址

Java 语言中并没有『内存地址』这个说法。所有的 Java 基础语法书上都没有出现这个概念。

在 C 语言中,有一个取地址运算符:&,通过它你可以『求得』一个变量背后的那块内存的内存地址,而 Java 语言中并没有这个取地址运算符。

当然,有一个地方还是残留了『内存地址』的一点点痕迹:在默认的 Java 虚拟机(HotSpot)中,调用对象的 hashCode 方法,默认的返回值就是这个对象在内存中的地址。

不过需要注意的是,如果运行你的代码的 Java 虚拟机并非 HotSpot,或者你自定义类时自己实现了 hashCode 方法,那么上述说法就不一定成立了。

# Java 语言中的引用

Java 语言并没有直接提出『内存地址』的概念,而是将它演化成了『引用』的概念。

大家都知道,Java 也是一种强类型编程语言,Java 中的变量也是有类型的。Java 也要求变量的『值的类型』要与『变量的类型』是一致的。也就是我们上面类比的例子:苹果盒子中必然只能放苹果,不能放橘子、橙子、梨、香蕉。

而 Java 中变量的类型又分为两大类:基本类型(boolean、byte、short、int、long、float、double)以及『引用』。

简单来说,在 Java 中,一个变量的类型,如果不是上述 7 种基本类型之一,那么一定、一定、一定就是『引用』类型,绝无例外。

那什么引用类型的变量?

你可以这么理解,Java 中引用类型的变量就如同 C 语言中的指针类型的变量,它是专门用来存一个对象在内存中的地址的。

# 一个常识性错误

先看代码示例:

Student tom = new Student();

对于上述代码,你如果愿意,也可以写成如下两行的形式:

Student tom;
tom = new Student();

一个初学者常见的错误就是认为上述代码中 tom 是一个对象!无论一个 Java 程序员他工作了多少年,他如果是这么认为的,那么他就是初学者!

以之前的类比的例子来说:我们可以在一张便签纸上去记录一个人的手机号。这里有几样东西?

两样。便签纸 和 人 。

我通常会用另一种类比去帮助学生去理解『引用』和『对象』是分开看的两样东西。

情况一:假设你今天突然灵感来了,觉得『张三』是个好名字,决定以后你的孩子就叫这个名字。但是问题是,你现在可能都没有孩子,甚至都没有女朋友呢。那么此时,有名字,但这个名字背后并没有人!

情况二:一个班上有 30 个同学,自然就有 30 个人名,然后大家集体确定给『张三』同学起个外号叫『豆豆』。从此以后,你就会发现,如果『张三』今天迟到了,那么『豆豆』今天也会迟到;如果『张三』今天过生日,那么『豆豆』今天也会过生日。好巧喔~。巧个屁,『张三』和『豆豆』本身就是同一个人的两个名字!

在上面两个例子中,你会发现『名字』和『人』其实是两个独立的概念,只不过大家平时没有专门去思考这个问题。通常大家就是笼统地将人名和人画上了等号,但是细说起来并非如此。

回到我们的 Java 代码中:我有一个对象,另外我还有一个引用类型的变量记录了这个对象在内存中的地址,那么这里有几个东西?两个。对象和引用类型的变量。

再看上述代码,tom 是一个引用类型的变量的名字,它本身并非 Student 对象!只不过它记录了一个 Student 对象在内存中的地址。

# 常识性错误的高阶版

先看代码:

Student tom = new Student();
Student jerry = tom;

上述代码中有几个对象?

标准的 错误答案 是两个:tom 和 jerry 。

回忆/复习上一章节,tom 和 jerry 是对象吗?不是,tom 和 jerry 是引用类型的变量,而非对象。

这个代码中对象有且仅有一个(显而易见,因为 new 只有一个嘛)。而 tom 和 jerry 记录的都是这一个对象在内存中的地址。

细说起来是 tom 变量先记录了这个 Student 对象在内存中的地址,而 jerry 变量把 tom 变量的内容原样炒了一份,那么毫无疑问,jerry 自然也是记录这个 Student 对象在内存中的地址。

这是不是我们之前类比的,在两张便签纸上记录同一个人的手机号。

# 引用传递

Java 中有一个引用传递的概念,示例代码如下:

public static void main(String[] args) {
    Student tom = new Student();
    demo(tom);  // 方法调用
}

public static void demo(Student jerry) {
    ...
} 

在 main 方法调用 demo 方法时会有一个参数传递的行为,逻辑上就是发生了形如如下代码的操作:

Student jerry = tom;

结合之前讲的概念,这里你应该很容易理解,main 方法里的 tom 和 demo 方法中的 jerry 实际上就是同一个对象的两个『名字』。

# == 判断

讲到这里,Java 中 == 判断的作用和底层逻辑就很清晰了:它是用来判断两个引用是否指向的是同一个对象。通俗地说,就是两个『人名』背后是否是同一个『人』。

Student tom = new Student();
Student jerry = tom;
System.out.println( tom == jerry ? "Yes" : "No"); // Yes

Student ben = new Student();
System.out.println( tom == ben ? "Yes" : "No"); // No

第一个输出会是 Yes,而第二个输出会是 No。因为 ben 所指向的那个 Student 对象是第二个 new 创建出来了,tom 和 ben 背后分别是两次 new 出来的两个 Student 对象。

# 引用类型的细分

『引用类型』实际上仍然是一个很宽泛的说法。例如下述错误代码:

Student tom = new Teacher();

上述代码中,tom 是一个引用类型的变量,这没错。不过,更进一步细说,tom 是一个 Student 类型的引用,也就是说,tom 这个变量只能去记录一个 Student 对象的地址。

而上述代码的错误就在于,你让 tom 去记录一个 Teacher 对象的内存地址。本质上,这就是犯了一个在苹果盒子里装橘子的错误。

# null

如果一个引用类型的变量自声明以后,从未被赋过值,那么它的值就是 null。

Student tom;
Student tom = null;

上述两行代码的效果是等价的。

这就相当于,你有个苹果盒子,但是在这个盒子中什么苹果都没有放。也相当于,你想到了一个叫做『豆豆』的外号,但是你还没有把这个外号『按到』任何一个人的头上。

这种情况下就是有『人名』,但是『人名』的背后还没有『人』。此时,你喊这个『人名』,就是喊破喉咙都没人答应,因为逻辑上这里就是有问题的。

# 数组与引用的关系 1

之前说过,在 Java 中,一个变量但凡不是 7 种基本类型之一,那它就是引用类型,无一例外。数组变量就是引用。一个数组变量,记录了一个数组对象在内存种的地址。

int data[] = new int[3];

借用之前的说法:此处有 2 样东西,一个是数组对象,其中可以存放三个 int 型的数据。另一个是用于存放数组对象的内存地址的东东,也就是变量 data,它是一个引用。

再看下面这段代码:

int array1[] = new int[]{1, 2, 3};
int array2[] = array1;

上述的代码和我们前面章节讲到的情况本质上是一样的:array1 和 array2 这两个引用指向了同一个对象。你动了 array1 数组,你会发现 array2 会同样发生变化。就跟今天『张三』过生日,结果『豆豆』也过生日一样。

###0 数组与引用的关系 2

前一章节中我们代码种的数组是基本类型的数组:

int data[] = new int[3];

除了有基本类型的数组之外,我们还会见到对象的引用类型的数组,例如:

Student array[] = new Student[10];

那么上述代码中的 array 中装的是什么东西?array 中是装了 10 个 Student 对象吗?

我们现实中会有这样的情景:老师手里拿着学生的名单走进教室,对同学们说:“所有的同学都在这个纸上,......”。

你想想什么叫“同学在纸上”?如果真的是人在纸上那是个什么样子?所以在纸上的并不是真的『人』,而是代表着人的『名字』或者是『身份证号』。

在上面的例子中,『纸』就是数组,数组中存的并不是 Student 对象本身,而是存 Student 对象在内存中的地址。也就是说,内存中有 10 个 Student 对象,它们所在的地址分别是:xxx、xxx、xxx、...,而这 10 个地址被记录在了一个数组中。而这个数组本身又有一个地址,这个地址记录在了引用类型的变量 array 中。

『此处欠图一张,后续补』

###1 清楚之后的马虎

Student tom = new Student();

现在我们都知道了 tom 本身并不是一个对象,它是一个引用,它记录了一个 Student 对象在内存中的地址,或者说它指向了一个内存中的 Student 对象。

在你能明确区分『引用』和『对象』两个概念之后,我们日常的沟通和交流中,也不至于每次都说的那么精确,那么严谨,不必每次都说“Student 类型的引用 tom ”,这个时候,在双方都清楚的情况下,我们还是可以“马虎”地说:对象 tom 。

数组也一样:

int data[] = new int[3];

在沟通双方都清楚的前提下,我们还是可以“马虎”地说数组 data,尽管 data 并非数组对象本身。

就好像我们可以说 136xxxxxxxx 这个人如何如何一样,它并非人本身,只是人的手机号,但是说话和听话双方还是可以相互沟通交流的。

###2 引用类型的对象的属性

我们经常会遇到这样的情况,要给类的属性中有引用类型的属性,例如:

class Student {
    String name;
    int age;
    Teacher teacher;
}

如何理解上面的 “Student 中『有』一个 Teacher” ?

如果你能理解前面章节的内容一路看到这里,那么这里的情况对你而言就十分简单了。很明显,一个 Student 对象中不可能有一个 Teacher 对象。

我们日常生活中会说这样的话:我心里有我女朋友。来来来,你给我表演一个把个活人装心里?!

Student 对象的 teacher 属性是一个引用,而非对象本身。这里和数组的情况一样,Teacher 对象本身在『另一个地方』,而 Student 对象的 teacher 属性中记录的是这个 Teacher 对象的所在内存地址。