java中的泛型详解

来源:互联网 发布:淘宝鬼脚七微信号 编辑:程序博客网 时间:2024/06/05 03:14

1.什么是泛型

泛型实用的另一个名字叫 “ 参数化类型 ” 。一般参数的类型在java的方法中或者说在类中,要么是具体的类型,要么是自定义的类型,再就是基本类型了。而此处的限制对于我们的java代码是一种束缚,不便于我们编写随意任意多种类型的代码。使用泛型,我们可以有效地避免强制类型转换。使得编译器在编译时期通过保留的参数类型,从而去执行参数类型检查,然后进行参数类型转换,在编译时期就确定了参数的类型,使得java类型安全提高了。然而常常容器类和泛型一起出场的概率很高。从两者一起使用来看,泛型也无形中给容器中的参数类型增加的约束。简而言之,泛型就是为我们在编译时指明了参数的类型(初识)。

2.初识java泛型

泛型类

拥有单个泛型的类的例程:

public class Coffee<T>{private T t;public Coffee(T t) {this.t = t;}public T getT() {return t;}public void setT(T t) {this.t = t;}public static void main(String[] args) {Coffee<String> coffee=new Coffee<String>("单身汪");String tVal =coffee.getT();System.out.println(tVal);}}
解释:和正常的类的操作是一样的,只是指明了类中构造器的参数类型按需输入。

拥有多个泛型的类的例程:

有的时候,我们想使用一个方法调用返回多个对象,那么我们可能会使用集合容器来解决。对于return只能返回单个对象而言,但是我们有时也想创建一个对象,用它来持有想要返回的多个对象,这就需要使用多个泛型了。

在java中会使用元组这个概念,就是它是将一组对象直接打包存储于其中的一个单一对象。然而这个对象允许读取其中的元素,不允许向其中存放新的对象。

例程:

class HolderPlus<T,K>{private T t;private K k;public HolderPlus(T t,K k){this.t=t;this.k=k;}@Overridepublic String toString() {return "HolderPlus [t=" + t + ", k=" + k + "]";}}
如果还想要增加泛型中的类型个数,那么就应该使用类的继承。

例程:

public class Holder<T,K,H> extends HolderPlus<T,K>{public final H h;public Holder(T t,K k,H h){super(t,k);this.h=h;}@Overridepublic String toString() {return "Holder [h=" + h + ", toString()=" + super.toString() + "]";}public static void main(String[] args) {Holder<String,Integer,Boolean> holder =new Holder<String,Integer,Boolean>("abc",123,true);System.out.println(holder);}}
结果:


泛型接口:

泛型在接口中也是经常使用的。譬如我们的生成器。就是设计模式中创建模式类目中工厂方法模式的一种应用。只是在使用生成器创建新的对象时,不需要知道创建了什么类型的对象就可以创建新的对象。而这也是泛型的本质。

如果对工厂方法模式不清楚,那么看我的博客:      工厂方法模式(java版)

工厂生产咖啡的例子(工厂方法模式+泛型):

咖啡产品:

public class Coffee{public void makeCoffee(){System.out.println("制造出了咖啡!");}}
解释:这个咖啡产品类就是所谓的具体产品,这里就不设计抽象产品接口了。
工厂接口:

public interface IFactory<T> {public T produceCoffee();}
解释:工厂接口,这个就是那个所谓的抽象工厂,里面使用了,至于工厂生产什么类,并不清楚。

工厂实现类:

public class ConcreteFactory implements IFactory<Coffee>{public static void main(String[] args) {IFactory<Coffee> factory =new ConcreteFactory();factory.produceCoffee().makeCoffee();}@Overridepublic Coffee produceCoffee() {// TODO Auto-generated method stubreturn new Coffee();}}
解释:具体的工厂类,实现了抽象的工厂接口,对接口进行了具体实现,并且完成了生产咖啡同时也提供了对外生产咖啡的接口。

泛型方法:

泛型方法其实就是独立于类而产生变化,而在我看来其实,泛型方法对操作相比泛型类来说细粒度更高了。那么如果我们可以采用泛型方法完成所需要的功能,那么我们就无需采用泛型类来完成。这样就可以使问题更加清晰。

例程:

public class Number{    public static <T> void printNum(T t){        System.out.println(t);    }    public static void main(String[] args) {        String strArr="abc";        printNum(strArr);        Integer id=45;        printNum(id);    }}
注意:

原来我们使用泛型类时,必须在创建对象时指定类的参数的类型,但是我们使用泛型方法的时候,不用指定参数的类型。就像上面的例程我们既可以打印String数组也可以打印Integer数组。这样编译器就为我们找出了具体的类型,叫做 “ 类型参数推断 ” 。

3.深入了解泛型

泛型的擦除

什么是泛型的擦除?

泛型的擦除就是当你在使用泛型的时候,任何具体的类型信息都会被擦除,以至于你只知道你只是在使用一个对象而已。

例程泛型擦除设计策略现象):

public class GenericInfo {public static void main(String[] args) {List<String> list =new ArrayList<String>();Map<String,Integer> map=new HashMap<String,Integer>();System.out.println(Arrays.toString(list.getClass().getTypeParameters()));System.out.println(Arrays.toString(map.getClass().getTypeParameters()));}}
结果:


解释:Class.getTypeParameters()方法,返回一个类型数组,里面装的都是泛型类型变量声明的泛型 类型 。所以得到的结果是在   “ 泛型的内部,你无法获取有关泛型参数类型的信息 ” 。而这正是泛型使用擦除来实现的(当你使用泛型的类型时,任何具体的泛型类型信息都会给擦除掉)。所以你只能知道它是一个对象而已了。

泛型的擦除引发编程中的问题?

例程(擦除引发的问题):


解释:从例程中可以看出,程序检查报错,意思是方法f()被T类型的调用不可以,无法将T类型解析出HasF类型。这个原因正是由于

泛型的擦除,使得编译器无法将T映射到HasF类型。而这里也说明T是没有边界的,需要引入边界的概念,让T可以映射到HasF。

解决办法:


解释:这里使用泛型继承的方式,让T有了边界,那就是必须得继承HasF这个类。而编译器实际上就会把类型参数替换为它的擦除。T擦除到了HasF,就好像HasF替换了T。

泛型的兼容性:

由于泛型擦除的出现,泛型类型会被当作第二类类型。为啥这么说呢。除非泛型类型只有在静态类型检查才出现,否则程序中的泛型都会被擦除,然后替换为它们的非泛型的上界。(一般普通的类型变量在未指定边界时都会擦除为Object)。

泛型的擦除存在的问题?

(1)不能创建泛型数组?

在创建数组的时候,必须要知道数组中元素的类型,然后会记住这个类型,如果往数组里插入元素,那么就会做类型检查。但是不巧的是java泛型具有擦除的特性,在运行时,参数类型会被擦除掉。导致无法确定数组的类型了,所以不能创建泛型数组了。(这个地方就不写错误例程了)。

解决创建泛型数组的策略:

Array.newInstance
例程:

public class GenericArray<T>{private Class<T> kind;public GenericArray(Class<T> kind) {super();this.kind = kind;}T[] create(int size){return (T[]) Array.newInstance(kind, size);}public static void main(String[] args) {GenericArray<String> n =new GenericArray<String>(String.class);String[] strArr=n.create(9);System.out.println(Arrays.toString(strArr));}}

泛型的擦除问题的解决办法

(1)显式地传递类型

public class ClassTypeCapture<T>{Class<T> kind;public ClassTypeCapture(Class<T> kind) {super();this.kind = kind;}public boolean f(Object arg){return kind.isInstance(arg);}public static void main(String[] args) {ClassTypeCapture<Building> ctt1 =new ClassTypeCapture<Building>(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCapture<House> ctt2 =new ClassTypeCapture<House>(House.class);System.out.println(ctt2.f(new Building()));System.out.println(ctt2.f(new House()));}}class Building{}class House extends Building{}
结果:

true

true

false

true

编译器保证了类型标签与泛型参数匹配。上面kind.isInstance(arg);方法就是arg是否可以向kind进行转化。Building类是House类的父类。说明Building类型是House类型的上界,转化是没问题的。所以结果是true。反之错误。

泛型的边界

什么是泛型的边界?

泛型的边界实际上就是泛型的参数类型的设置的限制条件。如前面介绍的继承关系中的上界等等。

边界在类级别上的应用

public interface IMake {//制造产品public void makeProduct();}

public class MachineImpl implements IMake{@Overridepublic void makeProduct() {// TODO Auto-generated method stubSystem.out.println("制造产品");}}

public class Mobile<T extends MachineImpl & IMake>{private T t;public Mobile(T t) {super();this.t = t;}public T getT() {return t;}public static void main(String[] args) {Mobile<MachineImpl> mobile =new Mobile<MachineImpl>(new MachineImpl());mobile.getT().makeProduct();}}

边界在方法级别上的应用

public interface ICharge {//给产品充电public void chargeProduct();}

public interface IMake {//制造产品public void makeProduct();}

public class MachineImpl implements IMake,ICharge{@Overridepublic void chargeProduct() {// TODO Auto-generated method stubSystem.out.println("为产品充电");}@Overridepublic void makeProduct() {// TODO Auto-generated method stubSystem.out.println("制造产品");}}

public class Mobile {static <T extends IMake> void make(T t){t.makeProduct();}static <T extends ICharge & IMake> void charge(T t){t.chargeProduct();t.makeProduct();}public static void main(String[] args) {MachineImpl machine =new MachineImpl();make(machine);charge(machine);}}
解释:可以看到泛型作为函数的参数可以让ICharge和IMake接口作为边界。

通配符

通配符在协变中的应用

协变实际就是小范围的类型代替大范围的类型。

数组的协变性

例程:

public class CovariantArray{public static void main(String[] args) {Number[] nums =new Integer[10];nums[0] =Integer.valueOf(1);try{nums[1] =new Float(1.0);}catch(Exception e){e.printStackTrace();}try{nums[2] =Byte.valueOf((byte)1);}catch(Exception e){e.printStackTrace();}}}
结果:


解释:可以看到数组具有协变性。

集合框架的协变性


可以看到集合框架是不支持泛型的协变的。所以使用通配符来模拟协变的实现。

public class CovariantList {public static void main(String[] args) {List<? extends Fruit> list =new ArrayList<Apple>();}}class Fruit{}class Apple extends Fruit{}

通配符在逆变中的应用

逆变实际上就是大范围类型代替小范围类型。java泛型中也同样不支持逆变,所以使用通配符模拟逆变实现。

public class CovariantList {public static void main(String[] args) {List<? super Apple> list =new ArrayList<Fruit>();}}class Fruit{}class Apple extends Fruit{}



0 0
原创粉丝点击