CORE JAVA 第八章 泛型程序设计

第八章 泛型程序设计

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

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

8.1.1 类型参数的好处

​ 在Java中增加泛型类之前,泛型程序设计是用继承实现的。ArrayList类只维护一个Object引用的数组:

public class ArrayList	// before generic classes
{
    private Object[] elementData;
    ……
    public Object get(int i){……}
    public void add(Object o){……}
}

​ 这种方法有两个问题:当获取一个值的时候必须进行强制类型转换;没有错误检查,可以向数组列表中添加任何类的对象。

​ 对于这些调用,编译和运行都不会出错。然而在其他地方,如果将get的结果强制类型转换为String类型,就会产生一个错误。

​ 泛型提供了一个更好的解决方案:类型参数。类型参数用来指示元素的类型:

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

注释:在Java SE 7及以后的版本中,构造函数中可以省略泛型类型:

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

​ 编译器可知道类型参数的类型。调用get方法时不需要进行强制类型转换;调用add方法时可以检查避免插入错误类型的对象。

​ 类型参数的魅力在于:使得程序具有更好的可读性和安全性。

8.1.2 谁想成为泛型程序员

​ 一个泛型程序员的任务就是预测出所用类的未来可能有的所有用途。

通配符类型??

8.2 定义简单泛型类

​ 一个泛型类就是具有一个或多个类型变量的类。例:

public class Pair<T>
{
    private T first;
    private T second;
    
    public Pair()	{ first = null; second = null;}
    public Pair(T first, T second)	{this.first = first; this.second = second;}
    
    public T getFirst() { return first;}
    
    public void setFirst(T newValue) { first = newValue; }
}

​ Pair类引入了一个类型变量T,用尖括号括起来,并放在类名的后面。

​ 泛型类可以有多个类型变量。

public class Pair<T, U> {……}

其中第一个域和第二个域使用不同的类型。

​ 类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。

注释:在Java库中,使用E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T/U/S表示“任意类型”。

​ 用具体的类型替换类型变量就可以实例化泛型类型。

8.3 泛型方法

​ 定义一个带有类型参数的简单方法。

class ArrayAlg
{
    public static <T> T getMiddle(T... a)
        ……
}

​ 这个方法是在普通类中定义的,而不是在泛型类中定义的。注意,类型变量放在修饰符的后面,返回类型的前面。

​ 泛型方法可以定义在普通类中,也可以定义在泛型类中。

​ 当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

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

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

String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

​ 几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会提示错误,此时需要解译错误报告。例:

double middle = ArrayAlg.getMiddle(3.14, 1729, 0);

​ 编译器将会自动打包参数为1个Double和2个Integer对象,而后寻找这些类的共同超类型。

8.4 类型变量的限定

​ 有时,类或方法需要对类型变量加以约束。

​ 因为类型变量T可以是任何类的对象,若想确信类型变量T所属的类有compareTo方法,可以将类型变量T限制为实现了Comparable接口的类。可以通过对类型变量T设置限定(bound)实现这一点:

public static <T extends Comparable> T min(T[] a) ……

​ 实际上Comparable接口本身就是一个泛型类型。

​ 现在,没有实现Comparable接口的类调用min方法将会产生一个编译错误。

​ 在此为什么使用extends而不是implements?毕竟Comparable是一个接口。

​ 下面的记法

<T extends BoundingType>

表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。

​ 一个类型变量或通配符可以有多个限定,例如:

T extends Comparable & Serializable

​ 限定类型用&分隔,而逗号用来分隔类型变量。

​ 在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。

8.5 泛型代码和虚拟机

​ 虚拟机没有泛型类型对象——所有对象都属于普通类。

8.5.1 类型擦除

​ 泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

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

​ 例如,Pair的原始类型如下所示:

public class Pair
{
    private Object first;
    private Object second;
    
    public Pair()	{ first = null; second = null;}
    public Pair(Object first, Object second)	{this.first = first; this.second = second;}
    
    public Object getFirst() { return first;}
    
    public void setFirst(Object newValue) { first = newValue; }
}

​ 结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。

​ 在程序中可以包含不同类型的Pair,例如,Pair或Pair。而擦除类型后就变成原始的Pair类型了。

​ 原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。

8.5.2 翻译泛型表达式

​ 当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如:

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

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

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

​ 当存取一个泛型域时也要插入强制类型转换。

8.5.3 翻译泛型方法

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

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

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

public static Comparable min(Comparble[] a)

​ 因为 java 在编译源码时, 会进行 类型擦除, 导致泛型类型被替换限定类型(无限定类型就使用 Object). 因此为保持继承和重载的多态特性, 编译器会生成 桥方法.

Pair 是个泛型类, 它具有泛型方法 setSecond(T second),
在经过编译时的 类型擦除 后变为 setSecond(Object second).

DateIntervalPair<LocalDate> 的实例化子类, 它具有方法 setSecond(LocalDate second).

​ 我们新建 DateInterval 对象, 并用基类 Pair<LocalDate> 来引用它,此时调用基类的 setSecond 方法时, 我们希望它能够实现多态, 即调用 DateInterval.setSecond(LocalDate) 方法.
​ 事实上, java 编译器通过插入 桥方法 的方式, 帮助我们实现了该功能(解决了类型擦除与多态发生的冲突:因为有了类型擦除,不满足多态的条件:继承/重写).

​ 反编译 DateInterval.class 会发现它具有两个 setSecond 方法:

  1. void setSecond(LocalDate);

  2. void setSecond(Object).

    并且, 虚拟机用pair引用的对象调用 void setSecond(Object) 中会调用 DateInteval.setSecond(Object), 这个方法是合成的桥方法.它调用DateInteval.setSecond(Date)。这正是我们期望的操作效果。

​ 另外,DateIntervalPair<LocalDate> 的实例化子类,不能这样编写代码:

class DateInterval extends Pair<LocalDate>
{
    public LocalDate getSecond()	{ return (Date) super.getSecond().clone();}
}

​ 在DateInterval类中,有两个getSecond方法:

LocalDate getSecond() 	// defined in DateInterval
Object getSecond()	// overrides the method defined in Pair to call the first method

​ 在这里,具有相同参数类型的两个方法是不合法的。它们都没有参数。但是,在虚拟机中,用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这样情况。

注释:桥方法不仅用于泛型类型。

在一个方法覆盖另一个方法时可以指定一个更严格的返回类型:

public class Employee implements Cloneable
{
    public Employee clone() throws CloneNotSupportedException {……}
}

Object.clone和Employee.clone方法被说成具有协变的返回类型。

实际上,Employee类有两个克隆方法:

Employee clone()	//defined above
Object clone()

合成的桥方法调用了新定义的方法。

​ 有关Java泛型转换的事实:

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

8.5.4 调用遗留代码

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

​ 当将一个泛型对象传递给一个参数是原始类型的函数时,编译器会发出一个警告。

Dictionary<Integer, Component> labelTable = new Hashtable<>();
slider.setLabelTable(labelTable);	// Warning
// void setLabelTable(Dictionary table)

编译器无法确定这个方法会对这个泛型对象做什么操作,未来的操作有可能会产生强制类型转换的异常。

​ 另一个相反的情形是由一个遗留的类得到一个原始类型的对象。可以将它赋给一个参数化的类型变量,当然,这样做会看到一个警告。例如:

Dictionary<Integer, Components> labelTable = slider.getLabelTable();	// Warning

​ 最差的情况就是程序抛出一个异常。

​ 在查看了警告之后,可以利用注解使之消失。注释必须放在生成这个警告的代码所在的方法之前:

@SuppressWarning("unchecked")
Dictionary<Integer, Components> labelTable = slider.getLabelTable();	// No warning

​ 或者,可以标注整个方法,如下:

@SuppressWarning("unchecked")
public void configureSlider()	{……}

​ 这个注解会关闭对方法中所有代码的检查。

8.6 约束与局限性

​ 下面将阐述使用Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。

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

​ 不能用类型参数代替基本类型。因此,没有Pair<double>,只有Pair<Double>

​ 其原因是类型擦除。擦除之后,Pair类含有Object类型的域,而Object不能存储double值。

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

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

if (a instanceof Pair<String>)	//Error

实际上仅仅测试a是否是任意类型的一个Pair。下面的测试同样如此:

if (a instanceof Pair<T>)	//Error

或强制类型转换:

Pair<String> p = (Pair<String>) a;	//Waring——can only test that a is a Pair

​ 同样的道理,getClass方法总是返回原始类型:

Pair<String> stringPair = ……;
Pair<Employee> employeePair = ……;
if (stringPair.getClass() == employeePair.getClass())	// they are equal

