简体   繁体   中英

how to collect from stream using multiple conditions

I'm trying to sort a list of Message object, this entity contain multiple attributs but only 4 are useful for us in this case:

  • Integer: Ordre
  • String: idOras
  • Date: sentDate
  • Integer: OrdreCalcule (a concatination of Ordre and sentDate "YYYYmmDDhhMMss" )

if this case, the conditions of selection are the following:

  • if two Messages have the same Ordre:
    • if they have the same idOras -> collect the newest one (newest sentDate) and remove the others
    • if they have different idOras -> collect both of them sorted by sentDate ASC
  • if two Messages have different Ordre:
    • collect both of them sorted by Ordre

for now I'm using this stream:

orasBatchConfiguration.setSortedZbusOrasList(messageList.stream()
            .collect(Collectors.groupingBy(Message::getIdOras,
                    Collectors.maxBy(Comparator.comparing(Message::getOrdreCalcule))))
            .values()
            .stream()
            .map(Optional::get)
            .sorted(Comparator.comparing(Message::getOrdreCalcule))
            .collect(Collectors.toList()));

You need composite comparator for that. Let me emphasize this is bad design from start - Comparator should be comparing to objects and that's all, not doing some other stuff while comparing. Add aditional field to your message + getters/setters:

private boolean isForRemoval;

Then create custom comparator, something like this should do the trick:

    Comparator<Message> comparator = Comparator.comparing(Message::getOrdre)
                .thenComparing((m1, m2) -> {
                    if (m1.getIdOras().equals(m2.getIdOras())) {
                        //compare dates
                        int cmp = m1.getSentDate().compareTo(m2.getSentDate());
                        switch (cmp) {
                            case 1:
                                //m2 date is before other date, so set it for removal
                                m2.setForRemoval(true);
                                break;
                            case 0:
                                //what to do if same date?
                                break;
                            case -1:
                                //m1 date is before other date, so set it for removal
                                m1.setForRemoval(true);
                                break;
                        }
                        return cmp;
                    } else {
                        return m1.getSentDate().compareTo(m2.getSentDate());
                    }
                });

This compares by ordre first, if equal compare idOras, if equal again, compare dates and set older for removal, otherwise compare by date.

Then sort and filter out elements set for removal:

        List<Message> list = ...;
        list.stream()
                .sorted(comparator)
                .filter(m -> !m.isForRemoval())
                .forEach(System.out::println);

I'll start with a few general suggestions. Firstly, please pay attention to the stream pipeline you've provided. The point in the middle where invocation values().stream() happens is a smell.

Keep in mind that streams and functions were introduced in java to reduce the complexity of code. Therefore it is not considered to be a good practice to create a stream that produces a value and then right away immediately after the terminal operation spans another stream with its own logic.

That leads to the creation of methods that violate the single responsibility principle. Consider invocations of stream() in the middle of a pipeline and nested streams as splitting points indicating that the next piece of code most likely must belong to a separate method.

Avoiding excessive chaining will pay off with methods that are high-cohesive and narrow-focused and as conscience easier to read and debug.

Now, regarding your task, you have to:

  • eliminate the stale messages;
  • sort those that are up-to-date by order and then by date.

To achieve the first goal I introduced a wrapper class that enables to resolve two messages based on their data and overrides equals and hashCode based on Ordre and idOras.

Auxilary class MessageWrapper

public class MessageWrapper {
    private Message message;

    public MessageWrapper(Message message) {
        this.message = message;
    }

    /**
     * @return message with the most recent sentDate
     */
    public MessageWrapper resolve(MessageWrapper other) {
        return this.message.getSentDate()
                .compareTo(other.message.getSentDate()) > 0 ? this : other;
    }

    public Message getMessage() {
        return message;
    }
    
    /**
     * concatenation of order Ordre and idOras
     * used as a key in the collector
     */
    public String getKey() {
        return message.getIdOras() + message.getOrdre();
    }

    @Override
    public int hashCode() {
        return message.getIdOras().hashCode() ^ (49 + message.getOrdre().hashCode());
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof MessageWrapper other) {
            return this.message.getOrdre() == other.message.getOrdre() &&
                    this.message.getIdOras().equals(other.message.getIdOras());
        }
        return false;
    }
}

I'm assuming that you have NO control over the message class. But if it is not the case you can add this logic to the message class. I'd even say it's highly advisable according to the information expert principle behavior of such nature as resolve() method must belong to the message class. Auxilary class MessageWrapper has a reason for exitance only if the message class cant be changed.

Quick side note: if you can change the message class consider improving property-names.

The first step - Elimination of duplicates .

It looks like this: Stream is being mapped into a Stream and then processed with a custom collector, based on HashMap. The finisher function of the collector transforms HashMap into the Collection of MessageWrapper objects.

the actual code you need

    private static Collection<MessageWrapper> getUniqueWrapped(List<Message> messageList) {
        return messageList.stream()
                .map(MessageWrapper::new)
                .collect(Collector.of(
                        HashMap<String, MessageWrapper>::new,
                        (map, next) -> map.merge(next.getKey(), next, MessageWrapper::resolve),
                        (map1, map2) -> combine(map1, map2), // could be replaced with a method reference _YourEnclosingClass_::combine
                        Map::values));
    }

Multiline lambdas spoil the readability of code. Hence this BinaryOperator intended to merge two maps containing partial results represented by a separate method.

    private static HashMap<String, MessageWrapper> combine(HashMap<String, MessageWrapper> map1,
                                                            HashMap<String, MessageWrapper> map2) {
        for (String key: map2.keySet()) {
            map1.merge(key, map2.get(key), MessageWrapper::resolve);
        }
        return map1;
    }

The second step - Sorting .

In your code, you used OrdreCalcule field for this purpose. I've intentionally written a comparator based on Ordre and SentDate to show that it's concise and transparent and doesn't require introducing a special field.

    public static List<Message> getUniqueMessages(List<Message> messageList) {
        return getUniqueWrapped(messageList).stream()
                .map(MessageWrapper::getMessage)
                .sorted(Comparator.comparing(Message::getOrdre)
                        .thenComparing(Message::getSentDate))
                .collect(Collectors.toList());
    }

main

A couple of dummy messages to prove that this implementation works as intended. By the way, you mentioned Date class - it's a legacy class, consider replacing it with LocalDate or LocalDateTime.

    public static void main(String[] args) {
        List<Message> messageList =
                List.of(new Message(123, "Foo", LocalDate.now()), new Message(123, "Bar", LocalDate.now())
                        , (new Message(123, "Foo", LocalDate.now().minusYears(1).plusMonths(3))), new Message(123, "Bar", LocalDate.now().plusDays(3))
                        , new Message(999, "FooBar", LocalDate.now()));

        System.out.println("initial messageList:");
        messageList.forEach(message -> System.out.println("\t" + message));

        System.out.println("\nprocessed messageList:");
        getUniqueMessages(messageList).forEach(message -> System.out.println("\t" + message));
    }

output

    initial messageList:
        Message {123, 28/01/2022, Foo}
        Message {123, 28/01/2022, Bar}
        Message {123, 28/04/2021, Foo}
        Message {123, 31/01/2022, Bar}
        Message {999, 28/01/2022, FooBar}
    
    processed messageList:
        Message {123, 28/01/2022, Foo}
        Message {123, 31/01/2022, Bar}
        Message {999, 28/01/2022, FooBar}

I hope it'll be helpful. If you have any questions fill free to ask.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM