前言

一、Lamdba表达式

1.演化示例

2.语法

3.作用域

二、方法引用

三 、默认方法

四、Lambda与Streams结合使用


前言

        Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它支持函数式编程,新的JavaScript 引擎,新的日期 API,新的Stream API 等。我们主要学习下Lambda表达式。Lambda表达式使我们能够封装单个行为并将其传递给其他代码。如果希望对集合的每个元素执行某个操作,也可以使用lambda表达式。以下功能支持Lambda表达式:方法引用、默认方法、Streams。这三个特性也是java8的新特性。下面一一来学习Lambda是什么,以及Lambda表达式与方法引用、默认方法、Streams这3大特性如何结合使用。

一、Lamdba表达式

        Lambda 允许把函数作为一个方法的参数,类似于匿名类。如果匿名类的实现非常简单,比如只包含一个方法的接口,那么匿名类的语法可能显得笨拙一些,此时用Lambda表达式会更简单易读。Lambda的好处我们先从示例来一步步,后面再来说语法就会理解得更深刻。

1.演化示例

        下面的演化示例,演示了如何从通用的写法一步步演化到使用Lamdba表达式,并顺带介绍几个函数式方法的使用,如Consumer、Predicate、Fucntion等,通过这几个示例可演化理解为什么Lambda表达式强大、应用越来越广泛。

        示例1、以下示例中主要做了以下操作:1、定义了一个Person对象,里面创建一个通用搜索方法printPersons执行一句打印语句,main方法去执行搜索方法printPersons;2、正常情况下由于搜索方法需要传一个接口判断搜索条件(示例中我们满足CheckPerson的test方法的Person实例即可调用printPerson方法),我们又定义了一个接口CheckPerson及其实现类CheckPersonEligibleForSelectiveService,故程序设计如下:

//1、定义一个Person对象
public class Person {
    //1.1 以下为对象基本属性和方法
    public enum Sex {
        MALE, FEMALE
    }
    String name;
    Sex gender;
    int age;
    public Person(String name, int age, Sex gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    //生成getter和setter...

    public void printPerson() {
        System.out.println("printPerson:name为"+this.name+",年龄为:"+this.age);
    }

    //1.2 创建一个通用搜索方法
    public static void printPersons(List<Person> roster, CheckPerson tester) {
        for (Person p : roster) {
            if (tester.test(p)) {
                p.printPerson();
            }
        }
    }

    //1.3 main函数执行搜索方法printPersons
    public static void main(String[] args) {
        List<Person> personList = new ArrayList<>();
        Person p1 = new Person("a",19, Person.Sex.MALE);
        Person p2 = new Person("b",10, Person.Sex.MALE);
        personList.add(p1);
        personList.add(p2);
        //1.3.1 搜索方案1:本地类
        printPersons(personList,new CheckPersonEligibleForSelectiveService());
    }
}

//2、 CheckPerson是一个接口,只有一个test方法
interface CheckPerson {
    boolean test(Person p);
}

//3、 CheckPerson接口的实现类
class CheckPersonEligibleForSelectiveService implements CheckPerson {
    @Override
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
                p.getAge() >= 18 &&
                p.getAge() <= 25;
    }
}

        示例2、尽管以上设计没有问题,可以考虑下还能不能更简洁呢?毕竟CheckPerson接口只有一个方法需要实现,对于不同的搜索条件就要定义定义本地实现类。这时我们想到了匿名类,不需要本地类,搜索条件可以直接写在匿名类中,修改后的代码如下(以下代码替换示例1-1.3.1写法):

//1.3.2 搜索方案2:匿名类
printPersons(personList, new CheckPerson() {
            @Override
            public boolean test(Person p) {
                return p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25;
            }
        });

        示例3、以上匿名类方案减少了所需的代码量,我们不必为要执行的每个搜索创建一个新类。然而,考虑到CheckPerson接口只包含一个方法,匿名类的语法比较笨重。在这种情况下,java1.8提供了lambda表达式代替匿名类,修改后的代码如下:

//1.3.3 搜索方案3:lambda
printPersons(personList,
    (Person p) -> p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25);

         示例4、大部分优化到上面步骤就很好了。但对于我们这个例子来说还可以再简单点,即用Predicate接口替代CheckPerson接口。为什么呢?CheckPerson是一个非常简单的接口,也是符合函数式接口,因为它只包含一个抽象方法并且这个方法接受一个参数并返回一个布尔值。JDK1.8定义了几个标准的函数式接口,您可以在java.util.function包中找到这些接口,比如Predicate、Consumer接口。Predicate源码如下:

