《Effective Java》——对象的创建与销毁

Chapter 1 Introduction

Some rules or principles

  • the user of a component shoud know about its behavior
  • code should be reused ranther than copied
  • the dependencies between components shold be kept to a minimum
  • errors should be detected as soon as possible after made [best time is at compile stage]

Chapter 2 Creating and Destroying Objects

Item 1: Consider static factory methods instead of constructors

静态工厂方法取代构造器。

Definiation

A class can provide a public static factory method, which is simply a static method that returns an instance of the class.

一个类可以提供一个返回其实例的静态工厂方法。

Advantages

1.Static factory methods have name: a static factory with a well-chosen name is eaiser to use and the resulting client code eaiser to read

2.Be not required to create a new object each time when they're invoked beacause of static. 静态工厂方法多次调用返回同一个对象的能力允许对实例对象创建的时间保持严格的控制,这样的类也称为实例控制(instance congtrol)。

3.Return an object of any subtype of their return type. 静态工厂方法可以返回 【返回类型】的任意子类型的对象。

4.The class of the returned object can vary from call to call as a function of the input parameters. 返回对象的类可以随着输入参数的变化而变化。

对于EnumSet类,其没有共有构造方法,只有静态工厂。如果元素的个数小于或等于64个,静态工厂返回一个RegularEnumSet实例,其为long类型;若元素大于64个,则返回一个JumboEnumSet实例,返回一个long类型的数组。

5.The class of the returned object need not exist when the class containing the method is written. 当包含此静态工厂的方法已经被编写的时候,静态工厂方法返回对象的类可以不存在。

Limitations

1.Classes without public or protected constructors cannot be subclassed. 没有共有或者受保护的构造其的类不能够被继承。

建议多用组合,少用继承。

2.Static factory methods are hard for programmers to find. 很难找到静态工厂方法。

由于缺少像接口一样的API文档,很难找到如何实例化一个提供静态工厂方法而非构造器的类。可以通过遵守通用的命名规约来减少这个问题。

Some common names for static factory methods:

  • from: 类型转换方法,接受一个其他类型的实例,返回当前类型的实例。

Date d = Date.from(instance);

  • of:将多个参数聚合在一起的方法
  • valueOf:可以替代offrom的更加详细的方法
  • instance | getInstance:返回一个由其参数描述的实例
  • create or newInstance:和上一个相似,但是保证返回的是两个不同的实例
  • getType:返回某个实例的类型
  • newType:和newInstance相似,但是如果用在不同的类中,则返回的是工厂方法返回对象的类型
  • typegetTypenewType更为简洁的替代

Summarization

静态工厂方法和公有构造方法都有他们自己的用处,要充分考虑他们的优劣情况。

Often static fatories are preferable, so avoid the reflex to provide public constructors without first considering static factories.

通常情况下静态工厂是更优的选择,避免在不先考虑静态工厂方法的情况下本能地提供共有构造器。


Item 2: Consider a builder when faced with many constructor parameters

遇到过多的构造器参数的时候,考虑使用builder

caseI-Nutrition Facts

食品包装上的营养成分表中,有许多营养成分。这些营养成分中大部分都是非零值,只有少部分是可选择的。如何为这样的类来编写构造方法?

Solution1: Telescoping constructor

使用可伸缩(telescoping constructor)构造方法模式,有一个完整的构造方法,其他的直接调用此构造方法。

kotlin 复制代码
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;
    }
}

思考: 这么写有什么缺点?

  • 当某个类的参数数量变得很大的时候,构造方法的数量会膨胀
  • 易读性比较差
  • 扩展性较低
Solution2: JavaBean Pattern
  • 只有一个无参构造器
  • 在外部使用setter()方法为属性赋值

思考:JavaBean 模式有什么缺点?

  • 将一条初始化语句拆分为数个赋值语句,处于线程不安全的环境中,会导致数据不一致的情况
  • 在对象手动赋值完成之前,可以锁住对象,防止在对象初始化之前被调用
