# 元组:javatuples
# 1. 问题
有时你会有类似如下的需求:
逻辑上,方法需要返回两个甚至更多的值(通常它们的类型并不一致,因此无法使用数组)。形如:
return "tom", 18; // 有些语言支持这种语法,但是 Java 并没有这个语法特性。
有时,你需要向方法传入类似于多个学生的姓名和年龄:
demo("tom", 19, "jerry", 18, ...) // 当然这里可以使用不定参/可变参语法,但是可读性并不是很好,不直观。
对于上述的情况,你可以采用这样的解决办法:
将多个数据封装到一个 Map 中,或者定义一个类(例如 Student),将数据封装到对象中:
map.put("name", "tom"); map.put("age", 18); return map; // 或 return new Student("name", 18);
对于第二个问题,可以使用数组
names[0] = "tom"; ages[0] = 19; names[1] = "jerry"; ages[1] = 18; demo(names, ages);
不过,在使用上述方案实现相关需求后,你可能会有如下想法:
- 使用 Map 或自定义类有点『杀鸡用牛刀』的感觉;
- 使用两个数组的时候,将一个人的两个信息分开存放,感觉又有点怪怪的。
# 2. tuples
数据结构领域中有一个较少提及的数据结构:『元组』(tuple) 。有些语言中,天生就有 tuple 类型的变量,但是 Java 中没有(其实,常见的编程语言中,大多数都没有)。
tuple 结构可以和数组做对比:
- 相同点在于:tuple 和数组一样,作为容器,其中可以存放多个值。并且,它和数组一样有下标索引的概念。
- 不同点在于:tuple 不强求其中的各个数据的类型必须一致。
当然,我们可以自己实现 tuple 数据结果(相较于 List、Set 它其实简单很多)。不过,很显然有现成的:javatuples (opens new window)
<dependency>
<groupId>org.javatuples</groupId>
<artifactId>javatuples</artifactId>
<version>1.2</version>
</dependency>
由于 tuple 数据结构的功能/作用实在是比较简单(有时可能项目中就有程序员自己随手就实现了它),所以这个库,在 2011 年就 “彻底” 完成了所有功能,不再升级更新了。因此,最后一个版本就是 1.2
。
# 2.1 The tuple classes
该工具库提供了以下不同容量的 tuple 类:
类 | 容量 |
---|---|
Unit<A> | 1 element |
Pair<A,B> | 2 elements |
Triplet<A,B,C> | 3 elements |
Quartet<A,B,C,D> | 4 elements |
Quintet<A,B,C,D,E> | 5 elements |
Sextet<A,B,C,D,E,F> | 6 elements |
Septet<A,B,C,D,E,F,G> | 7 elements |
Octet<A,B,C,D,E,F,G,H> | 8 elements |
Ennead<A,B,C,D,E,F,G,H,I> | 9 elements |
Decade<A,B,C,D,E,F,G,H,I,J> | 10 elements |
提示
我也是很服气作者能为每一种容量的 tuple 类都单独起了个名字!
由于上述 tuple 类并没有什么语义,所以,作者额外地为两个常见的情况提供了单独的 tuple 类。
KeyValue<A,B>
LabelValue<A,B>
实际上,它们俩就是 Pair
的别名。
# 2.2 Creating tuples
所有类型的 tuple 都可以通过 new
来创建:
Pair<Integer, Integer> pair = new Pair<>(10, 20);
Triplet<String, Integer, Date> triplet = new Triplet<>("hello", 10, new Date());
...
于此同时,tuple 还提供了静态方法 .with()
来创建各种 ruple 类的实例:
Pair<Integer, Integer> pair = Pair.with(10, 20);
Triplet<String, Integer, Date> triplet = Triplet.with("hello", 10, new Date());
# 2.3 Getting/Setting values
tuple 数据结构在概念上是下标索引的,但是你不能想当然地对其使用下标运算符 []
。
从一个 tuple 容器中取值,有两种方式:
通过
.getValueN()
方法:System.out.println( pair.getValue0() ); System.out.println( pair.getValue1() ); System.out.println( triplet.getValue0() ); System.out.println( triplet.getValue1() ); System.out.println( triplet.getValue2() );
通过
.getValue(index)
方法:System.out.println( pair.getValue(0) ); System.out.println( pair.getValue(1) ); System.out.println( triplet.getValue(0) ); System.out.println( triplet.getValue(1) ); System.out.println( triplet.getValue(2) );
不过
.getValue(index)
方法取出来的值统一都是Object
类型,后续使用时需要做类型转换。
优先考虑使用 .getValueN()
方法 。
另外,大家都能猜到,既然有 get 方法,这里也自然有 set 方法:
pair.setAt0(xxx);
pair.setAt1(xxx);
triplet.setAt0(xxx);
triplet.setAt1(xxx);
triplet.setAt2(xxx);
KeyValue
and LabelValue
这两种『额外』的 tuple 类型中,它们的 getting/setting 方法是叫:getKey()
/ getValue()
和 getLabel()
/ getValue()
。
# 2.4 Adding or removing elements
当你向一个 Pair 对象中添加元素时,你将获得一个 Triplet 对象;当你从一个 Triplet 对象中移除一个元素时,你将获得一个 Pair 对象。
也就是说,任何一种 tuple 类的容量是不可改变的。
Pair<Integer, Integer> pair = Pair.with(10, 20);
Triplet<Integer, Integer, Integer> triplet = pair.add(30);
System.out.println( triplet ); // 10, 20, 30
调用 .add()
方法时,添加的元素将被添加到末尾。
另外,tuple 还提供 .addAtN()
方法。将要添加的元素添加到指定位置,而原位置(及后续内容)依次后移。
triplet = pair.addAt1(30);
System.out.println( triplet ); // 10, 30, 20
从 tuple 中移除元素使用 .removeFromN()
方法。
pair = triplet.removeFrom0();
# 2.5 Converting to/from collections or arrays
任何一种 tuple 都可以转换成 List 或数组:
Object[] array = triplet.toArray();
List<Object> list = triplet.toList();
反向的操作有:
String[] array = ...
...
Quartet<String,String,String,String> quartet = Quartet.fromArray(array);
这里需要注意的是,由于 Array 和 List 是要求其中元素类型是一致的。所以,从 tuple 转成数组和 List 时,会失去元素的具体类型,从而得到一个 Object 的数组和 List 。如果这样处理,那么就失去了 tuple 的使用价值。
同样,对于一个一致的某种类型的数组和 List,转换成 tuple 时,其元素类型必然也都是一样的,这样也就没有必要去使用 tuple 了,为什么不直接使用这个数组和 List 呢。
# 2.6 Iterating
由于所有类型的 tuple 都是『可循环』对象,所以可以直接用便捷 for 循环对其进行遍历:
for (Object value : triplet) {
...
}
不过,这里失去了每个元素的具体类型,只能将它们统一当作 Object 来看待。
# Checking contents
tuple 提供了方法用来判断 tuple 中是否包含某个/某些对象:
if (quartet.contains(value)) {
...
}
if (quartet.containsAll(valueCollection)) {
...
}