java8 新特性 Lambda 函数式接口 方法引用 Optional Stream 默认接口方法

  1. 简介

毫无疑问,Java 8是Java自Java 5(发布于2004年)之后的最重要的版本。
这个版本包含语言、编译器、类库、工具和JVM等方面的十多个新特性。
语言 : 引入函数式编程思想中的lambda,Function,流式处理,默认接口方法.
编译器: 参数名称
类库: date/time类,Optional,红黑树优化hashMap,cas优化concurrentHashMap.
工具: jjs,jdeps
JVM : Metaspace,红黑树内存模型

  1. Lambda表达式

2.1 行为参数化

行为参数化简单的说就是函数的主体仅包含模板类通用代码,而一些会随着业务场景而变化的逻辑则以参数的形式传递到函数之中,采用行为参数化可以让程序更加的通用,以应对频繁变更的需求。简单点说就是将方法作为形参在方法间传递.
考虑一个业务场景,假设我们需要通过程序对苹果进行筛选.
苹果实体名字叫 Apple 包括如下属性:id,color,origin,weight.

用户最开始的需求可能只是简单的希望能够通过程序筛选出绿色的苹果,
于是我们可以很快的通过程序实现:

List<Apple> filterGreenApples(List<Apple> apples){
  List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (Color.GREEN.equals(apple.getColor())) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

如果过了一段时间用户提出了新的需求,希望能够通过程序筛选出红色的苹果,
于是我们又针对性的添加了筛选红色苹果的功能:

List<Apple> filterRedApples(List<Apple> apples){***}

更好的实现是把颜色作为一个参数传递到函数中,
这样就可以应对以后用户提出的各种颜色筛选请求了:

List<Apple> filterApplesByColor(List<Apple> apples, Color color){
  ***
  if(color.equals(apple.getColor())) //唯一变化的地方
  ***
}

再也不用担心用户的颜色筛选需求变化了,但是不幸的是,某一天用户提了一个需求要求能够选择重量达到某一标准的苹果,有了前面的教训,我们也把重量的标准作为参数传递给筛选函数,于是得到:

List<Apple> filterApplesByColorAndWeight(List<Apple> apples, Color color, float weight){
  ***
  if (color.equals(apple.getColor()) && apple.getWeight() >= weight) //唯一变化的地方
  ***
}

如果筛选条件越来越多,组合模式越来越复杂,我们是不是需要考虑到所有的情况,并针对每一种情况都有相应的应对策略呢,并且这些函数仅仅是筛选条件的部分不一样,其余部分都是相同的模板代码(遍历集合),这个时候我们就可以将行为 参数化 ,让函数仅保留模板代码,而把筛选条件抽离出来当做参数传递进来,在java8之前,我们通过定义一个过滤器接口来实现:

/** * 苹果过滤接口 */
@FunctionalInterface
public interface AppleFilter {
    boolean accept(Apple apple);
}

/** * 将筛选条件封装成接口 */
public static List<Apple> filterApplesByAppleFilter(List<Apple> apples, AppleFilter filter) {
    List<Apple> filterApples = new ArrayList<>();
    for (final Apple apple : apples) {
        if (filter.accept(apple)) {
            filterApples.add(apple);
        }
    }
    return filterApples;
}

通过上面行为抽象化之后,我们可以在具体调用的地方设置筛选条件,并将条件作为参数传递到方法中:

public static void main(String[] args) {
    List<Apple> apples = new ArrayList<>();

    // 筛选苹果
    List<Apple> filterApples = filterApplesByAppleFilter(apples, new AppleFilter() {
        @Override
        public boolean accept(Apple apple) {
            // 筛选重量大于100g的红苹果
            return Color.RED.equals(apple.getColor()) && apple.getWeight() > 100;
        }
    });
}

上面的行为参数化方式采用匿名类来实现,这样的设计在jdk内部也经常采用,比如java.util.Comparator,java.util.concurrent.Callable等,使用这一类接口的时候,我们都可以在具体调用的地方用过匿名类来指定函数的具体执行逻辑.
说道这里很容易想到,设计模式中的模板模式(Template Pattern):
一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
使用模板方法模式也可以解决类似问题,但会出现继承关系.
好了,之前的解决方式说完了,现在一起来看看在java8中我们怎样通过lambda来简化它:

// 筛选苹果
List<Apple> filterApples = filterApplesByAppleFilter(apples,
        (Apple apple) -> Color.RED.equals(apple.getColor()) && apple.getWeight() >= 100);

通过lambda表达式极大的精简了代码,下面来学习java的lambda表达式吧~

2.2 lambda表达式语法定义

lambda 表达式的语法格式如下:
一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示:
(parameters) -> {statements; }
例如 :

(String s, Integer i) ->{
  Integer si = Integer.valueOf(s);
  return i + si;
}

以下是lambda表达式的语法特征:
*·可选的大括号:如果主体包含了一个语句,就不需要使用大括号。

(String s, Integer i) -> return Integer.valueOf(s) + i;

*·隐含return关键字:无需显式的写return关键字.但是当表达式是一个语句集合的时候,则需要显式添加return,并用花括号{ }将多个表达式包围起来

(String s, Integer i) -> Integer.valueOf(s) + i;

*·可选类型声明:即类型推断.编译器会根据参数、返回类型、异常类型(如果存在)等做正确的判定.

(s, i) -> Integer.valueOf(s) + i;

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

i -> i * i;

2.3 lambda表达式的注意事项

在Lambda表达式中访问外层作用域和旧版本的匿名对象中的方式类似。你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。
但是和匿名对象不同的是,lambda表达式的局部变量可以不用声明为final.因为发现变量被lambda表达式引用后,编译器会隐式的把其当成final来处理.

final int num = 1;
(param) -> param + num;
// 运行结果 3

通过上面的描述也就意味着,在lambda表达式中是不可以修改局部变量的.

final int num = 1;
(param) -> num = param + num;
// 运行报错 Local variable waibu defined in an enclosing scope must be final or effectively final

在 Lambda 表达式当中被引用的变量的值不可以被更改。

public void repeat(String string, int count) {
        Runnable runnable = () -> {
            for (int i = 0; i < count; i++) {
                string = string + "a";//编译出错
                System.out.println(this.toString());
            }
        };
        new Thread(runnable).start();

Lambda 表达式中使用 this 会引用创建该 Lambda 表达式的方法的 this 参数。

public class Test2 {
    public static void main(String[] args) {
        Test2 test = new Test2();
        test.method();
    }
    @Override
    public String toString() {
        return "Lambda";
    }
    public void method() {
        Runnable runnable = () -> {
            System.out.println(this.toString());
        };
        new Thread(runnable).start();
    }
    //运行结果 : Lambda

Lambda表达式不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。
Lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。
此外,this关键字及其引用,在Lambda表达式内部和外部也拥有相同的语义。

  1. 函数式接口

3.1 函数式接口解决什么问题?

lambda表达式的使用需要借助于函数式接口,也就是说只有函数式接口出现地方,我们才可以将其用lambda表达式进行简化。

3.2 什么是函数式接口

只包含一个抽象方法的接口,称为函数式接口.

3.3 知识点

3.3.1 @FunctionalInterface 注解

可以在任意函数式接口上使用 @FunctionalInterface 注解,当添加了该注解之后,编译器就限制了该接口只允许有一个抽象方法,否则报错.
同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
所以推荐为函数式接口添加该注解

3.3.2 函数式接口里允许定义默认方法

java8在接口定义上的改进就是引入了默认方法,使得我们可以在接口中对方法提供默认的实现.
但是不管存在多少个默认方法,只要具备一个且只有一个抽象方法,那么它就是函数式接口.

3.3.3 函数式接口里允许定义静态方法

函数式接口里是可以包含静态方法,因为静态方法不能是抽象方法,是一个已经实现了的方法,所以是符合函数式接口的定义的;

3.4 java提供的函数式接口

类型接口名称抽象方法描述符原始类型特化备注
断言Predicateboolean test(T var1);T -> booleanIntPredicate, LongPredicate, DoublePredicate接受一个输入参数,返回一个布尔值结果
BiPredicate<T, U>boolean test(T t, U u);(T, U) -> boolean接受两个输入参数,返回一个布尔值结果
消费Consumervoid accept(T var1);T -> voidIntConsumer, LongConsumer, DoubleConsumer接受一个输入参数并且无返回的操作
BiConsumer<T, U>void accept(T var1, U var2);(T, U) -> void接受两个输入参数的操作,并且不返回任何结果
函数Function<T, R>R apply(T t);T -> RIntFuncation,IntToDoubleFunction,IntToLongFunction,LongFuncation…接受一个输入参数,返回一个结果
BiFunction<T, U, R>R apply(T var1, U var2)(T, U) -> R接受两个输入参数的方法,并且返回一个结果
供应SupplierT get();() -> TBooleanSupplier,IntSupplier,LongSupplier,DoubleSupplier无参数,返回一个结果
操作UnaryOperatorR apply(T t);T -> TIntUnaryOperator, LongUnaryOperator,DoubleUnaryOperator对单个参数执行操作,然后返回同类型结果
  1. 方法引用

通过方法的名字来指向一个方法.
方法引用使用一对冒号 :: 方法引用有很多种,它们的语法如下:
构造方法引用:Class::new
数组构造方法引用:TypeName[]::new
静态方法引用:ClassName::methodName
特定类的任意对象的方法引用:ClassName::methodName
特定对象的方法引用:instanceReference::methodName
超类上的实例方法引用:super::methodName

package com.wj.java8.testPackage.methodUseTest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;

/**
 * 方法引用
 */
class Fruit {
    private String name;
    //Supplier是jdk1.8的接口,这里和lamda一起使用了
    public static Fruit create(final Supplier<Fruit> supplier) {
        return supplier.get();
    }

    public static void collide(final Fruit car) {
        System.out.println("Collided " + car.toString());
    }

    public void follow(final Fruit another) {
        System.out.println("Following the " + another.toString());
    }

    public void repair() {
        System.out.println("Repaired " + this.toString());
    }

    public static void main(String[] args) {
        //构造器引用,语法是Class::new
        final Fruit fruit = Fruit.create( Fruit::new );
        final List< Fruit > cars = Arrays.asList( fruit );
        //静态方法引用:语法是Class::static_method
        cars.forEach(Fruit::collide);
        //特定类的任意对象的方法引用:语法是Class::method
        cars.forEach(Fruit::repair);
        //特定对象的方法引用:语法是instance::method
        final  Fruit fruit1 = Fruit.create(Fruit::new);
        cars.forEach(fruit1::follow);
        List fruits = new ArrayList();

        fruits.add("apple");
        fruits.add("pear");
        fruits.add("banana");
        fruits.add("peach");
        fruits.add("grape ");

        fruits.forEach(System.out::println);

    }
}



Comparator.comparing(u -> u.getUserName());

我们可以用方法引用替换上面的lambda表达式

Comparator<User> comparator = Comparator.comparing(User::getUserName);
  1. Optional类

Optional是一个为了解决NullPointerException而设计的.它是一个可以包含对象也可以包含null的容器对象.
不是对null关键字的一种替代,而是对于null判定提供了一种更加优雅的实现。

5.1 直观感受

  1. 怎么解决的NullPointerException? 答: 不在运行时才发现,而是在编码时必须考虑null.
  2. 为什么说是容器对象? 答: 提供了和其他容器对象相同的访问方法.map(),flatMap(),filter()等.
  3. 哪里优雅了? 答: 减少了null关键字的出现,通过ifPresent(),orElse(),orElseGet(),orElseThrow()等方法避免了防御性检查.

5.2 基本使用

  1. Optional对象创建
    static Optional of(T value)
    为非null的值创建一个Optional,如果值为null,则会抛出NullPointerException

    Optional optional = Optional.of(13);
    System.out.println(optional)

static Optional ofNullable(T value)
为指定的值创建一个Optional,如果指定的值为null,则返回一个空的Optional。

Optional<String> optional2 = Optional.ofNullable(null);
System.out.println(optional2);
optional2 = Optional.ofNullable("aaa");
System.out.println(optional2);

static Optional empty()
返回空的 Optional 实例。

Optional<Integer> optional = Optional.empty();
  1. Optional对象访问
    Optional map(Function<? super T,? extends U> mapper)
    如果有值,则对其执行调用映射函数得到返回值。如果返回值不为 null,则创建包含映射返回值的Optional作为map方法返回值,否则返回空Optional。

    Optional.of(13).map(o -> c.toLowerCase()).orElse(12);
    Optional.ofNullable(null).map(o -> c.toLowerCase()).orElse(12);

Optional flatMap(Function<? super T,Optional> mapper)
如果值存在,返回基于Optional包含的映射方法的值,否则返回一个空的Optional.
可以将一个二维的Optional对象映射成一个一维的对象

int fr = Optional.of(Optional.of(13)).flatMap(f -> f).get();
Optional<Integer> mr = Optional.of(Optional.of(13)).map(m -> m).get();

Optional filter(Predicate<? super predicate)
如果值存在,并且这个值匹配给定的 predicate,返回一个Optional用以描述这个值,否则返回一个空的Optional。

Optional.ofNullable(13).filter( u -> u == 13).ifPresent(u ->  System.out.println(" is 13."));
  1. Optional对象使用
    T get()
    如果在这个Optional中包含这个值,返回值,否则抛出异常:NoSuchElementException
    boolean isPresent()
    如果值存在则方法会返回true,否则返回 false。
    T orElse(T other)
    如果存在该值,返回值, 否则返回 other。
    T orElseGet(Supplier<? extends T> other)
    如果存在该值,返回值, 否则触发 other,并返回 other 调用的结果。
    T orElseThrow(Supplier<? extends X> exceptionSupplier)
    如果存在该值,返回包含的值,否则抛出由 Supplier 继承的异常
    void ifPresent(Consumer<? super T> consumer)
    如果值存在则使用该值调用 consumer , 否则不做任何事情。

  2. 注意事项
    谨慎使用isPresent()和get()方法,尽量多使用map()、filter()、orElse()等方法
    因为Optional没有实现序列化,所以Optioanl通常不被建议作为参数或字段类型使用.–仅作为方法返回值使用

  3. 流式数据处理Stream

6.1 什么是Stream

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream,Stream 也不是集合元素,它不是数据结构并不保存数据。
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。它以直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API 借助于 Lambda 表达式,极大的提高编程效率和程序可读性。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
同时它提供串行和并行两种模式进行聚合操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。
流的数据源本身可以是无限的.
通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。
所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。
一个流式处理可以分为三个部分
生成操作 -> 中间操作 -> 终端操作

生成操作 中间操作 终端操作
集合流 filter allMatch
数组流 map reduce
文件流 sorted Collect

6.1 生成操作

生成操作或者叫源处理,是指使用不同的数据源生成Stream的动作.

  1. Collection
    • Collection.stream() //new ArrayList<>().stream();
    • Collection.parallelStream() //new ArrayList<>().parallelStream();
    • Arrays.stream(T array) //Arrays.stream(new String[]{“a”, “b”, “c”, “d”, “e”, “f”})
    • Stream.of() //Stream.of(1,2,3)
  2. BufferedReader
    • java.io.BufferedReader.lines() //Files.lines(path, Charset.forName(“UTF-8”));
  3. 静态工厂
    • java.util.stream.IntStream.range() //IntStream.range(1, 3);
    • java.nio.file.Files.walk() //遍历文件夹
  4. 自己构建
    • java.util.Spliterator //可分割迭代器
  5. 其他
    • Random.ints() //返回一个无限的IntStream,源数据为随机数
    • Pattern.splitAsStream(java.lang.CharSequence)

启动并行流式处理很简单 只需要使用parallelStream()方法即可.
其本质上基于java7的Fork-Join框架实现,其默认的线程数为宿主机的内核数。
将大任务进行拆分,变成多个双端队列,且可以窃取.

6.2 中间操作(intermediate operation)

中间操作主要是打开流,做出某种程度的数据映射或过滤,然后返回一个新的流,交给下一个操作使用。
执行中间操作后的Stream 对象不改变,对新的Stream继续执行中间操作,这就允许中间操作可以像链条一样排列,变成一个管道.
中间操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
所以不用担心多个中间操作会遍历多变数据源,因为多个中间操作只会在 终端 操作的时候拼在起来,一次循环完成。
常用的中间操作
参数为Function,将该function应用于流中的所有元素
Stream map(Function<? super T, ? extends R> mapper);
参数为Function,Functionde 返回值为Stream类型,能够将一个二维的集合映射成一个一维的集合,比map()方法拥有更高的映射深度
Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

List<String> list = Arrays.asList("1 2", "3 4", "5 6");
list.stream().flatMap(item -> Arrays.stream(item.split(" "))).forEach(System.out::println);
ist.stream().map(item -> Arrays.stream(item.split(" "))).forEach(n ->n.forEach(System.out::println));

参数为Predicate,用于过滤
Stream filter(Predicate<? super T> predicate);
操作可以分为两种 完全的中间操作(full intermediate) 如 : map 短路的中间操作(short-circuiting) 如: filter
顾名思义完全的执行后源数据数量不变,而短路的执行后可以将一个无限流转换为有限流
更多的中间操作
filter、 distinct、 sorted、 peek、 limit、 skip、 parallel…

6.2 终端操作(Terminal operation)

终端操作会真正的开始遍历数据,并且生成一个结果或者副作用(side effect).
一个流中只有一个终端操作,执行过后流的元素就被消费了.
参数为Predicate,用于判断是否全部匹配
boolean allMatch(Predicate<? super T> predicate);
没有入参,用于返回第一个元素
Optional findFirst();
参数为 BinaryOperator,又称折叠操作,
Optional reduce(BinaryOperator accumulator);
规约操作的示例 :

// 前面例子中的方法
int totalAge = students.stream()
                .filter(student -> "计算机科学".equals(student.getMajor()))
                .mapToInt(Student::getAge).sum();
// 归约操作
int totalAge = students.stream()
                .filter(student -> "计算机科学".equals(student.getMajor()))
                .map(Student::getAge)
                .reduce(0, (a, b) -> a + b);

// 进一步简化
int totalAge2 = students.stream()
                .filter(student -> "计算机科学".equals(student.getMajor()))
                .map(Student::getAge)
                .reduce(0, Integer::sum);

// 采用无初始值的重载版本,需要注意返回Optional
Optional<Integer> totalAge = students.stream()
                .filter(student -> "计算机科学".equals(student.getMajor()))
                .map(Student::getAge)
                .reduce(Integer::sum);  // 去掉初始值

有了规约操作和java.util.stream.Collectors(收集器)中提供的toSet(),toMap,等方法我们可以轻松的进行集合间的转换.
这些收集器广义上均基于Collectors.reducing()实现。
更多的终端操作
forEach、 anyMatch、 noneMatch、 findFirst、 findAny…

通过peek和foreach可以清晰的感受到中间操作和终端操作的区别

Stream.of(1,2).peek(this :: doSomething).peek(this :: doSomething); //正确
Stream.of(1,2).forEach(this :: doSomething).forEach(this :: doSomething); //第二个forEach处报错.
  1. 默认接口方法

默认接口方法是指在接口中可以有实现方法,不需要实现类去实现其方法.
语法很简单,只需要在方法名前面加个 default 关键字:
public interface A {
default void print(){
System.out.println(“This is a default method!”);
}
}

7.1 为什么会出现?

1.8之前的接口,当需要修改接口中的抽象方法时需要修改全部的接口实现.
通常是依据开闭原则在原有接口中新增一个抽象方法.然后实现该接口的类都必须为新添加的方法添加相应的实现.
但是这样的做法兼容性很差,多了很多无意义的实现.

7.2 和抽象类的区别

一个类只能继承一个类,但是可以实现多个接口
抽象类可以定义变量,而接口不能

7.3 冲突解决

正因为一个类可以实现多个接口,所以当多个接口的默认方法的方法签名相同时就会产生冲突.
冲突解决三原则

  1. 类或父类中显式声明的方法,其优先级高于所有的默认方法

  2. 选择与当前类距离最近的具有具体实现的默认方法

  3. 需要显式指定接口
    `

    interface A{
    default void hello() {
    System.out.println(“from A”);
    }
    }

    interface B extends A{
    default void hello() {
    System.out.println(“from B”);
    }
    }

    class C implements B, A {
    public static void main(String[] args) {
    new C().hello();//打印B
    }
    }

8.日期

10.参考文章

https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/
https://www.jianshu.com/p/2b40fd0765c3
http://www.codeceo.com/article/streaming-data-processing-of-java-8.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值