Java 泛型:构建安全高效代码的基石
面试官问:请你说一下什么是泛型。
候选人:比如那个ArrayList,那个Book就是泛型
面试官:啊????
典型的 “把‘泛型的具体参数’当成了‘泛型本身’”
前置:
大家是不是很多时候都把T,E ,K,V
给搞混,其实我觉得他们是一个东西,只不过为了让程序员一眼可以看出他们的含义,就用了这些字母来定义
- T(Type 的缩写,代表一般的类型),不过你也可以用其他字母,
- E(通常用于集合中元素类型,Element 的缩写)、
- K(常用于表示键的类型,Key 的缩写)、
- V(常用于表示值的类型,Value 的缩写)。
正文开始
一、泛型是啥
简单讲,泛型就像是一个 “万能模板”。平常咱们写代码处理数据,每种数据都有自己的类型,像整数、字符串啥的。要是每次为不同类型数据写相似的代码,那可太麻烦了。
泛型呢,就是在定义类、接口或者方法的时候,不着急确定具体的数据类型,先用个 “代号”(也就是类型参数
)顶着。等真正要用这个类、接口或者方法的时候,再告诉它实际的数据类型。这就好比先造了个通用的盒子,等有东西要装了,再看装啥,这个盒子就能适应不同东西的形状。
二、泛型的好处
1. 让代码更安全
以前没有泛型的时候,为了能处理各种类型的数据,我们经常用Object
类型。这就好比不管啥东西都往一个大口袋里塞,拿出来的时候,也不知道到底是啥,很容易搞错
。比如说:
import java.util.ArrayList;import java.util.List;public class WithoutGenericExample { public static void main(String[] args) { List list = new ArrayList();// 创建一个普通的列表,没有指定元素类型 list.add(\"Hello\");// 向列表中添加一个字符串 Integer num = (Integer) list.get(0); // 这里尝试将从列表中获取的元素强制转换为Integer类型,运行时会抛出ClassCastException,因为实际元素是字符串 }}
但是有了泛型,就像给这个口袋贴上了标签,只能装特定类型的东西。编译的时候,Java 就会检查是不是装对了东西,如果不对,直接就告诉你,不让程序运行。像这样:
import java.util.ArrayList;import java.util.List;public class WithGenericExample { public static void main(String[] args) { List<String> list = new ArrayList<>();// 创建一个只能存储String类型元素的列表 list.add(\"Hello\");// 向列表中添加一个字符串 // Integer num = (Integer) list.get(0); // 这行代码会在编译时出错,因为列表被定义为只能存储字符串,不允许获取Integer类型的元素,提高了代码安全性 }}
2. 代码能重复用
泛型可以让我们写一份代码,适应好多不同的数据类型,不用每种类型都写一遍。比如说,咱们要做一个 “栈” 的数据结构(就像一摞盘子,先放上去的后拿下来)。以前可能得为整数写一个栈,为字符串再写一个栈,特别麻烦。用泛型就简单多啦:
import java.util.ArrayList;import java.util.List;class Stack<T> { // 这里的T是类型参数,代表栈中要存放的数据类型 private List<T> elements; public Stack() { elements = new ArrayList<>();// 初始化一个空的列表来存储栈中的元素 } public void push(T element) { // 将元素添加到栈顶,即列表的末尾 elements.add(element); } public T pop() { if (elements.isEmpty()) { throw new RuntimeException(\"栈是空的,不能取东西啦\");// 如果栈为空,抛出运行时异常 } return elements.remove(elements.size() - 1); // 移除并返回栈顶元素,即列表的最后一个元素 }}
这样,不管是整数栈
、字符串栈
,还是其他类型的栈,都可以用这个Stack类
来实现。
三、泛型怎么用
1. 泛型类
定义泛型类的时候,在类名后面加上,里面写上类型参数。常见的类型参数有
T
(Type 的缩写,代表一般的类型),不过你也可以用其他字母,像 E
(通常用于集合中元素类型,Element 的缩写)、K
(常用于表示键的类型,Key 的缩写)、V
(常用于表示值的类型,Value 的缩写)等 。以 T 为例
,就像上面的Stack
。用的时候,创建对象要告诉它具体的类型,像这样:
Stack<Integer> intStack = new Stack<>();// 创建一个只能存储Integer类型元素的栈intStack.push(10);// 将整数10压入栈中int num = intStack.pop();// 从栈中弹出一个整数并赋值给num
这里就创建了一个只能放整数的栈intStack。如果是一个表示键值对的泛型类,可能会这么定义:
class KeyValuePair<K, V> { // K代表键的类型,V代表值的类型 private K key; private V value; public KeyValuePair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; }}
使用的时候:
KeyValuePair<String, Integer> pair = new KeyValuePair<>(\"age\", 25);// 创建一个键为String类型,值为Integer类型的键值对String key = pair.getKey();// 获取键Integer value = pair.getValue();// 获取值
2. 泛型接口
和泛型类差不多,接口也能有类型参数。比如:
interface Box<T> { // T代表盒子中所装物品的类型 T get();// 获取盒子中的物品 void set(T value);// 设置盒子中的物品}class IntegerBox implements Box<Integer> { private Integer value; @Override public Integer get() { return value; } @Override public void set(Integer value) { this.value = value; }}
这个Box接口
可以用来创建不同类型的 “盒子”,IntegerBox
就是专门装整数的 “盒子”。
3. 泛型方法
泛型方法就是在方法里也用类型参数。类型参数写在方法返回类型前面。看这个例子:
class GenericMethods { // 是类型参数,这个方法可以打印任何类型的数组 public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + \" \"); } System.out.println(); }}
调用这个方法的时候,Java
能根据你传进去的数组类型,知道这个T
到底是什么类型:
java
Integer[] intArray = {1, 2, 3};GenericMethods.printArray(intArray);// 调用泛型方法,打印整数数组的元素
四、泛型与集合的紧密结合(这个我们在做项目的时候一般都会用到)
在 Java 编程里,泛型常常和集合携手合作,这是极为普遍且好处多多。
1.保障类型安全
要是集合
不搭配泛型
,就会把所有元素都当作Object类型
来处理。这就导致编译期没法检测出类型不匹配的问题,直到运行时才可能出现ClassCastException
异常。一旦集合结合泛型,在编译期就能明确元素类型,避免运行时的这类错误。举例来说:
不使用泛型的集合:
// 不使用泛型的集合import java.util.ArrayList;import java.util.List;public class NonGenericListExample { public static void main(String[] args) { List list = new ArrayList(); list.add(\"Hello\"); Integer num = (Integer) list.get(0); // 运行时抛出ClassCastException异常 }}
使用泛型的集合:
// 使用泛型的集合import java.util.ArrayList;import java.util.List;public class GenericListExample { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add(\"Hello\"); // Integer num = (Integer) list.get(0); // 编译时就会报错,提示类型不匹配 }}
2. 提升代码可读性与易用性
当泛型和集合一起使用时,代码能清楚表明集合里元素的类型,可读性大大提高。开发者一眼就能知道集合中存的是什么类型的数据,不用再额外去找相关操作逻辑。比如:
import java.util.HashMap;import java.util.Map;public class GenericMapExample { public static void main(String[] args) { Map<String, Integer> ageMap = new HashMap<>(); ageMap.put(\"Alice\", 30); // 从ageMap中获取值时,无需进行类型转换,代码简洁明了 Integer age = ageMap.get(\"Alice\"); }}
这里Map
很清晰地表明这个映射集合里,键是String
类型,值是Integer
类型。
3. 增强集合通用性
泛型让集合可以用于多种数据类型,不用为每种类型单独创建集合类。就拿ArrayList
来说,结合泛型后,既能创建存整数的ArrayList
,也能创建存字符串的ArrayList
,集合的通用性和复用性大大增强。
4. 适配泛型算法
很多集合操作算法都是基于泛型实现的,集合与泛型结合能更好地适配这些算法。比如Collections
类里的排序、查找等方法,能用于不同类型的泛型集合。例如:
import java.util.ArrayList;import java.util.Collections;import java.util.List;public class GenericCollectionAlgorithmExample { public static void main(String[] args) { List<Integer> numbers = new ArrayList<>(); numbers.add(3); numbers.add(1); numbers.add(2); Collections.sort(numbers); // 对泛型列表进行排序 System.out.println(numbers); }}
虽然泛型常和集合一起用,但它的应用可不只这些,在自定义数据结构、通用方法等方面也都大有用处。就像实现一个通用的栈或队列数据结构,用泛型能提高它们的通用性和类型安全性。
五、通配符
通配符是泛型里用来解决类型之间 “兼容” 问题的。常见的有下面几种:
1. 无界通配符
它表示这个类型可以是任何类型。比如说,我们有个方法可以打印任何类型的列表:
import java.util.ArrayList;import java.util.List;public class UnboundedWildcardExample { public static void printList(List<?> list) { // 这里的? 代表任何类型,通过遍历列表打印其中的元素 for (Object element : list) { System.out.print(element + \" \"); } System.out.println(); } public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); intList.add(1); printList(intList); List<String> stringList = new ArrayList<>(); stringList.add(\"Hello\"); printList(stringList); }}
2. 上界通配符
就是说他最顶的类型只是说Type
它表示这个类型可以是Type
类型,或者是Type的子类
。比如我们有个方法专门处理动物相关的逻辑,但它可以接受任何动物或者动物子类的列表:
import java.util.ArrayList;import java.util.List;class Animal {}class Dog extends Animal {}public class UpperBoundWildcardExample { public static void processAnimals(List<? extends Animal> animals) { for (Animal animal : animals) { // 这里可以处理动物相关的逻辑,例如调用动物的方法 } } public static void main(String[] args) { List<Dog> dogs = new ArrayList<>(); dogs.add(new Dog()); processAnimals(dogs); }}
3. 下界通配符
他最低的类型就是Type
它表示这个类型可以是Type类型
,或者是Type的父类
。比如我们有个方法往列表里添加苹果,这个列表可以是苹果类型,也可以是苹果的父类(比如水果)类型:
import java.util.ArrayList;import java.util.List;class Fruit {}class Apple extends Fruit {}public class LowerBoundWildcardExample { public static void addApples(List<? super Apple> list) { list.add(new Apple()); } public static void main(String[] args) { List<Fruit> fruits = new ArrayList<>(); addApples(fruits); }}
六、总结
Java 泛型
就像是给我们的编程工具箱里加了一件超级武器。它通过把类型变成 “参数”
,让我们的代码更安全,能重复用,还更灵活。不管是处理复杂的数据结构,还是不同类型的数据集合,泛型都能帮上大忙。掌握了泛型,你写代码的水平肯定能上一个台阶!
要是在实习阶段,我们一般直接用集合结合泛型来限制数据,那些写一个泛型方法呀,写泛型接口啊,一般很少会让我们自己来写,大多数多少组长已经写好给我们,我们用就行了,但是我们要知道有这么个东西。