文章目录
Java 8 进阶知识和用法
推荐文章:
《【Java 8新特性进阶】详解:Lambda、Stream、新日期API及更多(代码实战:上篇)》
还不了解java8 基础知识的可以先看这两篇文章:
《Java 8 新特性:Lambda 表达式与 Stream 流,重构你的编码效率(上篇:Lambda 表达式)》
《Java 8 新特性:Lambda 表达式与 Stream 流,重构你的编码效率(下篇:stream流)》
6. 新时间日期 API 的深入理解
6.1 java.time
包
Java 8 引入了一个全新的时间日期 API,位于 java.time
包中。这个 API 提供了一套更加现代、完整和一致的方式来处理日期、时间、时区和持续时间。以下是 java.time
包中的一些核心类及其用途:
LocalDate
:表示日期(年、月、日)而不包含时间信息。LocalTime
:表示时间(小时、分钟、秒、纳秒)而不包含日期信息。LocalDateTime
:表示日期和时间的组合,不含时区信息。ZonedDateTime
:表示带有时区的日期和时间。Instant
:表示从 Unix 时间戳开始的时间点,常用于网络传输。Duration
:表示两个时刻之间的时间间隔。Period
:表示两个日期之间的间隔。ZoneId
:表示时区 ID,如"America/New_York"
。ZoneOffset
:表示相对于 UTC/Greenwich 的偏移量。Clock
:提供系统时钟的访问点,可用于获取当前时间戳。
6.2 时区处理
正确处理时区是非常重要的,特别是在涉及跨时区的应用程序中。java.time
包提供了一些类来帮助处理时区问题。
ZoneId
:表示时区标识符,可以用来创建带有时区的日期时间对象。ZonedDateTime
:表示带有特定时区的日期时间,可以用来表示和操作带有时区的日期时间。
示例:
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class TimezoneExample {
public static void main(String[] args) {
// 获取当前时间
ZonedDateTime now = ZonedDateTime.now();
System.out.println("Current time in local timezone: " + now);
// 设置时区
ZoneId newYorkTimeZone = ZoneId.of("America/New_York");
ZonedDateTime newYorkTime = ZonedDateTime.now(newYorkTimeZone);
System.out.println("Current time in New York: " + newYorkTime);
// 格式化输出
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
String formattedNewYorkTime = newYorkTime.format(formatter);
System.out.println("Formatted New York time: " + formattedNewYorkTime);
}
}
6.3 代码示例
下面是一个综合示例,展示了如何使用 java.time
包中的类来处理日期和时间:
import java.time.*;
import java.time.format.DateTimeFormatter;
public class DateTimeExample {
public static void main(String[] args) {
// 创建 LocalDate 对象
LocalDate today = LocalDate.now();
System.out.println("Today's date: " + today);
// 创建 LocalTime 对象
LocalTime currentTime = LocalTime.now();
System.out.println("Current time: " + currentTime);
// 创建 LocalDateTime 对象
LocalDateTime currentDateTime = LocalDateTime.now();
System.out.println("Current date and time: " + currentDateTime);
// 创建 ZonedDateTime 对象
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("Current date and time in New York: " + zonedDateTime);
// 创建 Duration 对象
Duration duration = Duration.between(LocalTime.of(10, 0), LocalTime.of(11, 30));
System.out.println("Duration: " + duration);
// 创建 Period 对象
LocalDate birthday = LocalDate.of(1990, Month.JANUARY, 1);
Period age = Period.between(birthday, today);
System.out.println("Age: " + age.getYears() + " years, " + age.getMonths() + " months, " + age.getDays() + " days");
// 使用 DateTimeFormatter 格式化日期时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedDateTime = currentDateTime.format(formatter);
System.out.println("Formatted current date and time: " + formattedDateTime);
}
}
6.4 时区转换
处理时区转换时,你需要确保正确地处理夏令时和时区偏移的变化。ZonedDateTime
类可以帮助你轻松地在不同的时区之间转换。
示例:
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class TimezoneConversionExample {
public static void main(String[] args) {
// 创建一个带有时区的日期时间对象
ZonedDateTime londonTime = ZonedDateTime.now(ZoneId.of("Europe/London"));
System.out.println("London time: " + londonTime);
// 转换到另一个时区
ZonedDateTime newYorkTime = londonTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("New York time: " + newYorkTime);
// 使用 DateTimeFormatter 格式化输出
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
String formattedNewYorkTime = newYorkTime.format(formatter);
System.out.println("Formatted New York time: " + formattedNewYorkTime);
}
}
6.5 总结
java.time
包:提供了丰富的类来处理日期和时间,包括LocalDate
,LocalTime
,LocalDateTime
,ZonedDateTime
,Instant
,Duration
, 和Period
。- 时区处理:通过
ZoneId
和ZonedDateTime
类可以方便地处理和转换时区。
这个新的时间日期 API 更加强大、灵活,并且能够更好地处理各种日期时间相关的需求。
7. Nashorn JavaScript 引擎的深入理解
7.1 脚本语言集成
Nashorn 是 Java 8 中引入的一个 JavaScript 引擎,它允许在 Java 应用程序中直接嵌入和执行 JavaScript 代码。Nashorn 基于 ECMAScript 5.1 规范,并提供了一些扩展功能,使其能够与 Java 类型和对象无缝交互。
示例:
import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
try {
// 执行 JavaScript 代码
String script = "print('Hello, Nashorn!');";
engine.eval(script);
// 与 Java 对象交互
engine.put("javaString", "Java String");
script = "print(javaString.toUpperCase());";
engine.eval(script);
} catch (ScriptException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们首先创建了一个 ScriptEngineManager
实例,然后通过 getEngineByName("nashorn")
获取 Nashorn 引擎。接着,我们执行了一些简单的 JavaScript 代码,并且展示了如何将 Java 对象传递给 JavaScript 代码。
7.2 性能考量
虽然 Nashorn 提供了与 Java 的紧密集成,但它也有一些性能特点和限制,需要注意以下几点:
-
初始化成本:启动 Nashorn 引擎时有一定的初始化成本,这意味着如果频繁地启动和关闭引擎,可能会对性能产生影响。为了避免这种情况,可以复用一个已经初始化的引擎实例。
-
类型转换:Nashorn 在执行 JavaScript 代码时会进行类型转换,以便与 Java 对象交互。这些转换可能会带来一定的性能开销。
-
优化级别:Nashorn 支持不同的优化级别,可以通过设置
-Dnashorn.options.Opt=3
系统属性来启用最高级别的优化。然而,这些优化可能会影响启动时间和内存使用。 -
JavaScript 特性:Nashorn 不支持所有 JavaScript 特性,特别是那些在 ECMAScript 6 及更高版本中引入的新特性。如果需要使用较新的 JavaScript 特性,可能需要考虑其他 JavaScript 引擎,如 GraalVM。
-
安全性和沙箱:Nashorn 支持使用
--language=js
选项来限制 JavaScript 代码的权限,以提高安全性。但是,这些限制可能会影响性能。
7.3 示例代码
下面是一个更详细的示例,展示了如何在 Java 应用程序中使用 Nashorn 引擎执行 JavaScript 代码,并与 Java 对象进行交互:
import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
public class NashornExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
try {
// 执行简单的 JavaScript 代码
String script = "print('Hello, Nashorn!');";
engine.eval(script);
// 定义一个 Java 类
engine.eval("JavaClass = Java.type('com.example.NashornExample$JavaClass');");
// 创建 Java 对象并执行方法
engine.eval("var javaObject = new JavaClass();");
engine.eval("print(javaObject.greet('World'));");
// 使用 Java 对象作为 JavaScript 函数的参数
engine.eval("function greetWithJava(name) { return javaObject.greet(name); }");
String result = (String) engine.eval("greetWithJava('Nashorn');");
System.out.println(result);
} catch (ScriptException e) {
e.printStackTrace();
}
}
public static class JavaClass {
public String greet(String name) {
return "Hello, " + name + "!";
}
}
}
在这个示例中,我们定义了一个名为 JavaClass
的 Java 类,并且在 JavaScript 代码中创建了该类的实例。我们还定义了一个 JavaScript 函数 greetWithJava
,该函数使用 Java 对象的方法。
7.4 总结
- 脚本语言集成:Nashorn 引擎允许在 Java 应用程序中嵌入和执行 JavaScript 代码,提供了与 Java 类型和对象的紧密集成。
- 性能考量:Nashorn 在性能方面有一些特点和限制,包括初始化成本、类型转换、优化级别等。根据应用程序的具体需求,可能需要权衡这些因素。
Nashorn 是一个强大的工具,可以用于多种应用场景,例如脚本化、动态配置、嵌入式脚本等。然而,在 Java 11 之后,Nashorn 被标记为废弃,并计划在未来的版本中删除。因此,如果你正在开发一个新的项目,可能需要考虑使用其他 JavaScript 引擎,如 GraalVM。
8. 类依赖分析器 jdeps 的深入理解
8.1 依赖分析
jdeps
是一个命令行工具,用于分析 Java 类文件或 JAR 文件的依赖关系。它可以显示类和包之间的依赖关系,这对于理解代码结构和识别潜在的问题非常有用。
基本用法:
jdeps [options] <input>
其中 <input>
可以是类文件、JAR 文件或目录。[options]
允许你控制输出格式和其他行为。
示例:
假设你有一个名为 myapp.jar
的 JAR 文件,你可以使用 jdeps
来查看它依赖的其他类和包。
jdeps myapp.jar
这将列出 myapp.jar
中所有类的依赖关系。
8.2 迁移辅助
jdeps
可以帮助你迁移到新版本的 JDK 或者避免使用已弃用的功能。它提供了一些选项来突出显示这些问题:
--verbose
:显示更详细的输出,包括类文件的位置和版本。--recursive
:递归地处理目录中的所有文件。--classpath
:指定类路径。--deprecated
:显示已弃用的 API 的使用情况。--module
:显示模块依赖关系。--summary
:仅显示依赖关系的摘要。
示例:
假设你想检查一个名为 myapp.jar
的 JAR 文件是否使用了已弃用的 API。
jdeps --verbose --deprecated myapp.jar
这将显示所有使用已弃用 API 的类和方法。
8.3 示例代码
下面是一个具体的示例,展示了如何使用 jdeps
工具来分析一个简单的 Java 应用程序。
假设我们有一个简单的 Java 项目结构如下:
project/
├── src/
│ ├── com/
│ │ └── example/
│ │ └── Main.java
└── build/
└── classes/
└── com/
└── example/
└── Main.class
Main.java:
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
接下来,我们编译 Main.java
文件,并使用 jdeps
来分析生成的 Main.class
文件。
编译 Java 文件:
javac -d build/classes/ src/com/example/Main.java
使用 jdeps 分析:
jdeps build/classes/com/example/Main.class
这将显示 Main.class
文件的依赖关系。
如果你想查看是否有使用已弃用的 API,可以使用以下命令:
jdeps --verbose --deprecated build/classes/com/example/Main.class
8.4 迁移场景
假设你正在迁移到 Java 17,并且想检查一个名为 mylib.jar
的 JAR 文件是否使用了已弃用的功能或已被删除的 API。
jdeps --verbose --deprecated --module mylib.jar
这将显示所有使用已弃用 API 的类和方法,以及模块依赖关系。
8.5 总结
- 依赖分析:
jdeps
工具可以用于分析类文件或 JAR 文件的依赖关系,帮助你理解项目的结构。 - 迁移辅助:
jdeps
提供了多种选项来帮助迁移到新版本的 JDK 或者避免使用已弃用的功能,例如使用--deprecated
选项来突出显示已弃用的 API 的使用情况。
jdeps
是一个非常有用的工具,尤其是在处理大型项目时,它可以帮助你更好地理解项目的依赖关系,并且在迁移过程中提供指导。
9. 类文件结构变化与编译器优化
1. 编译器优化
Java 编译器(如 javac)在生成字节码时会执行多种优化,以提高运行时性能。其中一个常见的优化是在类文件中存储方法参数名称。这在调试和开发工具中非常有用,因为它可以帮助开发者更容易地理解方法签名和调用栈。
2. 方法参数名称保存
默认情况下,编译器不会在生成的 .class
文件中保留方法参数的名称。这是因为这些信息通常不是运行时所必需的,去掉它们有助于减小类文件的大小,并可能提升一些性能。然而,在调试和使用反射时,能够访问这些名称是非常有用的。
9.1 如何启用 -parameters
选项
为了在 .class
文件中包含方法参数名称,你需要在编译时使用 -parameters
选项。这可以通过 javac 命令行或其他构建工具(如 Maven 或 Gradle)来完成。
1. 使用 javac
当你使用 javac 编译 Java 源文件时,可以通过添加 -parameters
选项来指示编译器在 .class
文件中保留方法参数名称。
示例:
假设你有一个名为 Example.java
的源文件,其中包含一个具有多个参数的方法。
public class Example {
public void someMethod(int arg1, String arg2) {
// 方法体
}
}
要使用 -parameters
选项编译这个文件,你可以执行以下命令:
javac -parameters Example.java
2. 使用构建工具
如果你使用的是构建工具,如 Maven 或 Gradle,你也可以配置这些工具以使用 -parameters
选项。
Maven 示例:
在你的 pom.xml
文件中,可以配置 maven-compiler-plugin
插件来使用 -parameters
选项。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
Gradle 示例:
在你的 build.gradle
文件中,可以配置 java
插件来使用 -parameters
选项。
apply plugin: 'java'
sourceCompatibility = 17
targetCompatibility = 17
compileJava {
options.compilerArgs << '-parameters'
}
9.2 调试体验
启用 -parameters
选项后,你将在 .class
文件中看到方法参数名称被保留下来。这对于调试来说特别有用,因为在调试过程中,IDE 和调试器可以显示这些名称,从而让你更容易地跟踪方法调用。
示例
考虑以下代码片段:
public class Example {
public void logMessage(String message, int level) {
System.out.println("Message: " + message);
System.out.println("Level: " + level);
}
}
如果你使用 -parameters
选项编译此代码,然后运行调试器,你会看到类似于以下的调用栈信息:
at Example.logMessage(Example.java:5) [message=Hello, level=1]
这使得调试更加直观,因为你不需要查看源代码就可以知道每个参数的名称。
9.3 总结
- 编译器优化:编译器通常不会在
.class
文件中保存方法参数名称,但可以通过-parameters
选项来改变这一点。 - -parameters 选项:使用
-parameters
选项可以让.class
文件包含方法参数名称,从而提供更好的调试体验。
通过在编译时启用 -parameters
选项,你可以确保在调试过程中能够访问到方法参数的名称,从而简化调试过程并提高效率。
10. PermGen 到 Metaspace 的迁移
10.1 内存管理
在早期版本的 Java 虚拟机 (JVM) 中,类元数据(例如类定义、常量池等)被存储在一个称为“永久代”(Permanent Generation space,简称 PermGen)的特殊区域。PermGen 是堆的一部分,它有自己的初始大小和最大大小限制,可以通过 JVM 参数进行配置。由于 PermGen 是堆的一部分,所以它受到垃圾回收的影响,当 PermGen 区域满时,可能会触发 Full GC,这可能会导致应用暂停时间较长。
从 Java 8 开始,Oracle 和 OpenJDK 社区决定移除 PermGen 并引入了一个新的概念叫做“元空间”(Metaspace)。元空间位于本地内存(Native Memory)而不是堆中,这允许它使用更多的系统内存而不受堆大小的限制。元空间的设计旨在解决 PermGen 存在的一些问题,特别是频繁的垃圾收集问题。
10.2 元空间(Metaspace)与永久代(PermGen)的区别
-
位置:
- PermGen:位于 JVM 的堆中。
- Metaspace:位于本地内存中。
-
垃圾回收:
- PermGen:受垃圾回收的影响,当 PermGen 区域满时,可能会触发 Full GC。
- Metaspace:不受常规的垃圾回收影响,只有当元空间达到最大容量时,才会发生特殊的类卸载过程。
-
容量:
- PermGen:容量有限,可以通过
-XX:MaxPermSize
参数设置。 - Metaspace:默认情况下,容量几乎不受限,只受限于可用的物理内存和操作系统的限制。可以通过
-XX:MaxMetaspaceSize
参数设置最大值。
- PermGen:容量有限,可以通过
-
初始化大小:
- PermGen:需要手动设置初始大小。
- Metaspace:初始大小较小,随着类的加载而动态增长。
10.3 垃圾回收策略
尽管 Metaspace 不像 PermGen 那样经常受到垃圾回收的影响,但在某些情况下,你可能仍然需要调整 JVM 参数来优化 Metaspace 的使用。这里有一些关键参数:
-
-XX:MetaspaceSize:设置 Metaspace 的初始大小。这个参数可以用来控制 Metaspace 的增长速度,如果设置得过小,可能会导致频繁的类加载和卸载。
-
-XX:MaxMetaspaceSize:设置 Metaspace 的最大大小。当达到这个阈值时,JVM 将不再增加 Metaspace 的大小,并且会触发类卸载过程。如果未设置,则默认为没有限制。
-
-XX:MinMetaspaceFreeRatio 和 -XX:MaxMetaspaceFreeRatio:这两个参数可以用来控制 Metaspace 的增长和收缩行为。当 Metaspace 的空闲比例低于
MinMetaspaceFreeRatio
时,Metaspace 会自动增长;当空闲比例高于MaxMetaspaceFreeRatio
时,Metaspace 可能会缩小。这些参数可以帮助防止 Metaspace 过度增长或过度收缩。
10.4 示例
假设你想为 Metaspace 设置初始大小为 64MB,最大大小为 512MB,并且希望 Metaspace 的空闲比例保持在 40% 至 70% 之间,你可以使用以下 JVM 参数:
java -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=512m -XX:MinMetaspaceFreeRatio=40 -XX:MaxMetaspaceFreeRatio=70 YourApp
10.5 总结
- 内存管理:从 Java 8 开始,类元数据存储的位置从 PermGen 迁移到了 Metaspace。Metaspace 位于本地内存中,不受常规垃圾回收的影响,容量几乎不受限。
- 垃圾回收策略:可以通过调整
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
来优化 Metaspace 的使用。此外,还可以通过-XX:MinMetaspaceFreeRatio
和-XX:MaxMetaspaceFreeRatio
来控制 Metaspace 的动态扩展和收缩行为。
通过合理配置这些参数,你可以避免因 Metaspace 满而导致的不必要的类卸载,并确保应用程序稳定高效地运行。