1. 什么是泛型?
泛型(Generics)是 Java 引入的一种特性,允许你在定义类、接口或方法 时使用类型参数(Type Parameters) 。这些类型参数在编译时会被替换为具体的实际类型(Actual Type Arguments)。通俗地说,泛型让你可以编写能够处理多种数据类型的代码,而无需为每一种数据类型都重写一遍相似的逻辑,同时还能在编译时提供更强的类型检查。
泛型的本质:把具体的数据类型作为参考传给类型变量
2. 为什么需要泛型?
在泛型出现之前,Java 主要使用 Object 类型来实现代码的通用性(例如 ArrayList 可以存储任何对象)。但这带来了两个主要问题:
- 类型不安全: 从集合中取出元素时,需要进行强制类型转换(
(String) list.get(0))。如果转换错误(例如集合里存的是Integer,你却转成了String),会在运行时抛出ClassCastException。 - 繁琐的强制转换: 代码中需要大量显式的类型转换,降低了代码的可读性和易用性。
泛型的主要目的就是解决这些问题:
- 类型安全: 编译器可以在编译时检查你放入集合的元素类型是否符合预期。例如,一个
ArrayList<String>只能存放String对象。尝试放入其他类型会在编译时报错。 - 消除强制转换: 编译器知道集合中元素的类型,取出元素时无需手动强制转换。
- 代码复用: 编写一次泛型类或方法,就可以用于多种不同的数据类型。
3. 泛型的使用
3.1 泛型类
在类名后面使用尖括号 < > 来声明类型参数。类型参数通常用单个大写字母表示,常见的约定有:
-
T- 表示类型(Type) -
E- 表示元素(Element),常用于集合类 -
K- 表示键(Key) -
V- 表示值(Value) -
N- 表示数字(Number) -
S,U,V等 - 第二、第三、第四种类型修饰符 class 类名<类型变量,类型变量,...>{
}
public class ArrayList<E>{
}
类型变量常用的E、T、K、V建议大写!
java
// 定义一个简单的泛型类 Box
public class Box<T> {
private T content; // 使用 T 作为成员变量类型
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用
Box<String> stringBox = new Box<>(); // 创建时指定 T 为 String
stringBox.setContent("Hello World"); // OK
String str = stringBox.getContent(); // 无需强制转换
Box<Integer> integerBox = new Box<>(); // 创建时指定 T 为 Integer
integerBox.setContent(42); // OK, 自动装箱
int num = integerBox.getContent(); // 自动拆箱
// integerBox.setContent("abc"); // 编译错误!不能放 String
3.2 泛型接口
接口也可以定义类型参数。
修饰符 interface 接口名<类型变量,类型变量,...>{
}
public interface A<E>{
}
类型变量常用的E、T、K、V建议大写!
java
public interface Pair<K, V> {
K getKey();
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;
}
@Override
public K getKey() { return key; }
@Override
public V getValue() { return value; }
}
// 使用
Pair<String, Integer> pair = new OrderedPair<>("Age", 30);
String key = pair.getKey();
Integer value = pair.getValue();
3.3 泛型方法
方法也可以有自己的类型参数,写在返回值类型之前。泛型方法可以在普通类、泛型类或接口中定义。
修饰符<类型变量,类型变量,...>返回值类型 方法名(形参列表){
}
public static<T> void test(T,t){
}
java
public class Util {
// 泛型方法:交换数组中两个元素的位置
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 泛型方法:返回两个参数中较大的一个(要求参数实现 Comparable)
public static <T extends Comparable<T>> T max(T a, T b) {
return (a.compareTo(b) > 0) ? a : b;
}
}
// 使用
Integer[] intArray = {1, 2, 3, 4, 5};
Util.swap(intArray, 0, 4); // 交换第一个和最后一个元素
Integer maxVal = Util.max(10, 20); // 返回 20
String maxStr = Util.max("apple", "orange"); // 返回 "orange"
package com.yzdan.genericity.demo04;
import com.yzdan.genericity.demo03.Student;
public class GenericDome04 {
static void main(String[] args) {
String[] names = {"张三","李四","王五"};
printArray(names);
Student[] students = new Student[3];
printArray( students);
Student max = getMax(students);
String maxName = getMax(names);
}
private static <T> T getMax(T[] students) {
}
private static <T> void printArray(T[] names) {
}
}
4. 类型边界(Bounded Type Parameters)
有时你可能希望对类型参数施加限制。例如,你可能希望类型必须是某个类的子类,或者实现了某个接口。这可以通过 extends 关键字来实现。
- 上限(Upper Bound):
<T extends SomeClass>表示T必须是SomeClass或其子类。 - 多个边界(Multiple Bounds):
<T extends ClassA & InterfaceB & InterfaceC>表示T必须继承ClassA并实现InterfaceB和InterfaceC(类只能有一个,接口可以有多个,类写在接口前面)。
java
// 要求 T 必须是 Number 或其子类(如 Integer, Double)
public class NumericBox<T extends Number> {
private T number;
public NumericBox(T number) {
this.number = number;
}
public double square() {
return number.doubleValue() * number.doubleValue();
}
}
// 使用
NumericBox<Integer> intBox = new NumericBox<>(5); // OK
// NumericBox<String> strBox = new NumericBox<>("abc"); // 编译错误!String 不是 Number 子类
Double squared = intBox.square(); // 25.0
5. 通配符(Wildcards)
通配符 ? 用于表示未知的类型,通常用于提高 API 的灵活性,特别是在处理参数化类型时。
- 上界通配符(Upper Bounded Wildcard):
<? extends T>表示类型是T或其子类。适合用于读取数据(生产者 Producer)。 - 下界通配符(Lower Bounded Wildcard):
<? super T>表示类型是T或其父类。适合用于写入数据(消费者 Consumer)。 - 无界通配符(Unbounded Wildcard):
<?>表示任何类型。通常用于不关心具体类型,或只使用Object类中方法的情况。
java
import java.util.List;
public class WildcardExample {
// 方法接收一个 List,其元素类型是 Number 或其子类 (如 Integer, Double)
//方法可以接收接口作为参数。这是因为Java支持多态(polymorphism):
//接口(如 List)定义了行为规范,方法参数可以声明为接口类型。
//在调用方法时,你可以传递任何实现了该接口的类的对象。
//List 是一个泛型接口,代表一个有序的集合(例如,可以存储多个元素)。
//<? extends Number> 是泛型约束,表示 List 中的元素必须是 Number 类或其子类(如 Integer 或 Double)。
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// 方法接收一个 List,其元素类型是 Integer 或其父类 (如 Number, Object)
// 可以向这个列表添加 Integer 对象
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
}
// 使用
List<Integer> intList = List.of(1, 2, 3);
double intSum = WildcardExample.sumOfList(intList); // 6.0
List<Double> doubleList = List.of(1.5, 2.5, 3.5);
double doubleSum = WildcardExample.sumOfList(doubleList); // 7.5
List<Number> numberList = new ArrayList<>();
WildcardExample.addIntegers(numberList); // OK, 添加了 1, 2, 3
// WildcardExample.addIntegers(intList); // 也可以,但通常下界用于更通用的容器
6. 类型擦除(Type Erasure)
Java 的泛型是在编译时实现的特性,称为类型擦除。这意味着:
- 编译器在编译时会检查泛型类型的使用是否正确(类型安全)。
- 编译完成后,所有的泛型类型信息都会被擦除。泛型类中的类型参数会被替换为它们的边界 (如果指定了边界,例如
<T extends Number>则替换为Number)或Object(如果没有指定边界)。 - 在运行时,
ArrayList<String>和ArrayList<Integer>实际上是同一个类ArrayList。
java
// 编译前
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
// 编译后(类型擦除后的近似表示)
Box stringBox = new Box(); // Box 类中的 T 被替换为 Object
stringBox.setContent("Hello"); // 参数类型变为 Object
String content = (String) stringBox.getContent(); // 编译器插入的强制转换
类型擦除是 Java 为了保持向后兼容性(兼容没有泛型的旧 JVM 和代码)而采用的设计。它带来了运行时无法获取泛型类型参数信息等限制。
7. 泛型的注意事项
- 不能使用基本类型: 泛型类型参数必须是引用类型。 不能使用
int,double,char等基本类型。需要使用它们的包装类(Integer,Double,Character)。
泛型擦除,即编译后泛型就没用了,所有泛型在编译后都会被擦除,所有类型恢复成Object类型。即对象类型。而对象类型/引用类型不能指向基本数据,只能指向对象!
所以将基本数据包装成对象,即包装类。
1. 包装类
包装类(Wrapper Class)位于
java.lang包中,用于将基本数据类型封装成对象。每个基本数据类型都有对应的包装类:
基本数据类型 包装类 byteByteshortShortintIntegerlongLongfloatFloatdoubleDoublecharCharacterbooleanBoolean2. 基本数据类型 vs 包装类
特性 基本数据类型 包装类 存储方式 栈内存(直接值) 堆内存(对象引用) 默认值 有(如 0)null性能 更高(无对象开销) 略低(对象创建/GC) 用途 局部变量、计算 集合类(如 List<Integer>)、泛型允许 null否 是 方法支持 无 提供实用方法(如 Integer.parseInt())3. 自动装箱与拆箱
Java 5+ 引入了自动装箱(Autoboxing)和拆箱(Unboxing)机制,简化基本类型与包装类的转换:
java// 自动装箱:int -> Integer Integer num = 10; // 自动拆箱:Integer -> int int value = num; // 集合中使用 List<Integer> list = new ArrayList<>(); list.add(1); // 自动装箱 int first = list.get(0); // 自动拆箱
javapackage com.yzdan.genericity.demo05; import com.yzdan.genericity.demo02.MyArrayList; public class GenericDome05 { static void main(String[] args) { //泛型只支持对象类型/引用类型,不支持基本数据类型 MyArrayList<Integer> list = new MyArrayList<>(); //泛型擦除:泛型工作在运行期间,JVM将泛型擦除,只保留类型信息 //手动装箱,将基本数据类型转换为对象类型 Integer it1 = Integer.valueOf(123); Integer it2 = Integer.valueOf(123); System.out.println(it1 == it2);// true, 因为Integer.valueOf()方法会缓存-128~127的数值,范围内只缓存一个对象 Integer it3 = Integer.valueOf(130); Integer it4 = Integer.valueOf(130); System.out.println(it3 == it4);// false, 因为Integer.valueOf()方法不会缓存数值>127的数值,会创建新的对象 //自动装箱,将对象类型转换为基本数据类型 Integer int11 = 123; Integer int12 = 123; System.out.println(int11 == int12);// true, 因为Integer.valueOf()方法会缓存-128~127的数值,范围内只缓存一个对象 } }4.包装类的其他功能
可以把基本类型的数据转换成字符串类型
可以把字符串类型的数值转换成数值本身对应的真实数据类型
java//1.将基本数据类型转换为字符串 int i = 123; String s = i + ""; System.out.println(s); System.out.println(Integer.toString(i)); //2.将字符串转换为基本数据类型 String s1 = "123"; //int i1 = Integer.parseInt(s1); int i1 = Integer.valueOf(s1);//和Integer.parseInt()方法效果一样 // 区别在于parseInt()只能转换字符串为int,parseDouble()方法可以转换字符串为double,.. // 而valueOf()方法可以转换字符串为int、long、double、float、boolean、char等类型 System.out.println(i1+2);//125
- 不能创建参数化类型的数组:
new List<String>[10];这样的代码在 Java 中是不允许的(编译错误)。可以使用List<String>[] listArray = (List<String>[]) new List[10];来绕过,但这会失去部分类型安全,编译器会警告。通常推荐使用ArrayList的ArrayList(例如ArrayList<ArrayList<String>>) 或者使用通配符List<?>[]。 - 不能实例化类型参数:
new T()是不允许的,因为类型擦除后T可能是Object或没有无参构造函数的类。可以通过反射结合Class<T>参数等方式实现。 - 不能声明静态字段为泛型类型: 类的静态字段是类级别的,所有实例共享。而泛型类型参数是实例级别的(在创建对象时确定)。所以
private static T instance;是错误的。 - 注意类型擦除的影响: 在运行时无法直接获取泛型类的具体类型参数信息(例如
List<String>在运行时只是List)。
总结: Java 泛型是一个强大的工具,它极大地提高了代码的类型安全性、可读性和复用性。理解泛型类、泛型方法、类型边界、通配符以及背后的类型擦除机制,对于编写健壮、灵活的 Java 代码至关重要。