关注Java领域相关技术 记录有趣的事情

Java8 新特性

US-B.Ralph
US-B.Ralph
2016-09-18

前言

Java 8 是自 Java 5 发布(2004年发布)以来变更最大的一个版本。Java 8 为 Java 带来了许多新功能,包括语言特性,编译器,类库,工具以及 JVM( Java 虚拟机)。下面我们具体来看一看 Java 8 带来了哪些新功能。本文由以下几个部分组成,每个部分都涉及 Java 8 的特性:

  • 语言
  • 编译器
  • 类库
  • 开发工具
  • 运行时( Java 虚拟机)

Java 语言新特性

Java 8 作为一个大版本的升级,增加了很多新特性。下面我们一起看看 Java 8 在语言层面的新特性。

Lambda 表达式和函数式接口 (Lambda Expressions & FunctionalInterface)

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(1,2,3,4,5,6,7).forEach((Integer i)-> System.out.println(i));
  • 如果 Lambda 表达式的主体比较复杂,也可以像 Java 中函数的定义一样,使用花括号将函数体包裹起来。例如:
Arrays.asList(1, 2, 3, 4, 5, 6, 7).forEach((Integer i) -> {
    i = i * 5;
    System.out.println(i);
});
  • Lambda 表达式可以引用类的成员或局部变量(被隐式地转为 final 类型)。例如,下两个代码是等效的:
String multSymbol = "x";
Arrays.asList(1, 2, 3, 4, 5, 6, 7).forEach((Integer i) -> {
    System.out.println(i + multSymbol + "5=" + (i * 5));
});
final String multSymbol = "x";
Arrays.asList(1, 2, 3, 4, 5, 6, 7).forEach((Integer i) -> {
    System.out.println(i + multSymbol + "5=" + (i * 5));
});
  • Lambda 表达式可能会有返回值。 返回值的类型将由编译器根据上下文推断。 如果 Lambda 代码块只有一行,则不需要 return 语句。 以下两段代码段是等效的:
Arrays.asList(1, 2, 3, 4, 5, 6, 7).sort((i1, i2) -> i1.compareTo(i2));
Arrays.asList(1, 2, 3, 4, 5, 6, 7).sort((i1, i2)->{
    int res = i1.compareTo(i2);
    return res;
});

函数式接口(FunctionalInterface)

  • 函数接口是一种只有一个方法的接口,可以使现有的功能和 Lambda 表达式友好兼容。函数接口可以隐式地转换成 Lambda 表达式。
  • java.lang.Runnablejava.util.concurrent.Callable 是函数接口两个最好的例子。
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}
  • 实际开发中函数接口是非常脆弱的,只要有人在接口里多添加一个方法,那么这个接口就不再是函数接口了,会导致编译失败。为了解决这个问题 Java 8 提供了一个特殊的注解 @FunctionalInterface 来显式地表明函数式接口( java 里所有现存的接口都已经加上了 @FunctionalInterface )。让我们看看 @FunctionalInterface 的定义:
