《Effective Java》阅读笔记5 避免创建不必要的对象

301 阅读6分钟

1.序

尽量少的创建对象,如果单个对象能够满足要求,就使用单例模式,反复重用唯一的对象。对一些创建成本低的对象来说,这样做带来的好处也许并不明显。但对于一些创建成本高的对象来说,这样做可以明显地节约系统资源、提升系统性能。有以下几种方法:

  • 1 ==采用更合适的API或工具类减少对象的创建==
  • 2 ==重用相同功能的对象==
  • 3 ==小心自动装箱(auto boxing)==
  • 4 ==用静态工厂方法而不是构造器==

2. 采用更合适的API或工具类减少对象的创建

可能导致滥用对象的一个典型例子就是 ==字符串== 。在学习Java基础的过程中,一定会提到String类的对象一旦被创建,它的值就是不能改变的。通过查看JDK中String类的源码,我们可以看到String类是通过一个 ==byte数组== 来存储字符串的,而且这个数组被修饰为final常量。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {​
    /**     
    * The value is used for character storage.    
    *  ...    
    */
    @Stable
    private final byte[] value;
        
    ...}
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {​
    /**     
    * The value is used for character storage.     
    * ...     
    */
                   
    /*     
    * JDK9之前,String类是用一个char数组来存储字符串的。如果你觉得上面用byte数组来存储字符串不好理解           
    * 的话,也可以简单地理解为String类仍是用一个char数组来存储字符串     
    */
    private final char value[];
        
   }

所以如果我们用以下方式创建字符串对象的话,会产生不必要的对象。

String str = new String("aaa");

因为当我们往构造方法里传入aaa的时候,其实这个aaa就是一个String实例了。我们等于是创建了两个String实例。 我们应该直接这么写:

String str = "aaa";

根据jdk文档,上述方式实际上等同于:

char data[] = {'a', 'a', 'a'};
String str = new String(data);

传入一个字符数组来创建String,避免了创建重复对象。

再举一个常见的例子,我们有时希望遍历一个list,将其中的元素存到一个字符串里,并用逗号分隔。我们可能会用下面这种最low的办法:

public static String listToString(List<String> list) {
    String str = "";
    for (int i = 0; i < list.size(); i++) {
        str += list.get(i);
        if (i < list.size() - 1) {
            str += ",";
        }
    }
    return str;
}

这样其实在每次+=的时候都会重新创建String对象,极大地影响了性能。 我们可以修改一下,采用StringBuilder的方式来拼接list:

public static String listToString(List<String> list) {
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < list.size(); i++) {
        stringBuilder.append(list.get(i));
        if (i < list.size() - 1) {
            stringBuilder.append(",");
        }
    }
    return stringBuilder.toString();
}

这种方式每次只会生成两个实例——StringBuilder和最后返回的String。 那有没有更好的方法呢?我们可以采用Google Guava的Joiner,这样每次只用生成一个实例,如下所示:

