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)就是另一个三元组,包含String
、Date
和Integer
三种类型。
接下来我们通过具体示例,探讨在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 类型安全警告
原始列表允许添加不同类型值(如Long
和OP
),因此能处理任意三元组结构。但使用原始列表会丧失类型安全,看这个踩坑例子:
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
/Long
和String
/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获取。