如何优雅地编写Java

雷军有一句名言广为流传:你写程序有写诗一样的感觉吗?

我回答:很有写诗的感觉了,反正都是回车键嘛。

写诗固然要放飞想象,但也要注意格律,才会有美感。

写代码亦是如此,犹如戴着镣铐起舞,谨守章法,否则不成了社会摇了么。

比如前些年大火的“梨花体”、“乌青体”、“废话体”,更多的是戏谑的意味在里面吧,要说艺术成就,仿佛是在开一个很认真的玩笑。

假如用以上三种风格来写代码,几乎可以对程序员生涯宣判死刑了,可见诗歌圈还是很宽容的。

有人说,我就乐意用自己的风格来写,反正也能运行,效果也不错。这确实很有可能,毕竟游击队有时候也能打得过正规军呢,但是想当将军,首先就需要对编制标准了然于心。除非你是李云龙嘛。

刚进入公司写代码,我泥腿子一个,写得真是惨不忍睹。老大很耐心,建议我首先养成规范的习惯,这对于代码可读性和维护以及日后的问题定位都是大有裨益的,并给我推荐了一份Google Java Style的文档。

由于是全英文,我读得效率很低(还是要提高姿势水平),于是我找来了阿里的Java规范文档来读,阿里作为大中华Java界最坚实的堡垒,其规范很有标杆性。文档不长,40页,比较完备,在风格约定的同时,简述了原因,其实背后还是有很多Java原理中的细节体现,我读了受益匪浅,将其中一些适合初级程序员的内容筛选总结出来,分享给大家,也约束自己渐渐养成良好的编码习惯。

像写诗一样去写代码,需要很深的功力和理解,首先能做的,是把自己的代码写得像那么回事。

编程规约

命名风格

前提

  1. 禁止使用拼音和英文单词混搭
  2. 尽量避免使用拼音
  3. 切忌盲目缩写。

  1. 类名使用UpperCamelCase方式。
  2. 抽象类命名使用Abstract开头,测试类使用Test结尾 。
  3. 枚举类名后面带上Enum,成员全部大写。
  4. 假如类、接口、方法使用了某设计模式,需要在名称中体现出来。

变量

  1. 布尔类型的变量,不要加is前缀。否则可能会导致一些框架的解析错误,因为有的框架就是通过变量名来解析的。
  2. 变量、方法名使用lowerCamelCase方式。

常量定义

  1. 不允许任何未经定义的字符串直接出现在代码中。可读性差,如果是一些固定值的字符串,可以将其设为常量,并规范命名。

  2. 将常量通过其功能分类,分开维护。

  3. 如果常量在一个固定范围内变化,建议使用枚举型。

代码风格

空格

  1. 左右小括号不允许与括号内相邻字符间出现空格.比如,禁止:
if (`空格`a == b`空格`)
复制代码
  1. if/for/while/switch/do 等保留字与小括号之间必须空格。二目,三目运算符的左右两边必须加空格。比如,正确:
if (a == b && a == c);
a == b ? a = c : a = d;
复制代码
  1. 注释的双斜线必须与内容之间有且仅有一个空格。比如,正确:
// 这是一条注释
a = b;
复制代码
  • 多个参数情况下,每个参数的逗号后加一个空格。比如:
void swap(int a, int b)
复制代码

缩进

  1. 采用四个空格空格进行缩进,禁止使用tab,如果采用tab,必须设置为1个tab为4个空格。(使用tab还是空格键是世纪论题)
  2. 单行字符数不超过120个,超出需要换行,换行时:第二行相对第一行缩进4个空格,方法调用的点参与换行;但是如果是多个方法参数需要换行时,参数间的逗号不参与。比如:
List<VideoData> data = entityIds.stream()
    .map(i -> {
    VideoData o = videoDataRepository
    .findByPartnerIdAndEntityId(partnerId, i);
    o = o == null ? new VideoData() : o;
    o.setEntityId(i);
    o.setPartnerId(partnerId);
    return o;
    })
    .collect(Collectors.toList());
复制代码
public ResponseEntity<?> addxxx(@PathVariable("id") Long id,
                                @RequestBody List<Long> ids,
                                @RequestHeader(value="Authorization") String auth) 
复制代码

空行

  1. 不同逻辑、语义、业务的代码之间可以插入一个空行,但不必插入多个空行区分。

OOP规约

Object方法

  1. Object的equals方法容易抛空指针异常,所以应该使用常量或确定有值的对象来调用此方法。比如:
"test".equals(object); 
复制代码

避免:

object.equals("test"); 
复制代码
  1. 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。因为比如IntegerString在某种情况下,会引用缓存池中的对象,而另外的情况会在堆上生成对象,而==比较的是引用的地址。(具体什么时候指向缓存,什么时候指向堆中新建的对象可以查资料)
  2. 谨慎使用Objectclone方法来拷贝对象,因为这是浅拷贝,想要深拷贝可以对clone方法进行重写。

POJO类

  1. POJO类的属性必须使用包装类型,提醒使用者来进行初始化赋值。而基本数据类型会产生默认初始值,这虽然使程序正常运行,但是是不正确的。
  2. 定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值。
  3. 构造器中禁止任何业务逻辑,如果有必要的初始化逻辑,可以放在init()中。
  4. POJO类必须写toString方法。主要是为了在发生异常时,可以直接调用toString来打印属性值,便于排查问题。

其他

  1. 类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。(因为公有方法是调用者或维护者最关心的,这条规则我之前一直做反了)
  2. 循环中的字符串连接方式,使用StringBuilderappend方法,不要直接使用字符串相加,因为这会在堆中循环创建新的字符串对象,造成内存浪费(具体参考StringBuilderStringBufferString,这三者的区别以及String的不可变性也是面试的一个初级高频问题)
  3. 方法权限从严控制,严禁宽泛访问权限。这有利于模块解耦,也有利于维护时更加清晰。

集合处理

  1. Set之所以可以储存不可重复对象,是因为根据对象的equalshashcode方法来判断的。所以这两个方法有一定的关联性,重写equals就务必要重写hashcode。我们平时常采用的String类型作为Map的键来用,因为String中已经帮我们把这两个方法重写过了。如果要用其他类型的对象作为Map的键,务必重写这两个方法。而关于HashSet,可以点开HashSet的源码,发现其中主要是一个HashMap

  2. 在使用泛型通配符时,有这两种形式:<? extends T><? super T> 前者的容器无法接收返回的数据,而后者只能接收T类型及其子类的对象,其原因都是编译器提供了一种类型安全的保护。

  3. 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式。具体为什么这么做,可以看一下手册中的举例以及这个博客的解答foreach循环中为什么不要进行remove/add操作

  4. 集合初始化时,尽量根据实际情况指定集合的大小,因为集合动态扩容是很消耗性能的,具体可以查看源码或者相关博客,也是初级高频面试问题。

  5. 使用 entrySet遍历 Map 类集合KV,而不是 keySet 方式进行遍历。

  6. 利用Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 Listcontains 方法进行遍历、对比、去重操作。

并发处理

  1. 创建线程或线程池时,指定有意义的名字,方便维护和检查。
  2. 线程资源必须通过线程池分配,不能显示创建。这样是为了节省线程之间切换导致的过度开销。
  3. 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险(这一点感觉有点苛刻,个人认为Executors提供的四种线程池能较好地解决问题时,不需要自己去构建一个ThreadPoolExecutor对象):
1. FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2. CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
复制代码
  • 在高并发时,需要考虑锁的性能损耗,不要多余加锁,并且将锁的范围控制在最小化。
  • HashMap是线程不安全的,在并发情况下容易在扩容时出现死链,导致CPU飙升。可以使用其他数据结构比如ConcurrentHashMap来替代,或者加锁。

控制语句

  1. 在一个switch块中,必须包含一个default语句,即使为空。
  2. 在判断或循环中,必须使用大括号。即使只有一行代码,避免采用一行流。
  3. 在高并发场景下,尽量避免使用“=”来作为中断或退出的条件,可以使用“<”、“>”作为区间来判断。假设一个电商场景,商品数量为0时关闭购买入口,但是假如并发出现了错误,数量可能瞬间变为负值,那么程序会一直继续下去。
  4. 在使用判断时,若存在复杂的逻辑语句,可以使用一个命名规范的布尔值来代替这个冗长的语句,以提高代码的可读性。比如:
final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed) {
 ...
} 
复制代码

异常日志

异常处理

  1. 区分稳定代码与不稳定代码,避免粗暴地对大段代码进行try-catch,这不利于维护和阅读。同时,尽量区分可能抛出的异常类型,再进行相应的处理。
  2. 捕获异常是为了处理它,如果当前处理不了或者不想处理,可以抛给它的调用者。
  3. finally中必须对资源对象、流对象进行关闭。并且不能在内使用return,因为这会导致方法结束,而try中的return得不到执行。
  4. 可能出现的空指针异常:
    1. 包装类型自动拆箱成基本类型
    2. 数据库查询结果为null
    3. 集合中即使有元素,取出的元素也可能为null(比如HashMap允许存入null)
    4. 远程调用返回对象时,一律需要判断是否为空。
    5. 对于session中获取的值,建议进行空指针检查。
  5. 级联调用容易产生空指针异常,比如obj.getA().getB().getC()

日志规约

  1. 对于trace/debug/info级别的日志输出,必须使用条件或占位符的方式,否则可能会出现执行了操作,浪费了资源,但是日志没有打印的情况。建议占位符方式:
logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol); 
复制代码
  1. 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过 关键字 throws 往上抛出。,比如:
logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
复制代码
  1. 注意日志输出量的问题,并记得及时删除观察日志。
  2. 注意日志输出的级别。

MYSQL数据库

建表规约

数据类型

  1. 表达布尔概念的字段,必须使用is_xxx来命名,由于mysql中不存在布尔类型的数据类型,使用unsigned tinyint(0和1来表示)
  2. 小数类型为 decimal,禁止使用 floatdouble。因为后两者存在精度丢失的问题,导致在值比较时出现差错。
  3. 使用char定长字符串类型(会预先分配空间)来储存字符串长度几乎相等的数据,比如手机号。varchar是可变长字符串(不会预先分配空间),但是长度不要超过5000。

命名方式

  1. 任何库名、表名、字段名都只能使用小写字母、数字和下划线,并且数字不能放在开头和两个下划线之间
  2. 任何库名、表名、字段名都禁用保留字(这算一个坑,踩过,具体有哪些保留字可以去查文档)表名不能使用复数形式。
  3. 表必备三字段:id, gmt_create, gmt_modified。
  4. 表的命名最好是加上“业务名称_表的作用”。
  5. 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。

分库分表

  1. 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

索引规约

  1. 具有唯一特性的字段,必须建成唯一索引。唯一索引对insert操作的损耗是可以忽略不计的,而对于提升查找速度的效果非常明显。

工程结构

  1. 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。
  2. 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染, JSP 渲染,移动端展示等。
  3. Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  4. Service 层:相对具体的业务逻辑服务层。
  5. Manager 层:通用业务处理层,它有如下特征:
    1. 对第三方平台封装的层,预处理返回结果及转化异常信息;
    2. 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
    3. 与 DAO 层交互,对多个 DAO 的组合复用。
  6. DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
  7. 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值