Java中泛型的使用

简介

泛型:参数化类型,把类型当做一个参数传递到类中,这样做可以增加代码的灵活性。java从1.5开始提供了泛型

泛型的作用:

  • 任意化类型和编译时类型检查:任意化类型,是指一套代码可以应用在不同的类型上,例如,一个排序算法,它即可以对int类型的数据排序,也可以对long类型的数据排序,还可以对String类型的数据排序。在没有泛型之前,如果想要实现任意化类型,需要使用Object类,但是它需要强制类型转换,而且这种转换编译器无法判断正确与否,容易造成一些未知的错误。使用泛型,所有的类型转换全部由泛型完成,并且泛型可以提供类型检查,保证编译时类型安全。使用泛型最多的地方是各种用于存储数据的容器类,通过泛型,容器类中可以存储任何类型的数据,并且保证容器中数据类型的正确。
  • 代码复用:面向对象思想通过从上而下的继承关系实现了纵向维度的代码复用,泛型编程思想通过将平行关系的类联系起来实现了横向维度的代码复用

泛型的特点:

  • 不支持基本数据类型:泛型是使用Object类型来实现的,一个泛型在没有指定具体的数据类型之前,就是一个Object类型。基本数据类型和Object类型之间没有继承关系,Object类不可以强转为基本数据类型,所以泛型不支持基本数据类型,只支持引用数据类型。对于基本数据类型,可以使用它们的包装类来代替。
  • 只在编译阶段有效:泛型只在编译阶段有效,在编译结束后,会将泛型的相关信息擦除,本质上讲,这是一种伪泛型,Java之所以如此设计,主要是为了在二进制层面上的兼容之前的代码。同一个泛型类根据不同的数据类型创建的对象,本质上同一个类型。

使用方式

泛型可以使用在类、接口、方法上,它们分别称作:泛型类、泛型接口、泛型方法。

泛型标识:常用的有T、E、K、V,这些标识 是编码时的一种约定,

  • T:表示一个具体的类型,
  • E:表示一个元素,
  • K:表示一个键类型,
  • V:表示一个值类型

泛型类

使用泛型的类,泛型类可以持有任意类型的属性,这增加了类的灵活性。

声明:[public] class 类名称<泛型标识[, ...]>{ // 类体 }

实例化:类名称<泛型类型> 对象名 = new 类名称<[泛型类型]>([实参列表]);。在实例化泛型类时,需要指明泛型类中的类型参数。Java1.7之后,后面的尖括号中的具体的数据类型可以省略不写。

泛型类中使用泛型:

  • 在泛型类中,泛型不可以被实例化。因为不清楚传入的泛型实参是否可以被实例化
  • 在泛型类中,以泛型为数据类型的属性不可以是静态的,因为泛型实参是在类实例化时被传入的,静态属性的生命周期在实例化之前

不允许创建泛型数组:数组和泛型的运行机制不兼容,数组在运行时元素的类型信息是明确的,而泛型存在类型擦除,在运行时都是Object类型,这和数组的运行机制是相冲突的。

案例1:一个使用泛型的容器类

  • 泛型类:
import java.util.Arrays;

public class Box<T> {
    // 使用Object作为数组的类型,因为数组和泛型的运行机制不兼容
    private Object[] elements = new Object[4];
    private int size;

    public T get(int i) {
        return (T) elements[i]; // 告警信息:Unchecked cast: 'java.lang.Object' to 'T'
    }

    public void add(T ele) {
        if (size >= elements.length) {
            // 扩容
            resize();
        }
        elements[size++] = ele;
    }

    public int size() {
        return size;
    }

    private void resize() {
        int len = elements.length;
        Object[] newElements = new Object[len + 8];
        System.arraycopy(elements, 0, newElements, 0, elements.length);
        elements = newElements;
    }

    @Override
    public String toString() {
        return "Box{" +
                "elements=" + Arrays.toString(elements) +
                '}';
    }
}
  • 使用案例:
public class BoxTest {
    // 测试Box类
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.add("aaa");
        stringBox.add("bbb");
        stringBox.add("ccc");
        stringBox.add("ddd");
        stringBox.add("eee");
        stringBox.add("fff");
        System.out.println(stringBox.get(1)); // bbb
        System.out.println("stringBox = " + stringBox); // Box{elements=[aaa, bbb, ccc, ddd, eee, fff, ...
        System.out.println("stringBox.size() = " + stringBox.size()); // 6
    }
}

案例2:泛型类中的泛型不可以和静态属性一起使用

