Java枚举类型的入门

这个文章的示例代码是专门针对枚举的说明。因此,它包含了一些通常被认为不好的代码(例如,在计算金额时使用 int 或 double 而不是 BigDecimal)。

如何表示分类等

比如, 我们考虑一个虚构的电子商务网站系统。这个网站有三个会员等级:铜牌会员、银牌会员和金牌会员,不同等级的会员享有不同的折扣率等优惠。我们要如何表达这个呢?

首先,这是一个不好的例子。

public class Rank {
    public static final int BRONZE = 1;
    public static final int SILVER = 2;
    public static final int GOLD = 3;
}

使用整数常数来表示等级。这样做会有什么不利影响呢?

假设有一个以等级为参数的方法。

public class PriceCalculator {
    public int getDiscountPrice(int price, int rank) {
        switch (rank) {
            case Rank.BRONZE:
                return price;
            case Rank.SILVER:
                return (int) (price * 0.9);
            case Rank.GOLD:
                return (int) (price * 0.8);
            default:
                throw new IllegalArgumentException("Invalid rank");
        }
    }
}

希望使用这个方法的第二个参数能够指定为 Rank.BRONZE 等等。尽管已经在 Javadoc 中详细说明了这一点,但还是可能会有一些人没有阅读并按照说明来使用。

getDiscountPrice(10000, 1);  // 定数を使わずにハードコーディング
getDiscountPrice(10000, 0);  // 定数に定義されていない値を指定

另外,我也对排名和折扣率这两个非常相关的数值被记录在另一个类中感到担忧。这样后面的维护工作可能会非常困难。

即使在相同的等级类别内将折扣率固定化,也可能出现与等级的常数相同的现象(如硬编码)。而且,当需要与等级相关的折扣率以外的值时,进一步增加相同类型的常数是不希望的吧……。

public class Rank {
    public static final int BRONZE = 1;
    public static final int SILVER = 2;
    public static final int GOLD = 3;

    // 定数がどんどん増えていく
    public static final double DISCOUNT_RATE_BRONZE = 0.0;
    public static final double DISCOUNT_RATE_SILVER = 0.1;
    public static final double DISCOUNT_RATE_GOLD = 0.2;
}

枚举(enum)是什么?

在这里出现的是枚举。日语中也被称为“列举类型”。

以下是 enum 的含义。

public enum Rank {
    // 定数
    BRONZE(0.0),
    SILVER(0.1),
    GOLD(0.2);

    // フィールド
    private final double discountRate;

    // コンストラクタ
    private Rank(double discountRate) {
        this.discountRate = discountRate;
    }

    // メソッド
    public int getDiscountPrice(int price) {
        return (int) (price * (1 - discountRate));
    }
}

enum是一个具有以下特点的特殊类。

    フィールド・メソッドは普通のクラスと同様に定義できる
    コンストラクタはprivateにしかできない=外部からのインスタンス生成は不可能

    アクセス修飾子が無い場合はprivateと解釈される

    public・protectedを付けるとコンパイルエラー

    クラス直下に定数を宣言する

    これらの定数は自クラスのインスタンス
    自クラスのpublic static finalなフィールドとなる

换句话说,BRONZE,SILVER和GOLD是Rank类的实例,同时也是Rank类的公共静态常量字段。

()中指定的值是传递给构造函数的参数。换句话说,在这种情况下,它将成为构造函数中discountRate字段的值。

以自然的方式翻译成中文,可以描述如下:
如果用中文表达,可以这样描述:
强行将其归类为一类课程时,可以形象地描绘为这样。

但是要注意的是,它与实际的枚举类型不同。请把它仅仅当做一种想象。

public class Rank {
    // 定数
    public static final Rank BRONZE = new Rank(0.0);
    public static final Rank SILVER = new Rank(0.1);
    public static final Rank GOLD = new Rank(0.2);

    // フィールド
    private final double discountRate;

    // コンストラクタ
    private Rank(double discountRate) {
        this.discountRate = discountRate;
    }

    // メソッド
    public int getDiscountPrice(int price) {
        return (int) (price * (1 - discountRate));
    }
}

在中国的母语中,”enum的何处令人高兴”可以被解释为”枚举类型的哪些方面令人欣喜”。

总而言之,可以通过解决前面提到的int常量的问题来解决。

如果有一个接受等级为参数的方法,在指定未定义的值或者不使用常量硬编码的情况下将变得不可能。

// Rankに定義された定数(BRONZE・SILVER・GOLD)以外は引数に指定できない!
public void doSomething(Rank rank) {
    ...
}

当然,null可以作为参数进行指定。这在Java的语法上是无法避免的呢… ?

此外,我们可以将排名(rank)和折扣率(discountRate)这两个非常相关的值合并到一个类中。这样一来,我们还可以在同一个类内定义一个使用折扣率的方法(getDiscountPrice())。 这样做对于维护来说会更加轻松!

建议阅读下一本书,以了解更多有关enum的应用技巧。

    現場で役立つシステム設計の原則
    Software Design 2021年3月号の特集「Javaでもう一度学び直すオブジェクト指向プログラミング」

根据不同的设定值来进行处理,以减少使用者端的条件判断。

public double calc(double x, double y, Operation ope) {
    switch(ope) {
    case PLUS:
        return x + y;
    case MINUS:
        return x - y;
    case TIMES:
        return x * y;
    case DIVIDED_BY:
        return x / y;
    }
}

让我们在enum的每个常数中加入该条件分支的逻辑。这样一来,我们就可以消除使用方的条件判断。

public double calc(double x, double y, Operation ope) {
    return ope.eval(x, y);
}

只需一种选择,请用中文将以下内容进行改述:
第一种方法是针对每个特定的数值,对方法进行覆盖。

这段代码是从Java Language Specification借来的,并稍作修改(URL→https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#d5e15436)。

enum Operation {
    PLUS {
        @Override
        double eval(double x, double y) {
            return x + y;
        }
    },
    MINUS {
        @Override
        double eval(double x, double y) {
            return x - y;
        }
    },
    TIMES {
        @Override
        double eval(double x, double y) {
            return x * y;
        }
    },
    DIVIDED_BY {
        @Override
        double eval(double x, double y) {
            return x / y;
        }
    };

    abstract double eval(double x, double y);
}

這是匿名類別的語法寫法。

方案二:组合接口

如果逻辑很复杂的话,也许选择这个可能更好。

interface Calculator {
    double eval(double x, double y);
}

class PlusCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x + y;
    }
}

class MinusCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x - y;
    }
}

class TimesCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x * y;
    }
}

class DivideCalculator implements Calculator {
    @Override
    public double eval(double x, double y) {
        return x / y;
    }
}
enum Operation {
    PLUS(new PlusCalculator()),
    MINUS(new MinusCalculator()),
    TIMES(new TimesCalculator()),
    DIVIDED_BY(new DivideCalculator());

    private final Calculator calculator;

    Operation(Calculator calculator) {
        this.calculator = calculator
    }

    double eval(double x, double y) {
        return calculator.eval(x, y);
    }
}

[高階編] enum是Enum的子类

enum作为java.lang.Enum的子类被隐式定义。这意味着可以使用Enum类的所有方法。

枚举类的方法列表 → https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/lang/Enum.html

顺便提一句,泛型参数E将被定义的枚举自身填充。换句话说就是这样。

public class Rank extends Enum<Rank> {
    ...
}

特别重要的是在Enum定义的方法中,以下是其中的一些。

メソッド 説明 String name() 定数名を返す String toString() 定数名を返す int ordinal() 定数が宣言された順番を返す(最初の定数が0

此外,在定义枚举时还会自动创建方法。

メソッド 説明 static <T extends Enum<T>> T[] values() 全定数の配列を返す(順番は宣言されたとおり) static <T extends Enum<T>> T valueOf(String name) 引数で指定された名前(完全一致)の定数を返す
Rank[] ranks = Rank.values();
Rank rank = Rank.valueOf("GOLD");  // Rank.GOLDが返る

[高级编辑]使用EnumSet

java.util.EnumSet类是一个名为enum的Set。

假设只有银牌会员和金牌会员可以领取礼物。要实现这个判断方法,该如何实施呢?

public boolean canGetPresent(Rank rank) {
    return rank == Rank.GOLD || rank == Rank.SILVER;
}

在这个规模上,也许感觉还不错。但是,如果排名数量变为了10个,并且能够领取奖励的排名变为了5个,那会怎么样呢?写的时候和读的时候都很辛苦呢。

在这种情况下,可以使用EnumSet。

import java.util.EnumSet;

public enum Rank {
    BRONZE(1.0),
    SILVER(0.9),
    GOLD(0.8);

    // フィールド等省略

    // EnumSetを利用!
    private static final EnumSet<Rank> ranksCanGetPresent = EnumSet.of(SILVER, GOLD);

    public boolean canGetPresent() {
        // EnumSet#contains()を利用
        return ranksCanGetPresent.contains(this);
    }
}
Rank rank = ...;
if (rank.canGetPresent()) {
    System.out.println("プレゼントをどうぞ!");
}

这样的话,即使等级增加也很容易!

bannerAds