Usando streams para coletar em TreeSet com comparador personalizado

93

Trabalhando em Java 8, tenho um TreeSetdefinido assim:

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

PositionReport é uma classe bastante simples definida assim:

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

Isso funciona bem.

Agora quero remover as entradas de TreeSet positionReportsonde timestampé mais antigo que algum valor. Mas não consigo descobrir a sintaxe correta do Java 8 para expressar isso.

Essa tentativa realmente compila, mas me dá uma novidade TreeSetcom um comparador indefinido:

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

Como posso expressar que desejo coletar em um TreeSetcom um comparador como Comparator.comparingLong(PositionReport::getTimestamp)?

Eu teria pensado algo como

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

Mas isso não compila / parece ser uma sintaxe válida para referências de método.

tbsalling
fonte

Respostas:

120

As referências de método são para quando você tem um método (ou construtor) que já se ajusta à forma do alvo que você está tentando satisfazer. Você não pode usar uma referência de método neste caso porque a forma que você está almejando é um Supplierque não aceita argumentos e retorna uma coleção, mas o que você tem é um TreeSetconstrutor que aceita um argumento e você precisa especificar qual é esse argumento é. Portanto, você deve adotar uma abordagem menos concisa e usar uma expressão 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
fonte
4
Uma coisa a observar é que você não precisa do comparador se o tipo de seu TreeSet (neste caso, PositionReport) implementa comparável.
xtrakBandit
35
Seguindo @xtrakBandit - novamente se você não precisar especificar o comparador (classificação natural) - você pode torná-lo muito conciso:.collect(Collectors.toCollection(TreeSet::new));
Joshua Goldberg
Recebi este erro:toCollection in class Collectors cannot be applied to given types
Bahadir Tasdemir
1
@BahadirTasdemir O código funciona. Certifique-se de passar apenas um argumento para Collectors::toCollection: a Supplierque retorna a Collection. Supplieré um tipo com apenas um único método abstrato, o que significa que pode ser o destino de uma expressão lambda como nesta resposta. A expressão lambda não deve ter argumentos (daí a lista de argumentos vazia ()) e retornar uma coleção com um tipo de elemento que corresponda ao tipo dos elementos no fluxo que você está coletando (neste caso, a TreeSet<PositionReport>).
gdejohn
15

Isso é fácil, basta usar o próximo código:

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

Você pode simplesmente converter em um SortedSet no final (desde que você não se importe com a cópia adicional).

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

return new TreeSet(positionReports);
Daniel Scott
fonte
7
Você deve ter cuidado ao fazer isso. Você PODE perder elementos ao fazer isso. Como na pergunta feita acima, o comparador natural dos elementos é diferente daquele que OP deseja usar. Então você na conversão inicial, por ser um conjunto, pode perder alguns elementos que o outro comparador pode não ter (ou seja, o primeiro comparador pode retornar compareTo() como 0, enquanto o outro pode não retornar para algumas comparações. Todos aqueles em que compareTo()é 0 está perdido, pois este é um conjunto.)
looneyGod
6

Existe um método de coleção para este sem ter que usar correntes: default boolean removeIf(Predicate<? super E> filter). Veja Javadoc .

Portanto, seu código pode ter a seguinte aparência:

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

O problema com TreeSet é que o comparador que queremos para classificar os itens também é usado para detectar duplicatas ao inserir itens no conjunto. Portanto, se a função comparadora for 0 para dois itens, ela descartará um, considerando-o como duplicado.

A detecção de duplicatas deve ser feita por um método hashCode correto separado dos itens. Eu prefiro usar um HashSet simples para evitar duplicatas com um hashCode considerando todas as propriedades (id e nome no exemplo) e retornar uma lista ordenada simples ao obter os itens (classificação apenas por nome no exemplo):

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