如何扩展Java以引入通过引用传递?

问题描述 投票:27回答:10

Java is pass-by-value.您如何修改语言以引入通过引用传递(或某些等效行为)?

举个例子

public static void main(String[] args) {
    String variable = "'previous String reference'";
    passByReference(ref variable);
    System.out.println(variable); // I want this to print 'new String reference'
}

public static void passByReference(ref String someString) {
    someString = "'new String reference'";
}

其中(没有ref)汇编到以下bytecode

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String 'previous String reference'
       2: astore_1
       3: aload_1
       4: invokestatic  #3                  // Method passByReference:(Ljava/lang/String;)V
       7: return

  public static void passByReference(java.lang.String);
    Code:
       0: ldc           #4                  // String 'new String reference'
       2: astore_0
       3: return

3:的代码将引用从变量variable加载到堆栈中。

我正在考虑的一种可能性是让编译器确定方法是通过引用传递,可能使用ref,并将方法更改为接受Holder对象,该对象存储与变量相同的引用。当方法完成,并且可能更改持有者中的引用时,调用方的值的变量将替换为持有者引用的值。

它应该编译成相当于这个

public static void main(String[] args) {
    String variable = "'previous String reference'";
    Holder holder = Holder.referenceOf(variable);
    passByReference2(holder);
    variable = (String) holder.getReference(); // I don't think this cast is necessary in bytecode
    System.out.println(variable);
}

public static void passByReference(Holder someString) {
    someString.setReference("'new String reference'");
}

Holder可能是这样的

public class Holder {
    Object reference;
    private Holder (Object reference) {
        this.reference = reference;
    }
    public Object getReference() {
        return this.reference;
    }
    public void setReference(Object reference) {
        this.reference = reference;
    }
    public static Holder referenceOf(Object reference) {
        return new Holder(reference);
    }
}

哪个会失败,或者你怎么能改进它?

java pass-by-reference language-design bytecode bytecode-manipulation
10个回答
14
投票

回答你的问题:

哪个会失败?

  1. 最终变量和枚举常量
  2. '特殊'参考文献,如this
  3. 从方法调用返回的引用,或使用new内联构造的引用
  4. 文字(字符串,整数等)

......还有其他人。基本上,只有当参数源是非最终字段或局部变量时,才能使用ref关键字。与ref一起使用时,任何其他源都应生成编译错误。

(1)的一个例子:

final String s = "final";
passByReference(ref s);  // Should not be possible

(2)的一个例子:

passByReference(ref this);  // Definitely impossible

(3)的一个例子:

passByReference(ref toString());  // Definitely impossible
passByReference(ref new String("foo"));  // Definitely impossible

(4)的一个例子:

passByReference(ref "literal");  // Definitely impossible

然后有赋值表达式,在我看来像是一个判断调用:

String s;
passByReference(ref (s="initial"));  // Possible, but does it make sense?

你的语法在方法定义和方法调用中都需要ref关键字,这也有点奇怪。我认为方法定义就足够了。


-1
投票

Java(实际上)通过引用传递。调用该方法时,将传递对象的引用(指针),当您修改对象时,可以在从方法返回时看到修改。您的示例的问题是java.lang.String是不可变的。

您通过示例实际实现的是输出参数。

这是Jeffrey Hantin略有不同的版本:

public static void main(String[] args) {
  StringBuilder variable = new StringBuilder("'previous String reference'");
  passByReference(variable);
  System.out.println(variable); // I want this to print 'new String reference'
}

public static void passByReference(StringBuilder someString) {
  String nr = "'new String reference'";
  someString.replace(0, nr.length() - 1, nr);
}

22
投票

我在Java中看到的传递引用的常用习惯是传递一个单元素数组,这将保留运行时类型安全性(不同于经过擦除的泛型)并且避免引入新类的需要。

public static void main(String[] args) {
    String[] holder = new String[1];

    // variable optimized away as holder[0]
    holder[0] = "'previous String reference'";

    passByReference(holder);
    System.out.println(holder[0]);
}

public static void passByReference(String[] someString) {
    someString[0] = "'new String reference'";
}

9
投票

您尝试修改该语言忽略了这样一个事实,即明确省略了这个“功能”,以防止众所周知的副作用错误首先发生。 Java建议通过使用数据持有者类来执行您要归档的操作:

public class Holder<T> {
  protected T value;

  public T getValue() {
    return value;
  }

  public void setValue(T value) {
    this.value = value;
  }
}

一个线程安全的版本将是AtomicReference

现在在一个类中存储单个String似乎过度杀死并且很可能是,但是通常你有一个数据持有者类,用于几个相关值而不是单个String。

这种方法的最大好处是方法内部发生的事情是非常明确的。所以即使你在一个周末早上的周末早上编程并且咖啡机刚刚崩溃,你仍然可以轻松地告诉代码正在做什么(KISS),防止一些错误甚至发生在一开始,只是因为你忘了方法foo的那个特征。

如果您考虑数据持有者版本无法实现的方法,您很快就会意识到您实施的内容只是因为它不同,但实际上它没有实际价值。


7
投票

使用AtomicReference类作为持有者对象。

public static void main(String[] args) {
    String variable="old";
    AtomicReference<String> at=new AtomicReference<String>(variable);
    passByReference(at);
    variable=at.get();
    System.out.println(variable);
}

public static void passByReference(AtomicReference<String> at) {
  at.set("new");
}

3
投票

奇怪的是,我最近一直在考虑这个问题。我正在考虑创建一个在JVM上运行的VB方言是否有趣 - 我认为它不会。

无论如何,有两个主要的情况,这可能是有用的和明确定义:

  • 局部变量
  • 对象属性

我假设您正在为新的Java方言编写一个新的编译器(或调整现有的编译器)。

局部变量通常由类似于您提议的代码处理。我最熟悉Scala,它不支持pass-by-reference,但确实支持具有相同问题的闭包。在Scala中,有一个类scala.runtime.ObjectRef,类似于你的Holder类。对于基元,易变量等也有类似的{...}Ref类。

如果编译器需要创建一个更新局部变量的闭包,它会将变量“升级”为final ObjectRef(可以在其构造函数中传递给闭包),并用gets替换该变量的使用,并用sets替换。 ObjectRef。在编译器中,只要通过引用传递局部变量,就可以升级它们。

您可以使用与对象属性类似的技巧。假设Holder实现了一个接口ByRef。当您的编译器看到通过引用传递的对象属性时,它可以创建ByRef的匿名子类,该子类在其getset方法中读取和更新对象属性。同样,Scala为延迟评估的参数做了类似的事情(比如引用,但是只读)。

对于额外的布朗尼点,您可以将技术扩展到JavaBean属性甚至MapListArray元素。

这样做的一个副作用是,在JVM级别,您的方法具有意外的签名。如果你使用签名void doIt(ref String)编译一个方法,在字节码级别,你最终会得到签名void doIt(ByRef)(你可能会期望这类似于void doIt(ByRef<String>),但当然泛型使用类型擦除)。这可能会导致方法重载问题,因为所有by-ref参数都会编译为相同的签名。

有可能通过字节码操作来实现这一点,但存在一些缺陷,例如JVM允许应用程序重用局部变量这一事实 - 因此在字节码级别,可能不清楚参数是否正在重新分配,如果应用程序是在没有调试符号的情况下编译的,则重新使用其插槽。此外,如果在外部方法中不可能更改值,编译器可能会忽略aload指令 - 如果您不采取措施来避免这种情况,则对外部方法的更改可能不会反映在您的引用变量中。


1
投票

我认为你可以通过构建代理和使用cglib来完成你想要的大部分工作。

这里给出的许多例子都可以。我建议使用你提出的模板,因为它将使用普通编译器进行编译。

public void doSomething(@Ref String var)

然后在幕后使用cglib重写带注释的方法,这很容易。你还必须重写调用者,我认为在cglib中会更加复杂。 javassist使用更多的“源代码”导向方法,可能更适合重写调用者。


1
投票

想想如何用原始类型实现它,比如int。 Java - JVM,而不仅仅是语言 - 在本地变量,框架(方法堆栈)或操作数堆栈上没有任何“指针”类型。没有它,就不可能通过引用真正传递。

其他支持pass-by-reference使用指针的语言(我相信,虽然我没有看到任何其他可能性)。 C ++引用(如int&)是伪装的指针。

我曾想过创建一组扩展Number的新类,包含intlong等,但不是一成不变的。这可以通过引用传递原语的一些效果 - 但它们不会自动装箱,而其他一些功能可能无效。

如果没有JVM的支持,你就不能拥有真正的pass-by-reference。对不起,但这是我的理解。

顺便说一下,已经有几个参考类型(就像你对Holder一样)。 ThreadLocal<>(有get()set()),或Reference扩展器,如WeakReference(我认为只有get())。

编辑:在阅读其他一些答案后,我建议ref是一种自动拳击形式。从而:

class ReferenceHolder<T> {
    T referrent;
    static <T> ReferenceHolder<T> valueOf(T object) {
        return new ReferenceHolder<T>(object);
    }
    ReferenceHolder(T object) { referrent = object; }
    T get()            { return referrent; }
    void set(T value)  { referrent = value; }
}

class RefTest {
    static void main() {
        String s = "Hello";
        // This is how it is written...
        change(s);
        // but the compiler converts it to...
        ReferenceHolder<String> $tmp = ReferenceHolder.valueOf(s);
        change($tmp);
        s = $tmp.get();
    }
    // This is how it is written...
    static void change(ref Object s) {
        s = "Goodbye";              // won't work
        s = 17;             // *Potential ClassCastException, but not here*
    }
    // but the compiler converts it tothe compiler treats it as:
    static <T> void change(ReferenceHolder<T> obj) {
        obj.set((T) "Goodbye");     // this works
        obj.set((T) 17);    // *Compiler can't really catch this*
    }
}

但是看看哪里有可能在ReferenceHolder中放入错误的类型?如果通用正确,编译器有时可能会发出警告,但由于您可能希望新代码尽可能地与普通代码相似,因此每次自动引用调用都有可能出现CCEx。


0
投票

回答你关于如何扩展我选择的语言的问题: - 使用各种持有者技术作为其他几个答案描述 - 使用注释附加关于哪些参数应该通过引用传递的元数据,然后开始玩字节代码操作库,如cglib为了在字节码本身中实现您的想法。

虽然这整个想法看起来很奇怪。


0
投票

即使在标准的按值传递约定中,有几种方法可以将Java代码编写为有效的传递引用。

一种方法是使用实​​例或静态变量,其范围包括特定方法,代替显式参数。如果您真的想在方法的开头提及它们的名称,那么正在修改的变量可以包含在注释中。

这种方法的缺点是这些变量的范围需要包含所讨论的整个类,而不仅仅是方法。如果您想更精确地限制变量的范围,您可以始终使用getter和setter方法而不是参数来修改它们。

在使用Java和C / C ++之后,我认为Java所谓的仅仅是按值传递的不灵活性是一个大问题 - 对于任何知道变量发生了什么的程序员来说,有合理的解决方法可以完成功能相同的东西。

© www.soinside.com 2019 - 2024. All rights reserved.