MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java最佳编码实践:异常处理

2021-05-143.4k 阅读

异常处理基础

在Java编程中,异常是在程序执行期间发生的事件,它中断了程序指令的正常流程。异常处理机制使程序员能够优雅地处理这些异常情况,防止程序崩溃,并提供更好的用户体验和系统稳定性。

异常的分类

Java中的异常分为两大类:检查异常(Checked Exceptions)非检查异常(Unchecked Exceptions)

  1. 检查异常:这类异常在编译时被检测。编译器要求程序员在代码中显式处理这类异常,要么使用try - catch块捕获它们,要么在方法签名中使用throws关键字声明抛出它们。例如,IOException就是一种常见的检查异常。当你尝试读取一个可能不存在的文件时,就会抛出IOException
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class CheckedExceptionExample {
    public static void main(String[] args) {
        File file = new File("nonexistentfile.txt");
        try {
            FileReader reader = new FileReader(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在上述代码中,FileReader的构造函数可能会抛出IOException,因此必须进行处理。

  1. 非检查异常:这类异常包括运行时异常(RuntimeException及其子类)和错误(Error及其子类)。运行时异常如NullPointerExceptionArrayIndexOutOfBoundsException等,通常是由于编程错误导致的,编译器不会强制要求处理它们。错误如OutOfMemoryError,表示程序运行时出现了严重的系统问题,一般无法在程序中处理。
public class UncheckedExceptionExample {
    public static void main(String[] args) {
        String str = null;
        try {
            System.out.println(str.length());
        } catch (NullPointerException e) {
            e.printStackTrace();
        }
    }
}

在这段代码中,strnull,调用length()方法会抛出NullPointerException,这是一个运行时异常。虽然可以使用try - catch块捕获,但编译器并不强制要求。

异常处理语句

  1. try - catch块try块包含可能会抛出异常的代码。catch块用于捕获并处理try块中抛出的异常。可以有多个catch块来处理不同类型的异常。
public class TryCatchExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            System.out.println("发生算术异常: " + e.getMessage());
        }
    }
}

在上述代码中,10 / 0会抛出ArithmeticExceptioncatch块捕获并打印出异常信息。

  1. finally块finally块总是会在try块结束后执行,无论是否发生异常,也无论try块是正常结束还是通过returnbreakcontinue语句结束。
public class FinallyExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 2;
            return;
        } catch (ArithmeticException e) {
            System.out.println("发生算术异常: " + e.getMessage());
        } finally {
            System.out.println("finally块总是会执行");
        }
    }
}

在这段代码中,即使try块中有return语句,finally块仍然会执行。

最佳异常处理实践

精确捕获异常

  1. 避免捕获通用异常:捕获Exception类虽然可以捕获所有类型的异常,但这会隐藏具体的异常信息,使调试变得困难。应该尽量捕获具体的异常类型。
// 不好的实践
try {
    // 可能抛出多种异常的代码
} catch (Exception e) {
    e.printStackTrace();
}
// 好的实践
try {
    // 可能抛出多种异常的代码
} catch (IOException e) {
    // 处理IOException
} catch (SQLException e) {
    // 处理SQLException
}
  1. 异常捕获顺序:在多个catch块中,应该先捕获具体的异常,再捕获通用的异常。如果顺序颠倒,编译器会报错,因为具体的异常永远不会被捕获。
try {
    // 可能抛出异常的代码
} catch (NullPointerException e) {
    // 处理NullPointerException
} catch (RuntimeException e) {
    // 处理RuntimeException
}

异常处理的粒度

  1. 合适的捕获范围try块的范围应该尽量小,只包含可能抛出异常的代码。这样可以提高代码的可读性和维护性,并且更容易定位异常发生的位置。
// 不好的实践
try {
    // 大量代码,其中只有部分可能抛出异常
    // 读取文件
    // 数据库操作
    // 网络请求
} catch (IOException e) {
    // 处理IOException
}
// 好的实践
try {
    // 读取文件代码
} catch (IOException e) {
    // 处理读取文件的IOException
}
try {
    // 数据库操作代码
} catch (SQLException e) {
    // 处理数据库操作的SQLException
}
  1. 方法级别的异常处理:在方法中,应该根据方法的职责来决定如何处理异常。如果方法能够合理地处理异常,就应该在方法内部处理;如果方法无法处理,就应该将异常抛出,让调用者来处理。
public class FileUtil {
    public static String readFile(String filePath) throws IOException {
        // 读取文件的代码
        // 如果发生IOException,抛出异常
    }
}
public class Main {
    public static void main(String[] args) {
        try {
            String content = FileUtil.readFile("example.txt");
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("读取文件失败: " + e.getMessage());
        }
    }
}

