Java 集合

2023-05-16

1. Java 集合框架

1.1 Java 集合概述

1)Java 容器

集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器。

说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)

2)数组

数组在存储多个数据方面的特点:

  • 一旦初始化以后,其长度就确定了。
  • 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。比如:String[] arr;int[] arr1;Object[] arr2;

数组在存储多个数据方面的缺点:

  • 一旦初始化以后,其长度就不可修改。
  • 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  • 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
  • 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

1.2 Java 集合可分为 Collection 和 Map 两种体系

1)Collection 接口

单列集合,用来存储一个一个的对象

① List 接口:元素有序,可重复的集合---”动态”数组

面试:ArrayList 和数组的区别,调用 add 方法会调用哪些其他方法

ArrayList 是 Java 集合框架中的一个类,它是基于数组实现的动态数组。与数组相比,ArrayList 具有更灵活的长度、更方便的插入和删除操作等优点。

数组是 Java 语言中的一种数据类型,它具有固定长度,一旦被创建就不能改变长度。相比之下,ArrayList 可以根据需要动态增长或缩小。

调用 add 方法会调用以下方法:

  • 检查是否需要扩容,如果需要,则调用 grow 方法进行扩容。
  • 调用 rangeCheckForAdd 方法检查是否超出了数组的容量范围。
  • 将要添加的元素添加到数组的末尾。

因为ArrayList是基于数组实现的,所以它的底层实现还涉及到一些其他方法,比如移动元素、删除元素等。但是,调用 add 方法时主要涉及到上述三个方法。

如,ArrayList(主要的实现类)、LinkedList(对于频繁的插入、删除操作)、Vector(古老的实现类、线程安全的)

② Set 接口:元素无序、不可重复的集合 ---类似高中的“集合”

如,HashSet、LinkedHashSet、TreeSet

③ Collection 接口继承树:

2)Map 接口

双列集合,用来存储一对(key - value)一对的数据。--类似于高中的“函数” y = f(x)   (x1,y1) (x2,y2)。

如:HashMap、LinkedHashMap、TreeMap、Hashtable(子类:Properties)。

Map继承树:

3)相关面试题

① hashmap 和 hashtable的区别?

HashMap 和 Hashtable 都是 Java 集合框架中的哈希表实现类,它们之间的主要区别如下:

  • 线程安全性:Hashtable 是线程安全的,而 HashMap 是非线程安全的。在多线程环境下,如果需要使用哈希表,通常应该使用 ConcurrentHashMap,而不是 Hashtable。
  • null 值Hashtable 不允许 null 键或 null 值,而 HashMap 允许 null 键和 null 值。如果试图在 Hashtable 中存储 null 键或 null 值,将抛出 NullPointerException 异常。
  • 继承关系:Hashtable 是 Dictionary 类的子类,而 HashMap 没有继承任何类。Dictionary 是一个已经过时的类,通常不应该在新代码中使用。
  • 迭代器:Hashtable 的迭代器是通过 Enumeration 实现的,而 HashMap 的迭代器是通过 Iterator 实现的。Enumeration 是一个已经过时的接口,通常不应该在新代码中使用。
  • 性能:由于Hashtable 是线程安全的,因此它的性能通常比 HashMap 差,尤其是在高并发的环境下。但是,在单线程环境下,Hashtable 的性能与 HashMap 相当。

综上所述,HashMap 通常比 Hashtable 更适合在单线程环境下使用,而 Hashtable 则适合在多线程环境下使用。

② hashMap 源码。

③ 设计一个线程安全的 HashMap。

设计一个线程安全的 HashMap 可以有以下几种方案:

  • 使用 ConcurrentHashMap:ConcurrentHashMap 是一个线程安全的 HashMap,它采用了分段锁(Segment)的方式来保证线程安全。ConcurrentHashMap 将整个 Map 分成多个 Segment,每个 Segment 独立加锁,不同的线程可以同时对不同的 Segment 进行操作。这种方式可以有效地减小锁的粒度,提高并发性能。
  • 使用 synchronized 关键字:可以在 HashMap 的基本操作(如 put、get、remove)上添加 synchronized 关键字来保证线程安全,但是这种方式会降低并发性能,因为每个操作都要获取锁,如果同时有大量的线程在操作,会导致线程的竞争激烈,性能下降。
  • 使用读写锁:可以使用读写锁(ReentrantReadWriteLock)来保证 HashMap 的读操作和写操作的线程安全。读操作可以共享读锁,写操作需要独占写锁。这种方式可以提高读操作的并发性能,但是写操作的并发性能仍然受到限制。
  • 使用 volatile 和 CAS:可以使用 volatile 关键字和 CAS(Compare-and-Swap)操作来保证 HashMap 的线程安全。在 put、get、remove 等操作中,可以采用自旋锁的方式,使用 CAS 操作来更新 HashMap 的数据结构,保证线程安全。这种方式可以提高并发性能,但是实现上比较复杂。

综上所述,使用 ConcurrentHashMap 是一个比较简单、高效的线程安全的 HashMap 的实现方式。如果需要更细粒度的控制,可以考虑使用其他方式,根据实际情况进行选择。

④ HashMap 在大量哈希冲突该怎么处理?

如果 HashMap 在存储大量数据时发生了哈希冲突,即不同的 key 映射到了同一个桶(bucket)中,这时候就需要使用链表或者红黑树来解决这个冲突。

在 JDK 1.8 及之前的版本中,HashMap 使用的是拉链法解决哈希冲突。具体来说,当发生哈希冲突时,HashMap 会将冲突的元素以链表的形式存储在同一个桶中,同时在链表头部存储最新的元素。这样,在查询元素时,HashMap 会先根据 key 计算出它应该被放到哪个桶中,然后在这个桶中遍历链表,查找对应的元素。当然,如果链表过长,这样的查找效率就会变得很低。

从 JDK 1.8 开始,HashMap 在桶中的链表长度达到一定阈值(默认为 8)时,会将链表转化为红黑树,以提高查询效率。红黑树是一种自平衡的二叉搜索树,它的查询效率比链表更高,但是在插入和删除元素时会比链表慢一些。

此外,如果 HashMap 存储的数据量太大,会导致每个桶中存储的元素过多,从而降低查询效率。这时候可以考虑调整 HashMap 的容量,以减少哈希冲突。具体来说,可以通过调用 HashMap 的 rehash() 方法重新计算每个元素的哈希值,并重新分配桶的位置,以达到更好的散列效果。

⑤ hashMap 底层结构;

HashMap 的底层数据结构是一个数组(Array)和链表(LinkedList)或红黑树(Red-Black Tree)的组合。具体来说,HashMap 将键值对存储在一个 Entry 数组中,每个 Entry 对象包含 key、value 和一个指向下一个 Entry 的指针。当多个键发生哈希冲突时,它们会被存储在同一个数组位置,形成一个链表。当链表长度超过一定阈值(默认为 8)时,链表会自动转化为红黑树,以提高查询效率。 HashMap 的数组长度为 2 的幂次方,这样可以保证在计算数组位置时,只需要进行位运算(&)而不需要进行模运算(%),提高计算效率。此外,HashMap 还具有自动扩容和重新分布的功能,当数组中元素的数量超过了容量的 0.75 倍时,HashMap 会自动扩容,将数组长度翻倍,并将元素重新分布到新的数组位置上。这样可以避免哈希冲突,提高查询效率。

综上所述,HashMap 的底层结构是一个数组和链表或红黑树的组合,通过哈希算法将元素存储在数组中,通过链表或红黑树解决哈希冲突,实现高效的键值对存储和查询。

⑥ ConcurrentHashMap 线程安全原理。

ConcurrentHashMap 是 JDK 提供的一个线程安全的哈希表实现,它是通过分段锁(Segment)来保证线程安全的,而不是像传统的 Hashtable 或者 synchronizedMap 一样直接使用一个锁来保护整个哈希表。

具体来说,ConcurrentHashMap 内部维护了一个 Segment 数组,每个 Segment 就是一个小的哈希表,它们之间是互相独立的,也就是说每个 Segment 中的操作都是线程安全的。每次对 ConcurrentHashMap 的操作都会先根据 key 计算出它应该被放到哪个 Segment 中,然后再对这个 Segment 加锁进行操作。这样就避免了对整个哈希表加锁,提高了并发性能。

另外,ConcurrentHashMap 还使用了一种叫做 CAS(Compare And Swap)的机制来保证数据的一致性。CAS 是一种无锁算法,它的基本思想是先读取内存中的值,然后在一个原子操作中比较这个值和期望的值是否相等,如果相等就更新为新的值,否则不做操作。这样可以避免使用锁,提高并发性能。

综上所述,ConcurrentHashMap 通过分段锁和 CAS 机制来保证线程安全,提高了并发性能。

⑦ HashMap 初始大小,红黑树何时退化成链表;

HashMap 的初始大小是 16,它是一个比较合理的值,因为它是 2 的幂次方,便于进行位运算,同时也不会浪费太多内存空间。在实际使用中,如果需要存储更多的数据,可以通过指定负载因子(load factor)来增加 HashMap 的容量,避免哈希冲突。

红黑树在什么情况下会退化成链表,这取决于 HashMap 中对应的桶中存储的元素个数以及红黑树的结构。具体来说,当一个桶中存储的元素个数小于等于 8 时,HashMap 会使用链表来存储这些元素。当桶中存储的元素个数大于 8 时,HashMap 会将链表转化为红黑树,以提高查询效率。当然,在某些情况下,红黑树可能会退化成链表,主要有两种情况:

  • 插入元素:当往红黑树中插入元素时,如果插入的位置已经存在了一个相同的元素,且这个元素的 hashCode 和插入元素的 hashCode 不同,此时红黑树就会退化成链表。因为插入的元素无法在红黑树中找到对应的位置,只能插入到链表的尾部。
  • 删除元素:当从红黑树中删除元素时,如果删除后红黑树的结构发生了变化,可能会导致红黑树退化成链表。因为红黑树的结构变化会破坏红黑树的平衡性,使得查询效率降低,此时 HashMap 就会将红黑树恢复成链表结构,以保证查询效率。

综上所述,HashMap 中的红黑树退化成链表是由于插入或删除元素导致的,并且只有在特定情况下才会发生。

2. Collection 接口

Collection 接口是List、Set 和Queue接ロ的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List和Queue集合。

JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如: Set和List)实现 。

在Java5之前,Java集合会丢失容器中所有对象的数据类型,把所有对象都当成Object类型处理;从Java5増加了泛型以后,Java集合可以记住容器中对象的数据类型

2.1 Collection接口方法

① 添加 

add(Object obj) 

addAll(Collection coll)

② 获取有效元素的个数 

 int size()

③ 清空集合 

void clear()

④ 是否是空集合

 boolean isEmpty()

⑤ 是否包含某个元素

boolean contains(Object obj)  // 是通过元素的equals方法来判断是否 是同一个对象

boolean containsAll(Collection c)  // 也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。

向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()。

例:

⑥ 删除 

boolean remove(Object obj)  // 通过元素的equals方法判断是否是 要删除的那个元素。只会删除找到的第一个元素
boolean removeAll(Collection coll)  // 取当前集合的差集

⑦ 取两个集合的交集 

 boolean retainAll(Collection c)  // 把交集的结果存在当前集合中,不影响c

⑧ 集合是否相等  

 boolean equals(Object obj)

⑨ 转成对象数组   

Object[] toArray()

⑩ 获取集合对象的哈希值   

hashCode()

⑪ 遍历   

iterator()  // 返回迭代器对象,用于集合遍历

2.2 使用 Iterator 接口遍历集合元素

Iterator 对象称为迭代器(设计模式的一种),主要用于遍历Collection集合中的元素。

所有实现了Collection接口的集合类都有一个iterator()方法, 用以返回一个实现了Iterator接口的对象。

Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建lterator对象,则必须有一个被迭代的集合

例:

迭代器的执行原理

① 内部的方法:hasNext() 和  next()

② 集合对象每次调用 iterator() 方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

③ 内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用 remove()。

如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。

Enumeration

Enumeration接口是Iterator迭代器的“古老版本”

2.4 使用foreach循环遍历集合元素

Java 5 提供了foreach循环迭代访问Collection

例子:

2.5 Collection 子接口

Collection 子接口:

  • List 接口
  • set 接口

1)List 接口

List 集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

List 除了从 Collection 集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法

Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(…)  返回值是一个固定长度的 List 集合

Java 中数组用来存储数据的局限性:(长度、类型不可变)

  • 数组初始化以后,长度就不可变了,不便于拓展。
  • 数组中提供的属性和方法少,不便于进行添加、删除、插入等操作,且效率不高。同时无法直接获取存储元素的个数。
  • 数组存储的数据是有序的、可以重复的。------->存储数据的特点单一。

2)JDK API中 List 接口常用的实现类

面试题:ArrayList、LinkedList、Vector三者的异同?

a)同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据

b)不同

  • ArrayList:作为 List 接口的主要实现类;线程不安全的,效率高;底层使用 Object[] elementData 存储
  • LinkedList:对于频繁的插入、删除操作,使用此类效率比 ArrayList 高;底层使用双向链表存储
  • Vector:作为 List 接口的古老实现类;线程安全的,效率低;底层使用 Object[] elementData 存储

ArrayList、LinkedList、Vector 都是 Java 中的集合类,它们有以下异同点:

  1. ArrayList 和 Vector 都是基于数组实现的,而 LinkedList 是基于链表实现的。
  2. ArrayList 和 Vector 的区别在于线程安全性:ArrayList 不是线程安全的,而 Vector 是线程安全的。这是因为 Vector 在每个方法上都进行了同步处理,而 ArrayList 没有。LinkedList 也不是线程安全的。
  3. ArrayList 和 Vector 的区别在于扩容机制:ArrayList 每次扩容时将当前数组长度增加一半,而 Vector 每次扩容时将当前数组长度翻倍。LinkedList 不需要扩容,因为它是基于链表实现的。
  4. ArrayList 和 Vector 的随机访问效率比较高,因为可以根据索引直接访问元素,而 LinkedList 的随机访问效率比较低,因为需要从头开始遍历链表查找元素。
  5. LinkedList 的插入和删除效率比 ArrayList 和 Vector 高,因为只需要修改指针,不需要移动元素。而 ArrayList 和 Vector 的插入和删除效率比较低,因为需要移动元素。
  6. LinkedList 比 ArrayList 和 Vector 的空间效率高,因为不需要预先分配数组大小,可以根据实际需要动态分配空间。 综上所述,ArrayList、LinkedList、Vector 各有特点,应根据实际需求选择合适的集合类。如果需要高效的随机访问和遍历,可以选择 ArrayList 或 Vector;如果需要高效的插入和删除操作,可以选择 LinkedList。同时,如果需要线程安全的集合操作,可以选择 Vector。

① ArrayList

ArrayList是List接口的典型实现类。本质上,ArrayList是对象引用的一个变长数组。

ArrayList 的 JDK1.8 之前与之后的实现区别?

  • JDK1.7:ArrayList 像饿汉式,直接创建一个初始容量为10的数组
  • JDK1.8:ArrayList 像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为 10 的数组 

ArrayList 在 JDK 1.8 之前和之后的实现区别主要体现在如何处理扩容时的数组复制

在 JDK 1.8 之前,当 ArrayList 的元素数量达到容量时,需要进行扩容。扩容时,会创建一个新的数组,并将旧数组中的元素复制到新数组中。在 JDK 1.6 和 1.7 中,ArrayList 扩容时的复制方式是使用 System.arraycopy() 方法,这种方式效率较高。但是,在 JDK 1.8 中,ArrayList 扩容时的复制方式发生了改变。JDK 1.8 中的实现采用了一种新的复制方法,称为「分裂式迁移」(Splitting),它将原数组分为两个部分,分别复制到新数组的两个部分中。这种方式相对于 System.arraycopy() 方法,可以更好地利用 CPU 缓存,提高效率。

除了扩容时的数组复制方式发生了改变,JDK 1.8 还对 ArrayList 进行了性能优化,提高了其在遍历和删除元素时的效率。具体来说,JDK 1.8 在 ArrayList 中新增了一个成员变量 modCount,用于记录 ArrayList 的结构修改次数。在遍历和删除元素时,会先检查 modCount 是否发生变化,如果发生变化,则抛出 ConcurrentModificationException 异常,防止出现线程安全问题。同时,在删除元素时,JDK 1.8 新增了一个 removeIf() 方法,可以根据指定条件删除元素,提高了删除元素的效率。

综上所述,JDK 1.8 对 ArrayList 进行了一些性能优化和改进,提高了其在遍历和删除元素时的效率,并采用了一种新的扩容方式,提高了数组复制的效率。

ArrayList 源码分析:

HashMap初始化容量为16,扩容为2倍。   HashTable初始化为11,扩容为2n+1。

② LinkedList

LinkedList:双向链表,内部没有声明数组,而是定义了 Node 类型的 first 和 last, 用于记录首末元素。同时,定义内部类 Node,作为 LinkedList 中保存数据的基本结构。Node 除了保存数据,还定义了两个变量: 

  • prev变量记录前一个元素的位置 
  • next变量记录下一个元素的位置

对于频繁的插入或删除元素的操作,建议使用 LinkedList 类,效率较高。

新增方法:

void addFirst(Object obj) 

void addLast(Object obj) 

Object getFirst() 

Object getLast() 

Object removeFirst() 

Object removeLast()

源码分析:

③ Vector

Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。

在各种list中, 最好把ArrayList作为缺省选择。当插入、删除频繁时,使用 LinkedList; Vector 总是比 ArrayList 慢,所以尽量避免使用。

新增方法:

void addElement(Object obj) 

void insertElementAt(Object obj,int index) 

void setElementAt(Object obj,int index) 

void removeElement(Object obj) 

void removeAllElements()

Vector的源码分析:

jdk7 和 jdk8 中通过 Vector() 构造器创建对象时,底层都创建了长度为 10 的数组。在扩容方面,默认扩容为原来的数组长度的 2 倍。

3)List 接口中的常用方法

void add(int index, Object ele):  //在index位置插入ele元素
boolean addAll(int index, Collection eles): //从index位置开始将eles中的所有元素添加进来
Object get(int index): //获取指定index位置的元素    

int indexOf(Object obj):  //返回obj在集合中首次出现的位置
int lastIndexOf(Object obj):  //返回obj在当前集合中末次出现的位置
Object remove(int index):  //移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):  //设置指定index位置的元素为ele

List subList(int fromIndex, int toIndex):  //返回从fromIndex到toIndex位置的子集合

List 遍历:

一个面试题:

4)Set 接口

Set 接口是 Collection 的子接口,set 接口没有提供额外的方法。Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set集合中,则添加操作失败。

Set 判断两个对象是否相同不是使用 == 运算符,而是根据equals方法。

5)set 实现类

set接口实现类的对比:

set无序性与不可重复性的理解:

① HashSet

HashSet 是Set接口的典型实现,大多数时候使用Set集合时都使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能。 

HashSet具有以下特点:

  • 不能保证元素的排列顺序
  • HashSet不是线程安全的
  • 集合元素可以是null

HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相等。

对于存放在 Set 容器中的对象,对应的类一定要重写 equals() 和 hashCode(Object obj) 方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

HashSet中元素的添加过程:

当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法 来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象 在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在 数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布, 该散列函数设计的越好) 

要求:

以Eclipse/IDEA为例,在自定义类中可以调用工具自动重写equals和hashCode。 问题:为什么用Eclipse/IDEA复写hashCode方法,有31这个数字?

选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大,所谓的 “冲突”就越少,查找起来效率也会提高。(减少冲突)

并且31只占用5bits,相乘造成数据溢出的概率较小。

31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。(提高算法效 率)

31是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结 果只能被素数本身和被乘数还有1来整除!(减少冲突)

LinkedHashSet

LinkedHashSet是HashSet的子类。LinkedHashSet根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能。

LinkedHashSet不允许集合元素重复。

优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet。

LinkedHashSet的使用:LinkedHashSet 作为 HashSet 的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据。

② TreeSet

TreeSet是SortedSet接口的实现类,TreeSet 可以确保集合元素处于排序状态。

TreeSet 底层使用红黑树结构存储数据 。

特点:有序,查询速度比List快。

http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html

向TreeSet中添加的数据,要求是相同类的对象。

TreeSet两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。

a)自然排序(实现Comparable接口)

自然排序: TreeSet 会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列。

如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口。

实现Comparable的类必须实现compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj)方法的返回值来比较大小。

Comparable 的典型实现:

  • BigDecimal、BigInteger 以及所有的数值型对应的包装类:按它们对应的数值大小进行比较
  • Character: 按字符的unicode值 来进行比较
  • Boolean:true对应的包装类实例大于false对应的包装类实例
  • String: 按字符串中字符的unicode值进行比较
  • Date、Time:后边的时间、日期比前面的时间、日期大

向TreeSet中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。

当需要把一个对象放入TreeSet中,重写该对象对应的equal() 方法时,应保证该方法与compareTo(Objectobj)方法有一致的结果: 如果两个对象通过equals()方法比较返回true,则通过compareTo(Object obj)方法比较应返回0

自然排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals()。

例:

b)定制排序(Comparator)

TreeSet的自然排序是根据集合元素的大小,进行元素升序排列。如果需要定制排序,比如降序排列,可通过Comparator接口的帮助。需要重写compare(To1,T o2)方法。

利用int compare(T o1,T o2)方法, 比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2; 如果返回0,表示相等;返回负整数,表示o1小于o2。

要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。

此时,仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。使用定制排序判断两个元素相等的标准是:通过Comparator比较两 个元素返回了0。

定制排序中,比较两个对象是否相同的标准为:compare()返回0.不再是equals()。

例:

 面试题:

2.6 ListIterator接口(了解)

List 额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。

Iterator和ListIterator主要区别:

  • ListIterator和Iterator都 有hasNext()和next()方法,可以实现顺序向后遍历。但是ListIterator有 hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
  • Listlterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
  • Listlterator有add()方法,可以向List中插入对 象,而Iterator不能。
  • 都可实现删除对象,但是Listlterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能修改。因为Listlterator的这些功能,可以实现对LinkedList等List数据结构的操作。

3. Map 接口

3.1 Map 实现类

1)HashMap

HashMap 是 Map 接口使用频率最高的实现类。允许有 null 键和 null 值,与 HashSet 一样,不保证映射的顺序。一个 key-value 构成一个 entry。

HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等;判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

HashMap的存储结构:

① JDK1.8之前

HashMap 的内部存储结构其实是数组和链表的结合。当实例化一个 HashMap 时, 系统会创建一个长度为 Capacity 的 Entry 数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个 bucket 都有自己的索引,系统可以根据索引快速的查找 bucket 中的元素。

每个 bucket 中存储一个元素,即一个 Entry 对象,但每一个 Entry 对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个 Entry 链。 而且新添加的元素作为链表的 head。

添加元素的过程:

向 HashMap 中添加 entry1(key,value),需要首先计算 entry1 中 key 的哈希值(根据 key 所在类的 hashCode() 计算得到),此哈希值经过处理以后,得到在底层 Entry[] 数组中要存储的位置 i。如果位置 i 上没有元素,则 entry1 直接添加成功

如果位置 i 上已经存在 entry2(或还有链表存在的 entry3,entry4),则需要通过循环的方法,依次比较 entry1 中 key 和其他的 entry。如果彼此 hash 值不同,则直接添加成功

如果 hash 值相同,继续比较二者是否 equals。如果返回值为 true,则使用 entry1 的 value 去替换 equals 为 true的 entry 的 value。

如果遍历一遍以后,发现所有的 equals 返回都为 false,则 entry1 仍可添加成功。entry1 指向原有的 entry 元素。

HashMap的扩容:

当 HashMap 中的元素越来越多的时候,hash 冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。

那么HashMap什么时候进行扩容呢?

当 HashMap 中的元素个数超过数组大小(数组总大小 length,不是数组中个数 size) * loadFactor 时 , 就 会 进 行 数 组 扩 容 , loadFactor 的默认值(DEFAULT_LOAD_FACTOR) 为 0.75,这是一个折中的取值。也就是说,默认情况 下,数组大小 (DEFAULT_INITIAL_CAPACITY) 为 16,那么当 HashMap 中元素个数超过 16 * 0.75 = 12(这个值就是代码中的 threshold 值,也叫做临界值)的时候,就把数组的大小扩展为 2*16 = 32,即扩大一倍,然后重新计算每个元素在数组中的位置, 而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数, 那么预设元素的个数能够有效的提高 HashMap 的性能。

在 JDK8 中,HashMap 的内部存储结构其实是数组+链表+树的结合。当实例化一个 HashMap 时,会初始化 initialCapacity 和 loadFactor,在 put 第一对映射关系时,系统会创建一个长度为 initialCapacity 的 Node 数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为 “桶”(bucket),每个 bucket 都有自己的索引,系统可以根据索引快速的查找 bucket 中的元素。

每个bucket中存储一个元素,即一个Node对象,但每一个Node对象可以带一个引用变量next,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Node链。也可能是一个一个TreeNode对象,每一个TreeNode对象可以有两个叶子结点left和right,因此,在一个桶中,就有可能生成一个 TreeNode树。而新添加的元素作为链表的last,或树的叶子结点。

JDK8 的 HashMap 什么时候树形化呢?

当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。

在 JDK8 的 HashMap 中,当数组的某一个索引位置上的元素以链表形式存在的数据个数 >8 即为9,且当前数组的容量 >= 64(否则优先扩容)时,此时此索引位置上的所数据由链表改为使用红黑树存储。

关于映射关系的key是否可以修改?answer:不要修改

映射关系存储到 HashMap 中会存储 key 的 hash 值,这样就不用在每次查找时重新计算每一个Entry或Node(TreeNode)的 hash 值了,因此如果已经 put 到 Map 中的映射关系,再修改 key 的属性,而这个属性又参与 hashcode 值的计算,那么会导致匹配不上。

② JDK1.8之后

JDK1.8相较于之前的变化

  • HashMap map = new HashMap();  //默认情况下,先不创建长度为16的数组 
  • 当首次调用 map.put() 时,再创建长度为 16 的数组
  • 数组为 Node 类型,在 jdk7 中称为 Entry 类型
  • 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
  • 当数组指定索引位置的链表长度 >8 时,且 map 中的数组的长度 >64 时,此索引位置 上的所有 key-value 对使用红黑树进行存储。

HashMap的底层实现原理?以jdk7为例说明:

HashMap 中重要的常量:

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
  • DEFAULT_LOAD_FACTOR :HashMap的默认加载因子:0.75
  • threshold:扩容的临界值,=容量*填充因子:16*0.75=>12
  • TREEIFY_THRESHOLD :Bucket中链表长度大于该默认值,转化为红黑树:8
  • MIN_TREEIFY_CAPACITY :桶中的Node被树化时最小的hash表容量:64

面试题

谈谈你对 HashMap 中 put/get 方法的认识?

 HashMap 的扩容机制?默认大小是多少?

什么是负载因子( 或填充比)?负载因子值的大小,对HashMap有什么影响?

负载因子(Load Factor)是 HashMap 中的一个参数,它表示哈希表中元素的数量与容量的比值。具体来说,负载因子 = 元素数量 / 容量。例如,如果一个 HashMap 容量为 16,其中有 10 个元素,那么负载因子就是 10 / 16 = 0.625。

负载因子的作用是衡量 HashMap 中元素的密集程度。当负载因子超过一定阈值(一般为 0.75),意味着哈希表中的元素数量占用了大部分容量,此时就需要对哈希表进行扩容,以避免哈希冲突的发生,提高效率。因此,负载因子越大,哈希表扩容的次数越多,性能就会下降。相反,如果负载因子过小,哈希表的容量就会浪费,也会影响哈希表的性能。

在 Java 的 HashMap 中,负载因子是一个可调参数,可以通过构造方法或 setLoadFactor() 方法设置。默认的负载因子为 0.75,这是一个比较经验性的值,可以满足大多数场景的需求。如果需要更高的性能,可以将负载因子设置为更小的值,以减少扩容的次数;如果需要更少的内存占用,可以将负载因子设置为更大的值,以增加哈希表的容量。但需要注意的是,负载因子不宜设置过大或过小,否则都会影响哈希表的性能。

什么是吞吐临界值(或阈值、threshold)?

LinkedHashMap

LinkedHashMap 是 HashMap 的子类。与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 key-Value 对的插入顺序一致

LinkedHashMap 的底层实现原理(了解):

2)TreeMap

TreeMap 存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。

TreeMap的Key的排序:

自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。

定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。此时不需要Map的Key实现Comparable接口。

TreeMap 判断两个 key 相等的标准:两个 key 通过 compareTo() 方法或者 compare() 方法返回 0。

若使用自定义类作为TreeMap的key,所属类需要重写equals()和hashCode()方法,且equals()方法返回true时,compareTo()方法返回0。

TreeMap的两种添加方式:

3)Hashtable

Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap, Hashtable是线程安全的。 

Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。 

与HashMap不同,Hashtable不允许使用null作为key和value

与HashMap一样,Hashtable也不能保证其中key-Value对的顺序

Hashtable判断两个key相等、两个value相等的标准,与HashMap一致

Properties:

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件 
  • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型
  • 存取数据时,建议使用setProperty(String key,String value)方法和 getProperty(String key)方法

3.2 Map 中定义的方法

1)添加、删除、修改操作

Object put(Object key,Object value)  // 将指定key-value添加到(或修改)当前map对象中

void putAll(Map m)  // 将m中的所有key-value对存放到当前map中

Object remove(Object key)   // 移除指定key的key-value对,并返回value

 void clear()   // 清空当前map中的所有数据

2)元素查询的操作

Object get(Object key) // 获取指定key对应的value

boolean containsKey(Object key) // 是否包含指定的key

boolean containsValue(Object value) // 是否包含指定的value

int size()  // 返回map中key-value对的个数

boolean isEmpty()  // 判断当前map是否为空

boolean equals(Object obj) // 判断当前map和参数对象obj是否相等

3)元视图操作的方法

Set keySet()  // 返回所有key构成的Set集合

Collection values()  // 返回所有value构成的Collection集合

Set entrySet()  // 返回所有key-value对构成的Set集合

4. Collections 工具类

Collections:操作Collection、Map的工具类。

4.1 静态方法

排序操作:

reverse(List) // 反转 List 中元素的顺序
shuffle(List) // 对 List 集合元素进行随机排序
sort(List)  // 根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator)  // 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int)  // 将指定 list 集合中的 i 处元素和 j 处元素进行交换

查找、替换:

Object max(Collection)  // 根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection, Comparator)  // 根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection, Comparator)
int frequency(Collection, Object)  // 返回指定集合中指定元素的出现次数
void copy(List dest,List src)  // 将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal)  // 使用新值替换 List 对象的所有旧值

Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

面试题:Collection 和 Collections的区别?

Collection是Java集合框架中的一个接口,它是所有集合类型的根接口,定义了集合对象的基本操作,如添加、删除、遍历、判断元素是否存在等。

Collections是Java集合框架中的一个工具类,它提供了一系列静态方法,用于对集合对象进行排序、查找、替换、反转等操作。
简单来说,Collection是一个接口,描述了集合的基本操作,而Collections是一个工具类,提供了对集合进行操作的静态方法。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java 集合 的相关文章

随机推荐

  • 字符串比较大小

    1 规则 1 如果 字符串1的第n位的ASCII码值 等于 字符串2的第n位的ASCII码值 则 继续比较下一位 2 如果 字符串1的第n位的ASCII码值 大于 字符串2的第n位的ASCII码值 则 输出结果 1 表示字符串1 gt 字符
  • 将本地jar添加到Maven本地仓库

    在Maven项目中 xff0c 如果需要引入自己的jar包 xff0c 需要将jar添加到本地Maven仓库 方法一 xff1a 假设将包htmlparser jar放入了项目下的lib目录中 xff1a gt project lib ht
  • UART的奇偶校验

    1 奇校验 当数据位中 1 的个数为奇数时 xff0c 校验位为 0 xff0c 否则为 1 2 偶校验 当数据位中 1 的个数为偶数时 xff0c 校验位为 0 xff0c 否则为 1
  • windows 关闭占用端口的进程

    1 netstat ano findstr yourPortNumber 2 taskkill PID typeyourPIDhere F
  • Linux TCP server/client例程

    1 服务器端 span class token macro property span class token directive hash span span class token directive keyword include s
  • Nvidia Jetson 平台 DeepStream-6.0.1 部署 YoloV5-6.0 实现目标检测

    项目介绍 xff1a 在 Jetson 平台上利用 DeepStream 处理多路视频源 xff0c 并实现自己训练的 YoloV5 模型的部署 文章目录 前言1 YoloV5 模型训练自己的数据集1 1 建立自己的数据集1 1 1 开始之
  • 软路由保姆级入门教程 一篇看懂软路由

    前言 amp nbsp amp nbsp 玩张大妈也一年多了 xff0c 软路由改装 刷机文章写了不少 xff0c 很早就打算写篇软路由入门文章 xff0c 但是一直没落实 xff0c 原因有二 xff1a 圈子里大佬众多 xff0c 基础
  • CMake入门02-CMake中的静态库

    CMake中的静态库 静态库 文件树 CMakeLists txt include static Hello h src Hello cpp main cpp 1 1 Hello h 声明了Hello类 xff0c Hello的方法是pri
  • C++:struct与class的区别

    xff08 1 xff09 C语言中struct与class的区别 xff1a a struct只作为一种复杂数据类型定义的结构体 xff0c 不能用于面向对象编程 xff1b b C语言没有class关键字 xff08 2 xff09 C
  • Rplidar A1使用并改为ROS中3D点云输出(PointCloud2)

    这里写自定义目录标题 Rplidar A1使用并改为ROS中3D点云输出Rplidar安装测试修改后完整代码测试 Rplidar A1使用并改为ROS中3D点云输出 3D激光雷达价格不菲 xff0c 在研究过程中 xff0c 可以尝试将2D
  • Spring/SpringBoot常用注解总结!

    以下内容皆为转载 xff0c 转载地址 xff1a 接近8000字的Spring SpringBoot常用注解总结 xff01 安排 xff01 1 64 SpringBootApplication 这里先单独拎出 64 SpringBoo
  • MobaXterm 复制粘贴快捷键

    1 复制 不用设置 xff0c MobaXTerm 里面选取内容就已经复制了 xff0c 如图 xff0c 白色的内容就已经成功复制了哈哈哈哈 xff0c 真方便 注 xff1a 如果不行 xff0c 看看是否是这里没有勾上 xff08 在
  • Apollo配置中心

    1 Apollo 介绍 Apollo xff08 阿波罗 xff09 是携程框架部门研发的分布式配置中心 xff0c 能够集中化管理应用不同环境 不同集群的配置 xff0c 配置修改后能够实时推送到应用端 xff0c 并且具备规范的权限 流
  • Linux 命令

    1 Linux Shell 获取本地当前时间或前一分钟时间 1 1 获取前一分钟时间 xff1a 1 xff09 默认格式 date d 34 1 minute ago 34 date d 34 1 minute ago 34 Thu Oc
  • powershell 遍历数据库表导出为csv

    Write Output 0 server 61 34 34 database 61 34 dbname 34 tablequery 61 34 SELECT schemas name as schemaName tables name a
  • 回顾一:lili-om编译及运行问题(process has died、Leaf size is too small 等)[Livox Horizon激光雷达]

    度过考试周和作业周 xff0c 终于有时间可以搞自己的东西啦 xff0c 半个月前学习的东西忘得差不多了 xff0c 现在凭借着仅剩的记忆回顾一下 x1f604 末尾有小惊喜 xff0c 嘿嘿 xff01 1 Ceres Solver1 1
  • EXCEl 时间戳转换为日期格式

    1 EXCEl 时间戳转换为日期格式 公式为 xff1a 61 TEXT A2 1000 43 8 3600 86400 43 70 365 43 19 34 yyyy mm dd hh mm ss 34 具体操作如下 xff1a A2 1
  • 代码分析工具 - SonarQube

    1 常见代码质量分析工具 SonarQube xff1a 可以分析27多种不同编程语言中的代码 xff0c 并帮助您提高性能和检测安全漏洞 它由SonarSource的团队开发 xff0c 对社区免费开源 SonarQube可以添加到您的C
  • Lambda 表达式

    1 Lambda 表达式 1 1 通过接口传递代码 针对接口而非具体类型进行编程 xff0c 可以降低程序的耦合性 xff0c 提高灵活性 xff0c 提高复用性 接口常被用于传递代码 xff0c 比如 xff0c 我们知道 File 有如
  • Java 集合

    1 Java 集合框架 1 1 Java 集合概述 1 xff09 Java 容器 集合 数组都是对多个数据进行存储操作的结构 xff0c 简称 Java 容器 说明 xff1a 此时的存储 xff0c 主要指的是内存层面的存储 xff0c