(from geeksforgeeks)
Java 程式執行時有許多收集物件的情境(像陣列的加入移除元素就是其中一種),方便撰寫程式時能將重點擺在商業邏輯,而不是底層方法上,這篇會稍微記錄 Collection,並詳細討論泛型。
包含不同的類別與介面,。
Collection 在 Java 自成一個架構,包含各種集合介面和類別(統稱 Collection API),並通過這些 API 的方法,做到一些基本收集元素的操作。Collection 自 Java 1.2 版本開始引入,以 Collection 與 Map 這兩個主要介面,構成這個框架。(但 Map 在程式面不屬於 Collection,後面會詳述)
SE API 中的 Collection 介面,屬於 java.util 套件,是最上層共同的定義,針對排序、不重複物件、佇列操作等需求,底下分別有幾個主要介面擴充:
- List
- Set
- Queue
- Deque
這些介面再讓 Collection 與其他類別來實作,更方便的收集物件。之後的 Java 加入新引進 Lambda 的特性,讓程式寫的更簡潔。
Collection 底層語法沒特別規範收集的物件,得以哪一種類型儲存,像前面陣列物件提到的 ArrayList,就是 Collection 的實作類別之一,它可同時加入不同的類別物件,做到彈性的操作,但實務上,相對也不好管理,為規範收集物件的類別,Java 有所謂的泛型宣告,進入 Collection 之前,要先提到泛型的概念:
泛型 Generics
import java.util.ArrayList;
class Box {
int length;
int width;
int height;
CBox() {
this(6,10,8);
}
CBox(int l, int w, int h) {
this.length = l;
this.width = w;
this.height = h;
}
void print() {
System.out.println(this.length + " " + this.width + " " + this.height);
}
}
Array arraylist = new ArrayList();
arrayList.add(1);
arrayList.add(1.0F);
arrayList.add("1.0");
arrayList.add(new Box());
從陣列取出元素會是 Object 型態,要呼叫裡面的 print
方法,就得進行強制轉型,自行告訴編譯器類型。
var boxInArrayList = arrayList.get(3);
System.out.println( ((Box)boxInArrayList).print() );
泛型(Generics)語法方便使用引數或需回傳值,但不一定有特定類型的情況,在設計 API 時指定類別或方法支援了泛型,讓使用 API 更彈性。
泛型在 Java 5 引入,背後的實現是基於物件,而不是基本型別,只接受參考型別,需要基本型別的情況下要用包裝類別來代替(例如 Integer、Character、Boolean 等)。
雖然支援收集不同類型物件,但實務上還是建議收集同一種系列類型(繼承關係的父類、子類):
泛型基本語法
物件類型<指定泛型> 變數 = new 物件類型<指定泛型>();
套用至 ArrayList 為例(以 instanceof
檢查類型是 String 類別)
ArrayList<String> week = new ArrayList<String>();
week.add("Sunday");
week.add("Monday");
week.add("Tuesday");
System.out.println(week.get(0) instanceof String); // true
設計 API 時,泛型名稱可以自行定義,但全要英文大寫
/*
* 設計 API 時
* T 可以改成 TYPE, E ... 單字,但要求全部英文大寫
*/
public class MyCustomList<T> {
ArrayList<T> List = new ArrayList<>();
public void addElement(T element) {
list.add(element);
}
public void removeElement(T element) {
list.remove(element);
}
public T get(int index) {
return list.get(index);
}
}
套用至特定類別(以 String、Integer 為例)
public class GenericsRunner {
public static void main(String args[]) {
// String
MyCustomList<String> listStr = new MyCustomList<>();
listStr.addElement("Elem1");
listStr.addElement("Elem2");
String value = listStr.get(1);
System.out.println(value); // Elem2
// Integer
MyCustomList<Integer> listInt = new MyCustomList<>();
listInt.addElement(Integer.valueOf(1));
listInt.addElement(Integer.valueOf(10));
Integer value2 = listInt.get(1);
System.out.println(value2); // 10
}
}
使用宣告泛型的類別卻不指定類別(用 var
宣告本地變數),取得的物件會以 Object 類型作定義,這時候就像上面的情況一樣,要進行強制轉型才能使用方法。
public class GenericsRunner2 {
public static void main(String args[]) {
// var 宣告本地變數
var list = new MyCustomList<>();
// 也可以不加 <> 的寫法
var list = new MyCustomList();
list.addElement("Elem1");
list.addElement("Elem2");
// 取得元素需要指定型別
String value = (String)list.get(1);
System.out.println(value); // Elem2
}
}
宣告並限制類別
必須指定類型的情況下,泛型也能做到,是用繼承的方式去定義。
改寫 MyCustomList,讓只能存取 Number 底下類別(Integer, Short, Double, Float, Long...)
// 泛型宣告可以使用繼承限制類別種類
class MyCustomNumberList<T extends Number> {
List<T> list = new ArrayList<>();
public void addElement(T element) {
list.add(element);
}
public void removeElement(T element) {
list.remove(element);
}
public T get(int index) {
return list.get(index);
}
}
public class GenericsRunner2 {
public static void main(String args[]) {
var list = new MyCustomNumberList();
list.addElement(1);
list.addElement(2.0);
var value = (Integer)list.get(0);
System.out.println(value); // Elem2
}
}
泛型除了能定義類型,也能對方法(類別方法、靜態方法)的回傳值、引數類型進行定義。
static <X> doubleValue(X value) {
return value;
}
型別通配字元/通配符(Wildcard)
以上面例子,如果使用 <T>
,則限定所有傳入的參數都要是相同類型(泛型參數需要在方法簽名中保持一致,才能保證方法在編譯時期的類別安全)。
來個例子:建立一個類別與定義靜態方法,使可以深層複製新的陣列。
public class GenericsRunner3 {
public static <T> void copy(List<T> dest, List<T> src)
{
for (int i=0; i < src.size(); i++)
{
dest.add(src.get(i));
}
}
}
main 方法中執行邏輯,這樣可以正常執行
public class GenericsRunner3 {
// ...
public static void main(String args[]) {
List<Integer> output = new ArrayList<Integer>();
List<Integer> input = new ArrayList<Integer>();
input.add(1);
input.add(2);
input.add(3);
copy(output, input);
System.out.println(input.toString()); // [1, 2, 3]
System.out.println(output.toString()); // [1, 2, 3]
}
}
但使用不同泛型類型接住宣告就會報錯
public class GenericsRunner3 {
// ...
public static void main(String args[]) {
List<Object> output = new ArrayList<int>();
List<Object> input = new ArrayList<int>();
input.add(1);
input.add(2);
input.add(3);
copy(output, input);
}
}
UpperCase / Lower Wildcard
基於寫程式的靈活性,Java 提供通配字元 ?
,再搭配 extends
、super
,就能允許更靈活的類別指定。
範例情境:建立一靜態方法,比較陣列長度
沿用 & 改寫前面的 MyCustomList 類別(加上新增多重元素的方法)
// 泛型宣告可以使用繼承限制類別種類
class MyCustomList<T> {
List<T> list = new ArrayList<>();
public MyCustomList(T[] arr) {}
// 加上新增多重元素的方法
public void addByArray(T[] arr) {
list.addAll(Arrays.asList(arr));
}
public int size() {
return list.size();
}
}
建立靜態方法 getLongerList()
public class GenericsRunner4 {
public static MyCustomList<?> getLongerList(MyCustomList<?> arr1, MyCustomList<?> arr2)
{
if(arr1.size() > arr2.size())
{
return arr1;
} else
{
return arr2;
}
}
}
main 方法
public class GenericsRunner4 {
public static void main(String args[]) {
String[] strArr = { "one", "two", "three" };
Integer[] intArr = { 10, 20, 30, 40, 50, 60 };
MyCustomList<String> strMyCustomList = new MyCustomList<>(strArr);
MyCustomList<Integer> intMyCustomList = new MyCustomList<>(intArr);
System.out.println(getLongerList(strMyCustomList, intMyCustomList));
}
}
extends
- Upper Wildcard 上界通配字元
上面看到泛型可以用繼承方式 <T extends Number>
,去限定宣告類別只能是誰的子類,而搭配通配字元,也適用這樣的限制做法。
使用 Upper Wildcard,泛型類別放進的變數都需照上界(父類)的規定,因為如此,可以確保傳入變數都繼承自父類別。
改寫 MyCustomNumberList
class MyCustomNumberList<T extends Number> {
List<T> list = new ArrayList<>();
public MyCustomNumberList(T[] arr) {}
public void addByArray(T[] arr) {
list.addAll(Arrays.asList(arr));
}
public int size() {
return list.size();
}
}
public class GenericsRunner5 {
public static MyCustomNumberList<?> getLongerList(MyCustomNumberList<? extends Number> arr1, MyCustomNumberList<? extends Number> arr2)
{
if(arr1.size() > arr2.size())
{
return arr1;
} else
{
return arr2;
}
}
}
main 方法
list1, list2 放入變數類別只能是 Number 子類
public class GenericsRunner5 {
public static void main(String args[]) {
Double[] dblArr = { 1.1, 2.2, 3.3 };
Integer[] intArr = { 10, 20, 30, 40, 50, 60 };
MyCustomNumberList<Double> list1 = new MyCustomNumberList<>(dblArr);
MyCustomNumberList<Integer> list2 = new MyCustomNumberList<>(intArr);
System.out.println(getLongerList(list1, list2));
}
}
super
- Lower Wildcard 下界通配字元
相反於上界的 extends
用法,super
是限定為泛型變數的父類。
新增一個 MyCustomNumberList2 類別,改寫自 MyCustomNumberList
class MyCustomNumberList2<T> {
private T median;
List<T> list = new ArrayList<>();
public MyCustomNumberList2(T[] arr) {}
public void addByArray(T[] arr) {
list.addAll(Arrays.asList(arr));
}
public int size() {
return list.size();
}
public void sort() {
Collections.sort(list);
}
public void addElement(T element) {
list.add(element);
}
public T getElement(int index) {
return list.get(index);
}
}
新增元素,並在 main 方法取得陣列元素
public class GenericsRunner6 {
static void addCoupleOfValues(MyCustomNumberList2<? super Number> arr) {
arr.addElement(1);
arr.addElement(1.0);
arr.addElement(1.0F);
arr.addElement(1L);
}
}
main 方法
public class GenericsRunner6 {
public static void main(String args[]) {
MyCustomNumberList2<Number> list1 = new MyCustomNumberList2<>();
addCoupleOfValues(list1);
System.out.println(list1.getElement(2).getClass());
Number f1 = list1.getElement(2); // 1.0
Float f2 = list1.getElement(2); // ERROR
}
}
以 super
宣告的泛型,傳入的變數用 .get()
取出,因為對編譯器來說,他只知道傳入的最大父類別是 Number,不知道子類是誰,所以儘管我們知道取到元素是 Float,還是只能以 Number 類型,或更上一層的 Object 去接收變數。
介面實作泛型
除了類別、類別方法(含靜態方法),介面也支援泛型,以泛型實作介面,操作上也能更彈性。
public interface 介面<指定泛型> {
void 方法;
}
public interface Comparator<T> {
int compare(T o1, T o2);
}