传值,传引用
问题引入
先给出一个示例,你看看自己能不能做出来,对传值,传引用掌握了没有.
1 | public class SendDataTest |
点击查看运行结果(如果你不思考直接查看,将失去它的意义)
1 | [a, b] |
如果你有出错的地方,或你不清楚自己为什么这样答的话,就需要跟着我的思路来梳理一下.
- 首先你要明白问题中传值和传引用是什么意思?
传值:基本数据类型的传递
传引用:引用数据类型的传递
8大基本数据类型:byte,int,short,long,double,float,boolean,char
引用类型:数组,类,接口
所以,变量可分为基本数据类型变量
和引用数据类型变量
,
基本类型变量:直接把数据放入变量的储存单元里.
(例如int i=5;
这里就直接声明int
基本数据类型变量,变量名称为i
,把数据5
直接放入名称为i
的存储单元中)
引用类型变量:存储单元里面保存的不是数据对象,而是数据对象的引用地址
,真正的数据对象是放在堆内存中的.
(例如List<String> list=new ArrayList<>();
,这时就先声明一个List<String>
的引用类型变量,
引用变量的名称为list
,同时开辟了一个堆内存空间,给这块内存地址分配了一个值放入list
变量的储存单元中,创建ArrayList
对象放入堆内存中)
传递是通过变量之间的赋值实现的:(单从变量角度看的话,变量之间的传递是值传递.因为无论是值传递,还是引用传递,其传递的原理都是把传递变量
储存单元中的数据复制一份放在接收变量
储存单元中)
传值
把上面的一部分抽取出来,
1 | public static void main(String[] args) |
按照单线程执行顺序,我进行了步骤标注,下面按照步骤顺序来讲解:
- 声明基本数据类型变量,变量名称为
i
,储存单元里放入数据1
- 进入方法
change
,传入参数变量i
- 声明基本数据类型变量,变量名称为
n
,此时变量n
储存单元里面的数据就是步骤2中变量i
中的储存单元数据(储存单元数据复制),所以此时变量n
的存储单元中数据是1
- 把变量
n
中的储存单元数据改为2
(修改的是变量n
中储存单元的数据,并不影响变量i
中储存单元的数据) - 控制台打印变量
i
,输出变量i
中储存单元的值还是1
这样说你懂了吗?
还要记住的是,变量只在当前所在的{}大括号中有效,不同{}大括号中可以有不同的变量名称
为了方便解释,我们说上面有两个方法栈内存,分别是main方法栈
和change方法栈
,他们两个变量名称也可以相同.(我是为了做区分,命名了不同的变量名称)
在下面贴出步骤4执行前,和执行后的内存语义.(jvm中可以有多个线程栈,这里只有main线程栈.)
传引用
示例1:改变接收变量
所指向的数据对象.
1 | public static void main(String[] args) |
同理我依然对步骤进行了标注
- 声明
List<String>
(泛型为String的List接口)引用数据类型变量,变量名称为list
,因为是引用数据类型变量,在堆内存中分配一块内存空间用来存放数据对象,有一个值用来标注这块堆内存空间在堆内存中的位置,通过这个值访问到堆内存空间中的数据对象,就像通过下标获取数组值一样,我们称这个值为数据对象在堆内存中的地址,再把这个值放入引用变量list
的储存单元中,所以这个引用变量就可以通过这个值访问到数据对象,我们通常简称这个值为引用地址
. - 因为
list
是引用变量,而不是基本变量.所以要通过引用变量list
储存单元中的引用地址
找到数据对象,并调用数据对象的add方法,添加数据”a” - 进入change方法,传入参数为:引用变量
list
,同值传递一样,此时传入的也是储存单元中的值, - 声明引用变量,名称为
list
,注意,这里的变量名称是储存在change方法栈
中的,所以不会和main方法栈
中的list
变量造成冲突,同值传递一样,把main方法栈
中的list
变量储存单元中的值传递给change方法栈
中的list
变量,也就是说两个方法栈中的引用变量list
都指向了同样的数据对象. - 由上一步可得,两个方法栈中的
list
变量指向了同样的数据对象,此时往数据对象中添加数据”b”,两个方法栈中的list
变量所指向的数据对象也是同样进行了改变. - 打印数据对象,输出
[a, b]
.
同样在步骤5执行前,和执行后贴出内存语义.
示例1总结:
改变的是数据对象,而并未改变两个变量的引用地址
示例2:把接收变量
指向新的数据对象.
1 | public static void main(String[] args) |
注意这里与示例一
的不同点,我只对change方法进行了改变,重新标注了步骤顺序,这里省略了1-4步骤
5. 同步骤1一样,在change方法栈
中创建引用变量list2
,并分配内存空间放入ArrayList
数据对象,此时list2
的储存单元中是新数据对象的引用地址
,因为是新的对象,所以有新的引用地址
,即不同于这两个方法栈中的list
变量的引用地址
6. 根据list2
中的引用地址
找到新建的数据对象,往里面添加数据”b”(注意:此时第一个建的数据对象并未发生改变,还是只保存了数据”a”)
7. 把change方法栈
中list
变量储存单元的引用地址
改为change方法栈
中的list2
变量的引用地址
,即change方法栈
中的list
变量指向新建的数据对象.(注意:此时并没有改变main方法栈
中引用变量list
储存单元中的引用地址
,它还是指向原来的对象)
8. 由步骤7,8可知,main方法栈
中list
变量的引用地址并未改变(还指向原对象),原对象也未改变,所以控制台输出[a]
示例2总结:
改变的是change方法栈
中list
变量的引用地址,使之指向新创建的对象,而main方法栈
中的list
变量未曾改变.
在上面讲值传递
示例1
示例2
中,我反复说是哪个方法栈的那个变量,是引用变量,还是基本变量,改变的是储存单元的值还是改变数据对象,
只有清楚了这些才能知道,其为什么不能改变.
示例3:final是为了让储存单元中的值不变
1 | public static void main(String[] args) |
因为final修饰变量,是修饰变量储存单元中的数据不能改变,这里也就是不能改变list
中保存的引用地址
,也就是list
会一直指向一个数据对象而不能指向其他数据对象.
因为两个list
变量都指向那个数据对象,list.add("b");
修改的是那个数据对象,所以会输出[a,b]
示例4:String类.
1 | public static void main(String[] args) |
我们在讲解这个,应该先查看后面的String内存语义的讲解.
因为String不通过new创建的数据对象会放入String常量池
中,而通过new
创建的会放入堆内存中,
所以这里str = "b";
会在String常量池
中新建一个”b”数据对象,同时生成一个新的引用地址
,并把这个引用地址
放入change方法栈
中的list
变量储存单元中.
而这时change方法栈
中的list
变量就会指向String常量池
中的”b”数据对象.
示例5:Integer类.
1 | public static void main(String[] args) |
在下面我们也讲解了Integer
的内存语义.
如果不通过new
创建新的Integer
对象的话,如果范围在[-128,127]
就去Integer
内部类中cache
变量(Integer对象数据)中找到对应的Integer
对象,
如果不在就在堆内存new
一个Integer
数据对象,并把引用地址
放入对应Integer
变量的储存单元中.
所以说传递变量list
指向的是cache
中值为1
的Integer
对象,在执行i = 2;
之前,接收变量list
指向的是cache
中值为1
的Integer
对象,也就是指向同一个对象,两个list
变量储存单元中保存的数据是相同的.(因为change方法中list
变量储存单元中的值,是main中list
传递过来的)
Integer的==和equals比较
Integer,int,long的比较问题,先讲懂了这个,也就是知道String的比较了.
Integer与Integer比较
1 | Integer i1 = 127; |
点击查看运行结果
1 | true |
注意Integer i1 = 127;
创建的时候会调用Integer
类的valueof
方法,
把数据int基本数据类型127封装成Integer对象,并把这个对象的引用地址放入i1
变量的储存单元中.
1 | public static Integer valueOf(int i) { |
这里要想看懂,就必须了解IntegerCache
到底是什么?
1 | private static class IntegerCache { |
这里我们知道了它是Integer
的私有静态内部类,所以在Integer
的类中可以随意调用.
我们知道cache
变量是Integer
类型的数据,而low=-128
,那么high
变量到底是什么?
上面我们可以看出,要想知道high
变量,就要看懂静态的代码块,
要想知道代码块,就要知道那个if方法到底是true,还是false.下面我们进行测试
1 | String integerCacheHighPropValue = |
经过我的测试,它是为false的,下面我们把这个静态代码块来简化一下,省去无必要的代码.
1 | static { |
注意,这里我把这段静态代码块简化了,因为if为false,而且在我们使用的时候,如果省略了assert
,
因为在我们JVM不指定-ea
的时候,它是不影响程序的.
我们可以得到总结,这是一个cache
引用变量数据,数组中放的都是Integer
对象,
下标0~255
依次对应的是-128~127
,这里我们再回过头来看Integer
的valueOf
方法.
1 | public static Integer valueOf(int i) { |
从上面分析,我们知道了low = -128
,high = 127
,cache
是一个Integer
数组.
我们可以看出,如果你设置的int值在[-128,127]
这个范围,就不会创建新的对象,直接去cache
引用变量中去拿所对应的Integer
对象.
如果我们不在这个范围,就会重新创建一个Integer
对象.
下面我们看看创建Integer
对象的构造函数.
1 | private final int value; |
我们可以看到,Integer
对象内部是由一个int
基本数据类型变量value
来维护的,而这个value
是final
修饰的,
也就是说这个变量的储存单元的值是不可变的.它存放的数据就是我们在构造函数中初始化的值.
到这里我们就分析完毕了,所以我们再回过头来分析一下开头的那个示例.
1 | Integer i1 = 127; |
输出1
:==
比较的是储存单元中的值,127在[-128,127]
这个范围,所以它在初始化调用valueOf
方法时,不会创建新的对象,,i1
和i2
引用变量都是指向了cache
数组中value
为127的Integer
对象.
指向了同一个对象,储存单元中保存的引用地址也是相同的,所以为true.输出2
:因为128不在[-128,127]
的这个范围,所以它在初始化调用valueOf
方法时,会创建新的对象,他们两个都创建了新的对象.
虽然这两个Integer
对象的value
是相同的,但是他们两个对象在堆内存中所占的空间位置是不同的,他们各有各的引用地址
,所有i3
和i4
的储存单元中是不同的引用地址
,所有为false.输出3
输出4
同理可得.
我们再来分析下面这种情况
1 | Integer i1 = 127; |
点击查看执行结果
1 | false |
如果你还是做错的话,或不清楚为什么输出false,就说明,你对上面我的讲解还是理解不够到位,下面我们来分析这种情况的原因.
首先你要搞清楚new
的关键字,它是表示新建一个数据对象
,并在堆内存中分析一块新的空间,并且这个数据对象有一个新的引用地址
.
所以,你搞清楚了new
的关键字的内存语义,我们就可以分析出,i1
变量的引用地址
指向的是Integer
内部类的cache
数组中的value
为127的Integer
对象,而i2
指向的是通过new
关键字新建在堆内存中的Integer
对象.
虽然i1
和i2
变量指向数据对象
中value
变量的储存单元中的数据是同样的,都是127; 但是==
比较的是i1
和i2
两个变量中储存单元中储存的值,也就是两个不同的引用地址
(因为他们指向的是堆内存空间中两个不同位置的数据对象)
- 两个对象
==
比较的总结
所以,要想比较两个对象的==
是否为true,关键点是在于两个引用变量储存单元中的值是否相同,也就是引用地址
是否相同,也就是是否指向了同一个对象,是否指向同一个对象的关键点在于是不是new
了一个新的对象.
Integer与基本数据类型比较
分析完了,两个Integer
对象的==
比较,下面我们再来分析Integer
与int
long
的==
比较
1 | Integer i1 = 127; |
点击查看运行结果
1 | true |
如果你回答错了,也没关系,下面通过我的分析你就可以知道为什么了.
首先你要了解Integer
对其他基本数据类型变量进行==
比较,或直接直接进行数据比较,例如i1 == 5
这种情况会默认调用intValue
方法,
1 | public int intValue() { return value; } |
可以看出它会返回一个int
值,也就是返回一个基本数据类型的变量,其返回的就是Integer
内部维护的value
变量,在上面我们分析出了value
变量是初始化时指定的.
所以输出1
进行==
比较的就是:i1
变量指向的Integer
对象中value
变量储存单元的数据是否与i2
变量储存单元的数据相同,我们可以知道它们是相同的,都是指向了127.输出2
同理我们可以得出结果也为true.输出3
更为简单,因为i2
和i3
是基本数据类型的变量,所以它们储存单元中保存的都是数据127,所以为true.
基本数据类型与基本数据类型比较
1 | int i1 = 5; |
点击查看运行结果
1 | true |
至于结果为什么,下面我们做一些讲解.
根据我知道的,直接写数据5
可以赋值给int
long
double
float
变量.
如果是5.0
只能赋值给double
(因为它默认是double类型),如果想赋值给float
必须加(float)
强制转换,或在数字后面加f
F
例如5.0f
或 5.0F
long
类型后面可以写l
L
,如果赋值的数据超出了int
的范围,就需要指定字母double
类型后面可以写d
D
float
类型后面可以写f
F
这里的5.0可以默认更换为5,如果是5.00000默认也是转换成5放入基本数据类型变量的储存空间中.
因为它们后面的0是可以忽略的,可以更多的节省空间.
Integer与Integer,基本数据类型的equals比较
我们分析完了Integer
的==
比较,我们再来分析equals
比较.
有了前面我们对Integer
类的铺垫,下面我们先来看equals
方法,再来讲解示例.
1 | public boolean equals(Object obj) { |
我们要想知道equals
方法,就要知道obj instanceof Integer
的意思.
它表示传入的参数如果是Integer
类型,结果就为true,否则为false.
如果传入的也是Integer
类型的话,就让这两个Integer
对象来比较其内部的value
变量,看看这两个变量储存单元中的值是否相同.相同为true,不相同为false.
如果不是Integer
类型的话,就直接返回false.
1 | Integer i1 = 127; |
点击查看运行结果
1 | true |
如果你猜错了输出1
的结果,那是不可原谅的,因为在上面我们说的很清楚,如果是两个Integer
对象的话,就比较他们的value
变量中的储存单元中的值是否相同,如果传入的不是Integer
对象就直接返回false.
如果你猜错了输出2,3
,没关系.
那是因为**Integer
对象在与基本数据类型变量()进行equals
比较的时候,会把基本数据类型的变量转换为对象.
这里的输出2
就是把i3
基本变量的储存单元中的值,也就是127通过Integer
类的valueOf
方法转换成了Integer
对象,然后再if判断为true,再进行==
比较两个Integer
对象的value
.
为什么会调用Integer
的valueOf
方法,是因为i3
变量是int
类型,而int
类型对应的类就是Integer
.
如果直接使用i1.equals(127)
结果也是true,因为它会调用Integer
的valueOf
.
如果是使用i1.equals(127L)
结果就为false了,因为它会调用Long
的valueOf
,它会返回一个Long
对象,因为if判断为false,直接返回false.而不会比较内部value
变量储存单元中的数据.输出3
就是因为i4
是long
类型的,所以会调用Long
的valueOf
进行转换为Long
对象.所以为false.
总结
到这里Integer类型就讲述完毕了,下面进行了一个总结:==
比较:
(1)如果两个都是Integer
对象就比较他们的变量的引用地址
(这里需要注意new
是创建一个新的对象,并分配一个新的引用地址
,也就是说,在两个都是Integer对象时,有一个是通过new来创建的,结果就为false)
(2)只有一个是Integer
对象,另外一个是基本数据类型,就比较Integer
对象的value
变量储存单元中的数据与后者储存单元中的值进行比较是否相同
(3)如果都是基本数据类型,就直接比较他们的储存单元中的值是否相同.equals()
比较:
(1)后者是Integer
对象,比较它们的value
变量储存单元中的数据是否相同.
(2)后者是基本数据类型,把基本数据类型的数据转换为对象,然后再进行比较.
如果后者是int
类型,就调用Integer
的valueOf
方法.
如果后者是long
类型,就调用Long
的valueOf
方法 …
String
在上面我们讲解了Integer
类型的比较,下面我们来进行String
的讲解.
==
老样子,我们先做个示例,再进行讲解为什么.
1 | String a1 = "a";// 1 |
点击查看运行结果
1 | true |
在分析之前,我们要知道==
比较的是两个变量储存单元中的数据是否相同.(这个在之前已经讲到)String
类型的变量是引用变量,而引用变量是通过引用地址
指向堆内存中的一个数据对象.
而步骤1
没有通过new
来创建新的数据对象
,是因为步骤1
先去一个叫String常量池
的地方找是否有相同的数据对象,
如果有就直接把那个数据对象的引用地址
放到变量的储存单元中,如果没有就在String常量池
中新进一个数据对象.
所以,步骤1
就在String常量池
中新建了一个数据对象”a”,而步骤2
就去String常量池
中找是否有”a”数据对象,
这时找到了为”a”的数据对象,就把String常量池
中数据对象”a”的引用地址
放入a2
变量的储存单元中,
这时a1
和a2
的储存单元中放入的就是相同的数据(引用地址),所以他们指向的都是String常量池
中为”a”的数据对象,所以步骤5
输出true.
步骤3
和步骤4
都是通过new
来创建的数据对象,他们都在堆内存中分配了一块内存空间,有着不同的引用地址
,他们指向的数据对象,也是堆内存中不同的数据对象.
所以,步骤6
和步骤7
输出都为false.
equals
1 | String a1 = "a"; |
我们先来看String
类的equals
方法.
1 | private final char value[]; |
我们可以看出如果==
比较为true,他们的equals
也一定返回true.
如果==
为false,表示两个引用变量的储存单元中的数据(引用地址)不同,也就是指向了不同的数据对象.
先判断后者是否为String类型,如果不是直接返回false,双引号的字符串默认是String类型的,所以为true.
先把第二个变量转换为String
对象,如果两个字符串的长度不相同的话,也返回false,那是一定的,
如果长度相同,再比较数据对象中的value
数组,也就是char
数组中的值是否相同,不相同也返回false,相同才能为true.
到这里,String类型的比较也讲解完毕了,下面我们做个总结.
总结
==比较
比较的是两个引用地址
是否相同(注意点就是创建对象的方式,有一个为new
创建的就为false,否则为true)equals比较
如果==比较
为true,equals比较
也一定为true.
如果==
为false,那就查看后者是否为String
类型,不是为false,是的话再把后者强转为String
对象,最后比较两个String
对象中的char
数组中的数据是否一致.