public class Box<T> {
    // 编译时会报错:'Box.this' cannot be referenced from a static context,
    // 因为泛型是传递给实例的,需要通过一个实例来访问,在静态上下文中无法访问
    public static T boxEle; 
}

案例3:泛型类中的泛型不可以被实例化

public class Box<T> {
    // 编译时报错:Type parameter 'T' cannot be instantiated directly,泛型参数T不可以被直接实例化,
    // 因为不清楚Box实例化时传入的泛型实参是否可以被实例化
    private T e = new T();
    private T[] elements = new T[4];
}

泛型类的继承

继承规则:

  • 如果子类也是泛型类,子类和父类的泛型类型要一致;
  • 如果子类不是泛型类,子类的声明中,父类要明确泛型类型,因为子类的泛型信息会被传递给父类

案例1:子类是泛型类

public class SpecialBox<E, F, G> extends Box<E> { } // 子类和父类的泛型类型要一致,注意,子类的泛型名称,T、E之类的,可以和父类不一致,但是子类中的声明必须一致,例如,在当前案例中,泛型E将会被传递给父类

案例2:子类不是泛型类

public class SpecificBox extends Box<Integer> { }  // 子类不是泛型类,继承时需要明确父类的泛型类型

泛型接口

声明:[public] interface 接口名<泛型标识 [, ...]>{ // interface body }

泛型接口的实现和泛型类的继承是一样的。

案例:以java中的List接口为例,写两个实现类,一个是泛型类、一个不是泛型类

// 1. List接口的声明
public interface List<E> extends Collection<E> { }

// 2. 实现类是泛型类,实现类中,实现类和接口的泛型类型要一致
public class MyList2<T> implements List<T> {
    // 实现接口中的方法时,使用了泛型
    @Override
    public List<T> subList(int fromIndex, int toIndex) {
        return Collections.emptyList();
    }
}

// 3. 实现类不是泛型类,接口的泛型要明确数据类型
public class MyList implements List<Integer> {
    // 实现接口中的方法时,使用具体类
    @Override
    public ListIterator<Integer> listIterator(int index) {
        return null;
    }
}

泛型方法

方法支持泛型。在调用泛型方法的时候指明泛型的具体类型,它可以不依附于泛型类而存在,方法可以是成员方法也可以是静态方法。

声明:[访问权限修饰符] [static] [final] <泛型参数列表> 返回值类型 方法名(形参列表)

调用方式:和调用普通方法一样,通过实参的类型来指定泛型的类型。泛型类是在实例化类的时候指明泛型的具体类型。泛型方法是在调用方法的时候指明泛型的具体类型

案例:静态泛型方法和成员泛型方法

import java.util.Random;

public class GenericTest {
    public static void main(String[] args) {
        // 测试静态泛型方法
        genericMethod(new Object(), "aa", new Random());
      
        // 测试成员泛型方法
        new GenericTest().genericMethod2(new Object(), "bb", new Random());
    }

    // 静态泛型方法
    public static <T, E, K> void genericMethod(T t, E e, K k) {
        System.out.println("t = " + t);
        System.out.println("e = " + e);
        System.out.println("k = " + k);
    }

    // 成员泛型方法
    public <T, E, K> void genericMethod2(T t, E e, K k) {
        System.out.println("t = " + t);
        System.out.println("e = " + e);
        System.out.println("k = " + k);
    }
}

类型通配符

无界通配符

无界通配符用一个问号表示,表示任意类型,它通常用于方法参数,表示允许方法接收任意类型的泛型实例。无界通配符和Object不一样,无界通配符表示任意类型,Object是一个具体的类型,只能说无界通配符在运行时都是按照Object来处理的,但是普通的泛型也是这样。

案例:

import java.util.ArrayList;
import java.util.List;

public class GenericTest3 {
    // 测试
    public static void main(String[] args) throws ClassNotFoundException {
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        printList(list);  // list.toString() = [aaa, bbb, ccc]

        List<Integer> list2 = new ArrayList<>();
        list2.add(1);
        list2.add(2);
        list2.add(3);
        printList(list2);  // list.toString() = [1, 2, 3]
    }

    // 无界通配符,接收任意类型的list,如List、List
    public static void printList(List<?> list) {
        System.out.println("list.toString() = " + list.toString());  
    }
}

案例2:

// 这里使用一个无界通配符,因为此时编译器并不知道Class的具体类型
Class<?> strClass = Class.forName("java.lang.String");
System.out.println("strClass.getSimpleName() = " + strClass.getSimpleName());  // String

// Class的泛型实参是String,这在编译时就确定了
Class<String> stringClass = String.class;
System.out.println("stringClass.getSimpleName() = " + stringClass.getSimpleName());  // String

泛型的上界和下界

上界和下界:

  • 上界:格式:? extends 实参类型,要求该泛型的类型,只能是实参类型或实参类型的子类。用于读取数据时,确保数据的类型范围。这种通配符允许用户从泛型集合中读取数据,但不允许用户向集合中添加数据,因为编译器无法确定具体的类型。

  • 下界:格式:? super 实参类型,要求该泛型的类型,只能是实参类型或实参类型的父类。用于写入数据时,确保容器的类型范围。

泛型的上界和下界更加细致的限定了类型

案例1:上界

public static void processList(List<? extends Son> list) {
    Son son = list.get(0);
    // 错误:添加元素时报类型不匹配,要求的元素是 capture of ? extends Son,实际提供的元素Son。
    // 泛型的上界,? extends Son,是一个不确定的类型,只知道它是Son的子类,编译器无法确定list的具体类型,
    // 因此不允许用户向其中添加 Son 类型的对象。
    // list.add(new Son());
}

案例2:下界

// 下界
public static void processList2(List<? super Son> list) {
    Object o = list.get(0); // 正确:可以从集合中读取数组,但是无法获取具体的数据类型
    list.add(new Son());    // 正确:可以向集合中添加数据,只要它是Son类即可
    list.add(new Father()); // 错误,编译器只允许添加Son类型,不能添加Son的父类,因为集合中如果有两种类型的数据,会出现类型安全问题
}

为什么上界不允许向集合中添加数据,但是下界却可以,例如,

  • ? extends Son,不可以向集合中添加数据,
  • ? super Son,可以向集合中添加Son类型的数据。

在这个案例中,下界可以确保元素一定是Son类型或它的父类,添加Son类型的元素,不会出现类型安全问题,但是添加Son的父类,如果集合是Son类型,就会出现类型安全问题。上界可以确保元素是Son或它的子类,编译器不清楚具体是哪个子类,所以无法添加元素。

案例3:泛型的上界,正确的使用方式

import java.util.ArrayList;
import java.util.List;

public class GenericTest4 {
    // 测试
    public static void main(String[] args) {
        List<Integer> list2 = new ArrayList<>();
        list2.add(1);
        list2.add(2);
        list2.add(3);
        printNumbers(list2);   // 1 2 3 
    }

    // 泛型的上界,此时这里只能处理Number类型和其子类,集合中只能读取,不能写入
    public static void printNumbers(List<? extends Number> list) {
        for (Number number : list) {
            System.out.print(number);
            System.out.print(' ');
        }
    }
}

案例4:泛型的下界,此时集合可以被写入

import java.util.ArrayList;
import java.util.List;

public class GenericTest5 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        addNumbers(list);
        System.out.println("list = " + list); // [1, 2, 3]

        List<Number> list2 = new ArrayList<>();
        addNumbers(list2);
        System.out.println("list2 = " + list2); // [1, 2, 3]

        List<Object> list3 = new ArrayList<>();
        addNumbers(list3);
        System.out.println("list3 = " + list3); // [1, 2, 3]
    }

    // 泛型的下界,此时,方法可以接收Integer和它的父类作为泛型实参
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);  // 只能向集合中写入Integer类型的数据
        list.add(2);
        list.add(3);
    }
}

案例5:上界和下界一起使用,实现集合复制功能

import java.util.ArrayList;
import java.util.List;

public class GenericTest6 {
    public static void main(String[] args) {
        List<Integer> sourceList = new ArrayList<>();
        sourceList.add(1);
        sourceList.add(2);
        sourceList.add(3);

        List<Object> targetList = new ArrayList<>();
        copyList(sourceList, targetList);
        System.out.println("targetList = " + targetList);  // [1, 2, 3]
    }

    // 集合复制,原集合的泛型可以是指定类型和它的子类,目标集合的泛型是指定类型和它的父类
    public static <T> void copyList(List<? extends T> sourceList, List<? super T> targetList) {
        for (T ele : sourceList) {
            targetList.add(ele);
        }
    }
}

限定多重边界

使用 & 符合可以对泛型限定多重边界,如 ,只支持限定上界,不支持限定下界,

为什么不支持限定多重下界:因为这会违反单继承的原则。案例,? super Integer,在这个泛型下界中,用户可以向集合中写入Integer类型的数据,但是在 ? super Integer & String 中,编译器无法确定改向集合中写入什么类型的数据,因为集合只能接受一种类型的数据

案例:数字比较

import java.util.ArrayList;
import java.util.List;

public class GenericTest7 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(4);
        list.add(3);

        Integer i = maxNum(list.toArray(new Integer[0]));
        System.out.println("i = " + i); // 4
    }

    // 限定多重上界
    public static <T extends Number & Comparable<T>> T maxNum(T ... arr) {
        T max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (max.compareTo(arr[i]) < 0) {
                max = arr[i];
            }
        }
        return max;
    }
}

泛型的底层机制:泛型擦除

泛型信息只存在于代码编译阶段,在运行之前,与泛型相关的信息将会被擦除

类型擦除的分类:

  • 无限制类型擦除:使用Object类型代替泛型标识符
  • 有限制类型擦除:泛型如果指定上限,泛型标识被替换为上限类型

泛型的使用

泛型不能和数组一起使用

数组是Java中的原生数据类型,它在运行时依然保留了元素的类型信息,而泛型信息在运行之前会被擦除,这和数组的特点是冲突的,所以泛型和数组不可以一起使用。

案例:不可创建泛型数组,如果一个类带有泛型信息,以它为数据类型的数组该如何创建?

// 错误的做法:下面这行代码会报错:Generic array creation not allowed,不允许创建泛型数组
// 因为不允许在创建数组的同时声明泛型类的实参类型
// Class[] sClassARR = new Class[10];

// 正确的做法:创建数组时不带泛型信息,然后进行类型转换,然后阻止编译器抛异常
@SuppressWarnings("unchecked")
Class<String>[] sClassArr = (Class<String>[]) new Class[10];

// 另一种写法,使用泛型通配符,Java允许在数组创建时使用通配符
public static void main(String[] args) {
    Node<?, ?>[] nodes = new Node<?, ?>[10];
}
private static class Node<K, V> { }

泛型不能和静态属性一起使用

因为泛型实参是在类实例化时被指定的,静态属性的生命周期在类实例化之前,所以静态属性无法获取到泛型实参,也就无法使用泛型了

泛型和内部类

成员内部类会继承外部类的泛型信息,静态内部类则不会,因为成员内部类依赖外部类的实例,但是静态内部类不依赖外部类的实例。

案例:成员内部类,如果继承了外部类的泛型信息,在外部类中,不允许创建内部类类型的数组

public class HashTable<K, V> {
    private Node[] table;

    public HashTable() {
        // 这行代码会报错,Generic array creation not allowed,不允许创建泛型数组,因为内部类带有泛型信息,
        // 而且泛型信息是继承自外部类的,成员内部类会受外部类的影响
        // table = new Node[10];
    }

    // 继承了外部类中的泛型信息
    private class Node {
        private K key;
        private V value;
        private int hash;
        private Node next;

        public Node() { }

        public Node(K key, V value, int hash, Node next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
    }
}

案例:静态内部类

public class HashTable2<K, V> {
    private Node<K, V>[] table;
    
    @SuppressWarnings("unchecked")
    public HashTable2() {
        // 正确的做法,静态内部类带有泛型信息,但是泛型信息是自己声明的,实例化数组时不加泛型信息,随后进行类型强转
        table = (Node<K, V>[]) new Node[10];
    }

    // 没有继承外部类的泛型信息
    private static class Node<K, V> {
        private K key;
        private V value;
        private int hash;
        private Node next;

        public Node() { }

        public Node(K key, V value, int hash, Node<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
    }
}

在泛型类中无法访问一个泛型的类对象

案例:

public class GenericClass<T> {
    public void doSomething() {
        // 试图获取泛型参数的类对象
        // Class clazz = T.class; // 编译错误: 泛型类型'T'的类型未知。
    }
}

结果:编译器会报错,因为 T 的实际类型在运行时是未知的。当编译器看到 T.class 时,由于类型擦除,它实际上看不到任何具体类型,从而无法解析 T。

原因:在 Java 中,泛型类或方法不能直接访问其泛型参数的类对象(即 Class)。这是因为 Java 泛型在编译时会进行类型擦除,即在字节码中不保留任何关于泛型类型参数的信息。这使得在运行时无法知道泛型参数的实际类型。

类型擦除的目的是为了与非泛型代码保持兼容。例如,对于 List,编译器会将泛型参数 T 擦除为 Object,所以在运行时,所有的 ListList 等都会被视为 List

你可能感兴趣的:(java,开发语言)