本篇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.util
和java.sql
的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。 - 时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了
java.util.Calendar
和java.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、函数式接口以及静态和默认接口方法的。有一种豁然开朗的赶脚。