JAVA容器
容器
普通容器
集合类存放于 Java.util 包中, 主要有 3 种: set(集)、 list(列表包含 Queue)和 map(映射)。
Collection: Collection 是集合 List、 Set、 Queue 的最基本的接口。
Iterator:迭代器,可以通过迭代器遍历集合中的数据。
Map:是映射表的基础接口。
List
元素有序,可重复。
ArrayList(数组,初始容量10,负载因子1,扩容增量1.5)
扩容新建数组,复制原数组中元素,开销很大。
删除元素将后面元素向前移一位,复杂度On。
for循环遍历数据较快。
//遍历方式
public class ArrayListTraversal {
public void arrayListTraversal(List<Integer> lists){
/* 第一种遍历方式 */
System.out.print("for循环的遍历方式:");
for (int i = 0; i < lists.size(); i++) {
System.out.print(lists.get(i));
}
System.out.println();
/* 第二种遍历方式 */
System.out.print("foreach的遍历方式:");
for (Integer list : lists) {
System.out.print(list);
}
System.out.println();
/* 第三种遍历方式 */
System.out.print("Iterator的遍历方式:");
for (Iterator<Integer> list = lists.iterator(); list.hasNext();) {
System.out.print(list.next());
}
}
public static void main(String[] args) {
List<Integer> lists = new ArrayList<Integer>();
/* 添加元素 */
for (int i = 0; i < 10; i++) {
lists.add(i);
}
new ArrayListTraversal().arrayListTraversal(lists);
}
}
LinkedList(链表)
基于双向链表实现,使用 Node 存储链表节点信息。
在列表中插入和删除速度快,但是查找需要遍历整个链表。
foreach遍历数据较快。
(1) 无论ArrayList还是LinkedList,遍历建议使用foreach,尤其是数据量较大时LinkedList避免使用get遍历。
(2) List使用首选ArrayList。对于个别插入删除非常多的可以使用LinkedList。
(3) 可能在遍历List循环内部需要使用到下标,这时综合考虑下是使用foreach和自增count还是get方式。
Set
元素无序,不可重复。
对象的相等性本质是对象 hashCode 值判断(java 是依据对象的内存地址计算出的此序号)。
如果想要让两个不同的对象视为相等,必须覆盖 Object 的 hashCode 方法和 equals 方法。
HashSet(Hash表)
基于哈希表实现,支持快速查找,但不支持有序性操作,通过HashMap实现。
TreeSet(二叉树)
TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
LinkedHashSet(HashSet+LinkedHashMap)
继承 HashSet、基于 LinkedHashMap 实现。
底层使用 LinkedHashMap 来保存所有元素,所有方法操作与 HashSet 相同。
提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
Map
HashMap(数组+链表+红黑树,初始容量16,负载因子0.75,扩容增量1)
基于哈希表实现。
键可以是null,而且键值不可以重复,如果重复了以后就会对第一个进行键值进行覆盖。
大方向上, HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例, Entry 包含四个属性: key, value, hash 值和用于单向链表的 next。
1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
2. loadFactor:负载因子,默认为 0.75。
3. threshold:扩容的阈值,等于 capacity * loadFactor。
Java8 对 HashMap 进行了一些修改, 最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话, 需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中, 当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
HashMap特性总结
1. HashMap的底层是个Node数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。
2. 增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到key的hashCode值;
2)将hashCode的高位参与运算,重新计算hash值;
3)将计算出来的hash值与(table.length - 1)进行&运算。
3. HashMap的默认初始容量(capacity)是16,capacity必须为2的幂次方;默认负载因子(load factor)是0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
4. HashMap在触发扩容后,阈值会变为原来的2倍,并且会进行重hash,重hash后索引位置index的节点的新分布位置最多只有两个:原索引位置或原索引+oldCap位置。例如capacity为16,索引位置5的节点扩容后,只可能分布在新报索引位置5和索引位置21(5+16)。
5. 导致HashMap扩容后,同一个索引位置的节点重hash最多分布在两个位置的根本原因是:1)table的长度始终为2的n次方;2)索引位置的计算方法为“(table.length - 1) & hash”。HashMap扩容是一个比较耗时的操作,定义HashMap时尽量给个接近的初始容量值。
6. HashMap有threshold属性和loadFactor属性,但是没有capacity属性。初始化时,如果传了初始化容量值,该值是存在threshold变量,并且Node数组是在第一次put时才会进行初始化,初始化时会将此时的threshold值作为新表的capacity值,然后用capacity和loadFactor计算新表的真正threshold值。
7. 当同一个索引位置的节点在增加后达到9个时,会触发链表节点(Node)转红黑树节点(TreeNode,间接继承Node),转成红黑树节点后,其实链表的结构还存在,通过next属性维持。链表节点转红黑树节点的具体方法为源码中的treeifyBin(Node<K,V>[] tab, int hash)方法。
8. 当同一个索引位置的节点在移除后达到6个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的untreeify(HashMap<K,V> map)方法。
9. HashMap在JDK1.8之后不再有死循环的问题,JDK1.8之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
10. HashMap是非线程安全的,在并发场景下使用ConcurrentHashMap来代替。
//遍历map
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class TestMap {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(1, "a");
map.put(2, "b");
map.put(3, "ab");
map.put(4, "ab");
map.put(4, "ab");// 和上面相同 , 会自己筛选
System.out.println(map.size());
// 第一种:
/*
* Set<Integer> set = map.keySet(); //得到所有key的集合
*
* for (Integer in : set) { String str = map.get(in);
* System.out.println(in + " " + str); }
*/
System.out.println("第一种:通过Map.keySet遍历key和value:");
for (Integer in : map.keySet()) {
//map.keySet()返回的是所有key的值
String str = map.get(in);//得到每个key多对用value的值
System.out.println(in + " " + str);
}
// 第二种:
System.out.println("第二种:通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<Integer, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
// 第三种:推荐,尤其是容量大时
System.out.println("第三种:通过Map.entrySet遍历key和value");
for (Map.Entry<Integer, String> entry : map.entrySet()) {
//Map.entry<Integer,String> 映射项(键-值对) 有几个方法:用上面的名字entry
//entry.getKey() ;entry.getValue(); entry.setValue();
//map.entrySet() 返回此映射中包含的映射关系的 Set视图。
System.out.println("key= " + entry.getKey() + " and value= "
+ entry.getValue());
}
// 第四种:
System.out.println("第四种:通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}
}
TreeMap(可排序)
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
如果使用排序的映射,建议使用 TreeMap。
在使用 TreeMap 时, key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 ava.lang.ClassCastException 类型的异常。
LinkedHashMap(记录插入顺序)
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
Queue
public interface Queue<E> extends Collection<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
}
FIFO队列
Queue<Integer> q=new LinkedList<Integer>();
优先队列
默认升序,底层为堆,初始容量11
传入对象时需要指定比较器
Queue<Integer> q=new PriorityQueue<Integer>();
Queue<Student> q=new PriorityQueue<Student>((e1,e2)->(e1.id-e2.id));
阻塞队列
阻塞队列,在java.util.concurrent包下。
同步容器
可以简单地理解为通过synchronized来实现同步的容器。
Vector(数组实现,线程同步)
实现与 ArrayList 类似,使用 synchronized 进行同步,访问速度慢。
线程安全,底层数组,初始10,默认扩容2倍。
Stack(继承Vector类,底层实现是数组)
栈,继承Vector类,底层实现是数组,先进后出。
HashTable(线程安全)
Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似。
Collections.synchronized方法生成
这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized。
削弱了并发性,当多个线程共同竞争容器级的锁时,吞吐量就会降低。
并发容器
采用CAS算法,部分代码使用synchronized锁保证线程安全**。java.util.concurrent包中提供了多种并发类容器。
CopyOnWriteArrayList
对应的非并发容器:ArrayList。
目标:代替Vector、synchronizedList。
原理:利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证可见性,当然写操作的锁是必不可少的。
CopyOnWriteArraySet
对应的非并发容器:HashSet。
目标:代替synchronizedSet。
原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用 CopyOnWriteArrayList 的addIfAbsent方法,其遍历当前Object数组,如果Object数组中已有当前元素,则直接返回,如果没有则放入Object数组的尾部并返回。
ConcurrentHashMap
对应的非并发容器:HashMap。
目标:代替Hashtable、synchronizedMap,支持复合操作。
原理:JDK6采用更加细粒度的加锁机制Segment“分段锁”,JDK8采用CAS无锁算法。
ConcurrentSkipListMap
对应的非并发容器:TreeMap。
目标:代替synchronizedSortedMap(TreeMap)。
原理:Skip list(跳表)是一种可以代替平衡树的数据结构,默认按Key值升序。
ConcurrentSkipListSet
对应的非并发容器:TreeSet。
目标:代替synchronizedSortedSet。
原理:内部基于ConcurrentSkipListMap实现。
ConcurrentLinkedQueue
不会阻塞的队列。
对应的非并发容器:Queue。
原理:基于链表实现的FIFO队列(LinkedList的并发版本)。
LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue
对应的非并发容器:BlockingQueue。
特点:拓展了Queue,增加了可阻塞的插入和获取等操作。
原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒。
实现类:
LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
PriorityBlockingQueue:按优先级排序的队列
并发容器是专门针对多线程并发设计的,使用了锁分段技术,只对操作的位置进行同步操作,但是其他没有操作的位置其他线程仍然可以访问,提高了程序的吞吐量。