泛型:参数化类型,把类型当做一个参数传递到类中,这样做可以增加代码的灵活性。java从1.5开始提供了泛型
泛型的作用:
泛型的特点:
泛型可以使用在类、接口、方法上,它们分别称作:泛型类、泛型接口、泛型方法。
泛型标识:常用的有T、E、K、V,这些标识 是编码时的一种约定,
使用泛型的类,泛型类可以持有任意类型的属性,这增加了类的灵活性。
声明:[public] class 类名称<泛型标识[, ...]>{ // 类体 }
实例化:类名称<泛型类型> 对象名 = new 类名称<[泛型类型]>([实参列表]);
。在实例化泛型类时,需要指明泛型类中的类型参数。Java1.7之后,后面的尖括号中的具体的数据类型可以省略不写。
泛型类中使用泛型:
不允许创建泛型数组:数组和泛型的运行机制不兼容,数组在运行时元素的类型信息是明确的,而泛型存在类型擦除,在运行时都是Object类型,这和数组的运行机制是相冲突的。
案例1:一个使用泛型的容器类
import java.util.Arrays;
public class Box<T> {
// 使用Object作为数组的类型,因为数组和泛型的运行机制不兼容
private Object[] elements = new Object[4];
private int size;
public T get(int i) {
return (T) elements[i]; // 告警信息:Unchecked cast: 'java.lang.Object' to 'T'
}
public void add(T ele) {
if (size >= elements.length) {
// 扩容
resize();
}
elements[size++] = ele;
}
public int size() {
return size;
}
private void resize() {
int len = elements.length;
Object[] newElements = new Object[len + 8];
System.arraycopy(elements, 0, newElements, 0, elements.length);
elements = newElements;
}
@Override
public String toString() {
return "Box{" +
"elements=" + Arrays.toString(elements) +
'}';
}
}
public class BoxTest {
// 测试Box类
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.add("aaa");
stringBox.add("bbb");
stringBox.add("ccc");
stringBox.add("ddd");
stringBox.add("eee");
stringBox.add("fff");
System.out.println(stringBox.get(1)); // bbb
System.out.println("stringBox = " + stringBox); // Box{elements=[aaa, bbb, ccc, ddd, eee, fff, ...
System.out.println("stringBox.size() = " + stringBox.size()); // 6
}
}
案例2:泛型类中的泛型不可以和静态属性一起使用
public class Box<T> {
// 编译时会报错:'Box.this' cannot be referenced from a static context,
// 因为泛型是传递给实例的,需要通过一个实例来访问,在静态上下文中无法访问
public static T boxEle;
}
案例3:泛型类中的泛型不可以被实例化
public class Box<T> {
// 编译时报错:Type parameter 'T' cannot be instantiated directly,泛型参数T不可以被直接实例化,
// 因为不清楚Box实例化时传入的泛型实参是否可以被实例化
private T e = new T();
private T[] elements = new T[4];
}
继承规则:
案例1:子类是泛型类
public class SpecialBox<E, F, G> extends Box<E> { } // 子类和父类的泛型类型要一致,注意,子类的泛型名称,T、E之类的,可以和父类不一致,但是子类中的声明必须一致,例如,在当前案例中,泛型E将会被传递给父类
案例2:子类不是泛型类
public class SpecificBox extends Box<Integer> { } // 子类不是泛型类,继承时需要明确父类的泛型类型
声明:[public] interface 接口名<泛型标识 [, ...]>{ // interface body }
泛型接口的实现和泛型类的继承是一样的。
案例:以java中的List接口为例,写两个实现类,一个是泛型类、一个不是泛型类
// 1. List接口的声明
public interface List<E> extends Collection<E> { }
// 2. 实现类是泛型类,实现类中,实现类和接口的泛型类型要一致
public class MyList2<T> implements List<T> {
// 实现接口中的方法时,使用了泛型
@Override
public List<T> subList(int fromIndex, int toIndex) {
return Collections.emptyList();
}
}
// 3. 实现类不是泛型类,接口的泛型要明确数据类型
public class MyList implements List<Integer> {
// 实现接口中的方法时,使用具体类
@Override
public ListIterator<Integer> listIterator(int index) {
return null;
}
}
方法支持泛型。在调用泛型方法的时候指明泛型的具体类型,它可以不依附于泛型类而存在,方法可以是成员方法也可以是静态方法。
声明:[访问权限修饰符] [static] [final] <泛型参数列表> 返回值类型 方法名(形参列表)
调用方式:和调用普通方法一样,通过实参的类型来指定泛型的类型。泛型类是在实例化类的时候指明泛型的具体类型。泛型方法是在调用方法的时候指明泛型的具体类型
案例:静态泛型方法和成员泛型方法
import java.util.Random;
public class GenericTest {
public static void main(String[] args) {
// 测试静态泛型方法
genericMethod(new Object(), "aa", new Random());
// 测试成员泛型方法
new GenericTest().genericMethod2(new Object(), "bb", new Random());
}
// 静态泛型方法
public static <T, E, K> void genericMethod(T t, E e, K k) {
System.out.println("t = " + t);
System.out.println("e = " + e);
System.out.println("k = " + k);
}
// 成员泛型方法
public <T, E, K> void genericMethod2(T t, E e, K k) {
System.out.println("t = " + t);
System.out.println("e = " + e);
System.out.println("k = " + k);
}
}
无界通配符用一个问号表示,表示任意类型,它通常用于方法参数,表示允许方法接收任意类型的泛型实例。无界通配符和Object不一样,无界通配符表示任意类型,Object是一个具体的类型,只能说无界通配符在运行时都是按照Object来处理的,但是普通的泛型也是这样。
案例:
import java.util.ArrayList;
import java.util.List;
public class GenericTest3 {
// 测试
public static void main(String[] args) throws ClassNotFoundException {
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
printList(list); // list.toString() = [aaa, bbb, ccc]
List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
list2.add(3);
printList(list2); // list.toString() = [1, 2, 3]
}
// 无界通配符,接收任意类型的list,如List、List
public static void printList(List<?> list) {
System.out.println("list.toString() = " + list.toString());
}
}
案例2:
// 这里使用一个无界通配符,因为此时编译器并不知道Class的具体类型
Class<?> strClass = Class.forName("java.lang.String");
System.out.println("strClass.getSimpleName() = " + strClass.getSimpleName()); // String
// Class的泛型实参是String,这在编译时就确定了
Class<String> stringClass = String.class;
System.out.println("stringClass.getSimpleName() = " + stringClass.getSimpleName()); // String
上界和下界:
上界:格式:? extends 实参类型,要求该泛型的类型,只能是实参类型或实参类型的子类。用于读取数据时,确保数据的类型范围。这种通配符允许用户从泛型集合中读取数据,但不允许用户向集合中添加数据,因为编译器无法确定具体的类型。
下界:格式:? super 实参类型,要求该泛型的类型,只能是实参类型或实参类型的父类。用于写入数据时,确保容器的类型范围。
泛型的上界和下界更加细致的限定了类型
案例1:上界
public static void processList(List<? extends Son> list) {
Son son = list.get(0);
// 错误:添加元素时报类型不匹配,要求的元素是 capture of ? extends Son,实际提供的元素Son。
// 泛型的上界,? extends Son,是一个不确定的类型,只知道它是Son的子类,编译器无法确定list的具体类型,
// 因此不允许用户向其中添加 Son 类型的对象。
// list.add(new Son());
}
案例2:下界
// 下界
public static void processList2(List<? super Son> list) {
Object o = list.get(0); // 正确:可以从集合中读取数组,但是无法获取具体的数据类型
list.add(new Son()); // 正确:可以向集合中添加数据,只要它是Son类即可
list.add(new Father()); // 错误,编译器只允许添加Son类型,不能添加Son的父类,因为集合中如果有两种类型的数据,会出现类型安全问题
}
为什么上界不允许向集合中添加数据,但是下界却可以,例如,
在这个案例中,下界可以确保元素一定是Son类型或它的父类,添加Son类型的元素,不会出现类型安全问题,但是添加Son的父类,如果集合是Son类型,就会出现类型安全问题。上界可以确保元素是Son或它的子类,编译器不清楚具体是哪个子类,所以无法添加元素。
案例3:泛型的上界,正确的使用方式
import java.util.ArrayList;
import java.util.List;
public class GenericTest4 {
// 测试
public static void main(String[] args) {
List<Integer> list2 = new ArrayList<>();
list2.add(1);
list2.add(2);
list2.add(3);
printNumbers(list2); // 1 2 3
}
// 泛型的上界,此时这里只能处理Number类型和其子类,集合中只能读取,不能写入
public static void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.print(number);
System.out.print(' ');
}
}
}
案例4:泛型的下界,此时集合可以被写入
import java.util.ArrayList;
import java.util.List;
public class GenericTest5 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
addNumbers(list);
System.out.println("list = " + list); // [1, 2, 3]
List<Number> list2 = new ArrayList<>();
addNumbers(list2);
System.out.println("list2 = " + list2); // [1, 2, 3]
List<Object> list3 = new ArrayList<>();
addNumbers(list3);
System.out.println("list3 = " + list3); // [1, 2, 3]
}
// 泛型的下界,此时,方法可以接收Integer和它的父类作为泛型实参
public static void addNumbers(List<? super Integer> list) {
list.add(1); // 只能向集合中写入Integer类型的数据
list.add(2);
list.add(3);
}
}
案例5:上界和下界一起使用,实现集合复制功能
import java.util.ArrayList;
import java.util.List;
public class GenericTest6 {
public static void main(String[] args) {
List<Integer> sourceList = new ArrayList<>();
sourceList.add(1);
sourceList.add(2);
sourceList.add(3);
List<Object> targetList = new ArrayList<>();
copyList(sourceList, targetList);
System.out.println("targetList = " + targetList); // [1, 2, 3]
}
// 集合复制,原集合的泛型可以是指定类型和它的子类,目标集合的泛型是指定类型和它的父类
public static <T> void copyList(List<? extends T> sourceList, List<? super T> targetList) {
for (T ele : sourceList) {
targetList.add(ele);
}
}
}
使用 & 符合可以对泛型限定多重边界,如
为什么不支持限定多重下界:因为这会违反单继承的原则。案例,? super Integer,在这个泛型下界中,用户可以向集合中写入Integer类型的数据,但是在 ? super Integer & String 中,编译器无法确定改向集合中写入什么类型的数据,因为集合只能接受一种类型的数据
案例:数字比较
import java.util.ArrayList;
import java.util.List;
public class GenericTest7 {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(4);
list.add(3);
Integer i = maxNum(list.toArray(new Integer[0]));
System.out.println("i = " + i); // 4
}
// 限定多重上界
public static <T extends Number & Comparable<T>> T maxNum(T ... arr) {
T max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max.compareTo(arr[i]) < 0) {
max = arr[i];
}
}
return max;
}
}
泛型信息只存在于代码编译阶段,在运行之前,与泛型相关的信息将会被擦除
类型擦除的分类:
数组是Java中的原生数据类型,它在运行时依然保留了元素的类型信息,而泛型信息在运行之前会被擦除,这和数组的特点是冲突的,所以泛型和数组不可以一起使用。
案例:不可创建泛型数组,如果一个类带有泛型信息,以它为数据类型的数组该如何创建?
// 错误的做法:下面这行代码会报错:Generic array creation not allowed,不允许创建泛型数组
// 因为不允许在创建数组的同时声明泛型类的实参类型
// Class[] sClassARR = new Class[10];
// 正确的做法:创建数组时不带泛型信息,然后进行类型转换,然后阻止编译器抛异常
@SuppressWarnings("unchecked")
Class<String>[] sClassArr = (Class<String>[]) new Class[10];
// 另一种写法,使用泛型通配符,Java允许在数组创建时使用通配符
public static void main(String[] args) {
Node<?, ?>[] nodes = new Node<?, ?>[10];
}
private static class Node<K, V> { }
因为泛型实参是在类实例化时被指定的,静态属性的生命周期在类实例化之前,所以静态属性无法获取到泛型实参,也就无法使用泛型了
成员内部类会继承外部类的泛型信息,静态内部类则不会,因为成员内部类依赖外部类的实例,但是静态内部类不依赖外部类的实例。
案例:成员内部类,如果继承了外部类的泛型信息,在外部类中,不允许创建内部类类型的数组
public class HashTable<K, V> {
private Node[] table;
public HashTable() {
// 这行代码会报错,Generic array creation not allowed,不允许创建泛型数组,因为内部类带有泛型信息,
// 而且泛型信息是继承自外部类的,成员内部类会受外部类的影响
// table = new Node[10];
}
// 继承了外部类中的泛型信息
private class Node {
private K key;
private V value;
private int hash;
private Node next;
public Node() { }
public Node(K key, V value, int hash, Node next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
}
}
案例:静态内部类
public class HashTable2<K, V> {
private Node<K, V>[] table;
@SuppressWarnings("unchecked")
public HashTable2() {
// 正确的做法,静态内部类带有泛型信息,但是泛型信息是自己声明的,实例化数组时不加泛型信息,随后进行类型强转
table = (Node<K, V>[]) new Node[10];
}
// 没有继承外部类的泛型信息
private static class Node<K, V> {
private K key;
private V value;
private int hash;
private Node next;
public Node() { }
public Node(K key, V value, int hash, Node<K, V> next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
}
}
案例:
public class GenericClass<T> {
public void doSomething() {
// 试图获取泛型参数的类对象
// Class clazz = T.class; // 编译错误: 泛型类型'T'的类型未知。
}
}
结果:编译器会报错,因为 T 的实际类型在运行时是未知的。当编译器看到 T.class 时,由于类型擦除,它实际上看不到任何具体类型,从而无法解析 T。
原因:在 Java 中,泛型类或方法不能直接访问其泛型参数的类对象(即 Class
)。这是因为 Java 泛型在编译时会进行类型擦除,即在字节码中不保留任何关于泛型类型参数的信息。这使得在运行时无法知道泛型参数的实际类型。
类型擦除的目的是为了与非泛型代码保持兼容。例如,对于 List
,编译器会将泛型参数 T 擦除为 Object,所以在运行时,所有的 List
、List
等都会被视为 List
。