赞
踩
我们最常用的面向对象编程(Java)属于命令式编程(Imperative Programming)这种编程范式。常见的编程范式还有逻辑式编程(Logic Programming),函数式编程(Functional Programming)。
函数式编程作为一种编程范式,在科学领域,是一种编写计算机程序数据结构和元素的方式,它把计算过程当做是数学函数的求值,而避免更改状态和可变数据。
函数式编程并非近几年的新技术或新思维,距离它诞生已有大概50多年的时间了。它一直不是主流的编程思维,但在众多的所谓顶级编程高手的科学工作者间,函数式编程是十分盛行的。
什么是函数式编程?
简单的回答:一切都是数学函数。函数式编程语言里也可以有对象,但通常这些对象都是恒定不变的 —— 要么是函数参数,要什么是函数返回值。函数式编程语言里没有 for/next 循环,因为这些逻辑意味着有状态的改变。相替代的是,这种循环逻辑在函数式编程语言里是通过递归、把函数当成参数传递的方式实现的。
举个例子:
a = a + 1
这段代码在普通成员看来并没有什么问题,但在数学家看来确实不成立的,因为它意味着变量值得改变。
使用前提:
Java 中
Lambda
表达式一共有三种基本形式
案例定义:
说明:Lambda
表达式没有参数,使用空括号 () 表示没有参数。该Lambda
表达式 实现了Eatable
接口,该接口也只有一个eat
方法,没有参数,且返回类型为void
。
@FunctionalInterface public interface Eatable { void eat(); } public class EatableDemo { public static void main(String[] args) { useEatable(() -> System.out.println("一天一苹果,医生远离我")); } private static void useEatable(Eatable e) { e.eat(); } }
控制台打印结果
一天一苹果,医生远离我
说明:
该代码块和普通方法遵循的规则别无二致,可以用返回或抛出异常来退出。只有一行代码的Lambda
表达式也可使用大括号,用以明确Lambda
表达式从何处开始、到哪里结束。Lambda
表达式也可以表示包含多个参数的方法;
public class EatableDemo {
public static void main(String[] args) {
useEatable(() -> {
System.out.println("一天一苹果,医生远离我");
System.out.println("一天二苹果,能活100岁");
}
);
}
private static void useEatable(Eatable e) {
e.eat();
}
}
控制台打印结果
一天一苹果,医生远离我
一天二苹果,能活100岁
案例定义:
说明:
Lambda
表达式包含且只包含一个参数,可省略参数的括号,Lambda
表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号 ({})将代码块括起来,如下代码:
@FunctionalInterface
public interface Flyable {
void fly(String s);
}
public class FlyableDemo {
public static void main(String[] args) {
useFlyable(s -> System.out.println(s));
}
public static void useFlyable(Flyable f) {
f.fly("风和日丽,晴空万里");
}
}
控制台打印结果
风和日丽,晴空万里
案例定义:
说明:
这行代码并不是将两个数字相加,而是创建了一个函数,用来计算 两个数字相加的结果。变量add
的类型是 Addable
,它不是两个数字的和, 而是将两个数字相加的那行代码。到目前为止,所有Lambda
表达式中的参数类型都是由编译器推导得出的。这当然不错, 但有时最好也可以显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的 情况也是如此。
@FunctionalInterface
public interface Addable {
int add(int x, int y);
}
public class AddableDemo {
public static void main(String[] args) {
useAddable((int x, int y) -> x + y);
}
public static void useAddable(Addable a) {
int sum = a.add(10, 20);
System.out.println("sum="+sum);
}
}
控制台打印结果
sum=30
省略规则:
注意事项:
Lambda
对应的接口
Lambda
对应的接口:Runnable r = () -> System.out.println("Lambda表达式“);Lambda
对应的接口:new Thread(() -> System.out.println(“Lambda表达式”)).start();所需类型不同
使用限制不同
Lambda
表达式,也可以使用匿名内部类Lambda
表达式实现原理不同
.class
字节码文件Lambda
表达式:编译之后,没有一个单独的 .class
字节码文件。对应的字节码会在运行的时候动态生成如果你以前使用过匿名内部类,也许遇到过这样的问题。当你需要匿名内部类所在方法里的变量,必须把该变量声明为 final
。如下例子所示:
final String name = getUserName();
addActionListener(new ActionListener() {
public void actionPerformed(OrderDTO orderDTO) {
System.out.println("hi " + name);
}
});
Java 8放松了这一限制,可以不必再把变量声明为 final,但其实该变量实际上仍然是 final 的。虽然无需将变量声明为 final,但在Lambda
表达式中,也无法用作非终态变量。如果坚持用作非终态变量(即改变变量的值),编译器就会报错。
在程序编写过程中,集合的处理应该是很普遍的。Java 8 对于 Collection 的处理花了很大的功夫,如果从 JDK 7 过渡到 JDK 8,这一块也可能是我们感受最为明显的。
Java 8 中,引入了流(Stream)的概念,这个流和以前我们使用的 IO 中的流并不太相同。
所有继承自 Collection 的接口都可以转换为 Stream。还是看一个例子。
假设我们有一个 List 包含 Person 对象,Person 有姓名 name 和年龄 age 连个字段。现要求这个列表中年龄大于 20 的人数。
通常按照以前我们可能会这么写:
long count = 0;
for (Person p : persons) {
if (p.getAge() > 20) {
count ++;
}
}
但如果使用 stream 的话,则会简单很多:
long count = persons.stream()
.filter(person -> person.getAge() > 20)
.count();
这只是 Stream
的很简单的一个用法。现在链式调用方法算是一个主流,这样写也更利于阅读和理解编写者的意图,一步方法做一件事。
Stream 的方法分为两类。一类叫惰性求值
,一类叫及早求值
(终结操作)。
判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream,那么是惰性求值。其实可以这么理解,如果调用惰性求值方法,Stream 只是记录下了这个惰性求值方法的过程,并没有去计算,等到调用及早求值方法后,就连同前面的一系列惰性求值方法顺序进行计算,返回结果。
通用形式为:
Stream.惰性求值.惰性求值. ... .惰性求值.及早求值
整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调用一个 build 方法,这时,对象才被真正创建。
collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作。可以理解为 Stream 向 Collection 的转换。
注意这边的 toList() 其实是 Collectors.toList(),因为采用了静态倒入,看起来显得简洁。
List<String> collected = Stream.of("a", "b", "c")
.collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
如果有一个函数可以将一种类型的值转换成另外一种类型,Map 操作就可以使用该函数,将一个流中的值转换成一个新的流。
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
map 方法就是接受的一个 Function 的匿名函数类,进行的转换。
遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter。
List<String> beginningWithNumbers =
Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
assertEquals(asList("1abc"), beginningWithNumbers);
filter 方法就是接受的一个 Predicate 的匿名函数类,判断对象是否符合条件,符合条件的才保留下来。
flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream。
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
flatMap 最常用的操作就是合并多个 Collection。
Stream 上常用的操作之一是求最大值和最小值。Stream API 中的 max 和 min 操作足以解决这一问题。
List<Integer> list = Lists.newArrayList(3, 5, 2, 9, 1);
int maxInt = list.stream()
.max(Integer::compareTo)
.get();
int minInt = list.stream()
.min(Integer::compareTo)
.get();
assertEquals(maxInt, 9);
assertEquals(minInt, 1);
这里有 2 个要点需要注意:
reduce 操作可以实现从一组值中生成一个值。在上述例子中用到的 count、min 和 max 方法,因为常用而被纳入标准库中。事实上,这些方法都是 reduce 操作。
上图展示了 reduce 进行累加的一个过程。具体的代码如下:
int result = Stream.of(1, 2, 3, 4)
.reduce(0, (acc, element) -> acc + element);
assertEquals(10, result);
注意 reduce 的第一个参数,这是一个初始值。0 + 1 + 2 + 3 + 4 = 10。
如果是累乘,则为:
int result = Stream.of(1, 2, 3, 4)
.reduce(1, (acc, element) -> acc * element);
assertEquals(24, result);
因为任何数乘以 1 都为其自身嘛。1 * 1 * 2 * 3 * 4 = 24。
Stream 的方法还有很多,这里列出的几种都是比较常用的。Stream 还有很多通用方法,具体可以查阅 Java 8 的 API 文档。
https://docs.oracle.com/javase/8/docs/api/
Stream 的并行化也是 Java 8 的一大亮点。数据并行化是指将数据分成块,为每块数据分配单独的处理单元。这样可以充分利用多核 CPU 的优势。
并行化操作流只需改变一个方法调用。如果已经有一个 Stream 对象,调用它的 parallel() 方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用 parallelStream() 就能立即获得一个拥有并行能力的流。
int sumSize = Stream.of("Apple", "Banana", "Orange", "Pear")
.parallel()
.map(s -> s.length())
.reduce(Integer::sum)
.get();
assertEquals(sumSize, 21);
这里求的是一个字符串列表中各个字符串长度总和。
如果你去计算这段代码所花的时间,很可能比不加上 parallel() 方法花的时间更长。这是因为数据并行化会先对数据进行分块,然后对每块数据开辟线程进行运算,这些地方会花费额外的时间。并行化操作只有在 数据规模比较大 或者 数据的处理时间比较长 的时候才能体现出有事,所以并不是每个地方都需要让数据并行化,应该具体问题具体分析。
Stream 转换为 List 是很常用的操作,其他 Collectors 还有很多方法,可以将 Stream 转换为 Set, 或者将数据分组并转换为 Map,并对数据进行处理。也可以指定转换为具体类型,如 ArrayList, LinkedList 或者 HashMap。甚至可以自定义 Collectors,编写自己的收集器。
Collectors (收集器)的内容太多,有兴趣的可以自己研究。
http://my.oschina.net/joshuashaw/blog/487322
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
另外一个尚未提及的关于集合类的内容是流中的元素以何种顺序排列。一些集合类型中的元素是按顺序排列的,比如 List;而另一些则是无序的,比如 HashSet。增加了流操作后,顺序问题变得更加复杂。
总之记住。如果集合本身就是无序的,由此生成的流也是无序的。一些中间操作会产生顺序,比如对值做映射时,映射后的值是有序的,这种顺序就会保留 下来。如果进来的流是无序的,出去的流也是无序的。
如果我们需要对流中的数据进行排序,可以调用 sorted 方法:
List<Integer> list = Lists.newArrayList(3, 5, 1, 10, 8);
List<Integer> sortedList = list.stream()
.sorted(Integer::compareTo)
.collect(Collectors.toList());
assertEquals(sortedList, Lists.newArrayList(1, 3, 5, 8, 10));
我们讨论过函数接口定义的标准,但未提及 @FunctionalInterface 注释。事实上,每个用作函数接口的接口都应该添加这个注释。
但 Java 中有一些接口,虽然只含一个方法,但并不是为了使用 Lambda 表达式来实现的。比如,有些对象内部可能保存着某种状态,使用带有一个方法的接口可能纯属巧合。
该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错。重构代码时,使用它能很容易发现问题。
方法引用符
::
该符号为引用运算符,而它所在的表达式被称为方法引用;推导与省略
常见的引用方法:
引用类方法,其实就是引用类的静态方法
案例定义:
public interface Converter {
int convert(String s);
}
public class ConverterDemo {
public static void main(String[] args) {
useConverter(Integer::parseInt);
}
private static void useConverter(Converter c) {
int number = c.convert("665") + 1;
System.out.println(number);
}
}
控制台打印结果
666
引用对象的实例方法,其实就是引用类中的成员方法
"HelloWorld"::toUpperCase
案例定义:
public class PrintString { public void printUpper(String s) { System.out.println(s.toUpperCase()); } } public interface Printer { void printUpperCase(String s); } public class PrinterDemo { public static void main(String[] args) { PrintString ps = new PrintString(); usePrinter(ps::printUpper); } private static void usePrinter(Printer p) { p.printUpperCase("helloWorld"); } }
引用类的实例方法,其实就是引用类中的成员方法
案例定义:
public interface MyString {
String mySubString(String s, int x, int y);
}
public class MyStringDemo {
public static void main(String[] args) {
useMyString(String::substring);
}
private static void useMyString(MyString my) {
String s = my.mySubString("HelloWorld", 0, 5);
System.out.println(s);
}
}
控制台打印结果
hello
引用构造器,其实就是引用构造方法
案例定义:
并提供无参构造方法和带参构造方法,以及成员变量对应的 get 和 set 方法
public class Student { private String name; private int age; // 省略get/set/满参构造方法 } public interface StudentBuilder { Student build(String name, int age); } public class StudentDemo { public static void main(String[] args) { useStudentBuilder(Student::new); } private static void useStudentBuilder(StudentBuilder sb) { Student student = sb.build("张三", 18); System.out.println(student.getName() + "," + student.getAge()); } }
控制台打印结果
张三,18
函数式接口:有且仅有一个抽象方法的接口
Lambda
表达式,所以函数式接口就是可以适用于Lambda
使用的接口;如何检测一个接口是不是函数式接口呢?
注意:
@FunctionalInterface
是可选的,就算我不写这个注解,只要保证满足函数式接口定义的条件,也照样是函数式接口,但是,建议加上该注解;需求:
代码如下:
public class RunnableDemo {
public static void main(String[] args) {
startThread(() -> System.out.println(Thread.currentThread().getName() + " 线程启动了"));
}
private static void startThread(Runnable r) {
new Thread(r).start();
}
}
控制台打印结果
张三,18
** 需求:**
代码如下:
public class ComparatorDemo { public static void main(String[] args) { ArrayList<String> array = new ArrayList<>(); array.add("ccc"); array.add("aa"); array.add("b"); array.add("ddd"); System.out.println("排序前: " + array); array.sort(getComparator()); System.out.println("排序后: " + array); } private static Comparator<String> getComparator() { // 如果方法的返回值是一个函数式接口,我们可以使用 Lambda 表达式作为结果返回 return (s1, s2) -> s2.length() - s1.length(); } }
控制台打印结果
张三,18
JDK 8 中提供了一组常用的核心函数接口:
接口 | 参数 | 返回类型 | 描述 |
---|---|---|---|
Predicate | T | boolean | 用于判别一个对象。比如求一个人是否为男性 |
Consumer | T | void | 用于接收一个对象进行处理但没有返回,比如接收一个人并打印他的名字 |
Function<T, R> | T | R | 转换一个对象为不同类型的对象 |
Supplier | None | T | 提供一个对象 |
UnaryOperator | T | T | 接收对象并返回同类型的对象 |
BinaryOperator | (T, T) | T | 接收两个同类型的对象,并返回一个原类型对象 |
Java 8 在 java.util.function 包下与定义了大量的函数式接口供我们使用
我们重点来学习下面的 4 个接口
Supplier:包含一个无参的方法:
练习:
代码如下:
public class SupplierTest {
public static void main(String[] args) {
int[] arr = {1, 3, 4, 5, 2};
int max = getMax(() -> {
Arrays.sort(arr);
return arr[arr.length - 1];
});
System.out.println(max);
}
private static int getMax(Supplier<Integer> sup) {
return sup.get();
}
}
控制台打印结果
张三,18
Consumer:包含两个方法
练习:
要求:
代码如下:
public class ConsumerTest {
public static void main(String[] args) {
operatorString(s1 -> System.out.print("姓名: " + s1.split(",")[0] + ", "), s2 -> System.out.println("年龄: " + s2.split(",")[1]));
}
private static void operatorString(Consumer<String> con1, Consumer<String> con2) {
String[] strArray = {"林青霞,30", "张曼玉,35", "王祖贤,33"};
for (String s : strArray) {
con1.andThen(con2).accept(s);
}
}
}
控制台打印结果
张三,18
Predicate:常用的四个方法
练习:
public class PredicateDemo01 { public static void main(String[] args) { boolean b1 = checkString1(s -> s.length() > 8); System.out.println("b1: " + b1); boolean b2 = checkString2(s -> s.length() > 8); System.out.println("b2: " + b2); boolean b3 = checkString3(s -> s.contains("hello"), s -> s.length() > 5); System.out.println("b3: " + b3); boolean b4 = checkString4(s -> s.contains("hello"), s -> s.length() > 20); System.out.println("b4: " + b4); } // 判断给定的字符串是否满足要求 private static boolean checkString1(Predicate<String> pre) { return pre.test("hello"); } private static boolean checkString2(Predicate<String> pre) { return pre.negate().test("hello"); } // 同一个字符串给出两个不同的判断条件,最后把这两个判断的结果做逻辑与运算的结果作为最终的结果 private static boolean checkString3(Predicate<String> pre1, Predicate<String> pre2) { String s = "hello,world"; return pre1.and(pre2).test(s); } private static boolean checkString4(Predicate<String> pre1, Predicate<String> pre2) { String s = "hello,world"; return pre1.or(pre2).test(s); } }
控制台打印结果
张三,18
Function<T,R>:常用的两个方法
练习:
代码如下:
public class FunctionTest {
public static void main(String[] args) {
convert1(s -> s.split(",")[1], Integer::parseInt, i -> i + 70);
}
private static void convert1(Function<String, String> fun1, Function<String, Integer> fun2, Function<Integer, Integer> fun3) {
String s = "林青霞,30";
Integer i = fun1.andThen(fun2).andThen(fun3).apply(s);
System.out.println(i);
}
}
控制台打印结果
张三,18
我们在编写代码的时候出现最多的就是空指针异常。所以在很多情况下我们需要做各种非空的判断。例如:
if (xxx!=null){
}
而过多的判断语句会让我们的代码显得臃肿不堪。
所以在 JDK8 引入了Optional
,养成使用Optional
的习惯后你可以写出更优雅的代码来避免空指针异常。
代码如下:
Cat cat = new Cat("tom", 3);
Optional<Cat> optionalCat1 = Optional.of(cat);
Optional<Cat> optionalCat2 = Optional.empty();
Optional<Cat> optionalCat3 = Optional.ofNullable(cat);
void ifPresent(Consumer<? super T> consumer):如果有值,就执行Consumer
接口的实现代码,并且该值会作为参数传给它
代码如下:
Cat cat = new Cat("tom", 3);
Optional<Cat> optionalCat = Optional.ofNullable(cat);
optionalCat.ifPresent(s -> System.out.println("name: " + s.getName()));
控制台打印结果
name:tom
System.out.println(Optional.ofNullable(new Cat("tom", 3)).get());
System.out.println(Optional.ofNullable(null).get());
控制台打印结果
System.out.println(Optional.ofNullable(new Cat("tom", 3)).orElse(new Cat("rose", 4)));
System.out.println(Optional.ofNullable(null).orElse(new Cat("rose", 4)));
**
Cat(dog=tom, age=3)
Cat(dog=rose, age=4)
System.out.println(Optional.ofNullable(new Cat("tom", 3)).orElseGet(() -> new Cat("rose", 4)));
System.out.println(Optional.ofNullable(null).orElseGet(() -> new Cat("rose", 4)));
控制台打印结果
Cat(dog=tom, age=3)
Cat(dog=rose, age=4)
System.out.println(Optional.ofNullable(new Cat("tom", 3)).orElseThrow(() -> new RuntimeException("cat为空")));
System.out.println(Optional.ofNullable(null).orElseThrow(() -> new RuntimeException("cat为空")));
运行结果:
我们可以使用 filer 方法对数据进行过滤。如果原本是有数据的,但是不符合判断,也会变成一个无数据的 Optional 对象
代码如下:
Optional.ofNullable(new Cat("tom", 3)).filter(c -> c.getAge() > 5).ifPresent(System.out::println);
控制台打印结果
代码如下:
System.out.println(Optional.ofNullable(null).isPresent());
控制台打印结果
false
代码如下:
@Test
public void test() {
String str = "hello";
// map() 方法用于将 Optional 对象中的值进行转换,而 flatMap() 方法用于将 Optional 对象中的值进行转换,并返回一个包含值的 Optional 对象
Optional<String> s1 = Optional.ofNullable(str).map(String::toUpperCase);
Optional<String> s2 = Optional.ofNullable(str).flatMap(s -> Optional.of(s.toUpperCase()));
}
无论 Optional 对象中的值是否为空,orElse() 函数都会执行,而 orElseGet() 函数只有在 Optional 对象中的值为空时,orElseGet() 中的 Supplier 方法才会执行
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。