java object转泛型_详解Java泛型机制 - Go语言中文社区

java object转泛型_详解Java泛型机制


355528378343746a4df9158f2e6ff641.gif我们做开发的时候一直会强调数据类型的概念,在Java中分为基本类型和引用数据类型,其中基本数据类型有八种,除了类以外,我们还可以使用接口继承实现的方式来复用代码,降低耦合度,提高开发的灵活性。

而泛型则是将接口的概念进一步延伸,而泛型的意思就是广泛的类型,无论是类、接口还是方法都可以应用于非常广泛的类型,使得代码和它们操作的数据类型不再需要绑定在一起,同一套代码可以实现真正意义上的适用于多种数据类型,实现更灵活的代码复用,并且能提高代码的可读性和安全性。说到这,可能你还会比较迷茫,接下来我们先看一个简单的泛型,如下:

publicclassPair {

T first;

T second;

publicPair(T first, T second){

this.first = first;

this.second = second;

}

public T getFirst() {

return first;

}

public T getSecond() {

return second;

}

}

可以看出来Pair类就是一个泛型类,与普通的类区别在于:

1.类名后面多了一个

2.参数first和second分别是泛型T类型

那么这个T是什么呢?T是一种泛指,表示类型参数,泛型就是类型参数化,处理的数据不是固定的,而是可以动态指定类型作为参数传入。那么定义的泛型类如何使用呢?如下:

Pair<Integer> minmax = newPair<Integer>(1,100);

Integer min = minmax.getFirst();

Integer max = minmax.getSecond();

可以看到Pair中的Integer就是之前定义的泛型T的实际类型参数,当然这里的T可以是任何类型,我们这里可以指定为Integer,也可以指定为任何类型。同样的,泛型的参数类型数量不是固定的,我们可以申明多个不同类型的动态泛型类型,两个泛型之间使用逗号分割,如下:

publicclassPair {

U first;

V second;

publicPair(U first, V second){

this.first = first;

this.second = second;

}

public U getFirst() {

return first;

}

public V getSecond() {

return second;

}

}

改进后的Pair类可以这么使用:

Pair<String,Integer> pair = newPair<String,Integer>("张三",100);

泛型的基本原理

看到上面的案例我们大概知道了一个简单的泛型如何定义,那么不禁会有一个疑惑,那就是泛型类型到底是什么呢?我们为什么一定要定义一个类型参数呢?熟悉Java多态特性的我们都知道,我们完全可以定义一个通用的父类类型,然后传递具体的子类型不也能实现这样的操作吗?同样的Java中也存在所有的类的基类--Object,如果我们直接使用Object不也可以吗?如下:

publicclassPair{

Object first;

Object second;

publicPair(Object first, Object second){

this.first = first;

this.second = second;

}

publicObject getFirst() {

return first;

}

publicObject getSecond() {

return second;

}

}

使用的时候的代码只要这么改动:

Pair minmax = newPair(1,100);

Integer min = (Integer)minmax.getFirst();//字段强制转换

Integer max = (Integer)minmax.getSecond();//字段强制转换

这样使用其实是可以的,事实上Java提供的泛型机制其实底层就是如此实现的。之所以这么设计,与Java当初设计的时候的jvm虚拟机编译机制有关系,要知道泛型设计的时候Java才到Jdk1.4版本,而我们都知道Java有编译器和Java虚拟机,编译器会帮我们把Java代码转换为.Class,虚拟机则是负责加载.Class,对于泛型类,Java编译器会把泛型部分的代码转换为普通的代码,即和上面的Object类型接管一样,将类型的T进行擦除,替换为Object,并且进行必要的类型的强制转换操作,所以在Java虚拟机执行Java字节码的过程中,其实和Object操作是一样的,并不知道泛型,也不存在泛型。那么既然泛型还是会转换为Object,进行泛型擦除,Java为什么要在1.5开始支持并设计出泛型机制呢?

泛型的好处

其实想要理解这点,我们不妨考虑一下,泛型的好处在哪?同时也去思考一下如果我们使用Object编程,缺陷会存在在哪?熟悉泛型的都知道,泛型有两个好处:

1.更好的安全性

2.更好的可读性

我们也知道Java语言在我们开发编译的阶段,ide就会进行代码检查,当我们的语法出现问题的时候,ide会在编译阶段就把错误标识出来,减少程序的潜在Bug数。但是我们不妨看下Object操作的代码:

Pair pair = newPair("张三",1);

Integer id = (Integer)pair.getFirst();

String name = (String)pair.getSecond();

可以看出来,无论id是否为Integer类型,或者name是否为String类型,我们在编译阶段,由于类型为Object,我们都会进行强制转换操作,在编译期这些操作都是语法合理的,并不会报错,但是如果这些字段中存在类型错误,也必须等到程序运行到这里才会提示ClassCastException异常,但是如果我们使用的是泛型机制,并且使用的时候标明了类型为String和Integer,那么如果我们使用的类型不一致,在编译时已经报错,必须修改后才可以成功运行,如下:

Pair<String,Integer> pair = newPair<>("张三",1);

Integer id = pair.getFirst(); //编译错误

String name = pair.getSecond(); //编译错误

所以很明显的可以看出来,如果使用了泛型后,类的后缀添加对应的泛型类型,我们很明确的知道具体的类型是什么,提高开发的可读性,并且因为ide会做类型检查,所以安全性也会更高

泛型方法

当然泛型的作用域范围比较广,我们不仅可以定义在类/接口的申明上,我们也可以将泛型作用在方法上,与类的泛型相互隔离,实现更精细粒度的泛型操作。并且需要注意的是,一个类的泛型定义和方法的泛型定义并无直接关系,两者是相互独立的,即类的泛型可以定义为T,而方法也可以定义为泛型T,但是这两个T并不属于同一个。首先我们先看一个泛型方法的案例:

publicstaticint indexOf(T[] arr, T elm){

for(int i=0; i

if(arr[i].equals(elm)){

return i;

}

}

return-1;

}

可以看出来,indexOf方法就是一个泛型方法,使用的时候,我们可以如下:

indexOf(newInteger[]{1,3,5}, 10)

同样的泛型方法拥有和泛型类一样的所有特性,也可以定义多个泛型参数在方法上,比如:

publicstatic Pair createPair(U first, V second){

Pair pair = newPair<>(first, second);

return pair;

}

但是与泛型类不同的是,使用的时候只需要传入确定类型的值即可,并不需要申明泛型类型后缀,如下:

createPair("张三",1);

泛型的上限界定

在前面的学习中我们都知道泛型擦除会转化为Object类型,但是我们能不能给Object的范围缩小呢?即限制泛型的父类类型上限是多少,在Java中其实是支持的,而泛型中支持这个上限界定是使用了extends关键字来表示的,当然这里的父类类型可以是接口、类或者类型参数,我们分别介绍下:

接口作为父类类型

比如我们开发中遇到一个场景,我们必须实现Comparable接口来实现动态的类型的比较,这个时候代码如下:

publicstaticextendsComparable> T max(T[] arr){

T max = arr[0];

for(int i=1; i

if(arr[i].compareTo(max)>0){

max = arr[i];

}

}

return max;

}

max是泛型类型T的数组的对应下标的值,不过这么编写代码的话,会被编译器警告,因为Comparable接口本身也是个泛型接口,所以我们写的时候建议也去指定Comparable接口的泛型上界,修改如下:

publicstaticextendsComparable> T max(T[] arr){

...................

}

此种方式可以实现泛型类型的递归类型限制传递

上界为具体类

还记得我们上面的实例Pair类使用的泛型类型,我们可以实现一个子类:

publicclassNumberPairextendsNumber, V extendsNumber> extendsPair {

publicNumberPair(U first, V second) {

super(first, second);

}

}

当我们限制了对应的类型范围后,我们就可以把first和second变量作为Number类型进行处理了,比如我们内部有一个求和的方法:

publicdouble plus(){

return getFirst().doubleValue() + getSecond().doubleValue();

}

所以当我们定义完后,我们的使用即为如下这样:

NumberPair<Integer, Double> pair = newNumberPair<>(10, 12.34);

double sum = pair.plus();

可以看出来,限制了泛型类型范围后,编译器检查的会更严格,如果类型不对直接会报错,并且泛型擦除的时候转换的类型则为指定的范围上界的类型

泛型的通配符

上面我们提到了一些例子,就是使用了参数类型作为范围上界,但是这种写法比较繁琐,有木有更简化的写法呢?当然有,泛型支持通配符形式,可以简化范围上界的泛型写法,一个简单的通配符泛型如下:

publicvoid addAll(DynamicArray extends E> c) {

for(int i=0; i

add(c.get(i));

}

}

可以看到当前的写法中c的类型是DynamicArray类型,?表示通配符,表示有限定通配符,具体需要匹配泛型E或者E的子类型即可,至于具体是什么类型,完全可以得知,当我们使用的时候,代码如下:

DynamicArray<Number> numbers = newDynamicArray<>();

DynamicArray<Integer> ints = newDynamicArray<>();

ints.add(100);

ints.add(34);

numbers.addAll(ints);

这里E是Number类型的时候,?可以匹配为DynamicArray,那么通配符和范围上界指定的效果一样,这两者有什么区别呢?

1.写法仅限于用于定义类型参数,申明了一个类型参数T(使用的时候必须指定泛型类型)

2.用于实例化类型参数,可以用于实例化泛型变量中的类型参数,只是当前类型可以是未知的,只需要知道范围上限,即属于泛型E的子类即可(使用的时候可以不指定泛型类型,或者直接传递子类类型即可)

那么我们什么时候使用通配符,什么时候需要定义类型参数范围呢?首先我们先来认知下通配符分类以及各类通配符的用法

无限定通配符

在泛型中,除了上述的有限定通配符以外,还有无限定通配符超类型通配符,我们首先来了解无限定通配符,使用无限定通配符实现一个简单的DynamicArray中查找元素,代码如下:

