Java8 新特性学习

1、接口中的默认方法

Java8中允许接口中包含具有具体实现的方法,这种方法被称为“默认方法”,使用default关键字修饰。
如:

public interface MyInterface {

    String notDefault();

    default String testDefault() {
        return "Hello Default Method";
    }
}

该接口的实现类中只需要实现抽象方法即可,默认方法可以直接使用。

MyInterface myInterface = new MyInterface() {
    @Override
    public String notDefault() {
        return "这不是一个default方法,实现类需要重写这个方法";
    }
};

System.out.println(myInterface.notDefault());
System.out.println(myInterface.testDefault());

接口中默认方法的“类优先”原则:

若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时

  • 选择父类中的方法。如果一个父类中提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。 测试代码:
public  class MySuperClazz {

    public String testDefault() {
        return "This is default method";
    }
}

public interface MyInterface {

    default String testDefault() {
        return "Hello Default Method";
    }
}

public class MyClazz extends MySuperClazz implements MyInterface{

}
MyClazz myClazz = new MyClazz();
System.out.println(myClazz.testDefault());

输出结果:

This is default method
  • 接口冲突,如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么实现类必须覆盖该方法来解决冲突。

测试代码:

public class MyClazz /*extends MySuperClazz */implements MyInterface,MyInterface2{

    @Override
    public String testDefault() {
        return "测试两个接口中同名方法";
    }
}

public interface MyInterface2 {

    default String testDefault() {
        return "Hello Default Method2";
    }
}

public interface MyInterface {

    default String testDefault() {
        return "Hello Default Method";
    }
}

测试结果:

测试两个接口中同名方法

Java8的接口中也可以声明静态方法

public interface MyInterface {

    static void testStatic() {
        System.out.println("静态方法");
    }
}

2、Lambda表达式

上面提到的代码

MyInterface myInterface = new MyInterface() {
    @Override
    public String notDefault() {
        return "这不是一个default方法,实现类需要重写这个方法";
    }
};

可以通过Lambda表达式改写成如下方式

MyInterface myInterface = () -> "这不是一个default方法,实现类需要重写这个方法";

这种形式,便是java8提供的更加简洁的方式,即Lambda表达式。相比于jdk1.7之前的代码,使用Lambda表达式更加的简短易读。

Lambda表达式是一种“语法糖”,需要函数式接口的支持。
Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器可以通过上下文推断出数据类型,即:类型推断。Java的编译器能够自动识别参数的类型,所以可以省略类型不写。

Lambda表达式可以分为两部分:
左侧:指定了Lambda表达式需要的所有参数
右侧:指定了Lambda体,即lambda表达式要执行的功能

Lambda表达式有以下几种格式:

  • 语法格式一:无参数,无返回值
格式:
    () -> System.out.println("Hello Lambda");
例:
    Runnable r1 = () -> System.out.println("Hello Lambda");
  • 语法格式二:有一个参数,并且无返回值
格式:
     (x) -> System.out.println(x)
     只有一个参数小括号可以不写
     x -> System.out.println(x)
例:
    Consumer<String> con = (x) -> System.out.println(x);
    Consumer<String> con2 = x -> System.out.println(x);
    con.accept("zhw");
    con2.accept("nb");
  • 语法格式三:有两个参数以上,并且lambda体中有多条语句,并且有返回值,如果有多条语句必须使用{}
例:
    Comparator<Integer> com = (x,y) -> {
        System.out.println("语法格式三");
        return Integer.compare(x,y);
    };
  • 语法格式四:lambda体重只有一条语句,并有返回值,大括号可以省略,return可以省略
Comparator<Integer> com = (x,y) -> Integer.compare(x,y);

Lambda表达式的范围:

对于lambdab表达式外部的变量,其访问权限的粒度与匿名对象的方式非常类似。
你能够访问局部对应的外部区域的局部final变量,以及成员变量和静态变量。
注意:final关键字可以省略,但是编译的时候依然被隐式的当成final变量来处理。

方法引用:

若Lambda体中的内容有方法已经实现了,我们可以使用“方法引用”,(可以理解为方法引用是Lambda表达式的另一种表现形式)

注意:
1、 Lambda体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的参数列表和返回值类型保持一致
2、若lambda参数列表中的第一个参数是 实例方法的调用者,而第二个参数是实例方法的参数时,可以使用ClassName::method的形式

方法引用的三种语法格式:

  • 对象::实例方法名

  • 类::静态方法名

  • 类::实例方法名

构造器引用

格式:

  • ClassName::new

注意:
需要调用的构造器的参数列表要与函数式接口中抽象方法的参数保持一致

3、函数式接口

上文提到的Lambda表达式需要函数式接口的支持那什么是函数式接口呢?所谓的函数式接口就是必须有且仅有一个抽象方法声明的接口,Lambda表达式必须要与抽象方法的声明相匹配,由于默认方法不是抽象的,所以可以在函数式接口中任意添加默认方法。

