Java8再学习

声明

  • 以下内容来自《Java8 In Action》

介绍

  • 2014年3月 发布Java8

通过行为参数化传递代码

  • 行为参数化是可以帮助你处理频繁变更的需求的一种软件开发模式。
    • 可以将代码块作为参数传递给一个方法,稍后再去执行它。这样,方法的行为就基于代码块被参数化了。

应对不断变化的需求

  • 问题:苹果筛选(颜色、重量)
  • 编写一个代码之后,尝试将其抽象化

行为参数化

  • 对选择的标准建模,根据需要考虑苹果的某些属性(绿色?重量大于150g吗?)来返回一个boolean值。返回一个boolean值的函数成为谓词(Predicate)。
  • “策略设计模式”它让你定义一族算法,把它们封装起来(称为“策略”),然后在运行时选择一个算法。
  • 行为参数化:让方法接受多种行为(或战略)作为参数,并在内部使用,来完成不同的行为。
  • 行为参数化的好处在于你可以把迭代要筛选的集合的逻辑与对集合中每个元素应用的行为区分开来。这样你可以重复使用同一个方法,给它不同的行为来达到不同的目的。
  • 匿名类:同时声明和实例化一个类。

Lambda表达式

简单理解

  • Lambda表达式可以简洁地表示可传递的匿名函数的一种方式

    • 没有名称
    • 有参数列表、函数主体、返回类型
    • 可能还有一个可以抛出异常的列表
  • 匿名:没有明确的名称

  • 函数:Lambda函数不像方法那样属于某个特定的类

  • 传递:可以作为参数传递给方法或存储在变量中

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

Lambda表达式组成

  • 参数列表
  • 箭头:->
  • Lambda主体
  • (parameters) -> expression
  • (parameters) -> { statements; }
    • statements中可以显示调用return语句

应用场景

函数式接口
  • 函数式接口 就是只定义一个抽象方法的接口
    • 有两个抽象方法就不是函数式接口了
    • 可以用@FunctionalInterface注解标注
函数描述符
  • 函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符
  • Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法,Lambda表达式的签名要和函数式接口的抽象方法一样。

使用函数式接口

  • Java API中的函数式接口:Comparable、Runnable和Callable接口
  • Java8的java.util.function包下引入了几个新的函数式接口:Predicate、Consumer、Function
Predicate
  • java.util.function.Predicate 接口定义了一个名叫 test 的抽象方法,它接受泛型T 对象,并返回一个 boolean 。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
Consumer
  • java.util.function.Consumer 定义了一个名叫 accept 的抽象方法,它接受泛型 T的对象,没有返回( void )。
@FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
Function
  • java.util.function.Function<T, R> 接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}

方法引用

  • 方法引用就是让你根据已有的方法实现来创建Lambda表达式。
  • 当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法
  • 方法引用就是替代那些转发参数的Lambda表达式的语法糖。
方法引用的分类
  • (1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
  • (2) 指 向 任 意 类 型 实 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 写 作
    String::length)。
  • (3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction
    用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensive-
    Transaction::getValue)。

案例分析

package cn.iponkan.lambda;

import java.math.BigDecimal;

/**
 * 定义实体类
 *
 * @author dongtangqiang
 */
public class Apple {
    /**
     * 颜色
     */
    private String color;
    /**
     * 重量
     */
    private BigDecimal weight;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public BigDecimal getWeight() {
        return weight;
    }

    public void setWeight(BigDecimal weight) {
        this.weight = weight;
    }
}

package cn.iponkan.lambda;

import java.util.Comparator;

/**
 * 实现比较器接口
 *
 * @author dongtangqiang
 */
public class AppleComparator implements Comparator<Apple> {
    @Override
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }
}

package cn.iponkan.lambda;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import static java.util.Comparator.comparing;

/**
 * Lambda表达式测试
 *
 * @author dongtangqiang
 */
public class TestLambda {
    public static void main(String[] args) {
        List<Apple> inventory = new ArrayList<>();
        increaseInventory(inventory);

        // 实现对库存的排序
        // ①定义类实现比较器接口
        inventory.sort(new AppleComparator());

        // ②使用匿名类
        inventory.sort(new Comparator<Apple>() {
            @Override
            public int compare(Apple a1, Apple a2) {
                return a1.getWeight().compareTo(a2.getWeight());
            }
        });

        // ③使用Lambda表达式
        inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

        // Comparator具有一个叫作comparing的静态辅助方法,
        //它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象
        inventory.sort(comparing((a) -> a.getWeight()));

        // ④使用方法引用
        inventory.sort(comparing(Apple::getWeight));
    }

    private static void increaseInventory(List<Apple> inventory) {
        Apple a1 = new Apple();
        a1.setColor("red");
        a1.setWeight(BigDecimal.valueOf(120));
        inventory.add(a1);

        Apple a2 = new Apple();
        a2.setColor("green");
        a2.setWeight(BigDecimal.valueOf(160));
        inventory.add(a2);

        Apple a3 = new Apple();
        a3.setColor("red");
        a3.setWeight(BigDecimal.valueOf(155));
        inventory.add(a3);
    }
}

流的概念

  • 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不
    是临时编写一个实现)。

  • Java 8中的集合支持一个新的stream方法,它会返回一个流(接口定义在java.util.stream.Stream里)。

  • “从支持数据处理操作的源生成的元素序列”。

    • 元素序列——就像集合一样,流也提供了一个接口,**可以访问特定元素类型的一组有序值。**因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。
    • 源——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
    • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。
  • 流的两个特点

    • 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
    • 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。
  • 流只能遍历一次

  • 流是内部迭代,不需要像for-each,或使用iterator这样来显示迭代操作。内部迭代时,项目可以透明地并行处理,或者用更优化的顺序进行处理。

流操作

  • 可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。
中间操作
  • filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
终端操作
  • 终端操作会从流的流水线生成结果。
流的使用一般包括三件事
  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。

使用流

筛选和切片
  • 使用谓词筛选

    • Streams接口支持filter方法(你现在应该很熟悉了)。该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
  • 筛选各异的元素

    • 流还支持一个叫作distinct的方法,它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。
  • 截短流

    • 流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。
  • 跳过元素

    • 流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。
映射
  • 对流中每一个元素应用函数

    • 流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。
  • 流的扁平化

    • [“Hello”,“World”] 到 [“H”,“e”,“l”, “o”,“W”,“r”,“d”]
    • flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。
 // 流的扁平化
        List<String> words = Arrays.asList("Hello", "World");
        List<String> uniqueCharacters = words.stream()//
                .map(w -> w.split(""))//
                .flatMap(Arrays::stream)
                .distinct()
                .collect(toList());
查找和匹配
  • 检查谓词是否至少匹配一个元素
    • anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。
    • anyMatch方法返回一个boolean,因此是一个终端操作。
menu.stream().anyMatch(Dish::isVegetarian)
  • 检查谓词是否匹配所有元素
    • 看流中的元素是否都能匹配给定的谓词
boolean isHealthy = menu.stream()
.allMatch(d -> d.getCalories() < 1000);
  • noneMatch:确保流中没有任何元素与给定的谓词匹配
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories()  >= 1000);
  • findAny方法将返回当前流中的任意元素

  • findFirst找到第一个元素

归约
  • 如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,比如“计算菜单中的总卡路里”或“菜单中卡路里最高的菜是一个。此类查询需要将流中所有元素反复结合起来,得到一个值比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

// 使用方法引用
int sum = numbers.stream().reduce(0, Integer::sum);

  • reduce接受两个参数:

    • 一个初始值,这里是0;
    • 一个BinaryOperator来将两个元素结合起来产生一个值,这里我们用的是lambda (a, b) -> a + b。(一个Lambda来把两个流元素结合起来并产生一个新值)
  • 最大值和最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> max = numbers.stream().reduce(Integer::min);
  • map-reduce模式
    • 统计有多少个菜
    Integer count = menu.stream()//
                .map(d -> 1)// 把每一个元素映射成1
                .reduce(0, Integer::sum);//统计
                
    // 或者
    long count = menu.stream().count();

数值流

原始类型流特化
  • IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。

  • 映射到数值流

int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
  • 转换回对象流
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
数值范围
  • range()不包含末尾值
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);

构建流

由值创建流
  • 使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
