一般的类或方法,只能使用具体类型:要么是基本类型,要么是自定义类型。如果要编写应用于多种类型的代码,这种刻板的限制对代码的束缚很大。有时候我们希望编写一个更通用的代码,使得代码能够应用于“某种不具体类型”,而不是具体的接口或类。--所以使用泛型。
下面举一些例子说明泛型的基本用法。
泛型类
package com.john.generic;
public class Pair<T> {
public final T a;
public final T b;
protected Pair(T a, T b) {
super();
this.a = a;
this.b = b;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Pair [a=" + a + ", b=" + b + "]";
}
public static void main(String[] args) {
System.out.println(new Pair<String>("hi", "hello"));
}
}
泛型的主要目的之一:用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
上述Pair类的类型参数T最后被擦除成Object,有编译器负责在边界插入类型转换代码。
泛型接口
package com.john.generic;
//定义一个泛型接口,工厂接口,用于产生T类型对象
public interface Factory<T> {
T create();
}
package com.john.generic;
//Factory实现类,Integer对象工厂
public class IntegerFactory implements Factory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
//持有T类型对象的类,需要从对象工厂中获得对象,切记这里不能使用T t= new T();来生成对象,因为类型信息被擦除。
public class Foo<T> {
//假设这个类要使用T这个实例,不能使用 T t=new T() 由于运行时类型擦除。
//只能使用Class的newInstance()方法。
private T t;
//传入的参数可以是Factory接口的实现类
public <F extends Factory<T> > Foo(F factory) {
t=factory.create();
}
@Override
public String toString() {
return "t:"+t;
}
public static void main(String[] args) {
Factory factory=new IntegerFactory();
Foo<Integer> foo=new Foo<>(factory);
System.out.println(foo);
}
}
泛型方法
package com.john.generic;
public class GenericMethods {
public static <T> void f(T t)
{
System.out.println(t.getClass().getName());
}
public static void main(String[] args) {
//使用泛型方法时通常不用指定类型参数,好像f()被无限次重载过,实际上编译器会根据输入参数为我们找出具体的参数类型,这个叫类型参数推断。
//类型推断只对赋值操作有效,其他时候不起作用。
f("");
//有些情况编译器无法正确推断类型参数时,我们可以显示的指定它
GenericMethods.<Double>f(1.0);
}
}
上面举例类一些泛型的简单实用,下面介绍一下擦除的神秘之处
接上面Pair<T>
做一个测试
public static void main(String[] args) {
Pair<String>p=new Pair<String>("hi", "hello");
System.out.println(p.getClass().getTypeParameters()[0]);
}
结果居然返回了T,即仅仅返回了占位符标识,并没有有用的信息。我之前做过一个类似 class Pair2 extents Pair<String>{}
如何在Pair2对象中获得泛型String.class,可以参考 java 基础之ParameterizedType。
回归正题,也就是Pair<String>
最终编译后为Pair.class
而不是Pair<String>.class
最后在写代码时,Pair对象中的T无法使用真实类型中的方法,由于类型被擦除,若想使用T中的方法或数据,必须给T设置边界,具体怎么设置边界看一个例子。
public class Animal {
public void eat(){
System.out.println("i'am animal ,i'am eating food...");
}
}
public class Cat extends Animal{
public void eat(){
System.out.println("i'am cat ,i'am eating food...");
}
public void miao(){
System.out.println("miao~~~");
}
}
//动物商店
public class AnimalStore<T extends Animal> {
//动物
private T t;
protected AnimalStore(T t) {
super();
this.t = t;
}
//让动物商店的动物吃饭
public void eat(){
//若把T extends Animal 改成 T将不能调用t.eat(),因为类型参数会被擦除到第一个边界
t.eat();
}
public static void main(String[] args) {
AnimalStore<Cat> store =new AnimalStore<>(new Cat());
store.eat();
}
}
泛型类型只有在静态类型检查期间才出现,在之后,程序中的所有泛型类型都将被擦除,替换为非泛型上界,如Pair<T>
被擦除为Pair,即T变为Object,而AnimalStore<T extends Animal>
中的T被擦除到Animal,还可以使用期Animal中申明的方法。
上面介绍了Java泛型一些奇怪的擦除,下面谈谈它将产生一些问题,以及如何解决
1、 instance of
如 arg instance of T
尝试最终将失败,解决方法就是使用Class中的动态的isInstance()
方法
2、 new 创建一个实例
new T()
无法实现,可以传递T的Class进去,使用Class 中的 newInstance()
方法,这就必须保证该类必须有无参构造器,否则另一个实现方法就是如上面讲得利用工厂方法传递一工厂进去,由工厂来创建对象。
3、 泛型数组
T[] array=new T[]
是错误的,若T[] array=(T[])new Object[Size]
照样无法转型为T[],因为数组在穿监事,类型就被确定,所以这里为Object[]。一般解决方法时避免使用泛型数组,使用ArrayList。另一种解决方法是(T[])Array.newInstance(ClassType,size);
,这里的数组类型为ClassType[]类型。
谈谈通配符
我们知道数组是可协变的即Object[] array=String[]
是可行的的(题外话,如果大家对协变和逆变不是很理解可以看再谈对协变和逆变的理解)但是Java泛型是不支持协变的,由于类型擦除之后,泛型是没有内建的协变类型,即List<Object> l=new ArrayList<String>()
是不能通过编译的,即使String是Object的子类但是不行。那怎么解决呢?
List<? extends Object> l =new ArrayList<String>()
可以读作‘具有任何从Object继承的类型的列表’。但是,这实际上并不意味着List可以持有任何类型的Object。通配符引用的是明确的类型,它意味着‘l 引用某种指定的具体类型’。在这里? 指定的是String类型。在举个例子假设Fruit的子类有Apple和Orange。那么List<? extends Fruit> fruit=new ArrayList<Apple>();
这样可以从ArrayList<Apple>()
取出Fruit类型,有点类似前面介绍的边界<T extends Fruit>
但是通配符只能有一个边界。由于fruit是给定的“任何扩展自Fruit的队象”,所以编译器无法验证“任何事物的安全性”所以extends 确认的上界使得可以从中取出Fruit类型的对象,但是不能往里放任何东西。相反下面要说的上界则恰恰相反。
下面讲讲逆变
使用超类型通配符<? extends Apple>
那么 Apple就是上界例如如下
static <T> void f1(List<T> list,T t)
{
list.add(t);
}
static <T> void f2(List<? super T> list,T t)
{
list.add(t);
}
如
f1(l,new Cat());
按照think in java 中表述第一条语句是编译不通过的,但我在Java1.8版本中却可以,估计是改进了。有待考究。。
f2(l,new Animal());