⑴ java集合類哪個函數可以
java集合裡面的函數
java集合裡面的函數_java集合【1】——— 從集合介面框架說起
百里方欣
原創
關注
0點贊·155人閱讀
(一) java集合分類
之前大概分為三種,Set,List,Map三種,JDK5之後,增加Queue.主要由Collection和Map兩個介面衍生出來,同時Collection介面繼承Iterable介面,所以我們也可以說java裡面的集合類主要是由Iterable和Map兩個介面以及他們的子介面或者其實現類組成。我們可以認為Collection介面定義了單列集合的規范,每次只能存儲一個元素,而Map介面定義了雙列集合的規范,每次能存儲一對元素。
Iterable介面:主要是實現遍歷功能
Collection介面: 允許重復
Set介面:無序,元素不可重復,訪問元素只能通過元素本身來訪問。
List介面:有序且可重復,可以根據元素的索引來訪問集合中的元素。
Queue介面:隊列集合
Map介面:映射關系,簡單理解為鍵值對,Key不可重復,與Collection介面關系不大,只是個別函數使用到。
整個介面框架關系如下(來自網路):
(1) Iterable介面
1. 內部定義的方法
java集合最源頭的介面,實現這個介面的作用主要是集合對象可以通過迭代器去遍歷每一個元素。
源碼如下:
// 返回一個內部元素為T類型的迭代器(JDK1.5隻有這個介面)
Iterator iterator();
// 遍歷內部元素,action意思為動作,指可以對每個元素進行操作(JDK1.8添加)
default void forEach(Consumer super T> action) {}
// 創建並返回一個可分割迭代器(JDK1.8添加),分割的迭代器主要是提供可以並行遍歷元素的迭代器,可以適應現在cpu多核的能力,加快速度。
default Spliterator spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
從上面可以看出,foreach迭代以及可分割迭代,都加了default關鍵字,這個是Java 8 新的關鍵字,以前介面的所有介面,具體子類都必須實現,而對於deafult關鍵字標識的方法,其子類可以不用實現,這也是介面規范發生變化的一點。
下面我們分別展示三個介面的調用:
1.1 iterator方法
public static void iteratorHasNext(){
List list=new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
// 返回迭代器
Iterator iterator=list.iterator();
// hashNext可以判斷是否還有元素
while(iterator.hasNext()){
//next()作用是返回當前指針指向的元素,之後將指針移向下個元素
System.out.println(iterator.next());
}
}
當然也可以使用for-each loop方式遍歷
for (String item : list) {
System.out.println(item);
}
但是實際上,這種寫法在class文件中也是會轉成迭代器形式,這只是一個語法糖。class文件如下:
public class IterableTest {
public IterableTest() { }
public static void main(String[] args) {
iteratorHasNext();
}
public static void iteratorHasNext() {
List list = new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
Iterator iterator = list.iterator();
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String item = (String)var2.next();
System.out.println(item);
}
}
}
需要注意的一點是,迭代遍歷的時候,如果刪除或者添加元素,都會拋出修改異常,這是由於快速失敗【fast-fail】機制。
public static void iteratorHasNext(){
List list=new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
for (String item : list) {
if(item.equals("Jam")){
list.remove(item);
}
System.out.println(item);
}
}
從下面的錯誤我們可以看出,第一個元素是有被列印出來的,也就是remove操作是成功的,只是遍歷到第二個元素的時候,迭代器檢查,發現被改變了,所以拋出了異常。
Jam
Exception in thread "main" java.util.
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at IterableTest.iteratorHasNext(IterableTest.java:15)
at IterableTest.main(IterableTest.java:7)
1.2 forEach方法
其實就是把對每一個元素的操作當成了一個對象傳遞進來,對每一個元素進行處理。
default void forEach(Consumer super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
```java
當然像ArrayList自然也是有自己的實現的,那我們就可以使用這樣的寫法,簡潔優雅。forEach方法在java8中參數是`java.util.function.Consumer`,可以稱為**消費行為**或者說**動作**類型。
```java
list.forEach(x -> System.out.print(x));
同時,我們只要實現Consumer介面,就可以自定義動作,如果不自定義,默認迭代順序是按照元素的順序。
public class ConsumerTest {
public static void main(String[] args) {
List list=new ArrayList();
list.add("Jam");
list.add("Jane");
list.add("Sam");
MyConsumer myConsumer = new MyConsumer();
Iterator it = list.iterator();
list.forEach(myConsumer);
}
static class MyConsumer implements Consumer {
@Override
public void accept(Object t) {
System.out.println("自定義列印:" + t);
}
}
}
輸出的結果:
自定義列印:Jam
自定義列印:Jane
自定義列印:Sam
1.3 spliterator方法
這是一個為了並行遍歷數據元素而設計的迭代方法,返回的是Spliterator,是專門並行遍歷的迭代器。以發揮多核時代的處理器性能,java默認在集合框架中提供了一個默認的Spliterator實現,底層也就是Stream.isParallel()實現的,我們可以看一下源碼:
// stream使用的就是spliterator
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
default Spliterator spliterator() {
return Spliterators.spliterator(this, 0);
}
public static Stream stream(Spliterator spliterator, boolean parallel) {
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
使用的方法如下:
public static void spliterator(){
List list = Arrays.asList("1", "2", "3","4","5","6","7","8","9","10");
// 獲取可迭代器
Spliterator spliterator = list.spliterator();
// 一個一個遍歷
System.out.println("tryAdvance: ");
spliterator.tryAdvance(item->System.out.print(item+" "));
spliterator.tryAdvance(item->System.out.print(item+" "));
System.out.println("\n-------------------------------------------");
// 依次遍歷剩下的
System.out.println("forEachRemaining: ");
spliterator.forEachRemaining(item->System.out.print(item+" "));
System.out.println("\n------------------------------------------");
// spliterator1:0~10
Spliterator spliterator1 = list.spliterator();
// spliterator1:6~10 spliterator2:0~5
Spliterator spliterator2 = spliterator1.trySplit();
// spliterator1:8~10 spliterator3:6~7
Spliterator spliterator3 = spliterator1.trySplit();
System.out.println("spliterator1: ");
spliterator1.forEachRemaining(item->System.out.print(item+" "));
System.out.println("\n------------------------------------------");
System.out.println("spliterator2: ");
spliterator2.forEachRemaining(item->System.out.print(item+" "));
System.out.println("\n------------------------------------------");
System.out.println("spliterator3: ");
spliterator3.forEachRemaining(item->System.out.print(item+" "));
}
tryAdvance() 一個一個元素進行遍歷
forEachRemaining() 順序地分塊遍歷
trySplit()進行分區形成另外的 Spliterator,使用在並行操作中,分出來的是前面一半,就是不斷把前面一部分分出來
結果如下:
tryAdvance:
1 2
-------------------------------------------
forEachRemaining:
3 4 5 6 7 8 9 10
------------------------------------------
spliterator1:
8 9 10
------------------------------------------
spliterator2:
1 2 3 4 5
------------------------------------------
spliterator3:
6 7
還有一些其他的用法在這里就不列舉了,主要是trySplit()之後,可以用於多線程遍歷。理想的時候,可以平均分成兩半,有利於並行計算,但是不是一定平分的。
2. Collection介面 extend Iterable
Collection介面可以算是集合類的一個根介面之一,一般不能夠直接使用,只是定義了一個規范,定義了添加,刪除等管理數據的方法。繼承Collection介面的有List,Set,Queue,不過Queue定義了自己的一些介面,相對來說和其他的差異比較大。
2.1 內部定義的方法
源碼如下:
boolean add(Object o) //添加元素
boolean remove(Object o) //移除元素
boolean addAll(Collection c) //批量添加
boolean removeAll(Collection c) //批量移除
void retainAll(Collection c) // 移除在c中不存在的元素
void clear() //清空集合
int size() //集合大小
boolean isEmpty() //是否為空
boolean contains(Object o) //是否包含在集合中
boolean containsAll(Collection c) //是否包含所有的元素
Iterator iterator() // 獲取迭代器
Object[] toArray() // 轉成數組
default boolean removeIf(Predicate super E> filter) {} // 刪除集合中復合條件的元素,刪除成功返回true
boolean equals(Object o)
int hashCode()
default Spliterator spliterator() {} //獲取可分割迭代器
default Stream stream() {} //獲取流
default Stream parallelStream() {} //獲取並行流
裡面獲取並行流的方法parallelStream(),其實就是通過默認的ForkJoinPool(主要用來使用分治法(Divide-and-Conquer Algorithm)來解決問題),提高多線程任務的速度。我們可以使用ArrayList來演示一下平行處理能力。例如下面的例子,輸出的順序就不一定是1,2,3...,可能是亂序的,這是因為任務會被分成多個小任務,任務執行是沒有特定的順序的。
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
list.parallelStream()
.forEach(out::println);
2.2 繼承Collection的主要介面
graph LR;
Collection -->List-有順序,可重復
List-有順序,可重復 -->LinkedList-使用鏈表實現,線程不安全
List-有順序,可重復 -->ArrayList-數組實現,線程不安全
List-有順序,可重復 -->Vector-數組實現,線程安全
Vector-數組實現,線程安全 -->Stack-堆棧,先進後出
Collection-->Set-不可重復,內部排序
Set-不可重復,內部排序-->HashSet-hash表存儲
HashSet-hash表存儲-->LinkHashSet-鏈表維護插入順序
Set-不可重復,內部排序-->TreeSet-二叉樹實現,排序
Collection-->Queue-隊列,先進先出
2.2.1 List extend Collection
繼承於Collection介面,有順序,取出的順序與存入的順序一致,有索引,可以根據索引獲取數據,允許存儲重復的元素,可以放入為null的元素。
最常見的三個實現類就是ArrayList,Vector,LinkedList,ArrayList和Vector都是內部封裝了對數組的操作,唯一不同的是,Vector是線程安全的,而ArrayList不是,理論上ArrayList操作的效率會比Vector好一些。
裡面是介面定義的方法:
int size(); //獲取大小
boolean isEmpty(); //判斷是否為空
boolean contains(Object o); //是否包含某個元素
Iterator iterator(); //獲取迭代器
Object[] toArray(); // 轉化成為數組(對象)
T[] toArray(T[] a); // 轉化為數組(特定位某個類)
boolean add(E e); //添加
boolean remove(Object o); //移除元素
boolean containsAll(Collection> c); // 是否包含所有的元素
boolean addAll(Collection extends E> c); //批量添加
boolean addAll(int index, Collection extends E> c); //批量添加,指定開始的索引
boolean removeAll(Collection> c); //批量移除
boolean retainAll(Collection> c); //將c中不包含的元素移除
default void replaceAll(UnaryOperator operator) {}//替換
default void sort(Comparator super E> c) {}// 排序
void clear();//清除所有的元素
boolean equals(Object o);//是否相等
int hashCode(); //計算獲取hash值
E get(int index); //通過索引獲取元素
E set(int index, E element);//修改元素
void add(int index, E element);//在指定位置插入元素
E remove(int index);//根據索引移除某個元素
int indexOf(Object o); //根據對象獲取索引
int lastIndexOf(Object o); //獲取對象元素的最後一個元素
ListIterator listIterator(); // 獲取List迭代器
ListIterator listIterator(int index); // 根據索引獲取當前的位置的迭代器
List subList(int fromIndex, int toIndex); //截取某一段數據
default Spliterator spliterator(){} //獲取可切分迭代器
上面的方法都比較簡單,值得一提的是裡面出現了ListIterator,這是一個功能更加強大的迭代器,繼承於Iterator,只能用於List類型的訪問,拓展功能例如:通過調用listIterator()方法獲得一個指向List開頭的ListIterator,也可以調用listIterator(n)獲取一個指定索引為n的元素的ListIterator,這是一個可以雙向移動的迭代器。
操作數組索引的時候需要注意,由於List的實現類底層很多都是數組,所以索引越界會報錯IndexOutOfBoundsException。
說起List的實現子類:
ArrayList:底層存儲結構是數組結構,增加刪除比較慢,查找比較快,是最常用的List集合。線程不安全。
LinkedList:底層是鏈表結構,增加刪除比較快,但是查找比較慢。線程不安全。
Vector:和ArrayList差不多,但是是線程安全的,即同步。
2.2.2 Set extend Collection
Set介面,不允許放入重復的元素,也就是如果相同,則只存儲其中一個。
下面是源碼方法:
int size(); //獲取大小
boolean isEmpty(); //是否為空
boolean contains(Object o); //是否包含某個元素
Iterator iterator(); //獲取迭代器
Object[] toArray(); //轉化成為數組
T[] toArray(T[] a); //轉化為特定類的數組
boolean add(E e); //添加元素
boolean remove(Object o); //移除元素
boolean containsAll(Collection> c); //是否包含所有的元素
boolean addAll(Collection extends E> c); //批量添加
boolean retainAll(Collection> c); //移除所有不存在於c集合中的元素
boolean removeAll(Collection> c); //移除所有在c集合中存在的元素
void clear(); //清空集合
boolean equals(Object o); //是否相等
int hashCode(); //計算hashcode
default Spliterator spliterator() {} //獲取可分割迭代器
主要的子類:
HashSet
允許空值
通過HashCode方法計算獲取hash值,確定存儲位置,無序。
LinkedHashSet
HashSet的子類
有順序
TreeSet
如果無參數構建Set,則需要實現Comparable方法。
亦可以創建時傳入比較方法,用於排序。
2.2.3 Queue extend Collection
隊列介面,在Collection介面的接觸上添加了增刪改查介面定義,一般默認是先進先出,即FIFO,除了優先隊列和棧,優先隊列是自己定義了排序的優先順序,隊列中不允許放入null元素。
下面是源碼:
boolean add(E e); //插入一個元素到隊列,失敗時返回IllegalStateException (如果隊列容量不夠)
boolean offer(E e); //插入一個元素到隊列,失敗時返回false
E remove(); //移除隊列頭的元素並移除
E poll(); //返回並移除隊列的頭部元素,隊列為空時返回null
E element(); //返回隊列頭元素
E peek(); //返回隊列頭部的元素,隊列為空時返回null
主要的子介面以及實現類有:
Deque(介面):Queue的子介面,雙向隊列,可以從兩邊存取
ArrayDeque:Deque的實現類,底層用數組實現,數據存貯在數組中
AbstractQueue:Queue的子介面,僅實現了add、remove和element三個方法
PriorityQueue:按照默認或者自己定義的順序來排序元素,底層使用堆(完全二叉樹)實現,使用動態數組實現,
BlockingQueue: 在java.util.concurrent包中,阻塞隊列,滿足當前無法處理的操作。
(2) Map介面
定義雙列集合的規范Map,每次存儲一對元素,即key和value。
key的類型可以和value的類型相同,也可以不同,任意的引用類型都可以。
key是不允許重復的,但是value是可以重復的,所謂重復是指計算的hash值系統。
下面的源碼的方法:
V put(K key, V value); // 添加元素
V remove(Object key); // 刪除元素
void putAll(Map extends K, ? extends V> m); // 批量添加
void clear() // 移除所有元素
V get(Object key); // 通過key查詢元素
int size(); // 查詢集合大小
boolean isEmpty(); // 集合是否為空
boolean containsKey(Object key); // 是否包含某個key
boolean containsValue(Object value); // 是否包含某個value
Set keySet(); // 獲取所有key的set集合
Collection values(); // 獲取所有的value的set集合
Set> entrySet(); // 返回鍵值對的set,每一個鍵值對是一個entry對象
boolean equals(Object o); // 用於比較的函數
int hashCode(); // 計算hashcode
default V getOrDefault(Object key, V defaultValue) // 獲取key對應的Value,沒有則返回默認值()
default void forEach(BiConsumer super K, ? super V> action) {} // 遍歷
default void replaceAll(BiFunction super K, ? super V, ? extends V> function) {} // 批量替換
// 缺少這個key的時候才會添加進去
// 返回值是是key對應的value值,如果不存在,則返回的是剛剛放進去的value
default V putIfAbsent(K key, V value) {}
default boolean remove(Object key, Object value) {} // 移除元素
default boolean replace(K key, V oldValue, V newValue) {} // 替換
default V replace(K key, V value) {} //替換
// 和putIfAbsent有點像,只不過傳進去的mappingFunction是映射函數,也就是如果不存在key對應的value,將會執行函數,函數返回值會被當成value添加進去,同時返回新的value值
default V computeIfAbsent(K key,Function super K, ? extends V> mappingFunction) {}
// 和computeIfAbsent方法相反,只有key存在的時候,才會執行函數,並且返回
default V computeIfPresent(K key,BiFunction super K, ? super V, ? extends V> remappingFunction) {}
// 不管如何都會執行映射函數,返回value
default V compute(K key,BiFunction super K, ? super V, ? extends V> remappingFunction) {}
default V merge(K key, V value,BiFunction super V, ? super V, ? extends V> remappingFunction) {}
值得注意的是,Map裡面定義了一個Entry類,其實就是定義了一個存儲數據的類型,一個entry就是一個.
Map的常用的實現子類:
HashMap:由數組和鏈表組成,線程不安全,無序。
LinkedHashMap:如果我們需要是有序的,那麼就需要它,時間和空間效率沒有HashMap那麼高,底層是維護一條雙向鏈表,保證了插入的順序。
ConcurrentHashMap:線程安全,1.7JDK使用鎖分離,每一段Segment都有自己的獨立鎖,相對來說效率也比較高。JDK1.8拋棄了Segment,使用Node數組+鏈表和紅黑樹實現,在線程安全控制上使用Synchronize和CAS,可以認為是優化的線程安全的HashMap。
HashTable:對比與HashMap主要是使用關鍵字synchronize,加上同步鎖,線程安全。
(二)總結
這些集合原始介面到底是什麼?為什麼需要?
我想,這些介面其實都是一種規則/規范的定義,如果不這么做也可以,所有的子類自己實現,但是從迭代以及維護的角度來說,這就是一種抽象或者分類,比如定義了Iterator介面,某一些類就可以去繼承或者實現,那就得遵守這個規范/契約。可以有所拓展,每個子類的拓展不一樣,所以每個類就各有所長,但是都有一個中心,就是原始的集合介面。比如實現Map介面的所有類的中心思想都不變,只是各有所長,各分千秋,形成了大千集合世界。
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java源碼解析,JDBC,Mybatis,Spring,redis,分布式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。
平日時間寶貴,只能使用晚上以及周末時間學習寫作,關注我,我們一起成長吧~
⑵ java程序是由什麼組成的
要編寫Java程序,首先應該知道Java程序文件中必須包括什麼內容,Java程序的源程序文件結構如下:
package語句,0~1句,必須放在文件開始,作用是把當前文件放入所指向的包中。import語句,0~多句,必須放在所有類定義之前,用來引入標准類或已有類。publicclassDefinition,0~1句,文件名必須與類的類名完全相同。classDefinition,0~多句,類定義的個數不受限制。
interfaceDefinition,0~多句,介面定義的個數不受限制。Java程序的源代碼文件要求包含三個要素:
1)以package開始的包聲明語句,此句為可選。若有,且只能有一個package語句且只能是源程序文件的第一個語句,若沒有,此文件將放到默認的當前目錄下。
2)以import開始的類引入聲明語句,數量可以是任意個。
3)classDefinition和interfaceDefinition分別代表類和介面的定義。由public開始的類定義只能有一個,且要求源程序文件名必須和public類名相同,Java語言對字元的大小寫敏感,因此文件名相同意味著字母大小寫也完全相同。如果源程序文件中有主方法main(),它應放在public類中。這三個要素在程序中必須嚴格按上述順序出現。
2.類的構成
Java程序都是由類(class)所組成的,類的概念的產生是為了讓程序語言能更清楚地表達出現實事物的本性。在Java中,類就是用於創建對象的模板,包含了特定對象集合的所有特性。Java類由兩種不同的信息構成:屬性和行為。
屬性由一系列區別對象的數據組成,可用於確定屬於類的對象的外觀、狀態和其他性質。在Java程序中,屬性往往以類的成員變數形式出現。
行為指類對象對本身和其他對象所可以完成的事情,可以用於修改對象的屬性,接收來自其他對象的信息和向其他要求執行任務的對象發送信息。在Java中,行為往往以一段小程序的形式出現,Java稱這種小程序為「方法」(method)。統稱為類的成員方法。
Java中類定義的語法形式為:
修飾符class<類名>[extend<父類名>]{type類變數1;type類變數2;
成員變數修飾符type<類方法名1>(參數列表){type局
部變數;方法體
}修飾符type<類方法名2>(參數列表){type局部變數;方法體
}成員方法}
其中class是Java的關鍵字,表明其後定義的是一個類。class前面的修飾符用來限定所定義的類的使用方式。類名是用戶為該類起的名字,應該是一個合法的標識符。緊接著類定義語句的大括弧之間的內容稱為類主體。type指的是變數或方法的數據類型。類主體由成員變數和成員方法兩部分組成。
以上所有提到或未提到的概念,在後面的章節中將詳細為大家介紹。
3.注釋語句的添加
在開發Java程序的過程中,經常需要在適當的地方加上注釋語句,以便其他人閱讀
程序,一般來說,Java語言的源文件中,任何地方都可以加註釋語句,一個好的程序應該在
其需要的地方適當地加上一些注釋,以便於其他人閱讀並理解程序。
注釋語句有三種格式:
1)//注釋內容用於注釋一行語句。
2)/ˇ注釋內容ˇ/用於注釋一行或多行語句。
3)/ˇˇ注釋內容ˇˇ/用於注釋一行或多行語句且注釋語句中的內容可以通過使用Javadoc生成API文檔,實現文檔與程序同步實現的功能。
⑶ JAVA源代碼的擴展名為( )
JAVA源代碼的擴展名為.java。
Java源程序(.java文件)-java位元組碼文件(.class文件)-由解釋執行器(java.exe)將位元組碼文件載入到java虛擬機(jvm)-位元組碼文件(.class)就會在java虛擬機中執行。
Java的基本包
java.lang其中包含有:介面:Comparable、Cloneable、Runable等;類:八個基本數據類型封裝類、Byte、Short、Integer、Long、Boolean、Character、Float、Double等。
(3)java集合源碼擴展閱讀
Java為一個面向對象的語言。對程序員來說,這意味著要注意應中的數據和操縱數據的方法(method),而不是嚴格地用過程來思考。數據和方法一起描述對象(object)的狀態和行為。每一對象是其狀態和行為的封裝。
類按一定體系和層次安排的,使得子類可以從超類繼承行為。在這個類層次體系中有一個根類,它是具有一般行為的類。Java程序是用類來組織的。
Java還包括一個類的擴展集合,分別組成各種程序包(Package),用戶可以在自己的程序中使用。
例如,Java提供產生圖形用戶介面部件的類(java.awt包),這里awt為抽象窗口工具集(abstract windowing toolkit)的縮寫,處理輸入輸出的類(java.io包)和支持網路功能的類(java.net包)。
⑷ java iterator循環遍歷集合(比如HashSet)的原理
Iterator<String> it = set.iterator() 這時得到一個迭代器,它的指針位於第一個元素之前。
然後我們不停地 hasNext() 再 next 得到值,專當hasNext 為 false 時當然就不屬能去 next 了。
這背後的位置索引是在迭代器內部記錄著的,我們不能直接改它的值,只能 next 去下一個。
不過像 ListIterator 則是雙向的,可以 previous。遞增遞減都是由 next / previous 方法來操作的。
⑸ Java泛型集合的應用和方法
泛型(Generic type 或者 generics)是對 Java 語言的類型系統的一種擴展,以支持創建可以按類型進行參數化的類。可以把類型參數看作是使用參數化類型時指定的類型的一個佔位符,就像方法的形式參數是運行時傳遞的值的佔位符一樣。
可以在集合框架(Collection framework)中看到泛型的動機。例如,Map 類允許您向一個 Map 添加任意類的對象,即使最常見的情況是在給定映射(map)中保存某個特定類型(比如 String)的對象。
因為 Map.get() 被定義為返回 Object,所以一般必須將 Map.get() 的結果強制類型轉換為期望的類型,如下面的代碼所示:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
要讓程序通過編譯,必須將 get() 的結果強制類型轉換為 String,並且希望結果真的是一個 String。但是有可能某人已經在該映射中保存了不是 String 的東西,這樣的話,上面的代碼將會拋出 ClassCastException。
理想情況下,您可能會得出這樣一個觀點,即 m 是一個 Map,它將 String 鍵映射到 String 值。這可以讓您消除代碼中的強制類型轉換,同時獲得一個附加的類型檢查層,該檢查層可以防止有人將錯誤類型的鍵或值保存在集合中。這就是泛型所做的工作。
泛型的好處
Java 語言中引入泛型是一個較大的功能增強。不僅語言、類型系統和編譯器有了較大的變化,以支持泛型,而且類庫也進行了大翻修,所以許多重要的類,比如集合框架,都已經成為泛型化的了。這帶來了很多好處:
類型安全。 泛型的主要目標是提高 Java 程序的類型安全。通過知道使用泛型定義的變數的類型限制,編譯器可以在一個高得多的程度上驗證類型假設。沒有泛型,這些假設就只存在於程序員的頭腦中(或者如果幸運的話,還存在於代碼注釋中)。
Java 程序中的一種流行技術是定義這樣的集合,即它的元素或鍵是公共類型的,比如「String 列表」或者「String 到 String 的映射」。通過在變數聲明中捕獲這一附加的類型信息,泛型允許編譯器實施這些附加的類型約束。類型錯誤現在就可以在編譯時被捕獲了,而不是在運行時當作 ClassCastException 展示出來。將類型檢查從運行時挪到編譯時有助於您更容易找到錯誤,並可提高程序的可靠性。
消除強制類型轉換。 泛型的一個附帶好處是,消除源代碼中的許多強制類型轉換。這使得代碼更加可讀,並且減少了出錯機會。
盡管減少強制類型轉換可以降低使用泛型類的代碼的羅嗦程度,但是聲明泛型變數會帶來相應的羅嗦。比較下面兩個代碼例子。
該代碼不使用泛型:
List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);
該代碼使用泛型:
List<Integer> li = new ArrayList<Integer>();
li.put(new Integer(3));
Integer i = li.get(0);
在簡單的程序中使用一次泛型變數不會降低羅嗦程度。但是對於多次使用泛型變數的大型程序來說,則可以累積起來降低羅嗦程度。
潛在的性能收益。 泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制類型轉換(沒有泛型的話,程序員會指定這些強制類型轉換)插入生成的位元組碼中。但是更多類型信息可用於編譯器這一事實,為未來版本的 JVM 的優化帶來可能。
由於泛型的實現方式,支持泛型(幾乎)不需要 JVM 或類文件更改。所有工作都在編譯器中完成,編譯器生成類似於沒有泛型(和強制類型轉換)時所寫的代碼,只是更能確保類型安全而已。
泛型用法的例子
泛型的許多最佳例子都來自集合框架,因為泛型讓您在保存在集合中的元素上指定類型約束。考慮這個使用 Map 類的例子,其中涉及一定程度的優化,即 Map.get() 返回的結果將確實是一個 String:
Map m = new HashMap();
m.put("key", "blarg");
String s = (String) m.get("key");
如果有人已經在映射中放置了不是 String 的其他東西,上面的代碼將會拋出 ClassCastException。泛型允許您表達這樣的類型約束,即 m 是一個將 String 鍵映射到 String 值的 Map。這可以消除代碼中的強制類型轉換,同時獲得一個附加的類型檢查層,這個檢查層可以防止有人將錯誤類型的鍵或值保存在集合中。
下面的代碼示例展示了 JDK 5.0 中集合框架中的 Map 介面的定義的一部分:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
注意該介面的兩個附加物:
類型參數 K 和 V 在類級別的規格說明,表示在聲明一個 Map 類型的變數時指定的類型的佔位符。
在 get()、put() 和其他方法的方法簽名中使用的 K 和 V。
為了贏得使用泛型的好處,必須在定義或實例化 Map 類型的變數時為 K 和 V 提供具體的值。以一種相對直觀的方式做這件事:
Map<String, String> m = new HashMap<String, String>();
m.put("key", "blarg");
String s = m.get("key");
當使用 Map 的泛型化版本時,您不再需要將 Map.get() 的結果強制類型轉換為 String,因為編譯器知道 get() 將返回一個 String。
在使用泛型的版本中並沒有減少鍵盤錄入;實際上,比使用強制類型轉換的版本需要做更多鍵入。使用泛型只是帶來了附加的類型安全。因為編譯器知道關於您將放進 Map 中的鍵和值的類型的更多信息,所以類型檢查從執行時挪到了編譯時,這會提高可靠性並加快開發速度。
向後兼容
在 Java 語言中引入泛型的一個重要目標就是維護向後兼容。盡管 JDK 5.0 的標准類庫中的許多類,比如集合框架,都已經泛型化了,但是使用集合類(比如 HashMap 和 ArrayList)的現有代碼將繼續不加修改地在 JDK 5.0 中工作。當然,沒有利用泛型的現有代碼將不會贏得泛型的類型安全好處。
二 泛型基礎
類型參數
在定義泛型類或聲明泛型類的變數時,使用尖括弧來指定形式類型參數。形式類型參數與實際類型參數之間的關系類似於形式方法參數與實際方法參數之間的關系,只是類型參數表示類型,而不是表示值。
泛型類中的類型參數幾乎可以用於任何可以使用類名的地方。例如,下面是 java.util.Map 介面的定義的摘錄:
public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}
Map 介面是由兩個類型參數化的,這兩個類型是鍵類型 K 和值類型 V。(不使用泛型)將會接受或返回 Object 的方法現在在它們的方法簽名中使用 K 或 V,指示附加的類型約束位於 Map 的規格說明之下。
當聲明或者實例化一個泛型的對象時,必須指定類型參數的值:
Map<String, String> map = new HashMap<String, String>();
注意,在本例中,必須指定兩次類型參數。一次是在聲明變數 map 的類型時,另一次是在選擇 HashMap 類的參數化以便可以實例化正確類型的一個實例時。
編譯器在遇到一個 Map<String, String> 類型的變數時,知道 K 和 V 現在被綁定為 String,因此它知道在這樣的變數上調用 Map.get() 將會得到 String 類型。
除了異常類型、枚舉或匿名內部類以外,任何類都可以具有類型參數。
命名類型參數
推薦的命名約定是使用大寫的單個字母名稱作為類型參數。這與 C++ 約定有所不同(參閱 附錄 A:與 C++ 模板的比較),並反映了大多數泛型類將具有少量類型參數的假定。對於常見的泛型模式,推薦的名稱是:
K —— 鍵,比如映射的鍵。
V —— 值,比如 List 和 Set 的內容,或者 Map 中的值。
E —— 異常類。
T —— 泛型。
泛型不是協變的
關於泛型的混淆,一個常見的來源就是假設它們像數組一樣是協變的。其實它們不是協變的。List<Object> 不是 List<String> 的父類型。
如果 A 擴展 B,那麼 A 的數組也是 B 的數組,並且完全可以在需要 B[] 的地方使用 A[]:
Integer[] intArray = new Integer[10];
Number[] numberArray = intArray;
上面的代碼是有效的,因為一個 Integer 是 一個 Number,因而一個 In。