Java中泛型总结

来源:互联网 发布:衡量软件可靠性 编辑:程序博客网 时间:2024/04/29 08:06
前天晚上被人问了一个问题:“<?extends SomeClass>与<Textends SomeClass>的区别是什么?” ,思考了下,发现自己并不能解释清楚,于是有了这篇文章。(如果觉得文章太长可以直接看最下面的第七点)

1.为什么要有泛型

泛型的引入是因为想要将一个类或方法用在不同的对象身上,在java1.5 之前并没有泛型,那是的解决办法是写一个拥有Object对象的类或传入Object对象的方法,再进行强制转换来达到代码复用的目的。

package generics;public class Reason4G {private Object a;//普通类中的Object对象public Reason4G(Object a){this.a = a;}public void set(Object a){this.a = a;}public Object get(){return a;}public static void main(String[] args) {Reason4G r4g = new Reason4G("string");r4g.set("string1");String s = (String) r4g.get(); //一个丑陋的强制类型转换警告,因为编译器不知道强制转换是否安全,在运行阶段可能会发生异常java.lang.ClassCastException}}

于是为了更安全的考虑,最好是将错误的产生原因提前至编译器,由此产生了泛型

2.泛型类

还是刚才的例子,将上面的代码改用泛型来表示:

package generics;public class GClass<T> {private T a;public GClass(T a){this.a  = a;}public void set(T a){this.a = a;}public T get(){return a;}//指定泛型类型与协变的关系public static void main(String[] args) {GClass<Fruit> fruitGClass = new GClass<>(new Fruit());//存入并读取基类fruitGClass.set(new Fruit());Fruit fruit = fruitGClass.get();//存入并读取子类对象fruitGClass.set(new Apple());fruit = fruitGClass.get();//返回的结果是一个Fruit对象,向下转型是不安全的//Apple apple = fruitGClass.get();Apple apple = (Apple)fruitGClass.get();}}class Fruit{}class Apple extends Fruit{}

在改写成泛型类了以后,对比以前的非泛型类来说,对于get方法来说,不用自己再添加不安全的强制转换,返回参数被认为是创建类时给的泛型参数<Fruit>,经过反编译会发现编译器会在泛型类的get()方法处自动添加一个强制类型转换而不用我们手动添加。

3.泛型方法

泛型方法和定义该方法的类是否是泛型类无关,泛型方法的定义如下:

package generics;public class GMethodTest {public static void main(String[] args) {GMethod1 gm1 = new GMethod1();gm1.f("string");//泛型方法中可以利用类型推断,从传入参数的类型推出泛型方法使用的泛型类型gm1.<String>f("string");GMethod2.f("String");GMethod2.<String>f("String");}}//非泛型类GMethod,泛型方法定义时需要在返回类型前加上类型的参数列表class GMethod1{public <T> void f(T x){System.out.println(x.getClass().getSimpleName());}}class GMethod2<T>{//静态方法要使用泛型参数必须成为泛型方法(无法获得泛型类中的泛型参数)//public static  void f(T x){public static <U> void f(U x){System.out.println(x.getClass().getSimpleName());}}

在定义泛型方法时将参数列表放在返回值前面。

4.泛型类型限定和擦除

4.1 泛型类型限定

考虑一个下面的程序:

package generics;// 一个有2个方法的接口public interface People {void eat();void run();}package generics;// Student扩展了People接口public class Student implements People {@Overridepublic void eat() {System.out.println("Student eat");}@Overridepublic void run() {System.out.println("Student run");}}package generics;public class GErase {//当没有边界时编译时会有错误:The method eat/run() is undefined for the type T//原因是因为泛型的擦除,擦除后为参入参数的类型为Objcet,编译器无法确定是否有方法eat()和run();//public static<T> void f(T obj){        //引入擦除边界,当擦除时会擦除到第一个扩展的边界,意味泛型参数T为People的子类型,当然会有方法eat()和run();public static<T extends People> void f(T obj){obj.eat();obj.run();}public static void main(String[] args) {f(new Student());}}


使用<T extends People>意味着这个方法可以被实现了People接口的类调用,如果只使用泛型变量T,编译器会不知道传入的参数obj是否拥有方法eat()和run()。所以相当于将泛型类型中的范围缩小到了某一个类型的子类中。

4.2类型擦除

泛型是提供给Javac编译器看的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带参数类型说明的集合时会去去除掉“泛型类型”信息,使程序运行不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。具体的做法是,当一个对象在进入泛型类/方法时,首先编译器会检查该类是否符合泛型的规定,不符合就在编译器报错。当泛型方法返回对象时,编译器在程序中自动的插入强制转换语句保证我们得到的就是在泛型类型中定义的类型变量。

5.通配符

5.1上限通配符<? extends T>

限定泛型只能为T类型的子类或者本身

程序中Student继承了People

package generics;public class GenericsTest<T> {private T item;public GenericsTest(T t){item = t;}public T get(){return item;}public void set(T t){item = t;}public static void main(String[] args) {//不能把子类泛型类型容器的对象向上转型为父类泛型类型容器的对象,换句话来说,对于两种确定的泛型参数类型的容器来说,它们之间没有关系//GenericsTest<People> gt = new GenericsTest<Student>(new Student());GenericsTest<? extends People> gt = new GenericsTest<Student>(new Student());//上限通配符// 上限通配符类型的容器不能向里面存放任何类型,因为它只知道要匹配一个子类型,但不知道具体应该是什么类型//gt.set(new Student());People p = gt.get(); // 可以读取一个上界的类型}}

5.2 下限通配符<? super T>

泛型类型只能为T的超类或本身

Student1类继承了People1类

package generics;public class GenericsTest1<T> {private T item;public GenericsTest1(T t){item = t;}public T get(){return item;}public void set(T t){item = t;}public static void main(String[] args) {//不能把超类泛型类型容器的对象向下转型为子类泛型类型容器的对象,换句话来说,对于两种确定的泛型参数类型的容器来说,它们之间没有关系//GenericsTest<Student1> gt = new GenericsTest<People1>(new People1());GenericsTest<? super People1> gt = new GenericsTest<People1>(new People1());//下限通配符// 下限通配符类型的容器能向里面存放任何类型gt.set(new Student1());gt.set(new People1());Object p = gt.get(); // 只可以读取一个Object类型,因为他们共有的超类只有Object}}


5.3无解通配符<?>

作用

1.告诉了编译器这是一个使用了某种泛型的类/方法

2.利用类型捕获所捕获的类型来调用其他泛型方法

package generics;public class CaptureConversion {static <T> void f1(GenericsTest<T> g){T t = g.get();System.out.println(t.getClass().getSimpleName());}//非泛型方法f2(),利用无界通配符捕获实际传入参数的类型,并调用f1()使用这个确切的类型static void f2(GenericsTest<?> g){f1(g);}public static void main(String[] args) {GenericsTest raw = new GenericsTest<Integer>(1);f2(raw);GenericsTest rawBase = new GenericsTest();rawBase.set(new Object());f2(rawBase);GenericsTest<?> wildcarded = new GenericsTest<Double>(1.0);f2(wildcarded);}}

程序输出为:Integer, Object, Double ,说明f1()方法对传入的具体类型进行了捕获,调用了f2()方法来输出具体类型。(在f1()处没法直接输出,因为是无界通配符?).

6.泛型类型的继承关系

下面程序 Student1 类型 继承了 People1类型

package generics;public class GInherent<T> {T t;void set(T t){this.t = t;}T get(){return t;}}class GInherentSonClass<T> extends GInherent<T>{public static void main(String[] args) {//可以赋值给自己GInherentSonClass<People1> g1 = new GInherentSonClass<People1>();//可以赋值给原始类型GInherentSonClass g2 = new GInherentSonClass<People1>();//对于同一个类的泛型来说,GInherentSonClass<People1> 和 GInherentSonClass<Student1> 没有任何继承关系//GInherentSonClass<People1> g = new GInherentSonClass<Student1>();//可以赋值给父类的同一泛型变量的对象GInherent<People1> g3 = new GInherentSonClass<People1>();//可以赋值给父类原始类型对象GInherent g4 = new GInherentSonClass<People1>();}}


从上面程序的我们可以把继承的规则看成:1.每种泛型类都可以向其原始类型转换(但不能横跨原始类型转换) 2.子类类型(包括原始类型)可以向父类类型转换。

7.<? extends SomeClass>与<extends SomeClass>的区别是什么?
7.1<T extends SomeClass> 叫做有限制类型参数 ,产生它的原因是为了缩小泛型类型的范围,表示在SomeClass子类中的一个确定的类型。可以看作是一个具体类型T(受到SomeClass的限制)。
7.2<? extends SomeClass> 叫做通配符,产生它的原因是为了在泛型中将子类类型定义的泛型类型赋值给父类类型定义的泛型类型。表示在SomeClass子类中的某种类型,可以看作是一族类型<?>(SomeClass的子类的集合)




原创粉丝点击