0823(037天 集合框架02 Collection/Set/List,List的三个实现类的底层)

0823(037天 集合框架02 Collection/Set/List,List的三个实现类的底层)

每日一狗(田园犬西瓜瓜

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gyewc2lZ-1661866139375)(assets/37.png)]

集合框架02 Collection/Set/List,List的三个实现类的底层

1. List接口

有序、允许重复

1.1 特殊方法

排序default void sort(Comparator<? super E> c)

@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
    Object[] a = this.toArray(); // 获取数组对象
    Arrays.sort(a, (Comparator) c); // 使用Arrays工具类进行排序操作
    ListIterator<E> i = this.listIterator(); // 获取当前list的迭代器
    for (Object e : a) { // 使用forEach结构遍历排序后的数组
        i.next(); // 迭代器后移
        i.set((E) e); // 将排好序的数据进行覆盖
    }
}

// 前场一下

插入操作 void add(int index, E element);

插入操作只能操作已经操作过的位置,就是说这个地方之前必须要有值,或者是像当前数组的size位置进行插入操作,但是这个操作应该用boolean add(E e);来进行追加操作

List list = new ArrayList();
list.add(0, 20);
for (int i = 0; i < 5; i++) {
    list.add(i);
}
for (int i = 0; i < 5; i++) {
    list.add(0, i);
}

list.forEach(System.out::print);

// 432102001234

取出并且删除;

boolean remove(Object o); // 删除指定对象,返回是否成功

E remove(int index); // 获取并删除指定索引位置上的数据

List list = new ArrayList();
list.add(0, 20);
for (int i = 0; i < 5; i++) {
    list.add(i);
}
list.forEach(System.out::print);
System.out.println(list.remove((Object) 50)); // false
System.out.println(list.remove((Object) 20)); // true
System.out.println(list.remove(3)); // 3

找到指定对象在数组中的定位int indexOf(Object o);

List list = new ArrayList();
list.add(0, 20);
for (int i = 0; i < 5; i++) {
    list.add(i);
}

System.out.println(list); // [20, 0, 1, 2, 3, 4]
System.out.println(list.indexOf(4)); // 5
System.out.println(list.lastIndexOf(3)); // 4

1.2 常见的实现类

  • ArrayList:底层存储为数组,线程不安全

  • Vector:底层实现为数组,线程安全

  • LinkedList:底层实现为链表,线程不安全

ArrayListLinkedListVector
实现方法数组,按照索引下标访问速度快O(1),但是当删除或添加元素是会导致元素的移动,速度慢O(n)双向链表,按照索引下标访问速度慢O(n),但是删除或添加操作速度快O(1) // 但是这里的操作指的是你已经定位到了这个节点了,所以用索引对数据及性能添加删除操作就会先有一个索引的操作O(n)数组,按照索引下标访问O(1),添加或修改操作O(n)。
是否同步不同步,线程不安全,并发高访问效率高不同步,线程不安全,并发操作效率高同步,但是并发访问或操作的效率底
如何选择经常需要快速访问,较少在中间增加删除元素时使用;如果多线程访问,则需要自行编程解决线程安全问题经常需要在内部增删元素,但是很少需要通过索引快速访问时使用;如果多线程访问,则需要自行编程解决线程安全问题一般不使用,多线程中可以直接拿来用

1.3 ArrayList

public class ArrayList<E> extends AbstractList<E> // AbstractList抽象类中定义了所有的共享方法
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k6qn6iWi-1661866139377)(assets/image-20220823110925412.png)]

构造方法

**无参构造器;**使用无参构造器,则不会直接创建数组,而是采用空数组,当第一次添加元素时才创建

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 这是一个空数组
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

**带参构造器:**可以指定容器的容积,最多存储多少个对象

private static final Object[] EMPTY_ELEMENTDATA = {}; // 还是一个空数组

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) { // 大于0时按照指定
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) { // 为0时赋值空数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else { // 为负数时抛出 非法参数异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

这里在不确定开辟空间的数组的大小时,默认开辟的时空数组,这里是对内存的一种优化处理,等你用的时候在来创建大小。

添加元素

public boolean add(E e)

protected transient int modCount = 0; // 不被序列化

public boolean add(E e) {
    modCount++; // 用于统计当前集合对象的修改次数(要有对象数量上的修改)
    /*
    e:新增元素,
    elementData:存储数组
    size:添加索引
    */
    add(e, elementData, size); 
    return true;
}
//10//
transient Object[] elementData; // 非私有以简化嵌套类访问
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length) // 判定当前集合中存储的元素个数是否和数组长度相等
        elementData = grow(); // 相等时扩容,没空间了
    elementData[s] = e; // 在数组的指定位置位置存储元素
    size = s + 1; // 集合当前存储对象个数加一
}
//grow//
// 无参扩容方法
private Object[] grow() {
    return grow(size + 1); // 调用代参扩容方法,容量加一
}
//24//
// 代餐扩容方法
private Object[] grow(int minCapacity) {
    /*
    使用Arrays.copyOf进行拷贝操作
    newCapacity方法来计算所需要的新容积值
    */
    return elementData = Arrays.copyOf(
        elementData,
        newCapacity(minCapacity)
    );
}
//35//
private static final int DEFAULT_CAPACITY = 10; // 数组的尺寸下限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 数组的尺寸上限

private int newCapacity(int minCapacity) {
    // overflow-conscious code  溢出的代码
    int oldCapacity = elementData.length; // 获取老数组长度
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍
    // 判定是否满足最小容积的要求
    if (newCapacity - minCapacity <= 0) { 
        // 判定存储数据的数组是否时初始化的空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // 计算默认初始化容积10和所需最小容积值则最大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        // 最小容积小于0时内存溢出,超出Integer的范围了
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    // 判定新容积是否小于最大容积值,是则返会新容积
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}
//60//
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    //  所需容积值大于数组的尺寸上限时,返回整形的最大值,否则返回数组的尺寸上限
    return (minCapacity > MAX_ARRAY_SIZE)
        ? Integer.MAX_VALUE
        : MAX_ARRAY_SIZE;
}

扩展:数组拷贝

/*
创建一个newLength长度的新数组,将原始数组的数据拷贝到新数组中,返回新数组
T[] original 原始数组
int newLength 所需要的新数组的长度
*/
@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength) {
 return (T[]) copyOf(original, newLength, original.getClass());
}
删除并获取元素
  • 没有缩容处理
  • 底层使用 System.arraycopy 来实现数据的移动操作

public E remove(int index) 直接就有

public E remove(int index) {
    // 检查所需要删除的位置下表阐述是否在合理的范围内【0,size】
    Objects.checkIndex(index, size); 
    // 获取所存储的
    final Object[] es = elementData;

    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    fastRemove(es, index); // 通过拷贝将index+1位置以后的数据移动到index的位置,没有缩容处理

    return oldValue; // 返回需要取出的元素
}
 8 /
private void fastRemove(Object[] es, int i) {
    modCount++; // 修改次数加一
    final int newSize;
    if ((newSize = size - 1) > i) // size - 1就是可以删除的元素的最大下标
        System.arraycopy(es, i + 1, es, i, newSize - i); // 拷贝数据 ,从i+1 的位置往后的数据 拷贝到i往后的位置上
    es[size = newSize] = null; // 将最后一个位置的元素进行数据赋空
}

public boolean remove(Object o)

// 找到与之相同的第一个元素的索引,把后边的考到前边来
public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found; // 跳出 found:代码块
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;// 跳出 found:代码块
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}
获取数据

public E get(int index)

public E get(int index) {
    Objects.checkIndex(index, size); // 针对索引下标进行合法性验证
    return elementData(index);
}
/// 2 /
@ForceInline
public static
    int checkIndex(int index, int length) {
    return Preconditions.checkIndex(index, length, null);
}
// 9 //
@HotSpotIntrinsicCandidate
public static <X extends RuntimeException>int checkIndex(
    int index, 
    int length,       
    BiFunction<String, 
    List<Integer>, 
    X> oobef
) {
    if (index < 0 || index >= length) // 判定索引小于0或>=length时抛出索引越界
        throw outOfBoundsCheckIndex(
        oobef, 
        index, 
        length
    );
    return index;
}

1.4 LinkedList

使用双向链表来实现

理论上讲这个链表的长度是没有限制,但是size是int的那就限定在【0,Integer.MAX_VAULE】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cwLQpbIe-1661866139377)(assets/image-20220823161049231.png)]

实现

属性

transient int size = 0;

transient Node<E> first; // 头节点

transient Node<E> last; // 尾节点

节点的定义

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;
    }
}
构造器
  • 啥也不是
  • 在没有数据的情况下链表的存在没有意义
public LinkedList() {
}
// 这个就是将一个Set想转换为列表的时候用一下
// 构造一个包含指定集合元素的列表,按照集合迭代器返回元素的顺序。
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
追加add

在这里插入图片描述

public boolean add(E e)

  • 判空插入
  • 有值插入
public boolean add(E e) {
    linkLast(e);
    return true;
}
 2 
void linkLast(E e) {
    final Node<E> l = last; // 缓存尾节点
    final Node<E> newNode = new Node<>(l, e, null); // 创建新的节点
    last = newNode; // 将为节点指向为新创建的节点
    if (l == null) // 如果尾节点为空
        first = newNode; // 则将新增节点设置为头节点
    else
        l.next = newNode; // 否则将缓存尾节点的next指向新增的节点
    size++; // 长度加一
    modCount++; // 修改次数加一
}

public boolean add(E e)

  • 指定索引节点定位
  • 新节点插入
public void add(int index, E element) {
    checkPositionIndex(index); // 索引的合法性检查

    if (index == size) // 在末尾追加元素
        linkLast(element);
    else
        linkBefore(element, node(index));
}

/ 2 索引的合法性检查//
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/ 12 索引的合法性检查
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}
/// 7 在指定节点的前添加一个新节点/
void linkBefore(E e, Node<E> succ) { // 
    final Node<E> pred = succ.prev; // 获取新节点应该指向的前一个节点
    final Node<E> newNode = new Node<>(pred, e, succ); // 新节点 ,同时将新节点的前一个节点和后一个节点传入
    succ.prev = newNode; // 将指定的节点的前一个节点指向刚创建的新节点
    if (pred == null) // 如果前一个节点为空,指定节点为头节点
        first = newNode; // 把当前节点设定为头节点
    else
        pred.next = newNode; // 否则将新节点的前一个节点的next指向新节点
    size++; // 链表长度加一
    modCount++; // 修改次数加一
}

// 7 定位指定索引位置上的节点///
Node<E> node(int index) {
    // 分两头,靠近那头就从那头开始往中间找
    if (index < (size >> 1)) { // 索引里那头进
        Node<E> x = first;
        for (int i = 0; i < index; i++) // 从前向后遍历
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) //  // 从后向前遍历
            x = x.prev;
        return x;
    }
}
取出

在这里插入图片描述

public boolean remove(Object o)

循环判定找到存储这个对象的节点是哪一个节点,在用unlink删除指定的那个节点

public boolean remove(Object o) {
    if (o == null) { // 删除的元素是null
        for (Node<E> x = first; x != null; x = x.next) { // 从头节点开始找,找到第一个数据为null的节点
            if (x.item == null) { // 为空时
                unlink(x); // 删除指定节点
                return true; // 返回true
            }
        }
    } else { // 当删除的不是null时
        for (Node<E> x = first; x != null; x = x.next) { // 遍历定位指定节点
            if (o.equals(x.item)) { // 相等时
                unlink(x); // 删除指定节点
                return true; // 返回true
            }
        }
    }
    return false; // 没有时返回false 
}
/  5 删除指定的node节点,返回node中存储的数据
E unlink(Node<E> x) { // 1 2 3
    final E element = x.item; // 获取节点中的数据 
    final Node<E> next = x.next; // 获取节点的下一个节点对象的引用
    final Node<E> prev = x.prev; // 获取上一个节点

    if (prev == null) { // 当2是头节点时
        first = next; // 把头接指向3
    } else { // 当2不是头节点时
        prev.next = next; // 将1的下一个设定为3
        x.prev = null; // 将2节点的下一个设定为null
    }

    if (next == null) { // 3为空时
        last = prev; // 将1设定为尾接待你
    } else { // 3不为空时
        next.prev = prev; // 将3节点的上一个设定为1
        x.next = null; // 将2的下一个设定为null
    }

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

public E remove(int index)

使用node获取指定索引上的节点对象,使用unlink来删除这个节点

public E remove(int index) {
    checkElementIndex(index); // 检查序号是否合法,不合法时抛出异常
    return unlink(node(index)); // 
}
 3 node(index))获取指定索引的节点 
