01.01 Java基础篇|语言基础与开发环境速成
导读
- 适用人群:Java 初学者或需要系统梳理基础知识的开发者
- 学习目标:掌握 Java 语言核心语法、开发环境配置、常用 API 使用,为后续深入学习打下坚实基础
- 阅读建议:建议配合实际编码练习,理解每个概念的实际应用场景
核心知识架构
开发环境与项目结构
JDK/JRE/JVM 关系深度解析
三者的层次关系:
┌─────────────────────────────────────┐
│ JDK (Java Development Kit)│
│ ┌────────────────────────────────┐ │
│ │ JRE (Java Runtime Env) │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ JVM (Java Virtual │ │ │
│ │ │ Machine) │ │ │
│ │ └──────────────────────────┘ │ │
│ │ + 核心类库 (rt.jar等) │ │
│ └────────────────────────────────┘ │
│ + javac (编译器) │
│ + javap (反汇编器) │
│ + jconsole (监控工具) │
│ + jlink (模块化打包) │
└─────────────────────────────────────┘
源码级理解:
- JVM:负责字节码解释执行、内存管理(堆、栈、方法区)、GC 等
- JRE :JVM + 核心类库(
java.lang、java.util等),提供运行时环境 - JDK :JRE + 开发工具(
javac、javap、jconsole等),用于开发与调试
生产环境使用 jlink (JDK 9+)优化 :
jlink 是 Java Platform Module System (JPMS) 的一部分,允许创建只包含所需模块的定制化JRE。
bash
# 列出所有可用模块
java --list-modules
# 或使用 jlink 查看
jlink --list-plugins
# 基本语法
jlink --add-modules <模块列表> --output <输出目录> --module-path <模块路径>
# 示例:创建包含最小模块的运行时
jlink \
--module-path $JAVA_HOME/jmods \
--add-modules java.base \
--output myjre
# 压缩选项
--compress=0 # 不压缩(默认)
--compress=1 # 常量字符串共享
--compress=2 # 所有资源的 ZIP 压缩
# 优化选项
--strip-debug # 移除调试信息
--no-header-files # 移除头文件
--no-man-pages # 移除手册页
# 绑定服务
--bind-services # 链接服务提供者模块
# 生成启动脚本
--launcher <name>=<module>/<main-class>
安装与配置最佳实践
多版本管理(推荐使用 SDKMAN!):
bash
# 安装 SDKMAN!
curl -s "https://get.sdkman.io" | bash
# 安装多个 JDK 版本
sdk install java 17.0.8-tem
sdk install java 21.0.1-tem
# 切换版本
sdk use java 21.0.1-tem
# 查看当前版本
java -version
环境变量配置(跨平台):
bash
# Linux/macOS (.bashrc 或 .zshrc)
export JAVA_HOME=$(/usr/libexec/java_home -v 17) # macOS
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk # Linux
export PATH=$JAVA_HOME/bin:$PATH
# Windows (PowerShell)
[Environment]::SetEnvironmentVariable("JAVA_HOME", "D:\Java\jdk-17", "User")
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
Maven 项目结构详解
demo/
├─ pom.xml # 项目配置、依赖管理
├─ src/
│ ├─ main/
│ │ ├─ java/ # 源代码(包结构:com/company/module/)
│ │ │ └─ com/example/App.java
│ │ └─ resources/ # 资源文件(配置文件、模板等)
│ │ ├─ application.yml
│ │ └─ logback.xml
│ └─ test/
│ ├─ java/ # 测试代码
│ └─ resources/ # 测试资源
├─ target/ # 编译输出(.gitignore)
└─ .gitignore
pom.xml 核心配置示例:
xml
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- 依赖声明 -->
</dependencies>
</project>
语言语法速览
数据类型深度解析
基本类型与包装类型对比:
| 基本类型 | 大小 | 包装类型 | 默认值 | 缓存范围 |
|---|---|---|---|---|
byte |
1字节 | Byte |
0 | -128~127 |
short |
2字节 | Short |
0 | -128~127 |
int |
4字节 | Integer |
0 | -128~127 |
long |
8字节 | Long |
0L | -128~127 |
float |
4字节 | Float |
0.0f | 无 |
double |
8字节 | Double |
0.0d | 无 |
char |
2字节 | Character |
'\u0000' | 0~127 |
boolean |
1位 | Boolean |
false | true/false |
源码分析:Integer 缓存机制:
java
// java.lang.Integer 源码片段
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
// 默认缓存范围:-128 到 127
// 可通过 -XX:AutoBoxCacheMax=<size> 调整上限
实际应用示例:
java
Integer a = 100; // 自动装箱,使用缓存
Integer b = 100;
System.out.println(a == b); // true(同一对象)
Integer c = 200; // 超出缓存范围
Integer d = 200;
System.out.println(c == d); // false(不同对象)
System.out.println(c.equals(d)); // true(值相等)
字符串不可变性源码分析
String 类的核心设计:
java
// java.lang.String 源码关键部分
public final class String implements java.io.Serializable,
Comparable<String>,
CharSequence {
private final char[] value; // JDK 8 及之前
// JDK 9+ 改为 byte[] + coder (Latin1/UTF16)
private final int hash; // 缓存 hashCode
// 构造器确保 value 数组不可变
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}
不可变性的优势:
- 线程安全:无需同步即可多线程共享
- 缓存优化:字符串常量池(String Pool)复用相同字符串
- 安全性:作为参数传递时不会被修改(如 ClassLoader、网络连接)
StringBuilder vs StringBuffer 源码对比:
java
// StringBuilder (非线程安全,性能更高)
public final class StringBuilder extends AbstractStringBuilder {
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
// 方法未加 synchronized
}
// StringBuffer (线程安全,性能较低)
public final class StringBuffer extends AbstractStringBuilder {
@Override
public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
// 所有方法都加 synchronized
}
性能测试示例:
java
// 错误示例:频繁字符串拼接
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 每次创建新 String 对象,O(n²) 复杂度
}
// 正确示例:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i); // O(n) 复杂度
}
String result = sb.toString();
Switch 表达式演进(JDK 14+)
传统 switch 语句:
java
int day = 1;
String dayName;
switch (day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
default:
dayName = "Unknown";
}
Switch 表达式(JDK 14+):
java
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3, 4, 5 -> "Weekday"; // 多值匹配
default -> "Unknown";
};
// 使用 yield 处理复杂逻辑
int result = switch (day) {
case 1, 2, 3 -> {
System.out.println("Processing...");
yield day * 2; // yield 返回值
}
default -> day;
};
模式匹配(JDK 17+ 预览,JDK 21 正式):
java
Object obj = "Hello";
String result = switch (obj) {
case String s when s.length() > 5 -> "Long string: " + s;
case String s -> "Short string: " + s;
case Integer i -> "Number: " + i;
case null -> "Null value";
default -> "Unknown";
};
常用 API 深度解析
时间日期 API(java.time 包)
为什么需要新的时间 API?
Date和Calendar设计缺陷:可变性、线程不安全、API 混乱java.time包(JDK 8+)提供不可变、线程安全的时间处理
核心类层次:
java
// 本地日期时间(无时区)
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
// 带时区的日期时间
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 时间戳(UTC)
Instant instant = Instant.now();
// 时间间隔
Duration duration = Duration.between(start, end);
Period period = Period.between(startDate, endDate);
实战示例:时区转换:
java
// 北京时间转纽约时间
LocalDateTime beijingTime = LocalDateTime.of(2024, 1, 1, 12, 0);
ZonedDateTime beijing = beijingTime.atZone(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = beijing.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(newYork); // 2024-01-01T00:00-05:00[America/New_York]
异常体系源码分析
异常类层次结构:
java
Throwable (可抛出)
├── Error (错误,不应捕获)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
└── Exception (异常,可处理)
├── RuntimeException (非检查异常)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ └── IndexOutOfBoundsException
└── 其他 Exception (检查异常)
├── IOException
├── SQLException
└── ClassNotFoundException
try-with-resources 实现原理:
java
// 源代码
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 使用资源
}
// 编译器生成的等价代码
FileInputStream fis = new FileInputStream("file.txt");
Throwable primaryException = null;
try {
// 使用资源
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (fis != null) {
if (primaryException != null) {
try {
fis.close();
} catch (Throwable suppressedException) {
primaryException.addSuppressed(suppressedException);
}
} else {
fis.close();
}
}
}
自定义异常最佳实践:
java
// 业务异常基类
public class BizException extends RuntimeException {
private final String errorCode;
private final Object[] args;
public BizException(String errorCode, String message, Object... args) {
super(message);
this.errorCode = errorCode;
this.args = args;
}
// 提供错误码,便于前端展示
public String getErrorCode() {
return errorCode;
}
}
// 使用示例
if (user == null) {
throw new BizException("USER_NOT_FOUND", "用户不存在: %s", userId);
}
源码讲解|深度剖析
示例 1:命令行参数解析与配置加载(生产级实现)
基础版本:
java
public final class App {
public static void main(String[] args) {
String profile = args.length > 0 ? args[0] : "dev";
System.out.printf("Profile = %s%n", profile);
Properties props = new Properties();
try (InputStream in = App.class.getResourceAsStream("/app-" + profile + ".properties")) {
props.load(in);
System.out.println("DB URL: " + props.getProperty("db.url"));
} catch (IOException e) {
throw new IllegalStateException("加载配置失败", e);
}
}
}
增强版本(支持环境变量覆盖、类型转换):
java
public class ConfigLoader {
private final Properties props = new Properties();
public ConfigLoader(String profile) {
loadFromClasspath(profile);
overrideWithEnvVars(); // 环境变量优先级更高
}
private void loadFromClasspath(String profile) {
String resource = "/app-" + profile + ".properties";
try (InputStream in = getClass().getResourceAsStream(resource)) {
if (in == null) {
throw new IllegalStateException("配置文件不存在: " + resource);
}
props.load(in);
} catch (IOException e) {
throw new IllegalStateException("加载配置失败", e);
}
}
private void overrideWithEnvVars() {
// 环境变量格式:APP_DB_URL -> db.url
props.stringPropertyNames().forEach(key -> {
String envKey = "APP_" + key.replace(".", "_").toUpperCase();
String envValue = System.getenv(envKey);
if (envValue != null) {
props.setProperty(key, envValue);
}
});
}
// 类型安全的配置读取
public String getString(String key, String defaultValue) {
return props.getProperty(key, defaultValue);
}
public int getInt(String key, int defaultValue) {
String value = props.getProperty(key);
return value != null ? Integer.parseInt(value) : defaultValue;
}
public boolean getBoolean(String key, boolean defaultValue) {
String value = props.getProperty(key);
return value != null ? Boolean.parseBoolean(value) : defaultValue;
}
}
源码分析:Properties.load() 实现原理:
java
// java.util.Properties 源码片段
public synchronized void load(InputStream inStream) throws IOException {
load(new InputStreamReader(inStream, "ISO-8859-1")); // 注意编码
// 实际使用建议:使用 loadFromXML() 或手动指定 UTF-8
}
// 推荐使用方式(JDK 9+)
Properties props = new Properties();
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
props.load(reader);
}
Spring Boot 读取 YAML 配置文件原理:
Spring Boot 使用 YamlPropertiesFactoryBean 和 YamlMapFactoryBean 来解析 YAML 文件,底层依赖 SnakeYAML 库。
核心实现机制:
java
// Spring Boot 配置加载流程
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String dbUrl;
private int dbPort;
private List<String> servers;
// getter/setter
}
// application.yml
app:
db-url: jdbc:mysql://localhost:3306/test
db-port: 3306
servers:
- server1.example.com
- server2.example.com
Spring Boot YAML 加载源码分析:
java
// org.springframework.boot.env.YamlPropertySourceLoader
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
throw new IllegalStateException("SnakeYAML not found");
}
Yaml yaml = createYaml();
try (InputStream inputStream = resource.getInputStream()) {
// 解析 YAML 为 Map
List<Map<String, Object>> loaded = yaml.load(inputStream);
return loaded.stream()
.map(map -> new MapPropertySource(name, flattenMap(map)))
.collect(Collectors.toList());
}
}
// 扁平化嵌套 Map(app.db.url -> app.db.url)
private Map<String, Object> flattenMap(Map<String, Object> source) {
Map<String, Object> result = new LinkedHashMap<>();
buildFlattenedMap(result, source, null);
return result;
}
}
实际应用示例:
java
// 方式1:使用 @ConfigurationProperties(推荐)
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private Database database = new Database();
private Redis redis = new Redis();
@Data
public static class Database {
private String url;
private String username;
private String password;
}
@Data
public static class Redis {
private String host;
private int port;
private int timeout;
}
}
// 方式2:使用 @Value 注解
@Component
public class AppConfig {
@Value("${app.database.url}")
private String dbUrl;
@Value("${app.database.port:3306}") // 默认值
private int dbPort;
@Value("${app.servers}")
private List<String> servers;
}
// 方式3:使用 Environment
@Autowired
private Environment env;
public void loadConfig() {
String dbUrl = env.getProperty("app.database.url");
int dbPort = env.getProperty("app.database.port", Integer.class, 3306);
}
YAML vs Properties 对比:
| 特性 | YAML | Properties |
|---|---|---|
| 可读性 | 高(层次清晰) | 中(扁平结构) |
| 嵌套支持 | 原生支持 | 需用点号分隔 |
| 类型支持 | 丰富(List、Map) | 字符串为主 |
| 注释 | 支持 | 支持 |
| Spring Boot | 默认支持 | 默认支持 |
多环境配置(Profile):
yaml
# application.yml(默认配置)
app:
database:
url: jdbc:mysql://localhost:3306/dev
---
# application-prod.yml(生产环境)
spring:
profiles: prod
app:
database:
url: jdbc:mysql://prod-server:3306/prod
配置优先级(从高到低):
- 命令行参数:
--app.database.url=xxx - 环境变量:
APP_DATABASE_URL=xxx application-{profile}.ymlapplication.yml- 默认值
示例 2:不可变对象设计模式
基础 POJO:
java
public class User {
private final String id;
private final String name;
private int age; // 可变字段
public User(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getDisplay() {
return String.format("[%s] %s(%d)", id, name, age);
}
}
完全不可变对象(推荐):
java
// 使用 final 类 + final 字段,确保不可变
public final class ImmutableUser {
private final String id;
private final String name;
private final int age;
private final List<String> tags; // 集合也需要不可变
public ImmutableUser(String id, String name, int age, List<String> tags) {
this.id = Objects.requireNonNull(id, "id不能为空");
this.name = Objects.requireNonNull(name, "name不能为空");
this.age = age;
// 防御性拷贝,防止外部修改
this.tags = Collections.unmodifiableList(new ArrayList<>(tags));
}
// 只提供 getter,不提供 setter
public String getId() { return id; }
public String getName() { return name; }
public int getAge() { return age; }
public List<String> getTags() {
// 返回不可变视图
return tags;
}
// 提供 withXxx 方法创建新对象(函数式风格)
public ImmutableUser withAge(int newAge) {
return new ImmutableUser(this.id, this.name, newAge, this.tags);
}
}
使用 Record 类型(JDK 14+,更简洁):
java
// Record 自动生成 final 字段、构造器、equals、hashCode、toString
public record UserRecord(String id, String name, int age, List<String> tags) {
// 紧凑构造器,可添加验证逻辑
public UserRecord {
Objects.requireNonNull(id);
Objects.requireNonNull(name);
tags = List.copyOf(tags); // 创建不可变副本
}
// 自定义方法
public String getDisplay() {
return String.format("[%s] %s(%d)", id, name, age);
}
}
Builder 模式(复杂对象构造):
java
public final class User {
private final String id;
private final String name;
private final int age;
private final String email;
private final List<String> roles;
private User(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
this.roles = Collections.unmodifiableList(builder.roles);
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String id;
private String name;
private int age;
private String email;
private List<String> roles = new ArrayList<>();
public Builder id(String id) {
this.id = id;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder addRole(String role) {
this.roles.add(role);
return this;
}
public User build() {
// 构建时验证
Objects.requireNonNull(id, "id不能为空");
Objects.requireNonNull(name, "name不能为空");
if (age < 0) {
throw new IllegalArgumentException("age必须大于0");
}
return new User(this);
}
}
}
// 使用示例
User user = User.builder()
.id("u001")
.name("Alice")
.age(25)
.email("alice@example.com")
.addRole("USER")
.addRole("ADMIN")
.build();
实战案例
案例 1:命令行 Todo 管理器(完整实现)
需求分析:
- 支持添加、完成、列出、删除任务
- 数据持久化到 JSON 文件
- 支持任务优先级和分类
完整实现代码:
java
// Task 实体类(使用 Record)
public record Task(String id, String description, boolean completed,
Priority priority, LocalDateTime createdAt) {
public enum Priority { LOW, MEDIUM, HIGH }
public Task {
Objects.requireNonNull(id);
Objects.requireNonNull(description);
if (description.isBlank()) {
throw new IllegalArgumentException("描述不能为空");
}
}
public Task complete() {
return new Task(id, description, true, priority, createdAt);
}
public String format() {
String status = completed ? "[✓]" : "[ ]";
return String.format("%s %s (%s) - %s",
status, description, priority,
createdAt.format(DateTimeFormatter.ISO_LOCAL_DATE));
}
}
// 任务管理器
public class TodoManager {
private final Path dataFile;
private final ObjectMapper mapper;
private List<Task> tasks;
public TodoManager(Path dataFile) {
this.dataFile = dataFile;
this.mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
loadTasks();
}
private void loadTasks() {
try {
if (Files.exists(dataFile)) {
String json = Files.readString(dataFile, StandardCharsets.UTF_8);
tasks = mapper.readValue(json,
mapper.getTypeFactory().constructCollectionType(List.class, Task.class));
} else {
tasks = new ArrayList<>();
}
} catch (IOException e) {
throw new IllegalStateException("加载任务失败", e);
}
}
private void saveTasks() {
try {
String json = mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(tasks);
Files.writeString(dataFile, json, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalStateException("保存任务失败", e);
}
}
public void addTask(String description, Task.Priority priority) {
String id = UUID.randomUUID().toString();
Task task = new Task(id, description, false, priority, LocalDateTime.now());
tasks.add(task);
saveTasks();
System.out.println("✓ 任务已添加: " + task.format());
}
public void completeTask(String id) {
tasks = tasks.stream()
.map(task -> task.id().equals(id) ? task.complete() : task)
.collect(Collectors.toList());
saveTasks();
System.out.println("✓ 任务已完成: " + id);
}
public void listTasks(boolean showCompleted) {
tasks.stream()
.filter(task -> showCompleted || !task.completed())
.forEach(task -> System.out.println(task.format()));
}
public void deleteTask(String id) {
boolean removed = tasks.removeIf(task -> task.id().equals(id));
if (removed) {
saveTasks();
System.out.println("✓ 任务已删除: " + id);
} else {
System.out.println("✗ 任务不存在: " + id);
}
}
}
// 主程序
public class TodoApp {
public static void main(String[] args) {
if (args.length == 0) {
printUsage();
return;
}
Path dataFile = Paths.get(System.getProperty("user.home"), ".todo.json");
TodoManager manager = new TodoManager(dataFile);
String command = args[0];
switch (command) {
case "add" -> {
if (args.length < 2) {
System.err.println("错误: 请提供任务描述");
return;
}
String description = String.join(" ",
Arrays.copyOfRange(args, 1, args.length));
Task.Priority priority = args.length > 2 &&
args[args.length - 1].matches("LOW|MEDIUM|HIGH")
? Task.Priority.valueOf(args[args.length - 1])
: Task.Priority.MEDIUM;
manager.addTask(description, priority);
}
case "done" -> {
if (args.length < 2) {
System.err.println("错误: 请提供任务ID");
return;
}
manager.completeTask(args[1]);
}
case "list" -> {
boolean showAll = args.length > 1 && args[1].equals("--all");
manager.listTasks(showAll);
}
case "delete" -> {
if (args.length < 2) {
System.err.println("错误: 请提供任务ID");
return;
}
manager.deleteTask(args[1]);
}
default -> {
System.err.println("未知命令: " + command);
printUsage();
}
}
}
private static void printUsage() {
System.out.println("""
用法: todo <command> [args]
命令:
add <description> [PRIORITY] 添加任务 (PRIORITY: LOW/MEDIUM/HIGH)
done <id> 完成任务
list [--all] 列出任务 (--all 显示已完成)
delete <id> 删除任务
""");
}
}
技术要点:
- 使用 Record 简化不可变对象:自动生成 equals、hashCode、toString
- Jackson 序列化:处理 LocalDateTime 和集合类型
- 防御性编程:参数校验、异常处理
- 函数式风格:使用 Stream API 处理集合
案例 2:配置文件热加载(生产级实现)
需求:应用运行时动态加载配置文件变更,无需重启
java
public class HotReloadConfig {
private final Path configFile;
private final Properties props = new Properties();
private volatile long lastModified;
private final ScheduledExecutorService scheduler;
public HotReloadConfig(Path configFile) {
this.configFile = configFile;
this.scheduler = Executors.newScheduledThreadPool(1,
r -> {
Thread t = new Thread(r, "config-reloader");
t.setDaemon(true);
return t;
});
loadConfig();
startWatcher();
}
private void loadConfig() {
try {
if (Files.exists(configFile)) {
long currentModified = Files.getLastModifiedTime(configFile).toMillis();
if (currentModified != lastModified) {
synchronized (props) {
try (Reader reader = Files.newBufferedReader(configFile,
StandardCharsets.UTF_8)) {
props.load(reader);
lastModified = currentModified;
System.out.println("配置已重新加载: " + configFile);
}
}
}
}
} catch (IOException e) {
System.err.println("加载配置失败: " + e.getMessage());
}
}
private void startWatcher() {
scheduler.scheduleWithFixedDelay(this::loadConfig, 5, 5, TimeUnit.SECONDS);
}
public String getProperty(String key, String defaultValue) {
synchronized (props) {
return props.getProperty(key, defaultValue);
}
}
public void shutdown() {
scheduler.shutdown();
}
}
Maven 热启动部署插件实战:
在开发过程中,频繁重启应用会浪费大量时间。Maven 提供了多个热启动插件,可以在代码变更后自动重新编译和部署。
1. Spring Boot DevTools(推荐):
Spring Boot DevTools 提供了自动重启、LiveReload 等功能。
xml
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
工作原理:
- 类加载器分离 :DevTools 使用两个类加载器
base classloader:加载不常变的类(第三方库)restart classloader:加载应用代码
- 文件监控 :监控
classpath下的文件变更 - 自动重启 :检测到变更后,只重启
restart classloader,速度更快
配置示例:
yaml
# application.yml
spring:
devtools:
restart:
enabled: true
# 排除不需要重启的资源
exclude: static/**,public/**,templates/**
# 监控额外路径
additional-paths: src/main/java
livereload:
enabled: true # 启用 LiveReload(浏览器自动刷新)
2. JRebel(商业插件,性能最佳):
JRebel 是 ZeroTurnaround 开发的商业热部署工具,无需重启即可加载类变更。
xml
<!-- pom.xml -->
<plugin>
<groupId>org.zeroturnaround</groupId>
<artifactId>jrebel-maven-plugin</artifactId>
<version>1.2.0</version>
<executions>
<execution>
<id>generate-rebel-xml</id>
<phase>process-resources</phase>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
启动方式:
bash
# 使用 JRebel 启动
java -agentpath:/path/to/jrebel/lib/jrebel64.so -jar app.jar
3. DCEVM + HotswapAgent(开源替代方案):
DCEVM(Dynamic Code Evolution VM)是 JVM 的修改版本,支持更强大的热替换。
xml
<!-- pom.xml -->
<plugin>
<groupId>org.hotswap.agent</groupId>
<artifactId>hotswap-agent-maven-plugin</artifactId>
<version>1.4.0</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
4. Maven 插件:spring-boot-maven-plugin(开发模式):
xml
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
<!-- 开发模式:支持热部署 -->
<jvmArguments>
-XX:+UseG1GC
-Dspring.devtools.restart.enabled=true
</jvmArguments>
</configuration>
</plugin>
启动命令:
bash
# 开发模式启动(支持热部署)
mvn spring-boot:run
# 或使用 IDE 的 Run Configuration
5. 综合对比:
| 工具 | 类型 | 性能 | 成本 | 适用场景 |
|---|---|---|---|---|
| Spring Boot DevTools | 开源 | 中等(需重启) | 免费 | Spring Boot 项目 |
| JRebel | 商业 | 最佳(无需重启) | 付费 | 大型项目、生产环境 |
| DCEVM + HotswapAgent | 开源 | 高(需重启 JVM) | 免费 | 需要深度热替换 |
| Maven 插件 | 开源 | 低(完整重启) | 免费 | 简单项目 |
最佳实践:
java
// 1. 开发环境使用 DevTools
// application-dev.yml
spring:
devtools:
restart:
enabled: true
// 2. 生产环境禁用
// application-prod.yml
spring:
devtools:
restart:
enabled: false
// 3. 使用条件注解
@ConditionalOnProperty(
name = "spring.devtools.restart.enabled",
havingValue = "true",
matchIfMissing = true
)
@Configuration
public class DevToolsConfig {
// DevTools 配置
}
热部署限制:
- 不支持:修改方法签名、添加/删除字段、修改类继承关系
- 支持:修改方法体、添加新方法、修改注解
- 建议:重大变更时仍需要完整重启
应用场景
- 微服务配置管理 :基于
Properties/YAML加载环境配置,结合Optional处理缺省值。 - CLI 工具/脚本替代:利用标准输入输出、文件 API 构建自动化脚本(如发布、巡检)。
- 数据清洗/批处理 :使用
List+Stream快速实现 CSV 解析、过滤、聚合。 - 运行时参数校验 :
Objects.requireNonNull、Pattern正则,保障输入合法性。 - 日志审计 :
java.util.logging或 SLF4J 快速接入,演示LoggerFactory.getLogger。
高频面试问答(深度解析)
1. JDK、JRE、JVM 的区别和关系?
标准答案:
- JVM (Java Virtual Machine):Java 虚拟机,负责执行字节码,管理内存(堆、栈、方法区),垃圾回收等
- JRE (Java Runtime Environment):Java 运行时环境 = JVM + 核心类库(rt.jar 等)
- JDK (Java Development Kit):Java 开发工具包 = JRE + 开发工具(javac、javap、jconsole 等)
深入追问与回答思路:
Q: JVM 有哪些实现?
- HotSpot:Oracle/OpenJDK 默认,使用 JIT 编译优化
- GraalVM:高性能多语言运行时,支持 AOT 编译
- Eclipse OpenJ9:IBM 贡献,内存占用更小
- Zing:Azul 商业版,低延迟 GC
Q: 为什么需要 JRE?不能直接用 JVM 吗?
- JVM 只负责执行字节码,但程序需要调用
java.lang.*、java.util.*等核心类库 - JRE 提供了这些标准库的实现,如
String、ArrayList、HashMap等
Q: 生产环境应该用 JDK 还是 JRE?
- 推荐使用 JRE 或自定义运行时 (通过
jlink创建) - 原因:体积更小、安全性更高(不包含开发工具)
- 示例:
jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output minimal-jre
2. String 为什么设计成不可变的?
标准答案:
- 安全性:作为参数传递时不会被修改(如 ClassLoader、网络连接)
- 线程安全:多线程环境下可安全共享,无需同步
- 缓存优化:字符串常量池(String Pool)可复用相同字符串,节省内存
- HashCode 缓存 :
hash字段缓存 hashCode,提升 HashMap 等集合性能
源码分析:
java
// JDK 8 及之前
public final class String {
private final char[] value; // final 确保引用不可变
private int hash; // 缓存 hashCode
// 构造器创建新数组,不直接引用外部数组
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}
// JDK 9+ 优化:使用 byte[] + coder
public final class String {
private final byte[] value; // Latin1 或 UTF-16
private final byte coder; // 0=Latin1, 1=UTF16
}
深入追问与回答思路:
Q: String 不可变会带来性能问题吗?
-
会:频繁字符串拼接会创建大量临时对象
-
解决方案 :使用
StringBuilder或StringBuffer -
性能对比 :
java// 错误:O(n²) 复杂度 String s = ""; for (int i = 0; i < 10000; i++) { s += i; // 每次创建新对象 } // 正确:O(n) 复杂度 StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(i); } String s = sb.toString();
Q: String.intern() 的作用和风险?
- 作用:将字符串放入常量池,相同内容的字符串返回同一引用
- 风险 :
- JDK 6 及之前:常量池在永久代,可能导致 OOM
- JDK 7+:常量池在堆中,但仍需谨慎使用
- 使用场景:大量重复字符串的场景(如日志解析)
Q: 如何实现可变字符串?
- 使用
StringBuilder(非线程安全,性能更高) - 使用
StringBuffer(线程安全,性能较低) - 或使用
char[]数组手动管理
3. Switch 表达式支持哪些类型?有什么新特性?
标准答案:
- JDK 7 之前 :只支持
byte、short、int、char、枚举 - JDK 7+ :支持
String(通过hashCode()和equals()比较) - JDK 14+ :支持
switch表达式,使用->语法,支持yield返回值 - JDK 17+:支持模式匹配(预览)
- JDK 21+:模式匹配正式版
深入追问与回答思路:
Q: Switch 为什么不能支持 long、float、double?
- 技术原因:Switch 使用跳转表(Jump Table)实现,需要连续整数值
long、float、double范围太大,无法构建跳转表- 替代方案 :使用
if-else或Map<Long, Runnable>映射
Q: Switch 的跳转表(Jump Table)实现原理?
跳转表(Jump Table)机制:
Switch 语句在编译时会根据 case 值的分布情况选择不同的实现策略:
- 跳转表(Table Switch):当 case 值连续或接近连续时使用
- 查找表(Lookup Switch):当 case 值稀疏时使用
Table Switch 实现原理:
java
// 源代码
int day = 3;
String dayName;
switch (day) {
case 1: dayName = "Monday"; break;
case 2: dayName = "Tuesday"; break;
case 3: dayName = "Wednesday"; break;
case 4: dayName = "Thursday"; break;
case 5: dayName = "Friday"; break;
default: dayName = "Unknown";
}
// 编译器生成的字节码(伪代码)
int min = 1, max = 5;
if (day < min || day > max) {
goto default_label;
}
// 跳转表:直接通过索引跳转,O(1) 时间复杂度
jump_table[day - min] -> {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday"
}
Lookup Switch 实现原理:
java
// 源代码(case 值稀疏)
int value = 100;
switch (value) {
case 1: break;
case 100: break;
case 1000: break;
default: break;
}
// 编译器生成的字节码(伪代码)
// 使用二分查找,O(log n) 时间复杂度
lookup_table = [
{key: 1, offset: label1},
{key: 100, offset: label100},
{key: 1000, offset: label1000}
]
binary_search(lookup_table, value);
查看字节码验证:
bash
# 编译 Java 文件
javac SwitchExample.java
# 查看字节码
javap -c SwitchExample
# 输出示例
public static void testSwitch(int);
Code:
0: iload_0
1: tableswitch { // Table Switch
1: 28
2: 35
3: 42
default: 49
}
28: ldc #2 // String "Monday"
30: astore_1
31: goto 56
...
模式匹配中的跳转表使用:
JDK 17+ 的模式匹配(Pattern Matching)在编译时也会使用跳转表优化。
java
// 模式匹配示例
Object obj = "Hello";
String result = switch (obj) {
case String s when s.length() > 5 -> "Long: " + s;
case String s -> "Short: " + s;
case Integer i -> "Number: " + i;
case null -> "Null";
default -> "Unknown";
};
// 编译器优化策略:
// 1. 类型判断:使用 instanceof 检查
// 2. 条件判断:when 子句编译为 if 条件
// 3. 跳转表:相同类型的 case 可能使用跳转表优化
模式匹配编译优化示例:
java
// 源代码
int value = 5;
String result = switch (value) {
case 1, 2, 3 -> "Small";
case 4, 5, 6 -> "Medium";
case 7, 8, 9 -> "Large";
default -> "Unknown";
};
// 编译器可能生成的优化代码(伪代码)
// 使用跳转表,O(1) 时间复杂度
if (value >= 1 && value <= 9) {
result = jump_table[value - 1]; // 直接索引
} else {
result = "Unknown";
}
性能对比:
| 实现方式 | 时间复杂度 | 适用场景 |
|---|---|---|
| Table Switch | O(1) | case 值连续或接近连续 |
| Lookup Switch | O(log n) | case 值稀疏 |
| if-else 链 | O(n) | 少量分支,编译器可能优化为跳转表 |
| 模式匹配 | O(1) 或 O(log n) | 根据模式复杂度决定 |
实际应用建议:
- 连续整数:使用 switch,编译器会优化为 Table Switch
- 枚举类型:使用 switch,性能最优
- 字符串 :JDK 7+ 支持,使用
hashCode()+equals()比较 - 模式匹配:JDK 17+,类型判断 + 条件判断一体化,性能优秀
Q: Switch 表达式的优势?
java
// 传统 switch(容易忘记 break)
int result;
switch (day) {
case 1:
result = 1;
break; // 容易忘记
case 2:
result = 2;
break;
default:
result = 0;
}
// Switch 表达式(更简洁、安全)
int result = switch (day) {
case 1 -> 1; // 自动 break
case 2 -> 2;
default -> 0;
};
Q: 模式匹配的实际应用?
java
// 类型判断 + 类型转换 + 条件判断一体化
Object obj = getUserInput();
String result = switch (obj) {
case String s when s.length() > 10 -> "长字符串: " + s;
case String s -> "短字符串: " + s;
case Integer i when i > 0 -> "正数: " + i;
case Integer i -> "非正数: " + i;
case null -> "空值";
default -> "未知类型";
};
4. try-with-resources 的实现原理?
标准答案:
- 编译器自动生成
finally块,调用AutoCloseable.close() - 支持多个资源声明,按声明顺序逆序关闭
- 如果关闭时抛出异常,会作为抑制异常(Suppressed Exception)附加到主异常
源码级分析:
java
// 源代码
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
// 使用资源
}
// 编译器生成的等价代码(简化版)
FileInputStream fis = new FileInputStream("file.txt");
Throwable primaryException = null;
try {
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
Throwable secondaryException = null;
try {
// 使用资源
} catch (Throwable t) {
secondaryException = t;
throw t;
} finally {
if (br != null) {
if (secondaryException != null) {
try {
br.close();
} catch (Throwable suppressed) {
secondaryException.addSuppressed(suppressed);
}
} else {
br.close();
}
}
}
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (fis != null) {
if (primaryException != null) {
try {
fis.close();
} catch (Throwable suppressed) {
primaryException.addSuppressed(suppressed);
}
} else {
fis.close();
}
}
}
深入追问与回答思路:
Q: 如何自定义资源类?
java
public class DatabaseConnection implements AutoCloseable {
private Connection conn;
public DatabaseConnection(String url) throws SQLException {
this.conn = DriverManager.getConnection(url);
}
@Override
public void close() throws SQLException {
if (conn != null && !conn.isClosed()) {
conn.close();
System.out.println("连接已关闭");
}
}
public Connection getConnection() {
return conn;
}
}
// 使用
try (DatabaseConnection db = new DatabaseConnection("jdbc:mysql://...")) {
// 使用数据库连接
} // 自动关闭,无需手动调用 close()
Q: 如果 close() 抛出异常会怎样?
- 如果主代码块正常执行,
close()的异常会正常抛出 - 如果主代码块已抛出异常,
close()的异常会作为抑制异常附加 - 可通过
Throwable.getSuppressed()获取所有抑制异常
5. ArrayList 与 LinkedList 的差异?
标准答案:
- 底层结构 :
ArrayList基于动态数组,LinkedList基于双向链表 - 随机访问 :
ArrayListO(1),LinkedListO(n) - 插入删除 :
ArrayList平均 O(n),LinkedListO(1)(已知位置) - 内存占用 :
ArrayList更紧凑,LinkedList需要额外存储前后指针
源码分析:
java
// ArrayList 扩容机制
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
}
elementData = Arrays.copyOf(elementData, newCapacity);
}
// LinkedList 节点结构
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
深入追问与回答思路:
Q: 什么时候用 ArrayList,什么时候用 LinkedList?
- ArrayList 适用 :
- 频繁随机访问(如
get(index)) - 主要在尾部添加元素
- 内存敏感场景
- 频繁随机访问(如
- LinkedList 适用 :
- 频繁在中间插入/删除
- 需要实现队列/双端队列(
Deque) - 元素数量变化大,避免频繁扩容
Q: ArrayList 的扩容策略为什么是 1.5 倍?
- 平衡内存和性能 :
- 太小(如 1.1 倍):频繁扩容,性能差
- 太大(如 2 倍):浪费内存
- 1.5 倍是经验值,在大多数场景下表现良好
Q: LinkedList 真的比 ArrayList 快吗?
- 不一定 :
- 虽然插入删除是 O(1),但需要先定位到位置,定位是 O(n)
- 实际测试中,
ArrayList由于缓存友好性,在小数据量时可能更快 - 建议:根据实际场景做性能测试,不要盲目选择
6. 如何选择 JDK 版本?
标准答案:
- 生产环境首选 LTS 版本:JDK 8、11、17、21、25
- 评估因素 :
- 依赖库兼容性(Spring、MyBatis 等)
- 性能收益(GC 改进、虚拟线程等)
- 新特性需求(Record、Pattern Matching 等)
- 团队技术栈和迁移成本
版本选择建议:
| 版本 | 适用场景 | 迁移难度 |
|---|---|---|
| JDK 8 | 稳定、生态成熟,适合保守项目 | 低 |
| JDK 11 | 首个 LTS,云原生优化 | 中 |
| JDK 17 | 推荐升级目标,特性丰富 | 中 |
| JDK 21 | LTS,虚拟线程等新特性 | 高(需评估兼容性) |
| JDK 25 | 最新 LTS,最新语言特性 | 高(需充分测试) |
深入追问与回答思路:
Q: 如何评估迁移风险?
-
使用
jdeps分析依赖 :bashjdeps --multi-release 17 --class-path . your-app.jar -
使用
jdeprscan检查已弃用 API :bashjdeprscan your-app.jar -
在测试环境充分验证:功能测试、性能测试、压力测试
Q: 虚拟线程(JDK 21)适合哪些场景?
- 适合:IO 密集型应用(网络请求、数据库查询)
- 不适合:CPU 密集型任务、需要线程亲和的场景
- 示例:微服务网关、API 聚合服务
Q: 如何平滑升级?
- 双版本并行:新功能用新版本,旧功能保持旧版本
- 灰度发布:先升级部分服务,观察稳定性
- 回滚方案:保留旧版本镜像,随时回滚
- 监控告警:关注 GC、线程、内存等指标
延伸阅读
- 《Effective Java》第 3 版:条款 1-20 聚焦语言核心。
- 官方文档:OpenJDK Language Guide、Java Tutorials。
- 工具链:SDKMAN!、JEnv,便于多版本 JDK 切换。
- 推荐实践:建立 GitHub Template Repo,包含基础代码、构建脚本、CI 工作流,便于快速起步。