Java8新特性都到碗里来

对于Java开发者来讲,Java8的版本显然是一个具备里程碑意义的版本,蕴含了许多使人激动的新特性,若是能利用好这些新特性,可以大大提高咱们的开发效率。Java8的函数式编程可以大大减小代码量和便于维护,同时,还有一些跟并发相关的功能。开发中经常使用到的新特性以下:html

  1. 接口的默认方法和静态方法
  2. 函数式接口FunctionInterface与lambda表达式
  3. 方法引用
  4. Stream
  5. Optional
  6. Date/time API的改进
  7. 其余改进

1. 接口的默认方法和静态方法

在Java8以前,接口中只能包含抽象方法。那么这有什么样弊端呢?好比,想再Collection接口中添加一个spliterator抽象方法,那么也就意味着以前全部实现Collection接口的实现类,都要从新实现spliterator这个方法才行。而接口的默认方法就是为了解决接口的修改与接口实现类不兼容的问题,做为代码向前兼容的一个方法java

那么如何在接口中定义一个默认方法呢?来看下JDK中Collection中如何定义spliterator方法的:sql

default Spliterator<E> spliterator() {
    return Spliterators.spliterator(this, 0);
}
复制代码

能够看到定义接口的默认方法是经过default关键字。所以,在Java8中接口可以包含抽象方法外还可以包含若干个默认方法(即有完整逻辑的实例方法)。数据库

public interface IAnimal {
    default void breath(){
        System.out.println("breath!");
    };
}


public class DefaultMethodTest implements IAnimal {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.breath();
    }

}


输出结果为:breath!
复制代码

能够看出IAnimal接口中有由default定义的默认方法后,那么其实现类DefaultMethodTest也一样可以拥有实例方法breath。可是若是一个类继承多个接口,多个接口中有相同的方法就会产生冲突该如何解决?实际上默认方法的改进,使得java类可以拥有相似多继承的能力,即一个对象实例,将拥有多个接口的实例方法,天然而然也会存在方法重复冲突的问题。express

下面来看一个例子:编程

public interface IDonkey{
    default void run() {
        System.out.println("IDonkey run");
    }
}

public interface IHorse {

    default void run(){
        System.out.println("Horse run");
    }

}

public class DefaultMethodTest implements IDonkey,IHorse {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.breath();
    }

}
复制代码

定义两个接口:IDonkey和IHorse,这两个接口中都有相同的run方法。DefaultMethodTest实现了这两个接口,因为这两个接口有相同的方法,所以就会产生冲突,不知道以哪一个接口中的run方法为准,编译会出错:inherits unrelated defaults for run.....api

解决方法数组

针对由默认方法引发的方法冲突问题,只有经过重写冲突方法,并方法绑定的方式,指定以哪一个接口中的默认方法为准安全

public class DefaultMethodTest implements IAnimal,IDonkey,IHorse {
    public static void main(String[] args) {
        DefaultMethodTest defaultMethod = new DefaultMethodTest();
        defaultMethod.run();
    }

    @Override
    public void run() {
        IHorse.super.run();
    }
}
复制代码

DefaultMethodTest重写了run方法,并经过 IHorse.super.run();指定以IHorse中的run方法为准。并发

静态方法

在Java8中还有一个特性就是,接口中还能够声明静态方法,以下例:

public interface IAnimal {
    default void breath(){
        System.out.println("breath!");
    }
    static void run(){}
}
复制代码

2.函数式接口FunctionInterface与lambda表达式

函数式接口

Java8最大的变化是引入了函数式思想,也就是说函数能够做为另外一个函数的参数。函数式接口,要求接口中有且仅有一个抽象方法,所以常用的Runnable,Callable接口就是典型的函数式接口。可使用@FunctionalInterface注解,声明一个接口是函数式接口。若是一个接口知足函数式接口的定义,会默认转换成函数式接口。可是,最好是使用@FunctionalInterface注解显式声明。这是由于函数式接口比较脆弱,若是开发人员无心间新增了其余方法,就破坏了函数式接口的要求,若是使用注解@FunctionalInterface,开发人员就会知道当前接口是函数式接口,就不会无心间破坏该接口。下面举一个例子:

