Lombok指南

最近發現了一個好東西,初看驚為天人,各種黑科技,仔細看了一下確實是很方便實用的工具。Github上已經5000+star了,很優秀的項目。

Lombok為Java提供了不少很甜的語法糖,設計也很合理,很多都符合《Effective Java》所描述的最佳實踐。因此結合官方文檔和自己的一些實踐給大家詳細的介紹一下。

使用方法

在Settings->Plugins搜索並啟用Lombok plugin

然後再pom.xml加入

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.14</version></dependency>

完成之後就可以開始使用了,大部分都是採用註解形式來進行一些定義或是功能的增強。

val

可以代表任意類型

局部變數,不可用於實例變數

這個類型會從初始化的表達式中推測出來,有一種動態語言的感覺,但其實並不是

這個val是final,必須有個初始賦值的表達式,而且不可再賦值

舉例:

val i="string";val list=new ArrayList<String>();s.ensureCapacity(23);System.out.println(i);

等價於:

final String i="asd";final ArrayList<String> s=new ArrayList<>();s.ensureCapacity(23);System.out.println(i);

@NonNull

不能為空,如果為空會直接拋出NullPointerException

在源碼中聲明了可以在成員變數,方法,參數,本地變數中使用,但是實際測試只有參數和成員變數中可以正常工作

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})@Retention(RetentionPolicy.CLASS)@Documentedpublic @interface NonNull {}

舉例:

public class User { @NonNull private String name; public void setName(String name) { this.name = name; }}@Testpublic void main() throws Exception{ User user=new User(); user.setName(null);}@Testpublic void main() throws Exception{ display(null);}private void display(@NonNull String source){ System.out.println(source);}

等價於:

private void display(String source){ if(source == null) { throw new NullPointerException("source"); } System.out.println(source); }

下面並不會報錯

@Testpublic void main() throws Exception{ @NonNull Integer i=null; i=null; System.out.println("success");}@NonNullprivate void display(String source){ System.out.println(source);}

@Cleanup

只可用於本地變數

只可用於可釋放的資源,確切的說是實現了Closeable的類,否則在會出現編譯錯誤,但是IDEA並不會報錯

聲明了此註解的變數會在結束時自動釋放資源

原理是在定義此變數的後面的全部語言外面加上try{}finally(x.close);

舉例:

@Testpublic void main() throws Exception{ @Cleanup InputStream is=new FileInputStream(new File("D: estaa.txt")); System.out.println();}

將上述代碼生成的class反編譯

@Testpublic void main() throws Exception { FileInputStream is = new FileInputStream(new File("D: estaa.txt")); try { System.out.println(); } finally { if(Collections.singletonList(is).get(0) != null) { is.close(); } }}

這種寫法還等價於JDK1.7中的

@Testpublic void main() throws Exception{ try(InputStream is=new FileInputStream(new File("D: estaa.txt"))){ System.out.println(); } }

不過感覺這麼用註解更舒服了一點

@Getter and @Setter

用於成員變數和類

可以直接生產成員變數的set和get方法

如:

public class User { @Getter @Setter private Integer id; @Setter @Getter private String name;}

現在就可以直接使用

@Testpublic void main() throws Exception{ User user=new User(); user.setId(1); System.out.println(user.getId());}

也可以直接在類上聲明,那麼類的所有變數就都生成了get,set方法

@Getter@Setterpublic class User { private Integer id; private String name;}

可以指定訪問級別PUBLIC, PROTECTED, PACKAGE, and PRIVATE,默認是PUBLIC

@Setter(AccessLevel.PROTECTED)

@ToString

可以用於類

覆蓋默認的toString()方法,將呈現類的基本信息

@Testpublic void main() throws Exception{ User user=new User(); user.setId(1); user.setName("bb"); System.out.println(user.toString());}//輸出:User(id=1, name=Asens)

選項:

includeFieldNames:如果為false , 不顯示變數名

@ToString(includeFieldNames=false)//輸出:User(1, Asens)

exclude:不顯示哪些欄位

@ToString(exclude={"id"})//輸出:User(name=Asens)

of:只顯示哪些欄位

@ToString(of={"id"})//輸出:User(id=1)

@EqualsAndHashCode

只用於類

覆蓋默認的equals和hashCode

方法和參數都與@ToString類似

除了一般的equals和hashCode實現外可以使用of或exclude,只對或是不對哪些欄位實現比較會計算hashCode

@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor

均是只適用於類

@NoArgsConstructor

會生成一個無參的構造函數,一個類是有默認的無參的構造函數的,但是如果創建了有參數的構造函數,那麼無參的函數就沒有了,這個註解相當於添加了一個無參的構造函數,而且添加了一些新的功能

如果有finall的欄位的話會出現編譯錯誤,除非使用@NoArgsConstructor(force = true)那麼所有的final欄位會被定義為0,false,null等

@RequiredArgsConstructor

使用該註解會對final和@NonNull欄位生成對應參數的構造函數

如我設定了name不能為null

@Getter@Setter@RequiredArgsConstructorpublic class User { private Integer id; @NonNull private String name;}

那麼在調用的時候就生成了只有name的構造函數

如果我將id同樣設置為@NonNull

那麼在提示的時候就成了

@AllArgsConstructor

顧名思義,生成了全參數的構造函數,會配合@NonNull

@Getter@Setter@ToString@AllArgsConstructorpublic class User { private Integer id; @NonNull private String name;}

反編譯生成的代碼

@ConstructorProperties({"id", "name"})public User(Integer id, @NonNull String name) { if(name == null) { throw new NullPointerException("name"); } else { this.id = id; this.name = name; }}

三者都有一個參數staticName ,使用這個參數可以將構造函數變成靜態工廠方法

此時User不再可new,只能使用靜態工廠的方法

同樣可以使用access 控制類的訪問範圍

@AllArgsConstructor(staticName = "of")User user=User.of(1,"Asens");@AllArgsConstructor(access = AccessLevel.PROTECTED)

@Data

@ToString

@EqualsAndHashCode

@Getter

@Setter

@RequiredArgsConstructor

的集合

可以使用staticConstructor來實現上述staticName 的功能

@Data(staticConstructor="of")

@Value

只用於類

@Value用於生成不可變類,與@Data相似,@Value也是一系列註解的合集

分別包括

@ToString

@EqualsAndHashCode

@AllArgsConstructor

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)

@Getter

區別是不再提供@Setter,每個成員變數都是final和private的,實例化需要提供所有參數用於初始化

@Builder

如果你熟悉建造者模式,那麼你一定知道建造者模式有多麻煩

如果你不知道,可以參考我之前的文章:zhuanlan.zhihu.com/p/27

建造者的模式一般是這樣(減少了成員變數,只有一個成員變數一般不用此模式)

public class User { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public User(Builder builder) { this.name= builder.name; } public static Builder builder(){ return new Builder(); } public static class Builder{ private String name; public Builder name(String name) { this.name= name; return this; } public User build(){ return new User(this); } }}

使用了@Builder之後,只需要這樣

@Builder@Getter@Setterpublic class User { private String name;}

二者調用方法相同

@Testpublic void main() throws Exception{ User user=User.builder().name("Asens").build(); System.out.println(user.getName());}

@SneakyThrows

如果方法里有受檢異常使用的這個註解可以不必在方法中聲明throws對應的異常

比如讀取一個文件,FileNotFoundException是個受檢異常,需要try,catch或是throws拋出

