Utilisation de flux pour collecter dans TreeSet avec un comparateur personnalisé

92

Travaillant en Java 8, j'ai TreeSetdéfini comme ceci:

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

PositionReport est une classe assez simple définie comme ceci:

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

Cela fonctionne très bien.

Maintenant, je veux supprimer les entrées de TreeSet positionReportstimestampest plus ancienne qu'une certaine valeur. Mais je ne peux pas comprendre la syntaxe Java 8 correcte pour exprimer cela.

Cette tentative compile en fait, mais me donne un nouveau TreeSetavec un comparateur non défini:

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

Comment exprimer ce que je souhaite collecter dans un TreeSetavec un comparateur comme Comparator.comparingLong(PositionReport::getTimestamp)?

J'aurais pensé à quelque chose comme

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

Mais cela ne compile pas / ne semble pas être une syntaxe valide pour les références de méthode.

tbsalling
la source

Réponses:

118

Les références de méthode s'appliquent lorsque vous avez une méthode (ou un constructeur) qui correspond déjà à la forme de la cible que vous essayez de satisfaire. Vous ne pouvez pas utiliser une référence de méthode dans ce cas, car la forme que vous ciblez est une Supplierqui ne prend aucun argument et renvoie une collection, mais ce que vous avez est un TreeSetconstructeur qui prend un argument, et vous devez spécifier ce que cet argument est. Vous devez donc adopter l'approche la moins concise et utiliser une expression 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
la source
4
Une chose à noter est que vous n'avez pas besoin du comparateur si le type de votre TreeSet (dans ce cas PositionReport) implémente comparable.
xtrakBandit
35
Suivi sur @xtrakBandit - encore une fois si vous n'avez pas besoin de spécifier le comparateur (tri naturel) - vous pouvez le rendre très concis:.collect(Collectors.toCollection(TreeSet::new));
Joshua Goldberg
J'ai eu cette erreur:toCollection in class Collectors cannot be applied to given types
Bahadir Tasdemir
@BahadirTasdemir Le code fonctionne. Assurez-vous de ne transmettre qu'un seul argument à Collectors::toCollection: a Supplierqui renvoie a Collection. Supplierest un type avec une seule méthode abstraite, ce qui signifie qu'il peut être la cible d'une expression lambda comme dans cette réponse. L'expression lambda ne doit prendre aucun argument (d'où la liste d'arguments vide ()) et retourner une collection avec un type d'élément qui correspond au type des éléments dans le flux que vous collectez (dans ce cas a TreeSet<PositionReport>).
gdejohn
15

C'est facile, utilisez simplement le code suivant:

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

Vous pouvez simplement convertir en un SortedSet à la fin (à condition que la copie supplémentaire ne vous dérange pas).

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

return new TreeSet(positionReports);
Daniel Scott
la source
7
Vous devez faire attention en faisant cela. Vous pourriez perdre des éléments en faisant cela. Comme dans la question posée ci-dessus, le comparateur naturel des éléments est différent de celui que l'OP souhaite utiliser. Donc vous dans la conversion initiale, puisqu'il s'agit d'un ensemble, il pourrait perdre certains éléments que l'autre comparateur pourrait ne pas avoir (c'est-à-dire que le premier comparateur pourrait renvoyer compareTo() comme 0 tandis que l'autre pourrait ne pas avoir pour certaines comparaisons. Tous ceux où compareTo()vaut 0 est perdu car il s'agit d'un ensemble.)
looneyGod
6

Il existe une méthode de collecte pour cela sans avoir à utiliser les flux: default boolean removeIf(Predicate<? super E> filter). Voir Javadoc .

Votre code pourrait donc ressembler à ceci:

positionReports.removeIf(p -> p.timestamp < oldestKept);
Michael Damone
la source
1

Le problème avec TreeSet est que le comparateur que nous voulons pour trier les éléments est également utilisé pour détecter les doublons lors de l'insertion d'éléments dans l'ensemble. Donc, si la fonction de comparaison est 0 pour deux éléments, elle en écarte à tort un en la considérant comme dupliquée.

La détection des doublons doit être effectuée par une méthode de hashCode correcte distincte des éléments. Je préfère utiliser un simple HashSet pour éviter les doublons avec un hashCode en tenant compte de toutes les propriétés (id et nom dans l'exemple) et renvoyer une simple liste triée lors de l'obtention des éléments (tri uniquement par nom dans l'exemple):

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
la source
1
positionReports = positionReports.stream()
                             .filter(p -> p.getTimeStamp() >= oldestKept)
                             .collect(Collectors.toCollection(() -> new 
TreeSet<PositionReport>(Comparator.comparingLong(PositionReport::getTimestamp))));
Cyril Sojan
la source