说说枚举

关于前几天

好几天都没写博客了,这不是因为这几天国庆放假都在宿舍里面睡觉,而是前几次的研究竟然是研究到了爬虫的领域。导致我又燃起了学习python的欲望,结果学了不少有关python爬虫的内容。本来只是 打算简单的学习一下的,但是我要爬取的网站比如说半次元这个网站,使用的是异步加载的机制(当时我还不知道啥叫ajaxjson的异步加载的机制,也不知道这个网站竟然将json文件藏在了js文件中),所以我就进一步的进行了有关python爬虫的学习,以至于学到了后面的selenuim自动化测试的软件,这还不是最骚的。然后我还是进行了学习,直接学习到了python的爬虫的大型的框架scrapy。学到了这个地方我就感觉应该是比较困难花时间的了,于是简单的学习了一下就放弃了。至于为什么之前学习的python爬虫的内容没有写博家,那当然是因为我对这方面的知识也是一知半解的,根本就搞不透,现在也不想花时间在这个上面,所以说等之后可以花时间研究这个方面上。现阶段还是一学习java为重。

前言

枚举这个玩意其实在我而已是蛮没用的东西。从c/c++开始,我们就接触到了枚举这个玩意,不过事实上,我从来都没有使用过枚举这个玩意。我们或许是使用#define xxx xxx或许是使用const xxx xxx = xxx。很少有人去使用枚举这个玩意的。毕竟在c/c++中,枚举也就是int值,我们既然可以使用枚举,也就是可以使用int值来进行替代。而且,不同的枚举之间都是可以比较的,看起来确实是有点儿荒唐。不过Java中的枚举似乎是和我们之前学过的有点儿不一样。因为Java中枚举是一个类,这就避免了不同的枚举类之间荒唐的相互赋值,以及int值对枚举的替代的问题了。

Java中的枚举是什么样子的类?

上面我们已经说过了,Java为了避免C/C++那样的问题,将枚举做成了一个类。不过这个枚举到底是一个什么样子的类呢?

我们可以先来定义一个简单的枚举来测试测试。

enum Season{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER
}

使用上面的这种方式,我们就定义了一个季节的枚举。如果这是c++的话,他们四个也就是对应了0 1 2 3。那么在Java中我们可不可以指定给他们对应的int值呢?明显是非法的。

enum Season{
    SPRING = 1,
    SUMMER,
    AUTUMN,
    WINTER
}

后面全都报错了,这说明在Java中我们是没有办法给枚举赋值的。

那么这个枚举类是如何使用的呢?这个枚举类中的四个枚举似乎是没有修饰符的,是不是想接口那样,给定了默认的修饰符呢?比如说这些属性默认就是public static final的???

我们先来看一下枚举类的使用

public class Demo1 {
    public static void main(String[] args) {
        Season s1 = Season.SPRING;
        Season s2 = Season.AUTUMN;

        System.out.println(s1 == s2); // false
    }
}

可见我们使用枚举,就像使用枚举类的静态成员变量一样。那么通过上面的代码我们就可以知道了,枚举中的枚举变量的类型应该是public static final Season。至于为什么是final呢?我想这个很容易明白,如果不是final的话。一处被修改了,那么所有的使用这个枚举的类的地方不就都遭殃了吗?而且,似乎我们并没有要修改这个枚举类的必要。

既然我们已经知道了他的限定符了,那么我们是不是就可以想接口那样给他直接加上这些限定符呢??

答案是显然不能的,这些枚举似乎不是想接口哪样的值一个定义。这些SPRING已经是有值的了。不过这写玩意是如何实现的呢?当然不是这个枚举类实现的。枚举是一个类,那么他是否有父类呢?

对了,前面我们使用了==,我们可以使用equals方法看一看。

System.out.println(s1.equals(s2));

使用idea的方法源码查看,发现这个方法是来自java.lang.Enum这个类的,这就说明了,所有的枚举类都是继承于这个类的,不然怎么可以使用这个类的方法?

