欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 枚举类都不知道,还敢说自己会Java?

程序员文章站 2022-06-28 18:46:02
本文主要针对面试中经常被问到的枚举类的热点、难点由浅入深进行深入分析,祝你备战秋招,直通BAT...

       枚举类型是Java 5中新增的特性,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。当需要定义一组常量时,强烈建议使用枚举类。
       使用枚举类的条件:类的对象是有限个,确定的。例如星期类,它的对象只有星期一…星期日七个,而且是确定的,此时就可以把星期类定义为一个枚举类;又例如性别类,它的对象只有男和女两个,而且是确定的,此时同样可以把性别类定义为一个枚举类;还有诸如季节等这种类的对象是有限个,确定的都可以定义为一个枚举类。

1、枚举类的实现

在JDK1.5之前,还没有枚举类型,如果想要使用枚举类需要我们去自定义。在自定义枚举类时需要注意以下几点:
(1)枚举类对象的属性不应允许被改动,所以应该使用 private final 进行修饰;
(2)枚举类使用 private final 修饰的属性应该在构造器中为其赋值;
(3)枚举类的构造器要私有化,保证不能在类的外部创建其对象,否则就不能确定对象的个数;
(4)在枚举类内部创建的枚举类的实例(枚举)对象,要声明为:public static final。

下面就拿季节举例,来自定义一个枚举类。

public class Season {
    //1.声明Season对象的属性,又因为枚举类对象的属性不应允许被改动, 所以应该使用 private final修饰
    //枚举类的使用 private final 修饰的属性应该在构造器中为其赋值
    private final String seasonName;
    private final String seasonDesc;

    //2.私有化构造器,保证不能在类的外部创建其对象,否则就不能确定对象的个数
    private Season(String seasonName,String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    //3.提供当前枚举类的多个枚举对象,又因为枚举类是不可变的常量类,所以需要声明为:public static final
    public static final Season SPRING=new Season("春天","鸟语花香");
    public static final Season SUMMER=new Season("夏天","夏日炎炎");
    public static final Season AUTUMN=new Season("秋天","秋高气爽");
    public static final Season WINNER=new Season("冬天","寒风瑟瑟");

    //其他需求1:获取枚举类对象的属性
    //只需要提供属性的get方法即可,但是不能提供set方法,因为枚举类是不可变的常量类,不能被修改
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    //其他需求2:打印对象,提供toString方法即可
    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}

public class SeasonTest {
    public static void main(String[] args) {
        Season spring = Season.SPRING;
        System.out.println(spring); //Season{seasonName='春天', seasonDesc='鸟语花香'}
    }
}

在JDK 1.5 中新增了enum关键字用于定义枚举类,但是在使用时需要注意以下几点:
(1)使用 enum 定义的枚举类默认继承了 java.lang.Enum类,因此不能再继承其他类;
(2)使用 enum 定义的枚举类默认使用final进行修饰,不可以被继承;(也从侧面说明了它是一个常量类)
(3)枚举类的构造器只能使用 private 权限修饰符;
(4)枚举类的所有实例必须在枚举类中显式列出,多个对象之间使用",“隔开,末尾使用”;"结束。
列出的实例系统会自动添加 public static final 进行修饰;
(5)必须在枚举类的第一行声明枚举类对象;
(6)若枚举类只有一个枚举对象, 则可以作为一种单例模式的实现方式。

下面还是使用季节举例,来自定义一个枚举类。

//使用enum关键字定义枚举类
public enum  Season2 {
    //1.提供当前枚举类的对象,多个对象之间使用","隔开,末尾使用";"结束
    //系统默认使用public static final修饰
    SPRING("春天","鸟语花香"),
    SUMMER("夏天","夏日炎炎"),
    AUTUMN("秋天","秋高气爽"),
    WINNER("冬天","寒风瑟瑟");

    //2.声明Season对象的属性,又因为枚举类对象的属性不应允许被改动, 所以应该使用 private final修饰
    private final String seasonName;
    private final String seasonDesc;

    //3.枚举类的构造器只能使用 private 权限修饰符
    // 私有化构造器是为了保证不能在类的外部创建其对象,否则就不能确定对象的个数
    private Season2(String seasonName, String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    //其他需求:获取枚举类对象的属性
    //只需要提供属性的get方法即可,但是不能提供set方法,而且也不允许提供set方法,因为枚举类是不可变的常量类,不能被修改
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }
}

public class SeasonTest {
    public static void main(String[] args) {
        Season2 spring = Season2.SPRING;
        System.out.println(spring);//SPRING
    }
}

2、Enum类中的常用方法

values()方法:返回枚举类型的对象数组,该方法可以很方便地遍历所有的枚举值;
//使用方法如下:
Season2[] seasons = Season2.values();
for (int i = 0; i < seasons.length; i++) {
    System.out.println(seasons[i]);
}

valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。
如不是,会报运行时异常:IllegalArgumentException;
//使用方法如下:
Season2 spring = Season2.valueOf("SPRING");
System.out.println(spring);//SPRING

toString():返回当前枚举类对象的名称
//使用方法如下:
Season2 spring = Season2.SPRING;
System.out.println(spring.toString());//SPRING

3、使用enum关键字定义枚举类实现接口

枚举类和普通类一样,可以实现一个或多个接口。枚举类实现接口分为两种情况:
情况一:若枚举类的所有枚举对象在调用实现的接口方法时,呈现相同的行为方式,则只要统一实现该方法即可;此时与普通类实现接口一样,没有任何区别。

public interface Show {
    void show();
}

//使用enum关键字定义枚举类
public enum  Season2 implements Show{
    //1.提供当前枚举类的对象,多个对象之间使用","隔开,末尾使用";"结束
    //系统默认使用public static final修饰
    SPRING("春天","鸟语花香"),
    SUMMER("夏天","夏日炎炎"),
    AUTUMN("秋天","秋高气爽"),
    WINNER("冬天","寒风瑟瑟");

    //2.声明Season对象的属性,又因为枚举类对象的属性不应允许被改动, 所以应该使用 private final修饰
    private final String seasonName;
    private final String seasonDesc;

    //3.枚举类的构造器只能使用 private 权限修饰符
    // 私有化构造器是为了保证不能在类的外部创建其对象,否则就不能确定对象的个数
    private Season2(String seasonName, String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    //其他需求:获取枚举类对象的属性
    //只需要提供属性的get方法即可,但是不能提供set方法,而且也不允许提供set方法,因为枚举类是不可变的常量类,不能被修改
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    //重写show()方法,与普通类实现接口一样,没有任何区别
    @Override
    public void show() {
        System.out.println("一年四季:春夏秋冬");
    }
}

public class SeasonTest {
    public static void main(String[] args) {
        Season2 spring = Season2.SPRING;
        spring.show();

        Season2 summer = Season2.SUMMER;
        summer.show();

        Season2 autumn = Season2.AUTUMN;
        autumn.show();

        Season2 winner = Season2.WINNER;
        winner.show();
    }
}

运行结果:
荐
                                                        枚举类都不知道,还敢说自己会Java?
情况二:若枚举类的每个枚举对象在调用实现的接口方法时,需要呈现出不同的行为方式,则可以让每个枚举对象分别来实现该方法。

public interface Show {
    void show();
}

//使用enum关键字定义枚举类
public enum  Season2 implements Show{
    //1.提供当前枚举类的对象,多个对象之间使用","隔开,末尾使用";"结束
    //系统默认使用public static final修饰
    SPRING("春天","鸟语花香"){
        //每个枚举对象分别来实现该方法
        @Override
        public void show() {
            System.out.println("春天是一个鸟语花香的季节!");
        }
    },
    SUMMER("夏天","夏日炎炎"){
        @Override
        public void show() {
            System.out.println("夏天是一个夏日炎炎的季节!");
        }
    },
    AUTUMN("秋天","秋高气爽"){
        @Override
        public void show() {
            System.out.println("秋天是一个秋高气爽的季节!");
        }
    },
    WINNER("冬天","寒风瑟瑟"){
        @Override
        public void show() {
            System.out.println("冬天是一个寒风瑟瑟的季节!");
        }
    };

    //2.声明Season对象的属性,又因为枚举类对象的属性不应允许被改动, 所以应该使用 private final修饰
    private final String seasonName;
    private final String seasonDesc;

    //3.枚举类的构造器只能使用 private 权限修饰符
    // 私有化构造器是为了保证不能在类的外部创建其对象,否则就不能确定对象的个数
    private Season2(String seasonName, String seasonDesc){
        this.seasonName=seasonName;
        this.seasonDesc=seasonDesc;
    }

    //其他需求:获取枚举类对象的属性
    //只需要提供属性的get方法即可,但是不能提供set方法,而且也不允许提供set方法,因为枚举类是不可变的常量类,不能被修改
    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }
}

public class SeasonTest {
    public static void main(String[] args) {
        Season2 spring = Season2.SPRING;
        spring.show();

        Season2 summer = Season2.SUMMER;
        summer.show();

        Season2 autumn = Season2.AUTUMN;
        autumn.show();
        
        Season2 winner = Season2.WINNER;
        winner.show();
    }
}

运行结果:
荐
                                                        枚举类都不知道,还敢说自己会Java?

4、枚举类对switch的语句的影响

Java1.5新增enum关键字的同时,也扩大了switch的语句使用范围。Java1.5之前,switch中的值只能是简单数据类型,比如int、byte、short、char, 有了枚举类型之后,就可以使用枚举类的对象了。同时在switch表达式中使用enum定义的枚举类的对象作为表达式时, case子句可以直接使用枚举对象的名字, 无需添加枚举类作为限定。这样一来,程序的控制选择就变得更加的方便,看下面的例子:

public enum  WeekDay {
    // 定义一周七天的枚举类型
    Monday,Tuesday, Wednesday ,Thursday,Friday,Saturday,Sunday;
}

class Test{
    public static void getDay(WeekDay weekDay){
        switch (weekDay){
            case Monday:
                System.out.println("Today is Monday");
                break;
            case Tuesday:
                System.out.println("Today is Tuesday");
                break;
            case Wednesday:
                System.out.println("Today is Wednesday");
                break;
            case Thursday:
                System.out.println("Today is Thursday");
                break;
            case Friday:
                System.out.println("Today is Friday");
                break;
            case Saturday:
                System.out.println("Today is Saturday");
                break;
            case Sunday:
                System.out.println("Today is Sunday");
                break;
            default:
                System.out.println("data error");
        }
    }

    public static void main(String[] args) {
        WeekDay sunday = WeekDay.Sunday;
        getDay(sunday);
        WeekDay friday = WeekDay.Friday;
        getDay(friday);
    }
}

运行结果:
荐
                                                        枚举类都不知道,还敢说自己会Java?
对于这些枚举的日期,JVM都会在运行期构造成出一个简单的对象实例一一对应。这些对象都有唯一的identity,类似整型数值一样,switch语句就会根据此来identity进行执行跳转。

5、枚举类的线程安全问题

枚举类天生线程就是安全的,下面我们就来进行验证。

先写一个简单的枚举类,还是以季节类为例:

public enum Season {
    SPRING,SUMMER,AUTUMN,WINNER;
}

然后我们使用反编译,看看枚举类代码到底是怎么实现的,反编译后的代码内容如下:
荐
                                                        枚举类都不知道,还敢说自己会Java?

public final class zzuli.edu.Season extends java.lang.Enum<zzuli.edu.Season> {
  public static final zzuli.edu.Season SPRING;
  public static final zzuli.edu.Season SUMMER;
  public static final zzuli.edu.Season AUTUMN;
  public static final zzuli.edu.Season WINNER;
  private static final zzuli.edu.Season[] $VALUES;
  public static zzuli.edu.Season[] values();
  public static zzuli.edu.Season valueOf(java.lang.String);
  private zzuli.edu.Season();
  static {};
}

由上述代码可知,每一个枚举类的枚举对象都是被public static final 进行修饰的,又因为被static修饰的属性在类加载的时候就会被加载,而且只会被加载一次,所以枚举类天生就是线程安全的。

6、枚举类实现单例模式

实现单例模式的方法有很多种,但是使用枚举类实现单例模式是最好、最安全的一种方式,这种方式也是Effective Java作者Josh Bloch 提倡的方式。因为它天生线程安全,不仅能避免多线程同步问题,而且还能防止使用反射重新创建新的对象。

使用枚举类实现单例模式非常简单,如下所示:

public enum  EnumSingle {
    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }
}

下面使用代码进行测试,看创建的对象是否是单例:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
        System.out.println(instance1==instance2);
    }
}

运行结果:
荐
                                                        枚举类都不知道,还敢说自己会Java?
由运行结果可知,成功使用了单例模式。
接下来测试使用反射能不能创建新的实例对象。

先来看一下使用反射创建实例对象newInstance方法的源码:
荐
                                                        枚举类都不知道,还敢说自己会Java?
由newInstance方法的源码可知,反射在通过newInstance方法创建对象时,会先检查该类是否是枚举类,如果是,则会抛出IllegalArgumentException(“Cannot reflectively create enum objects”)异常,导致使用反射创建对象失败。下面我们就来测试一下:

先看一下枚举类的源码是有参构造函数还是无参构造函数,编译后的源码如下:
荐
                                                        枚举类都不知道,还敢说自己会Java?
由源码可知,枚举类的构造函数为无参构造函数,下面就使用反射获取枚举类的无参构造函数,看使用反射是否能创建新的实例对象。

public class Test2 {
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行结果:
荐
                                                        枚举类都不知道,还敢说自己会Java?
由运行抛出的异常可知,并不是我们预期的newInstance方法中的IllegalArgumentException(“Cannot reflectively create enum objects”)异常,而是NoSuchMethodException异常,说明枚举类中并没有无参构造函数,编译后的源码欺骗了我们。

接着我们通过javap反编译看一下枚举类的代码
荐
                                                        枚举类都不知道,还敢说自己会Java?

public final class zzuli.edu.enumTest.EnumSingle extends java.lang.Enum<zzuli.edu.enumTest.EnumSingle> {
  public static final zzuli.edu.enumTest.EnumSingle INSTANCE;
  private static final zzuli.edu.enumTest.EnumSingle[] $VALUES;
  public static zzuli.edu.enumTest.EnumSingle[] values();
  public static zzuli.edu.enumTest.EnumSingle valueOf(java.lang.String);
  private zzuli.edu.enumTest.EnumSingle();
  public zzuli.edu.enumTest.EnumSingle getInstance();
  static {};
}

由上述反编译后的枚举类代码可知,反编译后的枚举类中存在的仍然是无参构造函数,说明反编译后的代码仍然骗了我们。
下面我们就使用更专业的工具jad来进行反编译。

使用jad反编译后的枚举类源码如下所示:

public final class EnumSingle extends Enum
{

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

    public static EnumSingle valueOf(String name)
    {
        return (EnumSingle)Enum.valueOf(zzuli/edu/enumTest/EnumSingle, name);
    }

    private EnumSingle(String s, int i)
    {
        super(s, i);
    }

    public EnumSingle getInstance()
    {
        return INSTANCE;
    }

    public static final EnumSingle INSTANCE;
    private static final EnumSingle $VALUES[];

    static 
    {
        INSTANCE = new EnumSingle("INSTANCE", 0);
        $VALUES = (new EnumSingle[] {
            INSTANCE
        });
    }
}

由jad反编译后的源码可知,枚举类的构造函数为有参构造函数 EnumSingle(String s, int i),并且有两个参数。

下面我们就使用反射获取枚举类的有参构造函数,看使用反射是否能创建新的实例对象。

public class Test3 {
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;

        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

运行结果:
荐
                                                        枚举类都不知道,还敢说自己会Java?
由运行结果可知,与我们预期的异常一样,抛出了IllegalArgumentException(“Cannot reflectively create enum objects”)异常。
此时说明使用枚举类实现单例模式是十分安全的,使用反射进行暴力破解也不能创建新的对象。

本文地址:https://blog.csdn.net/Mr_wxc/article/details/107361551