Effective Java 读书笔记(一)

来源:互联网 发布:淘宝账号被冻结怎么办 编辑:程序博客网 时间:2024/05/17 01:20

一、创建和销毁对象
(一)考虑用静态工厂方法代替构造函数
1、静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字。
如果一个构造函数的参数并没有确切地描述被返回的对象,那么选用适当名字的静态工厂可以使一个类更易于使用,并且相应的客户代码更易于维护。因为静态工厂方法自己有名字,所以它们没有构造函数那样的限制,对于给定的原型特征,可以有不止一个静态工厂方法。如果一个类看起来需要多个构造函数,并且它们的原型特征相同,那么你应该考虑用静态工厂方法来代替其中一个或者多个构造函数,并且慎重选择它们的名字以便明显地标示出它们的不同。
2、静态工厂方法的第二个好处是,与构造函数不同,它们每次调用的时候,不要求非得创建一个新的对象。这使得一些非可变类可以使用一个预先构造好的实例,或者把已经构造好的实例缓存起来,以后再把这些实例分发给客户,从而避免创建不必要的重复对象。
静态工厂方法可以为重复的调用返回同一个对象,这也可以被用来控制“在某一时刻哪些实例应该存在”。这样做有2个理由:第一,它使得一个类可以保证是一个Singleton。第二,它使非可变类可以保证“不会有两个相等的实例存在”,即当且仅当a==b的时候才有a.equals(b)为true。如果一个类保证了这一点,那么它的客户就可以用==操作符来代替equals(Object)方法,其结果是实质性的性能提高。
3、静态工厂方法的第三个好处是,与构造函数不同,它们可以返回一个原返回类型的子类型的对象。这种灵活性的一个应用是,一个API可以返回一个对象,同时又不使改对象的类成为公有的。以这种方式把具体的实现隐藏起来,可以的到一个非常简洁的API。这项技术非常适合基于接口的框架结构,因为在这样的框架结构中,接口成为静态工厂方法的自然返回类型。
4、静态工厂方法的主要缺点是,类如果不含公有的或者受保护的构造函数,就不能被子类化。静态工厂方法的第二个缺点是,它们与其他的静态方法没有任何区别。
(二)使用私有构造函数强化singleton属性
1、singleton是指这样的类,它只能实例化一次。singleton通常被用来代表那些本质上具有唯一性的系统组件。实现singleton有2种方法,这2种方法都要把构造函数保持为私有的,并且提供一个静态成员,以便允许客户能够访问该类唯一的实例。
在第一种方法中,共有静态成员是一个final域:
//Singleton with final field
public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis() {
    ……
  }
  ……
}
私有构造函数仅被调用1次,用来实例化公有的静态final域Elvis.INSTANCE。由于缺少公有的或者受保护的构造函数,所以保证了Elvis的全局唯一性:一旦Elvis类被实例化之后,只有一个Elvis实例存在——不多也不少。客户的任何行为都不会改变这一点。
第二种方法提供了一个公有的静态工厂方法,而不是公有的静态final域:
//SingleTon with static factory
public class Elvis {
  private static final Elvis INSTANCE = new Elvis();
  private Elvis() {
    ……
  }
  public static Elvis getInstance() {
    return INSTANCE;
  }
}
2、第一种方法的主要好处在于,组成类的成员的声明很清楚地表明了这个类是一个singletone:公有的静态域是final的,所以该域将总是包含相同的对象引用。第一种方法可能在性能上稍微领先,但是在第二种方法中,一个优秀的JVM实现应该能够通过将静态工厂方法的调用内联化(inlining),来消除这种差别。
第二种方法的主要好处在于,它提供了灵活性:在不改变API的前提下,允许我们改变想法,把该类做成singleton,或者不作成singleton。singleton的静态工厂法返回该类的惟一实例,但是,它也很容易被修改,比如说,为每个调用该方法的线程返回一个惟一的实例。
言而总之,如果你确信该类将永远是一个singleton,那么使用第一种方法是有意义的。如果你希望保留一点余地,那么请使用第二种方法。
3、为了使一个singleton变成可序列化的,仅仅在声明中加上“implements Serializable”是不够的。为了维护sinleton性,你必须也要提供一个readResolve方法。否则的话,一个序列化的实例在每次反序列化的时候,都会导致创建一个新的实例。比如说,在我们的例子中,会导致“假冒的Elvis”。为了防止这种情况,在Elvis类中加入下面的readResolve方法:
//readResolve method to preserve singleton property
private Object readResolve() throws ObjectStreamException {
  return INSTANCE;
}
(三)通过私有构造函数强化不可实例化的能力
偶尔情况下,你可能会编写出址包含静态方法和静态域的类,这样的类不希望被实例化,对它进行实例化没有任何意义。然而在缺少显式构造函数的情况下,编译器会自动提供一个公有的、无参数的默认构造函数。所以,我们只要让这个类包含单个显式的私有购构函数,那么,它就不可被实例化了。
(四)避免创建重复的对象
重复使用同一个对象,而不是每次需要的时候就创建一个功能上等价的新对象,通常,前者更为合适。作为一个极端的反面例子,考虑下面的语句:String s = new String("silly"); 该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作没有一个是真正必须的。传递给String构造函数的实参(“silly”)本身就是一个String实例,功能上等同于所有被构造函数创建的对象。正确的做法应该是:String s = "No longer silly"; 这个版本只使用一个String实例,而不是每次被执行的时候创建一个新的实例。而且,它可以保证,对于所有在同一个虚拟机中运行的代码,只要它们包含相同的字符串字面常量,则该对象就会被重用。
除了重用非可变的对象之外,对于那些已知不会被修改的可变对象,你也可以重用它们。下面是一个比较微妙、也比较常见的反例,其中涉及到可变对象,它们的值一旦被计算出来之后就不会再有变化,代码如下:
//反例
public class Persion {
  private final Date birthDate;

