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

Posted

技术标签:

【中文标题】如何扩展 Java 以引入引用传递?【英文标题】:How can you extend Java to introduce passing by reference? 【发布时间】:2014-02-12 00:06:12 【问题描述】:

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

这可能在哪里失败,或者您如何改进它?

【问题讨论】:

你熟悉Jasmin吗?我一直很喜欢那里的答案 - 在“使用 JVM 指令集为您的语言实现调用引用”下。剧透:他们称之为“参考”-“价值”。 @ElliottFrisch 感谢您的链接,我不熟悉 Jasmin。看来我在建议类似于他们的包装类解决方案。 不幸的是,他们已经将近十年没有更新homepage了。我拥有这本书。 这听起来不像是一个理论语言设计问题;您的整个问题都是关于如何实现它,尽管您已经自己回答了问题,并在问题中给出了完整的解决方案。那么你的问题实际上是什么?您正在打开一罐蠕虫,引入一种使局部变量成为非局部变量的语言功能,无需讨论。十多年前,当创建 Java 并做出不支持此类事物的语言设计决定时,这一点就被理解了。如前所述,如果您不喜欢,可以使用其他语言。 它将使用单元素数组而不是自定义类。 【参考方案1】:

我在 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'";

【讨论】:

这对基础 java 有好处。不过,老实说,我更愿意让我的参数保持它们实际的类型,而不必检索数组元素。但是,如果我们可以更改字节码以使其与您显示的代码等效,那就没问题了。 @Sotirios,这就是我的意思:转换后的等效基本 Java 代码。您可以在声明和调用时在参数上使用 ref 关键字作为此转换的触发器,这样做将允许使用此习惯用法与现有基本 Java 代码进行互操作。 嗯,好的,我会检查一下在字节码级别执行此操作的难度/可能性。谢谢。【参考方案2】:

回答你的问题:

这会在哪里失败?

    最终变量和枚举常量 “特殊”引用,例如this 从方法调用返回的引用,或使用new 内联构造的引用 文字(字符串、整数等)

...可能还有其他人。基本上,您的 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 关键字,这也有点奇怪。我认为方法定义就足够了。

【讨论】:

感谢您的回答。这就是我一直在寻找的那种情况。所有这些在编译时都有解决方案(就像 C# 一样)。基本上,您只允许来自非最终变量的引用。 仅供参考 - 我不会声称我提供的列表是详尽的,可能还有其他列表。我确实添加了一个更多的灰色区域(赋值表达式)的示例 在Java中,赋值表达式用作值,例如(s = "initial")实际上将右侧的值压入堆栈两次,弹出一次分配给变量,再次弹出用作表达式的值。可能也有办法检测到这一点。【参考方案3】:

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

public class Holder<T> 
  protected T value;

  public T getValue() 
    return value;
  

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

线程安全版本是AtomicReference。

现在在一个类中存储单个字符串似乎有点过头了,而且很可能确实如此,但是通常您有一个用于多个相关值的数据持有者类,而不是单个字符串。

这种方法的最大好处是方法内部发生的事情非常明确。因此,即使您在一个忙碌的周末之后的星期一早上编程并且咖啡机刚刚坏了,您仍然可以轻松地判断代码在做什么(KISS),甚至可以防止出现一些错误,只是因为你忘记了方法 foo 的那个特性。

如果您考虑一下您的方法可以做哪些数据持有者版本不能做的事情,您很快就会意识到您正在实施某些东西,只是因为它不同,但实际上它没有真正的价值。

【讨论】:

引用传递会存在哪些问题,在使用持有者类时不会更糟?在 .NET 中,如果一个对象将其字段之一作为 byref 传递给外部方法,则可以确定由于该方法调用而对该字段发生的任何事情都会在它返回之前发生(除非外部代码被授予“不安全”权限)。相比之下,一旦对可变对象的 Java 引用暴露给外部代码,就无法知道该对象在未来的任何时间如何、何时或由谁任意修改。 假设你有函数add(myInt),但你不知道,调用该函数后myInt指向的不是你交出它时的样子。这可能是有原因的,但实际上它使代码难以理解并且难以找到错误。这就是为什么 Java 的核心原则之一是非常明确地说明代码在做什么,而传递引用违反了这一点。出于同样的原因,每本关于 Java 的书都告诉你不要公开可变类变量。 我同意你关于为什么它被排除在语言之外的论点,但我不同意关于替代方案容易出错的论点。我正在寻找的解决方案将在源代码中明确标记方法通过引用传递,例如新关键字ref@Ref 之类的注释。 即使你有注解,你仍然不知道里面发生了什么。这很容易出错,不是因为它通常会导致错误,而是因为总有一天,你会忘记这个功能的一个特殊之处。如果你有类似myInt = add(myInt, 1) 的东西,那么它的作用非常很明显,而且你不需要真正记住add 可能会做什么,它足够明确,只需阅读代码即可.甚至在你第一次喝咖啡之前的星期一。【参考方案4】:

使用 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");

【讨论】:

是否有特定原因需要引用是原子的? 分配是原子的:您可能希望保留该属性。无论如何,AtomicReference 类已经存在。无需添加持有者类。 @EJP 当然可以,但是什么线程可以拦截引用的变化? AtomicReference 不会在这里增加价值,除非您需要 getAndSetcompareAndSet。此外,如果变量是原始类型,则必须将其装箱才能进行一般处理;一个单元素数组已经充当了一个可变框,因此您最终不会得到两个引用,其中一个就足够了。【参考方案5】:

奇怪的是,我自己最近一直在思考这个问题。我正在考虑创建一个在 JVM 上运行的 VB 方言是否会很有趣 - 我决定不会。

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

局部变量 对象属性

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

局部变量通常由类似于您提议的代码处理。我最熟悉 Scala,它不支持按引用传递,但支持具有相同问题的闭包。在 Scala 中,有一个类 scala.runtime.ObjectRef,它类似于您的 Holder 类。也有类似的...Ref 类用于基元、易失变量等。

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

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

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

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

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

【讨论】:

【参考方案6】:

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

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

public void doSomething(@Ref String var)

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

【讨论】:

【参考方案7】:

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

支持按引用传递的其他语言使用指针(我相信,尽管我没有看到任何其他可能性)。 C++ 引用(如int&amp;)是变相的指针。

我曾考虑创建一组新的类来扩展Number,包含intlong 等,但不是不可变的。这可能会产生一些通过引用传递基元的效果 - 但它们不会被自动装箱,并且其他一些功能可能不起作用。

如果没有 JVM 的支持,您将无法进行真正的传递引用。抱歉,这是我的理解。

顺便说一句,已经有几个引用类型的类(就像您想要的 Holder 一样)。 ThreadLocal&lt;&gt;(有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。

【讨论】:

如果字节码有效地完成工作,JVM 并不重要。如果在编译时我可以处理注解(或 ref 之类的新关键字),那么我可以创建(或修改)字节码,使其相当于通过引用传递。 因此,您正在考虑将ref int 包装在SignedInt(例如)中,将引用(按值)传递给该对象并修改存储在该对象中的值。然后,当方法返回时,将包装(和修改)的值复制回局部变量。我想问题的症结在于最后一项任务。一种自动装箱和自动拆箱,您可以在其中寻找想法(或者,您可能已经有了)。 是的,这就是我想要最终将字节码转换为的内容。【参考方案8】:

回答您关于如何扩展我选择的语言的问题: - 正如其他几个答案所描述的那样,使用各种支架技术 - 使用注释来附加元数据,说明哪些参数应该通过引用传递,然后开始使用字节码操作库,如cglib,以便在字节码本身中实现您的想法。

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

【讨论】:

【参考方案9】:

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

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

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

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

【讨论】:

我不想在纯 Java 中做到这一点。我试图想出一些方法来操纵字节码来实现它,可能不会影响 Java 的语法和/或类型本身。例如,如果方法被声明为public void doSomething(@Ref String var),那么参数应该通过引用传递。我不想把源代码改成public void doSomething(Holder&lt;String&gt; var)【参考方案10】:

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

【讨论】:

不,Java 非常注重价值传递。对象的引用未通过。传递对象引用值的副本。在您的示例中,在passByReference 中,如果您将someString 的引用更改为新对象,即。 someString = new StringBuilder(),这在调用代码中是不可见的。我想扩展语言,以便更改 可见。 另外,我希望源代码中变量的类型保持不变。如有必要,可以在字节码中更改它们。 对不起,-1 因为“Java(实际上)是通过引用传递的。”

以上是关于如何扩展 Java 以引入引用传递?的主要内容,如果未能解决你的问题,请参考以下文章

阿里云名师课堂Java面向对象开发40:引用传递实际应用

Java int [] arr数组以“引用”方式被传递

java中String包装类枚举类的引用传递

通过引用方法传递对象数组

Java的参数传递是「按值传递」还是「按引用传递」?

这一次,彻底解决Java的值传递和引用传递