目录
前面讨论了如何编写 Lambda 表达式, 接下来将详细阐述另一个重要方面: 如何使用Lambda 表达式。 即使不需要编写像 Stream 这样重度使用函数式编程风格的类库, 学会如何使用 Lambda 表达式也是非常重要的。 即使一个最简单的应用,也可能会因为代码即数据的函数式编程风格而受益。
Java 8中的另一个变化是引入了默认方法和接口的静态方法,它改变了人们认识类库的方式,接口中的方法也可以包含代码体了。
1.基本类型
在 Java 中, 有一些相伴的类型, 比如 int 和 Integer——前者是基本类型, 后者是装箱类型。 基本类型内建在语言和运行环境中, 是基本的程序构建模块; 而装箱类型属于普通的 Java 类, 只不过是对基本类型的一种封装。
Java 的泛型是基于对泛型参数类型的擦除——换句话说, 假设它是 Object 对象的实例——因此只有装箱类型才能作为泛型参数。 这就解释了为什么在 Java 中想要一个包含整型值的列表 List<int>, 实际上得到的却是一个包含整型对象的列表 List<Integer>。麻烦的是, 由于装箱类型是对象, 因此在内存中存在额外开销。 比如, 整型在内存中占用4 字节, 整型对象却要占用 16 字节。
为了减小这些性能开销,Java 8对整型、长整型和双浮点型做了特殊处理, 因为它们在数值计算中用得最多, 特殊处理后的系统性能提升效果最明显。对基本类型做特殊处理的方法在命名上有明确的规范,如果方法返回类型为基本类型,则在基本类型前加 To,如下图所示:
如果参数是基本类型,则不加前缀只需类型名即可,如下图所示:
如果高阶函数使用基本类型,则在操作后加后缀To再加基本类型,如mapToLong。如有可能,应尽可能多地使用对基本类型做过特殊处理的方法,进而改善性能。 这些特殊的 Stream 还提供额外的方法,避免重复实现一些通用的方法,让代码更能体现出数值计算的意图。
private static void statisticsTest() {
IntSummaryStatistics summaryStatistics = studentDTOList.stream()
.mapToInt(student -> student.getAge()).summaryStatistics();
System.out.printf("Max:%d,Min:%d,Ave:%f,Sum:%d", summaryStatistics.getMax(), summaryStatistics.getMin(),
summaryStatistics.getAverage(), summaryStatistics.getSum());
}
这里使用对基本类型进行特殊处理的方法mapToInt,将每个学生映射为学生年龄并转换年龄为int类型。因为该方法返回一个IntStream对象,它包含一个summaryStatistics 方法,这个方法能计算出各种各样的统计值,如 IntStream 对象内所有元素中的最小值、最大值、平均值以及数值总和。
2.默认方法
Collection接口中新增加了stream方法,如何能让Collection的子类在不实现该方法的情况下通过编译?Java 8通过在接口中增加默认方法来解决该问题,在任何接口中,无论函数接口还是非函数接口,都可以使用该方法。Iterable接口中也新增加了一个默认方法:forEach,该方法的功能和for循环类似,但是允许用户使用一个Lambda表达式作为循环体。
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
它使用一个常规的for循环遍历Iterable对象,然后对每个值调用accept方法。和类不同的是,接口没有成员变量,因此默认方法只能通过调用子类的方法来修改子类本身,避免对子类的实现做出各种假设。
2.1 接口的继承
子接口重写了父接口中的默认方法,实现类方法在被调用的时候,调用的是子接口的默认方法。
定义接口Person:
public interface Person {
default void say() {
System.out.println("I am person!");
}
}
定义子接口Child
public interface Child extends Person{
@Override
default void say() {
System.out.println("I am Child");
}
}
实现类:
public class ChildImpl implements Child {
public static void main(String[] args) {
ChildImpl child = new ChildImpl();
child.say();
}
}
输出:I am Child
2.2 多重继承
接口允许多重继承,因此有可能碰到两个接口包含签名相同的默认方法的情况。比如我们将上面定义Child方法修改为:
public interface Child {
default void say() {
System.out.println("I am Child");
}
}
ChildImpl 方法定义如下:
public class ChildImpl implements Child, Person {
}
此时,javac并不明确应该继承哪个接口中的方法,因此编译器会报错:
类 com.martin.learn.java8.domain.ChildImpl从类型 com.martin.learn.java8.domain.Child 和 com.martin.learn.java8.domain.Person 中继承了say() 的不相关默认值
总结:
- 类胜于接口:如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法。
- 子类胜于父类:如果一个接口继承了另一个接口,且两个接口都定义个一个默认方法,那么子类中定义的方法胜出。
3.内置函数式接口
@FunctionalInterface注释会强制 javac 检查一个接口是否符合函数接口的标准。 如果该注释添加给一个枚举类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错,重构代码时,使用它能很容易发现问题。为了更好地支持函数式编程,所以jdk给我们内置了很多常用的函数式接口,一般都是放在java.util.function包中。
3.1 Optional
Optional是为核心类库新设计的一个数据类型,用来替换null值。人们常常使用null值表示值不存在,Optional对象能更好地表达这个概念,使用null代表值不存在的最大问题在于NullPointerException,一旦引用一个存储null值的变量,程序就会立即崩溃。下面我们举例说明Optional对象的API,从而切身体会一下它的使用方法。使用工厂方法of,可以从某个值创建出一个Optional对象,Optional对象相当于值的容器,而该值可以通过get方法提取。
public class OptionalTest {
public static void main(String[] args) {
Optional<String> optional = Optional.of("a");
Assert.isTrue("a".equals(optional.get()), "值不为a");
}
}
Optional对象也可能为空,因此还有一个对应的工厂方法empty,另外一个工厂方法ofNullable则可将一个空值转换成Optional对象。isPresent方法表示一个Optional对象里是否有值。
public class OptionalTest {
public static void main(String[] args) {
Optional<String> empty = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
System.out.println(empty.isPresent());
System.out.println(alsoEmpty.isPresent());
}
}
输出结果如下:
false
false
使用Optional对象的方式之一是在调用get()方法前,先使用isPresent检查Optional对象是否有值。使用orElse方法则更加简洁,当Optional对象为空时,该方法提供一个备选值;也可以使用orElseGet方法,该方法接收一个Supplier对象,只用在Optional对象真正为空时才会调用。
public class OptionalTest {
public static void main(String[] args) {
Optional<String> empty = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
System.out.println(empty.orElse("a"));
System.out.println(alsoEmpty.orElseGet(() -> "b"));
}
}
输出结果如下:
a
b
3.2 Supplier
supplier<T>接口的作用是向外提供一个数据,拥有get()方法,用它可以产生或者获取我们需要的数据。lambda如果要使用外部局部变量,局部变量必须是final的。
import java.util.function.Supplier;
/**
* @date: 2019/7/16 15:40
* @description:
*/
public class SupplierTest {
private static void method(Supplier<String> supplier) {
String str = supplier.get();
System.out.println(str);
}
public static void main(String[] args) {
String str = "Hello";
method(() -> str);
}
}
3.3 Consumer例子
和Supplier相反,该函数式接口用于消费数据,比如对于获取的数据,做下一步的处理:
import java.util.function.Consumer;
/**
* @date: 2019/7/16 15:53
* @description:
*/
public class ConsumerTest {
public static void method(Consumer<String> consumer) {
//接收待处理的数据
consumer.accept("Hello World");
consumer.accept("I am pretty");
}
public static void method2(Consumer<String> one, Consumer<String> two) {
//分别先执行one操作,然后再做two操作
one.andThen(two).accept("Hello World");
}
public static void main(String[] args) {
//t表示consumer中保存的数据逐一遍历
method(t -> {
System.out.println(t.toUpperCase());
});
method2(one -> System.out.println(one.toLowerCase()), two -> System.out.println(two.toUpperCase()));
}
}
输出结果如下:
HELLO WORLD
I AM PRETTY
hello world
HELLO WORLD
3.4 Predicate操作
Predicate接口用于断言,也叫谓语接口,简单的判断“是”与“不是”:接收一个参数,得到一个boolean结果值,多用于判断和过滤。
import java.util.function.Predicate;
/**
* @author: martin
* @date: 2019/7/16 16:37
* @description:
*/
public class PredicateTest {
private static void method(Predicate<String> predicate) {
boolean longCheck = predicate.test("Hello World");
if (longCheck) {
System.out.println("字符串太长");
} else {
System.out.println("字符串符合要求");
}
}
public static void main(String[] args) {
method(s -> s.length() > 40);
}
}
输出结果为:
字符串符合要求
3.5 Function例子
函数式编程中函数是可以作为参数的,该接口的作用即是如此。这样函数就可以被传递与复用,说白了就是一个复合函数:f(g(x),g(x)作为f(y)的参数:
import java.util.function.Function;
/**
* @date: 2019/7/16 16:55
* @description:
*/
public class FunctionTest {
/**
* 转换逻辑
*
* @param function
*/
public static void method(Function<String, Integer> function) {
Integer apply = function.apply("100");
System.out.println(apply);
}
public static void method2(Function<String, String> one, Function<String, Integer> two) {
//先执行one操作,再执行two操作
Integer age = one.andThen(two).apply("张三,12");
age++;
System.out.println("年龄:" + age);
}
public static void method3(Function<String, String> one, Function<String, Integer> two) {
//先执行one操作,再执行two操作
Integer age = two.compose(one).apply("张三,12");
age++;
System.out.println("年龄:" + age);
}
public static void main(String[] args) {
method(Integer::parseInt);
method2(s -> s.split(",")[1],Integer::parseInt);
method2(s -> s.split(",")[1],Integer::parseInt);
}
}
输出结果如下:
100
年龄:13
年龄:13
4.总结
- 使用为基本类型定制的Lambda表达式和Stream,如IntStream可以显著提升系统性能
- 默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀。
- 在一个值可能为空的情况下,使用Optional对象能替代使用null值。