无论这一年我们遇到了什么困难或者是喜事,在此时此刻,个人觉得都应该反思或者回味一下这些事情,对于好事,我们欣慰开心;坏事那我们能做到的就是极力避免它们再次发生,就如同接下来笔者要介绍的整个熟悉而陌生的名称“BUG”,接下来我主要会为大家介绍一下,发生在2022年这一年中的笔者在开发过程中所遇到的“bug”和“坑”。
希望大家不要当做笑话,认真去了解或者研究笔者所梳理出来的坑和bug,希望可以警示和告诫大
家,无论在代码的书写层面还是实际的开发层面都可以跳出这些问题改圈和坑点!在开发的航线中一路顺风,成为IT界的“海贼王”!
数据库存储数据有必要搞清空值,空字符串和 NULL 的概念。
null其实并不是空值,而是要占用空间,所以 MySQL在进行比较的时候,null会参与字段比较,所以对效率有一部分影响。对于表索引时不会存储null值的,所以如果索引的字段可以为null,索引的效率会下降很多。
空值也不一定为空,对于timestamp数据类型,如果往这个数据类型插入的列插入 null 值,则出现的值是当前系统时间,插入空值,则会出现 ‘0000-00-00 00:00:00’。
根据NULL的定义,NULL表示的是未知,因此两个NULL比较的结果既不相等,也不不等,结果仍然是未知。根据这个定义,多个NULL值的存在应该不违反唯一约束,所以是合理的,在oracle也是如此。
集合转换问题:用Array.asList转换基础类型数组,此时转换后的List集合的元素是有问题的,当接收页面请求的时候,循环以及获取元素的时候程序崩溃了!
int[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass());
复制代码
此时List集合的长度并不是我们预期的3,而是1,因为内部的元素是一个数组,而不是所有的元
素。直接遍历这样的List必然会出现 Bug:
public static List asList(T... a) {
return new ArrayList<>(a);
}
复制代码
如果使用Java8以上版本可以使用 Arrays.stream 方法来转换,否则可以把 int 数组声明为包装类型 Integer 数组:
int[] arr1 = {1, 2, 3};
List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());
List list2 = Arrays.asList(arr1);
复制代码
不能直接使用Arrays.asList来转换基本类型数组。
集合转换问题:Arrays.asList 返回的List不支持增删操作
String[] arr = {"1", "2", "3"};
List list = Arrays.asList(arr);
arr[1] = "4";
try {
list.add("5");
} catch (Exception ex) {
ex.printStackTrace();
}
复制代码
原因分析:Arrays.asList返回的List不支持增删操作。Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类 ArrayList。ArrayList 内部类继承自 AbstractList 类,并没有覆写父类的 add 方法,而父类中 add 方法的实现,就是抛出
UnsupportedOperationException。
private static class ArrayList extends AbstractList implements RandomAccess, java.io.Serializable
复制代码
集合转换问题:对原始数组的修改会直接影响得到的list
String[] arr = {"1", "2", "3"};
List list = Arrays.asList(arr);
arr[0]="aaaaa";
复制代码
asList生成的那个Array内部的ArrayList内部直接使用了原始的array导致的,这估计也是不让生成的list add和remove的原因吧,因为这样会影响到原始值。
List.subList操作还会导致OOM?
在日常开发过程中,经常会常常需要取集合中的某一部分子集来进行一下操作,而对于subList这个方法会经常的被我们所熟知。
List
在上面的代码终会,执行的subList方法的次数越多、或者分离的原始集合越大,越容易出现OOM,我们其实很容易误解,底层真正会对数组或者List集合进行相关的分割,其实不然,本身来讲会建立的方案只是单纯的逻辑分割哦!让我们来看看为什么会出现。
假设来100000次循环中的产生的一个个size为1000的list始终执行subList。那么返回的List强引用,使他得不到回收造成的。接下来我们来看一看为什么返回的子list会强引用原来的list。我们点进入ArrayList.subList()的源码。
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable {
transient Object[] elementData;
private int size;
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
public List subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList<>(this, fromIndex, toIndex);
}
private static class SubList extends AbstractList implements RandomAccess {
private final ArrayList root;
private final SubList parent;
private final int offset;
private int size;
public SubList(ArrayList root, int fromIndex, int toIndex) {
this.root = root;
this.parent = null;
this.offset = fromIndex;
this.size = toIndex - fromIndex;
this.modCount = root.modCount;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public Iterator iterator() {
return listIterator();
}
public ListIterator listIterator(int index) {
checkForComodification();
rangeCheckForAdd(index);
...
}
private void checkForComodification() {
if (root.modCount != modCount)
throw new ConcurrentModificationException();
}
}
}
复制代码
SubList类的构造方法:
public SubList(ArrayList root, int fromIndex, int toIndex) {
this.root = root;
this.parent = null;
this.offset = fromIndex;
this.size = toIndex - fromIndex;
this.modCount = root.modCount;
}
复制代码
List.subList操作导致OOM的根本原因就是分片后的List对饮食集合的强引用。为了避免这种情况的发生,在获取到分片后的List后,我们不要直接使用这个集合进行操作,可以使用一个新的变量保存分片后的list。
// 方法一
List arrayList = new ArrayList<>(rawList.subList(0, 2));
// 方法二
List arrayList1 = list.stream().skip(1).limit(3).collect(Collectors.toList());
复制代码
因为sublist中保存有原有list对象的引用——而且是强引用,这意味着, 只要sublist没有被jvm回收,那么这个原有list对象就不能gc,这个list中保存的所有对象也不能gc,即使这个list和其包含的对象已经没有其他任何引用。
Double和Float的计算操作,加减乘除方式会存在相关的误差哦,初级小伙伴们,一定要注意,如果
(1)要求比较高一定要采用BigDecimal类型进行计算操作。
String a = "16.11";
Double v = Double.parseDouble(a) * 100;
BigDecimal bigDecimal = new BigDecimal(a);
BigDecimal multiply = bigDecimal.multiply(new BigDecimal(100));
复制代码
最后的结果是会存在误差的哦,Double的数据会<1611。
(2)条件判断超预期
System.out.println( 1f == 0.9999999f ); // 打印:false**
System.out.println( 1f == 0.99999999f ); // 打印:true 惊喜不?
复制代码
最后的比较大小会存在歧义,差一位小数,竟然天壤之别
(3)数据转换超预期
float f = 1.1f; double d = (double) f;
System.out.println(f); // 打印:1.1
System.out.println(d); // 打印:1.100000023841858,咋会变成这样
复制代码
你以为BigDecimal就没有坑了?它的精度与相等比较的坑(equals方法可能不相等)
作为一个数字类型,经常有的操作是比较大小,有一种情况是比较是否相等。用equal方法还是compareTo方法?这里就是一个大坑。
//new 传进去一个double
BigDecimal newZero = new BigDecimal(0.0);
System.out.println(BigDecimal.ZERO.equals(newZero));
//new 传进去一个字符串
BigDecimal stringNewZero = new BigDecimal("0.0");
System.out.println(BigDecimal.ZERO.equals(stringNewZero));
//valueOf 传进去一个double
BigDecimal noScaleZero = BigDecimal.valueOf(0.0);
System.out.println(BigDecimal.ZERO.equals(noScaleZero));
//valueOf 传进去一个double,再手动设置精度为1
BigDecimal scaleZero = BigDecimal.valueOf(0.0).setScale(1);
System.out.println(BigDecimal.ZERO.equals(scaleZero));
复制代码
用于比较的值全都是0,猜一猜上面几个equals方法返回的结果是什么?全都是true?no no no...
true
false
false
false
复制代码
看看equal方法你就会豁然开朗咯,它还比较scale精度哦,哈哈,没有表面的那么简单哦!
那么对于这种本身就需要忽略scale的对比怎么办?其实BigDecimal类也提供了相关的compare方法,而且这个方法的设计也和comparable接口的实现也很相似,所以使用起来也挺舒服的。
public int compareTo(BigDecimal val) {
// Quick path for equal scale and non-inflated case.
if (scale == val.scale) {
long xs = intCompact;
long ys = val.intCompact;
if (xs != INFLATED && ys != INFLATED)
return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
}
int xsign = this.signum();
int ysign = val.signum();
if (xsign != ysign)
return (xsign > ysign) ? 1 : -1;
if (xsign == 0)
return 0;
int cmp = compareMagnitude(val);
return (xsign > 0) ? cmp : -cmp;
}
复制代码
一个更大的坑是,如果将BigDecimal的值作为HashMap的key,因为精度的问题,相同的值就可能出现hashCode值不同并且equals方法返回false,导致put和get就很可能会出现相同的值但是存取了不同的value。小数类型在计算机中本来就不能精确存储,再把其作为HashMap的key就相当不靠谱了,以后还是少用。
@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集
//父类
@Data
public class Parent { private String id;}
//子类
@Data
public class Child extends Parent { private String name;}
复制代码
所以如果继承父类时候使用@Data需要加上@EqualsAndHashCode(callSuper = true),如下:
@Data
@EqualsAndHashCode(callSuper = true)
public class Child extends Parent {
private String name;
}
复制代码
在登录认证后,我们系统频繁高并发去处理请求的时候,发现数据出现了紊乱,什么紊乱?就是多个账号之间的数据发生了流窜,道理很简单从数据上来看就是数据对应的userId完全对不上了。
分析了以后发现,系统在调用的时候对ThreadLocal的使用出现了内存泄漏以及内存数据紊乱的问
题,也就是和PageHelper一样的道理,需要清理参数执行,在公司内部的系统中出现了相关的权限认证和会话信息注入到ThreadLocal的内容,这个相信大家并不陌生,但是在有一些不需要鉴权的接口的时候,就会存在不会处理ThreadLocal中数据的remove以及更新的操作,导致出现了数据紊乱的问题。
web请求下的ThreadLocal使用要保证:请求进来的时候set,请求回去的时候remove。只有这样才能保证请求内的ThreadLocal 是唯一的。 这个特性在深刻的提醒我们:一次http请求和tomcat启动处理业务的线程并非一一对应的,而是通过一个线程池进行调度。
ConcurrentHashMap是个线程安全的哈希表容器,但它仅保证提供的原子性读写操作线程安全。
当我们在通过多线程情况下,如果在对相关的ConcurrentHashMap做较为复杂的操作处理功能的时候,就会存在线程不安全的场景:
map.put(1,getResult()); 这种场景就是线程不安全的考虑哦!请大家慎用和谨记!
ConcurrentHashMap对外提供能力的限制:
我们可以使用相关的computeIfAbsent、putIfAbsent等操作可以保证原子化处理。
可以参考一下这篇文章哦:blog.csdn.net/singwhatiwa…
发送Topic消息报该错误,
com.alibaba.rocketmq.client.exception.MQBrokerException: CODE: 2 DESC: [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 208ms, size of queue: 8
sendThreadPoolQueue取出头元素,转换成对应的任务,判断任务在队列存活时间是否超过了队列设置的最大等待时间,如果超过了则组装处理返回对象response,response的code为
RemotingSysResponseCode.SYSTEM_BUSY。
[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: [当前任务在队列存活时间], size of queue: [当前队列的长度]
说实在的就是RocketMQ处理不过来了:那么有以下几个选择供大家参考: