Java通用编程规范学习


说明:记录内容为平常代码中常用到的且容易忽视的内容

命名

  1. 规则1.2 所有标识符仅使用ASCII字母、数字、下划线_*,名称由正则表达式匹配\w{2,64},建议不超过31个
    不应使用特殊前缀或后缀。例如下面是不合适的:name_, mName, s_name 和
    kName
  2. 方法命名建议如下:
    1,get + 非布尔属性名
    2,is + 布尔属性名
    3,set + 属性名
    4,has + 名词/形容词
    5,动词
    6,动词 + 宾语
    7,回调方法(callback)允许介词 + 动词形式,比如:onCreate,onDestroy。其中动词主要用在动作的对象自身身上,如document.print();

在这里插入图片描述

  1. 类命名如图:不应用动词,而应使用名词,比如Customer,WikiPage,Account;避免采用类似Manager,Processor,Data,Info这样模糊的词
    在这里插入图片描述

  2. 不要使用魔鬼数字,用有意义的常量代替。SQL或日志的字符串,不应视为“魔鬼数字”,不需定义为字符串常量;不应该取NUM_FIVE = 5或NUM_5 = 5这样的魔鬼常量。

  3. 避免使用否定的布尔变量名,布尔型的局部变量和方法,加上表达是非意义的前缀
    关于前缀:JavaBeans常用的 is ,也可以是 has , can , should

注释

  1. Javadoc用于每一个public或protected修饰的元素
  2. Javadoc注释可以不出现在override覆盖超类型的方法上;对于setter/getter, 这类“简单,明显”的方法的注释是可选的;已使用junit框架默认的BeforeClass、 AfterClass注解,无需Javadoc

排版

  1. 一个源文件按顺序包含版权、package、import、顶层类,且用空行分隔
  2. import包应当按照先静态、安卓,华为公司,其它商业组织,其它开源第三方、net/org开源组织、最后java的分类顺序出现,并用一个空行分组
  3. 一个类或接口的声明部分应当按照类变量、静态初始化块、实例变量、实例初始化块、构造器、方法的顺序出现,且用空行分隔。
    类(静态)变量、实例变量、构造器,均按访问修饰符从大到小排列:
    public、protected、package(default)、private
  4. 在条件语句和循环块中必须使用大括号。对于非空块和块状结构,左大括号放在行尾。
  5. 使用空格进行缩进,每次缩进4个空格。
  6. 应该避免空块;多块的右大括号应该新起一行
  7. 大括号内的代码块行首之前和行尾之后不要加空行。
  8. 禁止C风格的数组声明:方括号构成类型的一部分,是: String[] args ,而不是 String args[] 。
  9. switch,case语句块结束时如果不加break,需要有注释说明(fall-through)。如果case语句是空语句,则可以不用加注释特别说明。
  10. switch语句要有default分支,除非switch的条件变量是枚举类型
  11. 数字字面量以大写字母为后缀:L、d、f

变量和类型

  1. 不能用浮点数作为循环变量。
  2. 需要精确计算时不要使用float和double,建议使用int, long, BigDecimal等。
  3. 浮点型数据判断相等不能直接使用==。考虑使用Float或Double的compare(f1, f2)方法,或BigDecimal在这里插入图片描述
  4. 禁止尝试与NaN进行比较运算,相等操作使用Double或Float的isNaN方法
  5. 在引用类型向下转换前用instanceof进行判断:在这里插入图片描述
  6. 基本类型优于包装类型,注意合理使用包装类型
    使用包装类型合理的场景有:
    作为集合中的元素、键和值
    泛型,必须使用包装类型,如 List list , OptionalInt,rpcResult()
    反射方法调用需使用包装类型,例如在Method.invoke,MethodHandle.invoke中
    POJO类的字段、RPC方法的返回值和参数等可能要序列化的且可能缺失值的场景中
  7. 明确地进行类型转换,不要依赖隐式类型转换。通过这种方式,程序员表明他知道所涉及的不同类型,并且混合是有意的。以免意外地浮点数转换截取,导致误差逐步放大。

方法

  1. 避免方法过长,不超过50行(非空非注释)
  2. 避免方法的代码块嵌套过深,不要超过4层。
    说明:函数本身算一层,try-catch的不算一层嵌套。方法内的lambda表达式、局部类和匿名类嵌套层次以最内层方法来计算,不累积enclosing method的嵌套层次。方法的代码块嵌套深度指的是方法中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。
  3. 使用类名调用静态方法,而不要使用实例或表达式来调用
  4. 为减轻因疏忽导致的再次对入参赋值,可在参数前加final关键字
  5. 方法的参数个数不应超过5个:
    函数的参数个数不要超过5个,如果超过可以考虑:
    ·看能否拆分函数
    ·看能否将相关参数合在一起,定义成类,用对象封装
    ·尝试建造者Builder或工厂模式,JDK有不少可参考,例如Calendar.Builder,HttpClient.Builder
  6. 构造方法如果参数较多,尽量重用参数最多的构造方法。
  7. 对于返回数组或者容器的方法,应返回长度为0的数组或者容器,代替返回null

类和接口

  1. 避免在无关的变量或无关的概念之间重用名字,避免隐藏(hide)、遮蔽(shadow)和遮掩
    (obscure)。
    隐藏:子类与父类之间。属性、静态方法或内部类可以分别隐藏(hide)在其超类中可访问到的具有相同名字(对方法而言就是相同的方法签名)的所有属性、方法或内部类。
    在这里插入图片描述
    遮蔽(shadow)------类内部:变量、方法或类可以分别遮蔽(shadow)在类内部具有相同名字的变量、方法或类。局部变量隐藏全局变量。
    遮掩(obscure)------类内部:一个变量可以遮掩具有相同名字的一个类,只要它们都在同一个范围内。
  2. 不要在父类的构造方法中调用可能被子类覆写的方法。
    当在父类构造方法中调用可能被子类覆写的方法时,构造方法的表现是不可预知的,很可能会导致异常。当子类初始化的时候,会调用父类的构造方法,当构造方法调用了被子类覆写的方法,往往会由于子类的初始化未完成而导致异常。
  3. 覆写equals方法时,应同时覆写hashCode方法:如果两个对象调用equals方法时相等,则这两个对象的hashCode方法,也必须返回相同的值;
  4. 子类覆写父类方法或实现接口时必须加上@Override注解
  5. 应避免public且非final的成员字段定义
    说明:将字段设置为私有(private)的理由是:我们不想其他人依赖这个变量,依赖类内部的实现细节。这样,当内部实现需要变更时,影响面就比较小,变更的成本就比较低。

异常和日志

  1. 方法抛出的异常,应该与本身的抽象层次相对应。
    意思是封装特定异常类型使之与类的API匹配起来?
  2. 日志工具Logger类的实例应声明为private static final或者private final
    1.声明为private是出于访问封装的考虑,防止Logger类的实例对象被其他类非
    法使用
    2.声明为static是为了防止重复new出Logger类的实例,造成资源的浪费,同时
    防止实例被序列化,造成安全风险(精心设计的library除外)
    3.声明为final是因为在类的生命周期内无需变更Logger类的实例

编程实践