其比较的结果是true,这是因为两次调用getClass都将返回Pair.class。

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

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

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

​ 擦除后,table的类型是Pair[]。如果把它转换为Object[]

Object[] objarray = table;

​ 并试图存储其他类型的元素,就会抛出一个异常:

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

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

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

能够通过数组存储检查,不过仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。

​ 需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]的变量仍是合法的。不过不能用new Pair<String>[10]初始化这个变量。

提示:如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:

ArrayList<Pair<String>>

8.6.4 Varargs警告

​ 向参数个数可变的方法传递一个泛型类型的实例。

public static <T> void addAll(Collection<T> coll, T... ts)
{
    for (t : ts) coll.add(t);
}

​ 实际上参数ts是一个数组,包含提供的所有实参。

​ 现在考虑以下调用:

Collection<Pair<String>> table = ……;
Pair<String> pair1 = ……;
Pair<String> pair2 = ……;
addAll(table, pair1, pair2);

​ 为了调用这个方法,Java虚拟机必须建立一个Pair<String>数组,这就违反了前面的规则。不过,对于这种情况,规则有所放松,你只会得到一个警告而不是错误。

​ 可以采用两种方法来抑制这个警告。一种方法是为包含addAll调用的方法增加注解@SuppressWarnings("unchecked")。或者在Java SE 7中,还可以用@SafeVarargs直接标注addAll方法。

注释:可以使用@SafeVarargs标注来消除创建泛型数组的有关限制:

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

​ 现在可以调用:

Pair<String>[] table = array(pair1, pair2);

​ 以下代码:

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

能顺利运行而不会出现ArrayStoreException异常,但在处理table[0]时会在别处得到一个异常。

8.6.5 不能实例化类型变量

​ 不能使用像new T(……)new T[……]T.class这样的表达式中的类型变量。类型擦除将T改变成Object。

​ 例如,下面的构造器就是非法的:

public Pair()	{ first = new T(); second = new T(); }

两种方式解决如何实例化类型变量

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

Pair<String> p = Pair.makePair(String::new);

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

public static <T> Pair<T> makePair(Supplier<T> constr)
{
    return new Pair<>(constr.get(), constr.get());
}

​ 比较传统的解决方法是通过反射调用Class.newInstance方法构造泛型对象。

​ 不能调用

first = T.class.newInstance();
// T.class会被擦除为Object.class

​ 应像这样设计以便得到一个Class对象:

public static <T> Pair<T> makePair(Class<T> cl)
{
    try { return new Pair<>(cl.newInstance(), cl.newInstance()); }
    catch (Exception ex)	{return null;}
}

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

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

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

8.6.6 不能构造泛型数组

​ 数组本身也有类型,用来监控存储在虚拟机中的数组。这个类型会被擦除。例如:

public static <T extends Comparable> T[] minmax(T[] a)	{ T[] mm = new T[2]; ……}	// Error

​ 类型擦除会让这个方法永远构造Comparable[2]数组。

​ 如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],并且在获取元素时进行类型转换。

​ 例如,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
}

​ 实际的实现没有这么清晰:

public class ArrayList<E>
{
    private E[] elements;
    ……
    public ArrayList()	{ elements = (E[]) new Object[10]; }
}

​ 这里,强制类型转换E[]是一个假象,而类型擦除使其无法察觉。

两种方式解决想要实例化数组而类型擦除引起的ClassCastException??

​ 由于minmax方法返回T[]数组,而不是仅仅作为私有域,使得上面这一技术无法施展。如果掩盖这个类型会有错误结果。

public static <T extends Comparable> T[] minmax(T... a)	
{ 
    Object[] mm = new Object[2];
    ……
    
    return (T[]) mm;
}	

​ 调用:

String[] ss = ArrayAlg.minmax("Tom", "Dick", "Harry");

编译时不会有任何警告。当Object[]引用赋给Comparable[]变量时,将会发生ClassCastException异常。

​ 在这种情况下,最好让用户提供一个数组构造器表达式:

String[] ss = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");

构造器表达式指示一个函数,给定所需的长度,会构造一个指定长度的String数组。

​ minmax方法使用这个参数生成一个有正确类型的数组:

public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)	
{ 
    T[] mm = constr.apply(2);
    ……

}	

​ 比较老式的方法是利用反射,调用Array.newInstance

public static <T extends Comparable> T[] minmax(T... a)	
{ 
    T[] mm = (T[]) Array.newInstance(a.getClass().getComponentTType(), 2);
    ……
}	

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

​ 不能在静态域或方法中引用类型变量。

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

​ 泛型类甚至都不能扩展Throwable。

​ catch子句中不能使用类型变量。不过,在异常规范中使用类型变量(方法头thorws中)是允许的。

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

8.6.10 注意擦除后的冲突

​ 当泛型类型被擦除时,无法创建引发冲突的条件。

​ 方法的参数被擦除,若该方法与继承来的方法重名,而且擦除后的参数类型与继承的方法的参数类型相同,就会发生冲突。补救的方法是重新命名引发错误的方法。

​ 要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化。例如:

class Employee implements Comparable<Employee>	{……}
class Manager extends Employee implements Comparable<Manager>	// Error
{……}

是非法的。Manager会实现Comparable<Employee>Comparable<Manager>,这是同一接口的不同参数化。

​ 其原因非常微妙。有可能与合成的桥方法产生冲突。实现了Comparable<X>的类可以获得一个桥方法:

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

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

8.7 泛型类型的继承规则

ManagerEmployee的子类,但Pair<Manager>不是Pair<Employee>的子类。

​ 永远可以将参数化类型转换为一个原始类型。例如,Pair<Employee>是原始类型Pair 的一个子类型。转换成原始类型后,会产生类型错误。例:

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

​ 这里失去的只是泛型程序设计提供的附加安全性。

​ 泛型类可以扩展或事项其他的泛型类。

8.8 通配符类型

8.8.1 通配符概念

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

Pair<? extends Employee>

表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是Pair<String>

	#### 通过有限定的通配符,可以区分安全的访问器方法和不安全的更改器方法???

8.8.2 通配符的超类型限定!!!

​ 可以指定一个超类型限定:

? super Manager

这个通配符限制为Manager的所有超类型。

​ 带有超类型限定的通配符可以为方法提供参数,但不能使用返回值。例如,Pair<? super Manager>有方法void setFirst(? super Manager)? super Manager getFirst()

​ 这不是真正的Java语法,但是可以看出编译器知道什么。编译器无法知道setFirst方法的具体类型,因此调用这个方法时不能接受类型为EmployeeObject的参数。只能传递Manager类型的对象,或某个子类型的对象。另外,如果调用getFirst,不能保证返回对象的类型。只能把它赋给一个Object

​ 直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

超类型限定的另一应用 ??

<T extends Comparable<? super T>>

​ 超类型限定的另一个常见的用法是作为一个函数式接口的参数类型。

8.8.3 无限定通配符

Pair<?>

​ 例如,Pair<?>有方法void setFirst(?)?getFirst()

getFirst的返回值只能赋给一个ObjectsetFirst方法不能被调用,甚至不能用Object调用。

Pair<?>Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setObject方法。

8.8.4 通配符捕获???

8.9 反射和泛型

8.9.1 泛型Class类

Class<T>中的许多方法使用了类型参数,使得Class<T>方法的返回类型更加具有针对性。

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

​ 有时,匹配泛型方法中的Class<T>参数的类型变量很有实用价值。

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

​ 虚拟机?需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用,如何解释类型参数。

个人理解

​ 使用强制类型转换,没有类型检测,只能够在运行时候,系统抛出异常后,你才会发现错误;

​ 泛型的特点:

  1. 能够对类型进行限定
  2. 在编译期对类型进行检查,编译时报错
  3. 对于获取明确的限定类型,无需进行强制类型转化

Java中泛型是不变的,而数组是协变的.

在java泛型中,引入了 ?(通配符)符号来支持协变和逆变.类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 !!

通配符表示一种未知类型,并且对这种未知类型存在约束关系.

https://blog.csdn.net/qq_35890572/article/details/80402840

https://segmentfault.com/a/1190000005337789

? 与 T 的差别

  1. ? 表示一个未知类型, T 是表示一个确定的类型. 因此,无法使用 ?T 声明变量和使用变量.如
    // OK
    static <T> void test1(List<T> list) {
        T t = list.get(0);
        t.toString();
    }
    // Error
    static void test2(List<?> list){
        ? t = list.get(0);
        t.toString();
    }```java
  1. ? 主要表示**使用泛型**,T`表示声明泛型
posted @ 2020-09-16 21:06  c1utchfan  阅读(121)  评论(0)    收藏  举报