Java易错点总结60例,这不得看看?

  1. 对于多线程场景下的计数操作,应该使用AtomicIntegerAtomicLong保证线程安全。不能使用i++。

  2. volatile关键字可以保证线程之间的可见性,但是无法保证操作的原子性。对于volatile i = 0; i++操作,需要加锁或使用CAS操作来保证原子性。

  3. Object.waitObject.notify方法必须在synchronized关键字修饰的代码块中使用。因为wait和notify的语义是:线程在获取到锁之后暂时释放锁(wait)或通知(notify)其他线程可以竞争锁了。所以必须使用synchronized关键字修饰。

  4. 对于中断异常InterruptedException,不能忽略,需要积极地响应中断,释放资源并准备退出线程。参考\# 聊聊JDK推荐的线程关闭方式[1]

  5. Thread.interrupted 方法用于返回当前线程的中断状态,并且在此过程中会清除中断状态。如果只想查询中断状态而不清除中断状态,可以使用 Thread.isInterrupted 方法。

  6. 在使用 new Thread() 创建线程对象时,只是在 Java 中创建了一个对象。执行start方法才真正在操作系统中创建线程并开始执行线程。Java 的线程是基于操作系统内核级线程实现的,而不是虚拟线程。

  7. 避免使用 Thread.stop 和 Thread.resume 方法停止线程,因为它们已经被标记为不推荐使用的方法。关于如何优雅地终止线程,请参考 # 聊聊JDK推荐的线程关闭方式[2]

  8. ThreadPoolExecutor在达到CoreSize线程数时,会将请求放入队列中,如果队列已满,则尝试增加线程数到MaxSize。因此,无界队列永远无法达到MaxSize,并且永远不会触发拒绝策略,但存在OOM的风险。

  9. 使用Executors.newFixedThreadPool创建固定线程数的线程池时,使用无界队列。在极端情况下,可能导致OOM。因此,在使用此API创建线程池时要谨慎。

  10. ThreadPoolExecutor线程池的默认拒绝策略是抛出异常,这会导致请求处理失败。可以使用CallerRunsPolicy策略,让提交线程处理任务,以避免请求处理失败的情况发生。

  11. ExecutorService的execute方法是同步提交任务的,而submit方法是异步提交任务的。

  12. synchronized是非公平模式的锁。如果需要使用公平模式的锁,可以使用ReentrantLock。公平模式下,先申请锁的先获取锁;非公平模式下,申请锁时会先尝试CAS加锁,如果加锁失败则排队等待获取锁,这样提高了锁切换的速度,但失去了公平性。

  13. synchronized只支持阻塞模式的锁申请。如果需要非阻塞模式,请使用ReentrantLock

  14. 在使用 CountDownLatch 时,即使是在异常场景下,也要确保进行 countDown 操作,否则等待线程的 await 方法可能永远无法被唤醒。建议 countDown 操作放到 finally 代码块中。

  15. Thread.sleep是一个用于将当前线程置为阻塞状态的方法,它会暂时让出CPU的调度权,直到指定的时间到达或者被Thread.interrupt()方法中断。相比之下,Thread.yield方法仅仅是让出CPU的调度权,但操作系统下一次调度时会正常考虑该线程。另一方面,Object.wait()方法通常用于在synchronized同步代码块中,它会暂时释放锁,并等待其他持有该锁的线程来唤醒它。在此期间,线程进入阻塞状态。Thread.join 是当前线程等待子线程运行结束才能继续执行

  16. 一个线程发生OOM ,只会导致这个线程抛出ERRO,进行退出执行。不会影响其他线程。如果想要在OOM后,退出进程,需要添加JVM 启动参数。【 -XX:+HeapDumpOnOutOfMemoryError参数表示当JVM发生OOM时,自动生成DUMP文件】;【-XX:+ExitOnOutOfMemoryError 在程序发生OOM异常时,强制退出】; 【XX:+CrashOnOutOfMemoryError 在程序发生OOM异常时,强制退出,并生成Crash日志】;

  17. Integer包装器类型若为 null 值转换为 int 会产生空指针异常。应先判断包装器类型是否为非 null,再转换为基本类型。

  18. 在使用 Double 和 Float 时会丢失精度,例如 1.0 - 0.9 期望是0.1,结果是 0.09999999999999998。因此,在涉及金额计算时,最好将金额转换为整型,例如将 1.1 元表示为 110 分。

double result = 1.0 - 0.9
System.out.println(result)

  1. 在 Java 8 中,需要小心使用泛型和重载,以避免类型转换异常。以下代码会抛出异常 String.valueOf(getSimpleString())。如果指定具体类型,则不会出错,例如 String.valueOf((String)getSimpleString())。更详细的内容请参考 \# 升级Java 8以后,上线就翻车了。这次是泛型的锅[3]

@Test
public void test2() {
   System.out.println(String.valueOf(getSimpleString()));
}

public static <T> T getSimpleString() {
   return (T) "121";
}

  1. break默认情况下可以跳出当前层的循环。然而,若需要跳出最外层的循环,我们需要为for循环取一个名字,并且在break语句中指定要跳出哪一层的循环。举个例子,下面的例子中使用了break loop1,用来指定跳出最外层的循环。

int m = 10
int n = 4

loop1:
for (int i = 0
    loop2:
    for (int j = 0
        break loop1
    }
    System.out.println("i:" + i)
}

  1. 内部类中无法直接修改外部引用的值,此时我们可以采用数组作为容器来传递数值。若在内部类中需要修改suc的值,我们可以将其声明为一个数组类型,并通过修改该数组元素的值来实现目的。

int[] suc = new int[1]

new Thread(new Runnable() {
   @Override
   public void run() {
      int i = 0
      i++
      suc[0] = i
   }
}).start()

  1. 为了判断字符串是否相等,我们应该使用String.equals或者Objects.equals方法,而不能简单地使用 == 运算符。

  2. 在处理包装器类型Integer、Long时,不建议使用"=="来判断它们是否相等,最好采用Objects.equals方法来判断相等。因为"=="判断相等的方式是根据对象的内存地址来判断的。(尽管在-127到128的范围内,由于对象池缓存的存在,可以使用"=="进行判等,但为了统一起见,最好还是使用Objects.equals来判断相等)

  3. 为了避免出现乱码,调用String.getBytes(Charset.forName("utf-8"))时应明确指定字符集,因为不同的操作系统环境有不同的默认字符集。

  4. 使用StringUtils.isEmpty判断字符串是否为null,使用StringUtils.isBlank判断字符串是否为空白或者为null。

  5. String是不可变对象,调用substring、replace等方法不会修改原始的String对象。如果需要修改字符串,应该使用线程安全的StringBuilder。在单线程环境下,也可以使用StringBuffer。

  6. 枚举类型的相等判断可以使用"=="。例如,statusEnum == StatusEnum.Success可以判断枚举类型statusEnum是否等于StatusEnum.Success

  7. Java中的基本类型和引用类型都是通过值传递的。因此以下代码尝试交换a、b两个基本类型的值的方法是错误的。

public void testSwap() {
   int a = 1
   int b = 2
   swap(a, b)
}
public void swap(int a, int b) {
   int c = a
   a = b
   b = c
}

  1. 不建议在 boolean 类型的命名中使用 is 前缀,因为 getter 方法默认会自动带上 is 前缀。如果使用 is 前缀,可能会导致 JSON 等解析异常。

  2. 在使用 switch 语句时,只能使用常量,不能使用变量。

  3. 使用 switch case 语句时,记得和 break 关键字搭配使用。执行完一个 case 后的语句后,流程控制会转移到下一个 case 继续执行。如果你只想执行这一个 case 语句,不想执行其他 case,那么就需要在这个 case 语句后面加上 break,以跳出 switch 语句。

  4. 如果要使用 Object.clone 方法,需要确保类型实现了 Cloneable 接口,否则调用 clone 方法会抛出 CloneNotSupportedException 异常。

  5. Object.clone 方法属于浅层拷贝,即基础数据类型会被复制,而引用类型则是共享的。如果需要实现深层拷贝,可以考虑使用 BeanUtils.copyProperties 方法。

  6. 在使用 List 进行 for 循环遍历时,不能在遍历期间直接删除元素。如果需要删除元素,可以使用 stream 表达式进行过滤,或者使用 iterator 删除元素。

Iterator<Integer> it = numbers.iterator();  
        while(it.hasNext()) {  
            Integer i = it.next();  
            if(i < 10) {    
                it.remove();  
            }  
        }

  1. 在服务正式运行时,为了记录日志,我们不能使用e.printStackTrace()System.out.println打印日志,而是应该使用log4j Logger。因为标准输入输出会被重定向,使用日志框架将请求记录到日志文件中更为合适。

  2. 应该谨慎处理异常捕获,避免直接忽略异常而频繁发生错误。

  3. 对于RuntimeException等异常,我们也应该记录异常的根因。我们应该使用new RuntimeException("构建xxx出现异常", e)的方式记录根异常,而不是仅仅使用new RuntimeException("构建xxx出现异常")。这样的做法可以保留异常堆栈信息,有助于问题的排查。

try{
    
}catch(Exception e){
    throw new RuntimeException("构建xxx出现异常", e);
}

  1. ErrorException都属于throwable类型。如果在捕获异常时使用catch Exception,那么遇到Error是无法被捕获的。常见的Error有OutOfMemoryErrorNoSuchMethodError等。如果想要捕获Error类型的异常,需要使用catch Throwable,这样就可以捕获到ExceptionError

  2. 当我们使用finally代码块中的return语句时,try代码块中的return语句也会同时执行。最终,返回的值将会是finally代码块中return的值。

@Test
public void testTry() {
    System.out.println("getValue:" + getValue());
}

public int getValue() {
    int i = 0;
    try {
        System.out.println("try");
        i = 1;
        return i;
    } finally {
        i = 2;
        return i;
    }
}

执行结果getValue 返回 2 24. 当try代码块中使用return语句返回i时,无论finally块中如何修改i,都不会影响返回值。

@Test
public void testTry() {
    System.out.println("getValue:" + getValue());
}

public int getValue() {
    int i = 0;
    try {
        System.out.println("try");
        i = 1;
        return i;
    } finally {
        i = 2;
    }
}

getValue()函数的执行结果为1。尽管 finllay 修改了 i 的值,但并不会对 try 模块的返回值产生影响。这是因为返回值在 return 语句执行后会被暂存在栈中,并且后续对其再次的修改也不会对该返回值产生影响,除非在 finllay 中再次使用 return 语句来覆盖返回值。

  1. 在使用HashMap时,我们可能会遇到哈希冲突的问题。因为两个元素可以拥有相同的hashCode,但是它们的equals方法却不能相等。为了解决这个问题,我们需要重写键值的hashCodeequals方法,确保当两个元素相等时,它们的hashCodeequals都应该相等。

  2. HashMap是非线程安全的。如果我们需要在多线程环境下使用HashMap,可以考虑使用ConcurrentHashMap这个线程安全类。

  3. HashMap的values和keySet方法返回的是无序集合。如果我们希望按照插入顺序排序,可以使用LinkedHashMap

  4. 对于Set集合,其中的对象也是无序的,遍历Set集合的结果与插入Set集合的顺序并不相同。如果我们希望有序,可以使用LinkedHashSet

  5. HashMap可以插入key、value都是null的元素,并且containsKey(null)会返回true。这是需要我们在使用HashMap时要注意的一点。

