Получить дубликаты объектов с помощью пользовательских Equals - PullRequest
0 голосов
/ 17 апреля 2019

Я создаю плагин на Java и у меня есть класс Listing, где я переопределил метод equals следующим образом:

   @Override
public boolean equals(Object listing) {
    if (listing == null)
        return false;
    if (listing instanceof Listing) {
        Listing l = (Listing) listing;
        return l.id.equals(this.id) &&
                l.getItems().stream().mapToInt(ItemStack::getAmount).sum() == this.getItems().stream().mapToInt(ItemStack::getAmount).sum() &&
                l.getItems().get(0).getType().equals(this.getItems().get(0).getType()) &&
                l.getSeller().equals(this.getSeller()) &&
                l.getPrice().equals(this.getPrice());
    }
    return false;
}

У меня есть кэш списков, но я хочу сопоставить списки с количеством дубликатов другим методом equals (в основном тот же компаратор, но без проверки id). В настоящее время я сделал это, и он работает

   public static Map<Listing, Long> getDuplicateCount(Collection<Listing> listings) {

    return listings.stream().collect(Collectors.groupingBy(e ->

            new Listing(-1,
                    e.getSeller(),
                    e.getItems(),
                    e.getCreatedTime(),
                    e.getPrice(),
                    e.isClaimed(),
                    e.isSold(),
                    e.getBuyer(),
                    e.getSoldDate(),
                    e.isCanceled()
            ), Collectors.counting()
    ));
}

но я хочу сохранить один из идентификаторов, чтобы идентификатор не был равен -1 для всех возвращенных записей (поэтому, если есть две повторяющиеся записи, но с идентификаторами 5 и 10, он просто вернет одну из них в качестве ключа и количество 2 как значение) есть идеи, как это сделать?

Ответы [ 4 ]

2 голосов
/ 24 апреля 2019

Вы можете использовать что-то вроде

public static Map<Listing, Long> getDuplicateCount(Collection<Listing> listings) {
    return listings.stream().collect(
        Collectors.groupingBy(e ->
            new Listing(-1, e.getSeller(), e.getItems(), e.getCreatedTime(),
                e.getPrice(), e.isClaimed(), e.isSold(), e.getBuyer(),
                e.getSoldDate(), e.isCanceled()
            ),
            Collector.of(() -> new Object() {
                Listing oneOfThem;
                long count;
            },
            (o, l) -> { o.oneOfThem = l; o.count++; },
            (o1, o2) -> {
                o1.count += o2.count;
                if(o1.oneOfThem == null) o1.oneOfThem = o2.oneOfThem;
                return o1;
            })))
        .values().stream()
        .collect(Collectors.toMap(o -> o.oneOfThem, o -> o.count));
}

Но мне кажется довольно странным, что equals метод Listing игнорирует несколько свойств и использует сумму свойство элементов содержимого списка для определения равенства.Похоже, что этот equals метод уже приспособлен для конкретной операции группировки, подобной этой, которая не должна использоваться для общего правила равенства.

Вместо этого вы должны определить это специальное правило равенства прямо в этой операции:

public static Map<Listing, Long> getDuplicateCount(Collection<Listing> listings) {
    return listings.stream().collect(
        Collectors.groupingBy(l -> Arrays.asList(l.id,
                l.getItems().stream().mapToInt(ItemStack::getAmount).sum(),
                l.getItems().get(0).getType(), l.getSeller(), l.getPrice()),
            Collector.of(() -> new Object() {
                Listing oneOfThem;
                long count;
            },
            (o, l) -> { o.oneOfThem = l; o.count++; },
            (o1, o2) -> {
                o1.count += o2.count;
                if(o1.oneOfThem == null) o1.oneOfThem = o2.oneOfThem;
                return o1;
            })))
        .values().stream()
        .collect(Collectors.toMap(o -> o.oneOfThem, o -> o.count));
}

Теперь операция совершенно не связана с тем, как Listing реализует равенство, что позволяет классу Listing обеспечить лучшую реализацию равенства общего назначения общего назначения.Или вообще не предоставлять пользовательское равенство, наследуя equals и hashCode от Object, что может быть правильной стратегией для объектов со значимой идентичностью.

1 голос
/ 24 апреля 2019

Возможно, немного проще было бы использовать карту с бизнес-идентификатором в качестве ключа:

Map<List, Integer> counts = new HashMap<>();
listings.forEach(lst -> counts.merge(getBusinessId(lst), 1, Integer::sum));

, где getBusinessId определяется следующим образом (он определяет бизнес-идентификатор только для этого сравнения):

public List getBusinessId(Listing listing) {
    return asList(listing.getItems().stream().mapToInt(ItemStack::getAmount).sum(),
            listing.getItems().get(0).getType(),
            listing.getSeller(),
            listing.getPrice());
}

Выглядит легче, чем потоки с groupingBy и т. Д.

1 голос
/ 24 апреля 2019

Вы можете добавить этот метод equalsWithoutId() в свой класс Listing, который похож на метод equals(), за исключением поля id:

public class Listing {

    private int id;
    private String field1;
    private String field2;

    public boolean equalsWithoutId(Listing o) {
        if (this == o) return true;
        if (o == null) return false;
        return Objects.equal(field1, o.field1) &&
                Objects.equal(field2, o.field2);
    }

    //equals and hashcode here
}

Тогда ваш метод будет выглядеть следующим образом:

public static Map<Listing, Long> getDuplicateCount(Collection<Listing> listings) {
    Map<Listing, Long> m = new LinkedHashMap<>();
    listings.forEach(listing -> m.entrySet().stream()
            .filter(e -> e.getKey().equalsWithoutId(listing))
            .findAny()
            .ifPresentOrElse(e -> e.setValue(e.getValue() + 1),
                    () -> m.put(listing, 1L)));
    return m;
}

Обратите внимание, что ifPresentOrElse был представлен в Java 9, поэтому, если вы работаете в Java 8, вы можете использовать:

    listings.forEach(listing -> {
        Optional<Map.Entry<Listing, Long>> entry = m.entrySet().stream()
                .filter(e -> e.getKey().equalsWithoutId(listing))
                .findAny();
        if (entry.isPresent()) entry.get().setValue(entry.get().getValue() + 1);
        else m.put(listing, 1L);
    });

Производительность будет порядкаиз O (n ^ 2), но учитывая ваши ограничения с моим другим ответом, я думаю, что это должно работать для вас.

0 голосов
/ 18 апреля 2019

Не лучшим образом, выглядит не очень аккуратно, но поскольку других ответов нет, я выложу то, что пришло мне в голову:

Резюме

public class ParentListing {
  //all fields here except 'id'
  //constructors, getters, setters, toString, equals, hashcode
}

public class Listing extends ParentListing {

  private int id;

  public ParentListing getParent() {
    //not too happy creating a new object here, perhaps there is a better way
    return new ParentListing(getField1(), getField2());
  }

  //constructors, getters, setters, toString, equals, hashcode
}

и используйте его следующим образом:

public static void main(String[] args) {
    List<Listing> list = List.of(
        new Listing(1, "f1", "f2"),
        new Listing(2, "f10", "f20"),
        new Listing(3, "f1", "f2"));

    //Following map maps listings (without id) to the list of IDs.
    //You can probably stop here and use it as it is. Or else see the next map.
    Map<ParentListing, Set<Integer>> collect = list.stream()
        .collect(Collectors.groupingBy(
            Listing::getParent,
            Collectors.mapping(Listing::getId, Collectors.toSet())));

    Map<Listing, Integer> map = collect.entrySet().stream()
        .map(e -> Map.entry(
            new Listing(e.getValue().stream().findFirst().get(), e.getKey()),
            e.getValue().size()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

    System.out.println(map);
}

Выход

{Listing {id = 2, field1 = f10, field2 = f20} = 1, Листинг {id = 1, field1 = f1, field2 = f2} = 2}

Полный код

@Getter
@Setter
public class ParentListing {
  private String field1;
  private String field2;

  public ParentListing(String field1, String field2) {
    this.field1 = field1;
    this.field2 = field2;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    ParentListing that = (ParentListing) o;
    return Objects.equal(field1, that.field1) &&
        Objects.equal(field2, that.field2);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(field1, field2);
  }

  @Override
  public String toString() {
    return "ParentListing{" +
        "field1='" + field1 + '\'' +
        ", field2='" + field2 + '\'' +
        '}';
  }
}
@Getter
@Setter
public class Listing extends ParentListing {

  private int id;

  public Listing(int id, String field1, String field2) {
    super(field1, field2);
    this.id = id;
  }

  public Listing(int id, ParentListing parentListing) {
    this(id, parentListing.getField1(), parentListing.getField2());
  }

  public ParentListing getParent() {
    return new ParentListing(getField1(), getField2());
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;
    Listing listing = (Listing) o;
    return id == listing.id;
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(super.hashCode(), id);
  }

  @Override
  public String toString() {
    return "Listing{" +
        "id=" + id +
        ", field1=" + getField1() +
        ", field2=" + getField2() +
        "} ";
  }
}
...