阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)

我躲在夜里取笑着黑,因为没有人能杀死鬼——草动没有派对《鬼》
这一章真的是看了蛮久的,一方面是最近事情比较多,另外一方面泛型这一部分自己不太熟悉,阅读理解起来比较困难。

Java核心技术Ⅰ

  • 第八章
    • 8.1 为什么要使用泛型程序设计
      • 8.1.1 类型参数的好处
      • 8.1.2 谁想称为泛型程序员
    • 8.2 定义简单泛型类
    • 8.3 泛型方法
    • 8.4 类型变量的限定
    • 8.5 泛型代码和虚拟机
      • 8.5.1 类型擦除
      • 8.5.2 翻译泛型表达式
      • 8.5.3 翻译泛型方法
      • 8.5.4 调用遗留代码
    • 8.6 约束与局限性
      • 8.6.1 不能用基本类型实例化类型参数
      • 8.6.2 运行时类型查询只适用于原始类型
      • 8.6.3不能创建参数化类型的数组
      • 8.6.4 Varargs警告
      • 8.6.5 不能实例化类型变量
      • 8.6.6 不能构造泛型数组
      • 8.6.7 泛型类的静态上下文中类型变量无效
      • 8.6.8 不能抛出或捕获泛型类的实例
      • 8.6.9 可以消除对受查异常的检查
      • 8.6.10 注意擦除后的冲突
    • 8.7 泛型类型的继承规则
    • 8.8 通配符类型
      • 8.8.1 通配符概念
    • 8.8.2 通配符的超类型限定
      • 8.8.3 无限定通配符
      • 8.8.4 通配符捕获
    • 8.9 反射和泛型
      • 8.9.1 泛型 Class 类
      • 8.9.2 使用 Class 参数进行类型匹配
      • 8.9.3 虚拟机中的泛型类型信息
      • 8.9.4 类型字面量

第八章

8.1 为什么要使用泛型程序设计

泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。

8.1.1 类型参数的好处

在 Java 中增加范型类之前, 泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组,没有错误检查,可以向数组列表中添加任何类的值。
泛型提供了一个更好的解决方案: 类型参数( type parameters)。ArrayList 类有一个类型参数用来指示元素的类型:

ArrayList<String> files = new ArrayList<String>():

这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。而且对插入操作有了类型检查,对于读取操作不需要强制类型转换,类型参数的魅力在于:使得程序具有更好的可读性和安全性。

8.1.2 谁想称为泛型程序员

一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。
程序员可能想要将ArrayList 中的所有元素添加到ArrayList 中去。然而, 反过来就不行,了。如果只能允许前一个调用, 而不能允许后一个调用呢? Java语言的设计者发明了一个具有独创性的新概念,通配符类型( wildcard type), 它解决了这个问题。通配符类型非常抽象,然而,它们能让库的构建者编写出尽可能灵活的方法。

8.2 定义简单泛型类

一个泛型类( generic class) 就是具有一个或多个类型变量的类。
如下的一个BookPair类,他只有一个类型变量T,放在类名的后面。

public class BookPair<T> {
    private T first;
    private T second;

    public BookPair() {
    }

    public BookPair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

类型变量使用大写形式,且比较短, 这是很常见的。在 Java 库中, 使用变量 E 表示集合的元素类型, K 和 V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的字母 U 和 S) 表示“ 任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型, 例如:

BookPair<String>

可以将结果想象成带有构造器的普通类:

Pair<String>()
Pair<String>(String, String)//两个类型变量

换句话说,泛型类可看作普通类的工厂
下面的程序使用了 Pair 类。静态的 minmax 方法遍历了数组并同时计算出最小值和最大值。它用一个 Pair 对象返回了两个结果。 回想一下 compareTo 方法比较两个字符串, 如果字符串相同则返回 0 ; 如果按照字典顺序, 第一个字符串比第二个字符串靠前, 就返回负值, 否则, 返回正值。

 public static BookPair<String> minmax(String[] a){
        if (a==null||a.length==0)return null;
        String min=a[0];
        String max=a[0];
        for (String s:a){
            if (min.compareTo(s)>0) min=s;
            if (max.compareTo(s)<0)max=s;
        }
        return new BookPair<>(min,max);
    }

8.3 泛型方法

实际上,还可以定义一个带有类型参数的简单方法。

public static <T> T getMiddle(T...a){
        return a[a.length/2];
}

注意,类型变量放在修饰符(这里是 public static) 的后面,返回类型的前面。
当调用一个泛型方法时在方法名前的尖括号中放人具体的类型:

String middle = ArrayAlg.<String>getMiddle("]ohnM", "Q", "Public");

在这种情况(实际也是大多数情况)下,方法调用中可以省略 类型参数。编译器有足够的信息能够推断出所调用的方法。它用 names 的类型(即 String[ ]) 与泛型类型 T[ ]进行匹配并推断出 T 一定是 String。也就是说,可以调用

String middle = ArrayAlg.getHiddle("]ohn", "Q.", "Public");

几乎在大多数情况下,对于泛型方法的类型引用没有问题。

8.4 类型变量的限定

我们要计算数组中的最小元素:

public static <T> T min(T...a){
        if (a==null||a.length==0)return null;
        T smallest=a[0];
        for (T s:a){
            if (smallest.compareTo(s)>0)smallest=s;
        }
        return smallest;
    }

但是,这里有一个问题。请看一下 min方法的代码内部。 变量 smallest 类型为 T, 这意味着它可以是任何一个类的对象。怎么才能确信 T 所属的类有 compareTo 方法呢?
解决这个问题的方案是将 T 限制为实现了 Comparable 接口(只含一个方法 compareTo 的标准接口)的类。可以通过对类型变量 T 设置限定(bound) 实现这一点

public static <T extends Comparable> T min(T...a){
        if (a==null||a.length==0)return null;
        T smallest=a[0];
        for (T s:a){
            if (smallest.compareTo(s)>0)smallest=s;
        }
        return smallest;
    }

记住Bound限定。
在此为什么使用关键字 extends 而不是 implements ? 毕竟,Comparable 是一个接口。下面的记法

<T extends Comparable>

表示 T 应该是绑定类型的子类型 (subtype)。 T 和绑定类型可以是类, 也可以是接口。选择关键字 extends 的原因是更接近子类的概念。
一个类型变量或通配符可以有多个限定, 例如:

T extends Comparable & Serializable

限定类型用“ &” 分隔,而逗号用来分隔类型变量。在 Java 的继承中, 可以根据需要拥有多个接口超类型但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个
用加了限定符的泛型类型重新实现maxmin方法。

public static <T extends Comparable> BookPair<T> minmax(T[] a){
        if (a==null||a.length==0)return null;
        T min=a[0];
        T max=a[0];
        for (T s:a){
            if (min.compareTo(s)>0) min=s;
            if (max.compareTo(s)<0)max=s;
        }
        return new BookPair<>(min,max);
    }

8.5 泛型代码和虚拟机

下面主要介绍编译器是如何擦除泛型的,并且这个过程对程序员有什么影响。

8.5.1 类型擦除

无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型 ( raw type )。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased) 类型变M, 并替换为限定类型(无限定的变量用 Object)。
BookPair类的原始类型如下

public class BookPair<T> {
    private T first;
    private T second;

    public BookPair() {}
    public BookPair(T first, T second) {
        this.first = first;
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public T getSecond() {
        return second;
    }
    public void setSecond(T second) {
        this.second = second;
    }
}

因为 T 是一个无限定的变量, 所以直接用 Object 替换。在程序中可以包含不同类型的 Pair, 例 如, Pair 或 Pair。 而擦除类型后就变成原始的 Pair 类型了。原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用 Object 替换。例如, 类 Pair 中的类型变量没有显式的限定, 因此, 原始类型用 Object 替换 T。假定声明了一个不同的类型。
例如假如声明了一个不同的类型
就会将所有的T类型转换为Comparable。
如果是
就会将所有的T类型转换为Serializable。

8.5.2 翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型, 编译器插入强制类型转换。例如,下面这个语句序列:

Pair<Employee> buddies = . . .;
Employee buddy = buddies.getFirst();

擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:

