簡體   English   中英

使用 Java 8 的復雜自定義收集器

[英]Complex custom Collector with Java 8

我有一個對象流,我想通過以下方式收集它們。

假設我們正在處理論壇帖子

class Post {
    private Date time;
    private Data data
}

我想創建一個按時間段對帖子進行分組的列表。 如果X分鍾內沒有帖子,請創建一個新組

class PostsGroup{
    List<Post> posts = new ArrayList<> ();
}

我想要一個List<PostGroups>包含按時間間隔分組的帖子

示例:間隔10分鍾。

帖子:

[{time:x, data:{}}, {time:x + 3, data:{}} , {time:x + 12, data:{}, {time:x + 45, data:{}}}]

我想獲取帖子組列表:

[
 {posts : [{time:x, data:{}}, {time:x + 3, data:{}}, {time:x + 12, data:{}]]},
{posts : [{time:x + 45, data:{}]}
]
  • 請注意,第一持續到X + 22 然后在X + 45收到了一個新帖子

這可能嗎?

使用我的StreamEx庫的groupRuns方法可以輕松解決此問題:

long MAX_INTERVAL = TimeUnit.MINUTES.toMillis(10);
StreamEx.of(posts)
        .groupRuns((p1, p2) -> p2.time.getTime() - p1.time.getTime() <= MAX_INTERVAL)
        .map(PostsGroup::new)
        .toList();

我假設你有一個構造函數

class PostsGroup {
    private List<Post> posts;

    public PostsGroup(List<Post> posts) {
        this.posts = posts;
    }
}

StreamEx.groupRuns方法采用BiPredicate應用於兩個相鄰的輸入元素,如果它們必須組合在一起,則返回 true。 此方法創建列表流,其中每個列表代表組。 此方法是惰性的,並且適用於並行流。

您需要保留流條目之間的狀態並為自己編寫一個分組分類器。 像這樣的事情將是一個好的開始。

class Post {

    private final long time;
    private final String data;

    public Post(long time, String data) {
        this.time = time;
        this.data = data;
    }

    @Override
    public String toString() {
        return "Post{" + "time=" + time + ", data=" + data + '}';
    }

}

public void test() {
    System.out.println("Hello");
    long t = 0;
    List<Post> posts = Arrays.asList(
            new Post(t, "One"),
            new Post(t + 1000, "Two"),
            new Post(t + 10000, "Three")
    );
    // Group every 5 seconds.
    Map<Long, List<Post>> gouped = posts
            .stream()
            .collect(Collectors.groupingBy(new ClassifyByTimeBetween(5000)));
    gouped.entrySet().stream().forEach((e) -> {
        System.out.println(e.getKey() + " -> " + e.getValue());
    });

}

class ClassifyByTimeBetween implements Function<Post, Long> {

    final long delay;
    long currentGroupBy = -1;
    long lastDateSeen = -1;

    public ClassifyByTimeBetween(long delay) {
        this.delay = delay;
    }

    @Override
    public Long apply(Post p) {
        if (lastDateSeen >= 0) {
            if (p.time > lastDateSeen + delay) {
                // Grab this one.
                currentGroupBy = p.time;
            }
        } else {
            // First time - start there.
            currentGroupBy = p.time;
        }
        lastDateSeen = p.time;
        return currentGroupBy;
    }

}

由於沒有人提供原始問題陳述中要求的自定義收集器的解決方案,因此這里是一個收集器實現,它根據提供的時間間隔對Post對象進行分組。

問題中提到的Date類自 Java 8 以來已過時,不建議在新項目中使用。 因此,將改為使用LocalDateTime

郵政和郵政集團

出於測試目的,我使用Post實現為 Java 16記錄如果將其替換為類,則整體解決方案將完全符合 Java 8 ):

public record Post(LocalDateTime dateTime) {}

此外,我還增強了PostGroup對象。 我的想法是,它應該能夠決定是否應該將提供的Post添加到帖子列表中,或者按照信息專家原則的建議被拒絕(簡而言之:對數據的所有操作都應該只發生在該數據所屬的類中)。

為了促進此功能,添加了兩個額外字段: java.time包中的Duration類型的interval ,表示最早帖子最新帖子之間的最大間隔,以及LocalDateTime類型的intervalBound ,它在第一次發布后初始化稍后將被添加,將由isWithinInterval()方法在內部使用,以檢查提供的帖子是否適合interval

public class PostsGroup {
    private Duration interval;
    private LocalDateTime intervalBound;
    private List<Post> posts = new ArrayList<>();
    
    public PostsGroup(Duration interval) {
        this.interval = interval;
    }
    
    public boolean tryAdd(Post post) {
        if (posts.isEmpty()) {
            intervalBound = post.dateTime().plus(interval);
            return posts.add(post);
        } else if (isWithinInterval(post)) {
            return posts.add(post);
        }
        return false;
    }
    
    public boolean isWithinInterval(Post post) {
        return post.dateTime().isBefore(intervalBound);
    }
    
    @Override
    public String toString() {
        return "PostsGroup{" + posts + '}';
    }
}

我做了兩個假設:

  • 源中的所有帖子都是按時間排序的(如果不是這樣,你應該在收集結果之前在管道中引入sorted()操作);
  • 帖子需要收集到最少數量的組中,因此無法拆分此任務並並行執行流。

構建自定義收集器

我們可以通過使用靜態方法Collector.of()的一個版本或通過定義實現Collector接口的class來內聯創建自定義收集器。

創建自定義收集器時必須提供這些參數

  • 供應商Supplier<A>旨在提供一個可變容器來存儲流的元素。 在這種情況下, ArrayDeque (作為Deque接口的實現)將作為容器方便地訪問最近添加的元素,即最新的PostGroup

  • 累加器BiConsumer<A,T>定義如何將元素添加到供應商提供的容器中。 對於這個任務,我們需要提供邏輯來確定流中的下一個元素(即下一個Post )是否應該進入Deque中的最后一個PostGroup ,或者需要為其分配一個新的PostGroup

  • Combiner BinaryOperator<A> combiner()建立了一個規則,用於合並並行執行流時獲得的兩個容器 由於此操作被視為不可並行化,因此組合器被實現為在並行執行的情況下拋出AssertionError

  • Finisher Function<A,R>旨在通過轉換可變容器來產生最終結果。 下面代碼中的finisher函數將容器(包含結果的雙端隊列)轉換為不可變列表

注意: Java 16 的toList()方法在Finisher函數中使用,對於 Java 8,它可以替換為collect(Collectors.toUnmodifiableList())collect(Collectors.toList())

  • 特性允許提供附加信息,例如在這種情況下使用的Collector.Characteristics.UNORDERED表示並行執行時產生的部分歸約結果的順序並不重要。 在這種情況下,收集器不需要任何特性。

下面的方法負責根據提供的時間間隔生成收集器

public static Collector<Post, ?, List<PostsGroup>> groupPostsByInterval(Duration interval) {
    
    return Collector.of(
        ArrayDeque::new,
        (Deque<PostsGroup> deque, Post post) -> {
            if (deque.isEmpty() || !deque.getLast().tryAdd(post)) { // if no groups have been created yet or if adding the post into the most recent group fails
                PostsGroup postsGroup = new PostsGroup(interval);
                postsGroup.tryAdd(post);
                deque.addLast(postsGroup);
            }
        },
        (Deque<PostsGroup> left, Deque<PostsGroup> right) -> { throw new AssertionError("should not be used in parallel"); },
        (Deque<PostsGroup> deque) -> deque.stream().collect(Collectors.collectingAndThen(Collectors.toUnmodifiableList())));
}

main() - 演示

public static void main(String[] args) {
    List<Post> posts =
        List.of(new Post(LocalDateTime.of(2022,4,28,15,0)),
                new Post(LocalDateTime.of(2022,4,28,15,3)),
                new Post(LocalDateTime.of(2022,4,28,15,5)),
                new Post(LocalDateTime.of(2022,4,28,15,8)),
                new Post(LocalDateTime.of(2022,4,28,15,12)),
                new Post(LocalDateTime.of(2022,4,28,15,15)),
                new Post(LocalDateTime.of(2022,4,28,15,18)),
                new Post(LocalDateTime.of(2022,4,28,15,27)),
                new Post(LocalDateTime.of(2022,4,28,15,48)),
                new Post(LocalDateTime.of(2022,4,28,15,54)));
    
    Duration interval = Duration.ofMinutes(10);

    List<PostsGroup> postsGroups = posts.stream()
        .collect(groupPostsByInterval(interval));
    
    postsGroups.forEach(System.out::println);
}

輸出:

PostsGroup{[Post[dateTime=2022-04-28T15:00], Post[dateTime=2022-04-28T15:03], Post[dateTime=2022-04-28T15:05], Post[dateTime=2022-04-28T15:08]]}
PostsGroup{[Post[dateTime=2022-04-28T15:12], Post[dateTime=2022-04-28T15:15], Post[dateTime=2022-04-28T15:18]]}
PostsGroup{[Post[dateTime=2022-04-28T15:27]]}
PostsGroup{[Post[dateTime=2022-04-28T15:48], Post[dateTime=2022-04-28T15:54]]}

你也可以玩這個在線演示

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM