Używanie strumieni do zbierania do TreeSet z niestandardowym komparatorem

92

Pracując w Javie 8 mam takie TreeSetzdefiniowane:

private TreeSet<PositionReport> positionReports = 
        new TreeSet<>(Comparator.comparingLong(PositionReport::getTimestamp));

PositionReport jest raczej prostą klasą zdefiniowaną w ten sposób:

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;
    }
}

To działa dobrze.

Teraz chcę usunąć wpisy z miejsca, w TreeSet positionReportsktórym timestampjest starszy niż jakaś wartość. Ale nie mogę znaleźć poprawnej składni Java 8, aby to wyrazić.

Ta próba faktycznie się kompiluje, ale daje mi nową TreeSetz niezdefiniowanym komparatorem:

positionReports = positionReports
        .stream()
        .filter(p -> p.timestamp >= oldestKept)
        .collect(Collectors.toCollection(TreeSet::new))

Jak wyrazić, że chcę zebrać do postaci TreeSetz komparatorem Comparator.comparingLong(PositionReport::getTimestamp)?

Pomyślałbym coś takiego

positionReports = positionReports
        .stream()
        .filter(p -> p.timestamp >= oldestKept)
        .collect(
            Collectors.toCollection(
                TreeSet::TreeSet(Comparator.comparingLong(PositionReport::getTimestamp))
            )
        );

Ale to nie kompiluje / nie wydaje się poprawną składnią dla odwołań do metod.

tballing
źródło

Odpowiedzi:

118

Odwołania do metod są używane, gdy masz metodę (lub konstruktor), która już pasuje do kształtu celu, który próbujesz spełnić. W tym przypadku nie możesz użyć odwołania do metody, ponieważ docelowy kształt nie Supplierprzyjmuje argumentów i zwraca kolekcję, ale masz TreeSetkonstruktor, który przyjmuje argument i musisz określić, jaki jest ten argument jest. Musisz więc przyjąć mniej zwięzłe podejście i użyć wyrażenia lambda:

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))
        )
    );
}
gdejohn
źródło
4
Należy zauważyć, że nie potrzebujesz komparatora, jeśli typ Twojego zestawu TreeSet (w tym przypadku PositionReport) jest porównywalny.
xtrakBandit
35
Kontynuując @xtrakBandit - ponownie, jeśli nie musisz określać komparatora (sortowanie naturalne) - możesz uczynić to bardzo zwięzłym:.collect(Collectors.toCollection(TreeSet::new));
Joshua Goldberg
Otrzymałem ten błąd:toCollection in class Collectors cannot be applied to given types
Bahadir Tasdemir
@BahadirTasdemir Kod działa. Upewnij się, że przekazujesz tylko jeden argument do Collectors::toCollection: a, Supplierktóry zwraca a Collection. Supplierjest typem z tylko jedną metodą abstrakcyjną, co oznacza, że ​​może być celem wyrażenia lambda, jak w tej odpowiedzi. Wyrażenie lambda nie może przyjmować żadnych argumentów (stąd pusta lista argumentów ()) i zwracać kolekcję z typem elementu, który pasuje do typu elementów w zbieranym strumieniu (w tym przypadku a TreeSet<PositionReport>).
gdejohn
15

Jest to łatwe, wystarczy użyć następnego kodu:

    positionReports = positionReports
        .stream()
        .filter(p -> p.timestamp >= oldestKept)
        .collect(
            Collectors.toCollection(()->new TreeSet<>(Comparator.comparingLong(PositionReport::getTimestamp)
)));
Владимир Дворник
źródło
9

Możesz po prostu przekonwertować na SortedSet na końcu (pod warunkiem, że nie masz nic przeciwko dodatkowej kopii).

positionReports = positionReports
                .stream()
                .filter(p -> p.getTimeStamp() >= oldestKept)
                .collect(Collectors.toSet());

return new TreeSet(positionReports);
Daniel Scott
źródło
7
Musisz przy tym uważać. Robiąc to, MOŻESZ stracić elementy. Podobnie jak w pytaniu zadanym powyżej, naturalny komparator elementów jest inny niż ten, którego chce użyć PO. Więc w początkowej konwersji, ponieważ jest to zbiór, może stracić niektóre elementy, których inny komparator może nie mieć (tj. Pierwszy komparator może zwrócić compareTo() jako 0, podczas gdy drugi może nie mieć dla niektórych porównań. Wszystkie te, w których compareTo()jest 0 jest stracony, ponieważ jest to zestaw.)
looneyGod
6

Jest to metoda na Collection za to bez konieczności stosowania strumienie: default boolean removeIf(Predicate<? super E> filter). Zobacz Javadoc .

Twój kod może wyglądać tak:

positionReports.removeIf(p -> p.timestamp < oldestKept);
Michael Damone
źródło
1

Problem z TreeSet polega na tym, że komparator, którego potrzebujemy do sortowania elementów, jest również używany do wykrywania duplikatów podczas wstawiania elementów do zestawu. Więc jeśli funkcja komparatora wynosi 0 dla dwóch pozycji, błędnie odrzuca jedną, która uważa ją za duplikat.

Wykrywanie duplikatów powinno odbywać się osobną, poprawną metodą hashCode elementów. Wolę używać prostego HashSet, aby zapobiec duplikatom z hashCode biorąc pod uwagę wszystkie właściwości (identyfikator i nazwę w przykładzie) i zwracać prostą posortowaną listę podczas pobierania elementów (sortowanie tylko według nazwy w przykładzie):

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());
    }
}
Daniel Mora
źródło
1
positionReports = positionReports.stream()
                             .filter(p -> p.getTimeStamp() >= oldestKept)
                             .collect(Collectors.toCollection(() -> new 
TreeSet<PositionReport>(Comparator.comparingLong(PositionReport::getTimestamp))));
Cyril Sojan
źródło