创建和销毁对象

1. 用静态工厂代替构造器

总结:

优点主要源于静态工厂的灵活性,它有名称,可以返回灵活的类型,包括子类,隐藏类,可拓展类。为了可读性,最佳实践是遵守命名约定from,of,valueOf,instance,getInstance,crate,newInstance,getType,newType

优点:

  • 有名称。构造器的缺点,参数列表多,有顺序,容易错误使用
  • 不必创建新对象。例如享元模式,常量池设计等。
  • 可以返回隐藏类型。参考Collections的实现(Collection的伴生类,由于JDK 1.8接口不支持静态方法,所以一般会实现伴生类封装相关的静态方法),其中实现了多种不同的集合,但这些集合对外隐藏,不必提供大量的独立的类,让API变得更复杂。
  • 可以返回不同类型对象。根据传入参数的不同条件,返回不同类型的对象。
  • 返回的类可拓展。例如JDBC,返回的类可以拓展多种数据库实现。

缺点:

  • 不可子类化。鼓励复合而不是继承。
  • 难以发现。相比于构造器,静态工厂方法的命名更加多样,这可以通过约定解决,常见的命名如from,of,valueOf,instance,getInstance,crate,newInstance,getType,newType

2. 多个参数考虑使用构造器

总结:

适合大量可选参数场景,流式API可读性好,方便的有效性检查,方便的初始值设置。但builder类编写起来比较麻烦。常规的实现如下:

// Builder Pattern
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) {
            // 进行参数校验
            Objects.requireNonNull(servingSize);
            Objects.requireNonNull(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;
    }
}

优点:

  • 对拓展大量可选参数的支持。静态工厂和构造器的不足正在于拓展大量可选参数的支持,例如通过重叠构造器的方法去解决,但只会使得代码难以编写、使用、阅读。
  • 保持不可变性。JavaBean的设计模式能支持可选参数拓展,但是它破坏了类的不可变性。这样一来,就必须考虑更多的线程安全问题。而且它还会引起不一致的状态——类无法通过校验构造器参数的有效性来确保一致性。
  • 有效性检查。可以在方法中,在build的时候检查参数的有效性。

不足:

  • 必须创建构造器,有一定的开销。
  • 构造器代码编写麻烦,尤其对于只有少量参数的情况下。

3. 使用枚举类强化单例属性

该实现方式和公有域很相似,但它更加简洁。枚举类带来的好处是,提供了序列化机制,保证即使面对序列化攻击和反射攻击都只序列化一次。

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

4. 通过私有构造器强化不可实例化

很多时候想一些工具类Utils,静态类我们不希望它被实例化,但Java会提供一个默认的无参构造函数(如果你没提供任何构造函数)。这种场景下的最佳实践就是通过私有化构造器防止其被实例化。

// Noninstantiable utility class
public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        // 防止内部错误调用,可以主动抛出异常
        throw new AssertionError();
    }
    ... // Remainder omitted
}

5. 优先考虑依赖注入的方式引用资源

这是一种非常常用的模式,以至于大家都不知道它就是依赖注入。它可以提供引用资源的灵活性

控制反转

另外值得一提的是,它通常和控制反转联系起来,依赖注入是实现控制反转的一种方式,即使是通过简单的构造器、set方法设置,但它将依赖这件事从程序本身(指该类,例如SpellChecker)交给了外部调用者。
依赖注入的不足是它会使得依赖变得复杂,会让大型项目变得混乱,但现代的框架都非常好地解决了这个问题,例如spring。依赖注入和控制反转,将原本依赖设置从程序转交容器(使用者),使代码对修改封闭,又可以简单支持拓展其他依赖。

// Dependency injection provides flexibility and testability
public class SpellChecker {
    private final Lexicon dictionary;
    public SpellChecker(Lexicon dictionary) {
    	this.dictionary = Objects.requireNonNull(dictionary);
    }
}

6. 避免创建不必要的对象

不必要的创建对象

在以下两种情况下,不应该创建不必要的对象:

  • 对象为不可变对象,可重用
  • 对象创建成本很高

编译器通常会为我们做很多事,但是很多时候又会引起没必要的对象创建,例如:

// 1. ""本身就会创建对象,而且用引号的话,有重复字符串会直接复用常量池中的内容
String s = new String("bikini"); // DON'T DO THIS!

// 2. 正则匹配看起来没问题,但是每次调用都会新建一个pattern,应该对pattern编译,进行复用
// 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})$");
}

// 3. 自动装箱和拆箱,常见的问题
// 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;
}

优先使用静态工厂

静态工厂方法可以灵活地实现这种对象的重用,例如valueOf,所以永远优先调用静态工厂方法。

什么时候使用连接池

另一方面,需要注意,对于现代编译器而言,创建对象的开销实际上非常小,所以大多数情况下,自己用线程池管理内存是没有必要的,但一种情况除外——对象创建开销极大。典型的例子如JDBC的连接池,线程池等等。

7. 消除过期的对象引用——避免内存泄漏

先来看一个栈的实现,看起来一点问题都没有。

// 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之后,把pop掉的元素(即size - 1)置为null。

如果你需要自己管理内存

Java的内存泄漏问题看起来非常棘手,隐蔽,但作者又指出,每次都刻意去清空对象是没有必要的。
清空对象是一种例外,不是一种规范。那么什么情况下需要去注意内存泄漏的问题呢?——答案是,当类自己管理内存的时候。
比如上述的stack自己用数组作为底层容器。
比如threadLocal中的map,类中存储数据的各种map容易出现这种问题。
比如监听、观察者模式,注册监听之后没有显示取消。

利器:弱引用

ThreadLocal就是使用弱引用,又比如JDK中就有这种容器,例如WeakHashMap。他们的核心是:引用对象的生命周期由外部引用决定,而不是值引用决定。弱引用决定了当一个对象只剩下弱引用的时候,一旦它被gc扫描到它就会被回收。

8. 避免使用终结方法

finalizer是在gc回收前会执行,由于gc的时间是不确定的,该方法的调用时机就是无法预期的。可想而知,如果用它来回收资源,那可能会造成严重的资源消耗。
终结方法的合理使用场景有两个:

  • 作为回收资源的一种兜底,有总比没有好,慢一点回收总比不回收好
  • 回收本地对象。由于本地对象,JVM的gc无法检测到,所以可以在终结方法回收类相关的本地对象。

此外,终结方法存在一个隐蔽的安全隐患:终结方法攻击,大致是本来创建失败的对象(例如构造器中创建失败抛出异常),它不应该存在,但恶意子类可以实现终结方法,让该对象引用记录在静态域中,使得它回收不了。解决方式,避免方法被重写的典型方法是,实现一个final的空终结方法。

9. try-with-resources优先于try-finally

来看try-finally的方式:

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

该实现的问题在于:

  • 需要手动close,容易忘记
  • 代码丑
  • 容易吞异常,第一个out.close引发异常会被吞掉

最佳实践是try-with-resources,它要求相关资源类需要实现AutoCloseable方法,它解决了上述所有问题。

// 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);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值