定义函数式接口的时候可以使用@FunctionalInterface注解,编译器会判断定义的接口是否满足函数是接口的条件,如果不满足,编译器会抛出异常。

@FunctionalInterface
public interface MyFunction<T,R> {

    R get(T t1,T t2);
}

Java8内置的函数式接口:

Consumers

Consumer<T> : 消费型接口,Consumer代表了在一个输入参数上需要进行的操作。
    void accept(T t); 

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Suppliers

Supplier<T>  : 供给型接口,口产生一个给定类型的结果。与Function不同的是,Supplier没有输入参数。

Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person

Functions

Function<T,R>  : 函数型接口,接受一个参数(T),并返回单一的结果(R)。默认方法可以将多个函数串在一起(compse, andThen)。

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"

Predicates

Predicate<T>  : 段言型接口,是一个布尔类型的函数,该函数只有一个参数。Predicate接口包含了多种默认方法,用于处理复杂的逻辑动词(and, or,negate)

Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

4、Stream 数据流

数据流表示元素的序列,并支持不同种类的操作来执行元素上的计算。数据流是单体(Monad),并且在Java8函数式编程中起到重要作用。

流(Stream)到底是什么:是数据渠道,用于操作数据源(集合,数组等)所生成的元素序列“集合讲的是数据,流讲的是计算”。

注意:

  1. Stream自己不会存储元素
  2. Stream不会改变源对象。相反,他们会返回一个持有结果的新的Stream
  3. Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才会执行。

Stream操作的三个步骤:

  • 创建数据流
  • 衔接操作
  • 终止操作

Stream数据流的操作过程图:
这里写图片描述

数据流操作要么是衔接操作(创建过程也可以看成衔接操作),要么是终止操作,数据流的操作大多数是无状态的而且是无干扰的,即当一个函数不修改数据流的底层数据源时,它就是无干扰的;当一个函数的操作不依赖于外部作用域中的任何操作过程中的可变的变量或状态时,它就是无状态的。

创建数据流:

创建Stream的五种方式:
1、 可以通过Collection系列集合中的stream()或者parallelStream()方法获取串行流或者并行流。

List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();

2、 通过Arrays中的静态方法stream()方法获取数组流

 Arrays.asList("a1", "a2", "a3").stream();

3、 通过Stream类中的静态方法of()方法获取流,可以从一系列对象引用中创建数据流

Stream<String> stream = Stream.of(“aa”,”bb”,”cc”);

4、 创建无限流

//迭代
Stream<Integer> stream = Stream.iterate(0,(x) -> x+ 2);
Stream.limit(10).forEach(System.out::println)
//生成
Stream.generate(()->Math.random()).forEach(System.out::println);

5、java8提供了一些特殊种类的流,用于处理基本数据类型int,long,double。分别为IntStream,LongStream,DoubleStream

前四种方式创建的数据流称为“对象数据流”,第五种方式创建的流称为“基本数据流”,基本数据流和对象数据流用法相似,但有一些不同,基本数据流使用的是特殊的lambda表达式,如:IntFunction而不是Function,IntPredicate而不是Predicate。同时基本数据流还支持一些额外的聚合终止操作sum()和average()等。有些时候需要进行对象数据流和基本数据流的转换。转换方式如下:

对象数据流转换成基本数据流:

mapToInt() 、 mapToLong() 和mapToDouble()

例:
Stream.of("a1", "a2", "a3")
        .map(s -> s.substring(1))
        .mapToInt(Integer::parseInt)
        .max()
        .ifPresent(System.out::println); 

基本数据流转换成对象数据流:

mapToObj()

例:
IntStream.range(1, 4)
        .mapToObj(i -> "a" + i)
        .forEach(System.out::println);

衔接操作:

多个衔接操作可以连接起来形成 一个流水线,除非流水线上出发终止操作,否则衔接操作不会执行任何处理,而在终止操作是一次性全部处理,称为“惰性求值”或“延迟执行”,延迟性是衔接操作的一个重要特性。

例:
Stream.of("d2", "a2", "b1", "b3", "c")
        .filter(s -> {
            System.out.println("filter: " + s);
            return true;
        });

上面这段代码并不会向控制台打印任何东西,因为并没有终止操作,衔接操作只有在终止操作调用时才会被执行。

衔接操作主要有以下几种方式:

筛选与切片

filter—接收Lambda,从六中排除某些元素
limit—截断流,使其元素不超过给定数量
skip(n)—跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit互补
distinct—筛选,通过流所生成元素的hashCode()equals()去除重复元素

内部迭代:迭代操作有Stream API完成
外部迭代:自己编写迭代操作

映射

map—接收Lambda,将元素转换成其他形式或提取信息。接收一个函数操作参数,该函数会应用到每个元素上,并将其映射成一个新的元素。

flatMap—接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流链接成一个流

排序

sorted()—自然排序
sorted(Comparator com)—定制排序

终止操作:

在数据流中,只有执行了终止操作时,衔接操作才会一次性执行。终止操作主要有以下几种方式:

查找与匹配

allMatch—检查是否匹配所有元素
anyMatch—检查是否至少匹配一个元素
noneMatch—检查是否没有匹配所有元素
findFirst—返回当前流中的第一个元素
findAny—返回流中元素的任意元素
count—返回流中元素的总个数
max—返回流中的最大值
min—返回流中的最小值

归约

ruduce(T identity,BinaryOperator) / ruduce(BinaryOperator) –可以将流中元素反复结合起来,组合为单一结果。

收集

collect—将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法,将流中的元素存放在不同类型的结果中。Java8通过内置的 Collectors 类支持多种内置的收集器。

List<Person> filtered = persons.stream()
                                .filter(p -> p.name.startsWith("P"))
                                .collect(Collectors.toList());
System.out.println(filtered); // [Peter, Pamela]

5、并行流与串行流

串行流:在一个线程上执行一个内容

Arrays.asList("a1", "a2", "a3")
        .stream()
        .findFirst()
        .ifPresent(System.out::println);

并行流:就是把一个内容分成多个数据快,并用不同的线程分别处理每个数据块的流。并行流能够在多个线程上执行操作。主要是为了解决处理大量元素时的性能问题。

Arrays.asList("a1", "a2", "a3")
        .stream()
        .findFirst()
        .parallel()
        .ifPresent(System.out::println);
--------------------------------------------------------------------
Arrays.asList("a1", "a2", "a3")
        .parallelStream()
        .findFirst()
        .ifPresent(System.out::println);

Stream API 可以声明性的通过parallel()与sequential()在并行流与串行流之间进行切换。
在一个串行流中使用parallel()切换为并行流,在一个并行流中使用sequential()切换为串行流。

Java8对并行流有了更好的支持
并行流使用公共的 ForkJoinPool ,由 ForkJoinPool.commonPool() 方法提供。底层线程池的大小最大为五个线程 – 取决于CPU的物理核数。

Fork/Join框架:在必要的情况下,讲一个大任务,进行拆分(fork)成若干个小任务(拆分到不可拆分时),再将一个个小人物运算的结果进行汇总(join)。
这里写图片描述

采用“工作窃取”算法(work-stealing),Java8之前这种框架使用的并不多,主要是因为这种方法实现起来过于麻烦,而Java8将实现的方法变得非常简单。

“工作窃取”算法:算法是指某个线程从其他队列里窃取任务来执行

对于常见的一个大型任务,我们可以把这个大的任务切割成很多个小任务,然后这些小任务会放在不同的队列中,每一个队列都有一个相应的的工作执行线程来执行,当一个线程所需要执行的队列中,任务执行完之后,这个线程就会被闲置,为了提高线程的利用率,这些空闲的线程可以从其他的任务队列中窃取一些任务,来避免使自身资源浪费,这种在自己线程闲置,同时窃取其他任务队列中任务执行的算法,就是工作窃取算法

6、Optional类

Optional类是一个容器类,代表一个值存在或不存在,可以避免空指针异常。Optional不是一个函数式接口,而是一个精巧的工具接口,用来方式NullPointException异常。Optional是一个简单的值容器,这个值可以是null,也可以是non-null,为了不直接返回null,我们在Java 8中就返回一个Optional类,使用Optional类可以避免使用if进行null检查,这种检查的过程交由底层自动处理。主要有如下一些方法:

of(T t) : 创建一个Optional实例
empty() : 创建一个空的Optional实例
ofNullable(T t) :  若t不为null,创建Optional实例,否则创建空的Optional实例
isPresent() : 判断是否包含值
orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
orElseGet(Suppplier s) : 如果调用对象包含值,返回该值,否则返回s获取的值
map(Function f) : 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
flatMap(Function mapper) : 与map类似,要求返回值必须是Optional

7、JAVA8全新的时间包

Java 8 包含了全新的时间日期API,这些功能都放在了java.time包下。主要有如下一些方法:

LocalDate,LocalTime,LocalDateTime : 时间
Instant : 时间戳(以Unix元年:19701月一日 00:00:00到某个时间时间的毫秒值)
Duration : 计算两个“时间”之间的间隔
Period : 计算两个“日期”之间的间隔
TemporalAdjust :时间校正器
DateTimeFormatter :格式化时间/日期
ZonedDate,ZonedTime,ZonedDateTime : 时区

猜你喜欢

转载自blog.csdn.net/zhwyj1019/article/details/80585245