泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用
在Java 中增加范型类之前, 泛型程序设计是用继承实现的。
泛型提供了一个更好的解决方案: 类型参数( type parameters)。
ArrayList<String> files = new ArrayList<>();//构造函数中可以省略泛型类型
类型参数的魅力在于:使得程序具有更好的可读性和安全性。
通配符类型( wildcard type)
一个泛型类( generic class ) 就是具有一个或多个类型变量的类。
public class Pair<T>
{
private T first;
private T second;
public Pair() { first = null ; second = null ; }
public PairfT first , T second) { this,first = first; this.second = second; }
public T getFirstO { return first; }
public T getSecondO { return second; }
public void setFirst (T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。类型变量使用大写形式,且比较短,使用变量E 表示集合的元素类型,K 和V 分别表示表的关键字与值的类型。T ( 需要时还可以用临近的字母U 和S) 表示“ 任意类型”。
用具体的类型替换类型变量就可以实例化泛型类型。换句话说,泛型类可看作普通类的工厂。
定义一个带有类型参数的简单方法。类型变量放在修饰符(这里是public static ) 的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length / 2];
}
}
String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");
String middle = ArrayAlg.getHiddle("]ohn", "Q.", "Public");//调用中可以省略 类型参数
有时,类或方法需要对类型变量加以约束。
将T 限制为实现了Comparable 接口(只含一个方法compareTo 的标准接口)的类。可以通过对类型变量T 设置限定(bound) 实现这一点:
public static <T extends Comparab1e> T min(T[] a) . . .
现在,泛型的min 方法只能被实现了Comparable 接口的类(如String、LocalDate 等)的数组调用
在此为什么使用关键字extends 而不是implements ?
这个记法表示T 应该是绑定类型的子类型(subtype)。T 和绑定类型可以是类, 也可以是接口。选择关键字extends 的原因是更接近子类的概念。
一个类型变量或通配符可以有多个限定。限定类型用“ &” 分隔,而逗号用来分隔类型变量。
T extends Comparable & Serializable
在Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
虚拟机没有泛型类型对象—所有对象都属于普通类。
无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型( raw type )。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased) 类型变M, 并替换为限定类型(无限定的变量用Object)。
当程序调用泛型方法时, 如果擦除返回类型, 编译器插入强制类型转换
例如,下面的语句序列,擦除getFirst 的返回类型后将返回Object 类型。编译器自动插人Employee 的强制类型转换。
Pair<Employee> buddies = . .
Employee buddy = buddies.getFirst();
也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
类型擦除也会出现在泛型方法中。
存在问题:类型擦除与多态发生了冲突。要解决这个问题, 就需要编译器在Datelnterval 类中生成一个桥方法(bridge method):
桥方法不仅用于泛型类型。在一个方法覆盖另一个方法时可以指定一个更严格的返回类型(有协变的返回类型)
总之,需要记住有关Java 泛型转换的事实:
设计Java 泛型类型时, 主要目标是允许泛型代码和遗留代码之间能够互操作。
在查看了警告之后,可以利用注解( annotation ) 使之消失。注释必须放在生成这个警告的代码所在的方法之前。
或者,可以标注整个方法,关闭对方法中所有代码的检査。
大多数限制都是由类型擦除引起的。
没有Pair
, 只有Pair
虚拟机中的对象总有一个特定的非泛型类型。因此, 所有的类型查询只产生原始类型。
同样的道理, getClass 方法总是返回原始类型,例如下面的例子。其比较的结果是true, 这是因为两次调用getClass 都将返回Pair.class。
Pair<String> stringPair = . .
Pair< Employee>employeePair = . .
if (stringPair.getClass() == employeePair .getClass()) // they are equal
不能实例化参数化类型的数组。
Pair<String>[] table = new Pair<String>[10]; // Error
需要说明的是, 只是不允许创建这些数组, 而声明类型为Pair
的变量仍是合法的。不过不能用new Pair
初始化这个变量。
如果需要收集参数化类型对象, 只有一种安全而有效的方法: 使用ArrayList:ArrayList
上一节中已经了解到, Java 不支持泛型类型的数组。这一节中我们再来讨论一个相关的问题:向参数个数可变的方法传递一个泛型类型的实例。
可以使用@SafeVarargs 标注来消除创建泛型数组的有关限制。
@SafeVarargs static <E> E[] array(E... array) { return array; }
不能使用像new T(...),newT[...] 或T.class
这样的表达式中的类型变量
在Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式
Pair<String> p = Pair.makePairCString::new);
就像不能实例化一个泛型实例一样, 也不能实例化数组。
最好让用户提供一个数组构造器表达式
String[] ss = ArrayAlg.minmax(String[]::new,"Tom" , "Dick", "Harry");
不能在静态域或方法中引用类型变量
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展Throwable 都是不合法的。
catch 子句中不能使用类型变量。
不过, 在异常规范中使用类型变量是允许的。
public static <T extends Throwable〉void doWork(T t) throws T // OK
{
try
{
do work
}
catch (Throwable real Cause)
{
t .initCause( real Cause) ;
throw t ;
}
}
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。
通过使用泛型类、擦除和@SuppressWamings 注解, 就能消除Java 类型系统的部分基本限制。
当泛型类型被擦除时, 无法创建引发冲突的条件。
泛型规范说明还提到另外一个原则:“ 要想支持擦除的转换, 就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类, 而这两个接口是同一接口的不同参数化。”
class Employee implements Coinparab1e<Emp1oyee> { . . . }
class Manager extends Employee implements Comparable<Manager>
{ . . . } // Error
Manager 会实现Comparable
和Comparable
, 这是同一接口的不同参数化。
这一限制与类型擦除的关系并不十分明确。毕竟,下列非泛型版本是合法的。
class Employee implements Comparable { . . . }
class Manager extends Employee implements Comparable { . . . }
无论S 与T 有什么联系,通常, Pail
与Pair
没有什么联系。
这一限制看起来过于严格, 但对于类型安全非常必要。
这也是泛型与Java 数组之间的重要区别:可以将一个Manager[]数组賦给一个类型为Employee[] 的变量。然而,数组带有特别的保护。如果试图将一个低级别的雇员存储到employeeBuddies[0],虚拟机将会抛出ArrayStoreException 异常。
泛型类可以扩展或实现其他的泛型类。
通配符类型中, 允许类型参数变化。
Pair<? extends Employee〉
表示任何泛型Pair 类型, 它的类型参数是Employee
的子类, 如Pair
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力, 即可以指定一个超类型限定(supertypebound)
? super Manager
这个通配符限制为Manager 的所有超类型。
带有超类型限定的通配符的行为,可以为方法提供参数, 但不能使用返回值。
带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
子类型限定的另一个常见的用法是作为一个函数式接口的参数类型。
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;
}
通配符不是类型变量, 因此, 不能在编写代码中使用“ ?” 作为一种类型。
但是 编写一个交换成对元素的方法时,交换的时候必须临时保存第一个变量。解决方案是写一个辅助方法swapHelper
:
public static <T> void swapHelper (Pair<T> p)
{
T t = p.getFirst();
p.setFirst (p. getSecond()) ;
p.setSecond(t) ;
}
public static void swap(Pair<?> p) { swapHelper(p) ; }
在这种情况下,swapHelper
方法的参数T 捕获通配符。它不知道是哪种类型的通配符, 但是,这是一个明确的类型,并且
的定义只有在T 指出类型时才有明确的含义。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。例如, ArrayList
中的T 永远不能捕获ArrayList
中的通配符。数组列表可以保存两个Pair>
, 分别针对?
的不同类型。
反射允许你在运行时分析任意的对象。如果对象是泛型类的实例,关于泛型类型参数则得不到太多信息, 因为它们会被擦除。
Class类是泛型的。
类型参数十分有用, 这是因为它允许ClaSS 方法的返回类型更加具有针对性。
ava 泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是, 擦除的类仍然保留一些泛型祖先的微弱记忆。例如, 原始的Pair 类知道源于泛型类Pair< T>, 即使一个Pair 类型的对象无法区分是由PaiKString> 构造的还是由PaiKEmployee> 构造的。
可以使用反射API 来确定,需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用, 如何解释类型参数。
为了表达泛型类型声明, 使用java.lang.reflect 包中提供的接口Type。
欢迎关注公众号:GitKid,暂时每日分享LeetCode题解,在不断地学习中,公众号内容也在不断充实,欢迎扫码关注