一、通过行为参数化传递代码
(一)、引言
- 在软件工程中,应用的需求不断变化,怎么办?
解决方案:行为参数化 —— 意味着:拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,将代码块作为参数传递给另一个方法,稍后再去执行它。
eg:行为参数化类似于:处理一个集合,会写以下方法:
可以对列表中的每个元素做“某件事”
可以在列表处理完后做“另一件事”
遇到错误时可以做“另外一件事”
(二)、应对不断变化的需求
引例:筛选绿苹果 ----> 把颜色或重量作为参数 ----> 对你能想到的每个属性做筛选
1、尝试一:值参数化
- 大量重复代码,繁琐,不能很好地应对变化的需求
2、尝试二:行为参数化
---->对选择标准建模:考虑的是苹果,需要根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值。我们把它称为谓词(即一个返回boolean值的函数)。
- 对选择标准建模:
public interface ApplePredicate{
boolean test (Apple apple);
}- 用ApplePredicate的多个实现代表不同的选择标准:
public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}这些标准看作filter方法的不同行为。在软件工程上,有很大好处:把filterApples方法迭代集合的逻辑与要应用到集合中每个元素的行为(这里是一个谓词)区分开了。
- 之后,根据抽象条件筛选,filter方法:
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p){
List<Apple> result = new ArrayList<>();
for(Apple apple: inventory){
if(p.test(apple)){
result.add(apple);
}
}
return result;
}(1)、传递代码/行为
代码已经灵活多了。
public class AppleRedAndHeavyPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "red".equals(apple.getColor()) && apple.getWeight() > 150;
}
}
List<Apple> redAndHeavyApples = filterApples(inventory, new AppleRedAndHeavyPredicate());filterApples方法的行为取决于通过ApplePredicate对象传递的代码。换句话说,把filterApples方法的行为参数化了。
(2)、多种行为,一个参数
行为参数化的好处:可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。
- eg:行为参数化:用谓词筛选苹果
public interface ApplePredicate{
boolean test (Apple apple);
}public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
return apple.getWeight() > 150;
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
return "green".equals(apple.getColor());
}
}
public class FilteringApples{
public static void main(String...args){
List<Apple> inventory = Arrays.asList(new Apple(80,"green"), new Apple(155, "green"), new Apple(120, "red"));
List<Apple> heavyApples = filterApples(inventory, new AppleHeavyWeightPredicate());
List<Apple> greenApples = filterApples(inventory, new AppleGreenColorPredicate());
}
public static List<Apple> filterApples(List<Apple> inventory, ApplePredicate p) {
List<Apple> result = new ArrayList<>();
for (Apple apple : inventory){
if (p.test(apple)){
result.add(apple);
}
}
return result;
}
}改善代码:Java 有一个机制称为 匿名类 ,它可以同时实现声明和实例化一个类。3、尝试三:匿名类
List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
public boolean test(Apple a){
return "red".equals(a.getColor());
}
});
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
System.out.println("Woooo a click!!");
}
});- 匿名类不够好。第一,比较笨重,占空间;第二,理解起来比较费解。
4、尝试四:Lambda 表达式
List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));5、尝试五:将 List 类型抽象化
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p){
List<T> result = new ArrayList<>();
for(T e: list){
if(p.test(e)){
result.add(e);
}
}
return result;
}- 抽象化,泛型,可以使用于不同的对象。
- eg:filter方法用在香蕉、桔子、 Integer或是String的列表上
List<Apple> redApples = filter(inventory, (Apple apple) -> "red".equals(apple.getColor()));
List<Integer> evenNumbers = filter(numbers, (Integer i) -> i % 2 == 0);- 超简单,既灵活又简洁。
(三)、实例
把一个行为(一段代码)封装起来,并通过传递和使用创建的行为将方法的行为参数化。
1、用 Comparator 来排序
- Java 8中, List自带了一个sort方法(你也可以使用Collections.sort)。 sort的行为可以用java.util.Comparator对象来参数化,它的接口如下:
// java.util.Comparator
public interface Comparator<T> {
public int compare(T o1, T o2);
}- 可以随时创建Comparator的实现,用sort方法表现出不同的行为。eg:
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
lambda表达式 ->
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));2、用 Runnable 执行代码块
- 线程就像是轻量级的进程:它们自己执行一个代码块。使用Runnable接口表示一个要执行的代码块。请注意,代码不会返回任何结果(即void):
// java.lang.Runnable
public interface Runnable{
public void run();
}- eg: 使用这个接口创建执行不同行为的线程:
Thread t = new Thread(new Runnable() {
public void run(){
System.out.println("Hello world");
}
});
Lambda表达式 ->
Thread t = new Thread(() -> System.out.println("Hello world"));3、GUI 事件处理
GUI编程的一个典型模式就是执行一个操作来响应特定事件,如鼠标单击或在文字上悬停。在JavaFX中,可以使用EventHandler,把它传给setOnAction来表示对事件的响应:
Button button = new Button("Send");
button.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent event) {
label.setText("Sent!!");
}
});setOnAction方法的行为就用EventHandler参数化了。用Lambda表达式:
button.setOnAction((ActionEvent event) -> label.setText("Sent!!"));二、Lambda表达式
(一)、引言
Lambda表达式可以简洁地表示一个行为或传递代码。Lambda表达式可以看作匿名功能,它基本上就是没有声明名称的方法,但和匿名类一样,它也可以作为参数传递给一个方法。
(二)、Lambda表达式简介
1、Lambda表达式
简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名 —— 它不像普通的方法那样有一个明确的名称:写得少而想得多!
- 函数 —— Lambda函数不像方法那样属于某个特定的类。但和方法一样, Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递 —— Lambda表达式可以作为参数传递给方法或存储在变量中。
- 简介 —— 无需像匿名类那样写很多模板代码。
eg:
先前代码 ->
Comparator<Apple> byWeight = new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
};
Lambda表达式 ->
Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());2、Lambda的基本语法
(parameters) -> expression 或 (parameters) -> { statements; }(三)、哪里以及如何use Lambda
1、函数式接口
- 函数式接口就是只定义一个抽象方法的接口。
- Lambda表达式允许直接以内联的形式为函数式接口的抽象方法提供实现, 并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。
2、函数描述符
函数式接口的抽象方法的签名基本上就是 Lambda表达式的签名。我们将这种抽象方法叫作 函数描述符。eg:
public void process(Runnable r){
r.run();
}
process(() -> System.out.println("This is awesome!!"));- 函数式接口的标注:@FunctionalInterface (不必要),这个标注用于表示该接口会设计成一个函数式接口。
(四)、实践:环绕执行模式
eg:资源处理 —— 打开一个资源,做一些处理,然后关闭资源。
、
Lambda表达式:
- 第1步:行为参数化
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());- 第2步:使用函数式接口来传递行为
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
public static String processFile(BufferedReaderProcessor p) throws IOException {
…
}- 第 3 步:执行一个行为
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br);
}
}- 第 4 步:传递 Lambda
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());(五)、使用函数式接口
- Java8中,除了见过的Comparable、 Runnable和 Callable,java.util.function包中引入了几个新的函数式接口 。
1、Predicate
- java.util.function.Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。
eg:定义一个接受String 对象的Lambda表达式。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
for(T s: list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
Predicate<String> nonEmptyStringPredicate = s -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);2、Consumer
- java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。
eg:创建一个forEach方法,接受一个Integers的列表,并对其中每个元素执行操作。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
public static <T> void forEach(List<T> list, Consumer<T> c){
for(T i: list){
c.accept(i);
}
}
forEach(Arrays.asList(1,2,3,4,5), i -> System.out.println(i))3、Function
- java.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。
到输出,就可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度)。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
public static <T, R> List<R> map(List<T> list, Function<T, R> f) {
List<R> result = new ArrayList<>();
for(T s: list){
result.add(f.apply(s));
}
return result;
}
// [7, 2, 6]
List<Integer> l = map(Arrays.asList("lambdas","in","action"), s -> s.length());4、原始类型特化
Java类型要么是引用类型(比如Byte、 Integer、 Object、 List),要么是原始类型(比如int、 double、 byte、 char)。但是泛型(比如Consumer<T>中的T)只能绑定到引用类型。
- 装箱:将原始类型转换为对应的引用类型的机制。
- 拆箱:将引用类型转换为对应的原始类型。
- 自动装箱机制:装箱和拆箱操作是自动完成的。
Java 8中的常用函数式接口
Lambdas及函数式接口的例子
5、注意
任何函数式接口都不允许抛出受检异常(checked exception)。
若需要Lambda表达式来抛出异常,有两种办法:
- 定义一个自己的函数式接口,并声明受检异常
- 把Lambda包在一个try/catch块中
(六)、类型检查、类型推断以及限制
1、类型检查
类型检查过程分解:
首先,你要找出filter方法的声明。
第二,要求它是Predicate<Apple>(目标类型)对象的第二个正式参数。
第三, Predicate<Apple>是一个函数式接口,定义了一个叫作test的抽象方法。
第四, test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
最后, filter的任何实际参数都必须匹配这个要求。
Object o = () -> {System.out.println("Tricky example"); };
Lambda表达式的上下文是Object(目标类型)。但Object不是一个函数式接口。为了解决这个问题,你可以把目标类型改成Runnable,它的函数描述符是:
Runnable r = () -> {System.out.println("Tricky example"); }2、类型推断
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。
3、使用局部变量
捕获Lambda:Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。即:Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。)
三、方法引用
(一)、简介
先前:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight));方法引用的基本思想是:若一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。方法引用就是让你根据已有的方法实现来创建Lambda表达式。
1、格式
使用方法引用时 ,目标引用放在分隔符 :: 前 ,方法的名称放在后面
Lambda 等效的方法引用
(Apple a) -> a.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println2、如何构建方法引用
方法引用主要有三类:
- 指向静态方法的方法引用。eg:Integer的parseInt方法,写作Integer::parseInt。
- 指向任意类型实例方法的方法引用。eg:String的length方法,写作String::length。
- 指向现有对象的实例方法的方法引用。eg:假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,写作expensiveTransaction::getValue
第二种:在引用一个对象的方法,而这个对象本身是Lambda的一个参数。
(String s) -> s.length() 写作 String::length
(String s) -> s.toUppeCase() 写作 String::toUpperCase第三种: 在 Lambda 中 调 用 一 个 已 经 存 在 的 外 部 对 象 中 的 方 法
()->expensiveTransaction.getValue() 写作 expensiveTransaction::getValueeg:
List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
写作 -->
List<String> str = Arrays.asList("a","b","A","B");
str.sort(String::compareToIgnoreCase);Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s); 写作 --> Integer::parseInt
BiPredicate<List<String>, String> contains = (list, element) -> list.contains(element); 写作 --> List::contains(二)、构造函数引用
对于一个现有构造函数,利用它的名称和关键字new来创建它的一个引用:ClassName::new。(类似于指向静态方法的引用类)
无参数构造函数:假设有一个构造函数没有参数。它适合Supplier的签名() -> Apple
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get();
等价于 -->
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get();有参数构造函数:构造函数的签名是Apple(Integer weight),那么它就适合Function接口的签名 。
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);
等价于 -->
Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(110);Java8中com.java.util.function包下:
1、Supplier<T>
@FunctionalInterface
public interface Supplier<T> {
T get();
}2、 Function<T, R>
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
/**
* Returns a composed function that first applies the {@code before}
* function to its input, and then applies this function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
/**
* Returns a composed function that first applies this function to
* its input, and then applies the {@code after} function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
/**
* Returns a function that always returns its input argument.
*/
static <T> Function<T, T> identity() {
return t -> t;
}
}3、 BiFunction<T, U, R>
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
/**
* Returns a composed function that first applies this function to
* its input, and then applies the {@code after} function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*/
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}4、自定义接口
可以自己创建一个:
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}可以像下面这样使用构造函数引用:TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new; 四、Lambda 和方法引用实战
- 用不同的排序策略给一个Apple列表排序的最终解决方案:
inventory.sort(comparing(Apple::getWeight));- 找寻最终解决方案过程:
1、传递代码
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());2、使用匿名类
inventory.sort(new Comparator<Apple>() {
public int compare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});3、使用 Lambda 表达式
inventory.sort((Apple a1, Apple a2)
-> a1.getWeight().compareTo(a2.getWeight())
);Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));简单写,即:
import static java.util.Comparator.comparing;
inventory.sort(comparing((a) -> a.getWeight()));4、使用方法引用 inventory.sort(comparing(Apple::getWeight));五、复合 Lambda 表达式的有用方法
1、比较器复合
使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator。
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);- 逆序:
inventory.sort(comparing(Apple::getWeight).reversed());- 比较器链:
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));2、谓词复合
谓词接口包括三个方法: negate、 and和 or,可以重用已有的Predicate来创建更复杂的谓词。- 使用negate方法来返回一个Predicate的非
Predicate<Apple> notRedApple = redApple.negate();Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getColor()));注意: and 和 or 方法是按照在表达式链中的位置,从左向右确定优先级的 。即:a.or(b).and(c)可以看作(a || b) && c。 3、函数复合
Function接口为此配了 andThen和 compose两个默认方法,它们都会返回Function的一个实例eg:假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2 ;
Function<Integer, Integer> h = f.andThen(g);
int result = h.apply(1);
System.out.println(result); --> 4 g(f(x))
h = f.compose(g);
result = h.apply(1);
System.out.println(result); --> 3 f(g(x))