泛型赋予容器强大的编译时类型检查能力。很多人不喜欢静态编程,感觉像是法西斯主义。多年以后,他们会发现,他们痛恨的东西,反而恰恰保护了他们。

  • List
  • Map
  • Map>

1. 为什么需要泛型

泛型是后来才有的,Java 1.5 才引入了泛型。

数组是类型安全的,String[] 声明一个存储了 String 对象的数组。而像 List 则不行,可以添加任意类型的数据:

  1. String[] array = new String[2];
  2. array[0] = "ok";
  3. array[1] = 1; // 报错
  4. List list = new ArrayList();
  5. list.add(new Object()); // ok
  6. list.add(1); // ok
  7. list.add(""); // ok

如何保证 List 类型安全或者约束?装饰器模式,如果需要的是 int 呢?复制改改:

  1. pubic class StringList {
  2. List list = new ArrayList();
  3. void add(String s) { list.add(s); }
  4. int size() { return list.size(); }
  5. String get(int i) { return (String) list.get(i); }
  6. }
  7. // 使用
  8. StringList stringList = new StringList();

引入泛型之后:

  1. ArrayList<String> myList = new ArrayList<>();

image.png
泛型化的类,其类型声明在类上,通过传递参数 E,使得 ArrayList 变为“全新”的(编译期)类型。ArrayList 和 ArrayList 是完全“独立”的两个类。

  1. new ArrayList<Integer>();
  2. new ArrayList<String>();

不同一个类,因为不能互相赋值:

  1. 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 并不是 List 的子类型(类比 String/Object、String[]/Object[])

    下例中,任何需要父类的地方总是可以传递子类,需要一个动物,那么给你一只猫显然符合需求,符合氏里替换原则(里氏代换原则(Liskow-Substitution-Principle)定义:子类对象能够替换父类对象,而程序逻辑不变。 )
    但为什么需要 ArrayList 却不可以传入 ArrayList
    虽然 Cat 是 Animal 的子类型,但泛型信息编译后被擦除,Cat 无法转换为 Animal,所以 ArrayList 并不是 ArrayList 的子类型。

    1. import java.util.ArrayList;
    2. public class Main {
    3. public static void testObject(Animal animal) {
    4. }
    5. public static void testArray(Animal[] animal) {
    6. }
    7. public static void testList(ArrayList<Animal> animal) {
    8. }
    9. public static void main(String[] args) {
    10. testObject(new Cat()); // ok
    11. testArray(new Cat[2]); // ok
    12. testList(new ArrayList<Cat>()); // 编译器检查报错
    13. }
    14. }
    • 泛型只是编译期的警告,运行期类型不安全【使用限定符如 List<?>,也可以利用泛型擦除来绕过编译器检查】

      1. ArrayList<Animal> list = new ArrayList<>();
      2. ArrayList rawList = list; // 重新声明为“裸”的 ArrayList
      3. rawList.add("ojbk"); // 可以添加任何类型的数据
      4. // 因为对象是引用的,绕过了编译期检查,运行时类型信息被擦除,不影响正常运行
      5. // 最终“正常”输出 ["ojbk"]
      6. 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. 泛型的限定符和绑定

    • 定义泛型方法:

      • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
      • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
      • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
      • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像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));
        }
    }