java 容器深入研究_Thinking In Java ---- 容器深入研究

1 完整的容器分类法

c536be738896ece5ec97c61aebe9a022.png

下面是集合类库更加完备的图。包括抽象类和遗留构件(不包括Queue的实现)

c2634c6859c21a7b91a31bc00663e0ba.png

JavaSE5新添加了:

Queue接口及其实现PriorityQueue和各种风格的BlockingQueue;ConcurrentMap接口及其实现ConcurrentHashMap,它们也是用于多线程机制的;CopyOnWriteArrayList和CopyOnWriteArraySet,它们也是用于多线程机制的;EnumSet和EnumMap,为使用enum而设计的Set和Map的特殊实现;在Collections类中的多个便利方法。

虚线框表示abstract类,你可以看到大量的类的名字都是以Abstract开头的。这些类可能初看起来有点令人困惑,但是它们只是部分实现了特定接口的工具。

2 Collection的功能方法

下面的表格列出了可以通过Collection执行的所有操作(不包括从Object继承而来的方法)。因此,它们也是可以通过Set或List执行的所有操作(List还有额外的功能)。Map不是继承自Collection的。

b5bbf8f6dcedb477be32318094f692d7.png

请注意,其中不包括随机访问所选择元素的get()方法。因为Collection包括Set,而Set是自己维护内部顺序的(这使得随机访问变得没有意义)。因此,如果想检查Collection中的元素,那就必须使用迭代器。

3 可选操作

执行各种不同的添加和移除的方法在Collection接口中都是可选操作。这意味着实现类并不需要为这些方法提供功能定义。 这是一种很不寻常的接口定义方式。正如你所看到的那样,接口是面向对象设计中的契约,它声明“无论你选择如何实现该接口,我保证你可以向该接口发送这些消息。”但是可选操作违反这个非常基本的原则,它声明调用某些方法将不会执行有意义的行为,相反,它们会抛出异常。这看起来好像是编译期类型安全被抛弃了。事实并不那么糟。如果一个操作是可选的,编译器仍旧会严格要求你只能调用该接口中的方法。这与动态语言不同,动态语言可以在任何对象上调用任何方法,并且可以在运行时发现某个特定调用是否可以工作。另外,将Collection当作参数接受的大部分方法只会从Collection中读取,而Collection的读取方法都不是可选的。为什么你会将方法定义为可选的呢?那是因为这样做可以防止在设计中出现接口爆炸的情况。容器类库中的其他设计看起来总是为了描述每个主题的各种变体,而最终患上了令人困惑的接口过剩症。甚至这么做仍不能捕捉接口的各种特例,因为总是有人会发明新的接口。“未获支持的操作这种方式可以实现Java容器类库的一个重要目标:容器应该易学易用。未获支持的操作是一种特例,可以延迟到需要时再实现。但是,为了让这种方式能够工作:

UnsupportedOperationException必须是一种罕见事件。即,对于大多数来说,所有操作都应该可以工作,只有在特例中才会有未获支持的操作。如果一个操作是未获支持的,那么在实现接口的时候可能就会导致UnsupportedOperationException异常,而不是将产品程序交给客户以后才出现此异常,这种情况是有道理的。毕竟,它表示编程上有错误:使用了不正确的接口实现。

值得注意的是,未获支持的操作只有在运行时才能探测到,因此它们表示动态类型检查。如果你以前使用的是C++这种静态类型语言,那么可能会觉得Java也只是另一种静态类型语言,但是它还具有大量的动态类型机制,因此很难说它到底是哪一种类型的语言。一旦开始注意到这一点了,你就会看到Java中动态类型检查的其他例子。

4 List的功能方法

正如你所看到的,基本的List很容易使用:大多数时候只是调用add()添加对象,使用get()一次取出一个元素,以及调用iterator()获取用于该序列的Iterator。

5 Set和存储顺序

5c767710a0848d642010a2710a166f0f.png

5.1 SortedSet

SortedSet中的元素可以保证处于排序状态,这使得它可以通过在SortedSet接口中的下列方法提供福加德功能:Comparator comparator()返回当前Set使用的Comparator;或者返回null,表示以自然方式排序。Object first()返回容器中的第一个元素Object last()返回容器中的最末一个元素SortedSet subSet(fromElement, toElement) 生成此Set的子集,范围从fromElement(包含)到toElement(不包含)。SortedSet headSet(toElement) 生成此Set的子集,由小于toElement的元素组成。SortedSet tailSet(fromElement) 生成此Set的子集,由大于或等于fromElement的元素组成。注意,SortedSet的意思是“按对象的比较函数对元素排序”,而不是指“元素插入的次序”。插入顺序可以用LinkedHashSet来保存。