public static String listToString(List<String> list) {
    return Joiner.on(",).join(list);
}

3.重用相同功能的对象

有时候我们提供的API中有一些每次调用都具备相同功能的对象,那么就可以把这些对象变成静态的不可变对象,只需实例化一次即可。比如下面这个类似书中的例子:

public static boolean isNumeral(String s) {
    return s.matches("^[0-9]*$");
}

这个例子用String.matches()方法来判断字符串是否为数字。每次调用matches()方法,里面==都==会创建一个Pattern对象,这会对性能造成影响。 因为每次调用isNumeral实际上都会生成一个功能完全相同的Pattern对象,所以我们可以把它抽出来,变成一个 ==静态不可变对象== ,如下所示:

public static final Pattern NUMBER = Pattern.compile("^[0-9]*$");

public static boolean isNumeral(String s) {
    return NUMBER.matcher(s).matches();
}

上面我们谈到了一个不可变对象的重用,接下来我们再看看可变对象的重用。可变对象的重用可以通过视图(views)来实现。比如,Map的keySet()方法就会返回Map对象所有key的Set视图。这个视图是可变的,但是当Map对象不变时,在任何地方返回的任何一个keySet都是一样的,当Map对象改变时,所有的keySet也会相应的发生改变。

package com.czgo.effective;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class TestKeySet {

    public static void main(String[] args) {

        Map<String,Object> map = new HashMap<String,Object>();
        map.put("A", "A");
        map.put("B", "B");
        map.put("C", "C");

        Set<String> set = map.keySet();
        Iterator<String> it = set.iterator();
        while(it.hasNext()){
            System.out.println(it.next()+"①");
        }

        System.out.println("---------------");

        map.put("D", "D");
        set = map.keySet();
        it = set.iterator();
        while(it.hasNext()){
            System.out.println(it.next()+"②");
        }

    }

}

4.小心自动装箱(auto boxing)

自动装箱允许程序员混用基本类型和包装类型,在两者相计算时,程序会构造出基本类型的包装类型实例。例如,我们看这个例子:

public class Main {
    public static void main(String[] args) {
        final long startTime = System.currentTimeMillis();​
        Long sum = 0L; // 将sum声明为Long类型        
        for (long i = 0; i <= Integer.MAX_VALUE; i++) {
            sum += i;
        }​
        final long endTime = System.currentTimeMillis();​
        System.out.println("程序执行了:" + (endTime - startTime) + "ms");
    }
}

程序执行了:7298ms 将sum声明为Long类型时,程序大约会构造 个多余的 Long实例。 如果将sum声明为long类型,程序的执行时间会大大地缩短。

public class Main {
    public static void main(String[] args) {
        final long startTime = System.currentTimeMillis();​
        long sum = 0L; // 将sum声明为long类型        for (long i = 0; i <= Integer.MAX_VALUE; i++) {
            sum += i;
        }​
        final long endTime = System.currentTimeMillis();​
        System.out.println("程序执行了:" + (endTime - startTime) + "ms");
    }}

程序执行了:1308ms

由此,我们可以得出结论:

  • ==优先使用基本数据类型==
  • ==避免不必要的自动装箱==

所以我们在日常开发中,方法内尽量用基本类型,只在入出参的地方用包装类型。多留心,切忌无意识地使用到自动装箱。

5.用静态工厂方法而不是构造器

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用==静态工厂方法==而不是构造器,以避免创建不必要的对象。

例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。 扩展思路:

package com.czgo.effective;

/**
 * 用valueOf()静态工厂方法代替构造器
 * @author AlanLee
 * @version 2016/12/01
 *
 */
public class Test {

    public static void main(String[] args) {
        // 使用带参构造器
        Integer a1 = new Integer("1");
        Integer a2 = new Integer("1");

        //使用valueOf()静态工厂方法
        Integer a3 = Integer.valueOf("1");
        Integer a4 = Integer.valueOf("1");

        //结果为false,因为创建了不同的对象
        System.out.println(a1 == a2);

        //结果为true,因为不会新建对象
        System.out.println(a3 == a4);
    }

}

可见,使用静态工厂方法valueOf不会新建一个对象,避免大量不必要的对象被创建,实际上很多类默认的valueOf方法都不会返回一个新的实例,比如原文提到的Boolean类型,不仅仅是Java提供的这些类型,我们在平时的开发中如果也有类似的需求不妨模仿Java给我们提供的静态工厂方法,给我们自己的类也定义这样的静态工厂方法来实现对象的获取,避免对象的重复创建,但是也不要过度迷信使用静态工厂方法的方式,这种方式也有它的弊端(有关静态工厂方法的知识可以看看《Effective Java》第一条),个人很少使用这种方式,平时的类多创建个对象也不会有太大的影响,只要稍微注意下用法就ok了。

6.补

如果涉及到对象池的应用,除非池中的对象非常重,类似数据库连接,否则最好不要去自己维护一个对象池,因为这样会很复杂。另外,有时考虑到系统的安全性,那么我们需要进行防御性复制,这个在后面会讲到。此时,重复创建对象就是有意义的,因为比起隐含错误和安全漏洞,重复创建对象带来的性能损失是可以接受的。

7.参考文献

《Effective Java(第3版)》 www.cnblogs.com/AlanLee/p/6… zhuanlan.zhihu.com/p/114881099

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

7.你们要的免费书来了