Java8学习笔记之Lambda表达式

Lambda表达式是Java8中一项重要的新特性,其本质是一个”语法糖“,由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能。

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

lambda表达式允许你通过表达式来代替功能接口。它和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body可以是一个表达式或一个代码块)。此外它还增强了集合库。 Java8添加了2个对集合数据进行批量操作的包:java.util.function和java.util.stream包。

Lambda表达式的特点:

1)匿名:写得少而想得多。

2)函数:不像方法那样属于某个特定的类。但和方法一样,有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。

3)传递:可作为参数传递给方法或存储在变量中。

4)简洁:无需像匿名类那样写很多模板代码。

Lambda表达式的语法:

(parameters) -> expression

(parameters) ->{ statements; }

1、有效的Lambda表达式

() ->5  //无参数,返回值为5

x ->2* x  //接受一个参数(数字),返回其2倍的值

(x, y) -> x – y  //接受2个参数(数字),并返回它们的差

(int x,int y) -> x + y  //接收2个int型整数,返回它们的和

(String s) -> System.out.print(s)  //接收一个string对象,并在控制台打印,不返回任何值(类似void)

(String s) -> s.length()  ///接收一个String类型的参数并返回一个int。Lambda没有return语句,因为已经隐含了return;

(Apple a) -> a.getWeight() > 150  //接收一个Apple类型的参数并返回一个boolean(苹果的重量是否超过150克)

(int x, int y) -> {

    System.out.println("Result:");

    System.out.println(x+y);

}    //接收两个int类型的参数而没有返回值。Lambda表达式可以包含多行语句,这里是两行;

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight())  //接收两个Apple类型的参数,返回一个int结果:比较两个Apple的重量;

2、函数式接口及函数描述符

函数式接口就是只定义一个抽象方法的接口,如下所示:

public interface Comparator { int compare(T o1, T o2); }

public interface Runnable{ void run(); }

public interface ActionListener extends EventListener{ void actionPerformed(ActionEvent e); }

public interface Callable{ V call(); }

public interface PrivilegedAction{ V run(); }

注:Java8中接口还可以拥有默认方法(即在类没有对方法进行实现时, 其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它仍然是一个函数式接口。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。代码如下:

Runnable r1 = () -> System.out.println("Hello World 1"); //使用Lambda

Runnable r2 = new Runnable(){ //使用匿名类

    public void run(){

        System.out.println("Hello World 2");

    }

};

public static void process(Runnable r){ r.run(); }

process(r1); //打印“Hello World 1”

process(r2); //打印“Hello World 2”

process(() -> System.out.println("Hello World 3")); //利用传递的Lambda打印“Hello World 3”

函数描述符

函数式接口的抽象方法的签名称为函数描述符。例如,Runnable接口可以看作是一个无参且不返回(void)的函数签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。

() -> void 代表参数列表为空,且返回void的函数。

(Apple, Apple) -> int 代表接受两个Apple对象作为参数且返回int的函数。

3、使用函数式接口

Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口:Predicate、Consumer、Function、Supplier等。

1)Predicate

java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。

@FunctionalInterface

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

使用示例:

public static List filter(List list, Predicate p) {

    List results = new ArrayList<>();

    for(T s: list){

        if(p.test(s))  results.add(s);

    }

    return results;

}

Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty();

List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

2)Consumer

java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回值。如果需要访问类型T的对象,并对其执行某些操作,可以使用这个接口。比如,可以用它来创建一个forEach方法,接受一个整数列表,并对其中每个元素执行操作。

@FunctionalInterface

public interface Consumer{void accept(T t);}

使用示例:

public static void forEach(List list, Consumer c){

    for(T i: list){

        c.accept(i);

    }

}

forEach(Arrays.asList(1,2,3,4,5),(Integer i) -> System.out.println(i));  //遍历list并打印元素

3)Function

java.util.function.Function接口定义了一个apply方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。

@FunctionalInterface

public interface Function{R apply(T t);}

使用示例:创建一个map方法,将一个String列表映射到包含每个String长度的Integer列表

public static List map(List list, Function f) {

    List result = new ArrayList<>();

    for(T s: list){

        result.add(f.apply(s));

    }

    return result;

}

List list = map(Arrays.asList("lambdas","in","action"), (String s) -> s.length()); //输出[7, 2, 6]

原始类型特化

Java中的数据类型包含原始类型(如int、double、byte、char)和引用类型(如Byte、Integer、Object、List)。泛型(比如Consumer中的T)只能绑定到引用类型(原因是由泛型内部的实现方式造成的)。将原始类型转换为对应的引用类型的机制叫装箱(boxing),反之将引用类型转换为对应的原始类型叫作拆箱(unboxing)。Java中的装箱和拆箱操作是自动完成的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值,在性能方面是要花费更高的代价。

Java 8为函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate就会把参数1000装箱到一个Integer对象中:

public interface IntPredicate{ boolean test(int t); }

IntPredicate evenNumbers = (int i) -> i % 2 == 0;

evenNumbers.test(1000); //true(无装箱)

Predicate oddNumbers = (Integer i) -> i % 2 == 1; 

oddNumbers.test(1000); //false(装箱)

一般针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。

以下是常用的函数式接口及其函数描述符:

常用的函数式接口


常用的函数式接口
Lambdas及函数式接口的例子 

4、类型检查、类型推断及限制

Lambda可以为函数式接口生成一个实例,但是Lambda表达式本身并不包含它在实现哪个函数式接口的信息。

1)类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(接受它传递的方法的参数,或接受它值的局部变量)中Lambda表达式需要的类型称为目标类型。

List list = filter(inventory, (Apple a) -> a.getWeight() > 150);

类型检查过程分解:

1>找出filter方法的声明

2>要求它是Predicate(目标类型)对象的第二个正式参数。

3>Predicate是一个函数式接口,定义了一个test抽象方法。

4>test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。

5>filter的任何实际参数都必须匹配这个要求。

2)同样的Lambda,不同的函数式接口

同一个Lambda表达式可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。

Callable c = () -> 1;

PrivilegedAction p = () -> 1;

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

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

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

3)类型推断

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

List greenApples = filter(list, a -> "green".equals(a.getColor())); //参数a没有显式类型

Comparator c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()); //没有类型推断

Comparator c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight()); //有类型推断

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

4)使用局部变量

Lambda表达式也允许使用自由变量(在外层作用域中定义的变量),就像匿名类一样,被称作捕获Lambda。

int n = 10;

Runnable r = () -> System.out.println(n); //表示Lambda捕获了n变量

Lambda可以没有限制地捕获(在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获终局部变量this)

int n = 10;

Runnable r = () -> System.out.println(n); //编译出错,Lambda引用的局部变量必须是最终的(final)或事实上最终的

n = 11;

对局部变量的限制原因:

第一:实例变量存在堆中,而局部变量则存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。若局部变量仅赋值一次则相当于是final的。

第二:这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

闭包(closure):

闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。

闭包可以作为参数传递给另一个函数,也可以访问和修改其作用域之外的变量。

Lambda和匿名类可以做类似于闭包的事情:

它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容,这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的可能性。实例变量可以,因为它们存在堆中,而堆是在线程之间共享的。

5、方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。方法引用可以看作仅仅调用特定方法的Lambda的一种快捷写法。

基本思想:如果一个Lambda代表的只是“直接调用这个方法”,最好用名称来调用它,而不是去描述如何调用它。可以把方法引用看作是针对仅仅涉及单一方法的Lambda的语法糖,因为在表达同样的事情时代码更少了。

格式:目标引用放在分隔符::前,方法的名称放在后面。

例如: Apple::getWeight 就是引用了Apple类中定义的getWeight方法,等效于(Apple a) -> a.getWeight()。

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

可以改写为:

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

注意:不需要括号,因为并没有实际调用这个方法。

方法引用主要有三类:

(1) 指向静态方法的方法引用(如Integer.parseInt方法,写作Integer::parseInt)。

(2) 指向任意类型实例方法的方法引用(如String.length方法,写作String::length)。

(3) 指向现有对象的实例方法的方法引用(假设有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,写成expensiveTransaction::getValue)。

示例:对一个字符串的List排序,忽略大小写。

List list = Arrays.asList("a","b","A","B");

list.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

可以改成:

list.sort(String::compareToIgnoreCase);

构造函数引用:

对于一个现有构造函数,可以用它的名称和关键字new来创建它的一个引用: ClassName::new,其功能与指向静态方法的引用类似。

如果构造函数没有参数,适合Supplier的签名:

Supplier c1 = Apple::new; //构造函数引用指向默认的Apple()构造函数

Apple a1 = c1.get(); //调用Supplier的get方法将产生一个新的Apple

等价于:

Supplier c1 = () -> new Apple(); //利用默认构造函数创建Apple的Lambda表达式

Apple a1 = c1.get();

如果构造函数的签名是Apple(Integer weight),那么它适合Function接口的签名:

Function c2 = Apple::new; //指向Apple(Integer weight) 的构造函数引用

Apple a2 = c2.apply(100); //调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple

等价于:

Function c2 = (weight) -> new Apple(weight); //用要求的重量创建一个Apple的Lambda表达式

Apple a2 = c2.apply(100);

完整示例:筛选指定重量集合中的苹果List

List weights = Arrays.asList(7, 3, 4, 10);

List apples = map(weights, Apple::new);

public static List map(List list, Function f){

    List result = new ArrayList<>();

    for(Integer e: list){

        result.add(f.apply(e));

    }

    return result;

}

如果具有两个参数的构造函数Apple(String color, Integer weight),那么它适合BiFunction接口的签名:

BiFunction c3 = Apple::new;

Apple c3 = c3.apply("green", 100);

等价于:

BiFunction c3 = (color, weight) -> new Apple(color, weight); 

 Apple c3 = c3.apply("green", 100);

如何构建具有三个参数的构造函数,如Color(int, int, int),使用构造函数引用呢?

public interface TriFunction{

    R apply(T t, U u, V v);

}

可以这样使用构造函数引用:

TriFunction colorFactory = Color::new;

6、Lambda和方法引用实战

如何实现对库存进行排序,然后比较苹果的重量?

方式1:使用传递代码

Java 8的API已经提供了一个List可用的sort方法;

void sort(Comparator c)

public class AppleComparator implements Comparator {

    public int compare(Apple a1, Apple a2){

        return a1.getWeight().compareTo(a2.getWeight());

    }

}

inventory.sort(new AppleComparator());

方式2:使用匿名类

inventory.sort(new Comparator() {

    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()));

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

Comparator c = Comparator.comparing((Apple a) -> a.getWeight());

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

方式4:使用方法引用

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

7、复合Lambda表达式的有用方法

1)比较器复合

Comparator 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来创建更复杂的谓词。

Predicate notRedApple = redApple.negate(); //产生现有Predicate对象redApple的非

Predicate redAndHeavyApple = 

    redApple.and(a -> a.getWeight() > 150); //链接两个谓词来生成另一个Predicate对象

Predicate redAndHeavyAppleOrGreen = 

    redApple.and(a -> a.getWeight() > 150)

        .or(a -> "green".equals(a.getColor())); //链接Predicate的方法来构造更复杂Predicate对象

执行优先级顺序:

and和or方法是按照在表达式链中的位置,从左向右确定优先级的。a.or(b).and(c)可以看作(a || b) && c。

3)函数复合

Function接口包含andThen和compose两个默认方法,它们都会返回Function的一个实例。

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

Function f = x -> x + 1;

Function g = x -> x * 2;

Function h = f.andThen(g); //数学上会写作g(f(x))或(g o f)(x)

int result = h.apply(1); //返回4

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

Function f = x -> x + 1;

Function g = x -> x * 2;

Function h = f.compose(g);  //数学上会写作f(g(x))或(f o g)(x)

int result = h.apply(1); //返回3

andThen和compose的区别

总结:

1)Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出异常的列表。

2)Lambda表达式让你可以简洁地传递代码。

3)函数式接口就是仅仅声明了一个抽象方法的接口。

4)只有在接受函数式接口的地方才可以使用Lambda表达式。

5)Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

6)Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate 、Function、Supplier、Consumer和BinaryOperator

7)为了避免装箱操作,对Predicate和Function等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。

8)环绕执行模式可以配合Lambda提高灵活性和可重用性。

9)Lambda表达式所需要代表的类型称为目标类型。

10)方法引用让你重复使用现有的方法实现并直接传递它们。

11)Comparator、Predicate和Function等函数式接口都有可以用来结合Lambda表达式的默认方法。

                                                                                        --示例摘自《Java8实战》

你可能感兴趣的:(Java8学习笔记之Lambda表达式)