已发布·持续改进
Java 指南 · 5/6
本章目前仅提供英文版。
Java reports failures with exceptions, split into checked exceptions, which the compiler makes you handle, and unchecked ones, which it does not. This chapter covers both, along with try-with-resources, custom exceptions, and unit testing with JUnit.
Every exception extends Throwable. The Exception branch covers problems a program can handle; the Error branch signals serious trouble you normally leave alone.
| Kind | Examples | Compiler enforcement |
|---|---|---|
| Checked | IOException, SQLException | Must be caught or declared with throws |
| Unchecked | NullPointerException, IllegalArgumentException | Not enforced |
| Error | OutOfMemoryError, StackOverflowError | Usually not handled |
Unchecked exceptions extend RuntimeException and usually signal programming mistakes such as bad arguments. Checked exceptions model outside failures, like a missing file, that callers should expect.
package com.example.config;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ConfigLoader {
// a checked exception can be passed up to the caller with throws
static String load(Path path) throws IOException {
return Files.readString(path);
}
static int parsePort(String value) {
int port = Integer.parseInt(value); // may throw NumberFormatException (unchecked)
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("port out of range: " + port);
}
return port;
}
}Wrap risky code in try and handle failures in catch clauses, most specific type first. One clause can catch several unrelated types with |. A finally block always runs.
try {
String text = ConfigLoader.load(Path.of("app.conf"));
int port = ConfigLoader.parsePort(text.strip());
System.out.println("port = " + port);
} catch (NoSuchFileException | AccessDeniedException e) {
System.err.println("Cannot access config file: " + e.getFile());
} catch (IOException e) {
System.err.println("Failed to read config: " + e.getMessage());
} catch (IllegalArgumentException e) { // also catches NumberFormatException
System.err.println("Bad port value: " + e.getMessage());
} finally {
System.out.println("Config load attempted");
}Never swallow an exception with an empty ; if you cannot handle it, let it propagate or wrap it in a more meaningful one.
catchAnything that must be closed, such as files, sockets, or connections, belongs in a try-with-resources header. It is closed automatically when the block exits, even on an exception. Any AutoCloseable works, and multiple resources close in reverse order.
static long countErrors(Path log) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(log)) {
return reader.lines()
.filter(line -> line.contains("ERROR"))
.count();
} // reader.close() runs automatically
}Domain-specific exceptions let callers tell failures apart. Extend Exception when callers must plan for the failure, RuntimeException otherwise, and accept a cause so wrapped stack traces are preserved.
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(long orderId) {
super("order not found: " + orderId);
}
public OrderNotFoundException(long orderId, Throwable cause) {
super("order not found: " + orderId, cause);
}
}JUnit is the standard testing framework for Java, and tests use the Jupiter API introduced with JUnit 5. With Maven, add junit-jupiter as a test-scoped dependency (the previous chapter shows Gradle); test classes go in the same package under src/test/java.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>Annotate test methods with @Test and verify results with the static methods in Assertions. assertThrows checks that an exception is thrown and hands it back, so you can inspect its message too.
package com.example.config;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class ConfigLoaderTest {
@Test
@DisplayName("a valid port is parsed to a number")
void parsesValidPort() {
assertEquals(8080, ConfigLoader.parsePort("8080"));
}
@Test
void rejectsOutOfRangePort() {
var e = assertThrows(IllegalArgumentException.class,
() -> ConfigLoader.parsePort("70000"));
assertTrue(e.getMessage().contains("70000"));
}
@ParameterizedTest
@ValueSource(strings = {"0", "-1", "65536"})
void rejectsInvalidPorts(String value) {
assertThrows(IllegalArgumentException.class, () -> ConfigLoader.parsePort(value));
}
}Run the suite with mvn test or ./gradlew test. @ParameterizedTest repeats a check over many inputs, and IDEs can run or debug a single test from the editor.
RuntimeException.cause constructor so wrapped failures keep their history.@Test, assertEquals, and assertThrows, and run tests through your build tool.
0 条评论
登录 · 登录后即可发表评论。
来发表第一条评论吧。