Java基础之集合

复习和准备一些集合常见的问题,也加强了一下原理的知识
在这里插入图片描述

ArrayList

有以下需要注意的:

  1. 底层采用的是动态数组实现的,这意味着有很多数组的特性
  2. ArrayList内部维护了一个数组,最开始的时候数组是空的,初始容量为10,等待有元素插入时才会进行扩容,扩容的大小是原来的1.5倍
  3. 在指定索引的增删是,会把后面位置的元素进行复制移动,这很消耗性能,也说明为什么增删慢了,查询快了
  4. 由于底层是数组,而数组是保存在堆中的,所以数组扩容是有极限的,超过极限就会发生OOM异常
  5. 数组与arraylist的转换,数组转list,指向的是同一片内存地址,所以数组修改了,list也会修改。而list转数组是拷贝的,所以不会影响。

LinkedList

有以下需要注意的:

  1. 底层采用双向链表实现,一个节点里包括三部分:数据+前驱节点+后继节点
  2. 它内存是不连续的,并且由于是链表,所有不需要做动态扩容
  3. 不支持随机访问,查询慢,增删快

ArrayList和LinkedList的都是线程不安全的,如何使用线程安全呢?

  • 使用工具类Collections.synchronizedList()
  • ArrayList还可以使用CopyOnWriteArrayList(),性能会更好,而LinkedList没有这个实现类

ArrayList参考博客

LinkedList参考博客

散列表(哈希表)

定义:

散列表(Hash Table)又名哈希表/Hash表,是根据**键(Key)**直接访问在内存存储位置值(Value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性。(key经过哈希函数的计算生成一个值,该值映射到数组的索引下标,即能通过key去访问存放在数组中的value了。其中关键就是哈希函数)

哈希冲突:

不同的key经过哈希函数计算后的值相同,这就发生了哈希冲突,数组的一个位置称为一个桶或一个槽

拉链法:

当发生了哈希冲突后,即在该桶中存放一个链表,依次存放。当链表的长度大于8时,就会转换为时间复杂度更低的红黑树,避免了查询链表的线性退化。链表里存储的数据是包含key的,就是说,通过key去遍历查询链表中的数据。

HashMap

参考博客

  1. 底层采用数组+链表实现,即散列表

  2. key可以为null

  3. 寻值算法

    • key.hashCode(),第一次获取哈希值

    • 使用扰动函数,二次计算哈希值,使哈希分布的更加均匀

      (h = key.hashCode()) ^ (h >>> 16)

      /**
       * 1、h>>>6,将哈希值向右移动了16位
       * 2、^ (异或运算),二进制时,同一位上相同,则该位为0,否则为1
       * 3、该方法能有效的扰动哈希值,减少了哈希冲突和使哈希的分布更加均匀
       */
      static final int hash(Object key) {
          int h;
          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }
      
    • 获取索引位置

      (n - 1) & hash]

      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                         boolean evict) {
              Node<K,V>[] tab; 
          	Node<K,V> p; 
          	int n, i;
          	// 这里虽然是判断,但是同时也做了两个操作,如果都未初始化,则调用resize()方法进行扩容
              if ((tab = table) == null || (n = tab.length) == 0)
                  n = (tab = resize()).length;
          	// 获取索引还需要进行 i = (n - 1) & hash],n为数组的容量
          	// 当索引位置为null时,直接插入
              if ((p = tab[i = (n - 1) & hash]) == null)
                  tab[i] = newNode(hash, key, value, null);
              else {
      			// 此处省略
                  // 插入到链表,或者添加到红黑树
              }
              ++modCount;
              if (++size > threshold)
                  resize();
              afterNodeInsertion(evict);
              return null;
          }
      
  4. put扩容的过程

    • HashMap的懒惰加载,即虽然创建了HashMap,但是没有初始化table数组,构造方法中只是将默认的加载因子赋值

      public HashMap() {
      	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
      }
      
    • 由于涉及到数组,就需要对进行动态扩容。当数据达到(加载因子*原数组容量)使即进行扩容。比如默认的加载因子是0.75,而数组的容量是16,所有当数据达到12时就会调用resize()方法进行扩容了。resize()方法扩容时,移动数据到新的数组,计算新的位置的方法:

      if(hash & oldCap = 0){
      	// 留在原位置
      }else{
      	// 原位置 + oldcap	
      }
      

      使用位运算扩大了两倍

      final Node<K,V>[] resize() {
          Node<K,V>[] oldTab = table;
          int oldCap = (oldTab == null) ? 0 : oldTab.length;
          int oldThr = threshold;
          int newCap, newThr = 0;
          if (oldCap > 0) {
              if (oldCap >= MAXIMUM_CAPACITY) {
                  threshold = Integer.MAX_VALUE;
                  return oldTab;
              }
              // 使用了位运算,往左移动了1位,即 2的1次幂,扩大了两倍
              else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                       oldCap >= DEFAULT_INITIAL_CAPACITY)
                  newThr = oldThr << 1; // double threshold
          }
      	....
          return newTab;
      }
      
  5. 为什么扩容是2的幂次方?

    使用&取代模运算,效率高,在两处用到了:

    • 计算索引位置
    • 扩容时,重新计算索引位置
  6. 线程安全的类ConcurrentHashMap()

    底层使用了synchronized将哈希桶锁住了,就是说同时间只会有一个线程操作桶里的链表或红黑树

