Java集合框架详解(源码详解)

java集合框架

集合可以看作是一种容器,用来存储对象信息。所有集合类都位于java.util包下,但支持多线程的集合类位于java.util.concurrent包下。

img

Collection接口

  • 所有已知实现常用类:
    1. List接口
      • ArrayList
      • LinkedList
      • Vextor
    2. Set接口
      • HashSet
      • TreeSet
    3. Queue接口
      • ArrayBlockingQueue
      • LinkedBlockingQueue

常见方法

返回值方法
booleanadd(E e) 确保此集合包含指定的元素(可选操作)。
booleanaddAll(Collection<? extends E> c) 将指定集合中的所有元素添加到此集合(可选操作)。
voidclear() 从此集合中删除所有元素(可选操作)。
booleancontains(Object o) 如果此集合包含指定的元素,则返回 true
booleancontainsAll(Collection<?> c) 如果此集合包含指定 集合中的所有元素,则返回true。
booleanequals(Object o) 将指定的对象与此集合进行比较以获得相等性。
inthashCode() 返回此集合的哈希码值。
booleanisEmpty() 如果此集合不包含元素,则返回 true
Iterator<E>iterator() 返回此集合中的元素的迭代器。
default Stream<E>parallelStream() 返回可能并行的 Stream与此集合作为其来源。
booleanremove(Object o) 从该集合中删除指定元素的单个实例(如果存在)(可选操作)。
booleanremoveAll(Collection<?> c) 删除指定集合中包含的所有此集合的元素(可选操作)。
default booleanremoveIf(Predicate<? super E> filter) 删除满足给定谓词的此集合的所有元素。
booleanretainAll(Collection<?> c) 仅保留此集合中包含在指定集合中的元素(可选操作)。
intsize() 返回此集合中的元素数。
default Spliterator<E>spliterator() 创建一个Spliterator在这个集合中的元素。
default Stream<E>stream() 返回以此集合作为源的顺序 Stream
Object[]toArray() 返回一个包含此集合中所有元素的数组。
<T> T[]toArray(T[] a) 返回包含此集合中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。

方法详细信息及其使用

public class CollectionDemo01 {
    public static void main(String[] args) {
        List list = new ArrayList();
        //添加字符串
        list.add("字符串1");
        list.add("字符串2");
        list.add("字符串3");
        System.out.println(list);
        List list1 = new ArrayList();
        list1.add("字符串4");
        list1.add("字符串5");
        list1.add("字符串6");
        list.addAll(list1);   //将addAll将集合list1添加到list集合中
        System.out.println(list);

        System.out.println(list.size());
        //返回list集合的大小  6
        System.out.println(list.contains("字符串2"));
        //集合list是否包含"字符串2" 返回true
        System.out.println(list.containsAll(list1));
        //集合list是否包含list1中的元素, 返回true
        System.out.println(list.remove("字符串6"));
        //移除指定的元素"字符串6" 返回true
        System.out.println(list.get(1));
        //获取指定下标的元素
        System.out.println(list.set(2,"字符串7"));
        //将下标为2的元素替换为"字符串7"元素
        list.clear();   // 清空list集合中所有元素
        System.out.println(list);
        // list.add(new Pet("宠物猫")); 报错,实际参数列表和形式参数列表长度不同,同一个ArrayList只能存贮同一种对象
        List list2 =  new ArrayList();
        list2.add(new Pet("宠物猫",15));
        list2.add(new Pet("宠物狗",10));
        list2.add(new Pet("仓鼠",5));
        System.out.println(list2);
        list2.hashCode();  //获取list2的哈希值

    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Pet{
    String name;
    int age;
}

迭代器Iterator

public class CollectionDemo02 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book("西游记",50));
        list.add(new Book("水浒传",60));
        list.add(new Book("红楼梦",64));
        Iterator iterator = list.iterator();
        //获取迭代器,快捷代码itit
        while(iterator.hasNext()){
            // iterator.hasNext() 判断是否存在迭代元素
            //  iterator.next()    获取迭代元素,指针下移
            System.out.println(iterator.next());
        }
        iterator.next();
        //报错:Exception in thread "main" java.util.NoSuchElementException
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    String name;
    int price;
}

注意:想再次迭代操作,需要重新定义迭代器,因为上一次的迭代指针在最低端,不刷新迭代器则会报NoSuchElementException异常

for循环与增强for循环遍历

public class CollectionDemo03 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book01("西游记",50));
        list.add(new Book01("水浒传",60));
        list.add(new Book01("红楼梦",64));

        //增强for循环,底层实现也是iterator迭代器,可用debug跟踪检测
        for (Object o : list) {
            System.out.println(o);
        }
        //普通for循环实现遍历
        for (int i = 0; i < list.size()-1; i++) {
            System.out.println(list.get(i));          
        }
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Book01{
    String name;
    int price;
}

手写集合的冒泡排序

public class CollectionDemo04 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Book02("西游记",80));
        list.add(new Book02("水浒传",60));
        list.add(new Book02("红楼梦",64));
        sort(list);
        for (Object o : list) {
            System.out.println(o);
        }
    }
    public static void sort(List list){
        for (int i = 0; i <= list.size()-1; i++) {

            for (int j = 0; j <= list.size()-1-1; j++) {
                Book02 book1 = (Book02) (list.get(j));
                Book02 book2 = (Book02) (list.get(j+1));
                if(book1.getPrice() > book2.getPrice()){
                    //大于 使用set更新覆盖
                    list.set(j,book2);
                    list.set(j+1,book1);
                }
            }
        }
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book02{
    String name;
    int price;
}

List接口

ArrayList

ArrayList介绍

  • 可调整大小的数组的实现List接口。 实现所有可选列表操作,并允许所有元素,包括null 。 除了实现List 接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。
  • size,isEmpty,get,set,iteratorlistIterator操作在固定时间内运行。 add操作以摊余常数运行 ,即添加n个元素需要O(n)个时间。 所有其他操作都以线性时间运行(粗略地说)。 与LinkedList实施相比,常数因子较低。
  • 每个ArrayList实例都有一个容量 。 容量是用于存储列表中的元素的数组的大小。 它总是至少与列表大小一样大。 当元素添加到ArrayList时,其容量会自动增长。 没有规定增长政策的细节,除了添加元素具有不变的摊销时间成本。
  • 应用程序可以添加大量使用ensureCapacity操作元件的前增大ArrayList实例的容量。 这可能会减少增量重新分配的数量。

请注意,此实现不同步 。如果多个线程同时访问884457282749实例,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作,或明确调整后台数组的大小;仅设置元素的值不是结构修改。)这通常是通过在一些自然地封装了列表。 如果没有这样的对象存在,列表应该使用Collections.synchronizedList方法“包装”。 这最好在创建时完成,以防止意外的不同步访问列表:

  List list = Collections.synchronizedList(new ArrayList(...)); 

