目录
Java 泛型是一种强大的特性,它允许你在编写代码时指定类型参数,从而提高代码的重用性和类型安全性。泛型是在编译时期检查类型安全,并且所有的类型参数在运行时被擦除的编程语言特性。
Java 泛型允许在类、接口和方法中指定类型参数。这些类型参数在实例化或使用时需要被具体的类型所替换。
下面详细介绍 Java 泛型的基本使用方法以及一些常见的应用场景。
1 泛型的基本使用
1.1 定义泛型类
public class Bread<T> { // T 是类型参数
private T item;
public Bread(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
public static void main(String[] args) {
Bread<String> bread = new Bread<>("Hello, World!");
System.out.println(bread.getItem()); // 输出 Hello, World!
}
}
泛型类的意义:
- 类型安全:通过使用泛型,可以在编译阶段就确保类型安全,避免了运行时的 ClassCastException 错误。
- 代码重用:泛型类可以被不同类型的对象实例化,提高了代码的重用性。
- 灵活性:泛型类可以根据实际需要动态决定存储的数据类型,而不需要修改类的实现。
- 清晰性:通过显式声明类型参数,代码的意图变得更加明确,提高了可读性。
1.2 定义泛型方法
泛型方法的定义格式:
<类型参数列表> 方法名(参数列表) {
// 方法体
}
其中 <类型参数列表>
可以包含一个或多个类型参数,用逗号隔开。
public class Electricity {
public <T> T getSomeValue(T value) {
return value;
}
public <T, U> String combine(T t, U u) {
return t.toString() + " and " + u.toString();
}
}
可以在非泛型类中定义泛型方法,或者在一个泛型类中定义泛型方法。
1.3 定义泛型接口
public interface FunctionInterface<T, R> { // T 是输入类型,R 是输出类型
R apply(T t);
}
泛型接口也是常见的使用场景。以下是具体使用示例:
示例一:实现接口
public class MyFunction implements FunctionInterface<Integer, String> {
@Override
public String apply(Integer t) {
return "The input is: " + t;
}
public static void main(String[] args) {
// 创建一个 MyFunction 实例
MyFunction myFunction = new MyFunction();
// 调用 apply 方法
String result = myFunction.apply(42);
System.out.println(result); // 输出 "The input is: 42"
}
}
示例二:使用Lambda 表达式
public class Main {
public static void main(String[] args) {
FunctionInterface<Integer, String> lambdaFunction = t -> "The input is: " + t;
String result = lambdaFunction.apply(42);
System.out.println(result); // 输出 "The input is: 42"
}
}
从 Java 8 开始,可以使用 Lambda 表达式来简化函数式接口的使用,而无需显式地创建一个实现类。
函数式接口:一个函数式接口是指仅包含一个抽象方法的接口。Java 8 引入了 @FunctionalInterface
注解来标记这样的接口,虽然不是强制性的,但这个注解有助于确保接口确实是函数式的。
Lambda 表达式:Lambda 表达式提供了一种简洁的方式,可以用来定义匿名函数。Lambda 表达式的形式为 (parameters) -> expression
或 (parameters) -> { statements; }
。
示例三:使用方法引用
public class Main {
public static Integer formatInput(String t) {
return Integer.parseInt(t);
}
public static void main(String[] args) {
FunctionInterface<String, Integer> methodRefFunction = Integer::parseInt;
Integer result = methodRefFunction.apply("42");
System.out.println(result); // 输出 42
FunctionInterface<String, Integer> methodRefFunction1 = Main::formatInput;
Integer result1 = methodRefFunction1.apply("43");
System.out.println(result1); // 输出 43
}
}
方法引用:基于已存在的方法或构造函数,允许程序员通过引用来重用这些方法或构造函数的功能。方法引用可以看作是 Lambda 表达式的一个特殊形式,但它更简洁。
用法了解:Java 方法引用-CSDN博客
2 泛型的高级使用
2.1 泛型通配符
泛型通配符是一种特殊的类型标记,用于指明一个泛型参数的类型未知或者不受限。它们通常用于集合、方法参数或返回类型中,以增加代码的灵活性和可重用性。
Java中常用的泛型通配符主要有三种:
2.1.1 无界通配符(?)
无界通配符表示“任意类型”,意味着你可以将任何类型的数据赋值给该通配符代表的类型。
public void printListContents(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
printListContents(Arrays.asList(1, 2, 3)); // 可以传递 List<Integer>
printListContents(Arrays.asList("a", "b", "c")); // 也可以传递 List<String>
2.1.2 上限通配符(? extends T)
上限通配符表示“类型 T 或其子类型”,这意味着你可以将类型 T 或者它的任何子类型的对象赋值给该通配符代表的类型。
public void printNumbers(List<? extends Number> numbers) {
for (Number num : numbers) {
System.out.println(num.doubleValue());
}
}
printNumbers(Arrays.asList(1, 2, 3)); // 可以传递 List<Integer>
// printNumbers(Arrays.asList("a", "b", "c")); // 编译错误,因为 List<String> 不是 List<Number>
2.1.3 下限通配符 (? super T)
下限通配符表示“类型 T 或其父类型”,这意味着你可以将类型 T 或者它的任何超类型的对象赋值给该通配符代表的类型。
public void addIntegers(List<? super Integer> list) {
list.add(5); // 合法的,因为可以向 List<Object> 添加 Integer
list.add(6); // 合法的
}
addIntegers(new ArrayList<>()); // 可以传递 ArrayList<Object>
addIntegers(new ArrayList<Integer>()); // 也可以传递 ArrayList<Integer>
// addIntegers(new ArrayList<String>()); // 编译错误,因为 String 不是 Integer 的超类型
在 Java 中,使用无界通配符(?
)和上限通配符(? extends T
)时,由于类型不确定性的原因,编译器不允许向这些集合中添加元素。只有下限通配符(? super T
)允许向集合中添加元素,因为它确保了集合至少可以接受类型 T
或其超类型(如 Object
)的对象。
2.2 泛型的继承和实现
可以使用泛型来定义泛型化的继承关系或实现泛型接口。
public class ConcreteClass<T> extends GenericBaseClass<T> implements GenericInterface<T> {
// 实现泛型接口或继承泛型基类
}
public interface GenericInterface<T> {
T process(T t);
}
public abstract class GenericBaseClass<T> {
protected abstract T doSomething(T t);
}
2.3 泛型与数组
由于 Java 的类型擦除机制,泛型不能用于创建泛型数组。不过,可以通过反射等方式绕过这一限制。
public class GenericArray {
public static <T> T[] createArray(Class<T> clazz, int size) {
// 创建泛型数组,注意类型擦除
@SuppressWarnings("unchecked")
T[] array = (T[]) java.lang.reflect.Array.newInstance(clazz, size);
return array;
}
}
3 泛型的好处
- 减少强制类型转换:使用泛型可以减少强制类型转换的需求,提高了代码的安全性和简洁性。
- 更好的性能:泛型避免了运行时的类型检查开销,提高了程序的性能。
- 易于维护:泛型代码更容易理解和维护,因为类型信息更加明确。
4 泛型的类型参数
在 Java 泛型中,类型参数通常用单个大写字母表示,比如 T
、E
、K
、V
等。这些字母并没有特定的含义,只是约定俗成的使用习惯。以下是一些常用的类型参数名称及其常见用途:
- T (Type):最常用的类型参数,表示“某种类型”。
- E (Element):常用在集合框架中,表示集合中的元素类型。
- K (Key):常用在映射(Map)中,表示键的类型。
- V (Value):常用在映射(Map)中,表示值的类型。
- N (Number):表示数字类型。
- S (Source):表示源类型。
- R (Result):表示结果类型。
示例代码:
public class Box<T> { // T 代表任何类型
private T item;
public Box(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
public class MyMap<K, V> { // K 代表 Key, V 代表 Value
private Map<K, V> map = new HashMap<>();
public void put(K key, V value) {
map.put(key, value);
}
public V get(K key) {
return map.get(key);
}
}
public class DataContainer<I, O> { // I 代表 Input, O 代表 Output
private I input;
private O output;
public DataContainer(I input, O output) {
this.input = input;
this.output = output;
}
public I getInput() {
return input;
}
public O getOutput() {
return output;
}
}
类型参数的名称可以是任何有效的 Java 标识符,但通常选择有意义的名称可以增加代码的可读性。常用的类型参数名称包括 T
、E
、K
、V
等,这些字母分别表示 Type、Element、Key 和 Value。根据你的具体需求和上下文选择合适的类型参数名称。