JDK8新特性(三)-聚合操作

参考https://docs.oracle.com/javase/tutorial/collections/streams/

为了更好地理解这篇文章的内容,强烈建议把前两篇介绍lambda表达式和方法引用的文章先看下。

一、聚合操作

依然考虑Person这个类,要打印集合roster中每个Person的名字,先看普通的写法:

for (Person p : roster) {
System.out.println(p.getName());
}

再看使用聚合操作forEach的写法:

roster
.stream()
.forEach(e -> System.out.println(e.getName());

单纯打印信息太简单,看不出聚合操作的优势,接着看:

管道与流

管道就是一系列聚合操作。下面的代码作用是打印所有过滤所有男性成员的名字并打印,其中用到了filter和forEach操作组成的管道:

roster
.stream()
.filter(e -> e.getGender() == Person.Sex.MALE)
.forEach(e -> System.out.println(e.getName()));

跟使用for-each循环的代码进行对比:

for (Person p : roster) {
if (p.getGender() == Person.Sex.MALE) {
System.out.println(p.getName());
}
}

管道包含以下组件:

  • 一个元素源:操作元素的来源,可以是集合、数组、 I/O channel。
  • 若干中间操作。 中间操作会生成新的流。流就是一个元素序列,跟集合的区别在于,流并不是存储元素的数据结构。每一步中间操作产生的新的元素流正好可以供下一步操作使用。
  • 一个终端操作。像forEach这种终端操作,不会生成流,而会生成一个基本类型数据、一个集合、或者什么都不生成。本例中的foreach接收一个lambda表达式,表达式做的操作只是打印Person的名字。

再看一个例子,计算所有男性成员的平均年龄:

double average = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();

mapToInt操作生成了新的流:IntStream,这是通过接收一个lambda表达式(e -> e.getAge())或方法引用(Person::getAge)来完成一个流到另一个流的转换。 average操作计算上一步中IntStream的元素的平均值,并返回一个OptionalDouble对象,最终调用getAsDouble获取一个double。

聚合操作与迭代器的区别

像forEach这种聚合操作,很像迭代器Iterator,但是他们之间还是有几点不同:

  • 聚合操作不提供next方法,对元素的迭代对调用者不可见,调用者只能决定对哪个集合进行迭代,但不能决定如何迭代。而迭代器Iterator对元素的迭代是暴露给调用者的,调用者可以决定对哪个集合进行迭代,也可以决定如何进行迭代。外部迭代只能串行执行,而内部迭代没有这种限制,可以并行执行提高效率。
  • 聚合操作的元素是从流(stream)获取的,迭代器操作的元素从集合获取。
  • 聚合操作可以接收lambda表达式,来指定如何操作元素。

二、化简操作

上个例子中的average()操作也叫化简操作,就是 JDK中还包含了 average, sum, min, max, count等一系列化简操作,就是把元素流通过某种规则转化成一个结果值, 比如计算平均值、最大值、最小值、和值、个数统计,这样的操作叫化简操作。除了上面提到的几种完成特定功能的操作,JDK还提供了通用的化简操作Stream.reduce和Stream.collect方法。

Stream.reduce

以计算男性成员的年龄和为例,完成这个功能即可以使用Stream.sum方法:

Integer totalAge = roster
.stream()
.mapToInt(Person::getAge)
.sum();

也可以使用Stream.reduce方法:

Integer totalAgeReduce = roster
.stream()
.map(Person::getAge)
.reduce(
0,
(a, b) -> a + b);

reduce方法接收两个参数,含义是:

  • 操作元素: 方法的初始值、默认返回值
  • 累加器: 表达式“(a, b) -> a + b”中a代表中间值,b代表下一个操作数,“a+b”作为新的中间值进行下一步计算

reduce方法在操作每一个元素时,总是返回一个新值。某些复杂场景下,这样做效率很低下。

Stream.collect

与reduce操作总是返回新值不同,collect操作是在原值基础上进行修改。 下面考虑计算平均值这个场景,需要两个数据:总元素个数、元素和值。

class Averager implements IntConsumer
{ private int total = 0;
private int count = 0;

public double average() {
    return count > 0 ? ((double) total)/count : 0;
}

public void accept(int i) { total += i; count++; }
public void combine(Averager other) {
    total += other.total;
    count += other.count;
}

}

使用方法如下:

Averager averageCollect = roster.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(Person::getAge)
.collect(Averager::new, Averager::accept, Averager::combine);

System.out.println(“Average age of male members: “ +
averageCollect.average());

collect方法接收三个参数:

  • supplier: supplier就是工厂方法,用来生成一个容器类,保存collect方法的返回值,本例中是一个Averager实例.
  • accumulator: accumulator方法把stream中的元素处理后放入supplier生成的结果容器。
  • combiner: combiner方法是用来把两个结果容器合并成一个结果返回。

尽管JDK提供了average聚合操作来计算stream中元素的平均值,但自定义的collect方法更灵活,适用于更复杂的计算。 再看一个例子:

List namesOfMaleMembersCollect = roster
.stream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.map(p -> p.getName())
.collect(Collectors.toList());

这次只有一个参数,Collectors这个类封装了很多实用操作,就不需要传入上面说的三个参数。 下面看一个按性别分组的例子:

Map<Person.Sex, List> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));

再看一个按性别分组,但是只返回姓名的例子,这个有点复杂:

Map<Person.Sex, List> namesByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));

按性别分类计算总年龄:

Map<Person.Sex, Integer> totalAgeByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.reducing(
0,
Person::getAge,
Integer::sum)));

按性别分类计算平均年龄:

Map<Person.Sex, Double> averageAgeByGender = roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.averagingInt(Person::getAge)));

上面提到的这些还是要实际使用几次,熟练了就不晕了。

并行计算

并行计算就是把父问题分解为子问题,多线程同时解决,然后合并子问题的结果。尽管JDK提供了并行计算fork/join框架, 但是还是需要手动指定如何分解问题。比起并行聚合操作来说,还是太麻烦。 自己实现并行计算的难点在于,要考虑线程安全的集合操作,不如使用JDK现成的解决方案方便。并行计算能不能提高程序运行效率取决于数据规模和CPU数量,具体场景具体分析。

并行处理Streams

Collection.parallelStream或者BaseStream.parallel可以创建并行stream。再看一个例子,并行计算男性平均年龄:

double average = roster
.parallelStream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();

并发Reduction

串行按年龄分组

Map<Person.Sex, List> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));

并行按年龄分组

ConcurrentMap<Person.Sex, List> byGender =
roster
.parallelStream()
.collect(
Collectors.groupingByConcurrent(Person::getGender));

顺序

下面讨论管道处理流中元素的顺序。先看一个打印元素的例子:

Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8 };
List listOfIntegers =
new ArrayList<>(Arrays.asList(intArray));

System.out.println(“listOfIntegers:”);
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + “ “));
System.out.println(“”);

System.out.println(“listOfIntegers sorted in reverse order:”);
Comparator normal = Integer::compare;
Comparator reversed = normal.reversed();
Collections.sort(listOfIntegers, reversed);
listOfIntegers
.stream()
.forEach(e -> System.out.print(e + “ “));
System.out.println(“”);

System.out.println(“Parallel stream”);
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + “ “));
System.out.println(“”);

System.out.println(“Another parallel stream:”);
listOfIntegers
.parallelStream()
.forEach(e -> System.out.print(e + “ “));
System.out.println(“”);

System.out.println(“With forEachOrdered:”);
listOfIntegers
.parallelStream()
.forEachOrdered(e -> System.out.print(e + “ “));
System.out.println(“”);

输出如下:

listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
Parallel stream:
3 4 1 6 2 5 7 8
Another parallel stream:
6 3 1 5 7 8 4 2
With forEachOrdered:
8 7 6 5 4 3 2 1

解释一下上面的输出:

  • 第一个就是按元素的初始化顺序进行遍历输出,没什么好说的。
  • 第二个是使用了排序方法Collections.sort排序后,遍历输出。这次是有顺序的。
  • 第三、第四个是使用并行流输出,两次打印元素的顺序完全随机,是运行时jvm随机决定的,以便能最大化并行的优势。
  • 第五个是调用了forEachOrdered方法,这次输出就有顺序了,但是运行效率可能不如并行流。

副作用

干扰

当管道在处理stream中的元素时,如果stream源发生了变化,就会报错。下面这个例子,尝试连接所有listOfStrings集合中的string,但是运行这段代码,会抛出ConcurrentModificationException:

try {
List listOfStrings =
new ArrayList<>(Arrays.asList(“one”, “two”));

// This will fail as the peek operation will attempt to add the
// string "three" to the source after the terminal operation has
// commenced. 

String concatenatedString = listOfStrings
    .stream()

    // Don't do this! Interference occurs here.
    .peek(s -> listOfStrings.add("three"))

    .reduce((a, b) -> a + " " + b)
    .get();

System.out.println("Concatenated string: " + concatenatedString);

} catch (Exception e) {
System.out.println(“Exception caught: “ + e.toString());
}

有状态的Lambda表达式

