目录:
Java8新特性概要Lamda表达式函数接口方法引用Stream1 Stream工作方式2 不同类型的streams3 操作的顺序4 重复使用Stream5 一些重要的操作总结
Java 8 在 2014年3月18日进行了发布,相较之前的版本,有了许多的改变,无论是从语法上还是类库上,都有了很多的变化,更加有利于程序的编写,本文用于此次小组分享,从其中个人认为的较重要的几个方面进行阐述。 总体来说,有了以下几个方面: 1. Lamda表达式 2. 函数接口 3. 类库的增加 4. 工具类
Lamda表达式也被称之为闭包(closures),利用该特性,我们可以将一个函数当做方法的参数进行传递,也就是将代码当做数据来看待。 由于以上原因,有人将Lamda表达式当做是匿名内部类的语法糖(Syntactic suger)来看待,但从虚拟机实现角度来看,并不是如此,因为Lamda表达式在编译的时候,并不会生成xxx$1.class的匿名类,而是通过动态绑定,在运行的时候在调用,因此避免了在编译时生成从而影响jvm的加载速度。 Lamda表达式没有名称,但是有参数列表,函数体,返回类型并且能够抛出异常,语法如下形式:
(parameters) -> {statements}(parameters) -> statements(parameters) -> exPRession举例:
() -> Math.PI * 2.0 (String s) -> s.length() (int i0, int i1) -> i0 + i1 (int x, int y) -> { return x + y; }使用:
//1. 省略类型(i, j) ->{System.out.println(i + j)};//2. 参数数量为1时,省略括号//行数体只有1行时,可以省略大括号i -> arrayList::add//3. 函数体多行的时候需要用大括号包围(String idStr) -> {Long id = Long.valueOf(idStr);try { TEliteUser user = eliteAdapter.getUserById(id); } catch (Exception e) { log.error("", e) } userList.add(user); };//4. 函数体只有一行且有返回值得可以省略return,此时大括号需要一并省略(i, j) -> i - j;//5. 用于Lamda的变量不可改变int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); // OK // 编译错误 // Local variable portNumber defined in an enclosing scope must be final or effectively final int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); // NG portNumber = 1338; // 通过数组实现 final int[] wrappedNumber = new int[] { 1337 }; Runnable r = () -> System.out.println(wrappedNumber[0]); // OK wrappedNumber[0] = 1338;其中所说的Lamda表达式中所引用的必须是不可变的类型,在编译器实现时是通过隐式的方式将类变量或局部变量进行转换的。也就是以下两种方法是等效的:
//隐式String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );//显式final String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );还有一点是,在使用Lamda表达式操作集合时,是无法对集合进行元素的删除的,否则会在产生RunTime Exception:
//Exception in thread "main" java.util.ConcurrentModificationException List<String> names = new ArrayList<String>(){{ add("Zhao"); add("Qian"); add("Sun"); }}; names.forEach(name -> { if (Objects.equals(name, "Sun")); names.remove(name); });当然,这并不是Lamda表达式的问题,使用foreach的话也会遇到同样的问题,在要对集合进行修改的时候,请使用iterator迭代器进行。
为了使Lamda表达式与原有功能友好兼容,增加了函数接口:只有一个方法的接口(比如java.lang.Runnable和java.util.concurrent.Callable)。通过函数接口,接口能够隐式的转换为Lamda表达式。 为了确保函数接口中只有一个方法,java8中增加了一个注释@FunctionalInterface来确保这点。 Java现有接口中均已添加该注释,比如Runnable函数:
@FunctionalInterfacepublic interface Runnable { public abstract void run();}不过需要注意的是,默认方法和静态方法并不会违背函数接口。比如Java8中引入的Consumer接口的定义:
@FunctionalInterfacepublic interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; }}关于接口的默认方法和静态方法: 在关于java的讨论中,抽象类和接口之间的相似性和区别一直是一个很经典的问题。而有趣的是,Oracle在java8中向接口中引入了默认方法和静态方法,以此来缩小接口和抽象类的区别。 * 默认方法: 默认方法允许我们在接口里添加新的方法,并不会破坏与实现该接口之前代码的兼容性。也就是并不要求实现该接口的类实现该方法。使用默认方法只需要在方法前加上default关键字。 * 静态方法: Java8同样在接口中定义了静态方法,使用关键字static来进行修饰,默认是public修饰符,所以可以省略,使用方法与在class中定义静态方法相同。建立了方法与接口之间的联系。 以下的例子同时包含了默认方法和静态方法:
private interface Defaulable { // Interfaces now allow default methods, the implementer may or // may not implement (override) them. default String notRequired() { return "Default implementation"; }}private static class DefaultableImpl implements Defaulable {}private static class OverridableImpl implements Defaulable { @Override public String notRequired() { return "Overridden implementation"; }}private interface DefaulableFactory { // Interfaces now allow static methods static Defaulable create( Supplier< Defaulable > supplier ) { return supplier.get(); }}public static void main( String[] args ) { Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new ); System.out.println( defaulable.notRequired() ); defaulable = DefaulableFactory.create( OverridableImpl::new ); System.out.println( defaulable.notRequired() );}为了使Lamda表达式更加简洁,Java8同样引入了方法引用。方法引用提供了一个很有用的语义来直接访问类或实例的方法。 比如我们定义了如下的类:
public class Car {//Supplier<T>为Java8引入的函数接口,不接受参数,返回T 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(() -> {return new Car();});//方法引用final Car car = Car.create(Car::new);可见,方法引用使得Lamda表达式简洁很多。方法引用具有四种类型:Class::new, Class::static_method, Class::method以及instance::method。例子如下:
//Class::newList<Car> cars = Arrays.asList(Car.create(Car::new));Car car = Car.create(Car::new);//Class::static_methodcars.forEach(Car::collide);//Class::methodcars.forEach(Car::repair);//instance::methodcars.forEach(car::follow);引入了Stream API(java.util.stream),从而与Lamda共同组成了Java的函数式编程,目的是简化,整洁复杂的代码编写,从而调高生产率。 Stream旨在简化基于集合的操作,专注于对集合对象进行各种便利、高效的聚合操作,或者批量数据操作。在原来的对集合操作时,只能通过对集合Iterator或者foreach循环来进行便利操作,非常笨拙,而通过Stream和函数编程,能够极大的简化该过程。
以下例子展示了stream的工作方式:
List<String> myString = Arrays.asList("a1", "a2", "c", "c2", "c1");myString.stream().filter(s -> s.startsWith("c")).map(String::toUpperCase).sorted().forEach(System.out::println);stream的操作分为两种,要不是中间操作,要不是终点操作。中间操作返回一个新的Stream。这些中间操作是延迟的,执行一个中间操作比如filter实际上不会真的做过滤操作,而是创建一个新的Stream,当这个新的Stream被遍历的时候,它里头会包含有原来Stream里符合过滤条件的元素。而终点操作则不返回或者返回一个非stream的结果。在以上例子中,filter, map, sorted均是中间操作,而forEach则是终点操作。以上的对于stream的操作,我们称之为操作管道(Operation pipeline)。在stream上所有的操作可以通过查看javadoc来进行查看。在一下的文章中,我们会就其中最重要几个函数进行介绍。 需要注意的是,一般stream操作均会和Lamda表达式,函数接口和方法引用等结合起来,并且是非引用(non-interfering)的。
stream可以是不同的来源的数据,不够大部分的时候我们用它来处理集合的问题。通过stream()和parallelStream()来分别构造同步或异步的stream。 我们可以通过如下的形式来构建同步的stream,异步parallelStream只是在实现的线程上有所区别。
//通过集合的stream方法List<String> myString = Arrays.asList("a1", "a2", "c", "c2", "c1");myString.stream().findFirst().ifPresent(System.out::println);//通过Stream.of()方法Stream.of("a1", "a2", "c", "c2","c1").findFirst().ifPresent(System.out::println);需要注意的是,对于不同的原生数据类型(Primitive DataType),Stream也有相对应的数据类型,IntStream,LongStream, DoubleStream等,与Stream一样,他们都是BaseStream<T,BaseStream<T>>的实现。 原生类型的Stream与普通对象的区别有一下几点(以IntStream为例): 1. IntFunction代替Function<T,R>(接受一个T类型参数,返回R类型参数),IntPredicate代替Predictae<T>(接受一个T类型参数,返回boolean值)… 2. 支持一些附加的终点操作,比如sum()或者average()。
Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1).average().ifPresent(System.out::println);3.普通Stream<T>与原生类型Stream之间的转换通过mapToInt(Function<T, Integer> mapper)和mapToObj(<Interger, T> mapper)转换。
//regular steam to intStreamStream.of("a1", "a2", "a3").map(s -> s.substring(1)).mapToInt(Integer::parseInt) .max().ifPresent(System.out::println);// intStream to regular Stream IntStream.range(1, 4).mapToObj(i -> "a" + i).forEach(System.out::println);//first Stream<Double> to int, then int to regularStream.of(1.0, 2.0, 3.0).mapToInt(Double::intValue).mapToObj(i -> "a" + i).forEach(System.out::println);对于stream来讲,操作管道的顺序是串行,垂直的(vertically),而不是水平的(horizontally)。为了理解这句话,我们看一下这个例子:
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> { System.out.println("filter: " + s); return true; }).forEach(s -> System.out.println("forEach: " + s));//结果是:filter: d2forEach: d2filter: a2forEach: a2filter: b1forEach: b1filter: b3forEach: b3filter: cforEach: c由此可见,操作的顺序是一个接着一个元素顺序进行的,也就是当”d2”元素全部操作完成后,”a2”才会继续进行。 这样顺序的一个好处是,可以减少进行判断的次数:
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); }); //结果:map: d2anyMatch: D2map: a2anyMatch: A2对于anyMatch来讲,当匹配到a2后,就不在进行后续的操作,从而减少了操作的次数。 由以上分析我们可以看出操作管道的顺序会影响到计算的性能,比如以下这个例子:
//order1Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }) .forEach(s -> System.out.println("forEach: " + s));//结果// map: d2// filter: D2// map: a2// filter: A2// forEach: A2// map: b1// filter: B1// map: b3// filter: B3// map: c// filter: C//order2Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s));//结果// filter: d2// filter: a2// map: a2// forEach: A2// filter: b1// filter: b3// filter: c可见,当我们更换了map和filter的操作顺序后,执行的次数也发生了变化,所以说顺序会影响整个操作管道执行的性能。
Stream的终结是以终点操作来标识结束的,也就是一旦调用了终点方法,那么这个stream就会关闭,不能再次使用。
Stream<String> stream =Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));stream.anyMatch(s -> true); // okstream.noneMatch(s -> true); // exception解决方法就是每次重新构造新的stream操作链来进行。比如我们可以构造一个stream supplier来每次获得新的stream:
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("a"));streamSupplier.get().anyMatch(s -> true); // okstreamSupplier.get().noneMatch(s -> true); // ok所有Stream操作管道支持的操作均在javadoc中列出。在上面我们已经使用了包括map,filter在内的一些操作,在下面我们将会介绍包括collect,reduce这两个操作。
CollectCollect是一个非常常用的终点操作,能够将stream转换为各种集合,比如List, Map或者Set。Collect接受Collector为参数,而Collector由四部分组成:Supplier,Accumulator,Combiner,Finisher。虽然Collector很复杂,但是我们可以通过框架类Collector来获得,在大多数情况下并不需要我们手动实现。 比如构返回一个List, Set, Map只需:
//返回ListList<Person> filtered = persons.stream().filter(p -> p.name.startsWith("P")).collect(Collectors.toList());System.out.println(filtered);//返回SetSet<Person> filtered = persons.stream().filter(p -> p.name.startsWith("P")).collect(Collectors.toSet());System.out.println(filtered);//返回MapMap<Integer, List<Person>> personsByAge = persons.stream().collect(Collectors.groupingBy(p -> p.age));personsByAge.forEach((age, p) -> System.out.format("age %s: %s/n", age, p));//结果// age 18: [Max]// age 23: [Peter, Pamela]// age 12: [David]Collectors能够做的功能远远不止这些,比如还能够计算平均值:
Double averageAge = persons.stream().collect(Collectors.averagingInt(p -> p.age));System.out.println(averageAge);计算统计数据summaryStatistics:
IntSummaryStatistics ageSummary =persons.stream().collect(Collectors.summarizingInt(p -> p.age));System.out.println(ageSummary);// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}连接字符串:
String phrase = persons.stream().filter(p -> p.age >= 18).map(p -> p.name).collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));System.out.println(phrase);// In Germany Max and Peter and Pamela are of legal age.对于映射到Map来讲,需要传递三个函数接口:分别是key和value的Function,以及value合并的BinaryOperation:
Map<Integer, String> map = persons.stream().collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2));System.out.println(map);// {18=Max, 23=Peter;Pamela, 12=David}ReduceReduce操作正如名字所言,是将stream中的各个数据结合为一个最终结果的方法。总共有三种reduce操作:
Optional<T> reduce(BinaryOperator<T> accumulator); T reduce(T identity, BinaryOperator<T> accumulator); <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);我们分别来看一下。 第一种通过accumulator将stream中的所有元素均生成一个最终数据,如下所示:
persons.stream().reduce((p1, p2) -> p1.age > p2.age ? p1 : p2).ifPresent(System.out::println);以上例子返回的是依据age得到的结果,函数接口使用的是BinaryOperator<Person>,该函数接口接受两个相同类型的参数,同时返回该类型的参数。返回的数据是Optional<Person>类型。 第二种接受一个变量identity和同样的accumulator。identity用于保存累加的结果。比如我们可以通过以下的方式来获取一个新的累加的人:
Person result = persons.stream().reduce(new Person("", 0), (p1, p2) -> { p1.age += p2.age; p1.name += p2.name; return p1; });System.out.format("name=%s; age=%s", result.name, result.age);// name=MaxPeterPamelaDavid; age=76第三种接受三个变量,一个identity用于保存累加结果,一个函数接口accumulator用于计算累加方式,还有一个函数接口combiner用于计算两个accumulator计算得到的值。也就是说,combiner函数接口主要是用于parallel并行方法的。
Integer ageSum = persons.stream().reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2);System.out.println(ageSum); // 76为了印证以上的说法,可以如下测试:
Integer ageSum = persons.stream().reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s/n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s/n", sum1, sum2); return sum1 + sum2; });// accumulator: sum=0; person=Max// accumulator: sum=18; person=Peter// accumulator: sum=41; person=Pamela// accumulator: sum=64; person=David可见,串行的时候并没有用到combiner函数接口,而当采用parallelStream()转换为并行时,又有如下的结果:
Integer ageSum = persons.parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s/n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s/n", sum1, sum2); return sum1 + sum2; });// accumulator: sum=0; person=Pamela// accumulator: sum=0; person=David// accumulator: sum=0; person=Max// accumulator: sum=0; person=Peter// combiner: sum1=18; sum2=23// combiner: sum1=23; sum2=12// combiner: sum1=41; sum2=35java8无论是Lamda表达式,函数接口,方法引用还是Stream类库的加入,目的都是将函数编程的便利引入到java中来,而这些特性的加入也是的编程更加的简洁与便利。而这些内容还需要我们在以后的实践中不断的去试错与尝试,才能够深入体会到其中真谛。 最后感谢以下资源的贡献。
Java8特性官方页1 Java8 features tutorial2 Java8特性,终极手册3 深入浅出Lamda表达式4 Java8默认方法5 Java8 Stream Tutorial6 Javadoc7
新闻热点
疑难解答