那么我们就正式进入数据结构的学习了,在数据结构开始之前,我们另外需要再学习一些Java的语法性质 那么本篇博客我将带大家认识一下泛型,泛型我们在之前可能都遇到过 就是两个尖括号<>,在后面的ArrayList,List等等都会使用到 源码中也有非常多的”<>“ 所以对于泛型的理解,不言而喻很重要啦,想要学好后面的内容,看懂后面的代码,本篇博客要仔细阅读下去哦

首先,很重要的一点,本篇博客的排版顺序不适合没接触过泛型的小白,如果你对泛型一无所知,请按照 1>3>4>5>6>2 的阅读顺序,说人话就是,看不懂的往下翻,下面就讲到了 创作不易,本篇干货满满,建议收藏食用哦

那废话不多说,首先大家看一下基本的框架 泛型.png

1. 🦩 灵魂三问

1.1 🦜 什么是泛型?

image.png
从百度百科我们可以看到,泛型的应用非常广泛。

泛型你可以理解为:将类型参数化,我们可以将一个类当作参数传过去,然后对应不同的类都能用这样一个泛型接口,泛型类或是泛型方法。

1.2 🦖 为什么要使用泛型?

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。包括类也是这样 那么我觉得,运用泛型一个目的就是上面所说的将类型参数化,当作参数来传递。 另外一个目的就是进行筛选,就比如你已经传过去一个类的参数,并且new出了一个对象,现在我们假设传过去的类型是String,那么这个对象所对应使用的类型就只能是String,如果是其他类,编译器就可以很精准得直接给我们报错,编译不成功,如果我们不用泛型,其实是挺麻烦的,具体怎么个麻烦法,请看下一个知识点

1.3 🐇 没有泛型之前是怎么做的?

我们接触过对象数组,那么对象数组中看起来是可以储存多个不同的对象的,但这里的不同只针对与同一个class创建出来的不同的对象,对象之间的属性不同 如果想要将不同class new出来的不同的对象放在一个数组中,这样的蜜汁操作似乎也不是不可以,我们来用万能的”Object[]”来试试吧

等一下!为啥要用Object类型创建数组捏?我来解释一下,
因为Object是所有类的爹,,咳咳,父类。
那么它就可以将不同类型创建出来的对象都向上转型为Object类,然后统一放到Object[]类型的数组里面,但是事实真的会与我们想象中的那样,Object[]真的有那么神通广大嘛?
image.png
好家伙,直接报错,那么这里的解决办法就是再去搞一个set的重写方法(真麻烦啊🥲)
image.png
那么改完之后,它倒还是报错,那么这一次的错误,应该将返回的Object类型的数组中的对象向下转型成String类型,才能被String类型的ret接收。
image.png
啊哈,终于不报错了。

  1. class MyObjectArray{
  2. public Object[] array = new Object[3];
  3. public Object get(int index){
  4. return this.array[index];
  5. }
  6. public void set(int index, int value){
  7. this.array[index] = value;
  8. }
  9. public void set(int index, String value){
  10. this.array[index] = value;
  11. }
  12. }
  13. public class TestDemo1 {
  14. public static void main(String[] args) {
  15. MyObjectArray myArray= new MyObjectArray();
  16. myArray.set(0,10);
  17. myArray.set(1,"Gremmie");
  18. String ret = (String) myArray.get(1);
  19. System.out.println(ret);
  20. }
  21. }

运行结果👇
image.png

那么问题来了,我们这么搞来搞去的,又是向上转型又是向下转型的,是不是有违我们代码简洁通用才是王道的理念? 而且,到了后面的数据结构的学习中,我们对于一个数组,一般只会去存放相同类型的对象,这样方便比较,也能方便我们进行排序,增删查改等操作,向上面所说的将不同类型的不同对象放在一个数组里面,我想这大概是面向对象OOP里面的缩小版,大可不必,这样做还不如单独写一个类,在类中定义不同的属性,用数组来存放不同类型的对象这个想法我觉得还是停留在C语言的面向过程的思想中。 跳出这个Thinking Box.就有一些牛人来引入了泛型这个概念,优化我们的代码,提高代码的执行率

2. 🐿️ 泛型的一些特性

ava的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。 如在代码中定义List和List等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与++模板机制实现方式之间的重要区别。

2.1 🐙 擦除机制

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 类型擦除的主要过程如下:

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  2. 移除所有的类型参数。

那么再白话一点,就是一个类的泛型<>中如果用不同类型(比如String,比如Integer)创建出不同的对象,那他们这里的<>中编译器最后都会转化为Object类型处理。

还不能理解?好吧,上代码

  1. /**
  2. * @author Gremmie102
  3. * @date 2022/4/27 15:44
  4. * @purpose : 类型擦除的实验
  5. */
  6. class MyGeneric<T> {
  7. T value;
  8. public MyGeneric(){
  9. }
  10. public MyGeneric(T val){
  11. value = val;
  12. }
  13. public T getValue(){
  14. return value;
  15. }
  16. public void setValue(T val){
  17. value = val;
  18. }
  19. }
  20. public class TestDemo2 {
  21. public static void main(String[] args) {
  22. MyGeneric<String> generic1 = new MyGeneric<>("Gremmie");
  23. MyGeneric<Integer> generic2 = new MyGeneric<>(19);
  24. if (generic1.getClass()==generic1.getClass()){
  25. System.out.println("Their class is same");
  26. }else {
  27. System.out.println("Their class is different");
  28. }
  29. }
  30. }
  31. 运行结果:
  32. C:\Users\lenovo\.jdks\openjdk-18\bin\java.exe "-javaagent:E:\IDEA\IntelliJ IDEA Community Edition 2021.3.2\lib\idea_rt.jar=63584:E:\IDEA\IntelliJ IDEA Community Edition 2021.3.2\bin" -Dfile.encoding=UTF-8 -classpath E:\JAVAcode\gyljava\GenericTest\out\production\GenericTest TestDemo2
  33. Their class is same
  34. Process finished with exit code 0

再举个例子: 数组要记住元素类型, 类型擦除会使得数组忘记自己定义的原来的泛型类型是什么, 放到List中就是这样的 List list = new ArrayList(); list.add(“a”); list.add(1); String o1 = (String) list.get(0); Integer o2 = (Integer) list.get(1); 这段能正常执行, 相当于在ArrayList中放了int类型, 就是因为类型擦除导致的,

那么我们从上面的代码运行结果可以看出,两次分别传入了不同的类型,一个是String,另外一个是Integer,但是最后我们通过getClass获取两个对象的信息时,发现两个对象的类是一样的,这就是泛型的擦除机制,关于反射什么的,我也不太懂,生硬讲解反而不好,等以后学到相关知识再来完善。

2.2 🤖泛型不能创建数组

这里我找了一篇文章,讲解的很好,大家可以去看一看这位大佬的观点
👉为什么泛型不能创建数组
那么我为大家总结一下

如果允许创建泛型数组,将能在数组p里存放任何类的对象,并且能够通过编译,因为在编译阶段p被认为是一个Object[ ],也就是p里面可以放一个int,也可以放一个Pair,当我们取出里面的int,并强制转换为Pair,调用它的info()时会怎样?java.lang.ClassCastException!这就违反了泛型引入的原则。所以,Java不允许创建泛型数组。 因为 在 Java 中是不能创建一个确切的泛型类型的数组的,除非是采用通配符的方式且要做显式类型转换才可以。

  1. List<Integer> [] lists = new ArrayList<>[];
  2. //后面的这个<>写上去是报错的
  3. //正确的写法应该是:
  4. List<Integer> [] lists1 = new ArrayList[];
  5. //如果允许第一种写方法的存在,那么多态的存在会导致运行时出现ClassCast异常
  6. //Object类是所有类的父类,所以可以:
  7. lists = [list1,list2,....]其中的list都是Integer泛型的
  8. Object[] objects = lists;
  9. listStr = new ArrayList<String>;
  10. listStr.add("...")
  11. //此时如果把listStr赋值到objects的第一个里,原则上是没有任何问题的
  12. //因为Object是所有类的父类
  13. //但是这个地方应该是Integer泛型的ArrayList,如果这样允许存在,
  14. // 要么运行时出现类转化异常
  15. // 要么lists定义时的泛型推断就失去了意义

