Java常见错误(1)

对于空指针的场景,有哪些方法可以避免问题

  1. 使用之间一定要初始化,或检查是否可以初始化

  2. 尽量避免函数中返回null。或给出详细的注释

  3. 外部传值,除非有明确的说明(非null),否则一定要及时判断

    ....

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

为什么需要包装器类型 

在向容器添加数据时,因为容器里面存的是object,不能是基本数据类型,只能存包装类型,使用包装类型就会触发包装器类型

规避自动拆箱引发空指针的建议

基本数据类型优于包装器类型,优先考虑使用基本类型

对于不确定的包装器类型,一定要校验是否是为null

对于值为null的包装器类型,赋值为0

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

场景:

字符串使用equals时报空指针错误

对象数组虽然new出来了,但是如果没有初始化,一样会报空指针错误

User[] users = new User[10];
for(int i=0;i != 10 ;++i){
    users[i] = new User();
    users[i].name = "im" +i
}
对象数组new出来,要初始             

list对象 add null 不报错,但是addAll不能添加null,否则会报空指针错误

List<User> users = new ArrayList<User>();
User user = null;
List<User> useraAll = null;
users.add(user);
Users.addAll(userAll);空指针异常

使用Optional规避空指针,需要注意什么?

什么是Optional

Optional<T> 容器类

日常使用方法?

与直接判断是否是null几乎一样,所以,使用新的的API意义不大

//这是没有意义的
private static void isUserEqualNull() {
​
        User user = null;
        if (user != null) {
            System.out.println("User is not null");
        } else {
            System.out.println("User is null");
        }
​
        Optional<User> optional = Optional.empty();
        if (optional.isPresent()) {
            System.out.println("User is not null");
        } else {
            System.out.println("User is null");
        }
    }

认识到orElse,orElseGet,map等方法的妙用

//ofNullable 方法:如果user为空,返回一个空的optional实例,如果user不为空就返回一个user实例
User user = null
Optional<User> optionalUser = Optional.ofNullable(user);
​
//存在即返回,空则提供默认值
//也就是如果optionalUser为空的话,就会返回一个默认值(空的user对象,自己new的user对象),
optionalUser.orElse(new User());
//存在即返回,空则由函数去返回
private static User anoymos() {
        return new User();
    }
optionalUser.orElseGet(() -> anoymos());
//存在即返回,否则抛出异常
optionalUser.orElseThrow(RuntimeException::new);
// 存在才去做相应的处理 如果不存在,这行代码不会执行任何事情
optionalUser.ifPresent(u -> System.out.println(u.getName()));
// map 可以对 Optional 中的对象执行某种操作, 且会返回一个 Optional 对象
//首先对optionalUser获取name属性,然后会被map方法封装成optional对象,这里这个对象是string类型,
//如果这个对象也是空,使用orElse可以返回一个默认值
 optionalUser.map(u -> u.getName()).orElse("anymos");
//map是可以无线级联操作
//比如也就是封装成一个string类型,然后再根据string的长度变成int类型对象,再来判断是否为空
optionalUser.map(u -> u.getName()).map(name -> name.length()).orElse(0);

get,ifPrensent这样的方法更应该看作是私有(不要去直接使用)方法

为什么使用try catch没有解决好异常

当catch不能捕获到对应异常时,会向上抛出异常,直到main上处理成错误

一定要去正确的抛出异常和捕获异常

​
/**
 * <h1>Java 异常处理</h1>
 * */
@SuppressWarnings("all")
public class ExceptionProcess {
​
    private static class User {}
​
    /**
     * <h2>Java 异常本质 -- 抛出异常</h2>
     * */
    private void throwException() {
​
        User user = null;
        // ....
        if (null == user) {
            throw new NullPointerException();
        }
    }
​
    /**
     * <h2>不能捕获空指针异常</h2>
     * */
    private void canNotCatchNpeException() {
​
        try {
            throwException();
        } catch (ClassCastException cce) {
            System.out.println(cce.getMessage());
            System.out.println(cce.getClass().getName());
        }
    }
​
    /**
     * <h2>能够捕获空指针异常</h2>
     * */
    private void canCatchNpeException() {
​
        try {
            throwException();
        } catch (ClassCastException cce) {
            System.out.println(cce.getMessage());
            System.out.println(cce.getClass().getName());
        } catch (NullPointerException npe) {
            System.out.println(npe.getMessage());
            System.out.println(npe.getClass().getName());
        }
    }
​
    public static void main(String[] args) {
​
        ExceptionProcess process = new ExceptionProcess();
        process.canCatchNpeException();
        process.canNotCatchNpeException();
    }
}

Java异常处理实践原则

使用异常,而不是返回码(或类似)因为异常会更加详细记录

主动捕获检查性异常,并对异常信息进行反馈(日志或者标记)

保持代码整洁,一个方法中不要多个try catch或者嵌套try catch

捕获更加具体的异常,而不是通用的exception

合理的设计自定义的异常类

编码中常见的异常(并发修改,类型转换,枚举查找)及其解决办法

常见的案例有那些

可迭代对象在遍历的同时做修改,则会报并发修改异常

//直接使用 for 循环会触发并发修改异常
for (User user : users) {
    if (user.getName().equals("imooc")) {
             users.remove(user);
    }
}
这种机制时java中fail-fast机制(快速失败机制),迭代器是工作在独立的线程中,并且拥有一个互斥锁,迭代器在创建之后会建立一个指向原来对象的单链索引表,当原来的数量发生变化,索引表的内容不会同步改变,所以当索引指针往后移动的时候,就找不到要迭代的对象

应该使用迭代器

// 使用迭代器则没有问题
Iterator<User> iter = users.iterator();
while (iter.hasNext()) {
    User user = iter.next();
    if (user.getName().equals("imooc")) {
        iter.remove();
    }
}
//这里需要注意的是:iter.next()要先执行,不然也会出现刚才fail-fast机制,原理和上面一样

类型转化不符合java继承关系,则会报类型转换异常

枚举在查找时,如果枚举值不存在,不会返回空,而是直接抛出异常

解决办法:

/**
 * <h1>员工类型枚举类</h1>
 * */
public enum StaffType {
​
    RD,
    QA,
    PM,
    OP;
}
1. 最普通、最简单的实现
        try {
            return StaffType.valueOf(type);
        } catch (IllegalArgumentException ex) {
            return null;
        }
2. 改进的实现, 但是效率不高
for (StaffType value : StaffType.values()) {
    if (value.name().equals(type)) {
        return value;
    }
 3. 静态 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){
    return typeIndex.get(type);
}
   
// 4. 使用 Google Guava Enums, 需要相关的依赖
private static StaffType enumFind(String type){
    return Enums.getIfPresent(StaffType.class, type).orNull();
}

解决使用try finally的资源泄露隐患

资源释放:使用完之后手动释放(关闭)

资源泄露:打开了资源,使用完之后由于某种原因(忘记,出现异常等等)没有手动释放

  private String traditionalTryCatch() throws IOException {
​
        // 1. 单一资源的关闭
       String line = null;
       BufferedReader br = new BufferedReader(new FileReader(""));
       try {
           line = br.readLine();
       } finally {
            br.close();
       }
       return line;
  }
​
        // 2. 多个资源的关闭
        // 第一个资源 输入流
        InputStream in = new FileInputStream("");
        try {
            // 第二个资源 输出流
            OutputStream out = new FileOutputStream("");
            try {
                byte[] buf = new byte[100];
                int n;
​
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }
​

  /**
     * <h2>java7 引入的 try with resources 实现自动的资源关闭</h2>
     * */
    private String newTryWithResources() throws IOException {
​
        // 1. 单个资源的使用与关闭
//        try (BufferedReader br = new BufferedReader(new FileReader(""))) {
//            return br. ();
//        }
​
        // 2. 多个资源的使用与关闭
        try (FileInputStream in = new FileInputStream("");
             FileOutputStream out = new FileOutputStream("")
        ) {
            byte[] buffer = new byte[100];
            int n = 0;
            while ((n = in.read(buffer)) != -1) {
                out.write(buffer, 0, n);
            }
        }
        return null;
    }

///

public class AutoClose implements AutoCloseable {
​
    @Override
    public void close() {
        System.out.println(">>> close()");
        throw new RuntimeException("Exception in close()");
    }
​
    public void work() throws MyException {
        System.out.println(">>> work()");
        throw new MyException("Exception in work()");
    }
}
​
public class MyException extends Exception {
​
    public MyException() {
        super();
    }
​
    public MyException(String message) {
        super(message);
    }
}
​

    public static void main(String[] args) throws MyException {
​
//        AutoClose autoClose = new AutoClose();
//        try {
//            autoClose.work();
//        } finally {
//            autoClose.close();
//        }
​
        try (AutoClose autoClose = new AutoClose()) {
            autoClose.work();
        }
    }

BigDecimal出错?都是精度的锅

bigDecimal核心是精度,精度如果不匹配,结果大概率不会符合预期

    /**
     * <h2>scale 需要与小数位匹配</h2>
     * */
    private static void scaleProblem() {
​
        BigDecimal decimal = new BigDecimal("12.222");
//        BigDecimal result = decimal.setScale(12);
//        System.out.println(result);
​
        BigDecimal result = decimal.setScale(2, BigDecimal.ROUND_HALF_UP);
        System.out.println(result);
    }
// BigDecimal.ROUND_HALF_UP 四舍五入
​
    /**
     * <h2>BigDecimal 做除法时出现除不尽的情况</h2>
     * */
    private static void divideProblem() {
​
//        System.out.println(new BigDecimal(30).divide(new BigDecimal(7)));
        System.out.println(
                new BigDecimal(30).divide(new BigDecimal(7), 2,
                        BigDecimal.ROUND_HALF_UP)
        );
    }
    /**
     * <h2>精度问题导致比较结果和预期的不一致</h2>
     * */
    private static void equalProblem() {
​
        BigDecimal bd1 = new BigDecimal("0");
        BigDecimal bd2 = new BigDecimal("0.0");
​
        System.out.println(bd1.equals(bd2)); //false 精度不同
        System.out.println(bd1.compareTo(bd2) == 0); //true
    }

SimpleDateFormat使用上常见的坑

可以解析大于/等于它定义的时间精度,但是不能解析小于它定义的时间精度

它是线程不安全的,在多线程环境下操作,会抛异常(解决思路:定义为局部变量,使用threadlocal,使用synchronizez)

/**
 * <h2>SimpleDateFormat 可以解析大于/等于它定义的时间精度</h2>
 * */
private static void formatPrecision() throws Exception {
​
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
​
    String time_x = "2020-03-01 10:00:00";
    String time = "2020-03";
​
    System.out.println(sdf.parse(time_x));
    System.out.println(sdf.parse(time)); //抛出异常
}
/**
 * <h2>SimplleDateFormat 存在线程安全问题</h2>
 * */
private static void threadSafety() {
​
    SimpleDateFormat sdf = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
​
    ThreadPoolExecutor threadPoolExecutor =
            new ThreadPoolExecutor(
            10, 100, 1, TimeUnit.MINUTES,
            new LinkedBlockingDeque<>(1000)
    );
​
    while (true) {
​
        threadPoolExecutor.execute(() -> {
            String dateString = "2020-03-01 10:00:00";
            try {
                Date parseDate = sdf.parse(dateString);
                String dateString2 = sdf.format(parseDate);
                System.out.println(dateString.equals(dateString2));
            } catch (ParseException ex) {
                ex.printStackTrace();
            }
        });
​
    }
}
//抛出异常
//原因是SimpleDateFormat里面继承DateFormat,里面维护了一个Calendar对象,我们调用的parse方法,以及format方法都是由Calendar这个引用去存储的,即使把DateFormat变成一个全局的或者是static,Calendar引用也会被共享,而Calendar内部没有实现线程安全机制,所以全局的SimpleDateFormat也不是一个线程安全的对象

解决方法

1.定义为局部变量

线程有独立的栈空间,局部变量保存在栈中,局部变量就不受多线程的影响,这是最简单也是最常见的方式

2.使用ThreadLocal

ThreadLocal使得线程有独立的数据,互相访问不受干扰,但是需要额外的维护,有维护成本

3.使用同步锁synchronizez

性能开销相对于前两种是非常高的

最好使用一些开源的工具类或者包

for循环,粘上集合出大问题

传统的for循环是怎样的

如果是数组:通过数组长度,建立索引

如果是集合:迭代器

传统的for循环存在怎样的弊端与劣势呢?

我需要的是可迭代对象中的元素,并不需要元素的索引

在嵌套环境下(多个可迭代对象),需要小心迭代器对象的正确性

public class ForeachOptimize {
​
    private static Collection<Integer> left =
            Arrays.asList(1, 2, 3, 4, 5, 6, 7);
    private static Collection<Integer> right =
            Arrays.asList(1, 2, 3, 4, 5);
     /**
     * <h2>集合迭代经常犯的错误</h2>
     * */
    private static void wrongIterator() {
        // 传统方式 - 使用索引
        int[] xyz = new int[]{1, 2, 3, 4, 5};
        for (int i = 0; i != xyz.length; ++i) {
            System.out.println(xyz[i]);
        }
        // 传统方式 - 迭代器
        for (Iterator<Integer> i = left.iterator(); i.hasNext(); ) {
            System.out.println(i.next());
        }
        //嵌套迭代容易出现问题
        for (Iterator<Integer> l = left.iterator(); l.hasNext(); ) {
            for (Iterator<Integer> r = right.iterator(); r.hasNext(); ) {
                System.out.println(l.next() * r.next());
            }
        }
        //这里输出 :1,4,9,16,25,6,14,后面抛出异常NoSuchElementException
        //原因是l.next()在内部循环里面,被right调用了多次,一直到没有元素就抛出异常。这个left被内部调用过早耗尽。
        //正确的用法, 嵌套迭代
        for (Iterator<Integer> l = left.iterator(); l.hasNext(); ) {
            Integer tmp = l.next();
            for (Iterator<Integer> r = right.iterator(); r.hasNext(); ) {
                System.out.println(tmp * r.next());
            }
        }
        
    }
}

for-each优于for

只专注于迭代对象自身,而不考虑多余的索引

任意实现Iterable接口的对象,都可以使用for-each循环处理

进一步扩展:Java8 Iterable.forEach在一起场景下会比for-each更加方便

for (Integer l : left) {
    for (Integer r : right) {
        System.out.println(l * r);
    }
}
private static void square(int value) {
        System.out.println(value * value);
    }
// Java8 Iterable.forEach vs for-each
for (Integer l : left) {
    square(l);
}
​
left.forEach(l -> square(l));
left.forEach(ForeachOptimize::square);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值