使数组不可变

时间:2016-08-06 01:05:12

标签: java oop immutability

我正在创建用于学习目的的扑克模拟器,但在使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}} !注意 - 外部不会看到cardCountCard类也是不可变的。

问题1:此设计如何影响Deck不变性?

问题2:未来会有什么影响?

问题3:值Deck课程是否完全不可变?如果是,那怎么办?

感谢您的帮助。

3 个答案:

答案 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个,它们绝对可以重复使用。所以我要做的是将这些卡的缓存存储在不可变的ListSet中:

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个对象。