考虑用静态工厂方法代替构造器
静态工厂方法的优势
- 它们有名称:具有适当名称的静态工厂更容易使用和易读(通过构造器重载经常会调用错误,因为有的构造器只是参数类型的顺序不同)
- 不必在每次调用的时候都创建一个新对象。便于对象的重复利用
- 他们可以返回原返回类型的任何子类型的对象。通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象(好习惯),取决于静态工厂的参数值,提高系统的可维护性。
- 在参数化类型实例的时候,使代码变得简洁
静态工厂方法的缺点
- 类如果不含有pubic或者protect的构造器,就不能被子类化。
- 他们与其他的静态方法实际上没有任何区别。
遇到多个构造器参数时要考虑用构造器
静态工厂和构造器的局限性
他们都不能很好地扩展到大量的可选参数
解决办法
- 采用重叠构造器模式。(参数多的时候,客户端代码难写,且难以阅读)
- JavaBean模式。在构造过程中JavaBean可能处于不一致状态。
Builder模式。既有重叠构造器的安全性,又有JavaBean模式的可读性。
与构造器相比,Builder的优势在于,builder可以有多个可变参数。自身的不足:为了创建对象,必须先创建它的构造器。
用私有构造器或者枚举类型强化Singleton
- Singleton通常被用来代表那些本质上唯一的系统组件。
1.5之前,实现Singleton有两种方法。都需要私有构造器
- 1.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
借助AccessibleObject.setAccessible方法,通过反射可以调用私有构造器。抵御这种攻击,可以在构造器中当第二次创建实例的时候抛出异常
2.
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
public static void main(String[] args) {
Elvis elvis = Elvis.getInstance();
elvis.leaveTheBuilding();
}
}
同上。
工厂方法好处:在不改变api的前提下,我们可以改变该类是否应该为Singlethon的想法
公有域方法好处:清楚的表明这个类是一个Singleton
- 包含单个元素的枚举类型
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {
System.out.println("Whoa baby, I'm outta here!");
}
// This code would normally appear outside the class!
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
}
功能上与公有方法相近,提供了序列化机制。
通过私有构造器强化不可实例化的能力
- 有时候可能只需要静态方法和静态域的类。比如java.util.Collections,java.util.Arrays 这样的类实例没有任何意义。
- 通过将类做成抽象类来强制该类不可被实例化是行不通的。因为该类可以被子类话,子类可以被实例化。
- 当类不包含显示构造器时,编译器才会生成缺省的构造器,因此私有构造器,这个类就不能被实例化了。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
}
AssertionError避免在类内部调用构造器
这种做法的副作用:使得其不能被子类化。子类没有可访问的超类构造器
避免创建不必要的对象
- 重用不可变对象,也可以从用那些已知不会被修改的可变对象
import java.util.*;
public class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
}
每次都创建一个Calendar、TimeZone是不必要的
改进
import java.util.*;
class Person {
private final Date birthDate;
public Person(Date birthDate) {
// Defensive copy - see Item 39
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}
这样Calendar、TimeZone只会被实例化一次
- 优先使用基本类型而不是而不是装箱基本类型。要当心无意识的自动装箱
public class Sum {
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
}
构造了大量的Long实例
因重用对象而付出的代价要远大于创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,会导致潜在的错误和安全漏洞。不必要的创建对象则只会影响程序的风格的性能
消除过期的对象引用
// Can you spot the "memory leak"?
import java.util.*;
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
当栈先增长再收缩时,从栈中弹出的对象不会被当作垃圾回收
清空过期数据
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
elements[size] = null;
return result;
}
==只要类是自己管理内存,就应该警惕内存泄露问题==
* 内存泄漏的另一个常见来源时缓存。一旦把对象引用放到缓存中,就很容易被遗忘,从而使得它不再有用之后很长一段时间内仍然留在缓存。
* 内存泄漏的第三个常见来源时监听器和其他回掉。
覆盖equals时请遵守通用约定
需要满足的条件
- 类的每个实例本质上都是唯一的。
- 不关心类是否提供了“逻辑相等”的测试功能。
- 超类已经覆盖了equals,从超类继承过来的行为对子类也是合适的。
- 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
- *
什么时候应该覆盖Object.equals?
如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖equals以实现期望的行为,这时需要覆盖equals方法。
覆盖equals方法需要遵守的通用约定。
- 自反性。x.eqauls(x)必须返回true
- 对称性。y.equals(x)返回true时,x.equals(y)必须返回true
- 传递性。x.equals(y)返回true且y.equals(z)返回true,那么x.equals(z)也必须返回true
- 一致性。只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或false
- 对于任何非null的引用值x,x.equals(null)必须返回false
实现高质量equals方法的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”
- 使用instanceof操作符检查“参数是否为正确的类型”
- 把参数转换成正确的类型
- 对于该类中国年的关键域,检查参数中的域是否与该对象中的域相匹配。==域的比较顺序可能会影响到equals方法的性能。应该最先比较最有可能不一致的域,或者时开销最低的域==
- 当编写完成equals方法后,应该要问自己三个问题:它是否时对称的、传递的、一致的
- 覆盖equals方法时总要覆盖hashCode
- 不要企图让equals方法过于只能。
* 不要奖equals声明中的Object对象替换为其他的类型
覆盖equals时总要覆盖hashCode
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashcode都应该返回同一个整数。
- 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的任意一个对象的hashcode方法都必须产生同样的整数结果。
- 如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashcode方法,则不一定要产生不同的结果。给不相等的对象产生截然不同的hashcode,有可能提高散列表的性能
// Shows the need for overriding hashcode when you override equals - Pages 45-46
import java.util.*;
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix,
int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max,
String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// Broken - no hashCode method!
// A decent hashCode method - Page 48
// @Override public int hashCode() {
// int result = 17;
// result = 31 * result + areaCode;
// result = 31 * result + prefix;
// result = 31 * result + lineNumber;
// return result;
// }
// Lazily initialized, cached hashCode - Page 49
// private volatile int hashCode; // (See Item 71)
//
// @Override public int hashCode() {
// int result = hashCode;
// if (result == 0) {
// result = 17;
// 31是一个奇素数,如果乘数是偶数,并且乘法溢出的话,信息就会丢失,习惯上都适用素数计算散列结果。31有个很好的特性即用移位和减法代替乘法,可以得到更好的性能:31*i == (i<<5)-i
// result = 31 * result + areaCode;
// result = 31 * result + prefix;
// result = 31 * result + lineNumber;
// hashCode = result;
// }
// return result;
// }
public static void main(String[] args) {
Map<PhoneNumber, String> m
= new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
}
}
一个好的散列函数通常倾向于:为不相等的对象产生不相等的散列码
始终要覆盖toString
- ==建议所有的子类都覆盖toString这个方法==
如果没有覆盖toString方法,产生的消息将难以理解.例如:PhoneNumber@163b91
// Adding a toString method to PhoneNumber - page 52
import java.util.*;
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix,
int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max,
String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
/**
* Returns the string representation of this phone number.
* The string consists of fourteen characters whose format
* is "(XXX) YYY-ZZZZ", where XXX is the area code, YYY is
* the prefix, and ZZZZ is the line number. (Each of the
* capital letters represents a single decimal digit.)
*
* If any of the three parts of this phone number is too small
* to fill up its field, the field is padded with leading zeros.
* For example, if the value of the line number is 123, the last
* four characters of the string representation will be "0123".
*
* Note that there is a single space separating the closing
* parenthesis after the area code from the first digit of the
* prefix.
*/
@Override public String toString() {
return String.format("(%03d) %03d-%04d",
areaCode, prefix, lineNumber);
}
public static void main(String[] args) {
Map<PhoneNumber, String> m
= new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(m);
}
}
谨慎地覆盖clone
- 实现Cloneable接口作用:它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则抛出CloneNotSupporteException异常。
覆盖clone方法的几个约定:
- x.clone() != x
- x.clone().getClass() == x.getClass();
- x.clone().equals(x)
// Adding a clone method to PhoneNumber - page 55
import java.util.*;
public final class PhoneNumber implements Cloneable {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix,
int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max,
String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
/**
* Returns the string representation of this phone number.
* The string consists of fourteen characters whose format
* is "(XXX) YYY-ZZZZ", where XXX is the area code, YYY is
* the prefix, and ZZZZ is the line number. (Each of the
* capital letters represents a single decimal digit.)
*
* If any of the three parts of this phone number is too small
* to fill up its field, the field is padded with leading zeros.
* For example, if the value of the line number is 123, the last
* four characters of the string representation will be "0123".
*
* Note that there is a single space separating the closing
* parenthesis after the area code from the first digit of the
* prefix.
*/
@Override public String toString() {
return String.format("(%03d) %03d-%04d",
areaCode, prefix, lineNumber);
}
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
public static void main(String[] args) {
PhoneNumber pn = new PhoneNumber(707, 867, 5309);
Map<PhoneNumber, String> m
= new HashMap<PhoneNumber, String>();
m.put(pn, "Jenny");
System.out.println(m.get(pn.clone()));
}
}
结果:Jenny
如果对象中包含的域引用了可变的对象,使用上述简单的clone方法会导致严重后果。
比如说前面的Stack类
// A cloneable version of Stack - Pages 56-57
import java.util.Arrays;
public class Stack implements Cloneable {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public boolean isEmpty() {
return size == 0;
}
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// To see that clone works, call with several command line arguments
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
Stack copy = stack.clone();
while (!stack.isEmpty())
System.out.print(stack.pop() + " ");
System.out.println();
while (!copy.isEmpty())
System.out.print(copy.pop() + " ");
}
}
如果该类的clone方法近返回super.clone(),那么这样得到的Stack实例,在其size域中具有正确的值,
但是它的elements域将引用与原始Stack实例相同的数组
- 简而言之,所有实现了Cloneable接口的类都应该用一个公有的方法覆盖clone。该公有方法首先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含内部“深层结构”的可变对象,并用指向新对象的引用替代原来指向这些对象的引用。虽然,这些内部拷贝操作往往可以通过递归地调用clone来完成,但是这通常不是最佳的方法。如果你扩展了一个实现了Cloneable接口的类,那么除了实现一个行为良好的clone方法之外,没有别的选择。
==另一个实现对象拷贝的好方法是提供一个拷贝构造器或拷贝工厂==
考虑实现Comparable接口
- 类实现了Comparable接口,就表明它的实例具有内在的排序关系
- 如果正在编写一个值类,它具有明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现这个接口
- 将一个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数,零,或者正整数。如果由于指定对象的类型无法与该对象进行比较,则抛出ClassCastException异常
compareTo约定:自反性 、对称性、传递性
* 实现者必须确保所有的x和y都满足sng(x.compareTo(y)) == -sgn(y.compareTo(x))
* (x.compareTo(y) > 0 && y.compareTo(z))暗示着x.compareTo(z) > 0
* 最后x.compareTo(y) == 0暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
* 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但这并非是必要的。若违反了这个条件需要予以说明。
Collection、Set或Map通用约定是按照equals方法来定义的,但是有序集合使用了compareTo方法施加等同性测试
CompareTo方法中域的比较是顺序的比较,而不是等同性的比较。比较对象引用域可以是通过递归地调用compareTo方法来实现。
如果一个类有多个关键域,那么,按照关键程度,逐步比较所有重要域
// Making PhoneNumber comparable - Pages 65-66
import java.util.*;
public final class PhoneNumber implements Cloneable, Comparable<PhoneNumber> {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix,
int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max,
String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
/**
* Returns the string representation of this phone number.
* The string consists of fourteen characters whose format
* is "(XXX) YYY-ZZZZ", where XXX is the area code, YYY is
* the prefix, and ZZZZ is the line number. (Each of the
* capital letters represents a single decimal digit.)
*
* If any of the three parts of this phone number is too small
* to fill up its field, the field is padded with leading zeros.
* For example, if the value of the line number is 123, the last
* four characters of the string representation will be "0123".
*
* Note that there is a single space separating the closing
* parenthesis after the area code from the first digit of the
* prefix.
*/
@Override public String toString() {
return String.format("(%03d) %03d-%04d",
areaCode, prefix, lineNumber);
}
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}
// Works fine, but can be made faster
// public int compareTo(PhoneNumber pn) {
// // Compare area codes
// if (areaCode < pn.areaCode)
// return -1;
// if (areaCode > pn.areaCode)
// return 1;
//
// // Area codes are equal, compare prefixes
// if (prefix < pn.prefix)
// return -1;
// if (prefix > pn.prefix)
// return 1;
//
// // Area codes and prefixes are equal, compare line numbers
// if (lineNumber < pn.lineNumber)
// return -1;
// if (lineNumber > pn.lineNumber)
// return 1;
//
// return 0; // All fields are equal
// }
// 改进后的compareTo 运用这种方法要确定最小和最大可能域值之差小于或等于INTEGER.MAX_VALUE,否则不要使用这种方法
public int compareTo(PhoneNumber pn) {
// Compare area codes
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != 0)
return areaCodeDiff;
// Area codes are equal, compare prefixes
int prefixDiff = prefix - pn.prefix;
if (prefixDiff != 0)
return prefixDiff;
// Area codes and prefixes are equal, compare line numbers
return lineNumber - pn.lineNumber;
}
public static void main(String[] args) {
NavigableSet<PhoneNumber> s = new TreeSet<PhoneNumber>();
for (int i = 0; i < 10; i++)
s.add(randomPhoneNumber());
System.out.println(s);
}
private static final Random rnd = new Random();
private static PhoneNumber randomPhoneNumber() {
return new PhoneNumber((short) rnd.nextInt(1000),
(short) rnd.nextInt(1000),
(short) rnd.nextInt(10000));
}
}
13 使类和成员的可访问性最小化
- 设计良好的模块会隐藏所有的实现细节,把它的API与它的实现清晰的隔离开来。
- 信息隐藏可以有效地解除系统的各模块之间的耦合关系。
==尽可能地使每个类或者成员不被外界访问==
成员的可访问性:
* 私有的(private)–只有在声明该成员的顶层类内部才可以访问这个成员。
* 包级私有的(package-private)–声明该成员的包内部的任何类都可以访问这个成员。
* 受保护的(protect)–声明该成员的类的子类可以访问这个成员,并且,声明该成员的包内部任何类也可以访问这个成员
* 公有的(public)–在任何地方都可以访问该成员
注意:
* 如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这样做可以确保任何可使用超类的实例的地方也都可以使用子类的实例。
* 包含公有可变域的类并不是线程安全的。
14 在共有类中使用访问方法而非公有域
- 如果类可以在它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。如果公有类暴露了他的数据域,要想在将来改变其内部的表示法是不可能的,因为公有域的客户端代码已经遍布各处了。
class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
* 如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。
使可变性最小化
- 不可变类知识其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。
为了使类成为不可变,要遵循以下五条原则:
1. 不要提供任何会修改对象状态的方法
2. 保证类不会被扩展
3. 使所有的域都是final的
4. 使所有的域都是私有的。
5. 确保对于任何可变组件的互斥访问
public final class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
public static Complex valueOfPolar(double r, double theta) {
return new Complex(r * Math.cos(theta),
r * Math.sin(theta));
}
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE = new Complex(1, 0);
public static final Complex I = new Complex(0, 1);
// Accessors with no corresponding mutators
public double realPart() { return re; }
public double imaginaryPart() { return im; }
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex subtract(Complex c) {
return new Complex(re - c.re, im - c.im);
}
public Complex multiply(Complex c) {
return new Complex(re * c.re - im * c.im,
re * c.im + im * c.re);
}
public Complex divide(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp,
(im * c.re - re * c.im) / tmp);
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
// See page 43 to find out why we use compare instead of ==
return Double.compare(re, c.re) == 0 &&
Double.compare(im, c.im) == 0;
}
@Override public int hashCode() {
int result = 17 + hashDouble(re);
result = 31 * result + hashDouble(im);
return result;
}
private int hashDouble(double val) {
long longBits = Double.doubleToLongBits(re);
return (int) (longBits ^ (longBits >>> 32));
}
@Override public String toString() {
return "(" + re + " + " + im + "i)";
}
}
其中的算术运算都是返回新的Complex实例,而不是修改这个实例。这被称为函数的做法,因为这些方法返回了一个函数的结果,这些函数对操作数进行运算但并不修改它。
- 不可变对象本质上是线程安全的。
- 不可变对象为其他对象提供了大量的构件。
* 缺点是对于每个不同的值都需要一个单独的对象。创建这种对象的代价可能很高
复合优先于继承
- 对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承是非常安全的。然而,对于普通类进行扩越包边界的继承,则是非常危险的。
- 与方法调用不同,继承打破了封装性。子类依赖于其超类中特定功能的实现细节。
- *
// Broken - Inappropriate use of inheritance!
import java.util.*;
public class InstrumentedHashSet<E> extends HashSet<E> {
// The number of attempted element insertions
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s =
new InstrumentedHashSet<String>();
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
结果是6
在HashSet内部,addAll是基于add方法来实现的,这里addAll方法首先addCount增加3,然后又依次调用到InstrumentedHashSet的add方法。
HashSet的addAll方法是在它的add方法上实现的。这种自用性是实现细节,不是承诺,不能保证在Java平台的所有实现中都保持不变。
- 导致子类脆弱的一个相关的原因是,它们的超类再后续的发行版本中可以获得新的方法。假设一个程序的安全性依赖于这样的事实:所有被插入到某个集合中的元素都满足某个先决条件。 对集合进行子类化,覆盖所有添加元素的方法。 超类在没有增加插入元素的新方法,这种做法就可以正常工作。一旦超类增加了这样的新方法,就可能会有非法元素添加进去。
上面的问题都来源于覆盖动作。如果再扩展一个类的时候,仅仅是增加新的方法,也不是完全安全的。因为如果超类在后续的发行版本中获得一个和子类签名相同但返回类型不同的方法,那么这样的子类将无法通过编译,如果和子类方法完全相同,实际上就是覆盖了超类的方法。
++有一种方法可以避免前面提到的问题。不用扩展现有的类,++而是在新的类中增加一个私有域,这种设计部称为“复合”++++
这样即使现有的类添加了新的方法,也不会影响新的类。
// Reusable forwarding class - Page 84
import java.util.*;
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
public void clear() { s.clear(); }
public boolean contains(Object o) { return s.contains(o); }
public boolean isEmpty() { return s.isEmpty(); }
public int size() { return s.size(); }
public Iterator<E> iterator() { return s.iterator(); }
public boolean add(E e) { return s.add(e); }
public boolean remove(Object o) { return s.remove(o); }
public boolean containsAll(Collection<?> c)
{ return s.containsAll(c); }
public boolean addAll(Collection<? extends E> c)
{ return s.addAll(c); }
public boolean removeAll(Collection<?> c)
{ return s.removeAll(c); }
public boolean retainAll(Collection<?> c)
{ return s.retainAll(c); }
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override public boolean equals(Object o)
{ return s.equals(o); }
@Override public int hashCode() { return s.hashCode(); }
@Override public String toString() { return s.toString(); }
}
// Wrapper class - uses composition in place of inheritance - Page 84
import java.util.*;
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
InstrumentedSet<String> s =
new InstrumentedSet<String>(new HashSet<String>());
s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
System.out.println(s.getAddCount());
}
}
InStrumentedSet类实现了Set接口,并且拥有单个构造器,它的参数也是Set类型。本质上讲这个类把一个Set变成了另一个Set,同时增加计数的功能。这里的包装类可以被用包装任何Set实现。
* 只有当两者之间确实存在“is-a”关系的时候,类B才能扩展类A
17 要么为继承而设计,并提供文档说明,要么就禁止继承
- 该类的文档必须精确地描述覆盖每个方法所带来的影响。换句话说,该类必须有文档说明它可覆盖的方法的自用性。
对于为了继承而设计的类,唯一的测试方法是编写子类。如果遗漏了关键的受保护成员,尝试编写子类就会使遗漏所带来的通过变得更加明显。如果编写了多个子类,并且无一使用受保护成员,或许就应该把它做成私有的。
* 构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。如果违反了这条原则,很有可能导致程序失败
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
import java.util.*;
public final class Sub extends Super {
private final Date date; // Blank final, set by constructor
Sub() {
date = new Date();
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(date);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
你可能会期待这个程序会打印出日期两次,但是它第一次打印出的是null,因为overrideMe方法被Super构造器调用的时候,构造器Sub还没有初始化date域。
* 完全消除这个类中可覆盖方法的自用特性。这样做之后,就可以创建“能够安全地进行子类化”的类。
18 接口优于抽象类
接口和抽象类这两种机制之间最明显的区别在于:抽象类允许包含某些方法的实现,但是接口则不允许。
虽然接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为程序员提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的股价实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架是实现类接管了所有与接口实现相关的工作
// Concrete implementation built atop skeletal implementation - Page 95
import java.util.*;
public class IntArrays {
static List<Integer> intArrayAsList(final int[] a) {
if (a == null)
throw new NullPointerException();
return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i]; // Autoboxing (Item 5)
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // Auto-unboxing
return oldVal; // Autoboxing
}
public int size() {
return a.length;
}
};
}
public static void main(String[] args) {
int[] a = new int[10];
for (int i = 0; i < a.length; i++)
a[i] = i;
List<Integer> list = intArrayAsList(a);
Collections.shuffle(list);
System.out.println(list);
}
}
以上是一个静态工厂方法,它包含一个完整的、功能全面的List实现
骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义时”所特有的严格限制
接口一旦被公开发行,并且已经被广泛实现,再想改变这个接口几乎是不可能的。
19 接口只用于定义类型
有一种接口被称为常量接口
public interface PhysicalConstants {
// Avogadro's number (1/mol)
static final double AVOGADROS_NUMBER = 6.02214199e23;
// Boltzmann constant (J/K)
static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
// Mass of the electron (kg)
static final double ELECTRON_MASS = 9.10938188e-31;
}
常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口,会导致把这样的实现细节泄漏到该类的导出API中。
如果要导出常量,可以有几种合理的选择方案。
1. 如果这些常量与某个现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者是接口中。
2. 如果这些常量最好被看作枚举类型的成员,就应该用枚举类型来导出这些常量。否则,应该使用不可实例化的工具类来导出这些常量
// Constant utility class
public class PhysicalConstants {
private PhysicalConstants() { } // Prevents instantiation
// Avogadro's number (1/mol)
public static final double AVOGADROS_NUMBER = 6.02214199e23;
// Boltzmann constant (J/K)
public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
// Mass of the electron (kg)
public static final double ELECTRON_MASS = 9.10938188e-31;
}
简而言之,接口应该只被用来定义类型,它们不应该被用来到处常量
20 类层次优先于标签类
// Tagged class - vastly inferior to a class hierarchy!
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// Tag field - the shape of this figure
final Shape shape;
// These fields are used only if shape is RECTANGLE
double length;
double width;
// This field is used only if shape is CIRCLE
double radius;
// Constructor for circle
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// Constructor for rectangle
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}
这种标签类有着许多缺点。它们中充斥着样板代码。由于多个实现乱七八糟地挤在了单个类中,破坏了可读性。
实例承担者属于其他风格的不相关代码
标签类过于冗长,容易出错,并且效率低下。
用类层次改进
// Class hierarchy replacement for a tagged class
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
double area() { return Math.PI * (radius * radius); }
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
double area() { return length * width; }
}
这个类层次,每个类型的实现都配有自己的类,这些类没有收到相关的数据域的拖累。
类层次另一种好处在于:它们可以用来反应 类型之间本质上的层次关系,有助于增强灵活性。
21 用函数对象表示策略
class StringLengthComparator {
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}
指向StringLengthComparator对象的引用可以被当作是一个指向该比较器的“函数指针”,可以在任意一对字符串上被调用。换句话说,StringLengthComparator实例是用于字符串比较操作的具体策略
用StringLengthComparator并不好,因为客户端无法传递任何其他的比较策略。我们需要定义一个Comparator接口,并修改StringLengthComparator来实现这个接口,换句话说,我们在设计具体的策略类时,还需要定一个策略接口
23 请不要在新代码中使用原生态类型
- 声明中具有一个或者多个类型参数的类或者接口就是泛型
- 每个泛型都定义一个原生态类型,既不带任何实际类型参数的名称。例如:List相对应的原生态类型是List
- 如果不提供类型参数,使用集合类型和其他泛型也仍然是合法的,但是不应该这么做。如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。
* 泛型有子类型化的规则,List是原生态类型List的一个子类型,而不是参数化类型List的子类型。如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List这样的参数化类型,则不会。
24 消除非受检警告
- 要尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的。
- 无法消除警告,同时可以证明引起警告的代码是类型安全的,可以用一个@SuppressWarnings(“unchecked”)注解来禁止这条警告。
- 永远不要在整个类上使用SupressWarnings,这么做可能会掩盖了重要的警告。
* 每当使用SupressWarnings(“unchecked”)注解时,都要添加一条注释,说明为什么这么做时安全的
25 列表优先于数组
数组与泛型相比有两个不同点
* 数组时协变的。即如果Sub为Super的子类型,那么数组类型Sub[]就是Subper[]的子类型。
* 数组时具体化的。数组会在运行时才知道并检查他们的元素类型约束。
由于以上区别,数组和泛型不能很好地混合使用。例如:创建泛型、参数化类型或者类型参数的数组是非法的。new List[]、new List[]和new E[]都是非法的。
List<String>[] StringLists = new List<String>[1];
List<Integer> intList = Arrays.asList(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = StringLists[0].get(0);
假设第一行是合法的。第4行中我们将一个List实例保存到了原本声明中只包含List实例的数组中。第5行中,编译器会获取到元素并转换成String,但它是一个Integer,因此会报类型转化异常。为了防止出现这种情况,创建泛型数组的时候产生了一条编译时错误。
// List-based generic reduction - Page 123
import java.util.*;
public class Reduction {
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snapshot;
synchronized(list) {
snapshot = new ArrayList<E>(list);
}
// 以上如果用E[] snapshot = (E[]) List.toArray 代替会得到一条警告
E result = initVal;
for (E e : snapshot)
result = f.apply(result, e);
return result;
}
// A few sample functions
private static final Function<Integer> SUM = new Function<Integer>(){
public Integer apply(Integer i1, Integer i2) {
return i1 + i2;
}
};
private static final Function<Integer> PRODUCT = new Function<Integer>(){
public Integer apply(Integer i1, Integer i2) {
return i1 * i2;
}
};
private static final Function<Integer> MAX = new Function<Integer>(){
public Integer apply(Integer i1, Integer i2) {
return Math.max(i1, i2);
}
};
private static final Function<Integer> MIN = new Function<Integer>(){
public Integer apply(Integer i1, Integer i2) {
return Math.min(i1, i2);
}
};
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(
2, 7, 1, 8, 2, 8, 1, 8, 2, 8);
// Reduce intList using each of the above reducers
System.out.println(reduce(intList, SUM, 0));
System.out.println(reduce(intList, PRODUCT, 1));
System.out.println(reduce(intList, MAX, Integer.MIN_VALUE));
System.out.println(reduce(intList, MIN, Integer.MAX_VALUE));
}
}
数组和泛型不能很好地混合使用。如果发现将它们混合使用起来,并且得到了编译时错误和警告,第一反应是用列表代替数组
26 优先考虑泛型
// Generic stack using E[] - Pages 125-127
import java.util.Arrays;
public class Stack<E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size==0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// Little program to exercise our generic Stack
public static void main(String[] args) {
Stack<String> stack = new Stack<String>();
for (String arg : args)
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
/** E是不可具化的,而数组是可具化的,所以不能用E来使用诸如new
E[]这样的数组申请方法,为了解决这个问题,我们可以将new E[]改为new Object[]。至此,又得到了一条警告信息,因为我们要把Object型的数组赋给E型,因为编译器
无法确定E的具体类型,所以警告这是不是类型安全的。但是由于elements只有在push中使用,
且压进的是E型元素,所以我们可以判断出这是安全的,因此加上强制类型转换(E())new
Object[...],这是解决上面错误问题的每一种方法。
*/
还有一种解决方
E result = (E)elements[--size];
由于E是一个不可具体化的类型,编译器无法在运行时检验转换。你还是可以证实未受检的转换时安全的,因此可以禁止该警告。
27 优先使用泛型方法
public static Set union(Set s1, Set s2) {
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
// 这个方法可以编译,但是有两条警告
为了修改警告,使方法变成是类型安全的,要将方法声明修改为声明一个类型参数,表示这三个集合的参数类型。
import java.util.*;
public class Union {
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
// Simple program to exercise generic method
public static void main(String[] args) {
Set<String> guys = new HashSet<String>(
Arrays.asList("Tom", "Dick", "Harry"));
Set<String> stooges = new HashSet<String>(
Arrays.asList("Larry", "Moe", "Curly"));
Set<String> aflCio = union(guys, stooges);
System.out.println(aflCio);
}
}
目前union方法的局限性在于,三个集合的类型必须全部相同,利用有限制的通配符类型,可以使这个方法变得更加灵活
类型限制
利用有限制通配符来提升API的灵活性
public void pushAll(Iterable<E> src) {
for(E e : src) {
push(e);
}
}
// 如果有如下调用
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integer);
// 这样做会引起错误,因为Iterable<Integer>不是Integer<E>的子类
有限制通配符可以解决上述问题
pushAll的输入参数类型不应该为“E的Iterable接口”,而应该是“E的某个子类型的Iterable接口”
修改后:
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
同理现在提供一个popAll方法
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
// 如果有如下调用
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
// 这样同样会有一条错误Collection<Object>不是Collection<Number>的子类型。
// popAll的输入参数类型不应该为“E的集合”,而应该是“E的某种超类集合。
修改后
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
==为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型==
* PESC producer-extends,consumer-super
如果参数化类型表示一个T生产者,就使用
29 优先考虑类型安全的异构容器
// Typesafe heterogeneous container - Pages 142-145
import java.util.*;
public class Favorites {
// Typesafe heterogeneous container pattern - implementation
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));
}
// Typesafe heterogeneous container pattern - client
public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class);
System.out.printf("%s %x %s%n", favoriteString,
favoriteInteger, favoriteClass.getName());
}
}
Favourite实例是类型安全的,当你向它请求String的时候,它从来不会返回一个Integer给你。同时它也是异构的:不像普通的map,它的所有键都是不同类型的。因此,我们将Favourite称作类型安全的异构容器
集合API说明了泛型的一般用法,限制你每个容器只能有固定数目的类型参数。你可以通过将类型参数放在键上而不是容器上来避开这一限制。对于这种类型安全的异构容器,可以用Class对象作为键。
30 用enum代替int常量
枚举类型是由一组固定的常量组成合法值的类型。
public static final int APPLE_FUJI = 0; // int枚举模式
public static final String APPLE_FUJI = "fuji"; // String枚举模式
int枚举常量翻译成可打印的字符串,并没有很便利的方法,如果打印出来,所见到的就是一个数字,要遍历一个组中的所有int枚举常量,甚至获取枚举组的大小,都没有很可靠的方法。String枚举模式存在同样的问题
Java1.5以后提供了枚举类型。Java的枚举类型本质上是int值。因为没有可以访问的构造器,枚举类型是真正的final。
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(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 surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
Plant中所示的方法对于大多数枚举类型已经足够了。
有时需要将本质上不同的行为与每个常量关联起来。
public enum Operation {
PLUS, MINUS, TIMES, DIVEDE;
double apply(double x , double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVEDE: return x / y;
}
throw new AssertionError("UnKnown op: " + this)
}
}
如果增加了新的枚举常量,却忘记给switch添加相应的条件,枚举仍然可以编译但是当你试图运用新的运算时,就会运行失败。
改进:在枚举类型中声明一个抽象的apply方法,并在特定于常量的主体中,用具体的方法覆盖每个常量的抽象apply方法中。这种方法被称作特定与常量的方法实现
// Enum type with constant-specific class bodies and data - Page 153
import java.util.*;
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; }
@Override public String toString() { return symbol; }
abstract double apply(double x, double y);
// Implementing a fromString method on an enum type - Page 154
private static final Map<String, Operation> stringToEnum
= new HashMap<String, Operation>();
static { // Initialize map from constant name to enum constant
for (Operation op : values())
stringToEnum.put(op.toString(), op);
}
// Returns Operation for string, or null if string is invalid
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
// Test program to perform all operations on given operands
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难了。
// The strategy enum pattern
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(hoursWorked, payRate);
}
// The strategy enum type
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 payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
该代码将加班工资计算移到一个私有的嵌套枚举中,将这个策略枚举的实例传到PayrollDay枚举的构造器中。
31 用实例域代替序数
永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。
// Enum with integer data stored in an instance field
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
32 用EnumSet代替位域
- 如果一个枚举类型的元素主要用在集合中,一般就使用int枚举模式
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 0;
public static final int STYLE_UNDERLINE = 1 << 0;
public static final int STYLE_STRIKETHROUGH = 1 << 0;
public void applyStyles(int styles) {
// Body goes here
}
}
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
}
// Sample use
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
33 用EnumMap代替序数索引
- 当你访问一个按照枚举的序数进行索引的数组时,使用正确的int值就是你的职责了;int不能提供枚举的类型安全。如果你使用了错误的值,程序就会完成错误的工作。
可以用EnumMap进行改进,是一种非常快速的Map用于枚举键
// Using an EnumMap to associate data with an enum - Pages 161-162
import java.util.*;
// Simplistic class representing a culinary herb - Page 161
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;
}
@Override public String toString() {
return name;
}
public static void main(String[] args) {
Herb[] garden = {
new Herb("Basil", Type.ANNUAL),
new Herb("Carroway", Type.BIENNIAL),
new Herb("Dill", Type.ANNUAL),
new Herb("Lavendar", Type.PERENNIAL),
new Herb("Parsley", Type.BIENNIAL),
new Herb("Rosemary", Type.PERENNIAL)
};
// Using an EnumMap to associate data with an enum - Page 162
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);
}
}
有的数组按照序数索引两次。下面的程序是将两个阶段映射到一个阶段过渡中
public enum Phase {
SOLID,LIQUID,GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
private static fianl Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME},
{FREEZE, null, BOIL},
{DEPOSIT, CONDENSE, null},
}
}
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
如果过度表中出了错,或者修改枚举类型时忘记更新,程序就会失败。
用EnumMap改进
// Using a nested EnumMap to associate data with enum pairs - Pags 163-164
import java.util.*;
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;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
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, trans);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
// Simple demo program - prints a sloppy table
public static void main(String[] args) {
for (Phase src : Phase.values())
for (Phase dst : Phase.values())
if (src != dst)
System.out.printf("%s to %s : %s %n", src, dst,
Transition.from(src, dst));
}
}
34 用接口模拟可伸缩的枚举
改进30条重的Operation
public interface Operation {
double apply(double x, double y);
}
// Emulated extensible enum using an interface - Basic implementation - Page 165
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
// Emulated extension enum - Page 166-167
import java.util.*;
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
// Test class to exercise all operations in "extension enum" - Page 167
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
System.out.println(); // Print a blank line between tests
test2(Arrays.asList(ExtendedOperation.values()), x, y);
}
// test parameter is a bounded type token (Item 29)
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));
}
// test parameter is a bounded wildcard type (Item 28)
private static void test2(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
35 注解优先于命名模式
- 一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。如:JUnit测试框架要求用户用test作为测试方法名称的开头。
38 检查参数的有效性
绝大数方法和构造器对于对于传递给它们的参数值都会有某些限制。
* 每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写在文档中,并且在这个方法体的开头处,通过显示的检查来实施这些限制。
39 必要时进行保护性拷贝
// Broken "immutable" time period class - Page 184
import java.util.*;
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
public String toString() {
return start + " - " + end;
}
// Remainder omitted
}
这里需要注意Date类本身是可变的。对于构造器的每个可变参数进行保护性拷贝是必要的。并且使用备份对象作为Period实例的组建,而不使用原始的对象。
// Repaired constructor - makes defensive copies of parameters - Page 185
// Stops first attack
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);
}
- 保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查时针对拷贝之后的对象,而不是针对原始对象。
第二种攻击方式
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);
为了防止这种攻击,只需要修改两个访问方法,使它返回可变内部域的保护性拷贝即可:
// Stops second attack
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
40 谨慎的设计方法签名
- 谨慎地选择方法的名称。方法的名称应该始终遵循标准的命名习惯
- 不要过于追求提供便利的方法。每个方法都应该尽其所能。
- 避免过长的参数列表。目标是四个参数,或者更少。太多,不利于记忆。
慎用重载
import java.util.*;
import java.math.*;
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}
这个程序会打印三次”Unknown Collection”。因为参数编译时类型为Collection
class Wine {
String name() { return "wine"; }
}
class SparklingWine extends Wine {
@Override String name() { return "sparkling wine"; }
}
class Champagne extends SparklingWine {
@Override String name() { return "champagne"; }
}
public class Overriding {
public static void main(String[] args) {
Wine[] wines = {
new Wine(), new SparklingWine(), new Champagne()
};
for (Wine wine : wines)
System.out.println(wine.name());
}
}
上面这个程序会打印出:”wine, sparkling和champagne”
使用重载安全而又保守的策略:永远不压迫导出两个具有相同参数数目的重载方法。
import java.util.*;
public class SetList {
public static void main(String[] args) {
Set<Integer> set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
for (int i = -3; i < 3; i++) {
set.add(i);
list.add(i);
}
for (int i = 0; i < 3; i++) {
set.remove(i);
list.remove(i);
}
System.out.println(set + " " + list);
}
}
这个程序打印出[-3,-2,-1][-2,0,2]
原因:set.remove(i)调用选择重载方法remove(E),这里的E是集合的元素类型,将i从int自动装箱到Integer中。
因此程序会从集合中去除正值。而list.remove(i)调用选择的是remove(int i),它从列表的指定位置上去除元素。
为解决这个问题需要奖int转换成Integer
list.remove((Integer) i);
43 返回零长度的数组或者集合,而不是null
* 对于一个返回null而不是零长度数组或者集合的方法,几乎每次调用都需要进行判断null处理。这样做很容易出错,因为客户端程序员很可能忘记对null返回值的处理。
44 为所有导出的API元素编写文档注释
- 为了正确地编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释
- 方法的文档注释应该简洁地描述出和它的客户端之间的约定。
45 将局部变量的作用域最小化
- 要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。如果变量在使用之前进行声明,这只会造成混乱。
- 几乎每个局部变量的声明都应该包含一个初始化表达式。
46 for-each循环优先于传统的for循环
1.5之前
for(Iterator<Suit> i = suit.iterator(); i.hasNext();){
Suit suit = i.next();
for(Iterator<Rank> j = rank.iterator(); j.hasNext;) {
deck.add(new Card(suit,j.next()));
}
}
这里要在外部循环的作用域中添加一个变量来保存外部元素
如果使用的是嵌套的for-each循环,这个问题就会消失
public (Suit suit : suits) {
for (Rank rank : ranks) {
deck.add(new Card(suit, rank));
}
}
47 了解和使用类库
- 通过使用标准类库,可以充分利用这些编写标准类库专家的知识,以及在你之前的其他人的使用经验
- 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。把时间花在应用程序上,而不是底层细节上。
- 标准类库,他们的性能往往会随时间的推移而不断提高。
- 可以使代码更加易读,更易维护。
48 如果需要精确的答案,请避免使用float和doule
==float和double类型尤其不适合用于货币计算==
// Avoid float and double if exact answers are required!! - Page 48
import java.math.*;
public class Arithmetic {
public static void main(String[] args) {
System.out.println(1.03 - .42);
System.out.println();
System.out.println(1.00 - 9 * .10);
System.out.println();
howManyCandies1();
System.out.println();
howManyCandies2();
System.out.println();
howManyCandies3();
}
// Broken - uses floating point for monetary calculation!
public static void howManyCandies1() {
double funds = 1.00;
int itemsBought = 0;
for (double price = .10; funds >= price; price += .10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);
}
public static void howManyCandies2() {
final BigDecimal TEN_CENTS = new BigDecimal( ".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;
funds.compareTo(price) >= 0;
price = price.add(TEN_CENTS)) {
itemsBought++;
funds = funds.subtract(price);
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);
}
public static void howManyCandies3() {
int itemsBought = 0;
int funds = 100;
for (int price = 10; funds >= price; price += 10) {
itemsBought++;
funds -= price;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: "+ funds + " cents");
}
}
使用double类型得出的结果是不精确的。
解决这个问题的办法是使用BigDecimal、int、或者long进行货币计算。
使用BigDecimal的好处:他允许你完全控制舎入。
49 基本类型优先于装箱基本类型
- 基本类型通常比装箱基本类型更省时间和空间
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42) {
System.out.println("Unbelievable");
}
}
}
它在计算表达式(i == 42)的时候会抛出NullPointException。i是个Integer,而不是int,就像所有的对象引用域一样,它的初始值为null。
* 当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱。如果null对象引用被自动拆箱,就会得到一个NullPointException。
==总之,当可以选择的时候,基本类型要优先于装箱基本类型。基本类型更加简单快速,如果必须使用装箱基本类型,要特别小心。自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险==
50 如果其他类型更适合,则尽量避免使用字符串
- 字符串不适合代替其他的值类型。如果它是一个数值,就应该被转换为适当的数值类型。
- 字符创不适合代替枚举类型。枚举类型比字符类型更加适合用来表示枚举类型的常量。
- 字符串不适合代替聚集类型。如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的。
51 当心字符串连接的性能
- 不要使用字符串连接操作符来合并多个字符串,除非性能无关紧要。相反,应该使用StringBuilder的append方法。另一种方法是,使用字符数组,或者每次只处理一个字符串,而不是将他们组合起来。
52 通过接口引用对象
- 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。
- 养成了用接口作为类型的习惯,你的程序将会更加灵活。
53 接口优先于反射机制
反射机制允许一个类使用另一个类,即使当前被编译的时候后者还根本不存在。然而,这种能力也要付出代价
* 丧失了编译时类型检查的好处
* 执行反射访问所需要的代码非常笨拙和冗长。
* 性能损失。反射方法调用比普通方法调用慢了许多。
核心反射机制最初是为了基于组件的应用创建工具而设计的。
==普通应用程序在运行时不应该以反射方式访问对象==
54 谨慎地使用本地方法
从历史上看,本地方法主要有三种用途:
* 他们提供了”访问特定于平台的机制”的能力,比如访问注册表和文件锁。
* 他们还提供了访问遗留代码库的能力,从而可以访问遗留数据。
* 本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
缺点:
* 使用本地方法来提高性能的做法不值得提倡。在早期的发行版中这样做往往是很有必要的,但是JVM实现变得越来越快了。
* 本地语音不是安全的。使用本地方法的应用程序也不再能免受内存损坏错误的音箱。
* 因为本地语言是与平台相关的,使用本地方法的应用程序也不再是可自由移植的。
55 谨慎地进行优化
- 不要因为性能而牺牲合理的结构。要努力编写好的程序而不是快的程序。
- 努力避免那些限制性能的设计决策。
- 要考虑API设计决策的性能后果。是公有的类型成为可变的,这可能会导致大量不必要的保护性拷贝。
56 遵守普遍接受的命名习惯
- 包的名字应该是层次状的,用句号分隔每个部分
- 包名称的其余部分应该包括一个或者多个描述该包的组成部分。这些组成部分应该比较简短,通常不超过8个字符。
- 类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写。应该尽量避免用缩写。
- 方法和域的名称的第一个字母应该小写
- 常量域的名称应该包含一个或者多个大写的单词
57 只针对异常的情况才使用异常
- 异常机制的设计初衷是用于不正常的情形,所以很少会有JVM实现试图对他们进行优化。
- 把代码放到try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特定优化。
- 对数组进行遍历的标准模式并不会导致冗余的检查。有些现代的JVM实现会将他们优化掉
- 异常应该只用于异常的情况下;它们永远不应该使用于正常的控制流。
58 对可恢复的使用情况使用受检异常,对编程错误使用运行时异常
- 如果期望调用者能够适当地回复,对于这种情况就应该使用受检的异常。API的设计者让API用户面对受检的异常,以此强制用户从这个异常条件中恢复。用户可以忽视这样的强制要求,只需要捕获异常并忽略即可,但这往往不是个好方法。
- 用运行时异常来表明编程错误。大多数的运行时异常都表示前提违例。所谓前提违例是指API的客户没有准守API规范建立的约定。
- 你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接的或者间接的)
59 避免不必要地使用受检异常
- 被一个方法单独抛出的受检异常,会给程序员带来非常高的额外负担。
- “把受检的异常变成未受检的异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。
60 优先使用标准的异常
- 重用现有的异常有很多方面的好处。它使你的API更加易于学习和使用。因为它与程序员已经熟悉的习惯用法是一致的。
- 对于用到这些API的程序而言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常。
- 异常类越少,意味着内存印迹就越小,装载这些类的时间开销也越少。
61 抛出与抽象对应的异常
- 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译。
- 一种特殊的异常转义形式称为异常链,如果底层的异常对于调试导致高层的异常的问题很有帮助,使用异常链就很合适。
try {
... // Use lower-level abstraction to do our bidding
} catch (LowserLevelException cause) {
throw new HigherLevelException(cause);
}
高层异常的构造器将原因传到支持链的超级构造器,因此他最终将被传给Throwable的其中一个运行异常链的构造器。
- 处理来自低层异常的最好做法是,在调用低层方法之前确保他们会成功执行,从而避免他们抛出异常。
- 如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,除非低层方法碰巧可以保证它抛出的所有异常对高层也合适才可以将异常从低层传播到高层。
62 每个方法抛出的异常都要有文档
==描述一个方法所抛出的异常,是正确使用这个方法时所需文档的重要组成部分==
- 始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确滴记录下抛出每个异常的条件。
- 对于接口中的方法,在文档中记录下他可能抛出的未受检异常显得尤为重要。这份文档构成了该接口的通用规定的一部分,它指定了该接口的多个实现必须遵循的公共行为。
63 在细节消息中包含能捕获失败的信息
当程序由于被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹。
* 为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。
64 努力使失败保持原子性
一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性。
* 获得失败原子性最常见的方法是在执行操作之前检查参数的有效性。
* 调整计算处理工程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
* 编写一段恢复代码,由它来拦截操作工程中发生的失败,以及使对象回滚到做操开始之前的状态上。这种办法主要用于永久性的数据结构。(不常用)
* 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。如果数据保存在临时的数据结构中,计算过程会更加迅速,使用这种办法就是很自然的事。
65 不要忽略异常
当API的设计者声明一个方法将抛出某个异常的时候,他们等于正在试图说明某些事情。所以,请不要忽略它。
* 空的catch块会使异常达不到应该有的目的,至少,catch块应该也包含一条说明,解释为什么可以忽略这个异常。
66 同步访问共享的可变数据
- 关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。
- 为了线程之间进行可靠地通信,也为了互斥访问,同步是必要的。
import java.util.concurrent.*;
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
这个程序永远不会终止:因为后台线程永远在循环。由于没有同步,就不能保证后台线程何时看到主线程对stopRequest的值所做的改变。
import java.util.concurrent.*;
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested())
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
// 如果读和写操作没有都被同步,同步就不会起作用
更简洁的方式,使用volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值。
import java.util.concurrent.*;
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
67 避免过度同步
过度同步可能会导致性能降低、死锁、甚至不确定的行为。
* 为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对于客户端的控制。(在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法)
68 executor和task优先于线程
java1.5 增加了java.util.concurrent这个包中包含了一个Executor Framework,这是一个很灵活的基于接口的任务执行工具
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable); // 为执行提交一个runnable方法
executor.shutdown();
为特殊的应用程序选择executor service是很有技巧的。如果要编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool通常是个不错的选择,因为它不需要配置,并且一般情况下能够正常使用。对于大负载的服务器来说,缓存的线程池就不是很好的选择了。最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池
69 并发工具优先于wait和notify
- java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合(Concurrent Collection) 以及同步器(Synchronizer)
- 并发集合为标准的集合接口提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步。
import java.util.concurrent.*;
public class Intern {
private static final ConcurrentMap<String, String> map =
new ConcurrentHashMap<String, String>();
// Concurrent canonicalizing map atop ConcurrentMap - not optimal - Page 273
// public static String intern(String s) {
// String previousValue = map.putIfAbsent(s, s);
// return previousValue == null ? s : previousValue;
// }
// Concurrent canonicalizing map atop ConcurrentMap - faster! - Page 274
public static String intern(String s) {
String result = map.get(s);
if (result == null) {
result = map.putIfAbsent(s, s);
if (result == null)
result = s;
}
return result;
}
}
- 同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作。
70 线程安全性的文档化
- 一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
线程安全的几种级别: - 不可变的-这个类的实例是不变得。所以,不需要外部的同步。(String、Long、BigInteger)
- 无条件的线程安全-这个类的实例是可变的,但是这个类有着足够的内部同步。(Random、ConcurrentHashMap)
- 有条件的线程安全-除了有些方法为进行安全的并发使用需要外部同步之外,这种线程安全级别与无条件的线程安全相同。(Collection.Synchronized包装返回的集合,它们的iterator要求外部同步)
- 非线程安全-这个类的实例是可变的。为了并发的使用它们,客户必须利用自己选择的外部同步包围每个方法调用。(ArrayList、HashMap)
- 线程对立的-这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。
在文档中描述一个有条件的线程安全类要特别小心。必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把锁。
慎用延迟初始化
延迟初始化时延迟到需要域的值时才将它初始化的这种行为。
延迟初始化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。
如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。
// Initialization styles - Pages 282-284
public class Initialization {
// Normal initialization of an instance field - Page 282
private final FieldType field1 = computeFieldValue();
// Lazy initialization of instance field - synchronized accessor 如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法。
private FieldType field2;
synchronized FieldType getField2() {
if (field2 == null)
field2 = computeFieldValue();
return field2;
}
// Lazy initialization holder class idiom for static fields 出于性能的考虑需要对静态域使用延迟初始化,就使用Lazy initialization holder class
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField3() { return FieldHolder.field; }
// Double-check idiom for lazy initialization of instance fields - 出于性能的考虑需要对实例域使用延迟初始化,就使用双重检查模式
private volatile FieldType field4;
FieldType getField4() {
FieldType result = field4;
if (result == null) { // First check (no locking)
synchronized(this) {
result = field4;
if (result == null) // Second check (with locking)
field4 = result = computeFieldValue();
}
}
return result;
}
// Single-check idiom - can cause repeated initialization! - Page 284
private volatile FieldType field5;
private FieldType getField5() {
FieldType result = field5;
if (result == null)
field5 = result = computeFieldValue();
return result;
}
private static FieldType computeFieldValue() {
return new FieldType();
}
}
class FieldType { }
72 不要依赖于线程调度器
- 任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
- 要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。
- 线程不应该移植处于忙-等的状态,即反复地检查一个共享对象,以等待某些事情的发生。忙-等这种做法会极大地增加处理器的负担,降低了同一个机器上其他进程可以完成的有用工作量。
73 避免使用线程组
已经过时
74 谨慎的实现Serializable接口
- 实现Serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域都将变成导出的API的一部分。
- 它增加了出现Bug和安全漏洞的可能性。反序列化过程必须也要所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问真正在构造过程中的对象的内部信息。依靠默认的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭到非法访问。
- 随着类发行新的版本,相关的测试负担也增加了。
// Nonserializable stateful class allowing serializable subclass - Pages 292-293
import java.util.concurrent.atomic.*;
public abstract class AbstractFoo {
private int x, y; // Our state
// This enum and field are used to track initialization
private enum State { NEW, INITIALIZING, INITIALIZED };
private final AtomicReference<State> init =
new AtomicReference<State>(State.NEW);
public AbstractFoo(int x, int y) { initialize(x, y); }
// This constructor and the following method allow
// subclass's readObject method to initialize our state.
protected AbstractFoo() { }
protected final void initialize(int x, int y) {
if (!init.compareAndSet(State.NEW, State.INITIALIZING))
throw new IllegalStateException(
"Already initialized");
this.x = x;
this.y = y;
// Do anything else the original constructor did
init.set(State.INITIALIZED);
}
// These methods provide access to internal state so it can
// be manually serialized by subclass's writeObject method.
protected final int getX() { checkInit(); return x; }
protected final int getY() { checkInit(); return y; }
// Must call from all public and protected instance methods
private void checkInit() {
if (init.get() != State.INITIALIZED)
throw new IllegalStateException("Uninitialized");
}
// Remainder omitted
}
// Serializable subclass of nonserializable stateful class - Pages 293-294
import java.io.*;
public class Foo extends AbstractFoo implements Serializable {
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Manually deserialize and initialize superclass state
int x = s.readInt();
int y = s.readInt();
initialize(x, y);
}
private void writeObject(ObjectOutputStream s)
throws IOException {
s.defaultWriteObject();
// Manually serialize superclass state
s.writeInt(getX());
s.writeInt(getY());
}
// Constructor does not use the fancy mechanism
public Foo(int x, int y) { super(x, y); }
private static final long serialVersionUID = 1856835860954L;
}
内部类不应该实现Serializable。静态成员类却可以实现Serializable接口。