Java8实战-用流收集数据

简介: Java8实战-用流收集数据

用流收集数据

我们在前一章中学到,流可以用类似于数据库的操作帮助你处理集合。你可以把Java 8的流看作花哨又懒惰的数据集迭代器。它们支持两种类型的操作:中间操作(如 filter 或 map )和终端操作(如 count 、 findFirst 、 forEach 和 reduce )。中间操作可以链接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果,例如返回流中的最大元素。它们通常可以通过优化流水线来缩短计算时间。

我们已经在前面用过了 collect 终端操作了,当时主要是用来把 Stream 中所有的元素结合成一个 List 。在本章中,你会发现 collect 是一个归约操作,就像 reduce 一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector 接口来定义的,因此区分 Collection 、 Collector 和 collect 是很重要的。

现在,我们来看一个例子,看看我们用collect和收集器能做什么。

  1. 对一个交易列表按照货币分组,获得该货币所有的交易总额和(返回一个 Map<Currency,Integer> )。
  2. 将交易列表分成两组:贵的和不贵的(返回一个 Map<Boolean, List> )。
  3. 创建多级分组,比如按城市对交易分组,然后进一步按照贵或不贵分组(返回一个 Map<Boolean, List> )。

我们首先来看一个利用收集器的例子,想象一下,你有一个Transaction构成的List,并且想按照名义货币进行分组。在没有Lambda的Java里,哪怕像这种简单的用例实现起来都很啰嗦,就像下面这样:

// 建立累积交易分组的Map
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>(16);
// 迭代 Transaction 的 List
for (Transaction transaction : transactions) {
    // 提取 Transaction的货币
    Currency currency = transaction.getCurrency();
    List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
    // 如果分组 Map 中没有这种货币的条目,就创建一个
    if (transactionsForCurrency == null) {
        transactionsForCurrency = new ArrayList<>();
        transactionsByCurrencies.put(currency, transactionsForCurrency);
    }
    // 将当前遍历的 Transaction加入同一货币的 Transaction 的 List
    transactionsForCurrency.add(transaction);
}
System.out.println(transactionsByCurrencies);
复制代码

如果你是一位经验丰富的Java程序员,写这种东西可能挺顺手的,不过你必须承认,做这么简单的一件事就得写很多代码。更糟糕的是,读起来比写起来更费劲!代码的目的并不容易看出来,尽管换作白话的话是很直截了当的:“把列表中的交易按货币分组。”你在本章中会学到,用Stream 中 collect 方法的一个更通用的 Collector 参数,你就可以用一句话实现完全相同的结果,而用不着使用上一章那个 toList 的特殊情况了:

Map<Currency, List<Transaction>> transactionsByCurrencies =
    transactions.stream().collect(groupingBy(Transaction::getCurrency));
复制代码

这一比差得还真多,对吧?

收集器简介

前一个例子清楚地展示了函数式编程相对于指令式编程的一个主要优势:你只需指出希望的结果——“做什么”,而不用操心执行的步骤——“如何做”。在上一个例子里,传递给 collect方法的参数是 Collector 接口的一个实现,也就是给 Stream 中元素做汇总的方法。上一章里的toList 只是说“按顺序给每个元素生成一个列表”;在本例中, groupingBy 说的是“生成一个Map ,它的键是(货币)桶,值则是桶中那些元素的列表”。要是做多级分组,指令式和函数式之间的区别就会更加明显:由于需要好多层嵌套循环和条件,指令式代码很快就变得更难阅读、更难维护、更难修改。

收集器用作高级归约

刚刚的结论又引出了优秀的函数式API设计的另一个好处:更易复合和重用。收集器非常有用,因为用它可以简洁而灵活地定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)。一般来说, Collector 会对元素应用一个转换函数(很多时候是不体现任何效果的恒等转换,例如 toList ),并将结果累积在一个数据结构中,从而产生这一过程的最终输出。例如,在前面所示的交易分组的例子中,转换函数提取了每笔交易的货币,随后使用货币作为键,将交易本身累积在生成的 Map 中。

归约和汇总

为了说明从 Collectors 工厂类中能创建出多少种收集器实例,我们重用一下前一章的例子:包含一张佳肴列表的菜单!就像你刚刚看到的,在需要将流项目重组成集合时,一般会使用收集器( Stream 方法 collect的参数)。再宽泛一点来说,但凡要把流中所有的项目合并成一个结果时就可以用。这个结果可以是任何类型,可以复杂如代表一棵树的多级映射,或是简单如一个整数——也许代表了菜单的热量总和。

我们先来举一个简单的例子,利用 counting 工厂方法返回的收集器,数一数菜单里有多少 种菜:

long howManyDishes = menu.stream().collect(Collectors.counting());
复制代码

这还可以写得更为直接:

long howManyDishes = menu.stream().count();
复制代码

counting 收集器在和其他收集器联合使用的时候特别有用,后面会谈到这一点。

查找流中的最大值和最小值

假设你想要找出菜单中热量最高的菜。你可以使用两个收集器, Collectors.maxBy和Collectors.minBy ,来计算流中的最大或最小值。这两个收集器接收一个 Comparator 参数来 比较流中的元素。你可以创建一个 Comparator来根据所含热量对菜肴进行比较,并把它传递给 Collectors.maxBy :

List<Dish> menu =  Dish.MENU;
Comparator<Dish> dishCaloriesComparator =
        Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
        menu.stream().max(dishCaloriesComparator);
System.out.println(mostCalorieDish.get());
复制代码

你可能在想 Optional 是怎么回事。要回答这个问题,我们需要问“要是 menu 为空怎么办”。那就没有要返回的菜了!Java 8引入了 Optional ,它是一个容器,可以包含也可以不包含值。这里它完美地代表了可能也可能不返回菜肴的情况。

