Java泛型详解(史上最全泛型知识详解)

目录

1. 引言

2. 泛型基础篇

2.1泛型介绍

2.2 泛型的细节注意点

2.3 泛型用法简单演示

2.4 泛型的好处

3. 泛型高级篇

3.1 泛型底层数据存取的实质

3.2 泛型的擦除

3.3 泛型类

3.4 泛型方法

3.5 泛型接口

3.6 泛型的继承和通配符

3.6.1 泛型不具备继承性

3.6.2 何为数据具备继承性

3.6.3 泛型的通配符


1. 引言

Java中的泛型想必大家都不陌生,我们在创建使用 ArrayList 数组时,IDEA通常会提示我们为这个数组写入泛型;

其实泛型很好理解,我先来简单说明一个场景大家就懂了。假如现在有一个数组,我们需要往里面存入了很多元素,有String类型,有自定义类型等等;假以时日,我们在需要的时候还会再将它们从容器中取出来,如果把它们都存放在一个数组中,我们存取的时候,会显得比较混乱。此外还有一个最关键的点,如果我们不使用泛型,那么我们存入到数组中的对象会被统一当作 Object 类进行处理,在取出的时候,就不能使用我们存入时的类型特有的方法,要进行强转,这样会非常麻烦。

那么我们能不能对数组进行分类呢?有些数组只存String类型的数据,有些数组只存自定义类型的数据,这样我们不管是存还是取的时候,都会比较清晰。

既然有了目标,我们再来说方法,怎么样才能对数组进行分类的?

这就要说到我们今天要讲的泛型。

2. 泛型基础篇

本篇主要讲解泛型最基本的定义与用法,刚接触Java的同学可以来简单了解一下泛型到底是怎么回事。下面我们开始进入正题。

2.1泛型介绍

泛型是在JDK5之后引入的一个新特性,可以在编译阶段约束操作的数据类型,并进行检查。

泛型的格式为 <数据类型>

用大白话来说,泛型就好比是给一个标签,通常情况下我们会在开发过程中或者个人学习或练习的过程中使用到泛型;就拿数组的泛型举例来说,我们把数组比作一个药瓶子,我们药瓶子贴上了什么标签,就放什么药,如果不管什么药都放在一个药瓶子里,那不得出大事吗?同样容器写上什么泛型,就存放什么数据;这样就不会导致我们存取数据的混乱。也就解决了我们引言中提到的问题,也就解释了什么是泛型。

2.2 泛型的细节注意点

(1)泛型的数据类型只能填写引用数据类型,基本数据类型不可以。至于原因,我在下面高级篇会提到;

(2)指定泛型的具体类型之后,可以传入该类类型或其子类类型;

(3)如果不手动添加泛型,则默认泛型为 Object 。

2.3 泛型用法简单演示

简单点来说,当我们使用了泛型之后,就好比是给我们要操作的数据贴上了一个标签,你贴上的是什么标签,就存什么样的数据,否则编译器会报错。如下所示

我们先 new 一个ArrayList数组,然后添加泛型,如果这里填 int 类型,编译器会报错,让你替换为包装类 Integer ,因为int 类型不是引用类型,而它的包装类 Integer 则是引用类型。如果有谁不懂什么是包装类的,或者有兴趣想要了解的,可以去看我的另一篇文章 “Java中的包装类有什么用?”里面我讲述了八种基本数据类型对应的包装类以及基本用法。

(1条消息) Java中包装类有什么用?_m0_70325779的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_70325779/article/details/130994541?spm=1001.2014.3001.5501所以,这里我们需要把 int 改为Integer,改过之后就不会报错了,如下所示

这个时候我们存入的“123”,“456”,“789”就会被当作Integer对象,那么我来试试存入一个String字符串会怎么样。

这里编译器告诉了我们几种方法,第一种方法是将字符串“abcdefg”变为Integer类型,但这种方法显然是不行的;另一种方法就是改变 List 的泛型为 String。

如下,写一个 main 方法,定义了多个 List 对象并标注不同的泛型,添加元素 

package cn.itcast.order.pojo;

import java.util.ArrayList;
import java.util.List;

