Java-泛型类的定义与使用
泛型是Java5引入的核心特性之一,它允许类、接口和方法在定义时不指定具体类型,而是在使用时动态指定,泛型类作为泛型的基础应用,能够显著提升代码的复用性和类型安全性。
一、为什么需要泛型类?
在泛型出现之前,开发者通常使用Object
类实现“通用”功能,但这种方式存在明显缺陷。
1.1 传统方式的问题(以容器类为例)
假设我们需要一个“可以存储任何类型的容器类”,传统方式可能这样实现:
// 传统容器类(使用Object存储)
class ObjectContainer {
private Object value;
// 存储数据
public void setValue(Object value) {
this.value = value;
}
// 获取数据(需要强制类型转换)
public Object getValue() {
return value;
}
}
使用该容器时:
public class Test {
public static void main(String[] args) {
ObjectContainer container = new ObjectContainer();
// 存储字符串
container.setValue("Hello");
// 取出时需要强制转换为String
String str = (String) container.getValue();
// 存储整数
container.setValue(123);
// 错误:将Integer强制转换为String,运行时抛出ClassCastException
String num = (String) container.getValue();
}
}
传统方式的缺陷:
- 类型不安全:编译时无法检查类型,错误只能在运行时发现(如
Integer
转String
的异常); - 代码冗余:每次获取数据都需要手动强制类型转换;
- 可读性差:无法通过代码直观判断容器中存储的是什么类型。
1.2 泛型类的优势
泛型类通过“在定义时声明类型参数,使用时指定具体类型”解决上述问题:
// 泛型容器类(T为类型参数)
class GenericContainer<T> {
private T value; // 使用类型参数T定义变量
// 存储数据(参数类型为T)
public void setValue(T value) {
this.value = value;
}
// 获取数据(返回类型为T,无需强制转换)
public T getValue() {
return value;
}
}
使用泛型容器:
public class Test {
public static void main(String[] args) {
// 1. 指定存储String类型
GenericContainer<String> strContainer = new GenericContainer<>();
strContainer.setValue("Hello");
String str = strContainer.getValue(); // 无需强制转换
// 2. 指定存储Integer类型
GenericContainer<Integer> intContainer = new GenericContainer<>();
intContainer.setValue(123);
Integer num = intContainer.getValue();
// 3. 编译时检查类型(错误在编译期暴露)
strContainer.setValue(123); // 编译报错:无法将int转换为String
}
}
泛型类的核心优势:
- 类型安全:编译时检查类型匹配,避免运行时类型转换异常;
- 消除强制转换:获取数据时无需手动转换,代码更简洁;
- 代码复用:一个泛型类可适配多种数据类型(如
GenericContainer
可存储String
、Integer
等); - 可读性强:通过
GenericContainer<String>
可直观判断存储类型。
二、泛型类的基本定义
泛型类的定义核心是“声明类型参数”,语法格式和关键概念如下。
2.1 基本语法
// 泛型类定义
修饰符 class 类名<类型参数列表> {
// 类体中可使用类型参数
}
- 类型参数列表:用尖括号
<>
包裹,可包含一个或多个类型参数(如<T>
、<K, V>
); - 类型参数命名:通常使用单个大写字母(约定俗成),常见命名:
T
(Type):表示任意类型;K
(Key):表示键类型;V
(Value):表示值类型;E
(Element):表示集合元素类型。
2.2 单类型参数泛型类
最常用的泛型类形式,适用于只需适配一种类型的场景(如容器类、工具类)。
/**
* 单类型参数泛型类示例:简单的链表节点
* T:表示节点存储的数据类型
*/
class Node<T> {
private T data; // 存储的数据
private Node<T> next; // 下一个节点(类型与当前节点一致)
public Node(T data) {
this.data = data;
}
// getter和setter
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public Node<T> getNext() { return next; }
public void setNext(Node<T> next) { this.next = next; }
}
使用示例:
public class NodeTest {
public static void main(String[] args) {
// 创建存储String的节点
Node<String> strNode = new Node<>("First");
strNode.setNext(new Node<>("Second"));
System.out.println(strNode.getNext().getData()); // 输出:Second
// 创建存储Integer的节点
Node<Integer> intNode = new Node<>(100);
intNode.setNext(new Node<>(200));
System.out.println(intNode.getNext().getData()); // 输出:200
}
}
2.3 多类型参数泛型类
当类需要适配多种相关类型(如键值对)时,可使用多类型参数(如<K, V>
)。
/**
* 多类型参数泛型类示例:键值对
* K:键类型
* V:值类型
*/
class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// getter和setter
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 class PairTest {
public static void main(String[] args) {
// 键为String,值为Integer(如"age" → 25)
Pair<String, Integer> agePair = new Pair<>("age", 25);
String key = agePair.getKey();
Integer value = agePair.getValue();
// 键为Integer,值为String(如1 → "张三")
Pair<Integer, String> userPair = new Pair<>(1, "张三");
System.out.println(userPair.getKey() + " → " + userPair.getValue()); // 输出:1 → 张三
}
}
三、泛型类的使用方式
使用泛型类时,需“指定具体类型参数”(类型实参),语法和注意事项如下。
3.1 基本使用(指定具体类型)
// 格式:类名<具体类型> 对象名 = new 类名<具体类型>();
GenericClass<String> obj = new GenericClass<String>();
// Java 7+支持菱形语法(右侧类型可省略)
GenericClass<String> obj = new GenericClass<>(); // 推荐
示例:使用Pair
泛型类:
// 指定K为String,V为Double
Pair<String, Double> pricePair = new Pair<>("apple", 5.99);
System.out.println(pricePair.getKey() + "的价格:" + pricePair.getValue());
3.2 类型参数的限制(有界泛型)
默认情况下,泛型类的类型参数可以是任何类型,但有时需要限制类型范围(如只允许数字类型)。这时可使用“有界泛型”(extends
关键字)。
3.2.1 单边界限制
语法:<T extends 父类型>
,表示T
必须是“父类型”或其子类型。
/**
* 有界泛型类示例:计算数值的工具类
* T extends Number:限制T必须是Number或其子类(Integer、Double等)
*/
class NumberCalculator<T extends Number> {
// 计算数组元素的总和
public double sum(T[] array) {
double total = 0;
for (T num : array) {
// Number类有doubleValue()方法,子类都可调用
total += num.doubleValue();
}
return total;
}
}
使用示例:
public class CalculatorTest {
public static void main(String[] args) {
// 1. 使用Integer类型(Integer是Number的子类)
NumberCalculator<Integer> intCalc = new NumberCalculator<>();
Integer[] intArray = {1, 2, 3, 4};
System.out.println("整数总和:" + intCalc.sum(intArray)); // 输出:10.0
// 2. 使用Double类型(Double是Number的子类)
NumberCalculator<Double> doubleCalc = new NumberCalculator<>();
Double[] doubleArray = {1.5, 2.5, 3.5};
System.out.println("小数总和:" + doubleCalc.sum(doubleArray)); // 输出:7.5
// 3. 错误:String不是Number的子类,编译报错
NumberCalculator<String> strCalc = new NumberCalculator<>(); // 编译报错
}
}
3.2.2 多边界限制(接口+类)
语法:<T extends 类 & 接口1 & 接口2>
,表示T
必须是“指定类的子类”且“实现了指定接口”。
注意:类必须放在第一个位置(只能有一个类,可多个接口)。
/**
* 多边界泛型类示例:支持比较和序列化的容器
* T extends Number & Comparable<T> & Serializable:
* - T必须是Number子类
* - 必须实现Comparable接口(支持比较)
* - 必须实现Serializable接口(支持序列化)
*/
class AdvancedContainer<T extends Number & Comparable<T> & java.io.Serializable> {
private T value;
public AdvancedContainer(T value) {
this.value = value;
}
// 比较当前值和另一个值
public int compareTo(AdvancedContainer<T> other) {
return this.value.compareTo(other.value); // 调用Comparable接口的方法
}
}
使用示例:
// Integer是Number子类,且实现了Comparable和Serializable
AdvancedContainer<Integer> container1 = new AdvancedContainer<>(10);
AdvancedContainer<Integer> container2 = new AdvancedContainer<>(20);
System.out.println(container1.compareTo(container2)); // 输出:-1(10 < 20)
3.3 泛型类的继承
泛型类可以继承普通类或其他泛型类,继承时需注意类型参数的传递。
3.3.1 泛型类继承普通类
// 普通父类
class Parent {
protected String name;
}
// 泛型子类继承普通父类
class GenericChild<T> extends Parent {
private T data; // 子类自己的泛型参数
}
3.3.2 泛型类继承泛型类
// 父泛型类
class ParentGeneric<T> {
protected T parentData;
}
// 子类保留父类的泛型参数
class ChildGeneric<T> extends ParentGeneric<T> {
private T childData; // 与父类使用相同的T
}
// 子类指定父类的具体类型(父类泛型参数固定)
class FixedChild extends ParentGeneric<String> {
// 父类的parentData固定为String类型
public void setParentData(String data) {
super.parentData = data;
}
}
使用示例:
// ChildGeneric使用Integer类型(父类和子类的T均为Integer)
ChildGeneric<Integer> child = new ChildGeneric<>();
child.parentData = 100; // 父类的T为Integer
child.childData = 200; // 子类的T为Integer
// FixedChild中父类的T固定为String
FixedChild fixedChild = new FixedChild();
fixedChild.setParentData("test"); // 正确(String类型)
fixedChild.parentData = 123; // 编译报错(父类T已固定为String)
四、泛型类的实际应用场景
泛型类在Java类库中被广泛使用(如ArrayList
、HashMap
),实际开发中,以下场景适合使用泛型类。
4.1 容器类(集合、缓存)
容器类是泛型最典型的应用,用于存储和管理不同类型的数据。
/**
* 自定义泛型链表
*/
public class GenericLinkedList<T> {
// 头节点
private Node<T> head;
// 添加元素
public void add(T data) {
Node<T> newNode = new Node<>(data);
if (head == null) {
head = newNode;
} else {
Node<T> current = head;
while (current.getNext() != null) {
current = current.getNext();
}
current.setNext(newNode);
}
}
// 获取指定索引的元素
public T get(int index) {
Node<T> current = head;
for (int i = 0; i < index; i++) {
if (current == null) {
throw new IndexOutOfBoundsException();
}
current = current.getNext();
}
return current.getData();
}
// 内部节点类(泛型)
private static class Node<T> {
private T data;
private Node<T> next;
public Node(T data) {
this.data = data;
}
// getter和setter
public T getData() { return data; }
public Node<T> getNext() { return next; }
public void setNext(Node<T> next) { this.next = next; }
}
}
使用示例:
public class LinkedListTest {
public static void main(String[] args) {
// 存储String类型的链表
GenericLinkedList<String> strList = new GenericLinkedList<>();
strList.add("A");
strList.add("B");
System.out.println(strList.get(1)); // 输出:B
// 存储Integer类型的链表
GenericLinkedList<Integer> intList = new GenericLinkedList<>();
intList.add(10);
intList.add(20);
System.out.println(intList.get(0)); // 输出:10
}
}
4.2 工具类(通用功能)
工具类通常需要处理多种类型的数据,泛型可避免重复开发。
/**
* 泛型工具类:数组操作工具
*/
class ArrayUtils<T> {
// 交换数组中两个位置的元素
public void swap(T[] array, int i, int j) {
if (i < 0 || j < 0 || i >= array.length || j >= array.length) {
throw new IndexOutOfBoundsException("索引越界");
}
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 查找元素在数组中的索引
public int indexOf(T[] array, T target) {
for (int i = 0; i < array.length; i++) {
if (target.equals(array[i])) {
return i;
}
}
return -1;
}
}
使用示例:
public class ArrayUtilsTest {
public static void main(String[] args) {
ArrayUtils<String> strUtils = new ArrayUtils<>();
String[] strArray = {"a", "b", "c"};
strUtils.swap(strArray, 0, 2);
System.out.println(Arrays.toString(strArray)); // 输出:[c, b, a]
ArrayUtils<Integer> intUtils = new ArrayUtils<>();
Integer[] intArray = {10, 20, 30};
System.out.println(intUtils.indexOf(intArray, 20)); // 输出:1
}
}
五、泛型类的常见问题与注意事项
5.1 不能使用基本类型作为类型参数
泛型的类型参数必须是引用类型,不能是基本类型(int
、double
等)。
// 错误:不能使用int作为类型参数
GenericContainer<int> container = new GenericContainer<>();
// 正确:使用包装类
GenericContainer<Integer> container = new GenericContainer<>();
container.setValue(123); // 自动装箱(int → Integer)
5.2 泛型类型的类型擦除(Type Erasure)
Java泛型是“编译期特性”,编译后会进行类型擦除:字节码中不保留泛型的具体类型,替换为上限类型(如T
擦除为Object
,T extends Number
擦除为Number
)。
示例:
class ErasureDemo<T> {
private T data;
public T getData() { return data; }
}
// 编译后(类型擦除后)的等效代码:
class ErasureDemo {
private Object data;
public Object getData() { return data; }
}
影响:
- 运行时无法获取泛型的具体类型(如
instanceof
无法判断obj instanceof GenericContainer<String>
); - 不能创建泛型数组(
new T[10]
编译报错,因类型擦除后无法确定数组类型)。
5.3 不能通过类型参数创建对象
由于类型擦除,无法在泛型类中通过new T()
创建对象(编译时不知道T
的具体类型)。
class CreateObjectDemo<T> {
// 错误:不能创建T的实例
public T create() {
return new T(); // 编译报错
}
// 正确:通过反射创建(需要传递Class对象)
public T create(Class<T> clazz) throws InstantiationException, IllegalAccessException {
return clazz.newInstance();
}
}
使用反射创建对象:
CreateObjectDemo<String> demo = new CreateObjectDemo<>();
String str = demo.create(String.class); // 正确(通过Class对象创建)
5.4 泛型类的静态成员不能使用类型参数
静态成员属于类,而类型参数是对象级别的(每个对象的类型参数可能不同),因此静态成员不能使用泛型类的类型参数。
class StaticMemberDemo<T> {
// 错误:静态成员不能使用T
public static T staticData;
// 错误:静态方法不能使用T作为参数或返回值
public static T staticMethod() { return null; }
}
总结
泛型类通过“类型参数化”实现了代码的复用与类型安全:
- 提升代码复用性:一个泛型类可适配多种数据类型,避免重复开发;
- 增强类型安全:编译期检查类型匹配,减少运行时异常;
- 简化代码编写:消除强制类型转换,代码更简洁易读。
实际开发中,泛型类广泛应用于容器类(如集合框架)、工具类、框架底层(如Spring的GenericBeanFactory
)等场景。
若这篇内容帮到你,动动手指支持下!关注不迷路,干货持续输出!
ヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノヾ(´∀ ˋ)ノ