Java 8 默认方法会破坏源代码兼容性吗?

56

目前为止,Java源代码通常是向前兼容的。 我所知道的是,在Java 8之前,编译后的类和源文件都可以与后来的JDK / JVM版本向前兼容。 [更新:这是不正确的,请参见下面有关“枚举”等的评论。] 然而,随着在Java 8中添加默认方法,这似乎不再是这种情况。

例如,我一直在使用的库包含一个java.util.List实现,其中包括一个List<V> sort(),该方法返回已排序列表的内容副本。将此库部署为jar文件依赖项后,在使用JDK 1.8构建的项目中运行良好。

但是,稍后我有机会使用JDK 1.8重新编译库本身,我发现该库不再编译:具有自己的sort()方法的List实现类现在与Java 8 java.util.List.sort()默认方法冲突。 Java 8 sort()默认方法原地对列表进行排序(返回void);我的库的sort()方法-因为它返回一个新的已排序列表-具有不兼容的签名。

因此,我的基本问题是:

  • JDK 1.8是否由于默认方法引入了Java源代码的向前不兼容性?

而且:

  • 这是第一个这样的向前不兼容的更改吗?
  • 在设计和实现默认方法时是否考虑或讨论了此问题? 是否有文档记录?
  • 与好处相比,(无论多小)的不便是否被忽略了?

以下是一些在1.7下编译和运行,在1.8下运行但在1.8下不能编译的代码示例:

import java.util.*;

public final class Sort8 {

    public static void main(String[] args) {
        SortableList<String> l = new SortableList<String>(Arrays.asList(args));
        System.out.println("unsorted: "+l);
        SortableList<String> s = l.sort(Collections.reverseOrder());
        System.out.println("sorted  : "+s);
    }

    public static class SortableList<V> extends ArrayList<V> {

        public SortableList() { super(); }
        public SortableList(Collection<? extends V> col) { super(col); }

        public SortableList<V> sort(Comparator<? super V> cmp) {
            SortableList<V> l = new SortableList<V>();
            l.addAll(this);
            Collections.sort(l, cmp);
            return l;
        }

    }

}
以下展示了此代码编译(或无法编译)并运行的过程。
> c:\tools\jdk1.7.0_10\bin\javac Sort8.java

> c:\tools\jdk1.7.0_10\bin\java Sort8 this is a test
unsorted: [this, is, a, test]
sorted  : [this, test, is, a]

> c:\tools\jdk1.8.0_05\bin\java Sort8 this is a test
unsorted: [this, is, a, test]
sorted  : [this, test, is, a]

> del Sort8*.class

> c:\tools\jdk1.8.0_05\bin\javac Sort8.java
Sort8.java:46: error: sort(Comparator<? super V>) in SortableList cannot implement sort(Comparator<? super E>) in List
                public SortableList<V> sort(Comparator<? super V> cmp) {
                                       ^
  return type SortableList<V> is not compatible with void
  where V,E are type-variables:
    V extends Object declared in class SortableList
    E extends Object declared in interface List
1 error

3
JDK 1.8是否因默认方法引入Java源代码的向前不兼容性?是的。事实上,它不仅如此,还引入了持续打破向前源代码兼容性的机会。我认为这是一个可取的折衷方案,但我很想知道当初的论据是什么。 - biziclop
6
在修改超类时,此情况以前可能已经发生过。现在默认方法使接口更不容易破坏子类。 - Andy Thomas
4
问题并不完全与默认方法的添加有关。即使它不是默认方法,而只是接口上没有实现的新方法,问题仍然会发生。 - Mark Rotteveel
2
@Andy 很好的观点。我认为我错过了这个事实,因为我倾向于使用接口而不是子类化进行设计和编码。我想这意味着你现在也可以从实现接口中获得基类演变所带来的类名称空间“污染”。 - Paul
16
这个问题以及这里的许多评论的语气有些过分夸张。默认方法带来的所有风险在将新方法引入非最终具体或抽象类中时已经存在。责怪默认方法大多只是打击使者;这是一种一直存在的兼容性风险。 (是的-专家组对此进行了广泛的兼容性后果审议。) - Brian Goetz
显示剩余8条评论
5个回答

59

JDK 1.8是否由于默认方法引入Java源代码的向前不兼容性?

任何超类或接口中的新方法都可能破坏兼容性。默认方法降低了在接口中进行更改会破坏兼容性的可能性。从默认方法打开在接口中添加方法的角度来看,您可以说默认方法可能会对某些兼容性造成破坏。

这是第一个类似的向前不兼容性更改吗?

几乎肯定不是,因为自Java 1.0以来,我们一直在对标准库的类进行子类化。

默认方法设计和实现时是否考虑或讨论了此问题?有记录在哪里吗?

是的,已经考虑过了。请参见Brian Goetz于2010年8月发表的论文"Interface evolution via “public defender” methods"

  1. 源代码兼容性

这种方案可能会引入源代码不兼容性,以至于库接口被修改以插入与现有类中的方法不兼容的新方法。 (例如,如果一个类具有float值的xyz()方法并实现Collection,并且我们向Collection添加int值的xyz()方法,则现有类将不再编译。)

与好处相比,(尽管小)不便是否被折扣?

以前,更改接口将确实破坏兼容性。现在,它可能会。从“肯定”到“可能”可以积极或消极地看待。一方面,它使向接口添加方法成为可能。另一方面,它打开了您所看到的不兼容性种类的大门,不仅是与类而且是与接口。

然而,好处大于不便,正如Goetz论文顶部所述:

  1. 问题陈述

一旦发布接口,就不可能在不破坏现有实现的情况下添加方法。自从一个库发布以来的时间越长,这个限制就越容易给维护者带来困扰。

JDK 7将闭包添加到Java语言中,对老化的集合接口增加了额外的压力。闭包最重要的好处之一是它可以开发更强大的库。如果在同时没有扩展核心库以利用该特性的情况下添加了一种语言功能,那将令人失望。


3
@Paul - 我同意,这是一种危险。然而,有一个中间的方法。以前,谨慎的做法是永远不要更改公共接口。(例如,Bloch在《Effective Java》中说:"一旦一个接口被发布并广泛实现,几乎不可能进行更改。") 现在,我们可以大喊"这是红色警报!" 并开始在公共接口中任意添加默认方法。但我认为谨慎的做法是认识到我们已经从 "会破坏" 变成了 "可能破坏",并在好处超过破坏某些实现的机会时,小心地、不经常地添加默认方法。 - Andy Thomas
2
具有讽刺意味的是,他们必须抵制添加大量有用的默认方法到收集API中的诱惑,以免破坏子类。更好的问题是,为什么他们认为将sort添加到List中是绝对必要的。 - ZhongYu
1
@bayou.io:比较和有条件地交换数组中的两个项往往比从List<T>加载两个项、比较它们,必要时将两个项存回List<T>更快。程序花费大量时间进行排序并不罕见,让List<T>自己排序通常可以比实现需要读写List<T>中项的排序提供50%或更好的加速。 - supercat
1
@bayou.io:虽然我同意在大多数情况下,想要对列表进行排序的代码将知道其确切类型(通常为ArrayList),但有时使用其他形式的后备存储可能会带来相当大的性能优势。例如,如果有一个不可变的列表类型,它可能有用包含一个asMutable方法,该方法返回一个List<T>,它保存对不可变列表的引用以及一些数组,这些数组跟踪已更改的内容。代码不应关心它接收到这样的列表还是ArrayList,但我不知道常见的超类型是什么... - supercat
1
除了接口List<T>之外,这样的列表可能会与ArrayList<T>有用地共享。 - supercat
显示剩余5条评论

10

JDK 1.8是否因默认方法对Java源代码引入了向前不兼容性?

是的,正如您所看到的。

这是第一次出现这样的向前不兼容性变化吗?

不是。Java 5的枚举 (enum)关键字也是具有向前不兼容性的,因为在Java 5之前,您可以使用该关键字命名变量,但在Java 5及以上版本中,它将无法编译。

在设计和实施默认方法时是否考虑或讨论过此事?是否有文档记录?

是的,在Oracle Java 8兼容性指南中有说明。

与收益相比,(尽管很小)的不便是否被折扣计算了?

是的。


@Codebender 没错。我尽量避免通过子类编码 - 大多数情况下我都让自己的类是final的 - 这也可能解释了为什么现在它也适用于接口时我更加注意到它。 - Paul
@Paul 这也总是适用于接口。 - Mark Rotteveel
1
虽然这是真的,但通常JDK的作者们会小心翼翼地避免向接口添加方法以避免这个问题。请参见GraphicsGraphics2D作为解决方案,以向子接口添加方法,以便不影响旧代码。 - dkatzel
@Codebencher。是的,但我认为人们会仔细考虑是否向接口添加方法(默认方法之前)。我不知道我使用过的任何库在主要新版本/重构之外都会这样做。(而类 - 抽象、基类或其他 - 似乎随意增加新方法。) - Paul
@dkatzel 一个众所周知的反例是JDBC - 其中的接口多次添加了新方法。 - Paŭlo Ebermann

3
我们可以将其与抽象类相提并论。抽象类旨在被子类化,以便实现抽象方法。抽象类本身包含调用抽象方法的具体方法。抽象类可以通过添加更多的具体方法来自由演变; 这种做法可能会破坏子类。
因此,即使在Java8之前,您描述的确切问题也存在。问题在于Collection API上更加明显,因为有很多不同的子类。
虽然默认方法的主要动机是在不破坏子类的情况下向现有的Collection API添加一些有用的方法,但他们必须克制自己,以免做得过多而破坏子类。只有在绝对必要的情况下才会添加默认方法。真正的问题在于,为什么List.sort被认为是绝对必要的。我认为这是有争议的。
无论默认方法最初为何引入,它现在是API设计人员的重要工具,我们应该像处理抽象类中的具体方法一样对待它们 - 它们需要事先经过谨慎设计,并且必须非常谨慎地引入新的方法。

1
一定会有一个“闪亮新玩具”的阶段,每个人都会在他们的API中添加默认方法(虽然它们不够完善,但使用它们作为特征非常诱人),然后谨慎开始占据主导地位。希望如此。 :) - biziclop

2
具有讽刺意味的是,接口中的默认方法被引入是为了允许使用这些接口的现有库不会出错,同时在接口中引入了大量新功能(向后兼容)。
可能会出现像“sort”方法这样的冲突。需要为额外的功能付出代价。在您的情况下,也需要进行调查(是否应该使用新功能)。
Java的向前兼容性问题很小,更多的是在其类型系统中,该系统不断扩展。首先是泛型类型,现在是从函数接口推断出的类型。从版本到版本和从编译器到编译器都存在轻微差异。

“向前兼容性破坏很少,更多的在其类型系统中。” 这些问题肯定是向后而不是向前兼容性问题吧? - Paul
@Paul 是的,使用Java 8时会遇到一些编译器问题,但这更多是因为各种编译器成长痛的原因。 - Joop Eggen

0
阅读这个问题,我在思考它的解决方案。 默认方法已经解决了向后兼容性问题,但向前兼容性问题仍然存在。 我认为,在这种情况下,我们可以使用特定于应用程序的接口来添加所需的行为,而不是扩展现有类。我们可以实现这个应用程序特定的接口并使用它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接