浅析Java中的Map接口及实现类


1. Map

之前所学习的Collections接口下个不同的实现类,如Arrays、ArrayList、Stack、Queue、HaashSet等都属于单列集合的范畴,集合中的元素都是孤立存在的,当向集合中存储元素时只能一个个的存储。而Map接口下的各个集合类属于双列集合,即集合中的元素是以键值对的形式存在的。类似于Python中的dict,键和值一一对应且键不允许重复。


在这里插入图片描述

Map集合的特点有:

  • Map集合是双列集合
  • Map集合中的元素,key和value的数据类型可以相同也可以不同
  • Map集合中的元素,key不允许重复,value可以重复
  • Map集合中的元素,key和value是一一对应的

在使用Map接口的实现类时需要注意如下事项:

  • 给定一个键和一个值,你可以将该值存储在一个Map对象. 之后,你可以通过键来访问对应的值
  • 当访问的值不存在的时候,方法就会抛出一个NoSuchElementException异常.
  • 当对象的类型和Map里元素类型不兼容的时候,就会抛出一个 ClassCastException异常
  • 当在不允许使用null的Map中使用null,会抛出一个NullPointerException 异常
  • 当尝试修改一个只读的Map时,会抛出一个UnsupportedOperationException异常

Java Map 接口


2. HashMap

2.1 获取和判断方法

Map集合中常用的方法如下所示 :

  • V put(K key, V value):把指定的键和值添加到Map集合中,存储键值对的时候,如果key不重复返回值为null;否则会用新的value覆盖掉之前的value,返回被替换后的value
  • V remove(Object key):指定的键所对应的键值对元素从Map集合中删除,返回被删除的值,如果key存在则返回被删除的值,否则返回null
  • V get(Object key):如果key存在则返回对应的value值,否则返回null
  • boolean containsKey(Object key):判断集合中是否包含指定的键
  • boolean containsValue(Object value):判断集合中是否包含指定的值
  • int hashCode( ):返回此映射的哈希码值
  • boolean isEmpty( ):如果map未包含键-值映射关系,则返回 true
  • int size( ):返回此map中的键-值映射关系数
  • boolean equals(Object obj):比较指定的对象与此映射是否相等
public class MapDemo {
    public static void main(String[] args) {
        Map<Integer,String> m = new HashMap<>();

        m.put(10, "Forlogen");
        m.put(23, "James");
        System.out.println(m.put(24, "kobe")); // null
        System.out.println(m); // {23=James, 24=kobe, 10=Forlogen}

        System.out.println(m.remove(1)); // null
        System.out.println(m.remove(10));  // Forlogen
        System.out.println(m); // {23=James, 24=kobe}

        System.out.println(m.get(23)); // James
        System.out.println(m.get(10));  // null

        System.out.println(m.containsKey(10)); // false
        System.out.println(m.containsKey(23));  // true
    }
}

2.2 集合遍历方法
  • Set<K> keySet():返回map中包含的所有键组成的Set视图

  • Set entrySet():把Map集合内部的多个Entry对象取出来存储到一个Set集合中

    Map.Entry<K ,V>是Map接口中的一个内部接口,当Map集合一创建,就会在Map集合中创建一个Entry对象用来记录键和值(键值对对象、键与值得映射关系)

上面的两个方法提供了两种用于遍历Map的方法,首先第一种是使用Set<K> keySet()方法,步骤如下:

  • 使用Map集合的keySet()把集合中所有的key取出来,存储到一个Set集合中
  • 遍历set集合,获取Map集合中的每一个key
  • 通过get(key)获取key对应value
public class MapDemo {
    public static void main(String[] args) {
        Map<Integer,String> m = new HashMap<>();
 
        m.put(10, "Forlogen");
        m.put(23, "James");
        m.put(24, "kobe"); 

        Set<Integer> keys = m.keySet();
        for(Integer ele : keys){
            System.out.println(m.get(ele));
        }
    }
}

第二种方法是使用Map集合内部的Entry对象来进行遍历,具体步骤为:

  • 使用Map集合中的entrySet()把Map集合中的多个Entry对象取出来存储到一个Set集合中
  • 遍历Set集合获取每一个Entry对象
  • 使用Entry对象中的getKey()getValue()获取键和值
