java集合类
集合类存放在java.util包中。集合类存放的都是对象的引用,而非对象本身。集合类型主要有3种:set(集)、list(列表)和map(映射)
1. List
List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许加入重复元素,因为他可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引。
List继承自Collection,较之Collection,List还添加了以下操作方法
(1) 位置相关:List的元素是有序的,因此有get(index)、set(index, object)、add(index, object)、remove(index)方法。
(2) 搜索:indexOf()、lastIndexOf()
(3) 迭代:使用Iterator的功能板迭代器
(4) 范围性操作:使用subList方法对list进行任意范围操作
List的数据结构是有序可以重复的,他有三个实现类:ArrayList、LinkedList、Vector三个实现类
(1) ArrayList
a. 容量不固定,可以动态扩容
b. 有序(基于数组的实现,当然有序)
c. 元素可以为null
d. 效率高
查找操作的时间复杂度为O(1)
增删操作的时间复杂度是O(n)
其他操作基本也是O(n)
e. 占用空间少,相比LinkedList,不用占用额外空间维护表结构
ArrayList的数据结构
为什么先继承AbstractList,而让AbstractList先实现List<E>?而不是让ArrayList直接实现List<E>?
接口中全都是抽象的方法,而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList是实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己在实现一些自己特有的方法,这样一来,让代码更简洁,就继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码
ArrayList总结
a. arraylist可以存放null
b. arraylist区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法
c. arraylist中的removeAll(collection c)和clear()的区别就是removaAll可以删除批量指定的元素,而clear是全是删除集合中的元素
d. arraylist由于本质是数组,所以他在查询方面会很快,而在插入删除方面,性能下降很多,移动很多数据才会达到相应的效果
e. arraylist实现了RandomAccess,所以在遍历它的时候推荐使用for循环
(2) LinkedList
LinkedList是基于链表的顺序表,和ArrayList相比,它的特点是:其随机插入和删除的效率较高,因为其不需要扩容和移动元素操作,随机访问效率较低(随机访问需要从头节点遍历)
LinkedList数据结构
a. LinkedList是一个双向链表,并且实现了List和Deque接口中所有的列表操作,并且能存储任何元素,包括null,可以当队列使用
b. LinkedList在执行任何操作的时候,都必须先遍历此列表来靠近通过index查找我们所需要的值
c. LinkedList是一个非线程安全的(异步)
继承结构和层次关系
a. List接口:列表,add、set、等一些对列表进行操作的方法。
b. Deque接口:有队列的各种特性。
c. Cloneable接口:能够复制,使用copy方法。
d. Serializable接口:能够序列化。
e. 没有RandomAccess,那么就推荐使用iterator,在其中就有一个foreach,增强的for循环,其中原理也就是iterator,我们在使用的时候,使用foreach或者iterator都可以。
LinkedList总结
a. linkedList本质上是一个双向链表,通过一个Node内部类实现的这种链表结构。
b. 能存储null值。
c. 跟arrayList相比较,LinkedList在删除和增加等操作上性能好,而ArrayList在查询的性能上好。
d. 不存在容量不足的情况。
e. LinkedList不光能够向前迭代,还能像后迭代,并且在迭代的过程中,可以修改值、添加值、还能移除值。
f. LinkedList不光能当链表,还能当队列使用,这个就是因为实现了Deque接口。
(3) Vector
Vector也是基于数组实现的,是一个动态数组,其容量能自动增长,和ArrayList相比,Vector是线程安全的(它的很多实现方法都加入了同步语句),可以用于多线程环境
LinkedList数据结构
Vector总结
a. Vector有四个不同的构造方法。无参构造方法的容量为默认值10,仅包含容量的构造方法则将容量增长量(从源码中可以看出容量增长量的作用,第二点也会对容量增长量详细说)明置为0。
b. 能存储null值。
c. 注意扩充容量的方法ensureCapacityHelper。与ArrayList相同,Vector在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就先看构造方法中传入的容量增长量参数CapacityIncrement是否为0,如果不为0,就设置新的容量为就容量加上容量增长量,如果为0,就设置新的容量为旧的容量的2倍,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后同样用Arrays.copyof()方法将元素拷贝到新的数组。
d. 很多方法都加入了synchronized同步语句,来保证线程安全。
e. 其他很多地方都与ArrayList实现大同小异,Vector现在已经基本不再使用。
2. Map
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。
Map接口和Collection接口的不同
(1) Map是双列的,Collection是单列的
(2) Map的键唯一,Collection的子体系Set是唯一的
(3) Map集合的数据结构值针对键有效,跟值无关;Collection集合的数据结构是针对元素有效。
(4) Map集合没有直接取出元素的方法,而是先转成Set集合,在通过迭代获取元素
Map主要提供了操作方法
(1) 添加功能:
a. put(Object obj, Object obj1)
b. putAll(Map map);
(2) 删除功能:
a. remove(Object obj);
b. clear();
(3) 判断功能:
a. containsKey(Object obj);
b. containsValue(Object obj);
c. isEmpty();
(4) 获取功能:get(),set()
(5) 长度功能:size()
Map不能包含重复键值,它主要有四个实现类:HashMap, TreeMap, HashTable 和 LinkedHashMap
(1) HashMap
a. 底层实现是 链表数组,JDK 8 后又加了 红黑树;
b. 允许空键和空值(但空键只有一个,且放在第一位);
c. 插入、获取的时间复杂度基本是 O(1);
d. 遍历整个 Map 需要的时间与 桶(数组) 的长度成正比(因此初始化时 HashMap 的容量不宜太大);
e. HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。它存储的时候是无序的;
f. HashMap 的实现不是同步的,这意味着它不是线程安全的。
HashMap的数据结构:链表式数组
HashMap 初始容量:16
加载因子:0.75f
最大容量:MAXIMUM_CAPACITY = 1 << 30(2^30)
当前 HashMap 修改的次数:transient int modCount;
阈值,下次需要扩容时的值,等于 容量*加载因子:int threshold
树形阈值:JDK 1.8 新增的,当使用 树 而不是列表来作为桶时使用。必须必 2 大:static final int TREEIFY_THRESHOLD = 8;
非树形阈值:也是 1.8 新增的:UNTREEIFY_THRESHOLD = 6;
树形最小容量:桶可能是树的哈希表的最小容量。至少是 TREEIFY_THRESHOLD 的 4 倍,这样能避免扩容时的冲突:static final int MIN_TREEIFY_CAPACITY = 64;
哈希表中的链表数组:transient Node<K,V>[] table;
键值对的数量:transient int size;
哈希表的加载因子:final float loadFactor;
Hash冲突
a. HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
b. jdk1.8之前的hashmap都采用单链表的结构,都是基于一个数组和多个单链表,hash值冲突的时候,就将对应节点以链表的形式存储。如果在一个链表中查找其中一个节点时,将会花费O(n)的查找时间,会有很大的性能损失。到了jdk1.8,当同一个hash值的节点数不小于8时,不再采用单链表形式存储,而是采用红黑树
关于初始容量和加载因子
a. 由于 HashMap 扩容开销很大(需要创建新数组、重新哈希、分配等等),因此与扩容相关的两个因素:
容量:数组的数量
加载因子:决定了 HashMap 中的元素占有多少比例时扩容
b. HashMap 的默认加载因子为 0.75,这是在时间、空间两方面均衡考虑下的结果:
加载因子太大的话发生冲突的可能就会大,查找的效率反而变低
太小的话频繁 rehash,导致性能降低
使用技巧:
当设置初始容量时,需要提前考虑 Map 中可能有多少对键值对,设计合理的加载因子,尽可能避免进行扩容;如果存储的键值对很多,干脆设置个大点的容量,这样可以少扩容几次。
HashMap常用方法:
a. V put(K key, V value) //添加指定的键值对到 Map 中,如果已经存在,就替换
b. putAll(Map t) //将指定 Map 中的所有映射复制到此 map
c. containsKey(Object key)//如果 Map 包含指定键的映射,则返回 true
d. containsValue(Object value)//如果此 Map 将一个或多个键映射到指定值,则返回 true
e. remove(Object key)//从 Map 中删除键和关联的值
f. entrySet()//返回 Map 中所包含映射的 Set 视图。Set 中的每个元素都是一个 Map.Entry 对象,可以使用 getKey() 和 getValue() 方法(还有一个 setValue() 方法)访问后者的键元素和值元素
g. keySet()//返回 Map 中所包含键的 Set 视图。删除 Set 中的元素还将删除 Map 中相应的映射(键和值)
put的主要过程:
HashMap总结:
a. HashMap是由数组和链表组成的,数组是HashMap的主体,链表是为了解决哈希冲突的问题,对于HashMap的插入问题,如果插入位置不含有链表,那么直接插入到链表的表头即可,如果包含链表,需要先遍历链表,判断key是否已经在链表中存在,存在则替换value值,不存在则插入到链表的表头。
b. HashMap中是根据hashCode和equals方法来决定存放的位置,因此不允许一个map将自己作为key值,但是可以将自己作为value值
c. HashMap是线程不安全的,对于多线程环境下,无法保证数据的正确性,使用的时候需要注意。
d. 如果在使用时能评估到HashMap中元素的个数,建议指定初始化容量的大小,在HashMap中默认容量时16,在插入元素的时候,会对元素进行检查是否需要扩容,每次扩容就是新建一个原来2倍容量的数组,对原有的元素全部重新哈希,因此如果不断插入元素,那么会需要不断的扩容,不断的哈希,这时候产生的内存开销和cpu开销在某些情况下可能是致命的。
(2) TreeMap:基于红黑树实现,具有如下特点
a. TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
b. TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
c. TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
d. TreeMap 实现了Cloneable接口,意味着它能被克隆。
e. TreeMap 实现了java.io.Serializable接口,意味着它支持序列化
二叉树搜索:
a. 左子树的值小于根节点,右子树的值大于根节点。
b. 二叉搜索树的优势在于每进行一次判断就是能将问题的规模减少一半
c. 平衡二叉搜索树查询元素的时间复杂度为log(n)
红黑树:红黑树是一种二叉搜索树,红黑树有如下5个规则保证了树是平衡的:
a. 树的节点只有红与黑两种颜色
b. 根节点为黑色的
c. 叶子节点为黑色的
d. 红色节点的字节点必定是黑色的
e. 从任意一节点出发,到其后继的叶子节点的路径中,黑色节点的数目相同
常用方法:
(1) put(k, v):插入一个元素,实际上是红黑树的插入过程,过程如下:
a. 校验根节点:校验根节点是否为空,若为空则根据传入的key-value的值创建一个新的节点,若根节点不为空则继续第二步;
b. 寻找插入位置:由于TreeMap内部是红黑树实现的则插入元素时,实际上是会去遍历左子树,或者右子树
c. 新建并恢复:因为红黑树插入一个节点后可能会破坏红黑树的性质
(2) remove(Object key):红黑树的删除过程,与插入类似
treeMap总结:
a. TreeMap 是有序的,按照自然排序或者指定比较器排序;
b. 不允许有null键,可以有nul值
c. 非线程安全
d. TreeMap 是基于红黑树实现的最大的好处就是可以按照业务场景得到已排序的结果
(3) HashTable
和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射,它具有如下特点
a. Hashtable 的函数都是同步的,这意味着它是线程安全的;
b. Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口;
c. key、value都不可以为null
d. Hashtable中的映射不是有序的
HashTable与HashMap实现方式相似,只是在方法前加了同步机制(synchronized)来确保线程安全
HashTable与HashMap 比较
a. HashMap是Hashtable的轻量级实现(非线程安全的实现),它们都实现了Map接口(Map中键不可重复,值可以重复)。
b. Hashtable不允许null作为key、value,当出现null值会抛出异常。
c. HashMap可以使用null作为key、value,并且null元素会计入size。
d. Hashtable是线程安全的,HashMap非线程安全的。
(4). LinkedHashMap
LinkedHashMap继承自HashMap,是一个有序的Map接口实现,它具有如下特点:
a. 有序,元素可以按插入顺序或访问顺序排列
b. 是基于散列表实现,与HashMap的不同之处在于LinkedHashMap内部多了一个双向循环链表的维护,该链表是有序的,可以按元素插入顺序或元素最近访问顺序(LRU)排列,即LinkedHashMap=散列表+循环双向链表
LinkedHashMap的数据结构
4. Set
Set集合与List一样,都是继承自Collection接口,和List不同的是,Set内部实现是基于Map的,所以Set取值时不保证数据和存入的时候顺序一致,并且不允许空值,不允许重复值。
主要有2个实现类:TreeSet、HashSet 。
(1) TreeSet
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。有如下特点:
a. 唯一性
b. 有序
c. 支持序列化
d. 非线程安全
(2) HashSet
a. HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持。它不保证set?的迭代顺序;特别是它不保证该顺序恒久不变。此类允许使用null元素,有且只允许一个null元素
b. 不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个private static final Object PRESENT = new Object();
c. HashSet的实现:对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet?的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。
TreeSet 与HashSet比较
a. HashSet不能保证元素的排列顺序,TreeSet可以确保集合元素处于排序状态
b. TreeSet 是二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值;HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。
c. HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashcode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。