Java语言的细枝末节

Java语言的细枝末节

一、将局部变量的作用域最小化

要使局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明

几乎每一个局部变量的声明都应该包含一个初始化表达式。

try-catch语句有关的例外情况:

如果一个变量被一个方法初始化,而这个方法可能会抛出一个受检异常,该变量就必须在try块的内部被初始化。

1.1 for循环优于while循环

无论是for循环,还是for-each形式的for循环,都允许声明循环变量,它们的作用域被限定在正好需要的范围之内。

使用for循环,更简短,从而增强了可读性。

二、for-each循环优于传统的for循环

for-each循环通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。这种模式同样适用于集合和数组,同时简化了将容器的实现类型从一种转换到另一种的过程。

利用for-each循环不会有性能损失,甚至用于数组也一样:它们产生的代码本质上与手工编写的一样。

对于嵌套式迭代,for-each循环相对于传统for循环的优势还会更加明显。

三种常见的情况无法使用for-each循环:

  • 解析过滤——需要调用remove方法删除选定的元素
  • 转换——需要取代部分或全部元素值
  • 平行迭代——并行地遍历多个集合

for-each循环不仅能遍历集合和数组,还能遍历实现Iterable接口的任何对象。

三、了解和使用类库

使用标准类库的优点:

  • 通过使用标准类库,可以充分利用这些编写标准类库的专家和知识,以及在你之前的其他人的使用经验;
  • 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案;
  • 标准类库的性能往往随着时间的推移而不断提高,无须使用者做任何努力;
  • 标准类库会随着时间的推移而增加新的功能;
  • 使用标准类库,可以使自己的代码融入主流

3.1 了解和使用类库的重要性

在每个重要的发行版本中,都会有许多新的特性被加入到类库中,所以与这些新特性保持同步是值得的。

不要重复发明轮子

(1)每个程序员都应该熟悉:java.longjava.utiljava.io及其子包中的内容。

关于其他类库的知识可以根据需要随时学习。

(2)Collection Framework(集合框架)和Stream类库应该成为每一位程序员基本工具箱中的一部分,同样也应该成为java.util.concurrent中并发机制的组成部分。

(3)Google 优秀的开源Guava类库[Guava]应该是在高级的第三方类库中去寻找

3.2 Random相关的案例

Random.nextInt(int)结合了同余伪随机数生成器、数论和2的求补算法相关知识。

Random random = new Random();
int r = random.nextInt(23);

从Java 7 开始,就不应该再使用Random了。现在选择随机数生成器时,大多使用ThreadLocalRandom

ThreadLocalRandomRandom生成随机数的速度快很多。

ThreadLocalRandom current = ThreadLocalRandom.current();
int r = current.nextInt(23);

对于Fork Join Pool和并行Stream,则使用SplittalbleRandom

四、如果需要精确的答案,请避免使用floatdouble

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

floatdouble类型尤其不适合用于货币计算。

因为要让一个float或者double精确地表示0.1(或者10的任何其他附属次方)是不可能的。

4.1 使用BigDecimalintlong进行货币计算

使用BigDecimal:

使用BigDeciaml的String构造器,而不是double构造器。可以避免将不正确的值引入到计算中。

    private static void payCanDeciaml(){
        final BigDecimal TEN_CENTS = new BigDecimal(".10");
        int itemsBought = 0;
        BigDecimal founds = new BigDecimal("1.00");
        for (BigDecimal price  = TEN_CENTS; founds.compareTo(price)>=0; price = price.add(TEN_CENTS)){
            founds = founds.subtract(price);
            itemsBought ++;
        }
        System.out.println(itemsBought + " items bought.");
        System.out.println("Maney left over: $ " + founds);
    }

BigDeciaml有两个缺点:与使用基本运算类型相比,这样做很不方便;而且速度很慢。

除了使用BigDecimal之外,还有一种方法是使用int或者long,到底选用int还是long要取决于所涉及数值的大小,同时要自己处理十进制小数点

