`
120153216
  • 浏览: 62008 次
  • 性别: Icon_minigender_1
  • 来自: 长沙
社区版块
存档分类
最新评论

Enum 枚举

    博客分类:
  • java
阅读更多

原文地址:http://www.cnblogs.com/Kavlez/p/4268601.html

Enumeration

于Java 1.5增加的enum type...
enum type是由一组固定的常量组成的类型,比如四个季节、扑克花色。
在出现enum type之前,通常用一组int常量表示枚举类型。
比如这样:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;


如果只是想用作枚举,感觉这样也没什么。
但如果把上面的苹果和橘子互作比较,或者写成....

int i = (APPLE_FUJI - ORANGE_TEMPLE) / APPLE_PIPPIN;

虽合法但诧异,这是在做果汁吗?


而且,这种常量是compile-time常量,编译后一切都结束了,使用这个常量的地方都被替换为该常量的值。
如果该常量值需要改变,所有使用该常量的代码都必须重新编译。
更糟糕的情况是,不重新编译也可以正常运行,只不过会得到无法预测的结果。
(ps:我觉得更遭的是有人直接把常量值写到代码里...)


另外,比如上面的APPLE_FUJI,我想打印它的名字,不是它的值。
不仅如此,我还想打印所有苹果,我想打印苹果一共有多少种类。
当然,如果想打印也可以,只是相比直接使用enum,无论怎么做都很麻烦。


如果使用enum,比如:

public enum Apple  { FUJI, PIPPIN, GRANNY_SMITH } 
public enum Orange { NAVEL, TEMPLE, BLOOD }


看起来就是一堆常量,但是enum没有实例,也没有可访问的构造器,无法对其进行扩展。
enum本身就是final,所以很多时候也直接用enum实现singleton。
enum在编译时是类型安全的,比如有地方声明了上面代码中的Apple类型的参数,那么被传到该参数的引用肯定是三种苹果之一。
而且enum本身就是一个类型,可以有自己的方法和field,而且可以实现接口。


附上书中太阳系enum,很难想象如果有类似需求时用普通常量来实现。
也许我可以声明一个Planet类,再给它加上field的方法,然后在一个constant类中声明为final 但这样却无法保证Planet类仅用作常量,所以还是用enum吧:

public enum Planet {
    MERCURY(3.302e+23, 2.439e6), VENUS(4.869e+24, 6.052e6), EARTH(5.975e+24,
            6.378e6), MARS(6.419e+23, 3.393e6), JUPITER(1.899e+27, 7.149e7), SATURN(
            5.685e+26, 6.027e7), URANUS(8.683e+25, 2.556e7), NEPTUNE(1.024e+26,
            2.477e7);
    private final double mass; // In kilograms
    private final double radius; // In meters
    private final double surfaceGravity; // In m / s^2

    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;

    // Constructor
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() {
        return mass;
    }

    public double radius() {
        return radius;
    }

    public double surfaceGravity() {
        return surfaceGravity;
    }

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity; // F = ma
    }
}


然后我们就可以这样使用Planet enum,无论是值还是名字,使用起来都很自然:

public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p :  Planet.values())
            System.out.printf("Weight on %s is %f%n",p, p.surfaceWeight(mass));
    }
}


其实像Planet这样的方式对多数使用枚举的场景而言足够了。
也就是说每个Planet常量表达的是不同的数据,但也有例外。
比如,我们要为enum中的每一个常量赋予不同的行为。
下面是书中用enum表达计算的例子:

import java.util.HashMap;
import java.util.Map;

public enum Operation {
    PLUS("+") {
        double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        double apply(double x, double y) {
            return x / y;
        }
    };
    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

    abstract double apply(double x, double y);

    private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>();
    static { 
        for (Operation op : values())
            stringToEnum.put(op.toString(), op);
    }


    public static Operation fromString(String symbol) {
        return stringToEnum.get(symbol);
    }


    public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
    }
}


对不同的枚举常量进行switch..case..其实也能表达出我们想要的效果。
如果以后增加了新的常量则需要再对应加上一个case,当然,不加也不会有任何提示,然后最坏的情况就是运行时出了问题。
如上面的代码是常量行为的正确使用方法,即constant-specific method implementation。
为行为提供一个抽象,并为每一个常量提供一个实现,即一个枚举常量也是constant-specific class body。
采用这种方式时,如果新增一个常量,则必须提供一个方法实现,否则编译器会给出提示,这就多了一层保障。


遗憾的是,这种方式也有缺陷。
比如我们有这样一个需求,计算某一天的薪水,这个某一天可以是一周中的某一天,也可能是某个节日,比如周一到周五使用相同的运算方式,周末另算,某节日另算。
也就是说我需要在枚举中声明代表周一到周日的常量,如果我继续使用之前的方式去声明一个抽象方法,如果周一到周五采用完全一样的计算,则会出现五段完全相同的代码。
但即使这样我们也不能用回switch..case..方式,增加一个常量时强制选择其选择一种行为实现是必须的。
于是我们有一种叫strategy enum的方式,即枚举中声明另外一个枚举的field,该field则代表策略,并提供策略相关的行为。
下面是书中代码:

enum PayrollDay {
    MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(
            PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(
            PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) {
        this.payType = payType;
    }

    double pay(double hoursWorked, double payRate) {
        return payType.pay(hoursWorked, payRate);
    }

    private enum PayType {
        WEEKDAY {
            double overtimePay(double hours, double payRate) {
                return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT)
                        * payRate / 2;
            }
        },
        WEEKEND {
            double overtimePay(double hours, double payRate) {
                return hours * payRate / 2;
            }
        };
        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hrs, double payRate);

        double pay(double hoursWorked, double payRate) {
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked, payRate);
        }
    }
}

 

Annotation

在Java 1.5之前时常有这样的情况,通过为程序元素进行特殊的命名以提供特殊的功能,比如JUnit中测试方法必须为test开头。
当然,这种方式在某种程度上确实可行,但不够优雅。
比如:

  • 错误的文字拼写并不会有任何提示,直到运行时才会发现出了问题。
  • 其次,这种方式无法特指某个程序元素,比如用户将某个类名的开头做了特殊命名,希望作用于类中所有的方法,结果可能没有提示、没有效果、没有意义。
  • 而且,这种方式太单调,比如我想和某个方法的参数或者和声明抛出的异常进行交互。当然,反射也可以,但问题是我如何在不知道用户行为的情况下提供反射方法。


平时工作很少提供过注解,大多数情况都是使用别人提供的注解。
没想过没有注解会是什么样子,但和naming pattern一比较发现确实太重要了。
比如在下面的例子,声明一个注解用于表示测试方法:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
    //..
}


对于代码中的retention和target,我们有专门的术语叫做"元注解(meta-annotation)"。
而对于这种没有参数,仅仅标注程序元素的注解,我们称作"标记注解(marker annotation)"。


如果需要给注解声明参数并不复杂,只是相当于给一个类添加实例field。
如下代码,表示测试时发生异常数组中的异常时进行通过:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}


当然,注解本身对程序元素并没有直接的影响,它无法改变代码本身的语义。
我们需要依赖于特定的注解处理类。
当然,并不是一个注解就对应一个处理类,一个处理类也可以处理很多种注解。
比如下面的代码为Test和ExceptionTest提供了处理:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("INVALID @Test: " + m);
                }
            }

            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (Throwable wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    Class<? extends Exception>[] excTypes = m.getAnnotation(
                            ExceptionTest.class).value();
                    int oldPassed = passed;
                    for (Class<? extends Exception> excType : excTypes) {
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed)
                        System.out.printf("Test %s failed: %s %n", m, exc);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}


代码就不多做解释了,主要是通过反射判断注解和获取异常。


其实标记注解非常常见,但说到标记注解就不得不说标记接口,比如Serializable什么的仅仅是作为注明。
相比接口只能在类名后面加上implements,注解可以作用于更多的程序元素。于是便得出结论,标记接口可以淘汰了?
但这样过于片面。


首先,被接口标记的类提供该接口的实现,而这一点是注解无法做到的,就算有处理类进行补助也无法成为一种约束。
就Serializable而言,如果被标记的类没有提供实现,ObjectOutputStream.write(Object)则毫无意义。
另外,这个接口有点特殊,它确实是一种约束,但在编译期没给出警告。
我之前以为write方法没有定义在Serializable中可能有什么特殊意义,但作者原话是:

Inexplicably, the authors of the ObjectOutputStream API did not take advantage of the Serializable interface in declaring the write method.

可见他也不知道其中的意义,既然如此,我们也不仿效这种作法了吧。


第二点是接口标记地更加精确。
乍一看似乎有些矛盾,相比接口只能作用于类元素,注解可以作用于多种元素不是注解的优点吗?
其实作者表达的并不是这个观点,就一个接口和Target为ElementType.Type的注解而言,后者可以作用于任何类和接口。


作者用Set接口进行了说明,Set这种情况有些特殊,Set继承了Collection接口。
乍一看,Set似乎不是一个标记接口,它声明了太多方法。
参考:

The Set interface places additional stipulations, beyond those inherited from the Collection interface, on the contracts of all constructors and on the contracts of the addequals and hashCode methods. Declarations for other inherited methods are also included here for convenience. (The specifications accompanying these declarations have been tailored to the Set interface, but they do not contain any additional stipulations.)

但作者将其描述为"a restricted marker interface",它声明的方法与Collection接口是相同的。
Set并没有改进Collection的契约,只是为实现类多提供了一种抽象描述。


但即便如此,也不能把注解设计成至少有一个参数的形式。
首先不得不承认,能标记的类型比接口更多,这个确实是一个优势。
另外,在一个类中,同一种标记注解可以出现多次,这一点也是其优势。
而最重要的,相比接口这种约定(即,声明后被一些类提供了实现,在后期版本中很难修改这个接口),注解则可以在后期变得更丰富。

分享到:
评论

相关推荐

    Java SE程序 Enum枚举类

    Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类Java SE程序 Enum枚举类...

    java enum 枚举 学习资料

    "Java Enum 枚举学习资料" Java Enum 枚举学习资料是 Java 编程语言中的一种特殊类型,它主要用来实现一组固定的常量。 Enum 枚举类型是 Java 5 中引入的一种新特性,旨在解决传统的 int 枚举常量的缺陷。 1. 枚举...

    java中enum枚举的详细用法

    Java中的枚举(enum)类型是在JDK 1.5版本引入的一个强大的特性,它用于定义一组相关的常量。在C/C++等语言中,枚举通常用来表示一组具有固定值的常量集合,但在Java中,枚举不仅限于此,它提供了更多的功能和灵活性...

    enum枚举的方法

    enum枚举的方法

    7enum枚举字函数程序案例VC共4页.pdf.zip

    【7enum枚举字函数程序案例VC共4页.pdf】 这个压缩包文件包含了一个关于“7enum枚举字函数”的程序案例,适用于Visual C++(VC)开发环境。7enum,顾名思义,可能是一种自定义的枚举类型或者一个特定的枚举工具,...

    Java中的Enum枚举用法.doc

    ### Java中的Enum枚举用法详解 #### 一、引言 在Java编程语言中,枚举(Enum)是一种特殊的数据类型,它被用来表示一组固定的常量集合。相比于使用常量接口或字符串来表示固定集合,枚举提供了更加安全且易于管理的...

    10-enum枚举.ts

    10-enum枚举

    java enum 枚举的spring boot2.x完美实现demo源码

    在Java编程语言中,枚举(`enum`)是一种强大的工具,它允许我们定义一组预定义的常量。在Spring Boot 2.x框架中,枚举的使用可以帮助我们更好地管理和组织代码,提供类型安全和可读性。本篇将深入探讨如何在Spring ...

    java简单的 enum枚举类介绍

    java简单的 enum枚举类介绍

    JAVA高级编程中Enum枚举

    在Java编程语言中,`Enum`(枚举)是一种特殊的类类型,用于定义固定的、预定义的常量集合。它们在程序设计中扮演着重要的角色,特别是在需要表示一组固定值时,如状态、颜色、星期等。本资源包提供了一些关于`Enum`...

    enum枚举类的应用

    包含了关于枚举类的应用,以及枚举类和接口的组合应用,扩展映射关系。很有用的工具类。

    C# C++ enum 枚举 例子.rar

    enum day {Sun,Mon,Tue,Wed,Thu,Fri,Sat}; 默认情况下,枚举符的值从0开始,其后值总是前面一个+1。 即Sun=0,Mon=1,Tue=2,Wed=3,Thu=4,Fri=5,Sat=6 也可以自己定义数值,其后值总是前面一个+1 例如: enum day {Sun=...

    java enum枚举教程学习资料

    Java枚举(enum)是Java语言提供的一种特殊的数据类型,用于定义一组有限的常量,这些常量在程序中作为固定的值使用。枚举在Java中是类的子类型,因此,它们具备类的一些特性,比如可以有方法、变量以及实现接口。...

    C语言enum枚举类型解析共13页.pdf.zip

    枚举类型以`enum`关键字开始,后跟枚举名,然后是花括号{},里面列举出枚举常量。例如: ```c enum Color {Red, Green, Blue}; ``` 这里定义了一个名为`Color`的枚举类型,包含三个枚举常量:Red、Green和Blue...

    enum枚举类的一些代码demo

    在Java编程语言中,`enum`(枚举)是一种特殊的类,用于定义一组固定的常量。枚举在很多场景下非常有用,例如表示状态、颜色、星期等有限且不变的集合。下面我们将深入探讨`enum`枚举类的使用方法,并通过具体的代码...

    python模拟enum枚举类型的方法小结

    本文实例总结了python模拟enum枚举类型的方法。分享给大家供大家参考。具体分析如下: python中没有enum枚举类型,可能python认为这玩意压根就没用,下面列举了三种方法模拟enum枚举类型 方法1. 使用自定义类 class...

    Python中模拟enum枚举类型的5种方法分享

    以下几种方法来模拟enum:(感觉方法一简单实用) 复制代码 代码如下: # way1 class Directions:  up = 0  down = 1  left = 2  right =3   print Directions.down # way2 dirUp, dirDown, dirLeft, dirRight =...

    C#利用enum枚举类型写发牌系统

    给刚学C#的同学一些帮助 public enum CardColor : byte { 红桃, 方片, 梅花, 黑桃, }

Global site tag (gtag.js) - Google Analytics