目录
Java编程思想(一)第1~4章:概述
Java编程思想(二)第5章:初始化和清理
Java编程思想(三)第6章:访问权限
Java编程思想(四)第7章:复用类
Java编程思想(五)第8章:多态
Java编程思想(六)第9章:接口
Java编程思想(七)第10章:内部类
Java编程思想(八)第11章:持有对象
Java编程思想(九)第12章:异常
Java编程思想(十)第13章:字符串
Java编程思想(十一)第14章:类型信息
Java编程思想(十二)第15章:泛型
Java编程思想(十三)第16章:数组
Java编程思想(十四)第17章:深入研究容器

第十七章、容器的深入研究

 

1. 完整的容器分类法

《JAVA编程思想》学习笔记:第17章(深入研究容器)

Java SE5新添加了:

  • Queue接口:LinkedList已经为实现该接口做了修改;及其实现PriorityQueue和各种风格的BlockingQueue(用于生产者-消费者模型,多线程机制);
  • ConcurrentMap接口及其实现ConcurrentHashMap,用于多线程机制;
  • CopyOnWriteArrayList和CopyOnWriteArraySet,用于多线程机制;
  • EnumSet和EnumMap,为使用enum而设计的set和map的特殊实现;
  • 在Collections类中的多个便利方法。

2. 填充容器

Collections类也有一些实用的static方法,其中包括fill(),同Arrays一样只复制同一对象引用来填充整个容器的,并且只对List对象有用,但是所产生的列表可以传递给构造器或addAll方法。

  • Collections.addAll() 将一些元素添加到一个Collection中
  • Collections.nCopies() 复制一些元素到Collections中,返回一个List集合
  • Collections.fill() 复制元素填充到集合当中

3. 使用Abstract类

每个java.util容器都有其自己的Abstract类,它们提供了该容器的部分实现,因此必须做的只是去实现那些产生想要的容器所必需的方法。

  • AbstractList 实现了List接口;
  • AbstractMap 实现了Map接口;
  • AbstractSet 实现了Set接口;

享元模式(GoF23):可在普通解决方案需要过多对象,或者产生普通对象太占用空间时使用享元。享元模式使得对象的一部分可以被具体化,因此,与对象中的所有事物都包含在对象内部不同,可以在更加高效的外部表中查找对象的一部分或整体。

 

4.Collection的功能方法(Collection是一个接口)

下面表格列出了可以通过Collection执行的所有操作。它们也可以是通过Set或者List执行的所有操作。

 

Iterator迭代器接口方法:

  • hasNext(); 判断是否存在下一个对象元素;
  • next(); 获取下一个元素;
  • remove(); 移除元素。

4. 可选操作

  • 概念:执行各种不同的添加和移除的方法在Collection接口中都是可选操作。这意味着实现类并不需要为这些方法提供功能定义。
  • 声明调用某些方法将不会执行有意义的行为,相反,它们会抛出异常。如果一个操作是可选的,编译器仍旧会严格要求你只能调用该接口中的方法。
  • 设计原因:可以防止在设计中出现接口爆炸的情况。

4.1 未获支持的操作

  • 未获支持操作异常:UnsupportedOperationException:必须是一种罕见的事件。即,对于大多数类来说,所有的操作都应该是可以工作的,只有在特例中才会有未获得支持的操作。在Java容器类库中确实如此,因为只有99%的时间里面使用的所有的容器类,如ArrayList、LinkedList、HashSet和HashMap,以及其他具体的实现,都支持所有的操作。
  • 如果一个操作是未获支持的,那么在实现接口的时候就可能会导致UnsupportedOperationException异常(表明此为编程错误)。

异常案例1(Arrays.asList())

  • Arrays.asList():将数组转换为List时,产生固定尺寸的List,仅支持那些不会改变数组大小的操作。
  • 最常见的未获支持的操作,源于背后由固定尺寸的数据结构支持的容器。
  • 任何会引起对底层数据结构的尺寸进行修改的方法都会产生一个UnsupportedOperationException异常,以表示对未获支持操作的调用。
  • 支持对容器的元素进行修改。

异常案例2(Collections.unmodifiableList())

  • Collections.unmodifiableList():产生不可修改的列表。
  • 不支持对容器的元素进行修改。

 

5. List的功能方法

