Effective Java——建立和銷毀對象
《Effective Java》這本書展現了絕大多數下的最佳程式設計的實踐,它關心的是如何寫出清晰、正确、健壯、靈活和可維護的程式來。這本書包含的内容非常豐富,這本書我就不多介紹了,隻能默默的說一句,作為一名java開發錯過了這本書難免會成為一個小遺憾,是以還是建議有時間的小夥伴能夠去看看這本書,時間擠擠總還是有的。
1、考慮用靜态工廠方法代替構造器
要擷取一個執行個體,通常會提供一個公有的構造器,但還有另外一種方法——提供一個公有的靜态工廠方法。 注意,這裡的靜态工廠方法與設計模式裡的工廠方法模式不是一個概念:
靜态工廠方法通常指的是某個類裡的靜态方法,通過調用該靜态方法可以得到屬于該類的一個執行個體;
工廠方法模式是一種設計模式,指的是讓具體的工廠對象負責生産具體的産品對象,這裡涉及多種工廠(類),多種對象(類),如記憶體工廠生産記憶體對象,CPU工廠生産CPU對象;
最大差別是:簡單工廠模式裡的靜态工廠方法會建立各種不同的對象(不同類的執行個體),而靜态工廠方法一般隻建立屬于該類的一個執行個體(包括子類)
1.1 例子
假設我們需要寫一個一定範圍内産生随機數的類RandomIntGenerator,該類有兩個成員屬性:最小值min和最大值max,
假設我們的需求是需要建立三種類型的RandomIntGenerator對象,
1、大于min,小于max;
2、大于min 小于Integer.MAX_VALUE;
3、大于Integer.MIN_VALUE 小于max
(1)使用構造器 以下代碼不僅可讀性差,不看注釋很難知道其建立的對象的具體含義,而且在設計最後一個構造方法的時候,還報錯
public class RandomIntGenerator {
private int min = Integer.MIN_VALUE;
private int max = Integer.MAX_VALUE;
/**
* 大于min 小于max
*/
public RandomIntGenerator(int min, int max){
this.min = min;
this.max = max;
}
/**
* 大于min 小于Integer.MAX_VALUE
*/
public RandomIntGenerator(int min){
this.min = min;
}
/**
* 大于Integer.MIN_VALUE 小于max
* 報錯,構造器重複了
*/
public RandomIntGenerator(int max){
this.max = max;
}
}
(2)使用靜态工廠方法
public class RandomIntGenerator {
private int min = Integer.MIN_VALUE;
private int max = Integer.MAX_VALUE;
/**
* 大于min 小于max
*/
public RandomIntGenerator(int min, int max){
this.min = min;
this.max = max;
}
/**
* 大于min 小于max
*/
public static RandomIntGenerator between(int min, int max){
return new RandomIntGenerator(min, max);
}
/**
* 大于min 小于Integer.MAX_VALUE
*/
public static RandomIntGenerator biggerThan(int min){
return new RandomIntGenerator(min, Integer.MAX_VALUE);
}
/**
* 大于Integer.MIN_VALUE 小于max
*/
public static RandomIntGenerator smallerThan(int max){
return new RandomIntGenerator(Integer.MIN_VALUE, max);
}
}
1.2 jdk中的執行個體
JDK中的Boolean類的valueOf方法可以很好的印證這個優勢,在Boolean類中,有兩個事先建立好的Boolean對象(True,False)
public final class Boolean implements java.io.Serializable,Comparable<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;
}
}
1.3 靜态工廠方法比構造器的優劣勢
優勢:
- 有名稱,可讀性強
- 調用的時候,不需要每次都建立一個新對象
- 可以傳回原傳回類型的任何子類型對象
劣勢:
- 如果類不含public或protect的構造方法,将不能被繼承
- 與其它普通靜态方法沒有差別,沒有明确的辨別一個靜态方法用于執行個體化類
2、遇到多個構造器參數時要考慮用建構器
靜态工廠和構造器有個共同的局限性:不能很好地擴充到大量的可選參數。以下3種方法可以解決該問題,但應優先考慮建構器。
2.1 重疊構造器模式
我們初學的時候都會選擇 重疊構造器(telecoping constructor)模式 。在這種情況下,第一個構造器是執行個體化對象必須的參數,第二個會多一個參數,就這樣疊加,最後是一個有所有參數的構造器。
public class Person {
private final String name;
private final int age;
private final String address;
private final String phone;
public Person(String name, int age) {
this(name,age,null);
}
public Person(String name, int age, String address) {
this(name,age,address,null);
}
public Person(String name, int age, String address, String phone) {
super();
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
}
@Override
public String toString() {
return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone;
}
}
缺點:重疊構造器可行,但當有很多的參數的時候,用戶端的代碼就會很難編寫并且不容易閱讀我們在使用的時候,必須很仔細的看每一個參數的位置和含義。
2.2 JavaBeans模式
這種模式下,使用無參的構造方法建立對象,然後調用setter 方法給屬性設定值。
public class Person {
private String name;
private int age;
private String address;
private String phone;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setAddress(String address) {
this.address = address;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone;
}
}
缺點:
- 構造的過程分到了幾個調用中,在構造JavaBeans的時候可能會不一緻
- 類無法僅僅通過檢驗構造器參數的有效性來保證一緻性!
- 對象的不一緻會導緻失敗,JavaBeans模式阻止了把類做為不可變的可能,需要程式員做額外努力來保證它線程安全
2.3 建構器模式
public class Person {
private final String name;
private final int age;
private final String address;
private final String phone;
public static class Builder{
private final String name;
private final int age;
private String address = null;
private String phone = null;
public Builder(String name,int age){
this.name = name;
this.age = age;
}
public Builder address(String val){
address = val;
return this;
}
public Builder phone(String val){
phone = val;
return this;
}
public Person builder(){
return new Person(this);
}
}
private Person(Builder builder){
this.name = builder.name;
this.age = builder.age;
this.address = builder.address;
this.phone = builder.phone;
}
@Override
public String toString() {
return "name:"+name+" age:"+age+" address:"+address+" phone:"+phone;
}
}
public class test {
public static void main(String[] args) {
Person p = new Person.Builder("tom", 18).address("深圳").phone("110").builder();
System.out.println(p.toString());
}
}
如果類的構造器或者靜态工廠中具有多個參數,設計這種類時,Builder模式就是不錯的選擇,特别是當大多數參數都是可選的時候。
- 與重疊構造器相比,builder牧師的用戶端更易與閱讀和編寫
- 與JavaBeans相比,更加的安全
3、使用枚舉類型實作單例
Singleton指僅僅被執行個體化一次的類,當然也要考慮單例在多線程下的安全性,如餓漢式單例、雙檢鎖式單例、靜态内部類式的單例,但如果Singleton加上“implements Serializable”的字樣,它就不再是一個 Singleton。 一個類實作了 Serializable接口,我們就可以把它往記憶體地寫再從記憶體裡讀出而"組裝"成一個跟原來一模一樣的對象, 從記憶體讀出而組裝的對象破壞了單例的規則。單例是要求一個JVM中隻有一個類對象的,而現在通過反序列,一個新的對象克隆了出來。
3.1 readResolve方法
實作序列化的Singleton,為了維持并保證Singleton,必須聲明所有執行個體域都是瞬時(transient)的,并提供一個readResolve方法。這樣,當JVM從記憶體中反序列化地"組裝"一個新對象時,就會自動調用這個 readResolve方法來傳回我們指定好的對象了,單例規則也就得到了保證。 深複制工具類:
public class DeepCopy {
public static Object copy(Object obj){
Object o=null;
if(obj==null)
return o;
try{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.close();
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
o = ois.readObject();
ois.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return o;
}
}
public class Singleton implements Serializable{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if(instance == null)
instance = new Singleton();
}
}
return instance;
}
private Object readResolve() {
return instance;
}
public static void main(String[] args) throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
Singleton instance=Singleton.getInstance();
Singleton i1=(Singleton) DeepCopy.copy(instance); //通過反序列化生成新的執行個體
System.out.println(i1==instance); //true
Constructor<?> constructor =Singleton.class.getDeclaredConstructors()[0]; //通過反射機制生成新的執行個體
constructor.setAccessible(true);
Singleton i2=(Singleton)constructor.newInstance();
System.out.println(i2==instance); //false
}
}
可以看見,通過反射機制,設定AccessibleObject.setAccessible(true),改變構造器的通路屬性,調用構造器生成了新的執行個體。也就是說該方法不能防止反射攻擊。
3.2 枚舉類Singleton
通過枚舉實作Singleton更加簡潔,同時枚舉類型無償地提供了序列化機制,可以防止反序列化的時候多次執行個體化一個對象。枚舉類型也可以防止反射攻擊,當你試圖通過反射去執行個體化一個枚舉類型的時候會抛出IllegalArgumentException異常
public enum Singleton{
INSTANCE;
public void method(){
System.out.println("666");
}
public static void main(String[] args) {
Singleton instance=Singleton.INSTANCE;
instance.method();
Singleton otherInstance=(Singleton) DeepCopy.copy(instance);
System.out.println(otherInstance==instance); //true
}
}
public enum Singleton{
INSTANCE;
public static void main(String[] args) throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
Singleton instance=Singleton.INSTANCE;
Singleton otherInstance=(Singleton) DeepCopy.copy(instance); //通過反序列化生成新的執行個體
System.out.println(otherInstance==instance); //true
Constructor<?> constructor =Singleton.class.getDeclaredConstructors()[0]; //通過反射機制生成新的執行個體
constructor.setAccessible(true);
Singleton i2=(Singleton)constructor.newInstance();
System.out.println(i2==instance);
}
}
抛出異常:
最後,不管采取何種方案,請時刻牢記單例的四大要點:
- 線程安全
- 延遲加載
- 序列化與反序列化安全
- 反射攻擊安全
4、避免建立不必要的對象
(1)最好能重用對象而不是在每次需要的時候就建立一個相同功能的新對象,如果對象是不可變的,那它就始終可以被重用。 如:String s=new String("hjy") 和String s="hjy" 前者每次執行都會建立一個新的String執行個體,而“hjy”本身就是一個String執行個體,這種用法在一個頻繁調用的方法中,就會建立大量String執行個體;而後者隻建立了一個String執行個體,對象儲存在常量池中,可以被重用。
(2)對于同時提供了靜态工廠方法和構造器的不可變類,建議使用靜态工廠方法,以避免建立不必要的對象。例如,靜态工廠方法,Boolean.valueOf(String)幾乎總是優先于構造器Boolean(String)。構造器在每次被調用的時候都會建立一個新的對象,而靜态工廠方法則傳回已建立的對象。
public static void main(String[] args) {
Integer a=Integer.valueOf(2);
Integer b=Integer.valueOf(2);
System.out.println(a==b); //true
}
(3)有一種建立多餘對象的新方法,稱作自動裝箱,自動裝箱使得基本類型和引用類型之間的差别變得模糊起來。
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而不是long,意味着程式構造了大約2的31次方個多餘的Long執行個體。結論很明顯:要優先使用基本類型而不是引用類型,要當心無意識的自動裝箱。
5、消除過期的對象引用
記憶體溢出:程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory
記憶體洩漏:為某對象申請記憶體後,無用時沒有釋放已申請的記憶體空間,造成記憶體浪費 造成記憶體洩漏的常見來源主要有以下3種:
- 沒有及時清除過期的對象引用
隻要類是自己管理記憶體的,程式員就需要警惕記憶體洩漏的問題。一旦元素被釋放掉,則該元素中包含的任何對象引用都應該被清空。
//彈出棧的對象引用應該被清除 public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null //清空引用 return result; }
- 緩存:放在緩存的引用可停留很長時間,解決方法——隻要在緩存之外存在對某個項的鍵的引用,該項就有意義這樣的緩存的話,就可以使用WeakHashMap代表緩存,因為當緩存中的項過期的時候,它們就會自動被删除掉。
- 螢幕和其他回調:注冊的回調卻沒有顯示取消注冊,那麼就會積累。確定回調立即被當作垃圾的最佳方法是隻儲存它們的弱引用