再读effective-java,关于程序设计还有这些细节!

1. 局部变量作用域最小化

  • 问题

    将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。那么,常用的将局部变量作用域最小化的方式有哪几个方面?

  • 解决

    1. 为了避免局部变量扩大作用域,污染到其他作用域。局部变量的作用域应该最小化,即在第一次使用它的地方进行声明,尽可能在声明处进行初始化。典型的例子是,使用for循环,变量作用域在循环内,而不会扩散。所以,如果循环终止之后不再需要循环变量的内容,for循环就优于while循环;
    2. 使方法小而集中。如果把两个操作合并到同一个方法中,与其中一个操作相关的局部变量就有可能会出现在执行另一个操作的代码范围之内。为了防止这种情况发生,只要把这个方法分成两个,每个方法各自执行一个操作;
    3. 几乎每个局部变量的声明都应该包含一个初始化表达式,如果没有足够信息来对一个变量进行有意义的初始化,就应该推迟这个声明,直到可以初始化为止,例外情况是与try..catch有关。
  • 结论

    为了方法局部变量域污染到其他的作用域,需要将局部变量域尽可能缩小,这样能够增强代码的可读性和可维护性。

2. For-Each由于for循环

  • 问题

    在Java1.5之前,对集合的遍历习惯用下面这种方式:

    for (Iterator i = c.iterator(); i.hasNext(); ) {
    
           doSomething((Element) i.next()); // (No generics before 1.5)
    
    }
    

    对数组的遍历,习惯采用下面这种方式:

    for (int i = 0; i < a.length; i++) {
    
           doSomething(a[i]);
    
    }
    

    这些习惯用法要好于while循环,但也不完美。代迭器与索引都有些混乱。而且,它们还可能引起错误。在上面的循环中迭代器与索引都出现了三次,其中有两个地方可能带来错误,如果的确出现了这种错误,却无法保证编译器能捕获到这些错误。那么,在集合和数组的遍历中优先采用哪种方式?

  • 解决

    1. 在集合和数组的遍历时,优先采用for-each遍历的方式,比如下面这种示例代码:

      for (Element e : elements) {
      
             doSomething(e);
      
      }
      
    2. for-each循环的性能要优于传统的for循环,并且能够减少异常的发生,for-each循环不仅可以遍历集合和数组,还可以遍历任何实现Iterable接口的对象。同时,使用for-each循环也有以下局限性:

      • 过滤:如果需要在遍历的过程中删除特定的元素,就需要用显式的迭代器,调用remove方法进行删除;
      • 转换:如果在遍历的过程中,需要转换部分元素,就需要使用迭代器或者索引,一遍设定特定的元素;
      • 平行迭代:如果需要并行的遍历多个集合,就需要显式的控制迭代器和索引变量,以便所有的迭代器和索引能够同步前移。
  • 结论

    在大多数情况下都应该使用for-each循环的方式去遍历集合和数组,只有在过滤、转换、平行迭代的时候才需要采用传统的迭代器和索引的方式进行遍历。

3. 避免使用float

  • 问题

    float和double类型主要是为了科学计算与工程计算而设计的,它们并没有提供完全精确的结果,所以不应该被用于需要精确结果的场合。比如:

    System.out.println(1.0-0.42)
    //输出 0.580000000000001
    

    那么,当需要精确结果的时候,应该怎样处理?

  • 解决

    1. 在需要精确结果的时候需要使用BigDecimal,BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法,方法中的参数也必须是BigDecimal的对象;
    2. 任何需要精确答案的计算任务,都不要使用float和double,可以使用int、long或者BigDecimal。使用BigDecimal可以很方便的选择舍入的方式,它一共提供了8种方式;
    3. 如果数值范围没有超过9为十进制数字,使用int;如果超过9位但不超过18位数字,可以使用long;如果有可能超过18位数字,就必须使用BigDecimal。
  • 结论

    总之,对于任何需要精确答案的计算任务,请不要使用float或者double,如果你想让系统来记录十进制小数点,并且不介意因为不使用基本类型带来的不便,就可以使用BigDecimal

4. 基本类型优于装箱类型

  • 问题

    Java有一个数据类型由两部分组成,包含基本类型(primitive),如int、double和boolean,和引用类型(reference type),如String和List。 每个基本类型都有一个对应的引用类型,称为装箱基本类型(boxed primitive)。由于Java自动装箱和拆箱机制,会使得在实际开发中基本类型和装箱类型混用,那么它们之间有什么区别?

  • 解决

    1. 基本类型和装箱类型主要有3个主要区别:

      • 基本类型只有值,而装箱类型具有和它们值不同的统一性,即new Integer(42)==new Integer(42),虽然这两个装箱类型都是表示数字42,但是同一性判断会返回false
      • 基本类型只具有具体功能值,如数值等,而装箱类型还具有非功能值null
      • 基本类型通常要比装箱类型更节省空间和运行时间。

      注意:如果基本类型和装箱类型混合使用,装箱类型会拆箱为基本类型,这时,如果装箱类型为null,就会容易报NullPointException。

    2. 什么时候用装箱类型

      • 在使用集合时,键、值都只能使用装箱类型;
      • 使用参数化类型时,如ThreadLocal类时,只能使用装箱类型
  • 结论

    当可以选择的时候,基本类型要优先于装箱类型。基本类型更加方便简单,性能更好。如果没办法避免使用装箱类型时,注意类型间同一性的比较,以及NullPointException。

5. 正确的使用字符串

  • 问题

    字符串在实际开发中被高频使用,同样也存在着被滥用的情况,那么,字符串不应该使用在哪些情形下呢?

  • 解决

    1. 字符串不适合代替其他值的类型:当原始类型为int、float等其他类型时,就不要使用字符串替代;

    2. 字符串不适合代替枚举类型:枚举类型比字符串更适合用来表示枚举的常量;

    3. 字符串不适合代替聚集类型:如果一个实体有多个组件,用字符串来表示这个实体通常不恰当,String compundKey = className + "#" + i.next(),更好的做法是编写一个类来描述这个数据集,通常是一个私有的静态成员类;

    4. 字符串不适合作为授权键:有时候,字符串被用于对某种功能进行授权访问,考虑设计一个提供线程局部变量的机制,这个机制提供的变量在每个线程中都有自己的值,示例代码:

      public class ThreadLocal {
          private ThreadLocal() {}
          public static void set(String key, Object value);
          public static Object get(String key);
      }
      

      这种方法的问题在于,字符串键代表一个共享的全局命名空间,要使这种办法可行,客户端提供的字符串必须是唯一的,如果使用了相同的字符串,实际上就共享了这个变量。可以采用以下这种方式进行修正:

      public class ThreadLocal {
          private ThreadLocal() {}
          public static class Key {
              Key() {}
          }
          public static Key getKey() {
              return new Key();
          }
          public static void set(Key key, Object value);
          public static Object get(Key key);
      }
      

      这种采用对象实例作为授权键的话,就能够保证全局唯一。事实上,ThreadLocal也是采用这种方式,将ThreadLocal实例作为了ThreadLocalMap的键。

  • 结论

    总之,如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免使用字符串来表示对象。如果使用不当,字符串会比其他类型更加笨拙、速度更慢。字符串经常被错误的用来替代基本类型、枚举类型。

6. 慎用反射机制

  • 问题

    反射机制提供了通过程序来访问关于已装载的类的信息的能力,给定一个Class实例,可以获得Constructor、Method、Field实例,这些对象提供了类构造器、访问类成员名称、域类型、方法签名等信息。反射机制很强大,但使用起来有哪些注意事项呢?

  • 解决

    在使用反射机制的时候需要注意如下几点:

    1. 丧失了编译时类型检查的好处:如果程序企图用反射访问不存在的方法时,在运行时将会失败;
    2. 执行反射访问的方法代码很冗长:由于使用反射,会有很多的Exception需要try catch;
    3. 性能损失:反射方法调用比普通方法调用要慢很多。
  • 结论

    反射很强大的功能机制不能否认,对于特定复杂系统编程任务,反射机制很有用途,但是他也有很多缺点,对于普通的方法调用,建议不采用反射机制。

7. 谨慎的进行优化

  • 问题

    有三条与优化有关的格言是每个人都应该知道的:

    1. 很多计算上的过失都被归咎于效率(没有必要达到的效率),而不是任何其他的原因,——甚至包括盲目地做傻事。 ——William A.Wulf[Wulf72] 2. 不要去计校效率上的一些小小的得失,在97%的情况下。不成熟的优化才是一切问题的根源。 ——Donald E.Knuth[Knuth74] 3. 在优化方面,我们应该遵守两条规则: 规则1:不要进行优化。 规则2(仅针对专家):还是不要进行优化一一也就是说,在你还没有绝对清晰的未优化方案之前,请不要进行优化. ——M.A.Jackson[Jackson75]

    在大多数情况下,不成熟的优化都会造成更严重的问题,在进行性能优化的时候应该着重关注于哪些方面?

  • 答案

    1. 不要因为性能而牺牲合理的结构要努力编写好的程序而不足快的栏序。如果好的程序不够快,它的结构将使它可以得到优化。好的程序体现了信息隐藏 (information hiding)的原则:只要有可能,它们就会把设计决策集中在单个模块中,因此,可以改变单个决策,而不会影响到系统的其他部分;
    2. 努力避免那些限制性能的设计决策。当一个系统设计完成之后,其中最难以更改的组件是那些指定了模块之间交互关系以及模块与外界交互关系的组件。在这些设计组件之中,最主要的是API协议以及永久数据格式。这些设计组件不仅在事后难以甚至不可能改变,而且它们都有可能对系统本该达到的性能产生严重的限制;
    3. 要考虑API设计决策的性能后果。使公有的类型成为可变的(mutable )。这可能会导致大量不必要的保护性拷贝(见第39条:必要时进行保护性拷贝 )。同样地,在适合使用复合模式的公有类中使用继承,会把这个类与它的超类永远地束缚在一起,从而人为地限制了子类的性能
  • 结论

    总而言之,不要费力去编写快速的程序——应该努力编写好的程序,速度自然会随之而来。在设计系统的时候,特别是在设计API、线路层协议和永久数据格式的时候,一定要考虑性能的因素。当构建完系统之后,要测量它的性能。如果它足够快,你的任务就完成了。如果不够快,则可以在性能剖析器的帮助下,找到问题的根源,然后设法优化系统中相关的部分。第一个步骤是检查所选择的算法:再多的底层优化也无法弥补算法的选择不当。必要时重复这个过程,在每次改变之后都要测量性能,直到满意为止。

8. 多使用抽象来进行声明

  • 问题

    先来看一个反例:

    Vector list= new Vector();
    

    大多数情况下,我们都喜欢用具体的类型来声明变量,这里有一个弊端,如果将来想将Vector换成ArrayList的话,可能会影响其他的代码。在声明变量是最佳的实践是什么?

  • 解决

    像上例中采用接口的方式声明变量更加合适,如:

     List list= new ArrayList();
    

    具有的优点:程序更加灵活,如果其他代码使用的是List接口中的方法,当你想改变具体实现类的时候,比如这里将Vector换成了ArrayList,只需要改变构造器就可以,对其他地方的代码而言是无感知的,并不会影响其他地方的操作。

    • 什么时候应该让具体类去声明变量呢?
      1. 如果没有合适的接口存在,可以用类来引用对象。例如,考虑值类(String、BigInteger)很少用多个实现编写,他们通常是final的,并且很少有对应的接口。使用这种值类作为参数、变量、域或者返回值类型就比较合适;
      2. 顶层类是抽象类的情况, 例如java.util.TimerTask抽象类,应该用相关的基类(往往是抽象类)来引用对象,而不是它的实现类;
      3. 代码依赖于具体类的特殊属性,比如上例中假设程序中需要利用Vector的线程安全的特性,如果采用接口声明变量的话,无意间将实现类改成了ArrayList就会造成极大的错误。
  • 结论

    如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,都应该使用接口类型进行声明。这样做可以让程序变得更加灵活,如果改变接口的具体实现类,其他代码都可以继续工作。

怀梦追码.png

你可能感兴趣的:(再读effective-java,关于程序设计还有这些细节!)