【Effective Java中文版】第二版:第四章 类和接口[005:19~21] [20180115]
2018-01-27 15:57
239 查看
第19条:接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了这个接口,就表名客户端可以对这个实例实施某些动作。为了任何其它目的的而定义接口是不恰当的。
有一种接口叫做常亮接口(constant interface),它不满足上面的 条件。这种接口没有包含任何方法,它只包含静态的final域,每个域导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。
public interface PysicalConstants { static final double AVG_NUMBER = 6.022141; static final double SUM_NUMBER = 2.430211; }
常量接口模式是对接口的不良使用,类在内部使用某些常量,这纯粹是细节实现。实现常量接口,会导致把这样的实现细节泄露到该类的导出API中。类实现常量接口,这对于这个类的用户讲并没有什么价值。实际上,这样做反而会使他们更加糊涂。
工具类通常要求客户端要用类名来修饰这些常量名,例如PhysicalConstants.AVOGADROS_NUMBER。如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量名,不过静态导出入1.5中才引入的:
import static com.effectivejava.science.PhysicalConstans.*
简而言之,接口应该只被用来定义类型,它们不应该被用来导出常量。
第20条:类层次优于标签类
有时候,可能会遇到带有两种甚至更多种风格的实例的类,并包含表示实例风格的标签域。例如,考虑下面这个类,它能够表示圆形或者矩形:
public class Figure { enum Shape{RECTANGLE, CIRCLE}; final Shape shape; double length; double width; double redius; Figure(double redius){ shape = Shape.CIRCLE; this.redius = redius; } 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 * (redius * redius); default: throw new AssertionError(); } } }
这种标签类(tagged class)有着许多缺点。他们中充斥这样板代码,包括声明枚举,标签域以及条件语句。由于多个实现路七八糟地挤在了单个类中,破坏了可读性。内存占用也增加了,因为实例承担着属于其它风格的不相关的域。域不能做成是final的,除非构造器初始化了不相关的域,产生更多的样板代码。构造器必须不借助编译器,来设置标签域,并初始化正确的数据域:如果初始化了错误的域,程序就会在运行时失败。无法给标签类添加风格,除非可以修改它的源文件。如果一定要添加风格,就必须记得给每个添加语句都添加一个条件,否则类就会在运行时失败。最后,实例的数据类型没有提供任何关于其风格的线索。一句话,标签过于冗长,容易出错,并且效率低下。
幸运的是,面向对象的语言例如Java,就提供了其他更好的方法来定义能表示多种风格对象的单个数据类型:子类型话(subtyping)。标签类正是类层次的一种简单的效仿。
public abstract class Figure1 { abstract double area(); }
public class Circle extends Figure1 { final double redius; public Circle(double redius) { super(); this.redius = redius; } @Override double area() { return Math.PI * (redius * redius); } }
public class Rectangle extends Figure1{ final double length; final double width; public Rectangle(double length, double width) { super(); this.length = length; this.width = width; } @Override double area() { return length * width; } }
这个类层次纠正了前面提到过的标签类的所有缺点,这段代码简单且清除,没有包含在原来的版本中所见到的所有样板代码。每个类型的实现都配有自己的类,这些类都没有收到不相关的数据域的拖累。所有的域都是final的。编译器确保每个类的构造器初始化它的数据域,对于根类中声明的每个抽象方法,都确保有一个实现。这样就杜绝了由于遗漏switch
case而导致运行时失败的可能性。
类层次的另一种好处在于,他们可以用来反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。假设上述例子中的标签类也允许表达正方形。类层次可以反映出正方形是一种特殊的矩形这一事实。
第21条:用函数对象表示策略有些语言支持函数指针(function pointer),代理(delegate),lambda表达式(lambda expression),或者支持类似的机制,允许
程序把"调用特殊函数的能力"存储起来并传递这种能力。这种机制常用于允许函数的调用者通过传入第二个函数,来指定自己的行为
。例如C语言标准库中的qsort函数要求一个指向comparator函数的指针作为参数,他用这个函数来比较排序的元素。比较器函数
有两个参数,都是指向元素的指针。如果第一个参数所指的元素小于第二个参数所指的元素,则返回一个负整数;如果两个元素
相等则返回0,如果一个参数所指的元素大于第二个参数所指的元素,则返回正整数。通过传递不同的比较器函数,就可以获得各种
不同的排列顺序。这正是策略(Strategy)模式的一个例子。比较器函数代表一种为元素排序的策略。
java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象(that object)上的某些
操作,然后,我们也可能定义这样一种对象,它的方法执行其它对象(这些对象被显示传递给这些方法)上的操作。如果一个类
仅仅导出这样的一个方法,他的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象(function object)。例如,
考虑下面的类:
class StringLengthComparator { public int compare(String s1, String s2) { return s1.length() - s2.length(); } }
这个类导出一个带两个字符串参数的方法,如果第一个字符串的长度比第二个短,则返回负整数.....。
这个方法是一个比较器 ,它根据长度来给字符串排序,而不是个根据常用的字典排序。指向
StringLengthComaprater对象的引用可以被当成是一个指向该比较器的函数指针,可以在任意一
对字符串上被调用。换句话说,StringLengthComaprater是用于字符串比较操作的具体策略作为
典型的具体策略类,StringLengthComparator类是无状态的。它没有域,所以,这个类的所有实例在
功能上都是相互等价的。因此,它作为一个singleton是非常合适的,可以节省不必要的对象创建开销。
class StringLengthComparator { private StringLengthComparator() {} public static final StringLengthComparator INSTANCE = new StringLengthComparator(); public int compare(String s1, String s2) { return s1.length() - s2.length(); } }为了把StringLengthComparator实例传递给方法,需要适当的参数类型。使用StringLengthComparator并不好,因为
客户端无法传递任何其它的比较策略。相反,我们需要定义一个Comparator接口,并修改StringLengthComparator来实现
这个接口。换句话说,我们在设计具体的策略类时,还需要定义一个策略接口(strategy interface),如下所示:
@FunctionalInterface public interface Comparator<T> { int compare(T o1, T o2); }Comparator接口这个定义碰巧也出现在java.util包中,但这并不神奇:你自己也完全可以定义它。Comparator接口是泛型的,因此它
适合作为除字符串以外的其它对象的比较器。它的compare方法的两个参数类型为T(它正常的类型参数),而不是String。只要声明
前面所示的StringLengthComparator类要这么做,就可以用它来实现Comparator<String>接口。
具体的策略类往往使用匿名类声明。下面的语句根据长度对一个字符串数组进行排序:
Arrays.sort(new String[] {""}, new Comparator<String>() { public int compare(String o1, String o2) { return o1.length() - o2.length(); } });但是注意,以这种方式使用匿名类时,将会在每次执行调用的时候创建一个新的实例。如果它被重复执行,考虑讲函数对象存储到一个私
有的静态final域里,并重用它。这样做的另一种好处是,可以为这个函数对象取一个有意义的域名称。
相关文章推荐
- 【Effective Java中文版】第二版:第四章 类和接口[002] [20180112]
- 【Effective Java中文版】第二版:第四章 类和接口[001] [20180111]
- 《Effective java》笔记(第二版) --第四章(17-19)
- 【Effective Java中文版】第二版:第四章 类和接口[003] [20180113]
- 【Effective Java中文版】第二版:第四章 类和接口[004] [20180114]
- Effective C++ 第二版 20)避免接口数据 21)使用const
- Effective C++ 第二版 17)operator=检查自己 18)接口完整 19)成员和友元函数
- Effective C++ 第二版 17)operator=检查自己 18)接口完整 19)成员和友元函数
- 输出1/3‐3/5+5/7‐7/9…+19/21 的结果
- 1/3-3/5+5/7-7/9...+19/21的运算
- 《你必须知道的.net》读书笔记 005——1.5 玩转接口
- 求1/3-3/5+5/7-7/9…+19/21的结果
- 求1/3-3/5+5/7...-19/21的和
- GradleUserGuide中文版 19)Plugins 20)插件规范 21)Java插件
- 《Python核心编程》第二版第75页第四章练习
- (19):接口只用于定义类型
- 《Python核心编程》第二版第四章练习
- 深入理解JavaScript系列(21):S.O.L.I.D五大原则之接口隔离原则ISP
- 王爽《汇编语言》(第二版) 学习笔记 (第四章 第一个程序 )
- 输出1/3-3/5+5/7-7/9...+19/21