  1. 对原始方法 Pair.getFirst 的调用。
  2. 将返回的 Object 类型强制转换为 Employee 类型。

8.5.3 翻译泛型方法

类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法

 public static <T extends Comparable> BookPair<T> minmax(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

 public static BookPair<Comparable> minmax(Comparable[] a)

方法的擦除带来了两个复杂问题。看一看下面这个示例:

class DateInterval extends BookPair<LocalDate>{
    public void setSecond(LocalDate second){
        if (second.compareTo(getFirst())>0){
            super.setSecond(second);
        }
    }
    //...
}

继承后重写的setSecond方法类型擦除之后就变成了如下所示:

class DateInterval extends BookPair{
    public void setSecond(LocalDate second){
        if (second.compareTo(getFirst())>0){
            super.setSecond(second);
        }
    }
    //...
}

这个时候还有一个从父类继承过来的setSecond方法:

public void setSecond(Object second)

当泛型擦除之后显然这是两个方法,他们有不同的类型参数,一个是LocalDate超类则是Object,但是不应该不一样,因为这里想要实现的是对父类方法的重写,但是类型擦除之后变成了方法的重载,显然我们并不想要方法的重载而是方法的重写例如下面:

Datelnterval interval = new Datelnterval(. . .);
Pair<Loca1Date> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);

这里父类型Pair中的泛型类型参数是LocalDate,当子类对象引用赋给超类变量的时候,调用setSecond方法希望调用是子类中的setSecond方法,但是类型擦除之后我们调用不到,因为类型擦除之后,这个方法并没有覆盖父类中的方法,属于子类独有的方法所以父类的变量是调用不到的。就是类型擦除与多态产生了冲突。要解决这个问题, 就需要编译器在 Datelnterval 类中生成一个桥方法(bridge method):

public void setSecond(Object second) { setSecond((Date) second);}

要想了解它的工作过程,请仔细地跟踪下列语句的执行:

pair.setSecond(aDate);

变量Pair已经声明为LocalDate类型了,当调用setSecond方法的时候,因为他引用的对象是DateInterval的对象,所以调用的是DateInterval.setSecond(Obect),这个方法是合成的桥方法,他又会调用DateInterval.setSecond(Date),这是我们期望的效果。
桥方法可能会变得十分奇怪。假设 Datelnterval 方法也覆盖了 getSecond 方法:

public LocalDate getSecond(){
        return (LocalDate)super.getSecond().clone();
    }

在原书中说类型擦除之后在DateInterval中会存在两个getSecond方法,我并不这么认为,我认为子类重写了父类Pair中的getSecond方法,擦除之后虽然两个方法的类型参数一样,但是子类的返回类型是父类返回类型的子类,所以是一个具有可协变返回类型的重写。
例如:

public class AbstractDemo {
    private int age;
    public int getAge(){
        return age;
    }
    public Employee f(){
        return new Employee();
    }
}

class Student extends AbstractDemo{
    @Override
    public Manager f(){
        System.out.println("我是子类");
        return new Manager();
    }
    public static void main(String[] args) {
        AbstractDemo demo=new Student();
        Manager f = (Manager) demo.f();
    }
}

输出如下:
阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第1张图片
强制转换不会出现ClassCastException,输出也有我是子类,说明执行的是子类的方法,所以这点我认为是方法的重写,而不是一个类中两个方法名和参数类型相同而返回类型不同的方法,这点作者在注释中也说了。

对于Java泛型的转换,需要记住以下几点:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都用它们的限定类型替换。
  • 桥方法被合成来保持多态。
  • 为保持类型安全性,必要时插人强制类型转换。

8.5.4 调用遗留代码

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

8.6 约束与局限性

这一节我看的时候十分的费劲,集中注意力!

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

不能用类型参数代替基本类型。因此, 没有 Pair, 只 有 Pair。 当然,其原因是类型擦除。擦除之后, Pair 类含有 Object 类型的域, 而 Object 不能存储 double 值。可以使用独立的类和方法来操作他们。基本类型不是类,无法擦除。

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

这个读起来十分他妈的拗口,那到底是什么意思?就是检测两个带泛型的运行时类的对象的继承关系,不能使用instanceof应该使用getClass,看代码来说:

