Java8 Lambda表达式学习笔记——一文学懂笔记系列(一)

1.什么是函数式接口

@FunctionalInterface注释的接口,只能有一个abstract方法;可以用多个default方法,因为这些方法已经不是抽象的了。

实例化函数式接口有下面三种方式:

  • lambda表达式
  • 方法引用
  • 构造器引用

需要注意的是,即使一个接口没有用@FunctionalInterface注释,如果他满足函数式接口的定义,就会被编译器视作函数式接口。

2.比较@FunctionalInterfaceinterface Function<T,R>

前者是函数式接口注释,后者是JDK自定义的一个函数接口,方便类库自身的使用,也方便用户直接使用。

3.JAVA API中函数接口命名规范:

  • XXFunction:代表一个有入参和返回结果的函数接口。
  • XXXConsumer:代表一个有入参但无返回结果的函数接口。
  • XXXSupplier: 代表一个无入参,但是有返回结果的函数接口。
  • XXXPredicate:谓词接口,无论输入类型,他的函数方法只返回boolean值。
  • XXXOperator: 计算接口,一般输入和输出是同一种类型的值。

4. 包 java.util.function 说明

函数式接口为 lambda 表达式和方法引用提供目标类型。每个函数式接口都有一个抽象方法,称为该函数式接口的函数式方法,lambda 表达式的参数和返回类型与之匹配或适应。函数式接口可以在多个上下文中提供目标类型,例如赋值上下文、方法调用或强制转换上下文:

 // 赋值上下文(Assignment context)
 Predicate<String> p = String::isEmpty;

 // 方法调用上下文(Method invocation context)
 stream.filter(e -> e.getSize() > 10)...

 // 强制转换上下文(Cast context)
 stream.map((ToIntFunction) e -> e.getSize())...

此包中的接口是 JDK 使用的通用功能接口,也可供用户代码使用。虽然它们没有确定 lambda 表达式可能适用的完整函数形状集,但它们提供了足够的内容来满足常见要求。为特定目的提供的其他功能接口,例如 FileFilter,在使用它们的包中定义。

此包中的接口使用 @FunctionalInterface 进行注释。此注释不是编译器将接口识别为功能接口的要求,而只是帮助捕获设计意图并在识别意外违反设计意图时获得编译器的帮助。

函数式接口通常表示抽象概念,如函数(functions)动作(actions)谓词(predicates)。在编写文档时,函数式接口或引用类型为函数式接口的变量,通常直接引用那些抽象概念,例如使用“此函数”而不是“此对象表示的函数”。当 API 方法以这种方式接受或返回功能接口时,例如“将提供的功能应用于…”,这被理解为对实现适当功能接口的对象的非空引用,除非明确指定了潜在无效性。

这个包中的功能接口遵循一个可扩展的命名约定,如下:

  • 有几种基本的函数形状,包括Function(从T到R的一元函数)、Consumer(从T到void的一元函数)、Predicate(从T到boolean的一元函数)和Supplier(输出为R的零元函数)。
  • 函数形状具有基于它们最常用的方式的自然多样性。基本形状可以通过元数前缀修改以指示不同的元数,例如 BiFunction(从 T 和 U 到 R 的二元函数)。
  • 还有其他派生函数形状是从扩展基本函数形状而来,包括 UnaryOperator(extends Function)和 BinaryOperator(extends BiFunction)。
  • 函数式接口的类型参数可以指定为某个原始类型(通过带前缀的方式)。为了指定返回类型,如同时具有泛型返回类型和泛型参数的类型,我们加上 ToXxx 前缀,如 ToIntFunction。否则,类型参数是从左到右指定类型的,如 DoubleConsumerObjIntConsumer。 (类型前缀 Obj 用于表示我们不想指定这个参数的类型,而是想继续下一个参数,如 ObjIntConsumer。)这些方案可以组合,如 IntToDoubleFunction
  • 如果所有参数都有专门化前缀,则可以省略 arity 前缀(如 ObjIntConsumer 中)。

