• [Java读书笔记] Effective Java(Third Edition) 第2章 创建和销毁对象


     

    第 1 条:用静态工厂方法代替构造器

    对于类而言,获取一个实例的方法,传统是提供一个共有的构造器。

      类可以提供一个公有静态工厂方法(static factory method), 它只是一个返回类的实例的静态方法。

      示例:Boolean的装箱类,将boolean基本类型值转换成一个Boolean对象引用

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

    静态工厂方法与构造器不同的优点

      第一大优势:有名称。更容易使用,更容易阅读理解。

      第二大优势:不必每次调用它们的时候都创建一个新对象。

      第三大优势:它们可以返回类型的任何子类型的对象。

      例如Java Collections Framework的集合接口45个工具实现,提供了不可修改的集合、同步集合等等。几乎所有都是通过静态工厂方法在一个不可实例化的类(java.util.Collections)中导出。

      第四大优势:所有返回对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。

      第五大优势: 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。

    静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。例如JDBC(Java数据库连接) API.

    静态工厂方法缺点

      第一: 类如果不含共有的或受保护的构造器,就不能被子类化。

      第二:程序员很难发现他们。在API文档中没有明确标识出来。

    静态工厂方法一些惯用名称:

     from:

    Date d = Date.from(instant);  

    of:

    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);  

    valueOf:

    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);  

    instance或getInstance:

    StackWaler luke = StackWaler.getInstance(options);  

    create或newInstance:

    Object newArray = Array.newInstance(classObject, arrayLen);  

    getType:

    FileStore fs = Files.getFileStore(path);  

    newType:

    BufferReader br = Files.newBufferReader(path);  

    type(getType和newType的简化版):

    List<Complaint> litany = Collections.list(legacyLitany);  

    总结,静态工厂方法和共有构造器各有用处,需要理解各自长处。静态工厂方法经常更加合适,因此切忌第一反应是提供公有构造器,而不是考虑静态工厂方法。

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

      静态工厂和构造器有个共同局限性:都不能很好地扩张到大量的可选参数。

      第一种方案(一般解决方法):重叠构造器(telescoping constructor)模式:

        提供第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有二个可选参数,以此类推,最后一个构造器包含所有可选参数。

        缺点:重叠构造器模式可行,但是有很多参数时,代码很难编写,难以阅读。

      第二种方案:JavaBeans模式:

        先调用一个无参数构造器来创建对象,然后再调用setter方法来设置每个必要参数和每个可选参数。

       缺点:在构造过程中,JavaBeans可能处于不一致的状态。(线程不安全) 另外一点是这种模式就不能把类做成不可变的。

      第三种方案:建造者(builder)模式:

       例子:

       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 {
            // 必须属性
            private final int servingSize;
            private final int servings;
            // 可选属性
            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 setCalories(int calories) {
                this.calories = calories;
                return this;
            }
    
            public Builder setFat(int fat) {
                this.fat = fat;
                return this;
            }
    
            public Builder setSodium(int sodium) {
                this.sodium = sodium;
                return this;
            }
    
            public Builder setCarbohydrate(int carbohydrate) {
                this.carbohydrate = carbohydrate;
                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;
    }

      使用代码:

    NutritionFacts cocaCola = new NutritionFacts.Builder(240,8).calories(100).sodium(35).carbohydrate(27).build();

    Builder模式便于编写和阅读。

    Builder模式也适用于类层次结构。例子:抽象类Pizza, 两个子类,一个NyPizza表示经典纽约风味披萨,一个Calzone表示馅料内置半月形披萨。(代码略)

     使用代码:

    NyPizza pizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addToping(ONION).build();
    Calzone calzone = new Calzone.Builder().addTopping(HAM).sauceInside().build();

    总结: 如果类的构造器或者静态工厂中具有多个参数,Builder模式是一种很好的选择,特别是当大多数参数是可选参数。

    第 3 条:使用私有构造器或枚举类型来强制实现 singleton 属性

    单例(singleton)就是一个只实例化一次的类。通常用于代表一个无状态的对象,比如函数,或者本质上是唯一的系统组件。

    使类成为Singleton会使它的客户端测试变得十分困难。

      两种常见实现方式:

        1. Public final field: 公有静态成员是一个final域:

      

    // Singleton with public final field
    public class Elvis {
        public static final Elvis INSTANCE = new Elvis();
        private Elvis() { ... }
        public void leaveTheBuilding() { ... }
    }

       该方法的优点:

    • API 明确表示这个类就是一个单例。公共静态属性是 final 的,所以它总是包含相同的对象引用。
    • 这个方法很简单。

        2. Singleton with static factory 公有成员是一个静态工厂方法:

    // 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() { ... }
    }

    该方法的优点:

    • 可以方便地将类的实现改为非单例,并且用户代码不需要改变。
    • 可以编写一个泛型单例工厂。
    • 方法引用可以被用作 supplier,例如 Elvis::instance 等同于 Supplier<Elvis>。

    为了实现Singleton类变成可序列化的(Serializable), 需要2点:

    1. 声明中加implements Serializable。

    2. 必须声明所有实例域都是瞬时的(transient),并提供一个readResolve方法。

         否则,每次反序列化一个序列化的实例时,都会创建一个新实例。

    // readResolve method to preserve singleton property
    private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
    }

    第三种实现方法:声明一个包含单个元素的枚举类型:

    // Enum singleton - the preferred approach
    public enum Elvis {
        INSTANCE;
        public void leaveTheBuilding() { ... }
    }

     这种方法在功能上与公有域方法相似,更加简洁。无偿地提供序列化机制,绝对防止多次实例化。

       总结:单元素的枚举类型经常成为实现Singleton的最佳方法。

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

      有时候可能需要编写只包含静态方法和静态域的类。比如java.lang.Math或者java.util.Arrays。

      还有java.util.Collections的方式,吧实现特定接口的对象上的静态方法,包括工厂方法组织起来。

      企图通过将类做成抽象类来强制该类不可被实例化是行不通的。

      该类可以被子类化,并且该子类也可以被实例化。

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

      副作用:使得一个类不能被子类化(不能被继承)。所有的构造器都必须显式或隐式地调用超类(superclass)构造器,

      而子类就没有可以访问的超类的构造器可调用了。

    第 5 条:优先考虑依赖注入来引入资源

      有许多类会依赖一个或者多个底层的资源。例如,拼写检查器需要依赖一个或多个词典。

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

      这里需要能够支持类的多个实例,每一个实例都使用客户端指定的资源(本例中的词典)。

      满足该条件的模式是:当创建一个新的实例时,就将该资源传到构造器中。(依赖注入的一种形式)

    // 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) { ... }
    }

      总之,不要用Singleton和静态工厂类来实现依赖一个或者多个底层资源的类,且该资源的行为会影响到该类的行为,也不要直接用这个类来创建这些资源。

      而应该讲这些资源或者工厂传给构造器(或者静态工厂,构建器),通过它们来创建类。这就是依赖注入

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

      通常来讲,重用一个对象比创建一个功能相同的对象更加合适。

      重用速度更快,并且更接近现代的代码风格。如果对象是不可变的(immutable)(条款 17),它总是可以被重用。

      第一种多余创建对象的场景:String字符串:

    String s = new String("bikini"); // DON'T DO THIS!

      这个语句每次执行时都会创建一个新的 String 实例,而这些实例的创建都是不必要的。

      如果这种用法发生在循环或者频繁调用的方法中,就会创建数百万个毫无必要的 String 实例。

      改进后:

    String s = "bikini";

      第二种多余创建对象的场景:正则表达式:

      写一个方法来确定一个字符串是否是一个合法的罗马数字:

    // 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})$");
    
    }

      这个实现的问题在于它依赖于 String.matches 方法。

      虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。

      因为它在内部为正则表达式创建一个 Pattern 实例,并且只使用一次,之后这个 Pattern 实例就会被 JVM 进行垃圾回收。

      创建 Pattern 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。

      为了提高性能,将正则表达式显式编译为一个 Pattern 实例(不可变)并且缓存它,在 isRomanNumeral 方法的每个调用中重复使用相同的实例:

    // 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();
        }
    }

      第三种多余创建对象的场景:自动装箱(autoboxing)

      它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。

    // 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 类型的 i)。

      把 sum 变量的类型由 Long 改为 long 会使性能得到很大提升。这个教训很明显:优先使用基本类型而不是包装的基本类型,也要注意无意识的自动装箱。

      这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。

      相反,创建和回收小的对象非常廉价,构造器只会做很少的工作,尤其在现代 JVM 实现上。 创建额外的对象以增强程序的清晰性,简单性或功能性通常是件好事。

      反之,通过维护自己的对象池来避免创建对象并不是一个好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例是数据库连接池。

    第 7  条:消除过期对象的引用

      Java语言,当你用完对象之后,他们会被自动回收。它很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。

    // 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);
        }
    }

      这个程序没有什么明显的错误,但有一个潜在的问题——“内存泄漏”。

      由于垃圾回收器的活动的增加,或内存占用的增加,程序的性能会下降。极端情况下导致程序失败(OutOfMemoryError)错误。

      程序中哪里发生了内存泄漏?

      如果一个栈先增长,后收缩,那么从栈弹出的对象不会被当作垃圾回收掉,即使使用栈的程序不再引用这些对象。

      这是因为栈内部维护了对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会再一次被解引用的引用。

      在上面这段代码中,数组“活动部分(active portion)”之外的任何引用都是过期的。活动部分是由索引下标小于 size 的那些元素组成的。

      这类问题的解决方法很简单:一旦对象引用过期,只需清空这些引用(将它们设置为 null)。

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

      清空对象引用应该是一种例外,而不是一种规范。消除过期引用的最好方法是让包含引用的变量结束其生命周期。

      常见的内存泄漏场景:

      1. 只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放,则该元素中包含的任何对象引用都应该被清空。

      2. 内存泄漏的另一个常见来源是缓存。一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。

        常见解决方案之一,用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会被自动删除。

        常见解决方案之二,缓存应该时不时的清除掉没用的项。清除工作可以由后台线程(ScheduledThreadPoolExecutor)来完成。

      3. 监听器和其他回调。如果你实现了一个API——其客户端注册回调(callbacks),但是没有显式地撤销他们的注册。

        除非采取一些操作来处理,否则这些回调会积累。确保回调被垃圾回收的一种方法是只存储弱引用(weak references)。

        例如,仅将它们保存在 WeakHashMap 的键(key)中。

    第 8 条:避免使用终结方法(finalizer)和清除方法(cleaner)

      终结方法通常是不可预测的、危险的、一般情况下不必要的。

      使用终结方法会导致行为不稳定、性能降低、以及可移植性问题。

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

        Finalizer 和 Cleaner 机制的缺点:

    •   不能保证他们能够及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,这段时间是任意长的。

                注重时间(time-critical)的任务不应该由终结方法或清除方法来完成。

                永远不应该依赖终结方法或清除方法来更新重要的持久状态。例如,释放共享资源(数据库)上的锁。

    •   如果忽略在终结过程中被抛出来的未捕获异常,该对象的终结过程也会终止。

                未被捕获的异常会使对象处于破坏状态。(corrupt state)

    •   严重的性能损失。
    •   严重的安全问题。从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。

                为了防止非final类受到终结方法攻击,要编写一个空的final的finalize方法。

      如果类的对象中封装的资源(例如文件或线程)确实需要终止,正确做法是:

         让类实现AutoCloseable,并且要求客户端在每个实例不再需要时调用close方法,利用try-with-resources确保终止,即使遇到异常也是如此。

      终结方法和清除方法的合法用途:

        1. 作为一个”安全网”,以防资源的拥有者忘记调用 close 方法。

            虽然不能保证 Finalizer 和 Cleaner 机制会及时运行(或者是否运行),但是将资源晚一点释放也要好过永远不释放。

        2. 使用Cleaner机制的方法与本地对等类(native peers)有关。

            本地对等类是一个由普通对象委托的本地(非 Java)对象。

            由于本地对等类不是普通的 Java 对象,所以垃圾收集器并不知道它,当它的 Java 对等对象被回收时,本地对等类也不会被回收。

            假设性能是可以接受的,并且本地对等类没有持有关键的资源,那么 Finalizer 和 Cleaner 机制可能是这项任务的合适的工具。

    总之,除非作为安全网或者为了终止非关键的本地资源,不要使用清除方法。对于Java9之前的版本,尽量不要使用终结方法。

    第 9 条:try-with-resources优先于try-finally

      Java 类库中包含许多必须手动调用 close 方法来关闭的资源, 比如InputStream、OutputStream 和 java.sql.Connection。

      从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:

    // try-finally - No longer the best way to close resources!
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }

     上述例子看起来还行,但是如果添加第二个资源,就会很糟:

    // 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();
        }
    }

    例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 方法的调用可能会引发异常,并且由于相同的原因,调用 close 方法可能也会失败。 在这种情况下,第二个异常完全冲掉了第一个异常。 在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调试变得非常复杂——通常你想要看到第一个异常来诊断问题。 虽然可以编写代码来抑制第二个异常,保留第一个异常,但是实际上没有人这样做,因为实现起来太繁琐了。

    Java 7 引入了 try-with-resources 语句时,所有这些问题都得到了解决。要使用这个构造,资源必须实现 AutoCloseable 接口,该接口由一个返回类型为 void 的 close 方法组成。Java 类库和第三方类库中的许多类和接口现在都实现或继承了 AutoCloseable 接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现 AutoCloseable 接口。

      实例1:

    // try-with-resources - the the best way to close resources!
    static String firstLineOfFile(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(
               new FileReader(path))) {
           return br.readLine();
    }

      实例2:

    // 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);
        }
    }

      使用try-with-resources更简洁,更容易进行诊断。以firstLineOffFile为例,如果调用readLine和close方法都抛出异常,后一个异常会被禁止,以保留第一个异常。

     实例3:

    // 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;
        }
    }

     还可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。上面例子它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值。

    总之,在处理必须关闭的资源时,优先考虑用try-with-resources,而不是try-finally。

  • 相关阅读:
    1.20 测试嵌套对象使用Object.defineProperty是否有效?
    1.6 对象解构赋值机制
    12.26 sessionStorage与locaStorage的区别(作用域、生命周期)
    break,continue,return 区别
    过滤关键词
    ES6 笔记
    Cesium 学习笔记
    Cesium 学习笔记
    【vue】清理代码
    《CSS世界》读书笔记(十六)
  • 原文地址:https://www.cnblogs.com/fyql/p/11369712.html
Copyright © 2020-2023  润新知