JAVA面试必问之Collection要点

JAVA集合之Collection

JAVA提供的数组是用来存储固定大小的同类型元素。其具有以下两个缺点:
1.数组一旦初始化后,长度就确定了,无法进行数组的动态扩展;
2.数组存储元素是确定的,且一个数组中只能存储同一类型的。
为了改进以上缺点,JAVA提供了一个集合框架供我们使用,其中最主要的接口为Collection和Map,集合框架图如下:

集合和数组的区别

属性 数组 集合
长度 固定 动态扩展
存储内容 同一类型元素 不同类型元素(但一般不会这样做)
存储元素类型 基本类型和引用类型 引用类型

Collection常用方法

方法名 说明
boolean add(E e) 向集合添加元素e,若指定集合元素改变了则返回true
boolean addAll(Collection<? extends E> c) 把集合C中的元素全部添加到集合中,若指定集合元素改变返回true(相当于集合取并)
void clear() 清空所有集合元素
boolean contains(Object o) 判断指定集合是否包含对象o
boolean containsAll(Collection<?> c) 判断指定集合是否包含集合c的所有元素
boolean isEmpty() 判断指定集合的元素size是否为0
boolean remove(Object o) 删除集合中的元素对象o,若集合有多个o元素,则只会删除第一个元素
boolean removeAll(Collection<?> c) 删除指定集合包含集合c的元素(集合取补)
boolean retainAll(Collection<?> c) 从指定集合中保留包含集合c的元素,其他元素则删除(集合取交)
int size() 集合的元素个数
T[] toArray(T[] a) 将集合转换为T类型的数组
Interator it = collection.iterator() 获取集合迭代器

Collection中的List

Collection中的List常用的实现类有两个:ArrayList和LinkedList。其两者区别如下:

属性 ArrayList LinkedList
扩容方式 1.5倍扩容 直接增加
默认空实例初始化方式 懒惰(即第一次添加元素之后才初始化数组,之前任为空数组) 不同类型元素(但一般不会这样做)
初始容量 10 引用类型
支持随机访问
随机访问的时间复杂度 O(1) O(n)
增删元素的时间复杂度 O(n) O(1)
内存要求 必须连续 不连续

ArrayList源码部分分析

ArrayList源码比较长,在这里不全部展示,只挑选一些经典方法进行分析。

  • 从源码中可以看出ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。  

  • ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。  

  • ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。  

  • ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。

    1.ArrayList初始化

      // 默认初始容量大小为10
      private static final int DEFAULT_CAPACITY = 10;
      //默认空实例
      private static final Object[] EMPTY_ELEMENTDATA = {};
    
       //用于默认大小空实例的共享空数组实例。
        //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
      private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
      //保存ArrayList数据的数组,即ArrayList底层实际上还是一个数组
      transient Object[] elementData; 
    
      //ArrayList 所包含的元素个数
      private int size;
    
      //带初始容量参数的构造函数。(用户自己指定容量)
      public ArrayList(int initialCapacity) {
          if (initialCapacity > 0) {
              //创建initialCapacity大小的数组
              this.elementData = new Object[initialCapacity];
          } else if (initialCapacity == 0) {
              //创建空数组
              this.elementData = EMPTY_ELEMENTDATA;
          } else {
              throw new IllegalArgumentException("Illegal Capacity: "+
                                                 initialCapacity);
          }
      }
    
      //默认构造函数,DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10
      public ArrayList() {
          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
      }

    2.扩容机制

    ArrayList的扩容机制提高了性能,如果每次只扩充一个,那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制使用每次扩容1.5倍大小的方式避免了这种情况。

       //扩容函数,确保ArrayList能容纳元素,其中minCapacity 为所需的最小容量
      public void ensureCapacity(int minCapacity) {
          //若当前ArrayList还没有初始化时,minExpand=0
          int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) ? 0 : DEFAULT_CAPACITY;
          //若所需大于当前的容量,则扩容
          if (minCapacity > minExpand) {
              ensureExplicitCapacity(minCapacity);
          }
      }
     //得到最小扩容量
      private void ensureCapacityInternal(int minCapacity) {
          if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                // 获取默认的容量和传入参数的较大值
              minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
          }
    
          ensureExplicitCapacity(minCapacity);
      }
    //判断是否需要扩容
      private void ensureExplicitCapacity(int minCapacity) {
          modCount++;
    
      // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。 
      //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1
          if (minCapacity - elementData.length > 0)
              //调用grow方法进行扩容,调用此方法代表已经开始扩容了
              grow(minCapacity);
      }
    
      //要分配的最大数组大小
      private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
      //ArrayList扩容的核心方法。
      private void grow(int minCapacity) {
          // oldCapacity为旧容量,newCapacity为新容量
          int oldCapacity = elementData.length;
          //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
          //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
          int newCapacity = oldCapacity + (oldCapacity >> 1);
          //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
          if (newCapacity - minCapacity < 0)
              newCapacity = minCapacity;
          //再检查新容量是否超出了ArrayList所定义的最大容量,
          //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
          //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
          if (newCapacity - MAX_ARRAY_SIZE > 0)
              newCapacity = hugeCapacity(minCapacity);
    
          elementData = Arrays.copyOf(elementData, newCapacity);
      }
      //比较minCapacity和 MAX_ARRAY_SIZE
      private static int hugeCapacity(int minCapacity) {
          if (minCapacity < 0) 
              throw new OutOfMemoryError();
          return (minCapacity > MAX_ARRAY_SIZE) ?
              Integer.MAX_VALUE :
              MAX_ARRAY_SIZE;
      }

    ArrayList中的数组拷贝方法:System.arraycopy()和Arrays.copyof()

