Reputation: 1524
I'm wondering what's the proper Java programming paradigm for overriding the equals
(and hashCode
) methods of an object of class C in cases where either (a) there's not enough information to determine if two instances of C are equal or (b) the calling method should not be able to determine if two instance of C are equal.
In my project, for example, I have a PlayingCard
class. It seems to me that if a PlayingCard
is face up, then calling methods should have access to its properties, but if it's face down, then those properties should remain unknown:
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; }
It also seems like, for the sake of the Java Collections Framework, two playing cards should be equal if they have the same rank and suit:
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;
}
}
But that reveals too much information:
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;
}
}
Using the getRank
and getSuit` methods doesn't seem to work either:
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
How have other developers dealt with this situation? Is this a situation where throwing some sort of RuntimeException
would be appropriate? Thanks for any input and ideas.
Upvotes: 12
Views: 886
Reputation: 12334
I'm not convinced that polymorphism is the best answer here. Whether a card is face-up or face-down doesn't change the card, it is a STATE of the card. It is a matter of the location of the logic. If your app knows a card is face down, why would it even bother checking for equality and thus there is no need to differentiate between a face-up equals()
and a face-down equals().
To elaborate on some suggestions in other answers here, there might be ways to identify different collections where one is face-down (like a draw deck) or face-up (like a discard pile) but there are many situations the OP didn't specify where the idea of a stack is less helpful. In standard solitare you can have any of the three combinations: all face-down (assumably the player is about to flip the top card), all face up, or a mix of face-down on the bottom with one or more face-up cards on top. In Blackjack the size of a stack (of cards in play) is always one with the players usually having all face-up cards and the dealer having one face-down and one or more face-up.
Focusing on the .equals()
method is somewhat like premature optimization. Back up a step and think about the domain. You have a Card
with attributes of Suite
and Rank
. A Deck
is just a collection of Card
(s) and all the complexity really belongs to whichever Game is using the Deck. Maybe in Blackjack
you would define a Hand
having several attributes, foremost being a collection of Card
s where you could have logic that determines the point value of a hand, whether it was 'soft' or has 'busted', etc. When comparing two Hands, you don't compare individual cards but the final value of the Hands, which in the dealers case is valid even during the time when they have a face-down card.
The malicious scenario in the question seems dubious in that I'd have to ask, who's system is running the game? If it is a multi-client system, it might be a valid concern but the answer would be to not give each client the state of everybody's cards. The typical architecture to prevent that type of cheating would be for a neutral VM/server to be in charge of the game state and evaluating winning and losing. If the game is not multi-client then even though the CPU running the game has complete access to the game data, there is only one player who is affected (the owner of the CPU) and attempting to prevent malicious tampering has a long history of failure - go read about copy-protection schemes used for games over the last 30 years - and the malicious scenario again seems too rare/paranoid to attempt a complex design solution.
Anyway, that is how I'd approach the problem.
Upvotes: 3
Reputation: 15186
I'd use a different equality method, perhaps calling it matches
, to implement the game logic of "unknown properties", but implement the equals
method in the normal fashion so as to let other classes like collections still function normally. This way you can have a Collection of PlayingCard objects and could guarantee that you don't have two aces of spades, for example, in your deck, regardless of whether the player knows the value of those cards.
So, for example:
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; }
}
This way, when you have a collection, it can do sorting and other built-in checking based on the equals
method, which checks the properties regardless of the state of the card (face up or down). When you need to implement game logic, you'd use the matches
method -- this checks if both cards are face up and, if they are, then checks the rank and suit.
Upvotes: 1
Reputation: 70899
Objects only need to equal each other. When one considers that objects may be viewed at different levels of the class hierarchy
So a Deck
that doesn't support cards being face up or down might be equal to another subclass of Deck
that does support cards being face up or down, provided that it is equal
according to Deck
's policy for determining equality.
However, a PlayableDeck
that does support cards being face up or down could never be equal
to a Deck
that doesn't support card position; because if it did, then the two would be equivalent (replaceable), effectively destroying the PlayableDeck
's ability to know the card's position.
Just remember to apply the rules for equality with sub classes and super classes in mind.
At first, rule 2 seems to imply that a Deck
and a PlayableDeck
cannot be equal under any circumstance, but that's not true. If you decide to view a PlayableDeck
as a Deck
then,
if Deck.equals((Deck)PlayableDeck); then ((Deck)PlayableDeck).equals(Deck)
will still work out just fine. Without the constant casting though, the equals method that must fail is that of PlayableDeck.
PlayableDeck.equals(PlayableDeck) // might fail, depending on state
PlayableDeck.equals(Deck) // always fails
Why this works so well is tied to polymorphisim, which ensures that the fact that the base class implementation is checked before attempting super class implementations.
Upvotes: 1
Reputation: 4443
I am not sure if I am missing something, but maybe there should be some logic outside of the class holding your comparator that will not compare cards when not visible?
Upvotes: 3
Reputation: 1298
This question seems like it has a few different layers to it...
But what I feel you're boiling down to is that maybe two classes are needed for cards depending on whether they are faceup or facedown.
When designing OO classes it's beneficial to think logically, on whether or not we treat a card differently if it is faceup or facedown.
I would think the convention in a well designed class system would be this: Two face down cards should not have an equals method on them, because you're able to decide from other game logic whether or not they should be compared.
Upvotes: 1
Reputation: 7957
You could add this condition in the equals method:
if(this.isFaceDown || other.isFaceDown) return false;
I think it's the only way to completely hide the card if it's faced down. The problem is that when adding it to a set you can have duplicates if the cards you're adding are faced down.
Upvotes: 6