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个回答

57

让我用四个例子来解释我的理解。Java是按值传递而不是按引用传递。

/**

按值传递

在Java中,所有参数都是按值传递的,即将方法参数赋值对调用者不可见。

*/

示例1:

public class PassByValueString {
    public static void main(String[] args) {
        new PassByValueString().caller();
    }

    public void caller() {
        String value = "Nikhil";
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

结果

output : output
value : Nikhil
valueflag : false

示例2:

/** * * 按值传递 * */

public class PassByValueNewString {
    public static void main(String[] args) {
        new PassByValueNewString().caller();
    }

    public void caller() {
        String value = new String("Nikhil");
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

结果

output : output
value : Nikhil
valueflag : false

例子3:

/** 这个“传递值的感觉像传递引用”

有些人说原始类型和“字符串”是“按值传递”,而对象是“按引用传递”。
但从这个例子中,我们可以了解到实际上只是传递值, 记住,在这里我们将引用作为值传递。 也就是说,通过值传递引用。 这就是为什么我们能够更改并且仍然保持本地范围之后仍然成立。 但我们无法在原始范围之外更改实际引用。 下一个PassByValueObjectCase2的例子演示了这意味着什么。

*/

public class PassByValueObjectCase1 {

    private class Student {
        int id;
        String name;
        public Student() {
        }
        public Student(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        new PassByValueObjectCase1().caller();
    }

    public void caller() {
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student);
    }

    public String method(Student student) {
        student.setName("Anand");
        return "output";
    }
}

结果

output : output
student : Student [id=10, name=Anand]

示例 4:

/**

除了Example3(PassByValueObjectCase1.java)中提到的内容外,我们不能在原始作用域之外改变实际引用。

注意:我不会贴出private class Student的代码。关于Student的类定义与Example3相同。

*/

public class PassByValueObjectCase2 {

    public static void main(String[] args) {
        new PassByValueObjectCase2().caller();
    }

    public void caller() {
        // student has the actual reference to a Student object created
        // can we change this actual reference outside the local scope? Let's see
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student); // Will it print Nikhil or Anand?
    }

    public String method(Student student) {
        student = new Student(20, "Anand");
        return "output";
    }

}

结果

output : output
student : Student [id=10, name=Nikhil]

57

我认为我可以贡献这个答案,从规范方面添加更多细节。

首先,传值和传引用有什么区别?

传引用意味着被调用函数的参数将与调用者传递的参数相同(不是值,而是标识符即变量本身)。

  • 传值意味着被调用函数的参数将是调用者传递参数的副本。

或者来自维基百科,关于传递引用的主题

在传递引用评估(也称为传递引用),函数接收一个隐式引用 用作参数的变量,而不是其值的副本。这通常意味着函数可以修改(即分配给)使用的参数变量,这是其调用者将看到的。

以及关于传递值的主题

在传值方式下,参数表达式会被计算,结果值会被绑定到函数中相应的变量上[...]。 如果函数或过程能够给其参数赋值,则只有其局部副本被分配[...]。

其次,我们需要知道Java在方法调用中使用什么。 Java语言规范指出

当方法或构造函数被调用时(§15.12),实际参数表达式的值将初始化新创建的参数变量,每个声明类型,在方法或构造函数体执行之前。

因此,它将参数的值分配(或绑定)给相应的参数变量。

参数的值是什么?

让我们考虑引用类型,Java虚拟机规范指出

有三种引用类型:类类型、数组类型和接口类型。它们的值是动态创建的类实例、数组或实现接口的类实例或数组的引用。

Java语言规范也指出

引用值(通常只是引用)是指向这些对象的指针,以及一个特殊的空引用,它不引用任何对象。

参数的值(某些引用类型)是指向对象的一个指针。请注意,变量、具有引用类型返回类型的方法调用和实例创建表达式(new ...)都解析为引用类型值。

因此

public void method (String param) {}
...
String variable = new String("ref");
method(variable);
method(variable.toString());
method(new String("ref"));

将引用类型的值绑定到新创建的参数param上,这正是传值的定义。因此,Java是按值传递的。调用方法或访问所引用对象的字段都与这个问题无关。

在Java中,修改变量意味着重新分配它,如果在方法内部重新分配变量,那么对调用者来说是不可见的。而修改所引用的对象是一个完全不同的概念。


原始值也在Java虚拟机规范中进行了定义,这里给出了相应类型的值,以适当的方式编码(8、16、32、64等位)。


55

在Java中,你无法通过引用传递参数。其中一个表现就是,当你想要从方法调用中返回多个值时,这一点就变得很明显了。请看下面这段C++代码:

void getValues(int& arg1, int& arg2) {
    arg1 = 1;
    arg2 = 2;
}
void caller() {
    int x;
    int y;
    getValues(x, y);
    cout << "Result: " << x << " " << y << endl;
}
有时候,你想在Java中使用相同的模式,但是你不能直接这样做。取而代之的是,你可以像这样做:
void getValues(int[] arg1, int[] arg2) {
    arg1[0] = 1;
    arg2[0] = 2;
}
void caller() {
    int[] x = new int[1];
    int[] y = new int[1];
    getValues(x, y);
    System.out.println("Result: " + x[0] + " " + y[0]);
}

正如之前的回答所解释的那样,在Java中,你将指向数组的指针作为值传递给getValues。这已经足够了,因为该方法会修改数组元素,并且按照约定,你希望元素0包含返回值。显然,你可以用其他方式来实现这个目的,比如重构代码使得不需要这样做,或者构建一个类来包含返回值或允许设置它。但上面在C++中简单的模式在Java中不可用。


48

区别在于,或许只是我的记忆有误,因为我曾经和原始贴主有着相同的印象。Java总是按值传递。在 Java 中,所有对象(除了原始数据类型)都是引用。这些引用是按值传递的。


45

正如许多人之前所提到的,Java始终是按值传递

以下是另一个示例,将帮助您理解差异(经典交换示例):

public class Test {
  public static void main(String[] args) {
    Integer a = new Integer(2);
    Integer b = new Integer(3);
    System.out.println("Before: a = " + a + ", b = " + b);
    swap(a,b);
    System.out.println("After: a = " + a + ", b = " + b);
  }

  public static swap(Integer iA, Integer iB) {
    Integer tmp = iA;
    iA = iB;
    iB = tmp;
  }
}

输出:

之前:a = 2,b = 3
之后:a = 2,b = 3

这是因为 iA 和 iB 是新的本地引用变量,它们具有传递引用的相同值(分别指向 a 和 b)。因此,尝试更改 iA 或 iB 的引用只会在本地作用域内进行更改,而不会在该方法之外。


嗨,swap方法的返回类型是什么? - Priyanka
1
@Priyanka 哈!这么多年后,你是第一个注意到这个问题的人!它是void。 - pek
语法: "传值" 是一个句子的对象吗? - Sam Ginrich

41

我一直将它视为“按拷贝传递”。它是值的副本,无论是原始类型还是引用类型。如果是原始类型,它就是值的位拷贝;如果是对象,则是引用的拷贝。

public class PassByCopy{
    public static void changeName(Dog d){
        d.name = "Fido";
    }
    public static void main(String[] args){
        Dog d = new Dog("Maxx");
        System.out.println("name= "+ d.name);
        changeName(d);
        System.out.println("name= "+ d.name);
    }
}
class Dog{
    public String name;
    public Dog(String s){
        this.name = s;
    }
}

Java PassByCopy的输出:

name= Maxx
name= Fido

基本包装类和字符串是不可变的,所以使用这些类型的任何示例都不能像其他类型/对象一样起作用。


38
与其他一些语言不同,Java不允许您选择传值还是传引用——所有参数都是按值传递的。方法调用可以向方法传递两种类型的值——原始值的副本(例如int和double的值)以及对象引用的副本。
当方法修改原始类型参数时,对参数的更改不会影响调用方法中的原始参数值。
当涉及到对象时,对象本身无法传递给方法。因此,我们传递对象的引用(地址)。我们可以使用这个引用来操作原始对象。
Java如何创建和存储对象:当我们创建一个对象时,我们将该对象的地址存储在一个引用变量中。让我们分析以下语句。
Account account1 = new Account();

"Account account1"是引用变量的类型和名称,"="是赋值运算符,"new"从系统请求所需的空间。关键字new右侧的构造函数隐式地被调用以创建对象。创建的对象的地址(称为“类实例创建表达式”的表达式的右值结果)使用分配运算符分配给指定名称和类型的引用变量的左值。
虽然对象的引用是按值传递的,但方法仍然可以通过使用对象引用的副本调用其公共方法与引用的对象交互。由于存储在参数中的引用是作为参数传递的引用的副本,所以调用方法中的参数和调用方法中的参数引用同一个内存中的对象。
出于性能原因,将引用传递给数组而不是数组对象本身是有意义的。因为Java中的所有内容都是按值传递的,如果传递了数组对象,则会传递每个元素的副本。对于大型数组,这将浪费时间并消耗大量存储来存储元素的副本。
在下面的图像中,您可以看到我们在主方法中有两个引用变量(在C / C ++中称为指针,我认为这个术语使理解这个特性更容易)。基本和引用变量保存在堆栈内存中(下图左侧)。array1和array2引用变量“指向”(C / C ++程序员称之为)或引用a和b数组,这些数组是堆内存中的对象(这些引用变量保存的值是对象的地址)。

Pass by value example 1

如果我们将array1引用变量的值作为参数传递给reverseArray方法,该方法中会创建一个引用变量,并且该引用变量开始指向同一个数组(a)。{{a和array1都是占位符,请根据实际情况进行替换}}
public class Test
{
    public static void reverseArray(int[] array1)
    {
        // ...
    }

    public static void main(String[] args)
    {
        int[] array1 = { 1, 10, -7 };
        int[] array2 = { 5, -190, 0 };

        reverseArray(array1);
    }
}

Pass by value example 2

因此,如果我们说

array1[0] = 5;

在reverseArray方法中,它将改变数组a的内容。

我们在reverseArray方法中有另一个引用变量(array2),它指向一个数组c。如果我们说

array1 = array2;

在reverseArray方法中,方法内的引用变量array1将停止指向数组a并开始指向数组c(第二张图片中的虚线)。
如果我们将引用变量array2的返回值作为reverseArray方法的返回值,并将该值分配给主方法中的引用变量array1,则主方法中的array1将开始指向数组c。
因此,现在让我们一次性写出我们所做的所有事情。
public class Test
{
    public static int[] reverseArray(int[] array1)
    {
        int[] array2 = { -7, 0, -1 };

        array1[0] = 5; // array a becomes 5, 10, -7

        array1 = array2; /* array1 of reverseArray starts
          pointing to c instead of a (not shown in image below) */
        return array2;
    }

    public static void main(String[] args)
    {
        int[] array1 = { 1, 10, -7 };
        int[] array2 = { 5, -190, 0 };

        array1 = reverseArray(array1); /* array1 of 
         main starts pointing to c instead of a */
    }
}

enter image description here

现在reverseArray方法已经结束,它的引用变量(array1和array2)已经消失。这意味着我们现在只有两个引用变量在main方法中,array1和array2分别指向c和b数组。没有引用变量指向对象(数组)a。因此,它可以被垃圾回收。
您也可以将main方法中array2的值赋给array1。array1将开始指向b。

38

Java只有值传递。一个非常简单的例子来验证这一点。

public void test() {
    MyClass obj = null;
    init(obj);
    //After calling init method, obj still points to null
    //this is because obj is passed as value and not as reference.
}
private void init(MyClass objVar) {
    objVar = new MyClass();
}

34
简而言之,Java对象具有一些非常奇特的属性。
通常情况下,Java具有原始类型(intboolchardouble等),这些类型会直接按值传递。然后Java有对象(从java.lang.Object派生的所有内容)。实际上,对象总是通过引用处理的(引用是一个指针,你无法触摸)。这意味着实际上对象是按引用传递的,因为引用通常不是有趣的。但这也意味着您不能更改引用所指向的对象,因为引用本身是按值传递的。
听起来很奇怪和令人困惑吗?让我们考虑一下C如何实现传递引用和传递值。在C中,默认约定是传递值。 void foo(int x) 按值传递一个整数。 void foo(int *x) 是一个函数,它不想要一个int a,而是想要一个指向int的指针:foo(&a)。人们可以使用&运算符将变量地址传递给它。

将此转换为C ++,我们会用到引用。在这个上下文中,“引用”基本上是隐藏等式中指针部分的语法糖:void foo(int &x)foo(a)调用,编译器本身知道它是一个引用,并且应该传递非引用a的地址。在Java中,所有引用对象的变量实际上都是引用类型,实际上强制执行大多数意图和目的的引用调用,而没有像C ++那样提供细粒度控制(和复杂性)。


1
这是错误的。Java所谓的“引用”在C++中被称为“指针”。C++所谓的“引用”在Java中不存在。C++引用是类似于指针的类型,但具有全局范围。当您更改C++引用时,该引用的所有出现都会更改,无论是在被调用函数中还是在调用函数中。Java无法做到这一点。Java严格按值传递,并且对Java引用的更改严格局限于本地。Java调用函数无法更改调用函数的引用值。您可以通过使用像AtomicReference这样的包装器对象来模拟C++引用。 - Talijanac
2
C++中的引用与作用域无关。在实现中,它们类似于不允许具有空值的指针。除此之外,主要区别在于语法上它们行为类似于所引用数据的别名。在Java中,引用的工作方式几乎相同,但具有特殊规则,允许:使用==运算符与null和其他引用值进行比较。C++也是按值传递,尽管这个值可以是指向引用的指针/引用。 - Paul de Vrieze
被调用方法对C++引用所做的更改也可以通过调用方法看到。这在Java中不存在,也不像指针一样。在Java和C中,指针值的更改仅局限于本地。我不知道如何正确地称呼这种行为,但它与某些脚本语言的“外部作用域”有些相似。 - Talijanac
关于正确的引用传递示例,请参见此处的交换程序:https://www.geeksforgeeks.org/references-in-c/。在Java中不可能编写具有相同副作用的swap方法。C++引用具有Java引用或C指针所没有的“质量”(语言运算符的行为)。 - Talijanac
@Paul de Vrieze "不允许有空值" - 考虑到在C方言中,当p是一个指针时,*p是一个引用;即使p为null也有效。关于赋值,在Java中的引用行为类似指针,并满足C的"按引用调用"语义。 - Sam Ginrich

31

对一些帖子进行了一些纠正。

C语言不支持引用传递。它始终是值传递。C++支持引用传递,但不是默认设置,而且相当危险。

在Java中,无论是原始值还是对象地址(大致上),它都始终是按值传递的。

如果Java对象像通过引用传递一样“表现”,那么这是可变性的属性,与传递机制毫无关系。

我不确定为什么会如此混淆,也许是因为许多Java“程序员”没有受过正式培训,因此不理解内存中真正发生的情况?


Really? https://dev59.com/VXE95IYBdhLWcg3wn_Vr - Sam Ginrich

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