随笔都是学习笔记
随笔仅供参考,为避免笔记中可能出现的错误误导他人,请勿转载。

简介:

在Collection接口中,保存的数据都是单个的对象,在数据结构中除了单个对象的数据,也可以进行二元偶对象的保存(key=value)的形式来存储,而存储二元偶对象的核心意义在于:通过key获取对应的value。

在开发中:Collection集合保存数据的目的是为了输出,而Map集合保存数据的目的是为了key的查找。

Map接口:

Map接口是进行二元偶对象保存的最大父接口,该接口定义:

 

public interface Map<K,V>;

 

该接口为一个独立的父接口,并且在进行接口对象实例化的 时候需要设置Key与Value的类型,所以在保存的时候需要两个内容,在Map接口中定义有许多操作方法,但是有几个方法需要牢记:

 

- 向集合中保存数据:public V put(K key, V value);
- 根据Key查询数据:public V get(Object key);
- 将Map集合转为Set集合:public Set<Map.Entry<K, V>> entrySet();
- 查询指定的Key是否存在:public boolean containsKey(Object key);
- 将Map集合中的Key转为Set集合:public Set<K> keySet();
- 根据Key删除指定的数据:public V remove(Object key);

 

红色的是重要方法。

从JDK1.9之后Map集合提供了一些静态方法供用户使用。

代码实现:

 

import java.util.Map;

public class MAIN {
    public static void main(String[] args) {
        Map<String, Integer> map = Map.of("one",1,"two",2);
        System.out.println(map);
    }
}

 

输出结果:

 

在Map集合中数据的保存按照形式“Key=Value”,并且使用of()方法操作,里面数据的Key是不允许重复的和为空。

正常的使用情况是使用Map的子类进行,常用的子类有:HashMap、HashTable、TreeMap、LinkedHashMap

 

Map接口子类 - HashMap类:

HashMap是Map接口中最为常见的一个子类,该类的特点是无序存储

 

public class HashMap<K,V> 
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

 

继承形式符合集合定义的形式,提供又要抽象类,需要重复实现Map接口。

 

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
当使用无参构造的时候会出现有一个loadFactor属性,并且该属性默认的内容是“0.75f”
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

在使用put()方法进行数据保存的时候会调用putVal()方法,同时会将Key进行hash处理(生成一个hash码),而对于putVal()方法会发现提供了一个节点类进行数据保存,而在使用putVal()方法操作的过程之中会调用一个resize()的方法可以进行容量的扩充。

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

 

static class Node<K,V> implements Map.Entry<K,V>

 

面试题:

在进行HashMap的put()操作的时候如何实现容量扩充的?

   - 在HashMap类里面专门提供有一个“ DEFAULT_INITIAL_CAPACITY ”常量,作为初始化容量配置,而后这个常量的默认大小为16个元素,默认可以保存最大长度是16;

  - 当保存的内容的容量超过了阈值( DEFAULT_LOAD_FACTOR = 0.75f ),相当于 “ 容量 * 阈值  = 12 ” 保存12个元素的时候就会进行容量的扩充。

  - 在进行扩充的时候HashMap采用的是成倍的扩充模式,即:每一次都扩充2倍的容量

 

 

面试题:

请解释HashMap的工作原理?(JDK1.8之后)

  - 在HashMap之中进行数据存储的依然是利用Node类完成的,那么这种情况下就证明可以使用的数据结构有两种:链表(时间复杂度“O(n)”)、二叉树(时间复杂度(“O(logn)”));

  - 从JDK1.8开始,HashMap的实现出现了改变,因为其要适应于大数据时代的海量数据,所以对于其存储发生了变化,并且在HashMap类中提供了一个重要的常量:

static final int TREEIFY_THRESHOLD = 8;

  在使用HashMap进行数据保存的时候,如果保存的数据没有超过阈值“ 8 ”,那么会按照链表的形式进行存储,如果超出了阈值,则会将链表转换为红黑树以实现树的平衡,并且利用左旋与右旋保证数据的查询性能。

 

 

