Effective Java 读书笔记

对于所有对象都同用的方法

 

1 考虑用静态工厂方法替代构造器

类可以提供一个公有的静态工厂方法,他只是一个返回类的实例的静态方法。

 

实例受控类

public static Boolean valueOf(boolean b)
{
    return b ? Boolean.TRUE : Boolean.FALSE;
}

编写实例受控类有几个原因。实例受控使得类可以确保他是一个Singleton或者是不可实例化的。他还使得不可变类可以确保不会存在两个相等的实例。

API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简介。这种结束适用于基于接口的框架(java.util.Collections)

 

这样做有几大优势。

  • 他们有名称。
  • 不必再为每次调用他们都创建一个新对象。
  • 他们可以返回原返回类型的任何子类型的对象。
  • 在创建参数化类型实例的时候,他们是代码变得更加简洁。

 

静态工厂方法的缺点

  • 类如果不含公有的或者受保护地构造器,就不能被子类化。
  • 他们与其他的静态方法实际上没有任何区别。

 

2 遇到多个构造器参数时要考虑用构件器

静态工厂和构造器有个共同的局限性:他们都不能很好的扩展大量的可选参数。

对于需要大量的可选参数的时候,一向习惯采用重叠构造器模式。

重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然较难以阅读。

 

遇到许多构造器参数的时候,还有第二种代替办法,即JavaBeans模式。

JavaBeans模式自身有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。

JavaBeans模式阻止了把类变成不可变的可能,这就需要程序员付出额外的努力来确保他的线程安全。

 

还有第三种替代方法,既能保证向重叠构造器模式那样的安全性,也能保证像JavaBeans模式那么好的可读性。这就是Builder模式。

不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个Builder对象。然后客户端在builder对象上调用类似setter的方法,来设置每个相关的可选参数。

public class NutritionFacts {
  
private final int servingSize;
  
private final int servings;
  
private final int calories;
  private final int fat;
  private final int sodium;   private final int carbohydrate;   public static class Builder {     private final int servingSize;     private final int servings;     private int calories = 0;     private int fat = 0;     private int sodium = 0;     private int carbohydrate = 0;     public Builder(int servingSize, int servings) {    this.servingSize = servingSize;    this.servings = servings;     }     public Builder calories(int val) {    ccalories = val; return this;     }
    
public Builder fat(int val) {    fat = val; return this;     }     public Builder carbohydrate(int val) {    carbohydrate = val; return this;     }     public Builder sodium(int val) {       sodium = val; return this;     }
    
public NutritionFacts build() {    return new NutritionFacts(this);     }   }   private NutritionFacts(Builder builder) {     servingSize = builder.servingSize;     servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate;   }   public static void main(String[] args) {     NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calories(100).calories(100).build();   } }

Builder像个构造器一样,可以对其参数加强约束条件。Build方法可以检验这些约束条件。

将参数从builder拷贝到对象中之后,并在对象域而不是在Builder域(39)中对他们进行检验,如果违反了任何约束条件,build方法就应该抛出IllegalStateException(60)。异常的详细信息应该显示出违反了那些约束条件。

 

设置参数的builder生成了一个很好的抽象工厂。

public  interface Builder<T> {
  public T build();
}

 

NutritionFacts.Builder implements Builder<NutritionFacts>

Tree buildTree(Builder<? Extends Node> nodeBuilder) {}

Class.newInstance破坏了编译时的异常检查。而Builder接口弥补了这些不足。

如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就始终不错的选择。

 

3 用私有构造器或者枚举类型强化Singleton属性

public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis {}
}

或者:

public class Elvis {
  private staic final Elvis INSTANCE = new Envis();
  private Elvis() {}
  private static Elvis getInstance() { return INSTANCE; }
}

 

工厂方法的优势之一在于,它提供了灵活性:在不改变其API的前提下,我们可以改变该类是否应该为Singleton的想法。

工厂方法返回该类的唯一实例,但是,他可以很容易的被修改,比如改成每个调用该方法的线程返回一个唯一的实例。第二个优势与泛型有关(27)。

 

4 通过私有构造器强化不可实例化的能力

企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的。该类可以被子类化,并且该子类也可以被实例化。

 

这要让这个类包含私有构造器,他就不能被子类化了:

public class UtilityClass {
  private UtilityClass() {
    throw new AssertionError();
  }
}

AssertionError不是必需的,但是它可以避免不小心在类的内部调用构造器。他保证该类在任何情况下都不会被实例化。

 

5 避免创建不必要的对象

对于同时提供了静态工厂方法(1)和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免不必要的对象。

例如,静态工厂方法Boolean.valueOf(String)几乎总是优先与构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。

public class Person {
  private final Date birthDate;
  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, , 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();

       return brithDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
  }
}

下面使用静态的初始化器:

public class Person {
  private final Date brithDate;
  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);
    BOOM_START
= gmtCal.getTime();

    gmtCaL.set(
1965, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_END
= gmtCal.getTime();
  }   

  public boolean isBabyBoomer() {
    
return brithDate.compareTo(BOOM_START) >= 0 && brithDate.compareTo(BOOM_END) < 0;
  }
}

如果改进后的person类被初始化了,他的isBadyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。

通过延迟初始化(71),即把对这个域的初始化延迟到isBadyBoomer方法第一次被调用的时候进行,则有可能消除这些不必要的初始化工作,但是不建议这样做。

 

适配器是指这样一个对象:把它功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对给定对象的特定适配器而言,他不需要创建多个适配器实例。(Map接口的keySet)

 

不要错误的认为本条目所介绍的内容暗示着“创建对象代价非常昂贵,我们应该要尽可能的避免创建对象”。相反,由于小对象的构造器制作很少量的显式工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。

反之,通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象时非常重量级的。(数据库连接池)

 

6 消除过期的对象引用

public class Stack {
  privateObject[] elements;
  private int size;
  private static final int DEFAULT_INITIAL_CAPACITY =16;

  public Stack() {
    elements = newObject[DEFAULT_INITIAL_CAPACITy];
  }

  public void push() {
    ensureCapacity();
    elements[size++] = e;
  }

  public Object pop() {
    if (size == 0)
      throw new EmptyStackException();
    returnn elements[--size];
  }
  
private void ensureCapacity() {     if (elements.length == size)       elements = Arrays.copyOf(elements, size >> 1);   } }

如果一个栈先是增长,然后再收缩,那么从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不在引用这些对象,他们也不会被回收。这是因为,栈的内部维护着对这些对象的过期引用。所谓过期引用,是指永远也不会被解除的引用。

 

修复办法:

publicObject pop() {
  if (size == 0)
    throw new EmptyStackException();
  Object result = elements[--size];
  elements[size] = null;
  return result;
}

清空过期引用的另一个好处是,如果他们以后又被错误地解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。

清空对象引用应该是一种例外,而不是一种规范行为。

 

  • 只要类时自己管理内存,程序员就应该警惕内存泄漏问题。
  • 内存泄漏的另一个常见来源是缓存。(WeakHashMap)。
  • 内存泄漏的第三个常见来源是监听器和其他回调。

 

7 避免使用终结方法

终结方法通常是不可预测的,也是很危险的,一般情况下是不必要的。

Java语言规范不仅不保证终结方法会被及时的执行,而且根本就不保证他们会被执行,当一个程序终止的时候,默写已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。

不应该以来中介方法来更新重要的持久状态。例如依赖和总结方法来释放共享资源上的永久锁,很容易让整个分布式系统垮掉。

 

使用中介方法有一个非常严重的性能损失。

现实的种植方法通常与try-fainally结构结合起来使用,以确保及时终止。

Foo foo = new Foo();
try {
  // ……
} finally {
  foo.terminate();
}

本地对等体是个一个本地对象,普通对象通过本地方法委托给一个本地对象。因为本地对等体不是一个普通的对象,所以垃圾回收期不知道他,当他的Java对等体被回收的时候,他不会被回收。终止方法可以是本地方法,或者他也可以调用本地方法。

 

那么终结方法的好处:

  • 当对象的所有者忘记调用前面段落中建议的显式终止方法时,终止方法可以充当安全网。
  • 在本地方法体并不拥有关键资源的前提下,终结方法正式执行回收任务的最合适的工具。

 

终结方法链:

try {
  // ……
} finally {
  super.finalize();
}

如果终结方法发现资源还未被终止,啫应该在日志中记录一条警告,因为这表示客户端中的一个Bug,应该被修复。

(FileInputStream、FileOutputStream、Timer、Connection),都具有终结方法,这些终结方法充当了安全网。

 

如果子类实现了超类的终结方法,但是忘了手工调用超类的终结方法,防范方法是为每个被终结的对象创建一个附加对象。

把终结方法放在一个匿名类中,该匿名类唯一的用途就是终结他的外围实例。该匿名类的单个实力被称为终结方法守卫者。

public class Foo {
  private final Object finalizerGuardian = newObject() {
    protected void finalize() throw Throwable {
      // Finalize outer Foo object
    }
  }
}

外围实例在他的私有实例域存放着一个对其终结方法守卫者的唯一引用,因为终结方法守卫与外围实例可以同时启动终结过程。当守卫被终结的时候,他执行外围实例所期望的终结行为,就好像他的终结方法是外围对象上的一个方法一样。

 

8 Equals方法

重写equals方法规范

  • 自反性
  • 对称性
  • 传递性
  • 一致性:对于任意的应用值x和y,如果对象信息没有修改,那么多次调用总是返回true,或false



9 HashCode方法

修改equals总是要修改hashCode

如果两个对象根据equals方法返回是相等的,那么调用这两个对象任一个对象的hashCode方法必须产生相同的结果

为不相等的对象产生不同的散列码

boolean类型 v ? 0 : 1
byte, char, short类型 (int) v
long类型 (int) (v ^ (v >>> 32))
float类型 Float.floatToIntBits(v)
double类型 Double.doubleToLongBits(v)
Object类型 v == null ? 0 : v.hashCode()
array类型 递归调用上述方法

result = 37 * result + n;

 

 

10 ToString方法

总是改写toString()方法


11 Clone方法

Cloneable接口
改变超类中一个受保护的方法的行为

Object的clone方法返回该对象的逐域拷贝,否则抛出一个
CloneNotSupportedException异常

x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)

拷贝一个对象往往会导至创建该类的一个新实例,
但同时他也会要求拷贝内部的数据结构,这个过程中没有调用构造函数

cone方法是另一个构造函数,必须确保他不会伤害到原始的对象,
并且正确地建立起被克隆对象中的约束关系

clone结构与指向可变对象的final域的正常用法是不兼容的

另一个实现对象拷贝的好办法是提供一个拷贝构造函数
public Yum(Yum yum)

静态工厂
public static Yum newInstance(Yum yum)


12 Comparable接口

一个类实现了Comparable接口就表明他的实例具有内在的排序关系

如果想为实现了Comparable接口的类增加一个关键特性,请不要
扩展这个类,而是编写一个不相关的类,其中包含一个域,其类型是的一个类,然后提供一个“视图”方法返回这个域。

BigDecimal("1.0")
BigDecimal("1.00")
加入到HashMap中,HashMap包含2个元素,通过equals方法比较是不相等的
加入到TreeMap中,TreeMap包含1个元素,通过compareTo方法比较是相等的

 

第四章 类和接口

 

13 使类和成员的可访问性最小化

要区别设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。

设计良好的模块会隐藏所有的实现细节,把他的API与他的实现清晰的隔离开来。然后,模块之间之通过他们的API进行通信,一个模块不需要知道其他模块的内部工作情况。这概念被称为信息隐藏或者封装,是软件设计的基本原则之一。

他可以有效地解除组成系统的模块各模块之间的耦合关系,使得这些模块可以独立地忾发、测试、优化、使用、理解和修改。

Java程序设计语言提供了许多机制来协助信息隐藏。访问控制机制决定了类、接口和成员可访问性。

尽可能地使每个类或者成员不被外界访问。
如果一个报己私有的顶层类,只是在某一个类的内部被用到,就应该考虑使他成为唯一使用他的那个类的私有嵌套类(22)。

实例域决不能是公有的。包含公有可变域的类并不是线程安全的。
长度为非零的数组总是可变的。类具有公有的静态final数组域,或者返回这种域的访问方法,这几乎总是错误的。

public static final 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_VALUE = { ... };
public static final Thing[] values() {
  return PRIVATE_VALUE.clone(); 
}

要在这两种方法之间作出选择,得考虑客户端可能怎么处理这个结果。

出了共有静态final域的特殊情形之外,公有类都不应该包含共有域。并且要确保共有静态final域所引用的对象都是不可变的。


14 在公有类中使用访问方法而非共有域

如果类可以在他所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。

如果类是包级私有的,或者是私有的嵌套类,直接暴露他的数据域并没有本质的错误。假设这些数据域确实描述了该类所提供的抽象。这种方法比访问方法的做法更不会产生是觉混乱,无论是在类定义中,还是在使用该类的客户端代码中。


15 支持非可变性

非可变对象本质上是线程安全的,他们不要求同步,并且可以自由共享。

  • 不要提供任何会修改对象的方法
  • 保证没有可被子类改写的方法
  • 是所有的域都是final的
  • 诉有的于都成为私有的
  • 保证对于任何可变组件的互斥访问

 

对于频繁用的到的值,为他们提供公有的静态final常量:

public static final Complex ZORE = new Complex(0, 0);
public static final Complex One = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);

这种方可以被进一步扩展。不可变的类可以提供一些静态工厂,他们把频繁被请求的实例缓存起来,从而当现有实例可以符合请求的时候,就不必创建新的实例。

不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。

 

对于这种问题有两种办法:

  1. 先猜测一下会经常用到哪些多步骤的操作,然后将他们作为基本类型提供。如果模个多步骤操作已经作为基本类型提供,不可变的类就可以不必在每个步骤单独创建一个对象。(例如BigInteger有一个包级私有的可变配套类)
  2. 如果无法预测,最好的办法是提供一个公有的可变配套类。(String类的配套类StringBuilder)。

 

不仅可以共享非可变对象,甚至也可以共享它们的内部信息
BigInteger中的negate方法
非可变对象为其他对象提供了大量构件
非可变类的缺点是,对于每一个不同的值都要求一个单独的对象

 

使一个类成为final的两种办法

  1. 让这个类的没一个方法都成为final的,而不是让整个类都成为final的。(可扩展)
  2. 使其所有的构造方法成为私有的,或者包级私有的,并增加送有静态工厂来替代个构造函数。(静态工厂)

 

public static Complex valueOf(a, b) { return new Complex(a, b); }

扩展则增加静态方法 static Complex valueOfPolar(a, b) ...

如果当前正在编写的类,他的安全性依赖于BigInteger的非可变性,那么你必须检查
一确定这个参数是不是一个真正的BigInteger,而不是一个不可新的子类实例。
if (arg.getClass() != BigInteger.class) r = new BigInteger(arg.toByteArray());


规则指出,没有方法会修改对象,并且所有的域必须是final的。

除非有很好的理由要让一个类成为可变类,否则就应该是非可变的。

实际上这些规则比真正的要求强了一点,为了提高性能实际上应该是没有一个方法能够对对象的状态产生外部可见的改变,然而许多非可变类拥有一个或者多个非final的冗余域,它们比一些开销昂贵的计算结果缓存到这些域中,如果将来再次请求这些计算,则直接返回这些被缓存的值,从而节约了从新计算所需的开销。这总技巧可以很好的工作应为对象是非可变的,他的非可变行保证了这些计算如果被再次执行的话,会产生相同的结果(延迟初始化,String的hashCode)


如果一个类不能被做成非可变类,那么你仍然应该尽可能地限制它的可变性。
构造函数应该创建完全初始化的对象,所有的约束关系应该在这时候建立起来。



16 复合优先于继承

与方法调用不同的是,继承打破了封装性。

上面的问题都来源于对方法的改写动作。如果你在扩展一个类的时候,仅仅
是增加信的方法,而不改写已有的方法,你可能会认为这样做是安全的,但是也并不是完全没有风险。

有一中办法可以避免前面提到的所有问题,你不再是扩展一个已有的类,二十在新的类中增加一个私有域,他引用了这个已有的类的一个实例。这种设计被称作复合。

public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);    
    }
    
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}


public class ForwardingSet<E> implements Set<E> {
    private final Set s;
    public ForwardingSet(Set<E> s) {
        this.s = s;    
    }

    public void add(E e) { return s.add(e); }
    // ......
}

应为原有已有的类边成了一个新类的一个组成部分。新类中的每个实例方法都可以被调用被包含的已有实例中对应的方法,并返回它的结果。这被称为转发,新类中的方法被称为转发方法。这样的到的类会非常稳固,他不依赖于已有类的事现细节。

每一个InstrumentedSet实例都把另一个Set实例包装起来,所以InstrumentedSet类被称作包装类。(Decorutor模式)

包装类不适合用在回调框架中,在回掉框家中,对象把自己的引用传递给其他的对象,
已便将来调用回来,因为被包装起来的对象并不知道他外面的包装对象,所以他传递一个只向自己的引用,回调时绕开了外面的包装对象这被称为SELF问题。

只有当子类真正是超类的子类型的时候,继承才是合适的,对于正在扩展的类,继承机制会把超类API中的所有缺陷传播到子类中,而复合技术运允许你设计一个新的API从而隐藏这些缺陷。



17 要么专门为继承而设计,并给出文档说明,要么禁止继承

一个类必须通过某种形式提供适当的钩子,已便能够进入到它的内部工作流程中,
这样的形式可以是精心选择的受保护的方法,也可以是保护域。

当你为了继承的目的而设计一个有可能被广泛使用的类时,比需要意识到,对于文档中所说明的自用模式,以及对于其受保护方法和域所有隐含的实现细节,你实际上已经作出了永久的承诺。这些承诺使得你在后续的版本中要提高这个类的性能,或者增加新功能非常困难,甚至不可能。

构造函数一定不能调用可被改写的方法。超类的构造函数将会在子类的构造函数运行之前先被调用,如果该改写版本的方法依赖于子类构造函数所执行的初始化工作,那么该方法将不会如预期般的执行。

无论是clone还是readObject,都不能他调用一个可改写的方法,不管是直接地方是,还是间接地方式。

为了继承而设计一个类,要求对这个类有一些实质的限制。
对于这个问题的最好解决方案是,对于那些并非为了安全地进行子类化而设计和编写文档的类,禁止子类化。

1声明为final类
2把所有的构造函数变成私有的,并增加一些公有静态工厂来代替构造函数

消除一个类中可改写的方法而不改变它的行为,做法如下
把每个可改写的方法的代码移到一个私有的辅助方法中,并让每个可改写的方法
调用他的私有辅助方法。然后,用直接调用可改写方法的私有辅助方法来代替可
改写方法的每个自用调用。

 

18 接口优于抽象

已有的类可以很容易被更新,已实现新的接口。
接口使得我们可以构造出非层次结构的类型框架。

接口使得安全地增强一个类的功能成为可能。
可以把接口和抽象类的优点结合起来,对于你期望导出的每一个重要接口,都提供一个抽想的骨架实现类。

实现了这个接口的类可以把对于接口方法的调用,转发到一个内部私有类的实例上,
而这个内部私有类扩展了骨架实现类。这项技术被称为模拟多重继承。

编写一个骨架类相对比较简单,首先确定那些方法是最为基本的,其他的方法在实现的时候将以他们为基础。基本方法将是骨架实现类中的抽象方法,必须为接口中所有其他方法提供具体的实现。

抽象类的演化比接口的演化要容易得多。



19 接口只是被用来定义类型

一个类实现了一个接口,就表明客户可以对这个类的实例实施某些动作。
有一中接口被称为常量接口,常量接口模式是对接口的不良使用。

要导出常量,可以有几种选择方案。如果这些常量被看作一个枚举类型的成员,
那么你应该应用一个类型安全枚举类(21),否则的话,你应该使用一个不可被实例化的工具类。(3)

接口应该使用来定义类型的,他们不应该被用来导出常量。

 

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() { ... }
}

标签类正是类层次的一种简单的仿效。

abstract class Figure {
  abstract double area(); 
}

class Circle extends Figure {
  final double radius;

  public Circle(double radius) {
    this.radius = raidus; 
  }

  public area() { return Math.PI * radius; }
}

class Rectangle extends Figure {
  final double length;
  final double width;

  public Rectangle(double length, double width) {
    this.length = length;
    this.width = width;
  }
  
double area() { return length * width; } }

这段代码简单且清楚,每个类型的实现都配有自己的类,这些类都没有受到不相关的数据域的拖累。所有的域都是final的。

他们可以用来反应类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。

 

21 用函数对象表示策略

有些语言支持函数指针、代理、lambda表达式,或者支持类似的机制,允许程序把“调用特殊函数的能力”存储起来并传递这种能力。

Java没有提供函数指针,但是可以用对象引用实现同样的功能。调用对象上的方法通常是执行该对象上的某项操作。

我们可以定义这样一种对象,他的方法执行其他对象上的操作。如果一个类仅仅导出这样的一个方法,他的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。

class StringLengthComparator {
  public int compare(String s1, String s2) {
    return s1.length() - s2.length();
  } 
}

函数指针可以载人一对字符串上被调用。换句话说,StringLengthComparator实例适用于字符串比较操作的具体策略。

作为典型的具体策略类,StringLengthComparator类是无状态的:他没有域,这个类的所有实例在功能上都是互相等价的。

我们在设计具体的策略类时,还需要定义一个策略接口:

public interface Comparator<T> {
  public int compare(T t1, T t2);
}

Class StringLengthComparator implements Comparator<String > {}

具体策略类往往使用匿名类声明。如果他被重复执行,考虑将函数对象存储到一个私有的静态final域里,并重用他。

Class Host {
  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_LENGTH_COMPRATOR = new StrLenCmp();
}

 宿主类可以到出公有的静态域(或静态工厂方法),起类型为策略接口,具体的策略类可以是宿主类的私有嵌套类。(String的不去分大小写比较)

 

22 优先考虑静态成员类

嵌套类是指被定义在另一个类的内部的类。
嵌套类存在的目的应该只是为他的外围类提供服务。
如果一个嵌套类将来有可能会用于其他的某个环境中,那么应该是顶层类。

嵌套类有四种:静态成员类,非静态成员类,匿名类和局部类。
处理第一种其他三种都被称为内部类。

静态成员类的一种通常用法是作为公有的辅助类,仅当与他的外部类一起使用时才有意义。

非静态成员类的每一个实例都隐含着与外围类的外围实例紧密关联在一起。
在非静态成员的实例方法内部,调用外围实例上的方法是有可能的,或者使用经过修饰的this也可以得到一个指向外围实例的引用。如果一个嵌套类的实例可以在他的外围类的实例之外独立存在,那么这个嵌套类不可能是一个非静态成员类,在没有外围实例的情况下要创建非静态成员类的实例是不可能的。

非静态成员应用Iteragor(迭代器)

如果你声明的成员类不要求访问外围实例,那么请记住把static修饰符放到成员类的声明中,使他成为一个静态成员类,而不是一个非静态成员类。

私有静态成员类的一种通常方法是用来代表外围类对象的组件。例如Map实例中的Entry每一对键值都与Map关联但是Entry的方法并不需要访问外围类,如果是用非静态成员来表示,每个Entry中将会包含一个指向Map的引用,只会浪费空间。


匿名类没有名字,所以他们被实例化之后就不能在对他们进行引用,他不是外围的一个成员,并不于其他的成员一起被声明,而是在被使用的点上同时被声明和实例化。匿名类可以出现在代码中任何允许表达式出现的地方。匿名类的行为与静态的或者非静态的成员类非常类似,取决于他所在的环境:
如果匿名类出现在一个非静态的环境中,则他有一个外围实例。

常见用法:

  • 创建一个函数对象。比如Comparator
  • 创建一个过程对象。比如Thread
  • 在一个静态工厂方法内部。比如intArrayAsList(16条)
  • 在很复杂的类行安全枚举类型中用于共有静态final域的初始化器中(21条Operation类)
public class Calculator
{
    public static abstract class Operation 
    {
        private final String name;

        Operation(String name) { this.name = name; }
        public String toString() { return this.name; }

        abstract double eval(double x, double y);

        public static final Operation PLUS = new Operation("+") {
            double eval(double x, double y) { return x + y; }
        }

        public static final Operation MINUS = new Operation("-") {
            double eval(double x, double y) { return x - y; }
        }

        public static final Operation TIMES = new Operation("*") {
            double eval(double x, double y) { return x * y; }
        }

        public static final Operation DIVIDE = new Operation("/") {
            double eval(double x, double y) { return x / y; }
        }
    }

    public double calculate(double x, Operation op, double y) {
        return op.eval(x, y)    
    }
}

如果一个嵌套类需要在单个方法外仍然是可见的,或者太长了不适合放在一个方法内部,那么应该是用成员类。

如果成员类的每一个实例都需要一个只向起外围实例的引用,则把成员类做成非静态的;否则就做成静态的。

 

第5章 泛型

 

参数化类型 List<String>
实际类型参数 String
泛型 List<E>
形式类型参数 E
无限制通配符类型 List<?>
原生态类型 List
有限制类型参数 <E extends Number>
递归类型限制 <T extends Comparable<T>>
有限制通配符类型 List<? extends Number>
泛性方法 static <E> List<E> asList(E[] a)
类型令牌 String.class

 

23 请不要在新代码中使用原生态类型

声明中具有一个或多个类型参数的类或者接口就是泛型类或者接口。

每种泛性定义一组参数化的类型,构成格式为:先是类或者接口的名称,接着用尖括好<>把对应与泛型形式类型参数的实际类型参数列表包括起来。

每个泛型都定义一个原生态类型,既不带任何实际类型参数的泛型名称。(例如List<E>的原生态是List) 

如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。

 

List和参数化的类型List<Object>的区别是,前者逃避了泛型检查,后者则明确的告知编译器,他能够支持任意类型的对象。虽然你可以将List<String>传递给类型List的参数,但是不能将它传给类型List<Object>的参数。(泛型有子类型化的规则,List<String>是原生态类型List的一个子类型,而不是参数化类型List<Object>的子类型)

 

如果使用像List这样的原生态类型,就会失掉类型安全性,单是如果是用像List<Object>这样的参数化类型,则不会。

不要在新代码中使用原生态类型,这条规则有两个小小的例外,两者都源于“泛型信息可以在运行时被擦除”(25)这一事实。在类文字中必须是用原生态类型。规范不允许使用参数化类型(List.class, String[].class和int.class都合法,但是List<String.class>和List<?>.class则不合法)。

 

这条规则的第二个例外与instanceof操作符有关。由于泛型信息可以在运行时被擦出,因此在参数化类型而非无限制通配符上使用instanceof操作是非法的。用无限制通配符类型代替原生态类型,对instanceof操作符的行为不会产生任何影像。

if (o instanceof Set) {
  Set<?> m = (Set<?>) o;
  // ......
}

一旦这个o是个Set,就必须将它转圜成通配符类型Set<?>,而不是转换成原生态类型Set

Set<Object>是个参数化类型,表示可以包含任何对象类型的一个集合;Set<?>则是一个通配符类型,表示只能包涵某种未知对象类型的一个集合;Set则是原生态类型,他脱离了泛型系统。全两者是安全的。

 

24 消除非受检警告

要尽可能地消除每一个非受检警告。如果消除了所有警告,酒可以确保代码是类型安全的。

如果无法消除警告,同时可以证明引起警告的代码是类型安全的可疑用一个@SuppressWarnings("unchecked")注解来禁止这条警告。 

public <T> T[] toArray(T[] a) {
  if (a.length < size) 
    return (T[]) Arrays.copyOf(elements, size, a.getClass());
  System.arraycopy(elements, 0, a, 0, size);
  if (a.length > size) 
  a[size] = null;
  return a;
}

public <T> T[] toArray(T[] a) {
  if (a.length < size) {
    @SuppressWarnings("unchecked")
    T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
    return result;
  }
  System.arraycopy(elements, 0, a, 0, size);
  if (a.length > size)
  a[size] = null;
  return a;
}

SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声名到整个类都可以。应该是重在尽可能小的范围中使用SuppressWarnings注解。

将SuppressWarnings注解放到return语句中是非法的,因为他不是一个声明。每当使用SuppressWarnings("unchecked")注解时,都要添加一条注释,说明为什么这么做是安全的。

非受检警告很重要,不要忽略他们。每一条警告都表示可能在运行时抛出ClassCastException异常。要尽最大的奴隶消除这些警告。

 

25 列表优先于数组

数组与泛型相比,有两个重要的不同点。

1、数组是协变的。
如果Sub是Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的:对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。

 

合法

Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in";

不合法

List<Object> ol = new ArrayList<Long>();
ol.add("I don't fit in");

无论哪种方法,都不能将String放进Long容器中,但是利用数组,你会在运行时发现所犯的错误;利用列表,则可以在编译时发现错误。


2、数组是具体化的。
数组会在运行时才知道并检查他们的元素类型约束。(将String保存到Long数组中,就会得到一个ArrayStoreException异常)泛型是通过擦除来实现的。因此泛型只在编译时强化他们的类型信息,并在运行时丢弃(擦除)他们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。(23)

 

创建泛型、参数化类型或者类型参数的数组都是非法的。

 

不可具体化的类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的参数化类型是无限制的通配符类型。(List<?>,Map<?,?>)创建无限制通配符类型的数组是合法的。 

interface Function<T> {
  T apply(T arg1, T arg2);
}

static Object reduce(List list, Function f, Object initVal) {
  Object[] snapshot = list.toArray();
  Object result = initVal;
  for (E e : snapshot) 
    result = f.apply(result, e)
  return result;
} 

使用泛型方法

static <E> E reduce(List<E> list, Function f, E initVal) {
  List<E> snapshot;
  synchronized (list) {
    snapshot = new ArrayList<E>(list);
  }

  E result = initVal;
  for (E e : snapshot) 
    result = f.apply(result, e);
  return result;
}

数组和泛型有着非常不同的类型规则。数组是协变切可以具体化的;泛型是不可变的且可以被擦除的。因此数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。

 

26 优先考虑泛型

E[] elements = new E[DEFAULT_INITIAL_CAPACITY]; 

如第25条中所述,你不能创建不可具体化的类型的数组,如E。每当编写用数组支持的泛型时,都会出现这个问题。

 

解决这个问题有两种方法:
1、直接绕过创建泛型数组的禁令:创建一个Object的数组,并将他转换成泛型数组类型。错误是消除了,但是编译起会产生一条警告。这种用法是合法的,但不是类型安全的。

E[] elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 

相关的数组保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其它方法。这个数组中保存的唯一元素,是传给push方法的那些元素,他们的类行为E,因此未受检的转换不会有任何危害。一旦你证明了未受检的转换是安全的,救药在尽可能小的范围内禁止警告(24)。 


2、将elements域的类型从E[]改为Object[]

E result = (E) elements[--size];

由于E是一个不可具体化的类型,编译器无法在运行时检验转换。你还是可以自己正是未受检的转换是安全的,因此可以禁止该警告。

@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null;
return result;

具体选择这两种方法中的哪一种来出力泛型数组创建错误,则主要看个人的偏好了。但是禁止数组类型的未受检转换比禁止标量类型的更加危险,所以建议采用第二种方案。

(但是在比Stack更实际的泛型类中,或许代码中会有多个地方须要从数组中读取元素,因此选择第二种方案需要多次转换成E,而不是只转换成E[],这也使地一种方案之所以更常用的原因)。

使用泛型比使用需要在客户端代码中进行转换的类型来的更加安全,也更加容易。在设计新类型的时候,要确保他们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。

 

27 优先使用泛型方法

如同类可以从泛型中受益一般,方法也一样。静态工具类尤其适合于泛型化(Collections中的算法)

public static Set union(Set s1, Set s2) {
  Set result = new HashSet(s1);
  result.addAll(s2);
  return result;
}

为了修正警告,是方法变成类型安全的,要将方法生命修改为声明一个类型参数,表示这三个集合的元素类型,并在方法中使用类型参数。 

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
  Set<E> result = new HashSet<E>(s1);
  result.addAll(s2);
  return result;
}

泛型方法的一个显著特征是,无需明确指定类型参数的值,不像调用泛型构造器的时候是必须指定的。编译器通过检查方法参数的类型来计算类型参数的值。
对于上述程序而言,编译器发现union的两个参数都是Set<String>类型,因此知道类型参数E必须为String。这个过程称作类型推导。 

Map<String, List<String>> anagrams = new HashMap<String, List<String>>();

类型参数出现在了变量声明的左右两边,为了消除这种冗余,可以编写一个泛型静态工厂方法,与想要使用的每个构造起相对应。

public static <K, V> HashMap<K, V> newHashMap() {
  return new HashMap<K, V>();
}

有时,会需要成见不可变但是又适合于许多不同类型的对象。由于泛型通过擦除实现的,可以给所有必要的类型参数时用单个对象。这种模式最常用余函数对象,相关模式是泛型单例工厂(Collection.reverseOrder, Collection.EmptySet)。

public interface UnaryFunction<T> {
  T apply(T arg);
}

private static UnaryFunction<Object> IDENTITY_FUNCTINO = new UnaryFunction<Object>() {
  public Object apply(Object arg) { return arg; }
}

@SuppressWarnings("unchecked")
public static <T> UnaryFunction<T> identityFunction() {
  return (UnaryFunction<T>) IDENTITY_FUNCTION; 
}

通过某个包涵该类型参数本身的表达式来限制类型参数是允许的。这就是递归类型限制。

 

28 利用有限制通配符来提升API的灵活性

如同25条所述,参数化类型是不可变的。换句话说,对于任何两个截然不同的类型Type1和Type2而言,List<Type1>既不是List<Type2>的子类型,也不是他的超类型。虽然List<String>不是List<Object>的子类型,这与直觉想悖,但是实际上很有意义。(你可以将任何对象放进一个List<Object>中,却只能将字符串放进List<String>中)

public void pushAll(Iterable<E> src) {
  for (E e : src) push(e);
}

假如有一个Stack<Number>,并且调用了push(intVal),这里的intVal就是Integer类型。这是可以的,因为Integer是Number的一个子类型。

Stack<Number> numberStack = new StackNumber();
Iterable<Integer> integers = ...;
numberStack.pushALl(integers);

会得到错误消息,因为参数化类型是不可变的

public void pushAll(Iterable<? extends E> src) {
  for (E e : src) push(e);
}

Pop操作

public void popAll(Collection<? super E> dst) {
  while (!isEmpty()) {
    dst.add(pop());
  } 
}
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects);

popAll的输入类型参数为“E的某种超类的集合”。

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。(如果某个输入参数即是生产者,有时消费者,那么通配符类型就没什么好出了)

 

如果参数化类型表示一个T生产者,就使用<? extends T>;如果他表示一个T消费者,就使用<? super T>。

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

返回类型仍然是Set<E>。不要用通配符类型作为返回类型。

如果类的用户必须考虑通配符类型,类的API或许就会出错。

Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = union(integers, doubles); 

如果编译起不能推断出你希望他用有的类型,可以通过一个显式的类型类型参数来告诉他要使用哪种类型。 

Set<Number> numbers = Union.<Number>union(integers, doubles);
public static <T extends Comparable<? super T>> T max(List<? extends T> list)

最直接的运用到参数list。他产生T实例,因为将类型从List<T>改成List<? extends T>。
最初T被指定用来扩展Comparable<T>,但是T的comparable消费T的实例(并产生表示顺序关系的整值)。因此,参数化类型Comparable<T>被有限制通配符类型Comparable<? super T>取代。comparable始终是消费者,因此使用时始终是Comparable<? super T>优先于Comparable<T>。

List<ScheduledFuture<?>> scheduledFutures = ...;

不能将初始化方法声名运用给这个泪表的原因在于,java.util.concurrent.ScheduledFuture没有实现Comparable<ScheduledFuture>接口。他是扩展Comparable<Delayed>接口的Delayed接口的子接口。

public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
  Iterator<T> i = list.iterator();
  T result = i.next();
  while (i.hasNext()) {
    T t = i.next();
    if (t.compareTo(result))
      result = t;
  }
  return result;
}

list不是一个List<T>,因此他的iterator方法没有返回Iterator<T>。他返回T的某个子类型的一个iterator。

 

因此我们用他来代替iterator声明,他使用了一个有限制的通配符类型:

Iterator<? extends T> i = list.iterator();

迭代器的next方法返回的元素属于T的某个子类型,因此他们可以被安全地保存在类型T的一个变量中。

public static <E> void swap(List<E>, int i, int j);
public static void swap(List<?>, int i, int j);

第二种更好一些,应为他更简单。将它传到一个列表中-任何列表-方法就会交换被索引的元素。不用担心参数类型。

 

如果类型参数只在方法声明中出现一次,就可以用通配符取代他。

如果是无限制的类型参数,就用无限制的通配符取代他;如果是有限制的类型参数,就用有限制的通配符取代他。

 

第二种方法有一个问题,他优先使用通配符而非类型参数:

public static void swap(List<?>, int i, int j) {
  list.set(i, list.set(j, list.get(i)));
}

list的类型为List<?>,你不能把null之外的任何值放到List<?>中。 

public static void swap() {
  swapHelper(list, i, j);
}


private static <E> void swapHelper(List<E> list, int i, in j) {
  list.set(i, list.set(j, list.get(i)));
}

swapHelper方法知道list是一个List<E>。他知道从这个列表中取出的任何值均为E类型,并知道将E类型的任何值放进列表都是安全的。

 

29 优先考虑类型安全的异构容器

有时候你会需要更多的灵活性(例如,数据库行可以有任意多的列,如果能以类型安全的方式访问所有的列就好了)。

有一中方法可以很容易的做到这一点。就是将键进行参数化而不是将容器参数化。然后将参数化的键提交给容器,来插入或者获取值。用泛型系统来确保值的类型与他的键相符。 

public class Favorites {
  private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>(); 

  public <T> void putFavorite(Class<T> type, T instance) {
    if (type == null)
      throw new NullPointerException("Type is null");
    favorites.put(type, instance);
  }

  public <T> T getFavorite(Class<T> type) {
    return type.cast(favorites.get(type));
  }
}

每个Favorites实例都得到一个称作favorites的私有Map<Class<?>, Object>的支持。你可能认为由于无限制通配符类型的关系,将不能把任何东西放进这个Map中,但事实正好相反。要注意的是通配符类型是嵌套的:他不是属于通配符类型的Map的类型,二十他的建的类型。
由此可见,每个键都可以有一个不同的参数化类型:一个可以是Class<String>,接下来是Class<Integer>等等。异构就是从这里来的。

 

第二个要注意的事情是,favorites Map的值类型是Object。Map并不能保证键和值之间的类型关系,即不能保证每个值的类型都与键的类型相同。事实上,Java类型系统还没有强大到足以表达这一点。但我们知道这是事实,并在获取favorite的时候利用了这一点。

getFavorite方法的实现利用Class的cast方法,将对象引用动态地转换成了Class对象所表示的类型。

 

Favorites类有两种局限值得注意:
恶意的客户端可以很轻松地破坏Favorites实例的类型安全,只要以他的原生态形式使用Class对象。

public <T> void putFavorite(Class<T> type, T instance) {
  favorites.put(type, type.cast(instance));
  // ClassCastException
}

他不能用在不可具体化的类型中。(你可以保存String或者String[]但不能保存List<String>)

 

 

第六章 枚举和注解

 

30 用enum代替int常量

枚举类型是指由一组固定的常量组成合法值的类型。

采用int枚举类模式的程序是十分脆弱的。因为int枚举是编译时常量,被编译到他们的客户端中。如果与枚举常量关联的int发生了变化,客户端就必须重新编译。

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH };

他们通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以方问的构造起,枚举类形是真正的final。

因为客户端不能创建枚举类型的实例,也不能对他进行扩展,而只有声明过得枚举常量。

出了完善了int枚举模式的不足之外,枚举类行还允许添加任意的方法和域,并实现任意的接口。他们提供了所有Object方法的高及实现,实现了Comparable,和Serializable接口,并针对枚举类型的可任意改变性设计了序列华方式。

public enum Planet {

  MERCURY(3.302e+23, 2.439e6),
  VENUS (3.302e+23, 2.439e6),
  EARTH (3.302e+23, 2.439e6);

  private fianl double mass;
  private fianl double radius;
  private final double surfaceGravity;

  private static final double G = 6.67300E-11;

  Plant(double mass, double radius) {
    this.mass = mass;
    this.radius = radius;
    surfaceGravity = G * mass / (radius * radius);
  }

  public double mass() {
    return mass; 
  }

  public double radius() {
    return radius;
  }

  public double surfaceGravity() {
    return surfaceGravity;
  }

  public double sufaceWeight(double mass) {
    return mass * surfaceGravity;
  }
}

为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器。

public static void main(String[] args) {
  double earthWeight = Double.parseDouble(args[0]);
  double mass = earthWeight / Planet.EARTH.surfaceGravity();
  for (Planet p : Planet.values()) {
    System.out.println("......");
  }
}

枚举有一个静态的values方法,按照生命顺序返回他的值数组。

如果一个枚举具有普遍适用性,他就应该成为一个顶层类;如果他知识备用在一个特定的顶层类中,他就应该是顶层类的一个成员。(22), (java.math.RoundingMode)

有一中更好的方法可以将不同的行为与每个枚举常量关联起来:
在枚举类行中生命一个抽象apply方法,并在特定于常量的类主体中,用具体方法覆盖每个常量的抽象apply方法。这种方法被称作特定于常量的方法实现。

public enum Operation {
  PLUS("+") { 
    double apply(double x, double y) { return x + y;}
  },
  MINUS("-") {
    double apply(double x, double y) { return x - y;} 
  },
  TIMES("*") { 
    double apply(double x, double y) { return x * y;} 
  },
  DIVIDE("/") { 
    double apply(double x, double y) { return x / y;} 
  };

  private final String symbol;

  Operation(String symbol) { this.symbol = symbol; }
  public String toString() { return symbol; }
  abstract double apply(double x, double y)
}

枚举类型中的抽象方法必须被他所有的常量中的具体方法所覆盖。
枚举中的switch语句适合于给外部的枚举类型增加特定于常量的行为。

enum PayrollDay {
  MONDAY(PayType.WEEKDAY),
  TUESDAY(PayType.WEEKDAY),
  WEDNESDAY(PayType.WEEKDAY),
  THURSDAY(PayType.WEEKDAY),
  FRIDAY(PayType.WEEKDAY),
  SATURDAY(PayType.WEEKEND),
  SUNDAY(PayType.WEEKEND);

  private final PayType payType;

  PayrollDay(PayType payType) { this.payType = payType; }
  
double pay(double hoursWorked, double payRate) {     return payType.pay(hoursWooked, payRate);   }   private enum PayType {     WEEKDAY {       double overtimePay(double hours, double payRate) {         return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;       }     }     WEEKEND {       double overtimePay(double hours, double payRate) {         return hours * payRate / 2;       }     }
    
private static final int HOURS_PER_SHIFT = 8;     abstract double overtimePay(double hrs, double);     double pay(double hoursWorked, double payRate) {       double basePay = hoursWorked * payRate;       return basePay + overtimePay(hoursWorked, pay)Rate;     }   } }

将加班工资计算移到一个私有的嵌套枚举中,将这个策略枚举的实例传到PayrollDay枚举的构造器中。之后PayroolDay枚举将加班工资计算委托给策略枚举,PayrollDay中就不需要switch语句或者特定于常量的方法实现了。


每当需要一组固定常量的时候就可以使用枚举。
与int常量相比,枚举类型的优势是不言而喻的。枚举要易读得多,也更安全,功能更强大。

许多枚举都不需要显式的构造器或者成员,但许多其他枚举则受益于“每个常量与属性得关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联。在这种相对少见的情况下,特定于常量的方法要优先于启用自有值的枚举。如果多个枚举常量同时共享相同的行为,则考虑策略枚举。

 

31 用实例域代替序数

枚举天生就与一个单独的int值想关联。所有的枚举都有一个ordinal方法,他返回每个枚举常量在类型中的数字位置。


永远不要根据枚举的序数到处与他相关联的值,而是要将他报存在一个实例域中:

public enum Ensemble {
  SOLO(1), DUET(2), OCTET(8), QUARTET(4), ...

  private final int numberOfMusicians;

  Ensemble(int size) { this.numberOfMusicians = size; }
  public int numberOfMusicians() { return numberOfMusicians; }
}

大多数程序员都不需要ordinal方法。他设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构。

 

32 用EnumSet代替位域

如果一个枚举类型的元素主要用在集合中,一般就使用int枚举模式,将2的不同倍数赋予每个常量。

public class Text {
  public static final int STYLE_BOLD = 1 << 0;
  public static final int STYEL_ITALIC = 1 << 1;
  public static final int STYEL_UNDERLINE = 1 << 2;
  public static final int STYEL_STRIKETHROUGH = 1 << 3;

  public void applyStyle(int style) { ... }
}

text.applyStyles(STYLE_BOOLD | STYLE_ITALIC);

这种表示发让你用OR位运算将几个常量合并到一个集合中,称作位域。

下面是前一个范例改成用枚举代替位域后的代码,他更加简短,更加清楚,也更加安全。

public class Text {
  public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
  public void applyStyles(Set<Style> styles) { ... }
}

text.applyStyle(EnumSet.of(Style.BOLD, Style.ITALIC));

正是因为枚举类型要用在集合中,所以没有理由用位域来表示他。
EnumSet有个缺点,无法创建不可变的EnumSet,可以用Collections.unmodifiableSet将EnumSet封装起来。

 

33 用EnumMap代替序数索引

public class Herb {

  public enum Type { ANNUAL, PERENNIAL< BIENNIAL } 

  private final String name;
  private final Type type;

  Herb(String name, Type type) {
    this.name = name;
    this.type = type;
  }
}

Herb[] garden = ...;

Set<Herb>[] herbsByType = (Set<Herb>) new Set[Herb.Type.values().length];

for (int i = 0; i < herbByType.length; i++) {
  herbsByType[i] = new HashSet<Herb>();
}

for (Herb h : grden) {
  herbsByType[h.type.ordina()].add(h);
}

for (int i = 0; i < herbsByType.length; i++) {
  System.out.printlnf("%s: %s%n", Herb.Type.values()[i], herbsByType[i]);
}

这种方法确实可行,但是因为数组不能于泛型兼容(25),程序需要进行未受检的转换,并且不能正确无误地进行编译。应为数组不知道他的索引代表着什么,你必须手工标注这些索引的输出。

幸运的是还有一种更好的方法可以达到同样的效果:

Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);

for (Herb.Type t : herb.Type.values()) {
  herbsByType.put(t, new HashSet<Herb>()); 
}

for (Herb h : garden) {
  herbsByType.get(h.type).add(h);
}

System.out.println(herbsByType);

EnumMap的构造器采用键类型的Class对象:这是一个有限制的类型令牌,他提供了运行时的泛型信息(29)。

public enum Phase {
  SOLID, LINQUID, GAS; 

  public enum Transition {
    MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

    private static final Transition[][] TRANSTIONS = {
      {null, MELT, SUBLIME}, {FREEZE, null, BOIL}, {DEPOSIT, CONDENSE, null}
    }
  }

  public static Transition from(Phase src, Phase dst) {
    return TRANSITION[src.ordinal()][dst.ordinal()];
  }
}

编译起无法知道序数和数组索引之间的关系。如果在过渡表中出了错,或者在修改Phase或者Phase.Transition枚举类型的时候忘记将它更新,程序就会运行失败。

public enum Phase {
  SOLID, LIQUID, GAS;

  public enum Transition {
    MELT(SOLID, LIQUID),
    FREEZE(LIQUID, SOLID),
    BOIL(LIQUID, GAS),
    CONDENSE(GAS, LIQUID),
    SUBLIME(SOLID, GAS),
    DEPOSIT(GAS, SOLID);

    private final Phase src;
    private final Phase dst;

    private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);
    static {
      for (Phase p : Phase.values()) {
        m.put(p, new EnumMap<Phase, Transition>(Phase.class));
      } 
      for (Transition trans : Transition.values()) {
        m.get(trans.src).put(trans.dst);
      }
    }
    public static Transition from(Phase src, Phase dst) {
      return m.get(src).get(dst);
    }
  }
}

最好不要用序数来索引数组,而要使用EnumMap。
如果这种关系是多维的,就使用EnumMap<..., EnumMap<...>>。

 

34 用接口模拟可伸缩的枚举

对于可伸缩的枚举类行而言,至少有一中具有说服力的用例,就是操作码,也称作opcode。

操作码是指他的元素表是在某种机器上的那些操作(例如30中的Operation类型,表示一个简单的计算器中的某些函数)。 

public interface Operation {
  double apply(double x, double y); 
}

public enum ExtendOperation implements Operation {
  EXP("^") {
    public double apply(double x, double y) {
      return Math.pow(x, y);
    } 
  }
  REMAINDER("%") {
    public double appley(double x, double y) {
      return x % y;
    }
  }

  private final String symbol;
  ExtendOperation(String symbol) {     
this.symbol = symbol;   }
  @Override   
public String toString() {     return symbol;   } }

虽然枚举类行不是可扩展的,但是接口则是可扩展的,他使用来表示API中的操作的接口类型。你可以定义另一个枚举类行,他实现了这个接口,并用这个新类型的实例代替基本类型。

在可以使用基础操作的任何地方,都可以使用新的操作,只要API是被写成采用接口类型而非实现。在枚举中,不必像在不可扩展的枚举中所作的那样,利用特定于实例的方法实现来声明抽想的apply方法。这是因为抽象方法是接口的一部分。

public static void main(String[] args) {
  double x = Double.parseDouble(args[0]);
  double y = Double.parseDouble(args[1]);
  test(ExtendOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {
  for (Operation op : opSet.getEnumConstants()) 
    System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}

ExtendedOperation.class从main被传递给了test方法,来描述被扩展操作的集合。这个类的字面文字充当有限制的类型令牌(29)。

opSet参数中更人很复杂的声明(<T extends Enum<T> & Operation> Class<T>)确保了Class对象即表示枚举又表示Operation的子类型,这正是遍历元素和执行与每个元素相关联的操作时所需要的。


第二种方法是使用Collection<? extends Operation>,这是个有限制的通配符类型(28),作为opSet参数类型

test(Arrays.asList(ExtendedOperation.values()))
private static void test(Collection<? extends Operation> opSet, double x, double y) { ... } 

用接口模拟可伸缩枚举有个小小的不足,即无法将实现从一个枚举类行继承到另一个枚举类型。(如果共享功能比较多,可以将它封装在一个辅助类或者静态辅助方法中)。

虽然无法编写可扩展的枚举类行,却可以通过编写接口以及实现该接口的基础枚举类行,对他进行模拟。

 

35 注解优先于命名模式

一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特输处理。(例如Junit测试框架要求他的用户一定要用test作为测试方法明成的开头)

命名模式的缺点:
文字拼写错误会导致失败,且没有任何提示。
无法确保他们只用于相应的程序元素上。
他们没有提供参数值与程序元素关联起来的好办法。

注解很好的解决了所有这些问题。
Retention和Target注解进行了注解。这解类型声明中的这种注解被称作元注解。
@Retention元注解表明,Test注解应该在运行时保留。
@Target元注解表明,Test注解只在方法声明中才是合法的:他不能运用到类声明、域声明或者其他程序元素上。

Test注解,称作标记注解,因为他没有参数,只是“标注”被注解的元素。

import java.lang.reflect.*;

public class RunTests {
  public static void main(String args) throws Exception {
    int tests = 0;
    int passed = 0;

    Class clazz = Class.forName(args[0]);
    for (Method m : clazz.getDeclaredMethods()) {
      if (m.isAnnotationPresent(Test.class)) {
        test++;
        try {
          m.invoke(null);
          passed++;
        }
        catch (InvocationTargetException ite) {
          Throwable exc = ite.getCause();
          System.out.println(m + " failed: " + exc);
        }
        catch (Exception e) {
          System.out.println("INVALID @Test: " + m);
        }
      } 

    }
    System.out.printf("Passed %d, Failed: %d%n", passed, tests - passed);
  } 
}

注解中数组参数的语法十分灵活,他是进行讨论优化的但元素数组。使用了ExceptionTest新版的数组参数后,之前的所有ExceptionTest注解仍然有效。并产生但元素数组。
为了指定多元素的数组,要用花括号({})将元素包围起来,并用逗号将他们隔开。

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class<? extends Exception> value(); 
}

 

例子:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
  Class<? extends Exception>[] value(); 
}
@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class})
public static void doubleyBad() { ... }
if (m.isAnnotationPresent(ExceptionTest.class)) {
  try {
    // ... 
  } 
  catch (Throwable t) {
    Throwable exc = t.getCause();

    Class<? extends Exception>[] excTypes = m.getAnnotition(ExceptionTest.class).value();
    int oldPassed = passed;

    for (Class<? extends Exception> excType : excTypes) {
      if (excType.isInstance(exc)) {
        passed++;
        break;
      }
    }

    if (passed == oldPassed)
    System.out.printf("Test %s failed: %s %n", m, exc); 
  }
}

既然有了注解,就完全没有理由在使用命名模式。
所有的程序员都应该使用Java平台所提供的于定义的注解类型。

 

36 坚持使用Override注解

这个注解只能用在方法声明中,他表示被注解的方法声明覆盖了超类型中的一个声明。

public class Bigrm {
  private final char finrst;
  private final char second;

  public boolean equals(Bigram b) {
    return b.first == first && b.second == second;
  } 
}

Bigram类的创建这原本想要覆盖equals方法,不幸的是程序员没有覆盖equals,而是将它重载了。为了覆盖Object.equals,必须定义一个参数为Object类型的equals方法。

@Override 
public boolean equals(Object o) {   if (!(o instanceof Bigram))     return false;   Bigram b = (Bigram) o;   return b.first == first && b.scond == second; }

想要覆盖超类声明的每个方法声明中使用Override注解。
这一规则有个例外,如果你在编写一个没有标注为抽象的类,并且确信他覆盖了抽象的方法,在这中情况下,就不比将Override注解放在该方法上。

如果在你想要的每个方法声明中使用Override注解来覆盖超类声明,编译起就可以题你防止大量的错误。

 

37 用标记接口定义类型

标记接口是没有包涵方法声明的接口,而是指一个类实现了具有某种属性得接口(Serializable接口)。

标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。

标记接口:
标记接口胜过标记注解的两一个优点是,他们可以被更加精确地进行锁定。如果注解类型利用@Target(ElementType.Type)声明,他就可以被应用到任何类或者接口。假设有一个标记只适用于特殊接口的实现。如果将它定义成一个标记接口,就可以用他将唯一的接口扩展成他适用的接口。


Set接口可以说就是这种有限制的标记接口。他只适用于Collection子类型,但是他不会添加除了Collection定义之外的方法。一般情况下,不把他当做是标记接口,因为他该进了几个Collection方法的契约(add、equals、hashCode)。

这种标记接口可以表述整个对象的某个约束条件,或者表明实例能够利用其他某个类的方法进行处理。


标记注解:
标记注解胜过标记接口的最大优点在于,可以通过默认的方式添加或者多个注解元素,给已被使用的注解类型添加更多的信息。
简单的标记注解类型可以演变成更加丰富的注解类型。这种演变对于标记接口而言是不可能的,因为他通常不可能在实现接口之后在给他添加方法(18)。


标记注解的另一个优点在于,他们是更大的注解机制的一部分。因此,标记注解在那些支持注解为编程元素之一的框架中同样具有一致性。


如果标记是应用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口可以用来实现或者扩展接口。

如果要永远限制这个标记只用于特殊接口的元素,最好将标记定义成该接口的一个子接口。如果两个问题都是否定的,则应该是用标记注解。

 

如果你发现字迹在编写的是目标为ElementType.Type的标记注解类型,就要花点时间考虑清楚,他是否真的应该为注解类型,想想标记接口是否会更佳合适。

 

 

第7章 方法

 

38 检查参数的有效性

如果传递无效的参数值给方法,这个方法在执行之前先对参数进行检查,那么他很快就会失败,并且清楚地出现适当的异常。

对于共有方法,要用Javadoc的@throws标签在文档中说明违反参数值限制时会抛出的异常。通常为(IllegalArgumentException, IndexOutOfBoundsException, NullPorterExceptoni)。

公有方法:

public BigInteger mod(BigInteger m) {
  if (m.signum() <= 0)
    throw new ArithmeticException("Modulus <= 0: " + m);
}

私有方法:

private static void sort(long a[], int offset, int length) {
  assert a != null;
  assert offset >= 0 && offset <= length;
  assert length >= 0 && length <= a.length;
}

从本质上讲,断言是在生成被断言的条件将会为真,无论外围的客户端如何使用它。不同于一般的有效性检查,断言如果失败,将会抛出AssertionError。也不同于一般的有效性检查,如果他们没有祈祷作用,从本质上也不会有成本开销,除非通过将-ea标记传递给Java解释器来启用它们。


在方法执行他的计算任务之前,应该现检查他的参数,这一规则也有例外。在有些情况下,有效性检查工作非常昂贵,或者根本是不切实际,而且有效性检查已隐含在计算过程中完成。(Collections.sort(List))(64)


有时候某些计算会隐式的之行必要的有效性检查,但是如果检查不成功,就会抛出错误的异常。这种情况下,应该是用61条中将书的异常转译技术,将计算过程中抛出的异常转为正确的异常。

 

39 必要时进行保护行拷贝

假设类的客户端乎尽其所能来破坏这个类的约束条件,因此你必须保护行地设计程序。

对於构造器的每个可变参数进行保护行拷贝是必须的。

public Period(Date start, Date end) {
  this.start = new Date(start.getTime());
  this.end = new Date(end.getTime());
  if (this.start.compareTo(this.end) > 0) 
    throw new IllegalArgumentException(start + " after " + end);
}

 

保护行拷贝是在加查参数有效性之前进行的,并且有效性检查是针对拷贝后的对象,而不是针对原始对象。

对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法进行保护行拷贝。

对于类的方法提供了对其可变内部成员的访问能力,使他返回可变内部域的保护行拷贝即可。

 

public Date start() { return new Date(start.getTime()); }
public Date end() { return new Date(end.getTime()); }

有经验的程序员通常使用Date.getTime()返回的long基本类型作为内部的时间表示法,而不是使用Date对象引用。因为Date是可变的。

长度非零的数组总是可变的。因此,再把内部数组返回给客户端之前,应该总要进行保护行拷贝。另一种解决方案是,给客户端返回该数组的不可变视图。(13)

如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝的成本受到限制,并且类相信他的客户端不会不切当地修改组件,就可以在文档中指明客户端的职责是不得修改受到应想的组件,以此来代替保护性拷贝。

 

40 谨慎设计方法签名

谨慎地选择方法名称。

不要过于追求提供便利的方法。

避免过长的参数列表

 

这三种方法都可以缩短过长的参数列表。


  第一种方法是把方法分解成多个方法,每个方法只需要这些参数的一个子集。(List.subList)
  第二种方法是创建辅助类来保存参数的分组。这些辅助类一般为静态成员类。如果一个频繁出现的参数序列可以被看作是代表了某个独特的实体,则建议使用这种方法。(22)
  第三种方法是从对象构建到方法调用都采用Builder模式(2)。如果方法带有多个参数,尤其是当他们中有些是可选的时候,最好定义一个对象来表示所有参数,并允许客户端在这个对象上进行多次“setter”调用,每次调用都设置一个参数,或者设置一个较小的相关集合。一旦设置了需要的参数,客户端就调用对象的执行方法,它对参数进行最终的有效性检查,并执行实际计算。

 

对于参数类型,要优先使用接口而不是类(52)。只要有适当的接口可用来定义参数,就优先使用这个接口,而不是使用实现该接口的类。


对于boolean参数,要优先使用两个元素的枚举类型。

