一、目的
本java代碼規范編寫主要是為了給開發人員編碼時提供一份參考文檔,在協作開發及多項目切換開發中,有規范可循,實現代碼高質量、高可讀以及可維護。本文結合阿里java代碼規范、項目實際情況以及個人的開發經驗編寫而成。本java開發規范的預期讀者為系統設計人員、軟件開發人員、技術經理。
二、適用范圍
適用於軟件開發人員閱讀。
三、術語表
序號 |
術語或縮略語 |
說明性定義 |
1 |
魔法值 |
是指在代碼中直接出現的數值,只有在這個數值記述的那部分代碼中才能明確了解其含義 |
2 |
POJO類 |
簡單的Java對象,實際就是普通JavaBeans,僅有一些屬性及其getter setter方法的類,沒有業務邏輯 |
3 |
事務 |
事務是指一個單元的工作,這些工作要么全做,要么全部不做。事務是必須滿足4個條件(ACID)::原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。 |
4 |
基本數據類型 |
基本數據類型包括:byte、short、int、long、float、double |
5 |
包裝數據類型 |
包裝數據類型包括:Byte、Short、Integer、Long、Float、Double |
6 |
方法 |
用來解決一類問題的代碼的有序組合,是一個功能模塊 |
四、代碼規范
4.1、強制遵循
1. 禁用魔法值
除預定義的外,在編碼中禁止使用魔術值。建議定義枚舉值或者靜態常量值,並寫好注釋說明。
反例:
if (key.equals("Id#taobao_1")) { //... }
正例:
String KEY_PRE = "Id#taobao_1"; if (KEY_PRE.equals(key)) { //... }
2. POJO類必須寫toString方法
在方法執行拋出異常時,可以直接調用POJO的toString()方法打印其屬性值,便於排查問題。說明:使用工具類source> generate toString時,如果繼承了另一個POJO類,注意在前面加一下super.toString。
正例:
public class ToStringDemo extends Super{ private String secondName; @Override public String toString() { return super.toString() + "ToStringDemo{" + "secondName='" + secondName + '\'' + '}'; } } class Super { private String firstName; @Override public String toString() { return "Super{" + "firstName=" + firstName + '\'' + '}'; } }
3. 手動回滾事務
事務場景中,拋出異常被catch后,如果需要回滾,一定要手動回滾事務。
反例:
@Transactional public class UserServiceImpl implements UserService { @Override public void save(User user) { //some code //db operation } }
正例1:
@Transactional(rollbackFor = Exception.class) public class UserServiceImpl implements UserService { @Override public void save(User user) { //some code //db operation } }
正例2:
public class UserServiceImpl implements UserService { @Override @Transactional(rollbackFor = Exception.class) public void save(User user) { //some code //db operation } }
正例3:
public class UserServiceImpl implements UserService { @Autowired private DataSourceTransactionManager transactionManager; @Override @Transactional public void save(User user) { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can only be done programmatically def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); try { // execute your business logic here //db operation } catch (Exception ex) { transactionManager.rollback(status); throw ex; } } }
4. 單個方法的總行數不超過80行
除注釋之外的方法簽名、結束右大括號、方法內代碼、空行、回車及任何不可見字符的總行數不超過80行。代碼邏輯分清紅花和綠葉,個性和共性,綠葉邏輯單獨出來成為額外方法,使主干代碼更加清晰;共性邏輯抽取成為共性方法,便於復用和維護。
5. 及時清理不再使用的代碼段或配置信息
對於垃圾代碼或過時配置,堅決清理干凈,避免程序過度臃腫,代碼冗余。如果是暫時注釋,請在上方使用 /// 標記說明。
正例:
public static void hello() { /// 暫時注釋. // Business business = new Business(); // business.active(); System.out.println("it's finished"); }
6. 集合初始化時,指定集合初始值大小
HashMap使用如下構造方法進行初始化,如果暫時無法確定集合大小,那么指定默認值(16)即可。
反例:
Map<String, String> map = new HashMap<String, String>();
正例:
Map<String, String> map = new HashMap<String, String>(16);
7. 內部的實現類用Impl的后綴與接口區別
對於Service和DAO類,基於SOA的理念,暴露出來的服務一定是接口,內部的實現類用Impl的后綴與接口區別。
正例:
public interface DemoService{ void f(); } public class DemoServiceImpl implements DemoService { @Override public void f(){ System.out.println("hello world"); } }
8. 定義DO/DTO/VO等POJO類時,不要加任何屬性默認值
POJO類屬性沒有初值是提醒使用者在需要使用時,必須自己顯式地進行賦值,任何NPE問題,或者入庫檢查,都由使用者來保證。
反例:
POJO類的createTime默認值為newDate(),但是這個屬性在數據提取時並沒有置入具體值,在更新其它字段時又附帶更新了此字段,導致創建時間被修改成當前時間。
public class DemoDO { String name = "demo"; Date createTime = new Date(); }
正例:
public class DemoDO { String name; Date createTime; }
9. 循環體內,字符串的聯接方式
循環體內,字符串的聯接方式,使用StringBuilder的append方法進行擴展。 說明:反編譯出的字節碼文件顯示每次循環都會new出一個StringBuilder對象,然后進行append操作,最后通過toString方法返回String對象,造成內存資源浪費。
反例1:
String result; for (String string : tagNameList) { result = result + string; }
反例2:
StringBuilder stringBuilder = new StringBuilder(); for (String string : tagNameList) { stringBuilder.append(string + ","); } String result = stringBuilder.toString();
正例:
StringBuilder stringBuilder = new StringBuilder(); for (String string : tagNameList) { stringBuilder.append(string); stringBuilder.append(","); } String result = stringBuilder.toString();
10. 不能在finally塊中使用return
不能在finally塊中使用return,finally塊中的return返回后方法結束執行,不會再執行try塊中的return語句。
反例:
public static Long readFileLength(String fileName) { try { File file = new File(fileName); RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); return randomAccessFile.length(); } catch (Exception e) { logger.error(e.getMessage(), e); } finally { countDownLatch.countDown(); return 0L; } }
11. switch塊使用
在一個switch塊內,每個case要么通過break/return等來終止,要么注釋說明程序將繼續執行到哪一個case為止;在一個switch塊內,都必須包含一個default語句並且放在最后,即使它什么代碼也沒有。
正例:
switch (x) { case 1: break; case 2: break; default: }
12. SimpleDateFormat 是線程不安全的類
SimpleDateFormat 是線程不安全的類,一般不要定義為static變量,如果定義為static,必須加鎖,或者使用DateUtils工具類。 說明:如果是JDK8的應用,可以使用LocalDate代替Date,LocalDateTime代替Calendar,DateTimeFormatter代替SimpleDateFormat,官方給出的解釋:simple beautiful strong immutable thread-safe。
正例1:
private static final String FORMAT = "yyyy-MM-dd HH:mm:ss"; public String getFormat(Date date){ SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT); return sdf.format(date); }
正例2:
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public void getFormat(){ synchronized (sdf){ sdf.format(new Date()); ….; }
正例3:
private static final ThreadLocal<DateFormat> DATE_FORMATTER = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }
13. 數據類型相等比較方式,equals 和 == 的選擇標准
1) 比較的兩個對象中其中一個為基本數據類型時使用==。
2) 比較的兩個對象中兩個都為包裝數據類型時使用equals 。
例子:
Byte c = (byte)1; Byte d = (byte)1; Integer a = 200; Integer b = 200; Integer e = 10; Integer f = 10; System.out.println(c.equals(d)); // ture System.out.println(c.equals(1)); // false System.out.println(c == 1); // true System.out.println(a.equals(b)); //true System.out.println(a == b); //false 因為byte、Integer long short在-128-127范圍是共享的堆,超出就會新建一個對象 System.out.println(e.equals(f));//true System.out.println(e == f); //true
14. 基本數據類型與包裝數據類型的使用標准
關於基本數據類型與包裝數據類型的使用標准如下:
1) 所有的POJO類屬性必須使用包裝數據類型。
2) RPC方法的返回值和參數必須使用包裝數據類型。
3) 所有的局部變量推薦使用基本數據類型。
反例:
返回類型為基本數據類型,return包裝數據類型的對象時,自動拆箱有可能產生NPE
public int demo(){ Integer result = null; return result; }
15. 包命名標准
包名統一使用小寫,點分隔符之間有且僅有一個自然語義的英語單詞。包名統一使用單數形式,但是類名如果有復數含義,類名可以使用復數形式。
反例:
cn.gov.shunde.detentionhouse.alarmModle
正例:
cn.gov.shunde.detentionhouse.alarm.modle
16. 常量命名標准
常量命名應該全部大寫,單詞間用下划線隔開,力求語義表達完整清楚,不要嫌名字長。
正例:
public class ConstantNameDemo { /** * max stock count */ public static final Long MAX_STOCK_COUNT = 50000L; }
17. 抽象類命名使用Abstract或Base開頭
正例:
abstract class BaseControllerDemo{ } abstract class AbstractActionDemo{ }
18. 類名使用UpperCamelCase風格
類名使用UpperCamelCase風格,必須遵從駝峰形式,但以下情形例外:(領域模型的相關命名)DO / BO / DTO / VO / DAO。
19. 方法名、參數名、成員變量、局部變量使用lowerCamelCase風格
方法名、參數名、成員變量、局部變量都統一使用lowerCamelCase,必須遵從駝峰形式。
20. 杜絕完全不規范的縮寫
1)杜絕完全不規范的縮寫,避免望文不知義。
2)為了達到代碼自解釋的目標,任何自定義編程元素在命名時,使用盡量完整的單詞組合來表達。
3)類名應該是個名詞或名詞詞組的名字,如Customer、WikiPage、 Account、AddressParser等。
4)變量名應該是名詞,如stockNumber 、username等。
5)方法名應該是一個動詞或動詞詞組,如postPayment()、deletePage()、save()等。
反例:
// 姓名 String xm = "xiaomin"; // 編號 String num = "A001";
正例:
// 姓名 String username = "xiaomin"; // 編號 String number = "A001";
21. 所有的抽象方法(包括接口中的方法)必須要用javadoc注釋
所有的抽象方法(包括接口中的方法)必須要用javadoc注釋、除了返回值、參數、異常說明外,還必須指出該方法做什么事情,實現什么功能。 說明:如有實現和調用注意事項,請一並說明。
正例:
/** * fetch data by rule id * * @param ruleId rule id * @param page page number * @param jsonContext json format context * @return Result<XxxxDO> */ Result<XxxxDO> fetchDataByRuleId(Long ruleId, Integer page, String jsonContext);
22. 所有的類都必須添加創建者信息
所有的類都必須添加創建者信息。 說明:在設置模板時,注意IDEA的@author為${USER},而eclipse的@author為${user},大小寫有區別,而日期的設置統一為yyyy/MM/dd的格式。
正例:
/** * Demo class * * @author keriezhang * @date 2016/10/31 */ public class CodeNoteDemo { }
23. 方法內注釋
方法內部單行注釋,在被注釋語句上方另起一行,使用//注釋。方法內部多行注釋使用/* */注釋。注意與代碼對齊。
正例:
public void method() { // Put single line comment above code. (Note: align '//' comment with code) int a = 3; /** * Some description about follow code. (Note: align '/**' comment with code) */ int b = 4; }
24. 枚舉類型字段必須要有注釋
所有的枚舉類型字段必須要有注釋,說明每個數據項的用途。
正例:
public enum TestEnum { /** * agree */ agree("agree"), /** * reject */ reject("reject"); private String action; TestEnum(String action) { this.action = action; } public String getAction() { return action; } }
25. 所有的覆寫方法,必須加@Override注解
getObject()與get0bject()的問題。一個是字母的O,一個是數字的0,加@Override可以准確判斷是否覆蓋成功。另外,還有一個好處如果在抽象類中對方法簽名進行修改,其實現類會馬上編譯報錯。
26. Map/Set的key為自定義對象時,必須重寫hashCode和equals
關於hashCode和equals的處理,遵循如下規則:
只要重寫equals,就必須重寫hashCode。
因為Set存儲的是不重復的對象,依據hashCode和equals進行判斷,所以Set存儲的對象必須重寫這兩個方法。
如果自定義對象做為Map的鍵,那么必須重寫hashCode和equals。
4.2、建議遵循
1.各層命名規約
Service/DAO層方法命名規約
1) 獲取單個對象的方法用get做前綴, 如:getUser、getUserByName等。
2) 獲取多個對象的方法用list做前綴,復數結尾,如:listUsers、listUsersByIds、pageUsersByIds等。
3) 獲取統計值的方法用count做前綴。
4) 插入的方法用save/insert做前綴。
5) 刪除的方法用remove/delete做前綴。
6) 修改的方法用update做前綴。
正例:
User getUserById(Long id); List<User> listUsersByIds(Set<Long> ids); void addUser(User user); void updateUsername(Long id,String username); void removeUser(Long id);
領域模型命名規約
1) 數據對象:xxxDO,xxx即為數據表名。
2) 數據傳輸對象:xxxDTO,xxx為業務領域相關的名稱。
3) 展示對象:xxxVO,xxx一般為接口相關名稱。
4) 入參對象:xxxPO,xxx一般為接口相關名稱。
5) 業務對象:xxxBO,xxx一般為業務相關名稱。
6) POJO是DO/DTO/BO/VO的統稱,禁止命名成xxxPOJO。
2. 重復代碼提煉
建議:
1)把重復的代碼封裝成一個函數,讓需要的地方調用這個函數即可;
2)如果重復的代碼是出現在“互為兄弟類”中,也就是這兩個類有共同的父類,可以把相同的代碼提到父類中;
3)如果代碼只是類似而不是完全相同,可以把相同的部分和相異的部分提煉出來,設計成模板模式;
4)如果兩個互不相干的類出現重復的代碼,可以把相同的部分,提到一個父類中或者提取到一個接口中;
5)重復代碼的類只屬於某一個函數,而且讓另外的類可以直接調用這個類中函數。
3. 發散式變化
問題:如果增加一個新功能,需要對原有功能做出大量的修改。這就是散發式變化的征兆。
處理:如果發生變化的兩個方向自然地形成了先后次序(比如說,先從數據庫取出數據,再對其進行金融邏輯處理),就可以用拆分階段將兩者分開,兩者之間通過一個清晰的數據結構進行溝通。不要讓一段代碼同時處理不同的事情 。
4. 主從關系明確的類應使用內部類
當一個對象A是另一個對象B的屬性時,並且對象A只會被對象B引用,此時應該將對象A定義為對象B的內部類,有利於避免對象A被其它對象引用或者被修改的風險。
正例:
public class FaceDetailVO { private Long faceId; private List<TogetherPersonInfo> togetherPersons; public static class TogetherPersonInfo{ private String name; private String idCard; } }
5. 方法參數列表應該使用內部基本類型還是自定義DTO
定義方法時,參數較多,會讓代碼變得混亂。但使用對象對參數進行封裝,可讀性又差,而且無法保證必要參數都有賦值。從而建議使用以下的判斷依據:
1)方法參數超過4個,可以通過創建DTO對象對方法入參進行封裝。
2)如果方法參數不超過4個,可以直接作為入參。
例子:
@Data public class CheckPasswordDTO { private String username; private String password; } // 不建議 Boolean checkPassword(CheckPasswordDTO checkPasswordDTO ); // 建議,更直觀 Boolean checkPassword(String username,String password);
6. 方法參數及方法返回值不建議使用Map
Map是一種鍵值對集合,使用Map作為方法參數及方法返回值,會讓調用者無法直觀地了解Map里到底是什么數據,並且容易造成調用者使用不當問題。在迫不得已,不得不使用Map的情況下,需要進行詳細的對鍵值對進行描述。
反例:
Map<Long, User> userMap(){ // key 為用戶id HashMap<Long, User> userMap = new HashMap<>(); // do something return userMap; }
正例:
List<User> listUsers(){ ArrayList<User> users = new ArrayList<>(); // do something return users; }
7. 業務方法中,盡可能使用拋出異常來代替if-else
在編輯業務方法時,要盡量突出主業務邏輯,故針對一些必要條件的判斷,應該在不滿足的情況下直接拋出異常,而不是在代碼中編碼大量if-else嵌套。
反例:
public Boolean login(User user){ if (user != null) { if ( user.getUsername() != null && user.getUsername() != "") { if (user.getPassword() != null && user.getPassword() != "") { if (user.getStatus() != null && user.getStatus() != 0) { // do something return true; } } } } return false; }
正例:
public void login(User user){ if ( user == null) { throw BusinessException.operate("用戶對象不能為空"); } if ( user.getUsername() == null || user.getUsername() == "" ) { throw BusinessException.operate("用戶名不能為空"); } if ( user.getPassword() == null || user.getPassword() == "") { throw BusinessException.operate("密碼不能為空"); } if ( user.getStatus() == null || user.getStatus() == 0) { throw BusinessException.operate("用戶已注銷"); } // do something }
8. 循環體內,不要操作數據庫
建議:
查詢時將數據批量查詢出來,得到List集合;
根據List集合得到相關外鍵集合,根據外鍵集合查詢相當記錄,並轉為Map;
后面通過根據相應的外鍵從Map中取出相應的對象;
正例:
List<Supervisor> records = iPage.getRecords(); Set<Long> identityIds = records.stream().map(Supervisor::getIdentityId).collect(Collectors.toSet()); Map<Long, Identity> identityMap = identityService.listByIds(identityIds).stream().collect(Collectors.toMap(Identity::getId, Function.identity())); ArrayList<SupervisorVO> supervisorVOs = new ArrayList<>(); for (Supervisor record : records) { Identity identity = identityMap.get(record.getIdentityId()); SupervisorVO supervisorVO = SupervisorVO.builder() .gender(identity != null ? identity.getGender() : (byte)0) .id(record.getId()) .idCard(record.getIdCard()) .name(record.getName()) .build(); supervisorVOs.add(supervisorVO); }
9. 領域模型使用區域
建議:
1)數據對象:xxxDO,xxx即為數據表名,建議只在dao層及service層出現。
2)數據傳輸對象:xxxDTO,xxx為業務領域相關的名稱,可以穿梭在各層中。
3)展示對象:xxxVO,xxx一般為接口相關名稱,建議只在controller層及service層出現,並且只作為接口或方法返回對象。
4)入參對象:xxxPO,xxx一般為接口相關名稱,建議只在controller層及service層出現,並且只作為接口或方法參數對象。
5)業務對象:xxxBO,xxx一般為業務相關名稱,根據業務需求,可以穿梭在各層中。
五、參考資料
5.1、javadoc標簽使用
1. 常用標簽
標簽 |
說明 |
@author |
作者標識 |
@version |
版本號 |
@return |
對函數返回值的描述 |
@deprecated |
標識過期API(為了保證兼容性,仍可用,但不推薦用) |
@throws |
構造函數或方法會拋出的異常 |
@exception |
同@throws |
@see |
引用,查看相關的內容,如類,方法,變量等,必須頂頭寫 |
{@link 包.類#成員} |
引用,同@see,但可寫在任意位置 |
{@value} |
對常量注釋,如果其值包含在文檔中,通過改標簽引用常量的值 |
{@code}} |
{@code text}將文本標記為code,會被解析成 text } ,在Javadoc成只要涉及到類名或者方法名,都需要使用@code進行標記 |
@param |
說明方法的參數 |
@inheritDoc |
用於繼承父類中的Javadoc,父類的文檔注釋,被繼承到了子類 |
在vm中要加:
錯誤“編碼 GBK 的不可映射字符”
-encoding utf-8 -charset utf-8
錯誤:未知標記:date
-tag date:a:"日期:"
2. 方法上的文檔標注
寫在方法上的文檔標注一般分為三段:
第一段:概要描述,通常用一句或者一段話簡要描述該方法的作用,以英文句號作為結束;
第二段:<p>詳細描述,通常用一段或者多段話來詳細描述該方法的作用,一般每段話都以英文句號作為結束,段中分行使用<br>;
第三段:文檔標注,用於標注參數、返回值、異常、參閱等;
方法必要的標簽
/**
*概述.
*
*<p>描述.
*
*@param
*@return
*@author
*@since
*/
例子:
/** * 添加緩存. * * <p>支持將已實現Serializable的類存入緩存中. * * @author wangquanqing * @since 2020/12/15 17:03 * @param key 唯一標識 * @param value 數據體 * @param timeout 過期時長,單位秒 * @return void */ <T extends Serializable> void put(String key, T value, Integer timeout);
3. 類上的文檔標
寫在類上的文檔標注一般分為三段:
第一段:概要描述,通常用一句或者一段話簡要描述該類的作用,以英文句號作為結束
第二段:<p>詳細描述,通常用一段或者多段話來詳細描述該類的作用,一般每段話都以英文句號作為結束,段中分行使用<br>
第三段:文檔標注,用於標注作者、創建時間、參閱類等信息
類必要的標簽
/**
*概述.
*
*<p>描述.
*
*@author
*@since
*/
例子:
/** * 緩存服務接口. * * <p>緩存服務接口. * * @author wangquanqing * @since 2020/12/15 17:12 */ public interface CacheService { }
4.idea類注釋模板配置
路徑:File>Settings>Editor>File and Code Templates
在File Header文檔中添加:
/**
* @author ${USER}
* @since ${DATE} ${TIME}
*/
5.idea方法注釋模板配置
路徑:File>Settings>Editor>Live Templates
點擊右上角+,新增Template Group,例如myGroup
選中新增的myGroup,點擊右上角+,新Live Template,命名為*
模板內容:
*
* $summary$.
*
* <p>$describe$.
*
* @author $user$
* @since $date$ $time$
$params$
* @return $returns$
*/
點擊Edit variables,按下圖選擇表達式
params中的Default value為:
groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+='* @param ' + params[i] + ((i < params.size() - 1) ? '\\n ' : '')}; return result", methodParameters())
使用方法:
在方法名上敲寫
/**
然后按"Tab"鍵