//函数式接口是具有一个抽象方法的接口,可以有多个默认方法或静态方法
//@FunctionalInterface可以来标记一个函数式接口.
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    //下面有几个默认方法
    ... ...
}

        Predicate接口里面有test方法,可以代替返回布尔值的 CheckPerson.boolean test(Person p),这进一步减少了所需的代码量。修改后的代码如下:1、先定义一个包含Predicate参数的搜索方法替代通过的方法printPersons;2、调用搜索方法与1.3.3代码写法相同:

//先定义一个Predicate参数的方法,可以替换示例1-1.2中的方法printPersons
public static void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) {
        for (Person p : roster) {
            if (tester.test(p)) {
                p.printPerson();
            }
        }
    }

        如何调用以上方法printPersonsWithPredicate: 

//1.3.4 搜索方案4:运用标准函数接口Predicate,与1.3.3代码写法相同
printPersonsWithPredicate(personList,
    p -> p.gender == Person.Sex.MALE &&p.getAge() >= 18 && p.getAge() <= 25);

         示例5、改造到这里后,还有地方可以用Lambda表达式吗?可以!除了调用Person的方法printPerson之外,我们还可以指定一个不同的方法,以在那些满足tester所指定标准的Person实例上执行。假设您想要用Lambda表达式传递一个类似于printPerson的方法,这个printPerson方法接受一个参数(Person类型的对象)并返回void。记住,要使用lambda表达式,您需要实现一个函数式接口。在这种情况下,您需要一个函数式接口,该接口包含一个抽象方法,该方法可以接受Person类型的参数并返回void。下面的方法用Consumer<Person>的实例替换了调用p.printPerson(),该实例调用accept方法:

//定义一个有Consumer参数的方法。可以替换示例1-1.2中的方法printPersons
public static void processPersons(List<Person> roster, Predicate<Person> tester, Consumer<Person> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                block.accept(p);
            }
        }
    }

        如何调用以上方法processPersons:

//1.3.5 搜索方案5:用Consumer接受Person对象的方法,这里传入printPerson方法
processPersons(personList,
                p -> p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25,
                p -> p.printPerson());

         示例6、如果你想对你的成员方法做更多的事情,而不是打印出来呢?假设您想验证成员的配置文件或检索他们的联系信息?在这种情况下,您需要一个包含返回值的抽象方法的函数式接口。 Function<T,R> 接口包含了R apply(T t)方法,下面的方法由参数mapper得到返回的data,然后blockg再对data执行一个操作:

//定义一个Function接口的方法,可以替换示例1-1.2中的printPersons
public static void processPersonsWithFunction(List<Person> roster, Predicate<Person> tester, Function<Person, String> mapper, Consumer<String> block) {
        for (Person p : roster) {
            if (tester.test(p)) {
                String data = mapper.apply(p);//有String返回值
                block.accept(data);//对返回值操作
            }
        }
    }

        如何调用以上方法processPersonsWithFunction: 

//1.3.6 搜索方案6:用Fuction定义一个有返回值的参数,用Consumer对返回再操作
processPersonsWithFunction(personList,
    p -> p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25,
    p -> p.getName(),
    name -> System.out.println(name));

 

2.语法

(parameters) -> expression 或 (parameters) ->{ body; }。->符号分隔参数和lambda表达式主体。以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
  • 注意:
  • lambda表达式主体可以有局部变量,语句。我们可以在lambda表达式主体中使用break,continue和return。我们甚至可以从lambda表达式主体中抛出异常。
  • lambda表达式没有名称,因为它表示匿名内部类。
  • lambda表达式的返回类型由编译器推断。
  • lambda表达式不能像方法一样有throws子句。
  • lambda表达式不能是泛型,而泛型在函数接口中定义。

3.作用域

        lambda表达式不会创建自己的作业域,如果我们在lambda中使用关键字thissuper表达式在方法中,它们的行为与我们在该方法中使用它们一样,即Lambda中的this指的是外部类不是lambda表达式本身。

二、方法引用

