JVM性能優化系列-(4) 編寫高效Java程序

JVM.jpg

4. 編寫高效Java程序

4.1 面向對象

構造器參數太多怎麼辦?

正常狀況下,若是構造器參數過多,可能會考慮重寫多個不一樣參數的構造函數,以下面的例子所示:java

public class FoodNormal {

    //required
    private final String foodName;//名稱
    private final int reilang;//熱量

    //optional
    private final int danbz;//蛋白質
    private final int dianfen;//澱粉
    private final int zf;//脂肪
    
    //全參數
    public FoodNormal(String foodName, int reilang, int danbz, 
            int dianfen, int zf, int tang, int wss) {
        super();
        this.foodName = foodName;
        this.reilang = reilang;
        this.danbz = danbz;
        this.dianfen = dianfen;
        this.zf = zf;
    }

    //2個參數
    public FoodNormal(String foodName, int reilang) {
        this(foodName,reilang,0,0,0,0,0);
    }
    
    //3....6個參數
    //
    
    public static void main(String[] args) {
        FoodNormal fn = new FoodNormal("food1",1200,200,0,0,300,100);
    }
}

可是問題很明顯:1.可讀性不好,特別是參數個數多,而且有多個相同類型的參數時;2.調換參數的順序,編譯器也不會報錯。git

針對這個兩個問題,一種選擇是 JavaBeans 模式,在這種模式中,調用一個無參數的構造函數來建立對象,而後調用 setter 方法來設置每一個必需的參數和可選參數。這種方法缺陷很明顯:排除了讓類不可變的可能性,而且須要增長工做以確保線程安全。github

推薦的方法是使用builder模式,該模式結合了可伸縮構造方法模式的安全性和JavaBean模式的可讀性。面試

下面的例子中,建立了一個內部類Builder用於接受對應的參數,最後經過Builder類將參數返回。後端

public class FoodBuilder {

    //required
    private final String foodName;
    private final int reilang;

    //optional
    private  int danbz;
    private  int dianfen;
    private  int zf;
    
    public static class Builder{
        //required
        private final String foodName;
        private final int reilang;

        //optional
        private  int danbz;
        private  int dianfen;
        private  int zf;
        
        public Builder(String foodName, int reilang) {
            super();
            this.foodName = foodName;
            this.reilang = reilang;
        }
        
        public Builder danbz(int val) {
            this.danbz = val;
            return this;
        }
        
        public Builder dianfen(int val) {
            this.dianfen = val;
            return this;
        }
        
        public Builder zf(int val) {
            this.zf = val;
            return this;
        }
        
        public FoodBuilder build() {
            return new FoodBuilder(this);
        }
    }
    
    private FoodBuilder(Builder builder) {
        foodName = builder.foodName;
        reilang = builder.reilang;
        
        danbz = builder.danbz;
        dianfen = builder.danbz;
        zf = builder.zf;
    }
    
    public static void main(String[] args) {
        FoodBuilder foodBuilder = new FoodBuilder.Builder("food2", 1000)
            .danbz(100).dianfen(100).zf(100).build();
    }
}

Builder模式更進一步api

標準的Builder模式,包含如下4個部分:數組

  1. 抽象建造者:通常來講是個接口,1)建造方法,建造部件的方法(不止一個);2)返回產品的方法
  2. 具體建造者:繼承抽象建造者,而且實現相應的建造方法。
  3. 導演者:調用具體的建造者,建立產品對象。
  4. 產品:須要建造的複雜對象。

下面的例子中,man和woman是產品;personBuilder是抽象建造者;manBuilder和womanBuilder繼承了personBuilder並實現了相應方法,是具體建造者;NvWa是導演者,調用建造者方法建造產品。安全

產品類多線程

public abstract class Person {

    protected String head;
    protected String body;
    protected String foot;

    public String getHead() {
        return head;
    }

    public void setHead(String head) {
        this.head = head;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public String getFoot() {
        return foot;
    }

    public void setFoot(String foot) {
        this.foot = foot;
    }
}

// 具體的產品
public class Man extends Person {
    public Man() {
        System.out.println("create a man");
    }

    @Override
    public String toString() {
        return "Man{}";
    }
}

public class Woman extends Person {

    public Woman() {
        System.out.println("create a Woman");
    }

    @Override
    public String toString() {
        return "Woman{}";
    }
}

抽象建造類框架

public abstract class PersonBuilder {
    
    //建造部件
    public abstract void buildHead();
    public abstract void buildBody();
    public abstract void buildFoot();
    
    public abstract Person createPerson();

}

具體建造者

public class ManBuilder extends PersonBuilder {
    
    private Person person;
    
    public ManBuilder() {
        this.person = new Man();
    }

    @Override
    public void buildHead() {
        person.setHead("Brave Head");
        
    }

    @Override
    public void buildBody() {
        person.setBody("Strong body");
        
    }

    @Override
    public void buildFoot() {
        person.setFoot("powful foot");
        
    }

    @Override
    public Person createPerson() {
        return person;
    }
}

public class WomanBuilder extends PersonBuilder {
    
    private Person person;
    
    public WomanBuilder() {
        this.person = new Woman();
    }

    @Override
    public void buildHead() {
        person.setHead("Pretty Head");
        
    }

    @Override
    public void buildBody() {
        person.setBody("soft body");
        
    }

    @Override
    public void buildFoot() {
        person.setFoot("long white foot");
        
    }

    @Override
    public Person createPerson() {
        return person;
    }
}

導演者

public class NvWa {
    
    public Person buildPerson(PersonBuilder pb) {
        pb.buildHead();
        pb.buildBody();
        pb.buildFoot();
        return pb.createPerson();
    }
}

下面是測試程序:

public class Mingyun {

    public static void main(String[] args) {
        System.out.println("create NvWa");
        NvWa nvwa =  new NvWa();
        nvwa.buildPerson(new ManBuilder());
        nvwa.buildPerson(new WomanBuilder());
    }
}

不須要實例化的類應該構造器私有

工程中的工具類,爲了防止實例化,能夠將構造器私有化。

不要建立沒必要要的對象

1. 自動裝箱和拆箱等隱式轉換。

自動裝箱就是Java自動將原始類型值轉換成對應的對象,好比將int的變量轉換成Integer對象,這個過程叫作裝箱,反之將Integer對象轉換成int類型值,這個過程叫作拆箱。由於這裏的裝箱和拆箱是自動進行的非人爲轉換,因此就稱做爲自動裝箱和拆箱。

自動裝箱和拆箱在Java中很常見,好比咱們有一個方法,接受一個對象類型的參數,若是咱們傳遞一個原始類型值,那麼Java會自動將這個原始類型值轉換成與之對應的對象。

自動裝箱的弊端:

Integer sum = 0;
 for(int i=1000; i<5000; i++){
   sum+=i;
}

上面的例子中,首先sum進行自動拆箱操做,進行數值相加操做,最後發生自動裝箱操做轉換成Integer對象。上面的循環中會建立將近4000個無用的Integer對象,在這樣龐大的循環中,會下降程序的性能而且加劇了垃圾回收的工做量。

2. 實例共用,聲明爲static

多個共用的狀況下,聲明爲static或者採用單例模式,以避免生成多個對象影響程序性能。

避免使用終結方法

finalize方法,由於虛擬機不保證這個方法被執行,因此釋放資源時,不能保證。

爲了合理的釋放資源,推薦下面兩種方法:

  • try resource

Java 1.7中引入了try-with-resource語法糖來打開資源,而無需本身書寫資源來關閉代碼。例子以下:

public class TryWithResource {
    public static void main(String[] args) {
        try (Connection conn = new Connection()) {
            conn.sendData();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

爲了可以配合try-with-resource,資源必須實現AutoClosable接口。該接口的實現類須要重寫close方法:

public class Connection implements AutoCloseable {
    public void sendData() {
        System.out.println("正在發送數據");
    }
    @Override
    public void close() throws Exception {
        System.out.println("正在關閉鏈接");
    }
}
  • try finally

在finally語句塊中釋放資源,保證資源永遠可以被正常釋放。

使類和成員的可訪問性最小化

能夠有效的解除系統中各個模塊的耦合度、實現每一個模塊的獨立開發、使得系統更加的可維護,更加的健壯。

如何最小化類和接口的可訪問性?

  1. 能將類和接口作成包級私有就必定要作成包級私有的。

  2. 若是一個類或者接口,只被另外的一個類應用,那麼最好將這個類或者接口作成其內部的私有類或者接口。

如何最小化一個了類中的成員的可訪問性?

  1. 首先設計出該類須要暴露出來的api,而後將剩下的成員的設計成private類型。而後再其餘類須要訪問某些private類型的成員時,在刪掉private,使其變成包級私有。若是你發現你須要常常這樣作,那麼就請你從新設計一下這個類的api。

  2. 對於protected類型的成員,做用域是整個系統,因此,能用包訪問類型的成員的話就儘可能不要使用保護行的成員。

  3. 不能爲了測試而將包中的類或者成員變爲public類型的,最多隻能設置成包級私有類型。

  4. 實例域絕對不能是public類型的.

使可變性最小化

不可變類只是實例不能被修改的類。每一個實例中包含的全部信息都必須在建立該實例的時候就提供,並在對象的整個生命週期(lifetime)內固定不變。

Java平臺類庫中包含許多不可變的類,其中有String、基本類型的包裝類、BigInteger和BigDecimal。

存在不可變的類有許多理由:不可變的類比可變的類更加易於設計、實現和使用。不容易出錯,且更加安全。

爲了使類成爲不可變,要遵循下面五條規則:

一、不要提供任何會修改對象狀態的方法(也稱爲mutator),即改變對象屬性的方法。

二、保證類不會被擴展。這樣能夠防止粗心或者惡意的子類僞裝對象的狀態已經改變,從而破壞該類的不可變行爲。爲了防止子類化,通常作法是使這個類成爲final的。

三、使全部的域都是final的。經過系統的強制方式,這能夠清楚地代表你的意圖。並且,若是一個指向新建立實例的引用在缺少同步機制的狀況下,從一個線程被傳遞到另外一個 線程,就必須確保正確的行爲。

四、使全部的域都成爲私有的。這樣能夠防止客戶端得到訪問被域引用的可變對象的權限,並防止客戶端直接修改這些對象。雖然從技術上講,容許不可變的類具備公有的final 域,只要這些域包含基本類型的值或者指向不可變對象的引用,可是不建議這樣作,由於這樣會使得在之後的版本中沒法再改變內部的表示法。

五、確保對於任何可變組件的互斥訪問。若是類具備指向可變對象的域,則必須確保該類的客戶端沒法得到指向這些對象的引用。而且,永遠不要用客戶端提供的對象引用初始化這樣的域,也不要從任何訪問方法(accessor)中返回該對象引用。在構造器、訪問方法和readObject中請使用保護性拷貝(defensive copy)技術。

優先使用複合

  • 繼承:會打破封裝性
  • 組合:在內部持有一個父類做爲成員變量。

使用繼承擴展一個類很危險,父類的具體實現很容易影響子類的正確性。而複合優先於繼承告訴咱們,不用擴展示有的類,而是在新類中增長一個私有域,讓它引用現有類的一個實例。這種設計稱爲複合(Composition)。

只有當子類和超類之間確實存在父子關係時,才能夠考慮使用繼承。不然都應該用複合,包裝類不只比子類更加健壯,並且功能也更增強大。

接口優於抽象類

接口和抽象類

  • 抽象類容許某些方法的實現,可是接口不容許(JDK 1.8開始已經能夠了)
  • 現有類必須成爲抽象類的子類,可是隻能單繼承,接口能夠多繼承

接口優勢

  • 現有類能夠很容易被更新,以實現新的接口。
  • 接口容許咱們構造非層次結構的類型框架,接口能夠多繼承。
  • 骨架實現類,下面對骨架類進行詳細介紹

假定有Interface A, 能夠聲明abstarct class B implements A,接着在真正的實現類C中 class C extends B implements A。B就是所謂的骨架類,骨架類中對A中的一些基礎通用方法進行了實現,使得C能夠直接使用骨架類中的實現,無需再次實現,或者調用骨架類中的實現進行進一步的定製與優化。C只須要實現B中未實現的方法或者添加新的方法。

骨架實現類的優勢在於,它們提供抽象類的全部實現的幫助,而不會強加抽象類做爲類型定義時的嚴格約束。對於具備骨架實現類的接口的大多數實現者來講,繼承這個類是顯而易見的選擇,但它不是必需的。若是一個類不能繼承骨架的實現,這個類能夠直接實現接口。該類仍然受益於接口自己的任何默認方法。此外,骨架實現類仍然能夠協助接口的實現。實現接口的類能夠將接口方法的調用轉發給繼承骨架實現的私有內部類的包含實例。這種被稱爲模擬多重繼承的技術,它提供了多重繼承的許多好處,同時避免了缺陷。

JDK的實現中,使用了大量的骨架類,按照慣例,骨架實現類被稱爲AbstractInterface,其中Interface是它們實現的接口的名稱。 例如,集合框架( Collections Framework)提供了一個框架實現以配合每一個主要集合接口:AbstractCollection,AbstractSet,AbstractList和AbstractMap。

4.2 方法

可變參數要謹慎使用

從Java 1.5開始就增長了可變參數(varargs)方法,又稱做variable arity method。可變參數方法接受0個或多個指定類型的參數。它的機制是先建立一個數組,數組的大小爲調用位置所傳遞的參數數量,而後將值傳到數組中,最後將數組傳遞到方法。

例以下面的例子,返回多個參數的和:

// Simple use of varargs - Page 197
    static int sum(int... args) {
        int sum = 0;
        for (int arg : args)
            sum += arg;
        return sum;
    }

可是這種方法也接受0個參數,因此通常須要對參數進行檢查。一般爲了規避這種狀況,就是聲明該方法有兩個參數,一個是指定類型的正常參數,另外一個是這種類型的varargs參數。這個方法彌補了上面的不足(不須要再檢查參數的數量了,由於至少要傳遞一個參數,不然不能經過編譯):

static int min(int firstArg, int... remainingArgs) {
        int min = firstArg;
        for (int arg : remainingArgs)
            if (arg < min)
                min = arg;
        return min;
    }

須要注意的是,在重視性能的狀況下,使用可變參數機制要特別當心。可變參數方法每次調用都會致使進行一次數組分配和初始化。

返回零長度的數組或集合,不要返回null

要求調用方單獨處理null的狀況。對於list的狀況,能夠直接返回jdk內置的Collections.emptyList()。

優先使用標準的異常

  • 可讀性。
  • 追求代碼的重用。
  • 在類裝載的性能上考慮,也提倡使用標準異常。

經常使用異常:

illegalArgumentException --- 調用者傳遞的參數不合適
illegalStateException --- 接收狀態異常
NullPointException --- 空指針
UnSupportOperationException -- 操做不支持

4.3 通用程序設計

用枚舉代替int常量

在枚舉類型出現以前,通常都經常使用int常量或者String常量表示列舉相關事物。如:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

針對int常量如下不足:

  1. 在類型安全方面,若是你想使用的是ORANGE_NAVEL,可是傳遞的是APPLE_FUJI,編譯器並不能檢測出錯誤;
  2. 由於int常量是編譯時常量,被編譯到使用它們的客戶端中。若與枚舉常量關聯的int發生了變化,客戶端需從新編譯,不然它們的行爲就不肯定;
  3. 沒有便利方法將int常量翻譯成可打印的字符串。這裏的意思應該是好比你想調用的是ORANGE_NAVEL,debug的時候顯示的是0,但你不能肯定是APPLE_FUJI仍是ORANGE_NAVEL

1. 默認枚舉

上面的例子可使用下面的enum重寫:

public enum Apple {
    APPLE_FUJI,
    APPLE_PIPPIN,
    APPLE_GRANNY_SMITH;
}

在調用的時候,直接使用enum類型,在編譯的時候能夠直接指定類型,不然編譯不經過;而且debug的時候,顯示的是enum中的常量(APPLE_FUJI這樣的),能夠一眼看出是否用錯;最後因爲枚舉導出的常量域(APPLE_FUJI等)與客戶端之間是經過枚舉來引用的,再增長或者重排序枚舉類型中的常量後,並不須要從新編譯客戶端代碼。

2. 帶行爲的枚舉

首先必須明白,java裏的枚舉就是一個類,枚舉中的每一個對象,是這個枚舉類的一個實例。

所以咱們能夠編寫下面的枚舉類,而且提供相應的計算方法。

public enum DepotEnum {
    UNPAY(0,"未支付"),PAID(1,"已支付"),TIMOUT(-1,"超時");
    
    private int status;
    private String desc;
    private String dbInfo;//其餘屬性
    
    private DepotEnum(int status, String desc) {
        this.status = status;
        this.desc = desc;
    }

    public int getStatus() {
        return status;
    }

    public String getDesc() {
        return desc;
    }

    public String getDbInfo() {
        return dbInfo;
    }
    
    public int calcStatus(int params) {
        return status+params;
    }
    
    public static void main(String[] args) {
        for(DepotEnum e:DepotEnum.values()) {
            System.out.println(e+":"+e.calcStatus(14));
        }
    }
}

下面是比較複雜的枚舉,這裏在類裏面定義了枚舉BetterActive枚舉類,進行計算加減乘除的操做,爲了保證每增長一個枚舉類後,都增長對應的計算方法,這裏將計算方法oper定義爲抽象方法,保證了在增長枚舉變量時,必定增長對應的oper方法。

public class ActiveEnum {
    
    public enum BetterActive{
        PLUS {
            @Override
            double oper(double x, double y) {
                return x+y;
            }
        },MINUS {
            @Override
            double oper(double x, double y) {
                return x-y;
            }
        },MUL {
            @Override
            double oper(double x, double y) {
                return x*y;
            }
        },DIV {
            @Override
            double oper(double x, double y) {
                return x/y;
            }
        };
        
        abstract double oper(double x,double y);    
    }

    public static void main(String[] args) {
        System.out.println(BetterActive.PLUS.oper(0.1, 0.2));
    }
}

3. 策略枚舉

主要是爲了優化在多個枚舉變量的狀況下,儘可能減小重複代碼。下面以不一樣的日期,薪水的支付方式不一樣爲例,進行說明,當增長了一個新的日期後,咱們只須要在外層枚舉類中進行修改,無需修改其餘計算方法。

public enum BetterPayDay {
    MONDAY(PayType.WORK), TUESDAY(PayType.WORK), WEDNESDAY(
            PayType.WORK), THURSDAY(PayType.WORK), FRIDAY(PayType.WORK), 
    SATURDAY(PayType.REST), SUNDAY(PayType.REST),WUYI(PayType.REST);

    private final PayType payType;//成員變量

    BetterPayDay(PayType payType) {
        this.payType = payType;
    }

    double pay(double hoursOvertime) {
        return payType.pay(hoursOvertime);
    }

    //策略枚舉
    private enum PayType {
        WORK {
            double pay(double hoursOvertime) {
                return hoursOvertime*HOURS_WORK;
            }
        },
        REST {
            double pay(double hoursOvertime) {
                return hoursOvertime*HOURS_REST;
            }
        };
        
        private static final int HOURS_WORK = 2;
        private static final int HOURS_REST = 3;

        abstract double pay(double hoursOvertime);//抽象計算加班費的方法
    }
    
    public static void main(String[] args) {
        System.out.println(BetterPayDay.MONDAY.pay(7.5));
    }
}

將局部變量的做用域最小化

  • 要使局部變量的做用域最小化,最有力的方法就是在第一次使用它的地方聲明。
  • 幾乎每一個局部變量的聲明都應該包含一個初始化表達式。若是沒有足夠信息來對一個變量進行有意義的初始化,就應該推遲這個聲明,直到能夠初始化爲止。
  • 儘可能保證方法小而集中。
  • 僅在某一代碼塊中使用的局部變量,那麼就在該代碼塊中聲明。

精確計算,避免使用float和double

float和double類型不能用於精確計算,其主要目的是爲了科學計算和工程計算,它們執行二進制浮點運算。

轉成int或者long,推薦使用bigDecimal。

小心字符串鏈接的性能

String是不可變的,每一次拼接都會產生字符串的複製。

StringBuilder和StringBuffer

  • 都是可變的類。
  • StringBuffer線程安全,能夠在多線程下使用;StrngBuilder非線程安全,速度比StringBuffer快。

控制方法的大小

這個好理解,主要是從解耦和可維護性角度考慮。

在Unix philosophy中也提到,編寫代碼時注意Do One Thing and Do It Well


本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,馬上獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png

相關文章
相關標籤/搜索