介绍瓜娃主要是觉得它是个老老实实的能帮助写代码的工具库. 不用把它当作正儿八经的什么东西学习 (说实话, 那里面的东西俺估计也就接触过两成了不起了), 把那个jar文件放到你的classpath里, 然后哪怕你就知道一个库函数呢, 对景的时候也是个帮手啊.
比如, 最简单地, 用java5以上的话(2010年了, 还在用上古神器jdk1.3或者1.4的首长请举手. 敬礼! 首长辛苦了!), 你怎么new一个ArrayList呢? 这样吧?
- List<String> list = new ArrayList<String>();
List<String> list = new ArrayList<String>();
怎么new一个HashMap呢? 是不是这样?
- Map<String, Integer> map = new HashMap<String, Integer>();
Map<String, Integer> map = new HashMap<String, Integer>();
对俺们这些用惯了瓜娃的懒人, 写那个<>里面的东东两次就觉得累得慌, 除非一些特殊的场合不想依赖瓜娃的jar文件的, 俺们一般都改写成:
- List<String> list = Lists.newArrayList();
List<String> list = Lists.newArrayList();
- Map<String, Integer> list = Maps.newHashMap();
Map<String, Integer> list = Maps.newHashMap();
Lists和Maps是两个工具类, Lists.newArrayList()其实和new ArrayList()几乎一模一样, 唯一它帮你做的(其实是javac帮你做的), 就是自动推导(不是"倒")尖括号里的那坨叉叉.
这两个东西要说技术含量, 附加价值差不多趋近于0. 但是越是这种简单的不用脑子的东西, 越是让人惊讶地有邪异的吸引力. 我就看过好几个人说, 他们使用瓜娃的主要目的, 就是newArrayList()和newHashMap(). 毕竟, 这东西每个人每时每刻差不多都要用到吧?
当然, 瓜娃提供的远不止这两毛五, 买一根小豆冰棍儿都不够. 比如, 除了new一个空的ArrayList, 你还想往里放点数据. 直接用jdk的话, 大概是这样:
- List<Integer> list = new ArrayList<Integer>();
- list.add(1);
- list.add(2);
- list.add(3);
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
用Lists是这样:
- List<Integer> list = Lists.newArrayList(1, 2, 3);
List<Integer> list = Lists.newArrayList(1, 2, 3);
嗯, 是不是有点意思了? 值一个"可爱多"了吧?
二. ImmutableList和ImmutableMap
现在主流java社区倾向于同意, immutable对象(就是说状态不变)是个好东西. 它能让程序逻辑变得简单易懂, 而且减少出错的可能性.
JDK目前不提供immutable集合. 瓜娃提供的最主要的immutable集合, 是ImmutableList, ImmutableSet和ImmutableMap.
顾名思义, 一旦你创建了一个ImmutableList或者ImmutableMap, 你就再也不能改变它了. 它就像你记忆中初恋的那个她, 不管时光荏苒, 沧海桑田, 永远年轻美丽语笑嫣然 --- 当然, 前提是你还活着.
那么怎么创建ImmutableList呢? 嗯, 除非语言直接提供支持, 这应该是最简单的方式了:
- ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
ImmutableMap一样简单:
- ImmutableMap<String, Integer> map = ImmutableMap.of(
- "1", 1,
- "2", 2,
- "3", 3
- );
ImmutableMap<String, Integer> map = ImmutableMap.of(
"1", 1,
"2", 2,
"3", 3
);
对比一下用HashMap的情况:
- Map<String, Integer> map = new HashMap<String, Integer>();
- map.put("1", 1);
- map.put("2", 2);
- map.put("3", 3);
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("1", 1);
map.put("2", 2);
map.put("3", 3);
还是前者舒服吧?
那么, 假设你所有的不是事先知道的一些常量. 比如说, 你要从一个数据库里读出一些基本信息保存在内存里. 用of()就不好使了, 这时候, 可以用builder模式:
- ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
- for (...) {
- builder.put(name, age);
- }
- ImmutableMap<String, Integer> map = builder.build();
ImmutableMap.Builder<String, Integer> builder = ImmutableMap.builder();
for (...) {
builder.put(name, age);
}
ImmutableMap<String, Integer> map = builder.build();
ImmutableList和ImmutableMap最常用的就是创建一些集合常量 (这些常量都是static final的).
另外一个主要的用途, 是作为其它Immutable对象内部使用的数据结构.
三. 讨论
一个也许会存在争议, 但是瓜娃开发组强烈推荐的使用方法, 是凡是可能的情况下, 给你的函数的返回类型用ImmutableList和ImmutableMap, 而不是它们实现的接口: List和Map. 比如:
- ImmutableList<String> getNames();
- ImmutableMap<String, Integer> getAgeMap();
ImmutableList<String> getNames();
ImmutableMap<String, Integer> getAgeMap();
而不是:
- List<String> getNames();
- Map<String, Integer> getAgeMap();
List<String> getNames();
Map<String, Integer> getAgeMap();
为什么这样呢? 这是因为immutable集合是一类特殊集合, 它们不象ArrayList, HashMap那样是纯粹的实现细节, 相反, 这些类型携带了重要的语义信息: 它们是不可变的.
如果调用者使用了list.add()或者map.put(), 得到的将是UnsupportedOperationException异常.
通过返回ImmutableMap和ImmutableList, 函数签名清楚地告诉调用者什么可以做, 什么不可以做. 这对大型程序中团队协作, 代码理解和维护好处多多.
实际上, 最有争议的, 是在jdk的集合框架设计上. Sun把List和Map的接口设计为mutable(可变的), 但是同时允许实现类不实现某些函数, 比如add(), put(). 一些貌似爱思考地程序员(包括俺)就想啦: 为啥你不干脆把写操作分离出来呢? 这样当一个人拿到ImmutableList的时候, 他根本就没有add()可以调用, 更甭说运行时抛异常了?
Sun对此专门有个回答. 是否信服就由你了. 不过不管喜不喜欢, 我们今天所有的就是这个设计: 一个List可能是可变的也可能是不可变的. 大概还是不要钻牛角尖, 入乡随俗吧.
这集要讲的工具叫MapMaker.
顾名思义, MapMaker就是帮你做Map滴, 而且是as fast as it can! 实在是居家旅行, 制作Map的必备工具啊!
可是啊, 这计算机一思考, 你就笑了: "小样儿, 做Map还用你做啊? 当我文盲?" 不就是:
- new HashMap<姓名, 奖金>()
new HashMap<姓名, 奖金>()
嘛. 如果用上回书提到的Maps就是:
- Maps.newHashMap();
Maps.newHashMap();
再不, 用ImmutableMap, 就是
- ImmutableMap.of("ajoo", 10000000000000000);
ImmutableMap.of("ajoo", 10000000000000000);
计算机: 这个, 这个, 啊, (耳语: 老大, 别这么不给面子啊. 出来混都不容易), 我们这里讲的不是HashMap, 也不是ImmutableMap, 其实是代表着当代最最高科技的
- java.util.concurrent.ConcurrentMap
java.util.concurrent.ConcurrentMap
, 它适合多线程操作, 高并发, 无阻塞, 无死锁, 无冲突, 无测漏, 无...
你: 等等, 你是在说:
- new ConcurrentHashMap<姓名, 奖金>()
new ConcurrentHashMap<姓名, 奖金>()
这么简单到离谱的东西么?
计算机: 这个, 别急, 别急. 其实不完全是. MapMaker提供了更多更强大的功能. 比如可以指定键是否用弱引用, 指定一个键多长时间过期之类的. 但是最最最有用的, 是那个 makeComputingMap()函数.
画面一转, ajoo平易近人地微笑着说: 对啦! 这也就是我今天要讲MapMaker的主要原因.
大家想一想, 平时有没有需要做延迟初始化(所谓lazy initialization)和缓存的? 比如说, 我要对每一个需要用到的员工名字通过数据库查询她一年以来的加班多少, 加班是否自觉自愿, 漂亮程度, 身材和三围指标, 领会领导意图能力, 帮领导挡酒总加仑数等等等等各种数据, 最后经过一个NP问题的近似解决算法来得到员工的奖金数额.
这个东西算起来太费资源, 弄不好比奖金本身还多. 那么, 就要延迟计算, 只有员工主动跑来跟我要加薪, 我才去算. 而且要缓存, 如果这个员工对只发50元奖金心存不满, 反复来吵闹, 甚至投诉, 我们不用重新计算.
一般如果你自己写怎么写呢? 假设我那个算奖金的函数写好了, 就叫computeBonus(姓名). 这样么?
- class BonusManager {
- private final Map<姓名, 奖金> cache = new HashMap<姓名, 奖金>();
- 奖金 getBonus(姓名 name) {
- 奖金 bonus = cache.get(name);
- if (bonus == null) {
- bonus = computeBonus(name); // 危险在这里!
- }
- cache.put(name, bonus);
- return bonus;
- }
- }
class BonusManager {
private final Map<姓名, 奖金> cache = new HashMap<姓名, 奖金>();
奖金 getBonus(姓名 name) {
奖金 bonus = cache.get(name);
if (bonus == null) {
bonus = computeBonus(name); // 危险在这里!
}
cache.put(name, bonus);
return bonus;
}
}
其实主要逻辑是这样的. 问题是这个实现不是多线程安全的.
你可以在getBonus()上面加一个synchronized关键字, 然后它就线程安全了. 但是, 让我们假设我们是个跨国知名连锁大托拉斯, 需要极高极高的并发能力.
聪明如你一定会想到把HashMap改成ConcurrentHashMap.
近了一步了, 但是还不完全. 这是因为两个线程可能同时执行if (bonus == null)然后同时去运行computeBonus(). 结果是, 我们可能对某些名字重复运行computeBonus(). 让我们再假设老板遇到这种你给公司浪费了资源的情况会炒你鱿鱼的.
呵呵, 到这里, 如果你想到了双检查锁机制(所谓的 double-checked locking), 那算你狠, 遇到你们这种聪明到煞风景的听众我真是无话可说, 卷帘朝散, 请假还宫!
而如果你没想到, 那么, 恭喜你, 你答对啦! 奖河南双汇火腿肠一根(94年珍藏版).
我想说的, 其实是, 俺也不知道怎么写最好. 所以呀, 最好是不写. 反正MapMaker都给咱写好了不是? 人生苦短, 当及时行乐啊, 同志! 有闲功夫多洗几块尿布好不好?
那么, 不卖关子了 (等不及已经看了javadoc文档的同志们都已经自己写出来了. 俺再摇头晃脑故作玄虚恐怕就要吃臭鸡蛋, 烂西红柿伺候咯). 请看. 当当当当 (贝多芬第九交响曲响起)!
- ConcurrentMap<名字, 奖金> cache = new MapMaker()
- // 如果一个计算一年有效 (嗯, 明年奖金减半!), 那么设一个过期时间
- .expiration(365, TimeUnit.DAYS)
- .makeComputingMap(new Function<名字, 奖金>() {
- @Override public 奖金 apply(名字 name) {
- return computeBonus(name);
- }
- });
ConcurrentMap<名字, 奖金> cache = new MapMaker()
// 如果一个计算一年有效 (嗯, 明年奖金减半!), 那么设一个过期时间
.expiration(365, TimeUnit.DAYS)
.makeComputingMap(new Function<名字, 奖金>() {
@Override public 奖金 apply(名字 name) {
return computeBonus(name);
}
});
就是这么简单. 等等, 我先摆个pose先, 样子屌不屌?
这里面唯一需要解释的就是这个Function. 大家都知道, java是一门罗唆的语言艺术, 讲究说学逗唱, 不是, 说错了, 讲究类, 方法, 表达式, 匿名类等等等等, 反正是怎么写累人怎么来. 这里的目的其实就是告诉makeComputingMap()怎样"compute". 这个function是个匿名类, 它的apply()函数封装了怎么计算这么一个概念
其实guava可以看成是支持java5的apache commons之类的项目.
第一第二回讲的主要是集合类的一些最常用的工具. 其实com.google.common.collect里还有很多非常有用的工具, 比如Ordering,ComparisonChain,Iterables, Multiset, Multimap等等. 这里有些我们后面还会涉及.
小兄弟正在做socket客户服务器通信相关的课程设计, 所以我有点迫不及待先讲讲common.io这个包了. 这里面最有用的, 在我看来, 就是CharStreams和ByteStreams这两个工具.
大家知道, Java里读外设主要是跟InputStream和Reader打交道. 其中InputStream用来读取原始的字节流, 而Reader是在你已经知道了输入是用什么字符集编码的情况下读取字符串.
但是这两个类还是相对底层了一些. 在做一些很灵活复杂的事情的时候是必要的, 甚至我们还需要抬出nio这个大杀器. 可它们对日常工作中一些简单的事情, 就有炮弹打蚊子的感觉.
比如, 我需要从一个Reader里一行一行读出所有文本. 直接用Reader的话, 你需要弄一个BufferedReader, 然后循环调用readLine(), 直到全部读完. 类似这样:
- BufferedReader buffered = new BufferedReader(reader);
- List<String> lines = new ArrayList<String>();
- for (;;) {
- String line = buffered.readLine();
- if (line == null) {
- break;
- }
- lines.add(line);
- }
BufferedReader buffered = new BufferedReader(reader);
List<String> lines = new ArrayList<String>();
for (;;) {
String line = buffered.readLine();
if (line == null) {
break;
}
lines.add(line);
}
用CharStreams的话, 一句话就搞定了:
- List<String> lines = CharStreams.readLines(reader);
List<String> lines = CharStreams.readLines(reader);
如果你还是需要类似于流一样的操作-比如, 输入的行数太多, 不能一下子都读进来, 那么, 还有一个 LineReader可以用. 用起来类似于:
- LineReader lineReader = new LineReader(reader);
- for (String line = lineReader.readLine(); line != null; line = lineReader.readLine()) {
- System.out.println(line);
- }
LineReader lineReader = new LineReader(reader);
for (String line = lineReader.readLine(); line != null; line = lineReader.readLine()) {
System.out.println(line);
}
其它的one-liner, 包括:
从一个Readable读取所有东西写到一个Appendable里去:
- CharStreams.copy(reader, writer);
CharStreams.copy(reader, writer);
从Readable读取所有内容到一个字符串:
- String content = CharStreams.toString(reader);
String content = CharStreams.toString(reader);
另外, 写io程序一个最常见的bug, 就是打开东西忘记关了. 或者关的不彻底. 一般来说, 你要开一个InputStream, 肯定要接下来用一个try-finally, 在用完之后调用close()把它关上. OutputStream, Reader, Writer同理.
但是这很麻烦, 而且容易忘. 在java7的 自动资源管理出来之前, 可以用common.io包定义的InputSupplier, OutputSupplier接口.
原理是, 你实现一个InputSupplier对象, 把"怎样打开这个InputStream/Reader"的逻辑封装在getInput()它的函数里, 然后, 把它传递给CharStreams, ByteStreams相关的API, 比如readLines(), copy()等, 这些API调用你的supplier, 然后做事, 做完之后它把自己屁股擦干净, 把它打开的InputSteam/Reader再关上.
ByteStreams很类似, 不过它是工作在字节流上, 而不关心字符编码问题.
对应于CharStreams.toString(), 是
- byte[] content = ByteStreams.toByteArray(inputStream)
byte[] content = ByteStreams.toByteArray(inputStream)
用来把整个InputStream的内容全部一次性读到一个byte[]里面.
对应于CharStreams.copy(), 是
- ByteStreams.copy(inputStream, outputStream);
ByteStreams.copy(inputStream, outputStream);
用来把所有内容从一个InputStream拷贝到另一个OutputStream.