Java 泛型知识点

时间:2022-10-30 17:02:47

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建议使用显示的工厂模式:

Java 泛型知识点

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的异常。