程式設計師必備基礎:改善Java程式的20個實用建議

初念初戀 發表於 2021-10-22
Java 程式設計師

最近細讀了秦小波老師的《編寫高質量程式碼改善Jaav程式的151個建議》,要說是151個建議,其實更合適的說是避免Java的一些冷門的坑,下面整理了20個比較有趣的建議重新學習了一遍。

三元操作符的型別務必一致

三元操作符運算也稱為三目運算,其表示式形如:"條件表示式 ? 表示式1 : 表示式2",在大部分語言中都有這樣的三元操作符,其目的就是為了簡化if-else,當條件表示式為真時執行表示式1,否則執行表示式2。 來分析一下下面這段程式碼:

public static void main(String[] args){
    int i = 80;
    String s = String.valueOf(i < 100 ? 80 : 100);
    String s1 = String.valueOf(i < 100 ? 80 : 100.0);
    boolean equals = s.equals(s1);
    // 兩者是否相等:false, 字串s的值:80, 字串s1的值:80.0
    System.out.println("兩者是否相等:" + equals);
    System.out.println("字串s的值:" + s);
    System.out.println("字串s1的值:" + s1);
}

說明:如果三目運算子的型別不一致,返回的結果也不一致。

避免帶有變長引數的方法過載

public class Client {

    private static final Logger log = LoggerFactory.getLogger(Client.class);

    public static void main(String[] args) {
        Client client = new Client();
        client.calPrice(5000, 80);
    }

    /**
     * calPrice 簡單折扣計算
     *
     * @param price    價格
     * @param discount 折扣
     * @description
     * @author luokangyuan
     * @date 2019-4-2 14:58
     * @version 1.0.0
     */
    private void calPrice(int price, int discount) {
        float knockdownPrice = price * discount / 100.0F;
        log.info("簡單的折扣後的價格:{}", knockdownPrice);
    }

    /**
     * calPrice
     *
     * @param price     價格
     * @param discounts 折扣
     * @description 複雜折扣計算
     * @author luokangyuan
     * @date 2019-4-2 15:08
     * @version 1.0.0
     */
    private void calPrice(int price, int... discounts) {
        float knockdownPrice = price;
        for (int discount : discounts) {
            knockdownPrice = knockdownPrice * discount / 100;
        }
        log.info("複雜折扣後的價格:{}", knockdownPrice);
    }
}

說明:方法過載就是方法名相同,引數型別或者引數數量不同,在上述例子中Java編輯器會根據方法簽名找到合適的合適的方法,上述測試呼叫的就是簡單的折扣計算,而非複雜折扣計算。

不要只替換一個類

public class Constant{
    public final static int MAX_AGE = 150;
}

public class Client{
    public static void main(String[] args){
        System.out.println("人類壽命極限是:" + Constant.MAX_AGE);
    }
}

對於final修飾的基本型別和String型別,編譯器會認為它是穩定態的(Immutable Status)所以在編譯時就直接把值編譯到位元組碼中了,避免了在執行期引用(Run-time Reference),以提高程式碼的執行效率。對於我們的例子來說,Client類在編譯時位元組碼中就寫上了"150",這個常量,而不是一個地址引用,因此無論你後續怎麼修改常量類,只要不重新編譯Client類,輸出還是照舊。

對於final修飾的類(即非基本型別),編譯器會認為它不是穩定態的(Mutable Status),編譯時建立的則是引用關係(該型別也叫作Soft Final)。如果Client類引入的常量是一個類或例項,及時不重新編譯也會輸出最新值。

用偶判斷,不用奇判斷

String s = n % 2 == 1 ? "奇數" : "偶數";
String s1 = n % 2 == 0 ? "偶數" : "奇數";

說明:通常使用第二種偶數判斷,使用第一種的話。-1也會被判斷為偶數。

用整數型別處理貨幣

// 0.40000000000000036
System.out.println(10.00 - 9.60);

說明:Java中的浮點數是不準確的,在處理貨幣上使用浮點數就會存在問題,因此使用BigDecimal,類來進行計算,或者使用整數,將需要計算的數放大100倍,計算後在縮小。

1、使用BigDecimal

BigDecimal是專門為彌補浮點數無法精確計算的缺憾而設計的類,並且它本身也提供了加減乘除的常用數學演算法。特別是與資料庫Decimal型別的欄位對映時,BigDecimal是最優的解決方案。

2、使用整型

把參與運算的值擴大100倍,並轉為整型,然後在展現時再縮小100倍,這樣處理的好處是計算簡單,準確,一般在非金融行業(如零售行業)應用較多。此方法還會用於某些零售POS機,他們輸入和輸出的全部是整數,那運算就更簡單。

使用String直接賦值

public static void main(String[] args) {
    String str1 = "China";
    String str2 = "China";
    String str3 = new String("China");
    String str4 = str3.intern();

    System.out.println(str1 == str2); // true

    System.out.println(str1 == str3); // false

    System.out.println(str1 == str4); // true
}

說明:建議使用String str1 = "China";這中方式對字串賦值,而不是通過new String("China");這種方式,在Java中會給定義的常量存放在一個常量池中,如果池中存在就不會在重複定義,所以str1 == str2返回truenew出的是一個物件,不會去檢查字串常量池是否存在,所以str1 == str3是不同的引用,返回false。經過intern()處理後,返回true,是因為intern()會去物件常量池中檢查是否存在字面上相同得引用物件。

asList產生的list不可修改

private static void arraysTest() {
    String[] week = {"Mon", "Tue", "Wed", "Thu"};
    List<String> strings = Arrays.asList(week);
    strings.add("Fri");
}

說明:執行報錯,asList產生的list不可修改。

別讓null值和空值威脅到變長方法

public void countSum(String type, Integer... is){}

public void countSum(String type, String... strs){}

public static void main(String[] args) {
    ClientReload clientReload = new ClientReload();
    clientReload.countSum("China", 0);
    clientReload.countSum("China", "People");
    // 編譯報錯
    clientReload.countSum("China");
    // 編譯報錯
    clientReload.countSum("China", null);
}

說明:同樣是含有變長引數的過載方法,外部呼叫的使用使用NULL或者空值都會出現編譯不通過的錯誤,這是應為NULL和空值在上述過載的方法中都滿足引數條件,所以編譯器不知道調什麼方法,在過載方法的設計中違反了KISS原則,同時在外部呼叫的時候,外部呼叫這隱藏了實參的型別,如將呼叫程式碼做如下修改,就不存在編譯報錯了。

String strs = null;
clientReload.countSum("China", strs);

警惕自增的陷阱

public static void main(String[] args) {
    int count = 0;
    for (int i = 0; i < 100; i++) {
        int i1 = count++;
        count = i1;
        System.out.println("每次count++的值:" + i1);
    }
    System.out.println(count);
}

說明:結果是0,而不是我們100,這是count++是一個表示式,返回的是自加之前count的值。

break不可忘記

public static void main(String[] args) {
    String s = toChineseNumber(2);
    log.info("轉換結果:{}", s);
}

private static String toChineseNumber(int n) {
    String chineseNumber = "";
    switch (n) {
        case 0 : chineseNumber = "零";
        case 1 : chineseNumber = "壹";
        case 2 : chineseNumber = "貳";
        case 3 : chineseNumber = "叄";
        case 4 : chineseNumber = "肆";
        case 5 : chineseNumber = "伍";
        case 6 : chineseNumber = "陸";
        case 7 : chineseNumber = "柒";
        case 8 : chineseNumber = "捌";
        case 9 : chineseNumber = "玖";
        case 10 : chineseNumber = "拾";

    }
    return chineseNumber;
}

說明:在switchbreak一定不能少。

不要讓型別悄悄轉換

/** 光速*/
private static final int LIGHT_SPEED = 30 * 10000 * 1000;

public static void main(String[] args) {
    long dis = LIGHT_SPEED * 60 * 8;
    // -2028888064
    System.out.println(dis);
}

說明:LIGHT_SPEED * 60 * 8計算後是int型別,可能存在越界問題,雖然,我們在程式碼中寫了轉換為Long型,但是,在Java中是先運算後在進行型別轉換的,也就是LIGHT_SPEED * 60 * 8計算後是int型,超出了長度,從頭開始,所以為負值,修改為顯示的定義型別。如下:

/** 光速*/
private static final long LIGHT_SPEED = 30L * 10000 * 1000;

public static void main(String[] args) {
    long dis = LIGHT_SPEED * 60 * 8;
    System.out.println(dis);
}

避免帶有變長引數的方法過載

public class MainTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println(PriceTool.calPrice(12, 1)); // 1
    }
}
 
class PriceTool {
    public static int calPrice(int price, int discount) {
        return 1;
    }
    public static int calPrice(int price, int... discount) {
        return 2;
    }
}

說明:編譯器會從最簡單的開始猜想,只要符合編譯條件的即採用。

少用靜態匯入

import static java.lang.Math.PI;

public double calCircleArea(double r) {
    return Math.PI * r * r;
}

public double calBallArea (double r) {
    return 4 * PI * r * r;
}

說明:靜態匯入可以減少程式碼的量,但不易於閱讀,可讀性差。

提防包裝型別的null值

public static int f(List<Integer> list){
    int count = 0;
    for (Integer i : list){
        count += (i != null) ? i : 0;
    }
    return count;
}

說明:包裝物件和拆箱物件可以自由轉換,這不假,但是要剔除null值,null值並不能轉換為基本型別。對於此問題,我們謹記一點:包裝型別參與運算時,要做null值校驗。

謹慎包裝型別的大小比較

舉個例子。i==j false。Integer是引用型別。

public static void main(String[] args){
    Integer i = new Integer(100);
    Integer j = new Integer(100);
    System.out.println(i == j);
}

避免instanceof非預期結果

instanceof是一個簡單的二元操作符,它是用來判斷一個物件是否是一個類例項的,兩側操作符需要有繼承或實現關係。

public static void main(String[] args) {
    // String物件是否是Object的例項 - true,"String"是要給字串,字串繼承了Object,當然是Object的例項。
    boolean b1 = "String" instanceof Object;
    // String物件是否是String類的例項 - true,一個類的物件當然是一個類的例項。
    boolean b2 = new String() instanceof String;
    // Object物件是否是String的例項,編譯報錯,Object是父類。
    boolean b3 = new Object() instanceof String;
    // 拆箱型別是否是裝箱型別的例項,編譯報錯,“A”是一個Char型,是一個基本型別,instanceof只能用於物件判斷。
    boolean b4 = "A" instanceof Character;
    // 空物件是否是String的例項 - false,instanceof的規則,左邊為null,無論右邊什麼型別,都返回false。
    boolean b5 = null instanceof String;
    // 型別轉換後的空物件是否是String的例項 - false,null可以說是沒有型別,型別轉換後還是null。
    boolean b6 = (String) null instanceof String;
    // Date物件是否是String的例項,編譯報錯,Date類和String類沒有繼承關係
    boolean b7 = new Date() instanceof String;
}

不要隨便設定隨機數種子

在Java中,隨機數的產生取決於種子,隨機數和種子之間的關係遵從以下兩個原則: 種子不同,產生不同的隨機數 ;種子相同,即使例項不同也產生相同的隨機數。

public static void main(String[] args)
{
    Random r = new Random();
    for (int i = 1; i < 4; i++)
    {
        System.out.println("第" + i + "次:" + r.nextInt());

    }
}

執行結果:
第1次:846339168
第2次:756051503
第3次:1696875906

程式啟動後,生成的隨機數會不同。但是每次啟動程式,生成的都會是三個隨機數。產生隨機數和種子之間的關係如下:

1)種子不同,產生不同的隨機數。

2)種子相同,即使例項不同也產生相同的隨機數。

Random的預設種子(無參構造)是System.nanoTime()的返回值(jdk1.5以前是System.currentTimeMillis()),這個值是距離某一個固定時間點的納秒數,不同的作業系統和硬體有不同的固定時間點,隨機數自然也就不同了。

避免在建構函式中初始化其它類

public class Client35 {
    public static void main(String[] args) {
        Son son = new Son();
        son.doSomething();
    }
}

// 父類
class Father {
    public Father() {
        new Other();
    }
}

// 相關類
class Other {
    public Other() {
        new Son();
    }
}

// 子類
class Son extends Father {
    public void doSomething() {
        System.out.println("Hi, show me Something!");
    }
}

說明:造成構造方法迴圈呼叫。

優先使用整型池

Integer快取了-128-127的Integer物件。所以通過裝箱(Integer.valueOf())獲得的物件可以複用。

 public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

效能考慮,首選陣列

private static int getSumForArray(int[] array) {
    int sum = 0;
    for (int i = 0; i < array.length; i++) {
        sum += array[i];
    }
    return sum;
}

private static int getSumForList(List<Integer> list) {
    return list.stream().mapToInt(Integer::intValue).sum();
}