《给java开发者的实操避坑指南》学习笔记——空指针和异常

JAVA空指针和异常

什么是空指针?

我们都知道java是没有指针的,这里说的java指针指的就是java的引用,我们不在这里讨论叫指针究竟合不合适,而只是针对这个异常本身进行分析。java中的空指针就是空引用,java空指针异常就是引用本身为空,却调用了方法,这个时候就会出现空指针异常。可以理解,成员变量和方法是属于对象的(除去静态),在对象中才存在相对应的成员变量和方法,然后需要通过对象去调用这些成员变量和方法。对于空指针来说,它不指向任何对象,也就没有所谓的成员变量和方法,这个时候用它去调用某些属性和方法,当然会出现空指针异常。

1、如何从根源避免空指针

首先我们来看一个代码实例,看看空指针会出现在哪些情况中

  • 先构建一个静态的对象类User,对象属性包括nameaddress,对象方法有print()readBook两个
public static class User{
        private String name;
        private String[] address;

        public void print(){
            System.out.println("这是User类");
        }

        public String readBook(){
            System.out.println("User在读书!");
            return null;
        }
}
  • 再构建一个main方法,此处演示的是第一种空指针情况,即:调用了空对象的实例方法(未进行实例化)
public static void main(String[] args) {
    User user = null;
    user.print();
}

来看看上面方法的运行结果如何

Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.WhatIsNpe.main(WhatIsNpe.java:24)

Process finished with exit code 1

可以看出,抛出了NullPointerException也就是我们通常所说的NPE空指针异常,且定位在java的24行也就是我们代码中的user.print();代码,因为user对象的引用都不存在,是为null 的,所以此处的null当然无法去调用print()方法。

  • 第二种空指针情况:访问了空对象的属性
public static void main(String[] args) {
        User user = null;
        System.out.println(user.name);
}
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.WhatIsNpe$User.access$000(WhatIsNpe.java:7)
	at com.wyf.escape.WhatIsNpe.main(WhatIsNpe.java:28)

Process finished with exit code 1

结果与之前的是一样,其原因也是一样的,这里就不过多赘述。

  • 第三种情况:当数组是一个空对象时,取它的长度
public static void main(String[] args) {
        User user = new User();
        System.out.println(user.address.length);
}
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.WhatIsNpe.main(WhatIsNpe.java:37)

Process finished with exit code 1

其原因与之前两种类似,是因为User对象中的数组对象address并未进行初始化导致其数组对象的方法无法调用

  • 第四种情况:thow一个为进行初始化的自定义异常
/**
 * 自定义运行时异常
 */
public static class CustomException extends RuntimeException{}
public static void main(String[] args) {
    CustomException exception = null;
    throw  exception;
}
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.WhatIsNpe.main(WhatIsNpe.java:41)

Process finished with exit code 1

异常的参数Throwable是不接受null值的,所有抛出的异常都需要进行初始化操作

  • 第五种情况:方法的返回值为null,未做校验直接使用
public static void main(String[] args) {
    User user = new User();
    System.out.println(user.readBook().equals("1"));
}
User在读书!
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.WhatIsNpe.main(WhatIsNpe.java:45)

Process finished with exit code 1

此处的readBook()方法返回的是一个null值,故而在调用时还是正常运行的,输出了User在读书!,但是其返回值null被直接拿来当做一个String对象进行equals比较,故而抛出空指针异常

以上的五个案例,都是空指针经常出现的案例,究其根本就是调用的对象可能并未初始化,所以为了从根源避免NPE空指针异常,这里给出以下几个可行的方法:

  • 使用对象前一定要进行初始化,或者对其继续校验是否已经初始化
  • 尽可能的避免我们编写的函数有返回null 的情况,如无法避免,最好写出详细的注释信息来提示开发者
  • 接收外部传值时,如果没有特别说明某个属性一定非空,则一定要及时判断

2、赋值时自动拆箱出现空指针

首先我们要知道,需要装箱拆箱的类型有哪些,具体如下
在这里插入图片描述

基本类型包装类型
int (4字节)Integer
byte (1字节)Byte
short (2字节)Short
long (8字节)Long
float (4字节)Float
double (8字节)Double
char (2字节)Character
boolean(1字节)Boolean

