【Java新特性学习 四】JDK8: 库函数新特性之Optional,Streams,Date/Time API(JSR 310),Base64,并行数组

简介: 【Java新特性学习 四】JDK8: 库函数新特性之Optional,Streams,Date/Time API(JSR 310),Base64,并行数组

本篇Blog继续学习和实践Java8中的新特性,主要分为两大部分:语言新特性和库函数新特性,重点落在工作中经常会用到的几个重大特性:

  • 语言新特性:Lambda表达式,方法引用,接口的默认方法和静态方法,重复注解
  • 库函数新特性:Optional,Streams,Date/Time API(JSR 310),Base64,并行数组

接下来按照如下几个结构分别介绍和学习以上知识点:基本概念,解决问题,语法范式,实践操作。本篇详细学习下新增的库函数

Optional

Java应用中最常见的bug就是NPE。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。

  • Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
  • Optional 是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测
  • Optional 类的引入很好的解决空指针异常

常用的方法如下:

我们常用下面几种方法进行判空:

最常用的用法就是,当我们想要给一个对象的属性

@Data
public class CompanyInfo {
    private LegalPersonInfo legalPerson;
}
class LegalPersonInfo {
  private String legalPersonName;
}

当我们想从类中获取某个属性的时候,很怕报NPE,但是如果一级一级的判空很麻烦,有了Optional之后就可以一句话判定:

Optional.ofNullable(company).ofNullable(company.getLegalPerson()).map(LegalPersonInfo::getLegalPersonName).orElse("tml")

最后获取到legalPersonName后,如果该值为null,则给一个默认值

Streams

新增的Stream API(java.util.stream)将生成环境的函数式编程引入了Java库中。这是目前为止最大的一次对Java库的完善,以便开发者能够写出更加有效、更加简洁和紧凑的代码:

stream极大简化了对于集合类的操作,Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象,Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象

Stream定义

Stream(流)是一个来自数据源的元素队列并支持聚合操作:

Stream(流)是一个来自数据源的元素队列并支持聚合操作

  • 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
  • 数据源流的来源可以是集合,数组,I/O channel, 产生器generator 等。
  • 聚合操作 类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

和以前的Collection操作不同, Stream操作还有两个基础的特征:

  • Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
  • 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现

在 Java 8 中, 集合接口有两个方法来生成流:

  • stream() − 为集合创建串行流。
  • parallelStream() − 为集合创建并行流

接下来看下常见的队列操作

常见流操作

在一个管道操作中,数据先是被处理,然后被转换和返回或者用于数据统计

数据处理

forEach,Stream 提供了新的方法 ‘forEach’ 来迭代流中的每个数据。以下代码片段使用 forEach 输出了10个随机数:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

其实这里的forEach也是函数式接口,并且可以使用lambda表达式:

/**
     * Performs an action for each element of this stream.
     *
     * <p>This is a <a href="package-summary.html#StreamOps">terminal
     * operation</a>.
     *
     * <p>The behavior of this operation is explicitly nondeterministic.
     * For parallel stream pipelines, this operation does <em>not</em>
     * guarantee to respect the encounter order of the stream, as doing so
     * would sacrifice the benefit of parallelism.  For any given element, the
     * action may be performed at whatever time and in whatever thread the
     * library chooses.  If the action accesses shared state, it is
     * responsible for providing the required synchronization.
     *
     * @param action a <a href="package-summary.html#NonInterference">
     *               non-interfering</a> action to perform on the elements
     */
    void forEach(Consumer<? super T> action);

map,map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数:

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
List<Integer> squaresList = numbers.stream().map( i -> i*i).distinct().collect(Collectors.toList());

其实这里的map也是函数式接口,并且可以使用lambda表达式:

/**
     * Returns a stream consisting of the results of applying the given
     * function to the elements of this stream.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * @param <R> The element type of the new stream
     * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
     *               <a href="package-summary.html#Statelessness">stateless</a>
     *               function to apply to each element
     * @return the new stream
     */
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

filter,filter 方法用于通过设置的条件过滤出元素。以下代码片段使用 filter 方法过滤出空字符串:

List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.stream().filter(string -> string.isEmpty()).count();

其实这里的filter也是函数式接口,并且可以使用lambda表达式:

/**
     * Returns a stream consisting of the elements of this stream that match
     * the given predicate.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * @param predicate a <a href="package-summary.html#NonInterference">non-interfering</a>,
     *                  <a href="package-summary.html#Statelessness">stateless</a>
     *                  predicate to apply to each element to determine if it
     *                  should be included
     * @return the new stream
     */
    Stream<T> filter(Predicate<? super T> predicate);

limit,limit 方法用于获取指定数量的流。 以下代码片段使用 limit 方法打印出 10 条数据:

Random random = new Random();
random.ints().limit(10).forEach(System.out::println);

其实这里的limit也是函数式接口,并且可以使用lambda表达式:

*
     * @param maxSize the number of elements the stream should be limited to
     * @return the new stream
     * @throws IllegalArgumentException if {@code maxSize} is negative
     */
    Stream<T> limit(long maxSize);

sorted,sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法对输出的 10 个随机数进行排序:

Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);

其实这里的sorted也是函数式接口,并且可以使用lambda表达式:

*
     * <p>This is a <a href="package-summary.html#StreamOps">stateful
     * intermediate operation</a>.
     *
     * @return the new stream
     */
    Stream<T> sorted();

结果转换

Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串

List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("筛选列表: " + filtered);
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("合并字符串: " + mergedString);

这里的collect也是一个函数式接口:

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

数据统计

另外,一些产生统计结果的收集器也非常有用。它们主要用于int、double、long等基本类型上,它们可以用来产生类似如下的统计结果。

List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());

返回结果如下:

串行和并行

parallelStream 是流并行处理程序的代替方法。以下实例我们使用 parallelStream 来输出空字符串的数量:

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();

串行则更常用:

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());

综合示例

一个综合例子如下:

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
        List list=numbers.stream().filter(x->x>2).distinct().limit(2).map(i->i*i).sorted().collect(Collectors.toList());
        list.forEach(System.out::println);
    }

当然也可以按照统计逻辑输出:

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
        IntSummaryStatistics stats =numbers.stream().filter(x->x>2).distinct().limit(2).map(i->i*i).sorted().mapToInt((x)->x).summaryStatistics();
        System.out.println(stats.getMax());
    }

Date/Time API(JSR 310)

Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:

  • 非线程安全− java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
  • 设计很差 − Java的日期/时间类的定义并不一致,在java.utiljava.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
  • 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendarjava.util.TimeZone类,但他们同样存在上述所有的问题。

Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:

  • Local(本地) − 简化了日期时间的处理,没有时区的问题。
  • Zoned(时区) − 通过制定的时区处理日期时间。

新的java.time包涵盖了所有处理**日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)**的操作。

本地时间

以下是一些本地时间和日期的例子:

public static void main(String[] args) {
        // 获取当前的日期时间
        LocalDateTime currentTime = LocalDateTime.now();
        System.out.println("当前时间: " + currentTime);
        LocalDate date1 = currentTime.toLocalDate();
        System.out.println("date1: " + date1);
        Month month = currentTime.getMonth();
        int day = currentTime.getDayOfMonth();
        int seconds = currentTime.getSecond();
        System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
        LocalDateTime date2 = currentTime.withDayOfMonth(26).withYear(2021);
        System.out.println("date2: " + date2);
        LocalDate date3 = LocalDate.of(2021, Month.DECEMBER, 26);
        System.out.println("date3: " + date3);
        LocalTime date4 = LocalTime.of(22, 15);
        System.out.println("date4: " + date4);
        // 解析字符串
        LocalTime date5 = LocalTime.parse("20:15:30");
        System.out.println("date5: " + date5);
    }

打印结果如下:

时区时间

当需要时区时间的时候,我们用ZonedDateTime来进行定义

public static void main(String[] args) {
        //Clock类使用时区来返回当前的纳秒时间和日期。Clock可以替代System.currentTimeMillis()和TimeZone.getDefault()
        final Clock clock = Clock.systemUTC();
        // 获取当前时间日期
        System.out.println("date1: " +ZonedDateTime.now());
        System.out.println("date2: " +ZonedDateTime.now( clock ));
        System.out.println("data3: " +  ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) ));
    }

返回结果如下:

时间范围

Duration类,它持有的时间精确到秒和纳秒。这使得我们可以很容易得计算两个日期之间的不同

public static void main(String[] args) {
        final LocalDateTime from = LocalDateTime.of( 2021, Month.APRIL, 16, 0, 0, 0 );
        final LocalDateTime to = LocalDateTime.of( 2077, Month.APRIL, 16, 23, 59, 59 );
        final Duration duration = Duration.between( from, to );
        System.out.println( "Duration in days: " + duration.toDays() );
        System.out.println( "Duration in hours: " + duration.toHours() );
    }

结果如下:

也就是我们还有20454天左右进入赛博朋克时代。

Base64

在Java 8中,Base64编码已经成为Java类库的标准。Java 8 内置了 Base64 编码的编码器和解码器。Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:

  • 基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/
  • URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。
  • MIME:输出隐射到MIME友好格式。输出每行不超过76字符,并且使用'\r'并跟随'\n'作为分割。编码输出最后没有行分割

内嵌类如下:

内嵌方法如下:

我们通过一个例子来测试一下:

public static void main(String[] args) {
        try {
            // 使用基本编码
            String base64encodedString = Base64.getEncoder().encodeToString("tml is study java8".getBytes("utf-8"));
            System.out.println("Base64 编码字符串 (基本) :" + base64encodedString);
            // 解码
            byte[] base64decodedBytes = Base64.getDecoder().decode(base64encodedString);
            System.out.println("(基本)原始字符串: " + new String(base64decodedBytes, "utf-8"));
            // 使用(URL)编码
            base64encodedString = Base64.getUrlEncoder().encodeToString("tml is study java8 使用(URL)编码".getBytes("utf-8"));
            System.out.println("Base64 编码字符串 (URL) :" + base64encodedString);
            // 解码
            byte[] base64decodedBytes1 = Base64.getUrlDecoder().decode(base64encodedString);
            System.out.println(" (URL)原始字符串: " + new String(base64decodedBytes1, "utf-8"));
            // 使用 (MIME)编码
            String mimeEncodedString = Base64.getMimeEncoder().encodeToString("tml is study java8 使用(MIME)编码".getBytes("utf-8"));
            System.out.println("Base64 编码字符串 (MIME) :" + mimeEncodedString);
            // 解码
            byte[] base64decodedBytes2 = Base64.getMimeDecoder().decode(mimeEncodedString);
            System.out.println(" (URL)原始字符串: " + new String(base64decodedBytes2, "utf-8"));
        }catch(UnsupportedEncodingException e){
            System.out.println("Error :" + e.getMessage());
        }
    }

打印结果如下:

并行数组

Java8版本新增了很多新的方法,用于支持并行数组处理。最重要的方法是parallelSort(),可以显著加快多核机器上的数组排序。

public static void main(String[] args) {
            long[] arrayOfLong = new long [ 20000 ];
            Arrays.parallelSetAll( arrayOfLong, index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
            Arrays.stream( arrayOfLong ).limit( 10 ).forEach(i -> System.out.print( i + " " ) );
            System.out.println();
            Arrays.parallelSort( arrayOfLong );
            Arrays.stream( arrayOfLong ).limit( 10 ).forEach(i -> System.out.print( i + " " ) );
            System.out.println();
    }

上述这些代码使用parallelSetAll()方法生成20000个随机数,然后使用parallelSort()方法进行排序。这个程序会输出乱序数组和排序数组的前10个元素,返回结果如下:

总结一下

之前一直不知道Optional是什么东西,stream又是什么东西,LocalTime又是什么,工作中都是一味的仿写,并不太懂其中真正的含义,想用的时候还要找相似的代码现看,太笨拙了,看来还是落后太多了,这篇Blog算是一个补齐Gap的Blog,大致懂了这些Java8新提供的库函数用途,以及其实际实现时如何依托Lambda、函数式接口以及静态和默认接口方法的。有一种豁然开朗的赶脚。

目录
打赏
0
0
0
0
33
分享
相关文章
CompreFace:Star6.1k,Github上火爆的轻量化且强大的人脸识别库,api,sdk都支持
CompreFace 是一个在 GitHub 上拥有 6.1k Star 的轻量级人脸识别库,支持 API 和 SDK。它由 Exadel 公司开发,基于深度学习技术,提供高效、灵活的人脸识别解决方案。CompreFace 支持多种模型(如 VGG-Face、OpenFace 和 Facenet),具备多硬件支持、丰富的功能服务(如人脸检测、年龄性别识别等)和便捷的部署方式。适用于安防监控、商业领域和医疗美容等多个场景。
JDK21有没有什么稳定、简单又强势的特性?
这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。
JDK21有没有什么稳定、简单又强势的特性?
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)
array.map()可以用来数据转换、创建派生数组、应用函数、链式调用、异步数据流处理、复杂API请求梳理、提供DOM操作、用来搜索和过滤等,比for好用太多了,主要是写法简单,并且非常直观,并且能提升代码的可读性,也就提升了Long Term代码的可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Java线程池ExecutorService学习和使用
通过学习和使用Java中的 `ExecutorService`,可以显著提升并发编程的效率和代码的可维护性。合理配置线程池参数,结合实际应用场景,可以实现高效、可靠的并发处理。希望本文提供的示例和思路能够帮助开发者深入理解并应用 `ExecutorService`,实现更高效的并发程序。
47 10
Java 访问数据库的奇妙之旅
本文介绍了Java访问数据库的几种常见方式
57 12
【潜意识Java】深度分析黑马项目《苍穹外卖》在Java学习中的重要性
《苍穹外卖》项目对Java学习至关重要。它涵盖了用户管理、商品查询、订单处理等模块,涉及Spring Boot、MyBatis、Redis等技术栈。
262 4
【潜意识Java】Java基础教程:从零开始的学习之旅
本文介绍了 Java 编程语言的基础知识,涵盖从简介、程序结构到面向对象编程的核心概念。首先,Java 是一种高级、跨平台的面向对象语言,支持“一次编写,到处运行”。接着,文章详细讲解了 Java 程序的基本结构,包括包声明、导入语句、类声明和 main 方法。随后,深入探讨了基础语法,如数据类型、变量、控制结构、方法和数组。此外,还介绍了面向对象编程的关键概念,例如类与对象、继承和多态。最后,针对常见的编程错误提供了调试技巧,并总结了学习 Java 的重要性和方法。适合初学者逐步掌握 Java 编程。
63 1
|
3月前
|
jdk8新特性-详情查看文档
jdk8新特性-详情查看文档
79 7
随机昵称网名[百万昵称库]免费API接口教程
该API接口用于随机生成网名,适用于机器人昵称、虚拟用户名等场景。支持POST和GET请求,需提供用户ID和KEY。返回状态码及信息提示,示例如下:{&quot;code&quot;:200,&quot;msg&quot;:&quot;豌豆公主&quot;}。详情见官方文档:https://www.apihz.cn/api/zicisjwm.html

热门文章

最新文章