第一部分 Java基础
第二部分 Java进阶

Java GC面试题及答案(第5题)

● 如何在lambda表达式中加入Predicate

上个例子说到,java.util.function.Predicate允许将两个或更多的Predicate合成一个。它提供类似于逻辑操作符AND和OR的方法,名字叫做and()、or()和xor(),用于将传入filter()方法的条件合并起来。例如,要得到所有以J开始,长度为四个字母的语言,可以定义两个独立的Predicate示例分别表示每一个条件,然后用Predicate.and()方法将它们合并起来。

如下所示:

// 甚至可以用 and()、or()和 xor()逻辑函数来合并 Predicate,
// 例如要找到所有以 J 开始,长度为四个字母的名字,你可以合并两个 Predicate 并传入
Predicate<String> startsWithJ = (n) -> n.startsWith("J");
Predicate<String> fourLetterLong = (n) -> n.length() == 4; names.stream().filter(startsWithJ.and(fourLetterLong)).forEach((n) ->System.out.print("nName, which starts with 'J' and four letter long is:"+n));

类似地,也可以使用or()和xor()方法。本例着重介绍了如下要点:可按需要将Predicate作为单独条件然后将其合并起来使用。简而言之,你可以以传统Java命令方式使用Predicate接口,也可以充分利用lambda表达式达到事半功倍的效果。

● Java 8中使用lambda表达式的Map和Reduce示例

本例介绍最广为人知的函数式编程概念map。它允许你将对象进行转换。例如在本例中,我们将costBeforeTax列表的每个元素转换成为税后的值。我们将x->x*x lambda表达式传到map()方法,后者将其应用到流中的每一个元素。然后用forEach()将列表元素打印出来。使用流API的收集器类,可以得到所有含税的开销。有toList()这样的方法将map或任何其他操作的结果合并起来。由于收集器在流上做终端操作,因此之后便不能重用流了。你甚至可以用流API的reduce()方法将所有数字合成一个,下一个例子将会讲到。

/ 不使用 lambda 表达式为每个订单加上 12%的税
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
for(Integer cost :costBeforeTax){
    double price = cost + .12 * cost;
    System.out.println(price);
}
// 使用 lambda 表达式
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
costBeforeTax.stream().map((cost) ->cost +.12*cost).forEach(System.out::println);

在上面例子中,可以看到 map 将集合类(例如列表)元素进行转换的。还有一个 reduce() 函数可以将所有值合并成一个。Map 和 Reduce 操作是函数式编程的核心操作,因为其功能,reduce 又被称为折叠操作。另外,reduce 并不是一个新的操作,你有可能已经在使用它。SQL 中类似 sum()、avg() 或者 count() 的聚集函数,实际上就是reduce 操作,因为它们接收多个值并返回一个值。流 API 定义的 reduceh() 函数可以接受 lambda 表达式,并对所有值进行合并。IntStream 这样的类有类似 average()、count()、sum() 的内建方法来做 reduce 操作,也有mapToLong()、mapToDouble() 方法来做转换。这并不会限制你,你可以用内建方法,也可以自己定义。在这个 Java 8 的 Map Reduce 示例里,我们首先对所有价格应用 12% 的 VAT,然后用 reduce() 方法计算总和。

// 为每个订单加上 12%的税
// 老方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double total = 0;
for(Integer cost :costBeforeTax){
    double price = cost + .12 * cost;
    total = total + price;
}
System.out.println("Total : "+total);
// 新方法:
List costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
double bill = costBeforeTax.stream().map((cost) -> cost + .12 * cost).reduce((sum, cost) ->sum +cost).get();
System.out.println("Total : "+bill);

● 通过过滤创建一个String列表

过滤是Java开发者在大规模集合上的一个常用操作,而现在使用lambda表达式和流API过滤大规模数据集合是惊人的简单。流提供了一个filter()方法,接受一个Predicate对象,即可以传入一个lambda表达式作为过滤逻辑。下面的例子是用lambda表达式过滤Java集合,将帮助理解。

/ 创建一个字符串列表,每个字符串长度大于 2
List costBeforeTax = Arrays.asList("abc", "bcd", "defg", "jk");
List<String>filtered=strList.stream().filter(x->x.length() >2).collect(Collectors.toList());
System.out.printf("Original List : %s, filtered list : %s %n",strList,filtered);

输出:

Original List:[abc,,bcd,,defg,jk],filtered list:[abc,bcd,defg]

另外,关于filter()方法有个常见误解。在现实生活中,做过滤的时候,通常会丢弃部分,但使用filter()方法则是获得一个新的列表,且其每个元素符合过滤原则。

● 对列表的每个元素应用函数

我们通常需要对列表的每个元素使用某个函数,例如逐一乘以某个数、除以某个数或者做其它操作。这些操作都很适合用map()方法,可以将转换逻辑以lambda表达式的形式放在map()方法里,就可以对集合的各个元素进行转换了,如下所示。

