目录
Set集合
7.1 Set子接口
Set子接口特点:无序、无下标、元素不可重复。
循环方式:forEach循环或者迭代(因为没有下标所以不能使用for循环)
经典面试题:
给定一个集合,如何快速将集合中所有重复的元素去掉?
将集合元素直接放入Set中,通过Set特性可以去重。
7.2 HashSet[重点]
基于HashCode实现元素不可重复,当存入对象的HashCode值相同时,在通过equals方法判断,如果判定重复则不可存入。
HashSet是无序的。
有序、无序、排序是什么意思?
有序:指存入元素时记得元素添加的顺序;
无序:指存入元素时不保留添加元素时的顺序;
排序:指添加后会按照给定的顺序(升序或降序)进行排列。
7.2.1 用法
HashSet去重时,通过HashCode和equals方法进行去重。(所以我们在使用HashSet时需要重写HashCode和equals方法以便得到我们想要的去重效果。)
HashSet 类位于 java.util 包中,使用前需要引入它,语法格式如下:
import java.util.HashSet;
HashSet<String> hashSet = new HashSet<String>();
hashSet.add("Hello");
hashSet.add("World");
hashSet.add("Hello");
hashSet.add("Chinese");
//我们添加了两次Hello,所以被判断为重复的元素,直接去重,看输出结果可知是无序的(不根据我们输入顺序来输出)
System.out.println(hashSet);//[Hello, Chinese, World
HashSet<String> hashSet1 = new HashSet<String>(hashSet);
System.out.println(hashSet1.equals(hashSet));//true
//HashSet的remove方法只能传入元素值进行删除,因为HashSet无序,返回值为Boolean类型
boolean remove = hashSet.remove("Hello");
System.out.println(hashSet);//[Chinese, World]
//元素个数
System.out.println(hashSet.size());//2
ArrayList<String> arrayList = new ArrayList<String>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");
arrayList.add("1");
System.out.println(arrayList);//[1, 2, 3, 1]
//去重ArrayList
HashSet<String> l = new HashSet<String>(arrayList);
System.out.println(l);//[1, 2, 3]
7.2.2 原理
HashSet底层实现依靠HashMap。
通过HashMap实现,先了解HashMap源码。、
HashSet去重其实是使用了HashMap中key值不能重复的原理实现的。
//定义了一个HashMap
private transient HashMap<E,Object> map;
//new了一个最小的对象
private static final Object PRESENT = new Object();
//HashSet无参构造方法,调用了HashMap的无参构造
public HashSet() {
map = new HashMap<>();
}
//HashSet有参构造指定容量,调用HashMap的有参构造指定容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//添加数据就是调用了map的put方法添加,key为添加的值无法重复,value为定义的最小对象。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//调用HashMap的remove方法
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
7.3 LinkedHashSet
链表实现的HashSet,按照链表进行存储,即可保留元素插入顺序。
用法与HashSet一致。
原理:
LinkedHashSet继承了HaashSet;
在HashSet基础上添加了链表结构,记住了添加顺序,所以是有序的。底层中使用LinkedHashMap实现。
7.4 TreeSet
会将添加的元素进行排序,使用Comparable比较,比较的元素需要实现Comparable接口,通过比较的方式来去重。
采用树型结构来存放元素。
遍历时采用中序遍历。先左->根->最后右
遍历方式还有左序、中序、右序遍历。[可自行了解]
八、Map体系集合
8.1 Map接口
映射。使用key-value(键值对)来存储数据。
key不可重复,value可重复。当key重复时,会覆盖之前的值。
常见方法:
v put(K key,V value):存入数据value,使用key作为名称;
get(K key):根据名称key查找value值;
keySet():得到所有的key名称的集合;
values():得到所有的value值的集合;
entrySet():得到所有键值对的集合。
8.2 HashMap[重点]
JDK 1.2添加,线程不安全,性能性对较好。
注意:允许null作为key和value。
jdk1.7使用数组+链表结构,既有数组优点(遍历块),又有链表优点(增删快)。
jdk1.8使用数组+链表+红黑树的结构。当链表到达一定长度就会转为红黑树提高效率。
8.2.1 常用方法
get()、size()、entrySet()
[注意]当添加对象相同时,希望其覆盖,需要重写equals和HashCode方法。
public class Demo {
public static void main(String[] args) {
HashMap<String,String> hashMap1 = new HashMap<String, String>();
HashMap<Student,String> hashMap2 = new HashMap<Student, String>();
//添加数据
hashMap1.put("1","张三");
hashMap1.put("2","李四");
hashMap1.put("3","王五");
hashMap1.put("1","赵六");
//因为key值相同,所以赵六覆盖张三
System.out.println(hashMap1);//{1=赵六, 2=李四, 3=王五}
//删除
hashMap1.remove("2");
System.out.println(hashMap1);//{1=赵六, 3=王五}
//修改,只需要添加相同的键修改不同的value即可完成修改
hashMap1.put("1","李七");
System.out.println(hashMap1);//{1=李七, 3=王五}
//查询,根据key获取value值
System.out.println(hashMap1.get("1"));//赵六
System.out.println(hashMap1.get("2"));//李四
//获取所有键的集合
System.out.println(hashMap1.keySet());//[1, 3]
//获取所有值的集合
System.out.println(hashMap1.values());//[李七, 王五]
//获取所有的键和值的集合
System.out.println(hashMap1.entrySet());//[1=李七, 3=王五]
//当对象为key值时,我们需要重写equals和HashCode方法达到覆盖目的,否则就不会覆盖
hashMap2.put(new Student(1,"张三"),"1");
hashMap2.put(new Student(2,"李四"),"2");
hashMap2.put(new Student(3,"王五"),"3");
hashMap2.put(new Student(1,"张三"),"3");
for (Student s:hashMap2.keySet() ) {
System.out.println("key值为:" + s.toString() + "value值为:" + hashMap2.get(s));
}
//key值为:Student{id=1, name='张三'}value值为:3
//key值为:Student{id=2, name='李四'}value值为:2
//key值为:Student{id=3, name='王五'}value值为:3
}
}
8.2.2 原理
基本原理:
HashMap的每个元素都会封装为单向链表的node节点。整个map都是一个链表型数组,称为table(hash表);
当无参构造创建HashMap时,会初始化loadFactor属性为默认的扩容因子0.75;
当有参构造指定容量创建HashMap时,会初始化loadFactor属性为默认的扩容因子0.75,且会计算得到一个>=指定容量最小2的n次方数的临界点(判断初始容量是否超出所规定的最大值,超出则将设置的最大值定为初始容量,然后根据初始容量计算出>=初始容量的最小 2 的n次方数并赋值给扩容临界点)。
扩容因子为0.75,即元素达到数组长度的3/4时,再添加新元素就会触发扩容,选择0.75的原因是在0.5到1之间,0.75是最合适能够乘以2的n次方得到整数的小数;
无参构造第一次添加元素会初始化扩容数组大小为16,再添加元素;
根据key的hashCode值低16位与高16位进行异或运算,再与数组长度求余数,得到元素应该存放在数组的索引位置;
当某个链表上的元素>=8个,且数组长度>=64时才会变树,如果<64则用扩容代替树化;
当数组扩容临界点>=设置的最大容量(2的30次方)时,直接指定临界点为int的最大值,不再进行扩容;
JDK1.7时采用头插法添加数据,JDK1.8开始采用尾插法添加数据;
每次扩容为原来数组的2倍,会将原来map中的内容放到新的map中,并重新排列;
//部分源码理解
//默认数组容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认扩容因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转为红黑树的阈值(临界点)
static final int TREEIFY_THRESHOLD = 8;
//红黑树转为链表的阈值(为什么两个阈值不一样,是为了防止在一个临界点一直转换)
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形容量(当链表达到阈值后还需要满足最小树形容量才会转为红黑树)
static final int MIN_TREEIFY_CAPACITY = 64;
//table表示的是hashmap中的数组,数组中存储的是Node节点
transient Node<K,V>[] table;
//键值对个数
transient int size;
//修改次数
transient int modCount;
//扩容的临界点(数组容量tab*扩容因子loadFactor计算而来)
int threshold;
//数组扩容的因子
final float loadFactor;
//定义的静态内部类,数组中每个位置存储的都是Node节点,节点中存储了相关属性(jdk1.7使用是Entry内部类)
//这里Node实现了Entry接口
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//Node节点中存储了(key的hash值,key,value,当前节点的下一节点地址)
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//此处省略部分源码,这里是实现Entry接口中的所有方法
}
//hashmap的无参构造方法
public HashMap() {
//将默认的扩容因子赋值给loadFactor
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//hashMap有参构造方法,传入参数为指定的初始容量
public HashMap(int initialCapacity) {
//调用另一个有参构造(传入定义的初始容量和默认的扩容因子0.75)
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//当指定容量创建hashMap会调用此方法或者有参构造指定初始容量和扩容因子会调用
public HashMap(int initialCapacity, float loadFactor) {
//判断设置的容量是否不规范(小于),不规范就报错(非法初始容量 Illegal initial capacity)
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判断设置的容量是否超出规定的最大容量(2的30次方)
if (initialCapacity > MAXIMUM_CAPACITY)
//超出的话就将初始容量规定为最大容量
initialCapacity = MAXIMUM_CAPACITY;
//判断扩容因子是否规范(<=0或不是数字),不规范就报错
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//将扩容因子赋值给对象属性,因为这里的loadFactor为形参
this.loadFactor = loadFactor;
//根据初始容量再进行计算,然后赋值给扩容临界点(>=给定容量的最小 2 次方数)
this.threshold = tableSizeFor(initialCapacity);
}
//该方法用于返回>=给定目标容量的最小 2 次方数。
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
//判断n是否为负数(当超出int最大值时成立)
//否则再判断是否超出我们设置的最大值(2的30次方),超出则返回最大值,没超出则返回n+1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//根据key值计算key的hash值方法
static final int hash(Object key) {
int h;
//判断key是否为null,是null则hashCode值为0
//不是null则计算key的hashCode值然后与key的hashCode值的高16位进行异或运算,得到一个计算后的值进行返回
//这里跟高16位进行异或运算可以减少hashCode值的冲突(1.7版本高16位并没有进行运算)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//添加键值对
public V put(K key, V value) {
//通过计算key的hash值作为参数,然后返回putVal方法的值
return putVal(hash(key), key, value, false, true);
}
//传入参数为(key的hash值,key,value,put时设置为false,put时设置为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;
//数组赋值给临时变量,并判断数组是否为空(无元素时)或数组长度==0
if ((tab = table) == null || (n = tab.length) == 0)
//对数组进行扩容,然后将扩容后的长度赋值给n
n = (tab = resize()).length;
//求出key的hash值在数组中存储的索引位置,并判断该位置是否为null(是否有元素),并将该索引位置的节点赋值给p
//这里的(n-1)&hash相当于(数组长度-1)%16得到余数,余数范围0-15
//这里印证了为什么数组长度必须是2的次方数,因为(2的次方数-1)&hash运算只能得到0-(数组长度-1)的值
if ((p = tab[i = (n - 1) & hash]) == null)
//该索引位置没有节点时,直接将添加的key,value设置为该索引位置第一个节点
tab[i] = newNode(hash, key, value, null);
//如果数组不是空且里面有元素节点时执行
else {
Node<K,V> e; K k;
//判断该索引位置的节点是否与添加key的hash值相等,且两个节点是否指向同一个内存地址并将key值赋值给k
//或者添加的key是否为null,且两个key值是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//若相等就表示key重复,将存在的节点赋值给e
e = p;
//判断该索引位置存在的节点是否为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//循环遍历该索引位置所有节点
for (int binCount = 0; ; ++binCount) {
//判断遍历的当前节点是否为尾节点(尾结点的下一个节点为null),当为null就是尾结点
if ((e = p.next) == null) {
//若是尾结点就让尾结点的next(指向下一节点地址)属性设置为添加的节点
p.next = newNode(hash, key, value, null);
//添加后判断当前索引位置的节点是否达到了变红黑树的阈值8
//因为节点是从0开始遍历的当遍历到7是就表示已经有了8个节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//超出变树阈值8数组当前索引位置要变树
treeifyBin(tab, hash);
//添加成功后就跳出循环
break;
}
//若不是尾结点就判断当前遍历节点是否与添加的节点相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//相等就跳出循环,表示key重复
break;
//切换到下一个节点
p = e;
}
}
//判断e是否为null(当e不为空时表示该索引位置中有重复的key)
if (e != null) { // existing mapping for key
//得到覆盖前的value值
V oldValue = e.value;
//当put进入的这个方法时,这里!false为true
if (!onlyIfAbsent || oldValue == null)
//添加的value覆盖之前的value
e.value = value;
//put时该方法为空
afterNodeAccess(e);
//后面代码不执行,返回值为覆盖前的value值
return oldValue;
}
}
//修改次数+1
++modCount;
//元素节点个数+1,判断是否到达数组扩容阈值
if (++size > threshold)
//扩容
resize();
put时该方法为空
afterNodeInsertion(evict);
//返回null
return null;
}
//扩容数组
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//判断数组是否为null
if (oldCap > 0) {
//判断扩容前数组容量是否达到了容量最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//达到则将int最大值设置为扩容临界点,然后返回扩容前数组
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果扩容前数组容量未达到容量最大值,然后将扩容前数组*2作为扩容后的数组容量
//扩容后的容量必须<最大容量,且扩容前容量>=默认数组容量16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容后的临界点为扩容前临界点*2
newThr = oldThr << 1; // double threshold
}
//判断是否有扩容临界点值是否初始化(有参构造会进入此条件,有参构造初始化了扩容临界点)
else if (oldThr > 0) // initial capacity was placed in threshold
//初始化容量为扩容临界点
newCap = oldThr;
//当无参构造创建hashMap时走这条分支
else { // zero initial threshold signifies using defaults
//初始化容量为默认的数组容量16
newCap = DEFAULT_INITIAL_CAPACITY;
//初始化扩容临界点为默认的扩容因子0.75*默认数组容量16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//判断扩容临界点是否为0(当走外层elseif分支时,并没有赋值newThr)
if (newThr == 0) {
//计算扩容临界点为数组容量*扩容因子
float ft = (float)newCap * loadFactor;
//判断容量和扩容临界点是否超出设置的最大值(2的30次方)
//没有超出则将临界点设置为新计算的扩容临界点,超出则将扩容临界点设置为int的最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//对象的扩容临界点属性初始化
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//根据前面计算出来的数组容量来new一个新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//扩容后的数组赋值给table属性
table = newTab;
if (oldTab != null) {
/**
此处代码省略,当扩容前数组不是空时执行此处代码
此处代码遍历数组中的所有节点,先判断索引位置是否为null,为null则表示没有元素直接遍历下一个索引;
若有元素则再判断该元素后面是否存在下一个元素,当没有下一个元素就表示该索引位置只有一个节点;根据节点的hash值与扩容后数组容量-1进行&运算得到新的索引值,将这一个节点移动到新数组的该索引位置。
若该索引位置不止一个节点时,再判断该索引位置是否为红黑树结构,红黑树移动过程如下:
1.红黑树移动到新数组的索引位置也最多有两种情况,跟链表一致分高位和低位;
2.遍历树的所有节点,分为高位红黑树和低位红黑树,并给高位和低位进行计数;
3.遍历结束后判断两个树中每个树的节点数量,当<=6,就将红黑树变为链表。红黑树变为链表只需要将树节点重新new成节点即可。
若有多个节点且也不是红黑树结构,则该节点就是链表,将链表中所有节点移动到新数组中,移动过程如下:
1.老数组的单向链表结构移动到新数组中,索引位置可能会有两种变化,一种是索引位置不变,一种是索引位置+老数组长度,如下:
例如:当老数组长度为16,且单向链表在5索引位置上,那么当他移动到新数组32中,会将节点的hash值&(新数组容量-1),其计算的索引位置只与后5位二进制有关:
假如索引位置为5的hash值有53(110101)和21(10101),这两个数&(16-1)都为5,所以在长度为16的数组中索引位置为5,当此时扩容后需要移动到新数组容量为32中,那么53和21&(32-1)的结果有两种,一个是索引值为21,一个是索引值为5,其中我们将索引值为21的称为高位索引,索引值为5称为低位索引。
2.遍历一个索引位置的所有节点,利用hash值&(新数组容量-1)计算新的索引值,根据计算的新索引值最多可将一个链表分为两个链表(高位链表和低位链表),两个链表中都有头结点和尾结点,再根据尾结点判断链表是否为null,若是null则不用放到新数组中,若不是空则将尾结点的next属性置为null,然后将链表存入新数组对应的索引位置中。
*/
}
//返回扩容后的数组
return newTab;
}
//删除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//判断数组是否为null,数组长度是否大于0,并根据key的hash值来计算索引值获取第一个节点,判断该所索引值是否存在第一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//判断删除的节点是否为该索引位置第一个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//判断该索引位置是否是树结构
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//索引位置是链表结构
else {
//循环遍历判断是否与删除key相等
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
//p中存放当前遍历节点的上一节点,因为当p的下一节点为要删除的key时,并不会执行到这一步,在上一步就break,而node中存放的是要删除的节点
p = e;
} while ((e = e.next) != null);
}
}
//若链表或树中找到与删除key相同的节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//判断是否为树节点
if (node instanceof TreeNode)
//删除树节点后还会判断该索引位置的节点数是否<=6,若小于则变链表
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//当删除的节点是索引位置第一个节点时,将删除节点的下一节点设置为第一个节点
else if (node == p)
tab[index] = node.next;
else
//当删除节点不是第一个节点时,将删除节点的下一节点赋值p的next属性
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
//返回值为要删除的节点
return node;
}
}
return null;
}
//查询
public V get(Object key) {
Node<K,V> e;
//根据查询key的hash值和key来获取数组中的节点,并判断数组中是否存在查询key,存在则返回该节点的value,不存在就返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判断数组是否为null,数组长度是否大于0,并根据key的hash值来计算索引值获取第一个节点,判断该所索引值是否存在第一个节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断该索引的第一个节点的hash和查询key的hash是否一致,是否和查询key指向同一个内存地址,是否等于查询的key值
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//若都符合说明key相同
return first;
//判断该索引位置是否只有一个节点
if ((e = first.next) != null) {
//判断该索引位置保存的是否为树
if (first instanceof TreeNode)
//如果为树,就在树中查询
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//如果是链表就循环遍历比较是否相同
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//相同则返回该节点
return e;
} while ((e = e.next) != null);
}
}
//返回空
return null;
}
8.3 Hashtable[不推荐]
Hashtable是在jdk1.0中,线程安全,性能不高。
用法与HashMap相同;
若需要使用安全的map推荐使用ConcurrentHashMap。
经典面试题:
HashMap与Hashtable的区别?
Hashtable是jdk1.0中,线程安全,性能不高;而HashMap是jdk1.2中添加,线程不安全,但性能相对较高;
Hashtable中key和value都不能为null(空指针异常),HashMap中key和value都能为null;
8.4 Properties
特点:
继承自Hashtable
主要用来加载配置文件,一般由服务器执行,所以可以不用考虑多线程性能问题;
主要对key和value进行限定了类型为String;
添加了直接加载文件流的方法;
8.5 TreeMap
通过key进行排序,需要实现Comparable接口。