码迷,mamicode.com
首页 > 编程语言 > 详细

编写高质量JAVA程序代码的建议

时间:2014-08-05 14:10:39      阅读:449      评论:0      收藏:0      [点我收藏+]

标签:java

---------------------------------------------------------------------------------------------------
       前言:原著《改善JAVA程序的151个建议》有151个建议,我在拜读的过程根据自己的理解合并了其中的几个,并将每个建议的核心要义进行了一次纯手工提炼,以方便想阅读这本书的同行能够更快的掌握这本书的所有核心内容。
---------------------------------------------------------------------------------------------------
建议1 不要在常量和变量中出现易混淆的字母   
    为了让程序更容易理解,字母“l”、“O”尽量不要与数字混用,若必须使用,字母“l”务必大写,字母“O”增加注释。
       例如:long i = 1L;   
建议2  不要让常量变成变量
      保持常量不变,增强程序的可读性。
      例如:public static final int RAND_CONST = new Random().nextInt();  // 此方式不可取。
建议3  三元操作符的类型务必保持一致
      三元操作符类型转换规则:a.若两个操作数不可转换,则不做转换,返回Object类型;b.若两个操作数可转换,则返回范围较大者的类型。
      例如:int i = 80; 
               String s1 = String.valueOf(i<100?90:100);
               String s2 = String.valueOf(i<100?90:100.0);
               System.out.println(s1.equals(s2));   // 结果为false,因为s1="90",s2="90.0"。
建议4  避免重载带有变长参数的方法
       例如:public void calPrice(int price, int discount){ ......  } // 方法1
                 public void calPrice(int price, int ... discount)( ...... ) // 方法2
                 client.calPrice(1000, 75); // 当客户端调用时,编译器可能不知道调用哪个方法。
建议5  避免null值与空值威胁到变长方法
       为减少错误的发生,调用者应明确指出null值的数据类型,这样编译才能顺利通过。
       例如:public void test(String str, Integer... is){ ... }  // 方法1
                 public void test(String str, String... strs){ ... } // 方法2
                 client.test("china");  // 此方法无法编译通过,因为编译器无法确定调用哪个方法.
                 client.test("china", null); // 此方法无法编译通过,因为编译器无法确定调用哪个方法.
                 String[] strs = null;
                 client.test("china", strs);  //明确指定带入null值的数据类型,编译通过!
建议6  重写的方法参数必须与父类相同,包括类型、数量以及显示型式
       重写父类方法需满足的条件:
  • 重写方法不能缩小访问权限; 
  • 参数列表必须与被重写方法相同;
  • 返回类型必须与被重写方法相同或是其子类;
  • 重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少更有限的异常,或者不抛出异常。
建议7  弄懂自增原理,警惕自增陷阱
    count++处理过程:把count的原值拷贝到一个临时变量区 ---> count加1 ---> 返回临时变量区的值,即原值,代码理解如下:
    public static int mockAdd(int count){
          int temp = count; // 先保存初始值
          count = count + 1;  // 做自增操作
          return temp; // 返回初始值
    }
   例如:
        int count = 0;
        for(int i = 0; i < 10; i++){
              count = count++;
        }
       System.out.println(count); // 结果为0.
   注意:C++语言中count++与count=count++等效;PHP与JAVA的处理方式相同。
建议8  避免使用goto语句实现跳转,甚至少用break或continue
    JAVA保留了goto关键字,且扩展了break和contine关键字,即在其后面可加上标号作为跳转实现goto功能,但它们的使用会降低代码的可读性与可维护性。
    例如:
tag: 
if(b < 6){
System.out.println("agb");
b++;
break tag;  // break后面可加上标号作为跳转.
}
建议9  慎用静态导入
    静态导入语法作用:减少字符输入量;提高代码的可读性,以便更好地理解程序。
    例如:
           import static  java.lang.Math.PI;  // 导入后可直接使用PI的来作为静态变量
    
    但静态导入不可滥用,需遵循以下原则:
  • 不使用 * 通配符,除非是导入静态常量类(只包含常量的类或接口)。
  • 方法名是具有明确、清晰表象意义的工具类。
建议10  避免在本类中覆盖静态导入的变量和方法
  • 若本类需变更一个被静态导入的方法,最好是在原始类中重构,而不是在本类中覆盖。
  • 若本类中的方法覆盖了被导入的静态方法,编译器会根据“最短路径”原则执行覆盖后的方法,即编译器若能在本类中找到方法、变量、常量,则不会到其他包或父类、接口中查找,以确保本类中方法、属性优先。
      例如:
      import static java.lang.Math.PI;
      class Client{
           public static int PI = "修改后的PI";
           public static void main(String[] args){ 
                  System.out.println(PI); // 结果为:修改后的PI 
           }
      }
建议11  显式声明UID(流标识符,即类的版本定义)
  • 实现Serializable接口的目的是为了实现持久化,如网络传输或本地存储,为系统的分布和异构部署提供先决支持条件。
  • serialVersionUID可显式声明,也可隐式声明。
  • UID(序列号)的使用实现了版本向上兼容的功能,提高了代码的健壮性。
 显示声明:private static final long serialVersionUID = 5799L;
 隐式声明:由编译器根据包名、类名、继承关系、非私有的方法和属性、参数、返回值等诸多因子计算出来,且生成的值是唯一的。
建议12  避免给序列化类中的不变量赋值
 序列化与反序列化的规则:
  • 保持新旧对象的final变量相同,有利于代码业务逻辑统一。
  • 反序列化时构造函数不会执行。
  • 序列化不保存静态变量的值。

例如:public class Test implements Serializable {
        private static final long serialVersionUID = 1L;
        public static int staticVar = 5;
        public static void main(String[] args) {
                try {
                        //初始时staticVar为5
                        ObjectOutputStream out = new ObjectOutputStream(
                                        new FileOutputStream("result.obj"));
                        out.writeObject(new Test());
                        out.close();

                        //序列化后修改为10
                        Test.staticVar = 10;

                        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
                                        "result.obj"));
                        Test t = (Test) oin.readObject();
                        oin.close();
                        
                        System.out.println(t.staticVar); // 结果为10,因为序列化不保存静态变量的值
                        
                } catch (FileNotFoundException e) {
                        e.printStackTrace();
                } catch (IOException e) {
                        e.printStackTrace();
                } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                }
        }
}
    总结:反序列化时final变量不会被重新赋值的情况:
  • 通过构造函数为final变量赋值。
  • 通过方法返回值为final变量赋值。
  • final修饰的属性不是基本数据类型(包括8个基本类型、数组、不通过new生成的字符串,但不能方法赋值)。
   建议13  使用序列化类的私有方法巧妙解决部分属性持久化问题
      在实现的序列化的类中实现两个私有方法:writeObject和readObject,以影响序列化与反序列化的过程(核心代码为黑体部分)。
      例如:
      public class Person implements Serialable{
             private static final long SerialVersionUID = 23324L;
             private String name; // 姓名
             private Salary salary; // 薪水
             public Person(String _name, Salary _salary){
                 name = _name;
                 salary = _salary;
             }
            private void writeObject(java.io.ObjectOutputStream out) throws IOException{
                 out.defaultWriteObject();
                 out.writeInt(salary.getBasePay());
            }
            private void readObject(java.io.ObjectInputStream in) throws IOException{
                in.defaultReadObject();
                salary = new Salary(in.readInt(), 0);
            }
      }
   建议14  在编写switch代码时,case子句后面的break切不可忘记
        如果case子句后面的break子句忘记书写,在软件投入运营后,可能会造成严重的损失。所以为了能够避免此类错误的发生,可修改IDE的警告级别::windows-->preferences-->java-->compiler-->errors/Warmings,设置相关的错误级别。
  建议15  易变业务使用脚本语言
      脚本语言特点:
  • 灵活。脚本语言一般都是动态类型,不用声明变量类型而直接运用,也可在运行期内改变类型。
  • 便捷。脚本语言是一种解释型语言,靠解释器解释执行,可在运行期变更代码,而不用停止应用。
  • 简单。一般的脚本语言都比较简单,易学易用。
  建议16   慎用动态编译
    动态编译的注意事项:
  • 在框架中谨慎使用;
  • 不要在要求高性能的项目中使用;
  • 动态编译要考虑安全问题;
  • 记录动态编译过程。
 建议17   避免instanceof非预期结果
     instanceof用来判断一个对象是否是一个类的实例,其操作类似于>=、==。注意:若做操作符为null,则直接返回false.
 建议18   断言绝对不是鸡肋  
     语法:assert <布尔表达式>       assert <布尔表达式>:<错误信息>
     特性:A. assert默认是不启用的;B. assert抛出的异常AssertionError是继承自Error的。
     使用原则:按照正常执行逻辑不可能到达的代码区域可以放置assert。具体分为三种情况:
  • 在私有方法中放置assert作为输入参数的校验;
  • 流程控制中不可能达到的区域;
  • 建立程序探针。    
建议19   不要只替换一个类   
     发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是完全之策。
建议20   用偶判断,不用奇判断  
     JAVA中的取余算法模拟如下:
     // dividend为被除数,divdisor为除数
     public static int remainder(int dividend, int divisor){
          return dividend - dividend / divisor * divisor;  
     }
     通过以上的模拟算法可知,若使用奇判断,当输入-1时,计算结果为-1,不为1,判断结果为偶数,因此,为避免出错,应使用偶判断。
建议21   用整数类型处理货币计算
    由于计算机无法进行浮点数的精确计算,因此在运用JAVA进行商业运算时,都不能用float和double.要解决此类问题,有两种方法:
  • 使用BigDecimal。BigDecimal是专门为了弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学运算,特别是与数据库的decimal类型的字段映射时,BigDecimal是最优先的解决方案。但在进行BigDecimal对象的构造时,建议使用String类型作为传入参数,而少用float、double等基本数据类型行,因为float、double等类型本身存在不精确的问题。
  • 使用整数。把参与运算的值增大一定的倍数使其成为整数后再进行简单的计算,在展示时再缩小相应的倍数。
建议22   不要让类型默默转换
     基本类型转黄时,使用主动声明方式减少不必要的bug。通过主动显式声明类型转换(注意不是强制类型转换,因为强制转换的过程是先计算出结果后才进行类型转换),
     例如:
     long result = 1L * LIGHT_SPEED * 60 * 8; // LIGHT_SPEED为光速.  
建议23   注重边界测试
     在单元测试中,对int等类型进行边界测试,三个比测值:0、正最大、负最小,其中正最大、负最小是边界值,若这三个值没有问题,方法才是比较安全可靠的。虽然在web前端用JS进行了严格的校验,但对于高手来说,这些都只是摆设,HTTP是明文传输的,拦截几次,分析一下数据结构,然后再写一个模拟器,一切前端校验都成了浮云。因此,在后端进行边界测试才能较好保证程序的安全运行。
建议24   慎用四舍五入  
     JAVA支持七种舍入方式:
  • ROUND_UP:远离零方向舍入,即向绝对值最大的方向舍入,只要舍弃位非0即进位。
  • ROUND_DOWN:趋向零方向舍入,即向零方向靠拢,注意所有的位都舍弃,不存在进位情况。
  • 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:银行家算法,主要运用于商业运算的项目中,以确保相关数据计算的准确性。
    因此,根据不同过得场景,慎重选择不同的舍入模式,以提高项目的精准度,减少算法损失。
建议25   提防包装类型的null值
    Java引入包装类型是为了解决基本类型的实例化问题,以便让每一个基本类型都能参与到面向对象的编程世界中,但在运用包装类参与运算时,要做Null值校验。
    例如:
    for(Integer i : intList){
          count += (i != null) ? i : 0; // 应该对i值进行null判断,否则可能出现异常  
    }   
建议26   谨慎包装类型的大小比较
     在java中,
  • ==:用于判断两个操作数是否相等,如果是基本类型,则判断值是否相等;若为对象,则判断是否是一个对象的两个引用,即地址是否相等。
  • > 与 < :用于判断两个数字类型的大小关系,注意只能是数字类型的判断。对于Integer包装类,根据其intValue()方法的返回值来判断。
     例如:
     Integer i = new Integer(1);
     Integer j = new Integer(1);
     System.out.println(i == j); // 结果为false
     System.out.println(i > j); // 结果为false
     System.out.println(i < j); // 结果为false
因此,对于两个对象的比较中,应该采用相应的对象的方法,而不是通过Java的默认机制来处理。
建议27   优先使用整形池
       先看如下代码:输入127的输出结果为:false/true/true;输入128的结果为:false/false/false
       Scanner in = new Scanner(System. in);
       while(in.hasNextInt()){
                   int ii = in.nextInt();
                   System. out.println( "*****   值相等判断    ******" );
                   Integer i = new Integer(ii);
                   Integer j = new Integer(ii);
                   System. out.println( "new 产生的对象:" +(i == j));
                                    
                   i = ii;
                   j = ii;
                   System. out.println( "基本类型转换:" +(i == j));
                                    
                    i = Integer. valueOf(ii);
                    j = Integer. valueOf(ii);
                    System. out.println( "valueOf产生的对象:" +(i == j));
        }
    分析:(1)用new产生的Integer两个不同的对象,结果肯定全部为false; 
              (2)"=="和valueOf方法利用到了整型池来装箱生成对象,若i不在-128与127范围内,就会生成新对象,否则就直接从整型池中获取,因此造成了不同的结果。
    底层代码如下:
              public static Integer valueOf(int i) {       
if(i >= -128 && i <= 127)           
      return IntegerCache.cache[i + 128];       
return new Integer(i);
     }
     因此,通过包装类的valueOf生成包装实例可以显著提高空间和时间性能。另外,判断对象是否相等,最好使用equals方法,避免使用"=="产生非预期结果。  
建议28   优先选择基本类型
     包装类型虽然提供了很多非常使用的方法,但从安全性、性能、稳定性方面,基本类型是首选方案。
建议29   不要随便设置随机数种子
      随机数与种子之间遵循如下关系:
  • 种子不同,产生不同的随机数;
  • 种子相同,即使十里不同也产生相同的随机数。
     说明:(1) Random类的默认种子是System.nanoTime()的返回值,JDK1.5以前默认种子为System.currentTimeMillis()的返回值,另外,System.nanoTime不能用于计                    
                    算日期,因为固定的时间是不确定的,纳秒值甚至是负值,这点与System.currentTimeMillis不同。
               (2) Java获取随机数的两种方法:A. java.util.Random类获取;B. java.lang.Math.random()方法获取,该方法通过生成Random实例后,然后委托nextDouble()
                    方法生成随机数,两者殊途同归,没有差别。 
     因此,若非必要,不要设置随机数种子。
建议30   在接口中不要存在实现代码
      接口是一种契约,是一种框架协议,这表明它的实现类都是同一种类型,或者是具备相似特征的一个集合体。它不仅仅约束着实现者,同事也是一个保证,保证提供的服务(常量、方法)是稳定、可靠的,若把实现代码写到接口中,那接口就绑定了可能变化的因素,这就导致实现不再稳定和可靠,是随时都可能被抛弃、被更改、被重构的。因此接口中虽然可以有实现,但应避免使用。
 例如:
      // 在接口中存在实现代码
      interface B{
          public static final s = new S(){
               public void doSomething(){  System.out.println("我在接口中实现了"); }
          }
      }
     // 被实现的接口
     interface S{  public void doSomething();   }
     // 客户端调用
     public class Client{ 
     public static void main(String[] args){
            B.s.doSometing(); // 打印出“我在接口中实现了”
     }
     } 
建议31   静态变量一定要先声明后赋值
  • 静态变量是类加载时被分配到数据区的,它在内存中只有一个拷贝,不会被分配多次,其后的所有赋值操作都是值改变,地址则保持不变。
  • 静态变量在被加载后,JVM会去查找所有的静态声明,然后分配空间,注意此时只完成了地址空间的分配,还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。
  • 虽然对于实例变量来说变量定义与位置没有关系,但对于静态变量来说,而且还在静态块中进行了赋值,那就可能造成结果的不一致。
例如:
public class Client{
     public static int i = 1;
     static{  i = 100;  }
     public static void main(String[] args){  System.out.println(i);   // 结果为:100  }
}
public class Client{
     static{ i = 100; }
     public static int i = 1;
     public static void main(String[] args){  System.out.println(i);   // 结果为:1  }
}
      因此,在进行编码时,尽量遵循java通用的开发规范"变量先声明后使用"是一个良好的编码风格。
建议32   不要覆写静态方法    
      1.  Java中可以通过覆写来增强或减弱父类的方法和行为,但覆写是针对非静态方法(也叫实例方法,只能生成实例才能调用的方法)的,不能针对静态方法(static修饰的方法,也叫做类方法)。
      2.  一个实例对象,有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。非静态方法是根据对象的实际类型来执行的,而静态方法,其一是静态方法不依赖实例对象,是通过类名来访问的;其二是可以通过对象方法访问静态方法,若通过对象调用静态放,JVM会通过对象的表面类型查找静态方法的入口,继而执行之。
     3.  在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限、并且父类、子类都是静态方法,此种行为叫做隐藏。
     4.  隐藏与覆写的比较:
  • 表现形式不同。隐藏用于静态方法,覆写用户非静态方法。
  • 职责不同。隐藏的目的是抛弃父类的静态方法,重现子类方法;而覆写则是将父类的方法增强或减弱,延续父类的职责。
     因此,静态方法虽然不能覆写,但是可以隐藏。另外,为提高代码的可读性,应尽量避免使用实例对象访问静态方法。
建议33    构造函数尽量简化
     构造函数简化,再简化,应该达到"一眼洞穿"的境界。
建议34    避免在构造函数中初始化其他类   
     不要在构造函数中声明初始化其他类,养成良好的习惯。
建议35    使用构造代码块精炼程序  
   1.  代码块:用大括号将多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合。
   2.  代码块类型:
  • 普通代码块:在方法后面用大括号括起来的代码片段,它不能单独执行,只能通过方法名调用执行;
  • 静态代码块:在类中使用static修饰,并使用大括号括起来的代码片段,用于对象变量的初始化或对象创建前的环境初始化;
  • 同步代码块:使用synchronized修饰,并使用大括号括起来的代码片段,它表示同一时间只能有一个线程进入到该代码块中,是一种多线程保护机制;
  • 构造代码块:在类中没有任何前缀或后缀,并使用大括号括起来的代码片段。
  3.  在使用new关键字创建实例对象时,JVM会把构造代码块中的代码片段自动添加到每个构造函数内首先执行(注意:构造代码块不是在构造函数之前运行,它依托于        
       构造函数执行)。
       例如:
  public class Client{
     // 构造代码块
     {    System.our.println("我是构造代码块.");       }

     // 无参构造函数
     public Client(){  System.out.println("我是无参构造函数."); }

     // 有参构造函数
      poublic Client(String str){  System.out.println("我是有参构造函数.");   }
  }
当通过new关键字生成一个实例时,JVM会把构造代码块插入到每个构造函数的最前端,等价代码如下:
  public class Client{
     // 无参构造函数
     public Client(){ 
           System.our.println("我是构造代码块.");  
           System.out.println("我是无参构造函数."); 
      }

     // 有参构造函数
      poublic Client(String str){  
 System.our.println("我是构造代码块.");  
 System.out.println("我是有参构造函数.");   
       }
  }
      4.  运用场景:
  • 初始化实例变量:若每个构造函数都要初始化变量,可通过构造代码块来实现。当然也可通过定义一个方法再调用该方法来实现,但没个构造函数都要调用方法,使用不够方便灵活;
  • 初始化实例环境:一个对象必须在适当的场景下才能存在,若没有适当场景,则需在创建对象时创建此场景。如
      5.  好处:减少代码量;可读性强。
建议36    构造代码块会想你所想
      我们知道,JVM会把代码块自动插入到每个构造函数的最前端,但有两个例外情况:
  • 若遇到this关键字,则不插入代码块,即Java只把构造代码块插入到没有this关键字的构造函数中,而调用其他构造函数的则不插入,确保每个构造函数只执行一次构造代码块;
  • 若遇到super关键字,JVM会把构造代码块插入到super()方法之后执行。
     例如:
     public class Base{
           { System.our.println("我是构造代码块.");  }
           public Base(String str){  this();  } // 当执行构造函数时,只将代码块放在this()方法前运行一次,而不会插入到this中
     } 
    因此,放心地使用构造代码块把,Java已经想你所想了。
建议37   使用静态内部类提高封装性
     Java中的嵌套类分为静态内部类和内部类两种。只有在是在静态内部类的情况下才能将static修饰符放在类的前面,其他任何时候static都是不能修饰类的。
     静态内部类的作用:
  • 提高封装性;
  • 提高代码的可读性;
  • 形似内部,神似外部。静态内部类虽然存在于外部类中,而且编译后的类文件名也包含外部类,但它是可以脱离外部类独立存在的,即可以通过new生成实例。
    静态内部类与普通类的区别:
  • 静态内部类不持有对外部类的引用。在普通内部类中,可直接访问外部类的中的包括有private修饰的所有属性、方法,这是因为内部类持有对外部类的一个引用,可以自由访问;而静态内部类则只可以访问外部类的静态方法和静态属性(包括private修饰的),其他则不能访问。
  • 静态内部类不依赖外部类。普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例而独立存在;而静态内部类可以独立存在,即使外部类消亡,静态内部类还是可以存在的;
  • 普通内部类不能使用static修饰变量和方法。注意:这里说的是变量,常量(final static修饰的属性)还是可以的;而静态内部类形似外部类,没有任何限制。
建议38   使用匿名类的构造函数
     匿名类的构造函数就是构造代码块,而类中的构造代码块可以是多个的。
     例如:
  •        List  list1 = new ArrayList();  // 声明ArrayList的实例对象
  •        List  list2 = new ArrayList(){};  // 匿名类,代码类似于:public class Sub extends ArrayList{};  List list2 = new Sub();
  •        List  list3 = new ArrayList(){ {} }; // 匿名类,代码类似于:public class Sub extends ArrayList{ { // 此处为构造函数代码块 } };  List list3 = new Sub();
  •        List  list4 = new ArrayList(){{}{}{}}; // 匿名类,可使用多个构造代码块.
      因此,学会使用匿名内部类可简化代码,提高代码的可读性。
建议39   匿名类的够咱函数很特殊
      一般类的所有构造函数默认都是调用父类的无参构造的,而匿名类由于没有名字,只能由代码块代替,也就无所谓的有参无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后调用了自己的构造代码块。
建议40      让多重继承成为现实
  • Java中提供的内部类可以解决多重继承的问题,内部类可以继承一个与外部类无关的类,保证了内部类的独立性,基于这一点,多重继承成为可能。
  • 多重继承指的是一个类可以同时从多于一个父类的那里继承行为和特征的类。
    例如:
   // 父亲接口
   interface Father{ public int strong();  }
   // 母亲接口
   interface Mother{ public int kind(); }
   // 实现父亲接口
   public class FatherImpl{
          @Override
          public int strong(){   return 8;   }
   }
  // 实现母亲接口
  public class MotherImpl{
          @Override
          public int kind(){   return 8;   }
   }
  // 儿子继承母亲与父亲的属性 
  public class  Son extends FatherImpl implements Mother{
       @Override
       public int strong(){  return super.strong()+1; // 儿子比父亲强壮  }
       @Override
       public int kind(){  return new MotherSpecial().kind();     }
       // 母亲内部类定义
       private class MotherSpecial extends MotherImpl{
             public int kind(){  return super.kind() - 1; // 儿子温柔指数降低  }
       }
  }
建议41      让工具类不可实例化
      为了保证工具类不可实例化,可以通过同时使用以下两种方式来限制:
  • 设置private权限;
  • 在构造函数中抛出异常。
     例如:
     public class UtilClass{
           private UtilClass(){ throw new Error("不要实例化我!"); }
     }
    因此,如果一个类不允许实例化,就要保证平常渠道都不能实例化它。
建议42   避免对象的浅拷贝
     Java中实现了Cloneable接口的类就具备了拷贝能力,若覆写clone()方法,就会完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快的多。每个类都继承自Object类,而Object提供一个对象拷贝的默认方法,但该方法有有缺陷,它提供的是一种浅拷贝的方式。拷贝规则如下:
  • 基本类型:若变量是基本类型(int,float,double等),则拷贝其值;
  • 对象:若变量是一个实例对象,则拷贝地址引用,即拷贝出的新对象与原有对象共享该实例变量,不受访问权限的限制;
  • String字符串:这个比较特殊,拷贝的也是一个地址引用,但在修改时,它会从字符串池中重新生成新的字符串,原有的字符串变量保持不变,此处可认为String是一个基本类型。
因此,浅拷贝只是Java提供的一种简单拷贝机制,不便于直接使用。
建议43   推荐使用序列化实现对象的拷贝   
      从上一建议中可以看出,拷贝要比直接使用new关键字生成实例对象的效率要快的多,但有大量的对象通过拷贝生成的话,那每个对象都要写一个clone方法,这就导致很大的工作量。因此,可以使用序列化方式来实现,在内存中通过字节流的拷贝来实现,即把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象不存在引用共享的问题,也就相当于一次深拷贝。
      例如:
      public class CloneUtils{
           // 拷贝一个对象
           public static <T extends Serializable> T clone(T obj){
               // 拷贝产生的对象
               T clonedObj = null;
               try{
                    // 读取对象字节数据
                    ByteArrayOutputStream baos = new ByteArrayOutputStream ();
                    ObjectOutputStream oos = new ObjectOutputStream(baos);
                    oos.writeObject(oos);
                    oos.close();
                    // 分配内存空间,写入原始对象,生成新对象
                    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
                    ObjectInputStream ois = new ObjectInputStream(bais);
                    // 返回新对象,并做类型转换
                    clonedObj = (T)ois.readObject();
                    ois.close();
               }catch(Exception e){
                    e.printStackTrace();
               }
               return clonedObj;
           }
      }
      使用该方法的注意事项:
  • 该工具类要求被拷贝的对象必须继承Serialiable接口,否则是无法实现拷贝的(反射是另一种技巧);
  • 对象的内部属性都是可以序列化的。若有内部属性不可序列化,则会抛出异常;
  • 注意方法和属性的特殊修饰符:注意final、static、transient修饰符对拷贝的影响。
     另外,采用序列化拷贝的更简单的方法是:使用Apache下commons工具包中的SerializationUtils类,直接使用更加简洁。
建议44   关注equals方法遵循的原则及覆写注意事项
      equals方法遵循的原则及覆写注意事项:
  • 自反性原则:对于任意非空引用x,x.equals(x)应该返回true;
  • 对称性原则:对于任意的引用x和y,若x.equals(y)返回true,则y.equals(x)也返回true,即覆写equals方式时应考虑额null值的情景;
  • 传递性原则:对于实例x、y、z来说,若x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也应该返回true,即覆写方法时应使用getClass方法进行类型判断,而不要使用instanceof;
  • 覆写equals方法必须覆写hashCode方法,覆写hashCode方法时,建议使用org.apache.commons.lang.builder包下的HashCodeBuilder类来直接实现。
建议45   推荐覆写toString方法
      为了在开发项目过程中调试bug,建议使用org.apache.commons.lang.builder包下的ToStringBuilder类来覆写toString方法,简介、实用又方便。
建议46   使用package-info类为包服务
     package-info类是Java中的一个特俗的类,主要体现在以下3个方面:
  • 它不能随便被创建。在一般的IDE中,Eclipse、package-info等文件时不能随便被创建的,会报"Type name is notvalid"错误,类名无效,而允许的字符有:字母、数字、下划线、$;创建方法:用记事本创建拷贝到IDE中后再修改一下;或者直接从别的项目中拷贝过来。
  • 它的服务对象很特殊。它用于描述和记录本包信息。
  • package-info不能有实现代码。它会被编译成package-info.class,但在package-info.java文件里不能声明package-info类。
  • 类不能带有public、private访问权限。
  • 不可继承,没有接口,没有类间关系(关联、组合、聚合等)。
    package-info类的作用:
  • 声明友好类和包内访问常量。如一个包中有很多内部访问的类或常量,可统一放在package-info中,便于集中管理,减少友好类到处游走的情况。
  • 为在包上标注注解提供便利。
  • 提供包的整体注释说明。
    总之一句话,在需要用到包的地方,就可以考虑一下package-info这个特殊类,也许能够起到事半功倍的效果。
建议47   不要主动进行垃圾回收   
     不要主动使用类似System.gc()等方法主动进行垃圾回收,因为System.gc()要停止所有的响应才能检查内存中是否存在可回收的对象,这对一个系统来说风险极大。  
建议48   推荐使用String直接量赋值
      1.  String字符串是程序中最常使用的类型,于是就设计了一个字符串常量池。
      2.  字符串常量池的运行机制:创建一个字符串时,首先检查字符串常量池中是否存在字面值相等的字符串,若存在,则不再创建,而直接返回池中该对象的引用,若没有则创建之,然后放到池中,再返回新建对象的引用。
      3.  三种不同的方式生成字符串对象:
  • 通过等号("=")直接赋值:运用字符串常量的运行机制创建;
  • 通过new关键在创建:直接生成一个String对象,而不检查字符串常量池,而不会放到池中;
  • 通过intern方法创建:intern会检查当前的对象在对象池中是否已经存在有字面值相等的相同的引用对象,若有,则直接返回池中的对象,若没有,则放到对象池中,并返回当前对象。
      4.  关于对象池中线程安全问题的解决,String类是一个不可变的对象:
  • String是final类,不可继承,不可能产生一个String类的子类;
  • String提供的所有方法中,若有返回值,就会新建一个String对象,不对原对象进行修改。这就保证了原对象是不可改变的。
      因此,为了提高效率,减少内存空间的占用,应尽量使用直接量赋值的方式创建String对象。
建议49   注意方法中传递的参数要求     
     事例代码:
       String src = "好是好";
       String replaced =  src.replaceAll("好", "");   // 返回结果为:“是”
       src = "$是$";
       replaced = src.replaceAll("$", ""); // 返回的结果为:"$是$".
      因此,由于replaceAll方法的第一个参数为正则表达式,所以为了避免以上原因造成的错误,建议使用replace方法,它传递两个String参数继续替换。
建议50   正确使用String、StringBuffer、StringBuilder
       
      String、StringBuffer、StringBuilder三个类都实现了CharSequence接口,它们的区别如下:
  • String类是不可改变的量,不能被修改,即使通过String提供的方法尝试修改,也是要么创建一个新的字符串对象,要么返回自己。如str.substring(1)就会新建一个字符串对象返回,str.subString(0)就是返回自己;
  • StringBuffer是一个可变字符序列,与String类一样,在内存中保存的都是一个有序的字符序列(Char字符数组),不同点是,StringBuffer对象的值是可变的。并且,StringBuffer是线程安全的,它的每个方法都加上了synchronized关键字;
  • StringBuilder也是可变字符序列,只是它是线程不安全的;
  • 在性能方面,String类的操作都是产生新的对象,而StringBuffer和StringBuilder只是一个字符数组的再扩容而已,所以String类的操作员慢于StringBuffer和StringBuilder.
     由以上的分析出不同使用场景:
  • 使用String类的情景:在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量变量运算等;
  • 使用StringBuffer的情景:在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程的运行环境中,则可考虑使用StringBuffer,如XML解析、HTTP参数解析与封装等;
  • 使用StringBuilder的情景:使用StringBuffer的情景:在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程的运行环境中,则可考虑使用StringBuffer,如SQL语句的拼接、JSON封装等。
建议51   注意字符串的位置
      Java对加号的处理机制:在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转换为String类型进行拼接;若为原始数据,则直接拼接;若为对象,则调用toString方法的返回值进行拼接。因此,在"+"表达式中,String字符串具有最高的优先级。
建议52   自由选择字符串拼接方法
      Java中对一个字符串的拼接有三种方式:
  • "+"方法拼接字符串,原理为:
           str = new StringBuilder(str).append("待拼接的字符串").toString()

           由此可见,每一次的拼接都会生成一个StringBuilder实例对象,然后调用toString方法转换为字符串,这正是影响拼接效率的地方。
  • concat方法拼接字符串,源码如下:
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this ;
        }
        char buf[] = new charcount + otherLen];
        getChars(0, count, buf, 0);
        str.getChars(0, otherLen, buf, count);
        return new String(0, count + otherLen, buf);
    }
          由此可见,整个拼接过程就是一个数组拷贝,虽然在内存中的处理都是原子性操作,速度非常快,但在返回时,都会创建一个新的String实例对象,这就是concat  
          速度慢下来的真正原因。
  • append方法拼接字符串:StringBuilder的append方法直接由父类AbstractStringBuilder类实现,源码如下:  
                public AbstractStringBuilder append(String str){
                      // 若str为null值,则把null作字符串处理
                       if(str == null)  str = "null";
                       int length = str.length();
                       // 字符串长度为0,则返回自身
                       return this;
                       int newCount = count + length;
                       // 追加后的字符数组长度是否超过当前值,若超过,则进行扩容
                      if(newCount > value.length)   expandCapicty(newCount); 
                      // 字符串复制到目标数组
                      str.getChars(0, length, value, count);
                      count = newCount;
                      return this;
                }
          由此可见,整个append方法都在做字符数组处理,加长,然后数组拷贝,这些都是基本的数据处理,没有新建任何对象,只有最后通过toString方法返回时才生     
     成了一个String实例对象。
          通过以上的分析可知,在需要进行大量的字符串拼接的操作中,append方法最快,concat次之,加号最慢。        
建议53   推荐在复杂字符串操作中使用正则表达式      
        正则表达式在字符串的查找、替换、拼接、剪切、复制、删除等方面有着非凡的作用,特别是面对大量的文本字符需要处理时,使用正则表达式可以大幅提供开发效率和系统性能,但正则表达式难以看懂,这也是它难以控制的地方。
建议54   强烈建议使用UTF编码
      Java程序设计的编码包括两部分:
  • Java文件编码:若使用记事本创建一个.java后缀的文件,则文件的编码格式是操作系统默认的编码格式;若用IDE工具创建,则依赖于IDE的设置,Eclipse是操作系统编码(Windows一般是GBK)。
  • class文件编码:通过javac命令生成的后缀名为.class文件的格式为UTF-8编码的UNICODE文件,这在任何操作系统上都是一样的。UTF是UNICODE的存储和传输格式,它是为了解决UNICODE高位占用冗余空间而产生的,使用UTF编码就标志字符集是UNICODE。
      因此,为了避免乱码问题的产生,最好的处理办法就是使用统一的编码格式,并建议各个组件、接口、逻辑层都使用UTF-8编码,拒绝独树一帜的情况。
建议55    对字符串排序持一种宽容的态度
       Java中字符的比较就是UNICODE码值的比较,对于非英文的String排序可能出现不准确的情况。Java推荐使用Collator类进行排序,示例代码如下:
       String[] strs = {"张三(Z)", "李四(L)", "王五(W)"};
       // 定义一个中文排序器
       Comparator c = Collator.getInstance(Locale.CHINA);
       // 升序排列
       Arrays.sort(strs, c);
       for(String str : strs){
            System.out.println(str); // 打印结果为:李四(L)  王五(W)  张三(Z)
       }
      由于Java使用的是UNICODE编码,而中文字符集来源于GB18030,GB18030又是从GB2312发展起来,GB2312是一个包含了7000多个字符的字符集,它是按照拼音排序,并且是连续的,之后的GBK、GB18030都是在其基础上扩充出来的。
       因此,若排序的对象是要经常使用的汉字,建议使用Collator类来完成排序,毕竟GB2312包含了大部分的汉字。若需要严格排序,可使用pinyin4j等开源包来处理,然后由自己来实现排序算法。
建议56    性能考虑,数组是首选
  •  在Java中虽然数组没有List、Set、Map这些集合类使用方便,但在基本类型处理方面,数组还是占优势的,而且集合类的底层都是通过数组来实现的。
  • 基本类型是在栈内存中操作的,而对象是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大。
  • 对于集合类,在进行诸如求和或者遍历操作时要做拆箱操作,因此产生无谓的开销。
   因此,在性能要求较高的场景中,建议使用数组代替集合类。
建议57    若有必要,使用变长数组
     Java中数组是定长的,一旦被初始化,长度是不可以改变的。但可以通过Arrays数组工具类中的copyOf()方法对数组进行扩容来实现变长数组的功能。
     例如:
     public static <T> T[] expandCapacity(T[] data, newLen){
          // 不能是负值
          newLen = newLen < 0 ? 0 : newLen;
         // 生成一个新数组,并拷贝原值
         return Arrays.copyOf(data, newLen);
     }
     由于集合类的长度自动维护功能的原理与此相似,因此,在实际开发中,如果确实需要变长的数据集,数组也是可以考虑在范围之内的。
建议58    警惕数组的浅拷贝
     通过Arrays数组工具类对数组进行的拷贝是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。数组的clone()方法也是浅拷贝,并且集合类的clone()方法也都是浅拷贝。 解决办法有以下几个建议:
  • 遍历需要拷贝的数组,然后重新生成新的对象,再将新生成的对象放入到新的数组中;
  • 在使用集合(如List)进行拷贝处理时,可集合没有提供拷贝方法,可使用List.toArray方法转换成数组,然后通过Arrays.copyOf拷贝,再转回集合。
建议59    在明确的场景下,为集合指定初始容量
  • ArrayList初始化时,默认长度为10,若在进行add操作时超过容量,系统将按照1.5倍(因为1.5倍满足性能要求,且减少了内存消耗)的规则扩容,每次扩容都是一次数组的拷贝。若数据量很大,则会非常消耗资源,并且效率非常低下(详情参看JDK源码)。
  • Vector与ArrayList类似,默认长度也是10,不同的是Vector提供了递增步长(即每次扩容时要增加的长度),不设置时以2倍规则扩容,并且Vector类是线程安全的,而ArrayList是非线程安全的。
  • HashMap初始化时默认长度为16,默认递增步长为2倍扩容规则,且非线程安全。
    通过以上可知,由于每一次的扩容都会进行一次拷贝操作,如果频繁进行扩容操作,将会严重影响性能,因此,非常有必要在集合初始化时声明容量。
建议60    多种最值算法,适时选择
     最值计算时,使用集合最简单,使用数组性能最优。
建议61    避开基本数据类型数组转换列表陷阱
     原始类型的数组不能作为Arrays.asList()方法的输入参数,否则会引起程序逻辑混乱。
建议62    Arrays.asList()方法产生的List对象不可更改
     注意Arrays.asList()方法返回的ArrayList对象是Arrays工具类的一个内置类,而不是java.util.ArrayList类,它没有提供诸如add、remove等方法,因此,无法对列表进行添加或删除操作。例如应避免使用如下代码初始化一个List对象:
     List<String> names = Arrays.asList("张三", "李四", "王五");  
     以上代码看似简捷,却深藏着重大的隐患---列表长度无法修改。因此,除非非常自信该List只用于读操作,否则,不要使用该方式初始化。
建议63    不同的列表选择不同的遍历方法
  • 遍历随机存取列表时,应采用下标的形式遍历指定元素,如对ArrayList等的遍历;
  • 遍历有序存取列表时,应采用循序遍历的形式遍历指定元素,如对LinkedList等的遍历。
   对于列表的遍历方式,可先判断是否实现了RandomAccess接口,然后进行遍历方式的选择。代码如下:
         if(list instanceof RandomAccess){  // 若实现RandomAccess接口,则随机访问,使用下标访问
               for(int i = 0; i < list.size(); i++){
                    System.out.println(list.get(i));
               }
         }else{  // 有序存取,用foreach方式 
               for(int i : list){
                      System.out.println(i);
               }
          }
     因此,列表遍历是很有学问的,适时选择最后的遍历方式,不要固话一种。
建议64     频繁插入或删除操作时使用LinkedList   
      (1) 插入元素
  • ArrayList插入一个元素,其后的元素就会向后移动一位,频繁的插入时,每次后面的元素都要拷贝一次,效率就会降低,特别是在头位置插入元素;
  • LinkedList插入元素时,整个过程只修改引用地址,没有任何拷贝过程,效率自然就高了;
  • 经过实际测试得知,LinkedList的插入效率比ArrayList快50倍以上。
      (2) 删除元素
  • ArrayList删除元素时,气候的元素就会向前移动一位,频繁删除,与插入操作类似,效率会降低;
  • LinkedList删除元素时,只有引用地址的变更,效率高;
  • 实际测试得知,LinkedList的删除效率比ArrayList快40倍以上。
      (3) 修改元素
  • ArrayList随机存取,效率肯定高,而LinkedList需要顺序读取,效率没有ArrayList高。
      通过以上的分析,在“写”方面,LinkedList占优势,在实际项目中修改操作也较少,因此,存在大量写操作时,推荐使用LinkedList。
建议65     列表相等只需关心元素数据
  • 只要列表都是实现了List接口,Java不关心它的具体实现类,只要所有的元素都相等,并且长度也相等就表明两个List是相等的,与具体的容量类型无关。
  • Java这样处理的是在为开发者考虑,列表只是一个容器,只要是同一种类型的容器(如List),不关心容器的细节差别(如ArrayList与LinkedList),只要确定了所有元素都相等,包括顺序也必须相同,那么这两个列表就相等。
     因此,判断集合是否相等时,只需关注元素是否相等即可。     
建议66     子列表只是原列表的一个视图
       通过阅读JDK源码可知,通过subList方法返回的SubList类也是AbstractList的子类,其所以的方法如get、set、add、remove等都是在原始列表上操作的,它自身并没有生成数组或链表,即子列表只是元列表的一个视图,所有的修改动作都反应在了原始列表上。实例代码如下:
      List<String> c = new ArrayList<String>();
      c.add("A");  
      c.add("B");
      // 构造一个包含c列表的字符串列表
      List<String> c1 = new ArrayList<String>(c);
      // 通过subList方法生成与c相同的新列表
      List<String> c2 = c.subList(0, c.size());
      // c2增加一个元素
      c2.add("C");
      System.out.println(c.equals(c1)); // 返回false,因为c1与c是两个完全不同的对象,且c2添加了“C”,使c包含3个元素“ABC”,而c1只有“AB”两个元素
      System.out.println(c.equals(c2)); // 返回true,因为子列表只是原始列表的一个视图,即c与c2是指向同一个内存空间的。
 因此,subList方法产生的列表只是一个视图,所有的修改动作直接作用于元列表。
建议67     推荐使用subList处理局部列表
  • 由上一个建议可知,subList方法取出子列表后的操作都是在原始列表上进行的,因此,操作子列表中的元素,最终会反映到原始列表上。
       实例代码如下(删除指定范围内的元素):
       // 初始化一个固定长度,不可变列表
       List<Integer> initData = Collections.nCopies(100, 0);
       // 转换为可变列表
      ArrayList<Integer> list = new ArrayList<Integer>(initData);
      // 删除指定范围内的元素
      list.subList(20, 30).clear(); // 删除下标在20-30范围内的元素
建议68     生成子列表后不要再操作原列表
        subList生成子列表后,保持原列表的只读状态。
建议69     使用Comparator进行排序
       在Java中,给数据排序有两种方式:一种是实现Comparable接口,另一种是实现Comparator接口。两者的区别如下:
  • 实现Comparable接口的类必须实现compareTo()方法,利用apache工具类操作,代码如下:
           @Override
           public int compareTo(User user){
               return new CompareToBuilder().append(id, user.id).toComparison(); // 利用apache的工具类实现compareTo()方法
          }
  • 实现Comparator接口的类也必须实现compareTo()方法,代码如下:
           @Override
           public int compareTo(User user){
               return new CompareToBuilder()
               .append(id, user.id) // 先按ID排序
               .append(name, user.name) // 再按名称排序,即ID相同的按名称排序
               .toComparison(); // 利用apache的工具类实现compareTo()方法
           }
  • 实现了Comparable接口的类表明自身是可以比较的,有了比较才能进行排序;
  • Comparator接口是一个工具类接口,它与原有类的逻辑没有关系,只是实现了两个类的比较逻辑,从这点来说,一个类可以有很多比较器,即可以产生N多种排序,而Comparable接口只是实现类的默认排序算法,即一个类只能有一个固定的、由compareTo()方法提供的默认排序算法。
      综上可知,Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具。因此,建议使用Comparator进行排序。
建议70     不推荐使用BinarySearch方法对列表进行检索
      使用indexOf()与binarySearch()方法对列表进行检索时,从使用上来说,indexOf()方法要比binarySearch()方法简单多,但从性能上考虑,binarySearch()方法是最好的选择,但必须注意到二分查找必须先排序。  
建议71     集合中的元素必须保证做到compareTo和equals同步
  • indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找;
  • equals是判断元素是否相等,compareTo判断元素在排序中的位置是否相同。
      因此,既然一个是决定位置排序,一个是决定相等,那我们就应该保证当排序位置相同时,其equals也相同,否则就会产生逻辑混乱,即在实际开发中,实现了compareTo方法,也应该覆写equals方法,保持两者同步。
建议72     集合运算时使用更优雅的方式 
      在实际开发中,应尽量使用JDK提供的方法来实现集合操作,以便代码更简洁优雅。特别是在持久层使用的非常频繁,如对从数据库中取出集合后的操作等。
  • 并集(可能有重复元素):  list1.addAll(list2); // 并集
  • 交集: list1.retainAll(list2);  // 交集
  • 差集: list1.removeAll(list2); // 差集,即从list1中删除出现在list2中的元素
  • 无重复并集: list2.remove(list1);  list1.addAll(list2);  // 先删除list2中出现在list1中的元素,然后将剩余的list2元素添加到list1中,注意不能使用HashSet剔除重复元素。
 建议73     使用shuffle打乱列表
      shuffle方法的使用场景:
  • 可以用在程序的为装上:标签云、游戏中的打怪、修行时的分配策略;
  • 可以用在抽奖程序中:先用shuffle打乱,然后再取出第一、第二名;
  • 可以用在安全传输方面:发送端发送一组数据,先随机打乱,然后加密发送,接收端解密,然后自行排序,即可实现相同的数据源产生不同的密文的效果,加强了数据的安全性。
 建议74     减少HashMap中元素的数量
      HashMap比ArrayList多了一个层Entry的底层对象封装,多占用了内存,且它的扩容策略是2倍长度的递增,同时还会依据阀值判断规则进行判断,因此,相对于ArrayList来说,它就会先出现内存溢出。
      因此,尽量让HashMap中元素少量且简单。
 建议75     集合中的哈希码不要重复
      HashMap的存储主线还是数组,遇到哈希冲突的时候则使用链表解决:使用hashCode定位元素,若有哈希冲突,则遍历对比,即在没有哈希冲突的情况下,哈希的查找是依赖hashCode定位的,由于是直接定位,效率就相当高了;若哈希码相同,则与ArrayList查找相率相当。
     因此,HashMap中的hashCode应避免冲突。
建议76     多线程使用Vector或HashTable
     Vector是ArrayList的多线程版本,HashTable是HashMap多线程版本。注意线程安全与同步修改的区别:
  • Java中基本上所有的集合类都有一个叫快速失败的校验机制,当一个集合同时被几个线程修改并访问时,就可能出现ConcurrentModificationException异常,它通过比较modCount修改计数器来判断:如果在读列表时,modCount发生变化就会抛出ConcurrentModificationException异常。这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的。
  • 从性能上考虑,除非有必要,尽量不要使用synchronized。
      以火车站售票为例,代码如下:
       final List<String> tickets = new Vector<String>(10000);
       forint i = 0; i < 10000; i++){
                 tickets.add( "火车票" +i);
        }
         // 10个窗口售票
         forint i = 0; i < 10; i++){
                 new Thread(){
                          @Override
                          public void run(){
                                 whiletrue){
                                       System. out.println(Thread. currentThread().getId()+"--"+tickets.remove(0));
                                 }
                           }
                   }.start();
           }
         理清概念:真正的多线程并不是并发修改的问题,如一个线程增加,一个线程删除,这不属于多线程的范畴。
        因此,多线程环境下应考虑使用Vector或HashTable。
建议77     非稳定排序推荐使用List
      Set与List的最大区别就是Set中的元素不可重复,在Set的实现类中,比较常用的实现类是TreeSet,该类实现了类默认排序为升序的Set集合(根据实现的Comparable接口的compareTo()方法的返回值确定排序位置),但这种排序不适用于元素经常变化的环境中,即它只定义了在插入元素时进行排序,并不保证修改后元素的排序结果,因此,TreeSet使用于不变量的集合数据排序,如String、Integer等类型,并不适用于可变量的排序。
      问题解决方法:
  • Set集合重新排序:重新生成一个Set对象,即对原有的Set对象重新排序,代码如下:
           set = new TreeSet<User>(new ArrayList<User>(set));  
  • 彻底重构TreeSet,使用List解决,即用List代替,再使用Collections.sort()方法对List进行排序。
 综上分析可知,对于不变量的排序,如String、Integer等,推荐使用TreeSet;对于可变量,如我们自己写的类,建议使用List进行排序。
建议78     推荐使用"拿来主义",使用工具类与扩展包操作数组与集合
  • 数组的工具类是java.util.Arrays和java.lang.reflect.Array,集合的工具类是java.util.Collections,有了这两工具类,操作数组和集合易如方掌,得心应手。
  • 扩展类可以使用Apache的commons-collections扩展包,也可使用Google的google-collections扩展包,这些足以应对我们的开发需求。
    总之,commons-collections、google-collections是JDK之外的优秀数据集合工具包,建议合理利用。
建议79     推荐使用枚举定义常量
       枚举是在Java1.5版本后出现的,它与类常量和静态常量相比的有点主要表现在以下4各方面:
  • 枚举常量更简单: 只需定义枚举项名称,无需定义其值,而接口常量或类常量必须定义其值;
  • 枚举常量属于稳态型:枚举常量在使用时无需关注其值而直接调用,接口常量或类常量可能需要关注其值;
  • 枚举具有内置方法:每个枚举都是java.lang.Enum的子类,该基类提供了诸如获得排序值得ordinal方法、compareTo比较方法等,大大简化常量的访问;
  • 枚举可以自定义方法:枚举常量不仅可以定义静态方法,还可以定义非静态方法,而且还能从根本上杜绝常量类被实例化。
      注意:每个枚举项都是该枚举的一个实例,那我们在枚举中定义的静态方法既可以在类中引用,也可以在实例中引用。枚举类不能有继承,无法做扩展,而接口常量和类常量可以通过继承进行扩展。
      因此,在项目开发中,推荐使用枚举常量代替接口常量或类常量。
建议80     使用构造函数协助描述枚举项
     枚举有两个属性:排序号(默认值是0、1、2...)和枚举描述(通过枚举的构造函数声明每个枚举项必须具有的属性和行为,这是对枚举项的描述和补充,目的是使枚举项表达的意义更加清晰准确)。示例代码如下:
     enum Role{
             ZhangSan ("张三" , 22, new Date()),
             LiSi"李四" , 25, new Date()); 
             private String name ;
             private int age ;
             private Date birthday ;
             // 枚举构造函数
            Role(String _name, int _age, Date _birthday){
                         name = _name;
                         age = _age;
                         birthday = _birthday;
            }
             /* name、age、birthday的getter与setter方法省略 */
    }
       由以上代码可知,对于开发者而言,对张三和李四都能有一个多维度的了解,特别是在大规模的项目开发中,大量的常量定义使用枚举类型描述比在接口常量或类常量中增加注释的方式友好的多,简介的多。
建议81     小心switch带来的枚举空值异常
      目前Java中的switch语句只能判断byte、short、char、int类型(JDK7允许使用String),这是Java编译器的限制,而处理枚举类型时是根据其排序值来执行的,示例代码如下:
      switch(Season.ordinal()){  // ordinal是枚举类型的内置方法,返回枚举项的排序值
          case Season.Spring.ordinal():....;break;
          case Season.Summer.ordinal();...;brea;
          ......
      } 
如果以上传入空值,则会抛出空值异常,这是因为null无法执行ordinal方法,因此,在switch语句中使用枚举时,建议判断是否为空值。
建议82     在switch的default代码块中增加AssertionError错误
      为了避免在switch的输入端加入了case语句没有包含的条件而导致的非预期错误,建议在default后直接抛出一个AssertionError异常,这样就可以在调用switch时增加了case中没有包含的条件下立即报错,便于查找错误。
建议83     使用枚举Enum的内置方法valueOf前必须校验是否存在指定枚举项
      Enum类中的valueOf方法有两个参数,但是我们在调用时只需指定一个枚举项的名称即可,这是因为valueOf(String name)方法是不可见的,是JVM的内置方法。Enum类中的valueOf方法的源码如下:
    public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
        T result = enumType.enumConstantDirectory().get(name); 
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException( "Name is null" );
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }
 从以上代码中可知,若指定的枚举项为空或者不存在指定的枚举项,都会抛出异常。因此,在使用内置方法valueOf前需进行校验,有以下两种方法:
  • 使用try...catch捕获,这个比较简单,不再赘述;
  • 扩展枚举类:可写一个扩展方法contains来先判断是否存在枚举项,然后再决定是否调用valueOf方法进行转换,示例代码如下:
       enum Season{
             Spring , Summer , WinterAutumn;
             // 自定义一个是否包含指定名称枚举项的方法,以实现扩展
             public static boolean contains(String name){
                         // 获取所有的枚举值
                        Season[] seasons = Season. values();
                        for(Season season : seasons){
                                     if(season.name().equals(name)){
                                                 return true ;
                                    }
                        }
                         return false ;
              }
        }
建议84     使用枚举实现工厂方法模式更简洁
       工厂方法模式:创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其子类。
       枚举实现工厂方法模式的方法有两种:
  • 枚举非静态方法实现工厂方法模式,代码如下:
         enum CarFactory{
             // 定义工厂类能够生产的汽车类型
             FordCar , BuickCar ;
             // 生产汽车
             public Car create(){
                         switch (this ){
                         case FordCar : return new FordCar ();
                         case BuickCar : return new BuickCar ();
                         default : throw new AssertionError("无效参数");
                        }
             }
         }
       // 客户端调用方式
        Car car = CarFactory .BuickCar .create();
  • 通过抽象方法生产产品:枚举类型虽不能继承,但是可以用abstract修饰其方法,表示该枚举额是一个抽象枚举,需要每个枚举自行实现该方法,即枚举项的类型是该枚举的一个子类,示例代码如下:
          enum CarFactory{
             FordCar {
                         @Override
                         public Car create() {
                                     return new FordCar();
                        }
            },
             BuickCar {
                         @Override
                         public Car create() {
                                     return new BuickCar();
                        }
            };
             public abstract Car create();
          }
     使用枚举实现工厂方法模式的优点:
  • 避免错误调用的发生:一般工厂方法模式的生产方法可接受三种类型的参数:类型参数、String参数、int参数,都是宽泛的数据类型,容易出错(如边界问题,null值问题),且编译器不会报错,而枚举不需要传递任何参数,只需选择生产什么产品即可;
  • 性能好,使用便捷:枚举的计算都是以int型计算为基础的,性能好;
  • 降低类间耦合:枚举类型只需依赖工厂类,完全可以无视具体产品类的存在。 
     因此,下一次,使用枚举来实现工厂方法模式。
建议85     枚举项的数量限制在64个以内
     Java提供了两个枚举集合:EnumSet和EnumMap,其中,EnumSet表示其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项。
     通过阅读EnumSet的JDK源码可知,它提供的两个实现都是基本数字类型的操作,其性能肯定比其他的Set类型要好的多,特别是枚举项少于64(为什么,看JDK源码)时,效率相当高。
     因此,枚举项尽量保持在64个以内,否则,建议拆分。
建议86     慎用注解继承
      Java从1.5版开始引入注解(Annotation),其目的是在不影响代码语义的情况下增强代码的可读性,并且不改变代码的执行逻辑。
      采用@Inherited元注解有利有弊,利在一个注解只要标注在父类,所有的子类都会自动具有与父类相同的注解,整齐、统一而便于管理;弊在单单阅读子类代码,我们无从知道为何逻辑会改变,因为子类没有明显标注该注释。
      总体上来说,使用@Inherited利大于弊,应谨慎使用。
建议87     枚举和注解结合使用威力更强大
      使用枚举与注解结合的方式实现诸如权限控制等业务逻辑会使代码更简洁、易读。(注意对注解的理解与运用)
建议88     注意@Override不同版本的区别
      @Override注解用于方法的覆写上,在编译期有效,使用该注解的好处是可以很大程度上解决我们的误写问题。
      注意:@Override注解在版本上的区别,对于实现的接口,Java 1.6及以上版本编译时不会有问题,但是1.5及以下版本就会出现编译错误。
      因此,在多环境部署应用时,需考虑@Override在不同版本下代表的意义。若从Java 1.6移植到 Java  1.5环境中时,需剔除实现接口方法上的@Override注解。
建议89    Java的泛型是类型擦除的
      Java泛型的引入加强了参数类型的安全性,减少了类型的转换,它在编译期有效,在运行期被删除,即所有的泛型参数类型在编译后都会被删除,会将所有的泛型类型做相应的转换,转换规则如下:
  • List<String>、List<Integer>、List<T>擦除后的类型为List;
  • List<String>[]擦除后的类型为List[];
  • List<? extends E>、List<? super E>擦除后的类型为List<E>;
  • List<? extends Serializable & Cloneable>擦除后为List<Serializable>.
示例代码如下:
        List<String> list = new ArrayList<String>();
        list.add( "abc" );
        String str = list.get(0);
经过编译类型擦除后的等效代码如下:
        List list = new ArrayList();
        list.add( "abc" );
        String str = (String)list.get(0);
在编译器擦除的原因:避免JVM的重构(因为Java是在编译期擦除的,若JVM把泛型延续到运行期,则需重构JVM);版本兼容。
利用Java泛型是类型擦除的原理,可解释如下问题:
  • 泛型的class对象是相同的;
  • 泛型数组初始化时不能声明泛型类型,如List<String>[] listArray = new List<String>[]; 编译不通过。
  • instanceof不能存在泛型类型。
建议90    不能初始化泛型参数和数组
       类的成员变量是在类初始化前初始化的,所以要求在初始化前必须具有明确的类型,否则就只能声明,不能初始化。处理泛型参数及数组的初始化可使用如下示例代码:
       class Initial<T>{
             private T t ;
             private T[] tArray ;
             // 由构造函数初始化
             public Initial(){
                        Class<?> type = Class.forName ("类名" ); // 获取T的具体类型
                         t = (T)type.newInstance(); // 实例化指定类型
                         tArray = (T[])Array.newInstance (type, 5);
            }
     }
建议91    强制声明泛型的实际类型
      无法从代码中推断出泛型类型的情况下,即可强制声明泛型类型。实例代码如下:
      class ArraysUtil{
             // 把一个变长参数转变为列表,且列表长度可变
             public static <T> List<T> asList(T...t){
                        List<T> list = new ArrayList<T>();
                        Collections. addAll(list, t);
                         return list;
            }
      } 
     在调用时强制声明泛型类型:List<Integer> list = ArraysUtil.<Integer>asList(1,2,3);
建议92    不同的场景使用不同的泛型通配符
      Java 1.5以后支持通配符,?表示任意类,extends表示某一个类(接口)的子类型,super表示某一个类(接口)的父类型。使用原则:
  • 泛型结构只参与"读"操作则限定上界(extends关键字)
            public static <E> void read(List<? extends E> list){
                    for(E e : list){   // 业务逻辑  }
            }
  • 泛型结构只参与"写"操作则限定下界(super关键字)
            public static void write(List<? super Number> list){
               list.add(12.3);
               list.add(123);
           }
      对于是要限定上界还是限定下界,JDK的Collections.copy()方法是一个非常典型的例子,核心源码如下:
      public static <T> void copy(List<? super T> dest, List<? extends T> src){
          for(int i = 0; i < src.size(); i++){
               dest.set(i, src.get(i));
          }
      }
      若一个泛型结构既用作"读"又用作"写"操作,则不作限定,使用确定的泛型类型即可,如List<E>.
建议93    警惕泛型是不能协变和逆变的  
      协变是用一个窄类型替换宽类型,逆变则是用宽类型覆盖窄类型。泛型既不支持协变,也不支持逆变。
  • 泛型不支持协变,如下代码编译不通过:List<Number> list = new ArrayList<Integer>(),但可使用通配符模拟协变,如:List<? extends Number> list = new ArrayList<Integer>();
  • 泛型不支持逆变,但可使用super关键字模拟实现,如:List<? super Integer> list = new ArrayList<Number>();
      因此,Java泛型是不支持协变和逆变的,只是能够实现协变和逆变。
建议94    建议采用的顺序是List<T>、List<?>、List<Object>
       List<T>、List<?>、List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是: List<T>、List<?>、List<Object>.
  • List<T>是确定的某一个类型;
  • List<T>可以进行读写操作: List<?>是只读类型,不能增加、修改操作,但可进行删除操作;List<Object>可以进行读写操作,但在写入时需想上转型,在读取时需向下转,使泛型存在的意义丧失。
建议95    严格限定泛型类型采用多重界限
      在Java的泛型中,可以是用"&"符号关联多个上界并实现多个边界限定,而且只有上界有此限定,下界没有多重限定的情况。
      示例代码如下:
     // 工资低于2500元的上班族并且站立的乘客车票打8折
     public static <T extends Staff & Passenger> void discount(T t){
          if(t.getSalary() < 2500 && t..isStanding()){
               System.out.println("打八折!");
          }
     }
     public static void main(String[] args){
          discount(new Me());
     }
     其中,Staff接口中定义了getSalary()方法,Passenger接口定义了isStangding()方法,类Me实现了这两个接口。
     说明:使用"&"限定多重边界,指定泛型类型T必须是staff和Passenger的共有子类型,此时,变量t就具有了所有限定的方法和属性。      
建议96    数组的真实类型必须是泛型类型的子类型
      当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型类型的父类型,只能是泛型类型的子类型(包括自身类型),否则就会抛出类型转换错误。
建议97    注意Class类的特殊性
      Java使用一个元类来描述加载到内存中的类数据,即Class类,它是描述一个类的类对象。Class类是"类中类",它的特殊点表现如下:
  • 无构造函数:Class对象是在加载类时由JVM通过调用类加载器中的defineClass方法自动构造的;
  • 可以描述基本类型:如在JVM中使用int.class来表示int类型的类对象;
  • 其对象都是单例模式:一个Class实例对象描述且只描述一个类,反过来也成立,一个类只有一个Class实例对象。如下代码都返回true:
         String.class.equals(new String().getClass());
         "ABC".class.equals(String.class);
         ArrayList.class.equals(new ArrayList<String>().getClass());

     Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态的加载、调用,获得一个Class对象的三种途径:
  • 类属性方式,如String.class;
  • 对象的getClass方法,如new String().getClass();
  • forName加载,如Class.forName("java.lang.String");
     获得了Class对象后,就可通过调用getAnnotations()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等。
建议98    适时选择getDeclaredXXX和getXXX方法
       Java的Class类提供了很多的getDeclaredXXX和getXXX方法,且成对出现,它们的区别如下:
  • getXXX方法获得的是所有public访问级别的方法,包括从父类继承的方法;
  • getDeclaredXXX方法获得的是自身类的所有方法,包括用public、private、protected修饰的方法,而不受限于访问权限。
      Java之所以这样处理是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑产生翻天覆地的变动,所以public的属性和方法最容易获取,私有方法和属性也可以获取,但限于在本类中。
建议99    反射访问属性或方法时将Accessible设置为true
      Java通过反射执行一个方法的过程如下:获取一个方法对象,然后根据isAccessible返回值确定是否能够执行,如果返回为false则需要调用setAccessible(true),然后再调用invoke()方法。示例代码如下:
      Method method = ...;
      // 检查是否可以访问
      if(!method.isAccessible()){
           method.setAccessible(true);    
      }
      // 执行方法
      method.invoke(obj, args);
通过反射方式执行方法时,必须在invoke之前检查Accessible属性,这是一个好习惯,但方法对象的Accessible属性并不是用来决定是否可访问的。
建议100   使用forName动态加载类文件
  • 动态加载是指程序运行时动态加载需要的类文件,对于Java程序来说,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时决定是否要加载一个类。此动态加载通常通过Class.forName(String)方法实现。
  •  一个对象的生成经历的过程:加载到内存中生成Class实例对象;通过new关键字生成实例对象。
  • 意义:加载一个类即表示要初始化该类的static变量,特别是static代码块,如注册自己、初始化环境等。
  • forName只是加载类,它只负责将类加载到内存中,并不保证生成加载类的实例对象,也不执行任何方法。
建议101   动态加载不适合数组
        bubuko.com,布布扣bubuko.com,布布扣
      Java中不能动态加载数组,如Class.forName("java.lang.String[]")会抛出异常,但可通过上表的方式加载,如Class.forName("Ljava.lang.String"),这又毫无意义,因为forName方法只负责将类加载到内存中,并不负责它的实例化。
要实现数组的动态加载,可使用Array数组反射类来实现,实例代码如下:
       String[] strs = (String[])Array.newInstance(String.class, 8);
       int[] ints = (int[])Array.newInstance(int.class, 8);
 因此,通过反射操作数组使用Array类,不要采用通用的反射处理API。
建议102   动态代理可以使代理模式更加灵活
      Java的反射框架提供了动态代理机制,允许在运行期对目标类生成代理,避免重复开发。代理方式分为两种:
  • 静态代理:通过代理主题角色(Proxy)和具体主题角色(Real Subject)共同实现抽象主题角色(Subject)的逻辑,只是代理主题角色把相关的执行逻辑委托给了具体的主题角色。
  • 动态代理:Java提供java.lang.reflect.Proxy实现动态代理,只要提供了一个抽象主题角色和具体主题角色,就可动态实现其逻辑,示例代码如下:    
interface Subject{ public void request(); }
class RealSubject implements Subject{            
public void request() {
        System.out.println( "执行真实对象的方法" );
}
}
class SubjectHandler implements InvocationHandler{            
private Subject subject ;            
public SubjectHandler(Subject _subject){ subject = _subject; }          
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println( "预处理" );                       
// 直接调用被代理类的 方法
Object obj = method.invoke(subject, args);
System.out.println( "后处理" );                       
return obj;
 }          
}
public static void main(String[] args) throws Exception {                                              
      // 具体主题角色
      Subject subject = new RealSubject();                      
      // 代理实例的处理handler
      InvocationHandler handler = new SubjectHandler(subject);                    
      // 当前加载器
      ClassLoader cl = subject.getClass().getClassLoader();                     
      // 动态代理
      Subject proxy = (Subject) Proxy.newProxyInstance(cl, subject.getClass().getInterfaces(), handler);           
      // 执行具体主题角色方法
      proxy.request();
 }   
       动态代理很容易实现通用的代理类,只要在InvocationHandler的invoke方法中读取持久化数据即可实现,而且还能实现动态切入的效果,即AOP编程理念。
建议103   使用反射增加装饰模式的普适性
       装饰模式:动态地给一个对象增加一些额外的功能,就增加功能来说,装饰模式相对于生成子类更为灵活。Java中的动态代理也可实现装饰模式的效果,而且其灵活性、适应性都会更强。(示例代码参看原著建议107)
       装饰行为由动态代理实现,实现了装饰类和被装饰类的完全解耦,提供了系统的扩展性。
建议104   反射让模板方法模式更强大      
      模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。
建议105   不要太多关注反射效率
     反射的效率相对于正常的代码执行确实低很多,但是它是一个非常有效的运行期工具类。
建议106    提倡异常封装
      Java语言的异常处理机制可以提高程序的健壮性,提高系统的可用率,但Java API提供的异常是比较底层的,为了使终端用户能够看懂,提倡对异常进行封装,封装的优点有以下几个方面:
  • 提高性能的友好性;
  • 提高性能的可维护性:尽量不要用一个catch块来处理异常,建议使用分类异常处理,方便程序的后期维护,实例代码如下:
          public void doStuff(){
               try{ 
                    // 业务代码
               }catch(FileNotFoundException e){ 
                    log.info("文件未找到!");
               }catch(SecurityException e){
                    log.error("无权访问.");
                    e.printStackTrace();
               }
          }
  • 解决Java异常机制自身的缺陷:Java中的异常一次只能抛出一个,但可通过如下方法实现一次抛出多个异常:
     class MyException extends Exception{
             // 容纳所有的异常
             private List<Throwable> causes = new ArrayList<Throwable>();
             // 构造函数,传递一个异常列表
             public MyException(List<? extends Throwable> _causes){
                         causes.addAll(_causes);
            }
             // 读取所有异常
             public List<Throwable> getExceptions(){
                         return this .causes ;
            }
      }
  MyException异常只是一个异常容器,可以容纳多个异常,但本身并不代表任何异常,它解决的是一次抛出多个异常,具体调用如下:
           public static void doStuff() throws MyException{
                        List<Throwable> causes = new ArrayList<Throwable>();
                         // 第一段逻辑代码
                         try{  
                        } catch(Exception e){
                                    causes.add(e);
                        }
                         // 第二段逻辑代码
                         try{
                        } catch(Exception e){
                                    causes.add(e);
                        }
                         // 检查是否有必要抛出异常
                         if(causes.size() > 0){
                                     throw new MyException(causes);
                        }
            }
建议107    采用异常链传递异常
      异常传递基本做法是(以在MVC模式中,文件找不到时发生的异常为例):
  • 把FileNotFoundException封装为MyException;
  • 抛出到逻辑层,逻辑层根据异常代码(或自定义的异常类型)确定后续处理逻辑,然后抛出到展现层;
  • 展现层自行决定要展示什么,如果是管理员则可展现底层的异常,若果为普通用户,则展示封装后的异常。
      因此,异常需要封装和传递,在开发过程中,不要吞噬异常,也不要赤裸裸的抛出异常,封装后再抛出,或者通过异常处理链传递,可以达到系统更健壮、友好的目的。
建议108    受检异常尽可能地转换为非受检异常
       非受检异常: 在编译期间无需对异常进行处理的异常为非受检异常。其中RuntimeException和它的子类以及Error和它的子类都是非受检异常。因此,对于一个方法抛出RuntimeException和它的子类或者Error和它的子类。调用它无需进行异常处理,编译器能通过。
       受检异常:在编译期间要对其可能出现的异常进行处理(使用try(){...} catch(...){....})的异常为受检异常。
受检异常的不足:
  • 受检异常使接口声明脆弱;
  • 受检异常使代码的可读性降低;
  • 受检异常增加了开发的工作量。
因此,当受检异常威胁到了系统的安全性、稳定性、可靠性、正确性时,则必须处理,不能转化为非受检异常,其他情况则可转化为非受检异常。
建议109    不要在finally块中处理返回值
    在finally块中加入return语句会导致以下两个问题:
  • 覆盖try代码块中的return返回值;
  • 屏蔽异常:若在finally中加入return返回值,就是告诉JVM,方法正确执行,即使有异常抛出,也不会被调用者捕获。
    因此,不要在finally代码块中出现return语句。   
建议110    不要在构造函数中抛出异常
  • 构造函数抛出错误是程序员无法处理的;
  • 构造函数不应该抛出非受检异常:加重了上层代码编写者的负担;后续代码不会执行;
  • 构造函数尽量不要抛出受检异常:导致子类代码膨胀;违背里氏代换原则;子类构造函数扩展受限。
  总之,对于构造函数,错误只能抛出,这是程序员无能为力的事情;非受检异常不要跑出,抛出了对人对己都是有害的;受检异常尽量不要抛出,能用曲线的方式实现就用曲线的方式实现。一句话,尽量不要在构造函数中抛出异常。
建议111    使用Throwable获得栈信息
      JVM在创建一个Throwable类及其子类时会把当前线程的栈信息记录下来,以便在输出时准确定位异常原因,其API源码如下:
      private StackTraceElement[] stackTrace;
     public Throwable(String message) {
         fillInStackTrace();
         detailMessage = message;
      }
      private native Throwable fillInStackTrace( int dummy);
      在出现异常时(或主动声明一个Throwable对象时),JVM会通过fillInStackTrace()方法记录下栈帧信息,然后生成一个Throwable对象,这样我们就可以知道类间的调用顺序、方法名称及当前行号等了。
建议112    异常只为异常服务
       异常只能用在非正常的情况下,不能成为正常逻辑的主逻辑,即异常只是主场景中的辅助场景,不能喧宾夺主。并且,异常虽然是描述例外类型的,但能避免则避免之,除非是确实无法避免的异常。
建议113    多使用异常,把性能问题放一边
      使用异常的优点:
  • 使业务更符合实际的处理逻辑,同时,使主逻辑更加清晰;
  • 让正常代码块和异常代码块分离,能迅速查找问题,但缺点是,性能比较慢。
建议114    多线程中不推荐覆写start方法
       Thread类的start方法的JDK源码如下:
 public synchronized void start() {
        if ( threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add( this);
        boolean started = false ;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed( this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
   private native void start0();

       其中,start0方法实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM负责的,如果覆盖了start方法,也就是撤销了线程管理和内存管理的能力,导致线程无法启动。
       因此,继承自Thread类的多线程类不必覆写start方法。  
建议115    启动线程前stop方法是不可靠的

       在使用线程的start方法中,通过其JDK源码可知,start0在stop0前面被调用,即即使stopBeforeStart为true(不可启动),也会先启动一个线程,然后再stop0结束这个线程。
       因此,建议不再使用stop方法进行状态的设置,直接通过判断条件来决定线程是否可启用。对于start方法的该缺陷,一般不会引起太大的问题,只是增加了线程启动和停止的精度而已。
建议116    不使用stop方法停止线程
       Java提供的用于停止线程的stop方法的三个问题:
  • stop方法是过时的:从Java编码规则来说,已过时的方法不建议采用;
  • stop方法会导致代码逻辑不完整:stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,且此操作非常隐蔽,子线程执行到何处被关闭很难定位,导致维护成本增加;
  • stop方法会破坏原子逻辑:多线程利用锁的概念解决了资源抢占的问题,避免资源不同步,而stop方法会丢弃所有的锁,导致原子逻辑受损。
     解决方法:使用自定义的标志位决定线程的执行情况,示例代码如下:
     class SafeStopThread extends Thread{
             // 此变量必须加上volatile关键字,表示该变量线程同步
             private volatile boolean stop = false;
             @Override
             public void run(){
                         whilestop){
                                     // do Something...
                        }
            }
             // 终止线程
             public void terminate(){
                         stop = true;
            }
      }
    注意:interrupt方法不能终止一个方法,它只会改变中断标志位。另外,使用线程池的shutdown方法可逐步关闭池中的线程,不会产生类似stop方法的弊端。
建议117    线程优先级只使用三个等级
      线程优先级决定了线程获得CPU的运行机会,优先级越高获得的运行机会就越大,反之,就越小。Java中提供了11个级别(其中0的线程是JVM的,应用程序不能设置该级别),但这些优先级别只代表了抢占CPU的机会大小,因此,时常出现以下情况:
  • 并不是严格遵照线程优先级别来执行的;
  • 优先级别差别越大,运行机会差别越明显。
Java是跨平台的系统,因此在Thread类中设置了三个级别的优先级,JDK源码如下:
    public final static int MIN_PRIORITY = 1;
     public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;
上述三个优先级常量在大多数情况下MAX_PRIORITY的线程比NORM_PRIORITY的线程先运行,但不能保证必先运行,只是执行的机会更大;若优先级相同,操作系统系统决定,先进先出(FIFO),但不能完全保证。
因此,线程优先级推荐使用MIN_PRIORITY,NORM_PRIORITY,MAX_PRIORITY三个级别,不建议使用其他7个数字。
建议118    使用线程异常处理器提升系统的可靠性     
      Java 1.5版本以后在Thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕获和处理。在实际运用时,需注意以下几点:共享资源锁定;脏数据引起系统逻辑混乱;内存溢出。
建议119    volatile关键字不能保证数据同步
     volatile关键字少用的的原因:
  • 在Java 1.5之前该关键在不同的操作系统有不同的表现,造成移植性差;
  • 比较难设计,而且误用较多。
    线程的计算一般是通过工作内存交互的,其示意图如下:
bubuko.com,布布扣
bubuko.com,布布扣
   Java在变量面前加上volatile关键字后的运行示意图如下:
bubuko.com,布布扣
bubuko.com,布布扣
 但要明确的是,volatile关键字并不能保证线程的安全性,它只能保证当线程需要该变量的值时能够获得最新的值,而不能保证多个线程修改的安全性。
建议120    异步运算考虑Callable接口
     实现Callable接口,只表明它是一个可调用的任务,并不表示它具有多线程的运算能力,还是需要执行器来执行的。
     异步计算的好处:
  • 尽可能的占用系统资源,提供快速运算;
  • 可以监控线程的执行情况,如是否执行完毕、是否有返回值、是否有异常等;
  • 可以为用户提供更好的支持,如运算进度等。
   示例代码如下:
// 税率计算器
class TaxCaculator implements Callable<Integer>{
             private int seedMoney ; // 本金
             // 接收主线程提供的数据
             public TaxCaculator(int _seedMoney){
                         seedMoney = _seedMoney;
            }
             public Integer call() throws Exception {
                         // 复杂计算,运行一次需要10秒
                        TimeUnit. MILLISECONDS .sleep(1000);
                         return seedMoney / 10;
            }
             public static void main(String[] args) throws Exception {                      
                         // 生成一个单线程的异步执行器
                        ExecutorService es = Executors. newSingleThreadExecutor();
                         // 线程执行后的期望值
                        Future<Integer> future = es.submit( new TaxCaculator(100));
                         while(!future.isDone()){
                                     // 还未运算完成,等待200毫秒
                                    TimeUnit. MICROSECONDS .sleep(20000);
                                     // 输出进度符号
                                    System. out.print( "#" );
                        }
                        System. out.println( "\n计算完成:税金是:" +future.get()+" 元");
                        es.shutdown();
            }
}
建议121    优先选择线程池
      线程的5个状态:新建状态、可运行状态、阻塞状态、等待状态、结束状态。每次创建线程都会经过创建、运行、销毁这三个过程,这势必会影响系统的性能,因此,建议使用线程池来提高系统的性能。示例代码如下:
         // 生成2个线程的线程池
         ExecutorService es = Executors. newFixedThreadPool(2);
         // 多次执行线程体
         forint i = 0; i < 4; i++){
                es.submit( new Runnable(){
                          public void run(){
                                      System. out.println(Thread. currentThread().getName()); // 输出结果为:pool-1-thread-1 pool-1-thread-2 pool-1-thread-1 pool-1-thread-2
                          }
                });
         }
        es.shutdown();
 使用线程池减少的是线程的创建与销毁的时间,如我们常用的Servlet容器,就是采用这种线程池技术实现的。
建议122    适时选择不同的线程池来实现
      Java线程池实现从根本上说只有两个:ThreadPoolExcutor和ScheduledThreadPoolExcutor类,这两个类为父子关系,为了简化,Java提供了Excutors的静态类,直接生成多种不同的线程执行器。
      线程池的管理执行过程:首先创建线程池,然后根据任务的数量逐步将线程增大到corePoolSize(最小线程数),若此时仍有任务增加,则放置到workQueue(任务队列)中,直到workQueue饱满为止,然后继续增加线程池中的线程数量,最终达到maximumPoolSize(最大线程数量)。那若此时还继续增加任务,就需要handler(拒绝任务处理器)来处理,或丢弃任务,或拒绝任务,或挤占已有的任务等。若线程池和任务队列都处于饱和的情况下,一旦有线程处于等待状态的时间超过keepAliveTime(线程最大生命期),则该线程终止。
建议123    适时选择不同的线程池来实现
     Lock类是显式锁,synchronized是内部锁,它们的有如下区别:
  • 为了保证锁的正确释放,显式锁的锁定与释放必须放在try...finally块中;
  • 显式锁是对对象级别的锁,内部锁是类级别的锁,即Lock所是跟随对象的,而synchronized锁是跟随类的;
  • Lock锁支持更细粒度的锁控制:假设读写锁分离,写操作时不允许有读写操作,读操作时读写操作可以并发执行,这一点内部锁很难实现;
  • Lock锁是无阻塞锁,synchronized锁是阻塞锁;
  • Lock锁可实现公平锁,synchronized只能实现非公平锁;
  • Lock锁是代码级别的,synchronized是JVM级的。
     两种不同的锁机制,更具不同的情况来选择:灵活、强大则选择Lock锁,快捷、安全则选择synchronized锁。
建议124    预防线程死锁
      Java是单进程多线程语言,一旦死锁产生,只能借助外部进程重启应用才能解决问题。
      产生死锁必备的四个条件:
  • 互斥条件:一个资源每次只能被一个线程使用;
  • 资源独占条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:线程获得的资源在使用完之前,不能强行剥夺;
  • 循环等待条件:若干线程之间形成首尾相接的循环等待资源关系。
     从死锁产生的条件入手避免死锁产生的方法:
  • 避免或减少资源共享;
  • 使用自旋锁:如为线程设置等待超时的时间,若等待超时,则自行终结该任务。
建议125    适当设置阻塞队列长度
      阻塞队列的长度是固定的,非阻塞队列的长度是变长的。阻塞队列可以在声明时指定队列的容量,若指定了容量,则元素的数量不能超过该容量,若不指定,则容量为Integer的最大值。
      阻塞队列是为了容纳(或排序)多线程任务而存在的,其服务的对象是多线程应用,而非阻塞队列容纳的则是普通的数据元素。 
      向队列中添加元素有两种方法:add方法与put方法,其中add方法在超出队列容量时不做插入,而直接丢弃;put方法会一直等到队列空出元素再插入,但这会造成资源的浪费。
建议126    使用CountDownLatch协助子线程
      CountDownLatch类是一个倒数的同步计数器,每个线程在运行结束后会执行countDown,表示自己运行结束,这对于多个子任务的计算特别有效,如一个异步任务需要拆分成10个子任务执行,主任务必须要知道各个子任务是否完成,所有子任务完成后才能进行合并运算,从而保证了一个主任务的逻辑正确性。示例代码如下:
 class Runner implements Callable<Integer>{
             // 开始信号
             private CountDownLatch begin ;
             // 结束信号
             private CountDownLatch end ;
             public Runner(CountDownLatch _begin, CountDownLatch _end){
                         begin = _begin;
                         end = _end;
            }
             public Integer call() throws Exception {
                         // 跑步的成绩
                         int score = new Random().nextInt(25);
                         // 等待发令枪响
                         begin.await();
                         // 跑步中
                        TimeUnit. MICROSECONDS .sleep(score);
                         // 跑步者跑完全程
                         end.countDown();
                         return score;
            }
     }
     public static void main(String[] args) throws Exception {                      
                         // 参赛人数
                         int num = 10;
                         // 发令枪只响一次
                        CountDownLatch begin = new CountDownLatch(1);
                         // 参与跑步者有多个
                        CountDownLatch end = new CountDownLatch(num);
                         // 每个跑步者一个跑道
                        ExecutorService es = Executors. newFixedThreadPool(num);
                         // 记录比赛成绩
                        List<Future<Integer>> futures = new ArrayList<Future<Integer>>();
                         // 跑步者就位,所有线程处于等待状态
                         forint i = 0; i < num; i++){
                                    futures.add(es.submit( new Runner(begin, end)));
                        }
                         // 发令枪响,跑步者开始跑步
                        begin.countDown();
                         // 等待所有跑步者跑完
                        end.await();
                         int count = 0;
                         // 统计总分
                         for(Future<Integer> f : futures){
                                    count += f.get();
                        }
                        System. out.println( "平均分数: " +(count/num));
     }