Solution3: Builder Pattern
  • Instead of making the desired object directly, the client calls a constructor (or static factory) with all of the required parameters and gets a builder object.
  • Then the client calls setter-like methods on the builder object to set each optional parameter of interest.
  • Finally, the client calls a parameterless build method to generate the object, which is typically immutable.

Step1:客户端调用带有所需参数的构造器或者静态工厂方法 获取一个builder对象

Step2:客户端调用builder对象中类似于setter的方法来设置每个可选参数

Step3:客户端调用一个无参build方法来生成一个不可变的对象。

kotlin 复制代码
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;
    
    // 静态内部类 Builder
    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;
    }
}

静态内部类Buildersetter方法返回其本身,因此可以进行链式调用。即:

scss 复制代码
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                        .calories(100).sodium(35).carbohydrate(27).build();

caseII: Pizza

Builder 模式非常适合继承结构。抽象的类有抽象的Builder,而具体的类有具体的Builder。

使用Builder模式的Pizza类为:

scala 复制代码
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();

        // subclass must override this method to return "this"
        protected abstract T self();
    }

    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }


}

注意 Notice

  • Pizza.Builder是一个带有递归类型参数 (Recursive type paramter)的泛型类型。与抽象的self方法在一起可以实现子类中的链式调用而不需要强制类型转换

NyPizza

scala 复制代码
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);
        }

        // Notice: build方法应该返回具体的子类型,而非父类
        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }

    @Override
    public String toString() {
        return "NyPizza{" +
                "size=" + size +
                ", toppings=" + toppings +
                '}';
    }
}

注意 Notice

NyPizza中,build()方法返回NyPizza类型的对象。

协变返回类型:子类中方法的返回值类型(NyPizza) 为 父类方法返回类型(Pizza)的子类

csharp 复制代码
public class PizzaClient {

    @Test
    public void testPizzaStore() {
        // 使用 Builder 创建一个 NyPizza 对象
        NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.SMALL)
                .addTopping(Pizza.Topping.PEPPER).addTopping(Pizza.Topping.HAM)
                .build();
        System.out.println(nyPizza);
    }
}

advantages

  • can have multiple varage parameters
  • quite flexible,
    • 单个Builder可以重复创建多个对象
    • Builder的参数可以在build()方法之前进行调整,以改变创建的对象
    • Builder可以在创建对象的时候,自动注入一些属性

disadvantages

  • must create a builder to create an object
  • more verbose than telescoping pattern, then consider use a builder when the class has more than 4 parameters

Item 3: Enforce the singleton property with a private constructor or an enum type

使用私有构造器或者枚举类来实现单例属性

常见的实现单例方式的方法为:私有构造器 + 静态公共方法。

Approach I Public Static Factory Method

csharp 复制代码
public class SingletonI {

    private static final SingletonI INSTANCE = new SingletonI();
    
    private SingletonI () { }

    /**
     * static factory method
     * @return
     */
    public static SingletonI getInstance() {
        return INSTANCE;
    }
}

advantages:

  • 可以使用泛型工厂
  • 调用方法的引用可以作为一个supplier,例如SingletonI::getInstance

Approach II Public Static Property

java 复制代码
public class SingletonII {
    
    public static final SingletonII INSTANCE = new SingletonII();
    
    private SingletonII() { }
}

advantages

  • 公共静态属性使用final修饰,引用不可变
  • 实现起来比较简单

Approach III Enum

arduino 复制代码
public enum SingletonIII {
    INSTANCE;
}

Approach IV Static Inner-Class

csharp 复制代码
public class SingletonIV {

    // private constructor
    private SingletonIV() { }

    private static class SingletonHolder {
        // define and initialize the outer instance in inner-class
        private static final SingletonIV INSTANCE = new SingletonIV();
    }
    
    public SingletonIV getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
}

Approach V Double Check Lock

csharp 复制代码
public class SingletonV {

    // private, static and volatile instance
    private static volatile SingletonV INSTANCE;

    // private constructor
    private SingletonV() { }

    // public static method to get instance
    public static SingletonV getInstance() {
        // check whether the instance is null
        if (INSTANCE == null) {
            // lock the Class by synchronize
            synchronized (SingletonV.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SingletonV();
                }
            }
        }
        return INSTANCE;
    }
}

常见破坏单例的方式为:

  • 使用反射的方式,破坏私有构造器,来创建更多的对象。如果想要避免通过反射的攻击,可以修改构造器,在构造方法被第二次访问的时候,自动抛出异常。
  • 序列化与反序列化 。为了防止单例类序列化和反序列化的过程中破坏单例,可以在单例类中添加readResolve()方法

Item 4: Enforce noninstantiablity with a private constructor

使用私有构造器来执行非实例化。

在开发的时候,经常会编写到其属性和方法全部为static的类,例如一些工具类等。对这些类进行实例化是没有意义的,有没有好的办法类阻止其被实例化?

Approach I Abstract class

最先想到的肯定是抽象类,抽象类不能被实例化。但是这样做有几个致命问题:

  • it can be subclassed and the subclass can be initialized
  • it misleads the user into thinking of inheritance of abstract class

Approach II Private Constructor

通过私有构造器来实现类的非实例化。

csharp 复制代码
public class UtilityClass {
    
    // private constructor
    private UtilityClass () {
        throw new AssertionError("This class is non-instantiable");
    }
}

但是,子类的实例化需要调用父类的构造器,父类私有构造器会阻止子类的实例化。

Item 5: Prefer dependency injection to hardwiring resources

倾向依赖注入而非硬链接

Existing problems

Static utility classes and singletons are inappropriate for classes whose behavior is parameterized by an underlying resource.

静态工具类和单例类并不适用于那些参数化底层依赖的类。

Case-SpellChecker

在创建实例的时候,将底层依赖通过构造器传入,这就是依赖注入的一种形式。

arduino 复制代码
public class SpellChecker {
    
    private final String dictionary;
    
    public SpellChecker(String dictionary) {
        this.dictionary = dictionary;
    }
    
    public boolean isValid(String word) { 
        return dictionary.contains(word)
    }
}

advantages

  • it works with an arbitrary number of resources
  • it preserves immutability(不变性)
  • it could apply to constructors, static factories and bulders

依赖注入还有一种形式,就是将一个工厂通过构造器传入。

disadvantages

  • clutter up large projects. 对于成千上万个依赖的项目,使用依赖注入很可能会导致项目的混乱,但是可以使用一些依赖注入的框架来消除混乱。

summary

  • do not use a singleton or static utility class to implement a class which depends on underlying resources
  • 不要直接在类中创建底层依赖,而应该通过构造方法获取这些底层依赖

Item 6: Avoid create unnecessary object

避免创建不需要的对象

If there is a object which is immutable, reuse it instead of creating a new one.

javascript 复制代码
String s = new String("[Effective Java]"); // DON'T DO THIS

some suggestions

  • 如果一个不可变的类提供了构造器和静态工厂方法,建议使用静态工厂方法获取对象实例来避免创建出不需要的实例。
  • 如果一个对象的创建代价比较大,但是又想复用该对象,可以将其缓存起来

case-RomanNumeral

假设要判断一个字符串是否为罗马数字,可以使用正则表达式进行匹配。

typescript 复制代码
public 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})$");
}

上面的代码中每次调用的时候,都会在内部创建一个Pattern对象,其创建代价比较大,但是只使用到一次就被GC回收了。思考一下,如何进行复用呢?

在外层显示地使用Pattern编译正则表达式。

arduino 复制代码
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})$");

public static boolean isRomanNum(String s) {
    return ROMAN.matcher(s).matches();
}
  • 性能提升
  • 增加了可读性

Autoboxing

自动装箱是java的一个语法糖,其模糊了基本数据类型和包装类的区别,但是包装类和基本数据类型在性能上有较大的差别。

在实际的开发中,要倾向于基本数据类型,而不是包装类,因为潜在的自动拆装箱会创建大量的无用对象,造成严重的性能问题。

Item 7: Eliminate obsolete object references

消除废弃的对象引用。

即使JVM存在垃圾回收机制,也一定要注意内存管理问题。

  • 对于某些一定不会用到的对象,一定要及时使用null值,释放掉其引用,让垃圾回收器回收该废弃对象。如果废弃对象仍然被强引用所指,则会造成严重的内存泄漏问题。
  • null值的另一个好处就是,当外部代码错误指向废弃的对象,那么系统会抛出空指针异常,而非错误地运行下去。

