你能区分这几种Java集合类型?

1.Java集合的背景

  1. Java知识中有一个类集框架的概念,每种文献各有各的说法,也有的叫做Java集合类,造成混乱。并且概念中Collection翻译成中文又是类集的意思,Set翻译成中文是集合的意思,混乱的翻译、指代不明的名词概念以及网络上copy的差错,导致初学者从入门到放弃。现在本学习笔记将帮你梳理基础概念、供你参考解决疑惑。
  2. JDK 1.2版本开始引入类集开发框架,提供一系列接口和实现子类,最初时JDK版本都是使用Object类型实现数据接收(导致可能会存在ClassCastException抛异常安全隐患),随技术提高增加泛型进行规定,才能使算法有良好的提高

2.Java集合的概述

2.1.什么是集合?

集合框架:集合就是一个放数据的容器,准确的说是放数据对象引用的容器。

  1. 集合类存放于java.util包中。
  2. 集合类型主要有3种:set(集)、list(列表)和map(映射)。
  3. 集合存放的都是对象的引用,而非对象本身。所以我们称集合中的对象就是集合中对象的引用。

2.2.集合与数组的区别

  1. 长度区别:集合长度可变,数组长度不可变
  2. 内容区别:集合可存储不同类型元素,数组存储只可单一类型元素
  3. 元素区别:集合只能存储引用类型元素,数组可存储引用类型,也可存储基本类型

下面画出Java集合间的框架关系,如图
在这里插入图片描述
Java各集合类之间的关系

  1. Java集合要从两大接口说起,一为Collection接口,二为Map接口,都继承了Iterable接口,它们是同一个层次的
  2. Collection接口被List接口和Set接口继承
  3. List接口有三个实现类,ArrayList,LinkedList,Vector
  4. Set接口被HashSet类实现,被SortedSet接口继承,同时TreeSet类实现SortedSet接口,LinkedHashSet类继承HashSet类
  5. Map接口有两个实现类,HashMap,HashTable,同时Propertise类继承HashTable
  6. Map接口被SortedMap接口继承,同时TreeMap类实现了SortedMap接口
  7. Set与Map集合之间密切关联,Set集合的底层是维护了一个Map集合来实现的,将Map的key作为Set集合的key,value则使用的常量值

2.3.集合框架优点

  1. 使用核心集合类降低开发成本,而非实现我们自己的集合类,易扩展或修改。
  2. 随着使用经过严格测试的集合框架类,代码质量会得到提高。
  3. 通过使用JDK附带的集合类,可以降低代码维护成本。
  4. 复用性和可操作性。
  5. 允许不同类型的类集以相同的方式和高度互操做方式工作。

3.集合框架实现

3.1.Collection接口

Collection接口为单列集合的顶层接口,含有允许常见操作。

单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.List和java.util.Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有java.util.ArrayList和java.util.LinkedList,Set接口的主要实现类有java.util.HashSet和java.util.TreeSet。

// 添加方法:
add(Object o) // 添加指定元素
addAll(Collection c) // 添加指定集合

// 删除方法:
remove(Object o) // 删除指定元素
removeAll(Collection c) // 输出两个集合的交集
retainAll(Collection c) // 保留两个集合的交集
clear() // 清空集合

// 查询方法:
size() // 集合中的有效元素个数
toArray() // 将集合中的元素转换成Object类型数组

// 判断方法:
isEmpty() // 判断是否为空
equals(Object o) // 判断是否与指定元素相同
contains(Object o) // 判断是否包含指定元素
containsAll(Collection c) // 判断是否包含指定集合

在这里插入图片描述

Collection接口的常用子类如下:

  1. List集合:有序(存取顺序一致)、存在索引(可随机取值)、可重复
    a. ArrayList:常用的有序集合(数组结构,非线程安全)
    b. Vector:并发安全的有序集合,效率较低逐渐淘汰,可使用Collections集合提供的Collections.synchronizedList()方法或者CopyOnWriteArrayList集合来替代
    c. LinkedList:LinkedList 的增加和删除的操作效率更高,而查找和修改的操作效率较低,底层使用链表来维护(链表结构,非线程安全)

  2. Set集合:无序(存取顺序不一致)、无索引(内部维护一个Map结构来实现的,hashcode定位)、不可重复
    a. HashSet:无序、唯一。HashSet是基于HashMap来实现的,实现了Set接口,同时还实现了序列化和可克隆化。而集合(Set)是不允许重复值的,允许空值,非线程安全
    b. LinkedHashSet:哈希表和链表结构,LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
    c. TreeSet有序、唯一、TreeSet 是 Java 集合框架中的一种有序集合,它实现了 Set 接口,因此具有不允许重复元素的特性。与 HashSet 不同,TreeSet 使用红黑树数据结构来存储元素,这使得元素在集合中保持有序。

  3. Queue
    a. PriorityQueue: Object[] 数组来实现二叉堆
    b. ArrayQueue: Object[] 数组 + 双指针

3.2.List集合

List 子接口的特点

  1. 元素可重复性,List接口的最大特点;
  2. 有索引,可进行精准的操作某特定元素;
  3. 元素有序性,存储及取出的顺序一致;
  4. 可以存多个null;

List接口扩充的方法

