Java8实战-总结37

Java8实战-总结37

  • 默认方法
    • 不断演进的 API
      • 初始版本的 API
      • 第二版 API

默认方法

传统上,Java程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。

现实情况是,现存的实体类往往不在接口设计者的控制范围之内,这些实体类为了适配新的接口约定也需要进行修改。由于Java 8API在现存的接口上引入了非常多的新方法,这种变化带来的问题也愈加严重,一个例子就是前面使用过的List接口上的sort方法。像GuavaApache 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,对列表中的元素进行排序。

List<Integer> numbers = Arrays.asList(3, 5, 1, 2, 6); 
numbers.sort(Comparator.naturalOrder()); 

不过除此之外,这段代码中还有些其他的新东西,Comparator.naturalOrder方法。这是Comparator接口的一个全新的静态方法,它返回一个Comparator对象,并按自然序列对其中的元素进行排序(即标准的字母数字方式排序)。

Collection中的stream方法的定义如下:

default Stream<E> stream() { 
	return StreamSupport.stream(spliterator(), false); 
} 

这里stream 方法中调用了SteamSupport.stream方法来返回一个流。

为什么要在乎默认方法?默认方法的主要目标用户是类库的设计者,默认方法的引入就是为了以兼容的方式解决像Java API这样的类库的演进问题的,如下图所示:
Java8实战-总结37_第1张图片
简而言之,向接口添加方法是诸多问题的罪恶之源;一旦接口发生变化,实现这些接口的类往往也需要更新,提供新添方法的实现才能适配接口的变化。如果对接口以及它所有相关的实现有完全的控制,这可能不是个大问题。但是这种情况是极少的。这就是引入默认方法的目的:它让类可以自动地继承接口的一个默认实现。

默认方法为接口的演进提供了一种平滑的方式,你的改动将不会导致已有代码的修改。此外,默认方法为方法的多继承提供了一种更灵活的机制,可以更好地规划代码结构: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方法,再更新SquareRectangle的实现就好了。不过,事情并非如此简单,要考虑已经使用了你接口的用户,他们已经按照自身的需求实现了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) { 
		List<Resizable> resizableShapes = 
		Arrays.asList(new Square(), new Rectangle(), new Ellipse()); 
		Utils.paint(resizableShapes); 
	} 
}
 
public class Utils { 
	public static void paint(List<Resizable> 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);
}

Java8实战-总结37_第2张图片
用户面临的窘境
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程序的影响大体可以分成三种类型的兼容性,分别是:二进制级的兼容、源代
码级的兼容,以及函数行为的兼容。向接口添加新方法是二进制级的兼容,
但最终编译实现接口的类时却会发生编译错误。

二进制级的兼容性表示现有的二进制执行文件能无缝持续链接(包括验证、准备和解析)
和运行。比如,为接口添加一个方法就是二进制级的兼容,这种方式下,如果新添加的方法不
被调用,接口已经实现的方法可以继续运行,不会出现错误。

简单地说,源代码级的兼容性表示引入变化之后,现有的程序依然能成功编译通过。比如,
向接口添加新的方法就不是源码级的兼容,因为遗留代码并没有实现新引入的方法,所以它们
无法顺利通过编译。

最后,函数行为的兼容性表示变更发生之后,程序接受同样的输入能得到同样的结果。比
如,为接口添加新的方法就是函数行为兼容的,因为新添加的方法在程序中并未被调用(抑或
该接口在实现中被覆盖了)。

你可能感兴趣的:(java,开发语言)