网传此书不错,最近用了几天的时间,将其读完,发现其中有不少错误,在此做一总结,希望给后续的读者做一参考。
关于GET和POST
在书中的第2章,第2.1.1小节,P19中有如下描述:
此处描述欠准确,GET和POST的本质区别是幂等性不同,关于幂等性的定义,请自行google,除此之外,HTTP协议还规定了HEAD,PUT,DELETE,OPTIONS等一些其他的方法。
关于时间复杂度
在书中的第2章,第2.1.3小节,P26中有如下描述:
作者认为,HashMap的查找操作的时间复杂度为o(1)。此处小o符号的使用是错误的。在算法导论中,大O与小o符号的含义是严格定义的。
- 如果一个算法的时间复杂度为O(n),意味着该算法在最坏情况下,其运行时间最多是输入规模的线性函数,而不会是二次或者更高次的函数。
- 如果一个算法的时间复杂度为o(n),意味着该算法在最坏情况下,其运行时间严格小于输入规模的线性函数(也就是常数级的时间复杂度,即O(1))。
因此,可以把大O符号看做“<=”,把小o符号看做“<”。从理论上讲,我们可以认为HashMap的查找操作的时间复杂度为O(1)(实际上会大于O(1),因为要加上解决冲突花费的时间),即常数级的时间复杂度,而不是o(1),因为小于常数级时间复杂度的算法并不存在。
在后续的章节中,还有一处这样的错误,在此不重复提及。
当然,如果作者在此并不使用算法导论中对上述符号的定义,那么应该在提及该符号时阐明其含义。
关于HTTP和TCP
在书中的第3章,第3.2.1小节,P58中有如下描述:
上述描述中,将TCP与HTTP相互比较,好像在用HTTP的时候就可以不用TCP,关于访问速度的对比,真实原因也没阐明。
实际上,HTTP属于应用层协议,而TCP属于传输层协议,所有HTTP请求都会通过TCP实现,HTTP请求慢的原因在于,对于每一个请求都会重建一个TCP连接,而建TCP连接是一个相对耗时的操作,当然也可以使用一个TCP连接进行多次HTTP请求(即所谓的长连接),在实际的项目中,基本上也都是这样做的。
关于Java序列化
在书中的第3章,第3.5.3小节,为了解决全局变量在App重启之后,其成员变量为空的问题,作者提出了采用Java序列化把全局变量序列化到本地的方法。其中有如下描述:
作者关于Java序列化与实例控制(Instance Control)的关系描述是正确的,除了上图中画红线部分。为了保持单例的性质,需要在readResolve方法中返回已有的实例,但是Instance Control并不需要readObject方法的参与,更不需要实现Cloneable接口。
在其全局变量的实现中也有问题,请看以下代码段:
- 首先,看UserBean user这个成员变量,UserBean在后文中提到,实现了Serializable接口。作者依靠辅助方法Utils.saveObject将整个单例全局变量序列化到本地,并在反序列化时初始化成员变量user。然而此处存在一个安全漏洞,如果可以修改序列化的文件,黑客可以轻而易举的创建一个新的全局变量(单例模式)的实例(具体如何攻击,请参考《Effective Java》 Item 77)。如果将user变量声明为transient,虽然不会再有安全问题,但是user也不会被实例化到本地。
实际上,Java 1.5之后,对于做实例控制的序列化单例而言,并没有一个好的方法,如果真要这样做,请使用枚举类型来实现单例,其反序列化过程的实例控制以及安全性都是由JVM来保证的,不会有任何问题。(关于单例的话题,请参考我之前写过的一篇文章:如何以正确的姿势写单例) - 其次,在readResolve中,作者调用了this.clone方法,其实并不需要调这个方法,因为实例控制不依赖于Cloneable接口,也没有任何关系。
关于比较器
在第6章,6.1.8小节中,关于比较器,作者有如下描述:
排序算法按照是否是通过元素之间的比较来划分,可以分为两类排序算法,一类是比较排序算法,包括归并排序,插入排序,冒泡排序,快速排序,堆排序等等;另一类是非比较排序算法,包括计数排序,桶排序等。任意一个比较排序算法在最坏情况下,其时间复杂度的下界为Ω(nlgn)(Ω符号请参考算法导论)。
比较器是比较排序算法中极其重要的一个工具,函数或者算子。有了比较器,比较排序算法就可以对输入的元素集合进行排序。
那Java中为什么会定义Comparator这个接口,并要求开发者在实现该接口时,其返回值必须符合文档给出的规范约束?因为,对于自定义的类,只有开发者才知道如何定义该类两个实例之间的顺序关系,返回值也只有符合文档规范,比较排序算法才能正常工作。所以,作者说Comparator是插入排序与归并排序相结合的产物是错误的(作者参考了一篇错误的文档,得出了一个错误的结论)。
再说说时间复杂度,插入排序的时间复杂度为O(n2) ,归并排序的时间复杂度为O(nlgn),冒泡排序的时间复杂度为O(n2)。在JDK 1.7的排序算法的实现是TimSort,TimSort在输入规模较大的时候选择归并排序算法进行,而在输入规模较小的时候选择插入排序算法进行。因为在输入规模较小时,插入排序的时间复杂度虽然为O(n2),由于其系数较小,运行时间反而比归并排序小。
关于Context和startActivity
在第6章,6.2.4小节中,作者说使用除Activity之外的Context实例进行startActivity时,需要增加Intent.FLAG_ACTIVITY_NEW_TASK才能不报错,但是没说明具体原因。
当使用Activity的实例来启动一个新的Activity时,系统知道新的Activity将加入到哪个Task当中去,因此是不需要指定一个NEW_TASK的标志位的。但是当使用Application或者Service的实例来启动一个新的Activity时,由于Application和Service的实例并不存在与一个特定的系统Task中,因此系统并不知道如何将新启动的Activity加入到哪个Task当中,因此要求开发者强制增加一个NEW_TASK的标志位。
一些书写错误
在第6章,6.1.1小节中,P108,空指针异常应该是NullPointerException,而不是NullPointException。
6.9.1小节中,P163,OOM异常类名为OutOfMemoryError,而不是OutOfMemoryException。
此外,本书中存在很多笔误,错误单词,还有一些说法不恰当或者问题不太大的地方,本文就不一一列举了。总体而言,此书对于新手,还是有些营养价值的,但是要注意辨别。。。