java中的ArrayList和LinkedList的底层剖析

引入:

数据结构的分类,数据结构可以分成:线性表,树形结构,图形结构。

  • 线性结构(线性表)包括:数组、链表、栈队列

  • 树形结构:二叉树、AVL树、红黑树、B树、堆、Trie、哈夫曼树、并查集

  • 图形结构:邻接矩阵、邻接表

线性表是具有存储n个元素的线性序列,常见的线性表有顺序表(数组)和链表两种实现方式。

为什么会出现ArrayList动态数组?

  • 在很多开发语言中数组有一个致命的缺点,就是数组的长度一旦确定,就不能再更改。
  • 我们在实际的开发中,更希望数组是可变的。

这个时候我们就可以使用数组去封装一个动态数组,java中的ArrayList的出现就是为了弥补数组长度不可变的缺点。

ArrayList的实现:

我们要对数组进行封装,也就是让这个数组如果元素装满了之后,重新创建一个更大的数组,将原来的元素复制过去。

分析:

实现动态数组,我们要创建一个类叫做ArrayList,因为他能存储所有引用类型的元素,我们要给它加一个泛型/,它要能动态获取它存储的元素的数量,所以它可以得到它应该有一个size成员,既然是一个动态数组,它的类成员一定有一个容器数组elements。这样我们就可以确定两个成员size和elements;

这个动态数组需要创建,所以我们得提供构造方法,既然它底层还是数组,那么创建的时候还是需要指定大小,可以自定义也可以创建一个默认大小的数组。java底层默认创建的是一个大小为10的数组。

java 复制代码
package com.lut.dynamicArray;

public class ArrayList<E> {
	//所装元素的数量
    public int size;
    //数组
    private E[] elements;
	//创建ArrayList底层数组默认的大小
    private static final int DEFAULT_CAPACITY = 10;
    
    //构造方法,自定义初始的ArrayList的大小,和java.util.ArrayList实现有所不同,可以自己进源码查看
    public ArrayList(int capacity){
        capacity = (capacity < DEFAULT_CAPACITY)?DEFAULT_CAPACITY:capacity;
        elements = (E[]) new Object[capacity];
    }
    public ArrayList(){
        //elements=new int[DEFAULT_CAPACITY];
        this(DEFAULT_CAPACITY);
    }
}

接口(方法)分析:

​ 这个类已经创建出来了,我们就需要向外界提供操作这个动态数组的方法。比如必须能够向这个ArrayList的对象里面添加元素、删除元素、按索引查找元素、按元素查索引、获取这个对象里面存储的元素个数等可以操作的方法。

​ 它至少包括以下方法:

java 复制代码
int size();// 元素的数量
boolean isEmpty(); // 是否为空
boolean contains(E element);// 是否包含某个元素
void add(E element);// 添加元素到最后面
E get(int index);/返回index位置对应的元素
E set(int index, E element);//设置index位置的元素
void add(int index, E element);//往index位置添加元素
E remove(int index);// 删除index位置对应的元素
int index0f(E element);// 查看元素的位置
void clear();// 清除所有元素

需要注意的是,我们需要通过add方法不断地向ArrayList的对象里面添加元素,如果元素操作了我们创建ArrayList指定的大小,这个时候就需要扩容,所以我们在添加元素之前,需要调用ensureCapacity方法(这个方法只是为了方便理解,官方方法写的很复杂,名字也不叫ensureCapacity,不够调用的时机和位置一样),确保下一个元素不会索引越界,如果越界就需要进行扩容处理,扩容是直接在原数组的大小基础上扩大1.5倍。

我们在查找元素的时候或者是按索引添加元素的时候可能存在索引大于了ArrayList的默认的size或者为负数等不合理的输入,这个时候我们需要在这些方法前面调用一个rangeCheck(index)函数,确保传入的索引合适,而添加的时候索引可以比size大1,所以单独定义了一个rangeCheckForAdd(index),如果rangeCheck或者rangeCheckForAdd方法在检查索引发现不合适的时候就会调用outOfBounds方法。

下面是自定义低配版ArrayList的简单实现

java 复制代码
private void ensureCapacity(int capacity) {
    int oldCapacity = elements.length;
    
    if(oldCapacity>=capacity) return;

    //新容量为旧容量的1.5倍
    int newCapacity = oldCapacity+(oldCapacity >> 1);
    //位运算右移1除以2,相当于旧容量1.5倍,相反左移1相当于乘以2
    E[] newElements = (E[]) new Object[newCapacity];
    for (int i = 0; i < size; i++) {
        newElements[i]=elements[i];
    }
    elements=newElements;
    System.out.println(oldCapacity+"扩容为:"+newCapacity);
}

