[TOC]
一、类集框架简介
1. 类集框架简介
从JDK1.2开始,Java引入了类集开发框架,所谓的类集指的就是一套动态对象数组的实现方案,在实际的开发之中,没有任何一项的开发可以离开十足,但是传统的数组实现起来非常的繁琐。而且长度是其致命伤。正是因为长度的问题,所以传统的数组是不可能大范围使用的,但是我们的开发又不可能离开数组,所以最初就只能依靠一些数据结构来实现动态的数组处理,而其中最为重要的两个结构: 链表、树,但是面对这些数据结构的实现又不得不面对如下的一些困哪:
- 数据结构的代码实现困难,对于一般的开发者是无法进行使用的;
- 对于链表或二叉树当进行更新处理的时候的维护是非常麻烦的;
- 对于链表或二叉树还需要尽可能保证其操作的性能。
正是因为这样的原因,所以从JDK1.2开始Java引入了类集,主要就是对常见的数据结构进行完整的实现包装,并且提供有一系列的接口与实现子类来帮助用户减少数据结构所带来的的开发困难,
这个就是类集的产生意义所在。以后数据结构不要自己来写了,但是你千万要记住一个过程: 对于现阶段开发里面,类集有它一些固定的使用方法,但是我们在进行子类选择的时候,我们一定要知道它的各个子类的实现算法是怎么回事,这样才能确保我们的程序高效。
举个例子来讲,如果在正常的设计选择过程当中,我发现我存的数据是固定长度或者说绝对不会超过某些长度,应该是用数据来操作是最简单的吧,因为什么? 因为数组是一个线性结构,时间复杂度为1。
也就是说,正常情况来讲,类集里面它也要区分子类的不同而有所不同。
但是最初的类集实现由于Java本身的技术所限,所以对于数据的控制并不严格,全部采用了Object类型进行数据接收,而在JDK1.5之后由于泛型技术的推广,所以类集本身也得到了良好的改进,可以直接利用泛型来保存相同类型的数据,并且随着数据量的不断增加,从JDK1.8开始类集中的实现算法也得到了良好的性能提升。
但是你们在学习之中应该清楚的知道类集之中所采用的性能提高的方式是什么,这个必须自己能够说出来
在整个类集框架里面提供有如下的几个核心接口: Collection、List、Set、Map、Iterator、Enumeration、Queue、ListIterator。
2. Collection接口简介
java.util.Collection是单值集合操作的最大的父接口,在该接口之中定义有所有的单值数据的处理操作。这个接口之中定义有如下的核心操作方法:
No | 方法名称 | 类型 | |
---|---|---|---|
01 | public boolean add(E e) | 普通 | 向集合保存数据 |
02 | public boolean addAll(Collection<? extends E> e) | 普通 | 追加一组数据 |
03 | public void clear() | 普通 | 清空集合,让根节点为空,同时执行GC处理 |
04 | public boolean contains(Object o) | 普通 | 查询数据是否存在,需要equals()方法支持 |
05 | public boolean remove(Object o) | 普通 | 数据删除,需要equals()方法支持 |
06 | public int size() | 普通 | 获取数据长度 |
07 | public Object[] toArray() | 普通 | 将集合变为对象数组返回 |
08 | public Iterator<E> iterator() | 普通 | 将集合变为Iterator接口 |
在进行集合操作的时候有两个方法最为常用:【增加】add()、【输出】iterator(),在JDK1.5版本以前,Collection只是一个独立的接口,但是从JDK1.5之后提供有了Iterable父接口,并且在JDK1.8的之后针对于Iterable接口也得到了一些扩充。
另外在JDK1.2~JDK1.4的时代如果要进行集合的使用往往会直接操作Collection的接口,但是从JDK1.5时代开始更多的情况下选择的都是Collection的两个子接口: 允许重复的List子接口、不允许重复的Set子接口;
现阶段的开发不要再去用Collection了,这个主要是 微软的Sun的 战争引起的。
二、List集合
1. List接口简介
List是Collection的子接口,其最大的特点是允许保存有重复元素数据,该接口的定义如下:
1 | public interface List<E> extends Collection<E> |
但是需要清楚的是List子接口对于Collection接口进行了方法扩充。
No | 方法名称 | 类型 | |
---|---|---|---|
01 | public E get(int index) | 普通 | 获取指定索引上的数据 |
02 | public E set(int index,E element) | 普通 | 修改指定索引数据 |
03 | public ListIterator |
普通 | 返回ListIterator接口对象 |
但是List本身依然属于一个接口,那么对于接口要想使用则一定要使用子类来完成定义,在List子接口中有三个常用子类: ArrayList、LinkedList、Vector。
从JDK1.9开始,List子接口里面追加有一些static方法,以方便用户的处理。
范例: 观察List中的静态方法
1 | package cn.ngp.demo; |
这些操作方法并不是List传统用法,是在新版本之后添加的新功能。
在开发当中,因为JDK9或JDK10没有广泛开来,所以大家最好用原始的方式开发
2. ArrayList子类
ArrayList是 List子接口使用最多的一个子类,但是这个子类在使用的时候也是有前提要求的,所以本次来对这个类的相关定义以及源代码组成进行分析,在 Java 里面ArrayList类的定义如下:
1 | public class ArrayList<E> |
ArrayList子类的继承结构
范例: 使用ArrayList实例化List父接口
1 | package cn.ngp.demo; |
通过本程序可以发现List存储的特征:
- 保存的顺序就是其存储顺序;
- List集合里面允许存在有重复数据。
在以上的程序里面虽然实现了集合的输出,但是这种输出的操作是直接利用了每一个类提供的toString()方法实现的,为了方便的进行处理,在JDK1.8之后Iterable父接口之中定义有一个forEach()方法,方法定义如下:
- 输出支持: default void forEach(Consumer<? super T> action)
范例: 利用forEach()方法输出(不是标准输出)
只是在使用java的时候方便一些,正常开发是不可能用它来完成的
1 | package cn.ngp.demo; |
需要注意的是,此种输出并不是在正常开发情况下要考虑的操作形式。
范例: 观察List集合的其他操作方法
1 | package cn.ngp.demo; |
如果以方法的功能为例,那么ArrayList里面操作支持与之前编写的链表形式是非常相似的,但是它并不是使用链表来实现的,通过类名称实际上就已经可以清楚的发现了,ArrayList应该封装的是一个数组。
ArrayList构造: public ArrayList() |
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } -------------------------------- public |
---|---|
private static final Object[] EMPTY_ELEMENTDATA = {}; | |
ArrayList构造: public ArrayList(int initialCapacity) |
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } |
transient Object[] elementData;// non-private to simplify nested class access |
通过有参构造方法可以发现,在ArrayList里面所包含的数据实际上就是一个对象数组。如果现在在进行数据追加的时候发现ArrayList集合里面保存的对象数组的长度不够的时候,会进行新的数组开辟,同时将原始的旧数组内容拷贝到新数组之中,而后数组的开辟操作:
1 | private int newCapacity(int minCapacity) { |
如果在实例化ArrayList类对象的时候并没有传递初始化的长度,则默认情况下会使用一个空数组,但是如果在进行数据增加的时候发现数组容量不够了。则会判断当前的增长的容量与默认的容量的大小,使用较大的一个数据进行新的数组开辟,所以可以得出一个结论:
JDK1.9之后: | ArrayList默认的构造只会使用默认的空数组,使用的时候才会开辟数组,默认的开辟长度为10; |
---|---|
JDK1.9之前 | ArrayList默认的构造实际上就会默认开辟大小为10的数组。 |
当ArrayList之中保存的容量不足的时候会采用成倍的方式进行增长,原始长度为10,那么下次的增长就是20,依此类推。
在使用ArrayList子类的时候一定要估算出你的数据量会有多少,如果超过了10个,那么使用有参构造方法进行创建,以避免垃圾数组的空间产生。
3. ArrayList保存自定义类对象
通过之前的分析已经清楚了ArrayList子类的实现原理以及List核心操作,但是在测试的时候使用的是系统提供的String类,这是一个设计非常完善的类,而对于类集而言也可以实现自定义类对象的保存。
范例: 实现自定义类对象的保存
1 | package cn.ngp.demo; |
在使用List保存自定义类对象的时候如果需要使用到contains()、remove()方法进行查询与删除处理的时候,一定要保证类之中已经成功覆写了equals()方法。
4. LinkedList子类
在List接口里面还有另外一个比较常用的子类: LinkedList,这个类通过名称就已经可以发现其特点了: 基于链表的实现,那么我们首先来观察一下LinkedList子类的定义:
范例: 使用LinkedList实现集合操作
1 | package cn.ngp.demo; |
如果说现在只是观察程序的功能你会发现和ArrayList使用是完全一样的,但是其内部的实现机制是完全不同的,首先观察LinkedList构造方法里面并没有提供有像ArrayList那样的初始化大小的方法,而只是提供有无参构造处理: “public LinkedList()” 随后观察add()方法的具体实现。
1 | public boolean add(E e) { |
在之前编写自定义链表的时候,是判断了传入数据是否为null,如果为null则不进行保存,但是在LinkedList里面并没有做这样的处理,而是所有的数据都可以保存,而后此方法调用了linkLast()方法 (在最后一个节点进行追加)。
1 | void linkLast(E e) { |
在LinedList类里面保存的数据都是利用Node节点进行的封装处理,同时为了提高程序执行性能,每一次都会保存上一个追加的节点(最后一个节点),这样就可以在增加数据的时候避免递归处理,在增加数据的时候要进行数据保存个数的追加。
通过一系列的分析之后可以发现,LinkedList封装的就是一个链表实现。
面试题: 请问ArrayList与LinkedList有何区别?
- ArrayList是数组实现的集合操作,而LinkedList是链表实现的集合操作。
- 在使用List集合中的get()方法根据索引取数据时,ArrayList的时间复杂度为"O(1)",而LinkedList时间复杂度为"O(n)"(n为集合的长度)
- ArrayList在使用的时候,默认的初始化对象数组的大小长度为10,如果空间不足则会采用2倍的形式进行容量的扩充,如果保存大数据量的时候有可能会造成垃圾的产生以及性能的下降,但是这个时候可以使用LinkedList子类保存。
5. Vector子类
Vector是一个原始古老的程序类,这个类是在JDK1.0的时候就提供的,而后到了JDK1.2的时候由于许多的开发者已经习惯于使用Vector,并且许多的系统类也是基于Vector实现的,考虑到其使用的广泛性,所以类集框架将其保存了下来,并且让其多实现了一个List接口,观察Vector定义结构:
1 | public class Vector<E> |
继承结构与ArrayList是相同的,所以来讲这个类继承结构如下:
范例: Vector类的使用
1 | package cn.ngp.demo; |
下面可以进一步的观察Vector类实现:
1 | public Vector() { |
Vector类如果使用的是无参构造方法,则一定会默认开辟一个10个长度的数组,而后其余的实现操作与ArrayList是相同的,通过源代码的分析可以发现Vector类之中的操作方法采用的都是synchronized同步处理,而ArrayList并没有进行同步处理,所以Vector类之中的方法在多线程访问的时候属于线程安全的,但是性能不如ArrayList高。
三、Set集合
1. Set接口简介
Set集合最大的特点就是不允许保存重复元素,其也是Collection子接口。
在JDK1.9以前Set集合与Collection集合的定义并无差别,Set继续使用了Collection接口中提供的方法进行操作,但是从JDK1.9之后,Set集合也像List集合一样扩充了一些static方法,Set集合的定义如下:
1 | public interface Set<E> |
需要注意的是Set集合并不像List集合那样扩充了许多的新方法,所以无法使用List集合中提供的get()犯法,也就是说,它无法实现指定索引数据的获取,Set接口的继承关系如下。
从JDK1.9之后,Set集合也提供像List集合之中类似的of()的静态方法。下面就使用此方法进行Set集合特点的验证。
范例: 验证Set集合特征
1 | package cn.ngp.demo; |
程序运行结果: Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: Hello
当使用of()这个新方法的时候如果发现集合之中存在有重复元素则会直接抛出异常。这与传统的Set集合不保存重复元素的特点相一致,只不过自己抛出了异常而已。
Set集合的常规使用形式一定是依靠子类进行实例化的,所以Set接口之中有两个常用子类: HashSet、TreeSet。
2. HashSet子类
HashSet是Set接口里面使用最多的一个子类,其最大的特点就是保存的数据是无序的,而HashSet子类的继承关系如下:
1 | public class HashSet<E> |
这种继承的形式和之前的ArrayList是非常相似的,那么现在来观察一下类的继承结构。
范例: 观察HashSet子类
1 | package cn.ngp.demo; |
通过执行结果就可以发现HashSet子类的操作特点: 不允许保存重复元素(Set接口定义的),另外一点HashSet之中保存的数据是无序的。
3. TreeSet子类
Set接口的另外一个子类就是TreeSet,与HashSet最大的区别在于TreeSet集合里面所保存的数据是有序的,首先来观察一下TressSet类的定义:
1 | public class TreeSet<E> |
在这个子类里面依然继承了AbstractSet父抽象类,同时又实现了一个NavigableSet父接口。
范例: 使用TreeSet子类
1 | package cn.ngp.demo; |
当利用TreeSet保存的数据的时候所有的数据都将按照数据的升序进行自动排序处理。
4. TreeSet子类排序操作
经过分析之后TreeSet子类之中爆粗你的数据是允许排序的,但是这个类必须实现Comparable接口,因为只有实现了此接口才能够确认出对象的大小关系。
提示: TreeSet本质上是利用TreeMap子类实现的集合数据的存储,而TreeMap(树) 就需要根据Comparable来确定大小关系。
那么下面就使用一个自定义的类来实现排序的处理操作。
范例: 实现自定义类排序
1 | package cn.ngp.demo; |
在使用自定义类对象进行比较处理的时候一定要将该类之中的所有属性都依次进行大小关系的匹配,否则如果某一个或某几个属性相同的时候,它也会认为是重复数据,所以TreeSet是利用了Comparable接口来确认重复数据的。
由于TreeSet在操作过程之中需要将类中的所有属性进行比对,这样的实现难度太高了,那么在实际的开发之中应该首选HashSet子类进行存储。
5. 分析重复元素消除
TreeSet子类是利用了Comparable接口来实现了重复元素的判断,但是Set集合的整体特征就是不允许保存重复元素。但是HashSet判断重复元素的方式并不是利用Comparable接口完成的,它利用的是Object类中提供的方法实现的:
- 对象编码: public int hashCode()
- 对象比较: public boolean equals(Object obj)
在进行重复元素判断的时候首先利用hashCode()进行编码的匹配,如果改编码不存在则表示数据不存在,证明没有重复,如果该编码存在了,则进一步进行对象比较处理,如果发现重复了,则此数据是不允许保存的。如果使用的是Eclipse开发工具,则可以帮助开发者自动创建hashCode()与equals()方法。
范例: 实现重复元素处理
1 | package cn.ngp.demo; |
在Java程序之中真正的重复元素的判断处理利用的就是hashCode()与equals()两个方法共同作用完成的,而只有在排序要求的情况下(TreeSet)才会利用Comparable接口来实现。
四、集合输出
集合输出实际上从JDK1.8开始就在Iterable接口之中提供有一个forEach()方法,但是这种方法的迭代输出并不是传统意义上的集合输出形式,并且也很难在实际的开发之中出现,对于集合操作而言,一共有四种输出形式: Iterator迭代输出(95%)、ListIterator双向迭代输出(0.1%)、Enumeration枚举输出(4.9%)、foreach输出(与Iterator相当)。
1. Iterator迭代输出
通过Collection接口的继承关系可以发现,从JDK1.5开始其多继承了一个Iterable父接口,并且在这个接口里面定义有一个Iterator()操作方法,通过此方法可以获取Iterator接口对象(在JDK1.5之前,这一方法直接定义在Collection接口之中)。
- 获取Iterator接口对象: public Iterator
iterator()
在Iterator接口里面定义有如下的方法:
No | 方法名称 | 类型 | |
---|---|---|---|
01 | boolean hasNext() | 普通 | 判断是否有数据 |
02 | public E next() | 普通 | 取出当前数据 |
03 | public default void remove() | 普通 | 删除 |
在之前使用的java.util.Scanner类就是Iterator接口的子类,所以此时类的继承关系如下:
范例: 使用Iterator输出
1 | package cn.ngp.demo; |
但是对于Iterator接口中的remove()方法的使用需要特别注意一下(如果不是必须不要使用)。实际上在Collection接口里面定义有数据的删除操作方法,但是在进行迭代输出的过程里面如果你使用了Collection中的remove方法会导致迭代失败。
范例: 采用Collection集合中的remove()方法删除
?为什么我自己写代码的时候,只是有的会显示错误信息捏 =。=? 例如: 下面的代码改为删除World的话 =。=
1 | package cn.ngp.demo; |
此时无法进行数据的删除处理操作,那么此时就只能够利用Iterator接口中的remove()方法删除。
1 | package cn.ngp.demo; |
此时程序执行之后没有出现任何的错误,并且可以成功的删除原始集合中的数据。
面试题: 请解释Collection.remove() 与 Iterator.remove() 的区别?
- 在进行迭代输出的时候如果使用了Collection.remove()则会造成并发更新的异常,导致程序删除出错,而此时只能利用Iterator.remove()方法实现正常的删除处理。
2. ListIterator双向迭代输出
使用Iterator进行的迭代输出操作有一个特点: 只允许由前向后实现输出,而如果说你现在需要进行双向迭代处理,那么就必须依靠Iterator的子接口: ListIterator接口来实现了。需要注意的是如果要想获取ListIterator接口对象Collection并没有定义相关的处理方法,但是List子接口有,也就是说这个输出接口是专门为List集合准备的。
在ListIterator接口里面定义有如下的操作方法:
-
判断是否有前一个元素: public boolean hasPrevious()
-
获取当前元素: public E previous()
范例: 实现双向迭代
1 | package cn.ngp.demo; |
如果要想实现右后向前的遍历,那么首先要实现的是由前向后实现遍历处理。
例如: 把上面代码的由前向后输出部分注释掉,右后向前则没有输出了。
3. Enumeration枚举输出
Enumeration是在JDK1.0的时候就使用的输出接口,这个输出接口主要是为了Vector类提供输出服务的,一直到后续的JDK的发展,Enumeration依然只为Vector一个类服务,如果要想获取Enumeration接口对象,就必须依靠Vector类提供的方法:
- 获取Enumeration: public Enumeration
elements()
在Enumeration接口之中定义有两个操作方法:
- 判断是否有下一个元素: public boolean hasMoreElements()
- 获取当前元素: public E nextElement()
范例: 使用Enumeration实现输出
1 | package cn.ngp.demo; |
由于该接口出现的时间比较长了,所以在一些比较早的开发过程之中,也有部分的方法只支持Enumeration的输出操作,但是随着类方法的不断完善,大部分的操作都直接利用Iterator实现了。
4. foreach输出
除了使用迭代接口实现输出之外,从JDK1.5开始加强型for循环也可以实现集合的输出了。这种输出的形式与数组的输出操作形式类似。
范例: 使用foreach输出
1 | package cn.ngp.demo; |
这种输出最初出现的时候很多人并不建议使用,因为标准的集合操作还是应该以Iterator为主,但是毕竟JDK1.5都已经推出十多年了,很多的语法也开始被大部分人所习惯。
五、Map集合
在之前已经学习了Collection接口以及其对应的子接口,可以发现在Collection接口之中所保存的数据全部都只是单个对象,在数结构里面除了可以进行单个对象的保存之外,实际上也可以进行二元偶对象的保存(key=value)的形式来存储,而存储二元偶对象的核心意义在于,需要通过key获取对应的value。
在开发里面:Collection集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。
1. Map接口简介
Map接口是进行二元偶对象保存的最大父接口,该接口定义如下:
1 | public interface Map<K,V> |
该接口为一个独立的父接口,并且在进行接口对象实例化的时候需要设置Key与Value的类型,也就是说在整体操作的时候需要保存两个内容,在Map接口里面定义有许多的操作方法,但是需要记住以下的核心操作方法:
No | 方法名称 | 类型 | |
---|---|---|---|
01 | public V put(K key,V value) | 普通 | 向集合之中保存数据 |
02 | public V get(Object key) | 普通 | 根据key查询数据 |
03 | public Set<Map.Entry<K,V>> entrySet() | 普通 | 将Map集合转为Set集合 |
04 | public boolean containsKey(Object key) | 普通 | 查询指定的key是否存在 |
05 | public Set |
普通 | 将Map集合中的key转为Set集合 |
06 | public V remove(Object key) | 普通 | 根据key删除掉指定的数据 |
从JDK1.9之后Map接口里面也扩充了一些静态方法供用户使用。
范例: 观察Map集合的特点
1 | package cn.ngp.demo; |
在Map集合之中数据的保存就是按照 “key=value” 的形式存储的,并且使用of()方法操作的时候,里面的数据是不允许重复的,如果重复则会出现 “IllegalArgumentException” 异常,如果设置的内容为null,则会出现"NullPointerException"异常。
对于现在见到的of()方法严格意义上来讲并不是Map集合的标准用法,因为正常的开发之中需要通过Map接口的子类来进行接口对象的实例化,而常用的子类:HashMap、Hashtable、TreeMap、LinkedHashMap。
2. HashMap子类
HashMap是Map接口之中最为常见的一个子类,该类的主要特点是 无序存储,通过java文档首先来观察一下HashMap子类的定义形式:
1 | public class HashMap<K,V> |
该类的定义继承形式符合之前的集合定形式,依然提供有抽象类,并且依然需要重复实现Map接口。
范例: 观察Map集合的使用
1 | package cn.ngp.demo; |
以上的操作形式为Map集合使用的最标准的处理形式,通过代码可以发现,通过HashMap实例化的Map接口可以针对key或value保存null的数据,同时也可以发现及时保存数据的key重复,那么也不会出现错误,而是出现内容的替换。
但是对于Map接口中提供的put()方法本身是提供有返回值的,那么这个返回值指的是在重复key的情况下返回key的value
范例: 观察put()方法
1 | package cn.ngp.demo; |
在设置了相同的key的内容的时候put()方法会返回原始的数据内容.
清楚了HashMap的基本功能之后, 下面就需要来研究一下HashMap之中给出的源代码. HashMap之中肯定要需要存储大量的数据, 那么对于数据的存储
1 | public HashMap() { |
当使用无参构造的时候会出现有一个loadFactor属性, 并且该属性默认的内容为 “0.75” (
static final float DEFAULT_LOAD_FACTOR = 0.75f;
)
1 | public V put(K key, V value) { |
在使用put()方法进行数据保存的时候会调用一个putVal()方法,同时会将key进行hash处理(生成一个hash码), 而对于putValue方法里面会发现依然会提供有一个Node节点类进行数据的保存, 而在使用putValue()方法操作的过程之中会调用有一个resize()的方法可以进行容量的扩充.
面试题: 在进行HashMap的put()操作的时候,如何实现容量扩充的?
- 在HashMap类里面提供有一个 “DEFAULT_INITIAL_CAPACITY” 常量,作为初始化的容量配置,而后这个常量的默认大小为16个元素, 也就是说默认可以保存的最大内容是16;
- 当保存的内容的容量超过了一个阈值 (DEFAULT_LOAD_FACTOR = 0.75f), 相当于 “容量*阈值=12” 保存12个元素的时候就会进行容量的扩充;
- 在进行扩充的时候HashMap采用的是成倍的扩充模式, 即: 每一次都扩充2倍的容量
面试题: 请解释HashMap的工作原理(JDK1.8之后开始的)
- 在HashMap之中进行数据存储的依然是利用了Node类完成的, 那么这种情况下就证明可以使用的数据结构只有两种: 链表(时间复杂度"O(n)")、二叉树(时间复杂度"O(logn)");
- 从JDK1.8开始, HashMap的实现出现了改变, 因为其要适应于大数据时代的海量数据问题,所以对于其存储发生了变化, 并且在HashMap类的内部提供有一个重要的常量: “static final int UNTREEIFY_THRESHOLD = 6;”, 在使用HashMap进行数据保存的时候,如果保存的数据个数没有超过阈值8(UNTREEIFY_THRESHOLD), 那么会按照链表的形式进行存储, 而如果超过了这个阈值, 则会将链表转为红黑树以实现树的平衡, 并且利用左旋与右旋保证数据的查询性能.
3. LinkedHashMap子类
HashMap虽然是Map集合最为常用的一个子类, 但是其本身所保存的数据都是无序的(有序与否对Map没有影响), 如果现在希望Map集合之中保存的顺序为其增加顺序, 则就可以更换子类为LinkedHashMap(基于链表实现的), 观察LinkedHashMap类的定义形式:
1 | public class LinkedHashMap<K,V> |
既然是链表保存, 所以一般在使用LinkedHashMap类的时候往往数据量都不要特别大, 因为会造成时间复杂度攀升. 通过继承结构可以发现LinkedHashMap是HashMap子类,继承关系如下:
范例: 使用LinkedHashMap
1 | package cn.ngp.demo; |
通过此事的程序执行可以发现当使用LinkedHashMap进行存储之后所有数据的保存顺序为我们的添加顺序.
4. Hashtable子类
Hashtable类是从JDK1.0的时候提供的, 与Vector、Enumeration属于最早的一批动态数组的实现类,后来为了将其继续保存下来所以让其多现实现了一个Map接口, Hashtable类的定义如下:
1 | public class Hashtable<K,V> |
范例: 观察Hashtable子类的使用
1 | package cn.ngp.demo; |
通过观察可以发现在Hashtable里面进行数据存储的时候设置的key或value都不允许为null, 否则会出现NullPointerException异常.
面试题: 请解释HashMap与Hashtable的区别?
- HashMap中的方法都属于异步操作,(非线程安全), HashMap允许保存有null数据
- Hashtable中的方法都属于同步方法(线程安全), Hashtable不允许保存null, 否则会出现NullPointerException.
5. Map.Entry接口
虽然已经清楚了整个的Map集合的基本操作形式, 但是依然需要有一个核心的问题要解决, Map集合里面是如何进行数据存储的? 对于List而言(LinkedList子类) 依靠的是链表的形式实现的数据存储, 那么在进行数据存储的时候一定要将数据保存在一个Node节点之中, 虽然在HashMap里面也可以见到Node类型定义, 通过源代码定义可以发现, HashMap类中的Node内部类本身实现了Map.Entry接口.
1 | static class Node<K,V> implements Map.Entry<K,V> {} |
所以可以得出结论: 所有的key和value的诗句都被封装在Map.Entry接口之中, 而此接口定义如下:
1 | public static interface Map.Entry<K,V> |
并且在这个内部接口里面提供有两个重要的操作方法:
- 获取key: public K getKey()
- 获取value: public V getValue()
在JDK1.9以前的开发版本之中, 使用者基本上都不会去考虑创建Map.Entry的对象, 实际上在正常的开发过程之中使用者也不需要关心Map.Entry对象的创建, 可是从 JDK1.9之后, Map接口里面追加有一个新的方法
- 创建Map.Entry对象: public static <K,V> Map.Entry<K,V> entry(K k, V v)
范例: 创建Map.Entry对象
1 | package cn.ngp.demo; |
通过分析可以发现在整个Map集合里面, Map.Entry的主要作用就是作为一个Key和Value的包装类型使用, 而大部分情况下在进行数据存储的时候都会将key和value包装为一个Map.Entry对象进行使用.
6. 利用Iterator输出Map集合
对于集合的输出而言, 最标准的做法就是利用Iterator接口来完成, 但是需要明确一点的是在Map集合里面并没有一个方法可以直接返回Iterator接口对象, 所以这种情况下就必须分析不直接提供Iterator接口实例化的方法的原因, 下面对Collection与Map集合的存储结构进行一个比较处理.
发现在Map集合里面保存的实际上是一组Map.Entry接口对象(里面包装的是Key与Value), 所以整个来讲Map依然实现的是单值的保存, 这样在Map集合里面提供有一个方法 “public Set<Map.Entry<K,V>> entrySet()”, 将全部的Map集合转为Set集合.
经过分析可以发现如果要想使用Iterator实现Map集合的输出则必须按照如下步骤处理:
- 利用Map接口中提供的entrySet()方法将Map集合转为Set集合
- 利用Set接口中的iterator()方法将Set集合转为Iterator接口实例;
- 利用Iterator进行迭代输出获取每一组的Map.Entry对象, 随后通过getKey()与getValue()获取数据
范例: 利用Iterator输出Map集合
1 | package cn.ngp.demo; |
虽然Map集合本身支持有迭代输出的支持, 但是如果从实际的开发来讲, Map集合最主要的用法在于实现数据的Key查找操作, 另外需要提醒的是, 如果现在不使用Iterator而使用foreach语法输出则也需要将Map集合转为Set集合.
范例: 使用foreach输出Map集合
1 | package cn.ngp.demo; |
由于map迭代输出的情况相对较少, 所以对于此类的语法应该深入理解一下, 并且一定要灵活掌握.
因为在实际开发之中, 这些集合互相倒来倒去的情况是非常常见的
7. 自定义Map的key类型
在使用Map集合的时候可以发现对于Key和Value的类型实际上都可以由使用者任意决定, 那么也就意味着现在依然可以使用自定义的类来进行key类型的设置. 对于自定义Key类型所在的类中一定要覆写hashCode()与equals()方法, 否则无法查找到.
1 | public V put(K key, V value) { |
在进行数据保存的时候发现会自动使用传入的key的数据生成一个hash码, 也就是说存储的时候是这个Hash数值.
1 | public V get(Object key) { |
在根据key获取数据的时候依然要将传入的key通过hash()方法爱获取其对应的hash码, 那么也就证明, 查询的过程之中首先要利用我们的hashCode()来进行数据查询, 当使用getNode()方法查询的时候还需要使用到euqals()方法.
范例: 使用自定义类作为Key类型
1 | package cn.ngp.demo; |
虽然允许你使用是定义的类作为key的类型, 但是也需要注意一点, 在实际的开发之中对于Map集合的Key常用的类型就是三种: String、Long、Integer, 尽量使用系统类.
面试题: 如果在进行HashMap进行数据操作的时候出现了Hash冲突(Hash码相同), HashMap是如何解决的?
当出现了Hash冲突之后为了保证程序的正常执行, 会在冲突的位置上将所有Hash冲突的内容转为链表保存.
六、集合工具类
1. Stack栈
栈是一种先进后出的数据结构. 例如: 在文本编辑器上都有撤销功能, 那么每次使用的时候你会发现, 最后一次的编辑操作永远是最先撤销, 那么这个功能就是利用栈来实现的, 栈的及基本操作形式如下:
在Java程序里面使用Stack来描述栈的操作, 这个类定义如下:
1 | public class Stack<E> |
可以发现Stack是Vector子类, 但是它使用的并不是Vector类之中所提供的方法, 而是采用如下的两个方法:
- 入栈: public E push(E item)
- 出栈: public E pop()
1 | package cn.ngp.demo; |
通过此时的数据可以发现, 所有的数据保存之后将按照倒序的形式进行弹出, 如果栈已经空了, 则会抛出空栈异常.
2. Queue队列
Queue描述的是一个队列, 而队列的主要特点是实现先进先出的操作形式. 其基本的操作形式如下:
如果将队列引用在多线程的 “生产者与消费者” 的模型处理上, 那么对于生产者过快生产过快的情况下,就没有必要等待消费者获取数据了, 可以将所有的内容直接保存在队列之中, 队列的实现可以使用LinkedList子类来完.
队列的使用主要依靠Queue接口之中提供的方法来处理. 提供有如下方法:
- 向队列之中追加数据: public boolean offer(E e), 可以直接使用add()方法
- 通过队列获取数据: public E poll(), 弹出后删除数据
范例: 实现队列操作
1 | package cn.ngp.demo; |
除了LinkedList子类之外, 还有一个优先级的概念, 可以使用PriorityQueue实现优先级队列(比较功能),
范例: 使用优先级队列
1 | package cn.ngp.demo; |
对于队列的选用原则也是需要根据实际的项目环境来决定的.
3. Properties属性操作
在之前讲解国际化程序的时候讲解过资源文件(*.properties), 那么这类文件的存储结构是按照 “key=value” 的, 而这种结构的保存形式与Map集合很相似, 但是唯一的区别在于其所保存的内容只能够是字符串, 所以为了可以方便的描述属性的定义, java.util包里面提供有Properties类型, 此类是Hashtable的子类.
1 | public class Properties extends Hashtable<Object,Object> |
可以发现在继承Hashtable的时候为Hashtable中定义的泛型为Object, Properties是不需要操作泛型的, 因为它可以操作类型只能是String类型. 在Properties之中如果要想实现属性的操作可以采用如下的方法来实现:
No | 方法名称 | 类型 | |
---|---|---|---|
01 | public Object setProperty(String key, String value) | 普通 | 设置属性 |
02 | public String getProperty(String key) | 普通 | 取得属性, key不存在返回null |
03 | public String getProperty(String key, String defaultValue) | 普通 | 取得属性, key不存在返回默认值 |
04 | public void store(OutputStream out, String comments) throws IOException | 普通 | 输出属性内容 |
05 | public void load(InputStream inStream) throws IOException | 普通 | 通过输入流读取属性内容 |
范例: 观察属性的设置与取得
1 | package cn.ngp.demo; |
通过代码可以发现Properties里面可以像Map结婚那样进行内容的设置与获取, 但是唯一的差别是它只能够操作String类型, 另外需要注意的是, 之所以会提供有Properties类还有一个最重要的功能是它可以通过输出流输出属性, 也可以使用输入流读取属性内容.
范例: 将属性内容保存在文件之中
1 | package cn.ngp.demo; |
通过程序的执行可以发现, 的确可以实现资源文件的输入处理, 但是如果输出的是中文则自动帮助用户进行转码处理.
范例: 读取资源文件
1 | package cn.ngp.demo; |
使用Properties类型的最大特点是可以进行资源内容的输入与输出的处理操作, 但是在实际的开发之中Properties往往用于读取配置资源的信息, 这一点主要是在标准设计之中做程序初始化准备的时候使用.
4. Collections工具类
Collection是java提供的一组集合数据的操作工具类, 也就是说利用它可以实现各个集合的操作.
范例: 使用Collection操作List集
1 | package cn.ngp.demo; |
范例: 数据的反转
1 | package cn.ngp.demo; |
范例: 使用二分查找
1 | package cn.ngp.demo; |
大部分情况下对于集合的使用可能没有这么多复杂要求, 更多的情况下就是利用集合保存数据, 要么进行输出要么进行查询.
面试题: 请解释Collection与Collections的区别?
- Collection是集合接口, 允许保存单值对象;
- Collection是集合操作的工具类.