Bootstrap

Java集合框架

一、集合框架总览

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • 接口:是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,是为了以不同的方式操作集合对象。
  • 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap。
  • 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。
    除了集合,该框架也定义了几个 Map 接口和类。Map 里存储的是键/值对。尽管 Map 不是集合,但是它们完全整合在集合中。

在这里插入图片描述

(ps:实线是继承,虚线是实现)

Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射。
在集合框架体系下,衍生出四种具体的集合类型:Map、Set、List、Queue,再下面是一些抽象类,最后是具体实现类。

  • List:元素放入有序,可重复;
  • Set:元素放入无序,不可重复;
  • Map:保存键值对映射,一对一或一对多;
  • Queue:一个可存储重复元素的队列,其特性与List相同,但只能从队头和队尾操作元素。

集合框架体系如图所示
在这里插入图片描述

二、迭代器

一般遍历数组都是采用for循环或者增强for,这两个方法也可以用在集合框架,但是还有一种方法是采用迭代器遍历集合框架,它是一个对象,实现了Iterator 接口或 ListIterator接口。

迭代器,使你能够通过循环来得到或删除集合的元素。

1. Iterator

Iterator接口提供的API接口如下:

  • hasNext():判断集合中是否存在下一个对象
  • next():返回集合中的下一个对象,并将访问指针移动一位
  • remove():删除集合中调用next()方法返回的对象

2. Iterator

Iterator 是提供集合操作内部对象的一个迭代器,它可以遍历、移除对象,且只能够单向移动
Iterable是对Iterator的封装,在JDK 1.8时,实现了Iterable接口的集合可以使用增强 for 循环遍历集合对象。

List<Integer> list = new ArrayList<>();
for (Integer num : list) {
    System.out.println(num);
}

3. ListIterator

ListIterator 继承了 Iterator ,以允许双向遍历列表和修改元素。

ListIterator 存在于 List 集合之中,通过调用方法可以返回起始下标为 index的迭代器,可以在任意一个下标位置返回该迭代器,且可以实现双向遍历。

public interface ListIterator<E> extends Iterator<E> {
    boolean hasNext();//判断集合中是否存在下一个对象
    E next();//返回集合中的下一个对象,并将访问指针移动一位
    boolean hasPrevious();//判断集合中是否存在上一个对象
    E previous();//返回集合中的上一个对象,并将访问指针移动一位
    int nextIndex();//返回下一个对象的下标位置
    int previousIndex();//返回上一个对象的下标位置
    void remove();//删除集合中调用next()方法返回的对象
    void set(E e);
    void add(E e);
}

三、Map集合体系详解

Map接口是由<key, value>组成的集合,由key映射到唯一的value,所以Map不能包含重复的key,每个键至多映射一个值。

1. HashMap

  • 它是集合中最常用的Map集合类型,底层由数组 + 链表 + 红黑树组成
  • 插入元素时,通过计算元素的哈希值,通过哈希映射函数转换为数组下标;查找元素时,同样通过哈希映射函数得到数组下标定位元素的位置
  • HashMap不是线程安全的

2. LinkedHashMap

  • LinkedHashMap是 HashMap 的子类,所以它具备 HashMap 的所有特点,它也不是线程安全的。
  • 它在 HashMap 的基础上维护了一条双向链表,该链表存储了所有元素,默认元素的顺序与插入顺序一致。
  • 可实现LRU缓存淘汰策略,其原理是通过设置accessOrder为true并重写removeEldestEntry方法定义淘汰元素时需满足的条件

3. TreeMap

  • TreeMap 是 SortedMap 的子类,所以它具有排序功能。
  • 它底层是由红黑树这种数据结构实现的,所以操作的时间复杂度恒为O(logN)
  • TreeMap 可以对key进行自然排序或者自定义排序,自定义排序时需要传入Comparator,而自然排序要求key实现了Comparable接口
  • TreeMap 不是线程安全的。

