Java泛型

泛型-Generics

泛型的概念

在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。
它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。

  • 不使用泛型
/**
 * 这样做的一个不好的是Box里面现在只能装入String类型的元素,今后如果我们需要装入Integer等其他类型的元素,
 * 还必须要另外重写一个Box,代码得不到复用,使用泛型可以很好的解决这个问题。
 */
public class Box {
    private String object;

    public void set(String object) {
        this.object = object;
    }

    public String get(){
        return object;
    }
}
  • 使用泛型
public class  GenericBox<T> {
    // T stands for "Type"
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

限定通配符和非限定通配符

  • 限定通配符

限定通配符对类型进行了限制。有两种限定通配符:

一种是<? extends T>它通过确保类型必须是T及T的子类来设定类型的上界,

另一种是<? super T>它通过确保类型必须是T及T的父类设定类型的下界。

泛型类型必须用限定的类型来进行初始化,否则会导致编译错误。

  • 非限定通配符

    表示了非限定通配符,因为可以用任意类型来替代。
public class BoundaryCharExample {
    //查找一个泛型数组中大于某个特定元素的个数
    public static <T> int countGreaterThan(T[] array,T elem){
        int count = 0;
        for (T e : array) {
            if (e > elem) { // compiler error
                ++count;
            }
        }
        return count;
    }
    /*
    * comliler error:但是这样很明显是错误的,
    * 因为除了short, int, double, long, float, byte, char等原始类型,
    * 其他的类并不一定能使用操作符" > "
    * 解决 --> 使用限定通配符/边界符
    * */
}
  • 使用限定通配符改进
public class BoundaryCharExample2 {
    public static <T extends Comparable<T>> int countGreaterThan(T[] array,T elem){
        //<T extends Comparable<T>>就是通配符,类型必须是 Comparable<T>及其子类
        int count = 0;
        for (T e : array) {
            if (e.compareTo(elem)>0) {
                ++count;
            }
        }
        return count;
    }
}
  • 面试题:List<? extends T> 和List <? super T>之间有什么区别 ?

List<? extends T>可以接受任何继承自T的类型的List,

List<? super T>可以接受任何T的父类构成的List。

例如List<? extends Number>可以接受List或List

PECS(Producer Extends Consumer Super)原则

PECS原则即Producer Extends,Consumer Super原则

Producer Extends:如果你需要一个只读List,用它来produce T,那么使用? extends T

Consumer Super:如果你需要一个只写List,用它来consume T,那么使用? super T

public class GenericExample {
    public static void main(String[] args) {
        List<? extends Fruit> fruits = new ArrayList<Apple>();
        //? extends Fruit表示的是Fruit及其子类
        
        // Compile Error: can't add any type of object:
        //fruits.add(new Apple());
        //fruits.add(new Orange());
        //fruits.add(new Fruit());
        //fruits.add(new Object());
        //fruits.add(null); // Legal but uninteresting
    }
}

Compile Error: can't add any type of object:
从编译器的角度去考虑。因为List<? extends Fruit> fruits它自身可以有多种含义:
因为List<? extends Fruit> fruits它自身可以有多种含义(参照下面的代码 GenericReading.java):

List<? extends Fruit> fruits = new ArrayList<Fruit>();

List<? extends Fruit> fruits = new ArrayList<Apple>();

List<? extends Fruit> fruits = new ArrayList<Orange>();

// 这里Apple和Orange都是Fruit子类

当我们尝试add一个Apple的时候,fruits可能指向new ArrayList();

当我们尝试add一个Orange的时候,fruits可能指向new ArrayList();

当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,

而fruits可能只想某种特定类型的Fruit,编译器无法识别所以会报错。

所以对于实现了<? extends T>的集合类只能将它视为 Producer 向外提供元素(只能读),
,而不能作为 Consumer 来向外获取元素。

/**
 * 对于实现了<? extends T>的集合类只能将它视为Producer向外提供(get)元素,
 * 而不能作为Consumer来对外获取(add)元素。
 */
public class GenericReading {
    private List<Apple> apples = Arrays.asList(new Apple());
    private List<Fruit> fruit = Arrays.asList(new Fruit());

    private class Reader<T>{ //Reader<T> 是自定义的泛型类
         /*T readExact(List<T> list){ 
             return list.get(0);
         }*/
        T readExact(List<? extends T> list){// 使用通配符来解决这个问题
            // ? extends T 表示 T 及 T 的子类
            return list.get(0); //TODO :get()方法
        }
    }

    @Test
    public void test(){
        Reader<Fruit> fruitReader=new Reader<Fruit>();
        //Fruit f=fruitReader.readExact(apples);
        // 使用 readExact(List<T> list)  
        // Errors: List<Fruit> cannot be applied to List<Apple>.
        
        Fruit f=fruitReader.readExact(apples);//正确
        System.out.println(f);
    }
}
/**
 *
使用super的坏处是以后不能get容器里面的元素了,
 原因很简单,我们继续从编译器的角度考虑这个问题,
对于List<? super Apple> list,它可以有下面几种含义:
List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Object>();
当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
*/
public class GenericWriting {
    private List<Apple> apples = new ArrayList<Apple>();
    private List<Orange> oranges = new ArrayList<Orange>();
    private List<Fruit> fruit = new ArrayList<Fruit>();

    <T> void writeExact(List<T> list, T item) {
        list.add(item); //TODO :这里是 add
    }

    // ? super T 
    // T 及 T 的父类
    <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item);
    }

    void func1(){
        writeExact(apples,new Apple());
        writeExact(fruit,new Apple());
    }

    void func2(){
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruit, new Apple());
    }

    @Test
    public void test(){
        func1();
        func2();
    }
}
  • JDK 8 Collections.copy() 源码:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        //dest 就是 只写的 List
        //src 就是 只读的 List
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

类型擦除

类型擦除简介

Java 中的泛型是伪泛型。

类型擦除就是Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息
这样到了运行期间实际上JVM根本不知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下兼容
对于这一点,如果阅读 Java 集合框架的源码,可以发现有些类其实并不支持泛型。

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 class Node {
    private Object data; //T转换成Object
    private Node next;
    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Object getData() { return data; }
}

这意味着不管我们声明 Node 还是Node,到了运行期间,JVM 统统视为Node

posted @ 2024-01-15 22:57  行行行行星  阅读(12)  评论(0)    收藏  举报