Java面试题----集合

一、集合和数组的区别

1)长度区别:集合长度可变,数组长度不可变

2)内容区别:集合可存储不同类型元素,数组存储只可单一类型元素

3)元素区别:集合只能存储引用类型元素,数组可存储引用类型,也可存储基本类型

二、集合框架图

图一

 图二

注:上图中粉红色的为接口,紫色的和蓝色框为实现类。

Java集合要从两大接口说起,一为Collection接口,二为Map接口,它们是同一个层次的。

Collection接口被List接口和Set接口继承;

List接口有三个实现类,ArrayList,LinkedList,Vector;

Set接口被HashSet类实现,被SortedSet接口继承,同时TreeSet类实现SortedSet接口,LinkedHashSet类继承HashSet类;

Map接口有两个实现类,HashMap,HashTable,同时Propertise类继承HashTable;

Map接口被SortedMap接口继承,同时TreeMap类实现了SortedMap接口;

三、概念

3.1Collection接口(单列集合)
Collection接口是单列集合的最顶层接口,定义了一些通用的方法。

add(E e)添加元素;  clear()清空元素;  remove(E e)移除元素;  size()元素数量;

toArray()集合转数组;  contains(E e)判断元素是否存在;  isEmpty()判断集合是否为空;

3.1.1List 接口
特点:有索引(下标),精准操作元素,可存null;

元素有序,存储及取出时顺序一致;

元素可重复,通过.equals()比较是否重复。

它利用索引(index),定义了一些特殊方法:

get(int index) 获取指定位置的元素;remove(int index)移除指定位置的元素; 

add(int index,E e) 将元素添加到指定位置;set(int index,E e) 用元素替换指定位置的元素;

3.1.1.1ArrayList实现类

数据结构:数组; 

特点:查询快,增删慢,主要用于查询遍历数据,为最常用集合之一;

底层分析:数组结构是有序的元素序列,在内存中开辟一段连续的空间,在空间中存放元素,每个空间都有编号,通过编号可以快速找到相应元素,因此查询快;数组初始化时长度是固定的,要想增删元素,必须创建一个新数组,把源数组的元素复制进来,随后源数组销毁,耗时长,因此增删慢。

3.1.1.2LinkedList实现类

数据结构:双向链表;

特点:查询慢,增删快;

底层分析:链表分为单向和双向,就是一条链子和两条链子的区别;多出的那条链子记录了元素的顺序,因此单向链表结构无序,双向链表结构有序;链表结构没有索引,因此查询慢;链表的增删只需在原有的基础上连上链子或切断链子,因此增删快。

特有方法:getFirst()  返回开头元素;  getLast()  返回结尾元素;

pop() 从所在堆栈中获取一个元素;  push(E e)  将元素推入所在堆栈;

addFirst(E e)  添加元素到开头,头插; addLast(E e) 添加元素到结尾,尾插;

3.1.1.3Vector实现类(基本不用)

数据结构:数组; 

特点:查询快,增删慢

底层分析:和ArrayList一样,都是数组实现,因此具有相似的特性,它们之间的区别在于Vector是线程安全的,效率低,ArrayList是线程不安全的,但效率高。

ps:Vector在JDK1.0就出现了,在JDK1.2集合出现的时候,Vector就归为List的实现类之一,这时候ArrayList才出现。Vector是一个古老的集合,《Java编程思想》中提到了它有一些遗留的缺点,因此不建议使用。

3.1.2Set接口
特点:元素不可重复,存一个null

元素无序,存储及取出时顺序不一致;

没有索引(下标),因此不能使用普通For循环遍历;

Set与Collection 接口中的方法基本一致,没有进行功能上的扩充;

3.1.2.1HashSet实现类

数据结构:JDK1.8之前:哈希表(数组+单向链表);JDK1.8之后:哈希表(数组+单向链表+红黑树),当链表长度超过阈值(8)时,链表将转换为红黑树。

特点:查询快,元素无序,元素不可重复,没有索引;

底层分析:哈希表底层用数组+单向链表实现,即使用链表处理冲突,同一Hash值的元素都存储在一个链表里,但是当位于一个链表中的元素较多,即Hash值相等的元素较多,通过key值依次查找的效率降低。JDK1.8之后,哈希表底层采用数据+单向链表+红黑树实现,当链表长度超过阈值(8)时,链表将转换为红黑树,极大缩短查询时间。

ps:哈希值是一个十进制的整数,是对象的地址值,是一个逻辑地址,不是实际存储的物理地址,由系统随机给出。Object类的int hashCode()方法,可以获取对象的哈希值。

3.1.2.2LinkedHashSet实现类

数据结构:JDK1.8之前:哈希表(数组+双向链表);JDK1.8之后:哈希表(数组+双向链表+红黑树),当链表长度超过阈值(8)时,链表将转换为红黑树。

特点:查询快,元素有序,元素不可重复,没有索引;

底层分析:作为HashSet的子类,只是比它多了一条链表,这条链表用来记录元素顺序,因此LinkedHashSet其中的元素有序。

3.1.2.3TreeSet实现类

数据结构:红黑树     

特点:查询快,元素有序,元素不可重复,没有索引;

底层分析:TreeSet实现了继承于Set接口的SortedSet接口 ,它支持两种排序方法,自然排序和定制排序,自然排序的意思就是放入元素“a”,“b”,a会自然地排在b前面,其中还有几个特有方法。

first() 返回第一个元素; last() 返回最后一个元素;comparator() 返回排序比较器;

3.2Map接口(双列集合)
特点:元素包含两个值(key,value)即键值对, key不允许重复,value可以重复, key与value是一一对应的。元素无序;

Map接口是双列集合的最顶层接口,定义了一些通用的方法。

put(key , value) 添加元素; remove(key) 删除key对应元素;

containsKey(key) 判断是否存在key对应元素;get(key) 获取key对应元素;

KeySet() 获取所有的key,存到Set集合中;entrySet() 获取所有的元素,存到Set集合中;

ps:Map集合必须保证保证key唯一,作为key,必须重写hashCode方法和equals方法,以保证key唯一。

3.2.1HashMap实现类
数据结构:JDK1.8之前:哈希表(数组+单向链表);JDK1.8之后:哈希表(数组+单向链表+红黑树),当链表长度超过阈值(8)时,链表将转换为红黑树。

特点:查询快,元素无序,key不允许重复但可以为null,value可以重复。

底层分析:和HashSet底层相类似,不赘述。

3.2.2LinkedHashMap实现类
数据结构:JDK1.8之前:哈希表(数组+双向链表);JDK1.8之后:哈希表(数组+双向链表+红黑树),当链表长度超过阈值(8)时,链表将转换为红黑树。

特点:查询快,元素有序,key不允许重复但可以为null,value可以重复。

底层分析:和LinkedHashSet底层相类似,不赘述。

3.2.3HashTable实现类(基本不用)
数据结构:哈希表

特点:查询快,元素无序,key不允许重复并且不可以为null,value可以重复。

底层分析:HashTable和Vector一样是古老的集合,有遗留缺陷,在JDK1.2之后 被更先进的集合取代了;HashTable是线程安全的,速度慢,HashMap是线程不安全的,速度快;

ps:Hashtable的子类Properties现在依然活跃,Properties集合是一个唯一和IO流结合的集合。

3.2.3TreeMap实现类
数据结构:红黑树

特点:查询快,元素有序,key不允许重复并且不可以为null,value可以重复。

底层分析:和TreeSet底层相类似,不赘述。

四、面试题
1.List 、Set 和 Map的区别?

Collection集合主要有List和Set两大接口

  • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
  • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

2.集合的底层数据结构

Collection

1.List

  • Arraylist:Object数组
  • Vector:Object数组
  • LinkedList:双向循环链表

2.Set

  • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map

  • HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
     
  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

3.哪些集合类是线程安全的?

  • vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
  • hashtable:就比hashmap多了个线程安全。
  • statck:堆栈类,先进后出。(了解)
  • enumeration:枚举,相当于迭代器。(了解)

4.遍历集合

1)遍历list集合

		//创建一个集合
		ArrayList<Student> alist = new ArrayList<>();
		//向集合中添加元素
		alist.add(new Student("张三",1));
		alist.add(new Student("李四",2));
		alist.add(new Student("王五",3));
		alist.add(new Student("赵六",4));
		alist.add(new Student("冯七",5));
		
		//遍历集合方式一 转成数组
		Student[] array = (Student[])alist.toArray();
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i]);
		}
		//遍历集合	方式二 普通迭代器
		Iterator<Student> it =  alist.iterator();
		while(it.hasNext()) {
			System.out.println(it.next());
		}
		//遍历集合	方式三  属于List集合使用的迭代器
		ListIterator<Student> st = alist.listIterator(alist.size());
		while(st.hasPrevious()) {
			System.out.println(st.previous());
		}
		//遍历集合	方式四	普通for循环
		for(int i = 0;i<alist.size();i++) {
			System.out.println(alist.get(i));
		}
		//遍历集合	方式五	增强for循环
		for (Student stu : alist) {
			System.out.println(stu);
		}
		

2)遍历set集合

  • 普通迭代器Iterator
  • 增强for循环(foreach,使用增强for循环遍历集合,由于Set集合没有get()方法,所有无法使用普通for循环遍历)

5.Iterator 和 listIterator 有什么区别?

  • Iterator 可以遍历Set 和 List 集合,而ListIrerator 只能遍历 List
  • Iterator 只能单向遍历,而ListIterator 可以双向遍历(向前/后遍历)
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置

6.如何实现数组和 List 之间的转换?

  • 数组转 List :使用 Arrays.asList(array);
  • List 转数组: 使用List 自带的toArray()方法
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);

7.ArrayList 和 LinkedList 的区别是什么?

1)数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

2)随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

3)增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

大致结论:
1、当向List添加元素的位置比较靠近头部时,LinkedList的效率是要优于ArrayList的,因为ArrayList需要对大部分元素进行迁移,迁移元素耗时占比较大;
2、当向List添加元素的位置比较靠近中间位置时,LinkedList的效率要低于ArrayList,因为LinkedList添加元素时需要通过从首尾向中间遍历来寻找节点,遍历耗时比较长,而ArrayList迁移的元素则相对少了。
3、当向List尾部添加元素时,ArrayList不需要数据迁移,LinkedList也不需要遍历,此时
(1)若添加元素数量很少时,则两种List的效率都很高,从使用方面来说,其效率差距 一般 可以忽略不计;
(2)若添加元素数量达到一定规模时,则LinkedList的效率要高于ArrayList,因为ArrayList会频繁触发扩容复制,扩容耗费了很多时间,拉低了效率;
(3)如添加元素数量非常大时,则ArrayList的效率要高于LinkedList。因为随着ArrayList长度的增大,其扩容的次数会大大降低。对于单个元素,ArrayList只需要一个简单赋值操作,而LinkedList需要不断分配新的内存空间再进行赋值,本身在效率上是低于ArrayList的,巨大的数量积累将这个差距放大了很多。

4)内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

5)线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;


8.List 和 Set 的区别

List , Set 都是继承自Collection 接口

List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

Set和List对比

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

9.HashSet实现原理

①是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

②当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。

③HashSet的其他操作都是基于HashMap的。

10.HashSet如何检查重复?HashSet是如何保证数据不可重复的?

HashSet底层结构是一个HashMap,HashSet将值放在HashMap的键中,如果HashMap的键相同时会发生覆盖,因此HashSet的值不会重复。HashMap检查Key是否相同会通过equles方法,并通过比较hash值判断是否重复。

HashSet 中的add ()方法会使用HashMap 的put()方法。

 //PRESENT是一个空的对象
 private static final Object PRESENT = new Object();
 private transient HashMap<E,Object> map;
 //HashSet的构造方法
 public HashSet() {
     //创建一个HashMap
     map = new HashMap<>();
 }
 //如果触发HashSet的add方法
 public boolean add(E e) {
     // PRESENT是一个不变的固定值
     return map.put(e, PRESENT)==null;
 }

1,如果hash码值不相同,说明是一个新元素,存;

2(1),如果hash码值相同,且equles判断相等,说明元素已经存在,不存;

2(2),如果hash码值相同,且equles判断不相等,说明元素不存在,存;

hashCode()与equals()的相关规定

  • 如果两个对象相等,则hashcode一定也是相同的
  • 两个对象相等,对两个equals方法返回true
  • 两个对象有相同的hashcode值,它们也不一定是相等的
  • 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  • hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

11.HashSet与HashMap的区别

 12.HashMap 的实现原理?

1)hashmap存储结构

这里需要区分一下,JDK1.7和 JDK1.8之后的 HashMap 存储结构。在JDK1.7及之前,是用数组加链表的方式存储的。

但是,众所周知,当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n)。因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树。

hashmap 结构示意图

 put方法详解

//put方法,会先调用一个hash()方法,得到当前key的一个hash值,
//用于确定当前key应该存放在数组的哪个下标位置
//这里的 hash方法,我们姑且先认为是key.hashCode(),其实不是的,一会儿细讲
public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

//把hash值和当前的key,value传入进来
//这里onlyIfAbsent如果为true,表明不能修改已经存在的值,因此我们传入false
//evict只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	//判断table是否为空,如果空的话,会先调用resize扩容
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	//根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素,
	//若没有,则把key、value包装成Node节点,直接添加到此位置。
	// i = (n - 1) & hash 是计算下标位置的,为什么这样算,后边讲
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	else { 
		//如果当前位置已经有元素了,分为三种情况。
		Node<K,V> e; K k;
		//1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等,
		//则把p赋值给e,跳转到①处,后续需要做值的覆盖处理
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		//2.如果当前是红黑树结构,则把它加入到红黑树 
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		else {
		//3.说明此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部
			for (int binCount = 0; ; ++binCount) {
				if ((e = p.next) == null) {
					//如果头结点的下一个节点为空,则插入新节点
					p.next = newNode(hash, key, value, null);
					//如果在插入的过程中,链表长度超过了8,则转化为红黑树
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					//插入成功之后,跳出循环,跳转到①处
					break;
				}
				//若在链表中找到了相同key的话,直接退出循环,跳转到①处
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		//①
		//说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			//用新值替换旧值,并返回旧值。
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			//看方法名字即可知,这是在node被访问之后需要做的操作。其实此处是一个空实现,
			//只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能
			// Callbacks to allow LinkedHashMap post-actions
			//void afterNodeAccess(Node<K,V> p) { }
			afterNodeAccess(e);
			return oldValue;
		}
	}
	//fail-fast机制
	++modCount;
	//如果当前数组中的元素个数超过阈值,则扩容
	if (++size > threshold)
		resize();
	//同样的空实现
	afterNodeInsertion(evict);
	return null;
}

resize()扩容机制

在上边 put 方法中,我们会发现,当数组为空的时候,会调用 resize 方法,当数组的 size 大于阈值的时候,也会调用 resize方法。 那么看下 resize 方法都做了哪些事情吧。

final Node<K,V>[] resize() {
	//旧数组
	Node<K,V>[] oldTab = table;
	//旧数组的容量
	int oldCap = (oldTab == null) ? 0 : oldTab.length;
	//旧数组的扩容阈值,注意看,这里取的是当前对象的 threshold 值,下边的第2种情况会用到。
	int oldThr = threshold;
	//初始化新数组的容量和阈值,分三种情况讨论。
	int newCap, newThr = 0;
	//1.当旧数组的容量大于0时,说明在这之前肯定调用过 resize扩容过一次,才会导致旧容量不为0。
	//为什么这样说呢,之前我在 tableSizeFor 卖了个关子,需要注意的是,它返回的值是赋给了 threshold 而不是 capacity。
	//我们在这之前,压根就没有在任何地方看到过,它给 capacity 赋初始值。
	if (oldCap > 0) {
		//容量达到了最大值
		if (oldCap >= MAXIMUM_CAPACITY) {
			threshold = Integer.MAX_VALUE;
			return oldTab;
		}
		//新数组的容量和阈值都扩大原来的2倍
		else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
				 oldCap >= DEFAULT_INITIAL_CAPACITY)
			newThr = oldThr << 1; // double threshold
	}
	//2.到这里,说明 oldCap <= 0,并且 oldThr(threshold) > 0,这就是 map 初始化的时候,第一次调用 resize的情况
	//而 oldThr的值等于 threshold,此时的 threshold 是通过 tableSizeFor 方法得到的一个2的n次幂的值(我们以16为例)。
	//因此,需要把 oldThr 的值,也就是 threshold ,赋值给新数组的容量 newCap,以保证数组的容量是2的n次幂。
	//所以我们可以得出结论,当map第一次 put 元素的时候,就会走到这个分支,把数组的容量设置为正确的值(2的n次幂)
	//但是,此时 threshold 的值也是2的n次幂,这不对啊,它应该是数组的容量乘以加载因子才对。别着急,这个会在③处理。
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	//3.到这里,说明 oldCap 和 oldThr 都是小于等于0的。也说明我们的map是通过默认无参构造来创建的,
	//于是,数组的容量和阈值都取默认值就可以了,即 16 和 12。
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	//③ 这里就是处理第2种情况,因为只有这种情况 newThr 才为0,
	//因此计算 newThr(用 newCap即16 乘以加载因子 0.75,得到 12) ,并把它赋值给 threshold
	if (newThr == 0) {
		float ft = (float)newCap * loadFactor;
		newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
				  (int)ft : Integer.MAX_VALUE);
	}
	//赋予 threshold 正确的值,表示数组下次需要扩容的阈值(此时就把原来的 16 修正为了 12)。
	threshold = newThr;
	@SuppressWarnings({"rawtypes","unchecked"})
		//我们可以发现,在构造函数时,并没有创建数组,在第一次调用put方法,导致resize的时候,才会把数组创建出来。这是为了延迟加载,提高效率。
		Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	//如果原来的数组不为空,那么我们就需要把原来数组中的元素重新分配到新的数组中
	//如果是第2种情况,由于是第一次调用resize,此时数组肯定是空的,因此也就不需要重新分配元素。
	if (oldTab != null) {
		//遍历旧数组
		for (int j = 0; j < oldCap; ++j) {
			Node<K,V> e;
			//取到当前下标的第一个元素,如果存在,则分三种情况重新分配位置
			if ((e = oldTab[j]) != null) {
				oldTab[j] = null;
				//1.如果当前元素的下一个元素为空,则说明此处只有一个元素
				//则直接用它的hash()值和新数组的容量取模就可以了,得到新的下标位置。
				if (e.next == null)
					newTab[e.hash & (newCap - 1)] = e;
				//2.如果是红黑树结构,则拆分红黑树,必要时有可能退化为链表
				else if (e instanceof TreeNode)
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				//3.到这里说明,这是一个长度大于 1 的普通链表,则需要计算并
				//判断当前位置的链表是否需要移动到新的位置
				else { // preserve order
					// loHead 和 loTail 分别代表链表旧位置的头尾节点
					Node<K,V> loHead = null, loTail = null;
					// hiHead 和 hiTail 分别代表链表移动到新位置的头尾节点
					Node<K,V> hiHead = null, hiTail = null;
					Node<K,V> next;
					do {
						next = e.next;
						//如果当前元素的hash值和oldCap做与运算为0,则原位置不变
						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;
}

get()方法

public V get(Object key) {
	Node<K,V> e;
	//如果节点为空,则返回null,否则返回节点的value。这也说明,hashMap是支持value为null的。
	//因此,我们就明白了,为什么hashMap支持Key和value都为null
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	//首先要确保数组不能为空,然后取到当前hash值计算出来的下标位置的第一个元素
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
		//若hash值和key都相等,则说明我们要找的就是第一个元素,直接返回
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		//如果不是的话,就遍历当前链表(或红黑树)
		if ((e = first.next) != null) {
			//如果是红黑树结构,则找到当前key所在的节点位置
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			//如果是普通链表,则向后遍历查找,直到找到或者遍历到链表末尾为止。
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	//否则,说明没有找到,返回null
	return null;
}

HashMap的几个重要知识点

  1. HashMap是无序且不安全的数据结构。

  2. HashMap 是以key–value对的形式存储的,key值是唯一的(可以为null),一个key只能对应着一个value,但是value是可以重复的。

  3. HashMap 如果再次添加相同的key值,它会覆盖key值所对应的内容,这也是与HashSet不同的一点,Set通过add添加相同的对象,不会再添加到Set中去。

  4. HashMap 提供了get方法,通过key值取对应的value值,但是HashSet只能通过迭代器Iterator来遍历数据,找对象。

JDK7与JDK8的HashMap区别

  1. jdk8中添加了红黑树,当链表长度大于等于8的时候链表会变成红黑树
  2. 链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红 黑树所以采用插入尾节点)

HashMap的默认负载因子

     /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     *默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用
     *无参构造方法创建的HashMap 对象,他初始长度默认是16  阈值 = 当前长度 * 0.75  就
     *能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容
     */

为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?

  1. 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的次幂乘积结果都是整数。

  2. 理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的

  3. 另一种说法:   当负载因子为1.0时,意味着只有当hashMap装满之后才会进行扩容,虽然空间利用率有大的提升,但是这就会导致大量的hash冲突,使得查询效率变低。当负载因子为0.5或者更低的时候,hash冲突降低,查询效率提高,但是由于负载因子太低,导致原来只需要1M的空间存储信息,现在用了2M的空间。最终结果就是空间利用率太低。(负载因子是0.75的时候,这是时间和空间的权衡,空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度也比较低,提升了空间效率 )

HashMap的扩容机制

写数据之后会可能触发扩容,HashMap结构内,我记得有一个记录当前数据量的字段,这个数据量字段到达扩容阈值的话,它就会触发扩容的操作 

阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 
当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
 
扩容的规则是这样的,因为table数组长度必须是2的次方数,
扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 
即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
容量的两倍,但记住HashMap的扩容是采用当前容量
向左位移一位(newtableSize = tableSize << 1),
得到的扩容后容量,而不是当前容量x2

为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?

因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

jdk8中HashMap为什么要引入红黑树?

其实主要就是为了解决jdk1.8以前hash冲突所导致的链化严重的问题,因为链表结构的查询效率是非常低的,他不像数组,能通过索引快速找到想要的值,链表只能挨个遍历,当hash冲突非常严重的时候,链表过长的情况下,就会严重影响查询性能,本身散列列表最理想的查询效率为O(1),当时链化后链化特别严重,他就会导致查询退化为O(n)为了解决这个问题所以jdk8中的HashMap添加了红黑树来解决这个问题,当链表长度>=8的时候链表就会变成红黑树,红黑树其实就是一颗特殊的二叉排序树嘛,这个时间复杂…反正就是要比列表强很多

什么是哈希冲突

由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)

如何解决哈希冲突

1.开放地址法

发生哈希冲突后,按照某一次序找到下一个空闲的单元,把冲突的元素放入。

线性探查法:
    从发生冲突的单元开始探查,依次查看下一个单元是否为空,如果到了最后一个单元还是空,那么再从表首依次判断。如此执行直到碰到了空闲的单元或者已经探查完所有单元。

平方探查法
    从发生冲突的单元加上1^2,2^2,3^2,...,n^2,直到遇到空闲的单元

双散列函数探查法
    定义两个散列函数,分别为s1和s2,s1的算法和前面一致,s2取一个1~m-1之间并和m互为素数的数。s2作为步长。

2.链地址法(拉链法)

将哈希值相同的元素构成一个链表,head放在散列表中。一般链表长度超过了8就转为红黑树,长度少于6个就变为链表。 

3.再哈希法

  同时构造多个不同的哈希函数,Hi = RHi(key) i= 1,2,3 … k; 
当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。

4.创建公共溢出区

 把哈希表分为公共表和溢出表,如果发生了溢出,溢出的数据全部放在溢出区。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值