62-Map集合

Map集合

  在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)普通向集合中保存数据
public V get(Object key)普通根据key查询数据
public Set<Map.Entry<K,V>> entrySet()普通将Map集合转换为Set集合
public boolean containsKey(Object key)普通查询指定的key是否存在
public Set<K> ketSet()普通将Map集合找那个的key转换为Set集合
public V remove(Object key)普通根据key删除指定数据

  从JDK1.9之后Map接口扩充了一些静态方法供用户使用。
Map结合特点

package per.lyz.demo;
import java.util.Map;
public class Map_Study {
	public static void main(String[] args) {
		Map<String,Integer> map = Map.of("Hello",1,"World",2,"Hello",101,null,0);
		System.out.println(map);
	}
}

运行结果:
Exception in thread “main” java.lang.IllegalArgumentException: duplicate key: Hello(Hello-101)
Exception in thread “main” java.lang.NullPointerException(null)

  Map中数据的保存是按照“key=Value”的形式存储的,且使用of()方法操作的时候数据不允许重复,如果出现重复会出现“IllegalArgumentException”,若设置为空则出现“NullPointerException”。
  对于现在见到的of()方法严格意义上来讲并不是Map集合的标准用法,正常开发中需要通过Map集合子类来进行接口对象实例化,而常用的子类:“HashMap”、“Hashtable”、“TreeMap”、“LinkedHashMap”。

HashMap子类

  HashMap是Map接口中最常见的子类,该类的主要特点是无序存储,通过Java文件观察HashMap的定义形式:

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

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

观察Map集合的使用

package per.lyz.demo;

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

public class Map_Study {
	public static void main(String[] args) {
		Map<String,Integer> map = new HashMap<String,Integer>();
		map.put("Hello", 1);
		map.put("World",2);
		map.put("World",101);		//key重复
		map.put(null, 0);			//key为null
		map.put("zero", null);		//value为null
		System.out.println(map.get("World"));	//key存在
		System.out.println(map.get(null));		//key存在
		System.out.println(map.get("ten"));		//key不存在
		System.out.println(map);
	}
}

运行结果:
101
0
null
{null=0, zero=null, Hello=1, World=101}

  以上的操作形式为Map集合的标准处理形式,通过代码可以发现,通过HashMap实例化的Map接口中可以针对于key或value保存null的数据,同时也可以发现即便保存数据的key重复,那么也不会出现错误,而是出现内容的替换。
  但是对于Map接口中提供的put()方法本身是提供有返回值的,那么这个返回值指的是在重复key的情况下返回旧的value

观察put()方法

package per.lyz.demo;

import java.util.HashMap;

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

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

  清楚了HashMap的基本功能之后下面就需要研究一下HashMap的源代码,HashMap中需要存储大量的数据,对于数据的存储Map是如何进行的。

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

static final float DEFAULT_LOAD_FACTOR = 0.75f;

  当使用无参构造的时候会有一个LOAD_FACTOR,该属性的默认内容为0.75

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

  在使用put()方法进行数据保存的时候会调用一个putVal()方法,同时会将key进行hash处理(生成一个hash码),而对于putVal()方法中依然存在一个Node节点类进行数据的保存,而在使用putVal()方法时会调用一个resize()方法主要是为了进行容量的扩充。

面试1:在进行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(TREEIFY_THRESHOLD)则会按照链表的形式进行存储,如果超过这个阈值则会将链表转为红黑树以实现树的平衡,并且利用左旋与右旋保证查询性能。

LinkedHashMap子类

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

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

  既然是链表保存,所以一般在使用LinkedHashMap类的时候往往数据量都不要特别大,否则会造成时间复杂度攀升,通过集成结构可以发现LinkedHashMap是HashMap的子类。

LinkedHashMap继承结构
6rOxj1.png

使用LinkedHashMap

package per.lyz.demo;

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

public class Map_Study {
	public static void main(String[] args) {
		Map<String,Integer> map = new LinkedHashMap<String,Integer>();
		map.put("Hello", 1);
		map.put("World",2);
		map.put("World",101);		//key重复
		map.put(null, 0);			//key为null
		map.put("zero", null);		//value为null
		System.out.println(map);
	}
}

  通过执行发现当使用LinkedHashMap进行存储之后,数据的保存顺序为添加顺序。

Hashtable子类

  Hashtable类是从JDK1.0的时候提供的,与Vector、Enumeration属于最早的一批动态数组的实现类,后来为了将其继续保存下来,让其多实现了Map接口。Hashtable定义结构:

  • public class Hashtable<K,V> extends Dictinary<K,V> implements Map<K,V>,Cloneable,Serializable

Hashtable继承结构
6spoRS.png

Hashtable使用

package per.lyz.demo;

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

public class Map_Study {
	public static void main(String[] args) {
		Map<String,Integer> map = new Hashtable<String,Integer>();
		map.put("Hello", 1);
		map.put("World",2);
		map.put("World",101);		//key重复,将被保存
		System.out.println(map);
	}
}

  通过观察可以发现在Hashtable中进行数据存储的时候设置的key或value都不允许为null,否则会出现NullPointerException异常。

面试:解释HashMap与Hashtable的区别?

  • HashMap中的方法都属于异步操作(非线程安全),允许保存null数据;
  • Hashtable中的方法属于同步方法(线程安全),不允许保存null,否则会出现NullPointerException异常。

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>

  并且在这个内部接口中提供有两个重要的操作方法:

  • 获取key:public K getKey()
  • 获取Value:public V getValue()

Map.Entry接口
6sPhI1.png
  在JDk1.9以前的开发版本中,使用者基本上都不会去考虑创建Map.Entry对象,实际上在正常的开发过程中使用者也不需要关心Map.Entry对象的创建。但是从JDK1.9之后Map接口中追加有一个新的方法;

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

创建Map.Entry对象

package per.lyz.demo;

import java.util.Map;

public class Map_Study {

	public static void main(String[] args) {
		Map.Entry<String, Integer> entry = Map.entry("one",1);
		System.out.println(entry.getKey());
		System.out.println(entry.getValue());
		System.out.println(entry.getClass().getName());		//观察使用的子类-"java.util.KeyValueHolder"
	}
}

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

利用Iterator输出Map集合

  对于集合输出而言最标准的做法就是利用Iterator接口完成,但是Map集合中并没有一个方法可以直接返回Iterator接口对象,所以这种情况下就必须分析不直接提供Iterator接口实例化方法的原因,下面对Collection与Map集合的存储结构进行比较:
6stzHH.png
  在Map集合中保存的实际上是一组Map.Entry接口对象(里面包装的是key-Value),所以Map实现的依然是单值的保存,这样在Map集合中存在一个方法public Set<Map.Entry<K,V>> entrySet(),将全部集合转为Set集合。
  经过分析可以发现如果要想使用Iterator实现Map集合的输出则必须按照如下步骤处理:

  1. 利用Map接口中提供的entrySet()方法将Map集合转为Set集合;
  2. 利用Set接口中的iterator()方法将Set集合转为Iterator接口实例;
  3. 利用Iterator进行迭代输出,获取每一张Map.Entry对象,随后通过getKey()与getValue()获取数据。

利用Iterator输出Map集合

package per.lyz.demo;

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

public class Map_Study {
	public static void main(String[] args) {
		Map<String,Integer> map = new HashMap<String,Integer>();
		map.put("one", 1);
		map.put("two", 2);
		Set<Map.Entry<String,Integer>> set = map.entrySet();		//将Map集合变为Set集合
		Iterator<Map.Entry<String,Integer>> iter = set.iterator();
		while (iter.hasNext()) {
			Map.Entry<String, Integer> entry = iter.next();
			System.out.println(entry.getKey()+ "-" + entry.getValue());
		}
	}
}

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

for(Map.Entry<String, Integer> entry : set) {
			System.out.println(entry.getKey() + "-" + entry.getValue());
		}

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

自定义Map的key类型

  在使用Map集合时可以发现对于Key和Value可以由使用者任意决定,意味着现在依然可以使用自定义的类进行Key的设置。

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()方法进行查询的时候还需要使用equal()方法。

  对于自定义Key类型所在的类中一定要覆写hashCode()与equals()方法,否则无法查找到。

使用自定义类作为Key类型

package per.lyz.demo;

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

class Number{
	private String name;
	private int age;
	public Number(String name, int age) {
		super();
		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;
		Number other = (Number) 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 Map_Study {
	public static void main(String[] args) {
		Map<Number,String> map = new HashMap<Number,String>();
		map.put(new Number("lyz", 78), "test");
		System.out.println(map.get(new Number("lyz", 78)));		//null	
	}
}

  虽然允许使用自定义类作为Key类型,但是也需要注意一点实际开发中对于Map集合的Key常用的类型就是:String、Long、Integer。

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

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

6sBvW9.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值