深入理解Java泛型

}

复制代码

这个代码灵活性很高,所有的类型都可以向上转型为Object类,这样我们就可以往里面存储各种类型的数据了。的确Java在泛型出现之前,也是这么做的。但是这样的有一个问题:如果集合里面数据很多,某一个数据转型出现错误,在编译期是无法发现的。但是在运行期会发生java.lang.ClassCastException。例如:

MyList myList=new MyList();

myList.add(“A”);

myList.add(1);

System.out.println(myList.get(0));

System.out.println((String)myList.get(1));

复制代码

我们在这个集合里面存储了多个类型(某些情况下容器可能会存储多种类型的数据),如果数据量较多,转型的时候难免会出现异常,而这些都是无法在编译期得知的。而泛型一方面让我们只能往集合中添加一种类型的数据,同时可以让我们在编译期就发现这些错误,避免运行时异常的发生,提升代码的健壮性。

如果你想要学习Java的话,我给你分享一些Java的学习资料,你不用浪费时间到处搜了,从Java入门到精通的资料我都给你整理好了,这些资料都是我做Java这几年整理的Java最新学习路线,Java笔试题,Java面试题,Java零基础到精通视频课程,Java开发工具,Java练手项目,Java电子书,Java学习笔记,PDF文档教程,Java程序员面经,Java求职简历模板等,这些资料对你接下来学习Java一定会带来非常大的帮助,每个Java初学者都必备,请你进我的**Java技术qq交流群127522921**自行下载,所有资料都在群文件里,进去要跟大家多交流学习哦。

Java泛型介绍


下面我们来介绍Java泛型的相关内容,下面会介绍以下几个方面:

  • Java泛型类

  • Java泛型方法

  • Java泛型接口

  • Java泛型擦除及其相关内容

  • Java泛型通配符

Java泛型类

类结构是面向对象中最基本的元素,如果我们的类需要有很好的扩展性,那么我们可以将其设置成泛型的。假设我们需要一个数据的包装类,通过传入不同类型的数据,可以存储相应类型的数据。我们看看这个简单的泛型类的设计:

class DataHolder{

T item;

public void setData(T t) {

this.item=t;

}

public T getData() {

return this.item;

}

}

复制代码

泛型类定义时只需要在类名后面加上类型参数即可,当然你也可以添加多个参数,类似于<K,V>,<T,E,K>等。这样我们就可以在类里面使用定义的类型参数。

泛型类最常用的使用场景就是“元组”的使用。我们知道方法return返回值只能返回单个对象。如果我们定义一个泛型类,定义2个甚至3个类型参数,这样我们return对象的时候,构建这样一个“元组”数据,通过泛型传入多个对象,这样我们就可以一次性方法多个数据了。

Java泛型方法

前面我们介绍的泛型是作用于整个类的,现在我们来介绍泛型方法。泛型方法既可以存在于泛型类中,也可以存在于普通的类中。如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法。下面我们通过例子来看一下泛型方法的使用:

class DataHolder{

T item;

public void setData(T t) {

this.item=t;

}

public T getData() {

return this.item;

}

/**

  • 泛型方法

  • @param e

*/

public void PrinterInfo(E e) {

System.out.println(e);

}

}

复制代码

我们来看运行结果:

1

AAAAA

8.88

复制代码

从上面的例子中,我们看到我们是在一个泛型类里面定义了一个泛型方法printInfo。通过传入不同的数据类型,我们都可以打印出来。在这个方法里面,我们定义了类型参数E。这个E和泛型类里面的T两者之间是没有关系的。哪怕我们将泛型方法设置成这样:

//注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。

public void PrinterInfo(T e) {

System.out.println(e);

}

//调用方法

DataHolder dataHolder=new DataHolder<>();

dataHolder.PrinterInfo(1);

dataHolder.PrinterInfo(“AAAAA”);

dataHolder.PrinterInfo(8.88f);

复制代码

这个泛型方法依然可以传入Double、Float等类型的数据。泛型方法里面的类型参数T和泛型类里面的类型参数是不一样的类型,从上面的调用方式,我们也可以看出,泛型方法printInfo不受我们DataHolder中泛型类型参数是String的影响。 我们来总结下泛型方法的几个基本特征:

  • public与返回值中间非常重要,可以理解为声明此方法为泛型方法。

  • 只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。

  • 表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。

  • 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

Java泛型接口

Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:

//定义一个泛型接口

public interface Generator {

public T next();

}

复制代码

此处有两点需要注意:

  • 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。例子如下:

/* 即:class DataHolder implements Generator{

  • 如果不声明泛型,如:class DataHolder implements Generator,编译器会报错:“Unknown class”

*/

class FruitGenerator implements Generator{

@Override

public T next() {

return null;

}

}

复制代码

  • 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。例子如下:

class DataHolder implements Generator{

@Override

public String next() {

return null;

}

}

复制代码

从这个例子我们看到,实现类里面的所有T的地方都需要实现为String。

Java泛型擦除及其相关内容

我们下面看一个例子:

Class<?> class1=new ArrayList().getClass();

Class<?> class2=new ArrayList().getClass();

System.out.println(class1); //class java.util.ArrayList

System.out.println(class2); //class java.util.ArrayList

System.out.println(class1.equals(class2)); //true

复制代码

