代碼簡潔之路——一些讓你代碼更加簡潔和清晰的JAVA特性

以上內容都是基於JAVA 8 的版本。對於之後的JAVA新特性並沒有整理進來。


平時我們開發功能時,總希望能用更簡潔的代碼實現業務需求,代碼簡潔一向都是各個開發人員追求的目標。

就像是爲了減少get、set我們可以使用lombok插件,爲了方便進行SQL查詢而使用MyBatis-Plus增強。而隨着java版本不斷的更新,Java也在語言層面上提供更多便利給開發人員。

如何使自己的代碼內容更加少,而可以實現的功能更多。最常提起的就是Java各個版本提供的語法糖。而關於語法糖的內容已經有足夠多人去介紹了,這裏我只是會介紹一些平時使用過的語法糖,而對於:基礎類型的自動裝箱泛型擦除條件編譯枚舉switch 增強。這些已經被廣泛使用的內容就不再介紹了。

數值分割

在java 1.7中,我們在設置數值的時候,可以在數字之間插入任意多個下劃線。這樣對於一些比較大的數字,可以通過下劃線來切割,方便我們閱讀。這是一個小的改進,雖然並沒有多少複雜的邏輯在裏面,但是經常在配置文件中設置比較大的數值的需求,使用這個小技巧會讓你的數值更加被區分出來。

    public long getLong() {
        long rest = 1_000_000_000L;
        return rest;
    }

try-with-resource

try-with-resource是JAVA爲了方便對資源進行關閉而提供的語法糖。在此之前我們每次使用資源操作之後往往需要頻繁的嵌套大量try-catch和null的判斷來保證所有資源都被正常關閉。而使用try-with-resource代碼會在編譯階段纔會補全這部分內容

try-with-resource語法的使用

我們需要將操作的資源在 try 之後的括號中聲明,然後就可以在後面的代碼塊中使用,就像是下面這樣子

public class ResourceSugar {

