泛型赋予容器强大的编译时类型检查能力。很多人不喜欢静态编程,感觉像是法西斯主义。多年以后,他们会发现,他们痛恨的东西,反而恰恰保护了他们。
- List<String>
- Map<String, Object>
- Map<String, List<Object>>
1. 为什么需要泛型
泛型是后来才有的,Java 1.5 才引入了泛型。
数组是类型安全的,String[] 声明一个存储了 String 对象的数组。而像 List 则不行,可以添加任意类型的数据:
String[] array = new String[2];
array[0] = "ok";
array[1] = 1; // 报错
List list = new ArrayList();
list.add(new Object()); // ok
list.add(1); // ok
list.add(""); // ok
如何保证 List 类型安全或者约束?装饰器模式,如果需要的是 int 呢?复制改改:
pubic class StringList {
List list = new ArrayList();
void add(String s) { list.add(s); }
int size() { return list.size(); }
String get(int i) { return (String) list.get(i); }
}
// 使用
StringList stringList = new StringList();
引入泛型之后:
ArrayList<String> myList = new ArrayList<>();
image.png
泛型化的类,其类型声明在类上,通过传递参数 E,使得 ArrayList 变为“全新”的(编译期)类型。ArrayList<String> 和 ArrayList<Integer> 是完全“独立”的两个类。
new ArrayList<Integer>();
new ArrayList<String>();
不同一个类,因为不能互相赋值:
ArrayList<Integer> listt = new ArrayList<String>();
image.png
也是同一个类,因为运行时泛型会被擦除。
2. 泛型擦除和问题
2.1 泛型擦除
从没有泛型的世界进化到有泛型的世界(1.4 -> 1.5),Java 选择了向后兼容,而非 Python2 到 Python3 那样断裂,编译成字节码后会被擦除,分别从 IDEA 反编译后的以及 ASM ByteCode Viewer 插件中可以看出:
image.png
2.1 擦除带来的问题
- Java 的泛型是编译期的假泛型,在编译和开发期间约束源代码中的类型,而编译后则完全被擦除,泛型信息在运行期完全不保留。
- List<String> 并不是 List<Object> 的子类型(类比 String/Object、String[]/Object[])
下例中,任何需要父类的地方总是可以传递子类,需要一个动物,那么给你一只猫显然符合需求,符合氏里替换原则(里氏代换原则(Liskow-Substitution-Principle)定义:子类对象能够替换父类对象,而程序逻辑不变。 )
但为什么需要 ArrayList<Animal> 却不可以传入 ArrayList<Cat> ?
虽然 Cat 是 Animal 的子类型,但泛型信息编译后被擦除,Cat 无法转换为 Animal,所以 ArrayList<Cat> 并不是 ArrayList<Animal> 的子类型。
import java.util.ArrayList;
public class Main {
public static void testObject(Animal animal) {
}
public static void testArray(Animal[] animal) {
}
public static void testList(ArrayList<Animal> animal) {
}
public static void main(String[] args) {
testObject(new Cat()); // ok
testArray(new Cat[2]); // ok
testList(new ArrayList<Cat>()); // 编译器检查报错
}
}
- 泛型只是编译期的警告,运行期类型不安全【使用限定符如 List<?>,也可以利用泛型擦除来绕过编译器检查】
ArrayList<Animal> list = new ArrayList<>();
ArrayList rawList = list; // 重新声明为“裸”的 ArrayList
rawList.add("ojbk"); // 可以添加任何类型的数据
// 因为对象是引用的,绕过了编译期检查,运行时类型信息被擦除,不影响正常运行
// 最终“正常”输出 ["ojbk"]
System.out.println(list);
- 数组运行期是安全的【即使绕过了编译期检查】
testArraySafety 方法传入 Object[] 的子类型 String[] 没毛病,而 testArraySafety 内部,int 666 被自动装箱成 Integer,符合参数 Object[] 的要求,也是其子类型,单独来看,两部分的操作都没毛病,编译期检查也没毛病。
结果运行时报错:Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
就是因为数组中即使绕过了编译期,在运行期也能发现端倪。
public static void testArraySafety(Object[] array) {
array[0] = 666;
}
public static void main(String[] args) {
String[] stringArray = new String[2];
testArraySafety(stringArray);
}
3. 泛型的限定符和绑定
- 定义泛型方法:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。
- 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
- 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
- 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。
- 限定符:
- 类型通配符 ? 代替具体的类型参数
- ? extends 要求泛型是某种类型及其子类型
- ? super 要求泛型是某种类型及其父类型
- Collections.sort 方法中的泛型
- 绑定:
- 按照参数绑定
public static <A extends Comparable<A>> A max(A a, A b) {
return a.compareTo(b) >= 0 ? a : b;
}
- 按照返回值自动绑定
public static <T> T cast(Object obj) {
return (T) obj;
}
4. 使用泛型的原则
- 不要一开始就使用泛型,而是慢慢重构。
- 编写泛型方法
- 编写泛型类
5. 实战
5.1 将方法泛型化
public class Main {
// 这里有四个结构、功能非常相似的方法,请尝试将其泛型化,以简化代码
// 泛型化之后的方法签名应该如下所示:
// public static boolean inAscOrder(T a, T b, T c)
public static boolean inAscOrder1(int a, int b, int c) {
return a <= b && b <= c;
}
public static boolean inAscOrder2(long a, long b, long c) {
return a <= b && b <= c;
}
public static boolean inAscOrder3(double a, double b, double c) {
return a <= b && b <= c;
}
public static void main(String[] args) {
System.out.println(inAscOrder1(1, 2, 3));
System.out.println(inAscOrder2(1L, 2L, 3L));
System.out.println(inAscOrder3(1d, 2d, 3d));
}
public static <T extends Comparable<T>> boolean inAscOrder(T a, T b, T c) {
return a.compareTo(b) <= 0 && b.compareTo(c) <= 0;
}
}
5.2 泛型化的二叉树
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class Main {
static class IntBinaryTreeNode {
int value;
IntBinaryTreeNode left;
IntBinaryTreeNode right;
}
static class StringBinaryTreeNode {
String value;
StringBinaryTreeNode left;
StringBinaryTreeNode right;
}
static class DoubleBinaryTreeNode {
double value;
DoubleBinaryTreeNode left;
DoubleBinaryTreeNode right;
}
// 你看,上面三种"二叉树节点"结构相似,内容重复,请将其泛型化,以节省代码
static class BinaryTreeNode<T> {
T value;
BinaryTreeNode<T> left;
BinaryTreeNode<T> right;
}
// 泛型化之后,请再编写一个算法,对二叉树进行中序遍历,返回中序遍历的结果
public static <T> List<T> inorderTraversal(BinaryTreeNode<T> root) {
List<T> list = new ArrayList<>();
inorderRec(root, list::add);
return list;
}
/**
* 辅助中序遍历的方法,以递归方式调用
*
* @param node 二叉树节点
* @param consumer 消费当前二叉树节点存储的数据
* @param <T> 二叉树中实际存储的数据类型
*/
public static <T> void inorderRec(BinaryTreeNode<T> node, Consumer<T> consumer) {
if (node != null) {
inorderRec(node.left, consumer);
consumer.accept(node.value);
inorderRec(node.right, consumer);
}
}
}
// 测试用例
import java.util.Arrays;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class MainTest {
Main.BinaryTreeNode<Integer> node1 = new Main.BinaryTreeNode<>();
Main.BinaryTreeNode<Integer> node2 = new Main.BinaryTreeNode<>();
Main.BinaryTreeNode<Integer> node3 = new Main.BinaryTreeNode<>();
Main.BinaryTreeNode<Integer> node4 = new Main.BinaryTreeNode<>();
Main.BinaryTreeNode<Integer> node5 = new Main.BinaryTreeNode<>();
Main.BinaryTreeNode<Integer> node6 = new Main.BinaryTreeNode<>();
{
node1.value = 1;
node2.value = 2;
node3.value = 3;
node4.value = 4;
node5.value = 5;
node6.value = 6;
node1.left = node2;
node1.right = node3;
node2.left = node4;
node2.right = node5;
node3.right = node6;
}
@Test
public void test() {
Assertions.assertEquals(Arrays.asList(4, 2, 5, 1, 3, 6), Main.inorderTraversal(node1));
}
}
网友评论