使用Java Streams获取嵌套在HashMap中元素最多的Set

9
所以这里是情况: 我需要为特定日期注册人们的投票。简而言之,提出一个日期,人们投票选择他们想要的日期。
数据结构如下:
private HashMap<LocalDateTime, Set<Vote>> votes;

投票是:

public class Vote {
    private String name;
    private VoteType vote;

    public Vote(String name, VoteType vote) {
        super();
        this.name = name;
        this.vote = vote;
    }
}

VoteType是一个枚举类型:

public enum VoteType {YES, NO, MAYBE}

现在我已经创建了一个流,返回可用性(VoteType)的投票数量:

public Map<LocalDateTime, Integer> voteCount(VoteType targetVote) {
    return this.votes.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new Integer(
            e.getValue().stream().filter(v -> v.getVote() == targetVote).collect(Collectors.toList()).size())));
}

我的问题是:如何使用 Java Streams 获取最多“YES”日期。
/* Returns the date that got the most 'YES' votes */
public LocalDateTime winningDate() {
    // TODO
}

感谢您的帮助!

MAYBE投票类型是否有获胜日期的值? - Mohsen
我的意思是,如果“YES”相同,我们应该计算“MAYBE”的投票吗? - Mohsen
只有“是”。 - Bratislav B.
永远没有使用 new Integer(…) 的理由。从Java 9开始,这个构造函数已被标记为已弃用 - Holger
6个回答

6
我的问题是:如何使用Java Streams获取最多“YES”的日期。 以下是详细步骤: 1. 首先,我们需要得到一个Stream<LocalDateTime>类型的数据流,然后通过使用flatMap转换它。这样我们可以按日期进行分组,应用计数器下游收集器来获取特定日期上的投票数。 2. 我们需要保留投票类型为YES的对象。 3. 将结果按日期分组,并且将值设为该日期上YES选票的数量。 4. 对于每个分组中选票数量最大的日期,我们使用entrySet方法流式处理并找到相应的最大日期。
请参考以下代码:
/* Returns the date that got the most 'YES' votes */
public Optional<LocalDateTime> getWinningDate() {
    return votes.entrySet() // Set<Entry<LocaleDateTime, Set<Vote>>
            .stream() // Stream<Entry<LocaleDateTime, Set<Vote>>
            .flatMap(e -> e.getValue().stream().filter(a -> a.getVote() == VoteType.YES)
                         .map(x -> e.getKey())) // Stream<LocalDateTime>
           .collect(groupingBy(Function.identity(), counting())) // Map<LocaleDateTime, Long>
           .entrySet() // Set<Entry<LocaleDateTime, Long>>
           .stream() // Stream<Entry<LocaleDateTime, Long>>
           .max(Comparator.comparingLong(Map.Entry::getValue)) // Optional<Entry<LocaleDateTime, Long>>
           .map(Map.Entry::getKey); // Optional<LocalDateTime>
}
  • 请注意我已将方法返回类型更改为Optional<LocaleDateTime>,我本可以返回.map(Map.Entry::getKey).orElse(null),这样您就可以保持当前方法的返回类型LocalDateTime,但那样做感觉不好,因此我决定将“无值情况”下的处理方式推迟到客户端。
  • 我已将方法名称更改为getWinningDate以提高可读性。

至于处理Optional<T>,在您的情况下,如果您希望在getWinningDate返回空的Optional时具有null值,则可以安全地取消包装它:

LocalDateTime winningDate = getWinningDate().orElse(null);

或者,如果您想提供一个默认日期:

LocalDateTime winningDate = getWinningDate().orElse(defaultDate);

如果你确定总会有结果,那么只需调用get()

LocalDateTime winningDate = getWinningDate().get();

etc..


1
您正在映射到SimpleEntry,尽管您没有在任何地方使用第二个值。 您可以在此处简单地映射到e.getKey(),并在随后的collect(groupingBy(…))中使用Function.identity()。 或者完全消除flatMap,就像您的第二个答案一样。 - Holger
@Holger一如既往地感谢您的有用评论。已根据您的建议进行了编辑。 - Ousmane D.

4
你可以这样做:
private LocalDateTime winningDate(Map<LocalDateTime, Integer> mapGroup) {
    Integer max = mapGroup
                    .values().stream()
                    .max(Comparator.naturalOrder())
                    .get();

    return mapGroup
                    .entrySet()
                    .stream()
                    .filter(e -> e.getValue().equals(max))
                    .map(Map.Entry::getKey)
                    .findFirst().orElse(null);
}

4
这个回答展示了一种不使用voteCount方法的方法,但以防万一您想在winningDate方法中编写一些逻辑来与已经制作好的voteCount方法集成,则可以这样做:
在这种情况下,我们可以这样做:
/* Returns the date that got the most 'YES' votes */
public Optional<LocalDateTime> getWinningDate() {
    return voteCount(VoteType.YES).entrySet() // call voteCount and stream over the entries
            .stream()
            .max(Comparator.comparingLong(Map.Entry::getValue))
            .map(Map.Entry::getKey);
}
  • 首先我们调用voteCount(VoteType.YES)方法来获取日期和当天YES投票数量的映射。
  • 其次,我们通过投票数找到最大的LocalDateTime
  • 请注意,我已将该方法的返回类型更改为Optional<LocaleDateTime>,我本可以返回.map(Map.Entry::getKey).orElse(null),这样您就可以保持当前的方法返回类型LocalDateTime,但那样会感觉不好,所以我决定将“没有值的情况”决策推迟给客户端。
  • 我将方法名称更改为getWinningDate以增强可读性。

此外,voteCount方法可以进行改进:

public Map<LocalDateTime, Long> voteCount(VoteType targetVote) {
        return this.votes.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, 
                e -> e.getValue().stream().filter(v -> v.getVote() == targetVote).count()));
}

这样做可以避免为了通过 size() 获取计数而构造通过筛选的所有元素列表的开销,而是只需使用 filter 并调用 count

4

使用您的第一种方法,计算YES投票数,返回一个yes计数的映射表,该表被传递到获胜日期方法中:

/* Returns the date that got the most 'YES' votes */
public LocalDateTime winningDate(Map<LocalDateTime, Integer> yesVotes) {
    return yesVotes.entrySet().stream().max(Map.Entry.comparingByValue()).get().getKey();
}

我不禁想到这可能是这里的意图,但我又怎么知道呢。


3

您问如何使用流来完成,这里有另一种方法:

class Max { long value = Long.MIN_VALUE; LocalDateTime date; }
Max max = new Max();
votes.forEach((d, vs) -> {
    long count = vs.stream().filter(v -> VoteType.YES == v.getVote()).count();
    if (count > max.value) {
        max.value = count;
        max.date = d;
    }
});

LocalDateTime maxDate = max.date;

获取投票集合的方法如下:

Set<Vote> maxVotesForYes = votes.get(maxDate);

这个解决方案遍历地图条目并计算每个日期的“是”票数。如果此计数大于当前的最大计数,则更改最大计数(以及其对应的日期)。
为了能够修改最大计数及其对应的日期,我们需要一个本地类Max来跟踪这些值(否则,我们将无法从lambda中更改变量)。

1
非常有想象力,像往常一样,费德里科总是超越传统思维。加一。 - Ousmane D.

2
这个问题是关于如何使用Java Streams来解决的。下面的方法就是使用Streams,以及一个for循环。
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;


public class VoteCountTest
{
    public static void main(String[] args)
    {
        Map<LocalDateTime, Set<Vote>> votes = 
            new LinkedHashMap<LocalDateTime, Set<Vote>>();

        Set<Vote> yes0 = votesWith(VoteType.NO, VoteType.NO);
        Set<Vote> yes1 = votesWith(VoteType.YES, VoteType.NO);
        Set<Vote> yes2 = votesWith(VoteType.YES, VoteType.YES);

        votes.put(LocalDateTime.of(2000, 1, 1, 1, 1), yes1);
        votes.put(LocalDateTime.of(2000, 1, 2, 1, 1), yes0);
        votes.put(LocalDateTime.of(2000, 1, 3, 1, 1), yes2);
        votes.put(LocalDateTime.of(2000, 1, 4, 1, 1), yes1);

        System.out.println(getWinningDateA(votes));
        System.out.println(getWinningDateB(votes));
    }

    public static Optional<LocalDateTime> getWinningDateA(
        Map<LocalDateTime, Set<Vote>> votes)
    {
        LocalDateTime bestDate = null;
        long maxCount = -1;
        Predicate<Vote> votedYes = v -> v.getVote() == VoteType.YES;
        for (Entry<LocalDateTime, Set<Vote>> entry : votes.entrySet())
        {
            long count = entry.getValue().stream().filter(votedYes).count(); 
            if (count > maxCount)
            {
                maxCount = count;
                bestDate = entry.getKey();
            }
        }
        return Optional.ofNullable(bestDate);
    }

    // As of https://dev59.com/xLDla4cB1Zd3GeqP83sB#53771478
    public static Optional<LocalDateTime> getWinningDateB(Map<LocalDateTime, Set<Vote>> votes) 
    {
        return votes.entrySet() // Set<Entry<LocaleDateTime, Set<Vote>>
                .stream() // Stream<Entry<LocaleDateTime, Set<Vote>>
                .flatMap(e -> e.getValue().stream().filter(a -> a.getVote() == VoteType.YES)
                             .map(x -> e.getKey())) // Stream<LocalDateTime>
               .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) // Map<LocaleDateTime, Long>
               .entrySet() // Set<Entry<LocaleDateTime, Long>>
               .stream() // Stream<Entry<LocaleDateTime, Long>>
               .max(Comparator.comparingLong(Map.Entry::getValue)) // Optional<Entry<LocaleDateTime, Long>>
               .map(Map.Entry::getKey); // Optional<LocalDateTime>
    }    


    //=========================================================================
    enum VoteType {YES, NO, MAYBE}

    static class Vote {
        private String name;
        private VoteType vote;

        public Vote(String name, VoteType vote) {
            super();
            this.name = name;
            this.vote = vote;
        }
        public VoteType getVote()
        {
            return vote;
        }
    }

    private static Set<Vote> votesWith(VoteType... voteTypes)
    {
        Set<Vote> votes = new LinkedHashSet<Vote>();
        for (int i = 0; i < voteTypes.length; i++)
        {
            votes.add(new Vote("v" + i, voteTypes[i]));
        }
        return votes;
    }

}

与“纯流”解决方案相比较,考虑一下哪种代码你更喜欢读、理解和维护。然后明智地选择。

(我知道这可能不是问题的期望答案。但有些人似乎故意过度使用流,并从中获得某种极客的自豪感。我也偶尔享受那种挑战。但想象一下我可能会成为未来维护这种函数式编程罪恶的人让我感到发抖...)


很棒的答案,确实是一篇好文章 +1。顺便问一下,在for循环之前声明谓词 --> Predicate<Vote> votedYes = v -> v.getVote() == VoteType.YES; 是否更好?这样你只需要构建一次而不是在每次迭代中都创建一个。 - Ousmane D.
@Aomine 这确实是一个合理的做法(已更新)。但现在你让我很好奇,这是否不是JIT可以找出来的东西呢...;-) - Marco13
既然你提到了,我大致记得读过一篇博客谈论类似的事情,但说实话并不确定。我可能会在有空的时候去看看,因为我也有点好奇;-) - Ousmane D.
1
@Aomine 嗯,查找资料很无聊;-) 但是我承认,我现在只做了一个天真的快速测试,即使是最简单的循环涉及流时,编译后的代码也非常庞大-远远超出了我能够合理分析的范围。除此之外,考虑到运行时编译、lambda类的实例化和逃逸分析将在这里发挥作用,很难从中找出一个可以获得“可靠”信息的示例。(但如果你找到一个好的链接,我仍然会感兴趣) - Marco13

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