private void read() throws FileNotFoundException { File file=new File("D: estaa.txt"); InputStream is = new FileInputStream(file); //ignore}

然後用了@SneakyThrows就可以直接無視受檢異常

但是在實際運行過程中和拋出並沒有什麼區別

@SneakyThrowsprivate void read() { File file=new File("D: estaa.txt"); InputStream is = new FileInputStream(file);}

這個是反編譯的代碼

private void read() { try { File $ex = new File("D: estaa.txt"); new FileInputStream($ex); } catch (Throwable var3) { throw var3; }}

這個反編譯的代碼並不能通過編譯[腦補笑著哭表情]

究其原因就是在運行期間不管方法是不是聲明了throws,都會拋出異常

但是在編譯的時候只要拋出了受檢異常就必須聲明

所以這個才能實現

但是謹慎使用,怎麼良好的使用異常是一門學問,推薦看一下《Effective Java》對應的章節

@Synchronized

顧名思義,加鎖,僅在方法級別可用

示例:

@Testpublic void mm() throws FileNotFoundException { aa();}@Synchronizedprivate void aa(){ System.out.println();}

反編譯之後

private final Object $lock = new Object[0];public TestS() {}@Testpublic void mm() throws FileNotFoundException { this.aa();}private void aa() { Object var1 = this.$lock; synchronized(this.$lock) { System.out.println(); }}

這個鎖並沒用放在方法上而是把方法內部的全部鎖住了,感覺和直接在方法上加synchronized也沒有什麼太大區別,效率稍微高一點

@Getter(lazy=true)

延遲載入

可用於類和成員變數

對於某些操作可以不必在初始化的時候載入而是等到了需要再去載入,此時就需要進行延遲載入

對於欄位,需要final

我們定義User,我們希望當getName的時候在賦予name的值

@Setter@Getterpublic class User { private Integer id; @Getter(lazy=true) private final String name=makeName(); private String makeName() { System.out.println("make name"); return "aa"; }}

調用

@Testpublic void mm() throws FileNotFoundException { User user=new User(); System.out.println("before user get name"); user.getName(); System.out.println("after user get name");}輸出:before user get namemake nameafter user get name

當user在getName的時候才去執行makeName

作為對比,去掉lazy

@Setter@Getterpublic class User { private Integer id; private final String name=makeName(); private String makeName() { System.out.println("make name"); return "aa"; }}

再次調用,顯然

@Testpublic void mm() throws FileNotFoundException { User user=new User(); System.out.println("before user get name"); user.getName(); System.out.println("after user get name");}輸出:make namebefore user get nameafter user get name

實現的原理大概和這個有點像

public class User { private Integer id; private String name=null; private String makeName() { System.out.println("make name"); return "aa"; } public String getName() { if(name!=null) return name; return name=makeName(); }}

但是這個實在有點簡陋,既不能實現name的final,也不是保存線程安全問題,兩個線程同時訪問,妥妥的調用兩次makeName

為了解決上述問題,lombok是是這麼解決的

反編譯之後

public class User { //省略其他代碼 private final AtomicReference<Object> name = new AtomicReference(); public String getName() { Object value = this.name.get(); if (value == null) { AtomicReference var2 = this.name; synchronized(this.name) { value = this.name.get(); if (value == null) { String actualValue = this.makeName(); value = actualValue == null ? this.name : actualValue; this.name.set(value); } } } return (String)((String)(value == this.name ? null : value)); }}

首先變數成為了一個容器,容器本身不改變,但是可以進行讀寫,並且AtomicReference的讀寫本身就是原子性的,並且在調用makeName和賦值的時候使用synchronized保證了線程安全。

@Log

那個log咋寫來著?

為了彰顯自家產品的與眾不同,各種log的寫法也是各具特色,每次建一個新類都要去別的類裡面複製Log出來,然後改類名,一樣的同學請舉手...

lombok總結了各種log的寫法

在實際的使用中完全做到了無縫銜接,與注釋調到的功能完全等價,簡潔而優雅

@Controller@Slf4jpublic class SampleController { //private static Logger log= LogManager.getLogger(SampleController.class); @RequestMapping("/") public String home(ModelMap model) { log.info("aa"); return "main"; }}

Lombok還有一系列實現性質的功能,這些功能可能不是那麼優雅,沒有很完整的測試,未來有可能刪除或是大改的功能

有興趣的可以看一下

projectlombok.org/featu

大家可以關注我的專欄,查看更多Java的技術文章

zhuanlan.zhihu.com/asen


推薦閱讀:

未來屬於演算法,而不是代碼!
還在用著5年前的cpu和顯卡,是一種什麼感受?
熊寫代碼這三年:閱讀寫作與技術成長
理論上任何代碼都可能沒有bug么?

TAG:Java | 程序员 | 代码 |