泛型和枚举
知识点一:Java泛型 Generics
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);
那怎么解决这个问题呢?
我们的目标是:
- 能够规定传入的数据类型必须是我们要求的。
- 从数组中获取的数据必须是确定的类型。
【泛型】就能够很好的解决这个问题。
2、泛型的定义
什么是泛型?
看表面的意思,泛型就是指广泛的、普通的类型。泛型能够帮助我们把【类型明确】的工作推迟到创建对象或调用方法的时候。
意思就是:我定义类的时候不用管到底是什么类型,new这个对象或者调用这个对象的方法时才确定具体的类型。
这听起来很不可思议,那到底是什么意思呢?咱们用以下的例子来说明情况:
(1)泛型类
泛型类也就是把泛型定义在类上,这样用户在使用类的时候才把类型给确定下来。
具体的方法就是使用<>加一个未知数,通常用 T K V 等大写字符表示,事实上只要是个单词就可以。
package com.ydlclass;
// 加上<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,然后才可以用泛型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;
}
}
(1)明确类型,子类存在的目的就是比较User对象
package com.ydlclass;
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);
}
(2)不明确类型
子类不去明确类型,明确类型的工作留在创建对象的时候:
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);
}
}
3、项目实战
用泛型改进超级数组和超级链表,这里只有部分代码:
package com.ydlclass;
public interface Super<T> {
/**
* 标记所有的子类实现必须有add方法,添加数据
* @param data
*/
void add(T data);
/**
* 标记所有的子类实现必须有get方法,获取数据
* @param index
* @return
*/
T get(int index);
/**
* 标记所有的子类实现必须有size方法,数据大小
* @return
*/
int size();
}
package com.ydlclass;
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;
}
}
package com.ydlclass;
/**
* @author itnanls
* @date 2021/7/16
**/
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;
}
}
}
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类。
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;
}
}
然后我们想要一个子类继承它。
package com.ydlclass;
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
,那么父类里面的两个方法的参数都为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
从编译的结果来看,我们本意重写setValue
和getValue
方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的【桥方法】,我们从字节码中看到两个标志【ACC_BRIDGE, ACC_SYNTHETIC】。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而在我们自己定义的setvalue
和getValue
方法上面的@Override
只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
并且,还有一点也许会有疑问,子类中的桥方法Object getValue()
和Date getValue()
是同时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分辨这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟机去区别。
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;
}
当然,以上的例子都是为了更好的解释枚举类,事实上枚举还有
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、Enum中添加新方法
- Enum 可以看做是一个常规类(除了不能继承自一个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;
}
}
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:
。
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());
}
}
5、枚举实现单例设计模式
目前我们的单例设计模式已经实现了三种了:
《Effective Java》
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。—-《Effective Java 中文版 第二版》
package com.ydlclass;
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());
}
}
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("蓝色");
}
}
}
这样当我们修改了枚举中的值,那程序就凉凉了。
枚举比较推荐使用 ==