string是值传递还是引用传递 Java是“传递引用”还是“按值传递”?
java赋值 (20)
我一直认为Java是传递引用的 。
但是,我看过一些博客文章(例如, 这个博客 )声称它不是。
我不认为我理解他们所做的区别。
解释是什么?
https://code.i-harness.com
无论您使用何种语言,引用始终是表示的值。
获取框外视图,让我们看看Assembly或一些低级内存管理。在CPU级别,如果将任何内容写入内存或其中一个CPU寄存器,则对任何内容的引用会立即变为值。(这就是为什么指针是一个很好的定义。它是一个值,它同时具有目的)。
内存中的数据有一个位置,在该位置有一个值(字节,字,等等)。在Assembly中,我们有一个方便的解决方案,可以为某个位置(也就是变量)提供一个名称,但是在编译代码时,汇编程序只是将Name替换为指定位置,就像浏览器用IP地址替换域名一样。
在核心的情况下,技术上不可能在不表示任何语言的情况下将引用传递给任何语言(当它立即变为值时)。
比方说,我们有一个变量富,它的位置是在内存中的第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推到堆栈。
- 223被复制到其中一个CPU寄存器。
- 223获取PUSHd到堆栈。
在每个情况下,都创建了一个值 - 现有值的副本 - 现在由接收方法来处理它。当你在方法中写入“Foo”时,它要么从EAX中读出,要么自动解除引用,或者双重解引用,这个过程取决于语言的工作方式和/或Foo的类型。这是开发人员隐藏的,直到她绕过解除引用过程。因此,引用是表示的值,因为引用是必须处理的值(在语言级别)。
现在我们已经将Foo传递给了方法:
- 在情况1.和2.如果您更改Foo(
Foo = 9
)它只影响本地范围,因为您有一个值的副本。从方法内部我们甚至无法确定原始Foo所在的内存位置。 - 如果您使用默认语言结构并更改Foo(
Foo = 11
),它可以全局更改Foo(取决于语言,即Java或类似Pascal的procedure findMin(x, y, z: integer;
var m: integer);
)。但是,如果该语言允许您绕过取消引用过程,您可以更改47
,比方说49
。在那一点上,如果你读它,Foo似乎已被改变了,因为你已经改变了它的本地指针。如果你要在方法(Foo = 12
)中修改这个Foo,你可能会搞砸程序的执行(又名.segfault),因为你会写一个不同于预期的内存,你甚至可以修改一个注定要保存可执行文件的区域程序和写入它将修改运行代码(Foo现在不在47
)。但是Foo的价值47
没有全局更改,只有方法内部的一个,因为47
也是方法的副本。 - 在第5和第6例中。如果你
223
在方法内修改它会产生与3.或4中相同的混乱。(一个指针指向一个现在错误的值,它再次用作指针)但这仍然是一个本地问题,因为223被复制了。但是,如果您能够取消引用Ref2Foo
(即223
),达到并修改指向的值47
,比如说,49
它将全局影响Foo ,因为在这种情况下,方法得到了一个副本,223
但引用的47
只存在一次,并且更改了对49
每一个会导致Ref2Foo
双解引用到一个错误的值。
对无关紧要的细节进行挑剔,即使是通过引用传递的语言也会将值传递给函数,但这些函数知道它们必须将它用于解除引用目的。这个传递参考值只是程序员隐藏的,因为它实际上是无用的,术语只是通过引用传递。
严格的pass-by-value也是无用的,这意味着每次调用一个以数组为参数的方法时都必须复制一个100 MB的数组,因此Java不能严格按值传递。每种语言都会传递对这个巨大数组的引用(作为一个值),并且如果该数组可以在方法内部进行本地更改,或者允许该方法(如Java那样)全局修改数组,则采用写时复制机制(来自调用者的视图)和一些语言允许修改引用本身的值。
因此,简而言之,在Java自己的术语中,Java是值传递,其中值可以是:实数值或作为引用表示的值。
Java始终是按值传递的 。 不幸的是,他们决定将对象的位置称为“引用”。 当我们传递一个对象的值时,我们将引用传递给它。 这对初学者来说很困惑。
它是这样的:
public static void main(String[] args) {
Dog aDog = new Dog("Max");
// we pass the object to foo
foo(aDog);
// aDog variable is still pointing to the "Max" dog when foo(...) returns
aDog.getName().equals("Max"); // true
aDog.getName().equals("Fifi"); // false
}
public static void foo(Dog d) {
d.getName().equals("Max"); // true
// change d inside of foo() to point to a new Dog instance "Fifi"
d = new Dog("Fifi");
d.getName().equals("Fifi"); // true
}
在上面的示例中, aDog.getName()
仍将返回"Max"
。 使用Dog
"Fifi"
在函数foo
不更改main
的值aDog
,因为对象引用按值传递。 如果它是通过引用传递的,那么main
的aDog.getName()
将在调用foo
后返回"Fifi"
。
同样:
public static void main(String[] args) {
Dog aDog = new Dog("Max");
foo(aDog);
// when foo(...) returns, the name of the dog has been changed to "Fifi"
aDog.getName().equals("Fifi"); // true
}
public static void foo(Dog d) {
d.getName().equals("Max"); // true
// this changes the name of d to be "Fifi"
d.setName("Fifi");
}
在上面的例子中, Fifi
是调用foo(aDog)
之后的狗的名字,因为对象的名字是在foo(...)
。 foo
在d
上执行的任何操作都是这样的,出于所有实际目的,它们是在aDog
本身上执行的(除非d
被更改为指向不同的Dog
实例,例如d = new Dog("Boxer")
)。
Java总是按值而不是通过引用传递参数。
让我通过一个example解释一下:
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
的Foo
类型的引用,并将其分配给类型为Foo
的新对象,其属性为"f"
。Foo f = new Foo("f");
从方法方面,声明了具有名称
a
的类型为Foo
的引用,并且它最初被赋值为null
。public static void changeReference(Foo a)
当您调用方法
changeReference
,引用a
将被分配给作为参数传递的对象。changeReference(f);
声明名为
b
的类型为Foo
的引用,并将其分配给类型为Foo
的新对象,其属性为"b"
。Foo b = new Foo("b");
a = b
将引用a
NOTf
重新分配给其属性为"b"
的对象。当您调用
modifyReference(Foo c)
方法时,将创建引用c
并将其分配给具有属性"f"
的对象。c.setAttribute("c");
将更改引用c
指向它的对象的属性,并且它是引用f
指向它的同一对象。
我希望你现在明白如何将对象作为参数传递在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
,如下所示:
希望这会有所帮助。
在Java中,只传递引用并按值传递:
Java参数都是按值传递的(当方法使用时会复制引用):
对于基本类型,Java行为很简单:将值复制到基本类型的另一个实例中。
在Objects的情况下,这是相同的:对象变量是仅包含使用“new”关键字创建的Object 地址的指针(桶),并且像原始类型一样被复制。
行为可能与原始类型不同:因为复制的对象变量包含相同的地址(对于同一个对象)对象的内容/成员可能仍然在方法中被修改并且稍后访问外部,给出了(包含)对象的错觉本身是通过引用传递的。
“字符串”对象似乎是城市传说中“对象通过引用传递” 的完美反例:
实际上,在一个永远不可能的方法中,更新作为参数传递的String的值:
String对象,由声明为final的数组保存,不能修改。只有Object的地址可能被另一个使用“new”替换。使用“new”更新变量,不会让Object从外部访问,因为变量最初是按值传递并复制的。
Java是按值调用的。
这个怎么运作
您总是传递参考值的位副本!
如果它是原始数据类型,则这些位包含原始数据类型本身的值,这就是为什么如果我们更改方法内的header的值,那么它不会反映外部的更改。
如果它是一个像Foo foo = new Foo()这样的对象数据类型,那么在这种情况下,对象地址的副本会像文件快捷方式一样传递,假设我们在C:\ desktop中有一个文本文件abc.txt,并假设我们设置了快捷方式相同的文件,并把它放在C:\ desktop \ abc-shortcut中,所以当你从C:\ desktop \ abc.txt访问文件并写入''并关闭文件时再次从快捷方式打开文件然后你write '是程序员学习的最大的在线社区'然后总文件更改将是'是程序员学习的最大的在线社区'这意味着从打开文件的位置开始无关紧要,每次我们访问同一个文件时,我们都可以假设Foo为文件,并假设foo存储在123hd7h(原始地址如C:\ desktop \ abc.txt)地址和234jdid(复制地址如C:\ desktop \ abc-shortcut,其中实际上包含了文件的原始地址)。所以为了更好地理解制作快捷方式文件和感觉...
一些帖子的一些更正。
C不支持通过引用传递。它总是通过价值。C ++确实支持按引用传递,但不是默认值,而且非常危险。
Java中的值是什么并不重要:对象的原始或地址(粗略),它总是按值传递。
如果Java对象“行为”就像通过引用传递一样,那就是可变性的属性,并且与传递机制完全无关。
我不确定为什么会这么混乱,也许是因为很多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只有两种类型的传递:内置类型的值,以及对象类型的指针值。
基本上,重新分配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
。
我想我会提供这个答案,以便从规格中添加更多细节。
通过引用传递意味着被调用函数的参数将与调用者的传递参数相同(不是值,而是标识 - 变量本身)。
按值传递意味着被调用函数的参数将是调用者传递的参数的副本。
或者来自维基百科,关于传递参考的主题
在逐个引用的评估(也称为pass-by-reference)中,函数接收对用作参数的变量的隐式引用,而不是其值的副本。这通常意味着函数可以修改(即赋值)用作参数的变量 - 其调用者将看到的内容。
在call-by-value中,计算参数表达式,并将结果值绑定到函数[...]中的相应变量。如果函数或过程能够为其参数赋值,则仅指定其本地副本[...]。
其次,我们需要知道Java在其方法调用中使用了什么。在Java语言规范状态
当调用方法或构造函数(第15.12节)时,实际参数表达式的值在执行方法体或构造函数体之前初始化新创建的参数变量,每个声明的类型。
因此它将参数的值赋予(或绑定)到相应的参数变量。
论证的价值是什么?
让我们考虑引用类型,在Java虚拟机规范状态
有三种引用类型:类类型,数组类型和接口类型。它们的值分别是对动态创建的类实例,数组或类实例或实现接口的数组的引用。
在Java语言规范还规定
引用值(通常只是引用)是指向这些对象的指针,以及一个特殊的空引用,它指的是没有对象。
参数(某种引用类型)的值是指向对象的指针。请注意,变量,具有引用类型返回类型的方法的调用以及实例创建表达式(new ...
)都将解析为引用类型值。
所以
public void method (String param) {}
...
String var = new String("ref");
method(var);
method(var.toString());
method(new String("ref"));
all将String
实例引用的值绑定到方法新创建的参数中param
。这正是pass-by-value的定义所描述的。因此,Java是按值传递的。
您可以按照引用来调用方法或访问引用对象的字段这一事实与对话完全无关。传递参考的定义是
这通常意味着函数可以修改(即赋值)用作参数的变量 - 其调用者将看到的内容。
在Java中,修改变量意味着重新分配它。在Java中,如果您在方法中重新分配了变量,那么调用者就不会注意到它。修改变量引用的对象完全是一个不同的概念。
here还在Java虚拟机规范中定义了原始值。该类型的值是相应的积分或浮点值,适当编码(8,16,32,64等位)。
我觉得争论“传递引用与传递价值”并不是非常有用。
如果你说“Java是任意的(参考/值)”,在任何一种情况下,你都没有提供完整的答案。 这里有一些额外的信息,有助于理解记忆中发生的事情。
在我们进入Java实现之前,堆栈/堆上的崩溃过程:值以一种非常有序的方式在堆栈中上下移动,就像在自助餐厅的堆栈一样。 堆中的内存(也称为动态内存)是杂乱无章的。 JVM只是在任何地方找到空间,并释放它,因为不再需要使用它的变量。
好的。 首先,本地原语进入堆栈。 所以这段代码:
int x = 3;
float y = 101.1f;
boolean amIAwesome = true;
结果如下:
声明和实例化对象时。 实际的对象在堆上。 什么在堆栈上? 堆上对象的地址。 C ++程序员会将此称为指针,但是一些Java开发人员反对“指针”这个词。 随你。 只要知道对象的地址就在堆栈上。
像这样:
int problems = 99;
String name = "Jay-Z";
数组是一个对象,所以它也在堆上。 那阵列中的对象怎么样? 它们获得自己的堆空间,每个对象的地址都在数组内部。
JButton[] marxBros = new JButton[3];
marxBros[0] = new JButton("Groucho");
marxBros[1] = new JButton("Zeppo");
marxBros[2] = new JButton("Harpo");
那么,当你调用一个方法时会传入什么? 如果传入一个对象,那么实际传入的是对象的地址。 有些人可能会说地址的“价值”,有些人说它只是对象的引用。 这是“参考”和“价值”支持者之间圣战的起源。 你所说的并不像你理解的那样重要,传入的是对象的地址。
private static void shout(String name){
System.out.println("There goes " + name + "!");
}
public static void main(String[] args){
String hisName = "John J. Jingleheimerschmitz";
String myName = hisName;
shout(myName);
}
创建一个String,并在堆中分配空间,并将字符串的地址存储在堆栈中并赋予标识符hisName
,因为第二个String的地址与第一个String的地址相同,因此不会创建新的String并且没有分配新的堆空间,但是在堆栈上创建了新的标识符。 然后我们调用shout()
:创建一个新的堆栈帧,并创建一个新的标识符, name
并分配已存在的String的地址。
那么,价值,参考? 你说“土豆”。
Java按VALUE传递参数,仅按值传递参数。
长话短说:
对于来自C#的人:没有“out”参数。
对于来自PASCAL的人:没有“var”参数。
这意味着您无法更改对象本身的引用,但您始终可以更改对象的属性。
解决方法是使用StringBuilder
参数String
。你总是可以使用数组!
区别,或者也许只是我记忆中的方式,因为我曾经与原始海报的印象相同:Java总是按价值传递。Java中的所有对象(在Java中,除了基元之外的任何东西)都是引用。这些引用按值传递。
您永远不能通过Java中的引用传递,并且一种明显的方法是当您想要从方法调用返回多个值时。考虑C ++中的以下代码:
void getValues(int& arg1, int& arg2) {
arg1 = 1;
arg2 = 2;
}
void caller() {
int x;
int y;
getValues(x, y);
cout << "Result: " << x << " " << y << endl;
}
有时你想在Java中使用相同的模式,但你不能; 至少不是直接的。相反,你可以做这样的事情:
void getValues(int[] arg1, int[] arg2) {
arg1[0] = 1;
arg2[0] = 2;
}
void caller() {
int[] x = new int[1];
int[] y = new int[1];
getValues(x, y);
System.out.println("Result: " + x[0] + " " + y[0]);
}
正如之前的答案中所解释的那样,在Java中,您将指向数组的指针作为值传入getValues
。这就足够了,因为该方法然后修改数组元素,按照惯例,您期望元素0包含返回值。显然,您可以通过其他方式执行此操作,例如构造代码以使其不必要,或者构造可以包含返回值或允许设置它的类。但是,在C ++中可用的简单模式在Java中不可用。
我一直认为它是“通过副本”。它是值的原始或引用的副本。如果它是原语,则它是作为值的位的副本,如果它是Object,则它是引用的副本。
public class PassByCopy{
public static void changeName(Dog d){
d.name = "Fido";
}
public static void main(String[] args){
Dog d = new Dog("Maxx");
System.out.println("name= "+ d.name);
changeName(d);
System.out.println("name= "+ d.name);
}
}
class Dog{
public String name;
public Dog(String s){
this.name = s;
}
}
java PassByCopy的输出:
name = Maxx
name = Fido
原始包装类和字符串是不可变的,因此使用这些类型的任何示例都不会与其他类型/对象相同。
我无法相信没人提到Barbara Liskov。当她在1974年设计CLU时,她遇到了同样的术语问题,并且通过共享(也称为通过对象共享调用和按对象调用)发明了这个特定情况的“通过值调用,其值为参考“。
据我所知,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,因为在交换函数中,您使用的copys对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创建一个包装器,这样就可以将它与字符串一起使用:
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对象具有一些非常特殊的属性。
一般来说,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 ++提供的细粒度控制(和复杂性)。