Java泛型的使用和解析
什么是泛型
在Java中,泛型是一种强类型约束的机制,可以在编译期间检查数据的类型安全性,并且可以提高代码的复用性和可读性。Java在1.5时增加了泛型,据说专家们为此花费了差不多五年时间(相当不容易了),有了泛型之后我们对集合的使用就变得很规范了。
看下下面这段简单的代码,其限定了集合插入元素的类型为String
ArrayList<String> list = new ArrayList<>();
list.add("string");
list.add("qg");
System.out.println(list.get(0));
那如果没有泛型我们该如何限定集合中元素的类型呢?
我们可以使用Object数组设计应该Arraylist类模拟一下
public class Arraylist {
//数组默认大小
private final int DEFAULT_SIZE = 10;
//存储元素的数组
private Object[] elementData = new Object[DEFAULT_SIZE];
//当前元素个数
private int size = 0;
//增加元素
public void add(Object o) {
elementData[size++] = o;
}
//根据索引(index)获取元素
public Object get(int index) {
return elementData[index];
}
}
然后我们像Arrraylist类对象中存储数据
Arraylist list = new Arraylist();
list.add("string");
list.add(new Date());
System.out.println((String)list.get(0));
大家有没有发现两个问题?
- Arraylist可以存放任何类型的数据(String、Date等),因为所有类都继承于Object类。
- 从Arraylist取出数据的时候需要强制转换类型,因为编译器并不能确定你取的是字符串函数日期。
对比之下,大家应该就可以感受到泛型的优势,规范了数据的类型,不需要进行类型的强制转换,对于使用者清晰的知道集合中存储的元素类型。
泛型的使用
下面使用泛型去定义一个类和泛型方法
public class Arraylist2<T> {
private Object[] elementData;
private int size = 0;
private int initialCapacity = 10;
public Arraylist2() {
elementData = new Object[initialCapacity];
}
//增加集合元素,元素的类型为T,即创建类时指定的类型
public void add(T o) {
elementData[size++] = 0;
}
//根据索引index获取元素,并转换为T类型
public T get(int index) {
return (T)elementData[size];
}
//将集合元素转换为数组,数组类型为E[],E为传入数组的元素类型
public <E> E[] toArray(E[] a) {
return (E[])Arrays.copyOf(elementData,size,a.getClass());
}
}
测试代码如下
public static void main(String[] args) {
Arraylist2<String> list = new Arraylist2<>();
list.add("1");
list.add("2");
list.add("3");
String[] strs = new String[3];
String[] strArr = list.toArray(strs);
for (int i = 0; i < strArr.length; i++) {
System.out.println(strArr[i]);
}
}
上述代码展示了定义泛型方法和泛型类的基本使用方法,可以看出泛型可以让方法接受任意类型的参数,并作出相同的动作,这就可以让方法的普适性更高,更加灵活。
泛型限定符 extends
首先说一下 extends
的作用
例如 <T extends Number>
,表示T只能接受 Number 或 Number 的子类。使用上限通配符可以提高程序的类型安全性。
在解释这个限定符前,我们假设有三个类
class A {
public String toString() {
return "father";
}
}
class B extends A {
public String toString() {
return "my";
}
}
class C extends B {
public String toString() {
return "son";
}
}
我们重新定义一下Arraylist类的泛型
class Arraylist<T extends B> {
//同上.....
}
此时我们像Arraylist的实例对象中添加A对象就会报错,添加B对象和C对象则不会报错,因为extends
限定了 T 这个泛型只能继承于B或者是B,即B是T的父类或者T,即定义了T的上限为B类。
泛型通配符 ?
通配符使用英文的问号(?)
来表示。在我们创建一个泛型对象时,可以使用关键字 extends
限定子类,也可以使用关键字 super
限定父类。
通配符用于表示某种未知的类型,例如 List<?>
表示一个可以存储任何类型对象的 List,但是不能对其中的元素进行添加操作**(因为你不知道List中存储的元素类型,如果让你添加元素,元素类型不一致,则不安全)**。通配符可以用来解决类型不确定的情况,例如在方法参数或返回值中使用。
使用通配符可以使方法更加通用,同时保证类型安全。
例如,定义一个泛型方法:
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
System.out.println();
}
这个方法可以接受任意类型的 List,例如 List<Integer>
、List<String>
等等。
上限通配符
泛型还提供了上限通配符 <? extends T>
,表示通配符只能接受 T 或 T 的子类。使用上限通配符可以提高程序的类型安全性。
例如,定义一个方法,只接受 Number 及其子类的 List:
public static void printNumberList(List<? extends Number> list) {
for (Number num : list) {
System.out.print(num + " ");
}
System.out.println();
}
这个方法可以接受 List<Integer>
、List<Double>
等等。
但上限通配符接受的集合一样不可以添加元素,原因和普通通配符使用一样,因为限制的是上限,集合的类型为Number的子类或Number类,不能确定类型,添加了会有安全问题。
下限通配符
下限通配符(Lower Bounded Wildcards)用 super 关键字来声明,其语法形式为 <? super T>
,其中 T 表示类型参数。它表示的是该类型参数必须是某个指定类的超类(包括该类本身)。
当我们需要往一个泛型集合中添加元素时,如果使用的是上限通配符,集合中的元素类型可能会被限制,从而无法添加某些类型的元素。但是,如果我们使用下限通配符,可以将指定类型的子类型添加到集合中,保证了元素的完整性。
因为下限通配符限定了类型为 T 或他的父类,T和T的父类都可以接受T的子类,即向上转型。
举个例子,假设有一个类 Animal,以及两个子类 Dog 和 Cat。现在我们有一个 List<? super Dog>
集合,它的类型参数必须是 Dog 或其父类类型。我们可以向该集合中添加 Dog 类型的元素,也可以添加它的子类。但是,不能向其中添加 Cat 类型的元素,因为 Cat 不是 Dog 的子类。
下面是一个使用下限通配符的示例:
List<? super Dog> animals = new ArrayList<>();
// 可以添加 Dog 类型的元素和其子类型元素
animals.add(new Dog());
animals.add(new Bulldog());
// 不能添加 Cat 类型的元素
animals.add(new Cat()); // 编译报错
需要注意的是,虽然使用下限通配符可以添加某些子类型元素,但是在读取元素时,我们只能确保其是 Object 类型的,无法确保其是指定类型或其父类型。因此,在读取元素时需要进行类型转换,如下所示:
List<? super Dog> animals = new ArrayList<>();
animals.add(new Dog());
// 读取元素时需要进行类型转换
Object animal = animals.get(0);
Dog dog = (Dog) animal;
总的来说,Java 的泛型机制是一种非常强大的类型约束机制,可以在编译时检查类型安全性,并提高代码的复用性和可读性。但是,在使用泛型时也需要注意类型擦除和通配符等问题,以确保代码的正确性。
类型擦除
什么是类型擦除?即虚拟机不存在泛型,在类文件被编译成字节码时,会将泛型的类型变量擦除,并替换为限定类型(没有限定的话,就用 Object
)
我们怎么确定虚拟机没有泛型呢?将类文件编译后的class文件进行反编译就可以了。
编译前类文件:
class Arraylist<E> {
private Object[] elementData;
private int size = 0;
public Arraylist(int initialCapacity) {
this.elementData = new Object[initialCapacity];
}
public boolean add(E e) {
elementData[size++] = e;
return true;
}
E elementData(int index) {
return (E) elementData[index];
}
}
反编译后的文件:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: Arraylist.java
package com.qg
import java.util.Arrays;
class Arraylist
{
public Arraylist(int initialCapacity)
{
size = 0;
elementData = new Object[initialCapacity];
}
public boolean add(Object e)
{
elementData[size++] = e;
return true;
}
Object elementData(int index)
{
return elementData[index];
}
private Object elementData[];
private int size;
}
大家可以看到 泛型 E
消失了,取而代之的是Object
。
既然如此,那如果泛型类使用了限定符 extends
,结果会怎么样呢?
结果是 类中的E都会被替换为Number。
那泛型擦除会给我们带来什么问题吗?
当然也会有
public class Cmower {
public static void method(Arraylist<String> list) {
System.out.println("Arraylist<String> list");
}
public static void method(Arraylist<Date> list) {
System.out.println("Arraylist<Date> list");
}
}
在以上这种情况下,我们会认为两个方法的参数是不同类型,方法可以编译通过,但因为类型擦除的存在,两个方法的参数被编译后类型都是object,编译器会出现编译异常,即无法通过编译。
欢迎大家指正!