List接口:

  • get(int); 获取指定索引位置的列表元素
  • set(int,E); 设置指定索引位置的列表元素
  • add(int,E); 将指定的元素添加到此列表的索引位置。
  • remove(int); 移除指定索引位置的元素;
  • indexOf(Object); 从列表头部查找Object对象元素
  • lastIndexOf(Objcet); 从列表尾部查找Object对象元素
  • listIterator(); 返回列表迭代器
  • listIterator(int); 返回指定索引位置的列表迭代器
  • subList(int,int); 该方法返回的是父list一个视图,从fromIndex(包含),到toIndex(不包含)

ListIterator迭代器接口方法:

  • hasPrevious(); 如果以逆向遍历列表,列表迭代器前面还有元素,则返回 true,否则返回false
  • previous(); 返回列表中ListIterator指向位置前面的元素
  • set(E); 从列表中将next()或previous()返回的最后一个元素返回的最后一个元素更改为指定元素e
  • add(E); 将指定的元素插入列表,插入位置为迭代器当前位置之前
  • nextIndex(); 返回列表中ListIterator所需位置后面元素的索引
  • previousIndex(); 返回列表中ListIterator所需位置前面元素的索引

 

6. Set和存储顺序

  • Set接口:继承来自Collection接口的行为。
  • 存储顺序:不同的set实现不仅具有不同的行为,而且它们对于可以在特定的set中放置元素的类型也有不同的要求。
  • default:如果没有其他限制,应该默认选择HashSet(加*),因为它对速度进行了优化。
  • equals():必须为散列存储(Hash)和树形(Tree)储存都创建equals方法;
  • hashCode():只有在类被置于HashSet和LinkedHashSet时才必须(即TreeSet在语法上非强制要求)。但是良好的编程风格(Effective Java)要求:覆盖equals方法时,总是同时覆盖hashCode方法。

 

 

6.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的元素组成

 

7. Queue

Queue接口包含:

  • boolean add(E e); 添加一个元素,添加失败时会抛出异常
  • boolean offer(E e); 添加一个元素,通过返回值判断是否添加成功
  • E remove(); 移除一个元素,删除失败时会抛出异常
  • E poll(); 移除一个元素,返回移除的元素
  • E element(); 在队列的头部查询元素,如果队列为空,抛出异常
  • E peek(); 在队列的头部查询元素,如果队列为空,返回null
  • Queue在Java SE5中仅有的两个实现是LinkedList和PriorityQueue,它们的差异在于排序行为而不是性能

 

7.1 优先级队列

PriorityQueue:

  • 设计:列表中的每个对象都包含一个字符串和一个主要的以及次要的优先级值。
  • 优先级排序:该列表的排序顺序也是通过实现Comparable而进行控制的:Queue<String> queue=new PriorityQueue<>();

 

7.2 双向队列

  • 概念:双向队列就像一个队列,但是可以在任意一段添加或移除元素。
  • 实现:在LinkedList中包含支持双向队列的方法,但在Java标准类库中没有任何显式的用于双向队列的接口。因此,LinkedList无法去实现这样的接口,无法像前面转型到Queue那样向上转型为Deque。但是可以使用组合创建一个Deque类,并直接从LinkedList中暴露相关方法。

LinkedList中支持双向队列操作的相关方法:

  • queue.addFirst(); 向队列首部添加元素
  • queue.addList(); 向队列尾部添加元素
  • queue.getLast(); 获取队列尾部元素
  • queue.getFirst(); 获取队列首部元素
  • queue.removeFirst(); 移除队列首部元素
  • queue.removeLast(); 移除队列尾元素
  • queue.size(); 返回队列大小

 

8. 理解Map

Map接口:

  • size(); 返回Map大小
  • isEmpty(); 判断Map集合是否为空
  • containsKey(); 判断Mpa集合是否包括Key键
  • containsVaule(); 判断Map集合是否包括Vaule
  • get(Object); 获得Map集合键为Object的Vaule
  • put(K,V); 添加一个K,V键值对
  • remove(Object); 移除K键值对
  • putAll(Map); 添加所有元素Map集合
  • clear(); 清空Map中所有集合元素
  • keySet(); 返回键Key的Set集合
  • values(); 返回值Vaule的Set集合
  • entrySet(); 返回Map.entrySet集合
  • remove(Objcet,Object); 移除K,V集合
  • replace(K,V,V); 替换键值对

8.1 性能

  • 性能是Map容器(K-V映射表)的一个重要问题。
  • get进行线性搜索时,执行速度会相当慢,这正是HashMap提高速度的地方。
  • 散列码(hash code):是相对唯一的,用以代表对象的int值,它是通过将该对象的某些信息进行转换而生成的。
  • 散列函数:hashCode()是类Object中的方法,因此所有Java对象都能产生散列码。

 

8.1.1 HashMap

  • 默认选择(*)
  • 使用散列码(对象的hashCode())来取代对键的缓慢搜索,此方法能够显著提高性能。

 

8.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的所有键值对组成

 

8.3 LinkedHashMap

  • 特性1:LinkedHashMap在迭代遍历键值对时,为了提高速度,以元素的插入或者LRU顺序来返回键值对(不同于HashMap)。
  • 特性2:LinkedHashMap使用链表维护内部次序(不同于HashMap)。
  • 最近最少使用(LRU)算法:没有被访问过的元素就会出现在队列的前面;LinkedHashMap可以在构造器中设定,使之使用此算法。

9. 散列与散列码

对于一个放入Map集合的对象,它的类必须同时实现hashCode()和equals()方法。

正确的equals必须满足5个条件:

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

 

9.1 理解hashCode()

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

 

9.2 为速度而散列

散列的价值在于速度,针对k-v查询(由于瓶颈位于键的查询速度):

  • 方式1(普通方式):线性查询,最慢的查询方式;
  • 方式2(次优方式):保持键的排序状态,然后使用Collections.binarySearch进行快速查询。
  • 方式3(更优方式):散列则更进一步,使用数组(存储一组元素最快的数据结构)来表示键的信息(不是键本身)。但数组不能调整容量,所以数组并不保存键本身,而是通过键对象生成一个数字,将其作为数组的下标(这个数字就是散列码)。

关于散列码&k值查询:

  • 不同的键可以产生相同的下标(散列码):也就是说,可能会有冲突,因此数组多大(可以固定size)就不重要了,任何键总能在数组中找到它的位置。
  • k值散列方式的查询过程:

step1:首先是计算散列码;

step2:然后使用散列码查询数组(保持了k值信息,非k值对象本身)。如果没有冲突,那就有了一个完美的散列函数。通常,如散列冲突由外部链接处理:

step3:但是如果散列函数好的话,数组的每个位置就只有较少的值。因此,不是查询整个list,而是快速地跳到数组的某个位置,只对很少的元素进行比较。这就是HashMap如此快的原因。

 

9.3 覆盖hashCode()

设计hashCode()时重要原则:

  • 原则1:无论何时,对同一个对象调用hashCode()都应该生成同样的值。
  • 原则2:此外,也不应该是hashCode()依赖于具有唯一性的对象信息,尤其是使用this的值。因为这样做无法生成一个新的键,使之与put中元素的键值对中的键相同。
  • 举例:以String为例:程序中有多个String对象,都包含相同的字符串序列,那么这些String对象都映射到同一块内存区域。所以new String(“hello”)生成的两个实例,虽然相互独立的,但是对它们使用hashCode()应该产生相同的结果。

如何设计一个好的hashCode方法?

 

10.选择接口的不同实现(容器选型)

  • 容器选型原则:容器之间的区别通常归结为由什么在背后支持它们。也就是说,所使用的接口是有什么样的数据结构实现的。
  • 举例1: ArrayList底层由数组支持;而LinkedList是由双向链表实现,其中每个对象包含数据的同时还包含执行链表的前一个与后一个元素引用。因此,如果经常在表中插入或删除元素,LinkedList就比较合适;否则,应该使用速度更快的ArrayList。
  • 举例2:Set中,HashSet是最常用的,查询速度最快;LinkedHashSet保持元素插入的次序;TreeSet基于Treemap ,生成一个总是处于排序状态的Set。

相关文章:

  • 2021-06-11
  • 2021-04-11
  • 2022-12-23
  • 2022-02-10
  • 2022-12-23
  • 2021-12-31
猜你喜欢
  • 2021-07-28
  • 2022-12-23
  • 2021-09-20
  • 2021-04-10
  • 2021-05-24
  • 2021-10-05
  • 2021-07-07
相关资源
相似解决方案