Java 中的泛型(超全详解)

一、泛型概述

1. 什么是泛型?为什么要使用泛型?

泛型,即"参数化类型"。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错
参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

复制代码
//没有泛型的时候,集合如何存储数据
//结论:
//如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型
//此时可以往集合添加任意的数据类型。
//带来一个坏处:我们在获取数据的时候,无法使用他的特有行为。
//此时推出了泛型,可以在添加数据的时候就把类型进行统一。
//而且我们在获取数据的时候,也省的强转了,非常的方便。

2. 泛型使用场景

在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。

cpp 复制代码
public class demo1 {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");
        for(String s:list){
            System.out.println(s);
        }
    }
}
  • 上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。
    但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:
cpp 复制代码
 public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add(666);

        for (int i = 0; i < list.size(); i++) {
            System.out.println((String)list.get(i));
        }
    }
  • 上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。

那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。

使用泛型代码 如下:

cpp 复制代码
 public static void main(String[] args) {
        ArrayList<String> list = new ArrayList();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        //list.add(666);// 在编译阶段,编译器会报错
        
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
  • < String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。

3.总结

泛型的出现就是为了统一集合当中数据类型的

二、泛型类

泛型类的定义

  • 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。

  • 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:

Dart 复制代码
  T :代表一般的任何类。
  E :代表 Element 元素的意思,或者 Exception 异常的意思。
  K :代表 Key 的意思。
  V :代表 Value 的意思,通常与 K 一起配合使用。
  S :代表 Subtype 的意思,文章后面部分会讲解示意。

自己实现集合

代码如下:

cpp 复制代码
package fangxing;

import java.util.Arrays;

public class MyArrayList<E> {
    Object[] obj = new Object[10];
    int size = 0;

    /*
    E: 表示不确定的类型,该类型在类名后面已经定义过了
    e: 形参的名字,变量名
     */
    public boolean add(E e) {
        obj[size++] = e;
        return true;
        //当添加成功以后,集合还是会把这些数据当做Object类型处理
    }

    public E get(int index) {
        return (E) obj[index];
        //获取的时候集合在把他强转<E>类型

    }

    @Override
    public String toString() {
           return Arrays.toString(obj);

    }
}
cpp 复制代码
package fangxing;

import javax.xml.stream.events.StartDocument;

public class demo3 {
    public static void main(String[] args) {
        MyArrayList<String> list = new MyArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        System.out.println(list);
    }
}

三、泛型方法

格式

cpp 复制代码
package fangxing;

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

public class ListUtil {
    private ListUtil() {
    }

    /*
    参数一:集合
    参数二: 最后要添加的元素
     */
    public static <E> void addAll(ArrayList<E> list, E e1, E e2) {
        list.add(e1);
        list.add(e2);
    }

}
cpp 复制代码
package fangxing;

import java.util.ArrayList;

public class demo4 {
    public static void main(String[] args) {
        ArrayList<String>list=new ArrayList<>();
        ListUtil.addAll(list,"zhangsan","lisi");
        System.out.println(list);//[zhangsan, lisi]

    }
}

添加很多元素

cpp 复制代码
public static <E> void addAll(ArrayList<E> list, E ...e1) {
        for (E e : e1) {
          list.add(e);
        }

四、泛型接口

方法1:实现类给出具体类型

举例:

cpp 复制代码
public class MyArrayList2  implements List<String> 
cpp 复制代码
public static void main(String[] args) {
        MyArrayList2 list2=new MyArrayList2();
    }

方法2: 实现类延续泛型,创建对象再确定

cpp 复制代码
public class MyArrayList3 <E>  implements List<E>
cpp 复制代码
  MyArrayList3<String> list = new MyArrayList3<>();

五、类型擦除

1. 什么是类型擦除

泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。

换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。

其实Java中的泛型本质是伪泛型

当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型, 如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型

当代码编译到class文件的时候,泛型就消失,叫泛型的擦除

看一个例子,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。

cpp 复制代码
public class GenericType {
    public static void main(String[] args) {  
        ArrayList<String> arrayString = new ArrayList<String>();   
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();   
        System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
    }  
}

在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。

明明我们在 <> 中传入了两种不同的数据类型,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。

那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢?

  • 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释

2. 类型擦除的原理

假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:

不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?

换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?

Java 是如何解决这个问题的?

其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。

可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。

当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;
当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。
在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。
进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)

举例如下:

cpp 复制代码
public class GenericType {
    public static void main(String[] args) {  
        ArrayList<Integer> arrayInteger = new ArrayList<Integer>();// 设置验票系统   
        arrayInteger.add(111);// 观众进场,验票系统验票,门票会被收走(类型擦除)
        Integer n = arrayInteger.get(0);// 获取观众信息,编译器会进行强制类型转换
        System.out.println(n);
    }  
}

擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:

对原始方法 get() 的调用,返回的是 Object 类型;
将返回的 Object 类型强制转换为 Integer 类型;

代码如下:

ruby 复制代码
	Integer n = arrayInteger.get(0);// 这条代码底层如下:
	
	//(1)get() 方法的返回值返回的是 Object 类型
	Object object = arrayInteger.get(0);
	//(2)编译器自动插入 Integer 的强制类型转换
	Integer n = (Integer) object;

3. 类型擦除小结

1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);

