简体   繁体   中英

How to group values from a list with Java Stream API (groupingBy collector)?

I have list of Entry objects. Entry is a:

class Entry {
   private final Date date;
   private final String value;

   // constructor
   // getters
}

I need to group these entries by day . For example,

2011-03-21 09:00 VALUE1
2011-03-21 09:00 VALUE2
2011-03-22 14:00 VALUE3
2011-03-22 16:00 VALUE4
2011-03-21 16:00 VALUE5

Should be grouped:

2011-03-21
    VALUE1
    VALUE2
    VALUE5

2011-03-22
    VALUE3
    VALUE4

I want to get a Map<Date, List<Entry>> . How can I get this using the Stream API (groupingBy collector)?

My attempt below:

final Map<Date, List<Entry>> entries =
        list.stream().collect(Collectors.groupingBy(request -> {
        final Calendar ogirinal = Calendar.getInstance();
        ogirinal.setTime(request.getDate());

        final Calendar cal = Calendar.getInstance();
        cal.set(Calendar.DAY_OF_MONTH, ogirinal.get(Calendar.DAY_OF_MONTH));
        cal.set(Calendar.MONTH, ogirinal.get(Calendar.MONTH));
        cal.set(Calendar.YEAR, ogirinal.get(Calendar.YEAR));

        return cal.getTime();
    }));

Output:

2011-03-21
    VALUE1
2011-03-21
    VALUE2
2011-03-22
    VALUE3
    VALUE4
2011-03-21
    VALUE5

You have to truncate the date values, as they may be different up to the millisecond:

import static java.time.temporal.ChronoUnit.DAYS;

final Map<Instant, List<Entry>> entries =
    list.stream().collect(Collectors.groupingBy(request -> 
        request.getDate().toInstant().truncatedTo(DAYS)));

Again don't understand why you are using java.util.Date when you can use LocalDateTime for example. But here goes my attempt:

Map<Date, List<Entry>> entries = list.stream().collect(Collectors.groupingBy(e ->
    // easier way to truncate the date
    Date.from(e.getDate().toInstant().truncatedTo(ChronoUnit.DAYS)))
);

DEMO

Your approach is pretty much the right way to go, but your entries end up in different groups because the Calendar objects you return all have slightly different timestamps. This is because you do not set hour/minute/... to 0. Only when two of the calendars have the same time by coincidence (due to timer inaccuracies for example) two entries will end up in the same group.

Use something like this to group instead:

LocalDate.fromDateFields(request.getDate());

LocalDate makes creating date-only timestamps much easier than Calendar . This snippet uses joda time's LocalDate , but Java's own is only slightly longer.

If you want to group by date (day, month and year), you can discard the time (hour, minutes, seconds). As you're using Java 8, just convert the java.util.Date to a java.time.LocalDate :

Map<LocalDate, List<Entry>> entries = list.stream()
    .collect(Collectors.groupingBy(request ->
         request.getDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()));

Just some background: a Date represents a specific point in time, the number of milliseconds since Unix epoch , while a LocalDate represents only a day/month/year date, without any notion of timezone.

To properly convert a Date to a LocalDate , I set it to the JVM default timezone (using toInstant().atZone() ) and then get only the local part ( toLocalDate() ).

You could also make your design simpler, changing the Entry class to have a LocalDate field, if possible. You're using Java 8, and unless you have a good reason to use the old API ("legacy code", "my boss doesn't want", etc), it's better to start using java.time stuff.

You are almost there. You are facing the issue because, while initiating the Calendar cal = Calendar.getInstance() object, the minutes, seconds, hours, and all other fields are also getting set which is causing trouble in the grouping. (The two timestamps aren't actually same for them to be grouped together.)

You need to clear all the other fields except the ones that you are setting so that they are essentially same.

You need to set all other fields except DAY_OF_MONTH , MONTH , YEAR to 0. Use Calender.clear() for the same.

final Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(Calendar.DAY_OF_MONTH, ogirinal.get(Calendar.DAY_OF_MONTH));
cal.set(Calendar.MONTH, ogirinal.get(Calendar.MONTH));
cal.set(Calendar.YEAR, ogirinal.get(Calendar.YEAR));

This should clear the problem.

There are many good and correct answers already. Still I should like to contribute my take. Take the full step into using java.time, the modern Java date and time API, by declaring your date field for example a LocalDateTime . You will probably want to change its name at the same time:

private final LocalDateTime dateTime;

Fit the Entry class with a convenience method for getting the date only:

public LocalDate getDateWithoutTimeOfDay() {
    return dateTime.toLocalDate();
}

Now the rest is really simple:

    final Map<LocalDate, List<Entry>> entries =
            list.stream().collect(Collectors.groupingBy(Entry::getDateWithoutTimeOfDay));

    // print the result
    entries.entrySet().forEach(e -> {
        System.out.println(e.getKey());
        e.getValue().forEach(v -> System.out.println("    " + v.getValue()));
        System.out.println();
    });

This printed:

2011-03-22
    VALUE3
    VALUE4

2011-03-21
    VALUE1
    VALUE2
    VALUE5

Since the map returned from the grouping isn't sorted, there's no guarantee about the order of the dates in the printout, but the grouping is as you desired.

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