构造方法

  • ArrayList() 构造一个初始容量为十的空列表。

  • ArrayList(Collection<? extends E> c) 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。

  • ArrayList(int initialCapacity) 构造具有指定初始容量的空列表。

扩容机制

ArrayList中维护了一个Object类型的数组elementData

transient Object[] elementData;  //transient表示该属性不会被序列化
  • 初始化ArrayList实例时,没有指定大小,默认为0,但添加数据时,会进行第一次扩容,扩容大小为10,但添加数据量大于10时,进行第二次扩容,扩容大小是原来的1.5倍,即为15,后面的扩容也遵循1.5的扩容机制

  • 初始化ArrayList实例并指定大小时,例如new ArrayList(8) ,添加的数据量大于8时进行扩容,大小是原来的1.5倍,即12

源码分析

ArrayList.java中扩容机制涉及源码如下

//注:DEFAULT_CAPACITY常量为10  size=0  elementData为一个Object数组  
private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
private int size;
//定义一些变量,先不用理解

//没有指定Arraylist大小
public ArrayList() {
    //  先创建一个空的elementData数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//指定ArrayList大小
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //如果指定了数值大小大于0,直接创建一个该指定大小的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果指定的数值大小为0,则创建一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果传入的参数不合法(传入负数或则不是数值类型),报IllegalArgumentException不合法参数异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

//添加元素操作
public boolean add(E e) {
     //添加一个泛型e(即任意类型的变量)
    //ensureCapacityInternal: 保证容量充足,是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
    //将元素添加到elementData数组中
        elementData[size++] = e;
    //添加成功返回true
        return true;
    }


public void trimToSize() {
        modCount++; //计数器,统计添加元素的次数
        if (size < elementData.length) {   
            
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

//
 public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            //如果elementData不为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,将minExpand设置为0
            //也就是当不走无参构造时,将minExpand设置为0,即不需要第一次扩容为10的这个操作 minExpan:最小扩容
            //否则,将minExpand设置为10 ,也就是无参构造 第一次扩容,大小为10  DEFAULT_CAPACITY=10
            // 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) {
            //如果最小的容量小于最小扩容,也就是指定的大小小于此时的minExpand 执行ensureExplicitCapacity方法
            ensureExplicitCapacity(minCapacity);
        }
    }
private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,相当于走有参构造,即已经指定大小
            //则将指定的大小和10比较大小,选出最大值作为新的minCapacity
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
       //执行ensureExplicitCapacity并传入新的minCapacity
        ensureExplicitCapacity(minCapacity);
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;  //计数,记录添加元素的个数
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //如果minCapacity大于elementData数组的长度,即执行grow方法,实现正真扩容
            grow(minCapacity);
    }
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//正真实现扩容
private void grow(int minCapacity) {
        // overflow-conscious code
        //Capacity :容量
        int oldCapacity = elementData.length; 
        //将elementData的长度赋值oldCapacity
        int newCapacity = oldCapacity + (oldCapacity >> 1);
         //新的容量等于原来容量的1.5倍  oldCapacity+oldCapacity/2  右移一位相当于除2
        if (newCapacity - minCapacity < 0)
            //如果新的容量小于最小容量,执行将最小容量赋值给新的容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //如果新的容量大于最大的数组大小,执行hugeCapacity(minCapacity);
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
        //将elementData扩容newCapacity大小
    /*
    Arrays.copyOf(T[] original, newLength);
    Arrays中的copyOf具有扩充数组的功能,其中original为待扩充的原始数组,newLength为需要扩充的容量的大小,方法不会直接在原数组中直接修改,而是返回新的一个数组,所以copyOf具有返回值。
    */
    }
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

Vector

Vector类实现了可扩展的对象数组,可变数组。像数组一样,它包含可以使用整数索引访问的组件。但是Vector的大小可以根据需要增长或缩小(扩容机制),以适应在创建Vector之后添加和删除项目。

Vector与ArrayList比较

底层结构版本线程安全(同步)效率扩容机制
ArrayList可变数组jdk1.2不安全 效率高有参构造每次1.5倍扩容 无参:默认为0 1.第一次10 2.第二次开始每次1.5倍扩容
Vector可变数组jdk1.0安全(同步)效率不高有参:每次2倍扩容 无参:直接默认10 满后每次按2倍扩容

常用操作

booleanadd(E e) 将指定的元素追加到此Vector的末尾。
voidadd(int index, E element) 在此Vector中的指定位置插入指定的元素。
booleanaddAll(Collection<? extends E> c) 将指定集合中的所有元素追加到该向量的末尾,按照它们由指定集合的迭代器返回的顺序。
booleanaddAll(int index, Collection<? extends E> c) 将指定集合中的所有元素插入到此向量中的指定位置。
voidaddElement(E obj) 将指定的组件添加到此向量的末尾,将其大小增加1。
intcapacity() 返回此向量的当前容量。
voidclear() 从此Vector中删除所有元素。
Objectclone() 返回此向量的克隆。
booleancontains(Object o) 如果此向量包含指定的元素,则返回 true
booleancontainsAll(Collection<?> c) 如果此向量包含指定集合中的所有元素,则返回true。
voidcopyInto(Object[] anArray) 将此向量的组件复制到指定的数组中。
EelementAt(int index) 返回指定索引处的组件。
Enumeration<E>elements() 返回此向量的组件的枚举。
voidensureCapacity(int minCapacity) 如果需要,增加此向量的容量,以确保它可以至少保存最小容量参数指定的组件数。
booleanequals(Object o) 将指定的对象与此向量进行比较以获得相等性。
EfirstElement() 返回此向量的第一个组件(索引号为 0的项目)。
voidforEach(Consumer<? super E> action)Iterable的每个元素执行给定的操作,直到所有元素都被处理或动作引发异常。
Eget(int index) 返回此向量中指定位置的元素。
inthashCode() 返回此Vector的哈希码值。
intindexOf(Object o) 返回此向量中指定元素的第一次出现的索引,如果此向量不包含元素,则返回-1。
intindexOf(Object o, int index) 返回此向量中指定元素的第一次出现的索引,从 index向前 index ,如果未找到该元素,则返回-1。
voidinsertElementAt(E obj, int index) 在指定的index插入指定对象作为该向量中的一个 index
booleanisEmpty() 测试此矢量是否没有组件。
Iterator<E>iterator() 以正确的顺序返回该列表中的元素的迭代器。
ElastElement() 返回向量的最后一个组件。
intlastIndexOf(Object o) 返回此向量中指定元素的最后一次出现的索引,如果此向量不包含元素,则返回-1。
intlastIndexOf(Object o, int index) 返回此向量中指定元素的最后一次出现的索引,从 index ,如果未找到元素,则返回-1。
ListIterator<E>listIterator() 返回列表中的列表迭代器(按适当的顺序)。
ListIterator<E>listIterator(int index) 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。
Eremove(int index) 删除此向量中指定位置的元素。
booleanremove(Object o) 删除此向量中指定元素的第一个出现如果Vector不包含元素,则它不会更改。
booleanremoveAll(Collection<?> c) 从此Vector中删除指定集合中包含的所有元素。
voidremoveAllElements() 从该向量中删除所有组件,并将其大小设置为零。
booleanremoveElement(Object obj) 从此向量中删除参数的第一个(最低索引)出现次数。
voidremoveElementAt(int index) 删除指定索引处的组件。
booleanremoveIf(Predicate<? super E> filter) 删除满足给定谓词的此集合的所有元素。
protected voidremoveRange(int fromIndex, int toIndex) 从此列表中删除所有索引为 fromIndex (含)和 toIndex之间的元素。
voidreplaceAll(UnaryOperator<E> operator) 将该列表的每个元素替换为将该运算符应用于该元素的结果。
booleanretainAll(Collection<?> c) 仅保留此向量中包含在指定集合中的元素。
Eset(int index, E element) 用指定的元素替换此Vector中指定位置的元素。
voidsetElementAt(E obj, int index) 设置在指定的组件 index此向量的要指定的对象。
voidsetSize(int newSize) 设置此向量的大小。
intsize() 返回此向量中的组件数。
voidsort(Comparator<? super E> c) 使用提供的 Comparator对此列表进行排序以比较元素。
Spliterator<E>spliterator() 在此列表中的元素上创建*late-binding故障切换* Spliterator
List<E>subList(int fromIndex, int toIndex) 返回此列表之间的fromIndex(包括)和toIndex之间的独占视图。
Object[]toArray() 以正确的顺序返回一个包含此Vector中所有元素的数组。
<T> T[]toArray(T[] a) 以正确的顺序返回一个包含此Vector中所有元素的数组; 返回的数组的运行时类型是指定数组的运行时类型。
StringtoString() 返回此Vector的字符串表示形式,其中包含每个元素的String表示形式。
voidtrimToSize() 修改该向量的容量成为向量的当前大小。

注意:addElement(E obj) 将指定的组件添加到此向量的末尾,将其大小增加1。该方法是Vector新增的方法

Vector扩容机制核心源码

思想与ArrayList差不多,这里指出重要部分

//无参直接默认10的大小
public Vector() {
    this(10);
}
//2倍扩容机制
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
//capacityIncrement大于0,相当于int newCapacity = oldCapacity + oldCapacity

一般在业务中常常选择ArrayList,因为其效率高,Vector因为每一个方法都有synchronized修饰,每次都需要校验锁,效率低

LinkedList

双链表实现了List和Deque接口。实现所有可选列表操作,并允许所有元素(包括null )。

所有的操作都能像双向列表一样预期。 索引到列表中的操作将从开始或结束遍历列表,以更接近指定的索引为准。

请注意,此实现不同步。 如果多个线程同时访问链接列表,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作;仅设置元素的值不是结构修改。)这通常通过在自然封装列表的对象上进行同步来实现。 如果没有这样的对象存在,列表应该使用Collections.synchronizedList 方法“包装”。 这最好在创建时完成,以防止意外的不同步访问列表:

  List list = Collections.synchronizedList(new LinkedList(...)); 

这个类的iteratorlistIterator方法返回的迭代器是故障快速的 :如果列表在迭代器创建之后的任何时间被结构化地修改,除了通过迭代器自己的removeadd方法之外,迭代器将会抛出一个ConcurrentModificationException 。 因此,面对并发修改,迭代器将快速而干净地失败,而不是在未来未确定的时间冒着任意的非确定性行为。

请注意,迭代器的故障快速行为无法保证,因为一般来说,在不同步并发修改的情况下,无法做出任何硬性保证。 失败快速迭代器尽力投入ConcurrentModificationException 。 因此,编写依赖于此异常的程序的正确性将是错误的:迭代器的故障快速行为应仅用于检测错误。

源码分析

效果体验:

LinkedList linkedList = new LinkedList();
linkedList.add("数据1");
linkedList.add("数据2");
System.out.println(linkedList); //[数据1, 数据2] 尾部插入数据2
linkedList.addFirst("数据3");
System.out.println(linkedList);  //[数据3, 数据1, 数据2] 尾部插入数据3
linkedList.remove();
System.out.println(linkedList);  //[数据1, 数据2] 头部移除数据3
    transient Node<E> last;
    

// 三种插入元素方式
//addFirst调用头插法 
    public void addFirst(E e) {
        linkFirst(e);
    }
//addLast调用尾插法addLast
    public void addLast(E e) {
        linkLast(e);
    }
//add调用尾插方法linkLast
public boolean add(E e) {
        linkLast(e);
        return true;
    }



/**
     * Links e as first element.
     */