5.专有名词解释

arity(缩写为:ary):元数; 奇偶; 自变量数目;参数数量;

primitive:原始类型,如:int ,double;

specialize:专门化;指定为某种类型;

inline: 内联的;

6.lambda表达式如何传递的

也就是说,lambda表达式为什么可以当做参数传递,传递的时候如何验证类型呢?

解释: 关键在于函数方法的签名。将函数式接口作为某个方法的入参时,他只有一个抽象方法,只要传递的lambda表达式的入参和出参,符合他这个方法的签名,就可以传递进来。这个抽象方法的签名也称作函数描述符

换句话说,定义外层方法时,函数类型就是对应的函数式接口类型,lambda表达式由于允许直接内联,为函数式接口唯一的抽象方法既提供了实现,又将整个表达式作为该函数式接口的一个实例(类似于匿名类的简化版)。

比如:

// 定义一个函数接口
@FunctionalInterface 
public interface BufferedReaderProcessor { 
 String process(BufferedReader b) throws IOException; 
}

//定义一个方法
public static String processFile(BufferedReaderProcessor p) throws 
 IOException { 
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 
     return p.process(br);
     }
}

//调用方法
String oneLine = processFile(((BufferedReader br) -> br.readLine());

从上面的例子可以看出,可以把样板代码放在一个静态方法内部,用户真正需要的行为需要抽象出来放在函数式接口里,这样就可以按需执行方法,减少重复代码。这种方式也称作行为参数化。

扩展阅读:

内联函数(inline):https://zh.wikipedia.org/wiki/%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0

计算机科学中,内联函数(有时称作在线函数编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。

7.lambda表达式声明

Lambda的基本语法是

//一行表达式
(parameters) -> expression 

或(请注意语句的花括号)

//java标准语句,支持多行。
(parameters) -> { statements; } 

8.lambda表达式类型检查

需要lambda表达式的参数列表和返回值类型和方法中的函数接口中的抽象方法参数签名一致。lambda表达式所反应的类型为目标类型,也就是检查目标类型和上下文中比如方法的参数或者局部变量的类型是否相一致。

特殊情况: 如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容。换句话说,lambda的主体是一行表达式时,既支持表达式本身返回的类型,也支持void返回类型。

如下图所示:

//list.add()本身返回值是boolean,但下面两个函数接口类型都支持
// Predicate返回了一个boolean 
Predicate<String> p = s -> list.add(s); 
// Consumer返回了一个void 
Consumer<String> b = s -> list.add(s);

9.lambda表达式类型推断

Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的参数签名,因为函数描述符可以通过目标类型来得到。

当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。

请注意:有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。

10.lambda表达式使用局部变量注意点

局部变量必须是final的。

扩展阅读:

闭包

(摘自:Java8 实战,P52)

你可能已经听说过闭包(closure,不要和Clojure编程语言混淆)这个词,你可能会想Lambda是否满足闭包的定义。

用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。

现在,Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。

如前所述,这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的(实例变量可以,因为它们保存在堆中,而堆是在线程之间共享的)。

11. 什么是方法引用

(1)方法引用

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。

此处可以看看代码里的例子,多写几个再体会体会。

方法引用主要有三种:

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

如下面例子所示:

 /**
     * (1) 指向静态方法的方法引用
     */
    @Test
    public void testStaticMethodReference() {
        Function<String, Integer> parseToIntFunction = x -> Integer.parseInt(x);

        Integer result = parseToIntFunction.apply("456");
        System.out.println(result);

        //等价于下面的方法引用
        /*
         * 因为函数式接口的默认方法,定义了参数列表和返回值类型,也就决定了方法引用真正调用的方法.
         * 比如parseInt有两个重载方法:parseInt(String)和parseInt(String,int),由于Function接口的默认方法只有一个入参,
         * 所以实际引用的就是parseInt(String)方法.
         */
        Function<String, Integer> parseToIntFunction2 = Integer::parseInt;
        Integer result2 = parseToIntFunction2.apply("123");
        System.out.println(result2);
    }

    /**
     * (2) 指向任意类型实例方法的方法引用
     */
    @Test
    public void testAnyInstanceMethodReference() {
        /*
         * 这个地方很有意思,将实例当做函数的入参中的一个,然后需要调用的方法的入参当做函数入参的第二个,实例方法的返回值当做函数的返回值,
         * 就正好能对上BiFunction函数的定义.
         */
        BiFunction<String, Integer, String> subStringFunction = (str, i) -> str.substring(i);

        String result = subStringFunction.apply("abcdefg", 2);
        System.out.println(result);

        //等价于
        BiFunction<String, Integer, String> subStringFunction2 = String::substring;
        String result2 = subStringFunction2.apply("abcdefg", 3);
        System.out.println(result2);
    }

    /**
     * (3) 指向现有对象的实例方法的方法引用
     */
    @Test
    public void testLocalInstanceMethodReference() {
        String s = "abcdefg";
        Supplier<Integer> getLengthFunction = () -> s.length();
        Integer result = getLengthFunction.get();
        System.out.println(result);

        //等价于
        Supplier<Integer> getLengthFunction2 = s::length;
        Integer reult2 = getLengthFunction2.get();
        System.out.println(reult2);
    }

请注意,还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用。

(2)构造函数引用

根据构造函数的签名的不同,对应不同的function接口,如下面例子所示:

 /**
     * (1) 无参构造方法引用
     */
    @Test
    public void testNoArgsConstructMethodReference() {
        Supplier<Apple> appleSupplier = Apple::new;

        Apple apple = appleSupplier.get();

        System.out.println(apple.toString());

        //等价于
        Supplier<Apple> appleSupplier2 = () -> new Apple();
        Apple apple2 = appleSupplier2.get();
        System.out.println(apple2.toString());
    }

    /**
     * (2) 有1个参数的构造方法引用
     */
    @Test
    public void testHasArgsConstructMethodReference() {

        Function<Integer, Apple> getAppleFunction = Apple::new;
        Apple apple = getAppleFunction.apply(100);
        System.out.println(apple.toString());

        //等价于
        Function<Integer, Apple> getAppleFunction2 = weight -> new Apple(weight);
    }

以此类推,java.util.function中的函数接口只可以满足0个,1个,2个构造参数的函数接口(如:Supplier<T>, Function<T,R>, BiFunction<T,U,R>),如果有更多的参数需要,可以自定义函数接口来满足。

(3)从定义比较器并实例化引用,到直接使用方法引用简写

下面的例子展示了,如何从自定义比较器,并实例化引用,再到使用匿名类,再到使用lambda表达式,再到方法引用的实现方式。

 public static List<Apple> appleList = Arrays.asList(new Apple("red", 100), new Apple("red", 110), new Apple("green", 107), new Apple("red", 104));

    /**
     * 第一步: 传递代码的方式
     *
     * <p>该方式需要提前实现对应的比较器类,并实例化后调用对应比较器
     */
    @Test
    public void testTransmitCode() {
        //调用显示定义好的苹果重量比较器
        appleList.sort(new AppleComparator());
        System.out.println(Arrays.toString(appleList.toArray()));
    }

    /**
     * 第二步: 使用匿名类的方式
     */
    @Test
    public void testAnonymousClass() {
        appleList.sort(new Comparator<Apple>() {
            @Override
            public int compare(Apple a1, Apple a2) {
                return a1.getWeight().compareTo(a2.getWeight());
            }
        });

        System.out.println(Arrays.toString(appleList.toArray()));
    }

    /**
     * 第三步: 使用lambda表达式的方式
     */
    @Test
    public void testLambdaExpression() {
        // (1) 完整的lambda表达式写法
        appleList.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

        // (2) 利用类型推断,再简写一些
        appleList.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

        // (3) 利用Comparator.comparing方法,提前要比较的键值,生成comparator对象
        Comparator<Apple> c = comparing((Apple a) -> a.getWeight());
        appleList.sort(c);
        // 导入静态方法后,可以简写为
        appleList.sort(comparing(a -> a.getWeight()));
    }

    /**
     * 第四步: 使用方法引用
     *
     * <p>这就是你的最终解决方案!这比Java 8之前的代码好在哪儿呢?它比较短;它的意
     * 思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”
     */
    @Test
    public void testMethodReference() {
        // 也就是说,方法引用就是替代那些转发参数的Lambda表达式的语法糖
        appleList.sort(comparing(Apple::getWeight));
    }

从这里可以看出,缩写的逻辑如下

  • 匿名类是直接实现接口并同时实例化使用的一种简写方式,不需要再额外定义实现类,减少了字面类的定义。
  • lambda表达式是匿名类的简写方式,减少了模板代码,更加简洁。
  • 方法引用是替代转发参数的lambda表达式的语法糖,是更加简洁直观的表达方式。

至此,就完成了从定义实现类到方法引用的简写,层层简化,每一次都是利用了不同的语言特性。

扩展阅读: Comparator.comparing 方法,下面是JDK 里对应的源码和相关注释,值得多读几次。


  /**
     * 接受从类型T中提取可比较(Comparable)排序键的函数,并返回按该排序键进行比较的Comparator<T> 。
     * 如果指定的函数是可序列化的,则返回的比较器也是可序列化的。
     * 参数:
     * keyExtractor – 用于提取可比较排序键的函数
     * 类型参数:
     * <T> - 要比较的元素类型
     * <U> - 可比较排序键的类型
     * 返回:
     * 通过提取的键进行比较的比较器
     * 抛出:
     * NullPointerException – 如果参数为空
     * API注意事项:
     * 例如,要获取按姓氏比较Person对象的Comparator ,
     *
     *      Comparator<Person> byLastName = Comparator.comparing(Person::getLastName);
     *
     * since:1.8
     */
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));
    }

