Java是"按引用传递"还是"按值传递"?

时间:2020-03-05 18:46:42  来源:igfitidea点击:

我一直以为Java是通过引用传递的。

但是,我已经看到一些博客文章(例如,此博客)声称不是。

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

有什么解释?

解决方案

回答

Java永远都是按值传递的,没有例外。

那么,任何人都可能对此感到困惑,以为Java是通过引用传递的,或者认为他们有一个Java充当引用传递的示例,这又是怎么回事呢?关键是,在任何情况下,Java都不提供对对象本身的值的直接访问。对对象的唯一访问是通过对该对象的引用。因为Java对象始终通过引用而不是直接访问来访问,所以通常将字段和变量以及方法参数称为对象,而在学究上它们仅是对象的引用。混淆源于命名上的这种变化(严格来说是不正确的)。

因此,在调用方法时

  • 对于基元参数(" int"," long"等),按值传递是基元的实际值(例如3)。
  • 对于对象,按值传递是对对象的引用的值。

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

自然地,按值传递对对象的引用看起来非常像(实际上在实践中是无法区分的)按引用传递对象。

回答

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值的副本,而baz只是对" Hah!"的引用。如果它是实际的引用本身,那么foo将把baz重新定义为null。

回答

Java按值传递对对象的引用。

回答

Java总是按值传递。不幸的是,他们决定将对象的位置称为"参考"。当我们传递对象的值时,我们正在传递对它的引用。这使初学者感到困惑。

它是这样的:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    // 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 
}

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"。函数" foo"中的" main"中的值" aDog"不会在带有" Dog"" Fifi"的函数" foo"中更改,因为对象引用是通过值传递的。如果通过引用传递,则在对foo的调用之后,main中的aDog.getName()将返回" Fifi"`。

同样地:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    foo(aDog);
    // when foo(...) returns, the name of the dog has been changed to "Fifi"
    aDog.getName().equals("Fifi"); // 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(...)"内部设置的。 foo在d上执行的任何操作实际上都是在aDog上执行的(除非将d更改为指向不同的Dog实例,例如d = new时除外)。狗("拳击手")。)。

回答

简而言之,Java对象具有一些非常特殊的属性。

通常,Java具有原始类型(int,bool,char,double等),这些原始类型通过值直接传递。然后,Java具有对象(一切都源自" java.lang.Object")。实际上,对象总是通过引用(引用是我们无法触摸的指针)来处理的。这意味着实际上,对象是通过引用传递的,因为引用通常并不有趣。但是,这确实意味着我们无法更改所指向的对象,因为引用本身是通过值传递的。

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

将其带到C ++,我们有参考。引用基本上是(在这种情况下)语法糖,它隐藏了等式的指针部分:foo foo(int&x)由foo(a)调用,编译器本身知道它是引用和地址。非引用a的值应该被传递。在Java中,所有引用对象的变量实际上都是引用类型,实际上在大多数意图和目的下都强制按引用进行调用,而没有C ++提供的精细控制(和复杂性)。

回答

区别,或者也许只是我曾经记得的印象与原始海报一样,是这样的:Java总是通过价值传递。 Java中的所有对象(在Java中,除基元以外的所有对象)都是引用。这些引用按值传递。

回答

正如许多人之前提到的,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;
  }
}

印刷:

Before: a = 2, b = 3

  After: a = 2, b = 3

发生这种情况是因为iA和iB是新的局部引用变量,它们具有与传递的引用相同的值(它们分别指向a和b)。因此,尝试更改iA或者iB的引用只会在本地范围内更改,而不会超出此方法。

回答

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

原始包装器类和字符串是不可变的,因此使用这些类型的任何示例都无法与其他类型/对象一起工作。

回答

我为这里的任何编程语言创建了一个专门针对此类问题的线程。

还提到了Java。这是简短的摘要:

  • Java按值传递参数
  • "按值"是Java中将参数传递给方法的唯一方法
  • 使用给定对象的方法作为参数将改变对象,因为引用指向原始对象。 (如果该方法本身更改了某些值)

回答

我只是注意到我们引用了我的文章。

Java规范说Java中的所有内容都是按值传递的。在Java中,没有"通过引用传递"这样的东西。

理解这一点的关键是

Dog myDog;

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

这意味着什么时候

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

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

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

假设"狗"对象位于内存地址42. 这意味着我们将42传递给该方法。

如果方法定义为

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

让我们看看发生了什么。

  • 参数" someDog"设置为值42
  • 要求"狗"(地址为42的那只狗)将他的名字更改为" Max"
  • 我们将参数someDog分配给74
  • 要求"狗"(地址为74的那只狗)将他的名字更改为Rowlf
  • 然后,我们返回

现在,让我们考虑一下该方法之外发生的情况:

" myDog"有变化吗?

有钥匙

请记住," myDog"是一个指针,而不是实际的" Dog",答案是否定的。 myDog的值仍然为42;它仍指向原始的"狗"(但请注意,由于行" AAA",其名称现在为" Max"仍然是" Dog";" myDog"的值未更改。)

跟随地址并更改地址末尾是完全有效的;但这不会更改变量。

Java的工作原理与C完全相同。我们可以分配一个指针,将指针传递给方法,在方法中跟随指针并更改所指向的数据。但是,我们不能更改该指针指向的位置。

在C ++,Ada,Pascal和其他支持传递引用的语言中,我们实际上可以更改传递的变量。

如果Java具有按引用传递的语义,那么我们在上面定义的foo方法将改变当它在BBB行上分配someDogmyDog指向的位置。

可以将参考参数视为传入变量的别名。分配别名后,传入变量也是如此。

回答

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

回答

有点难以理解,但是Java总是复制该点的值,通常该值是引用。因此,我们最终会想到相同的对象而无需考虑...

回答

问题的关键在于,"通过引用"一词中的"引用"一词的含义与Java中"引用"一词的通常含义完全不同。

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

回答

在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只知道按值调用。这意味着对于原始数据类型,我们将使用副本;对于对象,将使用对象引用的副本。但是我认为有一些陷阱。例如,这将不起作用:

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",因为在交换功能中,我们使用的副本对主引用没有影响。但是,如果对象不是一成不变的,则可以更改它,例如:

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制作包装器,使其能够与Strings一起使用:

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这样的不可变对象。

回答

对某些帖子进行了一些更正。

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

Java中的值无关紧要:基本值或者对象的地址(通常是对象的地址),它总是按值传递。

如果Java对象通过引用传递了"行为",那是可变性,并且与传递机制完全没有关系。

我不确定为什么这么混乱,也许是因为这么多Java"程序员"没有经过正式培训,因而不了解内存中到底发生了什么?

回答

Java通过值复制引用。因此,如果我们将其更改为其他内容(例如,使用" new"),则引用不会在方法之外更改。对于本机类型,它总是按值传递。

回答

看一下这段代码。这段代码不会抛出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通过引用传递,则它应该抛出" NullPointerException",因为引用设置为Null。

回答

我不敢相信还没有人提到过芭芭拉·里斯科夫(Barbara Liskov)。当她在1974年设计CLU时,也遇到了相同的术语问题,她针对"按值调用,其中值是参考"。