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

guykilcj  于 2022-09-16  发布在  Java
关注(0)|答案(30)|浏览(231)

我一直认为Java使用通过引用传递
然而,我看到a blog post声称Java使用传递值
我想我不明白他们的区别。
解释是什么?

gupuwyp2

gupuwyp21#

Java是一个传递值(堆栈内存)
工作原理

  • 让我们首先了解java存储基本数据类型和对象数据类型的位置。
  • 原始数据类型本身和对象引用存储在堆栈中。对象本身存储在堆中。
  • 这意味着,堆栈内存存储原始数据类型和对象地址。
  • 并且总是传递引用值位的副本。
  • 如果它是原始数据类型,那么这些复制的位包含原始数据类型本身的值,这就是为什么当我们在方法内部更改参数值时,它不会反映外部的更改。
  • 如果它是一个对象数据类型,如Foo Foo=new Foo()那么在这种情况下,对象地址的副本会像文件快捷方式一样传递,假设我们有一个文本文件abc。txtC:\desktop中,假设我们对同一文件进行快捷方式,并将其放入C:\ desktop\abc快捷方式中,因此当您从C:\ desktop \abc访问文件时。txt和write**“Stack Overflow”并关闭文件,然后再次从快捷方式打开文件,然后写入“是程序员学习的最大在线社区”则文件更改总数将为“Stack Overflow是程序员学习最大在线社区”,这意味着无论从何处打开文件,每次访问同一文件,这里我们可以假设Foo是一个文件,并假设Foo存储在123hd7h**(原始地址,如C:\desktop\abc.txt)地址和234jdid(复制地址,如*C:\desktop\abc快捷方式,其中实际包含文件的原始地址)…为了更好地理解,请制作快捷方式文件并感受。
kuhbmx9i

kuhbmx9i2#

我已经创建了一个线程,专门针对任何编程语言here的此类问题。
还提到了Java。以下是简短的总结:

  • Java通过值传递它的参数
  • “按值”是java中向方法传递参数的唯一方法
  • 当引用指向原始对象时,使用给定为参数的对象中的方法将改变对象。(如果该方法本身更改了某些值)
tuwxkamq

tuwxkamq3#

长话短说,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++提供的细粒度控制(和复杂性)。

gr8qqesn

gr8qqesn4#

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

ds97pgxw5#

与其他一些语言不同,Java不允许您在按值传递和按引用传递之间进行选择。所有参数都是按值传递的。方法调用可以向方法传递两种类型的值:原始值的副本(例如,int和double的值)和对象引用的副本。
当方法修改基元类型参数时,对参数的更改不会影响调用方法中的原始参数值。
当涉及对象时,对象本身不能传递给方法。因此,我们传递对象的引用(地址)。我们可以使用此引用操作原始对象。

**Java如何创建和存储对象:*创建对象时,我们将对象地址存储在引用变量中。让我们分析下面的陈述。

Account account1 = new Account();

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

如果我们将array1引用变量的值作为参数传递给reverseArray方法,则会在该方法中创建一个引用变量,该引用变量开始指向相同的数组(a)。

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

所以,如果我们说

array1[0] = 5;

在reverseArray方法中,它将对数组a进行更改。
我们在reverseArray方法(array2)中有另一个引用变量,它指向数组c

array1 = array2;

在reverseArray方法中,方法ReverseArlay中的参考变量array1将停止指向数组a,并开始指向数组c(第二幅图像中的虚线)。
如果我们返回引用变量array2的值作为方法reverseArray的返回值,并将该值分配给main方法中的引用变量array1,则main中的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 */
    }
}

现在reverseArray方法结束了,它的参考变量(array1和array2)消失了。这意味着我们现在在主方法array1和array2中只有两个参考变量,分别指向c和b数组。没有引用变量指向对象(数组)a。因此它有资格进行垃圾收集。
您还可以将主数组2的值分配给数组1。数组1将开始指向b。

dauxcl2d

dauxcl2d6#

我一直认为这是一个“过客”。它是值的副本,无论是原语还是引用。如果它是一个基元,它是值位的副本,如果它是对象,它是引用的副本。

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
名称=Fido
基本 Package 类和字符串是不可变的,因此使用这些类型的任何示例都不会与其他类型/对象相同。

o2gm4chl

o2gm4chl7#

正如许多人之前提到的,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的引用只会在局部范围内更改,而不会超出此方法。

iezvtpos

iezvtpos8#

区别在于,或者可能仅仅是我记忆中的方式,因为我曾经和最初的海报有着相同的印象:Java总是通过值传递的。Java中的所有对象(在Java中,除了原语以外的任何对象)都是引用。这些引用是按值传递的。

7gcisfzg

7gcisfzg9#

在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中不可用。

doinxwow

doinxwow10#

我想我会贡献这个答案,从规范中添加更多细节。
首先,What's the difference between passing by reference vs. passing by value?
通过引用传递意味着被调用函数的参数将与调用方传递的参数相同(不是值,而是标识

  • 变量本身)。

通过值传递意味着被调用函数的参数将是调用方传递参数的副本。
或来自维基百科,关于参考传递的主题
在按引用调用求值(也称为按引用传递)中,函数接收对用作参数的变量的隐式引用,而不是其值的副本。这通常意味着函数可以修改(即赋值)用作参数的变量,调用方可以看到这些变量。
关于传递价值的问题
在“按值调用”中,计算参数表达式,并将结果值绑定到函数[…]中的相应变量。如果函数或过程能够为其参数赋值,则仅为其本地副本赋值[…]。
其次,我们需要知道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"));

所有这些都将对String示例的引用值绑定到方法新创建的参数param。这正是传递值定义所描述的。因此,Java是传递值

**您可以跟随引用调用方法或访问被引用对象的字段,这一事实与对话完全无关。**引用传递的定义为:

这通常意味着函数可以修改(即赋值)用作参数的变量,调用方可以看到这些变量。
在Java中,修改变量意味着重新分配它。在Java中,如果在方法中重新分配变量,调用方将不会注意到它修改变量引用的对象是完全不同的概念
原语值也在Java虚拟机规范中定义。该类型的值是相应的整数或浮点值,适当编码(8、16、32、64等位)。

pieyvz9o

pieyvz9o11#

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

/**这个“传递值”有一种“传递参考”的感觉
有些人说基本类型和字符串是“按值传递”,对象是“按引用传递”。
但是从这个例子中,我们可以理解,它实际上只是传递值,请记住,这里我们将引用作为值传递。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:

/**
除了示例3(PassByValueObjectCase1.java)中提到的之外,我们不能在原始范围之外更改实际引用。”
注意:我没有粘贴private class Student的代码。Student的类定义与示例3相同。

  • /
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]
j9per5c4

j9per5c412#

不,它不是通过引用传递的。
根据Java语言规范,Java是通过值传递的:
调用方法或构造函数时(§15.12),实际参数表达式的值初始化新创建的参数变量,在执行方法或构造函数的主体之前,每个声明的类型。出现在声明器ID中的标识符可以用作方法或构造函数主体中的简单名称,以引用形式参数。

0md85ypi

0md85ypi13#

据我所知,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,因为在交换函数中,您使用的是对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);
}

但是,您可以像这样为字符串创建 Package 器,这样就可以将其用于字符串:

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的原因,因为你可以修改原始对象,而你不能修改像字符串这样的不可变对象。

z8dt9xmd

z8dt9xmd14#

在Java中,方法参数都是通过值传递的:

Java参数是全部通过值传递的(方法使用时复制值或引用):
对于基元类型,Java行为很简单:将值复制到基元类型的另一个示例中。
对于对象,这是一样的:对象变量是使用“new”关键字创建的引用(mem bucket只保存对象的地址,而不是原始值),并且像原始类型一样复制。
行为可能与基本类型不同:因为复制的对象变量包含相同的地址(到相同的对象)。对象的内容/成员仍可能在方法内修改,然后在外部访问,从而产生(包含)对象本身是通过引用传递的错觉。
“字符串”对象似乎是一个很好的反例,城市传说说“对象通过引用传递”:
实际上,使用方法,您将永远无法更新作为参数传递的字符串的值:
字符串对象,包含声明为final的数组中的字符,不能修改。只有对象的地址可以被另一个使用“new”的地址替换。使用“new”更新变量,将不允许从外部访问对象,因为变量最初是通过值传递并复制的。

qeeaahzv

qeeaahzv15#

无论使用何种语言,引用在表示时始终是一个值。

为了获得一个开箱即用的视图,让我们看看汇编或一些低级内存管理。在CPU级别,如果任何对象被写入内存或某个CPU寄存器,则对该对象的引用将立即变为。(这就是为什么指针是一个很好的定义。它是一个值,同时有一个目的)。
内存中的数据有一个位置,在该位置有一个值(字节、字等)。在汇编中,我们有一个方便的解决方案,为某个位置(又名变量)指定名称,但在编译代码时,汇编程序只需将名称*替换为指定位置,就像浏览器将域名替换为IP地址一样。
从根本上讲,在技术上不可能传递任何语言中的任何引用而不表示它(当它立即变成值时)。
假设我们有一个变量Foo,它的位置位于内存中的第47字节,其为5。我们有另一个变量Ref2Foo,位于内存中第223字节,其值为47。这个ref2fo可能是一个技术变量,不是由程序显式创建的。如果在没有任何其他信息的情况下查看5和47,您将只看到两个。如果您使用它们作为参考,那么为了达到5,我们必须旅行:

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

这就是跳转表的工作方式。
如果我们想用Foo值调用方法/函数/过程,有几种可能的方法将变量传递给方法,这取决于语言及其几种方法调用模式:
1.5被复制到CPU寄存器之一(即EAX)。
1.5被推送到堆栈。
1.47被复制到一个CPU寄存器
1.47推到堆栈。
223被复制到CPU寄存器之一。
1.223被推送到堆栈。
在上述每种情况下,都会创建一个值,即现有值的副本,现在由接收方法来处理它。当您在方法中写入“Foo”时,它要么从EAX中读取,要么自动取消引用,或者双重取消引用,过程取决于语言的工作方式和/或Foo的类型。这对开发人员是隐藏的,直到她绕过解引用过程。所以引用在表示时是,因为引用是必须处理的值(在语言级别)。
现在我们已经将Foo传递给方法:

  • 在案例1和案例2中,如果更改Foo(Foo = 9),则只会影响局部范围,因为您有该值的副本。从方法内部,我们甚至无法确定原始Foo在内存中的位置。
  • 在案例3和案例4中,如果您使用默认语言构造并更改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双重取消引用错误值。

挑剔无关紧要的细节,即使是通过引用传递的语言也会将值传递给函数,但这些函数知道它们必须使用它来解除引用。这个传递引用值对程序员来说是隐藏的,因为它实际上是无用的,而且术语只是“传递引用”。
严格传递值也是无用的,这意味着每次调用以数组作为参数的方法时都必须复制100 MB的数组,因此Java不能严格传递值。每种语言都会传递一个对这个巨大数组的引用(作为一个值),如果该数组可以在方法内部本地更改,则采用写时复制机制,或者允许该方法(如Java所做的)全局修改数组(从调用方的Angular ),少数语言允许修改引用本身的值。
简言之,用Java自己的术语,Java是传递值,其中可以是:实值,表示引用

相关问题