# 8. Java 集合

# 1. Collections

Collection 接口:单列数据,定义了存取一组对象的方法的集合。

  • List:元素有序、可重复的集合
    • ArrayList:基于数组实现,增删慢,查询快,线程不安全
    • Vector:基于数组实现,增删慢,查询快,线程安全
    • LinkedList:基于双向链表实现,增删快,查询慢,线程不安全
  • Set:元素无序、不可重复的集合
    • HashSet:HashTable 实现,无序
    • TreeSet:红黑树实现
    • LinkedHashSet:HashTable 实现数据存储,双向链表记录顺序。
image-20210120164344639

# 1.1 Collection 接口

# 介绍

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

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

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

# 常用方法

1、添加

  • add(Object obj)
  • addAll(Collection coll)

2、获取有效元素的个数

  • int size()

3、清空集合

  • void clear()

4、是否是空集合

  • boolean isEmpty()

5、是否包含某个元素

  • boolean contains(Object obj):是通过元素的 equals 方法来判断是否是同一个对象
  • boolean containsAll(Collection c):也是调用元素的 equals方法 来比较的,拿两个集合的元素挨个比较。

6、删除

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

7、取两个集合的交集

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

8、集合是否相等

  • boolean equals(Object obj)

9、转成对象数组

  • Object[] toArray()

10、获取集合对象的哈希值

  • hashCode()

11、遍历

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

# 1.2 Iterator 迭代器接口

# 介绍

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

  • GOF 给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。

  • Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator() 方法,那么所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象。

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

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

# 常用方法

  • boolean hasNext():判断是否有下一个元素
  • E next():取出下一个元素(使用前必须先用 hasNext() 进行判断
    • ① 指针下移
    • ② 将下移以后集合位置上的元素返回
  • void remove():删除迭代器最后返回的元素
    • Iterator 可以删除集合的元素,但是是遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法;
    • 如果还未调用 next() 或在上一次调用 next() 方法之后已经调用了 remove 方法,再调用 remove 都会报 IllegalStateException。
image-20210121153153029

# 1.3 List 接口

List 除了从 Collection 集合继承的方法外,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 位置的子集合

# ArrayList

  • ArrayList 是 List 接口的典型实现类、主要实现类
  • 本质上,ArrayList 是对象引用的一个变长数组
  • ArrayList 的 JDK1.8 之前与之后的实现区别?
    • JDK1.7:ArrayList 像饿汉式,直接创建一个初始容量为 10 的数组
    • JDK1.7:每次数组容量的增长大约是其原容量的 1.5 倍。
    • JDK1.8:ArrayList 像懒汉式,一开始创建一个长度为 0 的数组,当添加第一个元素时再创建一个始容量为 10 的数组。
    • JDK1.8:每次数组的容量增长大约是其原容量的 1.5 倍。
  • Arrays.asList(...) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。 Arrays.asList(...) 返回值是一个固定长度的 List 集合。

# LinkedList

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

新增方法:

  • void addFirst(Object obj)
  • void addLast(Object obj)
  • Object getFirst()
  • Object getLast()
  • Object removeFirst()
  • Object removeLast()

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

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

# Vector

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

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

Vector 每次扩容是扩容为 2 倍。

新增方法:

  • void addElement(Object obj)
  • void insertElementAt(Object obj,int index)
  • void setElementAt(Object obj,int index)
  • void removeElement(Object obj)
  • void removeAllElements()
# 请问 ArrayList/LinkedList/Vector 的异同? 谈谈你的理解? ArrayList 底层是什么? 扩容机制? Vector 和 ArrayList 的最大区别?
1. ArrayList 和 LinkedList 的异同 
二者都线程不安全,相对线程安全的 Vector,执行效率高。 此外,ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
对于随机访问 get 和 set,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针。对于新增和删除操作 add (特指插入)和 remove,LinkedList 比较占优势,因为 ArrayList 要移动数据。
2. ArrayList 和 Vector 的区别
Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步类(synchronized),属于强同步类。因此开销就比 ArrayList 要大,访问要慢。正常情况下,大多数的 Java 程序员使用 ArrayList 而不是 Vector,因为同步完全可以由程序员自己来控制。
Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是1.5倍。
Vector 还有一个子类 Stack。

# 1.4 Set 接口

  • Set 接口是 Collection 的子接口,Set 接口没有提供额外的方法
  • Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。
  • Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals()方法。
# 如何理解无序?
1. 无序性 ≠ 随机性
2. 无序性是指数据在底层存储中不是内存连续的

# 如何理解不可重复性?
1. 元素是不可重复的
2. 比较是否重复的时候先用 hashCode() 再用 equals(),都过关了才相等

# HashSet

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

特点:

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

HashSet 集合判断两个元素相等的标准:

  • 两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。

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

向 HashSet 中添加元素的过程:

image-20210121165511217

插入过程

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

如果两个元素的 hashCode 值相等,会再继续调用 equals() 方法,如果 equals() 方法结果为 true,添加失败;如果为 false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。

如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。

数组上已经有元素了,那么链表中如何添加?

  1. JDK 7:元素 a 放到数组中,指向原来的元素(头插法)
  2. JDK 8:原来的元素在数组中,指向元素 a(尾插法) 总结:七上八下

HashSet 扩容原理:

HashSet 底层也是数组,初始容量为`16`,当如果使用率超过 `0.75`,(16*0.75=12) 就会扩大容量为原来的 2 倍。(16 扩容为 32,依次为 64,128.... 等)

重写 hashCode() 方法的基本原则:

1. 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。 
2. 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返回值也应相等。
3. 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

重写 equals() 方法的基本原则:

当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写 hashCode(),根据一个类的 equals 方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object.hashCode() 方法, 它们仅仅是两个对象。
因此,违反了“相等的对象必须具有相等的散列码”。
# 结论:重写 equals 方法的时候一般都需要同时重写 hashCode 方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。

示例:

    @Override
    public boolean equals(Object o) {
      	//自己的话直接true
        if (this == o) return true;
      	//o不能为空,所属类型也应该一直,否则 false
        if (o == null || getClass() != o.getClass()) return false;
      	//每个字段根据自己的业务需求进行比较
        Hedon hedon = (Hedon) o;
        return Objects.equals(name, hedon.name) &&
                Objects.equals(age, hedon.age) &&
                Objects.equals(birth, hedon.birth);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, birth);
    }
# 为什么用 Eclipse/IDEA 复写 hashCode 方法,有 31 这个数字?
1. 选择系数的时候要选择尽量大的系数。因为如果计算出来的 hash 地址越大,所谓的 “冲突”就越少,查找起来效率也会提高。(减少冲突)
2. 并且 31 只占用 5bits,相乘造成数据溢出的概率较小。
3. 31 可以 由 i*31== (i<<5)-1 来表示,现在很多虚拟机里面都有做相关优化。(提高算法效率)
4. 31 是一个素数,素数作用就是如果我用一个数字来乘以这个素数,那么最终出来的结果只能被素数本身和被乘数还有 1 来整除!(减少冲突)

# LinkedHashSet

  • LinkedHashSet 是 HashSet 的子类。
  • LinkedHashSet 根据元素的 hashCode值来决定元素的存储位置, 但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
  • LinkedHashSet 不允许集合元素重复。
Set set = new LinkedHashSet(); set.add(new String("AA")); set.add(456);
set.add(456);
set.add(456);
set.add(new Customer("刘德华", 1001));
image-20210121172322921

# TreeSet

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

TreeSet 底层使用红黑树结构存储数据(底层其实利用了 TreeMap)。

新增的方法如下:

  • Comparator comparator()

  • Object first()

  • Object last()

  • Object lower(Object e)

  • Object higher(Object e)

  • SortedSet subSet(fromElement, toElement)

  • SortedSet headSet(toElement)

  • SortedSet tailSet(fromElement)

TreeSet 两种排序方法:

  • 自然排序(默认情况下,TreeSet 采用自然排序)
    • 也就是存储的对象实现 Comparable 接口,然后重新 compareTo() 方法,根据这个来判断大小。
  • 定制排序
    • 在 new TreeSet() 的时候传入一个 Comparator 比较器。

TreeSet 的不可重复性:

  • 根据比较结果 this.compareTo(o) 或 compare(o1,o2) 来判断是否重复,而不是根据 equals()。

# 2. Map

Map 接口:双列数据,保存具有映射关系 “key-value" 的集合。

  • HashMap:JDK7 数组+链表 存储数据,JDK 8 数组+链表+红黑树 存储数据,线程不安全,
  • ConcurrentHashMap:JDK7 是基于 分段锁 实现线程安全,JDK8 是 CAS + synchronized 实现线程安全。
  • HashTable:线程安全
  • TreeMap:基于红黑树实现。
  • LinkedHashMap:基于 HashTable 数据结构,使用链表保存插入顺序
image-20210120195732661

Map 接口常用方法:

# 添加、删除、修改操作
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 中的所有数据
# 元素查询的操作
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 是否相等
# 元视图操作的方法
Set keySet():返回所有 key 构成的 Set 集合
Collection values():返回所有 value 构成的 Collection 集合
Set entrySet():返回所有 key-value 对构成的 Set 集合

# HashMap

  • 允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序;
  • 所有的 key 构成的集合是 Set
    • 无序的、不可重复的;
    • 所以,key 所在的类要重写 equals() 和 hashCode();
  • 所有的 value 构成的集合是 Collection
    • 无序的、可以重复的;
    • 所以,value 所在的类要重写 equals();
  • 一个键值对:key-value 构成了一个 Entry 对象(JDK8 以后叫 Node);
  • Map 中的 Entry:无序的、不可重复的,使用 Set 存储所有的 Entry。

HashMap 判断两个 key 相等的标准是:

  • 两个 key 通过 equals() 方法返回 true, hashCode 值也相等。

HashMap 判断两个 value 相等的标准是:

  • 两个 value 通过 equals() 方法返回 true。

HashMap 底层实现原理(JDK7):

  • 数组+链表

image-20210125145123435

1. 实例化
  HashMap map = new HashMap():  在实例化以后,底层创建了长度是 `16` 的一维数组 Entry[] table。

...可能已经执行过多次put...

2. 插入
  map.put(key1,value1):

  首先,调用 key1 所在类的 `hashCode()` 计算 key1 哈希值,此哈希值经过某种算法计算以后,得到在 Entry 数组中的存放位置。
  如果此位置上的数据为空,此时的 key1-value1 添加成功。 ---- 情况1
  如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1 和已经存在的一个或多个数据
  的哈希值:
          如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功。---- 情况2
          如果 key1 的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用 key1 所在类的 `equals(key2)` 方法,比较:
                  如果 equals() 返回 false:此时 key1-value1 添加成功。---- 情况3
                  如果 equals() 返回 true:使用 value1 替换 value2。 ---- 情况4

  补充:关于情况 2 和情况 3:此时 key1-value1 和原来的数据以链表的方式存储。

  在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的 `2` 倍,并将原有的数据复制过来。

HashMap 底层实现原理(JDK8):

  • 数组+链表+红黑树

image-20210125150320236

jdk8 相较于 jdk7 在底层实现方面的不同:
1. new HashMap(): 底层没有创建一个长度为 16 的数组;
2. jdk 8底层的数组是:Node[], 而非 Entry[];
3. 首次调用 put() 方法时,底层创建长度为 16 的数组;
4. jdk7 底层结构只有:数组+链表。jdk8 中底层结构:`数组+链表+红黑树`。
	4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
	4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > `8` 且当前数组的长度 > `64` 时,此时此索引位置上的所数据改为使用 `红黑树` 存储。
      * 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

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

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

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

面试题:

# 谈谈你对HashMap中put/get方法的认识?如果了解再谈谈 HashMap的扩容机制?默认大小是多少?什么是负载因子( 或填充比)?什么是吞吐临界值(或阈值、threshold)?



# 负载因子值的大小,对HashMap有什么影响?
- 负载因子的大小决定了 HashMap 的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为 0.7~0.75,此时平均检索长度接近于常数。


# HashMap 并发情况下死循环造成 CPU 100%?
并发情况下可能在链表上插入两个数据可能会造成互相引用,造成死循环。

# LinkedHashMap

  • LinkedHashMap 是 HashMap 的子类;
  • 在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序;
  • 与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代 顺序:迭代顺序与 Key-Value 对的插入顺序一致

# TreeMap

  • TreeMap 存储 Key-Value 对时,需要根据 key-value 对进行排序。 TreeMap 可以保证所有的 Key-Value 对处于有序状态。
  • TreeSet 底层使用红黑树结构存储数据。
  • TreeMap 判断两个 key 相等的标准:两个 key 通过 this.compareTo(o) 方法或者 compare(o1,o2) 方法返回 0。

# 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) 方法。
Properties pros = new Properties();

//方式一
//配置文件默认在当前的 Module 下
FileInputStream fis = new FileInputStream("jdbc.properties");
pros.load(fis);

//方式二
//配置文件默认在当前类路径下
ClassLoader cl =  this.getClass().getClassLoader();
InputStream is = cl.getResourceAsStream("jdbc.properties");
pros.load(is)

String user = pros.getProperty("user");
String password = pros.getProperty("password");
上次更新: 9/25/2021, 11:26:28 PM