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