public enum TemperatureScale { FAHRENHEIT, CELSIUS };
Thermometer.newInstance(TemperatureScalc.CELSIUS)(30) 

 

41 慎用重载

要调用哪个重载方法是在编译时作出决定的。

对於重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的。

安全而保守的策略是,永远不要到处两个具有相同参数数目的重载方法,如果方法使用可变参数,保守的策略是不要重载他。


当一个子类包含的方法声明与起祖先类中的方法声明据有相同的签名时,方法就被覆盖了。如果实例方法在子类终被覆盖了,并且这个方法是在该子类的实例上被调用的,那么子类中的服该方法将会被执行,而不管该子类实例的编译时类型到底是什么。

 

42 慎用可变参数

Java1.5中增加了可变参数方法,可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中。

 

static int min(int... args) {
  if (args.length == 0)
    throw new IllegalArgumentException("Too few arguments");

  int min = args[0];
  for (int i = 1; i < args.length; i++)
    if (args[i] < min)
      min = args[i]
  return min;
}

如果客户端调用这个方法时,并没有传递参数进去,就会在运行时而不是编译时失败。如果要去掉args中包含显式的有效性检查,除非将min初始化为Integer.MAX_VALUE;

static int min(int firstArg, int... remainingArgs) {
  int min = firstArg;
  for (int arg : remainingArgs)
    if (arg < min)
      min = arg;
  return min;
}

声明该方法带有两个参数,一个是指定类型的正常参数,另一个是这种类型的varargs参数。

 

1.5发行版中,Arrays类得到了补充完整的Arrays.toString方法,专门为了将任何类型的数组专变成字符串而设计的。

System.out.println(Arrays.toString(myArray));

public static <T> List<T> gether(T... args) {
  return Arrays.asList(args)
}

不必改造具有final数组参数的每个方法,只当确实是在数量不确定的值上执行调用时才能使用可变参数。

 

ReturnType1 suspect1(Object... args) {}
<T> ReturnType2 suspect2(T... args) {}

带有上述任何一种签名的方法都可以接受任何参数列表。改造之前进行的任何编译时类型检查都会丢失。

在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导至进行一次数组分配和初始化。

 

public void foo() {}
public void foo(int a1) {}
public void foo(int a1, int a2) {}

EnumSet类对他的静态工厂使用这种方法,最大限度的减少创建枚举集合的成本。(32)

在定义参数数目不确定的方法时,可变参数方法是一种很方便的方式,但是他们不应该被过度滥用。

 

43 返回零长度数组而不是null

Collection.toArray(Object[])

如果输入的数组足够大到能容纳这个集合的话,他将返回这个输入数组。

没有理由从一个取数组值的方法中返回null,而不是返回一个零长度数组。

 

44 为所有导出的API元素编写文档注释

为了正确的编写API文档,你必须在每一个公有的类、接口、构造函数、方法、和属性声明之前加一个文档注释。 

 

每一个方法的文档注释应该简洁的描述出他和客户之间的约定。

 

注释文档应该列举出这个方法所有的前置条件、后置条件和副作用。

前置条件:为了使使用者能够调用这个方法,必须要满足的条件。

后置条件:调用陈国完成之后,哪些条件必须要满足。

 

通用程序设计

 

45 将局部变量的作用与最小化

是一个局部变量的作用域最小化,最有利的技术是在第一次使用他们的地方声明。

过早的声明局部变量不仅使他的作用域过早的扩展,而且结束的也过于晚。

 

几乎每个局部变量的声明都应该包含一个初始化表达式。

 

这个规则有一个例外的情况,如果一个变量被一个方法初始化,而这个方法可能抛出一个受检的异常,该变量就必须在try 快的内部被初始化。如果变量在try块的外部被使用到就必须在try块之前被声明。

如果再循环终止之后不再需要循环变量的内容,for循环优先于while循环。

 

46 for-each循环优先于传统循环

Collection a = new List();
Collection b = new List();
Collection c = new List();

for (Iterator i = a.iterator(); i.hasNext();)
       for (Iterator j = b.iterator(); j.hasNext();)
              c.add(i.next(); j.next());

在迭代器上对外部集合调用太多次next方法。

 

三种情况无法使用for-each循环

1过滤     如果要遍历集合,并删除选定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法

2转换     如果需要遍历列表或数组,并取代他部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。

3平行迭代     如果需要遍历多个集合,就需要显示的控制迭代器或者所因变量,以便所有的迭代器或者索引变量都可以得到同步前移。

 

47了解和实用类库

通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的编程经验。

不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。

他们的性能往往会随着时间的推移不断提高,无需你做任何努力。

 

48 如果需要精确的答案,请避免使用float和double

float和double并没有提供完全精切的结果,所以不应该被用于需要精确结果的场合。尤其不适合用于货币计算,因为要让一个float或者double精确地表示0.1是不可能的。

 

double funds = 1.00;
int itemBought = 0;

for (double price = 0.1; funds >= price; price += 0.1) {
       funds -= price;
       itemBought++;
}

只能支付3颗糖果,并且剩下0.399999999999999

如果数值范围没有超过9为十进制数字,就可以使用int;如果不超过18位数字,就可以使用long。

 

49 基本类型优先于装箱基本类型

类型系统有两部分组成,包括基本类型和引用类型。每个基本类型都有一个对应的引用类型,称作装箱基本类型。

 

基本类型和装箱基本类型之间有三个主要区别。

基本类型只有值,而装箱基本类型则具有与他们的值不同的同一性。

基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值除外,还有非功能值null。

基本类型通常比装箱基本类型更急省时间和空间。

 

对装箱基本类型运用==操作符几乎总是错误的。

当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱,这种情况唯一例外。如果null对象引用被自动拆箱,就会得到一个NullPorintException异常。

 

使用装箱基本类型的情况:

最为集合中的元素,键和值;在进行反射的方法调用时,必须使用装箱基本类型。

自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险。

 

50 如果其他类型更合适,则尽量避免使用字符串

 字符串不适合代替其他的值类型

如果存在适合的值类型,不管是基本类型还是对象引用,大多应该使用这种类型。

如果不存在这样的类型,就应该编写一个类型。

 

字符串不适合代替枚举类型。

字符串不适合代替聚集类型。

如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的。(22)

 

String compoundKey = className + “#” + i.next();

更好的做法是,简单的编写一个类来描述这个数据集,通常是一个私有的静态成员类。

 

字符串不适合代替能力表。

例子,线程局部变量内容访问授权

 

public class ThreaLocal
{
       private ThreadLocal() {}
       public static void set(String key, Object value);
       public static Object get(String key);
}

改变为->

public final class ThreaLocal<T>
{
       public ThreadLocal() {}
       public void set(T value);
       public T get();
}

 

如果可以使用更加适合的数据类型,或者可以编写更加适当的数据类型,就应该避免用字符串来表示对象。

经常被错误的用字符串来代替的包括基本类型、枚举类型和聚集类型。

 

51 当心字符串的连接性能

为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间。
不要使用字符串连接符来合并多个字符串,除非性能无关紧要。
应该使用StringBuilder的append方法。另一种方法是使用字符数组,或者每次只处理一个字符串。

 

52 通过接口引用对象

如果有合适的接口类型存在,那么对于参数、返回值、变量和属性来说,就都应该使用接口类型进行声明。只有当你利用构造器创建某个对象的时候,才真正需要引用这个对象的类。

如果原来的实现提供了某种特殊的功能,而这种功能并不是这个接口通用约定所要求,并且周围的代码又依赖与这种功能,那么很关键的一点是,新的应用也要提供同样的功能。(Vector的同步策略) 

适用于用类来引用对象:
如果没有合适的接口存在,完全可以用类而不是接口来引用对象。比如String和BigInteger。
对象属于一个框架,而框架的基本类型是类,不是接口。(java.util.TimerTask抽象类)
<spa字符串不适合代替聚集类型。n style="font-family: 'comic sans ms', sans-serif;">类实现了接口,但是它提供了接口中不存在的额外方法。(LinkedHashMap)

 

53 接口优先于反射机制

核心反射机制(java.lang.reflect)提供了通过程序来访问关于已装载的类的信息的能力。
通过Class实例可获得Constructor、Method、Field实例。

 

反射机制允许以各类实用另一个类,既是当前被编译的时候后者还根本不存在。

代价:

  • 丧失了便已是类型检查的好处,包括异常检查。
  • 执行反射访问需要的代码非常笨拙和冗长。
  • 性能损失。反射方法调用比普通方法调用慢了许多。
  •  

如果只是以非常有限的形式使用反射机制,虽然也要付出少许代价,但是可以获得许多好处。
对于有些程序,他们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过他们可以引用这个类(52)。

 

如果是这种情况,就可以以反射的方式创建实例,然后通过他们的接口或者超类,以正常的方式访问这些事例。

如果构造器不带参数Class.newInstance方法就提供了所需功能。

 

54 谨慎的使用本地方法

Java Native Interface (JNI) 允许Java应用程序可以调用本地方法,所谓本地方法是指本地程序设计语言来编写的特殊方法。

本地方法主要有三个途径:

  1. 提供了访问特定于平台的机制的能力,比如注册表和文件锁。
  2. 提供了访问遗留代码库的能力,从而可以访问遗留数据。
  3. 编写程序中注重性能的部分,以提高系统性能。

 

55 谨慎地进行优化

在优化方面,三条规则:

  • 不要进行优化
  • 在你还没有绝对清晰的优化方案之前,请不要紧行优化。
  • 在每次试图作优化之前和之后,要对性能进行测量。

不要因为性能而牺牲和里的结构。要努力编写好的程序而不是快的程序。如果好的程序不够快,他的结构将使他可以被优化。

好的程序体现了信息隐藏的原则:只要有可能,他们就会把设计决策集中在单个模块中,因此可以改变单个决策,而不影响到系统的其他部分。(13)

努力避免那些现执行能的设计决策。
要考虑API设计决策的性能后果。
未获的好的性能而对API进行包装,这是一种不好的想法。


56 遵守普遍接受的命名惯例

 

 

第九章 异常

 

57 只针对异常的情况使用异常

异常应该只用于异常的情况下;他们永远不应该用于正常的控制流。
设计良好的API不应该强迫他的客户端为了正常的控制流而使用异常。

状态测试方法和可是别的返回值

1、如果类具有状态相关的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的状态测试方法,即指示是否可以调用这个状态的相关方法。
例如Iterator的状态相关方法next和状态测试方法hasNext


2、如果状态相关的方法被调用时,改方法处于不合适的状态之中,他就会返回一个可识别值,比如null。
这种方法对于Iterator而言并不合适,因为null是next方法的合法返回值

如果对象在缺少外部同步的情况下被并发访问,或者可被外界改变状态,使用可识别的返回值可能是很有必要的。如果但堵得状态测试方法必须重复状态相关方法的工作,应该是用可识别返回值。如果所有其他方面都是相等的,那么状态测试方法则略优先于可被识别的返回值。

 

58 对可恢复的状况使用受检异常,对编程错误使用运行时异常

Java程序设计语言提供了三种可跑出结构(Throwable)

受检的异常
运行时异常
错误

 

在决定使用受检的异常或是为受检的异常时,主要原则是:

如果期望调用者能够适当地恢复,对于这种情况就应该是用受检的异常。

有两种未受检的可抛出结构:运行时异常和错误,他们都不需要也不应该被捕获的可抛出结构。

用运行时异常来表示变成错误。大多数的运行是异常都表示前提违例。

前提违例指API的客户没有遵守API规范建立的规定。(ArrayIndexOutOfBoundsException)


你实现的所有未受检的抛出结构都应该是RuntimeException的子类。

对于可恢复的情况使用受检的异常;对于程序错误使用运行时异常。


59 避免不必要地使用受检异常

如果正确的使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合用于使用未受检的异常。

把受检的异常变成未受检的异常的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。

try {
    obj.action(args);
}
    catch (TheCheckedException e) {
    //handle exception ......
}

重构为->

if (obj.actionPermitted(args)) {
    obj.action(args);
}
else {
    // handle exception ......
}

这种重构之后的API在本质上等同于57条的状态测试方法。


60 优先使用标准异常

IllegalArgumentException 参数不正确

IllegalStateException 对象状态不合适