2.3 🦚泛型的优点

  1. 泛型是将数据类型参数化,进行传递
  2. 使用 表示当前类是一个泛型类。
  3. 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换

看不懂没关系,继续往下翻,会有更新的理解!

3. 😺 泛型类

那么终于来到我们实打实的操作了

3.1 🐺 泛型类的使用

泛型类<类型实参> 变量名; // 定义一个泛型类引用 new 泛型类<类型实参>(构造方法实参); // 实例化一个泛型类对象

MyArray list = new MyArray(); 泛型只能接受类,所有的基本数据类型必须使用包装类!

我们也可以这样写 MyArray list = new MyArray<>(); // 可以推导出实例化需要的类型实参为 String 编译器将自动为我们推导出后面new 出来的对象中<>应该是什么类型

3.2 🐱 上界

我们在写一个带有泛型的类时,通常都希望传进<>的类有着一定的限制,比如父类子类继承关系,那我们将用到extends来定义一个上界,来限制泛型

  1. class 泛型类名称<类型形参 extends 类型边界> {
  2. ...
  3. }

image.png

这里即为只接受 Number 的子类型作为 E 的类型实参

有界的类型参数: 可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围。例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例。这就是有界类型参数的目的。 要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界

3.3 🐶 裸类型

裸类型我们用的不多,只是用来适应一些比较老的API的 例如:MyArray list = new MyArray(); 尽量规范语句,不要轻易使用裸类型

3.4 🦊 泛型标记符

常用的 有T,E,K,V,? 本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的: ? 表示不确定的 java 类型 T (type) 表示具体的一个java类型 K V (key value) 分别代表java键值中的Key Value E (element) 代表Element

那么?这个泛型通配符下面会详细讲

4. 🐯 泛型方法

下面是定义泛型方法的规则:

  1. 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的 )。
  2. 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  3. 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  4. 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像 int、double、char 等)。
  1. /**
  2. * @author Gremmie102
  3. * @date 2022/4/27 16:48
  4. * @purpose :
  5. */
  6. public class GenericMethodTest
  7. {
  8. // 泛型方法 printArray
  9. public static < E > void printArray( E[] inputArray )
  10. {
  11. // 输出数组元素
  12. for ( E element : inputArray ){
  13. System.out.printf( "%s ", element );
  14. }
  15. System.out.println();
  16. }
  17. public static void main( String args[] )
  18. {
  19. // 创建不同类型数组: Integer, Double 和 Character
  20. Integer[] intArray = { 1, 2, 3, 4, 5 };
  21. Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
  22. Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
  23. System.out.println( "整型数组元素为:" );
  24. printArray( intArray ); // 传递一个整型数组
  25. System.out.println( "\n双精度型数组元素为:" );
  26. printArray( doubleArray ); // 传递一个双精度型数组
  27. System.out.println( "\n字符型数组元素为:" );
  28. printArray( charArray ); // 传递一个字符型数组
  29. }
  30. }

image.png

5. 🦒 泛型接口

  1. /**
  2. * author : northcastle
  3. * createTime:2021/10/20
  4. * 自定义普通类,用来做泛型的实际参数
  5. */
  6. public class Cat {
  7. private String name;
  8. public Cat() {
  9. }
  10. public Cat(String name) {
  11. this.name = name;
  12. }
  13. public String getName() {
  14. return name;
  15. }
  16. public void setName(String name) {
  17. this.name = name;
  18. }
  19. @Override
  20. public String toString() {
  21. return "Cat{" +
  22. "name='" + name + '\'' +
  23. '}';
  24. }
  25. }
  26. public interface SayHello<T> {
  27. //声明了一个带有泛型类型的方法
  28. T printT(T t);
  29. }
  30. public class SayHelloImplA<T,E> implements SayHello<T>{
  31. private E e;
  32. @Override
  33. public T printT(T t) {
  34. System.out.println("This is SayHelloImplA "+t);
  35. return t;
  36. }
  37. //getter/setter方法
  38. public E getE() {
  39. return e;
  40. }
  41. public void setE(E e) {
  42. this.e = e;
  43. }
  44. //自定义普通的方法
  45. public void sayHelloE(){
  46. System.out.println("Hello E : "+e);
  47. }
  48. }
  49. public class Application {
  50. public static void main(String[] args) {
  51. //1.创建一个 泛型实现类的 对象
  52. SayHelloImplA<String, Integer> implA = new SayHelloImplA<>();
  53. implA.printT("我是泛型类-实现类");
  54. implA.setE(100);
  55. implA.sayHelloE();
  56. System.out.println("==================");
  57. //2.创建一个 普通实现类的 对象
  58. CommonImplB commonImplB = new CommonImplB();
  59. Cat huahua = new Cat("huahua");
  60. commonImplB.printT(huahua);
  61. }
  62. }

6. 🐮 泛型通配符”?”

那么我们创建出这么几个类

  1. class A{
  2. }
  3. class B extends A{
  4. }
  5. class C extends A{
  6. }

类B和类C都继承于类A。
那么再来实例化出两个List变量

  1. List<A> listA = new ArrayList<A>();
  2. List<B> listB = new ArrayList<B>();

我们这样做,都会报错

  1. listA = listB;
  2. listB = listA;

在 listA 中你可以插入 A类的实例,或者A类子类的实例(比如B和C)。如果下面的语句是合法的:

List listB = listA;

那么listA 里面可能会被放入非B类型的实例。

泛型通配符可以解决这个问题。泛型通配符主要针对以下两种需求:

  1. 从一个泛型集合里面读取元素
  2. 往一个泛型集合里面插入元素

这里有三种方式定义一个使用泛型通配符的集合(变量)。 List<?> listUknown = new ArrayList(); List<? extends A> listUknown = new ArrayList(); List<? super A> listUknown = new ArrayList(); List<?> 的意思是这个集合是一个可以持有任意类型的集合,它可以是List,也可以是List,或者List等等。 List、List 可以看成是不同的类型,这里的类型指的是集合的类型(如List、List),而不是集合所持有的类型(如A、B),但集合所持有元素的类型会决定集合的类型。

6.1 🐷 ?上界和下界

<? extends 上界> <? extends Number>//可以传入的实参类型是Number或者Number的子类 上界 <? extend Fruit> ,表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。 下界 <? super Apple>,表示Apple的所有父类,包括Fruit,一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了Object,其他的都接不住。

上界和下界的特点:

  1. 上界的list只能get,不能add(确切地说不能add出除null之外的对象,包括Object)
  2. 下界的list只能add,不能get
  1. import java.util.ArrayList;
  2. import java.util.List;
  3. class Fruit {}
  4. class Apple extends Fruit {}
  5. class Jonathan extends Apple {}
  6. class Orange extends Fruit {}
  7. public class CovariantArrays {
  8. public static void main(String[] args) {
  9. //上界
  10. List<? extends Fruit> flistTop = new ArrayList<Apple>();
  11. flistTop.add(null);
  12. //add Fruit对象会报错
  13. //flist.add(new Fruit());
  14. Fruit fruit1 = flistTop.get(0);
  15. //下界
  16. List<? super Apple> flistBottem = new ArrayList<Apple>();
  17. flistBottem.add(new Apple());
  18. flistBottem.add(new Jonathan());
  19. //get Apple对象会报错
  20. //Apple apple = flistBottem.get(0);
  21. }
  22. }

泛型就解释到这里,后面可能还会有补充 大家如果有什么补充直接dd我哦 希望能帮到你 感谢阅读~