《Java8实战》第3章 Lambda 表达式
利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一段代码块来表示一个行为,然后传递它。
采用匿名类来表示多种行为并不令人满意:代码十分啰唆,这会影响程序员在实践中使用行为参数化的积极性。
3.1 Lambda 管中窥豹
可以把 Lambda 表达式理解为一种简洁的可传递匿名函数:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。这个定义够大的,让我们慢慢道来。
- 匿名——说它是匿名的,因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
- 函数——说它是一种函数,是因为 Lambda 函数不像方法那样属于某个特定的类。但和方法一样,Lambda 有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
- 传递——Lambda 表达式可以作为参数传递给方法或存储在变量中。
- 简洁——你无须像匿名类那样写很多模板代码。
Lambda 解决了这个问题:它可以让你十分简明地传递代码。理论上来说,你在 Java 8 之前做不了的事情,Lambda 也做不了.
利用 Lambda 表达式,你可以更为简洁地自定义一个 Comparator 对象。
之前
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());
lambda有三个部分:
- 参数列表——这里它采用了 Comparator 中 compare 方法的参数,两个 Apple。
- 箭头——箭头 -> 把参数列表与 Lambda 主体分隔开。
- Lambda 主体——比较两个 Apple 的重量。表达式就是 Lambda 的返回值。
例子:
Lambda 表达式具有一个 String 类型的参数并返回一个 int。Lambda 没有 return 语句,因为已经隐含了 return
(String s) -> s.length() Lambda表达式有一个Apple类型的参数并返回一个 boolean(苹果的重量是否超过 150 克)
(Apple a) -> a.getWeight() > 150Lambda 表达式具有两个 int 类型的参数而没有返回值(void返回)。注意 Lambda 表达式可以包含多行语句,这里是两行
(int x, int y) -> { System.out.println("Result:"); System.out.println(x + y);
} Lambda 表达式没有参数,返回一个 int
() -> 42 Lambda表达式具有两个 Apple 类型的参数,返回一个 int:比较两个 Apple 的重量
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) 基本语法
(parameters) -> expression
块风格
(parameters) -> { statements; }
测验 3.1:Lambda 语法
以下哪个不是有效的 Lambda 表达式?
(1) () -> {}
(2) () -> “Raoul”
(3) () -> {return “Mario”;}
(4) (Integer i) -> return “Alan” + i;
(5) (String s) -> {“Iron Man”;}
答案:只有(4)和(5)是无效的 Lambda,其余都是有效的。详细解释如下。
(1) 这个 Lambda 没有参数,并返回 void。它类似于主体为空的方法:public void run() {}。一个有趣的事实:这种 Lambda 也经常被叫作“汉堡型 Lambda”。 如果只从一边看,它的形状就像是两块圆面包组成的汉堡。
(2) 这个 Lambda 没有参数,并返回 String 作为表达式。
(3) 这个 Lambda 没有参数,并返回 String(利用显式返回语句)。
(4) return 是一个控制流语句。要使此 Lambda 有效,需要使用花括号,如下所示:(Integer i) -> {return “Alan” + i;}
(5)“Iron Man”是一个表达式,不是一个语句。要使此 Lambda 有效,可以去除花括号和
分号,如下所示:
(String s) -> “Iron Man”
或者如果你喜欢,可以使用显式返回语句,如下所示:
(String s) -> {return “Iron Man”;}
3.2 在哪里以及如何使用 Lambda
可以在函数式接口上使用 Lambda 表达式。但是方法参数需要是这个Predicate类型的
3.2.1 函数式接口
Predicate 它就是一个函数式接口
函数式接口就是只定义一个抽象方法的接口。 Java API 中的一些其他函数式接口
下面哪些接口是函数式接口?
public interface Adder {
int add(int a, int b);
}
public interface SmartAdder extends Adder {
int add(double a, double b);
}
public interface Nothing {
}
答案:只有 Adder 是函数式接口。
SmartAdder 不是函数式接口,因为它定义了两个叫作 add 的抽象方法(其中一个是从Adder 那里继承来的)。
Nothing 也不是函数式接口,因为它没有声明抽象方法
用函数式接口可以干什么呢?Lambda 表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)
3.2.2 函数描述符
函数式接口的抽象方法的签名基本上就是 Lambda 表达式的签名。我们将这种抽象方法叫作函数描述符。
Runnable 接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作 run 的抽象方法,这个方法什么也不接受,什么也不返回(void)。
Lambda 及空方法调用
虽然下面这种 Lambda 表达式调用看起来很奇怪,但是合法的:
process(() -> System.out.println(“This is awesome”));
System.out.println 返回 void,所以很明显这不是一个表达式!为什么不像下面这样用花括号环绕方法体呢?
process(() -> { System.out.println(“This is awesome”); });
结果表明,方法调用的返回值为空时,Java 语言规范有一条特殊的规定。这种情况下,你不需要使用括号环绕返回值为空的单行方法调用。以下哪些是使用 Lambda 表达式的有效方式?
(1) execute(() -> {});
public void execute(Runnable r){ r.run(); }
(2) public Callable fetch() { return () -> “Tricky example 😉”; }
(3) Predicate p = (Apple a) -> a.getWeight();
答案:只有(1)和(2)是有效的。
第(1)个例子有效,是因为 Lambda() -> {}具有签名() -> void,这和 Runnable 中的抽象方法 run 的签名相匹配。请注意,此代码运行后什么都不会做,因为 Lambda 是空的!
第(2)个例子也是有效的。事实上,fetch 方法的返回类型是 Callable。Callable基本上就定义了一个方法,签名是() -> String,其中 T 被 String代替了。因为 Lambda() -> "Trickyexample;-)"的签名是() -> String,所以在这个上下文中可以使用 Lambda。
第(3)个例子无效,因为 Lambda 表达式(Apple a) -> a.getWeight()的签名是(Apple) -> Integer,这和 Predicate: (Apple) -> boolean 中定义的 test 方法的签名不同。
3.3 把 Lambda 付诸实践:环绕执行模式
在进行文件读取的时候,基本上就是 打开-处理-关闭 这几个模式。大多情况下 打开和关闭都是类似的操作,所以我们把这两个抽取出来
在以下代码中,加粗显示的就是从一个文件中读取一行所需的模板代码
3.3.1 第 1 步:记得行为参数化
但是这段代码的是有局限的,如果我想读取两行,或者是拼接的返回,那我我可能需要重写一个方法,也是复制在修改一下。
那么我们是不是可以把processFile方法的行为参数化呢?
如果想读取两行,写法可以是这样的:String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
3.3.2 第 2 步:使用函数式接口来传递行为
Lambda 仅可用于上下文是函数式接口的情况。你需要创建一个能匹配BufferedReader -> String,还可以抛出 IOException 异常的接口
@FunctionalInterface
public interface BufferedReaderProcessor { String process(BufferedReader b) throws IOException;
}
processFile 方法的参数public String processFile(BufferedReaderProcessor p) throws IOException {}
3.3.3 第 3 步:执行一个行为
任何 BufferedReader -> String 形式的 Lambda 都可以作为参数来传递,因为它们符合BufferedReaderProcessor 接口中定义的 process 方法的签名。
Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
public String processFile(BufferedReaderProcessor p) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { // 处理 BufferedReader 对象return p.process(br);}
}
3.3.4 第 4 步:传递 Lambda
现在通过传递Lambda 来重用 processFile 方法。
处理一行:
String oneLine = processFile((BufferedReader br) -> br.readLine());
处理两行:
String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());
总结了所采取的使 pocessFile 方法更灵活的四个步骤
3.4 使用函数式接口
函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述 Lambda 表达式的签名。函数式接口的抽象方法的签名称为函数描述符。
3.4.1 Predicate
java.util.function.Predicate接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean
@FunctionalInterface
public interface Predicate<T> { boolean test(T t);
}
public <T> List<T> filter(List<T> list, Predicate<T> p) { List<T> results = new ArrayList<>(); for(T t: list) { if(p.test(t)) { results.add(t); } } return results;
}
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
3.4.2 Consumer
java.util.function.Consumer接口定义了一个名叫 accept 的抽象方法,它接受泛型 T 的对象,没有返回(void)。你如果需要访问类型 T 的对象,并对其执行某些操作,就可以使用这个接口。
@FunctionalInterface
public interface Consumer<T>{ void accept(T t);
}
public <T> void forEach(List<T> list, Consumer<T> c){ for(T i: list){ c.accept(i); }
}
forEach( Arrays.asList(1,2,3,4,5), (Integer i) -> System.out.println(i) // Lambda 是 Consumer中 accept 方法的实现);
3.4.3 Function
java.util.function.Function<T, R>接口定义了一个叫作 apply 的抽象方法,它接受泛型 T 的对象,并返回一个泛型 R 的对象。
@FunctionalInterface
public interface Function<T, R> { R apply(T t);
}
public <T, R> List<R> map(List<T> list, Function<T, R> f) { List<R> result = new ArrayList<>(); for(T t: list) { result.add(f.apply(t)); } return result;
}
// [7, 2, 6]
List<Integer> l = map( Arrays.asList("lambdas", "in", "action"), (String s) -> s.length()); //Lambda是Function接口的 apply 方法的实现
Java 类型要么是引用类型要么是基本类型,但是泛型(比如 Consumer中的 T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。
把基本类型转化成引用类型,这个方法叫做装箱(boxing)。相反,把引用类型转成基本类型,就叫拆箱,
装箱是有性能代价的,装箱后的值本质上就是把基本类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的基本值。
在下面的代码中,使用 IntPredicate 就避免了对值 1000 进行装箱操作,但要是用 Predicate就会把参数 1000 装箱到一个 Integer 对象中:
public interface IntPredicate { boolean test(int t);
}
// true(无装箱)
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);
// false(装箱)
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 != 0;
oddNumbers.test(1000);
一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的基本类型前缀,比如 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction 等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction 等。
3.5 类型检查、类型推断以及限制
3.5.1 类型检查
Lambda 的类型是从使用 Lambda 的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中 Lambda 表达式需要的类型称为目标类型。
例子:List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);
类型检查过程分解如下。
- 你要找出 filter 方法的声明。
- 要求它是 Predicate(目标类型)对象的第二个正式参数。
- Predicate是一个函数式接口,定义了一个叫作 test 的抽象方法。
- test 方法描述了一个函数描述符,它可以接受一个 Apple,并返回一个 boolean。
- filter 的任何实际参数都必须匹配这个要求。
3.5.2 同样的 Lambda,不同的函数式接口
有了目标类型的概念,同一个 Lambda 表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
3.5.3 类型推断
Java 编译器会从上下文(目标类型)推断出用什么函数式接口来配合 Lambda 表达式,这意味着它也可以推断出适合 Lambda 的签名,
参数 apple 没有显式类型List<Apple> greenApples = filter(inventory, apple -> GREEN.equals(apple.getColor()));
可以这样来创建一个Comparator 对象:
没有类型推断Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
有类型推断Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
3.5.4 使用局部变量
我们迄今为止所介绍的所有 Lambda 表达式都只用到了其主体里面的参数。但 Lambda 表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
Lambda 可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为 final,或事实上是 final。
下面这个例子是错误的
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
因为Lambda 表达式引用的局部变量必须是最终的(final)或事实上最终的
对局部变量的限制
实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,局部变量则保存在栈上。
这一限制不鼓励你使用改变外部变量的典型命令式编程模式。
方法引用。可以把它们视为某些Lambda 的快捷写法。
3.6 方法引用
方法引用让你可以重复使用现有的方法定义,并像 Lambda 一样传递它们。
先前:inventory.sort((Apple a1, Apple a2) a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和 java.util.Comparator.comparing):inventory.sort(comparing(Apple::getWeight));
3.6.1 管中窥豹
如果一个 Lambda 代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。
方法引用就是让你根据已有的方法实现来创建Lambda 表达式。但是,显式地指明方法的名称,你的代码的可读性会更好。当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight 就是引用了 Apple 类中定义的方法 getWeight。getWeight 后面不需要括号,因为你没有实际调用这个方法,只是引用了它的名称。
Lambda 表达式重构为等价方法引用的简易速查表
List<String> str = Arrays.asList("a","b","A","B");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2)); Lambda 表达式的签名与 Comparator 的函数描述符兼容 可以修改成这样的
str.sort(String::compareToIgnoreCase);
测验 3.6:方法引用
下列 Lambda 表达式的等效方法引用是什么?
(1) ToIntFunction stringToInt = (String s) -> Integer.parseInt(s);
(2) BiPredicate<List, String> contains = (list, element) -> list.contains(element);
(3) Predicate startsWithNumber = (String string) -> this .startsWithNumber(string);
答案:(1) 这个 Lambda 表达式将其参数传给了 Integer 的静态方法 parseInt。这种方法接受一个需要解析的 String,并返回一个 Integer。因此,可以使用图 3-5 中的办法➊(Lambda 表达式调用静态方法)来重写 Lambda 表达式,如下所示:**ToIntFunction stringToInt = Integer::parseInt; **
(2) 这个 Lambda 使用其第一个参数,调用其 contains 方法。由于第一个参数是 List类型的,因此你可以使用图 3-5 中的办法➋,如下所示:BiPredicate<List, String> contains = List::contains; 这是因为,目标类型描述的函数描述符是(List,String) -> boolean,而List::contains 可以被解包成这个函数描述符。
(3) 这种“表达式–风格”的 Lambda 会调用一个私有方法。你可以使用图 3-5 中的办法❸,如下所示:
Predicate startsWithNumber = this::startsWithNumber
3.6.2 构造函数引用
对于一个现有构造函数,你可以利用它的名称和关键字 new 来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。
例如,假设有一个构造函数没有参数。它适合 Supplier 的签名() -> Apple。你可以这样做:
List<Integer> weights = Arrays.asList(7, 3, 4, 10);
List<Apple> apples = map(weights, Apple::new); // 将构造函数引用传递给 map 方法
public List<Apple> map(List<Integer> list, Function<Integer, Apple> f) { List<Apple> result = new ArrayList<>(); for(Integer i: list) { result.add(f.apply(i)); } return result;
}
如果你有一个具有两个参数的构造函数 Apple(String color, Integer weight),那么它就适合 BiFunction 接口的签名,于是你可以这样写:
如果有构造参数 ,那么Apple::new;这样返回一个对象,怎么知道我的构造参数呢?
测验 3.7:构造函数引用
你已经看到了如何将有零个、一个、两个参数的构造函数转变为构造函数引用。那要怎么样才能对具有三个参数的构造函数,比如 RGB(int, int, int),使用构造函数引用呢?
答案:你看,构造函数引用的语法是 ClassName::new,那么在这个例子里面就是 RGB::new。但是你需要与构造函数引用的签名匹配的函数式接口。由于语言本身并没有提供这样的函数式接口,因此你可以自己创建一个:
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
现在你可以像下面这样使用构造函数引用了:
TriFunction<Integer, Integer, Integer, RGB> colorFactory = RGB::new;
3.7 Lambda 和方法引用实战
用不同的排序策略给一个 Apple 列表排序,并需要展示如何把一个原始粗暴的解决方案转变得更为简明。这会用到书中迄今讲到的所有概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。我们想要实现的最终解决方案是这样的:inventory.sort(comparing(Apple::getWeight));
3.7.1 第 1 步:传递代码
Java 8 API 已经为你提供了一个 List 可用的 sort 方法。那么最困难的部分已经搞定了!但是,如何把排序策略传递给 sort 方法呢?sort 方法的签名是这样的:void sort(Comparator<? super E> c)
sort 的行为被参数化了:传递给它的排序策略不同,其行为也会不同。
方案:
public class AppleComparator implements Comparator<Apple> { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight());}
}
inventory.sort(new AppleComparator());
3.7.2 第 2 步:使用匿名类
使用匿名内部类修改
inventory.sort(new Comparator<Apple>() { public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); }
});
3.7.3 第 3 步:使用 Lambda 表达式
lambda传递代码方式,函数式接口inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
Java 编译器可以根据 Lambda 出现的上下文来推断 Lambda 表达式参数的类型。那么可以重写成这样:inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
Comparator 具有一个叫作 comparing 的静态辅助方法,它可以接受一个 Function 来提取 Comparable 键值,并生成一个 Comparator 对象Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
可以改成这样import static java.util.Comparator.comparing;
// 静态导入inventory.sort(comparing(apple -> apple.getWeight()));
3.7.4 第 4 步:使用方法引用
假设你静态导入了 java.util.Comparator.comparing
inventory.sort(comparing(Apple::getWeight));
3.8 复合 Lambda 表达式的有用方法
Java 8 的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递 Lambda 表达式的 Comparator、Function 和 Predicate 都提供了允许你进行复合的方法。
3.8.1 比较器复合
我们前面看到,你可以使用静态方法 Comparator.comparing,根据提取用于比较的键值的 Function 来返回一个 Comparator,如下所示:Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
- 逆序
用不着去建立另一个 Comparator 的实例。接口有一个默认方法 reversed 可以使给定的比较器逆序inventory.sort(comparing(Apple::getWeight).**reversed**());
// 按重量递减排序
- 比较器链
如果苹果一样重呢?需要进一步的按照其他属性排序
如果两个对象用第一个 Comparator 比较之后是一样的,就提供第二个 Comparator。你又可以优雅地解决这个问题了:
inventory.sort(comparing(Apple::getWeight) .reversed() // 按重量递减排序.thenComparing(Apple::getCountry)); // 两个苹果一样重时,进一步按国家排序
3.8.2 谓词复合
谓词接口包括三个方法:negate、and 和 or,让你可以重用已有的 Predicate 来创建更复杂的谓词。比如,你可以使用 negate 方法来返回一个 Predicate 的非,比如苹果不是红的:
// 产生现有 Predicate对象 redApple 的非**Predicate<Apple> notRedApple = redApple.negate();**
想要把两个 Lambda 用 and 方法组合起来,比如一个苹果既是红色又比较重:
// 链接两个谓词来生成另一个 Predicate 对象Predicate<Apple> redAndHeavyApple = redApple.and(apple -> apple.getWeight() > 150);
进一步组合谓词,表达要么是重(150 克以上)的红苹果,要么是绿苹果:
Predicate redAndHeavyAppleOrGreen =
redApple.and(apple -> apple.getWeight() > 150)
.or(apple -> GREEN.equals(a.getColor()));
and 和 or 方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and©可以看作(a || b) && c。同样,a.and(b).or© 可以看作(a && b) || c。
3.8.3 函数复合
还可以把 Function 接口所代表的 Lambda 表达式复合起来。Function 接口为此配了 andThen 和 compose 两个默认方法,它们都会返回 Function 的一个实例。
andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。
Function<Integer, Integer> f = x -> x + 1; // 函数 f 给数字加 1 (x -> x + 1)
Function<Integer, Integer> g = x -> x * 2; // 函数 g 给数字乘 2
// 数学上会写作 g(f(x))或(g o f)(x)
Function<Integer, Integer> h = f.andThen(g); // 组合成一个函数 h,先给数字加 1,再给结果乘 2
int result = h.apply(1); // 返回 4
也可以类似地使用 compose 方法,先把给定的函数用作 compose 的参数里面给的那个函数,然后再把函数本身用于结果
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
// 数学上会写作 f(g(x))或(f o g)(x)
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); // 返回 3
andThen 和 compose 之间的区别。
实际使用
比方说你有一系列工具方法,对用 String 表示的一封信做文本转换:
public class Letter{ public static String addHeader(String text){ return "From Raoul, Mario and Alan: " + text; } public static String addFooter(String text){ return text + " Kind regards"; } public static String checkSpelling(String text){ return text.replaceAll("labda", "lambda"); }
}
可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如图 3-7 所示。
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::checkSpelling) .andThen(Letter::addFooter);
第二个流水线可能只加抬头、落款,而不做拼写检查:
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline = addHeader.andThen(Letter::addFooter);
3.9 数学中的类似思想
3.9.1 积分
数学函数:f (x) = x + 10
求函数下方的面积
3.9.2 与 Java 8 的 Lambda 联系起来
Java 8 的表示法(double x) -> x + 10(一个 Lambda 表达式)恰恰就是为此设计的,因此你可以写:
**integrate((double x) -> x + 10, 3, 7) **
或者
**integrate((double x) -> f(x), 3, 7) **
或者使用方法引用的方式
**integrate(C::f, 3, 7) **// 这里 C 是包含静态方法 f 的一个类。理念就是把 f 背后的代码传给 integrate 方法。
integrate 方法本身,数学的形式。(Java函数的写法不能像数学里那样)
public double integrate((double -> double) f, double a, double b) {return (f(a) + f(b)) * (b - a) / 2.0
}
或者用 DoubleUnaryOperator,这样也可以避免对结果进行装箱:
public double integrate(DoubleUnaryOperator f, double a, double b) { return (f.applyAsDouble(a) + f.applyAsDouble(b)) * (b - a) / 2.0;
}
有点可惜的是你必须写 f.apply(a),而不是像数学里面写 f(a),但 Java 无法摆脱“一切都是对象”的思想——它不能让函数完全独立!
3.10 小结
- Lambda 表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
- Lambda 表达式让你可以简洁地传递代码。
- 函数式接口就是仅仅声明了一个抽象方法的接口。
- 只有在接受函数式接口的地方才可以使用 Lambda 表达式。
- Lambda 表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
- Java 8自带一些常用的函数式接口,放在 java.util.function 包里,包括 Predicate 、Function<T, R>、Supplier、Consumer和 BinaryOperator,如表 3-2所述。
- 为了避免装箱操作,对 Predicate和 Function<T, R>等通用函数式接口的基本类型特化:IntPredicate、IntToLongFunction 等。
- 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合 Lambda 提高灵活性和可重用性。
- Lambda 表达式所需要代表的类型称为目标类型。
- 方法引用让你重复使用现有的方法实现并直接传递它们。
- Comparator、Predicate 和 Function 等函数式接口都有几个可以用来结合 Lambda表达式的默认方法。
相关文章:

《Java8实战》第3章 Lambda 表达式
利用行为参数化来传递代码有助于应对不断变化的需求。它允许你定义一段代码块来表示一个行为,然后传递它。采用匿名类来表示多种行为并不令人满意:代码十分啰唆,这会影响程序员在实践中使用行为参数化的积极性。 3.1 Lambda 管中窥豹 可以…...

开放式耳机的颠覆之作!南卡OE Pro新皇降临!佩戴和音质双重突破
千呼万唤的南卡OE Pro终于要在最近正式官宣上线,此消息一经放出,蓝牙耳机市场就已经沸腾。NANK南卡品牌作为国内的音频大牌,发展和潜力一直备受业内关注,这次要上线的南卡OE Pro更是南卡十余年来积累的声学技术结晶之一。 据透露…...

生成器设计模式(Builder Design Pattern)[论点:概念、图示、示例、框架中的应用、场景]
文章目录概念相关图示代码示例框架中的应用场景多个生成器(Concrete Builder):单个生成器概念 生成器设计模式(Builder Design Pattern)是一种创建型设计模式,用于处理具有多个属性和复杂构造过程的对象。生…...

JUC并发工具
JUC并发工具 一、CountDownLatch应用&源码分析 1.1 CountDownLatch介绍 CountDownLatch就是JUC包下的一个工具,整个工具最核心的功能就是计数器。 如果有三个业务需要并行处理,并且需要知道三个业务全部都处理完毕了。 需要一个并发安全的计数器来操作。 CountDown…...
java面试题-基础问题-如何理解Java中的多态?
如何理解Java中的多态?如何理解Java中的多态?典型回答扩展知识方法的重载与重写重载和重写的区别如何理解Java中的多态? 典型回答 多态的概念比较简单,就是同一操作作用于不同的对象,可以有不同的解释,产…...

