作为Java中最富有争议的语言特性之一,泛型自从在JDK 1.5中被引入以来,就被热烈讨论和广泛应用。很多开发者为之称赞,认为它让代码更安全、更可读。但另一些开发者则觉得泛型语法晦涩难懂,不太受待见。那么泛型到底是造物还是弊端?让我们一探究竟!
一、泛型的作用与定义
泛型的本质是参数化类型,可以在定义类/接口或方法时预留类型位置,在使用时再指定具体的数据类型。
不使用泛型的写法:
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译器不会报错,但是存在类型安全隐患
使用泛型的写法:
List<String> list = new ArrayList<>();
list.add("hello"); // 正确
list.add(123); // 编译器会报错,防止了ClassCastException
1、泛型的作用
- 类型安全:泛型的主要目的是提高类型安全。在没有泛型之前,集合类只能存储 Object 类型的对象,这就需要在取出对象时强制类型转换,这增加了错误的可能性。泛型避免了这种类型转换。
- 消除类型擦除:在 Java 泛型出现之前,集合类使用 Object 类型作为元素类型,这被称为类型擦除。泛型通过在编译时检查类型来消除类型擦除。
- 代码复用:泛型允许开发者编写出可重用的代码。一个泛型类或者接口可以与多种类型一起工作,而不需要为每种类型编写新的类或接口。
- 提高代码的可读性:泛型使得代码更加清晰,因为类型信息在编译时就已经确定,这使得阅读和理解代码更加容易。
2、泛型的定义
泛型可以通过以下几种方式定义:
- 类和接口:在类名或接口名后使用尖括号
<>
来定义类型参数。
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
- 方法:在方法的返回类型前使用尖括号
<>
来定义泛型。
public <E> List<E> getList(E element) {
List<E> list = new ArrayList<E>();
list.add(element);
return list;
}
- 通配符:使用
?
来定义通配符,允许不确定的类型。
public void addAll(List<?> list) {
// 可以添加任何类型的对象到 list 中
}
二、通配符与嵌套
有时会遇到需要使用不确定泛型类型的情况,比如方法的参数或返回值。这时可以使用?
通配符来代替具体的类型。
List<?> list = new ArrayList<String>(); // 合法,? 是任何类型的实例
List<Object> objs = new ArrayList<?>(); // 不合法
还可以使用有限制的通配符,如<? extends Foo>
表示Foo或其子类的实例。
泛型类型也可以嵌套使用,比如Map<String, List<Integer>>
。
三、泛型上下边界
Java 泛型的上下界限定了泛型参数的取值范围,这是为了在提供类型安全性的同时,还能保持一定的灵活性。泛型的上下界限通过使用通配符 ?
来实现,并可以配合使用 extends
和 super
关键字。
1、上界限通配符(Extends)
使用 ? extends T
表示通配符的上界限,意味着通配符代表的类型是 T 或 T 的任何子类。这种用法常用于只能读取数据的场景,因为编译器无法确保可以安全地插入 T 的任何超类类型的实例。
2、下界限通配符(Super)
使用 ? super T
表示通配符的下界限,意味着通配符代表的类型是 T 或 T 的任何超类。这种用法常用于只能添加数据的场景,因为编译器无法确保可以安全地读取 T 的任何子类类型的实例。
3、代码案例
下面是一个使用泛型上界限和下界限的案例:
public class GenericUpperLowerBoundExample {
// 使用上界限通配符的泛型方法,只能读取数据
public static <T> void printListWithUpperBound(List<? extends T> list) {
for (T item : list) {
System.out.println(item);
}
}
// 使用下界限通配符的泛型方法,只能添加数据
public static <T> void addToListWithLowerBound(List<? super T> list, T item) {
list.add(item);
}
public static void main(String[] args) {
// 创建一个 Integer 类型的列表
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
// 创建一个 Number 类型的列表
List<Number> numberList = new ArrayList<>();
numberList.add(1.1);
numberList.add(2.2);
// 使用上界限通配符的泛型方法
// 可以传递 Number 或其子类类型的列表
printListWithUpperBound(intList); // 正确,因为 Integer 是 Number 的子类
printListWithUpperBound(numberList); // 正确
// 使用下界限通配符的泛型方法
// 可以向 Number 或其超类类型的列表中添加元素
Number numberItem = 3.3;
addToListWithLowerBound(numberList, numberItem); // 正确
addToListWithLowerBound(intList, numberItem); // 错误,因为 Number 不是 Integer 的超类
// 打印修改后的列表
System.out.println("Modified numberList contains: " + numberList);
}
}
在这个例子中:
printListWithUpperBound
方法使用上界限通配符? extends T
,它只能用于读取操作,因为列表中的元素是 T 或 T 的子类,所以可以安全地读取。addToListWithLowerBound
方法使用下界限通配符? super T
,它只能用于添加操作,因为列表可以接受 T 或 T 的超类,所以可以安全地添加 T 类型的实例。
四、RxJava中的泛型应用
RxJava广泛应用了泛型编程,其中Observable和Function类都使用了嵌套的泛型类型。
Observable.just("hello")
.map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
})
.subscribe(new Consumer<Integer>() {
@Override
public void accept(Integer len) {
System.out.println(len);
}
});
这个例子中,Observable的泛型参数是String,map操作符的Function的泛型是<String, Integer>
(输入String,输出Integer),subscribe的Consumer是<Integer>
。通过泛型的使用,数据流的类型被明确定义,提高了代码的安全性和可读性。
五、泛型的注意事项
- 静态方法/字段
静态方法或字段不能引用泛型类型参数,因为它们在加载时就需要确定类型。可以使用通配符代替。
- 泛型擦除
泛型只存在于编译期,在运行时会被擦除掉。所有的泛型参数都会被替换为Object类型,为了保证类型安全会执行必要的类型转换。了解泛型擦除后的真实字节码,有助于理解泛型背后的工作机制。
- 桥接方法
为了实现泛型的多态,编译器会生成桥接方法(Bridge Method)执行必要的类型擦除和转换。桥接方法可能会对性能造成一些影响,需要注意。
- 类型推断
从Java 7开始,可以让编译器自动推断泛型的类型参数,这被称为"钻石操作符"。比如List<String> list = new ArrayList<>()
可以简写为List<String> list = new ArrayList<>()
。
以上就是本文的全部内容,希望对你有所启发。欢迎在评论区分享你在使用Java泛型中的心得和体会,让我们共同进步!