目录
首先看一下类图
Collection集合称为单列集合,Map称为双列集合
一、List
有序、可重复
1、ArrayList 常用
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
底层维护的是一个数组,根据源码可以看到,如果使用无参构造来创建arraylist,默认大小为0,当添加第一个元素的时候,会默认扩容到10,添加满以后,会按照1.5倍扩容规则进行扩容。如果使用有参构造来创建arraylist,例如 List list = new ArrayList<>(8); 会按照指定的大小进行创建集合,也就是默认是8,扩容机制就是按照传入的大小的1.5倍来进行扩容,也就是8*1.5=12。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
/**源数组 开始位置 目标数组 开始位置
从源数组的index+1的位置开始截取至最后一个元素,替换从目标数组elementDataindex位置开
始的元素,实现remove效果,最后,数组的最后一个元素会有两个,将最后一个置为null值,实
现删除元素效果 且arraycopy方法为native方法*/
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
Arraylist底层维护的是一个可变数组,如果进行插入和删除操作,速度较慢,因为删除之后,要进行copyarray操作。
2、linkedList
linkedList底层维护的是一个双向链表,每个元素是一个node对象,里面有item(存值)、prev(指向上一元素)、next(指向下一元素),同时,linkedList中还维护有两个node,分别是first和last,指向整个linkedList的首元素和尾元素。
首先分析构造方法:
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public LinkedList() {
}
如图所示,只有两种构造方法,有参的构造也只是传入一个Collection集合,将内部的元素全部添加进去,并没有其他的什么逻辑。
其次,分析add方法:
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
add方法的逻辑也比较简单,实际上就是将每个元素添加至linkedList的末尾。首先添加进来的对象一定是在末尾,那么last最后的指向一定是这个元素。分析源码,先用节点L来存放last的指引,如果使用的是无参构造,那么last一定是默认的null,接下来创建新节点,上一个元素,也就是prev指向L,也就是添加前的最后一个元素,item存e,next存null,最后一个元素没有下一元素,因为是第一次添加,所以新节点的指向也就成了last,然后判断L是否为null,如果是null,那么说明这是第一次添加,first的指向也就是这个元素了,如果L不为null,也就是说这并不是第一次添加元素,那么L记录的添加前的最后元素的下一节点(next属性)就指向了新节点。最后执行size++(集合大小+1),modCount++(修改次数+1)。
3、Vector
vector底层维护的跟arrayList一样,也是一个对象数组。但是Vector是线程同步的,所以线程安全,Vector类的操作方法都带有synchronized
首先分析构造方法:
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
protected AbstractList() {
}
可见,无参构造实际调用的是一个参数传10的有参构造
分析Vector的扩容机制:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
可见,vector的add方法加有synchronized关键字,查看源码可以看到get、set.....都有,所以线程安全,同时也说明效率没有其他两个高。扩容规则:如果是无参构造创建,那就是初始化为10,扩容直接是2倍,如果是有参构造创建,是按照传进去的值的2倍来扩容。
arraylist和vector对比
利用java8新特性,给list去重
List<String> myList = list.stream().distinct().collect(Collectors.toList());
迭代器循环遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
二、Set
无序(添加和取出的顺序不一致)、不可重复
1、HashSet
首先查看构造方法:
public HashSet() {
map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
常用的是无参的构造方法,可见HashSet 的底层就是HashMap,所以HashSet的源码其实看的是HashMap的底层。
initialCapacity:初始化容量
loadFactor: 加载因子,和扩容有关,当容量大于默认的初始化容量或传入的自定义容量*loadFactor时,就提前开始扩容,并不是跟ArrayList那样,满了再扩容
dummy:用于区分另一构造方法,并没有实际意义,只是底层用的是LinkedHashMap,其余的底层用的是HashMap
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//索引计算的算法,并不是直接等于hashcode,而是有异或算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//接下来是主要的添加源码
static final int TREEIFY_THRESHOLD = 8;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果为null就去初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果该索引位置没有数据,就直接newNode放上去
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//该索引处有数据
//判断hash值是否相同,再判断key的地址是否相同或者key的值是否相同
//如果相同,说明元素重复,不进行添加
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是TreeNode类型,那么按照红黑树的方式进行树化操作
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//死循环
for (int binCount = 0; ; ++binCount) {
//不是TreeNode类型,且该索引处有值,且后面有链,需要进行循环判断
//说明都不相同,添加到最后面
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//链上面的大于等于7个的时候,开始进行树化操作,
//因为bincount是从0开始计数的,所以相当于是大于等于8个元素
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//说明后面的链里有相同的,break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//说明key是相同的,替换后面的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//具体的树化操作
static final int MIN_TREEIFY_CAPACITY = 64;
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//会继续做判断,数组是否扩容的大于等于64,如果没有,
//就继续扩容数组,链也会继续往后链
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
//这里是扩容的源码
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//相当于×2,两倍扩容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//临界值同样两倍扩容
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
源码分析
1、添加一个元素时,先得到hash值,根据算法计算出指定的索引值
2、找到存储数据表table,看这个索引位置是否已经存在元素
3、如果没有,直接加入
4、如果有,调用equals方法依次比较,如果相等,放弃添加,如果都不相同,就添加到最后,在table表的索引位置形成一个链表
5、在java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),如果链表到达8个,但是table的大小还没有到达64,就会继续给table扩容,并不会进行树化,树化的条件是两个必须同时满足。
扩容机制:
hashset的底层是hashmap,第一次添加时,table数组扩容到16,临界值(threshold)是16*0.75(默认的加载因子)= 12,如果table数组使用到了临界值12,那么就会扩容两倍,也就是16*2=32,新的临界值就是32*0.75=24,以此类推
2、LinkedHashSet
1)是HashSet的子类,实现了Set接口
2)LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组 + 双向链表
3)LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
4)LInkedHashSet不允许添加重复元素
public LinkedHashSet() {
//初始化容量 加载因子 可以忽略的一个参数,没有什么实际意义
super(16, .75f, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
说明:
1)HashSet底层是HashMap,维护的是一个数组和一个单向链表,LinkedHashSet中维护的是一个数组和一个双向链表,LinkedHashSet有head和tail,维护的首和尾
2)每个节点都有一个before和after属性,这样可以形成双向链表
3)在添加一个元素时,先求hash值,再求索引,确定改元素在table中的位置,然后将添加的元素加入到双向链表,如果已经存在,不添加,原则和hashset一样
4)正是因为维护的是一个双向链表,所以可以确保插入顺序和遍历顺序一致
5)第一次添加的时候,table的初始扩容到16,table的类型是HashMap$Node,里面的元素维护的是LinkedHashMap$Entry,第一次添加后,before为null,after为null,hash为计算的hash值,key存放的是数据,value跟hashset一样,是一个new Object(),第二次添加的时候,首节点的next就会指向新节点,新节点的before也会指向上一节点,形成一个双向链表
3、TreeSet
最大的特点是排序
public TreeSet() {
this(new TreeMap<E,Object>());
}
//可以传入排序规则
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//传入了比较器
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
//说明返回为0,两个值相等,更新值,不添加进去
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
当我们使用无参的构造方法创建TreeSet时,依然是无序的
通过构造方法,可以看到,treeset的底层就是treemap
三、Map
Map接口实现类的特点:
1)Map与Collection并列存在。用于保存具有映射关系的数据:key - value
2)Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
3)Map中的key不允许重复,当有相同的key时,等价于替换
4)Map中的value可以重复
5)Map中的key可以为null,value也可以为null,注意key为null,只能有一个,value为null,可以多个
6)常用string类作为Map的key,只是常用,key为object
7)key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value
8)HashMap也是无序的,跟hashset一样
//把map中的key value放在了node中
//同时为了方便遍历,会创建EntrySet集合,该集合存放的元素的类型是Entry,而一个Entry对象就有k,v
//key -> Set value -> Collection
HashMap$Node node = newNode(hash, key, value, null);
static class Node<K,V> implements Map.Entry<K,V>
map的遍历:
1)Set keySet = map.keyset(); 进行循环 或者迭代器
2)Collection values = map.values(); 进行循环 或者迭代器
3)使用entryset来循环 entry.getKey entry.getValue 或者迭代器
1、HashMap
jdk1.7的时候,底层是数组+链表,jdk1.8的时候,底层是数组+链表+红黑树
和hashset一样,hashset的底层就是hashmap,具体源码分析看hashset处
hashmap没有实现同步,因此是线程不安全的,没有做同步互斥的操作,没有synchronized
当链表的数据不断减少后,会有一个减枝的操作
2、HashTable
1)存放的元素是键值对
2)hashtable的键和值都不能为null,否则会抛出NullPointerException
3)hashTable使用方法基本上和HashMap一样
4)hashTable是线程安全的,hashMap是 线程不安全的,因为hashtable的方法都加了synchronized关键字
底层有一个数组,Hashtable$Entry 初始化大小为11,这里的entry是HashTable的静态内部类,加载因子也是0.75,初始化的临界值是8,扩容机制是2n+1
int newCapacity = (oldCapacity << 1) + 1;
3、Properties
1)Properties 继承 HashTable
2)通过kv存放数据,kv都不能为null
3)主要用作配置文件,通过io来获取配置文件中的内容
4、TreeMap
TreeSet的底层就是TreeMap,看TreeSet的源码分析即可。主要是定义比较的规则。
树结构
由于HashMap的底层是数组+链表+红黑树的结构,所以顺便整理一下关于红黑树的内容。
二叉查找树
特点:
1)左子树上所有的节点的值均小于或等于根节点的值
2)右子树上所有的节点的值均大于或等于根节点的值
3)左右子树也一定分别为二叉排序树
网上找了一个典型的二叉查找树模型:
这种结构的好处,在查找节点的时候速度较快,比方说查找10,路径就是 9 --> 13 --> 11 --> 10,查找所需的最大次数等同于二叉查找树的高度。插入的时候也是,按照这种思想,一层一层的找,直到找到合适的位置进行插入。但是这种树结构有一个比较大的问题。
初始化的二叉查找树只有三个节点:
依次插入7 6 5 4 3 ,结果如图:
这种结构左腿太长,如果要查找3的话,基本等同于线性查找,所以出现了红黑树。
红黑树
红黑树其实就是一种平衡的二叉查找树。
特点:
1)节点是红色或者黑色
2)根节点是黑色
3)每个叶子的节点都是黑色的空节点(null)
4)每个红色节点的两个子节点都是黑色的
5)从任意节点到其每个叶子的所有路径都包含相同的黑色节点
典型的红黑树模型:
这些规则保证了红黑树的平衡,最长路径不超过最短路径的两倍
当插入和删除节点的时候,就会对平衡造成破坏,这个时候需要对树进行调整,从而达成新的平衡。常用的调整是变色和旋转。旋转又分为左旋转和右旋转。
具体看后面链接里的内容,写的非常好
关于树结构的资料来自网络 (侵删)
http://www.360doc.com/content/18/0904/19/25944647_783893127.shtml