改造无效方法以返回其参数以促进流畅性:重大更改?

2022-09-03 14:41:45

“API设计就像性:犯一个错误,并在你的余生中支持它”Josh Bloch在twitter上

Java 库中有许多设计错误。 (讨论),我们无法在不造成破损的情况下解决这个问题。我们可以尝试弃用(讨论),但它可能会永远存在。Stack extends VectorInteger.getInteger

尽管如此,某些类型的改造可以在不造成破损的情况下进行。

有效的Java第2版,第18项:更喜欢接口而不是抽象类:可以很容易地改造现有类以实现新接口”。

示例:、等。String implements CharSequenceVector implements List

有效的 Java 第 2 版,第 42 项:明智地使用 varargs:您可以改造一个以数组作为其最终参数的现有方法,以采用 varags,而不会影响现有客户端。

一个(不)著名的例子是,它引起了混乱(讨论),但没有破损。Arrays.asList

这个问题是关于另一种改造的:

你能改造一个方法来返回一些东西而不破坏现有的代码吗?void

我最初的预感指向是的,因为:

  • 返回类型不会影响在编译时选择哪种方法
  • 即使您使用反射,像Class.getMethod这样的东西也不会区分返回类型。

但是,我希望听到其他在Java / API设计方面更有经验的人的更彻底的分析。


附录:动机

正如标题中所建议的那样,一个动机是促进流畅的界面风格编程。

考虑一下这个简单的代码段,它打印了一个随机排列的名称列表:

    List<String> names = Arrays.asList("Eenie", "Meenie", "Miny", "Moe");
    Collections.shuffle(names);
    System.out.println(names);
    // prints e.g. [Miny, Moe, Meenie, Eenie]

如果 Collections.shuffle(List) 被声明为返回输入列表,我们可以这样写:

    System.out.println(
        Collections.shuffle(Arrays.asList("Eenie", "Meenie", "Miny", "Moe"))
    );

还有其他方法,如果它们返回输入列表而不是,例如反向(列表),sort(List)等,使用起来会更愉快。事实上,拥有和返回是特别不幸的,因为它剥夺了我们编写这样的富有表现力的代码:CollectionsvoidCollections.sortArrays.sortvoid

// DOES NOT COMPILE!!!
//     unless Arrays.sort is retrofitted to return the input array

static boolean isAnagram(String s1, String s2) {
    return Arrays.equals(
        Arrays.sort(s1.toCharArray()),
        Arrays.sort(s2.toCharArray())
    );
}

当然,这种防止流利度的返回类型不仅限于这些实用方法。java.util.BitSet 方法也可以编写为返回 (ala 和 )以促进流畅性。voidthisStringBufferStringBuilder

// we can write this:
    StringBuilder sb = new StringBuilder();
    sb.append("this");
    sb.append("that");
    sb.insert(4, " & ");
    System.out.println(sb); // this & that

// but we also have the option to write this:
    System.out.println(
        new StringBuilder()
            .append("this")
            .append("that")
            .insert(4, " & ")
    ); // this & that

// we can write this:
    BitSet bs1 = new BitSet();
    bs1.set(1);
    bs1.set(3);
    BitSet bs2 = new BitSet();
    bs2.flip(5, 8);
    bs1.or(bs2);
    System.out.println(bs1); // {1, 3, 5, 6, 7}

// but we can't write like this!
//  System.out.println(
//      new BitSet().set(1).set(3).or(
//          new BitSet().flip(5, 8)
//      )
//  );

不幸的是,与 / 不同,所有的 mutator 都返回 。StringBuilderStringBufferBitSetvoid

相关主题


答案 1

不幸的是,是的,更改方法以返回某些内容是一个重大更改。此更改不会影响源代码兼容性(即相同的Java源代码仍将像以前一样编译,绝对没有明显的效果),但它会破坏二进制兼容性(即以前针对旧API编译的字节码将不再运行)。void

以下是 Java 语言规范第 3 版的相关摘录:

13.2 什么是二进制兼容性,什么是不兼容性

