文章目录
一、什么是泛型
1.1 泛型简介
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
java 中泛型标记符:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的 java 类型
1.2 用一个例子认识泛型
举个例子来体会下上面说的功能:我们的需求是,设计一个“可变长度”的数组,可以存取任何类型的数据。
方案一:使用Object[]型数组
我们可以使用一个Object[]数组,配合存储一个当前分配的长度,就可以充当“可变数组”,用来存储“任何类型”的数据了:
public class MyArrayList {
private Object[] array;
private int size;
public void add(Object e) {...}
public void remove(int index) {...}
public Object get(int index) {...}
}
当我们想存储 String 类型的数据时:
MyArrayList list = new MyArrayList();
list.add("Hello");
// 获取到Object,必须强制转型为String:
String value = (String) list.get(0);
当我们想存储 Integer 类型的数据时:
MyArrayList list = new MyArrayList();
list.add(new Integer(123));
// 获取到Object,必须强制转型为Integer:
Integer value = (Integer) list.get(0);
这样能满足需求,但存在一些缺陷:
- 需要强制转型
- 不方便,易出现“误转型”,报ClassCastException
例如,我们容易一不留神就出现这样问题:
MyArrayList list = new MyArrayList();
// 将 String 和 Integer 当作Object 混合存储
list.add("Hello");
list.add(new Integer(123));
String value = (String) list.get(0);
// ERROR: ClassCastException:
String value = (String) list.get(1);
方案二:为每一种类型数据单独编写一种ArrayList
要解决方案一的缺陷,我们可以为每一种类型数据单独编写一种ArrayList:
为String单独编写一种StringArrayList:
public class StringArrayList {
private String[] array;
private int size;
public void add(String e) {...}
public void remove(int index) {...}
public String get(int index) {...}
}
这样一来,存入的必须是String,取出的也一定是String,不需要强制转型,因为编译器会强制检查放入的类型:
StringArrayList list = new StringArrayList();
list.add("Hello");
String first = list.get(0);
// 编译错误: 不允许放入非String类型:
list.add(new Integer(123));
同样的,再为 Integer 类型编写个 IntegerArrayList,为Long类型编写个 LongArrayList,为 Double类型编写个 DoubleArrayList······
JDK的class就有千百个,这还不包含其他人写的,这个路子看样子也是行不通了。
方案三:使用泛型
我们把ArrayList变成一种模板:ArrayList<T>,代码如下:
public class ArrayList<T> {
private T[] array;
private int size;
public void add(T e) {...}
public void remove(int index) {...}
public T get(int index) {...}
}
T可以是任何class。上面的ArrayList就是Java标准库提供的ArrayList定义。
这样一来,我们就实现了:编写一次模版,可以创建任意类型的ArrayList:
// 创建可以存储String的ArrayList:
ArrayList<String> strList = new ArrayList<String>();
// 创建可以存储Float的ArrayList:
ArrayList<Float> floatList = new ArrayList<Float>();
// 创建可以存储Person的ArrayList:
ArrayList<Person> personList = new ArrayList<Person>();
泛型就是定义了一种模板,例如ArrayList<T>,然后在代码中为用到的类创建对应的ArrayList<类型>:
ArrayList<String> strList = new ArrayList<String>();
由编译器针对类型作检查:
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!
这样一来,既实现了编写一次,万能匹配,又通过编译器保证了类型安全:这就是泛型。
二、使用泛型
2.1 使用泛型类:以ArrayList< T>为例
泛型类一般用在集合类中,例如ArrayList<T>,下面以其为例,看看泛型的一般使用方式。
2.1.1 要定义泛型类型
使用ArrayList时,如果不定义泛型类型时,泛型类型实际上就是Object,相当于只把<T>当作Object使用,没有发挥泛型的优势。
// 编译器警告:
List list = new ArrayList();
list.add("Hello");
list.add("World");
String first = (String) list.get(0);
String second = (String) list.get(1);
当我们定义泛型类型<String>后,List<T>的泛型接口变为强类型List<String>:
// 无编译器警告:
List<String> list = new ArrayList<String>();
list.add("Hello");
list.add("World");
// 无强制转型:
String first = list.get(0);
String second = list.get(1);
2.1.2 可省略编译器能推断出的泛型类型
编译器如果能自动推断出泛型类型,就可以省略后面的泛型类型。例如,对于下面的代码:
List<String> list = new ArrayList<String>();
编译器看到泛型类型List<Number>就可以自动推断出后面的ArrayList<T>的泛型类型必须是ArrayList<Number>,因此,可以把代码简写为:
// 可以省略后面的String,编译器可以自动推断泛型类型:
List<String> list = new ArrayList<>();
2.2 使用泛型接口:以Comparable< T>为例
除了ArrayList<T>使用了泛型,还可以在接口中使用泛型。例如,Arrays.sort(Object[])可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>这个泛型接口:
public interface Comparable<T> {
/**
* 返回负数: 当前实例比参数o小
* 返回0: 当前实例与参数o相等
* 返回正数: 当前实例比参数o大
*/
int compareTo(T o);
}
可以直接对String数组进行排序:
public class Main {
public static void main(String[] args) {
String[] ss = new String[] { "Orange", "Apple", "Pear" };
Arrays.sort(ss);
System.out.println(Arrays.toString(ss));
}
}
//输出结果:
[Apple, Orange, Pear]
这是因为String本身已经实现了Comparable<String>接口:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {......}
如果换成我们自定义的Person类型试试:
public class Person {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return this.name + "," + this.score;
}
public static void main(String[] args) {
Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps);
System.out.println(Arrays.toString(ps));
}
}
运行程序,我们会得到ClassCastException:
Exception in thread "main" java.lang.ClassCastException: genericsDemo.Person cannot be cast to java.lang.Comparable
at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
at java.util.Arrays.sort(Arrays.java:1246)
at genericsDemo.Person.main(Person.java:33)
即无法将Person转型为Comparable。
我们修改代码,让Person实现Comparable<T>接口:
package genericsDemo;
import java.util.Arrays;
/**
* @author xiongkunworkmac
* @date 2022/03/13
*/
public class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
@Override
public String toString() {
return this.name + "," + this.score;
}
public static void main(String[] args) {
Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps);
System.out.println(Arrays.toString(ps));
}
}
运行得到结果:
[Alice,88, Bob,61, Lily,75]
所以,我们可以在接口中定义泛型类型,实现此接口的类必须实现正确的泛型类型。
三、编写泛型
3.1 编写泛型方法
在需要的时候,我们可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 <E>)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
我们举个例子,来验证上面各种规则:
public class GenericsMethodDemo {
/**
* 单类型参数 无返回值方法
* 功能:能打印各种类型的数组
* @param inputArray 输入数组
* @param <E> 类型参数声明
*/
public static <E> void printAnyTypeArray(E[] inputArray){
for(E e : inputArray){
System.out.print(e + " ");
}
System.out.println();
}
/**
* 单类型参数 有回值方法,返回 int
* 功能:返回输入数组的长度
* @param inputArray
* @param <E>
* @return
*/
public static <E> int getArrayLength(E[] inputArray){
return inputArray.length;
}
/**
* 单类型参数 有回值方法,返回 E
* 功能:打印数组最后的一个元素
* @param inputArray
* @param <E>
* @return
*/
public static <E> E getLastElement(E[] inputArray){
return inputArray[inputArray.length-1];
}
/**
* 两个类型参数 无返回值方法
* 打印两个数组信息
* @param inputArray1
* @param inputArray2
* @param <E>
* @param <T>
*/
public static <E,T> void twoArrayInfo(E[] inputArray1, T[] inputArray2){
System.out.println("第一个数组的类型是: " + inputArray1.getClass().getName() );
System.out.println("第二个数组的类型是: " + inputArray2.getClass().getName() );
}
public static void main(String[] args){
String[] strings = {"hello","world","!"};
Integer[] integers = {1,2,3,4,5,6};
Double[] doubles = {1.1, 2.2, 3.3,4.4,5.5,6.6};
System.out.println("***********验证 单个类型参数 相关方法*********");
System.out.println("打印 String 型数组:");
GenericsMethodDemo.printAnyTypeArray(strings);
System.out.println("数组长度是:" + GenericsMethodDemo.getArrayLength(strings));
String stringResult = GenericsMethodDemo.getLastElement(strings);
System.out.println("String 型数组的最后一个元素是: " + stringResult);
System.out.println("\n打印 Integer 型数组:");
GenericsMethodDemo.printAnyTypeArray(integers);
System.out.println("数组长度是:" + GenericsMethodDemo.getArrayLength(integers));
Integer integerResult = GenericsMethodDemo.getLastElement(integers);
System.out.println("String 型数组的最后一个元素是: " + integerResult);
System.out.println("\n打印 Double 型数组:");
GenericsMethodDemo.printAnyTypeArray(doubles);
System.out.println("数组长度是:" + GenericsMethodDemo.getArrayLength(doubles));
Double doubleResult = GenericsMethodDemo.getLastElement(doubles);
System.out.println("String 型数组的最后一个元素是: " + doubleResult);
System.out.println("***********验证 两个类型参数 相关方法*********");
System.out.println("\n打印 两个 数组信息:");
GenericsMethodDemo.twoArrayInfo(strings,doubles);
}
}
输出结果:
***********验证 单个类型参数 相关方法*********
打印 String 型数组:
hello world !
数组长度是:3
String 型数组的最后一个元素是: !
打印 Integer 型数组:
1 2 3 4 5 6
数组长度是:6
String 型数组的最后一个元素是: 6
打印 Double 型数组:
1.1 2.2 3.3 4.4 5.5 6.6
数组长度是:6
String 型数组的最后一个元素是: 6.6
***********验证 两个类型参数 相关方法*********
打印 两个 数组信息:
第一个数组的类型是: [Ljava.lang.String;
第二个数组的类型是: [Ljava.lang.Double;
3.2 编写泛型类
通常来说,泛型类一般用在集合类中,例如ArrayList<T>,我们很少需要编写泛型类。但如果我们确实需要编写一个泛型类,那么,也应该知道如何编写它。
泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
还是用例子来说明:
public class Box<T, V> {
private T obj1;
private V obj2;
public Box(T o1,V o2) {
obj1 = o1;
obj2 = o2;
}
public void showTypes() {
System.out.println("Type of T is " + obj1.getClass().getName());
System.out.println("Type of V is " + obj2.getClass().getName());
}
public T getObj1() {
return obj1;
}
public V getObj2() {
return obj2;
}
public static void main(String[] args) {
//创建对象时必须为Box传递两个类型参数,这里Integer替换T,String替换V。
Box<Integer,String> testObj1 = new Box<>(123,"bigBear");
testObj1.showTypes();
int t1 = testObj1.getObj1();
System.out.println("value: " + t1);
String v1 = testObj1.getObj2();
System.out.println("value: " + v1);
System.out.println("-----------------------------");
//在这个例子中,尽管两个类型参数是不同的,但是可以将两个类型参数设置为相同的类型。这T,V都是String类型
Box<String,String> testObj2 = new Box<>("bigBear","happyBird");
testObj2.showTypes();
String t2 = testObj2.getObj1();
System.out.println("value: " + t2);
String v2 = testObj2.getObj2();
System.out.println("value: " + v2);
}
}
运行结果如下:
ype of T is java.lang.Integer
Type of V is java.lang.String
value: 123
value: bigBear
-----------------------------
Type of T is java.lang.String
Type of V is java.lang.String
value: bigBear
value: happyBird
四、通配符extends、super、?
可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends (或 supper) 关键字,最后紧跟它的上界(或下界)。
4.1 上界通配符extends
通常在方法参数中会用到上界通配符extends,举个例子:
我们有个 add 方法的参数,希望接受Pair<Number>及Pair<Integer>、Pair<Long>等,即方法接收所有泛型类型为Number或Number子类的Pair类型。我们可以这样做:
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
//希望能支持所有Number或Number子类的参数,使用 <? extends Number> 做参数
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
public static void main(String[] args) {
Pair<Integer> p1 = new Pair<>(123, 456);
int result1 = add(p1);
System.out.println("Integer Pair 的结果是:" + result1);
Pair<Double> p2 = new Pair<>(123.1, 456.1);
int result2 = add(p2);
System.out.println("\nInteger Pair 的结果是:" + result2);
Pair<Long> p3 = new Pair<>(123L, 456L);
int result3 = add(p3);
System.out.println("\nLong Pair 的结果是:" + result3);
}
}
输出结果:
Integer Pair 的结果是:579
Integer Pair 的结果是:579
Long Pair 的结果是:579
这里add方法参数使用了 Pair<? extends Number> ,使得方法接收所有泛型类型为Number或Number子类的Pair类型。这样一来,给方法传入Pair<Integer>类型时,它符合参数Pair<? extends Number>类型。除了可以传入Pair<Integer>类型,我们还可以传入Pair<Double>类型,Pair<BigDecimal>类型等等,因为Double和BigDecimal都是Number的子类.
这种使用<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
关于 extends ,有些结论:使用类似<? extends Number>通配符作为方法参数时表示:
- 方法内部可以调用获取Number引用的方法,例如:Number n = obj.getFirst();;
- 方法内部无法调用传入Number引用的方法(null除外),例如:obj.setFirst(Number n);。
即一句话总结:使用extends通配符表示可以读,不能写。具体分析见 文档。
4.2 下界通配符super
和extends通配符相反,这次,我们希望方法参数接受Pair<Integer>类型,以及Pair<Number>、Pair<Object>,即方法接收所有泛型类型为Integer或Integer的父类的Pair类型(Number和Object是Integer的父类)。
我们可以这样做:
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
static void setSame(Pair<? super Integer> p, Integer n) {
p.setFirst(n);
p.setLast(n);
}
public static void main(String[] args) {
Pair<Number> p1 = new Pair<>(12.3, 4.56);
Pair<Integer> p2 = new Pair<>(123, 456);
setSame(p1, 100);
setSame(p2, 200);
System.out.println("p1 result: " + p1.getFirst() + ", " + p1.getLast());
System.out.println("p2 result: " + p2.getFirst() + ", " + p2.getLast());
}
}
注意到Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
使用类似<? super Integer>通配符作为方法参数时表示:
- 方法内部可以调用传入Integer引用的方法,例如:obj.setFirst(Integer n);;
- 方法内部无法调用获取Integer引用的方法(Object除外),例如:Integer n = obj.getFirst();。
**即一句话总结:使用super通配符表示只能写不能读。**具体分析见 文档。
4.3 对比extends和super通配符
我们再回顾一下extends通配符。作为方法参数,<? extends T>类型和<? super T>类型的区别在于:
- <? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
- <? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
我们来看Java标准库的Collections类定义的copy()方法:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}
它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。
这个copy()方法的定义就完美地展示了extends和super的意图:
- copy()方法内部不会读取dest,因为不能调用dest.get()来获取T的引用;
- copy()方法内部也不会修改src,因为不能调用src.add(T)。
这是由编译器检查来实现的。如果在方法代码中意外修改了src,或者意外读取了dest,就会导致一个编译错误:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
T t = dest.get(0); // compile error!
src.add(t); // compile error!
}
}
这个copy()方法的另一个好处是可以安全地把一个List<Integer>添加到List<Number>,但是无法反过来添加:
// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);
而这些都是通过super和extends通配符,并由编译器强制检查来实现的。
4.4 使用extends和super的PECS原则
何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。
还是以Collections的copy()方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T的src是生产者,因此声明为List<? extends T>,需要写入T的dest是消费者,因此声明为List<? super T>。
4.5 无限定通配符: ?
我们已经讨论了<? extends T>和<? super T>作为方法参数的作用。实际上,Java的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个?。
无限定通配符<?>很少使用,可以用<T>替换,同时它是所有<T>类型的超类。
例如:
void sample(Pair<?> p) {
}
因为<?>通配符既没有extends,也没有super,因此:
- 不允许调用set(T)方法并传入引用(null除外);
- 不允许调用T get()方法并获取T引用(只能获取Object引用)。
换句话说,既不能读,也不能写,那只能做一些null判断:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
大多数情况下,可以引入泛型参数<T>消除<?>通配符:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}
<?>通配符有一个独特的特点,就是:Pair<?>是所有Pair< T>的超类:
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
Pair<?> p2 = p; // 安全地向上转型
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
}
上述代码是可以正常编译运行的,因为Pair<Integer>是Pair<?>的子类,可以安全地向上转型。
五、参考文档
ref1. runoob.com: Java 泛型
ref2. 廖雪峰的官方网站:java泛型