Effective Java(第三版) 学习笔记 - 第九章 通用编程 Rule57~Rule68

目录

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

Rule58 for-each循环优先于传统的for循环

Rule59 了解和使用类库

Rule60 如果需要精准的答案,请避免使用flout和double

Rule61 基本类型优先于装箱基本类型

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

Rule63 了解字符串连接的性能

Rule64 通过接口引用对象

Rule65 接口优先于反射机制

Rule66 谨慎地使用本地方法

Rule67 谨慎地进行优化

Rule68 遵守普遍接受的命名习惯


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

最小作用域原则很简单,也很容易理解。目的是增强可读性、可维护性,降低出错可能性。需要注意的是声明局部变量时不要再方法一开始就声明所有可能会用到的变量。什么时候需要用,什么时候在声明。

同时,书中指出一种情况是大家容易忽略的写法问题,就是迭代器循环。

        List<String> list = Arrays.asList("1", "2", "3");
        // 写法一
        Iterator<String> it = list.iterator();
        while(it.hasNext()) {
            System.out.println(it.next());
            it.remove();
        }
        // 写法二
        for (Iterator<String> it2 = list.iterator(); it2.hasNext();) {
            System.out.println(it2.next());
            it2.remove();
        }

这里有两种写法,日常大家都习惯用第一中(包括我也是),但是如果真的要吹毛求疵的话,第一种写法的风险就是it变量的作用范围其实过大,有可能被后面的代码误用。以后用迭代器的时候注意就行。

还有一种方法可以使变量作用域变小就是,不断地方法拆分。用细小的方法方法名称代替大段的方法体中的注释,同样也可以变相的缩减变量作用域。同时使得方法显得结构化,更加便于阅读理解意图,让每个方法都专注于单一职责,也符合本身的设计模式原则。在《重构-改善代码的坏味道》一书中也有详细手法介绍。不得不说,拆分方法改变了我以前的编程习惯,显得代码更加精炼,而且便于日后维护。

Rule58 for-each循环优先于传统的for循环

for-each循环大家很熟悉了,就不过多介绍了。相比于for循环,for-each不需要用变量i、j做下标来标记当前获取第几个元素,特别是Java8之后在Iterable中内嵌了forEach()方法便于函数式编程。但是有些场景不适合用for-each循环

  • 解构过滤:遍历删除。如果用for循环,需要从后往前操作,不然下标容易出问题。而用迭代器会更加便利,同时可以用Java8中的removeIf方法,避免显示的遍历。而这都是for-each无法完成的操作。
  • 转换:如果需要遍历列表或者数组,替换其中的某些或者全部的元素值。就需要用列表迭代器或者数组索引来完成。(当然如果列表中都是对象,在for-each中对象赋值对象的属性是可以的)。
  • 平行迭代?:如果需要并行地遍历多个集合,那么需要显式地控制迭代器或索引变量,以便所有迭代器或索引变量都可以同步进行(正如上面错误的card和dice示例中无意中演示的那样)。

下例是书中的例子,但是没感觉是平行迭代中针对for-each不能用的感觉,相反,例子中是用嵌套for-each解决的问题。有点疑惑,这个例子看不出为什么不能用for-each,所以打了个问号。

// Same bug as NestIteration.java (but different symptom)!! - Page 213
public class DiceRolls {
    enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }

    public static void main(String[] args) {
        // Same bug, different symptom!
        Collection<Face> faces = EnumSet.allOf(Face.class);

        for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
            for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
                System.out.println(i.next() + " " + j.next());

//        for (Iterator<Face> i = faces.iterator(); i.hasNext(); ) {
//            Face f1 = i.next();
//            for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
//                System.out.println(f1 + " " + j.next());
//        }

        System.out.println("***************************");

        for (Face f1 : faces)
            for (Face f2 : faces)
                System.out.println(f1 + " " + f2);
    }
}

Rule59 了解和使用类库

总结归纳起来一句话,不要重复造轮子。而且往往类库中提供的内容,经由算法工程师、大多数程序员使用验证过,可能比起自己实现的功能具有更加优异的表现。

但是问题的核心在于,我们怎么知道类库或者三方类库中已经提供了哪些基础支撑,没有人能完全知道所有的类库。

  • 关注每次JAVA版本更新的版本说明。比如,Java7随机数推荐用java.util.concurrent.ThreadLocalRandom,Java8的并行流或者ForkJoinPool推荐搭配java.util.SplittableRandom。
  • 查看源码中常用的类库。比如,java.lang、java.util、java.io及其子包中的内容。特别是关于并发编程常提到的JUC,其实就是java.util.concurrent。

Rule60 如果需要精准的答案,请避免使用flout和double

做过小数计算,特别是金额的都知道,我们项目中不会用flout或者double来进行处理,因为它们存在精度问题。

    // Broken - uses floating point for monetary calculation!
    public static void main(String[] args) {
        double funds = 1.00;
        int itemsBought = 0;
        for (double price = 0.10; funds >= price; price += 0.10) {
            funds -= price;
            itemsBought++;
        }
        System.out.println(itemsBought + " items bought.");
        System.out.println("Change: $" + funds);
    }

一般项目中常用的两种替代方式解决小数是:

  • 用BigDecimal来操作小数计算。
  • 根据小数位数,将小数位向右偏移(即 * 1000),使其变成整型来进行操作。

但是两种做法都有一定的不是缺陷的缺点

  • 与基本运算符相比,BigDecimal操作起来显得不是那么方便,速度上比不上基本运算符。但是为了精度准确,其实都可以接受。
  • 整型具有位数限制,int9位、long18位,如果小数部分位数越多,可能能容纳下的整数部分越少。而且需要自己处理右偏移成整数。

Rule61 基本类型优先于装箱基本类型

Java很早就拥有了自动拆装箱的功能,平常的开发中放在这方面的注意力相对会少。

但是基本类型和装箱基本类型是有一些差别的,最直接的就是:

  • 基本类型都会有各自的默认值,而装箱基本类型默认都是null
  • 基本类型可以直接==比较,而装箱基本类型因为有可能是null,所以需要当成String一样来处理(避免空指针、比较要用equals)。
  • 装箱基本类型有可能会影响到性能方面。
public class Sum {
    private static long sumLong() {
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++)
            sum += i;
        return sum;
    }
    private static long sumlong() {
        long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++)
            sum += i;
        return sum;
    }

    public static void test(Supplier<Long> fun, boolean print) {
        long x = 0;
        List<Long> timeList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            long start = System.nanoTime();
            x += fun.get();
            long end = System.nanoTime();
            long time = (end - start) / 1_000_000;
            timeList.add(time);
        }
        if (print) {
            LongSummaryStatistics result = timeList.stream().mapToLong(
                    Long::longValue).summaryStatistics();
            System.out.println("最长时间:" + result.getMax() + "ms, 最短时间:"
                + result.getMin() + "ms,平均时间:" + result.getAverage() + "ms");
        }
    }
    public static void main(String[] args) {
        // 预热
        Sum.test(Sum::sumLong, false);
        // 实际测量
        Sum.test(Sum::sumLong,true);
        // 预热
        Sum.test(Sum::sumlong, false);
        // 实际测量
        Sum.test(Sum::sumlong,true);
    }
}

------打印结果
最长时间:7173ms, 最短时间:6914ms,平均时间:6977.3ms
最长时间:764ms, 最短时间:753ms,平均时间:758.1ms

事实剩余雄辩,仅仅只是一个Long sum = 0L; / long sum = 0L;的差异,最终的时间相差近10倍。

但是有些场合下,我们必须使用封装类型:

  • 集合中的键、值、元素。
  • 泛型声明,实际使用的场合。例如,ThreadLocal<int>不行,必须ThreadLocal<Integer>
  • 反射方法调用时。
  • 业务中允许是null可能的类属性。

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

  • 字符串不适合代替其他的值类型。改用boolean、int要用相应类型。
  • 字符串不适合代替枚举类型。枚举相比字符串的常量、更适合定义系列常量。
  • 字符串不适合代替聚合类型。常见的用符合进行拼接,然后用时拆分。
  • 字符串不适合代替能力表(即、键或实例)。例如ThreadLocal<T>,而不是用String来管理。

Rule63 了解字符串连接的性能

很平常的规约了,字符串拼接,建议用StringBuilder(非线程安全)、StringBuffer(线程安全、相应方法加上了synchronized)。

值得注意的是,如果能提前知道最终的字符长度,在创建StringBuilder或者StringBuffer时,与Map一样直接指定最终大小,避免频繁扩容。

Rule64 通过接口引用对象

在Rule51 应该使用接口而不是类作为参数类型中核心含义一样。尽量减少直接面向实现类编程,尽可能的利用接口来进行声明定义,这样有些时候程序更加灵活。就像我们日常用spring开发自动注入时一样,并不是直接指定impl实现类,而是service的接口开发。

Rule65 接口优先于反射机制

Java的反射机制十分强大,可以在运行期间再决定类型、构建不同的类实例,然后获得一个类中所有的信息:变量、方法、构造器、注解,可以运行类方法(包括私有的,通过setAccessible(true))。

反射基本是万能的,但是它有一定的缺陷:破坏了程序的封装性、绕过了编译期的检查、性能较慢。一般情况下不会用到,但是有些场合又是必须的,例如:数据库连接配置初始化。

Rule66 谨慎地使用本地方法

Java关键字native就是用来声明本地方法的。一般情况下我们不会遇到需要使用自定义本地方法的地方。

Rule67 谨慎地进行优化

书中开篇三句格言

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

--William A.Wulf [Wulf72]

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

--Donald E.Knuth [Knuth74]

在优化方面,我们应该遵守两条规则:
规则1:不要进行优化。
规则2(仅针对专家):还是不要进行优化--也就是说,在你还没有绝对清晰的优化方案之前,请不要进行优化。

--M.A. Jackson [Jackson75]

优化在我看来其实分为两种:

  1. 性能上的确影响很大、经常运行超时、报错等。
  2. 代码实在可读性差,新需求不知道如何迭代实现了,可能需要考虑代码结构优化重构。

性能优化可能会给阅读性带来障碍、代码结构重构可能会影响程序性能。看似是冲突的两种优化,其实本质不是矛盾的。

在《重构-改善代码的坏味道》中有与本书相同的一个观点,要努力编写好的程序而不是快的程序。本质的核心理解在于,代码是为人写的、还是为机器执行写的。我们不应该为了一点性能上的追求,而放弃可阅读性,因为最终代码的维护及迭代更新还是要靠人来实现。

在《JVM性能权威指南》中也提到了,如果要做性能优化,先用性能剖析工具分析,要把注意力放在最核心的地方。而不是将大量的精力用在一些猜想的性能有问题的地方,这样往往没有预想中的性能优化结果、甚至有时候更差了。

在做代码重构优化也好,性能优化也好,都有一个前提条件:拥有完善全面的自动化测试工具,防止改出了bug。性能测试还分为:微基准测试、宏基准测试、介基准测试。对于已经上线。并且运行良好,同时没有完善全面的自动化测试时,我们的建议都是不要进行代码重构优化、或者性能优化,不要动它。

几本书中也有另一个共通的理念:不要过早的进行优化!!!

但是这不是我们平常开发时就完全不注意的理由,在首次实现时,良好的代码结构、一些已知的性能很差的写法、日常开发规约还是需要遵守的。

Rule68 遵守普遍接受的命名习惯

基础中的基础,这里就不多说了。但是同时,英文命名也是普遍程序员里面感觉最头疼的事情之一吧。

本文技术菜鸟个人学习使用,如有不正欢迎指出修正。xuweijsnj

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值