您的位置 首页 java

2022/11 从实践中总结的Java编程经验

序言

优秀的 java 代码不仅是正确,还应该是简洁、可维护、可靠、可测试、高效、可移植的。

目标:风格一致、易阅读、高质量

代码风格

  • 【命名】标识符

标识符由不超过64字符的字母、数字、下划线组成

命名风格:驼峰命名, 如newCustomerId, supports Ipv6 ()

》接口、类、注解、枚举类型:大驼峰,测试类加Test后缀,文件名为顶层类名. Java

》属性、局部变量、方法、方法参数:小驼峰

 String  File Name;
String xmlData;  

静态变量 、枚举值:全大写, 下划线分割

泛型 类型变量: 如E, T, T2, E_IN

》异常:后缀 Exception 或Error, 如AccessException

  • 【命名】包

包名的字母应小写,以点号分隔,如:

 package com.xxx.service1.v2  

  • 【命名】类、枚举、接口

名词、名词短语,形容词、形容词短语, 采用大驼峰,如:

 class MacroPolo {}
interface HisPromotion {}  

  • 【命名】方法

》get + 非布尔属性名(), 如:public String getName()

》is/can/should + 布尔 属性名() ,如:public boolean isFinished()

》set + 属性名(),如:public void setVisible(boolean)

》has + 名词/形容词(),如: boolean hasNext()

》动词,如:public void drive()

》动词 + 宾语() , 如:public void addListeners(Listener)

CallBack 回调方法: 介词+动词,如 onCreate(), toString ()

  • 【命名】常量

》不可修改为其它的值/对象

》对象类型, 对象在初始化完成后其属性不能被修改

应由合大字字母与下划线组成 ,单词间以下划线分隔, 不要使用魔鬼数字。

 public  static  final int MAX_TIME_INTERVAL=360;
public static final String DISCOUNT_RATE_NAME=“half”
public static final int MAX_DESIGN_CAPACITY=3000;
 enum  SIZE {SMALL, MEDIUM, LARGE};  

  • 【命名】变量、方法参数

通常是名词或名词短语,采用小驼峰命名。

有集合意义的,采用复数形式。

 String customeName;
List<String> users = new ArrayList<>(MAX_DESIGN_CAPACITY);
【反例】boolean isNotError; boolean isNotFound;  

布尔型变量以表达是非意义的动词开头, 如: is, has, can , should等,

 boolean isGranted;
boolean hasLicense;
boolean canSwim;
boolean shouldAbort = false;  

  • 【注释】和代码一样重要, 按需注释

Java有3种标记注释的方式:

  1. // 注释单行内容
  2. /* */ 注释连续多行内容
  3. /** */ Javadoc注释,可以使用Javadoc工具生成一个HTML文档

》为每个public, protected 修饰的类、接口、枚举、类方法和属性添加注释, 采用Javadoc注释格式(/** */)

》顶层public类的Javadoc应该包含功能说明和创建日期、版本信息
》方法的Javadoc中包含功能说明 ,根据实际需要按顺序使用@param, @return, @throws标签对参数、返回值、异常进行注释

》不写空有格式的方法头注释

  • 【注释】文件头

》文件头注释中应该包含版权许可信息

》不写空有格式的文件头注释

 /*
*版权所有 (C) XXXX公司 2001~2022
*/  
  • 【注释】代码

》注释与代码之间应该有空行或空格, 注释符与注释内容之间应该有空隔

》正式交付给客户的代码不应该包含TODO/FIXME注释

 public interface InterfaceExample {
    // 类成员变量,与前面的代码之间保留一个空行
    String bField = ...;
}  

  • 【格式】源文件

》源文件编码格式(包括注释)使用UTF-8编码

》一个源文件按顺序包含版权、package、import、顶层类,且用空行分隔

》import包顺序:安卓>本公司>其它商业组织、开源第三方、net/org开源组织>Java

》一个类或接口的声明部分应该按类变量、静态初始化块、实例变量、实例初始化块、构造器、方法的顺序出现,且用空行分隔

  • 【格式】大括号

》在if, else, switch, for, do, while等语句中, 即使程序体是空的或只有一条语句,也应该使用 大括号 。对switch的case, default, 大括号可选。

》对于非空块状结构,左大括号应该放在行尾,右大括号另起一行

》应避免空块,必须使用空块时,采用统一的大括号换行风格 , 例如:

 void doNoghingElse() { //这样统一可以
}  

  • 【缩进】

使用空格进行缩进, 每次缩进4个空格;不允许使用tab、换页符等

  • 【行内容】每行不超过一条语句

》每行只写一条语句

》行宽不超过120个窄字符, 注:1个宽字符占用2个窄字符的宽度

》较长的package可以不换行

》建议换行起点在操作符之前, 例如:

 Student student = Student.builder()
    .setName("Tom")
    .setAge(19)
    .setGrander("F")
    .setMajor("软件工程")
    .build();  

  • 【水平空格】

》用空格突出关键字和重要信息,必须加空格的场景:

(包括复合)赋值运算符前后, 如=, *=

逗号 、非for-in的 冒号 、for循环等分隔的;符号之后加空格

二元运算符、类型并交的|和&符号、for-in的冒号的前后两侧,例如 base + offset;

Lambda表达式 中的箭头前后,如 str -> str.length();

方法声明、条件判断语句、循环语句等场景下的)与{之间加空格 , 如 void func() { …}

》不应插入多余空格使代码垂直对齐, 例如:

  private  int       size;    //不需要在变量名之前加空格使得与下一行对齐
private  String  name; //不必与上行对齐注释  

  • 【枚举】

》枚举常量间以逗号,换行可选。例如:

 private enum Encoding {
     UTF8  {
        @Override
        public String toString() {
            return " UTF -8";
        }
    },
    UTF16,
    US_ASCII
}  

》枚举的使用场景:

  1. 布尔型的两元素值,如: public enum TemperatureScale { CELSIUS, FAHRENHEIT } //表示温度用摄氏|华氏
  2. 变量值仅在一下固定范围内变化用枚举来定义
  3. 整数或 字符串 的枚举模式,蕴含有某种命名空间的,如:
 public enum ComparisonResult { ORDERED_BY_ASCENDING, ORDERED_BY_SAME, ORDERED_BY_DESCENDING }  

  • 【switch语句】

》当switch括号内的变量类型为String时,确保变量非空

》case语句块结束时,如果不加break,需要有注释说明(fall-through)

 switch (addresslabel) {
    case 0:
    case 1:
        system.out.println("11111");
        //$fall-through$
    case 2:
        system.out.println("22222");
        //$fall-through$
    case 3:
        system.out.println("33333");
        break;
    default:
        system.out.println("default!");
}  

  • 【注解】

》应用于类、方法、类属性的每个注解独占一行,例如:

 @Override
public int  hashcode () {
    ......
}  
 @Partial
@Mock
DataLoader dataloader;  

  • 【修饰符】

》类和成员修饰符(如果有),按Java语言建议的顺序显示:

public>protected>private>abstraact>default>static>final>transient>volatile>synchronized>native>strictfp

》对于long/float/double类型的数字使用后缀指定数值的类型,例如:

 long sum = 0L;
var isReady = true;
float flt=1.0f;
var dbl = 3.14d;  

编程实践

  • 【声明和初始化】

》每行只声明一个变量

局部变量 被声明在接近它们首次使用的行

 boolean is lock ed = lock.tryLock(); //isLocked变量在使用时进行声明,并赋初始值  

》禁止C风格的数组声明, 应该如数据元素类型紧跟中括号[]组成 ,例如:

 【应该】String[ ] nonEmptyArray = {"GREEN", "RED", "BLUE" };
【不应该】String nonEmptyArray[ ] = {"GREEN", "RED", "BLUE" };  

》禁止将mutable对象定义为常量, 使用public static final的意图是定义一个常量,如果用它修饰一个mutable(可变)对象,易造成功能异常。

 【反例】public static final List<String> EMPTY_RESULT_LIST = new ArrayList<>(); // 这个List集合是mutable, 不应该定义为常量  

  • 【数据类型】

》进行数值运算时, 避免整数溢出

 【反例】return num1 * num2; //当两个乘数的值较大,乘积大于 Integer .MAX_VALUE时会产生溢出
【正例】return Math.multiplyExact(num1, num2);  

》确保除法运算、模运算中的除数不为0

 if (divisorNum != 0) {
    long result1 = dividendNum / divisorNum ;
}  

》禁止使用 浮点数 作为循环计数器,由于浮点数存在精度问题,会导致非预期的结果

 【反例】for (float flt = (float) 200000000; flt < 20000000005; flt++) { ... }  

》需要精确计算时使用 BigDecimal , 不要使用float和double ,例如:

 BigDecimal income = new BigDecimal("1.03");  

》字符串,不要在代码中硬编码用于表示换行、文件路径分隔的字符

 【正例】String filePath = path + File.separator + "temp.txt"  

》字符串大小写转换、数字格式化为西方数字时,必须加上Locale.ROOT或Locale.ENGLISH

 【正例】String testString2 = String.format(Locale.ROOT, "%d", 2);  

》字符与字节的相互转换操作,要指明正确的编码方式

 【正例】String result = new String(buf, StandardCharsets.UTF_8); //可跨平台转换字符  

》明确地进行类型转换,避免依赖隐式类型转换

》在引用类型向下转换前,用 instanceof 进行判断

  • 【表达式】

》不要在单个表达式中对相同的变量赋值超过一次

》用括号明确表达式的操作顺序, 避免过分依赖默认优先级

》表达式的比较, 遵循左侧倾向于变化, 右侧倾向于不变的原则

 【正例】Objects.equals(var1, CONST_COMPANY_NAME)
【正例】if (obj != NULL) { ... }  

》代码中不应使用断言assert

  • 【控制语句】

》不要在控制性表达式中执行赋值操作,或执行复杂的条件判断

 【反例】 if (isFoo = false) { ... } //在条件判断中赋值,不易理解
【例外】 while ((line = reader.readLine()) != null ) { ... }
  

》含 else if分支的条件判断应在最后加一个else分支

》禁止使用空的无限循环, 如:

 while (true) { //do nothing }  

  • 【方法】

方法是可组合、可重用的代码最小单位,高内聚低耦合的设计,把代码有效组织起来。

》不要使用已标注为@Deprecated的方法、类、类的属性等,因为它是因各种原因被废弃,为了保持兼容性而没有删除。

》不应把方法的参数当成临时变量

》谨慎使用可变数量参数

》对于返回数值或容器的方法, 应返回长度为0的数组或容器,代替返回null

  • 【类】

》可使用Lambda表达式或方法引用代替匿名类

》设计类时,要为类及成员设置最小的可访问性

》应避免定义public 且非final的类属性

》不要在父类的 构造方法 中调用可能被子类覆盖的方法

》构造方法如果有多个, 尽量重用

》避免基本类型与期包装类型的同名重载方法

》覆写equals方法时, 要同时覆写hashcode方法

》子类覆写父类方法或实现接口时,必须加上@Override注解

》使用类名调用 静态方法 ,不要使用实例或表达式来调用

  • 【接口与面向对象编程】

》接口定义中去掉多余的修饰词

》可在接口中加上静态方法表示相关的工厂或助手方法

  • 【异常处理】

》不要通过一个空的catch块忽略异常

》不要直接捕获异常的基类 Throwable, Exception, RuntimeException

》不要直接捕获可通过预检查进行处理的RuntimeException, 如:NullPointerException, IndexOutOfBoundsException等

》方法抛出的异常,应该与本身的抽象层次相对应

【反例】

 public class Employer {
    ...
    public TaxId getTaxId() {
        ...
        throw new RuntimeException(); //把更底层的异常返回给了调用方,使调用方法与底层耦合起来。 应该改成抛出EmployerDataNotAvailable异常,抽象层次与方法一致
    }
    ...
}  

》在catch块中抛出新异常时, 避免丢失原始异常信息
》一个方法不应该抛出超过5个异常,并在Javadoc的@throws标签中记录每个抛出的异常及条件

》不要使用return, break, continue或抛出异常使得finally块非正常结束

》不要使用System.exit()终止 JVM

  • 【并发与 多线程

》优先使用Java标准库提供的高级同步机制在多线程中共享数据

》针对 线程 安全性,需要进行文档 Javadoc说明

》可以使用CompletableFuture编写异步任务

》优先使用不可变对象在多线程间传递数据

》避免数据竞争 Data Race

从Java 5 开始,Java Memory Model采用happens-before关系,来规定并发执行中,读写操作允许读到什么值,不允许读到什么值。

两个线程分别对一个非volatile的共享变量进行访问操作,其中至少一个操作是写操作,且这两个操作之间没有happens-before关系,就是Data Race,通常必须避免 Data Race, 正确同步的执行是指没有Data Race的执行。

【正解】在下例中演示使用volatile变量同步。volatile变量的写操作(2)和读操作(3)之间建立了happens-before关系,保证读操作(4)必须看到写操作(1)写入的值42,而不是初始值0。

 class Foo {
    public volatile int nr;   //(0)赋初值0
}
private volatile Foo sharedFoo;    //定义volatile共享变量

//-------------------生产者:写共享变量--------------------------------
public void publisher() {
    ...
    Foo newFoo =  new Foo();
    newFoo.nr = 42;   //(1)写操作:值写为42
    sharedFoo = newFoo;   //  (2) volatile变量的写操作, 写入新对象的引用
    ...
}

//-------------------使用者:读共享变量--------------------------------
public void consumer() {
   ...
   Foo myFoo;
   do {
         myFoo =  sharedFoo;   //(3)volatile变量的读操作, 读出共享变量中的值
   } while (myFoo == null);     //  不等于null 就证明操作(2)happens-before 操作(3)
  System.out.println(myFoo.nr);    //(4)读操作,(2)happens-before(3),得到返回值为42
  ...
}
    

除了volatile变量以外,还可以使用锁同步或者使用 Thread .join()同步,都可以建立happens-before关系。
一般来说,如果使用锁,那么读和写都要加锁,而不是写线程需要加锁,而读的线程可以不加锁。

【反例】在下例中演示了不正确的用锁。只有写线程加了锁,读线程没有加锁,导致写操作(1)和读操作(6)之间没有happens-before关系,造成Data Race,在操作(6)中有可能读到data的初始值0。

 int data;                   //(0)写初值0
boolean flag;           //        初值false
Object lock;

//-------------------生产者:写共享变量--------------------------------
void thread1() {
    data = 42;                     //(1)写共享变量的值为42
     synchronized (lock) {     //(2)加锁
        flag = true;               //(3) 写flag
    }                                    //(4) 解锁

//-------------------使用者:读共享变量--------------------------------
void thread2() {
    boolean myFlag;
    do {
        myFlag =  flag;        //(5)读锁状态 flag
    } while (!myFlag); 
    int myData =  data;     //(6)发现解锁,读共享变量的值data
    System.out.println(myData);    //输出可能不是42
}  

》使用相同的顺序请求和释放锁来避免 死锁

》对共享变量做同步访问控制时需避开同步陷阱, 如基于高级并发对象的synchronized块,使用实例锁来同步静态共享变量,使用可被重用的对象锁,使用class类对象锁

》在异常条件下,保证释放已持有的锁

》避免在持有锁时执行耗时或阻塞性的操作

》避免使用不正确形式的双重检查锁

》禁止使用非线程安全的方法来覆写线程安全的方法

》使用新并发工具代替wait() , notify()

》创建新线程时必须指定线程名

》使用Thread对象的setUncaughtExceptionHandler方法注册未捕获异常处理者

》不要依赖线程调度器,线程优先级和 yield ()方法

》线程中断由业务代码来协助完成,慎用Thread.interrupt方法

》禁止使用Thread.stop()来终止线程

》避免不加控制地创建新线程,应该使用 线程池 来管控资源

》线程池中的任务结束后必须清理其自定义的 ThreadLocal 变量

文章来源:智云一二三科技

文章标题:2022/11 从实践中总结的Java编程经验

文章地址:https://www.zhihuclub.com/195123.shtml

关于作者: 智云科技

热门文章

网站地图