调用现有方法之外什么也不做,方法引用为此而设计。方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 。方法引用的一般语法是:Qualifier::MethodName(限定符::方法)。方法引用的类型有4种(下图摘自oracle官网)

Kind

Example

示例(以第一节示例1为代码参考)

Reference to a static method(静态方法引用)

ContainingClass::staticMethodName

Person::printPersons

Reference to an instance method of a particular object(特定对象的方法引用)

containingObject::instanceMethodName

Person p = new Person("a",19, Person.Sex.MALE);

p::printPerson

Reference to an instance method of an arbitrary object of a particular type(特定类型的任意对象的方法引用)

ContainingType::methodName

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" }; Arrays.sort(stringArray, String::compareToIgnoreCase);

Reference to a constructor(构造器引用)

ClassName::new

例1:Supplier<String> func1 = () -> new String();//Supplier是标准函数式接口,他的get方法不接受参数且返回对象 System.out.println("Empty String:"+func1.get());//打印结果:Empty String:

例2:Function<String,String> func2 = String::new; //() -> new String()变形

System.out.println(func2.apply("hello"));//打印结果:hello

三 、默认方法

public修饰符。

//接口中定义了一个默认方法print(),实现类可以不用实现它,用Car.super.print()调用
public interface Car {
   void test();
   default void print(){
      System.out.println("我是一辆车!");
   }
}

四、Lambda与Streams结合使用

第一章节示例6中1.3.6的调用写法外,还可以用stream类的聚合操作来调用:

personList
            .stream() //生成串行流
            .filter(p -> p.gender == Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25) //聚合操作filter:过滤
            .map(p -> p.getName()) //聚合操作map:方法用于映射每个元素到对应的结果
            .forEach(name -> System.out.println(name)); //Stream 提供方法forEach来迭代流中的每个数据

生成流->中间处理操作->最终操作得到处理的结果。

生成流:

        在 Java 8 中, 集合接口有两个方法来生成流:stream() − 为集合创建串行流;parallelStream() − 为集合创建并行流。

//1 对象 
Stream stream = Stream.of("a", "b", "c");
String[] stringArray = {"Barbara", "James", "Mary", "John"};
//2 数组
Stream.of(stringArray).forEach(s -> System.out.println(s));
//3 集合
Arrays.asList(stringArray).stream().forEach(System.out::println);

中间处理:

         流元素会经过中间处理过程如,filter, map, find, sorted、distinct、skip等,这些操作后返回的仍然是个流。

最终处理:

        常见的流的最终处理方法有:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator。

常用示例:

String[] stringArray = {"Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda"};

//找出名称长度大于5,排序后只取4条结果。打印结果:Barbara Michael Patricia Robert
Arrays.asList(stringArray).stream().filter(s -> s.length()>5).sorted(String::compareToIgnoreCase).limit(4).forEach(System.out::println);

//找出名称长度大于5,并把名称变为大写。打印结果:PATRICIA
Arrays.asList(stringArray).stream().filter(s -> s.length()>7).map(String::toUpperCase).forEach(System.out::println);

//是否有叫Mary的人。
Arrays.asList(stringArray).stream().anyMatch(n -> n.equals("Mary"));//true,任意一个匹配
Arrays.asList(stringArray).stream().allMatch(n -> n.equals("Mary"));//false,所有元素都是
Arrays.asList(stringArray).stream().noneMatch(n -> n.equals("Marys"));//true,所有元素都不是

//名称长度为6,最终结果生成集合
List<String> cs = Arrays.asList(stringArray).stream().filter(s -> s.length()==6).map(String::toUpperCase).collect(Collectors.toList());//ROBERT

//合并字符串,结果为:BARBARA, PATRICIA, ROBERT, MICHAELROBERT
String cs = Arrays.asList(stringArray).stream().filter(s -> s.length()>5).map(String::toUpperCase).collect(Collectors.joining(", "));

//根据姓名的长度进行分组
Map<Integer,List<String>> mm =Arrays.asList(stringArray).stream().filter(s -> s.length()>5).map(String::toUpperCase).collect(Collectors.groupingBy(n -> n.length()));
//取姓名长度为7的名称,合并后输出,结果:BARBARA,MICHAEL
System.out.println(mm.get(7).stream().collect(Collectors.joining(",")));

      

参考:oracle官网