文章目录
- 泛型
- 概念
- 语法
- 注意事项
- 泛型方法
- 通配符
- 介绍
- 泛型上界
- 语法
- 读取
- 写入
- 泛型下界
- 语法
- 读取
- 写入
- 类型擦除
- 注意事项
- 参考资料
泛型
概念
在编写Java代码时,可能会有将所有类型的元素都可存放入一个数组存储的想法,此时自然而然能想到,使用超类Object类创建Object[]即可接收所有类的对象。但是这样的做法存在弊端:每当要使用Object[]内的元素时,要对元素进行强制转换,这又需要知道取出的元素为哪种类型,否则这种行为是不安全的。这种存储方式便偏离了方便使用的初衷。所以为了方便存储和调用,能否对这个Object[]进行规定?规定其存放的类型,在取用时就可直接使用,若能如此则十分便利。
泛型是JDK5引入的一个特性。泛型机制
在Java程序进行编译时进行安全检测,允许使用者在编译阶段检测出非法的类型。
语法
1
2
3
4
5class ClassName<T1,T2,...,Tn>{ } class ClassName<T1,T2,...,Tn> extends ParentClass<T1>{ }
Java常用中泛型标记符
符号 | |
---|---|
E - Element | 元素 |
T - Type | 类型(非8种数据类型) |
K - Key | 键 |
V - Value | 值 |
N - Number | 数值类型 |
S,U,V等 | 第二、第三、第四个类型 |
? | 表示不确定的java类型 |
注意事项
- <>菱形运算符中的类型在编译器可根据上下文推导出时可以省略
1
2
3
4List<Integer> s = new ArrayList<Integer>(); List<Integer> s = new ArrayList<>(); //两条语句效果相同
-
泛型将数据类型参数化进行传递,在编译时进行检测。只能使用类,不能使用8种基本的数据类型,若要使用则需要使用其包装类(每一个包装对象是不可变的,存储着基本数据类型原值,并提供获得基本数据类型值的方法,Java5后包装类支持自动装箱/拆箱)
-
使用表示当前类是泛型类
泛型方法
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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(); } }
在泛型方法中的类型参数位于返回类型前
通配符
介绍
通配符< ? >是用来弥补泛型和泛型集合无法协变的不足。
Java中的数组是类型兼容的,叫作协变数组类型。数组是协变的,而泛型和泛型集合不是协变的。
1
2
3
4
5//实现A、B类,B和A间为子类和父类关系 A[] test1 = new B[10];//编译通过 List<A> test2 = new ArrayList<B>();//编译报错 List<?> test3 = new ArrayList<B>();//<?>在此代表可以接收各种类型
假设A类和B类存在继承关系,A类为B类的父类,f(A)为A类更复杂的类型转换,如:ArrayList
协变:变化后保持对应关系,即 B ≤ A -> f(B) ≤ f(A)
逆变:变化后逆转对应关系,即 B ≤ A -> f(A) ≤ f(B)
不变:变化后不满足上述两种关系
但是这样又会带来另外的问题
看下面这段代码
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//定义一个泛型类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()); } }
此时我们只需将上述printFruit方法
改成如下,即可运行
1
2
3
4public static void printLife(Fruit<?> fruit){ System.out.println(fruit.getShelfLife()); }
但是上述的改法并不安全,因为<?>可以接收任何泛型,这不是我们想要看到的,应当要加以限制,这就引出了泛型上界
和泛型下界
泛型上界
泛型上界基于协变性质
语法
1
2
3
4
5<? extends 上界> public static int count(Fruit<? extends Fruit> fruit){ //可传入的实参类型被限定为Fruit及其子类 }
? extends设置的上界不能写入数据,只能进行数据读取
1
2
3
4
5//在public class Generics中添加该方法,同时构建一个Apple类继承Fruit类 public static void correctFruit(Fruit<? extends Fruit> fruit){ fruit.setFruit(new Apple());//发生错误 }
对于? extends限定的类型操作
1
2
3List<? extends Fruit> wareHouse; //Fruit为父类,Apple,Banana,Melon等为子类
读取
对于wareHouse来说可以读取到Fruit的对象,因为其中存储的是为Fruit类和Fruit的子类,而不一定能读取到Apple,因为可能其中存储的是Banana或Melon,出于数据安全考虑,一般用Fruit(基类
)进行接收
写入
对于wareHouse来说,我们不能存储Fruit入wareHouse中,因为wareHouse可能为List,不能存入Apple,因为wareHouse有可能为List,所以不能进行写入操作
泛型下界
泛型下界基于逆变性质
语法
1
2
3
4
5<? super 下界> public static int count(Fruit<? super Fruit> fruit){ //可传入的实参类型被限定为Fruit及其父类 }
? super设置的上界不能读取数据,只能进行数据存储
对于? super限定的类型操作
1
2List<? super Fruit> wareHouse;
读取
对于wareHouse来说,我们不能保证能读取到Fruit类型的数据,因为wareHouse可能存储的是Fruit的父类类型数据;不一定能读取Fruit的父类类型数据,有可能其中存储的是Object类型的数据,可以确定的是我们能读取的是Object类型的实例(见泛型擦除)
写入
对于wareHouse来说,可以存入Fruit及其子类类型的数据,但是不能存入Fruit父类,如Object类类型数据
类型擦除
泛型类可以由编译器通过类型擦除转变成非泛型类
1
2
3
4class Fruit<T>{ //在编译器编译时,T 会被替换成Object }
编译器生成了一种与泛型类同名的原始类
,类型参数都被删去,类型变量由类型界限来代替,这就是擦除机制
优点:程序员可省略一些类型转换代码,由编译器进行类型检查
注意事项
-
不能创建一个泛型类型的实例
- 复制代码1
2T obj = new T(); //非法
-
不能创建一个泛型数组
- 复制代码1
2T[] obj = new T[5]; //非法
-
不能进行参数化类型的数组的实例化
- 复制代码1
2Fruit<String>[] arr = new Fruit<String>[10];//非法
参考资料
[1] Java 之泛型通配符 ? extends T 与 ? super T 解惑
[2] 几个搞不太懂的术语:逆变、协变、不变
[3] 数据结构与算法分析 [美]Mark Allen Weiss
本文的主要目的是充当学习笔记,同时强化学习效果,且兼有分享所学知识之意,欢迎批评指正
最后
以上就是正直方盒最近收集整理的关于Java学习笔记:初识泛型泛型通配符类型擦除的全部内容,更多相关Java学习笔记内容请搜索靠谱客的其他文章。
发表评论 取消回复