下面对ArrayList进行了简单的实现

java 复制代码
package com.lut.dynamicArray;

@SuppressWarnings("unchecked")
public class ArrayList<E> {
    //元素的数量
    public int size;
    //所有的元素
    private E[] elements;
    //static能保证内存中只有一份此数据
    private static final int DEFAULT_CAPACITY = 10;
    //元素是否找到
    private static final int ELEMENT_NOT_FOUND = -1;
    public ArrayList(int capacity){
        capacity = (capacity < DEFAULT_CAPACITY)?DEFAULT_CAPACITY:capacity;
        elements = (E[]) new Object[capacity];//释放时机和外面的ArrayList同时释放
    }

    public ArrayList(){
        this(DEFAULT_CAPACITY);
    }

    /**
     * 清楚所有元素
     */
    public void clear(){
        for (int i = 0; i < size; i++) {
            elements[i] = null;
        }
        size = 0;
    }

    /**
     * 元素的数量
     * @return
     */
    public int size(){
        return size;
    }

    /**
     * 是否为空
     * @return
     */
    public boolean isEmpty(){
        return size==0;
    }

    /**
     * 是否包含某个元素
     * @param element
     * @return
     */
    public  boolean contains(E element){
        return indexOf(element) != ELEMENT_NOT_FOUND;
    }

    /**
     * 添加到元素尾部
     */
    public void add(E element){
        add(size,element);
    }

    /**
     * 获取index位置的元素
     */
    public E get(int index){
        rangeCheck(index);
        return elements[index];
    }

    /**
     * 设置index位置的元素
     * @return 原来的元素
     */
    public E set(int index,E element){
        rangeCheck(index);
        E old = elements[index];
        elements[index] = element;
        return old;
    }

    /**
     * 在index位置插入一个元素
     */
    public void add(int index,E element){
        rangeCheckForAdd(index);

        ensureCapacity(size+1);

        for (int i = size; i >index; i--) {
            elements[i]=elements[i-1];
        }
        elements[index]=element;
        size++;
    }

    /**
     * 删除index位置的元素
     * @return 返回删除的元素
     */
    public E remove(int index){
        rangeCheck(index);

        E temp = elements[index];
        for (int i = index+1; i < size; i++) {
            elements[i-1]=elements[i];
        }
        //size--;
        elements[--size] = null;
        return temp;
    }

    /**
     * 查看元素的索引,没找到返回-1
     */
    public int indexOf(E element){
        if(element == null){
            for (int i = 0; i < size; i++) {
                if(elements[i] == null) return i;
            }
        }else {
            for (int i = 0; i < size; i++) {
                //未重写的equals方法,就是默认比较的两个对象的地址
                if(element.equals(elements[i])) return i;
            }
        }

        return ELEMENT_NOT_FOUND;
    }

    /**
     * 保证要有capacity的容量
     * @param capacity
     */
    private void ensureCapacity(int capacity) {
        int oldCapacity = elements.length;
        
        if(oldCapacity>=capacity) return;

        //新容量为旧容量的1.5倍
        int newCapacity = oldCapacity+(oldCapacity >> 1);
        //位运算右移1除以2,相当于旧容量1.5倍,相反左移1相当于乘以2
        E[] newElements = (E[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newElements[i]=elements[i];
        }
        elements=newElements;
        System.out.println(oldCapacity+"扩容为:"+newCapacity);
    }

    private void outOfBounds(int index){
        throw new IndexOutOfBoundsException();
    }

    private void rangeCheck(int index){
        if(index<0||index>=size){
           outOfBounds(index);
        }
    }

    private void rangeCheckForAdd(int index){
        if(index<0||index>size){
            outOfBounds(index);
        }
    }
    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("size=").append(size).append(",[");
        for (int i = 0; i < size;java i++) {
            if(i!=0){
                string.append(",");
            }
            string.append(elements[i]);
        }
        string.append("]");
        return string.toString();
    }
}

仔细阅读上面的代码我们可以发现ArrayList有以下缺点:

  1. 固定大小:ArrayList的大小是固定的,一旦初始化后,无法动态调整大小,如果需要动态增加或减少元素,就需要创建一个新的数组elements,并将元素复制过去,这样会增加额外的开销。
  2. 性能问题:在插入或删除元素时,ArrayList的elements数组需要移动其他元素来保持连续性,这会导致性能下降,尤其是在大型数据集上。
  3. 内存占用:ArrayList在内存使用上比较消耗,因为它需要额外的空间来存储元素的索引和容量信息。

那么有没有和ArrayList一样的数据结构能够方便元素的插入删除,同时能够灵活的利用内存空间呢?有,LinkedList

LinkedList实现

分析:

LinkedList也是一个动态的线性表,所以它就必须能够存储元素,能够获取大小,ArrayList底层是通过一个数组存储元素,LinkedList内部是通过一个结点类来存储元素,同时这个Node里面有两个指针,prev指向前一个结点、next指向下一个结点,同时提供一个构造方法,当我们需要添加一个元素的时候本质是添加一个结点,再把这个元素放入到这个结点内部,同时维护好这个结点的前后结点。

所以LinkedList需要两个成员至少size和一个内部类Node

Node:

java 复制代码
	private static class Node<E> {
		E element;
		Node<E> prev;
		Node<E> next;
		public Node(Node<E> prev, E element, Node<E> next) {
			this.prev = prev;
			this.element = element;
			this.next = next;
		}
	}

为了方便获取头节点,我们还需要指定一个头结点,我们需要一个成员变量Node类型的first

java 复制代码
package com.lut.list;

public class LinkedList<E>{
	private Node<E> first;
    private int size;
	
	private static class Node<E> {
		E element;
		Node<E> prev;
		Node<E> next;
		public Node(Node<E> prev, E element, Node<E> next) {
			this.prev = prev;
			this.element = element;
			this.next = next;
		}
	}
}

ArrayList和LinkedList我们经常使用,ArrayList又叫做动态数组底层是由数组实现的,LinkedList底层是基于链表实现的集合,它们都是线性的数据结构。

既然ArrayList和LinkedList都具有相同的方法,我们就可以把这些相同的方法给提取出来成为一个单独的接口,这个接口就是List接口。

java 复制代码
public interface List<E> {
    //如果元素未找到返回-1
	static final int ELEMENT_NOT_FOUND = -1;
	//清除所有元素
	void clear();
	//元素的数量
	int size();
	// 是否为空
	boolean isEmpty();
	//是否包含某个元素
	boolean contains(E element);
	//添加元素到尾部
	void add(E element);
    //获取index位置的元素
	E get(int index);
    //获取index位置的元素
	E set(int index, E element);
    //在index位置插入一个元素
	void add(int index, E element);
    //删除index位置的元素
	E remove(int index);
    //查看元素的索引
	int indexOf(E element);
}

所以我们只需要去LinkedList去实现这些接口就可以了,然后对于LinkedList里面还有一些和ArrayList连实现都一样的方法,我们就可以单独写一个AbstractList/抽象类来复用相同的方法。

java 复制代码
public abstract class AbstractList<E>  {
   /**
    * 元素的数量
    */
   protected int size;
   /**
    * 元素的数量
    * @return
    */
   public int size() {
      return size;
   }

   /**
    * 是否为空
    * @return
    */
   public boolean isEmpty() {
       return size == 0;
   }

   /**
    * 是否包含某个元素
    * @param element
    * @return
    */
   public boolean contains(E element) {
      return indexOf(element) != ELEMENT_NOT_FOUND;
   }

   /**
    * 添加元素到尾部
    * @param element
    */
   public void add(E element) {
      add(size, element);
   }
   
   protected void outOfBounds(int index) {
      throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
   }
   
   protected void rangeCheck(int index) {
      if (index < 0 || index >= size) {
         outOfBounds(index);
      }
   }
   java
   protected void rangeCheckForAdd(int index) {
      if (index < 0 || index > size) {
         outOfBounds(index);
      }
   }
}

现在我们就可以直接实现专注实现List里面的不同的方法了。

需要注意理解里面的add方法,添加的时候,无论是调用add(E element)还是add(int index,E element),我们调用的add(E element)方法均会成为调用add(int index,E element)方法,原因是 public void add(E element) {add(size, element);}。

还需要注意,如果我们第一次添加的时候,需要把first指向它

下面是LinkedList的简单实现:

java 复制代码
package com.lut.list;

public class LinkedList<E> extends AbstractList<E> implements List<E>{
    private Node<E> first;

    private static class Node<E>{
        E element;
        Node<E> next;

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

