Effective Java (3rd Editin) 读书笔记:1 创建和销毁对象

1 创建和销毁对象

Item 1:考虑用静态工厂方法取代构造器

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

静态工厂方法的优点:

  1. 有名字,因此可以直接看出来它的用法,如 Boolean.valueOf(bool)
  2. 不要每次创建新对象
  3. 可以返回方法返回类型的子类
  4. 可以根据输入参数,返回不同的类型,比如 EnumSet.of(...) 方法,根据元素长度,返回 RegularEnumSet 或者 JumboEnumSet
  5. 方法返回对象的类,在写此方法时,可以不存在。典型的例子是,JDBC

静态工厂方法的缺点:

  1. 只提供静态工厂方法的类,如果没有 public 或 protected 的构造器,不能有子类
  2. 由于静态工厂方法在 API 文档中没有被特别标注(像构造器那样),程序员难以找到它们

Item 2:当构造参数很多时,考虑使用 builder

当构造时的参数不超过 3 个时,通常使用 Telescoping constructor pattern 或者 JavaBean Pattern

// Telescoping constructor pattern
public HashMap();
public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor)

Telescoping constructor pattern 的缺点是,当参数多于 3 个时,难以书写和阅读。

// JavaBean Pattern
public class User {
    private String name;
    private String password;
    
    public void setName(String name);
    public void setPassword(String password);
}

JavaBean Pattern 的缺点是无法实现类的 immutable,因为每个使用对象的客户都可以用 set 方法修改对象的属性。

当构造器或者静态工厂方法含有超过(或者将来可能超过) 3 个的参数时,推荐使用 buider。

// 支持类继承拓展的 builder pattern
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) {
            topping.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();
    }
}

Item 3:使用私有构造器或者枚举类型来加强单例属性

// public final 字段的单例
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    
    public void leaveTheBuilding() {...}
}

public final 字段实现单例的优点是:

  1. 显然是个单例模式
  2. 简单
// 静态工厂的单例
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    public static Elvis getInstance() { return INSTANCE; }
    
    public void leaveTheBuilding() {...}
}

静态工厂实现单例的优点:

  1. 灵活性,比如每个线程一个单例
  2. 可以实现泛型单例工厂
  3. 方法的引用可以作为 supplier,如 Elvis::instance 是一个 Supplier<Elvis>
// 枚举实现的单例 -- 推荐方式
public enum Elvis {
    INSTANCE;
    
    public void leaveTheBuilding() {...}
}

枚举类型实现单例可以保证绝对的安全:

  1. 保证单例,即使是面对复杂的序列化或反射攻击(前两个方法做不到)
  2. 免费提供序列化机制

Item 4:使用私有构造器来加强不可实例化

抽象类不能保证不可实例化性(noninstantiability),其子类可以实例化,而且它会误导使用者认为此类被设计用来继承。

可行的方案是私有化构造器:

// Noninstantiable utility class
public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionErrot(); // 防止反射实例化
    }
    ...
}

Item 5:使用依赖注入取代硬编码的依赖实例化

硬编码的依赖实例化是意译,原文是 hardwiring resources。

// 静态工具模式(不建议,不灵活且不可测试)
public class SpellChecker {
    private static final Lexicon dictionary = ...;
    
    private SpellChecker() {} // Noninstantiable
    
    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

// 单例模式(不建议,不灵活且不可测试)
public class SpellChecker {
    private final Lexicon dictionary = ...;
    
    private SpellChecker() {}
    public static INSTANCE = new SpellChecker(...);
    
    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

在实现一个类时,如果它依赖了其他资源且该资源的类型会影响此类,那么不要使用单例模式或者静态工具模式,也不要让它硬编码创建依赖实例。推荐的做法是,将依赖对象或依赖对象的工厂作为参数,传递给构造器、静态工厂方法或 builder,这就是依赖注入,它大大增强了类的灵活性、复用性和可测试性

// 依赖注入,具备了灵活性和可测试性
public class SpellChecker {
    private final Lexicon dictionary;
    
    private SpellChecker(Lexicon dictionary) {
        this.dictionary = dictionary;
    }
    
    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

对于大项目,通常包含成千上万的依赖,使用依赖注入会使得项目复杂化,但是可以通过使用优秀的依赖注入框架来解决这个问题,比如 Dagger、Guice 或 Sping。

Item 6:避免创建不必要的对象

创建了不必要的对象的例子:

  • String s = new String("bikini"); 中创建了两次 String 对象。
  • String.matches(...) 方法中,新建了 Pattern 对象来匹配,如果多次调用此方法,会多次创建 Pattern 对象。
  • Long sum = 0L; sum += 1; 原始类型的自动装箱也会创建不必要的对象,因此尽量使用原始类型替代其包装类。

但是,有些时候应该创建重复对象(Item 50)。因为没有做好必要的防御性拷贝可能带来可怕的 bugs 和安全漏洞,而创建了不必要的对象仅仅会影响代码风格和性能。

Item 7:消除过时的对象引用

// ArrayList
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

内存泄漏的来源:

  1. 如 ArrayList 中用到的对象数组,对于这类自己管理内存的类,程序员要对内存泄漏保持警惕
  2. 缓存。常常使用 LinkedHashMap 在内存紧张时回收最近没有访问的对象
  3. 监听器和回调。可以使用 WeakHashMap 保存 callback 的弱引用

Item 8:不要使用 finalizer 和 cleaner

filnalizer 的行为不可预测,危险,通常都不需要。cleaner 不如 finalizer 那么危险,但是仍然不可预测,缓慢,通常也不需要。最好的建议就是不要使用它们。

Item 9:用 try-with-resources 取代 try-finally

try-with-resources 语句块简洁又周到,是 Java 7 以来最优的关闭资源的方式,能使用它的场合就使用它,否则再考虑 try-finally。

举个例子:

	BufferedReader br = new BufferdReader(new FileReader(path));
	try {
        return br.readLine();
	} finally {
        if (c != null) {
            c.close();
        }
    }

如果物理设备出现故障无法访问,readLine() 会抛出异常,在 finally 代码块中 close() 也会抛出异常, 第二个异常会泯灭第一个异常,使得用户看不到自己真正关注的异常。

try (BufferedReader br = new BufferdReader(new FileReader(path))) {
    return br.readLine();
} 

使用 try-with-resources 代码块后,两个异常都会保留,且 close() (隐式自动关闭)的异常隐含在 readLine() 的异常中,因此保留了使用者真正关心的异常,同时被隐含的异常并没有被丢弃,可以在 stack trace print 中看到,还可以用 getSuppressed() 方法获取到 Throwable 对象。

猜你喜欢

转载自blog.csdn.net/weixin_40255793/article/details/82799147