Java8实战-总结38

默认方法

概述默认方法

默认方法是Java 8中引入的一个新特性,希望能借此以兼容的方式改进API。现在,接口包含的方法签名在它的实现类中也可以不提供实现。缺失的方法实现会作为接口的一部分由实现类继承(所以命名为默认实现),而无需由实现类提供。

那么,该如何辨识哪些是默认方法呢?其实非常简单。默认方法由default修饰符修饰,并像类中声明的其他方法一样包含方法体。比如,可以像下面这样在集合库中定义一个名为Sized的接口,在其中定义一个抽象方法size,以及一个默认方法isEmpty

public interface Sized { 
	int size(); 
	
	//默认方法
	default boolean isEmpty() { 
		return size() == 0; 
	} 
} 

这样任何一个实现了Sized接口的类都会自动继承isEmpty的实现。因此,向提供了默认实现的接口添加方法就不是源码兼容的。

现在,回顾一下最初的例子,那个Java画图类库和你的游戏程序。具体来说,为了以兼容的方式改进这个库(即使用该库的用户不需要修改他们实现了Resizable的类),可以使用默认方法,提供setRelativeSize的默认实现:

default void setRelativeSize(int wFactor, int hFactor) { 
	setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor); 
} 

默认方法在Java 8API中已经大量地使用了。Collection接口的stream方法就是默认方法。List接口的sort方法也是默认方法。很多函数式接口,比如Predicate、Function以及Comparator也引入了新的默认方法,比如Predicate.and或者Function.andThen(记住,函数式接口只包含一个抽象方法,默认方法是种非抽象方法)。

Java 8中的抽象类和抽象接口
那么抽象类和抽象接口之间的区别是什么呢?它们不都能包含抽象方法和包含方法体的
实现吗?
首先,一个类只能继承一个抽象类,但是一个类可以实现多个接口。
其次,一个抽象类可以通过实例变量(字段)保存一个通用状态,而接口是不能有实例变
量的。

默认方法的使用模式

默认方法有两种用例:可选方法和行为的多继承。

可选方法

可能碰到过这种情况,类实现了接口,不过却刻意地将一些方法的实现留白。以Iterator接口为例来说。Iterator接口定义了hasNext、next,还定义了remove方法。Java 8之前,由于用户通常不会使用该方法,remove方法常被忽略。因此,实现Iterator接口的类通常会为remove方法放置一个空的实现,这些都是些毫无用处的模板代码。

采用默认方法之后,可以为这种类型的方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法。比如,在Java 8中,Iterator接口就为remove方法提供了一个默认实现,如下所示:

interface Iterator<T> { 
	boolean hasNext(); 
	T next(); 
	default void remove() {  
		throw new UnsupportedOperationException(); 
	} 
} 

通过这种方式,可以减少无效的模板代码。实现Iterator接口的每一个类都不需要再声明一个空的remove方法了,因为它现在已经有一个默认的实现。

行为的多继承

默认方法让之前无法想象的事儿以一种优雅的方式得以实现,即行为的多继承。这是一种让类从多个来源重用代码的能力,如下图所示:
在这里插入图片描述
Java的类只能继承单一的类,但是一个类可以实现多接口。要确认也很简单,下面是Java API中对ArrayList类的定义:

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable, Iterable<E>, Collection<E> { 
//继承唯一一个类,但是实现了六个接口
} 
  1. 类型的多继承
    这个例子中ArrayList继承了一个类,实现了六个接口。因此ArrayList实际是七个类型的直接子类,分别是:AbstractList、List、RandomAccess、Cloneable、Serializable、IterableCollection。所以,在某种程度上,早就有了类型的多继承。

