Java学废之路06——泛型与集合

本文详细介绍了Java中的泛型和集合。泛型帮助我们在编译时检查类型安全,消除运行时类型转换,提高代码可读性和安全性。文章通过实例解释了ArrayList、LinkedList、Vector和HashSet、LinkedHashSet、TreeSet的特性和使用场景,以及HashMap、HashTable、LinkedHashMap和TreeMap的存储结构、扩容机制与区别。此外,还涉及迭代器、Collections工具类和阻塞队列的概念与应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

六、泛型与集合

image-20210507103005073

6.1 泛型

为了能够更好的学习容器,我们首先要先来学习一个概念:泛型。

泛型是JDK1.5以后增加的,它可以帮助我们建立类型安全的集合。在使用了泛型的集合中,遍历时不必进行强制类型转换。JDK提供了支持泛型的编译器,将运行时的类型检查提前到了编译时执行,提高了代码可读性和安全性。

泛型的本质就是“数据类型的参数化”。泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。使用泛型编写的代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性,可读性。

6.1.1 概述

所谓泛型, 就是允许在定义类、 接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。 这个类型参数将在使用时(例如,继承或实现这个接口, 用这个类型声明变量、 创建对象时) 确定(即传入实际的类型参数, 也称为类型实参) 。

  • 从JDK1.5以后, Java引入了“参数化类型” 的概念,允许我们在创建集合时再指定集合元素的类型, 正如: List, 这表明该List只能保存字符串类型的对象
  • JDK1.5改写了集合框架中的全部接口和类, 为这些接口、 类增加了泛型支持,从而可以在声明集合变量、 创建集合对象时传入类型实参
  • 泛型必须是类,不能是基本数据类型
  • 如果实例化时,没有知名泛型的类型。默认的类型为Object类
6.1.2 作用

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以引入泛型。

  • 解决元素存储的安全性问题,提高 Java 程序的类型安全
  • 解决获取数据元素时, 需要类型强制转换的问题。消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会
  • 使用泛型的主要优点是能够在编译时而不是在运行时检测错误

image-20201023153712282

image-20201023153831644

6.1.3 泛型应用举例

为解决运行时动态改变数组大小的问题,Java引入ArrayList类。——泛型数组列表

(1)概述

ArrayList是一个采用类型参数的泛型类。为指定数组列表保存的元素对象类型,使用一对尖括号将类名括起来加在后面。Java SE7中,右边的尖括号内的类型参数可以省略,此为“菱形”语法。

数组列表与数组的区别:

  • 数组列表

    new ArrayList<>(100),表示该数组列表有100个空位置可以使用,但当该100个空间满了的时候,数组列表会自动创建一个更大的数组,并且将所有的对象从较小的数组中拷贝到较大的数组中;在初始化完成之后,数组列表中不会含有任何元素;

    public static void main(String[] args) {
        List<String> list = new ArrayList<>(100);	//尽管已初始化100个单位
        int[] demo = new int[10];				//实例化时就已被分配空间
    
        System.out.println(demo.length);		//结果为10
    
        System.out.println(list.size());		//结果为0
        System.out.println(list.isEmpty());		//结果为true
    }
    
  • 数组

    new Children[100],表示为数组分配了100个元素的存储空间,而且就只有这100个空间可用。

(2)访问数组列表元素

数组列表可以通过add()方法来增加元素。数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。原因是ArrayList并不是Java语言的一部分,它只是由某些人编写的被放在标准库中的一个实用类。

使用get和set方法实现访问或改变数组元素的操作。

使用ArrayList的好处:

  • 不必指出数组的大小
  • 使用add将任意多的元素添加至数组中
  • 使用size()替代length计算元素数目
  • 使用a.get(i)替代a[i]访问元素
(3)容器中的泛型

容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避免了大量的类型判断,非常便捷。

public class Test {
    public static void main(String[] args) {
        // 以下代码中List、Set、Map、Iterator都是与容器相关的接口;
        List<String> list = new ArrayList<String>();
        Set<Man> mans = new HashSet<Man>();
        Map<Integer, Man> maps = new HashMap<Integer, Man>();
        Iterator<Man> iterator = mans.iterator();
    }
}
6.1.4 自定义泛型
  • 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型

    // static的方法中不能声明泛型
    // public static void show(T t) {}
    
  • 异常类不能是泛型的

    // 异常类不能声明为泛型类
    // public class MyException<T> extends Exception{}
    
  • 父类有泛型 class Father<T1, T2> {} ,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现
      • 没有类型:擦除。class Son1 extends Father {}
      • 具体类型:全覆盖。class Son2 extends Father<Integer, String>
    • 子类保留父类的泛型:泛型子类
      • 全部保留:class Son3<T1, T2> extends Father<T1, T2> {}
      • 部分保留:class Son4 extends Father<Integer, T2> {}

6.2 集合概述

开发和学习中需要时刻和数据打交道,如何组织这些数据是我们编程中重要的内容。 我们一般通过“容器”来容纳和管理数据。数组就是一种容器,可以在其中放置对象或基本类型数据。

  • 数组的优势:是一种简单的线性序列,可以快速地访问数组元素,效率高。如果从效率和类型检查的角度讲,数组是最好的。

  • 数组的劣势:不灵活。容量需要事先定义好,不能随着需求的变化而扩容。

