了解Java中的枚举类型

201 阅读13分钟

了解Java中的枚举类型

枚举类型是Java中强有力的工具。枚举可以定义一类命名的常量,并在开关语句或表达式中提供类型安全和键。

本文解释了Java中枚举类的基本结构,然后通过探讨枚举和类之间的关系(枚举实现接口,枚举有实例变量、方法和构造函数),探索如何设置自定义枚举属性,使其更上一层楼。

我们将演示枚举在构建扑克牌游戏控制器中的应用。这个应用决定了玩家在扑克游戏中的手牌的排名类别。

目标

在本教程结束时,读者应了解以下内容。

  • 枚举类型的基本结构。
  • 枚举和类之间的关系
  • 为枚举类型定义自定义属性,为枚举常量访问序数值。
  • 枚举的应用。

前提条件

为了充分理解本教程,你需要具备以下条件。

  • 对Java编程语言有基本了解。
  • [计算机上安装了]Java开发工具包(JDK)。
  • [安装了]IntelliJ代码编辑器。

Java枚举类型的基本结构

Java Enum的声明以关键词enum开始,后面是用camelCase (所有Java类的命名惯例)指定的类型名称。

在类型名后面是一对大括号,形成枚举类的上下文或范围。在这些大括号中是一组唯一的标识符,代表枚举常量。

注意:没有两个枚举常量可以使用相同的标识符。

大写枚举常量也是一种很好的做法,这与Java常量的命名惯例相一致,并使它们脱颖而出。

enum Suit{
    HEARTS,
    DIAMONDS,
    CLUBS,
    SPADES,
}

在这一点上,有必要指出。

  • 枚举常量是隐含的静态和最终的。
  • 试图用new关键字创建一个枚举类的对象,会导致编译错误。
  • 由于枚举常量是静态和最终的,一个枚举类的对象可以通过在枚举类名称上引用枚举常量来创建,如下例所示:Suit suit = Suit.HEART

枚举声明可以在一个类中出现,如下所示。

public class SuitTest {

    enum Suit {
        HEARTS,
        DIAMONDS,
        CLUBS,
        SPADES
    }
    public static void main(String[] args) {
    Suit suit = Suit.HEART;
    System.out.println(suit);
  }
}

这段代码给出的输出是 -HEART

枚举也可以在同一个Java文件中定义,如下所示。

public class CardSuitTest{

    public static void main(String[] args) {
        CardSuit suit = CardSuit.HEART;
        System.out.println(suit);
    }
}

enum CardSuit{
    HEARTS,
    DIAMONDS,
    CLUBS,
    SPADES,
}

枚举也可以定义在一个单独的Java文件中。

当枚举类被定义在类之外但在同一个文件内时,JVM会为枚举和类创建单独的.class文件(.class文件是在编译器编译了书面代码后产生的)。枚举不能与同一包内的类同名。枚举不能在方法中创建。

在开关表达式中使用枚举类型

枚举类型代表一组唯一的常量,这意味着它们可以在switch语句或较新的switch表达式中使用。考虑一下下面的例子。

public void printCardSuit(Suit suit){
    switch(suit){
        case HEARTS -> System.out.println("Its hearts!");
        case DIAMONDS -> System.out.println("Its diamonds!");
        case CLUBS -> System.out.println("Its clubs!");
        case SPADES -> System.out.println("Its spades!");
    }
}

我们调用了printCardSuit(),它接收了一个我们在教程中早先定义的Suit类型的枚举。然后,它将该枚举的值传递给一个封闭的开关表达式,这意味着对该方法的调用,如printCardSuit(Suit.HEARTS) ,将产生输出。它的红心!而调用printCardSuit(Suit.SPADES) 将产生输出。它的黑桃!

Java中的枚举类型从类java.lang.Enum ,这意味着当我们定义一个枚举类型时,额外的方法会隐含地添加到我们的定义中。其中一个方法是valueOf(),它允许我们使用枚举常量的toString表示来创建一个枚举常量,如下所示。

Suit cardSuit = Suit.valueOf(“HEARTS”)

java对象的toString() 表示是该对象作为字符串的表示。

试图做以下事情:Suit cardSuit = Suit.valueOf(“Hearts”) ,结果是java.lang.IllegalArgumentException ,因为在枚举定义中没有名为*"Hearts "*的常量。

在switch表达式中,如果有一个以上的常量映射到相同的动作,我们可以做如下的回避。

 public void printSuit(Suit suit){
    switch(suit){
        case HEARTS, DIAMONDS -> System.out.println("Its hearts and diamonds!");
        case CLUBS -> System.out.println("Its clubs!");
        case SPADES -> System.out.println("Its spades!");
    }
}

枚举和类之间的关系

正如前面提到的,枚举是特殊的类。JVM会将枚举定义转换为类定义。因此,枚举的定义。

enum Suit{
    HEARTS,
    DIAMONDS,
    CLUBS,
    SPADES,
}

被JVM表示为。

class Suit{
    public static final Suit HEARTS = new Suit();
    public static final Suit DIAMONDS = new Suit();
    public static final Suit CLUBS = new  Suit();
    public static final Suit SPADES = new Suit();
}

因此,每个枚举常量都是枚举类的一个对象的表示。枚举与Java中的类密切相关,但又不是那么相关。

其中一个显著的区别是,一个枚举不能扩展另一个类,因为一个枚举隐含地从Java.lang.Enum扩展,由于一个方法不能继承于Java中的多个方法,所以枚举类不能继承于另一个类。

从Java.lang.Enum扩展使得以下方法在枚举类中隐式可用。

  1. values()方法。这将返回枚举类中定义的所有常量的数组。例如:Suit.values() ,返回以下数组。[红心、方块、梅花、黑桃]。

  2. The ordinal()方法。每个枚举常量都可以通过它在枚举定义中的位置来识别。这个位置对应于一个数组索引。例如,在Suit枚举中。

  • HEARTS的序数是0
  • 钻石(DIAMOND)的序数为1
  • CLUBS的序号是2
  • SPADES的序号是3
  1. valueOf方法。如上所述,如果枚举定义中存在指定的常数,该方法将返回该常数的toString 表示。

枚举类也可以像普通类一样实现多个接口。枚举类也可以像普通的Java类一样拥有构造函数、实例变量和方法。

public enum Suit {
    HEARTS("Hearts"),
    DIAMONDS("Diamonds"),
    CLUBS("Clubs"),
    SPADES("Spades");

    private final String suitName;

    Suit(String suitName) {
        this.suitName = suitName;
    }

    public String getSuitName() {
        return suitName;
    }
}

从上面的例子中可以看出,枚举类Suit有一个构造函数,它定义了一个字符串作为参数,用来初始化其实例变量suitName 。枚举也有一个实例方法getSuitName() ,用于返回该对象的适当的suitName

为枚举类型定义自定义属性

如上所述,ordinal() 方法返回一个枚举常数的序数。我们不能设置该序数方法。我们能做的最接近的就是为枚举类定义一个自定义的整数属性。

考虑一下我们上面的西装枚举例子。假设我们希望每张牌的花色常数都由一个整数属性来表示。假设1代表红心,2代表钻石,3代表梅花,4代表黑桃。我们可以定义一个自定义的整数属性,将每个整数映射到相应的牌型,如下所示。

public enum CardSuit {
    HEARTS(1),
    DIAMONDS(2),
    CLUBS(3),
    SPADES(4);

    private int value;

    CardSuit(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

因此,当我们定义一个枚举常量时,如下所示。Suit suit = Suit.HEARTS

我们可以说suit.getValue() ,这样就会返回1

注意枚举定义的结构。常量首先被定义,接着是实例变量声明,然后是构造函数定义。

构建扑克牌游戏控制器。

这个发牌和洗牌的应用程序决定了玩家在扑克游戏中的手牌的排名类别。

为了巩固我们到目前为止所学到的知识,让我们建立一个发牌和洗牌的应用程序,确定玩家在扑克游戏中的手牌排名。

为了建立我们的应用程序,我们需要以下东西。

  • 一个卡片对象--一张卡片有一个牌面,属于一个牌型。牌面和牌型以枚举形式表示。
  • 一个玩家类。
  • 一副牌 - 一副牌被模拟为52张牌的集合。
  • 模拟一种洗牌算法(Fisher-Yates洗牌算法)和发牌。
  • 一个能确定的游戏控制器。
    • 一对
    • 两对
    • 三条
    • 四条
    • 同花顺(即所有五张相同花色的牌)。
    • 顺子(即五张连续面值的牌)
    • 满堂红(即两张一张面值的牌和三张另一张面值的牌)。

这里是类图。

Game Controller Class Diagram

首先,让我们定义玩家类。一个玩家有一个名字和一个playerHand ,它是一个5张牌的数组。

public class Player {
    private final String playerName;
    private Card[] playerHand;

    public Player(String playerName) {
        this.playerName = playerName;
    }

    public Card[] getPlayerHand(){
        return playerHand;
    }
    public String getPlayerName() {
        return playerName;
    }
}

接下来,让我们定义Suit和Face枚举。

public enum Suit {
    HEARTS,
    DIAMONDS,
    CLUBS,
    SPADES
}
public enum Face {
    ACE(1),
    DEUCE(2),
    THREE(3),
    FOUR(4),
    FIVE(5),
    SIX(6),
    SEVEN(7),
    EIGHT(8),
    NINE(9),
    TEN(10),
    JACK(11),
    QUEEN(12),
    KING(13);

    public int getFaceValue() {
            return this.faceValue;
    }

    private final int faceValue;

    Face(int faceValue){
            this.faceValue = faceValue;
    }
}

正如上面所定义的,每个牌面都有一个整数faceValue ,我们将其定义为一个自定义属性。卡面ACE的faceValue 为1。DEUCE的faceValue 为2,还有很多。

我们定义卡类如下。

public class Card {
    private final Face face;
    private final Suit suit;

    public Card(Face face, Suit suit){
            this.face = face;
            this.suit = suit;
    }

    public Face getFace(){
            return face;
    }

    public Suit getSuit(){
            return suit;
    }
}

我们在这里看到,一张卡有一个面,属于一个套牌。我们还定义了获取牌面和牌型的通用方法。接下来要做的是定义DeckOfCards类。一个DeckOfCards ,是52张牌的集合。

public class DeckOfCards {

    private Suit[] suits = Suit.values();
    private Face[] faces = Face.values();
    private Card[] deckOfCards = new Card[52];

    public DeckOfCards(){
            int counter = 0;
            for (Suit suit : suits) {
                    for (Face face: faces) {
                        deckOfCards[counter] = new Card(face,suit);counter++;
                    }
            }
    }
}

我们看到values() 方法在运行。这个方法返回一个枚举常量的数组。我们的构造函数包含嵌套的for循环,用于填充先前定义的Cards数组。

接下来,我们根据Fisher-Yates洗牌算法来定义shuffle方法。下面是对Fisher-Yates算法的描述。

  1. 写下从1到N的数字。
  2. 挑选一个介于1和剩余的未击球号码数(包括)之间的随机数k。
  3. 从低端开始算起,把尚未划掉的第k个数字划掉,并把它写在另一个列表的最后。
  4. 重复第2步,直到所有的数字都被划掉。
  5. 第3步中写下的数字序列现在是原始数字的随机变异。
public void shuffle(){
    Card[] copyOfDeck = new Card[deckOfCards.length];
    SecureRandom random = new SecureRandom();
    int randomIndex;
    for (int i = deckOfCards.length-1; i >=0 ; i--) {
        if (i == 0){
            randomIndex = 0;
        }
        else{
            randomIndex = random.nextInt(i);
        }
        copyOfDeck[deckOfCards.length - 1 - i] = deckOfCards[randomIndex];
        deckOfCards[randomIndex] = deckOfCards[i];
    }
    deckOfCards = copyOfDeck;
}

接下来,我们定义交易方法。deal() 方法接受一个玩家数组和要发的牌的数量作为参数。

在发牌方法中,我们首先用前面定义的洗牌方法将牌洗好。对于每个玩家,我们把作为参数传入的牌数分配给发牌方法。

public void deal(Player[] players, int numberOfCardsToDeal){
    shuffle();
    for (Player player: players) {
        for (int j = 0; j < numberOfCardsToDeal; j++) {
            player.getPlayerHand()[j] = deckOfCards[j];
        }
    }
}

我们的DeckOfCards 类最后达到了高潮。

public class DeckOfCards {

    private Suit[] suits = Suit.values();
    private Face[] faces = Face.values();
    private Card[] deckOfCards = new Card[52];

    public DeckOfCards(){
        int counter = 0;
        for (Suit suit : suits) {
            for (Face face: faces) {
                deckOfCards[counter] = new Card(face,suit);
                counter++;
            }
        }
    }

    public void shuffle(){
        Card[] copyOfDeck = new Card[deckOfCards.length];
        SecureRandom random = new SecureRandom();
        int randomIndex;
        for (int i = deckOfCards.length-1; i >=0 ; i--) {
            if (i == 0){
                randomIndex = 0;
}
            else{
                randomIndex = random.nextInt(i);
            }copyOfDeck[deckOfCards.length - 1 - i] = deckOfCards[randomIndex];deckOfCards[randomIndex] = deckOfCards[i];
        }
        deckOfCards = copyOfDeck;
}

public void deal(Player[] players, int numberOfCardsToDeal){
        shuffle();
        for (Player player: players) {
            for (int j = 0; j < numberOfCardsToDeal; j++) {
                player.getPlayerHand()[j] = deckOfCards[j];
            }
        }
    }
}

最后,让我们定义GameController。GameController类包含确定玩家手牌等级的方法。要做到这一点,我们将使用Java流和Lambda函数。

Java 8引入了流和lambdas的概念。流通过一连串的处理步骤传递元素。这些处理步骤可以是中间操作,如map,filter,distinct,limit, 和sorted, 或终端操作,如forEach,collect,min,max,findFirst, 和reduce

这些流操作以通常称为lambdas的功能接口作为参数。

现在让我们来定义确定玩家手牌等级的方法。

containsAPair()

这是一个包含两张相同等级(Face)的牌和三张其他等级的牌的手牌。为了确定一个玩家的手牌是否包含一对相同等级的牌和三张其他等级的牌,我们要做以下工作。

  • 首先,我们使用Arrays.stream() 方法生成一个牌流。
  • 接下来,我们将生成的牌流传递给地图操作。地图操作生成一个新的牌流,其中原始牌流中的每张牌都通过调用getFace() 方法被映射到其牌面。
  • 最后,我们通过使用collect(Collectors.toSet() 方法将流收集成一个集合来终止流的操作。我们收集一个集合,这样我们就可以消除重复的东西。因此,如果有两张卡片具有相同的faceValue ,我们的集合中应该只有四个元素而不是五个。我们检查这个集合是否只包含四个元素,将布尔值结果返回给调用者。
public static boolean containsAPair(Card[] playerHand){
Set<Face> cardFaces = Arrays.stream(playerHand).map(Card::getFace).collect(collectors.toSet());
return cardFaces.size() == 4;

Card::getFace 称为方法引用,它是lambda表达式的简称。card -> card.getFace()

containsTwoPairs()。

要确定一个玩家的手牌是否包含两对(两张面值相同的牌和一张面值不同的牌)。要做到这一点,我们。

  • 首先,创建一个Card的流。
  • 然后通过collect() 终端操作,将牌收集到一个地图中,并按牌面分组,使用: collect(Collectors.groupingBy(Card::getFace)) .
  • 接下来,我们检查地图上的组数,看只有两张牌的组数是否等于2。
public static boolean containsTwoPairs(Card[] playerHand) {
   Map<Face, List<Card>> cardFaceListMap =
           Arrays.stream(playerHand).collect(Collectors.groupingBy(Card::getFace));
   final int[] countOfDuplicates = {0};
   cardFaceListMap.forEach((face, cardList) -> {
       if (cardFaceListMap.get(face).size() == 2){
           countOfDuplicates[0]++;
       }
   });
   return countOfDuplicates[0] == 2;
}
含有三张牌的组()。

确定玩家的手牌是否包含三张相同的牌(三张相同面值的牌和另外两张不同面值的牌)。要做到这一点。

  • 首先,我们创建一个牌流。
  • 接下来,我们将产生的牌流收集成一个集合。
  • 最后,我们检查产生的集合的大小。如果它等于3,那么玩家的手牌中就有三张相同的牌。
public static boolean containsThreeOfAKind(Card[] playerHand) {
   Set<Face> cardSet = Arrays.stream(playerHand).map(Card::getFace).collect(Collectors.toSet());
   return cardSet.size() == 3;
}
containsFourOfAKind()。

确定玩家的手牌是否包含三张相同的牌(四张相同面值的牌和一张不同面值的牌)。要做到这一点。

  • 首先,我们像前面定义的方法一样生成一个牌流。
  • 接下来,我们将生成的牌流收集成一个集合。
  • 最后,我们检查这个集合的大小是否等于2。
public static boolean containsFourOfAKind(Card[] playerHand) {
  Set<Face> cardFaces = Arrays.stream(playerHand).map(Card::getFace).collect(Collectors.toSet());
  return cardFaces.size() == 2;
}
containsAFlush()。

确定玩家的手牌是否包含同花顺(玩家手牌中的所有五张牌都是相同花色)。要做到这一点。

  • 首先,我们生成一个牌流。
  • 接下来,我们将生成的牌流收集成一个套。
  • 最后,我们检查该套牌的大小是否等于1。
public static boolean isAFlush(Card[] playerHand) {
  Set<Suit> cardSuits = Arrays.stream(playerHand)
          .map(Card::getSuit).collect(Collectors.toSet());
  return cardSuits.size() == 1;
}
isAStraight()。

确定玩家的手牌是否是顺子(包含五张连续等级的牌,而不是相同的花色)。要做到这一点。

  • 首先,我们生成一个牌流。
  • 接下来,我们将每张牌映射到它的faceValue - 一个整数,使用map 操作。
  • 然后,我们把从map 操作中得到的流传到distinct操作中(另一个中间流操作)。区分操作从流中删除任何重复的内容。
  • 接下来,我们通过sorted 中间操作对distinct操作产生的流进行排序,该操作按照自然顺序(升序)对流中的faceValues
  • 最后,我们使用collect 操作终止流管道,该操作使用collect(Collectors.toList()) 将流元素收集到一个列表中。
  • 为了检查玩家的手牌,我们用玩家手中最小的牌faceValue减去最大的牌。如果差值为4,并且玩家手中的不同牌的数量为5,那么玩家的手牌是顺子。
public static boolean isAStraight(Card[] playerHand) {
  List<Integer> faceValues=
  Arrays.stream(playerHand).map(card -> card.getFace().getFaceValue()).distinct()
  .sorted(Comparator.naturalOrder()).collect(Collectors.toList());
  return (faceValues.get(faceValues.size() - 1) - faceValues.get(0) == 4)&&(faceValues.size()==5);
  }
isAFullHouse()。

确定玩家的手牌是否是满堂红(包含三种特定等级的牌,然后是另一种等级的两张牌)。为了确定这一点。

  • 首先,我们创建一个牌流。
  • 接下来,我们将每张牌通过映射操作映射到其牌面。
  • 我们通过collect操作将所得的牌流收集成一个集合。
  • 最后,我们检查所得集合的大小是否等于2。
public static boolean isAFullHouse(Card[] playerHand) {
  Set<Face> cardSet = Arrays.stream(playerHand).map(Card::getFace).collect(Collectors.toSet());
  return cardSet.size() == 2;
  }

总结

我们已经成功地学习了Java中的Enum类型,并应用我们的知识实现了一个扑克牌游戏控制器,以确定玩家手牌的排名。

在这个过程中,我们还学习了如何实现Fisher-Yates洗牌算法,使用Java中的Lambdas和流以及相关的流操作,使我们能够通过声明式编程,不慌不忙地进行复杂操作。