2

《Java编程思想》

 2 years ago
source link: https://blog.51cto.com/u_15773567/5650986
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

《Java编程思想》

精选 原创

小二上酒8 2022-09-05 14:42:36 ©著作权

文章标签 java 编译器 职责链 文章分类 Java 编程语言 阅读数263

第十九章:枚举类型

关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

enum的基本特性

我们已经知道,调用enum的values()方法,可以遍历enum实例。values()方法返回enum实例的数组,而且该数组中的元素严格保持其在enum中声明时的顺序,因此你可以在循环中使用values()返回的数组。

创建enum时,编译器会为你生成一个相关的类,这个类继承自java.lang.Enum。下面的例子演示了Enum提供的一些功能∶

package enumerated;

/**
* @author Mr.Sun
* @date 2022年09月02日 15:58
*
* 枚举的基本特性
*/
public class EnumClass {
public static void main(String[] args) {
for(Shrubbery s : Shrubbery.values()) {
System.out.println(s + " ordinal: " + s.ordinal());
System.out.print(s.compareTo(Shrubbery.CRAWLING) + " ");
System.out.print(s.equals(Shrubbery.CRAWLING) + " ");
System.out.println(s == Shrubbery.CRAWLING);
System.out.println(s.getDeclaringClass());
System.out.println(s.name());
System.out.println("----------------------");
}

// 从字符串名称生成枚举值
for(String s : "HANGING CRAWLING GROUND".split(" ")) {
Shrubbery shrub = Enum.valueOf(Shrubbery.class, s);
System.out.println(shrub);
}
}
}

enum Shrubbery {
GROUND, CRAWLING, HANGING
}

运行结果如下图:

《Java编程思想》_职责链

ordinal()方法返回一个int值,这是每个enum实例在声明时的次序,从0开始。可以使用==来比较enum实例,编译器会自动为你提供equals()和hashCode()方法。Enum类实现了Comparable 接口,所以它具有compareTo()方法。同时,它还实现了Serializable接口。

如果在enum实例上调用getDeclaringClass()方法,我们就能知道其所属的enum类。name()方法返回enum实例声明时的名字,这与使用toString()方法效果相同。valueOf()是在Enum中定义的static方法,它根据给定的名字返回相应的enum实例,如果不存在给定名字的实例,将会抛出异常。

values()的神秘之处

前面已经提到,编译器为你创建的enum类都继承自Enum类。然而,如果你研究一下Enum 类就会发现,它并没有values()方法。可我们明明已经用过该方法了,难道存在某种“隐藏的”方法吗?我们可以利用反射机制编写一个简单的程序,来查看其中的究竟∶

package enumerated;

import utils.OSExecute;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;

/**
* @author Mr.Sun
* @date 2022年09月02日 16:10
*
* 使用反射机制研究枚举类的values()
*/
enum Explore {
HERE, THERE
}

public class Reflection {

public static Set<String> analyze(Class<?> enumClass) {
System.out.println("----- Analyzing " + enumClass + " -----");
System.out.println("Interfaces:");
for (Type t : enumClass.getGenericInterfaces()) {
System.out.println(t);
}
System.out.println("Base: " + enumClass.getSuperclass());
System.out.println("Methods: ");
Set<String> methods = new TreeSet<String>();
for (Method method : enumClass.getMethods()) {
methods.add(method.getName());
}
System.out.println(methods);
return methods;
}

public static void main(String[] args) {
Set<String> exploreMethods = analyze(Explore.class);
Set<String> enumMethods = analyze(Enum.class);
System.out.println("Explore.containsAll(Enum)? " + exploreMethods.containsAll(enumMethods));
System.out.print("Explore.removeAll(Enum): ");
exploreMethods.removeAll(enumMethods);
System.out.println(exploreMethods);
// Decompile the code for the enum:
OSExecute.command("javap G:/github/cnblogs/gitee/thinking-in-java/out/production/thinking-in-java/enumerated/Explore.class");
}

} /* Output:
----- Analyzing class enumerated.Explore -----
Interfaces:
Base: class java.lang.Enum
Methods:
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, values, wait]
----- Analyzing class java.lang.Enum -----
Interfaces:
java.lang.Comparable<E>
interface java.io.Serializable
Base: class java.lang.Object
Methods:
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, wait]
Explore.containsAll(Enum)? true
Explore.removeAll(Enum): [values]
Compiled from "Reflection.java"
final class enumerated.Explore extends java.lang.Enum<enumerated.Explore> {
public static final enumerated.Explore HERE;
public static final enumerated.Explore THERE;
public static enumerated.Explore[] values();
public static enumerated.Explore valueOf(java.lang.String);
static {};
}
*///:~

答案是,values()是由编译器添加的static方法。可以看出,在创建Explore的过程中,编译器还为其添加了valueOf()方法。这可能有点令人迷惑,Enum类不是已经有valueOf()方法了吗。不过Enum中的valueOf()方法需要两个参数,而这个新增的方法只需一个参数。由于这里使用的Set只存储方法的名字,而不考虑方法的签名,所以在调用Explore.removeAl(Enum)之后,就只剩下【values】了。

从最后的输出中可以看到,编译器将Explore标记为final类,所以无法继承自enum。其中还有一个static的初始化子句,稍后我们将学习如何重定义该句。

由于擦除效应(在第15章中介绍过),反编译无法得到Enum的完整信息,所以它展示的Explore的父类只是一个原始的Enum,而非事实上的Enum 。

由于values()方法是由编译器插入到enum定义中的static方法,所以,如果你将enum实例向上转型为Enum,那么values()方法就不可访问了。不过,在Class中有一个getEnumConstants()方法,所以即便Enum接口中没有values()方法,我们仍然可以通过Class对象取得所有enum实例∶

enum Search {
HITHER, YON
}

public class UpcastEnum {
public static void main(String[] args) {
Search[] values = Search.values();
for (Search val : values) {
System.out.println(val.name());
}
System.out.println("--------------");
Enum e = Search.HITHER;
for (Enum en : e.getClass().getEnumConstants()) {
System.out.println(en);
}
}
}

运行结果如下:

《Java编程思想》_java_02

使用EnumSet代替标志

Set是一种集合,只能向其中添加不重复的对象。当然,enum也要求其成员都是唯一的,所以enum看起来也具有集合的行为。不过,由于不能从enum中删除或添加元素,所以它只能算是不太有用的集合。Java SE5引入EnumSet,是为了通过enum创建一种替代品,以替代传统的"基于int的“位标志”。这种标志可以用来表示某种“开/关”信息,不过,使用这种标志,我们最终操作的只是一些bit,而不是这些bit想要表达的概念,因此很容易写出令人难以理解的代码。

EnumSet的设计充分考虑到了速度因素,因为它必须与非常高效的bit标志相竞争(其操作与HashSet相比,非常地快)。就其内部而言,它(可能)就是将一个long值作为比特向量,所以EnumSet非常快速高效。使用EnumSet的优点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能。EnumSet中的元素必须来自一个enum。下面的enum表示在一座大楼中,警报传感器的安放位置∶

package enumerated;

public enum AlarmPoints {
STAIR1, STAIR2,
LOBBY,
OFFICE1, OFFICE2, OFFICE3, OFFICE4,
BATHROOM, UTILITY, KITCHEN
}

然后,我们使用EnumSet来跟踪报警器的状态:

package enumerated;

import java.util.EnumSet;

import static enumerated.AlarmPoints.*;

/**
* @author Mr.Sun
* @date 2022年09月02日 17:18
*/
public class EnumSetTest {
public static void main(String[] args) {
EnumSet<AlarmPoints> points = EnumSet.noneOf(AlarmPoints.class); // Empty set
points.add(BATHROOM);
System.out.println(points);

points.addAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);

points = EnumSet.allOf(AlarmPoints.class);
points.removeAll(EnumSet.of(STAIR1, STAIR2, KITCHEN));
System.out.println(points);

points.removeAll(EnumSet.range(OFFICE1, OFFICE4));
System.out.println(points);

points = EnumSet.complementOf(points);
System.out.println(points);
}
}

运行结果如下图:

《Java编程思想》_职责链_03

使用EnumMap

EnumMap是一种特殊的Map,它要求其中的键(key)必须来自一个enum。由于enum本身的限制,所以EnumMap在内部可由数组实现。因此EnumMap的速度很快,我们可以放心地使用enum实例在EnumMap中进行查找操作。不过,我们只能将enum的实例作为键来调用put()方法,其他操作与使用一般的Map差不多。

下面的例子演示了命令设计模式的用法。一般来说,命令模式首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。接下来,程序员就可以构造命令对象,并在需要的时候使用它们了∶

package enumerated;

import java.util.EnumMap;
import java.util.Map;

import static enumerated.AlarmPoints.*;

/**
* @author Mr.Sun
* @date 2022年09月02日 17:25
*
* 使用EnumMap
*/
interface Command{ void action(); }

public class EnumMapTest {
public static void main(String[] args) {
EnumMap<AlarmPoints, Command> em = new EnumMap<>(AlarmPoints.class);
em.put(KITCHEN, () -> System.out.println("Kitchen fire!"));
em.put(BATHROOM, () -> System.out.println("Bathroom alert!"));
for(Map.Entry<AlarmPoints,Command> e : em.entrySet()) {
System.out.print(e.getKey() + ": ");
e.getValue().action();
}
try {
// If there's no value for a particular key:
em.get(UTILITY).action();
} catch(Exception e) {
System.out.println(e);
}
}
}/* Output:
BATHROOM: Bathroom alert!
KITCHEN: Kitchen fire!
java.lang.NullPointerException
*///:~

与EnumSet一样,enum实例定义时的次序决定了其在EnumMap中的顺序。

main()方法的 最后部分说明,enum的每个实例作为一个键,总是存在的,但是如果你没有为这个键调用put()方法来存入相应的值的话,对应的值就是null。

常量相关的方法

Java的Enum有一个非常有趣的特性,即它允许程序员为enum实例编写方法,从而为每个enum实例赋予各自不同的行为,要实现常量相关的方法,你需要为enum定义一个或多个abstract方法,然后为每个enum实例实现该抽象方法。参考下面的例子:

package enumerated;

import java.text.DateFormat;
import java.util.Date;

public enum ConstantSpecificMethod {

DATE_TIME {
String getInfo() {
return DateFormat.getDateInstance().format(new Date());
}
},
CLASSPATH {
String getInfo() {
return System.getenv("CLASSPATH");
}
},
VERSION {
String getInfo() {
return System.getProperty("java.version");
}
};

abstract String getInfo();

public static void main(String[] args) {
for(ConstantSpecificMethod csm : values()) {
System.out.println(csm.getInfo());
}
}
}/* Output:
2022-9-2
null
1.8.0_211
*///:~

使用enum的职责链

在职责链(Chain of Responsibility)设计模式中,程序员以多种不同的方式来解决一个问题,然后将它们链接在一起。当一个请求到来时,它遍历这个链,直到链中的某个解决方案能够处理该请求。

通过常量相关的方法,我们可以很容易地实现一个简单的职责链。我们以一个邮局的模型为例。邮局需要以尽可能通用的方式来处理每一封邮件,并且要不断尝试处理邮件,直到该邮件最终被确定为一封死信。其中的每一次尝试可以看作为一个策略(也是一个设计模式),而完整的处理方式列表就是一个职责链。

我们先来描述一下邮件。邮件的每个关键特征都可以用enum来表示。程序将随机地生成Mail对象,如果要减小一封邮件的GeneralDelivery为YES的概率,那最简单的方法就是多创建几个不是YES的enum实例,所以enum的定义看起来有点古怪。

我们看到Mail中有一个randomMail()方法,它负责随机地创建用于测试的邮件。而generator()方法生成一个Iterable对象,该对象在你调用next()方法时,在其内部使用randomMail()来创建Mail对象。这样的结构使程序员可以通过调用Mail.generator()方法,很容易地构造出一个foreach循环∶

package enumerated;

import utils.Enums;

import java.util.Iterator;

/**
* @author Mr.Sun
* @date 2022年09月02日 17:45
*
* 以邮局的模型为例,通过常量相关的方法,实现一个简单的职责链
*/
public class PostOffice {

enum MailHandler {
GENERAL_DELIVERY {
boolean handle(Mail m) {
switch (m.generalDelivery) {
case YES:
System.out.println("Using general delivery for " + m);
return true;
default:
return false;
}
}
},

MACHINE_SCAN {
boolean handle(Mail m) {
switch (m.scannability) {
case UNSCANNABLE:
return false;
default:
switch (m.address) {
case INCORRECT:
return false;
default:
System.out.println("Delivering " + m + " automatically");
return true;
}
}
}
},
VISUAL_INSPECTION {
boolean handle(Mail m) {
switch (m.readability) {
case ILLEGIBLE:
return false;
default:
switch (m.address) {
case INCORRECT:
return false;
default:
System.out.println("Delivering " + m + " normally");
return true;
}
}
}
},
RETURN_TO_SENDER {
boolean handle(Mail m) {
switch (m.returnAddress) {
case MISSING:
return false;
default:
System.out.println("Returning " + m + " to sender");
return true;
}
}
};

abstract boolean handle(Mail m);
}

static void handle(Mail m) {
for(MailHandler handler : MailHandler.values()) {
if(handler.handle(m)) {
return;
}
}
System.out.println(m + " is a dead letter");
}

public static void main(String[] args) {
for(Mail mail : Mail.generator(10)) {
System.out.println(mail.details());
handle(mail);
System.out.println("*****");
}
}
}

