# 元组:javatuples

# 1. 问题

有时你会有类似如下的需求:

  1. 逻辑上,方法需要返回两个甚至更多的值(通常它们的类型并不一致,因此无法使用数组)。形如:

    return "tom", 18; // 有些语言支持这种语法,但是 Java 并没有这个语法特性。
    
  2. 有时,你需要向方法传入类似于多个学生的姓名和年龄:

    demo("tom", 19, "jerry", 18, ...) // 当然这里可以使用不定参/可变参语法,但是可读性并不是很好,不直观。
    

对于上述的情况,你可以采用这样的解决办法:

  1. 将多个数据封装到一个 Map 中,或者定义一个类(例如 Student),将数据封装到对象中:

    map.put("name", "tom");
    map.put("age", 18);
    return map;
    
    // 或
    
    return new Student("name", 18);
    
  2. 对于第二个问题,可以使用数组

    names[0] = "tom";
    ages[0] = 19;
    
    names[1] = "jerry";
    ages[1] = 18;
    
    demo(names, ages);
    

不过,在使用上述方案实现相关需求后,你可能会有如下想法:

  1. 使用 Map 或自定义类有点『杀鸡用牛刀』的感觉;
  2. 使用两个数组的时候,将一个人的两个信息分开存放,感觉又有点怪怪的。

# 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 容器中取值,有两种方式:

  1. 通过 .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() );
    
  2. 通过 .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)) {
    ...
}