还有你不知道的Java枚举特性(下篇)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/AndroidBluetooth/article/details/100171048

简介

这篇是博文 还有你不知道的Java枚举特性(上篇) 的下篇,可以点击下面的链接前往。

本篇主要内容:

  • Java 枚举是一个特殊的类,聊聊其方法的重写
  • 如何使用接口来组织 Java 枚举?
  • 如何使用枚举实现 Java 的单例模式
  • JDK 数据结构中关于枚举的集合 EnumSet 和字典 EnumMap

重写枚举的方法

所有的枚举类都继承自 Enum,在这个父类当中 toStringequalshashCode 的三个方法,可以看一下,源码如下:

public String toString() {
	return name;
}

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

public final int hashCode() {
	return super.hashCode();
}

可以看出在这三个方法当中,我们只能重写 toString 方法,另外两个方法都是 final 修饰的方法,不可以被子类重写。

我们在自定义的枚举中,可以重写 toString 方法的,示例如下:

public enum Color {
    RED("red color"),
    GREEN("green color"),
    BLUE("blue color"),
    YELLOW("yellow color");

    Color(String name) {
        _name = name;
    }

    private String _name;
    
    @Override
    public String toString() {
        return "this.name: " + _name;
    }
}

关于 Enum 的源码,可以在博文 Java 枚举的本质 中的文末翻阅。

对于 Java 中所有枚举都是继承自 Enum,大家可以去使用 javap 命令反编译看看,如下代码是 javap 后的简单示例:

public final class com.veryitman.Color extends java.lang.Enum<com.veryitman.Color> {
  public static final com.veryitman.Color RED;
  public static final com.veryitman.Color GREEN;
  public static final com.veryitman.Color BLUE;
  public static final com.veryitman.Color YELLOW;
  public static com.veryitman.Color[] values();
  public static com.veryitman.Color valueOf(java.lang.String);
  static {};
}

可以看到 Color 是继承自 java.lang.Enum 的,Enum 是一个抽象类并实现了 ComparableSerializable 这两个接口,如下:

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

使用接口组织枚举

当我们定义的枚举过多且又有很多嵌套,可以使用接口来组织这些枚举,将其归类,这样一来不仅代码看起来很规范,并且也很好管理代码。

如下示例,使用接口 MobileTool 来组织两个枚举。

public interface MobileTool {
        enum Phone implements MobileTool {
            HUAWEI, iPhone, OPPO, XIAOMI
        }

        enum Pad implements MobileTool {
            iPad, WEPad, sPad
        }
    }

简单的可以这样使用,示例如下:

MobileTool mphone = MobileTool.Phone.HUAWEI;
mphone = MobileTool.Phone.iPhone;

MobileTool mpad = MobileTool.Pad.iPad;
mpad = MobileTool.Pad.sPad;

枚举实现单例模式

Effective Java 这本书籍中,作者有个这样下面的描述:

"This approach is functionally equivalent to the public field approach, except that it is more concise, provides the serialization machinery for free, and provides an ironclad guarantee against multiple instantiation, even in the face of sophisticated serialization or reflection attacks. While this approach has yet to be widely adopted, a single-element enum type is the best way to implement a singleton."

核心的意思是

使用枚举实现单例的方法虽然还没有广泛采用,但单元素的枚举类型已经成为实现 Java 单例模式的最佳方法。

我们用枚举实现一下单例模式,示例代码如下:

public enum Foo {
	INSTANCE;

	public void printFoo() {
		System.out.println("Foo here.");
	}
}

相比 Java 中其他的单例实现方式,此时此刻你会发现,枚举实现单例的代码会精简很多。

那么,枚举实现单例模式到底有哪些优势呢?或者换句话说,就这样实现单例靠谱吗?

经过大量例子和 Java 编程专家的讲解,枚举实现单例模式相当靠谱,它具有以下一些特点:

1、枚举实现的单例模式是线程安全的

本质上面来讲,枚举实现的单例之所以是线程安全的,这个跟 Java 的类加载机制有关。从上面反编译的代码来看,枚举是 final class 并且每个枚举值都是 static 的,这里牵扯到 ClassLoader 的相关知识,如果有兴趣建议大家去研究一下。

总之,对于我们任何一个枚举在第一次被真正用到之时,会被 Java 虚拟机加载并且完成初始化,而这个初始化过程是线程安全的,所以你需要记住枚举实现的单例模式是多线程安全的就可以了。

2、枚举可解决反射/反序列化问题

我们知道,一般的单例模式都存在两个问题,一个是可以通过反射调用,另一个就是可以通过序列化和反序列化来破坏单例。

一般解决反射调用可以通过私有构造方法中做处理,示例代码如下:

private static boolean flag = false;

private Foo() {
	synchronized (this) {
		if (false == flag) {
			flag = true;
		} else {
			throw new RuntimeException("不能反复创建");
		}
	}
};

了解序列化原理的同学,可以通过在单例类中实现 readResolve 方法就可以避免反序列化攻击这个问题了。示例代码如下:

private Object readResolve() {
    return INSTANCE;
}

Java 的枚举的反序列化实现并不是通过反射实现的,也就是说枚举的序列化和反序列化是有经过特殊定制和处理的,这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。

总之,枚举实现的单例模式不仅可以防止反射破坏,还可以防止序列化破坏单例。

除枚举实现单这种方式以外,我一般使用下面两种方式来实现单例模式。

饿汉式的单例模式写法

public class Foo {
    private static Foo instance = new Foo();
    
    private Foo () {
        
    }
    
    public static Foo getInstance() {
    	return instance;
    }
}

静态内部类实现单例模式

public class Foo {
    private static class FooHolder {
    	private static final Foo INSTANCE = new Foo();  
    }
    
    private Foo () {
    	
    }
    
    public static final Foo getInstance() {  
    	return FooHolder.INSTANCE;
    }
}  

枚举集合

EnumSet 是一个专为枚举设计的集合类,EnumSet 中的所有元素都必须是指定枚举类型的枚举值。

EnumSet 类结构图如下:

[外链图片转存失败(img-uMucAKn8-1567223060694)(/upload/images/2019/08/11/1.png)]

EnumSet 是一个抽象类,无法被实例化,但是可以通过静态方法获取该类的实例。

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable

EnumMap 类结构图如下:

[外链图片转存失败(img-FTj714ba-1567223060716)(/upload/images/2019/08/11/2.png)]

EnumMap 定义如下:

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable

EnumSet 保证集合中的元素不重复;EnumMap 中的 key 是 enum 类型,而 value 则可以是任意类型。

关于这两个数据结构的使用方法,大家可以参考 JDK 手册。


赠人玫瑰,手留余香~

展开阅读全文

不知道java(SE)

08-06

(这个标题很有种似曾相识的感觉吧?嘿嘿。)rnrn知其然,知其所以然。我们学习、使用java,刚开始一些细节的东西可能会困扰我们,时间久了,往往会觉得理所当然,问题还是不太清楚。被人问起的时候,可能会“理所当然”的告诉别人,“这是java封装了的,知道怎么用就行啦”。长此以往,我们可能会的工具或配置很多,却无所长进。rnrn为了一定程度上辅助我们搞清楚这种模糊的概念、细节,特此在java大版各个子板,开通一个《你不知道的java》系列帖子,希望汇聚众“Java人”的经验和汗水,发扬“开源”的思想,使我们知道一些java的细节、工具或配置文件的处理流程,成为多懂一些Java的人。rnrn规则:rn[size=19px][color=#0000FF]1. 留言的同学,先抛出自己发现的,别人可能不知道的细节问题,再给出这个问题的答案。rn2. 围观的同学,如果觉得有哪些细节让你为之叫好,请您点一下“对我有用”。rn3. 每月结贴一次,根据“对我有用”的程度和问题精彩程度,奖励相应比例积分。[/color][/size](俺的权限就200分,要是僧多粥少了,希望众大大别怪俺小气哈!)rnrn你不知道的java(SE)rn这里先厚颜扔两块板砖:rn1. 执行java程序,如何给main方法传参?rn命令行执行的时候,在类名后跟参数,以空格分隔多个参数,例如:rnjava MyTest Tomcat is good in use.rn"Tomcat is good in use."会分成五个字符串,传递给MyTest的main方法(作为args)。rnrn2. 我们平时使用的String、Integer等j2se基本类,在java安装环境什么地方?rn在$HOME\jre\lib\rt.jar中。(rt,无处不在啊。。)rnrn3. 打的jar包没问题,MANIFEST.MF也设置了Main-Class,为何还是无法执行响应的main方法(报错“Invalid or corrupt jarfile”)?rnMain-Class: 冒号后面要加空格,再配置上带上包路径的类,例如:rnMain-Class: com.test.HelloWorldrn这样就可以在命令行里执行jar包:java -jar XXX.jarrn(当然,如果配置了jar文件的默认打开方式为“Java(TM) platform SE binary”,也可以双击jar文件执行)rnrn4. 都说字符串hashCode相同,不一定equals,如何构造俩字符串,使其hashCode相同,字符串不同?rn首先,为了简化问题,我们可以限定,长度同为两位的字符串。rn我们知道String.hashCode方法的哈希策略(参考源码):循环遍历字符串中的字符(c表示字符,ASCc表示c的ASCII值),计算:hashcode = 31*hashcode+ASCcrn因为字符串只有两位,hashcode初始值为0,我们的两个字符串假设为"XY"与"MN",则其hashcode分别为(31*ASCX+ASCY)和(31*ASCM+ASCN)。rn接下来凑数就可以了:ASCX和ASCM可以取任意值(尽量小些,在ASCII码范围内),ASCY和ASCN要根据ASCX和ASCM的取值,得到曲线关系(取值也要在ASCII码范围0~127内凑)。rn比如ASCX取值49,ASCM取值50,则得到等式:31*49+ASCY=31*50+ASCN ,转换:ASCY=31+ASCN。此时,不妨ASCN取值66,则ASCY取值97。rn接下来大家可以用System.out.println((char)49);方式得知各ASCII码对应的字符:49,'1'; 50,'2'; 66,'B'; 97,'a'。rn于是乎,hashCode相同,字符串不同的两个字符串构造好了:"1a","2B"(我擦,不是故意构造这个字符串的。。)。rn(这里为了好看,凑了个常规字符,大家也可以试试其他ASCII码字符,0~127范围内的哦)rnrn[size=19px][color=#0000FF][b]不积跬步无以至千里,不积小流无以成江海。[/b][/color][/size]rnrn题外话:rn因个人能力有限,先在J2SE版试行,欢迎对java其他版块有经验的同学,以同样的标题发个话题。rn如果分数有限的话,也可以给我留言,我来汇总发帖(并标注问题提供者的ID)。rn欢迎大家提出改进意见。 论坛

超级玛丽都30岁了——这里还有不知道的事!

03-22

几年前,我著书一部来分析《超级马里奥世界(1990)》的所有关卡,由此我发现市面上多数游戏关卡的系统设计体系。《超级马里奥世界》作为一款经典游戏,本身就有研究价值。不过书中所讲内容广泛适用于电子游戏设计。“超级马里奥世界的设计方法论”,或“任天堂系游戏设计大法”实际上出现在各种各样的游戏中-甚至是非任天堂系游戏。rnrn在本教程中,你会领略到二十世纪九十年代开始关卡设计的自然组织形式。随着电子游戏设计师(尤其是任天堂的设计师)实践越来越多,他们开始习惯性的组织运用设计技巧。这里我会解释如何将所有这些直观技巧从容不迫的展现在自己的游戏中。rnrnrn范例rnrn我应用这个方法设计制作了《超级马里奥制造》的一个关卡(附注释)。rn关卡代号: 0740-0000-00CD-4D5Brnrnrn挑战-节奏-技巧主题rnrn设计框架叫做“挑战,节奏,技巧主题”,简称CCST。我从《洛克人》,《银河战士》,《传送门》,《半条命》,《超级食肉男孩》和许多其他的游戏中都能找到CCST结构。为了方便起见,我以《超级马里奥制造》为例来解释CCST结构。通过练习,你将能够在一些你最喜欢的游戏身上识别出这个结构,从而极大提升你在游戏内容上的层次组织能力和深掘能力。rnrnrn挑战rnrn首先我们需要理解和并识别挑战。挑战是一个简短的任务,它跻身于相对安全的游戏阶段之间,要求玩家在短时间内完成。看看下面的例子,马里奥必须在一个移动平台上面跳上跳下,而平台下面是个无底深渊。 论坛

没有更多推荐了,返回首页