03.vue3的计算属性
文章目录1.计算属性1.get()和set()2.computed的简写3.computed和methods对比2.相关demo1.全选和反选2.todos列表1.计算属性 模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。所以,对于任何…...
Ceph性能调优
1. 最佳实践 1.1 基本 监控节点对于集群的正确运行非常重要,应当为其分配独立的硬件资源。如果跨数据中心部署,监控节点应该分散在不同数据中心或者可用性区域日志可能会让集群的吞吐量减半。理想情况下,应该在不同磁盘上运行操作系统、OSD…...
机器学习-问答题准备(英文)-更新中
第一章 入门 How would you define Machine Learning? Machine Learning is about building systems that can learn from data. Learning means getting better at some task, given some performance measure. Can you name four types of problems where it shines? To r…...

展示演示软件设计制作(C语言)
展示演示软件设计制作 所谓展示演示软件就像是PPT那样的东西。PPT是幻灯片式的展示,而我设计的软件是多媒体的,多样展示方法的,多种功能的。可以扩展为产品展示,项目介绍,景点导游,多媒体授课,…...

Android 自定义view 入门 案例
自定义一个圆环进度条: 1.首页Android Studio创建一个项目 2.在项目src/xxx/目录下右键选择创建一个自定义view页面:new->UICompoent->customer view 3.输入自定义名称,选择开发语言 4.确定之后,自动生成3个文件一个是&…...

