如何在Java中实现原始类型的按引用传递

126

这段 Java 代码:

public class XYZ {   
    public static void main(){  
        int toyNumber = 5;   
        XYZ temp = new XYZ();  
        temp.play(toyNumber);  
        System.out.println("Toy number in main " + toyNumber);  
    }

    void play(int toyNumber){  
        System.out.println("Toy number in play " + toyNumber);   
        toyNumber++;  
        System.out.println("Toy number in play after increement " + toyNumber);   
    }   
}  

输出结果如下:

当前玩具数量为 5 
增加后的玩具数量为 6 
主要玩具数量为 5 

在C++中,我可以将toyNumber变量作为引用传递,以避免阴影 i.e. 创建相同变量的副本,如下所示:

void main(){  
    int toyNumber = 5;  
    play(toyNumber);  
    cout << "Toy number in main " << toyNumber << endl;  
}

void play(int &toyNumber){  
    cout << "Toy number in play " << toyNumber << endl;   
    toyNumber++;  
    cout << "Toy number in play after increement " << toyNumber << endl;   
} 

以下是C++的输出:

Toy number in play 5  
Toy number in play after increement 6  
Toy number in main 6  

我的问题是,在Java中如何编写等效的代码以获得与C++代码相同的输出,考虑到Java是按值传递而不是按引用传递


26
我认为这不是一个重复的问题 - 所谓的重复问题涉及Java的工作原理和术语含义,这是教育性的;而这个问题特别询问如何获得类似于按引用传递行为的东西,这是许多C / C ++ / D / Ada程序员可能想知道的,以便完成实际工作,而不关心Java为什么全是按值传递。 - DarenW
1
@DarenW 我完全同意 - 已投票重新开放。哦,你有足够的声望来做同样的事情 :-) - Duncan Jones
关于原始类型的讨论有些误导,因为同样适用于引用值的问题。 - shmosel
在C++中,我可以将toyNumber变量作为引用传递来避免阴影效应 - 这不是阴影效应,因为在play方法中声明的toyNumber变量不在main方法的范围内。只有当作用域嵌套时,C++和Java中才会发生变量阴影效应。请参见https://en.wikipedia.org/wiki/Variable_shadowing。 - Stephen C
8个回答

187

你有几种选择。哪种选择最合适取决于你要做什么。

选择1:在类中将toyNumber设置为公共成员变量

class MyToy {
  public int toyNumber;
}

然后将一个 MyToy 的引用传递给您的方法。

void play(MyToy toy){  
    System.out.println("Toy number in play " + toy.toyNumber);   
    toy.toyNumber++;  
    System.out.println("Toy number in play after increement " + toy.toyNumber);   
}

方案二:返回值而不是通过引用传递

int play(int toyNumber){  
    System.out.println("Toy number in play " + toyNumber);   
    toyNumber++;  
    System.out.println("Toy number in play after increement " + toyNumber);   
    return toyNumber
}

这个选择需要对main函数中的调用做出小修改,改为toyNumber = temp.play(toyNumber);

选择3:将其变成类或静态变量

如果这两个函数是同一个类或类实例的方法,你可以将toyNumber转换成类成员变量。

选择4:创建一个int类型的单元素数组并传递该数组

这被视为一种hack,但有时会用于从内联类调用中返回值。

void play(int [] toyNumber){  
    System.out.println("Toy number in play " + toyNumber[0]);   
    toyNumber[0]++;  
    System.out.println("Toy number in play after increement " + toyNumber[0]);   
}

3
很清晰的解释,尤其是提供了多种编码方式来实现所需效果。对我目前正在处理的事情非常有帮助!关闭这个问题真是荒谬。 - DarenW
1
虽然你的选择1确实做到了你想表达的,但我实际上不得不回去再检查一遍。问题要求你是否可以通过引用传递; 因此,你应该在玩具传递给函数所在的同一作用域中执行printlns。你现在的方式是,printlns在增量操作的同一作用域中运行,这并不能真正证明这一点,当然在增量后打印本地副本将具有不同的值,但这并不意味着传递的引用也会如此。再次强调,你的例子确实可行,但并没有证明这一点。 - zero298
不,我认为现在的方式是正确的。我上面回答中的每个“play()”函数都旨在作为原始“play()”函数的替代品,该函数在增加值之前和之后也会打印该值。原始问题在主函数中有println(),证明了传递引用。 - laslowh
选项2需要进一步解释:为了使其按预期工作,主函数中的调用位置必须更改为toyNumber = temp.play(toyNumber); - ToolmakerSteve
1
这太丑陋了,感觉像是hack出来的...为什么这么基本的东西不是语言的一部分呢? - shinzou
你的选择1是属于还是与你的选择3相同?两者都为该类创建一个字段。 - user3207158

