12.2.1.1.2 ArrayList 的底层操作机制源码分析
12.2.1.2.4 ArrayList 和 LinkedList 的比较
第十二章 集合
12.1 集合框架体系
-
可以动态保存任意多个对象,使用比较方便
-
提供了一系列方便的操作对象的方法:add、remove、set、get等
-
使用集合添加,删除新元素
12.2 Collection
-
Collection 接口实现类的特点 public interface Collection<E> extends Iterable<E>
-
Collection 实现子类可以存放多个元素,每个元素可以是Object
-
有些Collection 的实现类,可以存放重复的元素,有些不可以
-
有些Collection 的实现类,有些是有序的(List),有些不是有序(Set)
-
Collection 接口没有直接的实现子类,是通过它的子接口Set 和 List 来实现的
-
-
Collection 接口和常用方法 -> 由于接口不能实例化,因此用实现了该接口的子类ArrayList对象来演示
-
add:添加单个元素
-
remove:删除指定元素
-
contains:查找元素是否存在
-
size:获取元素个数
-
isEmpty:判断是否为空
-
clear:清空
-
addAll:添加多个元素
-
removeAll:删除多个元素
-
containsAll:查找多个元素是否都存在
-
-
Collection 接口遍历元素方式1 -> 使用Iterable(迭代器)
-
基本介绍
-
Iterable对象称为迭代器,主要用于遍历 Collection 集合中的元素
-
所有实现了 Collection 接口的集合类都有一个iterable()方法,用以返回一个实现了Iterable 接口的对象,既可以返回一个迭代器
-
Iterable 的结构图
-
Iterable 仅用于遍历集合, Iterable 本身并不存放对象
-
-
-
Collection col = new ArrayList(); col.add(new Book("三国演义", "罗贯中", 10.1)); col.add(new Book("小李飞刀", "古龙", 7.2)); col.add(new Book("红楼梦", "曹雪芹", 37.5)); //System.out.println("col=" + col); //现在希望能够遍历 col 集合 //1. 先得到 col 对应的 迭代器 Iterator iterator = col.iterator(); //2. 使用 while 循环 遍历 while (iterator.hasNext()) {//判断是否还有数据 //返回下一个元素,类型是 Object Object obj = iterator.next(); System.out.println("obj=" + obj); } while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj); } //3. 当迭代器退出 while 循环后 , 这时 iterator迭代器,指向最后的元素 //iterator.next();//NoSuchElementException //4. 如果希望再次遍历,需要重置我们的迭代器 iterator = col.iterator();//重置迭代器 System.out.println("====第二次遍历===="); while (iterator.hasNext()) { Object obj = iterator.next(); System.out.println("obj=" + obj);
-
Collection 接口遍历元素方式2 -> for循环增强
-
基本介绍
-
增强for循环,可以代替iterator 迭代器,特点:增强 for 就是简化版的 iterator,本质一样。只能用于遍历集合或数组
-
-
基本语法
for ( 元素类型 元素名:集合名或数组名 ) {
访问元素
}
-
-
//1. 使用增强for,在Collection 集合 //2. 增强for, 底层仍然是迭代器 //3. 增强for 可以理解成 就是简化版本的 迭代器遍历I System.out.println("====第三次遍历===="); for (Object obj : col) { System.out.println("obj=" + obj); }
12.2.1 List
-
List 接口基本介绍 -> List 接口是 Collection 接口的子接口
-
List 集合类中元素有序(即 添加顺序和取出顺序一致)、且可重复
-
List 集合中的每个元素都有其对应的顺序索引,即 支持索引
-
List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
-
JDK API 中 List 接口 的实现类有 :ArrayList、LinkedList、Vector
-
-
List 接口的常用方法
-
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 lastIndexOf(Object obj):返回 obj 在集合中最后一次出现的位置
-
Object remove(int index):移除指定 index 位置的元素,并返回此元素
-
Object set(int index,Object ele):设置指定 index 位置的元素位 ele,相当于是替换
-
List subList(int fromIndex,int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
-
-
List list = new ArrayList(); list.add("张三丰"); list.add("贾宝玉"); //void add(int index,Object ele):在index 位置插入ele元素 list.add(1, "Andy"); System.out.println("list=" + list); //boolean addAll(int index,Collection eles):从index 位置开始将eles 中的所有元素添加进来 List list2 = new ArrayList(); list2.add("Wink"); list2.add("zhang"); list.addAll(1, list2); System.out.println("list=" + list); //Object get(int index):获取指定index 位置的元素 //int indexOf(Object obj):返回 obj 在集合中首次出现的位置 System.out.println(list.indexOf("Andy")); //int lastIndexOf(Object obj):返回 obj 在集合中最后一次出现的位置 System.out.println(list.lastIndexOf("Andy")); //Object remove(int index):移除指定 index 位置的元素,并返回此元素 list.remove(3); System.out.println("list=" + list); //Object set(int index,Object ele):设置指定 index 位置的元素为 ele,相当于是 替换 list.set(0, "张无忌"); System.out.println("list=" + list); //List subList(int fromIndex,int toIndex):返回从 fromIndex 到 toIndex 位置的子集合 //注意 返回的子集合 fromIndex <= subList < toIndex System.out.println(list.subList(0, 2));//返回索引为0,1的元素
-
集合中按照价格交换元素顺序
-
int listSize = list.size(); for (int i = 0; i < listSize - 1; i++) { for (int j = 0; j < listSize - i - 1; j++) { //取出对象Book 向下转型---- Book book1 = (Book) list.get(j); Book book2 = (Book) list.get(j + 1); if (book1.getPrice() > book2.getPrice()) { list.set(j, book2); list.set(j + 1, book1); } } }
12.2.1.1 *ArrayList底层结构
12.2.1.1.1 ArrayList 的注意事项
-
permits all elements, including null, ArrayList 可以加入null,并且多个
-
ArrayList 是由数组来实现数据存储的
-
ArrayList 基本等同于Vector,除了 ArrayList 是线程不安全(执行效率高)看源码,在多线程情况下,不建议使用 ArrayList
12.2.1.1.2 ArrayList 的底层操作机制源码分析
-
ArrayList 中维护了一个Object类型的数组elementData。 transient Object[] elementData;//transient 表示瞬间,短暂的;表示该属性不会被序列化
-
当创建ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0 ,第1次添加,则扩容 elementData 为10,如需要再次扩容,则扩容elementData 为 1.5 倍
-
如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍
12.2.1.2 *LinkedList底层结构
12.2.1.2.1 LinkedList 的全面说明
-
LinkedList 底层实现了双向链表和双端队列特点
-
可以添加任意元素(元素可以重复),包括null
-
线程不安全,没有实现同步
12.2.1.2.2 LinkedList 的底层操作机制
-
LinkedList 底层维护了一个双向链表
-
LinkedList 中维护了两个属性first 和 last 分别指向 首节点和尾节点
-
每个节点 (Node对象),里面又维护了prev、next、item三个属性,其中通过prev 指向前一个,通过next 指向后一个节点。最终实现双向链表
-
所以LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高
-
模拟一个简单的双向链表
-
public class LinkedList01 { public static void main(String[] args) { //模拟一个简单的双向链表 Node jack = new Node("jack"); Node tom = new Node("tom"); Node andy = new Node("Andy"); //连接三个结点,形成双向链表 //jack -> tom -> andy jack.next = tom; tom.next = andy; //andy -> tom -> jack andy.pre = tom; tom.pre = jack; Node first = jack;//让 first 引用指向 jack,就是双向链表的 头 结点 Node last = andy;//让 last 引用指向 andy,就是双向链表的 尾 结点 //演示从头到尾进行遍历 System.out.println("====演示从头到尾进行遍历===="); while (true) { if (first == null) { break; } //输出first 信息 System.out.println(first); first = first.next; } //演示从尾到头进行遍历 System.out.println("====演示从尾到头进行遍历===="); while (true) { if (last != null) { //输出last 信息 System.out.println(last); last = last.pre; } else { break; } } //要求,是在 tom--Andy 之间,插入一个对象 Wink //1.先创建一个 Node 结点, name 就是 Wink Node wink = new Node("Wink"); //2.下面就把 Wink 加入到双向链表了 tom.next = wink; wink.next = andy; andy.pre = wink; wink.pre = tom; //让 first 再次指向 jack first = jack;//让 first 重新指向头结点 System.out.println("====演示从头到尾进行遍历===="); while (true) { if (first == null) { break; } //输出first 信息 System.out.println(first); first = first.next; } last = andy;//让 last 重新指向尾结点 //演示从尾到头进行遍历 System.out.println("====演示从尾到头进行遍历===="); while (true) { if (last != null) { //输出last 信息 System.out.println(last); last = last.pre; } else { break; } } } } //定义一个Node类 ,Node对象表示双向链表的一个结点 class Node { public Object item;//真正存放数据 public Node next;//指向后一个结点 public Node pre;//指向前一个结点 public Node(Object name) { this.item = name; } public String toString() { return "Node name=" + item; } }
12.2.1.2.3 LinkedList 的增删改查案例
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedlist=" + linkedList);
//演示一个删除结点的
linkedList.remove();//默认删除第一个结点
//linkedList.remove(2);
System.out.println("linkedlist=" + linkedList);
//修改某个结点对象
linkedList.set(1, 999);
System.out.println("linkedlist=" + linkedList);
//得到某个结点对象
linkedList.get(1);//get(1) 是双向链表的第二个对象
System.out.println("linkedlist=" + linkedList.get(1));
//因为LinkedList 是 实现了 List接口,遍历方式一样
System.out.println("====LinkedList遍历迭代器====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
System.out.println("====LinkedList遍历增强for====");
for (Object o : linkedList) {
System.out.println(o);
}
System.out.println("====LinkedList遍历传统for====");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
/**
* 源码
* 1. LinkedList linkedList = new LinkedList();
* public LinkedList() { }
* 2. 这时 linkedlist 的属性 first = null last = null
* 3. 执行 添加
* public boolean add(E e) {
* linkLast(e);
* return true;
* }
* 4. 将新的结点,加入到双向链表的最后
* 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++;
* }
*
*/
}
}
12.2.1.2.4 ArrayList 和 LinkedList 的比较
底层结构 | 增删的效率 | 改查的效率 | |
---|---|---|---|
ArrayList | 可变数组 | 较低,数组扩容 | 较高 |
LinkedList | 双向链表 | 较高,通过链表追加 | 较低 |
如何选择 ArrayList 和 LinkedList:
-
如果我们改查的操作多,选择ArrayList
-
如果我们增删的操作多,选择LinkedList
-
一般来说,在程序中,80%~90%都是查询,因此大部分情况下会选择ArrayList
12.2.1.3 *Vector底层结构
12.2.1.3.1 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
-
Vector 和 ArrayList 的比较
底层结构 版本 线程安全(同步)效率 扩容倍数 ArrayList 可变数组 jdk1.2 不安全,效率高 如果有参构造,每次直接按1.5倍扩;如果是无参,第一次是10个,第二次开始就按1.5倍扩容 Vector 可变数组 jdk1.0 安全,效率不高 如果有参构造,每次直接按2倍扩;如果是无参,默认10个,满后,就按2倍扩容
12.2.2 Set
Set 接口基本介绍
-
无序(添加和取出的顺序不一致),没有索引
-
不允许重复元素,所以最多包含一个 null
Set 接口常用方法
-
和 List 接口一样,Set 接口也是Collection 的子接口,因此,常用方法和 Collection 接口一样
Set 接口的遍历方式
-
可以使用迭代器
-
可以使用增强for
-
不能使用索引的方式来获取
12.2.2.1 *HashSet
12.2.2.1.1 HashSet 的全面说明
-
HashSet 实现了 Set 接口
-
HashSet 实际上是HashMap,看源码
public HashSet() { map = new HashMap<>(); }
-
可以存放null值,但是只能有一个null
-
HashSet 不保证元素是有序的,取决于hash后,再确定索引的结果(即不能保证存放元素的顺序和取出的顺序一致)
-
不能有重复元素/对象,前面 Set 接口使用已经讲过
12.2.2.1.2 HashSet 底层机制说明
分析HashSet 底层是 HashMap,HashMap底层是(数组+链表+红黑树)
分析HashSet 的添加元素底层是如何实现(hash()+equals())
-
HashSet 底层是 HashMap
-
添加一个元素时,先得到hash值,会转成 -> 索引值
-
找到存储数据表 table,看这个索引位置是否已经存放的有元素
-
如果没有,直接加入
-
如果有,调用 equals 比较,如果相同,就放弃添加如果不相同,则添加到最后
-
在Java8 中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD(默认是 8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树)
-
/* HashSet源码解读 1. 执行 HashSet() public HashSet() { map = new HashMap<>(); } 2. 执行 add() public boolean add(E e) { return map.put(e, PRESENT)==null;//(static) PRESENT = new Object(); } 3. 执行 put(), 该方法会执行 hash(key) 得到 key 对应的hash值 算法h = key.hashCode()^(h >>> 16) public V put(K key, V value) {//key = "java" value = PRESENT 共享 return putVal(hash(key), key, value, false, true); } 4. 执行 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 就是 HashMap 的一个数组,类型是 Node[] //if 语句表示如果当前 table 是 null,或者 大小=0 //就是第一次扩容,到16个空间. if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //(1)根据key,得到的hash值, 去计算该key 应该存放到table 表的哪个索引位置 // 并且把这个位置的对象,赋给 p //(2)判断p 是否为 null //(2.1)如果 p为 null,表示还没有存放元素,就创建一个Node(key = "java",value = PRESENT) //(2.2)就放在该位置 tab[i] = newNode(hash, key, value, null) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k;// // 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样 // 并且满足 下面两个条件之一: //(1) 准备加入的 key 和 p指向的Node 结点的 key 是同一个对象 //(2) p指向的Node 结点的 key 的equals() 和准备加入的 key比较后相同 //就不能加入 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //再判断 p 是不是一颗红黑树, //如果是一颗红黑树,就调用 putTreeVal , 来进行添加 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //如果table 对应的索引位置,已经是一个链表,就是用for 循环比较 //(1) 依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后 // 注意在把元素添加到链表后,立即判断 该链表是否已经达到8个结点 // , 就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树) // 注意, 再转成红黑树时,要进行判断,判断条件如下 // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64)) // resize(); // 如果上面条件成立,先 table 扩容. // 只有上面条件不成立时,才进行转成红黑树 //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break else { for (int binCount = 0; ; ++binCount) { //无限循环 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD(8) - 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; // size 就是我们每加入一个结点Node(h,k,v,next),size++ if (++size > threshold) resize();//扩容 afterNodeInsertion(evict); return null; } */
分析HashSet的扩容和转成红黑树机制
-
HashSet 底层是HashMap,第一次添加时,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),就会进行树化(红黑树),否则仍然采用数组扩容机制
-
/* 当我们向 hashSet 增加一个元素, -> Node -> 加入table ,就算是增加了一个 size-----当size 达到 12 时,数组便会进行扩容 */ for (int i = 1; i <= 7; i++) {//在 table 表的某一条链表上添加了 7个A对象 hashSet.add(new A(i)); } for (int i = 1; i <= 7; i++) {//在 table 表的某一条链表上添加了 7个B对象 hashSet.add(new B(i)); }
12.2.2.2 LinkedHashSet
12.2.2.2.1 LinkedHashSet的全面说明
-
LinkedHashSet 是 HashSet 的子类
-
LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 双向链表
-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的
-
LinkedHashSet 不允许添加重复元素
12.2.2.2.2 LinkedHashSet 源码解读
-
在 LinkedHashSet 中维护了一个 hash表和双向链表(LinkedHashSet 有 head 和 tail)
-
每一个结点有 before 和 after 属性,这样可以形成双向链表
-
在添加一个元素时,先求 hash值,再求索引. 确定该元素在table 的位置,然后将添加的元素加入到双向链表(如果已经存在,则不添加)
-
tail.next = newElement //示意代码 newElement.pre = tail tail = newElement
-
遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致
-
//1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致 //2. LinkedHashSet 底层维护的是一个 LinkedHashMap(是HashMap的子类) //3. LinkedHashSet 底层结构 (数组+双向链表) //4. 添加第一次时,直接将 数组 table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry //5. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型 /* //继承关系是在内部类完成. 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); } } */
12.2.2.3 *TreeSet
public class TreeSet_ {
public static void main(String[] args) {
//1. 当我们使用无参构造器,创建TreeSet时,仍然是无序的
//2. 希望添加的元素,按照字符串大小来排序
//3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
// 并制定排序规则
TreeSet treeSet = new TreeSet();
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//下面 调用String 的 compareTo() 方法
//如果要求加入的元素,按照长度大小排序
return ((String) o1).compareTo((String) o2);//默认按照首字母顺序比较
return ((String) o1).length() - ((String) o2).length();
}
});
//添加数据.
treeSet.add("jack");
treeSet.add("python");
treeSet.add("Andy&Wink");
treeSet.add("eq");//2
treeSet.add("ff");//2
System.out.println(treeSet);
/*
源码
1. 构造器底层把传入的比较器对象,赋给了TreeSet底层的TreeMap的属性this.comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 在 调用 treeSet.add()方法时,在底层会执行到
if (cpr != null) { //cpr 就是我们的匿名内部类(对象)
do {
parent = t;
//动态绑定到我们的匿名内部类(对象)compare
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果相等,即返回0,这个Key就没有加入
return t.setValue(value);
} while (t != null);
}
*/
}
}
12.3 Map
Map 接口实现类的特点
-
Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key--Value
-
Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
-
Map 中的 key 不允许重复,原因和HashSet 一样,前面分析过源码
-
Map 中的value 可以重复
-
Map 的 key 可以为 null,value 也可以为 null,注意 key 为 null,只能有一个,value 为 null,可以多个
-
常用 String 类作为 Map 的 key
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
-
Map 存放数据的 key-value 示意图,一对 k-v 是放在一个 Node 中的,又因为Node 实现了 Entry 接口,有些书上也说一对 k-v 就是一个 Entry
-
Map map = new HashMap(); map.put("no1", "Andy");//k-v map.put("no2", "Wink");//k-v map.put(new Car(), new Person());//k-v //1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null) //2. k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合 , 该集合存放的元素的类型是 Entry, 而一个Entry // 对象就有k,v EntrySet<Entry<K,V>> 即, transient Set<Map.Entry<K,V>> entrySet; //3. entrySet 中, 定义的类型是 Map.Entry , 但是实际上存放的还是 HashMap$Node -----由于Node 实现了 Entry 接口 // 即 , 这是因为 static class Node<K,V> implements Map.Entry<K,V> //4. 当把 HashMap$Node 对象 存放到 entrySet 就方便我们的遍历,因为 Map.Entry 提供了两个非常重要的方法 // K getKey(); V getValue(); Set set = map.entrySet(); System.out.println(set.getClass());// HashMap$EntrySet for (Object obj : set) { //System.out.println(entry.getClass());// HashMap$Node //为了从 HashMap$Node 取出 k-v //1. 先做一个向下转型 Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + "-" + entry.getValue()); } Set set1 = map.keySet(); System.out.println(set1.getClass());//HashMap$KeySet Collection values = map.values(); System.out.println(values.getClass());//HashMap$Values
Map 接口和常用方法
-
put:添加
-
remove:根据键删除映射关系
-
get:根据键获取值
-
size:获取元素个数
-
isEmpty:判断个数是否为0
-
clear:清除
-
containsKey:查找键是否存在
Map 接口遍历方法
-
containsKey:查找键是否存在
-
keySet:获取所有的键
-
values:获取所有的值
-
entrySet:获取所有关系
-
//第一组:先取出所有的 Key,通过 Key 取出对应的Value Set keyset = map.keySet(); //(1)增强for System.out.println("-----第一种方式-----"); for (Object key : keyset) { System.out.println(key + "-" + map.get(key)); } //(2)迭代器 System.out.println("-----第二种方式-----"); Iterator iterator = keyset.iterator(); while (iterator.hasNext()) { Object key = iterator.next(); System.out.println(key + "-" + map.get(key)); } //第二组:把所有的 values 取出 System.out.println("-----取出所有的value 增强for-----"); Collection values = map.values(); //(1)增强for for (Object value : values) { System.out.println(value); } //(2)迭代器 System.out.println("-----取出所有的value 迭代器-----"); Iterator iterator1 = values.iterator(); while (iterator1.hasNext()) { Object value = iterator1.next(); System.out.println(value); } //第三组:通过 entrySet 来获取 k-v Set entrySet = map.entrySet(); //(1)增强for System.out.println("-----使用EntrySet 的 增强for(第3种)-----"); for (Object obj : entrySet) { //EntrySet //将 entry 转成 Map.Entry Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + "-" + entry.getValue()); } //(2)迭代器 System.out.println("-----使用EntrySet 的 迭代器(第4种)-----"); Iterator iterator2 = entrySet.iterator(); while (iterator2.hasNext()) { Object entry = iterator2.next();//HashMap$Node Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey() + "-" + m.getValue()); }
12.3.1 *HashMap
12.3.1.1 HashMap 底层机制及源码分析
-
HashMap 底层维护了 Node 类型的数组 table ,默认为 null
-
当创建对象时,将加载因子(loadfactor) 初始化为0.75
-
当添加 key-val 时,通过 key 的哈希值得到在 table 的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key 是否 和准备加入的key 相等。如果相等,则直接替换 val,如果不相等需要判断是树结构还是链表结构,做出相应的处理。如果添加时发现容量不够,则需要扩容。
-
第1次添加,则需要扩容table 容量为 16,临界值(threshold)为 12
-
以后再扩容,则需要扩容table 容量为原来的2倍,临界值为原来的 2倍,依此类推
-
在Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
12.3.1.2 HashMap 扩容树化触发
public class HashMapSource02 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 1; i <= 12; i++) {
hashMap.put(new A(i), "Andy");
}
System.out.println(hashMap);
}
}
12.3.1.3 HashMap 小结
-
Map 接口的常用实现类:HashMap、Hashtable 和 Properties
-
HashMap 是 Map 接口使用频率最高的实现类
-
HashMap 是以 key-val 对的方式来存储数据
-
key 不能重复,但是value 可以重复,允许使用 null 键和 null 值
-
如果添加相同的 key,则会覆盖原来的 key-val,等同于修改。(key 不会替换,val 会替换)
-
与 HashSet 一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的
-
HashMap 没有实现同步,因此是线程不安全的
12.3.2 *Hashtable
12.3.2.1 Hashtable 的基本介绍
-
存放的元素是键值对:即 K-V
-
hashtable 的键和值都不能为 null
-
hashtable 使用方法基本上和 HashMap 一样
-
hashtable 是线程安全的,hashMap 是线程不安全的
-
简单看下底层结构
/* 1. 底层有一个数组 Hashtable$Entry[] 初始化大小为 11 2. 临界值 threshold 8 = 11 * 0.75 3. 扩容:按照自己的扩容机制即可 4. 执行 方法 addEntry(hash, key, value, index); 添加 K-V 封装到 Entry 5. 当 if (count >= threshold) 满足时,就进行扩容 6. 按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容 */
12.3.3 *TreeMap
public class TreeMap_ {
public static void main(String[] args) {
//使用默认构造器,创建 TreeMap, 是无序的
/*
源码同 TreeSet 一致
*/
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// return ((String) o2).compareTo((String) o1);
return (((String) o2).length()) - (((String) o1).length());
}
});
treeMap.put("jack", "杰克");
treeMap.put("tom", "汤姆");
treeMap.put("smith", "史密斯");
treeMap.put("Wink$Andy", "机器学习");
treeMap.put("aaa", "大姆");//key 加入不了 value 替换
System.out.println(treeMap);
}
}
12.3.4 *Properties
12.3.4.1 基本介绍
-
Properties 类继承自 Hashtable 类并且实现了 Map 接口,也是使用一种键值对的形式来保存数据
-
它的使用特点和 Hashtable 类似
-
Properties 还可以用于 从 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改
12.4 Collections
12.4.1 Collections 工具类介绍
-
Collections 是一个操作 Set、List 和 Map 等集合的工具类
-
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
-
排序操作:(均为 static 方法)
-
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):根据 Comparator 指定的顺序,返回给定集合中的最小元素
-
int frequency(Collection, Object):返回指定集合中指定元素的出现次数
-
void copy(List dest, List src):将 src 中的内容复制到 dest 中
-
boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换 List 对象的所有旧值
-
-
12.5 总结
12.5.1 开发中如何选择集合实现类
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
-
先判断存储的类型(一组对象或一组键值对)
-
一组对象:Collection 接口
-
允许重复:List
-
增删多:LinkedList 【底层维护了一个双向链表】
-
改查多:ArrayList 【底层维护 Object 类型的可变数组】
-
-
不允许重复:Set
-
无序:HashSet 【底层是 HashMap ,维护了一个哈希表 即(数组+链表+红黑树)】
-
排序:TreeSet
-
插入和取出顺序一致:LinkedHashSet , 维护数组+双向链表
-
-
-
一组键值对:Map
-
-
键无序:HashMap 【底层是:哈希表 jdk7:数组+链表, jdk8:数组+链表+红黑树】
-
键排序:TreeMap
-
键插入和取出顺序一致:LinkHashMap
-
读取文件:Properties
-
-