<<、>>、>>>的理解

  • <<表示将数的二进制向左移动,右侧低位补0,这个时候不论有没有符号都不影响,往左移动一位就是等于乘以2

    8<<2 // 表示8 * 2次幂,结果为32
    
  • 有符号数:位运算>>表示将数的二进制向右移动,左侧补0,左侧最高位为符号位,补完0后,将保存原来的符号。往右移动一位等于除以2

    8>>2 // 结果为2
    -8>>2 // 结果为-2
    
  • 无符号数:位运算>>>表示将数的二进制向右移动,左侧补0,这时候不会恢复原来的符号。即说明这个数为无符号的二进制数。往右移动一位等于除以2

    8>>>2 // 结果为2
    -8>>> // 结果为1073741822
    

LinkedHashMap

基于HashMap实现,在其外还维护了一个链表,保证了其顺序

HashSet

Set具有特点:

  • 无序性:不能通过索引查找,数据没有顺序
  • 唯一性:后续重复的数据不能添加
  • 快速去重:快速查找(只遍历的速度快)
  • 可以简单理解为HashMap没有实现后续的链表,存储的值即是key也是value
  • 可以为null
  • 使用哈希表实现
Set<String> set = new HashSet<>();
set.add("hello");
set.add("ad");
// 可以添加null
set.add(null);
for (String s : set) {
System.out.println(s);
}

TreeSet

特点:

  • 底层使用平衡树实现

  • 同样具有无序性、唯一性等特点

  • 不可以添加null

    // 是平衡树实现
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("hello");
    treeSet.add("ad");
    //set2.add(null);
    
    for (String s : treeSet) {
    System.out.println(s);
    }
    

LinkedHashSet

特点:

  • 有序:HashSet外维护了一个双向循环链表,这也提供存储顺序和插入顺序

  • 可以添加null

    LinkedHashSet<String> h = new LinkedHashSet<>();
    h.add("hello");
    h.add("ad");
    
    h.add(null);
    
    for (String s : h) {
    System.out.println(s);
    }
    

ArrayBlockingQueue

ArrayBlockingQueue是一个基于数组实现的有界阻塞队列,队列就是FIFO,它是线程安全的。队列可以用于多线程的通信。它可以设置公平策略