    @Override
    public void clear() {
        size = 0;
        first = null;
    }

    @Override
    public E get(int index) {
        return node(index).element;
    }
    //复杂度跟node方法有关,它的复杂度为O(n)

    @Override
    public E set(int index, E element) {
        Node<E> node = node(index);
        E old = node.element;
        node.element = element;
        return old;
    }
    //O(n)

    @Override
    public void add(int index, E element) {
        if(index == 0){
            first = new Node<>(element,first);
        }else{
            Node<E> prev = node(index - 1);
            prev.next = new Node<>(element, prev.next);
        }
        size++;
    }
    //最好:O(1)
    //最坏:O(n)
    //平均:O(n)
    //可以换一个算法,传入一个节点直接可以删,这样才体现链表的节点,增删快查找慢
    @Override
    public E remove(int index) {
        E old = node(index).element;
        if(index == 0){
            first=first.next;
        }else{
            Node<E> prev = node(index - 1);
            prev.next = prev.next.next;
        }
        size--;
        return old;
    }
    //O(n)

    @Override
    public int indexOf(E element) {
        if (element == null) {
            for (int i = 0; i < size; i++) {
                if (element == node(i).element) {
                    return i;
                }
            }
        } else {
            for (int i = 0; i < size; i++) {
                if (element.equals(node(i).element)) {
                    return i;
                }
            }
        }
        return ELEMENT_NOT_FOUND;
    }

    /**
     * 获取index位置对应的节点的对象
     */
    private Node<E> node(int index){
        rangeCheck(index);

        Node<E> node = first;
        for (int i = 0; i < index; i++) {
            node = node.next;
        }
        return node;
    }

    @Override
    public String toString() {
        StringBuilder string = new StringBuilder();
        string.append("size=").append(size).append(",[");
        for (int i = 0; i < size; i++) {
            if(i!=0){
                string.append(",");
            }
            string.append(node(i).element);
        }
        string.append("]");
        return string.toString();
    }
}

ArrayList优化:上面我们的ArrayList只是在添加满的时候进行了扩容,但是对于删除的时候我们还需要进行缩容,不然会浪费存储空间。缩容的时机是当我们删除元素的时候,在remove方法之前进行判断是否可以进行缩容,如果可以就进行缩容操作。

java 复制代码
public class ArrayList<E> extends AbstractList<E> implements List<E>{
   /**
    * 所有的元素
    */
   private E[] elements;
   private static final int DEFAULT_CAPACITY = 10;
   
   public ArrayList2(int capaticy) {
      capaticy = (capaticy < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capaticy;
      elements = (E[]) new Object[capaticy];
   }
   
   public ArrayList2() {
      this(DEFAULT_CAPACITY);
   }
   
   /**
    * 清除所有元素
    */
   public void clear() {
      for (int i = 0; i < size; i++) {
         elements[i] = null;
      }
      size = 0;
      
      // 仅供参考
      if (elements != null && elements.length > DEFAULT_CAPACITY) {
         elements = (E[]) new Object[DEFAULT_CAPACITY];
      }
   }

   /**
    * 获取index位置的元素
    * @param index
    * @return
    */
   public E get(int index) { // O(1)
      rangeCheck(index);
      
      return elements[index]; 
   }

   /**
    * 设置index位置的元素
    * @param index
    * @param element
    * @return 原来的元素ֵ
    */
   public E set(int index, E element) { // O(1)
      rangeCheck(index);
      
      E old = elements[index];
      elements[index] = element;
      return old;
   }

   /**
    * 在index位置插入一个元素
    * @param index
    * @param element
    */
   public void add(int index, E element) { 
      /*
       * 最好:O(1)
       * 最坏:O(n)
       * 平均:O(n)
       */
      rangeCheckForAdd(index);
      
      ensureCapacity(size + 1);
      
      for (int i = size; i > index; i--) {
         elements[i] = elements[i - 1];
      }
      elements[index] = element;
      size++;
   } // size是数据规模

   /**
    * 删除index位置的元素
    */
   public E remove(int index) {
      /*
       * 最好:O(1)
       * 最坏:O(n)
       * 平均:O(n)
       */
      rangeCheck(index);
      
      E old = elements[index];
      for (int i = index + 1; i < size; i++) {
         elements[i - 1] = elements[i];
      }
      elements[--size] = null;
      
      trim();
      
      return old;
   }

   /**
    * 查看元素的索引
    */
   public int indexOf(E element) {
      if (element == null) {
         for (int i = 0; i < size; i++) {
            if (elements[i] == null) return i;
         }
      } else {
         for (int i = 0; i < size; i++) {
            if (element.equals(elements[i])) return i;
         }
      }
      return ELEMENT_NOT_FOUND;
   }
   
   /**
    * 保证要有capacity的容量
    * @param capacity
    */
   private void ensureCapacity(int capacity) {
      int oldCapacity = elements.length;
      if (oldCapacity >= capacity) return;
      
      // 新容量为旧容量的1.5倍
      int newCapacity = oldCapacity + (oldCapacity >> 1);
      
      // 新容量为旧容量的2倍
      // int newCapacity = oldCapacity << 1;
      E[] newElements = (E[]) new Object[newCapacity];
      for (int i = 0; i < size; i++) {
         newElements[i] = elements[i];
      }
      elements = newElements;
      
      System.out.println(oldCapacity + "扩容为" + newCapacity);
   }
   
   private void trim() {
      // 30
      int oldCapacity = elements.length;
      // 15
      int newCapacity = oldCapacity >> 1;
      if (size > (newCapacity) || oldCapacity <= DEFAULT_CAPACITY) return;
      
      // 剩余空间还很多
      E[] newElements = (E[]) new Object[newCapacity];
      for (int i = 0; i < size; i++) {
         newElements[i] = elements[i];
      }
      elements = newElements;
      
      System.out.println(oldCapacity + "缩容为" + newCapacity);
   }
   
   @Override
   public String toString() {
      // size=3, [99, 88, 77]
      StringBuilder string = new StringBuilder();
      string.append("size=").append(size).append(", [");
      for (int i = 0; i < size; i++) {
         if (i != 0) {
            string.append(", ");
         }
         
         string.append(elements[i]);
         
      }
      string.append("]");
      return string.toString();
   }
}

总结:

ArrayList 和 LinkedList 的区别是什么?

ArrayList 和 LinkedList 是 Java 中常用的两种集合类,它们在实现上有一些区别:

  1. 内部数据结构:

    • ArrayList 使用数组来存储元素,可以通过索引直接访问元素,因此查找速度较快,但插入和删除操作需要移动元素,效率较低。
    • LinkedList 使用双向链表来存储元素,每个元素都包含对前一个和后一个元素的引用,插入和删除操作只需改变相邻元素的引用,效率较高,但查找元素需要遍历链表,效率较低。
  2. 内存占用:

    • ArrayList 在添加或删除元素时可能需要重新分配内存空间,因为数组的大小是固定的,可能会导致内存碎片。
    • LinkedList 每个元素都需要额外的空间来存储前后元素的引用,可能会占用更多的内存空间。
  3. 随机访问和遍历:

    • ArrayList 支持随机访问,可以通过索引直接访问元素,适合需要频繁访问元素的场景。
    • LinkedList 需要从头或尾开始遍历链表才能访问元素,不支持随机访问,适合需要频繁插入和删除元素的场景。

综上所述,如果需要频繁进行插入和删除操作,可以选择使用 LinkedList;如果需要频繁进行随机访问操作,可以选择使用 ArrayList。

ArrayList的扩容和缩容是咋做的(基于官方的ArrayList)?

​ ArrayList在创建的时候可以指定一个数组的大小,或者默认给出一个长度为10的数组,当我们每次向这个集合添加元素的时候,先会去判断一下数组的容量还够不够,如果不够就需要去扩容,每次扩容会扩容原来容量的1.5倍。

​ 而ArrayList 的缩容是自动进行的,无需手动操作。当 ArrayList 中的元素数量减少到当前容量的一半以下时,ArrayList 会自动进行缩容操作,将容量减半。这样可以节省内存空间,并提高性能。因此,开发者无需手动调用任何方法来进行 ArrayList 的缩容操作。

相关推荐
倔强的石头1066 分钟前
【C++指南】类和对象(九):内部类
开发语言·c++
Jackey_Song_Odd7 分钟前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
ProtonBase10 分钟前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
Watermelo61711 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v16 分钟前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Evand J2 小时前
LOS/NLOS环境建模与三维TOA定位,MATLAB仿真程序,可自定义锚点数量和轨迹点长度
开发语言·matlab
LucianaiB2 小时前
探索CSDN博客数据:使用Python爬虫技术
开发语言·爬虫·python
️南城丶北离2 小时前
[数据结构]图——C++描述
数据结构··最小生成树·最短路径·aov网络·aoe网络