对于空指针的场景,有哪些方法可以避免问题
-
使用之间一定要初始化,或检查是否可以初始化
-
尽量避免函数中返回null。或给出详细的注释
-
外部传值,除非有明确的说明(非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);