“泛型”(generics)作为Java特性之一,相信大家也耳熟能详了,就算没听说过,也肯定看过或偶然间使用过泛型,在这里,我们一起重新温习一下泛型,通过一些案例引发些新的思考。Java中的泛型作为V1.5后新增的特性,在JDK源码、中间件源码中有大量的使用,如果掌握了泛型将更容易理解源码,也提升代码抽象能力,封装通用性更强的组件。
什么是泛型
泛型是在定义类、接口和方法时,可以在声明时通过一定的格式指定其参数类型,在使用时再指定具体的类型,从而使得类、接口和方法可以被多种类型的数据所实例化或调用,这种可以在编译时进行参数类型检查的技术被称为泛型,是 JDK 5 中引入的一个新特性。本质是参数化类型,给类型指定一个参数,在使用时再指定此参数具体的值,那这个类型就可以在使用时决定。
其优点在于把运行时的错误,提前到编译时,这样就可以在编译时把错误提示出来,避免了运行时出现错误,使得在使用泛型可以提高代码的复用性,因为它可以支持多种类型的数据。
为什么要用
在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果插入了错误的类型对象,在运行时的转换处理就会出错。集合容器里面如果没指定类型,默认都是Object类型,那什么都可以插入。泛型减少了源代码中的强制类型转换,代码更加可读。
泛型的分类
它可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。泛型字母通常类型参数都使用大写的单个字母,例如:
T:任意类型 type
E:集合中元素的类型 element
K:key-value形式 key
V: key-value形式 value
泛型类
public class 类名 <泛型类型1,...> {
}
泛型接口
public interface 接口名称 <泛型类型1,...> {
泛型类型 方法名();
....
}
泛型方法
public 修饰符 <T,E,...> 返回值类型 方法名( ){
....
}
泛型类
泛型类型必须是引用类型,即类类型(不能使用基本数据类型),在类名后添加一对尖括号,并在尖括号中填写类型参数,如果参数可以有多个,多个参数使用逗号分隔。
定义
public class 类名 <泛型类型,...> {
private 泛型类型 变量名
public 泛型类型 方法名(){ }
public 返回值 方法名(泛型类型 t){ }
....
}
使用(JDK1.7后,结尾的具体类型不用写 )
类名<具体数据类型> 对象名 = new 类名< >();
泛型类创建的使用没有指定类型,则默认是object类型,泛型类型从逻辑上看是多个类型,实际都是相同类型,Java 可以创建对应的泛型对象和泛型数组引用,但不能直接创建泛型对象和泛型数组。
因为Java 有类型擦除,任何泛型类型在擦除之后就变成了 Object 类型,因此创建泛型对象就相当于创建了一个 Object 类型的对象,所以直接创建泛型对象和泛型数组也的行为被编译器禁止
public class CustomArrayStack<T> {
private Object[] arr;
private int top;
public CustomArrayStack(int capacity) {
arr = new Object[capacity];
top = -1;
}
// 入栈
public void push(T value) {
if (top == arr.length - 1) {
throw new RuntimeException("栈已满,无法入栈");
}
arr[++top] = value;
}
// 出栈
public T pop() {
if (top == -1) {
throw new RuntimeException("栈为空,无法出栈");
}
return (T)arr[top--];
}
// 查看栈顶元素
public T peek() {
if (top == -1) {
throw new RuntimeException("栈为空,无法查看栈顶元素");
}
return (T)arr[top];
}
public static void main(String[] args) {
CustomArrayStack<String> stringStack = new CustomArrayStack(2);
stringStack.push("hello");
stringStack.push("springboot");
String stringValue = stringStack.pop();
System.out.println(stringValue);
CustomArrayStack<Integer> integerStack = new CustomArrayStack(2);
integerStack.push(12);
integerStack.push(88);
Integer integerValue = integerStack.pop();
System.out.println(integerValue);
System.out.println(stringStack.getClass());
System.out.println(integerStack.getClass());
}
}
泛型派生类
如果泛型类的子类也是泛型类,那父类和子类的类型要一致,如果子类泛型有多个,那需要包括父类的泛型类型。
class Child <T,E,F> extends Parent<T> {
....
}
//定义父类
public class Parent <T>{
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
//父子类需要同个类型
public class Child<T> extends Parent<T> {
@Override
public T getValue() {
return super.getValue();
}
}
//没指定则是默认Object
public class Child extends Parent {
@Override
public Object getValue() {
return super.getValue();
}
}
//如果子类不是泛型类,那父类要明确泛型类型
public class Child extends Parent<String> {
....
}
泛型接口
规则和泛型类一样,如果实现类是泛型类,那接口和实现类的泛型类型要一致;如果实现类泛型有多个,那需要包括接口的泛型类型。如果实现类不是泛型类,那接口要明确泛型类的类型
interface 接口名称 <泛型类型1,...> {
泛型类型 方法名();
....
}
泛型方法
调用方法的时候指定泛型的具体类型
修饰符 <T,E,...> 返回值类型 方法名( 参数列表 ){
方法体
....
}
修复符和返回值中间的有<T,E ...> 才是泛型方法 泛型类里面的普通返回值类型不是泛型方法
泛型类的类型和泛型方法的类型是互相独立的,同名也不影响,声明了【泛型方法】在参数列表和方法体里面才可以用对应的泛型。
public class CustomArrayStack<T> {
public <E> E getRandomElement(List<E> list){
Random random = new Random();
return list.get(random.nextInt(list.size()));
}
}
public class CustomArrayStack<T> {
public <T> T getRandomElement(List<T> list){
Random random = new Random();
return list.get(random.nextInt(list.size()));
}
}
//上述两种方式定义泛型方法都可以
List<String> list = Arrays.asList("springcloud","springboot","alibabacloud");
CustomArrayStack<Integer> integerStack = new CustomArrayStack(2);
String randomElement = integerStack.getRandomElement(list);
System.out.println(randomElement);
使用了类泛型的成员方法,不能定义为静态方法;使用了泛型方法的才可以定义为静态方法
可变参数的泛型方法
/**
* 支持多个泛型类型的,可变参数的泛型方法
* @param f
* @param arr
* @param <E>
* @param <F>
*/
public static <E,F,K> void print(F f,K k,E...arr){
System.out.println(f.getClass());
System.out.println(k.getClass());
for(E e:arr){
System.out.println(e);
}
}
//验证
print(1,"hello word",1,2,3,4,5,6);
泛型通配符
Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法
//表示类型参数可以是任何类型
public class CustomCollection<?>{}
//表示类型参数必须是A或者是A的子类
public class CustomCollection<T extends A>{}
//表示类型参数必须是A或者是A的超类型
public class CustomCollection<T supers A>{}
分类
通配符
通用类型通配符 < ? >,如List < ? >,主要作用就是让泛型能够接受未知类型的数据,可以把 ?看成所有泛型类型的父类,是一种真实的类型,类型通配符是实参,不是形参。
public class NumberCollection<T> {
private T value;
public NumberCollection(T t){
this.value = t;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
public static void main(String[] args) {
NumberCollection<Integer> integerNumberCollection = new NumberCollection<>(1);
NumberCollection<Long> longNumberCollection = new NumberCollection<>(23L);
printInteger(integerNumberCollection);
printLong(longNumberCollection);
//通用性更强
print(integerNumberCollection);
print(longNumberCollection);
}
//需要写个long类型的
public static void printLong(NumberCollection<Long> collection){
Number number = collection.getValue();
System.out.println(number);
}
//需要写个integer类型的
public static void printInteger(NumberCollection<Integer> collection){
Number number = collection.getValue();
System.out.println(number);
}
//使用泛型通配符,复用性更强
public static void print(NumberCollection<?> collection){
Object collectionValue = collection.getValue();
System.out.println(collectionValue);
}
}
通配符的上边界
固定上边界的通配符 采用<? extends E>的形式,使用固定上边界的通配符的泛型, 只能够接受指定类及其子类类型的数据。采用<? extends E>的形式, 这里的E就是该泛型的上边界
这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类
//使用泛型通配符,什么类型都适合,里面具体类型需要用到Object
public static void print(NumberCollection<?> collection){
Object collectionValue = collection.getValue();
System.out.println(collectionValue);
}
//使用泛型通配符, 固定上边界的通配符,不能任意元素,只能是Number的子类
public static void printUp(NumberCollection<? extends Number> collection){
Number collectionValue = collection.getValue();
System.out.println(collectionValue);
}
//字符串类型,测试报错
NumberCollection<String> stringNumberCollection = new NumberCollection<>("springboot");
printUp(stringNumberCollection);
通配符的下边界
固定下边界的通配符,采用<? super E>的形式,使用固定下边界的通配符的泛型, 只能够接受指定类及其父类类型的数据。采用<? super E>的形式, 这里的E就是该泛型的下边界,可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界
//只能是integer或者integer的父类
public static void printDown(NumberCollection<? super Integer> collection){
Object object = collection.getValue();
System.out.println(object);
}
//测试
NumberCollection<Long> longNumberCollection = new NumberCollection<>(23L);
//报错,类型不支持,需要ineter或其父类
// printDown(longNumberCollection);
NumberCollection<Integer> integerNumberCollection = new NumberCollection<>(1);
printDown(integerNumberCollection);
NumberCollection<Number> numberCollection = new NumberCollection<>(55L);
printDown(numberCollection);
泛型类型擦除
泛型是JDK1.5后出现的,但泛型代码和常规版本代码可以兼容,主要原因是泛型是在代码编译阶段,代码编译完成后进入JVM运行前,相关的泛型类型信息会被删除,这个即泛型类型擦除。
对于类泛型、接口泛型、方法泛型都有影响。
无限制类型擦除,擦除后都是Object类型
//没指定类型则擦除后是Object最顶级父类
public class Generic<T,K>{
private T age;
private K name;
public static void main(String[] args) {
Generic generic = new Generic<Ingeger,String>();
//反射获取字节码文件class对象
Class<? extends Generic> aClass = generic.getClass();
//获取所有成员变量
Field[] declaredFields = aClass.getDeclaredFields();
for(Field field : declaredFields){
//获取每个属性名称和类型
System.out.println(field.getName() +",类型="+field.getType().getSimpleName());
}
}
}
//打印输出
age,类型=Object
name,类型=Object
有限制类型擦除
//T 需要是 Number的子类,所以擦除后就是Number类型,没指定类型则擦除后是Object最顶级父类
public class Generic<T extends Number ,K >{
private T age;
private K name;
public static void main(String[] args) {
Generic<Integer,String> generic = new Generic<>();
//反射获取字节码文件class对象
Class<? extends Generic> aClass = generic.getClass();
//获取所有成员变量
Field[] declaredFields = aClass.getDeclaredFields();
for(Field field : declaredFields){
//获取每个属性名称和类型
System.out.println(field.getName() +",类型="+field.getType().getSimpleName());
}
}
}
//打印输出
age,类型=Number
name,类型=Object
泛型应用
如何创建泛型数组?
在 Java 中是不能直接创建泛型对象和泛型数组的,主要原因是 Java 有类型擦除,任何泛型类型在擦除之后就变成了 Object 类型或者对应的上限类型,那定义的类中如果需要用到泛型数组,如何解决这个问题?
方法一:可以通过反射下的 Array.newInstance 创建泛型数组,告知指定的类型字节码即可,使得可以创建实际类型的数组。
public class GenericsArray<T> {
private T[] array;
public GenericsArray(Class<T> clz , int capacity) {
array = (T[]) Array.newInstance(clz,capacity);
}
public T[] getAll() {
return array;
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return (T)array[index];
}
public static void main(String[] args) {
GenericsArray<String> genericsArray = new GenericsArray(String.class,3);
genericsArray.put(0,"springcloud");
genericsArray.put(1,"springboot");
String value = genericsArray.get(0);
System.out.println(value);
//下面代代码运行不报错,虽然有泛型的擦除,但在构造器中传递了类型标记Class,从擦除中恢复,使得可以创建实际类型的数组
String[] all = genericsArray.getAll();
System.out.println(all);
}
}
方法二:直接使用创建Object类型的数组,然后获取的时候,根据类型返回进行强转即可
public class GenricsArray<T> {
private Object[] arr;
public GenricsArray(int size) {
arr = new Object[size];
}
public void put(int index, T item) {
arr[index] = item;
}
public T get(int index) {
return (T) arr[index];
}
public T[] getArr() {
return (T[]) arr;
}
public static void main(String[] args) {
GenricsArray<String> ga = new GenricsArray<>(3);
ga.put(0, "Hello");
ga.put(1, "World");
ga.put(2, "!");
// 自动类型转换
String[] strArr = ga.getArr();
for (String str : strArr) {
System.out.println(str);
}}
}