/**
 * An informative annotation type used to indicate that an interface
 * type declaration is intended to be a <i>functional interface</i> as
 * defined by the Java Language Specification.
 *
 * Conceptually, a functional interface has exactly one abstract
 * method.  Since {@linkplain java.lang.reflect.Method#isDefault()
 * default methods} have an implementation, they are not abstract.  If
 * an interface declares an abstract method overriding one of the
 * public methods of {@code java.lang.Object}, that also does
 * <em>not</em> count toward the interface's abstract method count
 * since any implementation of the interface will have an
 * implementation from {@code java.lang.Object} or elsewhere.
 *
 * <p>Note that instances of functional interfaces can be created with
 * lambda expressions, method references, or constructor references.
 *
 * <p>If a type is annotated with this annotation type, compilers are
 * required to generate an error message unless:
 *
 * <ul>
 * <li> The type is an interface type and not an annotation type, enum, or class.
 * <li> The annotated type satisfies the requirements of a functional interface.
 * </ul>
 *
 * <p>However, the compiler will treat any interface meeting the
 * definition of a functional interface as a functional interface
 * regardless of whether or not a {@code FunctionalInterface}
 * annotation is present on the interface declaration.
 *
 * @jls 4.3.2 The Class Object
 * @jls 9.8 Functional Interfaces
 * @jls 9.4.3 Interface Method Body
 * @jls 9.6.4.9 @FunctionalInterface
 * @since 1.8
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
  • 需要注意的是:函数式接口中增加默认方法(default)和静态方法(static不会违反函数式接口的约定,请看例子:
@FunctionalInterface
public interface FunctionInterfaceDemo {
    void method();

    default void defatltMethod() {
    }

    static void staticMethod() {
    }
}

接口中的默认方法和静态方法 (Default Methods & Static Methods)

Java 8 支持在接口声明中声明默认方法和静态方法。

接口中声明默认方法

在 Java 的接口中声明默认方法和 c++ 中的 Trait 技术有些类似,但是目标不一样。默认方法允许我们在现有接口中添加新的方法,而不会破坏已经实现了这个接口的类的兼容性,也就是说 Java 8 不会强迫实现了该接口的类去实现默认方法。

  • 默认方法和抽象方法的区别是抽象方法必须被实现,但默认方法不需要。
  • 如果接口提供了一个默认的方法实现,所有实现了这个接口的类通过继承都将得到这个默认方法(如果有需要也可以重写这个方法),让我们来看看下面的例子:
    • 在接口 DefaultMethodable 中使用关键字 default 声明了一个默认方法 defaultMethod()
    • DefaultMethodableImpl 类实现了 DefaultMethodable 接口,并且让默认方法的实现保持原样;
    • OverridableImpl 类同样实现了 DefaultMethodable 接口,但是覆盖了默认方法的实现;
public interface DefaultMethodable {
    default void defaultMethod(){
        System.out.println("fun.usb.lambda.lambdainterfaces.DefaultMethodable.defaultMethod");
    }
}

class DefaultMethodableImpl implements DefaultMethodable {}

class OverrideableImpl implements DefaultMethodable {
    @Override
    public void defaultMethod() {
        System.out.println("fun.usb.lambda.lambdainterfaces.OverrideableImpl.defaultMethod");
    }
}
  • 默认方法使得我们可以在已经存在的接口中增加新的方法而不会编译失败。java.util.Collection 中添加额外的方法就是最好的例子,如:stream(), parallelStream(), forEach(), removeIf()等等。
public interface Collection<E> extends Iterable<E> {
    int size(); 
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);

    /**
     * Removes all of the elements of this collection that satisfy the given
     * predicate.  Errors or runtime exceptions thrown during iteration or by
     * the predicate are relayed to the caller.
     *
     * @implSpec
     * The default implementation traverses all elements of the collection using
     * its {@link #iterator}.  Each matching element is removed using
     * {@link Iterator#remove()}.  If the collection's iterator does not
     * support removal then an {@code UnsupportedOperationException} will be
     * thrown on the first matching element.
     *
     * @param filter a predicate which returns {@code true} for elements to be
     *        removed
     * @return {@code true} if any elements were removed
     * @throws NullPointerException if the specified filter is null
     * @throws UnsupportedOperationException if elements cannot be removed
     *         from this collection.  Implementations may throw this exception if a
     *         matching element cannot be removed or if, in general, removal is not
     *         supported.
     * @since 1.8
     */
    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }
    boolean retainAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();

    /**
     * Creates a {@link Spliterator} over the elements in this collection.
     *
     * Implementations should document characteristic values reported by the
     * spliterator.  Such characteristic values are not required to be reported
     * if the spliterator reports {@link Spliterator#SIZED} and this collection
     * contains no elements.
     *
     * <p>The default implementation should be overridden by subclasses that
     * can return a more efficient spliterator.  In order to
     * preserve expected laziness behavior for the {@link #stream()} and
     * {@link #parallelStream()}} methods, spliterators should either have the
     * characteristic of {@code IMMUTABLE} or {@code CONCURRENT}, or be
     * <em><a href="Spliterator.html#binding">late-binding</a></em>.
     * If none of these is practical, the overriding class should describe the
     * spliterator's documented policy of binding and structural interference,
     * and should override the {@link #stream()} and {@link #parallelStream()}
     * methods to create streams using a {@code Supplier} of the spliterator,
     * as in:
     * <pre>{@code
     *     Stream<E> s = StreamSupport.stream(() -> spliterator(), spliteratorCharacteristics)
     * }</pre>
     * <p>These requirements ensure that streams produced by the
     * {@link #stream()} and {@link #parallelStream()} methods will reflect the
     * contents of the collection as of initiation of the terminal stream
     * operation.
     *
     * @implSpec
     * The default implementation creates a
     * <em><a href="Spliterator.html#binding">late-binding</a></em> spliterator
     * from the collections's {@code Iterator}.  The spliterator inherits the
     * <em>fail-fast</em> properties of the collection's iterator.
     * <p>
     * The created {@code Spliterator} reports {@link Spliterator#SIZED}.
     *
     * @implNote
     * The created {@code Spliterator} additionally reports
     * {@link Spliterator#SUBSIZED}.
     *
     * <p>If a spliterator covers no elements then the reporting of additional
     * characteristic values, beyond that of {@code SIZED} and {@code SUBSIZED},
     * does not aid clients to control, specialize or simplify computation.
     * However, this does enable shared use of an immutable and empty
     * spliterator instance (see {@link Spliterators#emptySpliterator()}) for
     * empty collections, and enables clients to determine if such a spliterator
     * covers no elements.
     *
     * @return a {@code Spliterator} over the elements in this collection
     * @since 1.8
     */
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }

    /**
     * Returns a sequential {@code Stream} with this collection as its source.
     *
     * <p>This method should be overridden when the {@link #spliterator()}
     * method cannot return a spliterator that is {@code IMMUTABLE},
     * {@code CONCURRENT}, or <em>late-binding</em>. (See {@link #spliterator()}
     * for details.)
     *
     * @implSpec
     * The default implementation creates a sequential {@code Stream} from the
     * collection's {@code Spliterator}.
     *
     * @return a sequential {@code Stream} over the elements in this collection
     * @since 1.8
     */
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    /**
     * Returns a possibly parallel {@code Stream} with this collection as its
     * source.  It is allowable for this method to return a sequential stream.
     *
     * <p>This method should be overridden when the {@link #spliterator()}
     * method cannot return a spliterator that is {@code IMMUTABLE},
     * {@code CONCURRENT}, or <em>late-binding</em>. (See {@link #spliterator()}
     * for details.)
     *
     * @implSpec
     * The default implementation creates a parallel {@code Stream} from the
     * collection's {@code Spliterator}.
     *
     * @return a possibly parallel {@code Stream} over the elements in this
     * collection
     * @since 1.8
     */
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

接口中声明静态方法

Java 8 的另外一个有意思的新特性是接口里可以声明静态方法,并且可以实现。例子如下:

public interface DefaultMethodable {
    default void defaultMethod() {
        System.out.println("fun.us-b.lambda.lambdainterfaces.DefaultMethodable.defaultMethod");
    }
    //接口中的静态方法
    public static void main(String[] args) {
        DefaultMethodable defaultMethodableImpl = DefaultMethodableFactory.create(DefaultMethodAbleImpl::new);
        System.out.println(defaultMethodableImpl.getClass().getName());

        DefaultMethodable overrideableImpl = DefaultMethodableFactory.create(OverrideableImpl::new);
        System.out.println(overrideableImpl.getClass().getName());

    }
}

class DefaultMethodAbleImpl implements DefaultMethodable {
}

class OverrideableImpl implements DefaultMethodable {
    @Override
    public void defaultMethod() {
        System.out.println("fun.us-b.lambda.lambdainterfaces.OverrideableImpl.defaultMethod");
    }
}

interface DefaultMethodableFactory {
    static DefaultMethodable create(Supplier<DefaultMethodable> supplier) {
        return supplier.get();
    }
}

方法引用 (Method References)

方法引用使得我们可以直接访问类或者实例中已经存在的方法或者构造方法。结合 Lambda 表达式,方法引用使语法结构简洁紧凑。不再需要复杂的模板调用。
– 下面我们用 MethodReference 这个类来做示例,通过 MethodReference 中不同的方法。让我们来看看 java 8 支持的4种方法引用:

public class MethodReference {
    public static MethodReference create(final Supplier<MethodReference> supplier) {
        return supplier.get();
    }

    public static void staticMethod(final MethodReference methodReferance) {
        System.out.println("static method reference " + methodReferance.toString());
    }

    public void instanceMethod() {
        System.out.println("instacne method reference " + this.toString());
    }

    public void instanceMethodOfParticular(final MethodReference methodReference) {
        System.out.println("instance method of particular " + methodReference.toString());
    }

    @Test
    public void testCreate(){
        //1、无参构造方法的引用,语法Class::new ,对于含有泛型的构造方法来说语法是:Class<T >::new
        final MethodReference methodReference = MethodReference.create(MethodReference::new);
        List<MethodReference> methodReferences = Arrays.asList(methodReference);

        //2、静态方法的引用,语法Class::static_method,注意,该静态方法只接受一个MethodReference类型的参数
        methodReferences.forEach(MethodReference::staticMethod);

        //3、实例方法的引用,语法是Class::method,注意,该方法为无参方法
        methodReferences.forEach(MethodReference::instanceMethod);

        //4、引用特定类的实例方法,语法instance::method,注意,该方法只接受一个MethodReference类型的参数
        final MethodReference anthorInstance = MethodReference.create(MethodReference::new);
        methodReferences.forEach(anthorInstance::instanceMethodOfParticular);
    }
}

重复注解 Repeating Annotations

自从 Java 5 支持注解以来,注解被广泛使用。但是在 Java 8 之前使用注解有一个限制:同一个地方不能被相同的注解注释两次。Java 8 中引入了重复注解,允许同一个注解在同一个地方被重复使用。需要注意的是重复注解本身需要被 @Repeatable 标注。下面我们看一下示例:
– 本例中我们定义了 Filter 注解类,并且使用 @Repeatable 对其进行标注;
FiltersFilter 注解的容器;
– 若我们在接口 Filterable 中使用 Filter 注解进行重复注释,那么 Java 编译器会在编译时默认帮我们将重复注解的容器 (本例为 Filters) 添加到 Filterable 上。
– 另外,Reflection API 中提供了新的方法 getAnnotationsByType() 用来返回重复注解的类型。若使用之前的 Filterable.class.getAnnotation( Filter.class ) 将返回编译器注入的 Filters 实例。

import org.junit.Test;
import java.lang.annotation.*;

public class RepeatingAannotations {
    @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("f1")
    @Filter("f2")
    public interface Filterable {
    }

    @Test
    public void testRepeatingAannotations() {
        for (Filter filter : Filterable.class.getAnnotationsByType(Filter.class)){
            System.out.println(filter.value());
        }
    }
}
  • 我们使用 javap -c -v 反编译 RepeatingAannotations$Filterable.class 文件,可以看到编译器为我们添加了 Filters 注解(#6~#9)
  SHA-256 checksum 653f65ab4c6300c098c9b8b546fcf6c8fc4c5229650e5508fb58b00f1f5ce6ab
  Compiled from "RepeatingAannotations.java"
public interface org.zlmax.lambda.lambdainterfaces.RepeatingAannotationsFilterable
  minor version: 0
  major version: 52
  flags: (0x0601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
  this_class: #1                          // org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilterable
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 0, attributes: 3
Constant pool:
   #1 = Class              #17            // org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilterable
   #2 = Class              #19            // java/lang/Object
   #3 = Utf8               SourceFile
   #4 = Utf8               RepeatingAannotations.java
   #5 = Utf8               RuntimeVisibleAnnotations
   #6 = Class              #20            // org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilters
   #7 = Utf8               Filters
   #8 = Utf8               InnerClasses
   #9 = Utf8               Lorg/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilters;
  #10 = Utf8               value
  #11 = Class              #21            // org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilter
  #12 = Utf8               Filter
  #13 = Utf8               Lorg/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilter;
  #14 = Utf8               f1
  #15 = Utf8               f2
  #16 = Class              #22            // org/zlmax/lambda/lambdainterfaces/RepeatingAannotations
  #17 = Utf8               org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilterable
  #18 = Utf8               Filterable
  #19 = Utf8               java/lang/Object
  #20 = Utf8               org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilters
  #21 = Utf8               org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilter
  #22 = Utf8               org/zlmax/lambda/lambdainterfaces/RepeatingAannotations
{
}
SourceFile: "RepeatingAannotations.java"
RuntimeVisibleAnnotations:
  0: #9(#10=[@#13(#10=s#14),@#13(#10=s#15)])
    org.zlmax.lambda.lambdainterfaces.RepeatingAannotationsFilters(
      value=[@org.zlmax.lambda.lambdainterfaces.RepeatingAannotationsFilter(
        value="f1"
      ),@org.zlmax.lambda.lambdainterfaces.RepeatingAannotationsFilter(
        value="f2"
      )]
    )
InnerClasses:
  public static #7= #6 of #16;            // Filters=class org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilters of class org/zlmax/lambda/lambdainterfaces/RepeatingAannotations
  public static #12= #11 of #16;          // Filter=class org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilter of class org/zlmax/lambda/lambdainterfaces/RepeatingAannotations
  public static #18= #1 of #16;           // Filterable=class org/zlmax/lambda/lambdainterfaces/RepeatingAannotationsFilterable of class org/zlmax/lambda/lambdainterfaces/RepeatingAannotations

注解使用的扩展

  • Java 8 扩展了注解的使用的范围,Java 8 之后我们几乎可以在任何地方使用注解:局部变量、泛型、超类和接口实现、甚至是方法的 Exception 声明。请看以下示例:

示例1

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

    public static class TestAnnotation<@NotEmpty T> extends @NotEmpty Object {
        public void moehod() throws @NotEmpty Exception {
        }
    }

    @Test
    public void testAnnotation() {
        TestAnnotation<Integer> demo = new @NotEmpty TestAnnotation<>();

        @NotEmpty Collection<@NotEmpty String> strs = new ArrayList<>();
    }
}

示例2

package com.sun.istack.internal;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
public @interface NotNull {
}
  • 在 Java 8 的 ElementType 源码中可以看到新增了两个类型:
    • TYPE_PARAMETER
    • TYPE_USE
public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

更智能的类型推断

  • Java 8 在类型推断方面有了很大的改进,使得很多情况下,编译器可以推断参数的类型。让我们看看例子:
public class BetterTypeInference<T> {

    public T getOrDefault(T val, T defaultVal) {
        return val == null ? defaultVal : val;
    }

    public static <T> T defaultVal(){
        return null;
    }

    @Test
    public void testGetOrDefault(){
        final BetterTypeInference<Double> douObj = new BetterTypeInference<>();
        //这里参数BetterTypeInference.defaultValue()的类型被编译器推断出来,不需要显式声明
        //但是在java 7, 相同的代码编译报错,需要写成:BetterTypeInference.<Double>defaultVal()
        douObj.getOrDefault(100.0D, BetterTypeInference.defaultVal());
    }
}

Java 类库新特性

Java 8 新添加了很多新类,并且扩展了很多现有的类库来更好地支持并发编程、函数式编程。

Optional

NullPointerException 是开发中最常见的异常。人们为了避免 NPE 常常需要在代码中增加很多空指针的判断,直到Google GuavaOptional 作为 NullPointerExceptions 的解决方案,用来避免空指针检查对代码的污染,才拯救了广大 Java 开发者。受 Google Guava 的启发,Optional 现在已经是 Java 8 类库的一部分了。
Optional 是一个容器,它可以保存类型 T 的值或者 null ,而且 Optional 提供了很多方法来代替显式检查 null

public class T04_OptionalDemo {
    @Test(expected = IllegalArgumentException.class)
    public void optional() {
        //get获取Optional中的值
        assertThat(Optional.of(1).get(), is(1));
        //ofNullable值为null则初始化空Optional,orElse实现Optional中无数据时返回默认值
        assertThat(Optional.ofNullable(null).orElse("A"), is("A"));
        //OptionalDouble是基本类型double的Optional对象
        //isPresent判断有无数据Optional有非空数据返回true
        assertFalse(OptionalDouble.empty().isPresent());
        //orElseGet方法提供回退机制,当 Optional 中的值为空可以接受方法返回默认值
        OptionalDouble.empty().orElseGet(() -> 0.0);
        //通过map方法可以对Optional对象进行级联转换,不会出现空指针,转换后还是一个Optional
        assertThat(Optional.of(1).map(Math::incrementExact).get(), is(2));
        //通过filter实现Optional中数据的过滤,得到一个Optional
        assertThat(Optional.of(1).filter(integer -> integer % 2 == 0).orElse(null), is(nullValue()));
        //通过orElseThrow实现无数据时抛出supplier函数生成的异常
        Optional.empty().orElseThrow(IllegalArgumentException::new);
    }
}

Stream

Java 8 中新增加的 Stream API (java.util.stream) 将函数式编程引入到了 Java 语言中。这是迄今为止 Java 类库中规模最大的一次功能添加。

初识 Stream

  • Stream API 使集合处理大大简化(但它的使用并不局限于 Java 集合)。首先我们看一个例子:
  • 现在有一些 Point2D,我们需要求出 y>1 到原点的平均距离。
    • Java 8 之前往往使用 foreach 进行迭代处理;
    • Java 8 中引入了 Stream,Stream 是多个元素的序列,支持串行和并行操作;
public class T03_StreamDemo1 {
    /**
     * 将整数列表转为point2d列表
     * 遍历point2d列表,过滤y>1的对象
     * 求出符合条件的点到原点的距离,并计算平均值
     */
    public double cale(List<Integer> nums) {
        List<Point2D> point2DS = new ArrayList<>();
        for (Integer integer : nums) {
            if (integer >= 0) {
                Point2D point2D = new Point2D.Double((double)integer / 6.0, (double)integer / 7.0);
                point2DS.add(point2D);
            }
        }

        int count = 0;
        double totleDistance = 0d;
        for (Point2D p : point2DS
        ) {
            if (p.getY() > 1) {
                totleDistance += p.distance(0, 0);
                count++;
            }
        }
        return count == 0 ? 0 : totleDistance / count;
    }

    public double caleForLambda(List<Integer> nums) {
        double streamResult = nums.stream()
                //传入一个Function实现对象转换
                .map(i -> new Point2D.Double((double)i / 6 , (double)i / 7 ))
                //传入Predicate,过滤符合条件的元素
                .filter(p -> p.getY() > 1)
                //传入ToDoubleFunction,将对象转为double
                .mapToDouble(p -> p.distance(0, 0))
                //返回 OptionalDouble,可能包含无值空double
                .average()
                .orElse(0);

        return streamResult;
    }

    @Test
    public void testCaleForLambda() {
        List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 16, 17, 18, 19, 20);
        Assert.assertEquals(cale(ints), caleForLambda(ints), 0.001);
    }
}
  • 上面 caleForLambda 方法的执行流程是这样的:
    • 首先 Integer 集合被转为 Stream 表示;
    • 然后通过 mapInteger 转为 Point2D
    • 然后 filter 操作会过滤 p.getY()>1 的元素;
    • 接下来 mapToDoublePoint2D 转为点 p 到原点的距离,返回 OptionalDouble
    • 最后使用 average() 操作求得符合条件的点到原点的距离的平均值,并使用 orElse 缺保出现空值时返回默认值;

并行流

在看下一个例子之前,我们先来看看 Stream 操作的分类。Stream 操作被分为中间操作和终点操作。
中间操作返回一个新的 Stream 。这些中间操作是延迟的,执行一个中间操作比如 filter 实际上不会真的做过滤操作,而是创建一个新的 Stream ,当这个新的 Stream 被遍历的时候,它里头会包含有原来 Stream 里符合过滤条件的元素。
终点操作比如说 forEach 或者 sum 会遍历 Stream 从而产生最终结果或附带结果。终点操作执行完之后,Stream 管道就被消费完了,不再可用。在几乎所有的情况下,终点操作都是即时完成对数据的遍历操作。
Stream 的另外一个功能是 Stream 支持并行处理。让我们看看下面这个例子,这个例子是把所有 Point2D 到原点的距离加起来。

    public double caleDistanceSum(List<Integer>  nums){
        double streamRes = nums.stream()
                .parallel()
                .map(i -> new Point2D.Double((double)i / 6 , (double)i / 7 ))
                .mapToDouble(p -> p.distance(0, 0))
                .reduce(0.0, Double::sum);
        return streamRes;
    }
  • 这个例子中使用了 parallel() 方法执行并行处理,并且计算最终结果的时候使用了 reduce 方法。

分组处理

  • 有的时候我们会碰到这样的需求:将一组数据根据某个条件进行分组,正如下面的例子将一些 Point2D 根据 y 轴坐标是否大于 1 进行分组:
    public Map<Boolean, List<Point2D>> groupByYAxis(List<Integer> nums) {
        Map<Boolean, List<Point2D>> res = nums.stream()
                .map(i -> new Point2D.Double((double) i / 6, (double) i / 7))
                .collect(Collectors.groupingBy(p -> p.getY() > 1));
        return res;
    }
  • 输出结果:
{false=[Point2D.Double[0.1, 0.2], Point2D.Double[0.2, 0.4], Point2D.Double[0.3, 0.6]], true=[Point2D.Double[1.9, 3.8], Point2D.Double[2.0, 4.0]]}

