Lambda 表达式&函数式接口
Lambda表达式好像已经出现很久了好像大部分人还是比较倾向于使用内部类
(老程序员表示Lambda表达式影响可读性,但是在很多组件中使用Lambda表达式还是很有必要的)
Lamdba表达式和接口是分不开的,JAVA8改动Lambda表达式的同时也新增了函数式接口
函数式接口
Functional Interface的定义很简单:任何包含唯一一个抽象方法的接口都可以称之为函数式接口。
但是函数式接口中还可以存在签名与Object的public方法相同的接口(毕竟最终父类时Object),并且可以存在静态方法。
@FunctionalInterface
interface FunctionalInterfaceWithStaticMethod {
static int sum(int[] array) {
return Arrays.stream(array).reduce((a, b) -> a+b).getAsInt();
}
boolean equals(Object obj);
void apply();
}
//这依旧是一个函数式接口
为了让编译器帮助我们确保一个接口满足函数式接口的要求,Java8提供了@FunctionalInterface注解。
另外JDK中已有的一些接口本身就是函数式接口,如Runnable。 JDK 8中又增加了java.util.function包, 提供了常用的函数式接口。
举个函数式接口的栗子
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//Runnable接口提供了一个run()抽象方法
在java8之前,你可以通过匿名内部类来实现对这个接口的调用,像下面这样
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("In another thread");
}
});
但是如果我们想要我们的代码更加优雅,我们应该使用Lambda表达式,当我们使用Lambda表达式刚刚代码就可以这样写:
Thread thread = new Thread(() ->
System.out.println("In another thread"));
Lambda表达式
Lambda表达式在Java8中最大的改动是
它允许把函数作为一个方法的参数(函数作为参数传递进方法中)
以下是lambda表达式的重要特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动确定返回值
还是先上栗子:
public class Java8Tester {
public static void main(String args[]){
Java8Tester tester = new Java8Tester();
// 类型声明
MathOperation addition = (int a, int b) -> a + b;
// 不用类型声明
MathOperation subtraction = (a, b) -> a - b;
// 大括号中的返回语句
MathOperation multiplication = (int a, int b) -> { return a * b; };
// 没有大括号及返回语句
MathOperation division = (int a, int b) -> a / b;
System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
System.out.println("10 / 5 = " + tester.operate(10, 5, division));
// 不用括号
GreetingService greetService1 = message ->
System.out.println("Hello " + message);
// 用括号
GreetingService greetService2 = (message) ->
System.out.println("Hello " + message);
greetService1.sayMessage("Runoob");
greetService2.sayMessage("Google");
}
interface MathOperation {
int operation(int a, int b);
}
interface GreetingService {
void sayMessage(String message);
}
private int operate(int a, int b, MathOperation mathOperation){
return mathOperation.operation(a, b);
}
}
变量作用域
- lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。(其实也可以不声明final)
int num = 1;
Converter<Integer, String> s =
(param) -> String.valueOf(param + num);
num = 5;
//编译会出错
- 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
String first = "";
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());
//编译会出错
方法引用
方法引用通过方法的名字来指向一个方法。
方法引用使用一对冒号 ::
内容比较简单并且Java核心已经有讲过
所以直接上栗子,我们在 Car 类中定义了 4 个方法作为例子来区分 Java 中 4 种不同方法的引用。
package com.runoob.main;
@FunctionalInterface
public interface Supplier<T> {
T get();
}
class Car {
public static Car create(final Supplier<Car> supplier) {
return supplier.get();
}
public static void collide(final Car car) {
System.out.println("Collided " + car.toString());
}
public void follow(final Car another) {
System.out.println("Following the " + another.toString());
}
public void repair() {
System.out.println("Repaired " + this.toString());
}
}
- 构造器引用
final Car car = Car.create( Car::new );
final List< Car > cars = Arrays.asList( car );
- 静态方法引用
cars.forEach( Car::collide );
//Class< T >::new
- 特定类的任意对象的方法引用
//Class::static_method
cars.forEach( Car::repair );
//Class::method
- 特定对象的方法引用
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
//instance::method
Java Stream
Stream是 Java 8新增加的类,用来补充集合类。
既然是补充集合类,那么先来列举一下它们的区别(主要是与迭代器的区别):
1.不储存数据 流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
2.函数式编程 流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
3.延迟操作 流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
4.可以解绑 对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n) 或 findFirst(),这些操作可是实现"短路"(Short-circuiting),访问到有限的元素后就可以返回。
5.纯消费 流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。
在具体讲解方法之前,还是先来看一个栗子:
List<Integer> a = {1,2,3};
List<Integer> b = a.stream()
.filter(i -> i >= 2 )
.collect(Collectors.toList());
我们可以把以上代码分为三部分来具体分析:
1.创建Stream
- 通过集合的stream()方法或者parallelStream(),比如Arrays.asList(1,2,3).stream()。
- 通过Arrays.stream(Object[])方法, 比如Arrays.stream(new int[]{1,2,3})。
- 使用流的静态方法,比如Stream.of(Object[]), IntStream.range(int, int) 或者 Stream.iterate(Object, UnaryOperator),如Stream.iterate(0, n -> n *
- BufferedReader.lines()从文件中获得行的流。
- Files类的操作路径的方法,如list、find、walk等。
- 随机数流Random.ints()。
- 更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法。
2.中间操作
- distinct
distinct保证输出的流中包含唯一的元素,它是通过Object.equals(Object)来检查是否包含相同的元素。
List<String> l = Stream.of("a","b","c","b")
.distinct()
.collect(Collectors.toList());
System.out.println(l); //[a, b, c]
- filter
filter返回的流中只包含满足断言(predicate)的数据。
下面的代码返回流中的偶数集合。
List<Integer> l = IntStream.range(1,10)
.filter( i -> i % 2 == 0)
.boxed()
.collect(Collectors.toList());
System.out.println(l); //[2, 4, 6, 8]
- map
map方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。
map( c -> c*2)
- flatmap
flatmap方法将映射后的流的元素全部放入到一个新的流中。
String poetry = "Where, before me, are the ages that have gone?\n" +
"And where, behind me, are the coming generations?\n" +
"I think of heaven and earth, without limit, without end,\n" +
"And I am all alone and my tears fall down.";
Stream<String> lines = Arrays.stream(poetry.split("\n"));
Stream<String> words = lines.flatMap(line -> Arrays.stream(line.split(" ")));
- limit
limit方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。
List<Integer> l = IntStream.range(1,100).limit(5)
.boxed()
.collect(Collectors.toList());
System.out.println(l);//[1, 2, 3, 4, 5]
- peek
peek方法方法会使用一个Consumer消费流中的元素,但是返回的流还是包含原来的流中的元素。
String[] arr = new String[]{"a","b","c","d"};
Arrays.stream(arr)
.peek(System.out::println) //a,b,c,d
.count();
- sorted
sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。
sorted(Comparator<? super T> comparator)可以指定排序的方式。
对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。
String[] arr = new String[]{"b_123","c+342","b#632","d_123"};
List<String> l = Arrays.stream(arr)
.sorted((s1,s2) -> {
if (s1.charAt(0) == s2.charAt(0))
return s1.substring(2).compareTo(s2.substring(2));
else
return s1.charAt(0) - s2.charAt(0);
})
.collect(Collectors.toList());
System.out.println(l); //[b_123, b#632, c+342, d_123]
- skip
skip返回丢弃了前n个元素的流,如果流中的元素小于或者等于n,则返回空的流。
3.终点操作
- Match
分为三种具体方法,用来检查流中的元素是否满足段言。
public boolean allMatch(Predicate<? super T> predicate)
public boolean anyMatch(Predicate<? super T> predicate)
public boolean noneMatch(Predicate<? super T> predicate)
count
count方法返回流中的元素的数量。collect
这是一个比较重要的终点操作,实现最终对处理过的流的收集。辅助类Collectors
提供了很多的collector,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。find
findAny()返回任意一个元素,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
findFirst()返回第一个元素,如果流为空,返回空的Optional。forEach
forEach遍历流的每一个元素,执行指定的action。
Stream.of(1,2,3,4,5).forEach(System.out::println);
max/min
max返回流中的最大值,
min返回流中的最小值。toArray()
将流中的元素放入到一个数组中。
多说一句~~~~~~~~~
流可以从非线程安全的集合中创建,当流的管道执行的时候,非concurrent数据源不应该被改变。下面的代码会抛出java.util.ConcurrentModificationException异常:
List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
sl.forEach(s -> l.add("three"));
但是使用CopyOnWriteArrayList可以解决这个问题
Optional——一个可以为 null 的容器
Java 8中的Optional<T>是一个可以包含或不可以包含非空值的容器对象,在 Stream API中很多地方也都使用到了Optional。(暂时没有找到特别合适的用法...)
基本方法:
of()
ofNullable()
isPresent()
如果值存在,返回 true,否则返回 falsemap()
orElse()
orElseGet()