1. 概述

本文将先解释什么是三元组(Triple),然后讨论如何在Java的ArrayList中存储三元组元素。

2. 什么是三元组?

你可能熟悉Pair类型(键值对),三元组与之类似,但三元组始终包含三个值而非两个。例如3D坐标(x=-100L, y=0L, z=200L)就是典型的三元组结构。

在这个3D坐标例子中,三个值类型相同(都是Long)。但三元组中的三个值类型不必相同。比如足球运动员数据(name="Lionel Messi", birthday=1987年6月24日(Date), number=10)就是另一个三元组,包含StringDateInteger三种类型。

接下来我们通过具体示例,探讨在ArrayList中存储三元组的最佳实践。

3. 示例:算术题生成器

假设我们要为小学生开发一个算术题生成器。例如"100 + 200 = ?"这样的题目由三个部分组成:第一个数字、运算符和第二个数字。我们将这三部分存储为三元组。

首先定义支持的运算符枚举:

enum OP {
    PLUS("+"), MINUS("-"), MULTIPLY("x");
    final String opSign;
                                         
    OP(String x) {
        this.opSign = x;
    }
}

这里只支持三种运算符。题目生成逻辑很简单,先创建一个方法将三个部分组合成题目:

String createQuestion(Long num1, OP operator, Long num2) {
    long result;
    switch (operator) {
        case PLUS:
            result = num1 + num2;
            break;
        case MINUS:
            result = num1 - num2;
            break;
        case MULTIPLY:
            result = num1 * num2;
            break;
        default:
            throw new IllegalArgumentException("Unknown operator");
    }
    return String.format("%d %s %d = ? ( answer: %d )", num1, operator.opSign, num2, result);
}

当三元组存储在列表中时,我们可以把三个值传入此方法生成题目。为简化验证,使用单元测试断言检查预期输出:

List<String> EXPECTED_QUESTIONS = Arrays.asList(
    "100 - 42 = ? ( answer: 58 )",
    "100 + 42 = ? ( answer: 142 )",
    "100 x 42 = ? ( answer: 4200 )");

现在考虑核心问题:如何在列表中存储三元组结构?

虽然针对当前场景可以创建QuestionInput类,但我们的目标是通用地存储三元组,需同时解决算术题、3D坐标和足球运动员数据等不同场景。通常有两种思路:

  • List<List> – 将三个值存入列表/数组(本文以List为例),再嵌套到外层列表:List<List>
  • List<Triple<...>> – 创建泛型Triple

接下来分析这两种方案的优缺点。

4. 用列表存储三元组

原始列表(Raw List)可添加任意类型元素,下面演示具体实现。

4.1 将三元组存储为三元素列表

为每个三元组创建列表,添加三个值后存入外层列表:

List myTriple1 = new ArrayList(3);
myTriple1.add(100L);
myTriple1.add(OP.MINUS);
myTriple1.add(42L);

List myTriple2 = new ArrayList(3);
myTriple2.add(100L);
myTriple2.add(OP.PLUS);
myTriple2.add(42L);

List myTriple3 = new ArrayList(3);
myTriple3.add(100L);
myTriple3.add(OP.MULTIPLY);
myTriple3.add(42L);

List<List> listOfTriples = new ArrayList<>(Arrays.asList(myTriple1, myTriple2, myTriple3));

创建了三个原始ArrayList承载三元组,最后添加到外层列表listOfTriples

4.2 类型安全警告

原始列表允许添加不同类型值(如LongOP),因此能处理任意三元组结构。但使用原始列表会丧失类型安全,看这个踩坑例子:

List oopsTriple = new ArrayList(3);
oopsTriple.add("Oops");
oopsTriple.add(911L);
oopsTriple.add("The type is wrong");

listOfTriples.add(oopsTriple);
assertEquals(4, listOfTriples.size());

oopsTriple携带了完全不同的三元组结构,但listOfTriples毫无怨言地接受了它。现在listOfTriples包含两种三元组:Long/OP/LongString/Long/String。使用时必须检查类型是否符合预期。

4.3 使用列表中的三元组

理解优缺点后,看如何用listOfTriples生成算术题:

List<String> questions = listOfTriples.stream()
    .filter(
        triple -> triple.size() == 3
          && triple.get(0) instanceof Long
          && triple.get(1) instanceof OP
          && triple.get(2) instanceof Long
    ).map(triple -> {
        Long left = (Long) triple.get(0);
        String op = (String) triple.get(1);
        Long right = (Long) triple.get(2);
        return createQuestion(left, op, right);
    }).collect(Collectors.toList());

assertEquals(EXPECTED_QUESTIONS, questions);

使用Java流API的map()转换三元组列表为题目列表。但因原始列表类型不保证,必须检查每个元素是否符合Long/OP/Long类型。因此在map()前调用filter()跳过异常三元组(如String/Long/String)。

更坑的是,必须显式类型转换才能将值传给createQuestion()。测试虽通过,但此方案存在明显缺陷:

优点:无需创建新类即可存储任意三元组
缺点:丧失类型安全,使用前必须类型检查和转换
⚠️ 后果:代码可读性差、难维护、易出错,不推荐

5. 创建泛型Triple

现在探索更简单且类型安全的三元组存储方案。

5.1 泛型Triple

Java泛型提供类型安全,创建泛型Triple类:

public class Triple<L, M, R> {

    private final L left;
    private final M middle;
    private final R right;

    public Triple(L left, M middle, R right) {
        this.left = left;
        this.middle = middle;
        this.right = right;
    }

    public L getLeft() {
        return left;
    }

    public M getMiddle() {
        return middle;
    }

    public R getRight() {
        return right;
    }
}

代码简单直接。这里将Triple设为不可变,如需可变版本,移除final关键字并添加setter即可。

5.2 初始化三元组并存入列表

创建三个三元组对象并添加到列表:

Triple<Long, OP, Long> triple1 = new Triple<>(100L, OP.MINUS, 42L);
Triple<Long, OP, Long> triple2 = new Triple<>(100L, OP.PLUS, 42L);
Triple<Long, OP, Long> triple3 = new Triple<>(100L, OP.MULTIPLY, 42L);

List<Triple<Long, OP, Long>> listOfTriples = new ArrayList<>(Arrays.asList(triple1, triple2, triple3));

泛型Triple类自带类型参数,代码中无原始类型使用,且类型安全。测试添加非法三元组:

Triple<String, Long, String> tripleOops = new Triple<>("Oops", 911L, "The type is wrong");
listOfTriples.add(tripleOops);

编译直接报错:

java: incompatible types: 
com...Triple<...String, ...Long, ...String> cannot be converted to com...Triple<...Long, ...OP, ...Long>

类型安全机制完美避开了类型错误陷阱

5.3 使用Triple元素

类型安全下,无需类型检查和转换即可直接使用值:

List<String> questions = listOfTriples.stream()
  .map(triple -> createQuestion(triple.getLeft(), triple.getMiddle(), triple.getRight()))
  .collect(Collectors.toList());

assertEquals(EXPECTED_QUESTIONS, questions);

测试通过。相比列表方案,代码更简洁易读。

6. 结论

本文通过示例探讨了在列表中存储三元组的两种方式,分析了为何应创建泛型Triple类而非使用列表存储三元组。

所有代码片段可在GitHub获取。


原始标题:Storing Data Triple in a List in Java | Baeldung