7.通用程序设计_EJ
第45条: 将局部变量的作用域最小化
该条目与第13条(使类和成员的可访问性最小)本质上是类似的。要使局部变量的作用域最小化,最有利的方法就是在第一次使用它的地方声明。在每个局部变量的声明处都应该包含一个初始化表达式。还要使方法小而集中。
第46条: for-each循环优于传统的for循环
传统的for循环的迭代器和索引变量在每个循环中会出现三次,这很容易出错。考虑下面的例子:
public class Suits { public static void main(String[] args) { // TODO Auto-generated method stub Collection<Suit> suits = Arrays.asList(Suit.values()); Collection<Rank> ranks = Arrays.asList(Rank.values()); List<Card> deck = new ArrayList<>(); for(Iterator<Suit> i = suits.iterator(); i.hasNext(); ){ Suit suit = i.next(); for(Iterator<Rank> j = ranks.iterator(); j.hasNext(); ){ // deck.add(new Card(i.next(), j.next())); System.out.println(suit + " " + j.next()); } } System.out.println("-----------更好的方法---------------"); for(Suit suit : suits){ for(Rank rank : ranks){ System.out.println(suit + " " + rank); } } } } enum Suit {CLUB, DIAMOND, HEART, SPADE} enum Rank {ACE, DEUCE, THREE , FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING} class Card { private Suit suit; private Rank rank; Card(Suit suit, Rank rank){ this.suit = suit; this.rank = rank; } }
如果打开代码中的注释,我们会发现一个bug,迭代器对外部的集合(suits)调用了太多次的next方法,导致程序结果不是我们想要的。修复这个bug可以在外层添加 Suit suit = i.next();这段代码。但也有更好的方式,使用for-each循环,如上面代码中所示。
总之,for-each循环在简洁性和预防bug方面有着传统for循环无法比拟的优势。但下面三种情况无法使用for-each循环:
1.需要遍历集合,并删除选定的元素
2.需要遍历数组或列表,并取代它部分或全部的值
3.需要并行遍历多个集合
第47条:了解和使用类库
JDK中内置了大量的工具类库,但很多“不为人知”。这实际上考研的是编程人员对Java基础的掌握程度,例如输出数组的方法:Arrays.toString等等,再比如判断是否字符串为空是实际上有isEmpty方法的。书中建议每个程序员都应该熟悉java.lang、java.util。
在进行工程项目类的开发时,不应重复造轮子,利用现有的已成熟的技术能避免很多bug和其他问题。除非自己业余爱好研究,重复造轮子我认为就很能提高编程水平了。
第48条:如果需要精确答案,请避免使用float和double
float和double类型不适合用于货币计算。因为要让它们精确地表示0.1(或10的任何其他负数次方值)是不肯能的。
看下面的例子:
public class FloatTest { public static void main(String[] args) { // TODO Auto-generated method stub System.out.println(1.03 - .42); double funds = 1.00; int itemsBought = 0; for(double price = .10; funds >= price; price += .10){ funds -= price; itemsBought++; } System.out.println("itemsBought: " + itemsBought); //3 System.out.println("Change: " + funds); //0.3999999999999 } }
程序的结果不是我们想要的,按理第一条输出语句应返回4,第二条返回0.可以用BigDecimal类型代替double。
public class BigDecimalTest { public static void main(String[] args) { final BigDecimal TEN_CENTS = new BigDecimal(".10"); int itemsBought = 0; BigDecimal funds = new BigDecimal("1.00"); for(BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)){ itemsBought++; funds = funds.subtract(price); } System.out.println(itemsBought + " items bought"); System.out.println("Money left over: $" + funds); } }
这里结果就没问题了。当然如果不介意记录十进制的小数点,我们可以以分为单位,而不是以元为单位。
总而言之,如果需要精确答案,不要使用float或double。可以使用BigDecimal,如果性能关键,可以使用int或long,9位十进制数以内用int,不超过18位可以用long,超过18位就必须使用BigDecimal。
第49条:基本类型优先于装箱基本类型
基本类型和装箱基本类型有三个主要区别:
1.基本类型只有值,而装箱基本类型具有它们值不同的同一性,换句话说,两个装箱基本类型可以具有相同的值,但其对象引用不同。
2.基本类型有功能完备的值,而后者有非功能值-null
3.前者比后者更节省空间
来看一个有严重缺陷的例子:
public class IntegerTest { public static void main(String[] args) { Comparator<Integer> naturalOrder = new Comparator<Integer>() { @Override public int compare(Integer first, Integer second) { return first < second ? -1 : (first == second ? 0 : 1); // 修正方案 // int f = first; // int s = second; // return f < s ? -1 : (f == s ? 0 : 1); } }; int result = naturalOrder.compare(new Integer(42), new Integer(42)); System.out.println(result); //返回1 对装箱基本类型做==操作,执行对象同一性比较,导致结果与预期不一致 } }
那什么时候该用装箱基本类型呢?第一是作为集合中的元素、键和值。第二在参数化类型中用装箱基本类型作为类型参数。最后,在进行反射的方法调用时,必须使用装箱基本类型。总之,可以选择的时候,基本类型要优先于装箱基本类型。自动装箱减少了使用装箱基本类型的繁琐性,但并没有减少它的风险。
第50条:如果其他类型更合适,则尽量避免使用字符串
在开发过程中我们不应该为了省事,所有类型都定义为String类型。应该编写更加适当的数据类型,避免使用字符串来表示对象,若使用不当,字符串类型比其它类型更加笨拙、更加不灵活、速度更慢、也更容易出错。经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚集类型。
第51条:当心字符串连接的性能
String字符串是不可变的,每次对一个字符串变量的赋值实际上都在内存中开辟了新的空间。如果要经常对字符串做修改应该使用StringBuilder(线程不安全)或者StringgBuffer(线程安全),其中StringBuilder由于不考虑线程安全,它的速度更快。
第52条:通过接口引用对象
应该优先使用接口而不是类来引用对象,例如:
List<String> list = new ArrayList<String>();
这样带来的好处就是可以更换list的具体实现只需一行代码,之前有谈到将接口作为参数的类型,这两者配合使用就能最大限度实现程序的灵活性。
但如果是类实现了接口,但是它提供了接口中不存在的额外方法,且程序依赖这些额外方法,这个时候用接口来代替类引用对象就不合适了。
第53条:通过接口引用对象
反射机制能在运行时获取已装载类的信息,比如Constructor、method、field。但这种方式是有影响的:
丧失了编译时类型检查
执行反射访问所需要的代码非常笨拙和冗长(这需要一定的编码能力)
性能损失
所以要慎用反射机制,但如果以非常有限的形式使用反射机制,可以获得许多好处,还是值得的。比如可以用反射方式创建实例,然后通过它们的接口或超类,以正常的方式返回这些实例。下面程序创建了一个Set<String>实例。
public class Reflective { public static void main(String[] args) { Class<?> c1 = null; String className = args[0]; try { c1 = Class.forName(className); } catch (ClassNotFoundException e) { System.out.println("class not found!"); System.exit(1); } //instantiate the class // @SuppressWarnings("unchecked") Set<String> s = null; try { s = (Set<String>) c1.newInstance(); } catch (InstantiationException e) { System.out.println("class not accessible!"); System.exit(1); } catch (IllegalAccessException e) { System.out.println("class not instantiable!"); System.exit(1); } //exercise the set s.addAll(Arrays.asList(args).subList(1, args.length)); System.out.println(s); try { test(); }catch (Exception e){ System.out.println("catch a exception!"); } } public static void test(){ throw new RuntimeException(); } }
简而言之,反射机制是一种功能强大的机制,对于特定的复杂的系统编程任务,它是非常必要的。如有可能,应该使用反射机制来实例化类,而访问对象则用编译时已知的某个接口或超类。
第54条:谨慎地使用本地方法
所谓的本地方法就是在JDK源码中你所看到在有的方法中会有“native”关键字的方法,这种方法表示用C或者C++等本地程序设计语言编写的特殊方法。之所以会存在本地方法的原因主要有:访问特定平台的接口、提高性能。
实际上估计很少很少在代码中使用本地方法,就算是在设计比较底层的库时也不会使用到,除非要访问很底层的资源。当使用到本地方法时唯一的要求就是全面再全面地测试,以确保万无一失。
第55条:谨慎地使用本地方法
我在实际编码过程中,常常听到别人说,这么实现性能可能会好一点,少了个什么什么性能会好一点,甚至是少了个局部变量也会提到这么性能要好一点,能提高一点是一点。
但实际上是在编码中如果你没有考虑清楚就冒然想当然的去做优化,常常可能是得不偿失,就像我开头提到的那样,甚至为了优化性能而去减少一个局部变量。正确的做法应该是,写出结构优美、设计良好的代码,不是写出快的程序。性能的问题应该有数据做支撑,也就是有性能测试软件对程序测试来评判出性能问题出现在哪个地方,从而做针对性的修改。
第56条:遵守普遍接受的命名惯例
我们在编码时要把标准的命名惯例当做一种内在机制来看待,遵守普遍接受的命名惯例。