@java.lang.FunctionalInterface
public interface FunctionalInterface {
    void handle();
}
复制代码

该接口只有一个抽象方法,而且使用注解显式声明。可是,函数式接口要求只有一个抽象方法却能够拥有若干个默认方法的(实例方法),好比下例:

@java.lang.FunctionalInterface
public interface FunctionalInterface {
    void handle();

    default void run() {
        System.out.println("run");
    }
}
复制代码

该接口中,除了有抽象方法handle外,还有默认方法(实例方法)run。另外,任何被Object实现的方法都不能当作是抽象方法

lambda表达式

lambda表达式是函数式编程的核心,lambda表达式即匿名函数,是一段没有函数名的函数体,能够做为参数直接传递给相关的调用者。lambda表达式极大的增长了Java语言的表达能力。lambda的语法结构为:

(parameters) -> expression
或
(parameters) ->{ statements; }
复制代码
  • 可选类型声明:不须要声明参数类型,编译器能够统一识别参数值。

  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数须要定义圆括号。

  • 可选的大括号:若是主体包含了一个语句,就不须要使用大括号。

  • 可选的返回关键字:若是主体只有一个表达式返回值则编译器会自动返回值,大括号须要指定明表达式返回了一个数值。

完整示例为(摘自菜鸟教程

public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
        
      // 类型声明
      MathOperation addition = (int a, int b) -> a + b;
        
      // 不用类型声明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括号中的返回语句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 没有大括号及返回语句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}
复制代码

另外,lambda还能够访问外部局部变量,以下例所示:

int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));
复制代码

实际上在lambda中访问类的成员变量或者局部变量时,会隐式转换成final类型变量,因此上例实际上等价于:

final int adder = 5;
Arrays.asList(1, 2, 3, 4, 5).forEach(e -> System.out.println(e + adder));
复制代码

3. 方法引用

方法引用是为了进一步简化lambda表达式,经过类名或者实例名与方法名的组合来直接访问到类或者实例已经存在的方法或者构造方法。方法引用使用**::来定义,::**的前半部分表示类名或者实例名,后半部分表示方法名,若是是构造方法就使用NEW来表示。

方法引用在Java8中使用方式至关灵活,总的来讲,一共有如下几种形式:

  • 静态方法引用:ClassName::methodName;
  • 实例上的实例方法引用:instanceName::methodName;
  • 超类上的实例方法引用:supper::methodName;
  • 类的实例方法引用:ClassName:methodName;
  • 构造方法引用Class:new;
  • 数组构造方法引用::TypeName[]::new

下面来看一个例子:

public class MethodReferenceTest {

    public static void main(String[] args) {
        ArrayList<Car> cars = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Car car = Car.create(Car::new);
            cars.add(car);
        }
        cars.forEach(Car::showCar);

    }

    @FunctionalInterface
    interface Factory<T> {
        T create();
    }

    static class Car {
        public void showCar() {
            System.out.println(this.toString());
        }

        public static Car create(Factory<Car> factory) {
            return factory.create();
        }
    }
}


输出结果:

learn.MethodReferenceTest$Car@769c9116
learn.MethodReferenceTest$Car@6aceb1a5
learn.MethodReferenceTest$Car@2d6d8735
learn.MethodReferenceTest$Car@ba4d54
learn.MethodReferenceTest$Car@12bc6874
复制代码

在上面的例子中使用了Car::new,即经过构造方法的方法引用的方式进一步简化了lambda的表达式,Car::showCar,即表示实例方法引用。

4. Stream

Java8中有一种新的数据处理方式,那就是流Stream,结合lambda表达式可以更加简洁高效的处理数据。Stream使用一种相似于SQL语句从数据库查询数据的直观方式,对数据进行如筛选、排序以及聚合等多种操做。

4.1 什么是流Stream

Stream是一个来自数据源的元素队列并支持聚合操做,更像是一个更高版本的Iterator,原始版本的Iterator,只能一个个遍历元素并完成相应操做。而使用Stream,只须要指定什么操做,如“过滤长度大于10的字符串”等操做,Stream会内部遍历并完成指定操做。

Stream中的元素在管道中通过中间操做(intermediate operation)的处理后,最后由最终操做(terminal operation)获得最终的结果。

  • 数据源:是Stream的来源,能够是集合、数组、I/O channel等转换而成的Stream;
  • 基本操做:相似于SQL语句同样的操做,好比filter,map,reduce,find,match,sort等操做。

当咱们操做一个流时,实际上会包含这样的执行过程:

获取数据源-->转换成Stream-->执行操做,返回一个新的Stream-->再以新的Stream继续执行操做--->直至最后操做输出最终结果

4.2 生成Stream的方式

生成Stream的方式主要有这样几种:

  1. 从接口Collection中和Arrays:

    • Collection.stream();
    • Collection.parallelStream(); //相较于串行流,并行流可以大大提高执行效率
    • Arrays.stream(T array);
  2. Stream中的静态方法:

    • Stream.of();
    • generate(Supplier s);
    • iterate(T seed, UnaryOperator f);
    • empty();
  3. 其余方法

    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()
    • BufferedReader.lines()

下面对前面常见的两种方式给出示例:

public class StreamTest {


    public static void main(String[] args) {
        //1.使用Collection中的方法和Arrays
        String[] strArr = new String[]{"a", "b", "c"};
        List<String> list = Arrays.asList(strArr);
        Stream<String> stream = list.stream();
        Stream<String> stream1 = Arrays.stream(strArr);

        //2. 使用Stream中提供的静态方法
        Stream<String> stream2 = Stream.of(strArr);
        Stream<Double> stream3 = Stream.generate(Math::random);
        Stream<Object> stream4 = Stream.empty();
        Stream.iterate(1, i -> i++);

    }
}	
复制代码

4.3 Stream的操做

常见的Stream操做有这样几种:

  1. Intermediate(中间操做):中间操做是指对流中数据元素作出相应转换或操做后依然返回为一个流Stream,仍然能够供下一次流操做使用。经常使用的有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip。
  2. Termial(结束操做):是指最终对Stream作出聚合操做,输出结果。

中间操做

filter:对Stream中元素进行过滤

过滤元素为空的字符串:

long count = stream.filter(str -> str.isEmpty()).count();
复制代码

map:对Stream中元素按照指定规则映射成另外一个元素

将每个元素都添加字符串“_map”

stream.map(str -> str + "_map").forEach(System.out::println);
复制代码

map方法是一对一的关系,将stream中的每个元素按照映射规则成另一个元素,而若是是一对多的关系的话就须要使用flatmap方法。

concat:对流进行合并操做

concat方法将两个Stream链接在一块儿,合成一个Stream。若两个输入的Stream都时排序的,则新Stream也是排序的;若输入的Stream中任何一个是并行的,则新的Stream也是并行的;若关闭新的Stream时,原两个输入的Stream都将执行关闭处理。

Stream.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6)).
	forEach(System.out::println);
复制代码

distinct:对流进行去重操做

去除流中重复的元素

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.distinct().forEach(System.out::println);

输出结果:
a
b
c
复制代码

limit:限制流中元素的个数

截取流中前两个元素:

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.limit(2).forEach(System.out::println);

输出结果:
a
a
复制代码

skip:跳过流中前几个元素

丢掉流中前两个元素:

Stream<String> stream = Stream.of("a", "a", "b", "c");
        stream.skip(2).forEach(System.out::println);
输出结果:
b
c
复制代码

peek:对流中每个元素依次进行操做,相似于forEach操做

JDK中给出的例子:

Stream.of("one", "two", "three", "four")
            .filter(e -> e.length() > 3)
            .peek(e -> System.out.println("Filtered value: " + e))
            .map(String::toUpperCase)
            .peek(e -> System.out.println("Mapped value: " + e))
            .collect(Collectors.toList());
输出结果:
Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR
复制代码

sorted:对流中元素进行排序,能够经过sorted(Comparator<? super T> comparator)自定义比较规则

Stream<Integer> stream = Stream.of(3, 2, 1);
        stream.sorted(Integer::compareTo).forEach(System.out::println);
