先说结论,两个字:便捷。
在java7和之前的时代,程序员们需要为了一个简单的功能,写大量的代码,比如去重,比如排序,我们看到大量的for循环,大量的匿名内部类充斥在代码中,最后写完总觉得很烂,也有借用guava的,稍微写出了点气势,可总也不满意。
比如下面的去重操作:去除重复的数字
java7
List<Integer> list = Arrays.asList(1,2,3,3,2,4);
Set<Integer> integerSet = new HashSet<>();
for (Integer i:list) {
integerSet.add(i);
}
java8
Set integerSet = list.stream().collect(Collectors.toSet());
因为是个很简单的去重,大家看起来可能不觉得代码有多复杂,试想如果是给一组对象按照某一个或者某两个属性去重,复杂度如何呢,再加上排序呢。
Lambda表达式是Java8简洁的基础,在学写lambda之前,我们看一个例子。
1、现在有一群人,我们要对其按照年龄筛选,我希望得到年龄是10的人,很简单吧。
public static List filterTenYearsOldPerson(List peoples) {
List result = new ArrayList<>();
for (Person person : peoples) {
if (person.getAge() == 10) {//①
result.add(person);
}
}
return result;
}
上面①处是筛选条件,如果我改主意,想筛选年龄为8的人,应该怎么做呢,最简单的办法就是复制方法,把筛选条件改成8,不过这不是个好办法,如果我总是改主意,那就要不停地复制方法。下面我们想办法对其抽象化。
2、一个可以筛选不同年龄的方法,我们肯定能想到,把年龄作为方法的参数
public static List filterPersonByAge(List peoples, int age) {
List result = new ArrayList<>();
for (Person person : peoples) {
if (person.getAge() == age) {//①
result.add(person);
}
}
return result;
}
这样我们就实现了。当然,现在为止有点简单,我们希望可以处理更复杂问题,那接着看。
3、现在我想把这群人分成高矮两部分,就按照身高高于170算高。这也很简单对吧,那我们如法炮制
public static List filterPersonByHeight(List peoples, int height) {
List result = new ArrayList<>();
for (Person person : peoples) {
if (person.getHeight() > height) {//①
result.add(person);
}
}
return result;
}
筛选条件变了,这样,任务也算完成了,虽然我们依然复制了不少代码。不够最为一个有梦想的程序员,是不愿意这么做的,复制代码意味着抽象没做好,也意味着出错的概率更高,因为复制过来的代码很可能你改了一处,忘了改另一处。那么我们继续改造这个方法吧。
4、我们试着把筛选年龄和筛选身高放到一个方法。
public static List filterPerson(List peoples, int height, int age, boolean flag) {
List result = new ArrayList<>();
for (Person person : peoples) {
if ((flag && person.getHeight() > height) ||
(flag && person.getAge() == age)) {//①
result.add(person);
}
}
return result;
}
我们确实合并了,把筛选条件合并了,并且引入了一个boolean值,代表我们想筛选年龄还是身高。
虽然写出来了,可是感觉并不好,如果我想按照姓名筛选,那就要再加参数,最后变成复杂无比的方法,这不是我想要的,还有没有更好的办法了。当然,我想到了。
5、把行为抽象成接口
需要对我的选择标准建模:通过Person的某属性,返回boolean值
public interface PersonPredicate {
boolean test(Person person);
}
有了这个接口,我们就可以实现不同的筛选标准了
//选择年龄为10的人
public class PersonTenYearsOldPredicate implements PersonPredicate {
@Override
public boolean test(Person person) {
return person.getAge() == 10;
}
}
//选择高个子的人
public class PersonHeightHigherPredicate implements PersonPredicate {
@Override
public boolean test(Person person) {
return person.getHeight() > 170;
}
}
好了,上面有了我们的两个筛选策略,现在把一开始的那个方法稍加修改,给方法添加一个PersonPredicate参数
public static List filterPerson(List peoples, PersonPredicate predicate) {
List result = new ArrayList<>();
for (Person person : peoples) {
if (predicate.test(person)) {//①
result.add(person);
}
}
return result;
}
这样就满足了根据不同属性,筛选不同的人的功能了
//个子高的人
List higherPeople = filterPerson(people, new PersonHeightHigherPredicate());
//年龄为10的人
List TenYearsOldPeople = filterPerson(people, new PersonTenYearsOldPredicate());
如果我们的筛选功能只用一次,而为每个筛选提供一个实现未免有些浪费,当然,你也想到了,使用匿名类最好了。
6、使用匿名类
如果我们不想写那么多的实例,那用匿名类最合适不过
List higherPeople = filterPerson(people, new PersonPredicate() {
@Override
public boolean test(Person person) {
return person.getHeight() > 170;
}
});
List TenYearsOldPeople = filterPerson(people, new PersonPredicate() {
@Override
public boolean test(Person person) {
return person.getAge() == 10;
}
});
到这里,你会觉得依然熟悉,特别是经常写安卓的朋友一眼看出,这不就是每天经常写的给按钮加监听事件嘛,没错,现在我们才要讲重点。
上面的代码虽然已经很完美,可是匿名类仍然看着很臃肿,下面再改一下,就是我们的正题,Lambda表达式的引入。
7、使用Lambda表达式
现在用Lambda表达式重写一下6里面的最后提到的代码
List<Person> higherPeople = filterPerson(people, person -> person.getHeight() > 170);
List<Person> TenYearsOldPeople = filterPerson(people, person -> person.getAge() == 10);
哇,这么清爽,直接在传参的时候就把筛选条件传过去了,->前面的内容是PersonPredicate接口里test方法需要的参数,后面的内容是方法的返回值。这就是Lambda表达式的威力。这就叫行为参数化。
1、Lambda表达式组成
person -> person.getAge() == 10
或者
person -> {return person.getAge() == 10;}
或者
(Person person) -> {return person.getAge() == 10;}
两个参数的
Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
以上都是有效的,就是由参数,箭头,主体组成。参数就是方法需要几个参数,那就传几个参数,多个用括号括起来,主体就是表达式的返回值。
2、哪里可以用Lambda表达式
答案是你可以在函数式接口上使用Lambda表达式,什么是函数式接口呢,就是只有一个抽象方法的接口,就叫函数式接口。比如上面5中写的PersonPredicate接口,里面只有一个test方法,这就是函数式接口。Lambda表达式允许你直接以内联的方式为函数式接口里的方法提供实现,并把整个表达式作为接口实例。
3、函数式接口可以做什么
函数式接口里的抽象方法你可以理解为一种描述,比如我想要这么一个方法,我传进去一个Person类,返回一个boolean值,这就是一种描述,那么我就写出了PersonPredicate类和里面的test方法,Lambda形式就是T->boolean,
你可能还记得Runnable接口,他也是一个函数式接口,他不需要入参,也不返回值,那他的形式就是()->void,Java8也为我们提供了多种类似的形式的函数接口,供我们使用,如果他们提供的还不够,那我们就自己定义了。定义函数式接口的时候,最好给接口加上@FunctionalInterface注解,代表这是一个函数式接口。
4、方法引用
方法引用和Lambda表达式有的时候是可以转换的,方法引用就是我们可以使用现有方法像传递lambda表达式一样传递方法,比如
Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
可以看出Apple类里有个方法getWeight(),上面的代码就是对比两个Apple的Weight属性的大小,那么我就可以使用现成的getWeight()方法,以上代码修改为
comparing(Apple::getWeight)
在类和方法中间,使用::做分割,更为简洁,很多lambda表达式是可以转化为方法引用写的,以后的原则就是如果可以使用方法引用,最好使用,如果你使用的是idea的话,他会在你可以用的地方提醒你使用方法引用,省去一些思考的力气。
5、构造函数引用
构造函数引用形式:ClassName::new
比如Apple类有个无参构造函数,那描述它可以用()->Apple这样的形式,java8里有这样形式的接口为Supplier,那么我们要用lambda表达式实例化他就是
Supplier s = ()->new Apple();
Apple apple = s.get();
用构造函数引用,等价于
Supplier s = Apple::new;
Apple apple = s.get();
运用上面学到的,做个实战,对一个Person的list集合做排序这个集合我命名为people,并一步步实现最简单写法
1、传递代码
java8的List为我们提供了sort方法,
void sort(Comparator super E> c)
我们先写Comparator接口的实现,然后排序
public class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getHeight().compareTo(o2.getHeight());
}
}
people.sort(new PersonComparator());
2、用匿名类
使用匿名类可以更方便,不需要实现Comparator后再实例化,而是一步完成
people.sort(new Comparator() {
@Override
public int compare(Person o1, Person o2) {
return o1.getHeight().compareTo(o2.getHeight());
}
});
3、使用Lambda表达式
Comparator接口是一个函数式接口,是(T,T)->int形式的接口
people.sort((Person p1, Person p2) -> p1.getHeight().compareTo(p2.getHeight()));
这里我们发现已经简洁很多了,不过Comparator有个静态方法(java8里,接口是可以有默认方法的),接受一个Function来提取要对比的键值,返回一个Comparator对象。所以我们可以这么写
people.sort(Comparator.comparing(person -> person.getHeight()));
或更紧凑
people.sort(comparing(person -> person.getHeight()));
4、方法引用
因为getHeight()方法在Person类中存在,我们可以用现成的方法,使代码更简洁
people.sort(comparing(Person::getHeight));
到此,我们把上面1里面的代码经过处理,现在只有这么一点,同样完成任务,而且更简洁优美。
1、逆序
对于上面的例子,我们如果想递减排序,可以利用接口的另一个默认方法reversed(),于是可以直接这么写,
people.sort(comparing(Person::getHeight).reversed());
2、比较器链
上面的排序中,如果遇到身高一样的,应该怎么办呢,我们可以进一步比较,再比较年龄。
people.sort(comparing(Person::getHeight).reversed().thenComparing(Person::getAge));
3、谓词复合
谓词接口(函数式接口)包括3个方法,negate(非),and(和),or(或),你可以重用已有的Predicate来创建更复杂的谓词。
例如我有个高人群的Predicate
Predicate highPerson = person -> person.getHeight() > 170;
现在想要得到非高人群的Predicate
Predicate notHighPerson = highPerson.negate();
得到既高又是10岁的人Predicate
Predicate highAndTenOld = highPerson.and(person -> person.getAge() == 10);
以此类推。
4、函数复合
andThen和compose
例如有两个函数:
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
复合后
Function<Integer, Integer> h = f.andThen(g);
上面这个复合就是先对x做f,在做g操作
Function<Integer, Integer> h = f.compose(g);
这个就是对其先做g操作,再做f操作。
学会了Lambda表达式,是写抽象设计的基础,后面会发现有很多的以前的写法其实可以用更简单的方法重构,最后达到优雅与性能并存。
java8真正的厉害之处,在stream,是后面博客会写到的,欢迎关注。