Effective Java读书笔记-3

避免创建不必要的对象

一般来说,最好重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable),它就始终可以被重用。
下面是一个极端的反面例子:

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

该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数(“bikini”)本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的String实例。
下面是改进后的版本:

String s="bikini";

这个版本只用了一个string实例,而不是每次执行的时候都创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串常量,该对象就会被重用。

对于同时提供了静态工厂方法(static factory method)和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。 例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)(注意构造器 Boolean(String)在Java9中已经被废弃了)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不不会这样做。 除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。

有些对象创建的成本比其他对象要高得多。如果重复地需要这类"昂贵的对象",建议将它缓存下来重用。 假设想要编写一个方法,用它确定一个字符串是否为一个有效的罗马数字。下面介绍一种最容易的方法,使用一个正则表达式:

// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
        return s.matches("A(?=.)M*(C[MD]|D?C{0,3})"
        + "CX[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

这个实现的问题在于它依赖String.matches方法。虽然String.matches方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用。 问题在于,它在内部为正则表达式创建了一个Pattern实例,却只用了一次,之后就可以进行垃圾回收了。创建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]IL?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

改进后的isRomanNumeral方法如果被频繁地调用,会显示出明显的性能优势。除了提高性能之外,代码也更清晰了。将不可见的Pattern 实例做成final静态域时,可以给它起个名字,这样会比正则表达式本身更有可读性。

如果改进后的isRomanNumeral方法的类被初始化了,但是该方法没有被调用,那就没必要初始化ROMAN域。通过在isRomanNumeral方法第一次被调用的时候延迟初始化(lazily initializing)这个域,有可能消除这个不必要的初始化工作,但是不建议这样做。这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平。

另一种创建多余对象的方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。 下面的程序,用来计算所有int正整数值的总和。为此,程序必须使用long算法,因为int不够大,无法容纳所有int正整数值的总和:

// 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。导致程序构需要构造大约21个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。因此:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

不要错误地认为“创建对象的代价非常昂贵,应该要尽可能地避免创建对象" , 相反,由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。

**反之,通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。

消除过期的对象引用

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

这段程序中并没有很明显的错误。但是这个程序中隐藏着一个问题,不严格地讲,这段程序有一个“内存泄漏”,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄漏会导致磁盘交换(Disk Paging),甚至导致程序失败(OutOfMemoryError错误)。

上述示例程序中存在的内存泄漏:如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为栈内部维护着对这些对象的过期引用(obsolete reference)。过期引用,指的是永远也不会再被解除的引用。 在本例中,凡是在 elements数组的"活动部分"(active portion)之外的任何引用都是过期的。活动部分是指 elements中下标小于size 的那些元素。

在支持垃圾回收的语言中,内存泄漏非常隐蔽。如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。 即使只有少量的几个对象引用被无意识地保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。

这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。对于上述例子中的Stack类而言,只要一个单元被弹出栈,指向它的引用就过期了。pop方法的修订版本如下所示:

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

清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。另外,对于每一个对象引用,一旦程序不再用到它,就把它清空。其实这样做既没必要,也不是我们所期望的,因为这样做会把程序代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。
一般来说,只要类是自己管理内存,就应该警惕内存泄漏问题。

内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。

内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,如果能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,是最好的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值