由数组创建流
  • 使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
由文件生成流
  • Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。
由函数生成流:创建无限流
  • Stream API提供了两个静态方法来从函数生成流Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);

Stream.generate(Math::random) //Supplier<T>类型的Lambda提供新的值
.limit(5)
.forEach(System.out::println);

案例分析

package cn.iponkan.stream;

/**
 * 实体类定义
 *
 * @author dongtangqiang
 */
public class Dish {
    private final String name;
    /**
     * 荤素
     */
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    @Override
    public String toString() {
        return name;
    }

    public enum Type {MEAT, FISH, OTHER}
}

package cn.iponkan.stream;

import java.util.Arrays;
import java.util.List;

import static java.util.stream.Collectors.toList;

/**
 * @author dongtangqiang
 */
public class TestStream {

    public static void main(String[] args) {
        List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT),
                new Dish("beef", false, 700, Dish.Type.MEAT),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER),
                new Dish("rice", true, 350, Dish.Type.OTHER),
                new Dish("season fruit", true, 120, Dish.Type.OTHER),
                new Dish("pizza", true, 550, Dish.Type.OTHER),
                new Dish("prawns", false, 300, Dish.Type.FISH),
                new Dish("salmon", false, 450, Dish.Type.FISH));

        // 流的使用初体验
        List<String> threeHighCaloricDishNames = menu.stream()// 建立流水线
                .filter(d -> d.getCalories() > 300) //选取高热量菜肴
                .map(Dish::getName)//获取菜名
                .limit(3)//只取前三条
                .collect(toList());//将结果保存到集合中
        System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken]

        // 用流筛选前两个荤菜
        List<Dish> twoNotVegetarianDishList = menu.stream()//
                .filter(d -> d.getType() == Dish.Type.MEAT) //
                .limit(2)//
                .collect(toList());

        // 获取菜肴的名称:
        List<Integer> dishNames = menu.stream()//
                .map(Dish::getName)// 返回 Stream<String>
                .map(String::length)//
                .collect(toList());
    }
}

用流收集数据

收集器

  • Collector接口中方法的实现决定了如何对流执行归约操作。

预定义收集器

  • 将流元素规约和汇总为一个值(将流项目重组成集合时,一般会使用收集器(Stream方法collect的参数)。)
  • 元素分组
  • 元素分区

规约和汇总

  • counting收集器
long howManyDishes = menu.stream().collect(Collectors.counting());
查找流中的最大值和最小值
  • Collectors.maxBy和Collectors.minBy,来计算流中的最大或最小值。
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
汇总
  • Collectors.summingInt、Collectors.summingLong和Collectors.summingDouble
  • Collectors.averagingInt,连同对应的averagingLong和
    averagingDouble可以计算数值的平均数
  • summarizingInt、summarizingLong和summarizingDouble可以计算总和、平均值、最大值和最小值
// 求出菜单列表的总热量
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

IntSummaryStatistics menuStatistics =
menu.stream().collect(summarizingInt(Dish::getCalories));
连接字符串
  • joining工厂方法返回的收集器会把对流中每一个对象应用toString方法得到的所有字符串连接成一个字符串。
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
广义的规约汇总
  • 所有收集器,都是一个可以用reducing工厂方法定义的归约过程
    的特殊情况而已。Collectors.reducing工厂方法是所有这些特殊情况的一般化。
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
  • reducing方法它需要三个参数

    • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值和而言0是一个合适的值。
    • 第二个参数就是一个函数,将菜肴转换成一个表示其所含热量的int。
    • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。
  • reducing方法可以传单个参数

    • 单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。

分组

  • groupingBy方法传递了一个Function(以方法引用的形式),我们把这个Function叫作分类函数,因为它用来把流中的元素分成不
    同的组。
  • 分组操作的结果是一个Map,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。
多级分组
  • 要实现多级分组,我们可以使用一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。

  • 把收集器的结果转换为另一种类型

    • 可以使用Collectors.collectingAndThen工厂方法返回的收集器。这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。
按子组收集数据
  • 把收集器的结果转换为另一种类型

    • 你可以使用Collectors.collectingAndThen工厂方法返回的收集器
  • 与groupingBy联合使用的其他收集器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值