12. lambda表达式可以复合使用

(1) 比较器复合(Comparator)

1) 逆序排序

直接使用reversed()方法即可:

// 按重量递减排序
appleList.sort(comparing(Apple::getWeight).reversed());

2) 使用比较器链

使用thenComparing方法:

// 按重量递减排序;两个苹果一样重时,进一步按国家排序
appleList.sort(comparing(Apple::getWeight) 
 .reversed() 
 .thenComparing(Apple::getCountry));

(2) 谓词复合(Predicate)

// 取红苹果
Predicate<Apple> redApple = apple -> apple.getColor().equals("red");

1)非(negate)

Predicate<Apple> notRedApple = redApple.negate();

2) 与(and)

// 一个苹果既是红色又比较重
Predicate<Apple> redAndHeavyApple =  redApple.and(a -> a.getWeight() > 150);

3) 或(or)

// 要么是重(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)

函数复合的用处:可以创建各种各样的流水线处理方法,比如批量处理字符串,先去除空格,再做替换,最后再加上后缀等等。

Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2;

1) andThen

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

数学上会写作g(f(x))(g o f)(x)

Function<Integer, Integer> h = f.andThen(g); 

//result = (1+1)*2 = 4
int result = h.apply(1);

2) compose

先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。

数学上会写作f(g(x))(f o g)(x)

Function<Integer, Integer> h = f.compose(g);

//result=1*2+1 = 3
int result = h.apply(1);

13. lambda表达式小结

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

参考:
[1] Java 8 In Action: Raoul-Gabriel Urma, Mario Fusco, Alan Mycroft
[2] wiki
[3] JDK 8 Source Code

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值