public class MapDemo {
    public static void main(String[] args) {
        Map<Integer,String> m = new HashMap<>();
        m.put(23, "James");
        m.put(24, "kobe"); 
       
	    Set<Map.Entry<Integer, String>> entries = m.entrySet();
        System.out.println(entries);  // [23=James, 24=kobe]
        for (Map.Entry ele: entries) {
            System.out.println(ele.getKey() + " = " + ele.getValue()); // 23 = Jame 24 = kobe
        }
    }
}

第三种方法是直接通过Map类的forEach方法进行遍历,方法定义如下:

  • default void forEach(BiConsumer<? super K,? super V> action):参数列表中传递BiConsumer接口的实例,通常可以传入一个Lambda表达式,表示对key和value要执行的一系列逻辑。如果执行遍历集合,简单的输出即可。
public class MapDemo {
    public static void main(String[] args) {
        Map<Integer,String> m = new HashMap<>();
        m.put(23, "James");
        m.put(24, "kobe"); 
        
        m.forEach((k, v) -> System.out.println("key is: " + k + " and value is: " + v));
        /*
        key is: 23 and value is: James
        key is: 24 and value is: kobe
        */
    }
}
2.3 更新方法

如果使用Map<String, Integer> m = new HashMap<>();创建了map之后,使用m.put(key, m.get(key) + 1)更新给定键的值,并规则新的值是旧值 + 1:

  • 如果map中存在key,则可以添加成功
  • 如果map中没有指定的key,则会抛出空指针异常

一种改进方法是先使用default V putIfAbsent(K key, V value),然后再使用上面的put()。如果key不存在,则首先将其设为putIfAbsent()传入的值,否则不执行操作。这样执行put()时就不会抛出异常了。

更好的方法是使用default V merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)方法进行操作。merge()的实现原理为:

V oldValue = map.get(key);
V newValue = (oldValue == null) ? value :
             remappingFunction.apply(oldValue, value);
if (newValue == null)
    map.remove(key);
else
    map.put(key, newValue);

因此,当执行m.merge(key, 1, Integer::sum);时,如果当前key对应的值为空,则将其设置为1;否则将当前值加1作为新的值。加1的操作通过BiFunction()函数式接口完成,merge()中传入的是Integer的sum()的方法引用表达式。

public class MapUpdate {
    public static void main(String[] args) {
        Map<String, Integer> m = new HashMap<>();
        m.put("Forlogen", 10);
        m.put("James", 23);
        m.put("kobe", 24);
        m.forEach((k, v) -> System.out.println("key is: " + k + " and value is: " + v));
        System.out.println("-----------------");

        // 更新
        String key = "Forlogen";
        System.out.println("old value is: " + m.get(key));
        m.put(key, m.get(key) + 1);
        System.out.println("new value is: " + m.get(key));
        System.out.println("-----------------");

//        m.put("bill" , m.get("bill") + 1); // Exception in thread "main" java.lang.NullPointerException

//        m.putIfAbsent("bill", 0);
//        m.put("bill" , m.get("bill") + 1);
//        m.forEach((k, v) -> System.out.println("key is: " + k + " and value is: " + v));

        m.merge("bill", 1, Integer::sum);
        m.forEach((k, v) -> System.out.println("key is: " + k + " and value is: " + v));
        System.out.println("-----------------");

    }
}
2.4 应用

HashMap中的键和值的数据类型可以是任意的,因此HashMap同样可以存储自定义的数据类型的数据:

  • 如果自定义类型的数据作为map中的value存在,那么可以保证key是不重复的
  • 如果自定义类型的数据作为key存在,那么自定义的类就必须重写hashCode()equals()方法来保证key的唯一性

假设现在有一个Person类,定义如下,类内部重写了hashCode()equals()方法:

public class Person {
    private int age;
    private String name;

    public Person() {
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return getAge() == person.getAge() &&
                Objects.equals(getName(), person.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getAge(), getName());
    }

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

当类定义完毕后,我们就可以使用HashMap进行存储:

public class MapDemo {
    public static void main(String[] args) {
        Map<String, Person> map = new HashMap<>();
        map.put("China", new Person(10, "Forlogen"));
        map.put("US", new Person(20, "kobe"));
        System.out.println(map); // {China=Person{age=10, name='Forlogen'}, US=Person{age=20, name='kobe'}}
        Set<String> set1 = map.keySet();
        for (String ele: set1) {
            System.out.println(map.get(ele)); // Person{age=10, name='Forlogen'}  Person{age=20, name='kobe'}

        }

        Set<Map.Entry<String, Person>> entries1 = map.entrySet();
        System.out.println(entries1);  // [China=Person{age=10, name='Forlogen'}, US=Person{age=20, name='kobe'}]
        for (Map.Entry ele: entries1) {
            System.out.println(ele.getKey() + " = " + ele.getValue()); // China = Person{age=10, name='Forlogen'} US = Person{age=20, name='kobe'}
        }
    }
}

在HashMap之前使用的是HashTable,它也实现了Map接口,HashTable和HashMap的区别在于:

  • HashTable底层实一个哈希表,是一个线程安全的集合,是单线程集合,速度慢;不能存储null值、null键
  • HashMap底层也是哈希表,是一个线程不安全的集合,是多线程集合,速度快;可以存储null值、null键

3. LinkedHashMap

类似于Set接口中的HashSet和LinkedHashSet,HashMap对应的也有一个LinkedHashMap来使得Map中的元素是有序的。它的底层实现也是哈希表+链表,通过多加的双向链表来记录存储元素的顺序,从而实现Map中元素的有序性。

LinkedHashList中Entry的实现源码如下所示:

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);
        }
    }

before和after来指向当前元素的前一个元素和后一个元素,从而实现元素存放的有序性。

测试代码:

public class LinkedHashMapDemo {
    public static void main(String[] args) {
        Map<Integer, String> m = new LinkedHashMap<>();
        m.put(10, "Forlogen");
        m.put(23, "James");
        m.put(24, "kobe");

        System.out.println(m); // {10=Forlogen, 23=James, 24=kobe}
    }
}

4. TreeMap

类似于Set接口中的Treeset实现,Map集合中也有相应的实现,即TreeMap。TreeMap存储的是键值对形式的数据,而且存储是需要根据key-value进行排序,从而可以保证所有的键值对都处于有序状态。TreeSet的底层实现依赖于TreeMap,因此TreeSet所具有的特点TreeMap同样拥有。

与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。其中,可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。

例如,当TreeMap集合中的key为Java中已有的类型时,此时使用的就是自然排序:

public class TreeMapDemo {

    @Test
    public void test(){
        Map<Integer, String> map = new TreeMap<>();

        map.put(10, "Forlogen");
        map.put(24, "Kobe");
        map.put(23, "James");

        System.out.println(map.toString()); // {10=Forlogen, 23=James, 24=Kobe}
    }
}

如果key为自定义类型的数据时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator。例如,假设此时Book类定义如下,它实现了Compareable接口的实现类,并重写其中的compare()::

public class Book implements Comparator<Book>{

    private Integer price;

    public Book(Integer price) {
        this.price = price;
    }

    public Integer getPrice() {
        return price;
    }

    public void setPrice(Integer price) {
        this.price = price;
    }

    @Override
    public int compare(Book o1, Book o2) {
        return o1.getPrice() - o2.getPrice();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(price, book.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(price);
    }

    @Override
    public String toString() {
        return "Book{" +
                "price=" + price +
                '}';
    }
}


当Book对象作为key时,要么在TreeMap初始化时传入自定义的Comparator,此时按照Book的price字段降序排列:

public class TreeMapDemo {

   @Test
   public void test2(){
        Map<Book, String> map1 = new TreeMap<Book, String>(((o1, o2) -> o2.getPrice() - o1.getPrice()));

       map1.put(new Book(25), "Java");
       map1.put(new Book(10), "Python");
       map1.put(new Book(55), "AI");
       System.out.println(map1);
       System.out.println("---------------");
   }

   /*
   {Book{price=10}=Python, Book{price=25}=Java, Book{price=55}=AI}
   ---------------
    */
}


5. Hashtable

Hashtable的原理基本上合HashMap是相同的,不同之处在于:

  • Hashtable线程安全
  • Hashtable不允许使用null作为key和value

6. Properties

关于Properties的介绍可见:浅析Java中的Properties


7. 更多阅读

对于HashMap更深入的讲解可阅读以下文章:

Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例
Java 8系列之重新认识HashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值