背景

本文是《Java 后端从小白到大神》修仙系列第十五篇,正式进入Java后端世界,本篇文章主要聊Java基础。若想详细学习请点击首篇博文,我们开始把。

文章概览

  1. 泛型

泛型

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. 使用技巧

  1. 类型参数限制:使用 <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;
    }
    
  2. 避免原生类型:始终使用泛型类型(如 List<String> 而非 List),避免类型安全问题。

  3. PECS 原则(Producer-Extends, Consumer-Super):

    • 当从集合读取数据(生产者)时,使用 <? extends T>,编译器知道集合中的元素至少是 T 类型或其子类型,因此可以安全地读取这些元素。
    • 当向集合写入数据(消费者)时,使用 <? super T>,编译器知道集合中的元素至少是 T 类型或其父类型,因此可以安全地向集合中添加 T 类型或其父类型的元素。
  4. 类型推断:利用编译器自动推断类型,简化代码。

    1
    
    List<String> list = new ArrayList<>(); // Java 7+ 后可用空尖括号
    
  5. 泛型与静态方法静态方法不能使用类的类型参数,需单独声明:

    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 原则,以最大化泛型的优势。