通常,我們更喜歡重用一個對象而不是重新創(chuàng)建一個。如果對象是不可變的,它就始終可以被重用。
下面是一個反面例子:
String s = new String("stringette");該語句每次執(zhí)行時都創(chuàng)建一個新的實例。String構造器中的參數(shù)"stringette"本身是一個實例,功能方面等同于那些通過構造器創(chuàng)建的對象。如果這種語句放到循環(huán)里,效果會變得更糟。
于是我們只需要:
String s = "stringette";
這樣就永遠是同一個string實例,并且可以保證同一個VM中會重用相同字符串字面量的對象。(the object will be reused by any other code running in the same vitual machine that happens to contain the same string literal.)
如果類中同時提供了靜態(tài)工廠方法和構造器,客戶端通常可以使用靜態(tài)工廠方法來防止創(chuàng)建不必要的對象。比如java.lang.Boolean:
public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE);}而在構造器的javadoc中也做了相應的說明:
/** * Allocates a {@code Boolean} object rePResenting the * {@code value} argument. * * <p><b>Note: It is rarely appropriate to use this constructor. * Unless a <i>new</i> instance is required, the static factory * {@link #valueOf(boolean)} is generally a better choice. It is * likely to yield significantly better space and time performance.</b> * * @param value the value of the {@code Boolean}. */ public Boolean(boolean value) { this.value = value; }除了重用不可變的對象,我們也可以重用那些不會再有變化的可變對象。(Also use mutable object if you know they won't be modified.)
下面是一個反面例子,檢查某人是否出生于生育高峰期(1946~1964):
import java.util.Calendar;import java.util.Date;import java.util.TimeZone; public class Person { private final Date birthDate; public Person(Date birthDate) { // Defensive copy - see Item 39 this.birthDate = new Date(birthDate.getTime()); } // Other fields, methods omitted // DON'T DO THIS! public boolean isBabyBoomer() { // Unnecessary allocation of expensive object Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); Date boomStart = gmtCal.getTime(); gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); Date boomEnd = gmtCal.getTime(); return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0; }}一次Calendar.getInstance和兩次getTime,這些都會創(chuàng)建新的實例:
public static Calendar getInstance(TimeZone zone,Locale aLocale){ return createCalendar(zone, aLocale); } private static Calendar createCalendar(TimeZone zone,Locale aLocale){ Calendar cal = null; String caltype = aLocale.getUnicodeLocaleType("ca"); if (caltype == null) { // Calendar type is not specified. // If the specified locale is a Thai locale, // returns a BuddhistCalendar instance. if ("th".equals(aLocale.getLanguage()) && ("TH".equals(aLocale.getCountry()))) { cal = new BuddhistCalendar(zone, aLocale); } else { cal = new GregorianCalendar(zone, aLocale); } } else if (caltype.equals("japanese")) { cal = new JapaneseImperialCalendar(zone, aLocale); } else if (caltype.equals("buddhist")) { cal = new BuddhistCalendar(zone, aLocale); } else { // Unsupported calendar type. // Use Gregorian calendar as a fallback. cal = new GregorianCalendar(zone, aLocale); } return cal; } public final Date getTime() { return new Date(getTimeInMillis()); }于是我們做一下改進,將對象聲明為private static final,并在靜態(tài)初始化塊中把需要的實例都準備好:
import java.util.Calendar;import java.util.Date;import java.util.TimeZone; class Person { private final Date birthDate; public Person(Date birthDate) { // Defensive copy - see Item 39 this.birthDate = new Date(birthDate.getTime()); } // Other fields, methods /** * The starting and ending dates of the baby boom. */ private static final Date BOOM_START; private static final Date BOOM_END; static { Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); BOOM_START = gmtCal.getTime(); gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); BOOM_END = gmtCal.getTime(); } public boolean isBabyBoomer() { return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0; }}這樣不僅提高了效率,而且更加明確。
但是,我們還可以再改進一些。如果上面的例子中的isBabyBoomer沒有被調用過,那我們在靜態(tài)初始化塊中創(chuàng)建的實例就白白浪費了(可能例子中的對象太少了,多了就爽了)。對于這個問題,我們可以用延遲初始化(lazily initializing)來解決,也就是在isBabyBoomer被第一次調用時進行初始化。但是作者并不建議這樣做,因為這可能只會讓方法實現(xiàn)變得更復雜,而沒有顯著提高效率。
上面的例子中我們把重用的對象在實例化以后不會再有變化,那么能不能重用可變對象? 比如Map.keySet方法返回的是同一個Set實例,這個Set實例的狀態(tài)依賴于其backing Object,即Map實例。當Map發(fā)生變化時Set也跟著變化就可以了。雖然每次調用時重新創(chuàng)建一個Set實例也是可行的,但實在沒必要。
另外,autoboxing這個特性也為用戶提供了創(chuàng)建不必要的對象的新方法。試試基本類型和封裝類型混用,并讓他們超出封裝類型的緩存范圍。比如這樣做:
public class Sum { // Hideously slow program! Can you spot the object creation? public static void main(String[] args) { Long sum = 0L; for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i; } System.out.println(sum); }}只是把sum的類型聲明為Long,造成了巨大的浪費。(prefer primitives to boxed primitives,and watch out for unintentional autoboxing.)
新聞熱點
疑難解答