上周因为准备考试等一堆原因,没空看书,今天补上一点。
类和接口是java程序设计语言的核心,它们也是java语言的基本抽象单元,java语言提供了很多强大的基本元素,供程序员设计类和接口,这一章讲的是一些指导原则,可以设计出更加有用,健壮和灵活的类和接口。
第1条:使类和成员的可访问性最小化
首先说一个概念:模块之间只能通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念叫做“信息隐藏”,或者“封装”。(对,这就是面向对象的中封装继承多态三大特性之一的封装)
信息隐藏之所以重要,是因为:它可以有效的解除系统各模块之间的耦合关系,使得这些模块可以独立的开发,测试,优化,使用,理解和修改。这样可以加快系统开发的速度,同时也减轻了维护的负担。
Java语言提供了许多机制来协助信息隐藏。访问控制机制决定了类,接口和成员的可访问性。实体的可访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符(private,protected,public)共同决定的。第一规则是:尽可能使每个类或者成员不被外界访问。
对于成员,有4种访问级别,按照可访问性递增顺序排列:
private: 只在声明该成员的顶层 类内部才可以访问。
package private: 声明该成员的包内部的任何类都可以访问该成员,是缺省的访问级别(即如果没指定访问级别,就默认是package private的。)
protected:声明该成员的类的子类可以访问该成员,并且,声明该成员的包内部的任何类都可以访问。
public:在任何地方都可以访问该成员。
在判定一个成员的访问级别时,不仅要考虑我们想要这个成员暴露的程度,还有一些注意事项:
1.实例域绝对不能是public的。一旦使实例域成为共有,就放弃了读存储在这个域中的值进行限制的能力。或者说,对于一个实例域来说,它自己内部的属性应该由它自己掌控,但是如果它是Public的,那么所有人都可以掌控它的内部的值,可以拿两个国家来想象,如果一个国家内部的事务被另一个国家插手了,这感觉好丧权辱国。。。所以,对象也是有自尊的!绝对不能把它声明成Public。
2.对于final域,如果final修饰的是常量的话,可以通过public static final 来暴露这些常量。但如果final修饰的是引用的话,就不能用Public了,因为final的本意是不能修改,虽然引用的本身不会改变,但是它指向的对象却可能发生变化,这就违背了final的本意。
3.注意,长度非0的数组总是可变的,所以,类具有public static final的数组域,或是返回这种数组域的访问方法,这总是错误的。因为这样客户端能够修改数组中的内容,这是安全漏洞的一个常见原因。例如:
public static fnal Thing[] VALUES= {...}; //这样是错的
可以这样改进:
private static final Thing[] PRIVATE_VALUES={...}; public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
或者这样:
private static final Thing[] PRIVATE_VALUES={...}; public static final Thing[] values(){ return PRIVATE_VALUES.clone(); }
第2条:在共有类中使用访问方法而非共有域
这一条很简单,就是尽量使用getter和setter方法来访问共有类中的数据,而不是让数据自己public。例子:
class Point{ //这是不好的写法 public double x; public double y; }
class Point{ //这是较好的写法,使用访问方法访问共有域 private double x; private double y; public Point(double x,double y){ this.x = x; this.y = y; } public double getX(){ return x;} public double getY(){ return y;} public void setX(double x){this.x = x;} public void setY(double y){ this.y = y;} }
第3条:使可变性最小化
首先介绍不可变类:不可变类是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。Java中有许多不可变的类,比如String,BigInteger,BigDecimal。不可变类比可变类更易于设计,实现和使用,它们不容易出错,而且更加安全。
设计不可变类有下面5个原则:
1.不会提供任何会修改对象状态的方法。
2.保证类不会被扩展。(可以声明为final)
3.使所有域都是final的。
4.使所有的域都是私有的。这样可以防止客户端获得被访问域的引用的可变对象的权限,并防止客户端直接修改这些对象。
5.确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用,并且,永远不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用。(比如你家养了条狗,你肯定不会希望别人可以随随便便就把它牵走吧,或者是你出门上班,走的时候家里是条萨摩耶,回来时候变成哈士奇了,请别哭晕在厕所……)
这里有一个不可变类的例子,复数,但是太长了我懒得打……好吧,来个一小部分
public final class Complex{ private final double re; private final double im; public Complex(double re,double im){...} public double realPart(){return re;} public double imaginaryPart(){return im;}//这里是第一条,不会返回修改的方法 public Complex add(Complex c){ return new Complex(re+c.re,im+c.im); } public Complex subtract(Complex c){ return new Complex(re-c.re,im-c.im); } ... @Override public boolean equals(Object o){...} @Override public int hashCode(){...} @Override public String toString(){...}
那么,这个类就在这里啦,注意看这里的基本算数运算:加减乘除。它们创建并返回新的实例,而不是修改当前实例,这种方法被称作functional方法,因为这些方法返回一个函数的结果,这些函数对操作数进行运算但并不修改它。与之对应的是“过程的procedural”方法,会导致操作数的状态发生改变。
不可变对象本质上是线程安全的,他们不要求同步,所以,不可变对象可以被自由的共享。不仅尅共享不可变对象,甚至也可以共享它们的内部信息。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。创建这种对象的代价可能很高。如果你执行一个需要很多步骤的操作,每个步骤会产生一个新的对象,但是我们只用最后的结果,其他对象最终都会被丢弃,这时会产生性能问题。处理这个问题有2种方法:1.猜测常用的操作,把它们作为i基本类型提供。2.提供一个共有的可变配套类。比如String和StringBuilder。
如果类不能被做成是不可变的,仍然应该尽可能的限制它的可变性。因此,除非有令人信服的理由要让域变成非final ,否则要让每个域都是final的。
第4条:复合优先于继承
与方法调用不同的是,继承打破了封装性,换句话说,子类依赖于父类中某些功能的实现细节。父类的实现可能因为版本的变化有所变化,那么子类的功能可能会被破坏,即使它的代码并没有改变。
复合是不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。现有的类变成了新类的一个组件。
复合中,新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果,这被称为“转发”。比如
public class ForwardingSet<E> implements Set<E>{ private final Set<E> s; public forwardingSet(Set<E> s){this.s = s;} public void clear(){s.clear;}//转发 public boolean contains(Object o){return s.contans(o);}//转发 public boolean isEmpty(){return s.isEmpty();}//转发 ... }
继承可能导致的问题:1.子类中的函数调用了父类的super.f(),而父类的f()中其实又调用了父类的f2()函数,这种“自用性”是实现细节,不是承诺,不能保证Java平台的所有实现都不变,可能会因为版本不同而发生改变。
2.如果在子类中添加了一个父类中没有的类,但是不巧,在下一版本中父类也有了同名的类,那么可能会变成一个覆盖方法,或重载方法,又或者,子类中的这个方法无法遵守父类中方法的约定。
如果在适合用复合的地方使用了继承,则会暴露实现细节,这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更严重的是,由于暴露了内部细节,客户端有可能就直接访问这些细节,会导致语义上的混淆,甚至直接修改超类,从而破坏子类的约束条件。
在决定使用继承而不是符合之前,还应问自己最后一组问题:对于你试图扩展的类,它的AI中有没有缺陷?如果有,你是否愿意把那些缺陷传播到类的API中?
总之,只有确实是is a 关系时,才应该使用继承。
第5条:要么为继承而设计,并提供文档说明,要么就禁止继承
对于为了继承设计的类,文档必须全面:
1.文档必须精确描述覆盖每个方法所带来的影响。对于每个公有的或者受保护的方法或构造器,文档必须指明该方法调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。
2.必须在文档中说明,哪些情况下它会调用可覆盖的方法。
对于为了继承而设计的类,唯一的测试方法就是编写子类。
第6条:接口优于抽象类
接口与抽象类的区别在于,抽象类允许包含某些方法的实现,但是接口不允许。
1.现有的类可以很容易被更新,以实现新的金额口。
2.接口是定义混合类型的理想选呢。
3.接口允许我们构造非层次结构的类型框架。
通过对你导出的每个重要接口都提供一个抽象的骨架实现类,可以把接口和抽象类的优点结合起来。编写骨架实现类必须认真研究接口,确定哪些方法是最基本的,其他的方法可以根据它们来实现,这些基本方法将成为骨架实现类中的抽象方法。然后,为接口中的其他方法提供具体的实现。
第7条:接口只用于定义类型
当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。为了其他任何目的而定义接口是不恰当的。
之所以说接口应该只用于定义类型,是因为Java中有一种对接口的不良使用:常量接口。这是反面的典型,比如java.io.ObjectStreamConstants。不值得效仿。
第8条:类层次优于标签类
有时候,会遇到带有两种或者更多种风格的实例的类,并包含表示实例风格的标签域。比如,考虑下面这个类,它能够表示圆形或矩形。
class Figure{ enum Shape{ RECTANGLE,CIRCLE}; //tag field final Shape shape; //this fields are used only if shape is RECTANGEL double length; double width; //this field is used only if shape is CIRCLE double radius; //constructor for circle Figure(double radius){ shape = Shape.CIRCLE; this.radius = radius; } //constructor for rectangle 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(); } } }
这种标签类有许多缺点。许多实现乱七八糟的挤在一个类里,影响可读性,过于冗长,容易出错,而且效率低下。
还好,我们有一颗麦丽素~面向对象的语言比如Java,提供了更好的方法来定义能表示多种风格对象的单个数据类型:子类型化。
为了将标签类转化为类层次,首先把标签类中的每个方法都定义一个包含抽象方法的抽象类。然后定义具体子类。上面例子的层次化是这样的:
abstract class Figure{ abstract double area(); } class Circle extends Figure{ final double radius; Circle(double radius){this.radius = radius;} double area(){ return Maths.PI * radius * radius;} } class Rectangle extends Figure{ final double length; final double width; Rectangle(double length,double width){ this.length = length; this.width = width; } double area(){return length * width;} }
这样的代码清楚且简单,同时还反映了类型之间本质上的层次关系。
第9条:用函数对象表示策略
有些函数允许函数的调用者通过传入第二个函数,来指定自己的行为,这个叫策略。比如C语言中的qsort函数要求用一个指向comparator函数的指针作为参数。Java中没有提供函数指针,但是可以用对象引用实现同样的功能。
我们可以定义这样一种对象,它的方法执行其他对象上的操作。如果一个类仅仅导出一个这样的方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。
考虑下面的类:
class StringLengthComparator{ public int compare(String s1,String s2){ return s1.length() - s2.length(); } }
这个类导出一个比较字符串的方法,规则是比较两个字符串的长度,StringLengthComparator的实例就是用于字符串比较操作的具体策略。
作为典型的具体策略类,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接口。即:在设计具体的策略类时,还需要定义一个策略接口。
//strategy interface public interface Comparator<T>{ public int compare(T t1,T t2); }
第10条:优先考虑静态成员类
嵌套类有4种:静态成员类,非静态成员类,匿名类和局部类。除了第一种,其他3种都被称为内部类。
静态成员类是最简单的嵌套类,最好把它看做普通的类,只是碰巧被声明在另一个类的内部而已。它可以访问外围类的所有成员,包括private的。
从语法上来讲,静态成员类与非静态成员类之间唯一的区别是,静态成员类的声明中包含static。非静态成员类可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用。如果嵌套类的实例可以在它的外围类的实例之外独立存在,这个嵌套类就必须是静态成员类。
当非静态成员类的实例被创建的时候,它和外围实例之间的关联关系也随之被建立起来,而且,这种关联关系之后不能被修改。
如果声明成员类不要求访问外围实例,就要始终把static 修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类。
匿名类没有名字,不是外围类的一个成员,它在使用的同时被声明和实例化。它有很多限制。除了被声明的时候,你不能实例化它们,不能执行instanceof,同时,由于匿名类出现在表达式中,必须保持简短(不超过10行),否则会影响程序可读性。
局部类是嵌套类中用的最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类。与成员类一样,局部类有名字,可以重复使用。与匿名类一样,它不能包含静态成员,且必须简短。
总之,4种嵌套类每种都有自己的用途。如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其他外围实例的引用,就要把成员类做成非静态的;否则就要是静态的;
假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就要把它做成匿名类;否则就做成局部类。