java 54-66

 五十四、谨慎地使用本地方法: 

      JNI允许Java应用程序可以调用本地方法,所谓本地方法是指用本地程序设计语言,如C/C++来编写的特殊方法。本地方法在本地语言中可以执行任意的计算任务,并最终返回Java程序。它的主要用途就是访问一些本地的资源,如注册表、文件锁等,或者是访问遗留代码中的一些遗留数据。当然通过本地方法在有些应用场景中是可以大大提高提高系统执行效率的。

      随着Java平台的不断成熟,它提供了越来越多以前只有宿主平台才有的特性,如java.util.prefsjava.awt.SystemTray等。与此同时,随着JVM的不断优化,其效率也在不断的提高,因此只有在很少的情况下才会考虑使用JNI。还需要指出的是,JNI中胶合JavaC++的代码部分非常冗长且难以理解。

五十四:谨慎地使用本地方法

1.      指使用本地程序设计语言编写的特殊方法

2.      jvm越来越快,本地方法提供性能不值得

3.      本地语言不安全,平台相关,不方便移植

4.      尽可能少使用,必要时候才使用,而且极少数情况会用到,并全面测试

五十五:谨慎第进行优化

不要费力去编写快速的程序,应该努力编写好的程序,速度随之而来

五十六:遵守普遍接受的命名惯例 

如果长期养成的习惯与此不同,不要盲目遵从这些命名规范,可以运用常识

