00 31Java高级之Map集合


在之前已经学习了Collection接口以及其对应的子接口,可以发现在Collection接口之中所保存的数据全部都只是单个对象,而在数据结构里面除了可以进行单个对象的保存之外,实际上也可以进行二元偶对象的保存(key=value)的形式来存储,而存储二元偶对象的核心意义在于,需要通过key获取对应的value。

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

1 Map接口简介

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

public interface Map<K,​V>

该接口为一个独立的父接口,并且在进行接口对象实例化的时候需要设置Key与Value的类型,也就是说在整体操作的时候需要保存两个内容,在Map接口里面定义有许多的操作方法,但是需要记住以下的核心操作方法:
(1)向集合之中保存数据:V put​(K key, V value)
(2)根据Key查询数据:V get​(Object key)
(3)将Map集合转为Set集合:Set<Map.Entry<K,​V>> entrySet()
(4)查询指定的Key是否存在:boolean containsKey​(Object key)
(5)将Map集合中的Key转为Set集合:Set<K> keySet()
(6)根据Key删除指定数据:V remove​(Object key)

从JDK 1.9之后Map接口里面也扩充了一些静态方法。
范例:观察Map集合的特点

package org.lks.demo;

import java.util.Map;

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


在Map集合之中数据的保存就是按照“key=value”的形式存储的,并且使用of()方法操作的时候里面的数据是不允许重复的,如果重复则会出现java.lang.IllegalArgumentException异常,如果设置的内容为null,则会出现java.lang.NullPointerException异常。

对于现在见到的of()方法严格意义上来讲并不是Map集合的标准用法,因为正常的开发之中需要通过Map集合的子类来进行接口对象的实例化,而常用的子类:HashMap、Hashtable、TreeMap、LinkedHashMap。

2 HashMap子类

HashMap是Map接口之中最为常见的一个子类,该类的主要特点是无序存储,通过Java文档首先来观察一下HashMap子类的定义形式:

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

该类的定义继承形式符合之前的集合定义形式,依然提供有抽象类并且依然需要重复实现Map接口。

范例:观察Map集合的使用

package org.lks.demo;

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

public class JavaReflectDemo {
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);
		map.put("one", 100);
		map.put(null, 3);
		map.put("three", null);
		map.put(null, null);
		System.out.println(map);
	}
}


以上的操作形式为Map集合使用的最标准的处理形式,通过代码可以发现,通过HashMap实例化的Map接口里面可以针对key或value保存null的数据,同时也可以发现即便保存数据的key重复,那么也不会出现错误,而是出现内容的替换。

扫描二维码关注公众号,回复: 10402913 查看本文章

但是对于Map接口中提供的put()方法本身是提供有返回值的,那么这个返回值指的是在重复key的情况下返回旧的value。

观察put方法,在设置了相同的key的内容的时候put()方法会返回原始的数据内容。

清楚了HashMap的基本功能之后下面就需要来研究一下HashMap之中给出的源代码,HashMap之中肯定需要存储大量的数据,那么对于数据的存储。

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

当使用无参构造的时候会出现有一个loadFactor属性,并且该属性默认的内容为0.75fstatic 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()方法里面会发现依然会提供有一个Node的节点类进行数据的保存,而在使用putVal()方法操作的过程之中会调用有一个resize()方法可以及逆行容量的扩充。

面试题:在进行HashMap的put()操作的时候,如何实现容量扩充的?
(1)在HashMap类里面提供有一个DEFAULT_INITIAL_CAPACITY常量,作为初始化的容量配置,而后这个常量的默认大小为16个元素,也就说默认可以保存的最大内容是16;
(2)当保存的内容的容量超过了一个阈值(DEFAULT_LOAD_FACTOR = 0.75f),相当于容量*阈值=12,保存12个元素的时候就会进行容量的扩充;
(3)在进行扩充的时候HashMap采用的是成倍的扩充模式,即:每一次都扩充2倍的容量

面试题:请解释HashMap的工作原理(JDK 1.8之后开始的)
(1)在HashMap之中进行数据存储的依然是利用Node类完成的,那么这种情况下,就证明可以使用的数据结构只有两种:链表(时间复杂度O(n))、二叉树(时间复杂度O(logn));
(2)从JDK 1.8开始,HashMap的实现出现了改变,因为其要适应于大数据时代的海量数据问题,所以对于其存储发生了变化,并且在HashMap类的内部提供了有一个重要的常量:static final int TREEIFY_THRESHOLD = 8,在使用HashMap进行数据保存的时候,如果保存的数据个数没有超过阈值8,那么会按照链表的形式进行存储,如果超过了,则会将链表转为红黑树以实现树的平衡,并且利用左旋与右旋保证数据的查询性能。

3 LinkedHashMap子类

HashMap虽然是Map集合最为常用的一个子类,但是其本身所保存的数据都是无序的(有序与否对Map没有影响),如果现在希望Map集合之中保存的数据的顺序为其增加顺序,则就可以更换子类为LinkedHashMap(基于链表实现的)。
类的定义形式:

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

既然是链表保存,所以一般在使用LinkedHashMap类的时候往往数据量不要特别大,因为会造成时间复杂度攀升,通过继承结构可以发现LinkedHashMap是HashMap子类,继承关系如下:

范例:使用LinkedHashMap

package org.lks.demo;

import java.util.LinkedHashMap;
import java.util.Map;

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

/*
{one=100, two=2, null=null, three=null}
*/

通过此时程序的执行可以发现当使用LinkedHashMap进行存储之后所有数据的保存顺序为添加顺序。

4 Hashtable子类

Hashtable类是从JDK 1.0的时候提供的,与Vector、Enumeration术语最早的一批动态数组的实现类,后来为了将其保存下来所以让其多实现了一个Map接口,Hashtable类的定义结构如下:

public class Hashtable<K,​V>
extends Dictionary<K,​V>
implements Map<K,​V>, Cloneable, Serializable

Hashtable类的继承结构如下:

范例:观察Hashtable子类的使用

package org.lks.demo;

import java.util.Hashtable;
import java.util.Map;

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

/*
{two=2, one=100}
*/

通过观察可以发现在Hashtable里面进行数据存储的时候设置的key或value都不允许为null,否则会出现NullPointerException异常。
面试题:请解释HashMap与Hashtable的区别?
(1)HashMap中的方法都属于异步操作,非线程安全,HashMap允许保存有null数据;
(2)Hashtable中的方法都属于同步方法(线程安全),Hashtable不允许保存null,否则会出现NullPointerException异常;

5 Map.Entry内部接口

虽然现在已经清楚了整个的Map集合的基本操作形式,但是依然需要有一个核心的问题要解决,Map集合里面是如何进行数据存储的?对于List而言(LinkedList子类)依靠的是链表的形式实现的数据存储,那么在进行数据存储的时候一定要将数据把数据保存在Node节点之中,虽然在HashMap里面也可以见到Node类型定义,通过源代码的定义可以发现,HashMap类中的Node内部类本身实现了Map.Entry接口。

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

所以可以得出结论:所有的key和value的数据都被封装在Map.Entry接口之中,而此接口定义如下:

public static interface Map.Entry<K,​V>

并且在这个内部接口里面提供有两个重要的操作方法:
(1)获取key:K getKey()
(2)获取value:V getValue()

在JDK 1.9以前的开发版本之中,使用者基本上都不会去考虑创建Map.Entry的对象,实际上在正常的开发过程之中,使用者也不需要关心Map.Entry对象的创建,可是从JDK 1.9之后Map接口里面追加有一个新的方法:
(1)创建Map.Entry对象:static <K,​V> Map.Entry<K,​V> entry​(K k, V v)
范例:创建Map.Entry对象

package org.lks.demo;

import java.util.Map;

public class JavaReflectDemo {
	public static void main(String[] args) {
		Map.Entry<String, Integer> mapEntry = Map.entry("one", 1);
		System.out.println(mapEntry.getKey() + " = " + mapEntry.getValue());
	}
}

/*
one = 1
*/


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

6 利用Iterator输出Map集合

对于集合的输出而言,最标准的做法就是利用Iterator接口来完成,但是需要明确一点的是在Map集合里面并没有一个方法可以直接返回Iterator的接口对象,所以这种情况下就必须分析不直接提供Iterator接口实例化的方法原因,下面对Collection与Map集合的存储结构进行一个比较处理。

发现在Map集合里面保存的实际上是一组Map.Entry接口对象(里面包装的是Key和Value),所以整个来讲Map依然实现的是单值的保存,这样在Map集合里面提供有一个方法static <K,​V> Map.Entry<K,​V> entry​(K k, V v),将全部的Map集合转为Set集合。

经过分析可以发现要想使用Iterator实现Map集合的输出则必须按照如下步骤处理:
(1)利用Map接口中提供的entrySet()方法将Map集合转为Set集合;
(2)利用Set接口中的iterator()方法将Set集合转为Iterator接口实例;
(3)利用Iterator进行迭代输出获取每一组的Map.Entry对象,随后通过getKey()与getValue()获取数据。
范例:利用Iterator输出Map集合

package org.lks.demo;

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

public class JavaReflectDemo {
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);
		map.put("one", 100);
		map.put(null, 3);
		Set<Map.Entry<String, Integer>> set = map.entrySet();
		Iterator<Map.Entry<String, Integer>> iter = set.iterator();
		while(iter.hasNext()) {
			Map.Entry<String, Integer> temp = iter.next();
			System.out.println(temp.getKey() + " = " + temp.getValue());
		}
	}
}


虽然Map集合本身支持有迭代输出的支持,但是如果从实际开发来讲,Map集合最主要的用法在于实现数据key的查找操作,另外需要提醒的是,如果现在不使用Iterator而使用foreach语法输出则以需要将Map集合转为Set集合。
范例:使用foreach输出Map集合

package org.lks.demo;

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

public class JavaReflectDemo {
	public static void main(String[] args) {
		Map<String, Integer> map = new HashMap<String, Integer>();
		map.put("one", 1);
		map.put("two", 2);
		map.put("one", 100);
		map.put(null, 3);
		Set<Map.Entry<String, Integer>> set = map.entrySet();
		for(Map.Entry<String, Integer> temp : set) {
			System.out.println(temp.getKey() + " = " + temp.getValue());
		}
	}
}


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

7 自定义Map的key类型

在使用Map集合的时候可以发现对于Key和Value的类型实际上都可以由使用者任意定义,那么也就意味着现在依然可以使用自定义的类来进行Key类型的设置,对于自定义Key类型所在的类中一定要覆写hashCode()和equals()方法,否则无法查找到。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

在进行数据保存的时候发现会自动使用传入的key的数据生成一个hash码,也就是说存储的时候是有这个Hash数值。

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

再根据key获取数据的时候依然要将传入的key通过hash()方法来获取其对应的hash码,那么也就证明查询的过程之中首先要利用hashCode()来进行数据查询,当使用getNode()方法查询的时候还需要使用到equals()方法。

范例:使用自定义类作为Key类型

package org.lks.demo;

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

class PersonA{
	private String name;
	private int age;
	
	public PersonA() {}
	
	public PersonA(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + age;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		PersonA other = (PersonA) obj;
		if (age != other.age)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
	
}

public class JavaReflectDemo {
	public static void main(String[] args) {
		Map<PersonA, Integer> map = new HashMap<PersonA, Integer>();
		map.put(new PersonA("lks", 23), 1);
		map.put(new PersonA("hhy", 20), 2);
		System.out.println(map.get(new PersonA("hhy", 20)));
	}
}


虽然允许你使用自定义类作为Key的类型,但是也需要注意一点,在实际的开发之中对于Map集合的Key常用的类型就是:String、Long、Integer,尽量使用系统类。
面试题:如果在进行HashMap进行数据操作的时候出现了Hash冲突(Hash码相同),HashMap是如何解决的?
当出现了Hash冲突之后为了保证程序的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表保存。

发布了122 篇原创文章 · 获赞 11 · 访问量 4217

猜你喜欢

转载自blog.csdn.net/weixin_43762330/article/details/104771062
00
今日推荐