public interface List<E> extends Collection<E>
// 常用方法:
public void add(int index,E element)
// 在指定位 置处增加元素
boolean addAll(int index,Collection<? extends E> c)
// 在指定位置处增加- -组元素
public E get(int index)
// 根据索引位置取出每一个元素
publice int indexOf(Object o)
// 根据对象查找指定的位置,找不到返回-1 .
public int lastIndexOf(Object o)
// 从后面向前查找位置,找不到返回-1
public ListIterator<E> listIterator(
// 返回ListIterator 接口的实例
public ListIterator<E> listIterator(int index)
// 返回从指定位置的ListIterator接口的实例
public E remove(int index)
// 删除指定位 置的内容
public E set(int index,E element)
// 修改指定位置的内容
List<E> subList(int fromIndex,int toIndex)
注:get方法比较常用

List常见的子类
在这里插入图片描述

List和Vector的比较
在这里插入图片描述

示例:

import java.util.List;
import java.util.Arrays;
 
class Solution {
    public static void main(String[] args) {
    	// List<Integer> ls1 = Arrays.asList(1, 2, null);
    	List<String> ls2 = List.of("JieWen","2","King");
		Object result[] = ls2.toArray();//将List转为Array储存
		// System.out.println(ls1.contains(null));
    	for(Object temp :  result){
    		System.out.printf(temp + "\");
		}
    }
}
// 结果:JieWen、2、King
3.2.1.ArrayList

线性结构最方便的实现就是基于数组实现

ArrayList的类定义:

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

你了解这三种构造方法?

public ArrayList() {
    this(10);
    // 调用ArrayList(10) 默认初始化一个大小为10的object数组。
}

public ArrayList(int initialCapacity) {    
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
     // 如果用户初始化大小小于0抛异常,否则新建一个用户初始值大小的object数组。                                      
    this.elementData = new Object[initialCapacity];
} 

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // 当c.toArray返回的不是object类型的数组时,进行下面转化。
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

默认情况下使用ArrayList会生成一个大小为10的Object类型的数组。也可以调用ArrayList(int initialCapacity) 来初始化Object数组的大小。并且用户可以往ArrayList中传入一个容器只要这个容器是Collection类型的。调用ArrayList(Collection<? extends E> c)接口的时候会将容器数组化处理并将这个数组值赋给Object数组。
ArrayList的特点

  • ArrayList 基于索引的,因此可以随机访问;
  • ArrayList 不太适合删除操作,删除元素将导致大量的数据移动;
  • ArrayList 可以包含重复的元素;
  • ArrayList 是非线程安全的;
  • ArrayList 维护了元素插入时的顺序。

ArrayList的底层分析

  1. 数组结构是有序的元素序列,在内存中开辟一段连续的空间,在空间中存放元素,每个空间都有编号,通过编号可以快速找到相应元素,因此查询快;
  2. 数组初始化时长度是固定的,查询快、要想增删元素,动态开辟空间、必须创建一个新数组,把源数组的元素复制进来,随后源数组销毁,耗时长,因此增删慢。

ArrayList核心源码解读

package java.util;

import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;


public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认初始容量大小
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组(用于空实例)。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 用于默认大小空实例的共享空数组实例。
    // 我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList数据的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList 所包含的元素个数
     */
    private int size;

    /**
     * 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小)
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            // 如果传入的参数大于0,创建initialCapacity大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            // 如果传入的参数等于0,创建空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            // 其他情况,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 默认无参构造函数
     * DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。
     */
    public ArrayList(Collection<? extends E> c) {
        //将指定集合转换为数组
        elementData = c.toArray();
        //如果elementData数组的长度不为0
        if ((size = elementData.length) != 0) {
            // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断)
            if (elementData.getClass() != Object[].class)
                //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // 其他情况,用空数组代替
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

    /**
     * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
	// 下面是ArrayList的扩容机制
	// ArrayList的扩容机制提高了性能,如果每次只扩充一个,
	// 那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。
    /**
     * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
     * @param   minCapacity   所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        // 如果是true,minExpand的值为0,如果是false,minExpand的值为10
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;
        // 如果最小容量大于已有的最大容量
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }
   // 1.得到最小扩容量
   // 2.通过最小容量扩容
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取“默认的容量”和“传入参数”两者之间的最大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
  	// 判断是否需要扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            // 调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

    /**
     * 要分配的最大数组大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        // 将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        // 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 再检查新容量是否超出了ArrayList所定义的最大容量,
        // 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        // 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        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);
    }
    // 比较minCapacity和 MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

    /**
     * 返回此列表中的元素数。
     */
    public int size() {
        return size;
    }

    /**
     * 如果此列表不包含元素,则返回 true 。
     */
    public boolean isEmpty() {
        // 注意=和==的区别
        return size == 0;
    }

    /**
     * 如果此列表包含指定的元素,则返回true 。
     */
    public boolean contains(Object o) {
        // indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
        return indexOf(o) >= 0;
    }

    /**
     * 返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1
     */
    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                // equals()方法比较
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    /**
     * 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。.
     */
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

    /**
     * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。)
     */
    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // 这不应该发生,因为我们是可以克隆的
            throw new InternalError(e);
        }
    }

    /**
     *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
     *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。
     *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    /**
     * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素);
     *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。
     *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。
     *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。
     *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。)
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // 新建一个运行时类型的数组,但是ArrayList数组的内容
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
            //调用System提供的arraycopy()方法实现数组之间的复制
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

    // Positional Access Operations

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

    /**
     * 返回此列表中指定位置的元素。
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }

    /**
     * 用指定的元素替换此列表中指定位置的元素。
     */
    public E set(int index, E element) {
        //对index进行界限检查
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        //返回原来在这个位置的元素
        return oldValue;
    }

    /**
     * 将指定的元素追加到此列表的末尾。
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里看到ArrayList添加元素的实质就相当于为数组赋值
        elementData[size++] = e;
        return true;
    }

    /**
     * 在此列表中的指定位置插入指定的元素。
     *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大;
     *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

    /**
     * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。
     */
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
      //从列表中删除的元素
        return oldValue;
    }

    /**
     * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。
     *返回true,如果此列表包含指定的元素
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

    /**
     * 从列表中删除所有元素。
     */
    public void clear() {
        modCount++;

        // 把数组中所有的元素的值设为null
        for (int i = 0; i < size; i++)
            elementData[i] = null;

        size = 0;
    }

    /**
     * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 将指定集合中的所有元素插入到此列表中,从指定的位置开始。
     */
    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

    /**
     * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。
     *将任何后续元素移动到左侧(减少其索引)。
     */
    protected void removeRange(int fromIndex, int toIndex) {
        modCount++;
        int numMoved = size - toIndex;
        System.arraycopy(elementData, toIndex, elementData, fromIndex,
                         numMoved);

        // clear to let GC do its work
        int newSize = size - (toIndex-fromIndex);
        for (int i = newSize; i < size; i++) {
            elementData[i] = null;
        }
        size = newSize;
    }

    /**
     * 检查给定的索引是否在范围内。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * add和addAll使用的rangeCheck的一个版本
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

    /**
     * 返回IndexOutOfBoundsException细节信息
     */
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

    /**
     * 从此列表中删除指定集合中包含的所有元素。
     */
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        //如果此列表被修改则返回true
        return batchRemove(c, false);
    }

    /**
     * 仅保留此列表中包含在指定集合中的元素。
     *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。
     */
    public boolean retainAll(Collection<?> c) {
        Objects.requireNonNull(c);
        return batchRemove(c, true);
    }


    /**
     * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。
     *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。
     *返回的列表迭代器是fail-fast 。
     */
    public ListIterator<E> listIterator(int index) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index);
        return new ListItr(index);
    }

    /**
     *返回列表中的列表迭代器(按适当的顺序)。
     *返回的列表迭代器是fail-fast 。
     */
    public ListIterator<E> listIterator() {
        return new ListItr(0);
    }

    /**
     *以正确的顺序返回该列表中的元素的迭代器。
     *返回的迭代器是fail-fast 。
     */
    public Iterator<E> iterator() {
        return new Itr();
    }

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
ArrayList底层是动态数组存在扩容说法,默认的数组大小是10,在检测是否需要扩容后,如果扩容,会扩容为原来的1.5倍大小。原理就是把老数组的元素存储到新数组里面。
使用Collections.synchronizedList(new ArrayList())生成的ArrayList可以是线程安全的。

