提问人: 提问时间:9/3/2008 最后编辑:26 revs, 22 users 12%user4315 更新时间:11/24/2023 访问量:2663520
Java 是“按引用传递”还是“按值传递”?
Is Java "pass-by-reference" or "pass-by-value"?
答:
Java 始终是按值传递的,没有例外。
那么,怎么会有人对此感到困惑,并认为 Java 是通过引用传递的,或者认为他们有一个 Java 作为引用传递的例子呢?关键的一点是,在任何情况下,Java 都不会提供对对象本身值的直接访问。对对象的唯一访问是通过对该对象的引用。由于 Java 对象总是通过引用而不是直接访问,因此通常将字段、变量和方法参数视为对象,而它们只是对对象的引用。混淆源于命名法的这种(严格来说,不正确的)变化。
因此,在调用方法时
- 对于基元参数(、 等),按值传递是基元的实际值(例如,3)。
int
long
- 对于对象,传递依据值是对对象的引用的值。
因此,如果您有并且两个 Foos 复制了指向相同对象的引用。doSomething(foo)
public void doSomething(Foo foo) { .. }
当然,按值传递对对象的引用看起来非常像(在实践中与通过引用传递对象没有区别)。
评论
f(x)
x
int test(int *a) { int b = *(a); return b;)
Java 按值传递引用。
因此,您无法更改传入的引用。
评论
基本上,重新分配 Object 参数不会影响参数,例如,
private static 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 按值传递对对象的引用。
评论
术语“按值传递”和“按引用传递”在计算机科学中具有特殊、精确定义的含义。这些含义与许多人第一次听到这些术语时的直觉不同。这次讨论中的许多混乱似乎都来自这个事实。
术语“按值传递”和“按引用传递”谈论的是变量。按值传递意味着变量的值被传递给函数/方法。按引用传递意味着将对该变量的引用传递给函数。后者为函数提供了一种更改变量内容的方法。
根据这些定义,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
}
在上面的示例中,仍将返回 .其中的值不会在函数中更改,因为对象引用是按值传递的。如果它是通过引用传递的,则 in 将在调用 .aDog.getName()
"Max"
aDog
main
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");
}
在上面的示例中,是调用 to 后狗的名称,因为对象的名称是在 中设置的。执行的任何操作都是这样的,出于所有实际目的,它们都执行在 上,但不可能更改变量本身的值。Fifi
foo(aDog)
foo(...)
foo
d
aDog
aDog
有关按引用传递和按值传递的详细信息,请参阅以下答案:https://stackoverflow.com/a/430958/6005228。这更彻底地解释了两者背后的语义和历史,也解释了为什么 Java 和许多其他现代语言在某些情况下似乎同时做到了这两点。
评论
foo(aDog);
aDog
foo
d
d = new Dog("Fifi");
d
&d
长话短说,Java 对象具有一些非常奇特的属性。
通常,Java 具有直接按值传递的原始类型(、、、等)。然后 Java 有对象(派生自 的所有对象)。对象实际上总是通过引用(引用是无法触摸的指针)进行处理。这意味着实际上,对象是通过引用传递的,因为引用通常不感兴趣。但是,这确实意味着您不能更改指向哪个对象,因为引用本身是按值传递的。int
bool
char
double
java.lang.Object
这听起来是不是很奇怪,令人困惑?让我们考虑一下 C 如何实现按引用传递和按值传递。在 C 中,默认约定是按值传递。 按值传递 int。 是一个函数,它不需要 ,而是指向 int: 的指针。人们会将其与运算符一起使用来传递变量地址。void foo(int x)
void foo(int *x)
int a
foo(&a)
&
把它带到C++,我们有参考。引用基本上是(在此上下文中)隐藏等式的指针部分的语法糖:由 调用,其中编译器本身知道它是一个引用,并且应该传递非引用的地址。在 Java 中,所有引用对象的变量实际上都是引用类型,实际上在大多数意图和目的中强制引用调用,而没有 C++ 等提供的细粒度控制(和复杂性)。void foo(int &x)
foo(a)
a
评论
区别,或者说,我曾经的记忆中,我和原来的发帖人有同样的印象,那就是: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 的引用只会在局部范围内更改,而不会在此方法之外更改。
评论
我总是认为它是“通过副本”。它是值的副本,无论是基元值还是引用值。如果它是基元,则它是作为值的位的副本,如果它是对象,则它是引用的副本。
public class PassByCopy{
public static void changeName(Dog d){
d.name = "Fido";
}
public static void main(String[] args){
Dog d = new Dog("Maxx");
System.out.println("name= "+ d.name);
changeName(d);
System.out.println("name= "+ d.name);
}
}
class Dog{
public String name;
public Dog(String s){
this.name = s;
}
}
java PassByCopy 的输出:
name= Maxx
name= Fido
基元包装类和字符串是不可变的,因此使用这些类型的任何示例都不会与其他类型/对象相同。
我在这里为任何编程语言创建了一个专门针对此类问题的线程。
还提到了 Java。以下是简短的摘要:
- Java 按值传递参数
- “按值”是 Java 中将参数传递给方法的唯一方法
- 使用作为参数给出的对象中的方法将改变 对象,因为引用指向 原始对象。(如果 方法本身会更改某些值)
我刚刚注意到你引用了我的文章。
Java 规范说 Java 中的所有内容都是按值传递的。Java 中没有“按引用传递”这样的东西。
理解这一点的关键是,像
Dog myDog;
不是狗;它实际上是指向狗的指针。在 Java 中使用术语“引用”非常具有误导性,这也是导致这里大部分混淆的原因。他们所谓的“参考”的行为/感觉更像是我们在大多数其他语言中所说的“指针”。
这意味着,当你有
Dog myDog = new Dog("Rover");
foo(myDog);
实质上是将创建的对象的地址传递给方法。Dog
foo
(我之所以这么说,主要是因为 Java 指针/引用不是直接地址,但这样想是最容易的。
假设对象驻留在内存地址 42 处。这意味着我们将 42 传递给该方法。Dog
如果方法定义为
public void foo(Dog someDog) {
someDog.setName("Max"); // AAA
someDog = new Dog("Fifi"); // BBB
someDog.setName("Rowlf"); // CCC
}
让我们看看发生了什么。
- 该参数设置为值 42
someDog
- 在“AAA”行
someDog
跟在它指向的(地址 42 处的对象)之后Dog
Dog
- 那个(地址 42 的那个)被要求将他的名字改为 Max
Dog
- 在“BBB”行
- 将创建一个新的。假设他在地址 74
Dog
- 我们将参数赋值为 74
someDog
- 将创建一个新的。假设他在地址 74
- 在“CCC”行
- someDog 被跟踪到它所指向的(地址 74 处的对象)
Dog
Dog
- 那个(地址 74 的那个)被要求将他的名字改为 Rowlf
Dog
- someDog 被跟踪到它所指向的(地址 74 处的对象)
- 然后,我们返回
现在让我们考虑一下方法之外会发生什么:
myDog
变了吗?
这是关键。
请记住,这是一个指针,而不是实际的,答案是否定的。 仍然具有值 42;它仍然指向原始版本(但请注意,由于“AAA”行,它的名字现在是“Max”——仍然是同一个狗;的值没有改变。myDog
Dog
myDog
Dog
myDog
关注一个地址并更改其末尾的内容是完全有效的;但是,这不会改变变量。
Java 的工作方式与 C 完全相同。您可以分配指针,将指针传递给方法,跟随方法中的指针并更改指向的数据。但是,调用方不会看到您对该指针指向的位置所做的任何更改。(在具有按引用传递语义的语言中,方法函数可以更改指针,调用方将看到该更改。
在 C++、Ada、Pascal 和其他支持按引用传递的语言中,您实际上可以更改传递的变量。
如果 Java 具有按引用传递的语义,那么我们上面定义的方法在分配 BBB 行时会改变指向的位置。foo
myDog
someDog
将引用参数视为传入变量的别名。分配该别名后,传入的变量也会分配该别名。
更新
评论中的讨论值得澄清......
在 C 语言中,你可以写
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
int x = 1;
int y = 2;
swap(&x, &y);
这在 C 中不是特例。这两种语言都使用按值传递的语义。在这里,调用站点正在创建其他数据结构,以帮助函数访问和操作数据。
该函数将指针传递给数据,并遵循这些指针来访问和修改该数据。
在 Java 中,调用方设置辅助结构的类似方法可能是:
void swap(int[] x, int[] y) {
int temp = x[0];
x[0] = y[0];
y[0] = temp;
}
int[] x = {1};
int[] y = {2};
swap(x, y);
(或者,如果您希望这两个示例都演示其他语言所没有的功能,请创建一个可变的 IntWrapper 类来代替数组)
在这些情况下,C 和 Java 都在模拟按引用传递。它们仍然传递值(指向整数或数组的指针),并在被调用的函数中遵循这些指针来操作数据。
按引用传递是关于函数声明/定义,以及它如何处理其参数。引用语义适用于对该函数的每次调用,调用站点只需要传递变量,无需额外的数据结构。
这些模拟需要调用站点和函数进行协作。毫无疑问,它是有用的,但它仍然是按价值传递的。
评论
在 C++ 中: 注意:代码错误 - 内存泄漏!但它证明了这一点。
void cppMethod(int val, int &ref, Dog obj, Dog &objRef, Dog *objPtr, Dog *&objPtrRef)
{
val = 7; // Modifies the copy
ref = 7; // Modifies the original variable
obj.SetName("obj"); // Modifies the copy of Dog passed
objRef.SetName("objRef"); // Modifies the original Dog passed
objPtr->SetName("objPtr"); // Modifies the original Dog pointed to
// by the copy of the pointer passed.
objPtr = new Dog("newObjPtr"); // Modifies the copy of the pointer,
// leaving the original object alone.
objPtrRef->SetName("objRefPtr"); // Modifies the original Dog pointed to
// by the original pointer passed.
objPtrRef = new Dog("newObjPtrRef"); // Modifies the original pointer passed
}
int main()
{
int a = 0;
int b = 0;
Dog d0 = Dog("d0");
Dog d1 = Dog("d1");
Dog *d2 = new Dog("d2");
Dog *d3 = new Dog("d3");
cppMethod(a, b, d0, d1, d2, d3);
// a is still set to 0
// b is now set to 7
// d0 still have name "d0"
// d1 now has name "objRef"
// d2 now has name "objPtr"
// d3 now has name "newObjPtrRef"
}
在 Java 中,
public static void javaMethod(int val, Dog objPtr)
{
val = 7; // Modifies the copy
objPtr.SetName("objPtr") // Modifies the original Dog pointed to
// by the copy of the pointer passed.
objPtr = new Dog("newObjPtr"); // Modifies the copy of the pointer,
// leaving the original object alone.
}
public static void main()
{
int a = 0;
Dog d0 = new Dog("d0");
javaMethod(a, d0);
// a is still set to 0
// d0 now has name "objPtr"
}
Java 只有两种类型的传递:内置类型的按值传递,对象类型的按指针值传递。
评论
这有点难以理解,但 Java 总是复制该值 - 关键是,通常该值是一个引用。因此,你最终会得到同一个对象,而不假思索......
问题的关键在于,表达式“通过引用传递”中的“引用”一词的含义与Java中“引用”一词的通常含义完全不同。
通常,在 Java 中,引用是指对对象的引用。但是,来自编程语言理论的引用/值的技术术语是谈论对持有变量的存储单元的引用,这是完全不同的东西。
评论
在 Java 中,你永远不能通过引用传递,其中一个显而易见的方式是当你想从方法调用中返回多个值时。请考虑以下 C++ 代码:
void getValues(int& arg1, int& arg2) {
arg1 = 1;
arg2 = 2;
}
void caller() {
int x;
int y;
getValues(x, y);
cout << "Result: " << x << " " << y << endl;
}
有时你想在 Java 中使用相同的模式,但你不能;至少不是直接的。相反,您可以执行以下操作:
void getValues(int[] arg1, int[] arg2) {
arg1[0] = 1;
arg2[0] = 2;
}
void caller() {
int[] x = new int[1];
int[] y = new int[1];
getValues(x, y);
System.out.println("Result: " + x[0] + " " + y[0]);
}
正如前面的答案所解释的,在 Java 中,您将指向数组的指针作为值传递给 。这就足够了,因为该方法随后会修改数组元素,并且按照惯例,您期望元素 0 包含返回值。显然,您可以通过其他方式执行此操作,例如构建代码,因此这不是必需的,或者构造一个可以包含返回值或允许设置返回值的类。但是上面的 C++ 中可用的简单模式在 Java 中不可用。getValues
据我所知,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 制作一个包装器,这将使它能够将其与 String 一起使用:
class StringWrapper {
public String value;
public StringWrapper(String value) {
this.value = value;
}
}
public static void appendWorld(StringWrapper s){
s.value = s.value +" World";
}
public static void main(String[] args) {
StringWrapper s = new StringWrapper("Hello");
appendWorld(s);
System.out.println(s.value);
}
编辑:我相信这也是在“添加”两个字符串时使用 StringBuffer 的原因,因为您可以修改原始对象,而像 String 这样的不可变对象则无法修改。
对一些帖子进行了一些更正。
C 不支持通过引用传递。它始终按值传递。C++ 确实支持通过引用传递,但不是默认设置,并且非常危险。
在 Java 中,值是什么并不重要:对象的原始或地址(大致),它总是按值传递。
如果一个 Java 对象的“行为”就像它是通过引用传递的,那么这是一个可变性的属性,与传递机制完全无关。
我不确定为什么这如此令人困惑,也许是因为这么多 Java“程序员”没有接受过正式培训,因此不明白内存中到底发生了什么?
评论
Java 按值复制引用。因此,如果将其更改为其他内容(例如,使用 ),则引用不会在方法之外更改。对于本机类型,它始终按值传递。new
看看这个代码。此代码不会抛出...它将打印“Vinay”NullPointerException
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 是通过引用传递的,那么它应该被抛出,因为引用设置为 Null。NullPointerException
我简直不敢相信还没有人提到芭芭拉·利斯科夫。当她在 1974 年设计 CLU 时,她遇到了同样的术语问题,她发明了术语“通过共享调用”(也称为“按对象共享调用”和“按对象调用”)来描述“按值调用,其中值是引用”。
评论
这将使您对 Java 的真正工作方式有所了解,以至于在下一次讨论 Java 通过引用或按值传递时,您只会微笑 :-)
第一步,请从你的脑海中抹去那个以“p”开头的单词“_ _ _ _ _ _”,特别是如果你来自其他编程语言。Java 和 'p' 不能写在同一本书、论坛甚至 txt 中。
第二步请记住,当您将 Object 传递到方法中时,您传递的是 Object 引用,而不是 Object 本身。
- 学生:师父,这是否意味着Java是按引用传递的?
- 师傅:蚱蜢,没有。
现在想想一个对象的引用/变量是做什么的:
- 变量包含告诉 JVM 如何访问内存(堆)中引用的对象的位。
- 将参数传递给方法时,您不是在传递引用变量,而是在传递引用变量中位的副本。像这样的东西:3bad086a。3bad086a 表示一种访问传递对象的方法。
- 所以你只是传递 3bad086a 它是引用的值。
- 您传递的是引用的值,而不是引用本身(而不是对象)。
- 这个值实际上是 COPIED 并提供给方法的。
在以下内容中(请不要尝试编译/执行此...
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. }
会发生什么?
- 变量 person 在第 #1 行中创建,开头为 null。
- 在第 #2 行中创建一个新的 Person 对象,该对象存储在内存中,变量 person 被赋予对 Person 对象的引用。也就是说,它的地址。假设 3bad086a。
- 持有 Object 地址的变量 person 被传递给第 #3 行中的函数。
- 在第 #4 行中,您可以听到寂静的声音
- 检查第 #5 行的评论
- 创建了一个方法局部变量 -anotherReferenceToTheSamePersonObject-,然后在第 #6 行中出现了魔术:
- 变量/引用人员被逐位复制并传递给函数内的 anotherReferenceToTheSamePersonObject。
- 不会创建新的 Person 实例。
- “person”和“anotherReferenceToTheSamePersonObject”都具有相同的 3bad086a 值。
- 不要尝试这个,但 person==anotherReferenceToTheSamePersonObject 会是真的。
- 这两个变量都具有引用的相同副本,并且它们都引用同一个 Person 对象,堆上的同一个对象,而不是副本。
一张图片胜过千言万语:
请注意,anotherReferenceToTheSamePersonObject 箭头指向 Object,而不是指向变量 person!
如果你不明白,那么请相信我,并记住,最好说Java是按价值传递的。好吧,通过参考值传递。哦,好吧,更好的是变量值的传递副本! ;)
现在,请随意讨厌我,但请注意,鉴于此,在谈论方法参数时,传递原始数据类型和对象之间没有区别。
您总是传递引用值的位的副本!
- 如果是基元数据类型,则这些位将包含基元数据类型本身的值。
- 如果它是一个 Object,则位将包含地址的值,该地址告诉 JVM 如何访问该 Object。
Java 是按值传递的,因为在方法中,您可以根据需要修改引用的对象,但无论您多么努力,您都永远无法修改传递的变量,该变量无论如何都会继续引用(不是 p _ _ _ _ _ _)同一个对象!
上面的 changeName 函数永远无法修改传递引用的实际内容(位值)。换句话说,changeName 不能使 Person 引用另一个 Object。
当然,你可以把它缩短,只说 Java 是按值传递的!
评论
一切都是按值传递的。基元和对象引用。但是,如果对象的接口允许,则可以更改对象。
将对象传递给方法时,传递的是引用,并且该对象可以通过方法实现进行修改。
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”是引用(指向对象的指针),不能更改。
基元类型是按值传递的。对象的引用也可以被视为基元类型。
总而言之,一切都是按值传递的。
有两个案例值得关注:
对于基元类型的变量(例如、、等),当您在函数参数中使用变量名称时,您是按值传递的。此值(例如,、或 )是“复制的”,并且变量在方法调用后保留其原始值,因为现在存在两个数据副本。一个在函数调用之外,另一个在函数调用内部。int
boolean
char
5
true
'c'
对于引用类型的变量(例如,、等),当您将变量名称用于函数参数时,您将传递变量中包含的引用值。引用值被复制,就像上面的第一个示例一样,函数外部的变量也保留了其值,即方法调用。引用仍指向同一对象。在这种情况下,不同的是,该函数可能会更改所引用对象内部的数据。String
Object
无论哪种方式,你总是按价值传递东西。
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");
}
}
我将分步解释这一点:
声明一个名为 type 的引用,并为其分配一个具有属性 的新对象。
f
Foo
Foo
"f"
Foo f = new Foo("f");
从方法端,声明了具有名称的类型的引用,并最初为其赋值。
Foo
a
null
public static void changeReference(Foo a)
当你调用该方法时,引用将被分配作为参数传递的对象。
changeReference
a
changeReference(f);
声明一个名为 type 的引用,并为其分配一个具有属性 的新对象。
b
Foo
Foo
"b"
Foo b = new Foo("b");
a = b
对属性为 的对象的引用 而不是 进行新的赋值。a
f
"b"
调用 method 时,将创建一个引用,并为对象分配属性 。
modifyReference(Foo c)
c
"f"
c.setAttribute("c");
将更改引用指向它的对象的属性,并且它与引用指向它的对象相同。c
f
评论
不,它不是通过引用传递的。
Java 是根据 Java 语言规范按值传递的:
调用方法或构造函数 (§15.12) 时,实际参数表达式的值会在执行方法或构造函数的主体之前初始化新创建的参数变量(每个声明的类型)。DeclaratorId 中显示的 Identifier 可以用作方法或构造函数主体中的简单名称,以引用形式参数。
评论
Java 是通过常量引用传递的,其中传递了引用的副本,这意味着它基本上是按值传递。如果类是可变的,则可以更改引用的内容,但不能更改引用本身。换句话说,地址是按值传递的,因此无法更改,但地址指向的内容可以更改。对于不可变的类,引用的内容也无法更改。
评论
final.
Java 始终是按值传递的,参数是变量传递内容的副本,所有对象都是使用引用定义的,而 reference 是一个变量,用于存储对象在内存中位置的内存地址。
检查注释以了解执行中发生的情况;跟随数字,因为它们显示了执行流程。
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());
}
}
评论
为了增加更多内容,我想我会包括关于该主题的 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 Projects\Reference>java ReferenceTest Before modify() d.height = 10 dim = 11 After modify() d.height = 11
请注意,当第 4 行的 Dimension 对象传递给 modify() 方法时,对方法内部发生的对象所做的任何更改都将对传递其引用的对象进行。在前面的示例中,引用变量 d 和 dim 都指向同一个对象。
Java 是否使用按值传递语义?
如果 Java 通过传递引用变量来传递对象,这是否意味着 Java 对对象使用按引用传递?不完全是,尽管您经常会听到和读到它确实如此。Java 实际上是在单个 VM 中运行的所有变量的按值传递的。Pass-by-value 表示按变量值传递。这意味着,逐个复制变量!(又有那个字复制!
如果你传递的是原始变量或引用变量,这没有区别,你总是在传递变量中位的副本。因此,对于基元变量,您传递的是表示该值的位的副本。例如,如果传递值为 3 的 int 变量,则传递的是表示 3 的位的副本。然后,被调用的方法获取自己的值副本,以执行它喜欢的操作。
如果要传递对象引用变量,则传递的是表示对象引用的位的副本。然后,被调用的方法会获取自己的引用变量副本,以执行它喜欢的操作。但是,由于两个相同的引用变量引用完全相同的对象,因此,如果被调用方法修改了该对象(例如,通过调用 setter 方法),调用方将看到调用方的原始变量引用的对象也已更改。在下一节中,我们将了解当我们谈论基元时图片是如何变化的。
按值传递的底线:被调用的方法不能更改调用方的变量,尽管对于对象引用变量,被调用方法可以更改变量引用的对象。更改变量和更改对象有什么区别?对于对象引用,这意味着被调用的方法不能重新分配调用方的原始引用变量,并使其引用不同的对象或 null。例如,在以下代码片段中,
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 仅按值传递。一个非常简单的例子来验证这一点。
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 具有按值传递(和按值传递引用)。
- C# 还具有按引用传递
在 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#):按值和引用传递
按值传递的底线:被调用的方法不能更改调用方的 变量,尽管对于对象引用变量,调用的方法可以更改 对象所引用的变量。更改变量有什么区别 并更改对象?对于对象引用,这意味着被调用的方法不能 重新分配调用方的原始引用变量,并使其引用不同的对象, 或 null。
我从一本关于 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: test.abc.Foo@62f72617 的对象参考
我觉得争论“按引用传递与按值传递”并不是很有帮助。
如果你说,“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 并在堆中为其分配空间,字符串的地址存储在堆栈上并给定标识符,因为第二个 String 的地址与第一个 String 的地址相同,因此不会创建新 String,也不会分配新的堆空间,而是在堆栈上创建一个新标识符。然后我们调用:创建一个新的堆栈帧,创建一个新的标识符,并分配已经存在的 String 的地址。hisName
shout()
name
那么,价值,参考?你说“土豆”。
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 |
+---+
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 的值为 (我们不知道实际的 JVM 内部值,这只是一个示例)。0x100234
new PassByValue().changeValue(t);
当将引用 t 传递给函数时,它不会直接传递对象测试的实际引用值,而是会创建一个 t 的副本,然后传递给函数。由于它是按值传递的,因此它传递的是变量的副本,而不是变量的实际引用。既然我们说 t 的值是,那么 t 和 f 将具有相同的值,因此它们将指向同一个对象。0x100234
If you change anything in the function using reference f it will modify the existing contents of the object. That is why we got the output , which is updated in the function.changevalue
To understand this more clearly, consider the following example:
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;
}
Will this throw a ? No, because it only passes a copy of the reference.
In the case of passing by reference, it could have thrown a , as seen below:NullPointerException
NullPointerException
The Java programming language passes arguments only by value, that is, you cannot change the argument value in the calling method from within the called method.
但是,当对象实例作为参数传递给方法时, 参数的值不是对象本身,而是对 对象。您可以在被调用的方法中更改对象的内容,但 不是对象引用。
对许多人来说,这看起来像是按引用传递,从行为上讲,它有 与按引用传递有很多共同之处。但是,有两个原因 这是不准确的。
首先,将事物更改为 方法仅适用于对象,不适用于基元值。
二、实际 与对象类型的变量关联的值是对 对象,而不是对象本身。这是其他方面的一个重要区别 方式,如果清楚地理解,则完全支持以下观点: 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 中,一切都是引用,所以当你有类似的东西时: Java 执行以下操作:Point pnt1 = new Point(0,0);
- 创建新的 Point 对象
- 创建新的点引用,并将该引用初始化为以前创建的点对象上的点(引用)。
- 从这里开始,通过 Point 对象生命周期,您将通过 pnt1 访问该对象
参考。因此,我们可以说,在 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
所以在执行方法之后,当你返回时,就有这样的情况:tricky
main
所以现在,完全执行的程序将是:
X1: 0 Y1: 0
X2: 0 Y2: 0
X1: 100 Y1: 100
X2: 0 Y2: 0
评论
Java 按值传递对对象的引用。
因此,如果对引用参数指向的对象进行任何修改,它将反映回原始对象。
但是,如果引用参数指向另一个对象,则原始引用仍将指向原始对象。
引用在表示时始终是一个值,无论您使用哪种语言。
为了获得开箱即用的视图,让我们看一下程序集或一些低级内存管理。在 CPU 级别,如果将任何内容写入内存或某个 CPU 寄存器,则对任何内容的引用会立即成为值。(这就是为什么指针是一个很好的定义。它是一种价值,同时具有目的)。
内存中的数据有一个 Location,在该位置有一个值(字节、字等)。在 Assembly 中,我们有一个方便的解决方案,可以为某个位置(又名变量)指定一个 Name,但是在编译代码时,汇编程序只需将 Name 替换为指定位置,就像浏览器将域名替换为 IP 地址一样。
从本质上讲,从技术上讲,不可能在不表示任何语言的情况下传递对任何语言的引用(当它立即成为值时)。
假设我们有一个变量 Foo,它的位置位于内存中的第 47 个字节,其值为 5。我们还有另一个变量 Ref2Foo,它位于内存中的第 223 个字节,其值将为 47。此 Ref2Foo 可能是一个技术变量,不是由程序显式创建的。如果您只查看 5 和 47 而不查看任何其他信息,您将只看到两个值。
如果您将它们用作参考,那么要达到我们必须旅行:5
(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223] -> 47
(Foo)[47] -> 5
这就是跳转表的工作方式。
如果我们想用 Foo 的值调用方法/函数/过程,有几种可能的方法可以将变量传递给该方法,具体取决于语言及其几种方法调用模式:
- 5 被复制到其中一个 CPU 寄存器(即。EAX)。
- 5 将 PUSHd 推送到堆栈。
- 47 被复制到其中一个 CPU 寄存器
- 47 PUSHd 到堆栈。
- 223 被复制到其中一个 CPU 寄存器。
- 223 将 PUSHd 推送到堆栈。
在上述每种情况下,都创建了一个值(现有值的副本),现在由接收方法处理它。当您在方法中编写“Foo”时,它要么从 EAX 中读出,要么自动取消引用,或者双重取消引用,该过程取决于语言的工作方式和/或 Foo 的类型所指示的内容。这对开发人员是隐藏的,直到她绕过了取消引用过程。因此,引用在表示时是一个值,因为引用是必须处理的值(在语言级别)。
现在我们已经将 Foo 传递给了以下方法:
- 在案例 1 中。和 2.如果更改 Foo (),它只会影响本地范围,因为您有 Value 的副本。从方法内部,我们甚至无法确定原始 Foo 在内存中的位置。
Foo = 9
- 在案例 3.和 4.如果您使用默认语言结构并更改 Foo(),它可能会全局更改 Foo(取决于语言,即。Java 或类似 Pascal 的 var m)。但是,如果语言允许您规避取消引用过程,则可以将 、 更改为 。在这一点上,如果你阅读它,Foo似乎已经改变了,因为你已经改变了它的本地指针。如果你在方法()中修改这个Foo,你可能会对程序的执行(又名段错误)进行FUBAR,因为你将写入与预期不同的内存,你甚至可以修改一个注定要保存可执行程序的区域,写入它将修改正在运行的代码(Foo现在不在)。但是 Foo 的值没有全局更改,只有方法内部的值发生了变化,因为也是该方法的副本。
Foo = 11
procedure findMin(x, y, z: integer;
: integer);
47
49
Foo = 12
47
47
47
- 在案例 5.和 6.如果在方法内部进行修改,则会产生与 3 中相同的混乱。或 4.(一个指针,指向一个现在错误的值,再次用作指针)但这仍然是一个本地问题,因为 223 被复制了。但是,如果您能够取消引用(即),到达并修改指向值,例如,to,它将全局影响 Foo,因为在这种情况下,方法获得了 的副本,但引用的副本仅存在一次,并且将其更改为将导致每次双重取消引用都指向错误的值。
223
Ref2Foo
223
47
49
223
47
49
Ref2Foo
对无关紧要的细节吹毛求疵,即使是按引用传递的语言也会将值传递给函数,但这些函数知道它们必须将其用于取消引用目的。这种将引用传递为值对程序员来说是隐藏的,因为它实际上毫无用处,并且术语只是按引用传递。
严格的按值传递也是无用的,这意味着每次我们调用以数组为参数的方法时,都必须复制一个 100 MB 的数组,因此 Java 不能严格地按值传递。每种语言都会传递对这个巨大数组的引用(作为值),并且如果该数组可以在方法内部本地更改,则采用写入时复制机制,或者允许方法(如 Java 那样)全局修改数组(从调用者的角度来看),并且一些语言允许修改引用本身的值。
因此,简而言之,用 Java 自己的术语来说,Java 是按值传递的,其中 value 可以是:可以是实际值,也可以是作为引用表示的值。
@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
我想我会贡献这个答案来添加规范中的更多细节。
首先,按引用传递与按值传递有什么区别?
通过引用传递意味着被调用函数的参数将是 与调用方传递的参数相同(不是值,而是标识
- 变量本身)。
按值传递意味着被调用函数的参数将是 调用方传递的参数。
或者来自维基百科,关于通过引用的主题
在按引用调用评估(也称为 pass-by-reference),函数接收对 变量用作参数,而不是其值的副本。这 通常意味着该函数可以修改(即赋值给) 用作参数的变量 - 其调用者将看到的内容。
在按值调用中,计算参数表达式,并且 结果值绑定到函数 [...] 中的相应变量。 如果函数或过程能够为其赋值 参数,则仅为其本地副本分配 [...]。
其次,我们需要知道 Java 在其方法调用中使用了什么。Java 语言规范指出
调用方法或构造函数 (§15.12) 时, 实际参数表达式初始化新创建的参数 变量,每个声明的类型,在执行 方法或构造函数。
因此,它将参数的值赋值(或绑定)到相应的参数变量。
参数的价值是什么?
让我们考虑一下引用类型,Java 虚拟机规范状态
引用类型有三种:类类型、数组类型、 和接口类型。它们的值是动态引用 创建的类实例、数组或类实例或数组 分别实现接口。
Java 语言规范还指出
引用值(通常只是引用)是指向这些对象的指针,以及一个特殊的 null 引用,它不引用任何对象。
参数的值(某种引用类型)是指向对象的指针。请注意,变量、具有引用类型返回类型的方法的调用以及实例创建表达式 () 都解析为引用类型值。new ...
所以
public void method (String param) {}
...
String variable = new String("ref");
method(variable);
method(variable.toString());
method(new String("ref"));
都将对实例的引用值绑定到方法新创建的参数 。这正是按值传递的定义所描述的。因此,Java 是按值传递的。String
param
您可以按照引用来调用方法或访问引用对象的字段这一事实与对话完全无关。按引用传递的定义是
这通常意味着该函数可以修改(即分配给) 用作参数的变量 - 其调用者将看到的内容。
在 Java 中,修改变量意味着重新赋值它。在 Java 中,如果在方法中重新分配变量,调用者不会注意到它。修改变量引用的对象是完全不同的概念。
基元值也在此处的 Java 虚拟机规范中定义。类型的值是相应的整数或浮点值,经过适当编码(8、16、32、64 等位)。
分两步了解:
不能更改对对象本身的引用,但可以将此传递的参数用作对对象的引用。
如果要更改引用后面的值,则只需在堆栈上声明一个同名为“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
}
在所有答案中,我们看到 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 中有一个解决方法可供参考。让我通过这个例子来解释:
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)
}}
我希望这会有所帮助!
评论
Object
简单的程序
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按值传递所有内容!!
通过传入名称和年龄来创建对象:
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
如果你能理解这个例子,我们就完成了。 否则,请访问此网页以获取详细说明:
评论
Java 按 VALUE 传递参数,并且仅按值传递参数。
长话短说:
对于来自 C# 的用户:没有“out”参数。
对于那些来自 PASCAL 的用户:没有“var”参数。
这意味着您无法从对象本身更改引用,但始终可以更改对象的属性。
解决方法是改用参数。而且你总是可以使用数组!StringBuilder
String
让我试着用四个例子来解释我的理解。Java 是按值传递的,而不是按引用传递的
/**
按值传递
在 Java 中,所有参数都是按值传递的,即赋值方法参数对调用者不可见。
*/
示例 1:
public class PassByValueString {
public static void main(String[] args) {
new PassByValueString().caller();
}
public void caller() {
String value = "Nikhil";
boolean valueflag = false;
String output = method(value, valueflag);
/*
* 'output' is insignificant in this example. we are more interested in
* 'value' and 'valueflag'
*/
System.out.println("output : " + output);
System.out.println("value : " + value);
System.out.println("valueflag : " + valueflag);
}
public String method(String value, boolean valueflag) {
value = "Anand";
valueflag = true;
return "output";
}
}
结果
output : output
value : Nikhil
valueflag : false
示例 2:
/** * * 按值传递 * */
public class PassByValueNewString {
public static void main(String[] args) {
new PassByValueNewString().caller();
}
public void caller() {
String value = new String("Nikhil");
boolean valueflag = false;
String output = method(value, valueflag);
/*
* 'output' is insignificant in this example. we are more interested in
* 'value' and 'valueflag'
*/
System.out.println("output : " + output);
System.out.println("value : " + value);
System.out.println("valueflag : " + valueflag);
}
public String method(String value, boolean valueflag) {
value = "Anand";
valueflag = true;
return "output";
}
}
结果
output : output
value : Nikhil
valueflag : false
示例 3:
/** 这种“按值传递”有一种“按引用传递”的感觉
有人说基元类型和“字符串”是“按值传递” 对象是“通过引用传递”。
但从这个例子中,我们可以理解它实际上只是按值传递, 请记住,这里我们将引用作为值传递。 IE:引用是按值传递的。 这就是为什么能够改变,并且在本地范围之后仍然适用。 但是我们不能在原始范围之外更改实际引用。 PassByValueObjectCase2 的下一个示例演示了这意味着什么。
*/
public class PassByValueObjectCase1 {
private class Student {
int id;
String name;
public Student() {
}
public Student(int id, String name) {
super();
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + "]";
}
}
public static void main(String[] args) {
new PassByValueObjectCase1().caller();
}
public void caller() {
Student student = new Student(10, "Nikhil");
String output = method(student);
/*
* 'output' is insignificant in this example. we are more interested in
* 'student'
*/
System.out.println("output : " + output);
System.out.println("student : " + student);
}
public String method(Student student) {
student.setName("Anand");
return "output";
}
}
结果
output : output
student : Student [id=10, name=Anand]
示例 4:
/**
除了示例3(PassByValueObjectCase1.java)中提到的内容外,我们无法更改原始范围之外的实际引用。
注意:我没有粘贴 的代码。的类定义与 Example3 相同。private class Student
Student
*/
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 始终使用按值调用。这意味着该方法获取所有参数值的副本。
考虑以下 3 种情况:
1) 尝试更改原始变量
public static void increment(int x) { x++; }
int a = 3;
increment(a);
x 将复制 a 的值并递增 x,a 保持不变
2) 尝试更改对象的原始字段
public static void increment(Person p) { p.age++; }
Person pers = new Person(20); // age = 20
increment(pers);
p 将复制 PERS 的引用值并增加 age 字段,变量引用同一对象,因此 age 被更改
3) 尝试更改参考变量的参考值
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);
调用交换 P1 后,P2 复制 pers1 和 pers2 的引用值,与值交换,因此 pers1 和 pers2 保持不变
因此,您只能更改方法中对象的字段,将引用值的副本传递给此对象。
这么多长答案。让我举一个简单的例子:
- Java 总是按值传递所有内容
- 这意味着引用也是按值传递的
简而言之,您不能修改传递的任何参数的值,但可以调用方法或更改传递的对象引用的属性。
评论
我制作了这个小图表,显示了数据是如何创建和传递的
注意:基元值作为值传递,对该值的第一个引用是方法的参数
这意味着:
- 您可以在函数内部更改
myObject
- 但是你不能在函数内部更改对什么的引用,因为不是
myObject
point
myObject
- 请记住,两者都是引用,不同的引用,但是,这些引用指向相同
point
myObject
new Point(0,0)
围绕这个问题的很多困惑来自于这样一个事实,即 Java 试图重新定义“按值传递”和“按引用传递”的含义。重要的是要了解这些是行业术语,在该上下文之外无法正确理解。它们旨在帮助您编写代码,并且易于理解,因此让我们首先了解一下它们的含义。
可以在此处找到对两者的良好描述。
按值传递函数接收的值是调用方正在使用的对象的副本。它对于函数来说是完全唯一的,你对该对象所做的任何事情都只会在函数中看到。
通过引用传递函数接收的值是对调用方正在使用的对象的引用。调用方将看到函数对 value 引用的对象执行的任何操作,并且从该点开始它将处理这些更改。
从这些定义中可以清楚地看出,引用是按值传递的这一事实是无关紧要的。如果我们接受这个定义,那么这些术语就变得毫无意义,所有的语言都只是按值传递。
无论以何种方式传入引用,它都只能按值传递。这不是重点。关键是你将对自己的对象的引用传递给了函数,而不是它的副本。您可以扔掉收到的参考资料这一事实无关紧要。同样,如果我们接受这个定义,这些术语就会变得毫无意义,每个人都总是在传递价值。
不,C++ 特殊的“引用传递”语法并不是引用传递的唯一定义。它纯粹是一种方便的语法,旨在使您在传入指针后不需要使用指针语法。它仍然在传递指针,编译器只是向你隐瞒了这一事实。它仍然会传递该指针 BY VALUE,编译器只是对你隐藏了它。
因此,有了这种理解,我们可以看看 Java,看看它实际上两者兼而有之。所有 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
瞧,Foo 在 main 中被传递给 bar 的参考,仍然没有改变!
这个例子清楚地表明,当我们说“通过引用传递”时,java 与 C++ 不同。从本质上讲,java 将“引用”作为“值”传递给函数,这意味着 java 是按值传递的。
评论
Foo(99)
在 Java 中,方法参数都是按值传递的:
Java 参数都是按值传递的(当方法使用时会复制值或引用):
对于基元类型,Java 行为很简单: 该值将复制到基元类型的另一个实例中。
对于对象,这是一样的: 对象变量是使用 “new” 关键字创建的引用(仅保存 Object 地址而不是原始值的 mem 存储桶),并且像原始类型一样被复制。
行为可能看起来与基元类型不同:因为复制的对象变量包含相同的地址(到同一个对象)。 对象的内容/成员可能仍然在方法中被修改,然后访问外部,给人一种错觉,即(包含)对象本身是通过引用传递的。
“字符串”对象似乎是都市传说“对象通过引用传递”的一个很好的反例:
实际上,使用方法,您将永远无法更新作为参数传递的 String 的值:
String 对象,通过声明为 final 且无法修改的数组保存字符。 只有对象的地址可以被另一个使用“new”的地址替换。 使用“new”更新变量,不会允许从外部访问对象,因为变量最初是按值传递和复制的。
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 所示,该方法引用了 swap,但未引用原始引用。不幸的是,在方法调用之后,您只剩下未交换的原始引用。为了在方法调用之外成功进行交换,我们需要交换原始引用,而不是副本。
检查一种语言是否支持按引用传递的简单测试就是编写传统的交换。 你能用 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 不允许这样的事情,因为它只支持传递值,而不支持指针或引用。
评论
主要的基石知识必须是引用的知识,
当对象引用传递给方法时,引用本身 通过使用按值调用来传递。但是,由于值是 passed 引用一个对象,该值的副本仍将引用 由其对应参数引用的同一对象。
Java: A Beginner's Guide, Sixth Edition(《Java:初学者指南》第六版),Herbert Schildt
似乎一切都是按 java 中的值调用的,正如我试图通过以下程序理解的那样
S级
class S{
String name="alam";
public void setName(String n){
this.name=n;
}}
类示例
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);
}}
输出
阿拉姆
阿拉姆
塔列夫
阿拉姆
塔列夫
哈利
正如我们定义了具有实例变量名称的类 S 和值 taleev 一样,因此对于 我们从中初始化的所有对象都将具有值为 Taleev 的 Name 变量,但是如果我们更改任何对象的名称值,那么它只会更改该类 (Object) 副本的名称,而不是每个类的名称,因此在那之后,当我们执行 System.out.println(s.name) 时,它只是打印 taleev,我们不能更改我们最初定义的名称值, 我们要更改的值是对象的值,而不是实例变量的值,因此一旦我们定义了实例变量,我们就无法更改它
所以我认为这就是它如何表明 java 只处理值而不是引用
基元变量的内存分配可以这样理解
毫无疑问,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
因此,在这两种情况下,无论方法内部发生什么,都不会更改实际的 Object,因为传递的是该对象的值,而不是对对象本身的引用。
但是,当您将自定义对象传递给方法,并且该方法并更改它时,它也会更改实际对象,因为即使您传递了该对象,您也将其引用作为值传递给了该方法。让我们再举一个例子:
@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]
我试图简化上面的例子,只保留问题的本质。 让我把这个故事作为一个容易记住和正确应用的故事来呈现。 故事是这样的: 你有一只宠物狗,吉米,它的尾巴有 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 不允许您在按值传递和按引用传递之间进行选择。
所有参数都按值传递。
一个方法调用可以将两个传递给一个方法types of values
- 基元值的副本(例如,int 和 double 类型的值)
- 对象引用的副本。
Objects themselves cannot be passed to methods
.当方法修改基元类型参数时,对参数的更改对调用方法中的原始参数值没有影响。
对于引用类型参数也是如此。如果修改引用类型参数以使其引用另一个对象,则仅该参数引用新对象,而存储在调用方变量中的引用仍引用原始对象。
参考资料:Java™ How To Program (Early Objects), Tenth Edition
评论
Java 是按值传递(堆栈内存)
运作方式
我们先来了解一下 java 存储原始数据类型和对象数据类型的位置。
基元数据类型本身和对象引用存储在堆栈中。 对象本身存储在堆中。
这意味着,堆栈内存存储原始数据类型以及 对象的地址。
并且您始终传递引用值位的副本。
如果它是原始数据类型,那么这些复制的位包含原始数据类型本身的值,这就是为什么当我们在方法内部更改参数值时,它不会反映外部的变化。
如果它是像 Foo foo=new Foo() 这样的对象数据类型,那么在这种情况下,对象地址的副本会像文件快捷方式一样传递,假设我们在 C:\desktop 有一个文本文件 abc.txt,假设我们制作同一文件的快捷方式并将其放在 C:\desktop\abc-shortcut 中,以便当您从 C:\ desktop\abc.txt 并写入“Stack Overflow”并关闭文件,然后再次从快捷方式打开文件,然后您编写“是程序员学习的最大在线社区”,然后总文件更改将是“Stack Overflow 是程序员学习的最大在线社区”,这意味着您从哪里打开文件并不重要, 每次我们访问同一个文件时,这里我们可以假设 Foo 是一个文件,并假设 Foo 存储在 123hd7h(原始地址,如 C:\desktop\abc.txt )地址和 234jdid(复制的地址,如 C:\desktop\abc-shortcut,实际上包含文件的原始地址).. 因此,为了更好地理解,制作快捷方式文件和感觉..
评论
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 指向相同的 dummyObject。b = new DummyObject()
此线程 (https://stackoverflow.com/a/12429953/4233180) 中其中一个答案中的插图可以帮助获得更深入的理解。
评论
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
如果你看一下输出的前两行,很明显交换方法不起作用。这是因为 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 严格按值传递
当我说按值传递时,这意味着每当调用者调用被调用方时,参数(即:要传递给另一个函数的数据)都会被复制并放置在形式参数(被调用方用于接收输入的局部变量)中。Java 仅在按值传递环境中从一个函数到另一个函数进行数据通信。
重要的一点是要知道,即使是 C 语言也严格地仅按值传递:即:
数据从调用方复制到被调用方,并且被调用方执行的操作位于相同的内存位置,并且
我们传递给它们的是我们从 (&) 运算符中获取的该位置的地址,形式参数中使用的标识符被声明为指针变量 (*),使用它我们可以进入内存位置以访问其中的数据。
因此,这里的形式参数只不过是该位置的别名。在该位置上所做的任何修改都是可见的,只要该变量的作用域(标识该位置)处于活动状态。
在 Java 中,没有指针的概念(即:没有所谓的指针变量),尽管我们可以将引用变量视为指针,但在技术上,在 java 中我们将其称为句柄。我们之所以在 java 中将指向地址的指针称为句柄,是因为指针变量不仅能够执行单个取消引用,而且能够执行多个取消引用
例如:in P 表示 p 指向一个整数
在 C 中表示 p 是指向整数指针的指针
我们在 Java 中没有这个工具,所以把它说成句柄是绝对正确且技术上合法的,C 语言中也有指针算术的规则。它允许对具有约束的指针执行算术运算。int *p;
int **p;
在 C 中,我们将这种传递地址并使用指针变量接收它们的机制称为通过引用传递,因为我们在形式参数中传递它们的地址并将它们作为指针变量接收,但在编译器级别,该地址被复制到指针变量中(因为这里的数据是地址,即使它的数据也是如此),因此我们可以 100% 确定 C 是严格按值传递的(因为我们只传递数据)
(如果我们直接在 C 中传递数据,我们将其称为按值传递。
在 java 中,当我们做同样的事情时,我们用句柄来做;由于它们不像 in 中那样被称为指针变量(如上所述),即使我们正在传递引用,我们也不能说它是通过引用传递的,因为我们没有在 Java 中使用指针变量来收集它。
因此,Java 严格使用按值传递机制
评论
通过传递参数在函数之间共享数据。现在,有 2 种传递参数的方法:
通过引用传递:调用方和被调用方对参数使用相同的变量。
按值传递:caller 和 callee 有两个值相同的自变量。
Java 使用按值传递
- 传递基元数据时,它复制基元数据类型的值。
- 传递对象时,它复制对象的地址并传递给被调用方方法变量。
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
}
}
使用 object 的示例:
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 中,参数传递行为......
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 中寻找的是变量赋值的工作原理。我在文档中找到了它:
您将遇到的最常见的运算符之一是简单的赋值运算符“=” [...]它将右边的值分配给左边的操作数:
int 节奏 = 0;
int 速度 = 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");
}
}
这是我运行的输出:
Assignment operator evaluation using two MyInteger objects named height and width [1] Assign distinct integers to height and width values -> height is 9 and width is 1, we are different things! [2] Assign to height's value the width's value -> height is 1 and width is 1, are we the same thing now? [3] Assign to height's value an integer other than width's value -> height is 9 and width is 1, we are different things yet! [4] Assign to height the width object -> height is 1 and width is 1, are we the same thing now? [5] Assign to height's value an integer other than width's value -> height is 9 and width is 9, we are the same thing now! [6] Assign to height a new MyInteger and an integer other than width's value -> height is 1 and width is 9, we are different things again!
在 [2] 中,我们有不同的对象,并将一个变量的值分配给另一个变量。但是在 [3] 中分配新值后,对象具有不同的值,这意味着在 [2] 中分配的值是原始变量的副本,通常称为按值传递,否则,[3] 中打印的值应该是相同的。
在 [4] 中,我们仍然有不同的对象,并将一个对象分配给另一个对象。在 [5] 中分配一个新值后,对象具有相同的值,这意味着在 [4] 中分配的对象不是另一个对象的副本,这应该称为按引用传递。但是,如果我们仔细看[6],我们不能确定没有复制...... ?????
我们不能那么确定,因为在 [6] 中,对象是相同的,然后我们为其中一个对象分配了一个新对象,之后,对象具有不同的值!如果它们是一样的,它们现在怎么能不同呢?它们在这里也应该是一样的! ?????
我们需要记住文档才能了解发生了什么:
此运算符还可用于对象以指定对象引用
因此,我们的两个变量正在存储引用......我们的变量在 [4] 之后具有相同的引用,在 [6] 之后具有不同的引用......如果这样的事情是可能的,这意味着对象的赋值是通过对象引用的副本完成的,否则,如果它不是引用的副本,则 [6] 中变量的打印值应该是相同的。因此,对象(引用)就像基元一样,通过赋值复制到变量中,人们通常称之为按值传递。这是 Java 中唯一的路过。
PT 1:房地产房源数量
有一个蓝色的,120平方英尺的“小房子”目前停在主街1234号,前面有一个修剪整齐的草坪和花坛。
聘请了当地公司的房地产经纪人,并被告知要保留该房屋的房源。
我们称这位房地产经纪人为“鲍勃”。嗨,鲍勃。
Bob 通过网络摄像头保持他的房源(他称之为房源)的最新状态,这使他能够实时记录实际房屋的任何变化。他还记录了有多少人询问了该列表。
鲍勃的房子的整数今天是 42。tinyHouseAt1234Main
viewTally
每当有人想要了解位于主街 1234 号的蓝色小屋时,他们都会问鲍勃。
鲍勃查看了他的房源,并告诉他们所有关于它的信息——颜色、漂亮的草坪、阁楼床和堆肥厕所等。然后,他将他们的询问添加到他的.不过,他没有告诉他们真实的实际地址,因为鲍勃的公司专门从事随时可以移动的小房子。现在的总数是43。tinyHouseAt1234Main
viewTally
在另一家公司,房地产经纪人可能会明确表示他们的房源“指向”主街 1234 号的房子,并在旁边用一点表示这一点,因为他们主要处理很少搬家的房子(尽管这样做可能是有原因的)。鲍勃的公司不会费心这样做。*
现在,鲍勃当然不会亲自去把实际的房子放在卡车上直接向客户展示——这将是不切实际的,而且是荒谬的资源浪费。传递他的理货表的完整副本是一回事,但一直传递整个房子是昂贵且荒谬的。
(顺便说一句:鲍勃的公司也不会在每次有人询问时都3D打印出上市房屋的新副本和独特的副本。这就是新贵,同名的基于网络的公司及其衍生公司所做的 - 昂贵且速度较慢,人们经常将这两家公司混淆,但无论如何它们都很受欢迎)。
在靠近大海的其他一些老公司,像鲍勃这样的房地产经纪人甚至可能不存在来管理房源。相反,客户可以咨询 Rolodex “Annie”(简称)以获取房屋的直接地址。客户没有像 Bob 那样从列表中读取引用的房屋详细信息,而是从 Annie () 那里获得房屋地址,然后直接前往 1234 Main St,有时不知道他们会在那里找到什么。&
&
有一天,鲍勃的公司开始提供一项新的自动化服务,需要为客户感兴趣的房屋提供房源。
好吧,拥有该信息的人是 Bob,因此客户让 Bob 调用服务并向其发送列表副本。
jobKillingAutomatedListingService(Listing tinyHouseAt1234Main, int viewTally)
鲍勃一起发...
该服务在其结束时将此列表称为 Listing ,但实际上它收到的是 Bob 列表的精确副本,其中包含完全相同的值,指的是位于主街 1234 号的房子。houseToLookAt
这项新服务也有自己的内部统计,即有多少人查看了该列表。出于专业礼貌,该服务接受了 Bob 的计数,但它并不真正关心它,而是用自己的本地副本完全覆盖它。今天的总数是 1,而 Bob 仍然是 43。
房地产公司称其为“按价值传递”,因为鲍勃传递了他和他的房源的当前价值。他实际上并没有传递整个实体房子,因为这是不切实际的。他也没有像 Annie() 那样传递真实的物理地址。viewTally
tinyHouseAt1234Main
&
但是他正在传递一份他对房子的参考价值的副本。在某些方面似乎是一个愚蠢的迂腐差异,但这就是他的公司的运作方式...... ..............
PT II:事情变得混乱和危险的地方......
新的自动化服务,不像其他一些时髦的金融和科学公司那样全是功能和数学导向的,可能会产生不可预见的副作用......
一旦给定了一个 Listing 对象,它允许客户使用远程无人机机器人车队实际重新粉刷位于 1234 Main St 的真实房屋!它允许客户控制机器人推土机实际挖掘花坛!这太疯狂了!!
该服务还允许客户完全重定向到另一个地址的其他房子,而不涉及鲍勃或他的房源。突然之间,他们可能会看到榆树街 4321 号,这与鲍勃的房源没有任何关系(谢天谢地,他们不能再造成损害了)。houseToLookAt
鲍勃在他的实时网络摄像头上观看了这一切。
他辞去了他唯一的工作职责的苦差事,他告诉客户新的丑陋的油漆工作和突然缺乏遏制吸引力。毕竟,他的房源仍然是主街 1234 号。新服务无法改变这一点。鲍勃一如既往地准确而尽职尽责地报告他的细节,直到他被解雇或房子被 The Nothing 完全摧毁。houseToLookAt
tinyHouseAt1234Main
实际上,该服务唯一不能对 Bob 原始列表的副本执行的就是将地址从 1234 Main St. 更改为其他地址,或者更改为虚无的空洞,或者更改为某种随机类型的物体,例如鸭嘴兽。鲍勃的房源仍然总是指向主街 1234 号,无论它仍然值多少钱。他一如既往地传递其当前值。houseToLookAt
将列表传递给新的自动化服务这种奇怪的副作用让那些询问其工作原理的人感到困惑。真的,远程控制改变 1234 Main 房屋状态的机器人的能力与实际去那里并造成严重破坏的能力有什么区别,因为安妮给了你地址??
这似乎是一个吹毛求疵的语义论点,如果你通常关心的是列表中的房子的状态被复制和传递,对吧?
我的意思是,如果你从事的是实际捡起房屋并将它们移动到其他地址的业务(不像移动或微型房屋,这是平台的预期功能),或者你正在访问、重命名和洗牌整个社区,就像某种低级的玩神的疯子一样,那么也许你会更关心传递那些特定的地址引用,而不仅仅是房子详情...
与其他一些语言不同,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 仅按值传递。没有引用传递,例如,您可以看到以下示例。
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
正如迈克尔在评论中所说:
即使对对象的操作行为类似于按引用传递,对象仍按值传递。假设调用方对 person 对象的引用将保持不变。对象本身是按值传递的,但其成员可能会受到更改的影响。为了成为真正的按引用传递,我们必须能够将参数重新分配给新对象,并将更改反映在调用者中。
void changePerson(Person person){ person = new Person(); }
评论
Java 通过值传递基元类型,通过引用传递类类型
现在,人们喜欢无休止地争论“通过引用传递”是否是描述Java等人实际所做的事情的正确方式。关键是这样的:
- 传递对象不会复制该对象。
- 传递给函数的对象可以由函数修改其成员。
- 传递给函数的基元值不能由函数修改。制作副本。
在我的书中,这叫做通过引用传递。
评论
已经有很好的答案涵盖了这一点。我想通过分享一个非常简单的示例(将编译)来对比 c++ 中的 Pass-by-reference 和 Java 中的 Pass-by-value 之间的行为,从而做出一点贡献。
几点:
- 术语“参考”是具有两个不同含义的重载。在 Java 中,它只是表示指针,但在“按引用传递”的上下文中,它表示传入的原始变量的句柄。
- Java 是按值传递的。Java 是 C(以及其他语言)的后代。在 C 之前,一些(但不是全部)早期语言(如 FORTRAN 和 COBOL)支持 PBR,但 C 不支持。 PBR 允许这些其他语言对子例程中传递的变量进行更改。为了完成同样的事情(即更改函数内部变量的值),C 程序员将指向变量的指针传递到函数中。受 C 启发的语言,如 Java,借鉴了这个想法,并继续像 C 一样将指针传递给方法,只是 Java 将其指针称为 References。同样,这是“Reference”一词与“Pass-By-Reference”中的用法不同。
- C++ 通过使用“&”字符声明引用参数(恰好是 C 和 C++ 中用于指示“变量地址”的相同字符)来允许按引用传递。例如,如果我们通过引用传入指针,则参数和参数不仅仅是指向同一个对象。相反,它们是相同的变量。如果一个被设置为不同的地址或 null,另一个也会被设置为 null。
- 在下面的 C++ 示例中,我通过引用传递指向以 null 结尾的字符串的指针。在下面的 Java 示例中,我按值传递对 String 的 Java 引用(同样,与指向 String 的指针相同)。请注意注释中的输出。
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 引用”by value 示例
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 中发布了相同的示例,因为我认为 pass-by-reference 在 pascal 中看起来更干净,但我可能是错的。我可能只是让人们更加困惑;我希望不是。
在 pascal 中,通过引用传递的参数称为“var 参数”。在下面的 setToNil 过程中,请注意参数 'ptr' 前面的关键字 'var'。当指针传递到此过程时,它将通过引用传递。注意以下行为:当此过程将 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 的制作者无意中传播了错误信息。如果他们没有使用“引用”一词来表示指针,而是使用了其他东西,比如说 丁格贝里,不会有问题。你可以说,“Java 通过价值而不是引用传递 dingleberries”,没有人会感到困惑。
这就是只有 Java 开发人员对此有疑问的原因。他们看着“参考”这个词,认为他们确切地知道这意味着什么,所以他们甚至懒得考虑相反的论点。
无论如何,我在一篇较早的帖子中注意到了一条评论,它做了一个我非常喜欢的气球类比。如此之多,以至于我决定将一些剪贴画粘在一起,制作一组卡通来说明这一点。
按值传递引用 - 对引用的更改不会反映在调用方的作用域中,但对对象的更改会反映在调用方的作用域中。这是因为引用是复制的,但原始引用和副本都引用同一个对象。
按引用传递 -- 没有引用的副本。单个引用由调用方和被调用的函数共享。对引用或对象数据的任何更改都会反映在调用方的作用域中。
编辑 4
我看过关于这个主题的帖子,描述了 Java 中参数传递的低级实现,我认为这很棒并且非常有帮助,因为它使抽象的想法具体化。然而,对我来说,问题更多的是关于语言规范中描述的行为,而不是关于行为的技术实现。这是 Java 语言规范第 8.4.1 节的摘录:
调用方法或构造函数 (§15.12) 时, 实际参数表达式初始化新创建的参数 变量,每个声明的类型,在执行 方法或构造函数。出现在 DeclaratorId 可以用作方法主体中的简单名称,或者 构造函数来引用形式参数。
这意味着,java 在执行方法之前会创建传递参数的副本。像大多数在大学里学习编译器的人一样,我使用了“The Dragon Book”,这是编译器的书。在第 1 章中,它对“按值调用”和“按引用调用”进行了很好的描述。按值调用的描述与 Java 规范完全匹配。
早在 90 年代我学习编译器时,我就使用了 1986 年的第一版,它比 Java 早了大约 9 或 10 年。然而,我刚刚看到了 2007 年第 2 版的副本,其中实际上提到了 Java!第 1.6.6 节标记为“参数传递机制”,很好地描述了参数传递。以下是标题“Call-by-value”下的摘录,其中提到了 Java:
在按值调用中,将计算实际参数(如果它是 expression)或复制(如果它是一个变量)。该值位于 属于 称为过程。此方法在 C 和 Java 中使用,是一种常见的方法 C++ 以及大多数其他语言中的选项。
评论
长话短说:
- 非原语:Java 传递 Reference 的 Value。
- 基元:只是值。
结束。
(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 按值传递参数,在 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;
}
}
示例代码演示了更改对不同函数的对象的影响。
首先,让我们了解一下 Java 中的内存分配: 堆栈和堆是 JVM 为不同目的分配的内存的一部分。堆栈内存在创建时预先分配给线程,因此,一个线程无法访问其他线程的堆栈。但是堆可用于程序中的所有线程。
对于线程,Stack 存储所有本地数据、程序元数据、基元类型数据和对象引用。并且,堆负责存储实际对象。
Book book = new Book("Effective Java");
在上面的示例中,引用变量是存储在堆栈中的“book”。new 运算符 -> new Book(“Effective Java”) 创建的实例存储在堆中。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
int num =10;->这将为正在运行的线程的 Stack 中的 “int” 分配内存,因为它是一个原始类型。现在,当调用 printNum(..) 时,将在同一线程中创建私有堆栈。将“num”传递给此方法时,将在方法堆栈帧中创建“num”的副本。 num = num+10;-> 这将增加 10 并修改方法堆栈帧中的 int 变量。 因此,方法堆栈帧外的原始 num 保持不变。
考虑将自定义类的对象作为参数传递的示例。
在上面的例子中,ref 变量 “book” 驻留在执行程序的线程堆栈中,当程序执行 new Book() 时,会在堆空间中创建类 Book 的对象。堆中的这个内存位置由“book”引用。当“book”作为方法参数传递时,“book”的副本将在同一线程堆栈内的方法的专用堆栈框架中创建。因此,复制的引用变量指向堆中类“Book”的同一对象。
方法堆栈帧中的引用变量为同一对象设置新值。因此,当原始 ref 变量 “book” 获得其值时,它会反映出来。 请注意,在传递引用变量的情况下,如果在调用的方法中再次初始化它,则它指向新的内存位置,并且任何操作都不会影响堆中的前一个对象。
因此,当任何内容作为方法参数传递时,它始终是 Stack 实体 - 基元或引用变量。我们从不传递存储在堆中的东西。因此,在 Java 中,我们总是在堆栈中传递值,并且是按值传递的。
我认为这个简单的解释可能会帮助你理解,因为当我为此苦苦挣扎时,我想理解同样的事情。
当您将原始数据传递给函数调用时,它的内容将复制到函数的参数中,而当您传递对象时,其引用将复制到函数的参数中。说到对象,你不能在调用函数中更改复制的引用 - 参数变量引用。
考虑这个简单的例子,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/Object 数组时,问题出在数组上。让我们看看。
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 不会通过引用传递方法参数;它按值传递它们。
以 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 按值(而不是对象)复制和传递引用。因此,方法操作将更改对象,因为引用指向原始对象。但是,由于引用是副本,因此交换将失败。该方法引用交换,但不引用原始引用。不幸的是,在方法调用之后,您只剩下未交换的原始引用。为了在方法调用之外成功进行交换,我们需要交换原始引用,而不是副本。
我看到所有答案都包含相同的内容:按值传递。然而,最近 Brian Goetz 对 Valhalla 项目的更新实际上以不同的方式回答了它:
事实上,关于 Java 对象是按值传递还是按引用传递,这是一个常见的“陷阱”问题,答案是“两者都不是”:对象引用是按值传递的。
您可以在此处阅读更多内容:瓦尔哈拉州。第 2 部分:语言模型
编辑:Brian Goetz 是 Java 语言架构师,领导着 Project Valhalla 和 Project Amber 等项目。
Edit-2020-12-08:更新了瓦尔哈拉州
评论
我会用另一种方式说:
在 java 中,引用是传递的(但不是对象),这些引用是按值传递的(引用本身被复制,结果你有 2 个引用,并且你无法控制方法中的第一个引用)。
只是说:对于初学者来说,按值传递可能不够清楚。例如,在 Python 中也有同样的情况,但有些文章描述了他们称之为 ,仅使用原因引用。pass-by-reference
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 按值将参数传递给方法,如果变量指向对象引用,则实际对象也会发生变化。
评论
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,如下所示:
第三幅插图
希望这会有所帮助。
评论
不再重复,但有一点给那些在阅读了许多答案后可能仍然感到困惑的人:
pass by value
在 Java 中不等于在 C++ 中,尽管它听起来像这样,这可能就是为什么会造成混淆的原因pass by value
分解它:
pass by value
在 C++ 中意味着传递对象的值(如果对象),从字面上看是对象的副本pass by value
在 Java 中意味着传递对象的地址值(如果对象),而不是像 C++ 那样真正传递对象的“值”(副本)- 在 Java 中,对函数内部的对象(例如)进行操作会对函数外部的对象产生影响;在 C++ 中,它对外部没有影响。
pass by value
myObj.setName("new")
pass by value
- 但是,在 C++ 中,对函数中的对象进行操作确实对外部对象有影响!与 Java 相似(只是相似,不相同),不?..人们总是重复“Java 中没有引用传递”,=> BOOM,混乱开始了......
pass by reference
pass by value
所以,朋友们,一切都只是术语定义的差异(跨语言),你只需要知道它是如何工作的,仅此而已(尽管有时我承认它的称呼有点令人困惑)!
评论
foo(bar)
bar
bar
如果你想把它用一句话来理解和记忆,最简单的答案:
Java 总是使用新的引用传递值
(因此可以修改原始对象,但不能访问原始引用)
评论
这里有一个更精确的定义:
- 按值传递/调用:形式参数类似于 功能范围,它评估到当前的实际参数 函数调用。
- 通过引用/调用:形式参数只是 real 的别名 值,它在功能范围内的任何变化都可以有侧面 在代码的任何其他部分之外产生影响。
因此,在 C/C++ 中,您可以创建一个函数来交换使用引用传递的两个值:
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
你可以看到它对 a 和 b 有一个唯一的引用,所以我们没有副本,tmp 只是保存唯一的引用。
java中的相同函数没有副作用,参数传递就像上面的代码一样,没有引用。
尽管 java 使用指针/引用,但参数不是唯一的指针,在每个属性中,它们都会被复制,而只是像 C/C++ 一样分配
只有两个版本:
- 您可以传递值,即 (4,5)
- 您可以传递一个地址,即0xF43A
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 中如何不可能做到。无论出于何种原因,没有人试图展示如何从其他语言实现按值传递对象。
此代码演示如何完成以下操作:
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 没有按引用传递的语义,但将该语言称为“按值传递”是一厢情愿的想法。
评论
clone()
为了简单和详细其: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
}
评论
Java 使用按值传递,但无论使用原始类型还是引用类型,效果都不同。
当您将基元类型作为参数传递给方法时,它将获得基元的副本,并且方法块内的任何更改都不会更改原始变量。
当您将引用类型作为参数传递给方法时,它仍然会获得一个副本,但它是对对象的引用的副本(换句话说,您正在获取对象所在堆中内存地址的副本),因此方法块内对象的任何更改都会影响块外的原始对象。
猜猜看,基于不准确的语言,普通的经典是错误的
编程语言的作者无权重命名已建立的编程概念。
原始 Java 类型肯定是按值传递的。byte, char, short, int, long float, double
所有其他类型都是:从技术上讲,对象成员和参数都是引用。Objects
因此,这些“引用”是“按值”传递的,但在堆栈上没有发生对象构造。对象成员(或数组中的元素)的任何更改都适用于同一原始对象;这种引用正好满足了在任何 C 方言中传递给某个函数的实例的指针的逻辑,我们过去常常通过引用来调用这个传递对象
特别是我们确实有这个东西 java.lang.NullPointerException,这在纯粹的按值概念中是没有意义的
评论
这个问题已经有很多答案,其中许多都具有误导性。
有两个简单的语句可以解释 Java 在所有情况下的行为方式:
- Java 是一种按值传递的语言
- 基本类型是直接值,Java 对象是间接值
这是什么意思?
- 如果你有一个基本类型,比如 ,你实际上持有一个值。当变量初始化为 int 值时,直接“包含”该值。如果将其传递给函数,则会在包含函数参数的变量中复制该值。此变量称为“参数”。因此,Java 是一种按值传递的语言。
int
- 初始化为 Java Object 的变量(回想一下该语言中继承自 )的所有其他内容,包含对包含 Objects 数据的数据结构的间接引用。变量是一个标签,它不是直接保存值,而是包含一个用于引用或查找对象数据的标签。
Object
注意:我没有在上面描述实现细节。实现细节可以是任何内容。我只是在描述语言的行为。
Some further explanation as to why I think other answers are confusing or misleading.
- Some answers mention the word "pointers". This means the answer is fundamentally wrong because Java was deliberatly designed to not have pointer semantics. There are no pointers in Java, they fundamentally do not exist, hence there is no concept of them.
Therefore to even mention pointers is wrong. The implementation details of the JVM might use data which is akin to virtual memory addresses, but this is totally hidden from the user of the Java language. That is the point of programming languages, to abstract the programmer away from hardware details. With languages like C and C++, pointers are a key focus of the language because it was designed for systems engineering where detailed control over memory is required.
评论
"Functional" Definitions:
- pas-by-value - reassigning of a method argument is not visible to the caller
- pass-by-reference - reassigning of a method argument is visible to the caller
Java is always pass-by-value.
https://www.linkedin.com/pulse/java-pass-by-value-pass-by-reference-fuad-efendi/
P.S. We just need to improve slightly our intuitive "definitions" of "address of an object" vs. "an object" and focus more on functionality. I hope provided definitions are applicable not only to Java.
上一个:在 Go 函数中按引用和值传递
下一个:如何在 Java 中设置参数?
评论