List之ArrayList、LinkedList深入分析

集合

Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:ListSetQueue

Java 集合框架如下图所示:

Java 集合框架概览

Collection接口

概念:对象的容器,定义了对多个对象进项操作的的常用方法。可实现数组的功能。
与数组的区别:数组长度固定,集合长度不固定;数组可以存储基本类型和引用类型,集合只能存储引用类型
位置: java.util.*

接口中的方法

boolean add(Object obj) //添加一个对象。
boolean addAll(Collection c) //讲一个集合中的所有对象添加到此集合中。
void clear() //清空此集合中的所有对象。
boolean contains(Object o) //检查此集合中是否包含o对象。
boolean equals(Object o) //比较此集合是否与指定对象相等。
boolean isEmpty() //判断此集合是否为空。
boolean remove(Object o) //在此集合中移除o对象。
int size() //返回此集合中的元素个数。
Object[] toArray() //将此集合转换成数组。
/**
 * Collection接口的使用(一)
 * 1.添加元素
 * 2.删除元素
 * 3.遍历元素
 * 4.判断
 */
public class Demo1{
    pubic static void main(String[] args){
        //创建集合
        Collection collection = new ArrayList();        
//      * 1.添加元素
        Collection.add("苹果");
        Collection.add("西瓜");
        Collection.add("榴莲");
        System.out.println("元素个数:"+collection.size());
        System.out.println(collection);
//      * 2.删除元素
        collection.remove("榴莲");
        System.out.println("删除之后:"+collection.size());
//      * 3.遍历元素
        //3.1 使用增强for 
        for(Object object : collection){
            System.out.println(object);
        }
        //3.2 使用迭代器(迭代器专门用来遍历集合的一种方式)
        //hasnext();判断是否有下一个元素
        //next();获取下一个元素
        //remove();删除当前元素
        Iterator iterator=collection.Itertor();
        while(iterator.hasnext()){
            String object=(String)iterator.next();
            System.out.println(s);
            //删除操作
            //collection.remove(s);引发错误:并发修改异常
            //iterator.remove();应使用迭代器的方法
//      * 4.判断
        System.out.println(collection.contains("西瓜"));//true
        System.out.println(collection.isEmpty());//false
        }
    }
}

Collection子接口

List集合

特点:有序、有下标、元素可以重复

/**
 * List子接口的使用(一)
 * 特点:1.有序有下标 2.可以重复
 * 
 * 1.添加元素
 * 2.删除元素
 * 3.遍历元素
 * 4.判断
 * 5.获取位置
 */
public class Demo3 {
	public static void main(String[] args) {
		List list=new ArrayList<>();
		//1.添加元素
		list.add("tang");
		list.add("he");
		list.add(0,"yu");//插入操作
		System.out.println("元素个数:"+list.size());
		System.out.println(list.toString());
		//2.删除元素
		list.remove(0);
		//list.remove("yu");结果同上
		System.out.println("删除之后:"+list.size());
		System.out.println(list.toString());
		//3.遍历元素
		//3.1 使用for遍历
		for(int i=0;i<list.size();++i) {
			System.out.println(list.get(i));
		}
		//3.2 使用增强for
		for(Object object:list) {
			System.out.println(object);
		}
		//3.3 使用迭代器
		Iterator iterator=list.iterator();
		while (iterator.hasNext()) {
			System.out.println(iterator.next());
		}
		//3.4使用列表迭代器,listIterator可以双向遍历,添加、删除及修改元素。
		ListIterator listIterator=list.listIterator();
		//从前往后
		while (listIterator.hasNext()) {
			System.out.println(listIterator.next());		
		}
		//从后往前(此时“遍历指针”已经指向末尾)
		while(listIterator.hasPrevious()) {
			System.out.println(listIterator.previous());
		}
		//4.判断
		System.out.println(list.isEmpty());
		System.out.println(list.contains("tang"));
		//5.获取位置
		System.out.println(list.indexOf("tang"));
	}
}
/**
 * List子接口的使用(二)
 * 1.添加元素
 * 2.删除元素
 * 3.遍历元素
 * 4.判断
 * 5.获取位置
 */
public class Demo4 {
	public static void main(String[] args) {
		List list=new ArrayList();
		//1.添加数字数据(自动装箱)
		list.add(20);
		list.add(30);
		list.add(40);
		list.add(50);
		System.out.println("元素个数:"+list.size());
		System.out.println(list.toString());
		//2.删除元素
		list.remove(0);
		//list.remove(20);很明显数组越界错误,改成如下
		//list.remove(Object(20));
		//list.remove(new Integer(20));
		System.out.println("元素个数:"+list.size());
		System.out.println(list.toString());
		//3-5不再演示,与之前类似
		//6.补充方法subList,返回子集合,含头不含尾
		List list2=list.subList(1, 3);//[1,3)
		System.out.println(list2.toString());	
	}
}
ArrayList【重点】

ArrayList是List接口的实现类,也是开发当中最常用的集合类

  • 数组结构实现,查询快、增删慢;
  • JDK1.2版本,运行效率快、线程不安全。
/**
 * ArrayList的使用
 * 存储结构:数组;
 * 特点:查找遍历速度快,增删慢。
 * 1.添加元素
 * 2.删除元素
 * 3.遍历元素
 * 4.判断
 * 5.查找
 */
public class Demo5 {
	public static void main(String[] args) {
		ArrayList arrayList=new ArrayList<>();
		//1.添加元素
		Student s1=new Student("唐", 21);
		Student s2=new Student("何", 22);
		Student s3=new Student("余", 21);
		arrayList.add(s1);
		arrayList.add(s2);
		arrayList.add(s3);
		System.out.println("元素个数:"+arrayList.size());
		System.out.println(arrayList.toString());
		//2.删除元素
		arrayList.remove(s1);
		//arrayList.remove(new Student("唐", 21));
		//注:这样可以删除吗(不可以)?显然这是两个不同的对象。
		//假如两个对象属性相同便认为其是同一对象,那么如何修改代码?
        //romove的底层是调用对象的equals方法
        //所以想对象属性相同就修改,就必须修改对象的equals方法
		//        若一定要写包含任何类型的集合,可以这样写,Object就是包含任何类
//        ArrayList<Object> list1=new ArrayList<>();

//        1.获取某个索引位置处的元素值get(index)
        String e=list.get(4);
        System.out.println(e);

//        2.获取集合的大小size()
        System.out.println(list.size());

//        3.完成遍历
        for(int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

//        4.删除某个索引位置的元素值,并返回被删除的元素值remove(index)
        System.out.println(list);
        String e2=list.remove(1);
        System.out.println("e2=" + e2);
        System.out.println(list);

//        5.直接删除元素值,remove(value) 删除成功返回true,删除失败返回false,如果存在多个相同的元素,只会删除第一次出现的目标元素
        System.out.println(list.remove("hwq"));
        System.out.println(list);

//        6.修改某个索引位置处的元素值,并会返回被修改的元素 set(index,element(要修改的值))
        String name=list.set(0,"牛");
        System.out.println(name);
        System.out.println(list);

//        7.contains:查找元素是否存在
        System.out.println(list.contains("牛"));

//        8.clear 清空集合
        list.clear();

//        9.isEmpty 判断是否为空
        System.out.println(list.isEmpty());

//        10.addAll:添加多个元素
        ArrayList list1 = new ArrayList();
        list1.add("红雷梦");
        list1.add("三国");
        list.addAll(list1);
        System.out.println(list);

//        11.containsAll 查找多个元素
        System.out.println(list.containsAll(list1));

//        12.removeAll:删除多个元素
        list.removeAll(list1);
        System.out.println(list);
	}
}
ArrayList源码分析【重点】

ArrayList 是Java集合框架中常用的动态数组实现,它提供了动态增长的能力,允许在数组尾部进行快速的随机访问。下面我们将对 ArrayList 的源码进行分析,深入了解其实现原理和关键方法。

1. 类声明和继承关系
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // ...
}
  • ArrayList 类继承自 AbstractList,实现了 List 接口,同时还实现了 RandomAccess(随机访问标记接口)、Cloneable(可克隆接口)、Serializable(可序列化接口)。
2. 成员变量
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
private static final int DEFAULT_CAPACITY = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  • elementData:存储元素的数组。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:空数组,用于延迟数组的初始化。
  • DEFAULT_CAPACITY:默认初始容量。
  • MAX_ARRAY_SIZE:数组的最大容量。
3. 构造方法
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " +
                initialCapacity);
    }
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
  • ArrayList 提供了多个构造方法,包括默认构造方法、指定初始容量的构造方法以及接收 Collection 的构造方法。
  • 在默认构造方法中,使用了延迟加载的技术,初始时 elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA也就是空数组这点非常重要‼️,只有在第一次add添加元素时才会初始化为默认容量(10)的数组。
