泛型类

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。注意,这也是他的特点,在编译期就能够进行类型检测,而不是运行时进行检测。

我们通常写一个方法时都会有形参,当调用方法时传入具体的实参。但这种情况参数的类型是固定的。也就是说这个方法只能传固定类型的实参。但是在开发中很多时候或者设计框架的时候我们都不知真实的实参类型,这时候泛型就排上用场了。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个泛型参数。这句话什么意思呢?别急下面我将通过撸代码的方式告诉你这句话什么意思。

泛型又总体分为三种类型:泛型类、泛型接口、泛型方法。这里我们将详细讲解什么是泛型类,如何定义一个泛型类,泛型类有什么作用,泛型类的工作原理。带着问题我们一步一步来讲解

一、什么是泛型类

在开发中我们经常需要定义一些容器类,来缓存某些数据,或者叫Bean集合,如类似Map这样的集合来存储应用程序的数据结构。但是一个应用中这样的数据多到令人发指,且每个数据类型都不一样,如果每次存储一个数据时都要创建一个对应类型的bean结构,那得多恶心?就如同Map一样,如果每次存储一个Key、Value键值对时,都要根据Key和Value的类型重新创建一个Class对象,那开发人员怕是要疯了。而泛型类就完美的替我们解决了这些问题,他能够实现在不确定数据类型的情况下通过泛型来完成对不同类型数据的存储。说了这么多,到底什么是泛型类呢?下面通过三个例子来具体解释什么是泛型类以及他的优点

/**
* 这是一个普通的键值对数据结构类
*/
public class NormalClass1 {
private String key;//键的类型为String
private String value;//值的类型也是String
/**
* 这里是他的构造方法
* @param key 构造参数,类型与私有变量的类型一致。
* @param value
*
* 当有一个键值都是String类型的数据需要存储时,我们就可以new一个这样的对象类存储。
* 看起来是不是很方便?我们只需要缓存这个对象就完成了数据的缓存。
* 但是,问题来了。如果这个时候有一个key的类型是Int型,value 是String类型的数据需要缓存呢?更变态点的key的类型不确定,value的类型也不确定。
* 这个时候我们要怎么存储这样的数据?难道还要一个个对应的去创建Class? 怕是脑子有坑哦
* 这个时候泛型类就很好的帮我们解决了这样的问题。
*/
public NormalClass1(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}

public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}

二、泛型类的定义

定义一个泛型类与定义一个普通类最大的区别就是,在类名前通过 <泛型或泛型列表> 来定义这个类,表明与某一种或多种数据结构进行绑定。什么意思呢?如下面代码中的这个泛型类:他的类名前面指定的泛型为K、V。那么这个类就与这两个泛型参数进行唯一绑定,其内部变量可以使用K、V来进行表示,如果类里面私有变量想用泛型M来表示则是不被允许的。而K、V的具体类型是什么泛型类并不关心,只在实例化一个具体的泛型对象的时候指定具体类型就可以了。

/**

* 这是一个泛型类

* @param <K>

* @param <V>

*/

public class GenericClass2<K,V> {

private K key;//key 的类型不确定,暂定泛型K,

private V value;//value的泛型不确定,暂定V. 可能有人会说,啊,你怎么就知道怎么定义这个数据的泛型是K 还是V 呢?

// 这里我只能说,这个泛型的类型怎么定义,随你高兴怎么定义就怎么定义。但是一般在开发中约定俗成的都用T 、E、K、V、M



/**

* 这是这个泛型类的构造函数

* @param key //对应私有变量的key值类型K

* @param value//对应私有变量的value值类型V

*

* 这时候,只要有一个键值对的数据需要存储,我们都只需要new一个泛型类实例就解决了,再也不用根据键值对的类型创建不同的类了。

* 那么到底怎么用呢?

*/

public GenericClass2(K key,V value){

this.key = key;

this.value = value;

}

public K getKey() {

return key;

}



public void setKey(K key) {

this.key = key;

}



public V getValue() {

return value;

}



public void setValue(V value) {

this.value = value;

}

}

三、泛型类的作用和使用

有需要才有实现,那么我们为什么需要泛型?一、多种数据类型执行相同的代码时,我们需要泛型。二、泛型自带编译期类型检测,不需要我们再做类型判断和强制转换。

这里我们通过一个泛型类,存储了不同类型的键值对数据,有效的避免了每次都创建不同的类来存储对应类型的数据,完成了存储与数据类型的间接解绑(并没有真的解绑,只是通过泛型类型来指向对应的数据类型,泛型类型与数据类型直接绑定,这种方式就叫作类型参数化:数据类型通过泛型类型参数来指定,使用时通过外部传参来完成类型绑定)。

/*

* 使用时必须通过<具体类型,具体类型> 来指定容器的类型。

*/

GenericClass2<String,String> stringGenericClass2 = new GenericClass2<>("key","value");

GenericClass2<Integer,String> integerStringGenericClass2 = new GenericClass2<>(1,"value");

GenericClass2<Integer,Integer> integerGenericClass2 = new GenericClass2<>(1,1);

//存储完数据后,再依次去取其中value值

int value = integerGenericClass2.getValue();

String values = integerStringGenericClass2.getValue();

String value3 = stringGenericClass2.getValue();

/**

* 这里取value和setValue时,直接编译异常了。直接在编译期就进行了类型检测,

* 发现存储的value类型是String和要赋值的变量类型int不一致,直接编译不通过。是不是更方便?

*/

int value4 = stringGenericClass2.getValue();

stringGenericClass2.setValue(0);

四、泛型类的扩展使用

(1)、有界泛型类

什么叫有界泛型类呢?我们都知道,Java是可以继承的。那么泛型类就可以利用这个继承特性来为他的类型参数指定上下界。到底什么意思呢?我们同样通过撸代码来讲解

先来说说什么是泛型类上界,如何指定泛型类的上界。Java中通过extends和implements来分别指定类与类之间的继承关系,类与接口之间的实现关系等。泛型类有着同样的特性,只不过泛型类的这种类与类,类与接口的关系都是通过extends来指定。泛型类的上界就是通过extends来指定泛型参数的继承(或实现)关系。

/**

* 创建时间:2019/4/2

* 创建人:czf

* 功能描述:这是一个被指定了上界的泛型类,这里的extends限定了泛型类型变量的类型

* 也就是说这个泛型类的泛型参数只能够是BaseClass或者是BaseClass的子类,并且实现了BaseInterface接口。

* 注意这里extends 后面第一个必须是类,之后可以跟多个& 接口。这种写法也符合Java的特性:只能有一个extends 继承,多个implements实现

**/

public class GenericClassTop<T extends BaseClass & Baseinterface> {

private T data;



public GenericClassTop(T data) {

this.data = data;

}



public T getData() {

return data;

}



public void setData(T data) {

this.data = data;

}

}

/**

* 这里面的TopBean必须是BaseClass的子类,并且实现了BaseInterface接口,否则进行泛型类指定类型时直接编译错误。

*/

TopBean bean = new TopBean();

GenericClassTop<TopBean> beanGenericClassTop = new GenericClassTop<>(bean);

那么泛型类有没有下界呢?泛型的下界又如何定义呢?这个问题我们将在下一篇的泛型方法中进行讲解。

五、泛型的优点

泛型类有什么优点呢?其实前面我们已经提过好多次了,他最大的优点就是在编译期进行类型检测,是类型安全的,且完成了数据类型与存储间接解绑的作用。但是大家是不是感觉又很难理解呢?啥叫类型安全?下面我们通过一个对比类来讲解什么叫类型安全和泛型的优点。

/**

* 这也是一个能够存储不同类型数据的类,

* java中的所有对象都是Object子类,而利用Java的多态、继承特性就可以完成这样的存储所有子类型的数据。

* 但是这样和泛型有什么区别呢?

* 最大的区别就是这样存储是类型不安全的。什么叫类型不安全呢?

* 就是在编译时,并不会像泛型一样进行数据类型检测。而在取数据时需要我们自己去手动进行数据类型判断和

*强制转换。是不是一下子感觉好难受?

* 而泛型类就不需要担心这些,泛型类在编译期就会进行类型检测,

* 同时由于他在编译期已经做过类型检测了,所以在取数据时也就不再需要我们手动进行类型强制转换了

*/

public class NormalClass2 {

private Object data;

public NormalClass2(Object data){

this.data = data;

}



public Object getData() {

return data;

}



public void setData(Object data) {

this.data = data;

}

}

六、泛型类的局限和注意项

(1)、泛型是无法创建泛型类对象数组的。

上面这句话什么意思呢?我们都知道,Java中是有数组这种数据结构的,通常我们这样去创建一个对象数组

TopBean[] beans = new TopBean[10]; //这里我们定义并创建了一个大小为10的topBean对象数组,它的成员变量是TopBean对象

GenericClassTop<TopBean>[] classTops;//这里我们定义一个泛型类数组,它的成员变量是GenericClassTop<TopBean>结构。

classTops = new GenericClassTop<TopBean> [10];//但是如果你想去创建并初始化一个这样的数组是不被允许的,直接编译错误,这是因为泛型是不允许进行创建数组的,但是定义泛型数组完全没问题。

(2)、泛型类是不能够通过extends Exception/Throwable来捕获异常的。

java中我们通常自定义一个异常类是通过继承Exption或者Throwable。当某个方法执行时抛出该异常,外部再通过try/catch语句进行异常捕获。啥意思呢?我们们依旧是通过代码来看问题

//外部方法中通过try/catch语句进行异常捕获

public void catchException(){

try {

doWork();

} catch (ExClass1 exClass1) {

exClass1.printStackTrace();

}

}
/*这是一个会抛出自定义异常的普通方法,
*通常捕获一个自定异常的方式会如上面所示,在外部通过try/catch语句直接捕获异常类。那么泛型类是否也能这样做呢?
*/
public void doWork() throws ExClass1 {

throw new ExClass1();

}

//这是自定义的一个普通的异常类

private class ExClass1 extends Exception{



}


/*
*泛型类无法继承Exception,所以这里出现了编译错误,
*由于泛型类无法继承Exception所以也就不能够直接捕获泛型类异常,但是在泛型方法中很多时候会出现泛型参数是泛型类的情况,而且这个泛型类所绑定的泛型类型是个异常类对象。这个时候就需要捕获这个异常。
*/
private class ExpClass2<T> extends Exception{



}

private class ExpClass3{

/**
*在这个泛型方法中想捕获这个异常的泛型参数对象是不可行的
*/
public <T extends Throwable> void doWork(T e){

try {



}catch (T e){//无法实现对泛型类对象的捕获,这里同样会出现编译错误。



}

}

}

/**
*那么泛型类中的异常如何捕获呢?我们可以通过下面的方式来捕获泛型异常
*
*我们都知道只要是异常就必然是Throwable 的子类。但是又由于泛型参数类型是不确定的所以我们无法直接捕获泛型对象。那么我们可以通过抛出一个异常泛型类的父类Throwable ,只要这个泛型参数是个异常类对象我们就能够在外部捕获它。
*/

public<T extends Throwable> void catchThrowable(T e) throws T{

try {



}catch (Throwable throwable){//这里直接通过捕获Throwable,然后将泛型异常通过Throw抛出去,再由外部捕获这个泛型指向的具体异常类就可以了

throw e;

}

}

(3)、泛型类的泛型 不能够实例化和在静态域中引用

泛型是不能够实例化的,只能够通过外部先进行数据类型绑定再传具体值的方式初始化

public void newData(){

this.data = new T();//这里直接编译错误,因为泛型不能够实例化

}

泛型类的泛型不能够在静态域中引用

       那么为什么不能够在静态域中引用泛型类的泛型呢?

因为静态域属于类,而不属于对象,这又是什么意思呢?

泛型类的泛型类型在对象创建时才能够知道,虚拟机在创建一个对象时,会先执行static代码,然后再执行

类的构造方法。而一个对象的创建是在执行完构造方法后才完成的,所以static代码块在对象创建之前执行(也就是属于类)

此时静态域中根本就不知道这个泛型类的泛型类型具体是什么。

private static T code;//这里直接编译错误,因为泛型不能够再静态域中引用



/**

* 同理静态方法也是属于类,不能够直接引用泛型

* @return

*/

public static T getCode(){//这里同样编译错误,静态域中不能够直接引用泛型类的泛型类型

return T;

}

       在静态方法中不能够引用泛型类的泛型,那么我们要想在静态方法中引用泛型该怎么做呢?

下面的两个方法就不受限制,我把这样的方法叫做:静态泛型方法

如果静态方法本身也是泛型方法的话,引用泛型就没有问题,可能有的同学就和我一样有点懵了,这又是为什么呢?

 因为泛型方法是在执行时指定具体类型,能使方法独立于类而产生变化。他所引用的泛型是方法自身的泛型,而不是泛型类的泛型。

也就是说如果你想在静态方法中引用泛型,唯一的方法就是将静态方法同时变成泛型方法。

/* @param coe

* @param <E>

* @return

*/

public static <E> String getCode(E coe){



return "";

}

public static <E> E getCode1(E coe){



return coe;

}



/**

* 这就是一个再普通不过的泛型方法。泛型方法将再下一章详细讲解

* @param coe

* @param <E>

* @return

*/

public <E> E getCode2(E coe){

return coe;

}

(4)、泛型类型不能够指定基础数据类型

如下面这段代码就会直接编译错误,因为泛型类型不能够指定为基础数据类型

GenericClass1<double> doubleGenericClass1 = new GenericClass1<>(1.0);

但是可以是基础数据类型的包装类型

GenericClass1<Double> doubleGenericClass2 = new GenericClass1<>(1.0);

有的同学可能就要问了,为什么包装类型就可以呢?这个问题可以这样去理解:

基础数据类型不是一个类对象,而包装类型是个类对象,泛型类型必须于类对象进行绑定

(5)、泛型类的继承关系

1、泛型类的泛型参数的继承关系不会延续到泛型类中来

学习Java开发的都知道,Java的多态特性是依赖于抽象和继承来实现的,也就是说一个父类可以有很多子类。而每个子类实现父类方法的方式都可以各不相同,同时在所有使用父类的地方替换成子类是完全没有问题的,但是反过来就不可行。前面我们也说了,泛型类也可以通过extends来指定上界。

那个这里有一个问题,这里的TopBean是BaseClass的子类。那么这种父子关系是否能够延续到泛型类中呢?依旧是撸代码来看问题

TopBean topBean = new TopBean();

BaseClass baseClass = new BaseClass();

BaseClass base = new TopBean();//在Java普通类对象中这样赋值是没有问题的,因为TopBean是BaseClass的子类,子类可以赋值给父类。那么这种关系能否体现在泛型类中呢?

GenericClass1<TopBean> topBeanGenericClass1 = new GenericClass1<>(topBean);

GenericClass1<BaseClass> baseClassGenericClass1 = new GenericClass1<>(baseClass);

上面的代码编译都很完美,到了下面这条直接编译错误了,为什么呢?那是因为GenericClass1<TopBean>和GenericClass1<BaseClass> 是没有任何继承关系的,也就是说TopBean与BaseClass的这种继承关系并不会通过泛型类型指定后延续到泛型类上面来。

GenericClass1<BaseClass> topBeanGenericClass2 = new GenericClass1<>(topBean);

可能有些朋友又糊涂了,那么下面这段代码为什么又不会报错了呢?

baseClassGenericClass1.setData(topBean);

不是说继承关系不会延续到泛型类里面来吗?有这个问题的朋友可以这样来理解:

topBean是baseClass的子类,这里的setData方法是一个普通方法,它的参数可以替换成泛型类定义时所指定的具体参数类型,也就是BaseClass,也就是说这里的参数类型已经和普通方法一样参数类型是固定的了。 那么TopBean又是这个固定类型BaseClass的子类,所以当然能够直接进行替换啦!

这里我们说了TopBean与BaseClass的这种继承关系不会延续到泛型类中来。但是有没有办法让这种关系延续到泛型类上来呢?答案是肯定的,泛型的通配符就这样引用而生了。泛型的通配符我们将在下一篇泛型方法中讲解。

2、泛型中是不允许使用instanceof关键字的

如下Java中我们可以通过instanceof来判断对象是某种类型,或者是否有继承关系

if(topBean instanceof TopBean){

}



if(topBean instanceof BaseClass){

}

但是在泛型中是不允许使用instanceof关键字的,如下面这三种都直接编译错误,这种写法是不被允许的

if(topBeanGenericClass1 instanceof GenericClass1<TopBean>){

}

if(topBeanGenericClass1 instanceof GenericClass1<T>){



}

if(topBeanGenericClass1 instanceof GenericClass1<BaseClass>){



}

(6)、泛型类的原生类型

可能大家又和我一样懵了,啥时候又多出来一个泛型类的原生类型?这个原生类型又是个啥子玩意。

/**

*这里获取到的三个类名是相等的都是"com.example.singlecode.generic.generic.gclass.GenericClass1"。

*/

String genericName1 = doubleGenericClass1.getClass().getName();

String genericName2 = doubleGenericClass2.getClass().getName();

String genericName3 = topBeanGenericClass2.getClass().getName();

com.example.singlecode.generic.generic.gclass.GenericClass1就是这个泛型类的原生类型

泛型原生类型与泛型无任何关系,所以不管泛型类绑定的数据类型是什么,getClass()返回的原生类型都不会发生变化。

发布了29 篇原创文章 · 获赞 3 · 访问量 910

猜你喜欢

转载自blog.csdn.net/LVEfrist/article/details/90112954
今日推荐