主页 聊一聊Java(enmu)枚举的使用
Post
Cancel

聊一聊Java(enmu)枚举的使用

一、声明和使用基本枚举

枚举可以被认为是一个密封类的语法糖,该类在编译时仅实例化了若干次,以定义一组常量。

例子:一个简单的枚举列举出不同的季节:

1
2
3
4
5
6
public enum Season {
    WINTER,
    SPRING,
    SUMMER,
    FALL
}

我们可以在单独文件中声明枚举, 也可以在类中声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Day {
    private Season season;
    public String getSeason() {
        return season.name();
    }
    public void setSeason(String season) {
        this.season = Season.valueOf(season);
    }

    private enum Season {
       WINTER,
       SPRING,
       SUMMER,
       FALL
    }
}

枚举的每个常量默认情况下都是public、static和final的。由于每个常量都是静态的,因此可以使用enum名称直接访问它们。

枚举常数可以作为方法参数传递:

1
2
3
public static void display(Season s) {
    System.out.println(s.name());//name()是一个内置方法,它获取枚举常量
}
1
display(Season.WINTER); // 打印 "WINTER"

可以使用values()方法获得枚举常量的数组。保证返回数组中的值按照声明顺序:

1
Season[] seasons = Season.values();

注意:这个方法每次调用时都会分配一个新的值数组。

迭代枚举常量:

1
2
3
4
5
public static void enumIterate() {
for (Season s : Season.values()) {
        System.out.println(s.name());
    }
}

可以在switch语句中使用枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void enumSwitchExample(Season s) {
    switch(s) {
        case WINTER:
            System.out.println("It's pretty cold");
            break;
        case SPRING:
            System.out.println("It's warming up");
            break;
        case SUMMER:
            System.out.println("It's pretty hot");
            break;
        case FALL:
            System.out.println("It's cooling down");
            break;
    }
}

可以通过==来对比枚举:

1
2
 Season.FALL == Season.WINTER; // false
 Season.SPRING == Season.SPRING; // true

也可以通过equals对比:

1
 Season.FALL.equals(Season.FALL); // true Season.FALL.equals(Season.WINTER); // false

## 二、枚举的构造函数 枚举不能具有公共构造函数;但是,私有构造函数是可以接受的(枚举的构造函数默认是包私有的):

1
2
3
4
5
6
7
8
9
10
11
 public enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    //注意,上面的括号和构造函数参数匹配
    private int value;
    Coin(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}
1
int p = Coin.NICKEL.getValue(); // p = 5

如果你要实现一个枚举作为一个类,可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Coin<T extends Coin<T>> implements Comparable<T>, Serializable {
    public static final Coin PENNY = new Coin(1);
    public static final Coin NICKEL = new Coin(5);
    public static final Coin DIME = new Coin(10);
    public static final Coin QUARTER = new Coin(25);

    private int value;
    private Coin(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}
1
int p = Coin.NICKEL.getValue(); // p = 5

枚举常量在技术上是可变的,因此可以添加setter来更改枚举常量的内部结构。然而,这被认为是非常不好的做法,应该避免。

最佳实践是使枚举字段不可变,使用final:

1
2
3
4
5
6
7
8
public enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    private final int value;
    Coin(int value){
        this.value = value;
    }
...
}

也可以在同一个枚举中定义多个构造函数。这样做的话,你在enum声明中传递的参数将决定调用哪个构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Coin {
    PENNY(1, true), NICKEL(5, false), DIME(10), QUARTER(25);
    private final int value;
    private final boolean isCopperColored;
    Coin(int value){
        this(value, false);
    }
    Coin(int value, boolean isCopperColored){
        this.value = value;
        this.isCopperColored = isCopperColored;
    }
...
}

注意:所有非基本枚举字段都应该实现Serializable,因为枚举类实现了序列化。

## 三、带有抽象方法的枚举 枚举可以定义抽象方法,每个枚举成员都需要实现抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  enum Action {
    DODGE {
        public boolean execute(Player player) {
            return player.isAttacking();
        }
    },
    ATTACK {
        public boolean execute(Player player) {
             return player.hasWeapon();
        }
    },
    JUMP {
        public boolean execute(Player player) {
           return player.getCoordinates().equals(new Coordinates(0, 0));
        }
    };
    public abstract boolean execute(Player player);
}

这允许每个enum成员为给定的操作定义自己的行为,而不必在顶级定义中的方法中切换类型。

注意,此模式是使用多态性和/或实现接口通常实现的一种简短形式。 ## 四、实现接口 下面代码, 是一个enum,也是一个可调用函数,它根据预编译的正则表达式模式测试字符串输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.regex.Pattern;
import java.util.function.Predicate;

enum RegEx implements Predicate<String> {
    UPPER("[A-Z]+"), LOWER("[a-z]+"), NUMERIC("[+-]?[0-9]+");
    private final Pattern pattern;

    private RegEx(final String pattern) {
        this.pattern = Pattern.compile(pattern);
    }

    @Override
    public boolean test(final String input) {
        return this.pattern.matcher(input).matches();
    }
}
1
2
3
4
5
6
7
public class Main {
    public static void main(String[] args) {
        System.out.println(RegEx.UPPER.test("ABC"));
        System.out.println(RegEx.LOWER.test("abc"));
        System.out.println(RegEx.NUMERIC.test("+111"));
    }
}

enum的每个成员也可以实现该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.function.Predicate;

enum Acceptor implements Predicate<String> {
    NULL {
        @Override
        public boolean test(String s) {
            return s == null;
        }
    },
    EMPTY {
        @Override
        public boolean test(String s) {
            return s.equals("");
        }
    },
    NULL_OR_EMPTY {
        @Override
        public boolean test(String s) {
            return NULL.test(s) || EMPTY.test(s);
        }
    };
}
1
2
3
4
5
public class Main {
    public static void main(String[] args) {
        System.out.println(Acceptor.NULL.test(null)); // true System.out.println(Acceptor.EMPTY.test("")); // true System.out.println(Acceptor.NULL_OR_EMPTY.test(" ")); // false
    }
}

## 五、使用单元素枚举实现单例模式 枚举常量在第一次引用枚举时实例化。因此,这允许使用单元素枚举实现单例设计模式。

1
2
3
4
5
6
7
8
9
public enum Attendant {
    INSTANCE;
    private Attendant() {
    // 初始化
    }
    public void sayHello() {
        System.out.println("Hello!");
    }
}
1
2
3
4
5
public class Main {
    public static void main(String... args) {
        Attendant.INSTANCE.sayHello();// instantiated at this point
    }
}

这种方法实现单例模式有以下优点:

  • 线程安全
  • 单一实例化的保证
  • 开箱即用的序列化

如实现接口一节所示,这个单例也可能实现一个或多个接口。

六、使用方法和静态块

枚举可以像任何类一样包含方法。为了了解它是如何工作的,我们将像这样声明一个枚举:

1
2
3
public enum Direction {
    NORTH, SOUTH, EAST, WEST;
}

写一个方法,返回枚举在相反的方向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum Direction {
    NORTH, SOUTH, EAST, WEST;
    public Direction getOpposite(){
        switch (this){
            case NORTH:
                return SOUTH;
            case SOUTH:
                return NORTH;
            case WEST:
                return EAST;
            case EAST:
                return WEST;
            default:
                return null;
        }
    }
}

可以通过使用字段和静态初始化块进一步改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum Direction {
    NORTH, SOUTH, EAST, WEST;
    private Direction opposite;
    public Direction getOpposite(){
        return opposite;
    }
    static {
        NORTH.opposite = SOUTH;
        SOUTH.opposite = NORTH;
        WEST.opposite = EAST;
        EAST.opposite = WEST;
    }
}

在本例中,反向存储在一个私有实例字段opposite中,该字段在第一次使用方向时被静态初始化。在这种特殊的情况下(因为NORTH引用SOUTH,反之亦然),我们不能在这里将枚举与构造函数一起使用(构造函数NORTH(SOUTH)、SOUTH(NORTH)、SOUTH(NORTH)、EAST(WEST)、WEST(EAST)会更优雅,并且允许将opposite声明为final,但它是自引用的,因此是不允许的)。

七、枚举的多态性

当一个方法需要接受一组“可扩展”的枚举值时,可以像在普通类上应用多态性一样,创建一个接口,在枚举应使用的任何地方都可以使用这个接口:

1
2
3
public interface ExtensibleEnum {
    String name();
}

这样,任何由接口(implementing)标记的枚举都可以用作参数,从而允许我们创建方法可接受的可变数量的枚举。这可能很有用,例如,在api中存在一个默认的(不可修改的)枚举,并且这些api的用户希望用更多的值“扩展”枚举。

一组默认enum值可以定义如下:

1
2
3
public enum DefaultValues implements ExtensibleEnum {
    VALUE_ONE, VALUE_TWO;
}

然后可以这样定义附加值:

1
2
3
public enum ExtendedValues implements ExtensibleEnum {
    VALUE_THREE, VALUE_FOUR;
}

演示如何使用枚举的示例——注意printEnum()如何接受来自这两种枚举类型的值:

1
2
3
private void printEnum(ExtensibleEnum val) {
    System.out.println(val.name());
}
1
2
3
4
printEnum(DefaultValues.VALUE_ONE); // VALUE_ONE
printEnum(DefaultValues.VALUE_TWO); // VALUE_TWO
printEnum(ExtendedValues.VALUE_THREE); // VALUE_THREE
printEnum(ExtendedValues.VALUE_FOUR); // VALUE_FOUR

八、按名称获取枚举常数

我们定义一个一周的枚举:

1
2
3
enum DayOfWeek {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
}

枚举使用内置方法valueOf(),该方法可用于根据常量的名称查找该常量:

1
2
3
4
String dayName = DayOfWeek.SUNDAY.name();
assert dayName.equals("SUNDAY");
DayOfWeek day = DayOfWeek.valueOf(dayName);
assert day == DayOfWeek.SUNDAY;

这也可以使用动态枚举类型:

1
2
3
Class<DayOfWeek> enumType = DayOfWeek.class;
DayOfWeek day = Enum.valueOf(enumType, "SUNDAY");
assert day == DayOfWeek.SUNDAY;

如果指定的枚举没有具有匹配名称的常量,这两个valueOf()方法都会抛出IllegalArgumentException。

Guava库提供了一个helper方法Enums.getIfPresent(),它返回一个可选的Guava,以消除显式异常处理:

1
2
3
DayOfWeek defaultDay = DayOfWeek.SUNDAY;
DayOfWeek day = Enums.getIfPresent(DayOfWeek.class, "INVALID").or(defaultDay);
assert day == DayOfWeek.SUNDAY;

总结

Enum 类型提出给 JAVA 编程带了了极大的便利,让程序的控制更加的容易,也不容易出现错误。所以在遇到需要控制程序流程时候,可以多想想是否可以利用 enum 来实现。

更新于 2020-03-12 2020-03-12T12:37:36+08:00
This post is licensed under CC BY 4.0

SpringBoot学习笔记(-) 从一个简单的应用程序开始 (未完成)

聊一聊Java1.8流