【问题标题】:Mutate elements in a Stream改变 Stream 中的元素
【发布时间】:2016-02-19 06:05:09
【问题描述】:

是否有在 Stream 中改变元素的“最佳实践”?我特别指的是流管道内的元素,而不是它之外的元素。

例如,考虑我想要获取用户列表、为空属性设置默认值并将其打印到控制台的情况。

假设用户类:

class User {
    String name;

    static User next(int i) {
        User u = new User();
        if (i % 3 != 0) {
            u.name = "user " + i;
        }
        return u;
    }
}

在 java 7 中,它类似于:

for (int i = 0; i < 7; i++) {
    User user = User.next(i);
    if(user.name == null) {
        user.name = "defaultName";
    }
    System.out.println(user.name);
}

在 java 8 中,我似乎会使用 .map() 并返回对变异对象的引用:

IntStream.range(0, 7)
    .mapToObj(User::next)
    .map(user -> {
        if (user.name == null) {
            user.name = "defaultName";
        }
        return user;
    })
    //other non-terminal operations
    //before a terminal such as .forEach or .collect
    .forEach(it -> System.out.println(it.name));

有没有更好的方法来实现这一点?也许使用 .filter() 来处理空突变,然后连接未过滤的流和过滤的流?一些巧妙的使用 Optional?目标是能够在终端 .forEach() 之前使用其他非终端操作。

在流的“精神”中,我试图在没有中间集合和不依赖管道外部副作用的简单“纯”操作的情况下做到这一点。

编辑:官方 Stream java doc 声明'少量流操作,例如 forEach() 和 peek(),只能通过副作用进行操作;这些应该小心使用。鉴于这将是一个不干扰的操作,是什么让它特别危险?我看到的示例超出了管道,这显然是粗略的。

【问题讨论】:

  • “是否有‘最佳实践’来改变 Stream 中的元素?”。是的:不要。
  • 这种用法明显滥用map()。在forEach() 中做你的突变。
  • 如果我要在 .forEach() 中执行此操作,那么我如何获得对收集到的 User 对象的引用以执行其他工作?

标签: java java-8 java-stream


【解决方案1】:

不要改变对象,直接映射到名字:

IntStream.range(0, 7)
    .mapToObj(User::next)
    .map(user -> user.name)
    .map(name -> name == null ? "defaultName" : name)
    .forEach(System.out::println);

【讨论】:

    【解决方案2】:

    听起来你在找peek

    .peek(user -> {
        if (user.name == null) {
            user.name = "defaultName";
        }
    })
    

    ...虽然不清楚您的操作实际上需要修改流元素,而不仅仅是通过您想要的字段:

    .map(user -> (user.name == null) ? "defaultName" : user.name)
    

    【讨论】:

    • 来自peek javadoc:“此方法的存在主要是为了支持调试,您希望在元素流过管道中的某个点时查看它们
    • @DidierL:嗯,是的,那是因为您实际上不应该修改流管道中的元素。不过,如果你愿意,这就是方法。
    • 这才是问题的真正答案:最佳做法是这样做,正如@Tunaki 在评论中所述。不过,在forEach 中这样做也可以。
    • 您可以执行非干扰修改,但不建议这样做。 peekas explained here 的问题在于它自己不做任何事情,而是作为实际终端操作的副产品执行,并取决于其语义。因此,如果您想对 所有元素 执行 peek 操作,但将其与短路的终端操作相结合,甚至在没有处理元素的情况下预测最终结果,您就有麻烦了,尤其是因为它可能在某些情况下有效,但在其他情况下会失败。
    • @shmosel:这仍然可以通过让convert 方法进行转换,或者通常,如果 lambda 表达式变得太大,您总是可以将代码放入命名方法并使用方法引用改用那个新方法。这就是所说的清洁工……
    【解决方案3】:

    Streams 似乎无法在一个管道中处理此问题。 “最佳实践”是创建多个流:

    List<User> users = IntStream.range(0, 7)
        .mapToObj(User::next)
        .collect(Collectors.toList());
    
    users.stream()
        .filter(it -> it.name == null)
        .forEach(it -> it.name = "defaultValue");
    
    users.stream()
        //other non-terminal operations
        //before terminal operation
        .forEach(it -> System.out.println(it.name));
    

    【讨论】:

    • 我自己不会想到这个,但它确实看起来是最好的解决方案。
    • 正如其他人已经指出的那样,这显然是对 Streams 的滥用。希望人们停止对这个答案投票,因为它鼓励读者认为这是可以的。不要在流上变异。正如其他人指出的那样,如果只需要名称,请使用 Stream.map。如果在获取名称之前需要进行其他转换,请使用不可变的 User 对象,执行转换,然后通过 Stream.map 方法更改其名称
    猜你喜欢
    • 1970-01-01
    • 2015-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多