【JavaSE】泛型

泛型的诞生

为什么要使用泛型

首先我们先了解什么是泛型程序设计:这意味着编写的代码可以被很多不同类型的对象所重用。


在Java SE 5.0之前,如果我们想实现这个泛型程序设计,可以使用Object类来接受任意类型的对象类型,但是会出现类型转换的问题,例:

我们使用Object类来引用一个数组,然后模拟实现ArrayList类
class ArrayList {
    private Object[] elem = new Object[10];
    private int usedSize;
    public Object get(int pos) {
        return elem[pos];
    }
    public void add(Object o) {
        elem[usedSize++] = o;
    }
}
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        //可以接受任何类型
        arrayList.add("String");
        arrayList.add(12);
        //但是如果要得到其内容并存储。就必须要类型转化
        String s = (String)arrayList.get(0);
        System.out.println(s);
        int i = (int)arrayList.get(1);
        System.out.println(i);
        //如果我们忘记对应下标的类型,用错误的类型转化后就会报错
        System.out.println((String)arrayList.get(1));
    }
    
//String
//12
//Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
   

这样的数组虽然可以存放任何数据,但是由于其元素的不确定性,导致程序出错,这时候泛型就顺理成章的创建出来了。

泛型的概念

泛型:是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型(通俗的讲就是适用于多种类型,不局限于某一类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,把它交给编译器去处理;这样就避免了类型转化的麻烦,使程序具有更好的可读性和安全性。
注:当不指定其类型时,默认为Object类

泛型的使用

泛型主要使用在:泛型接口,泛型类,泛型方法

泛型类

像我们所熟知的List下的ArrayList和LinkedList,Set下的HashSet和TreeSet,Map下的HashMap和TreeMap

语法

修饰符 class 泛型类名 <泛型变量类型>  //定义泛型类引用
new 泛型类<泛型变量类型>(构造方法实参) //实例化泛型对象

示例

public class MyArrayList<T> {
    //成员变量t,其类型由外部确定
    private T t;

    //构造方法
    public MyArrayList(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}

public static void main(String[] args) {
	MyArrayList<String> list = new MyArrayList<>("String");
}

泛型接口

语法

修饰符 interface接口名<代表泛型的变量> {  }

示例

public interface Interface<T>{
    void add(T t);
}

public class MyArrayList<T> implements Interface<T>{
    //成员变量t,其类型由外部确定
    private T t;

    @Override
    public void add(T t) {
        this.t = t;
    }

    //构造方法
    public MyArrayList(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    public static <T> T print(T t) {
        System.out.println(t.getClass().getName());
        System.out.println(t);
        return t;
    }
}

    public static void main(String[] args) {
        MyArrayList<String> list = new MyArrayList<>("String");
        list.add("Hello");
        System.out.println(list.getT()); //Hello
    }

泛型方法

语法

修饰符 <泛型的变量> 返回值类型 方法名(参数){  }

示例

//静态的泛型方法 需要在static后用<>声明泛型类型参数
    public static <T> T print(T t) {
        System.out.println(t.getClass().getName());
        System.out.println(t);
        return t;
    }

public static void main(String[] args) {
    MyArrayList.print("Hello");
    MyArrayList.print("250");
}

java.lang.String
Hello
java.lang.String
250

代码解释及注意事项

  1. 类名后的 < T > 代表占位符,表示当前类是一个泛型类

    类型形参一般使用一个大写字母表示,如:
    E 表示 Element
    K 表示 Key
    V 表示 Value
    N 表示 Number
    T 表示 Type
    S, U, V 等等 - 第二、第三、第四个类型

  2. 不能new泛型数组

T[] test = new t[5];//error
  1. 我们指定泛型元素后,在之后的操作中,编译器会对我们放入元素进行类型检查。
  2. 由于我们创建时已表面其类型,所以不用这样实例化对象ArrayList<Integer> List = new ArrayList<Integer>();

泛型是如何编译的

我们先观察非泛型代码:

public class TestOne {
    private Object obj;

    public Object getObj() {
        return obj;
    }

    public void setObj(Object obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        TestOne one = new TestOne();
        one.setObj("Hello");
        String s = (String)one.getObj();
    }
}

在idea中先build形成字节码文件,找到该文件夹按住shift + 鼠标右键 打开powerSell 输入javap -c TestOne 反汇编这个类
在这里插入图片描述
可以看出get和set方法都是直接存储和得到值,而转型是在get()后接受检查的。

现在我们加入泛型:

public class TestTwo<T>{
    private T obj;

    public T getObj() {
        return obj;
    }

    public void setObj(T obj) {
        this.obj = obj;
    }

    public static void main(String[] args) {
        TestTwo<String> two = new TestTwo<>();
        two.setObj("Hello");
        String s = two.getObj();
    }
}

在这里插入图片描述

首先我们看到二者产生的字节码文件完全相同,一般来说,在运行时阶段,Java编译器先执行类型检查,然后执行擦除或删除泛型信息,也就是把所有的T替换为Object的这种机制称为:擦除机制。

擦除产生的问题

由于擦除机制的存在,我们在泛型代码内部,无法获得任何有关泛型类型参数的信息,比如 ArrayList<String>和LinkedList<Integer>在运行时变成了相同的类型,即其原生类型List。

1.为什么不能new T()

在编译期间由于擦除机制其类型变为Object类,而new T() 是必须要有运行时类型信息的,否则运行时不知道实例的类型,没有办法创建实例,

2.运行时需要知道确切类型信息的操作无法进行

由于擦除了类型信息,例如new 操作,instanceof操作都不能正常工作,一段代码证明:

    public static void main(String[] args) {
        Class a = new ArrayList<Integer>().getClass();
        Class b = new ArrayList<String>().getClass();
        System.out.println(a == b);
    }
    //ture
3.如何实例化泛型数组

方法1:使用强制类型转化

public class TestArray<T> {
    T[] array = (T[])new Object[10];
    public T[] getArray() {
        return array;
    }
}
public static void main(String[] args) {
    TestArray<Integer> testArray = new TestArray<>();
    Integer[] String = testArray.getArray();
}
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

这种方式创建的泛型数组,由于我们不知道其类型是什么,使用时会出现ClssCastException错误。


方法2:

由于擦除了类型参数,所以泛型数组在编译期检查(静态类型检查),我们知道数组是在运行时存储和检查类型信息的(动态类型检查),所以我们可以通过反射机制来创建一个指定的数组,让其在new的时候得到其类型信息;由于java.lang.reflect包中的Array类允许动态的创建数组,并且Array静态方法newInstance可以构造数组,我们就可以利用该方法来创建数组

Array.newInstance(ComponentType componentType, int newLength);

第一个参数是数组内容类型,第二参数是新数组长度。即该方法的作用是:

  1. 获得a的类对象(Class对象),确定a是一个数组,然后获得a的内容类型
  2. 获得源数组长度和newLength比较

import java.lang.reflect.*;
public class MyArray<T> {
    public T[] array;

    //无参构造器
    public MyArray() {
    }
    /**
     * 通过反射创建指定类型数组
     */
    public MyArray(Class<T> type, int capacity) {
        array = (T[])Array.newInstance(type, capacity);
    }

    public T[] getArray() {
        return array;
    }

    public void setVal(int pos, T item) {
        array[pos] = item;
    }
}
public static void main(String[] args) {
    MyArray<Integer> myArray = new MyArray<>(Integer.class, 10);
    Integer[] integers = myArray.getArray();
}

结论:

  1. 在虚拟机中没有泛型,只有普通类和方法
  2. 所有的类型参数都用它们的限定类型替换。
  3. 为保持类型安全性,必要时插入强制类型转换。

边界

由于擦除机制的存在,我们不知道类的边界在何处,这时候我们就可以使用extends关键字来制定边界。

上界

只能使用extend的父类及其子类作为类型参数

语法
class 泛型类名称<类型形参 extends 类型边界> {
...
}

示例

使用Number父类,其子类如下:
在这里插入图片描述

public class Array<T extends Number> {

}

    public static void main(String[] args) {
        Array<Integer> integerArray = new Array<>();
        Array<String> stringArray = new Array<String>();
    }
//Error:(13, 15) java: 类型参数java.lang.String不在类型变量T的范围内

通配符

通配符的应用

通配符是用来解决泛型无法协变的问题的,协变指的就是如果 Student 是 Person 的子类,那么 List< Student> 也应该是 List< Person> 的子类。但是泛型是不支持这样的父子类关系的。泛型 T 是确定的类型,一旦你传了我就定下来了,而通配符则更为灵活或者说是不确定,更多的是用于扩充参数的范围

示例:

public class Name<T> {
    private T name;

    public T getName() {
        return name;
    }

    public void setName(T name) {
        this.name = name;
    }
}

public static void main(String[] args) {
    Name<String> name = new Name<>();
    name.setName("zhangsan");
    print(name);
}
private static void print(Name<String> tmp) {
    System.out.println(tmp.getName());
}

很明显我们可以得到zhangsan这个结果,但是如果我们把类型参数更换:


    public static void main(String[] args) {
        Name<Integer> name = new Name<>();
        name.setName(313);
        print(name);
    }
    private static void print(Name<String> tmp) {
        System.out.println(tmp.getName());
    }
    //Error:(20, 15) java: 不兼容的类型: Package2.Name<java.lang.Integer>无法转换为Package2.Name<java.lang.String>   

所以我们现在需要的是一个可以接受所有参数的方法,但是不能让用户随意修改,这时候需要通配符来“?”来处理

    public static void main(String[] args) {
        Name<Integer> name = new Name<>();
        name.setName(313);
        print(name);
    }
    private static void print(Name<?> tmp) {
        tmp.setName(123);
        System.out.println(tmp.getName());
    }

//Error:(23, 21) java: 不兼容的类型: int无法转换为capture#1, 共 ?

因为通配符可以接受任意类型,所有其类型是不知道的,所以我们不能修改它

通配符上界

语法

<? extend 上界>//传入的类型参数只能是上界及其上界子类

示例

在这里插入图片描述
我们按照父子关系编写代码

public class Food {
}
class Fruit extends Food {
    @Override
    public String toString() {
        return "Fruit{}";
    }
}

class Apple extends Fruit{
    @Override
    public String toString() {
        return "Apple{}";
    }
}

class GreenApple extends Apple{
    @Override
    public String toString() {
        return "GreenApple{}";
    }
}
class RedApple extends Apple{
    @Override
    public String toString() {
        return "RedApple{}";
    }
}

class Banana extends Fruit{
    @Override
    public String toString() {
        return "Banana{}";
    }
}

class Meat extends Food{
    @Override
    public String toString() {
        return "Meat{}";
    }
}

class Pork extends Meat {
    @Override
    public String toString() {
        return "Pork{}";
    }
}
class Beef extends Meat {
    @Override
    public String toString() {
        return "Beef{}";
    }
}
public class Plate<T> {
    private T plate;

    public T getPlate() {
        return plate;
    }

    public void setPlate(T plate) {
        this.plate = plate;
    }
}
    public static void main(String[] args) {
        //Fruit的子类
        Plate<Banana> banana = new Plate<>();
        banana.setPlate(new Banana());
        print(banana);

        Plate<Apple> apple = new Plate<>();
        apple.setPlate(new Apple());
        print(apple);

        //Apple的子类
        Plate<GreenApple> greenApple = new Plate<>();
        greenApple.setPlate(new GreenApple());
        print(greenApple);

        Plate<RedApple> redApple = new Plate<>();
        redApple.setPlate(new RedApple());
        print(redApple);
    }

    private static void print(Plate<? extends Fruit> tmp) {
        System.out.println(tmp.getPlate());
        //tmp.setPlate(new Apple());
    }
    //Error:(33, 22) java: 不兼容的类型: Package3.Apple无法转换为capture#1, 共 ? extends Package3.Fruit

结论:通配符的上界不能写入数据,只能读取数据

通配符下界

语法

<? super 下界>//传入的实参类型只能是下界或者下界的父类

示例

    public static void main(String[] args) {
        Plate<Meat> meat = new Plate<>();
        meat.setPlate(new Meat());
        print(meat);

        Plate<Food> food = new Plate<>();
        food.setPlate(new Food());
        print(food);

    }
    //tmp接受Meat及其父类
    private static void print(Plate<? super Meat> tmp) {
        //此时上界是Meat,传入子类对象(Pork,Beef)发生向上转型
        tmp.setPlate(new Pork());
        tmp.setPlate(new Beef());
        //写入Meat本身
        tmp.setPlate(new Meat());
        //不能接受,因为不能确定是那个父类
        //Meat meat = tmp.getPlate();
        System.out.println(tmp.getPlate());
    }

结论:通配符的下界只能写入数据,不能读取数据

总结

<? extends E> 适用于使用**父类特性**,重点在于使用,读取内容。 <? super E>适用于**保存具有父类特性的子类**,重点在于保存,写入内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zzt.opkk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值