Java SE 8是否拥有Pairs或Tuples?

219

我正在尝试使用Java SE 8中的惰性函数操作,并且我想将索引i映射到一对/元组(i,value [i]),然后基于第二个value [i]元素进行filter过滤,最后仅输出索引。

在Lambda和流的新时代,我还必须忍受这个吗:What is the equivalent of the C++ Pair<L,R> in Java?

更新: 我提供了一个相当简化的例子,在下面的答案中@dkatzel提供了一个漂亮的解决方案。但是,它并不适用于所有情况。因此,让我添加一个更一般的例子:

package com.example.test;

import java.util.ArrayList;
import java.util.stream.IntStream;

public class Main {

  public static void main(String[] args) {
    boolean [][] directed_acyclic_graph = new boolean[][]{
        {false,  true, false,  true, false,  true},
        {false, false, false,  true, false,  true},
        {false, false, false,  true, false,  true},
        {false, false, false, false, false,  true},
        {false, false, false, false, false,  true},
        {false, false, false, false, false, false}
    };

    System.out.println(
        IntStream.range(0, directed_acyclic_graph.length)
        .parallel()
        .mapToLong(i -> IntStream.range(0, directed_acyclic_graph[i].length)
            .filter(j -> directed_acyclic_graph[j][i])
            .count()
        )
        .filter(n -> n == 0)
        .collect(() -> new ArrayList<Long>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2))
    );
  }

}

这会给出 不正确 的输出值 [0, 0, 0],这对应于三列都为 false计数。我需要的是这三列的索引。正确的输出应该是[0, 2, 4]。我如何获得这个结果?


4
多年来已经有了AbstractMap.SimpleImmutableEntry<K,V>。但无论如何,与其将i映射到(i, value[i])仅用于按value[i]进行过滤并将其映射回i:为什么不首先直接按value[i]进行过滤,而不需要进行映射呢? - Holger
1
@Holger 我需要知道数组中哪些索引包含与条件匹配的值。如果不保留流中的 i,我无法完成此操作。我还需要使用 value[i] 进行条件判断。这就是为什么我需要 (i, value[i]) - necromancer
1
@necromancer 没错,它只适用于从索引获取值很便宜的情况,比如数组、随机访问集合或者廉价函数。我猜问题在于你想呈现一个简化的用例,但是过于简化了,因此陷入了特殊情况。 - Stuart Marks
1
@necromancer 我稍微编辑了一下最后一段话,以澄清我认为你提出的问题。这样正确吗?此外,这是关于有向(非无环)图的问题吗?(虽然这并不重要。)最后,所需的输出应该是 [0, 2, 4] 吗? - Stuart Marks
1
我认为解决这个问题的正确方法是让未来的Java版本支持元组作为返回类型(作为Object的特殊情况),并且使lambda表达式能够直接使用这样的元组作为其参数。 - Thorbjørn Ravn Andersen
显示剩余3条评论
10个回答

240

更新: 这个答案是针对原始问题"Java SE 8是否有Pairs或Tuples?"(如果没有,为什么没有?)的回答。 OP已经使用一个更完整的示例更新了问题,但似乎可以在不使用任何类型的Pair结构的情况下解决它。[来自OP的说明:这是另一个正确的答案。]


简短的答案是没有。您要么需要自己编写代码,要么引入其中一个实现Pair的库。

在Java SE中拥有一个Pair类曾经被提出过并被拒绝过至少一次。请参见OpenJDK邮件列表中的此讨论线程。权衡利弊并不明显。一方面,其他库和应用程序代码中有很多Pair实现。这表明有需求,将这样的类添加到Java SE中将增加重用和共享。另一方面,有Pair类会诱惑人们使用Pair和集合创建复杂的数据结构而不创建必要的类型和抽象。(这是Kevin Bourillion的来信的释义。)

