你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java16 是在 2021 年 3 月发布的一个短期版本,新增特性如下:
接下来我们一起看看这些特性。
在 JDK15 之前,JDK 中使用的 C++语言限制在 C++98/03 版本,没有办法使用更高级的特性,从 JDK16 开始,可以支持 C++14 的语言特性。
这一点更新对应用开发者可能关系不大,但是对于底层组件的开发者意义重大。Java 的版本更新迅速,C++的特性也是飞速更新,如果 JDK 还是限制在 C++98/03 版本,没有办法使用 C++11/14 中的高级特性,也是一种损失。
这是两个提案,JEP 357 是将 OpenJDK 社区的源代码版本控制工具,从 Mercurial(hg)迁移到 Git,JEP 369 是将 OpenJDK 项目定向到 GitHub 中的仓库,我们可以看到从 OpenJDK 的 JIRA 工具中,代码提交和 Issue 预览的都是在 https://github.com/openjdk 中,有一部分是从 https://git.openjdk.java.net 重定向到 GitHub。
Mercurial(hg)是一个 Python 编写的跨平台的分布式版本控制软件,与 Git 是同一时代开始的工具,功能也是很强大,只是在发展过程中,有些方面稍弱于 Git,比如元数据的占用、与现代工具链的集成。所以 OpenJDK 转而投向了 Git 的怀抱。
ZGC 是在 Java11 引入的(参见 Java11 新特性),在 Java15 中正式特性(参见 Java15 新特性),可以用命令-XX:+UseZGC
启用 ZGC。
ZGC 是一个并发的垃圾回收器,可以极大地提升 GC 的性能,支持任意堆大小而保持稳定的低延迟。在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:
ZGC 的目标是实现垃圾回收与程序同时运行,将 STW 降低为 0,即不存在中断。目前在标记、重定位、参考处理、类卸载和跟处理阶段删除安全点处理。目前 ZGC 中仍然依靠安全点执行的包括部分的根处理和有时间限制的标记终止操作。这些根处理中有一项就是 Java 线程堆栈处理。随着线程数量增加,停顿时间增长。所以,我们需要实现并发的堆栈处理。目标包括:
对于本地进程间通信,Unix 套接字比 TCP/IP 更加安全高效。Unix 套接字一直是大多数 Unix 平台的一个特性,现在在 Windows 10 和 Windows Server 2019 也提供了支持。
所以在 Java16 中为java.nio.channels
包的SocketChannel
和ServerSocketChannel
添加了 Unix(AF_UNIX)套接字支持。Unix 套接字用于同一主机上的进程间通信(IPC), 在很大程度上类似于 TCP/IP,区别在于套接字是通过文件系统路径名而不是 Internet 协议(IP)地址和端口号寻址的。对于本地进程间通信,Unix 套接字比 TCP/IP 环回连接更安全、更有效。
这些移植的价值不在于移植本身,而在于支持平台的多样性。Java 的口号是一次编写到处运行。既然要到处运行,就得支持各种平台。而且,针对不同的操作系统支持,还能给我们提供更多的选择。
Alpine Linux 是一个独立非商业的 Linux 发行版,体系非常小,一个容器需要不超过 8MB 的空间,磁盘最小仅需 130MB 存储。如果我们通过 jlink 生产 JDK,Docket 镜像可以减小到 38MB,这样在微服务部署过程中,可以减少很多磁盘占用,也能减少镜像传输、部署时间。
这是在 HotSpot 中的空间分配上的优化,将未使用的元空间(metaspace,也叫类的元空间)中的内容存返回给操作系统。
应用程序如果存在大量类加载和类卸载的动作时,会占用大量的元空间内存,这部分内存得不到释放,造成内存利用率低。现在的应用系统为了应对高并发的流量,动辄部署数十上百台实例,这将造成极大的资源浪费。
元空间的内存方式使用的是基于区域的内存管理方式(Region-based memory management),也就是每个分配的对象都被分配到一个区域中。这里的区域有不同的叫法:zone(区域)、arena(竞技场)、memory context(内存上下文)等。
当类被回收后,其元空间区域中的内存块会返回自由列表中,以便以后重新使用。当然,可能很长使用不会被重新使用。这样就会造成元空间中很多的内存碎片,这些都是被标记为占用的内存。如果没有碎片的内存空间,是可以返回给操作系统的。
在 JEP 387 特性中,提出使用基于伙伴的内存分配算法(Buddy memory allocation)改善元空间的内存使用,这种方式是一种在 Linux 内核中经过验证的成熟算法。这种算法是在很小的块(chunk)中分配内存,这会降低类加载器的开销。
同时,JEP 387 增加了内存延迟提交给内存区域的特性,这样就会减少那种申请了内存却不使用的情况。
最后,JEP 387 将元空间的内存区域设计为不同大小,可以满足不同大小需求的内存申请。
这些操作与 Java13 中对 ZGC 的增强特性很类似(参见 Java13 的新特性)。他山之石可以攻玉,我们不妨学习一下这些方式,对我们在以后的开发中提供思路。
将基于值的类的公共构造函数设置启用移除警告。
比如Interger
的构造函数上设置了@Deprecated(since="9", forRemoval = true)
。如果某个类使用了Integer integer = new Integer(1);
这种写法,通过javac
命令编译时,会收到警告:[removal] Integer 中的 Integer(int) 已过时,且标记为待删除
这种警告信息。
基于值的类在类定义上都会有@jdk.internal.ValueBased
注解,比如java.lang.Integer
、java.lang.Double
等。这样的改动是为 Valhalla 项目做准备。
打包工具是在 Java14 中引入的孵化功能(参见 Java14 的新特性),可以打包成自包含的 Java 应用程序,比如 Windows 的 exe 和 msi、Mac 的 pkg 和 dmg、Linux 的 deb 和 rpm 等。
我们可以使用jlink
创建一个最小可运行的模块包,然后使用jpackage
将其构建成安装包:
jpackage --name myapp --input lib --main-jar main.jar
这里需要注意一点,因为已经成为正式功能,模块名从jdk.incubator.jpackage
改为jdk.jpackage
。
instanceof 模式匹配首先在 Java14 中提供预览功能(参见 Java14 特性),可以提供instanceof
更加简洁高效的实现,在 Java15 中进行了第二次预览,用于收集反馈,终于是多年的媳妇熬成婆,在 Java16 中成为正式功能。
我们再简单复习一下instanceof
模式匹配的功能(详细使用可以移步 Java14 特性):
@Testvoid test1() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String str) { result = str.length(); } else if (obj1 instanceof Number num) { result = num.intValue(); } Assertions.assertEquals(13, result);}
Record 类型用来增强 Java 语言特性,充当不可变数据载体。与 instanceof 模式匹配一样,Record 类型也是在 Java14 中提供预览功能(参见 Java14 新特性),在 Java15 中进行了第二次预览,用于收集反馈。
我们再简单复习一下 Record 类型的功能,比如,我们定义一个Person
类:
public record Person(String name, String address) {}
我们转换为之前的定义会是一坨下面这种代码:
public final class PersonBefore14 { private final String name; private final String address; public PersonBefore14(String name, String address) { this.name = name; this.address = address; } public String name() { return name; } public String address() { return address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonBefore14 that = (PersonBefore14) o; return Objects.equals(name, that.name) && Objects.equals(address, that.address); } @Override public int hashCode() { return Objects.hash(name, address); } @Override public String toString() { return "PersonBefore14{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; }}
Record 类型特性有四个特性:
equals
、getter
、setter
等方法;我们不能将 Record 类型简单的理解为去除“样板化”代码的功能,它不是解决 JavaBean 命名约定的中很多模板化方法的冗余繁杂问题,它的目标不是类似 Lombok 等工具自动生成代码的功能,是从开发人员专注模型的角度出发的。
这个功能特性是为了改进 JDK 的安全性和可维护性,是 Jigsaw 项目的主要目标之一。所以在 Java16 中,默认强封装 JDK 的绝大部分内部 API,有些关键性的 API,比如sun.misc.Unsafe
暂时可以放心使用。
我们可以使用启动参数--illegal-access
控制内部 API 的封装程度:
--illegal-access=permit
:JDK 8 中存在的每个包对未命名模块中的代码开放。也就是放心大胆地使用。Java9 中默认就是这个等级;--illegal-access=warn
:与许可相同,不同之处在于每次非法反射访问操作都会发出警告消息;--illegal-access=debug
:与 warn 相同,不同的是,每个非法反射访问操作都会发出警告消息和堆栈跟踪;--illegal-access=deny
:禁用所有非法访问操作,但由其他命令行选项(例如--add-opens
)启用的操作除外。密封类首次在 Java15 中预览(参见 Java15 新特性),在 Java16 中进行第二次预览,我们在复习一下功能:
public sealed interface JungleAnimal permits Monkey, Snake {}public final class Monkey implements JungleAnimal {}public non-sealed class Snake implements JungleAnimal {}
sealed
关键字与permits
关键字结合使用,以确定允许哪些类实现此接口。在我们的例子中,是Monkey
和Snake
。
sealed
:必须使用permits
关键字定义允许继承的子类;final
:最终类,不再有子类;non-sealed
:普通类,任何类都可以继承它。这是为向量计算专门定义的 API,可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。
尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。
final int[] a = {1, 2, 3, 4};final int[] b = {5, 6, 7, 8};final int[] c = new int[3];IntVector vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);IntVector vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);IntVector vectorC = vectorA.mul(vectorB);vectorC.intoArray(c, 0);
这个功能在 Java17 中进行了第二次孵化,基于使用安全的考虑,我们在短时间内用不上这个特性了。
这个特性提供了静态类型、纯 Java 访问原生代码的 API,大大简化绑定原生库的原本复杂且容易出错的过程。从 Java1.1 开始,我们可以通过原生接口(JNI)调用原生方法,但是并不好用,现在提供了外部链接器 API,可以不再使用 JNI 粘合代码了。
和向量 API 一样,暂时用不上了,等啥时候转正了,咱们重点说说怎么玩。
外部存储器访问 API 在 Java14 开始孵化(参见 Java14 新特性),在 Java15 中孵化第二版(参见 Java15 新特性),在 Java16 中进行第三版孵化。
外部存储器访问 API 使 Java 程序能够安全有效地对各种外部存储器(例如本机存储器、持久性存储器、托管堆存储器等)进行操作。外部内存通常是说那些独立 JVM 之外的内存区域,可以不受 JVM 垃圾收集的影响,通常能够处理较大的内存。
这次带来的特性包括:
MemorySegment
和MemoryAddress
接口之间更加清晰的职责分离;MemoryAccess
,提供了常见的静态内存访问器,以便在简单的情况下尽量减少对VarHandle
的需求;这些新的 API 虽然不会直接影响多数的应用类开发人员,但是他们可以在内存的第三方库中提供支持,包括分布式缓存、非结构化文档存储、大型字节缓冲区、内存映射文件等。
本文介绍了 Java16 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/16/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java15 是在 2020 年 9 月发布的一个短期版本,新增特性如下:
接下来我们一起看看这些特性。
Edwards-Curve 数字签名算法(EdDSA),一种根据 RFC 8032 规范所描述的 Edwards-Curve 数字签名算法(EdDSA)实现加密签名。
EdDSA 是一种现代的椭圆曲线方案,与 JDK 中的现有签名方案相比,EdDSA 具有更高的安全性和性能,因此备受关注。它已经在 OpenSSL 和 BoringSSL 等加密库中得到支持,目前在区块链领域用的比较多。
我们看下官方给的例子:
byte[] msg = "Hello, World!".getBytes(StandardCharsets.UTF_8);// example: generate a key pair and signKeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");KeyPair kp = kpg.generateKeyPair();// algorithm is pure Ed25519Signature sig = Signature.getInstance("Ed25519");sig.initSign(kp.getPrivate());sig.update(msg);System.out.println(Hex.encodeHexString(sig.sign()));// example: use KeyFactory to contruct a public keyKeyFactory kf = KeyFactory.getInstance("EdDSA");NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdECPoint(true, new BigInteger("1")));PublicKey pubKey = kf.generatePublic(pubSpec);System.out.println(pubKey.getAlgorithm());System.out.println(Hex.encodeHexString(pubKey.getEncoded()));System.out.println(pubKey.getFormat());
例子中 Ed25519 是使用 SHA-512(SHA-2)和 Curve25519 的 EdDSA 签名方案。旨在提供与高质量 128 位对称密码相当的抗攻击能力,公钥长度为 256 位,签名长度为 512 位。
Java15 引入了一个新的特性:隐藏类(Hidden Classes),一个专为框架而设计的特性。大多数开发人员不会直接使用这个特性,一般是通过动态字节码或 JVM 语言来使用隐藏类。
隐藏类有下面三个特点:
隐藏类的功能特性还是比较有意思的,会涉及类加载、卸载、不可见、反射等很多内容,后续会开文单独聊,文章会放在 从小工到专家的 Java 进阶之旅 专栏中。
老的 DatagramSocket API 在 Java15 中被重写,是继 Java14 重写 Socket API 的后续不走。这个特性是 Loom 项目的先决条件。
目前,DatagramSocket
和MulticastSocket
将所有的套接字委托为java.net.DatagramSocketImpl
的实现,根据不同的平台,Unix 平台使用PlainDatagramSocketImpl
,Windows 平台使用TwoStackPlainDatagramSocketImpl
和DualPlainDatagramSocketImpl
。抽象类DatagramSocketImpl
是 Java1.1 提供的,功能很少且有一些过时方法,阻碍了 NOI 的实现。
类似于 Java14 中对 Socket API 的重写(参见 Java14 新特性),会在DatagramSocket
内部封装一个DatagramSocket
实例,将所有调用直接委托给该实例。包装实例或者使用 NIO 的DatagramChannel::socket
创建套接字,或者是使用原始DatagramSocket
类的实现DatagramSocketImpl
实现功能(用于实现向后兼容)。
我们可以看下新的依赖图:
在 Java15 中,默认禁用偏向锁,弃用了所有相关命令行选项。
偏向锁是 HotSpot 中一种用于减少非竞争锁定开销的优化技术,不过在如今的应用程序中,优化增益不太明显了。
根据官方说法,使用偏向锁增益最多的是大量使用早期同步组件(比如Hashtable
、Vector
等),随着新的 API 实现和针对多线程场景引入的支持并发的数据结构,偏向锁的锁定及撤销,会带来性能的开销,从而是优化收益降低。
而且随着越来越多的功能特性引入,偏向锁在同步子系统中引入的大量代码,侵入 HotSpot 其他组件,带来代码的复杂性和维护成本,成为代码优化的阻碍。所以官方要将其移除。
不过,有些应用在禁用偏向锁后会出现性能下降,可以使用-XX:+UseBiasedLocking
手动开启。
ZGC 是在 Java11 引入的(参见 Java11 新特性),一直处于试验阶段,想要体验,需要在参数中使用-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
组合启用,在 Java15 中,ZGC 成为正式特性,想要使用可以直接用命令-XX:+UseZGC
就行。
ZGC 是一个重新设计的并发的垃圾回收器,可以极大的提升 GC 的性能,支持任意堆大小而保持稳定的低延迟。从 https://openjdk.java.net/jeps/333 给出的数据可以看出来,在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:
虽然 ZGC 愿景很好,但是还有很长的路要走,所以默认的垃圾收集器还是 G1。
Shenandoah 是在 Java12 引入的(参见)Java12 的新特性,本次和 ZGC 一起转正。同样的,想要使用 Shenandoah,不再需要参数-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
组合,只使用-XX:+UseShenandoahGC
即可。需要注意的是,Shenandoah 只在 OpenJDK 中提供,OracleJDK 中并不包含。
文本块是千呼万唤终于转正,在 Java13 中首次引入(参见 Java13 的新特性),在 Java14 中又增加了预览特性(参见 Java14 的新特性),终于在 Java15 确定下来,可以放心使用了。
我们再复习一下:
@Testvoid testTextBlock() { final String singleLine = "你好,我是看山,公众号「看山的小屋」。这行没有换行,而且我的后面多了一个空格 \n 这次换行了"; final String textBlockSingleLine = """ 你好,我是看山,公众号「看山的小屋」。\ 这行没有换行,而且我的后面多了一个空格、s 这次换行了"""; Assertions.assertEquals(singleLine, textBlockSingleLine);}
这个功能特性是代码可读性的优化。
目前,Java 没有提供对继承的细粒度控制,只有 public、protected、private、包内控制四种非常粗粒度的控制方式。
为此,密封类的目标是允许单个类声明哪些类型可以用作其子类型。这也适用于接口,并确定哪些类型可以实现它们。该功能特性新增了sealed
和non-sealed
修饰符和permits
关键字。
我们可以做如下定义:
public sealed class Person permits Student, Worker, Teacher {}public sealed class Student extends Person permits Pupil, JuniorSchoolStudent, HighSchoolStudent, CollegeStudent, GraduateStudent {}public final class Pupil extends Student {}public non-sealed class Worker extends Person {}public class OtherClass extends Worker {}public final class Teacher extends Person {}
我们可以先定义一个sealed
修饰的类Person
,使用permits
指定被继承的子类,这些子类必须是使用final
或sealed
或non-sealed
修饰的类。其中Student
是使用sealed
修饰,所以也需要使用permits
指定被继承的子类。Worker
类使用non-sealed
修饰,成为普通类,其他类都可以继承它。Teacher
使用final
修饰,不可再被继承。
从类图上看没有太多区别:
但是从功能特性上,起到了很好的约束作用,我们可以放心大胆的定义可以公开使用,但又不想被非特定类继承的类了。
instanceof 模式匹配首先在 Java14 中提供预览功能(参见 Java14 特性),可以提供instanceof
更加简洁高效的实现,在 Java15 中没有新增特性,主要是为了再次收集反馈,根据结果看,大家还是很期待这个功能,在 Java16 中正式提供。
我们再简单看下instanceof
的改进:
@Testvoid test1() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String str) { result = str.length(); } else if (obj1 instanceof Number num) { result = num.intValue(); } Assertions.assertEquals(13, result);}
Record 类型用来增强 Java 语言特性,充当不可变数据载体。在 Java14 中提供预览功能(参见 Java14 新特性),在 Java15 中提供第二次预览,这次预览的目标是收集用户反馈。
比如,我们定义一个Person
类:
public record Person(String name, String address) {}
我们转换为之前的定义会是一坨下面这种代码:
public final class PersonBefore14 { private final String name; private final String address; public PersonBefore14(String name, String address) { this.name = name; this.address = address; } public String name() { return name; } public String address() { return address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonBefore14 that = (PersonBefore14) o; return Objects.equals(name, that.name) && Objects.equals(address, that.address); } @Override public int hashCode() { return Objects.hash(name, address); } @Override public String toString() { return "PersonBefore14{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; }}
Record 类型特性有四个特性:
equals
、getter
、setter
等方法;我们不能将 Record 类型简单的理解为去除“样板化”代码的功能,它不是解决 JavaBean 命名约定的中很多模板化方法的冗余繁杂问题,它的目标不是类似 Lombok 等工具自动生成代码的功能,是从开发人员专注模型的角度出发的。
外部存储器访问 API 在 Java14 开始孵化(参见 Java14 新特性),在 Java15 中继续孵化状态,这个版本中增加了几个特性:
VarHandle
API,用于定制内存访问句柄;Spliterator
接口实现并行处理内存段;外部内存通常是说那些独立 JVM 之外的内存区域,可以不受 JVM 垃圾收集的影响,通常能够处理较大的内存。
这些新的 API 虽然不会直接影响多数的应用类开发人员,但是他们可以在内存的第三方库中提供支持,包括分布式缓存、非结构化文档存储、大型字节缓冲区、内存映射文件等。
Nashorn JavaScript 引擎最初在 Java8 中引入(参见 Java8 新特性),在 Java11 被标记为过期,在 Java15 中被删除,包括 Nashorn JavaScript 引擎、API、jjs 工具等内容。
Nashorn JavaScript 引擎是一个 JavaScript 脚本引擎,用来取代 Rhino 脚本引擎,对 ECMAScript-262 5.1 有完整的支持,增强了 Java 和 JavaScript 的兼容性,而且有很强的性能。
随着 GraalVM 和其他虚拟机技术最近的引入,Nashorn 引擎不再在 JDK 生态系统中占有一席之地。而且,ECMAScript 脚本语言结构、API 改变速度太快,Nashorn JavaScript 引擎维护成本太高,所以,直接删了。
Solaris 和 SPARC 都已被 Linux 操作系统和英特尔处理器取代。放弃对 Solaris 和 SPARC 端口的支持将使 OpenJDK 社区的贡献者能够加速开发新功能,从而推动平台向前发展。
Solaris 和 SPARC 端口 API 在 Java14 中标记过时,在 Java15 中彻底移除。仅仅半年就痛下杀手,可见社区对于维护这些 API 深受折磨。
RMI Activation 在 Java15 中标记为废除,会在未来版本删除。之所以被删除,是因为在现代的 web 应用中,已经不需要这种激活机制,继续维护,增加了 Java 开发人员的维护负担。在 Java8 的时候,已经将其设置为非必选项。
从开发系统的角度看,虽然 RMI Activation 是一个还不错的设计,但是已经有其他替代方案,继续维护开发下去,成本收益完全不匹配,及早舍弃,可以选择更加优秀的方案。有些类似于零边际成本的思想。
本文介绍了 Java15 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/15/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java15 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java15 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
本文讲解一下 Java14 的特性,这个版本带来了不少新特性、功能实用性的增强、GC 的尝试、性能优化等:
接下来我们一起看看这些特性。
Switch 表达式在 Java12 和 Java13 都处于功能预览阶段,到 Java14 终于转正了,从另一个角度,我们可以在生产环境中使用这个功能了。
我们以“判断是否工作日”的例子展示一下,在 Java14 之前:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java14 中:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> { System.out.println("Working Day: " + day); yield "Working Day"; } case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
注意看,例子中我们使用了两种写法,一种是通过yield
关键字表示返回结果,一种是在->
之后直接返回。
和前面的版本一样,Java14 中也提供了一些预览功能,我们可以在预览环境中试用一下。
这次的预览功能包括文本块(第二版预览)、instanceof 模式匹配、Record 声明,接下来我们分别说一下。
文本块在 Java13 中首次出现(参见 Java13),本次又提供了两个扩展:
\
:表示当前语句未换行,与 shell 脚本中的习惯一致;\s
:表示单个空格。我们看个例子:
@Testvoid testTextBlock() { final String singleLine = "你好,我是看山,公众号「看山的小屋」。不没有换行,而且我的后面多了一个空格 "; final String textBlockSingleLine = """ 你好,我是看山,公众号「看山的小屋」。\ 不没有换行,而且我的后面多了一个空格、s"""; Assertions.assertEquals(singleLine, textBlockSingleLine);}
个人感觉、是比较实用的,这个功能在 Java15 中转正,值得期待。
instanceof
主要用来检查对象类型,作为类型强转前的安全检查。
比如:
@Testvoid test() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String) { String str = (String) obj1; result = str.length(); } else if (obj1 instanceof Number) { Number num = (Number) obj1; result = num.intValue(); } Assertions.assertEquals(13, result);}
可以看到,我们每次判断类型之后,需要声明一个判断类型的变量,然后将判断参数强制转换类型,赋值给新声明的变量。这种写法显得繁琐且多余。
于是在 Java14 中对instanceof
进行了改进:
@Testvoid test1() { final Object obj1 = "Hello, World!"; int result = 0; if (obj1 instanceof String str) { result = str.length(); } else if (obj1 instanceof Number num) { result = num.intValue(); } Assertions.assertEquals(13, result);}
不仅如此,instanceof
模式匹配的作用域还可以扩展。在if
条件判断中,我们都知道&&
与判断是会执行所有的表达式,所以使用instanceof
模式匹配定义的局部变量继续判断。
比如:
if (obj1 instanceof String str && str.length() > 20) { result = str.length();}
与原来的写法对比,Java14 提供的写法代码更加简洁、可读性更高,能够提出很多冗余繁琐的代码,非常实用的一个特性,这个功能会在 Java16 中转正。
在 Java14 预览功能中新增了一个关键字record
,它是定义不可变数据类型封装类的关键字,主要用在特定领域类上。这个关键字最终会在 Java16 中正式提供。
我们都知道,在 Java 开发中,我们需要定义 POJO 作为数据存储对象,根据规范,POJO 中除了属性是个性化的,其他的比如getter
、setter
、equals
、hashCode
、toString
都是模板化的写法,所以为了简便,很多类似 Lombok 的组件提供 Java 类编译时增强,通过在类上定义@Data
注解自动添加这些模板化方法。在 Java14 中,我们可以直接使用record
解决这个问题。
比如,我们定义一个Person
类:
public record Person(String name, String address) {}
我们转换为之前的定义会是一坨下面这种代码:
public final class PersonBefore14 { private final String name; private final String address; public PersonBefore14(String name, String address) { this.name = name; this.address = address; } public String name() { return name; } public String address() { return address; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } PersonBefore14 that = (PersonBefore14) o; return Objects.equals(name, that.name) && Objects.equals(address, that.address); } @Override public int hashCode() { return Objects.hash(name, address); } @Override public String toString() { return "PersonBefore14{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}'; }}
我们可以发现 Record 类有如下特征:
equals()
、hashCode()
方法toString()
方法final
修饰,所以构造函数是包含所有属性的,而且没有 setter 方法在Class
类中也新增了对应的处理方法:
getRecordComponents()
:返回一组java.lang.reflect.RecordComponent
对象组成的数组,该数组的元素与Record
类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个RecordComponent
中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。isRecord()
:返回所在类是否是 Record 类型,如果是,则返回 true。看起来,Record 类和 Enum 很像,都是一定的模板类,通过语法糖定义,在 Java 编译过程中,将其编译成特定的格式,功能很好,但如果没有习惯使用,可能会觉得限制太多。
在没有考虑完全场景的情况下,很容易碰到空指针异常(NullPointerException,简称 NPE)。一般碰到这个异常,根据异常栈信息我们很容易定位到发生异常的代码行,比如:
@Testvoid test1() { Student s = null; Assertions.assertThrows(NullPointerException.class, ()-> s.getName()) .fillInStackTrace() .printStackTrace();}
如果是在 Java14 之前,这个时候打印出来的异常信息是:
Exception in thread "main" java.lang.NullPointerException at cn.howardliu.tutorials.java14.NpeTest.test1(NpeTest.java:20)
对于上面例子,我们可以直接定位到s
是null
,但是下面这个例子呢:
@Testvoid test2() { Student s = new Student(); s.setName("看山"); Assertions.assertThrows(NullPointerException.class, ()-> s.getClazz().getNo()) .fillInStackTrace() .printStackTrace();}
我们很难判断s
或者s
中的clazz
是null
,需要查看上下文代码,或者复杂情况还需要添加日志辅助定位问题。
在 Java14 中,对 NullPointerException 异常栈信息做了增强,通过分析程序的字节码信息,能够做到准确地定位到出现 NullPointerException 的变量,并且根据实际源代码打印出详细异常信息。此时,上面例子的异常信息是:
java.lang.NullPointerException: Cannot invoke "cn.howardliu.tutorials.java14.NpeTest$Clazz.getNo()" because the return value of "cn.howardliu.tutorials.java14.NpeTest$Student.getClazz()" is null at cn.howardliu.tutorials.java14.NpeTest.test2(NpeTest.java:30)
这样一目了然。
孵化功能是 Java 开发团队让我们提前尝鲜、公测的功能,在 Java9 模块化之后,孵化功能会放在jdk.incubator.
中。
Java 对象是驻留在堆上,但是有时候因为其算法或者内存结构的原因,使用效率低下、性能低下、受垃圾收集器 GC 算法影响。所以很多时候我们会使用本机内存或者称为直接内存。
在 Java14 之前,使用直接内存我们会用到ByteBuffer
或者Unsafe
,但是这两个都存在一些问题。
ByteBuffer
管理内存最大不能够超过 2G;ByteBuffer
管理的这部分内存需要使用垃圾收集器回收内存,使用不当可能造成内存泄漏;Unsafe
是非标准的 Java API,可能会因为不合法的内存使用致使系统崩溃。“天下苦秦久矣”,于是在 Java14 中提供了新的 API:
MemorySegment
:用来申请内存区域,可以是堆内存,也可以是对外内存;MemoryAddress
:从MemorySegment
实例获取已申请内存的内存地址用于执行操作,例如从底层内存段的内存中检索数据;MemoryLayout
:用来描述内存段的内容,它允许我们定义如何将内存分解为元素,并提供每个元素的大小。这部分功能截止到 Java17 还是孵化功能,而且内容比较多,后续会单独开一篇介绍。
一般来说,Java 程序会以一个 Jar 的形式提供,web 服务可能是 war 或者 ear 包,但是有时候我们的 Java 程序可能是在自己的 PC 机(比如 Windows 或者 MacOS)上运行,期望可以通过双击打开的方式。
于是 Java14 引入了引入了 jdk.incubator.jpackage.jmod
,基于 JavaFX javapackager tool,其目的就是创建一个打包工具,可以将 jar 包构建成 exe、pkg、dmg、deb、rpm 格式的安装文件。
我们可以使用jlink
创建一个最小可运行的模块包,然后使用jpackage
将其构建成安装包:
jpackage --name myapp --input lib --main-jar main.jar
ZGC 最初是在 Java11 中引入(参见 Java11 的新特性),在后续版本中,不断升级优化,实现可伸缩、低延迟的目标,使用了内存读屏障、染色指针、内存多重映射等技术。在之前,ZGC 只支持 Linux/x64 平台,在 Java14 之后,支持了 macOS/x64 和 Windows/x64 系统中。
开启参数:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
Java14 改进了非一致性内存访问(Non-uniform Memory Access,NUMA)系统上的 G1 垃圾收集器的整体性能,主要是对年轻代的内存分配做出优化,从而提升 CPU 计算过程中内存访问速度。
NUMA 主要是指在多插槽物理计算机体系中,处理器一般是多核,且越来越多具备 NUMA 访问体系结构,即内存与每个插槽或内核之间的距离不等。套接字之间的内存访问有不同的性能特征,更远的套接字访问会有更多的时间消耗。这样的结果是,每个核对于某一区域的内存访问速度会随核与物理内存的位置远近有所差异。
Java 分配内存时,G1 会申请一块 region,作为对象存放区域。如果能够感知 NUMA,就可以优先在当前线程绑定的 NUMA 节点空闲内存执行申请内存操作,用于提升访问速度。
启用参数是-XX:+UseNUMA
。
飞行记录器(Flight Recorder)是在 Java11 中引入(参见 Java11 的新特性)。
本次增强可以实现 JFR 数据的公开访问,可以通过使用jdk.jfr.consumer
中的方法持续读取或流式传输读取记录,用于持续监控。这样的话,我们可以与现有监控系统集成,实现 JFR 数据的持续监听,不用非得等着收集完成后再解析分析。
对FileChannel
进行扩展,定义了jdk.nio.mapmode.ExtendedMapMode
,用来创建MappedByteBuffer
实例,可以对非原子性的字节缓冲区映射(Non-Volatile Mapped Byte Buffers,NVM)实现持久化。
CMS 是老年代垃圾回收算法,通过标记-清除的方式进行内存回收,在内存回收过程中能够与用户线程并行执行。在 G1 之前,CMS 几乎占据了 GC 的全部江山。在使用过程中,一般是 CMS 与 Parallel New 搭配使用。
CMS 由于其算法特性,会产生内存碎片和浮动垃圾,随着时间推移,可能出现的情况是,虽然老年代还有空间,但是没有办法分配足够内存给大对象。
所以在 Java9 中开始放弃使用 CMS,在 Java14 中彻底删除,并删除与 CMS 有关的参数。从 Java14 开始,CMS 成为了历史。
Parallel Scavenge 是并行收集算法,SerialOld 提供老年代串行收集,这种年轻代使用并行算法、老年代使用串行算法的混搭的方式,使用场景少且有风险。但是却需要大量工作量维护,所以在 Java14 中,删除了这两种 GC 组合。
删除组合的方式是通过启用组合参数-XX:+UseParallelGC -XX:-UseParallelOldGC
,并在单独使用-XX:-UseParallelOldGC
时会收到警告信息。
these were deprecated for removal in Java 11, and now removed
删除java.util.jar
包中的pack200
和unpack200
工具以及 Pack200 API。这些工具和 API 已在 Java11 时标记弃用,删除也是意料之中。
Solaris 和 SPARC 都已被 Linux 操作系统和英特尔处理器取代。放弃对 Solaris 和 SPARC 端口的支持将使 OpenJDK 社区的贡献者能够加速开发新功能,从而推动平台向前发展。
这些 API 在 Java14 中标记弃用,在 Java15 中彻底删除。这样做,也是为了让很多正在进行的项目尽早适应新的架构。
本文介绍了 Java14 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/14/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java14 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java14 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
本文讲解一下 Java13 的特性,这个版本在语法特性上增加不多,值得关注的是两个预览功能:Switch 表达式和文本块,另外可以关乎的是性能优化方面的:动态类数据共享(CDS)存档、ZGC 动态释放未使用内存、Socket API 重构。这些方面可以看出,Java 的升级方向有两个,一是增加功能,增加新的语法特性;二是增强功能,提升已有功能性能。
Java13 引入了两个新的语法特性:Switch 表达式和文本块。这些预览功能是为了让开发者尝鲜的同时,可以快速调整,反馈好就留下,不好就移除。目前来看,这些特性还是挺香的。
在 Java12 中 Switch 表达式首次以预览版的身份出现,在 Java13 中又做了增强,在 Java14 正式提供。Java13 添加了yield
关键字,用来返回值。
yield
与return
的区别在于,yield
只会跳出switch
块,return
是跳出当前方法或循环。
比如下面的例子,在 Java12 之前,要判断日期可以这样写:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java12 提供的 Switch 表达式预览功能,我们可以简化一下:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Working Day"; case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
这样可以实现判断,但是没有办法在表达式中实现其他逻辑了。于是 Java13 补齐了这个功能:
@Testvoid testSwitchExpression13() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> { System.out.println("Working Day: " + day); yield "Working Day"; } case SATURDAY, SUNDAY -> { System.out.println("Day Off: " + day); yield "Day Off"; } }; Assertions.assertEquals("Day Off", typeOfDay);}
这里需要说明一下,既然是预览功能,会与正式提供功能有些出入。上面的代码是在 Java14 环境中编写,与 Java13 发布的功能描述有些差异,这点不必深究,已经废弃的约束就是不存在。
一直以来,Java 中的字符串定义都是以双引号括起来的形式,不支持多行书写,所以在需要多行字符串中,需要使用转义符表示,既不好看、还不好读,更不好维护。
千呼万唤始出来,终于有了文本块功能。
比如,我们想要写一段 Json 格式的数据,Java13 之前需要写成:
String json = "{\n" + " \"wechat\": \"hellokanshan\",\n" + " \"wechatName\": \"看山、",\n" + " \"mp\": \"kanshanshuo\",\n" + " \"mpName\": \"看山的小屋、"\n" + "}\n";
但是在 Java13 预览版中可以写作:
String json2 = """ { "wechat": "hellokanshan", "wechatName": "看山", "mp": "kanshanshuo", "mpName": "看山的小屋" } """;
少了很多的+、换行、转移等字符,看着更加直观。
这个功能在 Java15 中正式提供。
CDS 是 Java5 引入的一种类预处理方式,可以将一组类共享到一个归档文件中,借助内存映射加载类数据,减少启动时间,并可实现在多 JVM 之间共享的功能。在 Java10 对其进行扩展,增大了 CDS 使用范围,即 AppCDS(参见 Java10 新特性)。到了 Java12,将 CDS 归档文件作为了默认功能开放出来(参见 Java12 新特性)。
但是这个功能在使用的时候还是有些麻烦。为了生成归档文件,开发人员必须先对应用程序进行试运行,创建一个类列表,然后将其转储到归档文件中。然后,这个归档才可以用来在 JVM 之间共享元数据。
Java13 简化了这个过程:允许 Java 应用在运行结束后动态归档,即将已被加载但不属于 CDS 的类(包括自定义类和引用库的类)动态添加到 CDS 归档文件中。不用再提供归档类的列表,通过更加简洁的方式创建包含应用程序的归档。
我们可以使用-XX:ArchiveClassesAtExit
参数控制应用程序退出时创建 CDS 归档文件:
java -XX:ArchiveClassesAtExit=<archive filename> -cp <app jar> AppName
也可以使用-XX:SharedArchiveFile
来使用动态存档功能:
java -XX:SharedArchiveFile=<archive filename> -cp <app jar> AppName
ZGC 是 Java11 中引入的一个可伸缩、低延迟的垃圾收集器,主要目标包括:GC 停顿时间不超过 10ms;可以处理从几百 MB 的小堆,到几个 TB 的大堆;应用吞吐能力不会下降超过 15%等(参见 Java11 的新特性)。
但是 ZGC 并没有像 Hotspot 中的 G1 和 Shenandoah 那样,可以主动释放未使用的内存,对于多数应用程序来说,CPU 和内存都是稀缺资源,尤其是现在云上环境和虚拟化技术,如果应用程序占用的内存长期处于空闲状态,还紧握住不释放,就是极大的浪费。
在 Java13 中对其进行改进,包括:
我们来看下 ZGC 的内部逻辑。
ZGC 堆由一组称为 ZPages 的堆区域组成,每个 ZPage 都与提交的堆内存的可变数量相关联。当 ZGC 压缩堆时,ZPages 被释放并插入到页面缓存 ZPageCache 中,页面缓存中的 ZPages 可以重新使用,以满足新的堆分配。
ZPageCache 中的 ZPages 集合代表堆中未使用的部分,这部分可以释放回操作系统。ZPageCache 中的 ZPages 根据 LRU(最近最少使用)排序,并按照大中小进行分组。这样的话就可以根据算法按顺序释放未使用的内存。
Java13 还提供了-XX:ZUncommitDelay=<seconds>
命令,用于指定释放多长时间(默认是 5 分钟)未使用的内存,这个参数类似于 Shenandoah 中的-XX:ShenandoahUncommitDelay=<milliseconds>
。
在 Java13 中,ZGC 内存释放功能默认开启,可通过参数-XX:-ZUncommit
关闭该功能。由于 ZGC 释放内存时,不会低于最小堆内存,即当最小堆内存(-Xms)与最大堆内存(-Xmx)一样时,不会自动释放。
Java 中的 Socket 是从 Java1.0 开始就有的,是 Java 中不可或缺的网络 API,算起来已经服役 20 多年了。在这段时间内,信息技术已经发生了很多变化,这些上古 API 有一定的局限性,而且不容易维护和调试。
Java 的 Socket API 主要包括java.net.ServerSocket
和java.net.Socket
,ServerSocket
用来监听连接请求的端口,连接成功后返回的是Socket
对象,可以通过操作Socket
对象实现数据发送和读取。Java 是通过SocketImpl
实现这些功能。
在 Java13 之前,通过SocketImpl
的子类PlainSocketImpl
实现。在 Java13 中,引入NioSocketImpl
实现,该实现以 NIO 为基础,与高速缓冲机制集成,实现非阻塞式网络。
如果想用回PlainSocketImpl
,可以设置启动参数-Djdk.net.usePlainSocketImpl=true
即可。
本文介绍了 Java13 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/13/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java13 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java13 的新特性
你好,我是看山。
腊月二十七宰鸡赶大集,响应国家号召,就地过年。
难免思乡,于是翻了翻百度地图提供的迁徙数据。我们从迁出地和迁徙趋势看一看这几年的春运变化。
我们先来看看从 2020 年到现在(2022 年)的春运迁徙图:
从三张图中,我们不难看出一些规律,待我慢慢道来。
这几年中,比较热门的迁出地有北京、上海、广州、深圳,稍微热门的是杭州、武汉、重庆、长沙、西安等。不难看出,热门迁出地对应着一线、新一线城市。这些城市的外来务工人员比较多,在春节选择回家过年。
其中,北上广深是一线城市,很多人或为生活、或为梦想,选择了一线城市打拼。辛苦一年,趁着年关回到家,无论家乡是好是孬,总归是一个平静的港湾。至于年后,是选择以梦为马潇洒天涯,还是选择背起行囊漂泊他乡,都是年后的事情。
当人们在寻找工作和生活的平衡点时,很多城市也是快速发展,成为了新一线城市,成为了中国发展的新兴动力城市,也给了我们更多的选择。人们不在单纯的考虑收入,会更多的考虑感受、家人,更多的考虑幸福。
无论我们在哪,做什么,都是为了追求幸福,幸福才是我们心底最期望的东西。
疫情发生前(2019 年),我们可以看到,年前迁移流量持续增长,大年初一稍微少了一点,到初六假期结束,迁移量达到峰值。我们以此为参照,看下疫情的影响。
2020 年疫情爆发,武汉封城,春节期间全国人民上下齐心,共抗疫情。这个时候,春运流动基本上停止。作为普通民众,我们能够做到,就是待在家里,不给国家添麻烦。
2021 年是疫情第二年,咱们国家提出的“清零”政策取得了好成绩,生活工作基本上恢复正常,也有了十一小长假出行旅游复苏的场景。外国友人们躲在家里看我们堵在路上,心里一定是各种羡慕。而且,在年底开始全员接种疫苗,给我们增加一层保障。
不过由于冬天天气转冷,病毒的存活能力增强,为了保住来之不易的战果。各地倡导就地过年。所以能够看到,春运的流量比 2019 年减少了将近一半。
今年的春运刚刚开始,我们只能够根据趋势推测一下,到今天为止,迁徙流量的发展基本上和 2019 年相似。这不得不说在抗疫方面,2021 年取得了好成绩。全民接种加强针疫苗,各地的清零政策也是严格执行。虽然年底有些地方出现了本土疫情,但是都是在控制范围内,没有爆发的征兆。有了 2021 年春运、五一、十一等各种假期迁徙的经验,各地采取“有温度的严格控制”,让远在他乡想在春节回家的游子们,一解乡愁。
可以预见,等到了 2023 年春运的时候,我们可能就不再纠结能不能回、让不让回,只需要带着必要的证明,正常计划归乡日期就好。
按照习俗,给大家拜个早年,愿大家新年胜旧年,欢愉且胜意,万事尽可期。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:从春运迁徙图看到的一些东西
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:从春运迁徙图看到的一些东西
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
你好,我是看山。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
本文讲解一下 Java12 的特性,作为第一个长期支持版 Java11 之后的第一个版本,增加的功能也不少,除了一些小幅度的 API 增强,增加了另一个试验阶段的垃圾收集器 Shenandoah、对 G1 做了优化、增加微基准套件等。
Java12 提供了很多的语法特性,既有小而美的增强 API,又有特别方便的工具扩展。本节我们跟着代码看看比较好玩的功能。
在 Java12 中,String 又增强了两个方法。之所以说又,是因为在 Java11 中已经增加过小而美的方法,想要详细了解的可以查看 Java11 新特性。
这次增加的方法是indent
(缩进)和transform
(转换)。
顾名思义,indent
方法是对字符串每行(使用\r
或\n
分隔)数据缩进指定空白字符,参数是 int 类型。
如果参数大于 0,就缩进指定数量的空格;如果参数小于 0,就将左侧的空字符删除指定数量,即右移。
我们看下源码:
public String indent(int n) { if (isEmpty()) { return ""; } Stream<String> stream = lines(); if (n > 0) { final String spaces = " ".repeat(n); stream = stream.map(s -> spaces + s); } else if (n == Integer.MIN_VALUE) { stream = stream.map(s -> s.stripLeading()); } else if (n < 0) { stream = stream.map(s -> s.substring(Math.min(-n, s.indexOfNonWhitespace()))); } return stream.collect(Collectors.joining("\n", "", "\n"));}
这里会使用到 Java11 增加的lines
、repeat
、stripLeading
等方法。indent
最后会将多行数据通过Collectors.joining("\n", "", "\n")
方法拼接,结果会有两点需要注意:
\r
会被替换成\n
;\n
,最后会补上一个\n
,即多了一个空行。我们看下测试代码:
@Testvoid testIndent() { final String text = "\t\t\t 你好,我是看山。\n \u0020\u2005Java12 的 新特性。\r 欢迎三连+关注哟"; assertEquals(" \t\t\t 你好,我是看山。\n \u0020\u2005Java12 的 新特性。\n 欢迎三连+关注哟、n", text.indent(4)); assertEquals("\t 你好,我是看山。\n\u2005Java12 的 新特性。\n 欢迎三连+关注哟、n", text.indent(-2)); final String text2 = "山水有相逢"; assertEquals("山水有相逢", text2);}
我们再来看看transform
方法,源码一目了然:
public <R> R transform(Function<? super String, ? extends R> f) { return f.apply(this);}
通过传入的Function
对当前字符串进行转换,转换结果由Function
决定。比如,我们要对字符串反转:
@Testvoid testTransform() { final String text = "看山是山"; final String reverseText = text.transform(s -> new StringBuilder(s).reverse().toString()); assertEquals("山是山看", reverseText);}
其实这个方法在 Java8 中提供的Optional
实现类似的功能(完整的 Optional 功能可以查看 Optional 的 6 种操作):
@Testvoid testTransform() { final String text = "看山是山"; final String reverseText2 = Optional.of(text) .map(s -> new StringBuilder(s).reverse().toString()) .orElse(""); assertEquals("山是山看", reverseText2);}
在 Java12 中,Files
增加了mismatch
方法,用于对比两个文件中的不相同字符的位置,如果内容相同,返回-1L
,是long
类型的。
我们来简单看下怎么用:
@Testvoid testMismatch() throws IOException { final Path pathA = Files.createFile(Paths.get("a.txt")); final Path pathB = Files.createFile(Paths.get("b.txt")); // 写入相同内容 Files.write(pathA, "看山".getBytes(), StandardOpenOption.WRITE); Files.write(pathB, "看山".getBytes(), StandardOpenOption.WRITE); final long mismatch1 = Files.mismatch(pathA, pathB); Assertions.assertEquals(-1L, mismatch1); // 追加不同内容 Files.write(pathA, "是山".getBytes(), StandardOpenOption.APPEND); Files.write(pathB, "不是山".getBytes(), StandardOpenOption.APPEND); final long mismatch2 = Files.mismatch(pathA, pathB); Assertions.assertEquals(6L, mismatch2); Files.deleteIfExists(pathA); Files.deleteIfExists(pathB);}
我们可以看到,当第一次在两个文件中写入相同内容,执行mismatch
方法返回的是-1L
。当第二次追加进去不同的内容后,返回的是6L
。之所以是 6,是因为测试代码中使用的字符集是UTF-8
,大部分汉子是占用 3 个字符,前两个字相同,从第三个字开始不同,下标从 0 开始,所以开始位置是 6。
我们看下teeing
的定义:
public static <T, R1, R2, R> Collector<T, ?, R> teeing( Collector<? super T, ?, R1> downstream1, Collector<? super T, ?, R2> downstream2, BiFunction<? super R1, ? super R2, R> merger)
这个方法有三个参数,前两个是Collector
对象,用于对输入数据进行预处理,第三个参数是BiFunction
,用于将前两个处理后的结果作为参数传入BiFunction
中,运算得到结果。
我们来看下例子:
@Testvoid testTeeing() { var result = Stream.of("Sunday", "Monday", "Tuesday", "Wednesday") .collect(Collectors.teeing( Collectors.filtering(n -> n.contains("u"), Collectors.toList()), Collectors.filtering(n -> n.contains("n"), Collectors.toList()), (list1, list2) -> List.of(list1, list2) )); assertEquals(2, result.size()); assertTrue(isEqualCollection(List.of("Sunday", "Tuesday"), result.get(0))); assertTrue(isEqualCollection(List.of("Sunday", "Monday", "Wednesday"), result.get(1)));}
我们对输入的几个字符串进行过滤,然后将过滤结果组成一个新的队列。
这个工具比较好玩,可以对数字进行按需格式化。提供了public static NumberFormat getCompactNumberInstance(Locale locale, NumberFormat.Style formatStyle)
方法用于初始化:
SHORT
和LONG
,不过对于中文展示,似乎没啥区别。我们一起看下例子:
@Testvoid testFormat() { final NumberFormat zhShort = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.SHORT); assertEquals("1 万", zhShort.format(10_000)); assertEquals("1 兆", zhShort.format(1L << 40)); final NumberFormat zhLong = NumberFormat.getCompactNumberInstance(Locale.CHINA, Style.LONG); assertEquals("1 万", zhLong.format(10_000)); assertEquals("1 兆", zhLong.format(1L << 40)); final NumberFormat usShort = NumberFormat.getCompactNumberInstance(Locale.US, Style.SHORT); usShort.setMaximumFractionDigits(2); assertEquals("10K", usShort.format(10_000)); assertEquals("1.1T", usShort.format(1L << 40)); final NumberFormat usLong = NumberFormat.getCompactNumberInstance(Locale.US, Style.LONG); usLong.setMaximumFractionDigits(2); assertEquals("10 thousand", usLong.format(10_000)); assertEquals("1.1 trillion", usLong.format(1L << 40));}
我们也可以继续使用NumberFormat
中的方法定义,比如示例中保留小数点后 2 位。
Java12 引入了一个实验阶段的垃圾收集器:Shenandoah,作为一个低停顿的垃圾收集器。
Shenandoah 垃圾收集器是 RedHat 在 2014 年宣布进行的垃圾收集器研究项目,其工作原理是通过与 Java 应用执行线程同时运行来降低停顿时间。简单的说就是,Shenandoah 工作时与应用程序线程并发,通过交换 CPU 并发周期和空间以改善停顿时间,使得垃圾回收器执行线程能够在 Java 线程运行时进行堆压缩,并且标记和整理能够同时进行,因此避免了在大多数 JVM 垃圾收集器中所遇到的问题。
Shenandoah 垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为 200MB 还是 200GB,都将拥有一致的系统暂停时间,不过实际使用性能将取决于实际工作堆的大小和工作负载。
Java12 中 Shenandoah 处于实验阶段,想要使用需要编译时添加--with-jvm-features=shenandoahgc
,然后启动时使用-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
以开启。
后续会补充 Java 中各种垃圾收集器的文章,其中会有介绍 Shenandoah 的,敬请关注公众号「看山的小屋」。如果想要提前了解,欢迎访问https://wiki.openjdk.java.net/display/shenandoah。
Java12 中添加一套基准测试套件,该基准测试套件基于 JMH(Java Microbenchmark Harness),使开发人员可以轻松运行现有的基准测试并创建新的基准测试,其目标是提供一个稳定且优化的基准。
在这套基准测试套件中包括将近 100 个基准测试的初始集合,并且能够轻松添加新基准、更新基准测试和提高查找已有基准测试的便利性。
微基准套件与 JDK 源代码位于同一个目录中,并且在构建后将生成单个 Jar 文件。它是一个单独的项目,在支持构建期间不会执行,以方便开发人员和其他对构建微基准套件不感兴趣的人在构建时花费比较少的构建时间。
Switch 语句出现的姿势是条件判断、流程控制组件,与现在很流行的新语言对比,其写法显得非常笨拙,所以 Java 推出了 Switch 表达式语法,可以让我们写出更加简化的代码。这个扩展在 Java12 中作为预览版首次引入,需要在编译时增加-enable-preview
开启,在 Java14 中正式提供,功能编号是 JEP 361。
比如,我们通过 switch 语法简单计算工作日、休息日,在 Java12 之前需要这样写:
@Testvoid testSwitch() { final DayOfWeek day = DayOfWeek.from(LocalDate.now()); String typeOfDay = ""; switch (day) { case MONDAY: case TUESDAY: case WEDNESDAY: case THURSDAY: case FRIDAY: typeOfDay = "Working Day"; break; case SATURDAY: case SUNDAY: typeOfDay = "Rest Day"; break; } Assertions.assertFalse(typeOfDay.isEmpty());}
在 Java12 中的 Switch 表达式中,我们可以直接简化:
@Testvoid testSwitchExpression() { final DayOfWeek day = DayOfWeek.SATURDAY; final String typeOfDay = switch (day) { case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Working Day"; case SATURDAY, SUNDAY -> "Day Off"; }; Assertions.assertEquals("Day Off", typeOfDay);}
是不是很清爽。文末提供的源码中,pom.xml
定义的maven.compiler
版本写的是14
,这是因为 Switch 表达式是 Java14 正式提供,我没有重新编译 Java,所以只能指定 Java14 来实现这个功能代码的演示。
Java12 中引入 JVM 常量 API,用来更容易地对关键类文件和运行时构件的描述信息进行建模,特别是对那些从常量池加载的常量,这是一项非常技术性的变化,能够以更简单、标准的方式处理可加载常量。
具体来说就是java.base
模块新增了java.lang.constant
包,引入了ConstantDesc
接口以及Constable
接口。ConstantDesc
的子接口包括:
ClassDesc
:Class 的可加载常量标称描述符;MethodTypeDesc
:方法类型常量标称描述符;MethodHandleDesc
:方法句柄常量标称描述符;DynamicConstantDesc
:动态常量标称描述符。继续挖坑,这部分内容会在进阶篇再详细介绍,敬请关注公众号「看山的小屋」。
Java12 中将只保留一套 AArch64 实现,之前版本中,有两个关于 aarch64 的实现,分别是ope/src/hotspot/cpu/arm
以及open/src/hotspot/cpu/aarch64
,它们的实现重复了。为了集中精力更好地实现 aarch64,删除了open/src/hotspot/cpu/arm
中与 arm64(64-bit Arm platform)实现相关的代码,只保留 32 位 ARM 端口和 64 位 aarch64 的端口。
这样做,可以让开发人员将目标集中在剩下的这个 64 位 ARM 实现上,消除维护两套端口所需的重复工作。
目标聚焦,力量集中。
在 Java10 的新特性 中我们介绍过类数据共享(CDS,Class Data Sharing),其作用是通过构建时生成默认类列表,在运行时使用内存映射,减少 Java 的启动时间和减少动态内存占用量,也能在多个 Java 虚拟机之间共享相同的归档文件,减少运行时的资源占用。
在 Java12 之前,想要使用需要三步走手动开启,到了 Java12,将默认开启 CDS 功能,想要关闭,需要使用参数-Xshare:off
。
G1 垃圾收集器可以在大内存多处理器的工作场景中提升回收效率,能够满足用户预期降低 STW 停顿时间。
其内部是采用一个高级分析引擎来选择在收集期间要处理的工作量,此选择过程的结果是一组称为 GC 回收集(collection set,CSet)的区域。一旦收集器确定了 GC 回收集 并且 GC 回收、整理工作已经开始,则 G1 收集器必须完成收集集合集的所有区域中的所有活动对象之后才能停止;但是如果收集器选择过大的 GC 回收集,可能会导致 G1 回收器停顿时间超过预期时间。
在 Java12 中,GC 回收集拆分为必需和可选两部分,使 G1 垃圾回收器能中止垃圾回收过程。其中必需处理的部分包括 G1 垃圾收集器不能递增处理的 GC 回收集的部分,同时也可以包含老年代以提高处理效率。在 G1 垃圾回收器完成收集需要必需回收的部分之后,G1 垃圾回收器可以根据剩余时间决定是否停止收集。
在 Java11 中,G1 仅在进行 Full GC 或并发处理周期时才能向操作系统返还堆内存,但是这两种场景都是 G1 极力避免的,所以如果我们使用 G1 收集器,基本上很难返还 Java 堆内存,这样对于那种周期性执行大量占用内存的应用,会造成比较多的内存浪费。
Java12 中,G1 垃圾收集器将在应用程序不活动期间定期生成或持续循环检查整体 Java 堆使用情况,以便 G1 垃圾收集器能够更及时的将 Java 堆中不使用内存部分返还给操作系统。对于长时间处于空闲状态的应用程序,此项改进将使 JVM 的内存利用率更加高效。
本文介绍了 Java12 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/12/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java12 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java12 的新特性
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
Java11 是 2018 年 9 月发布的,是自 Java8 之后第一个长期支持版(long-term support,LTS)。相比于其他版本 6 个月维护期,长期支持版的维护期是 3 年。
长期支持版的更新会比较多,而且都是相对稳定的更新。今天我们就一起看看 Java11 都有哪些喜人的变化:增强的 API、全新的 HTTP 客户端、基于嵌套关系的访问控制优化、低开销的堆性能采用工具、ZGC、Epsilon 垃圾收集器、飞行记录器等。
首先说下String
中新增的方法:repeat
、strip
、stripLeading
、stripTrailing
、isBlank
、lines
。这些方法还是挺有用的,以前我们可能需要借助第三方类库(比如 Apache 出品的 commons-lang)中的工具类,现在可以直接使用嫡亲方法了。
repeat
是实例方法,顾名思义,这个方法是返回给定字符串的重复值的,参数是int
类型,传参的时候需要注意:
IllegalArgumentException
异常;Integer.MAX_VALUE
,会抛出OutOfMemoryError
错误。用法很简单:
@Testvoid testRepeat() { String output = "foo ".repeat(2) + "bar"; assertEquals("foo foo bar", output);}
小而美的一个工具方法。
strip
方法算是trim
方法的增强版,trim
方法可以删除字符串两侧的空白字符(空格、tab 键、换行符),但是对于Unicode
的空白字符无能为力,strip
补足这一短板。
用起来是这样的:
@Testvoid testTrip() { final String output = "\n\t hello \u2005".strip(); assertEquals("hello", output); final String trimOutput = "\n\t hello \u2005".trim(); assertEquals("hello \u2005", trimOutput);}
对比一下可以看到,trim
方法的清理功能稍弱。
stripLeading
和stripTrailing
与strip
类似,区别是一个清理头,一个清理尾。用法如下:
@Testvoid testTripLeading() { final String output = "\n\t hello \u2005".stripLeading(); assertEquals("hello \u2005", output);}@Testvoid testTripTrailing() { final String output = "\n\t hello \u2005".stripTrailing(); assertEquals("\n\t hello", output);}
这个方法是用于判断字符串是否都是空白字符,除了空格、tab 键、换行符,也包括Unicode
的空白字符。
用法很简单:
@Testvoid testIsBlank() { assertTrue("\n\t\u2005".isBlank());}
最后这个方法是将字符串转化为字符串Stream
类型,字符串分隔依据是换行符:\n
、\r
、\r\n
,用法如下:
@Testvoid testLines() { final String multiline = "This is\n \na multiline\nstring."; final String output = multiline.lines() .filter(Predicate.not(String::isBlank)) .collect(Collectors.joining(" ")); assertEquals("This is a multiline string.", output);}
本次更新在Files
中增加了两个方法:readString
和writeString
。writeString
作用是将指定字符串写入文件,readString
作用是从文件中读出内容到字符串。是一个对Files
工具类的增强,封装了对输出流、字节等内容的操作。
用法比较简单:
@Testvoid testReadWriteString() throws IOException { final Path tmpPath = Path.of("./"); final Path tempFile = Files.createTempFile(tmpPath, "demo", ".txt"); final Path filePath = Files.writeString(tempFile, "看山 howardliu.cn\n 公众号:看山的小屋"); assertEquals(tempFile, filePath); final String fileContent = Files.readString(filePath); assertEquals("看山 howardliu.cn\n 公众号:看山的小屋", fileContent); Files.deleteIfExists(filePath);}
readString
和writeString
还可以指定字符集,不指定默认使用StandardCharsets.UTF_8
字符集,可以应对大部分场景了。
java.util.Collection
提供了集合转数组的方法有两个:
Object[] toArray()
:可以直接转数组,但是转换后是Object
类型,后续使用的时候,需要强转,太不优雅了;<T> T[] toArray(T[] a)
:传入一个指定类型的数组,一般会有另种实现:Arrays.copyOf
创建列表长度的数组,这个数组与传入数组参数没有关系System.arraycopy
将列表写入数组,超过长度的数组元素置为null
。我们一般这样用:
@Testvoid testArray() { final List<String> vars = Arrays.asList("1", "2", "3"); final Object[] objArray = vars.toArray(); final String[] strArray = vars.toArray(new String[0]); Assertions.assertTrue(Arrays.asList(strArray).contains("1")); Assertions.assertTrue(Arrays.asList(strArray).contains("2")); Assertions.assertTrue(Arrays.asList(strArray).contains("3"));}
在 Java11 中,又新增了一种实现,相当于对<T> T[] toArray(T[] a)
做了增强,其源码是:
default <T> T[] toArray(IntFunction<T[]> generator) { return toArray(generator.apply(0));}
可以看到,是通过传入一个IntFunction
类型的函数,然后调用<T> T[] toArray(T[] a)
创建数组,其实是采用了我们常用的给toArray
传入空数组的方式,用法如下:
@Testvoid testArray() { final List<String> vars = Arrays.asList("1", "2", "3"); final String[] strArray2 = vars.toArray(String[]::new); Assertions.assertTrue(Arrays.asList(strArray2).contains("1")); Assertions.assertTrue(Arrays.asList(strArray2).contains("2")); Assertions.assertTrue(Arrays.asList(strArray2).contains("3"));}
从使用上,似乎没有太多的提升,但是写法上,使用了函数式编程,是不是很优雅。
这个也是方法增强,在以前,我们在Stream
中的filter
方法判断否的时候,一般需要!
运算,比如我们想要找到字符串列表中的数字,可以这样写:
final List<String> list = Arrays.asList("1", "a");final List<String> nums = list.stream() .filter(NumberUtils::isDigits) .collect(Collectors.toList());Assertions.assertEquals(1, nums.size());Assertions.assertTrue(nums.contains("1"));
想要找到非数字的,filter
方法写的就会用到!
非操作:
final List<String> notNums = list.stream() .filter(x -> !NumberUtils.isDigits(x)) .collect(Collectors.toList());Assertions.assertEquals(1, notNums.size());Assertions.assertTrue(notNums.contains("a"));
Java11 中为Predicate
增加not
方法,可以更加简单的实现非操作:
final List<String> notNums2 = list.stream() .filter(Predicate.not(NumberUtils::isDigits)) .collect(Collectors.toList());Assertions.assertEquals(1, notNums2.size());Assertions.assertTrue(notNums2.contains("a"));
有些教程还会推崇静态引入,比如在头部使用import static java.util.function.Predicate.not
,这样在函数式编程时,可以写更少的代码,语义更强,比如:
final List<String> notNums2 = list.stream() .filter(not(NumberUtils::isDigits)) .collect(toList());
喜好随人,没有优劣。
局部变量是 Java10 中增加的特性,具体可以查看 Java10 的新特性 中的介绍,但是不支持在 Lambda 中使用局部变量。
在 Lambda 中,我们可以这样操作:
(String s1, String s2) -> s1 + s2
也可以这样:
(s1, s2) -> s1 + s2
到 Java11 之后,我们还能这样:
(var s1, var s2) -> s1 + s2
单纯从语法上,似乎没啥特点,但是如果再加上一些别的用法,比如:
(@Nonnull var s1, @Nullable var s2) -> s1 + s2
是不是就能看出差别了,我们可以有如下的操作:
@Testvoid testLocalVariable() { final List<String> sampleList = Arrays.asList("Hello", "World"); final String resultString = sampleList.stream() .map((@NotNull var x) -> x.toUpperCase()) .collect(Collectors.joining(", ")); Assertions.assertEquals("HELLO, WORLD", resultString);}
不过,这里还是有一些限制,比如:
如果是多个参数,不能有的使用var
修饰,有的不指定类型:
// 错误写法(var s1, s2) -> s1 + s2
或者,不能混合使用,一个使用var
修饰,一个使用明确的类型:
// 错误写法(var s1, String s2) -> s1 + s2
如果是单个参数,如果是单行操作,我们可以不写{}
,但是使用var
修饰的时候,就不能省略{}
了:
// 错误写法var s1 -> s1.toUpperCase()
还是有一些限制的,我们在便利的同时,需要符合一定的约束。自由和规范不冲突。
在 Java9 的新特性 中说过,Java 中有一个全新的 HTTP 客户端,当时还在孵化模块中,到 Java11 可以正式使用了。
新客户端用法简单、性能可靠,而且支持功能也多。我们先简单看下使用:
@Testvoid testHttpClient() throws IOException, InterruptedException { final HttpClient httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(20)) .build(); final HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create("https://www.howardliu.cn/robots.txt")) .build(); final HttpResponse<String> httpResponse = httpClient.send(httpRequest, BodyHandlers.ofString()); final String responseBody = httpResponse.body(); assertTrue(responseBody.contains("Allow"));}
这部分是遗留的技术债务,从 Java1.1 开始,到 Java11 修复,属于 Valhalla 项目的一部分,我们在 Java11 中基于嵌套关系的访问控制优化 一文中有详细解释,这里就不再赘述了。
在 Java11 之前,想要运行源文件,需要先通过javac
命令编译,然后使用java
命令运行,先可以直接使用java
运行了:
$ java HelloWorld.javaHello Java 11!
为了使 JVM 对动态语言更具吸引力,Java 指令集引入了 invokedynamic。
不过 Java 开发人员通常不会注意到此功能,因为它隐藏在 Java 字节代码中。通过使用 invokedynamic,可以延迟方法调用的绑定。例如,Java 语言使用该技术来实现 Lambda 表达式,这些表达式仅在首次使用时才显示出来。这样做,invokedynamic 已经演变成一种必不可少的语言功能。
Java 11 引入了类似的机制,扩展了 Java 文件格式,以支持新的常量池:CONSTANT_Dynamic,它在初始化的时候,像 invokedynamic 指令生成代理方法一样,委托给 bootstrap 方法进行初始化创建,对上层软件没有很大的影响,降低开发新形式的可实现类文件约束带来的成本和干扰。
此功能可提高性能,并面向语言设计人员和编译器实现人员。
Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息。
引入这个低开销内存分析工具是为了达到如下目的:
对用户来说,了解堆中内存分布是非常重要的,特别是遇到生产环境中出现的高 CPU、高内存占用率的情况。目前有一些已经开源的工具,允许用户分析应用程序中的堆使用情况,比如:Java Flight Recorder、jmap、YourKit 以及 VisualVM tools.。但是这些工具都有一个明显的不足之处:无法得到对象的分配位置,headp dump 以及 heap histogram 中都没有包含对象分配的具体信息,但是这些信息对于调试内存问题至关重要,因为它能够告诉开发人员他们的代码中发生的高内存分配的确切位置,并根据实际源码来分析具体问题,这也是 Java 11 中引入这种低开销堆分配采样方法的原因。
ZGC 是一个可伸缩、低延迟的垃圾收集器,性能由于 G1 收集器,从 Java11 开始可以在 Linux/x64 平台体验,全平台支持是从 Java17 开始。详细介绍可以从https://wiki.openjdk.java.net/display/zgc/Main查看。
在 Java11 中尚处于试验阶段,没有包含在 JDK 构建中,想要启用,需要在 JDK 编译时添加参数--with-jvm-features=zgc
。显式启用了 ZGC 之后,我们可以使用构建好的 JDK 启动,需要添加参数-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
。
ZGC 有下面几个目标:
从https://openjdk.java.net/jeps/333给出的数据可以看出来,在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:
这里预告一下,Java12 中也增加了一个实现阶段的垃圾收集器 Shenandoah,到时候咱们看一下。
Java 11 优化了 ARM64 或 Arch64 处理器上现有的字符串和数组内部函数。还为java.lang.Math
的sin
、cos
和log
方法实现了新的内部函数。
我们像其他函数一样使用内在函数,但是,编译器会以特殊的方式处理内部函数,将使用 CPU 体系结构特定的汇编代码来提高性能。可以关注一下HotSpotIntrinsicCandidate
这个注解。
Java11 引入了一个新的实验性垃圾收集器:Epsilon。Epsilon 垃圾收集器提供一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间,适用于模拟内存不足错误的场景。
Epsilon 垃圾收集器有几个使用场景:
可以通过-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
参数开启。
飞行记录器(Flight Recorder)可是个好东西,之前是 Oracle JDK 中的一个商用产品,现已在 Open JDK 中开源。这是一种低开销的事件信息收集框架,主要用于对应用程序和 JVM 进行故障检查、分析。
飞行记录器记录的主要数据源于应用程序、JVM 和操作系统,这些事件信息保存在单独的事件记录文件中,故障发生后,能够从事件记录文件中提取出有用信息对故障进行分析。有些类似于飞机上的黑匣子。
比如,我们可以使用以下参数开启一个时长为 120 秒的记录:
-XX:StartFlightRecording=duration=120s,settings=profile,filename=recording.jfr
生成的文件可以使用 JMC 工具可视化查看,也可以自己写代码通过RecordedEvent
解析。不过嘛,有可视化的,干嘛还要自己敲代码呢?
我们也可以在运行时通过jcmd
命令启动记录:
$ jcmd <pid> JFR.start$ jcmd <pid> JFR.dump filename=recording.jfr$ jcmd <pid> JFR.stop
收到监控,想推广一下之前写的开源监控组件 Cynomys,源码在https://github.com/howardliu-cn/cynomys,里面包含通过 Netty 实现的 RPC 框架、javaagent 实现的探针、使用 javassist 操作字节码、JMX 实现 JVM 内部监控等,可以对操作系统、网络、JVM、请求、SQL 等内容进行监控。
社会在发展,技术在进步。又有一些功能或组件不合时宜,要么移除、要么标记过期。标记过期的最好不要再用了,不知道哪天就会被移除,想要升级依赖反而麻烦。
本文介绍了 Java11 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/11/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java11 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java11 的新特性
你好,我是看山。
Java 语言很强大,但是,有人的地方就有江湖,有猿的地方就有 bug,Java 的核心代码并非十全十美。比如在 JDK 中居然也有反模式接口常量 中介绍的反模式实现,以及本文说到的这个技术债务:嵌套关系(NestMate)调用方式。
在 Java 语言中,类和接口可以相互嵌套,这种组合之间可以不受限制的彼此访问,包括访问彼此的构造函数、字段、方法等。即使是private
私有的,也可以彼此访问。比如下面这样定义:
public class Outer { private int i; public void print1() { print11(); print12(); } private void print11() { System.out.println(i); } private void print12() { System.out.println(i); } public void callInnerMethod() { final Inner inner = new Inner(); inner.print4(); inner.print5(); System.out.println(inner.j); } public class Inner { private int j; public void print3() { System.out.println(i); print1(); } public void print4() { System.out.println(i); print11(); print12(); } private void print5() { System.out.println(i); print11(); print12(); } }}
上例中,Outer
类中的字段i
、方法print11
和print12
都是私有的,但是可以在Inner
类中直接访问,Inner
类的字段j
、方法print5
是私有的,也可以在Outer
类中使用。这种设计是为了更好的封装,在用户看来,这几个彼此嵌套的类/接口是一体的,分开定义是为了更好的封装自己,隔离不同特性,但是有因为彼此是一体,所以私有元素也应该是共有的。
我们使用 Java8 编译,然后借助javap -c
命令分别查看Outer
和Inner
的结果。
$ javap -c Outer.class Compiled from "Outer.java"public class cn.howardliu.tutorials.java8.nest.Outer { public cn.howardliu.tutorials.java8.nest.Outer(); Code: 0: aload_0 1: invokespecial #4 // Method java/lang/Object."<init>":()V 4: return public void print1(); Code: 0: aload_0 1: invokespecial #2 // Method print11:()V 4: aload_0 5: invokespecial #1 // Method print12:()V 8: return public void callInnerMethod(); Code: 0: new #7 // class cn/howardliu/tutorials/java8/nest/Outer$Inner 3: dup 4: aload_0 5: invokespecial #8 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java8/nest/Outer;)V 8: astore_1 9: aload_1 10: invokevirtual #9 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.print4:()V 13: aload_1 14: invokestatic #10 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$000:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)V 17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 20: aload_1 21: invokestatic #11 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$100:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)I 24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 27: return static int access$200(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: getfield #3 // Field i:I 4: ireturn static void access$300(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: invokespecial #2 // Method print11:()V 4: return static void access$400(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: invokespecial #1 // Method print12:()V 4: return}
再来看看Inner
的编译结果,这里需要注意的是,内部类会使用特殊的命名方式定义Inner
类,最终会将编译结果存储在两个文件中:
$ javap -c Outer\$Inner.classCompiled from "Outer.java"public class cn.howardliu.tutorials.java8.nest.Outer$Inner { final cn.howardliu.tutorials.java8.nest.Outer this$0; public cn.howardliu.tutorials.java8.nest.Outer$Inner(cn.howardliu.tutorials.java8.nest.Outer); Code: 0: aload_0 1: aload_1 2: putfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 5: aload_0 6: invokespecial #4 // Method java/lang/Object."<init>":()V 9: return public void print3(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 7: invokestatic #6 // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I 10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 17: invokevirtual #8 // Method cn/howardliu/tutorials/java8/nest/Outer.print1:()V 20: return public void print4(); Code: 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 7: invokestatic #6 // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I 10: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 17: invokestatic #9 // Method cn/howardliu/tutorials/java8/nest/Outer.access$300:(Lcn/howardliu/tutorials/java8/nest/Outer;)V 20: aload_0 21: getfield #3 // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer; 24: invokestatic #10 // Method cn/howardliu/tutorials/java8/nest/Outer.access$400:(Lcn/howardliu/tutorials/java8/nest/Outer;)V 27: return static void access$000(cn.howardliu.tutorials.java8.nest.Outer$Inner); Code: 0: aload_0 1: invokespecial #2 // Method print5:()V 4: return static int access$100(cn.howardliu.tutorials.java8.nest.Outer$Inner); Code: 0: aload_0 1: getfield #1 // Field j:I 4: ireturn}
我们可以看到,Outer
和Inner
中多出了几个方法,方法名格式是access$*00
。
Outer
中的access$200
方法返回了属性i
,access$300
和access$400
分别调用了print11
和print12
方法。这些新增的方法都是静态方法,作用域是默认作用域,即包内可用。这些方法最终被Inner
类中的print3
和print4
调用,相当于间接调用Outer
中的私有属性或方法。
我们称这些生成的方法为“桥”方法(Bridge Method),是一种实现嵌套关系内部互相访问的方式。
在编译的时候,Java 为了保持类的单一特性,会将嵌套类编译到多个 class 文件中,同时为了保证嵌套类能够彼此访问,自动创建了调用私有方法的“桥”方法,这样,在保持原有定义不变的情况下,又实现了嵌套语法。
“桥”方法的实现是比较巧妙的,但是这会造成源码与编译结果访问控制权限不一致,比如,我们可以在Inner
中调用Outer
中的私有方法,按照道理来说,我们可以在Inner
中通过反射调用Outer
的方法,但实际上不行,会抛出IllegalAccessException
异常。我们验证一下:
public class Outer { // 省略其他方法 public void callInnerReflectionMethod() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { final Inner inner = new Inner(); inner.callOuterPrivateMethod(this); } public class Inner { // 省略其他方法 public void callOuterPrivateMethod(Outer outer) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { final Method method = outer.getClass().getDeclaredMethod("print12"); method.invoke(outer); } }}
定义测试用例:
@Testvoid gotAnExceptionInJava8() { final Outer outer = new Outer(); final Exception e = assertThrows(IllegalAccessException.class, outer::callInnerReflectionMethod); e.printStackTrace(); assertDoesNotThrow(outer::callInnerMethod);}
打印的异常信息是:
java.lang.IllegalAccessException: class cn.howardliu.tutorials.java8.nest.Outer$Inner cannot access a member of class cn.howardliu.tutorials.java8.nest.Outer with modifiers "private" at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361) at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591) at java.base/java.lang.reflect.Method.invoke(Method.java:558) at cn.howardliu.tutorials.java8.nest.Outer$Inner.callOuterPrivateMethod(Outer.java:62) at cn.howardliu.tutorials.java8.nest.Outer.callInnerReflectionMethod(Outer.java:36)
通过反射直接调用私有方法会失败,但是可以直接的或者通过反射访问这些“桥”方法,这样就比较奇怪了。所以提出 JEP181 改进,修复这个技术债务的同时,为后续的改进铺路。
我们再来看看 Java11 编译之后的结果:
$ javap -c Outer.class Compiled from "Outer.java"public class cn.howardliu.tutorials.java11.nest.Outer { public cn.howardliu.tutorials.java11.nest.Outer(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void print1(); Code: 0: aload_0 1: invokevirtual #2 // Method print11:()V 4: aload_0 5: invokevirtual #3 // Method print12:()V 8: return public void callInnerMethod(); Code: 0: new #7 // class cn/howardliu/tutorials/java11/nest/Outer$Inner 3: dup 4: aload_0 5: invokespecial #8 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java11/nest/Outer;)V 8: astore_1 9: aload_1 10: invokevirtual #9 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print4:()V 13: aload_1 14: invokevirtual #10 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print5:()V 17: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 20: aload_1 21: getfield #11 // Field cn/howardliu/tutorials/java11/nest/Outer$Inner.j:I 24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V 27: return}
是不是很干净,与Outer
类的源码结构是一致的。我们再看看Inner
有没有什么变化:
$ javap -c Outer\$Inner.classCompiled from "Outer.java"public class cn.howardliu.tutorials.java11.nest.Outer$Inner { final cn.howardliu.tutorials.java11.nest.Outer this$0; public cn.howardliu.tutorials.java11.nest.Outer$Inner(cn.howardliu.tutorials.java11.nest.Outer); Code: 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return public void print3(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 7: getfield #4 // Field cn/howardliu/tutorials/java11/nest/Outer.i:I 10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 17: invokevirtual #6 // Method cn/howardliu/tutorials/java11/nest/Outer.print1:()V 20: return public void print4(); Code: 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 7: getfield #4 // Field cn/howardliu/tutorials/java11/nest/Outer.i:I 10: invokevirtual #5 // Method java/io/PrintStream.println:(I)V 13: aload_0 14: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 17: invokevirtual #7 // Method cn/howardliu/tutorials/java11/nest/Outer.print11:()V 20: aload_0 21: getfield #1 // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer; 24: invokevirtual #8 // Method cn/howardliu/tutorials/java11/nest/Outer.print12:()V 27: return}
同样干净。
我们在通过测试用例验证一下反射调用:
@Testvoid doesNotGotAnExceptionInJava11() { final Outer outer = new Outer(); assertDoesNotThrow(outer::callInnerReflectionMethod); assertDoesNotThrow(outer::callInnerMethod);}
结果是正常运行。
这就是 JEP181 期望的结果,源码和编译结果一致,访问控制一致。
在 Java11 中还新增了几个 API,用于嵌套关系的验证:
这个方法是返回嵌套主机(NestHost),转成普通话就是找到嵌套类的外层类。对于非嵌套类,直接返回自身(其实也算是返回外层类)。
我们看下用法:
@Testvoid checkNestHostName() { final String outerNestHostName = Outer.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.Outer", outerNestHostName); final String innerNestHostName = Inner.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.Outer", innerNestHostName); assertEquals(outerNestHostName, innerNestHostName); final String notNestClass = NotNestClass.class.getNestHost().getName(); assertEquals("cn.howardliu.tutorials.java11.nest.NotNestClass", notNestClass);}
对于Outer
和Inner
都是返回了cn.howardliu.tutorials.java11.nest.Outer
。
这个方法是返回嵌套类的嵌套成员数组,下标是 0 的元素确定是 NestHost 对应的类,其他元素顺序没有给出排序规则。我们看下使用:
@Testvoid getNestMembers() { final List<String> outerNestMembers = Arrays.stream(Outer.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toList()); assertEquals(2, outerNestMembers.size()); assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer")); assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner")); final List<String> innerNestMembers = Arrays.stream(Inner.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toList()); assertEquals(2, innerNestMembers.size()); assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer")); assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner"));}
这个方法是用于判断两个类是否是彼此的 NestMate,彼此形成嵌套关系。判断依据还是嵌套主机,只要相同,两个就是 NestMate。我们看下使用:
@Testvoid checkIsNestmateOf() { assertTrue(Inner.class.isNestmateOf(Outer.class)); assertTrue(Outer.class.isNestmateOf(Inner.class));}
嵌套关系是作为 Valhalla 项目的一部分,这个项目的主要目标之一是改进 JAVA 中的值类型和泛型。后续会有更多的改进:
Unsafe.defineAnonymousClass()
API 的安全替换,实现将新类创建为已有类的 Nestmate。本文阐述了基于嵌套关系的访问控制优化,其中涉及NestMate
、NestHost
、NestMember
等概念。这次优化是 Valhalla 项目中一部分,主要改进 Java 中的值类型和泛型等。文中涉及源码都上传在 GitHub 上,关注公号「看山的小屋」回复“java”获取源码。
青山不改,绿水长流,咱们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java11 中基于嵌套关系的访问控制优化
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java11 中基于嵌套关系的访问控制优化
你好,我是看山。
一晃又是一年,果然岁数越大,时间越快。有些内容在 原来还能这么干 一文中聊了一些,今天再聊点别的。让我们一起总结过去,把握现在,展望未来。
总结下来,2021 年还算幸运,平平淡淡过一年。工作上按部就班,生活上一如既往。
如果说有什么可以出牛的,就是开始好好写文了。幸运的是在 2021 年最后一天,得到了 InfoQ 官方认可,成为签约作者。
2021 年换了一份工作,感谢前司领导同事的帮助,知道了什么是好好工作,怎样做可以做好工作:
到了现司之后,也有了一些感悟:孤胆英雄是没有办法生存的,团队才是能够好好工作的最小单位。
上面这些,每一条都可以描述很多,既然是年终总结,就先一笔带过,看官可以先自行体会一下。如果有必要,再开文详细聊聊。
2021 年要好好感谢我媳妇,如果有哪位朋友恰好看到这篇文章,记得给小猪转发一下,我猜她一定忽略了我的这份心意。
生活方面没有太多要说的,只有满心的感动和感激。
在 Geek 青年说北京沙龙分享 中聊过,我是 2013 年开始写博客,2018 年停更一年,从 2021 年开始坚持周更。这个过程中,认识了很多志同道合的朋友,见到了很多优秀的博主。
一个人可以走的很快,一群人可以走的很远。
写博客是为了实现自己定的目标,不必太在意结果。不过,正如前面所说,一切用数字说话。下面就晒一下 2021 年的一些成绩(这些成绩和大佬没法比,只能小小的自嗨一下):
C 站粉丝达到 17000,访问量有 870000:
C 站 1024 活动时,收货博客专家勋章:
参加知乎海盐计划,直接升级到 4 级;
参加掘金 11 月更文活动,两次后端模块的周榜前 10;
参加 InfoQ 写作平台签约作者第二季,成功入选。评选结果是在 12 月 31 号公布的,算是给 2021 年的写作之旅画上一个不错的句号。
除了写博客,今年也开始健身了。一开始是维嘉带着练,后来维嘉回了学校是跟着斌哥练,终于看到了 75 公斤的影子。
新的一年,为了对自己负责,对家人负责,对朋友负责,我们总要做出新一年的计划。我 2022 年的计划就是搞定 2021 年那些原定于 2020 年未完成的安排,只为兑现 2019 年时要完成 2018 年许下的诺言,曾说 2017 年之后一定不要像 2016 年那样只会跟着 2015 年去做 2014 年没给 2013 年完成的那个目标。
哈哈哈,上面的文案摘自某音的段子,比较写实。
我真实的计划就不放出来了,心理学上有个研究结果,当多次向别人描述自己的计划,就会产生一种错觉,以为自己已经完成了计划。计划不广而告之,但是一定要有。
没有计划的人生不值得过。
青山不改,绿水长流,咱们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:这一年很幸运,平平淡淡的|2021 年度总结
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:这一年很幸运,平平淡淡的|2021 年度总结
你好,我是看山。
就这样 2022 年了,年纪越大,时间越快,又到了罗胖《时间的朋友》直播的时候,看完后想写点什么,奈何腹中墨水太少,索性不难为自己,随便写写。由于疫情原因,罗胖今年的跨年演讲现场的观众全部都是熊猫娃娃,评论区有整场直播的视频回访和文字稿,有些事情,还是要自己亲自感受一下。
整场演讲的主题是“原来还能这么干”,用我能够理解的话,就是打破思维的墙。
每个人都有自己的思维模式,这是我们赖以生存的根本,也是我们能够更快速、更高效处理普通事务的根本。之前在 别让非理性思维毁了你的人生 中聊过,我们的大脑为了节省能量,会经常处于自动驾驶模式,这让我们可以更有机会省出能量做其他事情。比如,我看看到知道冒白气的水是热的,不会直接喝;我们知道冷了要穿衣服,否则会生病……但是有些时候,我们用常规的办法解决不了问题,怎么办?
想要打破思维的墙,我们需要绝对的理性分析限制我们思维的问题是什么?真正要解决的问题有哪些?就不列举罗胖演讲中的例子了,我们考虑一下网约车出来之前那种打车难的问题。
很多人可能没有经历过打车难的时期,那个时候,我们没有办法知道哪里有出租车,只能就近在路边招手,出租车师傅同样不知道哪有顾客,只能满城转悠,或者在上客概率到的地方等着,比如 CBD、高铁站、机场等。平时还好,等会就等会,如果恰好有急事、或者带着老人小孩、亦或是刮风下雨的时候,就会比较难受。怎么办?
如果我们可以提前和司机师傅约好,在约定地点上车,是不是就可以了。想法有了,接下来就是实现,于是有了一系列的网约车 APP。现在大家打车记录不需要路边拦车了,直接网上下单,指定地点上车即可,方便快捷。
这几年,很多互联网公司的崛起,改变了我们的生活方式,比如:外卖、拼团、移动支付……他们的成功,是走了以前没有人走过的路。
《功勋》中屠呦呦关于常山碱的判断中,认为已经经过反复论证走不通的路,就该果断放弃,立马淘汰,找到更可靠的方式,于是有了后面的青蒿素的发现。
碰到问题,我们要投入百分之百的努力克服困难,但是如果已经不行了,就该考虑换个方式再上。
“行就行,不行再想想办法。”
唯一不变的就是变化。没有什么是永恒不变的,我们能够应对变化的手段,只有提前预知变化,做好准备,当变化来临时,坦然面对。
很多 2020 年风生水起的教培行业,在国家出台双减政策后,一夜之间,大厦轰然倒塌。很多教培行业的老师、研发人员,只能重新考虑未来的发展。其实国家一直有这个信号,我在 想躺平不是错 中也谈过相关的问题。很多人抱怨国家手段强硬,但他们真正抱怨的是,国家没有提前告诉他们要行霹雳手段,改变这个畸形发展的行业。
我们很多人相信风水、星座、命运,其实只是想从中探寻一些未来的可能。罗胖给出了一个观点是,我们没有办法一直追寻改变,只要找到未来的不变,试着靠近他,当未来来临时,我们就已经赶在了潮流的前列。
那怎么找到未来一定发生的事情?个人愚见是翻翻国家政策,比如“十四五”规划,看看规划的未来目标。跟着国家政策走,绝对不会有太大偏差。找到目标了怎么实现呢?有能力上,没能力提升能力也要上,如果还是上不去,就“打破思维的墙”,再想想别的办法。
35 岁焦虑是每个程序员都有的?各种营销号中一直鼓吹一个观点,到了 35 岁,就会一下子变成了没有任何价值的抹布。而且给出很多的理由:
似乎都有道理,但是总感觉哪里不对。罗胖的观点是,年龄大了之后,除了工作能力之外,我们拼的还有软技能。
以编程开发为例,简单的 CRUD,刚毕业的小伙子和 35 岁的人开发结果差不多,但是复杂逻辑呢?但凡有些经验的开发人员,会把场景考虑更加完善,会在开发时考虑更多的设计模式,这些经验,会让程序更加健壮,能够应对更多的变化。而且,经历了社会的毒打之后,我们会比较平和的接受一些职场上的不公平,这不是怂,而是一种心态的转变,“世间事,除了生死,哪一件事不是闲事。”
心态平和了,为人处世才会简单,能够更好的处理人际关系。这就是我们的软技能,如果我们可以在开发之外再有一些亮眼的特点,比如:架构设计、逻辑分析、产品设计、汇报总结等等。
之前看过一篇文章,里面说到,被辞退的员工,不会被告知被辞退的真正原因,只会说是公司效益不好、发展不畅。其实,很多时候是软技能太弱。
既然我们没有办法和 20 多岁的年轻人拼精力,那我们以一个更有生活阅历的年轻人身份在职场中打拼。
不知道从什么时候开始,这种情绪就渗进了我们骨子里面。
写这段内容的时候,写了改,改了删。我企图找到一些证据,证明我的这个想法是对的,我企图找到一个事件,能够代表这种情绪的起点。最后还是删了,这是潜移默化的一个结果。填饱肚子的不是最后一个包子,而是前面 9 个包子的铺垫。
我只表达这种情绪,其他的交给时间。
『1』给重要时刻:
“行万里路,读万遍经。笨鸭早飞,笨牛勤耕。让小的敬老的,拿次的留好的。宁欺官,不欺贤,宁欺贤,不欺天。人多的地方不去,没人的地方不留。赞美成功的人,安慰失败的人。犯病的东西不吃,犯法的事情不做。不要穿金戴银,只要好好做人。墙倒众人推,我不推;枪打出头鸟,我不打。种瓜得瓜瓜儿大,种豆得豆豆儿多。”————《王鼎钧回忆录》
『2』给理性乐观派:
“我的乐观并不需要这些头头是道的逻辑支撑,它就是一种朴素的信念:相信中国会更好。这种信念不是源于学术训练,而是源于司马迁、杜甫、苏轼,源于‘一条大河波浪宽’,源于对中国人勤奋实干的钦佩。它影响了我看待问题的角度和处理信息的方式,我接受这种局限性,没有改变的打算。”————兰小欢
『3』给犹豫不决的人:
“什么是事件?事件就是某种超出了原因的结果。”————齐泽克
『4』给准备出发的人:
“设计是一个不断生成目标和备选方案的过程。”————赫伯特·西蒙
『5』给正在路上的人:
“非常理想,特别现实。”————李希贵
『6』给正在拓荒的人:
“提前一个版本遵守法律”————王永治
『7』给知易行难的人:
“要改变一个成年人的行为,认知、能力、提醒,三者同样重要”————王建硕
『8』给身处困境的人:
“地球上最后一个人独自坐在房间里,这时忽然响起了敲门声……”————弗里蒂克·布朗
『9』给 2022 年的我们:
“让我们泰然自若,与自己的时代狭路相逢”————莎士比亚
生活很难,有时候就需要一种正能量激励我们,哪怕只是轻轻的推一把,齿轮就会转动起来,然后就沿着这种惯性继续下去。
愿大家 2022 年“各从其欲,皆得所愿”。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:原来还能这么干——罗胖2022年《时间的朋友》观后感
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:原来还能这么干——罗胖2022年《时间的朋友》观后感
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
从 Java10 开始,Java 版本正式进入每半年一个版本的更新节奏,更新频率加快,小步快跑。
接下来我们瞅瞅 Java10 都更新了哪些比较有意思的功能。
我们都知道 Java 是强类型语言,有着严格的类型限制。想要定义一个变量,必须明确指明变量类型,用Object
抬杠的可以离开了。但是从 Java10 开始,我们可以在定义局部变量时使用var
限定变量类型,Java 编译器会根据变量的值推断具体的类型。
比如,我们定义一个Map
对象:
Map<Integer, String> map = new HashMap<>();
现在我们可以写做:
var idToNameMap = new HashMap<Integer, String>();
这个功能算是 Java 的一次尝鲜,给 Java 语言增加了更多的可能,让我们的代码更加简洁,更加专注于可读性。
需要注意的是,新增的var
不会把 Java 变成动态语言,在编译时,编译器会自动推断类型,将其转换为确定的类型,不会在运行时动态变化。
目前var
只能用于局部变量,而且等号右侧必须是确定类型的定义,包括:初始化的实例、方法的调用、匿名内部类。
我们再回到刚才的例子:
// 以前的写法Map<Integer, String> map = new HashMap<>();// 现在可以这么写var idToNameMap = new HashMap<Integer, String>();
对于参数的名字,我们可以不在关注类型,可以更多的关注参数的意义,这也是编写可读代码的要求。
这也为我们提出了一些要求,如果是特别长的 Lambda 表达式,还是老老实实的使用明确的类型吧,否则写着写着就迷糊了。
再就是推断类型时没有那么智能,都是基于最明确的推断,比如:
var emptyList = new ArrayList<>();
这个时候推断emptyList
的结果是ArrayList<Object>
,绝对不会按照我们常用写法推断成List<Object>
。
如果是匿名内部类,比如:
var obj = new Object() {};
这个时候obj.getClass()
可就不是Object.class
了,而且匿名内部类的类型了。
所以,小刀虽好,但也要好好用,胡乱用容易误伤。
从 Java9 开始提供不可变集合的实现,Java10 继续扩展。集合是一个容器,作为一个参数传入方法中,我们并不知道方法是否会对容器中的元素进行修改,有了不可变集合,我们就能够在一定程度上进行控制(毕竟对容器中对象的数据进行修改,我们的控制力就没有那么强了)。
针对不可变集合,我们摘取java.util.List
的描述(其他的描述都是类似的):
Unmodifiable Lists
The List.of and List.copyOf static factory methods provide a convenient way to create unmodifiable lists. The List instances created by these methods have the following characteristics:
- They are unmodifiable. Elements cannot be added, removed, or replaced. Calling any mutator method on the List will always cause UnsupportedOperationException to be thrown. However, if the contained elements are themselves mutable, this may cause the List’s contents to appear to change.
- They disallow null elements. Attempts to create them with null elements result in NullPointerException.
- They are serializable if all elements are serializable.
- The order of elements in the list is the same as the order of the provided arguments, or of the elements in the provided array.
- They are value-based. Callers should make no assumptions about the identity of the returned instances. Factories are free to create new instances or reuse existing ones. Therefore, identity-sensitive operations on these instances (reference equality (==), identity hash code, and synchronization) are unreliable and should be avoided.
- They are serialized as specified on the Serialized Form page.
简单翻译一下:
UnsupportedOperationException
异常。但是,but,如果集合中的元素是可变的,那就控不住了。比如,元素是AtomInteger
就没法控制其中的值,集合只是元素不变;如果是String
,那集合是整体不变的。null
,会抛出NullPointerException
copyOf
和of
这些方法中返回的结果可能使用提前定义好的对象,比如空集合、原集合等。换句话说,在不同调用位置返回了相同对象。所以不要相信==
、hashCode
,也不要对其加锁。在java.util.List
、java.util.Map
、java.util.Set
这几个接口中都各自添加了一个copyOf
静态方法,用来创建不可变集合,最终都会是ImmutableCollections
中定义的几个集合实现,与 Java9 中定义的of
方法类似。
对于java.util.Map
、java.util.Set
,这里有一个优化,如果传入的本身就是不可变的集合,将直接返回传入的参数,代码如下:
static <E> Set<E> copyOf(Collection<? extends E> coll) { if (coll instanceof ImmutableCollections.AbstractImmutableSet) { return (Set<E>)coll; } else { return (Set<E>)Set.of(new HashSet<>(coll).toArray()); }}
Java10 很贴心的提供了Stream
中的操作,我们直接创建不可变集合了。比如:
Stream.of("1", "2", "3", "4", "5") .map(x -> "id: " + x) .collect(Collectors.toUnmodifiableList());
toUnmodifiableList
、toUnmodifiableSet
、toUnmodifiableMap
的用法与toList
、toSet
、toMap
没有太多区别,差别在于返回的是不可变集合。
这里说的 Optional 族包括Optional
、OptionalInt
、OptionalLong
、OptionalDouble
几个实现。以前有一个orElseThrow(Supplier<? extends X> exceptionSupplier)
方法,用于获取不到数据时,抛出exceptionSupplier
中定义的异常。
我们会写成:
Stream.of("1", "2", "3", "4", "5") .map(x -> "id: " + x) .findAny() .orElseThrow(() -> new NoSuchElementException("No value present"));
优点是我们可以自定义自己的异常以及异常信息。有时候,我们不关心具体的异常和异常信息,这个时候 Java10 中的新增的orElseThrow
方法就派上用场了:
Stream.of("1", "2", "3", "4", "5") .map(x -> "id: " + x) .findAny() .orElseThrow();
此时如果元素为空,将抛出NoSuchElementException
异常。
Java 当年在性能方面一直被诟病,中间隔着一层虚拟机,实现跨平台运行的功能同时,也致使其执行性能不如 C 语言。所以,Java 一直在性能方面投入大量精力。
我们看看 Java10 中都有哪些优化点。
从 Java9 开始,G1 已经转正,成为默认的垃圾收集器。不过在 Full GC 时,G1 还是采用的单线程串行标记压缩算法,这样 STW 时间会比较长。到 Java10,Full GC 实现了并行标记压缩算法,明显缩短 STW 时间。
CDS(Class-Data Sharing,类数据共享)是在 Java5 引入的一种类预处理方式,可以将一组类共享到一个归档文件中,在运行时通过内存映射加载类,这样做可以减少启动时间。同时在多个 JVM 之间实现同享同一个归档文件,减少动态内存占用。
但是 CDS 有一个限制,就是只能是 Bootstrap ClassLoader 使用,这样就将功能限制了类的范围。在 Java10 中,将这个功能扩展到了系统类加载器(System ClassLoader,或者成为应用类加载器,Application ClassLoader)、内置的平台类加载器(Platform ClassLoader),或者是自定义的类加载器。这样就将功能扩展到了应用类。
想要使用这个功能的话,总共分三步:
hello.jar
中的HelloWorld
类使用的类添加到hello.lst
中:$ java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=hello.lst \ -cp hello.jar HelloWorld
hello.lst
中的内容创建 AppCDS 文件hello.jsa
:$ java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=hello.lst \ -XX:SharedArchiveFile=hello.jsa -cp hello.jar
hello.jsa
启动HelloWorld
:$ java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \ -cp hello.jar HelloWorld
Graal 是使用 Java 编写的与 HotSpot JVM 集成的动态编译器,专注于高性能和可扩展性。是从 JDK9 引入的实验性 AOT 编译器的基础。
在 JDK10 中,我们可以在 Linux/x64 平台将 Graal 作为 JIT 编译器使用。开启命令如下:
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
需要注意的是,这是实验特性,相当于公测阶段,可能在某些场景下,性能不如现有的 JIT 编译器。
容器化是目前的趋势,在之前,由于 JVM 不能够感知容器,在同一个主机上部署多个虚拟机时,会造成内存占用、CPU 抢占等问题,这点也成为了很多大会上抨击 Java 语言不适合容器时代的一个点。
现在好了,JVM 可以感知容器了。只是暂时还只支持 Linux 系统(so what,其他平台也还没有用过)。这个功能默认开启,不想使用可以手动关闭:
-XX:-UseContainerSupport
我们还可以手动指定 CPU 核数:
-XX:ActiveProcessorCount=1
还有三个可以控制内存的使用量:
-XX:InitialRAMPercentage-XX:MaxRAMPercentage-XX:MinRAMPercentage
就目前来看,这部分还可以继续完善,相信只是时间问题。
自 Java9 起在 keytool 中加入参数 -cacerts,可以查看当前 JDK 管理的根证书。而 Java9 中 cacerts 目录为空,这样就会给开发者带来很多不便。从 Java10 开始,将会在 JDK 中提供一套默认的 CA 根证书。
作为 JDK 一部分的 cacerts 密钥库旨在包含一组能够用于在各种安全协议的证书链中建立信任的根证书。在 Java10 之前,cacerts 密钥库是空的,默认情况下,关键安全组件(如 TLS)是不起作用的,开发人员需要手动添加一组根证书来使用这些验证。
Java10 中,Oracle 开放了根证书源码,可以让 OpenJDK 构建对开发人员更有吸引力,并减少这些构建与 Oracle JDK 构建之间的差异。
有增有减,这样才能够保证 Java 的与时共进。
javah
命令,这个命令用于创建 native 方法所需的 C 的头文件和资源文件的,使用javac -h
替代。policytool
工具,这个工具用于创建和管理策略文件。可以直接还使用文本编辑器代替。java -Xprof
参数,这个参数本来是用于评测正在运行的程序,并将评测数据发送到标准输出。可以使用jmap
代替。java.security.acl
包标记为过期,标记参数forRemoval
是true
,将在未来版本中删除。目前,这个包内的功能已经被java.security.Policy
取代。java.security
包中的Certificate
、Identity
、IdentityScope
、Signer
的标记参数forRemoval
也是true
。这些都将在后续版本中删除。
从 Java10 开始,Java 正式进入每半年一个版本的更新节奏,主要改动如下:
$FEATURE.$INTERIM.$UPDATE.$PATCH
命名机制:$FEATURE
,每次版本发布加 1,不考虑具体的版本内容;$INTERIM
,中间版本号,在大版本中间发布的,包含问题修复和增强的版本,不会引入非兼容性修改;$PATCH
用于快速打补丁的。本文介绍了 Java10 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk/10/查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java10 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java10 的新特性
你好,我是看山。
今天项目依赖了一个基础组件之后,启动失败,排查过程走了一些弯路,最终确认是因为依赖组件版本冲突造成了java.lang.NoClassDefFoundError
异常。下面是排查过程,希望可以给你提供一些思路。
下面是打印的异常栈信息,从其中提炼可能的关键信息,能够找到“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”,还有“Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]”。继续从异常栈中找一下发生的时机,可以发现是调用AbstractAutowireCapableBeanFactory.createBeanInstance
时,这个方法是创建 Bean 实例。
这块是异常信息(getMessage 的内容,横向太长,手动换行了):org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'addressMapper' defined in file [/Users/liuxinghao/Documents/work/code/cn.howardliu/effective-spring/target/classes/cn/howardliu/demo/AddressMapper.class]: Unsatisfied dependency expressed through constructor parameter 0: Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Class'; nested exception is java.lang.IllegalArgumentException: Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]下面是异常栈: at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:799) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:540) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1341) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1181) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207) ~[spring-beans-5.2.13.RELEASE.jar:5.2.13.RELEASE]其他异常栈信息可以忽略了
我们可以根据目前有效的信息进行排查,首先看下我们的cn.howardliu.demo.AddressMapper
定义是否有问题,再看看依赖它的 Service 有没有问题,什么问题也没有发现。下一个检查点是配置,比如@MapperScan
是否正确、Mapper 类上有没有加上@Mapper
注解,发现也没有问题。
从异常信息找不到思路了,只能从代码入手了。
这里需要说一下,打印异常信息至关重要,直接影响我们排错的思路。如果打印的信息没有办法准确定位,我们将会花费大量的时间查找真正的错误,这就需要走查代码,有时候还需要一些经验。
我们由异常栈ConstructorResolver.createArgumentArray(ConstructorResolver.java:799)
入手,跟着断点往下追,最终会追到org.springframework.util.ClassUtils#forName
方法,其中会抛出异常的代码是下面这块:
try { return Class.forName(name, false, clToUse);}catch (ClassNotFoundException ex) { int lastDotIndex = name.lastIndexOf(PACKAGE_SEPARATOR); if (lastDotIndex != -1) { String innerClassName = name.substring(0, lastDotIndex) + INNER_CLASS_SEPARATOR + name.substring(lastDotIndex + 1); try { return Class.forName(innerClassName, false, clToUse); } catch (ClassNotFoundException ex2) { // Swallow - let original exception get through } } throw ex;}
出现错误的是Class.forName(name, false, clToUse)
这句,name
传的是”cn.howardliu.demo.AddressMapper”字符串,抛出的异常是java.lang.NoClassDefFoundError
,由于不是ClassNotFoundException
异常,不会进入catch
逻辑,会直接向上抛出。
找到错误我们就好定位问题了。
一般来说,java.lang.NoClassDefFoundError
错误是需要加载的类能够找到,但是加载时出现了异常,简单说就是,类的定义有问题。我们借助 JD-GUI 反编译一下运行 jar 包,结果如下:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;import cn.howardliu.demo.Address;import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface AddressMapper extends BaseMapper<Address> {}
观察仔细的话,我们可以看到import com.baomidou.mybatisplus.core.mapper.BaseMapper;
这行没有下划线,也就是说,在反编译工具中追溯不到这个接口,推断出来就是在运行环境中,找不到BaseMapper
这个类定义。
所以,当Class.forName
加载类的时候抛出了java.lang.NoClassDefFoundError
异常。
如果有一定经验,就会立刻想到,大概率出现了依赖 jar 的版本冲突。
我们可以借助 maven 命令行找到版本冲突的依赖:
mvn dependency:tree -Dverbose | grep conflict
打印结果为:
[INFO] | +- (com.baomidou:mybatis-plus:jar:3.1.2:compile - omitted for conflict with 2.1.6)
我们也可以借助 IDEA 的可视化工具,在 pom.xml 上打开依赖图:
我们可以看到 mybatis-plus 的红线指示出冲突信息:
结论就是 Mybatis-Plus 版本冲突了,项目中依赖了 mybatis-plus 的 2.1.6 和 3.1.2 两个版本,由于 2.1.6 路径更短,最终被选中。
此时只需要将低版本的依赖去掉即可。
为什么低版本的 mybatis-plus 会造成类加载失败呢?是因为 mybatis-plus 跨版本更新时,把BaseMapper
的包路径改了:
// 3.1.2 版本import com.baomidou.mybatisplus.core.mapper.BaseMapper;// 2.1.6 版本import com.baomidou.mybatisplus.mapper.BaseMapper;
而且还不止这一个,IService
、ServiceImpl
、TableName
、TableField
、Model
、TableField
等等,很多常用的类都改了位置。所以会造成找不到依赖的类。编译是 3.1.2 依赖还在运行环境中,就会出现编译没有问题,执行时出现加载类异常。
想要工程化的解决这个问题,我们可以创建基础的依赖 bom 配置,定义好基础依赖包,在项目中不在指定版本。这样做到统一版本,可以有效地避免这类问题。
我们还可以在 CI/CD 中加入冲突依赖检查,如果发现冲突依赖,就终止流水线。
接下来我们看下为什么明明是java.lang.NoClassDefFoundError
异常,结果异常栈中打印的是一堆不相干的错误。继续跟着刚才的断点 Debug:
org.springframework.util.ClassUtils#resolveClassName
会捕捉LinkageError
错误,然后包装成IllegalArgumentException
异常,这个时候真是异常还是继续上抛。
然后在org.springframework.beans.TypeConverterSupport#convertIfNecessary
方法会包装成TypeMismatchException
异常,此时,真实异常还在异常cause
参数中,并没有丢失。
等回到org.springframework.beans.factory.support.ConstructorResolver#createArgumentArray
方法后,捕捉异常的方法是:
try { convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);}catch (TypeMismatchException ex) { throw new UnsatisfiedDependencyException( mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), "Could not convert argument value of type [" + ObjectUtils.nullSafeClassName(valueHolder.getValue()) + "] to required type [" + paramType.getName() + "]: " + ex.getMessage());}
此时我们可以注意到,在包装成UnsatisfiedDependencyException
异常的时候,只是把捕捉到的TypeMismatchException
通过getMessage
方法追加在异常描述后面,此时经过前面几轮的包装再包装,真实的异常的异常信息仅剩Unresolvable class definition for class [cn.howardliu.demo.AddressMapper]
这段经过处理的信息,完全没有java.lang.NoClassDefFoundError
的影子了。
至此,真实异常消失无踪。
这也给我们一个提醒,我们要保证异常的时候,一定要保留有效信息,否则,排错会非常麻烦。
本文是抓虫文,从问题出发,到解决问题,给出完整的思路。java.lang.NoClassDefFoundError
一般都是出现在版本冲突的时候,这种异常是编译打包没有问题,在运行时加载类失败。在本文中之所以排查时走了一些弯路,是因为Spring
隐藏了真实异常,给我们排错造成了一些阻碍。所以,我们在日常开发时也要重视异常的明确信息,可以给我们排错提供准确的目标。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Mybatis-Plus 版本冲突触发“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”的 java.lang.NoClassDefFoundError 异常
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Mybatis-Plus 版本冲突触发“Could not convert argument value of type [java.lang.String] to required type [java.lang.Class]”的 java.lang.NoClassDefFoundError 异常
你好,我是看山。
本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。
相较于 Java8,Java9 没有新增语法糖,但是其增加的特性也都是非常实用的,比如 Jigsaw 模块化、JShell、发布-订阅框架、GC 等。本文将快速、高层次的介绍一些新特性,完整的特性可以参加https://openjdk.java.net/projects/jdk9/。
这里需要说明一下,由于 Java9 并不是长期支持版,当前也是从现在看过去,所以笔者偷个懒,文章的示例代码都是在 Java11 下写的,可能会与 Java9 中的定义有些出入,不过,这也没啥,毕竟我们真正使用的时候还是优先考虑长期支持版。
模块化是一个比较大的更新,这让以前 All-in-One 的 Java 包拆分成几个模块。这种模块化系统提供了类似 OSGi 框架系统的功能,比如多个模块可以独立开发,按需引用、按需集成,最终组装成一个完整功能。
模块具有依赖的概念,可以导出功能 API,可以隐藏实现细节。
还有一个好处是可以实现 JVM 的按需使用,能够减小 Java 运行包的体积,让 JVM 在内存更小的设备上运行。JVM 当时的初衷就是做硬件,也算是不忘初心了。
另外,JVM 中com.sun.*
的之类的内部 API,做了更强的封闭,不在允许调用,提升了内核安全。
在使用的时候,我们需要在 java 代码的顶层目录中定义一个module-info.java
文件,用于描述模块信息:
module cn.howardliu.java9.modules.car { requires cn.howardliu.java9.modules.engines; exports cn.howardliu.java9.modules.car.handling;}
上面描述的信息是:模块cn.howardliu.java9.modules.car
需要依赖模块cn.howardliu.java9.modules.engines
,并导出模块cn.howardliu.java9.modules.car.handling
。
更多的信息可以查看 OpenJDK 的指引 https://openjdk.java.net/projects/jigsaw/quick-start,后续会单独介绍 Jigsaw 模块的使用,内容会贴到评论区。
这是一个千呼万唤始出来的功能,终于有官方 API 可以替换老旧难用的HttpURLConnection
。只不过,在 Java9 中,新版 HTTP 客户端是放在孵化模块中(具体信息可以查看 https://openjdk.java.net/jeps/110)。
老版 HTTP 客户端存在很多问题,大家开发的时候基本上都是使用第三方 HTTP 库,比如 Apache HttpClient、Netty、Jetty 等。
新版 HTTP 客户端的目标很多,毕竟这么多珠玉在前,如果还是做成一坨,指定是要被笑死的。所以新版 HTTP 客户端列出了 16 个目标,包括简单易用、打印关键信息、WebSocket、HTTP/2、HTTPS/TLS、良好的性能、非阻塞 API 等等。
我们先简单的瞅瞅:
final String url = "https://postman-echo.com/get";final HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .GET() .build();final HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString());final HttpHeaders headers = response.headers();headers.map().forEach((k, v) -> System.out.println(k + ":" + v));System.out.println(response.statusCode());System.out.println(response.body());
新版 HTTP 客户端可以在 Java11 中正常使用了,上面的代码也是在 Java11 中写的,API 是在
java.net.http
包中。
在 Java9 中提供的进程 API,可以控制和管理操作系统进程。也就是说,可以在代码中管理当前进程,甚至可以销毁当前进程。
这个功能是由java.lang.ProcessHandle
提供的,我们来瞅瞅怎么用:
final ProcessHandle self = ProcessHandle.current();final long pid = self.pid();System.out.println("PID: " + pid);final ProcessHandle.Info procInfo = self.info();procInfo.arguments().ifPresent(x -> { for (String s : x) { System.out.println(s); }});procInfo.commandLine().ifPresent(System.out::println);procInfo.startInstant().ifPresent(System.out::println);procInfo.totalCpuDuration().ifPresent(System.out::println);
java.lang.ProcessHandle.Info
中提供了丰富的进程信息
我们还可以使用java.lang.ProcessHandle#destroy
方法销毁进程,我们演示一下销毁子进程:
ProcessHandle.current().children() .forEach(procHandle -> { System.out.println(procHandle.pid()); System.out.println(procHandle.destroy()); });
从 Java8 之后,我们会发现 Java 提供的 API 使用了
Optional
、Stream
等功能,*Eating your own dog food *也是比较值得学习的。
Java9 中还对做了对已有功能做了点改动,我们来瞅瞅都有哪些。
从 Java7 开始,我们可以使用try-with-resources
语法自动关闭资源,所有实现了java.lang.AutoCloseable
接口,可以作为资源。但是这里会有一个限制,就是每个资源需要声明一个新变量。
也就是这样:
public static void tryWithResources() throws IOException { try (FileInputStream in2 = new FileInputStream("./")) { // do something }}
对于这种直接使用的还算方便,但如果是需要经过一些列方法定义的呢?就得写成下面这个样子:
final Reader inputString = new StringReader("www.howardliu.cn 看山");final BufferedReader br = new BufferedReader(inputString);// 其他一些逻辑try (BufferedReader br1 = br) { System.out.println(br1.lines());}
在 Java9 中,如果资源是final
定义的或者等同于final
变量,就不用声明新的变量名,可以直接在try-with-resources
中使用:
final Reader inputString = new StringReader("www.howardliu.cn 看山");final BufferedReader br = new BufferedReader(inputString);// 其他一些逻辑try (br) { System.out.println(br.lines());}
钻石操作符(也就是<>
)是 Java7 引入的,可以简化泛型的书写,比如:
Map<String, List<String>> strsMap = new TreeMap<String, List<String>>();
右侧的TreeMap
类型可以根据左侧的泛型定义推断出来,借助钻石操作符可以简化为:
Map<String, List<String>> strsMap = new TreeMap<>();
看山会简洁很多,<>
的写法就是钻石操作符 (Diamond Operator)。
但是这种写法不适用于匿名内部类。比如有个抽象类:
abstract static class Consumer<T> { private T content; public Consumer(T content) { this.content = content; } abstract void accept(); public T getContent() { return content; }}
在 Java9 之前,想要实现匿名内部类,就需要写成:
final Consumer<Integer> intConsumer = new Consumer<Integer>(1) { @Override void accept() { System.out.println(getContent()); }};intConsumer.accept();final Consumer<? extends Number> numConsumer = new Consumer<Number>(BigDecimal.TEN) { @Override void accept() { System.out.println(getContent()); }};numConsumer.accept();final Consumer<?> objConsumer = new Consumer<Object>("看山") { @Override void accept() { System.out.println(getContent()); }};objConsumer.accept();
在 Java9 之后就可以使用钻石操作符了:
final Consumer<Integer> intConsumer = new Consumer<>(1) { @Override void accept() { System.out.println(getContent()); }};intConsumer.accept();final Consumer<? extends Number> numConsumer = new Consumer<>(BigDecimal.TEN) { @Override void accept() { System.out.println(getContent()); }};numConsumer.accept();final Consumer<?> objConsumer = new Consumer<>("看山") { @Override void accept() { System.out.println(getContent()); }};objConsumer.accept();
如果说钻石操作符是代码的简洁可读,那接口的私有方法就是比较实用的一个扩展了。
在 Java8 之前,接口只能有常量和抽象方法,想要有具体的实现,就只能借助抽象类,但是 Java 是单继承,有很多场景会受到限制。
在 Java8 之后,接口中可以定义默认方法和静态方法,提供了很多扩展。但这些方法都是public
方法,是完全对外暴露的。如果有一个方法,只想在接口中使用,不想将其暴露出来,就没有办法了。这个问题在 Java9 中得到了解决。我们可以使用private
修饰,限制其作用域。
比如:
public interface Metric { // 常量 String NAME = "METRIC"; // 抽象方法 void info(); // 私有方法 private void append(String tag, String info) { buildMetricInfo(); System.out.println(NAME + "[" + tag + "]:" + info); clearMetricInfo(); } // 默认方法 default void appendGlobal(String message) { append("GLOBAL", message); } // 默认方法 default void appendDetail(String message) { append("DETAIL", message); } // 私有静态方法 private static void buildMetricInfo() { System.out.println("build base metric"); } // 私有静态方法 private static void clearMetricInfo() { System.out.println("clear base metric"); }}
JShell 就是 Java 语言提供的 REPL(Read Eval Print Loop,交互式的编程环境)环境。在 Python、Node 之类的语言,很早就带有这种环境,可以很方便的执行 Java 语句,快速验证一些语法、功能等。
$ jshell| 欢迎使用 JShell -- 版本 13.0.9| 要大致了解该版本,请键入:/help intro
我们可以直接使用/help
查看命令
jshell> /help| 键入 Java 语言表达式,语句或声明。| 或者键入以下命令之一:| /list [<名称或 id>|-all|-start]| 列出您键入的源| /edit <名称或 id>。很多的内容,鉴于篇幅,先隐藏
我们看下一些简单的操作:
jshell> "This is a test.".substring(5, 10);$2 ==> "is a "jshell> 3+1$3 ==> 4
也可以创建方法:
jshell> int mulitiTen(int i) { return i*10;}| 已创建 方法 mulitiTen(int)jshell> mulitiTen(3)$6 ==> 30
想要退出 JShell 直接输入:
jshell> /exit| 再见
jcmd
是用于向本地 jvm 进程发送诊断命令,这个命令是从 JDK7 提供的命令行工具,常用于快速定位线上环境故障。
在 JDK9 之后,提供了一些新的子命令,查看 JVM 中加载的所有类及其继承结构的列表。比如:
$ jcmd 22922 VM.class_hierarchy -i -s java.net.Socket22922:java.lang.Object/null|--java.net.Socket/null| implements java.io.Closeable/null (declared intf)| implements java.lang.AutoCloseable/null (inherited intf)| |--sun.nio.ch.SocketAdaptor/null| | implements java.lang.AutoCloseable/null (inherited intf)| | implements java.io.Closeable/null (inherited intf)
第一个参数是进程 ID,都是针对这个进程执行诊断。我们还可以使用set_vmflag
参数在线修改 JVM 参数,这种操作无需重启 JVM 进程。
有时候还需要查看当前进程的虚拟机参数选项和当前值:jcmd 22922 VM.flags -all
。
在 Java9 中定义了多分辨率图像 API,我们可以很容易的操作和展示不同分辨率的图像了。java.awt.image.MultiResolutionImage
将一组具有不同分辨率的图像封装到单个对象中。java.awt.Graphics
类根据当前显示 DPI 度量和任何应用的转换从多分辨率图像中获取变量。
以下是多分辨率图像的主要操作方法:
Image getResolutionVariant(double destImageWidth, double destImageHeight)
:获取特定分辨率的图像变体-表示一张已知分辨率单位为 DPI 的特定尺寸大小的逻辑图像,并且这张图像是最佳的变体。List<Image> getResolutionVariants()
:返回可读的分辨率的图像变体列表。我们来看下应用:
final List<Image> images = List.of( ImageIO.read(new URL("https://static.howardliu.cn/about/kanshanshuo_2.png")), ImageIO.read(new URL("https://static.howardliu.cn/about/hellokanshan.png")), ImageIO.read(new URL("https://static.howardliu.cn/about/evil%20coder.jpg")));// 读取所有图片final MultiResolutionImage multiResolutionImage = new BaseMultiResolutionImage(images.toArray(new Image[0]));// 获取图片的所有分辨率final List<Image> variants = multiResolutionImage.getResolutionVariants();System.out.println("Total number of images: " + variants.size());for (Image img : variants) { System.out.println(img);}// 根据不同尺寸获取对应的图像分辨率Image variant1 = multiResolutionImage.getResolutionVariant(100, 100);System.out.printf("\nImage for destination[%d,%d]: [%d,%d]", 100, 100, variant1.getWidth(null), variant1.getHeight(null));Image variant2 = multiResolutionImage.getResolutionVariant(200, 200);System.out.printf("\nImage for destination[%d,%d]: [%d,%d]", 200, 200, variant2.getWidth(null), variant2.getHeight(null));Image variant3 = multiResolutionImage.getResolutionVariant(300, 300);System.out.printf("\nImage for destination[%d,%d]: [%d,%d]", 300, 300, variant3.getWidth(null), variant3.getHeight(null));Image variant4 = multiResolutionImage.getResolutionVariant(400, 400);System.out.printf("\nImage for destination[%d,%d]: [%d,%d]", 400, 400, variant4.getWidth(null), variant4.getHeight(null));Image variant5 = multiResolutionImage.getResolutionVariant(500, 500);System.out.printf("\nImage for destination[%d,%d]: [%d,%d]", 500, 500, variant5.getWidth(null), variant5.getHeight(null));
变量句柄(Variable Handles)的 API 主要是用来替代java.util.concurrent.atomic
包和sun.misc.Unsafe
类的部分功能,并且提供了一系列标准的内存屏障操作,用来更加细粒度的控制内存排序。一个变量句柄是一个变量(任何字段、数组元素、静态表里等)的类型引用,支持在不同访问模型下对这些类型变量的访问,包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap) 等。
这部分内容涉及反射、内联、并发等内容,后续会单独介绍,文章最终会发布在 从小工到专家的 Java 进阶之旅 中,敬请关注。
在 Java9 中增加的java.util.concurrent.Flow
支持响应式 API 的发布-订阅框架,他们提供在 JVM 上运行的许多异步系统之间的互操作性。我们可以借助SubmissionPublisher
定制组件。
关于响应式 API 的内容可以先查看 http://www.reactive-streams.org/的内容,后续单独介绍,文章最终会发布在 从小工到专家的 Java 进阶之旅 中,敬请关注。怎么感觉给自己刨了这么多坑,得抓紧时间填坑了。
在这个版本中,为 JVM 的所有组件引入了一个通用的日志系统。它提供了日志记录的基础。这个功能是通过-Xlog
启动参数指定,并且定义很多标签用来定义不同类型日志,比如:gc(垃圾收集)、compiler(编译)、threads(线程)等等。比如,我们定义debug
等级的 gc 日志,日志存储在gc.log
文件中:
java -Xlog:gc=debug:file=gc.log:none
因为参数比较多,我们可以通过java -Xlog:help
查看具体定义参数。而且日志配置可以通过jcmd
命令动态修改,比如,我们将日志输出文件修改为gc_other.log
:
jcmd ${PID} VM.log output=gc_other.log what=gc
在 Java9 中增加的java.util.List.of()
、java.util.Set.of()
、java.util.Map.of()
系列方法,可以一行代码创建不可变集合。在 Java9 之前,我们想要初始化一个有指定值的集合,需要执行一堆add
或put
方法,或者依赖guava
框架。
而且,这些集合对象是可变的,假设我们将值传入某个方法,我们就没有办法控制这些集合的值不会被修改。在 Java9 之后,我们可以借助ImmutableCollections
中的定义实现初始化一个不可变的、有初始值的集合了。如果对这些对象进行修改(新增元素、删除元素),就会抛出UnsupportedOperationException
异常。
这里不得不提的是,Java 开发者们也是考虑了性能,针对不同数量的集合,提供了不同的实现类:
List12
、Set12
、Map1
专门用于少量(List 和 Set 是 2 个,对于 Map 是 1 对)元素数量的场景ListN
、SetN
、MapN
用于数据量多(List 和 Set 是超过 2 个,对于 Map 是多余 1 对)的场景Java9 中为Optional
添加了三个实用方法:stream
、ifPresentOrElse
、or
。
stream
是将Optional
转为一个Stream
,如果该Optional
中包含值,那么就返回包含这个值的Stream
,否则返回Stream.empty()
。比如,我们有一个集合,需要过滤非空数据,在 Java9 之前,写法如下:
final List<Optional<String>> list = Arrays.asList( Optional.empty(), Optional.of("看山"), Optional.empty(), Optional.of("看山的小屋"));final List<String> filteredList = list.stream() .flatMap(o -> o.isPresent() ? Stream.of(o.get()) : Stream.empty()) .collect(Collectors.toList());
在 Java9 之后,我们可以借助stream
方法:
final List<String> filteredListJava9 = list.stream() .flatMap(Optional::stream) .collect(Collectors.toList());
ifPresentOrElse
:如果一个Optional
包含值,则对其包含的值调用函数action
,即action.accept(value)
,这与ifPresent
方法一致;如果Optional
不包含值,那会调用emptyAction
,即emptyAction.run()
。效果如下:
Optional<Integer> optional = Optional.of(1);optional.ifPresentOrElse(x -> System.out.println("Value: " + x), () -> System.out.println("Not Present."));optional = Optional.empty();optional.ifPresentOrElse(x -> System.out.println("Value: " + x), () -> System.out.println("Not Present."));// 输出结果为:// 作者:看山// 佚名
or
:如果值存在,返回Optional
指定的值,否则返回一个预设的值。效果如下:
Optional<String> optional1 = Optional.of("看山");Supplier<Optional<String>> supplierString = () -> Optional.of("佚名");optional1 = optional1.or(supplierString);optional1.ifPresent(x -> System.out.println("作者:" + x));optional1 = Optional.empty();optional1 = optional1.or(supplierString);optional1.ifPresent(x -> System.out.println("作者:" + x));// 输出结果为:// 作者:看山// 作者:佚名
本文介绍了 Java9 新增的特性,完整的特性清单可以从https://openjdk.java.net/projects/jdk9/查看。文中也给自己刨了几个坑,碍于篇幅,没有办法展开,所有这些需要展开的功能细述,都会在 Java8 到 Java17 的新特性系列完成后补充,博文会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java9 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java9 的新特性
你好,我是看山。
前面咱们聊了架构的演进过程,提到单体架构、SOA 架构、微服务架构、无服务架构。整个过程如下图:
目前无服务架构还未成熟,只能满足一些简单场景。所以大家在设计软件架构时,首选还是微服务架构。然后我们又聊了聊如何把单体架构改造为微服务架构,推荐采用绞杀模式,一步一步的实现系统微服务化。
在这个过程中,我们会碰到微服务架构的一个大坑:分布式错觉,即将分布式当成了微服务的全部(充要条件)。
出现分布式单体的主要原因在于,只是用进程间的远程调用替换进程内的方法调用。
从上图可以看出,单体架构在模块 A 与模块 B 之间的请求是通过进程内通信(通常是方法调用)实现的;在微服务架构中,两者之间是通过 REST 或 RPC 调用。抛开进程和消息通知机制的差异,两种架构中模块 A 与模块 B 之间的通信形式完全一致:
在这种情况下,模块 A 与模块 B 耦合在一起,任何一方变更请求契约(方法签名或接口参数),另外一个都必须同步修改。更糟糕的是,由于微服务架构服务之间是通过网络通信,由于其不可靠性和不稳定性,大大增加了出错的概率,使模块之间的调用关系更加脆弱。
模块 A 与模块 B 之间的网络请求是同步调用,请求过程中会占用一个网络连接和至少一个线程,如果模块 A 与模块 B 所在的服务的承压能力不同,很有可能模块 B 所在服务被打满,后续模块 A 的请求会阻塞等待,直到请求超时。
那又是什么原因让大家没有意识到这种方式不妥呢?原因有两个:
针对于微服务架构中的数据一致性问题,可以参考 关于微服务系统中数据一致性的总结。
下面我们重点说说如果解决第一个问题。
对于模块之间的关系,主要在于通信模式,对于查询请求,由于数据依赖,模块之间的耦合是天然的,我们这里要解耦的是数据变更(增、删、改)时的模块调用。
类比一下现实,我们如果想要通知某些人一个消息,会怎么处理?一般来说,有两种方式:
这两种方式对应了我们系统设计中消息传递的两个模式:指令(Command)、事件(Event)。
指令(Command)是表示从发起者(source)向执行者(destination)传递(send)一个必须执行某个动作(action)的请求(request)。这个模式有如下特点:
事件(event)是表示由生产者(producer)发布(public)一个已经发生的事情,表示行为(action)已经发生,某些状态(status)发生了改变,消费者(consumer)订阅这些事件,然后做出响应。这个模式有如下特点:
由于请求模式的不同,在依赖关系上就会发生改变:
在指令模式中,模块 A 调用模块 B,属于直接调用,模块 A 需要依赖模块 B;在事件模式中,模块 A 把事件发送给消息中间件,其他需要订阅事件的服务,直接从消息中间件获取,这种会产生依赖倒置,模块 B 依赖模块 A。这是解耦模块 A 与模块 B 很好的方式。
我们再回过头来看看我们的问题:
此时我们会比较清晰,由于全系统中使用了指令模式,上次调用者依赖下层,由于是同步请求,依赖会发生传递,这种依赖传递,将整个系统耦合在一起,一处修改,处处变动,也就是我们在抨击单体架构时常说的牵一发而动全身。
此时,我们就可以借助事件模式,将依赖链条打断。但是需要注意,不要矫枉过正的全部改为事件模式,那将会是另一个火坑。一般我们会将系统改造成下面的样子:
根据业务具体情况,我们可以归纳一下改造结果:
需要注意的是,每个服务内部还有有一些操作。抽象一下,整个系统中的指令、事件、操作如下图:
架构设计的过程不是非此即彼,全部指令会造成耦合,全部事件会致使开发难度提升以及边界不清。我们需要理性的看待两种模式,做到不偏不倚。
本文从分布式单体陷阱展开,讲述了分布式错觉带来的问题,然后通过事件、指令两种模式相结合的方式解决问题。微服务是目前比较完善的架构风格,从单体到微服务架构,是要实现架构的升级,所以调用模式不会一成不变。这个陷阱,也是我们在做新系统时需要避免的。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:微服务架构的陷阱:从单体到分布式单体
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:微服务架构的陷阱:从单体到分布式单体
你好,我是看山。
前文我们聊了介绍了单体架构、SOA 架构、微服务架构、无服务架构。如果原来是单体架构,想要切换到微服务架构,该怎么解决呢?本文来聊聊这个话题,解决“什么时候(WHEN)、怎样做(HOW)”。
微服务架构是一种架构风格,专注于软件研发效能,主要包括单位时间内实现更多功能,或者软件从想法到上线的整个持续交付的过程。在当前的互联网环境中,业务变化迅速,使用微服务架构,可以让团队迅速反应,快速实施,在方案没有过期之前已经上线运行,经受市场考察和考验。
目前国内大多数公司正在运行的系统都是单体架构系统,不可否认,这些系统在公司发展过程中,发挥了不可替代的作用,保障了公司正常运行,创造了很多价值。但是,随着系统的日渐膨胀,集成的功能越来越多,开发效率变得越来越低,一个功能从想法到实现,需要花费越来越长的时间。更严重的是,由于代码模块纠结在一起,很多已经老化的架构或者废弃的功能,已经成为新功能的阻碍。
上图中,X 轴是业务复杂度,Y 轴是单位效益。绿色线条代表单体架构,蓝色线条代表微服务架构。
可以看到,在业务发展初期,系统复杂度不高,业务不够成熟,我们主要经历在于业务试错。如果采用单体架构,可以将所有的功能、模块放在一个进程;如果此时采用微服务架构,我们就需要考虑进程间通信、不可靠网络故障等,还需要实现微服务的基础组件。这个时候,单体架构的单位效益是高于微服务架构的。
随着业务发展,系统承载的功能越来越多,单体架构的劣势凸显,比如:
总而言之,单体架构随着功能增多,不可避免的是研发效能的降低:研发周期变长、研发资源占用增多。从而引发的情况是:新员工培训时间增多、员工加班时间变长、员工要求涨薪或者跳槽。
当达到图中的交叉点时,说明单体架构已经不能够满足企业发展需要,这个时候,需要升级架构来提升研发效能,比如微服务架构。
有人会问,这个时间点不太好把握。我们只需要考虑三个问题即可:
如果都是肯定回答,那就该着手准备将单体架构切换为微服务架构了。
从单体架构到微服务架构,从一个大一统的系统,拆分成一个一个单独的小服务,我们需要投入精力,做基础的准备:
首先,什么是微服务架构呢?通俗的定义是:“一组围绕业务领域建模的、小而自治的、彼此协同工作的服务。”
微服务架构中的服务,是根据业务能力抽取的业务模块,独立开发和部署,但是需要彼此配合完成整个业务功能。服务不是单纯的数据存储组件,也不是单纯的逻辑函单元。只有同时包括数据+逻辑,才是真正意义上的服务。
服务拆解过程中,DDD(领域驱动设计)可以作为微服务架构的指导方针。因为微服务是围绕业务功能定义服务,根据服务定义团队,这与 DDD 将业务域拆解为业务子域、定义限定上下文的方法论如出一辙,于是 DDD 作为微服务的指导方针,快速定义各个服务组件,完成从单体架构到微服务架构的迁移。
Alberto Brandolini 提出识别服务上下文的方式叫做“Event Storming”。第一步是识别业务域中发生的事件,也就是说,我们的关注点是行为,不是数据结构。这样做的好处是,系统中不同服务之间是松散耦合关系,而且单个服务能够自治。
定义好了服务边界,还需要定义事务边界。过去,我们的服务在一个进程中,后面挂着一个数据库,事务可以选择强一致性事务,也就是 ACID。当服务增多,彼此配合,这个时候可以使用最终一致性事务,也就是 BASE。不同于 ACID,BASE 更加灵活,只要数据在最终能够保持一致就可以了。这个最终的时间范围,根据不同的业务场景不同,可能是分钟、小时、天,甚至是周或者月。
微服务架构愿景美好,属于重型武器,优点众多,缺点也很明显。服务增多,运维难度增大,错误调试难度增大。所以需要自动化构建、配置、测试和部署,需要日志收集、指标监控、调用链监控等工具,也就是需要 DevOps 实践。实现 DevOps 的三步工作法 中说明了实现 DevOps 文化的三个步骤。
除了上面提到的基础,还需要在早期确定服务之间如何集成和彼此调用方式,还需要确定数据体系,包括事务一致性和数据可靠性方法。随着服务增多,还需要配置管理、服务发现等众多组件。具体需要的基础组件可以参考 微服务的基建工作。
这些基础的服务和设计,最好在早期定义,否则,后期需要花费更多的资源才能够完善架构。如果前期缺失,后期也没有补足,造成的后果就是微服务架构迁移失败,最后的系统也只是披着微服务外衣的单体架构。
当我们确定开始使用微服务架构时,接下来的问题就是应该怎么做?是逐步进化更新系统、还是破釜沉舟重构整个系统。
第二种方式很诱人,比较符合大多数技术人的思维,系统不行,推倒重来,名为重构。但是在大多数情况下,这种方式不能被允许,因为市场变化迅速、竞争激烈,大多数公司不会停止业务,去等待重构一个能够运行、只是有些缺点的系统。所以,逐步替换更新系统才是王道,大多数公司也能接受。这种方式又被称为绞杀模式。
该如何逐步过渡到微服务架构?下面一步步进行展示:
第一步,将视图层与服务层部分逻辑进行分离。业务逻辑委托给服务层,支持页面展示的查询定向到数据库。这个阶段,我们不修改数据库本身。
第二步,用户视图层与数据库完全分离,依赖于服务层操作数据库。
第三步,将用户视图层与服务层拆分为不同服务,并在服务层创建一个 API 层,用于视图层与服务层之间通信。
第四步,拆分数据库,将不同业务数据拆分到不同的数据库中,同时对应业务服务层拆分到不同的服务。用户视图层通过 API 网关与不同业务服务层的 API 组件通信。这个时候需要注意,如果团队没有微服务开发经验,可以在这一步基础使用绞杀方式,先抽取简单业务域服务,因为业务简单,实现简单,可以练手,积累经验。
最后一步,拆分用户视图层。
绞杀模式的优势就在于,我们可以随着业务变化随时调整方案,不会造成整个业务进化过程的停摆。
If you cannot measure it, you cannot manage it!
引入微服务的目的首先是改善开发流程,我们可以通过简单的指标来衡量:
通过对比老架构和新架构的这些特性值,可以评估升级过程取得的效果。我们要时刻关注这些指标,只有一个个小阶段的胜利,才能组成最终完整的胜利。如果再某个阶段失败了,可能达不到我们最终的目的,或者埋下技术债务,后期不得不花更大的代价补偿。
想要说明微服务架构的好处,可以来一个比喻。我们建了一个空间站,为此,我们需要将人、货物和设备运输到空间站中,这个时候,运载火箭是比较好的选择,尽管运载火箭造价也比较高,但是几个月发射一次,也能够满足需求。随着空间站的扩大,火箭发射的间隔变短,运输成本高的离谱,而且越来越没法满足空间站运转需求。这个时候,可以尝试另外一种方式,比如,太空电梯。当然太空电梯的造价成本高于一次飞行的费用,但是只要建成,以后的成本就降低了很多。
这个比喻也是说明了微服务带来的美好期望,同时也说明一个问题,实施微服务架构会带来巨大的投资。所以,我们在建造太空电梯之前需要想好,我们真的需要这种投入,否则只能是一种浪费。
作为攻城狮,我们为能够解决或改善周围世界而自豪,着迷于提供解决方案。同时,我们也要意识到,我们付出的每一份努力,都要有回报。如果不能带来任何回报的重构升级,都是浪费时间。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:如何实现单体架构到微服务架构的蜕变?
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:如何实现单体架构到微服务架构的蜕变?
你好,我是看山。
前面我们聊了微服务的话题,现在微服务已经是业内通识。但凡系统开发、系统设计,必然采用微服务架构,或者宣称是微服务架构。
但大家有没有想过,微服务架构不是一开始就有的。如果追溯历史,微服务最早在 2005 的云计算博览会,由 Peter Rodgers 博士提出(那时候称为微 Web 服务(Micro-Web-Service))。到了 2014 年,Martin Fowler 与 James Lewis 共同提出微服务(Micron-Service)的概念,算是对概念归纳总结,天下一统。这一年也被称为微服务元年。
那就要问了,在 2014 年之前呢?大家用啥架构?再往前呢?上次互联网大潮的时候,大家又是用啥?我们今天来聊聊这段历史,可能你会对现在习以为常的架构,产生一些新的看法。在架构上,可以有更多的选择。
单体架构,人人都说这种架构不好,为什么不好呢?真的不好吗?可能真相并不是你认为的那样简单。
当前来说,如果有人说某个系统是单体架构,一定会有人投来怀疑的眼神,有的会带着些许不可思议,甚至带有一丝鄙夷。但是不得不说,单体架构(又称巨石系统,Monolithic)是整个软件发展过程中,出现时间最早、应用范围最广的一种架构风格。从另一个方面,原来本没有单体架构这个称呼,只是后来有了微服务架构,为了区分,才把所有“自包含”的系统称为单体架构。
上面这个图就是单体架构,所谓“自包含”,简单说就是自给自足,所有业务功能靠自己,不依赖其他业务系统。其优点有下面这些:
从这个角度,只要单机优势明显,就不该把单体架构视为地狱。
所谓“成也萧何败也萧何”,统一“集中”成就了单体架构,难以“隔离”也成为了单体架构最大的弊端。这里将隔离简单分为开发期隔离和运行期隔离。
单体架构省去了进程间通信、性能损失这些麻烦事,但因为在一个进程中执行,如果内部的某处逻辑异常,可能会造成整个系统的崩溃。最常见的内存溢出,可能仅仅是一个不相干的功能查了全表,整个系统都都会宕掉。
运行期没有办法隔离,升级的时候也没有办法隔离。想要对某些模块功能升级,只能重启整个服务。还要担心会不会有没有覆盖测试的点,提前做好预案,挂好停机维护页。
因此,一个成功的单体架构系统隐含了一个要求,需要一个对系统完全了解的大脑(一个人或一组人),大脑可以总控系统的开发、升级、运行,把控这个系统的每个细节,实现系统中的各个组件、模块有很高的品质,从而保障系统可在其生命周期内可以稳定的运行。
比如,SAP 和 Hyperion,妥妥的单体架构,作为国际化的软件公司,为什么不对它们升级改造?是能力不行,还是技术不行?是没有必要。所以,单体架构也不是一无是处,一切都要在合适的前提下评价。
都说 SOA 架构太重,但他是开创服务化江山的鼻祖。
单体架构对团队的要求较高,随着团队的扩大,必然会有短板或薄弱的环节,或者是组织、或者是个人,这样就会给系统代理风险。于是,很多前辈就开始思考,一个庞然大物难以维护,那就分为治之,拆分成多个规模小一些的单体架构,彼此之间通过某种方式交互。这种方案被称为面向服务架构(SOA,Service-Oriented Architecture)。
SOA 在 1994 年就被提出,这种架构风格是自然演化来的。只不过当时没有足够的条件支持,一直只能处于理论阶段。后来随着 webservice 等技术的提出,才有了技术支撑。到 2006 年,OSOA 成立,共同制定 SOA 架构相关行业标准,这套架构有了理论、技术、规范等一系列约定,从而真正落地。
SOA 架构开疆拓土,开创了很多目前也在使用的概念,比如服务注册/发现、服务治理、系统隔离、服务编排等。是不是觉得这些概念很熟悉,是的,在微服务架构中,同样有这些概念的身影。SOA 架构有自己的一套风格,使用下面一些组件实现普适的方法论:
SOA 架构是各大软件服务商共同愿景下的产物,总结出了一套自上而下的软件研发方法论,期望能够解决软件开发过程中的所有问题。有些类似于八股文,规定好起承转合,只要按照要求来,系统就不会出现太多问题。
愿景虽好,但是却忽略了一点,一套大而全的架构体系,不是所有公司都能够支撑起来的。有时候,大而全不如小而美。但是,我们不能否认 SOA 架构对于面向服务理论的贡献,在某些场景下的企业内部,SOA 是能够快速打破信息孤岛的重要手段。
本来是作为 SOA 的一种简化方案,结果直接发动宣武门之变,逼着 SAO 禅让。
如开篇所说,微服务架构是在 2005 年提出,在 2014 年崛起。经历了将近 10 年的时间,之所以没有得到太多重视,是因为 2014 年之前,微服务只是在作为 SOA 架构的简化版出现。直到 2014 年才作为独立的架构风格,与 SOA 架构划清界限。
Martin Fowler 与 James Lewis 在合写的 《Microservices》 对微服务下了定义:“微服务是一种通过多个小型服务组合来构建单个应用的架构风格,这些服务围绕业务能力而非特定的技术标准来构建。各个服务可以采用不同的编程语言,不同的数据存储技术,运行在不同的进程之中。服务采取轻量级的通信机制和自动化的部署机制实现通信与运维。”
文中还提出了微服务架构的 9 个核心特征:
由于微服务架构是从 SOA 架构中演化而来,所以很多的表现形式都是一致的。从《Microservices》对微服务架构全面细致阐述之后,也算是将微服务架构与 SOA 架构彻底划清界限。
在笔者看来,微服务架构与 SOA 架构最大的不同在于对于实现的约束,SOA 架构有一套完整的规约,微服务架构只有建议,追求的是根据实际情况自由变化,简单的理解就是“想怎么玩就怎么玩”。比如通信协议,SOA 架构明确要求使用 SOAP 通信协议;微服务架构只要求使用轻量级的 RPC 协议,这个选择就比较宽泛了,常见的就有 HTTP(一般采用 Restful 风格)、gRPC、Dubbo、Thrift、Motan2 等等。
自由意味着可以根据实际情况变化,需要什么引入什么,哪种技术能更好的解决问题就使用哪种技术。在 Java 栈中,也出现了 SpringCloud Netflix 和 SpringCloud Alibaba 之类的全家桶组件,作为开发者,只需要在需要的时候添加依赖即可。
从架构师的角度,自由带来的是约束力的下降,同时也缺少了规约的指导性。我们需要更加了解系统本身,也要更加了解各种技术的优缺点,才能够在架构设计时,更好的权衡利弊,做好取舍。加油,少年。
我们来看下微服务中的基础组件:弹性伸缩、服务发现、配置中心、服务网关、负载均衡、服务安全、跟踪监控、降级熔断等等,其实从本质来说,这些组件都是业务无关的。实现软件开发过程中,可以将这些与业务隔离开,也就是所谓的“透明化”。
比如服务发现,可选的方案包括 Nginx、HAProxy、DNS、Eureka、Nacos、KubeDNS,但是我们真的关心吗?不需要,只需要知道我们要进行网络调用,有一个目标即可,至于这个目标是通过哪种方式发现、传输、寻址,都与我们要实现的功能无关。那就将服务发现与业务剥离,通过承载服务的运行环境处理。这就是所谓的边车模型。
微服务之所以应用普及,不仅仅在于其独特优势,还与容器化技术的普及有密切关系。微服务与 Docker 虚拟化的高效结合,相当于给了微服务二次加速的动力,资源调度 Kubernetes 的成功,可以认为是直接实现了曲速推进。先进的理念还需要先进的技术实现。
又想要快,还想要简单。那就不要服务了,随便写个函数跑跑得了。
就目前而言,绝大部分的系统开发都是为了解决业务问题。在这个过程中,我们需要选择一些业务无关的技术组件。有时候,我们受限于研发环境,需要的某种技术组件不存在时,需要采购部署,或者使用替代方案。这就会分散我们的注意力。
于是,很多云服务商提出了无服务架构。无服务架构将系统开发涉及的资源分为两部分:后端设施(Backend)、函数(Function),对应的就是 BaaS(Backend as a Service,后端即服务)和 FaaS(Function as a Service,函数即服务)。
这种不能算是架构风格,只能算是一种系统开发过程中的美好愿景。让开发者只需要关注业务,需要的基础设施全部由云服务提供,不需要考虑运行容器、基础设施的部署、服务器运行能力等。只要将开发好的代码上传,就可以拥有一个可运行的系统。
但是愿景虽好,但是与自己掌控部署的区别仅在于对于基础设施的管控程度上。除非出现重大变革,否则这种架构很难像微服务架构一样普适。但是对于小程序、小型 web 网站、咨询平台等模板化的小型系统,采用这种架构还是有很大优势的。
本文从架构演进的角度分析了单体架构、SOA 架构、微服务架构、无服务架构的适用场景,作为架构师,我们在选择架构师,不应该一味追求主流,也不能盲从大厂的思路。我们要根据自身情况权衡利弊,找到适合的架构风格。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:除了微服务,我们还有其他选择吗?
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:除了微服务,我们还有其他选择吗?
该图片由Alexandr Podvalny在Pixabay上发布
你好,我是看山。
本文收录在 《Java 进阶》 系列专栏中。
从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中仍有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。
因此,准备出一个系列,配合示例讲解,阐述从 Java8 开始各个版本的新特性。
Java8 从 2014 年问世,到现在已是数个年头。这个版本新增了 Stream API、Lambda 表达式、新时间 API 等各种新特性,相比很多新兴语言也不遑多让。今天就来聊聊 Java8 中好玩好使的特性功能(完整特性请参见 这里)。
在 Java8 之前,接口只能够定义public abstract
方法,默认可以不写修饰符。当在接口中新增方法定义,该接口的所有实现类都需要新增这个方法的实现,这样对于升级扩展很不友好。
从 Java8 开始,我们可以在接口中定义静态方法和默认方法了,也就是我们可以在接口中定义具有具体操作行为的方法定义,这样接口的实现类可以有选择的实现接口方法。
Java8 之前,静态方法是类的专属技能,这样会引起概念上的一些歧义。比如,我们定义一个生产者Producer
接口,所有生产者都继承该接口,这个时候,我们需要一个静态方法提供Producer
的名字。这个时候,在单独定义一个类提供一个静态方法提供名字,可以实现功能,但是略显复杂。
现在我们直接在Producer
生产者接口中定义静态方法即可:
static String producer() { return "target: " + System.currentTimeMillis();}
沿用约定的限定范围,我们不需要在方法前面加public
。这个静态方法只能通过接口调用,或者在接口内部直接引用。比如:
final String target = Producer.producer();
接口的默认方法定义需要使用default
关键字,接口中定义的默认方法可以在实现类中重写。
比如,我们的生产者Producer
需要生产东西,我们可以在接口中定义一个默认方法:
default String produce() { return "NULL";}
我们可以定义Producer
的实现类是Hamburger
,可以选择重写接口的默认方法,也可以不用重写。比如:
public class Hamburger implements Producer {}
使用的时候直接调用:
final Producer producer = new Hamburger();System.out.println(producer.produce());
这个时候会打印“NULL”。我们还可以在Hamburger
中重写produce
方法:
@Overridepublic String produce() { return "HAMBURGER";}
这个时候会打印“HAMBURGER”。
我们在使用 Lambda 表达式时,可以使用方法引用,使表达式更短、更易读。方法引用有四种表达形式:
下面我们分别说一下。
静态方法引用语法是:类名:: 方法名
。假设我们需要判断一个List<String>
队列中所有元素是否为空,通过 Stream API 我们可以这样判断:
final List<String> list = Lists.newArrayList("1", "2", "3", null, "4");final boolean hasNullElement = list.stream() .anyMatch(x -> Objects.isNull(x));System.out.println(hasNullElement);
可以看到,anyMath
方法中只调用了Objects.isNull
方法,而且方法的入参直接是列表中的元素,此时,我们可以直接使用静态方法引用,将代码改写一下:
final boolean hasNullElementAlso = list.stream().anyMatch(Objects::isNull);
这样看起来清爽多了。
实例方法引用语法是:实例:: 方法名
。比如,我们有一个列表中全是LocalDate
类型数据,现在需要对其进行格式化,返回一个字符串列表。我们可以这样使用:
final DateTimeFormatter fmt = DateTimeFormatter.ISO_LOCAL_DATE;final List<LocalDate> dates = Lists.newArrayList( LocalDate.MIN, LocalDate.now(), LocalDate.MAX);final List<String> dateStrs = dates.stream() .map(d -> fmt.format(d)) .collect(Collectors.toList());
map
方法中通过DateTimeFormatter
的实例对象调用了format
方法,入参也是 Lambda 表达式中的元素,这样就可以使用实例方法引用,代码可以改写为:
final List<String> dateStrList = dates.stream() .map(fmt::format) .collect(Collectors.toList());
这样写起来顺手多了。
这种方法引用有一个前提条件,就是必须是 Lambda 表达式元素类型对应的方法。语法是:特定类型:: 方法名
。比如,我们需要判断一个全都不为null
的字符串列表中,空字符的数量,我们可以这样写:
final List<String> nonNullList = Lists.newArrayList("1", "2", "3", "", "4", "");final long emptyCount = nonNullList.stream() .filter(x -> x.isEmpty()) .count();
我们可以看到,filter
方法中引用的函数是利用 Lambda 表达式元素对象的方法,这个时候我们可以将代码改写为:
final long emptyElementCount = nonNullList.stream() .filter(String::isEmpty) .count();
这样能够清晰的看出是哪个类的方法了。
构造方法引用的语法是:类名::new
。在 Java 中,构造方法是一种特殊的方法,所以构造方法的引用与上面几种方法类似。比如,想要将字符串列表中的元素全部转换为Integer
格式:
final List<String> allIntList = Lists.newArrayList("1", "2", "3", "4");final List<Integer> ints = allIntList.stream() .map(x -> new Integer(x)) .collect(Collectors.toList());
我们可以改写为:
final List<Integer> intList = allIntList.stream() .map(Integer::new) .collect(Collectors.toList());
空指针异常(NullPointException,NPE)是特别低级但又很难避免的异常,说他低级是因为只要看到这个异常,就能够很容易的修复,但是我们很难百分之百的避免这个异常的存在。在 Java8 之前,我们只能通过类似obj != null
这种模板式方法判断。在 Java8 新增的神器Optional
可以更加优雅的解决这个问题。
Optional
的构造方法是使用private
修饰的,其提供了三个静态方法,用于创建Optional
实例,分别是empty
、of
、ofNullable
,创建之后,Optional
是不可变的。
我们可以使用empty
定义一个具有空值的Optional
对象:
final Optional<String> optional = Optional.empty();
使用of
定义一个不为空的对象:
final String str = "value";final Optional<String> optional = Optional.of(str);
这里需要注意一下,of
方法赋值时,使用Objects.requireNonNull
验证参数是否为空,为空就会抛出NullPointerException
异常。
如果不太确定是否为空,可以使用ofNullable
创建对象:
final String str = getSomeStr();final Optional<String> optional = Optional.ofNullable(str);
比如,我们需要返回一个字符串列表List<String>
,当结果是null
的时候,我们返回返回new ArrayList<>()
。如果是在 Java8 之前,我们得这样写:
List<String> list = getList();List<String> listOpt = list != null ? list : new ArrayList<>();
现在,我们可以借助Optional
的能力:
List<String> listOpt = Optional.ofNullable(getList()) .orElse(new ArrayList<>());
小试牛刀,还不错,下面放大招。
假设,我们有一个User
类,内部有个Address
类,在内部有个street
属性,我们现在想要获取一个User
对象的street
值。如果是以前,我们需要各种判断是否是null
,代码会写成这样:
User user = getUser();if (user != null) { Address address = user.getAddress(); if (address != null) { String street = address.getStreet(); if (street != null) { return street; } }}return "not specified";
是不是似曾相识,或者以前亲手写过。现在有了Optional
,我们就不需要这么麻烦了:
String result = Optional.ofNullable(getUser()) .map(User::getAddress) .map(Address::getStreet) .orElse("not specified");
是不是相当的优雅,map
方法返回的也是Optional
对象,所以我们可以无限处理下去。
如果User
类中的getAddress
方法返回的本身就是Optional
对象,我们可以使用flatMap
替换map
。
还有一种情况是我们需要捕捉 NPE 的情况,但是需要包装为其他自定义异常,这个时候可以使用orElseThrow
方法:
String value = null;Optional<String> valueOpt = Optional.ofNullable(value);String result = valueOpt.orElseThrow(CustomException::new).toUpperCase();
这里只是简单给出几个例子,更多功能可以参见 《一文掌握 Java8 的 Optional 的 6 种操作》。
本文给出了 Java8 中几个比较有意思的特性,完整的特性清单可以从https://openjdk.java.net/projects/Java8/features查看。
本文所有代码都可以通过在公众号「看山的小屋」回复“java”获取。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java8 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java8 的新特性
该图片由daschorsch在Pixabay上发布
你好,我是看山。
本文收录在《一个架构师的职业素养》专栏,日拱一卒,功不唐捐。
策略模式,英文全称是 Strategy Design Pattern。在 GoF 的《设计模式》一书中,它是这样定义的:
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
翻译成中文就是:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端。
这里所说的客户端代指使用算法的代码。
根据使用场景分类,策略模式是一种行为型模式,用于运行时控制类的行为或算法。
使用上的直观感受是,策略模式可以减少了 if-else/switch 分支代码。那减少分支代码有什么好处呢?
有编程经验的都知道,很多 bug 都是从分支逻辑产生的。我刚开始工作时晚上 12 点开始抓虫,一直抓到凌晨 2 点多,最后发现是有一个 if-else 分支中,在某个 if 前面少写了一个 else。下面是示例,实际代码比这个复杂很多:
if (a < 1) {} else if (a < 2) {} else if (a < 3) {}
结果写成了:
if (a < 1) {} else if (a < 2) {} if (a < 3) {}
代码编译不会错,但是在执行时,某些 case 会不符合预期。
我们来看看策略模式出现的场景。
以电商系统的支付功能为例,最早的时候,我们可能为了更快上线,选择一个较多人使用的支付方式,比如微信支付(也有可能是支付宝支付,根据售卖场景不同区分)。这个时候,我们只需要判断用户是从 PC 页面进入还是 H5 进入即可。
后来,业务发展比较好,涉及人群更多了,于是需要对接支付宝支付。支付宝支付也分为了多种的支付场景,对接接口变多了,但是也在可控范围内。
再后来,我们需要对接银联支付、对接各银行接口,等等,支付接口变得越来越臃肿。于是,每对接一种支付方式,支付相关接口就会增加一倍。此时,这坨臃肿的代码,无论是修复简单的 bug,还是微调传输参数,都会影响整个支付逻辑,从而增加了在已有正常运行代码中引入错误的风险。
如果是多人协作开发,我们还会陷入代码合并时应付各种冲突的情况。终于,在某一时刻,我们看着这一坨代码,已经无从下手维护了。
首先,我们来分析一下上面的场景,不变的是系统内部的支付业务逻辑,变化的是支付方式。
支付方式的可变性在于,可能会与多种支付方式对接,对接参数、协议、地址等都会不同。根据设计模式的整体思想,我们将变化的单独出去,将不变的稳定下来。
这种处理方式就是策略模式建议的:找出负责用许多不同方式完成特定任务的类,然后将其中的算法抽取到一族被称为策略的独立类中。
调用这些策略类的是调用上下文,它持有对所有策略类的引用。上下文不执行任务,它是任务的指挥者,将工作委派给已连接的策略对象。关系如下:
很多教程到这里就结束了,如果你能够看到这里,而且还用心看了,你就会发现一丝丝的不一样。
根据迪米特法则(LOD,Law of Demeter),上下文不需要知道具体策略类的功能,只需要通过特定的接口,用于触发选中策略即可。也就是说,完整的策略模式,应该有具体的策略判断是否由该策略执行,上下文只需要知道有哪些策略就行了。这样改动之后,上下文还能够与工厂模式结合。如果策略是无状态策略,还可以在上下文中引入单例模式。
根据上面的定义,策略模式是围绕可以互换的算法来创建业务的。简单的说就是,分支逻辑隔离。
设计模式只是解决问题的优雅实现,并不一定适用所有情况,比如下面这几种,就可以不用非得实现策略模式:
还是以支付为例,因为都是演示,一切从简。我曾经主导过支付中台,如果想要具体实现,可以具体聊一下。
首先定义支付策略接口:
public interface PayStrategy { String payType(); void callPay(BigDecimal amount);}
payType()
是在具体的策略实现中定义策略可执行的支付方式,也可以通过传参数的方式返回boolean
类型用于判断是否可执行。
然后是微信支付和支付宝支付分别实现支付策略接口:
public class WxpayPayStrategy implements PayStrategy { @Override public String payType() { return "WXPAY"; } @Override public void callPay(BigDecimal amount) { // 微信支付接口 // 这里只是演示,即使都是微信支付,也会分不同的接口 System.out.println("调用微信支付接口"); }}public class AlipayPayStrategy implements PayStrategy { @Override public String payType() { return "ALIPAY"; } @Override public void callPay(BigDecimal amount) { // 调用支付宝支付接口 // 这里只是演示,即使都是支付宝支付,也会分不同的接口 System.out.println("调用支付宝支付接口"); }}
我们再来看看持有策略算法的上下文:
public class StrategyContext { private static final Map<String, PayStrategy> PAY_STRATEGY_MAP = new HashMap<>(); static { final AlipayPayStrategy alipayPayStrategy = new AlipayPayStrategy(); final WxpayPayStrategy wxpayPayStrategy = new WxpayPayStrategy(); PAY_STRATEGY_MAP.put(alipayPayStrategy.payType(), alipayPayStrategy); PAY_STRATEGY_MAP.put(wxpayPayStrategy.payType(), wxpayPayStrategy); } public void pay(String payType, BigDecimal amount) { final PayStrategy payStrategy = PAY_STRATEGY_MAP.get(payType); payStrategy.callPay(amount); }}
可以看到,上下文只需要知道策略算法的存在,至于算法是否符合要求,由算法自己判断。
调用就比较简单了:
public class Main { public static void main(String[] args) { final StrategyContext strategyContext = new StrategyContext(); strategyContext.pay("ALIPAY", BigDecimal.TEN); strategyContext.pay("WXPAY", BigDecimal.ONE); }}
策略模式可能用来减少分支逻辑,将不同的算法分离开来。如果配合工厂模式、单例模式,可以更加灵活的使用。如果是在 Spring 当中,借助自动注入,上下文甚至可以不知道具体策略实现。
最近刚看到一句话,“日拱一卒,功不唐捐”。坚持下去,每天学点新东西,给生活加点色彩。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:人人都会设计模式:策略模式
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:人人都会设计模式:策略模式
该图片由Michael Kleinsasser在Pixabay上发布
你好,我是看山。
本文被《Java 进阶》专栏收录,在公众号「看山的小屋」,回复“java”可获取源码。
我们在系统开发过程中,对数据排序是很常见的场景。一般来说,我们可以采用两种方式:
今天要说的是第二种排序方式,在内存中实现数据排序。
首先,我们定义一个基础类,后面我们将根据这个基础类演示如何在内存中排序。
@Data@NoArgsConstructor@AllArgsConstructorpublic class Student { private String name; private int age; @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name, age); }}
Comparator
排序在 Java8 之前,我们都是通过实现Comparator
接口完成排序,比如:
new Comparator<Student>() { @Override public int compare(Student h1, Student h2) { return h1.getName().compareTo(h2.getName()); }};
这里展示的是匿名内部类的定义,如果是通用的对比逻辑,可以直接定义一个实现类。使用起来也比较简单,如下就是应用:
@Testvoid baseSortedOrigin() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); Collections.sort(students, new Comparator<Student>() { @Override public int compare(Student h1, Student h2) { return h1.getName().compareTo(h2.getName()); } }); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
这里使用了 Junit5 实现单元测试,用来验证逻辑非常适合。
因为定义的Comparator
是使用name
字段排序,在 Java 中,String
类型的排序是通过单字符的 ASCII 码顺序判断的,J
排在T
的前面,所以Jerry
排在第一个。
Comparator
匿名内部类使用过 Java8 的 Lamdba 的应该知道,匿名内部类可以简化为 Lambda 表达式为:
Collections.sort(students, (Student h1, Student h2) -> h1.getName().compareTo(h2.getName()));
在 Java8 中,List
类中增加了sort
方法,所以Collections.sort
可以直接替换为:
students.sort((Student h1, Student h2) -> h1.getName().compareTo(h2.getName()));
根据 Java8 中 Lambda 的类型推断,我们可以将指定的Student
类型简写:
students.sort((h1, h2) -> h1.getName().compareTo(h2.getName()));
至此,我们整段排序逻辑可以简化为:
@Testvoid baseSortedLambdaWithInferring() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); students.sort((h1, h2) -> h1.getName().compareTo(h2.getName())); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
我们可以在Student
中定义一个静态方法:
public static int compareByNameThenAge(Student s1, Student s2) { if (s1.name.equals(s2.name)) { return Integer.compare(s1.age, s2.age); } else { return s1.name.compareTo(s2.name); }}
这个方法需要返回一个int
类型参数,在 Java8 中,我们可以在 Lambda 中使用该方法:
@Testvoid sortedUsingStaticMethod() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); students.sort(Student::compareByNameThenAge); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
Comparator
的comparing
方法在 Java8 中,Comparator
类新增了comparing
方法,可以将传递的Function
参数作为比较元素,比如:
@Testvoid sortedUsingComparator() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); students.sort(Comparator.comparing(Student::getName)); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
我们在静态方法一节中展示了多条件排序,还可以在Comparator
匿名内部类中实现多条件逻辑:
@Testvoid sortedMultiCondition() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12), new Student("Jerry", 13) ); students.sort((s1, s2) -> { if (s1.getName().equals(s2.getName())) { return Integer.compare(s1.getAge(), s2.getAge()); } else { return s1.getName().compareTo(s2.getName()); } }); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
从逻辑来看,多条件排序就是先判断第一级条件,如果相等,再判断第二级条件,依次类推。在 Java8 中可以使用comparing
和一系列thenComparing
表示多级条件判断,上面的逻辑可以简化为:
@Testvoid sortedMultiConditionUsingComparator() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12), new Student("Jerry", 13) ); students.sort(Comparator.comparing(Student::getName).thenComparing(Student::getAge)); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
这里的thenComparing
方法是可以有多个的,用于表示多级条件判断,这也是函数式编程的方便之处。
Stream
中进行排序Java8 中,不但引入了 Lambda 表达式,还引入了一个全新的流式 API:Stream API,其中也有sorted
方法用于流式计算时排序元素,可以传入Comparator
实现排序逻辑:
@Testvoid streamSorted() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); final Comparator<Student> comparator = (h1, h2) -> h1.getName().compareTo(h2.getName()); final List<Student> sortedStudents = students.stream() .sorted(comparator) .collect(Collectors.toList()); Assertions.assertEquals(sortedStudents.get(0), new Student("Jerry", 12));}
同样的,我们可以通过 Lambda 简化书写:
@Testvoid streamSortedUsingComparator() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); final Comparator<Student> comparator = Comparator.comparing(Student::getName); final List<Student> sortedStudents = students.stream() .sorted(comparator) .collect(Collectors.toList()); Assertions.assertEquals(sortedStudents.get(0), new Student("Jerry", 12));}
排序就是根据compareTo
方法返回的值判断顺序,如果想要倒序排列,只要将返回值取返即可:
@Testvoid sortedReverseUsingComparator2() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); final Comparator<Student> comparator = (h1, h2) -> h2.getName().compareTo(h1.getName()); students.sort(comparator); Assertions.assertEquals(students.get(0), new Student("Tom", 10));}
可以看到,正序排列的时候,我们是h1.getName().compareTo(h2.getName())
,这里我们直接倒转过来,使用的是h2.getName().compareTo(h1.getName())
,也就达到了取反的效果。在 Java 的Collections
中定义了一个java.util.Collections.ReverseComparator
内部私有类,就是通过这种方式实现元素反转。
Comparator
的reversed
方法倒序在 Java8 中新增了reversed
方法实现倒序排列,用起来也是很简单:
@Testvoid sortedReverseUsingComparator() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); final Comparator<Student> comparator = (h1, h2) -> h1.getName().compareTo(h2.getName()); students.sort(comparator.reversed()); Assertions.assertEquals(students.get(0), new Student("Tom", 10));}
Comparator.comparing
中定义排序反转comparing
方法还有一个重载方法,java.util.Comparator#comparing(java.util.function.Function<? super T,? extends U>, java.util.Comparator<? super U>)
,第二个参数就可以传入Comparator.reverseOrder()
,可以实现倒序:
@Testvoid sortedUsingComparatorReverse() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); students.sort(Comparator.comparing(Student::getName, Comparator.reverseOrder())); Assertions.assertEquals(students.get(0), new Student("Jerry", 12));}
Stream
中定义排序反转在Stream
中的操作与直接列表排序类似,可以反转Comparator
定义,也可以使用Comparator.reverseOrder()
反转。实现如下:
@Testvoid streamReverseSorted() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); final Comparator<Student> comparator = (h1, h2) -> h2.getName().compareTo(h1.getName()); final List<Student> sortedStudents = students.stream() .sorted(comparator) .collect(Collectors.toList()); Assertions.assertEquals(sortedStudents.get(0), new Student("Tom", 10));}@Testvoid streamReverseSortedUsingComparator() { final List<Student> students = Lists.newArrayList( new Student("Tom", 10), new Student("Jerry", 12) ); final List<Student> sortedStudents = students.stream() .sorted(Comparator.comparing(Student::getName, Comparator.reverseOrder())) .collect(Collectors.toList()); Assertions.assertEquals(sortedStudents.get(0), new Student("Tom", 10));}
前面的例子中都是有值元素排序,能够覆盖大部分场景,但有时候我们还是会碰到元素中存在null
的情况:
如果还是使用前面的那些实现,我们会碰到NullPointException
异常,即 NPE,简单演示一下:
@Testvoid sortedNullGotNPE() { final List<Student> students = Lists.newArrayList( null, new Student("Snoopy", 12), null ); Assertions.assertThrows(NullPointerException.class, () -> students.sort(Comparator.comparing(Student::getName)));}
所以,我们需要考虑这些场景。
最先想到的就是判空:
@Testvoid sortedNullNoNPE() { final List<Student> students = Lists.newArrayList( null, new Student("Snoopy", 12), null ); students.sort((s1, s2) -> { if (s1 == null) { return s2 == null ? 0 : 1; } else if (s2 == null) { return -1; } return s1.getName().compareTo(s2.getName()); }); Assertions.assertNotNull(students.get(0)); Assertions.assertNull(students.get(1)); Assertions.assertNull(students.get(2));}
我们可以将判空的逻辑抽取出一个Comparator
,通过组合方式实现:
class NullComparator<T> implements Comparator<T> { private final Comparator<T> real; NullComparator(Comparator<? super T> real) { this.real = (Comparator<T>) real; } @Override public int compare(T a, T b) { if (a == null) { return (b == null) ? 0 : 1; } else if (b == null) { return -1; } else { return (real == null) ? 0 : real.compare(a, b); } }}
在 Java8 中已经为我们准备了这个实现。
Comparator.nullsLast
和Comparator.nullsFirst
使用Comparator.nullsLast
实现null
在结尾:
@Testvoid sortedNullLast() { final List<Student> students = Lists.newArrayList( null, new Student("Snoopy", 12), null ); students.sort(Comparator.nullsLast(Comparator.comparing(Student::getName))); Assertions.assertNotNull(students.get(0)); Assertions.assertNull(students.get(1)); Assertions.assertNull(students.get(2));}
使用Comparator.nullsFirst
实现null
在开头:
@Testvoid sortedNullFirst() { final List<Student> students = Lists.newArrayList( null, new Student("Snoopy", 12), null ); students.sort(Comparator.nullsFirst(Comparator.comparing(Student::getName))); Assertions.assertNull(students.get(0)); Assertions.assertNull(students.get(1)); Assertions.assertNotNull(students.get(2));}
是不是很简单,接下来我们看下如何实现排序条件的字段是 null 的逻辑。
这个就是借助Comparator
的组合了,就像是套娃实现了,需要使用两次Comparator.nullsLast
,这里列出实现:
@Testvoid sortedNullFieldLast() { final List<Student> students = Lists.newArrayList( new Student(null, 10), new Student("Snoopy", 12), null ); final Comparator<Student> nullsLast = Comparator.nullsLast( Comparator.nullsLast( // 1 Comparator.comparing( Student::getName, Comparator.nullsLast( // 2 Comparator.naturalOrder() // 3 ) ) ) ); students.sort(nullsLast); Assertions.assertEquals(students.get(0), new Student("Snoopy", 12)); Assertions.assertEquals(students.get(1), new Student(null, 10)); Assertions.assertNull(students.get(2));}
代码逻辑如下:
Comparator
,这里使用了Comparator.naturalOrder()
,是因为使用了String
排序,也可以写为String::compareTo
。如果是复杂判断,可以定义一个更加复杂的Comparator
,组合模式就是这么好用,一层不够再套一层。本文演示了使用 Java8 中使用 Lambda 表达式实现各种排序逻辑,新增的语法糖真香。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:Java 进阶:使用 Lambda 表达式实现超强的排序功能
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 进阶:使用 Lambda 表达式实现超强的排序功能
该图片由Roshan Bhatia在Pixabay上发布
你好,我是看山。
工具的发明能够节省体力,同时也可以减少重复劳动,软件也是工具的一种。今天要说的是,引用 IT 技术,减少大量文件重命名这种重复的劳动。
一直在用的存储云盘是百度网盘,里面收集了大量文件。各种资料、电子书,使用空间达到了 2500G。之前还清理过一些低质的书籍,结果使用工具导出发现,在待整理目录中,居然有 1942 条电子书的记录。如果有小伙伴想要什么书,可以从公号留言,只要不是商用,无私共享。
书归正传,这么多的文件,命名格式千奇百怪,因为有一些资料是从别人的分享中保存的,有的还会带网址的,可见网站运营也是无所不用其极了。从网上找到一些批量改名的工具,大多是 Windows 版本的,而且是加前缀或者后缀之类的,不太适用。
我想要的是,自己指定文件名,然后批量执行。就相当于有一双手,帮我在百度网盘中执行官方提供的重命名。之所以不用官方的重命名,是因为太难用,而且浪费时间(后面会具体说一下百度网盘的这个设计,也是可以借鉴的)。
想要实现自己的想法,需要有两步:
根据需求,我们来设计方案。
首先,我们需要能够导出所有的文件名。
百度网盘提供了网页版、客户端版,为了省时省事,我们使用网页版检查逻辑。打开控制台,发现进入目录时会有一个/api/list
的请求,如下图:
根据响应内容,我们可以看出来,这个接口可以获取指定目录的文件列表。这个请求是 Get 请求,包含了好几个参数,还不太请求参数的作用,先放过。
通常来说,简单的网络请求是通过 Cookie 鉴权,所以我们就无脑使用 Cookie 了。
接下来需要找到重命名的请求,同样的,执行百度网盘提供的重命名即可,新增了哪些请求。如下图:
可以看到,这里的重命名分为了两步:
这就是前面说的可借鉴的地方。对于百度网盘这种应用,虽然下载限速被各种诟病,还有阿里云盘的强势追击,但是不得不说,百度网盘还是现在用的比较多的云存储工具。必须有针对性的优化,将某些二级功能异步任务化,比如重命名。
我们可以借助阿里开源的 EasyExcel 导出 Excel 文件(具体操作,可以查看 写文件、写的好看、填充文件 三篇)。
这个时候需要定义导出文件的内容,根据重命名的请求我们可以知道,我们需要文件路径、文件的新名字,为了操作简单,我们可以直接把原名也导出来。为了检查网盘文件是否有重复的,最好把文件的摘要码也导出来。
至此,我们的需求和方案都设计好了,下面就开始编码。
开始编码前,我们需要定义一下鉴权参数:cookie、bdstoken,再定义一个扩展参数 path,我们只导出指定目录的文件列表。
根据设计方案中的定义,我们先创建导出文件的基础类:
@Datapublic class FileName { @ExcelProperty("路径") private String path; @ExcelProperty("MD5") private String md5; @ExcelProperty("原名称") private String originName; @ExcelProperty("新名称") private String newName;}
因为涉及到网络请求,我们需要定义请求参数。请求有一些共同参数:
@Datapublic abstract class BaseRequest { protected String channel = "chunlei"; protected String web = "1"; protected String appId = "250528"; protected String bdstoken = ""; protected String logid = ""; protected String clienttype = "0";}
文件列表参数为:
@EqualsAndHashCode(callSuper = true)@Datapublic class FileListRequest extends BaseRequest { private String order = "name"; private String desc = "0"; private String showempty = "0"; private int page = 1; private int num = 100; private String dir = "/"; private String t = "";}
重命名参数为:
@EqualsAndHashCode(callSuper = true)@Datapublic class FileRenameRequest extends BaseRequest { private String opera = "rename"; private String async = "2"; private String onnest = "fail";}
我们还需要一个查询任务状态的参数:
@EqualsAndHashCode(callSuper = true)@Datapublic class TaskStatusRequest extends BaseRequest { private Long taskid;}
前面有了基础类和请求类,接下来我们定义请求接口,这些类就是模板化的方法了,我们简单看一下。如果想要获取源码,关注公号「看山的小屋」回复“java”获取源码。
先定义文件列表请求方法:
private List<FileListItem> listFileCurrentPath(FileListRequest fileListRequest) { final String body = HttpRequest.get("https://pan.baidu.com/api/list") .form(fileListRequest.paramMap()) .header(this.headers) .cookie(this.cookie) .execute() .body(); final FileListResponse response = JSONUtil.toBean(body, FileListResponse.class); if (response.getErrno() == 0) { return response.getList(); } return Collections.emptyList();}
在定义文件重命名请求方法:
private Long rename(FileRenameRequest fileRenameRequest, String params) { final String queryParam = HttpUtil.toParams(fileRenameRequest.paramMap()); final HttpRequest httpRequest = HttpRequest.post("https://pan.baidu.com/api/filemanager?" + queryParam) .header(this.headers) .cookie(this.cookie) .body(params); final String body = httpRequest.execute().body(); final FileRenameResponse response = JSONUtil.toBean(body, FileRenameResponse.class); if (response.getErrno() == 0) { return response.getTaskid(); } return -1L;}
最后定义检查任务状态请求方法:
private TaskStatusResponse queryTaskStatus(TaskStatusRequest taskStatusRequest, String params) { TaskStatusResponse response; final String queryParam = HttpUtil.toParams(taskStatusRequest.paramMap()); do { final String body = HttpRequest.post("https://pan.baidu.com/share/taskquery?" + queryParam) .header(this.headers) .cookie(this.cookie) .body(params) .execute() .body(); response = JSONUtil.toBean(body, TaskStatusResponse.class); } while (response.getErrno() != 0 || StringUtils.equalsAny(response.getStatus(), "running", "pending")); return response;}
全部类定义完成后,我们可以直接在 IDE 中运行。但是,既然是工具,每次使用还得打开 IDE,是不是有些 low 了。为了升级体验,我们可以打成 jar 包,使用的时候直接运行 jar 包就行了。可以借助 maven 插件maven-assembly-plugin
实现,这个插件能够把我们的源码和三方库都打在一个 jar 包中,这样就是一个 FatJar 走天下了。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.3</version> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>cn.howardliu.effectjava.rename.TaskRunner</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>assembly</goal> </goals> </execution> </executions></plugin>
干完,手工。
本文从零开始实现制作一个网络小工具,实现百度网盘文件的批量重命名。这个工具是这类工具的一个代表,只要是网络应用,存在 http 请求,我们都可以通过这类方式实现网络小工具。
如果想要获取源码,关注公号「看山的小屋」回复“java”获取源码。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:看山聊 Java:从零实现“百度网盘批量重命名”工具
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:看山聊 Java:从零实现“百度网盘批量重命名”工具