EffectiveJava第二章:创建和销毁对象

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

静态工厂方法优点:

  1. 有名称
  2. 不必在每次调用的时候都创造一个新对象
  3. 可以返回原返回类型的任何子对象
  4. 所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值
  5. 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在

缺点:

  1. 类如果不含公有或者受保护的构造器,就不能被实例化
  2. 程序员很难发现它们

静态工厂方法惯用名称:

  1. from:类型转换方法,只有单个参数,返回该类型的一个相对应的实例,如
Date d = Date.from(instant);
  1. of:聚合方法,带有多个参数,返回该参数类型的一个实例,把它们合并起来,如
Set faceCards = EnumSet.of(JACK,QUEEN,KING);
  1. valueOf:比from和of更繁琐的替代方法,如
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

   4)instance/getInstance:返回的实例是通过方法的(如有)参数来描述的,但是不能说与参数具有同样的值,如

StackWalker luke = StackWalker.getInstance(Instance);
  1. create/newInstance:像4)一样,但其能确保每次调用都返回一个新的实例,如
Object newArray = Array.newInstance(ClassObject,arrayLen);
  1. getType:像getInstance,但是在工厂方法处于不同的类中的时候使用,Type表示工厂方法所返回的对象类型,如
FileStore fs = Files.getFileStore(path);
  1. newType:像newInstance,但是在工厂方法处于不同的类中的时候使用,Type表示工厂方法所返回的对象类型,如
BufferedReader br = Files.newBufferedReader(path);
  1. type:getType/newType的简版,如
List litany = Collections.list(legacyLitany);

 

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

如果类的构造器或者静态工厂中具有多个参数(遇到参数不固定的时候),构造器无法解决问题,原因:

  1. 重叠构造器模式(参数数量不一样就建一个set属性的构造方法):客户端代码难写且难以阅读
  2. JavaBeans模式:先建一个无参的构造器来创建对象,然后再调用setter方法来设置每个必要的参数以及每个可选的参数(一个属性一个set方法):①在构造过程中,JavaBean可能处于不一致的状态,②使得把类做成不可变的可能性不复存在

设计这种类时,使用建造者(Builder)模式:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成通常是不可变的对象。这个builder通常是它构建的类的静态成员类。

Builder优点:

  1. 模拟具名的可选参数

     2)使用类结构(继承父类extends),这里摘录两个例子:

①通过Builder操作接口返回的Model里的属性值,得到想要的结果,代码来自项目的ComeBackModel类

public class AMoled{
    boolean aBack;

    public boolean isABack() {
        return aBack;
    }

    AMoled(){

    }

    public static AMoledBuilder builder() {
        return new AMoledBuilder ();
    }


    public static final class AMoledBuilder {
        String aNum1;
        String aNum2;

        private AMoledBuilder () {
        }


        public AMoledBuilder withNum1(String aNum1) {
            this.aNum1= aNum1;
            return this;
        }

        public AMoledBuilder withNum2(String aNum2) {
            this.aNum2 = aNum2;
            return this;
        }

        public AMoledbuild() {
            AMoled  aMoled= new AMoled();
            // 判断是否相等,把值true/false返回给AMoled
            aMoled.aBack= (this.aNum1!= null && this.aNum1.equals(this.aNum2));
            return aMoled;
        }
    }
}

 

②一个类的参数不固定,有必选和可选

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{
        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.servings=servings;
            this.servingSize=servingSize;
        }

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

}

 

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

Singleton是指仅仅被实例化一次的类,用来代表无状态的对象,如函数,或者那些本质上唯一的系统组件。使类成为Singleton会使它的客户端测试变得十分困难。

两种常见的实现方法步骤1)保持构造器私有  2)导出公有的静态成员,以便允许客户端能够访问该类的唯一实例。

/**
 * ClassName: Elvis
 * Description:
 * date: 4/22/2019 1:33 PM
 *
 */
public class Elvis {
    /**
     * 方法一:公有静态成员是一个public final域
     * 优势:①公有的静态域是final的,所以该域总是包含相同的对象引用
     *      ②简单
     * 建议使用
     */
    public static final Elvis INSTANCE = new Elvis();

    /**
     * 方法二:公有的成员是个静态工厂方法
     * 优势:①不改变API的前提下可以改变该类是否为Singleton,工厂返回该类的唯一实例,但是容易被修改,
     *        如改成每个调用该方法的线程返回一个唯一的实例
     *      ②可以编写一个泛型Singleton工厂
     *      ③通过方法引用作为提供者,如Elvis::instance2就是一个Supplier
     * 除非满足以上,否则还是使用方法一
     */
    private static final Elvis INSTANCE2 = new Elvis();
    public static Elvis getInstance2(){
        return INSTANCE2;
    }


    // 其它共用的代码
    private Elvis(){
        // 私有的构造器
    }
    // otherMethod
    public void leaveTheBuilding(){

    }
}

/**
 * 将上述方法实现的Singleton类变成可序列化的
 * ①实现Serialzable
 * ②声明所有实例域都是瞬时的,并提供一个readResolve方法,否则每次反序列化一个序列号的实例时,都会创建一个新的实例
 * 在原来的类上修改
 */

public class Elvis implements Serializable{

    // 添加readResolve方法
    private Object readResolve(){
        return INSTANCE/INSTANCE2;
    }
}

 

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

/**
 * 与公有域方法类似但更加简洁,无偿的提供了序列化机制
 * 即使是在面对复杂的序列化或者反射攻击的时候,也绝对防止多次实例化
 * 
 * 推荐使用,除非Singleton必须扩展一个超类,而不是扩展Enum(可以声明枚举去实现接口)
 */
public enum Elvis{
    INSTANCE;

    // otherMethod
    public void leaveTheBuilding(){

    }
}

 

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

编写只包含静态方法和静态域的工具类不希望被实例化(newInstance()),因为实例化对它没有意义。然而缺少显示构造器的时候编译器会自动提供一个公有的、无参的缺省构造器。

  1. 企图通过将类做成抽象类来强制该类不可被实例化不可行。因为该类可以被子类化,并且该子类也可以被实例化
  2. 让这个类包含一个私有构造器,阻止当类缺少显示构造器时编译器自动提供
public class UtilityClass{
    // 显示的构造器是私有的,所以外部无法访问,保证了类在任何情况下都不会被实例化
    private UtilityClass(){
        throw new AssertionError();
    }
}

 

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

静态工具类和Singleton类不适合于需要引用底层资源的类。依赖注入形式:当创建一个新的实例时,就将该资源传到构造器中,提升类的灵活性、可重用性和可测试性。

public class SpellChecker{

    private final Lexicon dictionary;

    /**
     * 使用依赖注入来引用资源:
     * 创建SpellChecker实例的时候就把Lexicon实例的资源传到构造器
     * @param dictionary
     */
    public SpellChecker(Lexicon dictionary){
        this.dictionary = Objects.requireNonNull(dictionary);
    }
}

 

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

1)String.matches方法校验一个字符串是否与正则表达式相匹配,但不适合在注重性能的情形中重复使用,因为它在内部为正则表达式创建了一个Pattern实例却只用了一次。优化方法:把Pattern.compile(“正则表达式”)独立出来成为一个属性,定义正则表达式的规则,然后通过方法调用。

2)优先使用基本类型(int)而不是装箱基本类型(如Integer)

 

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

  1. 清空对象引用应该是一种例外而不是规范行为
  2. 只要类是自己管理内存,程序员就应该警惕内存泄露问题
  3. 内存泄露原因:

①元素被释放了却没有清空对象引用

②对象被缓存

③监听器和其他回调(确保回调被立即当做垃圾回收的最佳方法是只保存它们的弱引用,如只把它们保存成WeakHashMap中的键)

 

第8条:避免使用终结方法和清除方法

  1. 终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下不必要,在Java9使用清除方法(cleaner)代替。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也不必要的
  2. 注重时间的任务不应该由终结方法或者清除方法来完成
  3. 永远不应该依赖终结方法或者清除方法来更新重要的持久状态
  4. 使用终结方法和清除方法有一个非常严重的性能损失
  5. 终结方法有一个严重的安全问题:它们为终结方法攻击打开了类的大门。

攻击方法:从构造器或者它的序列化对等体(readObject和readResolve方法)抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途夭折的对象上运行。这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松的在这个对象上调用任何原本不允许在这里出现的方法。

从构造器抛出的异常,应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。这种攻击可能造成致命的后果。final类不会受到终结方法攻击,因为没人能够编写出final类的恶意子类。

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

  1. 如果类的对象中封装的资源(如文件/线程)需要终止,在不编写终结方法或者清除方法的情况下,让类实现AutoCloseable。详见第9条。

 

终结方法和清除方法的好处:

①当资源的所有者忘记调用它的close方法时,终结方法或者清除方法可以充当“安全网”

②本地对等体是一个本地的(非Java的)对象,普通对象通过本地方法委托给一个本地对象。因为本地对象不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。如果本地对等体没有关键资源,并且性能也可以接受,就使用终结方法或者清除方法执行这项任务。如果本地对等体拥有必须被及时终止的资源,或者性能无法接受,那么该类就应该具有一个close方法。

清除方法的使用:

public class Rom implements AutoCloseable{

    private static final Cleaner cleaner = Cleaner.create();

    private static class State implements Runnable{

        int numJunkPoles;

        State(int numJunkPoles){
            this.numJunkPoles=numJunkPoles;
        }
        @Override
        public void run() {
            System.out.println("Cleaning the room");
            numJunkPoles=0;
        }
    }

    private final State state;
    private final Cleaner.Cleanable cleanable;

    public Rom(int numJunkPiles){
        state=new State(numJunkPiles);
        cleanable=cleaner.register(this,state);
    }

    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}

 

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

①结论:处理必须关闭的资源,如InputStream、OutputStream和java.sql.Connection时,优先考虑使用try-with-resources,而不是try-finally

说人话:try-finally的关闭代码块太多了,而且如果第一个try发生了异常,那么第二个try的异常就会被禁止,以保留第一个异常。被禁止的异常会被打印在堆栈轨迹中,并注明它们是被禁止的异常,通过Throwable[] suppressed = e.getSuppressed();遍历该数组可以访问到

②使用方式:类实现implements  AutoCloseable接口,包含单个返回void的close方法

public class Test implements AutoCloseable{

    public static String main(String[] args) {

        try(BufferedReader br  = new BufferedReader(new FileReader("path"))){
             return br.readLine();
        } catch (IOException e) {
            // 输出catch块的异常信息
            System.out.println(e.getMessage());
            // 输出close块的异常信息
            Throwable[] suppressed = e.getSuppressed();
             for (int i = 0; i < suppressed.length; i++) {
                 System.out.println(suppressed[i].getMessage());
             }
        }

        @Override
        public void close() throws Exception {
            // 已自动关闭资源

            System.out.println(“已自动关闭资源”);
        }

    }

}

具体解释:摘自https://blog.csdn.net/weixin_40255793/article/details/80812961

try-finally异常处理有两种情况:

1、try 块没有发生异常时,直接调用finally块,如果 close 发生异常,就处理。

2、try 块发生异常,catch 块捕捉,进行第一处异常处理,然后调用 finally 块,如果 close 发生异常,就进行第二处异常处理。

 

在 try-with-resources 结构中,异常处理也有两种情况(注意,不论 try 中是否有异常,都会首先自动执行 close 方法,然后才判断是否进入 catch 块):

1、try 块没有发生异常时,自动调用 close 方法,如果发生异常,catch 块捕捉并处理异常。

2、try 块发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在catch 中被禁止,但是你可以在catch块中,用 Throwable.getSuppressed 方法来获取到被禁止异常的数组。

另外:

1)catch 块中,看不到 try-with-recourse 声明中的变量。

2)try-with-recourse 中,try 块中抛出的异常,在 e.getMessage() 可以获得,而调用 close() 方法抛出的异常在e.getSuppressed() 获得。

3)try-with-recourse 中定义多个变量时,由反编译可知,关闭的顺序是从后往前

 

 

你可能感兴趣的:(EffectiveJava,创建和销毁对象)