NullPointerException 参数值为null

IndexOutOfBoundsException 下标参数值越界

ConCurrentModificationException 对象的并发修改

UnsupportedOperationException 对象不支持用户请求的方法


61 抛出与抽想想对应的异常

更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译。(AbstractSequentialList)

try {
    // ......
}
catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}


一种特殊的异常转译形式成为异常链,如果底层的异常对于调试导致高层的异常的问题非常有帮助,使用异常链就很合适。

底层的异常被传到高层的异常,高层的异常提供访问方法来获得底层的异常。

class HigherLevelException extends Exception
{
    HigherLevelException(Throwable cause) {
    super(cause); 
    }
}    


高层异常的构造起将原因传到支持链的父类构造器,因此他最终被传给Throwable的其中一个运行异常链的构造器。例如Throwable(throwable)

大多数标准的异常都支持链的构造器。对于没有支持链的异常,可以使用Throwable的initCause方法设置原因。
尽管异常转译与不加选择地从底层传递异常的做法相比有所改进,但是他也不能被滥用。

最好办法:

  • 在调用底层方法之前确保他们会成功执行,从而避免他们抛出异常。(检查参数)
  • 让高层来悄悄地绕开这些异常,从而将高层方法的调用者与底层的问题隔离开来。(记录Log)


62 每个方法抛出的异常都要有文档

始终要单独地声明受检的异常,并且利用Javadoc的throws标记,准确地记录下抛出每个异常的条件。
使用Javadoc的@throws标签前记录下一个方法可能抛出的每个未受检异常,但是不要使用throws关键字将未受检的异常包含在方法声明中。
如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的。

 

63 在细节消息中包含能捕获失败的信息 

当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹。
在堆栈轨迹中包含该异常的字符串表示法,即他的toString方法的调用结果。
他通常包含该异常的类名,紧随其后的是细节消息。

 

为了捕获失败,异常的细节信息应该包含所有”对异常有贡献“的参数和属性的值。
例如IndexOutOfBoundsException异常的细节消息应该包含下界、上届以及没有落在界内的下标值。


64 努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。
具有这种属性的方法被称为具有失败原子性。

public Object pop {
    if (size == 0) 
        throw new EmptyStackException();
    Object result = elements[--size];
    element[size] = null;
    return result;
}     

 

获得失败原子性的办法:


  1、对于在可变对象上执行操作的方法,获得失败原子性的最长间的办法是,在执行操作之前检查参数的有效性。
    这可以使得在对象的状态被修改之前,先抛出适当的异常。

  2、调整计算处理过程的顺序,使得任何可能失败的计算部分都在对象状态被修改之前发生。
    入队参数检查只又在执行了部分计算之后才能进行,这种办法实际上是上一中办法的自然扩展。 

  3、编写一段恢复代码,由他来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态上。
    这种办法主要用于永久性的数据结构。

  4、在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。
    如果数据报存在临时的数据结构中,计算过程会更加迅速,使用这种办法就时间很自然的事。
    例如Collection.sort排序前首先把输入列表转到一个数组中。

 

错误通常誓不可恢复的,当方法抛出错误时,他们不需要努力保持失败原子性。


65 不要忽略异常

空的cache块会使异常达不到应有的目的,即强迫你处理异常的情况。

try {
    // ......
}
catch (SomeException e) {
}

至少,cache块应该包含一条说明,解释为什么可以忽略这个异常。

 

有一中情况可以忽略异常,即关闭FileInutStream的时候。因为你还没有改变文件的状态,因此不必执行任何恢复动作,并且已经从文件中读取到所有需要的信息,因此不必终止正在进行的操作。即使这种情况下,把异常记录下来也是明治的做法。

  

 

第十章 并发

 


66 同步访问共享的可变数据

对象在被创建的时候处于一直的状态,当有方法访问他的时候,他就被锁住了。这些方法观察到对象的状态,并且可能会引起状态转变,即把对象的一种一致状态转换到另一种一致的状态。

 

while (!done) 
    i++;

优化后->

if (!done)
    while (true)
        i++;  

导致状态的该并永远不会发生。这种优化称作提升,正是HotSpot Server VM的工作。结果是个活性失败。

 

为了提高性能,在读写原子数据的时候,应该避免使用同步。这个建议是非常危险而错误的。

虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是他并不保证一个线程写入的值对于另一个线程是可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。

 

如果读和写操作没有都被同步,同步就不会起作用。 

private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
        return nextSerialNumber++;
}

 

问题在于,增量操作符(++)不是原子的。他在nextSerialNumber域中执行两项操作:

首先读取值,然后写回一个新值,相当于原来的值再加上1

如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域

第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败。

 

修正generateSerialNumber方法的办法是增加synchronized修饰符、Atomic类或者使用锁。来确保调用不会交叉存取。

 

private static final AtomicLong nextSerialNum = new AtomicLong();
publib static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

只同步共享对象引用的动作。然后其他线程没有进一步的同步也可以读取对象,只要他没有再被修改。

这种对象被称作事实上不可变的。将这种对象引用从一个线程传递到其他的线程被称作安全发布。

 

安全发布对象引用有许多种方法:

  • 可以将它保存在静态域中,作为类初始化的一部分;
  • 可以将它保存在volatile域、final域或者通过正常锁定访问的域中;
  • 或者可以将它放到并发的集合中。

当多个线程共享可变数据的时候,每个读或者写书据的线程都必须执行同步。

 

67 避免过渡同步

为了避免活性失败和安全性失败,在一个被同步的方法或者代码中,永远不要放弃对客户端的控制。

在一个同步的区域内部,不要调用设计成被覆盖的方法,或者是由客户端以函数对象的形式提供的方法(21)。

在同步区域外被调用的外来方法被称作开放调用。除了可以避免死锁之外,开放调用还可以极大的增加并发性。

 

通常,你应该在同步区域内作尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后释放锁。如果你必须要执行某个很耗时的动作,则应该设法把这个动作移到同步区域外面,而不违背第66条中的指导方针。


如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁、分离锁和非阻塞并发控制。

如果方法修改了静态域,那么你也必须同步对这个域的访问,即使他往往之用于单个线程。

 为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。

 

68 executor和task优先于线程

ExecutorCompletionService

ScheduledThreadPoolExecutor

如果相让不知一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池。

 

69 并发工具优先于wait和notify

concurrent中更高级的工具分成三类:

  • Executor Framwork 执行框架
  • Concurrent Collection 并发容器
  • Synchronizer 同步器

 

并发集合中不可能排除并发活动;将他所定没有什么作用,只会使程序速度更慢。

同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作。最长用的同步器是CountDownLatch和Semaphore,不常用的是CyclicBarrier和Exchanger。

 

public static long time(Executor executor, int concurrency, final Runnable action) throws InterruptedException {
  final CountDownLatch ready = new CountDownLatch(concurrency);
  final CountDownLatch start new CountDownLatch(1);
  final CountDownLatch done = new CountDownLatch(concurrency);

  for (int i = 0; i < concurrency; i++) {
    executor.execute(new Runnable() {
      public void run() {
        ready.countDown();
        try {
          start.await();
          action.run();
        }
        catch (InterruptedException e) {
          Thread.currentThread().interrupt();
        }
        finally {
          done.countDown(); 
        }
      } 
    });
  }

  ready.await();
  long startTime = System.nanoTime();
  start.countDown();
  done.await();
  return System.nanoTime() - startTime;
}

对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMillis。System.nanoTime更加准确也更加精确,他不受系统的实时时钟调整所影响。


始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。

等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程等待之前,notify方法已被调用,则无法保证该线程从等待中苏醒过来。

等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系。

 

当条件不成立时,下面一些理由可是一个线程苏醒过来:

  • 另一个线程可能已经的到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态。
  • 条件并不成立,但是另一个线程可能意外地或恶意的调用了notify。
  • 通知线程在唤醒等待线程时即使只有某些等待线程的条件已经被满足,当时通知线程可能仍然调用notifyAll。
  • 在没有通知的情况下,等待线程也可能会苏醒过来。这称为伪唤醒。 

 

如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条件中被唤醒,那么你就应该选择调用notify,而不是notifyAll。

一般情况下,你应该优先使用notifyAll,而不是使用notify。

 

70 线程安全性的文档化

在一个方法声明中出现synchronized修饰符,这是个实现细节,并不是导出的API的一部分。

一个类为了可悲多个线程安全地使用,必须在文档中清楚地说明他所支持的线程安全级别。


包括:

  • 不可变的(String Long BigInteger)
  • 无条件的线程安全 (Random ConcurrentHashMap)
  • 有条件的线程安全 (Collections.synchronized)
  • 非线程安全 (ArrayList HashMap)
  • 线程对立的

 

有条件的线程安全必须指明:哪个方法条用序列需要外部同步,以及在执行这些序列的时候要获得哪把锁。

应对于Java并发编程实践一书中的线程安全注解。在文档中描述一个有条件的线程安全类要特别小心。

你必须致命那个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把所。

例外(一个对象代表了另一个对象的一个视图,用户通常就必须在后台对象上同步,以防止其他线程直接修改后台对象)

 

Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>);

Set<K> s = map.keySet();
synchronized (map) {
    for (K key : s) {
        k.method();
    }
} 

为了避免超时地保持公有可访问锁的攻击,应该使用一个私有锁对象来代替同步的方法。

 

private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        // ......
    }
}

 

 

lock域被声明为final的。这样可以防止不小心改变他的内容,而导致不同步访问对象的悲惨结果。

私有锁对象模式只能用在无条件的线程安全类上。有条件的线程安全类不能使用这种模式。

私有锁对象模式特别适用于那些专门为继承而设计的类。

如果这种类使用他的实例作为锁对象,子类可能很容易在无意种妨碍基类的操作,反之亦然。

 

71 甚用延迟初始化

延迟初始化是延迟到需要域的值时才能将它初始化的这种行为。

就像大多数的优化一样,对于延迟初始化,最好建议“除非绝对必要,否则就不要这么做”。

如果属性只在类的实例部分被访问,并且初始化这个属性的开销很高。可能就值得进行延迟初始化。

当有多个线程时,延迟初始化是需要技巧的。如果多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的。

在大多数情况下,正常初始化要优先于延迟初始化。

 

private final FieldType field = computeFieldValue();

如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法。

 

private FieldType field;
synchronized FieldType getField() {
    if (field == null) {
        field = computeFieldValue(); 
    } 
    return field;
}

如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。

 

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

static FieldType getField() {
    return FieldHolder.field; 
}

当个getField方法第一次被调用时,他第一次读取FieldHolder.field,导致FieldHolder类得到初始化。这种模式的魅力在于,getField方法没有被同步,并且只能执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。


如果处于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式。

这种模式避免了在域被初始化之后访问这个域时的锁定开销。

思想是:两次检查域的值(双重检查),第一次检查时没有锁定,看看这个域是否被初始化了;第二次检查时有锁定。当只有第二次检查时表明这个域没有被初始化,才会调用computeFieldValue方法对这个域进行初始化。

 

private volatile FieldType field;

FieldType getField() {
    FieldType result = field;
    if (result == null) {
        synchronized (this) {
            result = field;
            if (result == null) {
                field = result = computeFieldValue(); 
            }
        } 
    }
    return result;
}

对于需要用到局部变量result可能优点不解。这个变量的作用是确保field只能在已经被初始化的情况下读取一次。


单重检查模式:有时候你可能需要延迟初始化一个可以接受重复初始化的实例域。如果处于这种情况,就可以使用双重检查惯用法的一个变形,他省去了第二次检查。

 

private 
h4volatile FieldType field;

public FieldType getField() {
    FieldType result = field;
    if (result == null)
    field = result = computeFieldValue();
    return result;
}

当双重检查模式或者单重检查模式应用到数值类型的基本类型域时,就会用0来检查这个域,而不是用null。


如果你不在意是否每个线程都重新计算域的值,并且域的类型为基本类型,而不是long或者double类型,就可以选择从单重检查模式的域声明中删除volatile修饰符。他加快了某些架构上的域访问,代价是增加了额外的初始化访问该域的每一个线程都要进行一次初始化。


对于实例域,就可以使用双重检查模式;对于静态域,则可以使用惰性初始化;对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式。

 

72 不要依赖于线程调度器

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。

要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。

保持可运行线程数尽可能少的主要方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作。如果线程没有在做有意义工作,就不应该运行。

线程不应该一直处于忙等得状态,即反复地检查一个共享对象,以等待某些事情发生。

 

public class SlowCountDownLatch {
    private int count;

    public SlowCountDownLatch(int count) {
        if (count < 0) 
            throw new IllegalArgumentException();
        this.count = count;
    }

    public void await() {
        while (true) {
            synchronized (this) {
                if (count == 0) return;
            }
        }
    }

    public void countDown() {
        if (count != 0)
            count--;
    }
}

如果某一个程序不能工作,是因为某些线程无法向其他线程那样获得足够的CPU时间,那么,不要企图通过调用Thread.yield来修正该程序。

对于大多数程序员来说,Thread.yield的唯一用途是在测试期间人为地增加程序的并发性。通过探查程序中更大部分的状态空间,可以发现一些隐蔽的Bug。

 

73 避免使用线程组

除了线程、锁和监视器之外,线程系统还提供了一个基本的抽象,即线程组。

线程组并没有提供太多有用的功能,而且他们提供的许多功能还都是有缺陷的。

 

 

第11章 序列化

 

74 谨慎地实现Serializable接口

实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了改变这个类实现的灵活性。

如果你不努力设计一种自定义的序列化形式,而仅仅接受了默认的序列化形式,这种序列化形式将被永远的束缚在该类最初的内部表示方法上。(13)

序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符有关,通常他也被成为序列版本UID。

实现Serializable的第二个代价是,他增加了出现Bug和安全漏洞的可能性。

实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。


为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少的继承Serializable接口。

对于为了继承而设计的不可序列化的类,你应该考虑提供一个无参构造起。


有一种办法可以给“不可序列化但可扩展的类”增加无参构造器:

public AbstractFoo(int x, int y) { ... }

 

x和y的变量不能是final的

public abstract class AbstractFoo 
{   
private int x, y;   private enum State {NEW, INITIALIZING, INITIALIZED};   private final AtomicReference<State> init = nwe AtomicReference<State>(State.NEW);   protected AbstractFoo() {}   public AbstractFoo(int x, int y) { initialize(x, y);}
  
protected final void initialize()
  {     
if (!init.compareAndSet(State.NEW, State.INITIALIZING))     throw new IllegalStateException("Already initialized");     this.x = x;     this.y = y;     init.set(State.INITIALIZED);   }   protected final int getX() { checkInit(); return x; }   protected final int getY() { checkInit(); return y; }
  
private void checkInit()
  {     
if (init.get() != State.INITIALIZED)       throw new IllegalStateException("Uninitialized");   } }

AbstractFoo中所有公有的和受保护的实例方法在开始作任何其他工作之前都必须先调用ehckInit。

这样可以确保如果有编写不好的子类没有初始化实例,该方法调用就可以快速而干净地失败。


这种模式利用compareAndSet方法来操作枚举的原子引用,则是一个很好的线程安全状态机的通用实现。

public class Foo extends AbstractFoo implements Serializable 
{   
private void readObject(ObjectInputStream s) throws IOExceptioin, ClassNotFoundException {     s.defaultReadObject();     int x = s.readInt();     int y = s.readInt();     initialize(x, y);   }   private void writeObject(ObjectOutputStream s) throws IOException
  {     s.defaultWriteObject();     s.writeInt(x);     s.writeInt(y);   }   
public Foo() { super(x, y); }
  
private static final long serialVersionUID = 743647836L; }

内部类不应该是现Serializable。他们使用编译器产生的和成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值。(22)

内部类的默认序列化形式是定义不清楚的。然而,静态成员类确是可以实现Serializeable接口。



75 考虑使用自定义的序列化形式

 

如果没有先认真考虑默认的序列化形式是否合适、则不要贸然接受。

如果一个对象的物理表示法等同余他的逻辑内容,可能就适合于使用默认的序列化形式。

public class Name implements Serizlizable
{
  private final String lastName;
  private final String firstName;
  private final String middleName;
}

 

即使你确定了默认的序列化形式是最适合的,通常还必须提供一个readObject方法以保证结束关系和安全性。

@serial标签告诉Javadoc工具,把这些文档信息放在有关序列化形式的特殊文档中。

public final class StringList implements Serializable
{
  private int size = 0;
  private Entry head = null;


  private static class Entry implements Serializable
  {
    String data;
    Entry next;
    ENtry prev;
  }
}

从逻辑上讲,这个类表示了一个字符串序列。但是从物理意义上讲,他把该系列表示成了一个双向链表。

如果你接受了默认的序列化方式,该序列化形式将不遗余力地镜像出链表中的所有的项,以及这些项之间的所有双向链表。

 

当一个对象的物理表示法与他的逻辑数据内容有实质的区别时,使用默认序列化形式会有以下4个缺点:

  1. 他使这个类导出API永远地束缚在该类的内部表示法上。
  2. 他会消耗过多的空间。
  3. 他会消耗过多的时间。
  4. 他会引起栈溢出。 

 

上一个例子的修订版

public final class StringList implements Serializable
{
  private transient int size = 0;
  private transient Entry head = null;


  private static class Entry
  {
    String data;
    Entry prev;
    Entry next;
  }


  private void writeObject(ObjectOutputStream s) throws IOException
  {
    s.defaultWriteObject();
    s.writeInt(size);
    for (Entry e = head; e != null; e = e.next) {
      s.writeObject(e.data);
    }
  }

  private void readObject(ObjectOutputStream s) throws IOException
  {
    s.defaultReadObject();
    int numElements = s.readInt();


    for (int i = 0; i < numElements; i++) {
      add((String) s.readObject()); 
    }
  }
}


如果所有的实例域都是瞬时的,从技术角度而言,不调用defaultWriteObject和defaultReadObject也是允许的,但是不推荐这样作。

如果一个实例将在为来的版本中被序列化,旧版本的readObject方法没有调用defaultReadObject,反序列化过程将失败,引发StreamCorruptedException异常。


对于散列表对象进行序列化和反序列化操作所产生的对象,其约束关系会遭到严重的破坏。(键-值散列)

在决定将一个域做成非transient的之前,请一点个要确信他的值将是该对象逻辑状态的一部分。

 

如果你正在使用一种自定义的序列化形式,大多数实例域,或者所有的实例域则都应该被标记为transient,就像上面例子中的StringList那样。 

如果你正在使用默认的序列化形式,并把一个或者多个域标记为transient,则要记住,当一个实例域背反序列化的时候,这些域将被初始化为默认值。

如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。(70)

 

private synchronized void writeObject(ObjectOutputStream s) throws IOException
{
  s.defaultWriteObject(); 
}

不管你选择哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。 这样可以避免序列版本UID成为潜在的不见容根源。(版本不一致引发InvalidClassException异常)

private static final long serialVersionUID = randomLongValue;  

 

76 保护性地编写readObject方法

当一个对象被反序列化的时候,对于客户端,不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护行拷贝,这是非常重要的。

readObject方法实际上相当于另一个公有的构造器,如同其他的构造器一样,他也要求注意同样的所有注意事项。构造器必须检查其参数的有效性(38),并且在必要的时候对参数进行保护行拷贝(39)。  

// class Period implements Serializable
private void readObject(ObjectInputStream s) throws IOException
{
  s.defaultInputStream();
  start = new Date(start.getTime());
  end = new Date(end.getTime());
  
if (start.compareTo(end) > 0)     throw new InvalidObjectException(); }

注意,保护行拷贝是在有效性检查之前进行的,而且,我们没有使用Date的clone方法来执行保护行拷贝。这两个细节对于保护Period免受攻击是必要的(39)。同时也要注意到,对于final域,保护行拷贝是不可能的。为了使用readObject方法,我们必须要将start和end域做成非final的。


测试方法:增加一个公有的构造器,其参数对应于该对象中每个非transient的域,并且无论参书的值是什么,都是不进行检查就可以保存到相应的域中的。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的readObject方法,并且他必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种办法是使用序列化代理模式(78)。

 

在readObject方法和构造器之间还有其他类似的地方。readObject方法不可以调用可被覆盖的方法,无论是之皆调用还是间接调用都不可以。(17)

 

77 对于实例控制,枚举类行优先于readResolve

 

如果类的声明上加上了"implements Serizlizable"的字样,他就不是一个Singleton。无论该类使用了默认的序列化形式,还是自定义的序列化形式(75),都没有关系;也跟他是否提供了显式的readObject方法(76)无关。任何一个readObject方法,不管是显式的还是默认的,他都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

 

下面的readResolve方法足以保证他的Singleton属性:

public class Elvis
{
    public static final Elvis INSTANCE = new Elvis();


    private Elvis() {}


    public void leaveTheBuilding() {}


    private Object readResolve()
    {
       return INSTANCE; 
    }
}        

 

该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个特殊的Elvis实例。因此,Elvis实例的序列化形式并不需要包含任何实际的数据;所有的实例域都应该被声明为transient的。

如果以来readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。

 

readResolve的可访问性很重要。如果把readResolve方法方在一个final类上,他就因该是私有的。如果把readResoler方法方在一个非final的类上,就必须认真考虑他的可访问性。

  • 如果他是私有的,就不适合用于任何子类。
  • 如果他是包级私有的,就只适用于同一个包中的子类。
  • 如果他是受保护的或者公有的,就适用于所有没有覆盖他的子类。
  • 如果readResolve方法是受保护或者公有的,并且子类没有覆盖他,对序列化过得子类实例进行反序列化就会产生一个超类实例。

 

78 考虑用序列化代理代替序列化实例

序列化代理模式相当简单。首先未可序列化的类设计一个私有的静态嵌套类,精确的表示外围类的实例的逻辑状态。这个嵌套类被称作序列华代理,他应该有一个单独的构造器,其参数类型就是那个外围类。这个构造起只从他的参数中复制数据。

 

他不需要进行任何一致性检查或者保护行拷贝。外围类及其序列代理都必须声明实现Serializable接口

private static class SerializationProxy implements Serializable
{
  private final Date start;
  private final Date end;

  SerializationProxy(Period p)
  {
    this.start = p.start;
    this.end = p.end;
  }

  private static final long serialVersionUID = 85943085340L;

  private Object writeReplace()
  {
    return new SerializationProxy(this); 
  }

  private void readObject(ObjectInputStream stream) throws InvalidObjectException
  {
    throws new InvalidObjectException("Proxy required");
  }
}

 

在SerializationProxy类中提供一个readResolve方法,他返回一个逻辑上相当的外围类的实例。这个方法的出现,导致序列化系统在反序列化时将序列化代理转变回外围类的实例。

private Object readResolve()
{
  return new Period(start, end); 
}

正如保护行拷贝方法一样,序列华代理方法可以阻止伪字节流的攻击以及内部域的盗用攻击。与前两种方法不同,这种方法允许Period的域为final的,为了确保period类真正是不可变的(15),这一条很有必要。

 

这个类没有共有的构造器,只有静态工厂,当他被序列化的时候,是一个RegularEnumSet实例,但是一旦他被反序列化,他最好是一个JumboEnumSet实例。

private static class SerializationProxy<E extends Enum<E>> implements Serializable
{
  private final Class<E> elementType
  private final Enum[] elements;

  SetializationProxy()
  {
    elementType = set.elementType;
    elements = set.toArray(EMPTY_ENUM_ARRAY);
  }

  private Object readResolve()
  {
    EnumSet<E> result = EnumSet.noneOf(elementType);
    for (Enum e : elements) {
      result.add((E) e);
    }
    return result;
  }

  private static final long serialVersionUID = 74839274932L;
}

 

序列化代理模式有两个局限性。他不能与可以被客户端扩展的类兼容(17)。他也不能于对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCaseException异常,因为你还没有这个对象,只有他的序列化代理。

 

如果你不努力设计一种自定义的序列化形式,而仅仅接受了默认的序列化形式,这种序列化形式将被永远的束缚在该类最初的内部表示方法上。(13)

posted @ 2012-10-24 17:58  rilley  阅读(1075)  评论(0编辑  收藏  举报