多线程并发

  1. Java 8使用CompletableFuture编写异步任务。
    Thinking in parallel,Java 8使用stream做隐式的自动并行化,替代显式的循环。
    对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
    高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体; 能用对象锁,就不要用类锁。避免在锁代码块中调用RPC方法
    应该回收自定义的 ThreadLocal 变量,在try-finally调用其remove()方法,否则可能会造成内存泄露。
    在高并发场景中,避免使用 等于 判断作为中断或退出的条件。容易产生等值判断被“击穿” 的情况,使用大于或小于的区间判断条件来代替。(???)
    在使用阻塞等待获取锁的 lock.lock() 方式中,应该在try 代码块之外,并且在加锁方法与try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
    在使用尝试机制来获取锁的 lock.tryLock() 方式中,进入业务代码块之前,应该先判断当前线程是否持有锁。
    在这里插入图片描述
  2. 是否正确同步的标准是“是否建立了happens-before关系”。
    建议按照以下顺序选取合适的同步机制:
    1.首先考虑使用消息队列(如 BlockingQueue ),或者其他Java标准库中提供的高级同步机制(如executor、future等),需特别留意Java API中对各个类的happens-before关系的描述。(见建议8.1.1)
    2.其次考虑使用锁来保护共享变量。如果情形比较简单,容易保证正确,或者需要用无锁同步提高性能,可以使用volatile变量同步。如果需要原子读改写(atomic read-modify-write)操作,如原子自
    增( getAndAdd )、原子的 compareAndExchange 等,考虑使用java.util.concurrent.atomic 中的原子类(如AtomicInteger ),但不要使用带acquire、release、opaque、plain后缀的方法。
    3.如果尝试了前两条,性能仍然不理想,考虑采用更完善的无锁同步算法。如果性能仍然是问题,考虑采用 java.util.concurrent.atomic.AtomicXxxx 中的弱顺序(weak order)的原子内存访问(acquire、release、opaque、plain)。
  3. 一般来说,如果使用锁,那么读和写都要加锁。著名的双检锁(double-checked locking)模式是一个特例。它在读线程中不加锁,只在写的时候加锁,利用此法在惰性初始化中减少锁的代价,广泛运用于单例模式的实现。在Java中,双检锁需要用volatile配合锁共同实现同步才能正确实现。
  4. 创建新线程时需指定线程名。
    Java多线程程序中,所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉。但是无法避免的是uncheckedexception,也就是RuntimeException,当抛出异常时子线程会结束,但主线程不会知道,因为主线程通过try catch是无法捕获子线程异常的。
    Thread对象提供了setUncaughtExceptionHandler方法用来获取线程中产生的异常。而且还可使用Thread.setDefaultUncaughtExceptionHandler,为所有线程设置默认异常捕获方法。
    程序员应注意的是,在执行周期性任务例如ScheduledExecutorService时,为了健壮性,可考虑在提交的Runnable的run方法内捕获高层级的异常。ScheduledExecutorService的各种schedule方法,可以通过其返回的ScheduledFuture对象获取其异常。
  5. 不要依赖线程调度器、线程优先级和yield()方法
    Java中的线程调度,是基于操作系统以及JVM的实现,在不同的操作系统中,或者不同厂商的JVM(如Oracle、IBM等),即使是同一套代码,其多线程的调度机制也是不一样的。因此,在多线程的程序中,不要依赖于系统的线程调度器来决定程序的逻辑运作,如果程序依赖于线程调度器来达到正确性或者性能要求,会导致不可移植。同理,程序如果依赖Java的线程优先级来确保正确性,也是不可移植的; 而Thread.yield()对线程调度器仅仅是个提示,不保证确定的效果,因此代码也不能依赖Thread.yield()方法。更多细节可以参见[Effective Java 2nd Edition, Item 72]。
  6. 采用Java1.5提供新并发工具代替wait()和notify()
    自从Java1.5发行版本开始,Java平台就提供了更高级的并发工具,它们可以完成以前必须在wait()和notify()上手写代码来完成的各项工作。
    java.util.concurrent更高级的并发工具分成三类:Executor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。更多细节可以参见[Effective Java 2nd Edition, Item 69]。
    性能不是特别苛刻时可以采用synchronized简单可读。
  7. 线程中断由业务代码来协作完成,慎用Thread.interrupt方法
    优先使用协作式的线程同步机制,如j.u.c包中的各种synchronizer,加锁的共享变量、volatile共享变量等,来通知一个线程中止作业。
    慎用Thread.interrupt方法,它使文件句柄被强制关闭,注意此时的文件句柄不是被程序代码调close方法关闭的,此时程序代码是不知道文件句柄被关闭的,如果其他地方继续调用文件的其他方法,就会导致莫名奇妙的IO异常。
    如果需要一个线程让另一个线程中止执行,Java API推荐的方式是,让被中止的线程在运行中周期性地查询自己是否被中止。如果发现自己被中止,则应当主动清理状态并中止执行,而不是忽略请求继续执行。
    可以使用 Thread.interrupt() 方法请求另一个线程中止,线程本身用Thread.interrupted() 方法检查自己是否被中止。注意,不可以使用Thread.currentThread().isInterrupted() 检查自己是否被interrupt,因为isInterrupted() 不会清除一个线程的interrupted status。
    检测到当前线程被interrupt后,应抛出 InterruptedException ,并在finally或trywith-resource中清理执行状态。 InterruptedException 应不断上抛,直至线程中止。
    因此,根据Java语法,能抛此异常的函数要标注 throws InterruptedException 。
    但如果因为实现指定的接口(如线程入口 Runnable.run() )而无法抛出InterruptedException ,则按接口要求特殊处理。记住,调用 Thread.interupt() 的线程希望当前线程尽快停止。
    在编写需要中止的多线程程序时,必须选用能够响应interrupt的标准库或第三方库。
  8. 避免不加控制地创建新线程,而应该使用线程池来管控资源
    线程池不应该使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。
    1) newFixedThreadPool 和 newSingleThreadExecutor : 允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
    2) newCachedThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM 。 newScheduledThreadPool会自动增长工作队列大小。newWorkStealingPool实际的线程数量可能动态地增减。

运算和表达式

  1. 表达式的比较,应当遵循左侧倾向于变化、右侧倾向于不变的原则。
    常量放左边,如 if (MAX == v) 不符合阅读习惯,而 if (MAX > v) 更是难于理解。
    描述区间时,前半段表达式是常量在左的,也是允许的 if (MIN < bar && bar < MAX) ;
  2. 如果必须使用null,而且这个变量有可能是null,应该使用 Objects.equals(variable, “foo”)

控制语句

  1. 不要在控制性条件表达式中执行赋值:在控制性条件表达式中执行赋值,常常导致意料之外的行为。一般不应该出现在上下文中。
    boolean isFoo = false;
    if (isFoo == false) { // … } => if(isFoo)
  2. 不要在foreach循环里进行元素的remove/add 操作,删除元素请使用removeIf方法或Iterator。
// 使用Java 8 Collection中的removeIf方法
list.removeIf(item -> "1".equals(item));

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
	String item = iterator.next();
	if (isRemovable()) {
		iterator.remove();
	}
}

序列化

  1. 序列化对象中的HashMap、HashSet或HashTable等集合不能包含对象自身的引用this。
  2. 尽量不要实现Serializable接口;实现Serializable接口的可序列化类应该显式声明 serialVersionUID。
    serialVersionUID 对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。因此,为保证 serialVersionUID 值跨不同 java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID 值
  3. 不应序列化直接指向系统资源的句柄:建议实现Serializable的类,其字段为File或FileDescriptor时,用transient修饰。
    一个指向系统资源的对象可以被序列化,并且攻击者可以替换掉该对象的序列化形式,那么该对象就具备修改特定句柄指向的系统资源的能力。

泛型

  1. 尽量消除非受检的异常,不应该在整个类上使用SuppressWarning。
    每当使用SuppressWarning注解时,都要添加一条注释,说明为什么这么做是安全的,及其使用的业务场景和范围。
  2. 优先使用泛型集合,而不是数组。数组协变,泛型不可变。数据运行时才知道并检查它们的类型约束;泛型编译时即可知。
  3. 声明一个泛型类通过限定符限制可用的泛型类型。
    PECS:
    1、频繁往外读取内容的,适合用 <? extends T> 。
    2、经常往里插入的,适合用 <? super T>。
  4. 泛型的一些注意事项:
    1、Comparator实现类要满足三个条件(对称的,传递的,恒定的),不然Arrays.sort, Collections.sort 会抛 IllegalArgumentException 异常。
    2、使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。
    3、Map的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
    4、Collections类返回的对象,如 emptyList()/singletonList(),及collection的 of()方法等,都是 immutable的,不可对其进行添加或者删除元素的操作。
    5、ArrayList的 subList 结果 不可强转成 ArrayList,否则会抛出 ClassCastException 异常,高度注意对原集合元素的增删,均会导致subList的遍历、增删产生ConcurrentModificationException 异常。
    6、在使用Collection 接口任何实现类的 addAll()方法时,应确保输入的集合参数c非空。因为其代码直接解引用c.toArray()。

其他语言特性

  1. 不要使用已标注为@deprecated的方法。
  2. Java 8使用Optional代替null作为返回值或者可能的缺失值;禁止对optional对象赋值为null。
    优势:避免空指针异常,减少频繁地嵌套式判空处理,让业务逻辑更直观。
    注意点:
    1、禁止对optional对象赋值/返回为null,或与null比较;
    2、不应该返回 Optional , Optional , Optional ,而用OptionalInt,OptionalLong,OptionalDouble
    3、一般不应该返回 Optional<集合或数组> ,而用空集合或空数组替代
  3. 值类型,推荐使用equals而不是==作比较。

性能与资源管理

  1. 将集合转为数组时使用Collection.toArray(T[])方法且参数是类型相同零长度的数组;JDK 11后使用Collection.toArray(IntFunction<T[]>)。
    优势:不需要创建临时数组,一方面节省空间,另一方面这样就不用去考虑toArray(T[])里的参数长度对函数行为以及结果的影响。
    JDK11中:
List<String> xs = ...;
String[] sa = xs.toArray(String[]::new);

java.util.stream中各Stream的 toArray() 、toArray(IntFunction<A[]>) 也是常用的
JDK11前:

List<String> list = new ArrayList<>(DEFAULT_CAPACITY);
list.add(getElm());
String[] array = list.toArray(new String[0]);

数组容量大小length的影响:
等于 0,动态创建与 size 相同的数组,性能最好
大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担
等于 size,在高并发情况下,数组创建完成之后, size 正在变大的情况下,负面影响与上相同
大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患

  1. 使用System.arraycopy()或Arrays.copyOf()进行数组复制
  2. 初始化集合时,如果已知或可以预测元素数量,则给出初始化大小;不能预测的情况下使用默认大小
    ArrayList(默认10)、StringBuilder(默认16)、StringBuffer(默认16)、HashMap(默认16)、HashSet(默认16)、XxxBlockingQueue(array的要手工指定,linked默认Integer.MAX_VALUE)等等。
  3. 将对象存入HashSet,或作为key存入HashMap(或HashTable)后,必须确保该对象的hashcode值不变,避免因为hashcode值变化导致不能从集合内删除该对象。

可移植性

  1. 不要在代码中硬编码"\r"和"\n"作为换行符号。
    如果需要换行,尽量用PrintStream、PrintWriter的println来代替在字符串中使用硬编码换行符。也可以使用System.lineSeparator()获取运行时环境的换行符。可以使用java.io.File中的separator和pathSeparator静态字段表示名字和路径分隔符。
  2. 字符串大小写转换、数字格式化为西方数字时,必须加上Locale.ROOT或Locale.ENGLISH。
    String类的toUpperCase()和toLowerCase()方法、format()方法,如果不输入参数,则会按当前系统默认的编码模式转换,因此转换结果可能并非如你所预期,如下所示:
    字符对区域不敏感的,例如协议关键字,HTML的tags,优先用ROOT,字符对区域敏感或者强调英文习惯的,可用ENGLISH。
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值