【泛型】JAVA基础篇(三)
泛型(Generics)是Java编程语言中的一个强大的特性,它提供了编译时类型安全检测机制
,这意味着可以在编译期间检测到非法的类型。泛型的使用减少了程序中的强制类型转换和运行时错误的可能性。
一、泛型使用规范
- 类型参数命名约定:通常使用单个大写字母来表示类型参数,例如 E 表示集合的元素类型,K 和 V 分别代表键和值的类型,T 通常代表"类型"(Type)。
- 泛型类和接口:在定义泛型类或接口时,你应该在类名后面加上尖括号,并在其中声明类型参数。例如:
class Box<T>
。 - 泛型方法:在方法返回类型之前声明类型参数,以使该方法成为泛型方法。如
public <T> void method(T param)。
- 类型通配符:使用 ? 表示未知类型。? extends T 表示接受 T 或其子类(上界通配符),? super T 表示接受 T
或其父类(下界通配符)。 - 类型擦除:泛型信息仅在编译阶段存在,在进入 JVM 前,与泛型有关的类型信息就会被擦除,这个过程称为类型擦除。类型擦除是为了兼容旧版本的Java。
- 避免创建泛型数组:Java 不允许实例化泛型数组,如 new List[] 是非法的。可以通过创建类型为 List[]
的数组然后将其转换为List<T>[]
来绕过这个限制,但这种做法不安全并会引发警告。 - 限制:由于类型擦除,某些操作在泛型中是不允许的,比如:判断 (instanceof) 泛型类型、创建泛型实例 (new
T())、创建泛型的数组。
二、泛型的基本用法
1、泛型类
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
// 实例化一个泛型类
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
2、泛型接口
// 定义一个泛型接口
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
// 实现泛型接口
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
// 使用泛型接口
Pair<String, Integer> p1 = new OrderedPair<>("Even", 8);
Pair<String, String> p2 = new OrderedPair<>("hello", "world");
3、泛型方法
// 定义一个泛型方法
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
// 使用泛型方法
Pair<Integer, String> p1 = new OrderedPair<>(1, "apple");
Pair<Integer, String> p2 = new OrderedPair<>(2, "pear");
boolean same = Util.compare(p1, p2);
4、有界类型参数(边界类型)
public <U extends Number> void inspect(U u){
System.out.println("T: " + u.getClass().getName());
}
// 使用有界类型参数
inspect(123);
inspect(12.34);
5、通配符类型
public void printBoxContent(Box<?> box){
System.out.println("The box contains: " + box.get());
}
// 使用通配符类型
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
printBoxContent(integerBox);
printBoxContent(stringBox);
三、无泛型 和 有泛型对比
在引入泛型之前,Java 集合框架没有类型检查,所以所有对象都被当做 Object 类型存储
以下是一个没有使用泛型的例子:
import java.util.ArrayList;
import java.util.List;
public class NonGenericExample {
public static void main(String[] args) {
List integerList = new ArrayList();
// 添加整数到集合
integerList.add(new Integer(10)); // 自动装箱为 Integer
integerList.add(new Integer(20));
// 尝试添加一个字符串到整数列表
// 编译器不会报错,运行时也不会出错
integerList.add("thirty"); // 这是不安全的操作
// 获取元素并进行类型转换
Integer firstNumber = (Integer) integerList.get(0);
Integer secondNumber = (Integer) integerList.get(1);
// 这里尝试将第三个元素转换为 Integer,但它实际上是一个 String
// 这会引发 ClassCastException
try {
Integer thirdNumber = (Integer) integerList.get(2);
} catch (ClassCastException e) {
System.out.println("Error: " + e.getMessage());
}
// 输出正确的整数值
System.out.println("First number: " + firstNumber);
System.out.println("Second number: " + secondNumber);
}
}
在上面的代码中,我们创建了一个非泛型的 ArrayList,并向其中添加了两个 Integer 对象和一个 String 对象。由于没有类型检查,ArrayList 允许我们添加任何类型的对象,而不会在编译时报错。但是,当我们试图将 String 对象转换为 Integer 时,程序会在运行时抛出 ClassCastException,因为这是一个无效的转换。
引入泛型后,我们可以在编译时期进行类型检查,从而避免这种类型安全问题。下面是使用泛型的代码示例:
import java.util.ArrayList;
import java.util.List;
public class GenericExample {
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<Integer>();
// 添加整数到集合
integerList.add(10); // 自动装箱为 Integer
integerList.add(20);
// 下面的代码会在编译时报错,防止了运行时错误
// integerList.add("thirty"); // 编译错误
// 获取元素时不需要进行类型转换
Integer firstNumber = integerList.get(0);
Integer secondNumber = integerList.get(1);
// 输出正确的整数值
System.out.println("First number: " + firstNumber);
System.out.println("Second number: " + secondNumber);
}
}
在这个泛型版本的例子中,如果尝试将一个不是 Integer 类型的对象添加到 integerList 中,编译器将会报错,从而保证了类型安全
。这使得代码更安全,更清晰,也更容易维护。
四、泛型擦除
类型擦除是 Java 编译器应用的过程,它允许泛型代码与不支持泛型的旧有 Java 代码兼容
。在这个过程中,编译器将泛型类型参数替换为它们的限定边界(如果存在),或者其他情况下替换为 Object。这意味着在编译后的 Java 字节码中,所有的泛型类型信息都会丢失。
1、背景
Java在5.0版本中引入了泛型,这使得开发者能够在编写集合类(如List、Map等)时指定集合中元素的类型,例如 List<String> 或 Map<Integer, String>
。在引入泛型之前,所有的集合中的对象都是 Object 类型,这需要显式的类型转换,并且可能导致运行时错误。
为了保证向后兼容,Java的泛型是通过类型擦除来实现的。这意味着泛型信息只在编译阶段有效,一旦代码被编译,所有泛型类型参数都会被擦除,替换为它们的限定边界类型
(如果有的话)或者Object
。这也意味着在运行时,我们无法获取到泛型的类型参数信息。
2、泛型擦除实例
public class GenericErasure<T> {
public void doSomething(T t) {
System.out.println(t);
}
}
// 在编译后的class文件中,geString 和 geInteger 类型实际上是相同的
// 它们都被擦除成了 GenericErasure 类型,使用的是 Object 类型,如下:
public class GenericErasure {
public void doSomething(Object t) {
System.out.println(t);
}
}
如上,编译后 泛型 T 变成了 Object,泛型类型信息丢失,转变成了 Object,使得和不支持泛型的java代码一致。
3、限定边界
在Java泛型中,“限定边界”(Bounded Type)是指对可以使用的泛型的类型参数进行限制。限定边界可以是一个特定的类,或者是一个满足特定接口的任何类型。使用限定边界可以确保传递给泛型类型的类型参数满足某些基本要求,这样就可以在类或方法内安全地调用定义在边界类型上的方法。
限定边界有两种类型:
- 上界限定(Upper Bounded Wildcards):? extends Type 表示参数化类型可能是指定的类型,或者是此类型的子类。
- 下界限定(Lower Bounded Wildcards):? super Type 表示参数化类型可能是指定的类型,或者是此类型的父类。
这里是针对上界限定的一个例子:
假设你有一个类 Animal 和两个子类 Dog 和 Cat。你想要写一个方法,这个方法可以接受 Animal 的任何子类的列表。你可以通过使用限定边界来实现:
public class Animal {
public void feed() {
// ...
}
}
public class Dog extends Animal {
// ...
}
public class Cat extends Animal {
// ...
}
public class Main {
public static void feedAnimals(List<? extends Animal> animals) {
for (Animal a : animals) {
a.feed();
}
}
public static void main(String[] args) {
List<Dog> dogs = Arrays.asList(new Dog(), new Dog());
List<Cat> cats = Arrays.asList(new Cat(), new Cat());
feedAnimals(dogs); // 正确:dogs 是 Animal 的子类的 List
feedAnimals(cats); // 正确:cats 是 Animal 的子类的 List
}
}
在这个例子中,feedAnimals 方法的参数 animals 使用了限定边界 ? extends Animal,这意味着这个参数可以是 List、List、List 或任何 Animal 子类的列表。
当你没有具体的限定边界时,你可以简单地使用 Object 类型,因为在Java中所有的类都是 Object 的子类。如果你的泛型没有指定上界或下界,它其实隐式的被限定为 Object。例如:
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
在这个例子中,printList 方法接受一个未知类型的列表,并且由于每个对象最终都继承自 Object 类,我们可以安全地将列表中的每个元素作为 Object 输出。
总结:
限定边界用来指定泛型类型参数必须继承自特定的父类,或者实现特定的接口,从而在编译时期就保证了类型安全。
如果没有特定的限定边界,泛型类型默认是 Object。
4、结论
泛型:提供了编译时类型安全检测机制,预防了运行时报错异常问题。
泛型擦除:保证了泛型代码与不支持泛型的旧有 Java 代码兼容