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

时间:2014-01-22 04:42:56

标签: java pass-by-reference language-design bytecode bytecode-manipulation

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);
    }
}

这可能会失败或者您如何改进?

10 个答案:

答案 0 :(得分: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'";
}

答案 1 :(得分:14)

回答你的问题:

  

这可能会失败吗?

  1. 最终变量和枚举常量
  2. '特殊'引用,例如this
  3. 从方法调用返回的引用,或使用new
  4. 内联构造的引用
  5. 文字(字符串,整数等)
  6. ......可能还有其他人。基本上,只有参数源是非最终字段或局部变量时,您的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关键字,这有点奇怪。我认为方法定义就足够了。

答案 2 :(得分:9)

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

public class Holder<T> {
  protected T value;

  public T getValue() {
    return value;
  }

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

线程安全版本是AtomicReference

现在在一个类中存储一个String似乎过度杀戮,而且很可能是,但是通常你有一个数据持有者类来代替几个相关的值。

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

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

答案 3 :(得分: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");
}

答案 4 :(得分:3)

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

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

  • 局部变量
  • 对象属性

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

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

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

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

对于额外的布朗尼点,您可以将技术扩展到JavaBean属性,甚至是ListArrayvoid doIt(ref String)元素。

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

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

答案 5 :(得分:1)

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

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

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

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

顺便说一句,已经有几个参考类型(就像你对Holder一样)。 ThreadLocal<>(包含get()set())或Reference扩展程序,例如WeakReference(我认为只有get())。< / p>

编辑: 在阅读了其他一些答案后,我建议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。

答案 6 :(得分:1)

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

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

public void doSomething(@Ref String var)

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

答案 7 :(得分:0)

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

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

答案 8 :(得分:0)

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

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

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

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

答案 9 :(得分:-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);
}