JDK

JAVA

# 一、JDK

官网 Java 下载:https://www.oracle.com/java/technologies/downloads (opens new window)

# 二、JDK版本

LTS Long Term Support 长期支持版本

官网 Java 不同版本功能特性介绍 (opens new window)

在最近的SpringOne 大会上,Spring官方宣布了Spring Framework 6Spring Boot3计划将Java17来进行基线版本进行支持。

  • 一方面来说Java17是最新的一个LTS版本将支持到2029年。
  • 另一方面来说Java从9到17给我们带来了非常多新的特性,可以为我们工程带来非常明显的增益。

# 三、JDK8 新特性

JDK8 新特性清单

  • Lambda表达式
  • 函数式接口
  • 方法引用和构造器调用
  • Stream API
  • 接口中的默认方法和静态方法
  • 新时间日期API

# 函数式编程

函数式编程非常关键的几个特性如下:

  1. 闭包与高阶函数

    函数编程支持函数作为第一类对象,有时称为 闭包或者 仿函数(functor)对象。实质上,闭包是起函数的作用并可以像对象一样操作的对象。

    与此类似,FP 语言支持 高阶函数。高阶函数可以用另一个函数(间接地,用一个表达式)作为其输入参数,在某些情况下,它甚至返回一个函数作为其输出参数。这两种结构结合在一起使得可以用优雅的方式进行模块化编程,这是使用 FP 的最大好处。

  2. 惰性计算

    在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使您可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。

  3. 没有“副作用”

    所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局 变量的值),产生运算以外的其他结果。函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

综上所述,函数式编程可以简言之是: 使用不可变值和函数, 函数对一个值进行处理, 映射成另一个值。这个值在面向对象语言中可以理解为对象,另外这个值还可以作为函数的输入。

# Lambda表达式

lambda表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。

# 语法

完整的Lambda表达式由三部分组成:参数列表、箭头、声明语句;

(Type1 param1, Type2 param2, ..., TypeN paramN) ‐> { 
	statment1; 
	statment2; 
	//............. 
	return statmentM;
}
1
2
3
4
5
6
  1. 绝大多数情况,编译器都可以从上下文环境中推断出lambda表达式的参数类型,所以参数可以省略:

    (param1,param2, ..., paramN) ‐> { 
    	statment1; 
    	statment2; 
    	//............. 
    	return statmentM;
    }
    
    1
    2
    3
    4
    5
    6
  2. 当lambda表达式的参数个数只有一个,可以省略小括号:

    param1 ‐> { 
    	statment1; 
    	statment2; 
    	//............. 
    	return statmentM;
    }
    
    1
    2
    3
    4
    5
    6
  3. 当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号:

    param1 ‐> statment
    
    1

# 函数式接口

函数接口是只有一个抽象方法的接口, 用作Lambda表达式返回类型

接口包路径为java.util.function,然后接口类上面都有@FunctionalInterface这个注解。

这些函数接口在使用Lambda表达式时做为返回类型,JDK定义了很多现在的函数接口。而且这些接口在JDK8集合类使用流操作时大量被使用。

实际自己也可以定义接口去做为表达式的返回,只是大多数情况下JDK定义的直接拿来就可以用了。

# 类型检查、类型推断

Java编译器根据 Lambda 表达式上下文信息就能推断出参数的正确类型。 程序依然要经过类型检查来保证运行的安全性, 但不用再显式声明类型罢了。 这就是 所谓的类型推断

Lambda表达式中的类型推断, 实际上是 Java 7 就引入的目标类型推断的扩展。

有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,你必须做出自己的选择。

# 局部变量限制

Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像

匿名类一样。 它们被称作捕获Lambda。 Lambda可以没有限制地捕获(也就是在其主体

中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。

为什么局部变量有这些限制?

  1. 实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变 量。因此, Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
  2. 这一限制不鼓励你使用改变外部变量的典型命令式编程模式。

# 方法引用

方法引用的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。

但是,显式地指明方法的名称,你的代码的可读性会更好。所以方法引用只是在内容中只有一个表达式的简写

当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面,即ClassName::methodName

例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。

请记住,不需要括号,因为你没有实际调用这个方法。

方法引用就是Lambda表达式(Apple a) -­> a.getWeight()的快捷写法。

# 构造器调用

这里有种情况需要特殊说明,就是类的构造函数情况。这个时候是通过ClassName::new这种形式创建Class构造函数对应的引用,例如:

# 接口中的默认方法和静态方法

# 介绍

为了以兼容方式改进API,Java 8中在定义接口中加入了默认方法。主要是为了支持库设计师,让他们能够写出更容易改进的接口。

具体写法是在接口中加default关键字修饰。

# 使用说明

默认方法由于是为了避免兼容方式改进API才引入,所以一般正常开发中不会使用,除非你也想改进API,而不影响老的接口实现。

当然在JDK8有大量的地方用到了默认方法,所以对这种写法有一定的了解还是有帮助的。

采用默认方法之后,你可以为这个方法提供一个默认的实现,这样实体类就无需在自己的实现中显式地提供一个空方法,而是默认就有了实现。

# 注意事项

由于类可以实现多个接口,也可以继承类,当接口或类中有相同函数签名的方法时,这个时候到底使用哪个类或接口的实现呢?

这里有三个规则可以进行判断:

  1. 类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。
  2. 如果无法依据第一条进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B继承了A,那么B就比A更加具体。
  3. 最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现,不然编译都会报错。

# 方法参数反射

JDK8 新增了Method.getParameters方法,可以获取参数信息,包括参数名称。

不过为了避免.class文件因为保留参数名而导致.class文件过大或者占用更多的内存,另外也避免有些参数(secret/password)泄露安全信息,JVM默认编译出的class文件是不会保留参数名这个信息的。

这一选项需由编译开关 javac -parameters 打开,默认是关闭的。

在Eclipse(或者基于Eclipse的IDE)中可以如下图勾选保存:

# 新时间日期API

1.8之前JDK自带的日期处理类非常不方便,我们处理的时候经常是使用的第三方工具包,比如commons-lang包等。

不过1.8出现之后这个改观了很多,比如日期时间的创建、比较、调整、格式化、时间间隔等。

这些类都在java.time包下。比原来实用了很多。

# 日期-时间改进说明

  1. 之前使用的java.util.Date月份从0开始,我们一般会+1使用,很不方便,java.time.LocalDate月份和星期都改成了enum
  2. java.util.Date和SimpleDateFormat都不是线程安全的,而LocalDate和LocalTime和最基本的String一样,是不变类型,不但线程安全,而且不能修改。
  3. java.util.Date是一个“万能接口”,它包含日期、时间,还有毫秒数,更加明确需求取舍。
  4. 新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用java.util.Date配合Calendar要写好多代码,而且一般的开发人员还不一定能写对。

# LocalDateTime

LocalDate为日期处理类、LocalTime为时间处理类、LocalDateTime为日期时间处理类,方法都类似。

具体可以看API文档或源码,选取几个代表性的方法做下介绍。

  1. now相关的方法可以获取当前日期或时间,

    LocalDate localDate =LocalDate.now();
    LocalTimelocaTime = LocalTime.now();
    LocalDateTime localDateTime = LocalDateTime.now();
    
    1
    2
    3
  2. of方法可以创建对应的日期或时间,

    LocalDate localDate = LocalDate.of(2016, 10, 26);
    LocalTime localTime = LocalTime.of(02, 22, 56);
    LocalDateTime localDateTime = LocalDateTime.of(2016, 10, 26, 12, 10, 55);
    
    1
    2
    3
  3. parse方法可以解析日期或时间,

  4. get方法可以获取日期或时间信息,

  5. with方法可以设置日期或时间信息,

  6. plus或minus方法可以增减日期或时间信息;

LocalDate、LocalTime、LocalDateTime 类的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信 息。也不包含与时区相关的信息。

注: ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法

方法 描述
now() 静态方法,根据当前时间创建对象
of() 静态方法,根据指定日期/时间创建对象
plusDays, plusWeeks,plusMonths, plusYears 向当前LocalDate对象添加几天、几周、几个月、几年
minusDays, minusWeeks,minusMonths, minusYears 从当前 LocalDate 对象减去几天、几周、几个月、几年
plus, minus 添加或减少一个Duration或 Period
withDayOfMonth,withDayOfYear, withMonth,withYear 将月份天数、年份天数、月份、年份修改为指 定 的 值 并 返 回 新的LocalDate 对象
getDayOfMonth 获得月份天数(1-31)
getDayOfYear 获得年份天数(1-366)
getDayOfWeek 获得星期几(返回一个DayOfWeek枚举值)
getMonth 获得月份, 返回一个Month 枚举值
getMonthValue 获得月份(1-12)
getYear 获得年份
until 获得两个日期之间的Period 对象,或者指定ChronoUnits 的数字
isBefore, isAfter 比较两个 LocalDate
isLeapYear 判断是否是闰年

# Instant 时间戳

用于“时间戳”的运算。

它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的描述进行运算。

# Duration和Period

  1. Duration:用于计算两个“时间”间隔;
  2. Period:用于计算两个“日期”间隔;

# TemporalAdjusters

日期的操纵

  1. TemporalAdjuster时间校正器

    这个类在日期调整时非常有用,比如得到当月的第一天、最后一天,当年的第一天、最后一天,下一周或前一周的某天等。

  2. TemporalAdjusters

    该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。

# DateTimeFormatter

解析与格式化java.time.format.DateTimeFormatter类

该类提供了三种格式化方法:

  1. 预定义的标准格式
  2. 语言环境相关的格式
  3. 自定义的格式

以前日期格式化一般用SimpleDateFormat类,但是不怎么好用。

现在1.8引入了DateTimeFormatter类,默认定义了很多常量格式(ISO打头的)。

在使用的时候一般配合LocalDate/LocalTime/LocalDateTime使用,比如想把当前日期格式化成yyyy-MM-dd hh:mm:ss的形式:

LocalDateTime dt = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy‐MM‐dd hh:mm:ss");
System.out.println(dtf.format(dt));
1
2
3

# 时区的处理

Java8 中加入了对时区的支持,带时区的时间为分别为:

ZonedDateZonedTimeZonedDateTime

其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式

例如 :Asia/Shanghai

ZoneId:该类中包含了所有的时区信息getAvailableZoneIds() : 可以获取所有时区时区信息of(id) : 用指定的时区信息获取ZoneId 对象。

# Stream API

# Stream 流介绍

流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。

Java8中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API(java.util.stream.*)。

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。

使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

注意:

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

# 使用流

Stream的操作三个步骤:

  1. 创建Stream

一个数据源(如:集合、数组),获取一个流

  1. 中间操作

一个中间操作链,对数据源的数据进行处理

  1. 终止操作(终端操作)

一个终止操作,执行中间操作链,并产生结果

# 创建Stream

Java8 中的Collection接口被扩展,提供了两个获取流的方法:

  1. default Stream stream() : 返回一个顺序流
  2. default Stream parallelStream() : 返回一个并行流
# 由数组创建流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

  • static Stream stream(T[] array): 返回一个流

重载形式,能够处理对应基本类型的数组:

  1. public static IntStream stream(int[] array)
  2. public static LongStream stream(long[] array)
  3. public static DoubleStream stream(double[] array)
# 由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。

  • public static Stream of(T… values) : 返回一个流
# 由函数创建流:创建无限流

可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流。

  1. 迭代 public static Stream iterate(final T seed, final UnaryOperator f)
  2. 生成 public static Stream generate(Supplier s)

# Stream 的中间操作

# 筛选与切片
方 法 描 述
filter(Predicate p) 接收 Lambda , 从流中排除某些元素。
distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize) 截断流,使其元素不超过给定数量。
skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
# 映射
方 法 描 述
map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的DoubleStream。
mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。
flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流
# 排序
方 法 描 述
sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparator comp) 产生一个新流,其中按比较器顺序排序

# 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接口需要用户去做迭代,称为外部迭代。相反,StreamAPI 使用内部迭代——它帮你把迭代做了)
peek 循环遍历所有的元素,但是返回值是一个Stream,属于非终结方法
# 归约
方 法 描 述
reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。 返回 T
reduce(BinaryOperator b) reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返回 Optional

备注: map 和 reduce 的连接通常称为map-reduce 模式,因 Google 用它 来进行网络搜索而出名。

# 收集
方 法 描 述
collect(Collector c) 将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇的方法

Collector 接口中方法的实现决定了如何对流执行收集操作(如收 集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态 方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

方法 返回类型 作用 例子
toList List 把流中元素收集到List List emps= list.stream().collect(Collectors.toList());
toSet Set 把流中元素收集到Set Set emps= list.stream().collect(Collectors.toSet());
toCollection Collection 把流中元素收集到创建的集合 Collectionemps=list.stream().collect(Collectors.toCollection(ArrayList::new));
counting Long 计算流中元素的个数 long count = list.stream().collect(Collectors.counting());
summingInt Integer 对流中元素的整数属性求和 inttotal=list.stream().collect(Collectors.summingInt(Employee::getSalary));
averagingInt Double 计算流中元素Integer属性的平均值 doubleavg= list.stream().collect(Collectors.averagingInt(Employee::getSalary));
summarizingInt IntSummaryStatistics 收集流中Integer属性的统计值。 如:平均值 IntSummaryStatisticsiss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary));
joining String 连接流中每个字符串 String str= list.stream().map(Employee::getName).collect(Collectors.joining());
maxBy Optional 根据比较器选择最大值 Optionalmax= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)));
minBy Optional 根据比较器选择最小值 Optional min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary)));
reducing 归约产生的类型 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值 inttotal=list.stream().collect(Collectors.reducing(0, Employee::getSalar, Integer::sum));
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果转换函数 inthow= list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map<K, List> 根据某属性值对流分组,属性为K,结果为V Map<Emp.Status, List> map= list.stream().collect(Collectors.groupingBy(Employee::getStatus));
partitioningBy partitioningBy Map<Boolean, List> 根据true或false进行分区 Map<Boolean,List>vd= list.stream().collect(Collectors.partitioningBy(Employee::getManage));

上面是Stream API的一些常用操作,按场景结合lambda表达式调用对应方法即可。

至于Stream的生成方式,Streamof方法或者Collection接口实现类的stream方法都可以获得对应的流对象,再进一步根据需要做对应处理。

另外上述方法如果返回是Stream对象时是可以链式调用的,这个时候这个操作只是声明或者配方,不产生新的集合,这种类型的方法是惰性求值方法

有些方法返回结果非Stream类型,则是及早求值方法

“为什么要区分惰性求值及早求值

只有在对需要什么样的结果和操 作有了更多了解之后,才能更有效率地进行计算。

例如, 如果要找出大于 10 的第一个数字, 那么并不需要 和所有元素去做比较, 只要找出第一个匹配的元素就够了。 这也意味着可以在集合类上级联多种操作, 但迭代只需一次。这也是函数编程中惰性计算的特性,即只在需要产生表达式的值时进行计算。这样代码更加清晰,而且省掉了多余的操作。

Optional、Collectors

这里还对上述列表操作中相关的OptionalCollectors类做下说明。

Optional类是为了解决经常遇到的NullPointerException出现的,这个类是一个可能包含空值的容器类。用Optional替代null可以显示说明结果可能为空或不为空,再使用时使用isPresent方法判断就可以避免直接调用的空指针异常。

Collectors类是一个非常有用的是归约操作工具类,工具类中的方法常与流的collect方法结合使用。比如groupingBy方法可以用来分组,在转化Map时非常实用;partitioningBy方法可以用来分区(分区可以当做一种特殊的分组,真假值分组),joining方法可以用来连接,这个应用在比如字符串拼接的场景。

# 并行流

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

Collection接口的实现类调用parallelStream方法就可以实现并行流,相应地也获得了并行计算的能力。或者Stream接口的实现调用parallel方法也可以得到并行流。

并行流实现机制是基于fork/join 框架,将问题分解再合并处理。

# 了解 Fork/Join 框架

Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。

# Fork/Join 框架与传统线程池的区别

采用 “工作窃取”模式(work-stealing):

当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。

相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行.那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。

# 影响性能因素

不过并行计算是否一定比串行快呢?

这也不一定。实际影响性能的点包括:

  1. 数据大小

    输入数据的大小会影响并行化处理对性能的提升。 将问题分解之后并行化处理, 再将结果合并会带来额外的开销。 因此只有数据足够大、 每个数据处理管道花费的时间足够多时, 并行化处理才有意义。

  2. 源数据结构

    每个管道的操作都基于一些初始数据源, 通常是集合。 将不同的数据源分割相对容易,这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升。

  3. 装箱

处理基本类型比处理装箱类型要快。

  1. 核的数量

    极端情况下, 只有一个核, 因此完全没必要并行化。 显然, 拥有的核越多,获得潜在性能提升的幅度就越大。 在实践中, 核的数量不单指你的机器上有多少核, 更是指运行时你的机器能使用多少核。 这也就是说同时运行的其他进程, 或者线程关联性(强制线程在某些核或 CPU 上运行) 会影响性能。

  2. 单元处理开销

    比如数据大小, 这是一场并行执行花费时间和分解合并操作开销之间的战争。

    花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显实际在考虑是否使用并行时需要考虑上面的要素。

    在讨论流中单独操作每一块的种类时, 可以分成两种不同的操作: 无状态的和有状态的。

    无状态操作整个过程中不必维护状态, 有状态操作则有维护状态所需的开销和限制。如果能避开有状态, 选用无状态操作, 就能获得更好的并行性能。

    无状态操作包括 map、filter 和 flatMap

    有状态操作包括 sorted、 distinct 和 limit

    这种理解在理论上是更好的,当然实际使用还是以测试结果最为可靠 。

# 四、JDK9 新特性

JDK9 新特性清单

  • 目录结构
  • 模块化系统
  • jshell
  • 接口的私有方法
  • 改进try-with-resourcs
  • 改进钻石操作符
  • 限制使用单独下划线标识符
  • String存储结构变更
  • 快速创建只读结合
  • 增强Stream API
  • 改进Optional 类
  • 多分辨率图像 API
  • 全新 HTTP客服端API
  • 智能JAVA 编译工具
  • 统一JVM 日志系统
  • javadoc 的 HTML5 支持
  • java 动态编译

# 目录结构

JDK9具体目录结构如下所示:

  • bin: 该目录包含所有的命令。
  • conf: 包含用户可以编辑的配置文件,例如以前位于jre\lib 目录中的.properties 和
  • .policy 文件。
  • include: 包含一些编译本地代码时使用的C/C++头文件。
  • jmods: 包含JMOD 格式的平台模块,创建自定义运行映射时需要它。
  • legal: 包含法律声明。
  • lib: 包含非Windows 平台上动态链接的本地库,其子目录和文件不应由开发人员直接编辑或使用。

注:JDK9 目录中再有jre子目录。

# 模块化

在Java 9之前,一个大型Java程序会生成自己的jar文件,同时引用依赖的第三方jar文件,而JVM自带的Java标准库,实际上也是以jar文件形式存放的,这个文件叫 rt.jar ,一共有50多M。

从Java 9开始,原有的Java标准库已经由一个单一巨大的 rt.jar 分拆成了几十个模块,这些模块以 .jmod 扩展名标识。

这些 .jmod 文件每一个都是一个模块,模块名就是文件名。例如:模块 java.base 对应的文件就是java.base.jmod

模块之间的依赖关系已经被写入到模块内的module-info.java文件了。所有的模块都直接或间接地依赖java.base模块,只有 java.base 模块不依赖任何模块,它可以被看作是“根模块”,好比所有的类都是从 Object 直接或间接继承而来。

优势

JDK9将JDK分成一组模块,可以在编译时,运行时或构建时进行组合。模块化可以减少内存开销;只需必要的模块,并非全部模块,可以简化各种类库和大型应用的开发和维护。

使用

  • module-info.java:该文件必须位于模块的根目录中。该文件用于定义模块需要什么依赖,以及哪些包被外部使用。
  • exports:控制着哪些包可以被其他模块访问到,所有不被exports的包默认都被封装在模块里面不被外界所使用。
  • requires:指明对其他模块的依赖。

# jshell

REPL Read-Eval-Print Loop

JDK9新增了REPL工具jshell,JShell 将 REPL 功能添加到 Java 平台。 REPL是一种交互式编程工具,它循环、不断地读取用户输入、评估输入并打印输入值或输入引起的状态更改描述。Scala、Ruby、JavaScript、Python 等都具有 REPL,并且都允许小型初始程序。

jshell工具提供了一个交互式命令界面,可以评估声明,语句和表达式,无需编译即可返回执行结果。无论是在初学JAVA或学习新的API时都非常有用。

# 接口的私有方法

Java8 之前的接口功能有哪些?

  • 常量
  • 抽象方法

Java8 接口功能有哪些呢?

  • 静态方法,只能通过接口调用

  • 默认方法

    • 实现类可以访问
    • 如果实现类重写了默认方法,调用时调用的是重写的默认方法
    • 如果实现类没有重写默认方法,则调用的是父类的默认方法
    • 如果实现了多个接口,而多个接口的默认方法重复,则实现类需要重写默认方法,否则会报错

Java9 新支持的接口私有方法能够给我们带来什么帮助呢?

  • 私有静态方法
  • 私有方法

支持私有方法的接口抽象类的区别?

接口正逐渐取代抽象类的功能特性,Java9接口支持私有方法则进一步代替了抽象类的职责。

共同的特性:

  • 支持抽象函数

  • 允许函数实现

  • 允许部分函数实现

区别:

  • 接口针对于抽象类最大的优势在于多继承

  • 在接口中只能定义常量,无法定义成员变量,在Spring中也无法进行注入。

那各自使用的时机我们怎么去抉择呢?

  • 如果我们有多重继承的需求我们应该考虑使用接口实现
  • 如果有大量模板代码可以优先考虑抽象类,毕竟接口在定义参数的时候局限性比较大。

# 改进try-with-resourcs

try-with-resources语句

try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。这些关闭的资源必须在try语句后的括号中初始化。

所谓的资源(resource)是指在程序完成后,必须关闭的对象。try-with-resources 语句确保了每个资源在语句结束时关闭。

所有实现了 java.lang.AutoCloseable 接口(其中,它包括实现了 java.io.Closeable 的所有对 象),可以使用作为资源。

BufferedInputStream bin = null;
BufferedOutputStream bout = null;
try {
    bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
    bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // ....关闭资源
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

JDK7使用try-with-resources语法糖:

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
} catch (IOException e) {
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9

JDK9改进

在JDK9中改进了try-with-resources语句:

如果你已经有一个资源是final或等效于final变量,您可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。

即你可以在try外初始化资源,然后在try后的括号中添加需要自动关的资源即可。

final BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
final BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")));
try (bin, bout) {
    int b;
    while ((b = bin.read()) != -1) {
        bout.write(b);
    }
} catch (IOException e) {
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10

# 改进钻石操作符

Diamond Operator 钻石操作符

钻石操作符

<>这个运算符在java里叫钻石运算符。作用是从引用的声明中推断类型java7的新特性。可以让代码更易读,但它不能用于匿名的内部类。

HashMap<String,Integer> map =  new HashMap<String,Integer>();  // java7以前的版本
HashMap<String,Integer> map =  new HashMap<>(); // java7后的版本
1
2

JDK9改进

在 java 9 中, 它可以与匿名的内部类一起使用,从而提高代码的可读性。

// jdk9允许
Comparator<Integer> comparator = new Comparator<>() {
	@Override
	public int compare(Integer o1, Integer o2) {
		return 0;
	}
};
1
2
3
4
5
6
7

# 限制使用单独下划线标识符

在JDK8之前可以使用“_”单独的下划线作为标识符。但在JDK9中将单独的下划线标识符限制使用了。

int _ = 20; //jdk9已禁止
System.out.println(_);
1
2

# String底层存储结构变化

JDK8 String底层使用char数组存储数据。(char类型是由两个字节组成)

private final char value[];
1

JDK9将String底层存储数据改为byte数组存储数据。

private final byte[] value
1
  • 为何进行调整结构?

Java官方人员根据上万个应用程序的heap dump信息进行了分析,大多数String对象仅包含Latin-1字符。此类字符只需要一个字节的存储空间,因此String对象内部char中的一半空间其实是未使用的。

并且我们都知道字符串都是用String Poo 来存储的,String Pool 通常使用了 JVM 的 heap 内存空间,Heap 内存空间又是 JVM 垃圾清理程序活动的地方。

  • 改变后效果:
  1. 提高内存空间的使用率
  2. 减少JVM垃圾清理的工作量

# 快速创建只读集合

JDK9ListSetMap集合中新增of静态方法,快速创建只读集合。

List<String> list = List.of("三七","人参","鹿茸","紫河车");
list.add("黄连"); // 会抛出异常

Set<String> set = Set.of("三七","人参","鹿茸","紫河车");

Map<Integer,String> map = Map.of(1,"三七",2,"人参",3,"鹿茸",4,"紫河车");
1
2
3
4
5
6

# 增强Stream API

JDK9在Stream接口中新增方法:dropWhiletakeWhileofNullable

方法 描述
dropWhile 只保留流中第一个不满足条件的元素之前的所有元素
takeWhile 丢弃第一个不满足条件的元素之前的所有元素,只要后面的其他元素
ofNullable 避免NullPointerExceptions并避免对流进行空检查
List<Integer> numberList= Arrays.asList(1,3,5,8,10,20,35,2,5,7);
numberList.stream().takeWhile(num->num<=20).forEach(System.out::println);
// 输出 1,3,5,8,10,20

List<Integer> numberList= Arrays.asList(1,3,5,8,10,20,35,2,5,7);
numberList.stream().dropWhile(num->num<=20).forEach(System.out::println);
// 输出 35,2,5,7

Stream<Integer> stream4 = Stream.ofNullable(null);
System.out.println(stream4.count());
// 输出 0
1
2
3
4
5
6
7
8
9
10
11

# 改进Optional类

Optional 类是在JDK8中新增的类,主要是为了解决空指针异常

JDK9中对这个类进行了改进,主要是新增了三个方法:streamifPresentOrElseor

方法 描述
stream 可以将Optional可以直接转为stream 流来进行处理
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) 如果Optional 包含一个值,则会对所包含的值调用函数action;如果Optional不包含任何值,则将调用emptyAction。
or 如果值存在,返回 Optional 指定的值,否则返回一个预设的值。
List<String> list = List.of("三七","人参","鹿茸","紫河车");
Optional<List<String>> optional = Optional.ofNullable(list);
optional.stream().forEach(System.out::println);

Optional<String> optional2 = Optional.of("Mahesh");
optional2.ifPresentOrElse( x ‐> System.out.println("Value: " + x),()>
System.out.println("Not Present."));

Supplier<Optional<String>> supplierString = ()> Optional.of("Not Present");
optional2 = optional2.or(supplierString);
1
2
3
4
5
6
7
8
9
10

# 多分辨率图像API

java.awt.image包下新增了支持多分辨率图片的API。

  1. 将不同分辨率的图像封装到一张(多分辨率的)图像中,作为它的变体。
  2. 获取这个图像的所有变体。
  3. 获取特定分辨率的图像变体,表示一张已知分辨率单位为 DPI 的特定尺寸大小的逻辑图 像,并且这张图像是最佳的变体。
  4. java.awt.image.MultiResolutionImage接口的基础实现java.awt.image.BaseMultiResolutionImage获取所需要的变体。
  5. 通过接口的getResolutionVariant(double destImageWidth, double destImageHeight) 方法,根据分辨率获取图像。

# 全新的HTTP客户端API

HTTP,用于传输网页的协议,早在 1997 年就被采用在目前的 1.1版本中。直到 2015 年,HTTP2 才成为标准。

JDK9 中有新的方式来处理 HTTP 调用。它提供了一个新的HTTP客户端(HttpClient),它将替代仅适用于blocking模式的HttpURLConnection(HttpURLConnection是在HTTP 1.0的时代创建的,并使用了协议无关的方法),并提供对 WebSocket 和 HTTP/2 的支持。

此外,HTTP客户端还提供 API 来处理 HTTP/2 的特性,比如和服务器推送等功能。

全新的 HTTP 客户端 API 可以从jdk.incubator.httpclient模块中获取。因为在默认情况下,这个模块是不能根据 classpath 获取的,需要使用 add modules 命令选项配置这个模块,将这个模块添加到 classpath中。

# 智能JAVA编译工具

智能 java 编译工具( sjavac )的第一个阶段始于 JEP139 这个项目,用于在多核处理器情况下提升JDK 的编译速度。如今,这个项目已经进入第二阶段,即 JEP199,其目的是改进 Java 编译工具,并取代目前 JDK 编译工具 javac,继而成为 Java 环境默认的通用的智能编译工具。

JDK 9 还更新了 javac 编译器以便能够将 java 9 代码编译运行在低版本 Java 中。

# 统一的JVM日志系统

日志是解决问题的唯一有效途径:曾经很难知道导致 JVM 性能问题和导致 JVM 崩溃的根本原因。不同的 JVM 日志的碎片化和日志选项(例如:JVM 组件对于日志使用的是不同的机制和规则),这使得 JVM 难以进行调试。

解决该问题最佳方法:

对所有的 JVM 组件引入一个单一的系统,这些 JVM 组件支持细粒度的和易配置的 JVM 日志。

# javadoc的HTML5支持

JDK8 生成的java帮助文档是在 HTML4 中。而HTML4 已经是很久的标准了。

JDK9 的javadoc,现支持HTML5 标准。

# 五、JDK10 新特性

JDK10 新特性清单

  • 局部变量类型推断
  • 将JDK多存储库合并为单储存库
  • 垃圾回收接口
  • 并行Full GC 的G1
  • 应用数据共享
  • 线程局部管控
  • 移除Native-Header Generation Tool (javah)
  • Unicode 标签扩展
  • 备用内存设备上分配堆内存
  • 基于实验JAVA 的JIT 编译器
  • Root 证书

# 局部变量类型推断

JDK10可以使用var作为局部变量类型推断标识符,其目的是为了省略不必要的局部变量类型的声明

标识符var

  • 标识符var不是关键字;相反,它是一个保留的类型名称
  • 此符号仅适用于局部变量、增强for循环的索引、以及传统for循环的本地变量;
  • 不能使用于方法形式参数、构造函数形式参数、方法返回类型、字段、catch形式参数或任何其他类型的变量声明。
var str = "ABC"; //根据推断为 字符串类型
var l = 10L;//根据10L 推断long 类型
var flag = true;//根据 true推断 boolean 类型
var flag1 = 1;//这里会推断boolean类型。0表示false 非0表示true
var list = new ArrayList<String>(); // 推断 ArrayList<String>
var stream = list.stream(); // 推断 Stream<String>

// 以下声明不能使用var
var map = new HashMap<>();
1
2
3
4
5
6
7
8
9

该功能并不是一把万能钥匙,我们还需要因地制宜的去使用它,这是一把双刃剑。用的好,可以简化代码,减少复杂度并提高可读性。用错了地方可能会造成反效果。

# 将JDK多存储库合并为单存储库

为了简化开发,将JDK多存储库合并到一个存储库中。

多年来,JDK的完整代码已经被分解成多个存储库。

在JDK9 中有八个仓库:root、corba、hotspot、jaxp、jaxws、jdk、langtools和nashorn。

在JDK10中被合并为一个存储库。

虽然这种多存储库模型具有一些优点,但它也有许多缺点,并且在支持各种可取的源代码管理操作方面做得很差。特别是,不可能在相互依赖的变更存储库之间执行原子提交。

例如,如果一个bug修复或RFE的代码现在同时跨越了jdk和hotspot 存储库,那么对于两个存储库来说,在托管这两个不同的存储库中,对两个存储库的更改是不可能实现的。跨多个存储库的变更是常见。

# 并行Full GC的G1

JDK10 通过并行Full GC,改善G1的延迟。

G1垃圾收集器JDK9中是默认的。以前的默认值并行收集器中有一个并行的Full GC。为了尽量减少对使用GC用户的影响,G1的Full GC也应该并行。

G1垃圾收集器的设计目的是避免Full收集,但是当集合不能足够快地回收内存时,就会出现完全GC。目前对G1的Full GC的实现使用了线程标记-清除-压缩算法。

JDK10 使用并行标记-清除-压缩算法,并使用YoungMixed收集器相同的线程数量。线程的数量可以由-XX:ParallelGCThreads选项来控制,但是这也会影响用Young和Mixed收集器的线程数量。

# 六、JDK11 新特性

JEP JDK Enhancement Proposals JDK增强提案

2018年9月26日,Oracle 官方宣布 Java 11 正式发布。这是 Java 大版本周期变化后的第一个长期支持版本(LTS版本,Long-Term-Support,持续支持到2026年9月),非常值得关注。

Java11 带来了 ZGC、Http Client 等重要特性,一共包含 17 个 JEP。

# 七、JDK16新特性

# 模式匹配instanceof

在之前,我们如果需要实现一个获取子类对象,我们需要进行三步:先用 instanceof 进行判断,通过后再进行类型强转,最后再赋值给一个变量。

Fruit apple = new Apple();
if (apple instanceof Apple) {
	Apple a = (Apple)apple;
    a.exec();
}
1
2
3
4
5

JDK16instanceof进行更加简洁和安全的表达。

Fruit apple = new Apple();
if (apple instanceof Apple a) {
    a.exec();
}
1
2
3
4

# 档案类record

通常我们创建一个不可变的类,就是用final修饰实现不可继承与不可修改。并且,经常需要编写非常多的冗余代码(例如getter/setter、toString、equals、hashCode...)。这样带来了非常多的低价值、重复、还容易出错的代码。

public final class Test {
    final Integer code;
    final String name;
    
    // 无参构造方法、有参构造方法
    
    // getter/setter
    
    // toString
    
    // hashCode
}
1
2
3
4
5
6
7
8
9
10
11
12

JDK16提供了档案类——Record,其使得我们去构建不可变的类时,避免去编写低价值的代码,并且拥有不可变的特性可以天然支持我们的高并发操作,因为它是线程安全的。

public record Test2(Integer code, String name) {
    
    // 构造方法
    public Test2 {
        sout("...")
    }
}
1
2
3
4
5
6
7

说明:

  • 档案类直接用record进行修饰;
  • 参数直接声明在类名后面;
  • 只能有一个全部参数的构造方法;
  • JAVA编译器会将档案类,编译为final class,并且生成final修饰的参数,以及生成toString、equals、hashCode、获取参数的方法。

# 封闭类sealed、permits

封闭类的主要作用就是为了限制可扩展性,在可扩展性上实现更细粒度的控制。

封闭类增加sealed修饰符来修饰类,并通过permits指定可扩展的类。它可以作用在接口方法上。

  • 封闭类为什么要限制可扩展性呢?无限的可扩展性会带来什么坏处。

    比如说一个中间件的作者,有一个基类,但是只想让自己特定的几个子类去扩展,在作者知道已知子类的情况下,就不需要在基类中写代码来防御未知子类的过度扩展。

  • 我们以前有那些方案限制?

  1. 类使用 final 修饰,直接关闭可扩展
  2. 直接使用 public 全开放

permits指定许可子类

声明许可类需满足以下条件:

  • 许可类和封闭类要在同一个模块或者是包空间里, 也就是说在编译期间,封闭类必须可以访问到他的许可类;

  • 许可类必须是封闭类的直接扩展类;

  • 许可类必须声明是否继续保持封闭;

    • 声明final关键字,从而关闭扩展性;

    • 声明sealed,从而延续受限制的扩展性;

    • 声明non-sealed,从而支持不受限制的扩展性。

public sealed class Fruit permits Apple {}

public final class Apple extends Fruit {}
1
2
3

# 新的Switch表达式

Java16提供了Switch表达式的升级,其目的是为了解决switch语句的一些不规则性成为障碍。比如说case标签之间的默认控制流行为,case块中的默认范围,无意义的break语句。

private String getContentType(String suffix) {
    String contentType = "";
    switch (suffix) {
        case ".jpg":
        case ".jpeg":
            contentType = "image/jpeg";
            break;
        case ".png":
            contentType = "image/png";
            break;
        case ".gif":
            contentType = "image/gif";
            break;
        default:
            contentType = "multipart/form-data";
            break;
    }
    return contentType;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

新的 switch 给我们带来了下面几个特性

  • 不需要手动增加break
  • 支持多值匹配
  • 表达式支持;
  • yielding a value 支持case内容代码块,通过yield关键字返回;

新的语法特性给我们带来了便捷性和编码效率的提升。

private String getContentType(String suffix) {
    var contentType = "";
    switch (suffix) {
        case ".jpg", ".jpeg" -> contentType = "image/jpeg";
        case ".png" -> contentType = "image/png";
        case ".gif" -> contentType = "image/gif";
        default -> contentType = "multipart/form-data";
    }
    return contentType;
}

private String getContentType2(String suffix) {
    var contentType = switch (suffix) {
        case ".jpg", ".jpeg" -> "image/jpeg";
        case ".png" -> "image/png";
        case ".gif" ->  "image/gif";
        default -> {
            System.out.println("默认。。。");
            yield "multipart/form-data";
        };
    }
    return contentType;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 八、JDK17 新特性

# 九、其他版本新特性

# JDK14新特性

# JDK15新特性

# 文字块

其目的是为了支持多行字符串文字,避免大多数转义、换行、拼接操作,以可预测的方式自动格式化字符串,并在需要的时候让开发人员控制格式。

我们在Java程序开发中经常会遇到文本中需要嵌入HTML、XML、SQL、JSON片段等需求,在以前我们需要转义符、连接符、换行符进行大量的编辑才能编译包含该片段的代码。很多时候这些代码是非常难以阅读和维护的。

文字块特性实现了以下四种特性:

  • 以一种友好的方式来表达跨越多行源代码字符串以简化编写Java程序,同时又避免了常见情况下的转义序列( \n 符)。
  • 增强了在Java程序中编写非Java语言代码字符串的可读性。
  • 支持了字符串的文字迁移同时保持了原格式。
  • 添加转义序列 \s 以管理显式空格和换行控制符。
String html = """
    <div>
    	<span>测试文字块</span>
    </div>
    """;
    
String html2 = """
    <div>   \s
    	<span>测试文字块</span>
    </div>
    """;
1
2
3
4
5
6
7
8
9
10
11