文章目录
第14章 集合
1.框架体系 ( 背下来 ! )
- 难点:
- 原理
- 实现方法
- 什么时候用?
- 单列集合——Collection
- List
- ArrayList
- LinkedList
- Vector
- Set
- HashSet
- TreeSet
- List
- 双列集合——Map
- HashMap
- LinkedHashMap
- TreeMap
- Hashtable
- Properties
- HashMap
- 单列:一个个、单个单个的对象,双列:键—值对
- 这些集合存储的都是对象的引用,而不是对象的副本,因此修改原始对象,集合里的对象内容也会被修改!
2.Collection接口
常用方法(Collection不能直接被实例化)
- add
- remove (可以按索引,也可以按对象)
- contains
- size
- isEmpty
- clear
- addAll
- containsAll
- removeAll
- 注:最后三个方法传入实现Collection接口的对象
遍历方式1:iterator 迭代器
-
得到iterator:调用Collection实例化对象的iterator方法
Iterator iterator = col.iterator()
-
iterator方法
- next
- hasNext**(用next前必须用hasNext检测!否则会抛出异常)**
- 快捷指令
itit
- 显示所有的快捷指令
Ctrl + J
- 再次遍历,需要重置迭代器
遍历方式2:增强for循环
- 增强for等于简化版的迭代器iterator
- 只能用于数组和Collection集合
- 底层还是迭代器
- 快捷指令
I
3.List 接口(List特有,Set没有——细化)
- 特点
- 元素有序
- 可以重复
- 支持索引
- 常用方法
- add ( int index , Object ele )
- addAll ( int index, Collection eles )
- get**(注:不能使用 [ ] 随机访问!)**
- indexOf
- lastIndexOf
- remove( int index )
- set ( 替换,索引必须存在 )
- subList
- 遍历方式
- iterator
- 增强for
- 普通for
4.ArrayList 类
-
注意事项
- 可以放所有类型的元素,包括NULL
- 底层是用数组实现的
- 线程不安全(执行效率高),多线程时不适用
-
扩容机制
-
ArratList维护了一个Object类型的数组 -> ArrayList可以存放所有类型数据
transient Object[] elementData;
transient 瞬间,短暂的;表示该属性不会被序列化
-
创建ArrayList用无参构造器 -> 初始容量为零 -> 第一次扩容容量为10 -> 之后每次扩容是原来的1.5倍
-
有参构造器 -> 初始容量为设定值 -> 之后每次扩容是原来的1.5倍
-
扩容过程**(看源码!!!)**
-
创建element数组
transient Object[] elementData; // non-private to simplify nested class access
-
赋默认初值为零
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
-
加入数据的add方法,modCount是操作数组的次数
public boolean add(E e) { modCount++; add(e, elementData, size); return true; }
-
另一个add方法,如果size大小和length长度相等,则用grow方法进行扩容,否则正常存入数据
private void add(E e, Object[] elementData, int s) { if (s == elementData.length) elementData = grow(); elementData[s] = e; size = s + 1; }
private Object[] grow() { return grow(size + 1); }
-
oldCapacity是原来的长度
传入的参数是size + 1,若使用无参构造器,第一次size就是0,这里的minCapacity就是1
如果是之后的拓展,minCapacity就是现在的容量+1,minCapacity就是“至少需要的容量”的意思
如果原来的长度不是零,即不是默认初值时,进入if语句
newCapacity接收新的容量大小,用copyOf复制原来的数组,以免丢失数据
如果原来的长度是0,则返回一个新建的Object数组,这个数组就是elementData
private Object[] grow(int minCapacity) { int oldCapacity = elementData.length; if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, /* minimum growth */ oldCapacity >> 1 /* preferred growth */); return elementData = Arrays.copyOf(elementData, newCapacity); } else { return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)]; } }
-
接下来看if语句块里的newLength方法
这里传入了三个参数:oldLength是旧的长度,传入了oldCapacity,对应;minGrowth是最少增加的长度,传入了minCapacity - oldCapacity;prefGrowth是想要增加的长度,传入了oldCapacity >> 1,这里是位运算,向右移动一位,等于原来大小的二分之一,这就是之后每次增加的大小是原来的1.5倍的原因
prefLength 计算了最终增加的长度
SOFT_MAX_ARRAY_LENGTH 是在不溢出前提下的最大值
如果可以满足想要的prefLength长度,那么新的长度就是prefLength;否则进入处理方法hugeLength
public static int newLength(int oldLength, int minGrowth, int prefGrowth) { // preconditions not checked because of inlining // assert oldLength >= 0 // assert minGrowth > 0 int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { return prefLength; } else { // put code cold in a separate method return hugeLength(oldLength, minGrowth); } }
-
hugeLength代码如下,如果增加的长度为负,抛出异常。如果现在总长度加上最小增加的场长度正好达到最大值,则返回最大值。否则只能增长minGrowth,即需要的最小长度
private static int hugeLength(int oldLength, int minGrowth) { int minLength = oldLength + minGrowth; if (minLength < 0) { // overflow throw new OutOfMemoryError( "Required array length " + oldLength + " + " + minGrowth + " is too large"); } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) { return SOFT_MAX_ARRAY_LENGTH; } else { return minLength; } }
-
有参构造时,建立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); } }
只有创建的过程不一样,接下来添加数据的过程与无参构造是一样的
-
-
注意:IDEA 在默认情况下,Debug显示的数据是简化的
-
5.Vector 类
- 注意事项
- Vector底层也是用Object 对象数组实现的
- Vector 是线程同步的(效率不高),线程安全(方法基本都有 synchronized 关键字),在开发中需要线程同步安全的考虑用Vector
- 扩容机制
- 无参,默认大小是10,之后每次按2倍来扩容
- 有参,大小是传入的参数,之后每次按2倍来扩容
- (追源码的过程和ArrayList相似,不再写出)
6.LinkedList 类
- LinkedList 底层维护了一个双向链表
- 维护了两个属性 first 和 last,分别指向头节点和尾节点
- 有内部类Node,里面又有prev next item三个属性
- 注:remove方法默认删除的是第一个元素
- LinkedList 使用了 List 接口,所以可以使用迭代器和增强 for 循环遍历
7.ArrayList 和 LinkedList 的选择
- 如果改查的操作比较多,用 ArrayList
- 如果增删操作较多,用 LinkedList
- 在实际开发中,八九成的时间都是查询,因此大部分时间用ArrayList
- 项目中可能一个模块用ArrayList , 另一个模块用LinkedList
8.Set 接口
-
注意事项
-
不允许重复元素或对象,因此最多一个 NULL。添加重复元素时,加入失败,add方法返回false
set.add("lucy"); //可以添加 set.add("lucy"); //都是常量池中的数据,相同,不能添加 set.add(new Dog()); set.add(new Dog()); //虽然都是狗对象,但却是不同的对象,因此两个都可以加入 set.add(new String("hsp")); set.add(new String("hsp")); //加入不了-> 看源码做分析
-
存放元素无序,添加和取出元素的顺序并不一样,但是取出的顺序是固定的,不会改变
-
Set 接口对象 实际上指 “Set接口的实现类的对象”
-
-
**常用方法和遍历方式和Collection接口一样,但注意不能用随机访问的方式遍历(没有get方法)-> 不能用普通for循环遍历 **
9.HashSet 类
-
底层是HashMap -> HashMap底层是数组、链表、红黑树
-
add底层源码,不能加入重复元素 / 对象的真正含义
-
底层 HashMap 用的是拉链法(存储效率高)
-
添加步骤
- 添加一个元素,先得到 hash 值,再转化为索引值
- 在 table 表中查找该索引值,看该位置是否有元素。如果没有,就直接添加。
- 如果有元素,就用equals方法(可以重写)进行比较,相同就放弃添加,不同就放到该索引值对应的链表的最后面
- table 创建时初始值是16,到临界值(threshold)12时,就扩容到 16*2=32,新的临界值是32 * 0.75(临界因子) = 24,依此类推**(注:threshold临界值计算时也包括链表里面的那些节点,加了一个节点,size就加一)**
- 在Java8中:一条链表的元素个数到达
TREEIFY_THRESHOLD
(默认是8),并且 table 大小 >=MIN_TREEIFY_CAPACITY
(默认是64),就会进行树化(红黑树)。如果table大小没达到要求,就将table大小翻倍
-
add过程分析
按以下代码添加:
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java"); //第一次添加
hashSet.add("php"); //第二次添加
hashSet.add("java"); //第三次添加
}
- 第一次添加分析
//add方法
//里面为map的添加方法,PRESENT为一个参数,先不用管它
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//如果返回为null,则返回true,即添加成功,否则返回false,即添加失败
//put方法
//hash(key)能求出传入添加元素的索引值,key为要添加的值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash方法,求出索引值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//如果值为空,则返回0
//否则,求出索引值的算法:(h = key.hashCode()) ^ (h >>> 16)
// h >>> 16 表示h逻辑右移16位,将key的hashCode(哈希值)与其按位异或,就得到了索引值
//putVal方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//创建一个tab来保存哈希表,table就是系统已经创建好的哈希表
Node<K,V>[] tab; Node<K,V> p; int n, i;//辅助变量
if ((tab = table) == null || (n = tab.length) == 0)
//第一次创建时哈希表为空,进入if语句里
//if语句表示如果哈希表为空,就进行第一次扩容,到16个空间
n = (tab = resize()).length;//resize方法在下一个代码块里
//返回了一个新建的哈希表,被tab接收
//计算对应的索引,并把这个位置的对象给辅助变量p
if ((p = tab[i = (n - 1) & hash]) == null)
//如果这个位置为空,就添加节点
tab[i] = newNode(hash, key, value, null);
else {
//如果不为空,存到后面的链表里面
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果大小超过临界值(目前为12),就用resize进行扩容
if (++size > threshold)
resize();
//HashMap留给子类实现的方法,比如设置各种特殊链表,这里是一个空方法
afterNodeInsertion(evict);
//创建成功的话,就返回null
return null;
}
//resize 方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//oldCap为table现在的长度,第一次添加数据时为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr为临界值,目前为0
int oldThr = threshold;
int newCap, newThr = 0;
//经过判断,这里进入了最后的else代码块里
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
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
//开辟了默认的空间,默认为16位,定义如下(1左移4位,翻了16倍):
// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
newCap = DEFAULT_INITIAL_CAPACITY;
//提前扩容,这里DEFAULT_LOAD_FACTOR为加载因子0.75,当容量到达newThr = 0.75 * 16 = 12时就扩容,防止突然涌入大量数据导致溢出
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"})
//table 即哈希表的创建在这里
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;
}
-
第二次添加分析,直接到putVal方法:
//putVal方法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //已经创建了table,所以这里跳过 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //算得的hash值不重复,进入if语句块,创建新节点 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //创建完了跳过接下来的部分,直接到++modCount,最后返回null,创建成功 else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } 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; }
-
第三次添加分析,也是直接进入putVal方法
//putVal方法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //跳过 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //计算得hash值相同,进入else语句块 else { //开发小技巧:用到什么变量再在这个语句块里创建,不要一开始就在开头创建一大堆变量,这样方便查询,也比较好看 Node<K,V> e; K k; //判断内容是否一样,这里内容一样,都是“Java”,故进入if语句块 //首先判断hash值是否相同,相同并且: //1.新加入的节点和原来节点是同一个对象 //2.或者用equals方法(可重写,不能简单地认为就是内容相等)判断相等 //两个条件成立一个,就判断相同,进入if语句块 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //新创建的节点 e 等于 节点 p e = p; //如果链表的头节点(哈希表)里的节点已经是红黑树,则用红黑树的判断方法,此处略过不讲 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //最后一种情况为判断链表各个节点 else { //for循环为死循环,只有在找到重复节点或者建立新的节点后才会用break退出 for (int binCount = 0; ; ++binCount) { //链表的节点已经遍历完,没有重复的节点,则在链表末尾创建新节点存储数据 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //判断是否超过了创建红黑树的临界值,这里TREEIFY_THRESHOLD为8, //如果超过了,就用treeifyBin方法建立红黑树 //但是要满足table大小大于MIN_TREEIFY_CAPACITY(默认是64),这个判断在treeifyBin方法里,如果没有到,则用resize方法扩容 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //判断的方式和判断链表第一个节点是一样的 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //换到下一个节点 p = e; } } // e有内容,进入if语句块 if (e != null) { // existing mapping for key //取出该位置的值 V oldValue = e.value; //onlyIfAbsent为传入putVal的参数,为false,进入 if (!onlyIfAbsent|| oldValue == null) e.value = value; afterNodeAccess(e); //返回不为null,创建节点失败 return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
10.LinkedHashSet 类
- 是HashSet的子类
- 底层是数组+双向链表的方式维护(HashSet是单项链表),所以支持顺序取出数据
- pre指针(这里称为before)指向前一个存入的元素,next指针(这里称为after)指向后一个存入的元素(无论每个双向链表的第一个元素在table的什么位置,指针都可以跨链表指向)
- 不可以存重复元素
- 是否添加新元素的比较和HashSet一样
- LinkedHashSet 有 head 和 tail 属性
- 源码分析和扩容机制和HashSet类似,这里不再详细分析
- table表的数据类型是
HashSet$Node
,但存入的元素类型是HashSet#Entry
-> 多态数组,HashSet#Entry
是HashSet$Node
的子类
11.Map 接口
-
HashSet 实际上也用了 HashMap , 但其 value 值是系统自定的PRESENT,而Map里value值是自定的(Map 保存具有映射关系的数据 key - value,双列元素)
-
key 和 value 可以是任意数据类型,封装到
HashMap$Node
对象中 -
key不可以重复(原因同HashSet),有相同的key的元素加入时,等价于替换
-
value 可以重复(key需不同)
-
key 可以为 null,但只能有一个(不能重复),但value可以有多个null
-
常用 String 类对象作 Map 的 key(但只要是Object的子类都可以作key)
-
key 和 value 存在单向对应关系
-
k-v 最后存放的数据类型为
HashMap$Node node = newNode(hash, key, value, null)
[在putVal方法中]k-v 为了方便程序员的遍历,还会创建三个数组
key 存放在数据类型为 Set 的对象数组 keySet 中,value 存放在数据类型为 Collection 的对象数组 values 中,HashMap$Node 存放在数据类型为 Set 的对象数组 Entry 中
对于EntrySet:在 HashMap N o d e 中存放的含 k e y 和 v a l u e 的元素会先被转化为 M a p . E n t r y ( ∗ ∗ 做了向上转型的处理 ∗ ∗ ,所以有些人也说一个 k − v 就是一个 E n t r y ,实际上不够准确, ‘ H a s h M a p Node 中存放的含 key 和 value 的元素会先被转化为 Map.Entry (**做了向上转型的处理**,所以有些人也说一个k-v就是一个Entry,实际上不够准确,`HashMap Node中存放的含key和value的元素会先被转化为Map.Entry(∗∗做了向上转型的处理∗∗,所以有些人也说一个k−v就是一个Entry,实际上不够准确,‘HashMapNode` 实现了Map.Entry接口),再放进EntrySet中
而一个Entry对象就有k,v,EntrySet<Entry<K,V>> , 即 transient Set<Map.Entry<K,V>> entrySet
EntrySet 中定义的类型是 Map.Entry, 实际上存放的是 HashMap$Node,这是因为
static class Node<K,V> implements Map.Entry<K,V>
把 HashMap$Node 对象放到 EntrySet 的目的:方便遍历,因为Map.Entry 提供了重要方法,可以分别获取key 和 value
EntrySet 里存放的 Entry 对象只是一个指向,指向 table 中的
HashMap$Node
,实际上数据还是存放在HashMap$Node
中EntrySet 里有 getValue 、getKey、setValue、setKey 方法
常用方法
- put ( key , value)
- get(key)(返回对应的value)
- remove
- size
- isEmpty
- clear
- containsKey
遍历方式(基于上面讲的三个数组)
-
keySet方法(Set 的遍历方法)
-
先取出 key ,再取出 key 对应的 value
//使用keySet,返回的是一个Set数组 Set set = hashMap.keySet(); for (Object key : set) { //注意key取出来是Object的形式,但可以直接输出,不用向下转型成String的形式 //但如果取出来是一个自定义的对象,要使用对象里面的属性或者方法,就需要向下转型 //底层:当使用‘+’连接时,Java会自动调用对象的toString()方法将其转换成字符串。对于类型为Object的变量,如果它实际上是String类型,toString()方法会直接返回字符串本身 System.out.println( key + "-" + hashMap.get(key)); }
-
迭代器
Set set = hashMap.keySet(); Iterator iterator = set.iterator(); while (iterator.hasNext()) { //iterator.next()是set中的key元素,用Object类来接收 Object next = iterator.next(); System.out.println(next + "-" + hashMap.get(next)); }
-
-
values方法(Collection 的遍历方法)
-
增强for循环
Collection values = hashMap.values(); for (Object value : values) { System.out.println(value); }
-
iterator迭代器
Collection values = hashMap.values(); Iterator iterator = values.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next); }
-
-
通过entrySet方法获取
-
增强for(转换成Map.Entry类型,在用getKey和getValue方法)
Set set = hashMap.entrySet(); for (Object o : set) { //注意要转换成Map.Entry类型,才能使用特有的方法getKey()、getValue()等 //如果直接输出o,结果是 “key=value” 的形式 Map.Entry entry = (Map.Entry) o; System.out.println(entry.getKey() + "-" + entry.getValue()); }
-
迭代器
Set set = hashMap.entrySet(); Iterator iterator = set.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); //如果直接输出next,结果是 “key=value” 的形式 Map.Entry entry = (Map.Entry) next; System.out.println(entry.getKey() + "-" + entry.getValue()); }
-
- 注:如果 value (不是values) 为一个自定义对象,那么遍历时需要先把 Object 类型的对象向下转型为这个自定义的对象类型,这样才能获取这个对象的属性和方法,进行遍历
12.HashMap 类
- 是Map接口使用频率最高的实现类
- 和HashSet一样不保证映射的顺序,因为底层是以HashSet的方式存储的
- 没有同步机制,线程不安全
- table 表是一个数组,数组里面的链表元素(k,v) 是一个Node, 实现了Map.Entry<K,V>接口
- 扩容机制:和HashSet完全一样,只不用HashMap是 key-value 的形式
- 链表树化后若删除节点,节点少到一定阶段时,会恢复成链表,称为剪枝
13.Hashtable 类
- key 和 value 都不能为null,否则会抛出控制真异常
- 使用方法基本和HashMap一样
- 是线程安全的,但效率较低
- 底层有数组 Hashtable$Entry [ ] , 初始化大小为11
- threshold 为 8 (11*0.75)
- 扩容:不是按两倍来扩容,而是原来的容量乘以2再加1
14.Properties 类
- 是 Hashtable 的子类(key 和 value 不能为空)
- 使用和 Hashtable 类似
- 一般是读取外部配置文件(格式为xxx.properties)时用,加载数据到 Properties 类对象,再用这个类中的方法操作文件
- 工作时,properties文件是常见的配置文件(具体见IO流的内容)
- 常用方法
15.TreeSet & TreeMap 类
- 底层是 TreeMap
- 最大特点:可以排序
- 当使用无参构造器是,用的是默认按字母顺序排序
- 使用 TreeSet 提供的构造器,可以传入一个比较器 Comparator 参数(接口,匿名内部类形式使用),使用方法compare
- 注意:当compare比较时,若比较结果相同,则无法将新的元素添加进去
- 第一次添加元素时compare是自己和自己比较,也不返回内容,目的是检测传进来的是不是空值
- 若没有传入Comparable,则以添加的对象实现的Comparable接口的compareTo去重**(若自定义类型没有实现Comparable接口的compareTo方法,则会报出异常)**
- 与HashSet的去重机制不同:HashSet用hashCode() + equals() 去重
- TreeMap 类操作基本和 TreeSet 相同,只是 TreeMap 存储的是唯一元素,TreeMap 存储的是键值对
16.开发中如何选择集合实现类
- 先看存储的数据是一组对象(单列)还是键值对(双列)
- 一组对象:Collection 接口
- 允许重复:List 接口
- 增删多:LinkedList (底层维护了一个双向链表)
- 改查多:
- 线程不安全:ArrayList (底层维护 Object 类型的可变数组)
- 线程安全:Vector
- 不允许重复:Set 接口
- 无序:HashSet (底层是 HashMap , 维护了一个哈希表,即数组+链表+红黑树)
- 排序:TreeSet
- 插入和取出顺序一致:LinkedHashSet (底层维护数组+双向链表)
- 允许重复:List 接口
- 键值对:Map 接口
- 键无序:
- 线程不安全:HashMap (底层是:哈希表 jdk7:数组+链表 jdk8:数组+链表+红黑树)
- 线程安全:Hashtable
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件:Properties
- 键无序:
17.Collection 工具类
- 功能:提供一系列静态方法实现对 Set、Map、List 的操作
- 常用方法
- reverse
- shuffle
- sort ( List )
- sort( List, Comparator)
- swap( List, int, int)
- Object max (Collection)
- Object max (Collection, Comparator)
- Object min (Collection)
- Object min (Collection, Comparator)
- frequency
- copy
- replaceAll (注意需要先给目的集合 dest 赋任意值,否则会抛出异常)