程序 = 数据结构 + 算法
本文概述Java中常用的数据结构,并简述其使用场景
1. 数据结构的定义
数据结构是一种逻辑意义,指的是逻辑上的数据组织方式及相应的处理,与数据在磁盘的具体存储方式不完全相关。磁盘存储数据的方式可能是顺序存储也可能是链式存储。
逻辑上的数据组织方式有:队列、树、图、哈希等。
数据的处理:增删改查、遍历 。
2. 数据结构的分类
以数据是否存在前继和后继对数据结构做出如下分类 :
线性结构:0至1个直接前继和直接后继。如:顺序表(数组)、链表、栈、队列。
树结构:0至1个直接前继和0至n(n大于等于2)个直接后继。
图结构:0至n(n大于等于2)个直接前继和直接后继。
哈希结构:没有直接前继和直接后继,通过特定的哈希函数将索引与存储的值关联起来。
3.Java中的集合
List集合:存在明确的上一个和下一个元素,也存在明确的第一个元素和最后一个元素
ArrayList:非线程安全,内部使用数组进行存储;扩容时需要创建新的数组,并复制数据。访问数据较快,插入和删除较慢;
LinkedList:本质是双向链表,添加和删除速度较快,随机访问较慢。
Queue(队列)集合:队列是一种先进先出的数据结构,也是线性表的一种,只允许在表的一端获取数据,在另一端插入数据
自BlockingQueue(阻塞队列)发布后,队列多在高并发场景作为Buffer(数据缓冲区)使用
Map集合:Key-Value键值对作为存储元素实现的哈希结构,Key按哈希函数计算后保证唯一,Value则是可重复的
HashMap不是线程安全的,ConcurrentHashMap是线程安全的,TreeMap是Key有序的Map集合类
Set集合:不允许出现重复元素的集合类型
HashSet是使用HashMap来实现的,TreeSet是使用TreeMap实现的;LinkedHashSet继承自HashSet,具有HashSet的优点,内部使用链表维护元素的插入顺序。
* 集合初始化
集合初始化时指定集合初始值的大小可以减少扩容成本,有助于提高性能。
ArrayList默认初始值为10,每次扩容的大小大约为之前大小的1.5倍,oldCapacitiy + (oldCapacitiy >> 1)
HashMap默认初始值为16,扩容时需重建hash表,非常影响性能。HashMap中有两个重要参数:Capacity和Load Factor(值默认为0.75),
HashMap基于这两个数的乘积表示能放入集合的元素个数;HashMap的容量是在第一次put时才创建,并不是在new时分配。
4.Java中的数组
数组是一种顺序表,下标从0开始。数组是固定容量大小的,数组内的值可以修改,但数组大小不能改变。
* 数组转集合
Arrays.asList返回的是Arrays的内部类ArrayList,此内部类仍指向原数组,并不是java.util.ArrayList集合,在进行修改操作时会抛异常。
正确的转换方式:
List<Object> objectList = new java.util.ArrayList<Object>(ArrayList.asList(数组));
集合转数组时注意数组类型与集合数据类型保持一致,数组的大小为集合大小list.size()。
5. 集合与泛型
引入泛型是为了保证类型安全,集合间相互赋值时是传递引用,直接操作赋值后的集合会影响原集合。
List :无任何类型限制和赋值限定
List<Object>:用法等同于List,但是在接受其他泛型赋值时会编译报错;List<Integer>不能赋值给List<Object>
List<?> :通配符集合,可以接受任何类型的集合引用赋值,但可以remove和clear;常作为形参或返回值类型
public void collectionTest() {
List<Integer> listInteger = new ArrayList<Integer>(10);
listInteger.add(1);
List list = listInteger;
list.add("hello");
for(Object obj : listInteger ) {
//打印结果为: 1 hello
//原集合的值被修改了,不使用泛型可能造成隐藏Bug
System.out.println(obj);
}
List<?> a = listInteger;
//add编译报错
a.add(new Integer(2));
}
List<? extends T> :类型为T或T的子类;Get First,适用于消费集合元素为主的场景
List<? super T> :类型为T或T的父类;Put First,适用于生产集合元素为主的场景
6. 元素的比较
6.1 Comparable和Comparator
约定俗成,比较器的返回结果:小于的情况返回-1;等于的情况返回0;大于的情况返回1。
Comparable接口:与自己比,比较方法是compareTo
Comparator接口:平台性质的比较器,比较方法是compare
Arrays.sort中使用了TimSort算法,该算法是归并排序和插入排序优化后的算法,时间复杂度最优可达O(n),平均时间复杂度为O(nlogn)
6.2 hashCode和equals
hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。
若只复写equals方法,调用equals比较对象时,实质比较的是equals方法中的属性是否相同,并不判断对象本质是否相等。
若同时复写equals方法和hashCode,则调用equals时相当于使用"=="
尽量避免通过实例对象引用来调用equals方法,否则容易抛出空指针异常,建议使用Objects中的equals方法,源码如下:
public static boolean equals(Object a, Object b) {
return (a==b) || (a != null && a.equals(b));
}
7. fail-fast与fail-safe机制
fail-fast机制是集合中常见的错误检测机制,即检查集合的元素在遍历过程中的一致性,若遍历时集合产生了变化则产生fail-fast。java.util包下的所有集合类都是fail-fast的,ArrayList.subList()方法产生的子列表与主列表间会相互影响,如产生子列表后再删除主列表元素会导致子列表的操作异常。
在遍历删除集合元素时,使用Iterator机制;多线程并发的场景应加锁,源码如下:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
synchronized(对象) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
}
fail-safe机制可简单理解副本快照的模式,遍历时对副本进行操作,修改操作在原始数据上进行,修改完成后同步到副本中。concurrent包中的集合都是fail-safe的。
COW(Copy-On-Write):读写分离的设计思想,修改数据时复制一个新的集合,在新集合上进行修改,完成后将旧集合的引用指向新集合。仅适用于读多写极少的场景,若频繁进行写操作会导致效率急速下降;另一缺点为数据的实时性无法保证。
/**
* 此代码执行时间为几十s,使用ArrayList执行时间为ms级别
*/
public static void main(String[] args){
List<Long> copy = new CopyOnWriteArrayList<Long>();
long start = System.nanoTime();
for(int i=0; i < 20 * 10000; i++ ) {
copy.add(System.nanoTime());
}
}
8. Map集合
ConcurrentHashMap在性能上与HashMap区别不大,但是Key-Value均不能为空,两者互换使用时需注意空指针(NPE)问题。
8.1 树
节点的深度指当前节点到根节点的距离,节点的高度指当前节点到最远子叶子节点的距离。
二叉树
每个节点至多有两个子节点的树称为二叉树。
平衡二叉树
任何节点的左右子树高度差不超过1,空树或只有根节点的树也是平衡二叉树。
二叉查找树(Binary Search Tree)
二叉查找树的所有节点满足该节点左子树的值均小于该节点,右子树的值均大于该节点。
遍历方式:前序、中序、后序
(1)在任何递归子树中,左节点一定在右节点之前遍历;
(2)前中后序仅指父节点在遍历时的位置顺序。
二叉查找树在数据改变时容易失衡,导致操作效率变低;为了保证二叉树的平衡性,有如下算法实现,如AVL树,红黑树,SBT等等,现在常用的是红黑树。
AVL树通过对不平衡的树进行左旋或者右旋以达到平衡,属于平衡二叉树,在删除数据后可能发生大量旋转导致时间成本增加。
红黑树也是通过左旋或右旋以实现平衡,非绝对平衡。红黑树的特点(有红必有黑,红红不相连):
(1)节点非黑即红
(2)根节点必须为黑
(3)所有NIL节点都是黑色的(Nothing In Leaf,叶子节点上虚构的子节点)
(4)红色节点不相邻
(5)任何递归子树内,根节点到叶子节点的所有路径上包含相同数目的黑色节点
8.2 TreeMap
TreeMap的Key是有序不重复的,支持获取头尾元素。TreeMap依靠Comparable或Comparator来实现Key的去重;HashMap使用hashCode和equals方法去重。
8.3 HashMap
HashMap的两个主要问题:死链问题和扩容数据丢失。(多线程高并发场景)
哈希类集合的三个基本概念:
HashMap高并发场景新增对象丢失的原因:
* 并发赋值时被覆盖
* 已遍历区间新增元素会丢失
* “新表”被覆盖
* 迁移丢失。在迁移过程中,有并发时,next被提前置成null
HashMap死链形成原因(数据在桶内以链表的形式存储):
死链形成的原因是链表中指向下一个元素的next值被并发修改,导致形成对象间互链或对象自己互链。
8.4 ConcurrentHashMap
JDK对该类进行的优化:
(1)取消分段锁机制,进一步降低冲突概率。
(2)引入红黑树结构。同一个哈希槽上的元素超过一定阈值后,单向链表转化为红黑树结构。元素数量减少到一定的个数时,红黑树会退化成单向链表。
(3)使用了更加优化的方式统计集合内元素数量。使用CAS机制。
CAS(Compare And Swap):JDK提供的非阻塞原子性操作,通过硬件保证了比较-更新操作的原子性。解决轻微冲突的多线程并发场景下使用锁造成性能损耗的一种机制。它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 JDK中的Unsafe类提供了一系列的compareAndSwap方法。