Java是按值传递还是按引用传递?

7757

我一直认为Java使用的是按引用传递。然而,我看了一篇博客文章,声称Java使用的是按值传递。我不认为我理解作者所做的区分。

这是什么意思?


7
我们更常用的说法是,一个“按引用传递”的变量可以被改变。这个术语出现在教科书中,因为语言理论家需要一种区分原始数据类型(int、bool、byte)和复杂结构对象(数组、流、类)的方法,也就是说,那些可能具有无限内存分配的对象。 - jlau
7
我想提醒您,在大多数情况下不需要考虑这个问题。我在编程中使用Java很多年,直到学习了C++之前,我完全不知道传递引用和传递值是什么意思。直到那时,我的直觉解决方案总是有效的,这就是为什么Java是最适合初学者的语言之一。因此,如果您目前担心您的函数需要引用还是值,请按原样传递它,您会没问题的。 - Tobias
73
Java通过值传递引用。 - The Student
24
简单来说,这种混淆是因为在Java中,所有非基本数据类型都是通过"引用"来处理/访问的。然而,传递始终是按值进行的。因此,对于所有非基本类型,引用是按其值传递的。所有基本类型也都是按值传递的。 - Ozair Kafray
6
我觉得这篇文章非常有帮助: https://www.baeldung.com/java-pass-by-value-or-pass-by-reference - Natasha Kurian
显示剩余16条评论
95个回答

12

不想重复,但对于那些在阅读了许多答案后仍可能感到困惑的人,有一个要点:

  • 在Java中,按值传递(pass by value)与C++中的按值传递不相等的,尽管它听起来像这样,这可能就是混淆的原因。

分解一下:

  • C++中的按值传递意味着传递对象的值(如果是对象),实际上是对象的副本。
  • Java中的按值传递意味着传递对象的地址值(如果是对象),而不是像C++中那样实际上是对象的“值”(即副本)。
  • 通过Java中的按值传递,在函数内操作对象(例如myObj.setName("new"))对函数外的对象产生影响;但是在C++中通过按值传递,对函数外部的对象没有影响。
  • 然而,在C++中通过按引用传递,在函数中操作对象影响函数外部的对象!类似于只是类似于,而不是相同于Java中的按值传递,是不是有点像?..而人们总是重复“Java中没有按引用传递”,=> 嘭,混淆开始了...
那么,朋友们,一切都只是关于术语定义的差异(跨语言),你只需要知道它是如何工作的,就行了(虽然有时候我承认称呼有点令人困惑)!

1
在这两种语言中,“按值传递”意味着如果它是一个对象,则传递对象的值。在任何情况下,它都不意味着传递值的地址。术语上没有区别。没有一种情况我们称之为按值传递,并且在被调用的函数中更改值会更改调用者中的值。如果您在Java中像这样调用函数foo(bar),则bar的值永远不会更改--如果bar是对对象的引用,则其值是所引用对象的标识,无论函数执行什么操作,它仍然引用调用者中的同一对象。 - David Schwartz
3
我同意David Schwartz的观点:术语上实际上没有什么区别。Java和C++之间的最大区别在于,在Java中,“值”永远不能是一个完整的对象。它始终是一个引用或原始值。你根本无法在Java中传递一个完整的对象。 - Joachim Sauer
“按值传递”在不同语言中并不意味着不同的含义。“按值传递”是一个事实,可以针对一种语言或一种方法等进行询问。 - undefined

11

Java是按值传递。

这个主题上已经有很好的回答了。不知何故,我对于基本数据类型和对象的传值/传引用从未很清楚。因此,为了满足自己的好奇和清晰度,我测试了以下代码; 希望能帮助到寻求类似清晰度的某些人:

class Test    {

public static void main (String[] args) throws java.lang.Exception
{
    // Primitive type
    System.out.println("Primitve:");
    int a = 5;
    primitiveFunc(a);
    System.out.println("Three: " + a);    //5

    //Object
    System.out.println("Object:");
    DummyObject dummyObject = new DummyObject();
    System.out.println("One: " + dummyObject.getObj());    //555
    objectFunc(dummyObject);
    System.out.println("Four: " + dummyObject.getObj());    //666 (555 if line in method uncommented.)

}

private static void primitiveFunc(int b)    {
    System.out.println("One: " + b);    //5
    b = 10;
    System.out.println("Two:" + b);    //10
}

private static void objectFunc(DummyObject b)   {
    System.out.println("Two: " + b.getObj());    //555
    //b = new DummyObject();
    b.setObj(666);
    System.out.println("Three:" + b.getObj());    //666
}

}

