大家好,我是冰河~~
说实话,肝这篇文章花了我一个月的时间,关于Java8的新特性全在这儿了,建议先收藏后阅读。
Java8有哪些新特性?
简单来说,Java8新特性如下所示:
-
Lambda表达式
-
函数式接口
-
方法引用与构造器引用
-
Stream API
-
接口的默认方法与静态方法
-
新时间日期API
-
其他新特性
其中,引用最广泛的新特性是Lambda表达式和Stream API。
Java8有哪些优点?
简单来说Java8优点如下所示。
- 速度更快
- 代码更少(增加了新的语法Lambda表达式)
- 强大的Stream API
- 便于并行
- 最大化减少空指针异常Optional
Lambda表达式
什么是Lambda表达式?
Lambda表达式是一个匿名函数,我们可以这样理解Lambda表达式:Lambda是一段可以传递的代码(能够做到将代码像数据一样进行传递)。使用Lambda表达式能够写出更加简洁、灵活的代码。并且,使用Lambda表达式能够使Java的语言表达能力得到提升。
匿名内部类
在介绍如何使用Lambda表达式之前,我们先来看看匿名内部类,例如,我们使用匿名内部类比较两个Integer类型数据的大小。
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
在上述代码中,我们使用匿名内部类实现了比较两个Integer类型数据的大小。
接下来,我们就可以将上述匿名内部类的实例作为参数,传递到其他方法中了,如下所示。
TreeSet<Integer> treeSet = new TreeSet<>(com);
完整的代码如下所示。
@Test
public void test1(){
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
TreeSet<Integer> treeSet = new TreeSet<>(com);
}
我们分析下上述代码,在整个匿名内部类中,实际上真正有用的就是下面一行代码。
return Integer.compare(o1, o2);
其他的代码本质上都是“冗余”的。但是为了书写上面的一行代码,我们不得不在匿名内部类中书写更多的代码。
Lambda表达式
如果使用Lambda表达式完成两个Integer类型数据的比较,我们该如何实现呢?
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
看到没,使用Lambda表达式,我们只需要使用一行代码就能够实现两个Integer类型数据的比较。
我们也可以将Lambda表达式传递到TreeSet的构造方法中,如下所示。
TreeSet<Integer> treeSet = new TreeSet<>((x, y) -> Integer.compare(x, y));
直观的感受就是使用Lambda表达式一行代码就能搞定匿名内部类多行代码的功能。
看到这,不少读者会问:我使用匿名内部类的方式实现比较两个整数类型的数据大小并不复杂啊!我为啥还要学习一种新的语法呢?
其实,我想说的是:上面咱们只是简单的列举了一个示例,接下来,咱们写一个稍微复杂一点的例子,来对比下使用匿名内部类与Lambda表达式哪种方式更加简洁。
对比常规方法和Lambda表达式
例如,现在有这样一个需求:获取当前公司中员工年龄大于30岁的员工信息。
首先,我们需要创建一个Employee实体类来存储员工的信息。
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = -9079722457749166858L;
private String name;
private Integer age;
private Double salary;
}
在Employee中,我们简单存储了员工的姓名、年龄和薪资。
接下来,我们创建一个存储多个员工的List集合,如下所示。
protected List<Employee> employees = Arrays.asList(
new Employee("张三", 18, 9999.99),
new Employee("李四", 38, 5555.55),
new Employee("王五", 60, 6666.66),
new Employee("赵六", 16, 7777.77),
new Employee("田七", 18, 3333.33)
);
1.常规遍历集合
我们先使用常规遍历集合的方式来查找年龄大于等于30的员工信息。
public List<Employee> filterEmployeesByAge(List<Employee> list){
List<Employee> employees = new ArrayList<>();
for(Employee e : list){
if(e.getAge() >= 30){
employees.add(e);
}
}
return employees;
}
接下来,我们测试一下上面的方法。
@Test
public void test3(){
List<Employee> employeeList = filterEmployeesByAge(this.employees);
for (Employee e : employeeList){
System.out.println(e);
}
}
运行test3方法,输出信息如下所示。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
总体来说,查找年龄大于或者等于30的员工信息,使用常规遍历集合的方式稍显复杂了。
例如,需求发生了变化:获取当前公司中员工工资大于或者等于5000的员工信息。
此时,我们不得不再次创建一个按照工资过滤的方法。
public List<Employee> filterEmployeesBySalary(List<Employee> list){
List<Employee> employees = new ArrayList<>();
for(Employee e : list){
if(e.getSalary() >= 5000){
employees.add(e);
}
}
return employees;
}
对比filterEmployeesByAge()方法和filterEmployeesBySalary方法后,我们发现,大部分的方法体是相同的,只是for循环中对于条件的判断不同。
如果此时我们再来一个需求,查找当前公司中年龄小于或者等于20的员工信息,那我们又要创建一个过滤方法了。 看来使用常规方法是真的不方便啊!
这里,问大家一个问题:对于这种常规方法最好的优化方式是啥?相信有不少小伙伴会说:将公用的方法抽取出来。没错,将公用的方法抽取出来是一种优化方式,但它不是最好的方式。最好的方式是啥?那就是使用 设计模式 啊!设计模式可是无数前辈不断实践而总结出的设计原则和设计模式。大家可以查看《设计模式汇总——你需要掌握的23种设计模式都在这儿了!》一文来学习设计模式专题。
2.使用设计模式优化代码
如何使用设计模式来优化上面的方法呢,大家继续往下看,对于设计模式不熟悉的同学可以先根据《设计模式汇总——你需要掌握的23种设计模式都在这儿了!》来学习。
我们先定义一个泛型接口MyPredicate,对传递过来的数据进行过滤,符合规则返回true,不符合规则返回false。
public interface MyPredicate<T> {
/**
* 对传递过来的T类型的数据进行过滤
* 符合规则返回true,不符合规则返回false
*/
boolean filter(T t);
}
接下来,我们创建MyPredicate接口的实现类FilterEmployeeByAge来过滤年龄大于或者等于30的员工信息。
public class FilterEmployeeByAge implements MyPredicate<Employee> {
@Override
public boolean filter(Employee employee) {
return employee.getAge() >= 30;
}
}
我们定义一个过滤员工信息的方法,此时传递的参数不仅有员工的信息集合,同时还有一个我们定义的接口实例,在遍历员工集合时将符合过滤条件的员工信息返回。
//优化方式一
public List<Employee> filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate){
List<Employee> employees = new ArrayList<>();
for(Employee e : list){
if(myPredicate.filter(e)){
employees.add(e);
}
}
return employees;
}
接下来,我们写一个测试方法来测试优化后的代码。
@Test
public void test4(){
List<Employee> employeeList = this.filterEmployee(this.employees, new FilterEmployeeByAge());
for (Employee e : employeeList){
System.out.println(e);
}
}
运行test4()方法,输出的结果信息如下所示。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
写到这里,大家是否有一种豁然开朗的感觉呢?
没错,这就是设计模式的魅力,对于设计模式不熟悉的小伙伴,一定要参照《设计模式汇总——你需要掌握的23种设计模式都在这儿了!》来学习。
我们继续获取当前公司中工资大于或者等于5000的员工信息,此时,我们只需要创建一个FilterEmployeeBySalary类实现MyPredicate接口,如下所示。
public class FilterEmployeeBySalary implements MyPredicate<Employee>{
@Override
public boolean filter(Employee employee) {
return employee.getSalary() >= 5000;
}
}
接下来,就可以直接写测试方法了,在测试方法中继续调用filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)
方法。
@Test
public void test5(){
List<Employee> employeeList = this.filterEmployee(this.employees, new FilterEmployeeBySalary());
for (Employee e : employeeList){
System.out.println(e);
}
}
运行test5方法,输出的结果信息如下所示。
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=赵六, age=16, salary=7777.77)
可以看到,使用设计模式对代码进行优化后,无论过滤员工信息的需求如何变化,我们只需要创建MyPredicate接口的实现类来实现具体的过滤逻辑,然后在测试方法中调用filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)
方法将员工集合和过滤规则传入即可。
这里,问大家一个问题:上面优化代码使用的设计模式是哪种设计模式呢?如果是你,你会想到使用设计模式来优化自己的代码吗?小伙伴们自己先思考一下到底使用的设计模式是什么?文末我会给出答案!
使用设计模式优化代码也有不好的地方:每次定义一个过滤策略的时候,我们都要单独创建一个过滤类!!
3.匿名内部类
那使用匿名内部类是不是能够优化我们书写的代码呢,接下来,我们就使用匿名内部类来实现对员工信息的过滤。先来看过滤年龄大于或者等于30的员工信息。
@Test
public void test6(){
List<Employee> employeeList = this.filterEmployee(this.employees, new MyPredicate<Employee>() {
@Override
public boolean filter(Employee employee) {
return employee.getAge() >= 30;
}
});
for (Employee e : employeeList){
System.out.println(e);
}
}
运行test6方法,输出如下结果信息。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
再实现过滤工资大于或者等于5000的员工信息,如下所示。
@Test
public void test7(){
List<Employee> employeeList = this.filterEmployee(this.employees, new MyPredicate<Employee>() {
@Override
public boolean filter(Employee employee) {
return employee.getSalary() >= 5000;
}
});
for (Employee e : employeeList){
System.out.println(e);
}
}
运行test7方法,输出如下结果信息。
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=赵六, age=16, salary=7777.77)
匿名内部类看起来比常规遍历集合的方法要简单些,并且将使用设计模式优化代码时,每次创建一个类来实现过滤规则写到了匿名内部类中,使得代码进一步简化了。
但是,使用匿名内部类代码的可读性不高,并且冗余代码也比较多!!
那还有没有更加简化的方式呢?
4.重头戏:Lambda表达式
在使用Lambda表达式时,我们还是要调用之前写的filterEmployee(List<Employee> list, MyPredicate<Employee> myPredicate)
方法。
注意看,获取年龄大于或者等于30的员工信息。
@Test
public void test8(){
filterEmployee(this.employees, (e) -> e.getAge() >= 30).forEach(System.out::println);
}
看到没,使用Lambda表达式只需要一行代码就完成了员工信息的过滤和输出。是不是很6呢。
运行test8方法,输出如下的结果信息。
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
再来看使用Lambda表达式来获取工资大于或者等于5000的员工信息,如下所示。
@Test
public void test9(){
filterEmployee(this.employees, (e) -> e.getSalary() >= 5000).forEach(System.out::println);
}
没错,使用Lambda表达式,又是一行代码就搞定了!!
运行test9方法,输出如下的结果信息。
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=赵六, age=16, salary=7777.77)
另外,使用Lambda表达式时,只需要给出需要过滤的集合,我们就能够实现从集合中过滤指定规则的元素,并输出结果信息。
5.重头戏:Stream API
使用Lambda表达式结合Stream API,只要给出相应的集合,我们就可以完成对集合的各种过滤并输出结果信息。
例如,此时只要有一个employees
集合,我们使用Lambda表达式来获取工资大于或者等于5000的员工信息。
@Test
public void test10(){
employees.stream().filter((e) -> e.getSalary() >= 5000).forEach(System.out::println);
}
没错,只给出一个集合,使用Lambda表达式和Stream API,一行代码就能够过滤出想要的元素并进行输出。
运行test10方法,输出如下的结果信息。
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=王五, age=60, salary=6666.66)
Employee(name=赵六, age=16, salary=7777.77)
如果我们只想要获取前两个员工的信息呢?其实也很简单,如下所示。
@Test
public void test11(){
employees.stream().filter((e) -> e.getSalary() >= 5000).limit(2).forEach(System.out::println);
}
可以看到,我们在代码中添加了limit(2)
来限制只获取两个员工信息。运行test11方法,输出如下的结果信息。
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
使用Lambda表达式和Stream API也可以获取指定的字段信息,例如获取工资大于或者等于5000的员工姓名。
@Test
public void test12(){
employees.stream().filter((e) -> e.getSalary() >= 5000).map(Employee::getName).forEach(System.out::println);
}
可以看到,使用map过滤出了工资大于或者等于5000的员工姓名。运行test12方法,输出如下的结果信息。
张三
李四
王五
赵六
是不是很简单呢?
最后,给出文中使用的设计模式:策略模式。
匿名类到Lambda表达式
我们先来看看从匿名类如何转换到Lambda表达式呢?
这里,我们可以使用两个示例来说明如何从匿名内部类转换为Lambda表达式。
- 匿名内部类到Lambda表达式
使用匿名内部类如下所示。
Runnable r = new Runnable(){
@Override
public void run(){
System.out.println("Hello Lambda");
}
}
转化为Lambda表达式如下所示。
Runnable r = () -> System.out.println("Hello Lambda");
- 匿名内部类作为参数传递到Lambda表达式作为参数传递
使用匿名内部类作为参数如下所示。
TreeSet<Integer> ts = new TreeSet<>(new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2){
return Integer.compare(o1, o2);
}
});
使用Lambda表达式作为参数如下所示。
TreeSet<Integer> ts = new TreeSet<>(
(o1, o2) -> Integer.compare(o1, o2);
);
从直观上看,Lambda表达式要比常规的语法简洁的多。
Lambda表达式的语法
Lambda表达式在Java语言中引入了 “->” 操作符, “->” 操作符被称为Lambda表达式的操作符或者箭头操作符,它将Lambda表达式分为两部分:
- 左侧部分指定了Lambda表达式需要的所有参数。
Lambda表达式本质上是对接口的实现,Lambda表达式的参数列表本质上对应着接口中方法的参数列表。
- 右侧部分指定了Lambda体,即Lambda表达式要执行的功能。
Lambda体本质上就是接口方法具体实现的功能。
我们可以将Lambda表达式的语法总结如下。
1.语法格式一:无参,无返回值,Lambda体只有一条语句
Runnable r = () -> System.out.println("Hello Lambda");
具体示例如下所示。
@Test
public void test1(){
Runnable r = () -> System.out.println("Hello Lambda");
new Thread(r).start();
}
2.语法格式二:Lambda表达式需要一个参数,并且无返回值
Consumer<String> func = (s) -> System.out.println(s);
具体示例如下所示。
@Test
public void test2(){
Consumer<String> consumer = (x) -> System.out.println(x);
consumer.accept("Hello Lambda");
}
3.语法格式三:Lambda只需要一个参数时,参数的小括号可以省略
Consumer<String> func = s -> System.out.println(s);
具体示例如下所示。
@Test
public void test3(){
Consumer<String> consumer = x -> System.out.println(x);
consumer.accept("Hello Lambda");
}
4.语法格式四:Lambda需要两个参数,并且有返回值
BinaryOperator<Integer> bo = (a, b) -> {
System.out.println("函数式接口");
return a + b;
};
具体示例如下所示。
@Test
public void test4(){
Comparator<Integer> comparator = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};
}
5.语法格式五:当Lambda体只有一条语句时,return和大括号可以省略
BinaryOperator<Integer> bo = (a, b) -> a + b;
具体示例如下所示。
@Test
public void test5(){
Comparator<Integer> comparator = (x, y) -> Integer.compare(x, y);
}
6.语法格式六:Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器能够通过上下文推断出数据类型,这就是“类型推断”
BinaryOperator<Integer> bo = (Integer a, Integer b) -> {
return a + b;
};
等同于
BinaryOperator<Integer> bo = (a, b) -> {
return a + b;
};
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。
函数式接口
Lambda表达式需要函数式接口的支持,所以,我们有必要来说说什么是函数式接口。
只包含一个抽象方法的接口,称为函数式接口。
可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。
可以在任意函数式接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
我们可以自定义函数式接口,并使用Lambda表达式来实现相应的功能。
例如,使用函数式接口和Lambda表达式实现对字符串的处理功能。
首先,我们定义一个函数式接口MyFunc,如下所示。
@FunctionalInterface
public interface MyFunc <T> {
public T getValue(T t);
}
接下来,我们定义一个操作字符串的方法,其中参数为MyFunc接口实例和需要转换的字符串。
public String handlerString(MyFunc<String> myFunc, String str){
return myFunc.getValue(str);
}
接下来,我们对自定义的函数式接口进行测试,此时我们传递的函数式接口的参数为Lambda表达式,并且将字符串转化为大写。
@Test
public void test6(){
String str = handlerString((s) -> s.toUpperCase(), "binghe");
System.out.println(str);
}
运行test6方法,得出的结果信息如下所示。
BINGHE
我们也可以截取字符串的某一部分,如下所示。
@Test
public void test7(){
String str = handlerString((s) -> s.substring(0,4), "binghe");
System.out.println(str);
}
运行test7方法,得出的结果信息如下所示。
bing
可以看到,我们可以通过handlerString(MyFunc<String> myFunc, String str)
方法结合Lambda表达式对字符串进行任意操作。
注意:作为参数传递 Lambda 表达式:为了将 Lambda 表达式作为参数传递,接收Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型 。
Lambda表达式典型案例
案例一
需求
调用Collections.sort()方法,通过定制排序比较两个Employee(先比较年龄,年龄相同按姓名比较),使用Lambda表达式作为参数传递。
实现
这里,我们先创建一个Employee类,为了满足需求,我们在Employee类中定义了姓名、年龄和工资三个字段,如下所示。
@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = -9079722457749166858L;
private String name;
private Integer age;
private Double salary;
}
接下来,我们在TestLambda类中定义一个成员变量employees,employees变量是一个List集合,存储了Employee的一个列表,如下所示。
protected List<Employee> employees = Arrays.asList(
new Employee("张三", 18, 9999.99),
new Employee("李四", 38, 5555.55),
new Employee("王五", 60, 6666.66),
new Employee("赵六", 8, 7777.77),
new Employee("田七", 58, 3333.33)
);
前期的准备工作完成了,接下来,我们就可以实现具体的业务逻辑了。
@Test
public void test1(){
Collections.sort(employees, (e1, e2) -> {
if(e1.getAge() == e2.getAge()){
return e1.getName().compareTo(e2.getName());
}
return Integer.compare(e1.getAge(), e2.getAge());
});
employees.stream().forEach(System.out::println);
}
上述代码比较简单,我就不赘述具体逻辑了。运行test1方法,得出的结果信息如下所示。
Employee(name=赵六, age=8, salary=7777.77)
Employee(name=张三, age=18, salary=9999.99)
Employee(name=李四, age=38, salary=5555.55)
Employee(name=田七, age=58, salary=3333.33)
Employee(name=王五, age=60, salary=6666.66)
如果想倒叙输出如何处理呢,只需要在将return Integer.compare(e1.getAge(), e2.getAge());
修改成-return Integer.compare(e1.getAge(), e2.getAge());
即可,如下所示。
@Test
public void test1(){
Collections.sort(employees, (e1, e2) -> {
if(e1.getAge() == e2.getAge()){
return e1.getName().compareTo(e2.getName());
}
return -Integer.compare(e1.getAge(), e2.getAge());
});
employees.stream().forEach(System.out::println);
}
再次运行test1方法,得出的结果信息如下所示。
Employee(name=王五, age=60, salary=6666.66)
Employee(name=田七, age=58, salary=33