值传递与引用传递

几个误区

  1. Java是引用传递.

  2. 值类型是值传递, 引用类型是引用传递.

  3. 所有的都是值传递, 因为引用本质上只有个值, 本质上是指针.

  4. 声明的参数是引用类型, 就是引用传递; 声明的参数是一般类型或者指针的就是值传递.

求值策略(Evaluation Strategy)

首先我们要知道, 值传递和引用传递是一种求值策略(Evaluation Stragtegy), 表示的是调用函数的时候, 对于参数传递方式的描述, 而不是对参数本身类型的描述. 值类型和引用类型是两种内存分配方式, 值类型是在调用栈上分配, 而引用类型是在堆上分配. 一个是描述的内存分配方式, 一个是描述参数求值策略, 二者并无依赖和约束关系.

当我们进行函数调用的时候, 为函数所提供的实参, 可以是常量, 也可以是变量, 甚至可以是其他函数的返回值, 但这些实参的形式都称之为表达式, 求值就是对表达式化简并求解值的过程.

求值策略关注的点在于, 表达式在调用函数的过程中, 求值的实际, 值的形式的选取等问题. 求值的时机, 可以在函数调用之前, 也可以在函数调用之后, 由被调用者自己求值. 这里的调用后求值, 可以理解为lazy load.

我们根据求值时间和传值方式, 对不同的求值策略进行分类:

求值策略 求值时间 传值方式
值传递(pass by value) 调用前 值的结果(原值的副本)
引用传递(pass by reference) 调用前 原值(原始对象, 不生成副本)
名传递(pass by name) 调用后(用到后求值) 与值无关的一个名

值传递与引用传递的区别

我们重点看一下值传递和引用传递的区别, 首先是二者在行为表象上的区别:

- 值传递 引用传递
根本区别 会创建副本 不创建副本
所以 函数中无法改变原始对象 函数中可以改变原始对象

这里所说的改变, 是指把一个变量指向另一个对象, 而不是仅仅改变属性或者成员. 因此我们说Java是值传递, 因为调用时会发生copy, 实参不能指向另一个对象, 而不是说被传递东西的本质是一个value, 毕竟计算机里什么都是value.

因此我们知道, 这些行为与参数本身是值类型还是引用类型无关. 对于值传递, 无论是值类型还是引用类型, 都会在调用栈上创建一个副本, 不同的是, 对于值类型而言, 这个副本本身就是原始值的全部复制, 而对于引用类型而言, 由于引用类型的实例在堆上, 所以栈上只有他的一个引用, 其副本也是这个引用的复制, 而不是整个对象的复制.

因此值类型和引用类型的最大区别在于, 值类型作为参数被复制, 但是这不是值类型的特性, 只是值传递带来的效果, 和值类型本身没有关系.

综上所述, 我们对Java的函数调用方式可以描述为: 参数是通过值传递的方式, 传递的值是一个引用的拷贝.

我们可以通过C++来表现值传递和引用传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void ByValue(int a) {
a = a + 1;
}
void ByRef(int& a) {
a = a + 1;
}
void ByPointer(int* a) {
*a = *a + 1;
}
int main(int argv, char** args) {
int v = 1;
ByValue(v);
ByRef(v);
// Pass by Reference
ByPointer(&v);
// Pass by Value
int* vp = &v;
ByPointer(vp);
}

值得注意的是, 从行为考虑, 才是求值策略的本意. 如果把所有东西都抽象成值, 从数据考虑问题, 那就根本没有必要来引入求值策略这一概念了.

在C#中, 可以通过来ref/out来实现引用传递, 没有ref/out就是值传递.

几个解释

针对于上文的几个误区, 我们来一一进行解释:

  1. 引用传递是指的求值方式, 而不是说Java的参数类型是对对象的引用. 并且Java在函数调用时, 是通过值传递的方式, 传递引用的副本.

  2. 一个是描述的内存分配方式, 一个是描述参数求值策略, 二者并无依赖和约束关系.

  3. 从行为考虑, 才是求值策略的本意. 如果把所有东西都抽象成值, 从数据考虑问题, 那就根本没有必要来引入求值策略这一概念了.

  4. 同2.

Java foreach

Java中的foreach是对迭代到当前的对象进行完全拷贝, 而不是获得他的引用拷贝.

Syntax:

1
2
3
for (type var : array) {
statements using var;
}

等同于:

1
2
3
4
for (int i = 0; i < arr.length; i++) {
type var = arr[i];
statements using var;
}

因此对当前的对象改动, 并不会改变容器中的实际值, 这是值得注意的.