// 将字符串换成大写并用逗号链接起来
List<String> G7 = Arrays.asList("USA", "Japan", "France", "Germany", "Italy","U.K.", "Canada");
String G7Countries=G7.stream().map(x->x.toUpperCase()).collect(Collectors.joining(","));
System.out.println(G7Countries);

输出:

USA, JAPAN, FRANCE, GERMANY, ITALY, U.K., CANADA

● 复制不同的值,创建一个子列表

本例展示了如何利用流的distinct()方法来对集合进行去重

///用所有不同的数字创建一个正方形列表
List<Integer> numbers = Arrays.asList(9, 10, 3, 4, 7, 3, 4);
List<Integer> distinct = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());
System.out.print("Original List : %s, Square Without duplicates : %s %n", numbers, distinct);

输出:

Original List:[9,10,3,4,7,3,4],Square Without duplicates:[81,100,9,16,49]

● 计算集合元素的最大值、最小值、总和以及平均值

IntStream、LongStream和DoubleStream等流的类中,有个非常有用的方法叫做summaryStatistics()。可以返回IntSummaryStatistics、LongSummaryStatistics或者DoubleSummaryStatistic s,描述流中元素的各种摘要数据。在本例中,我们用这个方法来计算列表的最大值和最小值。它也有getSum()和getAverage()方法来获得列表的所有元素的总和及平均值。

/获取数字的个数、最小值、最大值、总和以及平均值
List<Integer> primes = Arrays.asList(2, 3, 5, 7, 11, 13, 17, 19, 23, 29);
IntSummaryStatistics stats = primes.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("Highest prime number in List : "+stats.getMax());
System.out.println("Lowest prime number in List : "+stats.getMin());
System.out.println("Sum of all prime numbers : "+stats.getSum());
System.out.println("Average of all prime numbers : "+stats.getAverage());

输出:

Highest prime number in List:29

Lowest prime number in List:2

Sum of all prime numbers:129

Average of all prime numbers:12.9

Java 8的10个lambda表达式,这对于新手来说是个合适的任务量,你可能需要亲自运行示例程序以便掌握。试着修改要求创建自己的例子,达到快速学习的目的。

补充:从例子中我们可以可以看到,以前写的匿名内部类都用了lambda表达式代替了。

● 那么,我们简单谈谈“lambda表达式&匿名内部类”

● 两者不同:

1.关键字this

(1)匿名内部类中的this代表匿名类

(2)Lambda表达式中的this代表lambda表达式的类

2.编译方式不同

(1)匿名内部类中会编译成一个.class文件,文件命名方式为:主类+$+(1,2,3.......)

(2)Java编译器将lambda表达式编译成类的私有方法。使用了Java 7的invokedynamic字节码指令来动态绑定这个方法

● Java8中的lambda表达式6要点

要点1:lambda表达式的使用位置,预定义使用了@Functional注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者Callable接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于java.util.function包内的接口,例如Predicate、Function、Consumer或Supplier,那么可以向其传lambda表达式。

要点2:lambda表达式和方法引用,lambda表达式内可以使用方法引用,仅当该方法不修改lambda表达式提供的参数。本例中的lambda表达式可以换为方法引用,因为这仅是一个参数相同的简单方法调用。

list.forEach(n -> System.out.println(n));
list.forEach(System.out::println); // 使用方法引用

然而,若对参数有任何修改,则不能使用方法引用,而需键入完整地 lambda 表达式,如下所示:

list.forEach((String s) -> System.out.println("*" + s + "*"));

事实上,可以省略这里的 lambda 参数的类型声明,编译器可以从列表的类属性推测出来。

要点 3:lambda 表达式内部引用资源,lambda 内部可以使用静态、非静态和局部变量,这称为 lambda 内的变量捕获。

要点 4:lambda 表达式也成闭包,Lambda 表达式在 Java 中又称为闭包或匿名函数,所以如果有同事把它叫闭包的时候,不用惊讶。

要点 5:lambda 表达式的编译方式,Lambda 方法在编译器内部被翻译成私有方法,并派发  invokedynamic  字节码指令来进行调用。可以使用 JDK中的  javap  工具来反编译 class 文件。使用  javap -p  或  javap -c -v  命令来看一看 lambda 表达式生成的字节码。

private static java.lang.Object lambda$0(java.lang.String);

要点 6:lambda 表达式的限制,lambda 表达式有个限制,那就是只能引用 final 或 final  局部变量,这就是说不能在 lambda 内部修改定义在域外的变量。

List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { factor++; });

Error:

Compile time error:"local variables referenced from a lambda expression must be final or effectively final"

另外,只是访问它而不作修改是可以的,如下所示:

List<Integer> primes = Arrays.asList(new Integer[]{2, 3,5,7});
int factor = 2;
primes.forEach(element -> { System.out.println(factor*element); });

输出:

4

6

10

14

因此,它看起来更像不可变闭包,类似于 Python。

全部教程