当前位置:   article > 正文

Java学习笔记:初识泛型_数组是协变的,而集合不是协变的

数组是协变的,而集合不是协变的

泛型

概念

在编写Java代码时,可能会有将所有类型的元素都可存放入一个数组存储的想法,此时自然而然能想到,使用超类Object类创建Object[]即可接收所有类的对象。但是这样的做法存在弊端:每当要使用Object[]内的元素时,要对元素进行强制转换,这又需要知道取出的元素为哪种类型,否则这种行为是不安全的。这种存储方式便偏离了方便使用的初衷。所以为了方便存储和调用,能否对这个Object[]进行规定?规定其存放的类型,在取用时就可直接使用,若能如此则十分便利。

泛型是JDK5引入的一个特性。泛型机制在Java程序进行编译时进行安全检测,允许使用者在编译阶段检测出非法的类型。

语法

class ClassName<T1,T2,...,Tn>{
    
}
class ClassName<T1,T2,...,Tn> extends ParentClass<T1>{
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Java常用中泛型标记符

符号
E - Element元素
T - Type类型(非8种数据类型)
K - Key
V - Value
N - Number数值类型
S,U,V等第二、第三、第四个类型
表示不确定的java类型

注意事项

  • <>菱形运算符中的类型在编译器可根据上下文推导出时可以省略
List<Integer> s = new ArrayList<Integer>();
List<Integer> s = new ArrayList<>();
//两条语句效果相同
  • 1
  • 2
  • 3
  • 泛型将数据类型参数化进行传递,在编译时进行检测。只能使用类,不能使用8种基本的数据类型,若要使用则需要使用其包装类(每一个包装对象是不可变的,存储着基本数据类型原值,并提供获得基本数据类型值的方法,Java5后包装类支持自动装箱/拆箱)

  • 使用表示当前类是泛型类

泛型方法

例:

public class Generics {
    public static void main(String[] args) {
        Integer[] arr = {1, 2, 3, 4, 5};
        print(arr);
    }
//泛型static方法
    public static <E> void print (E[] input){
        for (E element: input) {
            System.out.printf("%s ", element);
        }
        System.out.println();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在泛型方法中的类型参数位于返回类型前

通配符

介绍

通配符< ? >是用来弥补泛型和泛型集合无法协变的不足。

Java中的数组是类型兼容的,叫作协变数组类型。数组是协变的,而泛型和泛型集合不是协变的。

//实现A、B类,B和A间为子类和父类关系
A[] test1 = new B[10];//编译通过
List<A> test2 = new ArrayList<B>();//编译报错
List<?> test3 = new ArrayList<B>();//<?>在此代表可以接收各种类型
  • 1
  • 2
  • 3
  • 4

假设A类和B类存在继承关系,A类为B类的父类,f(A)为A类更复杂的类型转换,如:ArrayList

协变:变化后保持对应关系,即 B ≤ A -> f(B) ≤ f(A)

逆变:变化后逆转对应关系,即 B ≤ A -> f(A) ≤ f(B)

不变:变化后不满足上述两种关系

但是这样又会带来另外的问题

看下面这段代码

//定义一个泛型类Fruit,提供构造,get和set方法
class Fruit<T>{
    private T fruit;

    public Fruit() {
    }

    public Fruit(T fruit) {
        this.fruit = fruit;
    }

    public void setFruit(T fruit) {
        this.fruit = fruit;
    }

    public T getFruit() {
        return fruit;
    }
}

public class Generics {
    //main方法
    public static void main(String[] args) {
         Fruit<String> test1 = new Fruit<>("草莓");
        Fruit<Integer> test2 = new Fruit<>(3);
        printFruit(test1);//输出"草莓"
        printFruit(test2);//无法编译,因为printFruit方法中要求的参数类型为Fruit<String>
    }

    public static void printFruit(Fruit<String> fruit){
        System.out.println(fruit.getFruit());
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

此时我们只需将上述printFruit方法改成如下,即可运行

public static void printLife(Fruit<?> fruit){
        System.out.println(fruit.getShelfLife());
}
  • 1
  • 2
  • 3

但是上述的改法并不安全,因为<?>可以接收任何泛型,这不是我们想要看到的,应当要加以限制,这就引出了泛型上界泛型下界

泛型上界

泛型上界基于协变性质

语法

<? extends 上界>
public static int count(Fruit<? extends Fruit> fruit){
    //可传入的实参类型被限定为Fruit及其子类
}
  • 1
  • 2
  • 3
  • 4

? extends设置的上界不能写入数据,只能进行数据读取

//在public class Generics中添加该方法,同时构建一个Apple类继承Fruit类
public static void correctFruit(Fruit<? extends Fruit> fruit){
	fruit.setFruit(new Apple());//发生错误
}
  • 1
  • 2
  • 3
  • 4

对于? extends限定的类型操作

List<? extends Fruit> wareHouse;
//Fruit为父类,Apple,Banana,Melon等为子类
  • 1
  • 2

读取

对于wareHouse来说可以读取到Fruit的对象,因为其中存储的是为Fruit类和Fruit的子类,而不一定能读取到Apple,因为可能其中存储的是Banana或Melon,出于数据安全考虑,一般用Fruit(基类)进行接收

写入

对于wareHouse来说,我们不能存储Fruit入wareHouse中,因为wareHouse可能为List,不能存入Apple,因为wareHouse有可能为List,所以不能进行写入操作

泛型下界

泛型下界基于逆变性质

语法

<? super 下界>
public static int count(Fruit<? super Fruit> fruit){
    //可传入的实参类型被限定为Fruit及其父类
}
  • 1
  • 2
  • 3
  • 4

? super设置的上界不能读取数据,只能进行数据存储

对于? super限定的类型操作

List<? super Fruit> wareHouse;
  • 1

读取

对于wareHouse来说,我们不能保证能读取到Fruit类型的数据,因为wareHouse可能存储的是Fruit的父类类型数据;不一定能读取Fruit的父类类型数据,有可能其中存储的是Object类型的数据,可以确定的是我们能读取的是Object类型的实例(见泛型擦除)

写入

对于wareHouse来说,可以存入Fruit及其子类类型的数据,但是不能存入Fruit父类,如Object类类型数据

类型擦除

泛型类可以由编译器通过类型擦除转变成非泛型类

class Fruit<T>{
    //在编译器编译时,T 会被替换成Object
}
  • 1
  • 2
  • 3

编译器生成了一种与泛型类同名的原始类,类型参数都被删去,类型变量由类型界限来代替,这就是擦除机制

优点:程序员可省略一些类型转换代码,由编译器进行类型检查

注意事项

  • 不能创建一个泛型类型的实例

  • T obj = new T();	//非法
    
    • 1
  • 不能创建一个泛型数组

  • T[] obj = new T[5];	//非法
    
    • 1
  • 不能进行参数化类型的数组的实例化

  • Fruit<String>[] arr = new Fruit<String>[10];//非法
    
    • 1
参考资料

[1] Java 之泛型通配符 ? extends T 与 ? super T 解惑

[2] 几个搞不太懂的术语:逆变、协变、不变

[3] 数据结构与算法分析 [美]Mark Allen Weiss

本文的主要目的是充当学习笔记,同时强化学习效果,且兼有分享所学知识之意,欢迎批评指正

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/924753
推荐阅读
相关标签
  

闽ICP备14008679号