5天带你读完《Effective Java》(一)

《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)就是典型的静态工厂方法。

在某些场合更适合使用静态工厂方法,因为它相对构造函数有以下优点:

  1. 有确定的名字。
    如: BigInteger.probablePrime 要比BigInteger(int, int, Random)意义更加明确,从前者命名可以知道它返回的是一个可能的素数。
  2. 不需要每次调用都创建新的对象。
    如:Boolean.valueOf(boolean) 实际返回是静态缓存对象。
// Boolean.java
public static Boolean valueOf(boolean b) {
	return b ? Boolean.TRUE : Boolean.FALSE;
}
  1. 可以返回任何子类的对象。
    如: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));
}
  1. 返回对象的类型可以根据参数的不同而变化。
    这一条与第三条有点类似。如: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());
  1. 无需提前考虑返回对象的类是否存在。
    典型的如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

实现单例有三种常见方法:

  1. 通过公共静态字段
// Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}
  1. 通过静态工厂方法
// 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() { ... }
}
  1. 通过单元素枚举
// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

总结一下:

  1. 公共静态字段的优点是:实现简单。
  2. 静态工厂方法的优点是:提供了一定的灵活性,如可以为每个线程分配一个实例。
  3. 单元素枚举的优点是:类似公共静态字段,实现更简单。缺点是无法继承别的类。

第4条:用私有构造函数保证不可实例化

Item 4: Enforce noninstantiability with a private constructor

有些Java类我们不想实例化,只想调用它的静态方法,一般是对于一些工具类,如java.lang.Mathjava.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这样的容器引用时。如果一个对象是在方法内部创建,那么无需担心,因为随着方法的结束对象的生命周期就会走到终点。

另一个常见的内存泄漏来源是缓存。当缓存中的数据忘记删除后就会一直占用内存。有两种解决办法:

  1. 使用WeakHashMap。WeakHashMap当key没有再被外部引用时,条目会自动从WeakHashMap中删除。
  2. 采用缓存清除策略。典型的如LinkedHashMap实现的LRU。

WeakHashMap原理简析:

  1. 添加entry时,设置entry为弱引用,指向这个entry的key,并将这个弱引用注册到referenceQueue上
  2. 当key没有被外部引用时,GC触发时回收key,并导致entry(弱引用)被添加到referenceQueue中
  3. 当调用任意访问WeakHashMap的方法时,会触发expungeStaleEntries方法,将entry从WeakHashMap中移除

内存泄漏的第三种常见来源是监听器和其他回调。如果你注册了回调但不显示取消它们,那么它们会逐渐累积。

第8条:避免使用终结器和清除器

Item 8: Avoid finalizers and cleaners

Finalizer和Cleaner通常是不必要、危险而且不可预测的。Finalizer在Java 9中已经被弃用,在后面的版本中用Cleaner替代Finalizer。前者比后者的危险小。

Finalizer和Cleaner不能被保证立即执行,它们的执行时间取决于具体的GC实现。而且使用它们会严重影响性能。

Finalizer和Cleaner的合适使用场合有两种:

  1. 充当安全保底策略。
    我们推荐使用try-with-resources释放资源。但如果用户忘记释放资源时,那么最好是延迟释放资源。一些Java类库,如FileInputStreamFileOutputStream、ThreadPoolExecutorjava.sql.Connection,都将Finalizer作为保底策略。
  2. 释放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 方法时,必须遵守下列通用约定:

  1. 反身性:对于任何非空的参考值 x,x.equals(x) 必须返回 true。
  2. 对称性:对于任何非空参考值 x 和 y,x.equals(y) 必须在且仅当 y.equals(x) 返回 true 时返回 true。
  3. 传递性:对于任何非空的引用值 x, y, z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,那么 x.equals(z) 必须返回 true。
  4. 一致性:对于任何非空的引用值 x 和 y, x.equals(y) 的多次调用必须一致地返回 true 或一致地返回 false,前提是不修改 equals 中使用的信息。
  5. 对于任何非空引用值 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。现代虚拟机自动进行这种优化。

如果你需要不太可能产生冲突的哈希算法,请参考 Guavacom.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,满足以下条件

  1. x.clone() != x
  2. x.clone().getClass() == x.getClass()
  3. 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.compareFloat.compare

如果一个类有多个重要字段,那么需要确定它们的比较顺序。各个字段依次比较,如果结果是0,那么返回,否则比较下一个。例如实现PhoneNumbercompareTo方法:

// 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);
    }
};
``
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值