1 什么是JAVA 泛型?
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本 质是参数化类型,也就是说所操作的数据类型被指定为一个参数。比如我们要写一个排序方法, 能够对整型数组、字符串数组甚至其他任何类型的数组进行排序,我们就可以使用 Java 泛型。
2 引入泛型的动机
假设下,我们不用泛型,需要实现一个通用的类,用来处理不同类型的方法,我们则会用到Object来作为属性和方法参数,但是这样做有两个缺点:
每次使用时都需要强制转换成想要的类型
在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全
《Java 编程思想》中描述了,泛型出现是在于:创建容器类,JDK1.5泛型出来之后,很多集合用泛型保存不同类型的元素,概况的来讲主要有以下几点:
(1)类型安全
泛型的主要目标是提高 Java 程序的类型安全 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常 符合越早出错代价越小原则
(2)消除强制类型转换
泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换 所得即所需,这使得代码更加可读,并且减少了出错机会
(3)为性能考虑
由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改 所有工作都在编译器中完成 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已
3 泛型使用
3.1 泛型类
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一 样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开
一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数, 这些类被称为参数化的类或参数化的类型。
public class A<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
3.2 泛型接口
和泛型类一样,泛型接口在接口名后添加类型参数,比如以下 PhoneInterface
public interface PhoneInterface<T>{
void doSomething(T t);
}
3.3 泛型方法()
使用泛型的方法,比如我下面写的一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数 类型,编译器适当地处理每一个方法调用。
// 泛型方法 打印 Array
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
}
(1)<? extends T>表示该通配符所代表的类型是 T 类型的子类。
(2)<? super T>表示该通配符所代表的类型是 T 类型的父类。
4 泛型通配符
泛型中有三种通配符形式:
(1)<?>无限制通配符
(2)<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
(3)<? super E> super 关键字声明了类型的下界,表示参数化类型可能是指定类型,或者是此类型的父类。
4.1 无限制通配符 < ?>
要使用泛型,但是不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。
? 和 Object 不一样,List<?> 表示未知类型的列表,而 List表示任意类型的列表。
如传入1个 List
,这时 List 的元素类型就是 String,想要往 List 里添加一个 Object,这当然是不可以的。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
如果传入的类型不是 E 或者 E 的子类,编辑不成功
泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
4.3 下界通配符 < ? super E>
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类
4.4 通配符该怎么选择?
(1)无限制通配符 < ?> 和 Object 有些相似,用于表示无限制或者不确定范围的场景。
(2)< ? super E> 用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。
(3)< ? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象
一言以蔽之:
如果参数化类型结果要返回一个T,使用 < ? extends T>;(T 的子类)
操作一个类型 T ,比如容器得是T的父类,就使用 < ? super T>;(T 的父类)
举个例子:
//求容器最大元素
private <E extends Comparable<? super E>> E getMaxListElement(List<? extends E> e){
if(e == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator<? extends E> iterator = e.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if(next.compareTo(result)>0){
result = next;
}
}
return result;
}
5 类型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛 型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个 过程就称为类型擦除。
类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般 是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换 成具体的类。
6 泛型规范
(1)泛型的参数类型只能是类(包括自定义类),不能是简单类型。
(2)同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
(3)泛型的类型参数可以有多个
(4)泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”
(5)泛型的参数类型还可以是通配符类型,例如 Class
7 特别注意
(1)数组中不能使用泛型
Array 不支持泛型,应使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能
(2)Java 中 List和原始类型 List 区别
在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查
通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String 或 Integer
你可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误
8 小结
(1)使用 Object 来达到复用,会失去泛型在安全性和直观表达性上的优势
(2) 泛型可以用在方法,类,接口,通过擦除实现
(3)泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码可以和没有使用泛型的代码互用
(4)Array 不支持泛型,建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而array无
(5)可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误
一起探索架构技术,拥抱AI,欢迎与我交流!