你有没有掉进去过这些 Exception 的“陷阱“(Part C)

七、除了NullPointException外的其他常见异常

ConcurrentModificationException

在test包中新增测试类ConcurrentModificationExceptionTest

public class ConcurrentModificationExceptionTest {

    List<User> userList = new ArrayList<>();

    @Before
    public void before() {

        User stark = new User();
        stark.setName("stark");
        User thor = new User();
        thor.setName("thor");
        userList.add(stark);
        userList.add(thor);
    }

    @Test
    public void testModifyWhileIteratoringByFor(){

        // 直接使用for循环,触发并发修改异常
        for (User user : userList) {
            if (user.getName().equals("thor")){
                userList.remove(user);
            }
        }
    }
}
复制代码

image.png 在使用for循环进行遍历集合同时将符合条件的元素移出集合会报并发修改异常,也就是触发了Java中的fail-fast机制。

fail-fast 机制是 java 集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

要解决并发修改异常,可以使用迭代器进行遍历。增加测试方法testModifyWhileIteratoringByIterator()

@Test
public void testModifyWhileIteratoringByIterator(){

    // 直接使用迭代器
    Iterator<User> iter = userList.iterator();
    while (iter.hasNext()){
        User user = iter.next();
        if (user.getName() == "thor"){
            iter.remove();
        }
    }
}
复制代码

image.png next()方法一定在remove()方法之前调用,方法执行没有任何异常,虽然迭代器能够避免并发修改异常的问题,但是最好不要在遍历中删除

ClassCastException

在entity包中定义两个User的子类Admin和Employee

public class Admin extends User {
}
复制代码
public class Employee extends User {
}
复制代码

在test包下新增测试类ClassCastExceptionTest

public class ClassCastExceptionTest {
    
    User employee = new Employee();

    @Test
    public void testCastWithDifferentClass(){
        // 子类之间转换
        Admin admin = (Admin) employee;
    }
}
复制代码

image.png 两个子类之间是没有继承关系的,子类之间直接转换会抛出类型转换异常的错误,解决这类问题可以先进行类型关系判断,通过getClass().getName()来得到具体类型,再通过instanceof进行判断是否含有继承关系,如果有继承关系再进行类型转换,否则无法进行类型转换

IllegalArgumentException

日期转换时的非法参数异常

在日期转换时,如果传入的参数不对也会报错非法参数异常

@Test
public void testUser(){
    Date date = new Date();
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String simpleDate = simpleDateFormat.format(date);
    System.out.println(simpleDate);

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM");
    String format = dateFormat.format("");
    System.out.println(format);
}
复制代码

image.png format()函数的参数是一个Object类型,所以传入String类型时不会报编译错误,但是运行时会出现IllegalArgumentException的异常

枚举查找时的非法参数异常

新建一个enums包,增加一个枚举类LoginErrorEnum,包含了三个枚举值

扫描二维码关注公众号,回复: 14143376 查看本文章
public enum LoginErrorEnum {
    USERNAME_OR_PASSWORD_NOT_CORRECT,
    NOT_ADMIN_ROLE,
    USERNAME_NOT_REGISTER,
}
复制代码

在test包下新增IllegalArgumentExceptionTest,测试查找一个不存在的枚举值

public class IllegalArgumentExceptionTest {

    @Test
    public void testGetValueFromEnum(){
        LoginErrorEnum passwordNotCorrect = LoginErrorEnum.valueOf("PASSWORD_NOT_CORRECT");
        System.out.println(passwordNotCorrect);
    }

}
复制代码

image.png

枚举查找异常解决方案

第一种方式是使用try-catch这种比较通用的方式来解决枚举查找异常

@Test
public void testGetValueFromEnum(){
    try {
        LoginErrorEnum passwordNotCorrect = LoginErrorEnum.valueOf("PASSWORD_NOT_CORRECT");
        System.out.println(passwordNotCorrect);
    } catch (IllegalArgumentException e){
        System.out.println(e.getMessage());
    }
}
复制代码

image.png 当要查找的枚举值不存在时,直接在控制台输出异常信息

第二种方式可以使用for循环遍历的方式,遍历所有的枚举值,查看是否有符合条件的枚举值,但是for循环效率较低

第三种方式可以使用Guava,首先在pom.xml文件中导入guava依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>12.0</version>
</dependency>
复制代码

新增测试方法testGetValueFromEnumWithGuava

@Test
public void testGetValueFromEnumWithGuava(){

    System.out.println(Enums.getIfPresent(LoginErrorEnum.class, "PASSWORD_NOT_CORRECT").orNull());
}
复制代码

image.png 控制台输出为null,避免了非法参数异常

八、 资源关闭

资源以及资源泄露

资源有哪些?

  • 文件
  • socket连接
  • 数据库连接
  • ....

资源在使用过之后要进行关闭或者释放,如果没有释放怎会导致资源泄露的问题

try finally 关闭资源的问题

新增一个测试类HandlerResoucesTest,使用try-catch-finally关闭单个资源的代码如下

@Test
public void testCloseSingleByTryCatchFinally() throws IOException {
    String context = null;
    BufferedReader bufferedReader = new BufferedReader(new FileReader("info.txt"));
    try {
        context = bufferedReader.readLine();
        System.out.println(context);
    } catch (Exception e){
        System.out.println(e.getMessage());
    } finally {
        bufferedReader.close();
    }
}
复制代码

finally代码块中的代码无论是否出现异常都会执行,因此将资源关闭的代码放在finally中,确保操作结束后关闭资源

当try代码块中又包含另外一个资源的读取的时候,代码会变成这样

@Test
public void testCloseMultiByTryCatchFinally() throws IOException{
    String context = null;
    String message = null;
    BufferedReader bufferedReader = new BufferedReader(new FileReader("info.txt"));
    try {
        context = bufferedReader.readLine();
        BufferedReader messBufferedReader = new BufferedReader(new FileReader("message.txt"));
        try {
            message = messBufferedReader.readLine();
            System.out.println(message);
        } catch (Exception e){
            System.out.println(e.getMessage());
        } finally {
            messBufferedReader.close();
        }
        System.out.println(context);
    } catch (Exception e){
        System.out.println(e.getMessage());
    } finally {
        bufferedReader.close();
    }
}
复制代码

使用try-catch关闭多个资源时代码冗长且不易阅读

try-with-resources 解决资源泄露隐患

try-with-resources只需要声明和使用,不需要考虑关闭的问题,在try关键字后面的括号中里new一些需要自动关闭的资源。

BufferedRead从java 7开始就实现了 AutoCloseable 接口,无论try-with关闭资源是正常关闭还是异常关闭,autoClose都能关闭他们

image.png 关闭单个资源的代码

@Test
public void testCloseSingleByTryWithResources() throws IOException {
    try(BufferedReader reader = new BufferedReader(new FileReader("info.txt"))){
        System.out.println(reader.readLine());
    }
}
复制代码

关闭多个资源的代码

@Test
public void testCloseMultiByTryWithResources() throws IOException{
    try(FileInputStream inputStream = new FileInputStream("info.txt");
        FileOutputStream outputStream  = new FileOutputStream("message.txt")) {
         byte[] buffer = new byte[100];
         int n = 0;
         while ((n = inputStream.read(buffer)) != -1){
             outputStream.write(buffer, 0, n);
         }
    }
}
复制代码

异常被覆盖的情况

如果try finally都抛出异常,finally中抛出的异常会抑制try中的异常,导致很难发现最初的异常。

在exceptions包中自定义一个异常

public class LiException extends Exception {

    public LiException() {
        super();
    }

    public LiException(String message) {
        super(message);
    }
}
复制代码

定义一个类实现AutoCloseable接口实现AutoCloseable接口,这个接口可以被try-with-resource自动关闭掉

public class LilithAutoCloseable implements AutoCloseable {


    @Override
    public void close() throws Exception {
        System.out.println("close()方法被调用");
        throw new RuntimeException("close()方法中抛出的异常");
    }

    public void work() throws LiException{
        System.out.println("work()方法被调用");
        throw new LiException("work()方法中抛出的异常");
    }
}
复制代码

增加测试方法testCloseWithAutoCloseable()

@Test
public void testCloseWithAutoCloseable() throws Exception {

    LilithAutoCloseable lilithAutoCloseable = new LilithAutoCloseable();
    try {
        lilithAutoCloseable.work();
    } finally {
        lilithAutoCloseable.close();
    }
}
复制代码

image.png work()方法中的异常被finally中的close()方法的异常覆盖掉了

使用try-with-resources则不会出现这中问题

@Test
public void testCloseWithAutoCloseableByTryWith() throws Exception {

    try(LilithAutoCloseable lilithAutoCloseable = new LilithAutoCloseable()) {
        lilithAutoCloseable.work();
    }

}
复制代码

image.png

work()方法和close()方法抛出的异常都被展示出来了

猜你喜欢

转载自juejin.im/post/7097357143350706207