五十七、只针对异常情况才使用异常: 

      不知道你否则遇见过下面的代码:

  
  
  
  
  1. try { 
  2.          int i = 0
  3.          while (true
  4.              range[i++].climb(); 
  5.      } catch (ArrayIndexOutOfBoundsException e) { 
  6.      } 

  这段代码的意图不是很明显,其本意就是遍历变量数组range中的每一个元素,并执行元素的climb方法,当下标超出range的数组长度时,将会直接抛出ArrayIndexOutOfBoundsException异常,catch代码块将会捕获到该异常,但是未作任何处理,只是将该错误视为正常工作流程的一部分来看待。这样的写法确实给人一种匪夷所思的感觉,让我们再来看一下修改后的写法:

  
  
  
  
  1. for (Mountain m : range) { 
  2.          m.climb(); 
  3.      } 

  和之前的写法相比其可读性不言而喻。那么为什么又有人会用第一种写法呢?显然他们是被误导了,他们企图避免for-each循环中JVM对每次数组访问都要进行的越界检查。这无疑是多余的,甚至适得其反,因为将代码放在try-catch块中反而阻止了JVM的某些特定优化,至于数组的边界检查,现在很多JVM实现都会将他们优化掉了。在实际的测试中,我们会发现采用异常的方式其运行效率要比正常的方式慢很多。

      除了刚刚提到的效率和代码可读性问题,第一种写法还会掩盖一些潜在的Bug,假设数组元素的climb方法中也会访问某一数组,并且在访问的过程中出现了数组越界的问题,基于该错误,JVM将会抛出ArrayIndexOutOfBoundsException异常,不幸的是,该异常将会被climb函数之外catch语句捕获,在未做任何处理之后,就按照正常流程继续执行了,这样Bug也就此被隐藏起来。

      这个例子的教训很简单:"异常应该只用于异常的情况下,它们永远不应该用于正常的控制流"。虽然有的时候有人会说这种怪异的写法可以带来性能上的提升,即便如此,随着平台实现的不断改进,这种异常模式的性能优势也不可能一直保持。然而,这种过度聪明的模式带来的微妙的Bug,以及维护的痛苦却依然存在。

      根据这条原则,我们在设计API的时候也是会有所启发的。设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如IteratorJDK在设计时充分考虑到这一点,客户端在执行next方法之前,需要先调用hasNext方法已确认是否还有可读的集合元素,见如下代码: 

  
  
  
  
  1. for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { 
  2.          Foo f = i.next(); 
  3.      } 

 如果Iterator缺少hasNext方法,客户端则将被迫改为下面的写法:

  
  
  
  
  1. try { 
  2.          Iterator<Foo> i = collection.iterator(); 
  3.          while (true
  4.              Foo f = i.next(); 
  5.      } catch (NoSuchElementException e) { 
  6.      } 

  这应该非常类似于本条目开始时给出的遍历数组的例子。在实际的设计中,还有另外一种方式,即验证可识别的错误返回值,然而该方式并不适合于此例,因为对于next,返回null可能是合法的。那么这两种设计方式在实际应用中有哪些区别呢?

      1. 如果是缺少同步的并发访问,或者可被外界改变状态,使用可识别返回值的方法是非常必要的,因为在测试状态(hasNext)和对应的调用(next)之间存在一个时间窗口,在该窗口中,对象可能会发生状态的变化。因此,在该种情况下应选择返回可识别的错误返回值的方式。

      2. 如果状态测试方法(hasNext)和相应的调用方法(next)使用的是相同的代码,出于性能上的考虑,没有必要重复两次相同的工作,此时应该选择返回可识别的错误返回值的方式。

      3. 对于其他情形则应该尽可能考虑"状态测试"的设计方式,因为它可以带来更好的可读性。

五十八、对可恢复的情况使用受检异常,对编程错误使用运行时异常:

      Java中提供了三种可抛出结构:受检异常、运行时异常和错误。该条目针对这三种类型适用的场景给出了一般性原则。

      1. 如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常,如某人打算网上购物,结果余额不足,此时可以抛出自定义的受检异常。通过抛出受检异常,将强迫调用者在catch子句中处理该异常,或继续向上传播。因此,在方法中声明受检异常,是对API用户的一种潜在提示。

      2. 用运行时异常来表明编程错误。大多数的运行时异常都表示"前提违例",即API的使用者没有遵守API设计者建立的使用约定。如数组访问越界等问题。

      3. 对于错误而言,通常是被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。

      针对自定义的受检异常,该条目还给出一个非常实用的技巧,当调用者捕获到该异常时,可以通过调用该自定义异常提供的接口方法,获取更为具体的错误信息,如当前余额等信息。

五十九、避免不必要的使用受检异常   

      受检异常是Java提供的一个很好的特征。与返回值不同,它们强迫程序员必须处理异常的条件,从而大大增强了程序的可靠性。然而,如果过分使用受检异常则会使API在使用时非常不方便,毕竟我们还是需要用一些额外的代码来处理这些抛出的异常,倘若在一个函数中,它所调用的五个API都会抛出异常,那么编写这样的函数代码将会是一项令人沮丧的工作。

      如果正确的使用API不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采用有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合使用未受检异常,见如下测试:

  
  
  
  
  1. try { 
  2.          dosomething(); 
  3.      } catch (TheCheckedException e) { 
  4.          throw new AssertionError(); 
  5.      } 
  6.       
  7.      try { 
  8.          donsomething(); 
  9.      } catch (TheCheckedException e) { 
  10.          e.printStackTrace(); 
  11.          System.exit(1); 
  12.      }  

 当我们使用受检异常时,如果在catch子句中对异常的处理方式仅仅如以上两个示例,或者还不如它们的话,那么建议你考虑使用未受检异常。原因很简单,它们在catch子句中,没有做出任何用于恢复异常的动作。 

六十、优先使用标准异常: 

      使用标准异常,不仅可以更好的复用已有的代码,同时也使你设计的API更加容易学习和使用,因为它和程序员已经熟悉的习惯用法更为一致。另外一个优势是,代码的可读性更好,程序员在阅读时不会出现更多的不熟悉的代码。该条目给出了一些非常常用且容易被复用的异常,见下表:  

             异常                                        应用场合

 

  IllegalArgumentException    null的参数值不正确。

 

 IllegalStateException       对于方法调用而言,对象状态不合适。

 

 NullPointerException        在禁止使用null的情况下参数值为null

 

 IndexOutOfBoundsException         下标参数值越界

 

 ConcurrentModificationException   在禁止并发修改的情况下,检测到对象的并                                     发修改。

 

 UnsupportedOperationException    对象不支持用户请求的方法。

 

      当然在Java中还存在很多其他的异常,如ArithmeticExceptionNumberFormatException等,这些异常均有各自的应用场合,然而需要说明的是,这些异常的应用场合在有的时候界限不是非常分明,至于该选择哪个比较合适,则更多的需要依赖上下文环境去判断。

      最后需要强调的是,一定要确保抛出异常的条件和该异常文档中描述的条件保持一致。   

六十一、抛出与抽象相对应的异常: 

      如果方法抛出的异常与它所执行的任务没有明显的关系,这种情形将会使人不知所措。特别是当异常从底层开始抛出时,如果在中间层没有做任何处理,这样底层的实现细节将会直接污染高层的API接口。为了解决这样的问题,我们通常会做出如下处理:

  
  
  
  
  1. try { 
  2.          doLowerLeverThings(); 
  3.      } catch (LowerLevelException e) { 
  4.          throw new HigherLevelException(...); 
  5.      }  

这种处理方式被称为异常转译。事实上,在Java中还提供了一种更为方便的转译形式--异常链。试想一下上面的示例代码,在调试阶段,如果高层应用逻辑可以获悉到底层实际产生异常的原因,那么对找到问题的根源将会是非常有帮助的,见如下代码:

  
  
  
  
  1. try { 
  2.          doLowerLevelThings(); 
  3.      } catch (LowerLevelException cause) { 
  4.          throw new HigherLevelException(cause); 
  5.      } 

  底层异常作为参数传递给了高层异常,对于大多数标准异常都支持异常链的构造器,如果没有,可以利用ThrowableinitCause方法设置原因。异常链不仅让你可以通过接口函数getCause访问原因,它还可以将原因的堆栈轨迹集成到更高层的异常中。

      通过这种异常链的方式,可以非常有效的将底层实现细节与高层应用逻辑彻底分离出来。   

  
  
  
  
  1. public Object pop() { 
  2.          if (size == 0
  3.              throw new EmptyStackException(); 
  4.          Object result = elements[--size]; 
  5.          elements[size] = null
  6.          return result; 
  7.      }   

六十三、在细节中包含能捕获失败的信息: 

    当程序由于未被捕获的异常而失败的时候,系统会自动地打印出该异常的堆栈轨迹。在堆栈轨迹中包含该异常的字符串表示法,即toString方法的返回结果。如果我们在此时为该异常提供了详细的出错信息,那么对于错误定位和追根溯源都是极其有意义的。比如,我们将抛出异常的函数的输入参数和函数所在类的域字段值等信息格式化后,再打包传递给待抛出的异常对象。假设我们的高层应用捕捉到IndexOutOfBoundsException异常,如果此时该异常对象能够携带数组的下界和上界,以及当前越界的下标值等信息,在看到这些信息后,我们就能很快做出正确的判断并修订该Bug

    特别是对于受检异常,如果抛出的异常类型还能提供一些额外的接口方法用于获取导致错误的数据或信息,这对于捕获异常的调用函数进行错误恢复是非常重要的。

    六十四、努力使失败保持原子性: 

      这是一个非常重要的建议,因为在实际开发中当你是接口的开发者时,经常会忽视他,认为不保证的话估计也没有问题。相反,如果你是接口的使用者,也同样会忽略他,会认为这个是接口实现者理所应当完成的事情。

      当对象抛出异常之后,通常我们期望这个对象仍然保持在一种定义良好的可用状态之中,即使失败是发生在执行某个操作的过程中间。对于受检异常而言,这尤为重要,因为调用者希望能从这种异常中进行恢复。一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有"失败原子性"

      有以下几种途径可以保持这种原子性。

      1. 最简单的方法是设计不可变对象。因为失败的操作只会导致新对象的创建失败,而不会影响已有的对象。

      2. 对于可变对象,一般方法是在操作该对象之前先进行参数的有效性验证,这可以使对象在被修改之前,抛出更为有意义的异常,如:

  
  
  
  
  1. public Object pop() { 
  2.          if (size == 0
  3.              throw new EmptyStackException(); 
  4.          Object result = elements[--size]; 
  5.          elements[size] = null
  6.          return result; 
  7.      }  

 如果没有在操作之前验证sizeelements的数组也会抛出异常,但是由于size的值已经发生了变化,之后再继续使用该对象时将永远无法恢复到正常状态了。

      3. 预先写好恢复性代码,在出现错误时执行带段代码,由于此方法在代码编写和代码维护的过程中,均会带来很大的维护开销,再加之效率相对较低,因此很少会使用该方法。

      4. 为该对象创建一个临时的copy,一旦操作过程中出现异常,就用该复制对象重新初始化当前的对象的状态。

      虽然在一般情况下都希望实现失败原子性,然而在有些情况下却是难以做到的,如两个线程同时修改一个可变对象,在没有很好同步的情况下,一旦抛出ConcurrentModificationException异常之后,就很难在恢复到原有状态了。   

六十五、不要忽略异常: 

      这是一个显而易见的常识,但是经常会被违反,因此该条目重新提出了它,如:

  
  
  
  
  1. try { 
  2.          dosomething(); 
  3.      } catch (SomeException e) { 
  4.      }  

 可预见的、可以使用忽略异常的情形是在关闭FileInputStream的时候,因为此时数据已经读取完毕。即便如此,如果在捕获到该异常时输出一条提示信息,这对于挖出一些潜在的问题也是非常有帮助的。否则一些潜在的问题将会一直隐藏下去,直到某一时刻突然爆发,以致造成难以弥补的后果。

      该条目中的建议同样适用于受检异常和未受检的异常。

六十六、同步访问共享的可变数据: 

      Java中很多时候都是通过synchronized关键字来实现共享对象之间的同步的。事实上,对象同步并不仅限于当多个线程操作同一可变对象时,仍然能够保证该共享对象的状态始终保持一致。与此同时,他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。

      Java的语言规范保证了读写一个变量是原子的,除非这个变量的类型为longdouble。换句话说,读取一个非longdouble类型的变量,可以保证返回的值是某个线程保存在该变量中的,即时多个线程在没有同步的情况下并发地修改这个变量也是如此。然而需要特别指出的是,这样的做法是非常危险的。即便这样做不会带来数据同步修改的问题,但是他会导致另外一个更为隐匿的错误发生。见如下代码:

  
  
  
  
  1. public class StopThread { 
  2.          private static boolean stopRequested = false
  3.          public static void main(String[] args) throw InterruptedException { 
  4.              Thread bgThread = new Thread(new Runnable() { 
  5.                  public void run() { 
  6.                      int i = 0
  7.                      while (!stopRequested) 
  8.                          i++; 
  9.                  } 
  10.              }); 
  11.              bgThread.start(); 
  12.              TimeUnit.SECONDS.sleep(1); 
  13.              stopRequested = true
  14.          } 
  15.      }  

   对于上面的代码片段,有些人会认为在主函数sleep一秒后,工作者线程的循环状态标志(stopRequested)就会被修改,从而致使工作者线程正常退出。然而事实却并非如此,因为Java的规范中并没有保证在非同步状态下,一个线程修改的变量,在另一个线程中就会立即可见。事实上,这也是Java针对内存模型进行优化的一个技巧。为了把事情描述清楚,我们可以将上面代码中run方法的代码模拟为优化后的代码,见如下修改后的run方法:

  
  
  
  
  1. public void run() { 
  2.          int i = 0
  3.          if (!stopRequested) { 
  4.              while (true
  5.                  i++; 
  6.          } 
  7.      }  

这种优化被称为提升,正是HotSpot Server VM的工作。

      要解决这个问题并不难,只需在读取和写入stopRequested的时候加入synchronized关键字即可,见如下代码:

  
  
  
  
  1. private static boolean stopRequested = false
  2. private static synchronized void requestStop() { 
  3.     stopRequested = true
  4. private static synchronized boolean stopRequested() { 
  5.     return stopRequested; 
  6. public static void main(String[] args) throw InterruptedException { 
  7.     Thread bgThread = new Thread(new Runnable() { 
  8.         public void run() { 
  9.             int i = 0
  10.             while (!stopRequested()) 
  11.                 i++; 
  12.         } 
  13.     }); 
  14.     bgThread.start(); 
  15.     TimeUnit.SECONDS.sleep(1); 
  16.     requestStop(); 
  17.  

 在上面的修改代码中,读写该变量的函数均被加以同步。

      事实上,Java中还提供了另外一种方式用于处理该类问题,即volatile关键字。该单词的直译为“易变的”,引申到这里就是告诉cpu该变量是容易被改变的变量,不能每次都从当前线程的内存模型中获取该变量的值,而是必须从主存中获取,这种做法所带来的唯一负面影响就是效率的折损,但是相比于synchronized关键字,其效率优势还是非常明显的。见如下代码:

  
  
  
  
  1. public class StopThread { 
  2.          private static volatile boolean stopRequested = false
  3.          public static void main(String[] args) throw InterruptedException { 
  4.              Thread bgThread = new Thread(new Runnable() { 
  5.                  public void run() { 
  6.                      int i = 0
  7.                      while (!stopRequested) 
  8.                          i++; 
  9.                  } 
  10.              }); 
  11.              bgThread.start(); 
  12.              TimeUnit.SECONDS.sleep(1); 
  13.              stopRequested = true
  14.          } 
  15.      } 

和第一个代码片段相比,这里只是在stopRequested域变量声明之前加上volatile关键字,从而保证该变量为易变变量。然而需要说明的是,该关键字并不能完全取代synchronized同步方式,见如下代码:

  
  
  
  
  1. public class Test { 
  2.          private static volatile int nextID = 0
  3.          public static int generateNextID() { 
  4.              return nextID++; 
  5.          } 
  6.      } 

 generateNextID方法的用意为每次都给调用者生成不同的ID值,遗憾的是,最终结果并不是我们期望的那样,当多个线程调用该方法时,极有可能出现重复的ID值。这是因为++运算符并不是原子操作,而是由两个指令构成,首先是读取该值,加一之后再重新赋值。由此可见,这两个指令之间的时间窗口极有可能造成数据的不一致。如果要修复该问题,我们可以使用JDK(1.5 later)java.util.concurrent.atomic包提供的AtomicLong类,使用该类性能要明显好于synchronized的同步方式,见如下修复后的代码:

  
  
  
  
  1. public class Test { 
  2.          private static final AtomicLong nextID = new AtomicLong(); 
  3.          public static long generateNextID() { 
  4.              return nextID.getAndIncrement();  
  5.          } 
  6.      } 

你可能感兴趣的:(java,职场,休闲)