如何实现Set集合中禁止插入同类型对象?
Great question! The JDK doesn't have a built-in Set implementation that enforces "one instance per subclass" out of the box, but there are straightforward ways to build this yourself. Let's break down the problem and solutions:
Why Overriding equals()/hashCode() Isn't Reliable
You're right to avoid this approach. If you made all instances of the same subclass return true for equals() and the same hashCode(), you'd technically get the behavior you want—but it violates the contract of equals().
For example, two Diamonds objects with different id or materialName values would be considered equal, which doesn't make sense for your domain (these are distinct objects with different state). This could cause bugs elsewhere in your code when you expect to compare these cards by their actual properties.
Custom Set Implementation Using a Map
The cleanest approach is to wrap a Map where the key is the subclass Class (since each subclass has a unique Class object), and the value is the corresponding Card instance. This guarantees only one entry per subclass.
Here's a complete implementation:
import java.util.*; public class TypeUniqueCardSet implements Set<Card> { // Maps each Card subclass to its single instance in the set private final Map<Class<? extends Card>, Card> typeInstanceMap = new HashMap<>(); @Override public boolean add(Card card) { if (card == null) { throw new NullPointerException("Card cannot be null"); } Class<? extends Card> cardType = card.getClass(); // If the type already exists, reject the add if (typeInstanceMap.containsKey(cardType)) { return false; } typeInstanceMap.put(cardType, card); return true; } @Override public boolean contains(Object o) { if (!(o instanceof Card)) { return false; } Card candidate = (Card) o; // Check if the type exists AND the stored instance matches Card stored = typeInstanceMap.get(candidate.getClass()); return stored != null && stored.equals(candidate); } @Override public boolean remove(Object o) { if (!(o instanceof Card)) { return false; } Card card = (Card) o; return typeInstanceMap.remove(card.getClass()) != null; } // Delegate other Set methods to the map's values collection @Override public int size() { return typeInstanceMap.size(); } @Override public boolean isEmpty() { return typeInstanceMap.isEmpty(); } @Override public Iterator<Card> iterator() { return typeInstanceMap.values().iterator(); } @Override public Object[] toArray() { return typeInstanceMap.values().toArray(); } @Override public <T> T[] toArray(T[] a) { return typeInstanceMap.values().toArray(a); } @Override public boolean containsAll(Collection<?> c) { for (Object o : c) { if (!contains(o)) { return false; } } return true; } @Override public boolean addAll(Collection<? extends Card> c) { boolean modified = false; for (Card card : c) { if (add(card)) { modified = true; } } return modified; } @Override public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException("retainAll is not supported for this set"); } @Override public boolean removeAll(Collection<?> c) { boolean modified = false; for (Object o : c) { if (remove(o)) { modified = true; } } return modified; } @Override public void clear() { typeInstanceMap.clear(); } }
How to Use It
Set<Card> cards = new TypeUniqueCardSet(); System.out.println(cards.add(new Diamonds(1, new Universe(), "steel"))); // true System.out.println(cards.add(new Spades("iron", 7.87))); // true System.out.println(cards.add(new Spades("copper", 8.96))); // false (Spades already exists)
Simplified Implementation with Guava (Optional)
If you're using Google Guava, you can use ForwardingSet to avoid writing all the boilerplate Set methods. This delegates most work to a standard HashSet while adding your type-checking logic:
import com.google.common.collect.ForwardingSet; import java.util.HashSet; import java.util.Set; public class TypeUniqueCardSet extends ForwardingSet<Card> { private final Set<Card> delegate = new HashSet<>(); private final Set<Class<? extends Card>> registeredTypes = new HashSet<>(); @Override protected Set<Card> delegate() { return delegate; } @Override public boolean add(Card card) { Class<? extends Card> cardType = card.getClass(); if (registeredTypes.contains(cardType)) { return false; } registeredTypes.add(cardType); return super.add(card); } @Override public boolean remove(Object o) { if (o instanceof Card) { registeredTypes.remove(((Card) o).getClass()); } return super.remove(o); } @Override public void clear() { registeredTypes.clear(); super.clear(); } }
Key Takeaways
- Avoid overriding
equals()/hashCode()for this purpose—it breaks object equality semantics. - Use a custom
Setbacked by aMap(or Guava'sForwardingSet) to track which subclasses are already present. - This approach keeps your domain classes clean and adheres to standard collection contracts.
内容的提问来源于stack exchange,提问作者Johann MARTINET