通过传入新的Comparator比较器,可以覆盖默认的排序规则。
compare()方法的返回值有三种,分别是:0,-1,+1

  • 如果返回0,代表两个元素相等,不需要调换顺序
  • 如果返回+1,代表前面的元素需要与后面的元素调换位置
  • 如果返回-1,代表前面的元素不需要与后面的元素调换位置

4.WeakHashMap

  • 它的键是一种弱键,放入 WeakHashMap 时,里面Entry中的键在每一次的垃圾回收都会被清除掉,所以不能确保某次访问元素一定存在
  • 它依赖普通的Map进行实现,是一个非线程安全的集合
  • WeakHashMap 通常作为缓存使用,适合存储那些只需访问一次、或只需保存短暂时间的键值对

5. Hashtable

  • Hashtable 底层的存储结构是数组 + 链表
  • HashTable 是一个线程安全的 Map,它所有的方法都被加上了 synchronized 关键字
  • Hashtable 的性能在并发环境下非常差

四、Set接口

Set接口继承了Collection接口,是一个不包括重复元素的集合。存入可变元素时,必须非常小心,因为任意时候元素状态的改变都有可能使得 Set 内部出现两个相等的元素,即 o1.equals(o2) = true,所以一般不要更改存入 Set 中的元素,否则将会破坏了 equals() 的作用。而Set 的最大作用就是判重,在项目中最大的作用也是判重。

1. AbstractSet 抽象类

AbstractSet 是一个实现 Set 的一个抽象类,定义在这里可以将所有具体 Set 集合的相同行为在这里实现,避免子类包含大量的重复代码

2. SortedSet 接口

SortedSet 是一个接口,它在 Set 的基础上扩展了排序的行为,所以所有实现它的子类都会拥有排序功能。

Comparator<? super E> comparator();元素的比较器,决定元素的排列顺序
SortedSet subSet(E var1, E var2)获取 [var1, var2] 之间的 set
SortedSet headSet(E var1)获取以 var1 开头的 Set
SortedSet tailSet(E var1)获取以 var1 结尾的 Set
E first()获取首个元素
E last()获取最后一个元素

3. HashSet

  • 底层数据结构:HashSet 也是采用数组 + 链表 + 红黑树实现
  • 线程安全性:由于采用 HashMap 实现,而 HashMap 本身线程不安全,在HashSet 中没有添加额外的同步策略,所以 HashSet 也线程不安全
  • 存入 HashSet 的对象的状态最好不要发生变化,因为有可能改变状态后,在集合内部出现两个元素o1.equals(o2),破坏了 equals()的语义。

4. LinkedHashSet

  • 它继承了 HashSet,而 HashSet 默认是采用 HashMap 存储数据的,但是 LinkedHashSet 调用父类构造方法初始化 map 时是 LinkedHashMap 而不是 HashMap
  • 由于 LinkedHashMap 不是线程安全的,且在 LinkedHashSet 中没有添加额外的同步策略,所以 LinkedHashSet 集合也不是线程安全的

5.TreeSet

  • TreeSet 是基于 TreeMap 的实现,所以存储的元素是有序的,底层的数据结构是数组 + 红黑树,任意操作的平均时间复杂度为 O(logN)
  • 元素的排列顺序有2种,和 TreeMap 相同:自然排序和定制排序,TreeSet 默认按照自然排序,如果需要定制排序,需要传入Comparator
  • TreeSet 是一个线程不安全的集合
  • TreeSet 常应用于对不重复的元素定制排序

三、List接口

List 接口直接继承 Collection 接口,它定义为可以存储重复元素的集合,并且元素按照插入顺序有序排列,且可以通过索引访问指定位置的元素。常见的实现有:ArrayList、LinkedList、Vector和Stack

1.AbstractList 和 AbstractSequentialList

  • AbstractList 抽象类实现了 List 接口,其内部实现了所有的 List 都需具备的功能。
  • AbstractSequentialList 抽象类继承了 AbstractList,在原基础上限制了访问元素的顺序只能够按照顺序访问,而不支持随机访问,如果需要满足随机访问的特性,则继承 AbstractList。

