Java笔记-----(2)Java容器

(1)Java容器概览

容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射
表。

(1.1)Collection

在这里插入图片描述

  1. Set
    TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)
    HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说
    使用 Iterator 遍历 HashSet 得到的结果是不确定的。
    LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
  2. List
    ArrayList:基于动态数组实现,支持随机访问。保证线程安全使用CopyOnWriteArrayList
    Vector:和 ArrayList 类似,但它是线程安全的。
    LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
    Stack
  3. Queue
    LinkedList:可以用它来实现双向队列。
    PriorityQueue:基于堆结构实现,可以用它来实现优先队列。

(1.2)Map

在这里插入图片描述

TreeMap:基于红黑树实现。
HashMap:基于哈希表实现。
HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并
且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁
LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序
Properties

(1.3)List,Set,Map的区别

  • List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
  • Set(注重独一无二的性质): 不允许重复的集合。不会有多个元素引用相同的对象。
  • Map(用Key来搜索的专家): 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

  • List是有序的并且元素是可以重复的
  • Set是无序(LinkedHashSet除外)的,并且元素是不可以重复的
    (此处的有序和无序是指放入顺序和取出顺序是否保持一致)

(2)HashMap和Hashtable的区别

  • HashMap没有考虑同步,是线程不安全的Hashtable使用了synchronized关键字,是线程安全的;
  • 因为线程安全的原因,HashMap 要比 HashTable 效率高一点
  • HashMap允许null作为Key,这样的键只有一个,但是允许一个或多个键所对应的值为null;Hashtable不允许null作为Key,Hashtable的value也不可以为null,会直接NullPointerException;
  • 初始容量大小和每次扩充容量大小的不同
    ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。
    HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍
    ②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,
    而 HashMap 会将其扩充为2的幂次方大小,也就是说 HashMap 总是使用2的幂作为哈希表的大小
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

(2.1)HashMap线程不安全说明

HashMap线程不安全主要是考虑到了多线程环境下进行(扩容)可能会出现HashMap(死循环)

Hashtable线程安全由于其内部实现在put和remove等方法上使用synchronized进行了同步,所以对(单个方法的使用是线程安全的)。但是对多个方法进行复合操作时,线程安全性无法保证。 比如一个线程在进行get操作,一个线程在进行remove操作,往往会导致下标越界等异常。


常见的错误理解:
有一个快速失败fast-fail机制,当对HashMap遍历的时候,调用了remove方法使其迭代器发生改变的时候会抛出一个异常ConcurrentModificationException。Hashtable因为在方法上做了synchronized处理,所以不会抛出异常。(其实也会)

错误分析:
错误的认为快速机制就是HashMap线程不安全的表现。并且坚定的认为Hashtable和Vector等线程安全的集合不会存在并发修改时候的快速失败,这是大错特错。概念和原理理解的不清晰导致掉入了面试官的陷阱里了,大家可以打开JDK源码,会发现Hashtable也会在迭代的时候抛出该异常,可能发生快速失败


HashMap遍历调用remove方法问题详解链接
基本上java集合类(包括list和map)在遍历时没用迭代器进行删除了都会报ConcurrentModificationException错误,这是一种fast-fail的机制,初衷是为了检测bug。
通俗一点来说,这种机制就是为了防止高并发的情况下,多个线程同时修改map或者list的元素导致的数据不一致,这是只要判断当前modCount != expectedModCount即可以知道有其他线程修改了集合。

替换机制:

  • 用迭代器的remove方法。
  • 用currentHashMap替代HashMap

(2.2)Java集合中的快速失败(fast-fail)机制

快速失败是Java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast

假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就可能会抛出 ConcurrentModificationException异常,从而产生fast-fail快速失败。

快速失败机制底层实现:
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedModCount值,是的话就返回遍历;否则抛出异常,终止遍历。

if(modCount != expectedModCount)
	throw new ConcurrentModificationException();

异常ConcurrentModificationException
当检测到一个并发的修改,就可能会抛出该异常,一些迭代器的实现会抛出该异常,以便可以快速失败。但是你不可以为了便捷而依赖该异常,而应该仅仅作为一个程序的侦测。

(2.3)map遍历时调用remove的方式

Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();

while(it.hasNext()){
	Map.Entry<Integer, String> entry = it.next();
	Integer key = entry.getKey();
	String value = entry.getValue()
	it.remove();
}

(3)HashMap

(3.1)HashMap底层实现结构

HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
在这里插入图片描述

在这里插入图片描述

① JDK1.8之前 数组+链表

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的hashCode() 方法,换句话说使用扰动函数之后可以减少碰撞

JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。

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

对比一下 JDK1.7的 HashMap 的 hash 方法源码:

	static int hash(int h) {
		// This function ensures that hashCodes that differ only by
		// constant multiples at each bit position have a bounded
		// number of collisions (approximately 8 at default load factor).
		
		h ^= (h >>> 20) ^ (h >>> 12);
		return h ^ (h >>> 7) ^ (h >>> 4);
	}

相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

在这里插入图片描述

② JDK1.8之后 数组+链表+红黑树

相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
在这里插入图片描述

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

(3.2)HashMap的初始容量,加载因子,扩容增量

HashMap的初始容量16,加载因子为0.75,扩容增量是原容量的1倍。如果HashMap的容量为16,一次扩容后容量为32。HashMap扩容是指元素个数(包括数组和链表+红黑树中)超过了16*0.75=12之后开始扩容。

创建时如果给定了容量初始值, HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证)
例如如果传10,容量大小为16

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的幂作为哈希表的大小:

    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

(3.3)HashMap的长度为什么是2的幂次方?

(length - 1) & hashKey
  • 我们将一个键值对插入HashMap中,通过Key的hash值length-1进行&运算,实现了当前Key的定位2的幂次方可以减少冲突(碰撞)的次数,提高HashMap查询效率
  • 如果length为2的幂次方,则length-1转化为二进制必定是11111……的形式,在与h的二进制与操作效率会非常的快,而且空间不浪费
  • 如果length不是2的幂次方,比如length为15,则length-1为14,对应的二进制为1110,在与h与操作,最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率

总结:

  • 2的N次幂有助于减少碰撞的几率,空间利用率比较大。
  • 加载因子,如果设置太小不利于空间利用,设置太大则会导致碰撞增多,降低了查询效率,所以设置了0.75。

更详细的说明:
hashmap为什么容量是2的n次方

(3.4)HasMap的存储和获取原理

  • 当调用put()方法传递键和值来存储时,先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象,也就是找到了该元素应该被存储的桶中(数组)。当两个键的hashCode值相同时,bucket位置发生了冲突,也就是发生了Hash冲突,这个时候,会在每一个bucket后边接上一个链表(JDK8及以后的版本中还会加上红黑树)来解决,将新存储的键值对放在表头(也就是bucket中)。
  • 当调用get()方法获取存储的值时,首先根据键的hashCode找到对应的bucket,然后根据equals方法来在链表和红黑树中找到对应的值。

在这里插入图片描述

  1. 判断数组是否为空,为空进行初始化;
  2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
  3. 查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
  4. 存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
  5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;
  6. 如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;
  7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

以上内容,原文转载自:
一个HashMap跟面试官扯了半个小时

(3.5)HashMap的扩容步骤

HashMap里面默认的负载因子大小为0.75,也就是说,当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。

但是,需要注意的是在多线程环境下,HashMap扩容可能会导致死循环。(HashMap线程不安全的表现)

参考文章:
HashMap底层实现原理 扩容机制
HashMap的扩容机制—resize()

小结:
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。
(5) 还没升级JDK1.8的,现在开始升级吧。HashMap的性能提升仅仅是JDK1.8的冰山一角。

(3.6)解决Hash冲突的方法

  • 拉链法 (HashMap使用的方法)
  • 线性探测再散列法
  • 二次探测再散列法
  • 伪随机探测再散列法

Hash算法解决冲突的四种方法

1, 开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入

2, 再哈希法
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间

3, 链地址法
链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来

4, 建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

(3.7)哪些类适合作为HashMap的键?

String和Interger这样的包装类很适合做为HashMap的键,因为他们是(final类型)的类,而且重写了equals和hashCode方法,避免了键值对改写,有效提高HashMap性能。

为了计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashCode的话,那么就不能从HashMap中找到你想要的对象。

(3.8)一致性Hash算法

一致性Hash算法

(3.9)HashMap 多线程操作导致死循环问题

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

点击查看详情:Java HashMap的死循环

(4)ConcurrentHashMap和Hashtable的区别

ConcurrentHashMap结合了HashMap和Hashtable二者的优势。

  • HashMap没有考虑同步,Hashtable考虑了同步的问题。但是Hashtable在每次同步执行时都要锁住整个结构。
  • ConcurrentHashMap锁的方式是稍微细粒度的,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁上当前需要用到的桶。
  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
  • 实现线程安全的方式(重要)

① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。

Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。


HashTable:
在这里插入图片描述

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

① JDK1.7 分段+可重入锁 Segment

JDK1.7的ConcurrentHashMap:
在这里插入图片描述

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

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

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

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

多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。


  • 该类包含两个静态内部类MapEntry和Segment,前者用来封装映射表的键值对,后者用来充当锁的角色。
    在这里插入图片描述
  • Segment是一种可重入的锁ReentrantLock(显示锁),每个Segment守护一个HashEntry数组里得元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment锁。
    在这里插入图片描述

② JDK1.8 CAS+synchronized 首节点

JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点,Node: 链表节点):
在这里插入图片描述

JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。

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

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


在实际的开发中,我们在单线程环境下可以使用HashMap,多线程环境下可以使用ConcurrentHashMap,至于Hashtable已经不被推荐使用了(也就是说Hashtable只存在于面试题目中了)。

(5)TreeMap

TreeMap底层使用红黑树实现,TreeMap中存储的键值对按照键来排序

  • 如果Key存入的是字符串等类型,那么会按照字典默认顺序排序
  • 如果传入的是自定义引用类型,比如说User,那么该对象必须实现Comparable接口,并且覆盖其compareTo方法;或者在创建TreeMap的时候,我们必须指定使用的比较器。如下所示:
// 方式一:定义该类的时候,就指定比较规则
class User implements Comparable{
    @Override
    public int compareTo(Object o) {
        // 在这里边定义其比较规则
        return 0;
    }
}

public static void main(String[] args) {
    // 方式二:创建TreeMap的时候,可以指定比较规则
    new TreeMap<User, Integer>(new Comparator<User>() {
        @Override
        public int compare(User o1, User o2) {
            // 在这里边定义其比较规则
            return 0;
        }
    });
}

Comparable接口的后缀是able大概表示可以的意思,也就是说一个类如果实现了这个接口,那么这个类就是可以比较的。类似的还有cloneable接口表示可以克隆的。而Comparator则是一个比较器,是创建TreeMap的时候传入,用来指定比较规则

Comparable接口和Comparator接口的区别:

  • Comparable实现比较简单,但是当需要重新定义比较规则的时候,必须修改源代码,即修改User类里边的compareTo方法
  • Comparator接口不需要修改源代码,只需要在创建TreeMap的时候重新传入一个具有指定规则的比较器即可。

(6)ArrayList和LinkedList的区别

  • ArrayList底层使用了动态数组(Object数组)实现,实质上是一个动态数组
  • LinkedList底层使用了双向链表实现,可当作堆栈、队列、双端队列使用
  • ArrayList在随机存取方面效率高于LinkedList
  • LinkedList在节点的增删方面效率高于ArrayList
  • ArrayList必须预留一定的空间,当空间不足的时候,会进行扩容操作
  • LinkedList的开销是必须存储节点的信息以及节点的指针信息
  • ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全

集合Vector是线程安全的ArrayList,但是已经被废弃,不推荐使用了。多线程环境下,我们可以使用CopyOnWriteArrayList替代ArrayList来保证线程安全。

(6.1)Vector

  • Vector的实现与 ArrayList 类似,但是使用了 synchronized 进行同步,因此开销就比 ArrayList 要大,访问速度更慢。
  • Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。

Vector的替代方案:

  • 可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
  • 也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
List<String> list = new CopyOnWriteArrayList<>();

(6.2)CopyOnWriteArrayList

读写分离

  • 写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
  • 写操作需要加锁,防止并发写入时导致写入数据丢失。
  • 写操作结束之后需要把原始数组指向新的复制数组

适用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

(7)HashSet和TreeSet的区别

  • HashSet 底层使用了Hash表实现
    保证元素唯一性的原理:判断元素的hashCode值是否相同。如果相同,还会继续判断元素的equals方法,是否为true
  • TreeSet 底层使用了红黑树来实现
    保证元素唯一性是通过Comparable或者Comparator接口实现,可以按照键来排序

解析:
其实,HashSet的底层实现还是HashMap,只不过其只使用了其中的Key,具体如下所示:

  • HashSet的add方法底层使用HashMap的put方法key=e,value=PRESENT构建成key-value键值对,当此e存在于HashMap的key中,则value将会覆盖原有value,但是key保持不变,所以如果将一个已经存在的e元素添加中HashSet中,新添加的元素是不会保存到HashMap中,所以这就满足了HashSet中元素不会重复的特性。
  • HashSet的contains方法使用HashMap的containsKey方法实现

(8)LinkedHashMap和LinkedHashSet

LinkedHashMap可以记录下元素的插入顺序和访问顺序,具体实现如下:

  • LinkedHashMap内部的Entry继承于HashMap.Node,这两个类都实现了Map.Entry<K,V>
  • LinkedHashMap的Entry不光有valuenext,还有beforeafter属性,这样通过一个双向链表保证了各个元素的插入顺序
  • 通过构造方法public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)accessOrder传入true可以实现LRU缓存算法(访问顺序)
  • LinkedHashSet 底层使用LinkedHashMap实现,两者的关系类似与HashMap和HashSet的关系,大家可以自行类比。

(8.1)LRU算法

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

由于LinkedHashMap可以记录下Map中元素的访问顺序,所以可以轻易的实现LRU算法。

  1. 只需要将构造方法的accessOrder传入true
  2. 并且重写removeEldestEntry方法即可。

具体实现参考如下:

import java.util.LinkedHashMap;
import java.util.Map;
 
public class LRU {
 
    private static int size = 5;
 
    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<String, String>(size, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
            	//map元素个数 > size时返回true, 移除最不常读取的元素
                return size() > size;
            }
        };
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");
        map.put("5", "5");
        System.out.println(map.toString());
 
        map.put("6", "6");
        System.out.println(map.toString());
        map.get("3");
        System.out.println(map.toString());
        map.put("7", "7");
        System.out.println(map.toString());
        map.get("5");
        System.out.println(map.toString());
    }
}

控制台输出:
{1=1, 2=2, 3=3, 4=4, 5=5}
{2=2, 3=3, 4=4, 5=5, 6=6}
{2=2, 4=4, 5=5, 6=6, 3=3}  //map.get("3");
{4=4, 5=5, 6=6, 3=3, 7=7}
{4=4, 6=6, 3=3, 7=7, 5=5}  //map.get("5");

这里引用一下网络上的

LRU的原理和实现

LRU原理和Redis实现——一个今日头条的面试题

(9)Iterator和ListIterator的区别

  • Iterator可以遍历list和set集合;ListIterator只能用来遍历list集合
  • Iterator前者只能前向遍历集合;ListIterator可以前向和后向遍历集合
  • ListIterator其实就是实现了Iterator,并且增加了一些新的功能。
    ArrayList<String> list =  new ArrayList<>();
    list.add("zhangsan");
    list.add("lisi");
    list.add("wangwu");

    // 创建 iterator 实现遍历集合
    Iterator<String> iterator = list.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }

    // 创建 listIterator 实现遍历集合
    ListIterator<String> listIterator = list.listIterator(3);
    while (listIterator.hasPrevious()) {
        System.out.println(listIterator.previousIndex() + ", " + listIterator.previous());
    }
        
	控制台输出为:
	zhangsan
	lisi
	wangwu
	// 开始反向遍历
	2, wangwu
	1, lisi
	0, zhangsan

(10)数组和集合List之间的转换

list集合转换成数组:

ArrayList<String> list =  new ArrayList<>();
list.add("zhangsan");
list.add("lisi");
list.add("yangwenqiang");
Object[] arr = list.toArray();
//最好这么写
String[] arr2 = list.toArray(new String[list.size()]);

List.toArray方法搞定了集合转换成数组,这里最好传入一个类型一样的数组,大小就是list.size()

  • 因为如果入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;
  • 如果数组元素个数大于实际所需,下标为list.size()及其之后的数组元素将被置为null,其它数组元素保持原值。

所以,建议该方法入参数组的大小与集合元素个数保持一致。

若是直接使用toArray无参方法,此方法返回值只能是Object[ ]类,若强转其它类型数组将出现ClassCastException错误。


数组转换为list集合:

方法一:
String[] arr2 = {"niuke", "alibaba"};
List<String> strsToList1 = Arrays.asList(arr2);

方法二:
List<String> strsToList2=new ArrayList<>();
Collections.addAll(strsToList2,arr2);

通过Arrays.asList方法搞定,转换之后不可以使用add/remove等修改集合的相关方法,因为该方法返回的其实是一个Arrays的内部私有的一个类ArrayList,该类继承于Abstractlist,并没有实现这些操作方法,调用将会直接抛出UnsupportOperationException异常。这种转换体现的是一种适配器模式,只是转换接口,本质上还是一个数组

(11)Collection和Collections有什么关系?

  • Collection是一个顶层集合接口,其子接口包括List和Set;
  • 而Collections是一个集合工具类,可以操作集合,比如说排序,二分查找,拷贝集合,寻找最大最小值等。

类似的还有Array和Arrays,Executor和Executors
总而言之:带s的大都是工具类。

(12)集合的类图结构

(12.1)Map

① HashMap

在这里插入图片描述

② ConcurrentHashMap

在这里插入图片描述

③ Hashtable

在这里插入图片描述

④ TreeMap

在这里插入图片描述

⑤ LinkedHashMap

在这里插入图片描述

(12.2)List

① ArrayList

在这里插入图片描述

② LinkedList

在这里插入图片描述

③ Vector

在这里插入图片描述

(12.3)Set

① HashSet

在这里插入图片描述

② TreeSet

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值