Java 泛型深度学习笔记

泛型

描述泛型的优点
使用泛型类和接口
定义泛型类和接口
解释为什么泛型类型可以提高可靠性和可读性
定义并使用泛型方法和受限泛型类型
使用原始类型向后兼容
解释为什么有必须压迫必要有通配的泛型类型
描述泛型消除并列出一些由类型消除引起的泛型上的限制和局限性
设计并实现泛型矩阵类

21.1 引言

泛型(generic)是指参数化类型的能力。
使用泛型的主要优点是能够在编译时而不是运行时检测出错误。

21.2 动机和优点

package java.lang;
public interface Comparable<T> {
    pubic int compareTo(T o);
}

这里的<T>表示形式泛型类型(formal generic type)
随后可以用一个实际具体类型(actual concrete type)来替换它。
替换泛型类型称为泛型实例化(generic instantiation)
按照惯例,像E,T这类单个大写字母用于表示一个形式泛型类型

泛型类型必须是引用类型,
误:ArrayList<int> intList = new ArrayList<int>;
正:ArrayList<Integer> intList = new ArrayList<Integer>;

自动打包:intList.add(5),Java会自动把5包装为new Integer(5)
自动解包:int x = new Integer(5),

从泛型定义好的变量中取值时,不需要再做强制转化。

21.3 定义泛型类和接口

为了创建一个字符串堆栈,可以使用new GenericStack()
这个可能会会引起对构造方法的误解:
误:pubic GenericStack<E>()
正:pubic GenericStack()

有时候,泛型类可能会有多个参数。所有的参数应以其放在尖括号中,
并用逗号隔开。例如:<E1,E2,E3>

可以定义一个类或一个接口作为泛型或者接口的子类型。
譬如在Java API中,java.lang.String类被定义为实现Comparable接口

public class String implements Comparable<String>

21.4 泛型方法

可以定义泛型接口和泛型类,还可以使用泛型类型来定义泛型方法。
为了调用泛型方法,需要将实际类型放在尖括号内作为方法名的前缀。

public class GenericMethodDemo{
    public static void main(String[] args) {
        Integer[] integers = {1,2,3,4,5};
        String[] strings = {"London","Paris","NewYork","Austin"};

        GenericMethodDemo.<Integer>print(integers);
        GenericMethodDemo.<String>print(strings);
    }
    public static <E> void print(E[] list){

        Stream.of(list).forEach {

            x -> {System.out.println(x);}
        }
    }
}

可以将泛型指定为另一种类型的子类型。这样的泛型成为受限的(bounded)

非受限类型<E<E extends Object>是一样的。

  • 为了定义一个类为泛型类型,需要将泛型类型放在类名之后。
    例如GenericStack<E>
  • 为了定义一个方法为泛型类型,要将泛型类型放在方法返回类型之前。
    例如<E> void max <E o1, E o2>

21.5 原始类型和向后兼容

GenricStack stack = new GenericStack();
像GenericStack和ArrayList这样不使用类型参数的泛型类成为原始类型(raw type)

使用原始类型的Comparable

public class Max{
    public static Comparable max (Comparable o1, Comparable o2)  {
        if (o1.compareTo(o2) > 0)
            return o1;
        else
            return o2;
    }
}

这样的代码是不安全的, 因为Max.max("Welcome",23); 编译时不会出错,但运行时会出错。
更好的编程方法就是使用泛型类型。

public class Max{
    public static <E extends Comparable<E>> E max (E o1, E o2)  {
        if (o1.compareTo(o2) > 0)
            return o1;
        else
            return o2;
    }
}

E extends Comparable<E> 表示E是Comparable的子类型。
将其放在尖括号内<E extends Comparable<E>>放在函数的前面,表示定义这个方法为泛型类型。
E max (E, E),表示这个函数入力为两个E型,返回值也为E型。

21.6 通配泛型

