Java泛型入门到稍微懂

421 阅读22分钟

前言:

Java泛型还是比较难的,而且很重要;但是由于小白平时没怎么用/(ㄒoㄒ)/~~,所以一直都是一知半解;因此下定决心,咬牙切齿(有点夸张了,嘿嘿~~)专门抽了个时间根据Java核心卷和疯狂讲义,做了个笔记;不对或不合理的地方希望大家踊跃指出😝;

1.泛型入门

  • 起因:集合的缺点:某个对象保存在集合后,全都按照Object类型处理。泛型程序设计 ( Generic programming ) 意味着编写的代码可以被很多不同类型的对象所重用。
  • 没有泛型之前Java,Java中泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组。但是存在两个问题:①获取一个值时必须进行强制类型转换;②由于没有进行错误检查,可以添加任何类的对象

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

  • 无泛型时引发的问题:
//例子①:
import java.util.ArrayList;
import java.util.List;

public class ListErr {
    public static void main(String[] args){
        //创建一个只想保存字符串的List集合
        List strList = new ArrayList();
        //           1 234567 8
        strList.add("疯狂Java讲义");
        strList.add("疯狂Android讲义");

        //"不小心"往strList中add了一个Integer对象
        strList.add(5);
        strList.forEach(str -> System.out.println(((String)str).length())); //抛出ClassCastException,由于Integer转String
    }
}

1.2使用泛型

import java.util.ArrayList;
import java.util.List;

public class GenericList {
    //泛型:“参数化类型”    parameterized type
    public static void main(String[] args){
        //创建一个只想保存字符串的List集合,此处声明的List是带有类型参数的泛型接口,所以只能add,List声明的类型,add其他类型会发生编译异常
        List<String> strList = new ArrayList<>();
        //           0 123456 7
        strList.add("疯狂Java讲义");
        strList.add("疯狂Android讲义");
        //下面代码将引起编译错误
        strList.add(5);
        strList.forEach(str -> System.out.println(str.length()));
    }
}

1.3Java9增强的“菱形”语法

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Java 7的"菱形"语法: 省略new对象时<>中的类型参数
 */
public class DiamondTest {
    public static void main(String[] args){
        //Java 自动推断出ArrayList的<>中应该是String
        List<String> strList = new ArrayList<>();
        strList.add("hello");
        strList.add("LazyGoal");
        //遍历strList集合,集合元素就是String类型
        strList.forEach(temp -> System.out.println(temp));
        //Java自动推断出HashMap的<>里应该是String, List<String>
        Map<String, List<String>> schoolInfo = new HashMap<>();
        //Java自动推断出ArrayList的<>里应该是String
        List<String> schools = new ArrayList<>();
        schools.add("斜月三星洞");
        schools.add("西天取经路");
        schoolInfo.put("孙悟空", schools);
        //遍历Map时,Map的key是String类型,value是List<String>类型
        schoolInfo.forEach((key, value) -> System.out.println(key + "->" + value));
    }
}
/**
 * Java 9的“菱形”语法:允许创建匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型
 */
public class AnnoymousTest {
    public static void main(String[] args){
        //指定Foo类中泛型为String
        Foo<String> f = new Foo<>(){
            //test()方法的参数类型为String
            public void test(String t){
                System.out.println("test方法的t参数为:" + t);
            }
        };

        //使用泛型通配符,此时相当于通配符的上限为Object
        Foo<?> fo = new Foo<>(){
            //test()方法的参数类型为Object
            public void test(Object t){
                System.out.println("test方法的Object参数为:" + t);
            }
        };

        //使用泛型通配符,通配符的上限为Number
        Foo<? extends Number> fn = new Foo<>(){
            //此时test()方法的参数类型为Number
            public void test(Number t){
                System.out.println("test方法的Number参数为:" + t);
            }
        };
    }
}

2.深入泛型

  • 泛型:定义类、接口、方法时使用类型参数,而这个类型参数(泛型)会在声明变量、创建对象、调用方法时动态地指定(也就是传入实际的类型参数,也可称为类型实参)。泛型的形参在整个接口、类体内可当成类型使用,几乎所有可以使用普通类型的地方都可以使用这种泛型形参;简而言之,一个泛型类( generic class ) 就是具有一个或多个类型变量的类;换句话,泛型类可看作普通类的工厂;也就是编写的代码可以被很多不同类型的对象所重用;
  • 注释:类型变量使用大写形式,且比较短,这是很常见的;在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”;
  • **其中类型参数的使得程序有更好的可读性和安全性;**因为出现编译错误比类在运行时出现强制转换类型异常要好得多;

2.1定义泛型接口、类

/**
 * 可为任何类、接口增加泛型声明;
 * 定义Apple类时使用了泛型声明
 * @param <T>
 */
public class Apple<T> {
    //使用T类型定义实例变量
    private T info;
    public Apple(){}

    /**
     * 创建带泛型的自定义类时,写构造方法时方法名只写类名,不写类名后边的<>和其他的,而调用该构造方法时可以使用类名<>
     * @param info
     */
    //下面方法中使用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){
        /**
         * 使用Apple<T>类时可为T形参传入实际类型,从而可以生成多个逻辑子类;如:Apple<String>,Apple<Double>……
         */
        //由于传给T形参的是String,因此构造器参数只能是String
        Apple<String> a1 = new Apple<>("Apple");
        System.out.println(a1.getInfo());
        //由于传给T形参的是Double,因此构造器参数只能是Double或double
        Apple<Double> a2 = new Apple<>(5.67);
        a2.setInfo(1.112);
        System.out.println(a2.getInfo());
        //其实好像不传类型参数也行,其中的泛型类型都按照Object算
        Apple a3 = new Apple("1");
        a3.setInfo(1.0);
        System.out.println(a3.getInfo());
    }
}

2.2从泛型类派生子类

  • 带有泛型声明的接口、父类后,有子类想实现或继承时,可以有两种情况:①接口或父类的类型参数必须被传入;②接口或父类的泛型<>不被写在实现或继承语句中(也就是省略泛型的形式,被称为原始类型raw type)。如以下代码:
情况一:
public class A extends Apple<String> implements Foo<String> {
    @Override
    public void test(String o) {

    }
}
此种情况下,子类重写父类方法时必须保持和父类的泛型类型的一直(此泛型有具体的类型)
public class A1 extends Apple<String> {
    //正确重写了父类的方法,返回值
    //与父类Apple<String>的返回值完全相同
    public String getInfo(){
        return "sonOfClass" + super.getInfo();
    }
    //下面方法是错误的,重写父类的方法时返回值类型不一致
//    public Object getInfo(){
//        return "sonOfClass";
//    }
}

情况二:
public class A extends Apple implements Foo {
    @Override
    public void test(Object o) {
        
    }
}
public class A2 extends Apple {
    //重写父类的方法
    public String getInfo(){
        //super.getInfo()返回值是Object,所以需要用toString
        return super.getInfo().toString();
    }
}

2.3 并不存在泛型类

import java.util.ArrayList;
import java.util.List;

public class R<T> {
    //错误实例,不能在静态变量声明中使用泛型形参,原因请参见下面说明
    static T info;
    T age;
    public void foo(T msg){

    }
    //错误实例,不能在静态方法声明中使用泛型形参,原因请参见下面说明
    public static void bar(T msg){

    }

    /**
     * 不管泛型形参传入的是哪一种类型实参,对Java,它们依然被当成同一个类处理,内存中也只占用一块内存空间;
     * 因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参
     * 原因:①泛型在对象创建时才知道类型,而静态方法属于类,类在编译阶段就存在,所以虚拟机根本不知道静态***中引用的泛型的类型
     *     ②初始化时:对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等,所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西
     * @param args
     */
    public static void main(String[] args){
        //创建List<String>对象和List<Integer>对象
        List<String> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        //比较list1对象和list2对象
        System.out.println(list1.getClass() == list2.getClass());   //true

//        java.util.Collection<String> cs = new java.util.ArrayList<>();
        //系统中并不会真正的生成泛型类,因此instanceof运算符后不能使用泛型类
//        if(cs instanceof java.util.ArrayList<String>){ }
    }
}

3.类型通配符

  • 首先得解释一下什么是通配符,在维基百科中是这么定义的:在计算机(软件)技术中,通配符可用于代替单个或多个字符;也就是通配符类型中,允许类型参数变化;通常地,星号“*”匹配0个或以上的字符,问号“?”匹配1个字符。其中T,E,K,V,有什么区别么?本质上没区别,只是有某些使用情况和约定而已。如:

?表示不确定的Java类型 T (type) 表示具体的一个java类型 K V (key value) 分别代表java键值中的Key Value E (element) 代表Element

①Java泛型中,如果Foo是Bar的某个子类(或子接口),而G是具有泛型声明的类或接口,但G<Foo>不是G<Bar>的子类型~
②但是在数组中可以直接把一个Integer[]数组赋值给一个Number[]变量,若Double变量赋值给刚刚的Number[]数组,虽然编译可通过,但是运行时会抛出ArrayStoreException异常。
③Java泛型的设计原则:代码编译时没有警告,就不会遇到运行时ClassCastException异常;
总结:若Foo是Bar的一个子类(或子接口),Foo[]依然是Bar[]的子类型(这种情况称之为型变);而G<Foo>不是G<Bar>的子类型;Java数组支持型变,而集合不支持型变;
参考笔记:聊一聊-JAVA 泛型中的通配符 T,E,K,V,? - 掘金

3.1使用类型通配符

  • 类型通配符:是一个问号(?);
  • 格式:接口和类的名称(它们都支持泛型声明)<?>;
  • 用途:表示各种泛型类型***的父类;
  • List<?>:将一个问号作为类型实参传递给List集合,也就是元素类型未知的List;此问号(?)是通配符,它的元素类型可以匹配任何类型。
//现在使用任何类型的List调用此方法都可以,因为此方法的形参的类型是Object
public void test(List<?> c){
    for(int i = 0; i < c.size(); ++i)
        System.out.println(c.get(i));
}

//带通配符的List仅表示它是各种泛型List的父类,不能把元素加入到其中
List<?> c = new ArrayList<String>();
//下一行代码引起编译错误,原因:程序无法确定c集合中元素的类型,但是null可以add,因为null是所有引用类型的实例
c.add(null);
  • 注意:Pair<?>和Pair是有很大的区别的!!!

Pair<?>的setFirst()和getFirst()方法分别可以看成:? getFirst()和void setFirst(?);getFirst的返回值只能赋值给Object,而setFirst方法不能被调用(或仅仅可用null调用);

3.2设定类型通配符的上限

如果希望List<?>不是所有泛型List的父类,只是某一类泛型List的父类。如以下例子;List<? extends Shape>:是受限制的通配符类型,此处?号代表未知的类型,但是这个未知类型必须是Shape的子类型(也可以是Shape本身),因此可以把Shape视为这个通配符的上限(upper bound);由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中;

上界通配符<? extends E>: 上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。 在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:①如果传入的类型不是 E 或者 E 的子类,编译不成功;②泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用; 类型参数列表中如果有多个类型参数上限,用逗号分开;

        Pair<Manager> managerPair = new Pair<>();
        Pair<? extends Employee> employeePair = managerPair;    //OK
        //getFirstNumber函数可以使用,但是setFirstNumber函数不能使用,保证了安全性;
        System.out.println("firstNumber:" + employeePair.getFirstNumber()); //null
//        employeePair.setFirstNumber(1);   //compile-time error,因为setFirstNumber函数中编译器只知道需要这个类型(? extends Employee)的变量,但不知道具体类型,所以会拒绝任何特定的类型
/**
 * 定义一个抽象类Shape
 */
public abstract class Shape {
    public abstract void draw(Canvas canvas);
}

/**
 * 定义一个抽象类Shape
 */
public class Circle extends Shape {
    //实现画图方案,以打印字符串来模拟画图方法实现
    public void draw(Canvas canvas){
        System.out.println("在画布" + canvas + "上画个圆");
    }
}

/**
 * 定义Shape的子类Rectangle
 */
public class Rectangle extends Shape {
    //实现画图方法,以打印字符串来模拟画图方法实现
    public void draw(Canvas canvas){
        System.out.println("把一个矩形画在画布" + canvas + "上");
    }
}

public class Canvas {
    //同时在画布上绘画多个形状
    public void drawAll(List<? extends Shape> shapeList){
        for(Shape shape: shapeList)
            shape.draw(this);
    }
    public void addRectangle(List<? extends Shape> shapes){
         //下边代码引起编译错误
//      shapes.add(0, new Rectangle());
    }
    public static void main(String[] args){
        List<Circle> circles = new ArrayList<>();
        Canvas c = new Canvas();
        c.drawAll(circles);
    }
}

3.3设定类型通配符的下限(通配符超类型限定)

  • 通配符下限(通配符的超类型限定):<? super 类型>;
  • 用途:支持类型型变,逆变;

下界通配符 < ? super E> 下届:用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object;简而言之,此通配符的限制为**类型的所有超类型; 在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类;

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);
        //此处可准确地知道最后一个被复制的元素是Integer类型
        //与src集合元素的类型相同
        Integer last = copy(ln, li);
        System.out.println(ln);
    }
}
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 hashCode() > snd.hashCode()? 1: 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);
    }
}

?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行; 请看下边经典例子:

        /*
        通配符的超类限定:**此时可以为方法提供参数,但不能使用方法返回值**;请看以下例子:
         */
        Pair<Employee> employeePair = new Pair<>();
        Pair<? super Manager> managerPair = employeePair;

        //get方法的返回值不能保证返回对象的类型,只能把它赋给一个Object
        Object manager = managerPair.getFirstNumber();
//        Manager manager2 = managerPair.getFirstNumber();  //error
//        Employee manager3 = managerPair.getFirstNumber();
        System.out.println(managerPair.getFirstNumber());
        //编译器无法知道setFirst方法的具体类型,
        //因此调用这个方法时不能接受类型为Employee或Object的参数,
        //只能传递Manager类型对象,或它的某个子类型对象;
        managerPair.setFirstNumber(new Manager());
//        managerPair.setSecondNumber(new Employee());
  • 总结:带有超类限定的通配符可以向泛型对象写入,带有子类限定的通配符可以从泛型对象读取;

3.4设定泛型形参的上限

/**
 * java中使用通配符形参时可以设定上限,同时也可以在定义泛型形参时设定上限,泛型形参的上限设定好之后
 * 表示该泛型形参的实际类型或是该上限类型,或是该上限类型的子类
 * @param <T>
 */
public class Apple2<T extends Number> {
    T col;
    public static void main(String[] args){
        Apple2<Integer> ai = new Apple2<>();
        Apple2<Double> ad = new Apple2<>();
        //下面代码将引发编译异常,下面代码试图把String类型传给T形参
        //但String不是Number的子类型,所以引起编译错误
//        Apple2<String> as = new Apple<>();
    }
}
//泛型形参指定多个上限时,所有的接口上限必须位于类上限之后
//表明T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Apple3<T extends Number & java.io.Serializable> {

}

3.5通配符捕获

  • 请看通配符操作函数例子:
/**
     * 交换Pair对象的两个属性的方法
     * @param pair
     */
    public static void swap(Pair<?> pair){
        //很明显这方式不行,因为没有?类型,但可以用以下函数解决
//        ? t = pair.getFirstNumber();
//        pair.setFirstNumber(pair.getSecondNumber());
//        pair.secondNumber(t);
        swapHelper(pair);
    }

    /**
     * 交换Pair对象的两个方法,此方法的参数T捕获通配符;
     * @param pair
     * @param <T>
     */
    public static <T> void swapHelper(Pair<T> pair){
        T t = pair.getFirstNumber();
        pair.setFirstNumber(pair.getSecondNumber());
        pair.setSecondNumber(t);
    }
  • 通配符捕获只有在有许多限制的情况下才是合法的;编译器必须能确定通配符表达式的是单个、确定的类型;如:ArrayList<Pair>中的T永远不能捕获ArrayList<Pair<?>>中的通配符;

4.泛型方法

4.1泛型方法

我们先定义一个带类型参数的简单方法

public class ArrayAlg {
    public static <T extends Comparable> Pair<T> minMax(T[] a){
        if(a == null || a.length == 0)
            return null;
        T min = a[0];
        T max = a[0];
        for(int i = 1; i < a.length; ++i){
            if(min.compareTo(a[i]) > 0)
                min = a[i];
            if(max.compareTo(a[i]) < 0)
                max = a[i];
        }
        return new Pair<T>(min, max);
    }

    /**
    * 此方法中使用<T extends Comparable>限制了类型变量T只能被实现了Comparable接口的类(String、LocalDate等);
    * 当然一个类型变量或通配符可以有多个限定:<T extends Comparable & Serializable>,其中限定类型用"&"分隔;
    * 而逗号用来分隔类型变量;
    * @param a
    * @param <T>
    * @return
    */
    public static <T extends Comparable> T min(T[] a){
        if(a == null || a.length == 0)
            return null;
        T smallest = a[0];
        for(int i = 1; i < a.length; ++i)
          if(smallest.compareTo(a[i]) > 0)
                smallest = a[i];
        return smallest;
    }

    /**
     * 泛型方法,可在普通类中,也可在泛型类中;
     * 其中类型变量放在修饰符后,返回类型前;
     * @param a
     * @param <T>
     * @return
     */
    public static <T> T getMiddle(T[] a){
        return a[a.length / 2];
    }

    public static void main(String[] args){
        //当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型;在当前情况下,可省略<String>类型参数;
        //因为编译器可以自行判断出调用的方法;
        String middle = ArrayAlg.<String>getMiddle(new String[]{"a", "b", "c", "d", "e", "f"});
        String middle2 = ArrayAlg.getMiddle(new String[]{"a", "b", "c", "d", "e", "f"});
        System.out.println("middle:" + middle);
        Double middle3 = ArrayAlg.<Double> getMiddle(new Double[]{3.14, 1729.0, 1.0});
        System.out.println("middle3:" + middle3);
        
        String[] temp = new String[]{"a", "b", "d", "o", "p", "q"};
        String min = ArrayAlg.<String>minMax(temp).getFirstNumber();
        String max = ArrayAlg.<String>minMax(temp).getSecondNumber();
        System.out.println("min:" + min);
        System.out.println("max:" + max);
    }
}

5.泛型代码和虚拟机

虚拟机没有泛型类对象:所有对象都属于普通类;

5.1类型擦除

  • 原始类型(raw type):原始类型名是删除类型参数后的泛型类型名;擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object);若类型变量有多个限定,原始类型用第一个限定的类型变量替换(若没有给限定的类型用Object替换);它是最后在字节码中类型变量的真正类型,无论何时定义泛型,相应的原始类型都会被自动提供;

Java的泛型是伪泛型,因为Java在编译期间,所有的泛型信息都会被擦掉;Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除

//public class Pair<T> {
//    private T firstNumber;
//    private T secondNumber;
//
//    public Pair(T firstNumber, T secondNumber){
//        this.firstNumber = firstNumber;
//        this.secondNumber = secondNumber;
//    }
//
//    public Pair(){
//        firstNumber = null;
//        secondNumber = null;
//    }
//
//    public T getFirstNumber(){
//        return firstNumber;
//    }
//
//    public void setFirstNumber(T firstNumber){
//        this.firstNumber = firstNumber;
//    }
//
//    public T getSecondNumber(){
//        return secondNumber;
//    }
//
//    public void setSecondNumber(T secondNumber){
//        this.secondNumber = secondNumber;
//    }
//
//    public static void main(String[] args){
//        Pair<String> pair = new Pair<>("wwf", "fsy");
//        System.out.println("husband:" + pair.getFirstNumber());
//        System.out.println("wife:" + pair.getSecondNumber());
//    }
//}
//---对应的原始类型为---
public class Pair{
    private Object firstNumber;
    private Object secondNumber;

    public Pair(){}

    public Pair(Object firstNumber, Object secondNumber){
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    public Object getFirstNumber(){
        return firstNumber;
    }

    public void setFirstNumber(Object firstNumber){
        this.firstNumber = firstNumber;
    }

    public Object getSecondNumber(){
        return secondNumber;
    }

    public void setSecondNumber(){
        this.secondNumber = secondNumber;
    }
}
//public class Interval <T extends Comparable & Serializable> implements Serializable{
//    private T lower;
//    private T upper;
//
//    public Interval(){}
//    public Interval(T lower, T upper){
//        if(lower.compareTo(upper) <= 0){
//            this.lower = lower;
//            this.upper = upper;
//        }else {
//            this.lower = upper;
//            this.upper =  lower;
//        }
//    }
//}
//对应的原始类型
public class Interval implements Serializable{
    private Comparable lower;
    private Comparable upper;

    public Interval(){}
    //...
}

5.2翻译泛型表达式

程序调用泛型方法时,若擦除返回类型,编译器插入强制类型转换;

//Pair<Employee> buddies = ...;
//Employee buddy = buddies.getFirst();
//其中编译器可以把这个方法调用翻译为两条虚拟机指令:①对原始方法Pair.getFirst的调用;②把返回的Object类型强制转换为Employee类型;
//存取泛型域时也要插入强制类型转换;
//Employee buddy = buddies.first;(若first域是公有的)

5.3翻译泛型方法

  • 当然类型擦除也会出现在泛型方法中;
//    public static <T extends Comparable> T min(T[] a){
//        //...
//    }
    //擦除类型之后
    public static Comparable min(Comparable[] a){
        //...
    }
  • 类型擦除引起的问题及解决方法可参考笔记:Java泛型类型擦除以及类型擦除带来的问题 - 蜗牛大师 - 博客园
  • 总结:
    • 虚拟机中没有泛型,只有普通的类和方法;
    • 所有的类型参数都用它们的限定类替换;
    • 桥方法被合成来保持多态;
    • 为保持类型安全性,必要时插入类型转换;

5.4调用遗留代码

  • 设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作;……

6.约束与局限

6.1不能用基本类型实例化类型参数

  • 由于类型擦除的原因,Java泛型中不能用基本类型实例化类型参数;
List<Double> list = new ArrayList<>();
//编译不通过
List<double> list2 = new ArrayList<>();
//主要原因是类型擦除之后List类有Object类型的域,Object可以存储Double引用,而不能存储double的值;

6.2运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型;因此,所有的类型查询只产生原始类型;

//检查a是否是任意类型的一个Pair
//  if(a instanceof Pair<String>)   //Error
//  if(a instanceof Pair<T>)
    //同样也可用强制类型转换检查
//  Pair<String> p = (Pair<String>)a;

Pair<String> stringPair = new Pair<>();
Pair<Integer> integerPair = new Pair<>();
//getClass方法返回原始类型
if(stringPair.getClass() == integerPair.getClass())
    System.out.println("res: true");

6.3不能创建参数化类型的数组

  • 若想收集参数化类型的对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair>;
Pair<String>[] nums = new Pair<String>[10];   //Error
Pair<String>[] table =(Pair<String>[]) new Pair<?>[10]; //可以创建但是不安全

6.4Varargs警告

6.5不能实例化类型变量

6.6不能构造泛型数组

类似于不能实例化一个泛型实例,也不能实例化数组;

//ArrayList的最基本实现方式:Object
public class ArrayListW<E> {
    private Object[] elements;
    public ArrayListW(){
        elements = (E[])new Object[10];
    }

    public static void main(String[] args){
        List<String> list = new ArrayList<>();
    }
}

6.7泛型类的静态上下文中类型变量无效

  • 不能在静态域或方法中引用类型变量;下面的代码是错误的:
public class Singleton<T> {
    private static T singleInstance;
    public static T getSingleInstance(){
        if(singleInstance == null)
            return singleInstance;
    }
}

6.8不能抛出或捕获泛型类的实例

  • 即不能抛出也不能捕获泛型类对象;泛型类扩展Throwable都是不合法的;但是异常规范中使用类型变量是允许的;如以下代码:
public class Problem<T> extends Exception { //Error---can't extend Throwable
    public static <T extends Throwable> void doWork(Class<T> tClass){
        try{
            //do work
        }catch (T e){   //Error--can't catch type variable
            Logger.global.info("...");
        }
    }

    public static <T extends Throwable> void doWorkTwo(T t) throws T{   //OK
        try{
            //do work
        }catch (Throwable realCause){
            t.initCause(realCause);
            throw t;
        }
    }
}

6.9可以消除对受查异常的检查

  • 其实可以利用泛型消除,受查异常需要处理器的限制;也就是可把受查异常”包装“成非受查异常;其实是”哄骗“编译器,让它把受查异常认为是非受查异常;
/**
 * 此类的设计目的是:
 * 用户可覆盖body方法提供一个具体的动作;调用toThread时,会得到Thread类的一个对象,而它的run方法不会介意受查异常;
 */
public abstract class Block {
    public abstract void body() throws Exception;

    public Thread toThread(){
        return new Thread(){
            public void run(){
                try{
                    body();
                }catch (Throwable t){
                    Block.<RuntimeException>throwAs(t);
                }
            }
        };
    }
    @SuppressWarnings("unchecked")
    public static <T extends Throwable> void throwAs(Throwable e)throws T{
        throw (T) e;
    }
}

6.10注意擦除后的冲突

  • 泛型类型被擦除后,有可能会发生冲突;如下代码中的equals方法:
public class Pair<T> {
    private T firstNumber;
    private T secondNumber;

    public Pair(T firstNumber, T secondNumber){
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    public Pair(){
        firstNumber = null;
        secondNumber = null;
    }

    public T getFirstNumber(){
        return firstNumber;
    }

    public void setFirstNumber(T firstNumber){
        this.firstNumber = firstNumber;
    }

    public T getSecondNumber(){
        return secondNumber;
    }

    public void setSecondNumber(T secondNumber){
        this.secondNumber = secondNumber;
    }

    //Error:
    //'equals(T)' in 'genericity.Pair' clashes with 'equals(Object)' in 'java.lang.Object';
    // both methods have same erasure, yet neither overrides the other
    //当然,补救方式肯定是重新命名此方法~~~
    public boolean equals(T value){
        return firstNumber.equals(value) && secondNumber.equals(value);
    }

    public static void main(String[] args){
        Pair<String> pair = new Pair<>("wwf", "fsy");
        System.out.println("husband:" + pair.getFirstNumber());
        System.out.println("wife:" + pair.getSecondNumber());
    }
}

泛型规范提到的另一个原则:想支持擦除的转换,需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化;

7.泛型类型的继承规则

  • 无论S和T有什么联系,通常Pair<S>和Pair<T>没有什么联系!!!

虽然这限制过于严苛,但对于类型安全非常有必要;因为子类对象赋值给父类引用后,对其可修改的话可能会出现安全问题;具体看如下代码:

Pair<Manager> managerPair = new Pair<>(ceo, cfo);
Pair<Employee> employeePair = managerPair;  //illegal, but suppose it wasn't
employeePair.setFirstNumber(lowlyEmployee);
  • 这里必须要把泛型和Java数组间的区别理解清楚;因为Java数组中支持子类型的数组赋值给父类型的数组;原因是数组有特殊保护,存储不符合的值会抛出ArrayStoreException;
  • 永远都可以把参数化类型转换成一个原始类型(如下代码);在与遗留代码衔接时这个很重要;
Pair<Integer> pair = new Pair<>();
Pair rawPair = pair;
rawPair.setFirstNumber(1);
System.out.println(rawPair.getFirstNumber());
  • 泛型类可以扩展或实现其他的泛型类