在上述代码中,FileUtil.readFile方法将IOException抛出,由main方法来处理。

异常信息的处理

  1. 记录详细的异常信息:在捕获异常时,应该记录详细的异常信息,包括异常类型、异常消息以及堆栈跟踪信息。这对于调试和问题排查非常有帮助。
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class ExceptionLoggingExample {
    private static final Logger LOGGER = Logger.getLogger(ExceptionLoggingExample.class.getName());

    public static void main(String[] args) {
        File file = new File("nonexistentfile.txt");
        try {
            FileReader reader = new FileReader(file);
        } catch (IOException e) {
            LOGGER.log(Level.SEVERE, "读取文件时发生异常", e);
        }
    }
}

在上述代码中,使用java.util.logging.Logger记录了异常信息,包括异常消息和堆栈跟踪。

  1. 避免泄露敏感信息:在记录异常信息或向用户显示异常信息时,要注意避免泄露敏感信息,如数据库密码、文件路径等。可以对异常消息进行适当的处理,只显示通用的错误信息给用户。
try {
    // 可能抛出异常的数据库操作
    // 例如,使用JDBC连接数据库
} catch (SQLException e) {
    if (e.getMessage().contains("password")) {
        System.out.println("数据库连接失败,请检查用户名和密码");
    } else {
        System.out.println("数据库操作失败: " + e.getMessage());
    }
}

自定义异常

  1. 创建自定义异常类:当Java内置的异常类不能满足需求时,可以创建自定义异常类。自定义异常类通常继承自Exception(检查异常)或RuntimeException(非检查异常)。
// 自定义检查异常
class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}
// 自定义非检查异常
class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}
  1. 使用自定义异常:在代码中,当特定的业务逻辑出现问题时,可以抛出自定义异常。
public class CustomExceptionExample {
    public static void validateAge(int age) throws MyCheckedException {
        if (age < 0) {
            throw new MyCheckedException("年龄不能为负数");
        }
    }

    public static void main(String[] args) {
        try {
            validateAge(-5);
        } catch (MyCheckedException e) {
            System.out.println("捕获到自定义异常: " + e.getMessage());
        }
    }
}

在上述代码中,validateAge方法在年龄为负数时抛出MyCheckedExceptionmain方法捕获并处理该异常。

异常处理与性能

异常对性能的影响

  1. 抛出异常的开销:抛出异常是一个相对昂贵的操作,因为它涉及到创建异常对象、填充堆栈跟踪信息等操作。在性能敏感的代码中,应该尽量避免频繁地抛出异常。
public class PerformanceExample {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                if (i % 1000 == 0) {
                    throw new RuntimeException("模拟异常");
                }
            } catch (RuntimeException e) {
                // 忽略异常
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("抛出异常的总时间: " + (endTime - startTime) + " 毫秒");
    }
}

在上述代码中,通过循环抛出异常并捕获,测量抛出异常的时间开销。

  1. 异常处理对性能的影响:虽然try - catch块本身对性能的影响较小,但如果catch块中执行了复杂的操作,也会影响性能。
public class CatchPerformanceExample {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                if (i % 1000 == 0) {
                    throw new RuntimeException("模拟异常");
                }
            } catch (RuntimeException e) {
                // 复杂的日志记录操作
                System.out.println("捕获到异常: " + e.getMessage());
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("捕获异常并处理的总时间: " + (endTime - startTime) + " 毫秒");
    }
}

在这段代码中,catch块中执行了打印日志的操作,会增加处理异常的时间开销。

优化异常处理以提高性能

  1. 避免使用异常进行流程控制:异常应该用于处理意外情况,而不是用于控制程序的正常流程。例如,使用if - else语句来检查条件,而不是依赖异常来处理常见的业务逻辑。
// 不好的实践
try {
    int result = divide(10, 0);
} catch (ArithmeticException e) {
    // 处理除零情况
}
public static int divide(int a, int b) {
    return a / b;
}
// 好的实践
if (b == 0) {
    // 处理除零情况
} else {
    int result = a / b;
}
  1. 提前检查条件:在可能抛出异常的操作之前,先进行条件检查,避免不必要的异常抛出。
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class ConditionCheckExample {
    public static void main(String[] args) {
        File file = new File("nonexistentfile.txt");
        if (file.exists()) {
            try {
                FileReader reader = new FileReader(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("文件不存在");
        }
    }
}

在上述代码中,先检查文件是否存在,避免了FileReader构造函数抛出FileNotFoundException

异常处理与设计模式

异常处理中的责任链模式

  1. 责任链模式概述:责任链模式允许你将请求沿着处理者链进行发送。每个处理者都有机会处理请求,或者将请求传递给链中的下一个处理者。在异常处理中,可以使用责任链模式来处理不同类型的异常。
  2. 实现责任链模式处理异常
abstract class ExceptionHandler {
    protected ExceptionHandler nextHandler;

    public void setNextHandler(ExceptionHandler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public abstract void handleException(Exception e);
}
class IOExceptionHandler extends ExceptionHandler {
    @Override
    public void handleException(Exception e) {
        if (e instanceof IOException) {
            System.out.println("处理IOException: " + e.getMessage());
        } else if (nextHandler != null) {
            nextHandler.handleException(e);
        }
    }
}
class SQLExceptionHandler extends ExceptionHandler {
    @Override
    public void handleException(Exception e) {
        if (e instanceof SQLException) {
            System.out.println("处理SQLException: " + e.getMessage());
        } else if (nextHandler != null) {
            nextHandler.handleException(e);
        }
    }
}
public class ChainOfResponsibilityExample {
    public static void main(String[] args) {
        ExceptionHandler ioHandler = new IOExceptionHandler();
        ExceptionHandler sqlHandler = new SQLExceptionHandler();
        ioHandler.setNextHandler(sqlHandler);

        try {
            // 可能抛出异常的代码
        } catch (IOException e) {
            ioHandler.handleException(e);
        } catch (SQLException e) {
            sqlHandler.handleException(e);
        }
    }
}

在上述代码中,IOExceptionHandlerSQLExceptionHandler组成了一个责任链,根据异常类型进行处理。

异常处理中的策略模式

  1. 策略模式概述:策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。在异常处理中,可以使用策略模式来定义不同的异常处理策略。
  2. 实现策略模式处理异常
interface ExceptionHandlingStrategy {
    void handleException(Exception e);
}
class LoggingStrategy implements ExceptionHandlingStrategy {
    @Override
    public void handleException(Exception e) {
        System.out.println("记录异常: " + e.getMessage());
    }
}
class NotificationStrategy implements ExceptionHandlingStrategy {
    @Override
    public void handleException(Exception e) {
        System.out.println("发送异常通知: " + e.getMessage());
    }
}
public class StrategyExample {
    private ExceptionHandlingStrategy strategy;

    public StrategyExample(ExceptionHandlingStrategy strategy) {
        this.strategy = strategy;
    }

    public void process() {
        try {
            // 可能抛出异常的代码
        } catch (Exception e) {
            strategy.handleException(e);
        }
    }

    public static void main(String[] args) {
        ExceptionHandlingStrategy loggingStrategy = new LoggingStrategy();
        ExceptionHandlingStrategy notificationStrategy = new NotificationStrategy();

        StrategyExample loggingExample = new StrategyExample(loggingStrategy);
        StrategyExample notificationExample = new StrategyExample(notificationStrategy);

        loggingExample.process();
        notificationExample.process();
    }
}

在上述代码中,定义了LoggingStrategyNotificationStrategy两种异常处理策略,并通过StrategyExample类来使用这些策略处理异常。

多线程中的异常处理

线程内的异常处理

  1. 在Runnable中处理异常:当在Runnable接口的实现类中执行代码时,如果发生异常,需要在run方法内部进行处理,因为run方法不能抛出异常。
public class ThreadExceptionExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                int result = 10 / 0;
            } catch (ArithmeticException e) {
                System.out.println("线程内捕获到异常: " + e.getMessage());
            }
        });
        thread.start();
    }
}

在上述代码中,Runnablerun方法中使用try - catch块捕获并处理了ArithmeticException

  1. 使用Callable和Future处理异常Callable接口允许返回一个结果,并且可以抛出异常。Future接口用于获取Callable任务的执行结果,也可以获取异常信息。
import java.util.concurrent.*;

public class CallableExceptionExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Callable<Integer> callable = () -> {
            int result = 10 / 0;
            return result;
        };
        Future<Integer> future = executorService.submit(callable);
        try {
            Integer result = future.get();
        } catch (InterruptedException | ExecutionException e) {
            System.out.println("获取Callable结果时捕获到异常: " + e.getMessage());
        } finally {
            executorService.shutdown();
        }
    }
}

在这段代码中,Callable抛出的异常通过Future.get()方法获取并处理。

线程池中的异常处理

  1. 默认的线程池异常处理:在ThreadPoolExecutor中,如果任务抛出未处理的异常,默认情况下,线程池会调用Thread.UncaughtExceptionHandler的实现来处理异常。如果没有设置UncaughtExceptionHandler,默认行为是将异常堆栈信息打印到标准错误输出。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDefaultExceptionExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.submit(() -> {
            int result = 10 / 0;
        });
        executorService.shutdown();
    }
}

在上述代码中,线程池中的任务抛出ArithmeticException,默认会将异常堆栈信息打印到标准错误输出。

  1. 自定义线程池异常处理:可以通过设置Thread.UncaughtExceptionHandler来实现自定义的异常处理逻辑。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolCustomExceptionExample {
    public static void main(String[] args) {
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            System.out.println("线程 " + t.getName() + " 发生异常: " + e.getMessage());
        });
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        executorService.submit(() -> {
            int result = 10 / 0;
        });
        executorService.shutdown();
    }
}

在这段代码中,通过设置Thread.setDefaultUncaughtExceptionHandler,自定义了线程池任务异常的处理逻辑。

异常处理在企业级开发中的应用

分层架构中的异常处理

  1. 表现层异常处理:在Web应用的表现层(如Spring MVC的Controller层),通常捕获异常并将其转换为合适的HTTP响应码和错误信息返回给客户端。
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IOException.class) {
        public ResponseEntity<String> handleIOException(IOException e) {
            return new ResponseEntity<>("文件读取失败", HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

在上述代码中,使用Spring的ControllerAdviceExceptionHandler注解,将IOException转换为HTTP 500错误响应。

  1. 业务逻辑层异常处理:在业务逻辑层(如Service层),通常捕获并处理与业务相关的异常,或者将异常包装后重新抛出,以便上层处理。
import org.springframework.stereotype.Service;

@Service
public class UserService {
    public void registerUser(String username, String password) throws UserRegistrationException {
        if (username == null || password == null) {
            throw new UserRegistrationException("用户名和密码不能为空");
        }
        // 业务逻辑
    }
}
class UserRegistrationException extends RuntimeException {
    public UserRegistrationException(String message) {
        super(message);
    }
}

在这段代码中,UserService抛出UserRegistrationException,表示业务逻辑中的异常。

  1. 数据访问层异常处理:在数据访问层(如DAO层),通常捕获数据库相关的异常(如SQLException),并将其转换为更通用的异常类型,如DataAccessException,以便上层处理。
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class UserDao {
    private final JdbcTemplate jdbcTemplate;

    public UserDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void saveUser(String username, String password) {
        try {
            String sql = "INSERT INTO users (username, password) VALUES (?,?)";
            jdbcTemplate.update(sql, username, password);
        } catch (DataAccessException e) {
            // 处理数据访问异常
            throw new RuntimeException("保存用户失败", e);
        }
    }
}

在上述代码中,UserDao捕获DataAccessException,并重新抛出一个RuntimeException

分布式系统中的异常处理

  1. 远程调用异常处理:在分布式系统中,当进行远程调用(如使用RESTful API、RPC等)时,可能会发生网络异常、服务不可用等情况。应该对远程调用的异常进行适当处理,例如重试机制。
import com.google.common.util.concurrent.Retryer;
import com.google.common.util.concurrent.RetryerBuilder;
import com.google.common.util.concurrent.StopStrategies;
import com.google.common.util.concurrent.WaitStrategies;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class RemoteCallExample {
    private final RestTemplate restTemplate = new RestTemplate();
    private final Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
          .retryIfExceptionOfType(IOException.class)
          .withStopStrategy(StopStrategies.stopAfterAttempt(3))
          .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
          .build();

    public String makeRemoteCall() {
        try {
            return retryer.call(() -> {
                // 远程调用代码,例如restTemplate.getForObject(url, String.class);
                throw new IOException("模拟网络异常");
            });
        } catch (ExecutionException | InterruptedException e) {
            throw new RuntimeException("远程调用失败", e);
        }
    }
}

在上述代码中,使用Google Guava的Retryer对可能抛出IOException的远程调用进行重试。

  1. 微服务间异常传播:在微服务架构中,当一个微服务调用另一个微服务发生异常时,需要考虑如何将异常信息传播给最终用户或相关的监控系统。可以通过在HTTP响应头或消息体中传递异常信息,同时在服务内部记录详细的异常日志。
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class MicroserviceExceptionHandler {
    @ExceptionHandler(ServiceUnavailableException.class) {
        public ResponseEntity<String> handleServiceUnavailableException(ServiceUnavailableException e, WebRequest request) {
            HttpHeaders headers = new HttpHeaders();
            headers.add("X-Error-Detail", e.getMessage());
            return new ResponseEntity<>("服务不可用", headers, HttpStatus.SERVICE_UNAVAILABLE);
        }
    }
}

在这段代码中,当发生ServiceUnavailableException时,将异常信息添加到HTTP响应头X - Error - Detail中,并返回HTTP 503状态码。

通过以上全面的异常处理实践,在Java开发中能够更好地处理各种异常情况,提高程序的健壮性、可靠性和可维护性。无论是简单的单机应用还是复杂的分布式系统,合理的异常处理都是保证系统稳定运行的关键因素之一。