背景
本文是《Java 后端从小白到大神》修仙系列第十五篇
,正式进入Java后端
世界,本篇文章主要聊Java基础
。若想详细学习请点击首篇博文,我们开始把。
文章概览
- 泛型
泛型
1. 泛型的概念
泛型(Generics)是 Java 5 引入的特性,允许在类、接口或方法中将类型定义为参数,从而在编译时提供更强的类型检查和代码复用能力。泛型的核心思想是将类型本身作为参数传递,接口或方法在定义时不指定具体类型,而是通过使用时的参数来动态确定类型。这种设计类似于函数参数,但传递的是数据类型而非值。
2. 泛型的用法
1. 泛型类
在 Java 中,泛型类允许你在定义类时使用类型参数,从而创建可以处理不同类型数据的类。泛型类的类型参数通常在类名后面定义,这些类型参数可以在类的各个部分(如属性、方法参数、返回类型等)中使用。
1. 泛型类型参数的位置
在 Java 中,泛型类型参数通常定义在类名后面,使用尖括号 <> 包围。这种语法使得编译器能够识别和处理泛型类型。
2. 为什么泛型类型参数添加在类名后面
- 明确类型参数的作用范围:将类型参数定义在类名后面,明确表示该类型参数在整个类中都有效。
- 语法一致性:与泛型方法的定义方式保持一致,便于理解和使用。
- 编译时类型检查:编译器可以基于泛型类型参数进行类型检查,确保类型安全。
3. 类的属性为什么也是泛型类型
类的属性可以是泛型类型,因为泛型类型参数定义在类级别,可以在类的任何部分使用。属性使用泛型类型可以确保属性的类型与类的类型参数一致,从而实现类型安全。
4. 泛型类的类型与属性类型的关系
- 泛型类的类型参数:定义在类名后面,表示类可以处理的类型。
- 属性类型:可以使用泛型类型参数,确保属性的类型与类的类型参数一致。
- 类型安全:通过泛型类型参数,编译器可以在编译时进行类型检查,确保类型安全。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// Box<T>:T 是泛型类型参数,定义在类名 Box 后面
public class Box<T> {
// private T content,类的属性 content 使用泛型类型 T,表示它可以存储任何类型的数据,具体类型在实例化 Box 类时确定
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用示例
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
String value = stringBox.getContent(); // 无需类型转换
|
5. 泛型类的其他用法
- 方法参数和返回类型:泛型类型参数也可以用于方法的参数和返回类型。
- 内部类:泛型类可以包含泛型内部类。
- 继承:泛型类可以继承其他泛型类或实现泛型接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
// 泛型方法
public <U> void printContent(U additionalContent) {
System.out.println(content + " " + additionalContent);
}
}
public class Main {
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
stringBox.printContent("World"); // 输出: Hello World
Box<Integer> integerBox = new Box<>();
integerBox.setContent(42);
integerBox.printContent(100); // 输出: 42 100
}
}
|
2. 泛型接口
1. 泛型接口概述
泛型接口(Generic Interface)是 Java 中的一种接口,允许在接口定义中使用类型参数。通过泛型接口,可以创建可以处理不同类型数据的接口,从而提高代码的灵活性和可重用性。
1
2
3
4
|
public interface MyGenericInterface<T> {
void setValue(T value);
T getValue();
}
|
2. 实现泛型接口
实现泛型接口时,可以选择在实现类中指定具体的类型参数,也可以在实现类中继续使用泛型类型参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class MyGenericClass implements MyGenericInterface<String> {
private String value;
@Override
public void setValue(String value) {
this.value = value;
}
@Override
public String getValue() {
return value;
}
}
|
3. 在实现类中继续使用泛型类型参数
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class MyGenericClass<T> implements MyGenericInterface<T> {
private T value;
@Override
public void setValue(T value) {
this.value = value;
}
@Override
public T getValue() {
return value;
}
}
|
4. 使用泛型接口
使用泛型接口时,可以创建实现类的实例,并调用接口中定义的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class GenericInterfaceExample {
public static void main(String[] args) {
// 示例 1:在实现类中指定具体类型
MyGenericClass stringClass = new MyGenericClass();
stringClass.setValue("Hello, World!");
System.out.println(stringClass.getValue()); // 输出: Hello, World!
// 示例 2:在实现类中继续使用泛型类型参数
MyGenericClass<Integer> integerClass = new MyGenericClass<>();
integerClass.setValue(42);
System.out.println(integerClass.getValue()); // 输出: 42
}
}
|
5. 内置泛型接口示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class BuiltInGenericInterfacesExample {
public static void main(String[] args) {
// Consumer 示例
Consumer<String> printMessage = message -> System.out.println(message);
printMessage.accept("Hello, World!"); // 输出: Hello, World!
// Supplier 示例
Supplier<Integer> randomNumber = () -> (int) (Math.random() * 100);
System.out.println("Random Number: " + randomNumber.get()); // 输出: Random Number: <随机数>
// Function 示例
Function<Integer, Integer> square = x -> x * x;
System.out.println("Square of 5: " + square.apply(5)); // 输出: Square of 5: 25
// Predicate 示例
Predicate<Integer> isEven = x -> x % 2 == 0;
System.out.println("Is 4 even? " + isEven.test(4)); // 输出: Is 4 even? true
// BiFunction 示例
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println("Sum of 3 and 5: " + add.apply(3, 5)); // 输出: Sum of 3 and 5: 8
}
}
|
3. 泛型方法
1. 泛型方法概述
泛型方法(Generic Method)是指在方法声明中使用类型参数的方法。这些类型参数可以独立于类的类型参数,使得方法可以处理不同类型的数据,提高代码的灵活性和可重用性。在定义泛型方法时,类型参数通常定义在方法的返回类型之前
,使用尖括号 <> 包围。这些类型参数可以在方法的参数列表和返回类型中使用。
2. 泛型方法签名的基本结构
泛型方法的签名通常包括以下几个部分:
- 类型参数部分:定义在方法的返回类型之前,使用尖括号 <> 包围。类型参数 必须定义在方法的返回类型之前,这是 Java 语法的规定。
- 返回类型:可以是具体类型、泛型类型或 void。
- 方法名:方法的名称。
- 参数列表:方法的参数,可以包含具体类型或泛型类型。
- 异常声明:方法可能抛出的异常(可选)。
1
2
3
4
5
6
7
|
// <T>:定义了一个类型参数 T
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
|
3. 为什么需要类型参数
即使方法的返回类型是 void,类型参数 仍然需要包含在方法签名中,原因如下:
- 类型参数的作用范围:类型参数 定义了方法可以处理的泛型类型,这个类型参数可以在方法的参数列表、局部变量、异常声明等地方使用。
- 编译时类型检查:通过类型参数,编译器可以在编译时进行类型检查,确保类型安全。
- 代码复用:泛型方法可以处理多种类型的数据,提高代码的灵活性和可重用性。
4. 使用泛型方法
使用泛型方法时,可以显式地指定类型参数,也可以让编译器自动推断类型参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class GenericMethodExample {
// 定义一个泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
// 使用泛型方法打印整数数组
Integer[] intArray = {1, 2, 3, 4, 5};
printArray(intArray); // 输出: 1 2 3 4 5
// 使用泛型方法打印字符串数组
String[] stringArray = {"Hello", "World", "Java", "Generics"};
printArray(stringArray); // 输出: Hello World Java Generics
// 使用泛型方法打印双精度浮点数数组
Double[] doubleArray = {1.1, 2.2, 3.3};
printArray(doubleArray); // 输出: 1.1 2.2 3.3
}
}
|
5. 显式指定类型参数
在某些情况下,你可能需要显式地指定类型参数,尤其是在编译器无法自动推断类型时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class GenericMethodExample {
// 定义一个泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
// 显式指定类型参数
Integer[] intArray = {1, 2, 3, 4, 5};
GenericMethodExample.<Integer>printArray(intArray); // 输出: 1 2 3 4 5
// 显式指定类型参数
String[] stringArray = {"Hello", "World", "Java", "Generics"};
GenericMethodExample.<String>printArray(stringArray); // 输出: Hello World Java Generics
}
}
|
6. 内置泛型方法示例
Arrays.sort(T[] a, Comparator<? super T> c):对数组进行排序。
- 方法签名:public static void sort(T[] a, Comparator<? super T> c)
Collections.max(Collection<? extends T> coll, Comparator<? super T> comp):返回集合中的最大元素。
- 方法签名:public static T max(Collection<? extends T> coll, Comparator<? super T> comp)
Collections.min(Collection<? extends T> coll, Comparator<? super T> comp):返回集合中的最小元素。
- 方法签名:public static T min(Collection<? extends T> coll, Comparator<? super T> comp)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class BuiltInGenericMethodsExample {
public static void main(String[] args) {
// 使用 Arrays.sort 方法
Integer[] intArray = {5, 3, 1, 4, 2};
Arrays.sort(intArray, Comparator.naturalOrder());
System.out.println("Sorted Array: " + Arrays.toString(intArray)); // 输出: Sorted Array: [1, 2, 3, 4, 5]
// 使用 Collections.max 方法
List<String> stringList = Arrays.asList("Apple", "Banana", "Cherry");
String maxString = Collections.max(stringList, Comparator.naturalOrder());
System.out.println("Max String: " + maxString); // 输出: Max String: Cherry
// 使用 Collections.min 方法
String minString = Collections.min(stringList, Comparator.naturalOrder());
System.out.println("Min String: " + minString); // 输出: Min String: Apple
}
}
|
4. 通配符 ?
在 Java 泛型中,通配符 ? 用于表示未知类型。通配符在泛型中提供了灵活性和类型安全。
1. 通配符 ? 的含义
无界通配符 ?:
- 表示任何类型。
- 适用于读取操作,但不能添加元素(除了 null)。
有界通配符:
- 上界通配符 ? extends T:表示 T 或 T 的子类型。
- 下界通配符 ? super T:表示 T 或 T 的父类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
// 使用无界通配符 ?,表示可以接受任何类型的列表,printList 方法可以处理 List<Integer> 和 List<String>
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);
intList.add(2);
intList.add(3);
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
printList(intList); // 输出: 1 2 3
printList(stringList); // 输出: Hello World
}
}
// 上界通配符,表示可以接受 Number 或其子类型的列表,读取操作:使用无界通配符 ? 或上界通配符 ? extends T 进行读取操作
public class UpperBoundedWildcardExample {
// 计算列表中所有元素的总和,假设元素是 Number 或其子类型
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
List<Double> doubleList = new ArrayList<>();
doubleList.add(1.1);
doubleList.add(2.2);
doubleList.add(3.3);
System.out.println("Sum of intList: " + sumOfList(intList)); // 输出: Sum of intList: 6.0
System.out.println("Sum of doubleList: " + sumOfList(doubleList)); // 输出: Sum of doubleList: 6.6
}
}
// 下界通配符,表示可以接受 Number 或其父类型的列表,写入操作:使用下界通配符 ? super T 进行写入操作
public class LowerBoundedWildcardExample {
// 向列表中添加元素,假设列表中的元素是 Number 或其父类型
public static void addNumbers(List<? super Number> list) {
list.add(1); // 可以添加 Integer
list.add(1.1); // 可以添加 Double
list.add(1L); // 可以添加 Long
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println("numberList: " + numberList); // 输出: numberList: [1, 1.1, 1]
List<Object> objectList = new ArrayList<>();
addNumbers(objectList);
System.out.println("objectList: " + objectList); // 输出: objectList: [1, 1.1, 1]
}
}
|
3. 使用技巧
-
类型参数限制:使用 <T extends Class/Interface>
约束类型范围。
1
2
3
|
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
|
-
避免原生类型:始终使用泛型类型(如 List<String>
而非 List
),避免类型安全问题。
-
PECS 原则(Producer-Extends, Consumer-Super):
- 当从集合读取数据(生产者)时,使用
<? extends T>
,编译器知道集合中的元素至少是 T 类型或其子类型,因此可以安全地读取这些元素。
- 当向集合写入数据(消费者)时,使用
<? super T>
,编译器知道集合中的元素至少是 T 类型或其父类型,因此可以安全地向集合中添加 T 类型或其父类型的元素。
-
类型推断:利用编译器自动推断类型,简化代码。
1
|
List<String> list = new ArrayList<>(); // Java 7+ 后可用空尖括号
|
-
泛型与静态方法:静态方法不能使用类的类型参数
,需单独声明:
1
2
3
|
public class Box<T> {
public static <U> U staticMethod(U u) { return u; }
}
|
4. 代码示例合集
示例 1:泛型类与方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 泛型类
public class Pair<T, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
// 泛型方法
public static <V, W> Pair<V, W> create(V v, W w) {
return new Pair<>(v, w);
}
// Getter/Setter 省略
}
// 使用
Pair<String, Integer> pair = Pair.create("Age", 30);
|
示例 2:通配符与集合
1
2
3
4
5
6
7
8
9
|
// 合并两个列表(生产者)
public static void mergeLists(List<? extends Number> src, List<? super Number> dest) {
dest.addAll(src);
}
// 使用
List<Integer> intList = Arrays.asList(1, 2);
List<Number> numberList = new ArrayList<>();
mergeLists(intList, numberList);
|
5. 注意事项
- 类型擦除:泛型在编译后会被擦除(如
List<String>
变为 List
),运行时无法获取类型参数。
- 不可实例化泛型:
new T()
是非法的(可通过反射间接实现),这是因为泛型类型参数在编译时会被擦除(类型擦除),编译器无法确定具体的类型,因此无法实例化。
- 基本类型不支持:不能使用
List<int>
,需用包装类 List<Integer>
。
总结
泛型通过类型参数化显著提升了代码的安全性和复用性。合理使用泛型类、泛型方法、通配符和类型限制,可以写出更灵活且健壮的代码。注意类型擦除的局限性和 PECS 原则,以最大化泛型的优势。