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】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!