【集合】集合

6 篇文章 0 订阅
4 篇文章 0 订阅

集合

在jdk的java.util包下,集合主要派生自Collection接口和Map接口,Collection接口属于对象的集合,Map接口属于键值对的集合。

实现自Collection接口的又有List接口(有序可重复)和Set接口(不可重复)。

List接口的实现类有LinkedList(基于链表,增删快,查询慢,线程不安全),ArrayList(基于数组,查询快,增删慢,线程不安全),Vector(基于数组,线程安全,读写都加锁,每个方法都加了synchronized),CopyOnWriteArrayList(线程安全,写加锁,读不加锁,写时拷贝一个新的数组,写结束后数组指针指向新数组),Stack(基于数组,继承自Vector,线程安全的栈,先进后出)。

Set接口的实现类有HashSet(基于HashMap实现),TreeSet(底层红黑树,基于TreeMap实现),LinkedHashSet(继承自HashSet,保留元素插入顺序)。

实现自Map接口的HashMap(基于哈希映射,线程不安全,key唯一,key和value都可以为null),Hashtable(key和value都不能为null,大部分方法加锁,线程安全,效率低),ConcurrentHashMap(key或value不能为空,根据key的hash值找到桶的位置,锁住头节点,写操作后释放锁;针对读操作采用volatile修饰Node<K, V>[]类型的集合元素),TreeMap(底层红黑树,元素可排序),LinkedHashMap(基于哈希表和双向链表,保留元素插入顺序)。

Collection<E>是Java集合框架中的基本接口;

Collections是Java集合框架提供的一个工具类,其中包含了用于操作或返回集合的静态方法。

比如Collections.sort(list):对指定的list按升序进行排序,list中的元素要实现Comparable接口。

Collections.reverse(list):对指定的list进行反转。

Collections.copy(list,li):将li元素拷贝到list中。

Collections.synchronizedMap(Map):创建线程安全的Map集合。

1.1 HashMap
  • jdk1.7中HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。1.8中,除了数组和单链表外,当单链表中元素个数超过8个时(且元素数量大于64(小于64时会扩容而不是转红黑树)),会进而转化为红黑树存储,巧妙地将遍历元素的时间复杂度从O(n)降低到了O(logn))。1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行**&**操作来计算新的索引位置【(e.hash & oldCap) == 0/1,扩容后元素在新数组中的位置不变或+oldCap这两种】

  • 执行构造函数时,存储元素的数组并不会进行初始化,而是在第一次放入元素的时候,才会进行初始化操作。创建HashMap对象时,仅仅计算初始容量tableSizeFor()和新增阈值。

  • HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。但并发下put方法还是会出现数据覆盖的问题。

1.1.1 HashMap的底层数据结构?

JDK1.7:数组+单链表。JDK1.8:数组+单链表(单链表中元素个数超过8个且数组节点数超过64时转为红黑树存储,否则扩容)

链表查询O(N),插入Olog1;

红黑树查询插入都是OlogN

int hash = hash(key);//根据key计算哈希值
int i = indexFor(hash, table.length);//根据哈希值和数组长度计算在数组中的索引位置
									 //(n - 1) & hash数组长度和hash值相与计算index
1.1.2 HashMap的存取原理?

通过获取key对象的hashcode计算出该对象的哈希值,通过该哈希值与数组长度减去1进行位与运算(n-1 & hash),得到数组的位置,没有冲突就新建节点,当发生hash冲突时,如果key值一样,则会替换旧的key的value,key不一样则新建next链表结点,当链表的长度超过8且数组容量大于64(树形化最小容量),则转换为红黑树存储。

1.1.3 Java7和Java8的区别?

JDK1.7:数组+单链表。JDK1.8:数组+单链表(单链表中元素个数超过8个时转为红黑树存储)。

JDK1.7扩容时转移原来的数据到newtable中采用头插法,1.8改为尾插法。

JDK1.7创建hashmap对象会创建一个长度为16的Entry[] table用来存储键值对数据。jdk1.8之后不是在构造方法创建了,而是在第一次调用put方法时才进行创建Node[] table。

1.1.4 为啥会线程不安全?

jdk1.7中,在多线程环境下,(头插法)扩容时会造成环形链或数据丢失。

jdk1.8中,在多线程环境下,PUT方法会发生数据覆盖的情况。

1.1.1 有什么线程安全的类代替么?

currentHashMap 以及 hashTable

1.1.6 默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

设置太大,就会浪费内存空间;设置太小,放几个元素就会扩容。

与index计算公式有关【index = (n - 1) & hash】,16-1=11的所有二进制位全为1,这种情况下,index的结果等同于hash后几位的值,只要计算出的hash本身分布均匀,Hash算法的结果就是均匀的。

数组下标 = (数组长度 - 1)& hash。当数组长度即n是2的幂时,n-1的二进制则为0000 1111…,保证了后几位全都是1,此时再和hash值相与,就保证了结果一定是hash值的后几位

①为什么要下标结果和hash值后几位相同?—这样可以保证只要hash值分布足够均匀,数组下标就足够分散。

②这里的后几位是多少位?(为什么用数组长度-1?) —n-1,当n是8时,8-1的二进制为0000 1111。此时与hash值相与,后四位得到的结果一定小于等于1111,也就保证了结果一定小于8且大于等于0。同时也就保证了数组下标大于等于0,小于数组长度n。不会出现数组下标越界的情况。

方便位运算。

1.1.7 HashMap的扩容方式?负载因子是多少?为什么是这么多?

当前插入元素大于Capacity*(HashMap当前长度)* x LoadFactor*(负载因子,默认值0.71f)*时进行扩容。

:创建一个原数组两倍长度的空数组【确保2的幂】;

:jdk1.7:遍历原数组,把所有元素重新计算hash和index到新数组。

​ jdk1.8:根据(e.hash & oldCap=0/1)决定是原下标位置,还是原下标+oldCap下标位置。1

0.71----当负载因子过大的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利(时间换空间)。当负载因子过小,数组所能存储的元素就会变少(空间换时间)。负载因子是0.71的时候,存储元素比较多,避免了过多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。threshold = loadFactor * capacity。【0.71正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数】

1.1.8 HashMap的主要参数都有哪些?
/**
 * 默认初始化容量,必须是2的次方
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大容量。即HashMap的数组容量必须小于等于 1 << 30
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默认的负载因子
 */
static final float DEFAULT_LOAD_FACTOR = 0.71f;

/**
 * 树形化阈值;即当链表的长度大于8的时候,会将链表转为红黑树,优化查询效率。链表查询的时间复杂度为 
 * o(n) , 红黑树查询的时间复杂度为 o(log n)
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 解树形化阈值;其实就是当红黑树的节点的个数小于等于6时,会将红黑树结构转为链表结构。
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 树形化的最小容量;前面我们看到有一个树形化阈值,就是当链表的长度大于8的时候,会从链表转为红黑	  * 树。其实不一定是这样的。转为红黑树有两个条件:
*	① 链表的长度大于8
*	② HashMap数组的容量大于等于64
*	需要当上述两个条件都成立的情况下,链表结构才会转为红黑树。
 */
static final int MIN_TREEIFY_CAPACITY = 64;

1.1.9 HashMap是怎么处理hash碰撞的?

put流程:

如果是第一次put调用 resize初始化数组;

首先根据key的hashcode内存地址与自己右移16位进行异或得到hash值;

hash值与(数组容量-1)相与,求出数组下标,下标位置为空直接新建节点;

下标位置有值且key值相同则直接替换;

key值不同说明出现hash冲突,是树节点就用树节点添加方式加入;

不是树节点就添加到链表上,添加之后校验链表节点是否超过8,并且数组容量是否超过64,是则转为红黑树,否则将数组扩容;

put后如果节点数量大于阈值则扩容。

1.1.10 hash的计算规则?

将对象的key.hashcode()方法返回的hash值,进行无符号的右移16位,并与原来的hash值进行按位异或操作,目的是将hash的低16bit和高16bit做了一个异或,使返回的值足够散列

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

因为当前hashcode方法计算的散列值仍会出现较多冲突,由于数组的边界影响,hashcode值的高位几乎不会用到。所以利用右移16位后的hashcode与自身进行异或,提升hash值的散列性,减少系统的损失。

1.1.11 如何解决初始化,输入的值不是2的n次幂

tableSizeFor()方法,将值的二进制数,从左边第一个出现的1开始,右边的所有值都通过位或运算变成1,使得可以找出比当前值大一点点的2的幂的数

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }
1.1.12 Set集合是如何保证元素不重复的?

因为HashSet的底层实现都是创建了一个HashMap,而HashMap的add方法,是将元素作为map的key进行存储的,map的key是不会重复的,所以HashSet中的元素也不会重复。

1.1.13 重写equals方法,还必须要重写hashcode方法

使用hashcode方法将对象定义到一个地址上,hashcode值不同则对象一定不同;hashcode相同之后再用equals比较,减少了equals的使用。

如果重写了equals方法比较值是否相等,而没重写hashcode方法,会出现值相等的对象equals相同,但前一步的hashcode不同,结果导致equals不相等。

1.1.14 看hashmap源码学到了什么?
  • 计算机位运算更快
  • hashmap平时用的最多,看源码能理解原理,出现问题知道原因在哪。
  • 也学到了编码方式。
1.1.15 线程安全的map
  • jdk1.7中,在多线程环境下,(头插法)扩容时会造成环形链或数据丢失。

  • jdk1.8中,在多线程环境下,PUT方法会发生数据覆盖的情况。

实现线程安全的map:
  1. Collections.synchronizedMap(new HashMap<>());

    将map对象付给mutex对象,之后每次操作map都会在代码块加上synchronized(mutex)。

  2. Hashtable

    对大部分方法加上synchronized,缺点就是效率低,有时候不需要加锁,却仍要线程等待。

  3. ConcurrentHashMap

    在jdk1.7中ConcurrentHashMap的底层数据结构是数组(Segment)加链表(HashEntry)

    对于ConcurrentHashMap的添加,删除操作,在操作开始前,线程都会获取Segment的互斥锁;操作完毕之后,才会释放。而对于读取操作,它是通过volatile去实现的,HashEntry数组是volatile类型的

    jdk1.8使用红黑树O(logn)来优化链表O(n);并取消了segment数组,直接用Node[]保存数据,降低锁的粒度,减少并发冲突的概率。通过cas的方式添加元素。

List
  • ArrayList和LinkedList区别

    Arraylist是基于数组实现的,随机访问较快,插入慢,因为要移动插入位置后面的所有元素。

    LinkedList是基于链表实现的,查询慢,但是插入删除快,因为不需要移动元素,也不用扩容。

    查询多用Arraylist,删除插入多用Linkedlist。

  • Arraylist

    jdk1.8中,初始化时容量为空,第一次添加元素才扩大为10;也可以初始化时自定义容量。

    扩容:

    添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。10->15->22>。。。

    扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

    ArrayList动态扩容的全过程。如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时,才真正分配容量。每次按照1.5倍(位运算)的比率通过copeOf的方式扩容。 在JKD1.6中实现是,如果通过无参构造的话,初始数组容量为10.每次通过copeOf的方式扩容后容量为原来的1.5倍加1

  • 不允许插入空值的集合

    会自然排序的都不能插入空值,比如concurrenthashmap、treeset、treemap基于红黑树不能插空值,hashtable。

  • Treemap注意事项

    不能插入null值,因为底层数据结构是红黑树;

    作为key的对象必须实现comparable接口,实现排序;

    因为要比较和排序,所以key必须是同一种类型。

  • 线程安全的list
    1. Collections.synchronizedList(new ArrayList())

      代码块加synchronized(mutex),类似于Collections.synchronizedMap。

    2. vector

      每个方法都用synchronized修饰,太慢了。

    3. CopyOnWriteArrayList

      • 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将指针指向新数组。
      • 写时方法内部自动加锁,读不加锁,因为读的是旧数组

      缺点:

      • 占用太多内存,每次修改都要复制出一个新的数组
      • 只能保证数据最终一致性,不能保证数据实时一致性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值