基于数组并不能满足我们对于“管理和组织数据的需求”,所以我们需要一种更强大、更灵活、容量随时可扩的容器来装载我们的对象。 这就是容器,也叫集合(Collection)。Java 集合类可以用于存储数量不等的多个对象,还可用于保存具有映射关系的关联数组。

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

  • Collection接口: 单列数据, 定义了存取一组对象的方法的集合
    • List: 元素有序、可重复的集合
    • Set: 元素无序、不可重复的集合
  • Map接口: 双列数据,保存具有映射关系“key-value对”的集合

以下是容器的接口层次结构图:

image-20201007153626871

6.3 Collection接口

集合层次结构中的根界面 。 集合表示一组被称为其元素的对象。Collection 表示一组对象,它是集中、收集的意思。Collection接口的两个子接口是List、Set接口。

image-20200805152416106

由于List、Set是Collection的子接口,意味着所有List、Set的实现类都有上面的方法。

6.3.1 List
public interface List<E> extends Collection<E>

List是有序、可重复的容器,集合中的每个元素都有其对应的顺序索引。其可以精确控制列表中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。与集合不同,列表通常允许重复的元素。常用实现类有ArrayList \ LinkedList \ Vector.

  • 有序:List中每个元素都有索引标记。可以根据元素的索引标记(在List中的位置)访问元素,从而精确控制这些元素。
  • 可重复:List允许加入重复的元素。更确切地讲,List通常允许满足 e1.equals(e2) 的元素重复加入容器。
image-20200805152454540
(1)ArrayList

ArrayList是可以存放任意数量的对象,长度不受限制,可以通过定义新的更大的数组,将旧数组中的内容拷贝到新数组,来实现扩容。

public class ArrayList<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

ArrayList底层是用数组实现的存储。其与数组的区别是ArrayList可以扩容。ArrayList 的特点:查询效率高,增删效率低,线程不安全。

jdk7 中的 ArrayList 的对象的创建类似于单例的饿汉式,而 jdk8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。


jdk1.7

ArrayList像饿汉式,直接创建一个默认初始容量为10的数组。

ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。

默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。


jdk1.8

ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时再创建一个始容量为10的数组。

//底层Object[] elementData初始化为{},并没有创建长度为10的数组
ArrayList list = new ArrayList();

//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
list.add(123);

后续的添加和扩容操作与 jdk 7 相同。


扩容源码

/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 将新容量更新为旧容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
(2)LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, Serializable

LinkedList底层用双向链表实现的存储。

特点:查询效率低,增删效率高,线程不安全。LinkedList是不同步的。如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。


LinkedList源码分析:

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

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

内部Node节点的定义如下:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}
(3)Vector
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable

Vector底层是用数组实现的List,相关的方法都加了同步检查,因此“线程安全,效率低”。Vector类实现了可扩展的对象数组。 像数组一样,它包含可以使用整数索引访问的组件。 但是, Vector的大小可以根据需要增长或缩小,以适应在创建Vector之后添加和删除项目。

(4)总结

image-20201023150443387

  • 线程安全:Vector是线程安全的,ArrayList 与 LinkedList 是线程不安全的。在jdk1.8中,可以使用Collections工具类,返回一个SynchronizedList线程安全的List
  • 随机访问速度:由于Vector中同步锁的存在,ArrayList > Vector;由于 LinkedList 是基于双链表结构,所以LinkedList 的访问速度更慢
  • 随机移动速度:LinkedList > ArrayList

image-20201021155742812

6.3.2 Set
public interface Set<E> extends Collection<E>

Set接口是Collection的子接口, Set接口没有提供额外的方法,方法和Collection保持完全一致。

  • Set 内元素是不重复的。集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。即保证添加的元素按照equals()方法对比不能为 true,否则就不能加入;并且 Set 中只允许放入一个 null 元素,不能多个。

  • Set 内元素是无序的。无序指的是存放时的位置不会像List一样挨着依次存放,而是根据hash值确定。存储的数据在底层数组中并非按照数组(jdk1.7中Set底层是数组,jdk1.8中为HashMap)索引的顺序添加,而是根据数据的哈希值进行位置确定的,因此会无序。

Set常用的实现类有:HashSet、LinkedHashSet、TreeSet等。

(1)HashSet

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

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

HashSet 集合判断两个元素相等的标准: 两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。 【在同一个加载器下才有效】


HashSet 元素的添加元素 a 的过程:

元素的添加过程中为保证 Set 的不重复性,必定要进行比较操作。为了提高比较的效率,采用 hashCode()+equals() 方法相结合的方式进行:

通过先使用 hashCode() 方法来确定元素的索引位置,减少了散列表中添加元素时为保证数据的不重复性而做的 equals 操作次数,以此来提高散列表的查找与存取效率。

  • 哈希值:是一个十进制的整数,由系统随机给出【对象的地址值,是一个逻辑地址,是模拟出来的地址,不是数据实际存储的物理地址】int hashCode():返回对象的哈希码值

  • 索引值:HashSet底层结构中的存放位置【物理地址】,是某对象的哈希值经过特定算法计算求得的