    public void testResource() {
        try (FileReader fileReader = new FileReader("D:\\ web.xml");
             BufferedReader br = new BufferedReader(fileReader)) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println("do something...");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

上面是一個使用try-with-resource的例子,而其實現的原理就是在編譯的時候將我們關閉資源時候需要的循環try-catch內容進行補全。將上面的內容進行編譯後可以得到下面的代碼。

編譯後

public void testResource() {
        try {
            FileReader fileReader = new FileReader("D:\\ web.xml");
            Throwable var2 = null;

            try {
                BufferedReader br = new BufferedReader(fileReader);
                Throwable var4 = null;

                try {
                    while(br.readLine() != null) {
                        System.out.println("do something...");
                    }
                } catch (Throwable var29) {
                    var4 = var29;
                    throw var29;
                } finally {
                    if (br != null) {
                        if (var4 != null) {
                            try {
                                br.close();
                            } catch (Throwable var28) {
                                var4.addSuppressed(var28);
                            }
                        } else {
                            br.close();
                        }
                    }

                }
            } catch (Throwable var31) {
                var2 = var31;
                throw var31;
            } finally {
                if (fileReader != null) {
                    if (var2 != null) {
                        try {
                            fileReader.close();
                        } catch (Throwable var27) {
                            var2.addSuppressed(var27);
                        }
                    } else {
                        fileReader.close();
                    }
                }

            }

        } catch (IOException var33) {
            throw new RuntimeException(var33);
        }
    }

可以看到try-with-resource編譯後就能看到我們平時冗長當try-catch嵌套的流關閉操作,而使用try-with-resource語法後,這一步將不再需要我們自己驅編寫。從而大大減少代碼量。

try-with-resource使用時需要注意的問題

不允許修改聲明的資源變量

爲了防止在try內的代碼塊中修改變量引用從而導致編譯的資源關閉邏輯失效,從而try-with-resource中聲明的變量會隱式的加上final 關鍵字,比如下面代碼將無法編譯

    public void testResource() {
        try (FileReader fileReader = new FileReader("D:\\ web.xml");
             BufferedReader br = new BufferedReader(fileReader)) {
            fileReader = new FileReader("D:\\ web.xml")
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println("do something...");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

錯誤的資源變量創建方式,導致無法正常關閉資源

在開發中有些人可能因爲盲目相信try-with-resource可以自動關閉資源,這個時候可能會使用流嵌套的寫法比如下面這樣

    public void testResource() {
        try (BufferedReader br = new BufferedReader(new FileReader("D:\\ web.xml"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println("do something...");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

看起來並沒有太大問題,但是這個時候查看上面內容編譯後的代碼

    public void testResource() {
        try {
            BufferedReader br = new BufferedReader(new FileReader("D:\\ web.xml"));
            Throwable var2 = null;

            try {
                while(br.readLine() != null) {
                    System.out.println("do something...");
                }
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (br != null) {
                    if (var2 != null) {
                        try {
                            br.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        br.close();
                    }
                }

            }

        } catch (IOException var14) {
            throw new RuntimeException(var14);
        }
    }

可以看到編譯後的代碼少了很多,此時因爲BufferedReader聲明瞭變量,try-with-resource只會嘗試對BufferedReader進行關閉。而內部的FileReader進行資源關閉的代碼卻不見了。所以在使用try-with-resources的時候一定要爲各個資源聲明獨立變量。

可變參數

在開發中,有些時候我們希望一個方法能夠接受1-n個參數。在之前我們可能會把這樣的參數聲明爲一個數組或者集合,但是我們也可以使用可變參數。

創建使用可變參數的方法

要想定義一個可變參數,只需要在參數類型後面使用…標識,就像下面操作

    public static void setObj(Object... params) {
        System.out.println(params.length);
    }

這樣我們可以使用下面的方式調用上面參數

可變參數使用

在使用可變參數的方法時候,可變參數會被認爲一個數組參數。所以使用可變參數的方法時可以多個參數使用,也可以將可變參數拼裝成數組使用。下面兩種參數都是可以正確使用

setIntegerToObj(1,2,3);
setIntegerToObj(new Integer[]{1, 2, 3});

可變參數限制

使用可變參數的時候並不會限制方法有多少其他類型的參數也不要求其他參數類型和可變參數是否相同,但是可變參數必須在最後一位。

可變參數使用需要注意的問題

使用可變參數的時候對於重載和重寫有比較多的要求,不過好在現在的開發工具都會及時的拋出相關異常這裏就不介紹了。

下面介紹一個自己曾經遇見的問題。下面的main方法中最終的結果大家覺得會是多少?

public class ParamSugar {

    public static void main(String[] args) {
        setIntToObj(1,2,3);
        setIntegerToObj(new Integer[]{1, 2, 3});
        setObj(1,2,3);
    }

    public static void setIntToObj(int... params) {
        setObj(params);
    }

    public static void setIntegerToObj(Integer... params) {
        setObj(params);
    }

    public static void setObj(Object... params) {
        System.out.println(params.length);
    }

}

這裏我介紹下上面問題出現的場景,正常我們一般不會寫一個數據類型object的可變參數,object的類型會使得方法的重載變得更容易出現問題,但是在一些常用的工具上(比如:MongodbTemplate的Criteria所使用的批量條件的方法)會經常遇見object的可變參數,而這個時候上面的場景就可能出現。

而上面內容最終會輸出下面的結果:

1
3
3

可以看到,當直接使用int參數的時候,setObj識別爲3個參數,而使用Integer的數組在被setIntegerToObj中轉一次後也會被識別爲3個參數,但是使用setIntToObj中轉一次後卻被setObj識別爲了1個元素。

這裏會發現雖然對於可變參數我們使用數組作爲參數調用在方法內也可以正確使用,但是這只是針對一般對象或者基礎類型的包裝類。當我們直接使用setObj的時候三個數字會被自動裝箱成三個包裝類,而我將三個基礎類型拼裝爲數組後,setObj只會將其識別爲一個數組類型的參數。

增強的for循環

對於數據的循環,在開發中應該是使用最多的場景,最開始我們使用fori循環。的時候我們需要使用當前索引獲取當前目標的元素。但是使用新的For循環可以省去獲取當前元素的操作。

下面就是增強的for循環使用方法。

public class ForEachSugar {

    public void testForEach() {
        List<String> rest = new ArrayList<>();
        rest.add("one");
        rest.add("two");
        rest.add("three");
        for (String item : rest) {
            item = item + "test";
            System.out.println(item);
        }
    }
}

for括號中三個值分別爲(集合元素類型 當前的元素 : 集合)。而此循環底層使用的還是迭代器,對上面代碼編譯後可以看到下面內容

public class ForEachSugar {
    public ForEachSugar() {
    }

    public void testForEach() {
        List<String> rest = new ArrayList();
        rest.add("one");
        rest.add("two");
        rest.add("three");
        Iterator var2 = rest.iterator();

        while(var2.hasNext()) {
            String item = (String)var2.next();
            item = item + "test";
            System.out.println(item);
        }
    }
}

從編譯後的代碼可以看出來foreach其實就是使用了迭代器,只不過是簡化了迭代器的使用,變得更加簡單。這樣使得循環變得更加簡潔。

使用for循環需要注意的內容

使用迭代器的話,這個時候就需要注意,在循環中嘗試修改集合長度將會出現ConcurrentModificationException錯誤。但是對於元素爲對象的集合可以通過foreach修改其對象內屬性是沒有問題的。

ps.關於JAVA的循環,對於增強的for循環目前看起來的確可以提高循環的簡潔性,但是實際中我個人使用的不算多,因爲隨着下面的函數式接口Java提供了更加強大的對集合操作的支持。這裏我稍晚會單獨分出一部分來介紹Lambda和Stream結合起來的集合操作。

@FunctionalInterface(函數式接口)

爲什麼會有這個接口

Java是一種面向對象的語言,我們可以將對象作爲參數進行傳遞,但是我們不能將對象的方法作爲參數進行傳遞。而在Java Lambda的實現中,開發組不想再爲Lambda表達式單獨定義一種特殊的Structural函數類型(增加一個結構化的函數類型會增加函數類型的複雜性,破壞既有的Java類型,並對成千上萬的Java類庫造成嚴重的影響),因此最終還是利用SAM 接口作爲 Lambda表達式的目標類型。只要接口中只定義了唯一的抽象方法的接口那它就是一個實質上的函數式接口,就可以用來實現Lambda表達式。

對於這個接口我曾經有過一個學習筆記 函數式接口學習。當然對於當時的我並沒有意識到這個接口對Java後續的影響。

下面是對此接口的一個簡單使用。

// 接口
public interface UserFunction {

    User handleUser(User value);
}

public class FunctionalSugar {

    // Functional1,Functional2,Functional3都實現了UserFunction接口
    public void testFunctiona (int type) {
        User user = new User();
        switch (type) {
            case 1:
                handle(new Functional1()::handleUser,user);
                break;
            case 2:
                handle(new Functional2()::handleUser,user);
                break;
            default:
                handle(new Functional3()::handleUser,user);
                break;
        }
    }

    public void handle(UserFunction userFunction, User user) {
        userFunction.handleUser(user);
    }
}

函數式接口的影響

通過函數式接口使得我們將某個對象的方法作爲參數進行傳遞成爲可能。再此之前希望根據方法不同實現不同的操作這種業務,我們只能是使用Method類配合反射操作進行方法的調用,這種顯然有非常大的侷限性,並且有風險的。

方法引用

配合上面提到的函數式接口,Java提供了直接引用已有Java類或對象的方法的功能。

**可以引用那些方法 **

類型 語法 Lambda表達式
靜態方法 類::staticMethod (args) -> 類.staticMethod(args)
實例方法引用 實例::instMethod (args) -> 實例.instMethod(args)
構建方法 類::new (args) -> new 類(args)

方法引用的使用

每個類的方法根據其參數和結果都會返回其對應的函數式接口對象。

public class MethodReferencesSugar {

    public void testMethodReferences() {
        User user = new User("user","desc");
        BiConsumer<User, String> setDesc = User::setDesc;
        Consumer<String> setDesc1 = user::setDesc;
        Consumer<String> setName = user::setName;
    }
}

使用這種方式我們可以將對象中的方法隨意傳遞,更重要的是這種操作並不需要我們對之前的代碼進行任何修改。也不需要自己創建任何新的接口,這這要歸功於Java內置了大量@FunctionalInterface的接口。

在這裏插入圖片描述

方法引用帶來的變化

使用方法引用可以根據條件爲相同的參數提供不同的方法進行執行,比如下面內容我們可以將某個對象設置值的方法作爲
參數傳遞到某些公共方法中,在方法中我們只關注使用函數式方法設置什麼樣的內容,而無需關注給哪個對象設置哪個參數。這樣對於某些業務中會使我們的注意力用來關注在更加重要的內容上。

public class MethodReferencesSugar {

    public void testMethodReferences() {
        User user = new User("user","desc");
        BiConsumer<User, String> setDesc = User::setDesc;
        Consumer<String> setDesc1 = user::setDesc;
        setDesc1.accept("設置desc");
        Consumer<String> setName = user::setName;
        setName.accept("設置name");
    }
}

循環中的方法引用

Java本身對某些循環就提供了接收函數式參數的方法,而在這個時候我們可以直接調用一個方法的引用,而無需再設置參數的代碼。

    public static void testForEach3() {
        List<User> rest = new ArrayList<>();
        rest.add(new User("one","one"));
        rest.add(new User("two","two"));
        rest.add(new User("three","three"));
        rest.forEach(System.out::println);
    }

Lambda表達式

Lambda表達式,本人的看法,絕對是這幾年來Java最大的改進。配合Lambda表達式Java在嘗試將代碼變的簡潔和優雅的道路前進的更加迅速。Lambda表達式讓代碼更加簡潔,讓我們把注意力更加關注在業務上而不是枯燥的調用以及循環上, 。配合Lambda表達式,Java在方法調用、集合循環上都提供了更加優秀的功能。在代碼風格上,Lambda表達式成爲新舊代碼風格的分水嶺。

Lambda表達式的語法

Java中Lambda 表達式由三個部分組成

  1. 第一部分爲用逗號分隔的參數,外側使用()包裹(有些時候可以省略)
  2. 中間爲 ->
  3. 第三部分爲方法體,對於單行代碼可以直接輸入其內容,對於多行代碼可以使用{}包裹

最終使用方式類似下面的行爲

(參數(如果有的話)) -> {代碼塊} 

什麼時候使用Lambda表達式

Lambda表達式本質上是對函數式接口的的使用,所以當一個方法接收的參數爲函數式接口的時候,調用此類方法的時候其函數式接口的位置就可以使用Lambda表達式

循環中Lambda的使用

Java中使用forEach和stream都接收一個函數式接口作爲參數,所以在循環的時候都可以使用Lambda表達式。

    public static void testForEach3() {
        List<User> rest = new ArrayList<>();
        rest.add(new User("one","one"));
        rest.add(new User("two","two"));
        rest.add(new User("three","three"));
        rest.forEach(item -> {
            System.out.println(item.getDesc());
        });
    }

    public static void testForEach4() {
        HashMap<String,User> rest = new HashMap<>();
        rest.put("one",new User("one","one"));
        rest.put("two",new User("two","two"));
        rest.put("three",new User("three","three"));
        rest.forEach((k,v) -> {
            System.out.println(k);
            System.out.println(v.getDesc());
        });
    }

自定義函數接口的使用

只要接口形式符合函數式接口的定義都可以識別爲函數式接口,所以當一個接口實現了其規則,也可以通過Lambda表達式來直接實現其業務。就像下面內容

// 函數式接口
public interface UserFunction {

    User handleUser(User value);
}
// 具體使用
public class FunctionalSugar {

    // Functional1,Functional2,Functional3都實現了UserFunction接口
    public void testFunctiona (int type) {
        User user = new User();
        // 聲明一個函數式接口實現
        UserFunction userFunction = value -> {value.setDesc("");return value;};
        switch (type) {
            case 1:
                handle(userFunction,user);
                break;
            case 2:
                // 一個匿名的函數式接口
                handle(value -> {value.setDesc("");return value;},user);
                break;
            default:
                handle(new Functional3()::handleUser,user);
                break;
        }
    }
    // 支持函數式接口的參數
    public void handle(UserFunction userFunction,User user) {
        userFunction.handleUser(user);
    }
}

Lambda表達式和匿名內部類的區別

從上面例子可以看出來Lambda表達式很類似匿名內部類。就是對接口的直接實現。但是匿名內部類和Lambda實現的類在this指代的上下文是不一樣的。Lambda中的this指向的是外部,對於上面的例子中,在Lambda表達式中的代碼塊中使用this可以調用FunctionalSugar的方法屬性,而假如使用實現相關接口,其this則指向自身的引用。

不要過度使用Lambda表達式

我們使用Lambda表達式主要是爲了讓代碼更加簡潔,關注我們需要關注的內容。所以當需要實現的業務邏輯比較多的時候,我們需要將邏輯單獨抽出來使用。比如下面業務中,在Lambda代碼塊中可能需要嵌套大量邏輯,此時就不需要強行在方法中寫這麼多內容。可以單獨寫一個方法使用,亦或者在有些場景下,多個Lambda表達式放在一起代碼可能被簡化了很多,但是其理解難度也會提高。

public class FunctionalSugar {

    public void testFunctiona () {
        User user = new User();
        handle(value -> {
            value.setDesc("");
            // 業務2
            // 業務3
            // 業務4
            // ......
            // 業務n
            return value;},user);
    }

    public void handle(UserFunction userFunction,User user) {
        userFunction.handleUser(user);
    }

}
public class FunctionalSugar {

    public void testFunctiona () {
        User user = new User();
        handle(value -> someThings(value),user);
    }

    public void handle(UserFunction userFunction,User user) {
        userFunction.handleUser(user);
    }

    public User someThings(User user) {
        user.setDesc("");
        // 業務2
        // 業務3
        // 業務4
        // ......
        // 業務n
        return user;
    }

}


個人水平有限,上面的內容可能存在沒有描述清楚或者錯誤的地方,假如開發同學發現了,請及時告知,我會第一時間修改相關內容。假如我的這篇內容對你有任何幫助的話,麻煩給我點一個贊。你的點贊就是我前進的動力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章