计算权重

  • 有的时候会碰到计算集合中每个元素的占比情况的需求:
    public Collection<String> caleDistanceWeight(List<Integer> nums) {
        final double totleDistance = caleDistanceSum(nums);
        Collection<String> res = nums.stream()
                .parallel()
                //传入一个Function实现对象转换
                .map(i -> new Point2D.Double((double) i / 6, (double) i / 7))
                //传入ToDoubleFunction,将对象转为double
                .mapToDouble(p -> p.distance(0, 0) / totleDistance)
                .boxed()
                .mapToLong(weight -> (long) (weight * 100))
                .mapToObj(percentage -> percentage+"%")
                .collect(Collectors.toList());
        return res;
    }
  • 计算结果
[0%, 0%, 1%, 1%, 2%, 2%, 3%, 3%, 4%, 4%, 5%, 5%, 6%, 7%, 7%, 7%, 8%, 8%, 8%, 9%]

使用 Stream 处理 IO 操作

  • Stream API 不仅仅是集合框架,从文本中读取数据也可以使用 Stream API,下面看两个例子:
示例1
  • 读取当前项目下所有 .java 文件,并输出包含 public class 的首行内容
    @Test
    public void testFiles() throws IOException {
        //无限深度,递归遍历文件夹
        //利用 Files.walk 返回一个 Path 的流
        try (Stream<Path> pathStream = Files.walk(Paths.get("."))) {
            pathStream.filter(Files::isRegularFile) //只查普通文件
                    .filter(FileSystems.getDefault().getPathMatcher("glob:**/*.java")::matches) //搜索java源码文件
                    .flatMap(ThrowingFunction.unchecked(path ->
                            Files.readAllLines(path).stream() //读取文件内容,转换为Stream<List>
                                    .filter(line -> Pattern.compile("public class").matcher(line).find()) //使用正则过滤带有public class的行
                                    .map(line -> path.getFileName() + " >> " + line))) //把这行文件内容转换为文件名+行
                    .forEach(System.out::println); //打印所有的行

        }
    }
示例2
  • 逐行读取文件
   @Test
    public void readFileLineByLine() throws IOException {
        final Path path = new File("").toPath();

        try(Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)){
            //onClose 返回一个等价的有额外句柄的Stream,当Stream的close()方法被调用的时候这个句柄会被执行
            lines.onClose(()->System.out.println("Done !")).forEach(System.out::println);
        }
    }

日期时间 API

日期和时间一直是让 Java 开发人员头痛的问题。即使是 java.util.Date 和后来的java.util.Calendar 也未能改变这个情况(甚至让人们更加迷茫)。因此产生了 Joda-Time 第三方类库,Java 8 之前 Joda-Time 成为了事实上的 Java 标准时间日期类库。
Joda-Time 的影响,Java 8 引入了新的日期时间 API(JSR 310)。
– Java 8 新的日期时间 API 位于 java.time 包,其中包含了所有关于日期、时间、日期时间、时区、Instant(跟日期类似但精确到纳秒)、duration(持续时间)和时钟操作的类。
– 设计这些 API 的时候很认真地考虑了这些类的不变性(从 java.util.Calendar 吸取的痛苦教训)。如果需要修改时间对象,会返回一个新的实例。下面我们看一下 java.time 包中常用的类:

Clock

让我们看看一些关键的类和用法示例。第一个类是 ClockClock 使用时区来访问当前的 instant , datetimeClock 类可以替换 System.currentTimeMillis()TimeZone.getDefault()

    @Test
    public void clockDemo(){
        Clock clock = Clock.systemUTC();
        System.out.println(clock.instant());
        System.out.println(clock.millis());
        System.out.println(clock.getZone());
    }

LocalDate & LocalTime

  • LocalDate 只保存有 ISO-8601 日期系统的日期部分,有时区信息;
  • LocalTime 只保存 ISO-8601 日期系统的时间部分,没有时区信息;
  • LocalDateLocalTime 都可以从 Clock 对象创建;
    @Test
    public void localDateDdemo(){
        LocalDate localDate = LocalDate.now();
        LocalDate localDateFromClock = LocalDate.now(Clock.systemUTC());
        Assert.assertEquals(localDate.toString(), localDateFromClock.toString());
    }

    @Test
    public void localTimeDemo(){
        LocalTime localTime = LocalTime.now();
        LocalTime localTimeFromClock = LocalTime.now(Clock.systemUTC());
        Assert.assertEquals(localTime.toString(), localTimeFromClock.toString());
    }

LocalDateTime

  • LocalDateTim 类合并了 LocalDateLocalTime,它保存有 ISO-8601 日期系统的日期和时间,但是没有时区信息。让我们看一个简单的例子。
   @Test
    public void localDateTimeDemo(){
        LocalDateTime localDateTime = LocalDateTime.now();
        LocalDateTime localDateTimeFromClok = LocalDateTime.now(Clock.systemUTC());
        Assert.assertEquals(localDateTime.toLocalDate(), localDateTimeFromClok.toLocalDate());
    }

ZonedDateTime

  • ZonedDateTime,它不仅保存了 ISO-8601 日期系统的日期和时间,而且有时区信息。让我们看一些例子:
    @Test
    public void zonedDateTimeDemo(){
        ZonedDateTime zonedDateTime = ZonedDateTime.now();
        ZonedDateTime zonedDateTimeFromClock = ZonedDateTime.now(Clock.systemUTC());
        ZonedDateTime zonedDateTimeFromZone = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

        System.out.println(zonedDateTime);
        System.out.println(zonedDateTimeFromClock);
        System.out.println(zonedDateTimeFromZone);
    }
  • 输出
2020-09-15T14:02:42.394418900+08:00[Asia/Shanghai]
2020-09-15T06:02:42.394418900Z
2020-09-15T14:02:42.394418900+08:00[Asia/Shanghai]

Duration

  • Duration 持有的时间精确到纳秒。它让我们很容易计算两个日期中间的差异。让我们来看一下:
    @Test
    public void duration(){
        LocalDateTime from = LocalDateTime.of(2016, 8, 25, 14, 00);
        LocalDateTime to = LocalDateTime.of(2050, 8, 25, 14, 00);
        Duration duration = Duration.between(from, to);
        System.out.println(duration.toDays());
        System.out.println(duration.toHours());
        to = LocalDateTime.of(2016,8,25, 14,0,01);
        duration = Duration.between(from, to);//需要重新构建
        System.out.println(duration.toNanos());
    }

Nashorn javascript 引擎

  • Nashorn javascript 允许我们在 JVM 上运行特定的 JavaScript 应用。Nashorn javascript 引擎是 javax.script.ScriptEngine 另一个实现,两者规则一样,允许 Java 和 JavaScript 互相操作。这里有个小例子:
    @Test
    public void testJavaScriptEngine() throws ScriptException {
        ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName("JavaScript");
        System.out.println(scriptEngine.getClass().getName());
        System.out.println(scriptEngine.eval("function f(){return 1;} f()+1"));
    }
  • 输出如下:
jdk.nashorn.api.scripting.NashornScriptEngine
2.0

Base64

  • Java 8 中增加了对 Base64 的支持,下面看一个例子:
    @Test
    public void testBase64(){
        String txt = "Base64 finally in Java 8";
        String encoded = Base64.getEncoder().encodeToString(txt.getBytes(StandardCharsets.UTF_8));
        System.out.println(encoded);

        String decoede = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
        System.out.println(decoede);
    }
  • Java 8 中的 Base64 API 还支持对 URLMINE 的编码解码:
/**
 * This class consists exclusively of static methods for obtaining
 * encoders and decoders for the Base64 encoding scheme. The
 * implementation of this class supports the following types of Base64
 * as specified in
 * <a href="http://www.ietf.org/rfc/rfc4648.txt">RFC 4648</a> and
 * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
 *
 * <ul>
 * <li><a name="basic"><b>Basic</b></a>
 * <p> Uses "The Base64 Alphabet" as specified in Table 1 of
 *     RFC 4648 and RFC 2045 for encoding and decoding operation.
 *     The encoder does not add any line feed (line separator)
 *     character. The decoder rejects data that contains characters
 *     outside the base64 alphabet.</p></li>
 *
 * <li><a name="url"><b>URL and Filename safe</b></a>
 * <p> Uses the "URL and Filename safe Base64 Alphabet" as specified
 *     in Table 2 of RFC 4648 for encoding and decoding. The
 *     encoder does not add any line feed (line separator) character.
 *     The decoder rejects data that contains characters outside the
 *     base64 alphabet.</p></li>
 *
 * <li><a name="mime"><b>MIME</b></a>
 * <p> Uses the "The Base64 Alphabet" as specified in Table 1 of
 *     RFC 2045 for encoding and decoding operation. The encoded output
 *     must be represented in lines of no more than 76 characters each
 *     and uses a carriage return {@code '\r'} followed immediately by
 *     a linefeed {@code '\n'} as the line separator. No line separator
 *     is added to the end of the encoded output. All line separators
 *     or other characters not found in the base64 alphabet table are
 *     ignored in decoding operation.</p></li>
 * </ul>
 *
 * <p> Unless otherwise noted, passing a {@code null} argument to a
 * method of this class will cause a {@link java.lang.NullPointerException
 * NullPointerException} to be thrown.
 *
 * @author  Xueming Shen
 * @since   1.8
 */
public class Base64 {
    ...
}

并行数组处理

  • Java 8 中新增加了很多方法支持并行的数组处理。
  • 其中 parallelSort() 这个方法可以使得排序在多核计算机上得执行速度显著加快。下面的小例子演示了这个新的方法(parallelXXX)。
    @Test
    public void testParallelArray() {
        long[] arrOfLong = new long[20000];
        //填充arrOfLong数组
        Arrays.parallelSetAll(arrOfLong, i -> ThreadLocalRandom.current().nextInt(1000000));
        Arrays.stream(arrOfLong)
                .limit(5)
                .forEach(i -> System.out.print(i + " "));
        System.out.println();
        //对arrOfLong数组进行排序
        Arrays.parallelSort(arrOfLong);
        Arrays.stream(arrOfLong)
                .limit(5)
                .forEach(i -> System.out.print(i + " "));
    }
  • 输出结果:
531793 851002 185549 103262 272140 
20 27 43 100 115 

并发

  • java.util.concurrent.ConcurrentHashMap 类中添加了一些新的方法,用来支持 Stream 和 Lambda 表达式的聚合操作。此外,java.util.concurrent.ForkJoinPool 类中也添加了新的方法,中来支持 common pool。
  • 新增的 java.util.concurrent.locks.StampedLock 类提供了一个基于容量的锁,它有三种模式来控制读/写访问(java.util.concurrent.locks.StampedLockjava.util.concurrent.locks.ReadWriteLock 的替代品)。
  • java.util.concurrent.atomic 包中还增加了下面这些类:
    • DoubleAccumulator
    • DoubleAdder
    • LongAccumulator
    • LongAdder

Java 中新增加的工具

  • java 8 中新增加了一些命令行工具。

Nashorn engine: jjs

  • jjs 是个基于 Nashorn 引擎的命令行工具。它接受 JavaScript 源代码为参数,并且解释执行这些源代码,例如:
    • 首先编写 f.js 文件
function f(){
    return 1;
}
print(f()+1);
- 在命令行中执行 jjs []  [-- ]
D:\>jjs f.js
2

jdeps

  • jdeps 是一个功能强大的命令行工具。它可以帮我们显示出包的层级或者类的层级以及 java 类文件的依赖关系。它可以接受 .class 文件、目录或 JAR 文件作为输入。默认情况下,jdeps 会将依赖关系输出到系统输出(控制台)。
  • 我们使用 jdeps 分析 spring Framework 库的依赖关系:
I:\repository\org\springframework\spring-core\4.2.5.RELEASE>jdeps spring-core-4.2.5.RELEASE.jar >> out.txt

JVM 新特性

JVM 内存区域划分

  • JVM 内存永久区(PermGen space)已经被元数据区(metaspace)替换(JEP 122)。
  • 相应的 JVM 参数 -XX:PermSize–XX:MaxPermSize-XX:MetaSpaceSize-XX:MaxMetaspaceSize 代替。

参数名称

  • Java 8 之前 Java 开发开发人员想尽办法将方法的参数名称保存到 Java 字节码中,并且让这些参数在运行时可以使用。
  • 现在 Java 8 原生支持这个功能,但是需要同时满足以下条件:
    • 使用 Reflection API 中的 Parameter.getName() 方法
    • 使用 javac 编译时使用 –parameters 参数

示例

public class ParameterNames {
    public static void main(String[] args) throws NoSuchMethodException {
        Method mainMethod = ParameterNames.class.getMethod("main", String[].class);
        Arrays.asList(mainMethod.getParameters()).forEach(p -> System.out.println(p.getName()));
    }
}
  • 当编译时未使用 –parameters 参数时,输出:
    arg0
  • 当编译时使用 –parameters 参数时,输出:
    args

–parameters 参数的添加方法

  • 使用 Maven 的编译插件时,在 pom.xml 文件中增加:
<compilerArgument>-parameters</compilerArgument>
  • 修改后的pom.xml文件如下:
...
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerArgument>-parameters</compilerArgument>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
...
  • 使用IDEA时
    • 选择 “File”->”Settings”->”Build,Execution,Deployment”->”Compiler”->”Java Compiler”
    • 找到 “Additional command line parameters:” 填入 -parameters 重新编译项目即可
  • 有关Paramter类的其他方法可查阅Java SE documentation

尾声

本文是对 Java 8 新特性的一个总览,后续针对 Java 8 中所更新的内容会单独进行介绍。

US-B.Ralph
JavaLanguage Features

Leave a Comment

邮箱地址不会被公开。 必填项已用*标注

13 + 3 =