一、前言
前面我们了解了Stream API相关的内容,本文再来系统的学习下JDK8其他相关的功能,并且来学习下函数式编程相关的内容。
二、核心内容
1. 函数式编程简介
通常情况下,编程语言的编程范式有三种类型:
- 命令式编程(Imperative Programming):根据一条条命令去执行,通过变量赋值及流程控制来实现相应的算法,命令式编程关注的点在于解决问题的步骤;
- 函数式编程(Functional Programming):函数式编程是面向数学的抽象,将计算描述为一种表达式求值,以函数为最小单位,而最终函数式程序实际上就是一个表达式。在函数式编程中,数据都是不可变的,所以没有并发编程的问题,是线程安全的。函数式编程允许把函数本身作为参数传入另一个函数,还允许返回一个函数!函数式编程关注的点在于数据的映射,也就是输入类型与输出类型的关系。
- 逻辑式编程(Logic Programming):给出事实,然后然后按照规则和事实来解决问题,而非步骤。
而在JDK1.8之前,面向对象编程的Java语言其实属于命令式编程的一种,而JDK1.8所添加的Lambda表达式则是属于函数式编程。再来简单看下函数式编程的优点:
- 函数的运行不依赖外部变量或状态,不依赖调用的时间和位置,只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
- 函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
- 由于函数上面的特性,所以函数中数据都是不可变的,所以多个线程之间也不会出现状态的共享,也就不会有并发编程的问题,所以是线程安全的。
- 惰性求值,是在将表达式赋值给变量(或称作绑定)时并不计算表达式的值,而在变量第一次被使用时才进行计算,比如Stream API的各种中间操作 peek等方法。
有关函数式编程的概念,我们就了解到这,如果有需要,大家可以自行搜索,推荐:
什么是函数式编程思维?,接下来我们来看JDK1.8中相关的实现。
2. Lambda表达式
Lambda表达式(也称为闭包),它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理,以前的时候我们都是使用匿名内部类来代替Lambda表达式。一般情况下,Lambda表达式可由逗号分隔的参数列表、-> 符号和语句块组成,相应的Lambda表达式一般有下面几种形式:
2.1 没有参数的表达式,使用空括号来表示:
Runnable noArguments = () -> System.out.println("Hello World");
该 Lambda 表达式 实现了 Runnable
接口,该接口只有一个没有参数的run方法,且返回类型为 void。
2.2 有参数的表达式
如果只有一个参数,可以省略参数的括号,并且可以省略参数的类型,因为该类型可以通过编译器推理得到。如果是多个参数,使用逗号分割参数列表:
ActionListener oneArgument = event -> System.out.println("button clicked");
ActionListener oneArgument = (ActionEvent event) -> System.out.println("button clicked");
2.3 Lambda表达式的主体不仅可以是一个表达式,还可以是一块代码块,使用大括号将代码块括起来:
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
该代码块和普通方法遵循的规则别无二致,可以用返回或抛出异常来退出。
2.4 表达式返回一个函数:
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
这两行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果,而变量add的类型是BinaryOperator。
不过需要注意的是,在原来使用匿名内部类的时候,如果我们要使用局部变量,那这个变量必须定义为final类型的。而Lambda表达式当然可以引用类成员和局部变量,虽然Lambda表达式可以不必把变量声明为final类型,但还是会将这些变量隐式转换成final类型:
String separator = ",";
Arrays.asList( "a", "b", "d" ).forEach(
( String e ) -> System.out.print( e + separator ) );
在这里,计算的时候,separator变量会被转换为final类型。
还有一点,Lambda表达式都可以通过原始的匿名类来实现,如果有需要,我们也可以转成匿名类的实现。
3. 函数接口
Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable
和java.util.concurrent.Callable
是函数式接口的最佳例子,还有我们上文提到的ActionListener
接口,该接口只有一个方法:actionPerformed
,该方法只有一个参数,返回空。
而在实践中,函数式接口非常脆弱,只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解
@FunctionalInterface
(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义:
@FunctionalInterface
public interface Functional {
void method();
}
不过有一点需要注意,默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的:
@FunctionalInterface
public interface FunctionalDefaultMethods {
void method();
default void defaultMethod() {
}
}
而JDK 8 中提供了一组常用的核心函数接口:
函数接口 | 参数 | 返回类型 | 描述 |
---|---|---|---|
Predicate<T> | T | boolean | 判断某一个对象是否满足条件 |
Consumer<T> | T | void | 接收一个对象进行处理,不返回结果 |
Function<T, R> | T | R | 将一个对象T转换为另一个对象R,对象类型可以相同,也可以不同 |
Supplier<T> | None | T | 提供一个对象,无需传入参数 |
UnaryOperator<T> | T | T | 接收对象并返回同类型的对象 |
BinaryOperator<T> | (T, T) | T | 接收两个同类型的对象,并返回一个原类型对象 |
其中 Cosumer
与 Supplier
对应,一个是消费者,一个是提供者。
Predicate
用于判断对象是否符合某个条件,经常被用来过滤对象。
Function
是将一个对象转换为另一个对象,比如说要装箱或者拆箱某个对象。
UnaryOperator
一元运算符操作,接收和返回同类型对象;
BinaryOperator
二元操作符,对一些对象进行二元操作;
3.1 Function对象
Function对象用于将一个对象T转换为另一个对象R,该Function对象的input参数只有一个。该函数式接口共有4个方法,不过最终都需要调用apply方法来返回我们所需要的对象。我们来简单学习下:
// 将Function对象应用到输入的参数上,然后返回计算结果
R apply(T t);
// 整合两个Function对象,先执行当前的Function对象,再执行调用的Function对象
default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
// 整合两个Function对象,先执行调用的Function对象,再执行当前的Function对象
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
//返回执行了apply方法之后的 输入参数的函数对象
static <T> Function<T, T> identity()
直接来看几个简单的例子:
Function<Integer, Long> add = input -> input.longValue() * 2;
long output = add.apply(3);
Function<Long, Integer> square = input -> input.intValue() + 2;
output = add.andThen(square).apply(3);
add作为Function对象,用于计算一个整数转为长整形之后并乘以2的结果,所以直接调用apply方法之后的结果是:3 * 2
,而andThen方法会先获取add对象,将add对象的结果作为andThen方法的入参,来对square对象进行后续计算,结果:(3 * 2) + 2
。
Long value2 = add.compose(square).apply(3L);
而compose方法会先获取square对象,将square对象的结果Integer类型的3 + 2
作为compose方法的入参,再进行add对象的计算,得到:(3 + 2) * 2
。
而至于identity
方法,不太清楚有什么具体的作用,按字面意思是返回函数对象的输入参数:
static <T> Function<T, T> identity() {
return t -> t;
}
3.2 Predicate对象
Predicate对象就是用于判断对象是否满足某个条件,返回的是布尔类型。该对象也有几个方法,不过最终也都是调用test 方法返回我们所需要的布尔类型:
// 返回true 或 false
boolean test(T t);
// 与操作,既满足A操作又满足B操作
default Predicate<T> and(Predicate<? super T> other)
// 这个是取反操作
default Predicate<T> negate()
// 或操作,满足A操作或满足B操作
default Predicate<T> or(Predicate<? super T> other)
// 静态方法,判断是否相等
static <T> Predicate<T> isEqual(Object targetRef)
我们还是来看几个例子:
Predicate<Integer> greaterThan = input -> input > 10;
// 满足条件,返回true
boolean test = greaterThan.test(11);
Predicate<Integer> lessThan = input -> input < 20;
// 与操作,返回true
test = greaterThan.and(lessThan).test(15);
// 取反操作,返回false
test = greaterThan.negate().test(15);
// 或操作,返回true
test = greaterThan.or(lessThan).test(15);
// 两个值不同,返回false
test = Predicate.isEqual(15).test(20);
3.3 Consumer对象
Consumer对象表示接收单个输入参数进行处理,并且没有返回值的操作,不像其他函数式接口,Consumer接口期望执行带有副作用的操作,也就是Consumer的操作可能会更改输入参数的内部状态。先看一下参数:
// 将Function对象应用到输入的参数上
void accept(T t);
// 整个两个Consumer,先执行调用的Consumer,再执行当前的Consumer
default Consumer<T> andThen(Consumer<? super T> after)
再来看个简单的例子:
Person person = new Person("beijing", "wang", 30);
Consumer<Person> consumer = Person -> Person.setSum(40);
consumer.accept(person);
// output: Person{city='beijing', surname='wang', sum=40}
System.out.println(person.getSum());
再看一下andThen方法:
Person person = new Person("beijing", "wang", 30);
Consumer<Person> consumer = Person -> Person.setSum(40);
Consumer<Person> personConsumer = Person -> {
Person.setSum(50);
Person.setCity("shanghai");
};
consumer.andThen(personConsumer).accept(person);
// output: Person{city='shanghai', surname='wang', sum=50}
System.out.println(person);
3.4 Supplier对象
Supplier对象用于提供一个对象,该接口没有传入参数,只有返回对象,类似于工厂方法。该对象只有一个get方法用于获取对应类型的对象。
Supplier<Person> supplier = () -> new Person("beijing", "wang", 30);
Person person = supplier.get();
System.out.println(person);
3.5 UnaryOperator和BinaryOperator对象
UnaryOperator对象和BinaryOperator对象分别代表一员操作符与二元操作符的函数式对象,其中UnaryOperator对象继承自Function,BinaryOperator对象继承自BiFunction,这两个函数式对象常用于对数值类型的操作运算。
- UnaryOperator对象接收一个参数,并返回一个相同类型的值,而BinaryOperator是接收两个参数,也返回相同类型的值;
- 其中,BiFunction和Function对象很像,不过BiFunction是接收三个参数,该对象只有apply和andThen两个方法,而相应的BinaryOperator则在此继承上多了minBy和maxBy两个方法:
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
R apply(T t, U u);
// 两个对象的整合操作
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after)
// 根据比较器获取最小值对象
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator)
// 根据比较器获取最大值对象
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator)
}
我们先来看下UnaryOperator的例子:
UnaryOperator<Integer> unaryOperator = input -> input * input;
Integer test = unaryOperator.apply(15);
System.out.println(test);
再来看下BinaryOperator的实例:
BinaryOperator<String> binaryOperator = (a, b) -> a + "-" + b;
Map<String,String> map = new HashMap<>();
map.put("X", "A");
map.put("Y", "B");
map.put("Z", "C");
List<String> list = new ArrayList<>();
map.forEach((a, b) -> list.add(binaryOperator.apply(a, b)));
System.out.println(list);
至于BinaryOperator的minBy和maxBy方法,前面学习Collectors的时候已经了解过,就是用于返回最大值或最小值的,这里就不多说了。
java.util.function包下还有许多相关的函数式接口,比如BiConsumer,DoubleConsumer,DoubleFunction,DoublePredicate等,这些接口功能和本文介绍的这些基础的函数式接口功能都是类似的,这里就不多说了,等有时间再慢慢了解。
如需要了解更多,可参考官方文档:JDK 8 Lambda Expressions
4. 接口的默认方法和静态方法
jdk1.8中接口新增了两个概念:默认方法和静态方法,默认方法的好处是允许在不打破现有继承体系的基础上动态添加接口方法,间接的实现对原有接口实现的兼容。
public interface DefaultInterface {
default void print() {
System.out.println("default method");
}
}
public class DefaultInterfaceImpl implements DefaultInterface {
}
public class DefaultInterfaceImpl implements DefaultInterface {
@Override
public void print() {
System.out.println("default interface impl");
}
}
而JDK8中的另一类方法,静态方法,和我们平时的静态方法使用类似,直接使用接口名称.静态方法进行调用即可,前面学习函数方法时候其实已经使用了接口的静态方法了,比如接口Function的identity方法:
static <T> Function<T, T> identity() {
return t -> t;
}
public interface DefaultInterface {
default void print() {
System.out.println("default method");
}
static void testStatic() {
System.out.println("test interface static method");
}
}
不过需要注意,接口的静态方法是无法被Override的。
如需了解默认方法的更多实现,可参考官方文档:JDK 8 Default Methods
5. 方法引用
方法引用可以让我们直接引用现有的方法,类的构造方法或实例对象,方法引用通过使用::
符号来实现,一般情况下配合Lambda表达式,使我们的代码更加简洁美观。一般有如下几种类型的方法引用:
5.1 构造方法引用
构造方法引用,格式是Class::new
,或者Class<T>::new
,需要注意的是这个构造器是没有参数的。还拿我们前文的例子来举例:
Supplier<Person> supplier = Person::new;
Person person = supplier.get();
5.2 静态方法引用
静态方法引用格式是:Class::static_method
,和我们平时静态方法的引用没太大区别,就是将.
修改为了::
:
Function<Long, String> function = String::valueOf;
String value = function.apply(15L);
5.3 类成员方法引用
成员方法引用和静态方法引用是类似的,只不过成员方法没有参数,格式:Class::method
,来看简单的例子:
Person person = new Person();
Function<Person, String> function = Person::getCity;
function.apply(person);
// String的无参toUpperCase方法
List<String> collect = Stream.of("1","2","3").map(String::toUpperCase).collect(Collectors.toList());
5.4 实例对象的成员方法的引用
如果引用的是某个实例对象的成员方法的话,那么格式稍有不同:instance::method
:
String test="123";
List<String> collect = Stream.of("1","2","3").filter(test::contains).collect(Collectors.toList());
如需了解更多,参考JDK官方文档:JDK8 Method References
6. Optional类
JDK8中引入了Optional类来处理空指针的问题,从而避免我们的源码做过多的非空检查,影响代码的可读性,并且该类是final类型不可变的,这样会避免许多问题。接下来,我们来简单看一下它的一些方法,我们首先来看一下静态方法,静态方法都是用于创建Optional对象的:
empty
方法用于创建一个空的Optional对象;of
方法接收一个T参数,T必需为非null值,然后返回该T类型的Optional对象;ofNullable
对象方法接收一个T参数,如果T为null,调用empty方法,如果不为空,调用of方法,这些我们都可以通过源码来了解。ifPresent
方法: 如果这个对象的值不为null返回true,否则返回false;另外还有一个重载的函数式表达式参数的方法;orElse
方法:如果Optional对象的值为不为空,则返回该对象的值,如果为空,则返回该方法所传递的参数值;orElseGet
和orElse
方法类似,只不过传递的参数是一个函数式表达式;orElseThrow
该方法表示如果值不存在的话就抛出异常,值存在的话就返回该值;filter
方法:该方法的参数是Predicate
表达式,表示的是Optional对象中的值是否满足Predicate的条件,然后返回新的Optional;map
方法:对Optional中的值进行处理,通常配合Optional.ofNullable
一起操作;flatMap
方法:功能和map类似,不同就是flatMap的返回值必须是Optional;
简单介绍了这些方法之后,我们通过几个简单的例子来大致了解下Optional的常用方式:
// 对一个对象,为null则不进行打印
string.ifPresent(System.out::println);
// 比如我们要获取返回对象person的city值,如果不存在返回空字符串,避免返回null
Optional<Person> optional = Optional.ofNullable(person);
String city = optional.map(Person::getCity).orElse("");
// map与flatmap方法
Optional<Person> optional = Optional.ofNullable(person);
String city = optional.map(Person::getCity).orElse("");
String city = optional.flatMap(input -> Optional.ofNullable(input.getCity())).orElse("");
并且map方法是可以链式使用的,所以我们可以通过使用多级map来避免多级非空判断。有的时候,Optional还可以用来做参数校验,比如说:
public void setName(String name) throws IllegalArgumentException {
this.name = Optional.ofNullable(name).filter(Person::isNameValid)
.orElseThrow(()->new IllegalArgumentException("Invalid username."));
}
本文参考自:
JDK 8 函数式编程入门
【译】Java 8的新特性—终极版
使用 Java8 Optional 的正确姿势
网友评论