深入理解Java泛型机制
一、什么是泛型?
泛型是JDK5后引入的特性,本质是参数化类型。它提供了编译时类型安全检测机制,只支持引用数据类型。
首先我们先看看没有泛型的时候,集合如何存储数据?
public static void method1() {
List list = new ArrayList();
List.add(22);
List.add("hncboy");
List.add(new Object());
for (Object o : list) {
System.out.println(o.getClass());
}
}
未使用泛型前,我们对集合可以进行任意类型的 add 操作,遍历结果都被转换成 Object 类型,因为默认所有的数据类型都是Object类型,输出结果如下所示。
class java.lang.Integer
class java.lang.String
class java.lang.Object
采用泛型之后,创建集合对象可以明确的指定类型,在编译期间就确定了该集合存储的类型,存储其他类型的对象编译器会报错。这时遍历集合就可以直接采用明确的 String 类型输出。
public static void method2() {
List<String> list = new ArrayList();
list.add("22");
list.add("hncboy");
//list.add(new Object()); 报错
for (String s : arrayList) {
System.out.println(s);
}
}
二、使用泛型有哪些好处?
1、类型安全。
把运行时期的问题提前到了编译期间,编译期间确定类型,保证类型安全,放的是什么,取的也是什么,不用担心抛出 ClassCastException 异常。
2、 提高代码可读性和可维护性。
泛型可以增加代码的可读性,因为使用泛型可以清晰地指定代码的意图和目的。它还可以减少代码的冗余和重复,提高代码的可维护性。
3、避免了强制类型转换和重复代码
泛型可以减少手动进行类型转换的需求,并减少因为不同类型需要编写类似的代码而导致的代码冗余(在没有指定类型是类型转换必须使用 instanceof 关键字来进行判定)。
4、允许库的设计者在实现时限制类型
通过使用泛型,实现集合类的设计者可以指定可以使用的类型。这样可以在设计时就避免类被误用。例如ArrayList中T必须是引用类型,如果试图用基本类型会编译报错。
泛型的最大好处就是在编译期实现类型安全检查,这个好处远远大于泛型可能引入的复杂性。合理使用泛型可以在大幅度提高代码质量和健壮性的同时,减少运行时出现的ClassCastException。
三、泛型如何表示?
一般泛型有约定的符号:E 代表 Element, 通常在集合中使用;T 代表 Type,通常用于表示类;K 代表 Key,V 代表 Value,<K, V> 通常用于键值对的表示;? 代表泛型通配符。
泛型的表达式有如下几种:
- 普通符号
- 无边界通配符 <?>
- 上界通配符 <? extends E> 父类是 E
- 下界通配符 <? super E> 是 E 的父类
注:泛型的使用需要先声明,声明通过<符号>的方式,符号可以任意,编译器通过识别尖括号和尖括号内的字母来解析泛型。泛型的类型只能为类,不能为基本数据类型。尖括号的位置也是固定的,只能在类名之后或方法返回值之前。
四、如何使用泛型?
泛型可以定义在类、接口、方法中,分别表示为泛型类、泛型接口、泛型方法。
1、泛型类
当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类。但要注意,静态方法上的泛型需要在静态方法上声明,不能直接使用。举个例子:
public class Test<T> {
private T data;
public T getData() {
return data;
}
/** 这种写法是错误的,提示 T 未定义 */
/*public static T get() {
return null;
}*/
/** 正确写法,该方法上的 T 和类上的 T 虽然一样,但是是两个指代,可以完全相同,互不影响 */
public static <T> T get() {
return null;
}
public void setData(T data) {
this.data = data;
}
}
2、泛型方法
当方法中形参类型不确定时使用。但要注意,在方法申明上定义的泛型只有本方法能用。类后面定义的所有方法都能用。
应用如下:
public class ListUtil {
private ListUtil(){}
//类中定义一个静态方法addAll,用来添加多个集合的元素
/**
* 参数一:集合
* 参数二~最后:要添加的元素
*/
public static <E>void addAll(ArrayList<E> list,E e1,E e2,E e3,E e4){
list.add(e1);
list.add(e2);
list.add(e3);
list.add(e4);
}
//可变参数
public static <E>void addAll2(ArrayList<E> list,E...e){
for (E element : e){
list.add(element);
}
}
public void show(){
System.out.println("尼古拉斯·汤");
}
}
public class ListUtilTest {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ListUtil.addAll(list1,"aaa","bbb","ccc","ddd");
System.out.println(list1);
ArrayList<Integer> list2 = new ArrayList<>();
ListUtil.addAll(list2,1,2,3,4);
ListUtil.addAll2(list2,1,2,3,4,5,6,7,8,9,9,3);
System.out.println(list2);
}
}
3、泛型接口
当一个类型未确定的类实现接口时使用。使用方式有两种:
1、实现类给出具体类型。
2、实现类延续泛型,创建对象时再确定。
/**
* 泛型接口的两种使用方式:
* 1.实现类给出具体的类型
* 2.实现类延续泛型,创建实现类对象时再确定类型
*/
//方式二
public interface CalcGeneric<T> {
T add(T num1, T num2);
}
public class CalculatorGeneric<T> implements CalcGeneric<T> {
@Override
public T add(T num1, T num2) {
return null;
}
}
4、泛型的通配符
利用泛型方法有一个小弊端,此时他可以接受任意的数据类型,比如 Ye Fu Zi Student,但是我希望只能传递Ye Fu Zi该怎么实现呢?
此时我们就可以使用泛型的通配符:
?也表示不确定的类型,他可以进行类型的限定
?extends E:表示可以传递E或者E所有的子类类型
?super E:表示可以传递E或者E所有的父类类型
代码如下:
public class GenericsDemo2 {
public static void main(String[] args) {
//创建集合的对象
ArrayList<Ye> list1 = new ArrayList<>();
ArrayList<Fu> list2 = new ArrayList<>();
ArrayList<Zi> list3 = new ArrayList<>();
ArrayList<Student> list4 = new ArrayList<>();
method(list1);
method(list2);
method(list3);
method(list4);
}
public static void method(ArrayList<? extends Fu> list){
}
}
class Ye{
}
class Fu extends Ye{
}
class Zi extends Fu{
}
应用场景:
1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符
- 泛型的通配符关键点:可以限定类型的范围。
5、 泛型数组
Java中的泛型是在编译器进行类型检查和类型擦除的过程中实现的。泛型的目的是为了提供编译时的类型安全性,而数组在创建时就需要明确指定元素的类型。由于泛型在编译后会进行类型擦除,即泛型类型信息会被擦除为其上界或Object类型,因此无法在运行时获取泛型的具体类型信息。
如果数组支持泛型,可能会导致类型安全性问题。例如,如果允许创建泛型数组List<String>[] array = new List<String>[10]
,那么可以通过数组的元素赋值将不同类型的列表存储在同一个数组中,违背了泛型的类型安全性。
为了解决这个问题,可以使用集合类(如ArrayList
、LinkedList
等)来代替数组,并通过泛型来实现类型安全的操作。集合类在内部会进行类型检查,可以动态地添加、删除和操作元素,并且支持泛型。
public class Test {
public static void main(String[] args) {
Number[] numbers = new Integer[10]; // 1
// java.lang.ArrayStoreException: java.lang.Double
numbers[0] = new Double(1); // 2
//List<String>[] list = new ArrayList<String>[10]; // 3
List<String>[] list2 = new ArrayList[10]; // 4
//List<Number> list3 = new ArrayList<Integer>(); // 5
}
}
6、 泛型擦除
在泛型内部,无法获得任何有关泛型参数类型的信息,泛型只在编译阶段有效,**泛型类型在逻辑上可看成是多个不同的类型,但是其实质都是同一个类型。**因为泛型是在JDK5之后才出现的,需要处理 JDK5之前的非泛型类库。擦除的核心动机是它使得泛化的客户端可以用非泛化的类库实现,反之亦然,这经常被称为"迁移兼容性"。
代价:泛型不能用于显式地引用运行时类型地操作之中,例如转型、instanceof 操作和 new 表达式,因为所有关于参数地类型信息都丢失了。无论何时,当你在编写这个类的代码的时候,提醒自己,他只是个Object。catch 语句不能捕获泛型类型的异常。
举个例子:有输出可见泛型在运行期间对类型进行了擦除。
public static void method1() {
List<Integer> integerArrayList = new ArrayList();
List<String> stringArrayList = new ArrayList();
System.out.println(integerArrayList.getClass());
System.out.println(stringArrayList.getClass());
System.out.println(integerArrayList.getClass() == stringArrayList.getClass());
}
输出:
class java.util.ArrayList
class java.util.ArrayList
true
五、总结:
1、什么是泛型?
泛型是JDK5后引入的特性,本质是参数化类型。可以在编译阶段约束并检查数据类型。
2、泛型有哪些好处?
-
类型安全
-
提高代码可读性和可维护性
-
避免类型转换和重复代码
-
允许设计者在实现时限制类型
3、哪里定义泛型?
-
泛型类:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类。但要注意,静态方法上的泛型需要在静态方法上声明,不能直接使用。
-
泛型方法:当方法中形参类型不确定时使用。但要注意,在方法申明上定义的泛型只有本方法能用。类后面定义的所有方法都能用。
-
泛型方法:当一个类型未确定的类实现接口时使用。
使用方式有两种:
-
实现类给出具体类型。
-
实现类延续泛型,创建对象时再确定。
-
4、泛型的继承和通配符
- 泛型不具备继承性,但是数据具备继承性
- 泛型的通配符
- ?也表示不确定的类型,他可以进行类型的限定
- ?extends E:表示可以传递E或者E所有的子类类型
- ?super E:表示可以传递E或者E所有的父类类型
5、泛型的细节?
- 泛型中不能写基本数据类型
- 如果不写泛型,类型默认是Object
- 指定泛型的具体类型后,传递数据时,可以传入该类型和他的子类类型
- 数组不支持泛型
- Java中的泛型是伪泛型,只在编译阶段有效
6、有哪些应用场景?
- 定义类、方法、接口的时候,如果类型不确定,就可以定义泛型
- 如果类型不确定,但能知道是哪个继承体系中的,可以使用泛型的通配符。