函数扩展技术 2:元组





5.00/5 (2投票s)
这是我的开源库 functionExtensions 中关于 Tuple 类注意事项和技术的三个章节中的第二篇。
functionExtensions 是一个 Java 库,实现了可抛出异常的函数式接口、元组 (Tuples) 和存储库 (Repositories),旨在加速 JAVA 8 的函数式编程。它已在 Maven 上发布,并实现了以下目标:
- 声明了一系列丰富的高级函数式接口,它们可以抛出异常,并能通过共享的 `exceptionHandler` 转换为常规接口,处理已检查的异常,从而允许开发人员仅在 lambda 表达式中定义相关的业务逻辑。
- 实现了一个不可变的数据结构,可以存储和检索最多 20 个强类型值,作为一组 `Tuple` 类。
- 提供存储库 (Repositories),这是一种基于 Map 的智能实用工具,具有预定义的业务逻辑,用于评估给定的一个或多个键(最多 7 个强类型值作为单个 Tuple 键)以获取相应的值或值(最多 7 个强类型值作为单个 Tuple 值),并在未发生异常时进行缓冲并返回。
- 多种强大的泛型实用工具来支持上述三种类型的工具,主要基于存储库构建,支持原始类型和对象类型以及数组的各种组合。例如:
Object getNewArray(Class clazz, int length)
:以指定的元素类型和长度创建 **任意** 类型的新数组实例。String deepToString(Object obj)
:返回指定数组的“深层内容”的字符串表示形式。Object copyOfRange(Object array, int from, int to)
:将数组的全部或部分复制为同类型的新数组,无论数组是由原始值还是其他类型组成。T convert(Object obj, Class<T> toClass)
:将对象转换为任何等效或可分配的类型。boolean valueEquals(Object obj1, Object obj2)
:比较任何两个对象。如果两者都是数组,则通过将原始值视为与其包装器相等、null 和空数组元素视为具有预定义默认策略来比较它们。
本系列文章包括:
引言
在使用像 JAVA 和 C# 这样的面向对象编程语言时,处理一组相关数据,常见的做法是定义类作为各种强类型字段以及相关方法的容器。虽然在 JAVA 8 中可以声明匿名类,但有时,尤其是在涉及各种类型数据的不同组合时,使用类/结构到处看起来对我来说有点过头了。
在这篇文章中,我想介绍一下实现泛型 Tuple 类来封装相关数据是多么容易,以及它们在应用函数式编程范式以简化编码过程和启用更多高级功能方面的潜在用途,这些将在后续文章中进行讨论。
背景
我的 JAVA Tuple 类的概念和基本实现源自 C# 中的 Tuple 类,正如 Microsoft 的这篇帖子解释的那样,Tuples 通常有四种用法:
- 表示单个数据集。例如,一个元组可以表示数据库记录,其组件可以表示记录的各个字段。
- 提供对数据集的便捷访问和操作。
- 无需使用 `out` 参数(在 C# 中)或 `ByRef` 参数(在 Visual Basic 中)即可从方法返回多个值。
- 通过单个参数将多个值传递给方法。例如,`Thread.Start(Object)` 方法有一个参数,允许您向线程在启动时执行的方法提供一个值。如果将 `Tuple
` 对象作为方法参数提供,则可以为线程的启动例程提供三个数据项。
C# Tuple 的一个很好的总结可以在这里找到。
尽管 Tuple 在 .NET 中并不被广泛使用,但我观察到的原因是没有杀手级应用,但至少有三个吸引人的优点值得利用:
- 不可变性:这对于函数式编程至关重要。
- 紧凑的结构,用于承载动态的强类型值数据集,在某些情况下可以用来替代 POJO。
- 可变长度的值集合可以被视为同一种类型的对象 (`Tuple`)。
因此,这个简单的泛型类用于实现泛型 `Repository<>` 类和 Railway Oriented Programming 工具。
实现
在底层,Tuple 类使用 `final Object[] values` 来存储构成此数据结构的所有元素,这意味着原始值(int、float 等)将被装箱为其对应的包装器(Integer、Float 等)。
这些元素的强类型只读访问器实际上是在一系列 `WithValuesXx` 接口中定义的,作为它们的默认方法。可比性,在使用 Tuples 作为 map 的键时至关重要,由一组强大的泛型方法支持。
不可变性
一旦创建了 Tuple 实例,就无法通过添加/删除来更改其元素,因为 Object 数组是 final 的。尽管没有办法阻止更改这些元素的内容(例如,当列表是 Tuple 的一个元素时,删除或添加新元素到列表),但这是对该数据结构的一种滥用,不在考虑范围内。
假设 Tuple 一旦构建就没有任何元素会被更改,以下覆盖的方法将由私有变量支持,以避免不必要的评估:
@Override int hashCode()
;@Override String toString()
;int[][] getDeepLength()
:用于启用 `boolean equals(Object obj)`,将在下一节讨论。
强类型访问器
一旦使用不同类型的多个值来构建 Tuple 实例,以原始类型检索它们就是一个挑战。而不是像本库那样使用单个 Object[],一种替代方法是定义多个相同的私有 final 字段,例如,`Tuple3
public class Tuple3<T,U,V> extends Tuple { private final T t; //public T getFirst(){ return t; } private final U u; //public U getSecond(){ return u; } private final V v; //public V getThird(){ return u; } }
然而,这种实现使得扩展和维护变得困难且效率低下。一个由 N 个不同类型元素组成的 Tuple 类需要 N 个变量来存储它们,并需要 N 个方法来相应地访问它们,尤其是在这些变量和方法可以被由 N+1 个元素组成的 Tuple 类共享时。
虽然可以通过类继承来实现变量和方法的共享,但在此库中应用了一种新技术,关键点如下:
- 使用 JAVA 泛型接口来保留构成 Tuple 的元素的类型信息。
- 自 Java 8 起的接口默认方法,以及 C# 中的扩展方法,用于定义强类型的元素访问器,类型信息由泛型接口保留。
- 而不是类继承,一系列泛型接口是通过逐个扩展来定义的:
- 继承超接口的默认方法。例如,`WithValues3
` 将继承 `WithValues2 ` 的 `T getFirst()` 和 `U getSecond()` 方法。 - 定义自己的访问器,这些访问器可以被子接口继承。例如,`V getThird()` 由 `WithValues3
` 定义。
- 继承超接口的默认方法。例如,`WithValues3
- `TupleN` 类(具有 N 个强类型元素),作为 `Tuple` 的子类并扩展 `WithValueN` 接口,只需要声明其构造函数,因为 N 个元素访问器直接从 `WithValueN` 接口继承。
相关的泛型接口声明如下:
public interface WithValues { Object getValueAt(int index); } public interface WithValues1<T> extends WithValues { default T getFirst() { return (T)getValueAt(0); } } public interface WithValues2<T,U> extends WithValues1<T> { default U getSecond() { return (U)getValueAt(1); } } ... public interface WithValues20<T,U,V,W,X,Y,Z,A,B,C,D,E,F,G,H,I,J,K,L,M> extends WithValues19<T,U,V,W,X,Y,Z,A,B,C,D,E,F,G,H,I,J,K,L> { default M getTwentieth() { return (M)getValueAt(19); } }
这 20 个元素访问器(`T getFirst()`, ..., `M getTwentieth()`)只定义一次,但被所有比直接声明它们的接口拥有更多元素的继承接口共享。唯一的抽象方法 `Object getValueAt(int index)` 由 `Tuple` 实现,它是此模块中所有其他类的超类,如下所示:
public class Tuple implements AutoCloseable, Comparable<Tuple>, WithValues { @Override public Object getValueAt(int index) { if(index < 0 || index >= values.length) return null; return values[index]; }
它的子类具有非常简洁的结构,例如 `Tuple3
public class Tuple3<T,U,V> extends Tuple implements WithValues3<T,U,V> { protected Tuple3(T t, U u, V v){ super(t, u, v); } }
即使是 `TuplePlus
public class TuplePlus<T,U,V,W,X,Y,Z,A,B,C,D,E,F,G,H,I,J,K,L,M> extends Tuple implements WithValues20<T,U,V,W,X,Y,Z,A,B,C,D,E,F,G,H,I,J,K,L,M> { protected TuplePlus(T t, U u, V v, W w, X x, Y y, Z z, A a, B b, C c, D d, E e, F f, G g, H h, I i, J j, K k, L l, M m, Object... more){ super(ArrayHelper.mergeTypedArray(new Object[]{t,u,v,w,x,y,z,a,b,c,d,e,f,g,h,i,j,k,l,m}, more)); } }
通过这种方式,Tuple 的所有元素都存储在一个 Object 数组中,并且可以通过仅在泛型接口中定义一次的共享访问器来按原样检索。
即使在将 Object 转换为实际元素类型时,也有一些好处可以抵消其成本:
- 访问任何元素无需重复代码。
- 易于维护和扩展。(这就是为什么我将 Tuples 从 Single/Double 重命名为 Tuple1/Tuple2)。
- 易于测试:对 TuplePlus 的完整测试意味着所有其他 TupleN 类都已得到验证。
然而,在发布库到 Maven 之前运行“mvn javadoc:javadoc”时可能会出现一些问题:它占用了高达 5.6G 的内存!我已经向 Oracle 报告了此 bug,但编译或运行测试时未观察到此类实现的性能影响。
可比性
Tuple 类不仅是存储多个值的结构,还被设计为可比较的 `
为了实现这一目标,本库开发了几个泛型方法来支持对任何对象或数组的高级比较;尽管它们可以在更通用的场景中使用,但细节在这里讨论而不是在下一章中,作为 Tuple 的必备功能。
-
(在 TypeHelper 中)`int deepHashCode(Object obj)` 用于获取对象的哈希码,当 obj 是:
-
null 或单个对象:返回与 `int Objects.hashCode(Object o)` 相同的哈希码;
-
对象数组:返回与 `int Arrays.deepHashCode(Object a[])` 相同的哈希码;
-
单个原始值:将在评估前装箱为其包装器对象。
-
原始值数组:将在评估前转换为相应的包装器数组。例如,`int[]` 将转换为 `Integer[]`,`char[][]` 将首先转换为 `Character[][]`,然后才能使用转换后的数组调用 `TypeHelper.deepHashCode()`。转换由 TypeHelper 中的另一个泛型实用工具 `Object toEquivalent(Object obj)` 支持,将在第三集中讨论。
-
- (在 TypeHelper 中)`String deepToString(Object obj)` 用于获取相关对象的字符串表示形式,无论它是一个单个对象、原始值数组还是对象数组。此方法用于首次评估 Tuples 的 `String toString()`,并将结果保存以供以后使用,并假定 Tuples 是不可变的。`Comparable
` 接口的 `int compareTo(Tuple o)` 方法也基于 Tuples 的字符串表示。 - (在 TypeHelper 中)`int[][] getDeepLength(Object obj)` 意味着通过假定对象是数组的数组来获取对象的快照,以启用 `valueEquals()` 甚至并行评估。
- 3 种类型的节点被分类以供后续评估:`NORMAL_VALUE_NODE` (0)、`NULL_NODE`(-1) 或 `EMPTY_ARRAY_NODE`(-2)。
- 定位上述 3 种节点的索引,并结合它们的类型标识符,组成一个 int[] 来捕获如何获取该节点以及它们是什么类型的节点。
- 所有捕获的 int[] 都被分组为结果 int[][]。
- 例如,`Object target = new Object[]{1, new int[]{2,3}, new Object[]{null, '5', '6', null}, new char[0], 110}` 将获得 `[[0, 0], [1, 0, 0], [1, 1, 0], [2, 0, -1], [2, 1, 0], [2, 2, 0], [2, 3, -1], [3, -2], [4, 0]]` 的 deepLength。
- 第三个 int[] `"[1,1,0]"` 描述了 `target` 的 `第二个元素` `int[]{2,3}` 的 `第三个`,即 `3`,而 `0` 表示它是 `NORMAL_VALUE_NODE`。
- 第八个 int[] `"[3, -2]"` 表示 `target` 的第四个元素 `new char[0]`,它是一个空数组。
- (在 TypeHelper 中)`boolean valueEquals(Object obj1, Object obj2)` 和其他带有可选策略或运行模式(并行或串行)的版本来评估两个对象是否相等。
- 如果其中一个不是数组,则返回 `boolean Objects.equals(Object a, Object b)` 的结果;
- 与 `boolean Arrays.equals(Object[] a1, Object[] a2)` 类似,但是:
- 将原始值数组视为与其包装器数组相同:char == Character,int[] == Integer[],boolean[][] == Boolean[][]。
- 评估过程是先比较两个 Tuple 实例的 `deepLength`,这被缓冲并显示了构成 Tuple 的元素的结构。只有当两个 int[][] 的长度相同且完全匹配时,方法才会根据给定的或默认的评估策略评估每个节点。
- 由于每个节点都可以通过其自身的索引访问,因此评估可以并行进行。
- null 节点和空数组节点都可以通过 3 种策略进行评估:**TypeIgnored**、**BetweenAssignableTypes** 和 **SameTypeOnly**。因此,当 **TypeIgnored** 或 **BetweenAssignableTypes** 时,`new int[0]` 等同于 `new Integer[0]`,但在 **SameTypeOnly** 时则不是。
将原始值视为与其包装器对象相同,这是 `boolean Arrays.equals(Object[] a1, Object[] a2)` 的区别所在,后者无法得出 `new int[]{1,2,3}` 等同于 `new Integer[]{1,2,3}` 的结论。
更具体地说,当比较以下两个对象时:
obj1
:`new Object[]{new int[]{3,2,1}, new short[0], 1.1d, null, new String[]{"S1", "S2", null}, new Integer[]{null, 23}}`obj2
:`new Object[]{new Integer[]{3,2,1}, new Short[0], Double.valueOf(1.1), null, new Comparable[]{"S1", "S2", null}, new Number[]{null, 23}}`
然后,评估结果可能是:
- 默认情况下,当 `NullEquality` 和 `EmptyArrayEquality` 都为 `TypeIgnored` 时,`obj1` 中的 null(Object[] 的第 4 个元素,String[] 的第 3 个元素和 Integer[] 的第 1 个元素)将被视为与 `obj2` 中的 null(Object[] 的第 4 个元素,Comparable[] 的第 3 个元素和 Number[] 的第 1 个元素)相等,并且 `obj1` 中的唯一空数组(`new short[0]`)也被视为与 `obj2` 中的唯一空数组(`new Short[0]`)相等。因此,通过调用 `valueEquals(obj1, obj2)`,它们是相等的。
- 然而,如果选择 `NullEquality.SameTypeOnly`,那么 `obj1` 中的 null(Object[] 的第 4 个元素,String[] 的第 3 个元素和 Integer[] 的第 1 个元素)将 **不** 被视为与 `obj2` 中的 null(Object[] 的第 4 个元素,Comparable[] 的第 3 个元素和 Number[] 的第 1 个元素)相等,因此调用 `valueEquals(obj1, obj2, SameTypeOnly, TypeIgnored)` 将返回 false。
AutoCloseable
Tuple 类被声明为 `AutoCloseable`,具有以下 `close()` 方法:
@Override public void close() throws Exception { if(!closed) { //Close AutoCloseable object in reverse order for (int i = values.length - 1; i >= 0; i--) { Object value = values[i]; if (value != null && value instanceof AutoCloseable) { Functions.Default.run(() -> ((AutoCloseable) value).close()); Logger.L("%s closed()", value); } } closed = true; Logger.L("%s.close() run successfully!", this); } }
因此,如果一个 Tuple 由多个相互依赖的元素组成,例如,以下伪代码:
public Tuple<File, Stream, ExcelFile, ExcelSheet[]> getExcel(String filename){ File file = new File(filename); Stream stream = new FileInputStream(file); ExcelFile excel = new ExcelFile(stream); ExcelSheet[] shees = reader.getSheets(); return new Tuple(file, stream, reader, excel, sheets); } Tuple<File, Stream, ExcelFile, ExcelSheet[]> excelTuple = getExcel("somefile.xlsx"); ... handling of the Excel sheets excelTuple.close();
假设以上 Tuple 实例的所有元素都是 `AutoCloseable`,那么关闭 `excelTuple` 实例将按正确的顺序释放所有涉及的资源。使用上述工厂方法,`try-with-resources` 语句可以通过将 Tuple 视为一组资源来简化。
过滤为 Set
一个特殊类型的 Tuple - Set 被定义来存储相同类型的元素,这些元素保留它们的类型信息,并在使用 Tuple 类的比较器评估元素值之前在比较中使用它。
由于 Set 的元素类型相同,因此 Set 可以将存储的元素转换为强类型数组。
public T[] asArray(){ return Arrays.copyOf((T[]) values, values.length); }
Tuple 有两个方法可以用来提取特定类型的元素作为 Tuple:
- Set
getSetOf(Class :获取所有匹配给定类的非 null 元素,作为不可变的 Set。clazz) -
Set
getSetOf(Class :获取所有匹配给定类的非 null 元素,并根据预定义标准匹配,作为不可变的 Set。clazz, Predicate valuePredicate)
后者实现如下:
public <T> Set<T> getSetOf(Class<T> clazz, Predicate<T> valuePredicate){ Objects.requireNonNull(clazz); Objects.requireNonNull(valuePredicate); List<T> matched = new ArrayList<>(); Predicate<Class> classPredicate = TypeHelper.getClassEqualitor(clazz); int length = getLength(); for (int i = 0; i < length; i++) { Object v = values[i]; if(v != null){ try { if(classPredicate.test(v.getClass()) && valuePredicate.test((T)v)) matched.add((T)v); }catch (Exception ex){} } }; T[] array = (T[]) matched.stream().toArray(); return setOf(clazz, array); }
因此,可以方便地获取一个 Set 来存储 Tuple 中所有符合某些标准的元素。
Tuple manyValues = Tuple.of("abc", null, 33, true, "a", "", 'a', Tuple.TRUE, 47); assertEquals(Tuple.setOf("abc", "a", ""), manyValues.getSetOf(String.class)); assertEquals(Tuple.setOf("abc"), manyValues.getSetOf(String.class, s->s.length()>2));
此外,要直接创建 Set,Tuple 中定义了两个静态方法:
-
Set
setOf(T... elements) :创建一组由 JAVA 推断类型的元素。 -
Set
setOf(Class :创建一种类型可以被直接识别的 Set。elementType, T[] elements)
因此,如果所有元素都是 Integer,则创建一个 Set
Set<Integer> integerSet = Tuple.setOf(1, 2, 3); assertTrue(Arrays.deepEquals(new Integer[]{1,2,3}, integerSet.asArray()));
但也可以指定一个共享类型作为元素类型。
Set<Comparable> comparableSet = Tuple.setOf(Comparable.class, new Comparable[]{1.0, 'a', "abc"}); assertTrue(Arrays.deepEquals(new Comparable[]{1.0, 'a', "abc"}, comparableSet.asArray()));
如何使用
该项目托管在 github.com,要包含该库,请将以下信息添加到您的 maven 项目中:
|
包含包后,Tuple 类的构造函数不可访问,有两种方法可以创建强类型 Tuple:
- 调用 **Tuple.create(...)** 的静态方法,根据参数列表的长度使用可变数量的参数。
- 当不提供参数时,将返回 `Tuple0` 的单例 `Tuple.UNIT`;
- 当提供 1 到 20 个参数时,将调用 `Tuple1` 到 `Tuple20` 的构造函数来创建强类型的 `Tuple1`, `Tuple2`, ..., `Tuple20` 实例。
- 当提供超过 20 个参数时,将返回一个 `TuplePlus` 实例,该实例提供对前 20 个元素的强类型访问。
- 或者,`Tuple Tuple.of(...)` 的静态方法,使用可变数量的参数,只是上述 `create()` 方法的包装器。然而,*创建的 Tuple 的元素的类型信息在返回为 Tuple 时会被擦除*。
结果是,`Tuple.create` 将创建一个强类型的 `Tuple6` 实例,其中所有元素类型信息在声明期间都已保留。
Tuple6<Integer, Character, Double, String, DayOfWeek, Boolean> tuple2 = Tuple.create(1, 'a', 3.0, "abc", DayOfWeek.MONDAY, true);
而调用 `Tuple.of()` 需要一个类型转换,但这使得有可能将元素命名为更通用的值类型。
Tuple6<Comparable, Object, Number, String, DayOfWeek, Boolean> tuple = (Tuple6<Comparable, Object, Number, String, DayOfWeek, Boolean>) Tuple.of(1, 'a', 3.0, "abc", DayOfWeek.MONDAY, true);
Tuple 实例的前 3 个元素被声明为 **Comparable, Object, Number**,而不是 **Integer, Character, Double**,这使得上面声明的同一个 Tuple 变量以后可以存储不同类型的实例。
无论如何,一旦 Tuple 实例是用元素类型信息声明的,就可以按照声明的方式检索它们的元素。
Comparable first = tuple.getFirst(); assertEquals(Integer.valueOf(1), first); assertEquals(Character.valueOf('a'), tuple.getSecond()); assertEquals(Double.valueOf(3.0), tuple.getThird());
结论
总之,Tuple 类使得在一个不可变的数据结构中轻松地存储不同类型的多个值,并使用强类型访问器方便地检索它们的值。这些不可变的值可以通过不同的策略,串行或并行地按其值进行比较,如果它们是 AutoCloseable,则自动关闭,并过滤以生成特殊的数据集合 Set 以供进一步处理。
尽管 Tuple 类本身可以作为一个泛型数据结构来存储和管理可变长度和类型的数据,但关键在于将其用作 **JAVA Map** 的键或值,并将函数作为一等成员来构建强大的实用工具,结合缓冲和业务逻辑,这将在本系列的下一篇文章中讨论。