class Mail {
// “否”会降低随机选择的概率:
enum GeneralDelivery {YES, NO1, NO2, NO3, NO4, NO5}
enum Scannability {UNSCANNABLE, YES1, YES2, YES3, YES4}
enum Readability {ILLEGIBLE, YES1, YES2, YES3, YES4}
enum Address {INCORRECT, OK1, OK2, OK3, OK4, OK5, OK6}
enum ReturnAddress {MISSING, OK1, OK2, OK3, OK4, OK5}

GeneralDelivery generalDelivery;
Scannability scannability;
Readability readability;
Address address;
ReturnAddress returnAddress;
static long counter = 0;
long id = counter++;

public String toString() { return "Mail " + id; }

public String details() {
return toString() +
", General Delivery: " + generalDelivery +
", Address Scanability: " + scannability +
", Address Readability: " + readability +
", Address Address: " + address +
", Return address: " + returnAddress;
}

/**
* 生成测试邮件
*/
public static Mail randomMail() {
Mail m = new Mail();
m.generalDelivery= Enums.random(GeneralDelivery.class);
m.scannability = Enums.random(Scannability.class);
m.readability = Enums.random(Readability.class);
m.address = Enums.random(Address.class);
m.returnAddress = Enums.random(ReturnAddress.class);
return m;
}

public static Iterable<Mail> generator(final int count) {
return new Iterable<Mail>() {
int n = count;
public Iterator<Mail> iterator() {
return new Iterator<Mail>() {
public boolean hasNext() { return n-- > 0; }
public Mail next() { return randomMail(); }
public void remove() { // Not implemented
throw new UnsupportedOperationException();
}
};
}
};
}
}
/* Output:
Mail 0, General Delivery: NO2, Address Scanability: UNSCANNABLE, Address Readability: YES3, Address Address: OK1, Return address: OK1
Delivering Mail 0 normally
*****
Mail 1, General Delivery: NO5, Address Scanability: YES3, Address Readability: ILLEGIBLE, Address Address: OK5, Return address: OK1
Delivering Mail 1 automatically
*****
Mail 2, General Delivery: YES, Address Scanability: YES3, Address Readability: YES1, Address Address: OK1, Return address: OK5
Using general delivery for Mail 2
*****
Mail 3, General Delivery: NO4, Address Scanability: YES3, Address Readability: YES1, Address Address: INCORRECT, Return address: OK4
Returning Mail 3 to sender
*****
Mail 4, General Delivery: NO4, Address Scanability: UNSCANNABLE, Address Readability: YES1, Address Address: INCORRECT, Return address: OK2
Returning Mail 4 to sender
*****
Mail 5, General Delivery: NO3, Address Scanability: YES1, Address Readability: ILLEGIBLE, Address Address: OK4, Return address: OK2
Delivering Mail 5 automatically
*****
Mail 6, General Delivery: YES, Address Scanability: YES4, Address Readability: ILLEGIBLE, Address Address: OK4, Return address: OK4
Using general delivery for Mail 6
*****
Mail 7, General Delivery: YES, Address Scanability: YES3, Address Readability: YES4, Address Address: OK2, Return address: MISSING
Using general delivery for Mail 7
*****
Mail 8, General Delivery: NO3, Address Scanability: YES1, Address Readability: YES3, Address Address: INCORRECT, Return address: MISSING
Mail 8 is a dead letter
*****
Mail 9, General Delivery: NO1, Address Scanability: UNSCANNABLE, Address Readability: YES2, Address Address: OK1, Return address: OK4
Delivering Mail 9 normally
*****
*///:~

职责链由enum MailHandler实现,而enum定义的次序决定了各个解决策略在应用时的次序。对每一封邮件,都要按此顺序尝试每个解决策略,直到其中一个能够成功地处理该邮件,如果所有的策略都失败了,那么该邮件将被判定为一封死信。

第二十章:注解

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用某些数据。

package annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

除了@符号以外,@Test的定义很像一个空的接口。定义注解时,会需要一些元注解(meta-annotation),如@Target和@Retention。@Target用来定义你的注解将应用于什么地方(例如是一个方法或者一个域)。@Rectetion用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME)。

没有元素的注解称为标记注解,例如上例种的@Test。

注解元素可用的类型如下:

  • 所有基本类型(int, float, boolean等)
  • String
  • Class
  • Annotation
  • 以上类型的数组

如果你使用了其它类型,编译器就会报错。注意,也不允许使用任何包装类型,不过由于自动打包的存在,这算不上什么限制。注解也可以作为元素的类型,也就是说,注解可以嵌套。

Java目前只内置了四种元注解,元注解专职负责注解其它的注解:

[图片上传失败...(image-2b04a1-1662342777448)]

大多数时候,程序员主要是定义自己的注解,并编写自己的处理器来处理它们。

下面是一个简单的注解,我们可以用它来跟踪一个项目中的用例。如果一个方法或一组方法实现了某个用例的需求,那么程序员可以为此方法加上该注解。于是,项目经理通过计算已经实现的用例,就可以很好地掌控项目的进展。而如果要更新或修改系统的业务逻辑,则维护该项目的开发人员也可以很容易地在代码中找到对应的用例。

package annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
public int id();
public String description() default "no description";
}

注意,id和description类似方法定义。由于编译器会对id进行类型检查,因此将用例文档的追踪数据库与源代码相关联是可靠的。description元素有一个default值,如果在注解某个方法时没有给出description的值,则该注解的处理器就会使用此元素的默认值。

在下面的类中,有三个方法被注解为用例∶

package annotations;

import java.util.List;

/**
* @author Mr.Sun
* @date 2022年09月03日 9:05
*
* 注解用例
*/
public class PasswordUtils {

@UseCase(id = 47, description = "密码必须至少包含一个数字")
public boolean validatePassword(String password) {
return (password.matches("\\w*\\d\\w*"));
}

@UseCase(id = 48)
public String encryptPassword(String password) {
return new StringBuilder(password).reverse().toString();
}

@UseCase(id = 49, description = "新密码不能等于以前使用的密码")
public boolean checkForNewPassword(List<String> prevPasswords, String password) {
return !prevPasswords.contains(password);
}
}

注解的元素在使用时表现为名一值对的形式,并需要置于@UseCase声明之后的括号内。在encryptPassword()方法的注解中,并没有给出description元素的值,因此,在UseCase的注解处理器分析处理这个类时会使用该元素的默认值。

你应该能够想象得到如何使用这套工具来“勾勒”出将要建造的系统,然后在建造的过程中逐渐实现系统的各项功能。

编写注解处理器

如果没有用来读取注解的工具,那注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具apt帮助程序员解析带有注解的Java源代码。

下面是一个非常简单的注解处理器,我们将用它来读取PasswordUtils类,并使用反射机制查找@UseCase标记。我们为其提供了一组id值,然后它会列出在PasswordUtils种找到的用例,以及缺失的用例。

package annotations;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* @author Mr.Sun
* @date 2022年09月03日 9:11
*
* 编写注解处理器
*/
public class UseCaseTracker {

public static void trackUseCases(List<Integer> useCases, Class<?> clazz) {
for (Method m : clazz.getDeclaredMethods()) {
UseCase useCase = m.getAnnotation(UseCase.class);
if (useCase != null) {
System.out.println("找到@UseCase:" + useCase.id() + " " + useCase.description());
useCases.remove(Integer.valueOf(useCase.id()));
}
}
for (Integer i : useCases) {
System.out.println("警告: 缺失用例:" + i);
}
}

public static void main(String[] args) {
List<Integer> useCases = new ArrayList<>();
Collections.addAll(useCases, 47, 48, 49, 50);
trackUseCases(useCases, PasswordUtils.class);
}
} /* Output:
找到@UseCase:49 新密码不能等于以前使用的密码
找到@UseCase:47 密码必须至少包含一个数字
找到@UseCase:48 no description
警告: 缺失用例:50
*///:~

这个程序用到了两个反射的方法∶getDeclaredMethods()和getAnnotation(),它们都属于AnnotatedElement接口(Class、Method与Feld等类都实现了该接口)。getAnnoation()方法返回指定类型的注解对象,在这里就是UseCase。如果被注解的方法上没有该类型的注解,则返回null值。然后我们通过调用id()和description()方法从返回的UseCase对象中提取元素的值。其中,encriptPassword()方法在注解的时候没有指定description的值,因此处理器在处理它对应的注解时,通过description()方法取得的是默认值no description。

枚举和注解其实在日常开发中都很熟悉,因为是非常基础的知识,本文也只是把模糊的概念和比较冷门的知识点记录下来,方便日后查阅,原来是打算把第二十一章的内容也放在本文的,但发现并发这章内容太多了,限于篇幅,还是打算单独写一篇文章进行记录,而且并发也是比较重要的基础知识。等把并发这章内容看完了,《Java编程思想》读书笔记系列也就告一段落了

  • 收藏
  • 评论
  • 分享
  • 举报

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK