Триады не появляются, чтобы сражаться? (Java Set отсутствует элемент)

У меня есть код от двух компаний asoft и bsoft. Я тоже не могу изменить. Это упрощенная версия моей ситуации, в которой я уверен, достаточно информации для поиска того, что вызывает проблему.

bsoft предоставляет IGang, который представляет собой банду, которая может сражаться с другими бандами.

package bsoft;

public interface IGang {
    /** @return negative, 0, or positive, respectively
     *          if this gang is weaker than, equal to, or stronger
     *          than the other
     */
    public int compareTo(IGang g);
    public int getStrength();
    public String getName();
    public void attack(IGang g);
    public void weaken(int amount);
}

asoft предоставляет GangWar, что позволяет IGang сражаться:

package asoft;
import java.util.*;
import bsoft.*;
/** An `IGang` ordered by identity (name) */
public interface ComparableGang extends IGang, Comparable<IGang> {}

package asoft;
import java.util.*;

public class GangWar {
    public final Set<ComparableGang> gangs = new TreeSet<ComparableGang>();
    public void add(ComparableGang g) {gangs.add(g);}
    public void doBattle() {
        while (gangs.size() > 1) {
          Iterator<ComparableGang> i = gangs.iterator();
          ComparableGang g1 = i.next();
          ComparableGang g2 = i.next();
          System.out.println(g1.getName() + " attacks " + g2.getName());
          g1.attack(g2);
          if (g2.getStrength() == 0) {
              System.out.println(g1.getName() + " smokes " + g2.getName());
              gangs.remove(g2);
          }
          if (g1.getStrength() == 0) {
              System.out.println(g2.getName() + " repels " + g1.getName());
              gangs.remove(g1);
          }
        }
        for (ComparableGang g : gangs) {
            System.out.println(g.getName() + " now controls the turf!");
        }
    }
}

Для этого требуется дополнительное ограничение, которое Gang, которое вы ему поставляете, Comparable, по-видимому, может сортировать по имени или избегать дубликатов. Каждая банда (в произвольном порядке, порядок порядка, используемый здесь для простоты) атакует другую банду, пока не останется только одна банда (или нет банд, если последние два имеют галстук). Я написал простую реализацию ComparableGang для ее проверки:

import asoft.*;
import bsoft.*;
import java.util.*;

class Gang implements ComparableGang {
    final String name;
    int strength;

    public Gang(String name, int strength) {
        this.name = name;
        this.strength = strength;
    }

    public String getName() {return name;}
    public int getStrength() {return strength;}

    public int compareTo(IGang g) {
        return strength - g.getStrength();
    }

    public void weaken(int amount) {
        if (strength < amount) strength = 0;
        else strength -= amount;
    }

    public void attack(IGang g) {
        int tmp = strength;
        weaken(g.getStrength());
        g.weaken(tmp);

    }

    public boolean equals(Object o) {
      if (!(o instanceof IGang)) return false;
      return name.equals(((IGang)o).getName());
    }
}

class Main {
   public static void main(String[] args) {
       GangWar gw = new GangWar();
       gw.add(new Gang("ballas", 2));
       gw.add(new Gang("grove street", 9));
       gw.add(new Gang("los santos", 8));
       gw.add(new Gang("triads", 9));
       gw.doBattle();
   }
}

Тестирование...

$ java Main
ballas attacks los santos
los santos repels ballas
los santos attacks grove street
grove street repels los santos
grove street now controls the turf!

Проблема в том, что триады не появляются в борьбе. На самом деле, печать gangs.size() в начале doBattle() возвращает 3 вместо 4. Почему? Как это исправить?

Ответ 1

Проблема в том, что триады не появляются в борьбе. Фактически, печать gangs.size() прямо в начале doBattle() возвращает 3 вместо 4. Почему?

Оба triads и grove street имеют силу 9. Поэтому они равны в терминах Gang.compareTo (реализация Comparable). Поэтому разрешено только одно в TreeSet.

Если вы не хотите удалять элементы, дублирующие их порядок сортировки, не используйте TreeSet...

