JAVA常用数据结构及原理分析(面试总结)
JAVA常用数据结构及原理分析(面试总结)
最近准备面试,因此整理一份的Java中常用的数据结构资料,方便面试;
java.util包中三个重要的接口及特点:List(列表),Set(保证集合中元素唯一),Map(维护多键值对键,保证键唯一)。其不同子类的实现各有差异,如是否同步(线程安全),是否有序。
常用类继承树:以下结合源码讲解常用类实现原理及相互之间的差异。
Collection(所有集合类的接口)
List,Set将继承自Collection接口,查看JDK API,操作集合常用的方法大部分在该接口中定义了。
集合类的操作不得不提到工具类Collections ,它提供了许多方便的方法,如求两个集合的差集,并集,拷贝,排序
等等。由于大部分的集合接口实现类都是不同步的,可以使用Collections.synchronized *方法创建同步的集合类对象。
如创建一个同步的名单:
List synList = Collections.synchronizedList(new ArrayList());
其实现原理就是重新封装新出来的对象,操作对象时用关键字同步同步。看源码很容易理解
。Collections部分源码:
- 静态类 SynchronizedCollection <e> 实现了 Collection <e>,Serializable {
- 最终 收集<e> c; //支持收藏
- 最终 对象互斥体; //要同步的对象
- SynchronizedCollection(Collection <e> c){
- 如果 (c == null )
- 抛出新的 NullPointerException();
- 这个.c = c;
- mutex = this ;
- }
- // ...
- public boolean add(E e){
- //操作集合时简单调用原本的ArrayList的对象,只是做了同步
- synchronized (互斥){ return c.add(e);}
- }
- // ...
- }
List(列表)
ArrayList中,载体是线性表,使用对象数组作为容器去存储数据的,添加了很多方法维护这个数组,使其容量可以动态增长,极大地提升了开发效率。它们明显的区别是ArrayList的是非同步的,矢量是同步的。不用考虑多线程时应用ArrayList来提升效率。ArrayList
,Vector部分源码:
- public boolean add(E e){
- ensureCapacityInternal(size + 1 ); //增加modCount !!
- //可以看出添加的对象放到elementData中数组中去了
- elementData [size ++] = e;
- 返回true ;
- }
- //ArrayList.remove
- public E remove(int index){
- rangeCheck(索引);
- modCount的++;
- E oldValue = elementData(index);
- int numMoved = size - index - 1 ;
- 如果 (numMoved> 0 )
- //移除元素时数组产生的空位由System.arraycopy方法将其后的所有元素往前移一位,System.arraycopy调用虚拟机提供的本地方法来提升效率
- System.arraycopy(elementData,index + 1 ,elementData,index,
- numMoved);
- elementData [ - size] = null ; //让gc做它的工作
- 返回 oldValue;
- }
- // Vector add方法上多了synchronized关键字
- public synchronized boolean add(E e){
- modCount的++;
- ensureCapacityHelper(elementCount + 1 );
- elementData [elementCount ++] = e;
- 返回true ;
- }
LinkedList的是链表,略懂数据结构就知道其实现原理了链表随机位置插入,删除数据时比线性表快,遍历比线性表慢。
双向链表原理图:
- 公共类 LinkedList <e>
- 扩展 AbstractSequentialList <e>
- 实现 List <e>,Deque <e>,Cloneable,java.io.Serializable
- {
- //头尾节点
- 首先是瞬态 节点<e>;
- 瞬态 节点<e>最后;
- }
- //节点类
- 私人静态类 节点<e> {
- //节点存储的数据
- E项目;
- 节点<e>下一个;
- 节点<e> prev;
- Node(Node <e> prev,E element,Node <e> next){
- this .item = element;
- this .next = next;
- this .prev = prev;
- }
- }
由此可根据实际情况来选择使用的ArrayList(非同步,非频繁删除时选择),向量(需同步时选择),链表(频繁在任意位置插入,删除时选择)。
地图(存储键值对,键唯一)
HashMap的结构的实现原理是将投入进来的键值封装成一个条目对象存储到一个输入数组中,位置(数组下标)由关键的哈希值与数组长度计算而来。如果数组当前下标已有值,则将数组当前下标的值指向新添加的条目对象。
有点晕,看图吧:
- 公共类 HashMap <k,v>
- 扩展 AbstractMap <k,v>
- 实现 Map <k,v>,Cloneable,Serializable
- {
- 瞬态 条目<k,v> []表;
- 公共 V放(K键,V值){
- 如果 (key == null )
- 返回 putForNullKey(value);
- int hash = hash(key);
- int i = indexFor(hash,table.length);
- //遍历当前下标的输入对象链,如果键已存在则替换
- for (Entry <k,v> e = table [i]; e!= null ; e = e.next){
- 对象k;
- if (e.hash == hash &&((k = e.key)== key || key.equals(k))){
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this );
- 返回 oldValue;
- }
- }
- addEntry(hash,key,value,i);
- 返回null ;
- }
- }
- 静态类 Entry <k,v> 实现 Map.Entry <k,v> {
- 最终 K键;
- V值;
- 条目<k,v>下一个;
- int hash;
- }
TreeMap是由Entry对象为节点组成的一颗红黑树,放到TreeMap的数据默认按键的自然顺序排序,新的TreeMap时传入Comparator自定义排序。红黑树网上很多资料,我讲不清,这里就不介绍了。
集(保证容器内元素唯一性)
之所以先讲地图是因为设置结构其实就是维护一个地图来存储数据的,利用地图结构关键值唯一性。
HashSet的部分源码:
- 公共类 HashSet <e>
- 扩展 AbstractSet <e>
- 实现 Set <e>,Cloneable,java.io.Serializable
- {
- //无意义对象来作为地图的价值
- private static final Object PRESENT = new Object();
- public boolean add(E e){
- 返回 map.put(e,PRESENT)== null ;
- }
- }
- <span style = “font-size:18px;” > <strong> HashMap支持key = null 但是Hashtable不支持key = null </ strong> </ span> <span style = “font-size:14px;” >
- </跨度>
HashMap的哈希表和的区别
HashMap中和哈希表都实现了地图接口,但决定用哪一个之前先要弄清楚它们之间的分别主要的区别有:线程安全性,同步(同步),以及速度。
- HashMap中几乎可以等价于哈希表,除了HashMap中是非同步的,并可以接受空(HashMap中可以接受为无效的键值(key)的和值(值),而散列表则不行)。
- HashMap是非同步的,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多线程是不能共享HashMap的.Java 5提供了ConcurrentHashMap,它是哈希表的替代,比散列表的扩展性更好。
- 另一个区别是HashMap中的迭代器(迭代)是快速失败迭代器,而散列表的枚举迭代器不是快速失败的。所以当有其它线程改变了HashMap中的结构(增加或者移除元素),将会抛出ConcurrentModificationException的,但迭代器本身的删除()方法移除元素则不会抛出ConcurrentModificationException的异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是枚举和迭代器的区别。
- 由于散列表是线程安全的也是同步的,所以在单线程环境下它比HashMap中要慢。如果你不需要同步,只需要单一线程,那么使用的HashMap性能要好过哈希表。
- HashMap中不能保证随着时间的推移地图中的元素次序是不变的。
要注意的一些重要术语:
1)sychronized意味着在一次只有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。
2)Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用设置()方法,将会抛出抛出:IllegalArgumentException异常。
3)结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。
我们能否让HashMap的同步?
HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);
结论
Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。
HashSet的,TreeSet中分别默认维护一个HashMap中,TreeMap的。
最近准备面试,因此整理一份的Java中常用的数据结构资料,方便面试;
java.util包中三个重要的接口及特点:List(列表),Set(保证集合中元素唯一),Map(维护多键值对键,保证键唯一)。其不同子类的实现各有差异,如是否同步(线程安全),是否有序。
常用类继承树:以下结合源码讲解常用类实现原理及相互之间的差异。
Collection(所有集合类的接口)
List,Set将继承自Collection接口,查看JDK API,操作集合常用的方法大部分在该接口中定义了。
集合类的操作不得不提到工具类Collections ,它提供了许多方便的方法,如求两个集合的差集,并集,拷贝,排序
等等。由于大部分的集合接口实现类都是不同步的,可以使用Collections.synchronized *方法创建同步的集合类对象。
如创建一个同步的名单:
List synList = Collections.synchronizedList(new ArrayList());
其实现原理就是重新封装新出来的对象,操作对象时用关键字同步同步。看源码很容易理解
。Collections部分源码:
- 静态类 SynchronizedCollection <e> 实现了 Collection <e>,Serializable {
- 最终 收集<e> c; //支持收藏
- 最终 对象互斥体; //要同步的对象
- SynchronizedCollection(Collection <e> c){
- 如果 (c == null )
- 抛出新的 NullPointerException();
- 这个.c = c;
- mutex = this ;
- }
- // ...
- public boolean add(E e){
- //操作集合时简单调用原本的ArrayList的对象,只是做了同步
- synchronized (互斥){ return c.add(e);}
- }
- // ...
- }
List(列表)
ArrayList中,载体是线性表,使用对象数组作为容器去存储数据的,添加了很多方法维护这个数组,使其容量可以动态增长,极大地提升了开发效率。它们明显的区别是ArrayList的是非同步的,矢量是同步的。不用考虑多线程时应用ArrayList来提升效率。ArrayList
,Vector部分源码:
- public boolean add(E e){
- ensureCapacityInternal(size + 1 ); //增加modCount !!
- //可以看出添加的对象放到elementData中数组中去了
- elementData [size ++] = e;
- 返回true ;
- }
- //ArrayList.remove
- public E remove(int index){
- rangeCheck(索引);
- modCount的++;
- E oldValue = elementData(index);
- int numMoved = size - index - 1 ;
- 如果 (numMoved> 0 )
- //移除元素时数组产生的空位由System.arraycopy方法将其后的所有元素往前移一位,System.arraycopy调用虚拟机提供的本地方法来提升效率
- System.arraycopy(elementData,index + 1 ,elementData,index,
- numMoved);
- elementData [ - size] = null ; //让gc做它的工作
- 返回 oldValue;
- }
- // Vector add方法上多了synchronized关键字
- public synchronized boolean add(E e){
- modCount的++;
- ensureCapacityHelper(elementCount + 1 );
- elementData [elementCount ++] = e;
- 返回true ;
- }
LinkedList的是链表,略懂数据结构就知道其实现原理了链表随机位置插入,删除数据时比线性表快,遍历比线性表慢。
双向链表原理图:
- 公共类 LinkedList <e>
- 扩展 AbstractSequentialList <e>
- 实现 List <e>,Deque <e>,Cloneable,java.io.Serializable
- {
- //头尾节点
- 首先是瞬态 节点<e>;
- 瞬态 节点<e>最后;
- }
- //节点类
- 私人静态类 节点<e> {
- //节点存储的数据
- E项目;
- 节点<e>下一个;
- 节点<e> prev;
- Node(Node <e> prev,E element,Node <e> next){
- this .item = element;
- this .next = next;
- this .prev = prev;
- }
- }
由此可根据实际情况来选择使用的ArrayList(非同步,非频繁删除时选择),向量(需同步时选择),链表(频繁在任意位置插入,删除时选择)。
地图(存储键值对,键唯一)
HashMap的结构的实现原理是将投入进来的键值封装成一个条目对象存储到一个输入数组中,位置(数组下标)由关键的哈希值与数组长度计算而来。如果数组当前下标已有值,则将数组当前下标的值指向新添加的条目对象。
有点晕,看图吧:
- 公共类 HashMap <k,v>
- 扩展 AbstractMap <k,v>
- 实现 Map <k,v>,Cloneable,Serializable
- {
- 瞬态 条目<k,v> []表;
- 公共 V放(K键,V值){
- 如果 (key == null )
- 返回 putForNullKey(value);
- int hash = hash(key);
- int i = indexFor(hash,table.length);
- //遍历当前下标的输入对象链,如果键已存在则替换
- for (Entry <k,v> e = table [i]; e!= null ; e = e.next){
- 对象k;
- if (e.hash == hash &&((k = e.key)== key || key.equals(k))){
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this );
- 返回 oldValue;
- }
- }
- addEntry(hash,key,value,i);
- 返回null ;
- }
- }
- 静态类 Entry <k,v> 实现 Map.Entry <k,v> {
- 最终 K键;
- V值;
- 条目<k,v>下一个;
- int hash;
- }
TreeMap是由Entry对象为节点组成的一颗红黑树,放到TreeMap的数据默认按键的自然顺序排序,新的TreeMap时传入Comparator自定义排序。红黑树网上很多资料,我讲不清,这里就不介绍了。
集(保证容器内元素唯一性)
之所以先讲地图是因为设置结构其实就是维护一个地图来存储数据的,利用地图结构关键值唯一性。
HashSet的部分源码:
- 公共类 HashSet <e>
- 扩展 AbstractSet <e>
- 实现 Set <e>,Cloneable,java.io.Serializable
- {
- //无意义对象来作为地图的价值
- private static final Object PRESENT = new Object();
- public boolean add(E e){
- 返回 map.put(e,PRESENT)== null ;
- }
- }
- <span style = “font-size:18px;” > <strong> HashMap支持key = null 但是Hashtable不支持key = null </ strong> </ span> <span style = “font-size:14px;” >
- </跨度>
HashMap的哈希表和的区别
HashMap中和哈希表都实现了地图接口,但决定用哪一个之前先要弄清楚它们之间的分别主要的区别有:线程安全性,同步(同步),以及速度。
- HashMap中几乎可以等价于哈希表,除了HashMap中是非同步的,并可以接受空(HashMap中可以接受为无效的键值(key)的和值(值),而散列表则不行)。
- HashMap是非同步的,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多线程是不能共享HashMap的.Java 5提供了ConcurrentHashMap,它是哈希表的替代,比散列表的扩展性更好。
- 另一个区别是HashMap中的迭代器(迭代)是快速失败迭代器,而散列表的枚举迭代器不是快速失败的。所以当有其它线程改变了HashMap中的结构(增加或者移除元素),将会抛出ConcurrentModificationException的,但迭代器本身的删除()方法移除元素则不会抛出ConcurrentModificationException的异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是枚举和迭代器的区别。
- 由于散列表是线程安全的也是同步的,所以在单线程环境下它比HashMap中要慢。如果你不需要同步,只需要单一线程,那么使用的HashMap性能要好过哈希表。
- HashMap中不能保证随着时间的推移地图中的元素次序是不变的。
要注意的一些重要术语:
1)sychronized意味着在一次只有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。
2)Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用设置()方法,将会抛出抛出:IllegalArgumentException异常。
3)结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。
我们能否让HashMap的同步?
HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);
结论
Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。
HashSet的,TreeSet中分别默认维护一个HashMap中,TreeMap的。