Java 8如何将函数应用于Stream的所有元素而不破坏流链。

73

在Java中,有没有一种方法可以对Stream的所有元素应用一个函数,而不破坏Stream链?我知道可以调用forEach方法,但是该方法返回一个void,而不是一个Stream


你想调用的方法是否返回一个值? - fps
你能解释一下“不破坏数据流链”的意思吗?可以举个例子吗? - Eugene
2
Alex 指的是我虚构的 apply 方法,像这样:things.stream().apply(o -> o.status = newStatus).map(....)....forEach 类似,但不会 "break the chain",即返回的是 Stream<T> 而不是 void - Josh M.
例如,我对流应用了一些过滤器,然后对其元素的字段应用了一些设置器,然后进行其他操作,如更多的过滤或映射或收集。或者我有几个消费者,我不想将它们打包在一个函数中。但是我不能使用forEach,因为它不返回流,并且必须添加"return",如果我使用map就不需要这样做。 List<SomeType> list = requestEntityList.stream() .filter(Objects::nonNull) .filter(e -> sessionEntity.getId().equals(e.getiSession())) .map(e -> {e.setErrMsg(null); return e;}) .collect(Collectors.toList()); - user9999
@JoshM,没有名为stream().apply的方法,请尝试一下?你能提供一个可行的示例吗? - prnjn
@prnjn 我知道这个方法并不存在(我在之前的评论中说了我是编造的)...但它应该存在!你可以使用 stream().map(...) 来完成相同的操作,但需要在 lambda 中返回相同的项目,这很愚蠢。 - Josh M.
7个回答

91

至少有3种方法。为了举例说明,我假设您想调用2个消费者方法methodAmethodB

A. 使用peek()

list.stream().peek(x -> methodA(x)).forEach(x -> methodB(x));

尽管文档上只建议将其用于“调试”,但它实际运行正常(而且现在正在生产环境中使用)

B. 使用map()调用methodA方法,然后将元素返回到流中:

list.stream().map(x -> {method1(x); return x;}).forEach(x -> methodB(x));

这可能是最为“可接受”的方法。

C. 在 forEach() 中做两件事情:

list.stream().forEach(x -> {method1(x); methodB(x);});

这是最不灵活的,可能不适合您的需求。


3
map函数中执行具有副作用的操作以及使用peek进行此类(非调试)副作用的影响基本相同。 - Holger
3
如果没有副作用,调用method1(x);将完全无用。 - Holger
3
@holger 错了。Method1 可能会对它进行某些操作,但不会改变原来的内容。实际上,我曾经在流的中途使用过这个完全相同的模式(peek() 的版本),当我想要收集元素(到一个 Set 中,不涉及时间敏感性,没有问题)但同时也想将流的尾部发送给一个消费者通过 forEach() 进行处理时。这不是无用的。 - Bohemian
3
“副作用”是指状态发生了未被宣传和/或意外的改变。显然,通过添加元素来改变Set是一种预期的操作,因此这不是“副作用”。如果消费者未宣传地改变了元素,则会出现副作用。你似乎在说,如果调用方法时“某些东西发生了改变”,那么它就具有副作用,但这是无意义的。如果一个方法应该改变某些东西,那么它就不是副作用;它是预期的行为。 - Bohemian
10
你对“副作用”的定义与IT界的其余部分的定义不符。这特别危险,因为API文档(如Stream API)中对函数产生副作用的使用进行了抑制,并参考了标准定义,例如Wikipedia中所述:“在计算机科学中,如果一个函数或表达式修改其作用域之外的某些状态或与调用函数或外部世界有可观察的交互,它被认为具有副作用”。无论副作用是否是意料之中的都是无关紧要的。 - Holger
显示剩余10条评论

6
你正在寻找“Stream”的“map()”函数。
例子:
List<String> strings = stream
.map(Object::toString)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

1
如果我使用map,我将不得不使用多行代码,类似于myStream.map(obj -> {obj.foo(); return obj;})。我想知道是否有一行代码的解决方案。 - alexgbelov
你也可以在那里引用一个方法,查看我的模块。 - csenga
12
地图不是在流的元素上采取行动的正确位置,它是用于将一个项目“转换”为其他东西的。 - Josh M.

2

您最好的选择是将地图应用于您的流。该操作返回一个流,其中包含将给定函数应用于流元素的结果。 例如:

IntStream.range(1, 100)
           .boxed()
           .map(item->item+3)
           .map(item->item*2)... 

我们正在对流进行多个修改,但在某些情况下,我们不想修改流。我们只想访问每个元素,然后将其传递到流中而不进行修改(就像流API中的peek()方法一样)。在这种情况下,我们可以

StreamItem peekyMethod(StreamItem streamItemX) {
   // .... visit the streamItemX
   //Then pass it down the stream
   return streamItemX;
}

1
我不完全确定您所说的“打破流链”的意思,但对于返回Stream的任何Stream上的操作都不会打破消耗您的流。流被终端操作消耗,正如您所指出的,forEach 不返回Stream<T>,因此在执行所有中间操作之前和forEach本身之后结束流。

在您提供的评论示例中:

 myStream.map(obj -> {obj.foo(); return obj;}

你不可能只用一行代码就完成这个任务。当然,你可以使用方法引用,但是你返回的Stream类型会有所不同(假设foo返回一个类型):

  myStream.map(Obj::foo) // this will turn into Stream<T>, where T is 
           // the return type of foo, instead of Stream<Obj>

除此之外,你的map操作是有状态的,这是强烈不建议的。你的代码可以编译并且可能按照你想要的方式工作,但是它可能会在以后失败。map操作应该是无状态的。

1
你可以使用 map 方法,但你需要创建一个返回 this 的帮助方法。例如:
public class Fluent {
  public static <T> Function<T, T> of(Consumer<T> consumer) {
    return t -> {
      consumer.accept(t);
      return t;
   };
 }
}

当你想调用 void 方法时,请使用它:

list.stream().map(Fluent.of(SomeClass::method));

或者,如果你想使用带有一些参数的方法:

list.stream().map(Fluent.of(x -> x.method("hello")))

0

我认为你正在寻找Stream.peek。但是请仔细阅读文档,因为它主要是作为调试方法设计的。从文档中可以看到:

此方法主要用于支持调试,您希望在管道中的某个点看到元素的流动

传递给 peek 的操作必须是非干扰性的


0
我认为最简洁的方法是在流中的对象中添加一个mutator。
例如:
class Victim {
   private String tag;
   private Victim withTag(String t)
      this.tag = t;
      return this;
   }
}

List<Victim> base = List.of(new Victim());
Stream<Victim> transformed = base.stream().map(v -> v.withTag("myTag"));

如果你喜欢的话(很多人都会这样做),你可以让 withTag 方法创建并返回一个新的 Victim;这样可以使 Victim 不可变。


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