从零开始学Java-泛型

116 阅读13分钟

泛型入门

什么是泛型

Java中的泛型是伪泛型。一个假的泛型,只是在编译时期有效的。

Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。它允许我们通过预先定义模板,为多种不同的数据类型执行相同的逻辑,实现更好的代码复用。 Java 编译器实现了对泛型参数进行检测,并且运行我们通过泛型参数来指定传入的对象类型,比如ArrayList<Integer> list = new ArrayList<Integer>()就指定了这个 ArrayList 中只能存放 Integer 对象,如果传入其他类型的对象就会报错。

实例

如果我们没有给集合指定类型。默认认为所有的数据类型都是object类型,此时可以往集合添加任意的数据类型。

带来一个坏处:我们在获取数据的时候。无法使用他的特有行为

我们先来看个例子:

ArrayList list = new ArrayList();
list.add(123);
list.add("abc");
Iterator it = list.iterator();
while (it.hasNext()){
    Object obj = it.next();
    System.out.println(obj);
}

我们现在不指定泛型,他是不是表示什么类型都可以添加进去呀,那这不是很方便吗?那这个时候如果我想获取集合的长度怎么办?我们来看一下:

image.png

是不是就获取不到他的长度了呀,因为多态有个弊端:是不能访问子类的特有功能的。那我们想要获取怎么办呢?需要进行强转:

image.png

是不是觉得有点麻烦呀。此时Java就推出了泛型,可以在添加数据的时候就把类型进行统一,而且我们在获取数据的时候也不用强转了,非常的方便。我们来看一下吧:

image.png

此时加上了String类型的泛型,是不是添加整数的时候直接报错了呀,类型不符合,只能添加String类型的数据。这就是泛型给我们带来的好处。下面我们也来说一说他的好吃吧!

那下面我们就在想了,我们能不能自己定义一个泛型去使用呢?那必须是可以的呀,我们一起往下看:

泛型类别

泛型主要的使用方式有三种:

  1. 定义在类后面:泛型类
  2. 定义在方法上面:泛型接口
  3. 定义在接口后面:泛型方法

下面我们先来学习第一个泛型类吧:

泛型类

  • 概念:使用类名后面定义的泛型:所有方法都能用

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

格式

修饰符 class 类型<类型>{
}

例:
public class ArrayList<T>{
}

这里的T是什么意思呢?此处的E可以理解为变量,但是不是用来记录数据的,而是记录数据的类型。可以写成:T、E、K、V等。

下面我们来实操爽一下吧!

泛型类定义

  • 定义一个带有泛型的类:
public class MyArrayList<T> {
    Object[] obj = new Object[10];
    int size;

    /*
    * T:表示不确定的类型,该类是在类名后面已经定义过的。
    * t:形参的名字,变量名
    * */
    public boolean add(T t){
        obj[size] = t;
        size++;
        return true;
    }

    public T get(int index){
        return (T)obj[index];
    }

    @Override
    public String toString() {
        return Arrays.toString(obj);
    }
}
  • 实例化泛型类:
MyArrayList<String> list = new MyArrayList<>();
  • 使用泛型类: 下面我们来添加一些数据看一下吧:
MyArrayList<String> list = new MyArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
System.out.println(list);

那我如果想添加Integer类型的话是不是只需要把String改为Integer就可以啦。

MyArrayList<Integer> list = new MyArrayList<>();
list.add(111);
list.add(222);
list.add(333);
System.out.println(list);

好啦,这就是泛型类的定义和使用,我们下面来学习泛型的方法吧!

泛型方法

  • 概念:在方法申明上定义自己的泛型:只有本方法能用

使用场景:方法中形参类型不确定时,可以使用类名后面定义的泛型<>

我们会回到上面那个例子,我们来看一下:

public class MyArrayList2<T> {
    public boolean add(T t){
        obj[size] = t;
        size++;
        return true;
    }
}

add添加数据的类型不确定的,是不是可以使用类名后面的泛型T来表示不确定的类型呀,那么我们如果是在类名后面写个了泛型的话就表示这个类当中所有的方法都可以使用不确定的类型T。

那现在如果说这个类当中只有一个方法的形参不确定,那现在我们就没必要定义在类的后面,这个时候就可以把泛型定义在方上面,那么这个方法就叫做泛型方法。

格式

修饰符<类型> 返回值类型 方法名<类型 变量名>{
}

例:
public <T> void show<T t>{
}

这里的T是什么意思呢?此处的E可以理解为变量,但是不是用来记录数据的,而是记录数据的类型。可以写成:T、E、K、V等。

下面我们来实操爽一下吧!

泛型方法定义

  • 我们下面来看个案例:

    • 定义一个工具类:ListUtil,类中定义一个静态方法addAll,用来添加多个集合的元素
  • 由于是工具类,所有建议给他私有化构造方法:

private ListUtil(){}
  • 定义一个带有泛型的类:
/*
* 参数一:集合
* 参数二~最后:要添加的元素
* */
public static<T> void addAll(ArrayList<T> list,T num1,T num2,T num3,T num4){
    list.add(num1);
    list.add(num2);
    list.add(num3);
    list.add(num4);
}
  • 实例化泛型方法:
