Java基础 - 泛型

Introduction

范型的作用是在编译时推断参数的类型,提高代码重用性,并且将类型安全检查从 “运行期” 提前到 “编译期”,从而写出更安全。
在Java引入范型之前如果要定义一个通用容器,比如List:

import java.util.ArrayList;
import java.util.List;

List list = new ArrayList();

// 什么都能放进去,没有类型限制
list.add("Hello");
list.add(123);
list.add(new java.util.Date());

System.out.println("list.get(0) = " + list.get(0));

// 取出时必须强制类型转换
String firstItem = (String) list.get(0);

// RuntimeException: 编译时完全正常,因为编译器不知道list里第二个元素不是String
String secondItem = (String) list.get(1); // 程序崩溃

范型避免了这种“类型不匹配的错误在编译时无法被发现,只能等到代码运行时才暴露出来”的问题。

Generic Class

范型允许我们为类、接口和方法定义一个或多个类型参数。
语法:class ClassName<T1, T2, ...> { /* ... */ }

// T 是一个类型参数,代表 "Type"
public class Box<T> {
    private T item;

    public void set(T item) {
        this.item = item;
    }

    public T get() {
        return this.item;
    }
}

// 使用泛型类
public class GenericTypeExample {
    public static void main(String[] args) {
        // 实例化时,将 T 指定为 String
        Box<String> stringBox = new Box<>();
        stringBox.set("Hello, Generics!");
        // stringBox.set(123); // 编译错误!类型不匹配

        String content = stringBox.get();
        System.out.println(content);

        // 实例化时,将 T 指定为 Integer
        Box<Integer> integerBox = new Box<>();
        integerBox.set(42);
        int number = integerBox.get();
        System.out.println(number);
    }
}
  • stringBox 在编译时就被限定只能存储 String 类型,任何试图存入其他类型的操作都会导致编译失败。
  • 从 stringBox.get() 返回的就是 String 类型,无需进行类型转换

Generic Methods

泛型方法在编写静态工具类时尤其有用。
语法:public <T> ReturnType methodName(T parameter) { /* ... */ }

public class ArrayUtils {

    // 一个泛型方法,用于交换数组中的两个元素
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 另一个泛型方法,返回数组的中间元素
    public static <E> E getMiddleElement(E[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[array.length / 2];
    }
}

// 使用泛型方法
public class GenericMethodExample {
    public static void main(String[] args) {
        String[] names = {"Alice", "Bob", "Charlie"};
        ArrayUtils.swap(names, 0, 2); // 编译器会自动推断 T 是 String
        System.out.println(java.util.Arrays.toString(names)); // 输出: [Charlie, Bob, Alice]

        Integer[] numbers = {1, 2, 3, 4, 5};
        Integer middleNumber = ArrayUtils.getMiddleElement(numbers); // E 被推断为 Integer
        System.out.println(middleNumber); // 输出: 3
    }
}
  • <T> 的作用域仅限于该方法本身
  • 类型参数 <T> 声明在方法的返回类型之前
  • 范型方法允许在调用时动态指定类型,而不是固定使用某种类型

Generic Interface

Comparable<T> 接口就是一个典型的泛型接口。

public interface Comparable<T> {
    public int compareTo(T o);
}

public class Student implements Comparable<Student> { // 指定 T 就是 Student
    private String name;
    private int age;

    @Override
    public int compareTo(Student other) { // 参数直接就是 Student 类型,无需强制转换
        return this.age - other.age; 
    }
}

Wildcards

上界通配符(? extends T)

未知类型 (?) 是 T 或 T 的某个子类型。

import java.util.List;
import java.util.Arrays;

public class UpperBoundWildcard {

    // 使用上界通配符,表示这个 List 包含的是 Number 或者 Number 的某个子类
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number n : list) {
            // 我们可以安全地从中读取数据,并转型为 Number
            sum += n.doubleValue();
        }
        // list.add(Integer.valueOf(1)); // 编译错误!
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        System.out.println("Sum = " + sumOfList(intList)); // 合法

        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
        System.out.println("Sum = " + sumOfList(doubleList)); // 合法
    }
}
  • 这里的addNumbers函数可以接收List<Integer>, List<Double>等,只要是Number的子类.

下界通配符 (? super T)

未知类型 (?) 是 T 或 T 的某个父类型。

import java.util.List;
import java.util.ArrayList;

public class LowerBoundWildcard {

    // 使用下界通配符,表示这个 List 的类型是 Integer 或是 Integer 的某个父类
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 3; i++) {
            // 我们可以安全地向其中添加 Integer 对象
            list.add(i);
        }
        // Object obj = list.get(0); // 读取时只能保证是 Object 类型
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        addNumbers(intList);
        System.out.println(intList); // 输出: [1, 2, 3]

        List<Number> numList = new ArrayList<>();
        addNumbers(numList);
        System.out.println(numList); // 输出: [1, 2, 3]

        List<Object> objList = new ArrayList<>();
        addNumbers(objList);
        System.out.println(objList); // 输出: [1, 2, 3]
    }
}
  • 这里的addNumbers函数可以接收List<Integer>, List<Number>, List<Object>.

实际应用

封装统一的 API 响应