publicstaticint indexOf(DynamicArray> arr, Object elm){

for(int i=0; i

if(arr.get(i).equals(elm)){

return i;

}

}

return-1;

}

可以看到上述的泛型即使用了无限定通配符,当然此通配符也可以使用泛型类型T来代替,效果是相同的,不过无限定通配符使用起来更简洁,当然无论是上述的哪一种通配符,都有一个限制--只能读,不可以写入,我们先看例子:

DynamicArray<Integer> ints = newDynamicArray<>();

DynamicArray extendsNumber> numbers = ints;

Integer a = 200;

numbers.add(a); //代码错误,不允许添加

numbers.add((Number)a);//代码错误,不允许添加

numbers.add((Object)a); //代码错误,不允许添加

可以看到这三种方法,都尝试在泛型类型未确定的时候尝试插入操作,无一例外都失败了,这里就是Java对泛型的类型检查的优化,无论是?通配符,还是方式的泛型,这里的泛型类型都是不确定的,所以允许插入后就会有类型安全的问题。当然除了这一点以外,如果返回值依赖某个引用类型参数,也不允许使用通配符,如下:

publicstaticextendsComparable> T max(DynamicArray arr){

T max = arr.get(0);

for(int i=1; i

if(arr.get(i).compareTo(max)>0){

max = arr.get(i);

}

}

return max;

}

这里的代码如果使用通配符,就会出现意想不到的问题,所以也无法使用通配符操作,从上面我么可以总结出,无限定通配符和泛型类型参数的关系,如下:

1.无限定通配符能修饰的泛型,都可以使用泛型类型参数的方式替换

2.通配符可以减少泛型类型参数,代码更简洁,可读性更好

3.如果类型参数之间有依赖关系,或者返回值依赖于传递的类型参数,这里只能使用泛型类型参数

超类型通配符

上面我们知道可以在泛型中存在继承关系,所以我们可以指定泛型的父类上界,也可以使用有限定通配符,一定程度上可以实现我们开发的灵活简化,但是也存在这样的场景,比如我们知道某一个具体的子类实现,但是我们希望无论是哪一级的父类型都可以作为通用的操作,这个时候我们不确定类型的就是超类了,还能使用泛型吗?答案是能,泛型在Java1.6中加入了超类型通配符操作,形式为,首先我们先看没有超类型通配符的一个简单场景,如下:

//定义了一个copy方法

publicvoid copyTo(DynamicArray dest){

for(int i=0; i

dest.add(get(i));

}

}

我们想要做的操作很简单,只要将当前容器的元素传递到对应的容器中,这个时候我们可能希望这么使用:

DynamicArray<Integer> ints = newDynamicArray<Integer>();

ints.add(100);

ints.add(34);

//构建一个Number父类型的动态数组

DynamicArray<Number> numbers = newDynamicArray<Number>();

ints.copyTo(numbers);

按照java特性来说,Integer是Number的子类型,将Integer的实例数组对象copy进入父类型的数组中是完全合理的,但是由于这里使用了泛型,指定的类型参数不一致,导致java编译器会提示编译错误,但是我们使用了超类型通配符以后,问题迎刃而解,如下:

publicvoid copyTo(DynamicArray super E> dest){

for(int i=0; i

dest.add(get(i));

}

}

通配符比较

现在我们对三种通配符都有了一定的了解了,将三种通配符进行比较和总结如下:

1.三种通配符存在的意义都是为了使得java动态代码更加灵活,可以接受更广泛的类型

2.通配符方式更适合灵活写入的场景,使得java编译器不会捕捉子类型写入父类型的容器的错误,并且不能被泛型参数类型的方式替换

3.和方式更适合用于灵活的读取,使得代码可以读取E和任何子类的对象,这里的通配符操作和泛型类型参数操作完全等同,可以互相替换

泛型使用的细节与注意点

学习到这里,可能了解了泛型的原理,其实就是通过java的类型擦除的特性实现的,实际编译的时候还是会转换为Object类型或者限定的父类型,但是使用泛型并不是任何场景都适用的,下面我们来罗列一下泛型的细节与使用的局限性:

使用泛型类、接口和泛型方法时

需要注意:

1.基本类型不能作为实例化类型参数,应使用其包装类型

2.运行时类型信息不适用于泛型,例如"string".class这种不被允许作为泛型实例传递

3.类型擦除也可能会出现一些冲突,例如父类型实现某个接口,父类型作为泛型的时候,子类如果想实现父类接口的某个方法,重新实现该接口就会出现错误

定义泛型类、接口和泛型方法时

也需要注意:

1.不能通过类型参数创建对象,例如:T t = new T(),如果真的需要创建对象,建议使用Class类型作为类型参数而非泛型

2.泛型类的类型不能被用作静态变量或者静态方法

8a036ff70ee1080596a63954af010cff.png

万水千山总是情,点个在看行不行62aaf9c1559452a209147a96594782e6.png

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_39838302/article/details/111255873
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-04-11 14:27:16
  • 阅读 ( 511 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