2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。

六、泛型通配符

1. 泛型的继承

复制代码
泛型不具备继承性,但是数据具备继承性

此时,泛型里面写的什么类型,那么就传递什么类型的数据

泛型不具备继承性举例

ruby 复制代码
package fangxing;

import java.util.ArrayList;

public class demo5 {
    public static void main(String[] args) {
        /*
        泛型不具备继承性,但是数据具备继承性
         */
        ArrayList<Ye> list1=new ArrayList<>();
        ArrayList<Fu> list2=new ArrayList<>();
        ArrayList<Zi> list3=new ArrayList<>();
        //调用method方法
        method(list1);
        //method(list2);//编译错误
//method(list3);//编译错误
    }
    /*
    此时,泛型里面写的什么类型,那么就传递什么类型的数据
     */
    public static  void method(ArrayList<Ye> list){

    }
}

class Ye{

}
class Fu extends Ye{

}
class Zi extends Fu{

}

数据具备继承性

python 复制代码
   //数据具备继承性
        list1.add(new Ye());//添加爷爷的对象等
        list1.add(new Fu());
        list1.add(new Zi());
复制代码
定义一个方法,形参是一个集合,但是集合中的数据类型不确定。
复制代码
应用场景:
\* 1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
\* 2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符
\* 泛型的通配符:
\* 关键点:可以限定类型的范围。
复制代码
/*
 * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。
 * 弊端:
 *      利用泛型方法有一个小弊端,此时他可以接受任意的数据类型
 *      Ye  Fu   Zi    Student
 *
 * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi
 *
 * 此时我们就可以使用泛型的通配符:
 *      ?也表示不确定的类型
 *      他可以进行类型的限定
 *      ? extends E: 表示可以传递E或者E所有的子类类型
 *      ? super E:表示可以传递E或者E所有的父类类型
 *

举例

ruby 复制代码
package fangxing;

import java.util.ArrayList;

/*
 *   需求:
 *       定义一个方法,形参是一个集合,但是集合中的数据类型不确定。
 *
 * */
/*
 * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。
 * 弊端:
 *      利用泛型方法有一个小弊端,此时他可以接受任意的数据类型
 *      Ye  Fu   Zi    Student
 *
 * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi
 *
 * 此时我们就可以使用泛型的通配符:
 *      ?也表示不确定的类型
 *      他可以进行类型的限定
 *      ? extends E: 表示可以传递E或者E所有的子类类型
 *      ? super E:表示可以传递E或者E所有的父类类型
 *
 * 应用场景:
 *      1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
 *      2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符
 * 泛型的通配符:
 *      关键点:可以限定类型的范围。
 *
 * */
public class demo6 {
    public static void main(String[] args) {

        //创建集合的对象
        ArrayList<Ye> list1 = new ArrayList<>();
        ArrayList<Fu> list2 = new ArrayList<>();
        ArrayList<Zi> list3 = new ArrayList<>();

        ArrayList<Student2> list4 = new ArrayList<>();

        method(list1);
        method(list2);
        //method(list3);


        //method(list4);
    }
    public static void method(ArrayList<? super Fu> list) {

    }
}
class Ye {
}

class Fu extends Ye {
}

class Zi extends Fu {
}

class Student2{}

2.练习

复制代码
/\*
需求:
定义一个继承结构:
动物
\| \|
猫 狗
\| \| \| \|
波斯猫 狸花猫 泰迪 哈士奇
属性:名字,年龄
行为:吃东西
波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干
狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼
泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭
哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家
测试类中定义一个方法用于饲养动物
public static void keepPet(ArrayList\<???\> list){
//遍历集合,调用动物的eat方法
}
要求1:该方法能养所有品种的猫,但是不能养狗
要求2:该方法能养所有品种的狗,但是不能养猫
要求3:该方法能养所有的动物,但是不能传递其他类型
\*/

测试类

ruby 复制代码
package lx;

import java.util.ArrayList;

