1. 概述

本文将详细介绍如何使用 ModelMapper 框架完成不同类型元素列表之间的映射。核心思路是借助 Java 的泛型机制,实现数据在不同 List 类型之间的转换。

✅ 重点:解决泛型擦除导致的类型丢失问题
⚠️ 踩坑提示:直接传 List<T>.class 是无效的,必须使用 TypeToken


2. ModelMapper 基础

ModelMapper 的核心功能是对象之间的属性映射,常用于实体类(Entity)与数据传输对象(DTO)之间的自动转换,减少手动 set/get 的样板代码。

要使用 ModelMapper,首先在 pom.xml 中引入依赖:

<dependency> 
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.2.0</version>
</dependency>

2.1. 配置建议

ModelMapper 提供了丰富的配置选项来控制映射行为。一个常见的最佳实践是开启字段匹配,尤其是对私有字段的支持:

ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
    .setFieldMatchingEnabled(true)  // 允许匹配私有字段
    .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE);

这样即使目标类的字段是 private,也能完成映射。

关于匹配策略(Matching Strategy),默认的 STANDARD 策略要求源和目标的所有属性都能按名称匹配,顺序不限。这种策略在大多数场景下表现良好,推荐保持默认。

2.2. TypeToken:解决泛型擦除的关键

Java 的泛型在运行时会被擦除,这会导致 ModelMapper 无法识别目标 List 的实际泛型类型。

举个例子,下面这段代码不会生效

List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
integers.add(3);

List<Character> characters = new ArrayList<>();
modelMapper.map(integers, characters);

结果你会发现 characters 是个空列表 —— 因为 JVM 在运行时只知道它是 List,不知道应该是 List<Character>

✅ 正确做法:使用 TypeToken 创建类型字面量,保留泛型信息:

List<Character> characters = modelMapper.map(
    integers, 
    new TypeToken<List<Character>>() {}.getType()
);

🔍 原理:通过匿名内部类的方式,JVM 能在编译期保留 <List<Character>> 这个类型信息,从而让 ModelMapper 正确解析目标类型。


3. 自定义泛型映射方法

实际开发中,我们经常需要把一个实体列表转成 DTO 列表。比如 List<User>List<UserDTO>

最简单的做法是对每个元素单独映射:

List<UserDTO> dtos = users
    .stream()
    .map(user -> modelMapper.map(user, UserDTO.class))
    .collect(Collectors.toList());

虽然能跑通,但重复代码太多。我们可以封装一个通用工具方法:

<S, T> List<T> mapList(List<S> source, Class<T> targetClass) {
    return source
        .stream()
        .map(element -> modelMapper.map(element, targetClass))
        .collect(Collectors.toList());
}

调用时就非常简洁了:

List<UserDTO> userDtoList = mapList(users, UserDTO.class);

✅ 推荐把这个方法抽到工具类中,项目里复用率极高。


4. TypeMap 与属性级映射

当需要更精细的控制时,比如只映射某个特定字段(如从 User 列表中提取用户名列表),可以使用 TypeMap + Converter 的组合拳。

假设我们有以下结构:

public class UserList {
    private List<User> users;
    // getter/setter
}

public class UserListDTO {
    private List<String> usernames;
    // getter/setter
}

目标是将 UserList.users 中每个用户的 username 提取出来,映射到 UserListDTO.usernames

步骤一:定义转换器

public class UsersListConverter extends AbstractConverter<List<User>, List<String>> {

    @Override
    protected List<String> convert(List<User> users) {
        return users
            .stream()
            .map(User::getUsername)
            .collect(Collectors.toList());
    }
}

步骤二:注册 TypeMap 和属性映射

ModelMapper modelMapper = new ModelMapper();

TypeMap<UserList, UserListDTO> typeMap = modelMapper.createTypeMap(UserList.class, UserListDTO.class);

// 显式指定字段映射,并使用自定义转换器
typeMap.addMapping(
    source -> source.getUsers(), 
    (destination, usernames) -> destination.setUsernames(usernames)
).setConverter(new UsersListConverter());

✅ 效果:users 列表 → usernames 字符串列表,一行代码搞定复杂逻辑。

⚠️ 注意:addMapping 的第一个参数是 source 的字段提取函数,第二个是 destination 的字段设置函数。


5. 总结

通过本文你应掌握以下三种 List 映射方式:

方式 适用场景 是否推荐
TypeToken 泛型 List 转换 ✅ 强烈推荐
mapList 工具方法 实体转 DTO 列表 ✅ 日常必备
TypeMap + Converter 复杂字段映射 ✅ 高级用法

ModelMapper 结合泛型、TypeToken 和自定义转换器,能够轻松应对各种对象列表映射需求,大幅减少手动转换的繁琐代码。

完整示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/core-java-modules/core-java-collections-conversions-2


原始标题:Mapping Lists with ModelMapper | Baeldung