我一直认为 Java 使用pass-by-reference。
但是,我看到一篇博客文章声称 Java 使用pass-by-value。
我不认为我理解他们所做的区分。
解释是什么?
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
不会更改。如果它是通过引用传递的,那么in将在调用 . 之后返回。foo
Dog
"Fifi"
aDog.getName()
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
执行 on的任何操作d
都是这样的,出于所有实际目的,它们都执行 on aDog
,但不可能更改变量aDog
本身的值。
有关按引用传递和按值传递的更多信息,请参阅以下 SO 答案:https ://stackoverflow.com/a/430958/6005228 。这更彻底地解释了两者背后的语义和历史,也解释了为什么 Java 和许多其他现代语言在某些情况下似乎两者兼而有之。
我刚刚注意到你引用了我的文章。
Java 规范说 Java 中的所有内容都是按值传递的。Java 中没有“按引用传递”之类的东西。
理解这一点的关键是
Dog myDog;
不是狗;它实际上是一个指向Dog 的指针。在 Java 中使用术语“引用”非常具有误导性,并且是造成这里大部分混乱的原因。他们所说的“引用”行为/感觉更像是我们在大多数其他语言中所说的“指针”。
这意味着,当你有
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 和其他支持 pass-by-reference 的语言中,您实际上可以更改被传递的变量。
如果 Java 具有传递引用语义,那么我们上面定义的方法在 BBB 行分配时foo
会改变指向的位置。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
具有属性的新类型对象"f"
。
Foo f = new Foo("f");
在方法方面,声明Foo
了一个带有名称的类型引用,a
并且它最初是被赋值的null
。
public static void changeReference(Foo a)
当您调用方法changeReference
时,将为引用a
分配作为参数传递的对象。
changeReference(f);
声明一个名为b
type的引用,Foo
并为其分配一个Foo
具有属性的新类型对象"b"
。
Foo b = new Foo("b");
a = b
对属性为 的对象的引用而不是 进行新a
的赋值 。f
"b"
当您调用modifyReference(Foo c)
方法时,c
会创建一个引用并为该对象分配属性"f"
。
c.setAttribute("c");
将更改引用指向它的对象的属性,并且引用c
指向它的对象是同一个对象f
。
我希望您现在了解在 Java 中将对象作为参数传递是如何工作的 :)
Java 永远是按值传递的,没有例外。
那么,任何人怎么可能对此感到困惑,并认为 Java 是通过引用传递的,或者认为他们有一个 Java 充当通过引用传递的示例?关键是 Java在任何情况下都不会提供对对象本身值的直接访问。对对象的唯一访问是通过对该对象的引用。因为 Java 对象总是通过引用而不是直接访问,所以通常将字段、变量和方法参数称为对象,而学究式地它们只是对对象的引用。混乱源于命名法的这种(严格来说,不正确的)变化。
所以,当调用一个方法时
int
、long
等),按值传递是原始参数的实际值(例如,3)。因此,如果您有doSomething(foo)
并且public void doSomething(Foo foo) { .. }
两个 Foo 复制了指向相同对象的引用。
自然地,通过值传递对对象的引用看起来非常像(实际上与通过引用传递对象没有区别)。
这将使您了解 Java 的真正工作原理,以至于在您下次讨论 Java 通过引用传递或按值传递时,您会微笑 :-)
第一步,请从您的脑海中抹去以“p”“_ _ _ _ _ _ _”开头的单词,特别是如果您来自其他编程语言。Java 和“p”不能写在同一本书、论坛甚至 txt 中。
第二步请记住,当您将 Object 传递给方法时,您传递的是 Object 引用而不是 Object 本身。
现在想想对象的引用/变量的作用/是什么:
在下面(请不要尝试编译/执行这个......):
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 是按值传递的!
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 并且没有分配新的堆空间,但在堆栈上创建了一个新的标识符。然后我们调用shout()
:创建一个新的堆栈帧,并创建一个新的标识符,name
并为其分配已经存在的字符串的地址。
那么,价值,参考?你说“土豆”。
在 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
将重新定义baz
为null
.
Java 按值传递对对象的引用。
我不敢相信还没有人提到芭芭拉·利斯科夫。当她在 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
.Sopnt1
和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
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 传递给函数时,它不会直接传递对象 test 的实际引用值,而是会创建 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
,如下所示:
希望这会有所帮助。
已经有很好的答案涵盖了这一点。我想通过分享一个非常简单的示例(将编译)来对比 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”。当一个指针传递给这个过程时,它将通过引用传递。请注意行为:当此过程将 ptr 设置为 nil 时(这是帕斯卡对 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 编程语言”的部分摘录,第 2 章,第 2.6.5 节
方法的所有参数都是“按值”传递的。换句话说,方法中参数变量的值是指定为参数的调用程序的副本。
他继续对对象提出同样的观点。. .
您应该注意,当参数是对象引用时,“按值”传递的是对象引用,而不是对象本身。
在同一部分的结尾,他更广泛地声明了 java 仅通过值传递,从不通过引用传递。
Java 编程语言不通过引用传递对象;它 按值传递对象引用。因为同一个引用的两个副本引用同一个实际对象,所以通过一个引用变量所做的更改通过另一个引用变量是可见的。只有一个参数传递模式——按值传递——这有助于让事情变得简单。
本书的这一部分对 Java 中的参数传递以及按引用传递和按值传递之间的区别进行了很好的解释,它是由 Java 的创建者编写的。我会鼓励任何人阅读它,特别是如果你仍然不相信的话。
我认为这两个模型之间的差异非常微妙,除非您在实际使用传递引用的地方完成了编程,否则很容易忽略两个模型不同的地方。
我希望这能解决争论,但可能不会。
编辑 3
我可能对这篇文章有点着迷。可能是因为我觉得 Java 的制造者无意中传播了错误信息。如果他们不使用“引用”这个词来表示指针,而是使用其他东西,比如 dingleberry,那就没有问题了。你可以说,“Java 通过值而不是通过引用传递 dingleberries”,没有人会感到困惑。
这就是只有 Java 开发人员对此有问题的原因。他们看着“参考”这个词,并认为他们确切地知道这意味着什么,所以他们甚至不费心去考虑相反的论点。
无论如何,我注意到一篇较早的帖子中的评论,它做了一个我非常喜欢的气球类比。如此之多,以至于我决定将一些剪贴画粘合在一起,制作一组卡通片来说明这一点。
按值传递引用——对引用的更改不会反映在调用者的范围内,但对对象的更改会。这是因为引用被复制,但原始和副本都引用同一个对象。
通过引用传递——没有引用的副本。调用者和被调用的函数共享单一引用。对引用或对象数据的任何更改都会反映在调用者的范围内。
编辑 4
我看过关于这个主题的帖子,这些帖子描述了 Java 中参数传递的低级实现,我认为这很棒而且非常有帮助,因为它使抽象的想法变得具体。然而,对我来说,问题更多是关于语言规范中描述的行为,而不是关于行为的技术实现。这是Java 语言规范第 8.4.1 节的摘录:
当调用方法或构造函数时(第 15.12 节),实际参数表达式的值在执行方法或构造函数的主体之前初始化新创建的参数变量,每个声明的类型。出现在 DeclaratorId 中的 Identifier 可以用作方法体或构造函数的简单名称,以引用形式参数。
这意味着,java 在执行方法之前创建传递参数的副本。像大多数在大学学习编译器的人一样,我使用了编译器书籍“The Dragon Book ”。它在第 1 章中对“按值调用”和“按引用调用”进行了很好的描述。按值调用的描述与 Java Specs 完全匹配。
当我在 90 年代学习编译器时,我使用了 1986 年的第一版,它比 Java 早了大约 9 或 10 年。但是,我刚刚遇到了2007 年第二版的副本,其中实际上提到了 Java!标有“参数传递机制”的第 1.6.6 节很好地描述了参数传递。这是标题“按值调用”下的摘录,其中提到了 Java:
在按值调用中,实际参数被评估(如果它是一个表达式)或复制(如果它是一个变量)。该值被放置在属于被调用过程的相应形式参数的位置。此方法在 C 和 Java 中使用,并且是 C++ 以及大多数其他语言中的常用选项。
Java 是按值传递(堆栈内存)
这个怎么运作
我们先来了解一下java存储原始数据类型和对象数据类型的地方。
原始数据类型本身和对象引用存储在堆栈中。对象本身存储在堆中。
这意味着,堆栈内存存储原始数据类型以及对象的地址。
而且您总是传递引用值的位的副本。
如果它是原始数据类型,那么这些复制的位包含原始数据类型本身的值,这就是为什么当我们在方法内部更改参数的值时,它不会反映外部的更改。
如果它是像Foo foo=new Foo()这样的对象数据类型,那么在这种情况下,对象地址的副本会像文件快捷方式一样传递,假设我们在C:\desktop有一个文本文件abc.txt并假设我们创建了相同的文件并将其放在C:\desktop\abc-shortcut中,因此当您从C:\desktop\abc.txt访问该文件并写入“堆栈溢出”并关闭文件并再次从快捷方式打开文件时,您写“是程序员学习的最大在线社区”,那么总文件更改将是“堆栈溢出是程序员学习的最大在线社区”这意味着从哪里打开文件并不重要,每次我们访问同一个文件时,在这里我们可以假设Foo作为一个文件并假设 foo 存储在123hd7h(原始地址,如C:\desktop\abc.txt)地址和234jdid(复制的地址,如C:\desktop\abc-shortcut实际上包含文件的原始地址)..所以为了更好地理解制作快捷方式文件和感觉。
获得框外视图,让我们看一下汇编或一些低级内存管理。在 CPU 级别,如果将任何内容写入内存或 CPU 寄存器之一,则对任何内容的引用都会立即成为一个值。(这就是为什么指针是一个很好的定义。它是一个值,同时具有目的)。
内存中的数据有一个位置,并且在该位置有一个值(字节,字,等等)。在汇编中,我们有一个方便的解决方案来为某个位置(也称为变量)提供名称,但是在编译代码时,汇编器只需将名称替换为指定的位置,就像您的浏览器将域名替换为 IP 地址一样。
深入到核心,技术上不可能在不表示任何语言的情况下传递对任何东西的引用(当它立即成为一个值时)。
假设我们有一个变量 Foo,它的位置在内存中的第 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 ,您可能会 FUBAR 程序的执行(又名 segfault),因为您将写入与预期不同的内存,您甚至可以修改一个注定要保存可执行文件的区域程序并写入它将修改正在运行的代码(Foo 现在不在47
)。但是 Foo 的价值是47
没有全局更改,只有方法内部的那个,因为47
也是方法的副本。223
在方法内部进行修改,它会产生与 3. 或 4. 中相同的混乱(一个指针,指向一个现在错误的值,再次用作指针)但这仍然是一个本地的问题,因为 223 被复制了。但是,如果您能够取消引用Ref2Foo
(即223
),到达并修改指向的值47
,例如 to 49
,它将影响 Foo全局,因为在这种情况下,方法获得了一个副本,223
但被引用的47
只存在一次,并改变它to49
将导致每次Ref2Foo
双重取消引用到错误的值。挑剔无关紧要的细节,即使是通过引用传递的语言也会将值传递给函数,但这些函数知道他们必须使用它来解除引用。这个 pass-the-reference-as-value 只是对程序员隐藏,因为它实际上没有用,而且术语只是pass-by-reference。
严格的传值也是没用的,这意味着每次调用以数组为参数的方法时都必须复制一个 100 MB 的数组,因此 Java 不能严格传值。每种语言都会传递对这个巨大数组的引用(作为值),并且如果该数组可以在方法内部本地更改,或者允许方法(如 Java)全局修改数组(从调用者的观点)和一些语言允许修改引用本身的值。
所以简而言之,用 Java 自己的术语来说,Java 是按值传递的,其中value可以是:真实值或表示引用的值。
据我所知,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);
}
但是,您可以像这样为 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 等不可变对象则无法修改。
在 Java 中,方法参数都是按值传递的:
Java 参数都是按值传递的(方法使用时会复制值或引用):
对于原始类型,Java 行为很简单:将值复制到原始类型的另一个实例中。
在对象的情况下,这是相同的:对象变量是使用“new”关键字创建的引用(仅保存对象地址而不是原始值的内存桶),并且像原始类型一样被复制。
该行为可能与原始类型不同:因为复制的对象变量包含相同的地址(到相同的对象)。对象的内容/成员可能仍会在方法内进行修改,然后在外部进行访问,从而产生(包含)对象本身是通过引用传递的错觉。
“字符串”对象似乎是都市传说“对象通过引用传递”的一个很好的反例:
实际上,使用一种方法,您将永远无法更新作为参数传递的 String 的值:
一个字符串对象,通过一个声明为final且不能修改的数组来保存字符。只有对象的地址可能被另一个使用“new”替换。使用“new”来更新变量,不会让从外部访问对象,因为变量最初是按值传递并复制的。
不,它不是通过引用传递的。
Java 根据 Java 语言规范按值传递:
当调用方法或构造函数时(第 15.12 节),实际参数表达式的值会在执行方法或构造函数的主体之前初始化新创建的参数变量(每个声明的类型)。出现在 DeclaratorId 中的 Identifier 可以用作方法体或构造函数的简单名称,以引用形式参数。
让我试着用四个例子来解释我的理解。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:
/** 这个'Pass By Value'有一种'Pass By Reference'的感觉
有人说原始类型和“字符串”是“按值传递”,而对象是“按引用传递”。
但是从这个例子中,我们可以理解它实际上只是按值传递,请记住,这里我们将引用作为值传递。即:引用是按值传递的。这就是为什么能够改变并且在本地范围之后仍然适用的原因。但是我们不能在原始范围之外更改实际引用。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
与 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语言规范还指出
引用值(通常只是引用)是指向这些对象的指针,以及一个特殊的空引用,它不引用任何对象。
参数(某些引用类型)的值是指向对象的指针。请注意,变量、具有引用类型返回类型的方法调用以及实例创建表达式 ( new ...
) 都解析为引用类型值。
所以
public void method (String param) {}
...
String variable = new String("ref");
method(variable);
method(variable.toString());
method(new String("ref"));
all 将对实例的引用的值绑定String
到方法的新创建参数param
. 这正是 pass-by-value 的定义所描述的。因此,Java 是按值传递的。
您可以按照引用来调用方法或访问被引用对象的字段这一事实与对话完全无关。引用传递的定义是
这通常意味着该函数可以修改(即分配给)用作参数的变量——它的调用者将看到的东西。
在 Java 中,修改变量意味着重新分配它。在 Java 中,如果您在方法中重新分配变量,调用者不会注意到它。修改变量引用的对象完全是一个不同的概念。
Java 虚拟机规范中也定义了原始值,此处为。该类型的值是相应的整数或浮点值,经过适当编码(8、16、32、64 等位)。
区别,或者也许只是我记得的方式,因为我曾经和原始海报有相同的印象是这样的: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;
}
}
印刷:
之前:a = 2,b = 3
之后: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 的输出:
名称= Maxx
名称= Fido
原始包装类和字符串是不可变的,因此使用这些类型的任何示例都不会与其他类型/对象一样工作。
与其他一些语言不同,Java 不允许您在按值传递和按引用传递之间进行选择——所有参数都按值传递。方法调用可以将两种类型的值传递给方法——原始值的副本(例如,int 和 double 的值)和对象引用的副本。
当方法修改原始类型参数时,对参数的更改不会影响调用方法中的原始参数值。
当涉及到对象时,对象本身不能传递给方法。所以我们传递对象的引用(地址)。我们可以使用这个引用来操作原始对象。
Java 如何创建和存储对象:当我们创建一个对象时,我们将对象的地址存储在一个引用变量中。我们来分析下面的语句。
Account account1 = new Account();
“Account account1”是引用变量的类型和名称,“=”是赋值运算符,“new”要求系统提供所需的空间量。创建对象的关键字 new 右侧的构造函数由关键字 new 隐式调用。使用赋值运算符将创建对象的地址(右值的结果,这是一个称为“类实例创建表达式”的表达式)分配给左值(这是一个指定名称和类型的引用变量)。
尽管对象的引用是按值传递的,但方法仍然可以通过使用对象引用的副本调用其公共方法来与被引用对象交互。由于存储在参数中的引用是作为参数传递的引用的副本,因此被调用方法中的参数和调用方法中的参数引用内存中的同一对象。
出于性能原因,传递对数组的引用而不是数组对象本身是有意义的。因为 Java 中的所有内容都是按值传递的,所以如果传递数组对象,则将传递每个元素的副本。对于大型数组,这将浪费时间并为元素的副本消耗大量存储空间。
在下图中,您可以看到我们在 main 方法中有两个引用变量(这些在 C/C++ 中称为指针,我认为这个术语更容易理解这个特性。)。原始变量和参考变量保存在堆栈内存中(下图中的左侧)。array1 和 array2 引用变量“point”(如 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 方法中,则方法 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 数组。没有引用变量指向对象(数组) a。所以它有资格进行垃圾收集。
您还可以将 main 中的 array2 的值分配给 array1。array1 将开始指向 b。
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();
}
长话短说,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++ 中,我们有参考。引用基本上(在这种情况下)是隐藏等式指针部分的语法糖:void foo(int &x)
被调用foo(a)
,编译器本身知道它是一个引用并且a
应该传递非引用的地址。在 Java 中,所有引用对象的变量实际上都是引用类型,实际上强制按引用调用大多数意图和目的,而没有例如 C++ 提供的细粒度控制(和复杂性)。
一些帖子的一些更正。
C 不支持通过引用传递。它总是按值传递。C++ 确实支持按引用传递,但不是默认值,而且非常危险。
Java中的值是什么并不重要:对象的原始或地址(大致),它总是按值传递。
如果 Java 对象的“行为”就像是通过引用传递一样,那是可变性的一个属性,与传递机制完全无关。
我不确定为什么这会如此令人困惑,也许是因为这么多 Java“程序员”没有经过正式培训,因此不了解内存中到底发生了什么?
Java 编程语言中最大的困惑之一是 Java 是按值传递还是按引用传递。
首先,我们应该明白什么是值传递或引用传递。
传值:将方法参数值复制到另一个变量中,然后将复制的对象传递,这就是为什么它被称为传值。
通过引用传递:将实际参数的别名或引用传递给方法,这就是为什么它被称为传递引用。
假设我们有一个像下面这样的类 Balloon。
public class Balloon {
private String color;
public Balloon(){}
public Balloon(String c){
this.color=c;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
我们有一个简单的程序,它有一个通用方法来交换两个对象,类如下所示。
public class Test {
public static void main(String[] args) {
Balloon red = new Balloon("Red"); //memory reference 50
Balloon blue = new Balloon("Blue"); //memory reference 100
swap(red, blue);
System.out.println("red color="+red.getColor());
System.out.println("blue color="+blue.getColor());
foo(blue);
System.out.println("blue color="+blue.getColor());
}
private static void foo(Balloon balloon) { //baloon=100
balloon.setColor("Red"); //baloon=100
balloon = new Balloon("Green"); //baloon=200
balloon.setColor("Blue"); //baloon = 200
}
//Generic swap method
public static void swap(Object o1, Object o2){
Object temp = o1;
o1=o2;
o2=temp;
}
}
当我们执行上述程序时,我们得到以下输出。
red color=Red
blue color=Blue
blue color=Red
如果您查看输出的前两行,很明显 swap 方法不起作用。这是因为 Java 是按值传递的,这个 swap() 方法测试可以与任何编程语言一起使用,以检查它是按值传递还是按引用传递。
让我们一步一步分析程序执行。
Balloon red = new Balloon("Red");
Balloon blue = new Balloon("Blue");
当我们使用 new 操作符创建一个类的实例时,实例被创建并且变量包含了保存对象的内存的引用位置。对于我们的示例,假设“red”指向 50,“blue”指向 100,这是两个 Balloon 对象的内存位置。
现在,当我们调用 swap() 方法时,会创建两个新变量 o1 和 o2,分别指向 50 和 100。
所以下面的代码片段解释了 swap() 方法执行中发生的事情。
public static void swap(Object o1, Object o2){ //o1=50, o2=100
Object temp = o1; //temp=50, o1=50, o2=100
o1=o2; //temp=50, o1=100, o2=100
o2=temp; //temp=50, o1=100, o2=50
} //method terminated
请注意,我们正在更改 o1 和 o2 的值,但它们是“红色”和“蓝色”参考位置的副本,因此实际上,“红色”和“蓝色”的值没有变化,因此输出也没有变化。
如果你已经了解了这么多,你就很容易理解混乱的原因了。由于变量只是对对象的引用,我们会感到困惑,因为我们正在传递引用,所以 Java 是通过引用传递的。但是,我们正在传递引用的副本,因此它是按值传递的。我希望它现在可以消除所有疑问。
现在我们来分析foo()方法的执行。
private static void foo(Balloon balloon) { //baloon=100
balloon.setColor("Red"); //baloon=100
balloon = new Balloon("Green"); //baloon=200
balloon.setColor("Blue"); //baloon = 200
}
当我们调用方法时,第一行是重要的,该方法是在引用位置的 Object 上调用的。此时,气球指向 100,因此它的颜色变为红色。
在下一行中,气球引用更改为 200,并且执行的任何进一步的方法都发生在内存位置 200 的对象上,并且对内存位置 100 的对象没有任何影响。这解释了我们程序输出的第三行打印蓝色=红色。
我希望上面的解释能消除所有的疑惑,只要记住变量是引用或指针,它的副本是传递给方法的,所以 Java 总是按值传递的。当您了解堆和堆栈内存以及存储不同对象和引用的位置时,会更加清楚。
Java 通过 VALUE 和ONLY传递参数。
长话短说:
对于那些来自 C# 的人:没有“out”参数。
对于那些来自 PASCAL 的人:没有“var”参数。
这意味着您无法更改对象本身的引用,但您始终可以更改对象的属性。
一种解决方法是改用StringBuilder
参数String
。而且你总是可以使用数组!
这是我回答问题的最佳方式...
首先,我们必须了解,在 Java 中,参数传递行为......
public void foo(Object param)
{
// some code in foo...
}
public void bar()
{
Object obj = new Object();
foo(obj);
}
和...完全一样
public void bar()
{
Object obj = new Object();
Object param = obj;
// some code in foo...
}
不考虑堆栈位置,这与本讨论无关。
所以,事实上,我们在 Java 中寻找的是变量赋值是如何工作的。我在文档中找到了它:
您将遇到的最常见的运算符之一是简单的赋值运算符 "=" [...] ,它将右侧的值分配给左侧的操作数:
国际节奏= 0;
国际速度= 0;
int 齿轮 = 1;该运算符也可用于对象以分配对象引用[...]
很清楚该运算符如何以两种不同的方式起作用:分配值和分配引用。最后一个,当它是一个对象时……第一个,当它不是一个对象时,也就是说,当它是一个原始对象时。但是,我们能理解 Java 的函数参数可以是传值和传引用吗?
真相在代码中。让我们尝试一下:
public class AssignmentEvaluation
{
static public class MyInteger
{
public int value = 0;
}
static public void main(String[] args)
{
System.out.println("Assignment operator evaluation using two MyInteger objects named height and width\n");
MyInteger height = new MyInteger();
MyInteger width = new MyInteger();
System.out.println("[1] Assign distinct integers to height and width values");
height.value = 9;
width.value = 1;
System.out.println("-> height is " + height.value + " and width is " + width.value + ", we are different things! \n");
System.out.println("[2] Assign to height's value the width's value");
height.value = width.value;
System.out.println("-> height is " + height.value + " and width is " + width.value + ", are we the same thing now? \n");
System.out.println("[3] Assign to height's value an integer other than width's value");
height.value = 9;
System.out.println("-> height is " + height.value + " and width is " + width.value + ", we are different things yet! \n");
System.out.println("[4] Assign to height the width object");
height = width;
System.out.println("-> height is " + height.value + " and width is " + width.value + ", are we the same thing now? \n");
System.out.println("[5] Assign to height's value an integer other than width's value");
height.value = 9;
System.out.println("-> height is " + height.value + " and width is " + width.value + ", we are the same thing now! \n");
System.out.println("[6] Assign to height a new MyInteger and an integer other than width's value");
height = new MyInteger();
height.value = 1;
System.out.println("-> height is " + height.value + " and width is " + width.value + ", we are different things again! \n");
}
}
这是我运行的输出:
使用名为 height 和 width 的两个 MyInteger 对象进行赋值运算符评估 [1] 为高度和宽度值分配不同的整数 -> 高度为 9 宽度为 1,我们是不同的东西! [2] 将高度值分配给宽度值 -> 高度为 1 宽度为 1,我们现在是一样的吗? [3] 为高度值分配一个除宽度值以外的整数 -> 高度为 9 宽度为 1,我们还是不同的东西! [4] 指定宽度对象的高度 -> 高度为 1 宽度为 1,我们现在是一样的吗? [5] 将宽度值以外的整数赋给高度值 -> 高度为 9,宽度为 9,我们现在是一样的! [6] 为高度分配一个新的 MyInteger 和一个除宽度值之外的整数 -> 高度为 1 宽度为 9,我们又是不同的东西了!
在[2]中,我们有不同的对象并将一个变量的值分配给另一个。但是在[3]中分配新值后,对象具有不同的值,这意味着在[2]中分配的值是原始变量的副本,通常称为pass-by-value,否则,在[3]中打印的值]应该是一样的。
在[4]中,我们仍然有不同的对象并将一个对象分配给另一个对象。在[5]中分配新值后,对象具有相同的值,这意味着在[4]中分配的对象不是另一个对象的副本,应该称为pass-by-reference。但是,如果我们仔细查看[6],我们不能确定没有完成任何副本...... ?????
我们不能这么确定,因为在[6]中对象是相同的,然后我们为其中一个分配了一个新对象,之后,这些对象具有不同的值!如果它们是相同的,它们现在怎么能不同呢?他们在这里也应该是一样的!???
我们需要记住文档以了解发生了什么:
此运算符也可用于对象以分配对象引用
所以我们的两个变量是存储引用...我们的变量在[4]之后有相同的引用,在[6]之后有不同的引用...如果这样的事情是可能的,这意味着对象的分配是通过对象的副本完成的引用,否则,如果不是引用的副本,则[6]中变量的打印值应该相同。所以对象(引用),就像原语一样,通过赋值复制到变量中,人们通常称之为pass-by-value。这是Java中唯一的传递。
这真的非常非常简单:
对于原始类型的变量(例如int
、boolean
、char
等...),当您将其名称用作方法参数时,您将传递其中包含的值(5
、true
或'c'
)。该值被“复制”,并且即使在方法调用之后变量仍保留其值。
对于引用类型的变量(例如String
、Object
等),当您将其名称用于方法参数时,您将传递其中包含的值(“指向”对象的引用值)。此引用值被“复制”,并且即使在方法调用之后变量仍保留其值。引用变量一直“指向”同一个对象。
无论哪种方式,你总是按价值传递东西。
将其与 C++ 中可以使用 的方法进行比较int&
,或者在 C# 中可以使用 a (尽管在这种情况下,在将变量的名称传递给方法ref int
时还必须使用修饰符。)ref
Java 按值复制引用。因此,如果您将其更改为其他内容(例如,使用new
),则引用不会在方法之外更改。对于本机类型,它总是按值传递。
在所有答案中,我们看到 Java 传递值,或者更确切地说,正如@Gevorg 所写:“传递变量值的副本”,这是我们应该一直牢记的想法。
我专注于帮助我理解这个想法的例子,它是对以前答案的补充。
从 [1] 在 Java 中,您总是通过副本传递参数;也就是说,您总是在函数内创建值的新实例。但是有些行为会让你认为你是通过引用传递的。
通过副本传递:当将变量传递给方法/函数时,会生成一个副本(有时我们会听到当您传递原语时,您正在制作副本)。
通过引用传递:当变量传递给方法/函数时,方法/函数中的代码对原始变量进行操作(您仍然通过副本传递,但是对复杂对象内的值的引用是两个版本的一部分变量,包括函数内部的原始版本和版本。复杂对象本身被复制,但内部引用被保留)
[参考文献 1] 中的示例
void incrementValue(int inFunction){
inFunction ++;
System.out.println("In function: " + inFunction);
}
int original = 10;
System.out.print("Original before: " + original);
incrementValue(original);
System.out.println("Original after: " + original);
We see in the console:
> Original before: 10
> In Function: 11
> Original after: 10 (NO CHANGE)
[参考文献 2] 中的示例
很好地展示了机械 表最长 5 分钟
[ref 1] 中的示例 (请记住,数组是对象)
void incrementValu(int[] inFuncion){
inFunction[0]++;
System.out.println("In Function: " + inFunction[0]);
}
int[] arOriginal = {10, 20, 30};
System.out.println("Original before: " + arOriginal[0]);
incrementValue(arOriginal[]);
System.out.println("Original before: " + arOriginal[0]);
We see in the console:
>Original before: 10
>In Function: 11
>Original before: 11 (CHANGE)
复杂对象本身被复制,但内部引用被保留。
[参考文献 3] 中的示例
package com.pritesh.programs;
class Rectangle {
int length;
int width;
Rectangle(int l, int b) {
length = l;
width = b;
}
void area(Rectangle r1) {
int areaOfRectangle = r1.length * r1.width;
System.out.println("Area of Rectangle : "
+ areaOfRectangle);
}
}
class RectangleDemo {
public static void main(String args[]) {
Rectangle r1 = new Rectangle(10, 20);
r1.area(r1);
}
}
矩形的面积为 200 长度=10 宽度=20
我想分享的 最后一件事是讲座的这一刻:内存分配 ,我发现它对理解 Java 传递值或者更确切地说是 @Gevorg 所写的“传递变量值的传递”非常有帮助.
Java 是通过常量引用传递的,其中传递了引用的副本,这意味着它基本上是按值传递。如果类是可变的,则可以更改引用的内容,但不能更改引用本身。换句话说,地址不能更改,因为它是按值传递的,但地址指向的内容可以更改。在不可变类的情况下,引用的内容也不能更改。
当我说按值传递时,它意味着每当调用者调用被调用者时,参数(即:要传递给另一个函数的数据)被复制并放置在形式参数中(被调用者用于接收输入的局部变量)。Java 仅在值传递环境中进行从一个函数到另一个函数的数据通信。
重要的一点是要知道,即使是 C 语言也严格地仅按值传递:
即:数据从调用者复制到被调用者,并且被调用者执行的操作更多地位于同一内存位置,我们传递给它们的是我们从 (&) 运算符获得的那个位置的地址和形式参数中使用的标识符被声明为一个指针变量 (*),我们可以使用它进入内存位置以访问其中的数据。
因此,这里的形式参数只不过是该位置的别名。并且在该变量的范围(标识该位置)处于活动状态时,对该位置所做的任何修改都是可见的。
在 Java 中,没有指针的概念(即:没有什么叫做指针变量),虽然我们可以将引用变量视为技术上的指针,在 Java 中我们称之为句柄。我们在 java 中将指向地址的指针称为句柄的原因是因为指针变量不仅能够执行单次解引用,而且能够执行多次解引用,例如:int *p;
在 P 中表示 p 指向一个整数,int **p;
在 C 中表示 p 是指向一个指向整数的指针,我们在 Java 中没有这种功能,因此将其作为句柄说是绝对正确和技术上合法的,C 中也有指针算术规则。它允许对带有约束的指针执行算术运算。
在 C 语言中,我们将这种传递地址并使用指针变量接收它们的机制称为按引用传递,因为我们在形式参数中传递它们的地址并作为指针变量接收它们,但在编译器级别,该地址被复制到指针变量中(因为这里的数据是地址,即使是它的数据),因此我们可以 100% 确定 C 是严格按值传递的(因为我们只传递数据)
(如果我们直接在 C 中传递数据,我们称之为按值传递。)
在java中,当我们做同样的事情时,我们用句柄做;因为它们不像 in (如上所述)那样被称为指针变量,即使我们正在传递引用,我们也不能说它是按引用传递的,因为我们没有在 Java 中使用指针变量来收集它。
因此Java严格使用传值机制
这么长的答案。给个简单的吧:
简而言之,您不能修改传递的任何参数的值,但可以调用方法或更改传递的对象引用的属性。
Java 总是使用按值调用。这意味着该方法获取所有参数值的副本。
考虑接下来的 3 种情况:
public static void increment(int x) { x++; }
int a = 3;
increment(a);
x 将复制 a 的值并增加 x,a 保持不变
public static void increment(Person p) { p.age++; }
Person pers = new Person(20); // age = 20
increment(pers);
p 将复制 pers 的参考值并增加年龄字段,变量引用同一个对象,因此年龄发生了变化
public static void swap(Person p1, Person p2) {
Person temp = p1;
p1 = p2;
p2 = temp;
}
Person pers1 = new Person(10);
Person pers2 = new Person(20);
swap(pers1, pers2);
调用 swap p1 后,p2 从 pers1 和 pers2 复制参考值,正在与值交换,因此 pers1 和 pers2 保持不变
所以。在将引用值的副本传递给该对象的方法中,您只能更改对象的字段。
毫无疑问,Java 是“按值传递”。此外,由于 Java(大部分)是面向对象的,并且对象使用引用,因此很容易混淆并认为它是“通过引用传递”
按值传递意味着您将值传递给方法,如果方法更改传递的值,则真实实体不会改变。另一方面,按引用传递意味着将引用传递给方法,如果方法改变它,传递的对象也会改变。
在 Java 中,通常当我们将对象传递给方法时,我们基本上将对象的引用作为值传递,因为 Java 就是这样工作的;就堆中的对象而言,它适用于引用和地址。
但是要测试它是否真的是按值传递或按引用传递,您可以使用原始类型和引用:
@Test
public void sampleTest(){
int i = 5;
incrementBy100(i);
System.out.println("passed ==> "+ i);
Integer j = new Integer(5);
incrementBy100(j);
System.out.println("passed ==> "+ j);
}
/**
* @param i
*/
private void incrementBy100(int i) {
i += 100;
System.out.println("incremented = "+ i);
}
输出是:
incremented = 105
passed ==> 5
incremented = 105
passed ==> 5
所以在这两种情况下,方法内部发生的任何事情都不会改变真正的对象,因为传递了该对象的值,而不是对对象本身的引用。
但是,当您将自定义对象传递给方法时,该方法并对其进行更改时,它也会更改真实对象,因为即使您传递了对象,您也将它的引用作为值传递给了方法。让我们试试另一个例子:
@Test
public void sampleTest2(){
Person person = new Person(24, "John");
System.out.println(person);
alterPerson(person);
System.out.println(person);
}
/**
* @param person
*/
private void alterPerson(Person person) {
person.setAge(45);
Person altered = person;
altered.setName("Tom");
}
private static class Person{
private int age;
private String name;
public Person(int age, String name) {
this.age=age;
this.name =name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Person [age=");
builder.append(age);
builder.append(", name=");
builder.append(name);
builder.append("]");
return builder.toString();
}
}
在这种情况下,输出为:
Person [age=24, name=John]
Person [age=45, name=Tom]
主要的基石知识必须是引用的,
当一个对象引用被传递给一个方法时,引用本身是通过使用按值调用来传递的。但是,由于被传递的值引用了一个对象,该值的副本仍将引用其相应参数所引用的同一对象。
Java:初学者指南,第六版,Herbert Schildt
我看到所有答案都包含相同的内容:按值传递。然而,最近关于 Valhalla 项目的 Brian Goetz 更新实际上给出了不同的回答:
事实上,Java 对象是按值传递还是按引用传递是一个常见的“陷阱”问题,答案是“都不是”:对象引用是按值传递的。
您可以在此处阅读更多信息:瓦尔哈拉之国。第 2 节:语言模型
编辑: Brian Goetz 是 Java 语言架构师,领导着 Project Valhalla 和 Project Amber 等项目。
2020-12-08 编辑:更新的瓦尔哈拉状态
看看这段代码。此代码不会抛出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 时抛出。
现在,人们喜欢无休止地争论“通过引用传递”是否是描述 Java 等的正确方式。实际上做。重点是:
在我的书中,这被称为通过引用传递。
长话短说:
结束。
(2) 太容易了。现在,如果您想考虑 (1) 的含义,假设您有一个 Apple 类:
class Apple {
private double weight;
public Apple(double weight) {
this.weight = weight;
}
// getters and setters ...
}
然后当您将此类的实例传递给 main 方法时:
class Main {
public static void main(String[] args) {
Apple apple = new Apple(3.14);
transmogrify(apple);
System.out.println(apple.getWeight()+ " the goose drank wine...";
}
private static void transmogrify(Apple apple) {
// does something with apple ...
apple.setWeight(apple.getWeight()+0.55);
}
}
哦..但您可能知道,您对执行以下操作时会发生什么感兴趣:
class Main {
public static void main(String[] args) {
Apple apple = new Apple(3.14);
transmogrify(apple);
System.out.println("Who ate my: "+apple.getWeight()); // will it still be 3.14?
}
private static void transmogrify(Apple apple) {
// assign a new apple to the reference passed...
apple = new Apple(2.71);
}
}
与其他一些语言不同,Java 不允许您在按值传递和按引用传递之间进行选择。
所有参数都按值传递。
一个方法调用可以将两个传递types of values
给一个方法
Objects themselves cannot be passed to methods
. 当方法修改原始类型参数时,对参数的更改不会影响调用方法中的原始参数值。
对于引用类型参数也是如此。如果修改引用类型参数使其引用另一个对象,则只有参数引用新对象——存储在调用者变量中的引用仍然引用原始对象。
参考资料:Java™ 如何编程(早期对象),第十版
通过引用传递:调用者和被调用者使用相同的变量作为参数。
按值传递:调用者和被调用者有两个具有相同值的自变量。
使用原始数据类型的示例:
public class PassByValuePrimitive {
public static void main(String[] args) {
int i=5;
System.out.println(i); //prints 5
change(i);
System.out.println(i); //prints 5
}
private static void change(int i) {
System.out.println(i); //prints 5
i=10;
System.out.println(i); //prints 10
}
}
使用对象的示例:
public class PassByValueObject {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("prem");
list.add("raj");
new PassByValueObject().change(list);
System.out.println(list); // prints [prem, raj, ram]
}
private void change(List list) {
System.out.println(list.get(0)); // prem
list.add("ram");
list=null;
System.out.println(list.add("bheem")); //gets NullPointerException
}
}
检查一种语言是否支持按引用传递的简单测试就是编写一个传统的交换。你能用 Java 写一个传统的 swap(a,b) 方法/函数吗?
传统的交换方法或函数采用两个参数并将它们交换,以便传递给函数的变量在函数外部更改。它的基本结构看起来像
(非Java)基本交换函数结构
swap(Type arg1, Type arg2) {
Type temp = arg1;
arg1 = arg2;
arg2 = temp;
}
如果您可以用您的语言编写这样的方法/函数,以便调用
Type var1 = ...;
Type var2 = ...;
swap(var1,var2);
实际上切换变量 var1 和 var2 的值,该语言支持传递引用。但是Java 不允许这样的事情,因为它只支持传递值而不是指针或引用。
不再重复,但对于那些在阅读了许多答案后可能仍然感到困惑的人来说:
pass by value
在 Java 中不等于在pass by value
C++ 中,虽然听起来像那样,这可能就是为什么会出现混淆分解它:
pass by value
在 C++ 中意味着传递对象的值(如果是对象),字面意思是对象的副本pass by value
在 Java 中意味着传递对象的地址值(如果是对象),而不是像 C++ 这样的对象的真正“值”(副本)pass by value
Java中,对myObj.setName("new")
函数内部的对象(例如)进行操作会影响函数外部的对象;在pass by value
C++ 中,它对外部没有任何影响。pass by reference
在 C++ 中,对函数中的对象进行操作确实会对外部对象产生影响!类似于(只是相似,不一样)pass by value
在 Java 中,不是吗?.. 人们总是重复“ Java 中没有引用传递”,=> BOOM,混乱开始了......所以,朋友们,一切都只是术语定义的差异(跨语言),您只需要知道它是如何工作的,就是这样(尽管有时我承认它的名称有点令人困惑)!
为了尝试对此进行更多补充,我想我会包含有关该主题的 SCJP 学习指南部分。这是为了通过 Sun/Oracle 对 Java 行为的测试而制作的指南,因此它是用于此讨论的一个很好的来源。
将变量传递给方法(目标 7.3)
7.3 确定当对象引用和原始值被传递到对参数执行赋值或其他修改操作的方法时对它们的影响。
可以将方法声明为采用原语和/或对象引用。您需要知道调用者的变量如何(或是否)受到被调用方法的影响。当传递给方法时,对象引用和原始变量之间的区别是巨大而重要的。要理解本节,您需要熟悉本章第一部分中的作业部分。
传递对象引用变量
当您将对象变量传递给方法时,您必须记住您传递的是对象引用,而不是实际的对象本身。请记住,引用变量保存的位代表(对底层 VM)表示访问内存中(堆上)特定对象的方法。更重要的是,您必须记住,您甚至没有传递实际的引用变量,而是传递了引用变量的副本。变量的副本意味着您获得该变量中位的副本,因此当您传递引用变量时,您传递的位副本表示如何到达特定对象。换句话说,调用者和被调用方法现在都将拥有相同的引用副本,因此两者都将引用堆上相同的精确(而不是副本)对象。
对于此示例,我们将使用 java.awt 包中的 Dimension 类:
1. import java.awt.Dimension;
2. class ReferenceTest {
3. public static void main (String [] args) {
4. Dimension d = new Dimension(5,10);
5. ReferenceTest rt = new ReferenceTest();
6. System.out.println("Before modify() d.height = " + d.height);
7. rt.modify(d);
8. System.out.println("After modify() d.height = "
9. }
10.
11.
12.
13. }
14. }
当我们运行这个类时,我们可以看到 modify() 方法确实能够修改在第 4 行创建的原始(也是唯一的)Dimension 对象。
C:\Java 项目\参考>java 参考测试 在 modify() d.height = 10 之前 昏暗 = 11 在 modify() d.height = 11 之后
请注意,当第 4 行的 Dimension 对象被传递给 modify() 方法时,在方法内部发生的对对象的任何更改都将针对传递了引用的对象进行。在前面的示例中,引用变量 d 和 dim 都指向同一个对象。
Java 是否使用按值传递语义?
如果 Java 通过传递引用变量来传递对象,这是否意味着 Java 对对象使用传递引用?不完全是,尽管您会经常听到和读到它确实如此。Java 实际上是在单个 VM 中运行的所有变量的值传递。Pass-by-value 表示按变量值传递。这意味着,通过变量的副本传递!(又是那个词的副本!)
如果您传递原始变量或引用变量,这没有区别,您总是传递变量中位的副本。因此,对于原始变量,您传递的是表示该值的位的副本。例如,如果您传递一个值为 3 的 int 变量,那么您将传递一个表示 3 的位的副本。然后,被调用的方法将获得它自己的该值的副本,以按照自己的喜好进行处理。
如果你传递一个对象引用变量,你传递的是代表对对象引用的位的副本。然后,被调用的方法获得它自己的引用变量副本,以按照自己的喜好进行处理。但是由于两个相同的引用变量引用了完全相同的对象,如果被调用的方法修改了对象(例如通过调用 setter 方法),调用者将看到调用者的原始变量引用的对象也已更改。在下一节中,我们将看看当我们谈论原语时图片是如何变化的。
传值的底线:被调用的方法不能改变调用者的变量,虽然对于对象引用变量,被调用的方法可以改变变量引用的对象。改变变量和改变对象有什么区别?对于对象引用,这意味着被调用的方法不能重新分配调用者的原始引用变量并使其引用不同的对象,或者为空。例如,在以下代码片段中,
void bar() {
Foo f = new Foo();
doStuff(f);
}
void doStuff(Foo g) {
g.setName("Boo");
g = new Foo();
}
重新分配 g 不会重新分配 f!在 bar() 方法的最后,创建了两个 Foo 对象,一个由局部变量 f 引用,一个由局部(参数)变量 g 引用。因为 doStuff() 方法具有引用变量的副本,所以它有一种方法可以获取原始 Foo 对象,例如调用 setName() 方法。但是,doStuff() 方法无法获取 f 引用变量。所以 doStuff() 可以改变 f 引用的对象中的值,但 doStuff() 不能改变 f 的实际内容(位模式)。换句话说,doStuff() 可以改变 f 所指对象的状态,但不能使 f 指代不同的对象!
传递原始变量
让我们看看将原始变量传递给方法时会发生什么:
class ReferenceTest {
public static void main (String [] args) {
int a = 1;
ReferenceTest rt = new ReferenceTest();
System.out.println("Before modify() a = " + a);
rt.modify(a);
System.out.println("After modify() a = " + a);
}
void modify(int number) {
number = number + 1;
System.out.println("number = " + number);
}
}
在这个简单的程序中,变量 a 被传递给名为 modify() 的方法,该方法将变量加 1。结果输出如下所示:
Before modify() a = 1
number = 2
After modify() a = 1
请注意, a 在传递给方法后没有更改。请记住,它是传递给方法的 a 的副本。当一个原始变量被传递给一个方法时,它是按值传递的,这意味着通过变量中位的拷贝传递。
这有点难以理解,但 Java 总是复制值 - 关键是,通常值是引用。因此,您最终会得到相同的对象而不考虑它...
围绕这个问题的许多困惑来自于 Java 试图重新定义“按值传递”和“按引用传递”的含义这一事实。重要的是要了解这些是行业术语,并且在该上下文之外无法正确理解。它们旨在在您编写代码时为您提供帮助,并且很容易理解,所以让我们先来了解一下它们的含义。
可以在此处找到对两者的良好描述。
按值传递函数接收到的值是调用者正在使用的对象的副本。它对函数来说是完全独特的,你对该对象所做的任何事情都只会在函数中看到。
通过引用传递函数接收到的值是对调用者正在使用的对象的引用。函数对 value 引用的对象所做的任何事情都会被调用者看到,并且从那时起它将处理这些更改。
从这些定义中可以清楚地看出,引用按值传递的事实是无关紧要的。如果我们接受这个定义,那么这些术语将变得毫无意义,并且所有语言都只是通过值传递。
无论您如何传递引用,它都只能按值传递。那不是重点。关键是您将对自己的对象的引用传递给函数,而不是它的副本。您可以丢弃收到的推荐信这一事实无关紧要。同样,如果我们接受这个定义,这些术语就变得毫无意义,每个人都在传递价值。
不,C++ 特殊的“按引用传递”语法并不是按引用传递的唯一定义。它纯粹是一种方便的语法,旨在使您在传递指针后无需使用指针语法。它仍在传递指针,编译器只是向您隐藏了这一事实。它还仍然按值传递该指针,编译器只是对您隐藏它。
因此,有了这种理解,我们可以看看 Java 并看到它实际上两者兼有。所有 Java 原始类型始终按值传递,因为您收到调用者对象的副本并且无法修改它们的副本。所有 Java 引用类型总是通过引用传递,因为您收到对调用者对象的引用并且可以直接修改它们的对象。
您不能修改调用者的引用这一事实与按引用传递无关,并且在每种支持按引用传递的语言中都是如此。
有一个蓝色的 120 平方英尺的“小房子”目前停在 1234 Main St,前面有修剪得很好的草坪和花坛。
聘请了一家当地公司的房地产经纪人,并被告知要保留该房屋的清单。
让我们称那个房地产经纪人为“鲍勃”。嗨,鲍勃。
Bob 使用网络摄像头保持他的清单(他称之为tinyHouseAt1234Main
)是最新的,该网络摄像头允许他实时记录实际房屋的任何变化。他还记录了有多少人询问了该列表。BobviewTally
今天对房子的整数是 42。
每当有人想了解 1234 Main St 的蓝色小房子的信息时,他们都会问 Bob。
Bob 查看他的清单tinyHouseAt1234Main
并告诉他们所有相关信息 - 颜色、漂亮的草坪、阁楼床和堆肥厕所等。然后他将他们的询问添加到他的viewTally
. 不过,他没有告诉他们真实的实际地址,因为 Bob 的公司专门研究可以随时移动的小房子。现在的总数是43。
在另一家公司,房地产经纪人可能会明确表示他们的房源“指向”主街 1234 号的房子,并在*
旁边稍稍表示这一点,因为他们主要处理很少搬家的房屋(尽管可能有这样做的理由) . 鲍勃的公司不会费心去做这件事。
现在,鲍勃当然不会亲自去把真正的房子放在卡车上直接向客户展示——那是不切实际的,而且是一种荒谬的资源浪费。传递一份完整的理货单是一回事,但一直在整个房子里走来走去既昂贵又荒谬。
(旁白:鲍勃的公司也不会在每次有人询问时都 3D 打印上市房屋的新的和独特的副本。这就是新贵,同样命名的基于网络的公司及其衍生公司所做的 - 这既昂贵又慢,而且人们经常让这两家公司感到困惑,但无论如何它们都很受欢迎)。
在其他一些更靠近大海的老公司,甚至可能不存在像 Bob 这样的房地产经纪人来管理列表。客户可能会转而咨询 Rolodex “Annie”(&
简称)以获取房屋的直接地址。客户不像 Bob 那样从列表中读取引用的房屋详细信息,而是从 Annie ( &
) 获取房屋地址,然后直接前往 1234 Main St,有时他们不知道他们会在那里找到什么。
有一天,Bob 的公司开始提供一项新的自动化服务,该服务需要客户感兴趣的房屋清单。
好吧,拥有该信息的人是 Bob,因此客户让 Bob 调用服务并向其发送列表的副本。
jobKillingAutomatedListingService(Listing tinyHouseAt1234Main, int viewTally)
鲍勃送...
该服务最终调用此 Listing houseToLookAt
,但实际上它收到的是 Bob 列表的精确副本,其中包含完全相同的 VALUE,指的是位于 Main St 1234 的房子。
这项新服务也有自己的内部统计,显示有多少人查看了该列表。该服务出于专业礼貌而接受 Bob 的计数,但它并不真正关心并用它自己的本地副本完全覆盖它。今天的计数是 1,而 Bob 的仍然是 43。
房地产公司将此称为“按值传递”,因为 Bob 传递了他viewTally
和他的 Listing的当前值tinyHouseAt1234Main
。他实际上并没有经过整个物理房屋,因为那是不切实际的。他也不会像 Annie() 那样传递真实的物理地址&
。
但是他正在传递他对房子的参考价值的副本。在某些方面似乎是一个愚蠢的迂腐差异,但这就是他的公司的运作方式............
新的自动化服务,不像其他一些时尚的金融和科学公司那样完全以功能和数学为导向,可能会产生无法预料的副作用......
一旦给定一个列表对象,它就允许客户使用远程无人机机器人舰队实际重新粉刷主街 1234 号的真实房屋!它允许客户控制机器人推土机来真正挖掘花坛!这太疯狂了!!!
该服务还允许客户完全重定向houseToLookAt
到另一个地址的其他房子,而不涉及 Bob 或他的列表。突然之间,他们可能会转而查看 4321 Elm St.,这与 Bob 的列表没有任何关系(谢天谢地,他们不能再造成任何损害)。
Bob 在他的实时网络摄像头上观看了这一切。辞去了自己唯一工作职责的苦差事,他告诉客户新的丑陋油漆工作和突然缺乏遏制吸引力。毕竟,他的清单仍然是 1234 Main St.。新服务houseToLookAt
无法改变这一点。tinyHouseAt1234Main
Bob一如既往地准确而尽职地报告他的细节,直到他被解雇或房子被 The Nothing 完全摧毁。
实际上,该服务无法使用其houseToLookAt
Bob 原始列表的副本做的唯一事情是将地址从 1234 Main St. 更改为其他地址,或更改为虚无,或更改为诸如鸭嘴兽之类的随机类型的对象。Bob 的清单始终指向 1234 Main St,无论它仍然值多少钱。他像往常一样传递它的当前值。
将列表传递给新的自动化服务的这种奇怪的副作用让那些询问它如何工作的人感到困惑。真的,远程控制改变主街 1234 号房屋状态的机器人的能力与实际前往那里并因为安妮给了你地址而造成严重破坏的能力有什么区别?
如果您通常关心的是列表中房屋的状态被复制和传递,这似乎是一种挑剔的语义论点,对吧?
我的意思是,如果您的业务是实际拿起房屋并将它们实际移动到其他地址(不像移动或微型房屋,这是平台的预期功能),或者您正在访问、重命名和改组整个社区就像某种低级的玩上帝的疯子,那么也许您会更关心传递那些特定的地址引用,而不仅仅是房屋详细信息的最新值的副本...
Java 是按值传递的。
在这个线程上已经有很好的答案。不知何故,对于原始数据类型和对象,我从来没有清楚地通过值/引用传递。因此,我用下面的代码测试了它的满意度和清晰度;可能会帮助寻求类似清晰度的人:
class Test {
public static void main (String[] args) throws java.lang.Exception
{
// Primitive type
System.out.println("Primitve:");
int a = 5;
primitiveFunc(a);
System.out.println("Three: " + a); //5
//Object
System.out.println("Object:");
DummyObject dummyObject = new DummyObject();
System.out.println("One: " + dummyObject.getObj()); //555
objectFunc(dummyObject);
System.out.println("Four: " + dummyObject.getObj()); //666 (555 if line in method uncommented.)
}
private static void primitiveFunc(int b) {
System.out.println("One: " + b); //5
b = 10;
System.out.println("Two:" + b); //10
}
private static void objectFunc(DummyObject b) {
System.out.println("Two: " + b.getObj()); //555
//b = new DummyObject();
b.setObj(666);
System.out.println("Three:" + b.getObj()); //666
}
}
class DummyObject {
private int obj = 555;
public int getObj() { return obj; }
public void setObj(int num) { obj = num; }
}
如果该行b = new DummyObject()
未注释,则此后所做的修改将在一个新对象上进行,一个新的实例化。因此,它不会反映在调用方法的位置。但是,否则,更改会反映为仅对对象的“引用”进行修改,即 - b 指向相同的 dummyObject。
此线程 ( https://stackoverflow.com/a/12429953/4233180 )中的一个答案中的插图可以帮助获得更深入的理解。
Java始终是按值传递的,参数是传递的变量的副本,所有对象都使用引用定义,引用是存储对象在内存中的内存地址的变量。
检查评论以了解执行过程中发生了什么;跟随数字,因为它们显示了执行流程..
class Example
{
public static void test (Cat ref)
{
// 3 - <ref> is a copy of the reference <a>
// both currently reference Grumpy
System.out.println(ref.getName());
// 4 - now <ref> references a new <Cat> object named "Nyan"
ref = new Cat("Nyan");
// 5 - this should print "Nyan"
System.out.println( ref.getName() );
}
public static void main (String [] args)
{
// 1 - a is a <Cat> reference that references a Cat object in memory with name "Grumpy"
Cat a = new Cat("Grumpy");
// 2 - call to function test which takes a <Cat> reference
test (a);
// 6 - function call ends, and <ref> life-time ends
// "Nyan" object has no references and the Garbage
// Collector will remove it from memory when invoked
// 7 - this should print "Grumpy"
System.out.println(a.getName());
}
}
Java通过值传递参数,但是对于对象变量,值本质上是对对象的引用。由于数组是对象,因此以下示例代码显示了差异。
public static void dummyIncrease(int[] x, int y)
{
x[0]++;
y++;
}
public static void main(String[] args)
{
int[] arr = {3, 4, 5};
int b = 1;
dummyIncrease(arr, b);
// arr[0] is 4, but b is still 1
}
main()
arr +---+ +---+---+---+
| # | ----> | 3 | 4 | 5 |
+---+ +---+---+---+
b +---+ ^
| 1 | |
+---+ |
|
dummyIncrease() |
x +---+ |
| # | ------------+
+---+
y +---+
| 1 |
+---+
一切都是按值传递的。基元和对象引用。但是如果对象的接口允许的话,对象是可以更改的。
当您将对象传递给方法时,您传递的是一个引用,并且该对象可以通过方法实现进行修改。
void bithday(Person p) {
p.age++;
}
对象本身的引用,通过值传递:您可以重新分配参数,但更改不会反映回来:
void renameToJon(Person p) {
p = new Person("Jon"); // this will not work
}
jack = new Person("Jack");
renameToJon(jack);
sysout(jack); // jack is unchanged
实际上,“p”是引用(指向对象的指针)并且不能更改。
原始类型按值传递。对象的引用也可以被认为是原始类型。
回顾一下,一切都是按值传递的。
最短的答案:)
在 C# 中,这是通过“out”和“ref”关键字完成的。
通过引用传递:变量以这样的方式传递,即使在方法外部也能反映方法内部的重新分配。
下面是一个按引用传递 (C#) 的示例。这个特性在java中是不存在的。
class Example
{
static void InitArray(out int[] arr)
{
arr = new int[5] { 1, 2, 3, 4, 5 };
}
static void Main()
{
int[] someArray;
InitArray(out someArray);
// This is true !
boolean isTrue = (someArray[0] == 1);
}
}
另请参阅:MSDN 库 (C#):通过 ref 和 out 传递数组
另请参阅:MSDN 库 (C#):按值和按引用传递
简单的程序
import java.io.*;
class Aclass
{
public int a;
}
public class test
{
public static void foo_obj(Aclass obj)
{
obj.a=5;
}
public static void foo_int(int a)
{
a=3;
}
public static void main(String args[])
{
//test passing an object
Aclass ob = new Aclass();
ob.a=0;
foo_obj(ob);
System.out.println(ob.a);//prints 5
//test passing an integer
int i=0;
foo_int(i);
System.out.println(i);//prints 0
}
}
从 C/C++ 程序员的角度来看,java 使用按值传递,因此对于原始数据类型(int、char 等),函数中的更改不会反映在调用函数中。但是,当您传递一个对象并在函数中更改其数据成员或调用可以更改对象状态的成员函数时,调用函数将获得更改。
Java 仅按值传递。没有按引用传递,例如,可以看下面的例子。
package com.asok.cop.example.task;
public class Example {
int data = 50;
void change(int data) {
data = data + 100;// changes will be in the local variable
System.out.println("after add " + data);
}
public static void main(String args[]) {
Example op = new Example();
System.out.println("before change " + op.data);
op.change(500);
System.out.println("after change " + op.data);
}
}
输出:
before change 50
after add 600
after change 50
正如迈克尔在评论中所说:
对象仍然按值传递,即使对它们的操作表现得像按引用传递。考虑
void changePerson(Person person){ person = new Person(); }
调用者对 person 对象的引用将保持不变。对象本身是按值传递的,但它们的成员可能会受到更改的影响。要实现真正的按引用传递,我们必须能够将参数重新分配给新对象,并将更改反映在调用者中。
Java 总是按值传递参数。
Java 中的所有对象引用都是按值传递的。这意味着将值的副本传递给方法。但诀窍在于,传递值的副本也会改变对象的真实值。
请参考以下示例,
public class ObjectReferenceExample {
public static void main(String... doYourBest) {
Student student = new Student();
transformIntoHomer(student);
System.out.println(student.name);
}
static void transformIntoDuleepa(Student student) {
student.name = "Duleepa";
}
}
class Student {
String name;
}
在这种情况下,它将是 Duleepa!
原因是 Java 对象变量只是指向内存堆中真实对象的简单引用。因此,即使Java通过值向方法传递参数,如果变量指向一个对象引用,那么真实的对象也会发生变化。
有一种非常简单的方法可以理解这一点。让我们通过引用来传递 C++。
#include <iostream>
using namespace std;
class Foo {
private:
int x;
public:
Foo(int val) {x = val;}
void foo()
{
cout<<x<<endl;
}
};
void bar(Foo& ref)
{
ref.foo();
ref = *(new Foo(99));
ref.foo();
}
int main()
{
Foo f = Foo(1);
f.foo();
bar(f);
f.foo();
return 0;
}
结果如何?
1 1 99 99
因此,在 bar() 为传入的“引用”分配一个新值之后,它实际上改变了从 main 本身传入的那个,解释了从 main 打印 99 中最后一次 f.foo() 调用。
现在,让我们看看java怎么说。
public class Ref {
private static class Foo {
private int x;
private Foo(int x) {
this.x = x;
}
private void foo() {
System.out.println(x);
}
}
private static void bar(Foo f) {
f.foo();
f = new Foo(99);
f.foo();
}
public static void main(String[] args) {
Foo f = new Foo(1);
System.out.println(f.x);
bar(f);
System.out.println(f.x);
}
}
它说:
1 1 99 1
瞧,传递给 bar 的 main 中 Foo 的引用仍然没有改变!
这个例子清楚地表明,当我们说“按引用传递”时,java 与 C++ 是不一样的。本质上,java 将“引用”作为“值”传递给函数,这意味着 java 是按值传递的。
A:Java确实通过引用来操作对象,所有的对象变量都是引用。但是,Java 不会通过引用传递方法参数。它按值传递它们。
以 badSwap() 方法为例:
public void badSwap(int var1, int var2)
{
int temp = var1;
var1 = var2;
var2 = temp;
}
当 badSwap() 返回时,作为参数传递的变量仍将保持其原始值。如果我们将参数类型从 int 更改为 Object,该方法也会失败,因为 Java 也通过值传递对象引用。现在,这里是棘手的地方:
public 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("X: " + pnt1.x + " Y: " +pnt1.y);
System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
System.out.println(" ");
tricky(pnt1,pnt2);
System.out.println("X: " + pnt1.x + " Y:" + pnt1.y);
System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
}
如果我们执行这个 main() 方法,我们会看到以下输出:
X: 0 Y: 0
X: 0 Y: 0
X: 100 Y: 100
X: 0 Y: 0
该方法成功地改变了 pnt1 的值,即使它是按值传递的;但是,pnt1 和 pnt2 的交换失败了!这是混乱的主要来源。在 main() 方法中,pnt1 和 pnt2 只不过是对象引用。当您将 pnt1 和 pnt2 传递给 tricky() 方法时,Java 按值传递引用,就像任何其他参数一样。这意味着传递给该方法的引用实际上是原始引用的副本。下面的图 1 显示了在 Java 将对象传递给方法后指向同一个对象的两个引用。
Java 通过值而不是对象来复制和传递引用。因此,方法操作将改变对象,因为引用指向原始对象。但是由于引用是副本,交换会失败。方法引用交换,而不是原始引用。不幸的是,在方法调用之后,您只剩下未交换的原始引用。为了在方法调用之外成功进行交换,我们需要交换原始引用,而不是副本。
@Scott Stanchfield 先生写了一个很好的答案。这是您可以准确验证他的意思的课程:
public class Dog {
String dog ;
static int x_static;
int y_not_static;
public String getName()
{
return this.dog;
}
public Dog(String dog)
{
this.dog = dog;
}
public void setName(String name)
{
this.dog = name;
}
public static void foo(Dog someDog)
{
x_static = 1;
// y_not_static = 2; // not possible !!
someDog.setName("Max"); // AAA
someDog = new Dog("Fifi"); // BBB
someDog.setName("Rowlf"); // CCC
}
public static void main(String args[])
{
Dog myDog = new Dog("Rover");
foo(myDog);
System.out.println(myDog.getName());
}
}
因此,我们从 main() 传递了一个名为 的狗Rover
,然后我们为传递的指针分配了一个新地址,但最后,狗的名字不是Rover
,也不是Fifi
,当然也不是Rowlf
,而是Max
。
分两步理解:
您不能更改对对象本身的引用,但可以将此传递的参数用作对对象的引用。
如果你想改变引用后面的值,你只会在堆栈上声明一个同名的新变量“d”。查看带有符号的引用,@
您会发现引用已更改。
public static void foo(Dog d) {
d.Name = "belly";
System.out.println(d); //Reference: Dog@1540e19d
d = new Dog("wuffwuff");
System.out.println(d); //Dog@677327b6
}
public static void main(String[] args) throws Exception{
Dog lisa = new Dog("Lisa");
foo(lisa);
System.out.println(lisa.Name); //belly
}
我试图简化上面的例子,只保留问题的本质。让我把它作为一个易于记忆和正确应用的故事来介绍。故事是这样的:你有一只宠物狗吉米,它的尾巴有 12 英寸长。当你出国旅行时,你把它留给兽医几个星期。
兽医不喜欢吉米的长尾巴,所以他想把它剪掉一半。但作为一名优秀的兽医,他知道他没有权利残害别人的狗。所以他首先制作了狗的克隆体(使用新的关键词)并切割了克隆体的尾巴。当狗终于回到你身边时,它有原来的 12 英寸尾巴。美好的结局 !
下次你旅行时,你会在不知不觉中把狗带到一个邪恶的兽医那里。他也讨厌长尾巴,所以他把它剪短到可怜的 2 英寸。但他对你亲爱的吉米这样做,而不是它的克隆。当你回来时,你震惊地看到吉米可怜地摇着一根 2 英寸的存根。
故事的寓意:当您将宠物传给他人时,您就是将宠物的完整且不受约束的监护权交给了兽医。他可以随意对它进行任何破坏。按值传递、按引用传递、按指针传递都只是技术争论。除非兽医先克隆它,否则他最终会肢解原来的狗。
public class Doggie {
public static void main(String...args) {
System.out.println("At the owner's home:");
Dog d = new Dog(12);
d.wag();
goodVet(d);
System.out.println("With the owner again:)");
d.wag();
badVet(d);
System.out.println("With the owner again(:");
d.wag();
}
public static void goodVet (Dog dog) {
System.out.println("At the good vet:");
dog.wag();
dog = new Dog(12); // create a clone
dog.cutTail(6); // cut the clone's tail
dog.wag();
}
public static void badVet (Dog dog) {
System.out.println("At the bad vet:");
dog.wag();
dog.cutTail(2); // cut the original dog's tail
dog.wag();
}
}
class Dog {
int tailLength;
public Dog(int originalLength) {
this.tailLength = originalLength;
}
public void cutTail (int newLength) {
this.tailLength = newLength;
}
public void wag() {
System.out.println("Wagging my " +tailLength +" inch tail");
}
}
Output:
At the owner's home:
Wagging my 12 inch tail
At the good vet:
Wagging my 12 inch tail
Wagging my 6 inch tail
With the owner again:)
Wagging my 12 inch tail
At the bad vet:
Wagging my 12 inch tail
Wagging my 2 inch tail
With the owner again(:
Wagging my 2 inch tail
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
Let's understand step by step:
测试 t = new Test(); 众所周知,它将在堆中创建一个对象并将引用值返回给 t。例如,假设 t 的值为 0x100234(我们不知道实际的 JVM 内部值,这只是一个示例)。
第一个插图
new PassByValue().changeValue(t);
将引用 t 传递给函数时,它不会直接传递对象 test 的实际引用值,而是会创建 t 的副本,然后将其传递给函数。由于它是按值传递的,因此它传递变量的副本而不是它的实际引用。由于我们说 t 的值为 0x100234,因此 t 和 f 将具有相同的值,因此它们将指向同一个对象。
第二个插图
如果您使用引用 f 更改函数中的任何内容,它将修改对象的现有内容。这就是我们得到输出变化值的原因,它在函数中更新。
为了更清楚地理解这一点,请考虑以下示例:
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,如下所示:
第三个插图
希望这会有所帮助。
Java中有一个解决方法供参考。让我通过这个例子来解释:
public class Yo {
public static void foo(int x){
System.out.println(x); //out 2
x = x+2;
System.out.println(x); // out 4
}
public static void foo(int[] x){
System.out.println(x[0]); //1
x[0] = x[0]+2;
System.out.println(x[0]); //3
}
public static void main(String[] args) {
int t = 2;
foo(t);
System.out.println(t); // out 2 (t did not change in foo)
int[] tab = new int[]{1};
foo(tab);
System.out.println(tab[0]); // out 3 (tab[0] did change in foo)
}}
我希望这有帮助!
Java确实通过引用来操作对象,并且所有对象变量都是引用。但是,Java 不会通过引用传递方法参数。它按值传递它们。
以 badSwap() 方法为例:
public void badSwap(int var1, int
var2{ int temp = var1; var1 = var2; var2 =
temp; }
当 badSwap() 返回时,作为参数传递的变量仍将保持其原始值。如果我们将参数类型从 int 更改为 Object,该方法也会失败,因为 Java 也通过值传递对象引用。现在,这里是棘手的地方:
public 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("X:
" + pnt1.x + " Y: " +pnt1.y);
System.out.println("X: " + pnt2.x + " Y:
" +pnt2.y); System.out.println(" ");
tricky(pnt1,pnt2);
System.out.println("X: " + pnt1.x + " Y:" + pnt1.y);
System.out.println("X: " + pnt2.x + " Y: " +pnt2.y); }
如果我们执行这个 main() 方法,我们会看到以下输出:
X:0 Y:0 X:0 Y:0 X:100 Y:100 X:0 Y:0
该方法成功地改变了 pnt1 的值,即使它是按值传递的;但是,pnt1 和 pnt2 的交换失败了!这是混乱的主要来源。在 main() 方法中,pnt1 和 pnt2 只不过是对象引用。当您将pnt1 和pnt2 传递给tricky() 方法时,Java 按值传递引用,就像任何其他参数一样。这意味着传递给该方法的引用实际上是原始引用的副本。下面的图 1 显示了在 Java 将对象传递给方法后指向同一个对象的两个引用。
Java 通过值而不是对象来复制和传递引用。因此,方法操作将改变对象,因为引用指向原始对象。但由于引用是副本,交换将失败。如图 2 所示,该方法引用交换,而不是原始引用。不幸的是,在方法调用之后,您只剩下未交换的原始引用。为了在方法调用之外成功进行交换,我们需要交换原始引用,而不是副本。
Java 通过值传递一切!
//通过传入名称和年龄创建一个对象:
PersonClass variable1 = new PersonClass("Mary", 32);
PersonClass variable2;
//variable2 和 variable1 现在都引用同一个对象
variable2 = variable1;
PersonClass variable3 = new PersonClass("Andre", 45);
// variable1 现在指向 variable3
variable1 = variable3;
//这是什么输出?
System.out.println(variable2);
System.out.println(variable1);
Mary 32
Andre 45
如果你能理解这个例子,我们就完成了。否则,请访问此网页了解详细说明:
在我看来,“按价值传递”是一种单独描述两个相似但不同的事件的可怕方式。我想他们应该先问我。
使用原语,我们将原语的实际值传递给方法(或构造函数),可以是整数“5”、字符“c”,或者你有什么。然后,该实际值成为它自己的本地原语。但是对于对象,我们所做的只是给同一个对象一个额外的引用(本地引用),所以我们现在有两个指向同一个对象的引用。
我希望这个简单的解释会有所帮助。
传值的底线:被调用的方法不能改变调用者的变量,虽然对于对象引用变量,被调用的方法可以改变变量引用的对象。改变变量和改变对象有什么区别?对于对象引用,这意味着被调用的方法不能重新分配调用者的原始引用变量并使其引用不同的对象,或者为空。
我从一本关于 Java 认证的书中获取了这段代码和解释,并做了一些小的改动。
我认为这很好地说明了对象的值传递。在下面的代码中,重新分配 g 不会重新分配 f!在 bar() 方法的最后,创建了两个 Foo 对象,一个由局部变量 f 引用,一个由局部(参数)变量 g 引用。
因为 doStuff() 方法具有引用变量的副本,所以它有一种方法可以获取原始 Foo 对象,例如调用 setName() 方法。但是,doStuff() 方法无法获取 f 引用变量。所以 doStuff() 可以改变 f 引用的对象中的值,但 doStuff() 不能改变 f 的实际内容(位模式)。换句话说,doStuff() 可以改变 f 所指对象的状态,但不能使 f 指代不同的对象!
package test.abc;
public class TestObject {
/**
* @param args
*/
public static void main(String[] args) {
bar();
}
static void bar() {
Foo f = new Foo();
System.out.println("Object reference for f: " + f);
f.setName("James");
doStuff(f);
System.out.println(f.getName());
//Can change the state of an object variable in f, but can't change the object reference for f.
//You still have 2 foo objects.
System.out.println("Object reference for f: " + f);
}
static void doStuff(Foo g) {
g.setName("Boo");
g = new Foo();
System.out.println("Object reference for g: " + g);
}
}
package test.abc;
public class Foo {
public String name = "";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
请注意,以下控制台输出中的对象引用没有更改:
控制台输出:
f 的对象引用:test.abc.Foo@62f72617
g 的对象引用:test.abc.Foo@4fe5e2c3
f 的 Boo 对象参考:test.abc.Foo@62f72617
Java 按值传递对对象的引用。
因此,如果对引用参数指向的对象进行任何修改,它将反映在原始对象上。
但是如果引用参数指向另一个对象,原始引用仍将指向原始对象。
首先让我们了解 Java 中的内存分配:堆栈和堆是 JVM 为不同目的分配的内存的一部分。堆栈内存是预先分配给线程的,在创建时,一个线程不能访问其他线程的堆栈。但是堆对程序中的所有线程都可用。
对于一个线程,Stack 存储所有本地数据、程序元数据、原始类型数据和对象引用。而且,堆负责存储实际对象。
Book book = new Book("Effective Java");
在上面的示例中,引用变量是存储在堆栈中的“book”。new operator -> new Book("Effective Java") 创建的实例存储在 Heap 中。ref 变量“book”具有在堆中分配的对象的地址。假设地址是 1001。
考虑传递原始数据类型,即 int、float、double 等。
public class PrimitiveTypeExample {
public static void main(string[] args) {
int num = 10;
System.out.println("Value before calling method: " + num);
printNum(num);
System.out.println("Value after calling method: " + num);
}
public static void printNum(int num){
num = num + 10;
System.out.println("Value inside printNum method: " + num);
}
}
输出为:调用方法前的值:10 printNum 方法内的值:20 调用方法后的值:10
整数=10;-> 这为正在运行的线程的堆栈中的“int”分配内存,因为它是原始类型。现在,当调用 printNum(..) 时,会在同一个线程中创建一个私有堆栈。当“num”被传递给该方法时,“num”的副本会在方法堆栈帧中创建。数=数+10;-> 这增加了 10 并修改了方法堆栈帧中的 int 变量。因此,方法堆栈框架外的原始 num 保持不变。
考虑将自定义类的对象作为参数传递的示例。
在上面的例子中,引用变量“book”驻留在执行程序的线程堆栈中,当程序执行 new Book() 时,Book 类的对象是在堆空间中创建的。堆中的这个内存位置由“书”引用。当“book”作为方法参数传递时,“book”的副本会在同一线程堆栈内的方法的私有堆栈框架中创建。因此,复制的引用变量指向堆中“Book”类的同一个对象。
方法堆栈框架中的引用变量为同一对象设置了一个新值。因此,它会在原始 ref 变量“book”获取其值时反映出来。请注意,在传递引用变量的情况下,如果在被调用的方法中再次对其进行初始化,则它会指向新的内存位置,并且任何操作都不会影响堆中的先前对象。
因此,当任何东西作为方法参数传递时,它始终是 Stack 实体——原始变量或引用变量。我们从不传递存储在堆中的东西。因此,在 Java 中,我们总是在堆栈中传递值,而且是按值传递。
为了简单和冗长,
它pass reference by value
:
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
}
It seems everything is call by value in java as i have tried to understand by the following program
Class-S
class S{
String name="alam";
public void setName(String n){
this.name=n;
}}
Class-Sample
public class Sample{
public static void main(String args[]){
S s=new S();
S t=new S();
System.out.println(s.name);
System.out.println(t.name);
t.setName("taleev");
System.out.println(t.name);
System.out.println(s.name);
s.setName("Harry");
System.out.println(t.name);
System.out.println(s.name);
}}
Output
alam
alam
taleev
alam
taleev
harry
As we have define class S with instance variable name with value taleev so for all the objects that we initialize from it will have the name variable with value of taleev but if we change the name's value of any objects then it is changing the name of only that copy of the class(Object) not for every class so after that also when we do System.out.println(s.name) it is printing taleev only we can not change the name's value that we have defined originally, and the value that we are changing is the object's value not the instance variable value so once we have define instance variable we are unable to change it
So i think that is how it shows that java deals with values only not with the references
The memory allocation for the primitive variables can be understood by this
这里有一个更精确的定义:
因此,在 C/C++ 中,您可以创建一个函数来交换使用引用传递的两个值:
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
你可以看到它对 a 和 b 有一个唯一的引用,所以我们没有副本,tmp 只是保存唯一的引用。
java中相同的函数没有副作用,参数传递就像上面的代码没有引用一样。
尽管 java 使用指针/引用,但参数不是唯一的指针,在每个属性中,它们被复制而不是像 C/C++ 一样分配
我会用另一种方式说:
在 java 中传递引用(但不是对象),并且这些引用是按值传递的(引用本身被复制,因此您有 2 个引用,并且您无法控制方法中的第一个引用)。
只是说:按值传递对于初学者来说可能不够清楚。例如,在 Python 中,情况相同,但有文章描述他们称它为pass-by-reference
,只使用了原因引用。
Java按值传递参数,Java中没有传递引用的选项。
但是在编译器绑定级别层,它使用内部不暴露给用户的引用。
这是必不可少的,因为它可以节省大量内存并提高速度。
public class Test {
static class Dog {
String name;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Dog other = (Dog) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public String getName() {
return name;
}
public void setName(String nb) {
this.name = nb;
}
Dog(String sd) {
this.name = sd;
}
}
/**
*
* @param args
*/
public static void main(String[] args) {
Dog aDog = new Dog("Max");
// we pass the object to foo
foo(aDog);
Dog oldDog = aDog;
System.out.println(" 1: " + aDog.getName().equals("Max")); // false
System.out.println(" 2 " + aDog.getName().equals("huahua")); // false
System.out.println(" 3 " + aDog.getName().equals("moron")); // true
System.out.println(" 4 " + " " + (aDog == oldDog)); // true
// part2
Dog aDog1 = new Dog("Max");
foo(aDog1, 5);
Dog oldDog1 = aDog;
System.out.println(" 5 : " + aDog1.getName().equals("huahua")); // true
System.out.println(" part2 : " + (aDog1 == oldDog1)); // false
Dog oldDog2 = foo(aDog1, 5, 6);
System.out.println(" 6 " + (aDog1 == oldDog2)); // true
System.out.println(" 7 " + (aDog1 == oldDog)); // false
System.out.println(" 8 " + (aDog == oldDog2)); // false
}
/**
*
* @param d
*/
public static void foo(Dog d) {
System.out.println(d.getName().equals("Max")); // true
d.setName("moron");
d = new Dog("huahua");
System.out.println(" -:- " + d.getName().equals("huahua")); // true
}
/**
*
* @param d
* @param a
*/
public static void foo(Dog d, int a) {
d.getName().equals("Max"); // true
d.setName("huahua");
}
/**
*
* @param d
* @param a
* @param b
* @return
*/
public static Dog foo(Dog d, int a, int b) {
d.getName().equals("Max"); // true
d.setName("huahua");
return d;
}
}
示例代码演示了更改对象对不同功能的影响。
我认为这个简单的解释可能会帮助您理解,因为当我在努力解决这个问题时,我想理解同样的事情。
当您将原始数据传递给函数调用时,它的内容被复制到函数的参数中,而当您传递对象时,它的引用被复制到函数的参数中。说到对象,您无法更改复制的引用-参数变量在调用函数中引用。
考虑这个简单的例子,String 是 java 中的一个对象,当您更改字符串的内容时,引用变量现在将指向一些新的引用,因为 String 对象在 java 中是不可变的。
String name="Mehrose"; // name referencing to 100
ChangeContenet(String name){
name="Michael"; // refernce has changed to 1001
}
System.out.print(name); //displays Mehrose
相当简单,因为正如我提到的,您不允许在调用函数中更改复制的引用。但是当你传递一个字符串/对象数组时,问题出在数组上。让我们看看。
String names[]={"Mehrose","Michael"};
changeContent(String[] names){
names[0]="Rose";
names[1]="Janet"
}
System.out.println(Arrays.toString(names)); //displays [Rose,Janet]
正如我们所说,我们不能在函数调用中更改复制的引用,我们也看到了单个 String 对象的情况。原因是 names[] 变量引用了 200,而 names[0] 引用了 205,依此类推。您会看到我们没有更改 names[] 引用,它在函数调用之后仍然指向旧的相同引用,但现在 names[0] 和 names[1] 引用已更改。我们仍然坚持我们的定义,即我们不能更改引用变量的引用,所以我们没有。
当您将 Student 对象传递给方法并且您仍然可以更改 Student 名称或其他属性时,也会发生同样的事情,关键是我们没有更改实际的 Student 对象,而是更改了它的内容
你不能这样做
Student student1= new Student("Mehrose");
changeContent(Student Obj){
obj= new Student("Michael") //invalid
obj.setName("Michael") //valid
}
Java 编程语言仅按值传递参数,也就是说,您不能从被调用方法内部更改调用方法中的参数值。
但是,当对象实例作为参数传递给方法时,参数的值不是对象本身,而是对该对象的引用。您可以在被调用方法中更改对象的内容,但不能更改对象引用。
对很多人来说,这看起来像是传递引用,在行为上,它与传递引用有很多共同之处。然而,这有两个原因是不准确的。
首先,改变传递给方法的东西的能力只适用于对象,而不适用于原始值。
其次,与对象类型变量关联的实际值是对对象的引用,而不是对象本身。这是其他方面的一个重要区别,如果清楚地理解,它完全支持 Java 编程语言按值传递参数的观点。
The following code example illustrates this point:
1 public class PassTest {
2
3 // Methods to change the current values
4 public static void changeInt(int value) {
5 value = 55;
6 }
7 public static void changeObjectRef(MyDate ref) {
8 ref = new MyDate(1, 1, 2000);
9 }
10 public static void changeObjectAttr(MyDate ref) {
11 ref.setDay(4);
12 }
13
14 public static void main(String args[]) {
15 MyDate date;
16 int val;
17
18 // Assign the int
19 val = 11;
20 // Try to change it
21 changeInt(val);
22 // What is the current value?
23 System.out.println("Int value is: " + val);
24
25 // Assign the date
26 date = new MyDate(22, 7, 1964);
27 // Try to change it
28 changeObjectRef(date);
29 // What is the current value?
30 System.out.println("MyDate: " + date);
31
32 // Now change the day attribute
33 // through the object reference
34 changeObjectAttr(date);
35 // What is the current value?
36 System.out.println("MyDate: " + date);
37 }
38 }
This code outputs the following:
java PassTest
Int value is: 11
MyDate: 22-7-1964
MyDate: 4-7-1964
The MyDate object is not changed by the changeObjectRef method;
however, the changeObjectAttr method changes the day attribute of the
MyDate object.
猜猜,基于不准确的语言,通用经典是错误的
编程语言的作者无权重命名已建立的编程概念。
原始 Java 类型byte, char, short, int, long float, double
肯定是按值传递的。
所有其他类型是Objects
: 对象成员和参数在技术上是引用。
所以这些“引用”是“按值”传递的,但堆栈上不会发生对象构造。对象成员(或数组中的元素)的任何更改都适用于相同的原始对象;这样的引用恰好符合在任何 C 方言中传递给某个函数的实例指针的逻辑,我们曾经在其中调用 this通过引用传递对象
特别是我们确实有这个东西java.lang.NullPointerException,这在纯粹的按值概念中没有意义
Java 使用按值传递,但无论您使用原始类型还是引用类型,效果都不同。
当您将原始类型作为参数传递给方法时,它会获取原始类型的副本,并且方法块内的任何更改都不会更改原始变量。
当您将引用类型作为参数传递给方法时,它仍然会获得副本,但它是对对象的引用的副本(换句话说,您将获得对象所在堆中内存地址的副本),因此方法的块内对象的任何更改都会影响块外的原始对象。
只有两个版本:
Java 将原始值作为值传递,将对象作为地址传递。那些说“地址也是价值”的人并没有区分两者。那些关注交换函数效果的人关注的是传递完成后会发生什么。
在 C++ 中,您可以执行以下操作:
Point p = Point(4,5);
这会在堆栈上保留 8 个字节并将 (4,5) 存储在其中。
Point *x = &p;
这会在堆栈上保留 4 个字节并在其中存储 0xF43A。
Point &y = p;
这会在堆栈上保留 4 个字节并在其中存储 0xF43A。
我想每个人都会同意,如果 f 的定义是 f(Point p),那么对 f(p) 的调用就是按值传递。在这种情况下,额外的 8 个字节被保留,并且 (4,5) 被复制到其中。当 f 改变 p 时,保证原件在 f 返回时不变。
我想每个人都会同意,如果 f 的定义是 f(Point &p),那么对 f(p) 的调用就是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 改变 p 时,原件保证在 f 返回时改变。
如果 f 的定义是 f(Point *p),则对 f(&p) 的调用也是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 更改 *p 时,保证在 f 返回时更改原始文件。
如果 f 的定义是 f(Point *p),则对 f(x) 的调用也是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 更改 *p 时,保证在 f 返回时更改原始文件。
如果 f 的定义是 f(Point &p),则对 f(y) 的调用也是传递引用。在这种情况下,额外的 4 个字节被保留,0xF43A 被复制到其中。当 f 改变 p 时,原件保证在 f 返回时改变。
当然,通过后发生的事情会有所不同,但这只是一种语言结构。在指针的情况下,您必须使用 -> 来访问成员,而在引用的情况下,您必须使用 .. 如果要交换原始值,则可以执行 tmp=a; a=b; b=tmp; 在引用和 tmp=*a 的情况下;*b=tmp; *a=tmp 用于指针。而在 Java 中你会这样做: tmp.set(a); a.set(b); b.set(tmp)。专注于赋值语句是愚蠢的。如果你写一点代码,你可以在 Java 中做同样的事情。
所以Java通过值传递原始值,通过引用传递对象。Java 复制值来实现这一点,但 C++ 也是如此。
为了完整性:
Point p = new Point(4,5);
这在堆栈上保留 4 个字节并将 0xF43A 存储在其中,并在堆上保留 8 个字节并将 (4,5) 存储在其中。
如果你想像这样交换内存位置
void swap(int& a, int& b) {
int *tmp = &a;
&a = &b;
&b = tmp;
}
然后你会发现你遇到了硬件的限制。
如果你想让它放在一个句子中以便于理解和记住,最简单的答案:
Java 总是使用新的引用传递值
(所以你可以修改原始对象但不能访问原始引用)
“我是一名初级 Java 开发人员,我想知道 Java 是使用 Call-by-Value 还是 Call-by-Reference?”
对此没有普遍的答案,也不可能因为这是错误的二分法。我们有 2 个不同的术语(按值调用/按引用调用),但在将数据传递给方法时至少有 3(!)种不同的方式来处理所述数据:
int
Java 或 C++ 或 C#。)在 OO 世界中,#1 是Call-by-Value和 #2 是Call-by-Reference是没有争议的。但是,由于三个选项只有两个术语,因此由于选项 #3,这两个术语之间没有明确的界限。
“这对我意味着什么?”
这意味着在 Java 领域内,您可以合理地假设 #3 被假设为 Call-by-Value(或更口头的体操术语,Call-References-by-Value)。
但是,在更广泛的 OO 世界中,这意味着您必须要求明确区分 Call-by-Value 和 Call-by-Reference,特别是对方如何对 #3 进行分类。
“但我读到 JLS 将其定义为 Call-by-Value!”
这就是为什么您在与 Java 开发人员打交道时最初的假设应该是上述的原因。但是,JLS 在 Java 之外的权限要小得多。
“为什么 Java 开发人员要坚持使用自己的术语?”
我不能推测,但我认为指出 #2 存在一些潜在问题(显然是 Call-by-Reference)是公平的,正如所暗示的那样,导致 Call-by-Reference 没有最好的名声四起。
“这个烂摊子有办法解决吗?”
3 个选项,只有 2 个无处不在的名字。明显的退出是第三个术语,它获得了与 Call-by-Value 和 Call-by-Reference 相同的广泛使用(并且它比 Call-References-by-Value 更容易混淆,也许是Call-by-Sharing)。在那之前,您需要假设或询问,如上所述。
归根结底,我们如何称呼它并不重要,只要我们彼此了解并且没有混淆。
这里的每一个答案都是通过引用其他语言来获取传递指针,并展示在 Java 中这是不可能做到的。无论出于何种原因,没有人试图展示如何从其他语言实现按值传递对象。
这段代码展示了如何完成这样的事情:
public class Test
{
private static void needValue(SomeObject so) throws CloneNotSupportedException
{
SomeObject internalObject = so.clone();
so=null;
// now we can edit internalObject safely.
internalObject.set(999);
}
public static void main(String[] args)
{
SomeObject o = new SomeObject(5);
System.out.println(o);
try
{
needValue(o);
}
catch(CloneNotSupportedException e)
{
System.out.println("Apparently we cannot clone this");
}
System.out.println(o);
}
}
public class SomeObject implements Cloneable
{
private int val;
public SomeObject(int val)
{
this.val = val;
}
public void set(int val)
{
this.val = val;
}
public SomeObject clone()
{
return new SomeObject(val);
}
public String toString()
{
return Integer.toString(val);
}
}
这里我们有一个函数needValue
,它的作用是立即创建对象的克隆,这需要在对象本身的类中实现,并且需要将类标记为Cloneable
. so
在那之后设置不是必需的null
,但我在这里这样做是为了表明我们不会在那之后使用该引用。
很可能 Java 没有按引用传递语义,但称该语言为“按值传递”是一厢情愿的想法。
在进行了详尽的讨论之后,我认为是时候将所有严肃的结果放在一个片段中了。
/**
*
* @author Sam Ginrich
*
* All Rights Reserved!
*
*/
public class JavaIsPassByValue
{
static class SomeClass
{
int someValue;
public SomeClass(int someValue)
{
this.someValue = someValue;
}
}
static void passReferenceByValue(SomeClass someObject)
{
if (someObject == null)
{
throw new NullPointerException(
"This Object Reference was passed by Value,\r\n that's why you don't get a value from it.");
}
someObject.someValue = 49;
}
public static void main(String[] args)
{
SomeClass someObject = new SomeClass(27);
System.out.println("Here is the original value: " + someObject.someValue);
passReferenceByValue(someObject);
System.out.println(
"\nAs ´Java is pass by value´,\r\n everything without exception is passed by value\r\n and so an object's attribute cannot change: "
+ someObject.someValue);
System.out.println();
passReferenceByValue(null);
}
}
从输出中可以很容易地看出,在 Java 中,一切都是按值传递的,如此简单!
Here is the original value: 27
As ´Java is pass by value´,
everything without exception is passed by value
and so an object´s attribute cannot change: 49
'Exception in thread "main" java.lang.NullPointerException: This Object Reference was passed by value,
that´s why you don´t get a value from it.
at JavaIsPassByValue.passReferenceByValue(JavaIsPassByValue.java:26)
at JavaIsPassByValue.main(JavaIsPassByValue.java:43)