29

Java 不是 按引用传递,它只是 按值传递

但所有对象类型的变量实际上都是指针。

因此,如果您使用可变对象,就会看到所需的行为。

public class XYZ {

    public static void main(String[] arg) {
        StringBuilder toyNumber = new StringBuilder("5");
        play(toyNumber);
        System.out.println("Toy number in main " + toyNumber);
    }

    private static void play(StringBuilder toyNumber) {
        System.out.println("Toy number in play " + toyNumber);
        toyNumber.append(" + 1");
        System.out.println("Toy number in play after increement " + toyNumber);
    }
}

这段代码的输出结果:

run:
Toy number in play 5
Toy number in play after increement 5 + 1
Toy number in main 5 + 1
BUILD SUCCESSFUL (total time: 0 seconds)

你可以在标准库中看到这种行为。例如Collections.sort(); Collections.shuffle();这些方法不会返回一个新的列表,而是修改它们的参数对象。

    List<Integer> mutableList = new ArrayList<Integer>();

    mutableList.add(1);
    mutableList.add(2);
    mutableList.add(3);
    mutableList.add(4);
    mutableList.add(5);

    System.out.println(mutableList);

    Collections.shuffle(mutableList);

    System.out.println(mutableList);

    Collections.sort(mutableList);

    System.out.println(mutableList);
这段代码的输出结果是什么?
run:
[1, 2, 3, 4, 5]
[3, 4, 1, 5, 2]
[1, 2, 3, 4, 5]
BUILD SUCCESSFUL (total time: 0 seconds)

3
这不是对问题的回答。如果建议创建一个包含单个元素的整数数组,并在play方法内修改该元素,例如mutableList[0] = mutableList[0] + 1;,则可以回答该问题,如Ernest Friedman-Hill所建议的那样。 - ToolmakerSteve
没有原始类型。对象的值不是引用。也许你想说的是:“参数值”是“一个引用”。引用按值传递。 - vlakov
我正在尝试做类似的事情,但在你的代码中,方法 private static void play(StringBuilder toyNumber) - 如果它是例如返回整数的公共静态int,你如何调用它?因为我有一个返回数字的方法,但如果我不在某个地方调用它,它就没有被使用。 - frank17

19
制作一个
class PassMeByRef { public int theValue; }

那就传递它的一个实例的引用。注意,最好避免通过其参数改变状态的方法,特别是在并行代码中。


3
被我踩了。这有很多错误。Java始终是按值传递,没有例外。 - duffymo
17
当然,你可以自由地进行投票,但你有没有考虑过OP的要求?他想通过引用传递一个int,如果我传递上述实例的引用值,则可以实现这一点。请注意,不会改变原意。 - Ingo
8
哦?你错了,或者误解了问题。这确实是Java中实现OP请求的传统方法之一。 - ToolmakerSteve
6
@duffymo :你的评论在很多方面都是错误的。Stack Overflow旨在提供有用的答案和评论,而你只是因为(我猜测)它不符合你的编程风格和哲学而简单地对一个正确的答案进行了负评。你能至少提供一个更好的解释,甚至是一个更好的原问题答案吗?请不要仅仅因为个人偏好而抹杀其他人的工作成果。 - paercebal
2
@duffymo,这正是我所说的:通过值传递引用。你认为在那些速度较慢的语言中如何实现按引用传递? - Ingo
显示剩余4条评论

11

为快速解决问题,您可以使用AtomicInteger或任何原子变量,这些变量将允许您使用内置方法在方法内更改值。以下是示例代码:

import java.util.concurrent.atomic.AtomicInteger;


public class PrimitivePassByReferenceSample {

    /**
     * @param args
     */
    public static void main(String[] args) {

        AtomicInteger myNumber = new AtomicInteger(0);
        System.out.println("MyNumber before method Call:" + myNumber.get());
        PrimitivePassByReferenceSample temp = new PrimitivePassByReferenceSample() ;
        temp.changeMyNumber(myNumber);
        System.out.println("MyNumber After method Call:" + myNumber.get());


    }

     void changeMyNumber(AtomicInteger myNumber) {
        myNumber.getAndSet(100);

    }

}

输出:

MyNumber before method Call:0

MyNumber After method Call:100

1
我使用并喜欢这个解决方案,因为它提供了很好的复合操作,并且更容易集成到已经或将来可能使用这些原子类的并发程序中。 - randers

11

