美文网首页Java 之旅
中级14 - Java的泛型

中级14 - Java的泛型

作者: 晓风残月1994 | 来源:发表于2020-06-02 17:38 被阅读0次

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

  • 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.pngimage.png

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

new ArrayList<Integer>();
new ArrayList<String>();

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

ArrayList<Integer> listt = new ArrayList<String>();
image.pngimage.png

也是同一个类,因为运行时泛型会被擦除。

2. 泛型擦除和问题

2.1 泛型擦除

从没有泛型的世界进化到有泛型的世界(1.4 -> 1.5),Java 选择了向后兼容,而非 Python2 到 Python3 那样断裂,编译成字节码后会被擦除,分别从 IDEA 反编译后的以及 ASM ByteCode Viewer 插件中可以看出:


image.pngimage.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));
    }
}

相关文章

  • 中级14 - Java的泛型

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

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

  • JAVA 核心笔记 || [xxx] 泛型

    泛型 JAVA 的参数化类型 称为 泛型 泛型类的设计 Learn12.java 运行

  • Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型

    Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型 在代码进入和离开的边界处,会处理泛型 Java泛型作...

  • Java泛型

    参考:Java知识点总结(Java泛型) 自定义泛型类 自定义泛型接口 非泛型类中定义泛型方法 继承泛型类 通配符...

  • JAVA-泛型

    JAVA-泛型 sschrodinger 2018/11/15 简介 泛型是Java SE 1.5的新特性,泛型的...

  • Kotlin 泛型

    Kotlin 支持泛型, 语法和 Java 类似。例如,泛型类: 泛型函数: 类型变异 Java 的泛型中,最难理...

网友评论

    本文标题:中级14 - Java的泛型

    本文链接:https://www.haomeiwen.com/subject/oycpzhtx.html