目录
为什么HashMap链表转红黑树的阈值和红黑树转链表的阈值不都为8呢?
集合
保存一组相同类型的元素时,我们会用到数组,但数组一旦被定义,便无法进行修改。
在日常开发中,我们更加需要的是可以动态扩展的一个容器,此时就有了集合
集合的特点
提供一种存储空间可变的存储模型,存储的数据容量可以发生改变
List(接口)
实现类 arraylist linkedlist 存入数据 都是保证有序性 并且可以重复
ArrayList
ArrayList<>()创建一个长度为10的底层数组,第一次添加元素时,真正创建数组
add()添加一个元素到集合中的过程 调用add()添加元素时,先检查底层数组是否还能放得下,如果可以,直接将元素添加到末尾, 如果不可以,会创建一个新的数组,将原来的数组内容复制过去
size --->记录实际装入数组的元素个数
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查元素能否放的进去
elementData[size++] = e;
return true;
}
放进去后容量大小 - 底层数组长度 >0
if (minCapacity - elementData.length > 0)
grow(minCapacity); 扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); 新数组容量为原来的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
数组复制,创建一个新数组,将原来的数组内容复制到新数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
Vector和ArrayList集合区别
相同点:
-
ArrayList和Vector默认初始化容量=10
-
底层都是基于数组实现
-
List接口下的子类
不同点:
-
ArrayList 线程不安全 Vector 线程是安全的
-
ArrayList每次扩容是原来容量的1.5倍
-
Vector每次扩容是原来容量的2倍
-
Vector设置每次扩容的容量。
-
ArrayList 懒加载的形式初始化容量,Vector直接通过构造函数初始化 数组容量=10
LinkedList
底层是基于链表实现的,可以存放重复的元素,有序
add() 每添加一个元素创建一个Node对象
E item;
Node<E> next;
Node<E> prev;
LinkedList<String> llist = new LinkedList();
llist.add("a");
llist.add("b");
llist.add("c");
llist.add("d");
llist.add("a");
//index<size/2从头节点开始向后找,否则从尾节点向前找
System.out.println(llist.get(3));
System.out.println(llist);
ArrayList和LinkedList的区别
-
ArrayList基于数组实现,LinkedList基于双向链表实现
-
ArrayList查找效率更高,LinkedList增删效率更高
Set
实现类HashSet(无序)、TreeSet(有序)
HashSet
-
基于hashMap来实现的,是一个不允许有重复元素的集合
-
允许有null值
-
无序的,即不会记录插入的顺序
-
没有Get方法,所以不能使用普通for循环遍历
HashSet中添加元素时是如何去除重复元素的?
判断要求是既快又安全.
System.out.println("abc".equals("abc")); equals判断安全但效率低.
怎么提高比较速度 ?
使用内容的hash值比较(哈希值是整数), 但是哈希值是不安全(有可能内容不同,哈希值相同)
最终解决方案就是先用内容计算的哈希值比较,如果出现了相同的哈希值,那么再调用equals方法判断内容是否相同
equals和hashCode
hashCode属于Object父类中 Java虚拟机提供给每个对象生成一个hashCode值 整数类型 int类型
ps:
-
如果equals方法比较两个对象相等,则hashCode值也一定相等
-
但是两个对象的hashCode值相等不代表使用equals比较也相等
-
如果两个对象的hashCode值相等,但是值不同,这就是我们所说的Hash冲突问题
String strA = "a";
Integer int97 = 97;// 整数类型
System.out.println(strA.hashCode());//97
System.out.println(int97.hashCode());//97
Map(接口)
键值对存储
键不能重复,值可以重复
实现类HashMap、TreeMap、Hashtable
HashMap
数组+链表+红黑树 结构实现 线程不安全,默认初始化容量为16
当向HashMap中添加元素时,先用key计算出一个哈希值,通过哈希算法,计算出此key在哈希表中的位置,如果这个位置上没有元素,则直接插入
当再次插入值时,如果计算出的位置上已经存在值了,那么就会向元素的下一位添加(链表)
当链表的长度大于8并且数组长度大于64时,链表就会转成红黑树
如果红黑树节点个数小于6,转为链表
当数组被使用了0.75时,哈希数组会进行扩容,扩容为原来的2倍
HashMap中put方法
put(K,V) 添加元素整个过程 以及底层存储结构
public V put(K key, V value) {
计算哈希值
return putVal(hash(key), key, value, false, true);
}
以下源码我们可以看出hashMap的key是可以存储null的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) 哈希数组为空或长度为0时,初始化数组 resize()
n = (tab = resize()).length; n=哈希表长度
if ((p = tab[i = (n - 1) & hash]) == null) 数组长度-1 & hash == 哈希值%数组长度 计算元素位置
tab[i] = newNode(hash, key, value, null);
else {
}
Map接口下其他一些实现类
HashMap hash表+链表+红黑树 结构实现 线程不安全
TreeMap 底层是红黑树实现 并且是有序的
Hashtable hash表+链表+红黑树 结构实现 线程安全 不能存储为null的键
面试题
HashMap 是怎么解决哈希冲突的?
①开放定址法:从发生冲突的那个位置开始,通过一定的次序从hash表里面找一个空闲的位置。之后将发生冲突的元素存入到这个空闲位置中。
②链式寻址法:也是现在HashMap所用的方法,当发生hash冲突时,会进行链表的转化。以原本数组中相同位置的元素为链表头结点。
③再 hash 法:发生冲突后,再次用另外一个hash函数对这个key进行hash,一直运算下去直到不再冲突。这种方式会增加计算时间,性能影响较大
为什么HashMap的容量是2的倍数?
-
为了方便哈希取余 (数组长度-1)&哈希值
-
在扩容时,利用扩容后的大小也是2的倍数,将已经产生hash碰撞的元素完美的转移到新的数组中去
为什么HashMap链表转红黑树的阈值和红黑树转链表的阈值不都为8呢?
因为如果两者都是8,假如发生碰撞,节点增减刚好在8附近,会发生链表和红黑树的不断转换,导致资源浪费
扩容在什么时候呢?为什么扩容因子是0.75?
临界值(threshold )= 默认容量(DEFAULT_INITIAL_CAPACITY) * 默认扩容因子(DEFAULT_LOAD_FACTOR)
如果我们的负载因子比较大,元素比较多,空位比较少的时候才会扩容,那么发生哈希冲突的概率就会增加,查找的时间成本也就会增加
如果我们的负载因子比较小,元素比较少,空位比较多就会发生扩容,发生哈希碰撞的概率减小了,查找时间成本降低,但是空间成本就会增加
HashMap 是线程安全的吗?多线程下会有什么问题?
HashMap不是线程安全的,可能会发生这些问题:
-
多线程的 put 可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。
-
put 和 get 并发时,可能导致 get 为 null。线程 1 执行 put 时,因为元素个数超出 threshold 而导致 rehash,线程 2 此时执行 get,有可能导致这个问题。