《疯狂java讲义》学习(19):枚举类

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/86562661

枚举类

在某些情况下,一个类的对象时有限而且固定的,比如季节类,它只有4个对象;在比如行星类,目前只有9个对象。这种实例有限而且固定的类,在Java里被称为枚举类。

手动实现枚举类

如果需要手动实现枚举类,可以采用如下设计方式:

  • 通过private将构造器隐藏起来。
  • 把这个类的所有可能实例都使用public static final修饰的类变量来保存。
  • 如果有必要,可以提供一些静态方法,允许其他程序根据特定参数来获取与之匹配的实例。下面程序将定义一个Season类,这个类只能产生4个对象,该Season类被定义成一个枚举类。
public class Season
{
    //把Season类定义成不可变的,将其Field也定义成final
    private final String name;
    private final String desc;
    public static final Season SPRING
            =new Season("春天" , "趁春踏青");
    public static final Season SUMMER
            =new Season("夏天" , "夏日炎炎");
    public static final Season FALL
            =new Season("秋天" , "秋高气爽");
    public static final Season WINTER
            =new Season("冬天" , "围炉赏雪");
    public static Season getSeason(int seasonNum)
    {
        switch(seasonNum)
        {
            case 1 :
                return SPRING;
            case 2 :
                return SUMMER;
            case 3 :
                return FALL;
            case 4 :
                return WINTER;
            default :
                return null;
        }
    }
    //将构造器定义成private访问权限
    private Season(String name , String desc)
    {
        this.name=name;
        this.desc=desc;
    }
    //只为name和desc提供getter方法
    public String getName()
    {
        return this.name;
    }
    public String getDesc()
    {
        return this.desc;
    }
}

上面的Season类时一个不可变类,在上面的Season类中包含了4个static final常量Field,这4个常量Field就代表了该类所能创建的对象。当其他程序需要使用Season对象时,即可通过如Season.SPRING的方式来去的Season对象,也可通过getSeason()静态工厂方法来获得Season对象:

public class SeasonTest {
    public SeasonTest(Season s){
        System.out.println(s.getName()+",这真是一个"+s.getDesc()+"的季节");
    }
    public static void main(String[] args){
        //直接使用Season的FALL常量代表一个Season对象
        new SeasonTest(Season.FALL);
    }
}

从上面程序中不难看出,使用枚举类可以使程序更加健壮,避免创建对象的随意性。
在更早以前,程序员喜欢使用简单的静态常量来表示这种情况:

public static final int SEASON_SPRING=1;
public static final int SEASON_SUMMER=2;
public static final int SEASON_FALL=3;
public static final int SEASON_WINTER=4;

这种方法简单明了,但处在如下几个问题:

  • 类型不安全:因为上面的每个季节实际上是一个int整数,因此完全可以把一个季节当成一个int整数使用,例如进行加法运算SEASON_SPRING+SEASON_SUMMER,这样的代码完全正常。
  • 没有命名空间:当需要使用季节时,必须在SPRING前使用SEASON_前缀,否则程序可能与其他类中的静态常量混淆。
  • 打印输出的意义不明确:当我们打印输出某个季节时,例如打印SEASON_SPRING,实际上输出的是1,这个1很难猜测它代表了春天。

枚举类入门

