一.泛型的由来
场景一:当定义一个数组后,数组所能容纳的类型就确定下来了,不能向数组添加其他类型的元素。
由于数组无法动态的扩容,所以在项目中经常使用集合来存储多个元素。
场景二:当定义一个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.不能用基本类型实例化类型参数
解决:使用基本类型的包装类,作为泛型的参数。
2.运行时类型查询只适用于原始类型
ArrayArg<String> stringArrayArg = new ArrayArg<>();
ArrayArg<Integer> arrayArg = new ArrayArg<>();
System.out.println(arrayArg.getClass() == stringArrayArg.getClass()); // true
3.不能创建参数化类型的数组
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>更适用于需要根据整数值生成结果的场景。