Effective Java 第3版 创建和销毁对象分享

一 前言

  • 最近在看Effective Java 第3版的相关内容,这里做个学习总结,加深一下印象。后面会抽出时间持续更新。

二 创建和销毁对象的阅读分享

2.1 用静态方法代替构造器

背景:

平常我们在创建类的对象的时候一般都是直接通过new的方式创建,这样是最直接也是最简单的方式,但是随着我们的类功能的不断扩展,字段不断增加。直接new的方式会给我们带来一些问题。

  • 类的属性太多调用方不知道该传什么必要的参数。
  • 有些工具类不需要频繁的创建,只需要创建一个对象就可以支持各种应用场景调用。
  • 不支持根据条件灵活创建子类的对象。

优点:

  • 静态方法创建对象可以有名称。

相对于直接new的方式创建对象,这种可以更好的理解创建对象的常见。比如我们常见的Java自带的类的使用姿势

    Date date = Date.from(Instant.now()); // 根据Instant对象常见Date类型对象
    Set set = EnumSet.of(ApprovalStatusEnum.DRAFT); // 根据多个枚举值参数返回一个枚举值集合
    Array[]stringArray = (Array[]) Array.newInstance(String.class, 2); // 根据类和数字创建一个字符串数组
    List edg = Collections.singletonList("edg"); // 创建只有一个元素的链表    

实际项目中类似的用法。rpc返回Response时返回BaseResp中的静态工厂防范,根据名称可以明显知道这个方法的作用,望文生义。

    public static BaseResp successBaseResp() {
        BaseResp baseResp = new BaseResp();
        baseResp.StatusMessage = ResponseEnum.SUCCESS.getMessage();
        baseResp.StatusCode = ResponseEnum.SUCCESS.getCode();
        return baseResp;
    }

    public static BaseResp failedBaseResp(String message) {
        BaseResp baseResp = new BaseResp();
        baseResp.StatusMessage = message;
        baseResp.StatusCode = FAIL;
        return baseResp;
    }
  • 避免频繁的创建对象

不需要在每次调用的时候都去创建新对象。可以节省很多创建对象带来的资源消耗。多次重复的调用返回一个相同的对象,可以实现‘实例控制类’的实例控制,比如实现单例模式。节省频繁创建较为典型的Java中的应用示例:

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

     /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code true}.
     */
    public static final Boolean TRUE = new Boolean(true);

    /**
     * The {@code Boolean} object corresponding to the primitive
     * value {@code false}.
     */
    public static final Boolean FALSE = new Boolean(false);
  • 返回的对象更加灵活(可以定制化返回任意类的实例,支持黑盒化返回子类的实例。)

比如Java中的不可实例化集合类Java.util.collections,这个集合工具类里封装了很多集合类型的导出实例。

    Collections.singletonList(); // 创建一个单例List
    Collections.unmodifiableMap(); // 创建一个不可变的Map
    Collections.synchronizedCollection(); // 创建一个线程安全的集合

可以支持定制化的返回子类对象。比如java中的EnumSet的应用,在枚举数量不超过64时,实例化的子类是RegularEnumSet,否则是JumboEnumSet。这个过程对我们是透明的。

 public static > EnumSet noneOf(Class elementType) {
        Enum[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

这样做的好处是外部调用者不需要care我实际返回的是什么子类对象(因为两个子类是默认的类访问级别,对包外不可见。不会显式的知道返回了什么子类的对象),外部调用者只需要通过父类的句柄引用子类就ok了。当我不需要RegularEnumSet这个类了,直接去掉对外部调用方也没有任何影响。

  • 方法返回对象所属的类,在编写静态方法的类时可以不存在。

这个特点是是服务提供者框架(Service Provider Framework)的基础。服务提供者框架:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来,比如JDBC(Java DataBase Connectivity)。我们在加载JDBC的驱动的时候可以指定对应的服务实现类,比com.mysql.jdbc.Driver或者oracle.jdbc.driver.OracleDriver。"灵活的静态工厂创建对象"是服务提供者框架的基础。

java6开始有一个通用的服务提供者框架Java.util.serviceLoader。他是由ClassLoader来发现并加载服务的机制,有了这种机制可以把服务的实现类和接口本身解耦开,服务的实现类和接口可以不在一个应用里。后面我再其他文章中详细介绍下java的SPI机制。

2.2 多个参数的构造器考虑用构建器

背景:

当一个类的属性比较多时,有些属性时必填的,有些是选填的。我们哪种构造器或者上面说到的静态工厂取创建对象呢?

有些小伙伴会采用重叠构造器的方式,第一个构造器只有必要的参数,第二个除了有必要的参数之后,还有一个非必要参数...以此类推。这种方式可以适用于参数比较少的情况。但是一旦属性多了之后就会变得不可控。我们需要为不需要的属性设置默认值,保证构造函数的顺序,还需要仔细去check参数的顺序,防止构造顺序错了之后带来一些错误问题。

有些小伙伴会采用javaBean的方式利用无参构造函数创建对象,然后set需要的属性。这种方式虽然简单明了,但是其实把构造函数的过程分到了不同的过程调用中,这种模式使得把类做成不可变的可能性不复存在,需要我们在调用的时候确保没有线程安全的问题。

优点:

构建器既保证了线程安全的问题,又保证了代码的可读性可灵活性。这就是Builder建造者模式的一种形式。builder建造者不会直接生成一个对象。而且让调用方利用所有必要参数的构造器先生成一个builder。然后用在builder对象上利用类似于setter的方法设置不必要的参数,然后提供一个build方法返回最外层的类的对象,这个builder一般是外层类的静态内部类。可以直接通过外部类引用到这个静态builder类。最后再对构造器做个收口,把构造函数重写成private,使得创建对象只能通过builder来创建。

public class SelfIntroduce {

    private final String name; // 姓名(required)
    private final String sex; // 性别(required)

    private final Integer age; // 年龄(optional)
    private final String hobby; // 兴趣,多个用【,】分割(optional)


    public static class Builder {
        private String name; // 姓名(required)
        private String sex; // 性别(required)

        private Integer age = 0; // 年龄(optional)
        private String hobby = ""; // 兴趣,多个用【,】分割(optional)
        
        public Builder(String name, String sex) {
            this.name = name;
            this.sex = sex;
        }
        public Builder age(Integer age) {
            this.age = age;
            return this;
        }
        public Builder hobby(String hobby) {
            this.hobby = hobby;
            return this;
        }

        public SelfIntroduce build() {
            return new SelfIntroduce(this);
        }
    }

    private SelfIntroduce(Builder builder) {
        name = builder.name;
        sex = builder.sex;
        age = builder.age;
        hobby = builder.hobby;
    }
}
public class Test {
    public static void main(String[] args) {
        SelfIntroduce build = new SelfIntroduce.Builder("rng", "male").age(10).build();
    }
}

再看一个书中的例子,展示了“层次化构造器”的特点,利用java协变返回类型的特点返回子类的实例。

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 toppings;

    abstract static class Builder> {
        EnumSet 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 {
        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 {

        // 必需的蘸料在内在外参数 
        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;
    }
}


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

2.3 用私有构造器强化Singleton属性和不可实例化能力

背景:

对于一些我们不需要的创建对象的类,比如一些工具类。创建这些工具类的对象没有任何意义,直接用工具类的静态方法就可以满足我们的需求。这时候就需要类的不可实例化的能力。

对于一些需要节省资源的场景(比如数据库连接,打印机等场景),只需要单例实例来做这个事情就ok了。还有为了方便控制的场景,比如日志的写入,需要保证只有一个实例写日志,否则日志就写的乱七八槽,还有一些泛型单例工厂的场景,这时候需要类的单例化的能力。

优点:可以利用私有构造器的方式达到我们想要的效果,让一个类可以根据我们的需求来创建单一对象或者不可实例化对象。

实现单例模式的方式

// 用公有静态域实现单例。
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public void leaveTheBuilding() { ... }
}


// 用公有静态方法实现单例。
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }
    public void leaveTheBuilding() { ... }
}


// 利用单个枚举类实现单例
public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() { ... }
}

ps:注意事项:
1,2两种方式创建单例对象时,为了保证正常序列化对象,需要把字段设置成transient。
为了防止反序列化的时候生成多个对象,需要重写readResolve方法
private Object readResolve() {
    // Return the one true Elvis and let the garbage collector
    // take care of the Elvis impersonator.
    return INSTANCE;
}
还需要防止反射的方式创建新对象
译注:使用 AccessibleObject.setAccessible 方法调用私有构造函数示例:

Constructor[] constructors = Elvis.class.getDeclaredConstructors();
AccessibleObject.setAccessible(constructors, true);

Arrays.stream(constructors).forEach(name -> {
    if (name.toString().contains("Elvis")) {
        Elvis instance = (Elvis) name.newInstance();
        instance.leaveTheBuilding();
    }
});
3 更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。是实现单例的最佳方法。不过这种方式不适用于扩展一个超类。
如果尝试用反射的方式创建对象就会报【Cannot reflectively create enum objects】的错误。

实现不可实例化方式的方式

采用私有构造函数的方式可以帮助我们得到一个不可实例化的类,比如我们常见的工具类都是类似的做法,java.lang.Math、java.util.arrays、java.util.Collections等。

Effective Java 第3版 创建和销毁对象分享_第1张图片

为了方式反射我们可以在尝试创建对象时报错。这样做还会使这个类不能被继承(因为要继承他的子类找不到合适的父类的构造方法。)

public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionError();
    } ... // Remainder omitted
}

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

背景:我们在项目开发中随着项目复杂程度,各个类之间会存在互相依赖的情况。常见的做法是把类变成单例或者静态工具类(不可实例化的类)。然后用静态不可变的域来注入依赖。但是这种方式不够灵活,一旦类B需要变化,或者类A不需要类B,需要的是和类B功能相似的类C,对于类A的改造来说就比较痛苦。

比如有这样一个场景,小明需要用手机看书,刚开始是这样的场景

/**
 * @description: 人相关类
 */
public class Person {

    private String sex; // 性别

    private String name; // 名字
}


public class XiaoMing extends Person {

    private final Iphone myPhone = new Iphone6();

    public void read() {
        // additional action
        myPhone.read();
    }

    public void play() {
        // additional action
        myPhone.play();
    }
}


/**
 * @description: 手机相关类
 */
public class Iphone {

    public void read() {
        System.out.println("打开了知乎开始读书故事");
    }

    public void play() {
        System.out.println("打开了lol开始玩游戏");
    }
}
public class Iphone6 extends Iphone {

    @Override
    public void read() {
        System.out.println("用iphone6打开了知乎开始读书故事");
    }

    @Override
    public void play() {
        System.out.println("用iphone6打开了lol开始玩游戏");
    }
}
public class IphoneX extends Iphone {

    @Override
    public void read() {
        System.out.println("用IphoneX打开了知乎开始读书故事");
    }

    @Override
    public void play() {
        System.out.println("用IphoneX打开了lol开始玩游戏");
    }
}

/**
 * @description: 小明起床开始看书,然后玩手机。
 */
public class Mother {
    public static void main(String[] args) {
        XiaoMing xiaoMing = new XiaoMing();
        xiaoMing.read();
        xiaoMing.play();
    }
}

假设有一天,小明的iphone坏了,他不得不换一个iphoneX的手机来做自己的事情。又或者今天小明用的iphone6学习和玩游戏,明天Mother让他用iphonex来学习和玩游戏。对于小明来说就很比较头痛,每次Mother的变化都要让小明内部消化改造一下。有没有其他的方式来让小明可以专心做自己的事情,每次Mother在下达命令的时候,不需要自己去改造,直接执行就好了。于是他思考了良久,终于找到了解决办法。

问题的原因在于小明的行动是深度依赖手机的,在采取行动的时候就需要new一个手机出来,如果mother在命令小明的时候告诉自己应该用什么手机,自己是不是就可以不用麻烦的改造自己的逻辑了呢?于是变成了这样。困扰小明的问题终于解决了。

/**
 * @description: 人相关类
 */
public class Person {

    private String sex; // 性别

    private String name; // 名字
}


public class XiaoMing extends Person {

    private Iphone iphone;
    public XiaoMing(Iphone iphone) {
        this.iphone = iphone;
    }

    public void read() {
        // additional action
        this.iphone.read();
    }

    public void play() {
        // additional action
        this.iphone.play();
    }
}


/**
 * @description: 手机相关类
 */
public class Iphone {

    public void read() {
        System.out.println("打开了知乎开始读书故事");
    }

    public void play() {
        System.out.println("打开了lol开始玩游戏");
    }
}
public class Iphone6 extends Iphone {

    @Override
    public void read() {
        System.out.println("用iphone6打开了知乎开始读书故事");
    }

    @Override
    public void play() {
        System.out.println("用iphone6打开了lol开始玩游戏");
    }
}
public class IphoneX extends Iphone {

    @Override
    public void read() {
        System.out.println("用IphoneX打开了知乎开始读书故事");
    }

    @Override
    public void play() {
        System.out.println("用IphoneX打开了lol开始玩游戏");
    }
}

/**
 * @description: 小明起床开始看书,然后玩手机。
 */
public class Mother {
    public static void main(String[] args) {
        XiaoMing xiaoMing = new XiaoMing();
        xiaoMing.read();
        xiaoMing.play();

        XiaoMing tomorrowXiaoMing = new XiaoMing(new IphoneX());
        tomorrowXiaoMing.read();
        tomorrowXiaoMing.play();
    }
}

其实这就是依赖注入的思想。类A依赖类B,但A不控制B的创建和销毁,仅使用类B,那么B的控制权交给调用A的类,比如类C来处理,这就是控制反转(IOC),而a要依赖b,有三种方式实现

  • 通过a的接口,把b传入;
  • 通过a的构造,把b传入;
  • 通过设置a的属性,把b传入;

把被依赖的资源或者工厂通过构造器(或者前面说到的静态工厂或者构建器)的方式是比较常见的使用方式,也是大多数框架会采用的方式。

2.5 避免创建不必要的对象

背景:对于一些不可变的对象,或者一些需要缓存的大对象(比如链接池对象)等,我们需要做一些干脆不创建对象或者缓存对象避免一些无用的开销。这里根据书中的建议列出几点在开发中应该注意的问题

  • 不要用new String("")去创建对象

因为String类和属性本身是用final修饰的,除了保证线程安全外,JVM是编译的时候对字符串有缓存的,所以相同的字符串是可以重用的。

  • 不要用new Boolean("")去创建对象

这种创建对象的方式也是不必要的,可以直接用Boolean.True。或者Boolean.valueOf("True")来给变量赋值。在jdk9中new Boolean()方法已经是被废弃了的。

  • String.matches方法不适合用在注重性能的场景下重复使用。

这个方法在内部创建了一个Pattern实例。用完之后这个对象就要被垃圾回收。在创建Pattern实例的时候需要将正则表达式编辑成一个有限状体机,导致创建Pattern实例的成本比较高。所以在重复比较字符串的场景下,应该把正则表达式显示的编译成一个不可变的Pattern实例。如果被频繁的调用进行比较,就会有较为明显的性能优势。

  • 优先使用基本类型代替装修类型,当心无意识的装箱问题

当心无意识的装箱问题尤其要注意在循环里或者循环计算里的自动装箱场景。这里也本地实践了书中的例子,性能差距还是比较明显的。

    private static long sum() {
        long begin = System.currentTimeMillis();
        long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++)
            sum += i;
        long end = System.currentTimeMillis();
        long total = end - begin;
        System.out.println("cast ms :" + total);
        return sum;
    }

    private static long sum() {
        long begin = System.currentTimeMillis();
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++)
            sum += i;
        long end = System.currentTimeMillis();
        long total = end - begin;
        System.out.println("cast ms :" + total);
        return sum;
    }

