引言
首先我们要清楚什么是泛型?
泛型的意思是 类型参数化。
到底什么是类型参数化呢?通过这一节内容,我们希望大家能够彻底弄懂什么是泛型,以及如何在开发中使用泛型。
Java泛型应用是java核心基础之一,从java5开始引入泛型概念。如果你曾经使用过java中的collection相关的类,那么就算你已经接触过泛型了。在java的Collection中使用泛型是一件很简单的事情,可泛型还具有许多你想不到的作用。在深入了解泛型之前,首先来了解一下泛型的一些基本概念与原理。
1.泛型的引入
Java泛型的应用可以提高代码的复用性,同时泛型提供了类型检查,减少了数据的类型转换,同时保证了类型的安全。下面来看一下,泛型如何保证了类型的安全:
List list = new ArrayList();
list.add("abc");
list.add(new Integer(1)); //可以通过编译
for (Object object : list) {
System.out.println((String)object);//抛出ClassCastException异常
}
上面的代码会在运行时抛出ClassCastException,因为它尝试将一个Integer转换为String。接着,来看一下从java5开始,Collection的用法:
List<String> list = new ArrayList<>();
list.add("abc");
//list.add(new Integer(1)); //编译错误
for (String string : list) {
System.out.println(string);//无需任何强制类型转换
}
注意到,List的创建增加了类型参数String,因此只能向list添加String类型对象,添加其他对象会抛出编译异常;同样可以注意到,foreach循环不需要再添加任何强制类型转换,也就移除了运行时的ClassCastException异常。
2.泛型类与接口
既然是学泛型,自然就要知道如何去使用泛型定义自己的类和接口。同时为了加深理解泛型的作用,我们先定义一个不适用泛型的类:
public class Gen {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
Gen gen = new Gen();
gen.setObj("abc");
String str = (String) gen.getObj();//类型转换,可能会引起运行时ClassCastException
}
}
原始类的定义,容易引发ClassCastException,因为在使用的时候我们无法知道具体的类型到底是什么。现在来看一下泛型类来重新定义Gen — 使用<>指定泛型参数,如下:
public class Gen<T> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
Gen<String> gen = new Gen<>();
gen.setObj("abc");
// gen.setObj(10); //无法通过编译
String str = gen.getObj(); //无需类型转换
//-----------------------------
Gen gen2 = new Gen();//raw type原始类型
gen2.setObj("abc");
gen2.setObj(10); //可以通过编译,自动装箱将10转化为Integer对象
Integer num = (Integer) gen2.getObj();//使用了强制类型转换
}
}
细心的你会发现在main()方法里是使用泛型类型Gen<String>,便不再需要强制类型转换,也就移除了运行时的ClassCastException。同时为了区别,在此也定义了一个没有使用泛型类型的gen2,这时,编译器会弹出一个警告“Gen is a raw type,References to generic type Gen<T> should be parameterized”。当我们不提供泛型类型时,会默认使用Object会代替,也是因此这样,gen2可以设置String和Integer类型,不过,我们应尽量去避免这种这种情况的出现,如此,便又需要用到强制类型转换,也伴随着运行时的ClassCastException异常。
ps:可以使用@SuppressWarnings("rawtypes")来忽略编译器弹出警告。
接口的泛型应用和类的泛型应用非常类似:
public interface List <E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
另外,在定义泛型类和泛型接口的时候,我们也可以定义多个泛型化参数。例如Java中的Map<K,V>。
3.泛型类的使用
在使用泛型类的时候,我们就需要将泛型参数具体化,例如:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("ABC");
list.add(123);//报错!由于泛型已经具体化成String类型,就不能使用整数类型了
}
需要注意的是,当我们将泛型参数具体化成String类型之后,原本ArrayList中的add(E e)方法的参数类型就会变成String类型,这时候在调用add()方法的时候,就只能传入String类型的参数了。
另外,在Java中明确的规定了泛型参数在具体化的时候只能使用引用类型,所以是有的泛型参数必须使用Object类型及其子类类型,如果使用基本类型,就会出错:
//这里不能使用基本类型来具体化泛型参数
ArrayList<int> list = new ArrayList<>();
泛型类的继承和泛型接口的实现
我们在实现泛型接口的时候,也必须要定义泛型类去实现泛型接口。例如:
public class ArrayList<E> implements List<E>{
}
4.泛型的命名规范
为了更好地去理解泛型,我们也需要去理解java泛型的命名规范。为了与java关键字区别开来,java泛型参数只是使用一个大写字母来定义。各种常用泛型参数的意义如下:
E — Element,常用在java Collection里,如:List<E>,Iterator<E>,Set<E>
K,V — Key,Value,代表Map的键值对
N — Number,数字
T — Type,类型,如String,Integer等等
S,U,V etc. - 2nd, 3rd, 4th 类型,和T的用法一样。
当然,你如果硬是要标新立异,使用其它的字母或单词,也是可以使用的,只是这样,代码的规范度就大大下降了。
5.泛型方法与构造方法
有些时候,我们可能并不希望将整个类都泛型化。这个时候我们就可以只在某个方法上定义泛型,构造方法也是一样。例如:
public class GenMethod {
public static <T> void fromArrayToCollection(T[] a,Collection<T> c){
for (T t : a) {
c.add(t);
}
}
public static void main(String[] args) {
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
GenMethod.<Object>fromArrayToCollection(oa, co);
}
}
GenMethod 的代码不多,不过需要注意的地方却不少。第一、定义方法所用的泛型参数需要在修饰符之后添加,如上面的,public static <T>,如果有多个泛型参数,可如此定义<K,V>或者<T1,T2>。第二,不建议在泛型变量里添加其他类型,如下面的代码,将会引起编译错误(或隐含错误),如下:
public static <T> void fromArrayToCollection(T[] a,Collection<T> c){
for (T t : a) {
c.add(t);
c.add(new Object());
}
}
泛型构造方法的定义和泛型方法类似:
public class Gen {
public <T> Gen(T t){
}
}
6.泛型的继承与子类型
如果两个类之间有继承被被继承的关系,那么我们就可以将一个类的对象赋值类另外一个类的对象,比如:
String str = new String();
Object obj = new Object();
obj = str;
这种关系同样适用于泛型。比如我们将泛型参数设置为Number,那么在随后的调用中,就只需要传入一个Number类型或者是Number的子类类型的对象就行了,比如Integer,Float,Double都可以:
ArrayList<Number> list = new ArrayList<>();
list.add(new Integer(1));
list.add(new Float(1.0));
list.add(new Double(1.0));
但是有一种情况是我们需要特别注意的,比如我们定义一个如下的方法:
public void someMethod(ArrayList<Number> n) {
}
这个方法能接受什么样类型的参数类?
ArrayList<Nunber>?
ArrayList<Integer>?
ArrayList<Double>?
显然这个方法接受ArrayList<Nunber>类型的参数是没有问题的?而ArrayList<Integer>或者ArrayList<Double>类型都不行,原因是:虽然Integer和Double是Number类型的子类,但是ArrayList<Integer>和ArrayList<Double>类型并不是ArrayList<Number>类型的子类。
在泛型里也存在子类型,前提是其泛型参数的限制并没有发生改变,或者说泛型没有改变,其实就是从原来的类或接口来判断泛型的子类型。比如ArrayLIst<E> implements List<E>,而List<E> extends Collection<E>,那么ArrayList<E>就是List<E>的子类型,而List<E>又是Collection<E>的子类型。
7.泛型参数界限与通配符
有时候,你会希望泛型类型只能是某一部分类型,比如在操作数据的时候,你希望是Number或其子类类型。这个通常的做法就是给泛型参数添加一个参数。其定义的形式为:
<T extends ParentType>
这个定义表示T应该是Numner的子类型,T和Parant可以是类,也可以是接口,注意此处的extends表示的是ParentType的子类型,和继承是有区别的。
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u) {
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<String> integerBox = new Box<>();
integerBox.set("abc"); //能通过编译,因为T指定为String类型
// integerBox.inspect("abc");//不能通过编译,因为U必须是Number类型或其子类
integerBox.inspect(new Integer(10));
}
}