2.Vector

JDK 1.0 时代,ArrayList 还没诞生,大家都是使用 Vector 集合,但由于 Vector 的每个操作都被 synchronized 关键字修饰,即使在线程安全的情况下,仍然进行无意义的加锁与释放锁,造成额外的性能开销,做了无用功。因为性能低下原因被淘汰了。

3.Stack

Stack是一种后入先出(LIFO)型的集合容器,Stack 继承了 Vector 类,所以也被淘汰了。

4. ArrayList

  • 具备随机访问特点,访问元素的效率较高,ArrayList 在频繁插入、删除集合元素的场景下效率较低。
  • 底层数据结构:ArrayList 底层使用数组作为存储结构,具备查找快、增删慢的特点
  • 线程安全性:ArrayList 是线程不安全的集合
  • ArrayList 首次扩容后的长度为 10,调用 add() 时需要计算容器的最小容量。可以看到如果数组elementData为空数组,会将最小容量设置为10,之后会将数组长度完成首次扩容到 10。
  • 集合从第二次扩容开始,数组长度将扩容为原来的 1.5 倍

5. LinkedList

  • 优势:LinkedList 底层没有扩容机制,使用双向链表存储元素,所以插入和删除元素效率较高,适用于频繁操作元素的场景
  • 劣势:LinkedList 不具备随机访问的特点,查找某个元素只能从 head 或 tail 指针一个一个比较,所以查找中间的元素时效率很低
  • 查找优化:LinkedList 查找某个下标 index 的元素时做了优化,若 index > (size / 2),则从 head 往后查找,否则从 tail 开始往前查找
  • 双端队列:使用双端链表实现,并且实现了 Deque 接口,使得 LinkedList 可以用作双端队列。

遍历 ArrayList

import java.util.*;
 
public class Test{
 public static void main(String[] args) {
     List<String> list=new ArrayList<String>();
     list.add("Hello");
     list.add("World");
     list.add("HAHAHAHA");
     //第一种遍历方法使用 For-Each 遍历 List
     for (String str : list) {            //也可以改写 for(int i=0;i<list.size();i++) 这种形式
        System.out.println(str);
     }
 
     //第二种遍历,把链表变为数组相关的内容进行遍历
     String[] strArray=new String[list.size()];
     list.toArray(strArray);
     for(int i=0;i<strArray.length;i++) //这里也可以改写为  for(String str:strArray) 这种形式
     {
        System.out.println(strArray[i]);
     }
     
    //第三种遍历 使用迭代器进行相关遍历
     
     Iterator<String> ite=list.iterator();
     while(ite.hasNext())//判断下一个元素之后有值
     {
         System.out.println(ite.next());
     }
 }
}

遍历 Map

import java.util.*;
 
public class Test{
     public static void main(String[] args) {
      Map<String, String> map = new HashMap<String, String>();
      map.put("1", "value1");
      map.put("2", "value2");
      map.put("3", "value3");
      
      //第一种:普遍使用,二次取值
      System.out.println("通过Map.keySet遍历key和value:");
      for (String key : map.keySet()) {
       System.out.println("key= "+ key + " and value= " + map.get(key));
      }
      
      //第二种
      System.out.println("通过Map.entrySet使用iterator遍历key和value:");
      Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
      while (it.hasNext()) {
       Map.Entry<String, String> entry = it.next();
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }
      
      //第三种:推荐,尤其是容量大时
      System.out.println("通过Map.entrySet遍历key和value");
      for (Map.Entry<String, String> entry : map.entrySet()) {
       System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
      }
    
      //第四种
      System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
      for (String v : map.values()) {
       System.out.println("value= " + v);
      }
     }
}

总结

Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们。

集合是一个对象,可容纳其他对象的引用。集合接口声明对每一种类型的集合可以执行的操作。

集合框架的类和接口均在java.util包中。

任何对象加入集合类后,自动转变为Object类型,所以在取出的时候,需要进行强制类型转换。

;