由于Java 8中接口方法可以包含实现,类可以从多个接口中继承它们的行为(即实现的代码)。从一个例子入手,看看如何充分利用这种能力来为我们服务。保持接口的精致性和正交性能帮助你在现有的代码基上最大程度地实现代码复用和行为组合。

  1. 利用正交方法的精简接口
    假设需要为你正在创建的游戏定义多个具有不同特质的形状。有的形状需要调整大小,但是不需要有旋转的功能;有的需要能旋转和移动,但是不需要调整大小。这种情况下,怎么设计才能尽可能地重用代码?

可以定义一个单独的Rotatable接口,并提供两个抽象方法setRotationAnglegetRotationAngle,如下所示:

public interface Rotatable { 
	void setRotationAngle(int angleInDegrees); 
	int getRotationAngle(); 
	
	//rotateBy方法的一个默认实现
	default void rotateBy(int angleInDegrees) { 
		setRotationAngle((getRotationAngle () + angle) % 360); 
	} 
} 

这种方式和模板设计模式有些相似,都是以其他方法需要实现的方法定义好框架算法。
现在,实现了Rotatable的所有类都需要提供setRotationAnglegetRotationAngle的实现,但与此同时它们也会天然地继承rotateBy的默认实现。

类似地,可以定义之前看到的两个接口MoveableResizable。它们都包含了默认实现。下面是Moveable的代码:

public interface Moveable { 
	int getX(); 
	int getY(); 
	void setX(int x); 
	void setY(int y); 
	
	default void moveHorizontally(int distance){ 
		setX(getX() + distance); 
	} 
	
	default void moveVertically(int distance){ 
		setY(getY() + distance); 
	} 
} 

下面是Resizable的代码:

public interface Resizable { 
	int getWidth(); 
	int getHeight(); 
	void setWidth(int width); 
	void setHeight(int height); 
	void setAbsoluteSize(int width, int height); 
	
	default void setRelativeSize(int wFactor, int hFactor) { 
		setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor); 
	} 
} 
  1. 组合接口
    通过组合这些接口,现在可以为游戏创建不同的实体类。比如,Monster可以移动、旋转和缩放。
public class Monster implements Rotatable, Moveable, Resizable { 
//需要给出所有抽象方法的实现,但无需重复实现默认方法} 

Monster类会自动继承Rotatable、MoveableResizable接口的默认方法。这个例子中,Monster继承了rotateBy、moveHorizontally、moveVerticallysetRelativeSize的实现。

现在可以直接调用不同的方法:

Monster m = new Monster(); //构造函数会设置Monster的坐标、高度、宽度及默认仰角
m.rotateBy(180); //调用由 Rotatable 中继承而来的rotateBy
m.moveVertically(10); //调用由Moveable中继承而来的moveVertically

假设现在需要声明另一个类,它要能移动和旋转,但是不能缩放,比如说Sun。这时也无需复制粘贴代码,可以像下面这样复用MoveableRotatable接口的默认实现。下图是这一场景的UML图表。

public class Sun implements Moveable, Rotatable {}

在这里插入图片描述
像游戏代码那样使用默认实现来定义简单的接口还有另一个好处。假设需要修改moveVertically的实现,让它更高效地运行。可以在Moveable接口内直接修改它的实现,所有实现该接口的类会自动继承新的代码(这里我们假设用户并未定义自己的方法实现)。

关于继承的一些错误观点
继承不应该成为你一谈到代码复用就试图倚靠的万精油。比如,从一个拥有100个方法及
字段的类进行继承就不是个好主意,因为这其实会引入不必要的复杂性。完全可以使用代理
有效地规避这种窘境,即创建一个方法通过该类的成员变量直接调用该类的方法。这就是为什
么有的时候我们发现有些类被刻意地声明为final类型:声明为final的类不能被其他的类继
承,避免发生这样的反模式,防止核心代码的功能被污染。注意,有的时候声明为final的类
都会有其不同的原因,比如,String类被声明为final,因为我们不希望有人对这样的核心
功能产生干扰。
这种思想同样也适用于使用默认方法的接口。通过精简的接口,你能获得最有效的组合,
因为你可以只选择你需要的实现。	
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值