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

18

Java采用的是按照常量引用传递参数的方式,也就是说传递的是引用的一个拷贝,实际上是传递的按值传递。如果类是可变的,你可以改变引用的内容,但是你不能改变这个引用本身,也就是说地址不能被改变,因为是按值传递的,但是被地址指向的内容是可以被改变的。对于不可变类来说,引用的内容也无法被改变。


1
在Java中,除非程序员指定“finally”,否则不存在“常量引用”的概念。 - user207421
我的意思是常量引用,就是在函数中无法通过new MyClass()来改变引用本身。如果我说得正确的话,对象引用是按值传递的,这意味着传递的是引用的副本,因此您可以更改该引用所指向的数据,但不能使用new运算符更改它并分配一个新对象。 - fatma.ekici
所以请修正你的答案。如果它是一个常量,你就不能在被调用的方法内重新分配它,但是你可以,除非你指定了 final - user207421
不知道如何解开那个结:你传递一个对象引用并说对象是按值传递的。:( - Sam Ginrich

18

毫无疑问,Java是“传值”的,这个观点毋庸置疑。由于Java大部分是面向对象的,并且对象使用引用来工作,很容易让人混淆并认为它是“按引用传递”。

按值传递意味着您将值传递给方法,如果方法更改了传递的值,则真实实体不会改变。另一方面,“按引用传递”意味着将引用传递给方法,如果方法对其进行更改,则传递的对象也会更改。

在Java中,通常当我们将对象传递给方法时,我们实际上是传递对象作为值的引用,因为Java使用引用和地址来处理堆中的对象。

但是,为了测试它是否真正是按值传递或按引用传递,您可以使用基本类型和引用:

@Test
public void sampleTest(){
    int i = 5;
    incrementBy100(i);
    System.out.println("passed ==> "+ i);
    Integer j = new Integer(5);
    incrementBy100(j);
    System.out.println("passed ==> "+ j);
}
/**
 * @param i
 */
private void incrementBy100(int i) {
    i += 100;
    System.out.println("incremented = "+ i);
}

输出结果为:

incremented = 105
passed ==> 5
incremented = 105
passed ==> 5

所以在这两种情况下,方法内部发生了什么并不会改变真正的对象,因为传递的是该对象的值,而不是对象本身的引用。

但是当您将自定义对象传递给方法并且方法对其进行更改时,它将同时更改真正的对象,因为即使您传递了对象,也将其引用作为值传递给方法。让我们尝试另一个例子:

@Test
public void sampleTest2(){
    Person person = new Person(24, "John");
    System.out.println(person);
    alterPerson(person);
    System.out.println(person);
}

/**
 * @param person
 */
private void alterPerson(Person person) {
    person.setAge(45);
    Person altered = person;
    altered.setName("Tom");
}

private static class Person{
    private int age;
    private String name; 

    public Person(int age, String name) {
        this.age=age;
        this.name =name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Person [age=");
        builder.append(age);
        builder.append(", name=");
        builder.append(name);
        builder.append("]");
        return builder.toString();
    }

}

在这种情况下,输出为:

Person [age=24, name=John]
Person [age=45, name=Tom]

16

通过传递参数来共享数据的方法有两种:

  • 按引用传递:调用者和被调用者使用同一个变量作为参数。

  • 按值传递:调用者和被调用者具有相同值的两个独立变量。

Java使用按值传递

  • 当传递基本数据类型时,它会复制基本数据类型的值。
  • 当传递对象时,它会复制对象的地址并将其传递给被调用方法的变量。

Java在存储变量时遵循以下规则:

  • 像原始数据类型和对象引用这样的局部变量会在堆栈内存上创建。
  • 对象会在堆内存上创建。

使用基本数据类型的示例:

public class PassByValuePrimitive {
    public static void main(String[] args) {
        int i=5;
        System.out.println(i);  //prints 5
        change(i);
        System.out.println(i);  //prints 5
    }
    
    
    private static void change(int i) {
        System.out.println(i);  //prints 5
        i=10;
        System.out.println(i); //prints 10
        
    }
}

使用对象的示例:

public class PassByValueObject {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("prem");
        list.add("raj");
        new PassByValueObject().change(list);
        System.out.println(list); // prints [prem, raj, ram]
        
    }
    
    
    private  void change(List list) {
        System.out.println(list.get(0)); // prem
        list.add("ram");
        list=null;
        System.out.println(list.add("bheem")); //gets NullPointerException
    }
}

16

关键的核心知识必须是引用所指向的对象,

当将一个对象引用传递给一个方法时,该引用本身会通过按值调用(call-by-value)进行传递。然而,由于被传递的值是指向对象的,因此该值的副本仍将指向相应参数所引用的同一对象。

《Java入门指南》第六版,Herbert Schildt


15
我看到所有的回答都相同:传值。然而,Brian Goetz最近在Valhalla项目的更新中对此作了不同的回答: “确实,关于Java对象是按值传递还是按引用传递的常见“坑”,答案是“两者都不是”:对象引用是按值传递的。” 你可以在这里阅读更多:Valhalla状态。第2节:语言模型 编辑:Brian Goetz是Java语言架构师,领导诸如Valhalla项目和Amber项目等项目。 编辑-2020-12-08:更新Valhalla状态

6
对象引用(即指针)按值传递,基本类型也按值传递。这意味着所有内容都是按值传递的。我认为这里的关键术语是“按值传递”。 - Sanjeev
3
我认为领导Amber项目和Valhalla项目的Java语言架构师是一个可靠的消息来源,声称它不是按值传递。 - Mr.Robot
首先,我认为他不比Java的创造者James Gosling更可信,后者在他的书《Java编程语言》中明确指出Java确实是传值调用(第2章第2.6.5节)。其次,尽管Goetz说它既不是PBV也不是PBR,但他接着又说引用是按值传递的,从而与自己的观点相矛盾。如果你了解Java,你也知道基本类型也是按值传递的。由于Java中的所有内容都是按值传递的,因此Java是一种传值调用语言。 - Sanjeev
其他比Goetz更可信的来源是Aho、Lam、Sethi和Ullman,他们以其书籍《编译器:原理、技术和工具》而闻名,这是编译器构建的标准大学教材。该书第二版1.6.6节还指出Java是按值传递的。 - Sanjeev
3
所有参考资料中最相关的是Java语言规范,其中指出:“其效果是将参数值分配给方法的相应新创建的参数变量”(第15.12.4.5节)。请注意,它避免了使用“按...传递”的术语混淆。 (顺便说一句,我不同意Goetz将“按值传递”和“按引用传递值”视为在语义上有所不同的特性。而且我同意他与自己的观点相矛盾。) - Stephen C
这是迄今为止唯一有意义的答案。对象并不作为纯值传递,因为它们不像 C++ 的按值传递方式那样被复制。 - AFP_555

14

看一下这段代码。这段代码不会抛出NullPointerException,它会打印出"Vinay"。

public class Main {
    public static void main(String[] args) {
        String temp = "Vinay";
        print(temp);
        System.err.println(temp);
    }

    private static void print(String temp) {
        temp = null;
    }
}
如果 Java 是按引用传递的,那么当引用被设置为Null时,它应该抛出NullPointerException异常。

14

长话短说:

  1. 非基本数据类型:Java 传递引用值
  2. 基本数据类型:传递数值本身。

结束了。

(2) 太容易了。如果你想了解 (1) 意味着什么,想象一下你有一个类名为 Apple:

class Apple {
    private double weight;
    public Apple(double weight) {
        this.weight = weight;
    }
    // getters and setters ...

}

然后,当您将此类的实例传递给主方法时:

class Main {
    public static void main(String[] args) {
        Apple apple = new Apple(3.14);
        transmogrify(apple);
        System.out.println(apple.getWeight()+ " the goose drank wine...";

    }

    private static void transmogrify(Apple apple) {
        // does something with apple ...
        apple.setWeight(apple.getWeight()+0.55);
    }
}

哦,但是你可能已经知道了,你对这样做会发生什么很感兴趣:

class Main {
    public static void main(String[] args) {
        Apple apple = new Apple(3.14);
        transmogrify(apple);
        System.out.println("Who ate my: "+apple.getWeight()); // will it still be 3.14? 

    }

    private static void transmogrify(Apple apple) {
        // assign a new apple to the reference passed...
        apple = new Apple(2.71);
    }


}

14

与其他一些语言不同,Java 不允许你在传递参数时选择值传递或者引用传递。

所有的参数都是按值传递的。

方法调用可以传递两种类型的值给一个方法:

  • 基本类型的拷贝(例如,int 和 double 类型的值)
  • 对象引用的拷贝。

对象本身不能被传递给方法。当一个方法修改一个基本类型的参数时,对参数的修改不会影响调用方法中原始实际参数的值。

对于引用类型参数也是如此。如果你修改了一个引用类型参数,让它引用另一个对象,那么只有该参数会引用新的对象——在调用方法中存储的引用仍然指向原始对象。

参考资料:Java™ How To Program (Early Objects), Tenth Edition


对象是通过引用传递的。 :) - Sam Ginrich

12

一个简单的测试可以检查一个语言是否支持按引用传递,只需编写一个传统的交换函数来测试。 您能否在Java中编写一个传统的swap(a,b)方法/函数?

传统的swap方法或函数接受两个参数并将它们交换,以便在函数外部更改传递到函数中的变量。其基本结构如下:

(非Java) 基本交换函数结构

swap(Type arg1, Type arg2) {
    Type temp = arg1;
    arg1 = arg2;
    arg2 = temp;
}

如果您能够在您的语言中编写这样的方法/函数,以便调用

Type var1 = ...;
Type var2 = ...;
swap(var1,var2);

实际上,如果要交换变量var1和var2的值,该语言支持传递引用。但是,由于Java只支持传递值而不支持指针或引用,因此不允许这样做。


1
你可能需要澄清一下你最后一句话。我的第一反应是,“只传递值而不是指针...”是你的Java实现可能确实做到了这一点,传递了一个指针。你不能对该指针进行解引用似乎并不重要。 - Loduwijk

12

Java通过值传递基本类型,通过引用传递类类型

现在,人们喜欢无休止地争论“按引用传递”是否是正确描述Java等编程语言实际操作的方式。重点是:

  1. 传递对象不会复制对象。
  2. 传递给函数的对象可以被函数修改其成员。
  3. 传递给函数的基本值不能被函数修改。需要进行复制。

在我的书中,这被称为按引用传递。

Brian Bi - 哪些编程语言是按引用传递的?


14
这个回答完全不正确,只会产生混淆。Java 是一种纯粹的按值传递语言。让你感到困惑的是,值可以是指向对象的指针。按引用传递意味着调用方能够更改对象在调用方身上的标识。例如,将新对象赋给方法参数也会影响调用该方法的代码中传递的指针。 - Torben
2
@Dennis 字符串不是原始类型,它们是对象。 - nasch
2
这不是关于你的书中写了什么。"按引用传递"和"按值传递"是行业标准术语,具有非常明确的定义。按照这些定义,Java在没有例外的情况下是"按值传递"。 - Sanjeev
2
C++具有真正的按值传递,它将对象的所有字段复制到堆栈上。Java不这样做,因此它不是按值传递。 - Solubris
1
@Solubris 事实上,Java 不允许将整个对象传递给方法,但当您传递原始数据类型或引用(即指针)时,您是通过值传递它们。更重要的是,您从不通过引用传递任何内容。 - Sanjeev
显示剩余3条评论

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