Java高效編程:總結分享

  • 2020 年 3 月 26 日
  • 筆記

參考資料:慕課網:Java高效編程收費實戰課程、部落格園、CSDN、菜鳥教程以及其他文檔。

篇幅受限,不太想針對每個點都寫篇部落格,有的地方可能寫的不是很詳細,一筆帶過了。如果你覺得那個點在項目中用得上可以另行搜索一些相關的更詳細的部落格或文檔。

1.Lambda和函數式編程

  函數式編程即可以把函數當作變數、參數、返回值傳遞。實現的方法就是定義一個函數式介面,函數式介面即只有一個抽象方法的介面。這個介面類型的變數就可以當作參數傳遞,而在傳遞的時候可以通過匿名內部類把要執行的邏輯程式碼傳遞進去。

  函數式介面如下,其目的是操作兩個數,具體的邏輯程式碼在使用時通過匿名內部類實現

interface Handle{        int operation(int a, int b);     }

  使用:假設我們要調用一個use方法,其接收參數為兩個int和一個函數介面Handle類型的參數,然後要通過傳遞過來的邏輯程式碼處理這兩個int

use(1,2,new Handle(){      int operation(int a,int b){          return a+b;      }  }));

而在use方法裡面,就已經接收到了1、2和匿名內部類裡面的邏輯程式碼。

public void use(int a,int b,Handle h){      System.out.println(h.operation(a,b));//輸出3  }

此時Lambda的作用就是簡化匿名內部類的寫法。如果替換成Lambda表達式,上面調用use方法的程式碼就變成了這樣:

use(1,2,(a,b)->a+b); 

下面說說Lambda的語法

(參數)->{程式碼塊} 

1. 如果只有一個參數可以不要小括弧。

2. 如果程式碼塊只有一句可以不要大括弧,就像我上面寫的a+b一樣。

3. 需要返回值的話,如果只有一句程式碼可以不用寫return。就像我上面寫的,a+b,只有一句,需要一個int類型的返回值,這時候默認就返回a+b的結果。

4. 可選的類型聲明,在小括弧中,既可以寫上int,也可以不寫,它都會根據參數識別類型。

5. Lambd表達式的目標類型必須是函數式介面,就如上面程式碼的Handle類型。Lambda表達式也是有類型的,不然也不能當作參數傳遞。所以如果為了程式碼清晰,你不想在傳遞參數時寫Lambda表達式,也可以這麼寫:

Handle h=(a,b)->a+b 

然後把h傳遞給use方法。

函數式介面一般來說都比較泛化,Java專門給我們提供了一些函數式介面,所以不一定需要我們自己定義。比如:

 

2.使用方法引用精簡Lambda

  方法引用是對Lambda的簡化,如果Lambda表達式程式碼塊中只有一條程式碼,就可以使用方法引用,其語法如下:

類名或對象::方法名

下面舉例的每種情況,前面為Lambda程式碼,後面為簡化引用後的程式碼

1.靜態方法的引用:

String num->Integer.parseInt(num); Integer::parseInt; 

2.任意類型的實例方法的方法引用:

String str->str.length(); String::length; 

  3.引用指定對象的實例方法:

List<String> list=new ArrayList<>();//list為指定對象   String str->list.add(str); list::add;

  

3.使用Stream操作集合

  首先了解什麼是Stream?Stream類似SQL語句一樣,對數據源進行各種操作,如篩選,排序等,而Stream本身不會存儲數據。簡而言之就是:Stream是從支援數據處理操作的數據源生成的元素隊列。數據源可以是集合數組等。數據處理操作就是各種篩選、排序等,下面會重點講。

集合在實際程式碼中會經常用到,而Stream可以提供更簡便的數據操作,其很多API接收的還是函數式介面,所以還可以用上前面的Lambda表達式,進一步簡化程式碼。

Stream提供的API從功能上主要分為四類:中間方法無狀態、中間方法有狀態、末端短路方法和末端非短路方法。

所謂中間和末端:中間方法就是保持流的打開狀態,並允許後續還有操作,比如排序、遍歷等。末端方法就是對流的最終操作,比如最大(小)值、遍歷、計數等。

有無狀態和是否短路:有狀態就是這種方法會改變流的結果,如去重、截斷等,這種方法性能開銷會更大。無狀態相反。短路方法就是這種方法不一定會檢查所有元素,比如操作首個值,不匹配等,找到了就返回。非短路相反。

中間方法無狀態:過濾(filter)、映射(map)、扁平化(flatMap)、遍歷(peek)

中間方法有狀態:去重(distinct)、跳過(skip)、截斷(limit)、排序(sorted)

末端短路方法:所有匹配(allMatch)、任意匹配(anyMatch)、不匹配(noneMatch)、查找首個(findFirst)、查找任意(findAny)

末端非短路方法:遍歷(forEach)、歸納(reduce)、最大值(max)、聚合(collect)、最小值(min)、計數(count)

下面演示幾個示例,假設有個Person實體類,裡面有欄位gender、name、age,list為Person類型的集合。stream()方法作用是為集合創建串列流。

list.stream().filter(person -> person.getAge()>20&&person.getGender().equals("男"))               .forEach(person -> System.out.println("姓名:"+person.getName()));//輸出所有大於20歲男性的姓名

如果篩選過程你在資料庫里就已經做了,現在只想返回結果中的某個欄位,可以這麼做。

list.stream().map(person -> person.getName())//map是把一種類型的集合元素轉化成另一種類型的集合元素。如Person集合轉化成String集合,其內容就是name              .forEach(name-> System.out.println(name));//輸出所有name

  對年齡去重並排序

list.stream().map(person -> person.getAge())               .distinct().sorted()               .forEach(age-> System.out.println(age));

上面幾個示例只演示了一個末端方法,更多的API大家可以自己去試試。實際中我們篩選出數據後一般不會直接輸出,而是返回給客戶端,這時候就需要把篩選過後的流給收集起來:

List<Person> list1 = list.stream().filter(person -> person.getAge() > 18)                           .collect(Collectors.toList());//收集到一個集合中  Map<String,List<Person>> list2 = list.stream()                                      .collect(Collectors.groupingBy(person->person.getGender()));//按性別分類

 

4.try-with-resource

  這個是JDK7中提供的一個語法糖。可以在try後面的小括弧裡面聲明資源,之後就不用再在finally裏手動的關閉了。但也不是所有資源都可以自動關閉,必須實現了AutoCloseable介面。使用場景:打開外部資源時可以用try-with-resource自動關閉,如讀取文件,資料庫連接,網路連接等。在聲明的時候就可以在try後面的小括弧里聲明,不過最好注意一下,這些類是否實現了上面提到的介面,還有其源碼中是否有關閉方法。比如ByteArrayInputStream等,它的close方法裡面什麼也沒有,說明不需要關閉,所以就不需要聲明在try後的小括弧里了。

 

5.使用Optional避免空指針異常

  空指針異常應該是很多程式設計師見過最多的異常。如果想避免這個異常,經常會使用if判斷是否為null,這樣顯得一點都不精簡。Optional是一個可以為null的容器對象。

還是以上面的Person實體類為例,如果我們想安全的返回某個Person類型數據的name,正常的話應該這麼寫:

Optional的API就不在這寫了,在看下面的內容之前,建議先看看它的API:https://www.runoob.com/java/java8-optional-class.html.根據API說明,自己多試試。

public static String Get_Name(Person person) throws Exception{      if(person!=null){          if(person.getName()!=null){              return person.getName();          }      }      throw new Exception("Person為空");  }

使用Optional後可以這樣

public static String Optional_Get_Name(Person person) throws Exception{      return Optional.ofNullable(person).map(name->person.getName()).orElseThrow(()->new Exception("Person為空"));  }

實際情況中更多的可能是這樣:

if(person!=null){      dosomthing(person);//對person對象執行某種邏輯程式碼  }

使用Optional後可以這樣

Optional.ofNullable(person).ifPresent(p->{dosomthing(p);});

總結:

1.可以發現ofNullable在每種情況下使用了,它的作用就是構建一個Optional對象。如果傳進去的值為空而且後面沒有任何操作它就返回empty對象。

2.ofNullable和of的區別就是,如果值為null,of會報空指針異常。如果你想收集這個異常,那麼就可以用of,一般情況下就用ofNullable。

3.orElse開頭的三個方法,是針對空值進行操作的;兩個map方法是做映射的,就像Stream中的map一樣;兩個Present方法是判斷值是否為空的,is開頭的直接返回 boolean類型,if開頭的可以用Lambda表達式對非空的值做一些操作。

 

6.Guava工具集

  Guava是Google開發的一款拓展Java類庫的工具集。像上面說到的Optional就是Java開發團隊受到這個工具集中Optional的啟發。我的個人項目中就用到了Guava提供的RateLimiter限流、Multiset、還有一些工具類。如果你用的是Maven,在https://mvnrepository.com/artifact/com.google.guava/guava導入依賴就行,如果不是可自行在網上搜索jar包導入。

新集合Multiset:

  Multiset是一個可以有重複值的Set集合。其特點是無序可重複。具體的API不在這裡講了,大家可以看看這個http://www.bjpowernode.com/tutorial_guava/731.html其可實現這麼一個比較常見的場景,在一個集合中,統計出某個元素的出現的次數,利用這個集合可以很輕鬆的實現這個功能。還是以Person實體類為例,如果大家用的也是實體類,不要忘了重寫hashCode()和equals()方法:

List<Person> list=dao.Get_Personlist();//dao.Get_Personlist()模擬從數據源獲取perosn集合  Multiset<Person> multiset= HashMultiset.create();//HashMultiset為實現類  list.stream().forEach(person -> multiset.add(person));//通過Stream和Lambda依次把元素加入multiset  int count=multiset.count(StartPerson)//StartPerson為假設的指定的Person對象,count就是找出StartPerson出現的次數

原生集合的拓展工具類:

  除了Guava提供的一些新的集合,還針對Java原生集合提供了很多工具類,命名上基本都以原生集合名加s,比如List集合的工具類是Lists。工具類主要就是看API,由於不可抗拒的原因,Google官方的API是看不了了,可以看這個https://ifeve.com/google-guava-collectionutilities/

 

7.Lombok註解

  Lombok也是一個拓展類庫,提供了一些註解為我們省略了很多重複但經常會用到的程式碼,比如實體類裡面的get,set,equals等。下圖是其常用的註解:

  其中@Data註解是包括@Getter等四個註解的功能。和Guava一樣,在Maven里導入依賴https://mvnrepository.com/artifact/org.projectlombok/lombok,或自行下載jar包。這裡還是推薦大家使用Maven,平時下載jar包也方便些,自己在網上下載的一個lombok jar包導入後,沒有生效,換成Maven後就可以了。與普通的依賴不同,這裡多了個 <scope>provided</scope>,它的意思就是這個jar包是運行在編譯時期,加上註解後,並不會多出任何程式碼,javac編譯成class文件後,大家用IDEA打開這個class文件就會發現多了很多程式碼。

  還有一點要注意的是,這個需要下載插件。Lombok註解是在編譯時才添加程式碼的,當我們編寫程式碼時調用get、set方法,實際上這時候這些方法程式碼並不存在,IDE語法檢測就不會通過,如果用的是IDEA在settings->plugins里搜索Lombok下載安裝就行了。

  簡單的示例:

@Data  public class EntityDemo {      private String name;      private int age;  }

  編譯後查看class文件,用IDEA反編譯:

public class EntityDemo {      private String name;      private int age;        public EntityDemo() {      }        public String getName() {          return this.name;      }        public int getAge() {          return this.age;      }        public void setName(String name) {          this.name = name;      }        public void setAge(int age) {          this.age = age;      }        public boolean equals(Object o) {          if (o == this) {              return true;          } else if (!(o instanceof EntityDemo)) {              return false;          } else {              EntityDemo other = (EntityDemo)o;              if (!other.canEqual(this)) {                  return false;              } else {                  Object this$name = this.getName();                  Object other$name = other.getName();                  if (this$name == null) {                      if (other$name == null) {                          return this.getAge() == other.getAge();                      }                  } else if (this$name.equals(other$name)) {                      return this.getAge() == other.getAge();                  }                    return false;              }          }      }        protected boolean canEqual(Object other) {          return other instanceof EntityDemo;      }        public int hashCode() {          int PRIME = true;          int result = 1;          Object $name = this.getName();          int result = result * 59 + ($name == null ? 43 : $name.hashCode());          result = result * 59 + this.getAge();          return result;      }        public String toString() {          return "EntityDemo(name=" + this.getName() + ", age=" + this.getAge() + ")";      }  }

  其中@Getter和@Setter可以用在欄位前面,給欄位單獨使用,還可以在後面小括弧里設置一些屬性

public class EntityDemo {      @Getter(value = AccessLevel.PRIVATE,//設置get方法訪問許可權為private              onMethod_= {@NonNull}//再給name加個NotNull註解      )      private String name;      private int age;  }

這些註解的功能是非常實用的,用起來也比較簡單,也就不一一舉例了,平時比較常用也就是@Data相關的和構造方法還有就是日誌註解。主要就是它們的一些屬性可能會比較陌生,大家可以Ctrl加滑鼠左鍵點進源碼,看看它們的欄位,也就是它們可以設置的屬性,通過字面意思也應該能知道它們的意思。不過除了日誌註解,實際當中我基本沒用其他註解,一個是降低了程式碼的可讀性,二是錯誤異常可能正好在這些生成的程式碼中,增加了調試難度。IDEA的Ctrl+O和Alt+Insert快捷鍵就足夠了

 

8.驗證框架和約束註解

  Bean Validation和Hibernate Validator

  前者是驗證框架的一種規範,後者是對其的一種具體實現。使用時先導入這兩個依賴:https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator 和https://mvnrepository.com/artifact/javax.validation/validation-api。約束註解對欄位屬性設置約束或規則,驗證框架則是對實際數據進行驗證,驗證是否符合對應的約束或規則。

  下面列出一些常用的約束註解,每個註解基本都有一些屬性可以設置,和上面說的一樣,大家可以點進註解源碼查看有哪些欄位:

註解

描述

@AssertFalse

被注釋的元素必須為 false

@AssertTrue

同@AssertFalse

@DecimalMax

被注釋的元素必須是一個數字,其值必須小於等於指定的最大值

@DecimalMin

同DecimalMax

@Digits

被注釋的元素是數字

@Future

將來的日期

@Max

被注釋的元素必須是一個數字,其值必須小於等於指定的最大值

@Min

被注釋的元素必須是一個數字,其值必須大於等於指定的最小值

@NotNull

不能是Null

@Null

元素是Null

@Past

被注釋的元素必須是一個過去的日期

@Pattern

被注釋的元素必須符合指定的正則表達式

@Szie

被注釋的元素必須在指定長度內

@Email

元素必須是格式良好的電子郵箱地址

@Length

字元串的大小必須在指定的範圍內,有min和max參數

@NotEmpty

字元串的不能是空

@NotBlank

字元串不能使空,但是與@NotEmpty不同的是尾隨的空白被忽略

@URL    

字元串必須是一個URL

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  簡單的使用,還是以Person實體類為例(大家在嘗試時最好在web項目上,畢竟這個功能基本也都在web上才用得到。非web項目可能會出現一些錯誤,這時候就要引入el依賴):

@Data  @AllArgsConstructor  public class Person {      @NotNull(message = "用戶名不能為空")      @Size(min = 2,max = 15)      private String name;      @NotNull      private int age;      @NotNull(message = "性別不能為空")      private String gender;  }

private static Validator validator=Validation.buildDefaultValidatorFactory().getValidator();//初始化驗證器  public static void main(String[] args) {      Person person=new Person("",15,"男");//新建一個數據      Set<ConstraintViolation<Person>> validate = validator.validate(person);//進行驗證      validate.forEach(item ->System.out.println(item.getMessage()));//對驗證結果進行輸出,就是輸出註解後面的message。  }

 

9.IDEA使用技巧

  IDEA應該是目前Java開發者用的比較多的IDE了,在這分享一些小技巧。全部的快捷鍵就不重複說了,網上有很多說明。

  1.右鍵行號上的紅色斷點圖標可以給斷點添加判斷條件,符合條件才會在這個斷點停下。

  2.Debug運行後按Alt+F8(Windows如果是N卡可能會快捷鍵衝突),或點擊Debug欄上類似一個計算器的圖標(在“跳轉到游標”按鈕的右邊)。會打開一個小窗口,裡面可以對斷點處的數據進行計算,比如一個斷點處有一個list,我們可以輸入list.size()計算出它的元素個數。

  3.Stream調試,當我們在Stream程式碼處打上斷點,運行後Debug欄上會多出一個按鈕(在“跳轉到游標”按鈕的右邊第二個),可以看每一步stream操作後的數據集結果。如果沒有這個按鈕可能是你的IDEA版本太低了,在插件裡面搜索Java Stream Debugger安裝就行了。

  4.全局重命名,首先滑鼠游標放到一個變數名上(方法名也行),按住Shift+F6,重命名後,所有用到這個變數的地方都會自動重命名。

  5.滑鼠游標放到一個方法名上,按Ctrl+F6可以快捷的修改方法簽名(或點擊頂部工具欄Refactor->ChangeSinature)。如訪問修飾符、返回類型、方法名、參數列表。新增一個參數後,還可以設置默認值,調用了這個方法的地方就會自動填上這個默認值。

  6.在做後端開發時,經常會用到postman或者其他請求工具測試介面,有時候可能會需要發送json格式的數據,在插件裡面搜索POJO to Json插件,安裝重啟後滑鼠右鍵實體類名,有個POJO to Json的選項,點擊後就會自動複製這個實體類所有欄位的Json格式,粘貼一下就可以用了。

  7.序列化版本ID生成器插件:GenerateSerialVersionUID,插件里搜索UID就行了。在用Redis時、或實體類實現了Serialiable介面。如果改變了實體類結構或者其他問題可能會在查詢Redis數據後反序列化時報錯,報錯內容就是序列化版本id不一致,如果我們給實體類指定一個固定的序列化版本ID就不會出現這個問題了。而這個插件可以幫助我們一鍵自動生成,快捷鍵Alt+Insert,選擇最後一個SerialVersionUID。

  8.Mybatis Log Plugin,平時開發時,即使我們在配置文件裡面設置了在控制台輸出sql語句,但是還是會以?號佔位符的形式顯示。這個插件可以顯示完整可直接執行的sql語句。