EDIT: описание интерфейса ComparableGang указывает, что ожидалось:

/** An `IGang` ordered by identity (name) */
public interface ComparableGang extends IGang, Comparable<IGang> {}

Ваш метод compareTo не упорядочивает "по идентификатору (имени)" - он упорядочивает по силе. Честно говоря, это очень глупый интерфейс, в первую очередь, поскольку было бы легко создать public class GangNameComparator : Comparator<IGang> класс asoft, а затем предоставить это как компаратор для дерева, если они захотят заказать по имени.

Однако, поскольку они предполагают, что вы должны реализовать сравнение, вам нужно сделать это, как описывает интерфейс:

public int compareTo(IGang g) {
    return name.compareTo(g.getName());
}

Однако... как вы заметили в комментариях (и, как отмечено в ответе Роба), это противоречит условно-декларируемому названию IGang description:

public interface IGang {
    /** @return negative, 0, or positive, respectively
     *          if this gang is weaker than, equal to, or stronger
     *          than the other
     */
    public int compareTo(IGang g);
}

Невозможно реализовать ComparableGang для удовлетворения как собственной документации, так и документации IGang. Это в основном нарушено дизайном, на части asoft.

Любой код должен иметь возможность использовать реализацию IGang, зная только о IGang, и полагаться на реализацию, следующую за контрактом IGang. Однако asoft нарушил это предположение, требуя различного поведения в интерфейсе, расширяющем IGang.

Было бы разумно, если бы они добавили больше требований в ComparableGang, если они не нарушали существующие требования IGang.

Обратите внимание, что это важное различие между С# и Java. В С# две функции в двух разных интерфейсах с одной и той же сигнатурой могут быть объединены в один интерфейс, который наследует оба из них, и два метода остаются четкими и доступными. В Java эти два метода, поскольку они полностью абстрактны и имеют одну и ту же подпись, считаются тот же метод, и класс, реализующий объединенные интерфейсы, имеет только один такой метод. Таким образом, в Java ComparableGang является недопустимым, поскольку он не может иметь реализацию compareTo(), которая удовлетворяет контракту ComparableGang и контракту IGang.

Ответ 2

TL; DR: используйте B) Ниже

Из javadoc для Comparable (и то же самое для Comparator тоже!):

Естественное упорядочение для класса C называется согласованным с равенствами тогда и только тогда, когда e1.compareTo(e2) == 0 имеет то же булево значение, что и e1.equals(e2) для каждого e1 и e2 класса C. Примечание. что null не является экземпляром какого-либо класса, а e.compareTo(null) должен вызывать исключение NullPointerException, даже если e.equals(null) возвращает false.

В вашем случае (упрощенном),

  • equals определяется как равенство name
  • compareTo определяется как сравнение strength

Это не соответствует указанному выше условию:

  • когда два strength равны, но два name отличаются друг от друга
  • когда два name равны, но два strength отличаются (вероятно, это условие избегается вашей логикой приложения)

Ответ

Как исправить?

A) Если ваши требования позволяют вам отсортировать набор по name (в соответствии с комментарием в коде asoft):

 // will only return 0 if the strengths are equal AND the names are equal
 public int compareTo(ComparableGang g) {
     return name.compareTo(g.getName());
 }

B) Если ваши требования заставляют вас иметь набор, отсортированный по strength (и затем name) (в соответствии с комментарием в bsoft-коде).

 // will return 0 if & only if the strengths are equal AND the names are equal
 public int compareTo(ComparableGang g) {
     int result = strength - g.getStrength();
     if (result == 0) result =  name.compareTo(g.getName());
     return result;
 }

 // will return true if & only if the strengths are equal AND the names are equal
 public boolean equals(Object o) {
     if (!(o instanceof ComparableGang)) return false;
     ComparableGang gang2 = (ComparableGang)o;
     return name.equals(gang2.getName()) && strength == gang2.getStrength();
 }

 // For this case, if it illegal to have two gangs of same name but different 
 // strength (it should be illegal!), then app logic must enforce this - the Set 
 // no longer will.

Комментарий 1: если вам нужно изменить класс asoft GangWar, было бы лучше, если бы вы могли разместить выше двух методов B):

 class ComparableGangComparator implements Comparator<ComparableGang> {
 }

а затем измените, как GangWar создает Set:

 public final Set<ComparableGang> gangs = new TreeSet<ComparableGang>(
                                                  new ComparableGangComparator());

Таким образом, вы можете оставить два метода A) внутри класса Gang - оставив класс с ним "true" равно и сравните с идентификатором объекта POV.

Комментарий 2: Противоречивые комментарии к методам asoft и bsoft compareTo

Из теоретического POV: Если комментарий asoft не является опечаткой, то не только имеет расширенный расширенный интерфейс bsoft, но и изменил требуемое поведение одного из методов. На самом деле это не противоречие - это переопределение: asoft comment "выигрывает".

Из практического POV: вам нужно, чтобы ваши пальцы скрестились, так как они сделали это специально, а комментарий правильный. Если это опечатка от asoft, тогда лучший комментарий bsoft лучше, а bsoft "выигрывает". Вы можете отправить запрос в asoft или проверить их документы/примеры для подтверждения.

Ответ 3

Метод Gang.compareTo основан на их сильных сторонах, так как triads и grove street имеют одинаковую силу, TreeSet считает, что они равны, а затем удаляет их.

На основании того, как ComparableGang ожидает их сортировки, я бы сказал, игнорируя запрос интерфейса IGang для поведения compareTo и изменяя его на это.

public int compareTo(IGang g) {
    return name.compareTo(g.getName());
}

Ответ 4

В основном проблема заключается в том, что ComparableGang является IGang, но сортировки по IGangs по силе и ComparableGangs сортируются по имени, поэтому ComparableGang не является IGang.

TL; DR Правильный способ исправить это - исправить интерфейсы и код библиотеки. Обходной путь для написания кода для работы с обеими библиотеками объясняется в этом ответе.


Лучшее решение - исправить оба интерфейса. bsoft нужно изменить только одну строку своего кода:

public interface IGang extends Comparable<IGang> {

Ничто другое в интерфейсе или любом другом коде не нуждается в изменении, но asoft тогда будет уведомлен о том, что IGang уже сопоставимы. (EDIT: С другой стороны, поскольку IGang.compareTo() не соответствует equals(), это корень причин, по которым триады не появляются до боя, bsoft вероятно, сделал правильную вещь, не расширив Comparable. То, что они сделали неправильно, объявляло compareTo() вместо, скажем, compareStrengthTo().)

asoft не нужно ждать, пока bsoft ничего не изменит. Это действительно их вина, чтобы начать с того, что они создали интерфейс с разбитым дизайном. Они должны были просто выбрать способ получить Comparator<IGang>, который сортируется по имени. Поэтому, если бы я работал в asoft, GangWar выглядел бы следующим образом:

public class GangWar {
    public final Set<IGang> gangs;
    public GangWar(Comparator<IGang> gangNameComparator) {
        gangs = new TreeSet<IGang>(gangNameComparator);
    }
    public void add(IGang g) {gangs.add(g);}
    public void doBattle() {
        while (gangs.size() > 1) {
          Iterator<IGang> i = gangs.iterator();
          IGang g1 = i.next();
          IGang g2 = i.next();
          System.out.println(g1.getName() + " attacks " + g2.getName());
          g1.attack(g2);
          if (g2.getStrength() == 0) {
              System.out.println(g1.getName() + " smokes " + g2.getName());
              gangs.remove(g2);
          }
          if (g1.getStrength() == 0) {
              System.out.println(g2.getName() + " repels " + g1.getName());
              gangs.remove(g1);
          }
        }
        for (IGang g : gangs) {
            System.out.println(g.getName() + " now controls the turf!");
        }
    }
}

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

ballas attacks grove street
grove street repels ballas
grove street attacks los santos
los santos repels grove street
los santos attacks triads
triads repels los santos
triads now controls the turf!

Конечно, если вы застряли с библиотеками как есть, вы можете прочитать этот ответ для общего подхода к проживанию с поломкой, не касаясь библиотеки.