泛型类
和泛型方法
有类型参数,这使得它们可以准确地描述用特定类型实例化时会发生什么。在有泛型类之前,程序员必须使用Object编写适用于多种类型的代码。这很烦琐,也很不安全。 随着泛型的引入,Java有了一个表述能力很强的类型系统,允许设计者详细地描述变量和方法的类型要如何变化。对于简单的情况,你会发现实现泛型代码很容易。不过,在更高级的情况下,对于实现者来说这会相当复杂。其目标是提供让其他程序员可以轻松使用的类和方法而不会出现意外。
因此就有了泛型的引入
Java 5中泛型的引入成为Java程序设计语言自最初发行以来最显著的变化。Java的一个主要设计目标是支持与之前版本的兼容性。因此,Java的泛型有一些让人不快的局限性。
泛型程序设计
意味着编写的代码可以对多种不同类型的对象重用。例如,你并不希望为收集String 和 File对象分别编写不同的类。
实际上,也不需要这样做,因为一个ArrayList类就可以收集任何类的对象。这就是泛型程序设计的一个例子。
实际上,在 Java有泛型类之前已经有一个ArrayList类。下面来研究泛型程序设计的机制是如何演变的,另外还会介绍这对于用户和实现者来说意味着什么。
在Java中增加泛型类之前,泛型程序设计是用继承实现的。ArrayList
类只维护一个0bject引用的数组
:
public class ArrayList{
private Object[] elementData;
...
public Object get(int i){
...
}
public void add(Object o){
...
}
}
对于上面的代码,仔细观察,会发现存在下面的问题
- 当获取一个值时必须进行强制类型转换。
`AllayList list = new ArrayList(); //创建一个对象
获取里面的元素,比如里面存放了一个String类型的数据
String str = (String) list.get(0);
- 没有错误检查
这个时候,可以向数组列表中添加任何类的值。
str.add(new File("..."));
对于上面的调用,编译和运行都不会出错。不过在其它地方,如果将get的结果强制类型转换为String类型,就会产生一个一个错误。
对于上面的两点进行测试
public class A1 {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("hello");
String str = (String) list.get(0);
System.out.println(str);
list.add(new File(""));
String str1 = (String) list.get(1);
}
}
然而泛型提供了一个更好的解决方案: 类型参数(type paraeter)。ArrayList类有一个类型参数用来指定元素的类型。
var str = new ArrayList
;
这样使得代码有更好的可读性。并且从代码中就可以看出里面存放的类型时 String类型的。
首先—> 使用类似ArrayList的泛型类很容易。只需要简单的调用,不需要有太多的思考。
大多数Java程序员都会使用类似ArrayList
这样的类型,就好像它们是Java内置的类型一样(就像String[]数组)。(当然,数组列表比数组更好,因为数组列表可以自动扩展。)
然后—> 实现一个泛型类可没有那么容易。使用你的代码的程序员可能会插入各种各样的类作为类型参数。他们希望一切都能正常工作,不会有恼人的限制,也不会有让人混乱的错误消息。因此,作为一个泛型程序员,你的任务就是要预计到你的泛型类所有可能的用法。
其次—> 这个任务会有多难呢?
下面是让标准类库的设计者们饱受折磨的一个典型问题。
ArrayList类有一个方法addAll,用来添加另一个集合的全部元素。一个程序员可能想要将一个ArrayList中的所有元素添加到一个ArrayList中去。
不过,当然反过来就不行了。如何允许前一个调用,而不允许后一个调用呢?
Java语言的设计者发明了一个具有独创性的新概念来解决这个问题,即通配符类型。通配符类型非常抽象,不过,利用通配符类型,构建类库的程序员可以编写出尽可能灵活的方法。
然而—>应用程序员很可能不会编写太多的泛型代码。JDK开发人员已经做出了很大的努力,为所有的集合类提供了类型参数。凭经验来说,如果代码中原本涉及大量通用类型(如Object或Comparable接口)的强制类型转换,只有这些代码才会因使用类型参数而受益。(比较认同)
泛型类( generic class))就是有
一个或多个类型变量的类
。下面将使用一个简单的B类作为例子。这个类使我们可以只关注泛型,而不用为数据存储的细节而分心。
public class B<T> {
private T first;
private T second;
public B(){
first = null;
second = null;
}
public B(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;
}
}
在上面的类 B 中,引入了一个
类型变量 T
,并且用<>
括起来,放在类名的后面。泛型类可以有多个类型变量。
比如public class C
,其中第一个和第二个是不同类型
类型变量在整个类的定义中用于指定方法的类型,字段的类型,局部变量的类型
比如
private T first
可以用具体的类型替换类型变量实例化泛型类型如B
可以将结果想象成一个普通的类,它可以有下面的构造器
B<String>;
B<String,String>;
以及下面的方法
String getFirst(){}
String getSecond(){}
void setFirst(String){}
也就是说,泛型相当于普通类的工厂
public class C {
public static void main(String[] args) {
String[] words = {"Mary","hard","a","title","lamb"};
B<String> mm = ArrayAlg.minmax(words);
System.out.println("min" + mm.getFirst());
System.out.println("max = "+ mm.getSecond());
}
}
class ArrayAlg{
public static B<String> minmax(String[] a){
if(a==null||a.length==0){
return null;
}
String min = a[0];
String 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 B<>(min,max);
}
}
如何定义一个泛型参数呢?
下面是定义泛型方法的方式。
class ArrayAlg{
public static <T> T getMiddle(T ... a){
return a(a.length/2);
}
}
这个方法是在普通类中定义的,而不是在泛型类中。不过,这是一个泛型方法,可以从尖括号和类型变量看出这一点。
注意,类型修饰变量放在修饰符(这里的修饰符就是public static)的后面,并在返回类型的前面。
泛型方法可以在普通类中定义,也可以在泛型类中定义。
当调用一个泛型方法的时候,可以把具体的类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlg.
getMidddle("John","Q","Public");
几乎在所有情况下,泛型方法的类型推导都能正常工作。偶尔,编译器也会提示错误,此时你就需要解译错误报告。考虑下面这个示例:
double middle = ArrayAlg.getMiddle(3.14,1729,0);
错误消息以晦涩的方式指出(不同的编译器给出的错误消息可能有所不同):解释这个代码有两种方式,而且这两种方式都是合法的。简单地说,编译器将把参数自动装箱为1个Double和2个Integer对象,然后寻找这些类的共同超类型。事实上,它找到了2个超类型:Number和 Comparable接口,Comparable接口本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数都写为double值。
有的时候,类或方法需要对于类型变量进行约束。
下面是一个典型的例子。
class ArrayAlg{
public static <T> T min(T[] a){
T smallest = a[0];
for(int i = 1;i<a.length;i++){
if(smallest.compareTo(a[i]>0)){
smallest = a[i];
}
}
return smallest;
}
}
但是,这里有一个问题。请看min方法的代码。变量smallest的类型为T,这意味着它可以是任何一个类的对象。如何知道T所属的类有一个compareTo方法呢?
解决这个问题的办法是限制T只能是实现了Comparable接口(包含一个方法 compareTo的标准接口)的类。可以通过对类型变量T设置一个限定(bound)来实现这一点:
public static T min(T[] a) . . .
实际上Comparable接口本身就是一个泛型类型。目前,我们忽略其复杂性以及编译器产生的警告。后面将会详细的介绍。
现在,泛型方法min只能在实现了Comparable接口的类(如String、LocalDate等)的数组上调用。由于Rectangle类没有实现Comparable接口,所以在Rectangle数组上调用min将会产生一个编译错误。
你或许会感到奇怪—在这里我们为什么使用关键字extends而不是implements ?
毕竟,Comparable是一个接口。下面的记法
表示 T 应该是限定类型( bounding type)的子类型( subtype)。T和限定类型可以是类,也可以是接口。选择关键字extends 的原因是它更接近子类型的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字(如sub)。
一个类型变量或通配符可以有多个限定,例如:T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量。
在Java的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
在下面的程序代码中,我们把minmax重写为一个泛型方法。这个方法可以计算泛型数组的最大值和最小值,并返回一个Pair。
package java核心技术;
import java.time.LocalDate;
/**
* @author weijiangquan
* @date 2022/9/15 -18:15
* @Description
*/
public class PairTest2 {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(1996,12,9),
LocalDate.of(1815,12,10),
LocalDate.of(1903,12,3),
LocalDate.of(1903,6,22),
};
B<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = " + mm.getFirst());
System.out.println("min = " + mm.getSecond());
}
}
class ArrayAlg{
public static <T extends Comparable> B<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 B<>(min,max);
}
}
虚拟机没有泛型类型对象—–所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件!在下面的小节中你会看到编译器如何“擦除”类型参数,以及这个过程对Java程序员有什么影响。
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型( raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除( erased),并替换为其限定类型(或者,对于无限定的变量则替换为0bject)。
如上面写到过的Pair类,将会变成如下
public class Pair{
private Object first;
private Object second;
public Pair(Object newValue){
first = newValue;
}
public Object getSecond(){
return second;
}
public void setFirst(Object newValue){
first = newValue;
}
public void setSecond(Object newValue){
second = newValue;
}
}
因为T是一个无限定的变量,所以直接用0bject替换。
结果是一个普通的类,就好像Java语言中引入泛型之前实现的类一样。
在程序中可以包含不同类型的 Pair,例如,Pair
原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。例如,类Pair中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定我们声明了一个稍有不同的类型:
或许,你会有疑问对于上面的代码