public static double max(Generic<Number> stack){}
public static void main(String[] args) {
    GenericStack<Integer> intStack = new GenericStack<Integer>();
}

这样会出编译错误。因为GenericStack<Integer> 不是GenericStack<Number>的实例
通配泛型有3种形式,?,? extends T,? super T
第一种形式 ?称为非受限通配(unbounded wildcard)
它和? extends Object是一样的

第二种形式? extends T 成为受限通配(bounded wildcard),
表示T或者T的一个未知子类型。

第三种形式? super T 称为下限通配(lower-bounded wildcard)
表示T或者T的一个未知父类型。

使用下面的代码就可以修复上面的错误。

public static double max(Generic<E extends Number> stack){}

尽管Integer是Object的一个子类型,但是
GenericStack<Integer>并不是GenericStack<Object>的子类型

什么时候需要<? super T>通配符

public static void main(String[] args) {
    GenericStack<String> stack1 = new GenericStack<String>();
    GenericStack<Object> stack2 = new GenericStack<Object>();
    stack2.push("Java");
    stack2.push(2);
    stack1.push("Sun");
    add(stack1,stack2);
    print(stack2);
}
public static <T> void add(GenericStack<T> stack1, GenericStack<? super T> stack2){
    while (!stack1.isEmpty())
        stack2.push(stack1.pop());
}

如果用<T>代替<? super T> 那么在add处会产生编译错误。
因为stack1的类型为GenericStack而stack2的类型为GenericStack
<? super T>表示类型T或T的父类型,而Object是String的父类型。
2可以转换成Object而不能转换成String类型。

21.7 消除泛型和对泛型的限制

泛型是使用一种称为类型消除(type erasure)的方法来实现的。
编译器使用泛型类型信息来编译代码,但是随后会消除它。
因此泛型信息在运行时是不可用的。
这种方法可以使泛型代码向后兼容使用原始类型的遗留代码。

尽管在编译时ArrayListArrayList是两种类型,
但运行时只有ArrayList类会被加载到JVM中。
所以下面的结果都是true。

ArrayList<String> list1 = new ArrayList<String>();
ArrayList<String> list2 = new ArrayList<String>();
System.out.println(list1 instanceof ArrayList);
System.out.println(list2 instanceof ArrayList);

由于泛型类型在运行时会被消除,因此会有一些限制。

限制1 不能使用new E()

出错的原因是运行时执行new E(),但E在运行时已经是不存在的了。

限制2 不能使用new E[]

不能使用泛型类型创建数组。譬如下面的代码就是错误的。
E[] elements = new E[capacity];
可以通过创建一个Object类型的数组,然后将它的类型转换成E[]来规避这个限制。
E[] elements = (E[])new Object[capacity];
会导致下面的编译警告:

型の安全性: Object[] から E[] への未検査キャスト

需要加上 @SuppressWarnings("unchecked")

不允许使用泛型类创建泛型数组,譬如下面代码是错误的。
ArrayList<String>[] list = new ArrayList<String>[10];
会导致下面的编译错误:

ArrayList<String> の総称配列を作成できません

可以使用下面的代码来规避这种限制
ArrayList<String>[] list = (ArrayList<String>[])new ArrayList[10];

限制3 在静态环境下不允许蕾的参数是泛型类型

由于泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例所共享的。
因此在静态方法,数据域或者初始化语句中,为了类而引用泛型类型参数是非法的。

public static void main(E 01) { //非法
}

public static E o1; //非法

static {
    E o1; //非法
}
限制4 异常类不能是泛型
public class MyException<T> extends Excpetion{
}
try {
    ...
} catch (MyException<T> ex){
    ...
}

因为如果允许这么做,就应为MyException添加一个catch子句。
JVM必须检查这个从try子句中抛出的异常以确定它是否与catch子句中指定的类型匹配。
但这是不可能的,运行时类型信息是不出现的。

21.8 实例学习:泛型矩阵类