改进的 Stream API
还记得我们之前在JavaSE中学习的Stream流吗?当然这里不是指进行IO操作的流,而是JDK1.8新增的Stream API,通过它大大方便了我们的编程。
public static void main(String[] args) {
Stream
.of("A", "B", "B", "C") //这里我们可以直接将一些元素封装到Stream中
.filter(s -> s.equals("B")) //通过过滤器过滤
.distinct() //去重
.forEach(System.out::println); //最后打印
}
自从有了Stream,我们对于集合的一些操作就大大地简化了,对集合中元素的批量处理,只需要在Stream中一气呵成(具体的详细操作请回顾JavaSE篇)
如此方便的框架,在Java 9得到了进一步的增强:
public static void main(String[] args) {
Stream
.of(null) //如果传入null会报错
.forEach(System.out::println);
Stream
.ofNullable(null) //使用新增的ofNullable方法,这样就不会了,不过这样的话流里面就没东西了
.forEach(System.out::println);
}
还有,我们可以通过迭代快速生成一组数据(实际上Java 8就有了,这里新增的是允许结束迭代的):
public static void main(String[] args) {
Stream
.iterate(0, i -> i + 1) //Java8只能像这样生成无限的流,第一个参数是种子,就是后面的UnaryOperator的参数i一开始的值,最后会返回一个值作为i的新值,每一轮都会执行UnaryOperator并生成一个新值到流中,这个是源源不断的,如果不加limit()进行限制的话,将无限生成下去。
.limit(20) //这里限制生成20个
.forEach(System.out::println);
}
public static void main(String[] args) {
Stream
//不知道怎么写?参考一下:for (int i = 0;i < 20;i++)
.iterate(0, i -> i < 20, i -> i + 1) //快速生成一组0~19的int数据,中间可以添加一个断言,表示什么时候结束生成
.forEach(System.out::println);
}
Stream还新增了对数据的截断操作,比如我们希望在读取到某个元素时截断,不再继续操作后面的元素:
public static void main(String[] args) {
Stream
.iterate(0, i -> i + 1)
.limit(20)
.takeWhile(i -> i < 10) //当i小于10时正常通过,一旦大于等于10直接截断
.forEach(System.out::println);
}
public static void main(String[] args) {
Stream
.iterate(0, i -> i + 1)
.limit(20)
.dropWhile(i -> i < 10) //和上面相反,上来就是截断状态,只有当满足条件时再开始通过
.forEach(System.out::println);
}
其他小型变动
Try-with-resource语法现在不需要再完整的声明一个变量了,我们可以直接将现有的变量丢进去:
public static void main(String[] args) throws IOException {
InputStream inputStream = Files.newInputStream(Paths.get("pom.xml"));
try (inputStream) { //单独丢进try中,效果是一样的
for (int i = 0; i < 100; i++)
System.out.print((char) inputStream.read());
}
}
在Java 8中引入了Optional类,它很好的解决了判空问题:
public static void main(String[] args) throws IOException {
test(null);
}
public static void test(String s){
//比如现在我们想执行 System.out.println(str.toLowerCase())
//但是由于我们不清楚给进来的str到底是不是null,如果是null的话会引起空指针异常
//但是去单独进行一次null判断写起来又不太简洁,这时我们可以考虑使用Optional进行包装
Optional
.ofNullable(s)
.ifPresent(str -> System.out.println(str.toLowerCase()));
}
这种写法就有点像Kotlin或是JS中的语法:
fun main() {
test(null)
}
fun test(str : String?){ //传入的String对象可能为null,这里类型写为String?
println(str?.lowercase()) // ?.表示只有不为空才进行调用
}
在Java 9新增了一些更加方便的操作:
public static void main(String[] args) {
String str = null;
Optional.ofNullable(str).ifPresentOrElse(s -> { //通过使用ifPresentOrElse,我们同时处理两种情况
System.out.println("被包装的元素为:"+s); //第一种情况和ifPresent是一样的
}, () -> {
System.out.println("被包装的元素为null"); //第二种情况是如果为null的情况
});
}
我们也可以使用or()方法快速替换为另一个Optional类:
public static void main(String[] args) {
String str = null;
Optional.ofNullable(str)
.or(() -> Optional.of("AAA")) //如果当前被包装的类不是null,依然返回自己,但是如果是null,那就返回Supplier提供的另一个Optional包装
.ifPresent(System.out::println);
}
当然还支持直接转换为Stream,这里就不多说了。
在Java 8及之前,匿名内部类是没办法使用钻石运算符进行自动类型推断的:
public abstract class Test<T>{ //这里我们写一个泛型类
public T t;
public Test(T t) {
this.t = t;
}
public abstract T test();
}
public static void main(String[] args) throws IOException {
Test<String> test = new Test<>("AAA") { //在低版本这样写是会直接报错的,因为匿名内部类不支持自动类型推断,但是很明显我们这里给的参数是String类型的,所以明明有机会进行类型推断,却还是要我们自己填类型,就很蠢
//在Java 9之后,这样的写法终于可以编译通过了
@Override
public String test() {
return t;
}
};
}
当然除了以上的特性之外还有Java 9的多版本JAR包支持、CompletableFuture API的改进等,因为不太常用,这里就不做介绍了。
Java 10 新特性
Java 10主要带来的是一些内部更新,相比Java 9带来的直观改变不是很多,其中比较突出的就是局部变量类型推断了。
局部变量类型推断
在Java中,我们可以使用自动类型推断:
public static void main(String[] args) {
// String a = "Hello World!"; 之前我们定义变量必须指定类型
var a = "Hello World!"; //现在我们使用var关键字来自动进行类型推断,因为完全可以从后面的值来判断是什么类型
}
但是注意,var关键字必须位于有初始值设定的变量上,否则鬼知道你要用什么类型。

