在Java中,数组是按值传递还是按引用传递?

243
在Java中,数组不是原始类型,但它们也不是对象,所以它们是按值传递还是按引用传递呢?这是否取决于数组包含的内容,例如引用或原始类型?

18
数组是对象,但在Java中没有任何东西是通过引用传递的。所有参数传递都是按值传递的。对于一个对象,在传递时传递的是该对象的引用(即指针),而不是引用本身。将引用按值传递并不等同于通过引用传递。 - aroth
你可能会发现这个有用:https://dev59.com/d2ox5IYBdhLWcg3wIRD3#9404727 - Eng.Fouad
我无法对这个问题做出回答,但我写了一段代码片段,可能有助于理解下面的答案: https://write.as/1wjcm7m50w41k.md。 - myx
Java 不考虑引用,其他语言则会。 - Sam Ginrich
7个回答

313

Java中的所有内容都是按值传递的。在数组的情况下(数组本质上是一个对象),数组引用被按值传递(就像对象引用被按值传递一样)。

当您将一个数组传递给其他方法时,实际上是复制对该数组的引用。

  • 通过该引用更改数组内容会影响原始数组。
  • 但更改引用以指向新数组不会更改原始方法中的现有引用。

请参见此帖子:Java是“按引用传递”还是“按值传递”?

请查看此工作示例:

public static void changeContent(int[] arr) {

   // If we change the content of arr.
   arr[0] = 10;  // Will change the content of array in main()
}

public static void changeRef(int[] arr) {
   // If we change the reference
   arr = new int[2];  // Will not change the array in main()
   arr[0] = 15;
}

public static void main(String[] args) {
    int [] arr = new int[2];
    arr[0] = 4;
    arr[1] = 5;

    changeContent(arr);

    System.out.println(arr[0]);  // Will print 10.. 
  
    changeRef(arr);

    System.out.println(arr[0]);  // Will still print 10.. 
                                 // Change the reference doesn't reflect change here..
}

6
为什么不直接说数组是按引用传递的? - David Bandel
1
因为这不是真的。按引用调用意味着不同的东西。 - Stephen C
没有回答这个话题。问题是,数组是否是对象。 - Sam Ginrich

174
你的问题基于一个错误的前提。
数组在Java中既不是原始类型,也不是对象... "
事实上,Java中的所有数组都是对象。每个Java数组类型都有java.lang.Object作为其超类型,并继承了Object API中所有方法的实现。
所以它们是按值传递还是按引用传递呢?它是否取决于数组包含什么,例如引用或原始类型?
简短的答案:1)按值传递,2)没有区别。
更长的答案:
像所有Java对象一样,数组是按值传递的...但这个值是对数组的引用。因此,当您在被调用的方法中将某些内容分配给数组的单元格时,您将分配给调用者看到的同一个数组对象。
这不是按引用传递。真正的按引用传递涉及传递变量地址。使用真正的按引用传递,被调用方法可以分配给其本地变量,并导致调用者中的变量更新。
但在Java中不是这样的。在Java中,被调用方法可以更新数组内容,也可以更新其数组引用的副本,但无法更新保存调用者数组引用的变量。因此...Java提供的不是按引用传递。
以下是一些链接,解释了按引用传递和按值传递之间的区别。如果您不理解我上面的解释,或者如果您倾向于反对该术语,请务必阅读它们。

相关的SO问题:

历史背景:

“传引用调用”一词最初是“按引用调用”,它用于区分FORTRAN(按引用调用)和ALGOL-60(按值调用和按名称调用)的参数传递语义。

  • 在按值调用中,参数表达式被计算为一个值,并将该值复制到被调用的方法中。

  • 在按引用调用中,参数表达式部分被计算为“左值”(即变量或数组元素的地址),并将其传递给调用方法。调用方法可以直接读取和更新变量/元素。

  • 在按名称调用中,实际参数表达式被传递给调用方法(!!),该方法可以多次评估它(!!!)。这很难实现,并且可能被用来编写非常难以理解的代码。按名称调用仅在Algol-60中使用(谢天谢地!)。

更新

实际上,Algol-60的按名称调用类似于将lambda表达式作为参数传递。问题在于这些不完全是lambda表达式(在实现层面上被称为“thunks”)可以间接地修改在调用过程/函数中范围内的变量状态。这就是使它们难以理解的部分之一(例如,请参见维基百科页面Jensen's Device)。

1. 链接的问答中(Java中的数组及其在内存中的存储),没有任何说明或暗示数组不是对象。


1
那不是引用。很抱歉,但我认为继续这个讨论没有任何意义。我对“Java是按引用传递”这个争论不感兴趣。如果你想写自己的答案,随便。但我将请求管理员将这些评论从此答案中删除,因为它们“不再需要”。 - Stephen C

65

数组实际上是对象,因此传递的是引用(引用本身是按值传递的,感到困惑了吗?)。这里有一个快速的例子:

// assuming you allocated the list
public void addItem(Integer[] list, int item) {
    list[1] = item;
}

您将会看到在代码调用时列表的变化。然而,您不能更改引用本身,因为它是按值传递的:

// assuming you allocated the list
public void changeArray(Integer[] list) {
    list = null;
}
如果你传递一个非空的列表,那么在方法返回时它不会为空。

不,Java 中的所有内容都是按值传递的!按引用传递在 Java 中不存在,因为它在 ANSI C 中也不存在,这就是为什么指针存在的原因... - aleroot
2
@aleroot:我说的是将引用传递给方法,否则你就看不到变化,而不是Java是按引用传递的!是的,引用是按值传递的,但这不是重点。 - Tudor
2
@Tudor,你的句子不太清楚... - aleroot
@aleroot:好的,我已经添加了更多的注释... - Tudor
2
“然而,您无法更改引用本身,因为它是按值传递的。” - 实际上,您可以(在本地)更改引用。您无法更改的是调用上下文中提取引用的变量。只有当人们混淆引用和保存引用的变量时,才会感到困惑。 - Stephen C

13

不,那是错误的。在Java中,数组是特殊的对象。因此,传递数组时就像传递其他对象一样,传递的是引用值而不是引用本身。这意味着,在被调用的程序中更改数组的引用不会反映在调用程序中。


谢谢。那么每个数组访问都必须被解引用吗?这是否意味着在Java中使用数组与使用任何其他类型的列表一样慢,除了您可以在其中存储不需要被解引用的基本类型? - Froskoy
不行,因为堆中的数据存储是连续的,这意味着在 CPU 时间方面进行迭代查找要便宜得多。而 List 不保证连续存储。 - noisesmith
这就像传递其他对象一样,你传递的是引用的值,而不是引用本身。这种说法有两个误导/错误之处。1)在这方面,数组并不是“特殊对象”。它们与非数组对象的参数传递语义完全相同。2)“引用的值”和“引用”是同一回事。你应该说的是,你传递的是引用的值,而不是引用所指向的对象的值。 - Stephen C
数组更快,但不是因为“列表不能保证连续存储” - 它们更快是因为它们的API形状、内存、访问和实现都直接构建在语言中(而不是动态的)。Java规范已经定义了它们的存储方式,并定义了访问它们的方法。访问不涉及调用诸如#get#set#iterator#size等方法的开销。你可以理论上将int[]实现为一个对象,使用一块连续的内存等,并编写一个编译器来使用它代替本地数组。它仍然比本地数组慢得多。 - AndrewF

5
该文讨论了数组的定义,详见http://docs.oracle.com/javase/specs/jls/se5.0/html/arrays.html#27803。Java数组是对象,其类在10.8中定义。语言规范的第8.4.1节http://docs.oracle.com/javase/specs/jls/se5.0/html/classes.html#40420描述了如何传递方法参数。由于Java语法源自C和C++,因此行为类似。原始类型按值传递,与C一样。当传递对象时,通过值传递对象引用(指针),反映了C语法通过值传递指针的方式。请参见4.3.1http://docs.oracle.com/javase/specs/jls/se5.0/html/typesValues.html#4.3

实际上,这意味着在方法中修改数组内容会反映在调用作用域中的数组对象中,但在方法内部重新分配新值的引用对调用作用域中的引用没有影响,这正是您期望在C中结构体指针或C ++对象的指针的行为。

至少部分术语混淆源于在C普及之前的高级语言历史。在先前的流行高级语言中,直接通过地址引用内存是尽可能避免的事情,这被认为是语言提供抽象层次的工作。这使得语言必须明确支持从子例程(不一定是函数)返回值的机制。正式提到“按引用传递”时,指的是此机制。

当引入C时,它带有一个简化的过程调用概念,其中所有参数都是仅输入的,而唯一返回给调用者的值是函数结果。但是,通过显式和广泛使用指针,可以实现传递引用的目的。由于它具有相同的目的,因此将指针作为对值的引用传递的做法通常俗称为按引用传递。如果例程的语义要求通过引用传递参数,则C的语法要求程序员明确传递指针。通过值传递指针是在C中实现按引用传递语义的设计模式。

由于在 C 语言中,裸指针常常似乎只是为了制造崩溃性错误而存在,随后的发展,尤其是 Java,试图回归更安全的参数传递方式。然而,C 的主导地位使得开发人员需要模仿熟悉的 C 代码风格。结果是通过引用类似于指针传递,但实现更多保护以使其更安全。另一种选择可能是 Ada 等语言丰富的语法,但这将呈现出不受欢迎的学习曲线,并减少 Java 的采用率。
简而言之,Java 中对象(包括数组)的参数传递设计基本上是为了服务于按引用传递的语义意图,但是实现却采用了按值传递引用的语法。

1
“Java的语法源自C和C++,但行为并不相似。” - 胡说!类似的语法并不意味着相似的语义。 - Stephen C
我引用了旧规范,因为它仍然是正确的,而且我不知道OP正在使用哪个版本。参数传递在8.4.1中描述如下:当方法或构造函数被调用(§15.12)时,实际参数表达式的值将初始化新创建的参数变量,每个声明的类型,在方法或构造函数的主体执行之前。在DeclaratorId中出现的标识符可以在方法或构造函数的主体中用作简单名称,以引用形式参数。 - Steven McGrath
关于语法,Java、C和C++之间的相似性远非偶然,设计旨在为C和C++程序员提供过渡的便利。语言设计是人类交流的问题,而不是数学严谨性的问题,将熟悉的语法与陌生的语义混合在一起会造成不必要的复杂性。我们致力于打造一个易于采用的系统。 - Steven McGrath
你没有理解我的观点。我相信你知道有些相关的语言具有相同的语法,但是语义不同。我的观点是,无论这些语言是否相关,相同的语法并不意味着相同的语义。 - Stephen C
我理解你对Fortan,Algol和C的看法,但这些语言的语法与Java和C ++相比并不那么相似。也许我对历史的接近使我无法看到这一点。 - Steven McGrath
显示剩余7条评论

4

Java中的所有内容都是按值传递的。

对于数组,引用被复制到一个新的引用中,但请记住,在Java中,所有内容都是按值传递的。

阅读这篇有趣的文章,获取更多信息......


1

有点像房地产的技巧...即使在Java中,引用也是按值传递的,因此对于引用本身的更改仅限于被调用函数级别。编译器和/或JVM通常会将值类型转换为引用。


我认为Kernighan和Ritchie觉得将指针称为值。在这种思维的继承中,Java是面向引用而不是面向对象的,这是理解的关键。 - Sam Ginrich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接