建议127    CyclicBarrier让线程齐步走
     Java提供了CyclicBarrier(关卡)工具类来实现多线程编程中,两个独立运行的线程在没有线程间通信的情况下而汇集到同一原点的问题。
     CyclicBarrier关卡可以让所有的线程全部处于等待状态,然后在满足条件的情况下继续执行,它可以用在系统的测试中。如测试一个核心算法的可靠性和效率,可让N个线程汇集到测试原点上,然后一声令下,所有的线程都引用该算法,即可观察出线程是否有缺陷。
建议128    提升Java性能的基本方法
       运行一段程序需要三种资源:CPU、内存、I/O,任何程序优化都是从这三个方面入手的,Java优化的基本方法有:
  • 不要在循环条件中计算,如while(i < count * 2);
  • 尽可能把变量、方法声明为final static类型,如阿拉伯数字转换为中文的实例代码如下:
            final static String[] cns = {"零" , "壹" , "贰" , "叁" , "肆" , "伍" , "陆" , "柒" , "捌" , "玖" , "拾" };
            public String toChineseNum(int num){
                         return cns [num];
            }
  • 缩小变量的作用范围,以加快GC的回收;
  • 频繁字符串操作使用StringBuffer或StringBuilder;
  • 使用非线性检索;
  • 覆写Exception的fillStackTrace()方法,若不需关注异常信息,可进行该方法进行覆写;
  • 不建立冗余对象,即遵循需要时才创建的原则。
建议129    若非必要,不要克隆对象
      Java中,80%的对象都是通过new关键字创建出来的,所以new在生成对象时做了充分的性能优化,一般情况下,new生成的对象比clone生成的性能要好很多,clone方法主要是用在构造函数比较复杂,对象属性比较多,通过new关键字创建一个对象比较耗时的时候。
     因此,克隆对象并不比直接生成对象的效率高。
建议130    推荐使用"望闻问切"的方式诊断性能

      性能优化是一个漫长的工作,特别对于偶发性的性能问题,需要深入思考,寻找根源,最终必然能够找到根源所在。
  • 望:不可(或很难)重现的偶发性问题、可重现的性能问题;
  • 闻:主要关注的是项目的技术能力、文化氛围、群体的习惯和习性、他们专注和擅长的领域;
  • 问:与技术人员和业务人员一起探讨该问题,了解性能问题的历史状况;
  • 切:给出问题的结论,问题出自何处,如何处理等。
建议131    必须定义性能衡量标准
      指定性能衡量标准是为了:1)性能衡量标准是技术与业务之间的契约;2)性能衡量标准是技术优化的目标。
      一个好的性能衡量标准应该具备:1)核心业务的响应时间;2)重要业务的响应时间。
建议132    枪打出头鸟---解决首要系统性能问题
      系统一旦出现性能问题,也就意味着一批的功能出现了性能问题,在这种情况下,需统计业务人员认为重要的而且缓慢的功能,然后按重要优先级和响应时间进行排序,并找出前几个重要的急待解决的问题。不要把所有的问题都摆在眼前,这只会扰乱自己的思维。
      因此,解决性能优化要"单线程"小步前进,避免关注点过多而导致精力分散。
建议133    调整JVM参数以提升性能
      每一段Java程序都是在JVM中运行的, 若程序已经优化到极致,则可考虑优化JVM,常用的JVM优化手段有:
  • 调整堆内存大小:栈内存由线程开辟,线程结束,栈内存由JVM回收,它的大小一般不会对性能有太大的影响,但它会影响系统的稳定性。在超过栈内存的容量时,系统会报StackOverFlowError错误,可通过"java -Xss <size>"设置栈内存大小解决此类问题;堆内存的调整不能太随意,太大浪费资源,太小,导致Full GC频繁执行,导致系统性能急速下降,可通过"java -Xmx1536 -Xms1024"来设置。
  • 调整内存中各分区的比例:JVM堆内存包括新生区、养老区、永久存储区三部分,其中生成对象都在新生区,它又分为伊甸区、幸存0区和幸存1区(自行了解这些分区的利用原理);
  • 变更GC的垃圾回收策略:Java程序性能的最大障碍就是垃圾回收,我们不知道何时发生,也不知道执行多久,但可通过设置其垃圾回收策略进行优化;
  • 更换JVM:目前比较流行的有Java HotSpot JVM、Oracle JRockit JVM、IBM JVM,其中HotSpot是我们常用的。
     另外,需要指出的是,带有"-Xx"的JVM参数可能是不健壮的,SUN也不推荐使用,虽然好用,但在系统升级、迁移时慎重考虑。
建议134    性能是个大“咕咚”
      Java不存在系统性能问题,可从以下几个方面分析:
  • 没有慢的系统,只有不满足业务的系统;
  • 没有慢的系统,只有架构不良的系统;
  • 没有慢的系统,只有懒惰的技术人员;
  • 没有慢的系统,只有不愿意投入的系统。
建议135    大胆采用开源工具
      在选择开源工具和框架时应遵循一定的原则:
  • 普适性原则:考虑项目成员的整体技术水平,不能有太大的跨度或跳跃性;
  • 唯一性原则:相同的工具只选择一个或一种,不要让多种相同或相似职能的工具共存;
  • "大树纳凉"原则:在选择工具包时,应选择比较有名的开源组织,如Apache、Spring、Google等,具有固定的开发和运作风格,具有广阔的使用人群;
  • 精而专原则:选择的工具包应该精而专,如Spring提供的Utils工具包只是Spring框架的一个附加功能而已,应使用精而专的Apache commons的Utils包;
  • 高热度原则:一个开源项目的热度越高,更新就越频繁,使用人群就越广,Bug曝光率就越快,修复效率也就越高,这有有助于提高项目的稳定性。
建议136    推荐使用Guava扩展工具包
      Guava是google-collections工具包的超集,它是完全兼容google-collections工具包,因此建议使用Guava包替代google-collections包。它提供的主要功能如下:
  • Collections:不可变集、多值Map、Table表和集合工具类,它提供的不可变集与多值Map是JDK没有的功能;
  • 字符串操作:Joiner连接器和Splitter拆分器,JDK方法也提供的有,但使用Guava更简单;
  • 基本工具类型:在primitives包中,是以基本类型名加s的方式命名的,如Ints是int的工具类。
建议137    Apache扩展包
  • Lang包:字符串操作工具类、Object工具类、可变的基本类型、其他的Utils工具;
  • BeanUtils:JavaBean的操作工具包,不仅可以实现属性的拷贝、转换等,还可建立动态的Bean(属性拷贝:分层开发中的PO和VO之间的转换);
  • Collections包:Bag(可容纳重复元素,提供重复元素的统计功能)、lazy系列(在集合重元素被访问时才生成)、双向Map(键、值都唯一,可根据键或值操作);
  • 其他的包还有DBCP、net、Math等,但这些包大部分更新缓慢,Collections中的大部分集合类不支持泛型。
建议138    推荐使用Joda日期时间扩展包

      Joda可以很好地与现有的日期类保持兼容,在需要复杂计算时,使用Joda,在需要与其他系统进行通信或写到持久层中时则使用JDK的Date。另外,日期工具类还可以选择date4j,也是一款不错的日期开源工具。

建议139    可以选择多种Collections扩展

      程序的经典定义:程序=数据结构+算法,而数据结构主要是用于表示对象的信息的,Collections包就是用于处理这些信息的基本操作。几个有特点的Collections包如下:
  • fastutil:提供限定值类型的Map、List、Set等以及大容量的集合;
  • Trove:提供了一个快速、高效、低内存消耗的Collections集合,同时,提供过滤、拦截功能和基本类型的集合,它在进行一般的增加、删除、修改操作时,Trove的响应时间比JDK集合少一个数量级,比fastutil也会高很多,因此在高性能项目中可以考虑Trove;
  • lambdaj:一个纯净的集合操作工具,不提供任何的集合扩展,只提供对集合的操作,如查询、过滤、统一初始化等,特别是查询操作类似于DBRMS上的SQL语句,也提供诸如求和、求平均值等方法。它符合开发人员的习惯,对集合的操作提供了"One Line"式的解决方法,可以大大缩减代码量,也不会导致代码的可读性降低。
建议140    提倡良好的代码风格
      优秀团队的编码风格应具备以下几个特征:
  • 整洁:不管代码风格的定义有多优秀,有多适合开发人员,如果代码结构混乱不堪,即使效率再高,也会使维护难以持续;
  • 统一:从一个团队中诞生的代码应该具有统一的风格,同时还具有连贯性,即在不同的层级、模块中使用相同的编码风格;
  • 流行:使用流行的代码风格可使新成员尽快融入项目,也是一种省时、省力、省心的最好编码风格;
  • 便捷:制定出来的编码规范必须有通用的开发工具支撑,不能制定出只能由个别开发工具支持的规范,甚至绑定在某一个IDE上。
      另外,推荐使用CheckStyle工具统计代码,它可自定义模板,然后根据模板检查代码是否遵循规范,从而减少枯燥的代码走查。
建议141    不要完全依靠单元测试来发现问题
      单元测试只是排查程序错误的一种方式,并能保证代码中的所有错误都能被单元测试挖掘出来,原因如下:
  • 单元测试不可能测试到所有的场景(路径);
  • 代码整合错误是不可避免的;
  • 部分代码无法(或很难)测试;
  • 单元测试验证的是编码人员的假设。
建议142    让注释正确、清晰、整洁
      注释只是代码阅读的辅助信息,如果代码的表达能力足够清晰,根本就不需要注释,注释能够帮我们更好地理解代码,但它重视是质量而不是数量。
      因此,注释不是美化剂,而是催化剂,或为优秀加分,或为拙劣减分。
建议143    让接口的职责保持单一
      一个类所对应的需求功能越多,引起变化的可能性就越大,单一职责原则就是要求接口尽可能的保持单一。
     单一职责有以下几个好处:
  • 类的复杂性降低;
  • 可读性和可维护性提高;
  • 降低变更风险。
     如何实施单一职责原则:1)分析职责;2)设计接口;3)合并实现。
     因此,接口职责一定要单一,实现类职责尽量单一。
建议144    增强类的可替换性
     遵循里氏替换原则可增强类的可替换性,即所有引用基类的地方必须能够透明地使用其子类的对象。
     为了增强类的可替换性,设计类时可考虑如下几点:
  • 子类型必须完全实现父类型的方法;
  • 前置条件可以被放大:方法中的输入参数称为前置条件,前置条件的放大,可保证父类型行为逻辑的继承性;
  • 后置条件可以被缩小:即方法的返回值,父类型方法返回T,子类同名方法返回S,S可以是T的子类。
    增强类的可替换性,则增强了程序的健壮性,版本升级时也可保持非常好的兼容性。
建议145    依赖抽象而不是依赖实现
      依赖倒转原则在Java语言中的表现为:
  • 模块间的依赖是通过抽象发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类实现的;
  • 接口或抽象类不依赖实现类;
  • 实现类依赖接口或抽象类。
      在项目中应遵循的规则:1)尽量抽象;2)表面类型必是抽象的;3)任何类都不应该从具体类诞生;4)尽量不要覆写基类的方法;5)抽象不关注细节。
建议146    摒弃7条不良的编码习惯
     成为优秀的编码人员必须进行自我剖析,抛弃不良的习惯,展示自己优秀的编码能力。以下是几个应该摒弃的编码习惯:
     1)自由格式的代码;2)不使用抽象的代码;3)彰显个性的代码;4)死代码;5)冗余代码;6)自以为是的代码。
建议147    以技术员自律而不是工人
      作为一名javaer,我们要逐步培养自己,在提高自己技能的同时也提高自己的思维方式。以下是20条建议:
      1)熟悉工具;2)使用IDE;3)坚持编码;4)编码前思考;5)坚持重构;6)多写文档;7)保持程序版本的简单性;8)做好备份;9)做单元测试;10)不要重复发明轮子;           
      11)不要拷贝;12)让代码充满灵性;13)测试自动化;14)做压力测试;15)"剽窃"不可耻;16)坚持向敏捷学习;17)重里更重面;18)分享;19)刨根问底;20)横向扩展。

                                     坚持学习,坚持进步!



编写高质量JAVA程序代码的建议,布布扣,bubuko.com

编写高质量JAVA程序代码的建议

标签:java

原文地址:http://blog.csdn.net/xdweleven/article/details/38383077

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!