面试官问:你熟悉哪些HashMap的封装扩展类?

我习惯了无所谓,却不是真的什么都不在乎。                 请关注:源码猎人

目录

简介

LinkedHashMap 源码解读

LinkedHashMap属性

LinkedHashMap构造函数

LinkedHashMap 方法

LinkedHashMap 内部类

LinkedHashMap.Entry,v>

HashSet 类源码解读

HashSet 属性

HashSet 构造函数

HashSet 方法

LinkedHashSet 类源码解读

LinkedHashSet 构造函数

常见面试题


简介

LinkedHashMap、HashSet、LinkedHashSet都扩展了HashMap。它们是HashMap的二次封装,LinkedHashMap加入了一个单独链表存所有数据,并且完整保留HashMap结构。 HashSet 实际上只用到了HashMap的键,值为常量;LinkedHashSet 继承HashSet,并且和LinkedHashMap一样内部维护一个链表。

LinkedHashMap 源码解读

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

LinkedHashMap属性

// 链表头结点
transient LinkedHashMap.Entry<K,V> head;
// 链表尾结点
transient LinkedHashMap.Entry<K,V> tail;
// LRU算法相关  false 基于插入顺序,true 基于访问顺序 
final boolean accessOrder;

LinkedHashMap构造函数

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    // 默认使用插入顺序
    accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    // 默认使用插入顺序
    accessOrder = false;
}
public LinkedHashMap() {
    super();
    // 默认使用插入顺序
    accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    // 默认使用插入顺序
    putMapEntries(m, false);
}
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    // 自定义顺序
    this.accessOrder = accessOrder;
}

从构造函数中可以看出,LinkedHashMap几乎完全使用HashMap的构造方法初始化(参照HashMap类篇),至于初始长度和加载因子也是延用HashMap,LinkedHashMap类自身只关注链表结构,数字加链表结构交给父类去维护

LinkedHashMap 方法

根据键取值

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

实际上就是调用HashMap的getNode方法, afterNodeAccess(e)方法只是为了维护顺序,除了这个方法LinkedHashMap没有添加删除方法,那么他是怎么维护链表结构的呢?是否还记得在HashMap篇有几个方法要放在LinkedHashMap讲

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

这几个方法出现在HashMap元素增加或减少之后,就是为了给hashMap子类使用

父类元素变动后方法

// HashMap删除节点后
void afterNodeRemoval(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        // 如果是头节点,则把头节的下一个节点设为头节点
        head = a;
    else
        // 否则,把前一个节点的下一个节点指向当前下一个节点
        b.after = a;
    if (a == null)
        // 如果是尾节点,设置当前节点前一个节点为尾节点
        tail = b;
    else
        // 否则把后面节点的前面执行当前节点的前面
        a.before = b;
}

HashMap删除元素成功后会调用此方法afterNodeRemoval

// 插入成功删除头节点
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

afterNodeInsertion方法是在哈希表中插入了一个新节点时调用的,它会把链表的头节点删除掉,删除的方式是通过调用HashMap的removeNode方法。我们要使用此功能必须重写removeEldestEntry方法

// accessOrder为true时将节点移到最后
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

当accessOrder设置为true时,把当前节点e移至链表的尾部,在HashMap的putVal方法中,会调用此方法

LinkedHashMap 内部类

LinkedHashMap.Entry<K,V>

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

此内部类在HashMap中也用到,HashMap的TreeNode就是继承此类(是不是很绕)

HashSet 类源码解读

public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable

跟ArraryList相比没有实现RandomAccess接口,接下来看成员变量和构造函数

HashSet 属性

// 底层是HashMap实现的   
private transient HashMap<E,Object> map;
// PRESENT是一个假的value值,帮助用HashMap实现HashSet。
private static final Object PRESENT = new Object();

HashSet 构造函数

public HashSet() {
    map = new HashMap<>();
}
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

通过构造函数可以看出HashSet的初始化时内部构建了一个HashMap对象

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

此构造函数安全级别为默认,只有自己或同包子类可以调用。HashSet并没有调用它,它是给子类使用的,跟上面构造函数唯一的区别就是内部Map换成LinkedHashMap<>对象

HashSet 方法

// 获取长度
public int size() {
    return map.size();
}
// 是否为空
public boolean isEmpty() {
    return map.isEmpty();
}
// 是否包含
public boolean contains(Object o) {
    return map.containsKey(o);
}
// 添加元素
public boolean add(E e) {
    // 添加元素时,元素为键,值为固定常量存入Map
    return map.put(e, PRESENT)==null;
}
// 删除元素
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}
// 清空
public void clear() {
    map.clear();
}

HashSet只是用了HashMap的key,而值是一个固定的常量,所以HashMap的key拥有哪些特性HashSet就拥有哪些特性。

LinkedHashSet 类源码解读

public class LinkedHashSet<E> extends HashSet<E>
        implements Set<E>, Cloneable, java.io.Serializable

LinkedHashSet完成继承HashSet,内部只有构造函数

LinkedHashSet 构造函数

public LinkedHashSet(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
    super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
    super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
    super(Math.max(2*c.size(), 11), .75f, true);
    addAll(c);
}

LinkedHashSet 所有构造函数都调用HashSet的HashSet(int initialCapacity, float loadFactor, boolean dummy)构造函数,内部使用LinkedHashMap对象

常见面试题

1.说一下LinkedHashMap的数据结构

LinkedHashMap继承自HashMap,并维持了一个双向链表。插入节点时,将节点追加到双向链表尾部,从而实现按照插入顺序的有序访问。也可以在初始化LinkedHashMap对象时设定为按照访问顺序排序,此时每当访问一个节点,afternodeaccess方法就会将该节点放到双向链表的尾部,从而实现按照访问顺序的有序遍历访问。

2、请说说HashSet原理

HashSet在存元素时,会调用对象的hashCode方法计算出存储位置,然后和该位置上所有的元素进行equals比较,
如果该位置没有其他元素或者比较的结果都为false就存进去,否则就不存。这样的原理注定了元素是按照哈希值来找存储位置,所有无序,而且可以保证无重复元素,我们在往HashSet集合存储元素时,对象应该正确重写Object类的hashCode和equals方法
正因为这样的原理,HashSet集合是非常高效的。

3、为什么HashMap中String、Integer这样的包装类适合作为K?

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  • 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况

如果Object作为键,那么需要重写hashCode()equals()方法

  • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞
  • 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性

4、HashMap 和 LinkedHashMap 有什么区别?

  • LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
  • LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外通过 Entry 维护了一个双向链表。
  • HashMap 元素的遍历顺序不一定与元素的插入顺序相同,而 LinkedHashMap 则通过遍历双向链表来获取元素,所以遍历顺序在一定条件下等于插入顺序。

5、ArrayList 与 LinkedList 有什么区别 ?

  • 存储结构上 ArrayList 底层使用数组进行元素的存储,LinkedList 使用双向链表作为存储结构。
  • 两者均与允许存储 null 也允许存储重复元素。
  • 在性能上 ArrayList 在存储大量元素时候的增删效率 平均低于 LinkedList,因为 ArrayList 在增删的是需要拷贝元素到新的数组,而 LinkedList 只需要将节点前后指针指向改变。
  • 在根据角标获取元素的时间效率上ArrayList优于 LinkedList,因为数组本身有存储连续,有 index 角标,而 LinkedList 存储元素离散,需要遍历链表。
  • 不要使用 for 循环去遍历 LinkedList 因为效率很低。
  • 两者都是线程不安全的,都可以使用 Collections.synchronizedList(List<E> list) 方法生成一个线程安全的 List。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源码猎人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值