Java基础知识之什么是集合框架

x33g5p2x  于2021-08-22 转载在 Java  
字(8.9k)|赞(0)|评价(0)|浏览(337)

Java基础知识之什么是集合框架,前面的文章,我们已经学习了Java的一些基础知识,比如泛型、注解等等内容,接着本博客继续学习Java中一个很常见的内容,集合。

1、什么是Java中的集合框架?

Java Collections 框架由接口和类组成,集合框架是用于存储数据和操作一组对象的统一架构。

集合框架提供了很多接口,比如Set、List、Queue、Deque、… , 实现类的有ArrayList、Vector、LinkedList、PriorityQueue、HashSet、LinkedHashSet、TreeSet、… ,这些实现类都是数据架构和算法的应用,比如链表、红黑树、二叉树等等,集合框架的类可以在java.util这里package里找到

2、Java 集合层次结构

Java Collection提供了很多类,功能比较强大,这里画图简单描述一下Java中的主要类图,先画一个简版,只画一些最重要的接口,如图,Java中的集合类比如HashSet、ArrayList、LinkedList等等都是继承自Set、List、Queue、Deque这些接口,HashMap就是继承自Map接口
在这里插入图片描述

用idea软件画出uml图:

在这里插入图片描述

继承自Map接口的uml类图:

在这里插入图片描述

对集合框架有一个大概了解之后,下面挑几个比较常用的进行描述

3、ArrayList

3.1、uml类图结构

ArrayList是用的比较多的列表List,看一下ArrayList的类图结构:

在这里插入图片描述

3.2、声明的属性

简单列表ArrayList源码里声明的几个比较重要的属性
属性作用DEFAULT_CAPACITY默认的数组容量EMPTY_ELEMENTDATA用于共享的Empty实例数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA也是一个空的实例数组elementDataArrayList中真实存储数据的容器size集合中的元素个数

下面简单跟一下ArrayList源码

3.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) {
         // c.toArray might (incorrectly) not return Object[] (see 6260652)
         if (elementData.getClass() != Object[].class)
         	 // 赋值给elementData
             elementData = Arrays.copyOf(elementData, size, Object[].class);
     } else {
         // replace with empty array.
         this.elementData = EMPTY_ELEMENTDATA;
     }
 }
3.4、add方法

方法名作用public boolean add(E e)将指定的元素添加到此列表的末尾public void add(int index, E element)在指定的index位置加上元素public boolean addAll(Collection<?> c)将指定集合的所有元素添加到list末尾public boolean addAll(int index,Collection<? extends E> c)在指定位置加上集合的所有元素

ArrayList add方法

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

校验内部容量