class DummyObject   {
    private int obj = 555;
    public int getObj() { return obj; }
    public void setObj(int num) { obj = num; }
}
如果取消注释b = new DummyObject()这一行代码,随后所做的修改会在一个新的实例上进行,也就是说会创建一个新的对象。因此,这些修改不会反映在调用该方法的位置上。否则,由于修改只作用于对象的“引用”,即b指向相同的dummyObject,所以修改将被反映出来。
可以通过本主题下某个答案中的图示(https://dev59.com/EXVD5IYBdhLWcg3wQJOT#12429953)更深入地理解该问题。

虽然,Java是一种编程语言 ;) - Sam Ginrich

11

我做了一个小图示,展示了数据是如何被创建和传递的

数据创建和传递图示

注意:原始值作为值进行传递,对该值的第一个引用是方法的参数。

这意味着:

  • 您可以在函数内部更改myObject的值
  • 但您不能在函数内部更改myObject指向的内容,因为point不是myObject
  • 请记住,pointmyObject都是引用,不同的引用,但是这些引用指向相同的new Point(0,0)

11

这有点难理解,但Java始终会复制该值-重点是,通常该值是一个引用。因此,您最终会不假思索地得到相同的对象...


11

这个问题的许多混淆来自于Java试图重新定义“按值传递”和“按引用传递”的含义。重要的是要理解这些是行业术语,在这种背景下才能正确理解。它们旨在帮助您编写代码并且了解它们非常有价值,因此让我们首先了解一下它们的含义。

可以在这里找到对两者的很好描述。

按值传递 函数接收的值是调用者正在使用的对象的副本。它完全独特于函数,并且您对该对象所做的任何操作仅在函数内部可见。

按引用传递 函数接收的值是调用者正在使用的对象的引用。该值所指向的对象被函数修改的任何内容都将被调用者看到,并且从那时起它将使用这些更改。

从这些定义中可以清楚地看出,引用是按值传递的事实是无关紧要的。如果我们接受这个定义,那么这些术语变得毫无意义,而所有语言都只是按值传递。

无论你如何传递引用,它都只能按值传递。这不是重点。重点是你将自己对象的引用传递给了函数,而不是它的副本。你可以丢弃接收到的引用这一事实是不相关的。如果我们接受了这个定义,那么这些术语就变得毫无意义,每个人都总是按值传递。
不,C++特殊的“按引用传递”语法并不是按引用传递的独家定义。这纯粹是一种方便的语法,旨在使您在传递指针后不需要使用指针语法。它仍然是传递指针,编译器只是将这个事实隐藏起来。它也仍然通过值传递该指针,编译器只是将其隐藏起来。
因此,有了这个理解,我们可以看看Java,发现它实际上具有两者。所有Java原始类型始终按值传递,因为你会收到调用者对象的副本,并且不能修改他们的副本。所有Java引用类型始终按引用传递,因为你会收到调用者对象的引用,并且可以直接修改他们的对象。
你无法修改调用者的引用与按引用传递无关,在支持按引用传递的所有语言中都是如此。

2
Java没有重新定义这些术语。也没有其他人这样做。它只是避免了C语言中的“指针”一词。 - user207421
8
那些术语在Java或C之前就已存在。指针只是实现它们之一的方法。如果您接受Java对它们的定义,那么它们变得毫无意义,因为按照该定义,每种编程语言都只是传值调用。 - Cdaragorn
user207421 当然讨论过术语在 Java 存在之前 50 年,我们从未听说过像“按值传递指针”这样愚蠢的东西,即使整个 Java 社区认为它是术语的黄金标准。 - Sam Ginrich

