12

我想知道在(a)没有足够的信息来确定 C 的两个实例是否相等或(b)调用方法的情况下,覆盖 C 类对象的equals(and ) 方法的正确 Java 编程范式是什么hashCode应该无法确定 C 的两个实例是否相等。

例如,在我的项目中,我有一PlayingCard堂课。在我看来,如果 aPlayingCard面朝上,那么调用方法应该可以访问它的属性,但如果它面朝下,那么这些属性应该保持未知:

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

使用getRankand 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是合适的吗?感谢您的任何意见和想法。

4

6 回答 6

6

您可以在 equals 方法中添加此条件:

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

我认为如果卡片面朝下,这是完全隐藏卡片的唯一方法。问题是,当您将其添加到一组时,如果您添加的卡片面朝下,您可能会有重复。

于 2012-06-19T15:25:38.123 回答
3

我不相信多态性是最好的答案。一张牌是正面朝上还是正面朝下都不会改变这张牌,它是一张牌的状态。这是逻辑的位置问题。如果您的应用程序知道卡片面朝下,为什么还要费心检查是否相等,因此无需区分面朝上equals()和面朝下equals().

为了在此处详细说明其他答案中的一些建议,可能有一些方法可以识别不同的集合,其中一个是面朝下(如平局)或面朝上(如弃牌堆),但在许多情况下 OP 没有指定堆栈的想法在哪里不太有用。在标准纸牌中,您可以有以下三种组合中的任何一种:全部面朝下(假设玩家将要翻转最上面的牌),全部面朝上,或底部面朝下与一个或多个面朝上的混合卡在上面。在二十一点中,一叠(游戏中的牌)的大小始终为一,玩家通常拥有所有面朝上的牌,而庄家则拥有一张面朝下和一张或多张面朝上的牌。

专注于.equals()方法有点像过早的优化。后退一步,想想域。你有一个和Card的属性。A只是(s) 的集合,所有复杂性确实属于使用 Deck 的任何游戏。也许您会定义 a具有多个属性,首先是s 的集合,您可以在其中具有确定手牌点值的逻辑,无论它是“软”还是“破”等。比较两只手时,您不要比较单张牌,而是比较手牌的最终价值,在庄家的情况下,即使在他们有一张面朝下的牌的时候也是有效的。SuiteRankDeckCardBlackjackHandCard

问题中的恶意场景似乎令人怀疑,因为我不得不问,谁的系统在运行游戏?如果它是一个多客户端系统,这可能是一个有效的问题,但答案是不给每个客户端每个人的卡片状态。防止这种作弊的典型架构是让中立的虚拟机/服务器负责游戏状态并评估输赢。如果游戏不是多客户端,那么即使运行游戏的 CPU 可以完全访问游戏数据,也只有一个玩家(CPU 的所有者)受到影响,并且试图防止恶意篡改的历史由来已久。失败 - 阅读过去 30 年来用于游戏的复制保护方案 - 恶意场景再次似乎太罕见/偏执,无法尝试复杂的设计解决方案。

无论如何,这就是我解决问题的方式。

于 2012-06-19T15:36:07.547 回答
3

我不确定我是否遗漏了什么,但也许应该在类之外有一些逻辑持有你的比较器,当不可见时不会比较卡片?

于 2012-06-19T15:29:44.027 回答
1

对象只需要彼此相等。当人们认为可以在类层次结构的不同级别查看对象时

因此Deck,不支持卡片面朝上或朝下的 a 可能等于另一个Deck支持卡片面朝上或朝下的子类,前提是它equal符合Deck的确定相等性的政策。

但是,支持卡面朝上或朝下的 aPlayableDeck永远不会是不支持卡位置的 a;因为如果确实如此,那么两者将是等效的(可替换),从而有效地破坏了' 知道卡片位置的能力。equalDeckPlayableDeck

只要记住在考虑子类和超类的情况下应用相等规则即可。

  1. A = A
  2. 如果 A = B,则 B 必须 = A。
  3. 如果 A = B 且 B = C,则 A 必须 = C。

起初,规则 2 似乎暗示 aDeck和 aPlayableDeck在任何情况下都不能相等,但事实并非如此。如果您决定将 aPlayableDeck视为 a Deckthen,

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

为什么它如此有效与多态性有关,它确保在尝试超类实现之前检查基类实现这一事实。

于 2012-06-19T15:48:04.213 回答
1

这个问题似乎有几个不同的层次......

但我觉得你正在沸腾的是,卡可能需要两个类别,具体取决于它们是面朝上还是面朝下。

在设计 OO 类时,从逻辑上思考是有益的,即我们是否以不同方式对待一张牌是正面朝上还是正面朝下。

我认为一个设计良好的类系统中的约定是这样的:两张面朝下的牌不应该有一个 equals 方法,因为你可以从其他游戏逻辑中决定是否应该比较它们。

于 2012-06-19T15:28:54.180 回答
1

我会使用不同的相等方法,也许叫它matches,来实现“未知属性”的游戏逻辑,但equals以正常的方式实现该方法,以便让集合等其他类仍然正常运行。通过这种方式,您可以拥有一组 PlayingCard 对象,并且可以保证您没有两张黑桃 A,例如,无论玩家是否知道这些牌的价值。

因此,例如:

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方法 - 这会检查两张牌是否正面朝上,如果是,则检查等级和花色。

于 2012-06-19T16:04:52.003 回答