JavaSE进阶八 集合二 Map接口
1,Map接口
- Map和Collection没有继承关系。
- Map集合以key和value的方式存储数据:键值对
- key和value都是引用数据类型。
- key和value都是存储对象的内存地址。
- key起主导地位,value是key的一个附属品。
1.1 java.util.Map接口中常用的方法:
V put(K key, V value); // 向map集合中添加键值对
V get(Object key); // 通过key获取value
int size(); // 获取map集合中键值对的个数
V remove(Object key); // 通过key删除键值对
boolean containsKey(Object key); // 判断map中是否包含某个key
boolean containsValue(Object value);// 判断map中是否包含某个value
void clear(); // 清空map集合
boolean isEmpty(); // 判断map集合中的元素个数是否为0
Collection<V> values(); // 获取集合中所有的value
代码示例
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class MapTest01 {
public static void main(String[] args) {
// 创建map集合对象
Map<Integer,String> map = new HashMap<>();
// 向map集合中添加键值对
map.put(1,"zhangshan");
map.put(2,"lisi");
map.put(3,"wangwu");
// 通过key获取value
String s = map.get(2);
System.out.println(s);
// 获取键值对的数量
System.out.println("键值对的数量:" + map.size());
// 通过key删除key-value
map.remove(2);
// 获取键值对的数量
System.out.println("键值对的数量:" + map.size());
// 判断是否包含某个key
// contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重新equals方法。
System.out.println(map.containsKey(1)); // true
// 判断是否包含某个value
System.out.println(map.containsValue("wangwu")); // true
// 获取集合中所有的value
Collection<String> values = map.values();
for (String ss : values){
System.out.println(ss);
}
// 清空集合
map.clear();
System.out.println("键值对的数量:" + map.size());
// 判断是否为空
System.out.println(map.isEmpty());// true
}
}
1.2 Map集合的遍历
java.util.Map接口中常用的方法:
Set<Map.Entry<K, V>> entrySet(); // 把Map集合直接全部转换成Set集合,Set集合中元素的类型是:Map,Entry。
Set<K> keySet(); // 获取集合所有的key
代码示例
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class MapTest02 {
public static void main(String[] args) {
// 创建Map集合对象
Map<Integer,String> map = new HashMap<>();
// 添加元素
map.put(1,"zhangsan");
map.put(2,"lisi");
map.put(3,"wangwu");
map.put(4,"zhaosi");
// 遍历Map集合
System.out.println("=================================================");
// 方式一:先获取所有的key,通过遍历所有的key,来遍历value
Set<Integer> set = map.keySet();
// 使用迭代器遍历
Iterator<Integer> it = set.iterator();
while (it.hasNext()){
Integer key = it.next();
String value = map.get(key);
System.out.println(key + "=" + value);
}
// 使用增强for循环遍历
for (Integer key : set){
String value = map.get(key);
System.out.println(key + "=" + value);
}
System.out.println("=================================================");
// 方式一:使用 Set<Map.Entry<K, V>> entrySet();
// 把集合直接全部转换成Set集合
// Set集合中元素的类型是:Map.Entry
Set<Map.Entry<Integer,String>> set1 = map.entrySet();
Iterator<Map.Entry<Integer,String>> it1 = set1.iterator();
// 使用迭代器遍历
while (it1.hasNext()){
Map.Entry<Integer,String> entry = it1.next();
System.out.println(entry);
System.out.println("key:" + entry.getKey());
System.out.println("value:" + entry.getValue());
}
// 使用增强for循环遍历
for (Map.Entry<Integer,String> node : set1){
System.out.println(node);
System.out.println("key:" + node.getKey());
System.out.println("value:" + node.getValue());
}
}
}
2,HashMap集合
-
HashMap集合底层是哈希表/散列表的数据结构。
-
HashMap集合的默认初始化容量是16,默认加载因子是0.75。
- 加载因子:当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。
- 重点:HashMap集合的默认初始化容量必须是2的倍数,这是官方推荐的;是为了达到散列均匀,提高HashMap集合的存取效率。
-
哈希表示一个怎么样的数据结构
- 哈希表是一个数组和单向链表的结合体。
- 数组:在查询方面效率较高,随机增删方面效率很低。
- 单向链表:在随机增删方面效率很高,在查询方面效率很低。
- 哈希表将以上两种数据结构融合在一起,充分发挥它们各自的优点。
-
HashMap集合底层的源代码:
public class HashMap{ // HashMap底层实际上就是一个数组。(一维数组) Node<K,V>[] table; // 静态内部类 static class Node<K,V> implements Map.Entry<K,V> { final int hash; // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储数组的下标) final K key; // 存储到Map集合中的key V value; // 存储到Map集合中的value Node<K,V> next; // 下一个节点的内存地址。 } }
-
主要掌握(一定要掌握):
-
map.put(k,v)实现原理:
1,现将k,v封装到Node对象当中。 2,底层会调用k的hashCode()方法得出hash值,然后通过哈希函数/算法,将hash值转换成数组的下标, 下标位置上如果没有任何元素,就把Node添加到这个位置上,如果下标对应的位置上有链表,此时会拿着k 和链表上的每一个节点中的k进行equals,如果所有的equals方法返回都是false,那么这个新节点将被 添加到链表的末尾,如果其中有一个equals返回了true,那么这个节点的value将会被覆盖。
-
v = map.get(k)实现原理:
先调用k的hashCode()方法得出哈希值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个 位置上,如果这个位置上什么也没有,返回null;如果这个位置上有单向链表,那么会拿着参数k和单向链 表上的每个节点中的k进行equals,如果所以equals方法返回false,那么get方法返回null,只要有一 个节点的k和参数k equals的时候返回true;那么此时这个节点的value就是我们要找的value,get方 法最终返回这个要找得value。
-
-
HashMap集合的key部分特点:
- 无序,不可重复。
- 无序:不一定挂到哪个单向链表上。
- 不可重复:equals方法保证HashMap集合的key不可重复;如果key重复value覆盖。
-
哈希表HashMap使用不当时无法发挥性能
- 假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表,这种情况称为:散列分布不均匀。
- 什么是散列分布均匀?
- 假设有100个元素,10个单向链表,每个单向链表上有10个节点,这是最好的,是散列分布均匀的。
- 假设将所有的hashCode()方法返回值都设定为不一样的值可以吗?
- 不可以,这样会导致底层哈希表变成一维数组,没有链表的概念了;也是散列分布不均匀。
代码示例
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class HashMapTest01 {
public static void main(String[] args) {
// 测试HashMap集合key部分元素的特点
// Integer是key,hashCode()和equals()都重写了
Map<Integer,String> map = new HashMap<>();
map.put(11,"张三");
map.put(12,"李四");
map.put(13,"王二");
map.put(14,"麻子");
map.put(14,"小红"); // key值重复,value会自动覆盖
System.out.println(map.size());
// 遍历集合
Set<Map.Entry<Integer,String>> set = map.entrySet();
for (Map.Entry<Integer,String> entry : set){
System.out.println(entry);
System.out.println("key:" + entry.getKey());
System.out.println("value:" + entry.getValue());
}
}
}
3,HashSet集合
-
HashSet集合是一个哈希表数据结构。
-
HashSet集合初始化容量是16(初始化容量建议是2的倍数);扩容:原容量的2倍。
-
无序不可重复
- 1,存储顺序和取出顺序不同。
- 2,不可重复。
- 3,放到HashSet集合中的元素实际上是放到HashMap集合的key部分了。
-
向Map集合中存取元素时,先调用key的hashCode()方法,然后再调用equals方法;equals方法有可能调用,也有可能不调用。
-
拿put(k,v)举例,什么时候equals不调用?
- k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标;
如果下标位置上是null,equals方法不需要执行。
- k.hashCode()方法返回哈希值,哈希值经过哈希算法转换成数组下标;
-
拿get(k)举例,什么时候equals不调用?
- 如果单向链表上只有一个元素时,不需要调用equals方法。
-
-
重点:放在HashMap集合key部分元素(在HashSet集合中),以及放到HashSet集合中的元素;需要同时重写hashCode()和equals()方法。
代码示例
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class HashMapTest02 {
public static void main(String[] args) {
Student s1 = new Student("zhangsan");
Student s2 = new Student("zhangsan");
System.out.println(s1.equals(s2));// 重写equals方法之前:false 之后:true
System.out.println("s1的hashCode: " + s1.hashCode()); // 重写hashCode方法之前:2129789493 之后:-1432604525
System.out.println("s2的hashCode: " + s2.hashCode()); // 重写hashCode方法之前:1313922862 之后:-1432604525
//
Set<Student> students = new HashSet<>();
students.add(s1);
students.add(s2);
System.out.println(students.size()); // 重写hashCode方法之前:2 之后:1
}
}
class Student{
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
// 重写equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
// 重写hashCode方法
@Override
public int hashCode() {
return Objects.hash(name);
}
}
4,Hashtable集合
-
HashMap集合允许key为null,但是key值null只能有一个。
- HashMap的key和value都是可以为null的。
-
Hashtable的key可以为null吗?
- Hashtable的key和value都不能为null的。
-
Hashtable方法都带有synchronized,是线程安全的,
Hashtable对线程的处理导致效率较低,使用比较少(线程安全有其他的方案)。 -
Hashtable和HashMap一样,底层都是哈希表数据结构。
-
Hashtable的初始化容量是11,默认加载因子:0.75f。
-
Hashtable的扩容是:原容量 * 2 + 1
代码示例
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
public class HashMapTest03 {
public static void main(String[] args) {
Map map = new HashMap();
// HashMap集合允许key为null
map.put(null,null);
System.out.println(map.size());// 1
// key重复value被覆盖
map.put(null,"abc");
System.out.println(map.size());// 1
// 通过key获取value
System.out.println(map.get(null));// abc
// ==========================================================
Map map1 = new Hashtable();
map1.put(null,null); // 运行时会报空指针异常
}
}
5,了解Propertie属性对象
- 目前只需要掌握Properties属性对象的相关方法即可。
- Properties是一个Map集合,继承Hashtable;Properties的key值和value都是String类型。
- Properties被称为属性对象。
- Properties是线程安全的
代码示例
import java.util.Properties;
public class PropertiesTest01 {
public static void main(String[] args) {
// 创建一个Properties对象
Properties properties = new Properties();
// 添加存储元素
properties.setProperty("name","zhangsan");
properties.setProperty("password","123");
System.out.println(properties.size());
// 通过key获取value
System.out.println(properties.getProperty("name"));
System.out.println(properties.getProperty("password"));
}
}
6,TreeSet集合
- TreeSet集合底层实际上是一个TreeMap,TreeMap集合底层是一个二叉树。
- 放到TreeSet集合中的元素,等同于放到TreeMap集合的key部分。
- TreeSet集合存储元素特点:
- 1,无序不可重复,但存储的元素可以自动按照大小顺序排序!称为:可排序集合。
- 2,无序:指的是存进去的顺序和取出来的顺序不同,并且没有下标。
代码示例
import java.util.Set;
import java.util.TreeSet;
public class TreeSetTest01 {
public static void main(String[] args) {
// 创建一个TreeSet集合
TreeSet<String> set = new TreeSet<>();
set.add("a");
set.add("d");
set.add("c");
set.add("e");
set.add("c");
set.add("b");
System.out.println(set.size()); // 5 不可重复
for (String s : set){
System.out.println(s); // 输出会从小到大自动排序(升序)
}
}
}
6.1, 自平衡二叉树
-
TreeSet/TreeMap集合是自平衡二叉树,遵循左小右大原则排序。
-
遍历二叉树的时候有三种方式:
前序遍历:跟左右 中序遍历:左跟右 后序遍历:左右跟 前中后说的是"跟"的位置,跟在前面是前序,跟在中间是中序,跟在后面是后序。
-
TreeSet/TreeMap集合采用的是:中序遍历方式。
-
Iterator迭代器采用的是:中序遍历方式。
7, TreeSet集合元素的排序
- 放到TreeSet集合和TreeMap集合key部分的元素想要排序,有两种方式:
- 1,放在集合中的元素实现java.lang.Comparable接口。
- 2,在构造TreeSet或TreeMap集合的时候给它传一个比较器对象。
- Comparable和Comparator怎么选择?
- 当比较规则不会改变的时候,或当比较规则只有1个的时候,建议使用Comparable接口。
- 当比较规则有多个,并且需要在多个比较规则之间频繁切换,建议使用Comparator接口。(Comparator接口符合OCP原则)
7.1,第一种排序方式(Comparable接口)
自定义类型使用TreeSet集合进行排序,需要实现java.lang.Comparable接口;以及重写compareTo方法,进行规则比较。
import java.util.TreeSet;
public class TreeSetTest02 {
public static void main(String[] args) {
// 自定义类型使用TreeSet集合进行排序,需要重写compareTo方法,进行规则比较。
//
Customer c1 = new Customer(23);
Customer c2 = new Customer(20);
Customer c3 = new Customer(28);
Customer c4 = new Customer(25);
// 创建TreeSet集合
TreeSet<Customer> customers = new TreeSet<>();
//添加元素
customers.add(c1);
customers.add(c2);
customers.add(c3);
customers.add(c4);
// 遍历
for (Customer c : customers){
System.out.println(c);
}
}
}
// 放在TreeSet集合中的元素需要实现java.lang.Comparable接口
// 并且实现compareTo方法,equals可以不写。
class Customer implements Comparable<Customer> {
int age;
public Customer() {
}
public Customer(int age) {
this.age = age;
}
/*
重写compareTo方法
需要在这个方法中编写比较逻辑,或者说比较规则,按照什么进行比较!
k.compareTo(t.key)
拿参数k和集合中的每一个k进行比较,返回值:>0 =0 <0
比较规则由程序员指定:按年龄升序、降序等...
*/
@Override
public int compareTo(Customer o) {
return this.age - o.age;
}
// 重写toString方法
@Override
public String toString() {
return "Customer{" +
"age=" + age +
'}';
}
}
7.2,第二种排序方式(Comparator接口)
自定义类型使用TreeSet集合进行排序,需要实现java.util.Comparator接口;在构造TreeSet或TreeMap集合的时候给它传一个比较器对象。
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetTest03 {
public static void main(String[] args) {
// 创建TreeSet集合的时候,需要使用比较器
// TreeSet<WuGui> treeSet = new TreeSet<>(); //这样创建没有通过构造方法传递一个比较器进去是不行的。
// 使用匿名内部类实现比较器(类没有名字,直接new接口)
// TreeSet<WuGui> treeSet = new TreeSet<>(new Comparator<WuGui>() {
// @Override
// public int compare(WuGui o1, WuGui o2) {
// return o1.age - o2.age;
// }
// });
TreeSet<WuGui> treeSet = new TreeSet<>(new WuGuiComparator());
treeSet.add(new WuGui(1000));
treeSet.add(new WuGui(600));
treeSet.add(new WuGui(900));
treeSet.add(new WuGui(2000));
for (WuGui wg : treeSet){
System.out.println(wg);
}
}
}
class WuGui{
int age;
public WuGui(int age) {
this.age = age;
}
@Override
public String toString() {
return "WuGui{" +
"age=" + age +
'}';
}
}
// 单独定义一个比较器
// 实现java.util.Comparator接口。
class WuGuiComparator implements Comparator<WuGui>{
@Override
public int compare(WuGui o1, WuGui o2) {
// 比较规则:按年龄排序
return o1.age - o2.age;
}
}