《Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说:“如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。
本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。
本文是系列的第一部分,包含对第二、三章的14个准则的解读,约1.9万字。
第二章 创建和销毁对象
Chapter 2. Creating and Destroying Objects
第1条:考虑以静态工厂方法代替构造函数
Item 1: Consider static factory methods instead of constructors
构造函数和静态工厂方法都是获取对象实例的常用方式,如Boolean.valueOf(boolean)
就是典型的静态工厂方法。
在某些场合更适合使用静态工厂方法,因为它相对构造函数有以下优点:
- 有确定的名字。
如:BigInteger.probablePrime
要比BigInteger(int, int, Random)
意义更加明确,从前者命名可以知道它返回的是一个可能的素数。 - 不需要每次调用都创建新的对象。
如:Boolean.valueOf(boolean)
实际返回是静态缓存对象。
// Boolean.java
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
- 可以返回任何子类的对象。
如:Java的Collections
类通过静态工厂方法共计返回45种不同的实现类,这些实现类对用户不可见,可见的是List 、Map
这样的基类。如Collections.unmodifiableList
会根据参数list是否支持随机访问,返回不同类型的不可变List
。
// Collections.java
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
- 返回对象的类型可以根据参数的不同而变化。
这一条与第三条有点类似。如:EnumSet
的静态工厂方法根据参数enum
的大小,返回不同的子类实例。如果有64个或更少的元素,返回一个基于long
类型的RegularEnumSet
实例;否则返回一个基于long[]
类型的JumboEnumSet
实例。
// Thread.State是少于64个元素的enum
EnumSet<Thread.State> es1 = EnumSet.allOf(Thread.State.class);
// Character.UnicodeScript是多于64个元素的enum
EnumSet<Character.UnicodeScript> es2 = EnumSet.allOf(Character.UnicodeScript.class);
// 打印出es1的类型为RegularEnumSet
System.out.println(es1.getClass());
// 打印出es2的类型为JumboEnumSet
System.out.println(es2.getClass());
- 无需提前考虑返回对象的类是否存在。
典型的如JDBC
,通过DriverManager.getConnection
来获取连接。由不同的数据库驱动来提供连接的具体实现。
以下是常见的静态工厂方法归类:
// from
Date d = Date.from(instant);
// of
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
// valueOf
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
// instance or getInstance
StackWalker luke = StackWalker.getInstance(options);
// create or newInstance
Object newArray = Array.newInstance(classObject, arrayLen);
// getType
FileStore fs = Files.getFileStore(path);
// newType
BufferedReader br = Files.newBufferedReader(path);
// type
List<Complaint> litany = Collections.list(legacyLitany);
第2条:面对许多构造函数参数时,考虑构建器
Item 2: Consider a builder when faced with many constructor parameters
当构造函数有很多可选参数时,我们往往需要提供很多种参数组合,这样的代码编写麻烦,可读性也不好。如:
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
另一种选择是 JavaBean 模式,通过调用一个无参数的构造函数来创建对象,然后调用 setter 方法来设置每个参数:
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
public static void main(String[] args) {
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
}
}
JavaBean模式的缺点是,在构造函数调用后,setter方法调用前,可能会出现线程不安全的情况,需要程序员额外处理。
第三种选择是使用构建器,它既保证线程安全,又能提供较好的可读性。
// Builder Pattern
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 {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
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) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = 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).sodium(35).carbohydrate(27).build();
}
}
第3条:使用私有构造函数或枚举类型实现单例属性
Item 3: Enforce the singleton property with a private constructor or an enum type
实现单例有三种常见方法:
- 通过公共静态字段
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
- 通过静态工厂方法
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
- 通过单元素枚举
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
总结一下:
- 公共静态字段的优点是:实现简单。
- 静态工厂方法的优点是:提供了一定的灵活性,如可以为每个线程分配一个实例。
- 单元素枚举的优点是:类似公共静态字段,实现更简单。缺点是无法继承别的类。
第4条:用私有构造函数保证不可实例化
Item 4: Enforce noninstantiability with a private constructor
有些Java类我们不想实例化,只想调用它的静态方法,一般是对于一些工具类,如java.lang.Math
或 java.util.Arrays
。
为了保证非实例化,我们可以将构造函数设为私有,这样使用者就没法实例化这个类了。
第5条:依赖注入优于硬连接资源
Item 5: Prefer dependency injection to hardwiring resources
许多类依赖于一个或多个底层资源,如拼写检查程序依赖字典。一种实现方法是使用单例模式:
// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
这种方法缺点是不够灵活,每种检查程序需要有自己的字典,不可能只有一种字典。
更好的方法是使用依赖注入,将字典作为参数传递给检查程序的构造函数:
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
第6条:避免创建不必要的对象
Item 6: Avoid creating unnecessary objects
不要创建不必要的对象。如果对象不可变,那么它可以被复用。
下面语句会创建两个String对象:
String s = new String("bikini"); // DON'T DO THIS!
换一种写法就只会创建一个:
String s = "bikini";
你可以使用静态工厂方法来避免创建不必要的对象,如:Boolean.valueOf(String)
。
有些对象的创建比较昂贵,如正则匹配的模式:
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
这时最好将其缓存以备复用,以提升性能:
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
在视图场景也会用到缓存对象。如:Map
接口的 keySet
方法返回的Map
键的Set
视图。每次调用时都返回第一次调用时生成并缓存下来的KeySet
。
另一种创建不必要对象的方法是自动装箱。如下面例子由于将long误写为Long,导致创建很多Long对象,性能大大下降。
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
在一般情况下,用户不应该自己创建对象池,因为GC回收小对象的代价是很低的,而维护自己的对象池会使代码混乱,内存占用增加。除非这些对象非常重量级,如数据库连接、Netty中使用的堆外缓存。
第7条:清除过时的对象引用
Item 7: Eliminate obsolete object references
Java中也会存在内存泄漏,我们需要将不需要的对象引用及时清除,以避免这种泄漏。
考虑以下简单的堆栈实现:
import java.util.Arrays;
import java.util.EmptyStackException;
// Can you spot the "memory leak"?
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);
}
}
当堆栈增加再收缩时,从中弹出的对象不会被GC,因为堆栈中的elements数组保留了对它们的引用。
修复方法是将对象引用及时清除:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
所以内存泄漏的风险在于:一个对象被另外的对象引用时,通过根搜索就能找到。这种情况较多发生在对象被数组或Map、List这样的容器引用时。如果一个对象是在方法内部创建,那么无需担心,因为随着方法的结束对象的生命周期就会走到终点。
另一个常见的内存泄漏来源是缓存。当缓存中的数据忘记删除后就会一直占用内存。有两种解决办法:
- 使用WeakHashMap。WeakHashMap当key没有再被外部引用时,条目会自动从WeakHashMap中删除。
- 采用缓存清除策略。典型的如LinkedHashMap实现的LRU。
WeakHashMap原理简析:
- 添加entry时,设置entry为弱引用,指向这个entry的key,并将这个弱引用注册到referenceQueue上
- 当key没有被外部引用时,GC触发时回收key,并导致entry(弱引用)被添加到referenceQueue中
- 当调用任意访问WeakHashMap的方法时,会触发expungeStaleEntries方法,将entry从WeakHashMap中移除
内存泄漏的第三种常见来源是监听器和其他回调。如果你注册了回调但不显示取消它们,那么它们会逐渐累积。
第8条:避免使用终结器和清除器
Item 8: Avoid finalizers and cleaners
Finalizer和Cleaner通常是不必要、危险而且不可预测的。Finalizer在Java 9中已经被弃用,在后面的版本中用Cleaner替代Finalizer。前者比后者的危险小。
Finalizer和Cleaner不能被保证立即执行,它们的执行时间取决于具体的GC实现。而且使用它们会严重影响性能。
Finalizer和Cleaner的合适使用场合有两种:
- 充当安全保底策略。
我们推荐使用try-with-resources释放资源。但如果用户忘记释放资源时,那么最好是延迟释放资源。一些Java类库,如FileInputStream
、FileOutputStream、ThreadPoolExecutor
和java.sql.Connection
,都将Finalizer作为保底策略。 - 释放native资源。
如Netty中使用的堆外内存,这些对JVM来说是不可见的,无法直接处理。如果性能可接受,那么可以托管给Cleaner来释放。否则应定义自己类的close方法,保证立即释放。
下面是一个例子,Room类代表房间,在被回收前必须先清理。清理逻辑定义在State类的run方法中。无论用户主动close还是托管给Cleaner清理,这个方法都会调用到。
import sun.misc.Cleaner;
// An autocloseable class using a cleaner as a safety net
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// Resource that requires cleaning. Must not refer to Room!
private static class State implements Runnable {
int numJunkPiles; // Number of junk piles in this room
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// Invoked by close method or cleaner
@Override
public void run() {
System.out.println("Cleaning room");
numJunkPiles = 0;
}
}
// The state of this room, shared with our cleanable
private final State state;
// Our cleanable. Cleans the room when it’s eligible for gc
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override
public void close() {
cleanable.clean();
}
}
如果是成年人,那么能做到自己清理(try-with-resources
),会调用清理逻辑,打印出"Cleaning room"。
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("Goodbye");
}
}
}
如果是小孩,那么可能忘记清理,这时Cleaner可以排上用场。但由于GC时间不固定,可能程序退出了都还没有执行。
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("Peace out");
}
}
第9条:使用 try-with-resources 优于 try-finally
Item 9: Prefer try with resources to try finally
用try-finally
关闭资源,会使得代码可读性差,而且容易出错,特别是在关闭多个资源的时候。
// try-finally is ugly when used with more than one resource!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
}
finally {
in.close();
}
}
如果用try-with-resources
就简单多了,且不易出错,应该永远推荐使用:
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
第三章 对象的通用方法
Chapter 3. Methods Common to All Objects
第10条:覆盖 equals 方法时应遵守通用约定
Item 10: Obey the general contract when overriding equals
Object
类提供的equals
方法是比较两个引用是否指向同一个对象。一般情况下,自己的实现类无需覆盖这个方法。除非需要做逻辑相等的测试。所谓逻辑相等就是代表对象的值相等,如Integer、String
类的处理。
当覆盖 equals 方法时,必须遵守下列通用约定:
- 反身性:对于任何非空的参考值 x,
x.equals(x)
必须返回 true。 - 对称性:对于任何非空参考值 x 和 y,
x.equals(y)
必须在且仅当y.equals(x)
返回 true 时返回 true。 - 传递性:对于任何非空的引用值 x, y, z,如果
x.equals(y)
返回 true,y.equals(z)
返回 true,那么x.equals(z)
必须返回 true。 - 一致性:对于任何非空的引用值 x 和 y,
x.equals(y)
的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。 - 对于任何非空引用值 x,
x.equals(null)
必须返回 false。
下面是一个很好的实现equals
的范例,自己实现时可以模仿它的逻辑步骤:
// Class with a typical equals method
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
} ... // Remainder omitted
}
谷歌的开源 AutoValue 框架可以帮我们自动生成通用方法,如equals、hashCode、toString方法,它生成效果要好于用IDE。
第11条:当覆盖 equals 方法时,总要覆盖 hashCode 方法
Item 11: Always override hashCode when you override equals
当覆盖equals方法时,必须要覆盖hashCode方法。因为相等的对象必须要有相同的哈希码。
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
在上面的例子执行后,你可能期望 m.get(new PhoneNumber(707, 867,5309))
返回"Jenny",但是它返回 null。原因是用于检索的新对象虽然与旧对象相等,但是它们的哈希码不同,因此哈希码对应的哈希桶也不同,所以再也找不到了。
一个好的哈希算法倾向于为不相等的实例生成不相等的哈希码,并分布均匀。有一个简单的实现方式如下:
// fields是类中重要的字段
int hashCode(Object[] fields) {
int result = 0;
for (Object o : fields) {
int c = o.hashCode();
result = result * 31 + c;
}
return result;
}
在上例中,选择 31 是因为它是奇素数。如果是偶数,乘法运算就会溢出,信息就会丢失,因为乘法运算等同于移位。31 有一个很好的特性,可以用移位和减法来代替乘法以提升性能:31 * i == (i <<5) – i
。现代虚拟机自动进行这种优化。
如果你需要不太可能产生冲突的哈希算法,请参考 Guava
的 com.google.common.hash.Hashing
。
Objects 类有一个静态hashCode方法,实现的哈希码效果不错,但缺点是性能不佳,因为它们需要创建数组来传递可变数量的参数,如果任何参数是原始类型的,那么需要进行装箱和拆箱。推荐只在性能不重要的情况下使用这种哈希算法。下面是使用这种技术编写的 PhoneNumber 的哈希算法:
// One-line hashCode method - mediocre performance
@Override
public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
不要为了提升性能,从哈希码中排除重要字段。造成结果可能是生成大量重复的哈希码,导致性能变差。一个典型的错误是,Java 2之前,String的哈希算法最多只使用16个字符。对于许多前缀相同的URL,会造成大量的哈希碰撞。
第12条:始终覆盖 toString 方法
Item 12: Always override toString
原始Object的toString方法,返回的字符串通常不是用户希望看到的,如PhoneNumber@153b91之类。
所以,建议覆盖toString方法,以展示类中所有重要的信息。
第13条:明智地覆盖 clone 方法
Item 13: Override clone judiciously
除了通过构造函数创建对象,Java还支持通过克隆的方式复制一个对象。
Object类中实现了clone方法,这个方法是native的,所做操作是浅拷贝,即把原对象完整拷贝过来并包括其中的引用。
protected native Object clone() throws CloneNotSupportedException;
但是这个方法不能直接调用,直接调用会抛出CloneNotSupportedException
。要在自己的类中调用clone方法,需要为这个类实现Cloneable接口。
关于clone 方法的一般约定很薄弱,缺乏约束力。约定是:对于任何对象 x,满足以下条件
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
但是以上条件仅是作为推荐,并非绝对的。
按照上面第二条,clone方法应该通过super.clone()来实现。如果一个类和它的所有超类(Object类除外)都满足这个约定,那么x.clone().getClass() == x.getClass()
就可以成立。
上面讲到Object类的clone方法是浅拷贝。由于浅拷贝会拷贝对象内部对其他对象的引用而非实际对象,所以对这些字段的修改会影响到原始对象。例如下面的Stack类:
public class Stack {
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;
}
// Ensure space for at least one more element.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
对新对象的elements数组修改会影响到原对象。解决办法是采用深拷贝:对elements数组递归调用clone方法:
// Clone method for class with references to mutable state
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
有时仅仅递归调用clone方法还不够。例如下面的HashTable类:
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
} ... // Remainder omitted
}
如果只是递归克隆bucket数组,那么bucket数组内部存储的Entry对象仍然和原来相同,修改它们会影响原来的HashTable对象。
// Broken clone method - results in shared mutable state!
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
解决办法是复制buckets数组中每个链表,递归复制链表中的每个Entry:
// Recursive clone method for class with complex mutable state
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// Recursively copy the linked list headed by this Entry
Entry deepCopy() {
return new Entry(key, value,next == null ? null : next.deepCopy());
}
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
} ... // Remainder omitted
}
为了避免链表太长导致递归栈溢出,可以改为遍历的方式:
// Iteratively copy the linked list headed by this Entry
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
除了clone以外,复制功能还可以由构造函数或静态工厂方法提供。这两种方式没有clone的诸多限制,是比较推荐的做法。
第14条:考虑实现 Comparable 接口
Item 14: Consider implementing Comparable
如果你需要让类支持排序,那么就要实现Comparable
接口。
public interface Comparable<T> {
int compareTo(T t);
}
排序方法compareTo
类似于equals
,也需要保证反身性、对称性、传递性和一致性。
所有的包装类都提供了静态比较方法,对于这些类的比较都应该通过该方法,如:Double.compare
和 Float.compare
。
如果一个类有多个重要字段,那么需要确定它们的比较顺序。各个字段依次比较,如果结果是0,那么返回,否则比较下一个。例如实现PhoneNumber
的compareTo
方法:
// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
int result = Short.compare(areaCode, pn.areaCode);
if (result == 0) {
result = Short.compare(prefix, pn.prefix);
if (result == 0)
result = Short.compare(lineNum, pn.lineNum);
}
return result;
}
排序的另一种实现方式是实现Comparator
接口,性能大概比Comparable
慢10%,例如:
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
有时人们会用计算差值的方法来实现compare
方法,例如:
// BROKEN difference-based comparator - violates transitivity!
class DateWrap{
Date date;
}
static Comparator<DateWrap> order = new Comparator<>() {
public int compare(DateWrap d1, DateWrap d2) {
return d1.date.getTime() - d2.date.getTme();
}
};
但是这种做法是有缺陷的。在上例中,Date.getTime
返回long
型时间戳,两者的差值有可能溢出int
。所以应该用Date
类自带的比较方法:
// Comparator based on static compare method
static Comparator<DateWrap> order = new Comparator<>() {
public int compare(DateWrap d1, DateWrap d2) {
return d1.date.compareTo(d2.date);
}
};
``