Map<String, String> map = new HashMap();
map.put(null, null);
System.out.println(map.containsKey(null));

containsKey 返回 true

  1. ConcurrentHashMap 的 key 和 value 都不能为 null,否则会出现 NPE 错误。

  2. 当我们使用Arrays.asList方法创建一个List对象时,这个List对象是不允许添加add、清理clear元素的。

  3. 使用ArrayList的subList方法创建子List,这个子List会与父List共享相同的底层存储空间。因此,在子List中添加元素会对父级List产生影响,即父级List也会被修改。

  4. 在使用Java中的List.toArray方法将List转为数组时,我们需要指定转换后的数组类型。这个方法可以将一个List对象转换为其对应的数组形式。

List<String> list = Lists.newArrayList("1", "2", "3")
String[] array = (String[]) list.toArray()
System.out.println(array)

此时强转为 String[] 会出现异常。

需要指定类型,将其转换为具体的数组,因为List在运行时会进行类型擦除,所以需要重新指定入参的类型。可以使用list.toArray(new String[0])进行转换。在这种情况下,不需要给String数组指定长度,指定0即可,toArray方法会自动处理。

    List<String> list = Lists.newArrayList("1", "2", "3")
    String[] array = list.toArray(new String[0])
    System.out.println(array)

  1. ArrayList是一个非线程安全的集合类,可以使用Collections.synchronizedList方法将其转换为线程安全的List。但是,由于get方法也使用synchronize关键字修饰,这会严重影响并发读取。可以考虑使用CopyOnWriteArrayList来代替。

  2. 使用ArrayList.subList方法并不会修改原始对象,需要通过返回值获取subList。

  3. 在使用Optional.of方法时,如果参数为null,会抛出异常。如果允许参数为null,请使用Optional.ofNullable方法。

  4. SimpleDateFormat是一个非线程安全的类,可以使用线程局部变量避免多线程访问的问题。或者可以使用JDK 8中引入的线程安全的Formatter类DateTimeFormatter来代替,例如DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss")

  5. 慎用 Files.readAllBytes 方法一次性读取磁盘文件,这可能导致内存溢出的问题。建议使用 File 类和 BufferedReader 类的缓冲区循环读取文件,以避免一次性加载过多数据。

  6. 在比较大小时,可以使用 Comparable 接口。该接口定义了 compareTo 方法,当当前对象大于目标对象时,返回正数,即按升序排序;当当前对象小于目标对象时,返回负数,即按降序排序。

  7. 在使用 Class.isAssignableFrom 方法时,如果子类在后面,父类在前面,则返回 true;否则返回 false。

if (Object.class.isAssignableFrom(String.class)) {
    System.out.println("true");
}

  1. Redis 客户端 Jedis 不具备线程安全性,因此需要通过使用 JedisPool 来进行管理。

  2. 对于使用 Spring Async、Transactional 等注解的方法,需要注意它们应该被调用于不同的类中。如果在本类中调用这些注解,其效果将无法生效。

  3. 涉及到IO请求的连接一定能够要及时关闭。

  4. Java 泛型的实际类型在编译时会被擦除,因此无法在运行时获取其类型参数的具体类型。在运行时,泛型类型会统一被擦除为Object类型。(如果类型参数被定义为 T extends Fruit,则默认为Fruit类型)。泛型的好处是在编译的时候检查类型安全

List<String> list = new ArrayList<String>();  
System.out.println(list instanceof List<String>)

以上代码无法编译通过,JVM中并不存在List<String>.class或是List<Integer>.class,而只有List.class。

参考资料

[1]

https://juejin.cn/post/7277831097391513611: https://juejin.cn/post/7277831097391513611

[2]

https://juejin.cn/post/7277831097391513611: https://juejin.cn/post/7277831097391513611

[3]

https://juejin.cn/post/7282975291000586252: https://juejin.cn/post/7282975291000586252

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术小羊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值