private void ensureCapacityInternal(int minCapacity) {
    // calculateCapacity计算内部容量
    // ensureExplicitCapacity继续校验
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

calculateCapacity计算内容容量

private static int calculateCapacity(Object[] elementData, int minCapacity) {
     // 判断集合是否微empty数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 比较最小容量和默认容量,得出最大值,做第一次扩容
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

ensureExplicitCapacity方法

private void ensureExplicitCapacity(int minCapacity) {
    // 实际修改集合的次数
    modCount++;

    // overflow-conscious code
    // 判断最小容量是否大于数组长度
    if (minCapacity - elementData.length > 0)
        // 将计算出来的最小容器传值给grow核心扩容方法
        grow(minCapacity);
}

核心的扩容方法:

private void grow(int minCapacity) {
    // overflow-conscious code
    // 记录数组的实际长度
    int oldCapacity = elementData.length;
    // 扩容方法,原来容量的1.5倍,oldCapacity >> 1右移运算, 也即oldCapacity * (1/2)^1 
    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方法
public void add(int index, E element)

public void add(int index, E element) {
   // 校验index合法性
     rangeCheckForAdd(index);
	// 校验内部容量
     ensureCapacityInternal(size + 1);  // Increments modCount!!
     // index+1的目的是在指定index,后面再空出一个index+1的位置
     System.arraycopy(elementData, index, elementData, index + 1,
                      size - index);
     // 数组赋值
     elementData[index] = element;
	 // 计数器加1
     size++;
 }

public boolean addAll(Collection<? extends E> c)添加集合方法:

public boolean addAll(Collection<? extends E> c) {
    // 将集合转换为Object类型的数组
    Object[] a = c.toArray();
    // 计算要新增集合的数组长度
    int numNew = a.length;
    // 加上新集合之后,校验是否需要扩容
    ensureCapacityInternal(size + numNew);  // Increments modCount
    // 将数组a的元素复制到elementData
    System.arraycopy(a, 0, elementData, size, numNew);
    // 计算器加上新增集合的元素数量
    size += numNew;
    // 新增数组的元素个数不为0的情况就是新增成功
    return numNew != 0;
}

public boolean addAll(int index, Collection<? extends E> c)在指定的位置新增集合

public boolean addAll(int index, Collection<? extends E> c) {
        // 校验index是否合法
        rangeCheckForAdd(index);
		// 将集合转换为object数组
        Object[] a = c.toArray();
        // 数组的长度
        int numNew = a.length;
        // 校验新增元素之后,是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
         // 计算数组要加到集合的index下标
        int numMoved = size - index;
        if (numMoved > 0)
            // 调整elementData中数据的位置,给新插入的数据腾出空间
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
         // 复制数组a的数据到elementData的指定位置
        System.arraycopy(a, 0, elementData, index, numNew);
        // 加上新增集合的元素数量
        size += numNew;
        // numNew不等于0说明新增成功
        return numNew != 0;
    }

4、Vector

4.1、UML类图

在这里插入图片描述

4.2、Vector和ArrayList的区别

这里可以尝试着翻下Vector的源码,发现其本质也是一个数组,这点是和ArrayList是一样的。不同点是Vector是线程安全的。翻了源码发现很多方法都是有都是有加上同步锁的synchronized

public synchronized void insertElementAt(E obj, int index) {
    modCount++;
    if (index > elementCount) {
        throw new ArrayIndexOutOfBoundsException(index
                                                 + " > " + elementCount);
    }
    ensureCapacityHelper(elementCount + 1);
    System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
    elementData[index] = obj;
    elementCount++;
}

Vector实现逻辑和ArrayList很像,所以就不过多描述

5、HashSet

5.1、Uml类图

在这里插入图片描述

5.2、HashSet本质?

这里还是要跟下源码,在idea里点到源码里去,可以看出其实HashSet是通过HashMap实现的,所以后面看下hashMap实现就行

public HashSet() {
       map = new HashMap<>();
   }

public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

6、TreeSet

6.1、uml类图

在这里插入图片描述

6.2、TreeSet本质

跟下源码,可以发现TreeSet实现还是通过TreeMap实现的,所以待会再看看TreeMap怎么实现的就可以

public TreeSet() {
    this(new TreeMap<E,Object>());
 }

7、TreeMap

7.1、uml类图

在这里插入图片描述

7.2、TreeMap实现

翻下源码,从源码看起来似乎很熟悉,没错,TreeMap就是通过红黑树实现的,对红黑树比较熟悉的,应该可以马上看出来,不熟悉的读者可以参考我之前的博客数据结构系列之Java手写实现红黑树

  • TreeMap成员变量
// 比较器
private final Comparator<? super K> comparator;
// 根节点 
  private transient Entry<K,V> root;
  /** * The number of entries in the tree 树节点数量 */
  private transient int size = 0;
  /** * The number of structural modifications to the tree. 记录修改次数 */
  private transient int modCount = 0;

定义一棵红黑树信息,一棵红黑树都会有左节点left、右节点right、父节点parent、红黑树节点的颜色color、同时会保存key和value

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK
        // ...
}
  • put方法,新增节点
public V put(K key, V value) {
    Entry<K,V> t = root;
    // 第一次put,将新节点直接设置为root节点
    if (t == null) {
        compare(key, key); // type (and possibly null) check
        // 构建root节点
        root = new Entry<>(key, value, null);
        size = 1;
        // 修改次数
        modCount++;
        return null;
    }
    // cmp比较的值
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    // 进行比较,找到哪个节点作为新节点的parent节点
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                 // 新节点值比当前节点值小,继续找左节点
                t = t.left;
            else if (cmp > 0)
                // 找右节点
                t = t.right;
            else
               //插入的值在容器中有相同的值 直接覆盖
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
            //比较器为空就创建一个比较器
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
            // 找到一个节点作为新节点的parent节点
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    // 新增节点的父节点
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        // 新节点作为左子节点
        parent.left = e;
    else
         // 作为右子节点
        parent.right = e;
      // 变色和旋转
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

前面的逻辑其实就是和二叉搜索树的逻辑是很相似的,只是红黑树新增节点之后要通过变色或者是左旋右旋来保持红黑树的平衡

具体逻辑看一下fixAfterInsertion(node);,这个逻辑比较关键,这里做一下比较详细的描述,这里分情况分析

场景1: 红黑树是Empty的,这是最简单的场景,直接将新节点作为根节点就行,不过红黑树有个特性,根节点都是黑色的,所以将新节点涂黑就行

在这里插入图片描述

场景2:新增节点的父节点是黑节点,由于新增的节点都是红色的节点,所以这种情况不会影响平衡,直接新增就行

在这里插入图片描述

场景3:新增结点的父结点为红结点且为祖父节点的左子节点

场景3.1:新节点的叔叔节点是红色的
这种情况,如图005是新增节点,其叔叔节点是009是红色的,这种情况,需要做下变色,祖父节点008变为红色,父亲节点006变为黑色,叔叔节点009也变为黑色,这样整棵红黑色就可以平衡

在这里插入图片描述

场景3.2:叔叔节点是黑色,且新增节点作为右子节点
这种情况要做的是将新节点008指向父节点003,同时做左旋

在这里插入图片描述

场景3.3:叔叔节点是黑色,且新增节点作为左子节点
这种情况,需要将父节点008变黑色,祖父节点011变为红色,同时围绕祖父节点011做右旋

在这里插入图片描述

场景4:新增结点的父结点为红结点,且为祖父节点的右子节点
这种场景也有3种情况,不过和场景3是相反的,这种不做详细描述

private void fixAfterInsertion(Entry<K,V> x) {
   // 新增的节点都是红色的
   x.color = RED;
    // 父节点是红色的情况才需要调整红黑树
   while (x != null && x != root && x.parent.color == RED) {
        // 父节点作为祖父节点的左子树
       if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 找到叔叔节点
           Entry<K,V> y = rightOf(parentOf(parentOf(x)));
           // case1 : 叔叔节点也是红色
           if (colorOf(y) == RED) {
               setColor(parentOf(x), BLACK);
               setColor(y, BLACK);
               setColor(parentOf(parentOf(x)), RED);
               x = parentOf(parentOf(x));
           } else {
                // case2 : 叔叔节点是黑色,且新增节点是右子节点
               if (x == rightOf(parentOf(x))) {
                  // 将父节点和新增节点调换
                   x = parentOf(x);
                   // 从父节点处做左旋
                   rotateLeft(x);
               }
               // case 3 : 叔叔节点是黑色,且新增节点是左子节点
               setColor(parentOf(x), BLACK);
               setColor(parentOf(parentOf(x)), RED);
               rotateRight(parentOf(parentOf(x)));
           }
       } else { //情况与上面逻辑对称
           Entry<K,V> y = leftOf(parentOf(parentOf(x)));
           if (colorOf(y) == RED) {
               setColor(parentOf(x), BLACK);
               setColor(y, BLACK);
               setColor(parentOf(parentOf(x)), RED);
               x = parentOf(parentOf(x));
           } else {
               if (x == leftOf(parentOf(x))) {
                   x = parentOf(x);
                   rotateRight(x);
               }
               setColor(parentOf(x), BLACK);
               setColor(parentOf(parentOf(x)), RED);
               rotateLeft(parentOf(parentOf(x)));
           }
       }
   }
    // root节点肯定是黑色
   root.color = BLACK;
}

红黑树的逻辑相对比较复杂,比较详细的参考我上篇博客

8、HashMap

HashMap的逻辑也是相对比较复杂的,所以后面有时间再写一篇比较详细的文章对hashMap源码进行描述,本博客只做概述,目的是简述Java中的集合框架,通过源码的简单分析,让读者有更深层次的理解

8.1、什么是HashMap?

HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在 ,HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。

8.2、HashMap的特性
  • Hash存储无序的
  • key和value都可以存储null值,但是key只能存唯一的一个null值
  • jdk8之前的数据结构是数组+链表,Jdk8之后变成数组+链表+红黑树
  • 阀值大于8并且数组长度大于64才会转为红黑树
8.3、JDK1.7

在这里插入图片描述

8.4、JDK1.8

在这里插入图片描述

相关文章