// 往上找有原码
修改操作

获取并覆盖指定索引位置上的数据

先获取指定索引上的节点对象,将节点对象的旧数据进行缓存返回,用新数据将其覆盖

public E set(int index, E element) {
    checkElementIndex(index); // 索引合法性判定
    Node<E> x = node(index); // 获取指定索引的节点
    E oldVal = x.item; // 获取原有数据
    x.item = element; // 覆盖新数据
    return oldVal; // 返回原有数据
}
查询

indexOf(Object o) :定位元素索引位置

传入为空时遍历判定是否为空,传入不为空时遍历用equals判定相等

public int indexOf(Object o) {
    int index = 0;
    if (o == null) { // 为空时一种判定
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else { // 不为空时一种判定
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

1.5 Vector

一个非常古老的一个类,从JDK1.0+就开始提供。很多方法于原始数据有出入

  • 线程安全
  • 扩容双倍,或固定步长
  • 初始化为立即初始化,会立即分配空间
属性
protected Object[] elementData; // 数据存储
protected int elementCount; // 存储的元素个数
protected int capacityIncrement; // 容积增长的步长值
构造器

立即初始化,创建即分配空间

public Vector() {
    this(10);
}
 2
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
 6
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0) // 初始化容积不能小于0
        throw new 
        IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    this.elementData = new Object[initialCapacity]; // 立即创建数组
    this.capacityIncrement = capacityIncrement; // 设定上限制
}
方法查看

方法基本都有synchronized,线程安全

public synchronized boolean add(E e) {
    modCount++;
    add(e, elementData, elementCount);
    return true;
}
 3
private void add(
    E e,  // 需要添加的元素
    Object[] elementData,  // 存储数组
    int s // 元素的个数
) {
    if (s == elementData.length) // 容量判定
        elementData = grow(); // 扩容
    elementData[s] = e; // 
    elementCount = s + 1; // 存储元素个数加一
}
 13
private Object[] grow() {
    return grow(elementCount + 1);
}
/ 19
private Object[] grow(int minCapacity) {
    return elementData = Arrays.copyOf(
        elementData,
        newCapacity(minCapacity)
    );
}
/25
private int newCapacity(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity +  // 如果设置了容积增长步长,则按照步长来扩容,如果没有则扩容比为100%
        ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity <= 0) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return minCapacity;
    }
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}

2. Set集合

无序、不允许重复

public interface Set<E> extends Collection<E>

没有特殊方法,全部的方法都是Collection接口中的方法

2.1 实现类

  • HashSet
  • TreeSet
  • LinkedHashSet

2.2 一些方法

没有新方法

都是Collection接口中定义过的方法

package com.yang2;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Test01 {
	public static void main(String[] args) {
		Set set = new HashSet();
		set.add(new A1(20L, "wcao"));
		set.add(new A1(20L, "wcao"));
		set.add(new A1(22L, "wcao"));
		System.out.println(set);
		// [A1 [id=20, name=wcao], A1 [id=22, name=wcao]]
	}
}
class A1 {
	private Long id;
	private String name;

