12.1 集合的优点
前面我们保存多个数据使用的是数组,那么数组有不足的地方:
- 长度开始时必须指定,一旦指定,不能更改
- 保存的必须为同一类型的元素
- 使用数组进行增加/删除元素的代码比较麻烦
集合的优点:
- 可以动态保存任意多个对象,使用方便
- 提供了一系列方便的操作对象的方法:add、remove、set、get等
- 使用集合添加、删除新元素的代码简洁明了
12.2 集合的框架体系
Java的集合类有很多,主要分为两组(单列集合、双列集合):
-
Collection接口有两个重要的子接口 List 和 Set ,它们的实现子类都是单列集合
-
Map接口的实现子类都是双列集合,存放的是K-V
12.3 Collection接口
12.3.1 Collection接口实现类的特点
- Collection 实现子类可以存放多个元素,每个元素可以是Object
- 有些 Collection 的实现类,可以存放重复的元素,有些不可以
- Collection 的实现类,有些是有序的(List),有些不是有序的(Set)
- Collection 接口没有直接的实现子类,是通过它的子接口 List 和 Set 来实现的
12.3.2 Collection接口常用方法
以实现子类ArrayList为例:
public class CollectionMethod {
public static void main(String[] args) {
List list = new ArrayList();
//add:添加单个元素
list.add("jack");
list.add(10);
list.add(true);
//list=[jack, 10, true]
//remove:删除指定元素
list.remove(0);//删除第一个元素
//删除第一个元素后,第二个元素会变成第一个元素
list.remove(true);//删除指定元素
//contains:查找元素是否存在
System.out.println(list.contains(10));//true
//size:获取元素个数
System.out.println(list.size());//1
//isEmpty:判断是否为空
System.out.println(list.isEmpty());//false
//clear:清空
list.clear();//list=[]
//addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("西游记");
list2.add("红楼梦");
list.addAll(list2);//list=[西游记, 红楼梦]
//containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));//true
//removeAll:删除多个元素
list.add("三国演义");
list.removeAll(list2);//list=[三国演义]
System.out.println("list="+list);
}
}
12.3.3 Collection接口遍历元素
12.3.3.1 方式1 使用Iterator(迭代器)
12.3.3.1.1 介绍
- Iterator 对象为迭代器,主要用于遍历 Collection 集合中的元素
- 所有实现 Collection 接口的集合类都有一个
iterator()
方法,用以返回一个实现了 Iterator 接口的对象,即返回一个迭代器 - Iterator 的结构:
- Iterator仅用于遍历集合,Iterator本身并不存放对象
12.3.3.1.2 执行原理
调用过next()
方法后,iterator指向下一个元素
12.3.3.1.3 Iterator接口的方法
- hasNext():如果迭代具有更多元素,则返回
true
- next():返回迭代中的下一个元素
- remove():从底层集合中删除此迭代器返回的最后一个元素(可选操作)
在调用iterator.next()
方法之前必须要调用iterator.hasNext()
进行检测。若不调用,且下一条记录无效,直接调用iterator.next()
会抛出NoSuchElementException
异常
12.3.3.1.4 示例
public class IteratorDemo {
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Book("三国演义", "罗贯中", 10.1));
col.add(new Book("小李飞刀", "古龙", 5.1));
col.add(new Book("红楼梦", "曹雪芹", 34.6));
//遍历col集合
//1.得到col对应的迭代器
Iterator iterator = col.iterator();
//2.使用while循环遍历
while (iterator.hasNext()) {//判断是否还有数据
//返回下一个元素,类型是Object
Object obj = iterator.next();
//obj编译类型是Object,运行类型是真正存放的对象,此处为Book
System.out.println("obj="+obj);
}//itit快捷键自动生成此while循环
//3.当退出while循环时,iterator执行最后的元素
//iterator.next();//NoSuchElementException
//4.如果希望再次遍历,需要重置我们的迭代器
iterator = col.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("obj="+next);
}
}
}
class Book {
private String name;
private String author;
private double price;
public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
12.3.3.2 方式2 for循环增强
增强for就是简化版的iterator,本质一样,只能用于遍历集合或数组
语法:
for(元素类型 元素名 : 集合名或数组名) {
访问元素
}
示例:
//以上边集合col为例
for (Object obj : col) {
System.out.println("obj="+obj);
}
12.4 List接口
12.4.1 介绍
- List接口是Collection接口的子接口
- List集合类中的元素有序(添加顺序取出顺序一致)、且可重复
- List集合中的每个元素都有其对应的顺序索引,即支持索引
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
- List接口的实现类有:ArrayList、LinkedList、Vector等
12.4.2 常用方法
public class ListMethod {
public static void main(String[] args) {
List list = new ArrayList();
list.add("张三丰");
list.add("贾宝玉");
//void add(int index, Object ele):在 index 位置插入 ele 元素
list.add(1,"唐僧");//[张三丰, 唐僧, 贾宝玉]
//boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
List list1 = new ArrayList();
list1.add("tom");
list1.add("jack");
list1.add("唐僧");
list.addAll(list1);//[张三丰, 唐僧, 贾宝玉, tom, jack, 唐僧]
//Object get(int index):获取指定 index 位置的元素
System.out.println(list.get(1));//唐僧
//int indexOf(Object obj):返回 obj 在集合中首次出现的位置
System.out.println(list.indexOf("唐僧"));//1
//int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
System.out.println(list.lastIndexOf("唐僧"));//5
//Object remove(int index):移除指定 index 位置的元素,并返回此元素
list.remove(0);//[唐僧, 贾宝玉, tom, jack, 唐僧]
//Object set(int index, Object ele):设置指定 index 位置的元素为 ele, 相当于是替换.
list.set(4,"孙悟空");//[唐僧, 贾宝玉, tom, jack, 孙悟空]
//List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合
//返回的范围是[fromIndex, toIndex)
List subList = list.subList(0,3);
System.out.println(subList);//[唐僧, 贾宝玉, tom]
System.out.println(list);
}
}
12.4.3 List的三种遍历方式
- 使用Iterator
- 使用增强for
- 使用普通for
12.4.4 List练习
-
/** * 添加10个以上元素,在2号位插入一个元素"java",获得第五个元素 * 删除第六个元素,修改第七个元素,使用迭代器遍历集合 * 要求使用List的实现类ArrayList完成 */ public class ListExer { public static void main(String[] args) { List list = new ArrayList(); //添加10个以上元素 addEle(list); //在2号位插入一个元素"java" list.add(2,"java"); //获得第五个元素 System.out.println(list.get(5)); //删除第六个元素 list.remove(6); //修改第七个元素 list.set(7,100); //使用迭代器遍历集合 Iterator iterator = list.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next); } } public static void addEle(List list) { for (int i = 1; i < 11; i++) { list.add(i); } } }
-
/** * 使用List集合的额实现类添加三本图书,并遍历,打印如下效果 * 名称: xx 价格: xx 作者: xx * 名称: xx 价格: xx 作者: xx * 名称: xx 价格: xx 作者: xx * 要求: * 1.按照价格从低到高排序(冒泡) * 2.要求使用ArrayList、LinkedList和Vector三种集合实现 */ public class ListExer2 { public static void main(String[] args) { //List list = new LinkedList(); //List list = new ArrayList(); List list = new Vector(); list.add(new Book_("红楼梦", "曹雪芹", 100)); list.add(new Book_("西游记", "吴承恩", 10)); list.add(new Book_("水浒传", "施耐庵", 19)); list.add(new Book_("三国", "罗贯中", 80)); Iterator iterator = list.iterator(); System.out.println("===========排序前============"); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next); } //排序 sortList(list); System.out.println("===========排序后============"); iterator = list.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next); } } public static void sortList(List list) { int listSize = list.size(); for (int i = 0; i < listSize - 1; i++) { for (int j = 0; j < listSize - 1 - i; j++) { Book_ b1 = (Book_) list.get(j); Book_ b2 = (Book_) list.get(j+1); if(b1.getPrice() > b2.getPrice()) { list.set(j,b2); list.set(j+1,b1); } } } } } class Book_ { private String name; private String author; private double price; public Book_(String name, String author, double price) { this.name = name; this.author = author; this.price = price; } public double getPrice() { return price; } @Override public String toString() { return "名称: " + name + "\t\t价格: " + price + "\t\t作者: " + author; } }
12.4.5 ArrayList
12.4.5.1 ArrayList注意事项
- ArrayList可以添加null,并且可以添加多个
- ArrayList是由数组来实现数据存储的
- ArrayList基本等同于Vector,除了ArrayList是线程不安全的(没有synchronized修饰),执行效率高。在多线程情况下,不建议使用ArrayList
12.4.5.2 ArrayList的底层操作机制源码分析
- ArrayList中维护了一个Object类型的数组 elementData
transient Object[] elementData;
- transient表示瞬间,短暂的,表示该属性不会被序列化
- 当创建ArrayList对象时
- 如果使用无参构造器,则初始elementData容量为0。第一次添加,将elementData的容量扩容为10。若需要再次扩容,则扩容为当前容量的1.5倍
- 如果使用的是指定大小的构造器,则初始elementData容量为指定的大小,若需要扩容,则直接扩容为当前容量的1.5倍
对于代码:
ArrayList list = new ArrayList();
//ArrayList list = new ArrayList(8);
//使用 for 给 list 集合添加 1-10 数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
//使用 for 给 list 集合添加 11-15 数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);
源码分析:
12.4.6 Vector
12.4.6.1 介绍
-
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对象,默认容量是10,需要扩容时,扩容为当前容量的两倍
12.4.6.2 源码分析
对于代码:
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i);
}
vector.add(100)
源码分析:
-
//new Vector() 底层 public Vector() { this(10); } //补充:如果是 Vector vector = new Vector(8); //用的方法: public Vector(int initialCapacity) { this(initialCapacity, 0); }
-
//vector.add(i) //下面这个方法就添加数据到 vector 集合 public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true; }
-
//确定是否需要扩容 条件 : minCapacity - elementData.length>0 private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0 grow(minCapacity); }
-
//如果 需要的数组大小 不够用,就扩容 , 扩容的算法 //newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); //就是扩容两倍. 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); }
12.4.6.3 Vector和ArrayList比较
底层结构 | 版本 | 线程安全(同步)效率 | 扩容参数 | |
---|---|---|---|---|
ArrayList | 可变数组Object[] | jdk1.2 | 不安全,效率高 | 如果无参构造:默认为空数组,第一次扩容为10,第二次开始1.5倍扩容;如果有参构造,1.5倍扩容 |
Vector | 可变数组Object[] | jdk1.0 | 安全,效率不高 | 如果无参构造:默认容量为10,满后,按2倍扩容;如果有参构造,满后2倍扩容 |
12.4.7 LinkedList
12.4.7.1 介绍
- LinkedList底层实现了双向链表和双端队列特点
- 可以添加任意元素(可以重复),包括null
- 线程不安全,没有实现同步
12.4.7.2 LinkedList底层操作逻辑
- LinkedList底层维护了一个双向链表
- LinkedList中维护了两个属性
first
和last
,分别指向首节点和尾节点 - 每个结点(Node对象)里维护了
prev
、next
、item
三个属性,其中通过prev
指向前一个,通过next
指向后一个对象,最终实现双向链表 - LinkedList的元素的添加和删除不是通过数组完成的,相对来说效率较高
12.4.7.3 LinkedList增删改查
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);
//得到某个结点对象
//get(1) 是得到双向链表的第二个对象
Object o = linkedList.get(1);
System.out.println(o);//999
//因为 LinkedList 是 实现了 List 接口, 遍历方式
System.out.println("===LinkeList 遍历迭代器====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("next=" + next);
}
System.out.println("===LinkeList 遍历增强 for====");
for (Object o1 : linkedList) {
System.out.println("o1=" + o1);
}
System.out.println("===LinkeList 遍历普通 for====");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
源码分析:
//分析代码:
//LinkedList linkedList = new LinkedList();
//linkedList.add(1);
//1. LinkedList linkedList = new LinkedList();
//new LinkedList()构造方法对应的源码:
public LinkedList() {}
//这时 linkeList 的属性 first = null last = null
//2. linkedList.add(1);
//add(1) 执行 添加
public boolean add(E e) {
linkLast(e);
return true;
}
//linkLast(e)将新的结点,加入到双向链表的最后
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++;
}
//分析代码:
//linkedList.remove(); // 这里默认删除的是第一个结点
//remove() 执行了 removeFirst()
public E remove() {
return removeFirst();
}
//removeFirst() 通过执行unlinkFirst(f)完成删除
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//unlinkFirst(f)将 f 指向的双向链表的第一个结点拿掉
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;
}
12.4.8 ArrayList和LinkedList比较
底层结构 | 增删效率 | 改查效率 | |
---|---|---|---|
ArrayList | 可变数组 | 较低,需要数组扩容 | 较高,可以通过索引直接定位 |
LinkedList | 双向链表 | 较高,通过链表追加 | 较低,需要从头一个一个遍历 |
如何选择:
- 改查操作比较多,选ArrayList
- 增删操作比较多,选LinkedList
- 一般在程序中,80%~90%都是查询,因此大部分情况下选择ArrayList
- 在项目中,根据业务灵活选择,比如,一个模块使用ArrayList,因一个模块使用LinkedList
- ArrayList和LinkList都是线程不安全的,尽量在单线程操作时选用
12.5 Set接口
12.5.1 介绍
- 无序(添加和取出的顺序不一致),没有索引
- 取出的顺序的顺序虽然不是添加的顺序,但是取出的顺序是固定的
- 不允许重复元素,最多包含一个null
- Set接口的实现类有:HashSet、LinkedHashSet、TreeSet等
12.5.2 常用方法
和List接口一样,Set接口也是Collection接口的子接口,因此,常用的方法和Collection一样
12.5.3 遍历方式
- 迭代器
- 增强for
- 不能使用索引的方式获取
12.5.4 示例
public class SetMethod {
public static void main(String[] args) {
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john");//再次添加 john
set.add("jack");
set.add("hsp");
set.add("mary");
set.add(null);
set.add(null);//再次添加 null
System.out.println(set);//[null, hsp, mary, john, lucy, jack]
set.remove("mary");
System.out.println("====使用Iterator====");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
System.out.println("====使用增强for====");
for (Object o : set) {
System.out.println(o);
}
}
}
12.5.5 HashSet
12.5.5.1 介绍
-
HashSet实现了Set接口
-
HashSet实际上是HashMap
public HashSet() { map = new HashMap<>(); }
-
可以存放null,但只能有一个
-
HashSet不保证元素是有序的,取决于hash后,再确定索引的结果
-
不能有重复的元素/对象
12.5.5.2 案例
public class HashSet01 {
public static void main(String[] args) {
HashSet set = new HashSet();
//说明
//1. 在执行 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,返回 true, 否则返回 false
//3. 可以通过 remove 指定删除哪个对象
System.out.println(set.add("john"));//T
System.out.println(set.add("lucy"));//T
System.out.println(set.add("john"));//F
System.out.println(set.add("jack"));//T
System.out.println(set.add("Rose"));//T
set.remove("john");
System.out.println("set=" + set);//3 个对象
set = new HashSet();
System.out.println("set=" + set);//0
//4 Hashset 不能添加相同的元素/数据?
set.add("lucy");//添加成功
set.add("lucy");//加入不了
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//Ok
System.out.println("set=" + set);
//再加深一下. 非常经典的面试题
//看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然
//去看他的源码,即 add 到底发生了什么?=> 底层机制.
set.add(new String("hsp"));//ok
set.add(new String("hsp"));//加入不了.
System.out.println("set=" + set);
}
}
class Dog { //定义了 Dog 类
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
12.5.5.3 HashSet底层机制
12.5.5.3.1 HashSet存储结构
HashSet底层是HashMap,HashMap的底层是数组+链表+红黑树
HashSet的存储结构是哈希表
-
jdk1.8版本之前:
- 哈希表=数组+链表
-
jdk1.8版本之后:
- 哈希表=数组+链表
- 哈希表=数组+红黑树(查询速度极快)
模拟简单的数组+链表结构,实现如图所示效果:
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个HashSet的底层
//1.创建一个数组,数组的类型是Node[]
//2.有些人直接把Node[]数组称为表
Node[] table = new Node[16];
//3.创建结点
Node john = new Node("john", null);
table[2] = john;//把john结点放在table表索引为3的位置
Node jack = new Node("jack", null);
john.next = jack;//将jack结点 挂载 到john后边
Node rose = new Node("rose", null);
jack.next = rose;//将rose结点挂载到jack后边
Node lucy = new Node("lucy", null);
table[3] = lucy; //把lucy结点放在table表索引为3的位置
System.out.println("table="+table);
}
}
class Node {
Object item;
Node next;
public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}
12.5.5.3.2 创建HashSet及添加元素
创建HashSet底层其实是创建了一个HashMap
HashSet添加元素的底层实现:
- HashSet底层是HashMap
- 添加一个元素,先得到 hash值 会转成》索引值
- 找到存储数据表table,看这个索引位置是否已经存放的有元素
- 如果没有,直接加入
- 如果有,调用
equals()
依次进行比较,如果相同,则放弃添加;如果不同,则添加到最后 - 在JDK8中,如果一条链表的元素个数到达
TREEIFY_THRESHOLD
(默认为8),并且 table 的大小>= MIN_TREEIFY_CAPACITY
(默认为64),就会进行树化(成为红黑树)
源码解读:
对于代码:
HashSet hashSet = new HashSet();
hashSet.add("java");//到此位置,第 1 次 add 分析完毕.
hashSet.add("php");//到此位置,第 2 次 add 分析完毕
hashSet.add("java");
System.out.println("set=" + hashSet);
解读:
对于创建HashSet:HashSet hashSet = new HashSet();
及第一次add:hashSet.add("java");
-
//HashSet hashSet = new HashSet(); //执行的是构造器,底层是创建了HashMap public HashSet() { map = new HashMap<>(); }
执行后:
-
//hashSet.add("java"); //执行add() public boolean add(E e) { //e = "java" return map.put(e, PRESENT)==null; //PRESENT是HashSet的一个对象,没什么意义,占位的目的 //PRESENT是静态的,共享 //private static final Object PRESENT = new Object(); //put()之后返回null代表成功了 }
-
//执行 put() public V put(K key, V value) { //key = "java" value = PRESENT return putVal(hash(key), key, value, false, true); }
-
//hash(key)方法,如果key为null则返回0 //不为空则通过(h = key.hashCode()) ^ (h >>> 16) //算法得到key对应的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;//定义了辅助变量 //table 就是HashMap的一个属性,类型是 Node[] //if语句表示如果当前table是null,或者大小 = 0 //就是第一次扩容,到16个空间 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //调用resize()方法后,table容量变为16 //1.根据key得到的hash值,去计算该key应该存放到table的哪个索引位置 //此处key = "java",计算出的i的值为3 //并把这个位置的对象赋给辅助变量p //2.判断p是否为空 //(1)如果p为空,表示没有存放元素,就创建一个Node(key,value) //当前key = "java" value = PRESENT //(2)放在该位置tab[i] = newNode(hash, key, value, null); //即将key为"java"的newNode存放在tab[3] 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; //判断当前大小是否大于临界值threshold if (++size > threshold) resize();//resize()相当于扩容 afterNodeInsertion(evict); //afterNodeInsertion(evict)是空方法,作用是: //留给HashMap的子类(如LinkedHashMap)去实现该方法 //完成相关操作 return null;//返回空代表成功了 }
-
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; //oldTab也就是table为空时,oldCap为0 int oldThr = threshold; //threshold是HashMap的一个属性,相当于数组扩容临界值 //定义该属性的代码为 int threshold; //当table为空时,threshold为int类型默认值0 int newCap, newThr = 0; 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 //oldCap和oldThr均为0时 newCap = DEFAULT_INITIAL_CAPACITY; //将new赋值为1 << 4 即16,即扩容为16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //newThr相当于临界值 //计算方法是加载因子DEFAULT_LOAD_FACTOR*DEFAULT_INITIAL_CAPACITY //DEFAULT_LOAD_FACTOR = 0.75 //DEFAULT_INITIAL_CAPACITY = 16 //即0.75 * 16 = 12 时,就要准备扩容 } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;//将threshold更新为new @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //创建一个容量为newCap的Node[]数组newTab table = newTab;//将newTab赋给table 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; }
-
插入后
第二次add:hashSet.add("php");
-
public boolean add(E e) { //e = "php" return map.put(e, PRESENT)==null; }
-
public V put(K key, V value) { //key = "php" value = PRESENT return putVal(hash(key), key, value, false, true); }
-
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; //同上,根据key得到的hash值,去计算该key应该存放到table的哪个索引位置 //此处key = "php",计算得i = 9 //tab[9]为空,创建key为"php"的newNode放在该位置 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; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
-
插入后
对于插入相同的元素:hashSet.add("java");
-
public boolean add(E e) {//e = "java" return map.put(e, PRESENT)==null; }
-
public V put(K key, V value) {//key = "java" value = PRESENT return putVal(hash(key), key, value, false, true); }
-
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; //根据key = "java",计算得i = 3,tab[3]不为空 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //在需要局部变量(辅助变量)的时候再创建 Node<K,V> e; K k;//辅助变量 //如果 当前索引位置对应的链表的第一个元素p 和 准备添加的key 的hash值一样 //并且满足下面两个条件之一: //(1)准备加入的key 和 p指向的Node结点的key 是同一个对象 //(2)两者通过equals方法判断结果相同 //equals方法可以被程序员通过重写来自定义比较的内容 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //此时key已经与链表第一个元素比较过了 e = p; //判断p是不是一棵红黑树 //如果是红黑树,调用putTreeVal()方法来添加 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //如果table对应的索引位置已经是一个链表了 //就使用for循环依次比较 else { //有几种情况: //(1)依次跟该链表的的每个元素比较后,都不相同,则加入到该链表的最后 //(2)依次跟该链表的的每个元素比较的过程中,如果有相同的情况,就直接break for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {//判断是否比到链表最后一个元素了 //没有跟第一个元素比较吗? //在前边已经比较过 p.next = newNode(hash, key, value, null);//加入到链表最后 //添加到链表后,立即判断链表结点个数是否达到了临界值8 //true则调用treeifyBin()方法考虑是否将当前链表转换为红黑树 //treeifyBin()方法会判断当前table数组是否为空或大小小于64 //(1)true则调用resize()对table数组进行扩容 //(2)false才进行树化 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //key与链表的元素进行比较 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; }
-
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //treeifyBin()方法会判断当前table数组是否为空或大小小于64 //(1)true则调用resize()对table数组进行扩容 //(2)false才进行树化 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); } }
12.5.5.3.3 HashSet扩容或转成红黑树机制
- HashSet底层是HashMap,第一次添加时,table数组扩容到默认初始化容量值
DEFAULT_INITIAL_CAPACITY
(16),临界值threshold
= 加载因子loadFactor(默认为DEFAULT_LOAD_FACTOR = 0.75
) * 默认初始化容量值DEFAULT_INITIAL_CAPACITY
(16) = 12- HashMap维护一个int类型的变量
size
,表示当前结点的总数,每加入一个结点Node(k,v,hash,next),不管该节点是加在table表某索引的第一个位置,还是加在table表的某一条链表上,size的值都会加1
- HashMap维护一个int类型的变量
- 如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
- 在JDK8中,如果一条链表的元素个数达到了树形化阈值
TREEIFY_THRESHOLD
(8),并且table的大小 >= 最小树形化阈值MIN_TREEIFY_CAPACITY
(64),就会进行树化(红黑树),否则仍然采用数组扩容机制
debug以下代码,查看hashSet的值
public class HashSetIncrement {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
//HashSet底层是HashMap,第一次添加时,
//table数组扩容到默认初始化容量值`DEFAULT_INITIAL_CAPACITY`(16),
//临界值`threshold`= 加载因子loadFactor(默认为`DEFAULT_LOAD_FACTOR = 0.75`) * 默认初始化容量值`DEFAULT_INITIAL_CAPACITY`(16) = 12
//如果table数组使用到了临界值12,就会扩容到16 * 2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
// for (int i = 0; i < 100; i++) {
// hashSet.add(i);//1,2,3,4,5,...,100
// }
//在JDK8中,如果一条链表的元素个数达到了树形化阈值`TREEIFY_THRESHOLD`(8),
//并且table的大小 >= 最小树形化阈值`MIN_TREEIFY_CAPACITY`(64),
//就会进行树化(红黑树),否则仍然采用数组扩容机制
for (int i = 0; i < 13; i++) {
hashSet.add(new A(i));
//当i = 10时,table数组大小为64 = MIN_TREEIFY_CAPACITY,
//且索引为36存放的链表结点数为10 > TREEIFY_THRESHOLD,
//所以添加n = 10的A类对象后,进行了树化
}
System.out.println("hashSet=" + hashSet);
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;//将A类对象的hashCode都置为100,确保加在table表同一索引位置
}
}
12.5.5.3.4 HashSet练习
-
/** * 定义一个Employee类,该类包含:private成员属性name、age 要求: * 1. 创建三个Employee对象放入HashSet中 * 2. 当name和age相同时,认为是相同员工,不能添加到HashSet集合中 */ public class HashSetExercise { public static void main(String[] args) { HashSet hashSet = new HashSet(); System.out.println(hashSet.add(new Employee("jack", 18)));//true System.out.println(hashSet.add(new Employee("tom", 20)));//true System.out.println(hashSet.add(new Employee("tom", 22)));//true System.out.println(hashSet.add(new Employee("jack", 18)));//false } } class Employee { private String name; private int age; public Employee(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Employee{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return age == employee.age && Objects.equals(name, employee.name); } @Override public int hashCode() { return Objects.hash(name, age); } }
-
/** * 定义一个Employee类,该类包含:private成员属性name、sal、birthday(MyDate类) * 其中birthday为MyDate类的型(属性包括:year、month、day),要求: * 1. 创建三个Employee对象放入HashSet中 * 2. 当name和birthday相同时,认为是相同员工,不能添加到HashSet集合中 */ public class HashSetExercise { public static void main(String[] args) { HashSet hashSet = new HashSet(); System.out.println(hashSet.add(new Employee("tom", 2000, new MyDate(2000, 9, 1)))); System.out.println(hashSet.add(new Employee("jack", 3000, new MyDate(2000, 9, 1)))); System.out.println(hashSet.add(new Employee("tom", 4000, new MyDate(2000, 7, 1)))); System.out.println(hashSet.add(new Employee("jack", 5000, new MyDate(2000, 9, 1)))); } } class Employee { private String name; private double sal; private MyDate birthday; public Employee(String name, double sal, MyDate birthday) { this.name = name; this.sal = sal; this.birthday = birthday; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return Objects.equals(name, employee.name) && Objects.equals(birthday, employee.birthday); } @Override public int hashCode() { return Objects.hash(name, birthday); } } class MyDate { private int year; private int month; private int day; public MyDate(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MyDate myDate = (MyDate) o; return year == myDate.year && month == myDate.month && day == myDate.day; } @Override public int hashCode() { return Objects.hash(year, month, day); } }
12.5.6 LinkedHashSet
12.5.6.1 介绍
- LinkedHashSet是HashSet的子类
- LinkedHashSet底层是一个LinkedHashMap,底层维护了一个 数组+双向链表
- LinkedHashSet根据元素的hashCode来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来像是以插入顺序保存的
- LinkedHashSet不允许添加重复元素
12.5.6.2 LinkedHashSet底层原理
对于代码:
Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);//false
set.add(new Customer("刘",1001));
set.add(123);
set.add("tom");
System.out.println(set);
-
LinkedHashSet 加入顺序和取出元素/数据的顺序一致
-
LinkedHashSet底层维护的是一个LinkedHashMap(是HashMap的子类)
-
LinkedHashSet底层结构(数组table+双向链表)
-
第一次添加时,直接将 数组table 扩容至16,存放的节点类型是
LinkedHashMap$Entry
-
数组是
HashMap$Node[]
。存放的元素/数据是LinkedHashMap$Entry
类型//源码可见,Entry继承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); } }
12.5.6.3 练习
/**
* Car1类(属性:name、price),如果name和price一样
* 则认为是相同元素,就不能添加
*/
public class LinkedHashSetExercise {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car1("奥拓",1000));
linkedHashSet.add(new Car1("奥迪",300000));
linkedHashSet.add(new Car1("法拉利",10000000));
linkedHashSet.add(new Car1("奥迪",300000));
linkedHashSet.add(new Car1("保时捷",70000000));
linkedHashSet.add(new Car1("奥迪",300000));
}
}
class Car1 {
private String name;
private double price;
public Car1(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car1 car1 = (Car1) o;
return Double.compare(car1.price, price) == 0 && Objects.equals(name, car1.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
@Override
public String toString() {
return "Car1{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
12.5.7 TreeSet
12.5.7.1 介绍
- TreeSet 实现了 Set 接口
- TreeSet 最大的特点是可以排序
- 当我们使用无参构造器创建TreeSet时,仍然是无序的
- 若希望添加的元素按照某种规则排序,使用TreeSet提供的一个构造器,可以传入一个比较器(匿名内部类),并指定排序规则
- TreeSet 是线程不安全的(没有synchronized)
- TreeSet 底层用的是 TreeMap
12.5.7.2 示例
public class TreeSetDemo {
public static void main(String[] args) {
//TreeSet treeSet = new TreeSet();
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//下面 调用 String 的 compareTo 方法进行字符串大小比较
//如果认为字符串相等为相同元素
//return ((String) o2).compareTo((String) o1);
//如果要求加入的元素,按照长度大小排序
//此时认为长度相等为相同元素
return ((String) o1).length() - ((String) o2).length();
}
});
//添加数据.
treeSet.add("jack");
treeSet.add("tom");//长度为3
treeSet.add("sp");
treeSet.add("a");
treeSet.add("abc");//长度为3
//已经有tom,abc和tom比较长度相等,abc加入不了
System.out.println(treeSet);//[a, sp, tom, jack]
}
}
12.5.7.3 源码解读
-
构造器把传入的比较器对象,赋给了 TreeSet 的底层的 TreeMap 的属性 this.comparator
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
-
在 调用 treeSet.add(“tom”), 在底层会执行到 TreeMap 的 put() 方法
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) {//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 //如果相等,即compare返回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; }
12.6 Map接口
12.6.1 介绍
- 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 是放在一个
HashMap$Node
中的,又因为 Node 实现了 Entry 接口,有些书上也说一对k-v就是一个Entry
。
Map map = new HashMap();
map.put("no1", "韩顺平");//k-v
map.put("no2", "张无忌");//k-v
map.put("no1", "张三丰");//当有相同的 k, 就等价于替换
map.put("no3", "张三丰");//k-v
map.put(null, null);//k-v
map.put(null, "abc");//等价替换
map.put("no4", null);//k-v
map.put("no5", null);//k-v
map.put(1, "赵敏");//k-v
map.put(new Object(), "金毛狮王");//k-v
// 通过 get 方法,传入 key ,会返回对应的 value
System.out.println(map.get("no2"));//张无忌
System.out.println("map=" + map);
//1.k-v 最后是通过 HashMap$Node node = newNode(hash, key, value, null);存在table表中的HashMap$Node中
//2.k-v 为了方便遍历,还会创建 EntrySet 集合,该集合存放的是一个一个的Entry
// 一个Entry对象包含有k、v,即EntrySet<Entry<K, V>>
// 源码:transient Set<Map.Entry<K,V>> entrySet;
//3.Entry中,定义的类型是Map$Entry,但实际上他存放的还是HashMap$Node
// 这是因为HashMap$Node实现了Map.Entry接口
// 源码:static class Node<K,V> implements Map.Entry<K,V>{...}
// 其实就是用到了向上转型,即Map$Entry entry = node;
// 编译类型是Map$Entry,运行类型是HashMap$Node
//4.当把HashMap$Node对象放到entrySet中,就方便我们遍历
// 因为Entry提供了getKey()和getValue()方法
12.6.2 常用方法
- put:添加
- remove:根据 key 删除映射关系
- get:根据 key 获取 value
- size:获取元素个数
- isEmpty:判断元素个数是否为0
- clear:清空
- containsKey:查找 key 是否存在
public class MapMethod {
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超", new Book("", 100));//OK
map.put("邓超", "孙俪");//替换-> 一会分析源码
map.put("王宝强", "马蓉");//OK
map.put("宋喆", "马蓉");//OK
map.put("刘令博", null);//OK
map.put(null, "刘亦菲");//OK
map.put("鹿晗", "关晓彤");//OK
System.out.println("map=" + map);
// remove:根据键删除映射关系
map.remove(null);
System.out.println("map=" + map);
// get:根据键获取值
Object val = map.get("鹿晗");
System.out.println("val=" + val);
// size:获取元素个数
System.out.println("k-v=" + map.size());
// isEmpty:判断个数是否为 0
System.out.println(map.isEmpty());//F
// containsKey:查找键是否存在
System.out.println("结果=" + map.containsKey("宋喆"));//T
System.out.println("map=" + map);
// clear:清除 k-v
map.clear();
System.out.println("map=" + map);
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
12.6.3 六大遍历方式
常用方法:
- containsKey:查找 key 是否存在
- keySet:获取所有的 key
- values:获取所有的 value
- entrySet:获取所有的k-v
public class MraverseMap {
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超", "孙俪");
map.put("王宝强", "马蓉");
map.put("宋喆", "马蓉");
map.put("刘令博", null);
map.put(null, "刘亦菲");
map.put("鹿晗", "关晓彤");
//第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
System.out.println("======第一组======");
Set keySet = map.keySet();
System.out.println("====增强 for====");
for (Object key : keySet) {
System.out.println(key + "-" +map.get(key));
}
System.out.println("====迭代器====");
Iterator iterator = keySet.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next + "-" + map.get(next));
}
//第二组: 把所有的 values 取出
System.out.println("======第二组======");
Collection values = map.values();
//这里可以使用所有的 Collections使用的遍历方法
System.out.println("====增强 for====");
for (Object value : values) {
System.out.println(value);
}
System.out.println("====迭代器====");
Iterator iterator1 = values.iterator();
while (iterator1.hasNext()) {
Object next = iterator1.next();
System.out.println(next);
}
//第三组: 通过 EntrySet 来获取 k-v
System.out.println("======第三组======");
Set entrySet = map.entrySet();
System.out.println("====增强 for====");
for (Object entry : entrySet) {
//将entry转成Map.Entry
Map.Entry mEntry = (Map.Entry) entry;
System.out.println(mEntry.getKey() + "-" + mEntry.getValue());
}
System.out.println("====迭代器====");
Iterator iterator2 = entrySet.iterator();
while (iterator2.hasNext()) {
Map.Entry next = (java.util.Map.Entry) iterator2.next();
System.out.println(next.getKey() + "-" + next.getValue());
}
}
}
12.6.4 练习
/**
* 使用HashMap添加3个员工对象,要求:
* key:员工id
* value:员工对象
* 并遍历显示工资>18000的员工(遍历方式最少两种)
* 员工类:姓名、工资、员工id
*/
public class MapExercise {
public static void main(String[] args) {
Employee tom = new Employee("tom", 20000, 10001);
Employee mary = new Employee("mary", 25000, 10002);
Employee jack = new Employee("jack", 16000, 10003);
Map map = new HashMap();
map.put(tom.getId(),tom);
map.put(mary.getId(),mary);
map.put(jack.getId(),jack);
System.out.println("====keySet增强for====");
Set keySet = map.keySet();
for (Object key : keySet) {
Employee employee = (Employee) map.get(key);
if (employee.getSal() > 18000) {
System.out.println(employee);
}
}
System.out.println("====entrySet迭代器====");
Set entrySet = map.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry next = (Map.Entry) iterator.next();
Employee employee = (Employee) next.getValue();
if (employee.getSal() > 18000) {
System.out.println(employee);
}
}
}
}
class Employee {
private String name;
private double sal;
private int id;
public Employee(String name, double sal, int id) {
this.name = name;
this.sal = sal;
this.id = id;
}
public String getName() {
return name;
}
public double getSal() {
return sal;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", id=" + id +
'}';
}
}
12.6.5 HashMap
12.6.5.1 HashMap小结
-
Map 接口的常用实现类:HashMap、HashTable 和 Properties
-
HashMap 是 Map 接口使用频率最高的实现类
-
HashMap 是以 key-value 对的方式存储数据的(HashMap$Node类型)
-
key 不能重复,但是值可以重复,允许使用 null 键和 null值
-
如果添加相同的 key,则会覆盖原来的 key-value ,等同于修改(key 不会替换,value会替换)看源码
//putVal()方法中,当两者比较key相同为true后, //若原先的元素非空,进行value值的替换 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
-
和 HashSet 一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(JDK8的HashMap底层是 数组+链表+红黑树)
-
HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
12.6.5.2 HashMap底层机制及源码剖析
存储结构:
扩容机制(和HashSet相同):
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子
loadFactor
初始化为0.75 - 当添加一个 key-value 时,通过
key
的哈希值计算得到在 table 的索引,然后判断该索引处是否有元素- 如果没有元素则直接添加
- 如果该处有元素,继续判断该处的 key 和准备加入的 key 是否相等
- 如果相等,则直接替换 value
- 如果不相等需要判断是树结构还是链表结构,做出相应处理,如果添加时发现容量不够,则需要进行扩容
- 第一次添加,将 table 表扩容到16,临界值
threshold
为12(16 * 0.75) - 以后再扩容,则需要将table表扩容到原来的2倍,临界值为当前容量 * 0.75
- 在 JDK8 中,如果一条链表的元素个数超过了
TREEIFY_THRESHOLD
(默认为8),且 table 的大小 >=MIN_TREEIFY_CAPACITY
(默认为64),就会进行树化(红黑树)
public class HashMapSource1 {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java", 10);//ok
map.put("php", 10);//ok
map.put("java", 20);//替换 value
System.out.println("map=" + map);
//解读 HashMap 的源码+图解
//1. 执行构造器 new HashMap()
//初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null
//2. 执行 put 调用 hash 方法,计算 key 的 hash 值 (h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {//K = "java" value = 10
return putVal(hash(key), key, value, false, true);
}
//3. 执行 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 数组为 null, 或者 length =0 , 就扩容到 16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//取出 hash 值对应的 table 的索引位置的 Node, 如果为 null, 就直接把加入的 k-v
//, 创建成一个 Node ,加入该位置即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;//辅助变量
// 如果 table 的索引位置的 key 的 hash 相同和新的 key 的 hash 值相同,
// 并 满足(table 现有的结点的 key 和准备添加的 key 是同一个对象 || equals 返回真)
// 就认为不能加入新的 k-v
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//如果当前的 table 的已有的 Node 是红黑树,就按照红黑树的方式处理
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);
//加入后,判断当前链表的个数,是否已经到 8 个,到 8 个,后
//就调用 treeifyBin 方法进行红黑树的转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //如果在循环比较过程中,发现有相同,就 break,就只是替换 value
((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; //替换,key 对应 value
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//每增加一个 Node ,就 size++
if (++size > threshold[12-24-48])//如 size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
//5. 关于树化(转成红黑树)
//如果 table 为 null ,或者大小还没有到 64,暂时不树化,而是进行扩容. //否则才会真正的树化 -> 剪枝
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else ...
}
}
}
12.6.6 Hashtable
12.6.6.1 介绍
- Hashtable 存放的元素是键值对:
key-value
- Hashtable 的 key 和 value 都不能为 null,否则会抛出NullPointerException
- Hashtable 是线程安全的(synchronized),HashMap是线程不安全的
Hashtable table = new Hashtable();
table.put("john", 100);//ok
table.put("null", 100);//异常
table.put("john", null);//异常
table.put("lucy", 100);//ok
table.put("lic", 100);//ok
table.put("lic", 88);//替换
12.6.6.2 底层结构
-
底层是一个数组
Hashtable$Entry[]
,初始化大小为11 -
临界值threshold = 当前容量 * 0.75,即初始为8
-
扩容:先判断要添加的元素是否为空
-
为空则抛异常
-
不为空则调用方法
addEntry(hash, key, value, index);
添加k-v-
当元素个数未到达临界值,直接添加
-
当元素个数到达临界值,调用
rehash()
进行扩容if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash();
- 扩容算法为
int newCapacity = (oldCapacity << 1) + 1;
- 即当前容量的两倍加1
- 扩容后将 临界值 置为 新容量 * 0.75
- 扩容算法为
-
-
12.6.6.3 Hashtable和HashMap比较
版本 | 线程安全(同步) | 效率 | 允许null键null值 | |
---|---|---|---|---|
HashMap | 1.2 | 不安全 | 高 | 允许 |
Hashtable | 1.0 | 安全 | 较低 | 不允许 |
12.6.7 Properties
12.6.7.1 介绍
- Properties 类继承自 Hashtable 类并实现了 Map 接口,也是使用一种键值对的形式来保存数据
- 它的特点和 Hashtable 类似
- Properties 还可以用于从 xxx.properties 文件中,加载到 Properties 类对象,并进行读取和修改
12.6.7.2 基本使用
public class PropertiesDemo {
public static void main(String[] args) {
Properties properties = new Properties();
//增加
// properties.put(null, "abc");//抛出空指针异常
// properties.put("abc", null);//抛出空指针异常
properties.put("john", 100);//ok
properties.put("lucy", 100);//ok
properties.put("lic", 100);//ok
properties.put("lic", 88);//替换
//通过key获取value
System.out.println(properties.get("lic"));
//删除
properties.remove("lucy");
//修改
properties.put("john", "约翰");
System.out.println(properties);
}
}
12.6.8 TreeMap
12.6.8.1 介绍
可以参考TreeMap原理实现及常用方法
- TreeMap存储K-V键值对,通过红黑树(R-B tree)实现;
- TreeMap继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口,可支持一系列的导航定位以及导航操作的方法,当然只是提供了接口,需要TreeMap自己去实现;
- TreeMap实现了Cloneable接口,可被克隆,实现了Serializable接口,可序列化;
- TreeMap因为是通过红黑树实现,红黑树结构天然支持排序,默认情况下通过Key值的自然顺序进行排序;
12.6.8.2 示例
public class TreeMapDemo {
public static void main(String[] args) {
//使用默认的构造器,创建 TreeMap, 是无序的(也没有排序)
//TreeMap treeMap = new TreeMap();
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//按照 K(String) 的大小来排序
//return ((String) o1).compareTo((String) o2);
//按照 K(String) 的长度大小排序
return ((String) o1).length() - ((String) o2).length();
}
});
treeMap.put("jack", "杰克");
treeMap.put("tom", "汤姆");
treeMap.put("kristina", "克瑞斯提诺");
treeMap.put("smith", "斯密斯");
treeMap.put("mary", "玛丽");
//若按照字符串长度排序,key(mary)加入不了,value会替换为"玛丽"
System.out.println(treeMap);
}
}
12.6.8.3 源码分析
-
构造器. 把传入的实现了 Comparator 接口的匿名内部类(对象),传给给 TreeMap 的 comparator
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
-
调用 put 方法
public V put(K key, V value) { Entry<K,V> t = root; if (t == null) {//第一次添加 t为空 compare(key, key); // type (and possibly null) check //两个key相同,且没有接收结果,只是为了检查是不是空值 root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths //第二次及之后添加,将传入的比较器赋给cpr Comparator<? super K> cpr = comparator; if (cpr != null) { do {//遍历所有的key,给当前key找到适当位置 parent = t; cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的compare if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else//如果遍历过程中,发现准备添加的key和当前已有的key相等,就不添加,而是将value替换 return t.setValue(value);//替换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; }
12.7 如何选择集合实现类
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择
先判断存储的类型: 一组对象[单列]
或者 一组键值对[双列]
- 一组对象[单列]:Collection 接口
- 允许重复:List 接口
- 增删多:LinkedList【底层维护了一个双向链表】
- 改查多:ArrayList【底层维护了 Object 类型的可变数组】
- 不允许重复:Set 接口
- 无序:HashSet 【底层是 HashMap,维护了一个哈希表(即 数组 + 链表 + 红黑树)】
- 排序:TreeSet
- 插入和取出顺序一致:LinkedHashSet【维护了 数组 + 双向链表】
- 允许重复:List 接口
- 一组键值对[双列]:Map 接口
- 键无序:HashMap【底层是 哈希表】
- 键排序:TreeMap
- 键插入和取出顺序一致:LinkedHashMap
- 读取文件:Properties
12.8 Collections工具类
12.8.1 介绍
- Collections 是一个操作 Set、List 和 Map 等集合的工具类
- Collections 中提供了一系列静态方法对集合元素进行排序、查询和修改等操作
12.8.2 排序
reverse(List)
:反转 List 中元素的顺序shuffle(List)
:对 List 集合元素进行随机排序sort(List)
:根据元素的自然顺序对指定 List 集合元素按升序排序- 字符串集合按首字母大小排序
sort(List, Comparator)
:根据指定的 Comparator 产生的顺序对 List 集合元素进行排序swap(List, int i, int j)
:进指定 List 集合中的 i 处元素和 j 处元素进行交换
12.8.3 查找替换
Object max(Collection)
:根据元素的自然顺序,返回给定集合中的最大元素Object max(Collection, Comparator)
:根据 Comparator 指定的顺序,返回给定集合中的最大元素Object min(Collection)
:根据元素的自然顺序,返回给定集合中的最小元素Object min(Collection, Comparator)
:根据 Comparator 指定的顺序,返回给定集合中的最小元素int frequency(Collextion, Object)
:返回指定集合中指定元素的出现次数void copy(List dest, List src)
:将 src 中的内容复制到 dest 中boolean replaceAll(List list, Object oldVal, Object new Val)
:使用新值 oldVal 替换 list 的所有旧值 oldVal
12.8.4 示例
public class CollectionsDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("tom");
list.add("smith");
list.add("king");
list.add("milan");
list.add("tom");
System.out.println("原集合: "+list);//[tom, smith, king, milan, tom]
// reverse(List):反转 List 中元素的顺序
Collections.reverse(list);
System.out.println("reverse: "+list);//[tom, milan, king, smith, tom]
// shuffle(List):对 List 集合元素进行随机排序
for (int i = 0; i < 5; i++) {
Collections.shuffle(list);
System.out.println("第" + i + "次shuffle: " + list);
}
/**
* [milan, tom, king, tom, smith]
* [tom, king, tom, smith, milan]
* [tom, tom, milan, smith, king]
* [tom, milan, tom, smith, king]
* [king, smith, tom, milan, tom]
*/
// sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
Collections.sort(list);
System.out.println("sort: " + list);//[king, milan, smith, tom, tom]
// sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
});
System.out.println("按字符串长度排序: " + list);//[tom, tom, king, milan, smith]
// swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Collections.swap(list, 0, 4);
System.out.println("swap后: " + list);//[smith, tom, king, milan, tom]
//Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
System.out.println("max = " + Collections.max(list));//tom
//Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
System.out.println("字符串最长的: " + Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
}));//smith
//int frequency(Collection,Object):返回指定集合中指定元素的出现次数
System.out.println("tom出现次数: " + Collections.frequency(list, "tom"));//2
//void copy(List dest,List src):将 src 中的内容复制到 dest
ArrayList dest = new ArrayList();
//此时dest大小小于list,直接copy将抛索引越界异常
//为了完成一个完整拷贝,我们需要先给 dest 赋值,大小和 list.size()一样
for(int i = 0; i < list.size(); i++) {
dest.add("");
}
Collections.copy(dest, list);
System.out.println("dest = " + dest);//[smith, tom, king, milan, tom]
//boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
Collections.replaceAll(list, "tom", "汤姆");
System.out.println("将tom替换为汤姆: " + list);//[smith, 汤姆, king, milan, 汤姆]
}
}
12.9 练习
-
/** * 按要求实现: * (1)封装一个新闻类,包含标题和内容属性,提供get,set方法,重写toString方法,打印对象 * 时只打印标题 * (2)只提供一个带参数的构造器,实例化对象时,只初始化标题:并且实例化两个对象: * 新闻一:新冠确诊病例超干万,数百万印度教信徒赴恒河“圣浴”引民众担忧 * 新闻二:男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生 * (3)将新闻对象添加到ArrayList集合中,并且进行倒序遍历; * (4)在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加"..." * (5)在控制台打印遍历出经过处理的新闻标题 */ public class Exer12_1 { public static void main(String[] args) { //实例化两个对象 News news1 = new News("新冠确诊病例超干万,数百万印度教信徒赴恒河“圣浴”引民众担忧"); News news2 = new News("男子突然想起2个月前钓的鱼还在网兜里,捞起一看赶紧放生"); //将新闻对象添加到ArrayList集合中,并且进行倒序遍历 ArrayList list = new ArrayList(); list.add(news1); list.add(news2); int size = list.size(); //在遍历集合过程中,对新闻标题进行处理,超过15字的只保留前15个,然后在后边加"..." for (int i = size -1; i >= 0; i--) { System.out.println(handlingTitle((News) list.get(i))); } } public static String handlingTitle(News news) { String title = news.getTitle(); if (title == null || title.length() == 0) { return ""; } if (title.length() < 16) { return title; } else { return title.substring(0, 15) + "..."; } } } class News { private String title; private String content; public News(String title) { this.title = title; } @Override public String toString() { return "NEWS{" + "title='" + title + '\'' + '}'; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
-
/** * 按要求完成下列任务 * 1)使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员 * 工的姓名和工资,存入数据如下: jack-650元; tom-1200元: smith-2900元; * 2)将jack的工资更改为2600元 * 3)为所有员工工资加薪100元; * 4)遍历集合中所有的员工 * 5)遍历集合中所有的工资 */ public class Exer12_2 { public static void main(String[] args) { //1)使用HashMap类实例化一个Map类型的对象m,键(String)和值(int)分别用于存储员 //工的姓名和工资,存入数据如下: jack-650元; tom-1200元: smith-2900元; Map m = new HashMap(); m.put("jack", 650); m.put("tom", 1200); m.put("smith", 2900); //2)将jack的工资更改为2600元 m.put("jack", 2600); //3)为所有员工工资加薪100元; Set keySet = m.keySet(); for (Object key : keySet) { m.put(key, (int)(m.get(key)) + 100); } //遍历集合中所有的员工 Set entrySet = m.entrySet(); for (Object o : entrySet) { Map.Entry entry = (Map.Entry) o; System.out.println(entry.getKey() + "-" +entry.getValue()); } //5)遍历集合中所有的工资 Collection values = m.values(); Iterator iterator = values.iterator(); while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next); } } }
-
试分析HashSet和TreeSet分别如何实现去重的
- HashSet 的去重机制:hashCode() + equals(),底层先通过存入对象,进行运算得到一个 hash 值,通过 hash 值得到对应的索引,如果发现 table 索引所在的位置没有数据,就直接存放;如果有数据,就进行 equals 比较[遍历比较],如果比较后不相同,就加入,否则就不加入。
- TreeSet 的去重机制:如果你传入了一个 Comparator 匿名对象,就使用实现的 compare 去重,如果方法返回0,就认为是相同的元素/数据,就不添加;如果你没有传入一个 Comparator 匿名对象,则以你添加的对象实现的Compareable接口的compareTo去重。
-
下面代码运行会不会抛出异常,并从源码层面说明原因(考察 读源码+接口编程+动态绑定)
public class Exer12_3 { public static void main(String[] args) { TreeSet treeSet = new TreeSet(); //add方法,因为TreeSet构造器没有传入Comparator接口的匿名内部类 //所以在底层compare(key, key);时, //(Comparable<? super K>)k1会将key转换为Comparable类型 //即要将Person对象转成Comparable类型 //又因person没有实现Comparable接口,所以抛类型转换异常 treeSet.add(new Person());//Person cannot be cast to java.lang.Comparable } } class Person {}
-
/** * 下面代码输出什么? * 已知:Person类按照id和name重写了hashCode和equals方法 */ public class Exer12_3 { public static void main(String[] args) { HashSet set = new HashSet(); Person p1 = new Person(1001, "AA"); Person p2 = new Person(1002, "BB"); set.add(p1); set.add(p2); p1.name = "CC";//修改后,p1的hash值改变 set.remove(p1);//按照修改后的hash值找不到set中的p1,删除失败 System.out.println(set);//2个 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}] //Person(1001, "CC")的hash值对应的位置还未存放元素,可以添加成功 set.add(new Person(1001, "CC")); System.out.println(set);//3个 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}] //Person(1001, "AA")的hash对应的位置已有元素Person(1001, "CC") //比较内容不相同,将Person(1001, "AA")添加到Person(1001, "CC")后边 set.add(new Person(1001, "AA")); System.out.println(set);//4个 [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}] } } class Person { private int id; public String name; public Person(int id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return id == person.id && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(id, name); } }