扒一扒: Java 中的枚举

@

在 Java 中, 枚举, 也称为枚举类型, 其是一种特殊的数据类型, 它使得变量能够称为一组预定义的常量。 其目的是强制编译时类型安全。

因此, 在 Java 中, enum 是保留的关键字。

保留的关键字

1. 枚举的定义

在 Java 是在 JDK 1.4 时决定引入的, 其在 JDK 1.5 发布时正式发布的。

举一个简单的例子:以日常生活中的方向来定义, 因为其名称, 方位等都是确定, 一提到大家就都知道。

1.1 传统的非枚举方法

如果不使用枚举, 我们可能会这样子定义

public class Direction {
    public static final int EAST = 0;

    public static final int WEST = 1;

    public static final int SOUTH = 2;

    public static final int NORTH = 3;
    
}

以上的定义也是可以达到定义的, 我们在使用时

    @Test
    public void testDirection() {
        System.out.println(getDirectionName(Direction.EAST));
        System.out.println(getDirectionName(5));// 也可以这样调用
    }

    public String getDirectionName(int type) {
        switch (type) {
            case Direction.EAST:
                return "EAST";
            case Direction.WEST:
                return "WEST";
            case Direction.SOUTH:
                return "SOUTH";
            case Direction.NORTH:
                return "NORTH";
            default:
                return "UNKNOW";
        }
    }

运行起来也没问题。 但是, 我们就如同上面第二种调用方式一样, 其实我们的方向就在 4 种范围之内,但在调用的时候传入不是方向的一个 int 类型的数据, 编译器是不会检查出来的。

1.2 枚举方法

我们使用枚举来实现上面的功能

定义

public enum DirectionEnum {
    EAST, WEST, NORTH, SOUTH
}

测试

    @Test
    public void testDirectionEnum() {
        System.out.println(getDirectionName(DirectionEnum.EAST));
       // System.out.println(getDirectionName(5));// 编译错误
    }

    public String getDirectionName(DirectionEnum direction) {
        switch (direction) {
            case EAST:
                return "EAST";
            case WEST:
                return "WEST";
            case SOUTH:
                return "SOUTH";
            case NORTH:
                return "NORTH";
            default:
                return "UNKNOW";
        }
    }

以上只是一个举的例子, 其实, 枚举中可以很方便的获取自己的名称。

通过使用枚举, 我们可以很方便的限制了传入的参数, 如果传入的参数不是我们指定的类型, 则就发生错误。

1.3 定义总结

以刚刚的代码为例

public enum DirectionEnum {
    EAST, WEST, NORTH, SOUTH
}
  1. 枚举类型的定义跟类一样, 只是需要将 class 替换为 enum
  2. 枚举名称与类的名称遵循一样的惯例来定义
  3. 枚举值由于是常量, 一般推荐全部是大写字母
  4. 多个枚举值之间使用逗号分隔开
  5. 最好是在编译或设计时就知道值的所有类型, 比如上面的方向, 当然后面也可以增加

2 枚举的本质

枚举在编译时, 编译器会将其编译为 Java 中 java.lang.Enum 的子类。

我们将上面的 DirectionEnum 进行反编译, 可以获得如下的代码:

// final:无法继承
public final class DirectionEnum extends Enum
{
    // 在之前定义的实例
	public static final DirectionEnum EAST;
	public static final DirectionEnum WEST;
	public static final DirectionEnum NORTH;
	public static final DirectionEnum SOUTH;
	private static final DirectionEnum $VALUES[];
    
    // 编译器添加的 values() 方法
	public static DirectionEnum[] values()
	{
		return (DirectionEnum[])$VALUES.clone();
	}
    // 编译器添加的 valueOf 方法, 调用父类的 valueOf 方法 
	public static DirectionEnum valueOf(String name)
	{
		return (DirectionEnum)Enum.valueOf(cn/homejim/java/lang/DirectionEnum, name);
	}
    // 私有化构造函数, 正常情况下无法从外部进行初始化
	private DirectionEnum(String s, int i)
	{
		super(s, i);
	}
    
    // 静态代码块初始化枚举实例
	static 
	{
		EAST = new DirectionEnum("EAST", 0);
		WEST = new DirectionEnum("WEST", 1);
		NORTH = new DirectionEnum("NORTH", 2);
		SOUTH = new DirectionEnum("SOUTH", 3);
		$VALUES = (new DirectionEnum[] {
			EAST, WEST, NORTH, SOUTH
		});
	}
}

通过以上反编译的代码, 可以发现以下几个特点

2.1 继承 java.lang.Enum

通过以上的反编译, 我们知道了, java.lang.Enum 是所有枚举类型的基类。查看其定义

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

可以看出来, java.lang.Enum 有如下几个特征

  1. 抽象类, 无法实例化
  2. 实现了 Comparable 接口, 可以进行比较
  3. 实现了 Serializable 接口, 可进行序列化

因此, 相对应的, 枚举类型也可以进行比较和序列化

2.2 final 类型

final 修饰, 说明枚举类型是无法进行继承的

2.3 枚举常量本身就是该类的实例对象

可以看到, 我们定义的常量, 在类内部是以实例对象存在的, 并使用静态代码块进行了实例化。

2.4 构造函数私有化

不能像正常的类一样, 从外部 new 一个对象出来。

2.5 添加了 $values[] 变量及两个方法

  • $values[]: 一个类型为枚举类本身的数组, 存储了所有的示例类型
  • values() : 获取以上所有实例变量的克隆值
  • valueOf(): 通过该方法可以通过名称获得对应的枚举常量

3 枚举的一般使用

枚举默认是有几个方法的

3.1 类本身的方法

从前面我的分析, 我们得出, 类本身有两个方法, 是编译时添加的

3.1.1 values()

先看其源码

	public static DirectionEnum[] values() {
		return (DirectionEnum[])$VALUES.clone();
	}

返回的是枚举常量的克隆数组。

使用示例

    @Test
    public void testValus() {
        DirectionEnum[] values = DirectionEnum.values();
        for (DirectionEnum direction:
             values) {
            System.out.println(direction);
        }
    }

输出

EAST
WEST
NORTH
SOUTH

3.1.2 valueOf(String)

该方法通过字符串获取对应的枚举常量

    @Test
    public void testValueOf() {
        DirectionEnum east = DirectionEnum.valueOf("EAST");
        System.out.println(east.ordinal());// 输出0
    }

3.2 继承的方法

因为枚举类型继承于 java.lang.Enum, 因此除了该类的私有方法, 其他方法都是可以使用的。

3.2.1 ordinal()

该方法返回的是枚举实例的在定义时的顺序, 类似于数组, 第一个实例该方法的返回值为 0。

在基于枚举的复杂数据结构 EnumSetEnumMap 中会用到该函数。

    @Test
    public void testOrdinal() {
        System.out.println(DirectionEnum.EAST.ordinal());// 输出 0
        System.out.println(DirectionEnum.NORTH.ordinal()); // 输出 2
    }

3.2.2 compareTo()

该方法时实现的 Comparable 接口的, 其实现如下

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

首先, 需要枚举类型是同一种类型, 然后比较他们的 ordinal 来得出大于、小于还是等于。

    @Test
    public void testCompareTo() {
        System.out.println(DirectionEnum.EAST.compareTo(DirectionEnum.EAST) == 0);// true
        System.out.println(DirectionEnum.WEST.compareTo(DirectionEnum.EAST) > 0); // true
        System.out.println(DirectionEnum.WEST.compareTo(DirectionEnum.SOUTH) < 0); // true
    }

3.2.3 name() 和 toString()

该两个方法都是返回枚举常量的名称。 但是, name() 方法时 final 类型, 是不能被覆盖的! 而 toString 可以被覆盖。

3.2.4 getDeclaringClass()

获取对应枚举类型的 Class 对象

@Test
public void testGetDeclaringClass() {
    System.out.println(DirectionEnum.WEST.getDeclaringClass());
    // 输出 class cn.homejim.java.lang.DirectionEnum
}

2.3.5 equals

判断指定对象与枚举常量是否相同

    @Test
    public void testEquals() {
        System.out.println(DirectionEnum.WEST.equals(DirectionEnum.EAST)); // false
        System.out.println(DirectionEnum.WEST.equals(DirectionEnum.WEST)); // true
    }

4 枚举类型进阶

枚举类型通过反编译我们知道, 其实也是一个类(只不过这个类比较特殊, 加了一些限制), 那么, 在类上能做的一些事情对其也是可以做的。 但是, 个别的可能会有限制(方向吧, 编译器会提醒我们的)

4.1 自定义构造函数

首先, 定义的构造函数可以是 private, 或不加修饰符

自定义构造函数

我们给每个方向加上一个角度

public enum DirectionEnum {
    EAST(0), WEST(180), NORTH(90), SOUTH(270);

    private int angle;

   DirectionEnum(int angle) {
        this.angle = angle;
    }

    public int getAngle() {
        return angle;
    }
}

测试

    @Test
    public void testConstructor() {
        System.out.println(DirectionEnum.WEST.getAngle()); // 180
        System.out.println(DirectionEnum.EAST.getAngle()); // 0
    }

4.2 添加自定义的方法

以上的 getAngle 就是我们添加的自定义的方法

4.2.1 自定义具体方法

我们在枚举类型内部加入如下具体方法

    protected void move() {
        System.out.println("You are moving to " + this + " direction");
    }

测试

    @Test
    public void testConcreteMethod() {
        DirectionEnum.WEST.move();
        DirectionEnum.NORTH.move();
    }

输出

You are moving to WEST direction
You are moving to NORTH direction

4.2.2 在枚举中定义抽象方法

在枚举类型中, 也是可以定义 abstract 方法的

我们在DirectinEnum中定义如下的抽象方法

abstract String onDirection();

定义完之后, 发现编译器报错了, 说我们需要实现这个方法

抽象方法

按要求实现

实现

测试

    @Test
    public void testAbstractMethod() {
        System.out.println(DirectionEnum.EAST.onDirection());
        System.out.println(DirectionEnum.SOUTH.onDirection());
    }

输出

EAST direction 1
NORTH direction 333

也就是说抽象方法会强制要求每一个枚举常量自己实现该方法。 通过提供不同的实现来达到不同的目的。

4.3 覆盖父类方法

在父类 java.lang.Enum 中, 也就只有 toString() 是没有使用 final 修饰啦, 要覆盖也只能覆盖该方法。 该方法的覆盖相信大家很熟悉, 在此就不做过多的讲解啦

4.4 实现接口

因为Java是单继承的, 因此, Java中的枚举因为已经继承了 java.lang.Enum, 因此不能再继承其他的类。

但Java是可以实现多个接口的, 因此 Java 中的枚举也可以实现接口。

定义接口

public interface TestInterface {
    void doSomeThing();
}

实现接口

public enum DirectionEnum implements TestInterface{
    // 其他代码
    public void doSomeThing() {
       System.out.println("doSomeThing Implement");
    }
    // 其他代码
}

测试

    @Test
    public void testImplement() {
        DirectionEnum.WEST.doSomeThing(); // 输出 doSomeThing Implement
    }

5 使用枚举实现单例

该方法是在 《Effective Java》 提出的

public enum Singlton {
    INSTANCE;

    public void doOtherThing() {
        
    }
}

使用枚举的方式, 保证了序列化机制, 绝对防止多次序列化问题, 保证了线程的安全, 保证了单例。 同时, 防止了反射的问题。

该方法无论是创建还是调用, 都是很简单。 《Effective Java》 对此的评价:

单元素的枚举类型已经成为实现Singleton的最佳方法。


6 枚举相关的集合类

java.util.EnumSetjava.util.EnumMap, 在此不进行过多的讲述了。

posted @ 2018-12-03 11:03  阿进的写字台  阅读(870)  评论(0编辑  收藏  举报