写了几年的代码,总算知道为啥总是写bugl

前面的话

我们程序员开发,总是抱怨怎么又要加班,自己测试没问题,交给测试一堆问题;曾子曰:“吾日三省吾身“。每天自我反省是程序员的基本素养,然后总结忽略的地方记录下来。

因为基础不扎实而犯下的错

这里举几个Java代码中常见例子

小数点计算的问题

当业务中遇到金钱的计算,都是带小数点的,如果你不知道 小数 的二进制与十进制 互转的问题那么很有可能出现实际与预期不服的情况。

System.out.println(0.5-0.4);

》》》输出结果:
0.09999999999999998

Effective Java》中提到一个原则,那就是floatdouble只能用来作科学计算或者是工程计算,java的设计者给编程人员提供了一个很有用的类java.math.BigDecimal,他可以完善float和double类无法进行精确计算的缺憾。

光说不练,我们来看看BigDecimal,发现精度还是不准确

System.out.println(new BigDecimal(0.5).subtract(new BigDecimal(0.4)));

》》》输出结果:
0.09999999999999997779553950749686919152736663818359375

其实我们又忽视了一点,就是new BigDecimal(0.1)这时候就是已经是精度不准确了

System.out.println(new BigDecimal(0.1));

》》》输出结果:
0.1000000000000000055511151231257827021181583404541015625

那么究竟我们要怎样才能做到精度不丢失呢?
构造参数使用String

System.out.println(new BigDecimal("0.5").subtract(new BigDecimal("0.4")));

》》》输出结果:
0.1

equals的滥用

我们知道比较String是否相同,不能直接使用==,如果这都不清楚了,自己去复习,基本数据类型和引用类型是怎么比较相等的;

在有一些业务中会涉及到字符串的拼接,较少的拼接还好,如果是遇到for循环,那就会增加内存开销

这是后有经验的大佬就然你使用StingBuilder,内部的实现大家有兴趣可以去看看,简单来说就是内部有个可扩容的数组,让它成为可变字符串。

这时候如果要比较两个StringBuilder是否相等

StringBuilder str1=new StringBuilder("123");
StringBuilder str2=new StringBuilder("123");
System.out.println(str1.equals(str2));

》》》输出结果:
false

为啥会这样呢?
StringBuilder 没有去实现Object的equals方法
所以我们一定要注意,很容易忽视的;

这下是不是明白 阿里的开发手册中规定谨慎使用继承的方式进行扩展,代码越来越多,你根本不知道有没有实现哪些方法,都要点进去看,影响开发效率;

《Java 开发手册》中有一条规定:谨慎使用继承的方式进行扩展,优先使用组合的方式实现。

还有关于数组比较的

int[] a={1,2};
int[] b={1,2};
		
System.out.println(a.equals(b));

》》》输出结果:
false

//正确的比较
System.out.println(Arrays.equals(a, b));

》》》输出结果:
true

synchronized错误使用

因为没有弄清楚什么是对象锁,什么是类锁,导致线上问题

//编写两个线程模拟多用户操作
public class Demo implements Runnable{
	@Override
	public void run() {
		// TODO Auto-generated method stub
		update();
	}
	//更新数据的方法
	public void update(){
		synchronized (this) {  //this 为对象锁
			System.out.println("开始线程"+Thread.currentThread().getName());
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("结束线程"+Thread.currentThread().getName());
		}
	}
	public static void main(String[] args){
		Demo a=new Demo(); //用户a
		Demo b=new Demo(); //用户b
		
		Thread threadA=new Thread(a);
		Thread threadB=new Thread(b);
		threadA.start();
		threadB.start();
	}
}

》》输出:
开始线程Thread-0
开始线程Thread-1
结束线程Thread-0
结束线程Thread-1	
//线程同时开始同时结束   加锁失败

解决办法加类锁,这里只是举例,实际业务可能还有更复杂的设计,确保访问的效率

public void update(){
		synchronized (Demo.calss) {  //*.calss 为类锁 或者 加在静态方法上
 			System.out.println("开始线程"+Thread.currentThread().getName());
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("结束线程"+Thread.currentThread().getName());
		}
	}

集合过滤问题

你是否有过通过遍历一个ArrayLIst,然后做过滤操作?

List<String> list= new ArrayList<String>() {{
 add("a");
 add("b");
 add("c");
 add("d");
}};
for (String str: list) {
 if (str.equals("b")) {
 list.remove(str);
 }
}
System.out.println(list);

最后的结果是什么呢?
抛出异常java.util.ConcurrentModificationException 具体原因不展开分析了,需要的小伙伴可以给我留言,我再写一篇详细讲解。

这时候有小伙伴是不是开始尝试使用下标进行删除了

List<String> list= new ArrayList<String>() {{
 add("a");
 add("b");
 add("c");
 add("d");
}};
for (int i=0;i<list.size();i++) {
 if (str.get(i).equals("b")) { 
 list.remove(i);
 }
}
System.out.println(list);

这样为啥就会成功呢?
每次遍历的时候,都会重新执行list.size() 获取新的长度 看到这是不是也猜到了remove的时候会改变内部的数组长度。

使用对象作为Map的key

写段代码举个例子。新手总是喜欢复制粘贴

Map<Demo,String> map=new HashMap(2);
Demo a=new Demo("001");
map.put(a, "aaaa"); //使用对象作为key

//使用时,使用新的对象(内部的值是一样的)  同样可以获取值
//但是新手由于只是模仿了写法,获取到值总是null,百思不得其解,还说之前的代码就是这么写的啊
System.out.println(map.get(new Demo("001")));

这就是典型的没弄清楚HashMap底层原理的
所以说面试时,hashMap底层实现是必问的问题,简单提一下就是,为了保证获取元素的效率。底层通过数组的形式存放,通过hashCode() 计算出放到哪一个数组的位置,下次获取时可以通过hashCode()计算出数组下标拿到对应的值。
那么这样算下标肯定会又算成相同的啊,也就是hash碰撞,那么怎么解决呢?那么就在数组位置再增加一个链表(hash桶),再通过equals比较key的值,为了再次提高hash桶查询的效率,JDK8以后是链表+红黑树的结构。
了解到原理后,我们就可以知道 原来是要实现hashCode() equals(Object obj) 这两个方法

引用图片数据结构图

因为单元测试的不够

没有测试负数

//求一个数的是否是奇数
int num=5;
System.out.println(num%2==1); //对2求余数 == 1就是奇数

int num =-5;
num&2;  //结果为-1

num%2!=0; //这样判断才是正确的

测试不够而没有发现的小数计算问题

System.out.println(0.5-0.3);

》》》输出:
0.2   测试通过

//如果多考虑一些数据可能就不通过了
System.out.println(0.5-0.4);
》》》输出:
0.09999999999999998

条件测试不够,没有发现空指针问题

String name="aaa";
Demo demo=null;
return "aaa".equals(name)?"aaaaa":demo.getName();

//如果这时,把name的值改成bbbb呢?

集合中有元素是null的情况

List<Demo> list=new ArrayList();
Demo a=new Demo("001");
Demo b=null;
list.add(a);
list.add(b);

for(Demo demo:list){
	demo.getName();  //这就会报异常
}

引用阿里的Java编程规范
编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C:Correct,正确的输入,并得到预期的结果。
D:Design,与设计文档相结合,来编写单元测试。
E:Error,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果

总结

程序员的路注定不顺利,一路上可能不止九九八十难,所以我们需要团结,相互学习,总结工作中遇到的问题

当然我这篇文章只是抛砖引玉

欢迎各位大佬留言你们在工作中发现的问题😀

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值