05-javase-泛型和枚举

本文详细介绍了Java中的泛型和枚举。泛型可以帮助我们限制集合存储的数据类型,提高代码安全性,减少类型转换,并通过类型通配符实现更灵活的编程。类型擦除是泛型在编译后的处理方式,它导致了一些特定的限制。枚举则提供了一种安全且方便的方式来表示有限的固定数量的类实例,可以拥有自己的方法和属性,常用于表示状态、权限等固定集合。枚举在switch语句中使用方便,且支持静态导入。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

05-javase-泛型和枚举-ydl-笔记



一、java泛型 Generics

1.1 、引入泛型

之前咱们的超级数组中只能存数字,不能存其他类型的数据,是不是还是显得有些鸡肋。那我们能不能改进一下,让它可以存任意类型。

第一种解决方案:

​ 将内部的int数据,换成Object类型,使用引用数据类型替代基础数据类型。因为Object是所有类的超类,所以任何子类都可以传递进去,我们的代码可以简化如下:

package com.ydlclass;
public class SuperArray {
    private Object[] array;
    //根据下标查询数字

    //当前最后一个数字的下边,要为-1 ,以为数组的第一个下标为0
    private int currentIndex = -1;

    //构造是初始化
    public SuperArray(){
        array = new Object[8];
    }

    //添加数据的方法
    public void add(Object data){
        System.out.println("我是数组的实现!---add");
        currentIndex++;
        //自动扩容
        if(currentIndex > array.length-1){
            array = dilatation(array);
        }
        array[currentIndex] = data;
    }

    public Object get(int index){
        System.out.println("我是数组的实现---get");
        return array[index];
    }

    //数组扩容的方法
    private Object[] dilatation(Object[] oldArray){
        Object[] newArray = new Object[oldArray.length * 2];
        for (int i = 0; i < oldArray.length; i++) {
            newArray[i] = oldArray[i];
        }
        return newArray;
    }

    //验证下标是否合法
    private boolean validateIndex(int index) {
        //只要有一个不满足就返回false
        return index <= currentIndex && index >= 0;
    }
}
public static void main(String[] args) {
    SuperArray superArray = new SuperArray();
    superArray.add("abc");
    String item = (String)superArray.get(0);
}

思考这样会有什么问题吗?

1、我们规定传入的对象只要是Object子类就行,那就意味着,所有的对象都可以往篮子里扔,可以扔水果,也可以扔炸弹,数据类型不能很好的统一。

2、从超级数组中获取数据后必须强转才能使用,这是不是意味着,极有可能发生ClassCastException。

SuperArray superArray = new SuperArray();
superArray.add(new Date());
superArray.add(new Dog());

(Dog)superArray.get(0);

那怎么解决这个问题呢?

我们的目标是:
1、能够规定传入的数据类型必须是我们要求的。

2、从数组中获取的数据必须是确定的类型。

【泛型】就能够很好的解决这个问题

1.2、泛型的定义

什么是泛型?

​ 看表面的意思,泛型就是指广泛的、普通的类型。泛型能够帮助我们把【类型明确】的工作推迟到创建对象或调用方法的时候。

意思就是:我定义类的时候不用管到底是什么类型,new这个对象或者调用这个对象的方法时才确定具体的类型。

这听起来很不可思议,那到底是什么意思呢?咱们用以下的例子来说明情况:

(1)泛型类

泛型类也就是把泛型定义在类上,这样用户在使用类的时候才把类型给确定下来。

具体的方法就是使用<>加一个未知数,通常用 T K V 等大写字符表示,事实上只要是个单词就可以。

// 加上<T>之后,表示以后的SuperArray只能存某种类型,但是这个类型暂时不确定,使用T来代替
public class SuperArray<T> {
    private Object[] array;
    //根据下标查询数字

    //当前最后一个数字的下边,要为-1 ,以为数组的第一个下标为0
    private int currentIndex = -1;

    //构造是初始化
    public SuperArray(){
        array = new Object[8];
    }

    //添加数据的方法
    public void add(T data){
        System.out.println("我是数组的实现!---add");
        currentIndex++;
        //自动扩容
        if(currentIndex > array.length-1){
            array = dilatation(array);
        }
        array[currentIndex] = data;
    }

    public T get(int index){
        System.out.println("我是数组的实现---get");
        return (T)array[index];
    }

    //数组扩容的方法
    private Object[] dilatation(Object[] oldArray){
        Object[] newArray = new Object[oldArray.length * 2];
        for (int i = 0; i < oldArray.length; i++) {
            newArray[i] = oldArray[i];
        }
        return newArray;
    }

    //验证下标是否合法
    private boolean validateIndex(int index) {
        //只要有一个不满足就返回false
        return index <= currentIndex && index >= 0;
    }
}

有了上边的代码,我们需要怎么去定义一个超级数组呢?

public static void main(String[] args) {
    // jdk1.7以前
    SuperArray<String> superArray = new SuperArray<String>();
    // jdk1.7以后提出了钻石语法,可以进行类型的自动推断,后边的尖括号就不用写了
    SuperArray<String> superArray = new SuperArray<>();
    superArray.add("abc");
    String item = superArray.get(0);
}

​ 我们申明一个类的时候,无需关心将来我的超级数组存的是什么类型,但是new SuperArray的时候明确指出了这个超级数组只能存String类型的,其他类型就不能存。

​ 可以看到上面这个程序,在使用时如果定义了类型,那么在使用时就可以不用进行强制类型转换,直接就可以得到一个T类型的对象。

(2)泛型方法

有时候只关心某个方法,那么使用泛型时可以不定义泛型类,而是只定义一个泛型方法,如下:

public class Test2 {
    public <T> T show(T t) {
        System.out.println(t);
        return t;
    }

    public static <T> T show2(T one) { //这是正确的
        return null;
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        String show = test2.show("123");
        Integer show1 = test2.show(123);
    }
}

需要注意一下定义的格式,泛型必须得先定义才能够使用。

​ 说明一下,定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。

​ 泛型方法最好要结合具体的返回值,否则和Object作为参数差别不大。

​ 我们在学习反射的时候会学习类似的例子。

(3)继承关系

泛型类在继承时,可以明确父类(泛型类)的参数类型,也可以不明确。 还记得我们学习策略设计模式时的Comparator接口吗?之前的设计必须是User类型,而现在有了泛型我们可以灵活使用了:

// 泛型类
public interface Comparator<T>{
    int compare(T o1, T o2);
}
public class StudentComparator implements Comparator {
    @Override
    public Integer compare(Object o1, Object o2) {
        if(o1 instanceof Student && o2 instanceof Student){
            return ((Student) o1).getAge() - ((Student) o2).getAge();
        }
        return null;
    }
}
a. 明确类型,子类存在的目的就是比较User对象
public class User {
    private String name;
    private int age;
    private int height;

    public User() {
    }

    public User(String name, int age, int height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}
//在实现泛型类时明确父类的类型
import java.util.Comparator;

public class UserAgeComparator implements Comparator<User> {
    @Override
    public int compare(User o1, User o2) {
        return o1.getAge() - o2.getAge();
    }
}

public static void main(String[] args) {
    UserAgeComparator userAgeComparator = new UserAgeComparator();
    int compare = userAgeComparator.compare(new User(), new User());
    System.out.println(compare);
}
b. 不明确类型

子类不去明确类型,明确类型的工作留在创建对象的时候:

public class UserAgeComparator<T> implements Comparator<T> {

    @Override
    public int compare(T o1, T o2) {
        return o1.equals(o2) ? 0 : 1;
    }

    public static void main(String[] args) {
        UserAgeComparator<User> userAgeComparator = new UserAgeComparator<>();
        int compare = userAgeComparator.compare(new User(), new User());
        System.out.println(compare);
    }
}

1.3、项目实战

用泛型改进超级数组和超级链表,这里只有部分代码:

public interface Super<T> {

    /**
     * 标记所有的子类实现必须有add方法,添加数据
     * @param data
     */
    void add(T data);
    
    /**
     * 标记所有的子类实现必须有get方法,获取数据
     * @param index
     * @return
     */
    T get(int index);

    /**
     * 标记所有的子类实现必须有size方法,数据大小
     * @return
     */
    int size();
}
public class SuperArray<T> implements Super<T> {

    //维护一个数组,要想什么都存,就要使用顶级父类
    private Object[] array;
    //当前最后一个数字的下边,要为-1 ,以为数组的第一个下标为0
    private int currentIndex = -1;

    //构造是初始化
    public SuperArray(){
        array = new Object[8];
    }

    //添加数据的方法
    public void add(T data){
        System.out.println("我是数组的实现!---add");
        currentIndex++;
        //自动扩容
        if(currentIndex > array.length-1){
            array = dilatation(array);
        }
        array[currentIndex] = data;
    }


    //根据下标查询数字
    public T get(int index){
        System.out.println("我是数组的实现---get");
        return (T)array[index];
    }

    //查看当前有多少个数字
    public int size(){
        return currentIndex + 1;
    }

    //数组扩容的方法
    private Object[] dilatation(Object[] oldArray){
        Object[] newArray = new Object[oldArray.length * 2];
        for (int i = 0; i < oldArray.length; i++) {
            newArray[i] = oldArray[i];
        }
        return newArray;
    }

    //验证下标是否合法
    private boolean validateIndex(int index) {
        //只要有一个不满足就返回false
        return index <= currentIndex && index >= 0;
    }
}

public class SuperLinked<T> implements Super<T> {

    private Node head = null;
    private Node tail = null;

    private int length = 0;

    //添加元素
    public void add(T data){
        System.out.println("我是链表的实现-----add");
        Node<T> node = new Node<>();
        node.setNum(data);
        if (length == 0) {
            //如果第一次添加一共就一个节点
            head = node;
        }else{
            //和尾巴拉手
            tail.setNextNode(node);
        }
        //把新添加进来的当成尾巴
        tail = node;
        length ++;
    }

    //根据下标查询数字,非常有意思的写法
    public T get(int index){
        System.out.println("我是链表的实现------get");
        if(index > length){
            return null;
        }
        //小技巧
        Node targetNode = head;
        for (int i = 0; i < index; i++) {
            targetNode = targetNode.getNextNode();
        }
        return (T)(targetNode.getNum());
    }

    //查看当前有多少个数字
    public int size(){
        return length;
    }

    class Node<T> {

        //存储的真实数据
        private T num;

        //写一个节点
        private Node nextNode = null;

        public T getNum() {
            return num;
        }

        public void setNum(T num) {
            this.num = num;
        }

        public Node getNextNode() {
            return nextNode;
        }

        public void setNextNode(Node nextNode) {
            this.nextNode = nextNode;
        }
    }
}

1.4、类型通配符

新建三个类:

public class Animal {
}
public class Dog extends Animal {
}
public class Teddy extends Dog {
}

​ 当我们的一个方法的参数需要传入一个带有参数的类型的时候,可以使用通配符来确定具体传入的对象范围。

public static void print(Comparator<Object> comparator){

}

public static void main(String[] args) {
    Comparator<User> comparator = new Comparator<User>(){
        @Override
        public int compare(User o1, User o2) {
            return o1.getAge() - o2.getAge();
        }
    };
    print(comparator);
}

会有以下的报错信息:
在这里插入图片描述
意思就是我需要一个泛型是Object的Comparator但你提供的泛型是User,使用通配符就能很好的解决这些问题:

(1)无界

类型通配符我感觉上和泛型方法差不多,只是不用在使用前进行定义,例子如下:

public static void main(String[] args) {
    SuperArray<Dog> superArray = new SuperArray<>();
    superArray.add(new Dog());
    superArray.add(new Teddy());
    printSuperArray(superArray);
}

public static void printSuperArray(SuperArray<?> superArray){
    for (int i = 0;i<superArray.size();i++){
        System.out.println(superArray.get(i));
    }
}

"?"可以接收任何类型,有些聪明的小伙伴可能发现不加?,连泛型也不要行不行,悄悄的告诉你,可以,但是程序会报一个警告:

public static void print(Comparator comparator){  }
Raw use of parameterized class ‘xxxx‘ 警告
没有类型参数的泛型

​ 使用原始类型(没有类型参数的泛型)是合法的,但是你永远不应该这样做。如果使用原始类型,就会失去泛型的安全性和表现力。 既然你不应该使用它们,那么为什么语言设计者一开始就允许原始类型呢?答案是:为了兼容性。Java 即将进入第二个十年,泛型被添加进来时,还存在大量不使用泛型的代码。保持所有这些代码合法并与使用泛型的新代码兼容被认为是关键的。将参数化类型的实例传递给设计用于原始类型的方法必须是合法的,反之亦然。

(2)上界

我们可以使用(SuperArray<? extends Dog> superArray)的形式来约定传入参数的上界,意思就是泛型只能是Dog的或者Dog的子类。

  public static void main(String[] args) {
        SuperArray<Animal> superArray = new SuperArray<>();
        superArray.add(new Dog());
        superArray.add(new Teddy());
        superArray.add(new Animal());
        printSuperArray(superArray);
    }
    
    public static void printSuperArray(SuperArray<? extends Dog> superArray){
        for (int i = 0;i<superArray.size();i++){
            System.out.println(superArray.get(i));
        }
    }

这种情况下能够接收A类或者A类的子类。
在这里插入图片描述
注:当我们使用extends时,我们可以读元素,因为元素都是A类或子类,可以放心的用A类拿出

(3)下界

我们可以使用(SuperArray<? super Dog> superArray)的形式来约定传入参数的下界,意思就是泛型只能是Dog的或者Dog的超类。

public static void main(String[] args) {
    SuperArray<Teddy> superArray = new SuperArray<>();
    superArray.add(new Teddy());
    printSuperArray(superArray);
}
public static void printSuperArray(SuperArray<? super Dog> superArray){
    for (int i = 0;i<superArray.size();i++){
        System.out.println(superArray.get(i));
    }
}

在这里插入图片描述
当使用super时,可以添加元素,因为都是A类或父类,那么就可以安全的插入A类

1.5、类型擦除

我们刚刚讲过,为了兼容性,使用原始类型(没有类型参数的泛型)是合法的,泛型被添加进来时,还存在大量不使用泛型的代码。保持所有这些代码合法并与使用泛型的新代码兼容被认为是关键的。将参数化类型的实例传递给设计用于原始类型的方法必须是合法的,反之亦然。

​ 为了保持这种兼容性,Java的泛型其实是一种伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

​ 如在代码中定义SuperArray<Object>SuperArray<String>等类型,在编译后都会变成SuperArray,JVM看到的只是SuperArray,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现完全避免类型转换异常的情况。

(1)泛型不能是基本数据类型

​ 不能用类型参数替换基本类型。就比如,没有SuperArray<double>,只有SuperArray<Double>。因为当类型擦除后,SuperArray的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值
在这里插入图片描述
这一点尤其重要:必须要记住。

(2)重载方法

如果泛型类型因为具体的泛型不同而导致方法签名不同,那么以下两个方法就是两种重载方法:

public static void print(Comparator<Object> comparator){

}

public static void print(Comparator<User> comparator){

}

然而事实上:在这里插入图片描述
因为泛型被擦除后,其实这两个方法是一致的,并不能构成泛型。

(3)类型擦除和多态的冲突

现在有这样一个泛型类:

class Pair<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T value) {  
        this.value = value;  
    }  
}

import java.util.Date;

public class DatePair extends Pair<Date>{
    @Override
    public Date getValue() {
        return super.getValue();
    }

    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }
}

​ 在这个子类中,我们设定父类的泛型类型为Pair,在子类中,我们重写了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。

​ 所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?

分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:

class Pair {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}  
{
  public com.ydlclass.Pair();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair<TT;>;

  public T getValue();
  	// 这里我们知道这个方法的返回值是Object
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field value:Ljava/lang/Object;
         4: areturn
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/Pair<TT;>;
    Signature: #20                          // ()TT;

  public void setValue(T);
  	// 这里我们知道这个方法的参数是引用数据类型,Object
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field value:Ljava/lang/Object;
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/ydlclass/Pair;
            0       6     1 value   Ljava/lang/Object;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/ydlclass/Pair<TT;>;
            0       6     1 value   TT;
    Signature: #23                          // (TT;)V
}
Signature: #24                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Pair.java"

再看子类的两个重写的方法的类型:

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  
@Override  
public Date getValue() {  
    return super.getValue();  
}
{
  public java.util.Date getValue();
    descriptor: ()Ljava/util/Date;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method com/ydlclass/Pair.getValue:()Ljava/lang/Object;
         4: checkcast     #3                  // class java/util/Date
         7: areturn
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   Lcom/ydlclass/DatePair;

  public void setValue(java.util.Date);
    descriptor: (Ljava/util/Date;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #4                  // Method com/ydlclass/Pair.setValue:(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/ydlclass/DatePair;
            0       6     1 value   Ljava/util/Date;


  // 桥接方法,一会分析
  public void setValue(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/util/Date
         5: invokevirtual #5                  // Method setValue:(Ljava/util/Date;)V
         8: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/ydlclass/DatePair;

  public java.lang.Object getValue();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #6                  // Method getValue:()Ljava/util/Date;
         4: areturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/ydlclass/DatePair;
}
Signature: #29                          // Lcom/ydlclass/Pair<Ljava/util/Date;>;
SourceFile: "DatePair.java"

​ 先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果是在普通的继承关系中,根本就不会是重写,而是重载。

我们在一个main方法测试一下:

public static void main(String[] args) throws ClassNotFoundException {  
    DatePair DatePair = new DatePair();  
    DatePair.setValue(new Date());                  
    DatePair.setValue(new Object()); //编译错误  
}

​ 如果是重载,那么子类中两个setValue方法,一个是参数Object类型,一个是Date类型,可是我们发现,根本就没有这样的一个子类继承自父类的Object类型参数的方法。所以说,确实是重写了,而不是重载了。

关键字:ACC_BRIDGE, ACC_SYNTHETIC

​ 从编译的结果来看,我们本意重写setValuegetValue方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的【桥方法】,我们从字节码中看到两个标志【ACC_BRIDGE, ACC_SYNTHETIC】。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在我们自己定义的setvaluegetValue方法上面的@Override只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

​ 所以**,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突**。

​ 并且,还有一点也许会有疑问,子类中的桥方法Object getValue()Date getValue()是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分辨这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟机去区别。

1.6、静态方法和静态类中的问题

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
举例说明:

public class Test2<T> {    
    public static T one;   //编译错误    
    public static T show(T one){ //编译错误    
        return null;    
    }    
}

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

但是要注意区分下面的一种情况:

public class Test2<T> {    

    public static <T> T show(T one){ //这是正确的    
        return null;    
    }    
}

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T

二、枚举enum

​ 在某些情况下,一个类的对象的实例有限且固定的,如季节类,它只有春夏秋冬4个对象,再比如星期,在这种场景下我们可以使用枚举。当然我们也可以有自己的方法来实现。

方案一:静态常量

public class SeasonConstant {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
    public static final int AUTUMN = 3;
    public static final int WINTER = 4;
}

这种方式,我们可以简单的表示春夏秋冬四个季节,但是扩展性很差,我们想给春夏秋冬附加更多信息的时候就无能为力的,静态常量能保证内存独此一份,更够很好的表示春夏秋冬四个季节,同时不允许别人修改。

方案二:利用类似单例模式的方案

既然使用基础数据类型无法表示丰富的内容,我们不妨把基础类型改为引用数据类型。

public class Season {
    private int value;
    private String name;

    // 定义四个静态常量让每个季节在内存中独此一份
    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 Season(){}

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


    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

我们这样做不仅仅能够保证内存中只有四个对象,我们不妨验证一下:

public static void main(String[] args) {
    System.out.println(Season.SPRING == Season.SPRING);
}
使用==比较两个对象比较的是内存地址,内存地址一样不正好说明是同一个对象嘛。

只是这种写法略显复杂,我们可以使用简单的方式表达:

首先我们先简化一下代码,去掉其他属性:
public class Season {
    // 定义四个静态常量让每个季节在内存中独此一份
    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 WINTER = new Season();
}

我们发现这个重复的太多了,直接干掉

public class Season {
    // 定义四个静态常量让每个季节在内存中独此一份
    SPRING,SUMMER,AUTUMN,WINTER;
}

如果能写成这个样子,是不是就好了,这仅仅是将所有的重复代码删掉了而已。

Java1.5 引入了 enum 来定义枚举类,就可以使用这样的书写方式了,但是要将class换成enum,如下:

public enum SeasonEnum {
    SPRING, SUMMER, AUTUMN, WINTER;
}

当然,以上的例子都是为了更好的解释枚举类,事实上枚举还有

2.1、基本Enum特性

枚举类的定义

public enum SeasonEnum {
    SPRING,SUMMER,AUTUMN,WINTER;
}

不妨看看字节码文件:

这个是静态常量的:

C:\Users\zn\IdeaProjects\untitled\out\production\untitled>javap -v Season.class
Classfile /C:/Users/zn/IdeaProjects/untitled/out/production/com/ydlclass/Season.class
  Last modified 2021-8-28; size 1103 bytes
  MD5 checksum e7dac070287209c9e80194b38fa4b27b
  Compiled from "Season.java"
public class Season
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#42        // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#43         // Season.value:I
   #3 = Fieldref           #4.#44         // Season.name:Ljava/lang/String;
   #4 = Class              #45            // Season
   #5 = String             #46            // 春天
   #6 = Methodref          #4.#47         // Season."<init>":(ILjava/lang/String;)V
   #7 = Fieldref           #4.#48         // Season.SPRING:LSeason;
   #8 = String             #49            // 夏天
   #9 = Fieldref           #4.#50         // Season.SUMMER:LSeason;
  #10 = String             #51            // 秋天
  #11 = Fieldref           #4.#52         // Season.AUTUMN:LSeason;
  #12 = String             #53            // 冬天
  #13 = Fieldref           #4.#54         // Season.WINTER:LSeason;
  #14 = Class              #55            // java/lang/Object
  #15 = Utf8               value
  #16 = Utf8               I
  #17 = Utf8               name
  #18 = Utf8               Ljava/lang/String;
  #19 = Utf8               SPRING
  #20 = Utf8               LSeason;
  #21 = Utf8               SUMMER
  #22 = Utf8               AUTUMN
  #23 = Utf8               WINTER
  #24 = Utf8               <init>
  #25 = Utf8               ()V
  #26 = Utf8               Code
  #27 = Utf8               LineNumberTable
  #28 = Utf8               LocalVariableTable
  #29 = Utf8               this
  #30 = Utf8               (ILjava/lang/String;)V
  #31 = Utf8               getValue
  #32 = Utf8               ()I
  #33 = Utf8               setValue
  #34 = Utf8               (I)V
  #35 = Utf8               getName
  #36 = Utf8               ()Ljava/lang/String;
  #37 = Utf8               setName
  #38 = Utf8               (Ljava/lang/String;)V
  #39 = Utf8               <clinit>
  #40 = Utf8               SourceFile
  #41 = Utf8               Season.java
  #42 = NameAndType        #24:#25        // "<init>":()V
  #43 = NameAndType        #15:#16        // value:I
  #44 = NameAndType        #17:#18        // name:Ljava/lang/String;
  #45 = Utf8               Season
  #46 = Utf8               春天
  #47 = NameAndType        #24:#30        // "<init>":(ILjava/lang/String;)V
  #48 = NameAndType        #19:#20        // SPRING:LSeason;
  #49 = Utf8               夏天
  #50 = NameAndType        #21:#20        // SUMMER:LSeason;
  #51 = Utf8               秋天
  #52 = NameAndType        #22:#20        // AUTUMN:LSeason;
  #53 = Utf8               冬天
  #54 = NameAndType        #23:#20        // WINTER:LSeason;
  #55 = Utf8               java/lang/Object
{
  // 这一部分我们感觉像是四个静态常量    
  public static final Season SPRING;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public static final Season SUMMER;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public static final Season AUTUMN;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public static final Season WINTER;
    descriptor: LSeason;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public int getValue();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field value:I
         4: ireturn
      LineNumberTable:
        line 20: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LSeason;

  public void setValue(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #2                  // Field value:I
         5: return
      LineNumberTable:
        line 24: 0
        line 25: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LSeason;
            0       6     1 value   I

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 28: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LSeason;

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #3                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 32: 0
        line 33: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LSeason;
            0       6     1  name   Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #4                  // class Season
         3: dup
         4: iconst_1
         // 以下就是给每个静态常量构造一个对象
         // 但是我们发现人家调用的构造器是有一个参数的,String
         5: ldc           #5                  // String 春天
         7: invokespecial #6                  // Method "<init>":(ILjava/lang/String;)V
        10: putstatic     #7                  // Field SPRING:LSeason;
        13: new           #4                  // class Season
        16: dup
        17: iconst_2
        18: ldc           #8                  // String 夏天
        20: invokespecial #6                  // Method "<init>":(ILjava/lang/String;)V
        23: putstatic     #9                  // Field SUMMER:LSeason;
        26: new           #4                  // class Season
        29: dup
        30: iconst_3
        31: ldc           #10                 // String 秋天
        33: invokespecial #6                  // Method "<init>":(ILjava/lang/String;)V
        36: putstatic     #11                 // Field AUTUMN:LSeason;
        39: new           #4                  // class Season
        42: dup
        43: iconst_4
        44: ldc           #12                 // String 冬天
        46: invokespecial #6                  // Method "<init>":(ILjava/lang/String;)V
        49: putstatic     #13                 // Field WINTER:LSeason;
        52: return
      LineNumberTable:
        line 6: 0
        line 7: 13
        line 8: 26
        line 9: 39
}
SourceFile: "Season.java"

这个是枚举的:

C:\Users\zn\IdeaProjects\untitled\out\production\untitled>javap -v SeasonEnum.class
Classfile /C:/Users/zn/IdeaProjects/untitled/out/production/untitled/SeasonEnum.class
  Last modified 2021-8-28; size 974 bytes
  MD5 checksum dc612af3d340c0984bbf18b7cffbf2e6
  Compiled from "SeasonEnum.java"
public final class SeasonEnum extends java.lang.Enum<SeasonEnum>
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
   #1 = Fieldref           #4.#42         // SeasonEnum.$VALUES:[LSeasonEnum;
   #2 = Methodref          #43.#44        // "[LSeasonEnum;".clone:()Ljava/lang/Object;
   #3 = Class              #23            // "[LSeasonEnum;"
   #4 = Class              #45            // SeasonEnum
   #5 = Methodref          #16.#46        // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
   #6 = Methodref          #16.#47        // java/lang/Enum."<init>":(Ljava/lang/String;I)V
   #7 = String             #17            // SPRING
   #8 = Methodref          #4.#47         // SeasonEnum."<init>":(Ljava/lang/String;I)V
   #9 = Fieldref           #4.#48         // SeasonEnum.SPRING:LSeasonEnum;
  #10 = String             #19            // SUMMER
  #11 = Fieldref           #4.#49         // SeasonEnum.SUMMER:LSeasonEnum;
  #12 = String             #20            // AUTUMN
  #13 = Fieldref           #4.#50         // SeasonEnum.AUTUMN:LSeasonEnum;
  #14 = String             #21            // WINTER
  #15 = Fieldref           #4.#51         // SeasonEnum.WINTER:LSeasonEnum;
  #16 = Class              #52            // java/lang/Enum
  #17 = Utf8               SPRING
  #18 = Utf8               LSeasonEnum;
  #19 = Utf8               SUMMER
  #20 = Utf8               AUTUMN
  #21 = Utf8               WINTER
  #22 = Utf8               $VALUES
  #23 = Utf8               [LSeasonEnum;
  #24 = Utf8               values
  #25 = Utf8               ()[LSeasonEnum;
  #26 = Utf8               Code
  #27 = Utf8               LineNumberTable
  #28 = Utf8               valueOf
  #29 = Utf8               (Ljava/lang/String;)LSeasonEnum;
  #30 = Utf8               LocalVariableTable
  #31 = Utf8               name
  #32 = Utf8               Ljava/lang/String;
  #33 = Utf8               <init>
  #34 = Utf8               (Ljava/lang/String;I)V
  #35 = Utf8               this
  #36 = Utf8               Signature
  #37 = Utf8               ()V
  #38 = Utf8               <clinit>
  #39 = Utf8               Ljava/lang/Enum<LSeasonEnum;>;
  #40 = Utf8               SourceFile
  #41 = Utf8               SeasonEnum.java
  #42 = NameAndType        #22:#23        // $VALUES:[LSeasonEnum;
  #43 = Class              #23            // "[LSeasonEnum;"
  #44 = NameAndType        #53:#54        // clone:()Ljava/lang/Object;
  #45 = Utf8               SeasonEnum
  #46 = NameAndType        #28:#55        // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  #47 = NameAndType        #33:#34        // "<init>":(Ljava/lang/String;I)V
  #48 = NameAndType        #17:#18        // SPRING:LSeasonEnum;
  #49 = NameAndType        #19:#18        // SUMMER:LSeasonEnum;
  #50 = NameAndType        #20:#18        // AUTUMN:LSeasonEnum;
  #51 = NameAndType        #21:#18        // WINTER:LSeasonEnum;
  #52 = Utf8               java/lang/Enum
  #53 = Utf8               clone
  #54 = Utf8               ()Ljava/lang/Object;
  #55 = Utf8               (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
{
// 静态常量
  public static final SeasonEnum SPRING;
    descriptor: LSeasonEnum;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final SeasonEnum SUMMER;
    descriptor: LSeasonEnum;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final SeasonEnum AUTUMN;
    descriptor: LSeasonEnum;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final SeasonEnum WINTER;
    descriptor: LSeasonEnum;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static SeasonEnum[] values();
    descriptor: ()[LSeasonEnum;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[LSeasonEnum;
         3: invokevirtual #2                  // Method "[LSeasonEnum;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[LSeasonEnum;"
         9: areturn
      LineNumberTable:
        line 1: 0

  public static SeasonEnum valueOf(java.lang.String);
    descriptor: (Ljava/lang/String;)LSeasonEnum;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #4                  // class SeasonEnum
         2: aload_0
         3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #4                  // class SeasonEnum
         9: areturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  name   Ljava/lang/String;

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #4                  // class SeasonEnum
         3: dup
         4: ldc           #7                  // String SPRING
         6: iconst_0
         7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        10: putstatic     #9                  // Field SPRING:LSeasonEnum;
        13: new           #4                  // class SeasonEnum
        16: dup
        17: ldc           #10                 // String SUMMER
        19: iconst_1
        20: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        23: putstatic     #11                 // Field SUMMER:LSeasonEnum;
        26: new           #4                  // class SeasonEnum
        29: dup
        30: ldc           #12                 // String AUTUMN
        32: iconst_2
        33: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        36: putstatic     #13                 // Field AUTUMN:LSeasonEnum;
        39: new           #4                  // class SeasonEnum
        42: dup
        43: ldc           #14                 // String WINTER
        45: iconst_3
        46: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        49: putstatic     #15                 // Field WINTER:LSeasonEnum;
        52: iconst_4
        53: anewarray     #4                  // class SeasonEnum
        56: dup
        57: iconst_0
        58: getstatic     #9                  // Field SPRING:LSeasonEnum;
        61: aastore
        62: dup
        63: iconst_1
        64: getstatic     #11                 // Field SUMMER:LSeasonEnum;
        67: aastore
        68: dup
        69: iconst_2
        70: getstatic     #13                 // Field AUTUMN:LSeasonEnum;
        73: aastore
        74: dup
        75: iconst_3
        76: getstatic     #15                 // Field WINTER:LSeasonEnum;
        79: aastore
        80: putstatic     #1                  // Field $VALUES:[LSeasonEnum;
        83: return
      LineNumberTable:
        line 2: 0
        line 1: 52
}
Signature: #39                          // Ljava/lang/Enum<LSeasonEnum;>;
SourceFile: "SeasonEnum.java"
 protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

常用方法

方法说明
values()静态的自动生成的 可以遍历enum实例,其返回enum实例的数组
ordinal()父类的实例方法 返回每个实例在声明时的次序
name()父类的实例方法 返回enum实例声明时的名称
getDeclaringClass()返回其所属的enum类
valueOf()静态的自动生成的 根据给定的名称返回相应的enum实例
public static void main(String[] args) {
    SeasonEnum[] items = SeasonEnum.values();
    for (int i = 0; i < items.length; i++) {
        System.out.println(items[i].ordinal());
        System.out.println(items[i].name());
        System.out.println(items[i].getDeclaringClass());
        System.out.println(SeasonEnum.valueOf(SeasonEnum.class, items[i].name()));
        System.out.println("--------------------------");
    }
}

结果:
0
SPRING
class SeasonEnum
SPRING

1
SUMMER
class SeasonEnum
SUMMER

2
AUTUMN
class SeasonEnum
AUTUMN

3
WINTER
class SeasonEnum
WINTER

2.2、Enum中添加新方法

num 可以看做是一个常规类(除了不能继承自一个enum),enum 中可以添加方法和 main 方法

public enum SeasonEnum {
    SPRING("春天","春暖花开的季节"),
    SUMMER("夏天","热的要命,但是小姐姐都穿短裤"),
    AUTUMN("秋天","果实成熟的季节"),
    WINTER("冬天","冷啊,可以吃火锅");

    private String name;
    private String detail;

    SeasonEnum() {
    }

    SeasonEnum(String name, String detail) {
        this.name = name;
        this.detail = detail;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDetail() {
        return detail;
    }

    public void setDetail(String detail) {
        this.detail = detail;
    }
}

2.3、Switch语句中的Enum

  • 正确用法
public static void main(String[] args) {
    SeasonEnum season = SeasonEnum.SPRING;
    switch (season){
        case SPRING:
            System.out.println("春天来了,又到了万物交配的季节!");
        case SUMMER:
            System.out.println("夏天来了,又可以穿大裤衩了!");
        case AUTUMN:
            System.out.println("秋天来了,又到了收获的季节!");
        case WINTER:
            System.out.println("冬天来了,又到了吃火锅的季节了!");
        default:
            System.out.println("也没有别的季节了。");
    }
}
  • 常规情况下必须使用 enum 类型来修饰 enum 实例,但在 case 语句中不必如此,
  • 意思就是 case SPRING: 不需要写成 case SeasonEnum.SPRING:。

2.4、Enum的静态导入

  • static import 可以将 enum 实例的标识符带入当前类,无需再用enum类型来修饰 enum 实例
import static com.ydlclass.SeasonEnum.*;

public class Test {

    public static void main(String[] args) {
        System.out.println(SPRING.name());
        System.out.println(SUMMER.name());
    }
}

2.5、枚举实现单例设计模式

目前我们的单例设计模式已经实现了三种了:

《Effective Java》
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。—-《Effective Java 中文版 第二版》

public class Singleton {

    private Singleton(){}

    public static Singleton getInstant(){
        return SingletonHolder.INSTANT.instant;
    }

   private enum SingletonHolder{
        INSTANT;
        private final Singleton instant;

        SingletonHolder(){
            instant = new Singleton();
        }

    }

    public static void main(String[] args) {
        System.out.println(Singleton.getInstant() == Singleton.getInstant());
    }

}

2.6、枚举的优势

阿里《Java开发手册》对枚举的相关规定如下,我们在使用时需要稍微注意一下。

【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON。

第一, int 类型本身并不具备安全性,假如某个程序员在定义 int 时少些了一个 final 关键字,那么就会存在被其他人修改的风险,而反观枚举类,它“天然”就是一个常量类,不存在被修改的风险(原因详见下半部分);

第二,使用 int 类型的语义不够明确,比如我们在控制台打印时如果只输出 1…2…3 这样的数字,我们肯定不知道它代表的是什么含义。

那有人就说了,那就使用常量字符呗,这总不会还不知道语义吧?实现示例代码如下:

public static final String COLOR_RED = "RED";
public static final String COLOR_BLUE = "BLUE";
public static final String COLOR_GREEN = "GREEN";

但是这样同样存在一个问题,有些初级程序员会不按套路出牌,他们可能会直接使用字符串的值进行比较,而不是直接使用枚举的字段,实现示例代码如下:

public class EnumTest {

    public static final String COLOR_RED = "RED";
    public static final String COLOR_BLUE = "BLUE";
    public static final String COLOR_GREEN = "GREEN";
    
    public static void main(String[] args) {
        String color = "BLUE";
        if ("BLUE".equals(color)) {
            System.out.println("蓝色");

        }
    }
}

这样当我们修改了枚举中的值,那程序就凉凉了。

枚举比较推荐使用 ==

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七@归七

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值