跳到主要内容Java 泛型:编译期类型安全与实战应用 | 极客日志Javajava
Java 泛型:编译期类型安全与实战应用
Java 泛型通过参数化类型实现编译期检查,减少强制转换和运行时错误。本文解析泛型类、接口及方法的定义语法,探讨类型擦除机制对字节码的影响,以及通配符(?)的上限与下限约束逻辑。结合数组存储、集合操作等实战案例,说明如何编写高复用性且类型安全的代码,避免常见陷阱如泛型数组实例化问题。掌握泛型边界与推导规则,能显著提升 Java 程序的健壮性与可维护性。
beaabea2 浏览 什么是泛型
**泛型(Generics)**是 Java 编程语言中引入的强大特性,它提供了编译时的类型安全检测机制。这意味着编译器可以在编译期间检测到非法的类型操作,从而减少程序中的强制类型转换和运行时错误的可能性。
在泛型出现之前,一般的类和方法只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚很大。因此,JDK 1.5 引入了泛型语法。
通俗来讲,泛型就是适用于许多类型。从代码上讲,就是对类型实现了参数化。
引出泛型
假设我们需要实现一个类,类中包含一个数组成员,希望数组中可以存放任何类型的数据,同时也能根据下标返回特定元素。以前我们通常会将数组定义为 Object 类型。
class MyArray {
public Object[] array = new Object[10];
public Object getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, Object val) {
this.array[pos] = val;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray myArray = new MyArray();
myArray.setVal(0, 10);
myArray.setVal(1, "hello");
String ret = (String) myArray.getPos(1);
System.out.println(ret);
}
}
虽然上述代码能运行,但存在明显问题:
- 任何类型数据都可以存放,失去了类型约束。
获取数据时必须进行强制类型转换,容易引发 ClassCastException。我们希望容器只能够持有一种数据类型,让编译器去做检查。此时就需要把类型作为参数传递。需要什么类型,就传入什么类型。
泛型类
定义与语法
定义类时,在类名后加上用尖括号括起来的类型形参,这个类就是泛型类。
- 创建泛型类的实例对象时传入不同的类型实参,就可以动态生成任意多个该泛型类的子类。
- JDK 类包中泛型类最典型的应用就是各种容器类,如
ArrayList、HashMap 等。
class 泛型类名称 < 类型形参列表 > {
}
class 泛型类名称 < 类型形参列表 > extends 继承类 {
}
泛型类 < 类型实参 > 变量名;
new 泛型类 < 类型实参 > (构造方法实参);
类名 <类型形参变量> 是一个整体的数据类型,通常称为泛型类型。为了提高可读性,建议使用有意义的字母:
- E: Element(元素),常用在 Collection 中,如
List<E>。
- K, V: Key 和 Value(Map 的键值对)。
- N: Number(数字)。
- T: Type(类型),如
String、Integer 等。
注意:泛型只能接受类,所有的基本数据类型必须使用包装类。
改进示例
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public T getPos(int pos) {
return this.array[pos];
}
public void setVal(int pos, T val) {
this.array[pos] = val;
}
}
public class TestDemo {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>();
myArray.setVal(0, 10);
myArray.setVal(1, 12);
int ret = myArray.getPos(1);
System.out.println(ret);
}
}
- 类名后的
<T> 代表占位符,表示当前类是一个泛型类。
- 实例化时加入
<Integer> 指定当前类型,或者使用菱形运算符 <> 进行类型推导。
- 获取数据时不需要强制类型转换,编译器已保证类型安全。
- 如果尝试存入不兼容的类型,编译器会在编译阶段报错。
对于集合的使用也是同理,创建集合时传入对应的类型即可:
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
for (int i = 0; i < intList.size(); i++) {
Integer num = intList.get(i);
}
}
类型推导
当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写,这就是类型推导(Type Inference)。
MyArray<Integer> list = new MyArray<>();
自定义泛型类演示
class Goods<T> {
private T info;
public Goods(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
}
public class Example {
public static void main(String[] args) {
Goods<Integer> goods = new Goods<>(666);
System.out.println(goods.getInfo() + "..." + goods.getInfo().getClass());
goods.setInfo("热卖商品");
System.out.println(goods.getInfo() + "..." + goods.getInfo().getClass());
}
}
由输出可以看出:属性 info 的值在初始化为 666 时,类型为 Integer;当调用 setInfo("热卖商品") 之后,由于泛型类实例化时确定了类型,此处实际上会报错或保持原类型(取决于具体实现逻辑,但在本例中若未重新声明泛型类型则无法改变 T 的实际绑定)。更准确的理解是:类型形参会根据类型实参来确定。一旦实例化完成,T 就被固定了。
泛型是如何编译的?
类型擦除
Java 的泛型机制是在编译级别实现的。通过命令 javap -c 查看字节码文件,可以看到所有的 T 都被替换为了 Object。
在编译过程中,将所有的 T 替换为 Object 这种机制,我们称为擦除机制。
- 编译器生成的字节码在运行期间并不包含泛型的类型信息。
- 这解释了为什么泛型只能在编译期提供类型检查,而不能在运行时获取泛型的具体类型。
泛型类型数组为什么不能实例化
class MyArray<T> {
public T[] array = (T[]) new Object[10];
public T getPos(int pos) { return this.array[pos]; }
public T[] getArray() { return array; }
}
public class Example {
public static void main(String[] args) {
MyArray<Integer> myArray = new MyArray<>();
Integer[] num = myArray.getArray();
}
}
原因: 替换后的方法签名实际上是 public Object[] getArray()。将 Object[] 分配给 Integer[] 引用,程序报错。
通俗讲就是:返回的 Object 数组里面,可能存放的是任何的数据类型,可能是 String,可能是 Person,运行的时候,直接转给 Integer 类型的数组,编译器认为是不安全的。
class MyArray<T> {
public T[] array;
public MyArray(Class<T> clazz, int capacity) {
array = (T[]) Array.newInstance(clazz, capacity);
}
public T getPos(int pos) { return this.array[pos]; }
public void setVal(int pos, T val) { this.array[pos] = val; }
public T[] getArray() { return array; }
}
泛型接口
概述
定义泛型接口和定义泛型类的语法格式类似,在接口名称后面加上用尖括号括起来类型形参即可。与集合相关的很多接口也是泛型接口,如 Collection、List 等。
【访问权限】interface 接口名称 <类型形参变量> ()
interface Info<T> {
public T getVar();
}
- 使用非泛型类实现泛型接口。
- 使用泛型类实现泛型接口。
非泛型类实现泛型接口
当使用非泛型类实现接口时,需要明确接口的泛型类型,也就是需要将类型实参传入接口。
public interface Inter<T> {
public abstract void show(T t);
};
public class InterImpl implements Inter<String> {
@Override
public void show(String s) {
System.out.println(s);
}
}
public static void main(String[] args) {
Inter<String> inter = new InterImpl();
inter.show("Hello Island1314");
}
在上面代码中,在接口后面传入的类型实参类型为 String。这样,在 InterImpl 实现类中重写 Inter 接口中的 show() 方法时,就需要指明 show() 方法的参数类型为 String。
泛型类实现泛型接口
当使用泛型类实现泛型接口时,需要将泛型的声明加在实现类中,并且泛型类和泛型接口使用的必须是同一个类型形参变量。
public class InterImpl<T> implements Inter<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
public static void main(String[] args) {
Inter<String> inter = new InterImpl<>();
inter.show("IsLand~");
Inter<Integer> i = new InterImpl<>();
i.show(1314);
}
泛型上界
在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。
语法
class 泛型类名称 < 类型形参 extends 类型边界 > { ... }
示例
public class MyArray<E extends Number> { ... }
只接受 Number 的子类型作为 E 的类型实参。
MyArray<Integer> l1;
MyArray<String> l2;
如果没有指定类型边界 E,可以视为 E extends Object。
public class MyArray<E extends Comparable<E>> { ... }
泛型方法
概述
泛型方法是将类型形参的声明放在修饰符和返回值类型之间的方法。在 Java 程序中,定义泛型方法常用的格式如下:
[访问权限修饰符] [static] [final] <类型形参> 返回值类型 方法名 (形参列表) { ... }
- 访问权限修饰符、
static 和 final 都必须写在类型形参的前面。
- 返回值类型必须写在类型形参的后面。
- 泛型方法可以在泛型类中,也可以用在普通类中。
- 泛型类中的任何方法本质上都是泛型方法,所以在实际使用中很少会在泛型类中显式地用上面的形式定义泛型方法。
- 类型形参可以用在方法体中修饰局部变量,也可以修饰方法的返回值。
- 泛型方法可以是实例方法,也可以是静态方法。
泛型方法能提高代码的重用性和程序的安全性。如果设计泛型方法可以取代整个类的泛型化,就应该优先采用泛型方法。
public class Util {
public static <E> void swap(E[] array, int i, int j) {
E t = array[i];
array[i] = array[j];
array[j] = t;
}
}
Integer[] a = {1, 2};
swap(a, 0, 1);
String[] b = {"a", "b"};
swap(b, 0, 1);
Util.<Integer>swap(a, 0, 1);
泛型方法的使用
对象名 | 类名.<类型实参>方法名 (实参列表)
对象名 | 类名.方法名 (类型实参列表)
- 如果泛型方法是实例方法,则需要使用对象名进行调用。
- 如果泛型方法是静态方法,可以使用类名进行调用。
上述两种调用泛型方法的形式的差别在于方法名之前是否显式地指定类型实参。调用时是否需要显式地指定了类型实参,要根据泛型方法的声明形式以及调用时编译器能否从实参列表中获得足够的类型信息决定。如果编译器能够根据实参推断出参数类型,就可以不指定类型实参;反之则需要指定类型实参。
class Student {
public static <T> void staticMethod(T t) {
System.out.println(t + "..." + t.getClass());
}
public <T> void otherMethod(T t) {
System.out.println(t + "..." + t.getClass());
}
}
public class Demo {
public static void main(String[] args) {
Student.staticMethod("staticMethod");
Student.<String>staticMethod("staticMethod");
Student stu = new Student();
stu.otherMethod(666);
stu.<Integer>otherMethod(666);
}
}
我们发现形式一和形式二的输出结果一样。这说明泛型方法可以在非泛型类中定义,并且可以在调用泛型方法的时候确定泛型的具体类型。虽然输出一致,但形式一需要隐式传入类型实参,不能直观地看出调用的方法是泛型方法,不利于代码的阅读和维护。因此,通常建议使用第二种形式调用泛型方法。
类型通配符
一般情况下,创建泛型类的实例对象时,应该为泛型类传入一个类型实参,以确定该泛型类的泛型类型。有时候,使用泛型类或者接口时传递的类型实参是不确定的,使用固定的类型形参接收类型实参存在局限性,此时就可以使用类型通配符接收不同的类型实参。
概述
类型通配符用一个问号(?)表示,类型通配符可以匹配任何类型的类型实参。
class Person<T> {
private T info;
public Person(T info) { this.info = info; }
public void setInfo(T info) { this.info = info; }
public T getInfo(){ return info; }
}
public class Demo {
public static void main(String[] args) {
Person<?> person = new Person<String>("IsLand~");
System.out.println(person.getInfo() + "..." + person.getInfo().getClass());
person = new Person<Integer>(1314);
System.out.println(person.getInfo() + "..." + person.getInfo().getClass());
}
}
可以看出,控制台成功输出了两条信息,说明泛型类 Person 的对象接收了两种不同的类型实参。
注意: 如果创建 Person 对象时不使用类型通配符,而是使用知道的类型实参,则会出现编译时异常。这个时候,可能有些读者会觉得可以使用 Object 来代替类型通配符接收所有类型。那么下面使用 Object 代替上述代码的类型通配符,修改后也出现了编译时异常。
原因: 在泛型中类名和泛型的声明是一个整体类型,Person<Object> 并不是 Person<String> 的父类。
因此我们可以通过使用类型通配符,来接收所有的泛型类型,而且可以不让用户随意修改。
? extends 类:设置通配符上限。
? super 类:设置通配符下限。
类型通配符的限定
前面使用类型通配符的时候,实际上是任意设置的,只要是类就可以设置。但是有时候需要对类型通配符的使用进行限定,主要限定类型通配符的上限和下限。
设定类型通配符的上限
当使用 Person<?> 时,表示泛型类 Person 可以接收所有类型的类型实参。但有时不想让某个泛型类接收所有类型的类型实参,只想接收指定的类型及其子类,这时可以为类型通配符设定上限。
public static void getElement(Collection<? extends Number> coll){}
public static void main(String[] args) {
Collection<Number> l1 = new ArrayList<Number>();
Collection<Integer> l2 = new ArrayList<Integer>();
Collection<String> l3 = new ArrayList<String>();
getElement(l1);
getElement(l2);
getElement(l3);
}
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}
class Message<T> {
private T message;
public T getMessage() { return message; }
public void setMessage(T message) { this.message = message; }
}
public class Demo {
public static void main(String[] args) {
Message<Apple> message = new Message<>();
message.setMessage(new Apple());
fun(message);
Message<Banana> message2 = new Message<>();
message2.setMessage(new Banana());
fun(message2);
}
public static void fun(Message< ? extends Fruit> temp) {
System.out.println(temp.getMessage());
}
}
此时无法在 fun 函数中对 temp 进行添加元素,因为 temp 接收的是 Fruit 和他的子类,此时存储的元素应该是哪个子类无法确定。所以添加会报错!但是可以获取元素。
- 注意:通配符的上界,不能进行写入数据,只能进行读取数据。
设定类型通配符的下限
设定类型通配符时,除了可以设定类型通配符的上限,也可以对类型通配符的下限进行设定。设定类型通配符的下限后,类型实参只能是设定的类型或其父类型。
public static void getElement(Collection< ? super Number> coll) {}
public static void main(String[] args) {
Collection<Number> l1 = new ArrayList<Number>();
Collection<Object> l2 = new ArrayList<Object>();
Collection<Integer> l3 = new ArrayList<Integer>();
getElement(l1);
getElement(l2);
getElement(l3);
}
class Food {}
class Fruit extends Food {}
class Apple extends Fruit {}
class Plate<T> {
private T plate;
public T getPlate() { return plate; }
public void setPlate(T plate) { this.plate = plate; }
}
public class Demo {
public static void main(String[] args) {
Plate<Fruit> plate1 = new Plate<>();
plate1.setPlate(new Fruit());
fun(plate1);
Plate<Food> plate2 = new Plate<>();
plate2.setPlate(new Food());
fun(plate2);
}
public static void fun(Plate< ? super Fruit> temp) {
temp.setPlate(new Apple());
temp.setPlate(new Fruit());
System.out.println(temp.getPlate());
}
}
- 注意:通配符的下界,不能进行读取数据(指强转为具体子类),只能写入数据。
小结
- 泛型是将数据类型参数化,进行传递。
- 使用
<T> 表示当前类是一个泛型类。
- 泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换。
(1)提高类型的安全性
使用泛型后,将类型的检查从运行期提前到编译期。在编译期进行类型检查,可以更早、更容易地找出因为类型限制而导致的类型转换异常,从而提高程序的可靠性。
(2)消除强制类型转换
使用泛型后,程序会记住当前的类型形参,从而无须对传入的实参值进行强制类型转换,使得代码更加清晰和简洁,可读性更高。
(3)提高代码复用性
使用泛型后,可以更好地将程序中通用的代码提取出来,在使用时传入不同类型的参数,避免了多次编写相同功能的代码,提高了代码的复用性。
(4)拥有更高的运行效率
使用泛型前,传入的实际参数值作为 Object 类型传递时,需要进行封箱和拆箱操作,会增加程序运行的开销;使用泛型后,类型形参中都需要使用引用数据类型,即传入的实际参数的类型都是对应的引用数据类型,避免了封箱和拆箱操作,减少了程序运行的开销,提高了程序的运行效率。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online