Java集合框架-Collection

集合框架的概述

  • 集合、数组都是对多个数据进行存储操作的结构,简称Java容器(此时的存储,主要指内存层面的存储,不涉及持久化存储)
数组存储的特点:
1.一旦初始化,长度就确定了,元素类型也就定义好了
2.操作非常有限,添加,删除,插入等操作效率不高
3.获取数组中实际元素个数没有现成的方法
4.数据存储的特点:有序可重复
集合框架

 ------Collection接口:单列集合,存储单个数据
        ------List接口:有序可重复 -->“动态数组”
        ------Set接口:无需不可重复 -->“集合”
 -------Map接口:双列集合,存储一对数据 -->“函数”

List接口

 ------List接口:有序可重复 -->“动态数组”
        ------ArrayList:List接口的主要实现类,线程不安全,效率高,底层使用Object[] 数组存储
        ------LinkedList:对于频繁插入,删除操作效率比ArrayList高,底层使用双向链表存储
        ------Vector:List接口的古老实现类,线程安全,效率低,底层使用Object[] 数组存储

ArrayList源码分析
  • jdk7情况下:
    ArrayList list = new ArrayList();//底层创建了长度为十的Object[]数组
    list.add(123);//elementData[0] = new Integer(123)
    
    (1)底层调用无参构造方法:
      public ArrayList() {
      	this(10);
      }
    
    (2)调用有参构造:
     public ArrayList(int initialCapacity) {
        super();//父类无参构造器
        if (initialCapacity < 0)//如果初始化长度小于零,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        this.elementData = new Object[initialCapacity];//创建一个长度为传入参数的数组并赋给底层数组
    }
    
    (3).添加数据
     public boolean add(E e) {
    	ensureCapacityInternal(size + 1);  // Increments modCount!!
    	elementData[size++] = e;
    	return true;
    }
    
    如果添加元素导致底层数组容量不够,则扩容,默认情况下,扩容为原来数组的1.5倍,同时需要将原有数组中的数据复制到新的数组中
    (4).扩容
      private void ensureCapacityInternal(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)//如果元素下标大于数组长度则扩容
            grow(minCapacity);
    }
    
     private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;//数组长度赋给旧容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);//新容量变为旧容量的1.5倍
        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);//将原来的数组元素拷贝到新的数组中
    }
    
    结论:开发中,较多的选择带参的构造器
  • jdk8情况下:
    (1)底层调用无参构造方法:
      public ArrayList() {
      	//将数组初始化为空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
      }
      //空数组对象,如果采用默认构造方式,初始化为空数组
      private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    (2)调用有参构造:
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {//创建一个大小为initialCapacity的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {//如果输入初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
        }
    }
    //空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    (3).添加数据
    public boolean add(E e) {
    	//调用ensureCapacityInternal()方法
    	ensureCapacityInternal(size + 1); 
    	//将元素e赋值给elementData[size] 然后size++
    	elementData[size++] = e;
    	return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//判断是否是第一次添加
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//将10与参数中较大的赋给容量
        }
    	ensureExplicitCapacity(minCapacity);
    }
    private static final int DEFAULT_CAPACITY = 10;
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    (4).扩容
    private void grow(int minCapacity) {
    	//保存老的数组容量
        int oldCapacity = elementData.length;
        //新的数组容量 1.5倍扩容
        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);
    }
    private static int hugeCapacity(int minCapacity) {
    	//异常处理
        if (minCapacity < 0)
            throw new OutOfMemoryError();
        //如果数组容量超过最大数组容量,返回整型的最大值,否则返回最大数组容量
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    第一次调add()才将数组初始化好,延迟了数组的创建,节省内存
    (5)删除
    public E remove(int index) {
    	//范围检查
        rangeCheck(index);
        modCount++;
        //保存删除的元素
        E oldValue = elementData(index);
    	//要移动的第一个元素的下标
        int numMoved = size - index - 1;
        //如果删除的不是最后一个元素,那么numMoved大于0,将index以后的元素全部向前移动一个位置
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //将最后一个位置置为null,GC自动回收
        elementData[--size] = null; // clear to let GC do its work
    	//返回删除的元素
        return oldValue;
    }
    /*
     *删除所有和o相等的第一个元素
     */
    public boolean remove(Object o) {
    	//空指针检验
    	if (o == null) {
    	     for (int index = 0; index < size; index++)
    	     //判断数组中有没有null
    	          if (elementData[index] == null) {
    	              fastRemove(index);
    	              return true;
    	          }
    	} else {
    	    for (int index = 0; index < size; index++)
    	    //判断是否相等
    	        if (o.equals(elementData[index])) {
    	        //调用fastRemove方法删除index位置元素
    	            fastRemove(index);
    	            return true;
    	        }
    	}
    	//没有这个元素,返回false
    	return false;
    }
    
    (6)修改
    public E set(int index, E element) {
    	//范围检查,这个方法上面看过了
        rangeCheck(index);
    	//oldValue保存原来的元素
        E oldValue = elementData(index);
        //将index下标处的值修改为element
        elementData[index] = element;
        //返回旧元素
        return oldValue;
    }
    
    (7)查找
    public E get(int index) {
        rangeCheck(index);
    	//返回index下标处的元素
        return elementData(index);
    }
    
LinkedList源码分析
  • jdk7与jdk8无太大差别
    (1)底层调用无参构造方法:

    transient int size = 0; // 当前列表的节点个数
    transient Node<E> first; // 第一个节点
    transient Node<E> last; // 最后一个节点
    public LinkedList() {
    }
    
    • transient 关键字:当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。
    • 此处是告诉虚拟机,这三个成员变量不是 LinkedList 的永久性变量。

    (2)有参构造器

    public LinkedList(Collection<? extends E> c) {
      this();
      addAll(c);
    }
    

    (3)添加元素

    • 头插法
    public void push(E e) {
        addFirst(e);
    }
    public void addFirst(E e) {
        linkFirst(e);
    }
    private void linkFirst(E e) {
      final Node<E> f = first; // 当前第一个节点
      // 创建了一个新节点,以 null 为前一个节点、e 为值、当前第一个节点为下一个节点
      final Node<E> newNode = new Node<>(null, e, f);
      first = newNode; // 设置新建的节点为第一个节点
      if (f == null) // 当前第一个节点为空,说明列表为空
        last = newNode; // 所以最后一个节点为当前插入的节点
      else // 当前第一个节点不为空,说明列表不为空
        f.prev = newNode; // 当前列表头部连接上插入的节点
      size++;
      modCount++;
    }
    
    • 尾插法
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    public void addLast(E e) {
        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++;
    }
    
    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;
        }
    }
    
    • 某个节点前面插入一个节点
    void linkBefore(E e, Node<E> succ) {
      // assert succ != null;
      final Node<E> pred = succ.prev; // 获取到 succ 的上一个节点
      // 创建一个新的节点,连接到 succ 上一个节点后面
      final Node<E> newNode = new Node<>(pred, e, succ);
      succ.prev = newNode; // 将 succ 连接到 newNode 后面
      if (pred == null) // 如果 succ 的上一个节点为空,说明 succ 为头部节点
        first = newNode; // 直接将 newNode 设为头部节点
      else // 如果 succ 的上一个节点不为空,说明 succ 为中间或者尾部节点
        pred.next = newNode; // 将 succ 的上一个节点关联到 newNode 上
      size++;
      modCount++;
    }
    

    (4)获取元素

    public E get(int index) {
        checkElementIndex(index);// 检查 index 是否越界
        return node(index).item;
    }
    Node<E> node(int index) {
        // 如果 index 小于 size 的一半,从开头开始查找 
    	if (index < (size >> 1)) {
    	  Node<E> x = first;
    	  // 从头开始查找,直到 i == index
    	  for (int i = 0; i < index; i++)
    	    x = x.next;
    	  return x;
        } else { // 如果 index 大于 size 的一半,从尾部开始查找
    	  Node<E> x = last;
    	  for (int i = size - 1; i > index; i--)
    	    x = x.prev;
    	  return x;
        }
    }
    

    (5)修改元素

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }
    

    (6)移除元素

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
    
    • 移除链表上的某个节点
    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; // 当前节点元素设置为空,方便 GC
    	size--;
    	modCount++;
    	return element;
    }
    