在开发 RESTful API 时,我们通常希望返回统一的 JSON 结构,例如:
{ “success”: true, “code”: 200, “message”: “操作成功”, “data”: … }
这里的 data 字段内容是动态变化的,可能是单个用户信息,也可能是商品列表。这时可以使用范型响应类来解决:

public class ApiResponse<T> {
    private boolean success;
    private int code;
    private String message;
    private T data; // data 的类型由 T 决定

    // 构造函数、Getter、Setter...

    public static <T> ApiResponse<T> success(T data) {
        // ... 返回一个表示成功的 ApiResponse 实例
    }
}

// 在 Controller 中的应用
@GetMapping("/users/{id}")
public ApiResponse<UserDTO> getUserById(@PathVariable Long id) {
    UserDTO user = userService.findUserById(id);
    return ApiResponse.success(user);
}

@GetMapping("/products")
public ApiResponse<List<ProductDTO>> listProducts() {
    List<ProductDTO> products = productService.findAll();
    return ApiResponse.success(products);
}

创建通用的服务层/数据访问层

在典型的分层架构中,许多实体(如 User, Product, Order)的增删改查逻辑非常相似。为每个实体都写一套重复的 DAO/Repository 和 Service 是非常繁琐的。这时,可以利用泛型来抽象出一个通用的基类或接口。以下是一个用MyBatis的例子:

创建通用的 BaseMapper 接口:

// BaseMapper.java
// 这个接口不需要 @Mapper 注解,因为它不会被直接实例化
public interface BaseMapper<T, ID> {

    /**
     * 根据ID查找实体
     * @param id 主键
     * @return 实体对象
     */
    T findById(ID id);

    /**
     * 插入一个实体
     * @param entity 实体对象
     * @return 影响的行数
     */
    int insert(T entity);

    /**
     * 更新一个实体
     * @param entity 实体对象
     * @return 影响的行数
     */
    int update(T entity);

    /**
     * 根据ID删除
     * @param id 主键
     * @return 影响的行数
     */
    int deleteById(ID id);
}
  • 这个接口定义了一个契约:任何实现了 BaseMapper 的子接口,都将拥有这四个基本的 CRUD 方法。T 和 ID 此时是占位符。

创建具体的业务 Mapper 接口:

// UserMapper.java
@Mapper
public interface UserMapper extends BaseMapper<User, Long> {
    // 继承了 findById(Long), insert(User), update(User), deleteById(Long)
    // 只需在这里添加 User 特有的方法
    User findByUsername(String username);
}

// ProductMapper.java
@Mapper
public interface ProductMapper extends BaseMapper<Product, Long> {
    // 同理
    List<Product> findByStatus(int status);
}

编写xml语句:

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "[http://mybatis.org/dtd/mybatis-3-mapper.dtd](http://mybatis.org/dtd/mybatis-3-mapper.dtd)">
<mapper namespace="com.example.mapper.UserMapper">

    <select id="findById" resultType="com.example.model.User">
        SELECT * FROM users WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="com.example.model.User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users (username, password) VALUES (#{username}, #{password})
    </insert>

    <select id="findByUsername" resultType="com.example.model.User">
        SELECT * FROM users WHERE username = #{username}
    </select>

</mapper>

创建通用的 BaseService 抽象类:

// BaseService.java
// 这是一个抽象类,不能被直接实例化
public abstract class BaseService<T, ID> {

    // 使用 protected,子类也可以访问
    protected BaseMapper<T, ID> mapper;

    // 通过构造函数,强制子类传入具体的 Mapper 实例
    public BaseService(BaseMapper<T, ID> mapper) {
        this.mapper = mapper;
    }

    // 通用的业务逻辑
    public T findById(ID id) {
        return mapper.findById(id);
    }

    public int save(T entity) {
        // 其他逻辑...
        return mapper.insert(entity);
    }
}
  • 抽象类:它依赖一个不确定的 BaseMapper,它本身不能独立工作,必须由一个具体的子类来提供具体的 Mapper。
  • 构造函数注入:我们不能直接 @Autowired BaseMapper,因为 Spring 不知道该注入 UserMapper 还是 ProductMapper。所以,我们要求子类在创建时,必须通过构造函数把它自己对应的具体 Mapper 传给父类。

创建具体的业务 Service 类:

// UserService.java
@Service
public class UserService extends BaseService<User, Long> {

    // Spring 会自动找到 UserMapper 的实例并注入到这个构造函数中
    @Autowired
    public UserService(UserMapper userMapper) {
        // 调用父类的构造函数,将具体的 userMapper 传递进去
        // 这样,父类 BaseService 中的 mapper 字段就持有了 UserMapper 的引用
        super(userMapper);
    }
    // UserService继承了BaseService的基础的CRUD逻辑

    // 在这里编写 User 特有的业务逻辑
    public User login(String username, String password) {
        User user = ((UserMapper) this.mapper).findByUsername(username); // 需要强转来调用子类特有方法
        if (user != null && user.getPassword().equals(password)) {
            return user;
        }
        return null;
    }
}

Java基础 - 泛型
http://example.com/2025/07/27/JAVA/generics/
Author
Songlin Zhao
Posted on
July 27, 2025
Licensed under