在ArrayList的add(int index, E element)方法中使用到了 arraycopy() 方法让数组自己复制自己实现让index开始之后的所有成员后移一个位置:

    //在此列表中的指定位置插入指定的元素。
    public void add(int index, E element) {
        //先调用 rangeCheckForAdd 对index进行界限检查
        rangeCheckForAdd(index);
        //然后调用 ensureCapacityInternal 方法保证capacity足够大
        ensureCapacityInternal(size + 1);  
        //arraycopy()方法实现数组自己复制自己:从最后开始将成员后移一个位置,直到将element插入index位置;最后size加1
        //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量;
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        elementData[index] = element;
        size++;
    }

在ArrayList的toArray()方法中用到了 copyOf() 方法:

    //以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。
     //该方法必须分配一个新的数组
    public Object[] toArray() {
    //elementData:要复制的数组;size:要复制的长度
        return Arrays.copyOf(elementData, size);
    }

以上二者的联系:看两者源代码可以发现copyOf()内部调用了System.arraycopy()方法
以上二者的区别:arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位;copyOf()是系统自动在内部新建一个数组,并返回该数组。

LinkedList源码部分分析

LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的。

LinkedList的内部节点Node

LinkedList会将存入的对象包装为一个节点Node,并记录其前驱节点和后继节点,因此LinkedList是一个双端队列,其插入和删除都可以指明方向。

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;
        }
    }

LinkedList的增加元素的方法

  • add(E e):将元素添加到链表尾部
public boolean add(E e) {
        linkLast(e);//这里就只调用了这一个方法
        return true;
    }

   //linkLast方法:链接使e作为最后一个元素。
    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++;
    }    
  • addFirst(E e): 将元素添加到链表头部
 public void addFirst(E e) {
        linkFirst(e);
    }
//linkFirst方法:链接使e作为第一个元素。
private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);//新建节点,以头节点为后继节点
        first = newNode;
        //如果链表为空,last节点也指向该节点
        if (f == null)
            last = newNode;
        //否则,将头节点的前驱指针指向新节点,也就是指向前一个元素
        else
            f.prev = newNode;
        size++;
        modCount++;
    }
  • addLast(E e): 将元素添加到链表尾部,与 add(E e) 方法一样

    public void addLast(E e) {
          linkLast(e);
      }
  • add(int index, E e):在指定位置添加元素

    public void add(int index, E element) {
          checkPositionIndex(index); //检查索引是否处于[0-size]之间
    
          if (index == size)//添加在链表尾部
              linkLast(element);
          else//添加在链表中间
              linkBefore(element, node(index));
      }
    //返回下标为index的node节点
    Node<E> node(int index) {
      //使用对半查找,如果在index前半部分,则从头部使用next开始找
      if (index < (size >> 1)) {
          Node<E> x = first;
          for (int i = 0; i < index; i++)
              x = x.next;
          return x;
      } else { //如果在index后半部分,则从尾部prev开始找
          Node<E> x = last;
          for (int i = size - 1; i > index; i--)
              x = x.prev;
          return x;
      }
    }

LinkedList的获取元素的方法

根据索引获取元素
  • get(int index): 根据指定索引返回数据
    public E get(int index) {
          //检查index范围是否在size之内
          checkElementIndex(index);
          //调用Node(index)去找到index对应的node然后返回它的值(node源码见上)
          return node(index).item;
      }
    获取头节点
  • getFirst():获取链表头节点,若链表为空抛出异常;
  • element():本质就是getFirst(),性质同上。
  • peekFirst():获取链表头节点,若链表为空返回null;
  • peek():本质就是peekFirst(),性质同上;
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
public E element() {
    return getFirst();
}

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
 }
获取尾节点
  • getLast():获取尾节点,在链表为空时,会抛出NoSuchElementException;
  • peekLast() : 获取尾节点,在链表为空时,返回null;
    public E getLast() {
      final Node<E> l = last;
      if (l == null)
          throw new NoSuchElementException();
      return l.item;
    }
    public E peekLast() {
      final Node<E> l = last;
      return (l == null) ? null : l.item;
    }
    根据对象获取对应索引
  • int indexOf(Object o): 从头遍历找
    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;
    }
  • int lastIndexOf(Object o): 从尾遍历找
    public int lastIndexOf(Object o) {
      int index = size;
      if (o == null) {
          //从尾遍历
          for (Node<E> x = last; x != null; x = x.prev) {
              index--;
              if (x.item == null)
                  return index;
          }
      } else {
          //从尾遍历
          for (Node<E> x = last; x != null; x = x.prev) {
              index--;
              if (o.equals(x.item))
                  return index;
          }
      }
      return -1;
    }

    LinkedList检查是否包含某对象的方法

  • contains(Object o): 检查对象o是否存在于链表中
    public boolean contains(Object o) {
      return indexOf(o) != -1;
    }

    LinkedList删除元素

  • remove() ,removeFirst(),pop(): 删除头节点,本质都是removeFirst(),若为空链表则抛出异常;
  • removeLast(),pollLast(): 删除尾节点,区别在于在空链表时前者抛出异常,后者只返回null;
  • remove(Object o): 删除指定元素,且一次只会删除一个匹配的对象,如果删除了匹配对象,返回true,否则false。
  • remove(int index):删除指定位置的元素

ArrayList和LinkedList的线性安全

ArrayList和LinkedList都不是线性安全,如果想要使用线性安全的集合,可以调用静态类Collections类中的synchronizedList方法,在里面传入相应的集合。例子如下:

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

最后是广告时间~
我们阿里云最核心部门弹性计算21年暑假实习招聘还在火热进行中。我可继续帮助大家内推简历。收到同学们的简历后,会进行评估,合适的话后续会由面试官进行进一步的交流!所以欢迎大家踊跃投递,越早投递越先人一步抓住机会!

部门简介:
这里是 阿里云最核心的ECS团队,这里能接触到众多大佬,与王坚院士做同事!在这里你将体验极致性能与架构的复杂挑战;在这里你将体验超大规模集群调度及管理。暑期实习可能是你最容易进入阿里的渠道! 同学们也可提前了解下 阿里云 ,阿里云 官方网站: https://www.aliyun.com/ ,看看弹性计算的位置就知道我们的部门有多么的核心!而且在实习期间每位实习生都会分配师兄一对一负责,帮助实习生更快更好的成长,能够参与甚至负责具有挑战性的实习项目!

招聘对象及要求:
毕业于985/211或者海外知名高校,计算机或相关专业,2021年11月-2022年10月毕业,具备良好的计算机专业基础,包括操作系统与计算机网络,熟悉常见的数据结构和算法;熟悉至少一种编程语言,包括但不限于Java、C++、JavaScript、Golang、Python、Node.js、Php、C#、Rust、Typescript或者操作系统开发。

招聘岗位:
研发工程师java
研发工程师C++
Web前端开发工程师
算法工程师-机器学习
算法工程师
基础平台研发工程师
数据挖掘分析
实习地点:北京/杭州 任选

投递邮箱:kate_yan4@163.com (我是2020年的转正的实习生,因为离职后公司邮箱冻结了,只能先用个人邮箱啦😂)

注:邮件名称或备注请按“职位+学校+姓名+手机号+实习地点”格式,因工作时间,每天在饭后及早晚集中处理大家邮件, 有任何问题可以直接留言或私聊我。投递成功后也在请帖子下面回复“已投递”

#内推##实习##阿里巴巴##春招##字节跳动##腾讯##面经#
全部评论

相关推荐

11-15 17:19
湖南大学 Java
成果成果成果果:这是哪个公司的hr,这么离谱吗,我没见过用性别卡技术岗的,身边女性同学拿大厂offer的比比皆是
点赞 评论 收藏
分享
10-18 13:01
已编辑
西安理工大学 C++
小米内推大使:建议技能还是放上面吧,hr和技术面试官第一眼想看的应该是技能点和他们岗位是否匹配
点赞 评论 收藏
分享
1 13 评论
分享
牛客网
牛客企业服务