 BookPair<String> bookPair=new BookPair<>();
System.out.println(bookPair instanceof BookPair<String>);//Error
System.out.println(bookPair instanceof BookPair<T>);//Error
System.out.println(bookPair instanceof BookPair);//true
System.out.println(bookPair.getClass());//class com.generics.BookPair

getClass 方法总是返回原始类型。

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

不能实例化参数化类型的数组, 例如:

Pair<String>[] table = new Pair<String>[10]; // Error

这有什么问题呢? 擦除之后, table 的类型是 Pair[。] 可以把它转换为Object[] :

Object[] objarray = table;

数组会记住它的元素类型, 如果试图存储其他类型的元素, 就会抛出一个 ArrayStoreException 异常:

objarray[0] = "Hello"; // Error component type is Pair

不过对于泛型类型, 擦除会使这种机制无效。以下赋值:

objarray[0] = new Pair<Employee>();

能够通过数组存储检査, 不过仍会导致一个类型错误。出于这个原因, 不允许创建参数化类型的数组。需要说明的是, 只是不允许创建这些数组, 而声明类型为 Pair[] 的变量仍是合法的。不过不能用 new Pair[10] 初始化这个变量。
可以声明通配类型的数组, 然后进行类型转换

Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];

结果将是不安全的。如果在 table[0] 中存储一个 Pair, 然后对 table[0].getFirst() 调用一个 String 方法, 会得到一个 ClassCastException 异常。
注释:如果需要收集参数化类型对象, 只有一种安全而有效的方法:使用 ArrayList:ArrayList

8.6.4 Varargs警告

再来讨论一个相关的问题:向参数个数可变的方法传递一个泛型类型的实例。考虑下面这个简单的方法, 它的参数个数是可变的:

public static final  <T> void addAll(Collection<T> collection,T...ts){
        if (Objects.isNull(ts))return;
        for (T t:ts){
            collection.add(t);
        }
    }

考虑以下调用

Pair<String> pair=new Pair<>("a");
Pair<String> pair1=new Pair<>("c");
Collection<Pair<String,String>> collection=new ArrayList<>();
ArrayAlg.addAll(collection,pair,pair1);    System.out.println(Arrays.toString(collection.toArray()));

为了调用这个方法,在使用可变参数传参的时候就会创建一个数组,虽然违反了前面的规则但是这里不会有错误,会得到一个警告,而不是错误。
在 Java SE 7中, 还 可 以 用@SafeVarargs 直 接 标 注
addAll 方法:

 @SafeVarargs
    public static final  <T> void addAll(Collection<T> collection,T...ts){...}

可以使用 @SafeVarargs 标注来消除创建泛型数组的有关限制, 方法如下:

@SafeVarargs static <E> EQ array(E... array) { return array; }

8.6.5 不能实例化类型变量

不能使用像 new T(…,) newT[…] 或 T.class 这样的表达式中的类型变量。例如, 下面的Pair 构造器就是非法的:

first=new T();
second=new T(); //ERROR 类型擦除之后会变成new Object()

在 Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。例如:

Pair<String> p = Pair.makePairCString::new);

makePair 方法接收一个 Supplier,这是一个函数式接口,表示一个无参数而且返回类型为 T 的函数:

public static <T> Pair<T> makePair(Supplier<T> supplier1){
        //不能实例化类型变量
//        first=new T();
//        second=new T(); //ERROR 类型擦除之后会变成new Object()

        //再Java8之后,最好的解决办法是让调用者提供一个构造器表达式,表示一个无参数而返回类型是T的函数
        T first=supplier1.get();
        T second=supplier1.get();
       return new Pair<>(first,second);
    }

比较传统的解决方法是通过反射调用 Clasmewlnstance 方法来构造泛型对象。遗憾的是,细节有点复杂。不能调用:

first = T.dass.newInstance(); // Error

表达式 T.class 是不合法的, 因为它会擦除为 Objectclass。必须像下面这样设计 API 以便得到一个 Class 对象:

public static <T> Pair<T> makePair(Cl ass<T> cl) {
try { return new Pairo(d.newInstanceO. cl.newInstance();) }
catch (Exception ex) { return null; } }

这个方法可以按照下列方式调用:

Pair<String> p=Pair.makePair(String.class);

注意,Class类本身是泛型。 例如,String.daSS 是一个 Class 的实例(事实上,它是唯一的实例。) 因此,makePair 方法能够推断出 pair 的类型。

8.6.6 不能构造泛型数组

就像不能实例化一个泛型实例一样, 也不能实例化数组。不过原因有所不同,毕竟数组会填充 mill 值,构造时看上去是安全的。不过, 数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。

 /**不能构造泛型数组
         * 这里的R 其实默认的是R extends Object
         * 也就是说,类型擦除之后语句R[] rs1=new R[num];会变成 Object[] objs=new Object[num]
         * 可以把子对象引用赋给超类的变量,但是如果一个超类变量本就不是指向一个子类对象的引用
         * 如果强制转回去就会ClassCastException
         */
//        R[] rs1=new R[10];//ERROR

先看看关于ArrayList类的实现:

public class ArrayList<E> {
private Object[] elements; 
@SuppressWarnings("unchecked") 
public E get(int n) { return (E) elements[n]; }
public void set(int n , E e) { elements[n] = e; } // no cast needed
}

这里, 强制类型转换 E[ ] 是一个假象, 而类型擦除使其无法察觉。实际上还是在操作Object对象,这里数组内的elements[n]初始的时候指向自己设定的对象,所以在取出来的时候可以进行强制转换转成自己想要的对象,但是并不可能将一个数组的变量强制转换为自己想要的类型的数组变量,因为这个数组在初始化的时候就是new Object。
但是如果想要得到一个指定类型的数组,上面的在将Object强制转换为指定类型的时候就会出错误。
在这种情况下, 最好让用户提供一个数组构造器表达式:
String[] ss = ArrayAlg.minmax (String[]::new,“Tom”, “Dick”, “Harry”);
构造器表达式 Stringxnew 指示一个函数, 给定所需的长度, 会构造一个指定长度的String 数组。如下:一种是利用函数式接口,一种是利用反射创建。

public  static <R> R[] getGenericArray(IntFunction<R[]> function, R...args){
        R[] rs=function.apply(args.length);
        for (int i=0;i<args.length;i++){
            rs[i]=args[i];
        }
        return rs;
        //利用反射创建,只需要传一个R类型的可变长参数就可以了 getClass就是Array getComponentType才能得到类型
//        R[] rs1 =(R[]) Array.newInstance(args.getClass().getComponentType(), args.length);
//        return rs1;
    }

ArrayList 类的 toArray 方法就没有这么幸运。它需要生成一个 T[] 数组, 但没有成分类型。因此, 有下面两种不同的形式:

Object□toArray()
TQ toArray(TQ result)

第二个方法接收一个数组参数。如果数组足够大, 就使用这个数组。 否则, 用 result 的成分类型构造一个足够大的新数组。

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

不能在静态域或方法中引用类型变量。例如, 下列高招将无法施展:

public class Singleton<T> {
	private static T singlelnstance; // Error
	public static T getSinglelnstance() // Error
	{
		if (singleinstance == null) construct new instance of T
		return singlelnstance; 
	} 
}

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

既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展 Throwable 都是不合法的。例如, 以下定义就不能正常编译:

public class Problem<T> extends Exception { /* . . . */ } // Error can't extend Throwable

**catch 子句中不能使用类型变量。**例如, 以下方法将不能编译:

public static <T extends Throwablevoid doWork(Class<T> t) {
	try
	{
		do work
	}
	catch (T e) // Error can 't catch type variable
	{
		Logger,global.info(...) 
	} 
}