Senarios for memory leaky

  • 一般来讲,当一个类自己去管理内存的时候,就应该警惕内存内存泄漏问题
  • 缓存。 存放在缓存中的对象很容易被忘记,当没有引用指向对象的时候,对象仍然存在于缓存中。使用WeakHashMap,某个entry会在其key没有被外部对象引用的时候自动清理
  • 监听器和其他回调。 实现API之后,客户端注册了回调但是却没有显示地取消注册这些回调,这些回调就会无法被回收而积累起来。可以将其使用弱引用存储,例如WeakHashMapkey

Item 8: Aovid finalizaers and cleaners

避免使用Finalizaer和Cleaner机制。

Finalizer是不可预测的、危险的并且通常并不是必须要使用的。Cleaner的危险性不如Finalizer,但是依旧是不可预测的,其运行缓慢,并且是非必要的。

Shortcomings

  • They might not be executed promptly. 并不是立马就执行的,从对象不可达到对象被清理之间的时间不可控。
  • 禁止依赖finalizer或者cleaner去更新一个持久的状态。例如锁的释放,你永远不知道这些方法何时被执行或者其永远不可能被执行。
  • Finalizer在运行是抛出的异常被忽略,导致其他的对象被破坏
  • finalizer会打开你的类,并进行finalizer机制攻击

Finalizer机制攻击的原理:

如果构造器或者序列化方法中抛出异常,恶意子类的finalizer机制可以运行在部分构造对象上,Finalizer机制可以使用静态属性记录该对象的引用,以防止其被垃圾回收。有了缺陷对象的引用,就可以调用该对象的任何方法。

防范Finalizer机制攻击的方法:

  • 使用final来修饰类或者使用final来修饰finalize()方法
  • 让类实现AutoClosable接口

Advantages

  • 作为安全网,对一些资源的释放起到兜底作用,有好过没有。
  • 本地同伴类(native peer)是一个普通对象托管的本地对象,假设这个本地对等类持有了非关键资源,并且对性能要求不高,使用finalizer机制或者cleaner释放该资源是可行的

Item 9: Prefer try-with-resources to try-finally

优先使用try with resources语句块而非try finally

try-finally

  • 多层资源需要嵌套的时候,代码会变得很复杂
  • try语句块和finally语句块可能会因为相同的原因抛出异常,但是finally语句块抛出的异常

try-with-resources

  • 具有更好的可读性,更加精简
  • 避免了因处理多个异常而产生的多层嵌套,污染代码的情况

Summary

使用try-with-resource替代try-finally代码块的好处:

  • 生成的代码更加简洁
  • 生成的异常更加有用
  • 更加容易编写必须关闭的资源代码,不会出错
相关推荐
十二同学啊1 分钟前
Spring Boot 整合 Knife4j:打造更优雅的 API 文档
java·spring boot·后端
老猿讲编程19 分钟前
详解Rust 中 String 和 str 的用途与区别
开发语言·后端·rust
Q_274378510931 分钟前
springboot基于微信小程序的智慧小区管理系统
spring boot·后端·微信小程序
007php0071 小时前
深入了解计算机网络中的路由协议与性能优化
java·开发语言·数据库·后端·python·计算机网络·golang
秋野酱1 小时前
基于javaweb的SpringBoot景区旅游管理系统设计和实现(源码+文档+部署讲解)
spring boot·后端·旅游
小白的一叶扁舟2 小时前
Elasticsearch(ES)与 MySQL 的对比分析及在 Spring Boot 中的使用
java·数据库·spring boot·后端·mysql·elasticsearch·中间件
技术的探险家2 小时前
R语言的并发编程
开发语言·后端·golang
周末程序猿2 小时前
技术总结|十分钟了解GPU
人工智能·后端
SomeB1oody3 小时前
【Rust自学】13.5. 迭代器 Pt.1:迭代器的定义、iterator trait和next方法
开发语言·后端·rust