Java8中的函数式编程

Posted 灵派coder

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8中的函数式编程相关的知识,希望对你有一定的参考价值。

1.Lambda表达式与函数式接口

1.Lambda表达式简介

  • Lambda表达式的作用:使得Java可以把 函数 像对象一样作为语言的 一等公民 来对待。从而:

  1. 能够编写出 可读性更强、更加紧凑、抽象级别更高的代码

  2. 易于编写出可在多核CPU上高效运行、线程安全 的代码

  3. 在编写 回调函数 和事件处理程序时,可以摆脱匿名内部类的冗繁

  • Java8的lambda表达式其实是 匿名类 的 语法糖 ,本质上仍然是对象。

  • 函数成为 一等公民 的含义:可以将函数 赋值 给变量,可以在 参数 中传递函数,可以让一个方法的 返回值 是函数…
    即,凡是对象可以出现的地方,函数 都可以出现。

2.Lambda表达式的用法

  • 一个lambda表达式可由 用逗号分隔的参数列表 、 ->符号 、 函数体三部分组成,例如:

Arrays.asList("a""b""c").forEach((String item) -> System.out.println(item));
  • 因为 参数 item的 类型 可由编译器 推测 出来,因此也可简写成:

Arrays.asList("a""b""c").forEach(item -> System.out.println(item));
  • 上面的两种写法,等价于在Java8之前的版本中,使用 匿名类 的实现:

Arrays.asList("a""b""c").forEach(
        new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        }
);
  • 如果lambda的函数体 非单一语句 ,需要把函数体放在 一对花括号 中:

Arrays.asList("a""b""c").forEach((String item) -> {
    System.out.println(item);
    System.out.println(item);
});
  • lambda可以使用外部作用域的 常量 (如果是变量而非常量,会 隐式地 转为常量,建议显式地声明为常量):

public class Test {

    private int num = 1;

    public static void main(String[] args) {
        String separator = ",";
        Test test = new Test();
        Arrays.asList("a""b""c").forEach(
                item -> System.out.println(item + separator + test.num));
        //separator = ";"; 
        //编译期错误 Variable used in lambda expression should be final or effectively final
        //test = new Test(); 
        //编译期错误 Variable used in lambda expression should be final or effectively final
    }

}
  • lambda有可能会返回一个值。返回值 的类型也可由编译器 推断 出来。如果lambda的 函数体只有一行 的话,可以不显式使用 return语句,下面两个代码是等价的:

Arrays.asList("a""b""c").sort((s1, s2) -> s1.compareTo(s2));

Arrays.asList("a""b""c").sort((s1, s2) -> {
    return s1.compareTo(s2);
});

3.函数式接口简介

  • 所有的lambda表达式都可以替换成匿名类的实现方式,但并非所有的匿名类都能用lambda来表示。那么什么样的匿名类一定能用lambda来表示呢?那就是 实现于只有一个abstract方法的接口的匿名类 (不算Object中的方法,以及static方法和default方法)。而这种特殊的接口,在Java8中称为 函数式接口 ,用注解 @FunctionalInterface 表示:

@FunctionalInterface
public interface TestFunctional{
    void method();
}
  • 但为什么只有这样的接口,才能和lambda配合使用呢?其实不难理解。因为lambda本质是 没有显式重写方法、省略了重写方法名 的匿名类,而一个lambda只能对一个方法进行重写。

  • Java中所有 lambda表达式 的 类型 都是某个具体的函数式接口。

  • 一个 函数式接口 即使不加 @FunctionalInterface 注解,也可以与lambda配合使用,但这样的函数式接口是 容易出错 的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种 脆弱性 并且能够 明确声明 接口作为函数式接口的意图,建议显式使用该注解 。

4.默认方法与静态方法

  • Java 8用 默认方法 与 静态方法 这两个新概念来扩展接口的声明。

  • 默认方法 使用 default 关键字定义,默认方法与抽象方法不同,不需要被实现类来具体实现,但是可以被实现类继承或重写。默认方法的出现使Java可以 在扩展接口功能的同时保证向后兼容 (只要是Java1到Java7写出的代码,在Java8中依然可以编译通过)。例如Collection中的stream方法,如果Java8没有对默认方法的支持,那么所有的子类都需要对stream方法提供实现,显然无法保证向后兼容。

  • 静态方法 使用 static 关键字定义,与一般java类中的静态方法一样。静态方法的出现是为了 让工具方法和相关的类或接口放在一起,而不是放到另一个工具类中。例如下面的代码,如果Java8没有对静态方法的支持,那么ofAll方法必须放到另外的工具类Rulers中,显然不如直接放到Ruler中友好。

  • 示例:

@FunctionalInterface
public interface Ruler<T{

    /**
     * 校验T
     */

    void check(T checkTarget);

    /**
     * 或操作
     */

    @SuppressWarnings("unchecked")
    default Ruler<T> or(Ruler<T>... rulers) {
        return checkTarget -> {
            try {
                check(checkTarget);
            } catch (CheckException e) {
                ofAll(rulers).check(checkTarget);
            }
        };
    }

    /**
     * Ruler整合
     */

    @SafeVarargs
    static <T> Ruler<T> ofAll(Ruler<T>... rulers) {
        return (checkTarget -> Arrays.stream(rulers).forEach(ruler -> ruler.check(checkTarget)));
    }

}

5.四大常用的函数式接口

  • 消费型接口 Consumer<T>

/**
 * @name 消费型接口
 * @use Consumer<T>
 * @param T 传入参数
 * @fun 接受一个参数 无返回值
 * */

Consumer<String> con = (str) -> System.out.println(str);
con.accept("我是消费型接口!");
//输出:我是消费型接口!
  • 供给型接口 Supplier<R>

/**
 * @name 供给型接口
 * @use Supplier<R>
 * @param R 返回值类型
 * @fun 无参数 有返回值
 * */

Supplier<Date> supp = () -> new Date();
Date date = supp.get();
System.out.println("当前时间:" + date);
//输出:当前时间:Wed Jul 04 08:05:10 CST 2018
  • 函数型接口 Function<T,R>

/**
 * @name 函数型接口
 * @use Function<T,R>
 * @param T 传入参数
 * @return R 返回值类型
 * @fun 接受一个参数 有返回值
 * */

Function<String, String> fun = (str) -> "hello," + str;
String str = fun.apply("tom");
System.out.println(str);
//输出:hello,tom
  • 断定型接口 Predicate<T>

/**
 * @name 断定型接口
 * @use Predicate<T>
 * @param T 传入参数
 * @return Boolean 返回一个Boolean型值
 * @fun 接受一个参数 返回Boolean型值
 * */

Predicate<Integer> pre = (num) -> num > 0;
Boolean flag = pre.test(10);
System.out.println(flag);
//输出:true
  • 还有更多功能丰富的函数式接口,可自行了解。

2.方法引用

  • 在Java8中,我们可以直接通过 方法引用 来 简写lambda表达式 ,使语言的构造更紧凑简洁,减少冗余代码。但注意,方法引用与lambda表达式 并不完全等价 ,详见:java8中一个超易踩的坑:方法引用与lambda表达式的区别

  • 方法引用 有如下四种用法:

类型 示例
构造器引用 Class::new
静态方法引用 Class::static_method
任意对象的方法引用 instance::method
特定对象的方法引用 Class::method
  • 下面,我们以定义了4个方法的Car这个类为例子,区分Java8中支持的4种不同的方法引用。

public static class Car {
    public static Car create(final Supplier<Car> supplier) {
        return supplier.get();
    }

    public static void collide(final Car car) {
        System.out.println("Collided " + car.toString());
    }

    public void follow(final Car another) {
        System.out.println("Following the " + another.toString());
    }

    public void repair() {
        System.out.println("Repaired " + this.toString());
    } 
}

1.构造器引用

  • 第一种方法引用是构造器引用,它的语法是Class::new,或者更一般的Class<T>::new。请注意构造器 没有参数。

final Car car1 = Car.create(() -> new Car()); //lambda表达式
final Car car2 = Car.create(Car::new); //方法引用

2.静态方法引用

  • 第二种方法引用是静态方法引用,它的语法是Class::static_method。请注意这个方法 接受一个Car类型的参数。

final List<Car> cars = Arrays.asList(car1, car2);
cars.forEach((Car it) -> Car.collide(it)); //lambda表达式
cars.forEach(Car::collide); //方法引用

3.任意对象的方法引用

  • 第三种方法引用是特定类的任意对象的方法引用,它的语法是Class::method。请注意,这个方法 没有参数。

cars.forEach((Car it) -> it.repair()); //lambda表达式
cars.forEach(Car::repair); //方法引用

4.特定对象的方法引用

  • 第四种方法引用是特定对象的方法引用,它的语法是instance::method。请注意,这个方法  接受一个Car类型的参数。

final Car police = new Car();
cars.forEach((Car it) -> police.follow(it)); //lambda表达式
cars.forEach(police::follow); //方法引用

3.Optional

  • Java8引入 Optional 来通过一系列的 链式调用, 优雅地 解决 null安全问题。

1.Optional的三种构造方式

  • Optional.of(obj) :它要求传入的obj不能是null值, 否则会抛出NPE。

  • Optional.empty() :返回一个持有null的Optional实例。

  • Optional.ofNullable(obj) :它以一种智能的,宽容的方式来构造一个Optional实例。传null进到就得到Optional.empty(),非null就调用Optional.of(obj)。

2.Optional的错误使用

  1. 使用 isPresent() 方法:
    isPresent() 与 obj != null 没有任何分别,并不会使代码变得优雅

  2. 使用 get() 方法:
    没有 isPresent() 作铺垫的 get() 调用在IDEA中会收到警告

Reports calls to java.util.Optional.get() without first checking with a isPresent() call if a value is available. If the Optional does not contain a value, get() will throw an exception
调用 Optional.get() 前不事先用 isPresent() 检查值是否可用. 假如 Optional 不包含一个值, get() 将会抛出一个异常

  1. Optional 类型作为 类/实例属性 或  方法参数 时:
    把 Optional 类型用作属性或是方法参数在 IntelliJ IDEA 中是强力不推荐的

Reports any uses of java.util.Optional, java.util.OptionalDouble, java.util.OptionalInt, java.util.OptionalLong or com.google.common.base.Optional as the type for a field or a parameter. Optional was designed to provide a limited mechanism for library method return types where there needed to be a clear way to represent “no result”. Using a field with type java.util.Optional is also problematic if the class needs to be Serializable, which java.util.Optional is not.
使用任何像 Optional 的类型作为字段或方法参数都是不可取的. Optional 只设计为类库方法的, 可明确表示可能无值情况下的 返回类型 . Optional 类型不可被序列化, 用作字段类型会出问题的

  1. 错误示例:

//不要这么写:
Optional<User> userOpt = selectUserById(id);
if (userOpt.isPresent()) {
    return userOpt.get().getName();
else {
    return "none";
}

//这其实与我们以前不使用Optional时的代码没有任何区别:
User user = getUserById(id);
if (user != null) {
    return user.getName();
else {
    return "none";
}

//正确的写法:
return selectUserById(id).map(User::getName).orElse("none");

3.Optional的常见用法

  • 存在即返回,无则提供默认值:
    public T orElse(T other)

return opt.orElse(null);
//而不是 return opt.isPresent() ? opt.get() : null;
  • 存在即返回, 无则由函数来产生:
    public T orElseGet(Supplier<? extends T> other)

return user.orElseGet(() -> fetchAUserFromDatabase()); 
//而不要 return user.isPresent() ? user: fetchAUserFromDatabase();
  • 存在才对它做点什么:
    public void ifPresent(Consumer consumer)

user.ifPresent(System.out::println);

//而不要
if (user.isPresent()) {
  System.out.println(user.get());
}
  • Optional最重要的用法:
    public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
    map方法通常会搭配 orElse orElseGet orElseThrow 方法 使用:

return selectUserById(id)
        .map(u -> u.getUsername())
        .map(name -> name.toUpperCase())
        .orElse(null);

//在java8之前的版本,等价的写法是这样的:
User user = getUserById(id);
if (user != null) {
    String name = user.getUsername();
    if (name != null) {
        return name.toUpperCase();
    } else {
        return null;
    }
else {
    return null;
}
  • flatMap filter 方法的使用与map类似,这里就不展开了。

4.Stream

1.Stream简介

  • Stream是用函数式编程方式,在 集合类 上进行复杂操作的工具:

public static void main(String[] args) {
    long count1 = Stream.of(123)
            .filter(it -> it > 2)
            .count();

    long count2 = 0;
    List<Integer> list = Arrays.asList(123);
    for (Integer it : list) {
        if (it > 2) {
            count2++;
        }
    }


    System.out.println("count1:" + count1 + "---count2:" + count2);
    //输出:count1:1---count2:1
}
  • 上述代码实现了一种“获取集合中值大于2的元素个数”的功能。整个过程被 分解 为两种更简单的操作:过滤 和 计数。看似进行了两次遍历操作,事实上,类库巧妙的设计使得只对集合进行了一次遍历:Stream方法的返回值并不是一个新集合,而是一个Stream对象,是用来创建新集合的 配方(算法)。

  • 像filter这样只 描述 Stream,而不产生新集合的方法叫做 惰性求值方法;而像count这样用来从Stream中产生值的方法叫做 及早求值方法。

  • 只有调用了 及早求值方法 ,Stream中之前注入的 惰性求值方法 才会被真正的运行,否则,惰性求值方法 只会像 势能 一样,悬而不发:

// 势能存储
Stream<Integer> potentialEnergy = Stream.of(123)
        .filter(it -> {
            System.out.println("hello");
            return it > 2;
        });
System.out.println("world");

// 势能释放
long count = potentialEnergy.count();
System.out.println("count:" + count);

// 尝试将势能再次释放(抛出异常)
long countAgain = potentialEnergy.count();
System.out.println("countAgain:" + countAgain);

//输出:
//world 
//hello
//hello
//hello
//count:1
//java.lang.IllegalStateException: stream has already been operated upon or closed
  • 判断一个方法是 惰性求值 的还是 及早求值 的很简单:看它的 返回值。如果是 Stream 类型的就是惰性求值;否则就是及早求值。

  • 对Stream的使用方式是:形成一个惰性求值的 链,最后调用一个及早求值的方法返回想要的结果,或执行预期的操作。

为方便演示,这里定义了一个字段、两个实体类、一个main方法。下文的示例将放到main方法中,会用到这部分代码。

public class Test {

    public static void main(String[] args) {
        //示例代码
    }

    private static List<Business> businessList = Arrays.asList(
            new Business(1L"家政", Arrays.asList(
                    new Category(11L"保洁"),
                    new Category(12L"保姆")
            )),
            new Business(2L"速运", Arrays.asList(
                    new Category(21L"搬家"),
                    new Category(22L"货运")
            ))
    );

    @Data
    @AllArgsConstructor
    static class Business {
        private long id;
        private String name;
        private List<Category> categoryList;
    }

    @Data
    @AllArgsConstructor
    static class Category {
        private long id;
        private String name;
    }
}

2.常用惰性求值方法

  • map:一对一映射

businessList.stream()
        .map(Business::getName)
        .forEach(System.out::println);
//输出:
//家政
//速运
  • flatMap:一对多映射

businessList.stream()
        .flatMap(business -> business.getCategoryList().stream())
        .forEach(System.out::println);
//输出:
//Test.Category(id=11, name=保洁)
//Test.Category(id=12, name=保姆)
//Test.Category(id=21, name=搬家)
//Test.Category(id=22, name=货运)
  • filter:过滤

businessList.stream()
        .filter(business -> business.getId() > 1)
        .forEach(System.out::println);
//输出:
//Test.Business(id=2, name=速运, categoryList=[Test.Category(id=21, name=搬家), Test.Category(id=22, name=货运)])

3.常用及早求值方法

  • forEach:对每个元素执行指定操作(演示略)

  • reduce:归约

long sumOfCategoryId = businessList.stream()
        .flatMap(business -> business.getCategoryList().stream())
        .map(Category::getId)
        .reduce((x, y) -> x + y).orElse(0L);
System.out.println(sumOfCategoryId);
//输出:66
  • collect:收集

List categoryIdList = businessList.stream()
        .flatMap(business -> business.getCategoryList().stream())
        .map(Category::getId)
        .collect(Collectors.toList());
System.out.println(categoryIdList);
//输出:[11, 12, 21, 22]

5.Collector

  • Collector(收集器) 是一种通用的,从流生成复杂值的结构。

  • 标准类库java.util.stream.Collectors,已经提供了一些常用的收集器

  • 如果标准类库中的收集器无法满足需求,也可 定制 一个收集器。

  • 接下来先对标准库中的收集器做简要介绍,然后再说明如何定制收集器。

1.转换成其他集合

  • toList(),toSet(),toCollection()

//当希望使用集合对象/实现来收集值时,可以使用toCollection
TreeSet rst = businessList.stream()
        .map(Business::getName)
        .collect(Collectors.toCollection(TreeSet::new));
System.out.println(rst);
//输出:[家政, 速运]

2.转换成值

  • 按某种特定顺序生成一个值:maxBy(),minBy()

//获取id值最大的业务线
Optional<Business> rst = businessList.stream()
        .collect(Collectors.maxBy(Comparator.comparing(Business::getId)));
        //等价于 .max(Comparator.comparing(Business::getId));
System.out.println(rst.map(Business::getId).orElse(0L));
//输出:2
  • 实现一些常用的数值计算:averagingXX(),summingXX()

//求所有业务线的id均值
Double rst = businessList.stream()
        .collect(Collectors.averagingDouble(Business::getId));
System.out.println(rst);
//输出:1.5

3.数据分块

  • 将一个流根据指定的Predicate对象,分解成两个集合

    图片.png-66.9kB
//根据业务线id是否大于1分块
Map<Boolean, List<Business>> rst = businessList.stream()
        .peek(business -> business.setCategoryList(null))
        .collect(Collectors.partitioningBy(business -> business.getId() > 1));
System.out.println(rst);
//输出:{false=[Test.Business(id=1, name=家政, categoryList=null)], true=[Test.Business(id=2, name=速运, categoryList=null)]}

4.数据分组

  • 数据分开只能将数据分成两部分,而数据分组不受此限制,可以更自然地分割数据(类似与SQL中的 group by 操作)

    Java8中的函数式编程
    图片.png-61.2kB
    @Data
    @AllArgsConstructor
    static class Custom {
        private long id;
        private String name;
        private String city;
    }

    public static void main(String[] args) {
        List<Custom> customList = Arrays.asList(
                new Custom(1L"张三""北京"),
                new Custom(2L"李四""北京"),
                new Custom(3L"tom""纽约"),
                new Custom(4L"jerry""纽约"),
                new Custom(5L"thomas""纽约")
        );

        //根据城市分组
        Map<String, List<Custom>> rst = customList.stream()
                .collect(Collectors.groupingBy(Custom::getCity));
        System.out.println(rst);
    }
//输出:{纽约=[Test.Custom(id=3, name=tom, city=纽约), Test.Custom(id=4, name=jerry, city=纽约), Test.Custom(id=5, name=thomas, city=纽约)], 北京=[Test.Custom(id=1, name=张三, city=北京), Test.Custom(id=2, name=李四, city=北京)]}

5.字符串

  • 通过指定 分隔符、前缀、后缀 的方式生成一个格式化后的字符串:joining()

String rst = businessList.stream()
        .map(Business::getName)
        .collect(Collectors.joining(",""[""]"));
System.out.println(rst);
//输出:[家政,速运]

6.组合收集器

  • 组合收集器分为 主收集器 和 下游收集器,主收集器会用到下游收集器,下游收集器是用来生成最终values的 配方。

List<Custom> customList = Arrays.asList(
        new Custom(1L"张三""北京"),
        new Custom(2L"李四""北京"),
        new Custom(3L"tom""纽约"),
        new Custom(4L"jerry""纽约"),
        new Custom(5L"thomas""纽约")
);

Map<String, List<Long>> rst = customList.stream()
        .collect(Collectors.groupingBy
                (Custom::getCity,
                        Collectors.mapping(Custom::getId, Collectors.toList())));
System.out.println(rst);
//输出:{纽约=[3, 4, 5], 北京=[1, 2]}

7.定制收集器

  • 收集器接口的方法介绍

  1. supplier():用来创建容器的工厂方法,和reduce操作的第一个参数类似,是后续操作的 初值。

    Java8中的函数式编程
    图片.png-38.2kB
  2. accumulator():该方法的作用和reduce操作的第二个参数一样,用来 结合 之前操作的结果和当前值,生成并返回新的值。

    Java8中的函数式编程
    图片.png-40kB
  3. combiner():如果有多个容器,会通过该方法 合并为一个容器。

    图片.png-38.7kB
  4. finisher():对容器进行 转换 以得到预期的结果值。

    finisher.png-35.2kB
  • 定制一个字符串拼接收集器

    public static void main(String[] args) {
        List<Custom> customList = Arrays.asList(
                new Custom(1L"张三""北京"),
                new Custom(2L"李四""北京"),
                new Custom(3L"tom""纽约"),
                new Custom(4L"jerry""纽约"),
                new Custom(5L"thomas""纽约")
        );

        String rst = customList.parallelStream()
                .map(Custom::getName)
                .collect(new MyStrCollector());

        System.out.println(rst);
        //输出:[张三,李四,tom,jerry,thomas]
    }

    //泛型含义:<待收集元素的类型,累加器的类型,最终结果的类型>
    static class MyStrCollector implements Collector<StringStringBuilderString{

        private final String prefix = "[";
        private final String separator = ",";
        private final String suffix = "]";

        @Override
        public Supplier<StringBuilder> supplier() {
            return StringBuilder::new;
        }

        @Override
        public BiConsumer<StringBuilder, String> accumulator() {
            return (stringBuilder, item) -> {
                stringBuilder.append(separator).append(item);
            };
        }

        @Override
        public BinaryOperator<StringBuilder> combiner() {
            return StringBuilder::append;
        }

        @Override
        public Function<StringBuilder, String> finisher() {
            return (stringBuilder -> prefix + stringBuilder.toString().substring(1) + suffix);
        }

        @Override
        public Set<Characteristics> characteristics() {
            return Collections.emptySet();
        }
    }

end

图片来自于:《Java8函数式编程》

以上是关于Java8中的函数式编程的主要内容,如果未能解决你的问题,请参考以下文章

《On Java 8》中文版 第十三章 函数式编程

java8中的Stream

java8中的Stream

《Java8实战》读书笔记12:函数式编程

《Java8实战》读书笔记12:函数式编程

Java8函数式编程