11
为了更加详细地说明这个问题,我认为应该包括 SCJP Study Guide 上的内容。该指南是为通过 Sun/Oracle Java 行为测试而制作的,因此对于本讨论来说是一个很好的参考来源。
传递变量到方法中(目标 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对象。注意当在第4行创建的Dimension对象被传递给modify()方法时,方法内所做的任何更改都是针对传递的引用变量指向的对象进行的。在前面的示例中,引用变量d和dim都指向同一个对象。Java是否使用按值传递语义?如果Java通过传递引用变量来传递对象,那么这是否意味着Java对于对象使用按引用传递?不完全是,尽管您经常会听到和阅读到这样的说法。在单个VM中,Java实际上对所有变量使用按值传递。按值传递意味着传递变量值。这意味着,传递变量的副本!(又出现了那个复制的词!)传递基元或引用变量没有任何区别,您始终传递变量中表示值的位的副本。例如,如果您传递一个值为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引用的对象的值,但无法更改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 没有发生改变。请记住,传递给方法的是 a 的副本。当原始变量被传递到方法中时,它是按值传递的,这意味着传递变量中的位的副本。

10

所有东西都是按值传递的,包括原始类型和对象引用。但是如果它们的接口允许,对象可以被修改。

当您将对象传递给一个方法时,实际上传递的是该对象的引用,而方法实现可能会修改该对象。

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"是引用(指向对象的指针),无法更改。

基本类型按值传递。对象的引用也可以被视为基本类型。

总之,所有东西都是按值传递的。


9

Java始终采用按值传递,参数是传递变量的副本,所有对象都是使用引用定义的,引用是存储对象在内存中位置的变量。

查看注释以了解执行过程;按照数字显示执行流程。

class Example
{
    public static void test (Cat ref)
    {
        // 3 - <ref> is a copy of the reference <a>
        // both currently reference Grumpy
        System.out.println(ref.getName());

        // 4 - now <ref> references a new <Cat> object named "Nyan"
        ref = new Cat("Nyan");

        // 5 - this should print "Nyan"
        System.out.println( ref.getName() );
    }

    public static void main (String [] args)
    {
        // 1 - a is a <Cat> reference that references a Cat object in memory with name "Grumpy"
        Cat a = new Cat("Grumpy");

        // 2 - call to function test which takes a <Cat> reference
        test (a);

        // 6 - function call ends, and <ref> life-time ends
        // "Nyan" object has no references and the Garbage
        // Collector will remove it from memory when invoked

        // 7 - this should print "Grumpy"
        System.out.println(a.getName());
    }
}

“Pass its inner value” 是没有意义的。 - user207421
@EJP感谢您的留言,抱歉我在2013年的糟糕英语,我已经全部编辑过了,如果您看到更好的措辞,您可以建议或编辑。 - Khaled.K
我想补充一件事情。如果你改变了猫的名字而不是创建一个新的,即使方法返回后,它也会反映在内存中。 - user6091735

9

第1部分:关于房地产清单

目前有一栋蓝色的120平方英尺的“微型房屋”,停在1234 Main St,前面有一个修剪整齐的草坪和花坛。

当地一家房地产公司雇了一位经纪人,并告诉他要为那栋房子保留清单。

我们称呼那位经纪人为“鲍勃”。你好,鲍勃。

鲍勃通过网络摄像头更新他的清单,称之为tinyHouseAt1234Main,以实时记录实际房屋的任何变化。他还记录了询问该清单的人数。
今天鲍勃的整数viewTally是42。

每当有人想了解1234 Main St的蓝色微型房屋的信息时,他们会向鲍勃询问。

鲍勃查找他的清单tinyHouseAt1234Main并告诉他们所有相关信息-颜色、漂亮的草坪、阁楼床和堆肥厕所等。然后他将他们的询问添加到他的viewTally中。但他不会告诉他们真实的物理地址,因为鲍勃的公司专门经营随时可以移动的微型房屋。现在这个清单已经有43个人询问过了。

在另一家公司,房地产经纪人可能会明确表示他们的清单“指向”1234 Main St的房子,并在旁边加上一个小的*,因为他们主要处理很少搬迁的房屋(尽管可能有做此事的原因)。但鲍勃的公司不会这样做。

当然,鲍勃不会亲自去把实际房子放到卡车上直接展示给客户-那是不切实际和荒谬的浪费资源。传递完整的清单表格是一件事,但一直传递整个房子是昂贵和荒谬的。

(顺便说一下:鲍勃的公司也不会每次有人询问时都3D打印新的独特房屋副本。这正是新兴的、同名的基于网络的公司及其分支机构所做的-这是昂贵和缓慢的,人们经常混淆这两家公司,但它们仍然非常受欢迎)。

在一些靠近大海的老公司中,像鲍勃这样的房地产经纪人甚至可能不存在来管理清单。客户可能会直接向转轮卡片盘“安妮”(简称&)咨询房屋的实际地址。客户不是像鲍勃那样从清单中阅读引用房屋详细信息,而是从安妮(&)处获得房屋地址,然后直接去1234 Main St,有时并不知道他们可能会找到什么。

有一天,鲍勃的公司开始提供一项新的自动化服务,需要客户感兴趣的房屋的清单。

好吧,拥有这些信息的人是鲍

jobKillingAutomatedListingService(Listing tinyHouseAt1234Main, int viewTally) Bob发送...

该服务在其端口调用此列表houseToLookAt,但实际上它收到的是Bob的清单的精确副本,其中包含完全相同的值,这些值指的是位于1234 Main St.的房屋。

这项新服务还具有自己的内部计数器,用于记录查看清单的人数。出于职业礼貌,该服务接受Bob的计数器,但它并不关心并完全用其自己的本地副本覆盖它。今天的计数器为1,而Bob的计数器仍为43。

房地产公司称此为“按值传递”,因为Bob传递了他的viewTally和他的Listing tinyHouseAt1234Main的当前值。他实际上没有传递整个物理房屋,因为那是不切实际的。也没有像Annie(&)那样传递真正的物理地址。

但他确实传递了对该房子引用的值的副本。在某些方面,这似乎是一个愚蠢而拘泥于细节的区别,但这就是他的公司的工作方式...

第二部分:事情变得混乱和危险...

新的自动化服务不像其他一些时髦的金融和科学公司那样完全功能和数学导向,可能会产生意想不到的副作用...

一旦给定Listing对象,它允许客户端使用远程无人机机器人舰队实际上重新粉刷真正的1234 Main St房屋!它允许客户端控制机器人推土机,以实际上挖掘花坛!这是疯狂的!!!

该服务还允许客户完全重定向houseToLookAt到另一个地址的某个其他房子,而不涉及Bob或他的清单。突然间,他们可以看到4321 Elm St.,它与Bob的清单没有任何联系(幸运的是,他们不能造成更多的损害)。

Bob在实时网络摄像头上观看所有这些内容。他接受了他唯一的工作责任的单调乏味,告诉客户新的丑陋的油漆工作和突然失去的路缘吸引力。毕竟,他的Listing仍然是1234 Main St。无论它还值多少钱,Bob都会像往常一样准确地和尽职地报告其tinyHouseAt1234Main的详细信息,直到他被解雇或房子被“虚无”彻底摧毁。

实际上,该服务使用其houseToLookAt Bob原始清单的副本唯一不能做的事情是将地址从1234 Main St更改为其他地址,或将其更改为空虚,或将其更改为某种随机类型的对象,例如鸭嘴兽。 Bob的Listing仍然始终指向1234 Main St,无论它仍具有何种价值。他像往常一样传递其当前值。

将清单传递给新的自动化服务会导致这种奇怪的副作用,这让询问其工作原理的人感到困惑。实际上,在远程控制能够改变1234 Main街房屋状态的机器人与Annie提供地址后亲自前往并造成混乱之间有什么区别呢?
如果你通常关心的是清单中房屋的状态被复制和传播,那么这似乎是一种吹毛求疵的语义争论,对吧?
我的意思是,如果你真的从事将房屋搬到其他地址的业务(不像移动或微型住宅平台那样,这是平台功能的一种预期),或者你像某种低级神一样访问、重命名和洗牌整个社区,那么也许你更关心传递那些特定的地址引用而不仅仅是房屋详情的最新值的副本...

9

Java始终是按值传递,而不是按引用传递。

首先,我们需要了解什么是按值传递和按引用传递。

按值传递意味着您在内存中复制传入的实际参数值。这是实际参数内容的副本。

按引用传递(也称为按地址传递)意味着存储实际参数地址的副本。

有时Java可以产生按引用传递的幻觉。让我们通过使用下面的示例来看看它是如何工作的:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeValue(t);
        System.out.println(t.name);
    }
    
    public void changeValue(Test f) {
        f.name = "changevalue";
    }
}

class Test {
    String name;
}

这个程序的输出是:

changevalue

让我们逐步了解:

Test t = new Test();

众所周知,它将在堆中创建一个对象,并将引用值返回给t。例如,假设t的值为0x100234(我们不知道实际的JVM内部值,这只是一个例子)。
第一幅插图。
new PassByValue().changeValue(t);

当将引用t传递给函数时,它不会直接传递对象test的实际引用值,而是创建t的副本,然后将其传递给函数。由于它是按值传递,因此它传递的是变量的副本,而不是它的实际引用。由于我们说t的值为0x100234,因此t和f将具有相同的值,因此它们将指向同一个对象。

第二个例子

如果您使用引用f在函数中更改任何内容,则会修改对象的现有内容。这就是为令我们得到更新后的输出changevalue的原因。

为了更清楚地理解这一点,请考虑以下示例:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeRefence(t);
        System.out.println(t.name);
    }
    
    public void changeRefence(Test f) {
        f = null;
    }
}

class Test {
    String name;
}

这会抛出NullPointerException吗?不会,因为它只传递了引用的副本。如果是按引用传递,就可能会抛出NullPointerException,如下所示:

第三个例子

希望这能帮到你。


1
由于我们说 t 的值为 0x100234,因此 t 和 f 将具有相同的值,因此它们将指向同一个对象。这听起来不太对。创建对象的副本不应意味着副本和原始对象都指向同一个对象。这类似于按引用传递,或者至少是按对象引用传递。 - h0r53

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接