你是清华土木博士,毕业进了央企当经理;我是二本计科学士,毕业去了美团送外卖。你我都是共产主义接班人,我们都有光明的未来!
泛型是什么
泛型的本质是参数化类型。要掌握泛型,就必须了解到 Java 虚拟机对泛型一无所知。所有对泛型的各种操作,都是由编译器完成的。在本系列文章的第一篇,我们已经了解了多态这个概念,并且知道了所有的类都是 Object 的子类及其后代。泛型恰恰是(编译器)将模板类型全部视为 Object 类型,然后在获取模板类型变量时又强制转换为实际类型。
泛型的局限
由上一小节,我们知道了所有的模板类型都会被编译器编译成 Object 类型,所有模板类型修饰的变量在获取时的代码都会被加上强制(实际)类型转换。而我们知道,非基本类型包装类的引用类型是无法被强制转换为基本类型的!于是泛型就有了以下的局限:
- 模板类型不能是基本类型
- 不能实例化一个泛型数组
- 在代码中无法获知模板类型的真实类型
- 本类无法获取模板类型的真实类型,也就无法将模板类型实例化(编译器直接不允许)
- 不同实际类型的泛型,其实例化后调用
getClass(),所获取的类是同一个,因为实际类型都被替换成Object了
泛型的使用
-
泛型方法
- 返回值类型被泛型类型修饰 的方法即为泛型方法
- 定义格式:
修饰符 泛型标记符一 方法名(泛型标记符二或常规类型 参数名, ...)/** * 假设当前类名为 Service */ public T getObjectById(Integer id) { // 这一句看不懂没有关系,直接略过即可。未来的文章会详细介绍 Optional Optional<T> res = this.findById(id); if (res == null !! res.isPresent() != true) { return null; } return res.get(); } - 调用格式:
方法名(实参),只是实参可以不同的引用类型,或者不同的引用类型的数组Service service = new Service(); // 查询学生信息 Student object1 = getObjectById(1); // 查询老师信息 Teacher object2 = getObjectById(9);
-
泛型类(接口)
- 类(接口)的名字后接 <泛型类型> 即为泛型类(接口)
- 定义格式:
修饰符 类名<泛型标记符>public class Couple<T, K> { private T male; private K female; public Couple(T male, K female) { this.male = male; this.female = female; } } - 调用格式:
类名<实际类型名> 变量标记符 = new 类名<实际类型名>()/** * 假设当前已经引入了 Engineer 和 Teacher 等职业实体类 */ // 声明的部分已经确定了 T 和 K 的值,因此实例化的部分可省略 <> 中的值,由编译器推断获得 Couple<Engineer, Teacher> couple = new Couple<>(engineer, teacher);
标记符与通配符
由 泛型的局限 一节,我们已经知道泛型 T 在编译阶段会被编译成 Object。但在这个过程中,编译器还是会根据 T 的实际值来判断两个泛型是否可转换。所以以下代码是无法通过编译的:
class Count<T> {
private T t;
public Count(T t) {
this.t = t;
}
}
public class Application {
public static void main(String[] args) {
Count<Integer> j = new Count<>(3);
// 以下代码会报错,编译无法通过!
// java: 不兼容的类型: Count<java.lang.Integer>无法转换为 Count<java.lang.Number>
Count<Number> i = j;
}
}
可以看到,尽管 Integer 是 Number 的子类,但 Count<Integer> 不是 Count<Number> 的子类!要使 Count<Number> i = j; 生效,就必须使用上界通配符:<? extends Number>
class Count<T> {
private T t;
public Count(T t) {
this.t = t;
}
}
public class Application {
public static void main(String[] args) {
Count<Integer> j = new Count<>(3);
// 编译通过!
// 这下,变量 i 既能接受 Count<Number> 类型又能接受 Count<Integer> 类型了
Count<? extends Number> i = j;
}
}
接下来,我们将全面介绍泛型的标记符以及其通配符体系:
T- Type- 通常用于表示任何类
// 定义 class GenericClass<T> { private T sth; public GenericClass(T sth) { this.sth = sth; } public T getSth() { return sth; } } public class Application { public static void main(String[] args) { // 传入 String 作为特定的 T GenericClass<String> gg = new GenericClass<>("ego"); // 不传 T,相当于:GenericClass<Object> gg = new GenericClass<>(); GenericClass gg = new GenericClass(); } }
- 通常用于表示任何类
E- Element- 通常用于表示集合的元素或异常
// 定义 class CollectionClass<E> { private E[] arr; public CollectionClass(E[] arr) { this.arr = arr; } public E[] getArr() { return arr; } } public class Application { public static void main(String[] args) { CollectionClass<Integer> nc = new CollectionClass<>(new Integer[]{1, 3, 5, 7, 9}); CollectionClass<String> ns = new CollectionClass<>(new String[]{"1", "3", "5"}); } }
- 通常用于表示集合的元素或异常
K- Key- 通常用于表示键值对中的键
V- Value- 通常用于表示键值对中的值
// 定义 class MapClass<K, V> { private K key; private V val; public MapClass(K key, V val) { this.key = key; this.val = val; } public K getKey() { return key; } public V getVal() { return val; } } public class Application { public static void main(String[] args) { MapClass<Integer, String> mc = new MapClass<>(1, "ego"); Integer key = mc.getKey(); String val = mc.getVal(); } }
- 通常用于表示键值对中的值
?- Unknown- 又称无限定通配符,用于表示不确定的 Java 类型
- 不允许调用
set(T)方法并传入引用(null除外) - 不允许调用
T get()方法并获取T引用(只能获取Object引用) ClassName<?>是所有ClassName<T>的超类// mc 的方法既不能读,又不能写,只能做一些 null 判断工作这样子 boolean isNull(MapClass<?, ?> mc) { return mc.getKey() == null || mc.getVal() == null; }
? extends T- 上界通配符。即继承关系的上界
- 常用于修饰方法参数
- 以 T 使用 Number 为例,则可接受泛型类型为 Number 及其子类如 Integer
- 允许调用读方法
T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外) - 即被修饰的变量不可写
public class Collections { // 该方法的 <T> 的作用是为了确定参数 <? super T> 和 <? extends T> 中的 T 到底是什么,不能省略 public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (int i = 0; i < src.size(); i++) { // src 可以调用 getter 方法 T t = src.get(i); // dest 可以调用 setter 方法 dest.add(t); } } }
T extends Number- 常用于修饰 类 或 方法返回值类型
- Number 可替换为其他类型。当前使用的是 Number,可接受泛型类型为 Number 及其子类如 Integer
// 要求参数的实际类型必须实现了 Comparable 接口,并且 T 必须是 T 类型本身或 T 的任何父类 public static <T extends Comparable<T>> void max(T x, T y) { T max = x; if (y.compareTo(max) > 0 ){ max = y; } return max; }
T extends ArrayList & Runnable & Serializable- 常用于修饰 类 或 方法返回值类型
- 限定多个类型时用
&隔开 - 如果限定的类型是
class而不是interface,则class必须放在限定类表中的第一个(这里例子是ArrayList),且最多只能存在一个classclass Util { // 泛型方法,其中 T 必须是 ArrayList 的子类型,并且实现了 Runnable 和 Serializable 接口 public static <T extends ArrayList & Runnable & Serializable> void process(T list) { // 因为 T 是 Runnable 的子类型,我们可以调用 run 方法 list.run(); // 因为 T 是 ArrayList 的子类型,我们可以使用 ArrayList 的方法,例如 size() System.out.println("List size: " + list.size()); // 这里可以添加更多与 T 相关的逻辑 } } // 示例类,继承自 ArrayList 并实现了 Runnable 和 Serializable 接口 class MyArrayList<T> extends ArrayList<T> implements Runnable, Serializable { public void run() { System.out.println("Running with MyArrayList"); } } // 创建 MyArrayList 实例 MyArrayList<Integer> mal = new MyArrayList<>(); // 添加一些元素 mal.add(1); mal.add(2); // 调用泛型方法 Util.process(mal);
? super T- 下界通配符。即继承关系的下界
- 常用于修饰方法参数
- 以 T 使用 Integer 为例,则可接受泛型类型为 Integer 及其父类如 Number、Object
- 允许调用写方法
set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外) - 即被修饰的变量不可读
public class Collections { public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (int i = 0; i < src.size(); i++) { // 报错!dest 不可以调用 getter 方法 T t = dest.get(i); // 报错!src 不可以调用 setter 方法 src.add(t); } } }
小结
泛型对初学者是非常不友好的。一个字,绕。所以如果您作为项目开发负责人,所带的团队不是一个特别稳定的团队的话,对泛型的使用不要过于深入。