二进制兼容性与源兼容性不同。


13.4 类的演变

本节介绍对类及其成员和构造函数的声明进行更改对预先存在的二进制文件的影响。

13.4.15 方法结果类型

更改方法的结果类型、将结果类型替换为 或替换为结果类型具有以下组合效果:voidvoid

  • 删除旧方法,以及
  • 添加具有新结果类型或新结果的新方法。void

13.4.12 方法和构造函数声明

从类中删除方法或构造函数可能会破坏与引用此方法或构造函数的任何预先存在的二进制文件的兼容性;当链接来自预先存在的二进制文件的此类引用时,可能会引发 a。仅当超类中没有声明具有匹配签名和返回类型的方法时,才会发生此类错误。NoSuchMethodError

也就是说,虽然 Java 编译器在方法解析过程中在编译时会忽略方法的返回类型,但此信息在运行时的 JVM 字节码级别非常重要。


在字节码方法描述符上

方法的签名不包括返回类型,但其字节码描述符包含。

8.4.2 方法签名

如果两个方法具有相同的名称和参数类型,则它们具有相同的签名。


15.12 方法调用表达式

15.12.2 编译时步骤 2:确定方法签名

最具体方法的描述符(签名加返回类型)是在运行时用于执行方法调度的描述符。

15.12.2.12 示例:编译时解析

在编译时选择最适用的方法;它的描述符确定在运行时实际执行的方法。

如果将新方法添加到类中,则使用类的旧定义编译的源代码可能不会使用新方法,即使重新编译会导致选择此方法也是如此。

理想情况下,每当源代码所依赖的代码发生更改时,都应重新编译源代码。但是,在由不同组织维护不同类的环境中,这并不总是可行的。

对字节码进行一些检查将有助于澄清这一点。当 在名称随机播放代码段上运行 时,我们会看到如下说明:javap -c

invokestatic java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
             \______________/ \____/ \___________________/\______________/
                type name     method      parameters        return type

invokestatic java/util/Collections.shuffle:(Ljava/util/List;)V
             \___________________/ \_____/ \________________/|
                   type name        method     parameters    return type

相关问题


关于不间断改造

现在让我们来讨论为什么改造新的或vararg(如有效的Java 2nd Edition中所述)不会破坏二进制兼容性。interface

13.4.4 超类和超接口

更改类类型的直接超类或直接超接口集不会破坏与预先存在的二进制文件的兼容性,前提是类类型的超类或超接口的总集没有丢失任何成员。

改造新类型不会导致类型丢失任何成员,因此这不会破坏二进制兼容性。同样,由于 varargs 是使用数组实现的,这种改造也不会破坏二进制兼容性。interface

8.4.1 形式参数

如果最后一个形式参数是类型的变量 arity 参数,则认为它定义了类型的形式参数。TT[]

相关问题


是绝对没有办法做到这一点吗?

实际上,是的,有一种方法可以在以前的方法上改造返回值。我们不能在Java源代码级别拥有两个具有相同确切签名的方法,但是我们可以在JVM级别拥有它,只要它们具有不同的描述符(由于具有不同的返回类型)。void

因此,我们可以提供一个二进制文件,例如 同时具有同时具有返回类型和不可返回类型的方法。我们只需要将非版本发布为新的 API。事实上,这是我们唯一可以在API上发布的东西,因为在Java中拥有两个具有完全相同签名的方法是非法的。java.util.BitSetvoidvoidvoid

这个解决方案是一个可怕的黑客攻击,需要特殊的(和违反规范的)处理来编译为,因此可能不值得这样做。BitSet.javaBitSet.class


答案 2

如果你不能改造,你仍然可以把你的类包装成一个新的类,它使用相同的方法,但返回正确(MyClassFluent)。或者,您可以添加新方法,但名称不同,而不是我们可以有.Arrays.sort()Arrays.getSorted()

我认为解决方案不是强迫事情,只是处理它们。

编辑:我知道我没有回答“虚空方法的改造”问题,但你的答案已经很清楚了。


推荐