Java是“通过引用传递”还是“传递价值”?

问题描述 投票:6010回答:80

我一直认为Java是传递引用的。

但是,我看过一些博客文章(例如,this blog)声称它不是。

我不认为我理解他们所做的区别。

解释是什么?

java methods parameter-passing pass-by-reference pass-by-value
80个回答
5357
投票

Java始终是按值传递的。不幸的是,当我们传递一个对象的值时,我们将引用传递给它。这对初学者来说很困惑。

它是这样的:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    Dog oldDog = aDog;

    // we pass the object to foo
    foo(aDog);
    // aDog variable is still pointing to the "Max" dog when foo(...) returns
    aDog.getName().equals("Max"); // true
    aDog.getName().equals("Fifi"); // false
    aDog == oldDog; // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // change d inside of foo() to point to a new Dog instance "Fifi"
    d = new Dog("Fifi");
    d.getName().equals("Fifi"); // true
}

在上面的例子中,aDog.getName()仍将返回"Max"aDog中的main值在foo函数中没有随Dog "Fifi"改变,因为对象引用是通过值传递的。如果通过引用传递,那么aDog.getName()中的main将在调用"Fifi"之后返回foo

同样:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    Dog oldDog = aDog;

    foo(aDog);
    // when foo(...) returns, the name of the dog has been changed to "Fifi"
    aDog.getName().equals("Fifi"); // true
    // but it is still the same dog:
    aDog == oldDog; // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // this changes the name of d to be "Fifi"
    d.setName("Fifi");
}

在上面的例子中,Fifi是调用foo(aDog)之后的狗的名字,因为对象的名字设置在foo(...)中。 food上执行的任何操作都是这样的,出于所有实际目的,它们是在aDog上执行的,但是不可能改变变量aDog本身的值。


153
投票

基本上,重新分配Object参数不会影响参数,例如,

private void foo(Object bar) {
    bar = null;
}

public static void main(String[] args) {
    String baz = "Hah!";
    foo(baz);
    System.out.println(baz);
}

将打印出"Hah!"而不是null。这有效的原因是因为barbaz值的副本,它只是对"Hah!"的引用。如果它本身就是实际参考,那么foo会将baz重新定义为null


146
投票

我无法相信没人提到Barbara Liskov。当她在1974年设计CLU时,她遇到了同样的术语问题,并且她通过共享(也称为对象共享调用和对象调用)发明了这个特定情况的“通过值调用,其值为参考“。


109
投票

问题的关键在于表达“通过引用传递”中的单词引用意味着与Java中单词引用的通常含义完全不同的东西。

通常在Java引用中表示对对象的引用。但是,编程语言理论中通过引用/值传递的技术术语正在讨论对存储变量的存储器单元的引用,这是完全不同的。


80
投票

在java中,所有内容都是引用,所以当你有类似的东西时:Point pnt1 = new Point(0,0); Java确实如下:

  1. 创建新的Point对象
  2. 创建新的Point引用并初始化对先前创建的Point对象的指向(引用)的引用。
  3. 从这里开始,通过Point对象生命,您将通过pnt1引用访问该对象。所以我们可以说在Java中你通过它的引用操作对象。

Java不通过引用传递方法参数;它按值传递它们。我将使用来自this site的示例:

public static void tricky(Point arg1, Point arg2) {
  arg1.x = 100;
  arg1.y = 100;
  Point temp = arg1;
  arg1 = arg2;
  arg2 = temp;
}
public static void main(String [] args) {
  Point pnt1 = new Point(0,0);
  Point pnt2 = new Point(0,0);
  System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
  System.out.println(" ");
  tricky(pnt1,pnt2);
  System.out.println("X1: " + pnt1.x + " Y1:" + pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);  
}

程序流程:

Point pnt1 = new Point(0,0);
Point pnt2 = new Point(0,0);

使用两个不同的引用关联创建两个不同的Point对象。

System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
System.out.println(" ");

正如预期的输出将是:

X1: 0     Y1: 0
X2: 0     Y2: 0

在这条线上'传递价值'进入游戏......

tricky(pnt1,pnt2);           public void tricky(Point arg1, Point arg2);