//该方法实现头插法
    private void linkFirst(E e) {
        //当添加第一个元素时,fist为空=>f为空,即Node<>(null, e, null) 将first,last都指向这个本身节点 即只有这个节点
        
        //当添加第二个元素时,first不为空,指向的是上一个节点,此时该节点为 Node<>(null, e, f) 将first指向新添加的元素节点,last指向本身
        final Node<E> f = first;
        
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
             //判断是否只有一个元素,成立则将last指向本身
            last = newNode;
        else
             //当元素不只有一个元素时,成立则f的前驱指向新添加的元素节点
            f.prev = newNode;
        size++;
        modCount++;
    }

    /**
     * Links e as last element.
     */
//该方法实现尾插法
    void linkLast(E e) {
        //当添加第一个元素时,last为空=>l为空,即Node<>(null, e, null) 将first,last都指向这个本身节点 即只有这个节点
        //当添加第二个元素时,last不为空,l等于last,新增的元素Node<>(l, e, null),将last指向新添加的节点,first指向本身
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            //判断是否只有一个元素,成立则将first指向新添加元素节点的本身
            first = newNode;
        else
            //当元素不只有一个元素时,成立则将原节点的前驱指向新添加的元素
            l.next = newNode;
        size++;
        modCount++;
    }

    /**
     * Inserts element e before non-null Node succ.
     */
  //插入指定节点前的元素
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        //原理: 将指定节点的前驱赋值给新添加的节点的前驱 ,将指定节点的前驱指向当前新添加的节点 ,完成添加操作
        
        //指定节点的前驱赋值给pred ,新添加的元素为 Node<>(pred, e, succ) 
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        //将指定节点的前驱指向新添加的节点元素
        succ.prev = newNode;
        if (pred == null)
            //pred=null,即添加的节点位置位于最头节点
            first = newNode;
        else
            //新添加的节点不为头节点时,将前一个节点的next指向新添加的节点元素
            pred.next = newNode;
        size++;
        modCount++;
    }

    /**
     * Unlinks non-null first node f.
     */

    //移除头部元素
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        //原理:将指向给节点的前后节点
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

    /**
     * Unlinks non-null last node l.
     */
	//移除尾部元素
    private E unlinkLast(Node<E> l) {
        // assert l == last && l != null;
        final E element = l.item;
        final Node<E> prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

    /**
     * Unlinks non-null node x.
     */
   //移除指定元素
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

LinkList踩坑

1.remove()方法踩坑

remove(index)移除指定下标元素

使用for循环移除元素时

Java List在进行remove()方法是通常容易踩坑,主要有一下几点
循环时:问题在于,删除某个元素后,因为删除元素后,后面的元素都往前移动了一位,而你的索引+1,所以实际访问的元素相对于删除的元素中间间隔了一位

几种常见方法
使用for循环不进行额外处理时(错误)

for(int i=0;i<list.size();i++) {
	if(list.get(i)%2==0) {
		list.remove(i);
	}
}

2.使用foreach循环(错误)

for(Integer i:list) {
    if(i%2==0) {
     	list.remove(i);
    }
}

抛出异常:java.util.ConcurrentModificationException 该异常是基于java集合中的快速失败(fail-fast)机制产生的,在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了增删改,就会抛出该异常。
foreach的本质是使用迭代器实现,每次进入for (Integer i:list) 时,会调用ListItr.next()方法;
继而调用checkForComodification()方法, checkForComodification()方法对操作集合的次数进行了判断,如果当前对集合的操作次数与生成迭代器时不同,抛出异常

public E next() {
	checkForComodification();
	if (!hasNext()) {
		 throw new NoSuchElementException();
	}
	 lastReturned = next;
	next = next.next;
	nextIndex++;
	return lastReturned.item;
 }
 // checkForComodification()方法对集合遍历前被修改的次数与现在被修改的次数做出对比
final void checkForComodification() {
	  if (modCount != expectedModCount) {
	  		 throw new ConcurrentModificationException();
	  }
             
  }

使用for循环,并且同时改变索引;(正确)

//正确
for(int i=0;i<list.size();i++) {
	if(list.get(i)%2==0) {
		list.remove(i);
		i--;//在元素被移除掉后,进行索引后移
	}
}

使用for循环,倒序进行;(正确)

//正确

for(int i=list.size()-1;i>=0;i--) {
	if(list.get(i)%2==0) {
		list.remove(i);
	}
}

使用while循环,删除了元素,索引便不+1,在没删除元素时索引+1(正确)

//正确
int i=0;
while(i<list.size()) {
	if(list.get(i)%2==0) {
		list.remove(i);
	}else {
		i++;
	}
}

4.使用迭代器方法(正确,推荐)
只能使用迭代器的remove()方法,使用列表的remove()方法是错误的

//正确,并且推荐的方法
Iterator<Integer> itr = list.iterator();
while(itr.hasNext()) {
	if(itr.next()%2 ==0)
		itr.remove();
}

性能分析

下面来谈谈当数据量过大时候,需要删除的元素较多时,如何用迭代器进行性能的优化,对于ArrayList这几乎是致命的,从一个ArrayList中删除批量元素都是昂贵的时间复杂度为O(n²),那么接下来看看LinkeedList是否可行。LinkedList暴露了两个问题,一个:是每次的Get请求效率不高,而且,对于remove的调用同样低效,因为达到位置I的代价是昂贵的。

是每次的Get请求效率不高
需要先get元素,然后过滤元素。比较元素是否满足删除条件。
remove的调用同样低效
LinkedList的remove(index),方法是需要先遍历链表,先找到该index下的节点,再处理节点的前驱后继。
以上两个问题当遇到批量级别需要处理时时间复杂度直接上升到O(n²)

使用迭代器的方法删除元素
对于LinkedList,对该迭代器的remove()方法的调用只花费常数时间,因为在循环时该迭代器位于需要被删除的节点,因此是常数操作。对于一个ArrayList,即使该迭代器位于需要被删除的节点,其remove()方法依然是昂贵的,因为数组项必须移动。下面贴出示例代码以及运行结果

public class RemoveByIterator {

public static void main(String[] args) {
	
	List<Integer> arrList1 = new ArrayList<>();
	for(int i=0;i<100000;i++) {
		arrList1.add(i);
	}
	
	List<Integer> linList1 = new LinkedList<>();
	for(int i=0;i<100000;i++) {
		linList1.add(i);
	}

	List<Integer> arrList2 = new ArrayList<>();
	for(int i=0;i<100000;i++) {
		arrList2.add(i);
	}
	
	List<Integer> linList2 = new LinkedList<>();
	for(int i=0;i<100000;i++) {
		linList2.add(i);
	}
	
	removeEvens(arrList1,"ArrayList");
	removeEvens(linList1,"LinkedList");
	removeEvensByIterator(arrList2,"ArrayList");
	removeEvensByIterator(linList2,"LinkedList");
	
}
public static void removeEvensByIterator(List<Integer> lst ,String name) {//利用迭代器remove偶数
	long sTime = new Date().getTime();
	Iterator<Integer> itr = lst.iterator();
	while(itr.hasNext()) {
		
		if(itr.next()%2 ==0)
			itr.remove();
	}
	
	System.out.println(name+"使用迭代器时间:"+(new Date().getTime()-sTime)+"毫秒");
}

public static void removeEvens(List<Integer> list , String name) {//不使用迭代器remove偶数
	long sTime = new Date().getTime();
	int i=0;
	while(i<list.size()) {
		
		if(list.get(i)%2==0) {
			list.remove(i);
		}else {
			i++;
		}
	}

	System.out.println(name+"不使用迭代器的时间"+(new Date().getTime()-sTime)+"毫秒");
}
}

/*
*运行结果
ArrayList不使用迭代器的时间538毫秒
LinkedList不使用迭代器的时间4696毫秒
ArrayList使用迭代器时间:486毫秒
LinkedList使用迭代器时间:8毫秒

*/

原理 重点看一下LinkedList的迭代器
另一篇博客 Iterator简介 LinkedList使用迭代器优化移除批量元素原理
调用方法:list.iterator();

重点看下remove方法

private class ListItr implements ListIterator<E> {
        //返回的节点
        private Node<E> lastReturned;
        //下一个节点
        private Node<E> next;
        //下一个节点索引
        private int nextIndex;
        //修改次数
        private int expectedModCount = modCount;
        ListItr(int index) {
        //根据传进来的数字设置next等属性,默认传0
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }
    //直接调用节点的后继指针
    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();
        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }
    //返回节点的前驱
    public E previous() {
        checkForComodification();
        if (!hasPrevious())
            throw new NoSuchElementException();

        lastReturned = next = (next == null) ? last : next.prev;
        nextIndex--;
        return lastReturned.item;
    }
    /**
    * 最重要的方法,在LinkedList中按一定规则移除大量元素时用这个方法
    * 为什么会比list.remove效率高呢;
    */
    public void remove() {
        checkForComodification();
        if (lastReturned == null)
            throw new IllegalStateException();

        Node<E> lastNext = lastReturned.next;
        unlink(lastReturned);
        if (next == lastReturned)
            next = lastNext;
        else
            nextIndex--;
        lastReturned = null;
        expectedModCount++;
    }

    public void set(E e) {
        if (lastReturned == null)
            throw new IllegalStateException();
        checkForComodification();
        lastReturned.item = e;
    }

    public void add(E e) {
        checkForComodification();
        lastReturned = null;
        if (next == null)
            linkLast(e);
        else
            linkBefore(e, next);
        nextIndex++;
        expectedModCount++;
    }
}