我们看输出发现,class1和class2居然是同一个类型ArrayList,在运行时我们传入的类型变量String和Integer都被丢掉了。Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。我们来看一个更彻底的例子:

class Table {}

class Room {}

class House {}

class Particle<POSITION, MOMENTUM> {}

//调用代码及输出

List

tableList = new ArrayList
();

Map<Room, Table> maps = new HashMap<Room, Table>();

House house = new House();

Particle<Long, Double> particle = new Particle<Long, Double>();

System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));

System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));

System.out.println(Arrays.toString(house.getClass().getTypeParameters()));

System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));

/**

[E]

[K, V]

[Q]

[POSITION, MOMENTUM]

*/

复制代码

上面的代码里,我们想在运行时获取类的类型参数,但是我们看到返回的都是“形参”。在运行期我们是获取不到任何已经声明的类型信息的。

注意:

编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HasF类型来使用。

public interface HasF {

void f();

}

public class Manipulator {

T obj;

public T getObj() {

return obj;

}

public void setObj(T obj) {

this.obj = obj;

}

}

复制代码

extend关键字后后面的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HasF类型。

Java泛型擦除的原理

我们通过例子来看一下,先看一个非泛型的版本:

// SimpleHolder.java

public class SimpleHolder {

private Object obj;

public Object getObj() {

return obj;

}

public void setObj(Object obj) {

this.obj = obj;

}

public static void main(String[] args) {

SimpleHolder holder = new SimpleHolder();

holder.setObj(“Item”);

String s = (String) holder.getObj();

}

}

// SimpleHolder.class

public class SimpleHolder {

public SimpleHolder();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object.“”😦)V

4: return

public java.lang.Object getObj();

Code:

0: aload_0

1: getfield #2 // Field obj:Ljava/lang/Object;

4: areturn

public void setObj(java.lang.Object);

Code:

0: aload_0

1: aload_1

2: putfield #2 // Field obj:Ljava/lang/Object;

5: return

public static void main(java.lang.String[]);

Code:

0: new #3 // class SimpleHolder

3: dup

4: invokespecial #4 // Method “”😦)V

7: astore_1

8: aload_1

9: ldc #5 // String Item

11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V

14: aload_1

15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;

18: checkcast #8 // class java/lang/String

21: astore_2

22: return

}

复制代码

下面我们给出一个泛型的版本,从字节码的角度来看看:

//GenericHolder.java

public class GenericHolder {

T obj;

public T getObj() {

return obj;

}

public void setObj(T obj) {

this.obj = obj;

}

public static void main(String[] args) {

GenericHolder holder = new GenericHolder<>();

holder.setObj(“Item”);

String s = holder.getObj();

}

}

//GenericHolder.class

public class GenericHolder {

T obj;

public GenericHolder();

Code:

0: aload_0

1: invokespecial #1 // Method java/lang/Object.“”😦)V

4: return

public T getObj();

Code:

0: aload_0

1: getfield #2 // Field obj:Ljava/lang/Object;

4: areturn

public void setObj(T);

Code:

0: aload_0

1: aload_1

2: putfield #2 // Field obj:Ljava/lang/Object;

5: return

public static void main(java.lang.String[]);

Code:

0: new #3 // class GenericHolder

3: dup

4: invokespecial #4 // Method “”😦)V

7: astore_1

8: aload_1

9: ldc #5 // String Item

11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V

14: aload_1

15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;

18: checkcast #8 // class java/lang/String

21: astore_2

22: return

}

复制代码

在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了“还原”返回结果的类型,编译器在get之后添加了类型转换。所以,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。

所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。

Java泛型擦除的缺陷及补救措施

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof 和 new。因为在运行时,所有参数的类型信息都丢失了。类似下面的代码都是无法通过编译的:

public class Erased {

private final int SIZE = 100;

public static void f(Object arg) {

//编译不通过

if (arg instanceof T) {

}

//编译不通过

T var = new T();

//编译不通过

T[] array = new T[SIZE];

//编译不通过

T[] array = (T) new Object[SIZE];

}

}

复制代码

那我们有什么办法来补救呢?下面介绍几种方法来一一解决上面出现的问题。

类型判断问题

我们可以通过下面的代码来解决泛型的类型信息由于擦除无法进行类型判断的问题:

/**

  • 泛型类型判断封装类

  • @param

*/

class GenericType{

Class<?> classType;

public GenericType(Class<?> type) {

classType=type;

}

public boolean isInstance(Object object) {

return classType.isInstance(object);

}

}

复制代码

在main方法我们可以这样调用:

GenericType genericType=new GenericType<>(A.class);

System.out.println(“------------”);

System.out.println(genericType.isInstance(new A()));

System.out.println(genericType.isInstance(new B()));

复制代码

我们通过记录类型参数的Class对象,然后通过这个Class对象进行类型判断。

创建类型实例

泛型代码中不能new T()的原因有两个,一是因为擦除,不能确定类型;而是无法确定T是否包含无参构造函数。

为了避免这两个问题,我们使用显式的工厂模式:

/**

  • 使用工厂方法来创建实例

  • @param

*/

interface Factory{

T create();

}

class Creater{

T instance;

public <F extends Factory> T newInstance(F f) {

instance=f.create();

return instance;

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
8370753)]

[外链图片转存中…(img-qC3JTS7v-1713748370753)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

[外链图片转存中…(img-FMrsSn0K-1713748370753)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值