[imangazaliev/didom]一个简单又快速的DOM操作库
DiDOM是一个功能齐全、易于使用和高性能的解析器和操作库,可以帮助PHP开发者更加高效地处理HTML文档。 为了更好地了解这个项目,我们先来看看下面的介绍。 安装 你可以使用composer来安装DiDOM,只需要在你的项目目录下执行下面的命令&…...

Cookie和Session的工作流程及区别(附代码案例)
目录 一、 HTTP协议 1.1 为什么HTTP协议是无状态的? 1.2 在HTTP协议中流式传输和分块传输编码的区别 二、Cookie和Session 2.1 Cookie 2.2 Session 2.3 Cookie和Session的区别 三、servlet中与Cookie和Session相关的API 3.1 HttpServletRequest 类中的相关方…...

适用于高级别自动驾驶的驾驶员可预见误用仿真测试
摘要 借助高级别自动驾驶(HAD),驾驶员可以从事与驾驶无关的任务。在系统出现失效的情况下,驾驶员应该合理地重新获得对自动驾驶车辆(AV)的控制。不正确的系统理解可能会引起驾驶员的误操作,并可能导致车辆级的危害。ISO 21448预期功能安全标…...

Linux之进程知识点
一、什么是进程 进程是一个运行起来的程序。 问题思考: ❓ 思考:程序是文件吗? 是!都读到这一章了,这种问题都无需思考!文件在磁盘哈。 本章一开始讲的冯诺依曼,磁盘就是外设,和内…...

一种供水系统物联网监测系统
1.1供水系统 1.1.1监测范围选择依据 (1)管网老化区域管网 管网建设年代久远,通常管网发生破损问题较大,根据管网本身属性和历史发生事件的统计分析,结合数理统计,优先选择管网老化区域的管段所在区域进行…...

Linux驱动开发——字符设备(2)
目录 虚拟串口设备驱动 一个驱动支持多个设备 习题 虚拟串口设备驱动 字符设备驱动除了前面搭建好代码的框架外,接下来最重要的就是要实现特定于设备的操作方法,这是驱动的核心和关键所在,是一个驱动区别于其他驱动的本质所在,…...

【MySQL数据库原理】MySQL Community安装与配置
目录 安装成功之后查看版本验证1、介绍、安装与配置数据库2、操作MySQL数据库3、MySQL数据库原理安装成功之后查看版本验证 SELECT VERSION();查看mysql版本号 1、介绍、安装与配置数据库 下载安装包:https://download.csdn.net/download/weixin_41194129/87672588 MySQL…...

【ROS参数服务器增删改c++操作1】
需求:实现参数服务器参数的增删改查操作。 在C中实现参数服务器数据的增删改查,可以通过两套API实现:. ros::NodeHandle ros::param下面为具体操作演示: 在src下面的自己定义的作用包下面新建文件。 比如我的是一直存在的demo03_ws文件下的src里面&…...
elasticsearch 常用数据类型详解和范例
主要内容 elasticsearch 中的字符串(keyword)类型 的详解和范例 elasticsearch 中的字符串/文本(text)类型 的详解和范例 elasticsearch 中的数字(数值)类型 的详解和范例 elasticsearch 中的布尔&#…...

力扣119杨辉三角 II:代码实现 + 方法总结(数学规律法 记忆法/备忘录)
文章目录第一部分:题目第二部分:解法①-数学规律法2.1 规律分析2.2 代码实现2.3 需要思考第三部分:解法②-记忆法(备忘录)第四部分:对比总结第一部分:题目 🏠 链接:119.…...
【HTML-15.2】HTML表单按钮全面指南:从基础到高级实践
表单按钮是网页交互的核心元素,作为用户提交数据、触发操作的主要途径,其重要性不言而喻。本文将系统性地介绍HTML表单按钮的各种类型、使用场景、最佳实践以及高级技巧,帮助开发者构建更高效、更易用的表单交互体验。 1. 基础按钮类型 1.1…...

易学探索助手-个人记录(十二)
近期我完成了古籍处理板块页面升级,补充完成原文、句读、翻译的清空、保存和编辑(其中句读仅可修改标点)功能,新增原文和句读的繁简体切换功能 一、古籍处理板块整体页面升级 将原来一整个页面呈现的布局改为分栏呈现࿰…...
地震资料裂缝定量识别——学习计划
学习计划 地震资料裂缝定量识别——理解常规采集地震裂缝识别方法纵波各向异性方法蚁群算法相干体及倾角检测方法叠后地震融合属性方法裂缝边缘检测方法 非常规采集地震裂缝识别方法P-S 转换波方法垂直地震剖面方法 学习计划 地震资料裂缝定量识别——理解 地震资料裂缝识别&a…...