4. 核心方法
4.1 add 方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • add 方法:添加元素,确保容量足够,如果需要,进行数组扩容。
  • ensureCapacityInternal 方法:确保容量足够的内部方法,负责调用 ensureExplicitCapacity 进行实际的扩容操作。
  • calculateCapacity 方法:计算容量的方法,处理空数组的情况。
  • grow 方法:数组扩容的核心方法,通过 Arrays.copyOf 进行扩容,新容量为原容量的1.5倍。
理解过程
//这是一个空的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

这段源码说明当你没有向集合中添加任何元素时,集合容量为0。那么默认的10个容量怎么来的呢?

这就得看看add方法的源码了:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

假设你new了一个数组,当前容量为0,size当然也为0。这时调用add方法进入到ensureCapacityInternal(size + 1);该方法源码如下:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

该方法中的参数minCapacity传入的值为size+1也就是 1,接着我们再进入到calculateCapacity(elementData, minCapacity)里面:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

上文说过,elementData就是存放元素的数组,当前容量为0,if条件成立,返回默认容量DEFAULT_CAPACITY也就是10。这个值作为参数又传入ensureExplicitCapacity()方法中,进入该方法查看源码:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0) // 当需要的空间大于现在数组的容量就要考虑扩容
        grow(minCapacity); //数组扩容
}

我们先不要管modCount这个变量。

因为elementData数组长度为0,所以if条件成立,调用grow方法,重要的部分来了,我们再次进入到grow方法的源码中:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length; // 现在数组的容量0
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新数组的容量0
    if (newCapacity - minCapacity < 0) // 成立
        newCapacity = minCapacity;//newCapacity = 10
    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);
}

这个方法先声明了一个oldCapacity变量将数组长度赋给它,其值为0;又声明了一个newCapacity变量其值为oldCapacity+一个增量,可以发现这个增量是和原数组长度有关的量,当然在这里也为0。第一个if条件满足,newCapacity的值为10(这就是默认的容量,不理解的话再看看前面)。第二个if条件不成立,也可以不用注意,因为MAX_ARRAY_SIZE的定义如下:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

这个值太大了以至于第二个if条件没有了解的必要。

最后一句话就是为elementData数组赋予了新的长度,Arrays.copyOf()方法返回的数组是新的数组对象,原数组对象不会改变,该拷贝不会影响原来的数组。copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

