又是热爱学习的一天!!
Java8新增的 Lambda 表达式虽然看着很NB,但其实 Lambda 表达式的本质只是一个"语法糖"而已。
这篇文章只是初识 Lambda,先看看它的好处是什么,它能给我们带来什么样的体验,至于 Lambda 具体怎么用以及什么时候用,会在后面的文章陆续学习到。
都说 Lambda 表达式能够优化内部类,我们先来看看它优化后是怎么样的。
我们以前要比较两个 Integer 大小的时候,是怎样实现的呢?是不是通过下面这种内部类的方式实现的:
/**
* 匿名内部类实现 Comparator
*/
@Test
public void compareTest(){
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
System.out.println(com.compare(100, 20));
}
匿名内部类看起来有点杂乱,而使用 Lambda 优化后是什么样子呢?正如下所示:
/**
* 使用 Lambda 表达式优化匿名内部类实现 Comparator
*/
@Test
public void compareLambdaTest(){
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
System.out.println(com.compare(100, 20));
}
使用 Lambda 表达式之后将原来的6行代码,变成了一行代码实现,看起来确实很舒服了很多。
如果觉得这个简单的例子,还不足以说服自己,那么接下来就把 Lambda 带到需求中去,看看以前的实现方式和 Lambda 的实现方式有何不同吧。
学校中有一群学生,我要通过年龄或者学号来过滤学生。
看到这个需求的第一反映是不是得有一个学生类,里面有一些学生的属性,那么下面就创建一个叫做 Student 的学生类:
public class Student {
private String name;
private int age;
private int studentID;
...省略构造器
...省略 get/set 方法
...省略 toString 方法
}
有了学生类之后,我们还需要模拟一群学生,把他们用 List 装起来。
List<Student> students = Arrays.asList(
new Student("小王", 10, 1006),
new Student("小李", 11, 1005),
new Student("小陈", 12, 1004),
new Student("小明", 13, 1003),
new Student("小红", 14, 1002),
new Student("小张", 15, 1001)
);
好啦,数据已经准备就绪,接下来看具体的需求
按照以前最原始的方法,我们是怎么实现的呢?
是不是得先有一个方法,然后把 students 一群学生作为参数传进去,然后再一顿过滤,最后返回符合条件的学生集合就行了。
/**
* 根据年龄过滤学生
* @param list 所有学生的集合
* @return 过滤后符合条件的学生集合
*/
public List<Student> filterStudentByAge(List<Student> list){
List<Student> students = new ArrayList<>();
for (Student stu : list) {
if(stu.getAge() > 12){
students.add(stu);
}
}
return students;
}
实现完了之后,再来调用看看结果:
@Test
public void getStudentByAge(){
List<Student> list = filterStudentByAge(students);
for (Student stu : list) {
System.out.println(stu.toString());
}
}
执行结果如下:
Student{name='小明', age=13, studentID=1003}
Student{name='小红', age=14, studentID=1002}
Student{name='小张', age=15, studentID=1001}
结果符合我们的预期,我们将年龄大于12的学生过滤出来了。
假设现在我们的需求变化了。
那么还是按照以前最原始的方法,我们是怎么实现的呢?是不是得先有一个方法,然后把 students 一群学生作为参数传进去,然后再一顿过滤,最后返回符合条件的学生集合就行了。
/**
* 根据学号过滤学生
* @param list 所有学生的集合
* @return 过滤后符合条件的学生集合
*/
public List<Student> filterStudentByID(List<Student> list){
List<Student> students = new ArrayList<>();
for (Student stu : list) {
if(stu.getStudentID() > 1003){
students.add(stu);
}
}
return students;
}
实现完了之后,再来调用看看结果:
@Test
public void getStudentByID(){
List<Student> list = filterStudentByID(students);
for (Student stu : list) {
System.out.println(stu.toString());
}
}
执行结果如下:
Student{name='小王', age=10, studentID=1006}
Student{name='小李', age=11, studentID=1005}
Student{name='小陈', age=12, studentID=1004}
结果符合我们的预期,我们将学号大于1003的学生过滤出来了。
或许此时,你已经发现上面代码的问题所在。上面的两个需求除了方案一模一样之外,代码也几乎一模一样,唯一有区别的地方就是 if 判断里面的条件。假设现在再来一个需求,说:我要过滤学号小于1005的学生。那是不是我们只需要将上面的代码完全copy一份,然后只修改 if 里面的条件就可以了,就能达到我们过滤的目的。
诶,这就是我们常说的“冗余代码”,在遇到这类代码的时候,我们要做的就是优化!
那么如何优化呢?
我们常听人说,优化代码最好的方式就是利用设计模式…balabalabala
好的,下面就利用设计模式来优化代码。
首先我需要创建一个接口,这个接口先生说:“俺是一个过滤指定对象的接口,俺有一个方法,专门用来过滤指定对象。”
public interface StudentFilter<T> {
boolean filter(T t);
}
“俺的这个方法是你传给我一个指定对象,俺就悄悄告诉你这个对象符不符合过滤条件!” 接口先生说道。
有了接口,就需要有实现类去实现它,年龄实现类女士说:“你好,我是一个实现类,我是实现于 StudentFilter 接口先生,我的主要工作是根据年龄过滤学生。”
public class StudentAgeFilter implements StudentFilter<Student> {
@Override
public boolean filter(Student student) {
return student.getAge() >= 14;
}
}
“我…我…我今天的工作是过滤出年龄大于等于14岁的学生。”,年龄实现类女士害羞的说道。
现在接口和实现类有了,就需要一个通用方法实现过滤逻辑:
public List<Student> filterStudent(List<Student> list, StudentFilter<Student> sf){
List<Student> students = new ArrayList<>();
for (Student stu : list) {
if(sf.filter(stu)){
students.add(stu);
}
}
return students;
}
参数为:一群学生的集合和一个过滤接口。 (注意:是接口,不是实现类)
里面的实现逻辑是和上面一样的,区别在与 if 判断中,这里是调用了接口的过滤方法。
现在来测试一下过滤效果:
@Test
public void getStudentByAge2(){
List<Student> list = filterStudent(students, new StudentAgeFilter());
for (Student stu : list) {
System.out.println(stu.toString());
}
}
我在调用 filterStudent 通用过滤方法的时候,第二个参数传的就是实现类,这是利用 多态 达到的效果。
结果显示:
Student{name='小王', age=10, studentID=1006}
Student{name='小李', age=11, studentID=1005}
Student{name='小陈', age=12, studentID=1004}
和我们预期的一样。
现在需求又变了,说:我要过滤学号大于1001的学生。
举一反三,我们的实现方式和上面过滤年龄是一样的。只需新增一个实现类实现于StudentFilter 接口,并且实现自己的过滤逻辑。
public class StudentIDFilter implements StudentFilter<Student> {
@Override
public boolean filter(Student student) {
return student.getStudentID() > 1001;
}
}
现在来调用测试一下:
@Test
public void getStudentByID2(){
List<Student> list = filterStudent(students, new StudentIDFilter());
for (Student stu : list) {
System.out.println(stu.toString());
}
}
执行结果:
Student{name='小王', age=10, studentID=1006}
Student{name='小李', age=11, studentID=1005}
Student{name='小陈', age=12, studentID=1004}
Student{name='小明', age=13, studentID=1003}
Student{name='小红', age=14, studentID=1002}
和预期一样的结果。
那么注意看,使用了设计模式后,我们优化的地方的在哪里呢?
在以前的方法实现中,每一个需求我们都对应的有一个专门的实现方法,这样代码非常冗余而且耦合度高,修改起来也不方便。
而现在采用了设计模式后,我们就只需要一个 filterStudent 通用方法,不管多少个过滤需求,我始终只有这一个方法,他是不会变的,它把上面两个方法合二为一了。这是它的优势一。
那么优势二就是我们通过接口和实现类来实现业务逻辑,还给代码解了耦。以后如果有新增的需求和修改的需求,我只需要新增或修改实现类就可以了,根本不用动 filterStudent 这个方法。
但是这个优势二也是它的一个缺陷。
它的缺陷就是,每当我在新增需求的时候,我都需要新增一个对应的实现类,麻不麻烦啊。有没有更简单的办法呢?答案是有的,有请
在使用了匿名内部类后,就可以不用去新增实现类了,代码如下:
@Test
public void filterStudentInnerClass(){
List<Student> list = filterStudent(students, new StudentFilter<Student>() {
@Override
public boolean filter(Student student) {
return student.getStudentID() <= 1002;
}
});
for (Student stu : list) {
System.out.println(stu.toString());
}
}
直接在 Test 方法中调用我们的 filterStudent 方法,方法的第二个参数我们就是用匿名内部类来实现。
这样做的话就简便太多了。
在文章开篇就讲了,匿名内部类看起来有点杂乱(再吐槽一次…),我们可以使用 Lambda 表达式来优化它。优化如下
@Test
public void filterStudentLambda(){
List<Student> list = filterStudent(students, (x)->x.getStudentID() >= 1004);
list.forEach(System.out::println);
}
优化后的代码十分简洁。
回头看,我们先从每一个需求都有一个自己专门的实现方法,代码冗余且耦合度高,再到使用设计模式来进行优化并解耦,但是根据需求而新增的实现类是这设计模式的痛点,再到使用匿名内部类来代替实现类,最后到使用 Lambda 表达式简化代码。
我们每一步都在逐步的减少代码量。
在上面我们使用 Lambda 表达式的时候依赖了 filterStudent 方法和 接口StudentFilter,其实Java8中还有一个方法可以做到不依赖这两个东西。
@Test
public void streamTest(){
students.stream()
.filter((e)->e.getName().equals("小红"))
.forEach(System.out::println);
}
打印结果:
Student{name='小红', age=14, studentID=1002}
这个 Stream 只是一个引子,它会在 Lambda 学习后紧跟着学习。很是方便。
但是这里也要提个醒,不要在代码中大量的使用 Lambda和Stream。因为对于不了解他们的同事来说,代码阅读起来时候很困难的。
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。