Java是"按引用传递"还是"按值传递"?
我一直以为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行上分配someDog
时myDog
指向的位置。
可以将参考参数视为传入变量的别名。分配别名后,传入变量也是如此。
回答
只是为了显示对比,请比较以下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时,也遇到了相同的术语问题,她针对"按值调用,其中值是参考"。