在面向對象編程語言中,多態算是一種泛化機制。例如,你可以將方法的參數類型設置為基類,那么該方法就可以接受從這個基類中導出的任何類作為參數,這樣的方法將會更具有通用性。此外,如果將方法參數聲明為接口,將會更加靈活。
一、什么是泛型
在學習泛型之前,我們先看一個實例來真實的體驗一下泛型帶來的好處?
package study.javaenhance; import java.util.ArrayList; import java.util.Collection; public class GenericTest { public static void main(String[] args) { //JDK1.5 前 ArrayList collection = new ArrayList(); collection.add("abc"); collection.add(1); collection.add(1L); for (int i=0;i<collection.size();i++) { String abc = (String) collection.get(i); //1.必須強制轉換為對應的類型;2.強制轉型編譯時不會出錯,而運行時報異常java.lang.ClassCastException } } }
這樣的實現面臨兩個問題:
1、當我們獲取一個值的時候,必須進行強制類型轉換。
2、存在潛在風險問題,沒有對添加的數據提前編譯的時候告知.假定我們預想的是利用collection來存放String集合,因為ArrayList只是維護一個Object引用的數組,我們無法阻止將Integer類型(Object子類)的數據加入collection。然而,當我們使用數據的時候,需要將獲取的Object對象轉換為我們期望的類型(String),如果向集合中添加了非預期的類型(如Integer),編譯時我們不會收到任何的錯誤提示。但當我們運行程序時卻會報異常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at study.javaenhance.GenericTest.main(GenericTest.java:17)
這顯然不是我們所期望的,如果程序有潛在的錯誤,我們更期望在編譯時被告知錯誤,而不是在運行時報異常。
那么,下面我們采用泛型來試試看呢?
package study.javaenhance; import java.util.ArrayList; import java.util.Collection; public class GenericTest { public static void main(String[] args) { //JDK1.5 前 /*ArrayList collection = new ArrayList(); collection.add("abc"); collection.add(1); collection.add(1L); for (int i=0;i<collection.size();i++) { String abc = (String) collection.get(i); //1.必須強制轉換為對應的類型;2.強制轉型編譯時不會出錯,而運行時報異常java.lang.ClassCastException }*/ //jdk1.5 后泛型的使用 ArrayList<String> collection = new ArrayList<String>(); collection.add("abc"); //collection.add(1); //提示報錯 //collection.add(1L); //提示報錯 for (int i=0;i<collection.size();i++) { String abc = collection.get(i); //不需要強制轉換為對應的類型 } } }
可以看到當你定義為泛型以后,當你想集合中添加的元素的時候,只能添加進入String類型的元素,而不能添加進去int long 等其它類型進去了,可見在添加元素編譯前就進行了校驗了,解決了前面問題的第二點,另外取出元素的時候也不需要在強制轉換了,解決了前面問題的第一點,可見完美的解決了前面的問題。
那么到底什么是泛型呢?概念如下:
泛型,即“參數化類型”。一提到參數,最熟悉的就是定義方法時有形參,然后調用此方法時傳遞實參。那么參數化類型怎么理解呢?顧名思義,就是將類型由原來的具體的類型參數化,類似於方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),然后在使用/調用時傳入具體的類型(類型實參)。
結合上面的例子:ArrayList<String> collection = new ArrayList<String>();
這個時候ArrayList<String> 這個時候String 就是類型的實參,然后在ArrayList 類中會有形參進行接收,如下:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { .... ..... ...... public E get(int index) { RangeCheck(index); return (E) elementData[index]; }
可以看到,List接口中采用泛型化定義之后,<E>中的E表示類型形參,可以接收具體的類型實參,並且此接口定義中,凡是出現E的地方均表示相同的接受自外部的類型實參。且get()方法的返回結果也直接是此類型形參(也就是對應的傳入的類型實參)。
當然,我們也可以從用途來理解泛型:泛型是提供給編譯器使用的,在編譯階段判斷輸入的數據是否合法,編譯生成字節碼后會去掉泛型的類型信息。
二、泛型的內部原理和應用
上面提到,泛型是提供給編譯器使用的,在編譯的階段會判斷輸入的數據是否合法,編譯生成字節碼后會去掉泛型的信息,那么為了驗證這個結論,我們可以看一個簡單的例子:
//泛型僅僅在編譯階段用 List<String> list1 = new ArrayList<String>(); List<Integer> list2 = new ArrayList<Integer>(); System.out.println(list1.getClass() == list2.getClass());
可以看到結果為true,
在這個例子中,我們定義了兩個ArrayList數組,不過一個是ArrayList<String>泛型類型,只能存儲字符串。一個是ArrayList<Integer>泛型類型,只能存儲整型。最后,我們通過list2對象和list1對象的getClass方法獲取它們的類信息並比較,發現結果為true。
這是為什么呢,明明我們定義了兩種不同的類型?因為,在編譯期間,所有的泛型信息都會被擦除,List<Integer>和List<String>類型,在編譯后都會變成List類型(原始類型)。Java中的泛型基本上都是在編譯器這個層次來實現的,這也是Java的泛型被稱為“偽泛型”的原因。
上述結論可通過下面反射的例子來印證:
//繞過編譯階段 ArrayList<String> a = new ArrayList<String>(); a.add("pony1223"); Class clazz = a.getClass(); Method method = clazz.getMethod("add",Object.class); method.invoke(a,123); System.out.println(a);
因為繞過了編譯階段也就繞過了泛型,輸出結果為:
[pony1223, 123]
通過上面的學習,我們對泛型已經有了基本的認識,因此我們作一個總結即泛型中的一些術語:
ArrayList<E>類定義和ArrayList<Integer>類引用中涉及的術語:
1、 整個ArrayList<E>稱為泛型類型
2、ArrayList<E>中E稱為類型變量或類型形參
3、整個ArrayList<Integer>稱為參數化的類型
4、ArrayList<Integer>中的Integer叫類型參數的實例或類型實參
5、ArrayList<Integer>中的<>念typeof
6、ArrayList稱為原始類型
Collection<String> = new Vector(); Collection = new Vector<String >(); //參數化類型與原始類型的兼容性-編譯警告
Vector<String> v = new Vector<Object>(); //錯,參數化類型不考慮類型參數的繼承關系
Vector<Object> v = new Vector<String>();//錯
Vector<Integer> v[] = new Vector<Integer>[10]; //錯,在創建數組實例時,數組的元素不能使用參數化的類型
Vector v1 = new Vector<Integer>(); Vector<Object> v = v1; //編譯可以通過!編譯器只會按行解釋
三、泛型的通配符
為了引出通配符,我們先看一個例子:
//通配符 List<Integer> ext1 = new ArrayList<Integer>(); List<Number> ext2 = ext1; //編譯器報錯,提示不能轉換類型,注意再次強調編譯器是按行解釋的,不要認為進行運行操作 //這種是正確的 List ext3 = new ArrayList<Integer>(); List<Number> ext4 = ext3; //編譯器不會報錯兼容性,所以不要認為這里等效於List<Number> = new ArrayList<Integer> List<Number> ext5 = new ArrayList<Integer>(); //報錯
List<Integer> ext1 = new ArrayList<Integer>(); //List<Number> ext2 = ext1;
上面會報錯原因在於:ext1 和 ext2 是兩個不同的類型,要想相等,必須有共同的父類引用類型.
因為Integer雖然是Number的子類,但List<Integer>不是List<Number>的子類型。假定代碼沒有問題,那么我們可以使用語句ext2.add(newDouble())在一個List中裝入了各種不同類型的子類,這顯然是不可以的,因為我們在取出List中的對象時,就分不清楚到底該轉型為Integer還是Double了。
因此,我們需要一個在邏輯上可以用來同時表示為List<Integer>和List<Number>的父類的一個引用類型,類型通配符應運而生。在本例中表示為List<?>即可。如下例子:
//通配符 List<Integer> ext1 = new ArrayList<Integer>(); //List<Number> ext2 = ext1; //編譯器報錯,提示不能轉換類型,注意再次強調編譯器是按行解釋的,不要認為進行運行操作 //這種是正確的 List ext3 = new ArrayList<Integer>(); List<Number> ext4 = ext3; //編譯器不會報錯兼容性,所以不要認為這里等效於List<Number> = new ArrayList<Integer> //List<Number> ext5 = new ArrayList<Integer>(); //報錯 //通配符應用 List<String> ext5 = new ArrayList<String>(); printCollection(ext1); printCollection(ext4); printCollection(ext5); } private static void printCollection(List<?> list) { for(int i=0;i<list.size();i++) { System.out.println(list.get(i)); } }
注意點:
private static void printCollection(List<?> list) { for(int i=0;i<list.size();i++) { System.out.println(list.get(i)); } list.add("string");//錯 ,因為它不知道將來傳遞進來的一定是String list.size();//對,此方法與類型參數沒有關系 list = new ArrayList<Date>();//對 ? 通配符任意類型 }
通配符知識擴展:
<?>可以引用各種參數化的類型,可以調用與參數無關的方法,不能調用與參數有關的方法
限定通配符的上邊界
正確:Vector<? extends Number> v=new Vector<Integer>();
錯誤:Vector<? extends Number> v=new Vector<String>();
限定通配符的下邊界
正確:Vector<? Super Integer> v=new Vector<Number>();
錯誤:Vector<? extends Integer > v=new Vector<Byte>();
泛型的綜合應用:
private static void pringMap() { Map<String,Integer> map = new HashMap<String,Integer>(); map.put("abc",1); map.put("def",2); map.put("ghj",3); Set<Map.Entry<String,Integer>> set = map.entrySet(); for (Entry<String, Integer> entry : set) { System.out.println(entry.getKey()); System.out.println(entry.getValue()); } }
四、自定義泛型類、泛型方法和應用
首先我們了解什么是泛型類:定義泛型類,就是在類名的后面加上泛型標記。為什么會這樣用呢?看下面一個例子:
自定義泛型的方法呢?在返回值的類型之前加上一個說明<T>
package study.javaenhance; /** * 基本的增刪改查 * @author Pony * */ public class GenericDao { public <T> void add(T t){} public <T> T findById(int id){return null;} public <T> void delete(T t){} public <T> T update(T t){return null;} }
我們可以看到在沒有使用泛型類的時候需要在每個泛型方法的前面都加上<T>;那么有沒有辦法統一起來呢,不要去重復寫呢?那么就是泛型類出來了,改寫如下:
package study.javaenhance; /** * 基本的增刪改查 * @author Pony * */ public class GenericDao<T> { public void add(T t){} public T findById(int id){return null;} public void delete(T t){} public T update(T t){return null;} }
可以看到省掉了每個方法前面的<T>.即如果類的實例對象中的多處要用到同一個泛型參數,即這些地方引用的泛型類型要保持同一個實際類型時,這個時候就要采用泛型類型的方式進行定義,也就是類級別的泛型,如上就是。
類級別的泛型是根據引用該類名時指定的類型信息來參數化類型變量的,就是前面提到的類型實參來初始化類型的形參。
注意的是:
1.在對泛型類型進行參數化的時候,類型實參的實例必須是引用類型,不能是基本類型。
2.當一個變量被聲明為泛型的時候,只能被實例變量和方法調用,而不能被靜態變量和方法調用,因為靜態的成員是被所有的參數化的類共享的,所以靜態成員是不應該有類級別的類型參數的 ,可以定義為方法級別的。
自定義泛型的方法,前面說到過,就是在返回值的類型前面加上一個說明<T>
下面通過一個例子來看自定義泛型方法:
//不采用泛型方法 add(1,2); add(1.0,2.0); add(1.0,2.0); } public static int add(int x,int y){return x+y;} public static float add(float x,float y){return x+y;} public static double add(double x,double y){return x+y;}
在沒有實現泛型之前,我們需要通過重載的方式進行,但明明功能是一樣的,因為類型不同導致的,那么有沒有辦法來解決通用的類型接受,然后返回對應的類型呢?可以采用自定義泛型方法解決。
//不采用泛型方法 add(1,2); add(1.0,2.0); add(1.0,2.0); } public static <T> T add(T x ,T y){return null;}
那么返回的T 到底是什么類型呢? 我們推測嘗試看下:
Integer result1 = add(1,2); float result2 = add(1.0,2.0); //報錯 double result3 = add(1.0,2.0); String result4 = add(1.0,"abc");//報錯
這個例子貌似不太實用,而我們只是想通過這個例子說明怎么自定義泛型.以及如何進行泛型的推斷.我們可以推斷,Integer+String -->Object,推斷出這兩個T應該是Object. Integer + float --> Number,推斷出這兩個T應該是Number類型.
下面我們做一道題目為:交換任意數組中的任意兩個元素的位置.
思考,這里是要任意數組中的兩個元素的位置,因此可以看出,類型不能夠寫死,那么我們需要定義為泛型,如下:
//調用方法 swap(new String[]{"a","b","c","d"},1,2);//OK 沒有問題 swap(new int[]{1,2,3,4},1,2);//報錯,錯誤提示為:The method swap(T[], int, int) in the type GenericTest is not applicable for the arguments (int[], int, int) } public static <T> void swap(T[] arr,int a,int b) { T temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; }
錯誤提示為,沒法轉換到類型int[],那么為什么第一個是正確的呢?這里需要注意的是:因為int是基本類型,只有引用類型才能作為泛型的實際參數,(泛型的基本類型只能是引用類型,不能是值類型);雖然int[] 是Object類型,但int是基本類型。
總結:基本類型是不能使用泛型T;即如果實參傳遞給形參的時候,形參是泛型的時候,實參是不能是基本類型的。
可能會有人會有疑問,為什么上面的add(1,2) 沒有報錯,里面傳遞的是int類型,這里是因為int 在傳遞給泛型接受的時候,發現它不是引用類型,於是就自動進行裝箱和拆箱的操作,裝成了引用類型,而int[] 類型,不進行裝箱和拆箱操作是因為int[] 本身是引用類型,泛型就不會將一個引用類型再進行裝箱和拆箱操作了,但是int[] 雖然是引用類型,但里面的每一個元素卻會是int類型,那么在里面就會出錯了,因為泛型只能用於引用類型。
在定義泛型的時候,和通配符號? 一樣,也可以使用extend限定符號。也就是說在<> typeof 里面當成類型和返回值的時候也可以用限定符,不是僅僅做參數列表的時候可以用。

總結:
1.方法泛型類型的參數的尖括號應當出現在方法的其他所有修飾符之后和方法的返回類型之前,也就是說緊鄰返回值之前。按照慣例,類型的形參通常用單個大寫字母來表示。
2.除了在應用泛型的時候可以加上使用extends 限定符,在定義泛型的時候也可以使用extends限定符號,可以參見上面的例子。
3.普通方法,構造方法和靜態方法都可以使用泛型方法,編譯器也不允許創建類型變量的數組。
4.亦可以用類型變量來表示一次,稱為參數化的異常,可以用於方法的throws列表中,但是不能用於catch子句中。
5.在泛型中可以同時又多個類型參數,在定義他們的尖括號中用逗號分隔開,比如Map<K,V>
五、自定義泛型方法的練習與類型推斷總結
1.編寫一個泛型方法,自動將Object類型的對象轉換成其他類型
Object obj = "abc"; String x3 = autoConvert(obj); } private static <T> T autoConvert(Object obj){ return (T)obj; }
2.定義一個方法,可以將任意類型的數組中的所有元素填充為相應類型的某個對象.
private static <T> void fillArray(T[] a,T obj){ for(int i=0;i<a.length;i++){ a[i] = obj; } }
3.采用自定義泛型方法的方式,打印出集合中的所有的內容
public static void printCollection(Collection<?> collection){ //collection.add(1); System.out.println(collection.size()); for(Object obj : collection){ System.out.println(obj); } } public static <T> void printCollection2(Collection<T> collection){ //collection.add(1); System.out.println(collection.size()); for(Object obj : collection){ System.out.println(obj); } }
總結:
1.編譯器判斷泛型方法的實際類型參數的過程稱為類型推斷,類型推斷是相對於直覺推斷,其實實現方法是一種非常復雜的過程。
2.根據調用泛型方法時實際傳遞參數類型或返回值得類型來進行推斷,具體規則如下:
(1)當某個類型變量只在整個參數列表中的所有參數和返回值中的一處被調用,那么根據調用方法時該處的實際應用類型來確定,這很容易憑感覺推斷出來,即直接根據調用方法時候傳遞的參數類型或返回值來確定泛型參數的類型,如:swap(new String[3],3,4) ----> static <E> void swap(E[] a,int i,int b)
(2)當某個類型變量在整個參數類別中的所有參數和返回值中的多處調用了,如果調用方法時這多處的實際應用類型都是對應同一種類型,那么這個也很容易推斷出來:如:
add(3,5) ---> static <T> T add(T a,T b)
(3)當某個類型變量在整個參數列表中的所有類型和返回值多處應用,如果調用方法時候這多處的實際應用類型對應到了不同的類型上面,並且沒有使用返回值,這個時候取多個實際參數中的最大交際類型,例如:下面實際語句實際對應的類型就是Number類型了,編譯沒有問題,但是運行的時候會出現錯誤:
fill(new Integer[3],3.5f) ---->static <T> void fill(T[] a,T v);
(4)當某個類型變量在參數列表中的所有參數和返回值多處應用,如果調用方法時候這個多處的實際應用對應不同的類型,並且使用了返回值,這個時候優先會考慮返回值額類型
(5)參數類型的推斷具有傳遞性。
六、通過反射獲得泛型的實際類型參數
Vector<Date> v = new Vector<Date>();
通過v.getClass()是無法獲得泛型的參數化類型的。
將其傳遞給一個方法,可實現此功能.
public static void applyVector(Vector<Date> v)
{
}
如下:
Method applyMethod = GenericTest.class.getMethod("applyVector", Vector.class); Type[] types = applyMethod.getGenericParameterTypes(); ParameterizedType pType = (ParameterizedType)types[0]; System.out.println(pType.getRawType());//Vector System.out.println(pType.getActualTypeArguments()[0]);//Date
七、泛型相關面試題目
1. Java中的泛型是什么 ? 使用泛型的好處是什么?
泛型是一種參數化類型的機制。它可以使得代碼適用於各種類型,從而編寫更加通用的代碼,例如集合框架。
泛型是一種編譯時類型確認機制。它提供了編譯期的類型安全,確保在泛型類型(通常為泛型集合)上只能使用正確類型的對象,避免了在運行時出現ClassCastException。
2、Java的泛型是如何工作的 ? 什么是類型擦除 ?
泛型的正常工作是依賴編譯器在編譯源碼的時候,先進行類型檢查,然后進行類型擦除並且在類型參數出現的地方插入強制轉換的相關指令實現的。
編譯器在編譯時擦除了所有類型相關的信息,所以在運行時不存在任何類型相關的信息。例如List<String>在運行時僅用一個List類型來表示。為什么要進行擦除呢?這是為了避免類型膨脹。
3. 什么是泛型中的限定通配符和非限定通配符 ?
限定通配符對類型進行了限制。有兩種限定通配符,一種是<? extends T>它通過確保類型必須是T的子類來設定類型的上界,另一種是<? super T>它通過確保類型必須是T的父類來設定類型的下界。泛型類型必須用限定內的類型來進行初始化,否則會導致編譯錯誤。另一方面<?>表示了非限定通配符,因為<?>可以用任意類型來替代。
4. List<? extends T>和List <? super T>之間有什么區別 ?
這和上一個面試題有聯系,有時面試官會用這個問題來評估你對泛型的理解,而不是直接問你什么是限定通配符和非限定通配符。這兩個List的聲明都是限定通配符的例子,List<? extends T>可以接受任何繼承自T的類型的List,而List<? super T>可以接受任何T的父類構成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。在本段出現的連接中可以找到更多信息。
5. 如何編寫一個泛型方法,讓它能接受泛型參數並返回泛型類型?
編寫泛型方法並不困難,你需要用泛型類型來替代原始類型,比如使用T, E or K,V等被廣泛認可的類型占位符。泛型方法的例子請參閱Java集合類框架。最簡單的情況下,一個泛型方法可能會像這樣:
public V put(K key, V value) { return cache.put(key, value); }
6. Java中如何使用泛型編寫帶有參數的類?
這是上一道面試題的延伸。面試官可能會要求你用泛型編寫一個類型安全的類,而不是編寫一個泛型方法。關鍵仍然是使用泛型類型來代替原始類型,而且要使用JDK中采用的標准占位符。
7. 編寫一段泛型程序來實現LRU緩存?
對於喜歡Java編程的人來說這相當於是一次練習。給你個提示,LinkedHashMap可以用來實現固定大小的LRU緩存,當LRU緩存已經滿了的時候,它會把最老的鍵值對移出緩存。LinkedHashMap提供了一個稱為removeEldestEntry()的方法,該方法會被put()和putAll()調用來刪除最老的鍵值對。
8. 你可以把List<String>傳遞給一個接受List<Object>參數的方法嗎?
對任何一個不太熟悉泛型的人來說,這個Java泛型題目看起來令人疑惑,因為乍看起來String是一種Object,所以List<String>應當可以用在需要List<Object>的地方,但是事實並非如此。真這樣做的話會導致編譯錯誤。如果你再深一步考慮,你會發現Java這樣做是有意義的,因為List<Object>可以存儲任何類型的對象包括String, Integer等等,而List<String>卻只能用來存儲Strings。
List<Object> objectList; List<String> stringList; objectList = stringList; //compilation error incompatible types
9. Array中可以用泛型嗎?
這可能是Java泛型面試題中最簡單的一個了,當然前提是你要知道Array事實上並不支持泛型,這也是為什么Joshua Bloch在Effective Java一書中建議使用List來代替Array,因為List可以提供編譯期的類型安全保證,而Array卻不能。
10. 如何阻止Java中的類型未檢查的警告?
如果你把泛型和原始類型混合起來使用,例如下列代碼,Java 5的javac編譯器會產生類型未檢查的警告
,例如List<String> rawList = new ArrayList()
注意: Hello.java使用了未檢查或稱為不安全的操作;
這種警告可以使用@SuppressWarnings("unchecked")注解來屏蔽。
11、Java中List<Object>和原始類型List之間的區別?
原始類型和帶參數類型<Object>之間的主要區別是,在編譯時編譯器不會對原始類型進行類型安全檢查,卻會對帶參數的類型進行檢查,通過使用Object作為類型,可以告知編譯器該方法可以接受任何類型的對象,比如String或Integer。這道題的考察點在於對泛型中原始類型的正確理解。它們之間的第二點區別是,你可以把任何帶參數的泛型類型傳遞給接受原始類型List的方法,但卻不能把List<String>傳遞給接受List<Object>的方法,因為會產生編譯錯誤。
12、Java中List<?>和List<Object>之間的區別是什么?
這道題跟上一道題看起來很像,實質上卻完全不同。List<?> 是一個未知類型的List,而List<Object>其實是任意類型的List。你可以把List<String>, List<Integer>賦值給List<?>,卻不能把List<String>賦值給List<Object>。
List<?> listOfAnyType; List<Object> listOfObject = new ArrayList<Object>(); List<String> listOfString = new ArrayList<String>(); List<Integer> listOfInteger = new ArrayList<Integer>(); listOfAnyType = listOfString; //legal listOfAnyType = listOfInteger; //legal listOfObjectType = (List<Object>) listOfString; //compiler error - in-convertible types
13、List<String>和原始類型List之間的區別.
該題類似於“原始類型和帶參數類型之間有什么區別”。帶參數類型是類型安全的,而且其類型安全是由編譯器保證的,但原始類型List卻不是類型安全的。你不能把String之外的任何其它類型的Object存入String類型的List中,而你可以把任何類型的對象存入原始List中。使用泛型的帶參數類型你不需要進行類型轉換,但是對於原始類型,你則需要進行顯式的類型轉換。
List listOfRawTypes = new ArrayList(); listOfRawTypes.add("abc"); listOfRawTypes.add(123); //編譯器允許這樣 - 運行時卻會出現異常 String item = (String) listOfRawTypes.get(0); //需要顯式的類型轉換 item = (String) listOfRawTypes.get(1); //拋ClassCastException,因為Integer不能被轉換為String List<String> listOfString = new ArrayList(); listOfString.add("abcd"); listOfString.add(1234); //編譯錯誤,比在運行時拋異常要好 item = listOfString.get(0); //不需要顯式的類型轉換 - 編譯器自動轉換
參考資料:
張孝祥老師 JAVA增強視頻
http://www.cnblogs.com/lwbqqyumidi/p/3837629.html
http://www.cnblogs.com/lzq198754/p/5780426.html
http://blog.csdn.net/sunxianghuang/article/details/51982979