image-20201021222718606

  • 首先调用元素a所在类的hashCode()方法,计算元素a的哈希值
  • 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),以保证无序性
  • 判断数组此索引位置上是否已经有元素:
    • 如果此位置上没有其他元素,则元素a添加成功。 —>情况1
    • 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
      • 如果hash值不相同,则元素a添加成功。—>情况2
      • 如果hash值相同,进而需要调用元素a所在类的equals()方法,以保证不重复性
        • equals()返回true,元素a添加失败
        • equals()返回false,则元素a添加成功。—>情况3

对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。

image-20201021215036261

hashCode()的作用是为了提高在散列结构(Hash)存储中查找的效率,在线性表中没有作用;只有每个对象的 hash 码尽可能不同才能保证散列的存取性能,事实上 Object 类提供的默认实现确实保证每个对象的 hash 码不同(在对象的内存地址基础上经过特定算法返回一个 hash 码)。在 Java 有些集合类(HashSet)中要想保证元素不重复可以在每增加一个元素就通过对象的 equals 方法比较一次,那么当元素很多时后添加到集合中的元素比较的次数就非常多了,也就是说如果集合中现在已经有 3000 个元素则第 3001 个元素加入集合时就要调用 3000 次 equals 方法,这显然会大大降低效率,于是 Java 采用了哈希表的原理,这样当集合要添加新的元素时会先调用这个元素的 hashCode 方法计算出它的Hash值,然后就可以根据算法定位到它要存放的物理位置上,如果这个位置上没有元素则它就可以直接存储在这个位置上而不用再进行任何比较了,如果这个位置上已经有元素了则就调用它的 equals 方法与新元素进行比较,相同的话就不存,不相同就散列其它的地址,这样一来实际调用 equals 方法的次数就大大降低了,几乎只需要一两次,而 hashCode 的值对于每个对象实例来说是一个固定值(索引)

当在索引位置有元素时的比较过程是根据JAVA规定来实现的:

  • 因为hashCode()方法返回的是一个hash值,这对元素来说是一个固定值,其运算速度更快,所以优先调用hashCode()方法,判断返回值的异同;
    • 不同时,返回false,说明此索引位置的元素与新加的元素不同,则进行添加【减少equals()方法的调用】
    • 相同时,返回true,此时不能确定两元素是否相同,还需要进一步调用equals()方法来判断
      • equals返回true,说明两元素相同,不进行添加操作;
      • equals返回false,说明两元素不同,进行添加。
(2)LinkedHashSet
  • LinkedHashSet 是 HashSet 的子类
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet 插入性能略低于 HashSet, 但在遍历访问 Set 里的全部元素时有很好的性能。
  • LinkedHashSet 不允许集合元素重复。

image-20201022104322345

此时结果显示的“无序”与“有序”并非是Set集合中的无序性。Set中定义的无序性是指在存储位置中的无序性,而非是输出时表面显示的“无序性”。

(3)TreeSet
  • TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态
  • TreeSet 中存储数据的类型必须是具有可比性的,即数据的类型是一致
  • TreeSet 底层使用红黑树结构存储数据,因此也保证了数据的不重复性
  • TreeSet 两种排序方法:自然排序定制排序。默认情况下,TreeSet 采用自然排序

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

  • 向 TreeSet 中添加元素时,只有第一个元素无须调用compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较
  • 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个类型的对象
  • 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 方法比较的返回值是否为0,而不是根据equals()方法
  1. TreeSet 中存储基本数据类型的数据时,将自动将其转化为它们的包装类,然后调用各个包装类中已经重写好的compareTo方法来进行比较。前提是数据类型保持一致
  2. TreeSet 中存储某些特定类对象时,如String、Date等,由于这些类都实现了Comparable 接口并重写了compareTo方法,所以在 TreeSet 遍历时不会报错:
    • String:按字符串中字符的 Unicode 值进行比较
    • Date、 Time:后边的时间、日期比前面的时间、日期大
  3. TreeSet 中存储自定义类对象时,则该对象的类必须实现 Comparable 接口,否则会报如下错误。实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过compareTo(Object obj) 方法的返回值来比较大小
image-20201022153712224

TreeSet的自然排序要求元素所属的类实现Comparable接口,如果元素所属的类没有实现Comparable接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。

定制排序:通过 Comparator接口 来实现。 需要重写 compare(T o1,T o2) 方法。

  • 要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
  • 仍然只能向TreeSet中添加类型相同的对象。否则发生 ClassCastException 异常。
  • 使用定制排序判断两个元素相等的标准是:通过Comparator比较两个元素返回了0。
// 定制排序
@Test
public void test2(){
    // 自定义比较器
    Comparator com = new Comparator() {
        //按照年龄从小到大排列
        @Override
        public int compare(Object o1, Object o2) {
            if(o1 instanceof Person && o2 instanceof Person){
                Person u1 = (Person)o1;
                Person u2 = (Person)o2;
                return Integer.compare(u1.getAge(),u2.getAge());
            }else{
                throw new RuntimeException("输入的数据类型不匹配");
            }
        }
    };

    // 将比较器作为参数,传入TreeSet中
    TreeSet set = new TreeSet(com);
    set.add(new Person("Tom",12));
    set.add(new Person("Jerry",32));
    set.add(new Person("Jim",2));
    set.add(new Person("Mike",65));
    set.add(new Person("Mary",33));
    set.add(new Person("Jack",33));
    set.add(new Person("Jack",56));

    Iterator iterator = set.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }
}
(4)总结

image-20201101172532189

底层数据结构:

  • HashSet: HashMap

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }
    

    HashSet的add()方法添加元素的实现机制:

    需要注意的是,add方法中只有一个参数,而其调用的底层HashMap的put方法则需要两个参数<K,V>。从源码中可以看到,是将参数当作 HashMap 的 key,而其 value 则是赋予了一个常量PRESENT。

    /**如果元素不存在时,则添加元素
     * Adds the specified element to this set if it is not already present.
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    
    // 常量
    private static final Object PRESENT = new Object();
    
  • LinkedHashSet: HashMap+双向链表

    /**LinkedHashSet的构造方法
     * Constructs a new, empty linked hash set with the default initial
     * capacity (16) and load factor (0.75).
     */
    public LinkedHashSet() {
        // 调用父类HashSet的构造方法
        super(16, .75f, true);
    }
    
    /**LinkedHashSet的父类HashSet的构造方法
     * Constructs a new, empty linked hash set.  (This package private
     * constructor is only used by LinkedHashSet.) The backing
     * HashMap instance is a LinkedHashMap with the specified initial
     * capacity and the specified load factor.
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        // 其父类的底层数据结构是HashMap,但是该构造器中创建的是LinkedHashMap对象
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    
    /**LinkedHashMap构造器,其底层参看LinkedHashMap
     * Constructs an empty insertion-ordered(插入顺序) <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and load factor.
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
    // 其内部有一个存储插入顺序的Entry<K,V> before, after双向链表
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
  • TreeSet: TreeMap

    /**TreeSet的构造器
     * Constructs a new, empty tree set, sorted according to the
     * natural ordering of its elements(自然排序).  All elements inserted into
     * the set must implement the {@link Comparable} interface.
     * Furthermore, all such elements must be <i>mutually
     * comparable</i>: {@code e1.compareTo(e2)} must not throw a
     * {@code ClassCastException} for any elements {@code e1} and
     * {@code e2} in the set.  If the user attempts to add an element
     * to the set that violates this constraint (for example, the user
     * attempts to add a string element to a set whose elements are
     * integers), the {@code add} call will throw a
     * {@code ClassCastException}.
     */
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    
    /**TreeMap的构造器
     * Constructs a new, empty tree map, using the natural ordering of its
     * keys.  All keys inserted into the map must implement the {@link
     * Comparable} interface.  Furthermore, all such keys must be
     * <em>mutually comparable</em>: {@code k1.compareTo(k2)} must not throw
     * a {@code ClassCastException} for any keys {@code k1} and
     * {@code k2} in the map.  If the user attempts to put a key into the
     * map that violates this constraint (for example, the user attempts to
     * put a string key into a map whose keys are integers), the
     * {@code put(Object key, Object value)} call will throw a
     * {@code ClassCastException}.
     */
    public TreeMap() {
        comparator = null;
    }
    

6.4 Map接口

Map与Collection并列存在。用于保存具有映射关系的数据:key-value。

  • Map 中的 key 和 value 都可以是任何引用类型的数据
  • Map 中的 key 用 Set 来存放,不允许重复,即同一个 Map 对象所对应的类须重写hashCode()和equals()方法
  • 所有的 key 构成的集合是 Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
  • 所有的 value 构成的集合是 Collection:无序的、
  • 可以重复的。所以, value所在的类要重写: equals()
  • 一个 key-value 构成一个 entry。所有的entry构成的集合是Set:无序的、不可重复的
image-20201022162304774

Map 接口的实现类有HashMap、Hashtable、LinkedHashMap 和TreeMap等。

image-20201022164417530

6.4.1 HashMap

image-20210528214831695

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
(1)概述

Hashmap 是一个最常用的Map,它根据键的Hash值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的,不保证映射的顺序。

  • 允许使用 null 键和 null 值。HashMap最多只允许一条记录的键为Null;允许多条记录的值为 Null
  • HashMap 不支持线程安全。即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections的synchronizedMap方法使HashMap具有同步的能力
  • 所有的 key 构成的集合是Set:无序的、不可重复的。所以, key所在的类要重写:equals()和hashCode()
  • 所有的 value 构成的集合是Collection:无序的、可以重复的。所以, value所在的类要重写: equals()
  • 一个key-value构成一个entry。所有的entry构成的集合是Set:无序的、不可重复的

image-20201022200314754

(2)存储结构与扩容机制
  • JDK 7及以前版本: HashMap是数组 + 链表【即链地址法】
  • JDK 8版本发布以后: HashMap是数组 + 链表 / 红黑树

JDK 7 :HashMap的内部存储结构是数组和链表的结合。

