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

相关文章
相关标签/搜索