Fork me on GitHub

九.浅析Java中的参数传值机制(面试题)

前言

关于传值机制,之前自己再Java基础中了解过,但是昨天晚上却有了一些新的认知,所以记录一下,以便后期复习,今天我们以一道面试题作为开头来引导

一.面试题

在main中定义了两个Integer a和b ,通过swap方法,交换两个值,请写出swap方法

二.常规思路(仅仅是思路)

我们需要通过一个中间变量(temp)保存其中一个值,然后再进行交换,我们来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test_01 {
public static void main(String[] args) {
int a = 1;
int b = 2;

System.out.println("before swap a:" + a + "," + "b:" + b);
swap(a, b);
System.out.println("after swap a:" + a + "," + "b:" + b);

}
private static void swap(int num1, int num2) {
int temp = num1;
num1 = num2;
num2 = temp;
}
}

结果:

1
2
before swap a:1,b:2
after swap a:1,b:2

这个我们大家都知道,在 Java 中基本类型之间在参数中是指传递,也就是说,这里我们的 num1 和 num2 只是 main 函数中 a 和 b 的值,而并非 a 和 b 本身,我们在函数中改变的仅仅是 num1 和 num2 这个两个临时变量的值,对于 main 函数中 a 和 b 的值并没有什么影响

三.引用传递呢?

在Java中引用传递其实也是值传递,只是这个引用它本身保存的是此引用指向的内存地址,所以会将这个内存地址的值传过去,而我们在函数中操作此临时变量就相当于操作原来的引用,因为此时你是通过地址来操作啊,就是c语言中的指针

那我们来看看将 int 变为Integer呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test_02 {
public static void main(String[] args) {
Integer a = 1;//自动装箱 = Integer.valueof(1)
Integer b = 2;

System.out.println("before swap a:" + a + "," + "b:" + b);
swap(a, b);
System.out.println("after swap a:" + a + "," + "b:" + b);

}
private static void swap(Integer num1, Integer num2) {
Integer temp = num1;
num1 = num2;
num2 = temp;
}
}

这里会有一个自动装箱,后面解释。

打印结果

1
2
before swap a:1,b:2
after swap a:1,b:2

打印结果并没有改变,那这是为什么呢????