ArrayList 常见方法:

	1.add(E e) 在集合末尾新增一个元素
	2.add(int index, E element) 在指定位置添加元素
	3.get(int index)获取指定位置的元素
	4.remove(int index) 删除指定位置的元素
	5.remove(Object o) 删除指定元素
	6.indexOf(Object o) 查询指定元素的位置 lastIndexOf也一样,只是从尾部开始遍历
	7.set(int index, E element) 设置指定位置的元素值
	8.retainAll(Collection<?> c) 求两个集合的交集

ArrayLIst 实现:

import java.util.*;
public class Daycare{
   public static void main(String[] args){
	   // ArrayList  Collections.addAll(集合,元素,....)
	   // 创建一个集合对象装名字
	   ArrayList<String> list=new ArrayList<>();
	   // 一次添加一个元素的方式:添加:Andy   Lee
	   Collections.addAll(list,"Andy","Lee");
	   // 统计集合里面有几个人的姓名
	   System.out.println("人的个数:"+list.size());
	   // 打印第一个人的姓名
	   System.out.println("第一个人的名字:"+list.get(0));
	   // 判断集合里面是否出现Lee的名字
	   System.out.println(list.contains("Lee"));
	   // 使用两种不同的方式打印 所有以A开头的名字
	   for(String name:list){
	   	   if(name.charAt(0)=='A'){
		     System.out.println(name);
		   }
	   }
       for(String name:list){
	       if(name.startsWith("A")){
		      System.out.println(name);
		   }
	   }
      // 用迭代器
      for(Iterator<String> car=list.iterator();car.hasNext();){
		  String name=car.next();
		  if(name.startsWith("A")) {
		    System.out.println(name);
		  }
	  }
}

ArrayList 保存自定义类对象

必须实现的方法:

  1. 要在自定义类中重写equals()方法
  2. 在实际开发之中 remove() 与 contains(Object o)都是很少用的
  3. 主要用到的还是 add() 方法与 get()方法
  4. 在重写equals()方法的时候要判断3个为空,之后转型[1.是否是自己 2.是否为空 3.是否为本类(instanceof判断)]

方法的实现代码:

import java.util.ArrayList;

/**
 * ArrayList保存自定义类对象
 **/
public class ArrayListSaveDefineClassInstance {
    public static void main(String[] args) {
    
        ArrayList<ArrayListPerson> all= new ArrayList<ArrayListPerson> ();
        all.add(new ArrayListPerson("张三" , 19));
        all.add(new ArrayListPerson("李四" , 20));
        all.add(new ArrayListPerson("王五" , 12));

        // 由于地址不一样,无法找到也无法,删除,所以要在自定义类中复写equals()方法
        System.out.println(all.contains(new ArrayListPerson("王五" , 12)));
        all.remove(new ArrayListPerson("王五" , 12));

        System.out.println(all);
    }
}


/**
 1. 自定义类
 **/
class ArrayListPerson{
    private  String name;
    private  int age;
    // 需要覆写的equals()方法才能保证能够实现查找
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ArrayListPerson)) {
            return false;
        }

        if (this == obj) {
            return false;
        }

        if (obj == null) {
            return false;
        }

        ArrayListPerson objP = (ArrayListPerson) obj;
        return this.name==objP.name && this.age==objP.age;
    }

    @Override
    public String toString() {
        return "ArrayListPerson{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public ArrayListPerson(String name , int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
3.2.2.LinkedList

LinkedList 概述:
LinkedList 是 Java 集合中比较常用的数据结构,与 ArrayList 一样,实现了 List 接口,只不过 ArrayList 是基于数组实现的,而 LinkedList 是基于链表实现的。所以 LinkedList 插入和删除方面要优于 ArrayList,而随机访问上则 ArrayList 性能更好。除了 List 接口。之外,LinkedList 还实现了 Deque,Cloneable,Serializable 三个接口。这说明该数据结构支持队列,克隆和序列化操作的。与 ArrayList 一样,允许 null 元素的存在,且是不支持多线程的。

LinkedList 特点:

  1. 数据结构由双向链表实现
  2. 它不存在容量不足的问题
  3. 因为是链表无下标,所以查询慢、但是增删快。

构造方法:

public LinkedList() {}
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList 提供了两个构造方法:LinkedList() 和 LinkedList(Collection<? extends E> c)。

LinkedList 实现原理:

LinkedList是基于双向链表的数据结构实现的,链表是可以占用一段不连续的内存,双向链表有前驱节点和后驱节点,里面存储的是上一个元素和后一个元素所在的位置,中间的黄色部分就是业务数据了,当我们需要执行插入的任务,比如第一个元素和第二个元素之间,只需要改变他们的前驱节点和后驱节点的指向就可以了,不要像动态数组那么麻烦,删除也是同样的操作,但是因为是不连续的内存空间,当需要执行查找,需要从第一个元素开始查找,直到找到我们需要的数据。

在这里插入图片描述

▶LinkedList 常用方法:

// 增加:
add(E e) // 在链表后添加一个元素;   通用方法
addFirst(E e) // 在链表头部插入一个元素;  特有方法
addLast(E e) // 在链表尾部添加一个元素;  特有方法
push(E e) // 与addFirst方法一致  
offer(E e)  // 在链表尾部插入一个元素        
add(int index, E element)  // 在指定位置插入一个元素。                                                                                                                                          				       
offerFirst(E e)  // JDK1.6版本之后,在头部添加; 特有方法 
offerLast(E e)  // JDK1.6版本之后,在尾部添加; 特有方法

// 删除:
remove()  // 移除链表中第一个元素;    通用方法  
remove(E e)  // 移除指定元素;   通用方法
removeFirst(E e)  // 删除头,获取元素并删除;  特有方法
removeLast(E e)  // 删除尾;  特有方法
pollFirst()  // 删除头;  特有方法
pollLast()  // 删除尾;  特有方法
pop()  // 和removeFirst方法一致,删除头。 
poll()  // 查询并移除第一个元素     特有方法    

// 查:
get(int index)  // 按照下标获取元素;  通用方法
getFirst()  // 获取第一个元素;  特有方法
getLast()  // 获取最后一个元素; 特有方法
peek()  // 获取第一个元素,但是不移除;  特有方法
peekFirst()  // 获取第一个元素,但是不移除; 
peekLast()  // 获取最后一个元素,但是不移除;
pollFirst()  // 查询并删除头;  特有方法
pollLast()  // 删除尾;  特有方法
poll()  // 查询并移除第一个元素     特有方法

LinkedList 的实现方法:

public class LinkedListMethodsDemo {
    public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<>();
 
        linkedList.add("first");
        linkedList.add("second");
        linkedList.add("third");
        System.out.println(linkedList);
 
        linkedList.addFirst("addFirst");
        System.out.println(linkedList);
 
        linkedList.addLast("addLast");
        System.out.println(linkedList);
 
        linkedList.add(2, "addByIndex");
        System.out.println(linkedList);
    }
}
/* 输出结果:
[first, second, third]
[addFirst, first, second, third]
[addFirst, first, second, third, addLast]
[addFirst, first, addByIndex, second, third, addLast] */
3.3.3.Vector

注:Vector类在JDK1.0就已经提供,但是由于以下:

  1. 因为vector是线程安全的,所以效率低,这容易理解,类似StringBuffer
  2. Vector空间满了之后,扩容是一倍,而ArrayList仅仅是一半
  3. Vector分配内存的时候需要连续的存储空间,如果数据太多,容易分配内存失败
  4. 只能在尾部进行插入和删除操作,效率低

所以vector子类目前已经过时,但是由于其广泛性并没有弃用。

Vector底层分析:

底层数据结构:数组实现,和ArrayList 一样,但是Vector 是同步方法线程安全,但是效率低,所以不推荐使用,只有要求需要线程安全才会去使用该子类;

  1. Vector有四个不同的构造函数。 无参构造的容量默认值为10
  2. 当容量不足以容纳当前的元素个数时,就看构造方法中传入的容量增长系数CapacityIncrement是否为0,如果不为0,就设置新的容量为 旧容量 + 容量增长量;如果为0,设置新的容量为旧的容量的2倍,如果设置后的容量还不够,则直接新的容量设置为 旧容量 + 传入参数所需要的容量 而后同样用Arrays.copyof()方法将元素拷贝到新的数组

Vector 所定义结构

java.lang.Object 
    java.util.AbstractCollection<E> 
        java.util.AbstractList<E> 
            java.util.Vector<E> 

示例:

