前言
泛型在 JavaSE 阶段是学习过的,但是毕竟处理定义一些简单的集合就很少用到它了,至于最近 Flink 中遇到的 泛型方法,更是感觉闻所未闻,以及源码中加在接口、方法、类前的各种 <T,V> 让我实在自觉羞愧,于是今天就来专门深入学习一下泛型。为了和前几篇文章对应,这里就叫 JDK1.5 新特性吧。
后续应该还会再去深入学习一些基础的东西,比如注解反射,不用它就学不会。
泛型
1、为什么要使用泛型
要回答这个问题,我们先看看现在我们是怎么使用泛型的。
/** * 泛型定义了这个 ArrayList 存储的类型都是 String 类型 */ ArrayList<String> list = new ArrayList<>(); list.add("name"); list.add(10); //编译报错
我们可以看到,当我们定义了一个泛型为 String 类型的 ArrayList 集合时,当我们往进添加一条 int 类型的数据就会报错,这是因为这里的泛型约束了我们的集合中的数据类型必须都是一致的 String 类型。
那么在 JDK1.5 之前,在没有引入泛型之前是怎么防止数据类型不一致的呢?
/** * 在 JDK1.5 之前是没有泛型的 * 所以当时的 集合 存储的都是 Object 类型 */ ArrayList list = new ArrayList(); list.add("hello"); list.add(10);
在 JDK1.5 之前,集合存储的都是 Object 类型的数据,所以不管是数值类型、String 还是别的引用类型,通通可以存进去而且不会报错。但是这也给我们后期处理数据造成了麻烦。
Iterator iterator = old_list.iterator(); while (iterator.hasNext()){ // 遍历到 10 会报错 java.lang.Integer cannot be cast to java.lang.String String next = (String) iterator.next(); System.out.println(next); }
对于上面集合中的数据,集合中存在多种数据类型,当我们使用迭代器或者循环遍历时,必然会因为取出数据的类型不一致造成报错,因为我们没法判断下一个数据的类型。
那么当时的集合是怎么进行使用的呢?
对于我们上面的集合(包含 String 和 int 两种数据类型),放在当时是这样的:
/** * 所以在 JDK1.5 之前 使用集合需要判断元素类型 */ Object object = iterator.next(); if (object instanceof String){ String next = (String) object; System.out.println(next); }else if (object instanceof Integer){ int next = (int) object; System.out.println(next); }
我们需要判断每个数据的类型,防止因为数据类型不一致而转换失败。很明显这种方式是极其复杂繁琐的。
2、泛型概述
所以为什么要使用泛型?因为泛型提供了编译时的类型安全检测机制,它可以帮助我们程序员在编译时就检测到非法的类型。
泛型的本质是参数化类型,也就是说操作的数据类型被指定为一个参数。
泛型是一种把数据类型的明确工作推迟到创建对象或者调用方法的时候才去明确的特殊类型。
注意:泛型参数只能是引用类型,不能是原始类型(比如 int、double、float、char)。
泛型可以使用在 方法、接口、类,分别称作:泛型类、泛型方法和泛型接口。
3、泛型类
3.1、基本格式
基本格式:修饰符 class 类名<类型>{}
public class Student<T>{}
注意:这里的 T 可以随便写,但是我们常用的是 T(Type)、E(Element Java集合框架中经常使用)、K(Key)、V(Value) 等参数类型来表示泛型。
3.1、泛型类的使用
我们先定义一个普通的 Student 类,只有一个 id 属性:
public class Student { public String id; // 学号 public String getId() { return id; } public void setId(String id) { this.id = id; } }
我们可以这样来得到它的属性。
Student<String> student = new Student<>(); student.setId("2023001"); System.out.println(student.getId());
但是,我们的学号可能是 String 类型,也可能是 int 类型,这该怎么办?难道把 id 设置为 Object 类型?设置为 Object 类型当然是可以的,但是当我们要对该学号进行一些处理的时候(比如学号+1 或者 学号拼接一个字符串)就有可能又涉及到强制转换了(Object -> String 或者 Object-> Integer)。
所以,这里我们这里就可以定义一个泛型类:
public class Student<T> { public T id; // 学号 public T getId() { return id; } public void setId(T id) { this.id = id; } }
Student<String> student = new Student<>(); student.setId("2023001"); System.out.println(student.getId()); // 注意:泛型只能是引用类型 Student<Integer> stu = new Student<>(); stu.setId(2023001); System.out.println(stu.getId()); Student<Double> st = new Student<>(); st.setId(12.0); System.out.println(st.getId());
可以看到,我们定义泛型类后,每次创建 Student 对象时,我们可以随意地指定它参数的类型。
注意:我们虽然指定了泛型,但是我们依然可以创建一个没有指定泛型的对象,只不过此时我们类的属性类型以及 getter 方法的返回值都变成了 Object,setter 方法的参数也变成了 Object,所以这当我们要取值的时候,又要进行一个类型的判断。所以当我们既然使用了泛型类,就一定要指定泛型参数!
4、泛型方法
4.1、基本格式
基本格式:修饰符 <泛型参数> 返回值类型 方法名 (类型 变量名){}
public <T> void show(T t){...}
4.2、泛型方法的使用
我们继续定义一个 Person 类,我们需要定义一个 show 方法,你给它传什么类型的参数,它就返回什么类型的参数。因为参数类型不同,所有这里我们需要多次重载该 show 方法:
public class Person { public Integer show(int grade){ return grade; } public String show(String grade){ return grade; } public Double show(double grade){ return grade; } }
但是很明显,这样代码的冗余度很高。所以我们可以先使用泛型类来进行优化:
public class Person<T> { public T show(T grade) { return grade; } }
这样明显我们类中的代码简洁了很多,但是我们在调用该方法的时候依然很复杂
Person<String> a = new Person<>(); String res1 = a.show("100"); Person<Integer> b = new Person<>(); int res2 = b.show(95); Person<Double> c = new Person<>(); double res3 = c.show(95.8);
这就需要我们定义一个泛型方法:
public class Person { public<T> T show(T grade) { return grade; } }
使用泛型方法:
Person person = new Person(); String res1 = student.show("s"); int res2 = student.show(90); double res3 = student.show(96.5);
很明显,使用泛型方法后,不管是类的定义还是方法的调用都是最简洁的。
5、泛型接口
5.1、基本格式
基本格式:修饰符 interface 接口名<类型>{...}
public interface MyInterface<T>{...}
5.2、泛型接口的使用
5.2.1、普通方法
我们定义一个接口,并定义一个抽象方法,要求它的所有实现类的方法参数类型和返回值类型都是统一的类型。
public interface MyInterface <T>{ T show(T t); }
注意:这里接口中的方法并不是泛型方法,所以它的 T 指的就是泛型接口中的 T。
public class Person implements MyInterface<String>{ @Override public String show(String grade) { return grade; } }
可以看到,使用泛型接口,就相当于约束了其实现类的一些实现要求(比如上面 这个泛型接口,就要求了它的实现类必须实现 show 方法,并且该方法的参数类型和返回值类型都与接口的泛型类型必须保持一致)。
5.2.2、泛型方法
那,如果我们要实现上面 4.2 中的效果,也就是说这个show方法传入什么类型的参数,返回什么类型的结果,这又该怎么实现呢?我们可以在接口中定义一个泛型方法:
public interface MyInterface <T>{ <M> M show(M m); }
注意:泛型接口中的泛型类型是用来约束其 实现类 或者 非泛型方法的参数类型和返回值类型(比如上面 5.1.1)或者泛型方法的参数类型或者返回值类型之一(因为如果一个泛型方法的返回值类型和参数类型都是接口的泛型类型那么这个泛型方法就没有意义了),而泛型方法中的泛型类型是用来约束方法的返回值类型和参数类型的。
public class Person<T> implements MyInterface<T>{ @Override public <M> M show(M m) { return m; } }
我们调用方法:
Person<String> person = new Person<>(); String res1 = person.show("100"); int res2 = person.show(96); double res3 = person.show(98.5);
可以看到我们这个Student类的泛型 String 是完全没有必要的,所以我们可以这样改造:
public interface MyInterface{ <M> M show(M m); }
public class Person implements MyInterface{ @Override public <M> M show(M m) { return m; } }
Person person = new Person(); String res1 = person.show("100"); int res2 = person.show(96); double res3 = person.show(98.5);
6、泛型类型通配符
- 类型通配符 <?> 一般用于接受使用,不能够做添加。
- List <?> 表示元素类型未知的 list ,它的元素可以匹配任何类型。
- 带通配符的 List 仅表示它是各种泛型 list 的父类,并不能把元素添加到其中(不能调用 add方法 )。
6.1、泛型类型通配符的使用
public class Test { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<>(); list1.add("tom"); list1.add("bob"); list1.add("mike"); ArrayList<Integer> list2 = new ArrayList<>(); list2.add(10); list2.add(18); list2.add(20); printList(list1); printList(list2); } /** * 不知道接受到的list会是什么类型 * @param list 不能调用 add */ public static void printList(List<?> list){ list.forEach(System.out::println); } }
6.2、泛型类型通配符的上限和下限
类型通配符的上限:<? extends A> 表示类型必须是 A 或者 A 的子类。
类型通配符的下限:<? super B> 表示类型必须是 B 或者 B 的父类。
7、泛型擦除机制
泛型只在编译阶段限制参数类型的传递,在运行阶段都会被擦除,也就是说在运行时我们的 .class 文件中是没有泛型的!
我们可以通过反编译工具将我们的 .class 文件反编译为 .java 文件,完成后,我们就会发现,我们所定义的泛型 <T>、<K>... 其实在编译后都变成了 Object 。