Java SE Generics

Java SE Generics

Introduction

泛型(Generics)是编程中非常实用的工具,它让我们可以编写适用于多种数据类型的代码,而不需要为每种类型重复编写。可以把它想象成一个灵活的模板,可以根据不同的需求进行调整。

举个例子,如果需要编写一个函数来排序数字列表,在没有泛型的情况下,可能需要分别为整数、浮点数等写不同的函数。而有了泛型,只需要写一个函数,就能处理所有类型的数字排序问题。

Java中泛型设计背景

集合容器类在设计阶段 / 声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在 JDK1.5 之前只能把元素类型设计为 Object,这样就会导致在取出元素时需要强制类型转换,而这种强制类型转换是不安全的,可能会在运行时抛出 ClassCastException 异常。

而引入泛型后,可以在声明集合容器时指定容器中存放的元素类型,这样在编译阶段就可以进行类型检查,避免了强制类型转换导致运行时异常,提高了代码的安全性和可读性。

Advantages of Generics

  • Type Safety & Elimination of casts:Java 中的泛型提供了 compile-time 的 type-checking,能减少类型转换的操作。
  • Reduce code duplication:通过使用泛型,提升代码的复用性和扩展性,使代码更加简洁。
  • Readability:泛型能够更清晰地表达代码的意图,从而让代码更具可读性和可维护性。
1
2
3
4
5
6
7
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 编译时报错,因为集合只能存储 String 类型

for (String str : list) { // 不需要强制转换
    System.out.println(str);
}

Syntax

Java generics have the following syntax:

  • Generic class: class MyClass<T> { ... }
  • Generic interface: interface MyInterface<T> { ... }
  • Generic method: public <T> void myMethod(T data) { ... }

Naming conventions for generics:

  • T: Type parameter
  • E: Element or Entry type
  • K, V: Key and Value types

Finally, again let’s take note of the naming convention used for the type parameters. We use T for type, whenever there isn’t anything more specific about the type to distinguish it. This is often the case in generic methods. If there are multiple type parameters, we might use letters that neighbor T in the alphabet, such as S. If a generic method appears inside a generic class, it’s a good idea to avoid using the same names for the type parameters of the method and class, to avoid confusion. The same applies to nested generic classes.
Oracle Java Tutorials

Use Generic In Java

在 Java 中使用泛型可以提高代码的类型安全性和可重用性,但也有一些需要注意的地方和使用限制。

Best Practices

Method Overload

重载方法在泛型类型经过类型擦除后可能会产生冲突,应避免在同一类中定义仅泛型类型参数不同的方法重载

1
2
3
4
5
// 编译器报错:Erasure of method method(List<String>) is the same as another method in type Main
public class Main {
    public void method(List<String> list) { ... }
    public void method(List<Integer> list) { ... }
}

Utilize Type Inference

JDK 7 引入了Type Inference功能,指编译器根据上下文自动推断泛型类型参数的能力。它使得代码更加简洁易读,避免了重复的类型声明。

Java 泛型中的 类型推断 (Type Inference) 是指编译器根据上下文自动推断泛型类型参数的能力。它使得代码更加简洁易读,避免了重复的类型声明。

类型推断的优势:

  • 提高代码可读性: 减少冗余的类型参数,使代码更易于理解和维护。
  • 减少代码量: 避免显式指定类型参数,简化代码编写。
  • 增强代码灵活性: 使得代码更容易适应变化,例如,当修改一个方法的返回类型时,不需要更改所有调用该方法的地方的类型参数。

类型推断的场景:

  • constructor
  • method invocation
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
// 1. Constructor
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
// You can substitute the parameterized type of the constructor with an empty set of type parameters (<>):
Map<String, List<String>> myMap = new HashMap<>();


// 2. Method invocation
// 代码引用自@pdai: https://pdai.tech/md/java/basic/java-basic-x-generic.html
public class Test {
	public static void main(String[] args) {

		/** 不指定泛型的时候 */
		int i = Test.add(1, 2); // 这两个参数都是Integer,所以T为Integer类型
		Number f = Test.add(1, 1.2); // 这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
		Object o = Test.add(1, "asd"); // 这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object

		/** 指定泛型的时候 */
		int a = Test.<Integer>add(1, 2); // 指定了Integer,所以只能为Integer类型或者其子类
		int b = Test.<Integer>add(1, 2.2); // 编译错误,指定了Integer,不能为Float
		Number c = Test.<Number>add(1, 2.2); // 指定为Number,所以可以为Integer和Float
	}

	// 这是一个简单的泛型方法
	public static <T> T add(T x, T y) {
		return y;
	}
}

Avoid using Raw Types

在下面的代码中,使用了 Raw type List,这在现代 Java 中是不推荐的。Raw type 是为了向后兼容 Java 5 之前的代码而存在的,但它们绕过了泛型类型检查,可能会导致运行时的 ClassCastException

1
2
3
4
5
6
// Warning: List is a raw type. References to generic type List<E> should be parameterized
List list = new ArrayList();
list.add("Hello");

// Recommended: Use generic type List<String>
List<String> list = new ArrayList<>();

Restrictions on Generics

  • Cannot Instantiate Generic Types with Primitive Types
  • Cannot Create Instances of Type Parameters
  • Cannot Declare Static Fields Whose Types are Type Parameters
  • Cannot Use Casts or instanceof With Parameterized Types
  • Cannot Create Arrays of Parameterized Types
  • Cannot Create, Catch, or Throw Objects of Parameterized Types
  • Cannot Overload a Method Where the Formal Parameter Types of Each Overload Erase to the Same Raw Type

for more information, see Generic Restrictions —— docs.oracle

Design Effectively Using Generics

Wildcards

在 Java 泛型中,通配符(Wildcard) 是一个用来表示不确定类型的符号,常用的通配符有 ? 和与之相关的上界通配符 ? extends T、下界通配符 ? super T

  • Upper Bounds Wildcards ? extends T? 只能是 TT 的子类。编译后,? extends T 会被擦除为 T
  • Lower Bounds Wildcards? super T? 只能是 TT 的父类。
diagram

PECS

PECS(Producer Extends, Consumer Super)原则是 Java 泛型编程中的一个重要指导原则,用于确保类型安全和代码的正确性。

PECS 原则是指在使用泛型时,如果参数用来生产数据(Producer),则使用上界通配符 ? extends T;如果参数用来消费数据(Consumer),则使用下界通配符 ? super T

Producer ExtendsConsumer Superdescription
copy源数据集合 src被写入的集合 destpublic static <T> void copy(List<? super T> dest, List<? extends T> src)
sort传入的集合 listpublic static <T extends Comparable<? super T>> void sort(List<T> list)
compare比较器 comparator,读取两个参数public static <T> int compare(T a, T b, Comparator<? super T> c)

集合类Collections中的copy方法是一个很好的例子,它使用了 PECS 原则。copy方法的参数中,src 是生产者,用于读取数据;dest 是消费者,用于写入数据。因此,src 使用了上界通配符 ? extends Tdest 使用了下界通配符 ? super T;保证dest中的元素类型是src中元素类型的父类,避免ClassCastException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * @param dest 被拿来写入数据,作为consumer,使用super通配符
 * @param src 被拿来读取数据,作为producer,使用extends通配符
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

Generic Class

泛型类的实现方式与非泛型类的区别在于,泛型类包含类型参数(type parameters)。类型参数可以有多个,使用逗号分隔。泛型类通常用于创建通用的数据结构,例如集合类(ArrayList<T> e.g)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyClass<T> {
    private T data;

    public void setData(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }
}

public class Main {
    public static void main(String[] args) {
        MyClass<String> myClass = new MyClass<>();
        myClass.setData("Hello");
        String str = myClass.getData();
        System.out.println(str); // Output: Hello
    }
}

编写泛型类时需注意,静态方法不能直接引用泛型类型参数 T,原因与类加载机制以及类型参数的绑定时机紧密相关。

Why can’t static fields use type parameters?

A class’s static field is a class-level variable shared by all non-static objects of the class. Hence, static fields of type parameters are not allowed. Consider the following class:

1
2
3
4
5
6
7
8
public class MobileDevice<T> {
    private static T os;
    // ...
}
// If static fields of type parameters were allowed, then the following code would be confused:
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

Because the static field os is shared by phone, pager, and pc, what is the actual type of os? It cannot be Smartphone, Pager, and TabletPC at the same time. You cannot, therefore, create static fields of type parameters.

如果需要让静态成员使用泛型,可以通过以下几种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Pair<T> {
    private T key;
    private T value;
    public Pair(T key, T last) {
        this.key = value;
        this.value = valuet;
    }
    public T getFirst() { ... }
    public T getLast() { ... }

    // a. 使用泛型方法
    // 将泛型类型参数定义在方法级别,而不是类级别。
    public static <K> Pair<K> create(K first, K last) {
        return new Pair<K>(first, last);
    }
    // b. 使用通配符或具体类型
    // 在静态方法中使用通配符或具体类型参数,而不是类的泛型类型参数
    public static void staticMethod(List<?> list) {...}
    // 使用具体类型
    public static void staticMethod(List<String> list) {...}
}

Generic Interface

与泛型类相似,泛型接口也可以定义类型参数。class 在实现泛型接口时,可以指定具体的类型参数,也可以保留泛型

下面是一个泛型接口的示例:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.util.HashMap;
import java.util.Map;

interface Repository<T> {
    void add(T item);

    T getById(String id);

    void delete(String id);
}

class Computer {
    private String id;
    private String name;
    private double price;

    public Computer(String id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // getter
    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getPrice() {
        return price;
    }

    @Override
    public String toString() {
        return "Computer{id='" + id + "', name='" + name + "', price=" + price + '}';
    }
}

class ComputerRepository implements Repository<Computer> {
    private Map<String, Computer> computers = new HashMap<>();

    @Override
    public void add(Computer computer) {
        computers.put(computer.getId(), computer);
    }

    @Override
    public Computer getById(String id) {
        return computers.get(id);
    }

    @Override
    public void delete(String id) {
        computers.remove(id);
    }
}

public class Main {
    public static void main(String[] args) {
        Repository<Computer> computerRepository = new ComputerRepository();

        // Add computers to the repository
        computerRepository.add(new Computer("1", "RedMi", 999.99));
        computerRepository.add(new Computer("2", "Lenovo", 499.99));

        // Retrieve and display a computer
        Computer laptop = computerRepository.getById("1");
        System.out.println("Retrieved: " + laptop);

        // Delete a computer
        computerRepository.delete("1");
        System.out.println("computer with ID 1 deleted.");

        // Attempt to retrieve the deleted computer
        Computer deletedcomputer = computerRepository.getById("1");
        System.out.println("After deletion, retrieved: " + deletedcomputer);
    }
}

[OUTPUT]
Retrieved: Computer{id='1', name='RedMi', price=999.99}
computer with ID 1 deleted.
After deletion, retrieved: null

Generic Method

泛型方法在方法的返回类型之前使用尖括号 < > 声明类型参数。

为什么要使用泛型方法呢?
因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新 new 一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

java.util.Collections 类提供了许多泛型方法,用于操作集合。例如,sort 方法使用泛型来对任意类型的列表进行排序

1
2
3
4
5
6
7
8
9
public class Collections {
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }

    public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
    }
}

java.util.Arrays类提供了许多泛型方法,用于操作数组。例如,asList 方法使用泛型将数组转换为列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Arrays {
    @SafeVarargs
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
}

// usage example
public class Main {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("Banana", "Apple", "Cherry");
        Collections.sort(list);
        System.out.println(list); // 输出: [Apple, Banana, Cherry]
    }
}

java.util.stream.Stream 接口定义了许多非静态的泛型方法,例如 flatMapmapcollect 等。

1
2
3
4
5
public interface Stream<T> extends BaseStream<T, Stream<T>> {
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    <R, A> R collect(Collector<? super T, A, R> collector);
}

Reference

Generic Methods The Java™ Tutorials
泛型 - Java 教程 - 廖雪峰的官方网站
Generic Restrictions
Type Inference

作者

GnixAij

发布于

2023-09-15

更新于

2025-01-14

许可协议

评论