Java集合框架总结及面试题汇总

  • Collection接口
  • List
  • ArrayList
  • LinkedList
  • ArrayList与LinkedList的比较
  • Set接口
  • 散列集HashSet
  • 队列Queue
  • Map接口
  • HashMap
  • HashMap底层结构
  • HashMap的数据存储实现、put方法
  • HashMap的扩容
  • HashMap是怎么解决哈希冲突的
  • HashMap不用hashCode()处理后的哈希值作为table的下标?
  • HashMap1.8前后对比
  • HashMap中String、Integer等包装类适合作为K?
  • ConcurrentHashMap和Hashtable的区别?
  • 其它集合类
  • 1.Vector
  • 2.Stack
  • 3.Hashtable
  • 4.ConcurrentHashMap
  • 5.CopyOnWriteArrayList
  • 6.哈希、哈希冲突
  • 常见的Hash函数有以下几个:
  • 常见的解决碰撞的方法有以下几种:
  • HashMap解决哈希冲突:
  • 面试中出现的各种比较
  • Vector和ArrayList
  • ArrayList和LinkedList
  • Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?
  • HashMap与TreeMap
  • HashTable与HashMap
  • HashSet是如何保证数据不可重复的?
  • Java集合的快速失败机制 “fail-fast”?



java安全架构图 java安全框架面试题_java

Collection接口

Collections则是集合类的一个工具类/帮助类,其提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等操作。

List

List接口扩展自Collection,它可以定义一个允许重复的有序集合,从List接口中的方法来看,List接口主要是增加了面向位置的操作,允许在指定位置上操作元素,同时增加了一个能够双向遍历线性表的新列表迭代器ListIterator。AbstractList类提供了List接口的部分实现,AbstractSequentialList扩展自AbstractList,主要是提供对链表的支持。
List接口的两个重要的具体实现类,也是我们可能最常用的类,ArrayList和LinkedList。

ArrayList

它是用数组存储元素的,这个数组可以动态创建,如果元素个数超过了数组的容量,那么就创建一个更大的新数组,并将当前数组中的所有元素都复制到新数组中。

LinkedList

如果除了在末尾外不能在其他位置插入或者删除元素,那么ArrayList效率更高,如果需要经常插入或者删除元素,就选择LinkedList

ArrayList与LinkedList的比较

结构上
ArrayList是线性顺序存储 (顺序存储)(访问速度快,根据下标/索引直接)
LinkedList对象间彼此串连起来的一个链表(链式存储)双向链表
操作性能
ArrayList适合随机查询的场合
不适合插入删除(对某个元素的操作会使整个集合的位置发生变化)
LinkedList元素的插入和删除操作性高pre date next
差不多的场景:根据元素查找、更新数据

Set接口

与List的不同之处在于,规定Set的实例不包含重复的元素。AbstractSet是一个实现Set接口的抽象类,Set接口有三个具体实现类,散列集HashSet、链式散列集LinkedHashSet和树形集TreeSet。

散列集HashSet

散列集HashSet是一个用于实现Set接口的具体类,可以使用它的无参构造方法来创建空的散列集,也可以由一个现有的集合创建散列集。在散列集中,有两个名词需要关注,初始容量和客座率。实际上HashSet就是基于HashMap而实现的,客座率是确定在增加规则集之前,该规则集的饱满程度,当元素个数超过了容量与客座率的乘积时,容量就会自动翻倍。

链式散列集LinkedHashSet
树形集TreeSet

队列Queue

队列是一种先进先出的数据结构,元素在队列末尾添加,在队列头部删除。Queue接口扩展自Collection,并提供插入、提取、检验等操作。

poll()与remove()方法都是移除队列头部的元素,两者的区别在于如果队列为空,那么poll()返回的是null,而remove()会抛出一个异常nosuchelementexception。方法element()与peek()主要是获取头部元素,不删除。

接口Deque,是一个扩展自Queue的双端队列,它支持在两端插入和删除元素,因为LinkedList类实现了Deque接口,所以通常我们可以使用LinkedList来创建一个队列。PriorityQueue类实现了一个优先队列,优先队列中元素被赋予优先级,拥有高优先级的先被删除。

Map接口

Map,图,是一种存储键值对映射的容器类,在Map中键可以是任意类型的对象,但不能有重复的键,每个键都对应一个值,真正存储在图中的是键值构成的条目。

HashMap

HashMap的构造方法
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
为什么Java中的HashMap默认加载因子是0.75 :“哈希冲突”和“空间利用率”矛盾的一个折衷
加载因子是表示Hash表中元素的填满的程度。

HashMap底层结构

在之前的版本中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当链表中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
数组的元素类型是Node<K,V>,Node<K,V>继承自Map.Entry<K,V>,表示键值对映射。

HashMap存储元素的方式以及根据Hash值确定映射在数组中的位置还有JDK 1.8之后加入的红黑树的。
在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用hash(int h)方法所计算得到的hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中,(n - 1) & hash用于计算对象应该保存在table数组的哪个索引处。HashMap底层数组的长度总是2的n次方,当数组长度为2的n次幂的时候,(n - 1) & hash 算得的index相同的几率较小,数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

HashMap的数据存储实现、put方法

流程:

  1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
  2. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
    ① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
    ② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作;
    ③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:
    如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。
    注意:
    HashMap的put会返回key的上一次保存的数据,比如:
    HashMap<String, String> map = new HashMap<String, String>();
    System.out.println(map.put(“a”, “A”)); // 打印null
    System.out.println(map.put(“a”, “AA”)); // 打印A
    System.out.println(map.put(“a”, “AB”)); // 打印AA

HashMap的扩容

//该函数有2种使用情况:1.初始化哈希表;2.当前数组容量过小,需要扩容

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;// 扩容前的数组(当前数组)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 扩容前的数组容量(数组长度)
    int oldThr = threshold;// 扩容前数组的阈值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 针对情况2:若扩容前的数组容量超过最大值,则不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 针对情况2:若没有超过最大值,就扩容为原来的2倍(左移1位)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 针对情况1:初始化哈希表(采用指定或者使用默认值的方式)
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {     // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每一个bucket都移动到新的bucket中去
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
          }  }  }  } }
    return newTab;
}

HashMap是怎么解决哈希冲突的

数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:
将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化
hash()函数
上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 与自己右移16位进行异或运算(高低位异或)
}

这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),
在1.8中,只进行了1次位运算和1次异或运算(2次扰动);

通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);

简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:

  1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

HashMap不用hashCode()处理后的哈希值作为table的下标?

答:hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

面试官:那怎么解决呢?
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算(和位运算),降低哈希碰撞概率也使得数据分布更平均;

在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

面试官:为什么数组长度要保证为2的幂次方呢?
只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;

如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

面试官:那为什么是两次扰动呢?
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

HashMap1.8前后对比

不同 JDK 1.7 JDK 1.8
存储结构 数组 + 链表 数组 + 链表 + 红黑树
初始化方式 单独函数:inflateTable() 直接集成到了扩容函数resize()中
hash值计算方式
扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算
JDK 1.8 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则
无冲突时,存放数组;冲突时,存放链表
JDK 1.8 无冲突时,存放数组;
JDK 1.8 冲突 & 链表长度 < 8:存放单链表;
JDK 1.8 冲突 & 链表长度 > 8:树化并存放红黑树
插入数据方式
头插法(先讲原位置的数据移到后1位,再插入数据到该位置)
JDK 1.8 尾插法(直接插入到链表尾部/红黑树)
扩容后存储位置的计算方式
全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))
JDK 1.8 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)

HashMap中String、Integer等包装类适合作为K?

答:String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  2. 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
    面试官:如果我想要让自己的Object作为K应该怎么办呢?
    答:重写hashCode()和equals()方法
  3. 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
  4. 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

ConcurrentHashMap和Hashtable的区别?

答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现

LinkedHashMap
LinkedHashMap继承自HashMap,它主要是用链表实现来扩展HashMap类,HashMap中条目是没有顺序的,但是在LinkedHashMap中元素既可以按照它们插入图的顺序排序,也可以按它们最后一次被访问的顺序排序。

TreeMap
TreeMap基于红黑树数据结构的实现,键值可以使用Comparable或Comparator接口来排序。TreeMap继承自AbstractMap,同时实现了接口NavigableMap,而接口NavigableMap则继承自SortedMap。SortedMap是Map的子接口,使用它可以确保图中的条目是排好序的。

在实际使用中,如果更新图时不需要保持图中元素的顺序,就使用HashMap
如果需要保持图中元素的插入顺序或者访问顺序,就使用LinkedHashMap
如果需要使图按照键值排序,就使用TreeMap。

其它集合类

1.Vector

前面我们已经提到,Java设计者们在对之前的容器类进行重新设计时保留了一些数据结构,其中就有Vector。用法上,Vector与ArrayList基本一致,不同之处在于Vector使用了关键字synchronized将访问和修改向量的方法都变成同步的了,所以对于不需要同步的应用程序来说,类ArrayList比类Vector更高效。线程安全

2.Stack

Stack,栈类,是Java2之前引入的,继承自类Vector。线程安全

3.Hashtable

Hashtable和前面介绍的HashMap很类似,它也是一个散列表,存储的内容是键值对映射,不同之处在于,Hashtable是继承自Dictionary的,Hashtable中的函数都是同步的,这意味着它也是线程安全的,另外,Hashtable中key和value都不可以为null。

上面的三个集合类都是在Java2之前推出的容器类,可以看到,尽管在使用中效率比较低,但是它们都是线程安全的。下面介绍两个特殊的集合类。

4.ConcurrentHashMap

Concurrent,并发,从名字就可以看出来ConcurrentHashMap是HashMap的线程安全版。同HashMap相比,ConcurrentHashMap不仅保证了访问的线程安全性,而且在效率上与HashTable相比,也有较大的提高。 线程安全

5.CopyOnWriteArrayList

CopyOnWriteArrayList,是一个线程安全的List接口的实现,它使用了ReentrantLock锁来保证在并发情况下提供高性能的并发读取。

6.哈希、哈希冲突

常见的Hash函数有以下几个:

直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址。
数字分析法:提取关键字中取值比较均匀的数字作为哈希地址。
除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址。
分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
伪随机数法:采用一个伪随机数当作哈希函数。
任何哈希函数基本都无法彻底避免碰撞

常见的解决碰撞的方法有以下几种:

● 开放定址法:
○ 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
● 链地址法
○ 将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
● 再哈希法
○ 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
● 建立公共溢出区
○ 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。

HashMap解决哈希冲突:

链地址法,其实就是将数组和链表组合在一起,发挥了两者的优势,我们可以将其理解为链表的数组
总结
Java集合框架主要包括Collection和Map两种类型。
其中Collection又有3种子类型,分别是List、Set、Queue。
Map中存储的主要是键值对映射。

规则集Set中存储的是不重复的元素,
线性表中存储可以包括重复的元素,
Queue队列描述的是先进先出的数据结构,可以用LinkedList来实现队列。

效率上,规则集比线性表更高效。

ArrayList主要是用数组来存储元素,
LinkedList主要是用链表来存储元素,
HashMap的底层实现主要是借助数组+链表+红黑树来实现。

Vector、Hashtable等集合类效率比较低但都是线程安全的。
包java.util.concurrent下包含了大量线程安全的集合类,效率上有较大提升。

面试中出现的各种比较

Vector和ArrayList

1,vector是线程同步的,性能低,而arraylist是线程异步的,是不安全的。
2,vector扩容时为先前长度的2倍,而arraylist 扩容为1.5倍+1;

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引来取出某个元素,并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。
ArrayList 与 Vector 的区别主要包括两个方面:

  1. 同步性:
    Vector 是线程安全的,也就是说它的方法之间是线程同步(加了synchronized 关键字)的,而 ArrayList 是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全的问题,所以效率会高一些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
  2. 数据增长:
    ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个人超过了容量时,就需要增加 ArrayList 和 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要去的一定的平衡。Vector 在数据满时(加载因子1)增长为原来的两倍(扩容增量:原容量的 2 倍),而 ArrayList 在数据量达到容量的一半时(加载因子 0.5)增长为原容量的 (0.5 倍 + 1) 个空间。

ArrayList和LinkedList

1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
这一点要看实际情况的。
若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。
但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

  1. LinkedList 实现了 List 和 Deque 接口,称为双向链表;ArrayList 实现了 List 接口,动态数组;
  2. LinkedList 在插入和删除数据时效率更高,ArrayList 在查找某个 index 的数据时效率更高;
  3. LinkedList 比 ArrayList 需要更多的内存;
Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?
  1. Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
  2. Array 大小是固定的,ArrayList 的大小是动态变化的。
  3. ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。
    对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

HashMap与TreeMap

1、 HashMap通过hashcode对其内容进行快速查找,
而TreeMap中所有的元素都保持着某种固定的顺序,
如需要得到一个有序的结果就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。
2、在Map 中插入、删除和定位元素,HashMap是最好的选择。
但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
使用HashMap要求添加的键类明确定义了hashCode()和 equals()的实现。
两个map中的元素一样,但顺序不一样,导致hashCode()不一样。
同样做测试:
在HashMap中,同样的值的map, 顺序不同,equals时,false;
而在treeMap中,同样的值的map, 顺序不同,equals时,true,
说明,treeMap在equals()时是整理了顺序了的。

HashTable与HashMap

1、Hashtable是线程安全的synchronized,而HashMap是线程不安全的
2、HashMap允许存在一个为null的key,多个为null的value 。
3、hashtable的key和value都不允许为null。
4、HashMap继承自AbstractMap类;而Hashtable继承自Dictionary类;

HashSet是如何保证数据不可重复的?

答:HashSet的底层其实就是HashMap,只不过我们HashSet是实现了Set接口并且把数据作为K值,而V值一直使用一个相同的虚值来保存,我们可以看到源码:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
}
由于HashMap的K值本身就不允许重复,并且在HashMap中如果K/V相同,会用新的V覆盖掉旧的V,然后返回旧的V,那么在HashSet中执行这一句话始终会返回一个false,导致插入失败,这样就保证了数据的不可重复性;

Java集合的快速失败机制 “fail-fast”?

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:

  1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
  2. 使用CopyOnWriteArrayList来替换ArrayList