Java进阶 1-1 枚举

目录

枚举的基本特性

枚举类型中的自定义方法

switch语句中的枚举

编译器创建的values()方法

使用实现代替继承

构建工具:生成随机的枚举

组织枚举

EnumSet

EnumMap


本笔记参考自: 《On Java 中文版》


        枚举类型通过enum关键字定义,其中包含了数量有限的命名变量。

枚举的基本特性

       当我们创建枚举类型时,系统会自动为我们生成一个辅助类,这个类继承自java.lang.Enum。下面的例子展示了其中的一些方法:

【例子:枚举自带的方法】

enum Fruit {
    APPLE,
    BANANA,
    ORANGE
}

public class EnumClass {
    public static void main(String[] args) {
        for (Fruit f : Fruit.values()) {
            System.out.println(
                    f + "对应序数:" + f.ordinal());
            System.out.println(
                    "compareTo(BANANA):" + f.compareTo(Fruit.BANANA));
            System.out.println(
                    "equals(BANANA):" + f.equals(Fruit.BANANA));

            System.out.println("f == Fruit.ORANGE? " +
                    (f == Fruit.ORANGE));
            System.out.println(f.getDeclaringClass());
            System.out.println(f.name());
            System.out.println("====================");
        }
    }
}

        程序执行的结果是:

        简单介绍一些方法:

  • values():生成一个由枚举常量组成的数组,其中常量的顺序和常量声明的顺序保持一致。
  • ordinal():返回一个从0开始的int值,代表每个枚举实例的声明顺序。
  • getDeclaringClass():获得该枚举实例所属的外部包装类。
  • name():返回枚举实例被声明的名称。

    equals()方法由编译器自动生成,而compareTo()方法则来自Comparable接口(Enum实现了它),这里不再赘述。

静态导入枚举类型

        使用枚举类型的理由之一,就是枚举可以增强我们代码的可读性。有时,我们会使用静态导入枚举的方式使用枚举:

【例子:静态导入的枚举类型】

        首先创建一个枚举类型:

// 关于香料的枚举:
public enum SpicinessEnum {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

        现在,让我们在程序中静态导入它:

// 静态导入一个枚举类型:
import static enums.SpicinessEnum.*;

// 制作一个玉米煎饼:
public class Burrito2 {
    SpicinessEnum degree;

    public Burrito2(SpicinessEnum degree) {
        this.degree = degree;
    }

    @Override
    public String toString() {
        return "来个玉米饼,添加香料:" + degree;
    }

    public static void main(String[] args) {
        System.out.println(new Burrito2(NOT));
        System.out.println(new Burrito2(MEDIUM));
        System.out.println(new Burrito2(HOT));
    }
}

        程序执行的结果是:

        通过static import,我们将所有的枚举实例标识符都引入了本地命名空间。值得一提的是,是否静态导入枚举类型大多不会影响代码的运行,但我们仍需要考虑代码的可读性:若代码本身很复杂,静态导入或许就不会是一个更好的选择。

    若枚举定义在通过文件,或定义在默认包中,则无法使用上述的这种方式。

枚举类型中的自定义方法

        除去无法继承,基本上可以将枚举类型看做一个普通的类。可以向其中添加自定义方法,甚至于main()方法。

        通过创建一个含参构造器,枚举可以获取额外的信息,并通过额外的方法来扩展应用。例如:

【例子:在枚举中创建新的方法】

public enum MakeAHuman {
    HEAD("我来组成头部"),
    BODY("我来组成躯体"),
    TONSIL("我来组成扁桃体");
    private String description;

    private MakeAHuman(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    // 也可以进行方法重载:
    @Override
    public String toString() {
        String id = name();
        String lower = id.substring(1).toLowerCase();
        return id.charAt(0) + lower;
    }

    public static void main(String[] args) {
        for (MakeAHuman human : MakeAHuman.values())
            System.out.println(human +
                    ": " + human.getDescription());
    }
}

        程序执行的结果是:

        若想要添加自定义方法,首先必须用分号结束枚举实例的序列。注意:Java会强制我们在枚举中先定义实例

        之前也提到过,枚举类型不允许继承。这意味着枚举在被定义完毕后,无法被用于创建任何新的类型。

switch语句中的枚举

        枚举类型可以被应用于switch语句。一般,switch语句只支持整型或字符串类型,但ordinal()方法可以获取枚举内部的整型序列。在这里,编译器完成了后台的各种工作。

        在通常情况下,若要使用枚举实例,就需要使用枚举的类型名对其进行限定。但在switch语句中不需要这么做:

【例子:switch中的枚举】

enum Signal {
    GREEN, YELLOW, RED
}

public class TrafficLight {
    Signal color = Signal.RED;

    public void change() {
        // 在case语句中,无需使用Signal进行限定:
        switch (color) {
            case RED:
                color = Signal.GREEN;
                break;
            case GREEN:
                color = Signal.YELLOW;
                break;
            case YELLOW:
                color = Signal.RED;
                break;
        }
    }

    @Override
    public String toString() {
        return "现在,信号灯的颜色是:" + color;
    }

    public static void main(String[] args) {
        TrafficLight t = new TrafficLight();
        for (int i = 0; i < 7; i++) {
            System.out.println(t);
            t.change();
        }
    }
}

        程序执行的结果是:

        尽管没有添加default语句,但编译器也没有报错。这不见得是一件好事,因为即使我们注释掉了其中的一条分支,编译器也不会报错:

        因此,在编写分支语句时我们必须小心,确保代码已经覆盖了所有的分支。

编译器创建的values()方法

        根据官方文档的描述,Enum类中并不存在values()方法:

因此我们可以猜测,编译器在后台为我们完成了某件事。接下来的例子会通过反射分析Enum类中的方法:

【例子:通过反射探究Enum类】

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;

enum Explore {
    HERE,
    THERE
}

public class Reflection {
    public static Set<String> analyze(
            Class<?> enumClass) {
        System.out.println("_____分析" + enumClass + "_____");
        System.out.println("$接口:");
        for (Type t : enumClass.getGenericInterfaces())
            System.out.println(t);

        System.out.println("$基类:" +
                enumClass.getSuperclass());

        System.out.println("$方法:");
        Set<String> methods = new TreeSet<>();
        for (Method m : enumClass.getMethods())
            methods.add(m.getName());
        System.out.println(methods);
        return methods;
    }

    public static void main(String[] args) {
        Set<String> exploreMethods =
                analyze(Explore.class);
        System.out.println();
        Set<String> enumMethods =
                analyze(Enum.class);

        System.out.println();
        System.out.println("Explore.containsAll(Enum)? " +
                exploreMethods.containsAll(enumMethods));

        System.out.print("Explore.removeAll(Enum): ");
        exploreMethods.removeAll(enumMethods);
        System.out.println("[" + exploreMethods + "]");


    }
}

        程序执行的结果是:

        从反射的结果可以发现,枚举Explore中多出了Enum没有的values()方法。现在让我们进一步,通过反编译Explore来查看它的内部信息:

反编译告诉我们,values()方法是由编译器添加的一个静态方法。除此之外,还可以发现:Explore内部的valueOf()方法只有一个参数,而Enum自带的valueOf()却有两个参数:

然而,Set只会关注方法名,因此在执行Explore.remove(Enum)后,valueOf()方法也被去除了。

    从反射的结果中,我们还可以知道两件事:① Explore枚举是final类,因此我们无法继承;②反射认为Explore的基类就是Enum,这并不准确,Explore的基类应该是Enum<Explore>,类型擦除使得编译器无法获取完整类型信息,因此才会出现这种现象。

        需要注意的一点是,values()只在子类Explore中存在(由编译器插入),因此当我们将该枚举类型向上转型为Enum时。我们将无法使用这个方法。作为替代,可以使用Class.getEnumConstants()

【例子:getEnumConstants()的使用例】

enum Search {
    HITHER,
    YON
}

public class UpcastEnum {
    public static void main(String[] args) {
        Search[] vals = Search.values();
        Enum e = Search.HITHER; // 发生向上转型
        // e.values(); // 此时会发现Enum中并没有values()方法

        // Class.getEnumConstants()方法返回一个包含枚举中的每个元素的数组
        for (Enum en : e.getClass().getEnumConstants())
            System.out.println(en);
    }
}

        程序执行的结果是:

    因为getEnumConstants()方法属于Class类,因此非枚举类型也可以调用它。不过此时方法会返回null,调用这个结果会抛出异常。

使用实现代替继承

        已知,所有枚举类都会默认继承java.lang.Enum。而Java不支持多重继承,这就意味着一个枚举类无法再继承任何其他的类:

// enum NotPossible extends SomethingElse { ... // 不允许这么做

        作为替代,我们可以令枚举类型实现一些接口:

【例子:让枚举类实现接口】

import java.util.Random;
import java.util.function.Supplier;

enum LetterCharacter
        implements Supplier<LetterCharacter> {
    A, B, C, D, E, F, G;
    private Random rand =
            new Random(47);

    @Override
    public LetterCharacter get() {
        return values()[rand.nextInt(values().length)];
    }
}

public class EnumImplementation {
    public static <T> void printNext(Supplier<T> rg) {
        System.out.print(rg.get() + " ");
    }

    public static void main(String[] args) {
        LetterCharacter ll = LetterCharacter.G;
        for (int i = 0; i < 10; i++)
            printNext(ll);
    }
}

        程序执行的结果是:

        这种做法有一点很奇怪:我们必须传入一个枚举实例,然后才能使用printNext()方法。

构建工具:生成随机的枚举

       为了方便我们使用枚举,可以创建一个用于随机生成枚举的Enums类:

package onjava;

public class Enums {
    private static Random rand = new Random(47);

    public static <T extends Enum<T>> T random(Class<T> ec) {
        return random(ec.getEnumConstants());
    }

    public static <T> T random(T[] values) {
        return values[rand.nextInt(values.length)];
    }
}

        这个类中的random()方法会接收Class对象,并返回随机的枚举对象。

(因为之后也会使用该类,因此在这里提前进行展示)

组织枚举

        尽管枚举类型无法继承,但我们任然会有可能用到继承关系的情况。一般地,继承枚举有两个动机:

  1. 希望扩充原始枚举中的元素。
  2. 想要使用子类型创建不同的子分组。

        一个方法是通过接口对枚举进行分类。下面的例子在接口中将元素分类完毕,然后会基于这个接口生成一个枚举,这样就能实现分类的目的:

【例子:在接口中分类】

public interface Food {
    enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS;
    }

    enum MainCourse implements Food {
        RICE, NOODLES, BREAD, PASTA;
    }

    enum Dessert implements Food {
        CUPCAKE, JELLY, CANDY, CHOCOLATE, COOKIES;
    }

    enum Drink implements Food {
        COFFEE, TEA, JUICE, MILK
    }
}

        这种方式就像是将枚举作为了接口的子类型一样。通过静态导入,就可以使用它:

import enums.menu.Food;

import static enums.menu.Food.*;

public class TypeOfFood {
    public static void main(String[] args) {
        Food food = Appetizer.SALAD;
        food = MainCourse.RICE;
        food = Dessert.CANDY;
    }
}

        通过这种方法,我们就得到了“由接口组织的枚举”,但它还不足以应对所有情况。当我们需要处理一组类型时,接口并没有内置的方法能够为我们提供便利。此时,使用“由枚举组织的枚举”更为管用:

【例子:由枚举来组织枚举】

import onjava.Enums;

public enum Course {
    APPETIZER(Food.Appetizer.class),
    MAINCOURSE(Food.MainCourse.class),
    DESSERT(Food.Dessert.class),
    COFFEE(Food.Drink.class);

    private Food[] values;

    // 接收枚举类型对应的Class对象
    private Course(Class<? extends Food> kind) {
        values = kind.getEnumConstants();
    }

    public Food randomSelection() {
        return Enums.random(values);
    }
}

         因为Course是一个枚举类型,因此我们可以直接使用枚举特有的方法:

public class Meal {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            for (Course course : Course.values()) {
                Food food = course.randomSelection();
                System.out.println(food);
            }
            System.out.println("======");
        }
    }
}

        程序执行的结果如下:

        Course可以直接调用枚举所有的方法,因此可以很方便地进行遍历操作。

        上述的做法需要使用接口和枚举。显然,可以将它们整理到一起,形成一种更加清晰的写法:

【例子:在枚举中嵌套枚举】

import onjava.Enums;

enum SecurityCategory {
    STOCK(Security.Stock.class),
    BOND(Security.Bond.class);

    Security[] values;

    SecurityCategory(Class<? extends Security> kind) {
        values = kind.getEnumConstants();
    }

    interface Security {
        enum Stock implements Security {
            SHORT, LONG, MARGIN
        }

        enum Bond implements Security {
            MUNICIPAL, JUNK
        }
    }

    public Security randomSelection() {
        return Enums.random(values);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            SecurityCategory category =
                    Enums.random(SecurityCategory.class);
            System.out.println(category + ": " +
                    category.randomSelection());
        }
    }
}

        程序执行的结果是:

        在这种方法中,枚举内部存在一个接口。通过它,我们可以将所需的枚举类型进行聚合。

EnumSet

        Set和枚举都对元素的唯一性有所要求,但枚举无法进行增删操作,因此不如Set来得便利。因此,用于配合枚举的Set类型,EnumSet诞生了。

    EnumSet的一个目的,是替代原本基于int的“位标记”用法。

    这一类型的一大优势就是速度,其内部的实现基于一个long类型的变量(位向量)

        EnumSet中的元素必须来源于某个枚举类型:

【例子:EnumSet的使用】

        为了使用EnumSet,首先需要创建一个枚举(报警器的位置信息):

// 报警感应器的位置
public enum AlarmPoints {
    STAIR1, STAIR2,
    LOBBY,
    OFFICE1, OFFICE2, OFFICE3, OFFICE4,
    BATHROOM,
    UTILITY,
    KITCHEN
}

        利用这个枚举,下面的程序会展示一些EnumSet的基本用法:

import java.util.EnumSet;

import static enums.AlarmPoints.*;

public class EnumSets {
    public static void main(String[] args) {
        // 使用noneOf()方法创建一个空的EnumSet
        EnumSet<AlarmPoints> points =
                EnumSet.noneOf(AlarmPoints.class);

        points.add(BATHROOM);
        System.out.println(points);
        points.addAll(
                EnumSet.of(STAIR1, STAIR2, KITCHEN));
        System.out.println(points);

        System.out.println();
        points = EnumSet.allOf(AlarmPoints.class);
        points.removeAll(
                EnumSet.of(STAIR1, STAIR2, KITCHEN));
        System.out.println(points);
        points.removeAll(
                EnumSet.range(OFFICE1, OFFICE4));
        System.out.println(points);

        System.out.println();
        // complementOf()方法返回points中未包含的枚举集
        points = EnumSet.complementOf(points);
        System.out.println(points);
    }
}

        程序执行的结果是:

        EnumSet.of()方法具有多个重载

这是处于性能的考量。尽管这些of()方法可以被一个使用了可变参数的方法替代,但那样做的效率会略低于现在的这种做法。

    尽管表格上没有出现,但EnumSet中是存在使用可变参数列表的of()方法的。而如果我们只传入一个参数,编译器不会调用这个of()方法,因此也不会产生额外的开销。

        通常情况下,EnumSet是基于64位的long构建的。其中,每个枚举实例需要通过1位来表达其的状态。因此,在使用一个long时,单个EnumSet只能支持包含64个元素的枚举类型。但有时,我们的枚举会超过64个元素:

【例子:超过64个元素的EnumSet

public class BigEnumSet {
    enum Big {
        A1, A2, A3, A4, A5, A6, A7, A8, A9, A10,
        A11, A12, A13, A14, A15, A16, A17, A18, A19, A20,
        A21, A22, A23, A24, A25, A26, A27, A28, A29, A30,
        A31, A32, A33, A34, A35, A36, A37, A38, A39, A40,
        A41, A42, A43, A44, A45, A46, A47, A48, A49, A50,
        A51, A52, A53, A54, A55, A56, A57, A58, A59, A60,
        A61, A62, A63, A64, A65
    }

    public static void main(String[] args) {
        EnumSet<Big> bigEnumSet = EnumSet.allOf(Big.class);
        System.out.println(bigEnumSet);
    }
}

        程序执行的结果是:

        从输出结果可以看出,若元素超过64个,EnumSet会进行额外的处理。这一点也可以从源代码处了解:

EnumMap

        除了EnumSet,也存在EnumMap。这一特殊的Map要求所有的来自于某个枚举类型。EnumMap内部的实现基于数组,因此有着很高的效率。

    与普通的Map相比,EnumMap在操作上的特殊之处只在于:当我们调用put()方法时,只能使用枚举中的值。

【例子:EnumMap的使用例】

import java.util.EnumMap;
import java.util.Map;

import static enums.AlarmPoints.*;

// 使用了命令模式:
// 创建一个接口(只包含一个方法),衍生出不同的实现
interface Command {
    void action();
}

public class EnumMaps {
    public static void main(String[] args) {
        EnumMap<AlarmPoints, Command> em =
                new EnumMap<>(AlarmPoints.class);
        em.put(KITCHEN,
                () -> System.out.println("厨房失火"));
        em.put(BATHROOM,
                () -> System.out.println("水龙头坏了"));

        for (Map.Entry<AlarmPoints, Command> e :
                em.entrySet()) {
            System.out.println(e.getKey() + ": ");
            e.getValue().action();
        }

        try { // 若不存在指定key值
            em.get(UTILITY).action();
        } catch (Exception e) {
            System.out.println("异常:" + e);
        }
    }
}

        程序执行的结果是:

        在上述的em中,所有的枚举元素都有其对应的键。并且根据输出结果的异常显示,所有的键对应的值都会被初始化为null

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值