Java9新特性
接口里面可以声明私有方法了
在JDK9中新增了接口私有方法,我们可以在接口中声明private修饰的方法了。
1 | public interface Test { |
改进try with resource
Java7中新增了try with resource语法用来自动关闭资源文件,在IO流和JDBC部分使用的比较多。
使用方式是将需要自动关闭的资源对象的创建放到try后面的小括号中,在JDK9中我们可以将这些资源对象的创建代码放到小括号外面,然后将需要关闭的对象名放到try后面的小括号中即可,示例:
1 | /* |
不能使用下划线命名变量 __
下面语句在JDK9之前可以正常编译通过,但是在JDK9(含)之后编译报错,在后面的版本中会将下划线作为关键字来使用。
1 | String _ = "Java"; |
String字符串的变化
写程序的时候会经常用到String字符串,在以前的版本中String内部使用了char数组存储,对于使用英语的人来说,一个字符用一个字节就能存储,使用char存储字符会浪费一半的内存空间,因此在JDK9中将String内部的char数组改成了byte数组,这样就节省了一半的内存占用。
String中增加了下面2个成员变量
- COMPACT_STRINGS:判断是否压缩,默认是true,若为false,则不压缩,使用UTF16编码
- coder用来区分使用的字符编码,分别为LATIN1(值为0)和UTF16(值为1)
byte数组如何存储中文呢?通过源码(StringUTF16类中的toBytes方法)得知,在使用中文字符串时,1个中文会被存储到byte数组中的两个元素上,即存储1个中文,byte数组长度为2,存储2个中文,byte数组长度为4。
以如下代码为例进行分析:
1 | String str = "好" |
好对应的Unicode码二进制为0101100101111101,**分别取出高8位和低8位,放入到byte数组中{01011001,01111101}**,这样就利用byte数组的2个元素保存了1个中文。
当字符串中存储了中英混合的内容时,1个英文字符会占用2个byte数组位置。
在获取字符串长度时,若存储的内容存在中文,是不能直接获取byte数组的长度作为字符串长度的,String源码中有向右移动1位的操作(即除以2),这样才能获取正确的字符串长度。
@Deprecated注解的变化
该注解用于标识废弃的内容,在JDK9中新增了2个内容:
- String since() default “”:标识是从哪个版本开始废弃
- boolean forRemoval() default false:标识该废弃的内容会在未来的某个版本中移除
jshell
在一些编程语言中,例如:python,Ruby等,都提供了REPL(Read Eval Print Loop 简单的交互式编程环境)。jshell就是Java语言平台中的REPL。
有的时候我们只是想写一段简单的代码,例如HelloWorld,按照以前的方式,还需要自己创建Java文件,创建class,编写main方法,但实际上里面的代码其实就是一个打印语句,此时还是比较麻烦的。在JDK9中新增了jshell工具,可以帮助我们快速的运行一些简单的代码。
从命令提示符里面输入jshell,进入到jshell之后输入:
1 | System.out.println("HelloWorld"); |
如果要退出jshell的话,输入/exit即可。
Java10新特性
局部变量类型推断 var
在JDK10以前声明变量的时候,我们会像下面这样:
1 | String oldName = "jack"; |
上面我们声明的时候使用了4种不同类型的变量,在JDK10中前面的类型都可以使用var来代替,JVM会自动推断该变量是什么类型的,例如可以这样写:
1 | var newName = "jack"; |
注意:
当然这个var的使用是有限制的,仅适用于局部变量,增强for循环的索引,以及普通for循环的本地变量;它不能使用于方法形参,构造方法形参,方法返回类型等。
除了上面的新特性之外,还对JVM进行了一些优化,这里就不罗列了。
Java11新特性
直接运行 java
在以前的版本中,我们在命令提示下,需要先编译,生成class文件之后再运行,例如:
1 | javac HelloWorld.java |
在java 11中,我们可以这样直接运行
1 | java HelloWorld.java |
去掉了 javac 的环节,可以直接使用java来运行代码,而且如果你的代码有其他对象的引用,也是没有任何问题的。
可以帮助我们做一个级联的无感知编译。
lambda表达式中的变量类型推断
JDK11中允许在lambda表达式的参数中使用var修饰
函数式接口:
1 |
|
测试类:
1 | // 支持lambda表达式参数中使用var |
Java12新特性
升级的switch语句
在JDK12之前的switch语句中,如果没有写break,则会出现case穿透现象,下面是对case穿透的一个应用,根据输入的月份打印相应的季节。
1 | int month = 3; |
在JDK12之后我们可以省略全部的break和部分case,这样使用
1 | int month = 3; |
这个是预览功能,如果需要编译和运行的话需要使用下面命令,预览功能在2个版本之后会成为正式版,即如果你使用的是JDK14以上的版本,正常的编译和运行即可。否则需要使用预览功能来编译和运行
1 | 编译: |
Java13新特性
升级的switch语句
JDK13中对switch语句又进行了升级,可以switch的获取返回值
示例:
1 | int month = 3; |
对于JDK15之后的版本可以直接编译和运行,否则需要使用下面命令执行该预览功能
1 | 编译: |
文本块的变化 Python的插值表达式 fstring
在JDK13之前的版本中如果输入的字符串中有换行的话,需要添加换行符
1 | String s = "Hello\nWorld\nLearn\nJava"; |
JDK13之后可以直接这样写:
1 | String s = """ |
这样的字符串更加一目了然。
Java14新特性
java 14 新增了很多特性,我们针对较为突出的特性进行说明。JDK12和JDK13中预览版的switch特性,在JDK14中已经是正式的语法了。
instanceof模式匹配
该特性可以减少强制类型转换的操作,简化了代码,代码示例:
1 | public class TestInstanceof{ |
这个是预览版的功能所以需要使用下面命令编译和运行
1 | 编译: |
友好的空指针(NullPointerException)提示
JDK14中添加了对于空指针异常友好的提示,便于开发者快速定位空指针的对象。示例代码:
1 | class Machine{ |
我们在运行上面代码的时候,错误信息就可以明确的指出那个对象为null了。此外,还可以使用下面参数来查看:
1 | java -XX:+ShowCodeDetailsInExceptionMessages TestNull |
这样编译器会明确的告诉开发者哪个对象是null。
record类型
之前在编写javabean类的时候,需要编写成员变量,get方法,构造方法,toString方法,hashcode方法,equals方法。这些方法通常会通过开发工具来生成,在JDK14中新增了record类型,通过该类型可以省去这些代码的编写。
JDK14编写User
1 | public record User(String name,Integer age){} |
通过反编译命令可以看到该字节码文件中的内容,User类是继承了Record类型:
1 | javap -p -private user |
编写测试类:
1 | public class TestUser{ |
这个是预览版的功能所以需要使用下面命令编译和运行
1 | 编译: |
记录类型有自动生成的成员,包括:
- 状态描述中的每个组件都有对应的private final字段。
- 状态描述中的每个组件都有对应的public访问方法。方法的名称与组件名称相同。
- 一个包含全部组件的公开构造器,用来初始化对应组件。
- 实现了equals()和hashCode()方法。equals()要求全部组件都必须相等。
- 实现了toString(),输出全部组件的信息。
Java15新特性
java 15中更新了一些新的内容,这里仅列出对于写代码方面的新特性。
Sealed Classes
密封类和接口,作用是限制一个类可以由哪些子类继承或者实现。
- 如果指定模块的话,sealed class和其子类必须在同一个模块下。如果没有指定模块,则需要在同一个包下。
- sealed class指定的子类必须直接继承该sealed class。
- sealed class的子类要用final修饰。
- sealed class的子类如果不想用final修饰的话,可以将子类声明为sealed class。
Animal类,在指定允许继承的子类时可以使用全限定名
1 | public sealed class Animal permits Cat, Dog{// 多个子类之间用,隔开 |
Cat类
1 | public final class Cat extends Animal{ |
Dog类
1 | public sealed class Dog extends Animal |
Husky类
1 | public final class Husky extends Dog{ |
Test类
1 | public class Test{ |
Java16新特性
这里只介绍一些跟开发关联度较大的特性,除此之外JDK16还更新了许多其他新特性,感兴趣的同学可以去Oracle官网查看
包装类构造方法的警告
使用包装类的构造方法在编译的时候会出现警告,不建议再使用包装类的构造方法。下面代码在javac编译之后会出现警告。
1 | Integer i = new Integer(8); |
不建议使用包装类作为锁对象,倘若使用包装类作为锁对象,在编译时会出现警告。
1 | Integer i = 8; |
新增日时段
在DateTimeFormatter.ofPattern传入B可以获取现在时间对应的日时段,上午,下午等
1 | System.out.println(DateTimeFormatter.ofPattern("B").format(LocalDateTime.now())); |
在之前JDK版本中作为预览功能的Record类,模式匹配的instanceof,打包工具jpackage,已成为正式版。JDK16对GC,JVM运行时内存等内容有一些变化,例如:ZGC并发栈处理,弹性meta space
Java17新特性
java17是一个LTS(long term support)长期支持的版本,根据计划来看java17会支持到2029年(java8会支持到2030年,OMG),同时Oracle提议下一个LTS版本是java21,在2023年9月发布,这样讲LST版本的发布周期由之前的3年变为了2年。这里只介绍一些跟开发关联度较大的特性,除此之外JDK17还更新了一些其他新特性,感兴趣的同学可以从这里查看:https://www.oracle.com/news/announcement/oracle-releases-java-17-2021-09-14/
switch语法的变化(预览)
在之前版本中新增的instanceof模式匹配的特性在switch中也支持了,即我们可以在switch中减少强转的操作。比如下面的代码:
Rabbit和Bird均实现了Animal接口
1 | interface Animal{} |
新特性可以减少Animal强转操作代码的编写:
1 | public class Switch01{ |
该功能在java17中是预览的,编译和运行需要加上额外的参数:
1 | javac --enable-preview -source 17 Switch01.java |
去除了AOT和JIT
AOT(Ahead-of-Time)是java9中新增的功能,可以先将应用中中的字节码编译成机器码。
Graal编译器作为使用java开发的JIT(just-in-time )即时编译器在java10中加入(注意这里的JIT不是之前java中的JIT,在JEP 317中有说明https://openJDK.java.net/jeps/317)。
以上两项功能由于使用量较少,且需要花费很多精力来维护,因此在java17中被移除了。当然你可以通过Graal VM来继续使用这些功能。
Java18新特性
这里只介绍一些跟开发关联度较大的特性,除此之外JDK18还更新了许多其他新特性,感兴趣的同学可以去Oracle官网查看:
https://www.oracle.com/java/technologies/javase/18-relnote-issues.html#NewFeature
默认使用UTF-8字符编码
从JDK18开始,默认使用UTF-8字符编码。我们可以通过如下参数修改其他字符编码:
1 | -Dfile.encoding=UTF-8 |
将被移除的方法
在JDK18中标记了Object中的finalize方法,Thread中的stop方法将在未来被移除。
简单的web服务器
可以通过jwebserver命令启动JDK18中提供的静态web服务器,可以利用该工具查看一些原型,做简单的测试。在命令提示符中输入jwebserver命令后会启动,然后在浏览器中输入:http://127.0.0.1:8000/ 即可看到当前命令提示符路径下的文件了。
@snippet注解
以前在文档注释中编写代码时需要添加code标签,使用较为不便,通过**@snippet注解可以更方便的将文档注释中的代码展示在api文档中。**
Java19新特性
Virtual Threads (Preview)(虚拟线程)
该特性在java19中是预览版,虚拟线程是一种用户态下的线程,类似go语言中的goroutines 和Erlang中的processes,虚拟线程并非比线程快,而是提高了应用的吞吐量,相比于传统的线程是由操作系统调度来看,虚拟线程是我们自己程序调度的线程。如果你对之前java提供的线程API比较熟悉了,那么在学习虚拟线程的时候会比较轻松,传统线程能运行的代码,虚拟线程也可以运行。虚拟线程的出现,并没有修改java原有的并发模型,也不会替代原有的线程。虚拟线程主要作用是提升服务器端的吞吐量。
吞吐量的瓶颈
服务器应用程序的伸缩性受利特尔法则(Little’s Law)的制约,与下面3点有关
- 延迟:请求处理的耗时
- 并发量:同一时刻处理的请求数量
- 吞吐量:单位时间内处理的数据数量
比如一个服务器应用程序的延迟是50ms,处理10个并发请求,则吞吐量是200请求/秒(10 / 0.05),如果吞吐量要达到2000请求/秒,则处理的并发请求数量是100。按照1个请求对应一个线程的比例来看,要想提高吞吐量,线程数量也要增加。
java中的线程是在操作系统线程(OS thread)进行了一层包装,而操作系统中线程是重量级资源,在硬件配置确定的前提下,我们就不能创建更多的线程了,此时线程数量就限制了系统性能,为了解决该问题,虚拟线程就出现了。
与虚拟地址可以映射到物理内存类似,java是将大量的虚拟线程映射到少量的操作系统线程,多个虚拟线程可以使用同一个操作系统线程,其创建所耗费的资源也是极其低廉的,无需系统调用和系统级别的上下文切换,且虚拟线程的生命周期短暂,不会有很深的栈的调用,一个虚拟线程的生命周期中只运行一个任务,因此我们可以创建大量的虚拟线程,且虚拟线程无需池化。
虚拟线程的应用场景
在服务器端的应用程序中,可能会有大量的并发任务需要执行,而虚拟线程能够明显的提高应用的吞吐量。下面的场景能够显著的提高程序的吞吐量:
- 至少几千的并发任务量
- 任务为io密集型
下面代码中为每个任务创建一个线程,当任务量较多的时候,你的电脑可以感受到明显的卡顿(如果没有,可以增加任务数量试下):
1 | // ExecutorService实现了AutoCloseable接口,可以自动关闭了 |
将上面的代码改成虚拟线程之后,电脑不会感受到卡顿了:
1 | // newVirtualThreadPerTaskExecutor为每个任务创建一个虚拟线程 |
平台线程和虚拟线程
平台线程(platform thread):指java中的线程,比如通过Executors.newFixedThreadPool()创建出来的线程,我们称之为平台线程。
虚拟线程并不会直接分配给cpu去执行,而是通过调度器分配给平台线程,平台线程再被调度器管理。java中虚拟线程的调度器采用了工作窃取的模式进行FIFO的操作,调度器的并行数默认是JVM获取的处理器数量(通过该方法获取的数量Runtime.getRuntime().availableProcessors()),调度器并非分时(time sharing)的。在使用虚拟线程编写程序时,不能控制虚拟线程何时分配给平台线程,也不能控制平台线程何时分配给cpu。
以前任务和平台线程的关系:
使用虚拟线程之后,任务-虚拟线程-调度器-平台线程的关系,1个平台线程可以被调度器分配不同的虚拟线程:
创建虚拟线程的方式
java中创建的虚拟线程本质都是通过Thread.Builder.OfVirtual对象进行创建的,我们后面再来讨论这个对象,下面先看下创建虚拟线程的三种方式:
1.通过Thread.startVirtualThread直接创建一个虚拟线程
1 | // 创建任务 |
2.使用Thread.ofVirtual()方法创建
1 | // 创建任务 |
也可以在创建虚拟线程的时候直接启动
1 | // 创建任务 |
3.通过ExecutorService创建,为每个任务分配一个虚拟线程,下面代码中提交了100个任务,对应会有100个虚拟线程进行处理。
1 | /* |
现在平台线程和虚拟线程都是Thread的对象,那该如何区分该对象是平台线程还是虚拟线程?可以利用Thread中的isVirtual()方法进行判断,返回true表示虚拟线程:
1 | // 创建任务 |
Java21新特性
java20中没有太大的变化,这里主要聊下java21的新特性,21是继java17之后,最新的LTS版本,该版本中虚拟线程称为了正式版,对虚拟线程不了解的同学可以看下之前的java19中的介绍。接下来我们看下java21中一些新特性。
字符串模板 STR
字符串模板可以让开发者更简洁的进行字符串拼接(例如拼接sql,xml,json等)。该特性并不是为字符串拼接运算符+提供的语法糖,也并非为了替换SpringBuffer和StringBuilder。
这个和Python的插值表达式很类似,可以做变量的替换,多行文本的变量替换,还可以做一些计算 。
利用STR模板进行字符串与变量的拼接:
1 | String sport = "basketball"; |
这个特性目前是预览版,编译和运行需要添加额外的参数:
1 | javac --enable-preview -source 21 Test.java |
在js中字符串进行拼接时会采用下面的字符串插值写法
1 | let sport = "basketball" |
看起来字符串插值写法更简洁移动,不过若在java中使用这种字符串插值的写法拼接sql,可能会出现sql注入的问题,为了防止该问题,java提供了字符串模板表达式的方式。
上面使用的STR是java中定义的模板处理器,它可以将变量的值取出,完成字符串的拼接。在每个java源文件中都引入了一个public static final修饰的STR属性,因此我们可以直接使用STR,STR通过打印STR可以知道它是java.lang.StringTemplate,是一个接口。
在StringTemplate中是通过调用interpolate方法来执行的,该方法分别传入了两个参数:
- fragements:包含字符串模板中所有的字面量,是一个List
- values:包含字符串模板中所有的变量,是一个List
而该方法又调用了JavaTemplateAccess中的interpolate方法,经过分析可以得知,它最终是通过String中的join方法将字面量和变量进行的拼接。
scoped values 传递参数时无需声明形参
ThreadLocal的问题
scoped values 是一个隐藏的方法参数,只有方法可以访问scoped values,它可以让两个方法之间传递参数时无需声明形参。例如在UserDao类中编写了saveUser方法,LogDao类中编写了saveLog方法,那么在保存用户的时候需要保证事务,此时就需要在service层获取Connection对象,然后将该对象分别传入到两个Dao的方法中,但对于saveUser方法来说并不是直接使用Connection对象,却又不得不在方法的形参中写上该对象,其实仅从业务上来看,该方法中只要传入User对象就可以了。
int saveUser(Connection connection,User user);
对于上面的问题,开发者通常会使用ThreadLocal解决,但由于ThreadLocal在设计上的瑕疵,导致下面问题:
- 内存泄漏,在用完ThreadLocal之后若没有调用remove,这样就会出现内存泄漏。
- 增加开销,在具有继承关系的线程中,子线程需要为父线程中ThreadLocal里面的数据分配内存。
- 混乱的可变,任何可以调用ThreadLocal中get方法的代码都可以随时调用set方法,这样就不易辨别哪些方法是按照什么顺序来更新的共享数据。
随着虚拟线程的到来,内存泄漏问题就不用担心了,由于虚拟线程会很快的终止,此时会自动删除ThreadLocal中的数据,这样就不用调用remove方法了。但虚拟线程的数量通常是多的,试想下上百万个虚拟线程都要拷贝一份ThreadLocal中的变量,这会使内存承受更大的压力。为了解决这些问题,scoped values就出现了。
ScopeValue初体验
在java21中新增了ScopeValue类,为了便于多个方法使用,通常会将该类的对象声明为static final ,每个线程都能访问自己的scope value,与ThreadLocal不同的是,它只会被write 1次且仅在线程绑定的期间内有效。
下面代码模拟了送礼和收礼的场景
1 | public class Test{ |
多线程操作相同的ScopeValue
不同的线程在操作同一个ScopeValue时,相互间不会影响,其本质是利用了Thread类中scopedValueBindings属性进行的线程绑定。
1 | import java.util.concurrent.ExecutorService; |
record pattern 解构
通过该特性可以解构record类型中的值,例如
1 | public class Test{ |
switch格式匹配
之前的写法
1 | public class Test{ |
新的写法,代码更加简洁
1 | public class Test{ |
可以在switch中使用when
1 | public class Test{ |
Unnamed Classes and Instance Main Methods
对于初学者来说,写的第一个HelloWorld代码有太多的概念,为了方便初学者快速编写第一段java代码,这里提出了无名类和实例main方法,下面代码可以直接运行编译,相当于是少了类的定义,main方法的修饰符和形参也省略掉了
1 | void main() { |
Structured Concurrency
该特性主要作用是在使用虚拟线程时,可以使任务和子任务的代码编写起来可读性更强,维护性更高,更加可靠。
1 | import java.util.concurrent.ExecutionException; |
Java22新特性
无名变量 __
这个和Go的下划线挺类似的,一般使用在try-catch中,直接将e,变为下划线。主要是区分出需要使用的变量和不需要使用的变量,因为不是所有的返回值都是你需要的,这样可以使代码逻辑更加清晰。
以下场景可以使用Unnamed Variables
局部变量
try-with-resource
循环头中声明的变量
catch中声明的变量
lambda表达式中的参数