集合类
ArrayList
线程不安全问题
-
add方法执行的时候,可能出现脏读的问题
private void add(E e, Object[] elementData, int s) { if (s == elementData.length) // s=size elementData.length = capacity elementData = grow(); // 如果grow 则扩大1.5倍 elementData[s] = e; // 两个线程执行这一句。 size = s + 1; }
-
扩充的过程可能数组越界
private void add(E e, Object[] elementData, int s) { if (s == elementData.length) // 最后这个s是 size输入的,也就是说,这个地方是s elementData = grow(); elementData[s] = e; size = s + 1; }
-
专门引起
ConcurrentModificationException
异常的modCount
变量。
关于初始化
可以看出来,初始化的如果是个空对象(或者默认容量),会先指向元空间。而开辟动态数组所在的空间是在add的时候发现需要grow的时候创建的。
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 构造函数
public ArrayList(int initialCapacity){
if(initialCapacity>0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0){
this.elementData=EMPTY_ELEMENTDATA;
}else {
throw new IllegalArgumentException("Illegal Capacity" + initialCapacity);
}
}
public ArrayList(){
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
LinkedList
是一个双端队列。其底层实现主要包括:
- 内部静态类Node
- 静态内部类可以在外部类的非静态方法下new 也可以在静态方法下new。我个人感觉是更加厉害的,而内部类必须先new一个外部类,然后
.new
一个内部类,这么麻烦的写法相当于杜绝了外部接触到内部类了吧。 - 静态内部类是不能访问外部类的非静态成员
- 静态内部类可以在外部类的非静态方法下new 也可以在静态方法下new。我个人感觉是更加厉害的,而内部类必须先new一个外部类,然后
- unlink相关:包括unlink(), unlinkFirst(), unlinkLast()底层实现,具体代码如上,实现的接口为remove,poll等的时候。通过将节点拿出双向链表,并将Node的属性设置为null,帮助GC来清除内容。
- link相关:linkLast(), linkFirst(), linkBefore()底层实现,实现插入操作,实现的接口包括set,add,offer接口。
- node(int index)函数:通过for循环来定位数组下标对应Node的位置,这个也是LinkedList不适合查询的原因。
ArrayList和LinkedList的在序列化上的区别(关于空间上的优化问题)
序列化是和ArrayList
不同的,因为ArrayList
中有elementData,其中包含了许多null地址,不应该加入序列化当中,而LinkedList
的链表添加才是真正意义上的动态添加,他不需要加入了反而是first和last指针这些东西。
本质上来讲,这两个链表类型在序列化的时候需要的只有数组中的元素,而其他东西都应该是不需要序列化的,但是需要把一些辅助序列化的信息放进去。
RandomAccess
- RandomAccess接口,标记接口,表明List提供了随机访问功能,也就是通过下标获取元素对象的功能。之所以是标记接口,是该类本来就具有某项能力,使用接口对其进行标签化,便于其他的类对其进行识别(instanceof)。
- ArrayList数组实现,本身就有通过下标随机访问任意元素的功能。那么需要细节上注意的就是随机下标访问和顺序下标访问(LinkedList)的不同了。也就是为什么LinkedList最好不要使用循环遍历,而是用迭代器遍历的原因。
- 实现RandomAccess同时意味着一些算法可以通过类型判断进行一些针对性优化,例子有Collections的shuffle方法。简单说就是,如果实现RandomAccess接口就下标遍历,反之迭代器遍历
红黑树
对应到java里面的数据结构就是TreeSet<>
。以及TreeMap<>
。
public TreeSet() {
this(new TreeMap<>());
}
红黑树是一种比较平衡的搜索二叉树。
搜索二叉树:左子树任何值小于根节点小于右子树任何值。且没有重复值。相同的值是可以压缩在一起的,比如加一个链表。
平衡性问题
AVL树解决了搜索二叉树倾斜问题。但是AVL树是一个高度平衡的树,需要通过左旋和右旋达到平衡的目的。当AVL树把自己左树高度和右树高度,记录在节点值里面。当他插入一个节点的时候,它会从插入一个节点开始,想回走,修改沿途的高度。当修改以后,就会发现左边的高度和右边的高度不平衡。这样就找到了一个局部,这个时候就会发生旋转。
为了平衡,AVL有四种组合,红黑树也是有五种组合。
而AVL是严格平衡树,而红黑树也好,SB树也好都把平衡性阉割到一定程度,其中红黑树就是不要超过2倍到2倍以上。这样红黑树的调整的没有那么频繁,实际上,所有的树的增删改差都是O(log n)。
TreeMap和HashMap:我们知道红黑树的原理是先查有没有,然后如果没有再put,如果有则直接返回。而HashMap则是找到put的地方如果有则替换。
Redis底层是具备多级索引结构的链表->跳表
使用红黑树(重点)
只要他的时间复杂度小于hashMap,就是红黑树的存在的意义!CRUD都是O(log n)的时间复杂度
TreeSet<Integer> treeSet = new TreeSet<Integer>(); // 红黑树是一个值,key参与排序
TreeMap<Integer,String> treeMap = new TreeMap<>(); // 红黑树里面有两个值
treeSet.add(5);
treeSet.add(10);
treeSet.add(15);
treeSet.add(20);
treeSet.add(25);
treeSet.containKey(15); // 查询有没有
// 他比哈希表强的在lastKey()cellingKey()floorKey()时间复杂度都是O(log n)而哈希是O(n)
treeSet.lastKey(); // 可以直接找到最大值。O(log n) 而哈希表是O(n)的代价
treeSet.cellingKey(12); // 如果输入数值不存在,返回刚比这个数大的数
treeSet.floorKey(12); // 找到比这个数较小的数,
HashMap
变量
变量主要包括:一个Node[]数组的table。其中Node是接口,他可能是一个单向链表。
LinkedHashMap
大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。
LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序。
最常见的用处LRU。
我们可以简单的认为LinkedHashMap是HashMap+LinkedList,即它既使用HashMap操作数据结构,又使用LinkedList维护插入元素的先后顺序。
由于LinkedHashMap是HashMap的子类,当JDK8中HashMap可以变成树之后,相应的LInkedHashMap也做了这些优化。
The changes in node classes also require using two fields, (head, tail) rather than a pointer to a header node to maintain the doubly-linked before/after list. This class also previously used a different style of callback methods upon access, insertion, and removal.