11

集合框架

1. Java集合简介

集合:由若干个确定的元素所构成的整体。在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,就把这种Java对象称为集合。

 

Collection

Java标准库自带的java.util包提供了集合类——Collection,它是除Map外所有其他集合类的根接口。主要提供了以下三种类型的集合:

  • List:一种有序列表的集合;

  • Set:一种保证没有重复元素的集合;

  • Map:一种通过键值查找的映射表集合。

Java集合设计的特点:

  • 实现了接口和实现类相分离;

  • 支持泛型,可以限制在一个集合中只能放入同一种数据类型的元素。

Java访问集合总是通过统一的方式——迭代器来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。

注意:尽量不要使用遗留类(如Hashtable, Vector, Stack)和遗留接口(Enumeration<E>)。

2. 使用List

一种有序列表。

List内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List的索引和数组一样,从0开始。

使用数组实现增删元素非常麻烦,ArrayList把添加和删除的操作封装起来,操作List类似于操作数组,却不用关心内部元素如何移动。考察List<E>接口,可以看到几个主要的接口方法:

  • 在末尾添加一个元素:void add(E e)

  • 在指定索引添加一个元素:void add(int index, E e)

  • 删除指定索引的元素:int remove(int index)

  • 删除某个元素:int remove(Object e)

  • 获取指定索引的元素:E get(int index)

  • 获取链表大小(包含元素的个数):int size()

实现List接口并非只能通过数组,即ArrayList方式,另一种LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素。ArrayList和LinkedList的比较:

 ArrayList(优先使用)LinkedList
获取指定元素 速度很快 需要从头开始查找元素
添加元素到末尾 速度很快 速度很快
在指定位置添加/删除 需要移动元素 不需要移动元素
内存占用 较大

List的特点

  • List接口允许我们添加重复的元素,即List内部的元素可以重复。

  • List还允许添加null

创建List

除了使用ArrayList和LinkedList,还可以通过List接口提供的of()方法根据给定元素快速创建List。但是List.of()方法不接受null值,如果传入null会抛出NullPointerException异常。

遍历List

①可以使用for循环根据索引配合get(int)方法遍历(不推荐),但其代码复杂,且get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。

②使用迭代器Iterator来访问List。Iterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。该对象知道如何遍历一个List,且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。由于Iterator遍历十分常用,因此,Java的for each循环本身就可以帮我们使用Iterator遍历。

Iterator对象有两个方法:

  • boolean hasNext():判断是否有下一个元素;

  • E next():返回下一个元素。

List和Array转换

List转换为Array的三种方法:

  • 调用toArray()方法直接返回一个Obeject[]数组,但该方法会丢失类型信息,因而实际应用很少;

  • 给toArray(T[])传入一个类型相同的Array,List内部自动把元素复制到传入的Array中;

  • 通过List接口定义的T[] toArray(IntFunction<T[]> generator)方法。

Array转换为List:

  • 通过List.of(T...)方法,返回的是只读List,如果调用add()或remove()方法会抛出UnsupportedOperationException;

  • 对于JDK11之前的版本,可以使用Arrays.asList(T...)方法把数组转换成List。

3. 编写equals方法

List提供了boolean contains(Object o)方法来判断List是否包含某个指定元素。此外,int indexOf(Object o)方法可以返回某个元素的索引,如果元素不存在,返回-1。

上述List中包含的指定元素Object o可以与方法中的参数Object o不是同一个实例,其方法功能仍能实现,因为List内部并不是通过“==”来判断两个元素相等,而是使用equals()方法进行判断的。因此要正确使用contains()和indexOf()方法,放入的实例必须正确覆写equals方法,对于String,Integer这些对象,即Java标准库定义的这些类,已经正确实现了equals()方法,而对于我们自己编写的类,就需要我们编写equals()方法才能达到目标。

编写equals

equals()方法要求我们必须满足以下条件:

  • 自反性(Reflexive)

  • 对称性(Symmetric)

  • 传递性(Transitive)

  • 一致性(Consistent)

  • 对null的比较:即x.equals(null)永远返回false。

equals()方法的正确编写方法:

①先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;

②用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false;

③对引用类型用Objects.equals()比较,对基本类型直接用“==”比较。

4. 使用Map

Map这种键值映射表的数据结构,作用就是能高效通过key快速查找value(元素)。Map也是一个接口,最常用的实现类是HashMap。

  • put(K key, V value)方法把key和value做了映射并放入Map;

  • V get(K key)方法通过key获取到对应的value,如果key不存在,返回null;

  • boolean containsKey(K key)方法查询某个key是否存在。

注意:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。但是value可以重复。

遍历Map

①可以使用for each循环遍历Map实例的keySet()方法返回的Set集合,它包含不重复的key的集合。

②同时遍历key和value可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射。

Map与List的区别:Map存储的是key-value的映射关系,且它不保证顺序。在遍历的时候,遍历的顺序既不一定是put()时放入的key的顺序,也不一定是key的排序顺序。使用Map时,任何依赖顺序的逻辑都是不可靠的。

5. 编写equals和hashCode

HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引。

通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。

正确使用Map必须保证

①作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()方法必须返回true;

②作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:

  • 如果两个对象相等,则两个对象的hashCode()必须相等。必须保证实现,否则HashMap不能正常工作;

  • 如果两个对象不相等,则两个对象的hashCode()尽量不要相等。如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。

编写equals()和hashCode()遵循的原则是:

equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。

不同的key具有相同的hashCode()时就出现了哈希冲突,在冲突的时候,一种最简单的解决办法是用List存储hashCode()相同的key-value。显然,如果冲突的概率越大,这个List就越长,Map的get()方法效率就越低,所以要尽量满足条件二。

6. 使用EnumMap

如果作为key的对象时enum类型,那么还可以使用Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。

使用EnumMap时,总是用Map接口来引用它。

7. 使用TreeMap

SortedMap会在内部对key进行排序,它是一个接口,其实现类是TreeMap。SortedMap保证遍历时以key的顺序来进行排序。使用TreeMap时,放入的key必须实现Comparable接口,作为value的对象则没有任何要求。

注意:TreeMap在比较两个key是否相等时,依赖key的compareTo()方法或者Comparator.compare()方法。在两个key相等时,必须返回0。

8. 使用Properties

配置文件的特点是:key-value一般都是String-String类型的,因此我们完全可以用Map<String, String>来表示它。配置文件十分常用,所以Java集合库提供了一个Properties来表示一组“配置”。由于历史遗留原因,Properties内部本质是一个Hashtable,但我们只需要用到Properties自身关于读写配置的接口。

读取配置文件

Java默认配置文件以“.properties”为扩展名,每行以“key=value”表示,以“#”开头的是注释。读取配置文件步骤:

①创建Properties实例;

②调用load()读取文件;

③调用getProperty()获取配置。

写入配置文件

如果通过setProperty()修改了Properties实例,可以把配置写入文件,以便下次启动时获得最新配置。写入配置文件使用store()方法。

9. 使用Set

如果只需要存储不重复的key,并不需要存储映射的value,就可以使用Set。Set用于存储不重复的元素集合,它主要提供以下几个方法:

  • boolean add(E e):将元素添加进Set<E>

  • boolean remove(Object e):将元素从Set<E>删除;

  • boolean contains(Object e):判断是否包含元素。

Set实际上相当于只存储key、不存储value的Map。经常使用Set去除重复元素。因为放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set。最常用的Set实现类是HashSet,实际上HashSet仅仅是对HashMap的一个简单封装。

Set接口并不保证有序,而SortedSet接口则保证元素是有序的:

  • HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;

  • TreeSet是有序的,因为它实现类SortedSet接口。

使用TreeSet和使用TreeMap的要求一样,添加的元素必须正确实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。

10. 使用Queue

队列Queue实际上是实现了一个先进先出的有序表。它与List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

  • 把元素添加到队列末尾;

  • 从队列头部取出元素。

在Java标准库中,队列接口Queue定义了以下几个方法:

  • int size():获取队列长度;

  • boolean add(E) / boolean offer(E):添加元素到队尾;

  • E remove() / E poll():获取队首元素并从队列中删除;

  • E element() / E peek():获取队首元素但不从队列中删除。

其中,添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,两个方法的行为是不同的,前者抛出异常,即throw Exception,而后者返回false或null。

注意:不要把null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。

11. 使用PriorityQueue

Queue会严格按FIFO的原则取出队首元素,而PriorityQueue的出队顺序与元素的优先级有关,对其调用remove()或poll()方法,返回的总是优先级最高的元素。

放入PriorityQueue的元素必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。

12. 使用Deque

双端队列:两头都进,两头都出。Deque的功能是:

  • 既可以添加到队尾,也可以添加到队首;

  • 既可以从队首获取,又可以从队尾获取。

Deque接口实际上扩展自Queue,因此Queue提供的add()/offer()方法在Deque中也可以调用,但是使用Deque最好不要调用offer(),而是调用offerLast()。

Deque是一个接口,它的实现类有ArrayDeque和LinkedList。其中,LinkedList既是List,又是Queue,还是Deque。我们在使用时,总是用特定的接口来引用它,这是因为持有接口说明代码的抽象层次更高,而且接口本身定义的方法代表了特定的用途。

抽象编程的一个原则:尽量持有接口,而不是具体的实现类。

13. 使用Stack

栈:后进先出的数据结构。

Stack只有入栈和出栈操作:

  • push(E):把元素压栈;

  • pop(E):把栈顶的元素“弹出”;

  • peek(E):取栈顶元素但不弹出。

在Java中,可以使用Deque实现Stack的功能。

Stack的作用

①JVM在处理Java方法调用的时候就会通过栈维护方法调用的层次。JVM创建方法调用栈,每调用一个方法时,先将参数压栈,然后执行对应的方法;当方法返回时,返回值压栈,调用方法通过出栈操作获得方法返回值。因为方法调用栈有容量限制,嵌套调用过多会造成栈溢出,引发StackOverflowError。

②对整数进行进制转换。

③计算中缀表达式。计算机执行表达式时不能直接计算中缀表达式,而是通过编译器把中缀表达式转换成后缀表达式,其编译过程就会用到栈。

14. 使用Iterator

Java的集合类都可以使用for each循环,List、Set和Queue会迭代每个元素,Map会迭代每个key。实际上,Java编译器并不知道如何遍历List,编译成功只是因为编译器把for each循环通过Iterator改写为了普通的for循环。

使用迭代器的好处:调用方总是以统一的方式遍历各种集合类型,而不必关心它们内部的存储结构。

自定义的集合类想要使用for each循环,只需要满足以下条件:

  • 集合类实现Iterable接口,该接口要求返回一个Iterator对象;

  • 用Iterator对象迭代集合内部数据。

15. 使用Collections

Collections,JDK提供的工具类,位于java.util包中,提供了一系列静态方法,能更方便地操作各种集合。

  • 创建空集合:返回的空集合是不可变集合,无法向其中添加或删除元素;

  • 创建单元素集合:返回的单元素集合是不可变集合,无法向其中添加或删除元素;

  • 排序

  • 洗牌

  • 不可变集合:把可变集合封装成不可变集合,这种封装实际上是通过创建一个代理对象,拦截掉所有修改方法实现的;

  • 线程安全集合:把线程不安全的集合变成线程安全的集合。

附:廖雪峰Java集合框架教程链接

原文地址:https://www.cnblogs.com/java-learning-xx/p/13096593.html