java.lang.Enum中的方法

还是从官方的文档中看一看这个类。

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

这个类的声明似乎是有点儿奇怪,不过看一下他的构战函数。

protected  Enum(String name, int ordinal) 
// 唯一的构造函数。  

唯一的构造函数都是私有的说明这个类没有办法被继承的(通过extends方式)。enum声明的类的基础肯定是通过底层实现的。

protected Object clone() 
// 抛出CloneNotSupportedException。  
int compareTo(E o) 
// 将此枚举与指定的对象进行比较以进行订购。  
boolean equals(Object other) 
// 如果指定的对象等于此枚举常量,则返回true。  
protected void finalize() 
// 枚举类不能有finalize方法。  
class<E> getDeclaringClass() 
// 返回与此枚举常量的枚举类型相对应的Class对象。  
int hashCode() 
// 返回此枚举常量的哈希码。  
String name() 
// 返回此枚举常量的名称,与其枚举声明中声明的完全相同。  
int ordinal() 
// 返回此枚举常数的序数(其枚举声明中的位置,其中初始常数的序数为零)。  
String toString() 
// 返回声明中包含的此枚举常量的名称。  
static <T extends Enum<T>>
T valueOf(class<T> enumType, String name) 
// 返回具有指定名称的指定枚举类型的枚举常量。  

上面是这个类中的所有的方法。官方文档中对这个类的并没有什么描述,只是说这个类是所有的枚举的公开基类。下面我们就通过上面的这些方法来研究一下Java中枚举。

可以看到上面的方法都是子类中可以使用的。

第二个方法令我比较好奇,既然枚举不和int值挂钩,又是如何实现比较的呢?

System.out.println(s1.compareTo(s2)); // -2

输出-2。嗯,这个倒是和c/c++中一摸一样。不会枚举的内部也是对四个属性进行0 1 2 3这样的排序的吧。瞎的我感觉看了一下这个方法的源码。

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;
}

上面的那么多是用来判断是否是一个枚举类的。不同的枚举类之间是不能够进行比较的。不过关键是最后一行代码。

return self.ordinal - other.ordinal;

ordinal是序数的意思,看来我们猜想的是对的,在枚举的内部也还是将枚举和一个int值挂钩的。我们可以看到方法的列表中也是有int ordinal()方法的。

System.out.println(s1.ordinal()); // 0
System.out.println(s2.ordinal()); // 2

完全符合我们的猜想。也就是这个序数其实就是枚举中声明的序数。不过我们是否是可以改变这个序数呢?就像c/c++中我们做的那样一样。(虽然这个是完全没有一意义的)这个问题我们可以在下面再讨论。

我们继续来看枚举类的方法。

首先一眼看到的方法就是toString()方法。这个方法是Object中就有的,声明如下

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

枚举类中将这个方法从重载了,我们可以实验一下会输出什么东西。

System.out.println(s1); // SPRING

输入的就是我们给定的枚举类的名字。这又是如何实现的呢?再来看一下这个方法的源码的实现。

public String toString() {
    return name;
}

上面也有一个方法叫name()看来和ordinal一样这个属性也被枚举类给保存下来了。

还有一个方法是我们需要了解的。valueOf

System.out.println(Season.valueOf("SPRING") == Season.SPRING); // true

作用是返回和这个名字相同的枚举变量。上面我们已经知道了,这个名字其实就是name属性。这里的比较使用的==,其实使用equals()也是一样的。毕竟这个枚举的都是唯一实例,无论是比较内容还是地址都是一样的。何况这个类中的equals()方法都相当于没有重载。==equals是一样的。

public final boolean equals(Object other) {
    return this==other;
}

其实还有一个很重要的方法,是父类没有的。就是values()方法。这个方法的作用是返回枚举类中的所有的枚举变量。

Season[] seasons = Season.values();
for (Season season : seasons) {
    System.out.println(season);
}
// SPRING
// SUMMER
// AUTUMN
// WINTER

根据推测方法的声明可能是这样子的。

public Season[] values();

数组中枚举的顺序就是他们ordinal的顺序。

这让我想到一个问题,既然valueOf方法可以通过name属性获取到枚举值,我们是否可以通过ordinal属性来获取属性的值呢?很遗憾,Java中并没有提供这样的函数,不过我们可以通过values方法来写一个这样的方法。

枚举中的方法

其实上面我们一直在谈的都是枚举中的方法。不过我们似乎谈论的大多是枚举类中自带的方法。那我们是否可以在其中写一些方法呢?当然是可以的。比如上面我们说到的通过ordinal属性来获取枚举值。

enum Season{
    SPRING,
    SUMMER,
    AUTUMN,
    WINTER; // there is ;

    public static Season valueOf(int ordinal) {
        Season[] seasons = values();
        if (ordinal < 0 || ordinal >= seasons.length) {
            throw new IndexOutOfBoundsException("Ordinal is out of range!");
        }
        return seasons[ordinal];
    }
}

第一个点需要注意的是枚举变量之间使用的逗号相隔。如果我们要在枚举中写方法的时候,我们需要使用一个分号想方法和定义的枚举型变量隔开。

上面就像使用类一样定义了一个方法。而且经过测验之后也是满足我们的需求的。既然我们可以在枚举类中定义自己的方法。那么自己的属性也必然是可以的。只要在分号的后面就可以了。不仅如此,我们还可以重载枚举中的方法。比如toString()方法。

@Override
public String toString() {
    return "my toString: " + super.toString();
}

这样我们便overridetoString方法。

现在我们再回到上面的问题来,valueOf(int ordinal)方法对不对呢?看起来是对的。因为ordinal就是从0开始的。在java.lang.Enum中,ordinal的定义如下

private final int ordinal;

也就是ordinal一经初始化之后便不可以被修改了。不过我们是否可以操作一下ordinal的初始化呢?

枚举变量的构造函数

前面我们一直都忽略了一个细节,那就是java.lang.Enum的构造函数。我们继续来观察一下。

protected  Enum(String name, int ordinal) 
// 唯一的构造函数。  

这个构造函数有两个参数nameordinal。嗯?是不是有点儿熟悉~~那么我们是否可以通过构造函数指定枚举变量的nameordinal呢?

看起来是不可以的,我们似乎都接触不到这个构造函数。而且ordinaljava.lang.Enum中的声明是私有的,我们更不可能修改到这个ordinal属性了。不过我们是否是可以修改name属性呢?相同的,我们也是不可以的。因为没有构造函数,属性还是私有的。可见这个方法是正确的。

不过枚举类是否可以拥有自己的构造函数呢?

private String name;
private int ordinal;
Season(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

我们发现是真的可以的,构造函数并没有报错,但是上面的那些枚举变量却报错了。经过查阅资料,我们需要使用如下的形式来调用构造函数。

SPRING("春天",1),
SUMMER("夏天",2),
AUTUMN("秋天",3),
WINTER("冬天", 4);

原来如此,如果我们在后面没有使用括号,也就是一开始的写法的话就是调用无参的构造函数,如果有参数的话就会寻找相应的参数的构造函数。也就是说其实枚举就是这样的一个普通的类。

class Season extends java.lang.Enum{
    public static final Season SPRING = new Season("春天",1);
    public static final Season SUMMER = new Season("夏天",2);
    public static final Season AUTUMN = new Season("秋天",3);
    public static final Season WINTER = new Season("冬天",4);

    private String name;
    private int ordinal;
    Season(String name, int ordinal){
        this.name = name;
        this.ordinal = ordinal;
    }
}

上面只是实现的一个例子,我们自然是不可以写出这样的类的。首先继承java.lang.Enum就是一个不可能的事情。不过上面的代码清晰的表现了枚举类的简单的原理。这里我没有使用反编译。毕竟我也不会反编译的手段。不过不使用反编译,我们也可以大概猜出枚举类的结构了。

不过,还有一个非常大的问题。上面的枚举类中我没有使用限定符access modifier。没有使用限定符那么就是default,也就是包可访问的。那么我们可以在类外使用枚举类的构造函数?

Season s = new Season("xxx",5);

报错了,报错信息显示这个构造函数是access private的,这里不可以访问。

可见,枚举类的构造函数必然是private。就比如接口中的函数,即使是什么限定符都不加,他也是public abstract的,这里的构造函数如是。

枚举的继承以及枚举中的抽象函数

枚举是继承java.lang.Enum的。这就决定了枚举是不可以继承其他类的,同样的其他类也是不可以继承枚举的。(至于为什么,这不用说也应该可以明白吧)。

不过,虽然枚举类不可以被继承,但是枚举类中,还是允许存在抽象的方法的。这就很令人不解了?既然不可以被继承,那么抽象方法的意义何在呢?

当我们写下了这个抽象方法的时候,编译器给了一个提示。

class ”Season“ must either be decleared abstrace or implememt abstract method “func” in Season

不过当我使用abstract enum Season的时候他说abstract is not allowed here那这个的意思是自己要实现自己定义的抽象的方法。这也太没道理了吧。

不过更没想到的是,原来是需要这样去实现抽象方法的。

SPRING("春天",1){
    @Override
    public void func() {
        System.out.println("Season.func");
    }
},
SUMMER("夏天",2){
    @Override
    public void func() {
        System.out.println("Season.func");
    }
},
AUTUMN("秋天",3) {
    @Override
    public void func() {
        System.out.println("Season.func");
    }
},
WINTER("冬天", 4) {
    @Override
    public void func() {
        System.out.println("Season.func");
    }
};

也就是说需要枚举类的属性来实现这个抽象的方法。这个形式,是不是有一点眼熟,没错就是匿名内部类的格式。这些函数在外面都是可以调用的。比如说Season.SPRING.func()

这个东西之前我们说过的其实匿名函数实现接口,内部是产生了一个类。那么上面的实现抽象函数的过程中是否产生了新的类呢

我们可以在func()使用getClass()方法。

class com.sher.enumDemo.Season$1

看来果然是产生一个新的类。哇~,这个枚举中的奥秘也太多了吧。

System.out.println(Season.SPRING.getClass());
// class com.sher.enumDemo.Season$1

System.out.println(Season.AUTUMN.getClass());
// class com.sher.enumDemo.Season$3

在我们之前没有使用抽象函数,以及实现的时候。使用上面的函数输出是

System.out.println(Season.SPRING.getClass());
// class com.sher.enumDemo.Season

System.out.println(Season.AUTUMN.getClass());
// class com.sher.enumDemo.Season

正是由于实现了抽象函数所以才产生新的类的。不过这里又来了一个问题。Season.SPRINGSeason.AUTUMN的类型不一样了,一个是Season$1,一个是Season$3。按照我们之前的说法,不同的类之间是不可以比较的,因为他们都不是同一个枚举类了。之前似乎说过了一个函数compareTo(),还看过了这个的源码,现在我们在来看一看。

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;
}

上面判断的时候,使用了getDeclaringClass()这个函数。而Season$!Season$3DeclaringClass都是Season,原来是用这种方式保证枚举的相同类型的。所以说,如果我们要看枚举类型是否相同,其实看getDeclaringClass()就行了,毕竟如果getClass()想同,getDeclaringClass()也是相同的。

枚举与接口

虽然我们说由于Java中单继承的机制,枚举是无法继承其他的类的,但是Java中对与接口的实现是没有限制的,所以说枚举类还可以实现接口。不过这里需要注意的是,并不是枚举这个类要实现接口中的函数,而是枚举中每一个枚举变量要实现接口中的函数。比如说有一个接口A

interface A{
    void aFunc();
}
enum Season implements A{
    SPRING("春天",1){
        @Override
        public void aFunc() {
            System.out.println("Season.aFunc");
        }

        @Override
        public void func() {
            System.out.println("Season.func");
            System.out.println(this.getClass());
        }
    },
    SUMMER("夏天",2){
        @Override
        public void aFunc() {
            System.out.println("Season.aFunc");
        }

        @Override
        public void func() {
            System.out.println("Season.func");
        }
    },
    // ...

可见枚举中接口的实现是和普通的类不一样的。

除此之外,枚举和接口还有另外的一个搭配。那就是在接口中组织枚举。

这个挺起来似乎是有点儿荒唐,接口中怎么能有枚举呢?不过确实如此,可以有这样的语法。

如果我们需要将枚举类进行分组的话就可以使用这种方式。比如食物是一个枚举中的大类别,其中又可以分为甜的,酸的,辣的等等等。又比如说班级中中国学生,也有外国学生,我们想要保留他们都是学生的这个共同属性而又想要分别枚举就可以使用接口来组织枚举。

interface Student {
    enum ChineseStudent implements Student {
        ZHANG3(1),
        LI3(2),
        WANG5(3);

        @Override
        public boolean same(Student s) {
            if (s instanceof ChineseStudent) {
                return true;
            }else {
                return false;
            }
        }

        ChineseStudent(int a){

        }

        @Override
        public void printName() {
            System.out.println(this.name());
        }

    };

    enum ExchangeStudent implements Student {
        JACK,
        MIKE,
        TOM;

        @Override
        public boolean same(Student s) {
            if (s instanceof ExchangeStudent) {
                return true;
            }else {
                return false;
            }
        }

        @Override
        public void printName(){
            System.out.println(this.name());
        }

    }

    boolean same(Student s);
    void printName();
}

其中接口中的方法,每一个枚举类中都要实现。这里的接口实现和枚举类实现接口那样,不再是每一个属性都要实现了。通过这种方式,中国学生和交换什都保有了Student这样的一个共同的属性,而且还是分别的枚举了。

和普通的接口一样,这个接口也可以被实现。不过只需要实现接口中的那两个函数就行了。

class B implements Student {

    @Override
    public boolean same(Student s) {
        return false;
    }

    @Override
    public void printName() {

    }
}

不过需要注意的是,这时的枚举可以通过上面的类B访问到。比如

Student s = B.ChineseStudent.ZHANG3;

上面就是使用接口来组织枚举的简单的用法,至于更复杂的用法,这里暂时就不做更多的了解了。

枚举与单例模式

前面我们介绍了单例模式,在我们学习内部类的时候,我也提到了使用静态内部类的方式来实现单例模式的懒加载。不过在我们详细的谈论单例模式的时候,我们还谈到了一种使用枚举来实现的单例模式。

public class SingletonTest04 {
    public static void main(String[] args) {
        Singleton04 singleton1 = Singleton04.INSTANCE;
        Singleton04 singleton2 = Singleton04.INSTANCE;

        System.out.println("singleton1:" + singleton1.hashCode());
        System.out.println("singleton2:" + singleton2.hashCode());
    }
}

enum Singleton04 {
    INSTANCE;
}

这种方式是真的简单又粗暴。不仅可以解决多线程的同步问题,也可以防止反序列化创建新的对象。这种方式也是《Effect Java》中推荐的方式。不过这个代码也是有一定的问题的,比如说无法进行懒加载。如果一点问题都没有,这个单例模式可不是十全十美了嘛~~

总结

上面算是对Java中的枚举类的一个比较全面的一个学习了。不过都基本上是简单的介绍,并没有深入底层追究一些细节上面的东西,毕竟那玩意放到现在来说确实是有点儿复杂了。

主要参考文章:重新认识java(十) —- Enum(枚举类)


一枚小菜鸡