另一个常见的返回单个值的归约操作是对流中对象的一个数值字段求和。或者你可能想要求平均数。这种操作被称为汇总操作。让我们来看看如何使用收集器来表达汇总操作。

汇总

Collectors 类专门为汇总提供了一个工厂方法: Collectors.summingInt 。它可接受一个把对象映射为求和所需 int 的函数,并返回一个收集器;该收集器在传递给普通的 collect 方法后即执行我们需要的汇总操作。举个例子来说,你可以这样求出菜单列表的总热量:

List<Dish> menu =  Dish.MENU;
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
复制代码

除了Collectors.summingInt,还有Collectors.summingLong 和Collectors.summingDouble 方法的作用完全一样,可以用于求和字段为 long 或 double 的情况。

但汇总不仅仅是求和;还有 Collectors.averagingInt ,连同对应的 averagingLong 和 averagingDouble 可以计算数值的平均数:

List<Dish> menu =  Dish.MENU;
double avgCalories =
                menu.stream().collect(averagingInt(Dish::getCalories));
复制代码

到目前为止,你已经看到了如何使用收集器来给流中的元素计数,找到这些元素数值属性的最大值和最小值,以及计算其总和和平均值。不过很多时候,你可能想要得到两个或更多这样的结果,而且你希望只需一次操作就可以完成。在这种情况下,你可以使用 summarizingInt 工厂方法返回的收集器。例如,通过一次 summarizing 操作你可以就数出菜单中元素的个数,并得到菜肴热量总和、平均值、最大值和最小值:

List<Dish> menu =  Dish.MENU;
IntSummaryStatistics menuStatistics =
        menu.stream().collect(summarizingInt(Dish::getCalories));
System.out.println(menuStatistics.getMax());
System.out.println(menuStatistics.getAverage());
System.out.println(menuStatistics.getMin());
System.out.println(menuStatistics.getCount());
System.out.println(menuStatistics.getSum());
复制代码

复制代码

同样,相应的 summarizingLong 和 summarizingDouble 工厂方法有相关的LongSummaryStatistics 和 DoubleSummaryStatistics 类型,适用于收集的属性是原始类型 long 或double 的情况。

连接字符串

joining 工厂方法返回的收集器会把对流中每一个对象应用 toString 方法得到的所有字符 串连接成一个字符串。这意味着你把菜单中所有菜肴的名称连接起来,如下所示:

String shortMenu = menu.stream().map(Dish::getName).collect(joining());
复制代码

请注意, joining 在内部使用了 StringBuilder 来把生成的字符串逐个追加起来。结果:

porkbeefchickenfrench friesriceseason fruitpizzaprawnssalmon
复制代码

但该字符串的可读性并不好。幸好, joining 工厂方法有一个重载版本可以接受元素之间的 分界符,这样你就可以得到一个逗号分隔的菜肴名称列表:

String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
复制代码

结果:

pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
复制代码

到目前为止,我们已经探讨了各种将流归约到一个值的收集器。在下一节中,我们会展示为什么所有这种形式的归约过程,其实都是 Collectors.reducing 工厂方法提供的更广义归约收集器的特殊情况。

广义的归约汇总

事实上,我们已经讨论的所有收集器,都是一个可以用 reducing 工厂方法定义的归约过程的特殊情况而已。 Collectors.reducing 工厂方法是所有这些特殊情况的一般化。可以说,先前讨论的案例仅仅是为了方便程序员而已。(但是,请记得方便程序员和可读性是头等大事!)例如,可以用 reducing 方法创建的收集器来计算你菜单的总热量,如下所示:

List<Dish> menu =  Dish.MENU;
int totalCalories = menu.stream().collect(reducing(
        0, Dish::getCalories, (i, j) -> i + j));
System.out.println(totalCalories);
复制代码

它需要三个参数:

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

同样,你可以使用下面这样单参数形式的 reducing 来找到热量最高的菜,如下所示:

Optional<Dish> mostCalorieDish =
            menu.stream().collect(reducing(
                    (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
复制代码

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

收集框架的灵活性:以不同的方法执行同样的操作

你还可以进一步简化前面使用 reducing 收集器的求和例子——引用 Integer 类的 sum 方法,而不用去写一个表达同一操作的Lambda表达式。这会得到以下程序:

int totalCalories2 = menu.stream()
                .collect(reducing(0, // 初始值
                        Dish::getCalories, // 转换函数
                        Integer::sum)); // 积累函数
复制代码

使用语法糖,能帮助我们简化一部分代码。

还有另外一种方法不使用收集器也能执行相同操作——将菜肴流映射为每一道菜的热量,然后用前一个版本中使用的方法引用来归约得到的流:

int totalCalories =
            menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
复制代码

请注意,就像流的任何单参数 reduce 操作一样, reduce(Integer::sum) 返回的不是 int而是 Optional ,以便在空流的情况下安全地执行归约操作。然后你只需用 Optional对象中的 get 方法来提取里面的值就行了。请注意,在这种情况下使用 get 方法是安全的,只是因为你已经确定菜肴流不为空。一般来说,使用允许提供默认值的方法,如 orElse 或 orElseGet来解开Optional中包含的值更为安全。最后,更简洁的方法是把流映射到一个 IntStream ,然后调用 sum 方法,你也可以得到相同的结果:

int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
复制代码

根据情况选择最佳解决方案

这再次说明了,函数式编程(特别是Java 8的 Collections 框架中加入的基于函数式风格原理设计的新API)通常提供了多种方法来执行同一个操作。这个例子还说明,收集器在某种程度上比Stream 接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。在《Java8实战》中的的建议是,尽可能为手头的问题探索不同的解决方案,但在通用的方案里面,始终选择最专门化的一个。无论是从可读性还是性能上看,这一般都是最好的决定。例如,要计菜单的总热量,我们更倾向于最后一个解决方案(使用 IntStream ),因为它最简明,也很可能最易读。同时,它也是性能最好的一个,因为 IntStream 可以让我们避免自动拆箱操作,也就是从Integer到int的隐式转换,它在这里毫无用处。

分组

一个常见的数据库操作是根据一个或多个属性对集合中的项目进行分组。就像前面讲到按货币对交易进行分组的例子一样,如果用指令式风格来实现的话,这个操作可能会很麻烦、啰嗦而且容易出错。但是,如果用Java 8所推崇的函数式风格来重写的话,就很容易转化为一个非常容易看懂的语句。我们来看看这个功能的第二个例子:假设你要把菜单中的菜按照类型进行分类,有肉的放一组,有鱼的放一组,其他的都放另一组。用 Collectors.groupingBy 工厂方法返回的收集器就可以轻松地完成这项任务,如下所示:

Map<Dish.Type, List<Dish>> dishesByType =
                            menu.stream().collect(groupingBy(Dish::getType));
复制代码

其结果是下面的 Map:

{OTHER=[Dish{name='french fries'}, Dish{name='rice'}, Dish{name='season fruit'}, Dish{name='pizza'}], MEAT=[Dish{name='pork'}, Dish{name='beef'}, Dish{name='chicken'}], FISH=[Dish{name='prawns'}, Dish{name='salmon'}]}
复制代码

这里,你给 groupingBy 方法传递了一个 Function (以方法引用的形式),它提取了流中每一道 Dish 的 Dish.Type 。我们把这个 Function 叫作分类函数,因为它用来把流中的元素分成不同的组。分组操作的结果是一个 Map ,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。在菜单分类的例子中,键就是菜的类型,值就是包含所有对应类型的菜肴的列表。

但是,分类函数不一定像方法引用那样可用,因为你想用以分类的条件可能比简单的属性访问器要复杂。例如,你可能想把热量不到400卡路里的菜划分为“低热量”(diet),热量400到700卡路里的菜划为“普通”(normal),高于700卡路里的划为“高热量”(fat)。由于 Dish 类的作者没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式:

复制代码

public enum CaloricLevel {
    /**
        * 卡路里等级
        */
    DIET, NORMAL, FAT
}

 Map<Dish.CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
                groupingBy(dish -> {
                    if (dish.getCalories() <= 400) {
                        return Dish.CaloricLevel.DIET;
                    } else if (dish.getCalories() <= 700) {
                        return Dish.CaloricLevel.NORMAL;
                    } else {
                        return Dish.CaloricLevel.FAT;
                    }
                }));
复制代码

多级分组

要实现多级分组,我们可以使用一个由双参数版本的 Collectors.groupingBy 工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受 collector 类型的第二个参数。那么要进行二级分组的话,我们可以把一个内层 groupingBy 传递给外层groupingBy ,并定义一个为流中项目分类的二级标准。

Map<Dish.Type, Map<Dish.CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel =
                menu.stream().collect(
                        groupingBy(Dish::getType,
                                groupingBy(dish -> {
                                    if (dish.getCalories() <= 400) {
                                        return Dish.CaloricLevel.DIET;
                                    } else if (dish.getCalories() <= 700) {
                                        return Dish.CaloricLevel.NORMAL;
                                    } else {
                                        return Dish.CaloricLevel.FAT;
                                    }
                                })
                        )
                );
复制代码

这个二级分组的结果就是像下面这样的两级 Map :

{OTHER={DIET=[Dish{name='rice'}, Dish{name='season fruit'}], NORMAL=[Dish{name='french fries'}, Dish{name='pizza'}]}, MEAT={DIET=[Dish{name='chicken'}], FAT=[Dish{name='pork'}], NORMAL=[Dish{name='beef'}]}, FISH={DIET=[Dish{name='prawns'}], NORMAL=[Dish{name='salmon'}]}}
复制代码

这里的外层 Map 的键就是第一级分类函数生成的值:“fish, meat, other”,而这个 Map 的值又是一个 Map ,键是二级分类函数生成的值:“normal, diet, fat”。最后,第二级 map 的值是流中元素构成的 List ,是分别应用第一级和第二级分类函数所得到的对应第一级和第二级键的值:“salmon、pizza…” 这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结构的n级Map 。

一般来说,把 groupingBy 看作“桶”比较容易明白。第一个 groupingBy 给每个键建立了一个桶。然后再用下游的收集器去收集每个桶中的元素,以此得到n级分组。

按子组收集数据

在上一节中,我们看到可以把第二个 groupingBy 收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个 groupingBy 的第二个收集器可以是任何类型,而不一定是另一个 groupingBy 。例如,要数一数菜单中每类菜有多少个,可以传递 counting 收集器作为groupingBy 收集器的第二个参数:

 Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
复制代码

其结果是下面的 Map :

{OTHER=4, MEAT=3, FISH=2}
复制代码

还要注意,普通的单参数 groupingBy(f) (其中 f 是分类函数)实际上是 groupingBy(f,toList()) 的简便写法。 再举一个例子,你可以把前面用于查找菜单中热量最高的菜肴的收集器改一改,按照菜的类型分类:

Map<Dish.Type, Optional<Dish>> mostCaloricByType =
                menu.stream()
                        .collect(groupingBy(Dish::getType,
                                maxBy(comparingInt(Dish::getCalories))));
复制代码

这个分组的结果显然是一个 map ,以 Dish 的类型作为键,以包装了该类型中热量最高的 Dish的 Optional 作为值:

{OTHER=Optional[Dish{name='pizza'}], MEAT=Optional[Dish{name='pork'}], FISH=Optional[Dish{name='salmon'}]}
复制代码

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

因为分组操作的 Map 结果中的每个值上包装的 Optional 没什么用,所以你可能想要把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen 工厂方法返回的收集器,如下所示。

查找每个子组中热量最高的 Dish:

List<Dish> menu = Dish.MENU;
        Map<Dish.Type, Dish> mostCaloricByType =
                menu.stream()
                        .collect(groupingBy(Dish::getType, // 分类函数
                                collectingAndThen(
                                        maxBy(comparingInt(Dish::getCalories)), // 包装后的收集器
                                        Optional::get))); // 转换函数
复制代码

这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装, collect 操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用 maxBy 建立的那个,而转换函数 Optional::get 则把返回的 Optional 中的值提取出来。前面已经说过,这个操作放在这里是安全的,因为 reducing收集器永远都不会返回 Optional.empty() 。其结果是下面的 Map :

{OTHER=Dish{name='pizza'}, MEAT=Dish{name='pork'}, FISH=Dish{name='salmon'}}
复制代码

把好几个收集器嵌套起来很常见,它们之间到底发生了什么可能不那么明显。从最外层开始逐层向里,注意以下几点:

  1. 收集器用虚线表示,因此 groupingBy 是最外层,根据菜肴的类型把菜单流分组,得到三个子流。
  2. groupingBy 收集器包裹着 collectingAndThen 收集器,因此分组操作得到的每个子流都用这第二个收集器做进一步归约。
  3. collectingAndThen 收集器又包裹着第三个收集器 maxBy 。
  4. 随后由归约收集器进行子流的归约操作,然后包含它的 collectingAndThen 收集器会对其结果应用 Optional:get 转换函数。
  5. 对三个子流分别执行这一过程并转换而得到的三个值,也就是各个类型中热量最高的Dish ,将成为 groupingBy 收集器返回的 Map 中与各个分类键( Dish 的类型)相关联的值。

与 groupingBy 联合使用的其他收集器的例子

一般来说,通过 groupingBy 工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。例如,你还重用求出所有菜肴热量总和的收集器,不过这次是对每一组 Dish 求和:

Map<Dish.Type, Integer> totalCaloriesByType = menu.stream()
                .collect(groupingBy(Dish::getType,
                        summingInt(Dish::getCalories)));
复制代码

然而常常和 groupingBy 联合使用的另一个收集器是 mapping 方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。我们来看一个使用这个收集器的实际例子。比方说你想要知道,对于每种类型的 Dish ,菜单中都有哪些 CaloricLevel 。我们可以把 groupingBy 和 mapping 收集器结合起来,如下所示:

Map<Dish.Type, Set<Dish.CaloricLevel>> caloricLevelsByType =
                menu.stream().collect(
                        groupingBy(Dish::getType, mapping(
                                dish -> {
                                    if (dish.getCalories() <= 400) {
                                        return Dish.CaloricLevel.DIET;
                                    } else if (dish.getCalories() <= 700) {
                                        return Dish.CaloricLevel.NORMAL;
                                    } else {
                                        return Dish.CaloricLevel.FAT;
                                    }
                                },
                                toSet())));
复制代码

传递给映射方法的转换函数将 Dish 映射成了它的CaloricLevel :生成的CaloricLevel 流传递给一个 toSet 收集器,它和 toList 类似,不过是把流中的元素累积到一个 Set 而不是 List 中,以便仅保留各不相同的值。如先前的示例所示,这个映射收集器将会收集分组函数生成的各个子流中的元素,让你得到这样的 Map 结果:

{OTHER=[DIET, NORMAL], MEAT=[DIET, FAT, NORMAL], FISH=[DIET, NORMAL]}
复制代码

由此你就可以轻松地做出选择了。如果你想吃鱼并且在减肥,那很容易找到一道菜;同样,如果你饥肠辘辘,想要很多热量的话,菜单中肉类部分就可以满足你的饕餮之欲了。请注意在上一个示例中,对于返回的 Set 是什么类型并没有任何保证。但通过使用 toCollection ,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求 HashSet :

Map<Dish.Type, Set<Dish.CaloricLevel>> caloricLevelsByType =
                menu.stream().collect(
                        groupingBy(Dish::getType, mapping(
                                dish -> {
                                    if (dish.getCalories() <= 400) {
                                        return Dish.CaloricLevel.DIET;
                                    } else if (dish.getCalories() <= 700) {
                                        return Dish.CaloricLevel.NORMAL;
                                    } else {
                                        return Dish.CaloricLevel.FAT;
                                    }
                                },
                                toCollection(HashSet::new))));
复制代码

分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数。分区函数返回一个布尔值,这意味着得到的分组 Map 的键类型是 Boolean ,于是它最多可以分为两组—— true 是一组, false 是一组。例如,如果你是素食者或是请了一位素食的朋友来共进晚餐,可能会想要把菜单按照素食和非素食分开:

Map<Boolean, List<Dish>> partitionedMenu =
                // 分区函数
                menu.stream().collect(partitioningBy(Dish::isVegetarian));
复制代码

这会返回下面的 Map :

{false=[Dish{name='pork'}, Dish{name='beef'}, Dish{name='chicken'}, Dish{name='prawns'}, Dish{name='salmon'}], 
true=[Dish{name='french fries'}, Dish{name='rice'}, Dish{name='season fruit'}, Dish{name='pizza'}]}
复制代码

那么通过 Map 中键为 true 的值,就可以找出所有的素食菜肴了:

List<Dish> vegetarianDishes = partitionedMenu.get(true);
复制代码

请注意,用同样的分区谓词,对菜单 List 创建的流作筛选,然后把结果收集到另外一个 List中也可以获得相同的结果:

List<Dish> vegetarianDishes =
                        menu.stream().filter(Dish::isVegetarian).collect(toList());
复制代码

分区的优势

分区的好处在于保留了分区函数返回 true 或 false 的两套流元素列表。在上一个例子中,要得到非素食 Dish 的 List ,你可以使用两个筛选操作来访问 partitionedMenu 这个 Map 中 false键的值:一个利用谓词,一个利用该谓词的非。而且就像你在分组中看到的, partitioningBy工厂方法有一个重载版本,可以像下面这样传递第二个收集器:

Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
                menu.stream().collect(
                        // 分区函数
                        partitioningBy(Dish::isVegetarian,
                                // 第二个收集器
                                groupingBy(Dish::getType)));
复制代码

这将产生一个二级 Map :

{false={MEAT=[Dish{name='pork'}, Dish{name='beef'}, Dish{name='chicken'}], FISH=[Dish{name='prawns'}, Dish{name='salmon'}]}, 
true={OTHER=[Dish{name='french fries'}, Dish{name='rice'}, Dish{name='season fruit'}, Dish{name='pizza'}]}}
复制代码

这里,对于分区产生的素食和非素食子流,分别按类型对菜肴分组,得到了一个二级 Map,和上面的类似。再举一个例子,你可以重用前面的代码来找到素食和非素食中热量最高的菜:

Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = menu.stream().collect(
                partitioningBy(Dish::isVegetarian, collectingAndThen(
                        maxBy(comparingInt(Dish::getCalories)),
                        Optional::get
                )));
复制代码

这将产生以下结果:

{false=Dish{name='pork'}, true=Dish{name='pizza'}}
复制代码

你可以把分区看作分组一种特殊情况。 groupingBy 和partitioningBy 收集器之间的相似之处并不止于此。

将数字按质数和非质数分区

假设你要写一个方法,它接受参数 int n,并将前n个自然数分为质数和非质数。但首先,找出能够测试某一个待测数字是否是质数的谓词会很有帮助:

private static boolean isPrime(int candidate) {
    // 产生一个自然数范围,从2开始,直至但不包括待测数
    return IntStream.range(2, candidate)
            // 如果待测数字不能被流中任何数字整除则返回 true
            .noneMatch(i -> candidate % i == 0);
}
复制代码

一个简单的优化是仅测试小于等于待测数平方根的因子:

private static boolean isPrime(int candidate) {
    int candidateRoot = (int) Math.sqrt((double) candidate);
    return IntStream.rangeClosed(2, candidateRoot)
            .noneMatch(i -> candidate % i == 0);
}
复制代码

现在最主要的一部分工作已经做好了。为了把前n个数字分为质数和非质数,只要创建一个包含这n个数的流,用刚刚写的 isPrime 方法作为谓词,再给 partitioningBy 收集器归约就好了:

复制代码

private static Map<Boolean, List<Integer>> partitionPrimes(int n) {
    return IntStream.rangeClosed(2, n).boxed()
            .collect(
                    partitioningBy(candidate -> isPrime(candidate)));
}
复制代码

现在我们已经讨论过了 Collectors 类的静态工厂方法能够创建的所有收集器,并介绍了使用它们的实际例子。

收集器接口

Collector 接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。我们已经看过了 Collector 接口中实现的许多收集器,例如 toList 或 groupingBy 。这也意味着,你可以为 Collector 接口提供自己的实现,从而自由地创建自定义归约操作。

要开始使用 Collector 接口,我们先看看本章开始时讲到的一个收集器—— toList 工厂方法,它会把流中的所有元素收集成一个 List 。我们当时说在日常工作中经常会用到这个收集器,而且它也是写起来比较直观的一个,至少理论上如此。通过仔细研究这个收集器是怎么实现的,我们可以很好地了解 Collector 接口是怎么定义的,以及它的方法所返回的函数在内部是如何为collect 方法所用的。

首先让我们在下面的列表中看看 Collector 接口的定义,它列出了接口的签名以及声明的五个方法。

public interface Collector<T, A, R> {
        Supplier<A> supplier();
        BiConsumer<A, T> accumulator();
        Function<A, R> finisher();
        BinaryOperator<A> combiner();
        Set<Characteristics> characteristics();
}
复制代码

本列表适用以下定义。

  1. T 是流中要收集的项目的泛型。
  2. A 是累加器的类型,累加器是在收集过程中用于累积部分结果的对象。
  3. R 是收集操作得到的对象(通常但并不一定是集合)的类型。

例如,你可以实现一个 ToListCollector 类,将 Stream 中的所有元素收集到一个List 里,它的签名如下:

public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
复制代码

我们很快就会澄清,这里用于累积的对象也将是收集过程的最终结果。

理解 Collector 接口声明的方法

现在我们可以一个个来分析 Collector 接口声明的五个方法了。通过分析,你会注意到,前四个方法都会返回一个会被 collect 方法调用的函数,而第五个方法 characteristics 则提供了一系列特征,也就是一个提示列表,告诉 collect 方法在执行归约操作的时候可以应用哪些优化(比如并行化)。

1. 建立新的结果容器: supplier 方法

supplier 方法必须返回一个结果为空的 Supplier ,也就是一个无参数函数,在调用时它会创建一个空的累加器实例,供数据收集过程使用。很明显,对于将累加器本身作为结果返回的收集器,比如我们的 ToListCollector ,在对空流执行操作的时候,这个空的累加器也代表了收集过程的结果。在我们的 ToListCollector 中, supplier 返回一个空的 List ,如下所示:

@Override
public Supplier<List<T>> supplier() {
    return () -> new ArrayList<>();
}
复制代码

请注意你也可以只传递一个构造函数引用:

@Override
public Supplier<List<T>> supplier() {
    return ArrayList::new;
}
复制代码

2. 将元素添加到结果容器: accumulator 方法

accumulator 方法会返回执行归约操作的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前 n-1 个项目),还有第n个元素本身。该函数将返回void ,因为累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。对于ToListCollector ,这个函数仅仅会把当前项目添加至已经遍历过的项目的列表:

@Override
public BiConsumer<List<T>, T> accumulator() {
    return (list, item) -> list.add(item);
}
复制代码

你也可以使用方法引用,这会更为简洁:

@Override
public BiConsumer<List<T>, T> accumulator() {
    return List::add;
}
复制代码

3. 对结果容器应用最终转换: finisher 方法

在遍历完流后, finisher 方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操作的最终结果。通常,就像 ToListCollector 的情况一样,累加器对象恰好符合预期的最终结果,因此无需进行转换。所以 finisher 方法只需返回 identity 函数:

@Override
public Function<List<T>, List<T>> finisher() {
    return Function.identity();
}
复制代码

这三个方法已经足以对流进行循序规约。实践中的实现细节可能还要复杂一点,一方面是应为流的延迟性质,可能在collect操作之前还需完成其他中间操作的流水线,另一方面则是理论上可能要进行并行规约。

4. 合并两个结果容器: combiner 方法

四个方法中的最后一个————combiner方法会返回一个供归约操作的使用函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并。对于toList而言,这个方法的实现非常简单,只要把从流的第二个部分收集到的项目列表加到遍历第一部分时得到的列表后面就行了:

@Override
public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    };
}
复制代码