除此之外,我们还得了解包装器类型也就是我们所说的包装类型基本类型之间有什么区别呢?
在这里插入图片描述
从上面的图我们可以看出来,在日常开发中,咱们去new的一个对象,都是存储在java堆中,然后通过堆栈中的应用来获得期望的对象,但对于经常用到的基础类型来说,这种做法并不是那么有效,一些包装器对象的属性通常是用不到的,白白占用内存空间,所以就出现了基本类型,这些类型直接将变量值存储在堆栈中,以此来提高存取的效率,所以这些基本类型是不具有对象性质的,而包装类型才是对应着面向对象的类型与特征。

知道了基本的概念,接下来我们就要了解一下为何自动拆箱会出现空指针,主要来源有两个:

  • 变量赋值自动拆箱出现的空指针
public static void main(String[] args) {
        // 变量赋值自动拆箱出现空指针
        Long count = null;
        long count_= count;
    }
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.UnBoxingNpe.main(UnBoxingNpe.java:10)

Process finished with exit code 1

因为我们将一个包装类型的对象赋值给了一个基本类型对象,但实则该包装类型对象并未初始化,而包装类型赋值给基本类型需要进行一个拆箱操作,但它并未初始化又何来拆箱一说?所以导致了空指针异常!

  • 方法传参时自动拆箱出现的空指针
public static void main(String[] args) {
  	// 方法传值时自动拆箱引发的空指针
    Integer x = null;
    Integer y = null;
    System.out.println(add(x, y));
}
public static int add(int x,int y){
    return x+y;
}
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.UnBoxingNpe.main(UnBoxingNpe.java:10)

Process finished with exit code 1

此处的空指针异常,是因为方法传参的参数在拆箱过程中,因拆箱目标对象为null从而导致空指针异常。

找到了自动拆箱出现空指针异常的两个场景,也看了实例,但是为什么自动拆箱会出现空指针呢?什么是自动拆箱?这就涉及到java的编译了,让我们以第一个例子来看看,自动拆箱到底是个啥

首先我们需要将之前写好的案例通过javac UnBoxingNpe.java命令来进行编译生成其对应的UnBoxingNpe.class文件,或者直接在你项目的target目录底下找一找对应class文件,如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5vJCS0zd-1622691294847)(C:\Users\wangyifan\AppData\Roaming\Typora\typora-user-images\image-20210528145938829.png)]
然后用javap -c UnBoxingNpe.class来分析字节码
在这里插入图片描述

编译分析出来的结果如上图所示,值得注意的一个点就是我们用红色框框框出来的,在赋值的过程中调用了Long.longValue:()方法,也就是将包装类对象中的值取出来,而取出来的就是java 的基本类型对象了,所以此时我们就不难理解,一个为null未进行初始化的对象在调用方法时,自然会抛出空指针异常。

那么如何避免自动拆箱引发的空指针异常呢?有以下几个建议:

  • 基本数据类型的使用优于包装器类型,优先考使用基本类型。
  • 对于不确定的包装器类型,一定要校验是否是NULL
  • 对于值为NULL的包装器类型,复值为 0

3、字符串、数组、集合在使用时出现空指针怎么办?

咱们在开发中最常使用的几个东西:字符串、数组、集合,正因为其用的多,所以也经常遇到相关的问题,空指针就是其中之一,话不多说,直接上案例

  • 字符串使用时报空指针异常
public static void main(String[] args) {
    System.out.println(stringEquals("1", null));
    System.out.println(stringEquals(null, "1"));
}

private static boolean stringEquals(String x,String y){
    return x.equals(y);
}
false
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.BasicUsageNpe.stringEquals(BasicUsageNpe.java:14)
	at com.wyf.escape.BasicUsageNpe.main(BasicUsageNpe.java:10)

Process finished with exit code 1

从输出结果可以看出,第一个比较的调用时成功了的,但第二个却报了空指针异常,究其原因还是在于null的传递,在java的equals()方法中,是可以接收NULL作为参数来进行比较的,但是,第一个调用中,NULL并不是作为参数,而是作为对象来调用equals()方法,这就与之前未初始化的对象调用其内部方法导致空指针异常的逻辑是一样的。

  • 对象数组new出来了,但元素未进行初始化
