泛型本质上是一种参数化类型,它允许在定义类、接口或方法时使用一个类型参数,这个参数可以在使用时被具体的类型所替换。引入泛型的主要目的是为了提供更严格的类型检查,并消除代码中大量的强制类型转换,从而提升代码的健壮性和可读性。
为什么需要泛型?
在面向对象编程中,继承是核心特性之一。Java的集合框架在设计初期,为了容纳所有类型的对象,其方法(如 add())的参数普遍使用了 Object 类型。这带来了一个显著的问题:集合中元素的类型无法在编译时得到统一约束。
考虑这样一个典型场景:我们本意是向一个 ArrayList 集合中添加 Person 类的对象,但编码时不小心混入了一个 Boy 类的对象。
在使用非泛型集合时,代码和运行结果如下所示:
ArrayList arrayList = new ArrayList();
arrayList.add(new Person("pp",12));
arrayList.add(new Person("xx",14));
arrayList.add(new Boy("hh",13)); // 这里添加了Boy类型对象
for (Object o : arrayList) {
Person person = (Person) o; // 向下转型
System.out.println(person.getName() + "--" + person.getAge());
}
运行上述代码,控制台将抛出 ClassCastException 异常:
hh--13
Exception in thread "main" java.lang.ClassCastException: generic_.Boy cannot be cast to generic_.Person
at generic_.Generic01.main(Generic01.java:19)
异常信息清晰地指出:generic_.Boy 类型的对象无法转换为 generic_.Person 类型。这种错误在编译阶段无法被编译器发现,只有在运行时才会暴露,极大地降低了程序的健壮性。
传统方式的主要缺陷有两点:
- 安全性不足:无法对加入集合的数据类型进行约束。
- 效率影响:遍历时需要进行显式的类型转换,数据量大时影响性能。
使用泛型带来的好处
1. 提升程序的健壮性与规范性
针对上述问题,使用泛型可以非常优雅地解决。只需在声明集合时指定具体的类型参数,编译器便能在编译阶段进行类型检查。
例如,当我们声明 ArrayList<Person> 后,尝试添加 Boy 对象时,集成开发环境(IDE)会立即给出错误提示,指出类型不匹配,从而在代码编写阶段就阻止了潜在的错误。
ArrayList<Person> arrayList = new ArrayList();
arrayList.add(new Person("pp",12));
arrayList.add(new Person("xx",14));
arrayList.add(new Person("hh",13));
arrayList.add(new Boy("hh",13)); // 这行代码会导致编译错误
2. 减少类型转换,提升效率
使用泛型后,从集合中取出的元素就是明确的类型,无需再进行向下转型。
- 未使用泛型时的数据流程:
Person 对象存入集合时,先向上转型为 Object;取出时,再向下转型回 Person。这个过程涉及两次类型转换。
- 使用泛型时的数据流程:
Person 对象存入 ArrayList<Person> 后,取出时直接就是 Person 类型,无需任何转型,效率极高。
3. 泛型在类声明中的应用
泛型不仅用于集合,还可以在类、接口、方法中声明,以表示属性类型、方法返回值类型或参数类型。
class Person<E> {
E s; // E 作为属性类型
public Person(E s) { // E 作为构造函数参数类型
this.s = s;
}
public E f() { // E 作为方法返回类型
return s;
}
public void show() {
System.out.println(s.getClass()); // 显示 s 的运行类型
}
}
这里的 <E> 是一个类型形参,它只是一个占位符。在实例化类时,程序员可以指定具体的类型。
public static void main(String[] args) {
Person<String> person1 = new Person<String>("xxxx"); // E -> String
person1.show();
Person<Integer> person2 = new Person<Integer>(123); // E -> Integer
person2.show();
}
运行结果:
class java.lang.String
class java.lang.Integer
泛型的常见用法
1. 定义泛型接口
定义接口时使用泛型,可以规范实现类的行为,使其操作在确定的类型范围内进行。
interface Im<U, R> {
void hi(R r);
void hello(R r1, R r2, U u1, U u2);
default R method(U u) {
return null;
}
}
上述接口规定了其方法操作的对象类型必须是 U 或 R。如果在实现接口的方法时,试图使用未声明的类型,编译器会报错。例如,在方法 void hi(X n); 中,如果 X 不是接口声明的类型形参,IDE 会提示“无法解析符号 ‘X‘”。
2. 定义泛型集合
使用泛型集合是泛型最广泛的应用,它能确保集合中元素的类型安全,并简化迭代操作。
示例一:HashSet
HashSet<Student> students = new HashSet<Student>();
students.add(new Student("懒羊羊", 21));
students.add(new Student("喜羊羊", 41));
students.add(new Student("美羊羊", 13));
for (Student student : students) {
System.out.println(student); // 直接输出Student对象,无需转型
}
示例二:HashMap
HashMap 是一个双列集合,以键值对(K-V)方式存储,因此需要指定两个泛型参数。
HashMap<String, Student> hm = new HashMap<String, Student>();
hm.put("001", new Student("喜羊羊", 21));
hm.put("002", new Student("懒羊羊", 32));
hm.put("003", new Student("美羊羊", 43));
Set<Map.Entry<String, Student>> ek = hm.entrySet();
Iterator<Map.Entry<String, Student>> iterator = ek.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Student> next = iterator.next(); // 取出的就是明确类型的键值对
System.out.println(next.getKey() + " - " + next.getValue());
}
使用泛型定义 [K, V] 后,通过 entrySet() 取出的 Map.Entry 对象其键和值的类型已经是确定的(String 和 Student),省去了遍历时的转型步骤,使程序更简洁高效,这正体现了良好的设计模式与程序开发规范对提升代码质量的重要性。
泛型的使用细节
1. 类型参数的规范
泛型的类型参数(即 <> 中的内容)不能是基本数据类型(int, double, char 等)。如果需要使用,必须替换为其对应的包装类。
例如,ArrayList<int> list = new ArrayList<>(); 会导致编译错误,提示“类型实参不能为基本数据类型”。正确的写法是 ArrayList<Integer> list = new ArrayList<>();。编译器通常会提示将 ‘int’ 替换为 ‘java.lang.Integer’。
2. 继承性的体现
在给泛型指定具体类型后,可以传入该类型或其子类类型的对象。
P<A> ap = new P<A>(new A());
P<A> ap1 = new P<A>(new B()); // B是A的子类,这是允许的
class A {}
class B extends A {}
3. 类型推断与简写
从 Java 7 开始,引入了“钻石运算符” <>,允许在实例化时省略右侧的泛型类型声明,编译器会自动推断。
// 完整写法
P<A> ap = new P<A>(new A());
// 简写写法(推荐)
P<A> ap = new P<>(new A());
自定义泛型
1. 使用类声明的泛型
在类中定义方法时,可以直接使用类声明的泛型参数,这能确保方法参数与类定义的类型约束保持一致。
public static void main(String[] args) {
U<String, Double, Integer> u = new U<>();
u.hi("hello", 1.0); // 调用时,X被推断为String,Y被推断为Double
}
class U<X, Y, Z> {
public void hi(X x, Y y) {} // 使用了类声明的泛型X和Y
}
2. 自定义泛型方法
即使在普通类中,也可以定义属于自己的泛型方法。方法的泛型参数与类的泛型参数相互独立。
public static void main(String[] args) {
U<String, Double, Integer> u = new U<>();
u.m1(12, "hello"); // 调用时,编译器根据实参自动推断T为Integer,K为String
}
class U<X, Y, Z> {
public <T, K> void m1(T t, K k) {} // 自定义的泛型方法
}
这种自动类型推断非常灵活,它根据传入的实参(如 12 和 "hello")在类已声明的泛型参数(String, Double, Integer)中自动匹配最合适的类型,无需考虑声明的顺序。
3. 重要注意事项
-
不能初始化泛型数组:因为数组在 new 时需要确切知道类型才能在内存中开辟连续空间,而泛型 T 在运行时被擦除,无法确定。
// 错误写法
T[] ts = new T[5];
-
静态成员不能使用类的泛型:静态方法和静态属性在类加载时初始化,此时类的泛型参数 T 还没有被具体类型替换(对象尚未创建),因此JVM无法完成初始化。
class M<T> {
// public static void m1(T t) {} // 编译错误:无法从static上下文引用非静态类型变量T
}
参考资料
[1] 图文详解Java泛型,写得太好了!, 微信公众号:mp.weixin.qq.com/s/JcNCAfSOE60q8r0MmHikHw
版权声明:本文由 云栈社区 整理发布,版权归原作者所有。