enum关键字(它与class、interface关键字的地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,它一样可以有自己的Field、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。
但枚举类终究不是普通类,它与普通类有如下简单区别:

  • 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是继承Object类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang. Comparable两个接口。
  • 使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类。
  • 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。
  • 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。

所有的枚举类都提供了一个values方法,该方法可以很方便地遍历所有的枚举值。
下面程序定义了一个SeasonEnum枚举类:

public enum SeasonEnum {
    //列出枚举实例
    SPRING,SUMMER,FALL,WINTER
}

如果需要使用该枚举类的某个实例,则可使用EnumClass.variable的形式,如SeasonEnum.SPRING:

public class EnumTest
{
    public void judge(SeasonEnum s)
    {
        //switch语句里的表达式可以是枚举值
        switch (s)
        {
            case SPRING:System.out.println("春暖花开,正好踏青");
                break;
            case SUMMER:System.out.println("夏日炎炎,适合游泳");
                break;
            case FALL:System.out.println("秋高气爽,进补及时");
                break;
            case WINTER:System.out.println("冬日雪飘,围炉赏雪");
                break;
        }
    }
    public static void main(String[] args)
    {
        //所有的枚举类都有一个values方法,返回该枚举类的所有实例
        for (SeasonEnum s : SeasonEnum.values())
        {
            System.out.println(s);
        }
        //平常使用枚举实例时
        //总是通过EnumClass.variable的形式来访问
         new EnumTest().judge(SeasonEnum.SPRING);
     }
}

前面已经介绍过,所有的枚举类都继承了java.lang.Enum类,所以枚举类可以直接使用java.lang.Enum类中所包含的方法。java.lang.Enum类中提供了如下几个方法。

  • int compareTo(E o):该方法用于与指定枚举对象比较顺序,同一个枚举实例只能与相同类型的枚举实例进行比较。如果该枚举对象位于指定枚举对象之后,则返回正整数;如果该枚举对象位于指定枚举对象之前,则返回负整数,否则返回零。
  • String name():返回此枚举实例的名称,这个名称就是定义枚举类时列出的所有枚举值之一。与此方法相比,大多数程序员应该优先考虑使用toString()方法,因为toString()方法返回更加用户友好的名称。
  • int ordinal():返回枚举值在枚举类中的索引值(就是枚举值在枚举声明中的位置,第一个枚举值的索引值为零)。
  • String toString():返回枚举常量的名称,与name方法相似,但toString()方法更常用。
  • public static <T extends Enum> T valueOf(Class enumType, String name):这是一个静态方法,用于返回指定枚举类中指定名称的枚举值。名称必须与在该枚举类中声明枚举值时所用的标识符完全匹配,不允许使用额外的空白字符。

正如前面看到的,当我们使用System.out.println(s)语句来打印枚举值时,实际上输出的是该枚举值的toString()方法,也就是输出该枚举值的名字。

枚举类的Field、方法和构造器

枚举类也是一种类,只是它是一种比较特殊的类,因此它一定可以定义Field、方法。下面程序定义一个Gender枚举类,该枚举类里包含了一个name实例变量。

public enum Gender {
    MALE,FEMALE;
    //定义一个public修饰的实例变量
    public String name;
}

上面的Gender枚举类里定义了一个名为name的实例变量,并且将它定义成一个public访问权限的,下面使用该枚举类:

public class GenderTest {
    public static void main(String[] args){
        //通过Enum的valueOf方法来湖区指定枚举类的枚举值
        Gender g=Enum.valueOf(Gender.class, "FEMALE");
        //直接为枚举值的Field赋值
        g.name="女";
        //直接访问枚举值得Field值
        System.out.println(g + "代表:" + g.name);
    }
}

上面程序使用Gender枚举类时与使用一个普通类没有太大的差别,差别只是产生Gender对象的方式不同,枚举类的实例只能是枚举值,而不是随意地通过new来创建枚举类对象。
正如前面提到的,Java应该把所有类设计成良好封装的类,所以不应该允许直接访问Gender类的name成员变量,而是应该通过方法来控制对name的访问。否则可能出现很混乱的情形,例如上面程序,恰好我们设置了g.name=“女”,要是采用g.name=“男”,那程序就会非常混乱了,可能出现FEMALE代表男的局面。为此我们改进了Gender类的设计:

public enum Gender
{
    MALE,FEMALE;
    private String name;
    public void setName(String name){
        switch (this){
            case MALE:
                if (name.equals("男")){
                    this.name=name;
                }
                else{
                    System.out.println("参数错误");
                    return;
                }break;
            case FEMALE:
                if (name.equals("女")){
                    this.name=name;
                }
                else{
                    System.out.println("参数错误");
                    return;
                }break;
        }
    }    
    public String getName()
    {
        return this.name;
    }
}

上面程序把name设置成private,从而避免其他程序直接访问该name成员变量,必须通过setName()方法来修改Gender实例的name变量,而setName()方法就可以保证不会产生混乱。上面程序中粗体字部分保证FEMALE枚举值的name变量只能设置为"女",而MALE枚举值的name变量则只能设置为"男"。看如下程序:

public class GenderTest
{
    public static void main(String[] args)
    {
        Gender g=Enum.valueOf(Gender.class , "FEMALE");
        g.setName("女");
        System.out.println(g + "代表:" + g.getName());
        //此时设置name值时将会提示参数错误
        g.setName("男");
        System.out.println(g + "代表:" + g.getName());
    }
}

实现接口的枚举类

枚举类也可以实现一个或多个接口。与普通类实现一个或多个接口完全一样,枚举类实现一个或多个接口时,也需要实现该接口所包含的方法。下面程序定义了一个GenderDesc接口:

public interface GenderDesc
{
    void info();
}

在上面GenderDesc接口中定义了一个info方法,下面的Gender枚举类实现了该接口,并实现了该接口里包含的info方法:

public enum Gender
{
    //此处的枚举值必须调用对应的构造器来创建
     MALE("男"),FEMALE("女");
     private final String name;
    //枚举类的构造器只能使用private修饰
    private Gender(String name)
    {
        this.name=name;
    }
    public String getName()
    {
        return this.name;
    }
    public void info(){
        System.out.println("这是一个用于定义性别Field的枚举类");
    }
}

读者可能会发现,枚举类实现接口不过如此,与普通类实现接口完全一样:使用implements实现接口,实现接口里包含的抽象方法。
如果有枚举类来实现接口里的方法,则每个枚举值在调用该方法时都有相同的行为方式(因为方法体完全一样)。如果需要每个枚举值在调用该方法时呈现出不同的行为方式。在线面的Gender枚举类中,不同的枚举值对info方法的实现各不相同:

public enum Gender
{
    //此处的枚举值必须调用对应的构造器来创建
    MALE("男") // 花括号部分实际上是一个类体部分
    {public void info(){System.out.println("这个枚举值代表男性");}},
    FEMALE("女")
    {public void info(){System.out.println("这个枚举值代表女性");}};

    private final String name;
    //枚举类的构造器只能使用private修饰
    private Gender(String name)
    {
        this.name=name;
    }
    public String getName()
    {
        return this.name;
    }
    public void info(){
        System.out.println("这是一个用于定义性别Field的枚举类");
    }
}

当我们创建MALE和FEMALE两个枚举值时,后面又紧跟了一对花括号,这对花括号里包含了一个info方法定义。如果读者还记得匿名内部类语法的话,则可能对这样的语法有点印象了,花括号部分实际上就是一个类体部分,在这种情况下,当创建MALE、FEMALE枚举值时,并不是直接创建Gender枚举类的实例,而是相当于创建Gender的匿名子类的实例。因为粗体字括号部分实际上是一个匿名内部类的类体部分,所以这个部分的代码语法与前面介绍的匿名内部类语法大致相似,只是它依然是枚举类的匿名内部子类。
非抽象的枚举类才默认使用final修饰。对于一个抽象的枚举类而言——只要它包含了抽象方法,它就是抽象枚举类,系统会默认使用abstract修饰,而不是使用final修饰。
编译上面的程序,可以看到生成了Gender.class、Gender$1.class和Gender$2.class三个文件,这样的三个class文件正好证明了上面的结论:MALE和FEMALE实际上是Gender匿名子类的实例,而不是Gender类的实例。这样,当我们调用MALE和FEMALE两个枚举值的方法时,就会看到两个枚举值的方法表现不同的行为方式。

包含抽象方法的枚举类

假设有一个Operation枚举类,它的4个枚举值PLUS, MINUS, TIMES, DIVIDE分别代表加、减、乘、除4种运算。为此,我们定义下面的Operation枚举类:

public enum Operation {
    PLUS,MINUS,TIMES,DIVIDE;
    //为枚举类定义一个方法,用于实现不同的运算
    double eval(double x, double y){
        switch (this){
            case PLUS:return x+y;
            case MINUS:return x-y;
            case TIMES:return x*y;
            case DIVIDE:return x/y;
            default:return 0;
        }
    }
    public static void main(String[] args){
        System.out.println(Operation.PLUS.eval(3,4));
        System.out.println(Operation.DIVIDE.eval(4,2));
    }
}

上面程序中的default:return 0;代码其实没有实际意义,但是不写会无法通过编译。

仔细观察上面的Operation类不难发现,实际上PLUS,MINUS,TIMES,DIVIDE 4个值对eval方法各有不同的实现。为此,我们可以采用前面介绍的方法,让它们分别为4个枚举值提供eval的实现,然后在Operation类中定义一个eval的抽象方法:

public enum  Operation2 {
    PLUS{
        public double eval(double x, double y){
            return x+y;
        }
    },
    MINUS{
        public double eval(double x,double y){
            return x-y;
        }
    },
    TIMES{
        public double eval(double x,double y){
            return x*y;
        }
    },
    DIVIDE{
        public double eval(double x,double y){
            return x/y;
        }
    };
    //为枚举类定义一个抽象方法
    //这个抽象方法不同的枚举值提供不同的实现
    public abstract double eval(double x, double y);
    public static void main(String[] args){
        System.out.println(Operation2.PLUS.eval(3,4));
        System.out.println(Operation2.DIVIDE.eval(5,4));
    }
}

编译上面程序会生成5个class文件,其实Operation2对应一个class文件,它的4个匿名内部子类分别各对应一个class文件。
枚举类里定义抽象方法时不能使用abstract关键字将枚举类定义成抽象类(因为系统自动回为它添加abstract关键字),但因为枚举类需要显示创建枚举值,而不是作为父类,所以定义每个枚举值时必须为抽象方法提供实现,否则将出现编译错误

java编程实践

创建长度可变的数组

在Java中,对于数组的支持并不强大。程序员必须时刻注意数组中元素的个数,否则就会出现数组下标越界异常。为此,在Java API中定义了ArrayList帮助开发,但这意味着需要学习新的方法。本实例将先使用反射机制实现一个工具方法,每当调用该方法时,数组的长度就自动增加5。

1.

新建项目DynamicArray,并在其中创建一个DynamicArray.java文件。在该类的中定义两个方法,一个是increaseArray()方法,用于将给定的array数组长度加5;另一个是main()方法,用来进行测试:

package DynamicArray;

import java.lang.reflect.Array;
import java.util.*;

public class DyanmicArray {
    public static Object increaseArray(Object array){
        Class<?> clazz=array.getClass();   //获取代表数组的Class对象
        if (clazz.isArray()){   //如果输入是一个数组
            Class<?> componentType=clazz.getComponentType();  //获得数组元素的类型
            int length= Array.getLength(array);  //获取输入的数组长度
            Object newArray=Array.newInstance(componentType, length+5);  //创建数组
            System.arraycopy(array, 0, newArray, 5, length);   //复制原来数组中的所有数据
            return newArray;
        }
        return null;
    }
    public static void main(String[] args){
        int[] intArray=new int[10];
        System.out.println("整型数组原始长度是:" + intArray.length);
        Arrays.fill(intArray, 5);  //将数组中的元素全部赋值为5
        System.out.println("整型数组的内容:");
        System.out.println(Arrays.toString(intArray));
        int[] newIntArray=(int[]) increaseArray(intArray);  //增加数组的长度
        System.out.println("数组整组扩展后长度是:" + newIntArray.length);
        System.out.println("整型数组的内容:");
        System.out.println(Arrays.toString(newIntArray));
    }
}

Array类提供了动态创建和访问Java数组的方法。Array允许在执行getXXX()或setXXX()方法操作期间进行了扩展转换,则抛出IllegalArgumentException异常。为了创建新的数组对象,需要使用newInstance()方法,它可以根据指定的元素类型和长度创建新数组。方法的声明如下:public static Object newInstance(Class<?> componentType,int length)throws NegativeArraySizeException

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/86562661