image-20201022170613700
  • 首先创建一个HashMap实例:Map map = new HashMap();,其底层是一个默认初始容量为16的数组Entry[] table

  • 向HashMap中添加entry1(key,value),需要首先根据key所在类的hashCode()方法计算entry1中key的哈希值

  • 此哈希值经过处理以后,得到在底层Entry[]数组中要存储的位置 i 【索引位置】

    • 如果位置 i 上没有元素,则entry1直接添加成功。
    • 如果位置 i 上已经存在entry2或链表中还存在其他的entry3、 entry4,则需要通过循环的方法,依次比较entry1中key和其他entry的key:【key比较】
      • 如果彼此hashCode不同,则直接添加成功。【hashCode不同,则两元素肯定不同】
      • 如果与其中某一个元素的hashCode相同,继续比较二者是否equals():
        • 如果返回值为true,则使用entry1的value去替换equals()为true的entry的value。【更新】
        • 如果遍历一遍以后,发现所有的equals返回都为false,则entry1添加成功。 entry1指向原有的entry元素。【添加】

在不断的添加元素的过程中,会涉及到扩容问题。JDK7中默认的扩容方式为:扩容为原来容量的2倍,并将原有的数据复制过来。


JDK8 :HashMap是由数组+链表+红黑树实现。

  • 最底层的数据单元是一个Node节点,初始化后长度16的数组table中存储的并不是<K,V>键值对,而是一个Node节点:
/**数组table
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * 在第一次使用时初始化,并根据需要调整大小。被分配时,其长度总是2的幂。
 */
transient Node<K,V>[] table;
/**Node节点
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    // 首先比较key的hash,若发生冲突,就建立链表,将元素通过尾插法插入该索引位置的单链表
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
image-20201022170705193
  • 在创建HashMap实例时,JDK8 底层并不会创建一个长度为16的数组。其底层的数据结构也不再是数组Entry[] table,而是变为了Node,并且在第一次被put时,才会创建长度为16的数组Node[] table:
image-20201022210249707
  • JDK8 的底层结构:数组+链表+红黑树。当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 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
    
    image-20201022212129375
  • JDK8 的扩容机制

HashMap底层数组的大小默认是16,其原因是为了方便扩容与散列。【源码中规定数组的长度必须是2的N次幂】

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

    当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。

  • JDK8非树形化

    当链中的结点少于6个时,红黑树将会恢复为链表。

使用红黑树的原因是既保证搜索的效率较高,又能保证树的旋转速度尽可能地快,即实现了插入与查询的性能平衡。红黑树的特点:

  • 二叉“平衡“树:”平衡“的特点确定了红黑树可以实现较快地检索。
  • 黑色完美平衡:红黑树不是绝对意义上的平衡树。红黑树不需要全局地进行旋转操作,它只需要最长字数的深度不超过最短子树深度的两倍即可。
(3)负载因子

image-20201022214059927

6.4.2 HashTable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, Serializable

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。

Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中。

Hashtable同样实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆。

(1)Hashtable 与 HashMap的区别
  • 继承的父类不同

二者都实现了Map接口。但是它们继承的父类不同:HashMap继承自AbstractMap类。Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。

  • 线程安全

javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

Hashtable 中的方法大多是Synchronize的,而HashMap中的方法在一般情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。HashTable实现线程安全的代价就是效率变低,因为会锁住整个HashTable,而ConcurrentHashMap做了相关优化,因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定,效率比HashTable高很多。

HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。

  • 遍历方式不同

HashTable使用Enumeration遍历,HashMap使用Iterator进行遍历。

在hashmap的put方法调用addEntry()方法,假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。故解决方法是使用ConcurrentHashMap。

HashMap的迭代器(Iterator)是fail-fast迭代器,故当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException异常,而Hashtable的enumerator迭代器不是fail-fast的。但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

  • 是否允许null值

Hashmap是允许key和value为null值的,用containsValue和containsKey方法判断是否包含对应键值对;HashTable键值对都不能为空,否则包空指针异常。

  • 扩容方式不同

HashMap 哈希扩容必须要求为原容量的2倍,而且一定是2的幂次倍扩容结果,而且每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入;

Hashtable扩容为原容量2倍加1;

  • 存储结构不同
6.4.3 LinkedHashMap

使用双链表解决了HashMap的无序性,使用也解决了

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

image-20201022215710638

(1)LRU数据结构

LRU【最近最少/最久未使用】,其存储结构要满足以下3个条件:

  • 存储结构中的元素要求有时序,以区分最近使用与最久未使用,当容量满时要删除最久未使用的那个元素为新元素腾位置
  • 存储结构要满足可以快速找到某个key是否存在并得到对应的Val
  • 每次访问某个key时,需要将这个元素变为最近使用的,即将该元素从原结构中删除并添加至最近使用的位置。也即该存储结构要支持在任意位置快速插入与删除元素

综上所述,就需要哈希表与链表的结合体——LinkedHashmap。LRU缓存算法的核心数据是哈希链表,是双向链表与哈希表的结合体。

image-20210429094202830

根据上述的存储结构,我们可以得到:

  • 每次默认从双向链表的末位添加元素,那么显然越靠近尾部的元素就越是最近使用的,越靠近头部的元素就越是越久未使用的
  • 对于每一个key,可以通过哈希表快速定位到链表的结点,从而去双向链表中找到对应的val
  • 双向链表的使用显然是支持在任意位置快速插入与删除元素的。普通的链表是无法按照索引快速访问一个元素的【链表查找速度慢】,但是由于有哈希表的支持,那么就可以通过key快速映射到任何一个链表结点,从而实现了元素的快速定位
(2)LRU代码实现
  • 双向链表Node
/**
 * 双向链表结点
 */
