《Effective Java 》系列一

简介: image.png目录第二章 创建和销毁对象1 考虑用静态工厂方法替代构造器2 遇到多个构造器参数时要考虑用构件器3 用私有构造器或者枚举类型强化Singleton属性4 通过私有构造器强化不可实例化的能力5 避免创建不必要的...
img_66a51b3ee468d05e6469ea31284216ed.png
image.png

目录

第二章 创建和销毁对象
1 考虑用静态工厂方法替代构造器
2 遇到多个构造器参数时要考虑用构件器
3 用私有构造器或者枚举类型强化Singleton属性
4 通过私有构造器强化不可实例化的能力
5 避免创建不必要的对象
6 消除过期的对象引用
7 避免使用终结方法

第三章 对于对象都通用的方法
8 Equals方法
9 HashCode方法
10 ToString方法
11 Clone方法
12 Comparable接口

第四章 类和接口 
13 使类和成员的可访问性最小化
14 在公有类中使用访问方法而非共有域
15 支持非可变性
16 复合优先于继承
17 要么专门为继承而设计,并给出文档说明,要么禁止继承
18 接口优于抽象
19 接口只是被用来定义类型
20 类层次优先于标签类
21 用函数对象表示策略
22 优先考虑静态成员类

第十章 并发
66 同步访问共享的可变数据
67 避免过渡同步
68 executor和task优先于线程
69 并发工具优先于wait和notify
70 线程安全性的文档化
71 甚用延迟初始化
72 不要依赖于线程调度器
73 避免使用线程组

第二章 创建和销毁对象

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

对于代码来说, 清晰和简洁是最重要的.

代码应该被重用, 而不应该被拷贝

模块之间的依赖应该尽可能的小.

错误应该尽早被检测出来, 最好是在编译时刻.

代码的清晰, 正确, 可用, 健壮, 灵活和可维护性比性能更重要.

编程的艺术应该是先学会基本规则, 然后才能知道在什么时候打破这些规则.

静态工厂方法惯用的名称:

  • valueOf
  • of
  • getInstance
  • newInstance
  • getType
  • newType

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

实例受控类

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);

这种方可以被进一步扩展。不可变的类可以提供一些静态工厂,他们把频繁被请求的实例缓存起来,从而当现有实例可以符合请求的时候,就不必创建新的实例。
不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。

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

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

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

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

  • 让这个类的没一个方法都成为final的,而不是让整个类都成为final的。(可扩展)
  • 使其所有的构造方法成为私有的,或者包级私有的,并增加送有静态工厂来替代个构造函数。(静态工厂)
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)   
    }
}

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

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

第十章 并发

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
volatile 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 避免使用线程组

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

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


个人介绍:

高广超:多年一线互联网研发与架构设计经验,擅长设计与落地高可用、高性能、可扩展的互联网架构。

本文首发在 高广超的简书博客 转载请注明!

img_31e2e3075b097cabfd9b3643cd9abaa5.png
简书博客
img_3b1610566b09db3358ca5dcb3e015a52.png
头条号
目录
相关文章
|
4月前
|
设计模式 Java C语言
Java之父都需要的《Effective Java中文版(第3版)》到底有多牛
你是否正在寻找一本能够更加深入地了解Java编程语言的书,以便编写出更清晰、更正确、更健壮且更易于重用的代码?
|
2月前
|
存储 安全 Java
【Java】Effective Lambda Expressions in Java
【Java】Effective Lambda Expressions in Java
47 0
|
4月前
|
存储 安全 Java
【Java】Effective Lambda Expressions in Java(二)
【Java】Effective Lambda Expressions in Java
24 0
|
4月前
|
Java API
【Java】Effective Lambda Expressions in Java(一)
【Java】Effective Lambda Expressions in Java
29 0
|
8月前
|
设计模式 Java C语言
Java之父都需要的《Effective Java中文版(第3版)》到底有多牛
你是否正在寻找一本能够更加深入地了解Java编程语言的书,以便编写出更清晰、更正确、更健壮且更易于重用的代码? Effective Java中文版(第3版) 再适合不过了!这是一本分享经验并指引你少走弯路的经典著作,针对如何编写高效、设计优良的程序提出了最实用、最权威的指导方针,通过90条经验法则,探索新的设计模式和语言习惯用法,帮你更加有效地使用Java编程语言及其基本类库。
|
11月前
|
设计模式 Java C语言
Java之父都需要的《Effective Java中文版(第3版)》到底有多牛
你是否正在寻找一本能够更加深入地了解Java编程语言的书,以便编写出更清晰、更正确、更健壮且更易于重用的代码? Effective Java中文版(第3版) 再适合不过了!这是一本分享经验并指引你少走弯路的经典著作,针对如何编写高效、设计优良的程序提出了最实用、最权威的指导方针,通过90条经验法则,探索新的设计模式和语言习惯用法,帮你更加有效地使用Java编程语言及其基本类库。
|
12月前
|
存储 IDE 安全
Effective Java 第六章 枚举和注解2
Effective Java 第六章 枚举和注解
63 0
|
12月前
|
存储 安全 小程序
Effective Java 第六章 枚举和注解1
Effective Java 第六章 枚举和注解
63 0
|
12月前
|
存储 机器学习/深度学习 安全
Effective Java 第五章 泛型(2)
Effective Java 第五章 泛型(2)
139 0
|
12月前
|
存储 安全 Java
Effective Java 第五章 泛型(1)
Effective Java 第五章 泛型(1)
80 0