6 队列

除了并发应用,Queue在Java SE5中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能。

7 理解Map

映射表(也称为关联数组)的基本思想是它维护的是键-值(对)关联,因此你可以使用键来查找值。标准的Java类库中包含了Map的几种基本实现,包括:HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap。它们都有同样的基本借口Map,但是行为特性各不相同,这表现在效率、键值对的保存及呈现次序、对象的保存周期、映射表如何在多线程程序中工作和判定“键”等价的策略等方面。Map接口实现的数量应该可以让你感觉到这种工具的重要性。

7.1 性能

性能是映射表中的一个重要问题,当在get()中使用线性搜索时,执行速度会相当的慢,而这正是HashMap提高速度的地方。HashMap使用了特殊的值,称作散列码,来取代对键的缓慢搜索。散列码是“相对唯一”的、用以代表对象的int值,它是通过该对象的某些信息进行转换而生成的。hashCode()是根类Object中的方法,因此所有Java对象都能产生散列码。HashMap就是使用对象的hashCode()进行快速查询的,此方法能够显著提高性能。

要到达更高的性能,速度狂们可以参考Donald Knuth的The Art of Computer Programming, Volume 3: Sorting and Searching, Second Edition。使用数组代替溢出桶,这有两个好处:第一,可以针对磁盘存储方式做优化;第二,在创建和回收单独的记录时,能节约很多时间

e133248f06f770370c1842c214409ef6.png

7.2 SortedMap

使用SortedMap(TreeMap是其现阶段的唯一实现),可以确保键处于排序状态。这使得它具有额外的功能,这些功能由SortedMap接口中的下列方法提供:Comparator comparator(): 返回当前Map使用的Comparator;或者返回null,表示以自然方式排序。T firstKey()返回Map中的第一个键。T lastKey()返回Map中的最末一个键。SortedMap subMap(fromKey, toKey)生成此Map的子集,范围由fromKey(包含)到toKey(不包含)的键确定。SortedMap headMap(toKey)生成此Map的子集,由键小于toKey的所有键值对的组成。SortedMap tailMap(fromKey)生成此Map的子集,由键大于或等于fromKey的所有键值对组成。

7.3 LinkedHashMap

为了提高速度,LinkedHashMap散列化所有的元素,但是在遍历键值对时,却又以元素的插入顺序返回键值对。此外,可以在构造器中设定LinkedHashMap,使之采用基于访问的最近最少使用(LRU)算法,于是没有被访问过的(可被看作需要删除的)元素就会出现在队列的前面。对于需要定期清理元素以节省空间的程序来说,此功能使得程序很容易得以实现。

8 散列和散列码

正确的equals()方法必须满足下列五个条件:

自反性。对任意x,x.equals(x)一定返回true。对称性。对任意x和y,如果y.equals(x)返回true,则x.equals(y)也返回true。传递性。对任意x、y、z,如果有x.quals(y)返回true,y.equals(z)返回true,则x.equals(z)一定返回true。一致性。对任意x和y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y)多少次,返回的结果应该保持一直,要么一直是true,要么一直是false。对任何不是null的x,x.equals(null)一定返回false。

8.1 理解hashCode()

若不覆盖hashCode()和equals(),那么使用散列的数据结构(HashSet,HashMap,LinkedHashSet或LinkedHashMap)就无法正确处理你的键。然而,要很好的解决此问题,你必须了解这些数据结构的内部构造。首先,使用散列的目的在于:想要使用一个对象来查找另一个对象。不过使用TreeMap或者你自己实现的Map也可以达到此目的。

8.2 为速度而散列

散列的价值在于速度:散列使得查询得以快速进行。由于散列表中的“槽位“(slot)通常称为桶位(bucket),因此我们将表示实际散列表的数组命名为bucket。为使散列分布均匀,桶的数量通常使用质数。事实证明,质数实际上并不是散列桶的理想容量。进来,(经过广泛的测试)Java的散列函数都使用2的整数次方。对现代的处理器来说,除法与求余量是最慢的操作。使用2的整数次方长度的散列表,可以用掩码代替除法。因为get()是使用最多的操作,求余数的%操作是其开销最大的部分,而使用2的整数次方可以消除此开销(也可能对hashCode()有些影响)。

8.3 覆盖hashCode()

71778880e1aae57fbb07e2dfae1977c2.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值