java8特性

Lambda

Lambda是一种函数式编程,可以不严谨的理解为一个函数,是一套关于函数f(x)定义、输入量、输出量的计算方案。除了输入输出以及函数内容,其他的lambda都不关心:

factory = new Factory() {
    @Override
    public Object getObject() {
        return new User();
    }
}

// 只关心 输入 - ()  输出 factory  内容 {}
factory = () -> {
    return new User();
}

函数式编程的主要特点:

  1. 函数是一等公民

  2. 可以赋值给变量

  3. 可以作为其他函数的参数进行传递

  4. 可以作为其他函数的返回值

Lambda语法格式

  1. 格式1

    (parameters) -> {statements;}
    parameters: 函数的参数列表
    statements: 函数的执行语句
    -> : 代表使用指定的参数去完成某项功能
    
    public int method(int a, int b) {
        return a + b;
    }
    
    (int a, int b) -> {return a + b;}
  2. 格式2

    (parameters) -> expression
    parameters: 函数的参数列表
    statements: 表达式(一定会有一个结果)
    -> : 代表使用指定的参数去完成某项功能
    
    public int method(int a, int b) {
        return a + b;
    }
    
    (int a, int b) -> a + b

其他规则:

  1. 可选的大括号,当函数体只包含一个语句,可以省略大括号

    (int a) -> return a + 5;
  2. 可选的参数类型声明,编译器可以根据参数值进行推断

    interface Cal {
        int cal(int a);
    }
    // 这里编译器可以根据接口Cal推断出lambda函数的参数类型为int,故可以省略
    Cal cal = (a) -> return a + 5;
  3. 可选的小括号,如果只有一个参数,可以省略小括号

    a -> return a + 5;
  4. 可选的return关键字,如果函数体只有一个表达式,且运算结果匹配返回类型,return可以省略

    a -> a + 5

Lambda使用前提(函数式接口)

因为在Java中没有函数的概念,只有方法的概念,所有的方法一定从属于一个对象或者Class,所以为了可以表达函数的概念,提供了函数式接口。函数式接口是Java中一种特殊的接口:

  1. 有且只有一个抽象方法的接口是函数式接口

    public interface Factory {
        Object getObject();
    }
  2. 满足一条件的就是函数式接口,可以通过@FunctionalInterface注解标记,被标记的接口如果不符合函数式接口的条件将会报错

    @FunctionalInterface
    public interface Factory {
        Object getObject();
    }

看到函数式接口,就代表定义了一个这样的函数签名

常见的函数式接口

Runnable / Callable

略。

Comparator

略。

Supplier

Supplier即提供实例的供应商。该接口的定义,是获取一个结果,或者说,返回一个指定类型的实例,而且,针对同一个类型,不保证每次返回实例相同。

当我们把一个实例的类型、数据等信息收集好了,就可以交给Supplier接口(通过反射)去完成实现,当需要的时候就通过get()返回那个实例,这就是懒加载。在Spring和JDK中都有这么用过。

//创建Supplier容器,声明为TestSuppler类型,此时并不会调用对象的构造方法,即不会创建对象
Supplier<TestSupplier> sup= TestSupplier::new;

//调用get()方法,此时会调用对象的构造方法,即获得到真正对象
sup.get();
//每次get都会调用构造方法,即获取的对象不同
sup.get();

应用场景:

  1. 配合Future(J.U.C),把返回的信息封装/设置成具体类型的实例;

  2. 在流操作中,获取源数据(资源文件、管道等)的实例,封装各种buffer的实例等,包括反射获取source;

  3. java.util.stream,用作返回收集、分割、查找、过滤等操作的实例;

  4. 在日志系统中,封装一个“消息提供者”

    logger.log(level, msgSupplier, thrown);
  5. 在网络编程中,封装二进制数据

    Supplier<? extends ByteBuffer> binarySupplier;
  6. 在Spring的实例初始化时,在AbstracBeanDefinition中提供了实例供应商,用于回调生成bean

    private Supplier<?> instanceSupplier;
    Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
    if (instanceSupplier != null) {
        return obtainFromSupplier(instanceSupplier, beanName);
    }
  7. 常用于设计模式:委托、工厂等

Consumer

对象消费者:

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T> {

    // 接收一个t对象并处理消费
    void accept(T t);

    // 提供链式调用方式执行,先执行本身的accept在执行传入参数after.accept方法
    // 如果在执行调用链时出现异常,会将异常传递给调用链功能的调用者,且发生异常后的after将不会在调用
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

典型应用场景:

// Iterable 接口的 defult方法:forEach
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

BiConsumer

代表连续消费两个入参的操作:

BiConsumer<Integer,Integer> accept=(x,y)->{};

Predicate

谓语,Predicate代表一个断定式子,其函数名为 test

  1. 评估参数里面的表达式

  2. 返回值是一个boolean类型

示例:

Predicate<String> predicate = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.equals("zhangsan");
    }
};
System.out.println(predicate.test("lisi"));

提供的其他的default方法:

  1. and, 等同于短路与&&

  2. or,等同于逻辑或||

  3. negate,等同于逻辑非!

  4. isEqual,判断两个对象是否相同,实际是调用Objects.equals方法

注意,上述方法都返回当前Predicate函数对象,所以可以进行链式调用,但是test方法返回true。

Function

@FunctionalInterface
public interface Function<T, R> {

    // 传入类型T对象将其转换为类型R对象
    R apply(T t);

    // 返回一个新的Function,他会在当前函数之前先执行before函数,和andThen相反
    // 可以将整个执行想成一个链,往当前函数前面插入一个函数
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    // 返回一个新的Function,他会在当前函数之后先执行after函数,和compose相反
    // 可以将整个执行想成一个链,往当前函数后面插入一个函数
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    // 返回一个输入什么对象,就输出什么对象的Function函数
    // 如果某些方法需要Function但是又不需要对数据进行特殊处理的时候,可以使用这个函数
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

BiFunction

接受输入两个参数,返回一个结果

BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
//return 30
System.out.println(add.apply(10,20));

Lambda底层实现

Lambda表达式实际上是匿名内部类的匿名实现。

方法引用

方法引用是对Lambda表达式的再次简化:

printStr(s -> {
    System.out.println(s);
})

// 上述代码中,整个Lambda表达式仅仅只调用了一个println方法
// 也就是说整个Lambda要执行的函数体已经在println方法中存在了
// 我们就认为Lambda表达式的存在就是冗余的了
// 这时可以使用方法引用替代上述代码

printStr(System.out::println); 
// 上述代码的作用为,提取println的方法的内容,也就是方法引用,作为本来要传入的lambda表达式给printStr方法
// 格式为: 方法持有方::方法名
// 如果是实例方法,就是 对象::方法名     称为普通方法引用
// 如果是静态方法,就是 类::方法名       称为静态方法引用

方法引用语法格式

方法引用运算符:::

哪儿些方法可以引用?

  1. 类方法:Integer::parseInt

  2. 构造方法:Student::new

  3. 实例方法:System.out::printlnsuper::方法名this::方法名int[]::new 数组方法引用

可以采用引用的前提:

  1. 参数列表相同

  2. 返回值类型兼容

方法引用底层实现

与Lambda的原理一致,本质也是匿名内部类。

Stream

关注做什么,而不是怎么做。

  1. 专注对容器对象的聚合操作

  2. 提供串行/并行两种模式(fork/join框架拆分任务)

  3. 提高编程效率与可读性

Stream常用API

Stream流的API大致可以分为两大类:

  1. 中间操作,可以有零个或多个,打开流,过滤/映射,返回新流

    map
    filter
    distinct
    sorted
    peek
    limit
    skip
    parallel
    sequential
    unordered
    concat
  2. 终结操作,只能有一个的最后的操作,调用终结操作Stream流将会被关闭。

    终结操作也是一种短路操作,可以根据情况中断流处理。

