一个老生常谈的问题:在 Java 中,如何使用 Map 给对象计数,例如统计字符串出现的次数?
姿势一:containsKey()
| |
或者:
| |
这是最容易想到的方法,然而这种方法至少有两个问题:
Integer 中的
value声明为final,无法修改,每次更新均会产生一个新的 Integer 对象过于频繁且不必要的查表,具体来说,在
map中不含有word的情况下会查两次表,分别是containsKey()、put()方法;在含有word的情况下会查三次表,分别为containsKey()、get()、put()方法
姿势二:get() / getOrDefault()
| |
Java 8 则可以用 getOrDefault():
| |
Integer 对象问题仍存在
改善了查表问题,不管
map中是否已经存在word,都只查两次表
姿势三:AtomicInteger / 自定义可变 int 封装类
| |
| |
利用 AtomicInteger,可以避免产生不必要的 Integer 对象。
也可以自己封装一个 MutableInt:
| |
AtomicInteger 是为线程安全设计的,可能有一定性能损失,但我实际测试发现,AtomicInteger 与自己封装类的性能表现几乎相同。
解决了 Integer 对象问题
改善了查表问题,每次只查两次表
姿势四:利用 put() 方法的返回值
Map 的 put() 方法返回的是之前对应键的值,如果不存在,则为 null。
利用这一点就可以只查表一次即完成自增了:
| |
每次仍产生一个冗余的 int 封装对象
只查表一次,大大提高效率
姿势五:Java 8 中的 merge() / compute()
| |
或者:
| |
也可以用 compute() 方法实现:
| |
虽然 Integer 对象问题仍存在,但只查一次表就实现了自增,Integer 自动拆箱装箱机制使代码非常简洁,如果使用 Java 8,毫无疑问代替姿势四。
当然 Integer 问题也是可以解决的,例如我们使用 AtomicInteger:
| |
只不过代码没那么简洁了,这样既解决了 Integer 对象问题,又减少了查表时间。
总结
以下是我个人测试得到的性能表现:
在字符串重复率较高的情况下,姿势三 > 姿势五 > 姿势二 > 姿势四 > 姿势一
在重复率较低的情况下,姿势五 > 姿势二 > 姿势四 > 姿势一 > 姿势三
基本符合预期。
姿势一表现一直都较差
姿势二中规中矩,很多人也确实就是这样写的
姿势三避免了创建冗余对象,在重复率较高的情况下表现非常好,甚至好于姿势五
姿势四则比较奇葩,看似做了优化,但产生的冗余对象拖了后腿,代码可读性也不如其它,不建议使用
姿势五不管重复率如何整体表现都很稳定,代码也很简洁,如果不是量很大,那么使用封装类相比 Integer 优势不明显
以上均不考虑线程安全。