public class VectorTest{
	public static void main(String[] args){
		List<String> all = new Vector<String>();
		all.add("JieWen");
		System.out.printf(all);
	}
}

3.3.Set 集合

提供该接口原因:
为了与List接口的使用有所区分,在进行Set接口设计的时候要求内部不允许保存重复的元素,Set接口定义如下:

public interface Set<E> extends Collection<E>(){}

在这里插入图片描述

Set接口的使用

public class SetTest{
	public static void main(String[] args){
		Set<String> all = new Set.of("JieWen","XiaoFei");
		System.out.printf(all);
	}
}

Set的特性

  1. 存入集合的顺序和取出集合的顺序不一致
  2. 没有索引,所以不能使用普通for循环,可Foreach循环
  3. 存入集合的元素没有重复(如果重复会抛出异常)特点

基本和Collection接口的方法一种,没有进行功能的扩充。
Set的子类
在这里插入图片描述
Set常用的方法

Set 接口继承 Collection 接口,而且它不允许集合中存在重复项。所有原始方法都是现成的,没有引入新方法。具体的 Set 实现类依赖添加的对象的 equals() 方法来检查等同性。

public int size() // 返回set中元素的数目,如果set包含的元素数大于Integer.MAX_VALUE,返回Integer.MAX_VALUE;
public boolean isEmpty() // 如果set中不含元素,返回true ;
public boolean contains(Object o) // 如果set包含指定元素,返回true ;
public Iterator iterator()   //返回set中元素的迭代器,元素返回没有特定的顺序,除非set提高该保证的某些类的实例 ;
public boolean add(Object o) // 如果set中不存在指定元素,则向set加入;
public boolean remove(Object o) // 如果set中存在指定元素,则从set中删除 ;
public boolean removeAll(Collection c) // 如果set包含指定集合,则从set中删除指定集合的所有元素 ;
public void clear() // 从set中删除所有元素;
3.3.1.HashSet

数据结构
JDK 1.8之前是通过哈希表的单向链表和数组来组成、JDK 1.8之后将单链表该为(单链表+红黑树)目的在于:当链表的长度超过阀值(8)时改为红黑树,红黑树转变为链表的值为(6)、而链表查找元素的时间复杂度为 O(n),远远大于红黑树的 O(logn),尤其是在节点越来越多的情况下,O(logn) 体现出的优势会更加明显;简而言之就是为了提升查询的效率。装载因子(0.75f),

HashSet 的特性:(散列存放)

  1. 元素的无序性 != 随机性。真正的无序性,是指元素在底层存储的位置是无序的。(哈希表通过 hashcode 哈希值和 equals() 方法进行判断保证独一性)
  2. 不可重复性。当向Set集合中添加相同元素的时候,后面的这个添加不进去。程序不会报错!
  3. Set集合没有自己独有的方法,它的方法都是继承于Collection接口中的现有方法
  4. HashSet类是非线程安全的
  5. 允许集合元素值为null

如果你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
HashSet实现了 Map 接口 实现 Set 接口
HashMap存储键值对 HashSet仅存储对象
HashMap调用 put()向 map 中添加元素 HashMap调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcode HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

HashSet的构造方法

public class HashSet<E> extends AbstractSet<E>   
 implements Set<E>, Cloneable, java.io.Serializable  {}

方法概览

// 增加元素
add(Object o)
addAll(Collection c)
// 判断元素是否存在
contains(Object o)
containsAll(Collection c) 
// 判断集合是否为空
isEmpty()  
// 删除元素
remove(Object o)
removeAll(Collection c)  
// 返回集合的大小
size() 
// 清空集合
clear()  
// 迭代器
iterator()  
// 将内容转到数组中
toArray()  

示例:

import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;

public class Test {
	public static void main(String args[]) {
		int[] a= {6,5,4,3,2,1,1,5,6,7};
		HashSet<Integer> hs=new HashSet<>();       //所有的集合中存储的元素的类型都不会是基本数据类型!!!使用菱形推断创建对象
		for(int i:a) {
			if(!hs.add(i)) {
				System.out.println(i+"已存在!");       //HashSet中的元素不允许重复!
			}
			hs.add(i);
		}
		/*for(int i=0;i<a.length;i++) {
			if(!hs.add(a[i]))
				System.out.println(a[i]+"已存在!");
			hs.add(a[i]);
		}*/
		if(!hs.isEmpty()) {                 //判断集合是否为空
			System.out.println("hs不为空");
		}
		if(hs.contains(7)) {               //判断集合是否包含元素
			System.out.println("集合hs中包括元素7");
		}
		for(int j : hs) {
			System.out.print(j+"  ");
		}
		System.out.println();
		HashSet<Integer> h=new HashSet<>();
		h.add(102);           // 添加元素102
		// h.remove(102);        删除元素102
		h.add(103);
		h.add(2);
		if(hs.containsAll(h)) {           // 判断集合是否包含另一集合的所有元素
			System.out.println("hs集合中包含h集合中的所有元素");
		}
		else {
			System.out.println("hs集合中不包含h集合中的所有元素");
		}
		// hs.addAll(h);            将集合h中的所有元素添加到hs中
		hs.removeAll(h);         // 删除hs和h的交集中的元素
		// hs.retainAll(h);         取得hs和h的交集的元素
		// hs.clear();              删除所有hs的元素
		Object[] objects=hs.toArray();    //HashSet.toArray()的无参方法
		for(Object o:objects) {
			System.out.print(o+"  ");
		}
		System.out.println();
		Integer[] mid=hs.toArray(new Integer[hs.size()]);           
		// HashSet.toArray()的带参方法
		for(Integer integer:mid) {
			System.out.print(integer+"  ");
		}
		System.out.println();
		hs.removeAll(java.util.Collections.singleton(7));    
		// Collections.singleton(7):生成只有一个元素7的集合
		Iterator<Integer> iterator=hs.iterator();
		while(iterator.hasNext()) {
			System.out.print(iterator.next()+"  ");
		}
		System.out.println();	
	}
}

HashSet如何检查重复?
当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

// Returns: true if this set did not already contain the specified element
// 返回值:当 set 中没有包含 add 的元素时返回真
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
}
3.3.2.TreeSet

数据结构:红黑树

TreeSet特性

  1. TreeSet中存储的类型必须是一致的,不能一下存int,一下又存string
  2. TreeSet在遍历集合元素时,是有顺序的【从小到大】(ASCLL码排序)
  3. 排序:当向TreeSet中添加自定义对象时,有2种排序方法,1:自然排序 2、定制排序,自然排序:要求自定义类实现java.lang.Comparable接口并重写compareTo(Object obj)方法。在此方法中,指明按照自定义类的哪个属性进行排序

底层分析

TreeSet的底层是TreeMap,添加的数据存入了map的key的位置,而value则固定是PRESENT。TreeSet中的元素是有序且不重复的,因为TreeMap中的key是有序且不重复的。
注意: 不允许存放null。

示例

public class TreeTest{
    public static void main(String[] srgs){
    Set<String> all = new Set<>();
    all.add("JieWen");
    System.out.printf(all);
	}
}

TreeSet 子类排序分析

即类要实现Comparable接口,并重写compareTo()方法,TreeSet对象调用add()方法时,会将存入的对象提升为Comparable类型,然后调用对象中的compareTo()方法进行比较,判断有没有重复元素,根据比较的返回值进行存储。
因为TreeSet底层是二叉树,当compareTo方法返回0时,不存储;当compareTo方法返回正数时,存入二叉树的右子树;当compareTo方法返回负数时,存入二叉树的左子树。如果一个类没有实现Comparable接口就将该类对象存入TreeSet集合,会发生类型转换异常。

自然排序

import java.util.Set;
import java.util.TreeSet;
// TreeSet的自然排序
// 1.实体类实现Comparable接口,重写compareTo()方法
public class TestTreeSetSort1 {
	public static void main(String[] args) {
		Set<Student> tree=new TreeSet();
		Student s1=new Student("JieWen",21,88);
		Student s2=new Student("Feign",21,34);
		Student s3=new Student("K",24,99);
		Student s4=new Student("J",27,13);
		Student s5=new Student("N",22,65);
		tree.add(s1);
		tree.add(s2);
		tree.add(s3);
		tree.add(s4);
		tree.add(s5);
		// Exception in thread "main" java.lang.ClassCastException: com.qianfeng.kxf.day25.set.Student cannot be cast to java.lang.Comparable
		// 为什么报错?因为添加的是对象元素,必须实现一个排序,否则就会类型转换错误的问题
		System.out.println("元素对象多少个"+tree.size());
		for(Student s:tree) {
			System.out.println(s);
		}
		
	}
}
class Student implements Comparable{
	private String name;
	private int age;
	private int score;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public int getScore() {
		return score;
	}
	public void setScore(int score) {
		this.score = score;
	}
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + ", score=" + score + "]";
	}
	public Student(String name, int age, int score) {
		super();
		this.name = name;
		this.age = age;
		this.score = score;
	}
	public Student() {
		super();
	}
	
	/*//重写该方法:只通过名字进行排序
	@Override
	public int compareTo(Object o) {
		// 先转换为Student类型对象
		Student str=(Student)o;
		// 先对姓名进行排序,直接调用String类型中的compareTo()方法进行比较
		return this.name.compareTo(str.name);
	}
//	元素对象多少个3
//	Student [name=J, age=27, score=13]
//	Student [name=K, age=24, score=99]
//	Student [name=JieWen, age=21, score=88]
*/	
	
	/*//如果名字相同,就按年龄排序
	public int compareTo(Object o) {
		Student stu=(Student)o;
		//先对名字排序:升序
		int n=this.name.compareTo(stu.name);
		//如果名字相同就比较年龄 :升序
		return n==0?this.age-stu.age:n;
	}*/
	//如果名字相同,就按年龄排,如果年龄还相同就按成绩排
	public int compareTo(Object o) {
		Student stu=(Student)o;
		int n=this.name.compareTo(stu.name);
		int a=n==0?this.age-stu.age:n;
		return a==0?this.score-stu.score:a;
	}
}

定制排序

//主要实现方法
class MyComparator implements Comparator<Person>{
	
	// 按姓名排序
	/*@Override
	public int compare(Person o1, Person o2) {
		int n=o1.getName().compareTo(o2.getName());
		return n;
	}*/
	
	// 按姓名和年龄排序
	/*public int compare(Person p1,Person p2) {
		int n=p1.getName().compareTo(p2.getName());
		return n==0?p1.getAge()-p2.getAge():n;
	}*/
	
	// 按姓名.年龄.成绩排序
	public int compare(Person p1,Person p2) {
		int n = p1.getName().compareTo(p2.getName());
		int a = n == 0 ? p1.getAge() - p2.getAge() : n;
		return a == 0 ? p1.getScore() - p2.getScore() : a;
	}
}

消除重复元素

由于TreeSet有排序要求,所以利用Comparable接口实现重复元素判断,而非排序集合中对重复元素的判断Object类提供的hash码,boolean equals()对象比较。

代码实现

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    People people = (People) o;
    return Objects.equals(name, people.name) &&
            Objects.equals(age, people.age);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

@Override
public int compareTo(People o) {
    if(this.age > o.age){
        return 1;
    } else if (this.age < o.age){
        return -1;
    } else {
        return this.name.compareTo(o.name);
    }
}

3.3.3 LinkedHashSet

目的:为了解决HashSet实现类的无序性而创建的一个类,实现基于链表的顺序储存
数据结构:和HashSet基本一致,只是将单链表改为双向链表

LinkedHashSet 特性

>查询快、元素有序、元素不可重复、没有索引。

底层分析

为HashSet的子类,只是比他多添加一条记录元素顺序的链表,所以其为有序;

//从键盘输入字符串,去掉重复字符输出
public static void getSingleChar(){
    Scanner sc=new Scanner(System.in);
    System.out.println("请输入一串字符:");
    String str=sc.nextLine();
    char[] ch=str.toCharArray();
    LinkedHashSet<Character> linkedHashSet=new LinkedHashSet<>();
    for (char c:ch) {
        linkedHashSet.add(c);
    }
    for (Character c:linkedHashSet) {
        System.out.println(c);
    }

}

3.4 Map集合

概述

Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,它完成有可能在我们处理一对一数据的时候,在编程上提供快速通道。这里说下map内部数据的组织,map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的;
(key,value)即键值对,key不允许重复,value允许重复,一一对应;
Map属于双列集合,保证key唯一,必须重写hashCode 和equals()方法;

Map的子实现

  1. HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
  2. LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  3. Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
  4. TreeMap: 红黑树(自平衡的排序二叉树)

Map常用方法

// 添加、删除、修改操作:
// 将指定key-value添加到(或修改)当前map对象中
Object put(Object key,Object value)
// 将m中的所有key-value对存放到当前map中
void putAll(Map m)
// 移除指定key的key-value对,并返回value
Object remove(Object key)
// 清空当前map中的所有数据
void clear()

// 元素查询的操作:
// 获取指定key对应的value
Object get(Object key)
// 是否包含指定的key
boolean containsKey(Object key)
// 是否包含指定的value
boolean containsValue(Object value)
// 返回map中key-value对的个数
int size()
// 判断当前map是否为空
boolean isEmpty()
// 判断当前map和参数对象obj是否相等
boolean equals(Object obj)

// 元视图操作的方法:
// 返回所有key构成的Set集合
Set keySet()
// 返回所有value构成的Collection集合
Collection values()
// 返回所有key-value对构成的Set集合
Set entrySet()

注:JDK1.9后为了方便进行Map数据的操作,添加了Map.of()的方法接受每一组数据转为Map;

public class MapTest{
    public static void main(String[] srgs){
    // one =1,two =2;
    Map<String,Integer> map = Map.of("one",1,"two",2);//k,v型
    System.out.printf(map);
    // {two=2},{one=1}
	}
}

注意:

  1. 如果设置的key重复,则程序抛出IllegalArgumentException异常,提示key重复
  2. 如果设置的key或value为null,则抛出NullPointerException异常;

Map和Collection之间的区别

  1. Collection接口是目的为了设置内容再输出
  2. Map接口设置完内容再查找
3.4.1.HashMap

概述

HashMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即存储的对象是 Entry (同时包含了 Key 和 Value) 。在HashMap中,其会根据hash算法来计算key-value的存储位置并进行快速存取。特别地,HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null。此外,HashMap 是 Map 的一个非同步的实现。不同步导致可能多个线程同时写该方法;

数据结构:

  1. JDK8之前:HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
    所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
  2. JDK8之后:相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

JDK8之后扰动函数逻辑:

  1. 使用key.hashCode()计算hash值并赋值给变量h;
  2. 将h向右移动16位;
  3. 将变量h和向右移16位的h做异或运算(二进制位相同为0,不同为1)。
hashCode()  					  	  00000000 11111111 00000000 00001010 hash值
hashCode() >>> 16 				  	  00000000 00000000 00000000 11111111 右移16hashCode() ^ (hashCode() >>> 16)  	  00000000 11111111 00000000 11110101 异或高低位(相同为0,不相同为1)
hashCode() ^ (hashCode() >>> 16) & 15 00000000 00000000 00000000 00001111 与运算下标(都为1则为1,否则为0)
        							  00000000 00000000 00000000 00000101
                                                                   = 0101 
                                                                   = 5

HashMap的特性

  1. 允许key/value为null,但是key只能有一个null
  2. 非线程安全,多个线程同时操作同一个HashMap实例所做的修改在线程间不同步
  3. 遍历时不保证任何顺序,跟元素插入顺序或者访问顺序无关
  4. 进行遍历时如果执行HashMap的remove(Object key)或者put(Object value)方法时会快速失败,抛出异常ConcurrentModificationException。遍历时删除元素只能通过Iterator本身的remove()方法实现。

HashMap常用方法

// 在此映射中关联指定的Key-value
put(Object key,Object value)
// 在此映射中将指定的映射关系添加到被操作的映射中
putAll(Collection c)
// 根据key获取指定的value
get(Object key)
// 检测该映射中是否存在指定key的映射,有则返回true;没有则返回false
containsKey(Object key)
// 检测该映射中是否存在指定value的映射,有则返回true;没有则返回false
containsValue(Object value)
// 根据key的值删除指定的映射关系
remove(Object key)
// 测试映射是否为空
isEmpty()
// 返回值的集合
values()

HashMap方法实现

