1 行为参数化
使用场景:应对频繁变更的需求(的一种软件开发模式)
使用好处:”一个方法,多个行为”,即写一个方法,可以接受不同的新行为作为参数,实现不同的功能。
举个例子:你要处理一个集合,可能会写出这样的业务方法:
对列表中每个元素都做“事件A”
处理完列表后做“事件B”
遇到错误时做“事件E”
行为参数化就是将上面所述的事件A、B、E等行为提升为参数传递给这个业务方法
仍旧使用旧例子,大背景如下:一个菜单,菜单上有几十个菜,每个菜的属性为如下的Dish类:
/**
* 菜品
*/
@Data
@AllArgsConstructor
public class Dish {
private final String name; //菜名
private final boolean vegetarian; //是否为素菜
private int calories; //菜的热量
private final Type type; //菜的类型
public enum Type{ MEAT,FISH,OTHER}
}
public List<Dish> menu = Arrays.asList(
new Dish("pork",false,800,Dish.Type.MEAT),
new Dish("beef",false,700,Dish.Type.MEAT),
new Dish("chicken",false,400,Dish.Type.MEAT),
new Dish("french fries",true,530,Dish.Type.OTHER),
new Dish("rice",true,350,Dish.Type.OTHER),
new Dish("season fruit",true,120,Dish.Type.OTHER),
new Dish("pizza",true,550,Dish.Type.OTHER),
new Dish("prawns",false,300,Dish.Type.FISH),
new Dish("salmon",false,450,Dish.Type.FISH)
);
需求:对菜单menu数据集中的菜品Dish进行运算。
1.找出卡路里在300以上的菜品Dish。
(解决办法:java8以前你可能写一个方法1,直接遍历menu集合,按照条件得到卡路里大于300的菜品,存放在list中)
2.又增加了需求,需要找出鱼肉类蔬菜。
(又写了一个方法2,采用上面同样的思想遍历一遍,找到符合条件的菜品,存放在list中)
3.又增加了需求,找到符合上述两个条件的菜品?
(同样,又写了一个方法3遍历,选择菜品,存放在list,或者直接修改上面的方法1或者2)
4.其他需求……
应该怎么优化呢?
引入行为参数化:此时,需要让业务方法接受多种行为作为参数,并在内部使用,来完成不同的行为。
抽取需求行为(这种返回boolean类型的代码行为,我们叫他谓词)和共同行为分别为:
/**
* 函数式接口
*/
@FunctionalInterface //函数式接口声明注解,可省略
public interface Predicate<Dish> {
boolean pick(Dish dish);
}
//业务代码
public List<Dish> filterMenu(List<Dish> menu, Predicate<Dish> pre){
List<Dish> dishes=new ArrayList<>();
for(Dish dish:menu){
if(pre.pick(dish)){ //利用参数化的行为作为筛选条件
dishes.add(dish);
}
}
return dishes;
}
(1)策略模式
对,有的人想到了策略模式,将需求抽象为一个接口A,为每种需求都创建一个实现类来实现接口A,我们把这一系列实现类叫做策略模式的算法簇,当需要谁时就用谁。
//算法簇
public class HighCalories implements Predicate<Dish> { //需要高热量菜品
@Override
public boolean pick(Dish dish) {return dish.getCalories()>500; }
}
public class FishAndHighCalories implements Predicate<Dish> { //需要高热量鱼类菜品
@Override
public boolean pick(Dish dish) {
return dish.getCalories()>500&&dish.getType().equals(Dish.Type.FISH);
}
}
@Test
public void testStrategy() {
List<Dish> dishes=filterMenu(menu,new HighCalories());
List<Dish> dishes2=filterMenu(menu,new FishAndHighCalories());
}
此时可以根据需求编写灵活的算法,将其实例化带入即可。
But,使用策略模式,当有新的需求时,你不得不添加新的算法,实例化新的对象,比较啰嗦,也费时间!那么,能不能做的更好呢?
(2)匿名类
匿名类可以同时声明和实例化一个类,进一步改善代码,让它更简洁。
换句话说,匿名类可以随意创建
List<Dish> dishes= filterMenu(menu, new Predicate<Dish>(){
@Override
public boolean pick(Dish dish) {
return dish.getCalories()>500;
}
});
但是匿名类也比不太好,往往比较笨重,重写了很多代码,比如上面的pick方法签名和参数列表都要重写一遍,篇幅也较多。
所以,这种改善还是不到位,仍然有点啰嗦!所以java8引入了Lambda表达式—一种更为简洁的传递代码的方式。
(3)Lambda表达式
//蔬菜且热量大于500
List<Dish> dishes_5= filterMenu(menu,(Dish dish)->dish.isVegetarian()&&dish.getCalories()>500 );
采用这种方式,不用重新定义算法类,也无需使用臃肿的匿名类,直接使用lambda表达式,简洁的传递需求行为。
总之,从优化过程体现出的变化:
(4)数据抽象化
/**
* 函数式接口
*/
public interface Predicate<T> {
boolean pick(T t);
}
/**
* 抽象化数据,更加通用,通用方法
*/
public <T> List<T> filter(List<T> list, Predicate<T> pre){
List<T> result=new ArrayList<>();
for(T t:list){
if(pre.pick(t)){ //利用参数化的行为作为筛选条件
result.add(t);
}
}
return result;
}
List<Dish> dishes_4= filterMenu(menu,(Dish dish)->dish.isVegetarian() ); //选择蔬菜
真实实例:
@Test
public void test(){
//线程例子
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类实现的线程要执行的代码块A");
}
});
Thread thread=new Thread(()->System.out.println("Lambda实现的线程要执行的代码块A"));
t.run();
thread.run();
//比较器Comparator
List<Dish> menu=new ArrayList<>();
menu.sort(new Comparator<Dish>() {
@Override
public int compare(Dish o1, Dish o2) {
return o1.getCalories()-o2.getCalories();
}
});
menu.sort((Dish d1,Dish d2)->d1.getCalories()-d2.getCalories());
}
2 Lambda表达式
2.1 认识Lambda
我们可以把Lambda理解为匿名函数的一种简洁的传递形式。
匿名:没有方法签名,写的少想得多。
函数:lambda不属于某个类,但是像方法一样有自己的参数列表、函数主体、返回类型,还可能有异常列表。
简洁:lambda表达式不用过多的写模板代码。
传递:lambda表达式可以作为参数当成值传递给方法。
它的基本语法就是:
( parameters)->expression 或(含有花括号) (parameters)->{ statement ; }
两种形式,前者是表达式,后者含有花括号的是状态控制语句,要以分号结束。
Lambda表达式可以返回空、boolean、具体对象。典型的示例有:
Boolean | List<String> list->list.isEmpty() |
创建对象 | ()->new Dish() |
消费一个对象 | (Dish dish)->{System.out.println(dish.getName());} |
从对象中抽取数据 | (Dish dish)->dish.getCalories() |
组合两个值 | (int a,int b)->a*b |
比较两个对象 | (Dish d1,Dish d2)->d1.getCalories().compareTo(d2. getCalories()) |
2.2 Lambda如何使用
Lambda要建立在函数式接口上的。
函数式接口:只定义一个抽象方法的接口。诸如前面的:
public interface Predicate<T> {
boolean pick(T t);
}
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
都是函数式接口,@FunctionalInterface用于标注该接口是函数式接口,非必需,但推荐使用。
函数描述符:即函数式接口的抽象方法,因为该抽象方法签名也就是lambda表达式的方法签名。
而前面的menu.sort((Dish d1,Dish d2)->d1.getCalories()-d2.getCalories())这种lambda都是以内联的形式为函数式接口提供实现。
Lambda也适用于环绕行模式:一种“初始化-执行-关闭结束”的执行模式,使用lambda传递要执行的行为。
2.3 Java8中典型的函数式接口
2.3.1 Predicate
接受一个泛型T,返回一个boolean.
@FunctionalInterface //函数式接口声明注解,可省略
public interface Predicate<T> {
boolean test(T t);
}
2.3.2 Consumer
接受一个对象并消费,不返回值。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
2.3.4 Function
接受一个泛型T对象,返回一个泛型R对象。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
还有关于避免拆箱装箱的函数式接口:IntPredicate/LongToDoubleFunction/……
函数式接口那么多,编译器是如何确定我传入的lambda表达式是对哪个函数式接口实现呢?
2.4 Lambda验证机制
Java编译器会自动从上下文推断出用什么函数式接口来配合Lambda表达式。
上下文中lambda表达式需要的类型叫做目标类型。
比如: List<Dish> dishes_5= filterMenu(menu,(Dish dish)->dish.isVegetarian()&&dish.getCalories()>500 );
他的推断过程可以拆分成如下几步:
(1)先找到filterMenu()的声明
(2)要求lambda表达式是Predicate<T>对象的正式参数
(3)确认Predicate<T>是一个函数式接口,找到它定义抽象方法。
(4)该抽象方法描述了一个函数描述符:接受一个对象T,返回boolean。
(5)检查filterMenu的行为参数是否符合函数描述符要求的输入输出。
这样的检查机制就形成了一个闭环。
Lambda表达式可以使用外部的局部变量,但是只能使用final型的常量或者只被赋值一次的变量。
2.5 方法引用
方法引用可以让你重复使用已有的方法,使用已有的构造函数,自定义函数等等。如:
String::length,
Strategy::isHighCalories
等
此时编译器会进行一种与lambda表达式类似的类型检查机制。