Effective Java 第3版 创建和销毁对象分享_第2张图片

Effective Java 第3版 创建和销毁对象分享_第3张图片

2.6 消除过期的对象引用

我们知道JAVA是由JVM虚拟机帮助我们管理内存的,当一个对象不存在引用他的句柄的时候,这个对象就会被销毁,内存会被释放。但是有的时候我们也需要考虑内存管理,比如书中介绍的一个例子:

import java.util.Arrays;
import java.util.EmptyStackException;

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

这个自定义实现栈的方法用起来是没有问题的,不过却存在内存泄漏的风险。问题出现在pop方法上,当我们弹出一个元素时,我们知道这个对象以后大概率是不需要再用的。这时候对象是可以被回收的。不过编译器却不知道我们是否需要释放这块内存,当类似的用法多的时候就会有危险了。

所以我们需要稍微改造下pop方法的逻辑,当我们弹出一个元素之后,主动消除对对象的引用。就可以把这种潜在的风险解除了。

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

需要注意的是,清空对象引用应该是一种例外,而不是一种规范。当我们在紧凑的作用域定义对象,那么这个对象用完就会自然被释放掉。如果需要我们自己管理内存,比如上面的自定义栈场景。我们就要警惕内存泄漏的问题。内存泄漏的另一个常见来源在于缓存,比如把对象的引用放在缓存中,就容易被忽略掉。如果我们的缓存项的生命周期由缓存项的键的引用决定,而不是缓存项的值决定的。这时候我们可以用weakHashMap来做缓存的结构。

假设一种应用场景:我们需要保存一批大的图片对象,其中values是图片的内容,key是图片的名字,这里我们需要选择一种合适的容器保存这些对象。使用普通的HashMap并不是好的选择,这些大对象将会占用很多内存,并且还不会被GC回收,除非我们在对应的key废弃之前主动remove掉这些元素。WeakHashMap非常适合使用在这种场景下,下面是具体的实现:

WeakHashMap map = new WeakHashMap<>();
BigImage bigImage = new BigImage("image_id");
UniqueImageName imageName = new UniqueImageName("name_of_big_image"); //强引用

map.put(imageName, bigImage);
assertTrue(map.containsKey(imageName));

imageName = null; //map中的values对象成为弱引用对象
System.gc(); //主动触发一次GC

await().atMost(10, TimeUnit.SECONDS).until(map::isEmpty);

首先,创建一个WeakHashMap对象来存储BigImage实例,对应的key是UniqueImageName对象,保存到WeakHashMap里的时候,key是一个弱引用类型。然后,我们将imageName设置为null,这样就没有其他强引用指向bigImage对象,按照WeakHashMap的规则,在下一次GC周期中会回收bigImage对象。通过System.gc()主动触发一次GC过程,然后可以发现WeakHashMap成为空的了。

内存泄漏的第三个常见来源是侦听器和其他回调。 如果你实现了一个 API,其中客户端注册回调,但不显式取消它们,除非你采取一些行动,否则它们将累积。确保回调被及时地垃圾收集的一种方法是仅存储对它们的弱引用,例如,将它们作为键存储在 WeakHashMap 中。

简单说下对文中说的例子的理解:造成内存泄漏的源自于变量的游离,如果我们在注册了回调函数之后句柄变量定义之后,句柄放在某个数组或者某个HashMap中,然后我们实际上已经不需要这个句柄,不过因为数组和HashMap是强引用,甚至是全局的静态变量。就会导致回调函数的资源(主要包括参数变量占用的内存资源不能被释放。),从而导致内存泄漏。

内存泄漏通常不会表现为明显的故障,它们可能会在系统中存在多年。它们通常只能通过仔细的代码检查或借助一种称为堆分析器的调试工具来发现。因此,学会在这样的问题发生之前预测并防止它们发生是比较重要的。

2.7 避免使用终结方法(finalizer)和清除(cleaner)方法

书中提示我们避免使用终结器和清除器

终结器是不可预测的,通常是危险的,也是不必要的。 它们的使用可能导致不稳定的行为、糟糕的性能和可移植性问题。应该避免使用它们。Java 9 替代终结器的是清除器。清除器的危险比终结器小,但仍然不可预测、缓慢,而且通常是不必要的。

原因:

  • 终结器和清除器的一个缺点是不能保证它们会被立即执行 。当对象变得不可访问,终结器或清除器对它进行操作的时间是不确定的。这意味着永远不应该在终结器或清除器中执行任何对时间要求很严格的操作。例如,依赖终结器或清除器关闭文件就是一个严重错误,因为打开的文件描述符是有限的资源。如果由于系统在运行终结器或清除器的延迟导致许多文件处于打开状态,程序可能会运行失败,因为它不能再打开其他文件。
  •  错误的使用终结器可能会导致OutOfMemoryError 错误。如果有很多较大的对象再用完之后用终结方法释放,然然而终结器线程运行的优先级低于另一个应用程序线程,因此对象不能以适合终结器的速度完成。语言规范没有保证哪个线程将执行终结器,应该避免使用终结器。在这方面,清洁器比终结器要好一些,可以自己控制是否清理线程,不过清洁器仍然在后台运行,在垃圾收集器的控制下运行,所以不能保证及时清理。
  • 永远不应该用终结方法或者清除方法来更新重要的持久状态,比如释放关键的数据库锁,因为终结方法和清除方法不保证执行的时机甚至可能因为程序退出不能执行方法一直不能释放锁导致其他机器执行有问题。
  • 使用终结方法或清除算法有创建销毁对象的损失,因为使用这两种方法会阻止有效的垃圾回收。

2.8 使用 try-with-resources 优于 try-finally

根据经验: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();
    }
}

这可能看起来也还ok,但在添加第二个资源时,情况会变得更糟。

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

 使用 try-finally 语句关闭资源的正确代码(如前两个代码示例所示)也有一个细微的缺陷。try 块和 finally 块中的代码都能够抛出异常。例如,在 firstLineOfFile 方法中,由于底层物理设备发生故障,对 readLine 的调用可能会抛出异常,而关闭的调用也可能出于同样的原因而失败。在这种情况下,第二个异常将完全覆盖第一个异常。异常堆栈跟踪中没有第一个异常的记录,这可能会使实际系统中的调试变得非常复杂(而这可能是希望出现的第一个异常,以便诊断问题)。虽然可以通过编写代码来抑制第二个异常而支持第一个异常,但实际上没有人这样做,因为它太过冗长。

try-with-resources 为开发者提供了更好的排查问题的方式。考虑 firstLineOfFile 方法。如果异常是由 readLine 调用和不可见的 close 抛出的,则后一个异常将被抑制,以支持前一个异常。实际上,还可能会抑制多个异常,以保留实际希望看到的异常。这些被抑制的异常不会仅仅被抛弃;可以通过编程方式使用 getSuppressed 方法(Java7中添加到Throwable中的)访问到被抑制的异常。

改造后的代码:

使用单个资源后释放资源

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

使用多个资源后释放资源

// 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-resource时候需要注意,需要对应的资源类实现 AutoCloseable或者Closeable接口。Closeable继承自AutoCloseable。close方法的签名变成了更具体的IOException。

当使用try-with-resource的时候JVM可以帮助我们自动调用close 方法进行资源释放;当没有抛出异常正常退出try代码块时也会自动调用close方法。像数据库链接类Connection,io类 InputStream 或 OutputStream 都直接或者间接实现了该接口。用这类资源时不再需要我们手动在finally那里手动执行close方法,也可以解决多个资源释放(释放与创建资源的顺序相反)的问题和异常的处理问题。

你可能感兴趣的:(Java,Effective,Java,java,开发语言)