不过, 在异常规范中使用类型变量是允许的。以下方法是合法的:

 public static <T extends Throwable> void doWork(Class<T> tClass) throws Throwable {
        //在异常中使用类型变量是允许的
        T t = null;
        try {
            t = tClass.getConstructor(String.class).newInstance("这是一个自定义异常的类型变量");
            throw new Exception();
            //do work
        }catch (Exception e){
            t.initCause(e);
            throw t;
        }
    }

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

这个还是很实用的
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。关键在于以下方法:

public static <T extends Throwablevoid throwAs(Throwable e) throws T {
throw (T) e; }

假设这个方法包含在类 Block 中, 如果调用
Block.throwAs(t);
编译器就会认为 t 是一个非受查异常。 以下代码会把所有异常都转换为编译器所认为的
非受查异常:

try
{
do work
}
catch (Throwable t) { B1ock.<RuntimeException>throwAs(t) ; }

下面用这个方法解决一个比较棘手的问题,Runnable接口中的run方法只能抛出检查型异常,我们提供一个Task到Runnable的适配器,他的run方法就能抛出任何异常了。
如下:

public interface Task {
    void run() throws Exception;
    //包装抛出的异常,将检查型异常包装成非检查型异常,这样就可以不用try-catch处理,抛出来处理
    @SuppressWarnings("unchecked")
    static <T extends Throwable> void throwAs(Throwable t) throws T {
        throw (T)t;
    }
    static Runnable asRunnable(Task task){
        return ()->{
          try {
              task.run();
          }catch (Exception e){
              System.out.println(e.getClass().getName());
              Task.<RuntimeException>throwAs(e);
          }
        };
    }
}
class TaskImpl implements Task{

    @Override
    public void run() throws Exception {
        Thread.sleep(100);
        System.out.println("Hello world");
        throw new Exception("这是TaskImpl的一个对象!");
    }
}
public static void main(String[] args) {
        Thread thread=new Thread(Task.asRunnable(()->{
            Thread.sleep(1000);
            System.out.println("Hello world");
            throw new IOException("check this out");
        }));
        thread.start();
        Thread thread1=new Thread(Task.asRunnable(new TaskImpl()));
        thread1.setUncaughtExceptionHandler((t,e)->{
            System.out.println("我抓到你了!");
            System.out.println(t.getName()+","+e);
        });
        thread1.start();
//        thread1.interrupt();
    }

这有什么意义呢? 正常情况下, 你必须捕获线程 run 方法中的所有受查异常, 把它们“ 包装” 到非受查异常中, 因为 run 方法声明为不抛出任何受查异常。
不过在这里并没有做这种“ 包装”。我们只是抛出异常, 并“ 哄骗” 编译器, 让它认为这不是一个受查异常。

8.6.10 注意擦除后的冲突

泛型规范说明还提到另外一个原则:“ 要想支持擦除的转换, 就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。” 例如,下述代码是非法的:

class Employee implements Coinparab1e<Emp1oyee> { . . . }
class Manager extends Employee implements Comparable<Manager>{...}//Error

Manager 会实现 Comparable 和 Comparable, 这是同一接口的不同参数化。
其原因非常微妙, 有可能与合成的桥方法产生冲突。实现了 C0mpamble 的类可以获得一个桥方法:

public int compareTo(Object other) { return compareTo((X) other); }

对于不同类型的 X 不能有两个这样的方法。

8.7 泛型类型的继承规则

Pair 是Pair 的一个子类吗? 答案是“ 不是”, 或许人们会感到奇怪。例如, 下面的代码
将不能编译成功:

ManagerD topHonchos = . . .;
Pair<Employee> result = ArrayAlg.ininmax (topHonchos) ; // Error

minmax 方法返回 Pair, 而不是 Pair,并且这样的赋值是不合法的。
无论 S 与 T 有什么联系 (如图 8-1 所示,) 通常, Pair 与 Pair没有什么联系

阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第2张图片
永远可以将参数化类型转换为一个原始类型。例如,PaiKEmployee> 是原始类型 Pair 的一个子类型。在与遗留代码衔接时,这个转换非常必要。
转换成原始类型之后会产生类型错误吗? 很遗憾, 会! 看一看下面这个示例:

Pair<Manager> managerBuddies = new Pairo(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File(". . .")); // only a compile-time warning

当使用 getFirst 获得外来对象并赋给 Manager 变量时, 与通常一样, 会抛出 ClassCastException 异常。这里失去的只是泛型程序设计提供的附加安全性。
最后, 泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如, ArrayList 类实现 List 接口。这意味着, 一个 ArrayList 可以被转换为一个 List。但是, 如前面所见, 一个 ArrayList 不是一个ArrayLis 或 List。图 8-2 展示了它们之间的联系。
阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第3张图片

8.8 通配符类型

固定的泛型类型系统使用起来并没有那么令人愉快, 类型系统的研究人员知道这一点已经有一段时间了。Java 的设计者发明了一种巧妙的(仍然是安全的“) 解决方案”:通配符类型。

8.8.1 通配符概念

通配符类型中, 允许类型参数变化。 例如, 通配符类型

Pair<? extends Employee

假设要编写一个打印雇员对的方法, 像这样:

public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName()+ " are buddies.");

正如前面讲到的,不能将 Pair 传递给这个方法,这一点很受限制。解决的方法很简单:使用通配符类型:

public static void printBuddies(Pair<? extends Eiployee> p)

类型 Pair 是 Pair 的子类型(如图 8-3 所示)。
阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第4张图片
这可能不会引起破坏。对 setFirst 的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型 Pair

? extends Employee getFi rstO
void setFirst(? extends Employee)

这样将不可能调用 setFirst 方法。编译器只知道需要某个 Employee 的子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?不能用来匹配。
使用 getFirst 就不存在这个问题: 将 getFirst 的返回值赋给一个 Employee 的引用完全合法。

8.8.2 通配符的超类型限定

通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定 (supertypebound), 如下所示:

? super Manager

这个通配符限制为 Manager 的所有超类型。带有超类型限定的通配符的行为与 8.8 节介绍的相反。可以为方法提供参数, 但不能使用返回值。例如, Pair 有方法。

void setFirst(? super Manager) 
? super Manager getFirst()

编译器无法知道 setFirst 方法的具体类型, 因此调用这个方法时不能接受类型为 Employee 或 Object 的参数。 只能传递Manager 类型的对象,或者某个子类型(如 Executive) 对象。另外, 如果调用 getFirst, 不能保证返回对象的类型。只能把它赋给一个 Object。
直观地讲,带有超类型限定的通配符可以向泛型对象写人,带有子类型限定的通配符可以从泛型对象读取。
下面是超类型限定的另一种应用。Comparable 接口本身就是一个泛型类型。声明如下:

public interface Comparable<T> {
public int compareTo(T other); 
}

在此,类型变量指示了 other 参数的类型。例如, String类实现 Comparable , 它 的 compareTo 方法被声明为

public int compareTo(String other)

很好,显式的参数有一个正确的类型。接口是一个泛型接口之前,other 是一个 Object,并且这个方法的实现需要强制类型转换。由于 Comparable 是一个泛型类型, 也许可以把 ArrayAIg类的 min方法做得更好一些?可以这样声明:

public static <T extends Comparable<T» T RrinCrQ a)

阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第5张图片
看起来, 这样写比只使用 T extents Comparable 更彻底, 并且对许多类来讲, 工作得更好。
处理一个 LocalDate 对象的数组时, 会出现一个问题。LocalDate 实现了 ChronoLocalDate, 而 ChronoLocalDate 扩展了 Comparable。因此, LocalDate 实现的是 Comparable 而不是 Comparable
在这种情况下, 超类型可以用来进行救助:

public static <T extends Conparable<? super T» T min(T[] a) .

现在 compareTo 方法写成

int compareTo(? super T)

有可能被声明为使用类型 T 的对象, 也有可能使用 T 的超类型(如当 T 是 LocalDate, T的一个子类型。) 无论如何,传递一个 T 类型的对象给 compareTo 方法都是安全的。

8.8.3 无限定通配符

