Java 學習筆記 14 – Collection 架構 Part.1 - 泛型


Posted by vickyh1315 on 2024-06-18


(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 提供通配字元 ?,再搭配 extendssuper,就能允許更靈活的類別指定。

範例情境:建立一靜態方法,比較陣列長度
沿用 & 改寫前面的 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);
}

#java







Related Posts

hit the road (final project) 雜七雜八心得

hit the road (final project) 雜七雜八心得

如何使用 Python 進行字串格式化

如何使用 Python 進行字串格式化

2021-09-03 React & Redux 學習目標與檢討

2021-09-03 React & Redux 學習目標與檢討


Comments