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 章节。