public static void main(String[] args) {
  User[] users = new User[10];
    for (User user : users) {
        user.name = "wyf";
    }
}
public static class User{
    private String name;
}
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.BasicUsageNpe$User.access$002(BasicUsageNpe.java:20)
	at com.wyf.escape.BasicUsageNpe.main(BasicUsageNpe.java:16)

Process finished with exit code 1

此处我们已经初始化了一个容量为10的User数组,并对其进行遍历,在遍历的过程中向他的name属性进行赋值,但运行时却报了空指针异常,这也是开发中常见的一个问题,原因是User数组确实初始化了,但其初始化的知识数组的容量,让该数组可以存放10个User对象,实际上数组中是没有任何User对象的,此时对他的遍历,遍历出来的自然都是NULL,而Null自然也无法去给name属性赋值。

  • List对象add一个null是不报错的,但addAll会报空指针异常
public static void main(String[] args) {
    ArrayList<User> users = new ArrayList<>();
    User user = null;
    List<User> users_ = null;
    
    users.add(user);
    System.out.println(users);
    users.addAll(users_);
    System.out.println(users);
}
public static class User{
    private String name;
}
[null]
Exception in thread "main" java.lang.NullPointerException
	at java.util.ArrayList.addAll(ArrayList.java:583)
	at com.wyf.escape.BasicUsageNpe.main(BasicUsageNpe.java:29)

Process finished with exit code 1

这个案例也是一隐藏很深的坑,我们通过控制台输出的结果不难发现,第一个为null的user成功的add进了我们的users对象中,但在addAll()方法的调用时却抛了空指针异常,而其对象同样是一个为null的User集合,让我们来看看addAll()方法的源码

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

从源码中我们可以发现,addAll()的参数是作为一个集合对象传过来的,在其方法体的第一步,就是调用形参ctoArray()方法,而当传过来的集合对象是NULL时,那么就会出现空指针异常了!

以上三个案例分别对应字符串、数组、集合的使用过程中容易碰到的空指针案例,那么我们该如何避免呢?

  • 字符串在调用equals方法时,尽量以常量作为调用对象,以变量作为比较参数
  • 数组的遍历赋值,需判断数组内有无已经实例化的对象,若没有实例化,则在遍历过程中进行实例化后再操作其属性或调用其方法
  • 集合的接口的调用需了解其原理,知其然知其所以然。

4、如何使用Optional规避空指针

什么是Optional?

**Optional**是java8新增的一个对象,它可以用来判断一个值的存在与否,其设计初衷就是用来规避空指针异常,并未考虑其作为类的字段使用,也并未实现数列化接口,故而在领域模型中要小心使用。

Optional的方法

Optional作为java8中的新对象,那么他具有哪些方法呢,以及如何正确使用呢?先让我们一起看看,如何获得一个Optional对象,获取方法有三:

  • 通过Optional.empty()获取一个空对象
Optional<User> optional = Optional.empty()

源码如下:

private static final Optional<?> EMPTY = new Optional<>();

public static<T> Optional<T> empty() {
    @SuppressWarnings("unchecked")
    Optional<T> t = (Optional<T>) EMPTY;
    return t;
}
  • 通过Optional.of()生成一个对象
Optional<User> user1 = Optional.of(user);

但此方法需要注意,其of()方法中的对象不可为null,不然就会抛出空指针异常,其底层源码如下:

public static <T> Optional<T> of(T value) {
    //调用Optional构造方法
    return new Optional<>(value);
}

private Optional(T value) {
    //调用Objects.requireNonNull()方法
    this.value = Objects.requireNonNull(value);
}

public static <T> T requireNonNull(T obj) {
    if (obj == null)
       throw new NullPointerException();
    return obj;
}

通过源码我们就可以很清楚的看到为啥of()方法的对象必须不为null了

  • 通过Optional.ofNullable()生成一个对象
Optional<User> user2 = Optional.ofNullable(user);

该方法相比于of()方法,它可以接受为null的参数,如果有值则根据具体的值来创建,如果为null则创建一个空对象,源码如下:

public static <T> Optional<T> ofNullable(T value) {
    return value == null ? empty() : of(value);
}

ofNullable()方法通过三元运算符对传入的参数进行判断,如果为空,直接调用empty()创建空对象,如果不为空则调用of()创建对象。

Optional的正确使用

先看一个错误示例:

//判断是否为空
private static void isUserEqualNull(){
    // 获取一个空的Optional实例
    Optional<User> optional = Optional.empty();
    if (optional.isPresent()){
        System.out.println("User is not Null");
    }else{
        System.out.println("User is Null");
    }
}
//调用判断方法
public static void main(String[] args) {
    isUserEqualNull();
}

如果以上述的方法使用Optional来进行单纯的非空判断,那Optional的作用于equals()方法的作用没有差异,是一种没有意义的使用方式,下面看看Optional的正确打开方式:

  • .orElse()方法:
public static void main(String[] args) {
    // 创建空对象
    User user = null;
    //创建Optional对象
    Optional<User> optional = Optional.ofNullable(user);
    // 存在则返回,为空则返回默认值
    optional.orElse(new User());
}

上述方法描述的是,先对optional对象进行判断是否为空,不为空直接返回当前optional中的值,若为空的话则为optional赋值orElse()方法中的指定对象,其源码如下:

public T orElse(T other) {
    return value != null ? value : other;
}
  • .orElseGet()方法:
public static void main(String[] args) {
    // 创建空对象
    User user = null;
    //创建Optional对象
    Optional<User> optional = Optional.ofNullable(user);
    // 存在则返回,为空则使用Supplier函数生成一个对象
    optional.orElseGet(()->SupplierUser());
}

与上面的orElse()方法相比,orElseGet()方法则是会按照提供的Supplier函数来提供相应的参数,其优点也很明显,复用性强,使用起来更加灵活,但如果对象本身不存在且提供的Supplier函数也为NULL,则会抛出空指针异常,具体源码如下:

public T orElseGet(Supplier<? extends T> other) {
    return value != null ? value : other.get();
}
  • .orElseThrow()方法:
public static void main(String[] args) {
    // 创建空对象
    User user = null;
    //创建Optional对象
    Optional<User> optional = Optional.ofNullable(user);
    // 存在则返回,为空则抛出异常,具体异常类型可自定义
    optional.orElseThrow(RuntimeException::new);
}

该方法对Optional对象进行判断,若为NULL则会抛出自定义的异常,如果不存在任何值且exceptionSupplier为Null的话,会抛出空指针异常,源码如下:

public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
    if (value != null) {
        return value;
    } else {
        throw exceptionSupplier.get();
    }
}
  • .ifPresent() 方法:
public static void main(String[] args) {
    // 创建空对象
    User user = null;
    //创建Optional对象
    Optional<User> optional = Optional.ofNullable(user);
    // 存在就去进行相应的处理,为空则不执行操作
	optional.ifPresent(User::getName);
}

此方法对Optional对象进行非空判断,若不为空,则根据提供的Consumer函数进行操作,如果值存在且consumer为空,则会抛出空指针异常:

public void ifPresent(Consumer<? super T> consumer) {
    if (value != null)
        consumer.accept(value);
}
  • .map()方法:
public static void main(String[] args) {
    // 创建空对象
    User user = null;
    //创建Optional对象
    Optional<User> optional = Optional.ofNullable(user);
    // map可以对Optional中的对象进行某种操作,其返回结果仍然是一个Optional对象
	optional.map(u -> u.getName()).orElse("WYF");
	// map可以无限的进行级联操作
	optional.map(u -> u.getName()).map(n -> n.length()).orElse(0);
}

该方法在Optional中是一个非常特殊的方法,他与*Stream()*操作中的map()方法类似,用于映射某个操作,如获取字段,获取某个字段进行加工等操作,此处的map()方法也是如此,如果存在值,则对其应用提供的映射函数,如果结果非空,则返回描述结果的Optional , 否则返回一个空的Optional ,所以他是可以无限制的进行级联操作的:

public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
    Objects.requireNonNull(mapper);
    if (!isPresent())
        return empty();
    else {
        return Optional.ofNullable(mapper.apply(value));
    }
}

5、明明try…catch了却没解决好异常

什么是异常?

异常分很多种情况,如用户输入了非法格式数据、打开不存在的文件、网络通信中断、JVM内存溢出等等,异常引起的原因有多个方面,可能是程序引起的,也可能是用户错误引起的,甚至有些是因为硬件的问题引起的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PXKj9leV-1622691294851)(C:\Users\wangyifan\AppData\Roaming\Typora\typora-user-images\image-20210602140859498.png)]
那么在java中的异常体系结构又是怎样的呢?看看下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcz6g17o-1622691294853)(C:\Users\wangyifan\AppData\Roaming\Typora\typora-user-images\image-20210602141000529.png)]
在java中,所有的异常情况都是内置在Throwable中,是所有异常最顶层的超类,而且接下来的两个分支就很重要了

  • Error分支:java中不希望被程序捕获或者程序无法处理的异常
  • Exception分支:java中希望被捕获,或者程序可以进行处理的异常
    • CheckedException:非运行时异常
    • RuntimeException:运行时异常

ERROR异常

Error类对象由java虚拟机生成并抛出

Excepton异常

Exception是java开发过程中最常见的,其子类RuntimeException,为我们定义了许多可以直接抛出的异常,如常见的空指针异常NullPointException、数组下标越界异常IndexOutOfBoundsException等等,对于这些运行时异常,我们可以选择进行捕获处理,也可以选择不进行处理;CheckedException被称之为检查性异常,这种异常在一定程度上可以预测它的发生,大多数情况下我们都会通过try...catch对其捕获进行处理,下面我们来看个例子:

public class ExceptionProcess {

    public static class User{

    }

    /**
     * 异常的一个本质就是抛出异常
     * 另一个本质便是方法抛出异常,运行时系统将寻找潜在的异常处理器,当异常处理器所能处理的异常类型与方法相符,则视为找到合适的异常处理器,
     * 然后运行时系统会遍历方法调用栈中的方法来进行处理,若没有找到合适的异常处理器,则运行时系统终止,java程序终止
     */
    private void throwException(){
        User user = null;
        // 想要执行某个逻辑程序,但由于某个参数或者当前情况不允许继续向下执行,则抛出异常
        if (user == null){
            throw new NullPointerException();
        }
    }

    /**
     * 不能捕获异常
     */
    private void canNotCatchNpeException(){
        try {
            throwException();
        } catch (ClassCastException e) {//将捕获异常类型改为类型转换异常,就捕获不到NullPointerException
            System.out.println("捕获:"+e.getMessage());
            System.out.println("捕获:"+e.getClass().getName());
        }
    }
    /**
     * 能捕获异常
     */
    private void canCatchNpeException(){
        try {
            throwException();
        } catch (ClassCastException e) {//将捕获异常类型改为类型转换异常,就捕获不到NullPointerException
            System.out.println("捕获:"+e.getMessage());
            System.out.println("捕获:"+e.getClass().getName());
        }catch (NullPointerException npe){
            System.out.println("捕获:"+npe.getMessage());
            System.out.println("捕获:"+npe.getClass().getName());
        }
    }

    public static void main(String[] args) {
        ExceptionProcess exceptionProcess = new ExceptionProcess();
        //先调用可捕获的异常
        exceptionProcess.canCatchNpeException();
        //再调用不可捕获的异常,程序终止
        exceptionProcess.canNotCatchNpeException();
    }

}
捕获:null
捕获:java.lang.NullPointerException
Exception in thread "main" java.lang.NullPointerException
	at com.wyf.escape.ExceptionProcess.throwException(ExceptionProcess.java:22)
	at com.wyf.escape.ExceptionProcess.canNotCatchNpeException(ExceptionProcess.java:31)
	at com.wyf.escape.ExceptionProcess.main(ExceptionProcess.java:57)

Process finished with exit code 1

从上面的案例中咱们可以明显的看出来,调用可捕获的方法,捕获到的也只是与异常处理器对应上类型的异常,而类型不对应的异常是无法捕获的,所以在第一个方法中输出了异常信息与异常的名称,而第二个方法调用因为无法匹配到合适的异常处理器,导致运行时程序停止。

下面咱们来列举一下java异常处理的实践原则:

  • 使用异常,而不是返回码(或类似返回码的东西),因为通常来说异常里包含的信息更加详细
  • 主动捕获检查性异常,并对异常的信息进行反馈(存日志或者标记)
  • 保持代码的整洁,一个方法中尽量不要有多个try...catch或者嵌套的try...catch
  • 捕获更加具体的异常,而不是通用的Exception,具体的异常也有利于开发者有效的识别错误信息
  • 合理的设计自定义的异常类型

6、编码中的常见异常

常见的案例有哪些?

  • 可迭代对象在遍历的同时做修改,则会抛出并发修改异常
  • 类型转换不符合JAVA的继承关系,则会抛出类型转换异常
  • 枚举在查找时,如果枚举值不存在,不会返回空,而是直接抛出异常

并发修改异常:

public class GeneralException {
    public static class User{
        private String name;
        public User(){}
        public User(String name){this.name = name;}
        public String getName(){ return name;}
    }

    /**
     * 并发修改异常
     */
    private static void concurrentModificationException(ArrayList<User> users){
        for (User user : users) {
            if (user.getName().equals("2")){
                users.remove(user);
            }
        }
    }

    public static void main(String[] args) {
        ArrayList<User> users = new ArrayList<>(
                Arrays.asList(new User("1"), new User("2"))
        );
        concurrentModificationException(users);
    }

}
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at com.wyf.escape.GeneralException.concurrentModificationException(GeneralException.java:33)
	at com.wyf.escape.GeneralException.main(GeneralException.java:54)

Process finished with exit code 1

直接使用for循环对集合元素进行删除,会触发快速失败机制,其底层逻辑是,当集合遍历时,会生成一个迭代器,迭代器会生成一个专门用于指向原集合数据的索引表,且迭代器的线程是独立出来的,当主线程中的集合对其中的元素进行删除,改变了集合大小,因为迭代器的索引表在另一个线程中,无法同步主线程的改变,从而导致并发修改异常,若要避免在迭代的过程中因为删除元素等操作而抛出异常,可以使用迭代器本身的方法来进行删除,具体如下:

private static void concurrentModificationException(ArrayList<User> users){
   Iterator<User> iterator = users.iterator();
   while (iterator.hasNext()){
        User next = iterator.next();
        if (next.getName().equals("2")){
            iterator.remove();
        }
    }
}

通过迭代器本身来进行删除,就可以避免因为索引表不对等而导致的异常,但需要注意,iterator.next()需要在iterator.remove()之后,否则仍然会报并发修改异常,此方法可行,但如果真需要删除,建议使用java8的stream().filter()进行过滤操作,安全性更高,效率高。

强制类型转换异常

在上面的例子中继续操作,我们新增WorkerManager两个对象且都继承自User对象

public static class Manager extends User{}
public static class Worker extends User{}

随后我们开始构建案例,各创建一个WorkerManager对象,但其接收的对象类型为User,在java中父类型对象是可以接收子类型对象的转型的,然后我们做一个操作,将user1user2都转换成Manager对象,此处我们可以很清楚的看出来两个user的初始化类型其实是不一样的,但接收的对象类型一样,所以有时候我们就会忽略其原本的初始化类型

public static void main(String[] args) {
    // 类型转换异常
    User user1 = new Manager();
    User user2 = new Worker();
    //强制类型转换
    Manager m1 = (Manager) user1;
    Manager m2 = (Manager) user2;
}
Exception in thread "main" java.lang.ClassCastException: com.wyf.escape.GeneralException$Worker cannot be cast to com.wyf.escape.GeneralException$Manager
	at com.wyf.escape.GeneralException.main(GeneralException.java:67)

Process finished with exit code 1

最终结果如上,会报出ClassCastException类型转换异常,那么对于这种情况我们怎么处理呢?来看看下面的两种方式:

  • 通过.getClass().getName()来获得其初始化类型的名称,进而判断能否转换
  • 通过instanceof关键字来判断当前类型是否属于被转化的类型值
public static void main(String[] args) {
    // 类型转换异常
    User user1 = new Manager();
    User user2 = new Worker();
    System.out.println(user2.getClass().getName());
    System.out.println(user2 instanceof Manager);
}
com.wyf.escape.GeneralException$Worker
false

Process finished with exit code 0

枚举查找异常

首先我们创建一个枚举类

public enum StaffType {
    RD,
    QA,
    PM,
    OP;
}

然后在刚才的类中构建一个枚举查找方法:

private static StaffType enumFind(String type){
    return StaffType.valueOf(type);
}

通过上述方法,将要查找的值传进去即可获取对应的枚举值,然后来写一下测试方法看看效果

public static void main(String[] args) {
    // 3枚举查找异常
    System.out.println(enumFind("PM"));
    System.out.println(enumFind("abc"));
}
PM
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant com.wyf.escape.StaffType.abc
	at java.lang.Enum.valueOf(Enum.java:238)
	at com.wyf.escape.StaffType.valueOf(StaffType.java:8)
	at com.wyf.escape.GeneralException.enumFind(GeneralException.java:55)
	at com.wyf.escape.GeneralException.main(GeneralException.java:77)

Process finished with exit code 1

可以看到,第一个PM是正常输出的,但第二个abc却找不着,故而抛出了IllegalArgumentException非法参数异常,那么我们如何避免呢,有四种方法可以解决:

  • 捕获异常
try {
    return StaffType.valueOf(type);
} catch (IllegalArgumentException e) {
    return null;
}
  • 循环判断
for (StaffType value : StaffType.values()) {
    if (value.name().equals(type)){
        return value;
    }
}
return null;
  • 静态map索引
/**
 * 构建了一个枚举索引
 */
private static final Map<String,StaffType> typeIndex = new HashMap<>( StaffType.values().length );
static {
    for (StaffType value : StaffType.values()) {
        typeIndex.put(value.name(), value);
    }
}

private static StaffType enumFind(String type){
    // 静态map索引,只有一次遍历过程,其缺点是map中没有对应索引时可能返回null从而导致空指针异常
    return typeIndex.get(type);
}
  • 使用开源工具 Google Guava Enums
private static StaffType enumFind(String type){
    return Enums.getIfPresent(StaffType.class,type).orNull();
}

以上四种方式均可解决枚举值的获取中可能出现的异常,具体使用那种方案因地适宜。

7、解决try…finally的资源泄露隐患

什么是资源泄露?

  • 资源释放:打开了资源,使用完之后手动释放(正常关闭了资源,不会泄露)
  • 资源泄露:打开了资源,是用完之后由于某种原因(忘记,或者出现异常等)没有手动释放资源

如果发生了资源泄露,会影响到程序的运行效率与内存空间的浪费。那我们的开发中如何保证资源的关闭呢?先来看几个常见案例

private String traditionalTryCatch() throws IOException{
    // 1、 单一资源的关闭
    String line = null;
    BufferedReader bufferedReader = new BufferedReader(new FileReader(""));
    try {
        line = bufferedReader.readLine();
    } finally {
        //对于单一资源
        bufferedReader.close();
    }
    return line;
    
    // 2、 多个资源的关闭
    // 第一个资源
    FileInputStream in = new FileInputStream("");
    try{
        // 第二个资源
        FileOutputStream out = new FileOutputStream("");
        try {
            byte[] bytes = new byte[100];
            int n;
            while ((n = in.read(bytes)) >= 0){
                out.write(bytes,0,n);
            }
        }finally {
            // 关闭第二个资源
            out.close();
        }
    }finally {
        // 关闭第一个资源,顺序不能错,若顺序有错会导致IO异常,需要按照资源打开顺序的逆序进行关闭
        in.close();
    }
}

上面这个案例中咱们做了两个小demo,分别是关闭单一资源与关闭多资源的demo,从案例中就可以看出,在单一资源的关闭案例中,由于finally的存在,在系统不出错的前提下,我们使用的资源会被确保关闭,但在多资源情况下,我们还额外的需要注意资源关闭的顺序,且多资源情况下的try...finally多层嵌套,会显得代码非常的冗余,阅读体验极差,而且此处还有**一个需要注意的问题,就是在try中可能会发生异常,但finally中也可能因为某些原因发生异常,此时finally中的异常就会覆盖掉之前try中的异常,导致异常在排查的过程中十分困难。**咱们看看如何覆盖的

