Java 进阶编程实战
本文聚焦 Java 进阶编程实战,系统讲解异常处理、File 类、递归、网络编程、枚举、反射和注解等核心能力。文章结合原理说明与可运行示例,帮助读者从基础语法过渡到更接近真实项目的 Java 编程能力,并理解复杂特性背后的设计思路。
第一章 异常处理:程序的“安全带”
1.1 异常家族的族谱
Java 把所有程序运行中可能出现的不正常情况设计成一套继承体系:
Throwable(祖先)Error:系统级的灾难(JVM 内存溢出、栈溢出),普通程序处理不了,也不该处理。Exception:我们写代码时应该处理的异常。- 运行时异常(
RuntimeException):编译时不要求强制处理,比如空指针、索引越界。这通常说明代码逻辑有 Bug。 - 编译时异常(非 Runtime):如文件找不到、网络断开。编译器强制开发者提前写出处理方案,因为这些问题很可能发生。
1.2 处理异常的方式
- 自己处理(try-catch-finally):try 里面放可能有问题的代码,catch 捕获指定类型的异常并处理,finally 中的代码无论是否出异常都会执行,适合关闭资源(文件流、数据库连接等)。
try {
int[] arr = {1,2,3};
System.out.println(arr[5]); // 可能发生索引越界
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界了!");
e.printStackTrace(); // 打印异常信息,方便调试
} finally {
System.out.println("这一段一定会执行");
}
// 程序可以继续运行
- 抛出给调用者(throws):方法头声明该异常,自己不处理,扔给调用方。调用方必须继续处理或者继续往上抛。main 方法如果抛出异常,程序会终止。
JDK 7+ 推荐:try-with-resources 自动关闭实现了 AutoCloseable 的资源:
try (FileInputStream in = new FileInputStream("a.txt")) {
// 使用流
} catch (IOException e) {
e.printStackTrace();
}
// 无需手写 finally 关闭
调试建议:IDEA 中对断点行点击左侧,使用 Debug 而非 Run,可单步查看变量、调用栈,快速定位逻辑错误与异常来源。
1.3 自定义异常
当 JDK 提供的异常类无法精确表达你项目中的业务错误时,可以自定义异常。通常继承 Exception(编译时异常)或 RuntimeException(运行时异常)。
class NoMoneyException extends Exception {
public NoMoneyException(String message) { super(message); }
}
// 在取款方法中使用
public void withdraw(double amount) throws NoMoneyException {
if (balance < amount) {
throw new NoMoneyException("余额不足,还差 " + (amount - balance) + " 元");
}
// ...
}
第二章 File 类与递归
2.1 File 只是路径描述,不是真实文件
File 对象是一个文件或目录路径的抽象表示。
- 路径分类:
- 绝对路径:从盘符开始的完整路径,如
D:\test\a.txt。 - 相对路径:相对于当前项目的根目录,如
src\Main.java。 - 跨平台分隔符:Windows 使用
\,Linux 使用/。Java 提供了File.separator自动根据系统切换。
创建 File 对象时,文件可能根本不存在,它只是帮你封装了路径。你必须调用 exists() 等方法去确认。
import java.io.File;
File file = new File("D:/test/a.txt"); // 创建 File 对象,指定路径
System.out.println(file.exists()); // 判断文件或目录是否存在
System.out.println(file.isFile()); // 判断是否为文件
System.out.println(file.isDirectory()); // 判断是否为目录
System.out.println(file.getName()); // 获取文件名(带后缀)
System.out.println(file.length()); // 获取文件大小(字节数)
System.out.println(file.getAbsolutePath()); // 获取绝对路径
System.out.println(file.lastModified()); // 获取最后修改时间(毫秒值)
2.2 关键方法
- 创建功能:
createNewFile():文件不存在则创建,返回 true;存在则不创建,返回 false。mkdir():创建单级目录。mkdirs():创建多级目录(推荐使用)。- 删除功能:
delete():删除文件或空目录。注意:Java 的删除不走回收站,且目录中有内容时无法直接删除。- 遍历功能:
list():获取目录下所有文件和子目录的名称字符串数组。listFiles():获取目录下所有文件和子目录的 File 对象数组(最常用,因为可以直接调用 File 的方法)。
2.3 递归的威力
递归就是方法自己调用自己,把一个复杂的大任务分解成许多小任务。
- 递归的三要素:
- 出口(终止条件):没有出口就是无限死循环,最终导致
StackOverflowError(栈溢出)。 - 规则(递归公式):如何把大问题拆解成小问题。
- 方向:必须向着出口的方向演进。
案例 1:计算阶乘 (n!)
public static int factorial(int n) {
if (n == 1) return 1; // 出口
return n * factorial(n - 1); // 规则:n! = n * (n-1)!
}
案例 2:搜索所有 .java 文件
public static void findJavaFiles(File dir) {
File[] files = dir.listFiles(); // 获取目录下所有内容
if (files != null) {
for (File f : files) {
if (f.isFile() && f.getName().endsWith(".java")) {
System.out.println(f.getAbsolutePath()); // 打印符合条件的文件的绝对路径
} else if (f.isDirectory()) {
findJavaFiles(f); // 递归进入子目录
}
}
}
}
案例 3:计算文件夹总大小
public static long getDirSize(File dir) {
long size = 0;
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
if (f.isFile()) {
size += f.length(); // 如果是文件,累加大小
} else {
size += getDirSize(f); // 如果是目录,递归累加子目录大小
}
}
}
return size;
}
第三章 网络编程
3.1 UDP 与 TCP 的核心区别
- UDP:无连接协议,发送方只管发,不管对方是否收到。效率高但不可靠,常用于视频直播、语音通话。
- TCP:面向连接,必须先握手建立连接,再传输数据,保证可靠送达。就像打电话,必须接通后双方对话。Java 里对应的类是
Socket(客户端)和ServerSocket(服务端)。
3.2 TCP 编程的基本框架
客户端和服务端通过 Socket 对象建立连接,然后双方拿到对方的输入/输出流进行读写通信。
// 服务端
ServerSocket server = new ServerSocket(8888);
Socket client = server.accept(); // 监听连接请求
InputStream in = client.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
System.out.println(new String(buf, 0, len));
client.close();
// 客户端
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
out.write("Hello Server".getBytes());
socket.shutdownOutput(); // 告诉服务端我说完了
socket.close();
第四章 Java 高级技术
4.1 枚举:限定取值范围的常量
当某一类数据只可能有有限的几种值(如季节、星期、订单状态、错误码),用枚举可以避免乱传错误值,保证类型的安全性。
1. 枚举的基本使用 枚举本质上是一个特殊的类(继承自 java.lang.Enum)。
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER;
}
// 使用
Season s = Season.SUMMER;
System.out.println(s.name()); // 输出 "SUMMER"
System.out.println(s.ordinal()); // 输出 1 (声明的顺序,从 0 开始)
2. 进阶用法:带属性、构造器和方法的枚举 这是实际开发中最常用的场景,比如封装 HTTP 状态码或业务错误码:
public enum ErrorCode {
SUCCESS(200, "操作成功"),
NOT_FOUND(404, "资源不存在"),
SERVER_ERROR(500, "服务器内部错误");
// 1. 定义私有属性
private final int code;
private final String message;
// 2. 构造方法必须是私有的(默认也是私有)
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
// 3. 提供 getter 方法供外部读取
public int getCode() { return code; }
public String getMessage() { return message; }
}
// 使用场景
System.out.println(ErrorCode.NOT_FOUND.getCode()); // 输出 404
3. 枚举的常见操作
- 配合 Switch 使用:自 JDK 1.5 起,
switch支持枚举,代码更具可读性。
Season current = Season.SPRING;
switch (current) {
case SPRING:
System.out.println("春暖花开");
break;
case SUMMER:
System.out.println("夏日炎炎");
break;
default:
System.out.println("其他季节");
}
- 遍历所有值:
ErrorCode.values()可以获取枚举的所有实例数组,常用于下拉框数据展示或遍历校验。
for (ErrorCode error : ErrorCode.values()) {
System.out.println(error.name() + " -> " + error.getCode() + ": " + error.getMessage());
}
- 单例模式的最佳实践:《Effective Java》推荐使用枚举来实现单例,不仅能防止多线程同步问题,还能防止反射破坏和反序列化重新创建新对象。
public enum Singleton {
INSTANCE; // 唯一实例
public void doSomething() {
System.out.println("执行单例对象的方法");
}
}
// 调用方式
Singleton.INSTANCE.doSomething();
4.2 类加载器与双亲委派
当我们运行 new Student() 时,JVM 需要把 Student.class 文件加载到内存,这个工作由类加载器完成。Java 采用双亲委派机制:一个类加载器收到加载请求时,不会自己先尝试加载,而是把请求传给父加载器,一直传到最顶层的启动类加载器。只有当父加载器反馈无法加载时,子加载器才会自己加载。这保护了核心类库不会被随意篡改(比如有人自定义了 java.lang.String,但最终加载的还是核心库里的那个)。
4.3 反射:运行时看透一个类的一切
通常我们写代码都是先 new 对象然后再调用方法,这叫“正射”。而反射允许程序在运行时(而不是编译时)动态获取一个类的完整信息(包括私有的属性、方法、构造器),并且可以在运行时创建对象、修改属性、调用方法。这是绝大多数现代 Java 框架(如 Spring、MyBatis)的灵魂基石。
1. 核心入口:获取 Class 对象 一切反射操作的前提是拿到类的“图纸”——Class 对象。有三种常见方式:
Class.forName("全限定类名"):最常用,在只知道类名字符串时(比如读取配置文件)使用。类名.class:在编译期就明确知道要用哪个类时使用,安全且性能高。对象.getClass():已经拿到对象实例时,获取它的 Class 对象。
2. 暴力破解封装:setAccessible(true) 反射的一大特性是可以无视访问权限修饰符(如 private),直接操作类的私有成员。
// 假设 Student 类有一个 private String name; 和 private 的构造方法
Class<?> clazz = Class.forName("com.example.Student");
// 1. 获取并调用私有构造器
// getDeclaredConstructor 可以获取私有构造器,而 getConstructor 只能获取 public 的
Constructor<?> cons = clazz.getDeclaredConstructor(String.class);
cons.setAccessible(true); // 暴力破解权限,否则调用私有构造器会报错
Object stu = cons.newInstance("小明");
// 2. 获取并修改私有属性
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // 暴力破解权限
nameField.set(stu, "大明"); // 将 stu 对象的 name 属性改为“大明”
System.out.println(nameField.get(stu)); // 读取属性值
// 3. 获取并调用私有方法
Method method = clazz.getDeclaredMethod("sayHello");
method.setAccessible(true);
method.invoke(stu); // 执行 stu 对象的 sayHello 方法
*注:getFields() / getMethods() 只能获取 public 成员(包含父类);而带有 Declared 的方法(如 getDeclaredFields())可以获取本类声明的所有成员(包括 private),但不包含父类的。*
3. 反射的优缺点
- 优点:极大地提高了代码的灵活性和扩展性。框架可以通过读取 XML 或注解配置,动态地实例化类并调用方法,开发者无需在代码里硬编码依赖关系。
- 缺点:
- 性能开销:反射涉及动态类型解析,执行速度比直接调用的“正射”慢很多。
- 破坏封装:反射可以随意访问和修改私有成员,可能会引发安全问题或破坏对象的内部状态。因此,在日常业务代码中应谨慎使用反射,它主要用于编写底层框架或通用工具类。
4.4 注解:给代码贴上标签
注解像一个标签,可以贴在类上、方法上、属性上等等。它本身对代码运行没有直接影响,但可以通过反射解析这些标签,赋予特定功能。比如 @Override 告诉编译器检查是否真的重写了父类方法。
自定义注解:
@Target(ElementType.METHOD) // 可以贴在方法上
@Retention(RetentionPolicy.RUNTIME) // 保留到运行期
public @interface MyTest {
int value() default 0;
}
// 使用
@MyTest(value = 10)
public void testMethod() { }
通过反射判断某个方法是否有 @MyTest 注解,并读取其值,就可以实现自己的框架逻辑。
4.5 动态代理:无侵入式增强代码
动态代理是 Java 中非常重要的高级机制,它允许在不修改源码的情况下,在运行时为某个对象生成一个“代理对象”。当我们调用代理对象的方法时,可以拦截请求,在方法执行前后加上额外的逻辑(如日志记录、权限校验、事务管理)。
Java 原生的动态代理(JDK 动态代理)基于接口实现:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 1. 定义接口和实现类
interface UserService {
void save();
}
class UserServiceImpl implements UserService {
public void save() { System.out.println("保存用户数据"); }
}
public class ProxyDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
// 2. 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("--> 开启事务");
Object result = method.invoke(target, args); // 执行目标方法
System.out.println("--> 提交事务");
return result;
}
}
);
proxy.save(); // 调用时会自动触发 invoke 方法
}
}
反射 + 注解 + 动态代理 = 现代 Java 框架(如 Spring AOP)的基石。
4.6 Optional:优雅解决空指针
NullPointerException(空指针异常)是 Java 程序员最头疼的异常。Java 8 引入了 Optional 类,它就像一个盒子,强制开发者明确处理对象可能为 null 的情况,从而避免写出满屏的 if (obj != null)。
// 传统写法,容易遗漏判空导致 NPE
User user = getUser();
if (user != null) {
String name = user.getName();
if (name != null) {
System.out.println(name.toUpperCase());
}
}
// Optional 的优雅写法
Optional.ofNullable(getUser())
.map(User::getName)
.map(String::toUpperCase)
.ifPresent(System.out::println);
常用方法:
Optional.ofNullable(obj):包装可能为空的对象。orElse(defaultObj):如果对象为空,返回指定的默认值。orElseThrow(...):如果对象为空,抛出指定的异常。
第五章 学习衔接
| 下一步 | 文档 |
|---|---|
| 并发与线程安全 | Java 多线程编程 |
| 文件与配置实战 | Java IO 流与配置文件 |
| Web 与框架 | Spring 注解开发指南 → Java Web 后端开发 |
课后练习
- 用递归列出某目录下所有
.java文件路径。 - 自定义
BusinessException继承RuntimeException,在 Service 中抛出并由上层捕获。 - 用反射读取某实体类的字段名列表并打印。
至此,Java 进阶编程的核心知识点已梳理完毕。建议每章配合 IDEA Debug 敲示例,再进入多线程与 Web 章节。