本文描述 Java 8 的其余新特性,包括提升的类型推断,Java 类型注解,方法参数反射,针对存在键冲突的 HashMap 的性能改进,Date-Time 程序包,HotSpot 删除了 PermGen 。
提升的类型推断
类型推断这个概念可能比较陌生,但我们编程时或多或少都享受过它带来的便利。
在 Java 7 中,可以使用赋值语句的目标类型来进行类型推断。
List<String> stringList = new ArrayList<>();
这是在使用泛型类实例时进行的类型推断,只要编译器可以从上下文中推断类型参数,就可以用一组空的类型参数(<>)替换调用泛型类的构造函数所需的类型参数。这对尖括号非正式地称为钻石符。
理解类型推断需要明白一个目标类型和上下文的概念,目标类型是编译器在表达式出现的地方(上下文)所期待的数据类型。例如此处的目标类型便是 ArrayList<String>
,参数化类型是 String,上下文在此处应该是赋值语句。
在 JavaSE 8 中,可以在更多上下文中使用目标类型进行 类型推断。最突出的例子是使用 方法调用的目标类型 来推断其参数的数据类型。
List<String> stringList = new ArrayList<>();
stringList.add("A");
stringList.addAll(Arrays.asList());
此处 addAll 期待一个集合实例,在不考虑泛型的情况下,Arrays.asList 返回一个 List 实例,它作为集合的子类,这是正常的。
在考虑泛型的情况下,此处的目标类型是 Collection<? extends String> ,Arrays.asList 返回的是一个 List 实例。在这里 Java 8 编译器能够根据目标类型推断出 T 是 String。
Java 7 中,则无法从方法调用的参数目标类型中推断出类型,上述代码将在 Java 7 中得到一个错误:
error: no suitable method found for addAll(List<Object>) ...
method List.addAll(Collection<? extends String>) is not applicable (actual argument List<Object> cannot be converted to Collection<? extends String> by method invocation conversion)
因此,在这种Java编译器无法推断类型的情况下,必须用 类型见证器 显式地指定类型变量的值。例如,以下在Java SE 7中将正常工作:
List<String> stringList = new ArrayList<>();
stringList.add("A");
stringList.addAll(Arrays.<String>asList());
Java 类型注解
Java 8 之前注解只能出现在元素声明的地方,而在 Java 8 注解可以出现在任何类型使用的地方,这类注解称为类型注解。像这样:
public static <@Schedule T> void start(T time) throws @Schedule Exception{
Job job = new @Schedule Job();
float i = ((@Schedule float) 123);
class TestImpl implements @Schedule Serializable{
}
}
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Schedules.class)
public @interface Schedule {
String time() default "";
}
表示注解能够在哪里使用的元注解是 Target
, 其接受一个 ElementType
类型的 value 元素数组。
Java 8 为其新增了两种类型以支持类型注解,需要使用类型注解时,为期望注解的 Target
元注解添加 TYPE_USE
值即可:
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
TYPE_PARAMETER
表示可在声明参数类型使用。TYPE_USE
则表示可在类型出现的任何地方使用注解。如果仔细观察其它类型,会发现它们的注释均以 declaration
结尾,这也表明了在 Java 8 之前注解只能应用于声明。
类型注解的作用,官方文档这样描述道:
创建类型注解是为了支持对 Java 程序的改进分析,从而确保更强的类型检查。JavaSE 8 发行版不提供类型检查框架,但它允许您编写(或下载)一个类型检查框架,该框架实现为与Java编译器一起使用的一个或多个可插入模块.
这有一个官方推荐的 检查框架, 这个框架的简介非常吸引人,它这样描述道:“您是否厌倦了空指针异常、意外的副作用、SQL注入、并发错误、错误的相等性测试以及其他在测试期间或字段中出现的运行时错误?”。如果感兴趣的话,可以深入研究。
方法参数反射
java.lang.reflect.Executable.getParameters 方法能够从任何方法或者构造函数中获取形式参数的名称,但 .class 默认情况下并不存储形式参数名称,为了启用它,需要在使用 javac
编译时,使用 -parameters
配置选项。
抽象类
Executable
有两个子类,分别是Method
和Constructor
类。
public class Job {
public void testParametersName(String name, List<Integer> ages){
}
}
以上代码使用 javac Job.java
编译后,再使用 javap -v -s Job.class
反汇编后,可得到以下代码(部分):
public void testParametersName(java.lang.String, java.util.List<java.lang.Integer>);
descriptor: (Ljava/lang/String;Ljava/util/List;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 14: 0
Signature: #13 // (Ljava/lang/String;Ljava/util/List<Ljava/lang/Integer;>;)V
如果使用携带 -parameters 选项编译,再使用同样的反汇编命令,针对以上代码,我们可以得到如下:
public void testParametersName(java.lang.String, java.util.List<java.lang.Integer>);
descriptor: (Ljava/lang/String;Ljava/util/List;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 14: 0
MethodParameters:
Name Flags
name
ages
Signature: #17 // (Ljava/lang/String;Ljava/util/List<Ljava/lang/Integer;>;)V
对此,可以证实,方法参数名真实地存储于 .class 文件中。
针对存在键冲突的 HashMap 的性能改进
这指的是提高java.util.HashMap在高哈希冲突条件下,使用平衡树而不是链表来存储映射项,在LinkedHashMap类中实现相同的改进。
如果直接说红黑树,便是老生常谈的一个话题。这里简单描述下其主要思想:“一旦 hash bucket
中的项数增长超过某个阈值,该 bucket 将从使用条目的链表切换到平衡树。在高哈希冲突的情况下,这将提高从O(n)到O(logn)的最坏情况性能”。
那么,Hashtable
, WeakHashMap
以及 IdentityHashMap
有没有实现这个改进呢?
使用 Hashtable
的遗留代码可能依赖其迭代顺序,所以,不会做此改进。WeakHashMap
又由于必须考虑其弱密钥的复杂性,这将导致性能下降,这也是不允许的。IdentityHashMap
使用系统识别码来生成 hashcode,因此冲突通常很少发生,所以它并不需要实现此技术。
Date-Time 程序包
一组新的日期-时间程序包,提供新的日期-时间模型,这在 java.time
包中。常用的类应该是 LocalDateTime
,LocalDate
以及 LocalTime
,使用起来非常方便。
HotSpot 删除了 PermGen
PermGen
被称为永久代,也叫方法区。
Java 类在 Java Hotspot VM
中具有内部表示形式,被称为类元数据。在 Java Hotspot VM
的早期版本中,类元数据是在永久代中生成分配的。在 JDK 8
中,永久代已删除,并且类元数据已分配在本机内存中。默认情况下,可用于类元数据的本地内存数量是无限的。使用该选项 MaxMetaspaceSize
对用于类元数据的本机内存量设置上限。
Java Hotspot VM
显式管理用于元数据的空间。从操作系统请求空间,然后将其分成多个块。类加载器从其块分配元数据空间(块绑定到特定的类加载器)。当为类加载器卸载类时,其块将被回收以重新使用或返回给OS。
需要明白的是,尽管永久代也常被称作方法区,但永久代的移除和方法区并没有关系。方法区是在 JVM 规范中定义的,不同的虚拟机选择了不同的实现方式,这可能是最容易被误解的地方了。
写在最后
这是 Java 8 系列最后的一篇文章,此篇文章描述的内容都是浅尝辄止。Java 8 增强的特性还有很多,但日常接触的大多是这些了。
如果你觉得我的文章还不错,并对后续文章感兴趣的话,或者说我们有什么能够交流分享的,可以通过扫描下方二维码来关注我的公众号!