Java Rules 1
前言
最近走查了一些代码,发现很多同学写代码功能是写出来了,代码规范也ok,但是有很多坏味道,主要还是体现在可扩展性、性能方面、可复用等非功能方面,很早以前笔者也经历过这段时期,后来通过不断学习和看书也总结了一些小技巧和规则,这个系列的文章就把这些技巧和规则介绍给大家,其中很多技巧都是来自effective java,这本书中文版翻译的太烂,翻译的非常业余,这里我抽取了一些比较好的技巧用自己的专业性大白话给大家介绍下,本篇为系列第一篇,主要介绍了创建对象的一些规则和技巧。
1. 多构造函数参数使用builder模式
我们在开发过程中常会遇到一些类在创建的时候有很多属性,但是这些属性中有的是必输的,有的是非必输,但是我们为了能够创建这些对象会创建很多构造函数来覆盖这些属性初始化,如果后期一直增加属性那么构造函数会越来越多,后期就无法控制了。可以考虑使用Builder模式构建对象,调用类似setter的方法来设置每个相关的可选参数,调用无参的build来生成不可变的对象。
与使用多构造函数相比,builder模式的优势在于可以有多个可变的参数,builder可以通过单独的方法来设置每个参数,可以后期随意增加参数或减少参数,改动会很少。
在视线Builder模式的时候,我们发现需要在原先定义的实体类中,增加一个静态Builder类,Builder类中包含了所有的实体类字段,Builder的构造函数包含了实体类必输字段,Builder类中有所有实体类的字段的setter方法,但是和一般的setter方法不一样的地方在于,Builder中的所有setter返回对象为Builder类本身,这样就为我们后面实现函数式赋值做了铺垫。最后我们还需要一个build方法,来将Builder的实例赋值给原实体类,在原实体类中需要有一个参数为Builder的构造函数,在构造函数中将Buidler中所有的参数一一给实体类属性赋值。这样就实现了一个实体类的Builder化。
有以下几个关键点:
- 静态Builder类,包涵需要创建的实体类的所有字段属性
- Builder类的构造函数参数为实体类的必输属性
- 实体类需要有一个参数为Builder的构造函数,并在构造函数中将builder实例的所有值赋值给实体类属性
- Builder类需要有一个build方法,build方法将Builder类的实例作为参数进行构建实体类
NutritionFacts cocaCola = new NutritionFacts.Builder(1, 2).
calories(3).fat(4).sodium(5).carbohydrate(6).build();
public class NutritionFacts {
private final int servingSize; //(ml) required
private final int servings; //(per container) required
private final int calories; // optional
private final int fat; //(g) optional
private final int sodium; //(mg) optional
private final int carbohydrate; //(g) optional
public static class Builder{
//Required parameters
private final int servingSize;
private final int servings;
//Optional parameters - initialized to default values
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
public Builder(int servingSize, int servings){
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val){
calories = val;
return this;
}
public Builder fat(int val){
fat = val;
return this;
}
public Builder sodium(int val){
sodium = val;
return this;
}
public Builder carbohydrate(int val){
carbohydrate = val;
return this;
}
public NutritionFacts build(){
return new NutritionFacts(this);
}
}
public NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
2. 通过定义私有构造函数防止特殊类被实例化
在日常开发过程中,我们经常遇到一些类只有静态字段和静态方法,那么这些类就没有被实例化的必要,实例化了还会一直占用jvm内存空间,那么我们可以通过定义一个私有构造函数防止被实例化,这些类一般都是些工具类,如果为了编译时发现问题,可以在构造函数中throw出异常。
public class UtilityClass {
// 定义私有构造函数防止被无故实例化
private UtilityClass(){
throw new AssertionError();
}
public static int add(int a, int b){
return a + b;
}
public static int sub(int a, int b){
return a - b;
}
}
3. 避免创建不必要的对象
在日常开发过程中,我们会遇到一些类会被高频调用,如果这些类中存在一些每次函数调用都需要创建的对象,那么将这些对象最好定义为静态对象,只在类第一次初始化的时候进行定义,后续调用的时候都不去重复创建,这样如果调用次数很大,那么节约的内存空间和占用cpu的时间都会下降很多。
下面举个例子:
Person对象有一个isBabyBoomer方法,用于判断这个Person对象是不是在1946年到1965年之间出生,我们可以看到在这个方法中,每次调用都会创建Calendar对象gtmCal、Date对象boomStart、boomEnd,如果调用次数很多,那么内存中就会创建很多Calendar对象。
public class Person {
private final Date birthDate;
public Person(Date birthDate){
this.birthDate = new Date(birthDate.getTime());
}
// other fields,method,and constructor omitted
// DON'T DO THIS
public boolean isBabyBoomer(){
Calendar gtmCal = Calendar.getInstance(TimeZone.getTimeZone("GTM"));
gtmCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gtmCal.getTime();
gtmCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gtmCal.getTime();
return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
}
}
我可以将这几个重复创建的对象定义为静态类型,并进行统一初始化,这样这个类中的这些对象在内存中就会只有一份,将Person类改造如下:
public class Person {
private final Date birthDate;
// Defensive copy - see Item 39
public Person(Date birthDate) {
this.birthDate = new Date(birthDate.getTime());
}
// Other fields, methods
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gtmCal = Calendar.getInstance(TimeZone.getTimeZone("GTM"));
gtmCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gtmCal.getTime();
gtmCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gtmCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
}
}
这条规则在实际使用的时候要特别注意,只有当这个类的方法被高频调用的时候才值得将不必要重复创建的对象定义为静态,如果使用不是非常频繁,那么就没必要这么做,甚至对于一些使用频率非常低的对象,可以在使用的时候才加载,实现懒加载。
4. 覆盖euqals时一定要覆盖hashCode方法
在每一个覆盖了equals方法的类中也必须覆盖hashCode方法,如果不覆盖的话酒会违反hashCode的通用原则,会导致一些基于hash的集合类可能存在问题,例如HashMap、HashSet、HashTable。
如果两个对象通过equals方法调用后是相等的,那么这两个对象的hashCode也一定相等。如果两个对象equals不相等,那么他们的hashCode也一定不相等。
一个好的hashCode算法需要是能够将hash值均匀分布的,这里直接给出常用的算hashcode的公式:
result= 31 * result + a
如果有多个参数可以向下面这样叠加:
@Override
public int hashCode(){
int result = 17;
result = 31 * result + a;
result = 31 * result + b;
result = 31 * result + c;
return result;
}
上面介绍的hashCode的算法是比较简单的,并不推荐如上代码使用在企业级代码中,最好使用第三方库如Apache commons来生成hashocde方法,如下:
public int hashCode(){
HashCodeBuilder builder = new HashCodeBuilder();
builder.append(mostSignificantMemberVariable);
builder.append(leastSignificantMemberVariable);
return builder.toHashCode();
}