我正在创建用于学习目的的扑克模拟器,但在使Deck
类不可变时遇到问题。
public class Deck {
private final Card[] cards;
private int cardCount;
public Deck(@NotNull Card[] c) {
this.cards = Arrays.copyOf(c, c.length);
this.cardCount = c.length - 1;
}
public Deck shuffle() {
// shuffle logic
return new Deck(this.cards);
}
public Card pop() {
return this.cards[cardCount--];
}
// other methods
}
Dealer
类保存Deck
引用并实现处理逻辑。
public class Dealer {
private final Deck deck;
public Dealer(@NotNull Deck d) {
this.deck = d;
}
public void deal(@NotNull Holder<Card> h) {
h.hold(deck.pop());
}
// other methods
}
正如您所看到的,Deck
并非完全不可变,因为它保持cardCount
每次调用pop
时都会更新。{1}} !注意 - 外部不会看到cardCount
,Card
类也是不可变的。
问题1:此设计如何影响Deck
不变性?
问题2:未来会有什么影响?
问题3:值Deck
课程是否完全不可变?如果是,那怎么办?
感谢您的帮助。
答案 0 :(得分:2)
这是少数几个不可变性无效的案例之一。您似乎需要一个可变的共享状态,因为这是Deck
的全部目的。
要使其不可变,这意味着每次有人弹出一张卡时,你必须用剩余的卡片创建一个新的Deck
。但是你无法控制旧的Deck
(在删除顶部卡片之前),所以这可能仍然会被一些客户引用 - 在我看来这会破坏游戏的整个目的。
使用当前的实现,您可能遇到多个线程的问题,因此请考虑使您的类线程安全。使用java.util.Stack
代替带有计数器的数组将是一个很好的第一次尝试。
答案 1 :(得分:1)
最大的问题是你在Deck类上有一个变异方法。你需要能够在游戏过程中改变你在牌组中的牌,但实际上你不需要改变实际的Deck对象。假设您有一个不可变的卡类和一个不可变的甲板类。套牌初始化为全套卡片。当你需要玩游戏时,你需要一张经销商可以与之交互,修改等等的牌列表。所以Deck类只需要提供内部列表的副本。这样,经销商不会修改内部列表,并且Deck已准备好在请求时使用新游戏,并且复制列表保持对与内部Deck类相同的卡实例的引用并不重要,因为卡也是不可变的
public final class Card {
public enum Suit { HEART, SPADE, CLUB, DIAMOND }
public enum Value { ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING }
private final Suit suit;
private final Value value;
public Card( Suit suit, Value value) {
this.suit = suit;
this.value = value;
}
// ... rest omitted
}
public final class Deck {
private final List<Card> cards = new ArrayList<>();
public Deck() {
for (Card.Suit suit : Card.Suit.values()) {
for (Card.Value value : Card.Value.values()) {
cards.add(new Card(suit, value));
}
}
}
// Get a new list of cards (though the actual card instances are the same
// that's ok since they are immutable)
public List<Card> getCards() {
return new ArrayList<>(cards);
}
}
此外,由于没有共享状态且Deck不再可变,因此线程安全不再是一个问题。使用经销商卡片列表的另一个好处是,通过Collections类可以内置排序(如果你有卡片工具可比),shuffle等。
编辑:阅读完评论后添加:
如果您希望Deck保持状态,那么您不能让它变为不可变。它可以通过多种不同的方式实现。尽管在几乎所有情况下,ArrayList与数组相比都是一个相当大的数据结构,但它在性能上的使用优于LinkedList,并且为了安全性和易用性而优于数组。如果需要非常非常高级的性能,您可以使用原始数组(可能)更好,但Collections API已经过优化,JVM也非常擅长动态优化代码。到目前为止,根据我的专业经验,我还没有遇到一个案例,我只是因为性能而在列表中使用数组。一般来说,我会说这是一个过早优化的情况(谨防)。虽然在抽象意义上使用Stack over ArrayList是有道理的,但我认为列表的内置shuffle方法(在Collections中)的便利性会覆盖它。
使用与上面相同的Card对象,可变Deck看起来如下(带有列表):
public class EmptyDeckException extends RuntimeException {
static final long serialVersionUID = 0L;
public EmptyDeckException(String message) {
super(message);
}
}
public class Deck {
private final List<Card> cards = new ArrayList<>();
public Deck() {
for (Card.Suit suit : Card.Suit.values()) {
for (Card.Value value : Card.Value.values()) {
cards.add(new Card(suit, value));
}
}
}
public void shuffle() {
Collections.shuffle(cards);
}
public Card pop() {
if (!cards.isEmpty()) {
return cards.remove(cards.size() - 1);
}
throw new EmptyDeckException("The deck is empty");
}
}
如果坚持使用堆栈,那么它可能是这样的:
public class Deck {
private final Stack<Card> cards = new Stack<>();
public Deck() {
for (Card.Suit suit : Card.Suit.values()) {
for (Card.Value value : Card.Value.values()) {
cards.push(new Card(suit, value));
}
}
}
public void shuffle() {
// Now this needs to be done manually and will probably be less performant
// than the collections method for lists
}
public Card pop() {
return cards.pop();
}
}
答案 2 :(得分:1)
在我看来,可变性取决于两件事:如果对象是不可变的,你需要多少个实例,以及是否可以重用不可变副本。
第一个很容易,你不需要对一个套牌进行每一个排列,你可以在需要的时候制作一个新的,这样就不会阻止你使它变得不可变。 / p>
我开始看到问题是可重用性。你创造了一个不可变的牌组,但是如果不变性只能提供一个全新的物体而扔掉旧的物体,那么改变现有的物体似乎更合乎逻辑而不是创造不必要的物体。
然而,我确实看到了不可变卡的明确使用;它们将只有54个,它们绝对可以重复使用。所以我要做的是将这些卡的缓存存储在不可变的List
或Set
中:
private static final List<Card> CARDS = Collections.unmodifiableList(new ArrayList<Card>() {{
//store all your cards, can be done without double-brace initialiation
}});
从那里(在Deck
内部的上下文中),我们可以复制此List
而不需要花费太多内存 - Card
实例将是相同的Java是&#34;按值传递&#34;。
//Note: The left hand side is purposefully LinkedList and not List, you'll see why
private final LinkedList<Card> shuffledDeck = new LinkedList<>();
public Deck() {
this.shuffle();
}
public Deck shuffle() {
this.shuffledDeck.clear(); //wipe out any remaining cards
this.shuffledDeck.addAll(CARDS); //add the whole deck
Collections.shuffle(this.shuffledDeck); //Finally, randomize
return this;
}
public Card pop() {
return this.shuffledDeck.pop(); //Thanks Deque, which LinkedList implements
}
这使您节省的内存使Card
不可变,同时不会花费大量Deck
个对象。