首頁  >  文章  >  Java  >  11.Java 基礎 - 泛型

11.Java 基礎 - 泛型

黄舟
黄舟原創
2017-02-27 10:43:58978瀏覽

基本概念

泛型的本質是參數化類型(Parameterized Type)的應用,也就是說所操作的資料型態被指定為一個參數,在用到的時候在指定具體的型別。

這種參數類型可以用在類別、介面和方法的建立中,分別稱為泛型類別、泛型介面和泛型方法


1.發展

在JDK 1.5 之前,只能透過Object 是所有型別的父類別和型別強制轉換兩個特點的配合來實現類型泛化。

因此在編譯期間,編譯器無法檢查這個 Object 的強制轉型是否成功,這樣容導致發生 ClassCastException (強制轉換異常) 。

下面我們來看一個實例,就能知道泛型的作用:

  • #不使用泛型的情況(1.5 之前)

#
ArrayList arrayList = new ArrayList();
arrayList.add(100);
arrayList.add("abc");//因为不知道取出来的值的类型,类型转换的时候容易出错  String str = (String) arrayList.get(0);
  • 使用泛型的情況(1.5 之後)

#
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("abc");//因为限定了类型,所以不能添加整形,编译器会提示出错arrayList.add(100);

2.術語

// 以 ArrayList<E>,ArrayList<Integer> 为例:ArrayList<E>:泛型类型

E:类型变量(或者类型参数)

ArrayList<Integer> :参数化的类型

Integer:类型参数的实例(或实际类型参数)

ArrayList :原始类型

3.探究

泛型類別

class Demo<T> {    private T value;

    Demo(T value) {        this.value = value;
    }    public T getValue() {        return value;
    }    public void setValue(T value) {        this.value = value;
    }
}public class Test {
    public static void main(String[] args) {
        Demo<String> demo = new Demo("abc");
        demo.setValue("cba");
        System.out.println(demo.getValue()); // cba
    }
}

泛型介面

interface Demo<K, V> {    void print(K k, V v);
}

class DemoImpl implements Demo<String, Integer> {    @Override
    public void print(String k, Integer v) {
        System.out.println(k + "-" + v);
    }
}public class Test {
    public static void main(String[] args) {
        Demo<String, Integer> demo = new DemoImpl();
        demo.print("abc", 100);
    }
}

泛型方法

public class Test {
    public static void main(String[] args) {        int num = get("abc", 100);
        System.out.println(num);
    }    // 关键 --> 多了 <K, V> ,可以理解为声明此方法为泛型方法
    public static <K, V> V get(K k, V v) {        if (k != null) {            return v;
        }        return null;
    }
}

類型限定

類型限定在泛型類別、泛型介面和泛型方法中都可以使用,但要注意下面幾點:

  • #不管該限定是類別還是接口,統一都使用關鍵字extends

  • #可以使用& 符號給出多個限定

  • #如果限定既有介面也有類,那麼類別必須只有一個,並且放在第一位。例如:

public static <T extends Comparable&Serializable> T get(T t1,T t2)

下面再來分析下類型限定的作用…


1.不對型別參數設定界限

  • #觀察下面的程式碼,在沒有對型別參數進行型別限定時會出現編譯錯誤。原因如下:

因為在編譯之前,編譯器並不能確認泛型類型(T)是什麼型別

##因此它預設T為原始型別(Object)。


所以只能呼叫 Object 的方法,而不能呼叫 compareTo 方法。

    public static <T> T get(T t1,T t2) {    //编译错误
        if(t1.compareTo(t2)>=0);    return t1;
    }
  • 2.對型別參數設定界限
  • 當對型別參數T 設定界限(bound)後,編譯錯誤不再發生。因為此時編譯器預設 T 的原始型別為 Comparable。

    public static <T extends Comparable> T get(T t1,T t2) {    if(t1.compareTo(t2)>=0);    return t1;
    }
  • 類型擦除


Java 中的泛型基本上都是在編譯器這個層次來實現的。

    在產生的 Java 字節碼中是不包含泛型中的型別資訊的。
  • 使用泛型時加上的型別參數,會在編譯器在編譯的時候去掉,這個過程就稱為型別擦除。
  • 來看下面的這個範例:

    public class Test {
        public static void main(String[] args) {
            ArrayList<String> arrayList1 =new ArrayList<String>();
            ArrayList<Integer> arrayList2 = new ArrayList<Integer>();        // true
            System.out.println(arrayList1.getClass() == arrayList2.getClass());
        }
    }

    觀察程式碼,這裡定義了兩個ArrayList陣列:

一個是ArrayList泛型類型,只能儲存字串,一個是ArrayList泛型類型,只能儲存整形。

    透過比較它們的類別對象,發現結果為 true。
  • 說明泛型類型 String 和 Integer 在編譯過程中都被擦除掉了,只剩下了原始類型(即 Object)。
  • 再來看一個例子:

    public class Test {
        public static void main(String[] args) throws Exception{
            ArrayList<String> arrayList =new ArrayList<String>();
            arrayList.add("abc");
            arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, 100);         for (int i=0;i<arrayList.size();i++) {  
                    System.out.println(arrayList.get(i));  
                }  
        }
    }

    觀察程式碼,這裡定義了一個ArrayList 泛型類型實例化為Integer 的物件

#如果直接呼叫add 方法,那麼只能儲存整形的資料。

    利用反射呼叫 add 方法,卻可以儲存字串。
  • 說明 Integer 泛型實例在編譯之後就被擦除了,只保留了原始型別。

  • 1.原始類型
#原始類型(raw type)就是擦除去了泛型訊息,最後在
字節碼中的類型變數的真正類型

任一泛型的型別參數,都存在對應的原始變數。

    一旦類型變數被擦除(crased),就會使用其限定類型(無限定的變數以 Object)取代。
  • // 此时 T 是一个无限定类型,所以原始类型就是 Objectclass Pair<T> { } 
    
    // 类型变量有限定,原始类型就用第一个边界的类型变量来替换,即Comparableclass Pair<T extends Comparable& Serializable> { }  
    
    // 此时原始类型为 Serializable,编译器在必要的时要向 Comparable 插入强制类型转换
    // 为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界限定列表的末尾class Pair<T extends Serializable&Comparable>
  • 2.型別參數的型別

在下面的例子中,型別參數指T,T 的型別就是所謂的【類型參數】的類型。

觀察程式碼,可以得到以下結論:

不指定【類型參數T 】的類型,當參數的型別不一致時,原始型別取同一父類的最小級

###指定【類型參數T 】的類型時,原始類型只能為其指定的類型或類型的子類別#########
public class Test {    // 定义泛型方法
    public static <T> T add(T x, T y) {        return y;
    }    public static void main(String[] args) {        // 1.不指定泛型

        // 两个参数都是 Integer,所以 T 为 Integer 类型
        int i = Test.add(1, 2); 

        // 两个参数分别是 Integer,Float,取同一父类的最小级,T 为 Number 类型
        Number f = Test.add(1, 1.2);        // T 为 Object
        Object o = Test.add(1, "asd");        // 2.指定泛型

        // 指定了Integer,所以只能为 Integer 类型或者其子类
        int a = Test.<Integer> add(1, 2);        //编译错误,指定了 Integer,不能为Float
        int b=Test.<Integer>add(1, 2.2); 

         // 指定为Number,所以可以为 Integer,Float
        Number c = Test.<Number> add(1, 2.2);
    }

}
# ########3.型別檢查#########泛型的型別檢查是針對引用的,而不是針對被引用的物件本身。 ######在下面的例子中,list 是引用對象,因此類型檢查是針對它的。 ###
// 没有进行类型检查,等价于 ArrayList list = new ArrayLis()ArrayList list = new ArrayList<String>();
list.add(100);
list.add("hello");// 进行编译检查,等价于 ArrayList<String> list = new ArrayList<String>();ArrayList<String> list = new ArrayList();
list.add("hello");
list.add(100);  // 编译错误

4.类型擦除与多态的冲突

来看下面的例子,这里定义了一个泛型类 Parent,一个实现它的子类 Son,并在子类中重写了父类的方法。

class Parent<T> {    private T value;    public T getValue() {        return value;
    }    public void setValue(T value) {        this.value = value;
    }
}

class Son extends Parent<String>{    @Override
    public void setValue(String value) {         super.setValue(value);
    }    @Override
    public String getValue(){        return super.getValue();}
    }

在上面提到过泛型的类型参数在编译时会被类型擦除,因此编译后的 Parent 类如下:

class Parent {    private Object value;    public Object getValue() {        return value;
    }    public void setValue(Object value) {        this.value = value;
    }
}

此时对比 Parent 与 Son 的 getValue/setValue 方法,发现方法的参数类型已经改变,从 Object -> String,这也意味着不是重写(overrride) 而是重载(overload)。

然而调用 Son 的 setValue 方法, 发现添加 Object 对象时编译错误。说明也不是重载。

public class Test {
    public static void main(String[] args) {
        Son son = new Son();
        son.setValue("hello");        // 关键 -->编译错误
        son.setValue(new Object());
    }
}

那么问题来了,通过上面的分析?Son 中定义的方法到底是重写还是重载?答案是:重写。这里 JVM 采用了桥方法(Brige)来解决类型擦除和多态引起的冲突。

我们对 Son 进行反编译(”Javap -c 类名.class”),得到如下内容:

Compiled from "Test.java"class Son extends Parent<java.lang.String> {
  Son();
    Code:       0: aload_0       
       1: invokespecial #8                  // Method Parent."<init>":()V
       4: return        

  public void setValue(java.lang.String);
    Code:       0: aload_0       
       1: aload_1       
       2: invokespecial #16                 // Method Parent.setValue:(Ljava/lang/Object;)V
       5: return        

  public java.lang.String getValue();
    Code:       0: aload_0       
       1: invokespecial #23                 // Method Parent.getValue:()Ljava/lang/Object;
       4: checkcast     #26                 // class java/lang/String
       7: areturn       

  public java.lang.Object getValue();
    Code:       0: aload_0       
       1: invokevirtual #28                 // Method getValue:()Ljava/lang/String;
       4: areturn       

  public void setValue(java.lang.Object);
    Code:       0: aload_0       
       1: aload_1       
       2: checkcast     #26                 // class java/lang/String
       5: invokevirtual #30                 // Method setValue:(Ljava/lang/String;)V
       8: return        }

发现这里共有 4 个 setValue/getValue 方法,除了 Son 表面上重写的 String 类型,编译器又自己生成了 Object 类型的方法,也称为桥方法。结果就是,编译器通过桥方法真正实现了重写,只是在访问时又去调用表面的定义的方法。


注意事项

  • 不能用基本类型实例化类型参数,可以用对应的包装类来实例化类型参数

// 编译错误ArrayList<int> list = new ArrayList<int>();// 正确写法ArrayList<Integer> list = new ArrayList<Integer>();
  • 参数化类型的数组不合法

Demo<T >{
}public class Test {
    public static void main(String[] args) {        // 编译错误 --> 类型擦除导致数组变成 Object [],因此没有意义
        Demo<String>[ ]  demo =new Demo[10];
    }
}
  • 不能实例化类型变量

// 编译错误,需要类型参数需要确定类型Demo<T> demo = new Demo<T>
  • 泛型类的静态上下文中不能使用类型变量

public class Demo<T> {
    public static T name;    public static T getName() {
        ...
    }
}
  • 不能抛出也不能捕获泛型类的对象

//异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉。会导致这里捕获的类型一致try{  
}catch(Problem<Integer> e1){  
    //do Something... }catch(Problem<Number> e2){  
    // do Something ...}

 以上就是11.Java 基础 - 泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn