JavaRush /Java 博客 /Random-ZH /Java 8 功能 – 终极指南(第 1 部分)
0xFF
第 9 级
Донецк

Java 8 功能 – 终极指南(第 1 部分)

已在 Random-ZH 群组中发布
文章《Java 8 Features – The ULTIMATE Guide》 翻译的第一部分。第二部分在这里(链接可能会更改)。 Java 8 功能 – 终极指南(第 1 部分)- 1 编者注:本文是在 Java 8 向公众开放时发表的,所有迹象都表明这确实是一个主要版本。在这里,我们提供了丰富的 Java Code Geeks 指南,例如《Playing with Java 8 – Lambdas and Concurrency》《Java 8 Date and Time API Guide: LocalDateTime》《Java 8 Era 中的 Abstract Class vs. Interface》。我们还链接到其他来源的 15 篇必读 Java 8 教程。当然,我们也会看到一些缺点,例如Java 8 的黑暗面。因此,是时候将 Java 8 的所有主要功能集中在一处,以方便您使用。享受!

一、简介

毫无疑问,Java 8 的发布是自 Java 5(很久以前发布,2004 年发布)以来最伟大的事件。它在语言、编译器、库、工具和 JVM(Java 虚拟机)方面为 Java 带来了许多新功能。在本教程中,我们将了解这些更改,并通过现实生活中的示例演示不同的用例。该指南由几个部分组成,每个部分都涉及平台的一个特定方面:
  • 语言
  • 编译器
  • 图书馆
  • 工具
  • 运行时环境(JVM)

2.Java 8的新特性

无论如何,Java 8 是一个主要版本。可以说,之所以花了这么长时间,是因为实现了每个 Java 开发人员都在寻找的功能。在本节中,我们将介绍其中的大部分内容。

2.1. Lambda 和函数式接口

Lambda(也称为私有或匿名方法)是整个 Java 8 版本中最大且最受期待的语言变化。它们允许我们将功能指定为方法参数(通过在其周围声明一个函数),或将代码指定为数据:每个功能开发人员都熟悉的概念。编程_ JVM 平台上的许多语言(Groovy、Scala等)从第一天起就拥有 lambda,但 Java 开发人员别无选择,只能通过匿名类来表示 lambda。lambda 设计争论花费了公众大量的时间和精力。但最终找到了妥协方案,从而导致了新的简洁设计的出现。在最简单的形式中,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字段是否被访问,隐式地使调用有效)。例如,这两个片段是等效的:
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 可以返回一个值。返回类型将由编译器确定。return如果 lambda 主体由一行组成,则不需要声明。下面的两个代码片段是等效的:
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 库中的所有现有接口都收到了 @FunctionalInterface 注释)。让我们看一下函数式接口的简单定义:
@FunctionalInterface
public interface Functional {
    void method();
}
有一点需要记住:默认方法和静态方法不违反函数式接口的原则,可以声明:
@FunctionalInterface
public interface FunctionalDefaultMethods {
    void method();

    default void defaultMethod() {
    }
}
Lambda 是 Java 8 中最受欢迎的功能。它们有潜力吸引更多开发人员使用这个出色的平台,并为纯 Java 中的功能提供智能支持。更详细的信息请参考官方文档

2.2. 默认接口和静态方法

Java 8 通过两个新概念扩展了接口的定义:默认方法和静态方法。默认方法使接口与特征有些相似,但用途略有不同。它们允许您向现有接口添加新方法,而不会破坏这些接口以前编写的版本的向后兼容性。默认方法和抽象方法之间的区别在于,抽象方法必须实现,而默认方法则不需要。相反,每个接口必须提供一个所谓的默认实现,并且所有后代都将默认接收它(如果需要,可以覆盖此默认实现)。让我们看下面的例子。
private interface Defaulable {
    // Интерфейсы теперь разрешают методы по умолчанию,
    // клиент может реализовывать  (переопределять)
    // or не реализовывать его
    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声明默认方法。其中一个类实现了此接口,保留默认方法不变。另一个类覆盖默认实现并提供自己的实现。Java 8 中引入的另一个有趣的功能是接口可以声明静态方法(并提供静态方法的实现)。这是一个例子: notRequired()defaultDefaultableImplOverridableImpl
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.Collectionstream(), parallelStream(), forEach(), removeIf(), ... 尽管默认方法功能强大,但应谨慎使用:在声明默认方法之前,您应该三思而行是否真的有必要,因为这可能会导致复杂层次结构中的编译歧义和错误。有关更详细的信息,请参阅文档

2.3. 参考方法

引用方法实现有用的语法来引用 Java 类或对象(实例)的现有方法或构造函数。与 lambda 表达式一起,引用方法使语言构造紧凑、简洁,使其基于模板。下面是一个类Car作为不同方法定义的示例,让我们重点介绍一下支持的四种引用方法类型:
public static 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() );
    }
}
Class::new第一个引用方法是对具有语法的构造函数或泛型的替代方法 的引用Class< T >::new。请注意,构造函数没有参数。
final Car car = Car.create( Car::new );
final List<Car> cars = Arrays.asList( car );
第二个选项是使用语法引用静态方法Class::static_method。请注意,该方法仅采用一个类型为 的参数Car
cars.forEach( Car::collide );
第三种类型是对某种类型的任意对象实例的方法的引用,其语法为Class::method。请注意,该方法不接受任何参数。
cars.forEach( Car::repair );
最后,第四种类型是对特定类实例方法的引用,语法为instance::method。请注意,该方法仅接受一个类型为 的参数Car
final Car police = Car.create( Car::new );
cars.forEach( police::follow );
作为 Java 程序运行所有这些示例会产生以下控制台输出(类引用Car可能会有所不同):
Collided com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Repaired com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
Following the com.javacodegeeks.java8.method.references.MethodReferences$Car@7a81197d
更详细的信息和参考方法详情请参考官方文档

2.4. 重复注释

自从Java 5引入了对注解的支持以来,这个特性已经变得非常流行并且使用非常广泛。然而,使用注释的限制之一是同一注释不能在同一位置声明多次。Java 8 打破了这个规则并引入了重复的注释。这允许相同的注释在声明它们的位置重复多次。重复的注释应该使用 annotation 来注释自己@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() );
        }
    }
}
正如我们所看到的,该类Filter使用@Repeatable(Filters. class)进行注释。Filters只是注释的所有者Filter,但 Java 编译器试图向开发人员隐藏它们的存在。因此,该接口Filterable包含声明Filter两次的注释(不提及Filters)。Reflection API 还提供了一种getAnnotationsByType()用于返回某种类型的重复注释的新方法(请记住 Filterable..getAnnotation class( Filters. class) 将返回编译器注入的实例Filters)。程序的输出将如下所示:
filter1
filter2
更详细的信息请参考官方文档

2.5. 改进的类型推断

Java 8 编译器已获得许多类型推断改进。在许多情况下,编译器可以定义显式类型参数,从而使代码更简洁。让我们看一个例子:
package com.javacodegeeks.java8.type.inference;

public class Value<T> {
    public static<T> T defaultValue() {
        return null;
    }

    public T getOrDefault( T value, T defaultValue ) {
        return ( value != null ) ? value : defaultValue;
    }
}
这是 type 的用法Value<String>
package com.javacodegeeks.java8.type.inference;

public class TypeInference {
    public static void main(String[] args) {
        final Value<String> value = new Value<>();
        value.getOrDefault( "22", Value.defaultValue() );
    }
}
类型参数Value.defaultValue()是自动确定的,不需要显式提供。在 Java 7 中,同一示例将无法编译,必须重写为 <NOBR>Value.<String>defaultValue()</NOBR>。

2.6。扩展注释支持

Java 8 扩展了可以使用注释的上下文。如今,几乎任何东西都可以有注释:局部变量、泛型类型、超类和实现的接口,甚至方法异常。下面提供了一些示例:
package com.javacodegeeks.java8.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collection;

public class Annotations {
    @Retention( RetentionPolicy.RUNTIME )
    @Target( { ElementType.TYPE_USE, ElementType.TYPE_PARAMETER } )
    public @interface NonEmpty {
    }

    public static class Holder<@NonEmpty T> extends @NonEmpty Object {
        public void method() throws @NonEmpty Exception {
        }
    }

    @SuppressWarnings( "unused" )
    public static void main(String[] args) {
        final Holder<String> holder = new @NonEmpty Holder<String>();
        @NonEmpty Collection<@NonEmpty String> strings = new ArrayList<>();
    }
}
ElementType.TYPE_USE以及ElementType.TYPE_PARAMETER两个新的元素类型来描述相关的注释上下文。Annotation Processing API还进行了一些细微的更改以识别 Java 中的新注释类型。

3. Java编译器的新特性

3.1. 参数名称

长期以来,Java 开发人员发明了多种在 Java 字节码中存储方法参数名称的方法,以使它们在运行时可用(例如Paranamer 库)。最后,Java 8 在语言(使用反射 API 和方法Parameter.getName())和字节码(使用新的编译器参数javac:)中创建了这个困难的函数–parameters
package com.javacodegeeks.java8.parameter.names;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class ParameterNames {
    public static void main(String[] args) throws Exception {
        Method method = ParameterNames.class.getMethod( "main", String[].class );
        for( final Parameter parameter: method.getParameters() ) {
            System.out.println( "Parameter: " + parameter.getName() );
        }
    }
}
如果您在不使用参数的情况下编译此类–parameters,然后运行该程序,您将看到如下内容:
Parameter: arg0
随着参数–parameters传递给编译器,程序输出将有所不同(将显示参数的实际名称):
Parameter: args
对于高级 Maven 用户,可以使用以下部分将 –parameters 参数添加到编译中maven-compiler-plugin
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <configuration>
    <compilerArgument>-parameters</compilerArgument>
    <source>1.8</source>
    <target>1.8</target>
    </configuration>
</plugin>
要检查参数名称的可用性,该类isNamePresent()提供了一种方便的方法Parameter

4.新的Java工具

Java 8 附带了一组新的命令行工具。在本节中,我们将看看其中最有趣的部分。

4.1. Nashorn 引擎:jjs

jjs是一个基于命令行的独立 Nashorn 引擎。它获取 JavaScript 源代码文件列表并运行它们。例如,让我们创建一个包含以下内容的 func.js文件:
function f() {
     return 1;
};

print( f() + 1 );
要运行此文件,我们将其作为参数传递给jjs
jjs func.js
控制台输出将如下所示:
2
有关更多详细信息,请参阅文档

4.2. 类依赖分析器:jdeps

jdeps是一个非常棒的命令行工具。它显示了 Java 类的包或类级别的依赖关系。它接受.class文件、文件夹或JAR 文件作为输入。默认情况下,jdeps将依赖项输出到标准输出(控制台)。作为示例,让我们看一下流行的Spring 框架库的依赖关系报告。为了使示例简短,我们只看一下 JAR 文件的依赖关系org.springframework.core-3.0.5.RELEASE.jar
jdeps org.springframework.core-3.0.5.RELEASE.jar
该命令输出相当多,因此我们只分析部分输出。依赖项按包分组。如果没有依赖,会显示not found
org.springframework.core-3.0.5.RELEASE.jar -> C:\Program Files\Java\jdk1.8.0\jre\lib\rt.jar
   org.springframework.core (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.io
      -> java.lang
      -> java.lang.annotation
      -> java.lang.ref
      -> java.lang.reflect
      -> java.util
      -> java.util.concurrent
      -> org.apache.commons.logging                         not found
      -> org.springframework.asm                            not found
      -> org.springframework.asm.commons                    not found
   org.springframework.core.annotation (org.springframework.core-3.0.5.RELEASE.jar)
      -> java.lang
      -> java.lang.annotation
      -> java.lang.reflect
      -> java.util
更详细的信息请参考官方文档
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION