Java开发常出错5颗星——空指针和异常
常犯指数5颗星
空指针
空指针概念及样例
- 什么是空指针(java.lang.NullPointExcetion)?
空:内存地址
指针:引用
异常: 运行时
private static class User{
private String name;
private String[] address;
public void print(){
System.out.println("This is User class!");
}
public String readBook(){
System.out.println("user Read Imooc Escape");
return null;
}
}
public static class CustomException extends RuntimeException{
}
public static void main(String[] args) {
//第一种情况,调用了空对象的实例方法
// User user = null;
// user.print();
//第二种情况: 访问了空对象的属性
// User user = null;
// System.out.println(user.name);
//第三种情况: 当数组是一个空对象的时候,取它的长度
// User user = new User();
// System.out.println(user.address.length);
// 第四种情况: null 当做Throwable的值
// CustomException exception = null;
// throw exception;
//第五种情况:方法的返回值是null,调用方直接去使用
User user =new User();
System.out.println(user.readBook().concat("Oracle"));
}
- 由于疏忽造成的空指针问题
- 针对与空指针的场景,有哪些方式可以避免发生
- 使用之前一定要初始化,或检查是否初始化
- 尽量避免在函数中返回Null,或给出详细的注释(良好的编程习惯)
- 外部传值, 除非有明确的说明(非Null),否则一定要及时判断
......
赋值时自动拆箱出现空指针
- 为什么要有包装器类型
为了让基本类型具有对象的性质,丰富了基本类型的操作
常见的由于自动拆箱引发的空指针案例
private static int add(int x,int y){
return x+y;
}
private static boolean compare(long x, long y){
return x >= y;
}
public static void main(String[] args) {
// 1. 变量赋值自动拆箱出现的空指针
// javac UnboxingNpe.java
//javap -c UnboxingNpe.class
Long count = null;
long count_ = count;
//2. 方法传参时自动拆箱引发的空指针问题
// Integer left = null;
// Integer right = null;
// System.out.println(add(left,right));
//3. 用于大小比较的场景
// Long left = 10L;
// Long right = null;
// System.out.println(compare(left,right));
- 自动拆箱引发的空指针
变量赋值自动拆箱出现的空指针
方法传参时自动拆箱出现的空指针
- 规避自动拆箱引发空指针的建议
- 基本数据类型优于包装器类型,优先考虑使用基本类型
- 对于不确定的包装器类型,一定要校验是否是Null
- 对于值为Null的包装器类型,赋值为0
字符串、数组、集合在使用时出现空指针
- 寻找代码案例中出现的场景
字符串使用equals时报空指针错误
对象数组虽然new出来了,但是如果没有初始化,一样会报空指针错误
List对象add null不报错,但是all All 不能添加null,否则会报空指针错误
// 1. 字符串使用equals 可能会报空指针错误
//false
// System.out.println(stringEquals("xyz",null));
// // npe
// System.out.println(stringEquals(null,"xyz"));
// 2. 对象数组new出来了, 但是元素没有初始化
// User [] user = new User[10];
// for (int i = 0; i != 10; i++) {
// // 对user 进行初始化 就不会抛出异常了
// user[i] = new User();
// user[i].name = "imooc-"+i;
// }
//3. List对象 allAll 传递null会抛出空指针
List<User> users = new ArrayList<>();
User user = null;
List<User> users_ = null;
users.add(user);
users.addAll(users_);
查看ArrayList AddAll源码,由于addAll调用了传入对象的toArray()方法,引起空指针
使用Optional规避空指针时,需要注意些什么
- Java8 提供的optional
是什么、定义了哪些方法、日常使用
- 日常的使用方法有问题吗?
与直接判断是否null几乎一样,所以使用新的API意义不大,
认识到orElse、orElseGet、map等方法的妙用
get、ifPresent这样的方法更应该看做是私有(不要直接去使用)方法
- 什么是Optional
public class OptionalUsage {
public static class User{
private String name;
public String getName() {
return name;
}
}
private static void isUserEqualsNull(){
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");
}
}
private static User anoymos(){
return new User();
}
public static void main(String[] args) {
// 没有意义的使用方法
isUserEqualsNull();
User user = null;
Optional<User> optionalUser = Optional.ofNullable(user);
// orElse 存在即返回, 空则提供默认值
optionalUser.orElse(new User());
//orElseGet 存在即返回, 空则由函数去返回
optionalUser.orElseGet( ()->anoymos());
//存在即返回, 否则抛出异常
optionalUser.orElseThrow(RuntimeException::new);
//存在才去做相应的处理
optionalUser.ifPresent(u -> System.out.println(u.getName()));
// map 可以对Optional中的对象执行某种操作,且会返回一个optional对象
optionalUser.map(u -> u.getName()).orElse("anymos");
//map是可以无线级联操作的
optionalUser.map(u->u.getName()).map(name ->name.length()).orElse(0);
}
异常
异常的概念
- 用户输出非法数据
- 打开的文件不存在
- 网络通信链接中断
- JVM内存溢出
Java异常处理类
Error:OOM
Exception:RunTimeException, 例如空指针异常,数组下标越界。属于非检查时异常。
Java异常处理事件原则
- 使用异常,而不是返回码(或类似),因为异常会更加详细
- 主动捕获检查性异常,并对异常信息进行反馈(日志或标记)
- 保持代码整洁,一个方法中不要有多个try catch 或者嵌套的try catch(尝试将方法拆开解决)
- 捕获更加具体的异常,而不是通用的Exception
- 合理的设计自定义的异常类(更加明确的异常信息)
编码中常见的异常(并发修改、类型转换、每局查找及其解决办法)
- 常见的案例有哪些
可迭代对象在遍历的同时做修改,则会报并发修改异常
类型转换不符合Java的继承关系,则会报类型转换异常
枚举在查找时,如果枚举值不存在,不会返回空,而是直接抛出异常
- 直接使用for循环移除元素触发 并发修改异常
/**
* 并发修改异常指遍历时进行修改: 快速失败机制 ,索引表中指针找不到元素
* @param users userList
*/
private static void concurrentModificationException(ArrayList<User> users){
//直接使用for循环会触发并发修改异常
for(User user:users){
if(user.getName().equals("imooc")){
users.remove(user);
}
}
//使用迭代器则没有问题
// Iterator<User> iter = users.iterator();
// while (iter.hasNext()){
// //next 必须在remove之前,否则触发快速失败
// User user = iter.next();
// if(user.getName().equals("imooc")){
// iter.remove();
// }
//
// }
// 更好的实现该功能应该使用jdk 1.8中filter方法进行过滤
}
fail-fast原理
对于集合如List、Map,可以通过迭代器来遍历。而Iterator其实只是一个接口,需要调用具体的集合类中的内部类去实现Iterator并实现相关方法。ConcurrentModificationException都是在操作Iterator抛出的异常。
通过调用checkForComodification比较“expectedModCount”和“modCount”大小,而在执行remove动作后,触发了modCount++操作。
而在最初创建ArrayList时,expectModCount = modCount,因此触发了快速失败。
详细讲解见博客【并发基础】Java中的fail-fast(快速失败)机制
对于枚举类查找的常见策略
- 使用try-catch包裹
- 对枚举类values进行遍历
- 静态map索引,只有一次map的索引过程
- 使用Google Guava Enums,需要引入相关的依赖
// 1. 最普通、最简单的实现
// try{
// return StaffType.valueOf(type);
// }catch (IllegalArgumentException ex){
// return null;
// }
// 2. 改进的实现, 但是效率不高
// for(StaffType value:StaffType.values()){
// if(value.name().equals(type)){
// return value;
// }
// }
// return null;
// 3. 静态map索引, 只有一次map索引的过程
private static final Map<String,StaffType> typeIndex = new HashMap<>(
StaffType.values().length
);
static {
for (StaffType value : StaffType.values()) {
typeIndex.put(value.name(),value);
}
}
// return typeIndex.get(type);
// 4. 使用Google Guava Enums,需要相关的依赖
return Enums.getIfPresent(StaffType.class,type).orNull();
解决使用try finally的资源泄露隐患
- 资源释放:打开了资源,使用完之后手动释放(关闭)
- 资源泄露:打开了资源,使用完之后由于某种原因没有手动释放
try finally的问题及改进方案
对单个资源的操作基本不会有问题
当同时操作多个资源时,代码冗长,且存在资源泄露的风险
try-with-resource 不仅比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();
}
return null;
}
这样做风险在于后一个关闭的异常会隐藏前一个出现的异常信息,代码冗长
java 7 引入的try-resources
/**
* java7 引入的try with resource 实现自动的资源关闭
*
* @return
* @throws IOException
*/
private String newTryWithResources() throws IOException {
//1.单个资源的使用和关闭
// try(BufferedReader br = new BufferedReader(new FileReader(""))){
// return br.readLine();
// }
// 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;
}
只需要专注于自身业务逻辑即可,会自动对资源进行关闭操作,且不会覆盖异常信息。