一、java8的Lambda表达式

什么是Lambda表达式

Lambda表达式是一段可以传递的代码。
λ表达式本质上是一个匿名方法。使用Lambda表达式可以使代码变的更加紧凑,例如在Java中实现一个线程,只输出一个字符串Hello World!,我们的代码如下所示:

 public static void main(String[] args) throws Exception {
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    }).start();
    TimeUnit.SECONDS.sleep(1000);
}

使用Lambda表达式之后代码变成如下形式:

public static void main(String[] args) throws Exception {
    new Thread(() -> System.out.println("Hello World!")).start();
    TimeUnit.SECONDS.sleep(1000);
}

参数类型也可以省略,Java编译器会根据上下文推断出来:

可见λ表达式有三部分组成:参数列表,箭头(->),以及一个表达式或语句块。

下面这个例子里的λ表达式没有参数,也没有返回值(相当于一个方法接受0个参数,返回void,其实就是Runnable里run方法的一个实现):

   () -> { System.out.println("Hello Lambda!"); }

如果只有一个参数且可以被Java推断出类型,那么参数列表的括号也可以省略:

Lambda表达式的语法

(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }

第一个lambda表达式接收x和y这两个整形参数并返回它们的和;第二个lambda表达式不接收参数,返回整数’42’;第三个lambda表达式接收一个字符串并把它打印到控制台,不返回值。

lambda表达式的语法由参数列表、箭头符号->和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:

  • 表达式:表达式会被执行然后返回执行结果。
  • 语句块:语句块中的语句会被依次执行,就像方法中的语句一样——
    • return语句会把控制权交给匿名方法的调用者
    • break和continue只能在循环中使用
    • 如果函数体有返回值,那么函数体内部的每一条路径都必须返回值

函数式接口

我们把这些只拥有一个方法的接口称为函数式接口。(之前它们被称为SAM类型,即单抽象方法类型(Single Abstract Method))

我们并不需要额外的工作来声明一个接口是函数式接口:编译器会根据接口的结构自行判断
判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个Object已经提供的方法,比如toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴)。
不过API作者们可以通过@FunctionalInterface注解来显式指定一个接口是函数式接口(以避免无意声明了一个符合函数式标准的接口),加上这个注解之后,编译器就会验证该接口是否满足函数式接口的要求。

下面是Java SE 7中已经存在的函数式接口:

  • java.lang.Runnable

  • java.util.concurrent.Callable

  • java.security.PrivilegedAction

  • java.util.Comparator

  • java.io.FileFilter

  • java.beans.PropertyChangeListener

java.util.function

除此之外,Java SE 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如
● Predicate——接收T对象并返回boolean
● Consumer——接收T对象,不返回值
● Function

Predicate接口

Predicate 接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):

Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo");              // true
predicate.negate().test("foo");     // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Function 接口

Function 接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"

Supplier 接口

Supplier 接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

Consumer 接口

Consumer 接口表示执行在单个参数上的操作。

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Comparator 接口

Comparator 是老Java中的经典接口, Java 8在此之上添加了多种默认方法:

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);


Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

方法引用

lambda表达式允许我们定义一个匿名方法,并允许我们以函数式接口的方式使用它。我们也希望能够在已有的方法上实现同样的特性。

  • 方法引用有很多种,它们的语法如下:

  • 静态方法引用:ClassName::methodName

  • 实例上的实例方法引用:instanceReference::methodName

  • 超类上的实例方法引用:super::methodName

  • 类型上的实例方法引用:ClassName::methodName

  • 构造方法引用:Class::new

  • 数组构造方法引用:TypeName[]::new
    对于静态方法引用,我们需要在类名和方法名之间加入::分隔符,例如Integer::sum。

对于具体对象上的实例方法引用,我们则需要在对象名和方法名之间加入分隔符:

Set<String> knownNames = ...
Predicate<String> isKnown = knownNames::contains;

这里的隐式lambda表达式(也就是实例方法引用)会从knownNames中捕获String对象,而它的方法体则会通过Set.contains使用该String对象。

有了实例方法引用,在不同函数式接口之间进行类型转换就变的很方便:

Callable<Path> c = ...
Privileged<Path> a = c::call;

引用任意对象的实例方法则需要在实例方法名称和其所属类型名称间加上分隔符:

Function<String, String> upperfier = String::toUpperCase;

这里的隐式lambda表达式(即String::toUpperCase实例方法引用)有一个String参数,这个参数会被toUpperCase方法使用。

如果类型的实例方法是泛型的,那么我们就需要在::分隔符前提供类型参数,或者(多数情况下)利用目标类型推导出其类型。

需要注意的是,静态方法引用和类型上的实例方法引用拥有一样的语法。编译器会根据实际情况做出决定。

一般我们不需要指定方法引用中的参数类型,因为编译器往往可以推导出结果,但如果需要我们也可以显式在::分隔符之前提供参数类型信息。

和静态方法引用类似,构造方法也可以通过new关键字被直接引用:

SocketImplFactory factory = MySocketImpl::new;

如果类型拥有多个构造方法,那么我们就会通过目标类型的方法参数来选择最佳匹配,这里的选择过程和调用构造方法时的选择过程是一样的。

如果待实例化的类型是泛型的,那么我们可以在类型名称之后提供类型参数,否则编译器则会依照”菱形”构造方法调用时的方式进行推导。

数组的构造方法引用的语法则比较特殊,为了便于理解,你可以假想存在一个接收int参数的数组构造方法。参考下面的代码:

IntFunction<int[]> arrayMaker = int[]::new;
int[] array = arrayMaker.apply(10) // 创建数组 int[10]

变量的作用域

捕获的概念在于解决在λ表达式中我们可以使用哪些外部变量(即除了它自己的参数和内部定义的本地变量)的问题。
答案是:与内部类非常相似,但有不同点。不同点在于内部类总是持有一个其外部类对象的引用。而λ表达式呢,除非在它内部用到了其外部类(包围类)对象的方法或者成员,否则它就不持有这个对象的引用。
在Java8以前,如果要在内部类访问外部对象的一个本地变量,那么这个变量必须声明为final才行。在Java8中,这种限制被去掉了,代之以一个新的概念,“effectively final”。它的意思是你可以声明为final,也可以不声明final但是按照final来用,也就是一次赋值永不改变。换句话说,保证它加上final前缀后不会出编译错误。
在Java8中,内部类和λ表达式都可以访问effectively final的本地变量。λ表达式的例子如下:

    int tmp1 = 1; //包围类的成员变量
    static int tmp2 = 2; //包围类的静态成员变量
    public void testCapture() {
        int tmp3 = 3; //没有声明为final,但是effectively final的本地变量
        final int tmp4 = 4; //声明为final的本地变量
        int tmp5 = 5; //普通本地变量

        Function<Integer, Integer> f1 = i -> i + tmp1;
        Function<Integer, Integer> f2 = i -> i + tmp2;
        Function<Integer, Integer> f3 = i -> i + tmp3;
        Function<Integer, Integer> f4 = i -> i + tmp4;
        Function<Integer, Integer> f5 = i -> {
            tmp5  += i; // 编译错!对tmp5赋值导致它不是effectively final的
            return tmp5;
        };

        tmp5 = 9; // 编译错!对tmp5赋值导致它不是effectively final的
    }

Java要求本地变量final或者effectively final的原因是多线程并发问题。内部类、λ表达式都有可能在不同的线程中执行,允许多个线程同时修改一个本地变量不符合Java的设计理念。
这里写图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值