我建议每个人都阅读整个邮件线程。它非常有见地而且没有争吵。它相当有说服力。当它开始时,我认为:"是的,Java SE中应该有一个Pair类",但是当线程到达结尾时,我改变了我的想法。

然而请注意,JavaFX有javafx.util.Pair类。JavaFX的API与Java SE API分开演变。

正如从链接的问题"Java中C++ Pair的等效是什么?"中可以看出,围绕这个看似简单的API存在着相当大的设计空间。这些对象应该是不可变的吗?它们应该是可序列化的吗?它们应该能够比较吗?这个类应该是final还是不是?这两个元素是否有序?它应该是接口还是类?为什么要止步于一对?为什么不是三元组、四元组或N元组?

当然,这里还有不可避免的元素命名争论:

  • (a, b)
  • (first, second)
  • (left, right)
  • (car, cdr)
  • (foo, bar)
  • 等等。

一个很大的问题几乎没有被提到,那就是Pairs与基本类型之间的关系。如果您有一个表示2D空间中点的(int x,int y)数据,将其表示为Pair <Integer,Integer>将消耗三个对象而不是两个32位字。此外,这些对象必须驻留在堆上,并将产生GC开销。

显然,与流一样,Pai的基本特殊化至关重要。

Pair
ObjIntPair
ObjLongPair
ObjDoublePair
IntObjPair
IntIntPair
IntLongPair
IntDoublePair
LongObjPair
LongIntPair
LongLongPair
LongDoublePair
DoubleObjPair
DoubleIntPair
DoubleLongPair
DoubleDoublePair

即使使用IntIntPair,仍需要在堆上创建一个对象。

当然,这些与Java SE 8中的java.util.function包中函数接口的增加类似。如果您不想要一个臃肿的API,您会排除哪些接口?您还可以认为这不足够,应该添加针对Boolean等特定类型的专门化。

我个人认为,如果Java很早以前就添加了一个Pair类,它可能会很简单,甚至过于简单,并且不会满足我们现在所设想的许多用例。考虑一下,如果在JDK 1.0时期就添加了Pair,它很可能是可变的!(看看java.util.Date。)人们会对此感到满意吗?我的猜测是,如果Java中有一个Pair类,它可能会有点有用但又不完全符合需求,每个人仍然会自己编写代码来满足他们的需求,在外部库中会有各种Pair和Tuple实现,而人们仍将争论/讨论如何修复Java的Pair类。换句话说,我们今天所处的位置基本相同。

与此同时,正在进行一些工作来解决根本问题,即更好地支持值类型在JVM(以及最终的Java语言)。请参阅此Value状态文档。这是初步的、推测性的工作,仅从JVM的角度涵盖问题,但它已经有了相当多的思考。当然,不能保证这将进入Java 9,或者永远不会进入任何地方,但它确实显示了当前对这个主题思考的方向。


3
对于Pair<T,U>,基本类型的工厂方法并没有帮助。因为泛型必须是引用类型,所以任何基本类型都会在存储时进行装箱。如果要存储基本类型,您需要一个不同的类。 - Stuart Marks
3
回顾过去,封装基本类型的构造器不应该是公开的,而 valueOf 应该是获取包装实例的唯一方式。但这些已经存在于Java 1.0中,可能没有改变它们的必要了。 - Stuart Marks
3
显然,应该只有一个公共的“Pair”或“Tuple”类,并且该类应该有一个工厂方法,在后台透明地创建必要的优化存储专用类。最终,lambda就是这样做的:它们可以捕获任意数量和类型的变量。现在想象一下一种语言支持,允许在运行时通过“invokedynamic”指令触发创建适当的元组类... - Holger
3
如果在现有的JVM上进行价值类型的后期改造,那么类似于这样的东西可能会奏效,但是《价值类型提案》(现在称为"Valhalla项目")要求更加激进。特别地,它的值类型不一定是堆分配的。此外,与今天的对象不同,就像今天的基元一样,值将没有身份识别。 - Stuart Marks
2
@Stuart Marks:这不会干扰,因为我所描述的类型可以成为该值类型的“boxed”类型。使用基于invokedynamic的工厂(类似于lambda创建)进行后期改装也不是问题。顺便说一下,lambda表达式也没有身份识别。正如明确说明的那样,您今天可能感知到的身份是当前实现的产物。 - Holger
显示剩余7条评论

