c. 枚举
内容导视:
- 单例设计模式
- 枚举类
还欠最后三章,语法笔记就能搞完了。,实在弄得太久了,可以放松一下了。集合和 IO 流单独开章节。
按照老惯例,求职此简历以后再修改,软件测试实习生或其它岗位,月薪 3000。有时间,看看 b 站相关视频,考虑将简历改改,否则投再多家,也只能瞄一眼就被划走,写得太差劲,太不亮眼了,作为竞争力排名倒数的人,备胎都轮不到我。
还有我想问一下,审核累吗,最近 b 站开始给我推荐这个职位了。
我时常想着自己要是能够聪明一点就好了,无论情商还是智商,不用琢磨太长时间,是不是世界和时间能待我好一点,但聪明是相对于其它人,是比其它人更聪明。即使现在这个 “我” 能更聪明,那么还有大多数的 “我” 就被比下去了。再怎么折腾,胜利也只属于少数的 “我”,“胜利者” 再怎么传授自己 “成功” 的经验,“我” 就在那眼巴巴地看着,那一切也没意义。
如果不聪明的大多数的 “我” 能够成功,找到路,那么胜利就属于所有的 “我”,这世界就能被点亮。所以我很庆幸自己是个 “笨蛋”,庆幸自己什么都不知道,庆幸自己什么都做不好,庆幸偿还一个个代价,庆幸自己踩中一个又一个的坑乃至于丢掉性命。如果我天生就会,或者很快学会游泳,我就不能理解不会游泳的人的痛苦了。
想念中,能有多少泪珠儿。
c.1 单例设计模式
内容导视:
- 单例设计模式
- 饿汉式
- 懒汉式
伙伴们,那个单例设计模式主要为枚举类做铺垫的,只要能弄清楚,如何保证类只有有限个实例就行,其它的看不懂的可以跳过,不影响后面的理解。
c.1.1 设计模式
转载来处:单例模式 | 菜鸟教程(runoob.com)
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
共有 23 种设计模式,下面只介绍单例设计模式。
c.1.2 单例设计模式
意图:保证一个类仅有一个实例,并提供一个取得其对象实例的方法,让其它调用者获取的是同一个对象,保证数据共享。
主要解决:一个全局使用的对象频繁地创建与销毁。
何时使用:当实例是重量级对象,想控制实例数目,节省系统资源的时候,保证只有一个对象被创建,可以节约资源。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
分类:饿汉式、懒汉式。
比如一个查看当前时间的,一个实例就够了。谁查看时间就把当前时间返回去。如果创建多个实例,都显示一样的时间,对象重复,浪费资源。如果显示不一样的时间,会造成误导,不知道哪个是正确的。只要唯一的实例即可。
c.1.3 饿汉式
饿汉:还未使用这个对象,但对象已经创建好了。
饿汉式
是否懒加载:否
是否多线程安全:是
实现难度:易
描述:这种方式比较常用,但容易产生垃圾对象。
优点:基于 ClassLoader 机制避免了多线程的同步问题,不用加锁,执行效率会提高。
缺点:虽然可能没有用这个对象,只是访问了它的静态变量而导致类初始化,但这个对象已经创建好了,造成资源的浪费。
步骤:
- 将构造器私有化,防止外部类直接 new
- 创建一个对象
- 提供公开的静态方法,返回这个对象
class Son {
// 类初始化时创建对象
private static Son son = new Son();
// 构造器私有化防止其他类直接 new
private Son() {}
// 返回创建的实例
public static Son getInstance() {
return son;
}
}
class Test {
public static void main(String[] args) {
// 使用 getInstance 方法获取对象,看看是否获取到的对象是同一个对象
Son son1 = Son.getInstance();
Son son2 = Son.getInstance();
System.out.println(son1 == son2);// true
}
}
枚举
JDK 版本:JDK5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
enum Son {
INSTANCE;
}
c.1.4 懒汉式
懒汉:获取实例时才创建。
懒汉式,线程不安全
是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易
缺点:在多线程下不能保证单例
class Son {
private static Son son;
private Son() {}
// 获取实例时,如果为 null,就创建对象
public static Son getInstance() {
if (son == null) {
son = new Son();
}
return son;
}
}
可以直接在方法上加 synchronized,但调用此方法频繁时,线程都排队等着,影响效率。
双检锁/双重校验锁(DCL,即 double-checked locking)
JDK 版本:JDK5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
class Son {
private static volatile Son son;
private Son() {}
// 获取实例时,如果为 null,就创建对象
public static Son getInstance() {
if(son == null) {
synchronized (Son.class) {
if (son == null) {
son = new Son();
}
}
}
return son;
}
}
登记式/静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 ClassLoader 机制来保证初始化 instance 时只有一个线程,但是只有当主动使用 SonHolder 类时(调用 getInstance 方法时),此类的静态变量才会被显式初始化,不会浪费资源。
class Son {
// 只有使用此类时(参考类的初始化时机),才会初始化此类,并显示初始化此静态常量
private static class SonHolder {
private static final Son INSTANCE = new Son();
}
private Son() {}
public static Son getInstance() {
return SonHolder.INSTANCE;
}
}
c.2 枚举类
内容导视:
- 自定义类实现枚举类
- enum
- 常用方法
c.2.1 自定义类实现枚举类
要求:设计一个季节类,而季节只有 4 个对象:春夏秋冬,要是按普通创建对象的方式,可以随意创建实例,不能体现季节是 4 个对象。
可以借助单例模式的设计思想,将构造器设为私有,控制实例数量。
枚举类:将有限个、具体的对象一个个例举出来的类。
- 枚举对应英文 enumeration,简写 enum
- 枚举是一组常量的集合
- 当实例为有限个,使用枚举类更好
- 当 true、false 不足以表示所有结果时,使用枚举
步骤
- 在本类中创建本类的对象(枚举对象)。
- 枚举对象值通常为只读,不用修改,没有 set 方法。
- 构造方法私有化,避免其它类创建对象。
- 为 final 类,防止被继承;因为私有构造器本应没有后代。
final class Season {
public final static Season SPRING = new Season("春天", "春暖花开");
public final static Season SUMMER = new Season("夏天", "烈日炎炎");
public final static Season AUTUMN = new Season("秋天", "秋高气爽");
public final static Season WINTER = new Season("冬天", "天寒地冻");
private String name;
private String description;
private Season(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}
class Test {
public static void main(String[] args) {
// 保证本类只有 4 个实例
Season spring = Season.SPRING;
Season summer = Season.SUMMER;
System.out.println(spring.getDescription());
System.out.println(summer.getName());
}
}
c.2.2 enum
JDK5 新特性,简化创建枚举类的过程。
语法:每个常量名都对应一个对象,不同对象使用 , 分隔,最后使用 ; 结尾。(当枚举对象后没有其它语句时,; 可以省略)
enum 枚举类名 {
常量名(实参),
...
常量名(实参);
}
例:(请与自定义枚举类对比,看看省略了哪些内容)
enum Season {
// 序号按声明顺序排
SPRING("春天", "春暖花开"),// 0
SUMMER("夏天", "烈日炎炎"),// 1
AUTUMN("秋天", "秋高气爽"),// 2
WINTER("冬天", "天寒地冻");// 3
private String name;
private String description;
// 访问权限默认私有
Season(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
}
class Test {
public static void main(String[] args) {
Season spring = Season.SPRING;
Season summer = Season.SUMMER;
System.out.println(spring.getDescription());
System.out.println(summer.getName());
}
}
补充几点:
1.构造器访问权限默认为 private,不可更改
2.如果调用的是无参构造,可以省略枚举对象的小括号
enum Micro {
S, M, I, L
}
3.枚举对象必须定义在类的最前面
4.由于枚举类隐式继承了 Enum 类,而 Java 只支持单继承,不能再继承其它类了,只能通过实现接口扩展功能
通过 javap 命令反编译,发现 enum 本质也是类,继承了父类 Enum。
final class Season extends java.lang.Enum<Season> {
public static final Season SPRING;
public static final Season SUMMER;
public static final Season AUTUMN;
public static final Season WINTER;
private java.lang.String name;
private java.lang.String description;
private static final Season[] $VALUES;
// 返回枚举对象数组
public static Season[] values();
// 根据枚举常量名返回对应的枚举对象
public static Season valueOf(java.lang.String);
private Season(java.lang.String, java.lang.String);
public java.lang.String getName();
public java.lang.String getDescription();
static {};
}
调用 Season 的 <init> 方法时,会调用 Enum 的 <init> 方法,传入常量名与序号,赋给 Enum 类的实例变量 name、ordinal。
Season spring = Season.SPRING;
System.out.println(Season.SPRING.name());// SPRING
System.out.println(Season.SPRING.ordinal());// 0
c.2.3 常用方法
int compareTo(E o):返回 this.ordinal - o.ordinal,可以比较两个枚举常量的大小。
boolean equals(Object other):比较两个枚举常量是否相等,底层使用 == 实现,只有当自己与自己比较时,才会返回 true。
Class<E> getDeclaringClass():返回与此枚举常量的枚举类型对应的 Class 对象。
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
为什么不直接返回 this 的运行时类型,而要先获取父类型?
因为枚举常量允许指向子类型的实例。
class State {
// 匿名内部类对象
public static final State CLOSE = new State() {};
public static final State OPEN = new State() {};
}
enum State {
CLOSE {},
OPEN {};
}
此时 CLOSE 调用 getClass 方法返回的就是 State$1 类型。
想要获取枚举对象的实际类型就调用 getClass 方法,用于判断两个枚举对象是否是同一类型;想要获取枚举对象所在的枚举类型,就调用 getDeclaringClass 方法,用于判断两个枚举对象是否定义在同一个枚举类中。
String name():返回枚举常量名
String toString():返回枚举常量名,因为此方法不是 final 修饰,可以重写此方法,使结果更易读。
int ordinal():返回此枚举常量的序号(它在其枚举声明中的位置,其中初始常量的序号为零)。
static T valueOf(Class<T> enumType, String name):根据枚举类型和枚举常量名找到对应的枚举对象。
State close = Enum.valueOf(State.class, "CLOSE");
State close1 = State.valueOf("CLOSE");
System.out.println(close1 == close);// true
Object clone():为了保证实例数量不变,枚举类型不支持克隆,所以此方法内部抛出 CloneNotSupportedException 异常,且有 final 修饰。
static Season[] values():返回枚举对象数组
State[] states = State.values();
for (int i = 0; i < states.length; i++) {
System.out.println(states[i]);// CLOSE、OPEN
}
// 通过枚举类的 Class 对象调用 getEnumConstants 方法也能得到所有枚举对象
State[] s1 = State.class.getEnumConstants();
State[] s2 = State.CLOSE.getDeclaringClass().getEnumConstants();
枚举常量允许指向子类型的实例
在匿名内部类中定义方法,没法直接使用,因为这是子类独有的方法,必须向下转型,但转成什么,State$1 吗?这是编译之后才生成的,在此之前使用通不过编译。
enum State {
OPEN {
public void some() {
System.out.println("OPEN 的 some 方法");
}
},
CLOSE;
}
class Test {
public static void main(String[] args) {
State open = State.OPEN;
open.some();// 找不到符号
}
}
可以在 State 中定义 some 方法,在匿名内部类中重写,使用多态。(枚举类中可以定义抽象方法)
// 本质是继承 Enum 的抽象类
enum State {
OPEN {
@Override
public void some() {
System.out.println("some 方法~");
}
};
public abstract void some();
}
class Test {
public static void main(String[] args) {
State.OPEN.some();
}
}
c.x 总结回顾
单例模式保证每次获取都是同一个对象,节省资源。
懒汉式与饿汉式的区别:
- 创建对象的时机不同:饿汉式是在类初始化时创建对象实例,而懒汉式是在使用时才创建。
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。
- 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
什么时候使用枚举
- 实例有限个时
- true、false 不足以表示所有结果,枚举配合 switch 语句更好
Color color = Color.RED;
switch (color) {
// case 后直接写枚举常量名就行,不需要加前缀
case RED:
color = Color.YELLOW;
break;
case YELLOW:
color = Color.GREEN;
break;
case GREEN:
color = Color.RED;
break;
}
public enum Color {
RED, YELLOW, GREEN
}
c.y 脑海练习
c.1 运行后输出?
enum Clothes {
JACKET, TROUSERS;
}
class Test {
public static void main(String[] args) {
Clothes j1 = Clothes.JACKET;
Clothes j2 = Clothes.JACKET;
System.out.println(j1);
System.out.println(j1.equals(j2));
}
}
c.z 习题答案
c.1 运行后输出?
enum Clothes {
JACKET, TROUSERS;
}
class Test {
public static void main(String[] args) {
Clothes j1 = Clothes.JACKET;
Clothes j2 = Clothes.JACKET;
System.out.println(j1);
System.out.println(j1.equals(j2));
}
}
toString 默认返回常量名,输出 JACKET,equals 默认使用双等号比较,j1、j2 指向同一个对象,输出 true。
本文暂时没有评论,来添加一个吧(●'◡'●)