读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
Talk is cheap.Show me the code
建议21:用偶判断,不用奇判断★★☆☆☆
取余示例代码:
public static void main(String[] args) {
System.out.println(-1 % 2 == 1 ? "奇数" : "偶数");
//运行结结果:偶数
System.out.println(-1 % 2 == 0 ? "偶数" : "奇数");
//运行结结果:奇数
}
了解一下Java中的取余(%标示符)算法,模拟代码如下:
//模拟java取余计算,dividend被除数,divisor除数
public static int remainder(int dividend, int divisor) {
return dividend - dividend / divisor * divisor;
}
注意:对于基础知识,我们应该“知其然,并知其所以然”
建议22:用整数类型处理货币★★☆☆☆
示例代码:
public static void main(String[] args) {
System.out.println(10.00 - 9.60);
//运行结果:0.40000000000000036
}
为什么不是期望的结果0.4呢?首先看下0.4转二进制的过程
//0.4转二进制
0.4 * 2 = 0.8 ————————————————0
0.8 * 2 = 1.6 ————————————————1
0.6 * 2 = 1.2 ————————————————1
0.2 * 2 = 0.4 ————————————————0
0.4 * 2 = 0.8 ————————————————0
0.8 * 2 = 1.6 ————————————————1
......//无限循环,类似十进制的1/3
所以在计算机中浮点数有可能(注意是可能)是不准确的,它只能无限接近准确值,而不能完全精确。
处理货币的解决方法:
- 使用BigDecimal
BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案。 - 使用整型
把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小100倍,这样处理的好处是计算简单、准确,一般在非金融行业(如零售行业)应用较多。
扩展:
小数用二进制如何表示
文字描述:将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位;以此类推,直到小数部分为0。
特殊情况:小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中表示小数会出现误差的原因。
二进制表示的小数如何转换为十进制
文字描述:从左到右,v[i] * 2^( - i ), i 为从左到右的index,v[i]为该位的值。
例子:0.4 = 0 * 2 ^ -1 + 1 * 2 ^ -2 + 1 * 2 ^ -3 + 0 * 2 ^ -4 + ......
建议23:不要让类型默默转换★★☆☆☆
示例代码:
public static void main(String[] args) {
long val = 30 * 10000 * 1000 * 60 * 8;
System.out.println(val);
//运行结果:-2028888064
/**
* Java是先运算然后再进行类型转换的,
* 三个运算参数都是int类型,三者相乘的结果虽然也是int类型,但是已经超过了int的最大值,
* 所以其值就是负值了(为什么是负值?因为过界了就会从头开始),再转换成long型,结果还是负值。
*/
//主动声明式类型转化(注意不是强制类型转换):长整型,乘出来的结果也是长整型
long val2 = 1L * 30 * 10000 * 1000 * 60 * 8;
System.out.println(val2);
//运行结果:144000000000
}
注意:基本类型转换时,使用主动声明方式减少不必要的Bug。
建议24:边界,边界,还是边界★★☆☆☆
数字越界使检验条件失效
示例代码:
public static void main(String[] args) {
//输入参数,int类型的最大值
int order = 2147483647;
//内置业务参数
int cnt = 1000;
if (order > 0 && order + cnt < 2000) {
System.out.println("处理成功");
System.out.println("order:" + order + cnt);
} else {
System.out.println("超过限额,处理失败");
}
//运行结果:处理成功,order:-2147482649
/**
*已经超过了int的最大值, 所以其值就是负值了(为什么是负值?因为过界了就会从头开始)
*/
}
在单元测试中,有一项测试叫做边界测试(也有叫做临界测试),如果一个方法接收的是int类型的参数,那以下三个值是必测的:0、正最大、负最小,其中正最大和负最小是边界值,如果这三个值都没有问题,方法才是比较安全可靠的。
建议25:不要让四舍五入亏了一方★★☆☆☆
目前Java支持以下七种舍入方式:
- ROUND_UP:
远离零方向舍入。向远离0的方向舍入,也就是说,向绝对值最大的方向舍入,只要舍弃位非0即进位。 - ROUND_DOWN:
趋向零方向舍入。向0方向靠拢,也就是说,向绝对值最小的方向输入,注意:所有的位都舍弃,不存在进位情况。 - ROUND_CEILING:
向正无穷方向舍入。向正最大方向靠拢,如果是正数,舍入行为类似于ROUND_UP;如果为负数,则舍入行为类似于ROUND_DOWN。注意:Math.round方法使用的即为此模式。 - ROUND_FLOOR:
向负无穷方向舍入。向负无穷方向靠拢,如果是正数,则舍入行为类似于ROUND_DOWN;如果是负数,则舍入行为类似于ROUND_UP。 - HALF_UP:
最近数字舍入(5进)。这就是我们最最经典的四舍五入模式。 - HALF_DOWN:
最近数字舍入(5舍)。在四舍五入中,5是进位的,而在HALF_DOWN中却是舍弃不进位。 - HALF_EVEN(银行家算法):
舍去位的数值小于5时,直接舍去;
舍去位的数值大于等于6时,进位后舍去;
当舍去位的数值等于5时,分两种情况:5后面还有其他数字(非0),则进位后舍去;若5后面是0(即5是最后一个数字),则根据5前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。
注意:根据不同的场景,慎重选择不同的舍入模式,以提高项目的精准度,减少算法损失。
建议26:提防包装类型的null值★★☆☆☆
Java中的基本数据类型没有方法和属性,而包装类就是为了让这些拥有方法和属性,实现对象化交互。如想把一个整型放到List中,就必须使用Integer包装类型。
包装对象和拆箱对象可以自由转换,但是要剔除null值,null值并不能转化为基本类型。
注意:包装类型参与运算时,要做null值校验。
建议27:谨慎包装类型的大小比较★★☆☆☆
示例代码:
public static void main(String[] args) {
Integer i = new Integer(100);
Integer j = new Integer(90);
System.out.println(i > j);
//运行结果:true
/**
* 在Java中,“>”和“<”用来判断两个数字类型的大小关系,注意只能是数字型的判断,
* 对于Integer包装类型,是根据其intValue()方法的返回值(也就是其相应的基本类型)进行比较的
* (其他包装类型是根据相应的value值来比较的,如doubleValue、floatValue等)
*/
/**
* 但是两个对象之间的比较就应该采用相应的方法,而不是通过Java的默认机制来处理
*/
System.out.println(i.compareTo(j));
//运行结果:1
}
注意:应该养成良好的编码习惯
建议28:优先使用整型池★★☆☆☆
示例代码:
public static void main(String[] args) {
int i = 10;
int j = 10;
Integer ii = new Integer(i);
Integer jj = new Integer(j);
//注意下面的"=="是引用的比较
System.out.println("new产生的Integer对象" + (ii == jj));
ii = i;
jj = j;
System.out.println("自动装箱的Integer对象" + (ii == jj));
ii = Integer.valueOf(i);
jj = Integer.valueOf(j);
System.out.println("valueOf产生的Integer对象" + (ii == jj));
//运行结果:
//new产生的Integer对象false
//自动装箱的Integer对象true
//valueOf产生的Integer对象true
}
解释:
- new产生的Integer对象
new声明的就是要生成一个新的对象,地址肯定不等 - 装箱生成的对象
装箱动作是通过valueOf方法实现的,所以后面两个算法是相同的 - Integer.valueOf的实现代码
//Integer类中的内部缓存类
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
//Integer类中的valueOf方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache.cache是IntegerCache内部类的一个静态数组,容纳的是-128到127之间的Integer对象。通过valueOf产生包装对象时,如果int参数在-128和127之间,则直接从整型池中获得对象,不在该范围的int类型则通过new生成包装对象。
所以在判断对象是否相等的时候,最好是用equals方法,避免用“==”产生非预期结果。
注意:通过包装类的valueOf生成包装实例可以显著提高空间和时间性能。
建议29:优先选择基本类型★★☆☆☆
无论从安全性、性能还是稳定性方面,基本类型都是首选方案。
例如int可以加宽转变为long型,Integer却不能转为Long
建议30:不要随便设置随机种子★★★☆☆
示例代码:
public static void main(String[] args) {
//Random的有参构造
Random r = new Random(1000);
for (int i = 1; i <= 4; i++) {
System.out.println("第" + i + "次:" + r.nextInt());
}
/**
* 程序第一次的运行结果
* 第1次:-1244746321
* 第2次:1060493871
* 第3次:-1826063944
* 第4次:1976922248
*/
/**
* 程序第二次的运行结果
* 第1次:-1244746321
* 第2次:1060493871
* 第3次:-1826063944
* 第4次:1976922248
*/
//在同一台机器上,甭管运行多少次,所打印的随机数都是相同的
}
问题何在?
因为产生随机数的种子被固定了,在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个规则:
- 种子不同,产生不同的随机数。
- 种子相同,即使实例不同也产生相同的随机数。
Random类的默认种子(无参构造)是System.nanoTime()的返回值,注意这个值是距离某一个固定时间点的纳秒数,不同的操作系统和硬件有不同的固定时间点,也就是说不同的操作系统其纳秒值是不同的,而同一个操作系统纳秒值也会不同,随机数自然也就不同了。(System.nanoTime不能用于计算日期,那是因为“固定”的时间点是不确定的,纳秒值甚至可能是负值,这点与System. currentTimeMillis不同。)
注意:若非必要,不要设置随机数种子。
在Java中有两种方法可以获得不同的随机数:通过java.util.Random类获得随机数的原理和Math.random方法相同,Math.random()方法也是通过生成一个Random类的实例,然后委托nextDouble()方法的,两者是殊途同归,没有差别。