  public Persion(Date birthDate) {
    this.birthDate = birthDate;
  }

  //DON'T DO THIS
  public boolean isBabyBoomer() {
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomStart = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();
    return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
  }
}
isBabyBoomer每次被调用的时候,都会创建一个新的Calendar、一个新的TimeZone和两个新的Date实例,这是不必要的。下面的版本用一个静态的初始化避免了上面例子的低效率:
class persion {
  private final Date birthDate;

  public Persion(Date birthDate) {
    this.birthDate = birthDate;
  }

  /**
   * The starting and ending dates of the baby boom.
  **/
  private static final Date BOOM_START;
  private static final Date BOOM_End;

  static {
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomStart = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();
  }

  public boolean isBabyBoomer() {
    return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
  }
}
改进版本的Persion类仅在初始化时刻创建Calendar、TimeZone和Date实例一次,而不是在每次isBabyBoomer被调用的时候创建它们。如果isBabyBoomer方法被频繁调用的话,则这将会带来显著的性能提高。
(五)消除过期的对象引用
在支持垃圾回收的语言中,内存泄露是很隐蔽的。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象引用的所有其他对象。即使只有少量的几个对象引用被无意识地保留下来,也会有很多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。
要想修复这一类问题也很简单:一旦对象引用已经过期,只需要清空这些引用即可。清空过期引用的另一个好处是,如果它们在以后又被错误地解除引用,则程序会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。消除过期饮用最好的方法是重用一个本来已经包含对象引用的变量,或者让这个变量结束其生命周期。
一般而言,只要一个类自己管理它的内存,程序员就应该警惕内存泄露问题。一旦一个元素被释放掉,则该元素中包含的任何对象应用应该要被清空。
(六)避免使用finalizer
finalizer通常是不可预测的,常常也是很危险的,一般情况下是不必要的。使用finalizer会导致不稳定的行为、更差的性能,以及带来移植性问题。

