Java容器学习笔记

目录

1 集合概述

1.1 集合和数组的区别

1.2 Java集合概览

1.3 List、Set和Map的区别

1.4 集合底层数据结构

2 List接口

2.1 ArrayList与Vector的区别

2.2 ArrayList和LinkedList的区别

2.3 ArrayList扩容机制

2.4 List的遍历方式

3 Set接口

3.1 Comparable和Comparator的区别

3.2 无序性和不可重复性的含义

3.3 HashSet、LinkedHashSet和TreeSet的异同

3.4 HashSet如何检查重复

3.5 Set的遍历方式

4 Map接口

4.1 HashMap和HashTable的区别

4.2 HashMap和HashSet的区别

4.3 HashMap和TreeMap的区别

4.4 HashMap的底层实现

4.5 HashMap的长度为什么是2的幂次方

4.6 HashMap的遍历方式

4.7 HashMap多线程操作导致死循环问题

4.8 ConcurrentHashMap和HashTable的区别

4.9 ConcurrentHashMap线程安全的具体实现方式


1 集合概述

1.1 集合和数组的区别

  • 数组长度固定,而集合长度可变;
  • 数组可以存储基本数据类型可以存储引用数据类型,而集合只能存储引用数据类型;
  • 数组存储的元素必须是同一个数据类型,而集合存储的对象可以是不同数据类型;

1.2 Java集合概览

在这里插入图片描述

1.3 List、Set和Map的区别

  • List:单列集合,存储的元素有序(元素存入和取出的顺序一致),有索引可重复,可为null;
  • Set:单列集合,存储的元素无序(元素存入和取出的顺序可能不一致),无索引不可重复,只有一个null元素;
  • Map:双列集合,使用键值对(key-value)存储元素,key无序,不可重复value无序,可重复,key与value一一对应。

1.4 集合底层数据结构

List:

  • ArrayList/Vecctor:Object[] 数组
  • LinkedList:双向链表

Set:

  • HashSet:底层采用HashMap来保存元素(无序,唯一)
  • LinkedHashSet:底层采用LinkedHashMap(有序)
  • TreeSet:红黑树(有序,唯一)

Map:

  • HashMap:JDK1.8之前,数组+链表(解决哈希冲突);JDK1.8之后,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(注意:将链表转换成红黑树前会先判断,若当前数组长度小于64,则进行数组扩容,而不是红黑树转换)。
  • LinkedHashMap:在数组+链表/红黑树的基础上,增加了一条双向链表,以保证键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • HashTable:数组+链表
  • TreeMap:红黑树

2 List接口

2.1 ArrayList与Vector的区别

  • ArrayList线程不安全,线程不同步,效率高,每次扩容的大小是原来的1.5倍;
  • Vector线程安全,线程同步,效率低,可以指定扩容的大小,默认是原来的2倍。

2.2 ArrayList和LinkedList的区别

  • 线程安全:ArrayList和LinkedList都不同步,都不保证线程安全;
  • 底层数据结构:ArrayList底层使用Object数组,LinkedList底层采用双向链表
  • 插入和删除是否受元素位置的影响:ArrayList采用动态数组存储元素,所以插入和删除元素的时间复杂度元素位置的影响(列表末尾为O(1),指定位置i为O(n-i));LinkedList采用链表存储元素,所以插入和删除元素的时间复杂度不受元素位置的影响,都是近似O(1);
  • 是否支持快速随机访问:ArrayList支持快速随机访问,而LinkedList不支持(快速随机访问即通过元素的索引快速获取元素对象);
  • 内存空间占用:ArrayList的空间浪费主要体现在列表的结尾会预留一定的容量空间,而 LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

2.3 ArrayList扩容机制

详细内容请参考浅谈 ArrayList 及其扩容机制

//扩容函数grow()
//minCapacity=oldCapacity+1
private Object[] grow(int minCapacity) {
    // 获取老容量,也就是当前容量
    int oldCapacity = elementData.length;
    // 如果当前容量大于0 或者 数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //ArraySupport.newLength()函数的作用是创建一个大小为oldCapacity+max(minimium growth, prferred growth)的数组
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        //创建一个新数组,将旧数组拷贝到新数组,并赋给elementData
        return elementData = Arrays.copyOf(elementData, newCapacity);
    // 如果 数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量等于0的话,只剩这一种情况了)
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

扩容可分为两种情况:

  第一种情况,当ArrayList的容量为0时,此时添加元素的话,需要扩容,三种构造方法创建的ArrayList在扩容时略有不同:

    1.无参构造,创建ArrayList后容量为0,添加第一个元素后,容量变为10,此后若需要扩容,则正常扩容(对应else情况)。

    2.传容量构造,当参数为0时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。

    3.传列表构造,当列表为空时,创建ArrayList后容量为0,添加第一个元素后,容量为1,此时ArrayList是满的,下次添加元素时需正常扩容。

  第二种情况,当ArrayList的容量大于0,并且ArrayList是满的时,此时添加元素的话,进行正常扩容,每次扩容到原来的1.5倍(oldCapacity+oldCapacity>>1)

2.4 List的遍历方式

  • for循环遍历:基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止;
  • Iterator遍历:Iterator是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java在Collections中支持了Iterator模式。
  • for each遍历:foreach内部也是采用了Iterator的方式实现,使用时不需要显式声明Iterator或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

3 Set接口

3.1 Comparable和Comparator的区别

package java.lang;
public interface Comparable<T> {
    public int compareTo(T o); // 比较此对象与指定对象的顺序,如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
}
———————————————————————————————————————————————————————————————————————————————————————————
packet java.util;
public interface Comparator<T> {
	int compare(T o1, T o2);
	//还有很多其他方法...
}

  Comparable是排序接口,若一个类实现了Comparable接口,就意味着“该类支持排序”。而Comparator是比较器,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。

  Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。

3.2 无序性和不可重复性的含义

  • 无序性:无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的
  • 不可重复性:指添加的元素按照equals()判断时 ,返回false(需要同时重写equals()方法和HashCode()方法)。

3.3 HashSet、LinkedHashSet和TreeSet的异同

  • HashSet:底层采用HashMap线程不安全,可以存储null值;
  • LinkedHashSet:HashSet的子类,能够按照添加元素的顺序遍历
  • TreeSet:底层使用红黑树能够按照添加元素的顺序遍历,排序方式有自然排序和定制排序。

3.4 HashSet如何检查重复

当把对象加入HashSet时,首先计算对象的hashcode值来判断对象的加入位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相同的hashcode值,则将元素加入到相应位置;若发现有相同hashcode值的对象,则会调用equals()方法来检查哈希值相等的对象是否真的相同。两者相同,则加入操作失败,否则,加入操作成功。

注意:

  • 两个对象相等,hashcode值一定相同;
  • 两个对象相等,equals()方法返回true;
  • 两个对象hashcode值相同,两个对象不一定相等;
  • 重写equals()方法,必须重写hashcode()方法。

3.5 Set的遍历方式

只能采用Iterator迭代器遍历for each遍历

4 Map接口

4.1 HashMap和HashTable的区别

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
————————————————————————————————————————————————————————————————
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, Serializable
  • 线程是否安全:HashMap是非线程安全的,HashTable是线程安全的,因为HashTable内部的方法基本都经过synchronized修饰;
  • 效率:HashMap比HashTable效率高(HashTable基本被淘汰,不要在代码中使用它);
  • 对Null key和Null value的支持:HashMap可以存储null的key和value,但null作为key只能有一个,null作为value可以有多个。HashTable不允许有null的key和null的value,否则会抛出NullPointerException异常;
  • 初始容量大小和每次扩充容量大小:1)创建时如果不指定容量初始值HashTable默认的初始大小为11,之后每次扩充,容量变为原来的 2n+1HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的 2 倍。2)创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小(通过tableSizeFor()方法保证)。也就是说HashMap总是使用2的幂作为哈希表的大小;
  • 底层数据结构:HashMap在JDK1.8之后增加了链表转红黑树的机制,HashTable则没有。

4.2 HashMap和HashSet的区别

HashMapHashSet
实现了Map接口实现了Set接口
存储键值对仅存储对象
调用put()向map中添加元素调用add()方法向set中添加元素
HashMap使用key计算hashcodeHashSet使用成员对象计算hashcode值,对于两个对象来说,hashcode可能相同,所以还需用equals()方法判断对象的内容

4.3 HashMap和TreeMap的区别

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
————————————————————————————————————————————————————————————————————————————————————
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

从类的定义来看,HashMap和TreeMap都继承自AbstractMap,不同的是HashMap实现的是Map接口,而TreeMap实现的是NavigableMap接口。除此之外,TreeMap还实现了SortedMap接口

  • NavigableMap接口:让TreeMap有了对集合内元素的搜索的能力;
  • SortedMap接口:让TreeMap有了对集合内的元素根据键排序的能力(默认按key的升序排序)。

4.4 HashMap的底层实现

JDK1.8之前:数组+链表(即,链表散列)。HashMap首先计算key的hashcode值,然后通过扰动函数处理这个值得到hash值,再通过(n-1)&hash判断当前元素的存放位置(n为数组长度),如果当前位置存在元素,判断该元素与要存入的元素的hash值及key是否相同,如果相同,则直接覆盖,不同则通过拉链法解决冲突。

所谓扰动函数指的就是HashMap的hash方法。使用扰动函数是为了防止一些实现比较差的hashCode()方法,也就是说使用扰动函数可以减少碰撞。所谓 “拉链法” 就是指将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8之后:当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

详细分析请参考最通俗易懂搞定HashMap的底层原理

4.5 HashMap的长度为什么是2的幂次方

哈希值的范围很大(-2147483648 到 2147483647),内存是放不下的,因此不能直接作为数组下标。需要让哈希值对数组的长度做取模运算(hash%length),得到的余数才能是对应的数组下标,也就是要存放的位置。

取余(%)操作中如果除数(length)是2的幂次方,则等价于和其除数减一的数值做与(&)操作,即hash%length==hash&(length-1)因为二进制操作&,相对于%能提高运算效率,因此,当数组长度(length)是2的幂次方时,可以利用&操作提高计算效率。

4.6 HashMap的遍历方式

HashMap 的 7 种遍历方式与性能分析!

补充:Map的遍历方式?

键找值方式,即通过元素中的键,获取键所对应的值

  • 获取Map中所有的key,返回一个Set集合存储所有的key(keyset()方法);
  • 遍历Set集合,获取每个key;
  • 根据key,获取key所对应的value(get(K key)方法)。

Entry将键值对的对应关系封装成了对象,即键值对对象,每一对键值对对象,称为一个Entry项。在创建Map集合的时候,也会在Map集合中创建一个Entry对象,用来记录Key和Value。

键值对方式:即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值

  • 获取Map集合中,所有的键值对(Entry)对象,以Set集合形式返回(entrySet()方法);
  • 遍历Set集合,得到每一个键值对(Entry)对象;
  • 通过键值对(Entry)对象,获取Entry对象中的键与值(getkey()、getValue()方法)

4.7 HashMap多线程操作导致死循环问题

主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。

详细分析请参考JAVA HASHMAP的死循环

4.8 ConcurrentHashMap和HashTable的区别

ConcurrentHashMap和HashTable的区别主要体现在实现线程安全的方式上。

  • 底层数据结构:JDK1.7的ConcurrentHashMap采用分段的数组+链表实现,JDK1.8则采用数组+链表/红黑树实现。HashTable采用数组+链表实现;
  • 实现线程安全的方式:1)JDK1.7,采用分段锁(ConcurrentHashMap)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器的一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。JDK1.8摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。2)HashTable同一把锁 :使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用 get,竞争会越来越激烈效率越低。

 JDK1.7 的 ConcurrentHashMap:

  JDK1.8 的 ConcurrentHashMap:

4.9 ConcurrentHashMap线程安全的具体实现方式

JDK1.7:首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap由Segment数组结构和HashEntry数组结构组成。

Segment实现了ReentranLock所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。

一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。

JDK1.8

ConcurrentHashMap取消了Segment分段锁,采用 CAS 和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

参考链接

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值