目录:
- 什么是泛型
- 泛型类
- 泛型接口
- 泛型通配符
- 泛型方法
- 泛型通配符的上下边界
- 泛型数组
- 泛型的类型擦除
1. 什么是泛型
- 1.1 泛型的概念
泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候在指定具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。
泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父 类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于 java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个 Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多 ClassCastException的风险就会被转嫁到程序运行期之中。
- 1.2 Java的伪泛型
泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中 (Intermediate Language,中间语言,这时候泛型是一个占位符)或是运行期的CLR中都是切实存在的,List<int>与 List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<int>与 ArrayList<String>就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。
- 1.3 泛型的意义
使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。
泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。
2. 泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
- 2.1 语法
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
- 2.2 使用
public class TestGeneric {
private static final class MyData<T> {
private final T key;
MyData(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
public static void main(String[] args) {
MyData<String> myData = new MyData<>("kuang");
// 打印输出kuang
System.out.println(myData.getKey());
}
}
- 2.3 注意事项
(1) 泛型的类型参数只能是类类型,不能是基本数据类型,如int, short, byte, long, char。
(2) 不能对确切的泛型类型使用instanceof操作,否则编译时会出错。
// 不能是基本数据类型,否则编译期会报错
MyData<int> myData1 = new MyData<int>(1);
// 编译期会报 Illegal generic type for instanceof
System.out.println(myData instanceof MyData<String>);
3. 泛型接口
泛型接口与泛型类的定义及使用基本相同,泛型接口常被用在各种类的生产器中。
- 3.1 语法
public interface 接口名<T> {
public T method();
}
- 3.2 使用
private interface Factory<T> {
T newInstance();
}
private static final class Coder {
final String name;
final int age;
final String lang;
public Coder(String name, int age, String lang) {
this.name = name;
this.age = age;
this.lang = lang;
}
}
private static final class CoderFactory implements Factory<Coder> {
@Override
public Coder newInstance() {
return new Coder("Kuang", 28, "Java");
}
}
public static void main(String[] args) {
Factory factory = new CoderFactory();
Object object = factory.newInstance();
Coder coder = (Coder) object;
// 执行输出 Kuang - 28 - Java
System.out.println(coder.name + " - " + coder.age + " - " + coder.lang);
}
4. 泛型通配符
- 4.1 通配符的概念和例子
我们知道Ingeter
是Number
的一个子类,那么问题来了,在使用MyData<Number>
作为形参的方法中,能否使用MyData<Ingeter>
的实例传入呢?在逻辑上类似于MyData<Number>
和MyData<Ingeter>
是否可以看成具有父子关系的泛型类型呢?
private static void printMyDataKey(MyData<Number> myData) {
System.out.println(myData.getKey());
}
public static void main(String[] args) {
MyData<Integer> myData = new MyData<>(1);
MyData<Number> myData1 = new MyData<>(2);
// 这里将编译不过
printMyDataKey(myData);
printMyDataKey(myData1);
}
改成这样就能执行了:
private static void printMyDataKey(MyData<? extends Number> myData) {
System.out.println(myData.getKey());
}
类型通配符一般是使用'?'代替具体的类型实参,注意了,此处'?'是类型实参,而不是类型形参。
再直白点的意思就是,此处的'?'和Number、String、Integer一样都是一种实际的类型,可以把'?'看成所有类型的父类,是一种真实的类型。
可以解决当具体类型不确定的时候,这个通配符就是'?'。当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能,那么可以用'?'通配符来表未知类型。
- 4.2 ?和T的区别与联系
?和T都表示不确定的类型,但是两者还是有区别的。
(1) T是行参,?是实参。
(2) 如果是T的话,函数里面可以对T进行操作。
public static void printElement(ArrayList<?> al) {
Iterator<?> it = al.iterator();
while (it.hasNext()) {
System.out.println(it.next().toString());
}
}
使用T:
public static <T> void printElement(ArrayList<T> al) {
Iterator<T> it = al.iterator();
while (it.hasNext()) {
T next = it.next();
System.out.println(next.toString());
}
}
5. 泛型方法
上面的代码里也看到了泛型方法了,泛型方法,是在调用方法的时候指明泛型的具体类型。
- 5.1 语法
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
- 5.2 使用
private static final class MyData<T> {
private final T key;
MyData(T key) {
this.key = key;
}
public T getKey() {
return key;
}
}
private static final class MyMethod {
<T> void printMyDataKey(MyData<T> myData) {
T key = myData.getKey();
System.out.println(key);
}
<T> void printMyDataKeys(MyData<T>... myDatas) {
for (MyData<T> myData : myDatas) {
System.out.println(myData.getKey());
}
}
}
private static void printMyDataKey(MyData<? extends Number> myData) {
System.out.println(myData.getKey());
}
上面的例子可以看到普通泛型方法,静态泛型方法和可变参数泛型方法。
6. 泛型通配符的上下边界
- ? 通配符类型。
- <? extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类,最多为T,所以可以以T类型安全取出,但是不能存。
- <? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object,最少为T(包括T及其T的子类),可以以T类型安全存进去。
首先,泛型的出现是为了安全,所有与泛型相关的异常都应该在编译期间发现,因此为了泛型的绝对安全,java在设计时做了相关的限制:List<? extends E>表示该list集合中存放的都是E的子类型(包括E自身),由于E的子类型可能有很多,但是我们存放元素时实际上只能存放其中的一种子类型(这是为了泛型安全,因为其会在编译期间生成桥接方法<Bridge Methods>该方法中会出现强制转换,若出现多种子类型,则会强制转换失败)。
例如:
List<? extends Number> list = new ArrayList<Number>();
list.add(4.0); // 编译错误
list.add(3); // 编译错误
上例中添加的元素类型不只一种,这样编译器强制转换会失败,为了安全,Java只能将其设计成不能添加元素。虽然List<? extends E>不能添加元素,但是由于其中的元素都有一个共性:有共同的父类,因此我们在获取元素时可以将他们统一强制转换为E类型,我们称之为get原则。
对于List<? super E>其list中存放的都是E的父类型元素(包括E),我们在向其添加元素时,只能向其添加E的子类型元素(包括E类型),因为只能存放一种类型,E类型及其子类型是安全的,这样在编译期间将其强制转换为E类型时是类型安全的,因此可以添加元素。
例如:
List<? super Number> list = new ArrayList<Number>();
list.add(2.0);
list.add(3.0);
// 不能取,只能存,下面会编译错误
// Number number = list.get(0);
但是,由于该集合中的元素都是E的父类型(包括E),其中的元素类型众多,在获取元素时我们无法判断是哪一种类型,故设计成不能获取元素,我们称之为put原则。
实际上,我们采用extends,super来扩展泛型的目的是为了弥补例如List<E>只能存放一种特定类型数据的不足,将其扩展为List<? extends E> 使其可以接收E的子类型中的任何一种类型元素,这样使它的使用范围更广,List<? super E>同理。
- 6.1 extends使用
static class Food {
}
static class Fruit extends Food {
}
static class Apple extends Fruit {
}
static class RedApple extends Apple {
}
public static void main(String[] args) {
/**
* ? extends Fruit:
* 具有任何从Fruit继承类型的列表,编译器无法确定List所持有的类型,所以无法安全的向其中添加对象。
* 可以添加null, 因为null 可以表示任何类型。
* 所以List的add方法不能添加任何有意义的元素,但是可以接受现有的子类型List<Apple> 赋值。
*/
List<? extends Fruit> flist = new ArrayList<Apple>();
// 下面三行都会编译出错
// flist.add(new Apple());
// flist.add(new RedApple());
// flist.add(new Object());
// 可以编译通过
flist.add(null);
// 由于,其中放置是从Fruit中继承的类型,所以可以安全地取出Fruit类型。可以取出来(null可以表示任何类型)
Fruit fruit = flist.get(0);
Apple apple = (Apple) flist.get(0);
// 在使用Collection中的contains方法时,接受Object 参数类型,可以不涉及任何通配符,编译器也允许这么调用。
flist.contains(new Fruit());
flist.contains(new Apple());
}
- 6.2 super的使用
public static void main(String[] args) {
/**
* List<? super Fruit> 表示具有任何Fruit超类型的列表,列表的类型至少是一个 Fruit 类型,
* 因此可以安全的向其中添加Fruit 及其子类型。
*
* 由于List<? super Fruit>中的类型可能是任何Fruit 的超类型,无法赋值为Fruit的子类型Apple的List<Apple>.
*/
List<? super Fruit> flist = new ArrayList<Fruit>();
flist.add(new Fruit());
flist.add(new Apple());
flist.add(new RedApple());
// compile error:
// List<? super Fruit> flist1 = new ArrayList<Apple>();
// 因为,List<? super Fruit>中的类型可能是任何Fruit 的超类型,
// 所以编译器无法确定get返回的对象类型是Fruit, 还是Fruit的父类Food 或 Object.compile error:
// Fruit item = flist.get(0);
}
7. 泛型数组
java中是"不能创建一个确切的泛型类型的数组"的。
也就是说下面的这个例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] ls = new ArrayList<?>[10];
这样也是可以的:
List<String>[] ls = new ArrayList[10];
看看sun文档提供一个例子:
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面采用通配符的方式是被允许的: 数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
8. 泛型的类型擦除
Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉。正确理解泛型概念的首要前提是理解类型擦出(type erasure)。
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉,这个过程就称为类型擦除。
如在代码中定义的List<object>和List<String>等类型,在编译后都会编程List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。
public static void main(String[] args) {
ArrayList<String> arrayList1 = new ArrayList<String>();
arrayList1.add("abc");
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass() == arrayList2.getClass());
}
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型,只能存储字符串。一个是ArrayList<Integer>泛型类型,只能存储整型。最后,我们通过arrayList1对象和arrayList2对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。
再来看一个例子:
public static void main(String[] args) throws IllegalArgumentException, SecurityException,
IllegalAccessException, InvocationTargetException, NoSuchMethodException {
ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
// 这样调用add方法只能存储整形,因为泛型类型的实例为Integer
arrayList3.add(1);
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i = 0; i < arrayList3.size(); i++) {
System.out.println(arrayList3.get(i));
}
}
在程序中定义了一个ArrayList泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。