改善 Java 程序的151个建议之性能与开源工具

一、提升Java性能的基本方法

1.不要在循环条件中计算

如果在循环条件中计算,则每循环一次就要遍历一次,这会降低系统的效率,例如如下代码:

while(i < count*2){
    //do something
}

应该修改为

int total = count * 2;
while(i < count){
    //do something
}

2.尽可能把变量、方法声明为final static类型

假设要将阿拉伯数字转化为中文数字,其定义如下:

public String toChineseNum(int num){
    //中文数字
    String[] cns = {"零","壹",...};
    return cns[num];
}

每次调用方法时都会重新生成一个cns数组,注意数组不会改变,属于不变数组,在这种情况下把它声明为类变量,并且加上final static修饰会更合适,有助于提高系统性能。

    //中文数字
final static String[] cns = {"零","壹",...};
public String toChineseNum(int num){
    return cns[num];
}

3.缩小变量的作用范围

关于变量,能定义在方法内的就定义在方法内,能定义在循环体内的就定义在循环体内,其目的是加快GC的回收

4.频繁字符串使用StringBuilder或StringBuffer

5.使用非线性检索

如果在ArrayList中存储了大量的数据,使用indexOf查找元素会比java.utils.Collections.binarySearch的效率低很多,原因是binarySearch是二分法搜索法,而indexOf使用的是逐个元素比对的方法,但是binarySearch搜索时必须先对元素进行排序

6.覆写Exception的fillInStackTrace方法

fillInStackTrace方法是用来记录异常时的栈信息的,这是一个非常耗时的动作,如果不关注栈信息,可以覆盖之,进而提升系统性能

二、若非必要,不要克隆对象

通过clone方法生成对象时,就会不再执行构造函数,只是内存中数据块的拷贝,但是一般情况下clone的性能要比new的性能好很多,原因如下:

  • 构造函数简单,JVM对new做了大量的性能优化

clone方法一般用于构造函数比较复杂的情况,对象属性比较多。

三、推荐使用Guava扩展工具包

说起Guava(石榴),可能知道它的读者并不多,要是说起Google-collections,相信大部分读者都有所耳闻。2008年Google发布了Google-collections扩展工具包,主要是对JDK的Collection包进行了扩展,2010年Google发布了Guava项目,其中包含了collections、caching、primitives support、concurrency libraries、common annotations、I/O等,这些都是项目编码中的基本工具包,我们大致浏览一下它的主要功能。

1.Collections

com. google.common.collect包中主要包括四部分:不可变集合(Immutable Collections)、多值Map、Table表和集合工具类。

不可变集合

不可变集合包括ImmutableList、ImmutableMap、ImmutableSet、ImmutableSortedMap、ImmutableSortedSet等,它比不可修改集合(Unmodifiable Collections)更容易使用,效率更高,而且占用的内存更少。示例代码如下:

//不可变列表
ImmutableList list = ImmutableList.of("A","B","C");
//不可变Map
ImmutableMap map = ImmutableMap.of(1,"壹",2,"贰",3,"叁");

其中的of方法有多个重载,其目的就是为了便于在初始化的时候直接生成一个不可变集合。

多值Map

多值Map比较简单,在JDK中,Map中的一个键对应一个值,在put一个键值对时,如果键重复了,则会覆盖原有的值,在大多数情况下这比较符合实际应用,但有的时候确实会存在一个键对应多个值的情况,比如我们的通讯录,一个人可能会对应两个或三个号码,此时使用JDK的Map就有点麻烦了。在这种情况下,使用Guava的Multimap可以很好地解决问题,代码如下:

//多值Map
MultimapphoneBook=ArrayListMultimap.create();
phoneBook.put("张三","110");
phoneBook.put("张三","119");
System.out.println(phoneBook.get("张三"));

输出的结果是一个包含两个元素的Collection,这是一种很巧妙的处理方式,可以方便地解决我们开发中的问题。

Table表

在GIS(Geographic Information System,地理信息系统)中,我们经常会把一个地点标注在一个坐标上,比如把上海人民广场标注在北纬31.23、东经121.48的位置上,也就是说只要给出了准确的经度和纬度就可以进行精确的定位——两个键决定一个值,这在Guava中是使用Table来表示的,示例代码如下:

Table g =HashBasedTable.create();
//定义人民广场的经纬度坐标
g.put(31.23,121.48,"人民广场");
//输出坐标点的建筑物
g.get(31.23,121.48);

其实Guava的Table类与我们经常接触的DBRMS表非常类似,可以认为它是一个没有Schema限定的数据表,比如:

//Table,完全类似于数据库表
Table user = HashBasedTable.create();
//第一行、第一列的值是张三
user.put(1,1,"张三");
//第一行、第二列的值是李四
user.put(1,2,"李四");
//第一行第一列是谁
user.get(1,1);
集合工具类

Guava的集合工具类分得比较细,比如Lists、Maps、Sets分别对应的是List、Map、Set的工具类,它们的使用方法比较简单,比如Map的过滤,如下所示。

//姓名、年龄键值对
Map user = new HashMap();
user.put("张三",20);
user.put("李四",22);
user.put("王五",25);
//所有年龄大于20岁的人员
Map filtedMap = Maps.flterValues(user,new Predicate(){
    public boolean apply(Integer_age){
        return_age>20;
    }
});

2.字符串操作

Guava提供了两个非常好用的字符串操作工具:Joiner连接器和Splitter拆分器。当然,字符串的连接和拆分使用JDK的方法也可以实现,但是使用Guava更简单一些,比如字符串的连接,代码如下所示:

//定义连接符号
Joiner joiner=Joiner.on(",");
//可以连接多个对象,不局限于String;如果有null,则跳过
String str=joiner.skipNulls().join("嘿","Guava很不错的。");
Map map = new HashMap();
map.put("张三","普通员工");
map.put("李四","领导");
//键值之间以"是"连接,多个键值以空格分隔
System.out.println(Joiner.on("\r\n").withKeyValueSeparator("是").join(map));

Joiner不仅能够连接字符串,还能够把Map中的键值对串联起来,比直接输出Map优雅了许多。Splitter是做字符拆分的,使用方法也比较简单,示例代码如下:

String str="你好,Guava";  
//以","中文逗号分隔  
for(String s : Splitter.on(",").split(str)){  
    System.out.println(s); 
}  
//按照固定长度分隔  
for(String s:Splitter.fixedLength(2).split(str)){  
    System.out.println(s);  
} 

注意fixedLength方法,它是按照给定长度进行拆分的,比如在进行格式化打印的时候,一行最大可以打印120个字符,此时使用该方法就非常简单了。

3.基本类型工具

基本类型工具在primitives包中,是以基本类型名+s的方式命名的,比如Ints是int的工具类,Doubles是double的工具类,注意这些都是针对基本类型的,而不是针对包装类型的。如下代码所示:

intints={10,9,20,40,80};
//从数组中取出最大值
System.out.println(Ints.max(ints));
List integers = new ArrayList();
//把包装类型的集合转为基本类型数组
ints = Ints.toArray(integers);

Guava还提供了其他操作(如I/O操作),相对来说功能不是非常强大,不再赘述,读者有兴趣可以自行下载源码研究一番。

四、Apache扩展包

Apache Commons通用扩展包基本上是每个项目都会使用的,只是使用的多少不同而已。

1.Lang

Apache Lang在开发中是最常使用的,更新频率很高

  • 字符串操作工具类:诸如StringUtils(基本的String操作类)、StringEscapeUtils(String的转义工具)、RandomStringUtils(随机字符串工具)等
  • Object工具类:覆写equals、hashCode,toString方法会用到
  • 可变的基本类型:包装类型的运算会用到
  • 其它Utils工具:日期方面主要提供DateUtils和DateFormatUtils两个工具类,没有Joda强大

2.BeanUtils

它是JavaBean的操作工具包,不仅可以实现属性的拷贝,转换等,还可以建立动态的Bean,甚至加你一些自由度很高的bean

  • 属性拷贝:PO与VO之间的转换
  • 动态Bean和自由Bean
  • 转换器

3.Collections

Collections工具包提供了ListUtils、MapUtils等基本集合操作工具

  • Bag:可以荣南重复元素,提供重复统计的功能
  • lazy系列
  • 双向Map:key和value都是唯一的

五、推荐使用Joda日期时间扩展包

开发一个项目必然要和日期时间打交道,特别是一些全球性的项目,必须要考虑语言和时区问题,但是在JDK中,日期时间的操作比较麻烦,例如1000小时后是星期几,伦敦时间是几点等,这里介绍一下通过Joda开源包来操作时间的方法,非常简单方便。

1.本地格式的日期时间

依据操作系统或指定的区域输出日期或时间,例如:

//当前时间戳
DateTime dt=new DateTime();
//输出英文星期
dt.dayOfWeek().getAsText(Locale.ENGLISH);
//本地日期格式
dt.toLocalDate();
//日期格式化
dt.toString(DateTimeFormat.forPattern("yyyy年M月d日"));

2.日期计算

这是Joda最方便的地方,也是JDK最麻烦的地方,比如我们要计算100天后是星期几,直接使用JDK提供的日期类会非常麻烦,使用Joda就简单很多,例如:

//当前时间戳
DateTime dt=new DateTime();
//加100小时是星期几
dt.plusHours(100).dayOfWeek();
//100天后的日期
dt.plusDays(100).toLocalDate();
//10年前的今天是星期几
dt.minusYears(10).dayOfWeek().getAsText();
//离地球毁灭还有多少小时
Hours.hoursBetween(dt, new DateTime("2012-12-21")).getHours();

这里需要注意的是,DateTime是一个不可变类型,与String非常类似,即使通过plusXXX、minusXX等方法进行操作,dt对象仍然不会变,只是新生成一个DateTime对象而已。但是,Joda也提供了一个可变类型的日期对象:MutableDateTime类,这样,日期的加减操作就更加方便了,比如列出10年内的黑色星期五,实现代码如下(此实现若用JDK的类来计算会异常复杂,读者可以尝试一下):

//当前可变时间
MutableDateTime mdt=new MutableDateTime();
//10年后的日期
DateTime destDateTime=dt.plusYears(10);
while(mdt.isBefore(destDateTime)){
    //循环一次加1天
    mdt.addDays(1);
    //是13号,并且是星期五
    if(mdt.getDayOfMonth() == 13 && mdt.getDayOfWeek()==5){
    //打印出10年内所有的黑色星期五
        System.out.println("黑色星期五:" + mdt);
    }
}

3.时区时间

这个比较简单实用,给定一个时区或地区代码即可计算出该时区的时间,比如在一个全球系统中,数据库中全部是按照标准时间来记录的,但是在展示层要按照不同的用户、不同的时区展现,这就涉及时区计算了,代码如下:

//当前时间戳
DateTime dt=new DateTime();
//此时伦敦市的时间
dt.withZone(DateTimeZone.forID("Europe/London"));
//计算出标准时间
dt.withZone(DateTimeZone.UTC);

Joda还有一个优点,它可以与JDK的日期库方便地进行转换,可以从java.util.Date类型转为Joda的DateTime类型,也可以从Joda的DateTime转为java.util.Date,代码如下:

DateTime dt=new DateTime();
//Joda的DateTime转为JDK的Date
Date jdkDate=dt.toDate();
//JDK的Date转为Joda的DateTime
dt=new DateTime(jdkDate);

经过这样的转换,Joda可以很好地与现有的日期类保持兼容,在需要复杂的日期计算时使用Joda,在需要与其他系统通信或写到持久层中时则使用JDK的Date。Joda是一种令人惊奇的高效工具,无论是计算日期、打印日期,或是解析日期,Joda都是首选,当然日期工具类也可以选择date4j,它也是一个不错的开源工具,这里就不再赘述了。

六、可以选择多种Collection扩展

为什么这么多的开源框架热衷于Collections的扩展呢?是因为我们程序(经典的定义:程序=算法+数据结构,想想看数据结构是为谁而服务的)主要处理的是一大批数据,而能容纳大量数据的也就是Collections类和数组了,但是数据的格式具有多样性,比如数据映射关系多样,数据类型多样等,下面我们再介绍三个比较有个性的Collections扩展工具包。

1.fastutil

fastutil(按照Java的拼写规则应该为FastUtil,但是官网就是这样命名的,我们尊重官方)是一个更新比较频繁的工具包,它的最新版本是6.3,主要提供了两种功能:一种是限定键值类型(Type Specific)的Map、List、Set等,另一种是大容量的集合。我们先来看示例代码:

//明确键类型的Map
Int2ObjectMapmap=new Int2ObjectOpenHashMap();
map.put(100,"A");
//超大容量的List,注意调整JVM的Heap内存
BigListbigList=new ObjectBigArrayBigList(
1L+Integer.MAX_VALUE);
//基本类型的集合,不再使用Integer包装类型
IntArrayList arrayList=new IntArrayList();

这里要特别注意的是大容量集合,什么叫大容量集合呢?我们知道一个Collection的最大容量是Integer的最大值(2 147 483 647),不能超过这个容量,一旦我们需要把一组超大的数据放到集合中,就必须要考虑对此进行拆分了,这会导致程序的复杂性提高,而fastutil则提供了Big系列的集合,它的最大容量是Long的最大值,这已经是一个非常庞大的数字了,超过这个容量基本上是不可能的。但在使用它的时候需要考虑内存溢出的问题,注意调节Java的mx参数配置。

2.Trove

Trove提供了一个快速、高效、低内存消耗的Collection集合,并且还提供了过滤和拦截的功能,同时还提供了基本类型的集合,示例代码如下:

//基本类型的集合,不使用包装类型
TIntList intList=new TIntArrayList();
//每个元素值乘以2
intList.transformValues(new TIntFunction(){
    public int execute(int element){
        return element*2;
    }
});
//过滤,把大于200的元素组成一个新的列表
TIntList t2=intList.grep(new TIntProcedure(){
    public boolean execute(int_element){
        return_element>200;
    }
});
//包装为JDK的List
Listlist=new TIntListDecorator(intList);
//键类型确定Map
TIntObjectMapmap=new TIntObjectHashMap();

Trove的最大优势是在高性能上,在进行一般的增加、修改、删除操作时,Trove的响应时间比JDK的集合少一个数量级,比fastutil也会高很多,因此在高性能项目中要考虑使用Trove。

3.lambdaj

lambdaj是一个纯净的集合操作工具,它不会提供任何的集合扩展,只会提供对集合的操作,比如查询、过滤、统一初始化等,特别是它的查询操作,非常类似于DBRMS上的SQL语句,而且也会提供诸如求和、求平均值等的方法,示例代码如下:

Listints=new ArrayList();
//计算平均值
Lambda.avg(ints);
//统计每个元素出现的次数,返回的是一个Map
Lambda.count(ints);
//按照年龄排序
Listpersons=new ArrayList();
Lambda.sort(persons, Lambda.on(Person.class).getAge()));
//串联所有元素的指定属性,输出为:张三,李四,王五
Lambda.joinFrom(persons).getName();
//过滤出年龄大于20岁的所用元素,输出为一个子列表
Lambda.select(persons, new BaseMatcher(){
    @Override
    public boolean matches(Object_person){
        Person p=(Person)_person;
        return p.getAge()>20;
    }
    public void describeTo(Description desc){
    }
});
//查找出最大年龄
Lambda.maxFrom(persons).getAge();
//抽取出所有姓名形成一个数组
Lambda.extract(persons, Lambda.on(Person.class).getName()));

lambdaj算是一个比较年轻的开源工具,但是它符合开发人员的习惯,对集合的操作提供了"One Line"式的解决方法,可以大大缩减代码的数量,而且也不会导致代码的可读性降低,读者可以在下一个项目中使用此类开源工具。

你可能感兴趣的:(java)