猪脚:以下内容参考《Java 8 in Action》

本次学习内容:

  • Lambda 基本模式
  • 环绕执行模式
  • 函数式接口,类型推断
  • 方法引用
  • Lambda 复合

代码: https://github.com/Ryan-Miao/someTest/blob/master/src/main/java/com/test/java8/c3/AppleSort.java

上一篇: Java8学习(2)- 通过行为参数化传递代码--lambda代替策略模式


1. 结构

初始化一个比较器:

Comparator<Apple> byWeight = new Comparator<Apple>() {
public int copare(Apple a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight() );
}
}

使用Lambda表达式:

Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight() );

  • 参数列表--compare方法的的两个参数
  • 箭头 --- 把参数列表与lambda主体分割开
  • Lambda主体 --- 表达式的值就是Lambda的返回值

1.1 Java8中有效的Lambda表达式

接收一个字符串,并返回字符串长度int

(String a) -> s.length()

接收一个Apple类参数,返回一个boolean值

(Apple a) -> a.getWeight() > 150

接收两个参数,没有返回值(void),多行语句需要用大括号包围

(int x, int y) -> {
System.out.println("Result:");
System.out.println(x + y);
}

不接收参数,返回一个值

()-> 42

接收两个参数,返回一个值

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight() );

1.2 Lambda的基本语法

(parameters) -> expression

(parameters) -> {statements}

2. 函数式接口

在上次的学习中的通过行为参数化传递代码, Predicate(T)就是一个函数式接口:

public interface Predicate<T> {
boolean test(T t);
}

函数式接口就是只定义一个抽象方法的接口。

Java API中很多符合这个条件。比如:

public interface Comparable<T> {
public int compareTo(T o);
} public interface Runnable {
public abstract void run();
} @FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

2.1 函数式接口可以做什么

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把表达式作为函数式接口的实例(函数式接口一个具体实现的实例)。就像内部类一样,但看起来比内部类简洁。

Runnable r1 = () -> System.out.println("1");

Runnable r2 = new Runnable(){
public void run(){
System.out.println("2");
}
}; public static void process(Runnable r) {
r.run();
} process(r1);
process(r2);
process(() -> System.out.println(3));

@FunctionalInterface是一个标注,用来告诉编译器这是一个函数式接口,如果不满足函数式接口的条件,编译器就会报错。当然,这不是必须的。好处是编译器帮助检查问题。

3. 一步步修改为Lambda表达式

Lambda式提供了传递方法的能力。这种能力首先可以用来处理样板代码。比如JDBC连接,比如file读写。这些操作会有try-catcha-finally,但我们更关心的是中间的部分。那么,是不是可以将中间的部分提取出来,当做参数传递进来?

3.1 第1步: 行为参数化

下面是读一行:

public String read(){
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
} return null;
}

行为参数化就是把一个过程行为转换成参数。在这里就是将br.readLine()提取成参数。

3.2 第2步:使用函数式接口来传递行为

定义一个接口来执行上述的行为:

public interface BufferedReaderProcessor{
String process(BufferedReader b) throws IOException;
}

然后把这个接口当作参数:

public String read(BufferedReaderProcessor p) throws IOException{
try(BufferedReader br = new BufferedReader(new FileReader("data.txt"))){
return p.process(br);
}
}

3.3 第3步: 传递Lambda

@Test
public void readFile() throws IOException {
String oneLine = read(BufferedReader::readLine);
String twoLine = read((BufferedReader b) -> b.readLine() + b.readLine());
}

如此,我们就把中间的逻辑抽出来了。把行为抽象成一个接口调用,然后通过Lambda来实现接口的行为。传递参数。完毕。

4. Java API中内置的一些函数式接口

Java API中内置了一些很有用的Function接口。

4.1 Predicate

java.util.function.Predicate<T> 定义了一个抽象方法,返回一个boolean

使用demo如下:

private <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;
}
@Test
public void testPredicate(){
List<String> list = Arrays.asList("aa","bbb","ccc");
List<String> noEmpty = filter(list, (String s) -> !s.isEmpty());
}

4.2 Consuer

java.util.function.Consumer<T>定义了一个抽象方法,接收一个参数。

private <T> void forEach(List<T> list, Consumer<T> c){
for (T t : list) {
c.accept(t);
}
}
@Test
public void testConsumer() {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
forEach(integers, System.out::println);
}

4.3 Function

java.util.function.Function<T,R>定义了一个抽象方法,接收一个参数T,返回一个对象R

private <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;
} @Test
public void testFunction(){
List<String> strings = Arrays.asList("a", "bb", "ccc");
List<Integer> lengths = map(strings, String::length);
}

4.4 基本类型函数接口

前面三个泛型函数式接口Predicate<T>Consumer<T>Function<T,R>,这些接口是专门为引用类型设计的。那么基本类型怎么办?我们知道可以自动装箱嘛。但装箱是有损耗的。装箱(boxing)的本质是把原始类型包裹起来,并保存在堆里。因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取包裹的原始值。

Java8为函数式接口带来了专门的版本。

@Test
public void testIntPredicate() {
//无装箱
IntPredicate intPredicate = (int t) -> t%2 == 0;
boolean isEven = intPredicate.test(100);
Assert.assertTrue(isEven);
//装箱
Predicate<Integer> integerPredicate = (Integer i) -> i%2 == 0;
boolean isEven2 = integerPredicate.test(100);
Assert.assertTrue(isEven2);
}

类似的还有:

Java 8中的常用函数式接口



5. Lambda原理

  • 编译器可以推断出方法的参数类型,由此可以省略一些样板代码。
  • void和其他返回值做了兼容性处理

6. Lambda的局部变量

在Lambda中可以使用局部变量,但要求必须是final的。因为Lambda可能在另一个线程中运行,而局部变量是在栈上的,Lambda作为额外的线程会拷贝一份变量副本。这样可能会出现同步问题,因为主线程的局部变量或许已经被回收了。基于此,必须要求final的。

而实例变量则没问题,因为实例变量存储于堆中,堆是共享的。

7. 方法引用

Lambda表达式可以用方法引用来表示。比如

(String s) -> s.length()
==
String::length

这是因为可以通过Lambda表达式的参数以及方法来确定一个方法。在这里,每个方法都叫做方法签名。方法签名由方法名+参数列表唯一确定。其实就是重载的判断方式。

当Lambda的主体只是一个简单的方法调用的时候,我们可以直接使用一个方法引用来代替。方法引用可以知道要接受的参数类型,以及方法体的逻辑。

方法引用结构:

类名::方法名

什么可以使用方法引用?

  • 静态方法。
  • 指向任意类型实例方法的方法引用。
  • 指向现有对象的实例方法。

8. 构造函数引用

构造函数可以通过类名::new的方式引用。

9. Lambda实战

目标: 用不同的排序策略给apple排序。

过程: 把一个原始粗暴的解决方案变得更加简单。

资料: 行为参数化, 匿名类Lambda, 方法引用.

最终: inventory.sort(comparing(Apple::getWeight) );

9.1 原始方案

/**
* Created by ryan on 7/20/17.
*/
public class AppleSort {
private List<Apple> inventory; @Before
public void setUp() {
inventory = new ArrayList<>();
inventory.add(new Apple("red", 1));
inventory.add(new Apple("red", 3));
inventory.add(new Apple("red", 2));
inventory.add(new Apple("red", 21));
} @Test
public void sort_old() {
Collections.sort(inventory, new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() - o2.getWeight();
}
}); printApples();
} private void printApples() {
inventory.forEach(System.out::println);
}
}

排序首先要注意的一点就是排序的标准。那么要搞清楚为什么这样写?

Comparator定义的其实就是一个方法,此处就是将排序的原则抽取出来。特别符合Lambda的思想!这里先不说Lambda,先说这个方法的作用:定义什么时候发生交换

跟踪源码可以发现这样一段代码:

//java.util.Arrays#mergeSort(java.lang.Object[], java.lang.Object[], int, int, int, java.util.Comparator)
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
swap(dest, j, j-1);
return;
}

假设比较的两个数为o1o2,并且o1o2前一位(left>right)。如下:

....o1,o2...

compare(o1,o2)的结果大于0则,o1o2交换。那么,显然,如果

compare(o1,o2) = o1-o2

则说明,前一个值比后一个值大的时候,发生交换。也即大的往后冒泡。就是升序了。

所以:

  • o1-o2 升序
  • o2-o1 降序

9.2 使用List内置sort

好消息是Java8提供了sort方法给list:java.util.List#sort:

则原始方案转换为:

@Test
public void sort1(){
inventory.sort(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight() - o2.getWeight();
}
}); printApples();
}

9.3 Lambda表达式代替匿名内部类

从之前的学习可以得到,几乎所有的匿名内部类都可以用Lambda表达式替代!

inventory.sort((o1, o2) -> o1.getWeight() - o2.getWeight());

9.4 进一步优化Lambda

Comparator提供了一个生成Comparator的方法:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

其中,Function<T,R>已经在前面学习过了,就是一个接受一个参数并返回另一个参数的函数式接口。在本例中,apple.getWeight()符合接受一个参数apple返回一个int。那么,就可以使用这个方法:

inventory.sort(Comparator.comparing((Apple a)->a.getWeight()));

进一步,将Lambda改为方法引用:

inventory.sort(Comparator.comparing(Apple::getWeight));

这里有个问题,记得之前讲的基本类型的自动装箱吗。Apple::getWeight的返回值是int。而comparing的返回值是一个对象。那么,必然要经过自动装箱的过程。所以,应该使用基本类型的函数式接口:

inventory.sort(Comparator.comparingInt(Apple::getWeight));

至此,基本已经改造完毕了。最多就是静态引入comparingInt方法:

inventory.sort(comparingInt(Apple::getWeight));

目标达到。相比原始方法,不要太简洁!

话说,这种是不是只能默认升序?因此没有任何一个单词可以看出排序规则。

是的,想要降序?

inventory.sort(comparingInt(Apple::getWeight).reversed());

10 复合Lambda

上节看到逆序的方法就是后面追加一个逆序的方法。现在需求变更了。需要先按照颜色排序,然后再按照重量从大到小排序。

10.1 比较器链

这里,一共涉及了3个过程。往常的做法是连续写在一个方法里,或者3个方法连续调用。Lambda提供了类似语句陈述一般的写法。

inventory.sort(comparing(Apple::getColor)
.reversed()
.thenComparingInt(Apple::getWeight));

10.2 谓词复合

前面的Prediacate接口包含4个方法:negate,and,orisEqual,对应逻辑运算里的取反,,,==。这样,通过复合就可以写出语义声明式的代码:

想要红苹果:

Predicate<Apple> red = apple -> "red".equalsIgnoreCase(apple.getColor());

想要不是红的苹果:

Predicate<Apple> nonRed = red.negate();

想要大的红苹果:

Predicate<Apple> redAndHeavy = red.and(apple -> apple.getWeight() > 150);

想要大的红苹果或者绿色的:

Predicate<Apple> redAndHeavyOrGreen = redAndHeavy.or(apple -> "green".equalsIgnoreCase(apple.getColor()));
或者:
redAndHeavyOrGreen = ((Predicate<Apple>) apple -> "red".equalsIgnoreCase(apple.getColor()))
.and(apple -> apple.getWeight() > 150)
.or(apple -> "green".equalsIgnoreCase(apple.getColor()));

10.3 函数复合

f(x) = (x+1) * 2;
求 f(2)

普通写法:

Assert.assertEquals(6, f(2));

private int f(int x){
return (x + 1) * 2;
}

函数式可以这样写:

Function<Integer, Integer> f = x -> x +1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
int r = h.apply(2);
Assert.assertEquals(6, r);

看起来似乎更麻烦了,但这只是一个举例。事实上,Function提供了连续处理逻辑的能力,可以不断的处理上一次计算的返回值。

比如,封装一个写信的类:

public class Letter {
public static String addHeader(String text){
return "From Ryan Miao: " + text;
} public static String addFooter(String text) {
return text + " Kind regards";
} public static String checkSpelling(String text){
return text.replace("<", "&lt;");
}
} @Test
public void testFunction3(){
Function<String, String> transformationPipeline =
((Function<String, String>)Letter::addHeader)
.andThen(Letter::checkSpelling)
.andThen(Letter::addFooter);
String letter = transformationPipeline.apply("Hello world!");
Assert.assertEquals("From Ryan Miao: Hello world! Kind regards", letter);
}

11 小结

  • Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主题、返回值类型,可能还有一个可以抛出的异常列表。
  • Lambda表达式让你可以更简洁的传递代码
  • 函数式接口就是仅仅声明了一个抽象方法的接口。
  • 只有在接受函数式接口的地方才可以使用Lambda表达式。
  • Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例
  • Java 8自带了一些常用函数式接口,放在java.util.function里。包括Prediacate<T>,Function<T,R>,Supplier<T>,Consumer<T>,BinaryOperator<T>
  • 为了避免装箱操作,对Predicate和Function<T,R>等通用函数式接口的原始类特殊化:IntPredicate,InToLong等。
  • 环绕执行模式(方法的中间代码)可以配合Lambda提高灵活性和可重用性。
  • Lambda表达式所需要代表的类型成为目标类型。
  • Comparator,Predicate,Function等函数接口都有几个可以用来结合Lambda表达式的默认方法。

Java8学习(3)- Lambda 表达式的更多相关文章

  1. Java8学习笔记----Lambda表达式 (转)

    Java8学习笔记----Lambda表达式 天锦 2014-03-24 16:43:30 发表于:ATA之家       本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人 ...

  2. JAVA8学习——深入浅出Lambda表达式(学习过程)

    JAVA8学习--深入浅出Lambda表达式(学习过程) lambda表达式: 我们为什么要用lambda表达式 在JAVA中,我们无法将函数作为参数传递给一个方法,也无法声明返回一个函数的方法. 在 ...

  3. java8学习之Lambda表达式继续探讨&Function接口详解

    对于上次[http://www.cnblogs.com/webor2006/p/8186039.html]已经初步引入的Java8中Stream流的概念,其中使用了map的操作,它需要接受一个Func ...

  4. java8学习之Lambda表达式深入与流初步

    Lambda表达式深入: 在上一次[http://www.cnblogs.com/webor2006/p/8135873.html]中介绍Lambda表达式的作用时,其中说到这点: 如标红处所说,既然 ...

  5. java8学习之Lambda表达式初步与函数式接口

    对于Java8其实相比之前的的版本增加的内容是相当多的,其中有相当一大块的内容是关于Lambda表达式与Stream API,而这两部分是紧密结合而不能将其拆开来对待的,但是是可以单独使用的,所以从学 ...

  6. Java8新特性-Lambda表达式是什么?

    目录 前言 匿名内部类 函数式接口 和 Lambda表达式语法 实现函数式接口并使用Lambda表达式: 所以Lambda表达式是什么? 实战应用 总结 前言 Java8新特性-Lambda表达式,好 ...

  7. 乐字节-Java8新特性-Lambda表达式

    上一篇文章我们了解了Java8新特性-接口默认方法,接下来我们聊一聊Java8新特性之Lambda表达式. Lambda表达式(也称为闭包),它允许我们将函数当成参数传递给某个方法,或者把代码本身当作 ...

  8. Java8新特性 - Lambda表达式 - 基本知识

    A lambda expression is an unnamed block of code (or an unnamed function) with a list of formal param ...

  9. Java8简明学习之Lambda表达式

    函数式接口 就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,函数式接口可以被隐式转换为lambda表达式. 之前已有的函数式接口: java.lang.Runnable java.uti ...

随机推荐

  1. 分享几个python小脚本

    by 梁凯 今天我想给大家分享几个python脚本,分别是: 1.公司访问外网认证脚本(最初有同事写过,我优化了一下). 2.统计周报系统所有同事的最近一篇周报. 3.统计测试技术分享里指定一个月所有 ...

  2. 一天搞定CSS(扩展):CSS Hack

    做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...

  3. 在linux中导入sql文件的方法分享(使用命令行转移mysql数据库)

    因导出sql文件 在你原来的网站服务商处利用phpmyadmin导出数据库为sql文件,这个步骤大家都会,不赘述. 上传sql文件 前面说过了,我们没有在云主机上安装ftp,怎么上传呢? 打开ftp客 ...

  4. css中那些容易被我们程序猿所忽略的选择器

    css中那些容易被我们程序猿所忽略的选择器 作为一个程序猿,我们可能会遇到这样的问题,假如:有5个li,要求给第三个li设置背景颜色怎么办?有人会说,用JS啊,循环遍历出第三个不就行了.但是,用JS解 ...

  5. shell网络客户端

    需要把线上的access日志发送到另一个程序接收 开始想着用python实现,虽然python也有实现类似tail -F的方式,但太麻烦,而且效率也有折扣 偶然发现了shell可以实现网络client ...

  6. 基于angularJs的单页面应用seo优化及可抓取方案原理分析

    公司使用angularJs(以下都是指ng1)框架做了互联网应用,之前没接触过seo,突然一天运营那边传来任务:要给网站做搜索引擎优化,需要研发支持.搜了下发现单页面应用做seo比较费劲,国内相关实践 ...

  7. Mac下安装MySQL、Workbench以及建数据库建表最基础操作

    刚用上Mac,什么都不懂,加之以前还没有用过mysql,就想着在Mac上装一个mysql来自己玩,奈何,在网上找了大半天,没有一个干货!愤怒!下面是我安装的过程,希望能帮到和我情况差不多的朋友   首 ...

  8. java 单链表的实现

    package liaobiao;//链表测试public class Node { private int value; private Node next; //存放下一个节点的指针 //构造方法 ...

  9. javascript中apply,call,bind区别,bind兼容等问题总结

    1 三者的相似之处: (1).都是用来改变函数的this对象的指向的 (2).都是用第一个参数来做this对象的指向 (3).都可以传参数进去 那么,具体到它们有什么区别呢?请看下面的例子: 两个对象 ...

  10. Java 基础 程序流程控制 (下)

    Java 程序流程控制 (下) 此篇单独对循环结构的知识点进行整理: 之前讲到循环结构分为:for循环,while循环,do...while循环三种最基本的循环结构:在JDK1.5以后的版本还提供了f ...