根据不同的上下文区分不同字段的对象

时间:2018-04-02 14:20:37

标签: java java-8 unique equals equality

假设有这样的不可变类:

public class Foo {
    private final Long id;
    private final String name;
    private final LocalDate date;

    public Foo(Long id, String name, LocalDate date) {
        this.id = id;
        this.name = name;
        this.date = date;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getDate() {
        return date;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Foo foo = (Foo) o;
        return Objects.equals(getId(), foo.getId()) &&
                Objects.equals(getName(), foo.getName()) &&
                Objects.equals(getDate(), foo.getDate());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getName(), getDate());
    }
}

这个类有一组对象。在某些情况下,需要仅通过名称和某些情况下的名称和日期进行区分。

因此,将集合传递给java.util.Set<Foo>或创建java Stream<Foo>调用.distinct()方法对于这种情况不起作用。 我知道可以使用TreeSetComparator进行区分。它看起来像这样:

private Set<Foo> distinct(List<Foo> foos, Comparator<Foo> comparator) {
    TreeSet<Foo> treeSet = new TreeSet<>(comparator);
    treeSet.addAll(foos);
    return treeSet;
}

用法:

distinct(foos, Comparator.comparing(Foo::getName)); // distinct by name
distinct(foos, Comparator.comparing(Foo::getName).thenComparing(Foo::getDate)); // distinct by name and date

但我认为这不是一个好方法。 什么是解决这个问题最优雅的方法?

1 个答案:

答案 0 :(得分:3)

首先,让我们考虑一下您当前的方法,然后我会展示一个更好的选择。

您当前的方法很简洁,但只需TreeSet即可使用TreeMap。如果您对O(nlogn)的红/黑树结构强加的TreeMap复杂性感到满意,我只会将您当前的代码更改为:

public static <T> Set<T> distinct(
        Collection<? extends T> list, 
        Comparator<? super T> comparator) {

    Set<T> set = new TreeSet<>(comparator);
    set.addAll(list);
    return set;
}

请注意,我已经使您的方法具有通用性和静态性,因此无论其元素的类型如何,它都可以以通用方式用于任何集合。我还将第一个参数更改为Collection,以便它可以与更多数据结构一起使用。

此外,TreeSet仍有O(nlogn)时间复杂度,因为它使用TreeMap作为其支持结构。

TreeSet的使用有三个缺点:首先,它根据传递的Comparator对你的元素进行排序(也许你不需要这个);第二,时间复杂度为O(nlogn)(如果您需要的是具有不同的元素,则可能太多了);第三,它返回Set(可能不是调用者需要的集合类型)。

所以,这是另一种返回Stream的方法,然后您可以将其收集到所需的数据结构中:

public static <T> Stream<T> distinctBy(
        Collection<? extends T> list, 
        Function<? super T, ?>... extractors) {

    Map<List<Object>, T> map = new LinkedHashMap<>();  // preserves insertion order
    list.forEach(e -> {
        List<Object> key = new ArrayList<>();
        Arrays.asList(extractors)
                .forEach(f -> key.add(f.apply(e)));    // builds key
        map.merge(key, e, (oldVal, newVal) -> oldVal); // keeps old value
    });
    return map.values().stream();
}

根据作为varargs参数传递的提取器函数,将传递的集合的每个元素转换为对象列表。

然后,使用此键将每个元素放入LinkedHashMap并通过保留最初放置的值进行合并(根据您的需要进行更改)。

最后,从地图的值返回一个流,以便调用者可以用它做任何事情。

注意:这种方法要求提取器函数返回的所有对象一致地实现equalshashCode方法,以便由它们形成的列表可以安全地用作地图的关键。

用法:

List<Foo> result1 = distinctBy(foos, Foo::getName)
    .collect(Collectors.toList());

Set<Foo> result2 = distinctBy(foos, Foo::getName, Foo::getDate)
    .collect(Collectors.toSet());