使数组不可变
Making array immutable
我正在创建扑克模拟器用于学习目的,但在使 Deck
class 不可变时遇到问题。
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
class 持有 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
时都会更新。 !注意 - cardCount
对外部不可见,Card
class 也是不可变的。
问题 1:此设计如何影响 Deck
不变性?
问题 2:对未来有何影响?
问题 3:是否值得让 Deck
class 完全不可变?如果是,那又如何?
感谢您的帮助。
这是不可变性无济于事的少数情况之一。您似乎需要一个可变的共享状态,因为这是 Deck
.
的全部目的
要使其不可变意味着每次有人弹出一张时,您都必须用剩余的卡片创建一张新的 Deck
。但是你无法控制旧的 Deck
(在移除顶牌之前),所以这可能仍然被一些客户引用——在我看来,这违背了游戏的全部目的。
使用当前的实现,您可能 运行 遇到多线程问题,因此请考虑确保 class 线程安全。使用 java.util.Stack
而不是带有计数器的数组将是一个很好的第一次尝试。
最大的问题是你在 Deck 上有一个变异方法 class。您确实需要能够在游戏期间改变牌组中的牌,但实际上您不需要为此改变实际的 Deck 对象。假设你有一张不变的牌 class 和一副不变的牌组 class。牌组用完整的牌进行初始化。当您需要玩游戏时,您会要求发牌者能够与之交互、修改等的纸牌列表。因此,Deck class 只需要将内部列表的副本交给外面。这样 Dealer 就不会修改内部列表,并且 Deck 可以在请求时为新游戏做好准备,并且复制的列表包含对与内部 Deck class 相同的卡片实例的引用并不重要,因为卡片也是不可变的。
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 不再可变,因此线程安全不再是一个问题。为经销商使用卡片列表的另一个好处是,通过集合 class.
可以进行内置排序(如果您有类似的卡片工具)、洗牌等
编辑:阅读您的评论后添加:
如果你想让 Deck 保持状态,那么你不能让它不可变。它可以通过多种不同的方式实现。尽管 ArrayList 与数组相比是一个相当大的数据结构,但在几乎所有情况下,使用它在性能上都优于 LinkedList,并且在安全性和易用性方面优于数组。如果需要非常非常高级别的性能,您可以使用基本数组(也许)会更好,但是 Collections API 已经非常优化,而且 JVM 也非常擅长动态优化代码。到目前为止,根据我的专业经验,我还没有遇到过 运行 仅仅因为性能原因我会在列表上使用数组的情况。一般来说,我会说这是过早优化的情况(提防)。虽然在抽象意义上使用 Stack 而不是 ArrayList 是有意义的,但我认为列表(在 Collections 中)的内置 shuffle 方法的便利性将覆盖它。
使用与上面相同的 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();
}
}
在我看来,可变性取决于两件事:如果对象是不可变的,则需要多少个实例,以及是否会重复使用不可变副本。
第一个很简单,您不需要对套牌进行每一次排列,您可以在需要时制作一个新的套牌,所以这不会阻止您使其不可变。
我开始看到问题的地方是可重用性。您使套牌不可变,但如果不可变性仅用于提供一个全新的对象并丢弃旧的,那么只在适当的位置改变对象而不是创建不必要的对象似乎更合乎逻辑。
但是我确实看到了对不可变卡片的明确使用;永远只有 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 是 "pass-by-value-of-reference".
//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
个对象。
我正在创建扑克模拟器用于学习目的,但在使 Deck
class 不可变时遇到问题。
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
class 持有 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
时都会更新。 !注意 - cardCount
对外部不可见,Card
class 也是不可变的。
问题 1:此设计如何影响 Deck
不变性?
问题 2:对未来有何影响?
问题 3:是否值得让 Deck
class 完全不可变?如果是,那又如何?
感谢您的帮助。
这是不可变性无济于事的少数情况之一。您似乎需要一个可变的共享状态,因为这是 Deck
.
要使其不可变意味着每次有人弹出一张时,您都必须用剩余的卡片创建一张新的 Deck
。但是你无法控制旧的 Deck
(在移除顶牌之前),所以这可能仍然被一些客户引用——在我看来,这违背了游戏的全部目的。
使用当前的实现,您可能 运行 遇到多线程问题,因此请考虑确保 class 线程安全。使用 java.util.Stack
而不是带有计数器的数组将是一个很好的第一次尝试。
最大的问题是你在 Deck 上有一个变异方法 class。您确实需要能够在游戏期间改变牌组中的牌,但实际上您不需要为此改变实际的 Deck 对象。假设你有一张不变的牌 class 和一副不变的牌组 class。牌组用完整的牌进行初始化。当您需要玩游戏时,您会要求发牌者能够与之交互、修改等的纸牌列表。因此,Deck class 只需要将内部列表的副本交给外面。这样 Dealer 就不会修改内部列表,并且 Deck 可以在请求时为新游戏做好准备,并且复制的列表包含对与内部 Deck class 相同的卡片实例的引用并不重要,因为卡片也是不可变的。
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 不再可变,因此线程安全不再是一个问题。为经销商使用卡片列表的另一个好处是,通过集合 class.
可以进行内置排序(如果您有类似的卡片工具)、洗牌等编辑:阅读您的评论后添加:
如果你想让 Deck 保持状态,那么你不能让它不可变。它可以通过多种不同的方式实现。尽管 ArrayList 与数组相比是一个相当大的数据结构,但在几乎所有情况下,使用它在性能上都优于 LinkedList,并且在安全性和易用性方面优于数组。如果需要非常非常高级别的性能,您可以使用基本数组(也许)会更好,但是 Collections API 已经非常优化,而且 JVM 也非常擅长动态优化代码。到目前为止,根据我的专业经验,我还没有遇到过 运行 仅仅因为性能原因我会在列表上使用数组的情况。一般来说,我会说这是过早优化的情况(提防)。虽然在抽象意义上使用 Stack 而不是 ArrayList 是有意义的,但我认为列表(在 Collections 中)的内置 shuffle 方法的便利性将覆盖它。
使用与上面相同的 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();
}
}
在我看来,可变性取决于两件事:如果对象是不可变的,则需要多少个实例,以及是否会重复使用不可变副本。
第一个很简单,您不需要对套牌进行每一次排列,您可以在需要时制作一个新的套牌,所以这不会阻止您使其不可变。
我开始看到问题的地方是可重用性。您使套牌不可变,但如果不可变性仅用于提供一个全新的对象并丢弃旧的,那么只在适当的位置改变对象而不是创建不必要的对象似乎更合乎逻辑。
但是我确实看到了对不可变卡片的明确使用;永远只有 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 是 "pass-by-value-of-reference".
//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
个对象。