深入了解 Java 枚举及其实现方式
本文详细介绍了Java中的枚举的概念、原理以及案例演示,比如自定义枚举。
@[TOC]
1 枚举
枚举是指将变量的值一一列出来,变量的值只限于列举出来的值的范围内。举例:一周只有7天,一年只有12个月等。
回想单例设计模式:单例类是一个类只有一个实例。
那么多例类就是一个类有多个实例,但不是无限个数的实例,而是有限个数的实例,这也叫枚举类。
1.1 特性
enum 与 class、interface 具有相同地位; 可以继承多个接口; 可以拥有构造器、成员方法、成员变量;
枚举类与普通类不同之处:
- 默认继承 java.lang.Enum 类,所以不能继承其他父类;其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 接口;
- 使用 enum 定义,默认使用 final 修饰,因此不能派生子类;
- 构造器默认使用 private 修饰,且只能使用 private 修饰;
- 枚举类所有实例必须在第一行给出,默认添加 public static final 修饰,否则无法产生实例。
2 传统的自定义枚举类
2.1 第一种
public class Direction {
public static final Direction FRONT = new Direction();
public static final Direction BEHIND = new Direction();
public static final Direction LEFT = new Direction();
public static final Direction RIGHT = new Direction();
private Direction() {
}
}
2.2 第二种
public class Direction2 {
public static final Direction2 FRONT = new Direction2();
public static final Direction2 BEHIND = new Direction2("后");
public static final Direction2 LEFT = new Direction2("左");
public static final Direction2 RIGHT = new Direction2("右");
private Direction2() {
}
/**
* 加入一个成员变量
*/
private String name;
/**
* 加入有参构造器
*
* @param name
*/
private Direction2(String name) {
this.name = name;
}
/**
* 加入一个方法
*
* @return
*/
public String getName() {
return name;
}
}
2.3 第三种
public abstract class Direction3 {
public static final Direction3 FRONT = new Direction3("前") {
@Override
public void show() {
System.out.println("我是前面");
}
};
public static final Direction3 BEHIND = new Direction3("后") {
@Override
public void show() {
System.out.println("我是后面");
}
};
public static final Direction3 LEFT = new Direction3("左") {
@Override
public void show() {
System.out.println("我是左面");
}
};
public static final Direction3 RIGHT = new Direction3("右") {
@Override
public void show() {
System.out.println("我是右面");
}
};
/**
* 加入一个抽象方法.在定义枚举时,必须实现
*/
public abstract void show();
private String name;
private Direction3(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
2.4 测试
/**
* 枚举测试
*/
public class DirectionTest {
/**
* 测试自定义枚举1
*/
@Test
public void test1() {
Direction sbehind = Direction.BEHIND;
}
/**
* 测试自定义枚举2
*/
@Test
public void test2() {
//测试2
Direction2 behind = Direction2.BEHIND;
//后
System.out.println(behind.getName());
}
/**
* 测试自定义枚举3
*/
@Test
public void test3() {
//测试3
Direction3 d2 = Direction3.FRONT;
System.out.println(d2.getName());
d2.show();
}
}
3 JDK提供的枚举类
我们发现发现自己定义一个枚举类,比较麻烦,所以,JDK1.5开始就提供了枚举类供我们使用。
格式是:只有枚举项的枚举类
public enum 枚举类名 { 枚举项1,枚举项2,枚举项3…; }
3.1 第一种
public enum DirectionEnum1 {
/**
* 同第一种自定义枚举,不过构造器默认私有,成员默认public static final修饰,更加方便
*/
FRONT, BEHIND, LEFT, RIGHT
}
3.2 第二种
public enum DirectionEnum2 {
/**
* 同第二种自定义枚举,构造器默认私有,成员默认public static final修饰
*/
FRONT("前"), BEHIND("后"), LEFT("左"), RIGHT("右");
/**
* 可以有参数
*/
private String name;
/**
* 这里构造函数实际上默认是私有的
*
* @param name
*/
DirectionEnum2(String name) {
this.name = name;
}
/**
* 可以有方法
*
* @return
*/
public String getName() {
return name;
}
}
3.3 第三种
public enum DirectionEnum3 {
/**
* 同第三种自定义枚举,构造器默认私有,成员默认public static final修饰
* 抽象方法在枚举实例中必须实现
*/
FRONT("前") {
@Override
public void show() {
System.out.println("前面");
}
}, BEHIND("后") {
@Override
public void show() {
System.out.println("后面");
}
}, LEFT("左") {
@Override
public void show() {
System.out.println("左面");
}
}, RIGHT("右") {
@Override
public void show() {
System.out.println("右面");
}
};
private String name;
/**
* 这里构造函数实际上默认是私有的
*
* @param name
*/
DirectionEnum3(String name) {
this.name = name;
}
public String getName() {
return name;
}
/**
* 可以有抽象方法
*/
public abstract void show();
}
3.4 测试
/**
* 测试enum枚举1
*/
@Test
public void test4() {
//测试enum1
DirectionEnum1 front = DirectionEnum1.FRONT;
}
/**
* 测试enum枚举2
*/
@Test
public void test5() {
//测试enum2
DirectionEnum2 front = DirectionEnum2.FRONT;
System.out.println(front.getName());
}
/**
* 测试enum枚举3
*/
@Test
public void test6() {
//测试enum3
DirectionEnum3 front = DirectionEnum3.FRONT;
System.out.println(front.getName());
front.show();
}
4 注意事项
- 定义枚举类要用关键字enum
- 所有枚举类都是Enum的子类
- 枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的东西,这个分号就不能省略。建议不要省略
- 枚举类可以有带参构造器,但必须是private的,它默认的也是private的,同时带参枚举项的用法比较特殊:枚举项(实参);
- 枚举类也可以有抽象方法,但是枚举项必须重写该方法
- 枚举在switch语句中的使用(jdk1.5后),案例在下面。
- 枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的。因为枚举类Enum已经重写了equals方法
public final boolean equals(Object other) {
return this==other;
}
4.1 Switch中的枚举
Switch之前只能判断byte 、short、 char、 int;在jdk1.5后支持判断enum枚举;在jdk1.7后支持判断String。
Case后直接放枚举选项而不是通过类名.调用。
@Test
public void test7() {
DirectionEnum1 de = DirectionEnum1.BEHIND;
// de = DirectionEnum1.FRONT;
switch (de) {
case FRONT:
System.out.println("你选择了前");
break;
case BEHIND:
System.out.println("你选择了后");
break;
case LEFT:
System.out.println("你选择了左");
break;
case RIGHT:
System.out.println("你选择了右");
break;
default:
}
}
5 Enum常用API方法
public abstract class Enum<E extends Enum> extends Object implements Comparable, Serializable
位于java.lang.Enum包,JDK1.4。是所有 Java 语言枚举类型的公共基本类。
5.1 public final int compareTo(E o)
比较此枚举与指定对象的顺序。在该对象小于、等于或大于指定对象时,分别返回负整数、零或正整数。 枚举常量只能与相同枚举类型的其他枚举常量进行比较。该方法实现的自然顺序就是声明常量的顺序。
案例:下列枚举声明顺序为: FRONT, BEHIND, LEFT, RIGHT
public class EnumApi {
@Test
public void test1() {
DirectionEnum1 front = DirectionEnum1.FRONT;
DirectionEnum1 behind = DirectionEnum1.BEHIND;
DirectionEnum1 left = DirectionEnum1.LEFT;
DirectionEnum1 right = DirectionEnum1.RIGHT;
System.out.println(front.compareTo(front)); //0
System.out.println(behind.compareTo(front)); //1
System.out.println(left.compareTo(front)); //2
System.out.println(right.compareTo(front)); //3
System.out.println(behind.compareTo(right)); //-2
}
}
5.2 public final String name()
返回此枚举常量的名称,在其枚举声明中对其进行声明。 与此方法相比,大多数程序员应该优先考虑使用 toString() 方法,因为 toString 方法返回更加用户友好的名称。该方法主要设计用于特殊情形,其正确性取决于获取正确的名称,其名称不会随版本的改变而改变。
System.out.println(front.name()); //FRONT
System.out.println(behind.name()); //BEHIND
System.out.println(left.name()); //LEFT
System.out.println(right.name()); //RIGHT
5.3 其他方法
public final int ordinal()
返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)。 大多数程序员不会使用此方法。它被设计用于复杂的基于枚举的数据结构,比如 EnumSet 和 EnumMap。
以上枚举的常量序数为:0,1,2,3
public String toString()
返回枚举常量的名称,它包含在声明中。可以重写此方法。
public static <T extends Enum> T valueOf(Class enumType, String name)
返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。(不允许使用额外的空白字符。)
DirectionEnum2 behind1 =
DirectionEnum2.valueOf(DirectionEnum2.class, "BEHIND");
System.out.println(behind1.getName()); //后
static values()
此方法虽然在JDK文档中查找不到,但每个枚举类都具有该方法,它遍历枚举类的所有枚举值非常方便。这个方法是在编译其间虚拟机加的。
DirectionEnum2[] values = DirectionEnum2.values();
for (DirectionEnum2 value : values) {
System.out.print(value.getName()); //前 后 左 右
}
6 枚举的原理
枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法。
而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。
另外通过继承的Enum类中把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。可以利用枚举的这两个特性来实现线程安全的单例。
反编译后的枚举类:
public final class DirectionEnum1 extends Enum
{
public static final DirectionEnum1 FRONT;
public static final DirectionEnum1 BEHIND;
public static final DirectionEnum1 LEFT;
public static final DirectionEnum1 RIGHT;
private static final DirectionEnum1 $VALUES[];
public static DirectionEnum1[] values()
{
return (DirectionEnum1[])$VALUES.clone();
}
public static DirectionEnum1 valueOf(String name)
{
return (DirectionEnum1)Enum.valueOf(com/ikang/enumm/DirectionEnum1, name);
}
private DirectionEnum1(String s, int i)
{
super(s, i);
}
static
{
FRONT = new DirectionEnum1("FRONT", 0);
BEHIND = new DirectionEnum1("BEHIND", 1);
LEFT = new DirectionEnum1("LEFT", 2);
RIGHT = new DirectionEnum1("RIGHT", 3);
$VALUES = (new DirectionEnum1[] {
FRONT, BEHIND, LEFT, RIGHT
});
}
}
继承的Enum类的方法
/**
* 防止反序列化,序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
//防止克隆
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
下一篇: 枚举
推荐阅读
-
深入了解 Java 枚举 枚举类型的用法
-
[Java 小家]深入了解 Java 枚举类型(枚举)和 7 种常用方法(包括 EnumMap 和 EnumSet)
-
深入了解 Java 枚举类型(枚举)
-
深入了解 Java 枚举类型(枚举)--枚举和开关
-
深入了解 Java 枚举
-
深入了解 Java 枚举及其实现方式
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
深入了解 Java 类加载器及其在 JavaAgent 中的应用
-
深入了解 Java 类加载器及其在 JavaAgent 中的应用
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)