《Effective Java》笔记18-22:类和接口
第18条:接口优于抽象类
Java 提供两种机制用来定义允许多个实现的类型:接口和抽象类。
接口的优势
现有的类可以很容易被更新,以实现新的接口。当需要增加方法的时候只需要 implements 具体的接口即可,非常方便,而如果通过抽象类来实现,则需要在抽象类里新增方法,而这会导致其他继承该抽象类的类也被强制加上额外的方法!
接口是定义mixin(混合类型)的理想选择。mixin 是指主要的类型:类除了实现它的"基本类型"之外,还可以实现 mixin 类型(利用实现多个接口可以达到混合类型的目的,而利用抽象类只能继承一个类,则不能达到混合类型的效果)
接口允许我们构造非层次结构的类型框架。类型层次对于组织某些事物是非常合适的,但是其他有些事物并不能被整齐地组织成一个严格的层次结构。例如,假设我们有一个接口代表一个 Singer(歌唱家),另一个接口代表一个 SongWriter(作曲家)。
骨架实现
在 Java 的 Collections Framework 中存在一组被称为"骨架实现"(skeletal implementation)的抽象类,如 AbstractCollection、AbstractSet 和 AbstractList 等。如果设计得当,骨架实现可以使程序员很容易的提供他们自己的接口实现。这种组合还可以让我们在设计自己的类时,根据实际情况选择是直接实现接口,还是扩展该抽象类。
和接口相比,骨架实现类还存在一个非常明显的优势,既如果今后为该骨架实现类提供新的方法,并提供了默认的实现,那么他的所有子类均不会受到影响,而接口则不同,由于接口不能提供任何方法实现,因此他所有的实现类必须进行修改,为接口中新增的方法提供自己的实现,否则将无法通过编译。
简而言之,接口通常是定义允许多个实现的类型的最佳途径。这条规则有个例外,即当演变的容易性比灵活性更为重要的时候。在这种情况下,应该使用抽象类来定义类型,但前提是必须理解并且可以接受这些局限性。如果你导出了一个重要的接口,就应该坚决考虑同时提供骨架实现类。
第19条:接口只用于定义类型
有一种接口称为常量接口,这种接口不包含任何方法,它只包含静态的 final 域,每个域都导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名:
public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.02214199e23;
}
常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄露到该类的导出 API 中。类实现常量接口没有什么价值。如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保二制兼容性。如果非 final 类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。Java 平台类库中有几个常量接口,例如 java.io.ObjectStreamConstants,被认为是反面例子,不值得效仿。
如果要导出常量,可以有几种合理的方案。
- 如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口。例如,在 Java 平台类库中所有的数值包装类,如 Integer 和 Double,都导出了 MIN_VALUE 和 MAX_VALUE 常量。
- 如果这些常量最好被看作枚举类型的成员,就应该使用枚举类型(见第30条)来导出这些常量。
- 否则,应该使用不可实例化的工具类(见第4条)来导出这些常量,下面是前面的 PhysicalConstants 例子的工具类翻版:
public class PhysicalConstants {
private PhysicalConstants() { } // 私有构造器
public static final double AVOGADROS_NUMBER = 6.02214199e23;
}
简而言之,接口应该只被用来定义类型,它们不应该被用来导出常量。
第20条:类层次优于标签类
有时候,可能会遇到带有两种甚至更多风格(功能)的实例的类,并包含表示实例风格的标签域,例如:
class Figure {
enum Shape {RECTANGLE, CIRCLE}
// 标签域 - 是圆形还是长方形
final Shape shape;
// 长方形
double length;
double width;
// 圆形
double radius;
// 圆形构造器
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 长方形构造器
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
// 面积
double area() {
switch (shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
标签类的缺点非常明显,当你要表现的风格非常多样的时候,你需要写大量的判断语句,非常容易出错,而且当你需要修改某一个风格的时候,你需要在一大堆代码里找出你要改的地方,很有可能引入 bug,非常难以维护。
这个时候,将标签类转变成类层次就非常方便了:
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
private final double radius;
Circle(double radius) {
this.radius = radius;
}
double area() {
return Math.PI * (radius * radius);
}
}
class Rectangle extends Figure {
private final double length;
private final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() {
return length * width;
}
}
类层次的另一好处在于,它们可以用来反映类之间本质上的层次关系,有助于后期的扩充。假设现有加一个正方形有,标签类就需要修改源码,而利用类层次结构只需新加一个正方形类,并继承自长方形,
第21条:用函数对象表示策略
Java 没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象(that Object)上的某项操作。然而,我们也可能定义这样一种对象,它的方法执行其他对象(other Objects)上的操作。如果一个类仅仅导出这样的一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(Function Object),如 JDK 中 Comparator,我们可以将该对象看做是实现两个对象之间进行比较的"具体策略对象",如:
public class StringLengthComparator {
public int comare(String s1, String s2) {
return s1.length() - s2.length();
}
}
作为典型的具体策略类,StringLengthComparator 类是无状态的:它没有域,所以,这个类的所有实例在功能上都是相互等价的。因此,它作一个 Singleton 是非常合适的,可以节省不必要的对象创建开销(见第3与第5条):
public class StringLengthComparator {
private StringLengthComparator(){}
public static final StringLengthComparator INSTANCE = new StringLengthComparator();
public int comare(String s1, String s2) {
return s1.length() - s2.length();
}
}
为了把 StringLengthComparator 实例传递给方法,需要适当的参数类型,直接使用 StringLengthComparator 并不好,因为客户端将无法传递任何其他的比较策略,即不能随时动态的改变比较性为。此时,我们可以定义一个比较策略接口,这个接口在 java.util.Comparator 就已提供。
使用匿名类
具体的策略类往往使用匿名类(见第22条)来声明,下面的语句根据长度对一个字符串数组进行排序:
Arrays.sort(strArr, new Comparator<String>(){
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}});
但是注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑将函数对象存储到一个私有的静态 final 域时重复使用它。
使用私有内部类
下面的例子使用静态成员类,而不是匿名类,这样允许我们的具体的策略类实现第二个接口 Serializable:
public class Outer {
private static class StrLenCmp implements Comparator<String>, Serializable {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
public static final Comparator<String> STRING_LEN_CMP = new StrLenCmp();
//...
}
String 类利用这种模式,通过它的 CASE_INSENSITIVE_ORDER 域,实现了一个不区分大小写的字符串比较器。
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// use serialVersionUID from JDK 1.2.2 for interoperability
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
/** Replaces the de-serialized object. */
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
总结
简而言之,函数的主要用途就是实现策略模式。为了在 Java 中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的策略类。
当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体匿名策略类。
当一个具体策略是设计用来重复使用的时候,它的类通常要被实现为私有的静态成员类,并通过公有的静态 final 域被导出,其类型为该策略接口。
第22条:优先考虑静态成员类(四种嵌套类中)
嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为了它的外围类提供服务。
静态成员类
不称之为内部类(inner class)。可以看为普通的类,可以访问外围类的所有成员,和其他的静态成员一样,也遵循同样的可访问行规则(如果为私有,则只能在外围类的内部被访问)
如果嵌套类的实例可以在它外围类的实例之外独立存在,这个嵌套类就必须是静态成员类:在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
非静态成员类
非静态成员类的实例都隐含持有一个外部类的实例(enclosing instance)
这不仅仅会消耗更多的空间,还可能会导致外部类的实例泄漏,内存泄漏,而静态成员类并不会。
匿名类
不能拥有任何静态成员,通常用于创建函数对象(见21条),比如 Thread,Runnable
局部类
最少使用的类,在任何"可以声明局部变量"的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。
总结
如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合于放在方法内部,就该使用成员类。
如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的,否则做成静态的。
假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型接口可以说明这个类的特征,就要把它做成匿名类,否则,就做成局部类吧。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com
文章标题:《Effective Java》笔记18-22:类和接口
文章字数:3k
本文作者:Bin
发布时间:2018-10-12, 21:06:22
最后更新:2019-08-06, 00:43:14
原始链接:http://coolview.github.io/2018/10/12/Effective-Java/%E3%80%8AEffective%20Java%E3%80%8B%E7%AC%94%E8%AE%B018-22%EF%BC%9A%E7%B1%BB%E5%92%8C%E6%8E%A5%E5%8F%A3/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。