常见的方法:

  1. 添加方法

    方法名称描述
    add()在队列尾部插入数据,队列已满时,抛出异常 IllegalStateException
    offer()在队列尾部插入数据,队列已满时,返回false
    offer(E e, long timeout, TimeUnit unit)可以在队列满的时候指定等待时间,超出等待时间,而队列还是满的,则返回false
    put()在队列尾部插入数据,队列已满时,阻塞等待
  2. 获取方法

    方法名称描述
    peek()获取队列头部的数据,但是不会移除头部数据,队列为空时返回null
    poll()获取队列头部的数据,会移除头部数据,队列为空时返回null
    take()获取队列头部的数据,会移除头部数据,队列为空时一直阻塞
    poll(long timeout, TimeUnit unit)获取队列头部的数据,会移除头部数据,队列为空时阻塞指定时间
  3. 其他方法

    方法名称描述
    remainingCapacity()获取剩余可用容量
    size()获取队列中元素数量
    remove()移除指定元素
  4. 设置公平策略,设置fair后,就会按照创建线程去take的顺序获取,否则各线程随机竞争

    public class Test03 {
        public static void main(String[] args) throws InterruptedException {
            // 设置fair后,就会按照创建线程去take的顺序获取,否则各线程随机竞争
            ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(2,true);
    
            for (int i = 0; i < 10; i++) {
                new Thread(() -> {
                    try {
                        // 创建线程获取
                        System.out.println(Thread.currentThread().getName() + "获取" + queue.take());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
    
            Thread.sleep(1000);
    
            queue.offer("a");
            queue.offer("b");
        }
    }
    

LinkedBlockingQueue

基于链表实现的阻塞队列,FIFO,常用的方法和ArrayBlockingQueue的一致,不同点:

ArrayBlockingQueueLinkedBlockingQueue
数据结构数组链表
是否有界有界阻塞,需要指定固定数组大小默认无界,可以有界
线程安全内部一把锁,线程安全内部有头尾各一把锁,即两把锁。读写互不干扰,性能更好
是否支持公平

内部有两把锁,读写互不影响:

private final ReentrantLock putLock = new ReentrantLock();
private final ReentrantLock takeLock = new ReentrantLock();

示例:

public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        // 底层使用链表实现,其中默认的队列大小很大,也可以自行指定
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(2);
        // add:队列满时,抛出异常
        queue.add("a");
        queue.add("b");
        iteratorQueue(queue);
        // 队列满时,线程阻塞
//        queue.put("c");
        // 队列满时,返回false
        queue.offer("c");
        iteratorQueue(queue);
        // 获取头部元素,不移除,队列为空时,返回null
        queue.peek();
        iteratorQueue(queue);
        // 获取头部元素,移除,队列为空时,返回null
        queue.poll();
        iteratorQueue(queue);
        queue.poll();
        iteratorQueue(queue);
        // 获取头部元素,移除,队列为空时,阻塞
//        queue.take();
        queue.offer("a");
        queue.offer("b");
    }

    /**
     * 打印队列
     *
     * @param t
     * @param <T>
     */
    public static <T extends Queue<String>> void iteratorQueue(T t) {
        System.out.println("+++++++打印队列元素++++++++");
        for (String s : t) {
            System.out.println(s);
        }
        System.out.println("+++++++打印结束++++++++");
    }
}

SynchronousQueue

内部没有容量,写线程写入元素都需要等待读线程读取元素,反之也是如此

public class Test04 {
    public static void main(String[] args) throws InterruptedException {
        SynchronousQueue<String> queue = new SynchronousQueue<>();
        Thread threadIn = new Thread(() -> {
            try {
                queue.put("a");
                System.out.println(Thread.currentThread().getName() + "成功放入元素a");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread threadOut = new Thread(() -> {
            try {
                System.out.println(queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        threadOut.start();
        Thread.sleep(2000);
        threadIn.start();
    }
}

ps:方法peek()无效,因为它内部没有存储元素,而peek是获取元素,却不删除,所以没有意义。

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值