Java(六)

33 阅读4分钟

异常是 Java 错误处理的主线;文件 IO 是读配置、日志、导入导出的基础。


1. Throwable 体系

概念

Throwable
├── Error          JVM 级严重问题(一般不捕获)
└── Exception
    ├── RuntimeException(非受检 unchecked)
    └── 其他 Exception(受检 checked)
  • 受检异常:编译器要求处理或声明 throws(如 IOException)。
  • 非受检异常:RuntimeException 及子类、Error — 不强制 catch。

原理

异常是对象,抛出时栈回溯 unwinding,直到匹配的 catch 或线程终止。

完整示例

package com.example.demo;

public class ThrowableDemo {
    static int parsePort(String s) {
        return Integer.parseInt(s);  // 可能 NumberFormatException(非受检)
    }

    public static void main(String[] args) {
        try {
            System.out.println(parsePort("8080"));
            System.out.println(parsePort("abc"));
        } catch (NumberFormatException e) {
            System.err.println("非法端口: " + e.getMessage());
        }
    }
}

练习

写方法 divide(int a, int b),除零抛自定义非受检异常 BusinessException(带 message)。

完整例子

package com.example.demo;

class BusinessException extends RuntimeException {
    BusinessException(String message) { super(message); }
}

public class DivideDemo {
    static int divide(int a, int b) {
        if (b == 0) throw new BusinessException("divide by zero");
        return a / b;
    }

    public static void main(String[] args) {
        try {
            System.out.println(divide(10, 0));
        } catch (BusinessException e) {
            System.err.println(e.getMessage());
        }
    }
}

常见坑

  • 空 catch 吞掉异常 — 至少打日志。
  • 用异常控制正常业务流程(如找不到用异常)— 可用 Optional 或返回值。

2. try / catch / finally

概念

try 监控块;catch 按类型匹配(子类在前);finally 几乎总会执行(用于释放资源,现代更推荐 try-with-resources)。

完整示例

package com.example.demo;

public class TryCatchDemo {
    static String readSimulated(boolean fail) {
        if (fail) throw new IllegalStateException("simulated");
        return "ok";
    }

    public static void main(String[] args) {
        String result = "default";
        try {
            result = readSimulated(true);
        } catch (IllegalStateException e) {
            result = "fallback";
        } finally {
            System.out.println("cleanup always runs");
        }
        System.out.println(result);
    }
}

常见坑

  • return 在 try 与 finally 都有时,finally 后执行且其 return 会覆盖 try 的返回值 — 避免 finally 里 return。
  • 捕获 Exception 太宽,掩盖真实问题。

3. try-with-resources

概念

JDK 7+:try (Resource r = ...) { } 自动调用 AutoCloseable.close(),即使抛异常也会关闭(抑制异常机制)。

原理

编译器生成等价于 try-finally 的字节码,且正确处理 primary/suppressed exception。

完整示例

package com.example.demo;

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class TryWithResourcesDemo {
    static void printFirstLine(Path path) throws IOException {
        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line = reader.readLine();
            System.out.println(line);
        }
    }

    public static void main(String[] args) throws IOException {
        Path tmp = Files.createTempFile("demo", ".txt");
        Files.writeString(tmp, "hello\nworld", StandardCharsets.UTF_8);
        printFirstLine(tmp);
        Files.deleteIfExists(tmp);
    }
}

练习

读配置文件

要点:路径 config/app.properties;读 app.name、server.port(默认 8080)、feature.enabled;文件不存在友好提示;用 try-with-resources 或 Files.readAllLines。

完整例子

package com.example.demo.exercise;

import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class ReadAppConfigTryWithResources {
    public static void main(String[] args) {
        Path path = Path.of("config/app.properties");
        if (!Files.exists(path)) {
            System.err.println("配置文件不存在: " + path.toAbsolutePath());
            return;
        }
        Properties props = new Properties();
        // try-with-resources:自动 close Reader
        try (Reader r = Files.newBufferedReader(path)) {
            props.load(r);
        } catch (Exception e) {
            System.err.println("读取失败: " + e.getMessage());
            return;
        }
        String name = props.getProperty("app.name", "unknown");
        int port = Integer.parseInt(props.getProperty("server.port", "8080"));
        boolean enabled = Boolean.parseBoolean(props.getProperty("feature.enabled", "false"));
        System.out.printf("app=%s port=%d feature=%s%n", name, port, enabled);
    }
}

常见坑

  • 资源未实现 AutoCloseable 不能放进 try-with-resources。
  • 在 try 块外仍使用已关闭资源。

4. throws 与自定义异常

概念

方法签名 throws IOException 把处理责任交给调用方。业务层常用非受检自定义异常 + 全局处理器(Spring @ControllerAdvice)。

完整示例

package com.example.demo;

public class BusinessException extends RuntimeException {
    private final String code;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public String getCode() { return code; }
}

class OrderService {
    void cancel(Long orderId) {
        if (orderId == null) {
            throw new BusinessException("BAD_REQUEST", "orderId required");
        }
        // ...
    }
}

练习

定义 NotFoundException extends RuntimeException,findUser(id) 找不到时抛出。

完整例子

package com.example.demo.exercise;

import java.util.Map;

class NotFoundException extends RuntimeException {
    NotFoundException(String message) { super(message); }
}

public class FindUserDemo {
    static final Map<Long, String> DB = Map.of(1L, "alice");

    static String findUser(Long id) {
        String name = DB.get(id);
        if (name == null) {
            throw new NotFoundException("user not found: " + id);
        }
        return name;
    }

    public static void main(String[] args) {
        try {
            System.out.println(findUser(99L));
        } catch (NotFoundException e) {
            System.err.println(e.getMessage());
        }
    }
}

常见坑

  • 受检异常层层 throws 导致签名污染 — 业务异常多用 RuntimeException。
  • 自定义异常不调用 super(message) 导致堆栈/message 丢失。

5. 文件 IO 入门

概念

  • NIO.2(java.nio.file):Path、Files.readString / writeString、lines Stream。
  • 旧 IO(java.io.FileInputStream)仍存在,新项目优先 NIO。

原理

读文件 = OS 打开文件描述符 → 读字节 → 解码为 String(指定 Charset,默认 UTF-8 推荐)。

完整示例

package com.example.demo;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Properties;

public class FileIoDemo {

    /** 简单 key=value 行配置 */
    static Properties loadProperties(Path path) throws IOException {
        Properties p = new Properties();
        if (!Files.exists(path)) {
            return p;
        }
        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        for (String line : lines) {
            line = line.strip();
            if (line.isEmpty() || line.startsWith("#")) continue;
            int idx = line.indexOf('=');
            if (idx > 0) {
                p.setProperty(line.substring(0, idx).strip(), line.substring(idx + 1).strip());
            }
        }
        return p;
    }

    public static void main(String[] args) throws IOException {
        Path config = Path.of("app.properties");
        Files.writeString(config,
                """
                app.name=demo
                server.port=8080
                # comment
                """,
                StandardCharsets.UTF_8);

        Properties props = loadProperties(config);
        System.out.println(props.getProperty("server.port"));
        Files.deleteIfExists(config);
    }
}

练习

读 config:从 config/app.properties 读取 db.url、db.user,缺键时使用默认值并打印最终配置 Map。

完整例子

package com.example.demo;

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class DbConfigDemo {
    public static void main(String[] args) throws IOException {
        Path path = Path.of("config/app.properties");
        Properties props = new Properties();
        if (Files.exists(path)) {
            try (Reader r = Files.newBufferedReader(path)) {
                props.load(r);
            }
        }
        Map<String, String> cfg = new HashMap<>();
        cfg.put("db.url", props.getProperty("db.url", "jdbc:h2:mem:demo"));
        cfg.put("db.user", props.getProperty("db.user", "sa"));
        System.out.println(cfg);
    }
}

常见坑

  • 未指定编码用平台默认 charset — 跨环境乱码。
  • 大文件 readAllLines 一次进内存 — 用 Files.lines 流式处理(见 06 章 Stream)。

6. Properties 与路径

概念

Properties 继承 Hashtable,专门存 String 键值;Spring Boot 用 application.properties / application.yml(后续章节)。

完整示例

package com.example.demo;

import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class PropertiesLoadDemo {
    static Properties load(Path path) throws IOException {
        Properties props = new Properties();
        try (Reader reader = Files.newBufferedReader(path)) {
            props.load(reader);  // 标准 .properties 格式
        }
        return props;
    }
}

常见坑

  • 相对路径相对进程工作目录(IDE Run 的 Working Directory),不是 classpath — Spring 里 @Value 读的是 classpath 资源。

本章综合练习:读配置

  1. 创建 config/app.properties:
app.name=demo
server.port=8080
feature.enabled=true
  1. 程序读取并输出结构化信息;server.port 缺省为 8080。
  2. 文件不存在时打印友好提示,不崩溃(或抛带说明的 BusinessException)。

提示:exercises.md。

完整例子

package com.example.demo.exercise;

import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class ReadAppConfig {
    public static void main(String[] args) {
        Path path = Path.of("config/app.properties");
        if (!Files.exists(path)) {
            System.err.println("配置文件不存在: " + path.toAbsolutePath());
            return;
        }
        Properties props = new Properties();
        try (Reader r = Files.newBufferedReader(path)) {
            props.load(r);
        } catch (Exception e) {
            System.err.println("读取失败: " + e.getMessage());
            return;
        }
        String name = props.getProperty("app.name", "unknown");
        int port = Integer.parseInt(props.getProperty("server.port", "8080"));
        boolean enabled = Boolean.parseBoolean(props.getProperty("feature.enabled", "false"));
        System.out.printf("app=%s port=%d feature=%s%n", name, port, enabled);
    }
}

本章小结

主题要点
受检 vs 非受检IOException vs RuntimeException
资源try-with-resources
文件Path + Files + UTF-8
业务自定义 RuntimeException + message/code