[英]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.