zoukankan      html  css  js  c++  java
  • 改善java程序的151个建议

    《编写高质量代码-改善java程序的151个建议》

    --秦小波

    第一章、开发中通用的方法和准则

    1、不要在常量和变量中出现易混淆的字母

    long a=0l; --> long a=0L;
    

    2、莫让常量蜕变成变量

    static final int t=new Random().nextInt(); 
    

    3、三元操作符的类型无比一致

     int i=80;
     String s=String.valueOf(i<100?90:100);
     String s1=String.valueOf(i<100?90:100.0);
     System.out.print(s.equals(s1)); //false
    

     编译器会进行类型转换,将90转为90.0。有一定的原则,细节不表

    4、避免带有变长参数的方法重载

    public class MainTest {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            System.out.println(PriceTool.calPrice(12, 1)); // 1
        }
    }
    
    class PriceTool {
        public static int calPrice(int price, int discount) {
            return 1;
        }
        public static int calPrice(int price, int... discount) {
            return 2;
        }
    }
    

     编译器会从最简单的开始猜想,只要符合编译条件的即采用

    5、别让null值和空值威胁到变长方法 

    其中client.methodA("china")和client.methodA("china",null) 是编译不通过的,因为编译器不知道选择哪个方法

    6、覆写变长方法也循规蹈矩

    重写是正确的,因为父类的calprice编译成字节码后的形参是一个int类型的形参加上一个int数组类型的形参,子类的参数列表也是如此。

    sub.fun(100,50) 编译失败,方法参数是数组,java要求严格类型匹配

     7、警惕自增的陷阱

    输出:0

    步骤1:jvm把count(此时是0)值拷贝到临时变量区

    步骤2:count值加1,这时候count的值是1

    步骤3:返回临时变量区的值,0

    步骤4:返回值赋值给count,此时count值被重置为0

    8、少用静态导入

    java5开始引入 import static ,其目的是为了减少字符输入量,提高代码的可阅读性。

    举个例子:

    滥用静态导入会使程序更难阅读,更难维护。静态导入后,代码中就不用再写类名了,但是我们知道类是“一类事物的描述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限放大,这会让阅读者很难弄清楚所谓何意。

    举个糟糕的例子:

    对于静态导入,一定要遵循两个规则:

    1)、不使用*通配符,除非是导入静态常量类(只包含常量的类或接口)

    2)、方法名是具有明确、清晰表象意义的工具类

    9、不要在本类中覆盖静态导入的变量和方法

    本地的方法和属性会被使用。因为编译器有最短路径原则,以确保本类中的属性、方法优先

    10、养成良好习惯,显示声明UID

    11、避免用序列化类在构造函数中为不变量赋值

    序列化1.0

    序列化2.0

    此时反序列化,name:混世魔王

    因为饭序列化时构造函数不会执行。jvm从数据流中获取一个object对象,然后根据数据流中的类文件描述信息查看,发现时final变量,需要重新计算,于是引用person类中的name值,而辞职jvm又发现name竟然没有赋值,不能引用,于是不再初始化,保持原值状态。

     12、避免为final变量复杂赋值

    反序列化时final变量在以下情况不会被重新赋值

    1)通过构造函数为final变量赋值

    2)通过方法返回值为final变量赋值

    3)final修饰的属性不是基本类型

    原理:

    保存在磁盘(网络传输)的对象文件包括两部分

    1)类描述信息

    包括路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。与class文件不同的是,它不记录方法、构造函数、statis变量等的具体实现。

    2)非瞬态(transient关键字)和非静态实例变量值

    这里的值如果是一个基本类型,就保存下来;如果是复杂对象,就连该对象和关联类信息一起保存,并且持续递归下去,其实还是基本数据类型的保存。

    也正是因为这两点,一个持久化后的对象文件会比一个class类文件大很多

    13、使用序列化类的私有方法巧妙解决部分属性持久化问题

    举个例子

    一个服务像另一个服务屏蔽类A的一个属性x

    class A{
    int a;
    int b;
    int x;
    }

    可能有几种解决方案

    1)在属性x前加上transient关键字(失去了分布式部署的能力?todo)

    2)新增业务对象类A1,去掉x属性(符合开闭原则,而且对原系统没有侵入型,但是增加代码冗余,且增加了工作量)

    class A{
    int a;
    int b;
    int x;
    }

    3)请求端过滤。获得A对象以后,过滤掉x属性。(方案可行但不合规矩,自己服务的安全性需要外部服务承担,不符合设计规范)

    理想的解决方案:

    用Serializable接口的两个私有方法 writeObject和readObject,控制序列化和反序列化的过程

    序列化回调:

    java调用objectOutputStream类把一个对象转换成流数据时,会通过反射检查被序列化的类是否有writeObject方法,并且检查其是否符合私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法。

    14、switch-case-break 不要忽略break

    15、易变业务使用脚本语言编写

    java世界一直在遭受异种语言的入侵,比如php,ruby,groovy,js等。这种入侵者都有一个共同特征:脚本语言,他们都在运行期解释执行。为什么java这种强编译型语言会需要这些脚本语言呢?那是因为脚本语言的三大特性:

    1)灵活。脚本语言一般都是动态类型,可以不用声明变量类型而直接使用,也在可以在运行期改变类型

    2)便捷。脚本语言是一种解释性语言,不需要编译成二进制代码,也不需要像java一样生成字节码。它的执行是依靠解释器解释的,因此在运行期变更带啊吗非常容易,而且不用停止应用

    3)简单。

    脚本语言的这些特性是java所缺少的,引入脚本语言可以使java更强大,于是java6开始正式支持脚本语言。但是因为脚本语言比较多,java的开发者也很难确定该支持哪种语言,于是jcp提出了jsr223规范,只要符合该规范的语言都可以在java平台上运行(默认支持js)

    16、慎用动态编译(热部署)

    动态编译一直是java的梦想,从java6版本开始支持动态编译,可以在运行期直接编译.java文件,执行.class等,只要符合java规范都可以在运行期动态家在。

    在使用动态编译时,需要注意以下几点:

    1)在框架中谨慎使用

    比如在Spring中,写一个动态类,要让它动态注入到spring容器中,这是需要花费老大功夫的

    2)不要在要求高性能的项目使用

    动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果在工具类项目中它则可以很好的发挥其优越性,比如在idea中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试,非常方便。

    3)考虑安全问题

    如果你在web界面上提供了一个功能,允许上传一个java文件然后运行,那就等于说“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意java程序就可以让你所有的安全工作毁于一旦。

    4)记录动态编译过程

    建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序

    17、避免instanceof非预期结果

    instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的,两侧操作符需要有继承或实现关系。

    1)‘A’ instanceof Character :编译不通过 ‘A’ 是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断。

    2)null instanceof String:编译通过,返回false。这是instanceof特有的规则:若左操作符是null,结果直接返回false

    3)(String)null instanceof String :编译通过,返回false。null是一个万用类型,也可以说是没类型,即使做类型转换还是个null

    4)new Date()instanceof String:编译不通过,date类和string没有继承或实现关系

    5)new GenericClass<String>().isDateInstance("") :编译通过,返回false。T是string,与date之间没有继承或实现关系,是因为java的泛型是为编码服务的,在编译成字节码时,T已经是object类型了。传递的实参是string类型,也就是说T的表面类型是object,实际类型是string,这句话等价于object instance of date ,所以返回false。

    18、断言绝对不是鸡肋

    在防御式编程中经常会用断言对参数和环境做出判断,避免程序因不当的输入或错误的环境而产生逻辑异常,断言在很多语言中都存在,c、c++、python都有不同的断言表达形式。在java中断言的使用是assert关键字,如下

    assert <布尔表达式> :<错误信息>

    在布尔表达式为假时,抛出AssertionError错误,并附带错误信息

    两个特性

    1)assert默认是不开启的

    2)AssertionError是继承自Error的。这是错误,不可恢复

    不可使用断言的情况:

    1)在对外公开的方法中

    2)在执行逻辑代码的情况下。因为生产环境是不开启断言的。避免因为环境的不同产生不同的业务逻辑

    建议使用断言的情况:

    1)在私有方法中,私有方法的使用者是自己,可以更好的预防自己犯错

    2)流程控制中不可能到达的区域。如果到达则抛异常

    3)建立程序探针。我们可能会在一段程序中定义两个变量,分别代码两个不同的业务含义,但是两者有固定的关系。例如 var1=var2*2,那我们就可以在程序中到处设‘桩’,断言这两者的关系,如果不满足即表明程序已经出现了异常,业务也就没有必要运行下去了

    19、不能只替换一个类

    举个例子:

    如果在一个运行中项目,直接替换constans.class ,其中 maxage改为180。client中的输入依然是150

    原因是

    对于final修饰的基本类型和string类型,编译器会认为它是稳定态,所以在编译时就直接把值编译到字节码中了,避免了在运行期引用,以提高代码的执行效率。

    对于final修饰的类,编译器认为它是不稳定态,在编译时建立的则是引用关系(soft final),如果client类引入的常量是一个类或实例,即使不重新编译也会输出最新值

    基本数据类型相关

    21、用偶判断,不用奇判断

    i%2==1?奇数:偶数

    这个逻辑是不对的,当i为负数时计算错误。因为取余的计算逻辑为

    int remainder(int a,int b){
          return a-a/b*b;
    }

    22、用整数类型处理货币

    在计算机中浮点数有可能是不准确的,它只能无限接近准确值,而不能完全精确。这是由于浮点数的存储规则决定的(略过)。

    举个例子:system.out.print(10.00-9.06)  :0.4000000000000036

    有两种解决方案:

    1)BigDecimal

    BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDeciaml是最优的解决方案。

    2)使用整型

    把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小100倍。

    23、不要让类型默默转换

    举个例子:

    太阳逛照射到地球上需要8分钟,计算太阳到地球的距离。

    long result=light_speed * 60 * 8;

    输出的结果是 -202888064 

    原因:java是先运算然后再进行类型转换的,三者相乘,超过了int的最大值,所以其值是负值(溢出是负值的原因看一下

    正确的处理是 long result=light_speed * 60L * 8;

    24、数字边界问题

            举个例子:

             if(order+base<limit){...}

             当order+base足够大时,超过了int的最大值,其值是负值,所以业务逻辑会有问题

    25、四舍五入问题

    math.round(-10.5) 输出 -10 这是math。round采用的舍入规则所决定的(采用的是正无穷方向舍入规则)

    以上算法对于一个5000w存款的银行来说,一年将损失10w。一个美国银行家发现了此问题并提出了一个修正算法,叫做银行家舍入的近似算法(规则不记录了)。java5可以直接用RoundingMode类提供的Round模式。与BigDecimal绝配。RoundingMode支持7种舍入模式:

    远离零方向舍入、趋向零方向舍入、向正无穷方向舍入、向负无穷方向舍入、最近数字舍入、银行家算法

    26、提防包装类型的null值

    举个例子。当list中有null元素,自动拆箱时调用intValue()会报空指针异常。

    27、谨慎包装类型的大小比较

    举个例子。i==j false。Integer是引用类型

    28、优先使用整型池

    Integer缓存了-128-127的Integer对象。所以通过装箱(Integer.valueOf())获得的对象可以复用。

     public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
                return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }

     29、优先选择基本数据类型

    自动装箱有一个重要的原则:基本类型可以先加宽,再转变成宽类型的包装类型,但不能直接转变成宽类型的包装类型。

    举个例子

    两次调用都是基本类型的方法

    30、不要随便设置随机数种子

    程序启动后,生成的随机数会不同。但是每次启动程序,生成的都会是三个随机数。产生随机数和seed之间的关系如下:

    1)种子不同,产生不同的随机数

    2)种子相同,即使实例不同也产生相同的随机数

    Random的默认种子(无参构造)是System.nanoTime()的返回值(jdk1.5以前是System.currentTimeMillis()),这个值是距离某一个固定时间点的纳秒数,不同的操作系统和硬件有不同的固定时间点,随机数自然也就不同了

     第三章 类、对象及方法

    31、在接口中不要存在实现代码

    实际上是有这种可能的,但是千万不要这样写

    举个例子

     32、静态变量一定要先声明,后赋值

    举个例子:输出1

    静态变量的初始化:先分配空间,再赋值

    类初始化时会先先分配空间,再按照加载顺序去赋值 :静态的(变量、静态块)的加载顺序是 从上到下

    33、不要覆写静态方法

    在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限,并且父类、子类都是静态方法,此种行为叫做隐藏,它与重写有两点不同:

    1)表现形式不同。@override可以用于重写,不能用于隐藏

    2)指责不同。隐藏的目的是为了抛弃父类静态方法。重写则是将父类的行为增强或者减弱,延续父类的指责

    34、构造函数尽量简化

    35、避免在构造函数中初始化其他类

    1)更符合面向对象编程

    2)类与类关系复杂,容易造成栈溢出

    36、使用构造代码块精炼程序

    什么是构造代码块

    构造代码块的特性:在每个构造函数中都运行,且会首先运行

    38、使用静态内部类提高封装性

    1)提供封装性

    2)提高代码可读性

    39、使用匿名内部类的构造函数

    举个例子

            List l1=new ArrayList();  
            List l2=new ArrayList(){}; 
            List l3=new ArrayList(){{}};
            System.out.println(l1.getClass()==l2.getClass());//false 
            System.out.println(l1.getClass()==l3.getClass());//false
            System.out.println(l3.getClass()==l2.getClass());//false

    l1:arraylist实例

    l2:{}表示一个匿名内部类,但是没有重写任何方法,相当于匿名内部类的实例

    l3:外层{}表示一个匿名内部类,但是没有重写任何方法,内层{}表示匿名内部类的初始化块,可以有多个。

    40、匿名类的构造方法很特殊

     一般类默认都是调用父类的无参构造函数的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造函数类,它在初始化时直接调用类父类的同参构造函数,然后再调用自己的构造代码块

    41、让多重继承成为现实

    使用内部类实现多继承

    42、让工具类不可实例化

    java项目中使用的工具类非常多,比如jdk自己的工具类java.lang.math java.util.collections等都是我们经常用到的。工具类的方法和属性都是静态的,不需要生成实例即可访问,而且jdk也做了很好的处理,由于不希望被初始化,于是就设置构造函数为private。也可以在构造函数中抛一个error。

    43、避免对象的浅拷贝

    一个类实现类cloneable接口就表示它具备类被拷贝的能力,如果再重写clone方法就会完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是在大对象的生成上,这会使性能的提升非常显著。但是object提供的默认对象拷贝是浅拷贝。

    浅拷贝的规则:

    1)基本类型

    如果变量是基本类型,则拷贝其值

    2)对象

    拷贝地址引用

    3)string字符串

    这个比较特殊,拷贝的也是一个地址,是个引用。但是在修改时,它会从字符串池中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为string是一个基本类型

    44、推荐使用序列化实现对象的拷贝

    实现serializable接口,使用序列化实现对象的深拷贝。或者其他序列化方式json等

    45、重写equals方法时不要识别不出自己

    一句话总结,equals满足自反性,传递性,对称性,一致性规则 ,参考:https://www.cnblogs.com/amei0/p/5176037.html

     46、重写equals应该考虑null

    一句话总结,equals满足自反性,传递性,对称性,一致性规则 ,参考:https://www.cnblogs.com/amei0/p/5176037.html

    47、在equals中使用getclass进行类型判断 

    两个不同的类,可能具备相同的属性,导致equals相等

    48、重写equals方法必须重写hashcode方法

         参考:https://www.cnblogs.com/amei0/p/5176037.html

    49、推荐重写tostring

    50、使用package-info类为包服务

    java中有一个特殊的类:package-info类,它是专门为本包服务的。package-info特性

    1)它不能随便被创建

    不能通过new的形式创建。可以在text创建,拷贝过来

    2)它服务的对象很特殊

    一个类是一类或一组事物的描述,但package-info是描述和记录本包信息的

    3)package-info类不能有代码实现

    package-info也会被编译成package-info.class ,但是在package-info.java文件里不能声明package-info类。不可以继承,没有接口...

    package-info作用

    1)声明友好类和包内访问常量

    虽然它没有编写package-info的实现,但是package-info.class类文件还是会生成。

    2)为在包上标注注解提供便利

    比如我们要写一个注解,查看一个包下的所有对象,只要把注解标注到package-info文件中即可,而且很多开源项目也采用类此方法,比如struts2的@namespace、hibernate的@filterdef等

    3)提供包的整体注释说明

    通过javadoc生成文档时,会把这些说明作为包文档的首页,让读者更容易对该包有一个整体的认识。当然在这点上它与package.htm的作用是相同的,不够package-info可以在代码中维护文档的完整性,并且可以实现代卖与文档的同步更新

    51、不要主动进行垃圾回收 

    四、字符串

    52、推荐使用string直接量赋值

    常量池

    53、注意方法中传递的参数要求

    举个例子:

    string.replaceAll("","") 要求第一个参数传的是正则表达式。如果传了一些$($在正则中表示字符串的结束位置)等,会有异常

    54、正确使用string、stringbuffer、stringbuilder

    55、注意字符串的位置

    java对加号的处理机制:在使用加号进行计算的表达式中,只要遇到string字符串,则所有的数据都会转换为string类型进行拼接,如果是对象,调用tostring方法的返回值拼接

    string s=1+1+"a"; //2a  

    56、选择适当的字符串拼接方法

    1)+ :编译器对字符串的加号做了优化,它会使用tringbuilder的append方法进行追加,然后通过tostring方法转换成字符串

    2)concat():数组拷贝,但是会会创建string对象

        public String concat(String str) {
            int otherLen = str.length();
            if (otherLen == 0) {
                return this;
            }
            int len = value.length;
            char buf[] = Arrays.copyOf(value, len + otherLen);
            str.getChars(buf, len);
            return new String(buf, true);
        }

    3)stringbuffer、stringbuilder:数组拷贝

     public AbstractStringBuilder append(String str) {
            if (str == null)
                return appendNull();
            int len = str.length();
            ensureCapacityInternal(count + len);
            str.getChars(0, len, value, count);
            count += len;
            return this;
        }

    57、推荐在复杂字符串操作中使用正则表达式

    58、统一编码

    59、对字符串排序持一种宽容的心态

    比较器一般是通过compareTo比较。该方法是先取得字符串的字符数组,然后一个个比较大小(减号操作符),也就是unicode码值的比较。所以非英文排序会出现不准确的情况。java推荐使用collator类进行排序

     public int compareTo(String anotherString) {
            int len1 = value.length;
            int len2 = anotherString.value.length;
            int lim = Math.min(len1, len2);
            char v1[] = value;
            char v2[] = anotherString.value;
    
            int k = 0;
            while (k < lim) {
                char c1 = v1[k];
                char c2 = v2[k];
                if (c1 != c2) {
                    return c1 - c2;
                }
                k++;
            }
            return len1 - len2;
        }

    第五章 数组与集合

    60、性能考虑,数组是首选

    61、若有必要,使用可变数组

    参考list扩容 

    62、警惕数组的浅拷贝

    arrays.copyof, clone都是浅拷贝

    63、在明确的场景下,为集合指定初始容量

    64、多种最值算法,实时选择

    一句话总结:不必追求最快算法,还是要结合业务,找准侧重点

    65、避开基本类型数组转换列表陷阱

    基本数据类型不能作为aslist的输入参数

    输出1

    int类型不能泛型化。替换成Intger

    66、 aslist方法产生的list对象不可更改

      public static <T> List<T> asList(T... a) {
            return new ArrayList<>(a);
        }
     private static class ArrayList<E> extends AbstractList<E>
            implements RandomAccess, java.io.Serializable
        {}
    public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    public void add(int index, E element) {
            throw new UnsupportedOperationException();
        }    
    }

    arraylist是arrays的静态内部类,在父类声明类add方法,抛出异常 。为啥要设计成这样?如果是不可变类推荐guava。immlist

     67、不同的列表选择不同的遍历方法

    两种方式

    1) foreach :shi iterator的变形用法。也就是需要先创建一个迭代器容器,然后屏蔽内部遍历细节,对外提供hasnext等方法。

    2) for(int i=0 )  采用下标方式遍历列表

    arraylist 实现类RandomAccess接口(随机存取接口),这也就标志着arraylist是一个可以随机存取的列表 。适合采用下标方式来访问

    linkedlist,双向链表,两个元素本来就是有关联的,用foreach会高效

    68、频繁插入和删除时使用linkedlist

    69、列表相等只需关心元素数据

    s1.equals(s2) 。两者都是list,equals方法是在abstractlist中定义的

    70、子列表只是原列表的一个视图

    list接口提供来sublist方法,返回的子列表只是一个视图,对子列表的操作相当于操作原列表

    71、推荐使用sublist处理局部列表

    代码比较简洁

    72、生成子列表后不要再操作原列表(sublist)

    checkForconmodification方法是用于检测并发修改的。modcount是从子列表的构造函数中赋值的,其值等于生成子列表时的修改次数。因为在生成子列表后再修改原始列表modcount的值就不相等了。

       public void add(int index, E e) {
                rangeCheckForAdd(index);
                checkForComodification();
                parent.add(parentOffset + index, e);
                this.modCount = parent.modCount;
                this.size++;
            }
      private void checkForComodification() {
                if (ArrayList.this.modCount != this.modCount)
                    throw new ConcurrentModificationException();
            }

    73、使用Comparator进行排序

    74、不推荐使用binarySearch对列表进行检索

    binarySearch基于二分算法。要求列表本身升序。推荐indexof

    75、集合中的元素必须做到compareTo和equals同步

    比如说 indexOf()依赖equals方法查找,binarySearch则依赖compareTo方法查找

    76、集合运算时使用更优雅的方式

    1)并集:list1.addAll(list2)

    2) 交集:list.retainAll(list2)

    3) 差级:list1.removeAll(list2)

    4)无重复的并集:list1.removeAll(list2); list1.addAll(list2)

    77、使用shuffle打乱列表

    78、减少hashmap中元素的数量

    哈?行吧。entry对象和2倍扩容 注意下内存使用就行

    79、集合汇总的哈希码不要重复

    map key 冲突。降低效率

    80、多线程使用vector 、hashtable 

    算了吧

    81、非稳定排序推荐使用list

    原文是与treeset做对比的 

    82、有点及面,一叶知秋-集合大家族

    第六章 枚举和注解

    83、推荐使用枚举定义常量

    84、使用构造函数协助描述枚举项add code

    85、小心switch带来的空值异常 

    86、在switch的default代码快中增加assertionerror错误

    switch代码与枚举之间没有强制的约束关系,只是在语义上建立了联系。在default后直接抛出AssertionError错误,其含义就是“不要跑到这里来”

    87、使用valueof前必须校验

    valueof先通过反射从枚举类的常量声明中查找,若找到就直接返回,若找不到则抛出无效参数异常。valueof本意是保护编码中的枚举安全性,使其不产生空枚举对象。

    88、用枚举实现工厂方法模式更简洁

    1)避免错误调用

    2)性能更好

    3)降低类间耦合

    89、枚举项的数量限制在64个以内

    为了更好的使用枚举,java提供了两个枚举集合EnumSet和EnumMap。EnumSet很好用,但是它有一个隐藏的特点。

     一句话总结:当枚举项《64时,创建RegularenumSet实例,大于64时,创建JumboEnumSet实例对象。而JumboEnumSet内部分段处理。多了一次映射。所以小于64时效率比较高

    90、小心使用注解

    @inherited注解有利有弊,利的地方是一个注解只要标注到父类,所有的子类都会自动具有父类相同的注解,整齐、统一而且便于管理,弊的地方是单单

    91、枚举和注解结合使用威力更大

    92、注意@override不同版本的区别

    jdk1.5严格遵守重写的定义。1.6以后开放了很多。比如说继承接口的,在1.5不能用@override

    第七章、泛型和反射

    93、java的泛型是类型擦除的

    之所以这样处理:

    1)避免jvm的大换血。c++的泛型生命期延续到了运行期,而java是在编译器擦除掉的。避免jvm大量的重构工作

    2)版本兼容。在编译器擦除可以更好的支持原生类型。在java1.5以上,即使声明一个list这样的原生类型也是可以正常编译通过的,只是会产生警告

    94、不能初始化泛型参数和数组

    T[] tArray=new T[3]; 编译失败

    List<T> list=new ArrayList<T>();编译成功

    为什么数据不可以。但是集合可以?因为arraylist表面是泛型,其实已经在编译器转型为object了。在某些情况下,我们确实需要泛型数组,可以如下实现:

    95、强制声明泛型的实际类型

    96、不同的场景使用不同的泛型通配符

    ?:任意类型

    extends:某一个类的子类型

    super:某一个类的父类型

    1)泛型结构只参与‘读’操作则限定上界

    2)泛型结构只参与‘写’操作则限定下界

    97、警惕泛型是不能协变和逆变的

    协变:用一个窄类型替换宽类型

    举个例子:

    逆变:一个宽类型替换窄类型

    举个例子:

    逆变不属于重写,只是重载而已。由于此时的dostuff方法已经与父类没有任何关系类,只是子类独立扩展出的一个行为,所以是否声明为dostuff方法名意义不大,逆变已经不具有特别的意义类。所以重点关注下协变。(其实也就是多态)

    泛型不支持协变、逆变

     98、建议采用的顺序是List<T> List<?> List<Object>

    1)List<T>表示的是list集合中的元素都为t类型,具体类型在运行期决定; List<?> 也是;List<Object>则表示集合中的所有元素

    2)List<T>可以进行读写操作,它的类型是固定的T类型,在编码期不需要进行任何的转型操作;List<?> 是只读类型的,因为编译器不知道list中容纳的是什么类型的元素,而且读出来的元素都是object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意list<?>可以remove,clear等,因为删除动作与泛型类型无关 ; List<Object>也可以读写操作,但是它执行写入操作时需要转型,而此时已经失去了泛型存在的意义了

    99、严格限定泛型类型采用多重界限

     举个例子

    100、数组的真实类型必须是泛型类型的子类型

    101、注意Class类的特殊性

    java语言是先把java源文件编译成后缀为class的字节码文件,然后再通过classloader机制把这些类文件加载到内存中,最后生成实例执行的。java使用一个元类MetaClass来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象。特殊性:

    1)无构造函数。Class对象是在加载类时由java虚拟机通过调用类加载器中的defineClas方法自动构造的

    2)可以描述基本类型。虽然8个基本类型在jvm中并不是一个对象,它们一般存在于栈内,但是class类仍然可以描述它们,int.class

    3)其对象都是单例模式。一个Class的实例对象描述一个类,并且只描述一个类。

    Class类是java的反射入口,只有在获得类一个类的描述对象后才能动态地加载,调用。一般获得一个class对象有三种途径

    1) 类属性方式 String.class

    2) 对象的getClass方法 new String().getClass()

    3) forName方法加载 Class.forName("java.lang.String")

    102、实时选择getDeclaredMethod和getMethod

    getMethod:获得所有public访问级别的方法,包括从父类继承的方法

    getDeclaredMethod:获得自身类的所有方法,包括public、private等

    103、反射访问属性或者方法时将accessible设置为true

    accessible属性表示是否容易获得,是否需要进行安全检查。我们知道,动态修改一个类或方法都会受java安全体系的制约,而安全的处理是非常消耗资源的(性能非常低),因此对于运行期要执行的方法或属性就提供类accessible可选项:由开发者决定是否要逃避安全体系的检查

    invoke的执行:

      @CallerSensitive
        public Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException,
               InvocationTargetException
        {
            if (!override) {//保存了accessible的值
                if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                    Class<?> caller = Reflection.getCallerClass();
                    checkAccess(caller, clazz, obj, modifiers);
                }
            }
            MethodAccessor ma = methodAccessor;             // read volatile
            if (ma == null) {
                ma = acquireMethodAccessor();
            }
            return ma.invoke(obj, args);
        }

    accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度地提升系统性能(由于取消了安全检查,也可以运行private方法,访问private属性)。经过大量测试,在大量的反射情况下,设置accessible为true可以提升性能20倍以上

    104、使用forName动态加载类文件

    105、动态加载不适合数组

    数组是一个非常特殊的类,虽然它是一个类,但没有定义类路径。编译后会为不同的数组类型生成不同的类

    所以实际上是可以动态加载一个对象数组的

    但是这没有任何意思,因为它不能生成一个数组对象,也就是说以上代码只是把一个string类型的数组类和long类型的数组类加载到类内存中,并不能通过newinstance方法生成一个实例对象,因为它没有定义数组的长度,没有长度的数组是不允许存在的。

    但是!可以用使用array数组反射类来动态加载:

    因为数组比较特殊,要想动态创建和反问数组,基本的反射是无法实现的。于是java就专门定义来一个array数组反射工具类来实现动态探知数组的功能

    106、动态代理可以使代理模式更加灵活

    107、动态代理可以使装饰者模式更加灵活

    108、不需要太多关注反射效率

    一般情况下反射并不是性能的终极杀手,而代码结构混乱、可读性差则很可能会埋下隐患

    第八章、异常

    110、提倡异常封装

    1)提高系统的友好性

    2)提高系统的可维护性

    3)解决java异常机制自身的缺陷,抛多个异常

    111、采用异常链传递参数

    public
    class IOException extends Exception {
     public IOException() {
            super();
        }
       //记录上一级异常
        public IOException(String message, Throwable cause) {
            super(message, cause);
        }
    }

     112、受检异常尽可能转化为非受检异常

    1)受检异常使接口声明脆弱

    oop要求我们尽量多的面向接口编程,可以提高代码的扩展性、稳定性等。但是一旦设计异常问题就不一样了。比如一个接口抛出了异常a,随着业务的发展,该接口可能还会抛出异常b、异常c等。这会产生两个问题:

    a)异常是主逻辑的补充逻辑,修改一个补充逻辑,就会导致主逻辑也被修改,也就是出现了实现类“逆影响”接口的情景,我们知道实现类是不稳定的,而接口是稳定的,一旦定义了异常,则增加了接口的不稳定性

    b)实现的类变更最终会影响到调用者,破坏了封装性,这也是迪米特法则所不能容忍的(设计模式6原则:一个对象应该对其他对象保持最少的了解)

    2)受检异常使代码的可读性降低

    3)受检异常增加了开发工作量

    我们知道,异常需要封装和传递,只有封装才能让异常更容易理解,上层模块才能更好的处理,可这也会导致低层级的异常没完没了的封装,无端加重了开发的工作量。但是我们也不能把所有的受检异常转化为非受检异常,原因是在编码期上层模块不知道下层模块会抛出何种非受检异常,只有通过规则或文档来约束,可以这样说:

    受检异常:法律下的自由

    非受检异常:协约性质的自由

    受检异常威胁到系统的安全性、稳定性、可靠性、正确性时,不能转换为非受检异常

    113、不要在finally块中处理返回值

    1)覆盖了try代码块中的return返回值

    在代码中加上try代码块就标志着运行时会有一个throwable线程监视着该方法的运行,若出现异常,则交由异常逻辑处理。

    a)finally中修改基本数据类型返回值。返回值不会变化

    方法在栈内存中运行,并且会按照‘先进后出’的原则执行,当dostuff方法执行完return a时,此方法的返回值已经确定是int类型1,此后finally代码块再修改a的值已经于dostuff返回值没有任何关系了

    b)finally中修改基本引用类型返回值。返回值会变化

    返回李四。person是一个引用对象,在try代码块中的返回值的person对象的地址。

    2)屏蔽异常

    异常线程在监视到有异常发生时,就会登记当前的异常类型为dataformatexception,但是当执行finally代码块时,则会从新为dostuff方法赋值,也就是告诉调用者‘该方法执行正确,没有产生异常,返回值是1’

    114、不要在构造函数中抛出异常

    1)加重了上层代码编写者的负担

    只能通过文档约束来告知上层代码有异常

    2)导致子类代码膨胀

    子类的无参构造函数默认调用的是父类的构造函数,所以子类的无参构造也必须抛出该异常或父类

    3)违背来里氏替换原则(父类能出现的地方子类就可以出现,而且将父类替换为子类也不会产生任何异常)

          如果子类抛出的异常比父类抛出的异常范围大,则无法直接直接替换

    4)子类构造函数扩展受限

      子类存在的原因就是期望实现并扩展父类的逻辑,但是父类构造函数抛出异常却会让子类构造函数的灵活性大大降低

    115、使用throwable获得栈信息

    aop编程可以很轻松的控制一个方法调用哪些类,也能控制哪些方法允许被调用,一般来说切面编程只能控制到方法级别,不能实现代码级别的植入,比如一个方法被类A调用时放回1,在类B调用时放回0,这就要求被调用者具有识别调用者的能力。在这种情况下,可以使用throwable获得栈信息,然后鉴别调用者并分别输出

    116、异常只为异常服务

    不要包含业务流转

    117、多使用异常,把性能问题放一边

    java的异常处理机制确实比较慢,单单从对象的创建来说,new一个ioexception会比string慢5倍,因为它要执行fillinstatcktrace方法,要记录当前栈的快照,而string类则要直接申请一个内存创建对象。而且,异常类是不能缓存的,期望预先建立大量的异常对象是不可能的。(在jdk1.6,一个异常对象创建的时间1.4毫秒)

    第九章、多线程和并发

    118、不推荐覆写start方法

    从多线程的设计思想来说。run方法是业务的处理逻辑,start是启动一个线程,并执行run方法

    119、启动线程前stop方法是不可靠的

    stop():对于未启动的线程(线程状态为new),会设置其标志位为不可启动,而其他的状态则是直接停止

    start():会先启动线程,再判断标志位,如果标志位是不可启动,则停止线程

    会有一个时间差

    120、不使用stop方法停止线程

    1)stop方法是过时的

    2)stop方法会导致代码逻辑不完整(比如说stop时还没释放io资源等等)

    3)stop方法会破坏原子逻辑(会直接释放所有锁,导致原子逻辑受损)

    121、线程优先级只使用三个等级

    线程的优先级(priority)决定了线程获得cpu运行的机会,优先级越高获得的运行机会越大,优先级越低获得的机会越小。但不保证顺序执行。thread类中设置了三个优先级,建议使用优先常量,而不是1到10随机的数字。

    class thread{ 
    /**
         * The minimum priority that a thread can have.
         */
        public final static int MIN_PRIORITY = 1;
    
       /**
         * The default priority that is assigned to a thread.
         */
        public final static int NORM_PRIORITY = 5;
    
        /**
         * The maximum priority that a thread can have.
         */
        public final static int MAX_PRIORITY = 10;
    }

    122、使用线程异常处理器提升系统可靠性

    jdk1.5以后,在thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理

    其实比较鸡肋

    123、volatile不能保证数据同步

    volatile关键字比较少用,原因

    1)java1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题是移植性比较差;

    2)只保证了可见性,不保证原子性

    124、异步运算考虑使用callable接口

    1)尽可能多地占用系统资源,提供快速运算??

    2)可以监控线程执行的情况,比如是否执行完毕,是否有返回值,是否有异常等

    3)可以为用户提供更好的支持,比如计算进度

    125、优先选择线程池

    126、适时选择不同的线程池来实现

    127、lock于synchronized是不一样的

    128、预防线程死锁

    死锁需要4个条件

    1)互斥条件:一个资源每次只能被一个线程使用

    2)资源独占条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放

    3)不剥夺条件:线程已获得的资源在未使用完之前,不能强行剥夺

    4)循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

    解决:

    1)减少资源共享

    2) 使用自旋锁

    lock.trylock(1,TimeUnit.SECONDS)一定时间内获取不到锁则放弃

    129、适当设置阻塞队列长度

    130、使用countdownlatch协调自线程

    131、cyclicbarrier让多线程齐步走

    功能与countdownlotch类似,增加了子线程结束后的处理线程

    第十章、性能和效率

    132、提升java性能的基本方法

    1)不要在循环条件中计算

    2)尽可能把变量、方法声明为fianl static类型

    3)缩小变量的作用范围(加快gc)

    4)使用stringbuilder stringbuffer

    5)使用非线性检索

    6)覆写exception的fillinstacktrace方法

    7)不建立冗余对象

    这个方法是用来记录异常时的栈信息的,非常耗时,如果不关注可以覆盖之,会使性能提升10倍以上

    133、若非必要,不要克隆对象

    通过clone方法生成一个对象时,就会不再执行构造函数了,只是再内存中进行数据块的拷贝,此方法看上去似乎应该比new的性能好很多,但是java的缔造者们也认识到二八原则,80%的对象是通过new关键字创建出来的,所以对new再生成对象时做了充分的性能优化,事实上,一般情况下new生成的对象clone生成的性能方面要好很多

    134、推荐使用望闻问切的方式诊断性能

    ??????

    135、必须定义性能衡量标准

    136、枪打出头鸟--解决首要系统性能问题

    ??????

    137、调整jvm参数以提升性能

    138、性能是个大‘咕咚’

    ?????

    第11章、开源世界

    139、大胆采用开源工具

    140、推荐使用guava扩展工具包

    142、apache扩展包

    143、推荐使用joda日期时间扩展包

    144、可以选择多种collections扩展

    。。。。后面的就不说了 淡疼

  • 相关阅读:
    shell面试题整理
    用循环链表实现Josephus问题
    in与exists的区别
    单链表的建立/测长/打印/删除/排序/逆序/寻找中间值
    float在内存中的存放
    crontab定时任务详解
    螺旋队列问题之二
    螺旋队列问题之一
    android网络编程--从网络下载图片,并保存到内存卡
    android Shader类简介_渲染图像示例
  • 原文地址:https://www.cnblogs.com/amei0/p/10140120.html
Copyright © 2011-2022 走看看