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

Effective Java第三版1 《Effective Java》第三版中文版目录 第一章 介绍 1 第二章 创建和销毁对象 4 1  考虑用静态工厂方法替换构造器 4 2  当遇到多个构造器参 立即下载
AI摘要

本书提炼了Java编程中的90个重要原则,涵盖对象创建、销毁及通用方法等方面,帮助开发者提高代码质量和效率。

《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.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这样的容器引用时。如果一个对象是在方法内部创建,那么无需担心,因为随着方法的结束对象的生命周期就会走到终点。

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

  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类库,如FileInputStream、FileOutputStream、ThreadPoolExecutor 和 java.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。现代虚拟机自动进行这种优化。

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

  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.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);
    }
};
``
热更新方案 HybridCLR 学习教程 |(一)原理及准备工作 前面文章介绍了热更新相关的知识,包括使用xLua实现热更新实战等 那本文就来介绍一下 热更新方案 HybridCLR(代号wolong,原huatuo)和基本使用方法。 若是想了解更多关于HybridCLR的东西也可以去 官网 详细查阅。 阅读详情

相关推荐

PyTorch多机训练Loss不一致问题排查指南:基于算子级一致性验证

比较二次训练过程中所有算子的误差,定位存在一致性问题的pytorch算子

Hi20240217的博客 1158

史上最全EffectiveJava总结(一)_effective java,零基础入门到精通,收藏这篇就够了

接口只应该用来定义类型,不要用来导出常量。常量接口时对接口的不良使用。实现常量接口,会导致把这样的实现细节泄漏给该类的导出 API 中,当类不再需要这些常量时,还必须实现这个接口以确保兼容性。如果非final类实现了该常量接口,它的所有子类的命名空间都将被接口中的常量污染。想要导出常量,可以把它们放在相关的类中,如Integer类中的MAX_VALUE;或者定义一个XXXConstants类来存放一组相关的常量。标记接口是一种不包含任何方法声明的接口,它只是标记它的实现类具有某种特性。

Javachichi的博客 2022

多个MOS管并联应用场景的四大要点

动态运行时,阈值电压Vgsth最低的MOS管首先打开,最后关闭。这种MOS管一般会占据更多的开关损耗,并且在开关转换过程中,承担了更高的电应力。

VBsemi的博客 3749

《Effective Java》真的是一本值得一直去钻研的好书

文章目录《Effective Java》读书笔记第一章 引言第二章 创建和销毁对象1 考虑使用静态工厂方法替代构造方法**2 当构造方法参数过多时使用builder(建造者) 模式**3 使用私有构造方法或枚类实现Singleton 属性*4 使用私有构造方法执行非实例化*5 依赖注入优于硬连接资源(hardwiring resources)**6 避免创建不必要的对象*7 消除过期的对象引用*8 避免使用Finalizer 和Cleaner 机制**9 使用try-with-resources 语句替代t

追求幸福,探索未知的博客 8642

从零到一:TI AWR1843 Boost EVM与DCA1000 EVM的ADC数据采集实战指南

本文详细介绍了TI AWR1843 Boost EVM与DCA1000 EVM的ADC数据采集实战指南,涵盖硬件连接、软件配置、固件烧录、雷达参数设置及数据处理等关键步骤。通过MATLAB等工具,开发者可高效实现毫米波雷达数据采集,适用于自动驾驶、工业检测等领域。

weixin_29001327的博客 967

Hermes Agent 常见报错排查与问题解决

Hermes Agent 常见报错排查指南 摘要 本文全面梳理了 Hermes Agent 使用过程中的10类常见报错,按阶段划分为安装期、运行期认证类、后端基础设施类和长任务执行类四大类,并提供了系统化的排查流程: 安装期问题:包括命令未找到和Python版本不兼容,主要检查PATH环境和Python版本是否符合要求 认证类问题:涉及API Key认证失败和速率限制,需要验证密钥配置和配额状态 基础设施问题:包含Docker报错、MCP连接失败和OAuth令牌过期,需检查相关服务状态 执行类问题:主要是上

简简单单Onlinezuozuo 1345

一文简单理解《Effective Java》建议

​一种失败的接口就是所谓的常量接口这样的接口不包含任何方法;它只包含静态 final 属 性,每个输出一个常量。使用这些常量的类实现接口,以避免需要用类名限定常量名。​常量接口模式是对接口的糟糕使用。类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这 个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他 们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须 实现接口,以确保二进制兼容性。

weixin_40709965的博客 2627

SAP-ABAP:SAP IDOC 全面学习指南

SAP-ABAP:SAP IDOC 全面学习指南

baidu_35680696的博客 1616

带你快速看完9.8分神作《Effective Java》—— 并发篇(工作里的这些坑你都遇到过吗?)

???? Java学习:Java从入门到精通总结 ???? Spring系列推荐:Spring源码解析 ???? 最近更新:2021年12月16日 ???? 个人简介:通信工程本硕????、阿里新晋猿同学????。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤 ???? 点赞 ???? 收藏 ⭐留言 ???? 都是我最大的动力! 豆瓣评分9.8的图书《Effective Java》,是当今世界顶尖高手Josh Bloch的著作,在我之前的文章里我也提到过,编程就像练武,既需要外在的武功招

如切如磋,如琢如磨 1万+

【Proteus仿真】用24C04与1602LCD设计的简易加密电子密码锁

【Proteus仿真】用24C04与1602LCD设计的简易加密电子密码锁

perseverance51博客 1121

习题加餐4. 冒险者工会

这个问题通过贪心算法解决,展示了贪心算法在解决一些优化问题中的强大能力。关键在于如何识别贪心选择的性质,并通过数学证明确保这种方法能够达到全局最优解。通过对问题的深入理解和逻辑推理,我们可以设计出有效的算法来解决问题,同时也需要注意代码实现中的细节,如排序和双指针的使用,以确保算法的效率和正确性。贪心算法的应用这个问题展示了贪心算法在实际问题中的应用。贪心算法通过在每一步选择当前最优的解决方案,期望最终达到全局最优解。该问题中,每次选择能力最接近但足以完成最难任务的冒险者,就是贪心选择的体现。

tang7mj的博客 1349

Effective Java 经典学习(一)

  (尊重劳动成果,转载请注明出处:https://blog.youkuaiyun.com/qq_25827845/article/details/85016496冷血之心的博客) 系列文章: Effective Java经典学习(一) 目录 第二章:创建和销毁对象 (1)使用静态工厂方法代替构造器 (2)遇到多个构造器参数时要考虑用构建器 (3)用私有构造器或者枚举类型强化Singleton...

冷血之心的博客 8618

Ubuntu零基础入门到精通【1.1讲】:什么是 Ubuntu?一文带你彻底搞懂!

🏆 本文收录于 《滚雪球学 Ubuntu》 专栏。 本专栏面向有一定计算机基础,但尚未系统学习 Linux / Ubuntu 的读者,采用“滚雪球式学习法”:先装好、再会用、再理解、再优化、再实战,带你从第一次进入 Ubuntu 桌面 / 终端开始,逐步掌握 Ubuntu 的日常使用、命令操作、软件安装、系统配置、开发环境搭建与服务器部署能力。

**My Coding Family** 5794

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

在没有考虑默认序列化形式是否合适之前,不要接受它。如果对象的物理表示与其逻辑内容相同,则默认的序列化形式可能是合适的。/*** @serial/*** @serial/*** @serial即使你认为默认的序列化形式是合适的,你通常也必须提供readObject方法来确保不变性和安全性。Entry next;将导出的 API 永久地绑定到当前的内部实现。占用过多的空间。消耗过多的时间。可能导致堆栈溢出。

长三月的游戏开发 2538

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

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

接口只应该用来定义类型,不要用来导出常量。想要导出常量,可以把它们放在相关的类中,如Integer类中的MAX_VALUE;或者定义一个XXXConstants类来存放一组相关的常量。

长三月的游戏开发 1047

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

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

标记接口是一种不包含任何方法声明的接口,它只是标记它的实现类具有某种特性。如Serializable 接口,实现此接口的类可以写入ObjectOutputStream(被序列化)。如果要定义类型,那么使用标记接口优于使用标记注解。标记接口定义的类型由标记类的实例实现;标记注解不会。前者在编译时捕获错误,后者在运行时才能捕捉错误。可以更精确地定位。标记注解可以应用于任何类或接口,而标记接口限定于它的实现类。标记注解的优点是:它们可以是其他注解功能的一部分。

AlbenXie的博客 116

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

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

Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说: “如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

长三月的游戏开发 1677

读完 Effective Java,我整理这 59 条技巧!

作者:Dong GuoChao链接:https://blog.dogchao.cn/?p=70 Effective Java,Java名著,必读。 如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素质。 以下内容只记录了我自己整理的东西,还是建议读原文。为了聚焦知识点,一些说明故意忽略掉了。相当于是一篇摘要。 1、考虑用静态工厂方法替代构造函数 例子: Integer.valueOf(“1”)、Boolean.valueOf(“true”)等。 优势: 可读性高(方法名) 性

Java技术栈,分享最主流的Java技术 438

读完《Effective Java》: 我整理这 50 条技巧

点击上方Java后端,选择设为星标优质文章,及时送达《Effective Java》Java名著,必读。如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素...

公众号:Java后端 317

读完《Effective Java》后我淦了 50 条开发技巧

《Effective Java》Java名著,必读。如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素质。以下内容只记录了我自己整理的东西,还是建议读原文。为...

emprere的博客 156

读完 Effective Java,我整理了 59 条技巧!(含pdf)

点击????方“逆锋起笔”,公众号回复视频教程领取大佬们推荐的学习资料上一篇:CTO 写低级 Bug,致公司 70 GB 数据泄露!作者:Dong GuoChao链接:https://blog...

weixin_38754349的博客 908
上一篇: 跟着实例学Go语言(四)
下一篇: 5天带你读完《Effective Java》(二)
长三月
博客等级 码龄9年 228粉丝 80原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值