java指针 - Java是“传递引用”还是“按值传递”?



15 Answers

我刚刚注意到你引用了我的文章

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方法会更改someDog在BBB上分配someDog时指向的位置。

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

java赋值

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

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

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

解释是什么?




这将为您提供有关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是值得传递的!




Java按值传递引用。

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




为了显示对比,请比较以下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只有两种类型的传递:内置类型的值,以及对象类型的指针值。




基本上,重新分配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。这个工作的原因是因为bar是一个值的副本baz,它只是一个引用"Hah!" 。如果它是实际的引用本身,那么foo将重新定义baznull




的问题的关键是,字参考在表述“通过引用传递”是指某物从字的通常含义完全不同的参考在Java中。

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




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

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

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

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

比方说,我们有一个变量富,它的位置是在内存中的第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是值传递,其中可以是:实数值或作为引用表示的




据我所知,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这样的不可变对象。




让我试着借助四个例子来解释我的理解。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]



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




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




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



我一直认为它是“通过副本”。它是值的原始或引用的副本。如果它是原语,则它是作为值的位的副本,如果它是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

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




一些帖子的一些更正。

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

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

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

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




Java按VALUE传递参数,按值传递参数。

长话短说:

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

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

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

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




Related