如何将字符串流转换为字符串对流?

6
我想将一个字符串流转换成一个单词对流,例如:
我有:{ "A", "Apple", "B", "Banana", "C", "Carrot" } 我希望得到:{ ("A", "Apple"), ("Apple", "B"), ("B", "Banana"), ("Banana", "C") }
这与在Zipping streams using JDK8 with lambda (java.util.stream.Streams.zip)中概述的“压缩”几乎相同。
但是,这会产生:{ (A, Apple), (B, Banana), (C, Carrot) } 以下代码可以工作,但明显不是正确的方式(不是线程安全等等):
static String buffered = null;

static void output(String s) {
    String result = null;
    if (buffered != null) {
        result = buffered + "," + s;
    } else {
        result = null;
    }

    buffered = s;
    System.out.println(result);
}

// ***** 

Stream<String> testing = Stream.of("A", "Apple", "B", "Banana", "C", "Carrot");
testing.forEach(s -> {output(s);});

1
我会这样做:zip(stream, stream[1:])(这是Python,但思路相同:创建一个跳过第一个项的第二个流,stream.skip(1) 应该可以)。 - njzk2
你希望你的词对是由逗号分隔的字符串,还是类似元组的对象? - durron597
一个类似元组的对象会很好,但任何一个都可以。 - Nick Lothian
4个回答

4
如果你:
  1. 不喜欢从流中创建包含所有字符串的列表的想法
  2. 不想使用外部库
  3. 喜欢亲自动手
那么你可以创建一个方法,使用Java 8低级流构建器StreamSupportSpliterator来对流中的元素进行分组:
class StreamUtils {
    public static<T> Stream<List<T>> sliding(int size, Stream<T> stream) {
        return sliding(size, 1, stream);
    }

    public static<T> Stream<List<T>> sliding(int size, int step, Stream<T> stream) {
        Spliterator<T> spliterator = stream.spliterator();
        long estimateSize;

        if (!spliterator.hasCharacteristics(Spliterator.SIZED)) {
            estimateSize = Long.MAX_VALUE;
        } else if (size > spliterator.estimateSize()) {
            estimateSize = 0;
        } else {
            estimateSize = (spliterator.estimateSize() - size) / step + 1;
        }

        return StreamSupport.stream(
                new Spliterators.AbstractSpliterator<List<T>>(estimateSize, spliterator.characteristics()) {
                    List<T> buffer = new ArrayList<>(size);

                    @Override
                    public boolean tryAdvance(Consumer<? super List<T>> consumer) {
                        while (buffer.size() < size && spliterator.tryAdvance(buffer::add)) {
                            // Nothing to do
                        }

                        if (buffer.size() == size) {
                            List<T> keep = new ArrayList<>(buffer.subList(step, size));
                            consumer.accept(buffer);
                            buffer = keep;
                            return true;
                        }
                        return false;
                    }
                }, stream.isParallel());
    }
}

方法和参数的命名灵感来自于它们在Scala中的对应物。

让我们进行测试:

Stream<String> testing = Stream.of("A", "Apple", "B", "Banana", "C", "Carrot");
System.out.println(StreamUtils.sliding(2, testing).collect(Collectors.toList()));

[[A, 苹果], [苹果, B], [B, 香蕉], [香蕉, C], [C, 胡萝卜]]

不重复元素的情况怎么处理:

Stream<String> testing = Stream.of("A", "Apple", "B", "Banana", "C", "Carrot");
System.out.println(StreamUtils.sliding(2, 2, testing).collect(Collectors.toList()));

[[A, 苹果], [B, 香蕉], [C, 胡萝卜]]

现在使用无限的 Stream

StreamUtils.sliding(5, Stream.iterate(0, n -> n + 1))
        .limit(5)
        .forEach(System.out::println);

[0, 1, 2, 3, 4]
[1, 2, 3, 4, 5]
[2, 3, 4, 5, 6]
[3, 4, 5, 6, 7]
[4, 5, 6, 7, 8]

3

根据@njzk2的评论,这应该可以满足您的需求,使用两次流并在第二种情况下跳过第一个元素。它使用您在原始问题中链接的zip方法。

public static void main(String[] args) {
  List<String> input = Arrays.asList("A", "Apple", "B", "Banana", "C", "Carrot");
  List<List<String>> paired = zip(input.stream(),
                                  input.stream().skip(1),
                                  (a, b) -> Arrays.asList(a, b))
                              .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
  System.out.println(paired);
}

这将输出一个包含以下内容的List<List<String>>:

[[A, Apple], [Apple, B], [B, Banana], [Banana, C], [C, Carrot]]

在评论中,您问如何在已经拥有Stream的情况下实现此操作。不幸的是,这很困难,因为Streams不具有状态,并且没有真正概念上的“相邻”元素。这里有一个好的讨论
我能想到两种方法,但我认为您可能都不喜欢:
  1. Stream转换为List,然后执行上面我的解决方案。虽然丑陋,但只要Stream不是无限的,而性能也不太重要,就可以工作。
  2. 使用@TagirValeev下面的答案,只要您使用的是StreamEx而不是Stream,并且愿意添加对第三方库的依赖。
另外与此讨论相关的是这个问题:Can I duplicate a Stream in Java 8?;这对于您的问题来说并不好,但值得阅读,可能还有更适合您的解决方案。

好的,我明白了它是如何工作的,现在对我来说很有意义。有没有一种方法可以从单个现有流而不是两个相同的流中运行它? - Nick Lothian
@NickLothian 回应后编辑 - durron597
感谢您的编辑。我理解仅使用流进行操作的复杂性,但我认为一定有解决方案。关键在于保持状态,但我不知道如何做到。然而,像在流上执行平均和计数等操作都可以做到,因此一定有方法。 - Nick Lothian
@NickLothian 平均和计数都是一种归约(.reduce),但在这里显然不起作用。 - durron597

2
你可以使用我的StreamEx库,它增强了标准的Stream API。其中有一个方法pairMap,它正好能满足你的需求:
StreamEx.of("A", "Apple", "B", "Banana", "C", "Carrot")
        .pairMap((a, b) -> a+","+b)
        .forEach(System.out::println);

输出:

A,Apple
Apple,B
B,Banana
Banana,C
C,Carrot

pairMap参数是将相邻元素对转换为适合您需求的内容的函数。如果您的项目中有一个Pair类,您可以使用.pairMap(Pair::new)来获取一组元素对的流。如果您想创建一个由两个元素列表组成的流,则可以使用:

List<List<String>> list = StreamEx.of("A", "Apple", "B", "Banana", "C", "Carrot")
                                    .pairMap((a, b) -> StreamEx.of(a, b).toList())
                                    .toList();
System.out.println(list); // [[A, Apple], [Apple, B], [B, Banana], [Banana, C], [C, Carrot]]

这适用于任何元素源(您可以使用StreamEx.of(collection)StreamEx.of(stream)等),如果在pairMap之前有更多流操作,则能正确工作,并且非常适合并行处理(不像涉及流压缩的解决方案)。
如果您的输入是具有快速随机访问的List,并且实际上希望将其作为结果得到List<List<String>>,则在我的库中使用ofSubLists有一种更短而略有不同的方法可以实现此目的。
List<String> input = Arrays.asList("A", "Apple", "B", "Banana", "C", "Carrot");
List<List<String>> list = StreamEx.ofSubLists(input, 2, 1).toList();
System.out.println(list); // [[A, Apple], [Apple, B], [B, Banana], [Banana, C], [C, Carrot]]

在幕后,对于每个输入列表位置都会调用input.subList(i, i+2),因此您的数据不会被复制到新列表中,而是创建了引用原始列表的子列表。


ofSubLists 中,如果原始列表被修改,List<List<String>> 会发生什么? - durron597
1
@durron597,结果当然也会被修改。这就是为什么它是“有些不同”的原因。如果您想使用ofSubLists,但又想复制列表,您可以随时添加步骤,例如.map(lst -> StreamEx.of(lst).toList())或甚至.map(ArrayList::new)。如果您不需要复制(这是大多数情况),则不会隐式添加此步骤,因此您将获得更高效的代码。 - Tagir Valeev

0
这是一段最小化的代码,它创建了一个包含一组对的 List<List<String>>
List<List<String>> pairs = new LinkedList<>();
testing.reduce((a, b)-> {pairs.add(Arrays.asList(a,b)); return b;});

1
请注意,虽然它实际上可以工作,但它违反了reduce方法的文档合同(提供的函数必须是关联的、无干扰的、无状态的)。 - Tagir Valeev
@TagirValeev 实际上它不是 reduction(结果被丢弃/忽略),而只是一种方便的方法来传递连续的元素给一个方法——所以没有违反任何规定。但是它是非干涉性的(唯一被改变的对象是流和 Lambda 都外部的对象),所以它不会造成任何损害。对于它所实现的功能而言,它非常简洁,这在我看来使它成为有价值的代码。 - Bohemian
你可以说这个函数在未使用的结果方面确实是可结合的,但依赖于执行顺序来产生副作用显然违背了意图。减少函数应该是可结合的原因是它不应该依赖于执行顺序。 - Holger

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