	@Override
	public String toString() {
		return "A1 [id=" + id + ", name=" + name + "]";
	}

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

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		A1 other = (A1) obj;
		return Objects.equals(id, other.id) && Objects.equals(name, other.name);
	}

	public A1(Long id, String name) {
		super();
		this.id = id;
		this.name = name;
	}

}

3. 整理

List三种实现的区别?

Java已经提供的List接口的实现类有三个分别为ArrayList、Vector、LinkedList

相同点:

全都实现了List接口,实现了接口中的抽象方法,都是Java中的存储数据的容器

元素之间有序且不重复

区别:

  • ArrayList:

    • 底层存储为数组,不会存储元素数据,存储的只是元素的引用,真实的数据存储在内存中
    • 元素的访问用的是索引下标值,O(1);
    • 插入删除操作需要将指定索引位置上后边的元素全部操作一遍(本质上用的是数组的拷贝System.arraycopy),O(n)
    • 线程不安全,并发访问操作高效,但是修改操作更容易出现线程安全问题(数组的拷贝操作也是线程不安全的),如有需要请自行实现
  • Vector:

    • 底层实现为数组,不会存储元素数据,存储的只是元素的引用,真实的数据存储在内存中

    • 线程安全

    • 比较古老JDK1.0+,当时对于集合的设定还不态明确,功能比较少

    • 访问和插入删除操作于ArrayList的特性一致

    • 与ArrayList对比

      • 扩容步长确定,没设定则扩容比为100%

      • 没有延迟创建的优化,创建Vector对象的时候就已经将数组创建完毕

  • LinkedList:

    • 底层实现为链表,节点会在数据元素的基础上额外维护两个引用,一个指向当前元素的上一个元素,另一个指向下一个元素,所以直接存储的就是数据本身。

    • 元素的访问用的遍历判定来定位,O(n);

    • 插入删除操作直接修改节点的两个引用值即可O(1)(前提是获取到你要插入或者删除的节点对象,如果只有索引就需要先根据索引获取节点对象)

    • 线程不安全

选用:

  • ArrayList:由于其高效索引的特性,所以肯定要用于存储需要高速访问的数据上,插入和删除操作效率比较低,这里建议只在末尾进行元素的插入和删除操作,当然也不是不让用,改用还是得用,一般用于不会多线程并发访问。
  • Vector:一般情况下不会使用,除非你需要线程安全的容器可以使用
  • LinkedList:由于高并发高效添加修改的特性,经常用于元素需要高效进行插入删除操作的地方,但是很少用于索引访问的地方。线程不安全,需要则要编码者自行实现

集合的接口Collection/Set/List的区别

  • Collection
    • 是集合类型的顶层接口,上层接口Iterable可迭代的
    • 元素:无序、可重复、可以存在多个null值
  • Set
    • 继承于Collection
    • 元素:不可重复(任意的两个元素e1和e2都有e1.equals(e2)=false),无序
    • 由于无序的特性,元素之间并没有明确的先后顺序,访问时无法通过索引来对容器中的元素进行定位,方法中也都没有与索引相关的任何方法。对于元素的操作只能用元素自身来进行操作。
  • List
    • 继承于Collection
    • 元素:可重复,有序
    • 通过索引元素下标来访问,类似于Java中的数组的元素访问机制

扩展

forEach方法中传入的参数为Consumer消费者接口,Consumer接口又是一个函数式接口,forEach方法中遍历当前对象,使用函数视

list.forEach(System.out::print); // 特殊写法
/ 1
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}
 3 
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

所以说这个写法list.forEach(System.out::print)可以用于所有的函数式接口中

package com.yang2;

public class Test02 {
	public static void main(String[] args) {
		// 装备写法
		Test02.printT(System.out::println, 20);
		// lambda表达式
		Test02.printT((o) -> {
			System.out.println(o);
		}, 20);
		// 匿名内部类
		Test02.printT(new Test03() {

			@Override
			public void pp(Object obj) {
				System.out.println(obj);
			}
		}, 20);
	}

	public static void printT(Test03 test03, Object obj) {
		test03.pp(obj);
	}

}

@FunctionalInterface
interface Test03 {
	void pp(Object obj);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值