Avoid using stateful lambda expressions as parameters in stream operations. 有状态的lambda表达式就是最终返回值会被管道中的操作所影响。下面这个例子,是把listOfIntegers中的元素通过map方法复制到另一个list中,分别使用串行流与并行流

List serialStorage = new ArrayList<>();

System.out.println(“Serial stream:”);
listOfIntegers
.stream()

// Don't do this! It uses a stateful lambda expression.
.map(e -> { serialStorage.add(e); return e; })

.forEachOrdered(e -> System.out.print(e + " "));

System.out.println(“”);

serialStorage
.stream()
.forEachOrdered(e -> System.out.print(e + “ “));
System.out.println(“”);

System.out.println(“Parallel stream:”);
List parallelStorage = Collections.synchronizedList(
new ArrayList<>());
listOfIntegers
.parallelStream()

// Don't do this! It uses a stateful lambda expression.
.map(e -> { parallelStorage.add(e); return e; })

.forEachOrdered(e -> System.out.print(e + " "));

System.out.println(“”);

parallelStorage
.stream()
.forEachOrdered(e -> System.out.print(e + “ “));
System.out.println(“”);

“e -> { parallelStorage.add(e); return e; }”就是一个有状态的lambda表达式。输出结果是随机的。

Serial stream:
8 7 6 5 4 3 2 1
8 7 6 5 4 3 2 1
Parallel stream:
8 7 6 5 4 3 2 1
1 3 6 2 4 5 8 7

JDK8新特性(二)-方法引用

参考https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

方法引用

上一篇说了lambda表达式,主要是用来简化代码,代替匿名内部类的。那这篇说的方法引用是用来在特定场景下替代lambda表达式的,目的也是简化代码。 还是考虑Person这个类:

public class Person {

public enum Sex {
    MALE, FEMALE
}

String name;
LocalDate birthday;
Sex gender;
String emailAddress;

public int getAge() {
    // ...
}

public Calendar getBirthday() {
    return birthday;
}    

public static int compareByAge(Person a, Person b) {
    return a.birthday.compareTo(b.birthday);
}}

考虑一个 Person数组,现在要根据年龄对其排序,普通写法如下:

Person[] rosterAsArray = roster.toArray(new Person[roster.size()]);

class PersonAgeComparator implements Comparator {
public int compare(Person a, Person b) {
return a.getBirthday().compareTo(b.getBirthday());
}
}

Arrays.sort(rosterAsArray, new PersonAgeComparator());

因为Comparator接口是函数式接口,所以PersonAgeComparator类可以省去,用lambda表达式代替:

Arrays.sort(rosterAsArray,
(Person a, Person b) -> {
return a.getBirthday().compareTo(b.getBirthday());
}
);

或者直接调用Person类的compareByAge方法:

Arrays.sort(rosterAsArray,
(a, b) -> Person.compareByAge(a, b)
);

因为lambda表达式调用了已经存在的方法,所以可以使用更简化的方法引用替代:

Arrays.sort(rosterAsArray, Person::compareByAge);

Person::compareByAge与(a, b) -> Person.compareByAge(a, b)在语义上是等价的。都有如下特性:

  • 参数列表与函数式接口Comparator的方法compare(Person, Person)保持一致
  • 都调用了Person.compareByAge

几种方法引用的类型

类型

示例

引用静态方法

ContainingClass::staticMethodName

引用某对象的实例方法

containingObject::instanceMethodName

引用某类对象的实例方法

ContainingType::methodName

引用构造函数

ClassName::new

引用静态方法

上文的Person::compareByAge就是个例子。

引用某对象的实例方法

直接看代码:

class ComparisonProvider {
public int compareByName(Person a, Person b) {
return a.getName().compareTo(b.getName());
}

public int compareByAge(Person a, Person b) {
    return a.getBirthday().compareTo(b.getBirthday());
}

} ComparisonProvider myComparisonProvider = new ComparisonProvider();
Arrays.sort(rosterAsArray, myComparisonProvider::compareByName);
myComparisonProvider::compareByName”就是调用myComparisonProvider这个对象的compareByName方法。

引用某个类的实例的实例方法

这样翻译有点绕,直接看代码更清楚:

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

“String::compareToIgnoreCase”这一句接受两个参数,假定为(String a, String b),执行结果就相当于a.compareToIgnoreCase(b)。

引用构造函数

引用构造函数实例如下:

public static <T, SOURCE extends Collection, DEST extends Collection> DEST transferElements(
SOURCE sourceCollection,
Supplier collectionFactory) {

DEST result = collectionFactory.get();
for (T t : sourceCollection) {
    result.add(t);
}
return result;

}

上面代码中的transferElements方法是对集合进行复制。Supplier也是JDK8新增的函数式接口,源码如下,很简单:

@FunctionalInterface
public interface Supplier {
T get();
}

下面使用lambda表达式把list转换为hashset:

Set rosterSetLambda =
transferElements(roster, () -> { return new HashSet<>(); });

可以使用构造方法引用替换掉lambda表达式:

Set rosterSet = transferElements(roster, HashSet::new);

上面的语句没有指定HashSet结合中的具体元素类型,编译器会推断出应该是Person类型,当然,也可以指定明确指定集合中的元素类型,像下面这样:

Set rosterSet = transferElements(roster, HashSet::new);

JDK8新特性(一)-Lambda表达式

参考https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

Lambda 表达式

我们知道java中有匿名内部类,匿名内部类的缺陷之一就是如果你的实现很简单,简单到只有一个方法,甚至只有一行代码,但还是要编写冗长的内部类代码,而且内部类的语法看起来不简洁。Lambda表达式可以解决这个问题,它可以看做把函数当成一个参数传递给另一个函数,

Lambda 表达式的理想应用场景

假定我们正在开发一个社交类应用,其中有一个功能,是让管理员给指定条件的用户发信息。下表详细描述了这个功能:

属性

描述

名称

对指定用户执行操作

主要参与者

系统管理员

前置条件

系统管理员登入系统

后置条件

操作只在符合指定条件的用户上执行

主要场景

  1. 系统管理员指定过滤条件
  2. 系统管理员指定要执行的动作
  3. 系统管理员选择’提交’
  4. 系统根据提交的过滤条件查询出特定用户
  5. 系统对上一步过滤出的用户执行操作

扩展

执行操作前,系统管理员可以预览选中的用户。

使用频率

每天多次

用Person代表用户

public class Person {

public enum Sex {
    MALE, FEMALE
}

String name;
LocalDate birthday;
Sex gender;
String emailAddress;

public int getAge() {
    // ...
}

public void printPerson() {
    // ...
}

}

假定使用List存储所有用户。下面我们先用最笨的办法实现这个功能,然后一步步进行优化。

实现 1:针对某一个属性写特定查询方法

最简单的实现就是针对Person的每一个属性,都写一个对应的查询方法来完成过滤用户的工作。如下所示:打印年龄大于某个值的用户

public static void printPersonsOlderThan(List roster, int age) {
for (Person p : roster) {
if (p.getAge() >= age) {
p.printPerson();
}
}
}

这样实现的缺点很明显,如果Person类的age字段从Int改成String,或者新增了一个Habit属性,都需要改动或者新增相应的方法;而且这种实现限制性太强,如果想筛选小于某个年龄的用户,printPersonsOlderThan方法并不满足要求,还需要再写一个方法。

实现 2:针对某一个属性写通用查询方法

下面的方法比printPersonsOlderThan更通用,它会筛选某个范围内的用户

public static void printPersonsWithinAgeRange(
List roster, int low, int high) {
for (Person p : roster) {
if (low <= p.getAge() && p.getAge() < high) {
p.printPerson();
}
}
}

显然printPersonsWithinAgeRange方法比printPersonsOlderThan更通用,但是如果要筛选特定性别的用户,或者要选特定性别和年龄的用户,这个方法都不能满足要求,为每个属性都写这样的方法还是太麻烦了。

实现 3: 使用接口

新增一个CheckPerson接口,printPersons接收不同的CheckPerson实现来筛选不同属性的Person。

public static void printPersons(
List roster, CheckPerson tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

interface CheckPerson {
boolean test(Person p);
}

下面是CheckPerson的一种实现,功能是过滤年龄在18到25之间的男性:

class CheckPersonEligibleForSelectiveService implements CheckPerson {
public boolean test(Person p) {
return p.gender == Person.Sex.MALE &&
p.getAge() >= 18 &&
p.getAge() <= 25;
}
}

只需要把具体实现CheckPersonEligibleForSelectiveService传入printPersons中即可

printPersons(
roster, new CheckPersonEligibleForSelectiveService());

实现4: 使用匿名内部类

我们还可以在调用printPersons时传入匿名内部类代替实现3中的CheckPersonEligibleForSelectiveService:

printPersons(
roster,
new CheckPerson() {
public boolean test(Person p) {
return p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25;
}
}
);

以上几种方式都是JDK8之前的实现方法,匿名内部类相比前面几种实现已经简洁了不少,但是在JDK8中还可以更简洁:使用lambda表达式代替匿名内部类。

实现 5: 使用Lambda表达式指定过滤条件

前面代码中涉及的CheckPerson接口是函数式接口,就是只有一个抽象方法的接口,所以实现类只有一个可以实现的方法,那么就可以把方法名省略掉,看下面黑体部分代码:

printPersons(
roster,
(Person p) -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

);

可以看出来lambda表达式分为三部分,(Person p)代表操作对象,Person.Sex.MALE && p.getAge() >= 18 && p.getAge() <= 25则定义了操作逻辑,中间用’->‘连接。在此基础上,使用标准函数式接口替代CheckPerson接口,进一步简化代码。

实现 6:使用标准函数式接口简化Lambda表达式

我们重新看一下CheckPerson这个接口的定义:

interface CheckPerson {
boolean test(Person p);
}

这个接口很简单,只有一个抽象方法,这种接口也叫函数式接口。我们再考察下CheckPerson中的test方法,这个方法接收一个参数,并返回boolean值,JDK已经帮我们内置了一个这种接口,省去我们自己再去定义了。那就是java.util.function包中的Predicate接口,我们看下这个接口的代码

interface Predicate {
boolean test(T t);
}

这是一个泛型接口。只有一个test方法,接收一个参数,并返回boolean值,完全可以替代CheckPerson这个接口。所以printPersonsWithPredicate方法定义更新如下:

public static void printPersonsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

调用printPersonsWithPredicate方法时,第二个参数tester可以接收lambda表达式,如下:

printPersonsWithPredicate(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25
);

实现 7: 更广泛的应用Lambda表达式

考察printPersonsWithPredicate这个方法,看下哪里还可以应用lambda表达式:

public static void printPersonsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.printPerson();
}
}
}

printPersonsWithPredicate这个方法接收一个List实例roster和一个Predicate实例tester,对于roster中的每一个对象p,如果满足tester.test(p),就调用p.printPerson() 如果对满足条件的p进行别的操作呢?比如要发邮件sendEmails();那就需要再写一个sendEmailsWithPredicate()?

public static void sendEmailsWithPredicate(
List roster, Predicate tester) {
for (Person p : roster) {
if (tester.test(p)) {
p.sendEmails();
}
}
}

这样写显然太麻烦,把”p.printPerson();”和”p.sendEmails();”抽象化,就是调用某个对象p的方法,该方法没有返回值。要使用lambda表达式,需要实现函数式接口,jdk8中提供了一个Consumer接口如下:

public interface Consumer {
void accept(T t);
}

刚好满足要求,把printPersonsWithPredicate方法改写成:

public static void processPersons(
List roster,
Predicate tester,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
block.accept(p);
}
}
}

黑体部分引入新接口Consumer,调用processPersons()代码如下所示,黑体部分是新增的lambda表达式

processPersons(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.printPerson()
);

有了上面的经验,那么猜测一定存在接收一个参数,并且有返回值的函数式接口,这样你可以把Predicate过滤的对象转换成另一个操作对象。比如上一步过滤出了年龄在18~25岁之间的人,下一步你想验证这些人的email是不是合法,那么就需要有一个方法,传入Person对象,返回email,然后进行判断。jdk8中提供了Function<T,R>接口,该接口源码如下:

public interface Function<T, R> {
R apply(T t);
}

引入Function:

public static void processPersonsWithFunction(
List roster,
Predicate tester,
Function<Person, String> mapper,
Consumer block) {
for (Person p : roster) {
if (tester.test(p)) {
String data = mapper.apply(p);
block.accept(data);
}
}
}

processPersonsWithFunction方法调用如下:

processPersonsWithFunction(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);

实现 8: 使用泛型

实现7中的processPersonsWithFunction方法,Iterable、Predicate、Function、Consumer接口使用的都是具体类型,接下来可以引入泛型,使得processElements方法更通用,代码如下:

public static <X, Y> void processElements(
Iterable source,
Predicate tester,
Function <X, Y> mapper,
Consumer block) {
for (X p : source) {
if (tester.test(p)) {
Y data = mapper.apply(p);
block.accept(data);
}
}
}

调用如下:processElements

processElements(
roster,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25,
p -> p.getEmailAddress(),
email -> System.out.println(email)
);

上面

实现 9: 引入聚合操作

下面使用聚合操作加lambda表达式来完成上面的功能,这样连processElements这样的方法都不用定义了。

roster
.stream()
.filter(
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25)
.map(p -> p.getEmailAddress())
.forEach(email -> System.out.println(email));

The following table maps each of the operations the method processElements performs with the corresponding aggregate operation:

processElements 中的步骤

对应的聚合操作

获取元素流

Stream stream()

使用Predicate实例过滤元素

Stream filter(Predicate<? super T> predicate)

映射到另一个类型上

Stream map(Function<? super T,? extends R> mapper)

使用Consumer实例对元素做操作

void forEach(Consumer<? super T> action)

Lambda表达式语法

lambda表达式由以下几部分组成:

  • 圆括号包围的若干参数,参数之间用逗号分隔。例如:(String s, Person p)。可以省略类声明,改写成:(s, p);如果只有一个参数,圆括号也可以省略,直接写p。下面这个表达式就是最简略的那种情况:

    p -> p.getGender() == Person.Sex.MALE

    && p.getAge() >= 18
    && p.getAge() <= 25
  • 箭头, ->

  • 函数体,可以是一个表达式,也可以是代码块。如果是表达式,那表达式的值会作为返回值。

    p.getGender() == Person.Sex.MALE

    && p.getAge() >= 18
    && p.getAge() <= 25

    如果是代码块,必须用中括号包围(无返回值的方法例外)。

    p -> {

    return p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25;

    }

    注意:return 语句并不是表达式。

    email -> System.out.println(email)

lambda表达式看起来很像一个方法,最开始我们就是用来替代匿名内部类,只有一个方法的匿名内部类,省去接口声明,再省去方法声明,剩下的部分跟lambda表达式很像,所以lambda表达式可以看做匿名方法。 下面这个例子Calculator,展示了处理多个参数的lambada表达式:

public class Calculator {

interface IntegerMath {
    int operation(int a, int b);   
}

public int operateBinary(int a, int b, IntegerMath op) {
    return op.operation(a, b);
}

public static void main(String... args) {

    Calculator myApp = new Calculator();
    IntegerMath addition = (a, b) -> a + b;
    IntegerMath subtraction = (a, b) -> a - b;
    System.out.println("40 + 2 = " +
        myApp.operateBinary(40, 2, addition));
    System.out.println("20 - 10 = " +
        myApp.operateBinary(20, 10, subtraction));    
}

}

Calculator的operateBinary方法接收两个操作数a、b,还有一个IntegerMath接口,IntegerMath是函数式接口,定义IntegerMath接口时可以用lambda表达式。

访问局部变量、Lambda表达式变量的作用范围

像内部类一样,lambda表达式也可以访问局部变量,不过lambda表达式中的变量只在表达式中生效,但是完全不用担心lambda表达式会带来同名变量的污染问题。通过下面的例子说明:

import java.util.function.Consumer;

public class LambdaScopeTest {

public int x = 0;

class FirstLevel {

    public int x = 1;

    void methodInFirstLevel(int x) {

        // The following statement causes the compiler to generate
        // the error "local variables referenced from a lambda expression
        // must be final or effectively final" in statement A:
        //
        // x = 99;

        Consumer<Integer> myConsumer = (y) -> 
        {
            System.out.println("x = " + x); // Statement A
            System.out.println("y = " + y);
            System.out.println("this.x = " + this.x);
            System.out.println("LambdaScopeTest.this.x = " +
                LambdaScopeTest.this.x);
        };

        myConsumer.accept(x);

    }
}

public static void main(String... args) {
    LambdaScopeTest st = new LambdaScopeTest();
    LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
    fl.methodInFirstLevel(23);
}

}

输出如下:

x = 23
y = 23
this.x = 1
LambdaScopeTest.this.x = 0

如果把lambda表达式中的参数x换成y:

Consumer myConsumer = (x) -> {
// …
}

编译器会报错”variable x is already defined in method methodInFirstLevel(int)”,因为x是方法参数,已经被使用了。 jdk8之后,局部类、匿名内部类、lambda表达式都可以访问所属代码块的被final修饰的局部变量(方法参数)或值未发生变化的局部变量(方法参数),考虑下面代码:

void methodInFirstLevel(int x) {
x = 99;
// …
}

x=99这个赋值语句破坏了参数x的不变性,所以编译不通过,编译器报错“Local variable x defined in an enclosing scope must be final or effectively final”。

System.out.println(“x = “ + x);

Lambda表达式中的类型推断

怎么判断lambda表达式中的类型?回顾下实现3中表达式:

p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25

这个表达式在两个地方用到了,分别是实现3和实现6,回看这两个实现的代码:

  • public static void printPersons(List roster, CheckPerson tester) ----实现3
  • public void printPersonsWithPredicate(List roster, Predicate tester) ----实现6

运行printPersons方法时,这个方法接收一个CheckPerson参数,所以lambda表达式就应该是这个类型。而运行printPersonsWithPredicate方法时,该方法只有一个参Predicate,所以 lambda表达式就是这个类型。只有当编译器能根据上下文判断出lambda表达式的目标类型时,你才能使用lambda表达式,包含以下场景:

  • 变量申明语句
  • 赋值语句
  • Return语句
  • 数组初始化语句
  • 方法参数
  • Lambda expression bodies
  • 条件表达式
  • 类型转换语句

目标类型与方法参数

看一下下面两个函数式接口 ( java.lang.Runnable and java.util.concurrent.Callable):

public interface Runnable {
void run();
}

public interface Callable {
V call();
}

一个有返回值,一个没有返回值. 下面定义几个重载方法:

void invoke(Runnable r) {
r.run();
}

T invoke(Callable c) {
return c.call();
}

如下调用,那么最终是执行的哪个方法呢?

String s = invoke(() -> “done”);

答案是invoke(Callable c)这个方法,因为lambda表达式() -> “done”,表示字符串”done”是返回值。而invoke(Runnable r)没有返回值。

序列化

不推荐对lambda表达式序列化,这部分内容以后有机会再详细了解。

一些Hotspot JVM的GC问题

本文基本翻译自http://www.oracle.com/technetwork/java/faq-140837.html。

1.Hotspot如何实现分代垃圾收集器的?

HotSpot默认的GC收集器把堆分为年轻代、老年代。多数对象会先分配到年轻代,当对象经过几次GC后仍然存活,就会被移动到老年代。一般来说,年轻代区域较小,GC活动频繁;老年代区域较大,GC活动不频繁。 年轻代的GC收集器采用复制算法,年轻代被分为eden-space, to-space, from-space。对象先被分配到eden-space和from-space,这两个区满了以后触发Minor GC。一般情况下,年轻代的大部分对象都会被标记为垃圾,少量存活对象会被复制到to-space。如果to-space空间不够,剩余存活对象会被复制到老年代。-XX:+UseSerialGC参数可以指定年轻代使用串行收集器,-XX:+UseParallelGC参数可以指定年轻代使用并行收集器。 老年代的GC收集器采用标记-清除-整理算法,-XX:+UseConcMarkSweepGC or -XX:+UseG1GC可以指定使用并发收集器

2.年轻代大小如何设置?

使用-XX:NewSize和-XX:MaxNewSize来指定年轻代的初始值和上限,当年轻代动态变化时,eden space和survivor space都会变化。

3.eden-space在Minor GC后会清空吗?

是的,如果survivor space放不下eden space的存活对象时,剩余存活对象会被晋升到老年代。

4.MaxTenuringThreshold和TargetSurvivorRatio这两个参数如何影响对象晋升到老年代?

-XX:MaxTenuringThreshold=10就是直接设置对象存活周期的阈值,年轻代年龄大于10的对象会晋升到老年代。-XX:TargetSurvivorRatio=60是指晋升到老年代的对象总占用空间要达到survivor space空间的60%。每次Minor GC后,都会重新计算该阈值。Hotspot会遍历所有age的对象,并对其所占用的大小进行累积,当累积的大小超过了TargetSurvivorRatio时,以这个age和MaxTenuringThreshold中更小的那个作为阈值。

5.如果存在大量长期存活的对象,如何调优?

Minor GC没能回收的对象都会被复制,如果应用中存在大量长周期的对象,那么此类对象的复制会显得没必要,不如直接晋升到老年代,以优化GC速度。可以通过-XX:MaxTenuringThreshold=0来设定最大晋升年龄为0,这样每次Minor GC扫描到的对象会直接晋升。吞吐量优先的收集器无视该参数。

6.GC什么时候被触发?

  • 串行、并行收集器:当内存全部被占用或者剩余空间不够分配时,GC会启动。
  • 并发收集器:当老年代空间使用到一定比率(默认68%)时,GC启动。
  • 显式调用:代码中使用System.gc(),会触发Full GC,多见于NIO框架。

7.什么是CMS收集器?优缺点有哪些?

CMS是Concurrent Mark Sweep的缩写,翻译过来就是并发标记清除,优点是GC停顿时间短。因为CMS GC过程中最耗时的操作(标记、清除)都与用户线程并发执行。缺点是会产生内存碎片,因为CMS过程中没有复制、压缩存活对象,当内存碎片化,剩余空间不够分配对象时,JVM会停止用户线程,对老年代存活对象进行压缩整理,腾出连续分布的内存空间,可以使用-XX:+UseCMSCompactAtFullCollection参数显式指定每次Full GC后都进行压缩整理。

8.CMS GC的流程?

  1. 初始标记:暂停所有工作线程,标记所有直接跟GC ROOTS关联的对象,然后恢复工作线程。
  2. 并发标记:从上一步标记到的对象开始,扫描所有可达对象并标记。第2、 3、 5步都是并发执行阶段,对象的引用关系仍有可能发生变化。在这些并发阶段分配到老年代的对象(包括从年轻代晋升的对象),会被标记为存活。
  3. 并发预清理:并发标记阶段对象间引用关系可能发生变化。所有变化了、未被扫描的对象会被重新扫描标记。此阶段用户线程还在工作,所以同一对象可能会被扫描多次。
  4. 最终标记:此阶段会暂停所有用户线程,扫描标记变化的对象。此阶段完成后,因为用户线程恢复运行,所以可能存在之前被标记存活的对象已经成为垃圾,但是标记并未更新,这类对象本次GC不会收集,但是下次GC会收集。
  5. 并发清除:并发清理垃圾对象,存活的对象并不会被移动,这会产生内存碎片。
  6. 重置:清理数据为下次GC做准备。

9.什么是TLAB(thread local allocation buffer)?

翻译过来就是线程本地分配缓冲,这种技术是为了提升多线程场景下在堆上分配内存的效率。简单地说就是在堆上分配内存需要操作一个公共指针,同一时间只能有一个线程操作这个指针,其他线程都需要等待。而TLAB使得每个线程有自己私有的内存空间进行对象分配,如果TLAB区空间不足,就把旧的TLAB区释放(已分配的对象还在,只是移交控制权),申请新的TLAB。可以使用-XX:+ PrintTLAB打印相关信息;还可以使用-XX:+ ResizeTLAB调节默认TLAB区大小。

10.对象什么时候会进入老年代?

  • -XX: PretenureSizeThreshold=,该参数指定大于某个值的对象直接分配到老年代,这样做可以避免新生代复制对象的开销。
  • -XX: MaxTenuringThreshold=,该参数指定大于某个年龄的对象被晋升到老年代。每经过一次Minor GC,存活对象年龄加一,达到指定年龄时,晋升到老年代。
  • -XX:TargetSurvivorRatio=60,每次Minor GC后,Hotspot会遍历所有age的对象,并对其所占用的大小进行累积,当累积的大小超过了60%时,会把年龄大于等于age的对象晋升到老年代,而不会等对象年龄达到MaxTenuringThreshold。
  • Minor GC时,大量对象存活,Survivor区空间不足,此时存活对象进入老年代。

HotSpot JVM的一些GC概念

本文主要参考Oracle官方文档https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ 当初学JAVA时,就知道JAVA有内存管理的特性,不需要程序员手动管理。但是对于这个概念的理解很浅。

1.什么是GC(Garbage Collector)?

GC是指垃圾收集器,并不特指JAVA的垃圾收集器。

2.为什么要了解GC

JVM默认的GC参数可能在特定的程序上表现不好,需要手动调整这些参数来提高程序效率。那就有必要了解JVM中GC的大致原理及相关参数的含义。

3.GC算法

  • 引用计数—–给每个对象维护一个引用计数,当引用计数为0时,进行清理。但是无法回收循环引用的对象
  • 标记-清除—–从GCROOT开始标记所有存活的对象,清理剩下的垃圾对象。但是会产生内存碎片
  • 标记-复制—–把内存区分为两部分A、B,从GCROOT开始标记A中所有存活的对象,把存活对象复制到B中,清空A。但是会浪费一半内存空间
  • 标记-整理—–从GCROOT开始标记所有存活的对象,把存活对象重新分配

4.JVM内存分布

[caption id=”” align=”alignnone” width=”456”]Description of Figure 3-2 follows JVM的一种典型内存分布示意图[/caption] 上图中Young代表年轻代,Tenured代表老年代,Virtual代表暂时未分配的保留区域。其中年轻代又分为一个Eden区和两个Survivor区。之所以采用上面这种分配方式,理由见下图 [caption id=”” align=”aligncenter” width=”543”]Description of Figure 3-1 follows 对象存活示意图[/caption] 上图的横轴是分配的字节,也可以理解成时间,纵轴是存活的字节。可以看出大多数对象的生存时间很短,只有少部分对象可以存活过两次GC。

5.JVM的GC算法实现

  • Serial Collector—–串行收集器,只有一个GC线程工作,效率高,实现简单,适用于堆大小在100MB以下的应用
  • Parallel Collector—–并行收集器,多个GC线程工作,吞吐量高,适用于对GC停顿不敏感的应用
  • Mostly Concurrent Collectors—–并发收集器,GC线程与用户线程一起运行,可有效控制每次GC的停顿时间,适用于网站等堆响应时间敏感的应用

6.GC效率的评价标准

  • 停顿时间—–
  • 吞吐量
  • 资源占用

7.GC的大致步骤

  • 在年轻代分配对象,每次GC结束,仍存活的对象年龄加1,达到一定年龄的对象晋升到老年代
  • 内存使用到一定比率时,找出存活的对象,并标记,
  • 把存活对象移动到一端,完成内存清理。