ArrayList<String> list1 = new ArrayList<>();
  • 添加数据到集合看一下吧:
ArrayList<String> list1 = new ArrayList<>();
ListUtil.addAll(list1,"aaa","bbb","ccc","ddd");
System.out.println(list1);

ArrayList<Integer> list2 = new ArrayList<>();
ListUtil.addAll(list2,10,20,30,40);
System.out.println(list2);

那这个时候我如果想要添加多个元素怎么办呢?可以使用到后面的知识点:

public static<T> void addAll2(ArrayList<T> list,T...t){
    for (T elment : t) {
        list.add(elment);
    }
}

这个时候我们想要添加多少数据都可以啦,我们来看一下:

image.png

image.png

是不是想要添加多少数据都可以呀。好啦,这就是泛型方法的定义和使用,我们下面来学习泛型的接口吧!

泛型接口

  • 概念:泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中

格式

修饰符 interface 接口名<类型>{
}

例:
public interface list<T>{
}
  • 重点:如何使用一个带泛型的接口
    • 方式一:实现类给出具体类型
    • 方式二:实现类延续泛型,创建对象时再确定。

下面我们来实操爽一下吧!

泛型接口定义

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

public class MyArrayList1 implements List<String> {
    @Override
    public boolean add(String s) {
        return false;
    }
}

现在是不是指定了数据类型是String类型呀,我们来添加数据看一下:

  • 实例化泛型方法:
MyArrayList1 list = new MyArrayList1();

image.png

是不是添加int数据类型的时候就报错了呀,只能添加String类型。好啦,这就是实现类给出具体类型。

那如果说我实现类也不明确类型,该怎么办呢?这个时候就要用到我们的方式二了,我们一起往下看:

方式二:创建对象时再确定

public class MyArrayList2<T> implements List<T>{
    @Override
    public boolean add(T t) {
        return false;
    }
}

现在他的类型是不是就不确定了呀,我们来添加数据看一下:

  • 实例化泛型方法:
MyArrayList2<String> list1 = new MyArrayList2<>();

image.png

这个时候是不是就能确定类型了呀!

好啦,这就是泛型的三种定义和使用我们就学习完毕啦,下面我们来说一下最重要的泛型继承和通配符吧!

泛型的继承

泛型不具备继承性,但是数据具备继承性

泛型不具备继承性?是什么意思呢?我们来看一段代码:

image.png

是不是此时泛型里面写的是什么类型,只能传递什么类型的数据呀,这就是泛型不具备继承性。

那数据具备继承性又是什么意思呢?我们来看一段代码:

image.png

这个时候是不是可以同时创建对象又不报错呀,这就是数据的继承性。

那我们是不是可以使用泛型方法来给他定义呀:

public static<T> void method(ArrayList<T> list){}

添加一下看看:

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

image.png

这个时候是不是就没有报错了呀!

但是泛型方法有个弊端:此时他可以接收任意的数据类型。那我如果这个时候在添加一个学生类看一下:

image.png

是不是也能接收呀,但是我就想了,我只希望只能传递Ye Fu Zi的话怎么办呢?有没有解决方案呢?其实是有的,我们这个时候就可以使用泛型的通配符啦!

泛型的通配符

什么是泛型的通配符呢?

  • 泛型的通配符:他也表示不确定的类型。但是他可以进行类型的限定。
  • 他的类型限定有两种:
  1. ? extends E: 表示可以传递E或者E所有的子类类型。
  2. ? super E:表示可以传递E或者E所有的父类类型。

下面我们来演示一下吧:

代码演示:?

public static<T> void method(ArrayList<T> list){}
public static void method1(ArrayList<?> list){}

image.png

如果我们使用?的话是不是在前面就不用在自定义了呀,他也是表示不确定类型。

代码演示:? extends E

public static void method(ArrayList<? extends Ye> list){}

这个是表示?是任意类型,但是这些类型必须是继承于Ye的,如果没有继承于Ye那他就接收不了就会报错,我们来看一下:

image.png

是不是因为Student没有继承于Ye类,所以就接收不了就会报错了呀!我们再来看下一个吧:

代码演示:? super E

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

这个时候我改成Fu就表示只能接收Fu类或者Fu的父类,否则其他都会报错:

image.png

好啦,这就是泛型的通配符。到这里基本的泛型定义以及使用我们就学习完毕啦,下面我们来做个总结吧:

泛型总结

泛型的好处

  1. 统一数据类型
  2. 把运行时期的问题提前到了编译期间,避免了强转可能出现的异常,因为在编译阶段类型就能确定下来了。

泛型的细节

  1. 泛型中不能写基本数据类型
  2. 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型
  3. 如果不写泛型,类型默认是Object

定义泛型

  1. 泛型类:在类名后面定义泛型,创建该类对象的时候,确定类型
  2. 泛型方法:在修饰符后面定义方法,调用该方法的时候,确定类型
  3. 泛型接口:在接口名后面定义泛型,实现类确定类型,实现类延续泛型