    forEach
    forEachOrdered
    toArray
    reduce
    collect
    min
    max
    count
    iterator
    anyMatch
    allMatch
    noneMatch
    findFist
    findAny

构建Steam

// list集合转换Stream流
List<String> list = new ArrayList<>();
Stream<String> s1 = list.stream();

// set集合转换Stream流
Set<String> set = new HashSet<>();
Stream<String> s2 = set.stream();

// map转换Stream流
Map<String, String> map = new HashMap<>();
Stream<String> s3 = map.keySet().stream();
Stream<String> s4 = map.valueSet().stream();
Stream<String> s4 = map.entrySet().stream();

// 数组转换Stream流
Integer[] arr = {1, 2, 3, 4, 5, 6};
Stream<Integer> s5 = Stream.of(arr);

// 使用Stream.Builder
// 构建IntStream
IntStream.builder()
        .add(1)
        .add(99)
        .add(-6)
        .build();
// 构建LongStream
LongStream.builder()
        .add(1L)
        .add(99L)
        .add(-6L)
        .build();
// 构建DoubleStream
DoubleStream.builder()
        .add(1.0)
        .add(99.0)
        .add(-6.1)
        .build();
// 构建Stream
Stream.builder()
        .add("a")
        .add(new Object())
        .add(100)
        .build();

collect 收集

R collect(Collector<? super T, A, R> collector);

Collector收集器有一个对应的工具类 Collectors,可以返回一些比较常用的收集器:

// 收集到List
stringCollection.stream()
    .filter(x -> x.startsWith("a")).collect(Collectors.toList());
// 收集到Set
stringCollection.stream()
    .filter(x -> x.startsWith("a")).collect(Collectors.toSet());
// 收集到Collection
stringCollection.stream()
    .filter(x -> x.startsWith("a"))
    .collect(Collectors.toCollection(ArrayList::new));
// 收集到Map
stringCollection.stream()
    .filter(x -> x.startsWith("a"))
    .distinct()
    .collect(Collectors.toMap(Function.identity(), xu));
// 收集到ConcurrentHashMap
Collectors.toConcurrentMap();

并行Stream

stream()方法产生的流是串行的,也就是说流中的操作在一个线程中运行。而通过parallelStream()创建的流则是一个并行的流,他的内部是一个ForkJoinPool,处理时拆分处理,最后将结果合并。

获取并行流:

List<Integer> list = new ArrayList<>();
// 通过parallelStream()方法获取一个并行流
list.parallelStream();
// 通过parallel方法将流转换为一个并行流
Stream.of(1, 2, 3).parallel();

并行流因为基于ForkJoinPool,所以也有线程安全的问题(在流处理函数中访问非线程安全的变量)。个人认为,当数据量处理过慢,或者集合过大,应该优先从其他方面优化(提升执行速度、减少数据量),而不是一定要使用并行流。

使用Optional处理Null

三种构造

// obj 不能为null值,否则直接快速抛出NullpointException
Optional.of(obj);

// 允许为null,但是如果传入null,就得到了 Optional.empty()
//         不是null,则会获取 Optional.of(obj)
Optional.ofNullable(obj)

// 空的Optional
Optional.empty()

何时使用每种构造?

Optional.of(obj):

  1. 明确obj不可能为null

  2. 明确obj为null,并快速抛出异常

Optional.ofNullable(obj):

  1. 不明确obj是否为null,又要对obj进行处理的

存在即返回,无则提供默认值

User u = Optional.ofNullable(user).orElse(User.UNKNOWN_USER);

存在即返回,无则由函数生成

User u = Optional.ofNullable(user).orElseGet(() -> new User());

存在则返回, 无则抛出异常

User u = Optional.ofNullable(user).orElseThrow(() -> new IllegalArgumentException());

变种,存在则返回,无则抛出空指针异常:

User u = Objects.requireNonNull(user,"用户信息获取异常,无法进行操作");

存在才执行, 无则不会执行

Optional.ofNullable(user).ifPresent(uu -> System.out.println(uu));

map 处理级联数据

有一 user 对象,不知是否为null

如果为null,返回null

如果不为null,返回 name

如果name 为null,返回 null

如果name 不为null, 将name 转换为大写

这种级联的null 处理,需要使用map, map可以嵌套无数层。

String n = Optional.ofNullable(user).map(u -> u.getName()).map(name -> "姓名:" + name).orElse(null);

flatMap:

flatMapmap 类似,只是参数不同,他的函数参数需要返回 Optional 类型:

String n = Optional.ofNullable(user).flatMap(u -> Optional.ofNullable(u.getName())).map(name -> "姓名:" + name).orElse(null);

总结

一句话小结: 使用 Optional 时尽量不直接调用 Optional.get() 方法, Optional.isPresent() 更应该被视为一个私有方法, 应依赖于其他像 Optional.orElse(), Optional.orElseGet(), Optional.map() 等这样的方法.

新的日期API

JSR-310规范提供一个新的和改进的Java日期与时间API,该规范领导者Stephen Colebourne就是joda-time作者,因此很多环节很像joda-time。

JDK8的新的设计时间日期API,位于java.time下面,并且都是线程安全的:

  1. LocalDate 本地日期

  2. LocalTime 本地时间

  3. LocalDateTime 本地日期时间

  4. DateTimeFormatter 日期时间格式化类

  5. Instant 时间戳

  6. Duration 时间段(两个时间的间隔)

  7. Period 日期段(两个日期的间隔)

  8. ZonedDateTime 具有时区的日期时间

此外,Java中使用的历法是ISO 8601日历系统,也就是公历。平年有365天,闰年有366天。此外,Java8还提供了4套其他的历法,他们分别是:

  1. ThaiBuddhistDate :泰国佛教历

  2. MinguoDate :中华民国历

  3. JapaneseDate :日本历

  4. HijraDate :伊斯兰历

旧版日期时间API缺陷

  1. 日期时间类设计不合理,有java.util.date以及java.sql.Date两个类。其中前者拥有日期和时间,后者仅仅拥有日期。

  2. 日期时间格式化设计不合理,在java.text下。

  3. 非线程安全的,所有的日期类都是可变的。

  4. 时区处理麻烦,日期类并不提供国际化,没有时区的支持。

LocalDate

// 创建指定日期
LocalDate date1 = LocalDate.of(2021, 5, 6);
System.out.println(date1); // 2021-05-06

// 创建当前日期
LocalDate now = LocalDate.now();
System.out.println(now); // 2022-08-25

// 获取对应的日期信息
System.out.println(now.getYear()); // 2022
System.out.println(now.getMonth()); // 枚举值 AUGUST
System.out.println(now.getDayOfMonth());// 25
System.out.println(now.getDayOfWeek()); // 枚举值 THURSDAY

LocalTime

// 创建指定的时间
LocalTime time = LocalTime.of(6, 26, 33, 23145);
System.out.println(time);

// 得到当前时间
LocalTime now = LocalTime.now();
System.out.println(now);

// 获取时间信息
System.out.println(now.getHour());// 小时
System.out.println(now.getMinute());// 分钟
System.out.println(now.getSecond());// 秒
System.out.println(now.getNano());// 纳秒

LocalDateTime

// 创建指定的时间
LocalDateTime dateTime = LocalDateTime.of(2021, 3, 20, 6, 26, 33, 23145);
System.out.println(dateTime);

// 得到当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now);

// 获取时间信息
System.out.println(now.getYear()); // 年
System.out.println(now.getMonth()); // 月
System.out.println(now.getDayOfMonth());// 日
System.out.println(now.getDayOfWeek()); // 星期
System.out.println(now.getHour());// 小时
System.out.println(now.getMinute());// 分钟
System.out.println(now.getSecond());// 秒
System.out.println(now.getNano());// 纳秒

日期时间修改

LocalDateLocalTimeLocalDateTime提供了一系列的with方法,用于根据当前日期修改得到新的日期。注意,他们默认都是不可修改的对象,故他们是线程安全的,修改操作会返回一个新的日期时间对象:

LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime newDateTime = now.withYear(1998);

// withHour
// withDayOfMonth
// ....

也提供了一系列的plus以及minus方法,用于在当前日期的基础上,加上或者减去指定的时间:

now.plusDays(2);
now.plusYears(10);

now.minusYears(10);
now.minusHours(5);

更复杂的情况可以借助时间矫正器 TemporalAdjuster,完成类似的以下的日期时间修改功能:

  1. 获取下个月的第一天

  2. 获取上周三的日期

LocalDateTime now = LocalDateTime.now();
// 将当前日期调整到下个月的一号
TemporalAdjuster adjuster = temporal -> {
    return temporal
            // 下个月
            .plus(1, ChronoUnit.MONTHS)
            // 一号
            .with(ChronoField.DAY_OF_MONTH, 1);
};
LocalDateTime with = now.with(adjuster);
System.out.println(with);

TemporalAdjuster 提供了大量的默认实现,用于简化常用的日期修改操作。

日期时间比较

// 比较两个日期/时间的前后
now.isAfter(date);
now.isBefore(date);
now.isEqueal(date);

日期格式化

在JDK8中,可以通过java.time.format.DateTimeFormatter类进行日期时间格式化于解析:

// Date -> String 
// 使用提供的pattern
String dateStr = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 使用自定义的pattern
String dateStr2 = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

// String -> Date
LocalDateTime dateTime = LocalDateTime.parse("1997-05-07 21:12:55", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

Instant

表示时间戳,内部保存了从1770年1月1日0时0分以来的秒数以及纳秒数。

Instant now = Instant.now();

// 同样支持 plus一级minus 修改时间

now.getEpochSecond() // 秒
now.toEpochMilli() // 毫秒
now.getNano(); // 纳秒

// DateTime获取毫秒数(先转换为Instant)
Instant localDateTime2Instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();

Duration 和 Period

DK8提供的用于计算时间日期时间差的工具类。其中

  1. Duration,可以计算 LocalTimeLocalDateTimeInstant的时间差

  2. Period,可以计算LocalDate之间的时间差

LocalTime now = LocalTime.now();
LocalTime time = LocalTime.of(22, 34, 59);

// 通过Duration计算时间差(Diration内部记录了相差的纳秒数)
Duration durationTime = Duration.between(now, time);
durationTime.toDays(); // 差的天数
durationTime.toHours(); // 差的小时
durationTime.toMillis();
// ...


// 通过Period计算日期差(Period记录了相差的天数)
LocalDate nowDate = LocalDate.now();
LocalDate date = LocalDate.of(1997, 2, 2);
Period period = Period.between(date, nowDate);
period.getYears(); // 相差年份
period.getMonths(); // 相差月份
period.getDays(); // 相差天数

时区日期时间类

Java8中,LocalDateLocalTimeLocalDateTime是不带时区的,带时区的提供了三个对应的类,分别为:

  1. ZonedDate

  2. ZonedTime

  3. ZonedDateTime

其中每个时区都对应着一个ID,存储在ZonedId类中。

// 获取支持的时区
ZoneId.getAvailableZoneIds().forEach(System.out::println);
 
// 获取当前系统的时区的时间
ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC());

// 获取某个特定时区的时间
ZonedDateTime americaTime = ZonedDateTime.now(ZoneId.of("America/Argentina/Buenos_Aires"));

框架支持

要想在Mybatis、Jackson中使用JSR310,需要添加JSR310的实现。

<dependency>
   <groupId>org.mybatis</groupId>
   <artifactId>mybatis-typehandlers-jsr310</artifactId>
   <version>1.0.1</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.2</version>
</dependency>
  1. LocalDate映射数据库中的date类型

  2. LocalTime来映射数据库中的time类型

  3. LocalDateTime字段来映射数据库中的datetime类型

最后更新于