目录
一.双列集合(键值一一对应)与HashMap
1.键值对对象在Java中的名字与特点? Entry对象,键不可以重复,值可以重复 2.HashMap的特点?这些特点由什么决定?HashMap底层是什么结构? HashMap特点:无序,不重复,无索引,由键决定 HashSet与HashMap底层结构都是一样的,都是哈希表结构 3.Map(双列集合顶层接口)常见方法有哪些(增删查)? (1)增:非add,是put:HashMap加入元素与HashSet有何不同? (2)删:remove,clear,remove方法根据什么删除键值对呢? (3)查:containsKey,containsValue,isEmpty,size 4.当数组长度超过8并且数组长度大于等于64,那么就把链表转换成红黑树
public class Demo241 {
public static void main(String[] args) {
//用子类创建对象
Map<String,Integer> map = new HashMap<>();
map.put("张三",23);
map.put("李四",24);
System.out.println("1.添加相同的键会修改键的值,返回被修改的值,没有修改就返回空。和set返回的true和false不一样,");
System.out.println(map.put("张三",24));
System.out.println(map.put("王五",25));
System.out.println(map);
System.out.println("2.删除:根据键删除");
map.remove("王五");
System.out.println(map);
System.out.println("3.查询:是否有key,value,规模与是否为空");
System.out.println(map.containsKey("李四"));
System.out.println(map.containsValue(24));
System.out.println(map.size()+" "+map.isEmpty());
map.clear();
}
}
二.Map的三种遍历方式
Map下面的三种遍历方式具体如何实现? 1:根据取出键遍历 2:根据取出键值对遍历 3:使用forEach方法与BiConsumer遍历
public class Demo242 {
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<>();
map.put("张三",23);
map.put("李四",24);
map.put("王五",25);
System.out.println("1.获取Key的集合,再使用get(key)方法遍历");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key+"="+map.get(key));
}
System.out.println("2.获取所有键值对对象:放到一个集合当中,使用entry中的getKey与getValue遍历(Entry是Map的内部接口)");
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey()+"="+entry.getValue());
}
System.out.println("3.使用forEach方法遍历,接收2个对象,分别是Key和Value(底层依然是使用获取键值对的方式遍历)");
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String s, Integer integer) {}
});
map.forEach((str,i)->System.out.println(str+"="+i));
}
}
三.案例:景点计数
public class Demo243 {
public static void main(String[] args) {
String[] arr = {"A","B","C","D"};
String[] list = new String[80];
Random random = new Random();
for (int i = 0; i < list.length; i++)
list[i]=arr[random.nextInt(4)];
System.out.println("1.使用hashMap存储键(地点)值(统计次数)");
HashMap<String,Integer> hashMap = new HashMap<>();
for (String value : list) {
if (hashMap.containsKey(value)) hashMap.put(value, hashMap.get(value) + 1);
else hashMap.put(value, 0);
}
System.out.println("2.记录max的值,防止同时出现两种或者以上景点都是票数最高的");
int max = 0;
System.out.println("3.统计最大次数");
System.out.println("lambda相当于内部类的简写,调用外部类变量,必须是final类型的,故此处无法使用lambda遍历。");
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
int count = entry.getValue();
if (max<count)max=count;
}
hashMap.forEach((s,i)-> System.out.println(s+" "+i));
System.out.println("4.根据Max遍历Map");
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
int count = entry.getValue();
if (max==count) System.out.print(entry.getKey()+" ");
}
}
}
四.LinkedHashMap&TreeMap
1.LinkedHashMap有序,如果后加入的元素替换掉了前面的元素,后加入元素是新建立一个元素挂在链表上还是替代原先元素的位置呢? 2.在TreeMap里面传递自定义对象需要做什么准备?
下列代码中Student类为普通JavaBean类
public class Demo244 {
public static void main(String[] args) {
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("张三", 23);
linkedHashMap.put("李四", 24);
linkedHashMap.put("王五", 25);
linkedHashMap.put("张三", 23);
System.out.println("1.后加入的元素:如果和前面的元素键相同,依旧是替代值,而不是继续挂在后面");
System.out.println(linkedHashMap);
System.out.println("2.使用TreeMap加入自定义元素依旧要传递比较器或者在类里面实现比较接口");
//按年龄排序,否则按照姓名
TreeMap<Student, Integer> treeMap = new TreeMap<>(new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int result = o1.getAge() - o2.getAge();
if (result==0)result =o1.getName().compareTo(o2.getName());
return result;
}
});
Student s1 = new Student("zhangsan", 23);
Student s2 = new Student("wangwu", 25);
Student s3 = new Student("lisi", 24);
treeMap.put(s1, 1);
treeMap.put(s2, 1);
treeMap.put(s3, 1);
System.out.println(treeMap);
}
}
五.案例:统计字符个数
public class Demo245 {
public static void main(String[] args) {
System.out.println("如果要统计对象数量不确定,可以利用map去统计");
String str = "abesaebaebaebsaebsabeabsbaebsebasbeasbase哈哈哈哈哈哈哈哈哈哈哈哈哈";
TreeMap<Character,Integer> treeMap = new TreeMap<>();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if(treeMap.containsKey(c))treeMap.put(c,treeMap.get(c)+1);
else treeMap.put(c,1);
}
treeMap.forEach((c,i)-> System.out.println(c+" "+i));
}
}
六.HashMap底层源码与hashMap不覆盖添加
HashMap底层 内部类Node实现了Map里面的Entry接口 Node成员变量: 1.hash:通过键计算出hash值,只能被赋值一次 2.键(只能被赋值一次),值 3.Node<> next,记录下一个节点的位置 红黑树的话就换了一个节点内部类,叫TreeNode 继承于LinkedHashMap的Entry,这个Entry又继承与HashMap里面的Node 属性有父,左,右,red记录红黑。 由于继承,也有Node里面的变量 HashMap里面的成员变量: table表示数组,数组里面去装Node对象 DEFAULT_INITIAL_CAPACITY 1 << 4(1*2^4)初始容量是16 DEFAULT_LOAD_FACTOR = 0.75f 扩容时机,数组容量超过数组的1/4就扩容 空参构造方法public HashMap()中只加载了扩容时机,传入别的参数可以设置初始容量与扩容时机 从开始添加第一个元素的时候public V put(K key, V value)开始创建数组 return putVal(hash(key), key, value, false, true); 第一个参数是hash(key)是键的hash值,具体如何运算不看(最终目的还是为了让哈希后的结果更均匀的分部,减少哈希碰撞,提升hashmap的运行效率) 第四个参数代表的是onlyifAbsent:表示当前数据是否保留,false则不保留,能够覆盖,true则是保留,不能够被覆盖 点开putVal: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 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; } 方法中的成员变量: 参数tab:定义局部变量记录哈希表中数组的地址值,被赋值为table (方法在栈中,原来的table变量定义在堆中,在这里是为了避免反复调用堆浪费时间) 参数:Node<K,V> p:临时变量,记录键值对象的地址值 n:当前数组长度。i:表示索引 加入第一个元素的时候: if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; 如果没有创建数组或者数组长度为0:就调用resize方法创建新数组(长度为16,加载因子为0.75),返回给tab,并且赋值数组长度给n 在resize方法中:定义了oldTable,oldCap,oldThr, 分别为数组,规模(数组长度),阈值(扩容时机),数组一开始为空时,执行下列代码: newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); threshold = newThr; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; return newTab; 如果不是第一次添加数据,就会看数组中的元素是否达到扩容条件 如果达到了扩容条件:底层会把数组扩容*2,并把数据“全部转移”到新的哈希表中:(和ArrayList很像) (链表和数组想要赋值到新数组里需要重新进行哈希运算) 再回到putVal方法中: if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 这里给i赋值了hash值,给p赋值了数组中这个哈希值对应的下标的元素 如果它是空,直接给这个数组中的元素赋值就可以了。 newNode方法就是创建新一个节点:方法体就一行,这里newNode的时候,next为空 return new Node<>(hash, key, value, next); 最后再执行: ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; modCount是与并发修改异常相关的,可以先不看 afterNodeInsertion(evict)与linkedHashmap有关,先不看 如果size自增后大于了阈值,才执行resize方法 最后返回空:表示当前没有覆盖任何元素 当数组不为空:键不重复,但是形成链表的情况: Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 前面两个条件都不会成立,这里的p被赋值给已经存在的节点,执行下列代码: else { //这里定义了一个节点和一个键 Node<K,V> e; K k; //如果数组中键值对的哈希值是要加入的这个键值对的哈希值,这一段if不会执行 if (p.hash == hash && //数组中的键值对的键与要加入的这个键值对相等(一种是等于,另一种是equals方法) ((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); //下面这个是判断长度(循环次数+1)是否大于等于8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //如果大于等于8,就会执行变成红黑树的方法,当然在里面也会继续判断数组长度是否大于64 treeifyBin(tab, hash); break; } //如果在遍历链表的时候被相同哈希值,并且也是相同对象的逮到了,也会停下来,别遍历了 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //把遍历的节点给下一个 p = e; } } //这里由于e被赋值为空不会执行 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //到了最后,增加size,返回空 ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; 当数组不为空:键重复,元素覆盖的情况:这里也是哈希碰撞的情况:加入有元素的数组的哪个链表中: else { Node<K,V> e; K k; //这里就是还没有遍历,第一个节点就被逮到是相同的键了,直接跳到后面的if判断 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判断 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; } } 在HashMap中,默认是使用的Hash值来构建的红黑树,所以不需要实现comparable接口或者传递comparator对象 上面其实也没讲如何扩容,或者是说如何转换成tree,这里先不看咯,往后走,读TreeMap
public class Demo246 {
public static void main(String[] args) {
HashMap<String, Integer> hashMap = new HashMap<>();
System.out.println("使用putIfAbsent,键重复不会被覆盖");
//底层与put方法不同的就是传递的第三个参数是true
//布尔类型变量控制两种结果,int类型变量控制三种或者以上结果(如树添加节点)
hashMap.putIfAbsent("aaa",1);
hashMap.putIfAbsent("aaa",2);
System.out.println(hashMap);
}
}
七.TreeMap底层源码
TreeMap属性:comparator,root(根节点),size 空参构造只是把比较器赋值了NULL 带参构造就是传进去比较器,赋值给自己的比较器 在treemap中,entry继承于Map里面的entry 属性:键、值、左、右、父、颜色。在Java中给颜色设置了true false,提高了阅读性 在put方法中:调用三个参数的put方法,第三个参数代表代替老元素 public V put(K key, V value) { return put(key, value, true); } 在第二个put里面:执行下列代码:创建新的节点,返回为空 //t为根节点的地址值 Entry<K,V> t = root; if (t == null) { addEntryToEmptyMap(key, value); return null; } private void addEntryToEmptyMap(K key, V value) { compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; } 创建第二个节点时: //两个键比较的结果 int cmp; //当前添加节点的父节点 Entry<K,V> parent; 记录比较器,分有比较器和无比较器两条不同路径(其实两种情况都差不多,不同的地方只有一点点) Comparator<? super K> cpr = comparator; //传递了比较器进来 if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else { V oldValue = t.value; if (replaceOld || oldValue == null) { t.value = value; } return oldValue; } } while (t != null); //没有传递比较器进来 } else { Objects.requireNonNull(key); //这里传递一个比较规则(在Key的类或者其子类里面写的) @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; //这里的循环是找父节点(parent)和与父节点的大小比较(cmp) do { //先把根节点当作当前节点的父节点 parent = t; 比较两个键的结果,拿当前节点和根节点比较 cmp = k.compareTo(t.key); //根据比较结果到根节点的左右找 if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else { //用新值代替老值,返回被替代的老值 V oldValue = t.value; if (replaceOld || oldValue == null) { t.value = value; } return oldValue; } } while (t != null); } addEntry(key, value, parent, cmp < 0); return null; 在addEntry方法中:添加节点指定父节点,父节点指定孩子 private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) { //new一个新节点,把父节点的left记录为当前节点 Entry<K,V> e = new Entry<>(key, value, parent); if (addToLeft) parent.left = e; else parent.right = e; //添加完节点之后的调整:红黑规则 fixAfterInsertion(e); size++; modCount++; } 在这个方法中满足红黑规则 private void fixAfterInsertion(Entry<K,V> x) { //第一步就先设置了添加的节点为红色 x.color = RED; //X为当前要添加的节点,节点不为空,不是根节点,父节点是红 while (x != null && x != root && x.parent.color == RED) { //以下的父节点全是红色 //自己的父亲是爷爷的左孩子:这样就知道了叔叔是右孩子 if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { //获取叔叔 Entry<K,V> y = rightOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { //叔叔节点为红色,叔叔父亲全部变黑,爷爷变红,再把当前节点变为爷爷 //注意到外层的大循环:爷爷要是一个根节点才能停下来哦。 setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { //叔叔节点黑色,父亲节点红色,先看自己是不是右节点,如果是,那就左旋父亲 if (x == rightOf(parentOf(x))) { //左旋之后父子身份交换(他们两个都是红) x = parentOf(x); rotateLeft(x); } //把父亲变为黑色,爷爷变成红色,右旋爷爷(让分布变得更加均匀) setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateRight(parentOf(parentOf(x))); } //自己的父亲是爷爷的右孩子:这样就知道了叔叔是左孩子 } else { Entry<K,V> y = leftOf(parentOf(parentOf(x))); if (colorOf(y) == RED) { setColor(parentOf(x), BLACK); setColor(y, BLACK); setColor(parentOf(parentOf(x)), RED); x = parentOf(parentOf(x)); } else { if (x == leftOf(parentOf(x))) { x = parentOf(x); rotateRight(x); } setColor(parentOf(x), BLACK); setColor(parentOf(parentOf(x)), RED); rotateLeft(parentOf(parentOf(x))); } } } //不管怎么样,最后都得把祖父设置为黑色 root.color = BLACK; } 再多看看旋转源码(左旋): private void rotateLeft(Entry<K,V> p) { p为要左旋转的节点 if (p != null) { //设置r为当旋转节点的右孩子,r为上位者,p为下位者 //上位者需要把左孩子给下位者当作右孩子 Entry<K,V> r = p.right; //下面是交换孩子环节,父亲认孩子 //旋转节点右孩子的左孩子 给 旋转节点右孩子 p.right = r.left; //下面是孩子认父亲环节 //如果当 旋转节点右孩子的左孩子 不为空 就设置它的父母为旋转节点,到这里就完成了孩子交换 if (r.left != null) r.left.parent = p; //下面是父亲认爷爷环节 //把旋转节点的父母 给旋转节点的右孩子的父母 r.parent = p.parent; //如果旋转节点没父母,就设置旋转节点右孩子为根节点 if (p.parent == null) root = r; //下面是爷爷认父亲环节 //如果旋转节是左孩子 else if (p.parent.left == p) p.parent.left = r; else //旋转节点是右孩子 p.parent.right = r; //最后完成身份交换 r.left = p; p.parent = r; } }