13

这才是阿里巴巴Java开发手册的正确打开姿势!

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw%3D%3D&%3Bmid=2247487438&%3Bidx=2&%3Bsn=e74c1b62660cddc5b791914996024202
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

△Hollis, 一个对Coding有着独特追求的人△

qAZbim7.jpg!web

这是Hollis的第  243 篇原创分享

作者 l Hollis

来源 l Hollis(ID:hollischuang)

很多人都知道,阿里巴巴在2017发布了《阿里巴巴Java开发手册》,前后推出了很多个版本,并在后续推出了与之配套的IDEA插件和书籍。

相信很多Java开发都或多或少看过这份手册,这份手册有7个章节,覆盖了编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构以及设计规约等方面。

这份规约可以说是覆盖了Java开发的方方面面,如果还有人没看的话,强烈建议大家好好看看,并且仔细研读。

手册中,有那么一些规则,是比较容易理解的。比如一些变量命名规范,有另外一些规则,是不太容易理解的,背后是有很多思考的,有一些则是阿里这么多年来遇到的坑的总结。

这份手册在诞生之初,是在阿里内部的,那时候就引起了广泛的讨论。最终外界看到的那份手册,是阿里无数工程师"挑剔"后的结果,可以说是凝聚了无数工程师成功的经验、踩过的坑等。

其实,规约最大的价值,应该是促使人去思考规约制定背后的思考。真的去探查规约背后的原理,这个过程中可以学习到很多东西。

我写过几篇关于规约中部分规则的自己理解。这里面简单做个总结,这个系列并没有完结,我后面还会继续完善。

一、为什么阿里巴巴禁止工程师直接使用日志系统(Log4j、Logback)中的 API

ve6Fzam.jpg!web

在Java生态体系中,围绕着日志,有很多成熟的解决方案。关于日志输出,主要有两类工具。

一类是日志框架,主要用来进行日志的输出的,比如输出到哪个文件,日志格式如何等。另外一类是日志门面,主要一套通用的API,用来屏蔽各个日志框架之间的差异的。

所以,对于Java工程师来说,关于日志工具的使用,最佳实践就是在应用中使用如Log4j + SLF4J 这样的组合来进行日志输出。

这样做的最大好处,就是业务层的开发不需要关心底层日志框架的实现及细节,在编码的时候也不需要考虑日后更换框架所带来的成本。这也是门面模式所带来的好处。

详解: 为什么阿里巴巴禁止工程师直接使用日志系统(Log4j、Logback)中的 API

二、为什么阿里巴巴建议集合初始化时,指定集合容量大小?

3eiEvqm.jpg!web

HashMap有扩容机制,就是当达到扩容条件时会进行扩容。如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。

默认情况下,当我们设置HashMap的初始化容量时,实际上HashMap会采用第一个大于该数值的2的幂作为初始化容量。

但是,为了最大程度的避免扩容带来的性能消耗,我们建议可以把默认容量的数字设置成expectedSize / 0.75F + 1.0F 。在日常开发中,可以使用

Map < StringString > map = Maps.newHashMapWithExpectedSize( 10 );

来创建一个HashMap,计算的过程guava会帮我们完成。

但是,以上的操作是一种用内存换性能的做法,真正使用的时候,要考虑到内存的影响。

详解: 为什么阿里巴巴建议集合初始化时,指定集合容量大小?

三、为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作

6RBRziM.jpg!web

我们使用的增强for循环,其实是Java提供的语法糖,其实现原理是借助Iterator进行元素的遍历。

但是如果在遍历过程中,不通过Iterator,而是通过集合类自身的方法对集合进行添加/删除操作。那么在Iterator进行下一次的遍历时,经检测发现有一次集合的修改操作并未通过自身进行,那么可能是发生了并发被其他线程执行的,这时候就会抛出异常,来提示用户可能发生了并发修改,这就是所谓的fail-fast机制。

当然还是有很多种方法可以解决这类问题的。比如使用普通for循环、使用Iterator进行元素删除、使用Stream的filter、使用fail-safe的类等。

详解: 为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作

四、为什么阿里巴巴禁止把SimpleDateFormat定义为static类型的?

SimpleDateFormat主要可以在String和Date之间做转换,还可以将时间转换成不同时区输出。但是在并发场景中SimpleDateFormat是不能保证线程安全的,需要开发者自己来保证其安全性。

SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。这其实就是问题的关键。

如果一个SimpleDateFormat使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。就会有并发安全问题。

详解: 为什么阿里巴巴禁止把SimpleDateFormat定义为static类型的?

五、为什么阿里巴巴不建议在for循环中使用"+"进行字符串拼接

rMBFFjE.jpg!web

使用+拼接字符串,其实只是Java提供的一个语法糖,通过反编译,我们可以发现,其实字符串常量使用"+"在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。

如果在一个for循环中,使用"+"对字符串进行拼接,那么每一次都会重新new一个StringBuilder,然后再把String转成StringBuilder,再进行append。

而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。

详解: 为什么阿里巴巴不建议在for循环中使用"+"进行字符串拼接?

六、为什么阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值

QRN3qmr.jpg!web

序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID。

在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。

详解: 为什么阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值?

七、为什么阿里巴巴要求谨慎使用ArrayList中的subList方法

jqQFFrr.jpg!web

subList是List接口中定义的一个方法,该方法主要用于返回一个集合中的一段、可以理解为截取一个集合中的部分元素,他的返回值也是一个List。

但是subList 返回的并不是一个全新的List,而是一个视图。就是说,SubList并没有重新创建一个List,而是直接引用了原有的List(返回了父类的视图),只是指定了一下他要使用的元素的范围而已(从fromIndex(包含),到toIndex(不包含))。

所以,首先我们无法将subList方法得到的集合直接转换成ArrayList。因为SubList只是ArrayList的内部类,他们之间并没有继承关系,故无法直接进行强制类型转换。

另外,视图和原List的修改还需要注意几点,尤其是他们之间的相互影响:

  • 1、对父(sourceList)子(subList)List做的非结构性修改(non-structural changes),都会影响到彼此。

  • 2、对子List做结构性修改,操作同样会反映到父List上。

  • 3、对父List做结构性修改,会抛出异常ConcurrentModificationException。

所以,阿里巴巴Java开发手册中有另外一条规定:

详解: 为什么阿里巴巴要求谨慎使用ArrayList中的subList方法?

八、为什么阿里巴巴建议开发者谨慎使用继承?

Bze2Uzn.jpg!web

作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。

继承,在写代码的时候就要指明具体继承哪个类,所以,类的继承关系是在编译期就确定的。并且从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。

组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。另外,代码复用方式上也有一定区别:

  • 继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性。

  • 组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说通过组合的代码复用是黑盒式代码复用。因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法。

还有,Java中不支持多继承,而组合是没有限制的。就像一个人只能有一个父亲,但是他可以有很很多辆车。

详解: 为什么阿里巴巴建议开发者谨慎使用继承?

九、为什么阿里巴巴禁止开发人员使用isSuccess作为变量名

RZRbiyN.jpg!web

fastjson和jackson在把对象序列化成json字符串的时候,是通过反射遍历出该类中的所有getter方法,得到getHollis和isSuccess,然后根据JavaBeans规则,他会认为这是两个属性hollis和success的值。

但是Gson并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成json

可以看到,由于不同的序列化工具,在进行序列化的时候使用到的策略是不一样的,所以,对于同一个类的同一个对象的序列化结果可能是不同的。

如果对于同一个对象,我使用fastjson进行序列化,再使用Gson反序列化,并且恰巧这个对象中某个属性是以isXXX命名的,那么就会产生问题。

作为开发者,我们应该想办法尽量避免这种问题的发生,对于POJO的设计者来说,只需要做简单的一件事就可以解决这个问题了,那就是把isSuccess改为success。

详解: 为什么阿里巴巴禁止开发人员使用isSuccess作为变量名

十、为什么阿里巴巴不让使用 COUNT(列名)或 COUNT(常量)来替代 COUNT(*)呢

JJrQz2A.jpg!web

因为 COUNT(*) 是SQL92定义的标准统计行数的语法,所以MySQL对他进行了很多优化,MyISAM中会直接把表的总行数单独记录下来供 COUNT(*) 查询,而InnoDB则会在扫表的时候选择最小的索引来降低成本。当然,这些优化的前提都是没有进行where和group的条件查询。

在InnoDB中 COUNT(*) COUNT(1) 实现上没有区别,而且效率一样,但是 COUNT(字段) 需要进行字段的非NULL判断,所以效率会低一些。

因为 COUNT(*) 是SQL92定义的标准统计行数的语法,并且效率高,所以请直接使用 COUNT(*) 查询表的行数!

详解: MySQL的COUNT语句,竟然都能被面试官虐的这么惨!?

十一、为什么阿里巴巴不允许使用Executors创建线程池?

YZjQ7zr.jpg!web

Java中的BlockingQueue主要有两种实现,分别是ArrayBlockingQueue 和 LinkedBlockingQueue。

ArrayBlockingQueue是一个用数组实现的有界阻塞队列,必须设置容量。

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。

这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的容量的话,其默认容量将会是Integer.MAX_VALUE。

而newFixedThreadPool中创建LinkedBlockingQueue时,并未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。

上面提到的问题主要体现在newFixedThreadPool和newSingleThreadExecutor两个工厂方法上,并不是说newCachedThreadPool和newScheduledThreadPool这两个方法就安全了,这两种方式创建的最大线程数可能是Integer.MAX_VALUE,而创建这么多线程,必然就有可能导致OOM。

详解: Java中线程池,你真的会用吗?

总结

以上,就是我之前一段时间通过学习《手册》中的部分规则之后,自己总结的一些内容,这个过程中自己也学习到很多东西。

所以想说,这才是学习《手册》的正确姿势,这样才能最大程度的成长!

这个系列还在继续更新,后面还会会逐步完善。欢迎关注与交流。

关于作者 Hollis,一个对Coding有着独特追求的人,现任阿里巴巴技术专家,个人技术博主,技术文章全网阅读量数千万,《程序员的三门课》联合作者。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK