Java Stream:有没有办法迭代一次取两个元素而不是一个?

时间:2015-12-04 10:43:47

标签: java java-8 java-stream

假设我们有这个流

Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");

我希望在地图中保存相邻字符串的对,其中第一个以“err”开头。

我想到的是这样的事情

Map<String, String> map = new HashMap<>();

Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
.reduce((acc, next) -> {
    if (acc.startsWith("err"))
        map.put(acc,next);
    if (next.startsWith("err"))
        return next;
    else
        return "";
});

但由于两个主要原因,我并不完全满意

  1. 我“误用”reduce功能。在Stream API中,每个函数都有明确的,明确定义的目的:max应该计算最大值,filter应该根据条件进行过滤,reduce应该以递增方式生成累积价值等。
  2. 这样做阻止我使用Streams强大的机制:如果我想将搜索限制在前两个结果中怎么办?
  3. 这里我使用了reduce,因为(据我所知)它是唯一可以让你比较几个值的函数,你可以以某种方式返回类似于“当前值”和“下一个值”的值“概念。

    有更简单的方法吗?是否允许您为每次迭代考虑多个值来迭代流?

    修改

    我正在考虑的是一些机制,给定当前元素,允许您为每次迭代定义要考虑的“元素窗口”。

    这样的东西
    <R> Stream<R> mapMoreThanOne(
        int elementsBeforeCurrent,
        int elementsAfterCurrent,
        Function<List<? super T>, ? extends R> mapper);
    

    而不是

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    

    这将是对当前API的强大“升级”。

    EDIT2

    我很欣赏人们提出解决方案的努力,但问题不在于算法本身。通过将流,索引,临时变量放在一起来存储以前的值,有不同的方法来实现我的目标......但我想知道Stream API中是否有一些方法是为处理除当前元素以外的元素而设计的没有打破“流范式”。像这样的东西

    List<String> list =
            Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
            .filterFunctionImWonderingIfExist(/*filters couples of elements*/)
            .limit(2)
            .collect(Collectors.toList());
    

    鉴于答案,我认为没有“清晰快速”的解决方案,除非使用StreamEx库

5 个答案:

答案 0 :(得分:12)

您可以为此任务构建自定义收集器。

Map<String, String> map = 
    Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
          .collect(MappingErrors.collector());

使用:

private static final class MappingErrors {

    private Map<String, String> map = new HashMap<>();

    private String first, second;

    public void accept(String str) {
        first = second;
        second = str;
        if (first != null && first.startsWith("err")) {
            map.put(first, second);
        }
    }

    public MappingErrors combine(MappingErrors other) {
        throw new UnsupportedOperationException("Parallel Stream not supported");
    }

    public Map<String, String> finish() {
        return map;
    }

    public static Collector<String, ?, Map<String, String>> collector() {
        return Collector.of(MappingErrors::new, MappingErrors::accept, MappingErrors::combine, MappingErrors::finish);
    }

}

在此收集器中,保留两个运行元素。每次接受String时,它们都会更新,如果第一个以"err"开头,则会将这两个元素添加到地图中。

另一种解决方案是使用StreamEx库,该库提供pairMap方法,该方法将给定函数应用于此流的每个相邻元素对。在下面的代码中,如果第一个元素以"err"开头,则操作返回一个String数组,该数组包含该对的第一个和第二个元素,否则为null。然后过滤掉null个元素,并将Stream收集到地图中。

Map<String, String> map = 
    StreamEx.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
            .pairMap((s1, s2) -> s1.startsWith("err") ? new String[] { s1, s2 } : null)
            .nonNull()
            .toMap(a -> a[0], a -> a[1]);

System.out.println(map);

答案 1 :(得分:6)

您可以编写自定义收集器,或使用更简单的流式传输列表索引:

Map<String, String> result = IntStream.range(0, data.size() - 1)
        .filter(i -> data.get(i).startsWith("err"))
        .boxed()
        .collect(toMap(data::get, i -> data.get(i+1)));

这假设您的数据位于随机访问友好列表中,或者您可以暂时将其转储为一个。

如果您无法随机访问数据或将其加载到列表或数组中进行处理,您可以随时创建自定义pairing收集器以便编写

Map<String, String> result = data.stream()
        .collect(pairing(
                (a, b) -> a.startsWith("err"), 
                AbstractMap.SimpleImmutableEntry::new,
                toMap(Map.Entry::getKey, Map.Entry::getValue)
        ));

这里是收藏家的来源。它平行友好,可能在其他情况下派上用场:

public static <T, V, A, R> Collector<T, ?, R> pairing(BiPredicate<T, T> filter, BiFunction<T, T, V> map, Collector<? super V, A, R> downstream) {

    class Pairing {
        T left, right;
        A middle = downstream.supplier().get();
        boolean empty = true;

        void add(T t) {
            if (empty) {
                left = t;
                empty = false;
            } else if (filter.test(right, t)) {
                downstream.accumulator().accept(middle, map.apply(right, t));
            }
            right = t;
        }

        Pairing combine(Pairing other) {
            if (!other.empty) {
                this.add(other.left);
                this.middle = downstream.combiner().apply(this.middle, other.middle);
                this.right = other.right;
            }
            return this;
        }

        R finish() {
            return downstream.finisher().apply(middle);
        }
    }

    return Collector.of(Pairing::new, Pairing::add, Pairing::combine, Pairing::finish);
}

答案 2 :(得分:5)

如果您的输入位于随机访问列表中,事情会更容易。这样您可以使用这样的旧List.subList方法:

List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", 
     "f", "g", "h", "err3", "i", "j");

Map<String, String> map = IntStream.range(0, list.size()-1)
    .mapToObj(i -> list.subList(i, i+2))
    .filter(l -> l.get(0).startsWith("err"))
    .collect(Collectors.toMap(l -> l.get(0), l -> l.get(1)));

同样的事情可以通过已经提到的StreamEx库(由我编写)以更短的方式完成:

List<String> list = Arrays.asList("a", "b", "err1", "c", "d", "err2", "e", 
     "f", "g", "h", "err3", "i", "j");

Map<String, String> map = StreamEx.ofSubLists(list, 2, 1)
    .mapToEntry(l -> l.get(0), l -> l.get(1))
    .filterKeys(key -> key.startsWith("err"))
    .toMap();

虽然如果你不想要第三方依赖,那么糟糕的Stream API解决方案看起来也不是很糟糕。

答案 3 :(得分:0)

Collector.ofList<List<String>>作为收集对的结构的其他方法。 首先收集到List<List<String>>

List<List<String>> collect = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
        .collect(
                Collector.of(
                        LinkedList::new,
                        (a, b) -> {
                            if (b.startsWith("err"))
                                a.add(new ArrayList<>(List.of(b)));
                            else if (!a.isEmpty() && a.getLast().size() == 1)
                                a.getLast().add(b);
                        },
                        (a, b) -> { throw new UnsupportedOperationException(); }
                )
        );

然后可以将其转换为地图

Map<String, String> toMap = collect.stream().filter(l -> l.size() == 2)
        .collect(Collectors.toMap(
                e -> e.get(0),
                e -> e.get(1))
        );

或者与Collectors.collectingAndThen

合而为一
Map<String, String> toMap = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j")
        .collect(Collectors.collectingAndThen(
                Collector.of(
                        LinkedList<List<String>>::new,
                        (a, b) -> {
                            if (b.startsWith("err"))
                                a.add(new ArrayList<>(List.of(b)));
                            else if (!a.isEmpty() && a.getLast().size() == 1)
                                a.getLast().add(b);
                        },
                        (a, b) -> { throw new UnsupportedOperationException(); }
                ), (x) -> x.stream().filter(l -> l.size() == 2)
                        .collect(Collectors.toMap(
                                e -> e.get(0),
                                e -> e.get(1))
                        )
        ));

答案 4 :(得分:-1)

这是一个使用现成收藏家的简单单线:

Stream<String> stream = Stream.of("a", "b", "err1", "c", "d", "err2", "e", "f", "g", "h", "err3", "i", "j");

Map<String, String> map = Arrays.stream(stream
        .collect(Collectors.joining(",")).split(",(?=(([^,]*,){2})*[^,]*$)"))
    .filter(s -> s.startsWith("err"))
    .map(s -> s.split(","))
    .collect(Collectors.toMap(a -> a[0], a -> a[1]));

&#34;技巧&#34;这里首先将所有术语连接成一个字符串,然后将其拆分为成对的字符串,例如"a,b""err1,c"等。一旦你有一对字符串,处理很简单。