Java8新特性实践

Java 8 已经发行好几年了,作为一名Java程序员,再不应用它的美好的新特性肯定要被社会淘汰了。这篇文章,我作为一名Java8新手用代码实践Java8新特性,来探究它的美好。

1. Lambda表达式与Functional接口

Lambda表达式(也称为闭包),它允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据,这一特性和scala语言很像。 在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );//e的类型是由编译器推测出来的

某些情况下lambda的函数体会是多条语句,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。例如:

Arrays.asList( "a", "b", "d" ).forEach( e -> {
    System.out.print( e );
    System.out.print( e );
} );

Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。例如,下面两个代码片段是等价的:

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 ) );

Lambda可能会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。下面两个代码片段是等价的:

Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );
Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> {
            int result = e1.compareTo( e2 );
            return result;
        } );

如何使现有的函数友好地支持lambda。方法是:增加函数式接口的概念。函数式接口就是一个具有一个方法的普通接口。像这样的接口,可以被隐式转换为lambda表达式。java.lang.Runnable与java.util.concurrent.Callable是函数式接口最典型的两个例子。在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java 8增加了一种特殊的注解@FunctionalInterface(Java 8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义:

@FunctionalInterface
public interface Functional {
    void method();
}

需要记住的一件事是:默认方法与静态方法并不影响函数式接口的契约,可以任意使用:

@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();
    default void defaultMethod() {            
    }        
}

其实,lambda表达式本质就是匿名内部类。

        new Thread(()->{
            for (int i = 0; i <10 ; i++) {
                System.out.println("这是"+i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
lambda表达式常见形式
        //无参数单语句
        Runnable rundemo=()-> System.out.println("无参数单语句");
        //单参数单语句,event为javase中的事件类就是ActionListener的实现类
        ActionListener al= event-> System.out.println("单参数单语句");
        //无参数多语句
        Runnable r=()->{
            System.out.println("第一个");
            System.out.println("无参数多语句");
        };
        //多参数单语句
        BinaryOperator<Integer> add=(x,y)->x+y;
        //或者
        BinaryOperator<Integer> add2=(Integer x,Integer y)->x+y;

2. 接口的默认方法与静态方法

Java 8用默认方法与静态方法来扩展接口的声明。默认方法具有方法体,但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。 默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。让我们看看下面的例子:

private interface Defaulable {
    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";
    }
}

Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法。例如:

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() );
}

在JVM中,大量的默认方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……,这些默认方法非常高效。

3. Java 类库的新特性

Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。

3.1 Optional

空指针异常是导致Java应用程序失败的最常见原因。为了解决空指针异常,引入了Optional类,Optional类已经成为Java 8类库的一部分。 Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。更多详情请参考官方文档。 Optional<T>有方法 isPresent()get() 是用来检查其包含的对象是否为空或不是,然后返回它,如:

Optional<SomeType> someValue = someMethod();
if (someValue.isPresent()) { // check
    someValue.get().someOtherMethod(); // retrieve and call
}

可以将Optional看成是需要使用某个T值的方法之间某种中间人或者协调者Mediator,而不只是一个普通对象的包装器。如果你有一个值返回类型T,你有一个方法需要使用这个值,那么你可以让 Optional<T> 处于中间,确保它们之间交互进行,而不必要人工干预。这样,协调者Optional<T>能够照顾T的值提供给你的方法作为输入参数,在这种情况下,如果T是空,可以确保不会出错,这样在T值为空时也可以让一切都正常运作,你也可以让Optional<T>执行其他动作,如执行一段代码块等等,这样它就实际上是语言机制的很好的补充。   下面这个案例涉及到Lambda表达式 方法引用,是将单词流中第一个以"L"开始单词取出,作为返回结果是一个Optional<String>。

使用ifPresent()

Stream<string> names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional<string> longest = names
                                .filter(name -> name.startsWith("L"))
                                .findFirst();
longest.ifPresent(name -> {
            String s = name.toUpperCase();
            System.out.println("The longest name is "+ s);
        });

这里ifPresent() 是将一个Lambda表达式作为输入,T值如果不为空将传入这个lambda。那么这个lambda将不为空的单词转为大写输出显示。在前面names单词流寻找结果中,有可能找不到开始字母为L的单词,返回为空,也可能找到不为空,这两种情况都传入lambda中,无需我们打开盒子自己编写代码来判断,它自动帮助我们完成了,无需人工干预。

使用map()

Stream<string> names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional<string> longest = names
                                .filter(name -> name.startsWith("L"))
                                .findFirst();
Optional<string> lNameInCaps = longest.map(String::toUpperCase);

使用Optional<T>的map方法能够返回另外一个Optional,因为传入map()的参数值也许会导致一个空值。

使用orElse()

Stream<String> names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional<String> longest = names
                                .filter(name -> name.startsWith("Q"))
                                .findFirst();
 String alternate = longest.orElse("Nimrod");
 System.out.println(alternate); //prints out "Nimrod"

如果在T可能空时你需要一个值的话,那么可以使用 orElse(),它能在T值存在的情况下返回这个值,否则返回输入值。

使用orElseGet()

Stream<String> names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
Optional<String> longest = names
                                .filter(name -> name.startsWith("Q"))
                                .findFirst();
 String alternate = longest.orElseGet(() -> {
            // perform some interesting code operation
            // then return the alternate value.
            return "Nimrod";
 });
 System.out.println(alternate);

使用 orElseThrow()

//orElseThrow()是在当遭遇Null时,决定抛出哪个Exception时使用
Stream<String> names = Stream.of("Lamurudu", "Okanbi", "Oduduwa");
 Optional<String> longest = names
                                 .filter(name -> name.startsWith("Q"))
                                 .findFirst();
longest.orElseThrow(NoSuchElementStartingWithQException::new);

总结,你能创建下面三种类型的Optional<T>:

Optional<SomeType> getSomeValue() {
// 返回一个空的Optional类型;
return Optional.empty();
}
 
Optional<SomeType> getSomeValue() {
SomeType value = ...;
// 使用这个方法,值不可以为空,否则抛exception
return Optional.of(value);
}
 
Optional<SomeType> getSomeValue() {
SomeType value = ...;
// 使用这个方法,值可以为空,如果为空返回Optional.empty
return Optional.ofNullable(value);
 
// usage
Optional<SomeType> someType = getSomeValue();

3.2Stream

Stream API极大简化了集合框架的处理(但它的处理的范围不仅仅限于集合框架的处理)。让我们以一个简单的Task类为例进行介绍:

public class Streams  {
    private enum Status {
        OPEN, CLOSED
    };
     
    private static final class Task {
        private final Status status;
        private final Integer points;
        Task( final Status status, final Integer points ) {
            this.status = status;
            this.points = points;
        }
        public Integer getPoints() {
            return points;
        }
        public Status getStatus() {
            return status;
        }
        @Override
        public String toString() {
            return String.format( "[%s, %d]", status, points );
        }
    }
}

Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.

final Collection< Task > tasks = Arrays.asList(
    new Task( Status.OPEN, 5 ),
    new Task( Status.OPEN, 13 ),
    new Task( Status.CLOSED, 8 ) 
);

第一个问题是所有状态为OPEN的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用stream:一串支持连续、并行聚集操作的元素。

final long totalPointsOfOpenTasks = tasks
    .stream()
    .filter( task -> task.getStatus() == Status.OPEN )
    .mapToInt( Task::getPoints )
    .sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );

几个注意事项: 第一,task集合被转换化为其相应的stream表示。然后,filter操作过滤掉状态为CLOSED的task。下一步,mapToInt操作通过Task::getPoints这种方式调用每个task实例的getPoints方法把Task的stream转化为Integer的stream。最后,用sum函数把所有的分数加起来,得到最终的结果。

Stream操作被分成了中间操作与最终操作这两种: 中间操作返回一个新的stream对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始stream中符合给定谓词的所有元素。

像forEach、sum这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。

Stream另一个有价值的地方是能够原生支持并行处理。

final Map< Status, List< Task > > map = tasks
    .stream()
    .collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );

//控制台输出
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}

计算整个集合中每个task分数(或权重)的平均值:

final Collection< String > result = tasks
    .stream()                                        // Stream< String >
    .mapToInt( Task::getPoints )                     // IntStream
    .asLongStream()                                  // LongStream
    .mapToDouble( points -> points / totalPoints )   // DoubleStream
    .boxed()                                         // Stream< Double >
    .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
    .mapToObj( percentage -> percentage + "%" )      // Stream< String> 
    .collect( Collectors.toList() );                 // List< String > 
         
System.out.println( result );

//控制台输出
[19%, 50%, 30%]

注意:Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。

final Path path = new File( filename ).toPath();
try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
    lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}

对一个stream对象调用onClose方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用close()方法时,与关闭相关的处理器就会执行。

3.4. 一些新的操作方式

ForEach

        //map新的遍历方式
        Set<Map.Entry<Status, List<Task>>> entries = collect.entrySet();
        entries.forEach(map-> System.out.println(map.getKey()+"====="+map.getValue()));

4.并行(parallel)数组

Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。

public class ParallelArrays {
    public static void main(String[] args) {
        long[] arrayOfLong = new long [ 20000 ];

        Arrays.parallelSetAll( arrayOfLong,
                index -> ThreadLocalRandom.current().nextInt( 1000000 ) );

        System.out.print("unsorted: ");
        Arrays.stream( arrayOfLong ).limit( 20).forEach(
                i -> System.out.print( i + " " ) );
        System.out.println();

        System.out.print("sorted: ");
        Arrays.parallelSort( arrayOfLong );
        Arrays.stream( arrayOfLong ).limit( 20 ).forEach(
                i -> System.out.print( i + " " ) );

    }
}

打印结果如下:

unsorted: 391090 629642 229354 60426 771326 736856 463120 191727 58792 597735 881800 139717 606935 934037 971280 322328 959308 211004 685634 326106 
sorted: 128 161 217 245 308 331 335 602 608 637 671 678 716 770 773 909 919 926 954 1069

parallelSetAll()方法来对一个有20000个元素的数组进行随机赋值。然后调用parallelSort方法对其进行排序。

以上便是我刚进行的Java8实践,还有很多要学习的,待续!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏九彩拼盘的叨叨叨

学习纲要:JavaScript 数据类型

7910
来自专栏Android相关

Flutter--Dart学习

2011年10月公开。它的开发团队由Google Chrome浏览器V8引擎 (JavaScript引擎)")团队的领导者拉尔斯·巴克主持,目标在于成为下一代结...

35920
来自专栏Linyb极客之路

JVM 方法内联

调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。

24040
来自专栏测试开发架构之路

C++之类和对象的特性

简介:C++并不是一个纯粹的面向对象的语言,而是一种基于过程和面向对象的混合型的语言。 凡是以类对象为基本构成单位的程序称为基于对象的程序,再加上抽象、封装、...

36360
来自专栏软件开发

JavaSE学习总结(四)——Java面向对象十分钟入门

面向对象编程(Object Oriented Programming,OOP)是一种计算机模拟人类的自然思维方式的编程架构技术,解决了传统结构化开发方法中客观...

31570
来自专栏王磊的博客

《JavaScript权威指南》——JavaScript核心

前言 这本由David Flanagan著作,并由淘宝前端团队译的《JavaScript权威指南》,也就是我们俗称的“犀牛书”,算是JS界公认的“圣经”了。本书...

38490
来自专栏我的小碗汤

这个问题你能答对吗?

首先,还是给大家说声抱歉,由于微信限制,前两天抽奖的好友请求还没有全部通过验证,这两天都会通过并拉大家进抽奖群的,还请大家海涵。

8920
来自专栏Crossin的编程教室

大家都是拷贝,凭什么你这么秀?

但看了前面的文章后你应该知道,这样的赋值只相当于增加了一个标签,并没有新的对象产生:

10220
来自专栏大数据

Zzreal的大数据笔记-ScalaDay02

昨天整理了一下Scala的一些基本内容,不是很全面,不过作为学习Spark的基础足够了, 如果需要系统的学习Scala,建议还是去菜鸟教程一步步的看下来会比较条...

200100
来自专栏JetpropelledSnake

Python入门之面向对象之类继承与派生

本章内容     一、继承     二、抽象类     三、继承的实现原理 ==========================================...

36080

扫码关注云+社区

领取腾讯云代金券