泛型的继承和通配符

  1. 泛型的通配符:
  2. ? extends E: 表示可以传递E或者E所有的子类类型。
  3. ? super E:表示可以传递E或者E所有的父类类型。

使用场景

  1. 定义类、方法、接口的时候,如果类型不确定,就可以定义泛型
  2. 如果类型不确定,但是能知道是哪个继承体系中的,可以使用泛型的通配符

下面我们来做个综合案例吧:

综合案例

  • 需求:定义一个继承结构:

  • 动物:猫、狗。

    • 猫:波斯猫、狸花猫
    • 狗:泰迪、哈士奇
  • 属性:名字、年龄

  • 行为:吃东西

    • 波斯猫方法体打印:一只叫做xxx的。x岁的波斯猫。正在吃小饼干
    • 狸花猫方法体打印:一只叫做xxx的。X岁的狸花猫。正在吃鱼
    • 泰迪方法体打印:一只叫做xxx的。x岁的泰迪。正在吃骨头,边吃边蹭
    • 哈士奇方法体打印:一只叫做xxx的。X岁的哈士奇,正在吃骨头。边吃边拆家
    • 测试类中定义一个方法用于饲养动物
      • public static void keepPet(ArrayList list){
      • //遍历集合。调用动物的eat方法
      • }
    • 要求1:该方法能养所有品种的猫,但是不能养狗
    • 要求2:该方法能养所有品种的狗。但是不能养猫
    • 要求3:该方法能养所有的动物,但是不能传递其他类型
  • 第一步:定义一个动物类

public abstract class Animal {
    // 动物类
    private String name;
    private int age;

    public abstract void eat();
}
  • 第二步:定义一个猫类
public abstract class Cat extends Animal{
    // 猫类
    public Cat(String name,int age){
        super(name,age);
    }
}
  • 因为猫继承了动物类,动物类是个抽象类,实现抽象类就有两个方法:

    • 1.继承抽象类,重写里面所有的抽象方法
    • 2.本身Cat也是一个抽象类,让Cat的子类在重写方法

    因为两个猫吃的行为不一样,所以这边不能直接重写,因此采取第二种方案。

  • 定义一个波斯猫类并重写方法:

public class PersianCat extends Cat{
    public PersianCat(String name, int age) {
        super(name, age);
    }
    // 波斯猫类
    @Override
    public void eat() {
        System.out.println("一只叫做"+ getName() + "的。" + getAge() + "岁的波斯猫。正在吃小饼干");
    }
}
  • 定义一个狸花猫类并重写方法:
public class LiHuaCat extends Cat{
    public LiHuaCat(String name,int age){
        super(name,age);
    }
    // 狸花猫类
    @Override
    public void eat() {
        System.out.println("一只叫做"+ getName() + "的。" + getAge() + "岁的狸花猫。正在吃鱼");
    }

}
  • 第三步:定义一个狗类
public abstract class Dog extends Animal{
    // 狗类
    public Dog(String name,int age){
        super(name,age);
    }
}

因为两个狗的行为不一样,所以这边不能直接重写,因此和猫一样采取第二种方案。

  • 定义一个泰迪狗类并重写方法:
public class TeddyDog extends Dog{
    public TeddyDog(String name, int age) {
        super(name, age);
    }
    // 泰迪狗类
    @Override
    public void eat() {
        System.out.println("一只叫做"+ getName() + "的。" + getAge() + "岁的泰迪。正在吃骨头,还在边吃边蹭!");
    }
}
  • 定义一个哈士奇狗类并重写方法:
public class HuskyDog extends Dog{
    public HuskyDog(String name, int age) {
        super(name, age);
    }
    // 哈士奇类
    @Override
    public void eat() {
        System.out.println("一只叫做"+ getName() + "的。" + getAge() + "岁的哈士奇。正在吃骨头,还在边吃边拆家!");
    }
}
  • 第四步:回到测试类定义一个方法:
public static void KeepPet(ArrayList<? extends Animal> list){
    // 遍历集合。调用动物的eat方法
    for (Animal animal : list) {
        animal.eat();
    }
}
  • 第五步:创建对象并添加元素调用方法进行遍历:
// 1.定义集合
ArrayList<PersianCat> list1 = new ArrayList<>();
ArrayList<LiHuaCat> list2 = new ArrayList<>();
ArrayList<TeddyDog> list3 = new ArrayList<>();
ArrayList<HuskyDog> list4 = new ArrayList<>();
// 2.添加元素并调用方法
list1.add(new PersianCat("花花",2));
KeepPet(list1);
list2.add(new LiHuaCat("咪咪",3));
KeepPet(list2);
list3.add(new TeddyDog("乐乐",2));
KeepPet(list3);
list4.add(new HuskyDog("可乐",3));
KeepPet(list4);

下面我们来运行看一下吧:

image.png

是不是和需求一样啦。好啦,到这里泛型的三种定义和使用就学习完毕啦,有什么不懂的可以在评论区互相探讨哟,我们下期不见不散!!!

==最后非常感谢您的阅读,也希望能得到您的反馈  ==