zoukankan      html  css  js  c++  java
  • 阅读《Effective Java》每条tips的理解和总结(5)

    61 基本数据类型优于包装类型

        这里说基本数据类型优于包装类型,并不是说包装类型一无是处。某些场景,比如要表示值为null、使用包装类型的方法、与其他对象交互(添加到集合里、用作参数类型)都需要用包装类型。这里的意思是能用基本数据类型就尽量用基本数据类型,因为包装类型自动拆箱、装箱会带来性能损失尤其是循环使用时会大量创建对象;还有就是自动拆、装箱使得多很多隐患,这些错误编译时无法发现,运行时就会出现意料之外的结果。例如:

    static Integer i; 
    public static void main(String[] args) { 
        if (i == 42)  {                            //这里会报空指针异常
            System.out.println("test");    //打印不了,判断时会出错
        } 
    }         

    为什么上面那步报异常,因为虽然包装类型初始为null,拆箱时就会报空指针异常。再比如:

     Comparator<Integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);   //这个比较器的compare方法接收Integer参数,看起来很正常的比较但是有大bug

    如果传入compare(new Integer(666),new Integer(666))。传入两个1的包装类型,但是却不会正确的返回0。因为i == j时,由于两者都是对象于是返回false。

    总结,为什么会出现这两个例子的情况呢,因为我们都认为 拆箱、装箱会为我们解决一切的,而忽略了拆箱装箱有些情形不会触发。如上面包装类型为null、参与==判断的都是包装类型等就没有装、拆箱了。总之,如无必要不要用包装类型,增加隐患。

    62 当使用其他类型更合适时应避免使用字符串

        这一条主要是告诉我们不要看到什么属性,都把它声明为String,String应该仅仅用来表示文本。因为如果使用不当,字符串比其他 类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。 

        误用代替枚举:则失去了枚举的拓展性,失去了枚举类提供的api、单例限制等;误用代替聚合类型:如使用逗号隔开用一个字符串表示多个信息,则会增加处理、解析的开销与麻烦,与出错几率。误用代替数值:则需要用到数值时还需要将字符串转换为数字,增加开销、出错几率。

    63 当心字符串连接引起的性能问题 

        由于String类的不可变特性,用“+”连接两个字符串时不是在改变原有对象,而是根据两个字符串的内容生成新对象。所以,不要使用“+”频繁、大量的操作字符串,而应该用StringBuffer、StringBuilder(线程不安全)。

    当然,”+“这么久来也做了一些优化,如String s = "1" + "2",是直接生成“12”一个对象的。“+”可以满足少数几个字符串连接,但是,还是比StringBuilder拼接速度差几倍。

    64 通过接口引用对象 

        使用接口声明变量或作为方法参数要灵活多了,例如:

    List<Integer> list = new ArrayList<>();  //使用List接口声明,即便后续new LinkedList也不需要改动其他代码。因为对于其他代码来说都是看做List类型
    doSomething(list);               //list的实现类类型改动,这里也无需改动
    ================
    void doSomething(List<?> list) {  //使用List接口做参数类型,可以传入ArrayList、LinkedList等等
        ...
    }

    或许有人认为用实现类声明变量,改动时声明类型和new的构造方法名都一起改就行了。但是这样就会有安全隐患,因为声明的实现类换了后,能使用的方法就会发生变化,别的地方可能原本调用的对象某个方法就没有了,编译时会报错。而使用接口类型声明,改动实现类名时不会影响方法的使用,因为类型还是那个接口没有改动,自然别的用到这个变量的地方也无需改动。

        当然,如果一个类没有实现接口,使用实现类声明变量当然是可以的。例如String和其他表示值的类;和OutputStream这种属于框架的类,这个java.io框架的基础是一个类而不是接口;有就是类实现了接口但是我们需要使用它新增的接口中不存在的方法;等。总之,推荐使用接口声明对象,或者使用底层基类。

    65 接口优于反射

        Java不是动态语言,反射让Java具有一定的动态性,可以显示的加载类(而不是隐式触发),可以在运行时加载指定的类(而不是总是加载一个编译时确定的类名)。但是也有坏处,性能慢,通过反射加载类并创建对象比直接new慢的多;复杂,反射做什么事代码很多很乱;不安全,无法在编译时进行类型检查,运行时可能有很多异常抛出都需要处理;等等。

        反射合法用途是程序在运行时可能依赖某不存在的类、字段、方法,要用反射显示的加载进来。一般只有在编写复杂工具、大型框架时使用反射。一般我们使用反射有限的几个形式,还是可以获得不少好处的:

    List s = null;
    s = con.newInstance();     //后续可以根据接口引用使用创建的对象(不写异常捕获了,实际检查型异常必须捕获的)

    66  明智审慎地使用本地方法 

        本地方法使用cc++等本地语言编写,可以操作更底层粒度更细,一般用来编写注重性能的部分。但是写普通Java程序时绝对不推荐使用本地方法,经过优化java语言已经不比cc++慢多少了,将精力投入实现本地方法收益非常低。而且本地方法由于操作底层,也变的更危险,一个错误可以破坏整个程序,产生的垃圾不能有效被gc回收,需要额外代码整合Java代码。

        总之,如无必要(比如编写虚拟机)不要使用本地方法。

    67  明智审慎地进行优化

        很多情况我们忙着做优化,却忽略了全局架构,最后越优化越糟糕。不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就 完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多 的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。 

    68 遵守被广泛认可的命名约定 

        这条就是说命名要规范,要尽量简短但要包含足够的信息。同时就是驼峰命名法,类、接口首字母大写,方法、变量首字母小写等等。方法命名时get、set、as、to等等开头要仔细考虑后选择。

    69 只针对异常情况才使用异常

      看如下代码:

    try { 
        int i = 0; 
        while ( true )              //无限循环
             range[i++].climb();
    } catch ( ArrayIndexOutOfBoundsException e )   //捕获异常
    {...}       

    如何评价上面的代码?拙劣。可能有人自以为这样效率更高,每一步循环不需要判断循环的结束条件,而是无限循环,当越界时抛出异常结束循环。有这种想法其实是小看了JVM的优化了,经过测试上面那种模式比正常循环慢了一倍。

    滥用异常的坏处很多,比如:模糊了代码意图,使得阅读变困难;降低性能,且增加出错几率;可能会覆盖掉真正发生的异常,导致调试变的十分困难;

    所以,总结很简单,不要让异常控制正常流程,异常仅仅用于真正发生了异常的情况。

     70 对可恢复的情况使用受检异常,对编程错误使用运行时异常 

        异常模块中,Throwable基类下有Error、Exception,Exception下又分为各种受检异常和RuntimeExceptin(非受检Exception)。受检异常举例有:SqlException、IOException...非受检异常有:OutOfBounds、NullPointer、ClassCast....等等Exception。

        简单总结下。受检异常往往依赖外部资源,如果外部资源出错则会抛出异常,这往往不是我们怎么写代码可以控制的,因此抛出的受检异常必须要进行捕获进行处理,或者可以声明抛出给上一层让调用者处理;而RuntimeException类型的异常往往是我们代码错误导致,是可以避免的,因此这类异常不强制捕获,运行时因为这类异常结束运行还可以提示程序员去修改代码。

        对于任何异常,如果进行捕获了,那么程序将继续执行下去。如果抛出的RuntimeException没有进行捕获,那么程序将会在此时结束并打印方法调用路径信息。 自己继承Throwable类编写新类会隐式的看作受检查异常,但是绝对不推荐这么做,这样只会让调用者增加困惑。自己继承Exception类编写的新类将会作为受检查异常,编写受检查异常一般要增加一些方法获取出错原因和额外信息,比如我编写一个付款余额不足的受检查异常,支付时发现余额不足时会抛出,我们应该给这个异常增加个查询当前余额的方法,将当前余额返回给用户提示。

        总而言之,对于可恢复的情况,要抛出受检异常;对于程序错误,就要抛出运行时异常。不确定是否可恢复, 就跑出为受检异常。不要定义任何既不是受检异常也不是运行异常的抛出类型。要在受检异常上提供方法,以便 协助程序恢复。 

    71 避免不必要的使用受检异常 

        受检异常必须要求捕获或声明抛出,由调用者捕获处理。这仅仅只在异常确实能够恢复的情况下使用,如果不是这样则推荐使用未受检异常。例如这样的受检异常是完全没必要的:

     } catch ( TheCheckedException e ) {
        e.printStackTrace();     
        System.exit( 1 );           //捕获了异常但是并没有进行恢复,而是打印调用路径再关闭了程序。这样使用未受检异常也完全可以,使用受检异常是多余的。
    }

    像上面这种,没有恢复异常则完全没必要使用受检异常,因为这样没有得到好处,反而使得代码变得冗余,如果将受检异常抛给调用方法则会增加调用者的困惑,使得方法的使用很不安全。

    72 优先使用标准的异常 

        一般的,JDK里定义的标准异常足够我们应付大多数场景,这些异常包含的信息充分,适用性广且久经使用,应该优先使用。就算我们需要的异常JDK没有,我们自己创建时,也不应该直接 new Exception、Throwable、Error等,这些异常是基类不能准确的表达信息,我们应该把它们当成抽象类看待继承它们定义自己的异常。

    73 抛出与抽象对应的异常 

        这一条的意思是说,抛出的异常的信息和类型要与实际发生的情况相符。比如不能发生了数组越界异常,我报一个NullPointerException,这样就会给调用者和其他人造成很大困扰。这就要求我们要么使用Exception的子类标准异常,要么自己继承Exception创建异常,但是不能直接抛出Exception类异常,更不能new Throwable,这些类不能对应某个实际情况。

        另外,调用某些方法产生的异常,我们如果有需要,可以将子类的异常进行翻译,转而抛出另一种与情况相符的异常。一般这样做:

    try {  ...  
    } catch (ExceptionA cause) {
         throw new OtherException(...);    //可以完全抛出新异常
    } 
    ============================
    try {  ...  
    } catch (LowerLevelException cause) {
         throw new HigherLevelException(cause);  //也可以根据捕获的异常使用构造链的做法,抛出新异常,信息一样类型变了
    } 

    但是异常翻译也不能滥用,用多了会使代码变的很混乱,最先保证的应该是降低调用的方法抛出的异常数量,这就需要在调用前就进行参数检查、状态检查等等工作。

    74 每个方法抛出的异常都需要创建文档 

        方法抛出的受检异常都要一个个声明,不要使用一句throws Exception或throws Throwable就应付了全部,每个抛出的受检异常都应该单独声明。声明的异常,要是有@throws标签记录每个异常的抛出条件。注意未受检的异常只是指编程上的错误,一般不要用throws声明。

    75 在细节消息中包含失败一捕获信息 

        程序发出异常信息如果没有被捕获,就会终止并自动打印出堆栈轨迹,一般就是异常的字符串表示法(即toString方法)。失败时异常返回的信息非常重要,如果信息不足将使得异常重现、代码完善和调试变的非常困难,例如越界异常可以将越界发生时的上界、下界、发生异常的索引位置打印出来(Java的越界异常只打印发生的索引位置,也还可以)。

        当然,异常信息要全但是不是说越多越好,太多冗余信息反而会使调试变困难,应该准确有效。此外,异常发生时抛出的信息很多人都可以看到,要注意不要在异常字符串里加入敏感信息。

        还有就是异常信息是给专业人员看的,不必把这些信息设置的和用户层次的错误信息那么通俗易懂,那样就要多花很多内容描述,就会显得冗余。

  • 相关阅读:
    Broadcom 43228 Kali Linux Ubuntu
    linux 栈空间查看和修改
    mininet 操作命令
    linux shell note
    进程与线程的区别
    JAVA基础--JAVA 集合框架(泛型、file类)
    HashMap的实现原理
    Java 流(Stream)、文件(File)和IO
    总结接口和抽象类的异同
    Java 泛型
  • 原文地址:https://www.cnblogs.com/shen-qian/p/12411261.html
Copyright © 2011-2022 走看看