zoukankan      html  css  js  c++  java
  • 扒一拔:Java 中的泛型(一)

    @

    1 泛型

    泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

    1.1 为什么需要泛型

    泛型是JDK1.5才出来的, 在泛型没出来之前, 我们可以看看集合框架中的类都是怎么样的。

    以下为JDK1.4.2的 HashMap

    1.4 HashMap

    可以看到, 在该版本中, 参数和返回值(引用类型)的都是 Object 对象。 而在 Java 中, 所有的类都是 Object 子类, 实用时, 可能需要进行强制类型转换。 这种转换在编译阶段并不会提示有什么错误, 因此, 在使用时, 难免会出错。

    而有了泛型之后,HashMap的中使用泛型来进行类型的检查

    Java 8 HashMap

    通过泛型, 我们可以传入相同的参数又能返回相同的参数, 由编译器为我们来进行这些检查。

    这样可以减少很多无关代码的书写。

    因此, 泛型可以使得类型参数化, 泛型有如下的好处

    1. 类型参数化, 实现代码的复用
    2. 强制类型检查, 保证了类型安全,可以在编译时就发现代码问题, 而不是到在运行时才发现错误
    3. 不需要进行强制转换。

    1.2 类型参数命名规约

    按照惯例,类型参数名称是单个大写字母。 通过规约, 我们可以容易区分出类型变量和普通类、接口。

    • E - 元素
    • T - 类型
    • N - 数字
    • K - 键
    • V - 值
    • S,U,V - 第2种类型, 第3种类型, 第4种类型

    2 泛型的简单实用

    2.1 最基本最常用

    最早接触的泛型, 应该就是集合框架中的泛型了。

    List<Integer> list = new ArrayList<Integer>();
     
    list.add(100086);     //OK
     
    list.add("Number"); //编译错误 
    

    在以上的例子中, 将 String 加入时, 会提示错误。 编译器不会编译通过, 从而保证了类型安全。

    2.2 简单泛型类

    2.2.1 非泛型类

    先来定义一个简单的类

    public class SimpleClass {
        private Object obj;
    
        public Object getObj() {
            return obj;
        }
    
        public void setObj(Object obj) {
            this.obj = obj;
        }
    }
    

    这么写是没问题的。 但是在使用上可能出现如下的错误:

        public static void main(String[] args) {
            SimpleClass simpleClass = new SimpleClass();
            simpleClass.setObj("ABC");// 传入 String 类型
            Integer a = (Integer) simpleClass.getObj(); // Integer 类型接受
        }
    

    以上写是不会报错的, 但是在运行时会出现报错

    java.lang.ClassCastException
    

    如果是一个人使用, 那确实有可能会避免类似的情况。 但是, 如果是多人使用, 则你不能保证别人的用法是对的。 其存在着隐患。

    2.2.2 泛型类的定义

    我们可以使用泛型来强制类型限定

    public class GenericClass<T> {
        private T obj;
    
        public T getObj() {
            return obj;
        }
    
        public void setObj(T obj) {
            this.obj = obj;
        }
    }
    

    2.2.3 泛型类的使用

    在使用时, 在类的后面, 使用尖括号指明参数的类型就可以

        @Test
        public void testGenericClass(){
            GenericClass<String> genericClass = new GenericClass<>();
            genericClass.setObj("AACC");
        /*    Integer str = genericClass.getObj();//*/
        }
    
    

    如果类型不符, 则编译器会帮我们发现错误, 导致编译不通过。

    检查

    2.3 简单泛型接口

    2.3.1 定义

    与类相似, 以 JDK 中的 Comparable接口为例

    package java.lang;
    import java.util.*;
    
    public interface Comparable<T> {
        public int compareTo(T o);
    }
    
    

    2.3.2 实现

    在实现时, 指定具体的参数类型即可。

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        ...
        public int compareTo(String anotherString) {
            byte v1[] = value;
            byte v2[] = anotherString.value;
            if (coder() == anotherString.coder()) {
                return isLatin1() ? StringLatin1.compareTo(v1, v2)
                                  : StringUTF16.compareTo(v1, v2);
            }
            return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
                              : StringUTF16.compareToLatin1(v1, v2);
         }
         ...
        
    }
    

    2.4 简单泛型方法

    泛型方法可以引入自己的参数类型, 如同声明泛型类一样, 但是其类型参数我的范围只是在声明的方法本身。 静态方法和非静态方法, 以及构造函数都可以使用泛型。

    2.4.1 泛型方法声明

    泛型方法的声明, 类型变量放在修饰符之后, 在返回值之前

    public class EqualMethodClass {
        public static <T> boolean equals(T t1, T t2){
            return t1.equals(t2);
        }
    }
    

    如上所示, 其中 <T> 是不能省略的。 而且可以是多种类型, 如 <K, V>

    public class Util {
        public static <K, V> boolean sameType(K k, V v) {
            return k.getClass().equals(v.getClass());
        }
    }
    

    2.4.2 泛型方法的调用

    调用时, 在方法之前指定参数的类型

        @Test
        public void equalsMethod(){
            boolean same = EqualMethodClass.<Integer>equals(1,1);
            System.out.println(same);
        }
    

    3 类型变量边界

    3.1 定义

    如果我们需要指定类型是某个类(接口)的子类(接口)

    <T extends BundingType>
    

    使用 extends , 表示 TBundingType 的子类, 两者都可以是类或接口。

    此处的 extends 和继承中的是不一样的。

    如果有多个边界限定:

     <T extends Number & Comparable>
    

    使用的是 & 符号。

    注意事项

    如果边界类型中有类, 则类必须是放在第一个

    也就是说

     <T extends Comparable & Number> // 编译错误
    

    会报错

    3.2 示例

    有时, 我们需要对类型进行一些限定, 比如说, 我们要获取数组的最小元素

    public class ArrayUtils {
        public static <T> T min(T[] arr) {
            if (arr == null || arr.length == 0) {
                return null;
            }
            T smallest = arr[0];
            for (int i = 0; i < arr.length; i++) {
                if (smallest.compareTo(arr[i]) > 0) {
                    smallest = arr[i];
                }
            }
            return smallest;
        }
    }
    

    上面的是报错的。 因为, 在该函数中, 我们需要使用 compareTo 函数, 但是, 并不是所欲的类都有这个函数的。 因此, 我们可以这样子限定

    <T> 转换成 <T extends Comparable<T>> 即可。

    测试

        @Test
        public void testMin() {
            Integer a[] = {1, 4, 5, 6, 0, 2, -1};
            Assertions.assertEquals(ArrayUtils.<Integer>min(a), Integer.valueOf(-1));
    
        }
    

    4 泛型, 继承和子类型

    4.1 泛型和继承

    在 Java 继承中, 如果变量 A 是 变量 B 的子类, 则我们可以将 A 赋值给 B。 但是, 在泛型中则不能进行类似的赋值。

    对继承来说, 我们可以这样做

    public class Box<T> {
        List<T> boxs = new ArrayList<>();
    
        public void add(T element) {
            boxs.add(element);
        }
    
        public static void main(String[] args) {
            Box<Number> box = new Box<Number>();
            box.add(new Integer(10));   // OK
            box.add(new Double(10.1));  // OK
        }
    }
    

    但是, 在泛型中, Box<Intager> 不能赋值给 Box<Number>(即两个不是子类或父类的关系)。

    泛型之间没有继承

    可以使用下图来进行阐释
    在这里插入图片描述

    注意:

    对于给定的具体类型 A 和 B(如 Number 和 Integer), MyClass<A>MyClass<B> 没有任何的关系, 不管 A 和 B 之间是否有关系。

    4.2 泛型和子类型

    在 Java 中, 我们可以通过继承或实现来获得一个子类型。 以 Collection 为例

    Collection

    由于 ArrayList<E></code> 实现了 List, 而 List<E> 继承了Collection<E>。 因此, 只要类型参数没有更改(如都是 String 或 都是 Integer), 则类型之间子父类关系会一直保留。

    5 类型推断

    类型推断并不是什么高大上的东西, 我们日常中其实一直在用到。它是 Java 编译器的能力, 其查看每个方法调用和相应声明来决定类型参数, 以便调用时兼容。

    值得注意的是, 类型推断算法仅仅是在调用参数, 目标类型和明显的预期返回类型时使用

    5.1 类型推断和泛型方法

    在下面的泛型方法中

    public class Box<T> {
        private T t;
    
        public void set(T t) { this.t = t; }
        public T get() { return t; }
    
    }
    
    public class BoxDemo {
    
      public static <U> void addBox(U u, 
           List<Box<U>> boxes) {
        Box<U> box = new Box<>();
        box.set(u);
        boxes.add(box);
      }
    
      public static <U> void outputBoxes(List<Box<U>> boxes) {
        int counter = 0;
        for (Box<U> box: boxes) {
          U boxContents = box.get();
          System.out.println("Box #" + counter + " contains [" +
                 boxContents.toString() + "]");
          counter++;
        }
      }
    
      public static void main(String[] args) {
        ArrayList<Box<Integer>> listOfIntegerBoxes =
          new ArrayList<>();
        BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
        BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
        BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
        BoxDemo.outputBoxes(listOfIntegerBoxes);
      }
    }
    
    

    输出

    Box #0 contains [10]
    Box #1 contains [20]
    Box #2 contains [30]
    

    我们可以看到, 泛型方法 addBox 中定义了一个类型参数 U, 在泛型方法的调用时, Java 编译器可以推断出该类型参数。 因此, 很多时候, 我们不需要指定他们。

    如上面的例子, 我们可以显示的指出

     BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
    

    也可以省略, 这样, Java 编译器可以从方法参数中推断出

    BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
    

    由于方法参数是 Integer, 因此, 可以推断出类型参数就是 Integer。

    5.2 泛型类的类型推断和实例化

    这是我们最常用到的类型推断了: 将构造函数中的类型参数替换成<>>(该符号被称为“菱形(The diamond)”), 编译器可以从上下文中推断出该类型参数。

    比如说, 正常情况先, 我们是这样子声明的

    Map<String, List<String>> myMap = new HashMap<String, List<String>>();
    

    但是, 实际上, 构造函数的类型参数是可以推断出来的。 因此, 这样子写即可

    Map<String, List<String>> myMap = new HashMap<>();
    

    但是, 不能将 <> 去掉, 否则编译器会报警告。

    Map<String, List<String>> myMap = new HashMap(); // 警告
    

    警告

    5.3 类的类型推断和构造函数

    在泛型类和非泛型类中, 构造函数都是可以声明自己的类型参数的。

    class MyClass<X> {
      <T> MyClass(T t) {
        // ...
      }
    
      public static void main(String[] args) {
        MyClass<Integer> myObject = new MyClass<>("");
      }
    }
    

    在以上代码 main 函数中,X 对应的类型是 Integer, 而 T 对应的类型是 String

    那么, 菱形 <> 对应的是 X 还是 T 呢?

    在 Java SE 7 之前, 其对应的是构造函数的类型参数。 而在 Java SE 7及以后, 其对应的是类的类型参数。

    也就是说, 如果类不是泛型, 则代码是这样子写的

    class MyClass{
      <T> MyClass(T t) {
        // ...
      }
    
      public static void main(String[] args) {
        MyClass myObject = new MyClass("");
      }
    }
    

    T 的实际类型, 编译器根据方法的参数推断出来。

    5.4 类型推断和目标类型

    Java 编译器利用目标类型来推断泛型方法调用的类型参数。 表达式的目标类型就是 Java 编译器所期望的数据类型, 根据该数据类型, 我们可以推断出泛型方法的类型。

    Collections 中的方法为例

    static <T> List<T> emptyList();
    

    我们在赋值时, 是这样子

    List<String> listOne = Collections.emptyList();
    

    该表达式想要得到 List<String> 的实例, 那么, 该数据类型就是目标类型。 由于 emptyList 的返回值是 List<T>, 因此, 编译器就推断, T对应的实际类型就是 String

    当然, 我们也可以显示的指定该类型参数

    List<String> listOne = Collections.<String>emptyList();
    

    6 通配符

    在泛型中, 使用 ? 作为通配符, 其代表的是未知的类型。

    6.1 设定通配符的下限

    有时候, 我们想写一个方法, 它可以传递 List<Integer>, List<Double>List<Number>。 此时, 可以使用通配符来帮助我们了。

    设定通配符的上限

    使用?, 其后跟随着 extends, 再后面是 BundingType(即上边界)

    <? extends BundingType>
    

    示例

    class MyClass{
      public static void process(List<? extends Number> list) {
        for (Number elem : list) {
          System.out.println(elem.getClass().getName());
        }
      }
      public static void main(String[] args) {
        List<Integer> integers = new LinkedList<>(Arrays.asList(1));
        List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
        List<Number> numbers = new LinkedList<>(Arrays.asList(1));
        process(integers);
        process(doubles);
        process(numbers);
      }
    }
    

    输出

    java.lang.Integer
    java.lang.Double
    java.lang.Integer
    

    也就是说, 我们通过通配符, 可以将List<Integer>, List<Double>List<Number>作为参数传递到同一个函数中。

    6.2 设定通配符的下限

    上限通配符是限定了参数的类型是指定的类型或者是其子类, 使用 extends 来进行。

    而下限通配符, 使用的是 super 关键字, 限定了未知的类型是指定的类型或者其父类。

    设定通配符的下限

    <? super bundingType>
    

    ? 后跟着 super, 在跟上对应的边界类型。

    示例

      public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 10; i++) {
          list.add(i);
        }
      }
    

    对于该方法, 由于我们是要将整型添加到列表中, 因此, 需要传入的列表必须是整型或者其父类。

    6.3 未限定的通配符

    当然, 我们也可以使用未限定的通配符。 如List<?>, 表示未知类型的列表。

    使用通配符的情景

    1. 所写的方法需要使用 Object 类所提供的功能
    2. 所写的方法, 不依赖于具体的类型参数。 比较常见的是反射中, 用Class<?>而非Class<T>, 因为绝大部分方法都不依赖于具体的类型。

    那么, 为什么不使用 List<Object> 进行替代呢?

    public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }
    

    在以上的方法中, 我们想带引出列表的各项。 但是以上的函数只能输出的是 Object 的实例(我们只能传入List<Object>, 而不是 List<Interger>等, 因为不是子类和父类的关系)。

    而更改为通配符之后

    public static void printList(List<?> list) {
        for (Object elem: list)
            System.out.print(elem + " ");
        System.out.println();
    }
    

    我们可以传入任意的 List.

      public static void main(String[] args) {
        List<Integer> integers = new LinkedList<>(Arrays.asList(1));
        List<Double> doubles = new LinkedList<>(Arrays.asList(1.0));
        List<Number> numbers = new LinkedList<>(Arrays.asList(1));
        printList(integers);
        printList(doubles);
        printList(numbers);
      }
    

    以上的代码运行正常。

    6.4 通配符和子类型

    在泛型和子类型中, 我们论证了

    对于给定的具体类型 A 和 B(如 Number 和 Integer), MyClass<A>MyClass<B> 没有任何的关系, 不管 A 和 B 之间是否有关系

    但是, 通配符可以在类或接口之间创建关系。 实现了子类和父类的关系。 因为 IntegerNumber的子类, 因此, 可以有如下的关系。

    继承

    正因为如此, 我们在前面进行参数传递时, 才可以进行多种类型参数的传递。

    6.5 通配符捕获

    我们想编写一个方法, 该方法

    public class WildcardError {
    
        void foo(List<?> i) {
            ? t = i.get(0); // 错误
            i.set(0, t);
        }
    }
    

    我们需要取得传入的类型, 但是, 在编写时, 不能使用 "?" 来作为一种类型。 此时, 我们可以使用类型捕获来解决干问题。

    public class WildcardError {
    
        void foo(List<?> i) {
            fooHelper(i);
        }
        private <T> void fooHelper(List<T> l) {
             T t = l.get(0); // 错误
            l.set(0, t);
        }
    
    }
    

    在此过程中, fooHelper 是泛型方法, 而 foo 方法不是, 它具有固定类型的参数。 在此情况下, T 捕获通配符。 它不知道具体的类型是哪一个, 但是, 这是一个明确的类型。

    惯例上, helper 方法, 被命名为 xxxHelper。

    7 类型擦除

    为了实现泛型, 编译器使用类型擦除:

    1. 替换所有的类型为其边界类或 没有边界则为 Object。 因此, 其所产生的字节码, 仅仅 包含的是原始的类,接口, 方法。
    2. 在必要的地方插入类型转换以保证类型安全
    3. 生成桥接方法以保留扩展泛型类型的多态。

    也就是说, 经过编译之后, 任何的类型都会被擦除。 因此, List<Integer>List<String>在运行时是一样的类型, 进行类型擦除之后, 都是 List

    7.1 类型擦除

    定义一个泛型类

    public class Node<T> {
    
        private T data;
        private Node<T> next;
    
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    
        public T getData() { return data; }
    
        public static void main(String[] args) {
            Node<String> node = new Node<>("11", null);
            System.out.println(node.getData());
        }
    }
    

    对其进行反编译, 可以获得:

    public class Node
    {
    
    	private Object data;
    	private Node next;
    
    	public Node(Object data, Node next)
    	{
    		this.data = data;
    		this.next = next;
    	}
    
    	public Object getData()
    	{
    		return data;
    	}
    
    	public static void main(String args[])
    	{
    		Node node = new Node("11", null);
    		System.out.println((String)node.getData());
    	}
    }
    

    可以看到, 类型已经被替换成 Object, 然后在 main 方法中, 将 Object 转换为 String, 因为我们传入的是 String 类型。

    同理, 将

    public class Node<T> {
    

    替换为

    public class Node<T extends Serializable> {
    

    则, 反编译后, 替换 T 为边界类型

    public class Node
    {
    
    	private Serializable data;
    	private Node next;
    
    	public Node(Serializable data, Node next)
    	{
    		this.data = data;
    		this.next = next;
    	}
    
    	public Serializable getData()
    	{
    		return data;
    	}
    
    	public static void main(String args[])
    	{
    		Node node = new Node("11", null);
    		System.out.println((String)node.getData());
    	}
    }
    

    方法的类型擦除也是一样的。

    7.2 类型擦除和桥接方法

    正因为有类型擦除的存在, 因此, 任何在运行时需要知道确切类型信息的操作都无法工作。

    有时候也会导致一些我们无法预料到的情况。

    在方法的重写时, 我们会遇到这样的情况

    声明一个泛型类

    public class Node<T> {
    
        public T data;
    
        public Node(T data) { this.data = data; }
    
        public T getData() {
            return data;
        }
        public void setData(T data) {
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    

    继承泛型类, 并指明了它的类型为 Integer

    public class MyNode extends Node<Integer> {
    
        public MyNode(Integer data) {
            super(data);
        }
    
        @Override
        public Integer getData() {
            return super.getData();
        }
    
        @Override
        public void setData(Integer data) {
            super.setData(data);
        }
    
        public static void main(String[] args) {
            Class<?> clazz = MyNode.class;
            for (Method m:
                 clazz.getDeclaredMethods()) {
                System.out.println(m + ":" + m.isBridge());
            }
        }
    }
    

    那么, 这个时候, 由于类型擦除,Node 类变成了这样子

    public class Node
    {
    
    	public Object data;
    
    	public Node(Object data)
    	{
    		this.data = data;
    	}
    
    	public Object getData()
    	{
    		return data;
    	}
    
    	public void setData(Object data)
    	{
    		System.out.println("Node.setData");
    		this.data = data;
    	}
    }
    

    那么问题就出现了。 如果没有任何的情况, 对于 setData 方法来说, 在父类中

    	public void setData(Object data)
    	{
    		System.out.println("Node.setData");
    		this.data = data;
    	}
    

    在子类中

        public void setData(Integer data) {
            super.setData(data);
        }
    

    显然, 这两个方法并不是重写的关系。

    为了解决这个问题, 以便在泛型擦除之后保持多态性, 编译器会产生桥接方法, 以保证子类运行时正确的。

    生成的桥接方法:

    public volatile void setData(Object obj){
    		setData((Integer)obj);
    }
    

    先写到这吧, 后面在继续深入。已经太长了!

  • 相关阅读:
    java提高篇(九)-----实现多重继承
    java提高篇(八)----详解内部类
    java提高篇(七)-----关键字static
    在tomcat下部署工程
    java提高篇(六)-----使用序列化实现对象的拷贝
    java提高篇(五)-----抽象类与接口
    java提高篇(四)-----理解java的三大特性之多态
    java提高篇(三)-----java的四舍五入
    java那些小事---用偶数做判断,不要用基数做判断
    java提高篇(二)-----理解java的三大特性之继承
  • 原文地址:https://www.cnblogs.com/homejim/p/10178339.html
Copyright © 2011-2022 走看看