Java 的引用和对象

「是否完全掌握 Java 的引用?」是 Java 基础是否入门的重要标志,甚至没有之一。
相较于其它的 Java 基础问题,其它问题都是语法层面的熟悉度、熟练度的问题,基本是不用
动脑子思考的,直接怼就完事了。只有「Java 的引用」在概念上需要反复揣摩思考,才能彻
底掌握。
Java 的引用的难以理解和掌握,原因在于「它一脉相承与 C 语言的指针」,而 C 语言的指针
是很多 C 程序员的学习历程中的必备槽点。

1.从 C 的指针变量说起

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

1.1 变量

「变量」是所有的编程语言中都会涉及到的最最基本的概念。有的书为了便于零基础的人学
习,会举一个很贴切的例子:变量就像一个「盒子」,为一个变量赋值,就如同你往这个盒子
中存放东西。
之所以用盒子来类比变量,是因为一个变量意味着内存中的一块内存空间,为变量赋值,就是
为这块内存赋值,在这块内存上填入数据。而我们常说的「变量名」实际上就是这块内存的一
个「代号」 (你可以这么通俗地类比) 。

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

 C (以及 C++、Java 等) 语言都是「强类型」语言。所谓的强类型语言的标志就是「变量是有类
型的」,也就是说,变量的类型和变量中所存储的值必须一致。
以上面类比的「盒子」为例,这相当于说,这是一个放苹果的盒子,那么,无论未来这个盒子
中放的是一个大苹果,还是一个小苹果;放的是一个红苹果,还是一个绿苹果;放的是一个进
口苹果,还是一个本地苹果;这都是次要问题,最最基本一个问题是:是这个盒子里必须放苹
果!放橘子、橙子、梨子、香蕉都不行。

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

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

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

上面说到过,变量就代表着一块内存空间,变量名就相当于是这块内存空间的代号。除了通过
变量名能让你找到那块内存,进而操作它之外,还有一样东西能让你找到这块内存,进而操作
它:内存的地址 。
道理很简单。你想,现实生活中,不考虑同名的情况,你除了用「人名」找得到一个人之外,
你如果有这个人的「身份证号」或者说「手机号」,那么,你一样也能找得到这个人,进而做
后续的操作。
上面例子中的「人名」就相当于是变量名,「身份证号」/「手机号」就相当于是变量的地
址,而「人」就相当于是变量背后的那块实实在在的用于存储数据的内存空间。
看到这里你会发现一个问题:变量背后的那块内存的地址也是数据!地址本身也就是一串数
字! 我们完全完全可以将这串数字也存储于一个变量中。
例如,现实生活中,我们完全可以将一个人的身份证号/手机号,抄在一张便签纸上!
「此处欠图一张,后续补」
结合上面所讲到的变量类型的概念,在 C 语言中,这种专门用于存放内存的地址的变量就是
「指针变量」。
简单来说,结合变量的类型和指针变量两个概念一起考虑,类似于以下情况:

  • 有的便签纸上只允许写汉字,至于是什么汉字内容都可以,但必须是汉字;
  • 有的便签纸上只允许写英语,至于是什么英语内容都可以,但必须是英语;
  • 有些便签纸上只允许写拼音,至于是什么拼音内容都可以,但必须是拼音;
  • 而有一种/一类便签纸,它上面只允许写人的手机号,至于是谁的手机号都可以,但必须是

手机号。

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

现实中,我们完全可以做这样的一件事情:在两张「便签纸」上抄写同一个人的「手机号」。
更具体更形象的说法可以是这样,我们先将这个人的手机号抄写在一张变签纸上,让后再拿出
第二张便签纸,将第一张便签纸上的内容原样抄写一遍。
这是生活中很正常的操作,看到这里大家一定会觉得这个操作好像没什么值得说道的地方
呀?!
如果你能接受上述现实世界中的试试,那么你一定可以理解代码中的这种情况:有两个指针类
型的变量,它们俩记录的都是同一块内存的地址。
别说,两个便签纸/变量记录同一个手机号/内存地址,就是有百八十个的也是合情合理又合
法。

2. 回到 Java 语言

2.1 Java 语言中的内存地址

Java 语言中并没有「内存地址」这个说法。所有的 Java 基础语法书上都没有出现这个概念。
在 C 语言中,有一个取地址运算符: & ,通过它你可以「求得」一个变量背后的那块内存的内
存地址,而 Java 语言中并没有这个取地址运算符。
当然,有一个地方还是残留了「内存地址」的一点点痕迹:在默认的 Java 虚拟机
(HotSpot)中,调用对象的  hashCode 方法,默认的返回值就是这个对象在内存中的地址。

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

2.2 Java 语言中的引用

Java 语言并没有直接提出「内存地址」的概念,而是将它演化成了「引用」的概念。
大家都知道,Java 也是一种强类型编程语言,Java 中的变量也是有类型的。Java 也要求变量
的「值的类型」要与「变量的类型」是一致的。也就是我们上面类比的例子:苹果盒子中必然
只能放苹果,不能放橘子、橙子、梨、香蕉。
而 Java 中变量的类型又分为两大类:基本类型(boolean、byte、short、int、long、
float、double)以及「引用」。
简单来说,在 Java 中,一个变量的类型,如果不是上述 7 种基本类型之一,那么一定、一
定、一定就是「引用」类型,绝无例外。
那什么引用类型的变量?
你可以这么理解,Java 中引用类型的变量就如同 C 语言中的指针类型的变量,它是专门用来
存一个对象在内存中的地址的。

2.3 一个常识性错误

先看代码示例:

Student tom = new Student();

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

 Student tom;
 tom = new Student();

一个初学者常见的错误就是认为上述代码中 tom 是一个对象!无论一个 Java 程序员他工作了
多少年,他如果是这么认为的,那么他就是初学者!
以之前的类比的例子来说:我们可以在一张便签纸上去记录一个人的手机号。这里有几样东
西?
两样。便签纸 和 人 。
我通常会用另一种类比去帮助学生去理解「引用」和「对象」是分开看的两样东西。
情况一:假设你今天突然灵感来了,觉得「张三」是个好名字,决定以后你的孩子就叫这个名
字。但是问题是,你现在可能都没有孩子,甚至都没有女朋友呢。那么此时,有名字,但这个
名字背后并没有人!
情况二:一个班上有 30 个同学,自然就有 30 个人名,然后大家集体决定给「张三」同学起
个外号叫「豆豆」。从此以后,你就会发现,如果「张三」今天迟到了,那么「豆豆」今天也
会迟到;如果「张三」今天过生日,那么「豆豆」今天也会过生日。好巧喔~。巧个屁,「张
三」和「豆豆」本身就是同一个人的两个名字!
在上面两个例子中,你会发现「名字」和「人」其实是两个独立的概念,只不过大家平时没有
专门去思考这个问题。通常大家就是笼统地将「人名」和「人」画上了等号,但是细说起来并
非如此。
回到我们的 Java 代码中:我有一个对象,另外我还有一个引用类型的变量记录了这个对象在
内存中的地址,那么这里有几个东西?两个。对象和引用类型的变量。
再看上述代码, tom 是一个引用类型的变量的名字,它本身并非 Student 对象!只不过它记
录了一个 Student 对象在内存中的地址。

2.4 常识性错误的高阶版

先看代码:

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

上述代码中有几个对象?
标准的 错误答案 是两个:tom 和 jerry 。

回忆/复习上一章节,tom 和 jerry 是对象吗?不是,tom 和 jerry 是引用类型的变量,而非
对象。
这个代码中对象有且仅有一个(显而易见,因为 new 只有一个嘛)。而 tom 和 jerry 记录的
都是这一个对象在内存中的地址。
细说起来是 tom 变量先记录了这个 Student 对象在内存中的地址,而 jerry 变量把 tom 变量
的内容原样炒了一份,那么毫无疑问,jerry 自然也是记录这个 Student 对象在内存中的地址。
这是不是我们之前类比的,在两张便签纸上记录同一个人的手机号。

3. 基于引用的高阶知识

3.1 引用传递

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

1.  public static void main(String[] args) {
2.  Student tom = new Student();
3.  demo(tom); // 方法调用
4.  }
5.  public static void demo(Student jerry) {
6.  ...
7.  }

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

Student jerry = tom;

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

3.2 == 判断

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

1.  Student tom = new Student();
2.  Student jerry = tom;
3.  System.out.println( tom == jerry ? "Yes" : "No"); // Yes
4.  Student ben = new Student();
5.  System.out.println( tom == ben ? "Yes" : "No"); // No

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

3.3 引用类型的细分

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

Student tom = new Teacher();

上述代码中,tom 是一个引用类型的变量,这没错。不过,更进一步细说,tom 是一个
Student 类型的引用,也就是说,tom 这个变量只能去记录一个 Student 对象的地址。
而上述代码的错误就在于,你让 tom 去记录一个 Teacher 对象的内存地址。本质上,这就是
犯了一个在苹果盒子里装橘子的错误。

3.4 null

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

1.  Student tom;
2.  Student tom = null;

上述两行代码的效果是等价的。
这就相当于,你有个苹果盒子,但是在这个盒子中什么苹果都没有放。也相当于,你想到了一
个叫做「豆豆」的外号,但是你还没有把这个外号「按到」任何一个人的头上。
这种情况下就是有「人名」,但是「人名」的背后还没有「人」。此时,你喊这个「人名」,
就是喊破喉咙都没人答应,因为逻辑上这里就是有问题的。

3.5 数组与引用的关系

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

 int data[] = new int[3];

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

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

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

3.6 数组与引用的关系 2

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

  int data[] = new int[3];

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

Student array[] = new Student[10];

那么上述代码中的 array 中装的是什么东西?array 中是装了 10 个 Student 对象吗?
我们现实中会有这样的情景:老师手里拿着学生的名单走进教室,对同学们说:「所有的同学
都在这个纸上,......」。
你想想什么叫「同学在纸上」?如果真的是人在纸上那是个什么样子?所以在纸上的并不是真
的「人」,而是代表着人的「名字」或者是「身份证号」。
在上面的例子中,「纸」就是数组,数组中存的并不是 Student 对象本身,而是存 Student
对象在内存中的地址。也就是说,内存中有 10 个 Student 对象,它们所在的地址分别是:
xxx、xxx、xxx、...,而这 10 个地址被记录在了一个数组中。而这个数组本身又有一个地址,
这个地址记录在了引用类型的变量  array 中。

3.7 清楚之后的马虎
Student tom = new Student();

现在我们都知道了 tom 本身并不是一个对象,它是一个引用,它记录了一个 Student 对象在
内存中的地址,或者说它指向了一个内存中的 Student 对象。
在你能明确区分「引用」和「对象」两个概念之后,我们日常的沟通和交流中,也不至于每次
都说的那么精确,那么严谨,不必每次都说「Student 类型的引用 tom」,这个时候,在双
方都清楚的情况下,我们还是可以「马虎」地说:对象 tom 。

数组也一样:

int data[] = new int[3];

在沟通双方都清楚的前提下,我们还是可以「马虎」地说数组 data,尽管 data 并非数组对象本身。
就好像我们可以说 136xxxxxxxx 这个人如何如何一样,它并非人本身,只是人的手机号,但是说话和听话双方还是可以相互沟通交流的。

3.8 引用类型的对象的属性

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

1.  class Student {
2.      String name;
3.      int age;
4.      Teacher teacher;
5.  }

如何理解上面的「Student 中「有」一个 Teacher」?
如果你能理解前面章节的内容一路看到这里,那么这里的情况对你而言就十分简单了。很明
显,一个 Student 对象中不可能有一个 Teacher 对象。
我们日常生活中会说这样的话:我心里有我女朋友。来来来,你给我表演一个把个活人装心里?!
Student 对象的 teacher 属性是一个引用,而非对象本身。这里和数组的情况一样,Teacher对象本身在「另一个地方」,而 Student 对象的 teacher 属性中记录的是这个 Teacher 对象
的所在内存地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值