Java基础20:Java8新特性终极指南(上篇)

2019-07-03 00:00:00 新特性 终极 上篇

毫无疑问,Java 8发行版是自Java 5(发行于2004,已经过了相当一段时间了)以来最具革命性的版本。Java 8 为Java语言、编译器、类库、开发工具与JVM(Java虚拟机)带来了大量新特性。在这篇教程中,我们将一一探索这些变化,并用真实的例子说明它们适用的场景。

本文由以下几部分组成,它们分别涉及到Java平台某一特定方面的内容:

Java语言 编译器 类库 工具 Java运行时(JVM)

本文参考http://www.importnew.com/11908.html

具体代码在我的GitHub中可以找到

https://github.com/h2pl/MyTech

喜欢的话麻烦点一下星哈谢谢。

文章首发于我的个人博客:

https://h2pl.github.io/2018/05/06/javase20

更多关于Java后端学习的内容请到我的CSDN博客上查看:

https://blog.csdn.net/a724888

这是一个Java8新增特性的总结图。接下来让我们一次实践一下这些新特性吧

《Java基础20:Java8新特性终极指南(上篇)》
《Java基础20:Java8新特性终极指南(上篇)》

Java语言新特性

Lambda表达式

Lambda表达式(也称为闭包)是整个Java 8发行版中最受期待的在Java语言层面上的改变,Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据:函数式程序员对这一概念非常熟悉。在JVM平台上的很多语言(Groovy,Scala,……)从一开始就有Lambda,但是Java程序员不得不使用毫无新意的匿名类来代替lambda。

关于Lambda设计的讨论占用了大量的时间与社区的努力。可喜的是,最终找到了一个平衡点,使得可以使用一种即简洁又紧凑的新方式来构造Lambdas。在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。例如:

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

请注意参数e的类型是由编译器推测出来的。同时,你也可以通过把参数类型与参数包括在括号中的形式直接给出参数的类型:

Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( 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是函数式接口最典型的两个例子。

在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。

为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java8增加了一种特殊的注解@FunctionalInterface(Java8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义:

@FunctionalInterface public interface Functional { void method(); } 需要记住的一件事是:默认方法与静态方法并不影响函数式接口的契约,可以任意使用:

@FunctionalInterface public interface FunctionalDefaultMethods { void method();

default void defaultMethod() {            
}

} Lambda是Java 8最大的卖点。它具有吸引越来越多程序员到Java平台上的潜力,并且能够在纯Java语言环境中提供一种优雅的方式来支持函数式编程。更多详情可以参考官方文档。

下面看一个例子:

public class lambda和函数式编程 {
    @Test
    public void test1() {
        List names = Arrays.asList("peter", "anna", "mike", "xenia");

        Collections.sort(names, new Comparator<String>() {
            @Override
            public int compare(String a, String b) {
                return b.compareTo(a);
            }
        });
        System.out.println(Arrays.toString(names.toArray()));
    }

    @Test
    public void test2() {
        List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

        Collections.sort(names, (String a, String b) -> {
            return b.compareTo(a);
        });

        Collections.sort(names, (String a, String b) -> b.compareTo(a));

        Collections.sort(names, (a, b) -> b.compareTo(a));
        System.out.println(Arrays.toString(names.toArray()));
    }

}

    static void add(double a,String b) {
        System.out.println(a + b);
    }
    @Test
    public void test5() {
        D d = (a,b) -> add(a,b);//        interface D {//            void get(int i,String j);//        }        //这里要求,add的两个参数和get的两个参数吻合并且返回类型也要相等,否则报错//        static void add(double a,String b) {//            System.out.println(a + b);//        }    }

    @FunctionalInterface
    interface D {
        void get(int i,String j);
    }

函数式接口

所谓的函数式接口就是只有一个抽象方法的接口,注意这里说的是抽象方法,因为Java8中加入了默认方法的特性,但是函数式接口是不关心接口中有没有默认方法的。 一般函数式接口可以使用@FunctionalInterface注解的形式来标注表示这是一个函数式接口,该注解标注与否对函数式接口没有实际的影响, 不过一般还是推荐使用该注解,就像使用@Override注解一样。

lambda表达式是如何符合 Java 类型系统的?每个lambda对应于一个给定的类型,用一个接口来说明。而这个被称为函数式接口(functional interface)的接口必须仅仅包含一个抽象方法声明。每个那个类型的lambda表达式都将会被匹配到这个抽象方法上。因此默认的方法并不是抽象的,你可以给你的函数式接口自由地增加默认的方法。

我们可以使用任意的接口作为lambda表达式,只要这个接口只包含一个抽象方法。为了保证你的接口满足需求,你需要增加@FunctionalInterface注解。编译器知道这个注解,一旦你试图给这个接口增加第二个抽象方法声明时,它将抛出一个编译器错误。

下面举几个例子

public class 函数式接口使用 {
    @FunctionalInterface    interface A {
        void say();
        default void talk() {

        }
    }
    @Test    public void test1() {
        A a = () -> System.out.println("hello");
        a.say();
    }

    @FunctionalInterface    interface B {
        void say(String i);
    }
    public void test2() {
        //下面两个是等价的,都是通过B接口来引用一个方法,而方法可以直接使用::来作为方法引用        B b = System.out::println;
        B b1 = a -> Integer.parseInt("s");//这里的a其实换成别的也行,只是将方法传给接口作为其方法实现        B b2 = Integer::valueOf;//i与方法传入参数的变量类型一直时,可以直接替换        B b3 = String::valueOf;
        //B b4 = Integer::parseInt;类型不符,无法使用
    }
    @FunctionalInterface    interface C {
        int say(String i);
    }
    public void test3() {
        C c = Integer::parseInt;//方法参数和接口方法的参数一样,可以替换。        int i = c.say("1");
        //当我把C接口的int替换为void时就会报错,因为返回类型不一致。        System.out.println(i);
        //综上所述,lambda表达式提供了一种简便的表达方式,可以将一个方法传到接口中。        //函数式接口是只提供一个抽象方法的接口,其方法由lambda表达式注入,不需要写实现类,        //也不需要写匿名内部类,可以省去很多代码,比如实现runnable接口。        //函数式编程就是指把方法当做一个参数或引用来进行操作。除了普通方法以外,静态方法,构造方法也是可以这样操作的。    }
}

请记住如果@FunctionalInterface 这个注解被遗漏,此代码依然有效。

方法引用

Lambda表达式和方法引用

有了函数式接口之后,就可以使用Lambda表达式和方法引用了。其实函数式接口的表中的函数描述符就是Lambda表达式,在函数式接口中Lambda表达式相当于匿名内部类的效果。 举个简单的例子:

public class TestLambda {

public static void execute(Runnable runnable) {
    runnable.run();
}
public static void main(String[] args) {
    //Java8之前    execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("run");
        }
    });

    //使用Lambda表达式    execute(() -> System.out.println("run"));
}

}

可以看到,相比于使用匿名内部类的方式,Lambda表达式可以使用更少的代码但是有更清晰的表述。注意,Lambda表达式也不是完全等价于匿名内部类的, 两者的不同点在于this的指向和本地变量的屏蔽上。

方法引用可以看作Lambda表达式的更简洁的一种表达形式,使用::操作符,方法引用主要有三类:

指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt);

指向任意类型实例方法的方法引用(例如String的length方法,写作String::length);

指向现有对象的实例方法的方法引用(例如假设你有一个本地变量localVariable用于存放Variable类型的对象,它支持实例方法getValue,那么可以写成localVariable::getValue)。

举个方法引用的简单的例子:

Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);

//使用方法引用

Function<String, Integer> stringToInteger = Integer::parseInt;

方法引用中还有一种特殊的形式,构造函数引用,假设一个类有一个默认的构造函数,那么使用方法引用的形式为:

Supplier<SomeClass> c1 = SomeClass::new;SomeClass s1 = c1.get();

//等价于

Supplier<SomeClass> c1 = () -> new SomeClass();
SomeClass s1 = c1.get();

如果是构造函数有一个参数的情况:

Function<Integer, SomeClass> c1 = SomeClass::new;SomeClass s1 = c1.apply(100);

//等价于

Function<Integer, SomeClass> c1 = i -> new SomeClass(i);SomeClass s1 = c1.apply(100);

接口的默认方法

Java 8 使我们能够使用default 关键字给接口增加非抽象的方法实现。这个特性也被叫做 扩展方法(Extension Methods)。如下例所示:

public class 接口的默认方法 {
    class B implements A {//        void a(){}实现类方法不能重名    }
    interface A {
        //可以有多个默认方法        public default void a(){
            System.out.println("a");
        }
        public default void b(){
            System.out.println("b");
        }
        //报错static和default不能同时使用//        public static default void c(){//            System.out.println("c");//        }    }
    public void test() {
        B b = new B();
        b.a();

    }
}

默认方法出现的原因是为了对原有接口的扩展,有了默认方法之后就不怕因改动原有的接口而对已经使用这些接口的程序造成的代码不兼容的影响。 在Java8中也对一些接口增加了一些默认方法,比如Map接口等等。一般来说,使用默认方法的场景有两个:可选方法和行为的多继承。

默认方法的使用相对来说比较简单,唯一要注意的点是如何处理默认方法的冲突。关于如何处理默认方法的冲突可以参考以下三条规则:

类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。

如果无法依据第一条规则进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口。即如果B继承了A,那么B就比A更具体。

最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。那么如何显式地指定呢:

public class C implements B, A {
 
    public void hello() {
        B.super().hello();    
    }
 
}

使用X.super.m(..)显式地调用希望调用的方法。

Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。默认方法使接口有点像Traits(Scala中特征(trait)类似于Java中的Interface,但它可以包含实现代码,也就是目前Java8新增的功能),但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。

默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。让我们看看下面的例子:

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";
    }
}

Defaulable接口用关键字default声明了一个默认方法notRequired(),Defaulable接口的实现者之一DefaultableImpl实现了这个接口,并且让默认方法保持原样。Defaulable接口的另一个实现者OverridableImpl用自己的方法覆盖了默认方法。

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

这个程序的控制台输出如下:

Default implementation Overridden implementation 在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。默认方法允许继续使用现有的Java接口,而同时能够保障正常的编译过程。这方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……

尽管默认方法非常强大,但是在使用默认方法时我们需要小心注意一个地方:在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法,因为默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。更多详情请参考官方文档

重复注解

自从Java 5引入了注解机制,这一特性就变得非常流行并且广为使用。然而,使用注解的一个限制是相同的注解在同一位置只能声明一次,不能声明多次。Java 8打破了这条规则,引入了重复注解机制,这样相同的注解可以在同一地方声明多次。

重复注解机制本身必须用@Repeatable注解。事实上,这并不是语言层面上的改变,更多的是编译器的技巧,底层的原理保持不变。让我们看一个快速入门的例子:

package com.javacodegeeks.java8.repeatable.annotations;
 import java.lang.annotation.ElementType;import java.lang.annotation.Repeatable;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;
 public class RepeatingAnnotations {
    @Target( ElementType.TYPE )    @Retention( RetentionPolicy.RUNTIME )    public @interface Filters {
        Filter[] value();
    }
     
    @Target( ElementType.TYPE )    @Retention( RetentionPolicy.RUNTIME )    @Repeatable( Filters.class )    public @interface Filter {
        String value();
    };
     
    @Filter( "filter1" )    @Filter( "filter2" )    public interface Filterable {        
    }
     
    public static void main(String[] args) {
        for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) {
            System.out.println( filter.value() );
        }
    }
}

正如我们看到的,这里有个使用@Repeatable( Filters.class )注解的注解类Filter,Filters仅仅是Filter注解的数组,但Java编译器并不想让程序员意识到Filters的存在。这样,接口Filterable就拥有了两次Filter(并没有提到Filter)注解。

同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型(请注意Filterable.class.getAnnotation( Filters.class )经编译器处理后将会返回Filters的实例)。

程序输出结果如下:

filter1 filter2 更多详情请参考官方文档

    原文作者:黄小斜
    原文地址: https://zhuanlan.zhihu.com/p/40815920
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章