JDK8新特性【Lambda、函数式接口、Stream流】

Properties集合

1.1 概述

java.util.Properties 继承于Hashtable ,来表示一个持久的属性集。它使用键值结构存储数据,每个键及其对应值都是一个字符串。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。

1.2 Properties类

构造方法

  • public Properties() :创建一个空的属性列表。

与流相关的方法

  • public void load(InputStream inStream): 从字节输入流中读取键值对。

  • public void load(Reader reader): 从字符输入流中读取键值对。

参数中使用了字节输入流,通过流对象,可以关联到某文件上,这样就能够加载文本中的数据了。文本数据格式:

filename=a.txt
length=209385038
location=D:\a.txt

加载代码演示:

public static void main(String[] args) throws FileNotFoundException {
    // 创建属性集对象
    Properties pro = new Properties();
    // 加载文本中信息到属性集
    pro.load(new FileInputStream("read.txt"));
    // 遍历集合并打印
    Set<String> strings = pro.stringPropertyNames();
    for (String key : strings ) {
        System.out.println(key+" -- "+pro.getProperty(key));
    }
}

Lambda表达式

2.1 函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

y = 2*x + 5;

public class A {

public int method(int x) {

return 2*x + 5;

}

}

A a = new A();

int y = a.method(5);

java.util.Scanner类

Scanner sc = ...;

int num = sc.nextInt();

做什么,而不是怎么做

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

2.2 Lambda的优化

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。

传统写法,代码如下:

public class Demo03Thread {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多线程任务执行!");
            }
        }).start();
    }
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

代码分析:

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;

  • 为了指定run的方法体,不得不需要Runnable接口的实现类;

  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;

  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;

  • 而实际上,似乎只有方法体才是关键所在

Lambda表达式写法,代码如下:

借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

public class Demo04LambdaRunnable {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
    }
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

2.3 Lambda的格式

标准格式:

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数

  • 一个箭头

  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。

  • ->是新引入的语法格式,代表指向动作。

  • 大括号内的语法与传统方法体要求基本一致。

匿名内部类与lambda对比:

new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("多线程任务执行!");
            }
}).start();

仔细分析该代码中,Runnable接口只有一个run方法的定义:

  • public abstract void run();

即制定了一种做事情的方案(其实就是一个方法):

  • 无参数:不需要任何条件即可执行该方案。

  • 无返回值:该方案不产生任何结果。

  • 代码块(方法体):该方案的具体执行步骤。

同样的语义体现在Lambda语法中,要更加简单:

() -> System.out.println("多线程任务执行!")
  • 前面的一对小括号即run方法的参数(无),代表不需要任何条件;

  • 中间的一个箭头代表将前面的参数传递给后面的代码;

  • 后面的输出语句即业务逻辑代码。

参数和返回值:

下面举例演示java.util.Comparator<T>接口的使用场景代码,其中的抽象方法定义为:

  • public abstract int compare(T o1, T o2);

当需要对一个对象数组进行排序时,Arrays.sort方法需要一个Comparator接口实例来指定排序的规则。假设有一个Person类,含有String nameint age两个成员变量:

public class Person { 
    private String name;
    private int age;
    
    // 省略构造器、toString方法与Getter Setter 
}

传统写法

如果使用传统的代码对Person[]数组进行排序,写法如下:

public class Demo05Comparator {
    public static void main(String[] args) {
        // 本来年龄乱序的对象数组
        Person[] array = { new Person("古力娜扎", 19),          new Person("迪丽热巴", 18),             new Person("马尔扎哈", 20) };
​
        // 匿名内部类
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例
​
        for (Person person : array) {
            System.out.println(person);
        }
    }
}

这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

代码分析

下面我们来搞清楚上述代码真正要做什么事情。

  • 为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;

  • 为了指定compare的方法体,不得不需要Comparator接口的实现类;

  • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类;

  • 必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;

  • 实际上,只有参数和方法体才是关键

Lambda写法

public class Demo06ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
            new Person("古力娜扎", 19),
            new Person("迪丽热巴", 18),
            new Person("马尔扎哈", 20) };
​
        Arrays.sort(array, (Person a, Person b) -> {
            return a.getAge() - b.getAge();
        });
​
        for (Person person : array) {
            System.out.println(person);
        }
    }
}

省略格式:

省略规则

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;

  2. 如果小括号内有且仅有一个参数,则小括号可以省略;

  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。

可推导即可省略

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());

2.4 Lambda的前提条件

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。

  2. 使用Lambda必须具有接口作为方法参数。 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

函数式接口

3.1 概述

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

备注:从应用层面来讲,Java中的Lambda可以看做是匿名内部类的简化格式,但是二者在原理上不同。

格式

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:

public interface MyFunctionalInterface {    
    void myMethod();
}

FunctionalInterface注解

@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
    void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

3.2 常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。前文的MySupplier接口就是在模拟一个函数式接口:java.util.function.Supplier<T>。其实还有很多,下面是最简单的几个接口及使用示例。

Supplier接口

java.util.function.Supplier<T>接口,它意味着"供给" , 对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

抽象方法 : get

仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。

public class Demo08Supplier {
    private static String getString(Supplier<String> function) {
        return function.get();
    }
​
    public static void main(String[] args) {
        String msgA = "Hello";
        String msgB = "World";
        System.out.println(getString(() -> msgA + msgB));
    }
}

求数组元素最大值

使用Supplier接口作为方法参数类型,通过Lambda表达式求出int数组中的最大值。提示:接口的泛型请使用java.lang.Integer类。

代码示例:

public class DemoIntArray {
    public static void main(String[] args) {
        int[] array = { 10, 20, 100, 30, 40, 50 };
        printMax(() -> {
            int max = array[0];
            for (int i = 1; i < array.length; i++) {
                if (array[i] > max) {              
                    max = array[i];
                }
            }
            return max;
        });
    }
​
    private static void printMax(Supplier<Integer> supplier) {
        int max = supplier.get();
        System.out.println(max);
    }
}

Consumer接口

java.util.function.Consumer<T>接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。

抽象方法:accept

Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。基本使用如:

//给你一个字符串,请按照大写的方式进行消费
import java.util.function.Consumer;
​
public class Demo09Consumer {
    public static void main(String[] args) {
        String str = "Hello World";
        //1.lambda表达式标准格式
        fun(str,(String s)->{
            System.out.println(s.toUpperCase());
        });
        //2.lambda表达式简化格式
        fun(str,s-> System.out.println(s.toUpperCase()));
    }
    /*
        定义方法,使用Consumer接口作为参数
        fun方法: 消费一个String类型的变量
     */
    public static void fun(String s,Consumer<String> con) {
        con.accept(s);
    }
}

Function接口

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有进有出,所以称为“函数Function”。

抽象方法:apply

Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。使用的场景例如:将String类型转换为Integer类型。

//给你一个String的数字,你给我转成一个int数字
public class Demo11FunctionApply {
    private static void method(Function<String, Integer> function, Str str) {
        int num = function.apply(str);
        System.out.println(num + 20);
    }
​
    public static void main(String[] args) {
        method(s -> Integer.parseInt(s) , "10");
    }
}

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>接口。

抽象方法:test

Predicate接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景,条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。

//1.练习:判断字符串长度是否大于5
//2.练习:判断字符串是否包含"H"
public class Demo15PredicateTest {
    private static void method(Predicate<String> predicate,String str) {
        boolean veryLong = predicate.test(str);
        System.out.println("字符串很长吗:" + veryLong);
    }
​
    public static void main(String[] args) {
        method(s -> s.length() > 5, "HelloWorld");
    }
}

第四章 Stream流

在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

4.1 引言

传统集合的多步遍历代码

几乎所有的集合(如Collection接口或Map接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

public class Demo10ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        for (String name : list) {
            System.out.println(name);
        }
    }  
}

这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。

循环遍历的弊端

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:

  • for循环的语法就是“怎么做

  • for循环的循环体才是“做什么

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B

  2. 然后再根据条件二过滤为子集C

那怎么办?在Java 8之前的做法可能为:

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;

  2. 然后筛选名字有三个字的人;

  3. 最后进行对结果进行打印输出。

public class Demo11NormalFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
​
        List<String> zhangList = new ArrayList<>();
        for (String name : list) {
            if (name.startsWith("张")) {
                zhangList.add(name);
            }
        }
​
        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
                shortList.add(name);
            }
        }
​
        for (String name : shortList) {
            System.out.println(name);
        }
    }
}

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。

那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?

Stream的更优写法

下面来看一下借助Java 8的Stream API,什么才叫优雅:

public class Demo12StreamFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
​
        list.stream()
            .filter(s -> s.startsWith("张"))
            .filter(s -> s.length() == 3)
            .forEach(s -> System.out.println(s));
    }
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

4.2 流式思想概述

注意:请暂时忘记对传统IO流的固有印象!

整体来看,流式思想类似于工厂车间的“生产流水线”。

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。

这里的filtermapskip都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

4.3 获取流方式

java.util.stream.Stream<T>是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)

获取一个流非常简单,有以下几种常用的方式:

  • 所有的Collection集合都可以通过stream默认方法获取流;

  • Stream接口的静态方法of可以获取数组对应的流。

方式1 : 根据Collection获取流

public default Stream<E> stream(): 获取Collection集合对象对应的Stream流对象

首先,java.util.Collection接口中加入了default方法stream用来获取流,所以其所有实现类均可获取流。

import java.util.*;
import java.util.stream.Stream;
/*
    获取Stream流的方式
​
    1.Collection中 方法
        Stream stream()
    2.Stream接口 中静态方法
        of(T...t) 向Stream中添加多个数据
 */
public class Demo13GetStream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // ...
        Stream<String> stream1 = list.stream();
​
        Set<String> set = new HashSet<>();
        // ...
        Stream<String> stream2 = set.stream();
    }
}

方式2: 根据数组获取流

如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream接口中提供了静态方法of,使用很简单:

import java.util.stream.Stream;
​
public class Demo14GetStream {
    public static void main(String[] args) {
        String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
        Stream<String> stream = Stream.of(array);
    }
}

备注:of方法的参数其实是一个可变参数,所以支持数组。

4.4 常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。本小节中,终结方法包括countforEach方法。

  • 非终结方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

备注:本小节之外的更多方法,请自行参考API文档。

forEach : 逐一处理

虽然方法名字叫forEach,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的

void forEach(Consumer<? super T> action);

该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。例如:

import java.util.stream.Stream;
​
public class Demo15StreamForEach {
    public static void main(String[] args) {
        Stream<String> stream =  Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爷爷","蛇精","蝎子精");
        //Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
        stream.forEach((String str)->{System.out.println(str);});
    }
}

在这里,lambda表达式(String str)->{System.out.println(str);}就是一个Consumer函数式接口的示例。

filter:过滤

可以通过filter方法将一个流转换成另一个子集流。方法声明:

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个Predicate函数式接口参数(可以是一个Lambda)作为筛选条件。

基本使用

Stream流中的filter方法基本使用的代码如:

public class Demo16StreamFilter {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter((String s) -> {return s.startsWith("张");});
    }
}

在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

count:统计个数

正如旧集合Collection当中的size方法一样,流提供count方法来数一数其中的元素个数:

long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:

public class Demo17StreamCount {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s -> s.startsWith("张"));
        System.out.println(result.count()); // 2
    }
}

limit:取用前几个

limit方法可以对流进行截取,只取用前n个。方法签名:

Stream<T> limit(long n):获取Stream流对象中的前n个元素,返回一个新的Stream流对象

参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:

import java.util.stream.Stream;
​
public class Demo18StreamLimit {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.limit(2);
        System.out.println(result.count()); // 2
    }
}

skip:跳过前几个

如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:

Stream<T> skip(long n): 跳过Stream流对象中的前n个元素,返回一个新的Stream流对象

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

import java.util.stream.Stream;
​
public class Demo19StreamSkip {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.skip(2);
        System.out.println(result.count()); // 1
    }
}

concat:组合

如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把参数列表中的两个Stream流对象a和b,合并成一个新的Stream流对象

备注:这是一个静态方法,与java.lang.String当中的concat方法是不同的。

该方法的基本使用代码如:

import java.util.stream.Stream;
​
public class Demo20StreamConcat {
    public static void main(String[] args) {
        Stream<String> streamA = Stream.of("张无忌");
        Stream<String> streamB = Stream.of("张翠山");
        Stream<String> result = Stream.concat(streamA, streamB);
    }
}

4.5 Stream综合案例

现在有两个ArrayList集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;

  2. 第一个队伍筛选之后只要前3个人;

  3. 第二个队伍只要姓张的成员姓名;

  4. 第二个队伍筛选之后不要前2个人;

  5. 将两个队伍合并为一个队伍;

  6. 打印整个队伍的姓名信息。

两个队伍(集合)的代码如下:

public class Demo21ArrayListNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        one.add("迪丽热巴");
        one.add("宋远桥");
        one.add("苏星河");
        one.add("老子");
        one.add("庄子");
        one.add("孙子");
        one.add("洪七公");
​
        List<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("张无忌");
        two.add("张三丰");
        two.add("赵丽颖");
        two.add("张二狗");
        two.add("张天爱");
        two.add("张三");
        // ....
    }
}

传统方式

使用for循环 , 示例代码:

public class Demo22ArrayListNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        // ...
​
        List<String> two = new ArrayList<>();
        // ...
​
        // 第一个队伍只要名字为3个字的成员姓名;
        List<String> oneA = new ArrayList<>();
        for (String name : one) {
            if (name.length() == 3) {
                oneA.add(name);
            }
        }
​
        // 第一个队伍筛选之后只要前3个人;
        List<String> oneB = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            oneB.add(oneA.get(i));
        }
​
        // 第二个队伍只要姓张的成员姓名;
        List<String> twoA = new ArrayList<>();
        for (String name : two) {
            if (name.startsWith("张")) {
                twoA.add(name);
            }
        }
​
        // 第二个队伍筛选之后不要前2个人;
        List<String> twoB = new ArrayList<>();
        for (int i = 2; i < twoA.size(); i++) {
            twoB.add(twoA.get(i));
        }
​
        // 将两个队伍合并为一个队伍;
        List<String> totalNames = new ArrayList<>();
        totalNames.addAll(oneB);
        totalNames.addAll(twoB);        
​
        // 打印整个队伍的姓名信息。
        for (String name : totalNames) {
            System.out.println(name);
        }
    }
}

运行结果为:

宋远桥
苏星河
洪七公
张二狗
张天爱
张三

Stream方式

等效的Stream流式处理代码为:

public class Demo23StreamNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        // ...
​
        List<String> two = new ArrayList<>();
        // ...
​
        // 第一个队伍只要名字为3个字的成员姓名;
        // 第一个队伍筛选之后只要前3个人;
        Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);
​
        // 第二个队伍只要姓张的成员姓名;
        // 第二个队伍筛选之后不要前2个人;
        Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);
​
        // 将两个队伍合并为一个队伍;
        // 根据姓名创建Person对象;
        // 打印整个队伍的Person对象信息。
        Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
    }
}

运行效果完全一样:

宋远桥
苏星河
洪七公
张二狗
张天爱
张三

4.6 函数拼接与终结方法

在上述介绍的各种方法中,凡是返回值仍然为Stream接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream接口的为终结方法,不再支持链式调用。如下表所示:

方法名 方法作用 方法种类 是否支持链式调用
count 统计个数 终结
forEach 逐一处理 终结
filter 过滤 函数拼接
limit 取用前几个 函数拼接
skip 跳过前几个 函数拼接
concat 组合 函数拼接

第五章 方法引用

5.1 概述和方法引用符

来看一个简单的函数式接口以应用Lambda表达式 , 在accept方法中接收字符串 , 目的就是为了打印显示字符串 , 那么通过Lambda来使用它的代码很简单:

public class DemoPrintSimple {
    private static void printString(Consumer<String> data, String str) {
        data.accept(str);
    }
    public static void main(String[] args) {
        printString(s -> System.out.println(s), "Hello World");
    }
}

由于lambda表达式中,调用了已经实现的println方法 ,可以使用方法引用替代lambda表达式.

符号表示 : ::

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用

应用场景 : 如果Lambda要表达的函数方案 , 已经存在于某个方法的实现中,那么则可以使用方法引用。

如上例中,System.out对象中有个println(String)方法 , 恰好就是我们所需要的 , 那么对于Consumer接口作为参数,对比下面两种写法,完全等效:

  • Lambda表达式写法:s -> System.out.println(s); 拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理。

  • 方法引用写法:System.out::println 直接让System.out中的println方法来取代Lambda。

推导与省略 : 如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。函数式接口是Lambda的基础,而方法引用是Lambda的简化形式。

5.2 方法引用简化

只要“引用”过去就好了:

public class DemoPrintRef {
    private static void printString(Consumer<String> data, String str) {
        data.accept(str);
    }
    public static void main(String[] args) {
        printString(System.out::println, "HelloWorld");
    }
}

请注意其中的双冒号::写法,这被称为“方法引用”,而双冒号是一种新的语法。

5.3 扩展的引用方式

对象名--引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:

public class DemoMethodRef {
     public static void main(String[] args) {
        String str = "hello";
        printUP(str::toUpperCase);
    }
​
    public static void printUP(Supplier< String> sup ){
        String apply =sup.get();
        System.out.println(apply);
    }
}

类名--引用静态方法

由于在java.lang.Math类中已经存在了静态方法random,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:

public class DemoMethodRef {
  public static void main(String[] args) {
        printRanNum(Math::random);
    }
​
    public static void printRanNum(Supplier<Double> sup ){
        Double apply =sup.get();
        System.out.println(apply);
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:n -> Math.abs(n)

  • 方法引用:Math::abs

类--构造引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用类名称::new的格式表示。首先是一个简单的Person类:

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

要使用这个函数式接口,可以通过方法引用传递:

//给你一个字符串String(名字),转换成一个Person对象
public class Demo09Lambda {
    public static void main(String[] args) {
        String name = "tom";
        Person person = createPerson(Person::new, name);
        System.out.println(person);
        
    }
​
    public static Person createPerson(Function<String, Person> fun , String name){
        Person p = fun.apply(name);
        return p;
​
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:name -> new Person(name)

  • 方法引用:Person::new

数组--构造引用

数组也是Object的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时,需要一个函数式接口:

在应用该接口的时候,可以通过方法引用传递:

//给你一个Integer数字,获取到一个数组,数组的长度就是给的Integer数字
public class Demo11ArrayInitRef {   
   public static void main(String[] args) {
​
        int[] array = createArray(int[]::new, 3);
        System.out.println(array.length);
​
    }
​
    public static int[] createArray(Function<Integer , int[]> fun , int n){
        int[] p = fun.apply(n);
        return p;
​
    }
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:length -> new int[length]

  • 方法引用:int[]::new

注意 : 方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式 , 同学们可以尝试着 , 将之前使用lambda的地方 , 改写成方法引用的形式 ,不过要注意的是方法引用只能"引用"已经存在的方法!

猜你喜欢

转载自blog.csdn.net/qq_46020806/article/details/130831178
今日推荐