Java泛型的使用

148 阅读5分钟

一.泛型的由来

场景一:当定义一个数组后,数组所能容纳的类型就确定下来了,不能向数组添加其他类型的元素。

数组.png

由于数组无法动态的扩容,所以在项目中经常使用集合来存储多个元素。

场景二:当定义一个ArrayList集合后,我们来看一下在没有使用泛型的情况下,集合能存储哪些类型的元素。

ArrayList arrayList = new ArrayList();
arrayList.add("Hello");
arrayList.add(123);
arrayList.add(true);

此时编译没有报错,我们来分析一下原因:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    
    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; 

    private int size;
    
    
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

    private Object[] grow() {
        return grow(size + 1);
    }
    
    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
	
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    protected transient int modCount = 0;
   
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

}

首先调用了ArrayList的无参构造器,ArrayList对象的elementData指向了空,接着调用ArrayList对象的add方法时,该方法又调用了私有的add方法[封装思想],在私有add方法中:完成了数组的初始化和元素的添加。应该注意elementData的类型是Object[],由于所有类的超类都是Object,因此可以将不同类型的元素放入ArrayList集合中[多态思想]。

问题:那么如何指定ArrayList集合所能容纳元素的类型呢?

泛型应运而生。

解决:在创建ArrayList集合时,在构造器使用泛型,指明该集合容纳数据类型。

二.定义泛型

1.类泛型

在类上定义泛型,实例化该类时,指明类型。此时的泛型可以声明在成员变量,也可以声明在方法上。

2.方法泛型

在方法上定义泛型,该泛型可以声明在返回值类型或者参数类型中。

以上代码案例如下:

public class ArrayArg<T> {
    private T[] arr;

    // 构建泛型数组
    public void setArr(T... strs){
        arr = (T[]) Array.newInstance(strs.getClass().getComponentType(), strs.length);
        for (int i = 0; i < strs.length; i++) {
            arr[i] = strs[i];
        }
    }

    // 获取泛型数组
    public T[] getArr(){
        return arr;
    }

    // 方法泛型
    public <E> E sayHello(E e){
        return e;
    }
}

public class Test01 {
    public static void main(String[] args) {
        ArrayArg<String> stringArrayArg = new ArrayArg<>();
        stringArrayArg.setArr("Jack","Jerry","Tom","Alice");
        String[] arr = stringArrayArg.getArr();
        System.out.println(Arrays.toString(arr));
        String studying = stringArrayArg.sayHello("I am studying");
        System.out.println(studying);
    }
}

3.泛型的继承

public class ArrayArg<T extends Comparable> {
    private T[] arr;

    // 构造泛型数组1
    /*public void setArr(IntFunction<T[]> function,T... strs){
        arr = function.apply(strs.length);
        for (int i = 0; i < strs.length; i++) {
            arr[i] = strs[i];
        }
    }*/

    // 构建泛型数组2
    public void setArr(T... strs){
        arr = (T[]) Array.newInstance(strs.getClass().getComponentType(), strs.length);
        for (int i = 0; i < strs.length; i++) {
            arr[i] = strs[i];
        }
    }

    // 获取泛型数组
    public T[] getArr(){
        return arr;
    }

    // 方法泛型
    public <E> E sayHello(E e){
        return e;
    }
}

public class ArrayArg<T extends Comparable>表明Comparable是T的父类型。

三.使用泛型需要注意的事项以及解决方案

1.类型擦除

类型擦除的定义

无论何时定义一个泛型类型,编译器都会自动提供一个相应的原始类型。这个原始类型的名称就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型(或者,对于无限定的变量则替换为Object)

public class ArrayArg<T> // rawType:Object
public class ArrayArg<T extends Comparable> // rawType:Comparable

类型擦除的时间

编译阶段,编译器会将泛型类型擦除,使得泛型类型参数在运行时不可见。

类型擦除的优缺

优点:

1.向后兼容性:在运行时,处理的都是原始类型。

2.节省空间和性能:擦除后的类型都是原始类型,避免创建额外的类型对象的开销,同时减少了内存占用和运行处理时间。

缺点:

1.不能用基本类型实例化类型参数

image.png

image.png

解决:使用基本类型的包装类,作为泛型的参数。

2.运行时类型查询只适用于原始类型

ArrayArg<String> stringArrayArg = new ArrayArg<>();
ArrayArg<Integer> arrayArg = new ArrayArg<>();
System.out.println(arrayArg.getClass() == stringArrayArg.getClass()); // true

3.不能创建参数化类型的数组

image.png

4.不能实例化类型变量

最好的解决方式是让调用者传入一个构造器表达式。

public class ArrayArg<T extends Comparable> {

    private T obj;

    public T getObj() {
        return obj;
    }

    // 实例化泛型变量1
    public void setObj1(Supplier<T> constr) {
        this.obj = constr.get();
    }

    // 实例化泛型变量2
    public void setObj2(Class<T> cl){
        try {
            this.obj = cl.getConstructor().newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    
ArrayArg<String> stringArrayArg = new ArrayArg<>();
stringArrayArg.setObj2(String.class);
stringArrayArg.setObj1(String::new);
System.out.println(stringArrayArg.getObj().getClass()); // class java.lang.String

5.不能构造泛型数组

最好的解决方式是让调用者传入一个数组构造器表达式。

public class ArrayArg<T extends Comparable> {

    private T[] arr;

    // 构造泛型数组1
    public void setArr1(IntFunction<T[]> function,T... strs){
        arr = function.apply(strs.length);
        for (int i = 0; i < strs.length; i++) {
            arr[i] = strs[i];
        }
    }

    // 构建泛型数组2
    public void setArr2(T... strs){
        arr = (T[]) Array.newInstance(strs.getClass().getComponentType(), strs.length);
        for (int i = 0; i < strs.length; i++) {
            arr[i] = strs[i];
        }
    }

    // 获取泛型数组
    public T[] getArr(){
        return arr;
    }
    
}
stringArrayArg.setArr1(String[]::new,"Jack","Jerry","Tom","Alice");
stringArrayArg.setArr2("Jack","Jerry","Tom","Alice");
String[] arr = stringArrayArg.getArr();
System.out.println(Arrays.toString(arr)); // [Jack, Jerry, Tom, Alice]

注意这里的Supplier<T>IntFunction<T>的区别:

1.Supplier<T>是一个无参的函数式接口,它不接受任何参数,返回一个泛型类型T的结果。它通常用于延迟提供一个值,即每次调用get()方法时都会生成一个新的值。例如:

Supplier<String> supplier = () -> "Hello";
String result = supplier.get(); // 返回"Hello"

2.IntFunction<T>是一个接受一个int类型参数的函数式接口,返回一个泛型类型T的结果。它在输入为int类型时可用于生成特定类型的结果。例如:

IntFunction<String> intFunction = (int i) -> "Number: " + i;
String result = intFunction.apply(42); // 返回"Number: 42"

总结区别:

  • Supplier<T>接口不接受任何参数,只返回一个结果;IntFunction<T>接口接受一个int类型参数,然后返回一个结果。
  • Supplier<T>主要用于惰性生成值,可以用于重复调用以获得多个值;IntFunction<T>主要用于根据给定的int值生成特定类型的结果。
  • Supplier<T>适用于需要提供延迟计算的场景,而IntFunction<T>更适用于需要根据整数值生成结果的场景。