Java 中的 Map 与 Set 集合

1. Map 和 Set 是什么?

1.1 概念

  在Java中,MapSet都是接口,是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。Map的实例化子类有TreeMapHashMap等,Set的实例化子类有TreeSetHashSet

  它们的模型:一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种,纯 key 模型Key-Value 模型

  Map中存储的是key-value的键值对,Set中只存储了Key,并且每个key都是唯一的,不能重复。

2.Map 接口的使用

  在Java中,Map接口的实现类有:TreeMap、HashMap

  Map中提供了一些方法的规范,下面是一些常用的:

方法描述
void clear()从Map中删除所有的键值对。
boolean containsKey(Object key)判断Map中是否包含指定的键。
boolean containsValue(Object value)判断Map中是否包含指定的值。
Set<Map.Entry<K, V>> entrySet()返回Map中包含的所有键值对的Set集合。
V get(Object key)返回与指定键相关联的值。如果Map中不包含该键,则返回null。
boolean isEmpty()判断Map是否为空。
Set<K> keySet()返回Map中包含的所有键的Set集合。
V put(K key, V value)将指定的键值对添加到Map中。如果Map中已经包含了该键,则使用新值替换旧值,并返回旧值。
void putAll(Map<? extends K, ? extends V> m)将指定Map中的所有键值都添加到当前Map中。
V remove(Object key)从Map中删除与指定键相关联的键值对。如果Map中不包含该键,则返回null。
int size()返回Map中键值对的数量。
Collection<V> values()返回Map中包含的所有值的Collection集合。
V getOrDefault(Object key, V defaultValue)返回 key 对应的 value,key 不存在,返回默认值
  1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
  2. Map中存放键值对的Key是唯一的,value是可以重复的。
  3. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
  4. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
  5. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
  6. TreeMap中的key不能为null,HashMap中的key可以为null。

2.1 TreeMap

  在Java中,TreeMap是基于红黑树实现的一种有序Map数据结构。红黑树是一种自平衡的二叉搜索树,它可以保证插入、删除、查找等操作的最坏情况时间复杂度为O(log n),在TreeMapkey不能为null。 (二叉搜索树 的基本操作 - 掘金 (juejin.cn))

(1)TreeMap 的常见构造方法

构造方法描述
TreeMap()创建一个空的TreeMap。
TreeMap(Comparator<? super K> comparator)创建一个空的TreeMap,使用指定的比较器对键进行排序。
TreeMap(Map<? extends K, ? extends V> m)创建一个包含指定Map中的所有键值对的TreeMap。

  由于TreeMap底层是用搜索树实现的,所以TreeMap里元素的 key 必须是可比较的(根据key来比较,生成搜索树),不然要报错,这时候就需要传一个比较器。

class Student{
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" + "name=" + name + ", age=" + age + '}';
    }
}

public class Main {
    public static void main(String[] args) {

        //因为 Student 不可比较,所以传入一个比较器
        Map<Student,Integer> map = new TreeMap<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                //比较名字
                return o1.name.compareTo(o2.name);
            }
        });

        map.put(new Student("小明",20),1);
        map.put(new Student("张三",20),2);
        map.put(new Student("王五",20),3);
        map.put(new Student("李四",20),4);

        System.out.println(map);//TreeMap重写了toString()方法,可以直接打印。
    }
}

结果:{Student{name=小明, age=20}=1, Student{name=张三, age=20}=2, Student{name=李四, age=20}=4, Student{name=王五, age=20}=3}
复制代码

  还有另一种方式,就是 Student 实现Comparable接口重写compareTo()方法。

class Student implements Comparable<Student>{
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ", age=" + age + '}';
    }

    @Override
    public int compareTo(Student o) {
        return this.name.compareTo(o.name);
    }
}
复制代码

(2)TreeMap 方法的演示

  TreeMap的常见方法都是重写了Map接口的方法,这里演示几个比较有趣的方法。

  1. getOrDefault() 方法。
public static void main(String[] args) {
    Map<String,Integer> map = new TreeMap<>();
    map.put("小明",20);
    map.put("张三",19);
    map.put("王五",18);

    //返回 key 对应的 value,key 不存在,返回默认值
    int n = map.getOrDefault("李四",1000);
    System.out.println(n);
}

结果:1000
复制代码
  1. keySet() 方法 与 values() 方法
public static void main(String[] args) {
    Map<String,Integer> map = new TreeMap<>();
    map.put("小明",20);
    map.put("张三",19);
    map.put("王五",20);

    //返回所有 key 的不重复集合
    Set<String> set = map.keySet();
    System.out.println(set);

    //返回所有 value 的可重复集合
    Collection<Integer> collection = map.values();
    System.out.println(collection);
}

结果:
[小明, 张三, 王五]
[20, 19, 20]
复制代码

(4)Map 的遍历问题:Map.Entry<K, V>

  Map.Entry<K, V> Map内部实现的用来存放<key, value>键值对映射关系的内部类,这就好比链表中的Node一样,该内部类中主要提供了<key, value>的获取,value的设置以及Key的比较方式。

方法解释
K getKey()返回 entry 中的 key
V getValue()返回 entry 中的 value
V setValue(V value)将键值对中的value替换为指定value

  当我们遍历Map时,需要注意的是Map中存储的是键值对(Entry),因此我们需要先获取Map中的所有Entry,再逐个遍历每个Entry的键和值。

public static void main(String[] args) {
    Map<String,Integer> map = new TreeMap<>();
    map.put("小明",20);
    map.put("张三",19);
    map.put("王五",20);

    //遍历 Map,   Map.Entry<String,Integer> 表示 Map 中的键值对,就像链表中的 Node 一样
    Set<Map.Entry<String,Integer>> set = map.entrySet();//将 Map 里的所有键值对存入一个 Set 里面
    //遍历 Set
    for(Map.Entry<String,Integer> entry: set){  
        String key = entry.getKey();
        //将“小明”的 value 改为 30
        if(entry.getKey().equals("小明")){
            entry.setValue(30);
        }
        int value = entry.getValue();
        System.out.println("Key: " + key +" "+ "Value: " + value);
    }
}
结果:
Key: 小明 Value: 30
Key: 张三 Value: 19
Key: 王五 Value: 20
复制代码

  为什么需要将所有键值对存入set里面,再对set遍历?为什么不能直接对Map进行for-each遍历?

  因为Map接口没有直接实现Iterable接口,因此无法直接使用for-each语句遍历Map中的元素。

2.2 HashMap

  Java中HashMap的底层是一个哈希桶(开散列表),什么是哈希桶?

哈希桶:一个数组,每个数组元素都是一个单向链表或红黑树,这些链表或树的头结点组成了一个桶数组,用来存储键值对,也可以叫做哈希表。(哈希表是什么?哈希冲突又是什么?又如何解决哈希冲突? - 掘金 (juejin.cn)

(1)构造方法

构造方法描述
HashMap()构造一个空的 HashMap
HashMap(int initialCapacity)构造具有指定初始容量的 HashMap
HashMap(int initialCapacity, float loadFactor)构造具有指定初始容量和负载因子的 HashMap。负载因子是哈希表在自动增长之前可以达到多满的一种尺度,通常情况下,负载因子越大,哈希表的装填因子越高,空间利用率就越高,但冲突的可能性也会越高。
HashMap(Map<? extends K, ? extends V> m)构造一个新的 HashMap,其映射关系与给定的 Map 相同。

  HashMap的使用与TreeMap都是相同的,比如遍历等,下面是使用HashMap的注意点:

  1. HashMap中的元素必须是能比较出是否相同的,自定义类型需要重写equals和 hashCode方法,对是否能比较大小没有要求。
class Student{
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ", age=" + age + '}';
    }
}

public class Main {
    public static void main1(String[] args) {

        
        Map<Student,Integer> map = new HashMap<>();

        map.put(new Student("小明",19),1);
        map.put(new Student("张三",18),2);
        map.put(new Student("王五",20),3);
        map.put(new Student("王五",20),4);

        System.out.println(map);
    }
}
复制代码

结果:

  我们期望的是王五只出现一次,但是这里出现了两次,为什么?因为Student没有比较是否相等的能力,它需要重写equals和 hashCode方法。

class Student{
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" + "name=" + name + ", age=" + age + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && name.equals(student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

public class Main {
    public static void main(String[] args) {
       
        HashMap<Student,Integer> map = new HashMap<>();
        map.put(new Student("小明",19),1);
        map.put(new Student("张三",18),2);
        map.put(new Student("王五",20),3);
        map.put(new Student("王五",20),4);
        System.out.println(map);
    }
}

结果:

{Student{name=小明, age=19}=1, Student{name=张三, age=18}=2, Student{name=王五, age=20}=4}
复制代码

  在HashMap源码中,重写的hashCode()方法是用来计算哈希值进而确定key的位置,equals()方法是用来比较两个对象是否相等的。

  1. 顺序问题:HashMap中键值对的顺序是不确定的,而TreeMap中默认按键的自然顺序或指定的比较器进行排序。

  2. 扩容机制:如果new的时候没有传参数,那么当第一次 put() 的时候,才会为HashMap分配内存,大小为 16;如果new的时候传了参数n那么会分配最接近(大于等于)你给的参数的2次幂的值,比如:n = 19,那么会分配2525 = 32 的大小。

  3. 在 JDK 8 中,当一个桶中的元素数量超过了 8 个,并且哈希表的容量大于等于 64,这个桶就会被转换成红黑树。

2.4 TreeMap 与 HashMap 的区别

Map底层结构TreeMapHashMap
底层结构红黑树哈希桶
插入/删除/查找时间 复杂度�(���2�)O(log2​N)�(1)O(1)
是否有序关于Key有序无序
线程安全不安全不安全
插入/删除/查找区别需要进行元素比较通过哈希函数计算哈希地址
比较与重写key必须能够比较,否则会抛出 ClassCastException异常自定义类型需要重写equals和 hashCode方法
应用场景需要Key有序场景下Key是否有序不关心,需要更高的 时间性能

3. Set 接口的使用

  Set接口提供的方法

方法描述
boolean add(E e)将指定元素添加到集合中(如果尚未存在)。
boolean remove(Object o)从集合中移除指定元素。
boolean contains(Object o)如果集合中包含指定元素,则返回 true。
boolean isEmpty()如果集合不包含任何元素,则返回 true。
int size()返回集合中的元素数量。
void clear()从集合中移除所有元素。
Object[] toArray()返回一个包含集合中所有元素的数组。
<T> T[] toArray(T[] a)将集合中的所有元素转换为指定类型的数组。
boolean containsAll(Collection<?> c)如果集合中包含指定集合中的所有元素,则返回 true。
boolean addAll(Collection<? extends E> c)将指定集合中的所有元素添加到集合中(如果尚未存在)。
boolean retainAll(Collection<?> c)仅保留集合中包含在指定集合中的元素(删除所有其他元素)。
boolean removeAll(Collection<?> c)从集合中移除指定集合中包含的所有元素。

  由于Set接口实现了lterable ,所以TreeSet、HashSet的遍历直接用for-each即可。

  1. Set是继承自Collection的一个接口类。
  2. Set中只存储了key,并且要求key一定要唯一。
  3. Set的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
  4. Set最大的功能就是对集合中的元素进行去重。
  5. TreeSet中不能插入null的key,HashSet能插入null的key。

3.1 TreeSet

  TreeSet底层是什么呢?它的底层其实就是TreeMap

  现在有以下代码

public static void main(String[] args) {
    Set<String> set = new TreeSet<>();
    set.add("小明");
}
复制代码

我们点进源码中的 add():

  也就是说,当我new TreeSet()的时候,它会 new TreeMap() 然后赋给 m;但是Set明明不是键值对的形式来存储数据的呀,底层怎么会是Map呢?从上面的图中看到put()方法中第二个参数是PRESENT,而PRESENT的值是new Object(),换言之,无论add多少,第二个参数永远都是一个Object类,这就相当于忽略了value值。

3.2 HashSet

  HashSet的底层是HashMapHashSet的一些注意事项与HashMap相同,比如它们的key可以为null,自定义类型要重写equalshashCode方法等,这里就不赘述了。

3.3 TreeSet 与 HashSet 的区别

Set底层结构TreeSetHashSet
底层结构红黑树哈希桶
插入/删除/查找时间 复杂度�(���2�)O(log2​N)�(1)O(1)
是否有序关于Key有序不一定有序
线程安全不安全不安全
插入/删除/查找区别需要进行元素比较通过哈希函数计算哈希地址
比较与覆写key必须能够比较,否则会抛出 ClassCastException异常自定义类型需要覆写equals和 hashCode方法
应用场景需要Key有序场景下Key是否有序不关心,需要更高的时间性能
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值