这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战
前言
一般而言,最好能重用一个对象,而不是在每次需要时就创建一个功能相同的新对象。重用方式既快速,也更流行。如果一个对象是不可变的(比如String),那它总是可以被重用。
从String开始
我们平时几乎没有过新建String对象的操作,今天我们就来看看
String patt =new String("^[a-zA-Z0-9]{0,29}$"); // 千万不要这么做!
复制代码
如图是在Idea中写完之后的样子,可以看到new String
这里是灰色的,也就代表着
该语句在每次执行时都创建一个新的 String
实例,但是这些对象的创建都是不必要的,因为String是不可变得!
改进后的版本如下:
String pattern="^[a-zA-Z0-9]{0,29}$";
复制代码
这个版本只使用了一个 String
实例,而不是在每次执行时都创建一个新的实例。对于所有在同一虚拟机中运行的代码,只要它们包含相同的字符串字面量 ,该对象就会被重用。
这里就涉及了JVM的字符串常量池了。
String w1 = "word";
String w2 = "word";
String b = new String("word");
复制代码
首先我们创建了三个String,因为变量池的存在,所以w1
和w2
是同一个对象引用!
System.out.println(w1 == w2); // true
复制代码
System.out.println(w1 == b); // false
复制代码
而b
变量则是因为它使用的构造器创建的,所以它和w1
不是同一个引用!
b = b.intern(); // checks if pool contains this string, if not puts this string in pool,
// then returns reference of string from pool and stores it in `b` variable
复制代码
用变量池中的变量替换这个变量。
System.out.println(w1 == b); // true -> now b holds same reference as w1
复制代码
解决
使用静态工厂方法
通常可以使用静态工厂方法而不是构造器来避免创建不必要的对象。构造器在每次被调用时都会创建一个新的对象,而工厂方法则可以根据你的代码进行动态调整是否重用不可变或者可变对象。
重用已知不会被修改的可变对象
某些对象的创建要比其他对象花费的代价高得多。如果反复需要这样一个“创建代价高昂的对象”(expensive object),那最好将其缓存以供重用。不幸的是,在创建这样的对象时,并不总是显而易见的。
警惕无意识的自动装箱
自动装箱使得我们可以将基本类型和基本类型的包装类型混用,按需进行自动装箱和拆箱。自动装箱机制使得基本类型和包装类型之间的差别变得模糊起来,但并没有完全消除。它们在语义上还有微妙的差异,在性能上也有比较明显的差别。考虑下面的方法,它要计算所有正整数的和。为了做到这一点,程序必须使用 long
进行运算,因为 int
没有大到足以容纳所有 int
正值的总和:
这段程序得到了正确的答案,但由于一个字符的排版错误,它比它应该达到的运算速度要慢得多。变量 sum
被声明为 Long
而不是 long
,这意味着程序构造了大约 个不必要的 Long
实例(大约在每次将 long i
添加到 Long sum
中时构造一个)。将 sum
的声明从 Long
改为 long
会将运行时间大大缩短。要优先使用基本类型而不是基本类型的包装类型,当心无意识的自动装箱。
总结
本文的关键是掌握好创建和重用之间的度, 现代的 JVM 实现上小对象的构造器只做少量的显式工作,因而小对象的创建和回收成本是非常廉价的。通过创建附加的对象来提升程序的清晰度、简洁性或功能,这通常是件好事。
自己维护对象池还是不太建议,风险太大,除非池中的对象是非常重量级的,比如数据库连接。建立连接的成本非常高,因此重用这些对象是有意义的。