java基础之集合篇

集合

1、List

  • ArrayListArrayList源码解读

    ArrayList是基于数组来实现动态扩容。
    size()为实际元素个数,elementData.length为容量。

    // 先看几个重要的成员变量和构造函数
    // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
    // 空数组,有参构造参数为0时,即创建一个容量为0的list时使用
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 空数组,无参构造时使用
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 无参构造函数
    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);
        }
    }
    

    重点来了

    // add方法
    public boolean add(E e) {
    	// 判断list容量是否够,不够则扩容。
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    // 获取所需要的最小容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	// 如果是无参构造创建的list,则直接用默认初始容量和最小所需容量比较获取较大者。
    	// 即如果创建list的时候没有指定大小,添加元素时list会默认扩容至10。
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
        	// 如果所需最小容量大于list的容量,则需要扩容。
            grow(minCapacity);
    }
    
    // 开始扩容
    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);
       // 将数据拷贝到新数组中,这里会损失性能。
       // 所以如果能确定一个list的大小,最好使用在创建时指定大小。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

    这里提供一个通过反射获取list容量的方法

        public static int getArrayListLength(ArrayList list) throws Exception{
        //获取Class对象
        Class c = Class.forName("java.util.ArrayList");
        //映射Class对象c所表示类(即Arraylist)的属性
        Field field = c.getDeclaredField("elementData");
        //设置访问状态表示为true
        field.setAccessible(true);
        //返回指定对象上此 Field 表示的字段的值
        Object[] object = (Object[])field.get(list);
        return object.length;
    }
    
  • LinkedList

    LinkedList比较简单,可以尝试自己写一个简易版本,然后慢慢补充细节。

    public class MyLinkedList<T> {
      // 定义列表的头结点和尾结点
      private Node<T> last;
      private Node<T> first;
      private int size;
    
      private static class Node<T> {
      	// 定义结点类所需要的属性:值、上一个结点和下一个结点。
          T value;
          Node<T> pre;
          Node<T> next;
    
          Node(T value, Node<T> pre, Node<T> next) {
              this.value = value;
              this.pre = pre;
              this.next = next;
          }
      }
    
      public void add(T value) {
          addLast(value);
      }
    
      public T get(int index) {
          return getNode(index).value;
      }
    
      private Node<T> getNode(int index) {
          Node<T> f;
          if (index < size / 2) {
              f = first;
              for (int i = 0; i < index; i++) {
                  f = f.next;
              }
          } else {
              f = last;
              for (int i = size - 1; i > index; i--) {
                  f = f.pre;
              }
          }
          return f;
      }
    
      private void addLast(T value) {
          // 定义一个新的指针指向尾结点,因为后续需要将尾结点指针指向新插入的结点。
          Node<T> l = last;
          // 创建一个新结点,即我们新插入的结点。
          Node<T> node = new Node<>(value, l, null);
          // 每次将新结点插到最后
          last = node;
          if (l == null) {
              // 首次添加,first和last都指向同一结点
              first = node;
          } else {
              // 之前的last结点的next需指向新结点
              l.next = node;
          }
          size++;
      }
    
      public int size() {
          return size;
      }
    }
    

2、Map

hashmap数据结构为数组+链表,链表结点存储的是Entry对象,其有四个属性:hash、key、value、next;

数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;
如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;
对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

在这里插入图片描述

hashmap的默认容量为16,只有进行“put”操作时才会进行判断是否需要扩容,当发生哈希冲突并且size大于阈值的时候,需要进行数组扩容;扩容需要新建一个长度为之前数组2倍的新的数组,然后将当前的Entry数组中的元素全部传输过去,扩容后的新数组长度为之前的2倍,所以扩容相对来说是个耗资源的操作。

当链表长度的超过8时,链表会转成红黑树(此时也需要满足table>=64,否则会进行扩容而不是树化),利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 如果此映射中包含的键值映射的数量小于64,则进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

在这里插入图片描述

concurrentHashmap:线程安全且高效
实现原理:JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。
将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

如图

Map<Integer, Integer> chm = new ConcurrentHashMap<Integer, Integer>(32); //实际容量为64
// 在JDK1.8之后,创建ConcurrentHashMap对象时如果设置了初始容量32,
// 底层会计算真实容量为比((32+16)+1)大的最小2的次幂(即64)。
// tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));

扩容

常见问题:
1、数据结构
2、常用参数
3、源码
4、扩容流程
5、CAS+Synchronized
6、volatile

问:ConcurrentHashmap支持key、value为空吗?为什么?
答:不支持,会有二义性(不知道本来就是null,还是被其他线程改为null值)
问:ConcurrentHashmap的并发度是多少?
答:1.7为Segment[]的数组长度,默认是16;1.8后锁的细度为数组元素,所以并发度为数组长度。
问:ConcurrentHashmap的迭代器是强一致性还是弱一致性??
答:与 HashMap 迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。

什么是弱一致性-->数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到。
而弱一致性中最常用到的是最终一致性:
最终一致性-->不保证在任意时刻任意节点上的同一份数据都是相同的,
但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化(最终会达到一致状态)。

ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素。
但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,
而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。
这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变。
更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。

  • LinkedhashmapLinkedhashmap—JDK1.8 || 彻头彻尾理解LinkedHashMap—JDK1.6
    LinkedHashMap通过维护一个双向链表实现有序,accessOrder属性为true时按访问顺序迭代,accessOrder属性为false时按插入顺序迭代。
    Linkedhashmap中的Entry继承了HashMap.node,并且添加了两个属性before、after用于维护整个双向链表。
    须注意得是:next指针指向单向链表中的下一节点;before和after指针指向双向链表中的节点(由此来实现有序)。
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

在这里插入图片描述

  • LRU算法(进阶)
    1、通过LinkedHashMap实现LRU算法。
    2、自行实现(HashMap --> LinedHashMap --> LRU)。

3、Set

Set集合使用最多的就是HashSet,其余set有兴趣可自行了解。

  • HashSet
    底层就是个hashmap,下面简单分析源码。
private transient HashMap<E,Object> map;
// 一个空的object(),用于map的value。
private static final Object PRESENT = new Object();
// 构造方法,创建一个HashMap
public HashSet() {
        map = new HashMap<>();
    }

public boolean add(E e) {
		// 这里把添加的值存入map的key,因为key天然具有唯一性,value放一个静态变量。
        return map.put(e, PRESENT)==null;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值