集合底层结构
1.HashMap的底层原理
HashMap的底层原理面试必考题。
为什么面试官如此青睐这道题?
HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好offer,这是一个迈不过的坎,接下来我用最通俗易懂的语言带着大家揭开HashMap的神秘面纱
一:HashMap的节点:HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node是一个内部类,这里的key为键,value为值,next指向下一个元素,可以看出HashMap中的元素不是一个单纯的键值对,还包含下一个元素的引用。
二:HashMap的数据结构:HashMap的数据结构为 数组+(链表或红黑树),如下图:
为什么采用这种结构来存储元素呢?
数组的特点:查询效率高,插入,删除效率低。
链表的特点:查询效率低,插入删除效率高。
在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。
三:HashMap存储元素的过程:
有这样一段代码:
HashMap<String,String> map = new HashMap<String,String>();
map.put("刘德华","张惠妹");
map.put("张学友","大S");
现在我要把键值对 “刘德华”,”张惠妹”存入map:
第一步:计算出键“刘德华”的hashcode,该值用来定位要将这个元素存放到数组中的什么位置.
什么是hashcode?
在Object类中有一个方法:
public native int hashCode();
该方法用native修饰,所以是一个本地方法,所谓本地方法就是非java代码,这个代码通常用c或c++写成,在java中可以去调用它。
调用这个方法会生成一个int型的整数,我们叫它哈希码,哈希码和调用它的对象地址和内容有关.
哈希码的特点是:
对于同一个对象如果没有被修改(使用equals比较返回true)那么无论何时它的hashcode值都是相同的
对于两个对象如果他们的equals返回false,那么他们的hashcode值也有可能相等
明白了hashcode我们再来看元素如何通过hashcode定位到要存储在数组的哪里,通过hashcode值和数组长度取模我们可以得到元素存储的下标。
刘德华的hashcode为20977295 数组长度为 16则要存储在数组索引为 20977295%16=1的地方
可以分两种情况:
1. 数组索引为1的地方是空的,这种情况很简单,直接将元素放进去就好了。
2. 已经有元素占据了索引为1的位置,这种情况下我们需要判断一下该位置的元素和当前元素是否相等,使用equals来比较。
如果使用默认的规则是比较两个对象的地址。也就是两者需要是同一个对象才相等,当然我们也可以重写equals方法来实现我们自己的比较规则最常见的是通过比较属性值来判断是否相等。
如果两者相等则直接覆盖,如果不等则在原元素下面使用链表的结构存储该元素
每个元素节点都有一个next属性指向下一个节点,这里由数组结构变成了数组+链表结构,红黑树又是怎么回事呢?
因为链表中元素太多的时候会影响查找效率,所以当链表的元素个数达到8的时候使用链表存储就转变成了使用红黑树存储,原因就是红黑树是平衡二叉树,在查找性能方面比链表要高.
四:HashMap中的两个重要的参数:HashMap中有两个重要的参数:初始容量大小和加载因子,初始容量大小是创建时给数组分配的容量大小,默认值为16,用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫做扩容.
在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能.
创建HashMap时我们可以通过合理的设置初始容量大小来达到尽量少的扩容的目的。加载因子也可以设置,但是除非特殊情况不建议设置.
如果该文章对你有帮助请帮忙点个赞,加收藏以免日后找不到!
我会频繁发布面试最热门考点文章,下面是我前几天发布的精心准备的面试考点文章,无论是对完善技术栈还是准备面试都大有裨益:
2.ArrayList,LinkedList和Vector的区别
ArrayList,linkedList和Vector都继承自List接口。ArrayList和Vector的底层是一个动态数组,LinkedList的底层是一个双向链表.
ArrayList和Vector的区别就是ArrayList是线程不安全的,Vector是线程安全的,Vector中的方法都是同步方法(synchronized),所以ArrayList的执行效率要高于Vector,它也是用的最广泛的一种集合。
我们重点比较一下ArrayList和LinkedList的区别,其实ArrayList和LinkedList之间的区别就是数组和双向链表之间的区别。
数组的特点:因为数组分配的是一块连续的内存空间,使用索引来查找元素是非常快速的。但是插入,删除数据比较消耗性能。
要把邓丽君删掉需要先把从王志文开始的元素挨个往前移,将邓丽君覆盖掉。
再来看一下双向链表的结构:
在该链表中每一个元素除了存储本身的内容之外还存储指向前一个元素的指针和指向后一个元素的指针,下图展示了一个包含三个元素的双向链表,每个链表都有一个头部,头部指向第一个元素,尾部元素也指向头部。
双向链表的特点,查询效率较低,因为查询一个元素需要从头部开始查询,挨个遍历每一个元素直到找到所需元素,插入,删除效率高比如我们删掉一个元素直接把它前一个元素的指针指向它后一个元素就可以了:
我们通过源码来比价一下这两种数据结构集合的特点
- 添加元素到集合尾部:List接口提供了一个方法可以将元素添加到集合的尾部boolean add(E e),ArrayList和LinkedList都实现了这个方法:
先看ArrayList是如何实现的:
public boolean add(E e) { ensureCapacityInternal(size + 1); //判断当前数组的容量是否够大如果不够大则扩容 elementData[size++] = e;//将元素添加到数组尾部 return true; }
这里的执行效率取决于:ensureCapacityInternal(size + 1)方法的执行,在该方法中会判断数组容量是否足够,如果不够则进行扩容到原来的1.5倍。在扩容的过程中会生成一个新的数组,将原数组中的元素复制到新数组中。所以在这种情况下如果数组容量足够大ArrayList的效率是非常高的。
再来看一下LinkedList集合:
public boolean add(E e) { linkLast(e); return true; }
void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
我们可以看到每新增一个元素就要创建一个Node对象,进行频繁的赋值操作 “final Node<E> newNode = new Node<>(l, e, null);”对效率有一定的影响。
我们再来看一下在特定的位置插入元素:List接口中提供的方式是这样的:void add(int index,E element)
在ArrayList中是这样实现的:
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
从代码中我们可以看出,每插入一个元素就要进行大量元素复制操作:“System.arraycopy(elementData, index, elementData, index + 1,size - index);”
从插入点往后的元素依次后移将新元素插入到空出来的位置上。效率非常低下。
而在LinkedList中开销和在集合最后插入元素开销差不多,只需要把它前一个元素的指针指向自己,自己的指针指向下一个元素就可以了。
总结一下:
- ArrayList和Vector结构一样都是动态数组,区别是ArrayList是线程不安全的,Vector是线程安全的,ArrayList性能好于Vector,ArrayList是应用最多的集合。
- ArrayList和LinkedList的区别是他们的数据结构不同,ArrayList是动态数组,LinkedList是双向链表,在查询操作较多,在特定位置插入数据和删除数据较少的情况下一般选用ArrayList,在特定位置插入数据,删除数据操作较多,查询较少的情况下一般选用LinkedList,但是在大多数应用中都是对查询效率要求较高,所以ArrayList集合应用更广泛一些。
- HashSet的原理?
我们先看一下HashSet和TreeSet在整个集合框架中的位置。他们都实现了Set接口。他们之间的区别是HashSet不能保证元素的顺序,TreeSet中的元素可以按照某个顺序排列。他们的元素都不能重复。
先来看一下HashSet:
public static void main(String[] args) { Set<String> set = new HashSet<String>(); set.add("张三"); set.add("李四"); set.add("王五"); System.out.println(set); System.out.println(set.size()); System.out.println(set.contains("张三")); }
打印输出的顺序是是: [李四, 张三, 王五]
可以看出和存进去的顺序不一致。
我们先看一下 Set<String> set = new HashSet<String>();
这行代码创建了一个HashSet,构造函数如下:
public HashSet() { map = new HashMap<>(); }
可以看到实际上是创建了一个HashMap的对象。没错,HashSet底层就是一个HashMap.
再来看一下这行代码:
set.add("张三"); public boolean add(E e) { return map.put(e, PRESENT)==null; }
非常的简单,就是调用了一下HashMap的put方法对元素进行插入。
这里的PERSENT是什么呢?继续顺藤摸瓜:
private static final Object PRESENT = new Object();
原来就是一个普通的Object对象前面用static final修饰说明是不可变的。
继续添加:set.add("李四");
可以看出来HashMap的key分别为”张三”,”李四”,“王五”, 因为HashSet用不到value,他们的value都是一样的指向同一个地方。继续往下看:
System.out.println(set.size()); public int size() { return map.size(); }
也是调用的HashMap的size方法。
System.out.println(set.contains("张三")); public boolean contains(Object o) { return map.containsKey(o); }
同样调用的HashMap的contains方法。