62

3
就内置的键值对功能而言,这是正确的答案。请注意,SimpleImmutableEntry 只保证存储在 Entry 中的引用不会改变,但并不保证链接的 keyvalue 对象的字段(或它们所链接的对象的字段)不会更改。 - Luke Hutchison

30

遗憾的是,Java 8没有引入pairs或tuples。当然,你可以使用org.apache.commons.lang3.tuple(我个人与Java 8一起使用)或者创建自己的包装器,或者使用Maps等类似的东西,正如你链接的那个问题的接受的回答中所解释的那样。


更新: JDK 14作为预览功能引入了record classes,JDK 16将其作为标准语言特性引入。这些不是元组,但可以用来解决许多相同的问题。在你上面的具体示例中,可能会像这样:

public class Jdk14Example {
    record CountForIndex(int index, long count) {}

    public static void main(String[] args) {
        boolean [][] directed_acyclic_graph = new boolean[][]{
                {false,  true, false,  true, false,  true},
                {false, false, false,  true, false,  true},
                {false, false, false,  true, false,  true},
                {false, false, false, false, false,  true},
                {false, false, false, false, false,  true},
                {false, false, false, false, false, false}
        };

        System.out.println(
                IntStream.range(0, directed_acyclic_graph.length)
                        .parallel()
                        .mapToObj(i -> {
                            long count = IntStream.range(0, directed_acyclic_graph[i].length)
                                            .filter(j -> directed_acyclic_graph[j][i])
                                            .count();
                            return new CountForIndex(i, count);
                        }
                        )
                        .filter(n -> n.count == 0)
                        .collect(() -> new ArrayList<CountForIndex>(), (c, e) -> c.add(e), (c1, c2) -> c1.addAll(c2))
        );
    }
}

如果使用--enable-preview标志和JDK 14JDK 16或更高版本进行编译和运行,您将获得以下结果:

[CountForIndex[index=0, count=0], CountForIndex[index=2, count=0], CountForIndex[index=4, count=0]]

实际上,@StuartMarks的一个答案让我在不使用元组的情况下解决了它,但由于它似乎不能推广,所以我可能最终还是需要它。 - necromancer
@necromancer 是的,这是一个非常好的答案。Apache库有时仍然很有用,但最终还是要看Java语言设计。基本上,元组必须成为原语(或类似物)才能像其他语言中那样工作。 - blalasaadri
1
如果你没有注意到的话,答案中包含了这个非常有信息量的链接:http://cr.openjdk.java.net/~jrose/values/values-0.html,关于这种原语(包括元组)的需求和前景。 - necromancer

26

自Java 9起,您可以比以前更容易地创建Map.Entry实例:

Entry<Integer, String> pair = Map.entry(1, "a");

Map.entry 返回一个不可修改的 Entry,并禁止使用 null。


19

看起来完整的示例可以在不使用任何类型的Pair结构的情况下解决。关键是过滤列索引,谓词检查整个列,而不是将列索引映射到该列中false条目的数量。

执行此操作的代码在此处:

    System.out.println(
        IntStream.range(0, acyclic_graph.length)
            .filter(i -> IntStream.range(0, acyclic_graph.length)
                                  .noneMatch(j -> acyclic_graph[j][i]))
            .boxed()
            .collect(toList()));

这将产生[0, 2, 4]的输出结果,这是我认为OP所要求的正确结果。

还请注意boxed()操作,它将int值打包成Integer对象。这使得可以使用现有的toList()收集器而不必编写自己的收集器函数来完成打包操作。


1
+1 张王牌 :) 这个方案还不能通用化,对吧?这是问题更为重要的方面,因为我预计会面临其他无法使用此类方案的情况(例如列中不超过3个值“true”)。因此,我将接受您的另一个答案是正确的,但也指向这个!非常感谢 :) - necromancer
这是正确的,但应该接受相同用户提供的其他答案(请参见上面和其他地方的评论)。 - necromancer
1
@necromancer 对的,在某些情况下,如果您想要检索索引,但无法使用索引检索或计算数据元素。 (至少不容易。)例如,考虑一个问题,您正在从网络连接中读取文本行,并且您想要找到与某个模式匹配的第N行的行号。最简单的方法是将每行映射到Pair或某些复合数据结构以对行进行编号。然而,可能有一种hacky、副作用的方法可以在不使用新数据结构的情况下完成此操作。 - Stuart Marks
@StuartMarks,一对是<T,U>,三元组是<T,U,V>等。你的例子是一个列表,而不是一对。 - Pacerier

7
Vavr(原名为Javaslang)(http://www.vavr.io)也提供了大小为8的元组。这里是javadoc: https://static.javadoc.io/io.vavr/vavr/0.9.0/io/vavr/Tuple.html
以下是一个简单的示例:
Tuple2<Integer, String> entry = Tuple.of(1, "A");

Integer key = entry._1;
String value = entry._2;

为什么JDK本身到现在还没有简单的元组类型,这对我来说是个谜。编写包装类似乎是每天都要做的事情。


某些版本的 Vavr 在底层使用了 sneaky throws。请注意不要使用这些。 - Thorbjørn Ravn Andersen

7

是的。

Map.Entry可以用作Pair

不幸的是,它并不能帮助Java 8流,因为问题在于,尽管lambda可以接受多个参数,但Java语言仅允许返回单个值(对象或基本类型)。这意味着每当你有一个流时,你最终会从上一个操作中传递一个单一对象。这是Java语言的缺陷,因为如果支持多个返回值,并且流支持它们,我们可以通过流完成更好的非平凡任务。

在那之前,只有很少的用处。

编辑2021-05-10:Java 16引入了记录,这是解决此问题和其他问题的非常好的解决方案。这是将目标定位到即将推出的Java 17 LTS的非常强有力的原因。


6

既然您只关心索引,那么根本不需要映射到元组。为什么不编写一个过滤器,在数组中查找元素呢?

     int[] value =  ...


IntStream.range(0, value.length)
            .filter(i -> value[i] > 30)  //or whatever filter you want
            .forEach(i -> System.out.println(i));

+1 非常棒的实用解决方案。然而,我不确定它是否适用于我的情况,因为我是在运行时生成值。我将我的问题描述为一个数组,以提供一个简单的思考案例,你确实提出了一个优秀的解决方案。 - necromancer

2
Eclipse Collections有Pair和所有基本/对象对的组合(适用于所有八个基本类型)。
可以使用Tuples工厂创建Pair实例,并且可以使用PrimitiveTuples工厂来创建所有基本/对象对的组合。
这些内容是在Java 8发布之前添加的。它们非常有用,可以为原始映射实现键/值迭代器,我们还支持所有基本类型与对象的组合。
如果您愿意增加额外的库开销,可以使用Stuart的经过接受的解决方案,并将结果收集到一个原始的IntList中以避免装箱。我们在Eclipse Collections 9.0中添加了新方法,以允许从Int / Long / Double流创建Int / Long / Double集合。
IntList list = IntLists.mutable.withAll(intStream);

注意:我是 Eclipse Collections 的提交者。

0

许多第三方库都支持元组。例如,jOOλ支持从度数016元组,例如:

// Assuming this static import
import static org.jooq.lambda.tuple.Tuple.*;

// Write:
var t = tuple(1, "a", 2L);
Integer i = t.v1;
String s = t.v2;
Long l = t.v3;

其他也有元组的库,例如:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接