public class demo1 {
    /*
          需求:
              定义一个继承结构:
                                  动物
                       |                           |
                       猫                          狗
                    |      |                    |      |
                 波斯猫   狸花猫                泰迪   哈士奇
               属性:名字,年龄
               行为:吃东西
                     波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干
                     狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼
                     泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭
                     哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家

          测试类中定义一个方法用于饲养动物
              public static void keepPet(ArrayList<???> list){
                  //遍历集合,调用动物的eat方法
              }
          要求1:该方法能养所有品种的猫,但是不能养狗
          要求2:该方法能养所有品种的狗,但是不能养猫
          要求3:该方法能养所有的动物,但是不能传递其他类型
       */
    public static void main(String[] args) {
        HuskyDog h = new HuskyDog("哈士奇", 1);
        LihuaCat l = new LihuaCat("狸花猫", 2);
        PersianCat p = new PersianCat("波斯猫", 3);
        TeddyDog t = new TeddyDog("泰迪", 4);
        ArrayList<LihuaCat> list1 = new ArrayList<>();
        ArrayList<PersianCat> list2 = new ArrayList<>();
        // 向列表中添加一些猫的实例
        list1.add(l);
        list2.add(p);
        //调用方法
        keepPet1(list1);
        keepPet1(list2);
        System.out.println("-------------------------------------------");
        ArrayList<HuskyDog> list3 = new ArrayList<>();
        ArrayList<TeddyDog> list4 = new ArrayList<>();
        // 向列表中添加一些狗的实例
        list3.add(h);
        list4.add(t);
        //调用方法
        keepPet2(list3);
        keepPet2(list4);
        System.out.println("-------------------------------------------");
        list1.add(l);
        list2.add(p);
        list3.add(h);
        list4.add(t);
        keepPet3(list1);
        keepPet3(list2);
        keepPet3(list3);
        keepPet3(list4);
    }


    /*
    此时我们就可以使用泛型的通配符:
      ?也表示不确定的类型
      他可以进行类型的限定
      ? extends E: 表示可以传递E或者E所有的子类类型
     ? super E:表示可以传递E或者E所有的父类类型
     */
    //  要求1:该方法能养所有品种的猫,但是不能养狗
    public static void keepPet1(ArrayList<? extends Cat> list) {
        //遍历集合,调用动物的eat方法
        for (Cat cat : list) {
            cat.eat();
        }
    }
    //  要求2:该方法能养所有品种的狗,但是不能养猫
    public static void keepPet2(ArrayList<? extends Dog> list) {
        //遍历集合,调用动物的eat方法
        for (Dog dog : list) {
            dog.eat();
        }
    }

    //  要求3:该方法能养所有的动物,但是不能传递其他类型
    public static void keepPet3(ArrayList<? extends Animal> list) {
        //遍历集合,调用动物的eat方法
        for (Animal animal : list) {
            animal.eat();
        }
    }
}

Animal类

cpp 复制代码
package lx;

public abstract class Animal {
    private String name;
    private int age;

    public Animal() {
    }

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 获取
     * @return name
     */
    public String getName() {
        return name;
    }

    /**
     * 设置
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 获取
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "Animal{name = " + name + ", age = " + age + "}";
    }
    public abstract  void eat();
}

cat类型

cs 复制代码
package lx;

public abstract class Cat extends Animal{

    public Cat() {
    }

    public Cat(String name, int age) {
        super(name, age);
    }
}

Dog类

cs 复制代码
package lx;

public abstract class Dog extends Animal{
    public Dog() {
    }

    public Dog(String name, int age) {
        super(name, age);
    }
}

哈士奇类

cpp 复制代码
package lx;

public class HuskyDog extends Dog{
    @Override
    public void eat() {
        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的哈士奇,正在吃骨头,边吃边拆家");

    }

    public HuskyDog() {
    }

    public HuskyDog(String name, int age) {
        super(name, age);
    }
}

狸花猫类

cpp 复制代码
package lx;

public class LihuaCat extends Cat {
    @Override
    public void eat() {
        System.out.println("一只叫做" + getName() + "的," + getAge() + "岁的狸花猫,正在吃鱼");
    }

    public LihuaCat() {
    }

    public LihuaCat(String name, int age) {
        super(name, age);
    }
}

波斯猫类

cpp 复制代码
package lx;

public class PersianCat extends Cat{
    @Override
    public void eat() {
        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的波斯猫,正在吃小饼干");
    }

    public PersianCat() {
    }

    public PersianCat(String name, int age) {
        super(name, age);
    }
}

泰迪猫类

cpp 复制代码
package lx;

public class TeddyDog extends Dog{
    @Override
    public void eat() {
        System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁泰迪,正在吃骨头,边吃边蹭");
    }

    public TeddyDog() {
    }

    public TeddyDog(String name, int age) {
        super(name, age);
    }
}

总结

相关推荐
小_太_阳20 分钟前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾22 分钟前
scala借阅图书保存记录(三)
开发语言·后端·scala
黑胡子大叔的小屋40 分钟前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark43 分钟前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
唐 城44 分钟前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
雷神乐乐2 小时前
Spring学习(一)——Sping-XML
java·学习·spring
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
V+zmm101342 小时前
基于小程序宿舍报修系统的设计与实现ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·ssm
码银2 小时前
【python】银行客户流失预测预处理部分,独热编码·标签编码·数据离散化处理·数据筛选·数据分割
开发语言·python
从善若水3 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust