网站结构优化的内容和方法,网站建设项目收获,长沙网站seo技术厂家,网站开发项目项目运营Java8实战-总结37 默认方法不断演进的 API初始版本的 API第二版 API 默认方法
传统上#xff0c;Java程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现#xff0c;或者从父类中继承它的实现。但是#xff0c;一旦类库… Java8实战-总结37 默认方法不断演进的 API初始版本的 API第二版 API 默认方法
传统上Java程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现或者从父类中继承它的实现。但是一旦类库的设计者需要更新接口向其中加入新的方法这种方式就会出现问题。
现实情况是现存的实体类往往不在接口设计者的控制范围之内这些实体类为了适配新的接口约定也需要进行修改。由于Java 8的API在现存的接口上引入了非常多的新方法这种变化带来的问题也愈加严重一个例子就是前面使用过的List接口上的sort方法。像Guava和Apache Commons这样的框架现在都需要修改实现了List接口的所有类为其添加sort方法的实现。
Java 8为了解决这一问题引入了一种新的机制。Java 8中的接口现在支持在声明方法的同时提供实现通过两种方式可以完成这种操作。其一Java 8允许在接口内声明静态方法。其二Java 8引入了一个新功能叫默认方法通过默认方法可以指定接口方法的默认实现。换句话说接口能提供方法的具体实现。因此实现接口的类如果不显式地提供该方法的具体实现就会自动继承默认的实现。这种机制可以使你平滑地进行接口的优化和演进。实际上到目前为止已经使用了多个默认方法。两个例子就是你前面已经见过的List接口中的sort以及Collection接口中的stream。
List接口中的sort方法是Java 8中全新的方法它的定义如下
default void sort(Comparator? super E c) { Collections.sort(this, c);
} 注意返回类型之前的新default修饰符。通过default修饰符能够知道一个方法是否为默认方法。这里sort方法调用了Collections.sort方法进行排序操作。由于有了这个新的方法现在可以直接通过调用sort对列表中的元素进行排序。
ListInteger numbers Arrays.asList(3, 5, 1, 2, 6);
numbers.sort(Comparator.naturalOrder()); 不过除此之外这段代码中还有些其他的新东西Comparator.naturalOrder方法。这是Comparator接口的一个全新的静态方法它返回一个Comparator对象并按自然序列对其中的元素进行排序即标准的字母数字方式排序。
Collection中的stream方法的定义如下
default StreamE stream() { return StreamSupport.stream(spliterator(), false);
} 这里stream 方法中调用了SteamSupport.stream方法来返回一个流。
为什么要在乎默认方法默认方法的主要目标用户是类库的设计者默认方法的引入就是为了以兼容的方式解决像Java API这样的类库的演进问题的如下图所示 简而言之向接口添加方法是诸多问题的罪恶之源一旦接口发生变化实现这些接口的类往往也需要更新提供新添方法的实现才能适配接口的变化。如果对接口以及它所有相关的实现有完全的控制这可能不是个大问题。但是这种情况是极少的。这就是引入默认方法的目的它让类可以自动地继承接口的一个默认实现。
默认方法为接口的演进提供了一种平滑的方式你的改动将不会导致已有代码的修改。此外默认方法为方法的多继承提供了一种更灵活的机制可以更好地规划代码结构Java 8类可以从多个接口继承默认方法。
静态方法及接口
同时定义接口以及工具辅助类companion class是Java语言常用的一种模式工具类定
义了与接口实例协作的很多静态方法。比如Collections就是处理Collection对象的辅
助类。由于静态方法可以存在于接口内部你代码中的这些辅助类就没有了存在的必要你可
以把这些静态方法转移到接口内部。为了保持后向的兼容性这些类依然会存在于Java应用程
序的接口之中。不断演进的 API
假设你是一个流行Java绘图库的设计者。库中包含了一个Resizable接口它定义了一个简单的可缩放形状必须支持的很多方法 比如setHeight、setWidth、getHeight、getWidth以及setAbsoluteSize。此外还提供了几个额外的实现out-of-box implementation如正方形、长方形。由于这个库非常流行一些用户使用Resizable接口创建了他们自己感兴趣的实现比如椭圆。
发布API几个月之后你突然意识到Resizable接口遗漏了一些功能。比如如果接口提供一个setRelativeSize方法可以接受参数实现对形状的大小进行调整那么接口的易用性会更好。这看起来很容易为Resizable接口添加setRelativeSize方法再更新Square和Rectangle的实现就好了。不过事情并非如此简单要考虑已经使用了你接口的用户他们已经按照自身的需求实现了Resizable接口他们该如何应对这样的变更呢非常不幸你无法访问也无法改动他们实现了Resizable接口的类。这也是Java库的设计者需要改进Java API时面对的问题。以一个具体的实例为例深入探讨修改一个已发布接口的种种后果。
初始版本的 API
Resizable接口的最初版本提供了下面这些方法
public interface Resizable extends Drawable { int getWidth(); int getHeight(); void setWidth(int width); void setHeight(int height); void setAbsoluteSize(int width, int height);
} 用户实现 一位用户根据自身的需求实现了Resizable接口创建了Ellipse类
public class Ellipse implements Resizable { …
} 他实现了一个处理各种Resizable形状包括Ellipse的游戏
public class Game { public static void main(String...args) { ListResizable resizableShapes Arrays.asList(new Square(), new Rectangle(), new Ellipse()); Utils.paint(resizableShapes); }
}public class Utils { public static void paint(ListResizable l) { l.forEach(r - { r.setAbsoluteSize(42, 42); r.draw(); }); }
} 第二版 API
库上线使用几个月之后你收到很多请求要求你更新Resizable的实现让Square、Rectangle以及其他的形状都能支持setRelativeSize方法。为了满足这些新的需求你发布了第二版API具体如下图所示
public interface Resizable { int getWidth(); int getHeight(); void setWidth(int width);void setHeight(int height); void setAbsoluteSize(int width, int height); void setRelativeSize(int wFactor, int hFactor);
}用户面临的窘境 对Resizable接口的更新导致了一系列的问题。首先接口现在要求它所有的实现类添加setRelativeSize方法的实现。但是用户最初实现的Ellipse类并未包含setRelativeSize方法。向接口添加新方法是二进制兼容的这意味着如果不重新编译该类即使不实现新的方法现有类的实现依旧可以运行。不过用户可能修改他的游戏在他的Utils.paint方法中调用setRelativeSize方法因为paint方法接受一个Resizable对象列表作为参数。如果传递的是一个Ellipse对象程序就会抛出一个运行时错误因为它并未实现setRelativeSize方法
Exception in thread main java.lang.AbstractMethodError: lambdasinaction.chap9.Ellipse.setRelativeSize(II)V 其次如果用户试图重新编译整个应用包括Ellipse类他会遭遇下面的编译错误
lambdasinaction/chap9/Ellipse.java:6: error: Ellipse is not abstract and does not override abstract method setRelativeSize(int,int) in Resizable 最后更新已发布API会导致后向兼容性问题。这就是为什么对现存API的演进比如官方发布的Java Collection API会给用户带来麻烦。当然还有其他方式能够实现对API的改进但是都不是明智的选择。比如可以为你的API创建不同的发布版本同时维护老版本和新版本但这是非常费时费力的原因如下。 其一这增加了你作为类库的设计者维护类库的复杂度。其次类库的用户不得不同时使用一套代码的两个版本而这会增大内存的消耗延长程序的载入时间因为这种方式下项目使用的类文件数量更多了。
这就是默认方法试图解决的问题。它让类库的设计者放心地改进应用程序接口无需担忧对遗留代码的影响这是因为实现更新接口的类现在会自动继承一个默认的方法实现。
不同类型的兼容性二进制、源代码和函数行为
变更对Java程序的影响大体可以分成三种类型的兼容性分别是二进制级的兼容、源代
码级的兼容以及函数行为的兼容。向接口添加新方法是二进制级的兼容
但最终编译实现接口的类时却会发生编译错误。二进制级的兼容性表示现有的二进制执行文件能无缝持续链接包括验证、准备和解析
和运行。比如为接口添加一个方法就是二进制级的兼容这种方式下如果新添加的方法不
被调用接口已经实现的方法可以继续运行不会出现错误。简单地说源代码级的兼容性表示引入变化之后现有的程序依然能成功编译通过。比如
向接口添加新的方法就不是源码级的兼容因为遗留代码并没有实现新引入的方法所以它们
无法顺利通过编译。最后函数行为的兼容性表示变更发生之后程序接受同样的输入能得到同样的结果。比
如为接口添加新的方法就是函数行为兼容的因为新添加的方法在程序中并未被调用抑或
该接口在实现中被覆盖了。