引用pnt1pnt2通过值传递给棘手的方法,这意味着现在你的引用pnt1pnt2有他们的copies名为arg1arg2.So pnt1arg1指向同一个对象。 (与pnt2arg2相同)

tricky方法:

 arg1.x = 100;
 arg1.y = 100;

接下来在tricky方法

Point temp = arg1;
arg1 = arg2;
arg2 = temp;

在这里,您首先创建新的temp点参考,它将指向同一个地方,如arg1参考。然后你移动参考arg1指向像arg2参考相同的地方。最后arg2将指向像temp一样的地方。

从这里tricky方法的范围已经消失,你不再访问参考文献:arg1arg2temp。但重要的是,当你在生活中使用这些引用时所做的一切都将永久地影响它们所指向的对象。

所以在执行方法tricky之后,当你返回main时,你会遇到这种情况:

所以现在,完全执行程序将是:

X1: 0         Y1: 0
X2: 0         Y2: 0
X1: 100       Y1: 100
X2: 0         Y2: 0

77
投票

Java总是按值传递,而不是通过引用传递

首先,我们需要了解通过值传递的内容和通过引用传递的内容。

按值传递意味着您在内存中复制传入的实际参数值。这是实际参数内容的副本。

通过引用传递(也称为按地址传递)意味着存储实际参数的地址的副本。

有时Java可以给出通过引用传递的错觉。让我们看看它是如何工作的,使用下面的例子:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeValue(t);
        System.out.println(t.name);
    }

    public void changeValue(Test f) {
        f.name = "changevalue";
    }
}

class Test {
    String name;
}

该程序的输出是:

changevalue

让我们一步一步地理解:

Test t = new Test();

众所周知,它将在堆中创建一个对象,并将引用值返回给t。例如,假设t的值是0x100234(我们不知道实际的JVM内部值,这只是一个例子)。

new PassByValue().changeValue(t);

将参考t传递给函数时,它不会直接传递对象测试的实际参考值,但会创建t的副本,然后将其传递给函数。由于它是通过值传递的,因此它传递变量的副本而不是它的实际引用。由于我们说t的值是0x100234,t和f都将具有相同的值,因此它们将指向相同的对象。

如果使用引用f更改函数中的任何内容,它将修改对象的现有内容。这就是为什么我们得到输出changevalue,它在函数中更新。

要更清楚地理解这一点,请考虑以下示例:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeRefence(t);
        System.out.println(t.name);
    }

    public void changeRefence(Test f) {
        f = null;
    }
}

class Test {
    String name;
}

这会抛出NullPointerException吗?不,因为它只传递了引用的副本。在通过引用传递的情况下,它可能抛出一个NullPointerException,如下所示:

希望这会有所帮助。


68
投票

无论您使用何种语言,引用始终是表示的值。

获取框外视图,让我们看看Assembly或一些低级内存管理。在CPU级别,如果将任何内容写入内存或其中一个CPU寄存器,则对任何内容的引用会立即变为值。 (这就是为什么指针是一个很好的定义。它是一个值,它同时具有目的)。

内存中的数据有一个位置,在该位置有一个值(字节,字,等等)。在Assembly中,我们有一个方便的解决方案,可以为某个位置(也就是变量)提供一个名称,但是在编译代码时,汇编程序只需将Name替换为指定的位置,就像浏览器用IP地址替换域名一样。

在核心的情况下,技术上不可能在不表示任何语言的情况下将引用传递给任何语言(当它立即变为值时)。

假设我们有一个变量Foo,它的位置在内存中的第47个字节,其值为5.我们有另一个变量Ref2Foo,它在内存中的第223个字节,它的值将是47.这个Ref2Foo可能是一个技术变量,不是由程序明确创建的。如果您只查看5和47而没有任何其他信息,您将只看到两个值。如果您使用它们作为参考,那么到达5我们必须旅行:

(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223]  -> 47
(Foo)[47]       -> 5

这就是跳转表的工作原理。

如果我们想用Foo的值调用方法/函数/过程,有几种可能的方法将变量传递给方法,具体取决于语言及其几种方法调用模式:

  1. 5被复制到其中一个CPU寄存器(即EAX)。
  2. 5获得PUSHd到堆栈。
  3. 47被复制到其中一个CPU寄存器
  4. 47推到堆栈。
  5. 223被复制到其中一个CPU寄存器。
  6. 223获取PUSHd到堆栈。

在高于某个值的每种情况下 - 现有值的副本 - 都已创建,现在由接收方法来处理它。当你在方法中写入“Foo”时,它要么从EAX读出,要么自动解除引用,或者双重解引用,这个过程取决于语言的工作方式和/或Foo的类型。这是开发人员隐藏的,直到她绕过解除引用过程。因此,引用是表示的值,因为引用是必须处理的值(在语言级别)。

现在我们已经将Foo传递给了方法:

  • 在情况1.和2.如果您更改Foo(Foo = 9)它只影响本地范围,因为您有一个值的副本。从方法内部我们甚至无法确定原始Foo所在的内存位置。
  • 如果您使用默认语言结构并更改Foo(Foo = 11),它可以全局更改Foo(取决于语言,即Java或类似Pascal的procedure findMin(x, y, z: integer;var m: integer);)。但是,如果该语言允许您绕过取消引用过程,您可以更改47,比如49。在那一点上,如果你读它,Foo似乎已被改变,因为你已经改变了它的本地指针。如果你要在方法(Foo = 12)中修改这个Foo,你可能会搞砸程序的执行(又名.segfault)因为你会写一个不同于预期的内存,你甚至可以修改一个注定要保留的区域可执行程序和写入它将修改运行代码(Foo现在不在47)。但是,Foo的47值并没有全局变化,只有方法中的那个变化,因为47也是该方法的副本。
  • 在第5和第6例中。如果你在方法中修改223它会产生与3.或4相同的混乱。(一个指针,指向一个现在错误的值,它再次用作指针)但这仍然是本地问题,因为223被复制了。但是,如果你能够取消引用Ref2Foo(即223),达到并修改指向值47,比如说,49,它将影响全局Foo,因为在这种情况下,方法得到了223的副本,但引用的47存在只有一次,将其更改为49将导致每个Ref2Foo双重解除引用错误值。

对无关紧要的细节进行挑剔,即使是通过引用传递的语言也会将值传递给函数,但这些函数知道它们必须将它用于解除引用目的。这个传递参考值只是程序员隐藏的,因为它实际上是无用的,术语只是通过引用传递。

严格的pass-by-value也没用,这意味着每次调用一个以数组为参数的方法时都必须复制一个100 MB的数组,因此Java不能严格按值传递。每种语言都会传递对这个巨大数组的引用(作为一个值),并且如果该数组可以在方法内部进行本地更改,或者允许该方法(如Java所做的那样)全局修改数组,则采用写时复制机制(来自调用者的视图)和一些语言允许修改引用本身的值。

因此,简而言之,在Java自己的术语中,Java是值传递,其中值可以是:实数值或作为引用表示的值。


58
投票

Java是一种价值呼叫

这个怎么运作

  • 你总是传递一份参考值的副本!
  • 如果它是原始数据类型,则这些位包含原始数据类型本身的值,这就是为什么如果我们更改方法内的header的值,那么它不会反映外部的更改。
  • 如果它是一个像Foo foo = new Foo()这样的对象数据类型,那么在这种情况下,对象地址的副本会像文件快捷方式一样传递,假设我们在C:\ desktop中有一个文本文件abc.txt,并假设我们设置了快捷方式相同的文件,并将其放在C:\ desktop \ abc-shortcut中,所以当您从C:\ desktop \ abc.txt访问该文件并写入'Stack Overflow'并关闭该文件并再次从快捷方式打开该文件然后您write'是程序员学习的最大的在线社区'然后总文件更改将是'Stack Overflow是程序员学习的最大的在线社区',这意味着每次我们访问时打开文件的位置无关紧要相同的文件,这里我们可以假设Foo作为文件并假设foo存储在123hd7h(原始地址如C:\ desktop \ abc.txt)地址和234jdid(复制地址如C:\ desktop \ abc-shortcut实际包含内部文件的原始地址)..所以为了更好地理解制作快捷方式文件和感觉...

53
投票

据我所知,Java只知道按值调用。这意味着对于原始数据类型,您将使用副本,对于对象,您将使用对象的引用副本。不过我觉得有一些陷阱;例如,这不起作用:

public static void swap(StringBuffer s1, StringBuffer s2) {
    StringBuffer temp = s1;
    s1 = s2;
    s2 = temp;
}


public static void main(String[] args) {
    StringBuffer s1 = new StringBuffer("Hello");
    StringBuffer s2 = new StringBuffer("World");
    swap(s1, s2);
    System.out.println(s1);
    System.out.println(s2);
}

这将填充Hello World而不是World Hello,因为在交换函数中,您使用copys,它对main中的引用没有影响。但是,如果您的对象不是不可变的,您可以更改它,例如:

public static void appendWorld(StringBuffer s1) {
    s1.append(" World");
}

public static void main(String[] args) {
    StringBuffer s = new StringBuffer("Hello");
    appendWorld(s);
    System.out.println(s);
}

这将在命令行上填充Hello World。如果将StringBuffer更改为String,它将生成Hello,因为String是不可变的。例如:

public static void appendWorld(String s){
    s = s+" World";
}

public static void main(String[] args) {
    String s = new String("Hello");
    appendWorld(s);
    System.out.println(s);
}

但是你可以为这样的String创建一个包装器,它可以使它与字符串一起使用:

class StringWrapper {
    public String value;

    public StringWrapper(String value) {
        this.value = value;
    }
}

public static void appendWorld(StringWrapper s){
    s.value = s.value +" World";
}

public static void main(String[] args) {
    StringWrapper s = new StringWrapper("Hello");
    appendWorld(s);
    System.out.println(s.value);
}

编辑:我相信这也是使用StringBuffer的原因,当涉及到“添加”两个字符串,因为你可以修改原始对象,你不能用像String这样的不可变对象。


53
投票

不,它不是通过引用传递。

根据Java语言规范,Java是按值传递的:

当调用方法或构造函数(第15.12节)时,实际参数表达式的值在执行方法体或构造函数体之前初始化新创建的参数变量,每个声明的类型。 DeclaratorId中出现的Identifier可以在方法体或构造函数体中用作简单名称来引用formal parameter


49
投票

让我试着在四个例子的帮助下解释我的理解。 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:

/ **这个'通过价值传递'有一种'通过参考传递'的感觉

有些人说原始类型和'String'是'按值传递'而对象是'按引用传递'。

但是从这个例子中,我们可以理解它只是通过值传递,记住这里我们将引用作为值传递。 ie:引用按值传递。这就是为什么能够改变并且在本地范围之后仍然适用的原因。但是我们不能改变原始范围之外的实际参考。这意味着下一个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]

2865
投票

我刚刚注意到你引用了my article

Java规范说Java中的所有内容都是按值传递的。在Java中没有“pass-by-reference”这样的东西。

理解这一点的关键是类似的东西

Dog myDog;

不是狗;它实际上是指向狗的指针。

这意味着,就在你拥有的时候

Dog myDog = new Dog("Rover");
foo(myDog);

你实际上是将创建的Dog对象的地址传递给foo方法。

(我说的主要是因为Java指针不是直接地址,但最容易想到它们)

假设Dog对象驻留在内存地址42处。这意味着我们将42传递给该方法。

如果方法被定义为

public void foo(Dog someDog) {
    someDog.setName("Max");     // AAA
    someDog = new Dog("Fifi");  // BBB
    someDog.setName("Rowlf");   // CCC
}

让我们来看看发生了什么。

  • 参数someDog设置为值42
  • 在线“AAA” someDog跟随它指向的Dog(地址42处的Dog对象) Dog(地址为42的那个)被要求将他的名字改为Max
  • 在线“BBB” 创建了一个新的Dog。让我们说他在地址74 我们将参数someDog赋值为74
  • 在线“CCC” someDog跟随它指向的Dog(地址为74的Dog对象) 要求Dog(地址为74的那个人)将他的名字改为Rowlf
  • 然后,我们回来了

现在让我们考虑一下方法之外会发生什么:

myDog改变了吗?

关键是。

请记住,myDog是一个指针,而不是一个真正的Dog,答案是否定的。 myDog的价值仍为42;它仍然指向原始的Dog(但请注意,由于行“AAA”,它的名称现在是“Max” - 仍然是相同的狗; myDog的值没有改变。)

按照地址并改变最后的内容是完全有效的;但是,这不会改变变量。

Java的工作方式与C完全相同。您可以分配指针,将指针传递给方法,按照方法中的指针操作并更改指向的数据。但是,您无法更改指针指向的位置。

在C ++,Ada,Pascal和其他支持pass-by-reference的语言中,您实际上可以更改传递的变量。

如果Java具有pass-by-reference语义,那么我们在上面定义的foo方法会在myDog在BBB上分配someDog时指向的地方发生变化。

将引用参数视为传入的变量的别名。当分配该别名时,传入的变量也是如此。


47
投票

您永远不能通过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中不可用。


46
投票

我想我会提供这个答案,以便从规格中添加更多细节。

首先,What's the difference between passing by reference vs. passing by value?

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

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

或者来自维基百科,on the subject of pass-by-reference

在逐个引用的评估(也称为pass-by-reference)中,函数接收对用作参数的变量的隐式引用,而不是其值的副本。这通常意味着该函数可以修改(即分配给)用作参数的变量 - 其调用者将看到的内容。

on the subject of pass-by-value

在call-by-value中,计算参数表达式,并将结果值绑定到函数[...]中的相应变量。如果函数或过程能够为其参数赋值,则仅指定其本地副本[...]。

其次,我们需要知道Java在其方法调用中使用了什么。 Java Language Specification

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

因此它将参数的值赋予(或绑定)到相应的参数变量。

争论的价值是什么?

让我们考虑参考类型,Java Virtual Machine Specification状态

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

Java Language Specification也说

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

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

所以

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

all将String实例的引用值绑定到方法新创建的参数param。这正是pass-by-value的定义所描述的内容。因此,Java是按值传递的。

您可以按照引用来调用方法或访问引用对象的字段这一事实与对话完全无关。传递参考的定义是

这通常意味着该函数可以修改(即分配给)用作参数的变量 - 其调用者将看到的内容。

在Java中,修改变量意味着重新分配它。在Java中,如果在方法中重新分配变量,它将不会被调用者忽视。修改变量引用的对象完全是一个不同的概念。


原始值也在Java虚拟机规范here中定义。该类型的值是相应的积分或浮点值,适当编码(8,16,32,64等位)。


36
投票

区别,或者也许只是我记忆中的方式,因为我曾经与原始海报的印象相同:Java总是按价值传递。 Java中的所有对象(在Java中,除了基元之外的任何东西)都是引用。这些引用按值传递。


35
投票

正如许多人之前提到的那样,Java is always pass-by-value

这是另一个帮助您了解差异的例子(the classic swap example):

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的引用只会在本地范围内更改,而不会在此方法之外。


32
投票

在Java中,只传递引用并按值传递:

Java参数都是按值传递的(当方法使用时会复制引用):

对于基本类型,Java行为很简单:将值复制到基本类型的另一个实例中。

在Objects的情况下,这是相同的:对象变量是仅包含使用“new”关键字创建的Object的地址的指针(桶),并且像原始类型一样被复制。

行为可能与原始类型不同:因为复制的对象变量包含相同的地址(对于同一个对象)对象的内容/成员可能仍然在方法中被修改,然后在外部访问,给出了(包含)对象的错觉本身是通过引用传递的。

“字符串”对象似乎是城市传说中“对象通过引用传递”的完美反例:

实际上,在一个永远无法实现的方法中,更新作为参数传递的String的值:

String对象,由声明为final的数组保存,不能修改。只有Object的地址可能被另一个使用“new”替换。使用“new”更新变量,不会让Object从外部访问,因为变量最初是按值传递并复制的。


31
投票

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();
}

30
投票

我一直认为它是“通过副本”。它是值的原始或引用的副本。如果它是原始的,则它是作为值的位的副本,如果它是Object,则它是引用的副本。

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

原始包装类和字符串是不可变的,因此使用这些类型的任何示例都不会与其他类型/对象相同。


25
投票

我已经为任何编程语言here创建了一个致力于这类问题的线程。

Java is also mentioned。以下是简短摘要:

  • Java按值传递参数
  • “by value”是java将参数传递给方法的唯一方法
  • 使用作为参数给出的对象的方法将改变对象,因为引用指向原始对象。 (如果该方法本身改变某些值)

25
投票

一些帖子的一些更正。

C不支持通过引用传递。它总是通过价值。 C ++确实支持按引用传递,但不是默认值,而且非常危险。

Java中的值是什么并不重要:对象的原始或地址(大致),它总是按值传递。

如果Java对象“行为”就像它通过引用传递一样,那就是可变性的属性,并且与传递机制完全无关。

我不确定为什么会这么混乱,也许是因为很多Java“程序员”没有经过正式培训,因此不了解内存中真正发生了什么?


24
投票

长话短说,Java物体有一些非常特殊的属性。

通常,Java具有直接通过值传递的原始类型(intboolchardouble等)。然后Java有对象(从java.lang.Object派生的所有东西)。实际上,对象总是通过引用来处理(引用是一个你无法触摸的指针)。这意味着实际上,对象是通过引用传递的,因为引用通常不是很有趣。但它确实意味着您无法更改指向的对象,因为引用本身是按值传递的。

这听起来有点奇怪和令人困惑吗?让我们考虑C如何通过引用传递并按值传递。在C中,默认约定是按值传递。 void foo(int x)按值传递int。 void foo(int *x)是一个不需要int a的函数,而是指向int:foo(&a)的指针。可以使用&运算符来传递变量地址。

把它带到C ++,我们有参考。引用基本上(在此上下文中)隐藏等式的指针部分的语法糖:void foo(int &x)foo(a)调用,其中编译器本身知道它是引用并且应该传递非引用a的地址。在Java中,所有引用对象的变量实际上都是引用类型,实际上强制通过引用调用大多数意图和目的,而没有由例如C ++提供的细粒度控制(和复杂性)。


1584
投票

Java总是按值而不是通过引用传递参数。


让我通过example解释这个:

public class Main{
     public static void main(String[] args){
          Foo f = new Foo("f");
          changeReference(f); // It won't change the reference!
          modifyReference(f); // It will modify the object that the reference variable "f" refers to!
     }
     public static void changeReference(Foo a){
          Foo b = new Foo("b");
          a = b;
     }
     public static void modifyReference(Foo c){
          c.setAttribute("c");
     }
}

我将逐步解释这个:

  1. 声明一个名为f的类型为Foo的引用,并为其指定一个类型为Foo的新对象,其属性为"f"Foo f = new Foo("f");
  2. 从方法方面,声明了名为Fooa类型的引用,并且它最初被赋予nullpublic static void changeReference(Foo a)
  3. 当您调用方法changeReference时,将为参考a分配作为参数传递的对象。 changeReference(f);
  4. 声明一个名为b的类型为Foo的引用,并为其指定一个类型为Foo的新对象,其属性为"b"Foo b = new Foo("b");
  5. a = b对参考a,而不是f,对其属性为"b"的对象进行了新的赋值。
  6. 在调用modifyReference(Foo c)方法时,会创建一个引用c,并为该对象分配属性"f"
  7. c.setAttribute("c");将更改引用c指向它的对象的属性,并且它是引用f指向它的相同对象。

我希望你现在明白如何将对象作为参数传递在Java中:)


23
投票

Java通过VALUE和值传递参数。

长话短说:

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

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

这意味着您无法更改对象本身的引用,但您始终可以更改对象的属性。

解决方法是使用StringBuilder参数而不是String。你总是可以使用数组!


698
投票

这将为您提供一些有关Java如何工作的见解,以至于在您下次讨论Java通过引用传递或通过值传递时,您只需微笑:-)

第一步请从脑海中删除以'p'“_ _ _ _ _ _ _”开头的单词,特别是如果您来自其他编程语言。 Java和'p'不能写在同一本书,论坛,甚至txt中。

第二步记住,当你将一个Object传递给一个方法时,你传递的是Object引用,而不是Object本身。

  • 学生:师父,这是否意味着Java是传递参考?
  • 师父:蚱蜢,没有。

现在想想Object的引用/变量是什么/是什么:

  1. 变量保存位,告诉JVM如何到达内存中引用的Object(Heap)。
  2. 将参数传递给方法时,您不会传递引用变量,而是传递引用变量中的位副本。像这样:3bad086a。 3bad086a代表了一种获取传递对象的方法。
  3. 所以你只是传递3bad086a,它是参考的价值。
  4. 您传递的是引用的值而不是引用本身(而不是对象)。
  5. 该值实际上是COPIED并赋予方法。

在下面(请不要尝试编译/执行此...):

1. Person person;
2. person = new Person("Tom");
3. changeName(person);
4.
5. //I didn't use Person person below as an argument to be nice
6. static void changeName(Person anotherReferenceToTheSamePersonObject) {
7.     anotherReferenceToTheSamePersonObject.setName("Jerry");
8. }

怎么了?

  • 变量person在第1行中创建,并且在开头时为null。
  • 在第2行创建一个新的Person对象,存储在内存中,并为变量person提供对Person对象的引用。那就是它的地址。比方说3bad086a。
  • 保存Object的地址的变量person被传递给第3行中的函数。
  • 在第4行,你可以听到沉默的声音
  • 检查第5行的评论
  • 创建一个方法局部变量-anotherReferenceToTheSamePersonObject-然后在#6行中产生魔力: 变量/引用人被逐位复制并传递给函数内的anotherReferenceToTheSamePersonObject。 没有创建Person的新实例。 “person”和“anotherReferenceToTheSamePersonObject”都保持相同的3bad086a值。 不要试试这个,但是人== anotherReferenceToTheSamePersonObject会是真的。 两个变量都具有引用的IDENTICAL COPIES,它们都引用相同的Person对象,堆上的SAME对象而不是COPY。

一张图片胜过千言万语:

请注意,anotherReferenceToTheSamePersonObject箭头指向Object而不是指向变量person!

如果你没有得到它,那么只要相信我,并记住最好说Java是通过值传递的。那么,通过参考值传递。哦,更好的是传递变量值的副本! ;)

现在可以随意讨厌我,但请注意,在讨论方法参数时,传递原始数据类型和对象之间没有区别。

你总是传递一份参考值的副本!

  • 如果它是原始数据类型,则这些位将包含原始数据类型本身的值。
  • 如果它是一个Object,那么这些位将包含告诉JVM如何到达Object的地址值。

Java是按值传递的,因为在方法中你可以根据需要修改引用的对象,但无论你怎么努力,你都永远无法修改将继续引用的传递变量(不是p _ _ _ _ _ _ _)同样的对象无论如何!


上面的changeName函数永远不能修改传递的引用的实际内容(位值)。换句话说,changeName不能使Person人引用另一个Object。


当然,你可以缩短它,只是说Java是值得传递的!


625
投票

Java总是按值传递,没有例外。

那么如何让任何人都对此感到困惑,并相信Java是通过引用传递的,或者认为他们有一个Java作为参考传递的例子?关键是Java在任何情况下都不会直接访问对象本身的值。对对象的唯一访问是通过对该对象的引用。因为Java对象总是通过引用访问,而不是直接访问,所以通常将字段和变量以及方法参数作为对象进行讨论,而当它们只是对对象的引用时。这种混淆源于这种(严格来说,不正确的)命名法的变化。

所以,在调用方法时

  • 对于原始参数(intlong等),pass by value是原语的实际值(例如,3)。
  • 对于对象,pass by value是对象引用的值。

因此,如果您有doSomething(foo)public void doSomething(Foo foo) { .. },那么两个Foos已经复制了指向相同对象的引用。

当然,通过值传递对对象的引用看起来非常像(并且在实践中无法区分)通过引用传递对象。


302
投票

Java按值传递引用。

因此,您无法更改传入的引用。


218
投票

我觉得争论“传递参考与传递价值”并不是非常有用。

如果你说,“Java是通过任何东西(参考/价值)”,在任何一种情况下,你都没有提供完整的答案。这里有一些额外的信息,有助于理解记忆中发生的事情。

在我们进入Java实现之前,堆栈/堆上的崩溃过程:值以一种非常有序的方式在堆栈中上下移动,就像在自助餐厅的堆栈一样。堆中的内存(也称为动态内存)是杂乱无章的。 JVM只是在任何地方找到空间,并释放它,因为不再需要使用它的变量。

好的。首先,本地原语进入堆栈。所以这段代码:

int x = 3;
float y = 101.1f;
boolean amIAwesome = true;

结果如下:

声明和实例化对象时。实际的对象在堆上。什么在堆栈上?堆上对象的地址。 C ++程序员会将其称为指针,但是一些Java开发人员反对“指针”这个词。随你。只要知道对象的地址就在堆栈上。

像这样:

int problems = 99;
String name = "Jay-Z";

数组是一个对象,所以它也在堆上。那么阵列中的对象呢?它们获得自己的堆空间,每个对象的地址都在数组内部。

JButton[] marxBros = new JButton[3];
marxBros[0] = new JButton("Groucho");
marxBros[1] = new JButton("Zeppo");
marxBros[2] = new JButton("Harpo");

那么,当你调用一个方法时会传入什么?如果传入一个对象,那么实际传入的是对象的地址。有些人可能会说地址的“价值”,有些人说它只是对象的引用。这是“参考”和“价值”支持者之间圣战的起源。你所说的并不像你理解的那样重要,传入的是对象的地址。

private static void shout(String name){
    System.out.println("There goes " + name + "!");
}

public static void main(String[] args){
    String hisName = "John J. Jingleheimerschmitz";
    String myName = hisName;
    shout(myName);
}

创建一个String,并在堆中分配空间,并将字符串的地址存储在堆栈中并给出标识符hisName,因为第二个String的地址与第一个String的地址相同,因此不会创建新的String并且没有分配新的堆空间,但是在堆栈上创建了新的标识符。然后我们调用shout():创建一个新的堆栈框架并创建一个新的标识符name并分配已存在的String的地址。

那么,价值,参考?你说“土豆”。


178
投票

只是为了显示对比,比较以下C++Java片段:

在C ++中:注意:错误的代码 - 内存泄漏!但它证明了这一点。

void cppMethod(int val, int &ref, Dog obj, Dog &objRef, Dog *objPtr, Dog *&objPtrRef)
{
    val = 7; // Modifies the copy
    ref = 7; // Modifies the original variable
    obj.SetName("obj"); // Modifies the copy of Dog passed
    objRef.SetName("objRef"); // Modifies the original Dog passed
    objPtr->SetName("objPtr"); // Modifies the original Dog pointed to 
                               // by the copy of the pointer passed.
    objPtr = new Dog("newObjPtr");  // Modifies the copy of the pointer, 
                                   // leaving the original object alone.
    objPtrRef->SetName("objRefPtr"); // Modifies the original Dog pointed to 
                                    // by the original pointer passed. 
    objPtrRef = new Dog("newObjPtrRef"); // Modifies the original pointer passed
}

int main()
{
    int a = 0;
    int b = 0;
    Dog d0 = Dog("d0");
    Dog d1 = Dog("d1");
    Dog *d2 = new Dog("d2");
    Dog *d3 = new Dog("d3");
    cppMethod(a, b, d0, d1, d2, d3);
    // a is still set to 0
    // b is now set to 7
    // d0 still have name "d0"
    // d1 now has name "objRef"
    // d2 now has name "objPtr"
    // d3 now has name "newObjPtrRef"
}

在Java中

public static void javaMethod(int val, Dog objPtr)
{
   val = 7; // Modifies the copy
   objPtr.SetName("objPtr") // Modifies the original Dog pointed to 
                            // by the copy of the pointer passed.
   objPtr = new Dog("newObjPtr");  // Modifies the copy of the pointer, 
                                  // leaving the original object alone.
}

public static void main()
{
    int a = 0;
    Dog d0 = new Dog("d0");
    javaMethod(a, d0);
    // a is still set to 0
    // d0 now has name "objPtr"
}

Java只有两种类型的传递:内置类型的值,以及对象类型的指针值。


160
投票

Java按值传递对象的引用。

© www.soinside.com 2019 - 2024. All rights reserved.