不知道各位在項目開發過程中有沒有過這種體會,接手上一任的代碼,看到代碼的那一刻,有一種想要砸電腦的沖動,一個方法體內寫了無數行代碼,到處皆可看到復制粘貼的代碼,變量命名也讓人看不懂。
各位在編碼時,是否有想過,我如何才能寫出高質量的代碼,寫出優雅的代碼,寫出高度可擴展的代碼。我相信大家都希望能寫出那樣的代碼,讓人佩服的五體投地,可不知道如何寫,那么本文就是為幫助大家提高編碼質量而生的。
我相信,大家在看完本文后,一定會有所領悟。下面,我們就進入主題。
使用 lombok 簡化代碼
在介紹 lombok 之前,我們先來看一段代碼:
public class Person {
private Long id;
private String name;
private Integer age;
private Integer sex;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
}
這段代碼大家應該都很熟悉,我們在開發 JAVAWeb 項目時,定義一個 Bean,會先寫好屬性,然后設置 getter/setter 方法,這段代碼本身沒有任何問題,也必須這樣寫。
但是每個 bean 都需要些 getter/setter,這樣寫的話就不夠優雅了,這段代碼我們如何優雅的寫呢?接下來就輪到強大的 lombok 出場了。
lombok 是一個可以通過注解的形式來簡化我們的代碼的一個插件,要使用它,我們應該先安裝插件,安裝步驟如下:
1.
打開 IDEA 的 Plugins,點擊 Browse repositories。
2.
搜索 lombok,并安裝它,安裝好后重啟 IDEA。
3.
打開 Settings,找到 Annotaion Processors,將 Enable annotaion processing 勾選上。
4.單純這樣還不夠,我們要用到 lombok 的注解還需要添加其依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
<scope>provided</scope>
</dependency>
接下來,我們改造 Person 類:
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Person {
private Long id;
private String name;
private Integer age;
private Integer sex;
}
我們可以看到,在類上加入了 Getter 和 Setter 兩個注解,將之前寫的 getter/setter 方法干掉了,這種代碼看著清爽多了,寫個 main 方法來測試下:
public static void main(String[] args) {
Person person = new Person();
person.setName("lynn");
person.setId(1L);
System.out.println(person.getName());
}
我們并沒有寫任何 setter/getter 方法,只是加了兩個注解就可以調用了,這是為什么呢?這是因為 lombok 提供的 Getter 和 Setter 注解是編譯時注解,也就是在編譯時,lombok 會自動為我們添加 getter/setter 方法,因此我們不需要顯式地去寫 getter/setter 方法而可以直接調用,這樣的代碼是不是看著非常優雅。
當然,lombok 的功能不止于此,它提供了很多注解以簡化我們的代碼,下面,我將分別介紹它的其他常用注解:
@Accessors
該注解的作用是是否開啟鏈式調用,比如我們開啟鏈式調用:
@Getter
@Setter
//chain設置為true表示開啟鏈式調用
@Accessors(chain = true)
public class Person {
private Long id;
private String name;
private Integer age;
private Integer sex;
public static void main(String[] args) {
Person person = new Person();
person.setName("lynn").setId(1L);
}
}
@Builder
構建者模式,我們在使用第三方框架時經常能看到 Builder 模式,比如 HttpClient 的:
RequestConfig config = RequestConfig.custom()
.setConnectionRequestTimeout(timeout)
.setConnectTimeout(timeout)
.setSocketTimeout(timeout)
.build();
那么,通過 Builder 注解可以很方便的實現它:
@Getter
@Setter
@Builder
public class Person {
private Long id;
private String name;
private Integer age;
private Integer sex;
public static void main(String[] args) {
Person person = new PersonBuilder()
.name("lynn")
.id(1L)
.build();
}
}
@Data
編譯時添加 getter、setter、toString、equals 和 hashCode 方法,如:
@Data
public class Person {
private Long id;
private String name;
private Integer age;
private Integer sex;
public static void main(String[] args) {
Person person = new Person();
person.setName("lynn");
System.out.println(person);
}
}
@Cleanup
添加輸入輸出流 close 方法,如:
public static void main(String[] args) throws Exception{
@Cleanup InputStream inputStream = new FileInputStream("1.txt");
}
我們通過斷點調試發現,它會調用 close(),說明 lombok 幫我們生成了 close():
通過 lombok 的注解,可以極大的減少我們的代碼量,并且更加清爽,更加優雅。
lombok 還有很多注解,如:
- @ToString: 生成 toString() 方法。
- @EqualsAndHashCode: 生成 equals 和 hashCode 方法。
日志相關注解(當然需要添加相關日志依賴):
- Slf4j、Log4j 等。
- NoArgsConstructor、AllArgsConstructor: 生成無參和全參構造函數。
- Value: 生成 getter、toString、equals、hashCode 和全參構造函數。
- NonNull: 標注在字段和方法上,表示非空,會在第一次使用時判斷。
巧用設計模式
一說到設計模式,我相信大家都能說出那 23 種設計模式,并且還能說出每種設計模式的用法,但是大多數同學應該都沒有在實際應用中真正運用過設計模式,還只是停留在理論階段。
不知道各位是否有過這個感覺,整個應用被相同的代碼充斥著,自己也知道這種代碼不好,但是不知道怎么做優化,雖然知道有 23 種設計模式,卻不知道怎么運用。
本節我就將以實際的例子教大家如何在實際應用中靈活運用所學的設計模式。
(實際應用中,一個場景可能不只包含一個設計模式,很有可能需要多種設計模式配合使用才能寫出優雅的高質量的代碼。)
場景 1:導出報表
我們在做后臺管理系統時,會有這樣一個需求,根據后臺的數據統計導出報表,需要支持 Excel、word、PPT、PDF 等格式。
對于以上需求,一般做法是:為每一個導出報表的方法提供一個方法,然后在 Service 里判斷,如果為 excel,則調用 excel 的方法,如果為 Word 則調用 word 的方法,
如:
public void exportReport(String type){
if("Excel".equals(type)){
exportExcel();
}else if("Word".equals(type)){
exportWord();
}
...
}
這樣寫本身沒有問題,也能實現需求,但是它有以下缺點:
- 1.代碼不夠優雅,業務方法內存在太多 if - else。
- 2.擴展性不強,每增加一個報表格式,就需要修改業務方法,增加一個 if - else。
我們在開發時需要遵循有一個原則:一個方法做你該做的事。也就是無論增加什么樣的報表格式,業務方法 exportReport 的作用依然是導出功能,除非業務需求發生改變,否則不能修改業務方法。
那么,我們該怎么改造呢?
我們發現,導出報表可以導出不同的格式,這些格式我們可以理解為產品,需要由一個地方產出,因此馬上就能想到可以利用工廠模式對其進行改造,下面是改造后的代碼:
public enum Type {
EXCEL,
WORD,
PPT,
PDF;
}
/**
* 模板引擎基類
* 所有模板類繼承此類
* @author 李熠
*
*/
public abstract class Template {
//讀取內容
public abstract List<Serializable> read(InputStream inputStream)throws Exception;
//寫入內容
public abstract void write(List<Serializable> data)throws Exception;
}
//Excel模板
public class ExcelTemplate extends Template {
@Override
public List<Serializable> read(InputStream inputStream)throws Exception{
List<Serializable> list = new ArrayList<>();
return list;
}
@Override
public void write(List<Serializable> data) throws Exception {
}
}
public class TemplateFactory {
public static Template create(Type type){
Template template = null;
switch (type){
case EXCEL:
template = new ExcelTemplate();
break;
}
return template;
}
public static void main(String[] args) {
Template template = TemplateFactory.create(Type.EXCEL);
template.read(input);
template.write(data);
}
}
這樣就完成了工廠模式對報表導出的改造,在業務方法內,通過 TemplateFactory 創建 template,然后調用 template 的 read 或 write 方法,以后我們每增加一個格式,只需要實現 Template 的相應方法,在 factory 實例化它即可,無需修改業務方法。
此場景用到的設計模式有:簡單工廠模式。
場景 2:分步驟執行任務
這種場景也比較多見,比如:
- 1.我們實現一個注冊功能,注冊的字段比較多,可能就會分步驟進行,第一步,填寫手機號驗證碼,第二步,填寫頭像昵稱。
- 2.我們發布一篇文章,第一步,填寫標題和內容,第二步,設置定時任務,第三步,設置文章打賞規則。
針對這些情況,一般做法也是在業務方法內,做個 if - else 判斷,如果是第一步,則執行第一步的業務,如果是第二步,則執行第二步的業務,這種方式同場景 1 一樣,代碼也比較難看。
對于這樣的場景,我們同樣可以使用設計模式來實現,因為每一步都是有關聯的,執行完第一步,才能執行第二步,執行完第二步才能執行第三步,它很像一條鏈子將它們聯系起來,所以很容易想到可以采用責任鏈模式。
下面,請看具體的實現:
public abstract class Handler {
protected Handler handler;
public void setHandler(Handler handler) {
this.handler = handler;
}
public abstract void handleRequest();
}
public class StepOneHandler extends Handler {
@Override
public void handleRequest() {
if(this.handler != null){
this.copy(this.handler);
this.handler.handleRequest();
}else{
//執行第一步的方法
}
}
}
public class StepOneHandler extends Handler {
@Override
public void handleRequest() {
if(this.handler != null){
this.copy(this.handler);
this.handler.handleRequest();
}else{
//執行第一步的方法
}
}
}
public class HandlerFactory {
public static Handler create(int step){
Handler handler = null;
switch (request.getStep()){
case 1:
handler = new StepOneHandler();
break;
case 2:
Handler stepTwoHandler = new StepTwoHandler();
handler.setHandler(stepTwoHandler);
break;
default:
break;
}
return handler;
}
public static void main(String[] args) {
Handler handler = HandlerFactory.create(step);
handler.handleRequest();
}
}
業務類傳入一個 step,通過 HandlerFactory 實例化 handler,通過 handler 就可以執行指定的步驟,同樣地,增加一個步驟,業務類無需任何變動。
場景 3:多重循環改造
有些時候,我們會使用多重循環,直接帶業務方法里寫,看著很不優雅,就像這樣:
for (int i = 0;i < list.size();i++){
for (int j = 0;j < list.size();j++){
for (int k = 0;k < list.size();k++){
}
}
}
我們可以將其進行封裝改造,將循環細節封裝起來,只將一些方法暴露給業務方調用:
public class Lists {
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
list1.add("1");
list1.add("1");
List<Object> list2 = new ArrayList<>();
list2.add("2");
list2.add("2");
List<Object> list3 = new ArrayList<>();
list3.add("3");
list3.add("3");
//通過這樣的方式,使代碼更加優雅,更加清晰,調用方無需理解循環細節
Lists.forEach(list1,list2,list3).then(new Each() {
@Override
public void test(Object... items) {
System.out.println(items[0]+"t"+items[1]+"t"+items[2]);
}
});
}
private List[] lists;
public static class Builder{
private List[] lists;
public Builder setLists(List[] lists){
this.lists = lists;
return this;
}
//通過構建者實例化Lists類
public Lists build(){
return new Lists(this);
}
}
private Lists(Builder builder){
this.lists = builder.lists;
}
/**
*
* @param lists
* @return
*/
public static Lists forEach(List...lists){
return new Lists.Builder().setLists(lists).build();
}
public void then(Each each){
if(null != lists && lists.length > 0){
List list1 = lists[0];
for (int i = 0;i < list1.size() ;i++){
List list2 = lists[1];
for(int j = 0;j < list2.size();j++){
List list3 = lists[2];
for(int k = 0;k < list3.size();k++){
each.test(list1.get(i),list2.get(j),list3.get(k));
}
}
}
}
}
//設置觀察者
public interface Each{
void test(Object...items);
}
}
上面的 main 方法就是我們業務調用時需要調用的方法,可以看出,我們將循環細節封裝到 Lists 里面,使調用方的代碼更加優雅。
此場景用到的設計模式有:構建者模式、觀察者模式。
場景 4:再見吧!if - else
在實際應用中,我們看到最多的代碼便是 if - else,這樣的代碼在業務場景中出現太多的話,看著就不太優雅了,前面的場景其實已經多次將 if - else 用設計模式替換,本場景,我將會用新的設計模式來替換討厭的 if - else,那就是策略模式。
策略模式,通俗點講,就是根據不同的情況,采取不同的策略,我們把它轉化成 if - else,即:
if(情況1){
執行策略1
}else if(情況2){
執行策略2
}
我們用策略模式該怎么實現呢,請看代碼:
public interface Strategy {
/**
* 策略方法
*/
void strategyInterface();
}
public class ConcreteStrategyA implements Strategy{
@Override
public void strategyInterface() {
System.out.println("實現策略1");
}
}
public class Context {
//持有一個具體策略的對象
private Strategy strategy;
/**
* 構造函數,傳入一個具體策略對象
* @param strategy 具體策略對象
*/
public Context(Strategy strategy){
this.strategy = strategy;
}
/**
* 策略方法
*/
public void contextInterface(){
strategy.strategyInterface();
}
public static void executeStrategy(int type){
Strategy strategy = null;
if(type == 1){
strategy = new ConcreteStrategyA();
}
Context context = new Context(strategy);
context.contextInterface();
}
public static void main(String[] args) {
Context.executeStrategy(1);
}
}
這樣,我們就避免了在業務場景中大量地使用 if - else 了。
此場景用到的設計模式有:策略模式.
通過以上的學習,我們其實是可以寫出很多優雅的代碼,各位在實際中如果有什么問題,或者在實際應用中發現一段代碼不知道如何優化也可以再本 chat 的讀者圈隨時向我提問。
接下來,我將告訴大家一些 Java 編程的小技巧,利用這些技巧,可以避免一些低級 bug,也可以寫出一些優雅的代碼。
Java 編程實用技巧
重寫方法務必加上 Override
我們在集成一個類時,可能會重寫父類方法,大家務必加上 Override 注解,請看下面的代碼:
public class Parent {
public void method(int type){
}
}
public class Son extends Parent{
public void method(String type) {
}
}
我們的本意是要重寫 method,但是參數類型寫錯了,變成了重載,編譯器不會報錯,如果我們加上 @Override,編譯器會報錯,我們就能馬上發現代碼的錯誤,而避免運行一段時間導致的 bug。
警告比錯誤更可怕
為什么這么說呢?錯誤我們馬上就能發現,而且如果是編譯時錯誤,都無法運行,但是警告并不影響編譯和運行,舉個例子:
int i = 0;
for(int j = 0;j < 10;j++){
}
我的本意是 for 循環用 i,但是卻寫成了 j,這時編譯不會報錯,但是 IDE 會給出警告:
它告訴我們i這個變量沒有使用到,如果忽略警告,那么很可能運行一段時間出現致命性的 bug,但是如果我們重視警告,當編譯器提出這個警告時,我們就會想,i為什么沒有用到呢,檢查代碼,馬上就能發現隱藏的 bug。
盡量使用枚舉類型
在開發數據庫項目時,經常會有一些譬如狀態、類型、性別等具有固定值的字段,一般我們會用數字表示,在業務中,也會經常判斷,比如狀態為 1 時,執行什么操作,如果直接這樣寫數字,必須要寫注釋,否則很難懂。類似這種字段,盡量封裝成枚舉類型,如:
/**
* 驗證碼類型(1、注冊2、動態碼登錄3、修改密碼4、忘記密碼)
*/
public enum CaptchaType {
/**
* 注冊
*/
REGISTER(1),
/**
* 動態碼登錄
*/
DYNAMIC(2),
/**
* 修改密碼
*/
UPDATE_PASSWORD(3),
/**
* 忘記密碼
*/
FORGET_PASSWORD(4);
private int type;
CaptchaType(int type){
this.type = type;
}
public int getType() {
return type;
}
}
我們在使用時直接調用枚舉,可讀性增加了,也利于擴展。
優秀的代碼比注釋更重要
小王是公司的 Android 開發工程師,在開發應用時,封裝了一些常量,用于提示語。
架構師在 code review 時發現,變量命名很成問題,如:
String MSG0001 = "網絡有問題,請檢查網絡設置!";
架構師要求小王更正,但小王給我的理由是這種編碼是產品經理定的,我可以在每個調用者上面加上注釋,而保留現狀。
很明顯,這樣的代碼是不可取的,如果換成一個可讀變量名是不是更清晰呢?比如:
String NET_CONNECT_ERROR = "網絡有問題,請檢查網絡設置!";
堅持單一職責原則
這個原則很好理解,即一個方法只做一件事,如果一個方法做了太多的事,請考慮重構此方法,合理運用類似上面提到的設計模式。
equals 判斷時常量放在前面
下面對兩種代碼進行比較:
if("1".equals(type)){}
if(type.equals("1")){}
如果變量放在前面,一旦變量為 null,則會出現空指針異常,但是常量放在前面,則不會出現空指針異常。
謹慎使用位運算
各位看到網上經常再說,位運算效率高怎么怎么樣的,但事實真的如此嗎,我們不妨做個測試:
long start = System.currentTimeMillis();
for (int i = 0;i < 1000000;i++){
int sum = i * 2;
}
long end = System.currentTimeMillis();
System.out.println((end - start)+"ms");
start = System.currentTimeMillis();
for (int i = 0;i < 1000000;i++){
int sum = i >> 1;
}
end = System.currentTimeMillis();
System.out.println((end - start)+"ms");
以上代碼,我分別測試了 1 萬次,10 萬次和 100 萬次,得出的結論是 1 萬次速度一樣,10 萬次和 100 萬次都只相差 2 毫秒,如今計算機計算性能越來越好,利用位運算和四則運算效率相差太小,而位運算的可讀性非常低,除非有詳細的注釋,否則一般人真看不懂。
因此,盡量少用位運算。當然有些場景是避免不了的,比如:密碼生成、圖像處理等,但實際應用中,我們很少自己寫這類算法。
避免使用 float 和 double 進行精確計算
我們如果要精確計算浮點數,切記不要用 float 和 double,它們的計算結果往往不是你想要的,比如:
double a = 11540.0;
double b = 0.35;
System.out.println(a * b);
計算結果為:
4038.9999999999995
我們要精確計算,需要用 BigDecimal 類,如:
double a = 11540.0;
double b = 0.35;
BigDecimal a1 = new BigDecimal(a+"");
BigDecimal b1 = new BigDecimal(b+"");
System.out.println(a1.multiply(b1));
這樣就能得出精確的值:
4039.000
優先考慮 lambda 表達式
java8 為我們帶來了 lambda 表達式,也帶來了集合的流式運算,java8 以前,我們循環集合是這樣的:
for (int i = 0;i < list.size() ;i++){
}
java8 以后,我們可以這樣做:
list.stream().forEach(item -> {
//TODO write your code here
});
通過集合的流式操作,我們可以很方便的過濾元素、分組、排序等,如:
//表示篩選元素不為null的
list.stream().filter(item -> item != null).forEach(item -> {
});
//集合排序
list.stream().sorted(((o1, o2) -> {
return o1 > o2;
}));
除了,集合的流式操作,通過 lambda 表示,我們還可以實例化匿名類,如:
new Thread(()->{
//TODO write your code
}).start();
//上下兩段代碼是一樣的效果
new Thread(new Runnable() {
@Override
public void run() {
}
}).start();
可以看出,使用 lambda 表達式,讓我們的代碼更加簡潔,也更加優雅,同學們,請擁抱 lambda 吧!