Java 进阶编程实战

本文聚焦 Java 进阶编程实战,系统讲解异常处理、File 类、递归、网络编程、枚举、反射和注解等核心能力。文章结合原理说明与可运行示例,帮助读者从基础语法过渡到更接近真实项目的 Java 编程能力,并理解复杂特性背后的设计思路。

第一章 异常处理:程序的“安全带”

1.1 异常家族的族谱

Java 把所有程序运行中可能出现的不正常情况设计成一套继承体系:

  • Throwable(祖先)
  • Error:系统级的灾难(JVM 内存溢出、栈溢出),普通程序处理不了,也不该处理。
  • Exception:我们写代码时应该处理的异常。
  • 运行时异常RuntimeException):编译时不要求强制处理,比如空指针、索引越界。这通常说明代码逻辑有 Bug。
  • 编译时异常(非 Runtime):如文件找不到、网络断开。编译器强制开发者提前写出处理方案,因为这些问题很可能发生。

1.2 处理异常的方式

  • 自己处理(try-catch-finally):try 里面放可能有问题的代码,catch 捕获指定类型的异常并处理,finally 中的代码无论是否出异常都会执行,适合关闭资源(文件流、数据库连接等)。
Java
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 的资源:

Java
try (FileInputStream in = new FileInputStream("a.txt")) {
    // 使用流
} catch (IOException e) {
    e.printStackTrace();
}
// 无需手写 finally 关闭

调试建议:IDEA 中对断点行点击左侧,使用 Debug 而非 Run,可单步查看变量、调用栈,快速定位逻辑错误与异常来源。

1.3 自定义异常

当 JDK 提供的异常类无法精确表达你项目中的业务错误时,可以自定义异常。通常继承 Exception(编译时异常)或 RuntimeException(运行时异常)。

Java
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() 等方法去确认。

Java
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 递归的威力

递归就是方法自己调用自己,把一个复杂的大任务分解成许多小任务。

  • 递归的三要素
  1. 出口(终止条件):没有出口就是无限死循环,最终导致 StackOverflowError(栈溢出)。
  2. 规则(递归公式):如何把大问题拆解成小问题。
  3. 方向:必须向着出口的方向演进。

案例 1:计算阶乘 (n!)

Java
public static int factorial(int n) {
    if (n == 1) return 1; // 出口
    return n * factorial(n - 1); // 规则:n! = n * (n-1)!
}

案例 2:搜索所有 .java 文件

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:计算文件夹总大小

Java
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 对象建立连接,然后双方拿到对方的输入/输出流进行读写通信。

Java
// 服务端
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();
Java
// 客户端
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)。

Java
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 状态码或业务错误码:

Java
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 支持枚举,代码更具可读性。
Java
Season current = Season.SPRING;
switch (current) {
    case SPRING:
        System.out.println("春暖花开");
        break;
    case SUMMER:
        System.out.println("夏日炎炎");
        break;
    default:
        System.out.println("其他季节");
}
  • 遍历所有值ErrorCode.values() 可以获取枚举的所有实例数组,常用于下拉框数据展示或遍历校验。
Java
for (ErrorCode error : ErrorCode.values()) {
    System.out.println(error.name() + " -> " + error.getCode() + ": " + error.getMessage());
}
  • 单例模式的最佳实践:《Effective Java》推荐使用枚举来实现单例,不仅能防止多线程同步问题,还能防止反射破坏和反序列化重新创建新对象。
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 对象。有三种常见方式:

  1. Class.forName("全限定类名"):最常用,在只知道类名字符串时(比如读取配置文件)使用。
  2. 类名.class:在编译期就明确知道要用哪个类时使用,安全且性能高。
  3. 对象.getClass():已经拿到对象实例时,获取它的 Class 对象。

2. 暴力破解封装:setAccessible(true) 反射的一大特性是可以无视访问权限修饰符(如 private),直接操作类的私有成员。

Java
// 假设 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 或注解配置,动态地实例化类并调用方法,开发者无需在代码里硬编码依赖关系。
  • 缺点
  1. 性能开销:反射涉及动态类型解析,执行速度比直接调用的“正射”慢很多。
  2. 破坏封装:反射可以随意访问和修改私有成员,可能会引发安全问题或破坏对象的内部状态。因此,在日常业务代码中应谨慎使用反射,它主要用于编写底层框架或通用工具类。

4.4 注解:给代码贴上标签

注解像一个标签,可以贴在类上、方法上、属性上等等。它本身对代码运行没有直接影响,但可以通过反射解析这些标签,赋予特定功能。比如 @Override 告诉编译器检查是否真的重写了父类方法。

自定义注解:

Java
@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 动态代理)基于接口实现:

Java
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)

Java
// 传统写法,容易遗漏判空导致 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 后端开发

课后练习

  1. 用递归列出某目录下所有 .java 文件路径。
  2. 自定义 BusinessException 继承 RuntimeException,在 Service 中抛出并由上层捕获。
  3. 用反射读取某实体类的字段名列表并打印。

至此,Java 进阶编程的核心知识点已梳理完毕。建议每章配合 IDEA Debug 敲示例,再进入多线程与 Web 章节。