Featured image of post 《EffectiveJava》阅读笔记

《EffectiveJava》阅读笔记

创建和销毁对象

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

  1. 静态工厂方法的一个优点是,不像构造方法,它们是有名字的。
  2. 静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。
  3. 静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。
  4. 静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。
  5. 静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。
  6. 只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。
  7. 静态工厂方法的第二个缺点是,程序员很难找到它们。

下面是一 些静态工厂方法的常用名称。以下清单并非完整:

  1. from——A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
  2. of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set faceCards = EnumSet.of(JACK, QUEEN, KING);
  3. valueOf——from 和 to 更为详细的替代 方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  4. instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如: StackWalker luke = StackWalker.getInstance(options);
  5. create 或 newInstance——与 instance 或 getInstance 类似,除了该方法保证每个调用返回一个新的实例,例如: Object newArray = Array.newInstance(classObject, arrayLen);
  6. getType——与 getInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例 如:FileStore fs = Files.getFileStore(path);
  7. newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型, 例如:BueredReader br = Files.newBueredReader(path);
  8. type—— getType 和 newType 简洁的替代方式,例如:List litany = Collections.list(legacyLitany);

遇到多个构造器参数时要考虑使用构建器

三种构建器:

  1. 重叠构造器(telescoping constructor)模式
  2. JavaBeans模式
  3. 建造者(Builder)模式

builder 比 JavaBeans 更安全。

Buillder模式模拟了具名的可选参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 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;

    private NutritionFacts(Builder builder) {
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.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);
        }
    }
}
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();

Builder 模式也适用于类层次结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Builder pattern for class hierarchies
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

public abstract class Pizza {
    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}
import java.util.Objects;
public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }
        @Override public NyPizza build() {
            return new NyPizza(this);
        }
        @Override protected Builder self() {
            return this;
        }
    }
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}
public class Calzone extends Pizza {
    private final boolean sauceInside;
    public static class Builder extends Pizza.Builder<Builder> {
        private boolean sauceInside = false; // Default
        public Builder sauceInside() {
            sauceInside = true;
            return this;
        }
        @Override public Calzone build() {
            return new Calzone(this);
        }
        @Override protected Builder self() {
            return this;
        }
    }
    private Calzone(Builder builder) {
        super(builder);
        sauceInside = builder.sauceInside;
    }
}

如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builder模式就是一种不错的选择。

用私有构造器或者枚举类型强化Singleton属性

  1. 第一种方法中,成员是 final 修饰的属性
  2. 在第二个实现单例的方法中,公共成员是一个静态的工厂方法:
  3. 实现一个单例的第三种方法是声明单一元素的枚举类:
1
2
3
4
5
6
// Singleton with public final field
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}
1
2
3
4
5
6
7
// 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() { ... }
}

创建一个使用这两种方法的单例类 (第 12 章),仅仅将 implements Serializable 添加到声明中是不够的。 为了维护单例的保证,声明所有的实例属性为 transient ,并提供一个 readResolve 方法 (条目 89)。否则,每 当序列化实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情 况发生,将这个 readResolve 方法添加到 Elvis 类:

1
2
3
4
5
// Enum singleton - the preferred approach
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

单元素的枚举类型经常成为实现Singleton的最佳方式。

通过私有构造器强化不可实例化的能力

企图通过将类做成抽象类来强制该类不可被实例化是行不通的。因为该类可以被子类化,并且该子类也可以被实例化。它误导 用户认为该类是为继承而设计的(条目 19)。

让这个类包含一个私有构造器,它就不能被实例化。

1
2
3
4
5
6
7
8
// Noninstantiable utility class
public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    }
    ... // Remainder omitted
}

优先考虑依赖注入来引用资源

ps: 有点像设计原则的依赖倒转原则

静态工具类和Singleton类不适合于需要引用底层资源的类。

当创建一个新的实例时,就将该资源传到构造器中。

字典dictionary是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器SpellChecker中。

1
2
3
4
5
6
7
8
9
// 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) { ... }
}

避免创建不必要的对象

String s = new String(“bikini”); // DON’T DO THIS!

改进后的版本如下:

String s = “bikini”;

一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个「昂贵的对象」,建议将其缓存起来 以便重复使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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();
    }
}

自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。

1
2
3
4
5
6
7
// 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;
}

由于打错了一个字符,变量sum被声明成Long而不是long,这意味着程序构造了大约2的31次方个不必要的 Long 实例(大约每次往 Long 类型的 sum 变量 中增加一个 long 类型构造的实例),把 sum 变量的类型由 Long 改为 long ,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。

消除过期的对象引用

考虑以下简单的堆栈实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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);
    }
}

如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序 不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不 会解除的引用。

清空对象引用应该是例外而不是规范。

只要类是自己管理内存,程序员就应该警惕内存泄露问题。

内存泄露的另一个常见来源是缓存。

内存泄露的第三个常见来源是监听器和其他回调

避免使用终结方法和清除方法

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。

清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

不应该 Finalizer 和 Cleaner 机制做 任何时间敏感(time-critical)的事情。

不应该依赖于 Finalizer 和 Cleaner 机制来更 新持久化状态。

使用 finalizer 和 cleaner 机制会导致严重的性能损失。

finalizer 机制有一个严重的安全问题:

try-with-resources优先于try-finally

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 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);
    }
}

作为一个稍微有些做作的例子,这里有一个版本的 firstLineOfFile 方法,它不 会抛出异常,但是如果它不能打开或读取文件,则返回默认值:

1
2
3
4
5
6
7
8
9
// try-with-resources with a catch clause
static String firstLineOfFile(String path, String defaultVal) {
    try (BufferedReader br = new BufferedReader(
        new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}

对于所有对象都通用的方法

覆盖equals时请遵守通用约定

约定内容,来自Object的规范

  • 自反性: 对于任何非空引用 x, x.equals(x) 必须返回 true。
  • 对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。
  • 传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则x.equals(z) 必须返回 true。
  • 一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用 必须始终返回 true 或始终返回 false。
  • 对于任何非空引用 x, x.equals(null) 必须返回 false。

自反性(Reflexivity)

对称性(Symmetry)如果这样编码:

传递性(Transitivity)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import java.util.Objects;
public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }
    // Broken - violates symmetry!
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(
                ((CaseInsensitiveString) o).s);
        if (o instanceof String) // One-way interoperability!
            return s.equalsIgnoreCase((String) o);
        return false;
    }
    ...// Remainder omitted
}

将会导致:

重构为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Point {
    private final int x;
    private final int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
    ... // Remainder omitted
}

现在,p.equals(cp)返回true,cp.equals(p)则返回flase。

你可以这样尝试,让ColorPoint.equals在进行"混合比较"时忽略颜色信息:

这种方法确实提供了对称性,但是却牺牲了传递性:

现在, p1.equals(p2) 和 p2.equals(p3) 返回了 true,但是 p1.equals(p3) 却返回了 false,很明显违 背了传递性的要求。前两个比较都是不考虑颜色信息的,而第三个比较时却包含颜色信息。

未完待续。。。

类和接口

使类和成员的可访问性最小化

尽可能地使每个类或者成员不被外界访问公有类的实例域决不能是公有的

皖ICP备2024056275号-1
发表了78篇文章 · 总计149.56k字
本站已稳定运行