public class HashMap{
    public static void main(){
		Map<String,Integer> map = new HashMap<>();
		map.put("one",1);
		map.put("two",2);
		map.put("null",0);
		map.put("one",101);// 就近原则覆盖前面的
		System.out.printf(map.get("ten")); // 不存在则返回null;
	}
}

迭代器输出

Map<String, String> map = new HashMap<>();
       
Iterator iter = map.entrySet().iterator();// 比Set多一步
while (iter.hasNext()) {
	Map.Entry entry = (Map.Entry) iter.next();
	System.out.println("[Key] : " + entry.getKey() + " [Value] : " + entry.getValue());
}
3.4.2 LinkedHashMap

数据结构:和HashMap基本一致,只是将单链表改为双向链表

LinkedHashMap 特性:

迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。而LinkedHashMap,它虽然增加了时间和空间上的开销,通过一个双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序有两种,可以是插入顺序或者是访问顺序。LinkedHashMap继承了HashMap类,有着HashMap的所有功能,还提供了记住元素添加顺序的方法,通过链表实现;
key不可重复但是可以为null;value可以重复;

LinkedHashMap 定义:

 public class LinkedHashMap<K,V> extends HashMap<K,V>
    implements Map<K,V>{}

LinkedHashMap方法实现和上面 HashMap 基本一致。

3.4.3 HashTable

概述

早期的字典实现类(通过哈希值查找),可以更方便实现数据查询;但是这个子类比较古老,目前已经不推荐使用该类,因为该方法是同步方法,线程安全但是效率比较低;同时Hashtable不允许储存null数据,无序;

Hashtable 的定义

public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

该子类的保存数据和Map类一致 ;
:目前将方法contains()改为以下两种:containsValue() 值判断以及containsKey() 键判断;

还有一种实现线程安全: 使用ConcurrentHashMap

其实现原理是Hashtable是对整个表进行加锁,而ConcurrentHashMap是把表进行分段,初始情况下分成16段,每一段都有一把锁,当多个线程访问不同的段时,因为获取到的锁是不同的,所以可以并行的访问。效率比Hashtable高多了,推荐使用。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

3.4.4 TreeMap

概述

TreeSet是一个有序集合,可以以任意顺序将元素插入到集合中,在对集合进行遍历的时候,每个元素将自动按照排序后的顺序呈现。底层使用的是二叉树(更具体点是红黑树)实现,对于元素之间排序,如果不指定自定义的比较器Comparator,那么插入的对象 必须实现Comparable 接口,元素按照实现此接口的compareTo()方法去排序。如果指定了自定义的比较器Comparator,优先使用Comparator去对元素进行排序。比较规则决定了元素是否可以重复,以及元素的排序结果。

数据结构:红黑树

TreeMap 特性

  1. 保存的元素按照一定的规则有序
  2. 可自定义排序规则
  3. 非线程安全

底层分析:和TreeSet的相类似;

TreeMap 排序方法

public class MapTest{
    public static void main(String[] srgs){
    //one =1,two =2;
    Map<String,Integer> map = new TreeMap<>();//k,v型
    map.put("A",1);
    map.put("C",3);
    map.put("B",2);
    System.out.printf(map);
    // 结果:
    // {A=1,B =2,C= 3}//自然排序默认
	}
}

自然排序和定制排序和TreeSet一致(因为TreeSet保存的值为Map的键Key储存的值)利用Key值来进行排序;

hashCode()与equals()的相关规定:

  1. 如果两个对象相等,则hashcode一定也是相同的 两个对象相等,对两个equals方法返回true
  2. 两个对象有相同的hashcode值,它们也不一定是相等的 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  3. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
3.4.5 Map.Entry

概述

在Map类设计是,提供了一个嵌套接口(static修饰的接口):Entry。Entry将键值对的对应关系封装成了对象,即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。可以获取相应的值;

示例:

public class MapEntryTest{
	public static void main(String[] args){
		Map.Entry<String,Integer> entry= Map.entry("one",1);
		System.out.printf(entry.getKey());// 获取Key
		System.out.printf(entry.getValue());// 获取Value
	}
}

4.Iterator 迭代输出

概述

Java集合框架的集合类,我们有时候称之为容器。容器的种类有很多种,比如ArrayList、LinkedList、HashSet…,每种容器都有自己的特点,ArrayList底层维护的是一个数组;LinkedList是链表结构的;HashSet依赖的是哈希表,每种容器都有自己特有的数据结构。
  
因为容器的内部结构不同,很多时候可能不知道该怎样去遍历一个容器中的元素。所以为了使对容器内元素的操作更为简单,Java引入了迭代器模式!

Iterator中常用方法

public boolean hasNext();// 判断下一个是否有值
public E next();// 先移动到下一位置取出元素;
public default void remove();// 移除当前元素

示例:

public class Demo {
	public static void main(String[] args) {
		Collection coll = new ArrayList();
		// List<String> list = new ArrayList();//List和Set都需要先获取iterator方法
		/* 
		Set<String> all = Set.of("1","2","3");
		Iterator<String> iter = all.iterator();
		*/
		coll.add("aaa");
		coll.add("bbb");
		coll.add("ccc");
		coll.add("ddd");
		System.out.println(coll);
		Iterator it = coll.iterator();
		while (it.hasNext()) {
			it.next();
			it.remove();
		}
		System.out.println(coll);
	}
}

注意:

  1. next()方法和remove()连用,使用remove()必须使用next();
  2. 迭代出来的元素只是迭代器copy了原集合的元素或者对象,并没有对元素类进行改变,只是引用;

怎么使用迭代器?
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

  • 30
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值