“二哥,这节讲注解吗?”三妹问。
“是的。”我说,“注解是 Java 中非常重要的一部分,但经常被忽视也是真的。之所以这么说是因为我们更倾向成为一名注解的使用者而不是创建者。 注解用过吧?方法重写的时候用到过。但你知道怎么自定义一个注解吗?”
三妹毫不犹豫地摇摇头,摆摆手,不好意思地承认自己的确没有自定义过。
“好吧,哥来告诉你吧。”
注解(Annotation)是在 Java 1.5 时引入的概念,同 class 和 interface 一样,也属于一种类型。注解提供了一系列数据用来装饰程序代码(类、方法、字段等),但是注解并不是所装饰代码的一部分,它对代码的运行效果没有直接影响,由编译器决定该执行哪些操作。
来看一段代码。
注意到 这个注解了吧?它本来是为 Spring(后面会讲)容器注入 Bean 的,现在被我无情地扔在了字段 name 的身上,但这段代码所在的项目中并没有启用 Spring,意味着 注解此时只是一个摆设。
“既然只是个摆设,那你这个地方为什么还要用 呢?”三妹好奇地问。
“傻呀你,就是给你举个例子,证明:注解对代码的运行效果没有直接影响,明白我的用意了吧?”我毫不客气地说。
“哦。”三妹若有所思地说。
“认真听哈,接下来给你讲讲注解的生命周期。”我瞅了瞅三妹,看她是否在专注的听,然后继续说,“注解的生命周期有 3 种策略,定义在 RetentionPolicy 枚举中。”
1)SOURCE:在源文件中有效,被编译器丢弃。
2)CLASS:在编译器生成的字节码文件中有效,但在运行时会被处理类文件的 JVM 丢弃。
3)RUNTIME:在运行时有效。这也是注解生命周期中最常用的一种策略,它允许程序通过反射的方式访问注解,并根据注解的定义执行相应的代码。
“然后我们来讲注解装饰的目标。”我看三妹还在线,就继续说。
注解的目标定义了注解将适用于哪一种级别的 Java 代码上,有些注解只适用于方法,有些只适用于成员变量,有些只适用于类,有些则都适用。截止到 Java 9,注解的类型一共有 11 种,定义在 ElementType 枚举中。
1)TYPE:用于类、接口、注解、枚举
2)FIELD:用于字段(类的成员变量),或者枚举常量
3)METHOD:用于方法
4)PARAMETER:用于普通方法或者构造方法的参数
5)CONSTRUCTOR:用于构造方法
6)LOCAL_VARIABLE:用于变量
7)ANNOTATION_TYPE:用于注解
8)PACKAGE:用于包
9)TYPE_PARAMETER:用于泛型参数
10)TYPE_USE:用于声明语句、泛型或者强制转换语句中的类型
11)MODULE:用于模块
“哥,你将这些我都记不住,能不能直接开撸注解呀!!!!!”三妹不耐烦了。
“确实哈,说再多,都不如撸个注解来得让人心动。撸个什么样的注解呢?一个字段注解吧,它用来标记对象在序列化成 JSON 的时候要不要包含这个字段。”我笑着对三妹说,“怎么样?”
“好呀!”
“来看下面这段代码。”
1)JsonField 注解的生命周期是 RUNTIME,也就是运行时有效。
2)JsonField 注解装饰的目标是 FIELD,也就是针对字段的。
3)创建注解需要用到 关键字。
4)JsonField 注解有一个参数,名字为 value,类型为 String,默认值为一个空字符串。
“为什么参数名要为 value 呢?有什么特殊的含义吗?”三妹问。
“当然是有的,value 允许注解的使用者提供一个无需指定名字的参数。举个例子,我们可以在一个字段上使用 ,也可以把 省略,变成 。”我说。
“那 有什么特殊含义吗?”三妹继续问。
“当然也是有的,它允许我们在一个字段上直接使用 ,而无需指定参数的名和值。”我回答说。
“明白了,那 注解已经撸好了,是不是可以使用它了呀?”三妹激动地说。
“嗯,假设有一个 Writer 类,他有 3 个字段,分别是 age、name 和 bookName,后 2 个是必须序列化的字段。就可以这样来用 注解。”我说。
1)name 上的 注解提供了显式的字符串值。
2)bookName 上的 注解使用了缺省项。
接下来,我们来编写序列化类 JsonSerializer,内容如下:
“JsonSerializer 类的内容看起来似乎有点多啊,二哥,我有点看不懂。”三妹说。
“不要怕,我一点点来解释,直到你搞明白为止。”
1) 方法是用来序列化对象的,它接收一个 Object 类型的参数。 通过反射的方式获取对象声明的所有字段,然后进行 for 循环遍历。在 for 循环中,先通过 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常);再通过 判断字段是否装饰了 注解,如果是的话,调用 方法,以及获取该对象上由此字段表示的值,并放入 jsonElements 中。
2) 方法用来获取字段上注解的值,如果注解的值是空的,则返回字段名。
3) 方法借助 Stream 流的方式返回格式化后的 JSON 字符串。Stream 流你还没有接触过,不过没关系,后面我再给你讲。
“现在是不是豁然开朗了?”我问三妹,看到三妹点了点头,我继续说,“接下来,我们来写一个测试类 JsonFieldTest。”
程序输出结果如下:
从结果上来看:
1)Writer 类的 age 字段没有装饰 注解,所以没有序列化。
2)Writer 类的 name 字段装饰了 注解,并且显示指定了字符串“writerName”,所以序列化后变成了 writerName。
3)Writer 类的 bookName 字段装饰了 注解,但没有显式指定值,所以序列化后仍然是 bookName。
“怎么样,三妹,是不是也不是特别难?”我对三妹说。
“撸个注解好像真没什么难度,但你接下来的那个 JsonSerializer 我还需要再消化一下。”三妹很认真地说。
“嗯,你好好复习下,我看会《编译原理》。”说完我拿起桌子边上的一本书就走了。
GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程
微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。
“今天我们来学习枚举吧,三妹!”我说,“同学让你去她家玩了两天,感觉怎么样呀?”
“心情放松了不少。”三妹说,“可以开始学 Java 了,二哥。”
“OK。”
“枚举(enum),是 Java 1.5 时引入的关键字,它表示一种特殊类型的类,继承自 java.lang.Enum。”
“我们来新建一个枚举 PlayerType。”
“二哥,我没看到有继承关系呀!”
“别着急,看一下反编译后的字节码,你就明白了。”
“看到没?Java 编译器帮我们做了很多隐式的工作,不然手写一个枚举就没那么省心省事了。”
- 要继承 Enum 类;
- 要写构造方法;
- 要声明静态变量和数组;
- 要用 static 块来初始化静态变量和数组;
- 要提供静态方法,比如说 和 。
“确实,作为开发者,我们的代码量减少了,枚举看起来简洁明了。”三妹说。
“既然枚举是一种特殊的类,那它其实是可以定义在一个类的内部的,这样它的作用域就可以限定于这个外部类中使用。”我说。
PlayerType 就相当于 Player 的内部类。
由于枚举是 final 的,所以可以确保在 Java 虚拟机中仅有一个常量对象,基于这个原因,我们可以使用“==”运算符来比较两个枚举是否相等,参照 方法。
“那为什么不使用 方法判断呢?”三妹问。
“我来给你解释下。”
“==”运算符比较的时候,如果两个对象都为 null,并不会发生 ,而 方法则会。
另外, “==”运算符会在编译时进行检查,如果两侧的类型不匹配,会提示错误,而 方法则不会。
“枚举还可用于 switch 语句,和基本数据类型的用法一致。”我说。
“如果枚举中需要包含更多信息的话,可以为其添加一些字段,比如下面示例中的 name,此时需要为枚举添加一个带参的构造方法,这样就可以在定义枚举时添加对应的名称了。”我继续说。
“get 了吧,三妹?”
“嗯,比较好理解。”
“那接下来,我就来说点不一样的。”
“来吧,我准备好了。”
“EnumSet 是一个专门针对枚举类型的 Set 接口(后面会讲)的实现类,它是处理枚举类型数据的一把利器,非常高效。”我说,“从名字上就可以看得出,EnumSet 不仅和 Set 有关系,和枚举也有关系。”
“因为 EnumSet 是一个抽象类,所以创建 EnumSet 时不能使用 new 关键字。不过,EnumSet 提供了很多有用的静态工厂方法。”
“来看下面这个例子,我们使用 静态工厂方法创建了一个空的 PlayerType 类型的 EnumSet;使用 静态工厂方法创建了一个包含所有 PlayerType 类型的 EnumSet。”
“来看一下输出结果。”
有了 EnumSet 后,就可以使用 Set 的一些方法了,见下图。
“除了 EnumSet,还有 EnumMap,是一个专门针对枚举类型的 Map 接口的实现类,它可以将枚举常量作为键来使用。EnumMap 的效率比 HashMap 还要高,可以直接通过数组下标(枚举的 ordinal 值)访问到元素。”
“和 EnumSet 不同,EnumMap 不是一个抽象类,所以创建 EnumMap 时可以使用 new 关键字。”
有了 EnumMap 对象后就可以使用 Map 的一些方法了,见下图。
和 HashMap(后面会讲)的使用方法大致相同,来看下面的例子。
“来看一下输出结果。”
“除了以上这些,《Effective Java》这本书里还提到了一点,如果要实现单例的话,最好使用枚举的方式。”我说。
“等等二哥,单例是什么?”三妹没等我往下说,就连忙问道。
“单例(Singleton)用来保证一个类仅有一个对象,并提供一个访问它的全局访问点,在一个进程中。因为这个类只有一个对象,所以就不能再使用 关键字来创建新的对象了。”
“Java 标准库有一些类就是单例,比如说 Runtime 这个类。”
“Runtime 类可以用来获取 Java 程序运行时的环境。”
“关于单例,懂了些吧?”我问三妹。
“噢噢噢噢。”三妹点了点头。
“通常情况下,实现单例并非易事,来看下面这种写法。”
“要用到 volatile、synchronized 关键字等等,但枚举的出现,让代码量减少到极致。”
“就这?”三妹睁大了眼睛。
“对啊,枚举默认实现了 Serializable 接口,因此 Java 虚拟机可以保证该类为单例,这与传统的实现方式不大相同。传统方式中,我们必须确保单例在反序列化期间不能创建任何新实例。”我说。
“好了,关于枚举就讲这么多吧,三妹,你把这些代码都手敲一遍吧!”
“好勒,这就安排。二哥,你去休息吧。”
“嗯嗯。”讲了这么多,必须跑去抽烟机那里安排一根华子了。
GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程
微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/8348.html