Using streams to collect into TreeSet with custom comparator
JavaCollectionsJava 8Java Problem Overview
Working in Java 8, I have a TreeSet
defined like this:
private TreeSet<PositionReport> positionReports =
new TreeSet<>(Comparator.comparingLong(PositionReport::getTimestamp));
PositionReport
is a rather simple class defined like this:
public static final class PositionReport implements Cloneable {
private final long timestamp;
private final Position position;
public static PositionReport create(long timestamp, Position position) {
return new PositionReport(timestamp, position);
}
private PositionReport(long timestamp, Position position) {
this.timestamp = timestamp;
this.position = position;
}
public long getTimestamp() {
return timestamp;
}
public Position getPosition() {
return position;
}
}
This works fine.
Now I want to remove entries from the TreeSet positionReports
where timestamp
is older than some value. But I cannot figure out the correct Java 8 syntax to express this.
This attempt actually compiles, but gives me a new TreeSet
with an undefined comparator:
positionReports = positionReports
.stream()
.filter(p -> p.timestamp >= oldestKept)
.collect(Collectors.toCollection(TreeSet::new))
How do I express, that I want to collect into a TreeSet
with a comparator like Comparator.comparingLong(PositionReport::getTimestamp)
?
I would have thought something like
positionReports = positionReports
.stream()
.filter(p -> p.timestamp >= oldestKept)
.collect(
Collectors.toCollection(
TreeSet::TreeSet(Comparator.comparingLong(PositionReport::getTimestamp))
)
);
But this does not compile / appear to be valid syntax for method references.
Java Solutions
Solution 1 - Java
Method references can be used when you have a method (or constructor) that fits the shape of the target you're trying to satisfy. You can't use a method reference in this case because the shape you're targeting is a Supplier
, which takes no arguments, but what you have is a TreeSet
constructor, which does take an argument, and you need to specify what that argument is. So you have to take the less concise approach and use a lambda expression:
TreeSet<Report> toTreeSet(Collection<Report> reports, long timestamp) {
return reports.stream().filter(report -> report.timestamp() >= timestamp).collect(
Collectors.toCollection(
() -> new TreeSet<>(Comparator.comparingLong(Report::timestamp))
)
);
}
Solution 2 - Java
This is easy just use next code:
positionReports = positionReports
.stream()
.filter(p -> p.timestamp >= oldestKept)
.collect(
Collectors.toCollection(()->new TreeSet<>(Comparator.comparingLong(PositionReport::getTimestamp)
)));
Solution 3 - Java
You can just convert into a SortedSet at the end (provided that you don't mind the additional copy).
positionReports = positionReports
.stream()
.filter(p -> p.getTimeStamp() >= oldestKept)
.collect(Collectors.toSet());
return new TreeSet(positionReports);
Solution 4 - Java
There is a method on Collection for this without having to use streams: default boolean removeIf(Predicate<? super E> filter)
. See Javadoc.
So your code could just look like this:
positionReports.removeIf(p -> p.timestamp < oldestKept);
Solution 5 - Java
The problem with TreeSet is that the comparator that we want for sorting the items is used also for detecting duplicates when inserting items into the set. So if the comparator function is 0 for two items it wrongly discards one considering it as duplicate.
The duplicates detection should be done by a separate correct hashCode method of the items. I prefer to use a simple HashSet to prevent duplicates with a hashCode considering all properties (id and name in the example) and return a simple sorted List when getting the items (sorting only by name in the example):
public class ProductAvailableFiltersDTO {
private Set<FilterItem> category_ids = new HashSet<>();
public List<FilterItem> getCategory_ids() {
return category_ids.stream()
.sorted(Comparator.comparing(FilterItem::getName))
.collect(Collectors.toList());
}
public void setCategory_ids(List<FilterItem> category_ids) {
this.category_ids.clear();
if (CollectionUtils.isNotEmpty(category_ids)) {
this.category_ids.addAll(category_ids);
}
}
}
public class FilterItem {
private String id;
private String name;
public FilterItem(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof FilterItem)) return false;
FilterItem that = (FilterItem) o;
return Objects.equals(getId(), that.getId()) &&
Objects.equals(getName(), that.getName());
}
@Override
public int hashCode() {
return Objects.hash(getId(), getName());
}
}
Solution 6 - Java
positionReports = positionReports.stream()
.filter(p -> p.getTimeStamp() >= oldestKept)
.collect(Collectors.toCollection(() -> new
TreeSet<PositionReport>(Comparator.comparingLong(PositionReport::getTimestamp))));