先自定义一个咱们自己的异常MyException

public class MyException extends Exception{
    public MyException(){
        super();
    }
    public MyException(String msg){
        super(msg);
    }
}

再定义一个自动关闭的类来抛出异常

public class AutoClose implements AutoCloseable{

    @Override
    public void close() {
        System.out.println(">>>>调用close()");
        throw new RuntimeException("close()方法异常");
    }

    public void work() throws MyException{
        System.out.println(">>>>调用work()");
        throw new MyException("work()方法异常");
    }
}

最后使用main方法调用一下看看结果

public static void main(String[] args) throws MyException {
    AutoClose autoClose = new AutoClose();
    try {
        //先运行work让他抛出异常
        autoClose.work();
    } finally {
        //再运行close让他抛出异常
        autoClose.close();
    }
}
>>>>调用work()
>>>>调用close()
Exception in thread "main" java.lang.RuntimeException: close()方法异常
	at com.wyf.escape.ty_with_resources.AutoClose.close(AutoClose.java:11)
	at com.wyf.escape.ty_with_resources.Main.main(Main.java:78)

Process finished with exit code 1

根据上述案例与结果我们可以看出,方法正常启动并按顺序调用了work()close()两个方法,但最终抛出的异常只有close的异常,而work的异常就被覆盖掉了。对于try...finally我们做出如下总结:

  • 对单个资源的操作基本不会用问题,出现异常的概率较低
  • 当多个资源同时被操作的时候,代码会变得冗长,且存在资源泄露的风险
  • 多个异常情况下可能存在异常的覆盖,导致我们的调试工作变得更加困难

改进方案:使用tyr-with-resources语法来代替try-finally,更加方便且更不容易出错,具体案例如下:

private String newTryWithResources() throws IOException{
    // 1、 单个资源的使用与关闭
    /*try(BufferedReader bufferedReader = new BufferedReader(new FileReader(""))){
        return bufferedReader.readLine();
    }*/
    
    // 2、 多个资源的使用与关闭
    try (FileInputStream in = new FileInputStream("");  //第一个资源
         FileOutputStream out = new FileOutputStream("")//第二个资源
    ){
        byte[] bytes = new byte[100];
        int n;
        while ((n = in.read(bytes)) != -1){
            out.write(bytes,0,n);
        }
    }
    return null;
}

将代码改造成上面的样子后,我们可以发现,我么将资源的开启放在的tyr()的括号中,而资源的使用放在了大括号中,这样在资源使用完毕后,程序会自动的将资源关闭,不需要我们手动关闭,这样避免了代码的冗余,也避免了资源关闭顺序错误而导致异常,此外对于异常覆盖的问题,我们看看下面的案例:

public static void main(String[] args) throws MyException {
    try(AutoClose autoClose = new AutoClose()){
        autoClose.work();
    }
}
>>>>调用work()
>>>>调用close()
Exception in thread "main" com.wyf.escape.ty_with_resources.MyException: work()方法异常
	at com.wyf.escape.ty_with_resources.AutoClose.work(AutoClose.java:16)
	at com.wyf.escape.ty_with_resources.Main.main(Main.java:81)
	Suppressed: java.lang.RuntimeException: close()方法异常
		at com.wyf.escape.ty_with_resources.AutoClose.close(AutoClose.java:11)
		at com.wyf.escape.ty_with_resources.Main.main(Main.java:82)

Process finished with exit code 1

很明显的,我们的代码调用变得更加简洁,且调用的顺序也不曾改变,符合我们的预期,最主要的是,我们的方法主体work()抛出的异常没有被自动调用的close()方法抛出的异常给覆盖掉,这让我们在代码的debug过程中可以更加准确的定位到问题所在!

PS:以上笔记根据慕课网《给java开发者的实操避坑指南》课程学习记录,图片是从自己的代码案例与视频中截取,如有侵权请联系作者删除,如需转载使用请标明出处,在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值