1. 集合框架体系
Collection
Map
2. 集合关系图
Collection
Map
3. Collection
1. 方法
2. 遍历
迭代器遍历
public class TestIterator {
public static void main(String[] args) {
Collection<Integer> col = new ArrayList<>();
col.add(1);
col.add(2);
col.add(3);
col.add(4);
//使用迭代器遍历
Iterator iterator = col.iterator();
while(iterator.hasNext()){
//返回下一个元素,类型是Object
System.out.println(iterator.next());
}
//注意,当退出 while 循环后,这时 iterator 迭代器指向最后的元素
//如果希望重新遍历,需要重置迭代器
iterator = col.iterator();
while(iterator.hasNext()){//第二次遍历
//返回下一个元素,类型是Object
System.out.println(iterator.next());
}
}
}
增强for循环
注意增强for循环底层依然是迭代器,增强for循环可以理解为一个简化版的迭代器
public class TestFor {
public static void main(String[] args) {
Collection<Integer> col = new ArrayList<>();
col.add(1);
col.add(2);
col.add(3);
col.add(4);
//使用增强for循环
//底层依然是迭代器
for(Integer i : col){
System.out.println(i);
}
//可以使用在数组
int[] nums = {1,2,3,4,5};
for(int i : nums){
System.out.println(i);
}
}
}
4. List
- List 集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
- List 集合中的每个元素都有其对应的顺序索引,即支持索引。(底层是数组)
1. 常用方法
- void add(int index, Object ele):在index位置插入ele元素
- boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
- Object get(int index):获取指定index位置的元素
- int indexOf(Object obj):返回obj在集合中首次出现的位置
- int lastlndexOf(Object obj):返回obj在当前集合中末次出现的位置Object remove(int index):移除指定index位置的元素,并返回此元素
- Object set(int index,Object ele):设置指定index位置的元素为ele相当于是替换.
- List subList(int fromlndex, int tolndex):返回从fromlndex到tolndex位置的子集合(左闭右开)
2. ArrayList(线程不安全)
注意事项
- ArrayList 可以加入 null,并且多个
- ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高)看源码,在多线程情况下,不建议使用ArrayList,举例如下
//下面是 ArrayList 的添加元素源码,可以看出并没有用synchronize方法修饰
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
扩容机制
- ArrayList中维护了一个Object类型的数组elementData.
- transient Object ] elernentData;//transient表示瞬间,短暂的,表示该属性不会被序列化
- 当创建ArrayList对象时
- 如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
- 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
add()源码分析
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
}
运行过程分析
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//创建一个空数组
}
public boolean add(E e) {
ensureCapacityInternal(size + 1); //确定是否需要扩容
elementData[size++] = e; //赋值
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //看看是否为空数组
//DEFAULT_CAPACITY: 10
//minCapacity:1
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); //确定minCapacity,第一次扩容为10
}
ensureExplicitCapacity(minCapacity);//明确是否扩容
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//记录集合被修改次数
// overflow-conscious code
if (minCapacity - elementData.length > 0) // 如果最小容量-当前数组容量大于零,即elementData 的大小不够
grow(minCapacity);//真正扩容的方法
}
/*
第一次newCapacity =10
第二次及其以后,按照1.5倍扩容
*/
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);
}
有参构造器源码
//创造一个大小为 initialCapacity 的数组
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);
}
}
3. Vector(线程安全)
基本介绍
-
Vector类的定义说明
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
-
Vector底层也是一个对象数组,protected Object[] elementData;
-
Vector是线程同步的,即线程安全, Vector类的操作方法带有synchronized
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); }
-
在开发中,需要线程同步安全时,考虑使用Vector
扩容机制
如果是无参,默认10,满后,按照2倍扩容;如果是指定大小,则每次直接按照2倍扩
//无参构造器
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;
}
//扩容方法
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);
}
4. Vector和ArrayList的比较
底层结构 | 版本 | 线程安全(同步)效率 | 扩容机制 | |
---|---|---|---|---|
ArrayList | 可变数组 | jdk1.2 | 不安全,效率高 | 有参构造器1.5倍;无参构造器:第一次是:10,第二次开始按照1.5倍扩容 |
Vector | 可变数组 | jdk1.0 | 安全,效率不高 | 如果是无参,默认10,满后,按照2倍扩容;如果是指定大小,则每次直接按照2倍扩 |
5. LinkedList(线程不安全)
基本介绍
- LinkedList 底层实现了双向链表和双端队列特点
- 可以添加任意元素(元素可以重复),包括null
- 线程不安全,没有实现同步
底层结构
- LinkedList 底层维护了一个双向链表.
- LinkedList 中维护了两个属性 first 和 last 分别指向首节点和尾节点
- 每个节点(Node对象),里面又维护了 prev、next、item 三个属性,其中通过 prev 指向前一个,通过 next 指向后一个节点。最终实现双向链表.
- 所以 LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高。
源码解读
List<Integer> linkedList = new LinkedList<>();
linkedList.add(1);
linkedList.remove();//默认删除第一个元素
System.out.println(linkedList);
-
LinkedList linkedList = new LinkedList();
public LinkedList(){}
-
这时linkeList 的属性 first = null last = null
-
执行添加
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++;//集合修改次数 }
-
执行删除
//执行removeFirst public E remove() { return removeFirst(; } //removeFirst public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); } //unlinkFirst private E unlinkFirst(Node<E> f) { // assert f == first && f != null; final E element = f.item; final Node<E> next = f.next; f.item = null; f.next = null; // help GC first = next; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
5. Set
1. 基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 注意:去除顺序和添加顺序虽然不一致,但是==set集合的去除顺序是固定的==
- 不允许重复元素,所以最多包含一个null
- JDK API中Set接口的实现类有
-
方法列举
-
遍历方式
- 迭代器
- 增强for循环
- 不能用索引的方式遍历
2. HashSet
基本介绍
-
HashSet 实际上是 HashMap,HashMap底层实际上是**(数组+链表+红黑树)**
public HashSet() { map = new HashMap<>(); }
-
可以存放null值,但是只能有一个null。
-
HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果。
-
不能加入相同元素或数据
public class TestSet { public static void main(String[] args) { //set 不能添加相同数据的理解 Set set = new HashSet(); set.add("lucy"); set.add("lucy"); //下面两个都可加入 set.add(new Dog("Tom")); set.add(new Dog("Tom")); //加深一下 set.add(new String("hsp")); set.add(new String("hsp"));//加入不了 System.out.println(set); } } class Dog { private String name; public Dog(String name) { this.name = name; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + '}'; } }
运行结果:
[Dog{name='Tom'}, hsp, Dog{name='Tom'}, lucy]
扩容机制
- HashSet底层是HashMap
- 添加一个元素时,先得到hash值->会转成->索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用equals比较,(需要遍历链表)如果相同,就放弃添加;如果不相同,则添加到该节点所指向链表的末尾
- 第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75 =12
- 如果 table 数组使用到了临界值12,就会扩容到**16*2 =32**,新的临界值就是 32*0.75 = 24,依次类推
- 在 Java8 中**,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且 table 的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制**
- 当一个数组节点的链表长度达到8,但是数组的长度小于64时,链表每添加一个元素,数组 扩容 2倍,直到数组长度大于64,链表树化,不再扩容数组
- 数组的扩容临界值,来自于加入集合中的元素个数,不管该元素是否各自占有一个数组
源码分析
public class TestSet {
public static void main(String[] args) {
Set set = new HashSet();
set.add("java");
set.add("php");
set.add("java");
System.out.println(set);
}
}
-
执行 无参构造器
public HashSet() { map = new HashMap<>(); }
-
执行 add()
public boolean add(E e) { // e = "java" return map.put(e, PRESENT)==null;// PRESENT:private static final Object PRESENT = new Object(); }
-
执行 put(),执行hash方法
public V put(K key, V value) { // key = "java" value = PRESENT 共享 return putVal(hash(key), key, value, false, true); }
-
hash 的计算:得到 hash 值,并不是hashCode
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
执行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; //定义辅助变量 //tab:HashMap 的一个数组,类型为 Node[] :transient Node<K,V>[] table; // if语句表示如果当前table是null,或者大小=0就是第一次扩容,到16个空间。 if ((tab = table) == null || (n = tab.length) == 0) // resize():将 tab 扩容成长度为16的node数组, // 注意: //newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 当数组已用空间为16 * 0.75时扩容,而不是等数组空间用完 n = (tab = resize()).length; /* 1. 根据传入的key得到的 hash 去计算该 key应该存放到 table 表的哪个索引位置,并把这个位置的对象,赋给p 2. 判断 p 是否为null (1)如果p 为null,表示还没有存放元素,就创建一个Node (key="java" , value=PRESENT) 就放在该位置 tab[i] = newNode(hash,key, value,null) (2) */ 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 && //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样 ((k = p.key) == key// 并且满足准备加入的 key 和 p 指向的 Node 节点的 key 是同一个对象 || (key != null && key.equals(k)))) // p 指向的 Node 结点的 key 的 equals() 和准备加入的 key比较后相同就不能加入 e = p; //再判断 p 是不是一个红黑树 //如果是一颗红黑树,就调用 putTreeVal,来进行添加 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 如果当前table对应的索引位置,已经是一个链表,就使用 for 循环比较 /* 1. 依次和该链表的每一个元素比较后,都不相同 注意在把元素添加到链表后,立即判断该链表是否已经达到8个结点,就调用 treeifyBin(); 对当前这个链表进行树化(转成红黑树) 注意,在转成红黑树时,要进行判断,判断条件 if (tab == null ll (n = tab.length)< MIN_TREEIFY_CAPACITY(64)) resize(); 如果上面条件成立,先table扩容.只有上面条件不成立时,才进行转成红黑树 2. 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break */ for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); /* 注意在把元素添加到链表后,立即判断该链表是否已经达到8个结点,就调用 treeifyBin(); 对当前这个链表进行树化(转成红黑树) 注意,在转成红黑树时,要进行判断,判断条件 if (tab == null ll (n = tab.length)< MIN_TREEIFY_CAPACITY(64)) resize(); 如果上面条件成立,先table扩容.只有上面条件不成立时,才进行转成红黑树 */ 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 (p.hash == hash && //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样 ((k = p.key) == key// 并且满足准备加入的 key 和 p 指向的 Node 节点的 key 是同一个对象 || (key != null && key.equals(k)))) // p 指向的 Node 结点的 key 的 equals() 和准备加入的 key比较后相同就不能加入 e = p; 由于上述语句,再第三次添加时, 添加的是 java 与第一次重复,所以返回值不为空,再结合上述 return map.put(e, PRESENT)==null; 不为空则返回失败 */ if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //如果集合元素大于 16 * 0.75 则扩容 if (++size > threshold) resize(); //空方法,留给 HashMap 子类去实现 afterNodeInsertion(evict); return null; }
3. LinkedHashSet
基本介绍
-
LinkedHashSet是 HashSet的子类
-
LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个**数组+双向链表**
-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
-
LinkedHashSet 不允许添重复元素
-
说明
-
在LinkedHastSet中维护了一个hash表和双向链表:(LinkedHashSet有head和tail )
-
每一个节点有 pre 和 next 属性,这样可以形成双向链表
-
在添加一个元素时,先求hash值,在求索引,确定该元素在 hashtable 的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加[原则和hashset一样])
tail.next = newElement//简单指定 newElement.pre = tail tail = newEelment;
-
这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致
-
添加第一次时,直接将数组table 扩容到16,存放的结点类型是 LinkedHashHap$Entry
-
数组是 HashMap N o d e [ ] 存放的元素 / 数据是 L i n k e d H a s h M a p Node[]存放的元素/数据是 LinkedHashMap Node[]存放的元素/数据是LinkedHashMapEntry类型,HashSet则存放的是HashMap$Node
-
//继承关系在内部类中完成 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); } }
-
-
4. TreeSet
-
无参构造器创建TreeSet时,仍然是无序的
-
使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类)并指定排序规则
- 构造器把传入的比较器对象,赋给了TreeSet的底层的 TreeMap的属性this.comparator
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
- 在调用treeSet.add( “tom”),在底层会执行到
if (cpr != null) {//cpr就是我们的匿名内部类(对象) do { parent = t; //动态绑定到我们的匿名内部类(对象)comparecmpI=cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else //如果相等,即返回0,这个Key就没有加入,表示TreeSet不能加入同一个元素 return t.setValue(value); }while (t != null); }
6. Map
1. 基本介绍
- Map中的 key 和 value 可以是任何引用类型的数据,会封装到HashMad$Node对象中
- Map中的key不允许重复,原因和 HashSet 一样,前面分析过源码.Map 中的 value 可以重复
- Map 的key可以为null, value也可以为null,注意 key 为 null, 只能有一个,value 为 null ,可以多个.
- key 和 value之间存在单向一对一关系,即通过指定的key 总能找到对应的 value
- 当有相同的 key 时,等同于替换
- k-v最后是 HashMap$Node node = newNode(hash,key,value,null)
- 理解 EntrySet
- k-v为了方便程序员的遍历,还会创建EntrySet 集合,该集合存放的元素的类型 Entry,而一个Entry对象就有k, v EntrySet<Entry<K, V>>,即:transient set<Hap.Entry<K, V>> entrySet;
- entrySet中,定义的类型是 Map.Entry ,但是实际上存放的还是 HashMap$Node这时因为 static class Node<K,V> implements Map.Entry<K, V>
2. 常用方法
3. 遍历方法
/*
containsKey:查找键是否存在
keySet:获取所有的键
entrySet:获取所有关系
k-vvalues:获取所有的值
*/
Map<String,Integer> map = new HashMap<>();
map.put("101",1);
map.put("102",2);
map.put("103",3);
map.put("104",4);
map.put(null,1);
map.put("106",1);
//第一种:keySet(简洁)
Set<String> sets = map.keySet();
for(String key : sets){
System.out.println(key + "-" + map.get(key));
}
//第二种:EntrySet
Set entries = map.entrySet();
for(Object entry : entries){
//将 entries 转换为 EntrySet
Map.Entry m = (Map.Entry)entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
4. HashMap(线程不安全)
- (k,v)是一个HashMap$Node实现了Map.Entry<K,V>
- jdk7.0的hashmap底层实现==[数组+链表],jdk8.0底层[数组+链表+红黑树]==
扩容机制(与HashSet完全一样)
- HashMap 底层维护了 Node 类型的数组 table,默认为 null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75.
- 当添加 key-val 时,通过 key 的哈希值得到在 table 的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的 key 是否和准备加入的 key 相等,如果相等,则直接替换 val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
- 第1次添加,则需要扩容 table 容量为16,临界值(threshold)为**12**.
- 以后再扩容,则需要扩容 table 容量为原来的2倍,临界值为原来的2倍,即 24,依次类推。在 Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且table的大小>= MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)、
5. HashTable(线程安全)
基本介绍
- 存放的元素是键值对:即 K-V
- hashtable的键和值都不能为null,否则会抛出NullPointerException,HashTable使用方法基本上和HashMap一样
- hashTable是线程安全的,hashMap是线程不安全的
- 底层有数组:Hashtable$Entry[] 初始化大小为11
- 临界值threshold 8 =11 *0.75
扩容机制
- 当if (count >= threshold)满足时,就进行扩容,按照int newCapacity = (oldCapacity <<1) +1;的大小扩容.
- 大于等于临界值时,将数组扩到原来的两倍加一
Properties
- Properties 类**继承自 Hashtable 类并且实现了Map 接口**,也是使用一种键值对的形式来保存数据。
- 它的使用特点和 Hashtable 类似
- 键和值都不能为null
- Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
- 说明:工作后xoxx.properties文件通常作为配置文件
6. TreeMap
7. Collections工具类
- Collections是一个操作Set、List和 Map等集合的工具类
- Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
- 方法
- reverse(List):反转List中元素的顺序
- shuffle(List):对List集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定List集合元素按升序排序
- sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
- Object min(Collection)
- Object min(Collection,Comparator)
- int frequency(Collection,Object):返回指定集合中指定元素的出现次数
- void copy(List dest,List src):将src中的内容复制到dest中
- boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值