Java基础系列
目录
为什么需要泛型?
Java引入泛型(Java SE 5)的主要原因是为了提高类型安全性 和代码复用性。在泛型出现之前,Java程序员通常会使用 Object 类型或者一组相关的具体类型来编写集合类和其他可重用组件。这种方式的问题在于缺乏类型安全性和额外的类型转换需求。以下是引入泛型的一些主要原因:
- 类型安全性 :在使用非泛型集合时,编译器不会检查你放入集合中的元素类型是否与取出时一致。这可能导致运行时的
ClassCastException
。而使用泛型后,编译器会在编译期就确保类型的安全性。 - 消除强制类型转换:在非泛型时代,从集合中获取元素时往往需要显式地进行类型转换,这不仅增加了代码量,还容易出错。泛型则允许编译器自动处理类型转换。
- 更好的可读性和可维护性:使用泛型可以清楚地表明集合中存储的对象类型,使得代码更容易阅读和理解。此外,当类型信息在编译时就被确定下来,也有助于减少错误并简化调试过程。
- 代码复用:泛型允许编写通用的类和方法,能够处理多种数据类型,这样可以减少重复代码的编写,并且让程序更加灵活。
- 静态类型检查:泛型提供了一种静态类型检查机制,使得错误能够在编译阶段被发现,而不是等到运行时才暴露出来。
泛型的用法
Java 泛型的用法非常广泛,主要集中在以下几个方面:泛型类、泛型方法、通配符、泛型接口和类型限定。下面我将详细解释每个方面的用法,并给出相应的代码示例。
泛型类
泛型类允许你在定义类时使用类型参数,从而使得这个类可以处理多种数据类型。
示例:泛型类
//一套代码可以处理多种数据类型,实现代码的复用
public class Box<T> {
private T item;
public Box(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
public class Main {
public static void main(String[] args) {
// 创建一个 Integer 类型的 Box
Box<Integer> intBox = new Box<>(10);
System.out.println("Integer Box: " + intBox.getItem());
// 创建一个 String 类型的 Box
Box<String> stringBox = new Box<>("Hello");
System.out.println("String Box: " + stringBox.getItem());
}
}
泛型接口
泛型接口允许你在定义接口时使用类型参数,从而使得实现该接口的类可以处理多种数据类型。
示例:泛型接口
public interface Container<T> {
void add(T item);
T get(int index);
}
public class ArrayListContainer<T> implements Container<T> {
private List<T> items = new ArrayList<>();
@Override
public void add(T item) {
items.add(item);
}
@Override
public T get(int index) {
return items.get(index);
}
}
public class Main {
public static void main(String[] args) {
Container<String> stringContainer = new ArrayListContainer<>();
stringContainer.add("Hello");
stringContainer.add("World");
System.out.println(stringContainer.get(0)); // 输出: Hello
System.out.println(stringContainer.get(1)); // 输出: World
Container<Integer> intContainer = new ArrayListContainer<>();
intContainer.add(1);
intContainer.add(2);
System.out.println(intContainer.get(0)); // 输出: 1
System.out.println(intContainer.get(1)); // 输出: 2
}
}
泛型方法
泛型方法允许你在方法签名中使用类型参数,从而使得同一个方法可以处理多种数据类型。
示例:泛型方法
public class Util {
// 泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// 带返回值的泛型方法
public static <T> T getFirstElement(T[] array) {
if (array.length > 0) {
return array[0];
}
return null;
}
}
public class Main {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
Util.printArray(intArray); // 输出: 1 2 3 4 5
System.out.println("First element: " + Util.getFirstElement(intArray)); // 输出: First element: 1
String[] stringArray = {"Hello", "World"};
Util.printArray(stringArray); // 输出: Hello World
System.out.println("First element: " + Util.getFirstElement(stringArray)); // 输出: First element: Hello
}
}
通配符
通配符 ?
表示未知类型,可以用于泛型方法的参数类型。通配符可以分为无界通配符、上界通配符和下界通配符。
示例:通配符
public class Util {
// 无界通配符,可以是任何类型
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// 上界通配符,必须是Number的子类
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.print(num + " ");
}
System.out.println();
}
// 下界通配符,必须是Integer的父类
public static void addNumbers(List<? super Integer> list) {
list.add(10);
}
}
public class Main {
public static void main(String[] args) {
List<String> stringList = Arrays.asList("Hello", "World");
Util.printList(stringList); // 输出: Hello World
List<Integer> intList = Arrays.asList(1, 2, 3);
Util.printNumbers(intList); // 输出: 1 2 3
List<Number> numberList = new ArrayList<>();
Util.addNumbers(numberList);
System.out.println(numberList); // 输出: [10]
List<Object> objectList = new ArrayList<>();
Util.addNumbers(objectList);
System.out.println(objectList); // 输出: [10]
}
}
类型限定
类型限定允许你在定义泛型类或泛型方法时对类型参数进行约束,确保传入的类型满足一定的条件。
示例:类型限定
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
// 带类型限定的方法,上界通配符
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
public class Main {
public static void main(String[] args) {
Pair<String, Integer> pair = new Pair<>("Age", 25);
System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
String maxString = Pair.max("Apple", "Banana");
System.out.println("Max String: " + maxString); // 输出: Max String: Banana
Integer maxInt = Pair.max(10, 20);
System.out.println("Max Integer: " + maxInt); // 输出: Max Integer: 20
}
}
总结
通过这些示例,你可以看到 Java 泛型在不同类型的应用场景中的具体用法。泛型的使用不仅提高了代码的类型安全性,还增强了代码的复用性和可读性。
类型擦除
什么是类型擦除
类型擦除(Type Erasure)是指在编译时,JVM编译器会将所有的泛型信息都擦除掉,变成原始类型。具体来说,就是将泛型的类型参数替换成具体类型的上限或下限(如果没有指定上界,则默认为Object)。这样,虽然我们在代码中使用了泛型,但在编译后,所有的泛型类型都会被擦除掉,转而使用其对应的原始类型。
为何需要类型擦除
- **兼容性:**Java在引入泛型之前(JDK1.5及之前版本)是没有泛型概念的。为了与之前的JDK版本保持兼容,Java引入了类型擦除的概念。
- **效率:**如果为每个泛型类型都生成不同的目标代码,会导致代码膨胀和内存占用增加。类型擦除可以避免这个问题,因为它使得所有使用泛型的类在编译后都使用相同的字节码。
如何类型擦除
- **无泛型上界:**如果定义泛型的地方没有指定泛型上界,则所有该泛型类型的变量的数据类型在编译之后都替换为Object。
- **有泛型上界:**如果定义泛型的地方指定了泛型上界,则所有该泛型类型的变量的数据类型在编译之后都替换为泛型上界。
类型擦除的验证
- **通过Class对象验证:**使用Class对象的getTypeParameters方法获取的泛型信息是占位符(如T、K、V等),而不是实际的泛型类型。
- **通过反射机制验证:**泛型约束只在编译阶段有效,在编译之后泛型就被擦除了。因此,如果可以绕过编译阶段对泛型的约束检测,就可以传入任何类型的变量(因为都可以向上转型为Object类型)。
类型擦除的影响与限制
- 无法获取泛型类型信息: 由于类型擦除,运行时无法获取泛型类型参数的信息。例如,无法通过反射获取Box<Integer>中的Integer类型信息。再例如,
instanceof
操作符不能用于泛型类型参数,if (box instanceof Box<Integer>)
会编译失败,因为Box<Integer>
在运行时被擦除为Box
。 - 不能创建泛型类型的数组 :由于类型擦除,你不能创建具有泛型元素的数组。例如,
new Box<Integer>[10]
会编译失败。 - 不能实例化类型参数 :你不能在泛型类或方法中直接实例化类型参数。例如,
new T()
会编译失败。 - **类型转换:**编译器在编译时会进行类型检查和转换,以确保类型安全。然而,运行时类型转换错误仍然可能发生,这通常是由于不正确的类型假设导致的。
- **桥方法的生成:**为了确保多态性,编译器可能会生成桥方法。这些方法在子类中提供与父类中被类型擦除的方法相同的方法签名。
类型擦除的补偿机制
尽管类型擦除有一些限制,但Java提供了反射机制来操作泛型类型,使得泛型类型在某些情况下还是可以被获取到的。此外,Java还通过编译时的类型检查和转换来确保类型安全。
示例讲解
泛型类类型擦除
以下是一个简单的泛型类示例,以及编译后类型擦除的结果:
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
在编译后,这个泛型类的类型参数T会被擦除,变成其对应的原始类型Object:
public class Box {
private Object value;
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
使用泛型类的代码在编译后也会进行相应的类型转换:
Box<Integer> intBox = new Box<>();
intBox.set(10);
Integer value = intBox.get();
编译后变成:
Box intBox = new Box();
intBox.set(10);
Integer value = (Integer) intBox.get(); // 插入类型转换
泛型方法的类型擦除
public class Util {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
编译后的字节码(伪代码):
public class Util {
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
桥接方法
桥接方法是编译器为了确保多态性而自动生成的方法。这些方法在字节码中存在,但在源代码中不可见。
public class Box<T> {
public void setItem(T item) {
// 方法体
}
}
public class IntegerBox extends Box<Integer> {
@Override
public void setItem(Integer item) {
// 重写的方法体
}
}
编译后的字节码(伪代码):
public class Box {
public void setItem(Object item) {
// 方法体
}
}
public class IntegerBox extends Box {
// 桥接方法
public void setItem(Object item) {
setItem((Integer) item);
}
// 重写的方法
public void setItem(Integer item) {
// 重写的方法体
}
}
总结
本文主要讲解了为什么需要泛型,以及泛型的一些常用用法,比如泛型类、泛型接口、泛型方法、通配符等,并且给出了这些用法的代码案例。同时也介绍了泛型的类型擦除机制,讲述了为什么要类型擦除以及编译的时候是怎么做类型擦除的,同时因为类型擦除,也给出了一些泛型的使用限制。