△Hollis, 一个对Coding有着独特追求的人△
这是Hollis的第 243 篇原创分享
作者 l Hollis
来源 l Hollis(ID:hollischuang)
很多人都知道,阿里巴巴在2017发布了《阿里巴巴Java开发手册》,前后推出了很多个版本,并在后续推出了与之配套的IDEA插件和书籍。
相信很多Java开发都或多或少看过这份手册,这份手册有7个章节,覆盖了编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构以及设计规约等方面。
这份规约可以说是覆盖了Java开发的方方面面,如果还有人没看的话,强烈建议大家好好看看,并且仔细研读。
手册中,有那么一些规则,是比较容易理解的。比如一些变量命名规范,有另外一些规则,是不太容易理解的,背后是有很多思考的,有一些则是阿里这么多年来遇到的坑的总结。
这份手册在诞生之初,是在阿里内部的,那时候就引起了广泛的讨论。最终外界看到的那份手册,是阿里无数工程师"挑剔"后的结果,可以说是凝聚了无数工程师成功的经验、踩过的坑等。
其实,规约最大的价值,应该是促使人去思考规约制定背后的思考。真的去探查规约背后的原理,这个过程中可以学习到很多东西。
我写过几篇关于规约中部分规则的自己理解。这里面简单做个总结,这个系列并没有完结,我后面还会继续完善。
在Java生态体系中,围绕着日志,有很多成熟的解决方案。关于日志输出,主要有两类工具。
一类是日志框架,主要用来进行日志的输出的,比如输出到哪个文件,日志格式如何等。另外一类是日志门面,主要一套通用的API,用来屏蔽各个日志框架之间的差异的。
所以,对于Java工程师来说,关于日志工具的使用,最佳实践就是在应用中使用如Log4j + SLF4J 这样的组合来进行日志输出。
这样做的最大好处,就是业务层的开发不需要关心底层日志框架的实现及细节,在编码的时候也不需要考虑日后更换框架所带来的成本。这也是门面模式所带来的好处。
详解: 为什么阿里巴巴禁止工程师直接使用日志系统(Log4j、Logback)中的 API
HashMap有扩容机制,就是当达到扩容条件时会进行扩容。如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容,而HashMap中的扩容机制决定了每次扩容都需要重建hash表,是非常影响性能的。
默认情况下,当我们设置HashMap的初始化容量时,实际上HashMap会采用第一个大于该数值的2的幂作为初始化容量。
但是,为了最大程度的避免扩容带来的性能消耗,我们建议可以把默认容量的数字设置成expectedSize / 0.75F + 1.0F 。在日常开发中,可以使用
Map < String , String > map = Maps.newHashMapWithExpectedSize( 10 );
来创建一个HashMap,计算的过程guava会帮我们完成。
但是,以上的操作是一种用内存换性能的做法,真正使用的时候,要考虑到内存的影响。
详解: 为什么阿里巴巴建议集合初始化时,指定集合容量大小?
我们使用的增强for循环,其实是Java提供的语法糖,其实现原理是借助Iterator进行元素的遍历。
但是如果在遍历过程中,不通过Iterator,而是通过集合类自身的方法对集合进行添加/删除操作。那么在Iterator进行下一次的遍历时,经检测发现有一次集合的修改操作并未通过自身进行,那么可能是发生了并发被其他线程执行的,这时候就会抛出异常,来提示用户可能发生了并发修改,这就是所谓的fail-fast机制。
当然还是有很多种方法可以解决这类问题的。比如使用普通for循环、使用Iterator进行元素删除、使用Stream的filter、使用fail-safe的类等。
详解: 为什么阿里巴巴禁止在 foreach 循环里进行元素的 remove/add 操作
SimpleDateFormat主要可以在String和Date之间做转换,还可以将时间转换成不同时区输出。但是在并发场景中SimpleDateFormat是不能保证线程安全的,需要开发者自己来保证其安全性。
SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。这其实就是问题的关键。
如果一个SimpleDateFormat使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。就会有并发安全问题。
详解: 为什么阿里巴巴禁止把SimpleDateFormat定义为static类型的?
使用+拼接字符串,其实只是Java提供的一个语法糖,通过反编译,我们可以发现,其实字符串常量使用"+"在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。
如果在一个for循环中,使用"+"对字符串进行拼接,那么每一次都会重新new一个StringBuilder,然后再把String转成StringBuilder,再进行append。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
详解: 为什么阿里巴巴不建议在for循环中使用"+"进行字符串拼接?
序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化ID,就是我们在代码中定义的serialVersionUID。
在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。
详解: 为什么阿里巴巴要求程序员谨慎修改serialVersionUID 字段的值?
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方法?
作为一门面向对象开发的语言,代码复用是Java引人注意的功能之一。Java代码的复用有继承,组合以及代理三种具体的表现形式。
继承,在写代码的时候就要指明具体继承哪个类,所以,类的继承关系是在编译期就确定的。并且从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。
组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。另外,代码复用方式上也有一定区别:
继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性。
组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说通过组合的代码复用是黑盒式代码复用。因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法。
还有,Java中不支持多继承,而组合是没有限制的。就像一个人只能有一个父亲,但是他可以有很很多辆车。
详解: 为什么阿里巴巴建议开发者谨慎使用继承?

fastjson和jackson在把对象序列化成json字符串的时候,是通过反射遍历出该类中的所有getter方法,得到getHollis和isSuccess,然后根据JavaBeans规则,他会认为这是两个属性hollis和success的值。
但是Gson并不是这么做的,他是通过反射遍历该类中的所有属性,并把其值序列化成json
可以看到,由于不同的序列化工具,在进行序列化的时候使用到的策略是不一样的,所以,对于同一个类的同一个对象的序列化结果可能是不同的。
如果对于同一个对象,我使用fastjson进行序列化,再使用Gson反序列化,并且恰巧这个对象中某个属性是以isXXX命名的,那么就会产生问题。
作为开发者,我们应该想办法尽量避免这种问题的发生,对于POJO的设计者来说,只需要做简单的一件事就可以解决这个问题了,那就是把isSuccess改为success。
详解: 为什么阿里巴巴禁止开发人员使用isSuccess作为变量名
因为 COUNT(*)
是SQL92定义的标准统计行数的语法,所以MySQL对他进行了很多优化,MyISAM中会直接把表的总行数单独记录下来供 COUNT(*)
查询,而InnoDB则会在扫表的时候选择最小的索引来降低成本。当然,这些优化的前提都是没有进行where和group的条件查询。
在InnoDB中 COUNT(*)
和 COUNT(1)
实现上没有区别,而且效率一样,但是 COUNT(字段)
需要进行字段的非NULL判断,所以效率会低一些。
因为 COUNT(*)
是SQL92定义的标准统计行数的语法,并且效率高,所以请直接使用 COUNT(*)
查询表的行数!
详解: MySQL的COUNT语句,竟然都能被面试官虐的这么惨!?
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有着独特追求的人,现任阿里巴巴技术专家,个人技术博主,技术文章全网阅读量数千万,《程序员的三门课》联合作者。