Подтвердить что ты не робот

Java равна, когда равенство невозможно определить

Мне интересно, какова правильная парадигма программирования Java для переопределения методов equalshashCode) объекта класса C в случаях, когда либо (a) недостаточно информации, чтобы определить, являются ли два экземпляра C равны или (b) метод вызова не должен определять, являются ли два экземпляра C равными.

В моем проекте, например, у меня есть класс PlayingCard. Мне кажется, что если a PlayingCard обращен лицом вверх, то вызывающие методы должны иметь доступ к его свойствам, но если они обращены вниз, то эти свойства должны оставаться неизвестными:

class PlayingCard {
    private Rank rank;
    private Suit suit;
    private boolean isFaceDown;

    public PlayingCard(Rank rank, Suit suit, boolean isFaceDown) {
        this.rank = rank;
        this.suit = suit;
        this.isFaceDown = isFaceDown;
    }

    public Rank getRank() { return isFaceDown ? null : rank; }

    public Suit getSuit() { return isFaceDown ? null : suit; }

Похоже, что ради Рамки коллекций Java две игровые карты должны быть равны, если они имеют одинаковый ранг и костюм:

    public boolean equals(Object obj) {       // attempt #1
        if(this == obj) return true;
        if(obj == null) return false;
        if(!(obj instanceof PlayingCard)) return false;
        PlayingCard other = (PlayingCard) obj;
        if(rank != other.rank) return false;
        if(suit != other.suit) return false;
        return true;
    }
 }

Но это показывает слишком много информации:

class Malicious {

    public Rank determineRankOfFaceDownCard(PlayingCard faceDownCard) {
        Set<PlayingCard> allCards = /* a set of all 52 PlayingCards face up */;
        for(PlayingCard c : allCards) {
            if(c.equals(faceDownCard)) {
                return c.getRank();
            }
        }
        return null;
    }
}

Использование методов getRank и getSuit` тоже не работает:

    public boolean equals(Object obj) {       // attempt #1
        if(this == obj) return true;
        if(obj == null) return false;
        if(!(obj instanceof PlayingCard)) return false;
        PlayingCard other = (PlayingCard) obj;
        if(getRank() != other.getRank()) return false;
        if(getSuit() != other.getSuit()) return false;
        return true;
    }
}

/* outside the PlayingCard class */

Set<PlayingCard> s = new HashSet<PlayingCard>();
s.add(new PlayingCard(Rank.ACE, Suit.SPADES, true));
s.contains(new PlayingCard(Rank.TWO, Rank.HEARTS, true)); // returns true

Как другие разработчики справились с этой ситуацией? Является ли это ситуацией, когда бросать какой-то RuntimeException вариант? Спасибо за любой вклад и идеи.

4b9b3361

Ответ 1

Вы можете добавить это условие в метод equals:

if(this.isFaceDown || other.isFaceDown) return false;

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

Ответ 2

Я не уверен, что у меня что-то не хватает, но, может быть, должна быть какая-то логика вне класса с вашим компаратором, который не будет сравнивать карты, если их не видно?

Ответ 3

Я не уверен, что полиморфизм - лучший ответ здесь. Независимо от того, карта ли лицевой стороной вверх или лицом вниз, карта не меняется, это СОСТОЯНИЕ карты. Это вопрос расположения логики. Если ваше приложение знает, что карта лицевой стороной вниз, почему бы ему даже побеспокоиться о равенстве и, следовательно, нет необходимости проводить различие между лицом вверх equals() и лицом вниз equals().

Чтобы подробно рассказать о некоторых предложениях в других ответах здесь, могут быть способы идентифицировать разные коллекции, где один находится лицом вниз (например, колода для рисования) или лицом вверх (например, куча сбрасывания), но есть много ситуаций, которые OP не указал, где идея стека менее полезна. В стандартном solitare вы можете использовать любую из трех комбинаций: все лицо вниз (предположительно, игрок собирается перевернуть верхнюю карту), все лицо вверх или комбинация лицевого спуска на дне с одним или несколькими лицом вверх карты сверху. В Блэкджеке размер стека (карт в игре) всегда один с игроками, обычно имеющими все лицевые карты, а у дилера - один лицевой стороной вниз и один или несколько лиц вверх.

Фокусировка на методе .equals() несколько напоминает преждевременную оптимизацию. Сделайте резервную копию шага и подумайте о домене. У вас есть Card с атрибутами Suite и Rank. A Deck - это всего лишь набор из Card (s), и вся сложность действительно принадлежит той игре, в которой используется колода. Возможно, в Blackjack вы определили бы Hand, имеющий несколько атрибутов, прежде всего являющийся коллекцией Card, где у вас могла бы быть логика, определяющая значение точки руки, будь то "мягкая" или "выбитая", и т.д. При сравнении двух рук вы не сравниваете отдельные карты, а конечное значение Руки, которое в случае дилеров действует даже в то время, когда у них есть лицевая карточка.

Злоумышленный сценарий в вопросе кажется сомнительным в том, что я должен был спросить, какая система запускает игру? Если это многоклиентская система, это может быть серьезной проблемой, но ответ будет заключаться в том, чтобы не дать каждому клиенту состояние всех карт. Типичная архитектура для предотвращения такого типа мошенничества будет заключаться в том, что нейтральный VM/сервер будет отвечать за состояние игры и оценивать выигрыш и проигрыш. Если игра не является мультиклиентом, то, несмотря на то, что процессор, работающий в игре, имеет полный доступ к игровым данным, есть только один игрок, который затронут (владелец процессора) и пытается предотвратить вредоносное вмешательство, имеет долгую историю отказ - прочитайте о схемах защиты от копирования, используемых для игр за последние 30 лет, - и вредоносный сценарий снова кажется слишком редким/параноидальным, чтобы попытаться создать сложное дизайнерское решение.

Во всяком случае, так я подхожу к проблеме.

Ответ 4

Этот вопрос кажется, что у него есть несколько разных слоев...

Но то, что я чувствую, что вы кипятите вниз, состоит в том, что, возможно, для карт нужны два класса в зависимости от того, находятся ли они лицом вверх или лицом вниз.

При разработке классов OO полезно логически мыслить, независимо от того, обрабатываем ли мы карточку по-разному, если она находится лицом вниз или лицом вниз.

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

Ответ 5

Объекты должны быть равны друг другу. Когда вы считаете, что объекты могут просматриваться на разных уровнях иерархии классов

Таким образом, Deck, который не поддерживает карты вверх или вниз, может быть равен другому подклассу Deck, который поддерживает карты вверх или вниз, при условии, что это equal в соответствии с Deck политика для определения равенства.

Однако PlayableDeck, поддерживающий карты вверх или вниз, никогда не может быть equal до Deck, который не поддерживает позицию карты; потому что если бы это было так, то два были бы эквивалентны (заменяемы), эффективно разрушая способность PlayableDeck знать положение карты.

Не забудьте применить правила для равенства с подклассами и суперклассами.

  • A = A
  • если A = B, тогда B должен = A.
  • если A = B и B = C, то A должно = C.

Вначале правило 2, по-видимому, означает, что a Deck и a PlayableDeck не могут быть ни при каких обстоятельствах не равны, но это не так. Если вы решили просмотреть PlayableDeck как Deck, тогда

if Deck.equals((Deck)PlayableDeck); then ((Deck)PlayableDeck).equals(Deck)

все равно будет работать отлично. Однако без постоянной кастинга метод equals, который должен быть неудачным, - это метод PlayableDeck.

PlayableDeck.equals(PlayableDeck) // might fail, depending on state
PlayableDeck.equals(Deck) // always fails

Почему это так хорошо работает, привязано к polymorphisim, что гарантирует, что тот факт, что реализация базового класса проверена перед попыткой реализации суперкласса.

Ответ 6

Я бы использовал другой метод равенства, возможно, назвав его matches, чтобы реализовать логику игры "неизвестных свойств", но реализовать метод equals в обычном режиме, чтобы другие классы, такие как коллекции, все еще нормально функционируют. Таким образом, вы можете иметь коллекцию объектов PlayingCard и можете гарантировать, что у вас нет двух тузов пик, например, в вашей колоде, независимо от того, знает ли игрок значение этих карт.

Итак, например:

abstract class PlayingCard {
    protected Rank rank;
    protected Suit suit;

    public PlayingCard(Rank rank, Suit suit) {
        this.rank = rank;
        this.suit = suit;
    }

    public abstract Rank getRank();
    public abstract Suit getSuit();
    public abstract boolean isComparableWith(PlayingCard other);
    public abstract boolean matches(PlayingCard other);

    @Override public boolean equals(Object obj) {
        boolean isEqual = false;
        if (obj == null || !(obj instanceof PlayingCard)) {
            isEqual = false;
        } else if (obj == this) {
            isEqual = true;
        } else {
            PlayingCard other = (PlayingCard) obj;
            isEqual = (other.rank.equals(rank)) && (other.suit.equals(suit));
        }
        return isEqual;
    }
}

class FaceUpPlayingCard extends PlayingCard {
    public FaceUpPlayingCard(Rank rank, Suit suit) {
        super(rank, suit);
    }
    public boolean isComparableWith(PlayingCard other)  {
        return other instanceof FaceUpPlayingCard;
    }
    public boolean matches(PlayingCard other) {
        return isComparableWith(other) && equals(other);
    }
    public Rank getRank() { return rank; }
    public Suit getSuit() { return suit; }
}

class FaceDownPlayingCard extends PlayingCard {
    public FaceDownPlayingCard(Rank rank, Suit suit) {
        super(rank, suit);
    }
    public boolean isComparableWith(PlayingCard other)  {
        return false;
    }
    public boolean matches(PlayingCard other) {
        return false;
    }
    public Rank getRand() { return null; }
    public Suit getSuit() { return null; }
}

Таким образом, когда у вас есть коллекция, она может выполнять сортировку и другую встроенную проверку на основе метода equals, который проверяет свойства независимо от состояния карты (вверх или вниз). Когда вам нужно реализовать логику игры, вы должны использовать метод matches - это проверяет, стоят ли обе карты лицевой стороной вверх и, если они есть, затем проверяет ранг и костюм.