public class FanXing {
    public static void main(String[] args) {

        // 存放字符串类型数据
        List<String> list1 = new ArrayList<>();
        list1.add("abcdefg");
        list1.add("hijklmn");
        System.out.println(list1);

        // 存放 Integer 类型数据
        List<Integer> list2 = new ArrayList<>();
        list2.add(123);
        list2.add(456);
        list2.add(789);
        System.out.println(list2);

        // 存放自定义类型数据,提前定义好的Order订单实体类
        List<Order> list3 = new ArrayList<>();
        list3.add(new Order());
        list3.add(new Order());
        System.out.println(list3);

        try {
            System.out.println(Class.forName("cn.itcast.order.pojo.FanXing"));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

然后运行main方法,打印容器中的内容

打印输出成功!

所以我们也证实了一个结论,一个容器泛型是什么,就要存入什么类型的数据,否则会报错。

2.4 泛型的好处

通过刚才的简单展示,这里可以简单总结出泛型的几点好处

(1)统一数据类型,对于后续业务层中取出数据有很强的统一规范性,方便对数据的管理;

(2)把运行时期的问题提前到了编译期,避免了强转类型转换可能出现的异常,降低了程序出错的概率;

3. 泛型高级篇

以下就是关于泛型知识的高级篇,可能会有些晦涩难懂,主要以 ArrayList 数组为例,结合ArrayList 源码讲解泛型的用法,包括泛型能用在哪些地方,几乎解说了泛型所有的用法,如果看不懂可以多看几遍,或者下先掌握基础篇,以后慢慢提升。

3.1 泛型底层数据存取的实质

刚才我说到了泛型只能用引用数据类型,是有原因的;

当我们往写入泛型的集合中添加元素的时候,泛型其实可以理解成一个看门大爷,你在添加数据之前,它会看看你要添加的数据类型是否与标注的泛型类型相匹配,不匹配则不会让你存入,匹配的话,在存入之后,容器底层还是会把你存入的所有数据类型当作 Object 类型保存起来,当你取数据的时候,它会做一个强转,再从 Object 类型强转变成泛型对应的类型。这也就是为什么泛型只能写引用数据类型,因为泛型的底层会做一个强转,在存取时会在Object类型与泛型类型之间互相强转,显然,int,float,double等基本数据类型是不能强转为Object类型的,所以泛型必须为引用数据类型,如果想存入 int 类型数据,只能写 int 的包装类 Integer。

3.2 泛型的擦除

通过刚才的讲解,我们知道了,泛型需要定义在容器的后面,并用 <> 进行标注。

其实,Java中的泛型是伪泛型。

为什么要这么说呢?其实我们在编码时期所指定的泛型,只在代码编译时期可以看到,当我们编写的类生成字节码文件之后,我们加入的泛型 <数据类型> 就会消失,不会在字节码中体现出来,这种现象在Java中有个专业的名词就叫 “泛型的擦除”。

这里我就不做演示了,感兴趣的小伙伴可以自己试一试,定义一个类,类中添加一个带有泛型的容器,简单写几个添加打印操作,然后使用编译器编译成字节码文件,查看该字节码文件时你就会发现,我们在编写代码时所写的泛型其实在字节码文件中并不存在。

3.3 泛型类

泛型的使用方法非常多,这里来简单说一下泛型类的使用;泛型类,就是把泛型定义在类上。

泛型类的使用场景:当一个类中,某个变量的数据不确定时,就可以定义带有泛型的类。

我们平常所用的ArrayList类,就是一个泛型类,我们看如下源码

ArrayList 源码上显示,在ArrayList类的后面,便是 <E>泛型,定义了这样的泛型,就可以让使用者在创建ArrayList对象时自主定义要存放的数据类型。

这里的 E 可以理解成变量,它不是用来记录数据的,而是记录数据的类型的。可以写成很多字母,T,V,K都可以,通常这些字母都是英文单词的首字母,V表示 value,K表示 key,E表示   element,T表示 type;如果你想,自己练习的时候写成ABCDEFG都可以,但建议养成好习惯,用专业名词的首字母,便于理解。

下面我简单自己写一个泛型类,

// 自定义泛型类
public class MyArrayList<T> {
    // 给出该数组的默认长度为10
    Object[] obj = new Object[10];

    // 定义一个指针,默认为0
    int size;

    // 写一个泛型类中添加元素的方法
    public boolean add(T t){
        // size默认为0,刚好指向数组的第一个位置,添加元素,将要添加的元素t赋值给到obj数组的第一个位置
        obj[size] = t;
        // size指针加一,指向下一个位置,下次元素添加到size指向的位置
        size++;
        // 添加完成并size加一之后,操作完成,返回成功true
        return true;
    }

    // 写一个泛型类中取出元素的方法,index索引可以取出指定位置的元素
    public T get(int index){
        // 取出元素后,强转为我们泛型所指定的类型
        return (T)obj[index];
    }
}

定义一个 main 方法,创建我的自定义泛型类的类对象,测试 add 方法,如下所示

 这里打印出来的是 list 的内存地址,说明我们自定义的 泛型类没有问题。

其实 ArrayList 底层源码就是这样写的,这里我只是简单的写了两个方法,有兴趣的可以把删除方法和修改方法也写出来,动手测试一下。

3.4 泛型方法

我们什么时候会用到泛型方法呢?

通常情况下,当一个方法的形参不确定的情况下,我们会使用到泛型方法。

泛型方法其实与泛型类有着紧密的联系,通过上面我写的自定义泛型类不难看出,在泛型类中,所有方法都可以使用类上定义的泛型。

但是,泛型方法却可以脱离泛型类单独存在,泛型方法上定义的泛型只有本方法上可以使用,其他方法不可用。

泛型方法的格式如下所示

 根据上面泛型方法的模板,我们就可以定义一个简单的泛型方法模板

根据模板写一个泛型方法

public class MyArrayList {
    private MyArrayList(){}

    // 定义一个静态泛型方法,可以封装到工具类中以备后期使用
    public static<E> void addAll(ArrayList<E> list, E e1, E e2, E e3){
        list.add(e1);
        list.add(e2);
        list.add(e3);
    }
    // 写一个main方法测试刚才所写的泛型方法
    public static void main(String[] args) {
        // 因为泛型方法中需要一个集合对象,所以提前先定义一个集合对象,泛型就写String
        ArrayList<String> list = new ArrayList<String>();
        // 调用静态方法,   类名.方法名调用
        MyArrayList.addAll(list,);
    }

}

看下图,当我向泛型为String集合中添加 int 类型的元素时,编译器报错,给出的解决方案是修改定义的addAll方法,或重新定义一个addAll方法。

这里要知道一点,我们调用了addAll方法,并传入了参数 list ,而list我们定义的泛型为String,所以我们后续添加的元素类型也只能是String。

因此,当我们传入的参数为String类型的数据时,报错就会消失,打印数组,如下图

运行成功!

这里可以总结出一点:泛型方法,在调用它的时候参数类型就已经确定了,该泛型方法会根据给定的参数类型执行相应的逻辑,得出结果。

泛型方法的使用场景也并不少见,在开发过程中,我们通常会把一些重复或者相似的方法写成一个泛型通用方法,我们只需要在方法上指定泛型。这样在调用过程中,传入什么样的参数,方法就会执行什么样的逻辑,可以简化开发,减少代码量,提高编程效率;但对开发者对泛型的理解以及方法的执行逻辑有一定深入的把控与理解。如果你能写出来,说明你对泛型的理解已经提高了一个层次。

3.5 泛型接口

泛型接口与泛型方法相似,当我们的接口中,参数类型不确定的时候,就可以使用泛型。

泛型接口的格式也很简单,和泛型方法相似,如下图

 在Java中,List 接口就是一个泛型接口,我们看源码就可以得知

泛型接口的格式虽然简单,但这不是我们要学习的重点。

我们的重点是:如何使用一个带有泛型的接口?

通常情况下,我们有两种方式

方式一:实现类给出具体的类型。

方式二:实现类延续泛型,在创建对象时再指定泛型类型。

相比于方式一,方式二的扩展性更强。

Java中 List 的实现类 ArrayList 就是采用的第二种方式,延续泛型,我们看源码即可得知

别的不用看,只看我画红线的部分,ArrayList 实现了list接口,但后面还是泛型<E>,延续了泛型,是方式二。

那么我再给各位演示一下方式一,如下我自己定义的一个泛型接口

// 定义一个泛型接口
public interface MyList<E> {
    
    // 定义一个方法做简单测试
    public boolean add(E e);
}

再定义一个类实现该接口,

// 定义MyArrayList类实现MyList接口,并在实现时就指定泛型类型
public class MyArrayList implements MyList<String> {

    // 定义一个长度为十的默认数组
    Object[] object = new Object[10];

    // 定义一个size作为指针
    int size;

    @Override
    public boolean add(String s) {
        /**
         * size初始化为零,刚好指向数组的第一个位置,添加第一个元素时,我们默认将元素添加到数组的第一个位置
         */
        object[size] = s;
        // size则合理可以作为指针,当添加第一个元素之后,size++,向后移动一位,下一次就会添加到第二个元素的位置,循环往复
        size++;
        return true;
    }
}

可以看到,在实现类中重写add方法,方法的参数就已经确定,就是我们在实现它时指定的String类型。

然后我们写一个main方法测试是否成功

创建对象,添加元素,打印结果,运行发现成功

但这里是一个内存地址,因为我这里只是简单的定义了一个接口,在Java中ArrayList的源码上千行,里面定义了很多方法,我这里只做简单测试验证一下方式一是如何完成的,很多东西都没有写,大家明白即可。

3.6 泛型的继承和通配符

泛型本身并不具备继承性,但是数据具备继承性。

3.6.1 泛型不具备继承性

如下图,我定义了GrandFathor类,Fathor类,Son类;Fathor类继承GrandFathor类,Son类又继承Fathor类。

我们再定义一个空方法体的 method 方法,方法需要传入一个带泛型的集合,我就写 GrandFathor;

分别创建泛型为 GrandFathor,Fathor,Son 的集合对象 list1,list2,list3;

调用method方法,传入list1,编译器不报错,

传入list2,编译器报错;

传入list3,编译器又报错;

我们可以得出结论,当然了也是事实,泛型是不具备继承性的也就是说,一个方法传入的对象泛型是什么类型,我们不能把参数泛型的子类泛型对象作为参数传递给方法,该泛型是不具备继承性的,传入编译器会报错。

3.6.2 何为数据具备继承性

刚才我们验证了也演示了泛型不具备继承性,那么接下来我们来说一下,数据具备继承性是什么意思。

还拿刚才的代码举例,

我们把刚才的代码注释,然后往 list1 对象中添加对象;

添加 GrandFathor 类对象,添加成功,这也是当然的,因为该类的泛型指定的就是 GrandFathor;

添加 Fathor 类对象,发现也添加成功;

添加 Son 类对象,发现也添加成功;

执行 main 方法,如下结果,说明没有问题

如果一个例子不能说明问题,我们再写一个,如下图:

 定义一个 list2,还是和刚才一样,运行如下图

这也从侧面说明了一个结论

当我们为一个类指定泛型并创建对象之后,对象中不仅可以加入泛型所指定的类对象,还可以加入泛型类子类的类对象,这就是数据的继承性。

注意这里说的是对象,上面不具备继承性中说的是参数,不要混为一谈。

3.6.3 泛型的通配符

说回我们刚才3.6.1泛型不具备继承性的例子,method()方法,假设我希望能将GrandFathor类,Fathor类,Son类的类对象都加入到list集合中去,该怎么做?

很显然,以我们现在的想法和所学的知识,可以给 ArrayList 数组添加一个不确定的泛型<E>,因为不确定类型,所以 method() 方法中的参数可以是GrandFathor类,Fathor类,Son类的任意类对象,就可以达到我们的目的了。

但各位想过没有,如果传入一个不确定的类型<E>,这样做有没有什么缺点?

其实这样做是有很大一个缺点的,那就是如果添加了这个不确定的泛型<E>,虽然能将GrandFathor类,Fathor类,Son类的类对象都加入到list集合中去,但其它所有类的类对象也都能加入到该 list,那这还和不使用泛型有什么区别呢?

继续我们的话题,3.6.1的method()方法,虽然我不确定传入method()方法的类型,但我能确定我要传入的是GrandFathor类,Fathor类,Son类这三个其中的一个,而且它们三个有继承关系。但是泛型又不具备继承性,我们又不能直接传入GrandFathor作为泛型,否则另外两个无法作为参数传递进去,那该怎么做呢?

这就要用到我们下面要说的通配符了。

在Java中,泛型的通配符是一个 "?","?" 也代表不确定的类型,它配合关键词 extend 或 super 可以对类型做出限定。

我们可以写出如下两种写法

?extend <E>:这个写法表示可以传递泛型E包括泛型E的所有子类类型。

?super <E>:这个写法表示可以传递泛型E包括泛型E的所有父类类型。

根据上面这两种写法,我们就可以对method()方法作出修改,如下图

我们把 method方法中的泛型改为<? extend GrandFathor>,表示可以传入GrandFathor类对象包括其子类对象,修改之后可以发现,再次调用method方法传入list1,list2,list3,编译器就不报错了。

同理,也可以把泛型改成 <? super Son> 表示Son类以及Son类的所有父类对象,这里就不做演示了,也很简单。

根据上面的例子,我们可以总结出来泛型通配符的使用场景:如果类型不确定,但是知道要传入的参数类型是某个继承体系中的一个,就可以使用泛型通配符来表示。

  • 8
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Java泛型Java 5引入的新特性,可以提高代码的可读性和安全性,降低代码的耦合度。泛型是将类型参数化,实现代码的通用性。 一、泛型的基本语法 在声明类、接口、方法时可以使用泛型泛型的声明方式为在类名、接口名、方法名后面加上尖括号<>,括号中可以声明一个或多个类型参数,多个类型参数之间用逗号隔开。例如: ```java public class GenericClass<T> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T> { T getData(); void setData(T data); } public <T> void genericMethod(T data) { System.out.println(data); } ``` 其中,`GenericClass`是一个泛型类,`GenericInterface`是一个泛型接口,`genericMethod`是一个泛型方法。在这些声明中,`<T>`就是类型参数,可以用任何字母代替。 二、泛型的使用 1. 泛型类的使用 在使用泛型类时,需要在类名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericClass<String> gc = new GenericClass<>(); gc.setData("Hello World"); String data = gc.getData(); ``` 在这个例子中,`GenericClass`被声明为一个泛型类,`<String>`指定了具体的类型参数,即`data`字段的类型为`String`,`gc`对象被创建时没有指定类型参数,因为编译器可以根据上下文自动推断出类型参数为`String`。 2. 泛型接口的使用 在使用泛型接口时,也需要在接口名后面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java GenericInterface<String> gi = new GenericInterface<String>() { private String data; @Override public String getData() { return data; } @Override public void setData(String data) { this.data = data; } }; gi.setData("Hello World"); String data = gi.getData(); ``` 在这个例子中,`GenericInterface`被声明为一个泛型接口,`<String>`指定了具体的类型参数,匿名内部类实现了该接口,并使用`String`作为类型参数。 3. 泛型方法的使用 在使用泛型方法时,需要在方法名前面加上尖括号<>,并在括号中指定具体的类型参数。例如: ```java genericMethod("Hello World"); ``` 在这个例子中,`genericMethod`被声明为一个泛型方法,`<T>`指定了类型参数,`T data`表示一个类型为`T`的参数,调用时可以传入任何类型的参数。 三、泛型的通配符 有时候,我们不知道泛型的具体类型,可以使用通配符`?`。通配符可以作为类型参数出现在方法的参数类型或返回类型中,但不能用于声明泛型类或泛型接口。例如: ```java public void printList(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } } ``` 在这个例子中,`printList`方法的参数类型为`List<?>`,表示可以接受任何类型的`List`,无论是`List<String>`还是`List<Integer>`都可以。在方法内部,使用`Object`类型来遍历`List`中的元素。 四、泛型的继承 泛型类和泛型接口可以继承或实现其他泛型类或泛型接口,可以使用子类或实现类的类型参数来替换父类或接口的类型参数。例如: ```java public class SubGenericClass<T> extends GenericClass<T> {} public class SubGenericInterface<T> implements GenericInterface<T> { private T data; @Override public T getData() { return data; } @Override public void setData(T data) { this.data = data; } } ``` 在这个例子中,`SubGenericClass`继承了`GenericClass`,并使用了相同的类型参数`T`,`SubGenericInterface`实现了`GenericInterface`,也使用了相同的类型参数`T`。 五、泛型的限定 有时候,我们需要对泛型的类型参数进行限定,使其只能是某个类或接口的子类或实现类。可以使用`extends`关键字来限定类型参数的上限,或使用`super`关键字来限定类型参数的下限。例如: ```java public class GenericClass<T extends Number> { private T data; public T getData() { return data; } public void setData(T data) { this.data = data; } } public interface GenericInterface<T extends Comparable<T>> { T getData(); void setData(T data); } ``` 在这个例子中,`GenericClass`的类型参数`T`被限定为`Number`的子类,`GenericInterface`的类型参数`T`被限定为实现了`Comparable`接口的类。 六、泛型的擦除 在Java中,泛型信息只存在于代码编译阶段,在编译后的字节码中会被擦除。在运行时,无法获取泛型的具体类型。例如: ```java public void genericMethod(List<String> list) { System.out.println(list.getClass()); } ``` 在这个例子中,`list`的类型为`List<String>`,但是在运行时,`getClass`返回的类型为`java.util.ArrayList`,因为泛型信息已经被擦除了。 七、泛型的类型推断 在Java 7中,引入了钻石操作符<>,可以使用它来省略类型参数的声明。例如: ```java List<String> list = new ArrayList<>(); ``` 在这个例子中,`ArrayList`的类型参数可以被编译器自动推断为`String`。 八、总结 Java泛型是一个强大的特性,可以提高代码的可读性和安全性,降低代码的耦合度。在使用泛型时,需要注意它的基本语法、使用方法、通配符、继承、限定、擦除和类型推断等问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值