Vector源码分析
  • 通过构造器创建对象时,底层都创建了长度为十的数组,在扩容方面,默认扩容为原数组的2倍。
Set接口

 ------Set接口:无序不可重复 -->“集合”
        ------HashSet:Set的主要实现类,线程不安全,可以存储null值
                ------LinkedHashSet:遍历时可以按照添加顺序进行(但不代表有序)
        ------TreeSet:可以按照对象属性,进行排序,底层是红黑树

Set简述
  • Set不提供对插入元素的位置的控制。
  • 它不能通过索引访问元素,但是可以搜索列表中的元素。
  • Set允许最多仅添加一个null元素
  • 存储无序不可重复的数据
    无序性:不等于随机性,存储数据在底层中并非按照数组索引的顺序添加,而是根据数据的哈希值判断位置
    不可重复性:添加元素时进行equals()判断,相同的元素只能添加一个
  • 向Set中添加数据时,其所在的类一定要重写hashCode()和equals(),并且hashCode()和equals()尽可能保持一致性,相等的对象必须要有相等的散列码
以HashSet 为例(底层使用HashMap结构,详细见Map)
  • HashSet是Set的典型接口,按照Hash算法来存储集合中的元素,因此具有很好的存取,查找,删除性能
    (1)HashSet特点

    • 不能保证元素排列顺序
    • 线程不安全
    • 集合元素可以是null,但仅添加一个null元素
    • 底层是数组,初始容量为16,如果使用率超过0.75,扩容为原来的2倍

    (2)添加元素
           向HashSet中添加元素,首先要调用元素所在类的hashCode()方法,计算哈希值,通过哈希值计算出在数组中的存放位置,判断此位置是否有元素:
    |-------如果没有,则添加成功
    |-------如果有,则逐个比较哈希值
           |-------哈希值不同,添加成功
           |-------哈希值相同,调用元素所在类的equals()方法
                  |-------equals()返回true,添加失败
                  |-------equals()返回false,添加成功
    1.jdk7:添加元素放入数组,指向原元素
    2.jdk8:原元素放入数组,指向添加元素

hashCode()重写(31
public static int hashCode(int a[]) {
    if (a == null)
		return 0;
	int result = 1;
		for (int element : a)
			result = 31 * result + element;
	return result;
}
  • 选择系数要尽量选择大一点的系数,这样哈希冲突会越小,查找效率也会提高
  • 31只占用5bits,相乘造成数据溢出的概率较小
  • 31是个素数,用一个数字乘以素数,最终得出来的结果只能被素数,被乘数和1来整数,减少冲突。提高算法效率
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值