1、在你创建参数化类型的一个实例时,编译器会为你负责转型操作,并且保证类型的正确性。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。示例:
public class Holder<T>{
private T a;
public Holder(T a){
this.a = a;
}
public T get(){
return a;
}
public void set(T a){
this.a = a;
}
public static void main(String[] args){
Holder<Test> h = new Holder<Test>(new Test());
Test t = h.get();
//h.set("test a stirng");error
}
}
现在,当你创建Holder对象时,必须指明想持有什么类型的对象,将其置于尖括号内。就像main()中那样。那么,你就只能在Holder中存入该类型(或其子类)了。并且,在你从Hodler中去取出它持有的对象时,自动地就是正确的类型。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
2、Java泛型的局限性:基本类型无法作为类型参数。不过,Java SE5具备了自动打包和拆包的功能,可以很方便的在基本类型和包装类之间进行转换。
3、是否拥有泛型方法,与其所在的类是否是泛型类没有关系。
4、关于泛型方法的一个基本的指导原则:无论何时,只要你能做到,你就应该使用泛型方法。也就是说,如果使用泛型方法可以取代整个类泛型化,那么就应该只使用泛型方法,因为它可以使事情更清楚明白。
5、类型推断只对赋值操作有效,其他的时候并不起作用。示例:
List<String> l = new ArrayList<>();
而public static <k,v> Map<k,v> map(){
return new HashMap<k,v>();
}
f(Test.Map());由此此时并非赋值操作,这时编译器并不会执行类型推断,编译器认为:调用泛型方法后,其返回值被赋值给了一个Object类型的变量。解决方式是:使用显式的类型说明。f(Test.<String,String>Map());只有在编写非赋值语句的时候,我们才需要这样的额外说明。
6、在泛型代码内部,你无法获得任何有关泛型参数类型的信息。即:你可以知道诸如类型参数标识符和泛型类型边界这类的信息,你无法知道用来创建某个特定实例的实际的类型参数。原因是Java泛型是通过擦除来实现的,因为这任何具体的类型信息都会被擦出了,你唯一知道的是你在使用一个对象,因此List<String>和List<Long>在运行时事实上是相同的类型,这两种形式都被擦除成它们的原始类型:List。
7、类型边界
与Java相比(Java的设计灵感来源于C++),C++模板实现了真正意义上的泛型:
template<Class T> class Test{
T object;
public :Test(T t){
this.object = t;
}
void operate(){
object.f();
}}
class HasF{
public:void f(){
.....
}
}
当实例化这个模板的时候,C++编译器将进行检查,因此在Test<HasF>被实例化的这一刻,它会看到HasF拥有一个f()方法。如果情况并非如此,就会得到一个编译器错误,这样类型安全就得到了保障。但是,Java泛型就不同了:
public class HasF{
public void f(){
System.out.println("F()");
}
public class Test<T>{
private T object;
public Test(T t){
this.object = t;
}
public void operate(){
objecte.f();//error
}
public static void main(String[] args){
HasF hasf = new HasF();
Test<HasF> test = new Test<HasF>(hasf);
test.operate();
}
}
由于有了擦出,Java编译器无法将operate必须能够在object上调用f()这一需求映射到HasF拥有f()这一事实上。为了可以调用f()方法,我们必须协助泛型类,给定泛型类的边界,例如:T extends HasF。我们说泛型类型参数将擦出到它的第一个边界(它可能会有多个边界),编译器会把类型参数替换为它的擦出,像T extends HasF,T擦出到HasF,就好像在类的声明中用HasF替换了一样。
擦出的代价是显著的。泛型不能用于显示地引用运行时类型的操作之中,例如转型、instanceof操作和new表达式。因为所有 关于参数的类型信息都丢失了。因此也就丧失了在泛型代码中执行某些操作的能力,任何在运行时需要知道确切类型信息的操作都无法工作。
8、擦出的补偿:类型标签 -- Class对象,这意味着你需要显式的传递你的类型的Class对象,以便你可以在类型表达式中使用它。例如,对instanceof的尝试失败了,是因为其类型信息已经被擦出了,如果引入类型标签,你就可以转而使用动态的isInstance():
public class TestB<T>{
class<T> kind;
public boolean f(Object obj){
return kind.isInstance(obg);
}
}
同样,new T()操作失败,部分原因是因为擦出,而另一个原因 是因为编译器不能验证T是否有默认的构造函数。同样可以使用类型标签Class对象来解决这个问题:注意:因为newInstance会调用默认构造函数,但是并非所有的类都有默认构造函数的,比如Integer。因此Sun建议使用显示的工厂模式:
9、泛型数组
我们不能直接创建泛型数组,一般的解决方案是在任何想创建泛型数组的地方使用ArrayList,例如private List<T> array = new ArrayList<T>();这里你将获得数组的行为,以及由泛型提供的编译期的类型安全。
成功的创建泛型数组的唯一方式是创建一个被擦出类型的新数组,然后对其转型。
T[] array = (T[])new Object[size];
因为有了擦除,数组的运行时类型就只能是Object[]。如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。
正因为这样,所以我们应该在使用数组元素的时候,添加一个对T的转型。
10、通配符
先看一下数组的一种特殊的行为:可以向子类型数组赋予基类型的数组引用
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();//ok
fruit[1] = new Jonathan();//ok
fruit[2] = new Fruit();//complier allows you to add fruit,but in runtime you will get a ArrayStoreException
最后一行代码为啥会得到一个异常呢?因为fruit的实际数组类型是Apple[],你应该只能在其中放置Apple或Apple的子类。又因为fruit的声明是一个Fruit[]引用 -- 它有什么理由不允许将Fruit对象或者任何从Fruit继承而来的对象,放置到这个数组呢。因此,在编译期,这是允许的。但是,运行时的数组机制知道它处理的是Apple[],因此会在数组中放置类型时抛出异常。所以很明显,数组对象可以保留有关它们包含的对象类型的规则。就好像数组对它们持有的对象是有意识的。
对数组的这种赋值并不是那么可怕,因为在运行时可以发现你已经插入了不正确的类型。但是,泛型的主要目标之一是将这种错误检测移入到编译期。因此,当我们视图使用泛型容器代替数组的时候,会发生什么?
List<Fruit> flist = new ArrayList<Apple>();//很显然,会产生编译错误,由于编译器不能对该行代码了解足够多的信息,因此拒绝向上转型的,实际上这根本不是向上转型,因为Apple的List在类型上不等价于Fruit的List。
与数组不同,泛型没有内建的协変类型。这是因为数组在语言中是完全定义的,因此内建了编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用什么类型做些什么,以及应该采用什么样的规则。
但是,有时你想要在两个类型之间建立某种类型的向上转型关系,这正是通配符所允许的。
List<? extends Fruit> flist = new ArrayList<Apple>();//ok
//complie error
flist.add(new Apple());
flist.add(new Fruit());
flist.add(new Object());
通配符引用的是明确的类型,你无法向flist放置任何一个对象,因为一旦执行这种类型的向上转型,你就将丢失掉向其中传递任何对象的能力,甚至是传递Object都不行。
Fruit f = flist.get(0);
如上代码是安全的,因为这个List中的任何对象至少具有Fruit类型,因此编译器允许这么做。
11、超类型通配符
<? super MyClass>可以声明通配符是由某个特定类的任何基类来界定的,甚至可以使用类型参数:<?super T>(尽管你不能对泛型参数给出一个超类型边界:即不能声明<T super Myclass>)。这使得你可以安全的传递一个类型对象到泛型类型中。因此,有了超类型通配符就可以向集合中写入了:
public void weiteTo(List<? super Apple> apples){
apples.add(new Apple());
apples.add(new Jonathan());
apples.add(new Fruit());//Error
}
捕获转换:
有一种情况特别需要使用<?>而不是原生类型。如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。这种技术被称为“捕获转换”;
public class CaptureConversion{
static <T> void f1(Holder<T> holder){
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder){
f1(holder);
}
main方法中调用:
Holder raw = new Holder<Integer>(1);
f1(raw);//有warning
f2(raw);没有warining
}
f1()中的类型参数都是确切的,没有通配符或边界。在f2()中,Holder参数是一个无界通配符,因此它看起来是未知的。但是,在f2()中,f1()被调用,而f1()需要一个已知参数。这里发生的是:参数类型在调用f2()的过程中被捕获,因此可以在对f1()的调用中被使用。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要知道确切的类型。
12、Java泛型的限制之一是:不能将基本类型用作类型参数。解决之道是使用基本类型的包装器类以及Java SE5的自动包装机制。示例:
List<Integer> list = new ArrayList<Integer>();
list.add(5);
注意:自动包装机制解决了一些问题,但是并不是解决了所有的问题:自动包装机制不能应用于数组,因此这无法工作。示例:
class FArray{
public static <T> T[] fill(T[] a,Generator<T> gen){
for(int i=0;i<a.length;i++){
a[i] = gen.next();
}
return a;
}
main方法中调用:
Integer[] integer = FArray.fill(new Integer[7],new RandomGenerator.Integer();//ok
FArray.fill(new int[7],new RandomIntGenerator());//error,无法自动包装
}
13、自限定类型
自限定所做的,就是要求在继承关系中,向下面这样使用这个类:(对使用自限定的每个类的要求)
class A extends SelfBounded<A>{}
这会强制要求将正在定义的类当做参数传递给基类。
示例:
class SelfBounded<T extends SeleBounded<T>>{
T element;
SelfBounded<T> set(T arg){
element = arg;
return this;
}
T get(){return element;}
}
class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}//also ok
class D{}
class E extends SelfBounds<D>{}//complie error:D 不是自限定类型
自限定的参数的意义是什么呢?它可以保证类型参数必须与正在被定义的类相同。还有,自限定限制只能强制作用于继承关系。还可以将自限定用于泛型方法。
14、由于擦除的原因,将泛型应用于异常是非常受限的。catch语句不能捕获泛型类型的异常。因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自Throwable。但是,类型参数可能会在一个方法的throws子句中用到。这使得你可以编写随检查型异常的类型而发生变化的泛型代码:
interface Processor<T,E extends Exception>{
void process(List<T> result) throws E;
}
Processor执行process(),并且可能会抛出具有类型E的异常。