
学习必须往深处挖,挖的越深,基础越扎实!
Java 现在发布的版本很快,每年两个,但是真正会被大规模使用的是 3 年一个的 LTS 版本。
每 3 年发布一个 LTS(Long-Term Support),长期维护版本。意味着只有Java 8 ,Java 11, Java 17,Java 21 才可能被大规模使用。
每年发布两个正式版本,分别是 3 月份和 9 月份。
在 Java 版本中,一个特性的发布都会经历孵化阶段、预览阶段和正式版本。其中孵化和预览可能会跨越多个 Java 版本。所以在介绍 Java 新特性时采用如下这种策略:
- 每个版本的新特性,都会做一个简单的概述。
- 单独出文介绍跟编码相关的新特性,一些如 JVM、性能优化的新特性不单独出文介绍。
- 孵化阶段的新特性不出文介绍。
- 首次引入为预览特性、新特性增强、首次引入的正式特性,单独出文做详细介绍。
- 影响比较大的新特性如果在现阶段没有转正的新特性不单独出文介绍,单独出文的重大特性一般都在 Java 21 版本之前已转为正式特性,例如:
- 虚拟线程,Java 19 引入的,在 Java 21 转正,所以在 Java 19 单独出文做详细介绍
- 作用域值,Java 20 引入的,但是在 Java 21 还处于预览阶段,所以不做介绍,等将来转正后会详细介绍
Java 8 新特性一
JEP 101:类型推断优化
简单理解泛型
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。通俗点将就是“类型的变量”。这种类型变量可以用在类、接口和方法的创建中。
理解Java泛型最简单的方法是把它看成一种便捷语法,能节省你某些Java类型转换(casting)上的操作:
List
上面的代码自身已表达的很清楚: box是一个装有Apple对象的List。get方法返回一个Apple对象实例,这个过程不需要进行类型转换。没有泛型,上面的代码需要写成这样:
Apple apple = (Apple)box.get(0);
泛型的尴尬
泛型的最大优点是提供了程序的类型安全同时可以向后兼容,但也有尴尬的地方,就是每次定义时都要写明泛型的类型,这样显示指定不仅感觉有些冗长,最主要是很多程序员不熟悉泛型,因此很多时候不能够给出正确的类型参数,现在通过编译器自动推断泛型的参数类型,能够减少这样的情况,并提高代码可读性。
java7的泛型类型推断改进
在以前的版本中使用泛型类型,需要在声明并赋值的时候,两侧都加上泛型类型。例如:
1 | Map<String, String> myMap = new HashMap<String, String>(); |
你可能觉得:老子在声明变量的的时候已经指明了参数类型,为毛还要在初始化对象时再指定? 幸好,在Java SE 7中,这种方式得以改进,现在你可以使用如下语句进行声明并赋值:
1 | Map<String, String> myMap = new HashMap<>(); //注意后面的"<>" |
在这条语句中,编译器会根据变量声明时的泛型类型自动推断出实例化HashMap时的泛型类型。再次提醒一定要注意new HashMap后面的“<>”,只有加上这个“<>”才表示是自动类型推断,否则就是非泛型类型的HashMap,并且在使用编译器编译源代码时会给出一个警告提示。
但是: Java SE 7在创建泛型实例时的类型推断是有限制的: 只有构造器的参数化类型在上下文中被显著的声明了,才可以使用类型推断,否则不行。例如: 下面的例子在java 7无法正确编译(但现在在java8里面可以编译,因为根据方法参数来自动推断泛型的类型):
1 | List<String> list = new ArrayList<>(); |
Java8的泛型类型推断改进
java8里面泛型的目标类型推断主要2个:
1.支持通过方法上下文推断泛型目标类型
2.支持在方法调用链路当中,泛型类型推断传递到最后一个方法
让我们看看官网的例子
1 | class List<E> { |
根据JEP101的特性,我们在调用上面方法的时候可以这样写
1 | //通过方法赋值的目标参数来自动推断泛型的类型 |
以上是JEP101的特性内容了,Java作为静态语言的代表者,可以说类型系统相当丰富。导致类型间互相转换的问题困扰着每个java程序员,通过编译器自动推断类型的东西可以稍微缓解一下类型转换太复杂的问题。 虽然说是小进步,但对于我们天天写代码的程序员,肯定能带来巨大的作用,至少心情更愉悦了
JEP 104:类型注解
从java5开始加入这一特性,发展到现在已然是遍地开花,在很多框架中得到了广泛的使用,用来简化程序中的配置。那充满争议的类型注解究竟是什么? 复杂还是便捷?
在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性;
java 8里面,注解可以应用在任何地方,比如:
创建类实例
1 | new MyObject(); |
类型映射
1 | myString = ( String) str; |
implements 语句中
1 | class UnmodifiableList<T> implements List< T> { … } |
throw exception声明
1 | void monitorTemperature() throws TemperatureException { … } |
需要注意的是,类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。
类型注解的作用
先看看下面代码
1 | Collections.emptyList().add("One"); |
上面的代码编译是通过的,但运行是会分别报UnsupportedOperationException; NumberFormatException;NullPointerException异常,这些都是runtime error;
类型注解被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量。这就是类型注解的作用了。
check framework是第三方工具,配合Java的类型注解效果就是1+1>2。它可以嵌入到javac编译器里面,可以配合ant和maven使用, 地址是http://types.cs.washington.edu/checker-framework/。 check framework可以找到类型注解出现的地方并检查,举个简单的例子:
1 | import checkers.nullness.quals.*; |
使用javac编译上面的类
1 | javac -processor checkers.nullness.NullnessChecker GetStarted.java |
编译是通过,但如果修改成
1 | Object ref = null; |
1 | GetStarted.java:5: incompatible types. |
类型注解向下兼容的解决方案
如果你不想使用类型注解检测出来错误,则不需要processor,直接javac GetStarted.java是可以编译通过的,这是在java 8 with Type Annotation Support版本里面可以,但java 5,6,7版本都不行,因为javac编译器不知道@NonNull是什么东西,但check framework 有个向下兼容的解决方案,就是将类型注解nonnull用/**/注释起来,比如上面例子修改为
1 | import checkers.nullness.quals.*; |
这样javac编译器就会忽略掉注释块,但用check framework里面的javac编译器同样能够检测出nonnull错误。 通过类型注解+check framework我们可以看到,现在runtime error可以在编译时候就能找到。
关于JSR 308
JSR 308想要解决在Java 1.5注解中出现的两个问题:
- 在句法上对注解的限制: 只能把注解写在声明的地方
- 类型系统在语义上的限制: 类型系统还做不到预防所有的bug
JSR 308 通过如下方法解决上述两个问题:
- 对Java语言的句法进行扩充,允许注解出现在更多的位置上。包括: 方法接收器(method receivers,译注: 例public int size() @Readonly { … }),泛型参数,数组,类型转换,类型测试,对象创建,类型参数绑定,类继承和throws子句。其实就是类型注解,现在是java 8的一个特性
- 通过引入可插拔的类型系统(pluggable type systems)能够创建功能更强大的注解处理器。类型检查器对带有类型限定注解的源码进行分析,一旦发现不匹配等错误之处就会产生警告信息。其实就是check framework
对JSR308,有人反对,觉得更复杂更静态了,比如
1 | new ArrayList< String>()> List< String> strings = |
换成动态语言为
1 | var strings = ["one", "two"]; |
有人赞成,说到底,代码才是“最根本”的文档。代码中包含的注解清楚表明了代码编写者的意图。当没有及时更新或者有遗漏的时候,恰恰是注解中包含的意图信息,最容易在其他文档中被丢失。而且将运行时的错误转到编译阶段,不但可以加速开发进程,还可以节省测试时检查bug的时间。
总结
并不是人人都喜欢这个特性,特别是动态语言比较流行的今天,所幸,java 8并不强求大家使用这个特性,反对的人可以不使用这一特性,而对代码质量有些要求比较高的人或公司可以采用JSR 308,毕竟代码才是“最基本”的文档,这句话我是赞同的。虽然代码会增多,但可以使你的代码更具有表达意义。对这个特性有何看法,大家各抒己见。。。。
JEP 107:Stream API
Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程 序员的生产力,让程序员写出高效率、干净、简洁的代码。
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。
使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。 也可以使用 Stream API来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
为什么要使用Stream API?
实际开发中,项目中多数数据源都来自于Mysql,Oracle等。但现在数据源可以更多了,有MongDB,Radis等,而这些NoSQL的数据就需要Java层面去处理。
Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,而 Stream 是有关计算的。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。
什么是 Stream?
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。 “集合讲的是数据,Stream讲的是计算!”
注意:
- Stream关注的是对数据的运算,与CPU打交道,集合关注的是数据的存储,与内存打交道。
- Stream 自己不会存储元素。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行
Stream 操作的三个步骤
创建 Stream
一个数据源(如:集合、数组),获取一个流。
中间操作
一个中间操作链,对数据源的数据进行处理。
终止操作(终端操作)
一旦执行终止操作,就执行中间操作链,并产生结果,之后,不会再被使用。
注意:
- 一个中间操作链,对数据源的数据进行处理
- 一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用
创建Stream实例
- 创建 Stream方式一:通过集合
Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
方法 | 描述 |
---|---|
default Stream |
返回一个顺序流 |
default Stream |
返回一个并行流 |
代码演示:
1 | //创建 Stream方式一:通过集合 |
- 创建 Stream方式二:通过数组
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
方法 | 描述 |
---|---|
static |
返回一个流 |
public static IntStream stream(int[] array) | 返回一个 int 型流 |
public static LongStream stream(long[] array) | 返回一个 long 型流 |
public static DoubleStream stream(double[] array) | 返回一个 double 型流 |
代码演示:
1 | //创建 Stream方式二:通过数组 |
- 创建 Stream方式三:通过Stream的of()
可以调用Stream类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量的参数。
方法 | 描述 |
---|---|
spublic static |
返回一个流 |
代码演示:
1 | //创建 Stream方式三:通过Stream的of() |
- 创建 Stream方式四:创建无限流
可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流。
方法 | 描述 |
---|---|
public static |
迭代 |
public static |
生成 |
代码演示:
1 | //创建 Stream方式四:创建无限流 |
Stream 的中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止 操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全
部处理,称为“惰性求值”。
- 筛选与切片
方法 | 描述 |
---|---|
filter(Predicate p) | 接收 Lambda, 从流中排除某些元素 |
distinct() | 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素 |
limit(long maxSize) | 截断流,使其元素不超过给定数量 |
skip(long n) | 跳过元素,返回一个扔掉了前 n个元素的流。若流中元素不足 n个,则返回一个空流。与 limit(n) 互补 |
代码演示:
1 | //1-筛选与切片 |
- 映射
方法 | 描述 |
---|---|
map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。 |
mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。 |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。 |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。 |
代码演示:
1 | //映射 |
- 排序
方法 | 描述 |
---|---|
sorted() | 产生一个新流,其中按自然顺序排序。 |
sorted(Comparator com) | 产生一个新流,其中按比较器顺序排序。 |
代码演示:
1 | //3-排序 |
Stream 的终止操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例 如:List、Integer,甚至是 void 。
流进行了终止操作后,不能再次使用。
- 匹配与查找
方法 | 描述 |
---|---|
allMatch(Predicate p) | 检查是否匹配所有元素。 |
anyMatch(Predicate p) | 检查是否至少匹配一个元素。 |
noneMatch(Predicate p) | 检查是否没有匹配所有元素 |
findFirst() | 返回第一个元素 |
findAny() | 返回当前流中的任意元素 |
count() | 返回流中元素总数 |
max(Comparator c) | 返回流中最大值 |
min(Comparator c) | 返回流中最小值 |
forEach(Consumer c) | 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了) |
代码演示:
1 | //1-匹配与查找 |
- 归约
方法 | 描述 |
---|---|
reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T |
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 Optional |
代码演示:
1 | //2-归约 |
- 收集
方法 | 描述 |
---|---|
collect(Collector c) | 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 |
Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。
另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法参考文档:https://www.matools.com/api/java8
代码演示:
1 | //3-收集 |
- 本文标题:Java 8 新特性一
- 本文作者:形而上
- 创建时间:2023-12-31 20:13:00
- 本文链接:https://deepter.gitee.io/2023_12_31_java/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!