该篇文章是《Effective Java 2nd》的读书总结,关于此书自然不必多言。如果有需要pdf中文&英文的,可以下方留言。据说现在已经有了第三版啦,点击查看:Effective Java 第三版的相关介绍
第8章 通用程序设计
——以下内容都是来自于该章节的总结
1. 将局部变量的作用域最小化
在第一次使用它的地方声明,几乎每个局部变量的声明都应该直接初始化
(如果没有足够的信息来对这个局部变量进行有意义的初始化,那么就应该推迟这个声明,直到可以初始化为止。 有例外情况,具体视情况而定)
如果在循环终止之后不再需要变量的内容,我们选择for循环就优先于while循环。
// 我们使用Iterator遍历java集合
首选的做法:
for(Iterator i = c.iterator(); i.hasNext() ){
doSomething((Element)i.next());
}
另外一种对局部变量的作用域进行最小化的循环做法:
// 此处的countLength()表示的是可能耗时需要计算的操作;
// 这样写可以避免每次迭代中执行冗余计算的开销
for(int i = 0 , length = countLength() ; i < lenght ; i ++){
doSomething(i);
}
对于局部变量,晚声明(初始化),早结束,即不可扩大局部变量的作用域
在实际开发中,对于该条规则肯定是深有体会的。尤其是我们擅长的Ctrl+C & Ctrl +V,异常就离程序不远了。
2. for-each循环优先于传统的for循环
前提条件——我们不需要借助于index来执行操作
for - each循环适用于数组&集合及任何实现Iterator接口的对象,且无毒副作用(无性能损失),IAW,使用它百利而无一害。这一点在多个集合进行嵌套迭代时,它的优势将会更加明显。
/**
* 假设2个骰子,有多少种组合
*/
enum Face {ONE,TWO,THREE,FOUR,FIVE,SiX}
Collection faces = Arrays.asList(Face.values());
//使用传统的for循环
for(Iterator i = faces.iterator(); i.hasNext();){
for(Iterator j = faces.iterator(); j.hasNext();){
Sysout.out.println(i.next() +" , "+j.next());
// 结果输出的是:6个重复的单词("ONE,ONE" 到 "SIX,SIX"),而不是我们期望的36种
}
}
为了fix 上面的bug,必须在外部循环的作用添加一个变量来保存外部元素,即:
// 此处只贴出关键代码
for (Iterator i = faces.iterator(); i.hasNext(); ) {
Face nextI = i.next();
for (Iterator j = faces.iterator(); j.hasNext(); ) {
System.out.println(nextI + "," + j.next());
}
}
按照传统的for循环来做上述简单功能,我们难道还需如履薄冰吗?NO
// for- each循环,想你之所想
for (Face face : faces) {
for (Face f : faces) {
System.out.println(face+","+f);
}
}
IAW,for-each循环在简洁性&预防Bug方面有着传统for循环无可比拟的优势,并且没有性能损失,So开发中,尽可能地使用for-each循环。
补充一句,如果牵涉到Index,我们就必须使用传统的for循环啦。
3. 强烈建议使用标准类库
“不要重复的造轮子”,能用标准库实现那就别犹豫,因为我们不一样。性能,代码风格都有保障。
在java中,每个程序员都应该熟悉以下三种类库:
- java.lang
- jang.util ( java.util.concurrent 很重要哦)
- java.io
其他的类库根据需要进行学习。
4. 如果需要精准的答案,那就扔掉Float & Double
强行装13一波,可以直接略过
float 和 double 类型主要是为了科学计算和工程计算而设计的。它们执行二进制浮点运算,这是为了在广泛的数值范围上提供较为精准的快速近似计算而精心设计的。然而它们并没有提供完全精确的结果,所以不应该被用于需要精准结果的场合。
// WTF,这咋不是我想要的呢?
System.out.println(1.03 - 0.42);//0.6100000000000001
System.out.println(1.0 - 0.9);//0.09999999999999998
System.out.println(1.0 - 0.2);// 0.8
System.out.println(1.0-0.1-0.2-0.3-0.4); // -5.551115123125783E-17
如何解决上述这种问题呢?
正确姿势是:使用BigDecimal 或者想办法转化为int,long来计算;
// 使用 BigDecimal计算
public static void main(String[] args){
// 其实BigDecimal构造函数有很多,注意这里的BigDecimal的构造函数里面是字符串
BigDecimal bigDecimal = new BigDecimal("1.03");
BigDecimal bigDecimal1 = new BigDecimal("0.42");
BigDecimal result = bigDecimal.subtract(bigDecimal1);
System.out.println("1.03 - 0.42 = "+result); // 1.03 - 0.42 = 0.61 perfect
BigDecimal bigDecimal2 = BigDecimal.valueOf(1.0);
BigDecimal bigDecimal3 = BigDecimal.valueOf(0.9);
BigDecimal subtract = bigDecimal2.subtract(bigDecimal3);
System.out.println("1.0 - 0.9 = "+ subtract); // 1.0 - 0.9 = 0.1 nice
// 想办法凑成int 或者long再做
System.out.println((1.03 * 100 - 0.42 * 100)/100); // 0.61
}
构造 BigDecimal 对象常用以下方法:
BigDecimal BigDecimal(double d); //不允许使用,得不到精确值
BigDecimal BigDecimal(String s); //常用,推荐使用
static BigDecimal valueOf(double d); //常用,推荐使用
其中,
- double 参数的构造方法,不允许使用!!!!因为它不能精确的得到相应的值;
- String 构造方法是完全可预知的: 写入 new BigDecimal("0.1") 将创建一个 BigDecimal,它正好等于预期的0.1; 因此,通常建议优先使用 String 构造方法;
- 静态方法 valueOf(double val) 内部实现,仍是将 double 类型转为 String 类型; 这通常是将 double(或float)转化为 BigDecimal 的首选方法;
BigDecimal详情,值得你收藏。
这里也要注意,BigDecaimal也是有缺点的:
你自己写也能感觉出来 1. 不够方便;2. 慢,影响程序性能低
根据实际情况取舍。
如果性能非常关键,你又不介意十进制的小数点,且数值不大,就可以考虑使用int 或者long.
如果数值范围没有超过9位十进制数值,就可以使用int;
如果数值范围没有超过18位十进制数值,就可以使用long;
否则,必须使用BigDecimal.
5.基本类型优先于装箱基本类型
Java的类型系统由两部分组成:
1.基本类型(primitive); byte ,char, int ,long,float ,double , boolean
2.引用类型(reference); String, List...and so on.
装箱基本类型(boxed primitive)就是 基本类型对应的引用类型。
1.Byte
2.Character
3.Integer
4.Long
5.Float
6.Double
7.Boolean
Java 1.5版本引入了自动装箱(autoboxing)和自动拆箱(auto-unboxing)。 该特性模糊了但没有完全抹去基本类型和装箱基本类型之间的区别。我们在开发中要谨慎选择。
基本类型和装箱基本类型的区别:
1.基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性(IOW,两个装箱基本类型可以具体相同的值和不同的同一性)[这里的同一性: 其实就是对象的内存地址];
2.每个装箱基本类型都有一个值:null;
3.基本类型通常比装箱基本类型更节省空间和时间,虽然装箱基本类型也进行了部分的性能优化(可以查看相关类的valueOf()方法);
// 装箱基本类型
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1 == i2? " + (i1 == i2)); // false
System.out.println("i1.equals(i2)? " + (i1.equals(i2))); // true;
// 装箱基本类型
Integer j1 = 1; //自动装箱
Integer j2 = 1; //自动装箱
System.out.println("j1 == j2? " + (j1 == j2)); // true;
System.out.println("j1.equals(j2)? " + (j1.equals(j2))); // true
再看个更神奇的栗子
Integer i11 = 127;
Integer i22 = 127;
System.out.println("i11 == i11? " + (i11 == i22)); // true
System.out.println("i11.equals(i22)? " + (Objects.equals(i11, i22)));// true
Integer i33 = 128;
Integer i44 = 128;
/**
* 在通过valueOf方法创建Integer对象的时候,如果数值在[-128,127]之间,
* 便返回指向IntegerCache.cache中已经存在的对象的引用;否则创建一个
* 新的Integer对象。
*/
System.out.println("i33 == i44? " + (i33 == i44));// false
System.out.println("i33.equals(i44)? " + (Objects.equals(i33, i44)));// true
Double d1 = 200.0;
Double d2 = 200.0;
System.out.println("d1 == d2? "+(d1 == d2)); // true
System.out.println("d1.equals(d2)? " + (Objects.equals(d1, d2))); // true
无形装B,最为致命。
以后看见装箱基本类型,比较的时候就直接用equals()就安全啦。实在不行,就装个Alibaba的插件
注意:
重要的事情说3遍:
1.装箱基本类型比较相等时用equals();
2.装箱基本类型比较相等时用equals();
3.装箱基本类型比较相等时用equals();
装箱基本类型在开发中还容易引起以下错误,这里顺便提一下:
public class TestAutoBox{
public static Integer num;
public static void main(){
// NullPointerException
/**
* 错误原因:
* 当使用基本类型和装箱基本类型进行操作时,一般装箱基本类型都会自动拆
* 箱。但是类似于这种情况,此时装箱基本类型为null时,就得到这个
* NullPointerException异常。
*/
if(num == 42){
System.out.println("You are so lucky as to Unbelievable");
}
}
}
另一种情况:
public class TestBox2{
// 装箱基本类型
private Long sum = 0L;
public static void main(){
for(long i = 0; i < Integer.MAX_VALUE; i++){
/**
* 程序编译运行没有问题,但是由于sum是装箱基本类型,
* 所以 += 操作将进行反复地装箱和拆箱,导致明显的性能下降
*/
sum += i;
}
System.out.println(sum);
}
}
到这里,你可能有疑惑,So Why 发明这个装箱基本类型呢?存在即合理在某些场景下,我们不得不用装箱基本类型。
使用装箱基本类型的场景:
1.使用集合(Collections Framework)中;
2.在参数化类型中;
3.反射的方法调用中;
总结
1.包装基本类型套路太多,尽量避免使用;
2.包装类型进行操作时,请尊重人家对象的本质,按对象的规格接待它(equals(),null);
3.使用基本类型安全可靠;
另外,关于装箱基本类型的详细内容,
可以参考深入理解Java中的包装类与自动拆装箱
6.选择合适的类型,放字符串一条生路
这条规则其实是一些人在开发中为了方便而养成的坏习惯,坦白地讲,以前团队就有成员这么干,我也想不通为什么。
7.注意字符串连接的性能
String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)
StringBuilder类是Java5.0新增加的,用于代替在非同步情况下的StringBuffer类。
String 类型和 StringBuffer(或者StringBuilder)类型的主要性能区别在于String是不可变对象。所以在每次对String类型进行改变的时候,其实就等同于生成了一个新的String对象,然后将指针指向新的String对象。到此体验一把
在字符串连接的使用中,大部分情况下,性能上:
- StringBuffer > String;
- StringBuilder > StringBuffer;
需要注意的是:
// 这样的拼接是没有任何问题的,因为在JVM'Eyes,它
// 等价于 String string = "Hello,Good Morning Sir";
String string = "Hello" +" Good" + "Morning" +"Sir";
总而言之,经常改变内容的字符串最好不要用String,在非同步的情况下,直接选择StringBuilder。因为每次生成对象都会系统性能产生影响。
String,StringBuffer与StringBuilder的区别
Difference between StringBuilder and StringBuffer
另外,身边的同事转化字符串时,总是直接(+"")来搞,令我十分反感。在这里恳求大家,在开发中,一定要优雅地来处理String.valueOf(param):
int i = 10;
String s1 = i + "";// 不推荐,产生两个对象
String s = String.valueOf(i);//推荐
8.通过接口引用对象
一般来讲,应该优先使用接口而非类来引用对象。如果有合适的接口类型存在,那么对于参数,返回值,变量和域来讲,都应该使用接口类型进行声明。
ArrayList arrayList = new ArrayList(10);//不推荐
正确姿势:
List list = new ArrayList(10);// 推荐
Don't ask me WHY? 你想想,那天发现ArrayList不满足需求,要换成LinkedList。采用第一种写法,你会不会Crazy.
如果顶层不是接口,使用基类也是可以的啊
9.谨慎地进行优化
关于优化的格言:
1.很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是其他的任何原因----甚至包括盲目地做傻事。
2.不要去计较效率上的一些小损失,在97%的情况下,不成熟的优化才是一切问题的根源。
3.在优化方面,我们应该遵守两条规则:
*规则1:不要进行优化;
*规则2:(仅针对专家):还是不要优化——That's to say,在你还没有绝对清晰的未优化方案前,请不要优化。
要努力编写好的程序而不是快的程序
过早地优化乃万恶之源
善用性能剖析工具来分析代码。
IOW,不要费力去编写快的程序——应该努力编写好的程序,速度自然会随之而来。
10.遵守大众的命名规范
这里只强调一点,
类型参数:
T:表示任意的类型;
E:表示集合的元素;
K&V:表示映射的键和值的类型;
X:表示异常。
任何类型可以是T,U,V或者T1,T2,T3。
我之前的CTO曾对我说过一句话,我感觉很受用:
你的代码能让后来看的人不骂你就足够啦
这里分享一个不错的在线编辑网站:GeeksforGeeks