Java是按值传递还是按引用传递?

7757

我一直认为Java使用的是按引用传递。然而,我看了一篇博客文章,声称Java使用的是按值传递。我不认为我理解作者所做的区分。

这是什么意思?


7
我们更常用的说法是,一个“按引用传递”的变量可以被改变。这个术语出现在教科书中,因为语言理论家需要一种区分原始数据类型(int、bool、byte)和复杂结构对象(数组、流、类)的方法,也就是说,那些可能具有无限内存分配的对象。 - jlau
7
我想提醒您,在大多数情况下不需要考虑这个问题。我在编程中使用Java很多年,直到学习了C++之前,我完全不知道传递引用和传递值是什么意思。直到那时,我的直觉解决方案总是有效的,这就是为什么Java是最适合初学者的语言之一。因此,如果您目前担心您的函数需要引用还是值,请按原样传递它,您会没问题的。 - Tobias
73
Java通过值传递引用。 - The Student
24
简单来说,这种混淆是因为在Java中,所有非基本数据类型都是通过"引用"来处理/访问的。然而,传递始终是按值进行的。因此,对于所有非基本类型,引用是按其值传递的。所有基本类型也都是按值传递的。 - Ozair Kafray
6
我觉得这篇文章非常有帮助: https://www.baeldung.com/java-pass-by-value-or-pass-by-reference - Natasha Kurian
显示剩余16条评论
95个回答

31

我创建了一个线程,专门用于这些问题的提问,适用于任何编程语言。

Java也被提到了。以下是简短的摘要:

  • Java按值传递参数
  • “按值传递”是在Java中将参数传递给方法的唯一方式
  • 使用作为参数给定的对象的方法将更改该对象,因为引用指向原始对象。 (如果该方法本身更改了某些值)

28

在Java编程语言中最大的困惑之一是Java是“值传递”还是“引用传递”。

首先,我们应该理解什么是“值传递”或“引用传递”。

值传递:方法参数的值被复制到另一个变量,然后传递复制的对象,这就是所谓的值传递。

引用传递:将实际参数的别名或引用传递给方法,这就是所谓的引用传递。

假设我们有一个类Balloon如下所示。

public class Balloon {

    private String color;

    public Balloon(){}

    public Balloon(String c){
        this.color=c;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }
}

我们有一个简单的程序,其中包含一个通用方法来交换两个对象,该类如下所示。

public class Test {

    public static void main(String[] args) {

        Balloon red = new Balloon("Red"); //memory reference 50
        Balloon blue = new Balloon("Blue"); //memory reference 100

        swap(red, blue);
        System.out.println("red color="+red.getColor());
        System.out.println("blue color="+blue.getColor());

        foo(blue);
        System.out.println("blue color="+blue.getColor());

    }

    private static void foo(Balloon balloon) { //baloon=100
        balloon.setColor("Red"); //baloon=100
        balloon = new Balloon("Green"); //baloon=200
        balloon.setColor("Blue"); //baloon = 200
    }

    //Generic swap method
    public static void swap(Object o1, Object o2){
        Object temp = o1;
        o1=o2;
        o2=temp;
    }
}

执行以上程序时,我们得到以下输出。

red color=Red
blue color=Blue
blue color=Red

如果你看一下输出的前两行,就会清楚地发现swap方法没有起作用。这是因为Java是按值传递的,在任何编程语言中都可以使用此swap()方法测试,以检查它是按值传递还是按引用传递。

让我们逐步分析程序的执行过程。

Balloon red = new Balloon("Red");
Balloon blue = new Balloon("Blue");

当我们使用new操作符创建一个类的实例时,该实例被创建并且变量包含了对象所保存的内存引用位置。在我们的示例中,假设"red"指向50,“blue”指向100,这些都是Balloon对象的内存位置。

现在,当我们调用swap()方法时,会创建两个新的变量o1和o2,分别指向50和100。

因此,下面的代码片段解释了swap()方法执行中发生的情况。

public static void swap(Object o1, Object o2){ //o1=50, o2=100
    Object temp = o1; //temp=50, o1=50, o2=100
    o1=o2; //temp=50, o1=100, o2=100
    o2=temp; //temp=50, o1=100, o2=50
} //method terminated

请注意,我们正在更改o1和o2的值,但它们只是“red”和“blue”的引用位置的副本,因此实际上,“red”和“blue”的值没有更改,因此输出结果不变。

如果您到这里已经理解了,那么您可以很容易地理解混淆的原因。由于变量仅是对象的引用,因此我们会困惑我们正在传递引用,所以Java是按引用传递的。然而,我们传递的是引用的副本,因此它是按值传递的。我希望现在所有的疑惑都清除了。

现在让我们分析 foo() 方法的执行。

private static void foo(Balloon balloon) { //baloon=100
    balloon.setColor("Red"); //baloon=100
    balloon = new Balloon("Green"); //baloon=200
    balloon.setColor("Blue"); //baloon = 200
}

当我们调用一个方法时,重要的是第一行,该方法被调用于引用位置上的对象。此时,气球指向100,因此它的颜色被改变为红色。

在下一行中,气球的引用被更改为200,并且任何进一步执行的方法都是在内存位置为200的对象上进行的,并不会对内存位置为100的对象产生任何影响。这解释了我们程序输出的第三行打印出蓝色颜色=红色的原因。

希望以上解释能够澄清所有疑问,只需记住变量是引用或指针,并且其副本被传递给方法,因此Java始终按值传递。当您学习堆和栈内存以及存储不同对象和引用的位置时,这将更加清晰明了。


25

Java使用传值方式传递参数,仅限于传值。

简而言之:

对于来自C#的人:没有“out”参数。

对于来自PASCAL的人:没有“var”参数。

这意味着你不能从对象本身改变引用,但是你总是可以改变对象的属性。

一种解决方法是使用 StringBuilder 参数代替 String 。而且你总是可以使用数组!


24

这是我个人认为最好的回答方式...

首先,我们必须明白,在Java中,参数传递行为...

public void foo(Object param)
{
  // some code in foo...
}

public void bar()
{
  Object obj = new Object();

  foo(obj);
}

完全相同于...

public void bar()
{
  Object obj = new Object();

  Object param = obj;

  // some code in foo...
}

在这里不考虑栈位置,因为它们与此讨论无关。

实际上,我们在Java中要研究的是变量赋值的工作方式。我在文档中找到了答案:

你将会遇到的最常见的运算符之一是简单赋值运算符"=" [...] 它将其右侧的值赋给左侧的操作数:

int cadence = 0;
int speed = 0;
int gear = 1;

这个运算符也可以用于对象,以分配对象引用[...]

很明显,这个运算符有两种不同的作用方式:分别是赋值和分配引用。当它是一个对象时,则是后者;当它不是对象时,即是原始类型时,则是前者。但是,那么我们能否理解Java的函数参数可以是按值传递按引用传递

答案存在于代码中。让我们试试:

public class AssignmentEvaluation
{
  static public class MyInteger
  {
    public int value = 0;
  }

  static public void main(String[] args)
  {
    System.out.println("Assignment operator evaluation using two MyInteger objects named height and width\n");

    MyInteger height = new MyInteger();
    MyInteger width  = new MyInteger();

    System.out.println("[1] Assign distinct integers to height and width values");

    height.value = 9;
    width.value  = 1;

    System.out.println("->  height is " + height.value + " and width is " + width.value + ", we are different things! \n");

    System.out.println("[2] Assign to height's value the width's value");

    height.value = width.value;

    System.out.println("->  height is " + height.value + " and width is " + width.value + ", are we the same thing now? \n");

    System.out.println("[3] Assign to height's value an integer other than width's value");

    height.value = 9;

    System.out.println("->  height is " + height.value + " and width is " + width.value + ", we are different things yet! \n");

    System.out.println("[4] Assign to height the width object");

    height = width;

    System.out.println("->  height is " + height.value + " and width is " + width.value + ", are we the same thing now? \n");

    System.out.println("[5] Assign to height's value an integer other than width's value");

    height.value = 9;

    System.out.println("->  height is " + height.value + " and width is " + width.value + ", we are the same thing now! \n");

    System.out.println("[6] Assign to height a new MyInteger and an integer other than width's value");

    height = new MyInteger();
    height.value = 1;

    System.out.println("->  height is " + height.value + " and width is " + width.value + ", we are different things again! \n");
  }
}

这是我的运行输出:

使用名为height和width的两个MyInteger对象进行赋值操作符评估
[1] 给height和width分配不同的整数值 -> height是9,width是1,我们是不同的东西!
[2] 将宽度的值分配给高度 -> height是1,width是1,我们现在是同一个东西吗?
[3] 将高度的值分配为不等于宽度值的整数 -> height是9,width是1,我们还是不同的东西!
[4] 分配新的MyInteger对象为height -> height是1,width是1,我们现在是同一个东西吗?
[5] 将不等于宽度值的整数分配给高度的值 -> height是9,width是9,我们现在是同一个东西!
[6] 将一个新的MyInteger和不等于宽度值的整数分配给高度 -> height是1,width是9,我们又变成不同的东西了!

[2]中,我们有不同的对象并将一个变量的值分配给另一个变量。但在[3]中分配新值后,这些对象具有不同的值,这意味着在[2]中分配的值是原始变量的副本,通常称为按值传递,否则,[3]中打印的值应该是相同的。

[4]中,我们仍然有不同的对象并将一个对象分配给另一个对象。在[5]中分配新值后,这些对象具有相同的值,这意味着在[4]中分配的对象不是另一个对象的副本,这应该称为按引用传递。但是,如果我们仔细观察[6],我们不能确定是否进行了拷贝...... ?????

我们不能确定,因为在[6]中,这些对象是相同的,然后我们将一个新对象分配给其中一个对象,之后,这些对象具有不同的值!如果它们以前是相同的,现在怎么可能是不同的?这里它们也应该是相同的!?????

我们需要记住文档才能理解发生了什么:

该操作符也可以用于对象以分配对象引用

所以我们的两个变量存储了引用...在[4]之后,我们的变量具有相同的引用,在[6]之后具有不同的引用......如果这样的事情是可能的,这意味着对象的分配通过复制对象的引用完成,否则,在[6]中打印变量的值应该相同。因此,对象(引用)就像原始数据类型一样,通过分配被复制到变量中,人们通常称之为按值传递。这是Java中唯一的传递方式


22

Java通过值传递引用。因此,如果您将其更改为其他内容(例如使用new),则该引用在方法外部不会更改。对于原生类型,始终是按值传递。


21

有两种情况需要注意:

对于原始类型的变量(例如 intbooleanchar 和其他类型),当您在函数参数中使用变量名称时,传递的是按值传递。这个值(例如 5true'c')被“复制”,方法调用后变量保留其原始值,因为现在存在两个数据副本。一个在函数调用外部,另一个在内部。

对于引用类型的变量(例如 StringObject 等),当您使用变量名称作为函数参数时,会传递变量中包含的引用值。与上面的第一个示例一样,引用值被复制,函数外部的变量也在方法调用后保留其值。引用仍然指向相同的对象。不同之处在于,函数可以更改引用的对象内部的数据。

无论哪种情况,您总是通过值传递东西。


20

让我简单地回答一下:

  • Java总是按值传递所有东西
  • 这意味着引用也是按值传递的

简而言之,您不能修改任何传递参数的值,但可以调用对象引用的方法或更改其属性。


如果“按引用传递”意味着您可以在被调用的方法内修改传递的对象,从而影响方法外部存在的对象,那会怎么样? - Sam Ginrich
这个答案比大多数其他答案都要好。 - FreelanceConsultant

20

在所有答案中,我们看到Java采用按值传递或者正如@Gevorg所写的“传递变量值的副本”的方式,这就是我们应该时刻牢记的概念。

我专注于帮助我理解这个概念的例子,并且这是对之前答案的补充。

来自[1]:在Java中,您总是通过复制传递参数;也就是说,在函数内部始终会创建值的新实例。但是,有些行为可能会让您认为您正在按引用传递。

  • 通过复制传递:当将变量传递给方法/函数时,会进行复制(有时我们听说当传递基元类型时,会进行复制)。

  • 按引用传递:当将变量传递给方法/函数时,方法/函数中的代码会操作原始变量(仍然通过复制传递,但复杂对象内部值的引用是变量两个版本即原始版本和函数内部版本的一部分。复杂对象本身被复制,但内部引用被保留)

按复制/按值传递的示例

[ref 1]的示例:

void incrementValue(int inFunction){
  inFunction ++;
  System.out.println("In function: " + inFunction);
}

int original = 10;
System.out.print("Original before: " + original);
incrementValue(original);
System.out.println("Original after: " + original);

We see in the console:
 > Original before: 10
 > In Function: 11
 > Original after: 10 (NO CHANGE)

[参考文献2]的示例

清晰地展示了机制 观看时间不超过5分钟

(按引用传递)按变量值的副本进行传递

[参考文献1]的示例 (请记住数组是一个对象)

void incrementValu(int[] inFuncion){
  inFunction[0]++;
  System.out.println("In Function: " + inFunction[0]);
}

int[] arOriginal = {10, 20, 30};
System.out.println("Original before: " + arOriginal[0]);
incrementValue(arOriginal[]);
System.out.println("Original before: " + arOriginal[0]);

We see in the console:
  >Original before: 10
  >In Function: 11
  >Original before: 11 (CHANGE)

复杂对象本身被复制,但内部引用被保留。

来自[参考文献3]的示例

package com.pritesh.programs;

class Rectangle {
  int length;
  int width;

  Rectangle(int l, int b) {
    length = l;
    width = b;
  }

  void area(Rectangle r1) {
    int areaOfRectangle = r1.length * r1.width;
    System.out.println("Area of Rectangle : " 
                            + areaOfRectangle);
  }
}

class RectangleDemo {
  public static void main(String args[]) {
    Rectangle r1 = new Rectangle(10, 20);
    r1.area(r1);
  }
}

这个矩形的面积是200,长度为10,宽度为20。

最后一件事我想分享的是课堂上的这个瞬间: 内存分配, 我发现这非常有助于理解Java按值传递或者更确切地说是“变量值的副本传递” ,就像@Gevorg所写的那样。

  1. 参考1 Lynda.com
  2. 参考2 Mehran Sahami教授
  3. 参考3 c4learn

19

Java的参数传递严格按值传递

当我说按值传递时,这意味着每当调用者调用被调用方时,参数(即要传递给其他函数的数据)会被复制并放置在形式参数中(接收输入的被调用方的本地变量)。 Java只在按值传递的环境下从一个函数向另一个函数进行数据通信。

一个重要的要点是要知道,即使C语言也仅严格按值传递:
即:数据从调用方复制到被调用方,而被被调用方执行的操作都在同一内存位置上,我们传递给它们的是(&)运算符获得的该位置的地址,并且在形式参数中使用的标识符声明为指针变量(*),使用该变量我们可以进入内存位置以访问其中的数据。

因此,在这里,形式参数仅是该位置的别名。对该位置所做的任何修改都将在变量的范围内(标识该位置的变量)可见。

在Java中,没有指针的概念(即:没有指针变量),尽管我们可以将引用变量技术上视为指针,在Java中我们称之为句柄。我们将指向地址的指针变量称为Java中的句柄的原因是,指针变量能够执行多次取消引用,而不仅仅是单次取消引用,例如:int *p; 在P中意味着p指向一个整数, int **p; 在C中意味着p是指向整数的指针 我们在Java中没有这个功能,因此将其称为句柄是完全正确且技术上合法的,同时,在C中还有指针算术运算规则,允许对指针进行算术运算,但存在约束。

在C语言中,我们称将地址传递并使用指针变量接收它们的机制为按引用传递,因为我们在形式参数中传递它们的地址并将它们作为指针变量接收,但在编译器级别上,该地址被复制到指针变量中(因为这里的数据是地址,即使是数据),因此我们可以百分之百地确定C语言是严格按值传递的(因为我们只传递数据)。

(如果在C语言中直接传递数据,则称为按值传递。)

在Java中,当我们执行相同的操作时,我们使用句柄来完成;由于它们不像上面所述的指针变量那样被称为指针变量,即使我们传递了引用,也不能说它是按引用传递,因为在Java中我们没有使用指针变量来收集它。

因此,Java 严格使用按值传递机制


Java传递到哪里了? - Sam Ginrich

18

Java始终使用按值调用。这意味着方法会获得所有参数值的副本。

考虑下面的3种情况:

1)尝试更改原始变量

public static void increment(int x) { x++; }

int a = 3;
increment(a);

x将复制a的值并递增x,a保持不变

2)尝试更改对象的基本字段

public static void increment(Person p) { p.age++; }

Person pers = new Person(20); // age = 20
increment(pers);

p将复制pers的引用值,并增加age字段,这些变量都指向同一个对象,因此age会改变

3) 尝试更改引用变量的引用值

public static void swap(Person p1, Person p2) {
    Person temp = p1;
    p1 = p2;
    p2 = temp;
}

Person pers1 = new Person(10);
Person pers2 = new Person(20);
swap(pers1, pers2);

调用swap(p1, p2)后,p1和p2复制了pers1和pers2的引用值,然后交换了这些值,因此pers1和pers2保持不变。

因此,在方法中仅能通过传递引用值的副本来改变对象的字段。


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