参数传递

思考问题

第一节 思考问题

  当String作为方法参数时,和包装类型一样都不会因为方法内的值改变导致原值变化。

  一直没有去深入思考其中的原因和细节,所以抽空通过简单的编程和知识回顾整理了类似过程的实现细节。


第二节 Demo

  首先通过编写简单的演示代码来观察整个过程:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class test {
public static void main(String[] args){
Integer a = 0;
Float b = 0.0f;
String c = "original";
Student student = new Student("001","张三","男",18,60.0f);

System.out.println("调用前: " + a);
System.out.println("调用前: " + b);
System.out.println("调用前: " + c);
System.out.println("调用前: " + student.toString());

System.out.println("---------------开始第一次调用");
change1(a);
change1(b);
change1(c);
change1(student);

System.out.println("第一次调用后: " + a);
System.out.println("第一次调用后: " + b);
System.out.println("第一次调用后: " + c);
System.out.println("第一次调用后: " + student.toString());


System.out.println("---------------开始第二次调用");
change2(a);
change2(b);
change2(c);
change2(student);
System.out.println("第二次调用后: " + a);
System.out.println("第二次调用后: " + b);
System.out.println("第二次调用后: " + c);
System.out.println("第二次调用后: " + student.toString());
}

private static void change1(Integer a){
a = 1;
System.out.println("函数内: " + a);
}

private static void change1(Float b){
b = 1.0f;
System.out.println("函数内: " + b);
}

private static void change1(String c){
c = "changed";
System.out.println("函数内: " + c);
}

private static void change1(Student student){
student.setId("002");
student.setName("小兰");
student.setSex("女");
student.setAge(17);
student.setScore(100.0f);
System.out.println("函数内: " + student);
}

private static void change2(Integer a){
a = Integer.valueOf(1);
System.out.println("函数内: " + a);
}

private static void change2(Float b){
b = Float.valueOf(1.0f);
System.out.println("函数内: " + b);
}

private static void change2(String c){
c = new String("changed");
System.out.println("函数内: " + c);
}

private static void change2(Student student){
student = new Student("003","小明","男",17,60.0f);
System.out.println("函数内: " + student);
}
}

  测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
调用前: 0
调用前: 0.0
调用前: original
调用前: Student{id='001', name='张三', sex='男', age=18, score=60.0}
---------------开始第一次调用
函数内: 1
函数内: 1.0
函数内: changed
函数内: Student{id='002', name='小兰', sex='女', age=17, score=100.0}
第一次调用后: 0
第一次调用后: 0.0
第一次调用后: original
第一次调用后: Student{id='002', name='小兰', sex='女', age=17, score=100.0}
---------------开始第二次调用
函数内: 1
函数内: 1.0
函数内: changed
函数内: Student{id='003', name='小明', sex='男', age=17, score=60.0}
第二次调用后: 0
第二次调用后: 0.0
第二次调用后: original
第二次调用后: Student{id='002', name='小兰', sex='女', age=17, score=100.0}

  根据结果可以发现Main函数中定义的包装类型在调用函数中修改后,其value是不变的,而抽象对象的成员变量则改变了。所以String也可以看作是包装类型,它有点像是char[]的包装类。

  包装类作为方法参数调用时应该和普通对象一样,传递的同样是引用。那么为什么最终值却没有变化呢?

  绘制如下过程:

分析运行过程:

  1. 首先在main函数中定义了成员变量Integer a,String c,Student student等,并赋值0和”original”等,存放引用于栈中,值则分别存于堆的对应区域。
  2. 分别调用函数change1,作为形参传入参数,然后分别赋值1和” changed”,change执行到}结束,存放于栈中的局部变量生命结束并销毁,而跳回main函数,栈内对应区域可以看作被清除,所以只有抽象对象student因为X001内存中对应的值被修改导致main函数中也改变了。
  3. 分别调用函数change2,可以发现执行新建对象,只是在change2函数内改变引用,其作为局部变量只存活到函数执行结束,而不会影响main函数中对象引用。

第三节 相关知识

3.1 包装类

基本类型 包装类型
byte Byte
int Integer
short Short
long Long
float Float
double Double
boolean Boolean
char Character

3.2 基本数据类型和引用类型

  两者存储位置不同,基本数据类型根据其作为成员变量或局部变量分别存在堆和栈中,引用类型则同时占用堆和栈,数据存在堆中,堆地址存于栈中。

3.3 Java 变量

再回顾一下变量的作用域:

  • 成员变量:整个类
  • 局部变量:定义的方法体或语句块中,执行完方法出栈后就无效了。
    成员变量可以分为实例变量和类变量/静态变量/全局变量
  • 实例变量:对象私有
  • 类变量/静态变量/全局变量:static修饰,不和对象关联,对象共用

3.4 局部变量

  方法结构如下,方法中的参数变量可以叫做形参,即局部变量,在方法 / 代码块 被线程调用时创建(入栈),执行结束后即销毁。可以简单认为局部变量只能在方法 / 代码块 内被感知到。

1
2
3
4
5
6
[方法修饰符] <返回值类型> <方法名> ([<参数列表>]) {
方法体
}
public void main (String[] args){
...
}

3.5 值传递和引用传递

Java中两种数据类型:

  • 基本数据类型,值存在变量中
  • 引用数据类型,引用存在变量中,引用指向实际对象

  所以当对两种类型的变量进行赋值操作时,基本数据类型变量所保存的值被覆盖,引用数据类型变量则是引用地址被覆盖。

  值传递是指调用函数时将实参复制一份传递到函数中,这样若在函数中对参数进行修改,将不会影响到实参。

  引用传递则是指调用函数时直接将实参的地址直接传递到函数中,这样在函数中对参数进行的修改,将影响到实参。

  值传递和引用传递应该是根据是否会创建副本,以及是否会影响到原始对象来判断。

值传递 引用传递
创建副本 ×
影响原值 ×

3.6 参数传递的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void main (String[] args){
int a = 1;
method1(a);
System.out.println(a);
Student student = new Student("001","张三","男",18,60.0f);
method2(student);
System.out.println(student);
}

public void method1 (int value){
value++;
}

public void method2 (Student value){
student.setId("002");
student.setName("小兰");
student.setSex("女");
student.setAge(17);
student.setScore(100.0f);
}

  main函数中调用方法method1,传入实参a=1,变量value由1进行初始化,并进行自增操作,值变为2,函数执行结束,局部变量value销毁。

  此时对于原调用者main来说,局部变量的变化是无从感知的,所以变量a值仍为1,最后控制台输出结果为1。

  main函数中调用方法method2,传入student对象地址,函数内分别对student的各个成员属性进行修改,其结果直接影响到堆中所存对象数据,所以虽然局部变量value依旧被销毁了,但对于原调用者main来说,变化已经造成了影响,所以打印出来的是”小兰”。

3.7 一些数据类型方法源码

3.7.1 Integer

  在Demo中通过Integer.valueOf()创建整型实例。

1
2
3
4
5
6
7
8
private final int value;

public static Integer valueOf(int i) {
//如果i的值大于-128小于127则返回一个缓冲区中的一个Integer对象,否则返回 new 一个Integer 对象。结果都导致了a指向了一个新的内存空间。
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

3.7.2 Float

  在Demo中通过Float.valueOf()创建整型实例。

1
2
3
4
5
private final float value;

public static Float valueOf(float f) {
return new Float(f);
}

3.7.3 String

  String在赋值改变时,和包装类一致,即都会指向新的对象。

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
@Stable
private final byte[] value;

public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}

public static String valueOf(char data[]) {
return new String(data);
}

public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}

public static String valueOf(boolean b) {
return b ? "true" : "false";
}

public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}

public static String valueOf(int i) {
return Integer.toString(i);
}

public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

  所以在Demo中除了Student以外所有函数都是在构造新的对象并赋值给局部变量。

3.8 所有引用类型都会被调用函数影响值吗?

  总结一下就可以知道,并非所有的引用类型都会因函数内的变化而使值被改变(其实是全部,被影响的案例并非是影响实参,而是修改其成员属性),需要根据具体情况来分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void main (String[] args){
String s = "ori";
method3(s);
System.out.println(s);
StringBuilder sb = new StringBuilder("ori");
method4(sb);
System.out.println(sb);
sb = new StringBuilder("ori");
method5(sb);
System.out.println(sb);
}

public static void method3 (String value){
value = "changed";
}

public static void method4 (StringBuilder value){
value.append("changed");
}

public static void method5 (StringBuilder value){
value = new StringBuilder("changed");
}

  结果如下。

1
2
3
ori
orichanged
ori

由以上案例结合Demo,可以总结出:

  • 被影响值的抽象对象,赋值操作的对象不是参数变量,而一般是对象内的成员变量。
  • 不被影响值的抽象对象,大都是直接对参数变量进行赋值操作。

第四节 总结

4.1 Java中只有值传递

  根据值传递和引用传递的定义,Java中应该只有值传递。因为不管形参是哪种数据类型,最终传递给形参的都是实参的拷贝,函数对形参的赋值操作也不能被调用者感知。

  这种设计其实对应着求值策略中的传共享对象调用

4.2 Java求值策略

  Java中的传递其实是传共享对象调用(Call by sharing),传递的实际上是实参地址的拷贝(对于基本类型,拷贝的是值),但都相当于生成了一份变量副本,最终直接的赋值操作都不会影响到原值。

  对于调用者而言,在被调用函数里修改参数是没有影响的。如果要达成传引用调用的效果就需要传一个共享对象,一旦被调用者修改了对象,调用者就可以看到变化(因为对象是共享的,没有拷贝)