在Java中,您无法按引用传递原始类型。当然,所有对象类型的变量实际上都是指针,但我们将它们称为“引用”,并且它们也总是按值传递。

在确实需要按引用传递基本类型的情况下,有时人们会将参数声明为基本类型的数组,然后将单个元素数组作为参数传递。因此,您传递一个引用int[1],在方法中,您可以更改数组的内容。


1
不,所有的包装类都是不可变的——它们代表一个固定的值,一旦对象创建后就无法更改。 - Ernest Friedman-Hill
1
在Java中,你不能通过引用传递基本类型。OP似乎明白这一点,因为他们正在将C++(其中可以)与Java进行对比。 - Raedwald
1
需要注意的是,Ernest所说的“所有包装类都是不可变的”仅适用于JDK内置的Integer、Double、Long等类。您可以通过自定义包装类来绕过此限制,就像Ingo的答案所示。 - user3207158
@Ernest Friedman-Hill 设计上是有意为之的,但并不正确,可以通过反射轻松实现。 - Sam Ginrich

3
public static void main(String[] args) {
    int[] toyNumber = new int[] {5};
    NewClass temp = new NewClass();
    temp.play(toyNumber);
    System.out.println("Toy number in main " + toyNumber[0]);
}

void play(int[] toyNumber){
    System.out.println("Toy number in play " + toyNumber[0]);
    toyNumber[0]++;
    System.out.println("Toy number in play after increement " + toyNumber[0]);
}

0

在checkPassByValue中,我们将a设置为5,但它并没有反映在a的值上。 同样地,当我们传递一个人时,我们可以使用Person.setName更新该人所指向的数据,但是如果更改该人并使其引用一个新的Person(),那么该更新将不会反映在主函数中。

class Person {
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private String name;

    public Person(String name) {
        this.name = name;
    }

}

public class PassByValueVsReference {
    public static void main(String[] args) {
        int a = 3;
        Person person = new Person("Foo");
        checkPassByValue(a, person);
        System.out.println(a);
        System.out.println(person.getName());
    }

    public static void checkPassByValue(int number, Person person) {
        number = 5;
        person.setName("Foo-updated");
        person = new Person("Foo_new reference");
    }
}

//C++ equivalent of checkPassByValue(int ,Person *);


以上的 C++ 等效函数将是 checkPassByValue(int number, Person * person)。但在 C++ 中,我仍然可以使用 checkPassByValue(int &number, Person & person)、checkPassByValue(int number, Person & person) 或 checkPassByValue(int number, Person person)。 - swapnil darmora
我的评论是,在Java中,void checkPassByValue(int number, Person person)等同于checkPassByValue(Int number, Person * person),其中person是指向堆上某个Person对象的引用。如果我错了,你能告诉我它的C++等效物吗? - swapnil darmora
在C++中,引用使用变量和参数的形式是 ´Person& r;´,指针则是 ´Person* ptr;´ 引用又可以写成´r = *ptr;´,后者在C语言中可以作为引用。Java和C都将引用/指针视为值,这种思想在术语“引用”和“值”在Pascal、Delphi、Fortran或C++中使用之后很久才被引入,其中“按引用传递”是“共享实例”的同义词。 - Sam Ginrich
我想说的是,checkPassByValue(int number, Person person) 的 C++ 等效形式是 checkPassByValue(int number, Person *person),这样说是否不正确?此外,我了解在 C++ 中指针和引用是不同的。所以你的意思是,在 C++ 中 foo(int a, Person & person) 以引用方式传递 person,而在 foo(int a, Person *person) 中则以值传递指针,这与 Java 的等效方式相同。 - swapnil darmora
1
我最近开始学习Java,所以还在努力掌握它。我尝试使用int[] = new int[1]来解决这个问题,但看起来不如C++那么优雅。使用包装器来传递参数仍然感觉更加清晰明了。 - swapnil darmora
显示剩余9条评论

0
"

“Pass-by…” 在 JavaC 中是保留字。除此之外,如果您想要更改一个作为引用给出的基本类型的包装器实例,可以通过反射来完成。例如:Integer

"
public class WrapperTest
{
    static void mute(Integer a)
    {
        try
        {
            Field fValue = a.getClass().getDeclaredField("value");
            fValue.setAccessible(true);
            fValue.set(a, 6);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        Integer z = 5;
        mute(z);
        System.out.println(z);
    }

}

输出为6,就像您的C++示例一样。需要进行反射,因为Java设计认为基本类型的包装类是不可变的。否则,任何其他类都可以作为包装器,甚至是长度为1的数组,例如int[]


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