• java容器学习笔记


    容器

    容器的组成

    容器有两个接口Map和Collection。
    collection接口有List类和set类。

    List类可以分为:Vector、LinkedList、ArrayList、CopyOnWriteArrayList
    Set类可以分为:HashSet、LinkedHashSet、TreeSet

    Map接口拥有:HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap

    结论:

    1. 如果是集合类型,有List和Set供我们选择。List的特点是插入有序的,元素是可重复的。Set的特点是插入无序的,元素不可重复的。
    2. 如果是key-value型,就可以选择Map。如果要保持插入顺序,则可以选择LinkedHashMap,如果不需要则选择HashMap,如果要排序则选择TreeMap。

    选择什么样的容器来存储对象,关键在于了解每一个常用集合类的数据结构!

    容器的初步了解

    List

    List集合基础

    • 实现了Collection接口
    • 特性:有序的,元素可重复的
    • 允许元素为null

    List常用的子类

    • Vector

    底层结构是数组,初始容量是10,每次增长2倍。
    它是线程同步的,已被ArrayList代替。

    • LinkedList

    底层结构是双向链表
    实现了Deque接口,因此可以向操作栈和队列一样操作它。
    线程非同步。

    • ArrayList

    底层结构是数组,初始容量为10,每次增长1.5倍。
    在增删的时候,需要数组的拷贝复制。
    线程非同步。

    • CopyOnWriteArrayList

    原理:在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
    写加锁,读不加锁。
    缺点: CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
    适合在读多写少的场景中使用。

    Set

    Set集合基础

    • 实现了Collection接口
    • 特性:无序的,元素不可重复
    • 底层大多数是Map结构的实现
    • 常用的三个子类都是非同步的

    Set常用的子类

    • HashSet

    底层数据结构是哈希表(是一个元素为链表的数组) + 红黑树。
    实际上就是封装了HashMap。
    元素无序,可以为null。

    • LinkedHashSet

    底层数据结构由哈希表和双向链表组成。
    父类是HashSet。
    实际上就是LinkHashMap。
    元素可以为null。

    • TreeSet

    底层实际上是一个TreeMap实例(红黑树)。
    可以实现排序的功能。
    元素不能为null。

    Map

    Map基础知识

    • 存储的结构是key-value键值对,不像Collection是单列集合
    • 需要先了解一下散列表和红黑树

    Map常用的子类

    • HashMap

    底层是散列表 + 红黑树。
    初始容量为16, 装载因子为0.75,每次扩容2倍。
    允许为null,存储无序。
    非同步。
    散列表容量大于64且链表大于8时,转为红黑树。
    Key的哈希值会与该值的高16位做异或操作,进一步增加随机性。
    当散列表的元素大于容量 * 装载因子时,会再散列,每次扩容2倍。
    如果hashCode相同,key不同则替换元素,否则就是散列冲突。

    • LinkedHashMap

    底层是散列表 + 红黑树 + 双向链表,父类是HashMap。
    允许为null,插入有序。
    非同步。
    提供插入顺序和访问顺序两种,访问顺序是符合LRU算法,一般用于扩展(默认是插入排序)
    迭代与初始容量无关(迭代的是维护的双向链表)
    大多使用HashMap的API,只不过在内部重写了某些方法,维护了双向链表。

    • TreeMap

    底层是红黑树,保证了时间复杂度为log(n)。
    可以对其进行排序,使用Comparator或者Comparable。
    只要compare或者CompareTo认定该元素相等,那就相等。
    非同步。
    自然排序(手动排序),元素不能为null。

    • ConcurrentHashMap

    底层是散列表 + 红黑树,支持高并发操作。
    key和value都不能为空。
    线程是安全的,利用CAS算法和部分操作上锁实现。
    get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值。
    在高并发环境下,统计数据(如计算size等)其实是无意义的,因为在下一个时刻size值就变化了。

    Collection的功能

    1.添加功能

    boolean add(Object obj):添加一个元素
    boolean addAll(Collection c): 添加一个集合的元素
    

    2.删除功能:

    void clear(): 移除所有元素
    boolean remove(Object obj): 移除一个元素
    boolean removeAll(Collection c): 移除一个集合的元素,只要一个元素被移除了就返回true。
    

    3.判断功能

    boolean contain(Object obj): 判断集合是否包含该元素
    boolean containsAll(Collection c): 判断集合中是否包含指定的集合元素,只有包含所有元素才叫包含。
    boolean isEmpty(): 判断集合是否为空
    

    4.获取功能

    Iterator<E> iterator(): 迭代器
    

    5.长度功能

    int size(): 元素的个数
    

    6.交集功能

    boolean retainAll(Collection c): 移除次collection中未包含在指定collection中的所有元素。集合A和集合B做交集,最终的结果保存在集合A,返回值表示的是A是否发生过变化。
    

    迭代器Iterator

    Iterator实际上就是在遍历集合。

    遍历集合的步骤:

    1. 通过结合对象获取迭代器对象
    2. 通过迭代器对象的hasNext()方法判断是否有元素
    3. 通过迭代器对象的next()方法获取元素并移动到下一个位置

    我们有一个集合:Collection c = new ArrayList();
    给集合添加元素:c.add("hello"); c.add("world"); c.add("java");
    通过集合获取迭代器对象:Iterator it = c.iterator();

    while(it.hashNext()){
    	String s = (String)it.next();
    	System.out.println(s);
    }
    

    ArrayList解析

    add方法

    1.add(E e)

    首先去检查一下数组的容量是否足够:

    • 足够:直接添加
    • 不足够:扩容:
      • 扩容到原来的1.5倍
      • 第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为minCapacity。

    2.add(int index, E e)

    • 检查角标
    • 空间检查,如果有需要进行扩容
    • 插入元素

    get方法

    get(int index)

    • 检查角标
    • 返回元素

    set方法

    set(int index, E e)

    • 检查角标
    • 替代元素
    • 返回旧值

    remove方法

    remove(int index)

    • 检查角标
    • 删除元素
    • 计算出需要移动的个数,并移动
    • 设置为null,让Gc回收

    总结:

    • ArrayList是基于动态数组实现的,在增删时候,需要数组的拷贝复制。
    • ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍
    • 删除元素时不会减少容量,若希望减少容量则调用trimToSize()
    • 它不是线程安全的。它能存放null值。

    Vector与ArrayList

    • Vector底层也是数组,与ArrayList最大的区别就是:同步(线程安全)
      Vector。
    • 在要求非同步的情况下,我们一般都是使用ArrayList来替代Vector的了。
    • 如果想要ArrayList实现同步,可以使用Collections的方法: List list = Collections.synchronizedList(new ArrayList(...));
    • Vector扩展2倍

    LinkedList解析

    底层是双向链表

    方法的一些细节

    • add方法实际上就是往链表最后添加元素
    • remove方法实际上就是用equals看看这两个元素是否在里面

    • get方法查看下标,如果下标小于长度的一半就从头遍历,否则从尾遍历
    • set方法和get方法其实差不多,根据下标来判断是从头遍历还是从尾遍历

    List总结:

    • ArrayList:

    底层实现是数组
    ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍
    在增删时候,需要数组的拷贝复制(navite 方法由C/C++实现)

    • LinkedList:

    底层实现是双向链表[双向链表方便实现往前遍历]

    • Vector:

    底层是数组,现在已少用,被ArrayList替代,原因有两个:
    Vector所有方法都是同步,有性能损失。
    Vector初始length是10,超过length时,以100%比率增长,相比于ArrayList更多消耗内存。

    总的来说:查询多用ArrayList,增删多用LinkedList。

    Map的功能

    1.添加功能

    v put(K key, V value):添加元素
    
    • 如果键是第一次存储,就直接存储,返回null
    • 如果键不是第一次存储,就用值把它以前的值替换掉,返回以前的值

    2.删除功能

    void clear(): 移除所有的键值对元素
    v remove(Object key): 根据键删除值,并把值返回
    

    3.判断功能

    boolean containsKey(Object key):判断集合是否包含指定的键
    boolean containsValue(Object value):判断集合是否包含指定的值
    boolean isEmpty(): 判断集合是否为空
    

    4.获取功能

    Set<Map.Empty<K key, V value>> entrySet():返回的是键值对对象的集合
    v get(Object key): 根据键获取值
    Set<K> keySet():获取集合中所有的键的集合
    Collection<V> values(): 获取集合汇总所有的值的集合
    

    5.长度功能

    int size(): 返回集合汇总键值对的对数
    

    散列表

    首先我们回顾下数据和链表:
    链表和数组都可以按照人们的意愿来排列元素的次序,他们可以说是有序的(存储的顺序和取出的顺序是一致的)。但同时,这会带来缺点:想要获取某个元素,就要访问所有的元素,直到找到为止。这会让我们消耗很多的时间在里边,遍历访问元素。

    而还有另外的一些存储结构:不在意元素的顺序,能够快速的查找元素的数据
    其中就有一种非常常见的:散列表

    散列表为每个对象计算出一个整数,称为散列码。根据这些计算出来的整数(散列码)保存在对应的位置上!

    在Java中,散列表用的是链表数组实现的,每个列表称之为桶。一个桶上可能会遇到被占⽤的情况(hashCode散列码相同,就存储在同一个位置上),这种情况是无法避免的,这种现象称之为:散列冲突。

    • 此时需要用该对象与桶上的对象进行比较,看看该对象是否存在桶上了——如果存在,就不添加了,如果不存在则添加到桶上
    • 当然了,如果hashcode函数设计得足够好,桶的数目也足够,这种比较是很少的
    • 在JDK1.8中,桶满时会从链表变成平衡二叉树

    如果散列表太满,是需要对散列表再散列,创建一个桶数更多的散列表,并将原有的元素插入到新表中,丢弃原来的表:

    • 装填因子(load factor)决定了何时对散列表再散列
    • 装填因子默认为0.75,如果表中超过了75%的位置已经填入了元素,那么这个表就会用双倍的桶数自动进行再散列

    HashMap

    总结:

    • 在JDK8中HashMap的底层是:数组+链表(散列表)+红黑树
    • 在散列表中有装载因子这么一个属性,当装载因子*初始容量小于散列表元素时,该散列表会再散列,扩容2倍!
    • 装载因子的默认值是0.75,无论是初始大了还是初始小了对我们HashMap的性能都不好
      • 装载因子初始值大了,可以减少散列表再散列(扩容的次数),但同时会导致散列冲突的可能性变大(散列冲突也是耗性能的操作,得操作链表(红黑树)!
      • 装载因子初始值小了,可以减小散列冲突的可能性,但同时扩容的次数可能就会变多!
    • 初始容量的默认值是16,它也一样,无论初始大了还是小了,对我们的HashMap都是有影响的:
      • 初始容量过大,那么遍历时我们的速度就会受影响
      • 初始容量过小,散列表再散列(扩容的次数)可能就变得多,扩容也是一件非常耗费性能的事
    • 从源码上我们可以发现:HashMap并不是直接拿key的哈希值来用的,它会将key的哈希值的高16位进行异或操作,使得我们将元素放入哈希表的时候增加一定的随机性。
    • 还要值得注意的是:并不是桶上有8位元素的时候它就能变成红黑树,它得同时满足我们的散列表容量大于64才行。

    TreeMap

    • TreeMap实现了NavigableMap接口,而NavigableMap接口继承着SortedMap接口,致使我们的TreeMap是有序的!
    • TreeMap底层是红黑树,它方法的时间复杂度都不会太高:log(n)
    • 非同步
    • 使用Comparator或者Comparable来比较key是否相等与排序的问题

    注意:

    1. TreeMap有序是通过Comparator来进行比较的,如果comparator为null,那么就使用自然顺序。
    2. key值不能为null。

    总结:

    TreeMap底层是红黑树,能够实现该Map集合有序。如果在构造方法中传递了Comparator对象,那么就会以Comparator对象的方法进行比较。否则,则使
    用Comparable的compareTo(T o)方法来比较。值得说明的是:

    • 如果使用的是compareTo(T o)方法来比较,key一定是不能为null,并且得实现了Comparable接口。
    • 即使是传入了Comparator对象,不用compareTo(T o)方法来比较,key也是不能为null的。

    要点:

    1. 由于底层是红黑树,那么时间复杂度可以保证为log(n)
    2. key不能为null,为null为抛出NullPointException的
    3. 想要自定义比较,在构造方法中传入Comparator对象,否则使用key的自然排序来进行比较
    4. TreeMap非同步,想要同步可以使用Collections来封装

    Set集合总结

    • HashSet:
      无序,允许为null,底层是HashMap(散列表+红黑树),非线程同步
    • TreeSet:
      有序,不允许为null,底层是TreeMap(红黑树),非线程同步
    • LinkedHashSet:
      迭代有序,允许为null,底层是HashMap+双向链表,非线程同步
  • 相关阅读:
    centos7.6安装Oracle11g过程记录(下)
    centos7.6 安装解压缩软件
    centos7.6 安装及配置ftp服务
    MySQL8.0的主从配置过程记录
    解决 /dev/mapper/centos-root 空间不足的问题
    ASP判断当前页面上是否有参数ID传递过来
    通过ASP禁止指定IP和只允许指定IP访问网站的代码
    asp自动补全html标签自动闭合(正则表达式)
    asp中utf8不会出现乱码的写法
    通过安全字符串过滤非法字符
  • 原文地址:https://www.cnblogs.com/kylinxxx/p/13974850.html
Copyright © 2020-2023  润新知