九、泛型
一、是什么?
1.语言特征
泛型(Generics)是Java 5引入的一项语言特征,它允许在定义类、接口和方法时使用类型参数。
2.主要目的
- 提供类型安全性和代码重用性
- 减少类型转换和运行时错误。
二、怎么用?
1.泛型的语法
泛型类和泛型方法的定义通常使用尖括号<>
来指定参数。常见的类型参数表示如下:
E
(Element)——集合元素k
(Key)——键v
(Value)——值T
(Type)——类型N
(Number)——数值
示例
泛型类
// 泛型类
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
泛型方法
// 泛型方法
public class GenericsMethodTest {
public static void main(String[] args) {
/**
* reason: no instance(s) of type variable(s) T exist so that int[] conforms to T[]
* resource:Java编译器
* 通常出现在尝试将基本类型数组(如`int[]`)与泛型类型(如`T[]`)混合使用时。
* 泛型在Java中是类型擦除的,这意味着泛型类型在运行时不存在,并且所有类型参数都被擦除为它们的上界(默认`object`)
*/
// int[] nums = {1, 2, 3, 4, 5};
//故此这样使用包装类才正确
Integer[] nums = {1, 2, 3, 4, 5};
printArray(nums);
}
//***
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
实际使用
类使用
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Integer value = integerBox.get();
//输出10
System.out.print(value)
方法使用
public class GenericMethodTest {
// 泛型方法
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String args[]) {
Integer[] intArray = { 1, 2, 3, 4, 5 };
// 输出 1 2 3 4 5
printArray(intArray);
}
}
三、为什么要用?
简而言之,泛型使类型(类和接口)能够在定义类、接口和方法时成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种通过不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。
使用泛型的代码比非泛型代码有很多好处
-
编译时更强的类型检查。
Java 编译器对泛型代码应用强类型检查,如果代码违反类型安全,则会发出错误。修复编译时错误比修复运行时错误更容易,后者很难发现。 -
消除强制转换
以下没有泛型的代码片段需要强制转换:
List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0);
-
当重写为使用泛型时,代码不需要转换:
List list = new ArrayList();
list.add(“hello”);
String s = list.get(0); // no cast
> 使程序员能够实现通用算法。
通过使用泛型,程序员可以**实现适用于不同类型集合的泛型算法**,可以自定义,并且**类型安全且更易于阅读**。
### 四、实现原理
#### 内部实现原理
##### 1. 类型擦除(Type Erasure)
Java泛型在编译时会进行类型擦除,所有的泛型信息在编译后都会被擦除,泛型参数被替换为它们的边界类型(通常是 `Object`)。这意味着:
- 泛型类和泛型方法在运行时不保留具体的类型信息。
- 不能在运行时实例化泛型类型的对象(如 `new T()` 是不允许的)。
> 示例
```java
public class Box<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
在编译之后,类型 T
会被擦除,生成如下代码:
public class Box {
private Object t;
public void set(Object t) { this.t = t; }
public Object get() { return t; }
}
问答
问:既然Java泛型在编译时会进行类型擦除,所有的泛型信息在编译后都会被擦除。那么指定泛型是怎么实现的呢?
Java泛型在编译时确实会进行类型擦除(Type Erasure),这意味着在运行时,泛型类型的信息会被移除。然而,Java泛型仍然在编译时提供类型安全和灵活性:
类型擦除的具体实现步骤:
-
类型替换:编译器会将泛型类型参数替换为其边界。
例如
List<T>
中的T
:-
如果没有边界限制,会被替换为
Object
; -
如果有边界限制,例如
List<T extands Number>
,则T
会替换为Number
。
-
-
插入类型检查和类型转换:编译器会在必要的地方插入类型检查和类型转换代码。
例如,对于
List<String> list = new ArrayList<>();
,在向list
添加元素时,编译器会插入类型检查代码。 泛型通过在编译期间进行类型检查和插入类型转换代码来实现类型安全。尽管在运行时类型信息被擦除,但编译器通过类型擦除和类型转化确保了泛型的正确性和安全性。在实际使用中,泛型提供了强大的类型检查机制,提高了代码灵活性和可维护性!
2. 限定通配符(Bounded Wildcards)
通过使用限定通配符,可以限制泛型的类型参数。主要有三种类型:
- 无限定通配符
<?>
:表示任意类型。 - 上限限定通配符
<? extends T>
:表示类型必须是T
或T
的子类型。 - 下限限定通配符
<? super T>
:表示类型必须是T
或T
的超类型。
示例:
// 只能接收 Number 或 Number 的子类型
public void processNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
3. 泛型和反射
由于类型擦除,Java中的泛型和反射结合使用时需要特别注意,通常需要使用反射方法时的实际类型信息。
示例:
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class ReflectionGenericTest {
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
list.add("Hello");
Method method = list.getClass().getMethod("add", Object.class);
method.invoke(list, 123);
System.out.println(list); // 输出 [Hello, 123]
}
}
上述代码中,由于类型擦除,List<String>
在运行时被视为 List<Object>
。
4.泛型的优缺点
优点
- 类型安全:在编译时进行类型检查,减少了运行时类型错误。
- 代码重用:通过泛型可以编写更通用、更复用的代码。
- 减少强制类型转换:避免了繁琐和易错的强制类型转换。
缺点
- 复杂性增加:泛型的引入增加了语言的复杂性,可能不易理解。
- 运行时类型擦除:泛型信息在运行时被擦除,限制了某些操作(如实例化泛型类型对象)。
五、总结
总结,Java中的泛型是一种强大且灵活的工具,虽然其实现细节(如类型擦除)带来了一些限制和复杂性,但它显著提高了代码的类型安全性和可重用性。理解和正确使用泛型是成为高级Java工程师的重要一步。