我们再来看一下String类型(这个之前尧哥面试还问过我,我都说错了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test_03 {
public static void main(String[] args) {
String a = "1";
String b = "2";

System.out.println("before swap a:" + a + "," + "b:" + b);
swap(a, b);
System.out.println("after swap a:" + a + "," + "b:" + b);

}
private static void swap(String num1, String num2) {
String temp = num1;
num1 = num2;
num2 = temp;
}
}

打印结果

1
2
before swap a:1,b:2
after swap a:1,b:2

还是之前的值,String和Integer不都是引用类型吗,为什么没有改变呢???

内存图解

image

image


也就是说,如果我们在函数中将传进来的引用类型给它赋一个新的值,那么其改变并不会影响外部引用的值,就像上面那样,那什么时候会在函数内部的改变会影响外面的值呢?我们在Java中不经常传自己定义的JavaBean对象进去吗???我们来看看

函数内的改变影响函数外的值的例子

自定义一个User对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class User{
int id;
String name;
String pwd;

//无参构造
public User() {
}

//带两个参数的构造
public User(int id, String name) {
this.id = id;
this.name = name;
}

public void testParameterTransfer01(User u){
u.name="风清扬";
}

public void testparametertransfer02(User u){
u = new User(200,"东方不败");
}
}

测试案例

1
2
3
4
5
6
7
8
9
10
11
12
public class Y_01_测试值传递 {
public static void main(String[] args) {
User user = new User(100,"张三丰");
System.out.println(user.name);

user.testParameterTransfer01(user);
System.out.println(user.name);

user.testparametertransfer02(user);
System.out.println(user.name);
}
}

我们来分析一下:

image
image

image

image


总结出什么呢?


1. 就是说当我们在函数内部直接给传进来的引用赋一个新的值的时候,这种影响并不会影响外面的引用的值,它仅仅是将函数中复制的那个临时变量给改了,并未改变函数外部的值比如下面这种


1
2
3
public void testparametertransfer02(User u){
u = new User(200,"东方不败");
}


2. 而当我们在函数内部改变引用对象内部的属性的值的时候,这种影响会扩散到函数外部,因为在函数内部复制的那个引用指向的跟函数外部指向的是一个地址,你改变其内部属性的值就是改变了外部指向的值,如下:

1
2
3
public void testParameterTransfer01(User u){
u.name="风清扬";
}


我们改变了传进来的User对象内部的值,那么外部的值也就被改变了

所以说在函数内部通过赋值的操作就不要想了,行不通的弟弟。

包装类分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test_03 {
public static void main(String[] args) {
String a = "1";
String b = "2";

System.out.println("before swap a:" + a + "," + "b:" + b);
swap(a, b);
System.out.println("after swap a:" + a + "," + "b:" + b);

}
private static void swap(String num1, String num2) {
String temp = num1;
num1 = num2;
num2 = temp;
}
}

现在我们分析出来,在函数内部直接交换是行不通的。

我们都知道八大基本数据类型都对应其一个包装类,int–>Integer,byte–>Byte等。当我们用一个包装类来接收一个基本类型的时候发生了什么呢?比如

1
Integer a = 1;

这里会发生自动装箱,先不考虑这个问题

这里这个Integer对象将这个1到底存到了哪里呢??

Integer源码
image

其实呢,Integer这个对象会将这个基本数据存放到其内部的一个 int value中

注意:这个value是final类型的,就是不可变

那么其他的包装类也一样。

String不是一个包装类,但是String是一个对象,在String对象这个内部有一个char[] 数组,String就是将值保存到了这个char[] 数组中的,而且这个char[] 数组也是final修饰的


既然我们都说了,我们给传进来的引用直接赋值是不行的,我们只能想法去改变其内部属性,对吧。也就是说我们要去改变这些包装类和String类内部真正保存数据的那个东西,就像这样:
1
2
3
public void testParameterTransfer01(User u){
u.name="风清扬";
}
但是,这些内部属性都是final修饰的,我们该怎么办?????

四.反射技术

看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 在main中定义了两个Integer a和b ,通过swap方法,交换两个值,请写出swap方法
*/
public class Test_01 {
public static void main(String[] args) {
Integer a = 1;//自动装箱 = Integer.valueof(1)
Integer b = 2;

System.out.println("before swap a:" + a + "," + "b:" + b);
swap(a, b);
System.out.println("after swap a:" + a + "," + "b:" + b);

}

private static void swap(Integer num1, Integer num2) {
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);

int temp = num1.intValue();

//将num1中的value属性设置为num2中的value属性
field.set(num1,num2);

System.out.println(temp);

field.set(num2,temp);

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

以上是我们使用反射技术来实现的交换,有没有什么问题

问题大了去了

打印结果:

1
2
before swap a:1,b:2
after swap a:2,b:2
只交换了一般,这是怎么回事呢???

分析一波

在我们执行下面这句代码的时候会发生一个自动装箱操作

1
2
Integer a = 1;//自动装箱 = Integer.valueof(1)
Integer b = 2;

而这个自动装箱操作内部呢,有一个Integer的缓冲数组,叫做IntegerCache
,IntegerCacheIntegerCache内部有一个已经初始化好的Integer[],而这个Integer[]就是我们的真正的缓存数组

image
image

这个IntegerCachelow的范围是-128high+127,也就说,如果我们这个穿进来的这个数如果大于-128且小于+127,那么就将已经初始化好的这个缓存数组的值给它,也就是说现在这个a和这个b指向的是这个缓冲数组中的的值。

现在我们比如执行Integer a = 1;那么这个就会返回 IntegerCache.cache[1+(-(-128))] = IntegerCache.cache[129]

而这个IntegerCache.cache[129]的值正好是1,就是人家帮咱们初始化好的,提高效率的。那么依次类推IntegerCache.cache[130]就等于2,对吧,这里可以看懂吧

那么我执行这段代码时

1
2
3
4
5
6
7
8

Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);

int temp = num1.intValue();

//将num1中的value属性设置为num2中的value属性
field.set(num1,num2);

Field field = Integer.class.getDeclaredField(“value”);获取到Integervalue属性

field.set(num1,num2);num1对象中的value值设置为num2中的value值,就是将IntegerCache.cache[129]的值设置为IntegerCache.cache[130](因为num2IntegerCache.cache[130]),所以如下

1
IntegerCache.cache[129] = num2 = 2 ===> IntegerCache[129]=2

在当我们执行下面代码时,对发生什么呢???

1
field.set(num2,temp);

num2中的value值设置为temp,也即将IntegerCache.cache[130]的值设置为temp,此时temp是一个基本类型,而我们的field.set()方法需要传进去两个Object类型,此时tempint类型,所以这里又会有一个自动装箱操作,即

1
Integer temp = Integer.valueof(temp);

即这样,temp中保存的是num1value

1
Integer temp = Integer.valueof(1);

此时它又去Integer的缓冲数组中去比较了对吧,于是返回了

1
Integer temp = Integer.valueof(1) = IntegerCache.cache[129]

而此时 IntegerCache.cache[129] 已经等于2了,我们之前分析的,所以这里

1
temp = Integer.valueof(1) = IntegerCache.cache[129] = 2;

所以只改变了一半,那怎么操作呢???

很简单

1
field.set(num2,new Integer(temp));

这样就ok了

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package yp.Java_Interview;

/**
* @author RickYinPeng
* @ClassName Test_01
* @Description
* @date 2019/1/28/20:09
*/

import java.lang.reflect.Field;

/**
* 在main中定义了两个Integer a和b ,通过swap方法,交换两个值,请写出swap方法
*/
public class Test_01 {
public static void main(String[] args) {
Integer a = 1;//自动装箱 = Integer.valueof(1)
Integer b = 2;

System.out.println("before swap a:" + a + "," + "b:" + b);
swap(a, b);
System.out.println("after swap a:" + a + "," + "b:" + b);

}

private static void swap(Integer num1, Integer num2) {
try {
Field field = Integer.class.getDeclaredField("value");
field.setAccessible(true);

int temp = num1.intValue();
System.out.println(temp);

field.set(num1,num2);// IntegerCache[129] = num2 = 2 ===> IntegerCache[129]=2

/* Integer bb = 1; // bb 到底是什么值? 值是2
System.out.println(bb);//打印的是 2,有意思*/

System.out.println(temp);

field.set(num2,new Integer(temp));// IntegerCache[130] = temp = ? = 2
/*
或者这样
field.setInt(num1,temp);
*/

} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}