还可以使用无限定的通配符, 例如,Pair。初看起来,这好像与原始的 Pair 类型一样。实际上, 有很大的不同。类型 Pair 有以下方法:

? getFirst()
void setFirst(?)

getFirst 的返回值只能赋给一个 Object。setFirst 方法不能被调用, 甚至不能用 Object 调 用。Pair 和 Pair 本质的不同在于: 可以用任意 Object 对象调用原始 Pair 类的 setObject方法。
为什么要使用这样脆弱的类型? 它对于许多简单的操作非常有用。例如,下面这个方法将用来测试一个 pair 是否包含一个 mill 引用,它不需要实际的类型。

public static boolean hasNulls(Pair<?> p) {
return p.getFirst() = null || p.getSecond() =null; 
}

8.8.4 通配符捕获

编写一个交换成对元素的方法:

public static void swap(Pair<?> p)

但是如下

? t = p.getFirst(); // Error

这是一个问题, 因为在交换的时候必须临时保存第一个元素。幸运的是, 这个问题有一个有趣的解决方案。我们可以写一个辅助方法 swapHelper, 如下所示:

public static <T> void swapHelper(Pair<T> p)
 { 	
 	T t = p.getFirst(); 
 	p.setFirst(p. getSecond());
  	p.setSecond(t); 
  }

注意, swapHelper 是一个泛型方法, 而 swap 不是, 它具有固定的 Pair 类型的参数。
现在可以由 swap 调用 swapHelper:

public static void swap(Pair<?> p) { swapHelper(p); }

在这种情况下,swapHelper 方法的参数 T 捕获通配符。它不知道是哪种类型的通配符, 但是,这是一个明确的类型,并且 swapHelper 的定义只有在 T 指出类型时才有明确的含义。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、 确定的类型。例如, ArrayList 中的 T 永远不能捕获 ArrayList>中的通配符。数组列表可以保存两个 Pair, 分别针对?的不同类型。

8.9 反射和泛型

反射允许你在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息,因为它们会被擦除。

8.9.1 泛型 Class 类

现在, Class 类是泛型的。例如, String.class 实际上是一个Class类的对象(事实上,是唯一的对象)。
类型参数十分有用, 这是因为它允许 Class方法的返回类型更加具有针对性。下面Class 中的方法就使用了类型参数:
阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第6张图片

8.9.2 使用 Class 参数进行类型匹配

下面是一 标准的示例:

public static <T> Pai r<T> makePair(Class<T> c) throws InstantiationException,
IllegalAccessException
{
return new Pair<>(c.newInstance(), c.newInstance()); 
}

如果调用

makePair(Employee.class)

Employee.class 是类型 Class 的一个对象。makePair 方法的类型参数 T 同 Employee匹配, 并且编译器可以推断出这个方法将返回一个 Pair

8.9.3 虚拟机中的泛型类型信息

可以使用反射 API 来确定

  • 这个泛型方法有一个叫做 T 的类型参数。
  • 这个类型参数有一个子类型限定, 其自身又是一个泛型类型。
  • 这个限定类型有一个通配符参数。
  • 这个通配符参数有一个超类型限定。
  • 这个泛型方法有一个泛型数组参数。

换句话说,需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用,如何解释类型参数。
为了表达泛型类型声明,使用java.lang.reflect 包中提供的接口 Type。这个接口包含下列子类型:

  • Class 类,描述具体类型。
  • TypeVariable 接口,描述类型变量(如 T extends Comparable)
  • WildcardType 接口, 描述通配符 (如?super T)
  • ParameterizedType 接口, 描述泛型类或接口类型(如 Comparable。)
  • GenericArrayType 接口,描述泛型数组(如 T[ ])。

图 8-5 给出了继承层次。注意, 最后 4 个子类型是接口, 虚拟机将实例化实现这些接口的适当的类。
阅读Java核心技术Ⅰ的笔记(Java基础、第八章、泛型程序设计)_第7张图片
还有个比较牛皮的例子,我这里不是很理解,需要再看一遍关于反射泛型这部分。不贴了

8.9.4 类型字面量

null

你可能感兴趣的:(java基础)