call-by-value
和call-by-reference
区别是非常关键的。(我个人比较喜欢用call-by-object-sharing
这些天的时间call-by-value[-of-the-reference]
,因为这是从高层次上描述语义的,并且不会与call-by-value
,这是基础实现相冲突。)
call-by-value
和call-by-reference
区别是非常关键的。(我个人比较喜欢用call-by-object-sharing
这些天的时间call-by-value[-of-the-reference]
,因为这是从高层次上描述语义的,并且不会与call-by-value
,这是基础实现相冲突。)
Answers:
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()
in 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(...)
。任何操作是foo
执行上d
是这样的,对于所有的实际目的,它们被执行的aDog
,但它是不是可以改变变量的值aDog
本身。
我只是注意到您引用了我的文章。
Java规范说Java中的所有内容都是按值传递的。Java中没有“通过引用传递”这样的东西。
理解这一点的关键是
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
设置为值42someDog
跟着Dog
它指向(Dog
地址42对象)Dog
(地址42的那个)被要求将他的名字改成MaxDog
创建一个新的。假设他在地址74someDog
给74Dog
它所指向的位置(Dog
地址为74 的对象)Dog
(地址74的那个)被要求将他的名字改成Rowlf现在,让我们考虑一下方法外发生的情况:
有myDog
变化吗?
有钥匙
请记住,这myDog
是一个指针,而不是实际的Dog
,答案是否定的。myDog
仍然具有值42;它仍指向原始文件Dog
(但请注意,由于行“ AAA”,其名称现在为“ Max”-仍是同一条Dog;其myDog
值未更改。)
遵循完全正确地址并更改地址末尾是;但这不会更改变量。
Java的工作原理与C完全相同。您可以分配一个指针,将该指针传递给方法,在该方法中跟随指针并更改所指向的数据。但是,您不能更改该指针指向的位置。
在C ++,Ada,Pascal和其他支持按引用传递的语言中,实际上可以更改传递的变量。
如果Java具有按引用传递的语义,那么foo
我们在BBB行上myDog
分配时,上面定义的方法将更改指向的位置someDog
。
可以将参考参数视为传入变量的别名。分配别名后,传入变量也是如此。
Java总是按值传递参数,而不是按引用传递参数。
让我通过一个例子解释一下:
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");
}
}
我将逐步解释这一点:
声明一个名为f
type 的引用,Foo
并为其分配一个Foo
带有属性的type新对象"f"
。
Foo f = new Foo("f");
从方法方面,声明Foo
具有名称的类型引用,a
并对其进行初始分配null
。
public static void changeReference(Foo a)
调用方法时changeReference
,a
将为引用分配作为参数传递的对象。
changeReference(f);
声明一个名为b
type 的引用,Foo
并为其分配一个Foo
带有属性的type新对象"b"
。
Foo b = new Foo("b");
a = b
使新的分配给参考a
,不 f
,它的属性是对象"b"
。
在调用modifyReference(Foo c)
method时,将c
创建一个引用并将对象分配给attribute "f"
。
c.setAttribute("c");
将更改引用c
指向该对象的对象的属性,并且引用指向该对象的对象也相同f
。
我希望您现在了解如何将对象作为参数传递给Java :)
a
指向与相同的对象f
(并且从未获得f
指向对象的自己的副本),则使用对该对象所做的任何更改也a
应进行修改f
(因为它们都在同一个对象上工作) ),因此在某个点上a
必须获取自己指向的对象的副本f
。
这将使您对Java的实际工作方式有一些见解,以至于在下一次有关Java通过引用传递或通过值传递的讨论中,您只会笑:-)
第一步,请您清除以“ p”“ _ _ _ _ _ _ _ _”开头的单词,尤其是如果您来自其他编程语言时。Java和'p'不能写在同一本书,论坛甚至txt中。
第二步要记住,当您将对象传递给方法时,您传递的是对象引用而不是对象本身。
现在考虑一下对象的引用/变量的作用/是:
在以下内容中(请不要尝试编译/执行此操作...):
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. }
怎么了?
一张图片胜过千言万语:
请注意,anotherReferenceToTheSamePersonObject箭头指向对象而不是变量人!
如果您没有得到它,那就请相信我,并记住,最好说Java是通过价值传递的。好吧,通过参考值。哦,更好的是通过变量值的传递!;)
现在,请随时恨我,但请注意,鉴于此,在谈论方法参数时传递原始数据类型和对象之间没有区别。
您总是传递参考值的位的副本!
Java是按值传递的,因为在方法内部,您可以随意修改所引用的对象,但是无论尝试多么努力,都将永远无法修改将保持引用的传递变量(而不是p _ _ _ _ _ _ _)相同的对象,无论如何!
上面的changeName函数将永远无法修改传递的引用的实际内容(位值)。换句话说,changeName不能使Person人引用另一个Object。
当然,您可以简而言之,只是说 Java是按价值传递的!
public void foo(Car car){ ... }
,car
是本地的foo
,它包含对象的堆的位置?因此,如果我将car
的值更改为car = new Car()
,它将指向堆上的不同Object?如果我通过更改car
属性valu car.Color = "Red"
,则指向堆中的对象car
将被修改。另外,在C#中也一样吗?请回复!谢谢!
System.out.println(person.getName());
会显示什么?“汤姆”还是“杰里”?这是最后一件事,将帮助我克服这种困惑。
Java的始终是按值传递,没有例外,永远。
那么,如何让所有人对此感到困惑,以为Java是通过引用传递的呢,还是认为他们有一个Java充当引用传递的示例呢?关键是,在任何情况下,Java 都不提供对对象本身的值的直接访问。对对象的唯一访问是通过对该对象的引用。因为Java对象总是通过引用而不是直接访问来访问,所以通常将字段和变量以及方法参数称为对象,而在传统上它们只是对对象的引用。混淆源于命名上的这种变化(严格来说,是错误的)。
因此,在调用方法时
int
,long
等),传递值是基本参数的实际值(例如3)。因此,如果您拥有doSomething(foo)
并且public void doSomething(Foo foo) { .. }
两个Foos复制了指向相同对象的引用。
自然地,通过值传递对对象的引用看起来非常像(实际上在实践中是无法区分的)通过引用传递对象。
Java通过值传递引用。
因此,您无法更改传入的引用。
我觉得争论“按引用传递与按值传递”不是很有帮助。
如果您说“ 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的地址。
那么,价值,参考?您说“土豆”。
在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仅具有两种传递类型:内置类型按值传递,对象类型按指针值传递。
Dog **objPtrPtr
到C ++示例中,这样我们可以修改指针“指向”的内容。
基本上,重新分配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
重新定义baz
为null
。
我不敢相信没有人提到过芭芭拉·里斯科夫(Barbara Liskov)。当她在1974年设计的CLU,她就遇到了这个相同的术语问题,她发明了长期的共享调用(也称为按对象共享呼叫和通话的对象),用于调用”这个特定的情况下,由值,其中值参考”。
的问题的关键是,字参考在表达手段的东西从字的通常含义完全不同的“按引用传递” 参考在Java中。
通常在Java中引用是指对对象的引用。但是编程语言理论中引用/值传递的技术术语是指对包含变量的存储单元的引用,这是完全不同的。
在Java中,所有内容都是参考,因此当您遇到类似情况时:
Point pnt1 = new Point(0,0);
Java会执行以下操作:
Java不会通过引用传递方法参数。它通过价值传递它们。我将使用此站点的示例:
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);
参考文献pnt1
和pnt2
被按值传递给棘手的方法,现在你引用其手段pnt1
和pnt2
有自己的copies
命名arg1
和arg2
。所以pnt1
,并arg1
点到同一个对象。(对于pnt2
和arg2
)
在tricky
方法中:
arg1.x = 100;
arg1.y = 100;
接下来的tricky
方法
Point temp = arg1;
arg1 = arg2;
arg2 = temp;
在这里,您首先创建新的temp
点参考,该点将指向与arg1
参考相同的位置。然后,您将参考arg1
移至指向与arg2
参考相同的位置。最后arg2
会指向一样的地方temp
。
从这里的范围tricky
方法走了,你没有获得任何更多的引用:arg1
,arg2
,temp
。但是重要的一点是,当这些引用“存在”时,您所做的一切都会永久性地影响它们所指向的对象。
因此,在执行method之后tricky
,当您返回时main
,您将遇到以下情况:
因此,现在,完全执行程序将是:
X1: 0 Y1: 0
X2: 0 Y2: 0
X1: 100 Y1: 100
X2: 0 Y2: 0
arg1.x = 1; arg1.y = 1; arg2.x = 2; arg2.y = 2;
,例如arg1现在持有pnt2引用,而arg2现在持有pnt1引用,因此,其打印X1: 2 Y1: 2 X2: 1 Y2: 1
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
,如下所示:
希望这会有所帮助。
进入框视图的外部,让我们看一下Assembly或一些低级的内存管理。在CPU级别,如果将任何内容写入内存或CPU寄存器之一,则对任何内容的引用都会立即成为值。(这就是为什么指针是一个很好的定义。它是一个值,同时具有目的)。
内存中的数据有一个位置,在该位置有一个值(字节,字或其他内容)。在组装中,我们有一个方便的解决方案来给特定位置命名(又名变量),但是在编译代码时,汇编程序将简单地替换Name为指定的位置,就像您的浏览器用IP地址替换域名一样。
从根本上讲,从技术上讲不可能在不表示任何语言的情况下传递对任何语言的引用(当它立即成为值时)。
比方说,我们有一个变量富,它的位置是在第47字节内存,其价值是5,我们有另一个变量Ref2Foo这是在第223次内存字节,并且它的值是47.这Ref2Foo可能是一个技术性的变量,不是由程序明确创建的。如果仅查看5和47而不显示任何其他信息,则只会看到两个Values。如果您将它们用作参考,则必须前往5
我们:
(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223] -> 47
(Foo)[47] -> 5
这就是跳转表的工作方式。
如果要使用Foo的值调用方法/函数/过程,则有几种方法可以将变量传递给方法,具体取决于语言及其几种方法调用模式:
在上述每种情况下,都会创建一个值- 现有值的副本 -现在由接收方法来处理它。当您在方法中编写“ Foo”时,它是从EAX中读出的,还是被自动 取消引用或双重取消引用的,所以过程取决于语言的工作方式和/或Foo的类型。这对开发人员是隐藏的,直到她规避了取消引用过程。所以参考就是一个值表示时,因为引用是必须处理的值(在语言级别)。
现在我们将Foo传递给该方法:
Foo = 9
),则仅会影响本地范围,因为您拥有值的副本。从方法内部,我们甚至无法确定原始Foo在内存中的位置。Foo = 11
),则可能会全局更改Foo(取决于语言,即Java或类似Pascal的procedure findMin(x, y, z: integer;
var m: integer);
)。但是,如果该语言允许您绕过取消引用过程,则可以更改47
为49
。那时,如果您读了Foo,似乎已经更改了,因为您已经更改了指向它的本地指针。而且,如果您要在方法(Foo = 12
)中修改此Foo,则可能会执行程序(aka。segfault),因为您将写入与预期不同的内存,甚至可以修改一个预定存放可执行文件的区域。程序并对其进行写入将修改正在运行的代码(Foo现在不在47
)。但是富的价值47
没有全局更改,只是方法内部的更改,因为47
它也是该方法的副本。223
在方法内部进行修改,则会创建与3.或4.中相同的混乱(一个指针,指向一个现在不好的值,该指针再次用作指针),但这仍然是局部的问题,因为223被复制了。但是,如果您能够取消引用Ref2Foo
(即223
),则到达并修改指向的值47
(例如to)49
,它将全局影响Foo ,因为在这种情况下,方法获得的副本,223
但引用的方法47
仅存在一次,并对其进行更改到49
会导致每Ref2Foo
双解引用到一个错误的值。忽略细节,即使是通过引用传递的语言也会将值传递给函数,但是那些函数知道必须将其用于解引用目的。这种“按值传递引用”对程序员而言只是隐藏的,因为它实际上是无用的,并且术语仅是“ 按引用传递”。
严格的值传递也是没有用的,这意味着每次调用以数组为参数的方法时,都必须复制100 MB的数组,因此Java不能严格地值传递。每种语言都会传递对该巨大数组的引用(作为值),并且如果该数组可以在方法内部进行本地更改,或者采用该方法(如Java),则可以采用写时复制机制; (调用者的视图)和几种语言可以修改引用本身的值。
因此,在短期和Java的自己的术语,Java是传址值,其中值可以是:无论是实值或值是一个的表示参考。
addr
做它非类型化,@
类型做它,你可以在以后修改引用变量(除了本地复制的)。但是我不明白你为什么要这么做。您的示例(对象#24601)中的那条纸条仅供参考,其目的是帮助在内存中查找数组,它本身不包含任何数组数据。如果重新启动程序,则即使其内容与上一次运行相同,相同的数组也可能会获得不同的object-id。
AtomicReference
,既不公开引用也不公开其目标,而是包括对目标执行操作的方法;一旦将对象传递给的代码返回,AtomicReference
[其创建者直接引用的对象]应该无效并放弃。那会提供适当的语义,但是会很慢而且很棘手。
Java是按值调用
怎么运行的
您总是传递参考值的位的副本!
如果它是原始数据类型,则这些位包含原始数据类型本身的值,这就是为什么如果我们在方法内部更改标头的值,则它不会反映外部的更改。
如果它是像Foo foo = new Foo()这样的对象数据类型,则在这种情况下,对象地址的副本通过文件shortit传递,假设我们在C:\ desktop处有一个文本文件abc.txt并假设我们将相同的文件,并将其放在C:\ desktop \ abc-shortcut中,因此当您从C:\ desktop \ abc.txt访问文件并写入“ Stack Overflow”并关闭文件,然后再次从快捷方式打开文件时,写“是最大的供程序员学习的在线社区”,那么文件总更改将为“堆栈溢出是最大的供程序员学习的在线社区”意味着每次打开同一文件都从这里打开文件无关紧要,在这里我们可以假设将Foo作为文件,并假定将foo存储在123hd7h(原始地址,例如C:\ desktop \ abc.txt)和234jdid(复制的地址,例如C:\ desktop \ abc-shortcut,实际上包含文件的原始地址)中。因此为了更好的理解,使快捷方式文件和感觉变得更好。
已经有很好的答案可以解决这个问题。我想通过分享一个非常简单的示例(将进行编译)做出一点贡献,以比较 c ++中的“按引用传递”和Java中的“按值传递”之间的行为。
几点:
C ++通过参考示例:
using namespace std;
#include <iostream>
void change (char *&str){ // the '&' makes this a reference parameter
str = NULL;
}
int main()
{
char *str = "not Null";
change(str);
cout<<"str is " << str; // ==>str is <null>
}
Java通过值示例传递“ Java参考”
public class ValueDemo{
public void change (String str){
str = null;
}
public static void main(String []args){
ValueDemo vd = new ValueDemo();
String str = "not null";
vd.change(str);
System.out.println("str is " + str); // ==> str is not null!!
// Note that if "str" was
// passed-by-reference, it
// WOULD BE NULL after the
// call to change().
}
}
编辑
一些人发表了评论,似乎表明他们不是在看我的例子,还是没有得到c ++例子。不知道断开连接在哪里,但是不清楚c ++示例。我在pascal中发布了相同的示例,因为我认为通过引用传递在pascal中看起来更干净,但是我可能是错的。我可能只会让人们更加困惑;我希望不是。
在pascal中,通过引用传递的参数称为“ var参数”。在下面的过程setToNil中,请注意在参数“ ptr”之前的关键字“ var”。当指针传递给该过程时,它将通过reference传递。请注意行为:当此过程将ptr设置为nil(pascal表示NULL)时,它将把参数设置为nil -在Java中无法做到这一点。
program passByRefDemo;
type
iptr = ^integer;
var
ptr: iptr;
procedure setToNil(var ptr : iptr);
begin
ptr := nil;
end;
begin
new(ptr);
ptr^ := 10;
setToNil(ptr);
if (ptr = nil) then
writeln('ptr seems to be nil'); { ptr should be nil, so this line will run. }
end.
编辑2
Ken Arnold,James Gosling(发明Java的人)和David Holmes的“ THE Java Programming Language”摘录,第2章,第2.6.5节
方法的所有参数都“按值”传递。换句话说,方法中参数变量的值是指定为参数的调用程序的副本。
他继续就物体提出了相同的观点。。。
您应该注意,当参数是对象引用时,是“按值”传递的是对象引用(而不是对象本身)。
在同一部分的末尾,他对Java仅通过值传递而从不传递引用进行了更广泛的说明。
Java编程语言不会通过引用传递对象。它 按值传递对象引用。由于相同引用的两个副本引用了相同的实际对象,因此通过一个引用变量进行的更改将通过另一个变量可见。仅有一个参数传递模式- 按值传递- 有助于简化操作。
本书的这一部分对Java中的参数传递以及按引用传递和按值传递之间的区别进行了很好的解释,它是由Java的创建者进行的。我会鼓励任何人阅读它,特别是如果您仍然不相信的话。
我认为这两个模型之间的差异非常微妙,除非您在实际使用传递引用的地方进行了编程,否则很容易错过两个模型之间的差异。
我希望这能解决辩论,但可能不会。
编辑3
我可能对这个职位有些痴迷。可能是因为我觉得Java的制造商无意间散布了错误信息。如果他们没有使用“引用”一词作为指针,而是使用了其他东西(例如dingleberry),那就没有问题了。您可能会说:“ Java通过值而不是通过引用传递dingleberries”,并且不会有人感到困惑。
这就是只有Java开发人员对此有疑问的原因。他们看着“指称”一词,以为自己确切地知道那是什么意思,因此他们甚至不必理会相反的论点。
无论如何,我在一个较旧的帖子中注意到了一条评论,该评论使我非常喜欢一个气球类比。如此之多,以至于我决定将一些剪贴画粘合在一起,制作出一系列动画片来说明这一点。
按值传递引用 -引用的更改不会反映在调用者的作用域中,但对象的更改会反映在调用者的作用域中。这是因为引用已复制,但是原始副本和副本都引用同一对象。
通过引用传递-没有引用的副本。调用者和被调用函数都共享单个引用。对引用或对象数据的任何更改都会反映在调用者的作用域中。
编辑4
我看过有关此主题的文章,这些文章描述了Java中参数传递的低级实现,我认为这很好并且非常有帮助,因为它使抽象概念具体化。但是,对我来说,问题更多的是语言规范中描述的行为,而不是行为的技术实现。这是Java语言规范第8.4.1节的摘录:
调用方法或构造函数时(第15.12节),实际参数表达式的值会在执行方法或构造函数的主体之前,初始化每个声明类型的新创建的参数变量。出现在DeclaratorId中的标识符可用作方法或构造函数主体中的简单名称,以引用形式参数。
这意味着,java在执行方法之前创建了传递的参数的副本。就像谁在大学学习的编译器大多数人来说,我用“龙书”是的编译器的书。在第1章中对“按值调用”和“按引用调用”有很好的描述。“按值调用”描述与Java规范完全匹配。
当我在90年代研究编译器时,我使用了1986年的第一版书,该书比Java早9到10年。但是,我遇到了2007年第二版的副本,其中实际上提到了Java!标有“参数传递机制”的第1.6.6节很好地描述了参数传递。以下是“按值调用”标题下的摘录,其中提到了Java:
在按值调用中,将对实际参数求值(如果它是一个表达式)或将其复制(如果它是一个变量)。该值放置在属于被调用过程的相应形式参数的位置。此方法用在C和Java中,是C ++和大多数其他语言中的常用选项。
据我所知,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这样的不可变对象。
swap(a, b)
,该函数可以(1)从调用者的POV进行交换a
并b
从调用者的POV进行交换,则(2)在静态类型允许的范围内是与类型无关的(这意味着与其他类型一起使用只需要更改a
and 的声明类型b
)。 ,而(3)不需要调用者显式传递指针或名称,则该语言支持按引用传递。
不,它不是通过引用传递的。
Java根据Java语言规范按值传递:
调用方法或构造函数时(第15.12节),实际参数表达式的值会在执行方法或构造函数的主体之前,初始化每个声明类型的新创建的参数变量。出现在DeclaratorId中的标识符可用作方法或构造函数主体中的简单名称,以引用形式参数。
让我尝试通过四个示例来解释我的理解。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:
/ **这种“按价值传递”具有“按引用传递”的感觉
有人说原始类型和“字符串”是“按值传递”,而对象是“按引用传递”。
但是从这个例子中,我们可以理解它实际上只是按值传递,请记住,这里我们将引用作为值传递。即:引用按值传递。这就是为什么能够更改并且在本地范围后仍然适用的原因。但是我们不能在原始范围之外更改实际参考。下一个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语言规范状态
调用方法或构造函数时(第15.12节),实际参数表达式的值将初始化新创建的参数变量在执行方法或构造函数的主体之前,每个声明类型的。
因此,它将参数的值分配(或绑定)到相应的参数变量。
该论点的价值是什么?
让我们考虑引用类型,Java虚拟机规范指出
有三种引用类型:类类型,数组类型和接口类型。它们的值分别引用动态创建的类实例,数组或实现接口的类实例或数组。
在Java语言规范还规定
引用值(通常只是引用)是指向这些对象的指针,还有一个特殊的null引用,它不引用任何对象。
参数(某种引用类型)的值是指向对象的指针。请注意,变量,具有引用类型返回类型的方法的调用以及实例创建表达式(new ...
)都可以解析为引用类型的值。
所以
public void method (String param) {}
...
String var = new String("ref");
method(var);
method(var.toString());
method(new String("ref"));
全部将String
实例的引用值绑定到方法的新创建参数param
。这正是值传递定义的描述。因此,Java是按值传递。
您可以遵循引用来调用方法或访问引用对象的字段这一事实与对话完全无关。引用传递的定义是
这通常意味着该函数可以修改(即分配给)用作参数的变量,该变量将被其调用者看到。
在Java中,修改变量意味着重新分配它。在Java中,如果您在方法中重新分配了变量,则调用者将不会注意到该变量。修改变量引用的对象完全是一个不同的概念。
原始值也在Java虚拟机规范(此处)中定义。类型的值是相应的整数或浮点值,已正确编码(8、16、32、64等位)。
在Java中,仅传递引用,并按值传递:
Java参数全部按值传递(方法使用该引用时,该引用将被复制):
对于基本类型,Java行为很简单:将值复制到基本类型的另一个实例中。
对于对象,这是相同的:对象变量是仅包含使用“ new”关键字创建的对象地址的指针(存储桶),并且像原始类型一样被复制。
该行为可能与基本类型有所不同:由于复制的对象变量包含相同的地址(指向相同的对象)。对象的内容/成员可能仍然可以在方法中进行修改,然后在外部进行访问,从而产生一种幻想,即(包含的)对象本身是通过引用传递的。
“字符串”对象似乎是城市传奇的一个很好的反例,它说“对象通过引用传递”:
实际上,使用一种方法,您将永远无法更新作为参数传递的String的值:
字符串对象,通过声明为final的数组保存字符,该数组不能修改。只有对象的地址可以用“ new”替换为另一个。使用“新”来更新变量,将不会从外部访问对象,因为变量最初是通过值传递并复制的。
strParam.setChar ( i, newValue )
。就是说,字符串和其他任何东西都是按值传递的,并且由于String是非原始类型,因此该值是对用new创建的对象的引用,您可以使用String.intern()进行检查。 。
这种区别,或者也许只是我以前记得与原始海报相同的印象时的方式是:Java总是通过价值传递。Java中的所有对象(在Java中,除基元以外的所有对象)都是引用。这些引用按值传递。
正如许多人之前提到的,Java始终是按价值传递的
这是另一个示例,可以帮助您了解不同之处(经典的swap示例):
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的引用只会在本地范围内更改,而不会在此方法之外更改。
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();
}
obj
(null
)的值传递给init
,而不是对的引用obj
。
我一直认为它是“通过副本”。它是值的副本,可以是原始值或引用。如果它是原语,则是作为值的位的副本;如果它是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的输出:
名称= Maxx
名称= Fido
原始包装器类和字符串是不可变的,因此使用这些类型的任何示例都无法与其他类型/对象一起工作。
与某些其他语言不同,Java不允许您在按值传递和按引用传递之间进行选择-所有参数均按值传递。方法调用可以将两种类型的值传递给方法:原始值的副本(例如,int和double的值)和对对象的引用的副本。
当方法修改原始类型的参数时,对该参数的更改不会影响调用方法中的原始参数值。
当涉及对象时,对象本身不能传递给方法。因此,我们传递了对象的引用(地址)。我们可以使用此引用来操纵原始对象。
Java如何创建和存储对象:创建对象时,我们将对象的地址存储在引用变量中。让我们分析以下语句。
Account account1 = new Account();
“ Account account1”是引用变量的类型和名称,“ =”是赋值运算符,“ new”从系统中请求所需的空间量。创建对象的关键字new右侧的构造函数由关键字new隐式调用。使用assign运算符将创建对象的地址(右值的结果,这是一个称为“类实例创建表达式”的表达式)分配给左值(这是具有名称和类型的参考变量)。
尽管对象的引用是按值传递的,但是方法仍然可以通过使用对象引用的副本调用其公共方法来与所引用的对象进行交互。由于存储在参数中的引用是作为参数传递的引用的副本,因此被调用方法中的参数和调用方法中的参数引用内存中的同一对象。
出于性能考虑,将引用传递给数组而不是数组对象本身是有意义的。因为Java中的所有内容都是按值传递的,所以如果传递数组对象,则将传递每个元素的副本。对于大型阵列,这将浪费时间并消耗大量的元素副本存储空间。
在下面的图像中,您可以看到main方法中有两个参考变量(在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方法(数组2)中还有另一个引用变量,它指向数组c。如果我们要说
array1 = array2;
在reverseArray方法中,则方法reverseArray中的引用变量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数组。没有引用变量指向对象(数组)因此,它符合垃圾收集的条件。
您也可以将main中的array2的值分配给array1。array1将开始指向b。
简而言之,Java对象具有一些非常特殊的属性。
一般来说,Java有原始类型(int
,bool
,char
,double
,等等)由值直接传递。然后,Java具有对象(从派生的所有对象java.lang.Object
)。实际上,对象总是通过引用(引用是您无法触摸的指针)来处理的。这意味着实际上,对象是通过引用传递的,因为引用通常并不有趣。但是,这确实意味着您无法更改指向哪个对象,因为引用本身是通过值传递的。
这听起来很奇怪并且令人困惑吗?让我们考虑一下C如何实现按引用传递和按值传递。在C语言中,默认约定是按值传递。void foo(int x)
通过值传递一个int值。void foo(int *x)
是一个不需要an的函数int a
,而是一个指向int:的指针foo(&a)
。可以将它与&
运算符一起使用以传递可变地址。
将其带到C ++,我们有参考。引用基本上是(在这种情况下)语法的糖,它隐藏了等式的指针部分:void foo(int &x)
由调用foo(a)
,其中编译器本身知道它是引用,并且a
应该传递非引用的地址。在Java中,所有引用对象的变量实际上都是引用类型,实际上,对于大多数意图和目的,都强制按引用进行调用,而没有C ++提供的精细控制(和复杂性)。