疯狂Java讲义 第九章 Java泛型

jefxff 153,688 2020-04-05

1. 泛型入门

  1. 在没有泛型之前, 一旦把一个对象放进Java集合中, 集合就会忘记对象的类型, 把所有的对象当成Object类型处理; 该对象的编译类型就变成了Object类型(其运行时类型没变) 当程序从集合中取出对象后, 就需要进行强制类型转换,
  2. 增加泛型支持后的集合, 完全可以记住集合中元素的类型, 并可以在编译时检查集合中元素的类型, 如果试图向集合中添加不满足类型要求的对象, 编译器就会提示错误
  3. 泛型的实质: 允许在定义接口, 类时声明类型形参, 类型形参在整个接口, 类体内可当成类型使用, 几乎所有可使用普通类型的地方都可以使用这种类型形参

1.1 编译时不检查类型的异常

示例代码

// 例子:
// 下面程序在编译时不检查类型导致异常
public class ListErr{
    public static void main(String[] args){
        // 创建一个只想保存字符串的List集合
        List strList = new ArrayList();
        strList.add("Python");
        strList.add("Java");
        // 将一个Integer对象添加到集合
        strList.add(5);   // ①
        strList.forEach(str -> System.out.println(((Stirng)str).length()));  // ②
    }
}
// 程序说明:
//     1. strList只希望保存字符串类型的对象, 但是程序不能进行任何限制, 如果程序在①处把一个Integer
//         对象添加到strList集合中, 这将导致在②处引发ClassCastException异常, 因为程序试图将一个
//         Integer对象转换为String类型

1.2 使用泛型

  1. Java5之后, Java引入了"参数化类型(parameterized type)"的概念, 允许程序再创建集合时指定集合元素的类型,
  2. 语法: 在集合接口, 类后面增加尖括号(<Xxx>), 尖括号中放一个数据类型, 即表明这个集合接口, 集合类只能保存特定类型的对象 如: List 表示List集合只能添加String类型的对象

示例代码

// 例子:
public class GenericList{
    public static void main(String[] args){
        // 创建一个只想保存字符串的List集合
        List<String> strList = new ArrayList<String>();  // ①
        strList.add("Python");
        strList.add("Java");
        // 将一个Integer对象添加到集合, 编译时将报错
        // strList.add(5);  // ② 
        strList.forEach(str -> System.out.println(strList.length()));  // ③
    }
}
// 程序说明:
//     1. 在① 进行了类型声明 List<String>, 类型参数是String, 在创建这个 ArrayList对象时也指定了
//         一个类型参数; strList 只能添加 String 类型的元素, 
//     2. 在②出试图添加一个 Integer类型的元素, 编译时会报错
//     3. 由于 strList 集合"记住了"所有集合元素都是String类型, 所以无须对集合进行强制类型转换

1.3 Java7 泛型的 "菱形" 语法

  1. Java7开始, Java允许在构造器后不需要带完成的泛型信息, 只要给出一对尖括号(<>)即可, Java 可以推断尖括号里应该是什么泛型信息(例子2)
  2. (Java7之前吗如果使用带泛型的接口, 类定义变量, 那么调用构造器创建对象时构造器的后面也必须使用泛型(例子1))

示例代码

// 例子1:
// java7 之前构造器后面也必须使用泛型
List<String> strList = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String Integer>();
// 例子2:
// java7 之后构造器后面使用<>即可, Java自己推断类型
List<String> strList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();
// 例子3:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//
// Java 的 "菱形" 语法 
public class DiamondTest{
    public static void main(String[] args) {
        // java 自动推断出 ArrayList的<>里应该是String
        List<String> books = new ArrayList<>();  
        books.add("Python");
        books.add("Java");
        // 遍历books集合, 集合元素就是String类型
        books.forEach(ele -> System.out.println(ele.length()));
        // java 自动推断出HashMap的<>里应该是String, List<String>
        Map<String, List<String>> schoolsInfo = new HashMap<>();
        // java自动推断出ArrayList的<>里应该是String
        List<String> schools = new ArrayList<>();
        schools.add("Hello !");
        schools.add("Thank you!");
        schoolsInfo.put("雷军", schools);
        // 遍历 Map 时, Map 的 key 是 String 类型, value 是 List<String>类型
        schoolsInfo.forEach((key, value) -> System.out.println(key + "-->" + value));
    }
}

2. 深入泛型

  1. 泛型: 就是允许在定义类, 接口, 方法时使用类型形参, 这个类型形参将在声明变量, 创建对象, 调用方法时动态地指定(即传入实际的类型参数, 也可以称为类型实参)

2.1 定义泛型接口, 类

  1. 可以为任何类, 接口增加泛型声明(并不是只有集合类才可以使用泛型声明, 虽然集合类是泛型的重要使用场所)
  2. 包含泛型声明的类型可以在定义变量, 创建对象时传入一个类型实参, 从而可以动态地生成无数多个逻辑上的子类, 但这种子类在物理上并不存在
  3. 当创建带泛型声明的自定义类, 为该类定义构造器时, 构造器名还是原来的类名, 不能增加泛型声明;

示例代码

// 例子:
// 定义泛型的Apple类
public class Apple<T>{
    // 使用T类型形参定义实例变量
    private T info;
    public Apple(){}
    // 使用T类型形参来定义构造器
    public Apple(T info){
        this.info = info;
    }
    public void setInfo(T info){
        this.info = info;
    }
    public T getInfo(){
        return this.info;
    }
    public static void main(String[] args){
        // 由于传给T形参的是String, 所以构造器参数只能是String
        Apple<String> a1 = new Apple<>("苹果");
        System.out.println(a1.getInfo());
        // 由于传给T形参的是Double, 所以构造器参数只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getInfo());
    }
}
// 程序说明:
//     1. 上面程序定义了一个带泛型声明的Apple<T>类, 使用Apple<T>类时就可为T类型形参传入实际类型, 这样
//         就可以生成如 Apple<String>, Apple<Double>...形式的多个逻辑子类(物理上并不存在)
//     2. 为 Apple<T类定义构造器, 其构造器名依然是Apple, 而不是Apple<T>; 调用该构造器时却可以使用
//         Apple<T>的形式, 当然应该为T形参传入实际的类型参数, Java7之后, 也可以使用省略<>中的类型参数
// 例子: 
// Java5 之后, List接口, Iterator接口, Map接口的代码片段
// 定义接口时指定了一个类型参数, 该形参名为E
public interface List<E>{
    // 在该接口里, E可以作为类型使用
    // 下面方法可以使用E作为参数类型
    void add(E x);
    Iterator<E> iterator();   // ① 
    // ...
}
// 定义接口时指定了一个类型形参, 该形参名为E
public interface Iterator<E>{
    // 在该接口里E完全可以作为类型使用
    E next();
    boolean hasNext();
    // ...
}
// 定义该接口时指定了两个类型形参, 其形参名为K, V 
public intetface Map<K, V>{
    Set<K> keySet()    // ②
    V put(K key, V value)
    // ...
}
// 程序说明:
//     1. ①②处方法声明返回值类型是Iterator<E>, Set<K>, 这表明Set<K> 形式是一种特殊的数据类型, 是一种
//         与Set不同的数据类型, 可以认为是Set类型的子类
//     2. 例如使用 List 类型时, 如果E形参传入String类型实参, 则产生了一个新的类型: List<String>类型,
//         可以把List<String>想象成E被完全替换成儿了String的特殊List子接口
//     3. 虽然程序只定义了一个List<E>接口, 但实际使用时可以产生无数多个List接口, 只要E传入不同的类型实
//         参, 系统就是多出一个新的List子接口

2.2 从泛型类派生子类

  1. 当创建带泛型声明的接口, 父类之后, 可以为该接口创建实现类, 或从该父类派生子类, 注意: 当使用这些接口, 父类时不能在包含类型形参如:
    // 定义类A继承Apple类, Apple类不能跟类型形参
    // public class A extends Apple{} // 错误代码示范

  2. 定义方法时可以声明数据形参, 调用方法(使用方法)时必须为这些数据形参传入实际的数据; 于此类似的是, 定义类, 接口, 方法时可以声明类型形参, 使用类, 接口, 方法时应该为类型形参传入实际类型如:
    // 想从Apple类派生一个子类
    // 使用Apple类时为T形参传入String类型
    public class A extends Apple{}

  3. 调用方法时必须为所有的数据形参传入参数值, 与调用方法不同的是,使用类, 接口时也可以不为类型形参传入实际的类型参数如:
    // 使用Apple类时, 没有为T形参传入实际的类型参数
    public class A extends Apple{}

示例代码

// 例子:
/**
 * 如果从 Apple<String>类派生子类, 则在Apple类中所有使用T类型形参的地方都将被替换成String类型, 即 
 *   它的子类将会继承到 String getInfo() 和 void setInfo(String info) 两个方法,  
 */
public class A1 extends Apple<String>{
    // 正确重写了父类的方法, 返回值
    // 与父类Apple<String>的返回值完全相同
    public String getInfo(){
        return "SubClass" + super.getInfo();
    }
    /*
    // 下面的方法是错误的, 重写父类的方法的返回值类型不一致
    public Object getInfo(){
        return "SubClass";
    }
    */
}
// 例子:
// 使用Apple类时没有传入实际的类型参数, Java编译器会报警告
public class A2 extends Apple{
    // 重写父类的方法
    public String getInfo(){
        // super.getInfo()方法返回的值是Object类型
        // 所以加上toString()才返回String类型
        return super.getInfo().toString();
    }
}

2.3 并不存在泛型类

  1. ArrayList类确实象一种特殊的ArrayList类: 该 ArrayList 对象只能添加String对象作为集合元素, 但实际上, 系统并没有为 ArrayList 生成新的 class文件, 而且也不会把ArrayList当成新类来处理(例子1)
  2. 不管为泛型的类型参数传入了哪一种类型参数, 对于Java来说, 他们依然被当成同一类处理, 在内存中也只占用一块内存空间, 因此在静态方法, 静态初始化块或静态变量的声明和初始化中不允许使用类型形参(例子2)

示例代码

// 例子1:
// 分别创建List<String>对象和List<Integer>对象
List<Stirng> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
// 调用getClass()方法来比较l1和l2的类是否相等
System.out.println(l1.getClass() == l2.getClass());  // out: true
// 例子2:
// 静态方法, 静态初始化块或静态变量的声明和初始化中不允许使用类型形参
public class R<T>{
    // 下面的代码错误, 不能在静态变量声明中使用类型形参
    static T info;
    T age;
    public void foo(T msg){}
    // 下面的代码错误, 不能在静态方法声明中使用类型形参
    public static void nar(T msg){}
}

3. 类型通配符

  1. 当使用泛型类时(包括声明变量和创建对象两种情况), 都应该为这个泛型类传入一个类型实参
  2. Java泛型的设计原则: 只要代码在编译是没有出现警告,就不会遇到运行时 ClasscastException异常
  3. Java泛型中不允许将如 List 对象赋值给 List变量
    如: List iList = new ArrayList<>();
    // 下面的代码导致编译错误
    // List nList = iList;

3.1 使用类型通配符

  1. 为了表示各种泛型List的父类, 可以使用通配符, 类型通配符是一个问号(?), 将一个问号作为类型实参传给List集合(例子1)
  2. 写作: List<?> 意思是:元素类型位置的List, 这个问号(?)被称之为通配符, 它的元素类型可以匹配任何类型
  3. 但是这种带通配符的List仅表示它是各种泛型List的父类, 并不能将元素加入其中(null除外)
  4. 这样记: 在使用通配符的情况下, Java不允许把对象放进一个未知类型的集合中

示例代码

// 例子1:
public void test(List<?> c){
    for (int i = 0; i < c.size(); i++){
        System.out.println(c.get(i));
    }
}
// 程序说明:
//     1. 现在使用任何类型的List来调用test()方法, 程序依然可以访问集合c中的元素, 其类型是Object, 这永
//         远都是安全的, 因为不管List的真实类型是什么, 他包含的都是Object
// 例子:
List<?> c = new ArrayList<String>();
// 下面的代码引起编译错误
c.add(new Object());
// 程序说明:
//     1. 程序无法确定c集合中元素的类型, 所以不能向其添加对象,(null除外, null是所有引用类型的实例)
//     2. 程序可以调用get()方法来返回 List<?>集合指定索引处的元素, 由于不知道返回值是什么类型, 可以将
//         返回值赋值给Object类型的变量, 或者放在任何希望是Object类型的地方

3.2 设定类型通配符的上限

  1. 因为List 这种形式表示这个List集合可以是任何泛型List的父类, 如果程序不希望这个List是任何泛型List的父类, 只希望它代表某一类泛型List的父类
  2. 通过 List<? extends Shape> 这种形式表示被限制的泛型通配符: 他表示所有Shape泛型List的父类; 此处的问号(?)表示一个未知的类型, 但是此处的这个未知的类型一定是Shape的子类型(也可以是Shape本身), 因此,这个例子中将Shape称之为这个通配符的上限(upper bound)
  3. 由于程序无法确定这个受限制的通配符的具体类型, 所以不能把Shape对象或其子类的对象加入到这个泛型集合;

示例代码

// 例子:
// 先定义一个抽象类 Shape 
public abstract class Shape{
    public sbatract void draw(Canvas c);
}
// 
// 定义一个Shape的子类Circle
public calss Circle extends Shape{
    // 实现画布的方法, 以打印字符串来模拟画布的方法
    public void draw(Canvas c){
        System.out.println("在画布" + c + "上画一条龙");
    }
}
//
// 在定义一个Shape的子类Rectangle
public class Rectangle extends Shape{
    // 实现画布的方法, 以打印字符串来模拟画布的方法
    public void draw(Canvas c){
        System.out.println("在画布" + c + "上画一道彩虹");
    }
}
//
// 实现Canvas
public class Canvans{
    // 同时在画布上绘制多个形状, 使用被限制的泛型通配符
    public void drawAll(List<? extends Shape> shapes){
        for (Shape s : shapes){
            s.draw(this);
        }
    }
}

3.3 设定类型形参的上限

  1. Java泛型不仅允许在使用通配符形参时设定上限, 而且可以在定义类型形参时设定上限, 用于表示传给该类型形参的实际类型要么是该上限类型, 要么是该上限类型的子类 (例子1)
  2. 如果程序需要为类型形参设定多个上限(最多一个父类上限, 可以有多个接口上限), 表明该类型形参必须是其父类的子类(或父类本身), 并且实现多个上限接口 (例子2)
  3. 与类同时继承父类, 实现接口类似的是, 为类型形参指定多个上限时, 所有的接口上限必须位于类上限之后, 即: 如果需要为类型形参指定类上限, 类上限必须位于第一位

示例代码

// 例子1:
// 定义一个Apple类, 该类的类型形参上限是Number类
// 表明使用Apple类时为T传入的实际类型只能是Number类或Number类的子类
public class Apple <T extends Number>{
    T col;
    public static void main(String[] args){
        Apple<Integer> ai = new Apple<>();
        Apple<Double> ad = new Apple<>();
        // 下面代码将引发编译异常, 下面代码试图把String类型传给T类型
        // 但是 String 不是 Number 的子类, 所以会引起编译错误
        // Apple<string> as = new Apple<>();
    }
}
// 例子2:
// 下面程序表明Apple类的T类型必须是Number类或其子类, 并且必须实现 java.io.Serializable 接口
public calss Apple<T extends Number & java.io.Serializable>{
    // ...
}

4. 泛型方法

4.1 定义泛型方法

  1. 用于在定义类,接口时没有使用类型形参, 但定义方法时想自己定义类型形参
  2. 泛型方法: 就是在声明方法时定义一个或多个类型形参
  3. 方法声明中定义的形参只能在该方法里使用; 而接口, 类声明中定义的类型形参则可以在整个类, 接口中使用
  4. 方法中的泛型形参无须显式传入实际类型参数

泛型方法的格式:(例子1)

[修饰符] <T , S> 返回值类型 方法名(形参列表){
    // 方法体...
}

示例代码

// 例子1:
import java.util.ArrayList;
import java.util.Collection;
// 
public class GenericMethodTest {
    // 什么一个泛型方法, 该泛型方法中带一个T类型形参
    static <T> void fromArrayToCollection(T[] a, Collection<T> c){
        for (T o : a){
        c.add(o);
        }
    }
    public static void main(String[] args){
        Object[] oa = new Object[100];
        Collection<Object> co = new ArrayList<>();
        // 下面代码中的T代表Object类型
        fromArrayToCollection(oa, co);
        String[] sa = new String[100];
        Collection<String> cs = new ArrayList<>();
        // 下面代码中的T代表String类型
        fromArrayToCollection(sa, cs);
        Integer[] ia = new Integer[100];
        Float[] fa = new Float[100];
        Number[] na = new Number[100];
        Collection<Number> cn = new ArrayList<>();
        // 下面代码中的T代表Number类型
        fromArrayToCollection(ia, cn);
        // 下面代码中的T代表Number类型
        fromArrayToCollection(fa, cn);
        // 下面的代码中T代表Number类型
        fromArrayToCollection(na, cn);
        // 下面的代码中T代表Object类型
        fromArrayToCollection(na, co);
        // 下面的代码中T代表String类型,但na是一个Number数组
        // 因为Number既不是String类型, 也不是它的子类, 所以出现编译错误
        // fromArrayToCollection(na, cs);
    }
}

4.2 泛型方法和类型通配符的区别

  1. 大多数时候都可以使用泛型方法来代替类型通配符(例子1)
  2. 在不同的调用点传入不同的实际类型时, 就应该使用通配符
  3. 泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系, 或者方法返回值与参数之间的类型依赖关系; 这种情况下就应该使用泛型方法
  4. 也可以同时使用泛型方法可通配符 (例子2)
  5. 类型通配符既可以在方法签名中定义形参的类型, 也可以用于定义变量的类型; 但泛型方法中的类型形参必须在对应方法中显式声明

示例代码

// 例子1:
// Java的Collection接口中两个使用泛型的方法
public interface Collection<E>{
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
}
//
// 上面的方法也可以修改为类型通配符方法
public interface Collection<E>{
    <T> boolean containsAll(Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
}
// 说明:
//     1. <T extends E>这句代码使用了泛型形式, 这时定义类型形参时设定上限(其中E是Collection接口里
//         定义的类型形参, 在该接口里E可当成普通类型使用)
// 例子2:
// 同时使用泛型方法和通配符的情况
public class Collections{
    public static <T> void copy(List<T> dest, List<? extends T> src){//...}
}
// 说明:
//     1. 上面的copy方法中的dest和src存在明显的依赖关系, 从源src中复制出来的元素, 必须可以"添加"到目标
//         List中, 所以源List集合元素的类型只能是目标集合元素的类型的子类型或者它本身

4.3 Java7的"菱形"语法和泛型构造器

  1. 泛型构造器: 就是在定义构造器时, 在构造器签名中声明类型形参
  2. 作用: 定义了泛型构造器, 在接下来的调用构造器时, 就不仅可以让Java根据数据参数的类型来"推断"类型形参的类型, 而且也可以显示地为构造器中类型形参指定实际的类型(例子1)
  3. Java7新增的"菱形"语法允许调用构造器时在构造器后使用一对尖括号来代表泛型信息; 但是程序显式指定了泛型构造器中什么的类型形参的实际类型, 则不可以使用 "菱形" 语法; (例子2)

示例代码

// 例子1:
class Foo{
    public <T> Foo(T t){
        System.out.println(t);
    }
}
public class GenericConsttutor{
    public static void main(String[] args){
        // 泛型构造器中T参数为String
        new Foo("Need Study Python Now!");
        // 泛型构造器中T参数为Integer
        new Foo(200);
        // 显式指定泛型构造器中T参数为String
        // 传给Foo构造器的实参也是String对象, 不会出错
        new <String> Foo("Java is Hard, But I Need Study Continue!");
        // 显式指定泛型构造器的T参数为String
        // 但传给Foo构造器的实参是Double对象, 下面代码出错
        // new <String> Foo(12.3);
    }
}
// 例子2:
class MyClass<E>{
    public <T> MyClass(T t){
        System.out.println("t参数的值为: " + t);
    }
}
public class GenericDiamondTest{
    public static void main(String[] args){
        // 下面代码中MyClass类声明中的E行参数是String类型
        // 泛型构造器中声明的T形参是Integer类型
        MyClass<String> mc1 = new MyClass<>(5);
        // 显式指定泛型构造器中声明的T形参是Integet类型
        MyClass<String> mc2 = new <Integer> MyClass<String>(5);
        // MyClass类声明中的E形参是String类型
        // 如果显式指定泛型构造器中声明的T形参是Integer类型
        // 此时就不能使用"菱形"语法, 下面的代码报错
        // MyClass<String> mc3 = new <Integer> MyClass<>(5);
    }
}

4.5 设定通配符的下限

语法

<? super Type> //表示: 这个通配符表示它必须是Type本身, 或是Type的父类

示例代码

//例子1:
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
//
public class MyUtils {
    // 下面 dest 集合元素的类型必须与 src 集合元素的类型相同, 或是其父类
    public static <T> T copy(Collection<? super T> dest, Collection<T> src){
        T last = null;
        for (T ele : src){
            last = ele;
            dest.add(ele);
        }
        return last;
    }
    public static void main(String[] args){
        List<Number> ln = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        li.add(5);
        li.add(3);
        li.add(12);
        // 此处可准确地知道最后一个被赋值的元素是Integer类型
        // 与src集合元素的类型相同
        Integer last = copy(ln, li);
        System.out.println(last);
        System.out.println(ln);
        System.out.println(li);
    }
}
//例子2:
/**
 * 下面的E是定义TreeSet类时的类型形参
 * TreeSet(Comparator<? super E> c)
 * 在TreeSet中,如果需要定制排序, 则TreeSet对象有一个与之关联的Comparator对象, 上面构造器中的参数c
 * 就是进行定制排序的Comparator对象
 * 通过这种带下限的通配符的语法, 可以在创建TreeSet对象时, 灵活的选择合适的Comparator
 * 
 */
import java.util.Comparator;
import java.util.TreeSet;
//
public class TreeSetTest {
    public static void main(String[] args){
        // Comparator的实际类类型是TreeSet的元素类型的父类, 满足要求
        TreeSet<String> ts1 = new TreeSet<>(
            new Comparator<Object>(){
                public int compare(Object fst, Object snd){
                    return fst.hashCode() > snd.hashCode() ? 1 
                    : fst.hashCode() < snd.hashCode() ? -1 : 0;
                }
            });
        ts1.add("hello");
        ts1.add("wa");
        // Comparator 的实际类型是TreeSet元素的类型, 满足要求
        TreeSet<String> ts2 = new TreeSet<>(
            new Comparator<String>(){
                public int compare(String first, String second){
                    return first.length() > second.length() ? 1 
                    : first.length() < second.length() ? -1 : 0;
                }
            });
        ts2.add("Hello");
        ts2.add("wa");
        System.out.println(ts1);
        System.out.println(ts2);
    }
}
// 说明:
//     1. 通过使用这种通配符下限的方式定义TreeSet构造器参数, 就可以将所有可用的Comparator作为参数传入,
//         从而增加程序的灵活性
//     2. 主要理解TreeSet<Stirng> 和 Comparator<String>中, Comparator的泛型类型必须是TreeSet泛型类
//         型的父类型(或其本身)

4.5 泛型方法与方法重载

  1. 书中的例子阐述了, 在一个类中方法的重载时, 两个方法分别用了通配符上限和通配符下限, 在调用方法时两个方法都匹配, 结果就是把虚拟机搞懵逼了; 结果想说的就是: 不要再一个类中不要在方法的重载中分别调用通配符上限和通配符下限

示例代码

// 例子:
// 通过例子再理解一下上面说明1
public class MyUtils{
    // 这个方法中, dest的类型是T, src的类型是T类型或者T类型的子类
    // 通配符上限
    public static <T> void copy(Collection<T> dest, Collection<? extends T> src){...}  // ①
    // 这个方法中, dest的类型是T类型或者T的父类, src的类型是T
    // 通配符下限
    public static <T> T copy(Collection<? super T> dest, Collection<T> src){...}   // ② 
}
// 下面的调用copy()方法时会搞懵逼Java虚拟机
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln, li);
// 说明:
//     1. 调用copy()方法时, 这个方法既可以匹配①处的copy()方法, 此时T类型参数的类型是Number; 也可以匹
//         配②处的copy()方法, 此时T参数的类型是Integer; 编译器无法确定调用那个方法, 所以会出错
//     2. 记住: 如果把虚拟机搞懵逼了, 那就是代码写错了 

4.6 Java 8 改进的类型推断

  1. 可通过调用方法的上下文来推断类型参数的目标类型
  2. 可在方法调用链中, 将推断得到的类型参数传递到最后一个方法

示例代码

// 例子:
class MyUtil<E>{
    public static <Z> MyUtil<Z> nil(){
        return null;
    } 
    public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail){
        return null;
    }
    E head(){
        return null;
    }
}
public class InterfaceTest(){
    public static void main(String[] args){
        // 可通过方法赋值的目标参数来推断类型参数为String
        MyUtil<String> ls = MyUtil.nil();    // ①
        // 无须使用下面语句在调用nil()方法时指定类型参数的类型
        MyUtil<String> mu = MyUtil.<String>mil();
        // 可调用cons()方法所需的参数类型来推断类型参数为Integer
        MyUtil.cons(42, MyUtil.nil());   // ②
        // 无须使用下面的语句在调用nil()方法时指定类型参数的类型
        MyUtil.cons(42, MyUtil.<Integer>nil());   // ③
    }
}
// 程序说明:
//     1. 在①处的代码和下面一条代码语句作用完全相同, 区别在于在①处, 代码无须在调用MyUtil类的nil()方法
//         时显式的指定类型参数为String, 这是因为程序需要将该方法的返回值赋值给MyUtil<String>类型, 因
//         此系统可以自动推断此处的类型参数为String类型
//     2. 在②③处的代码作用完全相同, 在③处的代码也无须在调用MyUtil类的nil()方法时显示的指定类型参数为
//         Integer, 这是因为程序将nil()方法的返回值作为了MyUtil类的cons()方法的第二个参数, 而程序可以
//         根据cons()方法的第一个参数(42)推断出此处的类型参数为Integer类型

5. 擦除和转换

  1. 如果没有为这个泛型类指定实际的类型参数, 则该类型参数被称作 raw type(原始类型), 默认是声明该类型参数时指定的第一个上限类型
  2. 当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时, 所有在尖括号之间的类型信息都将被扔掉;比如: List类型被转换为List, 则该List对集合元素的类型检车变成了类型参数的上限(即Object)
  3. 从逻辑上来看, List是List的子类, 如果直接把一个List对象赋给一个List对象不会引起编译器错误, 但是对泛型来说, 可以直接把一个List对象赋给一个List对象, 编译器仅仅提示"未经检查的转换" (例子2)

示例代码

//例子1:
class Apple<T extends Number>{
    T size;
    public Apple(){}
    public Apple(T size){
        this.size = size;
    }
    public void setSize(T size){
        this.size = size;
    }
    public T getSize(){
        retuen this.size;
    }
}
public class ErasureTest{
    public staitc void main(String[] args){
        Apple<Integer> a = new Apple<>(6);    // ①
        // a 的 getSize() 方法返回Integer对象
        Integer as = a.getSize();
        // 把a对象赋值给Apple变量, 丢失尖括号里的类型信息
        Apple b = a;    // ②
        // b 只知道size 的类型是 Number 
        Number size1 = b.getSize();
        // 下面的代码引起编译错误
        // Integer size2 = b.getSize();   ③
    }
}
// 程序说明:
//     1. 上面的程序定义了带泛型声明的Apple类, 其类型形参的上限是Number, 这个类型形参用来定义Apple类的
//         size变量; 
//     2. 程序在①处的代码创建了一个Apple对象, 该Apple对象传入了Integer作为类型形参的值, 所以调用 a 的
//         getSize() 方法时返回Integer类型的值; 
//     3. 当把 a 赋给一个不带泛型信息的b变量时, 编译器会丢失a对象的泛型信息, 即所有尖括号里的信息都会
//         丢失--因为Apple的类型形参的上限是Number类, 所以编译器依然知道 b 的 getSize() 方法返回
//         Number 类型, 但具体是Number的那个子类就不知道了
//例子2:
public class ErasureTest2{
    public staitc void main(String[] args){
        List<Integer> li = new ArrayList<>();
        li.add(6);
        li.add(9);
        List list = li;
        // 下面的代码引起 "未经检查的转换" 警告, 编译, 运行时完全正常
        List<String> ls = list;   // ① 
        // 但是只要访问ls里的元素, 下面代码将引起运行时异常
        System.out.println(ls.get(0));
    }
}
// 程序说明:
//     1. 上面程序中定义了一个List<Integer>对象, 这个List对象保留了集合元素的类型信息, 当把这个List对
//         象赋给另外一个List类型的list后, 编译器就会丢失前者的泛型信息, 即丢失list集合里元素的类型信
//         息, 这就是典型的擦除
//     2. Java又允许直接把List对象赋给一个List<Type> (Type可以是任何类型)类型的变量, 所以程序再①处编
//         译通过, 只发出"未经检查的转换" 警告
//     3. 但对list变量实际上引用的是List<Integer>集合, 所以试图把该集合里的元素当成String类型的对象取
//         出时, 将引发运行时异常

6. 泛型与数组

  1. 如果一段代码在编译时没有提出 "[unchecked]"未经检查的转换警告, 则程序在运行时不会引发 ClassCastException 异常;
  2. 所以数组元素的类型不能包含类型变量或类型形参, 除非是无上限的类型通配符; 但是可以声明元素类型包含类型变量或类型形参的数组即: 只能声明 List[]形式的数组, 不能创建 ArrayList[10]这样的数组

# Java