输出结果:
1
2
3
复制代码

match:检查流中元素是否匹配指定的匹配规则

Stream 有三个 match 方法,从语义上说:

  • allMatch:Stream 中所有元素符合传入的 predicate,返回 true;
  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true;
  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true。

如检查Stream中每一个元素是否都大于5:

Stream<Integer> stream = Stream.of(3, 2, 1);
boolean match = stream.allMatch(integer -> integer > 5);
System.out.println(match);
输出结果:
false
复制代码

结束操做

Collectors中常见归约方法总结

重构和定制收集器Collectors

count:统计Stream中元素的个数

long count = stream.filter(str -> str.isEmpty()).count();
复制代码

max/min:找出流中最大或者最小的元素

Stream<Integer> stream = Stream.of(3, 2, 1);
    System.out.println(stream.max(Integer::compareTo).get());

输出结果:
3
复制代码

forEach

forEach方法前面已经用了好屡次,其用于遍历Stream中的所元素,避免了使用for循环,让代码更简洁,逻辑更清晰。

示例:

Stream.of(5, 4, 3, 2, 1)
    .sorted()
    .forEach(System.out::println);
    // 打印结果
    // 1,2,3,4,5
复制代码

reduce

Stream中的Reduce讲解

Stream归约方法总结以下:

Collectors提供的归约方法.png

5. Optional

为了解决空指针异常,在Java8以前须要使用if-else这样的语句去防止空指针异常,而在Java8就可使用Optional来解决。Optional能够理解成一个数据容器,甚至能够封装null,而且若是值存在调用isPresent()方法会返回true。为了可以理解Optional。先来看一个例子:

public class OptionalTest {


    private String getUserName(User user) {
        return user.getUserName();
    }

    class User {
        private String userName;

        public User(String userName) {
            this.userName = userName;
        }

        public String getUserName() {
            return userName;
        }
    }
}
复制代码

事实上,getUserName方法对输入参数并无进行判断是否为null,所以,该方法是不安全的。若是在Java8以前,要避免可能存在的空指针异常的话就须要使用if-else进行逻辑处理,getUserName会改变以下:

private String getUserName(User user) {
    if (user != null) {
        return user.getUserName();
    }
    return null;
}
复制代码

这是十分繁琐的一段代码。而若是使用Optional则会要精简不少:

private String getUserName(User user) {
    Optional<User> userOptional = Optional.ofNullable(user);
    return userOptional.map(User::getUserName).orElse(null);
}
复制代码

Java8以前的if-else的逻辑判断,这是一种命令式编程的方式,而使用Optional更像是一种函数式编程,关注于最后的结果,而中间的处理过程交给JDK内部实现。

到如今,能够直观的知道Optional对避免空指针异常颇有效,下面,对Optional的API进行概括:

建立Optional

  1. Optional.empty():经过静态工厂方法Optional.empty,建立一个空的Optional对象;
  2. Optional of(T value):若是value为null的话,当即抛出NullPointerException;
  3. Optional ofNullable(T value):使用静态工厂方法Optional.ofNullable,你能够建立一个容许null值的Optional对象。

实例代码:

//建立Optional
Optional<Object> optional = Optional.empty();
Optional<Object> optional1 = Optional.ofNullable(null);
Optional<String> optional2 = Optional.of(null);
复制代码

经常使用方法

1.	boolean equals(Object obj):判断其余对象是否等于 Optional;
2. Optional<T> filter(Predicate<? super <T> predicate):若是值存在,而且这个值匹配给定的 predicate,返回一个Optional用以描述这个值,不然返回一个空的Optional;
3. <U> Optional<U> flatMap(Function<? super T,Optional<U>> mapper):若是值存在,返回基于Optional包含的映射方法的值,不然返回一个空的Optional;
4. T get():若是在这个Optional中包含这个值,返回值,不然抛出异常:NoSuchElementException;
5. int hashCode():返回存在值的哈希码,若是值不存在 返回 0;
6. void ifPresent(Consumer<? super T> consumer):若是值存在则使用该值调用 consumer , 不然不作任何事情;
7. boolean isPresent():若是值存在则方法会返回true,不然返回 false;
8. <U>Optional<U> map(Function<? super T,? extends U> mapper):若是存在该值,提供的映射方法,若是返回非null,返回一个Optional描述结果;
9. T orElse(T other):若是存在该值,返回值, 不然返回 other;
10. T orElseGet(Supplier<? extends T> other):若是存在该值,返回值, 不然触发 other,并返回 other 调用的结果;
11. <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier):若是存在该值,返回包含的值,不然抛出由 Supplier 继承的异常;
12. String toString():返回一个Optional的非空字符串,用来调试
复制代码

Optional经常使用方法总结:

Option的一些方法.png

6. Date/time API的改进

在Java8以前的版本中,日期时间API存在不少的问题,好比:

  • 线程安全问题:java.util.Date是非线程安全的,全部的日期类都是可变的;
  • 设计不好:在java.util和java.sql的包中都有日期类,此外,用于格式化和解析的类在java.text包中也有定义。而每一个包将其合并在一块儿,也是不合理的;
  • 时区处理麻烦:日期类不提供国际化,没有时区支持,所以Java中引入了java.util.Calendar和Java.util.TimeZone类;

针对这些问题,Java8从新设计了日期时间相关的API,Java 8经过发布新的Date-Time API (JSR 310)来进一步增强对日期与时间的处理。在java.util.time包中经常使用的几个类有:

  • 它经过指定一个时区,而后就能够获取到当前的时刻,日期与时间。Clock能够替换System.currentTimeMillis()与TimeZone.getDefault()
  • Instant:一个instant对象表示时间轴上的一个时间点,Instant.now()方法会返回当前的瞬时点(格林威治时间);
  • Duration:用于表示两个瞬时点相差的时间量;
  • LocalDate:一个带有年份,月份和天数的日期,可使用静态方法now或者of方法进行建立;
  • LocalTime:表示一天中的某个时间,一样可使用now和of进行建立;
  • LocalDateTime:兼有日期和时间;
  • ZonedDateTime:经过设置时间的id来建立一个带时区的时间;
  • DateTimeFormatter:日期格式化类,提供了多种预约义的标准格式;

示例代码以下:

public class TimeTest {
    public static void main(String[] args) {
        Clock clock = Clock.systemUTC();
        Instant instant = clock.instant();
        System.out.println(instant.toString());

        LocalDate localDate = LocalDate.now();
        System.out.println(localDate.toString());

        LocalTime localTime = LocalTime.now();
        System.out.println(localTime.toString());

        LocalDateTime localDateTime = LocalDateTime.now();
        System.out.println(localDateTime.toString());

        ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
        System.out.println(zonedDateTime.toString());
    }
}
输出结果为:
2018-04-14T12:50:27.437Z
2018-04-14
20:50:27.646
2018-04-14T20:50:27.646
2018-04-14T20:50:27.647+08:00[Asia/Shanghai]
复制代码

7. 其余改进

Java8还在其余细节上也作出了改变,概括以下:

  1. 以前的版本,注解在同一个位置只能声明一次,而Java8版本中提供@Repeatable注解,来实现可重复注解;
  2. String类中提供了join方法来完成字符串的拼接;
  3. 在Arrays上提供了并行化处理数组的方式,好比利用Arrays类中的parallelSort可完成并行排序;
  4. 在Java8中在并发应用层面上也是下足了功夫:(1)提供功能更强大的Future:CompletableFuture;(2)StampedLock可用来替代ReadWriteLock;(3)性能更优的原子类::LongAdder,LongAccumulator以及DoubleAdder和DoubleAccumulator;
  5. 编译器新增一些特性以及提供一些新的Java工具

参考资料

Stream的参考资料:

Stream API讲解

Stream讲解系列文章

对Stream的讲解很细致

Java8新特性之Stream API

Optional的参考资料:

Optional的API讲解很详细

对Optional部分的讲解还不错,值得参考

Java8新特性的介绍:

Java8新特性指南,很好的资料

跟上 Java 8 – 你忽略了的新特性

Java8新特性学习系列教程

相关文章
相关标签/搜索