Effective Java阅读笔记(3)
Effective Java第4章 类和接口
13.使类和成员的可访问性最小化
访问控制机制决定了类、接口和成员的可访问性。实体的可访问性是由该实体声明所在的位置,以及该实体声明中所出现的访问修饰符共同决定的。
尽可能地使每个类或者成员不被外界访问。
私有的
private——只有在声明该成员的顶层类内部才可以访问这个成员。包级私有的
package-private/default——声明该成员的包内部的任何类都可以访问这个成员。受保护的
protected——声明该成员的类的子类可以访问这个成员(有一些限制),并且,声明该成员的包内部的任何类也可以访问这个成员。公有的
public——在任何地方都可以访问该成员。
实现了Serializable接口的类的私有域可能会被“泄漏”。
子类中的访问级别不允许低于超类中的访问级别。
除了公有静态final域的特殊情形之外,公有类都不应该包含公有域。
14.在公有类中使用访问方法而非公有域
如果类可以在它所在的包的外部进行访问,就提供访问方法。公有类永远都不应该暴露可变的域。但是,有时候会需要用包级私有的或者私有的嵌套类来暴露域,无论这个类是可变还是不可变的。
15.使可变性最小化
不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。不可变类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全。
为了使类成为不可变,要遵循下面五条规则:
不要提供任何会修改对象状态的方法。
保证类不会被扩展。一般使类成为
final,但后面还会讨论其他做法。使所有的域都是
final的。使所有的域都成为私有的。
确保对于任何可变组件的互斥访问。
不可变对象本质上是线程安全的,它们不要求同步;不仅可以共享不可变对象,甚至也可以共享它们的内部信息;不可以对象为其他对象提供了大量的构件。不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。
让不可变的类变成final的另一种方法就是,让类的所有构造器都变成私有的或者包级私有的,并添加公有的静态工厂来代替公有的构造器。
*16.复合优先于继承
继承打破了封装性,子类依赖于其超类中特定功能的实现细节。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。
为了避免继承导致的脆弱性,我们可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。
public class ForwardingSet implements Set {
private final Set s;
public ForwardingSet(Set s) {
this.s = s;
}
public void clear() {
s.clear();
}
public boolean contains(Object o) {
return s.contains(o);
}
public boolean isEmpty() {
return s.isEmpty();
}
public int size() {
return s.size();
}
public Iterator iterator() {
return s.iterator();
}
public boolean add(E e) {
return s.add(e);
}
public boolean remove(Object o) {
return s.remove(o);
}
public boolean containsAll(Collection> c) {
return s.containsAll(c);
}
public boolean addAll(Collection extends E> c) {
return s.addAll(c);
}
public boolean removeAll(Collection> c) {
return s.removeAll(c);
}
public boolean retainAll(Collection> c) {
return s.retainAll(c);
}
public Object[] toArray() {
return s.toArray();
}
public T[] toArray(T[] a) {
return s.toArray(a);
}
@Override
public boolean equals(Object o) {
return s.equals(o);
}
@Override
public int hashCode() {
return s.hashCode();
}
@Override
public String toString() {
return s.toString();
}
}
public class InstrumentedSet extends ForwardingSet {
private int addCount = 0;
public InstrumentedSet(Set s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
17.要么为继承而设计,并提供文档说明,要么就禁止继承
为继承而设计的类必须有文档说明它可覆盖overridable的方法的自用性,且必须通过某种形式提供适当的钩子hook,以便能够进入到它的内部工作流程中,这种形式可以使精心选择的受保护protected的方法,也可以是受保护的域,后者比较少见。该类的唯一的测试方法就是编写子类。
为了允许继承,类还必须遵守其他一些约束。构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。
18.接口优于抽象类
现有的类可以很容易被更新,以实现新的接口。
接口是定义
mixin(混合类型)的理想选择。接口允许我们构造非层次结构的类型框架。
抽象类的演变比接口的演变要容易得多。接口一旦被公开发行,并且已被广泛实现,再想改变这个接口几乎是不可能的。
19.接口只用于定义类型
接口应该只被用来定义类型,不应该被用来导出常量,导出常量可以使用枚举类型和不可实例化的工具类。
20.类层次优于标签类
标签类过于冗长、容易出错,并且效率低下。标签类很少有适用的时候,当你想要编写一个包含显式标签域的类时,应该考虑一下这个标签是否可以被取消,这个类是否可以使用类层次来代替。
以下为一个标签类:
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
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();
}
}
}
修改后:
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
double area() { return Math.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; }
}
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}
21.用函数对象表示策略
函数指针的主要用途就是实现策略模式。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体的策略只被使用一次时,通常使用匿名内部类来声明和实例化这个具体策略类。当一个具体策略被设计用来重复使用的时候,它的类通常要被实现为私有的静态成员,并通过公有的静态final域被导出,其类型为该策略接口。
Arrays.sort(StringArray,new Comparator(){
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
public class Host {
private static class StrLenCmp implements Comparator,Serializable{
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
public static final Comparator STRING_LENGTH_COMPARATOR = new StrLenCmp();
public static void main(String[] args) {
String s1 = "";
String s2 = "";
int result = Host.STRING_LENGTH_COMPARATOR.compare(s1, s2);
System.out.println(result);
}
}
22.优先考虑静态成员类
嵌套类是指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为他的外围类提供服务。如果嵌套类将来可能会用于其他的某个环境中,他就应该是顶层类。嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了第一种之外,其他三种都称为内部类。
从语法上讲,静态成员类和非静态成员类之间的唯一区别是,静态成员类的声明中包含修饰符static。尽管他们的语法非常相似,但是两种嵌套类有很大的不同。非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修改过的this构造获得外围实例的引用。如果嵌套类的实例可以在他外围类的实力之外独立存在,这个嵌套类就必须是静态成员类,在没有外围实例的情况下,要想创建非静态成员类的实例是不可能的。
当非静态成员类的实例被创建的时候,他和外围之间的关联关系也随之被创立起来;而且,这种关联关系以后不能被修改。通常情况下,当在外围类的某个实例方法的内部调用非静态成员类的构造器时,这种关系被自动建立起来。 使用表达式enclosingInstance.new MemberClass(args)来手工建立这种关联关系也是有可能的,但是很少使用。正如你所预料的那样,这种关联关系需要消耗费静态成员类的实例空间,并且构造的时间开销。
如果声明成员类不要求访问外围实例,就要始终把static修饰符放在他的声明中。
匿名类的三种常见用法:1.动态的创建函数对象,如sort方法的调用利用了匿名的Comparator实例;2.创建过程对象,如Runnable、Thread或者TimerTask实例;3.在静态工厂方法的内部使用。
如果一个嵌套类需要在单个方法之外仍然是可见的,或者他太长了,不适合方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就要把成员类做成非静态的;否则就做成静态的。假设这个嵌套类属于一个方法的内部,如果你需要在一个地方创建实例,并且已经有了一个预置的类型可以说明这个类的特征,就把他做成匿名类;否则,就做成局部类。