例如:以分位单位进行计算,而不是以元位单位。

如果数值范围没有超过9位十进制数,就可以使用int;如果不超过18位数字,就可以使用long。如果数值可能超过18位数字,就必须使用BigDeciaml

五、基本类型优先于装箱基本类型

自动装箱和自动拆箱模糊了但并没有完全抹去基本类型和装箱类型之间的区别:

  • 基本类型只有值,而装箱基本类型则具有与它们的值不同的统一性;

    两个装箱基本类型可以具有相同的值和不同的统一性;

  • 基本类型只有函数值,而每个装箱基本类型则都有一个非函数值,除了它对应基本类型的所有函数值之外,还有个null;

  • 基本类型通常比装箱基本类型更节省时间和空间;

5.1 对装箱基本类型运用==操作符几乎总是错误的

下面的代码,计算表达式i==j,它在两个对象引用上执行统一性比较。

        Comparator<Integer> naturalOrder = (i,j)->(i<j)?-1 : (i == j? 0 : 1);
        // return 1
        int compare = naturalOrder.compare(new Integer(23), new Integer(23));

5.2 如果null对象引用被自动拆箱,就会抛出一个NullPointerException异常

当在一项操作中混合使用基本类型和装箱基本类型时,装箱基本类型就会自动拆箱

5.3 反复的装箱和拆箱会导致明显的性能下降

使用装箱基本类型的几个合理用处:

  • 作为集合中的元素、键、和值;

    不能讲基本类型放在集合中,因此必须使用装箱基本类型。

  • 在参数话类型和方法中,必须使用装箱基本类型作为类型参数

    因为Java不允许,例如,必须声明为ThreadLocal<Integer>,不能声明为ThreadLocal<int>

  • 在进行反射的方法调用时,必须使用装箱基本类型

六、如果其他类型更适合,则尽量避免使用字符串

6.1 字符串不适合代替其他的值类型

如果存在适当的值类型,不管是基本类型,还是对象引用,大多应该使用这种类型;

如果不存在这样的类型,就应该编写一个类型。

6.2 字符串不适合代替枚举类型

6.3 字符串不适合代替聚合类型

如果一个实体有多个组件,用一个字符串来表示这个实体通常时很不恰当的。

七、了解字符串连接的性能

7.1 字符串连接操作符(+)

为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级的时间。

7.2 为了获得可以接受的性能,用StringBuilder代替String

可以为StringBuilder分配一个足够大的容量,这样就不需要自动扩展。

StringBuilder sb = new StringBuilder(CAPACITY);

八、通过接口引用对象

如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。

如果没有适合的接口,就用类层次结构中提供了必要功能的最小的具体类来引用对象。

九、了解反射机制

使用反射机制要付出代价:

  • 损失了编译时类型检查的优势;

  • 执行反射访问所需要的代码非常笨拙和冗长;

  • 性能损失;

    反射方法调用比普通方法调用慢了许多。

使用反射机制的一种情况

许多程序必须用到的类在编译时时不可用的,但是在编译时存在适当的接口或超类,通过它们可以引用这个类。如果是这种情况,就可以用反射方法创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。

    private static void setTest(String[] args){
        Class<? extends Set<String>> cl = null;
        try {
            cl = (Class<? extends Set<String>>) Class.forName(args[0]);
        } catch (ClassNotFoundException e) {
            fatalError("Class not found.");
        }

        Constructor<? extends Set<String>> cons = null;
        try {
            cons = cl.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            fatalError("No parameterless constructor");
        }
        Set<String> s = null;
        try {
            s = cons.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

        s.addAll(Arrays.asList(args).subList(1, args.length));
        System.out.println(s);
    }

    private static void fatalError(String msg){
        System.err.println(msg);
        System.exit(1);
    }

十、谨慎地使用本地方法

所谓本地方法是指本地编程语言(比如C或者C++)来编写的方法。

他们提供了“访问特定于平台的机制”的能力,比如访问注册表(registry)。他们还提供了访问本地一流代码库的能力,从而可以访问遗留数据。最后,本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。

  • 使用本地方法来访问特定于平台的机制是合法的,但是几乎没有必要;

    当Java中没有相当的类库可用时,使用本地方法来使用遗留代码库也是合法的。

  • 使用本地方法来提供性能的做法不值得提倡;

使用本地方法的一些严重缺陷:

  • 因为本地方法不是安全的,所以使用本地方法的应用程序也不再能免受内存损坏错误的影响;
  • 因为本地语言是与平台相关的,使用本地方法的应用程序也不再是可自由移植的;
  • 使用本地方法的应用程序也更难调试;
  • 如果不小心,本地方法还可以降低性能,因为回收垃圾器不是自动的,甚至无法追踪本机内存;
  • 需要“胶合代码”的本地方法编写起来单调乏味,并且难以阅读;

十一、谨慎地进行优化

有三条格言:

  • 很多计算机上的过失都被归咎于效率(没有达到必要的效率),而不是任何其他的原因——甚至包括盲目地做傻事;

  • 不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源;

  • 在优化方法,我们应该遵守两条规则:

    规则一:不要进行优化;

    规则二(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的为优化方案之前,请不要进行优化。

优化的弊大于利,特别是不成熟的优化

(1)不要为了性能而牺牲合理的结构

**要努力编写好的程序而不是快的程序。**遍布全局而且限制性能的结构缺陷几乎是不可能被改正的,除非重新编写系统。

(2)必须在设计过程中考虑到性能问题

  • 要努力避免那些限制性能到设计决策;

  • 要考虑API设计决策的性能后果;

    API设计对于性能的影响是非常实际的。

    为了·获得好的性能而对API进行包装,这是一种非常不好的想法。

  • 再多的底层优化也无法弥补算法上的选择不当。

在每次试图做优化之前和之后,要对性能进行测量。

十二、遵守普遍接受的命名惯例

如果长期养长的习惯用法与下面的不同,请不要盲目遵从下面的惯例。

12.1 字面上

  • 包和模块的名称应该是层次结构,用句号分隔每个部分。每个部分都包含小写字母,极少情况下还有数字;

    包名称的其余部分应该包含一个或者多个描述该包的组成部分。这些组成部分应该比较简短,通常不超过8个字符。鼓励使用有意义的缩写形式。

    许多包的名称都只有一个组成部分再加上Internet域名。

    比较大的名称使用附加部分是正确的,它们的规模要求它们要被分隔成一个非正式的层次结构。

  • 类和接口的名称,包括枚举和注解类型的名称,都应该包括一个或者多个单词,每个单词的首字母大写。

  • 方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,只不过方法或者域的名称的第一个字母应该小写。

  • 常量域,它的名称应该包含一个或者多个大写的单词,中间用下划线符号隔开。

    常领域是唯一推荐使用下划线的情形。

  • 局部变量名称的字面命名惯例与成员名称类似,只不过它也允许缩写,单个字符和短字符序列的意义取决于局部变量所在的上下文环境。

  • 参数类型通常由单个字母组成。

    T表示任意的类型;

    E表示集合的元素类型;

    KV表示映射的键和值类型;

    X表示异常;

    R表示函数的返回类型;

    任何类型的序列可以是:TUV或者T1T2T3

12.2 语法上

  • 可被实例化的类(包括枚举类型)通常用一个名词或名词短语命名。不可实例化的工具类经常用复数名词命名。

有些方法的名称值得专门提及:

  • 转换对象类型的实例方法,它们返回不同类型的独立对象的方法,经常被称为toType,例如:toStringtoArray
  • 返回视图的方法经常被称为asType,例如: asList
  • 返回一个与被调用对象同值的基本类型的方法,经常被称为typeValue,例如:intValue
  • 静态工厂的常用名称:fromofvalueOfinstancegetInstancenewInstancegetTypenewType
原创文章 161 获赞 19 访问量 6万+

猜你喜欢

转载自blog.csdn.net/hefrankeleyn/article/details/104327022