这时候再回到add的方法中,接着就向下执行elementData[size++] = e;到这里为止关于ArrayList就讲解得差不多了,当数组长度为10的时候你们可以试着过一下源码,查一下每次的增量是多少(答案是每次扩容为原来的1.5倍

总结

  1. 存储元素的结构是Object[]数字,具体的Object由new ArrayList();的T决定

  2. 没有给定初始容量,默认初始容量为10,当添加元素后才初始化为10

  3. 达到额定容量,每次扩容为原来的1.5倍,比如初始容量为10,当添加第11个元素的时候,发现容量不足了,就会调用grow方法进行扩容,扩容后容量为(10 + 10 / 2) = 15 ,如果初始容量为33,则扩容后的容量为(33 + 33 / 2) = 49,会舍弃小数部分,因为底层使用了位运算 >> 1

Vector

Vector在实际开发中使用较少,为了保证线程安全,很多方法使用了synchronized而牺牲了性能,大部分源码和ArrayList相同,这里就不深入介绍了

在这里插入图片描述

  • 数组结构实现,查询快、增删慢;
  • JDK1.0版本,运行效率慢、线程安全。
/**
 * Vector的演示使用
 * 
 *1.添加数据
 *2.删除数据
 *3.遍历
 *4.判断
 */
public class Demo1 {
	public static void main(String[] args) {
		Vector vector=new Vector<>();
		//1.添加数据
		vector.add("tang");
		vector.add("he");
		vector.add("yu");
		System.out.println("元素个数:"+vector.size());
		//2.删除数据
		/*
		 * vector.remove(0); vector.remove("tang");
		 */
		//3.遍历
		//使用枚举器
		Enumeration enumeration=vector.elements();
		while (enumeration.hasMoreElements()) {
			String s = (String) enumeration.nextElement();
			System.out.println(s);
		}
		//4.判断
		System.out.println(vector.isEmpty());
		System.out.println(vector.contains("he"));
		//5. Vector其他方法
		//firstElement()  lastElement()  ElementAt();
	}
}
LinkedList
  • 特点:链表结构实现,增删快,查询慢
/**
 * LinkedList的用法
 * 存储结构:双向链表
 * 1.添加元素
 * 2.删除元素
 * 3.遍历
 * 4.判断
 */
public class Demo2 {
	public static void main(String[] args) {
		LinkedList linkedList=new LinkedList<>();
		Student s1=new Student("唐", 21);
		Student s2=new Student("何", 22);
		Student s3=new Student("余", 21);
		//1.添加元素
		linkedList.add(s1);
		linkedList.add(s2);
		linkedList.add(s3);
		linkedList.add(s3);
		System.out.println("元素个数:"+linkedList.size());
		System.out.println(linkedList.toString());
		//2.删除元素
		/*
		 * linkedList.remove(new Student("唐", 21));
		 * System.out.println(linkedList.toString());
		 */
		//3.遍历
		//3.1 使用for
		for(int i=0;i<linkedList.size();++i) {
			System.out.println(linkedList.get(i));
		}
		//3.2 使用增强for
		for(Object object:linkedList) {
			Student student=(Student) object;
			System.out.println(student.toString());
		}
		//3.3 使用迭代器
		Iterator iterator =linkedList.iterator();
		while (iterator.hasNext()) {
			Student student = (Student) iterator.next();
			System.out.println(student.toString());
		}
		//3.4 使用列表迭代器(略)
		//4. 判断
		System.out.println(linkedList.contains(s1));
		System.out.println(linkedList.isEmpty());
		System.out.println(linkedList.indexOf(s3));
	}
}
LinkedList源码分析

LinkedList首先有三个属性:

  • 链表大小:transient int size = 0;
  • (指向)第一个结点/头结点:transient Node<E> first;
  • (指向)最后一个结点/尾结点:transient Node<E> last;

关于Node类型我们再进入到类里看看:

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

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

首先item存放的是实际数据;next指向下一个结点而prev指向上一个结点。

Node带参构造方法的三个参数分别是前一个结点、存储的数据、后一个结点,调用这个构造方法时将它们赋值给当前对象。

LinkedList是如何添加元素的呢?先看看add方法:

public boolean add(E e) {
    linkLast(e);
    return true;
}

进入到linkLast方法:

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;
    size++;
    modCount++;
}

假设刚开始new了一个LinkedList对象,first和last属性都为空,调用add进入到linkLast方法。

首先创建一个Node变量 l 将last(此时为空)赋给它,然后new一个newNode变量存储数据,并且它的前驱指向l,后继指向null;再把last指向newNode。如下图所示:

img

如果满足if条件,说明这是添加的第一个结点,将first指向newNode:

img

至此,LinkedList对象的第一个数据添加完毕。假设需要再添加一个数据,我们可以再来走一遍,过程同上不再赘述,图示如下:

img

ArrayList和LinkedList区别
  • ArrayList:必须开辟连续空间,查询快,增删慢。
  • LinkedList:无需开辟连续空间,查询慢,增删快。

img

总结

在不能提前确定容器大小的情况下可以使用List接口实现的集合类,ArrayList适合频繁查询、修改的场景,不适合频繁增删的场景;而LinkedList则相反

如果觉得本篇文章对你有帮助,可否点个小赞😺;篇幅较长建议收藏🫠;关注一手等待后续更新更多干货🚀

相关推荐

  1. Pythonlist

    2024-03-11 10:24:03       44 阅读
  2. ScalaList

    2024-03-11 10:24:03       34 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-11 10:24:03       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-11 10:24:03       106 阅读
  3. 在Django里面运行非项目文件

    2024-03-11 10:24:03       87 阅读
  4. Python语言-面向对象

    2024-03-11 10:24:03       96 阅读

热门阅读

  1. git pull 跟 git pull origin master的区别

    2024-03-11 10:24:03       40 阅读
  2. 佛祖保佑,永不宕机,永无BUG

    2024-03-11 10:24:03       45 阅读
  3. BUG:Enigma Virtual Box打包.net独立程序不正常

    2024-03-11 10:24:03       43 阅读
  4. 判断cursor是否为空

    2024-03-11 10:24:03       49 阅读
  5. CocoaPods 安装使用

    2024-03-11 10:24:03       48 阅读
  6. 解读电影级视频生成模型 MovieFactory

    2024-03-11 10:24:03       52 阅读
  7. 物联网行业如何发展新质生产力

    2024-03-11 10:24:03       48 阅读