Map集合的使用:

 

import java.util.HashMap;
import java.util.Map;

public class MAIN {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();
        map.put("1", 1);
        map.put("2", 2);
        map.put("3", 3);
        map.put("4", 4);
        map.put("4", 3);    // Key重复
        map.put(null, 0);    // Key为空
        map.put("5", null);    // Value为空
        System.out.println(map.get(null));    // Key不存在
        System.out.println(map.get("4"));    // Key存在
        System.out.println(map.get("5"));    // Key存在
    }
}

 

输出结果:

 

可以发现:

  Key不存在也能获取对应的Value

  而出现重复Key的时候,后面的Key会覆盖前面的Key

  只要Key存在就能获取Value,即使Value为空

 

Put()方法的返回值观察:

 

import java.util.HashMap;
import java.util.Map;

public class MAIN {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();
        System.out.println(map.put("1", 1));    // Key不重复,返回null
        System.out.println(map.put("1", 101));    // Key重复,返回旧数据
        
    }
}

 

输出结果:

 

在设置了相同Key内容的时候,put()方法会返回旧的Value.

 

 

Map接口子类 - HashMap类

HashMap是Map集合最为常用的一种子类,但是其本身保存的数据都是无序的(有没有序对Map没有影响),如果希望Map集合之中保存的数据的顺序为其增加顺序,则可以更换子类为LinkedHashMap(基于链表实现)。

public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>

因为是链表的形式,所以一般在使用LinkedHashMap类的时候数据量都不要太大,因为会造成时间复杂度攀升。

从继承关系可以发现LinkedHashMap是HashMap的子类:

使用LinkedHashMap:

import java.util.LinkedHashMap;
import java.util.Map;
public class MAIN {
    public static void main(String[] args) {
        Map<String, Integer> map = new LinkedHashMap<String, Integer>();
        map.put("one", 1);
        map.put("three", 3);
        map.put("two", 2);
        System.out.println(map);
    }

}

输出结果:

从结果可以发现,LinkedHashMap集合中元素的顺序就是代码添加的顺序。

 

 

 

Map接口子类 - Hashtable类

Hashtable类是JDK1.0提供的,和Vector、Enumeration属于最早的一批动态数组的实现类,后来为了将其保存下来,就让其多实现了一个Map接口。

Hashtable类定义:

public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable

Hashtable类继承结构:

使用Hashtable类:

import java.util.Hashtable;
import java.util.Map;
public class MAIN {
    public static void main(String[] args) {
        Map<String, Integer> map = new Hashtable<String, Integer>();
        map.put("one", 1);
        map.put("three", 3);
        map.put("two", 2);
        System.out.println(map);
    }
}

输出结果:

可以试试将Key或者Value设置为空,运行后会发现程序出现异常NullPointerException。

面试题:

请解释HashMap与Hashtable的区别?

  - HashMap中的方法都属于异步操作,非线程安全;而Hashtable都属于同步方法,属于线程安全。

  - HashMap允许保存null的数据;而Hashtable不允许保存null的数据,否则会出现NullPointerException的异常。

但是更多情况下使用的还是HashMap。

 

Map.Entry接口:

在Map集合中有一个核心问题需要理解:Map集合里面是如何进行数据存储的?

对应List而言(LinkedList类)依靠的是链表的形式实现的数据存储,在进行数据存储的时候一定是将数据保存在Node节点之中,虽然在HashMap里面也能见到Node类型的定义(puVal()方法),HashMap中的Node内部类本身实现了Map.Entry接口:

static class Node<K,V> implements Map.Entry<K,V> 

所以可以得出的结论:所有的Key和Value的数据都被封装在Map.Entry 接口之中。

Map.Entry定义:

public static interface Map.Entry<K, V>

可以发现这是一个内部接口,并且在这个内部接口中提供有两个重要的操作方法:

- 获取Key:public K getKey();
- 获取Value:public VgetValue();

在JDK1.9之前的开发中使用者基本不考虑Map.Entry的对象,在正常的开发过程之中使用者也不需要关心Map.Entry对象,从JDK1.9之后Map接口中追加有一个新的方法:

 - 创建Map.Entry对象:public static <K, V> Map.Entry<K, V> entry(K k, V v) 

创建此对象:

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class MAIN {
    public static void main(String[] args) {
        Map.Entry<String, Integer> entry = Map.entry("one", 1);
        System.out.println("获取Key:" + entry.getKey());
        System.out.println("获取Value:" + entry.getValue());
        System.out.println(entry.getClass().getName());        // 观察使用的子类
        
    }
}

输出结果:

获取Key:one
获取Value:1
java.util.KeyValueHolder

可以发现,使用的是java.util.KeyValueHolder,那么java.util.KeyValueHolder是什么:

专门做数据保存的类。

final class KeyValueHolder<K,V> implements Map.Entry<K,V>
final class KeyValueHolder<K,V> implements Map.Entry<K,V> {
    @Stable
    final K key;
    @Stable
    final V value;

    KeyValueHolder(K k, V v) {
        key = Objects.requireNonNull(k);
        value = Objects.requireNonNull(v);
    }

    /**
     * Gets the key from this holder.
     *
     * @return the key
     */
    @Override
    public K getKey() {
        return key;
    }

    /**
     * Gets the value from this holder.
     *
     * @return the value
     */
    @Override
    public V getValue() {
        return value;
    }

    /**
     * Throws {@link UnsupportedOperationException}.
     *
     * @param value ignored
     * @return never returns normally
     */
    @Override
    public V setValue(V value) {
        throw new UnsupportedOperationException("not supported");
    }

    /**
     * Compares the specified object with this entry for equality.
     * Returns {@code true} if the given object is also a map entry and
     * the two entries' keys and values are equal. Note that key and
     * value are non-null, so equals() can be called safely on them.
     */
    @Override
    public boolean equals(Object o) {
        return o instanceof Map.Entry<?, ?> e
                && key.equals(e.getKey())
                && value.equals(e.getValue());
    }

    /**
     * Returns the hash code value for this map entry. The hash code
     * is {@code key.hashCode() ^ value.hashCode()}. Note that key and
     * value are non-null, so hashCode() can be called safely on them.
     */
    @Override
    public int hashCode() {
        return key.hashCode() ^ value.hashCode();
    }

    /**
     * Returns a String representation of this map entry.  This
     * implementation returns the string representation of this
     * entry's key followed by the equals character ("{@code =}")
     * followed by the string representation of this entry's value.
     *
     * @return a String representation of this map entry
     */
    @Override
    public String toString() {
        return key + "=" + value;
    }
}

 通过分析可以发现在整个的Map集合里面,Map.Entry的主要作用就是作为一个Key和Value的包装类型使用,而大部分情况下在进行数据存储的时候都会将Key和Value包装为一个Map.Entry对象进行使用。

 

使用Iterator输出Map集合:

对于集合的输出最标准的做法就是利用Iterator接口来完成,但是Map接口中没有一个方法可以直接返回Iterator接口对象,所以这种情况下就必须分析不直接提供Iterator接口实例化的原因:

在Map集合中实际上保存的是一组Map.Entry接口对象(里面包装的是Key和Value),所以整个来讲Map依然实现的是单值的保存,Map里面提供有一个方法:

- 将全部的Map集合转换为Set集合:public Set<Map.Entry<K, V>> entrySet();

通过这个方法将Map集合(Key不重复)转换为Set集合 (元素不重复),Set集合中保存的就是Map.Entry对象,而Set集合又能使用Iterator接口,所以就实现了Map集合间接使用Iterator接口的操作。

 

 

  1、使用Map集合中的entrySet()方法将Map集合转换成Set集合;

  2、利用Set接口中的iterator()方法将Set集合转为Iterator接口实例;

  3、利用Iterator进行迭代输出获取每一组的Map.Entry对象,然后使用getKey()、getValue()获取Key和Value。

利用Iterator输出与Map集合:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class MAIN {
    public static void main(String[] args) {
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);
        map.put("four", 4);
        map.put("five", 5);
        System.out.println(map);
        Set<Entry<String, Integer>> set = map.entrySet();
        Iterator<Entry<String, Integer>> iter = set.iterator();
        while(iter.hasNext()) {
            Entry<String, Integer> me = iter.next();
            System.out.println("**Key:" + me.getKey() + "  ---   Value:" + me.getValue());
        }
    }
}

输出结果:

{four=4, one=1, two=2, three=3, five=5}
**Key:four  ---   Value:4
**Key:one  ---   Value:1
**Key:two  ---   Value:2
**Key:three  ---   Value:3
**Key:five  ---   Value:5

虽然Map支持迭代输出,但是大部分情况下Map都不会使用迭代输出,因为Map集合主要的作用就是通过Key实现查询操作;

如果现在不使用Map集合而使用foreach进行迭代输出,同样需要将Map集合转为Set集合:

使用foreach:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class MAIN {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<String, Integer>();
        map.put("one", 1);
        map.put("two", 2);
        map.put("three", 3);
        map.put("four", 4);
        map.put("five", 5);
        System.out.println(map);
        Set<Entry<String, Integer>> set = map.entrySet();
        for(Map.Entry<String,Integer> me : set) {
            System.out.println(me.getKey() + " = " + me.getValue());
        }
    }
}

输出结果:

{four=4, one=1, two=2, three=3, five=5}
four = 4
one = 1
two = 2
three = 3
five = 5

由于Map迭代输出的情况相对较少,所以对于此类的语法应该深入理解,并且一定要灵活掌握。

 

自定义Key类型

在使用Map集合的时候可以发现对于Key和Value的类型都可以由使用者任意决定,那么就可以使用自定义的类进行Key的类型定义。

自定义Person类做Key的类型:

package Demo_3_15_Map集合;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;

// 自定义类做为Map集合的Key的类型
class Person {
    private String name;
    private int age;

    public Person() {
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        return age == other.age && Objects.equals(name, other.name);
    }
    
}
public class MAIN {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<Person, String>();
        map.put(new Person("张三",12), "1");
        map.put(new Person("李四",14), "2");
        map.put(new Person("王五",15), "3");
        map.put(new Person("狗蛋",16), "4");
        System.out.println(map.get(new Person("张三",12)));
    }
}

这个通过Key获取Value有所差异,因为是类对象,所以需要实现hashCode()、equals()方法才能获取Key对象对应的Value。

输出结果:

{name = 张三, age = 12=1, name = 王五, age = 15=3, name = 狗蛋, age = 16=4, name = 李四, age = 14=2}
1

源代码:

hashMap的put()方法

 hashMap的get()方法

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                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;
    }
 

在进行数据保存的时候会自动使用传入Key的数据生成一个hash码,存储的时候是有个Ha

sh数值的。

 在根据Key获取数据的时候依然要将传入的Key通过hash()方法来获取其对应的hash码,所以查询的过程之中首先要利用hashCode()来进行数据查询。在getNode()方法中还可以发现需要进行equals()方法的使用。

通过以上分析查看得出结论:

对于自定义Key类型所在的类中一定要重写hashCode()方法和equals()方法

否则无法查找到。

虽然允许使用自定义类进行Key类型的定义,但是也要注意一点,在实际的开发之中对于Map集合的Key常用的类型就是:String、Long、Integer,尽量使用系统类。

面试题:

如果在进行HashMap进行数据操作的时候出现了Hash冲突(Hash码相同),HashMap是如何解决的?

  - 当出现了Hash冲突之后为了保证程序的正常执行,会在冲突的位置将所有重复 Hash冲突的内容转为链表保存;

 

 

posted on 2022-03-15 16:51  时间完全不够用啊  阅读(69)  评论(0编辑  收藏  举报