How to combine unlike streams in Java 8

2019-07-18 13:38发布

问题:

I have a Set<DateCount> which I create using the following code. This creates a set of 7 DateCount objects with an initial count of 0, one for each day of the current week starting from the current day.

// Create an initial Set of DateCount objects for the current week with counts of 0
Set<DateCount> dateCounts = IntStream.range(0, DAYS_IN_WEEK)
        .mapToObj(idx -> new DateTime().withTimeAtStartOfDay().plusDays(idx))
        .map(dt -> new DateCount(dt.toDate(), 0L))
        .collect(toSet());

I have a List<Object[]> which is returned from a database repository. Each Object[] in the list has a BigDecimal at index 0 and a Long at index 1. The BigDecimal is actually a date, something like 20141001. What I'm wondering is if there is a way I can update the dateCounts set using this data in a Stream fashion. Right now, I'm doing the following which just iterates over the list of object arrays and creates new DateCount objects that are then added to the dateCounts set. The DateCount class has a custom equals() and hashCode() method to ensure the dateCounts set contains only a single DateCount instance for any given date.

data.stream()
        .forEach(obj -> {
            DateTime date = null;
            try {
                date = (sdf == null) ? new DateTime(obj[0]) : new DateTime(sdf.parse(obj[0].toString()));
                dateCounts.add(new DateCount(date.toDate(), (Long) obj[1]));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        });

On a side note, I'm wondering if there's also a way to avoid doing a try/catch in my lambda expression just for parsing a String to a Date.

Update -- This is what I've come up with so far. It doesn't seem natural though to stream over the List<Object[]> within the .map call, not sure if there's a better way. I introduced my own Tuple<X, Y> class because working with an Object[] in streams doesn't work very well given there's no accessor methods. Also in the last 2 lines, I'm creating a TreeSet with a Comparator so that I can sort the data by date. I don't feel that's the best approach, but I tried calling sorted() and making DateCount implement Comparable which seems to work fine, but as soon as the collect(toSet()) method is called, the sorting goes out the window. I assume that's the nature of it being a streamed call. I'm curious if there's a way to sort it though before the collect method call and retain the sort after calling collect.

Set<DateCount> dateCounts = IntStream.range(0, DAYS_IN_WEEK)
        .mapToObj(idx -> new Tuple<>(new DateTime().withTimeAtStartOfDay().plusDays(idx).toDate(), 0L))
        .map(t -> {
            Tuple<String, Long> d = data.stream()
                    .map(arr -> new Tuple<>(arr[0].toString(), (Long) arr[1]))
                    .filter(tuple -> sdf.format(t.getX()).equals(tuple.getX()))
                    .findFirst().orElse(new Tuple<>(sdf.format(t.getX()), 0L));

            return new DateCount(DateTime.parse(d.getX(), DateTimeFormat.forPattern("yyyyMMdd")).toDate(), d.getY());
        })
        .collect(toSet());

TreeSet<DateCount> set = new TreeSet<>((a, b) -> a.compareTo(b));
set.addAll(dateCounts);

回答1:

You can provide a Supplier for the collection. Just replace

.collect(toSet());

with

.collect(toCollection(() -> new TreeSet<>((a, b) -> a.compareTo(b))));

But note, that specifying (a, b) -> a.compareTo(b) for the Comparator implies natural ordering. If your elements implement Comparable, it is unnecessary to provide such a comparator. You can simply use

.collect(toCollection(TreeSet::new));

If DateCount has a compareTo method but does not implement Comparator you could also specify DateCount::compareTo rather than (a, b) -> a.compareTo(b).


Note that in your first mapToObj operation it is unnecessary to wrap the DateTime in a Tuple. You can simply map to DateTime and use that value in the next map operation (as I see only t.getX() there, the second value which is 0L is not used at all).


After all, I’m not sure what you are trying to achieve but I have the strong feeling, that you might have a look at Collectors.groupingBy



回答2:

You can map your database data to a Map<DateTime,Long> for quick look-ups so you only have to process it once. Then as you stream through your Set<DateCount> you can pull in updated values using peek(). Here's an example. I used Date in place of DateTime just to make it easier to code the example.

Instant baseDate = Instant.now();
Date date1 = new Date(baseDate.plus(1, ChronoUnit.DAYS).toEpochMilli());
Date date2 = new Date(baseDate.plus(2, ChronoUnit.DAYS).toEpochMilli());
Date date3 = new Date(baseDate.plus(3, ChronoUnit.DAYS).toEpochMilli());

Set<DateCount> dateCounts = new TreeSet<DateCount>(); // Our mock Set
dateCounts.add(new DateCount(date1, 0L));
dateCounts.add(new DateCount(date2, 0L));
dateCounts.add(new DateCount(date3, 0L));

List<Object[]> data = new ArrayList<Object[]>(); // Our mock database Data      
data.add(new Object[]{date1.toInstant().toEpochMilli(), 5L});
data.add(new Object[]{date2.toInstant().toEpochMilli(), 3L});
data.add(new Object[]{date3.toInstant().toEpochMilli(), 2L});

//Map our data so we only have to process it once, and then we can look it up
Map<Date,Long> mappedData = data.stream()
    .collect(Collectors.toConcurrentMap(k->new Date((long) k[0]), v->(Long)v[1]));

//Update the data using peek as we stream through it
dateCounts.stream()
    .peek(dc->dc.setCount(mappedData.get(dc.getDate())))
    .forEachOrdered(System.out::println);   

When run, here's the output, so that you can see the dateCounts are updated:

DateCount: Thu Dec 25 11:25:56 EST 2014 - 5
DateCount: Fri Dec 26 11:25:56 EST 2014 - 3
DateCount: Sat Dec 27 11:25:56 EST 2014 - 2

And here's the implementation of DateCount I used to make the above code work... I imagine it's not too dissimilar to yours:

public class DateCount implements Comparable<DateCount>{
    private Date date = null;
    private Long count = 0L;

    public DateCount(Date datetime, Long count){
        this.date = datetime;
        this.count = count;
    }

    public void setDateTime(Date date){
        this.date = date;
    }

    public Date getDate(){
        return date;
    }

    public void setCount(long count){
        this.count = count;
    }

    public long getCount(){
        return count;
    }

    @Override
    public int hashCode(){
        return date.hashCode() + 459;
    }

    @Override
    public boolean equals(Object o){
        boolean result = false;
        if (o instanceof DateCount){
            DateCount other = (DateCount) o;
            result = this.compareTo(other) == 0;
        }
        return result;
    }

    @Override
    public int compareTo(DateCount other) {
        if (this.date == null){
            return other.getDate() == null ? 0 : -1;
        } else {
            return date.compareTo(other.getDate());
        }
    }

    @Override
    public String toString(){
        return "DateCount: " + date.toString() + " - " + count;
    }
}