Java 8 修改流元素

41

我想使用Java 8编写纯函数,该函数将集合作为参数,对该集合中的每个对象应用一些更改,并在更新后返回新的集合。 我想遵循FP原则,因此不希望更新/修改传递作为参数的集合。

有没有使用Stream API的方法可以在不先创建原始集合的副本的情况下完成这项任务(然后使用forEach或“普通”for循环)?

下面是一个示例对象,假设我想将文本附加到其中一个对象属性:

public class SampleDTO {
    private String text;
}

我想做与下面类似的事情,但不修改集合。 假设“list”是一个List<SampleDTO>

list.forEach(s -> {
    s.setText(s.getText()+"xxx");
});

2
我认为这有点违背了函数式编程的原则。这个想法是不要改变状态,而是通过函数创建新的状态。因此,你应该使用Java流API提供的map函数来创建一个新的列表,而不是改变原始列表。 - christopher
6个回答

69

你必须有一种方法/构造函数来生成现有的SampleDTO实例的副本,例如复制构造函数。

然后你可以将每个原始的SampleDTO实例映射到一个新的SampleDTO实例,并将它们收集到一个新的List中:

List<SampleDTO> output = 
    list.stream()
        .map(s-> {
                     SampleDTO n = new SampleDTO(s); // create new instance
                     n.setText(n.getText()+"xxx"); // mutate its state
                     return n; // return mutated instance
                 })
       .collect(Collectors.toList());

谢谢。你的答案帮助我解决了这个问题。https://stackoverflow.com/questions/59967184/convert-one-class-object-into-another-using-stream :) - Sanal S

5

我认为更干净、更易读的解决方案是:

List<SampleDTO> unmodifiable = Arrays.asList(new SampleDTO("1"), new SampleDTO("2"));

List<SampleDTO> modified = unmodifiable.stream()
            .map(s -> new SampleDTO(s.getText())) // '.map(SampleDTO::new)' with copy constructor
            .peek(s -> s.setText(s.getText() + "xxx"))
            .collect(Collectors.toList());

如果您拥有SampleDTO的复制构造函数,您可以用.map(SampleDTO::new)替换map函数。

peek函数将在不改变流结果类型的情况下对流元素进行操作。


3
注意:在生产代码中不应使用peek(),仅用于调试。 - alex87
peek()并不是用来改变流元素的(尽管它肯定可以,并且根据文档,“主要用于支持调试,您想在管道中某个点看到元素流过的情况”) - ghost28147
3
'peek'是一种惰性方法,不能对流中的所有元素执行操作。例如,当您使用“findFirst”方法查找流中的第一个元素时,“peek”将不会对其他元素执行操作。但如果在使用“collect”方法之前使用它,则表示所有元素都将用于插入集合中,因此可以使用“peek”。通常情况下,您只需要在了解流内部工作原理的情况下使用“peek”方法。文档中提到,“peek”主要用于调试,并非必须只在生产中使用。 - Alex
@alex87请看我上面的评论。 - Alex

5
为了更加优雅,我建议在类中创建一个方法
 public class SampleDTO {
 private String text;
public String getText() {
    return text;
}

public void setText(String text) {
    this.text = text;
}

public SampleDTO(String text) {
    this.text = text;
}

public SampleDTO getSampleDTO() {
    this.setText(getText()+"xxx");
    return this;
}
    }

并将其添加如下:

List<SampleDTO> output =list.stream().map(SampleDTO::getSampleDTO).collect(Collectors.toList();

@FedericoPeraltaSchaffner 感谢您的评论和投票。我已经修复了它。现在它将编译。 - soorapadman
我之前给你的回答点了个踩,因为它只有一个赞,而其他正确的回答却没有得到推广。如果我的行为过于严厉,那我很抱歉。现在你已经改正了回答,我不仅撤销了我的踩,还点了个赞。 - fps
1
然而,现在原始对象被修改了, OP 希望既不改变原始列表也不改变其元素。您应该考虑创建一个带有其文本属性修改的原始对象的副本。 - fps
5
方法getSampleDTO修改了它的文本?方法命名非常糟糕,并且存在非常不好的副作用。 - Michal Pasinski

3

流(Streams)与字符串一样是不可变的,因此您无法避免需要创建一个新的流/列表/数组。

话虽如此,您可以使用.Collect()方法在更改后返回一个新的集合。

List<Integer> result = inList.stream().map().Collect()

2
我认为,特别是在进行多线程工作时,最好将原始列表流式传输到新的修改列表或其他所需结构中。
新的列表、映射或其他所需结构可以作为流处理的一部分创建。
当流处理完成后,只需用新的列表替换原来的列表。
所有这些都应该发生在同步块中。
通过这种方式,您可以获得最大的性能和并行性,以及一个原子交换。

1
如果您使用Lombok,您可以使用不可变的setter方法。
.map(s -> s.withText(s.getText() + "..."))

请参阅:https://projectlombok.org/features/With。这是与IT技术有关的内容。

该死,我喜欢它! - C. Oltan

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