LinkedList 源码的remove(int index)的过程是
先逐一移动指针,再找到要移除的Node,最后再修改这个Node前驱后继等移除Node。如果有批量元素要按规则移除的话这么做时间复杂度O(n²)。但是使用迭代器是O(n)。

先看看list.remove(idnex)是怎么处理的
LinkedList是双向链表,这里示意图简单画个单链表
比如要移除链表中偶数元素,先循环调用get方法,指针逐渐后移获得元素,比如获得index = 1;指针后移两次才能获得元素。
当发现元素值为偶数是。使用idnex移除元素,如list.remove(1);链表先Node node(int index)返回该index下的元素,与get方法一样。然后再做前驱后继的修改。所以在remove之前相当于做了两次get请求。导致时间复杂度是O(n)。

继续移除下一个元素需要重新再走一遍链表(步骤忽略当index大于半数,链表倒序查找)

以上如果移除偶数指针做了6次移动。

删除2节点
get请求移动1次,remove(1)移动1次。
删除4节点
get请求移动2次,remove(2)移动2次。
迭代器的处理
迭代器的next指针执行一次一直向后移动的操作。一共只需要移动4次。当元素越多时这个差距会越明显。整体上移除批量元素是O(n),而使用list.remove(index)移除批量元素是O(n²)

Set集合

不包含重复元素的集合。更正式地,集合不包含一对元素e1e2 ,使得e1.equals(e2) ,并且最多一个空元素。正如其名称所暗示的那样,这个接口模拟了数学集抽象。

Set接口除了继承自Collection接口的所有构造函数以及add,equalshashCode方法的外 ,还增加了其他规定。 其他继承方法的声明也包括在这里以方便。 (伴随这些声明的规范已经量身定做Set接口,但它们不包含任何附加的规定。)

构造函数的额外规定并不奇怪,所有构造函数都必须创建一个不包含重复元素的集合(如上所定义)。

注意:如果可变对象用作设置元素,则必须非常小心。 如果对象的值以影响equals比较的方式更改,而对象是集合中的元素, 则不指定集合的行为。 这种禁止的一个特殊情况是,一个集合不允许将其本身作为一个元素。

一些集合实现对它们可能包含的元素有限制。 例如,一些实现禁止空元素,有些实现对元素的类型有限制。 尝试添加不合格元素会引发未经检查的异常,通常为NullPointerExceptionClassCastException 。 尝试查询不合格元素的存在可能会引发异常,或者可能只是返回false; 一些实现将展现出前者的行为,一些实现将展现出后者。 更一般来说,尝试对不符合条件的元素的操作,其完成不会导致不合格元素插入到集合中,可能会导致异常,或者可能会成功执行该选项。 此异常在此接口的规范中标记为“可选”。

HashSet

底层实际上是一个HashMap,元素不重复且无序,只能存在一个null

 public HashSet() {
        map = new HashMap<>();
    }
  • 可以存放null 但是只能存放一个null
  • HashSet不保证元素是有序,取决于索引的结果
  • 不能有重复元素/对象(这里指的是同一个对象
public class HashSeto1 {
    public static void main(String[] args){
        HashSet hashSet = new HashSet();
        System.out.println(hashSet.add("数据1"));   //ture
        System.out.println(hashSet.add("数据2"));   //ture
        System.out.println(hashSet.add("数据2"));   //false
        System.out.println(hashSet.add("数据3"));   //true
        System.out.println("=======================");
        People people = new People("李四");
        System.out.println(hashSet.add(people));    //ture
        System.out.println(hashSet.add(people));    //false
        System.out.println("=======================");
        //System.out.println(new People("张三") == new People("张三"));
        System.out.println(hashSet.add(new People("张三")));  //ture
        System.out.println(hashSet.add(new People("张三")));   //ture
        System.out.println("=======================");
        System.out.println(hashSet.add(new String("hashset")));   //ture
        System.out.println(hashSet.add(new String("hashset")));    //false

        System.out.println(hashSet);
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class People{
    String name;
}

发现一个有趣现象:

System.out.println(hashSet.add(new People(“张三”)));
System.out.println(hashSet.add(new People(“张三”)));

People对象使用lombok插件重写toString和自己手动重写toString结果不一致

手动重写toString结果:true true

lombok重写toString结果:true false

解释,因为lomok重写了hashcode方法和equals方法,而HashSet是根据这两个方法判断是否相同

源码如下:

 if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))

因此,当我们需要实现参数一样的两个对象只能添加一个这种情况时,重写hashcode和equals方法即可!

分析HashSet

HashSet底层是一个HashMap,而HashMap底层是一个数组+链表+红黑树 (存储效率高)

简单理解 数组+链表+红黑树 :数组中每一个位置都可以存放一条链表,链表长度大于等于8时,链表树化成红黑树,小于6时退化为链表

  • HashSet底层是 HashMap
  • 添加一个元素时,先得到hash值-会转成->索引值
  • 找到存储数据表table,看这个索引位置是否已经存放的有元素
  • 如果没有。直接加入
  • 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
  • 在Java8中,如果一条链表的元素个数达到TREEIFY_THRESHOLD(默认是8),并且table的大小>=
    MIN_TREEIFY_CAPACITY(默认64)(如果链表长度到达了8,但table没有到达64,则table会进行扩容),就会进行树化(红黑树)

简单模拟数组+链表

public class HashSet02 {
    public static void main(String[] args) {
        Node[] table = new Node[16];
        Node node1 = new Node("数据1",null);

        table[1] = node1;
        Node node2 = new Node("数据2",null);
        node1.next = node2;
        Node node3 = new Node("数据3",null);
        node2.next = node3;
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Node{
    Object item; //该变量存储数据
    Node next;  //指向下一个节点
}

打断点观察数据的变化!

注意点:在源码中,死循环一般不用while(true),而是用的是for(循环开始条件; ;循环结束条件)实现,原因for编译出来的指令比while少,相对来说不会占用寄存器空间

聊一聊for循环执行流程

for(循环开始条件;循环判定条件;循环结束条件){
	语句体
}
//1.当不存在循环判定条件,为for死循环
//2.执行流程为  循环开始条件--》循环判定条件--》语句体--》循环结束条件
//3.for死循环可以用break跳出

底层实现添加元素 的基本步骤

  1. 先获取元素的哈希值(hashCode方法)
  2. 对哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
  3. 如果该位置上没有其他元素,则直接存放
  4. 如果该位置上已经有其他元素,则需要进行equals判断,如果相等,则不再添加,如果不相等,则以链表的方式添加

LinkedHashSet

  • LinkedHashSet是HashSet的一个子类,继承了HashSet实现了Set接口
  • LinkedHashSet底层是一个LinkedHashMap(是HashMap的子类),底层维护了一个 数组+双向链表(HashSet是单链表)
  • LinkedHashSet根据元素的hashCode值来确定元素的存储位置,同时使用链表维护的次序(图),这使得元素看起来是以插入顺序保存的,是有序的
  • LinkedHashSet不允许添加重复元素

说明

  1. 在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet有head和tail)

  2. 每一个节点有pre和next属性,这样可以形成双向链表

  3. 在添加一个元素时,先求hash值,在求索引,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样)

    tail.next = newElement ;
    newElement.pre = tail;
    tail = newElement;
    
  4. 这样,我们遍历LinkedHashSet也能确定插入顺序一致

package org.example;

import java.util.HashSet;
import java.util.LinkedHashSet;

public class LinkedHashSet01 {
    public static void main(String[] args) {
        //HashSet和LinkedHashSet对比
        HashSet hashSet = new HashSet();
        hashSet.add("数据1");
        hashSet.add("数据2");
        hashSet.add("数据3");
        hashSet.add("数据4");
        hashSet.add("数据5");
        hashSet.add("数据6");
        System.out.println(hashSet);


        LinkedHashSet linkedHashSet = new LinkedHashSet();
        linkedHashSet.add("数据1");
        linkedHashSet.add("数据2");
        linkedHashSet.add("数据3");
        linkedHashSet.add("数据4");
        linkedHashSet.add("数据5");
        linkedHashSet.add("数据6");
        System.out.println(linkedHashSet);
    }
}
//运行结果
//[数据6, 数据1, 数据2, 数据3, 数据4, 数据5]
//[数据1, 数据2, 数据3, 数据4, 数据5, 数据6]
//由此可见,HashSet是无序的,LinkedHashSet是有序的,即所有节点都由双链表连接

相比于hashset的优点是使得查询效率变高了,单链表只能找位于同一数组下标下的,不能找其他数组下标的

源码分析

//先从构造器入手
	public LinkedHashSet() {
        super(16, .75f, true);
    }
     public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
//三个构造方法
//1.无参构造:默认容量大小initialCapacity=16 集合负载量75%,即容量大于75%进行扩容,loadFactor负载
//2.有参构造分别为指定容量大小使用默认负载量75%,指定容量大小和指定负载量
//三个构造方法分别调用父类(HashSet)的构造方法super

//进入无参构造(其他两个类比)
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
//创建一个LinkedHashMap,由此可见,LinkedHashSet底层由LinkedHashMap实现的


//进入LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
//accessOrder:存取顺序

//进入LinkedHashMap调用父类,由此可见LinkedHashSet与HashMap机制一样,只不过指定了默认的大小和负载量
public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }


//添加元素操作,使用debug调试
//LinkedHashSet 加入顺序和取出元素/数据的顺序一致
//LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
//LinkedHashSet 底层结构(数组table + 双向链表)
//添加第一次时,直接将 数组table 扩容到 16,存放的节点类型是LinkedHashMap$Entry table数组类型是HashMap$Node[](打断点可知)而HashMap存放的节点则是HashMap$Node  table数组类型也是Hash$Node[]
//由此抛出问题,table和存放的数组不一致是怎么存进去的?显然此时用了多态数组,即Entry是Node的子类或则实现了Node类
//列出当前类的idea的快捷键为Alt+7

//找出了Entry接口
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);
        }
    }
//该接口继承了HashMap.Node<K,V>,证明之前猜测对的,而且Node类是由HaspMap以类名形式调用,可以猜测Node类是HashMap的一个静态内部类,可追源码验证
//由此可见,它有两个属性before, after,分别代表前后节点的指向

//插入一条数据 debug调试
/*
...
10 = null
11 = {LinedHashMap$Entry@549} "数据1=java.lang.Object@3abfe8836"
	before = null
	after = null
	hash = 25743995
	key = "数据1"
		value = {cahr[3]@556[数,剧,1]}
		hash = 25744371
	value = {Object@547}
		Class has no fields(类没有字段)
	next = null
12 = null
...
16 = null
*/
//插入三条条数据,分别为数据1、数据2、数据3 debug调试
/*
...
10 = null
11 = {LinedHashMap$Entry@549} "数据1=java.lang.Object@3abfe836"
	before = null
	after = null
	hash = 25743995
	key = "数据1"
		value = {cahr[3]@556[数,剧,1]}
		hash = 25744371
	value = {Object@547}
		Class has no fields(类没有字段)
	next = null
12 = {LinedHashMap$Entry@561} "数据2=java.lang.Object@3abfe836"
	before = {LinkedHashMap$Entry@549}"数据1"
	after = {LinkedHashMap$Entry@561} 数据3 
	hash = 25743996
	key = "数据2"
	value = {Object@547}
	next = null
13 = {LinedHashMap$Entry@574} "数据2=java.lang.Object@3abfe836"
...
*/

//注意:这里的next要和after区分开,next是数组同一个位置中链表的下一个元素,而after是双向链表的后一个元素

//添加元素的源码底层同Hasp一致

//双向链表源码分析
transient LinkedHashMap.Entry<K,V> tail;
// link at the end of list 元素添加到链表的末尾
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        //初始tail为空,则last为null
        tail = p;
        //将添加的该节点赋值给tail
        if (last == null)
            //如果last为空,则证明该集合中只有一个元素,则将head赋值为空
            head = p;
        else {
            //如果不为空,则说明并不是只有一个元素,将该节点的before指向上一个元素,last指向本身,即没有后续节点
            p.before = last;
            last.after = p;
        }
    }

TreeSet

Queue

Map

在前面所说的HaspSet中我们知道其底层就是使用了HaspMap,而Map是一个K-V结构,而HaspSet只用了K类存储数据,V用了一个常量present填充

Map接口实现类的特点,在实际应用中极为广泛,

注意:这里讲的是JDK8的Map接口特点 Map_.java

  1. Map与Collection并列存在。用于保存具有映射关系的数据:Key-Value
  2. Map 中的key 和 value可以是任何引用类型的数据,会封装到HashMap$Node
    对象中
  3. Map 中的key 不允许重复,原因和HashSet一样,前面分析过源码
  4. Map 中的value可以重复
  5. Map 的key可以为null, value也可以为null,注意key 为null, 只能有一个,
    value为null ,可以多个.
  6. 常用String类作为Map的key
  7. key 和 value之间存在单向一对一关系,即通过指定的key 总能找到对应的value

常见的实现子类常有HashMap(子类有LinkedHashMap) TableMap(子类有Properties) TreeMap

Collection的添加操作是add,而Map是put

Map遍历底层原理

  • k-v为了方便遍历,会创建一个EntrySet集合,该集合存放的元素的类型是Entry,而一个Entry对象就有K和V (EntrySet<Entry<K,V>) 源码:transient Set<Map.Entry<K,V>> entrySet

  • EntrySet 中,定义的类型是Map.Entry,但是实际中存放的还是 HashMap$Node

    源码:static class Node<K,V> implements Map.Entry<K,V>

  • 当把HashMap$Node对象 存放到 entrySet 就方便我们的遍历,因为Map.Entry 提供了两个重要方法 K getKey() 和 V getValue()

  • 同理,为了只单独遍历K或则单独遍历Value,也都创建了一个对应的集合,K对应Set集合,value对应Collection集合

package org.example;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class HashMap01 {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        System.out.println(hashMap.put("name1", "数据1"));
        System.out.println(hashMap.put("name2", "2"));
        System.out.println(hashMap.put("name3", 5));
        System.out.println(hashMap.put("name4", new People()));
        System.out.println(hashMap.put("name2", "替换value"));
        System.out.println(hashMap.put("name1", "数据1"));
        System.out.println(hashMap);

        //get是通过k返回对应的value
        System.out.println(hashMap.get("name1"));

        System.out.println("==============================");
        Set set = hashMap.entrySet();
        System.out.println(set.getClass());
        for(Object obj : set){
            System.out.println(obj.getClass());  //HashMap$Node
            //为了从HashMap$Node取出 k-v 先向下转型
            Map.Entry entry = (Map.Entry)obj;
            System.out.println(entry.getKey()+"----"+entry.getValue());

        }
        System.out.println("==============================");
        Set set1 = hashMap.keySet();
        System.out.println(set1.getClass());
        for (Object o : set1) {

            System.out.println(o);
        }
        System.out.println("==============================");
        Collection values = hashMap.values();
        System.out.println(values.getClass());
        for (Object value : values) {
            System.out.println(value);
        }

    }
}

小结:为了方便遍历,会创建一个对应的集合,该集合中存储了table数组中Node节点的地址,可debug观察

Map的六大遍历方式

主要涉及到的方法

  • containsKey :查找键是否存在
  • keySet:获取所有的键
  • entrySet:获取所有的关系k-v
  • values:获取所有的值
package org.example;

import java.util.*;

public class MapDemo01 {
    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("key1","value1");
        map.put("key2","value2");
        map.put("key3","value3");
        map.put("key4","value4");
        map.put("key5","value5");

        System.out.println("========方式一========");
        //方式一:先取出所有的Key 通过Key 取出对应的Value
        Set set = map.keySet();
        //set遍历又有两种,迭代器和增强for
        //迭代器
        Iterator iterator = set.iterator();
        while (iterator.hasNext()){
            Object key = iterator.next();
            Object value = map.get(key);
            System.out.println("key:" +key +"===="+"value:"+value);
        }
        System.out.println("--------------------");
        //增强for
        for (Object key : set) {
            Object value = map.get(key);
            System.out.println("key:" +key +"===="+"value:"+value);
        }

        System.out.println("========方式二========");
        //方式二:把所有的values取出
        Collection values = map.values();
        //Collection可以使用三种遍历方法 迭代器 增强for
        //迭代器
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object next =  iterator1.next();
            System.out.println("values:"+next);

        }
        System.out.println("--------------------");
        //增强for
        for (Object value : values) {
            System.out.println("values:"+value);
        }

        System.out.println("========方式三========");
        //方式三,通过EntrySet 获取K-V
        Set set1 = map.entrySet();
        //增强for
        for (Object entry : set1) {
            //将entry转化为Map.Entry,因为Map.Entry中提供了getKey()和getValue()
            Map.Entry m = (Map.Entry)entry;
            Object key = m.getKey();
            Object value = m.getValue();
            System.out.println("key:" +key +"===="+"value:"+value);

        }
        System.out.println("--------------------");
        //迭代器
        Iterator iterator2 = set1.iterator();
        while (iterator2.hasNext()) {
            Object entry =  iterator2.next();
            Map.Entry m = (Map.Entry)entry;
            Object key = m.getKey();
            Object value = m.getValue();
            System.out.println("key:" +key +"===="+"value:"+value);

        }
    }
}

常用方法

Modifier and TypeMethod and Description
voidclear() 从该地图中删除所有的映射(可选操作)。
booleancontainsKey(Object key) 如果此映射包含指定键的映射,则返回 true
booleancontainsValue(Object value) 如果此地图将一个或多个键映射到指定的值,则返回 true
Set<Map.Entry<K,V>>entrySet() 返回此地图中包含的映射的Set视图。
booleanequals(Object o) 将指定的对象与此映射进行比较以获得相等性。
default voidforEach(BiConsumer<? super K,? super V> action) 对此映射中的每个条目执行给定的操作,直到所有条目都被处理或操作引发异常。
Vget(Object key) 返回到指定键所映射的值,或 null如果此映射包含该键的映射。
default VgetOrDefault(Object key, V defaultValue) 返回到指定键所映射的值,或 defaultValue如果此映射包含该键的映射。
inthashCode() 返回此地图的哈希码值。
booleanisEmpty() 如果此地图不包含键值映射,则返回 true
Set<K>keySet() 返回此地图中包含的键的Set视图。
default Vmerge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction) 如果指定的键尚未与值相关联或与null相关联,则将其与给定的非空值相关联。
Vput(K key, V value) 将指定的值与该映射中的指定键相关联(可选操作)。
voidputAll(Map<? extends K,? extends V> m) 将指定地图的所有映射复制到此映射(可选操作)。
default VputIfAbsent(K key, V value) 如果指定的键尚未与某个值相关联(或映射到 null )将其与给定值相关联并返回 null ,否则返回当前值。
Vremove(Object key) 如果存在(从可选的操作),从该地图中删除一个键的映射。
default booleanremove(Object key, Object value) 仅当指定的密钥当前映射到指定的值时删除该条目。
default Vreplace(K key, V value) 只有当目标映射到某个值时,才能替换指定键的条目。
default booleanreplace(K key, V oldValue, V newValue) 仅当当前映射到指定的值时,才能替换指定键的条目。
default voidreplaceAll(BiFunction<? super K,? super V,? extends V> function) 将每个条目的值替换为对该条目调用给定函数的结果,直到所有条目都被处理或该函数抛出异常。
intsize() 返回此地图中键值映射的数量。
Collection<V>values() 返回此地图中包含的值的Collection视图。

HashMap

  • 数据存放形式是键值对
  • 当添加的元素的k相同时,会替换原有的value值
  • 存取无序,value可以重复
  • key可以为null,value也可以为null,但是key为null只能有一个,value可以为多个
  • 常用String作为Map的key,但是k可以是任意Object

HaspMap结构

jdk7中 底层实现 数组+链表

jdk8中 底层实现 数组+链表+红黑树

扩容机制

  • HashMap底层维护的是一个Node类型table数组(默认大小是null,默认负载因子0.75),数组中存放着链表的节点,是HashMap$Node 该node节点是实现了Map.Entry<K,V>

  • 当创建HashMap对象时,将加载因子(loadfactor)初始化为0.75

  • 当添加一个k-v时 ,如果第一次添加,则需要将table数组容量设为16,临界值(treshold)12

  • 添加一个元素并计算Key其hash值,通过一个算法得到对应的下标,如果该下标存在元素(哈希碰撞),就会进行equals对比确定是否是同一个对象,是,直接替换Val,否,判断是树结构还是链表结构,做出相应处理,如是链表结构,添加到该元素的链表的后端,这也就是解决哈希碰撞的一种方法,拉链法

  • 以后再次扩容时,则table扩容大小是原来的两倍,临界值也为原来的两倍(其实底层就是乘负载因子)即24,以此类推

  • 在jdk8中,当链表长度大于等于TREEIFY_THRESHOLD(默认是8)时,并且table的大小>= MIN_TREEIFY_CAPACITY(默认是16)时,链表树化为红黑树,因为树的查询效率是很高效的

    小于等于临界值6时,退化为链表

注:当链表的长度到达8时且数组长度到达64时,才会进行转换为红黑树,如果当链表的长度到达8时,但是数组的长度小于64时,不会转化为红黑树,因为数组的长度较小,应该尽量避开红黑树,因为红黑树需要进行左旋右旋变色操作来保持平衡,所以当数组长度小于64时,使用数组加链表比使用红黑树查询效率要更快,效率要更高

TableMap

TreeMap

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值