介绍
Java 集合类(位于java.util包)主要由两个根接口 Collection 和 Map 派生出来的。任何对象加入集合类后,自动转变为
Object
类型
,所以在取出的时候,需要进行强制类型转换。
Collection
Collection存储元素集合
。分三大类:List、Queue、Set(元素不可重复),如下:
List
参考资料:百度安全验证
List接口继承自Collection接口,它是有序、可重复的单列集合
。 它的行为和数组几乎完全相同,它们都是有序的存储结构。另外List集合中允许有重复的元素,甚至可以有多个null值。
在List接口中定义了子类的一些通用方法,如下所示:
- boolean add(E e):在集合末尾添加一个数据元素;
- boolean add(int index, E e):在集合的指定索引出添加一个数据元素;
- E remove(int index):删除集合中指定索引的元素;
- boolean remove(Object e):删除集合中的某个元素;
- E get(int index):获取集合中指定索引的元素;
- E set(int index, E e):修改集合中指定索引的元素;
- int size():获取集合的大小(包含元素的个数)。
- List of():可以根据给定的数据元素快速创建出List对象,但该方法不接受null值,如果传入null会抛出NullPointerException异常(Java 9引入)。
ArrayList
ArrayList是一个数组队列,位于java.util包中,它继承自AbstractList,并实现了List接口。其底层是一个可以动态修改的数组
,该数组与普通数组的区别,在于它没有固定的大小限制,我们可以对其动态地进行元素的添加或删除。
因为ArrayList的底层是一个动态数组,当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除
(应该考虑到使用Linkedlist)。
LinkedList
LinkedList直接继承自AbstractSequentialList,并实现了List、Deque、Cloneable、Serializable等多个接口。通过实现List接口,具备了列表操作的能力;通过实现Cloneable接口,具备了克隆的能力;通过实现Queue和Deque接口,可以作为队列使用;通过实现Serializable接口,可以具备序列化能力。
它的底层是基于线性链表这种常见的数据结构,但并没有按线性的顺序存储数据,而是在每个节点中都存储了下一个节点的地址。
链表可分为单向链表和双向链表。一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接。
一个双向链表有三个整数值: 数值、向后的节点链接、向前的节点链接。
LinkedList的优点是便于向集合中插入或删除元素,尤其是需要频繁地向集合中插入和删除元素时,使用LinkedList类比ArrayList的效率更高。但LinkedList随机访问元素的速度则相对较慢,即检索集合中特定索引位置上的元素速度较慢。
Vector(不推荐)
Vector (向量) 可以实现可增长的对象数组,当容量不够时,会自动扩容,如果在创建Vector时定义了每次增长的容量大小,那么每次会按照规定的大小进行扩容,如果没有规定,则每次扩容为原来的2倍,和Arraylist一样,扩容时每次先创建一个新的数组,随后将原来的数组拷贝过去完成扩容。
和 ArrayList 区别:
- Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,Vector中的方法都使用了synchronized关键字以确保所有的操作都是线程安全的。但实现同步需要很高的花费,因此如果不考虑到线程的安全因素,用Arraylist效率比较高。
- ArrayList每次扩容时会增加1.5倍,而Vector会增加2倍。
与数组互转
List转数组有如下几种方式:
- toArray()方法:该方法会返回一个Object[]数组,但该方法会丢失类型信息,在实际开发时较少使用;
- toArray(T[])方法:传入一个与集合的数据元素类型相同的Array,List会自动把元素复制到传入的Array中;
- T[] toArray(IntFunction<T[]> generator)方法:函数式写法,这是Java中的新特性。
数组也可以转为List集合,一般的方式如下:
- List.of(T...)方法(Java9 引入):该方法会返回一个只读的List集合,如果我们对只读List调用add()、remove()方法会抛出UnsupportedOperationException异常。其中的T是泛型参数,代表要转成List集合的数组;
- Arrays.asList(T...)方法:该方法也会返回一个只读的List集合,但它返回的List不一定就是ArrayList或者LinkedList,因为List只是一个接口。
注意:无论我们是通过List.of()方法,还是通过Arrays.asList()方法,都只会返回一个只读的集合。这种集合在遍历时不能进行增删改等更新操作,只能进行读取操作,否则会产生java.lang.UnsupportedOperationException异常。
Set
参考资料:百度安全验证
Set继承自Collection接口,主要有两个常用的实现类HashSet类和TreeSet类。与其他集合不同,Set集合具有自己的一些特性:
- Set集合中的元素都是唯一的,
不允许有重复值,且最多只允许包含一个
null
元素
; - Set集合中的元素
没有顺序
,我们无法通过索引来访问元素,但TreeSet是有序的; - Set集合
没有固定的大小限制
,可以动态地添加和删除元素; - Set集合提供了高效的元素查找和判断方法。
从特性上来看,Set
相当于是一个只存储
key
、不存储
value
的
Map
。我们可以把Set想象成是一个“特殊的Map”,这个Map只有key却没有value,所以我们可以用Set去除重复的元素。
另外,由于放入Set的元素和Map的key类似,需要被放入元素要正确地实现
equals()
和
hashCode()
方法
,否则该元素就无法正确地放入Set。
Set接口定义的常方法:add、remove、contains、size、toArray等
HashSet
HashSet是一种非常常用的集合类型,它实现了Set接口,并继承了AbstractSet抽象类。HashSet集合的底层是通过HashMap实现,它使用哈希算法来存储和管理集合中的元素。HashSet不是线程安全的,默认线程不同步,如果有多个线程同时访问或修改同一个HashSet,必须通过代码来保证同步操作。
HashSet<String> sites = new HashSet<String>();
去重原理
从底层实现来看,HashSet的底层其实就是一个值为Object的HashMap,如下图所示:
所以HashSet其实就是按照Hash算法来实现元素的查找和存储的,具有很好的存取和查找性能。当我们向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。此时如果有两个元素通过equals()方法进行比较,返回的结果为true,但它们的hashCode却不相等,HashSet也会把它们存储在不同的位置,我们依然可以添加成功。也就是说,如果两个对象的hashCode值相等,且通过equals()方法比较返回的结果也为true, HashSet集合才会认为两个元素相等。
TreeSet
TreeSet也是一种很常用的集合类型,它实现了Set和SortedSet接口,并且继承自AbstractSet抽象类。TreeSet集合的底层基于红黑树
,可以使用自然排序或指定的比较器对集合中的元素进行排序。
另外,SortedSet接口是Set接口的子接口,能够对集合进行自然排序,因此TreeSet
类默认情况下就是自然排序
(
升序
)
的
。
但TreeSet只能对实现了Comparable接口的类对象进行排序,所以我们使用TreeSet集合存储对象时,该对象必须要实现Comparable接口。
除了Set类中通用的方法之外,TreeSet类还有如下几个特有的方法:
Map
Map与Collection并列存在。其中的key和value都可以是任何引用类型的数据。Map中的key用Set来存放,不允许重复。
Map常用方法:
1、添加、删除操作
2、元素查询的操作
3、元视图操作
哈希表概念
哈希表是一种用于实现关联数组的数据结构,它通过将键映射到表中的某个位置来实现快速的数据检索。哈希表的主要特点包括:
- 快速检索:哈希表使用哈希函数来计算键的哈希值,然后将该哈希值映射到表中的索引位置,使得查找、插入和删除操作的平均时间复杂度为O(1)。
- 哈希冲突:不同的键可能映射到相同的索引位置,这种情况称为哈希冲突。解决冲突的方法包括链地址法(HashMap采用的)、开放寻址法、再哈希法等。
- 动态扩展:哈希表通常具有动态扩展的能力,当表中元素数量达到一定阈值时自动进行扩容,以保持较低的负载因子,提高性能。
- 哈希函数:哈希函数负责将键映射到哈希值。好的哈希函数应当尽量减少冲突,使得哈希值分布均匀。
在Java中,哈希表被实现为HashMap类,它实现了Map接口。
HashMap
介绍
HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。它不是线程安全的,可以接受为null的key和value。使用了自定义的哈希算法来计算hash值。
实现原理
参考资料:Java 8 HashMap 详解_java8 hashmap-优快云博客
Java架构直通车——Java8 HashMap详解-优快云博客
底层结构
JDK 1.8 的 HashMap 底层数据结构与 JDK1.7 时大致相同,主要为数组+链表。它采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。
但在1.8 版本中如果链表元素数量达到 8 ,就会尝试进行树化操作(如果数组长度小于 64, 则进行扩容操作,而不是树化操作),将链表转化为红黑树结构。故 HashMap 数据结构可作如下总结:
- JDK 1.7: 数组+链表
- JDK 1.8: 数组+链表+红黑树
链表和红黑树转换
- 为何不直接使用红黑树?
当元素小于8个时,做查询操作链表结构已经能保证查询性能。当元素大于8个的时候,此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。因为红黑树需要进行左旋,右旋,变色操作来保持平衡,这些都是性能损耗。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑是浪费性能。
- 红黑树什么时候退化为链表?
节点数量为6的时候退转为链表,中间有个差值7可以防止链表和树之间频繁的转换。如果设计成链表个数超过8则链表转换成树结构,小于8则树结构转换成链表,如果一个HashMap不停插入删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率很低。
容量扩容
- 初始化容量
HashMap默认容量为 16,最大容量为 1<<30 。扩容负载因子为 0.75,也就是默认容量下数组中实际存储的元素数量达到 16 * 0.75 = 12,就会触发扩容。
- 扩容为什么是2的次幂
HashMap为了存取高效就要尽量减少碰撞,也就是尽量把数据分配均匀,每个index下标上的链表(也可能是红黑树)长度需要大致相同, 这个算法实际就是取模,hash%length,但是取模运算不如位移运算快,也就是hash%length 等价于 hash()&(length-1)。
之所以取数组长度为 2 的n 次方,是因为2的n次方用二进制表示实际就是1后面n个0,而2的n次方-1,实际就是n个1。 所以,保证容积是2的n次方,是为了保证在做 hash&(length-1)的时候,每一位都能&1,尽量减少哈希冲突。
- 扩容机制
HashMap 扩容为原来2倍,所以链表中节点的位置要么是在数组原位置,要么是在数组原位置再移动2次幂的数组位置,且链表元素的顺序不变。
总结
HashMap简单的来说,就是通过数组+链表或者红黑树的形式实现的。单链表和红黑树之间的转换条件是,底层数组容量大于64的时候并且单链表长度大于8的时候,这样就可以进行一个转换。因为在底层数组长度比较小的时候,Hash冲突会比较频繁,更可能出现长链表,这时候会优先考虑扩容而不是树化。
对于扩容来说。初始时候,采用一个懒加载的方式初始化底层数组,也就是对于数组有实际操作的时候才进行初始化,这个初始化默认长度是16。对于这个数组,有负载因子默认是0.75,当哈希桶占用量超过负载因子乘以底层数组长度的时候,就会进行一个二倍的扩容。进行二倍扩容的考虑是:对于每个元素,要么就在当前的位置,要么就是当前位置+扩容长度的位置,非常好计算。
LinkedHashMap
LinkedHashMap继承自HashMap,是将HashMap与双向链表合二为一,即一个将所有Entry节点链入一个双向链表的HashMap(LinkedHashMap = HashMap + 双向链表)。
相比于Hashmap,LinkedHashMap新增成员变量:双向链表头结点header和标志位accessOrder。
- private transient Entry<K,V> header; //双向链表头节点,也即哨兵节点,里面不存储任何信息;
- private final boolean accessOrder; //有序性标识,默认为false(即默认按照插入顺序迭代),为true时(按照访问顺序迭代,支持实现LRU算法时)。
HashTable(不推荐)
(1)Hashtable 是一个散列表,它存储的内容是键值对(key-value)映射。
(2)Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。
(3)HashTable直接采用的key的hashCode()。
(4)Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
(5)Hashtable默认的容量大小是11;增加容量时,每次将容量变为“原始容量x2 + 1”。
(6)Hashtable支持contains(Object value)方法,而且重写了toString()方法。
HashTable非常粗暴,使用synchronized关键字,多线程环境下效率很低。我们在HashTable类注释上看到上面一段说明:如果不要求线程安全,推荐使用HashMap,如果要求线程安全,推荐使用ConcurrentHashMap或者Collections.synchronizedMap(new HashMap()) 来实现。
Properties
Properties 类是 Hashtable 的子类,该对象用于处理属性文件。由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型。
存取数据时,建议使用setProperty()方法和getProperty()方法。
集合操作
集合遍历
对集合进行遍历操作,以下是几种常用的集合遍历方式:
- 普通for循环:配合get(索引值)方法进行,这种遍历方式实现起来代码较为复杂,且get(int)取值方法只对ArrayList比较高效,但对LinkedList效率较低,索引越大时访问速度越慢。
- Iterator迭代器:不同的集合对象调用iterator()方法时,会返回不同实现的Iterator对象,该Iterator对象对集合总是具有最高的访问效率。
- 增强for循环:比普通for循环更为简洁,内部会自动适用迭代器;
- 集合对象的forEach (BiConsumer<? super K, ? super V> action):Java8以上支持,效率更高。
Iterator(迭代器)
Iterator(迭代器)不是一个集合,它是一种用于访问集合的方法,有几个常用方法。
- boolean hasNext() 该方法用于判断集合中是否还有下一个元素;
- E next():该方法用于返回集合的下一个元素,并且更新迭代器的状态;
- remove() 将迭代器返回的元素删除; Iterator<String> it = sites.iterator(); // 获取迭代器
虽然使用Iterator遍历List集合的代码,看起来比使用索引较复杂,但Iterator遍历List集合的效率却是最高效的方式。
fail-fast和fail-safe
参考资料:fail-fast(快速失败)机制和fail-safe(安全失败)机制的介绍和区别_请先说说非并发集合中fail-fast机制-优快云博客
- fail-fast (快速失败):直接在容器上进行遍历,在遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException异常导致遍历失败。java.util包下的集合类都是快速失败机制的, 常见的的使用fail-fast方式遍历的容器有HashMap和ArrayList等。
- fail-safe (安全失败):这种遍历基于容器的一个克隆。因此,对容器内容的修改不影响遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。
【原理】:采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
【缺点】:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容。
Map移除指定key
- 使用Lambda表达式
map.entrySet().removeIf(entry -> entry.getKey().equals("A")); // 移除Key为"A"的元素
- Google Guava的filterKeys方法
Map<String, Integer> newMap = Maps.filterKeys(map, key -> !key.equals("A")); // 移除Key为"A"的元素