二、对于所有对象都通用的方法
(七)在改写equals的时候请遵守通用约定
改写equals方法看起来非常简单,但是,有许多改写的方式会导致错误,并且后果非常严重。要避免问题最容易的方法是不改写equals方法,在这种情况下,每个实例只与它自己相等。如果下面的任何一个条件满足的话,这正是所期望的结果:
1、一个类的每个实例本质上都是惟一的。对于代表了活动实体而不是值的类,确实是这样的,比如Thread。Object提供的equals实现对于这些类是正确的。
2、不关心一个类是否提供了“逻辑相等(logical equality)”的测试功能。
3、超类已经改写了equals,从超类继承过来的行为对于子类也是合适的。
4、一个类是私有的,或者是包级私有的,并且可以确定它的equals方法永远也不会被调用。尽管如此,在这样的情形下,应该要改写equals方法,以免万一有一天它会被调用到,改写如下:
public boolean equals(Object o) {
  throw new UnsupportedOperationException();
}
那么,什么时候应该改写Object.equals呢?当一个类有自己特有的“逻辑相等”概念(不同于对象身份概念),而且超类也没有改写equals以实现期望的行为,这时我们需要改写equals方法。这通常适合于“值类(value class)”的情形。
改写equals方法的时候,你必须要遵守它的通用约定。下面是约定的内容,来自java.lang.Object的规范:
equals方法实现了等价关系(equivalence relation):
●自反性(reflexive)。对于任意的引用值x,x.equals(x)一定为true。
●对称性(symmertric)。对于任意给定的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)也一定返回true。
●传递性(transitive)。对任意的引用值x、y和z,如果x.equals(y)返回ture,并且y.equals(z)也返回true,那么x.equals(z)也一定返回true。
●一致性(consistent)。对于任意的引用值x和y,如果用于equals比较的对象信息没有被修改的话,那么,多次调用x.equals(y)要么一致地返回true,要么一致地返回false。
●对于任意的非空值x,x.equals(null)一定返回false。
假设创建一个简单的二维Point类:
public class Point {
  private final int x;
  private final int y;
  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
  public boolean equals(Object o) {
    if(!(o instanceof Point))
      return false;
    Point p = (Point)o;
    return p.x == x && p.y == y;
  }
}
假如你想要扩展这个类,为一个点增加颜色信息:
public class ColorPoint extends Point {
  private Color color;
  public ColorPoint(int x, int y, Color color) {
    super(x, y);
    this.color = color;
  }
}
那么equals方法会怎样呢?如果你完全不提供equals方法,而是直接从Point继承过来,那么在equals做比较的时候,颜色信息就被忽略掉了。虽然这样做不会违反equals约定,但是很明显这是不可接受的。假如你编写了一个equals方法,只是当实参是另一个有色点,并且具有同样的位置和颜色的时候,它才返回true。这个方法的问题在于,你在比较一个普通点和一个有色点,以及反过来的情形的时候,可能会的到不同的结果。
要想在扩展一个可实例化的类的同时,既要增加新的特征,同时还要保留equals约定,没有一个简单的办法可以做到这一点。这个问题的好的解决办法是使用组合,而不是继承,以及一个公有的视图方法。
public class ColorPoint {
  private Point point;
  private Color color;
  public ColorPoint(int x, int y, Color color) {
    point = new Point(x, y);
    this.color = color;
  }
  public Point asPoint() {
    return point;
  }
  public boolean equals(Object o) {
    if(!(o instanceof ColorPoint))
      return false;
    ColorPoint cp = (ColorPoint)o;
    return cp.point.equals(point) && cp.color.equals(color);
  }
}

实现高质量equals方法的一个“处方”:
1、使用==操作符检查“实参是否为指向对象的一个引用”。如果是的话,则返回true。
public boolean equals(Object o) {
  if (o == null) {
    return false;
  }
  if (o == this) {
    return true;
  }
  ……
}
2、使用instanceof操作检查“实参是否为正确的类型”。如果不是的话,则返回false。
public boolean equals(Object o) {
  ……
  if (! o instanceof Mytype) {
    return false;
  }
  ……
}
3、把实参转换到正确的类型。因为前面已经有了instanceof测试,所以这个转换可以确保成功。
4、对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹配。如果所有的测试都成功,则返回true;否则返回false。如果第2步中的类型是一个接口,那么你必须通过接口的方法,访问实参中的关键域;如果该类型是一个类,那么你也许能够直接访问实参中的关键域,这要取决于它们的可访问性。对于既不是float,也不是double类型的原子类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,先是用Float.floatToIntBits转换成int类型的值,然后使用==操作符比较int类型的值;对于double域,先是用Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较long类型的值(这里,对float和double域进行特殊的处理是有必要的,因为存在Float.Nan、-0.0f以及类似double类型的常量)。对于数组域,把以上这些指导原则应用到每个元素上。
5、当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
(八)改写equals时总要改写hashCode
一个很常见的错误根源在于没有改写hashCode方法。在每个改写了equals方法的类中,你必须也要改写hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法与所有基于hashCode的集合类结合在一起正常运作,这样的集合类包括HashMap、HashSet和Hashtable。
下面是hashCode约定的内容,来自java.lang.Object的规范:
●在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同,即这个应用程序这次执行返回的整数与下次执行返回的整数可以不一致。
●如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
●如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一个对象的hashCode方法,不要求必须产生不同的整数结果。然而,程序员应该意识到这样的事实,对于不相等的对象产生截然不同的整数结果,有可能提高散列表(hashCode)的性能。
因没有改写hashCode而违反的关键约定是第二条:相等的对象必须具有相同的散列码(hash code)。根据一个类的equals方法,两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object类的hashCode方法,他们仅仅是两个对象,没有其他共同的地方。
(九)总是要改写toString
提供一个好的toString实现,可以使一个类用起来更加愉快。在实际应用中,toString方法应该返回对象中包含的所有令人感兴趣的信息。
(十)谨慎地改写clone
如果你扩展了一个实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。否则的话,最好的做法是,提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的能力。另一个实现对象拷贝的好办法是提供一个拷贝构造函数(copy constructor)。所谓拷贝构造函数,它也是一个构造函数,其惟一的参数的类型是包含构造函数的类,例如:public Yum(Yum yum);或者,另一种方法是提供一个静态工厂来代替构造函数:public static Yum newInstance(Yum yum);
拷贝构造函数的做法,以及它的静态工厂方法变形,比Cloneable/clone方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未良好文档化的规范;它们不会与final域的正常使用发生冲突;它们不会要求客户捕捉不必要的被检查异常(checked exception);它们为客户提供一个静态类型化的对象。
(十一)考虑实现Comparable接口
comapreTo方法在Object中并没有被声明,相反,它是java.lang.Comparable接口中惟一的方法。compareTo方法允许进行简单的相等比较,也允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特征。一个类实现了Comparable接口,就表明它的实例具有内在的排序关系。
compareTo方法的通用约定与equals方法的通用约定具有相似的特征,下面是它的内容,摘自Comparable的规范:将当前这个对象与制定的对象进行顺序比较。当该对象小于、等于或大于制定对象的时候,分别返回一个负整数、0或者正整数。如果由于指定对象的类型而使得无法进行比较,则抛出ClassCastException异常。
三、类和接口
(十二)使类和成员的可访问能力最小化
要想区别一个涉及良好的模块与一个设计不好的模块,最重要的因素是,这个模块对于外部的其他模块而言,是否隐藏了内部的数据和其他的实现细节。一个设计良好的模块会隐藏所有的实现细节,把它的API与实现清晰地隔离开来。然后,模块之间指通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这个概念被称为“信息隐藏(Information Hiding)”,或封装(encapsulation)。
经验表明,你应该尽可能地使每一个类或成员不被外界访问。换句话说,你应该使用最低可能的、并且与该软件的正确功能相一致的访问级别。
对于成员(域、方法、嵌套类合嵌套接口)有4种可能的访问级别,下面按照可访问性递增的顺序列出来:
私有的(private)——只有在声明该成员的顶层类内部才可以访问这个成员。
包级私有的(package-private)——声明该成员的包内部的任何类都可以访问这个成员,并且,该成员声明所在的包内部的任何类也可以访问这个成员。
受保护的(protected)——该成员声明所在类的子类可以访问这个成员,并且,该成员声明所在的包内部的任何类也可以访问这个成员。
公有的(public)——任何地方都可以访问该成员。
公有类应该尽可能少地包含公有的域,但是,对于这个规则也有一个例外,通过公有的静态finaly域来暴露类的常量是允许的。按照惯例,这样的域的名字由大写字母组成,单词之间用下划线隔开。很重要的一点是,这些域要么包含原语类型的值,要么包含指向非可变对象的引用。注意,非0长度的数组总是可变的,所以,具有公有的静态final数组域几乎总是错误的。
(十三)支持非可变性
一个非可变类就是一个简单的类,它的实例不能被修改。每个实力中包含的所有信息都必须在该实例被创建的时候就提供出来,并且在对象的整个生存期内固定不变。Java平台库包含许多非可变类,其中有String、原语类型的包装类、BigInteger和BigDecimal。
为了使一个类变成非可变类,要遵循下面五条规则:
1、不要提供任何会修改对象的方法。
2、保证没有可被子类改写的方法。
3、使所有的域都是final的。
4、使所有的域都成为私有的。
5、保证对于任何可变组件的互斥访问。
非可变对象比较简单,一个非可变对象可以只有一个状态,即最初被创建时刻的状态。非可变对象本质上是线程安全的,他们不要求同步。当多个线程并发访问这样的对象时,他们不会被破坏。
非可变对象为其他对象——无论是可变的还是非可变的——提供了大量的构建。如果一个夫在对象内部的组建对象不会改变的话,那么要维护它的不便另约束是很容易的。
非可变类整整唯一的缺点是,对于每一个不同的值都要求一个单独的对象。除非有很好的理由要让一个类成为可变类,否则就应该是非可变的。如果一个类不能被做成非可变类,那么你仍然应该尽可能地限制它的可变性。降低一个对象中存在的状态的数目,可以更容易地分析该对象的行为,同时降低出错的可能性。因此,构造函数应该创建完全初始化的对象,所有的约束关系应该在这时候建立起来,构造函数不应该把“只构造了一部分的实例”传递给其他的方法。你不应该在构造函数之外在提供一个“重新初始化”方法,除非有绝对很好的理由要这么做。
(十四)复合优先于继承
与方法调用不同的是,继承打破了封装性。换句话说,一个子类依赖于其他超类众特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,则子类可能会打破,即使它的代码完全没有改变。因而一个子类必须要跟着超类的更新而发展,除非超类是专门为了扩展而被设计的,并且具有很好的文档说明。
只有当子类真正是超类的“子类型(subtype)”的时候,继承才是合适的。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展类A。如果你打算让类B扩展类A,那么你应该问自己:“每一个B确实也是A吗?”如果你不能够确定这个问题的答案是yes,那么B就不应该扩展A。如果答案是no,那么通常情况下,B应该包含A的一个私有实例,并且暴露一个小的、简单的API:A本质上不是B的一部分,只是它的实现细节而已。
(十五)要么专门为继承而设计,并给出说明文档,要么禁止继承
首先,专门为继承而设计的类的文档必须精确地描述了改写每一个方法所带来的影响。换句话说,该类必须有文档说明其可以改写(overridable)的方法的自用性(self-use):对于每一个公有的或受保护的方法或者构造函数,它的文档必须指明它调用了那些可以改写的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程(所谓可改写(overridable)的方法,是指非final的,公有的或受保护的)。更一般地,一个类必须在文档中说明,在哪些情况下它会调用一个可改写的方法。
为了允许继承,一个类还必须遵守其他一些约束:构造函数一定不能调用可被改写的方法,无论是直接进行还是间接进行。如果违反了这条规则,很有可能会导致程序失败。超类的构造函数在子类的构造函数之前运行,所以,子类中改写版本的方法将会在子类的构造函数运行之前就先被调用。如果改写版本的方法依赖于子类构造函数所执行的初始化工作,那么该方法将不会如预期般地执行。为了更加直观地说明这一点,下面这个很小的类违反了这条规则:
public class Super {
  //Broken - constructor invokes overridable method
  public Super() {
    m();
  }
  public void m() {
    ……
  }
}
下面的字类改写了方法m,Super惟一的构造函数就错误地调用了这个方法m:
final class Sub extends Super {
  private final Date date;//Blank final, set by constructor
  Sub() {
    date = new Date();
  }
  //Overrides Super.m, invoked by the constructor Super()
  public void m() {
    System.out.println(date);
  }
  public static void main(String [] args) {
    Sub s = new Sub();
    s.m();
  }
}
你可能会期望这个程序会打印出日期两次,但是,它第一次打印出null,因为方法m被构造函数Super()调用的时候,构造函数Sub()还没有机会初始化date域。
在为了继承而设计类的时候,Cloneable和Serializable接口表现出了特殊的困难,如果一个累是为了继承而被设计的,那么无论实现其中哪个接口都不是一个好主意,因为它们把一些实质性的负担转嫁到了扩展这个类的程序员身上。如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,你应该意识到,因为clone和readObject方法在行为上非常类似于构造函数,所以一个类似的限制规则也是适用的:无论是clone还是readObject,都不能调用一个可改写的方法,不管是直接的方式,还是间接的方式。
有两种办法可以禁止子类化:比较容易的办法是把这个类声明为final的,另一种办法是把所有的构造函数变成私有的,或者包级私有的,并且增加一些共有的静态工厂来替代构造函数的位置。
(十六)接口由于抽象类
接口是的安全地增强一个类的功能成为可能,如果你用抽象类来定义类型,那么这样就使得程序员除了使用继承的手段来增加功能之外,没有别的选择途径。这样的到地类与包装类相比,功能更差、也更加脆弱。
虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序员提供实现上的帮助。你可以把接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽象的骨架实现(skeletal implementation)类。接口的作用仍然是定义类型,但是骨架实现类负责所有与接口相关的工作。按照惯例,骨架实现被称为AbstractInterface,这里interface是所实现的接口的名字。
(十七)接口只是被用于定义类型
当一个类实现了一个接口的时候,这个接口被用作一个类型(type),通过此类型可以引用这个类的实例。因此,一个类实现了一个接口,就表明客户可以对这个类型的实例实施某些动作。为了任何其他目的而定义接口是不合适的。
(十八)优先考虑静态成员类
嵌套类(nested class)是指被定义在另一个类的内部的类。嵌套类存在的恶目的应该只是为它的外围类提供服务。如果一个嵌套类将来可能会用于其他的某个环境中,那么它应该是顶层类(top-level class)。嵌套类有4种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。除了第一种之外,其他三种都被称为内部类(inner class)。如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合于放在一个方法内部,那么应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,则把成员类做成非静态的;否则就做成静态的。假设一个嵌套类属于一个方法的内部,如果你只需要在一个地方创建它的实例,并且已经有了一个预先存在的类型可以说明这个类的特征,则把它做成匿名类;否则就做成局部类。