有了这第四个方法,就可以对流进行并行归约了。它会用到Java7中引入的分支/合并框架和Spliterator抽象。

5.  characteristics 方法

最后一个方法—— characteristics 会返回一个不可变的 Characteristics 集合,它定义了收集器的行为——尤其是关于流是否可以并行归约,以及可以使用哪些优化的提示。Characteristics 是一个包含三个项目的枚举。

  1. UNORDERED ——归约结果不受流中项目的遍历和累积顺序的影响。
  2. CONCURRENT —— accumulator 函数可以从多个线程同时调用,且该收集器可以并行归约流。如果收集器没有标为 UNORDERED ,那它仅在用于无序数据源时才可以并行归约。
  3. IDENTITY_FINISH ——这表明完成器方法返回的函数是一个恒等函数,可以跳过。这种情况下,累加器对象将会直接用作归约过程的最终结果。这也意味着,将累加器 A 不加检查地转换为结果 R 是安全的。

我们迄今开发的 ToListCollector 是 IDENTITY_FINISH 的,因为用来累积流中元素的List 已经是我们要的最终结果,用不着进一步转换了,但它并不是 UNORDERED ,因为用在有序流上的时候,我们还是希望顺序能够保留在得到的 List 中。最后,它是 CONCURRENT 的,但我们刚才说过了,仅仅在背后的数据源无序时才会并行处理。

全部融合到一起

前一小节中谈到的五个方法足够我们开发自己的 ToListCollector 了。你可以把它们都融合起来,如下面的代码清单所示。

复制代码

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return List::add;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
    }
}
复制代码

请注意,这个是实现与Collections.toList()方法并不完全相同,但区别仅仅是一些小的优化。这些优化的一个主要方面是Java API所提供的收集器在需要返回空列表时使用了 Collections.emptyList() 这个单例(singleton)。这意味着它可安全地替代原生Java,来收集菜单流中的所有 Dish 的列表:

List<Dish> dishes = menuStream.collect(new ToListCollector<>());
复制代码

这个实现和标准的

List<Dish> dishes = menuStream.collect(toList());
复制代码

构造之间的其他差异在于 toList 是一个工厂,而 ToListCollector 必须用 new 来实例化。

进行自定义收集而不去实现 Collector

对于 IDENTITY_FINISH 的收集操作,还有一种方法可以得到同样的结果而无需从头实现新的 Collectors 接口。 Stream 有一个重载的 collect 方法可以接受另外三个函数—— supplier 、accumulator 和 combiner ,其语义和 Collector 接口的相应方法返回的函数完全相同。所以比如说,我们可以像下面这样把菜肴流中的项目收集到一个 List 中:

List<Dish> dishes = menuStream.collect(
                ArrayList::new,
                List::add,
                List::addAll);
复制代码

我们认为,这第二种形式虽然比前一个写法更为紧凑和简洁,却不那么易读。此外,以恰当的类来实现自己的自定义收集器有助于重用并可避免代码重复。另外值得注意的是,这第二个collect 方法不能传递任何 Characteristics ,所以它永远都是一个 IDENTITY_FINISH 和CONCURRENT 但并非 UNORDERED 的收集器。

在下一节中,我们一起来实现一个收集器的,让我们对收集器的新知识更上一层楼。你将会为一个更为复杂,但更为具体、更有说服力的用例开发自己的自定义收集器。

开发你自己的收集器以获得更好的性能

我们用 Collectors 类提供的一个方便的工厂方法创建了一个收集器,它将前n个自然数划分为质数和非质数,如下所示。

将前n个自然数按质数和非质数分区:

private static Map<Boolean, List<Integer>> partitionPrimes(int n) {
    return IntStream.rangeClosed(2, n).boxed()
            .collect(
                    partitioningBy(candidate -> isPrime(candidate)));
}
复制代码

当时,通过限制除数不超过被测试数的平方根,我们对最初的 isPrime 方法做了一些改进:

private static boolean isPrime(int candidate) {
    int candidateRoot = (int) Math.sqrt((double) candidate);
    return IntStream.rangeClosed(2, candidateRoot)
            .noneMatch(i -> candidate % i == 0);
}
复制代码

还有没有办法来获得更好的性能呢?答案是“有”,但为此你必须开发一个自定义收集器。

仅用质数做除数

一个可能的优化是仅仅看看被测试数是不是能够被质数整除。要是除数本身都不是质数就用不着测了。所以我们可以仅仅用被测试数之前的质数来测试。然而我们目前所见的预定义收集器的问题,也就是必须自己开发一个收集器的原因在于,在收集过程中是没有办法访问部分结果的。这意味着,当测试某一个数字是否是质数的时候,你没法访问目前已经找到的其他质数的列表。

假设你有这个列表,那就可以把它传给 isPrime 方法,将方法重写如下:

private static boolean isPrime(List<Integer> primes, int candidate) {
    return primes.stream().noneMatch(i -> candidate % i == 0);
}
复制代码

而且还应该应用先前的优化,仅仅用小于被测数平方根的质数来测试。因此,你需要想办法在下一个质数大于被测数平方根时立即停止测试。不幸的是,Stream API中没有这样一种方法。你可以使用 filter(p -> p <= candidateRoot) 来筛选出小于被测数平方根的质数。但 filter要处理整个流才能返回恰当的结果。如果质数和非质数的列表都非常大,这就是个问题了。你用不着这样做;你只需在质数大于被测数平方根的时候停下来就可以了。因此,我们会创建一个名为 takeWhile 的方法,给定一个排序列表和一个谓词,它会返回元素满足谓词的最长前缀:

public static <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
    int i = 0;
    for (A item : list) {
        if (!p.test(item)) {
            return list.subList(0, i);
        }
        i++;
    }
    return list;
}
复制代码

利用这个方法,你就可以优化 isPrime 方法,只用不大于被测数平方根的质数去测试了:

private static boolean isPrime(List<Integer> primes, int candidate){
    int candidateRoot = (int) Math.sqrt((double) candidate);
    return takeWhile(primes, i -> i <= candidateRoot)
            .stream()
            .noneMatch(p -> candidate % p == 0);
}
复制代码

请注意,这个 takeWhile 实现是即时的。理想情况下,我们会想要一个延迟求值的takeWhile ,这样就可以和 noneMatch 操作合并。不幸的是,这样的实现超出了本章的范围,你需要了解Stream API的实现才行。

有了这个新的 isPrime 方法在手,你就可以实现自己的自定义收集器了。首先要声明一个实现 Collector 接口的新类,然后要开发 Collector 接口所需的五个方法。

1. 第一步:定义 Collector 类的签名

让我们从类签名开始吧,记得 Collector 接口的定义是:

public interface Collector<T, A, R>
复制代码

其中 T 、 A 和 R 分别是流中元素的类型、用于累积部分结果的对象类型,以及 collect 操作最终结果的类型。这里应该收集 Integer 流,而累加器和结果类型则都是 Map<Boolean,List>,键是 true 和 false ,值则分别是质数和非质数的 List :

public class PrimeNumbersCollector implements Collector<Integer, Map<Boolean, List<Integer>>,
        Map<Boolean, List<Integer>>>
复制代码

2. 第二步:实现归约过程

接下来,你需要实现 Collector 接口中声明的五个方法。 supplier 方法会返回一个在调用时创建累加器的函数:

@Override
public Supplier<Map<Boolean, List<Integer>>> supplier() {
    return () -> new HashMap<Boolean, List<Integer>>(2) {
        {
            put(true, new ArrayList<>());
            put(false, new ArrayList<>());
        }
    };
}
复制代码

这里不但创建了累积器的Map,还为true和false两个键下面出实话了对应的空列表。在收集过程中会把质数和非指数分别添加到这里。收集器重要的方法是accumulator,因为它定义了如何收集流中元素的逻辑。这里它也是实现了前面所讲的优化的关键。现在在任何一次迭代中,都可以访问收集过程的部分结果,也就是包含迄今找到的质数的累加器:

@Override
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
    return ((Map<Boolean, List<Integer>> acc, Integer candidate) -> acc.get(isPrime(acc.get(true), candidate)).add(candidate));
}
复制代码

在这个个方法中,你调用了isPrime方法,将待测试是否为质数的数以及迄今为止找到的质数列表(也就是累积Map中true键对应的值)传递给它。这次调用的结果随后被用作获取质数或非质数列表的键,这样就可以把新的被测数添加到恰当的列表中。

3.第三步:让收集器并行工作(如果可能)

下一个方法要在并行收集时把两个部分累加器合并起来,这里,它只需要合并两个Map,即将第二个Map中质数和非质数列表中的所有数字合并到第一个Map的对应列表中就行了:

@Override
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
    return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
        map1.get(true).addAll(map2.get(true));
        map1.get(false).addAll(map2.get(false));
        return map1;
    };
}
复制代码

请注意,实际上这个收集器是不能并行的,因为该算法本身是顺序的。这意味着永远都不会调用combiner方法,你可以把它的实现留空。为了让这个例子完整,我们还是决定实现它。

4.第四步:finisher方法和收集器的characteristics方法

最后两个方法实现都很简单。前面说过,accumulator正好就是收集器的结果,也用不着进一步转换,那么finisher方法就返回identity函数:

@Override
public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
    return Function.identity();
}
复制代码

就characteristics方法而言,我们已经说过,它既不是CONCURRENT也不是UNOREDERED,但却是IDENTITY_FINISH的:

@Override
public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
复制代码

现在,你可以用这个新的自定义收集器来替代partitioningBy工厂方法创建的那个,并获得完全相同的结果了:

private static Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n) {
    return IntStream.rangeClosed(2, n).boxed()
            .collect(new PrimeNumbersCollector());
}
Map<Boolean, List<Integer>> primes = partitionPrimesWithCustomCollector(10);
// {false=[4, 6, 8, 9, 10], true=[2, 3, 5, 7]}
System.out.println(primes);
复制代码

收集器性能比较

用partitioningBy工厂方法穿件的收集器和你刚刚开发的自定义收集器在功能上是一样的,但是我们没有实现用自定义收集器超越partitioningBy收集器性能的目标呢?现在让我们写个小程序测试一下吧:

public class CollectorHarness {
    public static void main(String[] args) {
        long fastest = Long.MAX_VALUE;
        // 运行十次
        for (int i = 0; i < 10; i++) {
            long start = System.nanoTime();
            // 将前100万个自然数按指数和非质数区分
            partitionPrimes(1_000_000);
            long duration = (System.nanoTime() - start) / 1_000_000;
            // 检查这个执行是否是最快的一个
            if (duration < fastest) {
                fastest = duration;
            }
            System.out.println("done in " + duration);
        }
        System.out.println("Fastest execution done in " + fastest + " msecs");
    }
}
复制代码

在因特尔I5 6200U 2.4HGz的笔记上运行得到以下的结果:

done in 976
done in 1091
done in 866
done in 867
done in 760
done in 759
done in 777
done in 894
done in 765
done in 763
Fastest execution done in 759 msecs
复制代码

现在把测试框架的 partitionPrimes 换成 partitionPrimesWithCustomCollector ,以便测试我们开发的自定义收集器的性能。

public class CollectorHarness {
    public static void main(String[] args) {
        excute(PrimeNumbersCollectorExample::partitionPrimesWithCustomCollector);
    }

    private static void excute(Consumer<Integer> primePartitioner) {
        long fastest = Long.MAX_VALUE;
        // 运行十次
        for (int i = 0; i < 10; i++) {
            long start = System.nanoTime();
            // 将前100万个自然数按指数和非质数区分
            // partitionPrimes(1_000_000);
            primePartitioner.accept(1_000_000);
            long duration = (System.nanoTime() - start) / 1_000_000;
            // 检查这个执行是否是最快的一个
            if (duration < fastest) {
                fastest = duration;
            }
            System.out.println("done in " + duration);
        }
        System.out.println("Fastest execution done in " + fastest + " msecs");
    }
}
复制代码

现在,程序打印:

done in 703
done in 649
done in 715
done in 434
done in 386
done in 403
done in 449
done in 416
done in 353
done in 405
Fastest execution done in 353 msecs
复制代码

还不错!看来我们没有白费功夫开发这个自定义收集器。

总结

  1. collect 是一个终端操作,它接受的参数是将流中元素累积到汇总结果的各种方式(称为收集器)。
  2. 预定义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。
  3. 预定义收集器可以用 groupingBy 对流中元素进行分组,或用 partitioningBy 进行分区。
  4. 收集器可以高效地复合起来,进行多级分组、分区和归约。
  5. 你可以实现 Collector 接口中定义的方法来开发你自己的收集器。
目录
相关文章
|
4天前
|
缓存 NoSQL Java
Java高并发实战:利用线程池和Redis实现高效数据入库
Java高并发实战:利用线程池和Redis实现高效数据入库
20 0
|
5天前
|
安全 Java 程序员
Java8实战-新的日期和时间API
Java8实战-新的日期和时间API
15 3
|
23小时前
|
Java
在 Java 中,类是一种定义对象的模板,它包含数据成员(字段)和方法。
在 Java 中,类是一种定义对象的模板,它包含数据成员(字段)和方法。
|
1天前
|
Java 索引
Java List实战:手把手教你玩转ArrayList和LinkedList
【6月更文挑战第17天】在Java中,ArrayList和LinkedList是List接口的实现,分别基于动态数组和双向链表。ArrayList适合索引访问,提供快速读取,而LinkedList擅长插入和删除操作。通过示例展示了两者的基本用法,如添加、访问、修改和删除元素。根据场景选择合适的实现能优化性能。
|
1天前
|
存储 分布式计算 监控
Java一分钟之-Hazelcast:内存数据网格
【6月更文挑战第17天】**Hazelcast是开源的内存数据网格(IMDG),加速分布式环境中的数据访问,提供内存存储、分布式计算、线性扩展及高可用性。常见挑战包括内存管理、网络分区和数据分布不均。通过配置内存限制、优化网络和分区策略可避免问题。示例展示如何创建Hazelcast实例并使用分布式Map。使用Hazelcast提升性能和扩展性,关键在于理解和调优。**
17 1
|
3天前
|
Java 开发者
Java 面向对象编程实战:从类定义到对象应用,让你成为高手!
【6月更文挑战第15天】在Java中,掌握面向对象编程至关重要。通过创建`Book`类,展示了属性如`title`和`author`,以及构造方法和getter方法。实例化对象如`book1`和`book2`,并访问其属性。进一步扩展类,添加`pages`和`calculateReadingTime`方法,显示了类的可扩展性。在更大规模的项目中,如电商系统,可以定义`Product`、`User`和`Order`类,利用对象表示实体和它们的交互。实践是精通Java OOP的关键,不断学习和应用以提升技能。
|
3天前
|
设计模式 Java
一文掌握 Java 面向对象精髓:从类定义到对象实战
【6月更文挑战第15天】Java面向对象编程初学者指南:类是对象模板,如`Person`类含`name`和`age`属性。创建对象用`new`,如`Person person = new Person()`。访问属性如`person.name=&quot;Alice&quot;`,调用方法如`person.sayHello()`。类能继承,如`Student extends Person`。对象间共享数据可传参或共用引用。多态性允许父类引用调用子类方法。注意对象生命周期和内存管理,避免内存泄漏。通过实践和理解这些基础,提升编程技能。
|
4天前
|
Java 编译器 程序员
【实战攻略】Java高手教你如何灵活运用if-else和switch,提升代码效率!
【6月更文挑战第14天】本文探讨了Java中if-else和switch语句的巧妙运用,通过示例展示了如何提升代码效率和可读性。通过使用Map重构if-else结构,使代码更简洁易维护;利用switch处理枚举类型,实现清晰的代码结构。在性能方面,switch在选项少时占优,而现代JIT编译器优化后的if-else适用于大规模字符串比较。理解并灵活运用这两种控制结构,能助你在Java编程中写出高效、易读的代码。
|
4天前
|
监控 Java Spring
Java 动态代理详解与实战示例
Java 动态代理详解与实战示例
4 1
|
5天前
|
JSON Java 数据格式
java读取接口返回的json数据 (二)
java读取接口返回的json数据 (二)
17 5