“API设计就像性:做出一个错误并在余生中支持它” (Josh Bloch on twitter)
Java库中存在许多设计错误。 Stack extends Vector
(discussion),我们无法在不造成破坏的情况下解决这个问题。我们可以尝试弃用Integer.getInteger
(discussion),但它可能会永远存在。
尽管如此,可以在不造成破损的情况下进行某些改装。
Effective Java 2nd Edition,第18项:首选接口到抽象类:现有类可以轻松改进以实现新接口“。
示例:String implements CharSequence
,Vector implements List
等
有效的Java第2版,第42项:明智地使用varargs :您可以改进现有的方法,该方法将数组作为其最终参数,而不是对现有客户端采用varags。
一个着名的例子是Arrays.asList
,它引起了混淆(discussion),但没有破坏。
这个问题是关于不同类型的改造:
void
方法以返回内容?我的初步预感指向是,因为:
void
进行改造以返回某些内容是合法的(但不是相反!)Class.getMethod
之类的内容也无法区分返回类型但是,我希望听到其他在Java / API设计方面经验丰富的人员进行更全面的分析。
正如标题中所建议的那样,一个动机是促进fluent interface样式编程。
考虑这个简单的片段,它打印一个洗牌的名单:
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"))
);
Collections
中的其他方法如果要返回输入列表而不是void
,则使用起来会更加愉快,例如Collections.sort
。 reverse(List)
,sort(List)
等。实际上,让Arrays.sort
和void
返回// 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())
);
}
特别不幸,因为它剥夺了我们编写像这样的表达代码:
void
这种阻止流畅的this
返回类型当然不仅限于这些实用方法。 java.util.BitSet
方法也可以写成返回StringBuffer
(ala StringBuilder
和// 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)
// )
// );
)以方便流利。
StringBuilder
不幸的是,与StringBuffer
/ BitSet
不同,void
的{{1}}突变者的 ALL 会返回{{1}}。
答案 0 :(得分:14)
不幸的是,是的,更改void
方法以返回某些内容是一个重大变化。此更改不会影响源代码兼容性(即,相同的Java源代码仍将像以前一样编译,完全没有明显的效果)但它会破坏二进制兼容性(即先前针对旧API编译的字节码将不再运行)。
以下是 Java语言规范第3版的相关摘录:
13.2 What Binary Compatibility Is and Is Not
二进制兼容性与源兼容性不同。
13.4 Evolution of Classes
本节描述了对类及其成员和构造函数的声明更改对预先存在的二进制文件的影响。
13.4.15 Method Result Type
更改方法的结果类型,将结果类型替换为
void
,或将void
替换为结果类型具有以下组合效果:
- 删除旧方法和
- 使用新结果类型或新
void
结果添加新方法。13.4.12 Method and Constructor Declarations
从类中删除方法或构造函数可能会破坏与引用此方法或构造函数的任何预先存在的二进制文件的兼容性;当链接来自预先存在的二进制文件的此类引用时,可能会抛出
NoSuchMethodError
。只有在超类中没有声明具有匹配签名和返回类型的方法时,才会发生此类错误。
也就是说,虽然Java编译器在方法解析过程中在编译时忽略了方法的返回类型,但此信息在JVM字节码级别的运行时很重要。
方法的签名不包括返回类型,但其字节码描述符不包括。
8.4.2 Method Signature
如果两个方法具有相同的名称和参数类型,则它们具有相同的签名。
15.12 Method Invocation Expressions
15.12.2 Compile-Time Step 2: Determine Method Signature
最具体方法的描述符(签名加返回类型)是在运行时用于执行方法调度的方法。
15.12.2.12 Example: Compile-Time Resolution
在编译时选择最适用的方法;它的描述符确定在运行时实际执行的方法。
如果向类中添加了新方法,那么使用旧类定义编译的源代码可能不会使用new方法,即使重新编译会导致选择此方法。
理想情况下,只要更改了依赖的代码,就应重新编译源代码。但是,在不同组织维护不同类的环境中,这并不总是可行的。
对字节码进行一点检查有助于澄清这一点。在名称shuffling代码段上运行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
现在让我们解决为什么改进新的interface
或vararg,如 Effective Java 2nd Edition 中所述,不会破坏二进制兼容性。
13.4.4 Superclasses and Superinterfaces
更改直接超类或类类型的直接超接口集不会破坏与预先存在的二进制文件的兼容性,前提是类类型的超类或超接口的总集合不会丢失任何成员。
对新interface
进行改造不会导致该类型丢失任何成员,因此这不会破坏二进制兼容性。同样,由于varargs是使用数组实现的,因此这种改进也不会破坏二进制兼容性。
8.4.1 Formal Parameters
如果最后一个形式参数是
T
类型的变量arity参数,则认为它定义了T[]
类型的形式参数。
实际上,是的,有一种方法可以改进先前void
方法的返回值。我们不能在Java源代码级别有两个具有相同精确签名的方法,但我们 CAN 在JVM级别具有该方法,前提是它们具有不同的描述符(由于具有不同的返回类型)
因此,我们可以为例如提供二进制文件。 java.util.BitSet
同时包含void
和非void
返回类型的方法。我们只需将非void
版本发布为新API。事实上,这是我们可以在API上发布的唯一内容,因为在Java中使用具有完全相同签名的两种方法是非法的。
这个解决方案是一个糟糕的黑客攻击,需要特殊的(并且反式规范)处理才能将BitSet.java
编译为BitSet.class
,因此可能不值得这样做。
答案 1 :(得分:1)
如果你不能改装,你仍然可以将你的类包装成一个新的类,它使用相同的方法,但返回正确(MyClassFluent)。
或者您可以使用不同的名称添加新方法,而不是Arrays.sort()
我们可以Arrays.getSorted()
。
我认为解决方案不是强迫事情,只是处理它们。
编辑:我知道我没有回答“改造无效方法”的问题,但你的回答已经很清楚了。答案 2 :(得分:1)
如果您只需要处理源兼容性,那么请继续。从void更改为return类型不会中断。
但要解决你真正想做的事情: 我认为流畅的接口的问题是线条往往变得相当长,并且 - 在格式化之后 - 有些不可读。对于建筑商而言,它工作正常,但我可能不会将其用于其他任何事情。
这是为了玩它,还是因为你发现这真的很棒?