第十三章 java集合常见问题总结(下)

HashMap是非线程安全的,Hashtable是线程安全但效率低。HashMap在JDK1.8后处理冲突时用到了红黑树,增加了性能。ConcurrentHashMap在JDK1.7和1.8中采用了不同的线程安全策略。Collections工具类提供排序、查找和同步控制等功能。
摘要由CSDN通过智能技术生成

集合

Map接口

HashMap和Hashtable区别

  1. 线程是否安全
    HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap)
  2. 效率
    因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它
  3. 对 Null key 和 Null value 的支持
    HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  4. 初始容量大小和每次扩充容量大小的不同
    ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍
    ② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小
  5. 底层数据结构
    JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable 没有这样的机制。

HashMap中带有初始容量的构造函数:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal 
            initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal 
            load factor: " + loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

下面的方法保证了HashMap总是使用2的幂次方作为哈希表的大小:

    static final int tableSizeFor(int cap) {
    	//-1可以保证当传入的数刚好是2的次方时,可以正确
    	的返回其本身,例:传入的是16,经过下面的计算后还是返回16
        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;
    }

方法的作用是找到一个离cap最近的2的n次方数。

  1. 运算符|=:
    n |= n 等同于 n = n | n;
    | 是位运算符(或)
  2. 运算符>>>
    是无符号右移运算符
    就是把一个二进制数右移指定位数,高位补0

HashMap和HashSet区别

HashSet底层是基于HashMap实现的。
HashSet的源码很少,因为除了clone(),writeObject(),readObject()是自己不得不实现的,其他的都直接调用HashMap中的方法
HashMap和HashSet

  1. 实现了Map接口;实现了Set接口
  2. 存储键值对;仅存储对象
  3. 调用put()向map中添加元素;调用add()方法向Set中添加元素
  4. 使用键来计算hashcode;使用成员对象来计算hashcode值,对于两个对象来说可能相同,所以需要再用equals方法来判断对象的想等性

HashMap和TreeMap区别

TreeMap和HashMap都继承于AbstractMap,但是TreeMap还实现了NavigableMap接口和SortedMap接口。
NavigableMap作用:让TreeMap有了对集合内元素的搜索的能力
SortedMap作用:让TreeMap有了对集合中元素根据键排序的能力。默认是按照key进行升序排序,也可以指定排序的比较器。例如:

public class Person {
    private Integer age;

    public Person(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }
    public static void main(String[] args) {
        TreeMap<Person, String> treeMap = new TreeMap<>(new 
        Comparator<Person>() {
            @Override
         public int compare(Person person1, Person person2) {
               int num = person1.getAge() - person2.getAge();
               return Integer.compare(num, 0);
            }
        });
        treeMap.put(new Person(3), "person1");
        treeMap.put(new Person(18), "person2");
        treeMap.put(new Person(35), "person3");
        treeMap.put(new Person(16), "person4");
   treeMap.entrySet().stream().forEach(personStringEntry -> {
            System.out.println(personStringEntry.getValue());
        });
    }
}

是通过传入匿名内部类的方式实现,输出如下:

person1
person4
person2
person3

也可以替换成Lambda表达式:

TreeMap<Person, String> treeMap = new TreeMap<>((person1,
 person2) -> {
  int num = person1.getAge() - person2.getAge();
  return Integer.compare(num, 0);
});

注:

  1. keySet()和entrySet(),是Map集合中的两种取值方法。
    与get(Object key)相比,get(Object key)只能返回到指定键所映射的值,不能一次全部取出。而keySet()和entrySet()可以。
    Map集合中没有迭代器,Map集合取出键值的原理:将map集合转成set集合,再通过迭代器取出。
  2. 通过数据源(集合,数组等)生成流。如:list.stream();
  3. list.forEach()和list.stream().forEach()区别
    功能都是遍历数组每个元素并执行入参的accept方法,但是它们的实现方式却不一样,在一些特定的情况下,执行会出现不同的结果。
    list.stream().forEach()它首先将集合转换为流,然后对集合的流进行迭代
    list.forEach()使用增强的for循环(默认)
    综上,HashMap和TreeMap相比,TreeMap主要多了对集合中的元素根据键排序的能力和对集合内元素的搜索的能力

HashSet检查重复

把对象加入HashSet时,会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相同的hashcode,HashSet会假设对象没有重复出现。
但是如果发现有相同的hashcode值的对象,会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,那么就不能令它加入操作成功。
在JDK1.8中,HashSet的add()方法制式简单的调用了HashMap的put()方法,并且判断了一下返回值以确保是否有重复元素:

// 返回值:当 set 中没有包含 add 的元素时返回真
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}

在HashMap的putVal()方法中,可以看到如下说明:

final V putVal(int hash, K key, V value, 
boolean onlyIfAbsent, boolean evict) {
...
}

由putVal()源码得知,HashMap的putVal方法:插入一个新的键值对,如果该键存在,则用新值覆盖旧值,方法返回值为旧值,如果该键不存在,方法返回值为null。

HashMap的底层实现

JDK1.8之前,HashMap的底层是数组和链表结合在一起使用,叫做:链表散列。HashMap通过key的hashcode经过扰动函数处理后得到hash值,然后通过(n-1)&hash,判断当前元素存放的位置(此处n代表数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值和key是否相同,如果相同的话,就直接进行覆盖,不相同就通过拉链法解决冲突。
扰动函数:HashMap的hash方法。使用hash方法就是扰动函数为了防止一些实现比较差的hashCode()方法。总结一句话就是为了减少碰撞
JDK1.8的hash方法源码如下:

    static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) 
      ^ (h >>> 16);
  }

HashMap的“拉链法”:
将链表和数组相结合。创建一个链表数组,数组中的每一格就是一个链表,如果遇到哈希冲突,则将冲突的值加到链表中。
JDK1.8以后,再解决哈希冲突有了很大的变化:如果链表长度大于阈值(默认是8),则将链表转换为红黑树,减少搜索的时间。将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树
注意:
TreeMap,TreeSet和JDK1.8以后的HashMap底层都用到了红黑树。红黑树是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化为一个线性结构。
分析HashMap链表到红黑树的转换,如下:

  1. putVal方法中执行链表转红黑树的判断逻辑
    链表的长度大于 8 的时候,就执行 treeifyBin (转换红黑树)的逻辑。
// 遍历链表
for (int binCount = 0; ; ++binCount) {
    // 遍历到链表最后一个节点
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        // 如果链表元素个数大于等于TREEIFY_THRESHOLD(8)
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            // 红黑树转换(并不会直接转换成红黑树)
            treeifyBin(tab, hash);
        break;
    }
    if (e.hash == hash &&
        ((k = e.key) == key || 
        (key != null && key.equals(k))))
        break;
    p = e;
}
  1. treeifyBin 方法中判断是否真的转换为红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 判断当前数组的长度是否小于 64
    if (tab == null || (n = tab.length) < 
    MIN_TREEIFY_CAPACITY)
        // 如果当前数组的长度小于 64,那么会选择先进行数组扩容
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 否则才将列表转换为红黑树

        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

HashMap的长度是2的幂次方的原因

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。 这也就解释了 HashMap 的长度为什么是 2 的幂次方。
算法设计如下:
首先会想到采用%取余的操作来实现。但是:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作,也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方。而且采用二进制位操作 &,相对于%能够提高运算效率

HashMap多线程操作可能会出现死循环

原因:
并发下的Rehash(数组扩容),会造成元素之间会形成一个循环链表。jdk1.8后解决了这个问题,但不建议在多线程下使用HashMap,因为多线程下使用HashMap存在,例如:数据丢失。
并发环境下推荐使用ConcurrentHashMap

ConcurrentHashMap和Hashtable的区别

区别主要体现在实现线程安全的方式不同。

  1. 实现线程安全的方式
    在JDK1.7,ConcurrentHashMap对整个桶数据进行了分割分段,用到了分段锁,每把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    在JDK1.8,ConcurrentHashMap摒弃了分段锁的概念,直接用Node数组+链表+红黑树,并发控制使用synchronized和CAS操作。
    JDK1.6以后的synchronized锁做了优化,看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到分段锁的数据结构,但是简化了属性,只是为了兼容旧版本
    Hashtable是同一把锁,使用synchronized来保证线程安全,效率低下。当一个线程访问同步方时,其他线程也访问同步方法,可能会进入阻塞或者轮询状态。例如:使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争越激烈,效率越低。
    注意:
    Node只能用于链表的情况,红黑树的情况需要使用TreeNode。当冲突链表达到一定长度,链表会转换成红黑树。
    TreeNode是存储红黑树节点,被TreeBin包装。TreeBin通过root属性维护红黑树的根节点,因为红黑树在旋转的时候,根节电可能会被原来的子节点替换掉,这个时间点,如果有其他线程要写这颗红黑树就会发生线程不安全问题。所以在ConcurrentHashMap中TreeBin通过waiter属性维护当前使用这颗红黑树的线程,防止其他线程的进入。
    Java 8在链表长度超过一定阈值(8)时将链表转换为红黑树。寻址时间复杂度由O(n)变为O(log(n))。锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突就不会产生并发,不影响其他Node的读写,提高了效率
  2. 底层数据结构
    JDK1.7的ConcurrentHashMap底层采用分段数组+链表,JDK1.8采用数据结构和HashMap的结构一样,是数组+链表/红黑二叉树
    Hashtable和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表,数组是HashMap的主题,链表是为了解决哈希冲突

JDK1.7和1.8中的ConcurrentHashMap的不同点

  1. 线程安全实现方式
    分段锁,Segment是继承ReentrantLock;Node+CAS+synchronized保证线程安全,锁粒度更细,synchronized只锁定当前链表或红黑二叉树的首节点
  2. Hash碰撞解决方法
    拉链法;拉链法+红黑树
  3. 并发度
    分段锁的个数,默认是16;Node数组的大小,并发度更大

Collections工具类(了解即可)

常用方法

排序;查找,替换;同步控制(如果需要线程安全的集合类型,使用JUC包下的并发集合)

排序
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)
//定制排序,由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)
//旋转。当distance为正数时,将list后distance个元素整体移到前面。
//当distance为负数时,将 list的前distance个元素整体移到后面
查找,替换操作
int binarySearch(List list, Object key)
//对List进行二分查找,返回索引,注意List必须是有序的

int max(Collection coll)
//根据元素的自然顺序,返回最大的元素。 
//类比int min(Collection coll)

int max(Collection coll, Comparator c)
//根据定制排序,返回最大元素,排序规则由Comparatator类控制。
//类比int min(Collection coll, Comparator c)

void fill(List list, Object obj)
//用指定的元素代替指定list中的所有元素

int frequency(Collection c, Object o)//统计元素出现次数

int indexOfSubList(List list, List target)
//统计target在list中第一次出现的索引,找不到则返回-1,
//类比int lastIndexOfSubList(List source, list target)

boolean replaceAll(List list, Object oldVal, Object newVal)
//用新元素替换旧元素
同步控制

Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。我们知道 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections 提供了多个静态方法可以把他们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。

synchronizedCollection(Collection<T>  c) 
//返回指定 collection 支持的同步(线程安全的)collection。
synchronizedList(List<T> list)
//返回指定列表支持的同步(线程安全的)List。
synchronizedMap(Map<K,V> m) 
//返回由指定映射支持的同步(线程安全的)Map。
synchronizedSet(Set<T> s) 
//返回指定 set 支持的同步(线程安全的)set。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值