在泛型引入前,程序员必须使用Object编写适用于多种类型的代码,既繁琐(使用时需要强制类型转换,否则只能使用Object声明过的方法)又不安全(Java泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型)。
泛型定义
- 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
- 泛型类和泛型方法有类型参数,这使得它们可以准确地描述用特定类型实例化时会发生什么。
应用开发程序员可能不会编写很多泛型代码,而JDK开发人员则需要编写大量泛型代码。 如果代码中原本设计大量通用类型(如
Object
或Comarable
接口)的强制类型转换,那么这些代码会因为使用类型参数而受益。
1 类型参数
- 一个类型参数用于指定一个泛型类型名称的标识符。
- 类型参数命名
- 类型参数通常使用大写字母,而且很简短。
- Java库中使用E表示集合的元素类型,K和V分别表示表的键和值的类型,T(或者U和S)则表示“任意类型”
- 类型参数可以被用来声明返回值类型。
- 类型参数可以作为变量的类型
- 类型参数只能代表对象(引用类型),不能是基本类型。
2 泛型方法
- 泛型方法定义
- 泛型方法在调用时可以接收不同类型的参数。
根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
- 泛型方法既可以在普通类中定义,也可以在泛型类中定义
定义泛型方法的规则
- 泛型方法体的声明和其他方法基本一样,但是所有泛型方法声明都额外有一个类型参数声明部分(由尖括号分隔),可以包含一个或多个类型参数,参数间用逗号隔开。
- 类型参数声明部分在方法的返回类型之前,各种修饰符之后。
public static <E> void printArray(E[] inputArray){...}
实例
//泛型方法printArray
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5}; //不能是int[]类型
Character[] charArray = {'H', 'E', 'L', 'L', 'O'};
System.out.println("整型数组元素为:");
printArray(intArray); //out: 1 2 3 4 5
System.out.println("\n字符型数组元素为:");
printArray(charArray); //out: H E L L O
}
3 泛型类
泛型类定义
- 泛型类的声明和非泛型类的声明基本一样,但是泛型类在类名后面添加了类型参数声明部分。
- 类型参数声明部分的规则和泛型方法完全一样
- 在泛型类中,静态方法不能使用类所声明的泛型类型,但是可以使用自己声明的泛型类型。
实例化泛型类
- 泛型类对象的类型参数实际类型可以由泛型类变量所指定的类型决定(菱形语法,省略构造器中的类型参数),也可以不明确指定实际类型。
- 有多种方式实例化泛型类,如下例,实例化一个
HashMap
HashMap类定义
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {...}
实例化HashMap类的方法
HashMap<String, Integer> hashMap1 = new HashMap();
HashMap<String, Integer> hashMap2 = new HashMap<>(); //推荐使用
HashMap<String, Integer> hashMap3 = new HashMap<String, Integer>();
HashMap hashMap4 = new HashMap<String, Integer>();
var hashMap5 = new HashMap<String, Integer>();
HashMap hashMap6 = new HashMap();
- 泛型类的继承规则
无论**S**
与**T**
有什么关系,**Pair<S>**
与**Pair<T>**
都没有任何关系
Manager ceo = new Manager("崔奕宸", 1000);
Manager cfo = new Manager("邱雪静", 2000);
Pair<Manager> p = new Pair<>(ceo, cfo);
Employee ceo2 = ceo; //正确,多态
Pair<Employee> p = new Pair<>(ceo, cfo); //正确,多态
Pair<Employee> p2 = p; //错误,类型不兼容
Pair<? extends Employee> p3 = p; //正确,Pair<Manager>是Pair<? extends Employee>的子类
实例
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 first) { this.first = first; }
public T getSecond() { return second; }
public void setSecond(T second) { this.second = second; }
public static void main(String args[]) {
Pair<Integer> integerPair = new Pair(1, 2);
Pair<String> stringPair = new Pair("崔奕宸", "邱雪静");
System.out.println(integerPair.getFirst()); //out: 1
System.out.println(stringPair.getSecond()); //out: 邱雪静
}
}
4 类型参数的子类型限定
有时我们需要限制那些被允许传递到一个类型参数的类型种类范围。例如:
- 一个操作数字的方法可能只希望接收
Number
或者Number
子类的实例。此时就需要使用有界的类型参数。 - 一个进行比较的方法,只希望接收可以进行比较的类,因此需要限制类型参数为实现了
Comparable
接口的类
- 一个操作数字的方法可能只希望接收
声明有界的类型参数
语法
<T extends BoundingType>
表示T应该是限定类型(BoundingType)的子类型,BoundingType可以是类也可以是接口。
- Java选择关键字
extends
的原因是它更接近子类型的概念,并且不需要为Java再添加一个新的关键字。 - 一个类型参数可以有多个限定,不同的限定使用
**&**
分隔
一个类型参数最多有一个限定是类,而且如果有类限定,则必须是限定列表中的第一个
<T extends Comparable & Serializable>
泛型方法的类型参数可以进行限定,但不能在方法参数中进行限定
<T extends Xxx> void func(List<T> list){}; // 正确
<T extends Xxx> void func(T t){}; //正确
<T> void func(List<T extends Xxx> list){}; //编译错误
实例 ```java //比较三个值并返回最大值,如果不对T加以限定,那么可能传入不可比较的对象导致错误 public static
> T maximum(T x, T y, T z) { T max = x; if (y.compareTo(max) > 0) max = y;
if (z.compareTo(max) > 0)
max = z;
return max; }
public static void main(String args[]) { System.out.printf(“%d,%d和%d中最大的数为%d\n”, 3, 4, 5, maximum(3, 4, 5)); //out: 3,4和5中最大的数为5 System.out.printf(“%.1f,%.1f和%.1f中最大的数为%.1f\n”, 6.6, 8.8, 7.7, maximum(6.6, 8.8, 7.7)); //out: 6.6,8.8和7.7中最大的数为8.8 System.out.printf(“%s,%s和%s中最大的数为%s\n”, “pear”, “apple”, “orange”, maximum(“pear”, “apple”, “orange”)); //out: pear,apple和orange中最大的数为pear }
<a name="tsYXx"></a>
### 5 类型参数擦除
- 虚拟机没有泛型类型对象,所有对象都属于普通类。即**泛型类与普通类在Java虚拟机内没有任何区别**。
因此,编译器会“擦除”类型参数。
- **对于编译器来说,无论类型参数被指定为任何具体类型,都会被擦除为原始类型。**
- **原始类型的具体类型有两种情况**
- 类型参数存在限定:**原始类型为第一个限定**
- 类型参数没有限定:**原始类型为Object**
- **类型擦除后,编译器会在必要的地方自动插入强制类型转换**。
- **类型擦除的优点**
Java泛型与C++模板有很大区别,因为C++会为每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。Java则不会出现这一问题
- **实例**
类`Internal<T extends Comparable & Serializable>`擦除为原始类型`Interval`
```java
//擦除前
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
...
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) { lower = first; upper = second; }
else { lower = second;upper = first; }
}
...
}
//擦除后
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first, Comparable second) {...}
...
}
6 通配符
- 类型通配符定义
- 类型通配符是指使用
**?**
代替具体的类型参数,因此在需要传入具体类型的地方可以使用通配符。 - 如使用
List<?>
代替List<String>
、List<Integer>
等
- 类型通配符是指使用
可以说List<?>
是List<String>
、List<Integer>
等所有List<具体类型实参>
的父类型。
- 所有能用类型通配符
**?**
解决的问题都能用类型参数解决
- 实例
编写一个打印员工对的方法
public static void printBuddies(Pair<Employee> p){
Employee firstE = p.getFirst();
Employee secondE = p.getSecond();
System.out.println(firstE.getName() + " and " + secondE.getName() + " are buddies.");
}
由于Pair<Employee>
的限制,不能将Pair<Manager>
传递给这个方法,此时可以使用通配符来解决
public static void printBuddies(Pair<? extends Employee> p)
也可以使用泛型方法解决
public static <T extends Employee> void printBuddies(Pair<T> p)
- 通配符
**?**
的代表问题
**?**
只代表一个范围的类型,但没有被指定为任何具体类型
Manager ceo = new Manager("崔奕宸", 1000);
Pair<? extends Employee> p = new Pair<Manager>(); //多态
p.setFirst(ceo); //报错
Employee e = p.getFirst(); //多态
查看p的方法(不是真正的Java语法,但是可以看出编译器知道什么),如下
? extends Employee getFirst(){...};
void setFirst(? extends Employee){...};
这里
setFirst()
方法只知道需要Employee
的某个子类型,但是不知道具体是什么类型,因此拒绝接收任何特定的类型。- 将
getFirst()
的返回值赋给一个Employee
变量是合法的。在实际应用中一般不会出现这种问题,因为要对通配符和类型参数进行规约,如上述这种类型间存在依赖的情况,将会使用类型参数而不是通配符。
- 通配符的超类型限定
通配符限定与类型参数的限定十分类似(子类型限定),但是,通配符限定有额外的能力,即可以指定一个超类型限定
超类型语法格式
<? super BoundingType>
**<? extends BoundingType>**
和**<? super BoundingType>**
的区别<? extends BoundingType>
表示该通配符所代表的类型是**BoundingType**
的子类。<? super BoundingType>
表示该通配符所代表的类型是**BoundingType**
的父类。
- 使用超类型限定后的通配符,会产生与之前所说的通配符代表问题完全相反的情况
此时可以为? super Manager getFirst(){...};
void setFirst(? super Manager){...};
setFirst()
提供参数,但不能接收getFirst()
的返回值。
带有超类型限定的通配符允许写入一个泛型对象,而带有子类型限定的通配符允许读取一个泛型对象。
通配符和类型参数的区别
本质不同
- 类型参数: 是一个形参,可以理解为一个占位符,被使用时,会在程序运行的时候替换成具体的类型。可以作为一种变量类型。
- 通配符: 是一个实参,这是Java定义的一种特殊类型,比
Object
更特殊,就像一个无所不能的对象更胜于Object
。但是?
不是类型变量,不能将?
作为一种变量类型
代表和指定
- 类型参数:被指定(匹配)为一个具体类型,即在实际调用时,类型是确定的。
- 通配符:可以代表范围内的所有类型,但不知道具体代表的是什么类型
- 规约类型参数和通配符的使用
如果一个方法的返回值、某些参数的类型依赖另一个参数的类型就必须使用泛型方法(因为被依赖的类型如果是不确定的?
,那么其他元素就无法依赖它),否则,应该使用通配符**?**
- 实例
```java
void func(List l, T t) { l.add(t); } //有依赖关系,必须使用泛型方法
<a name="ior3W"></a>
### 7 泛型的局限
- 使用Java泛型时需要考虑一些限制,这些限制大多是由类型擦除引起的。
1. **不能用基本类型实例化类型参数**
1. **运行时类型只适用于原始类型**
```java
Pair<String> p1 = new Pair<>();
Pair<Integer> p2 = new Pair<>();
System.out.println(p1.getClass()==p2.getClass()); //out: true,因为原始类型都是Pair
System.out.println(p1 instanceof Pair<Integer>); //报错
System.out.println(p1 instanceof Pair); //out: true
- 在泛型类中,静态方法不能使用类所声明的泛型类型,但是可以使用自己声明的泛型类型。
- 泛型参数不能用于实例化对象,即形如语句
**new T()**
是禁止的,因为其相当于**new Object()**