智能守护电网安全:探秘输电线路测温装置的科技力量
在现代电力网络的庞大版图中,输电线路如同一条条 “电力血管”,日夜不息地输送着能量。然而,随着电网负荷不断增加,长期暴露在户外的线路,其线夹与导线在电流热效应影响下,极易出现温度异常。每年因线路过热…...

从法律层面剖析危化品证书:两证一证背后的安全逻辑
《安全生产法》第 24 条明确规定,危化品单位主要负责人和安全管理人员 “必须考核合格方可上岗”。这并非仅仅是行政要求,而是通过法律来筑牢安全防线。在某危化品仓库爆炸事故中,由于负责人未持证,导致事故责任升级,企…...

快手可灵视频V1.6模型API如何接入免费AI开源项目工具
全球领先的视频生成大模型:可灵是首个效果对标 Sora 、面向用户开放的视频生成大模型,目前在国内及国际上均处于领先地位。快手视频生成大模型“可灵”(Kling),是全球首个真正用户可用的视频生成大模型,自面…...

C++学习-入门到精通【12】文件处理
C学习-入门到精通【12】文件处理 目录 C学习-入门到精通【12】文件处理一、文件和流二、创建顺序文件三、从顺序文件读取数据文件定位指针对之前的程序进行修改:贷款查询程序 四、更新顺序文件五、随机存取文件1.创建随机存取文件2.修改程序:贷款处理程序…...

多模态AI的企业应用场景:视觉+语言模型的商业价值挖掘
关键词:多模态AI | 视觉语言模型 | 企业应用 | 商业价值 | 人工智能 📚 文章目录 一、引言:多模态AI时代的到来二、多模态AI技术架构深度解析三、客服场景:智能化服务体验革命四、营销场景:精准投放与创意生成五、研…...

一篇学习CSS的笔记
一、简介 Cascading Style Sheets简称CSS,中文翻译为层叠样式表。当HTML被发明出来初期,不同的浏览器提供了各种各样的样式语言给用户控制网页的效果,HTML包含的显示属性并不是很多。但是随着各种使用者对HTML的需求,HTML添加了大…...

使用lighttpd和开发板进行交互
文章目录 🧠 一、Lighttpd 与开发板的交互原理1. 什么是 Lighttpd?2. 与开发板交互的方式? 🧾 二、lighttpd.conf 配置文件讲解⚠️ 注意事项: 📁 三、目录结构说明💡 四、使用 C 编写 CGI 脚本…...