public class Node {
    public int key, val;
    public Node pre, next;

    public Node(int key, int val) {
        this.key = key;
        this.val = val;
    }
}
  • 双向链表
/**
 * 双向链表
 * 双向链表结构及对链表的基本操作
 * - 添加结点【只在队尾添加】
 * - 删除节点【删除随机位置元素——用于访问元素,将元素提升为最近使用元素场景】
 * - 删除头结点【linkedhashmap容量满了,需要删除最久未使用元素】
 */
public class DoubleList {
    // 首尾虚节点
    private Node head, tail;
    // 链表容量=linkedhashmap的容量
    private int size;

    public DoubleList() {
        this.head = new Node(0, 0);
        this.tail = new Node(0, 0);
        this.size = 0;
    }

    // 队尾添加元素
    public void addLast(Node node) {
        node.next = tail;
        node.pre = tail.pre;
        tail.pre.next = node;
        tail.pre = node;
        size++;
    }

    // 删除任意元素
    public void remove(Node node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
        size--;
    }

    // 删除队首元素,并返回该元素,用以同时删除掉哈希表中的相应元素
    public Node removeFirst(){
        if (head.next == tail) return null;
        Node first = head.next;
        // 调用remove()方法,删除元素
        remove(first);
        return first;
    }

    public int getSize(){
        return this.size;
    }
}
  • LRU存储结构及基本操作
/**
 * LRU存储结构:双向链表+哈希表
 * 对于整个LRU存储结构而言,其操作有
 * - get(key):根据key获取元素的val
 * - 哈希表根据key定位元素
 * - 删除双向链表中的元素
 * - 将元素添加至链表尾部作为最近使用元素
 * - put(key,node):插入元素
 * - 若key存在,那么就更新元素
 * - 若key不存在
 * - 容量已满,删除队首元素【最久未使用】,再添加元素
 * - 容量未满,添加元素
 */
public class LRUStorageStructure {
    // 基本存储结构:哈希表+双向链表【也可使用Java内置的LinkedHashmap】
    private HashMap<Integer, Node> map;
    private DoubleList list;
    // 容量
    private int cap;

    public LRUStorageStructure(int cap) {
        this.cap = cap;
        this.map = new HashMap<>();
        this.list = new DoubleList();
    }

    //===========LRU基本操作,对外提供接口===============
    //1 get(key)
    public int get(int key) {
        if (!map.containsKey(key))
            return -1;
        Node node = map.get(key);
        // 将元素提升为最近使用元素
        makeRecent(key);
        return node.val;
    }

    //2 put(key,val)
    public void put(int key, int val) {
        if (map.containsKey(key)) {
            // 删除该已存在的key【该元素的位置是随机的】
            deleteKey(key);
            // 添加key至最近使用位
            addRecent(key, val);
            return;
        }
        // 容量满了
        if (this.cap == list.getSize()) {
            // 删除最久未使用元素
            removeLeastRecent();
        }
        // 添加为最近使用元素
        addRecent(key, val);
    }

    //===============封装对哈希表与双向链表的操作=================
    //1 添加为最近使用元素
    private void addRecent(int key, int val) {
        Node node = new Node(key, val);
        // 操作双向链表
        list.addLast(node);
        // 操作哈希表
        map.put(key, node);
    }

    //2 访问某个存在的元素,将其提升为最近使用使用元素
    private void makeRecent(int key) {
        Node node = map.get(key);
        // 在原链表上删除节点
        list.remove(node);
        // 将该节点转移为最近使用元素
        list.addLast(node);
    }

    //3 删除某个元素
    private void deleteKey(int key) {
        Node node = map.get(key);
        // 删除双向链表中的结点
        list.remove(node);
        // 删除哈希表中的结点
        map.remove(key);
    }

    //4 删除最久未使用元素
    private void removeLeastRecent() {
        // 删除双向链表中的头结点【最久未使用结点】
        Node node = list.removeFirst();
        // 删除哈希表中结点
        map.remove(node.key);
    }
}
6.4.4 TreeMap

TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历TreeMap时,得到的记录是排过序的。

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

  • TreeSet 底层使用红黑树结构存储数据
  • TreeMap 的 Key 的排序:
    • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClassCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
6.4.5 总结

一般情况下,我们用的最多的是HashMap。在 Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果要按自然顺序或自定义顺序遍历键,那么 TreeMap 会更好。如果需要输出的顺序和输入的相同,那么用LinkedHashMap 可以实现,它还可以按读取顺序来排列。

HashMap 是一个最常用的Map,它根据键的hash值存储数据,根据键可以直接获取它的值,具有很快的访问速度。HashMap最多只允许一条记录的键为NULL,允许多条记录的值为NULL。

HashMap 不支持线程同步,即任一时刻可以有多个线程同时写 HashMap ,可能会导致数据的不一致性。如果需要同步,可以用 Collections 的 synchronizedMap 方法使HashMap具有同步的能力。

image-20201023151350627

底层数据结构:

  • HashMap:数组+链表+红黑树

    【resize()初始化或扩容方法+putVal()向链表中添加元素方法+treeify()调整为红黑树方法】

    /**对HashMap初始化或扩容
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //****
    }
    
    /**
    * Associates the specified value with the specified key in this map.
    * If the map previously contained a mapping for the key, the old
    * value is replaced.
    *
    * @param key key with which the specified value is to be associated
    * @param value value to be associated with the specified key
    * @return the previous value associated with <tt>key</tt>, or
    *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
    *         (A <tt>null</tt> return can also indicate that the map
    *         previously associated <tt>null</tt> with <tt>key</tt>.)
    */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    	//****
    }
    
    /**
    * Forms tree of the nodes linked from this node.
    * @return root of tree
    */
    final void treeify(Node<K,V>[] tab) {
        TreeNode<K,V> root = null;
        //*****
    }
    
  • LinkedHashMap: HashMap+双向链表

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
    /**LinkedHashMap中双向链表的结构
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    
    /**
     * The head (eldest) of the doubly linked list(双向链表).
     */
    transient LinkedHashMap.Entry<K,V> head;
    
  • TreeMap:树+比较器

    /**TreeMap的构造器
     * Constructs a new, empty tree map, using the natural ordering of its
     * keys.  All keys inserted into the map must implement the {@link
     * Comparable} interface.  Furthermore, all such keys must be
     * <em>mutually comparable</em>: {@code k1.compareTo(k2)} must not throw
     * a {@code ClassCastException} for any keys {@code k1} and
     * {@code k2} in the map.  If the user attempts to put a key into the
     * map that violates this constraint (for example, the user attempts to
     * put a string key into a map whose keys are integers), the
     * {@code put(Object key, Object value)} call will throw a
     * {@code ClassCastException}.
     */
    public TreeMap() {
        comparator = null;
    }
    
    /**树节点
    * Node in the Tree.  Doubles as a means to pass key-value pairs back to
    * user (see Map.Entry).
    */
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
    

6.5 迭代器

迭代器提供了统一的遍历容器的方式。

6.5.1 遍历List
public static void main(String[] args) {
    List<String> aList = new ArrayList<String>();
    for (int i = 0; i < 5; i++) {
        aList.add("a" + i);
    }
    System.out.println(aList);
    for (Iterator<String> iter = aList.iterator(); iter.hasNext();) {
        String temp = iter.next();
        System.out.print(temp + "\t");
        if (temp.endsWith("3")) {// 删除3结尾的字符串
            iter.remove();
        }
    }
    System.out.println();
    System.out.println(aList);
}
6.5.2 遍历Set
public static void main(String[] args) {
    Set<String> set = new HashSet<String>();
    for (int i = 0; i < 5; i++) {
        set.add("a" + i);
    }
    System.out.println(set);
    for (Iterator<String> iter = set.iterator(); iter.hasNext();) {
        String temp = iter.next();
        System.out.print(temp + "\t");
    }
    System.out.println();
    System.out.println(set);
}
6.5.3 遍历Map
//1 使用map.entrySet()获取Map的键值关系映射
public static void main(String[] args) {
    Map<String, String> map = new HashMap<String, String>();
    map.put("A""高淇");
    map.put("B""高小七");
    //将键值关系映射存入Set集合中
    Set<Entry<String, String>> ss = map.entrySet();		//ss输出为:[A=高淇, B=高小七]
    for (Iterator<Entry<String, String>> iterator = ss.iterator(); 							iterator.hasNext();) 
    {
        Entry<String, String> e = iterator.next();
        System.out.println(e.getKey() + "--" + e.getValue());
    }
}
//2 使用map.keySet()获取Map的key值
public static void main(String[] args) {
    Map<String, String> map = new HashMap<String, String>();
    map.put("A", "高淇");
    map.put("B", "高小七");
    //获取Map中所有的key值,存入Set集合中
    Set<String> ss = map.keySet();		//ss输出为:[A, B]
    for (Iterator<String> iterator = ss.iterator(); iterator.hasNext();) {
        String key = iterator.next();
        System.out.println(key + "--" + map.get(key));
    }
}

6.6 Collections工具类

类 java.util.Collections 提供了对Set、List、Map进行排序、填充、查找元素的辅助方法。

  1. void sort(List) //对List容器内的元素排序,排序的规则是按照升序进行排序。

  2. void shuffle(List) //对List容器内的元素进行随机排列。

  3. void reverse(List) //对List容器内的元素进行逆续排列 。

  4. void fill(List, Object) //用一个特定的对象重写整个List容器。

  5. int binarySearch(List, Object)//对于顺序的List容器,采用折半查找的方法查找特定对象。

public static void main(String[] args) {
    List<String> aList = new ArrayList<String>();
    for (int i = 0; i < 5; i++){
        aList.add("a" + i);
    }
    System.out.println(aList);
    Collections.shuffle(aList); //随机排列
    System.out.println(aList);
    Collections.reverse(aList); //逆序
    System.out.println(aList);
    Collections.sort(aList); 	//排序
    System.out.println(aList);
    System.out.println(Collections.binarySearch(aList, "a2")); 	//二分法查找
    Collections.fill(aList, "hello");
    System.out.println(aList);
}

6.7 阻塞队列

阻塞队列是 JAVA 层面 Queue 的数据类型。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞的插入和移除:

  • 支持阻塞的插入方法:即当队列满时,队列会阻塞插入元素的线程,直到队列不满。

  • 支持阻塞的移除方法:即在队列为空时,队列会阻塞获取元素的线程,直到等待队列变为非空后再获取。

6.7.1 作用

阻塞队列,顾名思义首先它是一个队列,而一个阻塞队列在数据结构中所起的作用如下图:

  • 当阻塞队列为空时,从队列中获取元素的操作将会被阻塞。
  • 当阻塞队列为满时,向队列里添加元素的操作将会被阻塞。

在某些场景下,必须要执行阻塞操作时,阻塞队列可以避免一些不必要的操作。在多线程领域:所谓阻塞,在某些情况下会挂起线程(阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。在concurrent包发布以前,多线程环境下,需要每个程序员去自己控制这些细节,但是有了阻塞队列后,我们就不需要去关心什么时候需要阻塞线程,什么时候唤醒线程了。

6.7.2 架构介绍

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为MAX_VALUE)阻塞队列
  • SynchronousQueue:不存储元素的队列,即单个元素的队列
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  • LinkedTransferQueue:由链表组成的无界阻塞队列
  • LinkedBlockingDeque:由链表组成的双向阻塞队列
6.7.3 核心方法
方法类型抛出异常特殊值阻塞超时
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek()不可用不可用

(1)抛出异常组
  • add()
  • remove()
public static void main(String[] args) {
    // 创建一个容量为3的阻塞队列
    BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

    //1 抛出异常组:add()+remove()
    System.out.println(blockingQueue.add("a"));
    System.out.println(blockingQueue.add("b"));
    System.out.println(blockingQueue.add("c"));

//        // 当队列满时,再进行添加操作就会抛出:IllegalStateException: Queue full
//        System.out.println(blockingQueue.add("d"));

        // 检索队列,不会删除元素,会返回队列的头元素
        System.out.println(blockingQueue.element());

        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
//        // 当队列为空时,再进行移除操作就会抛出:NoSuchElementException
//        System.out.println(blockingQueue.remove());

        // 检索队列,当队列为空时,会抛出异常:NoSuchElementException
        System.out.println(blockingQueue.element());
}
(2)特殊值组
  • offer()
  • poll()

特殊值表示若队列满或空时,其返回值为布尔值或null

public static void main(String[] args) {
    // 创建一个容量为3的阻塞队列
    BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

    //2 特殊值组:offer+poll
    System.out.println(blockingQueue.offer("a"));
    System.out.println(blockingQueue.offer("b"));
    System.out.println(blockingQueue.offer("c"));
    // 队列满时,返回false布尔值(特殊值即代表布尔值)
    System.out.println(blockingQueue.offer("d"));

    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    System.out.println(blockingQueue.poll());
    // 队列为空时,返回null
    System.out.println(blockingQueue.poll());
}
(3)阻塞
  • put()
  • take()
public static void main(String[] args) {
    // 创建一个容量为3的阻塞队列
    BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
        //3 阻塞,没有返回值
        blockingQueue.put("a");
        blockingQueue.put("b");
        blockingQueue.put("c");
//        // 如果队列已满,则会一直阻塞,直到腾出空位
//        blockingQueue.put("c");

        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
//        // 当队列为空时,队列会一直阻塞,直到队列中有元素
//        System.out.println(blockingQueue.take());
}
(4)超时退出
  • offer(time)
  • poll(time)
public static void main(String[] args) throws InterruptedException {
    // 创建一个容量为3的阻塞队列
    BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);

    //4 超时
    System.out.println(blockingQueue.offer("a", 3L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("b", 3L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.offer("c", 3L, TimeUnit.SECONDS));
    // 当队列满时,该元素会等待3秒看是否有空位腾出,若有则进行添加,否则返回false
    System.out.println(blockingQueue.offer("d", 3L, TimeUnit.SECONDS));

    System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));
    System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));
    // 当队列为空时,该方法会等待3秒看是否有元素添加,若有就进行取出操作,否则就返回null
    System.out.println(blockingQueue.poll(3L, TimeUnit.SECONDS));
}
6.7.4 SynchronousQueue

SynchronousQueue不存储任何元素。每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

/**
 * SynchronousQueue不存储任何元素
 * 每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然
 */
public class MySynchronousQueue {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();

        // 创建线程AAA只负责put操作
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + "put 1...");
                blockingQueue.put("1");

                System.out.println(Thread.currentThread().getName() + "put 2...");
                blockingQueue.put("2");

                System.out.println(Thread.currentThread().getName() + "put 3...");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAA").start();

        // 创建线程BBB只负责take操作
        new Thread(() -> {
            try {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("3秒后" + Thread.currentThread().getName() + "\t take...");
                blockingQueue.take();

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("3秒后" + Thread.currentThread().getName() + "\t take...");
                blockingQueue.take();

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("3秒后" + Thread.currentThread().getName() + "\t take...");
                blockingQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"BBB").start();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我姓弓长那个张

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值