Java 流式处理列表并检查列表是否包含列表中具有三个给定字段值之一的对象

Java Stream over a list and check if the list contains at list one object with one of three given field values

给定一个 class 球(针对此问题进行了简化),我无法更改 equalshashCode 方法

class Ball {
    String color;
    //some more fields, getters, setters, equals, hashcode ..
}

和一个球列表,如果列表中每个颜色值 "RED", "YELLOW" and "GREEN" 至少包含一个球,我想 return 为真。输入示例:

List<Ball> first = List.of(
        new Ball("RED"),
        new Ball("BLUE"),
        new Ball("GREEN"),
        new Ball("RED"),
        new Ball("YELLOW"),
        new Ball("RED"));

List<Ball> second = List.of(
        new Ball("RED"),
        new Ball("BLUE"),
        new Ball("GREEN"),
        new Ball("RED"));

第一个列表的预期结果为真,第二个为假。现在我有一个 classic 循环和三个计数器变量:

private static boolean isValidList(final List<Ball> balls) {
    int r = 0;
    int y = 0;
    int g = 0;
    for (Ball ball : balls) {
        String color = ball.getColor();
        if("RED".equals(color)){
            r++;
        }
        else if("YELLOW".equals(color)){
            y++;
        }
        else if("GREEN".equals(color)){
            g++;
        }

        if(r > 0 && y > 0 && g > 0){
            break;
        }
    }
    return r > 0 && y > 0 && g > 0;
}

我尝试重构它以使用如下所示的流

private static boolean isValidListStreams(final List<Ball> balls) {
    long r = balls.stream().filter(ball -> "RED".equals(ball.getColor())).count();
    long y = balls.stream().filter(ball -> "YELLOW".equals(ball.getColor())).count();
    long g = balls.stream().filter(ball -> "GREEN".equals(ball.getColor())).count();
    return r > 0 && y > 0 && g > 0;
}

但上面的内容需要遍历列表 3 次。有什么办法可以一次性完成吗?我无法使用 or

进行过滤
return balls.stream()
            .filter(ball -> ball.getColor().equals("RED") ||
                            ball.getColor().equals("YELLOW") || 
                            ball.getColor().equals("GREEN")).count() >= 3;

因为可能有多个相同的颜色。

您可以提取不同的颜色(使用 Stream API),然后只需在 Set.

中搜索
Set<String> colors = balls.stream().map(Ball::getColor)
    .collect(Collectors.toSet());
if (colors.contains("RED") && colors.contains("GREEN") && colors.contains("YELLOW")) {
    // test passes  ...
}

如果需要的颜色被预先计算为最终的 Set<String>,代码可以通过使用 containsAll 更具可读性(检查检索到的集合是否是所需集合的超集):

final Set<String> requiredColors = Set.of("RED", "GREEN", "YELLOW");
Set<String> colors = balls.stream().map(Ball::getColor)
    .collect(Collectors.toSet());
if (colors.containsAll(requiredColors)) { /* test passes */ }

I can't do it with filter using or since there may be multiple of the same color.

您可以只使用 distinct 删除重复的颜色。

因为你不能修改 equals,你应该首先 map 一切都到他们的颜色,然后 distinctfilter.

return balls.stream()
            .map(Ball::getColor)
            .distinct()
            .filter(color -> color.equals("RED") ||
                             color.equals("YELLOW") || 
                             color.equals("GREEN")).count() == 3;

请注意,您原来的 for 循环是 short-circuiting - 一旦找到所需的三种颜色,就停止循环。但是,count 会计算一切。如果这不是您想要的,您可以在它之前做一个 limit(3)

此外,如果要检查的颜色很多,将 || 链替换为 Set.of(...).contains 可能会更好看:

return balls.stream()
            .map(Ball::getColor)
            .distinct()
            .filter(Set.of("RED", "YELLOW", "GREEN")::contains)
            .limit(3)
            .count() == 3;

让我们公平竞争,您的原始代码段比需要的要长得多:

boolean r = false, y = false, g = false;
  for (Ball ball : balls) {
    String color = ball.getColor();
    if ("RED".equals(color)) r = true;
    if ("YELLOW".equals(color)) y = true;
    if ("GREEN".equals(color)) g = true;
    if (r && y && g) return true;
  }
  return false;

如果您必须引用其他操作的结果,流不会 'like it'。那是因为流 API 试图迎合太多的场景,因此,你得到了最低的公分母。在这种情况下,这是并行处理:想象一下 java 运行 通过将每个单独的项目交给一个单独的系统来处理您的流 - 现在不再有 'any previous result' 或'have we seen at least 1 red, at least 1 green, and at least 1 yellow ball at this point' - 没有 'this point',只有流本身。

因此,它要么看起来很难看(因为您使用了错误的工具来完成这项工作),要么从根本上说效率低得多。它看起来像这样:

return balls.stream()
  .map(Ball::getColor)
  .filter(x -> x.equals("RED") || x.equals("GREEN") || x.equals("YELLOW"))
  .distinct()
  .count() == 3;

比较代码长度并没有简单得多。它的性能要差得多:它需要进行不同的扫描,这需要另一个 运行 通过,并且必须迭代整个过程,而第一个片段会在看到第三种颜色时停止。

试图将它们粉碎回去,你看到的是一个真正丑陋的烂摊子。尽可能小地打高尔夫球:

boolean[] c = new boolean[4];
return balls.stream()
  .map(Ball::getColor)
  .peek(x -> c[x.equals("RED") ? 0 : x.equals("YELLOW") ? 1 : x.equals("BLUE") ? 2 : 3] = true)
  .anyMatch(x -> c[0] && c[1] && c[2]);

它的代码不多,但它引入了各种奇怪的东西——这很奇怪,可能需要评论来解释发生了什么。所以不是真正的 'win'。它肯定不会比原来的更快。

通常,当您为了在值之间进行对比而遍历集合时,这些操作不能用列表本身的基元来描述(例如 .distinct().sorted().limit) 并且没有 pre-baked 终端操作(例如 .max())可以满足您的需求,很可能您 想要流.

在排序上我的建议是:

  • 不要 hard-code 值在方法内部检查,将它们作为参数提供。

  • 使用enums,不要依赖字符串。

因为您要用字符串名称描述每个 Ball object 的颜色( 例如不是 hex-code)暗示您希望在您的应用程序中只使用中等数量的颜色。

并且您可以通过使用自定义 enum 类型 Color 而不是流来改进 Ball class 的设计。它将防止您输入错误,还提供了使用 Color 枚举引入有用行为的可能性,并且还受益于各种语言和与枚举相关的 JDK 功能。

public enum Color {RED, YELLOW, GREEN}

即使您不考虑使用枚举,也值得通过包含一个附加参数来更改您列出的方法的方法签名 - Set 颜色而不是 hard-coding 它们.

注:还有一个不一致 在标题和您提供的代码之间。标题说:

check if the list contains at list one object with one of three given

但是,您的代码旨在检查是否所有给定值都存在。

这就是您如何检查给定集合中是否存在至少一种颜色,如问题标题所述,:

private static boolean isValidListStreams(final List<Ball> balls, Set<Color> colors) {
    return balls.stream()
        .map(Ball::getColor)
        .anyMatch(colors::contains);
}

但是如果你需要检查所有 给定的颜色存在,你可以这样做:

private static boolean isValidList(final List<Ball> balls, Set<Color> colors) {
    return colors.equals(
      balls.stream()
          .map(Ball::getColor)
          .filter(colors::contains)
          .limit(colors.size())
          .collect(Collectors.toSet())
    );
}

main()

public static void main(String[] args) {
    List<Ball> balls = // initializing the source list
    
    isValidListStreams(balls, Set.of(Color.RED, Color.GREEN, Color.YELLOW)); // or simply EnumSet.allOf(Color.class) when you need all enum elements instead of enumerating them
}