我们来看看是不是类型也能正常获取:
public static void main(String[] args) {
var a = "Hello World!";
System.out.println(a.getClass());
}
这里虽然是有了var关键字进行自动类型推断,但是最终还是会变成String类型,得到的Class也是String类型。但是Java终究不像JS那样进行动态推断,这种类型推断仅仅发生在编译期间,到最后编译完成后还是会变成具体类型的:

并且var关键字仅适用于局部变量,我们是没办法在其他地方使用的,比如类的成员变量:

有关Java 10新增的一些其他改进,这里就不提了。
Java 11 新特性
Java 11 是继Java 8之后的又一个TLS长期维护版本,在Java 17出现之前,一直都是此版本作为广泛使用的版本,其中比较关键的是用于Lambda的形参局部变量语法。
用于Lambda的形参局部变量语法
在Java 10我们认识了var关键字,它能够直接让局部变量自动进行类型推断,不过它不支持在lambda中使用:

但是实际上这里是完全可以进行类型推断的,所以在Java 11,终于是支持了,这样编写就不会报错了:

针对于String类的方法增强
在Java 11为String新增一些更加方便的操作:
public static void main(String[] args) {
var str = "AB\nC\nD";
System.out.println(str.isBlank()); //isBlank方法用于判断是否字符串为空或者是仅包含空格
str
.lines() //根据字符串中的\n换行符进行切割,分为多个字符串,并转换为Stream进行操作
.forEach(System.out::println);
}
我们还可以通过repeat()方法来让字符串重复拼接:
public static void main(String[] args) {
String str = "ABCD"; //比如现在我们有一个ABCD,但是现在我们想要一个ABCDABCD这样的基于原本字符串的重复字符串
System.out.println(str.repeat(2)); //一个repeat就搞定了
}
我们也可以快速地进行空格去除操作:
public static void main(String[] args) {
String str = " A B C D ";
System.out.println(str.strip()); //去除首尾空格
System.out.println(str.stripLeading()); //去除首部空格
System.out.println(str.stripTrailing()); //去除尾部空格
}
全新的HttpClient使用
在Java 9的时候其实就已经引入了全新的Http Client API,用于取代之前比较老旧的HttpURLConnection类,新的API支持最新的HTTP2和WebSocket协议。
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient(); //直接创建一个新的HttpClient
//现在我们只需要构造一个Http请求实体,就可以让客户端帮助我们发送出去了(实际上就跟浏览器访问类似)
HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://www.baidu.com")).build();
//现在我们就可以把请求发送出去了,注意send方法后面还需要一个响应体处理器(内置了很多)这里我们选择ofString直接吧响应实体转换为String字符串
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
//来看看响应实体是什么吧
System.out.println(response.body());
}
利用全新的客户端,我们甚至可以轻松地做一个爬虫(仅供学习使用,别去做违法的事情,爬虫玩得好,牢饭吃到饱),比如现在我们想去批量下载某个网站的壁纸:
网站地址:https://pic.netbian.com/4kmeinv/
我们随便点击一张壁纸,发现网站的URL格式为:

并且不同的壁纸似乎都是这样:https://pic.netbian.com/tupian/数字.html,好了差不多可以开始整活了:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
for (int i = 0; i < 10; i++) { //先不要一次性获取太多,先来10个
HttpRequest request = HttpRequest.newBuilder().uri(new URI("https://pic.netbian.com/tupian/"+(29327 + i)+".html")).build(); //这里我们按照规律,批量获取
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body()); //这里打印一下看看网页
}
}
可以看到,最后控制台成功获取到这些图片的网站页面了:

接着我们需要来观察一下网站的HTML具体怎么写的,把图片的地址提取出来:

好了,知道图片在哪里就好办了,直接字符串截取:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
for (int i = 0; i < 10; i++) {
...
String html = response.body();
String prefix = "<a href=\"\" id=\"img\"><img src=\""; //先找好我们要截取的前面一段,作为前缀去匹配位置
String suffix = "\" data-pic="; //再找好我们要截取的屁股后面紧接着的位置,作为后缀去匹配位置
//直接定位,然后前后截取,得到最终的图片地址
html = html.substring(html.indexOf(prefix) + prefix.length());
html = html.substring(0, html.indexOf(suffix));
System.out.println(html); //最终的图片地址就有了
}
}
好了,现在图片地址也可以批量拿到了,直接获取这些图片然后保存到本地吧:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
for (int i = 0; i < 10; i++) {
...
//创建请求,把图片取到
HttpRequest imageRequest = HttpRequest.newBuilder().uri(new URI("https://pic.netbian.com"+html)).build();
//这里以输入流的方式获取,不过貌似可以直接下载文件,各位小伙伴可以单独试试看
HttpResponse<InputStream> imageResponse = client.send(imageRequest, HttpResponse.BodyHandlers.ofInputStream());
//拿到输入流和文件输出流
InputStream imageInput = imageResponse.body();
FileOutputStream stream = new FileOutputStream("images/"+i+".jpg"); //一会要保存的格式
try (stream;imageInput){ //直接把要close的变量放进来就行,简洁一些了
int size; //下面具体保存过程的不用我多说了吧
byte[] data = new byte[1024];
while ((size = imageInput.read(data)) > 0) {
stream.write(data, 0, size);
}
}
}
}
我们现在来看看效果吧,美女的图片已经成功保存到本地了:

当然,这仅仅是比较简单的爬虫,不过我们的最终目的还是希望各位能够学会使用新的HttpClient API。
Java 12-16 新特性
由于Java版本的更新迭代速度自Java 9开始为半年更新一次(Java 8到Java 9隔了整整三年),所以各个版本之间的更新内容比较少,剩余的6个版本,我们就多个版本放在一起进行讲解了。

Java12-16这五个版本并非长期支持版本,所以很多特性都是一种处于实验性功能,12/13版本引入了一些实验性功能,并根据反馈进行调整,最后在后续版本中正式开放使用,其实就是体验服的那种感觉。
新的switch语法
在Java 12引入全新的switch语法,让我们使用switch语句更加的灵活,比如我们想要编写一个根据成绩得到等级的方法:
/**
* 传入分数(范围 0 - 100)返回对应的等级:
* 100-90:优秀
* 70-80:良好
* 60-70:及格
* 0-60:寄
* @param score 分数
* @return 等级
*/
public static String grade(int score){
}
现在我们想要使用switch来实现这个功能(不会吧不会吧,不会有人要想半天怎么用switch实现吧),之前的写法是:
public static String grade(int score){
score /= 10; //既然分数段都是整数,那就直接整除10
String res = null;
switch (score) {
case 10:
case 9:
res = "优秀"; //不同的分数段就可以返回不同的等级了
break; //别忘了break,不然会贯穿到后面
case 8:
case 7:
res = "良好";
break;
case 6:
res = "及格";
break;
default:
res = "不及格";
break;
}
return res;
}
但是现在我们可以使用新的特性了:
public static String grade(int score){
score /= 10; //既然分数段都是整数,那就直接整除10
return switch (score) { //增强版switch语法
case 10, 9 -> "优秀"; //语法那是相当的简洁,而且也不需要我们自己考虑break或是return来结束switch了(有时候就容易忘记,这样的话就算忘记也没事了)
case 8, 7 -> "良好";
case 6 -> "及格";
default -> "不及格";
};
}
不过最后编译出来的样子,貌似还是和之前是一样的:

这种全新的switch语法称为switch表达式,它的意义不仅仅体现在语法的精简上,我们来看看它的详细规则:
var res = switch (obj) { //这里和之前的switch语句是一样的,但是注意这样的switch是有返回值的,所以可以被变量接收
case [匹配值, ...] -> "优秀"; //case后直接添加匹配值,匹配值可以存在多个,需要使用逗号隔开,使用 -> 来返回如果匹配此case语句的结果
case ... //根据不同的分支,可以存在多个case
default -> "不及格"; //注意,表达式要求必须涵盖所有的可能,所以是需要添加default的
};
那么如果我们并不是能够马上返回,而是需要做点什么其他的工作才能返回结果呢?
var res = switch (obj) { //增强版switch语法
case [匹配值, ...] -> "优秀";
default -> { //我们可以使用花括号来将整套逻辑括起来
//... 我是其他要做的事情
yield "不及格"; //注意处理完成后需要返回最终结果,但是这样并不是使用return,而是yield关键字
}
};
当然,也可以像这样:
var res = switch (args.length) { //增强版switch语法
case [匹配值, ...]:
yield "AAA"; //传统的:写法,通过yield指定返回结果,同样不需要break
default:
System.out.println("默认情况");
yield "BBB";
};
这种全新的语法,可以说极大地方便了我们的编码,不仅代码简短,而且语义明确。唯一遗憾的是依然不支持区间匹配。
注意: switch表达式在Java 14才正式开放使用,所以我们项目的代码级别需要调整到14以上。
文本块
如果你学习过Python,一定知道三引号:
#当我们需要使用复杂字符串时,可能字符串中包含了很多需要转义的字符,比如双引号等,这时我们就可以使用三引号来囊括字符串
multi_line = """
nice to meet you!
nice to meet you!
nice to meet you!
"""
print multi_line
没错,Java13也带了这样的特性,旨在方便我们编写复杂字符串,这样就不用再去用那么多的转义字符了:

可以看到,Java中也可以使用这样的三引号来表示字符串了,并且我们可以随意在里面使用特殊字符,包括双引号等,但是最后编译出来的结果实际上还是会变成一个之前这样使用了转义字符的字符串:

仔细想想,这样我们写SQL或是HTML岂不是就舒服多了?
注意: 文本块表达式在Java 15才正式开放使用,所以我们项目的代码级别需要调整到15以上。
新的instanceof语法
在Java 14,instanceof迎来了一波小更新(哈哈,这版本instanceof又加强了,版本强势语法)
比如我们之前要重写一个类的equals方法:
public class Student {
private final String name;
public Student(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) { //首先判断是否为Student类型
Student student = (Student) obj; //如果是,那么就类型转换
return student.name.equals(this.name); //最后比对属性是否一样
}
return false;
}
}
在之前我们一直都是采用这种先判断类型,然后类型转换,最后才能使用的方式,但是这个版本instanceof加强之后,我们就不需要了,我们可以直接将student替换为模式变量:

public class Student {
private final String name;
public Student(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Student student) { //在比较完成的屁股后面,直接写变量名字,而这个变量就是类型转换之后的
return student.name.equals(this.name); //下面直接用,是不是贼方便
}
return false;
}
}
在使用instanceof判断类型成立后,会自动强制转换类型为指定类型,简化了我们手动转换的步骤。
注意: 新的instanceof语法在Java 16才正式开放使用,所以我们项目的代码级别需要调整到16以上。
空指针异常的改进
相信各位小伙伴在调试代码时,经常遇到空指针异常,比如下面的这个例子:
public static void test(String a, String b){
int length = a.length() + b.length(); //可能给进来的a或是b为null
System.out.println(length);
}
那么为空时,就会直接:

但是由于我们这里a和b都调用了length()方法,虽然空指针异常告诉我们问题出现在这一行,但是到底是a为null还是b为null呢?我们是没办法直接得到的(遇到过这种问题的扣个1吧,只能调试,就很头疼)
但是当我们在Java 14或更高版本运行时:

这里会明确指出是哪一个变量调用出现了空指针,是不是感觉特别人性化。
记录类型
继类、接口、枚举、注解之后的又一新类型来了,它的名字叫"记录",在Java 14中首次出场,这一出场,Lombok的噩梦来了。
在实际开发中,很多的类仅仅只是充当一个实体类罢了,保存的是一些不可变数据,比如我们从数据库中查询的账户信息,最后会被映射为一个实体类:
@Data
public class Account { //使用Lombok,一个注解就搞定了
String username;
String password;
}
Lombok可以说是简化代码的神器了,他能在编译时自动生成getter和setter、构造方法、toString()方法等实现,在编写这些实体类时,简直不要太好用,而这一波,官方也是看不下去了,于是自己也搞了一个记录类型。
记录类型本质上也是一个普通的类,不过是final类型且继承自java.lang.Record抽象类的,它会在编译时,会自动编译出 public get hashcode 、equals、toString 等方法,好家伙,这是要逼死Lombok啊。
public record Account(String username, String password) { //直接把字段写在括号中
}
使用起来也是非常方便,自动生成了构造方法和成员字段的公共get方法:

并且toString也是被重写了的:

equals()方法仅做成员字段之间的值比较,也是帮助我们实现好了的:
Account account0 = new Account("Admin", "123456");
Account account1 = new Account("Admin", "123456"); //两个属性都是一模一样的
System.out.println(account0.equals(account1)); //得到true
是不是感觉这种类型就是专门为这种实体类而生的。
public record Account(String username, String password) implements Runnable { //支持实现接口,但是不支持继承,因为继承的坑位已经默认被占了
@Override
public void run() {
}
}
注意: 记录类型在Java 16才正式开放使用,所以我们项目的代码级别需要调整到16以上。
Java 17 新特性
Java 17作为新的LTS长期维护版本,我们来看看都更新了什么(不包含预览特性,包括switch第二次增强,哈哈,果然还是强度不够,都连续加强两个版本了)
密封类型
密封类型可以说是Java 17正式推出的又一重磅类型,它在Java 15首次提出并测试了两个版本。
在Java中,我们可以通过继承(extends关键字)来实现类的能力复用、扩展与增强。但有的时候,可能并不是所有的类我们都希望能够被继承。所以,我们需要对继承关系有一些限制的控制手段,而密封类的作用就是限制类的继承。
实际上在之前我们如果不希望别人继承我们的类,可以直接添加final关键字:
public final class A{ //添加final关键字后,不允许对此类继承
}
这样有一个缺点,如果添加了final关键字,那么无论是谁,包括我们自己也是没办法实现继承的,但是现在我们有一个需求,只允许我们自己写的类继承A,但是不允许别人写的类继承A,这时该咋写?在Java 17之前想要实现就很麻烦。
但是现在我们可以使用密封类型来实现这个功能:
public sealed class A permits B{ //在class关键字前添加sealed关键字,表示此类为密封类型,permits后面跟上允许继承的类型,多个子类使用逗号隔开
}
密封类型有以下要求:
- 可以基于普通类、抽象类、接口,也可以是继承自其他接抽象类的子类或是实现其他接口的类等。
- 必须有子类继承,且不能是匿名内部类或是lambda的形式。
sealed写在原来final的位置,但是不能和final、non-sealed关键字同时出现,只能选择其一。- 继承的子类必须显式标记为
final、sealed或是non-sealed类型。
标准的声明格式如下:
public sealed [abstract] [class/interface] 类名 [extends 父类] [implements 接口, ...] permits [子类, ...]{
//里面的该咋写咋写
}
注意子类格式为:
public [final/sealed/non-sealed] class 子类 extends 父类 { //必须继承自父类
//final类型:任何类不能再继承当前类,到此为止,已经封死了。
//sealed类型:同父类,需要指定由哪些类继承。
//non-sealed类型:重新开放为普通类,任何类都可以继承。
}
比如现在我们写了这些类:
public sealed class A permits B{ //指定B继承A
}
public final class B extends A { //在子类final,彻底封死
}
我们可以看到其他的类无论是继承A还是继承B都无法通过编译:


但是如果此时我们主动将B设定为non-sealed类型:
public non-sealed class B extends A {
}
这样就可以正常继承了,因为B指定了non-sealed主动放弃了密封特性,这样就显得非常灵活了。
当然我们也可以通过反射来获取类是否为密封类型:
public static void main(String[] args) {
Class<A> a = A.class;
System.out.println(a.isSealed()); //是否为密封
}
至此,Java 9 - 17的主要新特性就讲解完毕了。