今天看到一面试题,对于输出结果为 0 有很多一知半解的人解释,
对于新手来说看得似懂非懂,然后看完还是一头雾水。
@Test
public void test1(){
Integer i = new Integer(0);
//Integer@853 -----1
add(i);
//Integer@853 -----5
System.out.println(i);//0
i +=3;
//Integer@864 -----6
System.out.println(i);//3
}
private void add(Integer i) {
//Integer@853 -----2
i = i + 3;
//Integer@864 -----3
i = new Integer(i);//3
//Integer@865 -----4
}
在代码上我都标注了 i 的各步骤的引用地址。
从调试信息上来看,方法传递的就是对象的地址。
而让新手迷惑的关键地方是,add 方法中改变了 i 的值啊,为什么还是返回 0 ?
Integer 的加法运算生成了一个新的 Integer 对象,并申明为变量 i,而局部变量的生命周期只存在自己的方法中,两个方法中的变量名都为 i,但是此时他们已经没有关系了。
不知道解释的是否正确,希望错误的地方各位指正,以免让别人产生误解。
add方法的形参i是局部变量,从这个角度看,确实是test方法的i地址的copy。
思考了一下值传递和引用传递的本质:
class A:
Integer i ;
i= new Integer(0);
一个对象A的初始化,会在内存堆中申请一些内存空间,并会明确一些地址信息
i在哪里:指向class A实例化对象的meta存着的地址;i是什么类型:指向Integer类型地址;i是什么值:指向new Integer(0)申请的内存地址。
值传递,也就是说把i的值引用地址作为值,赋给add方法的形参i,那么此时test和add方法的关联就是,都有一个局部变量i,值引用同一个内存地址。
更正
生命周期的说法是不正确的,局部变量的生命周期在压栈至出栈时期内。并且局部变量开辟的内存地址在栈内存中。
1
sagaxu 2018-07-20 00:40:12 +08:00 via Android
这是 Java 吗? Java 方法调用是没有传引用的,都是传值。
|
2
zjp 2018-07-20 00:47:49 +08:00 via Android 1
都打印出 Integer@853 只是说明 test1()中的 i 和 add()中的 i 指向同一个地址,后者是前者的副本,依然为传值。我觉得不要引入"传引用"这个概念更清晰
|
3
lhx2008 2018-07-20 00:51:44 +08:00 via Android 1
很简单,传进函数里面的会复制一份引用,就是 int b = a,你再改 b=1,因为 a 本身不可变,所以没用,a 不会变,但是如果 a 是可变的对象,再引用是可以改里面的内容的
|
4
lhx2008 2018-07-20 00:54:15 +08:00 via Android 1
a 不可变的话,改 b 等于是重新赋值,a 可变,那可以改 a 里面的内容不用重新赋值
|
5
lhx2008 2018-07-20 00:58:23 +08:00 via Android 1
关键在于你有没对函数的参数变量重新赋值,没有的话是可以改对象内容的
|
6
tsaohai 2018-07-20 01:41:23 +08:00 via iPhone
test1 里边的 i 在 add 调用前后不会改变,add 里边那个 i 的 copy 变了。
|
7
starcraft 2018-07-20 07:31:21 +08:00 via iPhone
这是书看少了啊。有点名气的外国佬的书,都提到了 java 值传递特性吧。
|
8
araraloren 2018-07-20 08:22:26 +08:00
C 系列语言的参数传递不都是传值的么。。
|
9
unforgiven 2018-07-20 09:09:07 +08:00
@araraloren c++有引用传递
|
10
unforgiven 2018-07-20 09:13:24 +08:00
你的 add 方法,实际传入的 i 其实是参数的内存地址的值得拷贝
|
11
araraloren 2018-07-20 09:14:29 +08:00
@unforgiven :) 莫名就忽略了它。。。
|
12
lux182 OP @unforgiven 怎么解释指向相同的地址呢
|
14
saberpowermo 2018-07-20 09:25:10 +08:00 via Android
java 的‘’引用‘’传递 传递的是这个引用的 内存地址值
|
16
raysonx 2018-07-20 09:33:14 +08:00 via Android
@hugedata C#我了解一些,参数前加 ref 或者 out 关键字才是传引用,否则都是传值。传引用比如:
void swap(ref int a, ref int b) { int t = a; a = b; b = t; } 调用时,两个变量的赋值会被调换: int x = 3; int y = 5; swap(ref x, ref y); |
17
Jarvix 2018-07-20 09:33:53 +08:00
这是 java 吗?
public static void main(String[] args) { Integer i = new Integer(0); i = add(i); System.out.println(i);//0 } private static Integer add(Integer i) { i = i + 3; i = new Integer(i);//3 return i; } } 好像这样才行。(我也是上周才开始学的 java )逃…… |
18
WhyAreYouSoSad 2018-07-20 09:34:24 +08:00 1
赞成楼上 2,3 楼说的,其实不要用引用传递这种说法,因为还是值传递的操作影响。我读书时有想过这个问题。
其实在面向对象里面,所谓的引用传递他是传递一个一级指针(不能改变该对象指向的空间,只能改变该对象成员的值),而所谓的值传递,就是传值。 如果用 c 语言表达的话就是,如果类要改变指向,那么就必须传一个二级指针过去。而基础类型要改变指向就传一个一级指针,要把一个类当成一个不定长的一维数组。 |
19
raysonx 2018-07-20 09:36:36 +08:00 via Android
楼主的例子中,add 函数的参数前没加 ref,当然是传值。后续对 i 重新赋值不会影响调用方的变量的值。
另外,这里 i 的类型用 int 还是 Integer 效果上没有什么区别,不知道楼主为何要用 Integer。 |
20
alamaya 2018-07-20 09:37:42 +08:00
可以这么理解 java 值传递传的是指针地址的 copy
|
21
LINWAYNE 2018-07-20 09:38:21 +08:00
Integer 自动拆箱装箱了解一下
|
22
raysonx 2018-07-20 09:39:30 +08:00 via Android
我猜困惑来自于所谓的值类型和引用类型,后者类似于 C C++的指针。但这个概念和值传递、引用传递没有关系。
|
23
sagaxu 2018-07-20 09:39:48 +08:00 via Android 1
内存是货柜,一个单元就是一个抽屉,变量是写了货柜抽屉编号的卡片,int 这种 primitive 类型,直接记卡片上,不放抽屉里。而 Integer 仍然放抽屉,卡片上只记编号,自动装箱拆箱。
函数调用的时候,假设有个变量 a,在 f(a)的时候,会另外拿一张卡片 b,抄好 a 卡片里的抽屉编号,f 函数体执行的时候,拿到的是 b 卡片,抽屉编号跟 a 一样,它可以去读写这个抽屉,但是改变不了 a 卡片。 变量赋值,只是把卡片擦了重写抽屉号码。 |
24
raysonx 2018-07-20 09:44:05 +08:00 via Android
好吧楼主问题中的语言是 Java,不是 C#(谁让两个语言那么像呢)。
Java 中根本没有引用传递,所有的函数调用都是值传递。 |
25
StephenDev 2018-07-20 09:44:10 +08:00
卧槽,我刚才算半天发现怎么算都不对,然后我仔细再看了一下,发现我看错了。
我以为你那个 Integer@xx---1 后面跟着的这个数字是 Integer 对象当前的值。。。。。 我人晕了。 |
26
98jiang 2018-07-20 10:08:38 +08:00
你把 i 传过去了,但是没有返回回来所以还是 0 呀?看上面的人说的,应该就是只是值传过去了,并不会影响那个变量。
|
28
joshu 2018-07-20 10:38:16 +08:00
Integer 是不可变量,Integer i=0;i+=3;执行到这一步时,i 的地址已经不是原来的地址了,数不是原来的那个数了。
而 java 传引用,在子函数里修改 Integer 实际上是新建了一个 Integer 对象覆盖到这个名字上,而不是修改引用的那个对象。 因此主函数里这个数不变。 另外 Integer 对于常用的、较小的数有 cache,可以看看源码。 |
29
jzq526 2018-07-20 10:40:46 +08:00
我觉得楼主的说法是正确的,但也不完全正确。C 语言和 Java 语言的参数传递是一样的。但为什么 C 里面分了个值传递和地址传递(引用传递)呢?看内容。从形式上,都是把实参的值复制给了形参,然后形参带入到函数中运算。从内容上看,如果实参是个基本数据类型,那就变量本身就保存值,所以复制给形参的也就是这个变量的值,这就叫做值传递;如果实参是个复合数据类型,比如数组,结构体,Java 中的对象等,实参只保存了真实对象的地址,复制给形参的也是这个对象的地址,这就是地址传递或者引用传递。
所谓值传递和引用传递,形式上一样的,但内容是不同的。 楼主这个问题在哪里?我认为主要是包装类的机制造成的。add 方法中的参数 i,一开始获取的确实是实参的地址,调用的也的确是实参的对象,但在“ i=i+1 ”这一行上,i+1 这个运算并不是在 i 原来的内存空间中进行的,而是将结果放到了另一个空间中,也就是说,JVM 把结果存放到另一个对象中,地址在保存到形参 i 中,原来的空间就不管了。这也是为什么经过这一行程序后,i 的地址发生改变的原因。这个方法在 String 类对象用+号连接时也用了,好处就是能快一点,缺点就是频繁操作的话比较占内存。 所以,最后的结论就是,楼主以为 i=i+1 和普通的基本数据类型运算一样,运算结果会存放到原来的内存空间中,但 Java 没这么干,而是把结果存放到了另一个内存空间中再修改了对象名保存的地址值。 另外,Integer 类貌似没有提供修改自身值属性的方法,所以楼主只能想别的方法了。 |
30
zhujinliang 2018-07-20 10:50:13 +08:00 3
关于指向相同的地址
理解代码只是表达逻辑,最终实际的地址位置以及操作指令,还有编译、优化等等多层包装最终决定,不能想当然的认为计算机严格按照代码流程来做。 假设编译器将 add 函数内联,即将 add 代码片段拷贝到 test1 函数的对应位置,结果可以是这样: public void test1(){ Integer i = new Integer(0); // add(i); Integer add_i = i + 3; add_i = new Integer(add_i);//3 System.out.println(i);//0 i +=3; System.out.println(i);//3 } 对照来看,实际进入 add 函数的 i 还是原来的 i,只不过将 i+3 的结果存到了另外的地方,避免修改原来的 i 我们说值传递、引用传递,也只是为了方便理解和讨论。实际 CPU 做了什么,抄了哪些近道,我们在这个层面并不关心。 |
31
momocraft 2018-07-20 11:07:28 +08:00
都写 java 了就不要担心地址了,引入地址这个(只存在于 JVM 层的)要素只会让你更混乱
|
32
mx1700 2018-07-20 12:14:39 +08:00
如果把 Integer 换成 String 你能理解吗?
Integer 和 String 一样,都是不可变对象 |
33
sc13 2018-07-20 12:34:22 +08:00
java 只有值传递的,对象传递的是引用的复制的值
|
34
hyyou2010 2018-07-20 13:12:20 +08:00 1
目前为止,可能只有 @joshu 直接说出了关键
本质上只有两种传递。 值传递:function(int i),复制了一份 i 进去 指针传递:function(Object o),复制了一份 o 的地址进去 还真不知道 function(Integer i)是哪一种传递,按说应该是指针传递,也即,函数内部操作的 Integer 就是外部的 Integer 但根据 @joshu 提示搜了一下,Integer 内部还真是一个 final int value,所以 add 函数的 i=i+3 时会生成新 Integer,因此原先的 i 值,同时也是函数外部的 i 值不会被改变 但是,在外部函数的 i+=3 这一步,由于生成了新的 Integer,且 i 指向新的 Integer,所以打印 i 是打印的新 Integer,所以是新值 3 |
35
suixn 2018-07-20 14:22:37 +08:00
String、Integer,Long, Short, Double, Float, Character, Byte, Boolean 都是不可变的。
意思是只要改变了他们的值,地址映射就变了。 |
36
suixn 2018-07-20 14:27:02 +08:00
按你的代码:
```java public void test1(){ Integer i = new Integer(0); add(i); System.out.println(i);//0 } private void add(Integer i) { i = i + 3; } ``` 而其他类型,比如 map ```java public static void main(String[] args) { HashMap<String, String> map = new HashMap<>(2); System.out.println(map.size());//0 add(map); System.out.println(map.size());//1 } private static void add(HashMap<String, String> i) { i.put("a", "a"); } ``` |
39
suixn 2018-07-20 14:37:21 +08:00
@alamaya #37 另外,你说的是
public static void main(String[] args) { HashMap<String, String> map = new HashMap<>(2); System.out.println(map.size());//0 add(map); System.out.println(map.size());//1 } private static void add(HashMap<String, String> i) { i.put("a", "a"); i = new HashMap<>();//这样吗? } |
42
ShineSmile 2018-07-20 15:27:59 +08:00
@sagaxu #1 证据?
|
43
luopengfei14 2018-07-20 16:07:53 +08:00
看来楼主 C 语言没学好
虽然我是对着答案。。。看懂代码的 |
44
luopengfei14 2018-07-20 16:13:24 +08:00 1
Integer i = new Integer(0);
... private void add(Integer i) ... 虽然这两个引用(指针)指向同一个实例,但是只是两个指针,后来 add()中 i 的指向变了,也就不会改变原来指向的实例。 我的想法是这样。。。 |
45
johnj 2018-07-20 16:30:55 +08:00 1
首先 Java 里只有按值传递。所谓按值传递,遇到值类型,复制一份传进去;遇到引用类型,复制一份引用传进去。会出现多个引用指向同一个对象的情况:可以通过每个引用,来改变指向的对象的属性值;其中一个引用改变了指向的对象,对其他的引用没有影响。
|
47
unforgiven 2018-07-20 18:52:45 +08:00
@lux182 因为他是参数的内存地址的值得拷贝,所以在访问这个参数的时候指向的是同一个内存地址,当你修改这个参数 i 的时候,你修改的只是这个拷贝的值并不会影响到原来的值
|
48
unforgiven 2018-07-20 19:05:52 +08:00
@suixn 和可变不可变没有关系
@Test public void patchModuleRecord() { User user = new User("aaa","bbb"); testan(user); System.out.print(user.toString()); test(user); System.out.print(user.toString()); } public void test(User user){ user = new User("123","456"); } public void testan(User user){ user.userName = "ccc"; } class User{ String userName; String password; public User(String userName, String password){ this.userName = userName; this.password = password; } public User(){ } @Override public String toString() { return "User{" + "userName='" + userName + '\'' + ", password='" + password + '\'' + '}'; } } User{userName='ccc', password='bbb'}User{userName='ccc', password='bbb'} 我的 User 对象到是可变的,一样你在函数里修改不了 user 的引用,说到底还是 Java 的所谓引用 还是传递的内存地址的值,真不了解的话拿着 c 语言的指针去玩玩你就懂了 |