以下内容为B站动力节点的JAVA基础课程2024全部内容,课程链接如下:https://www.bilibili.com/video/BV1p7421N7XT/,该笔记根据课程资料整理,顺序按照课程PPT截图整理,附有部分个人的问题和重点整理,如有侵权请联系下架,后续会持续更新整理课程中的重难点,课程中有很多重点和细节部分值得反复思考、消化,个人觉得是很不错的JAVA课程
第十一章 注解
注解概述
Java预置注解
自定义注解
元注解
反射注解
综合练习
第十二章 网络编程
网络编程概述
网络编程三要素
网络编程基础类
TCP与UDP协议
基于TCP协议的编程
基于UDP协议的编程
第十三章 Lambda表达式
主要内容
- Lambda表达式的概述
- Lambda表达式的使用
- Lambda表达式的方法引用
- Lambda表达式的在集合中的使用
学习目标
知识点 | 要求 |
---|---|
Lambda表达式的概述 | 理解 |
Lambda表达式的使用 | 了解 |
Lambda表达式的方法引用 | 掌握 |
Lambda表达式的在集合中的使用 | 理解 |
Lambda表达式的概述
Lambda表达式的引入
Lambda表达式是JDK1.8的一个新特性,可以取代大部分的匿名内部类,以便写出更优雅的Java代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。
在以前的学习中,想要实现对List集合的“降序”排序操作,就需要使用匿名内部类来实现,这样的代码非常的复杂和繁琐,代码如下:
// 方式一:使用匿名内部类来实现
List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println("排序后:" + list);
针对以上对List集合的的“降序”排序操作,除了使用匿名内部类来实现外,还可以使用Lambda表达式来实现,使用Lambda表达式的代码非常优雅,并且还非常的简洁,代码如下:
// 方式二:使用Lambda表达式来实现
List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, (o1, o2) -> o2 - o1);
System.out.println("排序后:" + list);
函数式编程思想的概述
Java从诞生之日起就一直倡导“一切皆对象”,在Java语言中面向对象(OOP)编程就是一切,但是随着Python和Scala等语言的崛起和新技术的挑战,Java也不得不做出调整以便支持更加广泛的技术要求,即Java语言不但支持OOP还支持OOF(面向函数编程)。
JDK1.8引入Lambda表达式之后,Java语言也开始支持函数式编程,但是Lambda表达式不是Java语言最早使用的,目前C++、C#、Python、Scala等语言都支持Lambda表示。
- 面向对象的思想
- 做一件事情,找一个能解决这个事情的对象,然后调用对象的方法,最终完成事情。
- 函数式编程思想
- 只要能获得结果,谁去做的,怎么做的都不重要,重视的是结果,不重视实现过程。
在函数式编程语言中,函数被当成一等公民对待。在将函数当成一等公民的编程语言中,Lambda表达式的类型是函数,但是Lambda表达式却是一个对象,而不是函数,它们必须依附于一类特别的对象类型,也就是所谓的函数式接口。
简单点说,JDK1.8中的Lambda表达式就是一个函数式接口的实例,这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以使用Lambda表达式来表示。
如何去理解函数式接口
能够使用Lambda表达式的一个重要依据是必须有相应的函数式接口,所谓的函数式接口,指的就是“一个接口中有且只能有一个抽象方法”。也就是说,如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口。
如果我们在接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,也就是该接口中有且只能定义一个抽象方法,如果该接口中定义了多个或0个抽象方法,则程序编译时就会报错。
【示例】定义一个函数式接口
@FunctionalInterface
public interface Flyable {
// 在函数式接口中,我们有且只能定义一个抽象方法
void showFly();
// 但是,可以定义任意多个默认方法或静态方法
default void show() {
System.out.println("JDK1.8之后,接口还可以定义默认方法和静态方法");
}
}
另外,从某种意义上来说,只要你保证你的接口中有且只有一个抽象方法,则接口中没有使用 @FunctionalInterface 注解来标注,那么该接口也依旧属于函数式接口。
在以下代码中,Flyable接口中没有使用@FunctionalInterface 注解,但是Flyable接口中只存在一个抽象方法,因此Flyable接口依旧属于函数式接口,那么使用Lambda表达式就可以表示Flyable 接口的实例,代码如下:
/**
* 没有使用@FunctionalInterface标注的接口
*/
public interface Flyable {
void showFly();
}
/**
* 测试类
*/
public class Test01 {
public static void main(String[] args) {
// 使用lambda表示来表示Flyable接口的实例
Flyable flyable = () -> {
System.out.println("小鸟自由自在的飞翔");
};
// 调用Flyable接口的实例的showFly()方法
flyable.showFly();
}
}
Lambda和匿名内部类
- 所需类型不同
- 匿名内部类:可以是接口,抽象类,具体类。
- Lambda表达式:只能是接口。
- 使用限制不同
- 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类。
- 如果接口中有多个抽象方法,则就只能使用匿名内部类,而不能使用Lambda表达式。
- 实现原理不同
- 匿名内部类:编译之后,会生成一个单独的.class字节码文件。
- Lambda表达式:编译之后,没有生成一个单独的.class字节码文件。
Lambda表达式的使用
Lambda表达式的语法
Lambda表达式本质就是一个匿名函数,在函数的语法中包含返回值类型、方法名、形参列表和方法体等,而在Lambda表达式中我们只需要关心形参列表和方法体即可。
在Java语言中,Lambda表达式的语法为“(形参列表) -> {方法体}”,其中“->”为 lambda操作符或箭头操作符,“形参列表”为对应接口实现类中重写方法的形参列表,“方法体”为对应接口实现类中重写方法的方法体。
接下来,我们就以匿名内部类为例,从而将匿名内部类演化为Lambda表达式,代码如下:
List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
System.out.println("排序后:" + list);
在以上的匿名内部类中,黄色背景颜色标注的代码都属于不可变的固定代码,而红色背景颜色标注的代码,属于可变的并且是完成该功能的核心代码。因此,将此处的匿名内部类转化为Lambda表达式,我们只需保留红色部分的形参列表和方法体即可,对应的Lambda表达式代码实现如下:
List<Integer> list = Arrays.asList(3, 6, 1, 7, 2, 5, 4);
Collections.sort(list, (Integer o1, Integer o2) -> {
return o2 - o1;
});
System.out.println("排序后:" + list);
在以上代码中,黄色背景颜色标注的就是重写于Comparator接口中抽象方法的形参列表,而红色背景颜色标注的就是重写方法对应方法体的代码实现。因此Lambda本质上就是去掉了一堆没有意义的代码,只留下核心的代码逻辑,从而让代码看起来更加的简洁且优雅。
Lambda表达式的使用
Lambda表达式的基本使用
接下来,我们以自定义的函数式接口为例,先从匿名对象的实现过程,慢慢演变为Lambda表达式的实现过程。另外,使用Lambda表达式的时候,则必须有上下文环境,才能推导出Lambda对应的接口类型。
无返回值函数式接口
情况一:无返回值无参数
// 情况一:无返回值无参数
interface NoParameterNoReturn {
void test();
}
public class Test01 {
public static void main(String[] args) {
// 方式一:使用匿名内部类来实现
NoParameterNoReturn obj1 = new NoParameterNoReturn() {
@Override
public void test() {
System.out.println("无参无返回值");
}
};
obj1.test();
// 方式二:使用Lambda表达式来实现
NoParameterNoReturn obj2 = () -> {
System.out.println("无参无返回值");
};
obj2.test();
}
}
情况二:无返回值一个参数
// 情况二:无返回值一个参数
interface OneParameterNoReturn {
void test(int num);
}
public class Test01 {
public static void main(String[] args) {
// 方式一:使用匿名内部类来实现
OneParameterNoReturn obj1 = new OneParameterNoReturn() {
@Override
public void test(int num) {
System.out.println("无返回值一个参数 --> " + num);
}
};
obj1.test(10);
// 方式二:使用Lambda表达式来实现
OneParameterNoReturn obj2 = (int num) -> {
System.out.println("无返回值一个参数 --> " + num);
};
obj2.test(20);
}
}
情况三:无返回值多个参数
// 情况三:无返回值多个参数
interface MoreParameterNoReturn {
void test(String str1, String str2);
}
public class Test01 {
public static void main(String[] args) {
// 方式一:使用匿名内部类来实现
MoreParameterNoReturn obj1 = new MoreParameterNoReturn() {
@Override
public void test(String str1, String str2) {
System.out.println(str1 + " : " + str2);
}
};
obj1.test("hello", "world");
// 方式二:使用Lambda表达式来实现
MoreParameterNoReturn obj2 = (String str1, String str2) -> {
System.out.println(str1 + " : " + str2);
};
obj2.test("你好", "世界");
}
}
有返回值函数接口
情况一:有返回值无参数
// 情况一:有返回值无参数
interface NoParameterHasReturn {
int test();
}
public class Test01 {
public static void main(String[] args) {
// 方式一:使用匿名内部类来实现
NoParameterHasReturn obj1 = new NoParameterHasReturn() {
@Override
public int test() {
return 520;
}
};
System.out.println(obj1.test()); // 输出:520
// 方式二:使用Lambda表达式来实现
NoParameterHasReturn obj2 = () -> {
return 1314;
};
System.out.println(obj2.test()); // 输出:1314
}
}
情况二:有返回值一个参数
// 情况二:有返回值一个参数
interface OneParameterHasReturn {
String test(double num);
}
public class Test01 {
public static void main(String[] args) {
// 方式一:使用匿名内部类来实现
OneParameterHasReturn obj1 = new OneParameterHasReturn() {
@Override
public String test(double num) {
return "传入的小数为:" + num;
}
};
System.out.println(obj1.test(520.0));
// 方式二:使用Lambda表达式来实现
OneParameterHasReturn obj2 = (double num) -> {
return "传入的小数为:" + num;
};
System.out.println(obj2.test(1314.0));
}
}
情况三:有返回值多个参数
// 情况三:有返回值多个参数
interface MoreParameterHasReturn {
String test(int num1, int num2);
}
public class Test01 {
public static void main(String[] args) {
// 方式一:使用匿名内部类来实现
MoreParameterHasReturn obj1 = new MoreParameterHasReturn() {
@Override
public String test(int num1, int num2) {
return "运算的结果为:" + (num1 + num2);
}
};
System.out.println(obj1.test(10, 20));
// 方式二:使用Lambda表达式来实现
MoreParameterHasReturn obj2 = (int num1, int num2) -> {
return "运算的结果为:" + (num1 + num2);
};
System.out.println(obj2.test(20, 30));
}
}
Lambda表达式的语法精简
在以上代码中,虽然Lambda表达式的语法已经很简洁了,但是Lambda表达式的语法格式还可以更加的精简,从而写出更加优雅的代码,但是相应的代码可读性也会变差。
在以下的应用场景中,我们就可以对Lambda表达式的语法进行精简,场景如下:
- 形参类型可以省略,如果需要省略,则每个形参的类型都要省略。
- 如果形参列表中只存在一个形参,那么形参类型和小括号都可以省略。
- 如果方法体当中只有一行语句,那么方法体的大括号也可以省略。
- 如果方法体中只有一条return语句,那么大括号可以省略,且必须去掉return关键字。
接下来,我们就对以下的Lambda表达式代码进行精简,从而写出更加优雅的代码。
public class Test01 {
public static void main(String[] args) {
// (1)形参类型可以省略,如果需要省略,每个形参的类型都要省略。
// 没有精简的Lambda表达式代码
MoreParameterNoReturn obj1 = (String str1, String str2) -> {
System.out.println(str1 + " : " + str2);
};
obj1.test("hello", "world");
// 精简之后的Lambda表达式代码
MoreParameterNoReturn obj2 = (str1, str2) -> {
System.out.println(str1 + " : " + str2);
};
obj2.test("你好", "世界");
// (2)如果形参列表中只有一个形参,那么形参类型和小括号都可以省略。
// 没有精简的Lambda表达式代码
OneParameterHasReturn obj3 = (double num) -> {
return "传入的小数为:" + num;
};
System.out.println(obj3.test(520.0));
// 精简之后的Lambda表达式代码
OneParameterHasReturn obj4 = num -> {
return "传入的小数为:" + num;
};
System.out.println(obj4.test(1314.0));
// (3)如果方法体当中只有一行代码,那么方法体的大括号也可以省略。
// 没有精简的Lambda表达式代码
NoParameterNoReturn obj5 = () -> {
System.out.println("无参无返回值");
};
obj5.test();
// 精简之后的Lambda表达式代码
NoParameterNoReturn obj6 = () -> System.out.println("无参无返回值");
obj6.test();
// (4)方法体中只有一条return语句,则大括号可以省略,且必须去掉return关键字
// 没有精简的Lambda表达式代码
MoreParameterHasReturn obj7 = (int a, int b) -> {
return "运算的结果为:" + (a + b);
};
System.out.println(obj7.test(10, 20));
// 精简之后的Lambda表达式代码
MoreParameterHasReturn obj8 = (a, b) -> "运算的结果为:" + (a + b);
System.out.println(obj8.test(20, 30));
}
}
四个基本的函数式接口
名字 | 接口名 | 对应的抽象方法 |
---|---|---|
消费 | Consumer | void accept(T t); |
生产 | Supplier | T get(); |
转换 | Function<T, R> | R apply(T t); |
判断 | Predicate | boolean test(T t); |
以上的函数式接口都在java.util.function包中,通常函数接口出现的地方都可以使用Lambda表达式,所以不必记忆函数接口的名字,这些函数式接口及子接口在后续学习中很常用。
Lambda表达式的方法引用
方法引用的概述
我们在使用Lambda表达式的时候,如果Lambda表达式的方法体中除了调用现有方法之外什么都不做,满足这样的条件就有机会使用方法引用来实现。
在以下的代码中,在重写的apply()方法中仅仅只调用了现有Math类round()方法,也就意味着Lambda表达式中仅仅只调用了现有Math类round()方法,那么该Lambda表达式就可以升级为方法引用,案例如下:
// 需求:实现小数取整的操作
// 方式一:使用匿名对象来实现
Function<Double, Long> function1 = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(function1.apply(3.14));
// 方式二:使用Lambda表达式来实现
Function<Double, Long> function2 = aDouble -> Math.round(aDouble);
System.out.println(function2.apply(3.14));
// 方式三:使用方法引用来实现
Function<Double, Long> function3 = Math :: round;
System.out.println(function3.apply(3.14));
对于方法引用,我们可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖。
在Lambda表达式的方法引用中,主要有实例方法引用、静态方法引用、特殊方法引用和构造方法引用、数组引用这五种情况,接下来我们就对这五种情况进行讲解。
实例方法引用
语法:对象 :: 实例方法
特点:在Lambda表达式的方法体中,通过“对象”来调用指定的某个“实例方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过对象调用某个实例方法的返回值类型和形参列表 保持一致。
【示例】实例化Consumer接口的实现类对象,并在重写的accept()方法中输出形参的值
// 方式一:使用匿名内部类来实现
Consumer<String> consumer1 = new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
consumer1.accept("hello world");
// 方式二:使用Lambda表达式来实现
Consumer<String> consumer2 = str -> System.out.println(str);
consumer2.accept("hello world");
// 方式三:使用方法引用来实现
Consumer<String> consumer3 = System.out :: println;
consumer3.accept("hello world");
【示例】实例化Supplier接口的实现类对象,并在重写方法中返回Teacher对象的姓名
Teacher teacher = new Teacher("ande", 18);
// 方式一:使用匿名内部类来实现
Supplier<String> supplier1 = new Supplier<String>() {
@Override
public String get() {
return teacher.getName();
}
};
System.out.println(supplier1.get());
// 方式二:使用Lambda表达式来实现
Supplier<String> supplier2 = () -> teacher.getName();
System.out.println(supplier2.get());
// 方式三:使用方法引用来实现
Supplier<String> supplier3 = teacher :: getName;
System.out.println(supplier3.get());
静态方法引用
语法:类 :: 静态方法
特点:在Lambda表达式的方法体中,通过“类名”来调用指定的某个“静态方法”。
要求:函数式接口中抽象方法的返回值类型和形参列表 与 内部通过类名调用某个静态方法的返回值类型和形参列表保持一致。
【示例】实例化Function接口的实现类对象,并在重写的方法中返回小数取整的结果
// 方式一:使用匿名内部类来实现
Function<Double, Long> function1 = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
System.out.println(function1.apply(3.14));
// 方式二:使用Lambda表达式来实现
Function<Double, Long> function2 = aDouble -> Math.round(aDouble);
System.out.println(function2.apply(3.14));
// 方式三:使用方法引用来实现
Function<Double, Long> function3 = Math :: round;
System.out.println(function3.apply(3.14));
特殊方法引用
语法:类名 :: 实例方法
特点:在Lambda表达式的方法体中,通过方法的第一个形参来调用指定的某个“实例方法”。
要求:把函数式接口中抽象方法的第一个形参作为方法的调用者对象,并且从第二个形参开始(或无参)可以对应到被调用实例方法的参数列表中,并且返回值类型保持一致。
【示例】使用Comparator比较器,来判断两个小数的大小
// 方式一:使用匿名内部类来实现
Comparator<Double> comparator1 = new Comparator<Double>() {
@Override
public int compare(Double o1, Double o2) {
return o1.compareTo(o2);
}
};
System.out.println(comparator1.compare(10.0, 20.0));
// 方式二:使用Lambda表达式来实现
Comparator<Double> comparator2 = (o1, o2) -> o1.compareTo(o2);
System.out.println(comparator2.compare(10.0, 20.0));
// 方式三:使用方法引用来实现
Comparator<Double> comparator3 = Double :: compareTo;
System.out.println(comparator3.compare(10.0, 20.0));
需求:实例化Function接口的实现类对象,然后获得传入Teacher对象的姓名。
// 方式一:使用匿名内部类来实现
Teacher teacher = new Teacher("ande", 18);
Function<Teacher, String> function1 = new Function<Teacher, String>() {
@Override
public String apply(Teacher teacher) {
return teacher.getName();
}
};
System.out.println(function1.apply(teacher));
// 方式二:使用Lambda表达式来实现
Function<Teacher, String> function2 = e -> e.getName();
System.out.println(function2.apply(teacher));
// 方式三:使用方法引用来实现
Function<Teacher, String > function3 = Teacher :: getName;
System.out.println(function3.apply(teacher));
构造方法引用
语法:类名 :: new
特点:在Lambda表达式的方法体中,返回指定“类名”来创建出来的对象。
要求:创建对象所调用构造方法形参列表 和 函数式接口中的方法的形参列表 保持一致,并且方法的返回值类型和创建对象的类型保持一致。
【示例】实例化Supplier接口的实现类对象,然后调用重写方法返回Teacher对象
// 方式一:使用匿名内部类来实现
Supplier<Teacher> supplier1 = new Supplier<Teacher>() {
@Override
public Teacher get() {
return new Teacher();
}
};
System.out.println(supplier1.get());
// 方式二:使用Lambda表达式来实现
Supplier<Teacher> supplier2 = () -> new Teacher();
System.out.println(supplier2.get());
// 方式二:使用构造方法引用来实现
// 注意:根据重写方法的形参列表,那么此处调用了Teacher类的无参构造方法
Supplier<Teacher> supplier3 = Teacher :: new;
System.out.println(supplier3.get());
【示例】实例化Function接口的实现类对象,然后调用重写方法返回Teacher对象
// 方式一:使用匿名内部类来实现
Function<String, Teacher> function1 = new Function<String, Teacher>() {
@Override
public Teacher apply(String name) {
return new Teacher(name);
}
};
System.out.println(function1.apply("ande"));
// 方式二:使用Lambda表达式来实现
Function<String, Teacher> function2 = name -> new Teacher(name);
System.out.println(function2.apply("ande"));
// 方式二:使用构造方法引用来实现
// 注意:根据重写方法的形参列表,那么此处调用了Teacher类name参数的构造方法
Function<String, Teacher> function3 = Teacher :: new;
System.out.println(function3.apply("ande"));
数组引用
语法:数组类型 :: new
特点:在Lambda表达式的方法体中,创建并返回指定类型的“数组”。
要求:重写的方法有且只有一个整数型的参数,并且该参数就是用于设置数组的空间长度,并且重写方法的返回值类型和创建数组的类型保持一致。
【示例】实例化Function接口的实现类对象,并在重写方法中返回指定长度的int类型数组
// 方式一:使用匿名内部类来实
Function<Integer, int[]> function1 = new Function<Integer, int[]>() {
@Override
public int[] apply(Integer integer) {
return new int[integer];
}
};
System.out.println(Arrays.toString(function1.apply(10)));
// 方式二:使用Lambda表达式来实现
Function<Integer, int[]> function2 = num -> new int[num];
System.out.println(Arrays.toString(function2.apply(20)));
// 方式三:使用方法引用来实现
Function<Integer, int[]> function3 = int[] :: new;
System.out.println(Arrays.toString(function3.apply(30)));
Lambda在集合当中的使用
为了能够让Lambda和Java的集合类集更好的一起使用,集合当中也新增了部分方法,以便与Lambda表达式对接,要用Lambda操作集合就一定要看懂源码。
forEach()方法
在Collection集合和Map集合中,都提供了forEach()方法用于遍历集合。
在Collection集合中,提供的forEach()方法的形参为Consumer接口(消费型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】遍历List集合中的元素
List<Integer> list = Arrays.asList(11, 22, 33, 44, 55);
// 方式一:使用匿名内部类来实现
list.forEach(new Consumer<Integer>() {
/**
* 获得遍历出来的元素
* @param element 遍历出来的元素
*/
@Override
public void accept(Integer element) {
System.out.println(element);
}
});
// 方式二:使用Lambda表达式来实现
list.forEach(element -> System.out.println(element));
// 方式三:使用方法引用来实现
list.forEach(System.out :: println);
【示例】遍历Set集合中的元素
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
HashSet<String> hashSet = new HashSet<>(list);
// 方式一:使用匿名内部类来实现
hashSet.forEach(new Consumer<String>() {
/**
* 获得遍历出来的元素
* @param element 遍历出来的元素
*/
@Override
public void accept(String element) {
System.out.println(element);
}
});
// 方式二:使用Lambda表达式来实现
hashSet.forEach(element -> System.out.println(element));
// 方式三:使用方法引用来实现
hashSet.forEach(System.out :: println);
在Map集合中,提供的forEach()方法的形参为BiConsumer接口,而BiConsumer接口属于两个参数的消费型接口,通过该方法再配合Lambda表达式就可以遍历Map集合中的元素。
【示例】遍历Map集合中的元素
// 实例化Map集合并添加键值对
HashMap<String, String> map = new HashMap<>();
map.put("张三", "成都");
map.put("李四", "重庆");
map.put("王五", "西安");
// 方式一:使用匿名内部类来实现
map.forEach(new BiConsumer<String, String>() {
/**
* 获得遍历出来的key和value
* @param key 键
* @param value 值
*/
@Override
public void accept(String key, String value) {
System.out.println("key:" + key + ",value:" + value);
}
});
// 方式二:使用Lambda表达式来实现
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));
removeIf()方法
在Collection集合中,提供的removeIf()方法的形参为Predicate接口(判断型接口),通过该方法再配合Lambda表达式就可以遍历List和Set集合中的元素。
【示例】删除List集合中的某个元素
// 创建List集合并添加元素
List<String> list = new ArrayList<>(Arrays.asList("aa", "bb", "cc", "dd"));
// 方式一:使用匿名内部类来实现
list.removeIf(new Predicate<String>() {
/**
* 删除指定的某个元素
* @param element 用于保存遍历出来的某个元素
* @return 返回true,代表删除;返回false,代表不删除
*/
@Override
public boolean test(String element) {
return "bb".equals(element);
}
});
System.out.println(list); // 输出:[aa, cc, dd]
// 方式二:使用Lambda表达式来实现
list.removeIf("cc" :: equals);
System.out.println(list); // 输出:[aa, dd]
【示例】删除Set集合中的某个元素
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
HashSet<String> hashSet = new HashSet<>(list);
// 方式一:使用匿名内部类来实现
hashSet.removeIf(new Predicate<String>() {
/**
* 删除指定的某个元素
* @param element 用于保存遍历出来的某个元素
* @return 返回true,代表删除;返回false,代表不删除
*/
@Override
public boolean test(String element) {
return "bb".equals(element);
}
});
System.out.println(hashSet); // 输出:[aa, cc, dd]
// 方式二:使用Lambda表达式来实现
hashSet.removeIf("cc" :: equals);
System.out.println(hashSet); // 输出:[aa, dd]
第十四章 Stream API
主要内容
- Stream API的概述
- 创建Stream的方式
- Stream的中间操作
- Stream的终止操作
学习目标
知识点 | 要求 |
---|---|
Stream API的概述 | 理解 |
创建Stream的方式 | 掌握 |
Stream的中间操作 | 掌握 |
Stream的终止操作 | 掌握 |
Stream API的概述
什么是StreamAPI呢? (JAVA 流式编程)
从JDK1.8开始,Java语言引入了一个全新的流式Stream API,StreamAPI把真正的函数式编程风格运用到Java语言中,使用StreamAPI可以帮我们更方便地操作集合,允许开发人员在不改变原始数据源的情况下对集合进行操作,这使得代码更加简洁、易读和可维护。
使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询,也可以使用Stream API来并行执行的操作。简而言之,Stream API提供了一种高效且易于使用的处理数据的方式。
Stream和Collection的区别
Collection:是静态的内存数据结构,强调的是数据。
Stream API:是跟集合相关的计算操作,强调的是计算。
总结:Collection面向的是内存,存储在内存中;StreamAPI面向的是CPU,通过CPU来计算。
Stream API的操作步骤
- 第一步:创建Stream
- 通过数据源(如:集合、数组等)来获取一个Stream对象 。
- 第二步:中间操作
- 对数据源的数据进行处理,该操作会返回一个Stream对象,因此可以进行链式操作。
- 第三步:终止操作
- 执行终止操作时,则才会真正执行中间操作,并且并返回一个计算完毕后的结果。
Stream API的重要特点
- Stream自己不会存储元素,只能对元素进行计算。
- Stream不会改变数据对象,反而可能会返回一个持有结果的新Stream。
- Stream上的操作属于延迟执行,只有等到用户真正需要结果的时候才会执行。
- Stream一旦执行了终止操作,则就不能再调用其它中间操作或终止操作了。
创建 Stream的方式
通过Collection接口提供的方法
通过Collection接口提供的stream()方法来创建Stream流。
List<String> list = Arrays.asList("aa", "bb", "cc");
Stream<String> stream = list.stream();
通过Arrays类提供的方法
通过Arrays类提供的stream()静态方法来创建Stream流。
String[] arr1 = {"aa", "bb", "cc"};
Stream<String> stream = Arrays.stream(arr1);
int[] arr2 = {11, 22, 33, 44};
IntStream intStream = Arrays.stream(arr2);
long[] arr3 = {11, 22, 33, 44};
LongStream longStream = Arrays.stream(arr3);
double[] arr4 = {1.0, 2.0, 3.0};
DoubleStream doubleStream = Arrays.stream(arr4);
注意:Stream、IntStream、LongStream和DoubleStream都继承于BaseStream接口。
使用Stream接口提供的方法
通过Stream接口提供的of(T… values)静态方法来创建Stream流。
Stream<String> stringStream = Stream.of("aa", "bb", "cc");
Stream<Integer> integerStream = Stream.of(11, 22, 33, 44);
顺序流和并行流的理解
在前面获得Stream对象的方式,我们都称之为“顺序流”,顺序流对Stream元素的处理是单线程的,即一个一个元素进行处理,处理数据的效率较低。
如果Stream流中的数据处理没有顺序要求,并且还希望可以并行处理Stream的元素,那么就可以使用“并行流”来实现,从而提高处理数据的效率。
一个普通Stream转换为可以并行处理的Stream非常简单,只需要用调用Stream提供的parallel()方法进行转换即可,这样就可以并行的处理Stream的元素。那么,我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
【示例】把顺序流转化为并行流
// 创建一个“顺序流”Stream对象
Stream<String> stream = Stream.of("aa", "bb", "cc");
// 验证:stream是否为并行流
System.out.println(stream.isParallel()); // 输出:false
// 将Stream对象转化为“并行流”
// 注意:parallel()方法返回的就是“方法的调用者对象”
Stream<String> parallelStream = stream.parallel();
System.out.println(stream == parallelStream); // 输出:true
// 验证:stream是否为并行流
System.out.println(stream.isParallel()); // 输出:true
在Collection接口中,还专门提供了一个parallelStream()方法,用于获得一个并行流。
【示例】使用parallelStream()方法获得一个并行流
List<String> list = Arrays.asList("aa", "bb", "cc");
// 创建一个“并行流”Stream对象
Stream<String> stream = list.parallelStream();
// 验证:stream是否为并行流
System.out.println(stream.isParallel()); // 输出:true
Stream API的中间操作
中间操作属于惰式执行,直到执行终止操作才会真正的进行数据的计算,此处调用中间操作只会返回一个标记了该操作的新Stream对象,因此可以进行链式操作。
在后续的操作中,我们调用StudentData类的getStudentList()静态方法,则就能获得一个存储Student对象的List集合,其代码实现如下:
public class Student {
private String name;
private int age;
private String sex;
private String city;
public Student() {}
public Student(String name, int age, String sex, String city) {
this.name = name;
this.age = age;
this.sex = sex;
this.city = city;
}
/*setter和getter方法省略*/
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", city='" + city + '\'' +
'}';
}
}
public class StudentData {
/**
* 获得一个存储Student对象的List集合
*/
public static List<Student> getStudentList() {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("张三", 21, "男", "武汉"));
list.add(new Student("李四", 18, "女", "重庆"));
list.add(new Student("王五", 25, "女", "成都"));
list.add(new Student("赵六", 22, "男", "武汉"));
list.add(new Student("王麻子", 16, "女", "成都"));
return list;
}
}
筛选(filter)
筛选(filter),按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。该操作使用了Stream接口提供的“Stream filter(Predicate<? super T> predicate);”方法来实现。
【示例】使用筛选的案例
// 需求:筛选出年龄大于20的学生对象
Stream<Student> stream1 = StudentData.getStudentList().stream();
stream1.filter(stu -> stu.getAge() > 20).forEach(System.out :: println);
// 需求:筛选出字符串长度大于3的元素
Stream<String> stream2 = Stream.of("hello", "too", "like", "ande");
stream2.filter(str -> str.length() > 3).forEach(System.out :: println);
映射(map)
映射(map),将一个流的元素按照一定的映射规则映射到另一个流中。该操作使用了Stream接口提供的“ Stream map(Function<? super T, ? extends R> mapper);”方法来实现。
【示例】使用映射的案例
// 需求:把字符串中的字母全部转化为大写
Stream<String> stream1 = Stream.of("hello", "too", "like", "ande");
// stream1.map(str -> str.toUpperCase()).forEach(System.out :: println);
stream1.map(String :: toUpperCase).forEach(System.out :: println);
// 需求:获得集合中所有学生的名字
Stream<Student> stream2 = StudentData.getStudentList().stream();
// stream2.map(stu -> stu.getName()).forEach(System.out :: println);
stream2.map(Student :: getName).forEach(System.out :: println);
// 需求:获得集合中性别为男的学生名字
// 思路:先筛选,后映射
Stream<Student> stream3 = StudentData.getStudentList().stream();
stream3.filter(stu -> stu.getSex().equals("男")).map(Student :: getName).forEach(System.out :: println);
在Stream接口中,可以实现“将多个集合中的元素映射到同一个流中”,该操作使用了Stream接口提供的“ Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);”方法来实现。
【示例】将多个集合中的元素映射到同一个流中
// 需求:将两个集合中的元素映射到同一个流中
List<String> list1 = new ArrayList<>();
list1.add("aa");
list1.add("bb");
list1.add("cc");
List<String> list2 = new ArrayList<>();
list2.add("dd");
list2.add("ee");
list2.add("ff");
Stream<List<String>> stream = Stream.of(list1, list2);
stream.flatMap(List<String>::stream).forEach(System.out::println);
除重(distinct)
除重(distinct),也就是除去重复的元素,底层使用了hashCode()和equals(Object obj)方法来判断元素是否相等。该操作使用了Stream接口提供的“Stream distinct();”方法来实现。
【示例】演示除重的操作
// 需求:除去重复的元素
Stream.of(11, 22, 33, 44, 33).distinct().forEach(System.out :: println);
// 需求:除去重复的学生(除重后输出学生对象)
StudentData.getStudentList().stream().distinct().forEach(System.out :: println);
// 需求:除去年龄相同的学生(除重后输出学生年龄)
// 思路:先映射,后除重
StudentData.getStudentList().stream().map(Student :: getAge).distinct().forEach(System.out :: println);
排序(sorted)
排序(sorted),也就是对元素执行“升序”或“降序”的排列操作。在Stream接口中提供了“Stream sorted();”方法,专门用于对元素执行“自然排序”,使用该方法则元素对应的类就必须实现Comparable接口。
【示例】使用自然排序的案例
// 需求:对元素执行“升序”排序
Stream.of(4, 1, 3, 6, 2, 5).sorted().forEach(System.out :: println);
// 需求:按照学生的年龄执行“升序”排序(排序后输出学生对象)
StudentData.getStudentList().stream().sorted().forEach(System.out :: println);
// 需求:按照学生的年龄执行“升序”排序(排序后输出学生年龄)
StudentData.getStudentList().stream().map(Student :: getAge).sorted().forEach(System.out :: println);
在Stream接口中还提供了“Stream sorted(Comparator<? super T> comparator);”方法,专门用于对元素执行“指定排序”,这样就能对某一个类设置多种排序规则。
【示例】使用指定排序的案例
// 需求:对元素执行“升序”排序
Stream.of(4, 1, 3, 6, 2, 5).sorted(Integer :: compare).forEach(System.out :: println);
// 需求:按照学生的年龄执行“降序”排序(排序后输出学生对象)
StudentData.getStudentList().stream().sorted((stu1, stu2) -> stu2.getAge() - stu1.getAge()).forEach(System.out :: println);
// 需求:按照学生的年龄执行“升序”排序(排序后输出学生年龄)
StudentData.getStudentList().stream().map(Student :: getAge).sorted(Integer :: compare).forEach(System.out :: println);
合并(concat)
合并(concat),也就是将两个Stream合并为一个Stream,此处使用Stream接口提供的“public static Stream concat(Stream<? extends T> a, Stream<? extends T> b)”静态方法来实现。
【示例】将两个Stream合并为一个Stream。
Stream<String> stream1 = Stream.of("aa", "bb", "cc");
Stream<String> stream2 = Stream.of("11", "22", "33");
Stream.concat(stream1, stream2).forEach(System.out :: println);
截断和跳过
跳过(skip),指的就是跳过n个元素开始操作,此处使用Stream接口提供的“Stream skip(long n);”方法来实现。
截断(limit),指的是截取n个元素的操作,此处使用Stream接口提供的“Stream limit(long maxSize);”方法来实现。
【示例】从指定位置开始截取n个元素
// 需求:从索引为2的位置开始截取3个元素
Stream.of(11, 22, 33, 44, 55, 66).skip(2).limit(3).forEach(System.out :: println);
Stream API的终止操作
触发终止操作时才会真正执行中间操作,终止操作执行完毕会返回计算的结果,并且终止操作执行完毕那么操作的Stream就失效,也就是不能再执行中间操作或终止操作啦。
遍历(forEach)
遍历(forEach),使用Stream接口提供的“void forEach(Consumer<? super T> action);”方法来遍历计算的结果。
【示例】遍历操作的案例
List<Student> list = StudentData.getStudentList();
// 遍历所有的元素
list.stream().forEach(System.out :: println);
// 遍历学生年龄大于20的元素
list.stream().filter(stu -> stu.getAge() > 20).forEach(System.out :: println);
匹配(match)
匹配(match),就是判断Stream中是否存在某些元素,Stream接口提供的匹配方法如下:
- boolean allMatch(Predicate<? super T> predicate); 检查是否匹配所有的元素
- boolean anyMatch(Predicate<? super T> predicate); 检查是否至少匹配一个元素
- boolean noneMatch(Predicate<? super T> predicate); 检查是否一个元素都不匹配
- Optional findFirst(); 获得第一个元素
注意:此处的Optional是一个值的容器,可以通过get()方法获得容器的值。
【示例】匹配操作的案例
List<Student> list = StudentData.getStudentList();
// 需求:匹配学生名字是否都为“王五”
boolean all = list.stream().allMatch(stu -> stu.getName().equals("王五"));
System.out.println("检查是否匹配所有的元素:" + all);
// 需求:匹配学生名字是否至少有一个为“王五”
boolean any = list.stream().anyMatch(stu -> stu.getName().equals("王五"));
System.out.println("检查是否至少匹配一个元素:" + any);
// 需求:匹配学生名字中是否全部都没有“王五”
boolean none = list.stream().noneMatch(stu -> stu.getName().equals("王五"));
System.out.println("检查是否一个元素都不匹配:" + none);
// 需求:获得第一个学生
Student firstStu = list.stream().findFirst().get();
System.out.println(firstStu);
// 需求:获得第四个学生
// 思路:跳过前面3个学生,然后再获得第一个元素
Optional<Student> skipStu = list.stream().skip(3).findFirst();
System.out.println(skipStu);
归约(reduce)
归约(reduce),将所有元素按照指定的规则合并成一个结果。在Stream接口中,常用的归约方法如下:
- Optional reduce(BinaryOperator accumulator);
- T reduce(T identity, BinaryOperator accumulator);
【示例】归约操作的案例
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 需求:获得集合中所有元素“相加”的结果
// Integer sum = list.stream().reduce((x, y) -> x + y).get();
Integer sum = list.stream().reduce(Integer :: sum).get();
System.out.println(sum);
// 需求:获得集合中所有元素“相乘”的结果
Integer result = list.stream().reduce((x, y) -> x * y).get();
System.out.println(result);
// 需求:获得最大长度的元素
String str = Stream.of("I", "love", "you", "too").reduce((str1, str2) -> str1.length() > str2.length() ? str1 : str2).get();
System.out.println(str);
// 需求:获得所有学生的总年龄
Integer sumAge = StudentData.getStudentList().stream().map(Student::getAge).reduce((age1, age2) -> age1 + age2).get();
System.out.println(sumAge);
// 需求:获得10和集合中所有元素“相加”的结果
Integer sum1 = list.stream().reduce(10, Integer :: sum);
System.out.println(sum1);
reduce操作可以实现从一组元素中生成一个值,而max()、min()、count()等方法都属于reduce操作,将它们单独设为方法只是因为常用,在Stream接口中这些方法如下:
long count(); 获得元素的个数
Optional max(Comparator<? super T> comparator); 获得最大的元素
Optional min(Comparator<? super T> comparator); 获得最小的元素
【示例】获得最大、最小和元素的个数
List<Student> list = StudentData.getStudentList();
// 需求:获得元素的个数
long count = StudentData.getStudentList().stream().count();
System.out.println(count);
// 需求:获得年龄“最大”的学生
Student maxStu = list.stream().max((stu1, stu2) -> stu1.getAge() - stu2.getAge()).get();
System.out.println(maxStu);
// 需求:获得学生的“最大”年龄
Integer maxAge = list.stream().map(Student::getAge).max(Integer::compare).get();
System.out.println(maxAge);
// 需求:获得年龄“最小”的学生
Student minStu = list.stream().min((stu1, stu2) -> stu1.getAge() - stu2.getAge()).get();
System.out.println(minStu);
// 需求:获得学生的“最小”年龄
Integer minAge = list.stream().map(Student::getAge).min(Integer::compare).get();
System.out.println(minAge);
收集(collect)
收集(collect),可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。
调用Stream接口提供的“<R, A> R collect(Collector<? super T, A, R> collector);”方法来实现收集操作,并且参数中的Collector对象大都是直接通过Collectors工具类获得,实际上传入的Collector决定了collect()的行为。
归集(toList/toSet/toMap)
因为Stream流不存储数据,那么在Stream流中的数据完成处理后,如果需要把Stream流的数据存入到集合中,那么就需要使用归集的操作。在Collectors提供的toList、toSet和toMap比较常用,另外还有Collectors提供的toCollection等比较复杂一些的用法。
【示例】演示toList、toSet和toMap的实现
List<String> stringList = Arrays.asList("I", "love", "you", "too");
// 需求:将Stream转化为List集合
List<String> list = stringList.stream().collect(Collectors.toList());
System.out.println(list);
// 需求:将Stream转化为Set集合
Set<String> set = stringList.stream().collect(Collectors.toSet());
System.out.println(set);
// 需求:将Stream转化为Map集合
// 明确:每个元素以“:”来分割,左边的为key,右边的为value
Stream<String> stream = Stream.of("张三:成都", "李四:武汉", "王五:重庆");
Map<String, String> map = stream.collect(Collectors.toMap(str -> str.substring(0, str.indexOf(":")), str -> str.substring(str.indexOf(":") + 1)));
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));
在以上的代码中,我们将Stream流中计算的数据转化为List和Set集合时,此时并没有明确存储数据对应集合的具体类型,想要明确存储数据对应集合的具体类型,则就需要使用toCollection来实现。
【示例】演示toCollection的实现
List<String> list = Arrays.asList("I", "love", "you", "too");
// 需求:将Stream转化为ArrayList集合
ArrayList<String> arrayList = list.stream().collect(Collectors.toCollection(ArrayList::new));
System.out.println(arrayList);
// 需求:将Stream转化为LinkedList集合
LinkedList<String> linkedList = list.stream().collect(Collectors.toCollection(LinkedList::new));
System.out.println(linkedList);
// 需求:将Stream转化为HashSet集合
HashSet<String> hashSet = list.stream().collect(Collectors.toCollection(HashSet::new));
System.out.println(hashSet);
// 需求:将Stream转化为TreeSet集合
TreeSet<String> treeSet = list.stream().collect(Collectors.toCollection(TreeSet::new));
System.out.println(treeSet);
【示例】获得年龄大于20岁的女同学,然后返回按照年龄进行升序排序后的List集合
List<Student> list = StudentData.getStudentList();
ArrayList<Student> arrayList =
list.stream().filter(stu -> stu.getAge() > 18). // 过滤年龄小于等于18的学生
filter(stu -> stu.getSex().equals("女")). // 过滤男性学生
sorted(Comparator.comparing(Student::getAge)). // 按照年龄执行升序排序
collect(Collectors.toCollection(ArrayList::new)); // 转化为ArrayList存储
arrayList.forEach(System.out :: println);
在归集的知识点中,我们实现了将Stream中计算的数据转化为集合或Map,那么能否将Stream中计算的数据转化为数组呢?答案是可以的,我们可以使用Stream提供的toArray静态方法来实现。
【示例】将Stream中计算的数据转化为数组
List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
// 需求:将Stream转化为数组
Object[] array = list.stream().toArray();
System.out.println(Arrays.toString(array));
// 需求:将Stream转化为“指定类型”的数组
String[] stringArray = list.stream().toArray(String[]::new);
System.out.println(Arrays.toString(stringArray));
统计(counting/averaging)
Collectors提供了一系列用于数据统计的静态方法:
- 计数:counting
- 平均值:averagingInt、averagingLong、averagingDouble
- 最值:maxBy、minBy
- 求和:summingInt、summingLong、summingDouble
- 统计以上所有:summarizingInt、summarizingLong、summarizingDouble
【示例】对学生的年龄进行统计
List<Student> list = StudentData.getStudentList();
// 需求:获得元素的个数
Long count = list.stream().collect(Collectors.counting());
System.out.println(count);
// 需求:获得学生的平均年龄
Double averAge = list.stream().collect(Collectors.averagingDouble(Student::getAge));
System.out.println(averAge);
// 需求:获得最大年龄的学生
Student stu = list.stream().collect(Collectors.maxBy((stu1, stu2) -> stu1.getAge() - stu2.getAge())).get();
System.out.println(stu);
// 需求:获得所有学生年龄之和
Long sum = list.stream().collect(Collectors.summingLong(Student::getAge));
System.out.println(sum);
// 需求:获得年龄的所有的信息
IntSummaryStatistics collect = list.stream().collect(Collectors.summarizingInt(Student::getAge));
System.out.println(collect);
分组(groupingBy)
分组(groupingBy),将Stream按条件分为两个Map,比如按照学生年龄分为两个Map集合。
【示例】按照学生性别分为两个Map集合
List<Student> list = StudentData.getStudentList();
// 需求:按照学生性别进行分组
Map<String, List<Student>> map = list.stream().collect(Collectors.groupingBy(Student::getSex));
map.forEach((k, v) -> System.out.println("key:" + k + ",value:" + v));
接合(joining)
接合(joining),把Stream计算的数据按照一定的规则进行拼接。
【示例】获得所有学生的名字拼接成一个字符
List<Student> list = StudentData.getStudentList();
// 需求:将所有学生的姓名连接成一个字符串,每个名字之间以“,”连接
String allName = list.stream().map(Student::getName).collect(Collectors.joining(", "));
System.out.println(allName);
第十五章 Java新特性概述
主要内容
- 新特性的概述
- 新语法的结构
- API层面的变化
学习目标
知识点 | 要求 |
---|---|
新特性的概述 | 了解 |
新语法方面的变化 | 理解 |
API层面的变化 | 理解 |
新特性的概述
纵观Java这几年的版本变化,在Java被收入Oracle之后,Java以小步快跑的迭代方式,在功能更新上迈出了更加轻快的步伐。基于时间发布的版本,可以让Java研发团队及时获得开发人员的反馈,因此可以看到最近的Java版本,有很多语法层面简化的特性。同时,Java在支持容器化场景,提供低延迟的GC方面(ZGC等)也取得了巨大的进步。
注意一个新特性的出现通常会经过以下阶段:
- 孵化器(Incubator)阶段:这是新特性最早的开发和试验阶段,此时新特性只能作为一个单独的模块或库出现,而不会包含在Java SE中。在这个阶段,特性的设计可能会有些不稳定,而且会经常调整和变更。
- 预览(Preview)阶段:在经过了孵化器阶段的验证和修改后,新特性进入了预览阶段,这是一种在Java SE内部实现的,开发人员可以使用并对其提供反馈的渠道。此时特性可能被包含在Java SE版本中,但是它默认是未开启的,需要通过特定的命令行参数或其他方式进行启用。
- 正式版(GA)阶段:在经过了预览阶段的反复测试和修复后,新特性最终会在Java SE的稳定版本中发布。此时,特性被默认开启,成为Java SE的一部分,并可以在各个Java应用程序中使用。
需要注意的是,上述阶段并非一成不变,并不是所有JEP(Java Enhancement Proposal:Java增强方案)都需要经过孵化器阶段和预览阶段,这取决于特定的提案和规划。但是,Java SE领导小组通常会遵循这些阶段的流程,以确保新特性可以经过充分的评估和测试,以便能够稳定和可靠地使用在Java应用程序中。
在以下的内容中,我们对Java9到Java21新特性做一个简单的概述。
Java9新特性
Java9经过4次推迟,历经曲折的Java9最终在2017年9月21日发布,提供了超过150项新功能特性。
- JEP 261: Module System
- JDK 9 开始引入的一种全新的模块化编程方式。JPMS 的目的是为了更好地支持大型应用程序的开发和维护,同时也可以使 Java 程序在更为动态、可移植和安全的环境下运行。
- JEP 222: jshell: The Java Shell (Read-Eval-Print Loop)
- 一种交互式的 Java Shell,可以在命令行上快速地进行 Java 代码的编写、验证和执行,从而提高开发者的生产力。
- JEP 213: Milling Project Coin(细化工程改进,该计划旨在引入小型语言特性来提高代码的简洁性和可读性)
- 在Java 9中,@SafeVarargs注解可以用于一个私有实例方法上。在Java 7和Java 8中,@SafeVarargs注解只能用于静态方法、final实例方法和构造函数。
- 在Java 9中,可以将效果等同于final变量作为try-with-resources语句块中的资源来使用。在Java 7/8中,try-with-resources语句块中的资源必须是显式的final或事实上的final(即变量在初始化后未被修改),否则编译器会报错。这个限制限制了Java程序员使用try-with-resources语句块的能力,特别是在涉及lambda表达式、匿名类或其他读取外部变量的代码段时。
- Java 9允许在匿名类实例化时使用钻石操作符(<>)来简化代码,但参数类型必须是具体的、可推导的类型。
- 从Java9开始,不能使用一个单一的“_”作为标识符了。
- 从Java9开始,接口中支持定义私有方法。
- JEP 224: HTML5 Javadoc
- 从Java9开始,javadoc开始支持HTML5的语法。
- JEP 254: Compact Strings
- 一种新的字符串表示方式,称为紧凑型字符串,以提高Java应用程序的性能和内存利用率。通过String源码得知:char[] 变成了 byte[]。
- JEP 269: Convenience Factory Methods for Collections
- 更加方便的创建只读集合:List.of(“abc”, “def”, “xyz”);
- JEP 269:对Stream API进行了增强
- 其中最显著的是引入了四个新的方法,分别是
takeWhile()
,dropWhile()
,ofNullable()
和iterate()
- 其中最显著的是引入了四个新的方法,分别是
- JEP 110:一个新的HTTP客户端API,名为HttpClient,它是一种基于异步和事件驱动的方式,更加高效和灵活的HTTP客户端。
Java10新特性
2018年3月21日,Oracle官方宣布JAVA10正式发布。JAVA10一共定义了109个新特性,其中包含JEP,对开发人员来说,真正的新特性也就一个,还有一些新的API和JVM规范以及JAVA语言规范上的改动。
- JEP 286:局部变量类型推断
- JEP 296:将 JDK 森林合并到单个存储库中
- JEP 304:垃圾收集器接口
- JEP 307:G1 的并行完整 GC
- JEP 310:应用程序类数据共享
- JEP 312:线程局部握手
- JEP 313:删除本机头生成工具 (javah)
- JEP 314:附加 Unicode 语言标签扩展
- JEP 316:替代内存设备上的堆分配
- JEP 317:基于 Java 的实验性 JIT 编译器
- JEP 319:根证书
- JEP 322:基于时间的发布版本控制
Java11新特性
2018年9月26日,Oracle官方发布JAVA11。这是JAVA大版本周期变化后的第一个长期支持版本,官方支持到2026年。
- JEP 181:基于 Nest 的访问控制
- JEP 309:动态类文件常量
- JEP 315:改进 Aarch64 内部函数
- JEP 318:Epsilon:无操作垃圾收集器
- JEP 320:删除 Java EE 和 CORBA 模块
- JEP 321:HTTP 客户端(标准)
- JEP 323:本地变量语法LAMBDA参数
- JEP 324:与Curve25519密钥协商和Curve448
- JEP 327:Unicode的10
- JEP 328:飞行记录器
- JEP 329:ChaCha20和Poly1305加密算法
- JEP 330:启动单文件源代码程序
- JEP 331:低开销堆纹
- JEP 332:传输层安全性 (TLS) 1.3
- JEP 333:ZGC:可扩展的低延迟垃圾收集器(实验性)
- JEP 335:弃用 Nashorn JavaScript 引擎
- JEP 336:弃用 Pack200 工具和 API
Java12新特性
2019年3月19日,java12正式发布。
- JEP 189:Shenandoah:一个低暂停时间的垃圾收集器(实验性)
- JEP 230:微基准套件
- JEP 325:switch表达式(预览)
- JEP 334:JVM 常量 API
- JEP 340:一个 AArch64 端口
- JEP 341:默认 CDS 档案
- JEP 344:G1 支持可中断的 Mixed GC
- JEP 346:及时从 G1 返回未使用的已提交内存
Java13新特性
- JEP 350:动态 CDS 档案
- JEP 351:ZGC:取消提交未使用的内存
- JEP 353:重新实现旧的 Socket API
- JEP 354:开关表达式(预览)
- JEP 355:文本块(预览)
Java14新特性
- JEP 305:instanceof 的模式匹配(预览)
- JEP 343:包装工具(孵化器)
- JEP 345:G1 的 NUMA 感知内存分配
- JEP 349:JFR 事件流
- JEP 352:非易失性映射字节缓冲区
- JEP 358:有用的空指针异常
- JEP 359:记录(预览)
- JEP 361: switch表达式(标准)
- JEP 362:弃用 Solaris 和 SPARC 端口
- JEP 363:删除并发标记清除 (CMS) 垃圾收集器
- JEP 364:macOS 上的 ZGC
- JEP 365:Windows 上的 ZGC
- JEP 366:弃用 ParallelScavenge + SerialOld GC 组合
- JEP 367:删除 Pack200 工具和 API
- JEP 368:文本块(第二次预览)
- JEP 370:外部内存访问 API(孵化器)
Java15新特性
- JEP 339:爱德华兹曲线数字签名算法 (EdDSA)
- JEP 360:密封类(预览)
- JEP 371:隐藏类
- JEP 372:删除 Nashorn JavaScript 引擎
- JEP 373:重新实现旧版 DatagramSocket API
- JEP 374:禁用和弃用偏向锁定
- JEP 375:instanceof 的模式匹配(第二次预览,无改动)
- JEP 377:ZGC:可扩展的低延迟垃圾收集器(确定正式版)
- JEP 378:文本块(确定正式版)
- JEP 379:Shenandoah:一个低暂停时间的垃圾收集器(确定正式版)
- JEP 381:删除 Solaris 和 SPARC 端口
- JEP 383:外内存访问API(第二孵化器)
- JEP 384:记录(第二次预览)
- JEP 385:弃用 RMI 激活以进行删除
Java16新特性
- JEP 338:Vector API(孵化器)
- JEP 347:启用 C++14 语言功能
- JEP 357:从 Mercurial 迁移到 Git
- JEP 369:迁移到 GitHub
- JEP 376:ZGC:并发线程栈处理
- JEP 380:Unix 域套接字通道
- JEP 386:Alpine Linux 端口
- JEP 387:弹性元空间
- JEP 388:Windows/AArch64 端口
- JEP 389:外链 API(孵化器)
- JEP 390:基于值的类的警告
- JEP 392:打包工具
- JEP 393:外内存访问API(第三孵化器)
- JEP 394:instanceof 的模式匹配
- JEP 395:记录
- JEP 396:默认情况下强封装JDK内部
- JEP 397:密封类(第二次预览)
Java17新特性
2021年9月14日,java17正式发布(LTS)。长期支持版,支持到2029年。Oracle 宣布,从JDK17开始,后面的JDK都全部免费提供。
- JEP 306:恢复始终严格的浮点语义
- JEP 356:增强型伪随机数发生器
- JEP 382:新的 macOS 渲染管线
- JEP 391:macOS/AArch64 端口
- JEP 398:弃用 Applet API 以进行删除
- JEP 403:强封装JDK内部
- JEP 406:switch模式匹配(预览)
- JEP 407:删除 RMI 激活
- JEP 409:密封类(正式确定)
- JEP 410:删除实验性 AOT 和 JIT 编译器
- JEP 411:弃用安全管理器以进行删除
- JEP 412:外部函数和内存 API(孵化器)
- JEP 414:Vector API(第二孵化器)
- JEP 415:上下文特定的反序列化过滤器
Java18新特性
2022年3月22日发布。非长期支持版本。
- JEP 400:从JDK18开始,UTF-8是Java SE API的默认字符集。
- JEP 408:从JDK18开始,引入了jwebserver这样一个简单的WEB服务器,它是一个命令工具。
- JEP 416:使用方法句柄重新实现核心反射
- JEP 418:互联网地址解析SPI
- JEP 413:Java API文档中的代码段(javadoc注释中使用括起来的代码段会原模原样的生成到帮助文档中)
- JEP 417:Vector API(第三孵化器)
- JEP 419:Foreign Function & Memory API(第二孵化器)
- JEP 420:switch 的模式匹配(第二次预览)
- JEP 421:Object中的finalize()方法被移除
Java19新特性
2022年9月20日发布。非长期支持的版本。直到 2023 年 3 月它将被 JDK 20 取代。
- JEP 425:虚拟线程(预览版)
- 一种新的线程模型,即虚拟线程;“虚拟线程” 指的是一种轻量级线程,可以通过 JVM 进行管理和调度,而不需要操作系统进行支持
- JEP 428:结构化并发(孵化器)
- 一组新的API和规范,用于优化并简化Java程序的并发编程
- JEP 405:Record模式 (预览版)
- JEP 427:switch语句中的模式匹配(第三次预览版)
- "switch语句中的模式匹配"表示该特性是针对 switch 语句的改进,可以使用模式匹配的方式处理 switch 语句中的分支
- JEP 424:外部函数和内存API(预览版)
- “外部函数”指的是在Java程序中调用非Java语言编写的函数,比如C/C++函数
- “内存API”指的是在Java程序中直接操作内存的API
- JEP 426:向量API(第四版孵化器)
- 一组专用于向量化处理的API,允许在Java程序中轻松高效地执行向量化计算
Java20新特性
2023年3月21日发布。非长期支持版本。直到 2023 年 9月它将被 JDK 21 取代。
- JEP 432: Record模式(第二次预览版)
- JEP 433: switch的模式匹配 (第四次预览版)
- JEP 434: 外部函数和内存API(第二次预览版)
- JEP 438: 向量API (第五版孵化器)
- JEP 429: Scoped Values (Incubator)
- JEP 436: 虚拟线程(第二次预览版)
- JEP 437: 结构化并发(第二版孵化器)
Java21新特性
2023年9月19日发布。长期支持版本。
- JEP 440:Record模式(正式确定)
- JEP 441:switch的模式匹配(正式确定)
- JEP 430:String Templates (Preview)
- JEP 443:Unnamed Patterns and Variables (Preview)
- JEP 445:Unnamed Classes and Instance Main Methods (Preview)
- JEP 444:Virtual Threads(正式确定)
- JEP 431:Sequenced Collections(正式确定)
- JEP 452:Key Encapsulation Mechanism API
- JEP 442:Foreign Function & Memory API (Third Preview)
- JEP 453:Structured Concurrency (Preview)
- JEP 446:Scoped Values (Preview)
- JEP 448:Vector API (Sixth Incubator)
- JEP 439:Generational ZGC
- JEP 451:Prepare to Disallow the Dynamic Loading of Agents
新语法方面的变化
jShell命令
jShell命令是Java9引进的新特性,像Python和Scala之类的语言早就有交互式编程环境REPL (read-evaluate-print-loop),以交互式的方式对语句和表达式进行求值。开发者只需要输入一些代码,就可以在编译前获得对程序的反馈。而之前的Java 版本要想执行代码,必须创建文件、声明类、提供测试方法方可实现。
我们打开DOS命令窗口,然后输入jshell,就能进入交互式编程环境REPL,如下图所示:
通过jShell命令,我们能够定义一些变量,并执行相关的运算操作,如下图所示:
通过jShell命令,我们能够定义方法,并执行调用方法的操作,如下图所示:
想要查看JShell提供的所有指令,则直接输入“/help”即可,如下图所示:
想要查看书写的所有代码,则直接输入“/list”指令即可,如下图所示:
想要查看定义的所有变量,则直接输入“/vars”指令即可,如下图所示:
想要查看定义的所有方法,则直接输入“/methods”指令即可,如下图所示:
想要将输入的历史代码片段保存到文件中,就需要使用“ /save”指令,如下图所示:
try-with-resources
众所周知,所有被打开的系统资源,比如流、文件、Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。
在Java7以前,我们想要关闭资源就必须的finally代码块中完成。
【示例】Java7之前资源的关闭的方式
public void copyFile1(File srcFile, File destFile) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 实例化IO流(输入流和输出流)
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
// 拷贝文件(存储和读取)
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭资源
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Java7及以后关闭资源的正确姿势:try-with-resource,该语法格式为:
try(/*实例化需要关闭资源的对象或引用需要关闭资源的对象*/){
// 书写可能出现异常的代码
} catch(Exception e) {
// 处理异常
}
使用try-with-resource来自动关闭资源,则需要关闭资源的对象对应的类就必须实现java.lang.AutoCloseable接口,该接口中提供了一个close()的抽象方法,而自动关闭资源默认调用的就是实现于java.lang.AutoCloseable接口中的close()方法。
因为FileInputStream类和FileOutputStream类都属于java.lang.AutoCloseable接口的实现类,因此此处文件拷贝的操作就可以使用try-with-resource来自动关闭资源。
【示例】Java7之后资源的关闭的方式
public void copyFile(File srcFile, File destFile) {
// 实例化IO流(输入流和输出流)
try (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile)) {
// 拷贝文件(存储和读取)
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
通过try-with-resource来关闭放资源,即使资源很多,代码也可以写的很简洁,如果用Java7之前的方式去关闭资源,那么资源越多,用finally关闭资源时嵌套也就越多。
在Java9之后,为了避免在try后面的小括号中去实例化很多需要关闭资源的对象(复杂),则就可以把需要关闭资源的多个对象在try之前实例化,然后在try后面的小括号中引用需要关闭资源的对象即可,从而提高了代码的可读性。
【示例】Java9之后的使用方式
public void copyFile(File srcFile, File destFile) throws FileNotFoundException {
// 实例化IO流(输入流和输出流)
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
// 拷贝文件(存储和读取)
try (fis; fos) {
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
在以上代码中,表达式中引用了fis和fos,那么在fis和fos就自动变为常量啦,也就意味着在try代码块中不能修改fis和fos的指向,从而保证打开的资源肯定能够关闭。
局部变量类型判断
在Java10中,新增了局部变量类型判断。在方法体或代码块中,对于可以在编译期确定的类型,可以使用var来定义。这个特性并不意味着java是弱类型的语言,仅是提供了更简洁的书写方式。对于编译期无法确定的类型,依然要写清楚类型。
【示例】局部变量类型判断案例
// 使用var来作为变量的引用声明
var num = 123;
var str = "hello world";
var arr = new int[] {11, 22, 33};
var arrayList = new ArrayList<String>();
var calendar = Calendar.getInstance();
// 以下为不可以声明为var的情况
// 1.使用var必须要求变量必须初始化
// var userName;
// 2.不能给变量赋null值
// var userName = null;
// 3.lambda表达式不可以声明为var
// var function = (num) -> Math.round(3.51);
// 4.方法引用不可以声明为var
// var method = System.out :: println;
// 5.数组静态初始化不可以声明为var
// var arr = {"aa", "bb", "cc"};
// 6.类的成员变量不可以使用var类型推断
// 7.所有参数声明,返回值类型,构造方法参数都不可以
instanceof的模式匹配
在JDK14中新增instanceof模式匹配增强(预览),在JDK16中转正。通过instanceof模式匹配增强,我们就可以直接在模式匹配的括号内声明对应类型的局部变量。
【示例】执行向下转型的操作,从而调用show()方法
/**
* 以前的代码实现方式
*/
@Test
public void testOld() {
// 父类引用指向子类对象(多态)
Animal animal = new Dog();
// 判断animal是否为Dog类的实例
if (animal instanceof Dog) {
// 指向向下转型的操作
Dog dog = (Dog) animal;
// 调用Dog类特有的show()方法
dog.show();
}
}
/**
* 使用instanceof模式匹配增强的实现方式
*/
public void testNew() {
// 父类引用指向子类对象(多态)
Animal animal = new Dog();
// 如果animal是Dog类的实例,则向下转型后就命名为dog
if (animal instanceof Dog dog) {
// 调用Dog类特有的show()方法
dog.show();
}
}
【示例】重写equals(),判断成员变量是否相等
public class Tiger {
String name;
int age;
/**
* 以前的代码实现方式
*/
/*@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
// 如果obj属于Tiger类型,则就执行向下转型的操作
if (obj instanceof Tiger) {
// 执行向下转型的操作,恢复对象的实际类型
Tiger tiger = (Tiger) obj;
// 如果成员变量都相等,则返回true,否则返回false
return age == tiger.age && Objects.equals(name, tiger.name);
}
// 如果obj不属于Tiger类型,则返回false即可
return false;
}*/
/**
* 使用instanceof模式匹配增强的实现方式
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
// 如果obj属于Tiger类型并且成员变量值都相等,那么返回true
if (obj instanceof Tiger tiger) {
return age == tiger.age && Objects.equals(name, tiger.name);
}
// 如果obj不属于Tiger类型,则返回false即可
return false;
}
}
switch表达式
目前switch表达式的问题:
- 匹配自上而下,若无break,后面的case语句都会执行
- 不同的case语句定义的变量名不能相同
- 不能在一个case后面写多个值
- 整个switch不能作为表达式的返回值
在Java12中对switch表达式做了增强(预览),能够使用更加简洁的代码来解决这些问题。
【示例】switch表达式使用的案例
/**
* 需求:根据月份输出对应季节的特点
* 方案一:使用以前的技术来实现
*/
public static void normalSwitch(int month) {
// 定义一个变量,用于保存季节的特点
String season;
// 判断month的取值,从而知晓对应的季节
switch (month) {
case 12:
case 1:
case 2:
season = "白雪皑皑";
break;
case 3:
case 4:
case 5:
season = "春意盎然";
break;
case 6:
case 7:
case 8:
season = "夏日炎炎";
break;
case 9:
case 10:
case 11:
season = "秋高气爽";
break;
default:
throw new RuntimeException("没有该月份。。。");
}
// 输出month对应季节的特点
System.out.println(season);
}
/**
* 需求:根据月份输出对应季节的特点
* 方案二:使用Java12的新特性来实现
*/
public static void newSwitch(int month) {
// 判断month的取值,获得对应季节的特点
String season = switch (month) {
case 12, 1, 2 -> "白雪皑皑";
case 3, 4, 5 -> "春意盎然";
case 6, 7, 8 -> "夏日炎炎";
case 9, 10, 11 -> "秋高气爽";
default -> throw new RuntimeException("没有该月份。。。");
};
// 输出month对应季节的特点
System.out.println(season);
}
在Java13中,增加关键字yield关键字(预览), 用于在switch表达式中返回结果。到Java14版本中,Java12和Java13中关于switch的新特性都确定为正式版本。
【示例】switch表达式中的yield关键字
/**
* 需求:根据月份输出对应季节的特点
* 演示:Java13版本中新增的yield新特性
*/
public static void yieldSwitch1(int month) {
// 判断month的取值,获得对应季节的特点
String season = switch (month) {
case 12, 1, 2:
yield "白雪皑皑";
case 3, 4, 5:
yield "春意盎然";
case 6, 7, 8:
yield "夏日炎炎";
case 9, 10, 11:
yield "秋高气爽";
default:
throw new RuntimeException("没有该月份。。。");
};
// 输出month对应季节的特点
System.out.println(season);
}
文本块
在Java语言中,通常需要使用String类型表达HTML,XML,SQL或JSON等格式的字符串,在进行字符串赋值时需要进行转义和连接操作,然后才能编译该代码,这种表达方式难以阅读并且难以维护。
在Java12版本中,新增了文本块(预览)。文本块就是指多行字符串,例如一段格式化后的xml、json等。而有了文本块以后,用户不需要转义,Java能自动搞定。因此,文本块将提高Java程序的可读性和可写性。
【示例】演示文本块的使用
// 使用以前拼接的方式
String html1 = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>";
System.out.println(html1);
// 使用文本块的方式
String html2 = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
System.out.println(html2);
在Java14版本中,针对文本块又新增两个特性(阅览)。1)在一行的结尾增加“\”可以取消改行的换行符;2)可以通过“\s”增加空格。
【示例】演示文本块新增特性
// 取消换行(\)
String json1 = """
{
"username":"ande",\
"age":18
}
""";
System.out.println(json1);
// 添加空格(\s)
String json2 = """
{
"username"\s:\s"ande",
"age"\s:\s18
}
""";
System.out.println(json2);
Record
早在2019年2月份,Java语言架构师Brian Goetz就吐槽了Java语言,他和很多程序员一样抱怨“Java太啰嗦”或有太多的“繁文缛节”,他提到:开发人员想要创建纯数据载体类,通常都必须编写大量低价值、重复的、容易出错的代码。例如:构造方法、getter/setter、equals()、hashCode()以及toString()等。
以至于很多人选择使用IDE的功能来自动生成这些代码。还有一些开发会选择使用一些第三方类库,如Lombok等来生成这些方法,从而会导致了令人吃惊的表现和糟糕的可调试性。
那么,Brian Goetz大神提到的纯数据载体到底指的是什么呢?我们举了一个简单的例子:
public final class Tiger {
private final String name;
private final int age;
public Tiger(String name, int age) {
this.name = name;
this.age = age;
}
public String name() {
return name;
}
public int age() {
return age;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Tiger) obj;
return Objects.equals(this.name, that.name) &&
this.age == that.age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Tiger[" +
"name=" + name + ", " +
"age=" + age + ']';
}
}
这里面的Tiger其实就是一个纯数据载体,Tiger类中提供了name和age两个私有常量,并且只提供了全参构造方法和常量名相同的getter方法,以及一些equals、hashCode和toString等方法。于是,BrianGoetz大神提出一种想法,他提到,Java完全可以对于这种纯数据载体通过另外一种方式表示。
在Java14版本中,新增了Record类型。Record是Java的一种新的类型,Record使数据类型变得非常简洁,一般可以帮助我们定义一些简单的用于纯数据载体的实体类。
Record类的特点:
状态声明中的每个属性,都是默认采用了private和final修饰,则属性值就不可修改
在Record类中,默认已经重写了Object类提供的equals(),hashcode(),toString()方法
在Record类中,默认提供全参的构造方法,并且提供的getter方法名和属性名保持一致。
Record类采用了final修饰,并且显示的继承于java.lang.Record类,因此就不能继承别的父类。
【示例】将以上的Tiger类转化为Record类
public record Tiger(String name, int age) {
}
在以上的Record类中,Tiger类默认采用了final修饰,并且显示的继承于java.lang.Record抽象类,因此Tiger类就不能继承于别的父类。在Tiger类中,提供了name和age两个私有常量,并且还提供了一个public修饰的全参构造方法,提供的getter方法的名字和属性名保持一致,但是并没有提供setter方法。并且,在Tiger类中还重写了Object类提供的equals(),hashcode(),toString()方法。
在Record类中,我们还可以新增静态属性、无参构造方法、成员方法和静态方法,但是创建对象时不能调用无参构造方法,而是通过全参构造方法创建对象的时候,默认就会调用Record类中的无参构造方法。
【示例】在Record类中添加的内容
public record Tiger(String name, int age) {
// 新增静态属性
static double score;
// 新增无参构造方法
// 注意:通过全参构造方法创建对象,默认就会调用此处的无参构造方法
public Tiger {
System.out.println("无参构造方法");
}
// 新增成员方法
void show() {
System.out.println("show. ..");
}
// 新增静态方法
static void method() {
System.out.println("method ...");
}
}
密封类
Java中的密封类是一种新的类修饰符,它可以修饰类和接口,可以控制哪些类可以扩展或实现该类或接口。下面是密封类的一些主要用途:
- 维护类层次结构的封闭性
密封类的一个主要用途是确保类层次结构的封闭性。这意味着,如果您想保护一组类,而不希望其他类继承或实现它们,可以使用密封类来实现这一目标。这对于确保代码的安全性和稳定性非常有用。
- 预防代码的意外扩展
密封类可以防止其他程序员意外地扩展一个类。在进行类设计时,您可能希望自己或其他程序员只能在特定的类中实现或继承指定的类。在这种情况下,您可以将类标记为“密封”,强制限制其他程序员可以实现或继承的类的范围。
- 增强代码的可读性和可维护性
密封类可以增强代码的可读性和可维护性。由于密封类明确规定了哪些类可以扩展或实现它,因此其他开发人员可以更清晰地看到代码的结构并理解它们的关系。这使得代码更易于维护和修改。
总之,密封类是一种灵活而有用的类修饰符,可以帮助您维护类的封闭性、预防代码的意外扩展、增强代码的可读性和可维护性。
在Java15版本中,新增了密封类和密封接口(预览)。
使用sealed关键字修饰的类,我们就称之为密封类。密封类必须是一个父类,我们可以使用permits关键字来指定哪些子类可以继承于密封类,并且密封类的子类必须使用sealed、final或non-sealed来修饰。
【示例】密封类的演示
// 密封类必须被继承,并且使用permits来指定哪些子类可以被继承
sealed class Animal permits Dog, Bird, Tiger { }
// 注意:密封类的子类必须使用sealed、final或non-sealed来修饰
// final关键字修饰的子类,则该子类不能被继承
final class Tiger extends Animal { }
// non-sealed修饰的子类,则该子类就是一个普通类
non-sealed class Bird extends Animal { }
// sealed修饰的子类,则该类就必须被继承,否则就会编译错误
sealed class Dog extends Animal {}
non-sealed class SmallDog extends Dog {}
使用sealed关键字修饰的接口,我们就称之为密封接口。密封接口必须使用permits关键字来指定实现类或子接口。针对密封接口的实现类,则必须使用sealed、final或non-sealed来修饰;针对密封接口的子接口,则必须使用sealed或non-sealed来修饰。
【示例】密封接口的演示
// 使用sealed修饰的接口,则必须使用permits来指定实现类或子接口。
public sealed interface InterA permits Student, InterB { }
// 密封接口的实现类,必须使用sealed、final或non-sealed来修饰
non-sealed /*final*/ /*sealed*/ class Student implements InterA { }
// 密封接口的子接口,必须使用sealed或non-sealed来修饰
non-sealed /*sealed*/ interface InterB extends InterA {}
sealed与record:
因为Record类默认采用了final关键字修饰,因此Record类就可以作为密封接口的实现类。
【示例】密封接口和Record类
// 密封接口
sealed interface Flyable permits SuperMan { }
// 让Record类作为密封接口的实现类
record SuperMan(String name, int age) implements Flyable { }
API层面的变化
String存储结构改变
在Java8及其之前,String底层采用char类型数组来存储字符;在Java9及其以后,String底层采用byte类型的数组来存储字符。将char[]转化为byte[],其目的就是为了节约存储空间。
String 新增的方法
在Java11版本中,对String类新增了一些方法,新增的方法如下:
// 空格,制表符,换行等都认为是空的
boolean blank = "\t \n".isBlank();
System.out.println(blank); // 输出:true
String source = "\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000www.baidu.com\u3000\u3000\u3000\u3000\u3000";
// 去除“前后”的中文空格
System.out.println(source.strip());
// 去除“开头”的中文空格
System.out.println(source.stripLeading());
// 去除“末尾”的中文空格
System.out.println(source.stripTrailing());
// 把字符串内容重复n份
String repeat = "xixi".repeat(3);
System.out.println(repeat); // 输出:xixixixixixi
// 按照换行来分割字符串,返回的结果是Stream对象
Stream<String> lines = "a\nb\nc\n".lines();
System.out.println(lines.count()); // 输出:3
在Java12版本中,对String类新增了一些方法,新增的方法如下:
// 在字符串前面添加n个空格
String result2 = "Java Golang".indent(4);
System.out.println(result2);
接口支持私有方法
在Java8版本中,接口中支持“公开”的静态方法和公开的默认方法;在Java9版本中,接口中还允许定义“私有”的静态方法和成员方法,但是不能定义私有的默认方法。
【示例】演示接口中的私有静态方法和成员方法
/**
* 接口(JDK1.9)
*/
public interface Flyable {
// 私有的静态方法
private static void staticMethod() {
System.out.println("static method ...");
}
// 私有的成员方法
private void method() {
System.out.println("default method ...");
}
}
标识符命名的变化
在Java8及其之前,标识符可以独立使用“_”来命名。
String _ = "hello";
System.out.println(_);
但是,在Java9中规定“_”不能独立命名标识符了,如果使用就会报错:
简化编译运行程序
在我们的认知里面,要运行一个Java源代码必须先编译(javac命令),再运行(java命令),两步执行动作。而在Java 11版本中,通过一个java命令就直接搞定了。
需要执行的程序:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
执行java命令进行运行,如下图所示:
创建不可变集合
在Java9版本中,我们可以通过List、Set和Map接口提供的of(E… elements)静态方法来创建不可变集合。通过此方式创建的不可变集合,我们不但不能添加或删除元素,并且还不能修改元素。
【示例】创建不可变集合
// 创建不可变List集合
List<Integer> list = List.of(1, 2, 3, 4, 5);
System.out.println(list);
// 创建不可变Set集合
// 注意:如果Set集合中有相同的元素,则就会抛出IllegalArgumentException异常。
Set<Integer> set = Set.of(1, 2, 3, 4, 5, 4);
System.out.println(set);
// 创建不可变Map集合
Map<Integer, String> map = Map.of(123, "武汉", 456, "成都");
System.out.println(map);
Arrays.asList与List.of的区别:
List.of:不能向集合中添加或删除元素,也不能修改集合中的元素。
Arrays.asList:不能向集合中添加或删除元素,但是可以修改集合中的元素。
【示例】Arrays.asList与List.of的区别
// 通过Arrays.asList()方法创建不可变集合
List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
// list1.add(6); // 抛出UnsupportedOperationException异常
// list1.remove(2); // 抛出UnsupportedOperationException异常
list1.set(2, 33); // 没有问题
System.out.println(list1); // 输出:[1, 2, 33, 4, 5]
// 通过List.of()方法创建不可变集合
List<Integer> list2 = List.of(1, 2, 3, 4, 5);
// list2.add(6); // 抛出UnsupportedOperationException异常
// list2.remove(2); // 抛出UnsupportedOperationException异常
// list2.set(2, 33); // 抛出UnsupportedOperationException异常
Optional API
在Java8以前,Java程序员操作对象时,为了避免错误引用null造成的空指针异常,往往需要一系列繁杂冗余的判空操作,增加了许多重复代码,降低了代码可读性,于是Java 8引入Optional类,优雅简洁的对null值进行处理,从而避免出现空指针异常(NullPointerException)。
本质上,Optional 类是一个包含有可选值的包装类,这意味着 Optional 类中既可以含有对象也可以为null。
创建Optional对象
使用Optional类提供的of()和ofNullable() 静态方法来创建包含值的Optioanal实例。
如果将null当作参数传进去of()会抛出空指针异常,如果将null当作参数传进去 ofNullable() 就不会抛出空指针异常。
因此当对象可能存在或者不存在,应该使用 ofNullable()方法来创建Optional实例。
【示例】创建一个Optional实例
// 创建一个包含“null”的Optional示例
Optional<Object> optional1 = Optional.ofNullable(null);
// 创建一个包含“对象”的Optional示例
Optional<String> optional2 = Optional.ofNullable("hello");
Optional类的方法
想要获得Optional实例中包含的值,那么就可以使用以下两个方法来实现。
方法名 | 描述 |
---|---|
public T get() | 如果值不为null,则直接取出该值;如果值为null,则抛出空指针异常。 |
public T orElse(T other) | 如果值不为null,则直接取出该值;如果值为null,则取出的就是参数other的值。 |
开发中,我们获取Optional中存储的值,一般都是采用orElse(T other)方法来实现。
【示例】演示get()方法
// 创建一个包含“null”的Optional示例
Optional<Object> optional1 = Optional.ofNullable(null);
Object obj1 = optional1.get(); // 抛出空指针异常
// 创建一个包含“对象”的Optional示例
Optional<String> optional2 = Optional.ofNullable("hello");
String str = optional2.get();
System.out.println(str); // 输出:hello
【示例】演示orElse(T other)方法
// 创建一个包含“null”的Optional示例
Optional<Object> optional1 = Optional.ofNullable(null);
Object str1 = optional1.orElse("world");
System.out.println(str1); // 输出:world
// 创建一个包含“对象”的Optional示例
Optional<String> optional2 = Optional.ofNullable("hello");
String str2 = optional2.orElse("world");
System.out.println(str2); // 输出:hello
Optional的使用案例
需求:有一场商业表演,原计划让“刘亦菲”来表演,如果“刘亦菲”不能参加,则就换“佟丽娅”来表演,该需求的实现代码如下:
// 定义一个变量,用于保存表演者的名字
// String name = "刘亦菲"; // 原计划
String name = null; // 刘亦菲不能参加的情况
// 使用Optional来封装表演者的名字
Optional<String> optional = Optional.ofNullable(name);
// 获得实际参与表演对应人的名字
// 如果name的值为null,则就换为“佟丽娅”参与表演
String finalName = optional.orElse("佟丽娅");
// 输出实际表演者的名字
System.out.println(finalName);