Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
一. 为什么需要泛型?
如下代码所示,我们创建一个list,并向其中加入两个字符串类型的值,随后加入Integer值。此时编译器不会报错,因为此时List默认的类型是Object,随后我们忘记之前加入了Integer数据,之后我们取出元素转为String数据,即 1 处。编译时不会出错,但运行时,会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。
public class GenericTest {
public static void main(String[] args){
List list=new ArrayList();
list.add("string1");
list.add("string2");
list.add(3);
for (int i = 0; i <list.size() ; i++) {
String currentData=(String)list.get(i); // 1
System.out.println(i+": "+currentData);
}
}
}
运行结果:
0: string1
1: string2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at other.GenericTest.main(GenericTest.java:18)
Process finished with exit code 1
因此,我们发现,将对象放入List时,List是记不住对象的类型,该对象编译时为Object型,但运行时仍为对象本身类型。所以在1处运行时会报异常“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。
那么有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型。
二. 泛型
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)
下面例子采用泛型
public class GenericTest {
public static void main(String[] args) {
/**
List list=new ArrayList();
list.add("string1");
list.add("string2");
list.add(3);*/
List<String> list = new ArrayList<>();
list.add("string1");
list.add("string2");
//list.add(3);// 1 提示编译错误
for (int i = 0; i < list.size(); i++) {
String currentData = list.get(i);// 2
System.out.println(i + ": " + currentData);
}
}
}
采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List<String>,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。
我们来看看List接口的具体定义:
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
void clear();
boolean equals(Object o);
int hashCode();
E get(int index);
E set(int index, E element);
void add(int index, E element);
E remove(int index);
int indexOf(Object o);
int lastIndexOf(Object o);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int index);
List<E> subList(int fromIndex, int toIndex);
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}
可以看到,在list接口中采用泛型化定义后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。
三. 泛型接口、泛型类和泛型方法
1、泛型类
- 定义:泛型类的声明和非泛型类的声明类似,除了在类名后面增加类型参数声明部分。
- 我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。
- 泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。
定义一个泛型类:
通过<T> 声明告诉JVM返回值定义一个泛型 T这里的T只是个占位符的效果,26个字母随便写哪个字母都可以,但一定要是和< >里面相同的字母,这里使用T。
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<>();
name.add("memory");
Box<Integer> age = new Box<>();
age.add(24);
System.out.println("name: " + name.get());
System.out.println("age: " + age.get());
}
}
class Box<T> {
private T data;
public T get() {
return data;
}
public void add(T data) {
this.data = data;
}
}
编译以上代码,运行结果是:
name: memory
age: 24
Process finished with exit code 0
2、泛型擦除
对于传入的不同类型实参,生成的类对象实例的类型是否一样?
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<>();
name.add("memory");
Box<Integer> age = new Box<>();
age.add(24);
System.out.println("name类的类型:"+name.getClass());
System.out.println("age类的类型:"+age.getClass());
System.out.println(name.getClass()==age.getClass());
}
}
编译以上代码,运行结果:
name类的类型:class other.泛型.Box
age类的类型:class other.泛型.Box
true
Process finished with exit code 0
在使用泛型类时,虽然传入不同类型的实参,但在内存上还是同一个类,没有生成真正不同的类型。在逻辑上我们可以理解为不同的泛型类型。
- Java中的泛型只是作用于代码编译阶段,编译过的class文件不包含任何泛型信息。(在编译过程中对于正确检验泛型结果后,会将泛型的相关信息擦出,泛型信息不会进入到运行时阶段)
2、泛型方法:
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
下面是定义泛型方法的规则:
所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。
每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。并且还要注意的一点是,Java中没有所谓的泛型数组一说。
实例:
public class GenericTest {
public static void main(String[] args) {
Integer[] integerArray = new Integer[]{1, 2, 3, 4};
System.out.println("Integer数组改变前的数据是:");
printArrray(integerArray);
changeData(integerArray);
System.out.println("Integer数组改变后的数据是:");
printArrray(integerArray);
Character[] characterArray = new Character[]{'a', 'b', 'c', 'd'};
System.out.println("Character数组改变前的数据是:");
printArrray(characterArray);
changeData(characterArray);
System.out.println("Character数组改变后的数据是:");
printArrray(characterArray);
}
public static <E> changeData(E[] array) {
if (array == null || array.length == 0) {
return;
}
for (int i = 0, j = array.length - 1; i < array.length / 2; i++, j--) {
E temp;
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
public static <E> void printArrray(E[] array) {
if (array == null || array.length == 0) {
return;
}
for (E element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
编译以上代码,程序执行结果是:
Integer数组改变前的数据是:
1 2 3 4
Integer数组改变后的数据是:
4 3 2 1
Character数组改变前的数据是:
a b c d
Character数组改变后的数据是:
d c b a
Process finished with exit code 0
四. 类型通配符
1. 为什么需要类型通配符
我们看一下例子:
Box<Number>和Box<Integer>实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Box<Number>和Box<Integer>是否可以看成具有父子关系的泛型类型呢?
为了弄清楚,我们看以下的例子:
public class GenericTest {
public static void main(String[] args) {
Box<Number> score= new Box<>(39.3);
Box<Integer> age=new Box<>(18);
getData(score);
/**The method getData(Box<Number>) in the type GenericTest is not applicable for
the arguments (Box<Integer>)*/
//getData(age); // 1
}
public static void getData(Box<Number> data){
System.out.println(data.get());
}
}
通过提示信息,我们知道在逻辑上Box<Number>不能视为Box<Integer>的父类。因此,我们需要一个在逻辑上可以用来表示同时是Box<Integer>和Box<Number>的父类的一个引用类型,由此,类型通配符应运而生。
2.类型通配符的定义
类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer> 等所有List<具体类型实参>的父类。
实例:
public class GenericTest {
public static void main(String[] args) {
List<String> name=new ArrayList<>();
List<Integer> age=new ArrayList<>();
List<Number> number=new ArrayList<>();
name.add("memory");
age.add(18);
number.add(90.7);
getDataOfList(name);
getDataOfList(age);
getDataOfList(number);
}
public static void getDataOfList(List<?> data){
System.out.println("当前数据是:"+data.get(0));
}
}
程序执行结果:
当前数据是:memory
当前数据是:18
当前数据是:90.7
Process finished with exit code 0
五. 类型通配符上下限
有时候想限制传入到类型参数的类型种类范围。例如:一个操作数字仅允许传入Number和Number子类的实例。这就是有界类型参数的种类。
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。
类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反。
实例:该实例的泛型方法,返回三个可比较对象的最大值。
public class MaximumTest {
public static void main(String[] args) {
System.out.print(String.format("%d,%d,%d 中的最大值是:%d\n", 2, 5, 1, getMax(2, 5, 1)));
System.out.print(String.format("%.1f,%.1f,%.1f 中的最大值是:%.1f\n", 2.2, 5.5, 1.1, getMax(2.2, 5.5, 1.1)));
System.out.print(String.format("%s,%s,%s 中的最大值是:%s\n", "apple", "orange", "pear", getMax("apple", "orange", "pear")));
//getMax(new MyObject(1),new MyObject(2),new MyObject(3));//1 编译出错
/**在(//1)处会出现错误,因为getMax()方法中的参数已经限定了
参数泛型上限为Comparable,所以泛型为String是不在这个范围之内,所以会报错*/
}
public static <E extends Comparable<E>> E getMax(E x, E y, E z) {
E max = x;
if (y.compareTo(max) > 0) {
max = y;
}
if (z.compareTo(max) > 0) {
max = z;
}
return max;
}
}
class MyObject {
int age;
public MyObject(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
程序执行结果:
2,5,1 中的最大值是:5
2.2,5.5,1.1 中的最大值是:5.5
apple,orange,pear 中的最大值是:pear
Process finished with exit code 0