黑马程序员——集合框架4:Map集合

------- android培训java培训、期待与您交流! ----------

在前面的内容中我们介绍了集合框架中的List集合、Set集合以及JDK1.5版本新特性——泛型的内容,在这篇博客中我们来继续介绍集合框架中的另一个重要部分——Map集合。

1.     Map集合的特点

我们去查阅Map集合的API文档可以发现,Map接口的泛型类型有两个,分别是K和V,这与我们之前看到的其他只应用一个泛型类型的接口和类是不同的。其中,K代表Key,意思是键;V代表Value,意思是值。另外,Map的英文本意就有映射的意思,这其实也就体现了Map集合存储元素的特点。看到这我们就可以很自然的联想到,我们在使用Map集合的时候需要同时存储两个元素——键和与其对应的值,而键与值之间存在着映射关系,或者说是一一对应的关系。这里我们引用一下Map接口API文档对它的描述:将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。可以将Map集合和List集合进行比较:List集合其实也有映射关系,只不过它的键是基本数据类型——int,与其对应的值是对象;Map集合的的映射关系是,对象与对象之间的映射。

 

小知识点1:

我们阅读Map接口的API文档知可知,Map并不是Collection接口的子接口,可以说Map是单独成一派的,但两个接口之间还是由其内在联系。

 

我们用一句话来总结Map集合的特点:该集合存储一对一对的键值对,可以保证键值对映射关系的唯一性。

2.     Map接口常见实现类

       Map接口有三个较为常见的实现类,分别为:Hashtable、HashMap、TreeMap,我们就按照这个顺序对其逐一进行介绍。

2.1 Hashtable

Hashtable类的API文档描述为:此类实现一个哈希表,该哈希表将键映射到相应的值。任何非null对象都可以用作键或值。为了成功地在哈希表中存储和获取对象,用作键的对象必须实现hashCode方法和equals方法。这与我们之前介绍的HashSet集合是类似的,若需要把对象存储到HashSet集合中,那么被存储对象所属的类必须复写hashCode和equals方法。

此外,Hashtable出现于JDK1.0版本中,那么通常该类的方法都是实现了同步机制的,这也可以从API文档描述得到证实。

2.2 HashMap

同样,我们首先来阅读该类的API文档:基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。(除了非同步和允许使用null 之外,HashMap类与Hashtable大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。简单来说,HashMap就是Hashtable的升级版本,而升级版本通常都不会默认具备同步功能,因此HashMap是不同步的。

 

小知识点1:

       区分HashMap和Hashtable之间的不同是面试中经常被问到的题目,那么除了回答前者不同步,后者同步以外,一定要答出,前者允许null值和null键;而后者不允许null值和键。如果想要表达得更为全面,那么两者的出现版本也不同,而且前者因未使用同步机制而效率较高,后者效率较低。另外,如果问两者的相同点,那就是两个底层都是用了哈希表数据结构。

 

2.3   TreeMap

基于二叉树的数据结构的Map集合。它可以根据键的自然顺序进行排序,或者根据创建集合时提供的Comparator比较器进行排序。同样,线程不同步。

       了解了上述三个Map实现类的特点,细心的朋友可能会发现,Map集合的特点与Set集合非常类似,而实际上Set底层的实现原理就是应用的Map集合,有兴趣的朋友可以去查阅Map集合的源代码。

3       Map集合共性方法

3.1 Map集合共性方法介绍

与学习Set和List集合一样,我们还是通过顶层接口Map,来了解Map集合系列的共性方法。详细的方法信息,请参考Map集合的API文档。

(a)    添加

V put(K key, Vvalue):向集合中存储一个指定键值对。返回值为该映射关系中的值。

voidputAll(Map<?extends K, ? extends V> m):向集合中存储参数集合中的所有映射关系。

(b)    删除

void clear():从此映射中移除所有映射关系。可以理解为删除全部元素。

V remove(Object key):从集合中删除指定键对应的值,换句话说,删除指定键值对。返回值为被删除的值。

(c)    判断

booleancontainsKey(Objectkey):判断集合中是否存在指定的键。若存在,则返回true;否则返回false。

booleancontainsValue(Objectvalue):判断集合中是否存在指定的值。若存在,则返回true;

否则返回false。

booleanisEmpty():判断集合是否为空。如果该集合中没有任何元素,则返回true。

(d)    获取

V get(Object key):返回指定键对应的值。如果集合中不存在指定键,那么返回null。

int size():获取集合的长度。

Collection<V>values():将该集合中所有值存储到一个新集合中,并返回该新集合。

以上这些方法相对比较易于掌握,而我们需要重点介绍的是entrySetkeySet两个方法。  按照以往的惯例,我们通过顶层接口来了解该集合系列的共性方法,并通过该接口的实现类来掌握这些方法的具体调用方式及效果,因此在应用这些方法以前首先简单介绍Map接口常见实现类及其特点。

3.2Map集合共性方法应用

这里我们以HashMap为例介绍Map集合的具体调用方式及效果。

3.2.1基本方法应用

首先介绍Map集合较为常见而易于掌握的一些方法,阅读下面代码,

代码1:

import java.util.*;
 
class MapDemo
{
	public static void main(String[] args)
	{
		//多态地创建一个HashMap集合对象
		Map<String,String> map = new HashMap<String,String>();
 
		//添加非空键值对
		map.put("201501","Zoey");
		map.put("201502","Peter");
		map.put("201503","Charlie");
		map.put("201504","Jack");
 
		//判断集合中是否存在指定的键
		System.out.println("containsKey:"+map.containsKey("201502"));
		//判断集合是否存在指定的值
		System.out.println("containsValue:"+map.containsValue("Charlie"));
		System.out.println("=======================================");
 
		//删除集合中指定键对应的键值对
		System.out.println(map);//删除元素前打印集合内容
		System.out.println("remove:"+map.remove("201502"));//删除集合中存在的键值对
		System.out.println("remove:"+map.remove("201505"));//删除集合中不存在键值对
		System.out.println(map);//删除元素后打印集合内容
		System.out.println("=======================================");
 
		//获取集合中指定键对应的值
		System.out.println("201503="+map.get("201503"));//获取集合中存在的键值对
		System.out.println("201523="+map.get("201523"));//获取集合中不存在的键值对
		System.out.println("=======================================");
 
		//获取集合中所有的值
		Collection<String> coll= map.values();
		System.out.println(coll);
	}
}
运行结果为:

containsKey:true

containsValue:true

=======================================

{201503=Charlie, 201502=Peter,201504=Jack, 201501=Zoey}

remove:Peter

remove:null

{201503=Charlie, 201504=Jack,201501=Zoey}

=======================================

201503=Charlie

201523=null

=======================================

[Charlie, Jack, Zoey]

运行结果表明,元素的打印顺序与存储顺序是无关,这也就体现了哈希表数据结构元素顺序的无序性。

作两点补充说明:

(1)   除了通过containsKey和containsValue方法以外,我们还可以通过remove或get方法的返回值来判断集合中是否存在指定键对应的值,也就是说当这两个方法的返回值为null时,表明集合中不存在指定键对应的键值对。

(2)   API文档中显示的put方法返回值类型为V,而实际的返回值为指定键对应的前一个值,观察下面的代码,

代码2:

import java.util.*;
 
class MapDemo2
{
	public static void main(String[] args)
	{
		Map<String,String> map = new HashMap<String,String>();
 
		System.out.println("key1="+map.put("key1","value1"));
		//将key1键对应的值更新为value2
		System.out.println("key1="+map.put("key1","value2"));   
	}
}
运行结果为:

key1=null

key1=value1

第一行结果表明,键key1对应的前一个值为null,换句话说在存储key1和value1键值对以前,集合中不存在key1的值,而存储了这一键值对后key1真正对应的值为value1;此后,将key1对应的值设置为value2时,打印的值为前一个对应的值value1。那么其实,这一过程也就体现了,哈希表数据结构保证元素(这里指的是值)唯一性的特点,或者说映射关系的唯一性——一个键只能对应一个值,当存储相同键时,新值将覆盖旧值,以保证值的唯一性。

       我们再举一个特殊情况,由前述HashMap的API文档可知,HashMap是允许存存储空键和空值的,阅读下面代码

代码3:

import java.util.*;
 
class MapDemo3
{
	public static void main(String[] args)
	{
		Map<String,String> map = new HashMap<String,String>();
 
		//添加元素
		map.put(null,"value1");//空键,非空值
		map.put("key2",null);//非空键,空值
 
		//获取元素
		System.out.println("null="+map.get(null));
		System.out.println("key2="+map.get("key2"));
	}
}
运行结果为:

null=value1

key2=null

上述结果表明,HashMap集合是允许存储空键或空值的,并且可以通过null来获取对应的值,或者通过非空键来获取null值。当然这种情况在实际开发中是很少见的。此外,键与值均为空也是允许的,有兴趣的朋友可以自行尝试。

3.2.2 高级方法应用

       在上面的内容中我们介绍了Map集合的一些基本方法,包括了增删改查,而其中通过get方法获取元素的方式是比较低效的,因为一次只能取一个元素,而Map作为集合框架的一员,我们自然就想到通过迭代器的方式获取到全部元素,keySet和entrySet两个方法便间接地实现了这一功能。

(1)   keySet

Set<K>Keyset():将集合中所有元素均存储到一个Set集合中,并返回该Set集合。

通过keySet方法获取元素的思想就是首先获取到集合中的所有键,而这些键都存储在一个Set集合中,通过Set集合的迭代器取出其中所有的键,再通过Map集合get方法,获取到键所对应的值,阅读下面的代码,

代码4:

import java.util.*;
 
class MapDemo4
{
	public static void main(String[] args)
	{
		Map<String,String> map = new HashMap<String,String>();
 
		map.put("201501","Sara");
		map.put("201502","Bob");
		map.put("201503","Anna");
		map.put("201504","Charlie");
		map.put("201505","Douglas");
		map.put("201506","Emma");
 
		//获取到键的Set集合
		Set<String> numbers = map.keySet();//Set集合的泛型是Map中键的类型
		String number = null, name = null;
		//通过迭代的方式获取到所有键的Set集合,再通过get方法获取到键对应的值
		for(Iterator<String> it = numbers.iterator(); it.hasNext(); )
		{
			number= it.next();//获取到Set集合中的键
			name= map.get(number);//通过get获取到Map中的值
			System.out.println(number+"= "+name);
		}
	}
}
运行结果为:

201506 = Emma

201503 = Anna

201502 = Bob

201505 = Douglas

201504 = Charlie

201501 = Sara

通过上述方法就先后获取到了Map集合中的所有键与值。这里我们强调了“先后”,因为上述获取过程的确是先获取键,再获取到值,而下面我们要介绍的entrySet方法可以实现同时获取键值对。

(2)   entrySet

Set<Map.Entry<K,V>>entrySet():将Map集合中的键值对(或称为映射关系)存储到一个Set集合中,并返回该Set集合。

       我们看到entrySet方法的返回值Set的泛型类型为Map.Entry<K,V>,而由Map.Entry<K,V>后面的泛型类型可知其用于存储键值对,或者称为键与值的映射关系,就是说将键与值作为一个整体存储到Map.Entry后,再将Map.Entry作为元素存储到Set集合中。那么最终,我们再从Map.Entry中取出键与值。我们去查阅Map.Entry的API文档可知,它是一个接口,并定义了若干方法,其中getKey和getValue方法分别用于获取键和获取值。另外,需要提醒大家的,Map.Entry接口的实现类都是每个Map集合类的内部类,这与Iterator是相类似的。观察下面的代码,

代码5:

import java.util.*;
 
class MapDemo5
{
	public static void main(String[] args)
	{
		Map<String,String> map = new HashMap<String,String>();
 
		map.put("201501","Sara");
		map.put("201502","Bob");
		map.put("201503","Anna");
		map.put("201504","Charlie");
		map.put("201505","Douglas");
		map.put("201506","Emma");
 
		//将Map集合中的映射关系取出,存入到Set集合中
		Set<Map.Entry<String,String>> entrySet= map.entrySet();
		for(Iterator<Map.Entry<String,String>>it = entrySet.iterator(); it.hasNext(); )
		{
			Map.Entry<String,String> me = it.next();//依次取出entrySet集合中的Map.Entry
			//再通过Map.Entry的getKey和getValue分别获取到键和值
			String key = me.getKey();
			String value = me.getValue();
 
			System.out.println(key+"= "+value);
		}
	}
}
运行结果为:

201506 = Emma

201503 = Anna

201502 = Bob

201505 = Douglas

201504 = Charlie

201501 = Sara

结果表明,通过entrySet方法同样可以获取到Map集合中的所有键和值。我们通过下图来更为形象的介绍,通过entrySet方法获取键值对的过程。

       首先,Map集合中存储了一对一对的键值对,如下左图所示,entrySet方法返回的就是存储着键与值映射关系的Set集合,下右图所示,Set集合中存储的是将键与值作为一个整体的Map.Entry对象。

 

  因此我们可以将Map.Entry对象理解为,键与值的关系对象,它既不能是键类型,也不能是值类型,因此单独定义了一个类型——Map.Entry——用于描述键与值的映射关系。实际上,Entry也是一个接口,而从Map.Entry这样的语法格式我们也可以推测出,Entry是Map接口的内部接口,它的源代码(部分)为:

代码6:

interface Map<K,V>
{
	//Map接口的公有静态内部接口,泛型类型同为K,V
	public static interface Entry<K,V>
	{
		//只呈现了部分方法
		public abstract K getKey();//获取键的抽象方法,需由实现类复写
		public abstract V getValue();//获取值的抽象方法,需由实现类复写
	}
}
//以HashMap为例来说明其实现方式
class HashMap implementsMap<K,V>//实现Map接口
{
	//定义一个内部类,实现Map的内部接口Entry
       class HashEntry implements Map.Entry<K,V>
	{
		//复写Map.Entry接口的抽象方法
		public K getKey(){}
		public V getValue(){}
	}
}
其实对于Map.Entry的理解,与Iterator是类似的。因为Iterator是直接操作List或者Set集合内部的元素,因此它的实现类均是作为集合类的内部类存在。同样,只有首先有Map接口存在,并向其中存储元素,才能存在键与值的映射关系,换句话说,Entry同样是Map这个事物的内部事物,因此Entry被定义为了Map接口的内部接口,其实现类也同样作为Map实现类的内部类存在。

       可以通过Map接口的API文档来验证上述说明。嵌套类摘要中的唯一一项就是Map.Entry接口,对它的说明为:映射项(键-值对)。再打开Map.Entry的API文档,其接口名修饰符为“publicstatic”,这就表明Entry处于Map接口的成员位置上。

4.    Map集合练习

4.1  练习一

需求:模拟存储学生信息的过程。规定学生有两个属性——姓名和年龄,并以学生对象为键,以学生地址为值,姓名和年龄相同的学生视为同一个学生。要求是保证学生的唯一性。

分析:

(1)   定义学生类,定义两个成员变量(属性)——姓名和地址,并定义对应的获取姓名和地址的方法。

(2)   创建一个Map集合,将学生作为键,地址作为值,存入。

(3)   获取Map集合中的元素。

代码7:

import java.util.*;
 
class Student implementsComparable<Student>
{
	private String name;
	private intage;
	Student(String name, int age)
	{
		this.name= name;
		this.age= age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
	public String toString()
	{
		return name+" = "+age;
	}
	/*
		为了便于后期将Student对象存储到哈希表数据结构的集合
		(无论HashSet还是HashMap)中
		所以需要复写hashCode和equals方法
	*/
	public int hashCode()
	{
		return name.hashCode()*37+age;
	}
	public booleanequals(Object obj)
	{
		if(!(objinstanceofStudent))
		/*
			当传入的参数类型不是Student时,抛出异常(运行时异常子类)
			停止程序的继续运行
		*/
			throw new IllegalArgumentException("参数类型不符!");
 
		Student stu = (Student)obj;
		return name.equals(stu.getName())&& age == stu.getAge();
	}
	/*
		为了便于将Student对象存储到二叉树数据结构的集合中
		需要实现Comparable接口,并复写compareTo方法
	*/
       public int compareTo(Studentstu)
	{
		int value = new String(name).compareTo(stu.getName());
 
		if(value== 0)
			return new Integer(age).compareTo(new Integer(stu.getName()));
		return value;
	}
}
class MapTest
{
	public static void main(String[] args)
	{
		HashMap<Student,String> hm= new HashMap<Student,String>();
 
		hm.put(newStudent("Peter",23),"USA");
		hm.put(newStudent("Robert",31),"Canada");
		hm.put(newStudent("Susan",19),"France");
		hm.put(newStudent("Robert",31),"Italy");//存储同名同年龄,但不同地址的键值对
 
		//用于接收Student对象和地址字符串对象的变量
		Student stu = null;
		String address = null;
 
		//通过keySet方法获取元素
		Set<Student> students = hm.keySet();
		for(Iterator<Student>it = students.iterator(); it.hasNext(); )
		{
			stu= it.next();
			address= hm.get(stu);
			System.out.println(stu+": "+address);
		}
		System.out.println("==============================================");
 
		//通过entrySet方法获取元素
		Set<Map.Entry<Student,String>> entryset= hm.entrySet();
		Map.Entry<Student,String> me = null;
		for(Iterator<Map.Entry<Student,String>>it = entryset.iterator(); it.hasNext(); )
		{
			me= it.next();
			stu= me.getKey();
			address= me.getValue();
			System.out.println(stu+": "+address);
		}
	}
}
运行结果为:

Peter = 23 : USA

Robert = 31 : Italy

Susan = 19 : France

================================================

Peter = 23 : USA

Robert = 31 : Italy

Susan = 19 : France

在上述代码中我们分别使用keySet和entrySet两种方式获取了HashMap集合中的所有元素。

       由于在实际操作的过程中,可能会产生大量的学生对象,通常需要一些容器对其进行存储。因此,我们为Student类复写了hashCode方法和equals方法,方便后期存储到哈希表数据结构的集合中;实现了Comparable接口,并复写了compareTo方法,便于后期存储到二叉树数据结构的集合中。这也是今后在实际开发过程中需要注意的——如果需要大量创建自定义类对象,那么尽量复写hashCode和equals方法,并实现Comparable接口,复写compreTo方法。

       我们注意到,由于复写了hashCode和equals方法,HashMap集合就展现了其保证元素唯一性的特征——当存储的键相同(同名同年龄),而值(地址)不相同时,新值会覆盖旧值,因此Robert的地址被更新为了Italy。

4.2  练习二

需求:同上述练习一,但需要对学生对象按照年龄进行升序排序。

分析:主要步骤与练习一相同,因需要对元素进行排序,因此存储容器需要更改为TreeMap集合。

代码8:

import java.util.*;
 
public class Student implements Comparable<Student>
{
	private String name;
	private int age;
	public Student(String name, int age)
	{
		this.name= name;
		this.age= age;
	}
	public String getName()
	{
		return name;
	}
	public int getAge()
	{
		return age;
	}
	public String toString()
	{
		return name+" = "+age;
	}
	public int hashCode()
	{
		return name.hashCode()*37+age;
	}
	public boolean equals(Object obj)
	{
		if(!(objinstanceofStudent))
			throw new IllegalArgumentException("参数类型不符!");
 
		Student stu = (Student)obj;
		return name.equals(stu.getName())&& age == stu.getAge();
	}
	/*
		由于需要按照年龄升序排序
		因此,compareTo方法中的主要条件为年龄
		次要条件为姓名
	*/
	public int compareTo(Studentstu)
	{
		intv alue = new Integer(age).compareTo(new Integer(stu.getAge()));
 
		if(value== 0)
			return name.compareTo(stu.getName());
		return value;
	}
}
public class MapTest2
{
	public static void main(String[] args)
	{
		TreeMap<Student,String> tm = new TreeMap<Student,String>();
 
		tm.put(newStudent("Peter",23),"USA");
		tm.put(newStudent("Robert",31),"Canada");
		tm.put(newStudent("Susan",19),"France");
		tm.put(newStudent("Robert",31),"Italy");//存储同名同年龄,但不同地址的键值对
 
		Student stu = null;
		String address = null;
 
		//通过keySet方法获取元素
		Set<Student> students = tm.keySet();
		for(Iterator<Student>it = students.iterator(); it.hasNext(); )
		{
			stu= it.next();
			address= tm.get(stu);
			System.out.println(stu+": "+address);
		}
		System.out.println("=========================================");
 
		//通过entrySet方法获取元素
		Map.Entry<Student,String> me = null;
		Set<Map.Entry<Student,String>> entryset= tm.entrySet();
		for(Iterator<Map.Entry<Student,String>>it = entryset.iterator(); it.hasNext(); )
		{
			me= it.next();
			stu= me.getKey();
			address= me.getValue();
			System.out.println(stu+": "+address);
		}
	}
}
运行结果为:

Susan = 19 : France

Peter = 23 : USA

Robert = 31 : Italy

=========================================

Susan = 19 : France

Peter = 23 : USA

Robert = 31 : Italy

结果显示,无论采用哪种获取元素方式,取出顺序均是按照Student对象的年龄升序排序的,并且存储同键元素的时候,新值覆盖了旧值。如果我们还想按照姓名的升序排序的话,在不修改原有代码的前提下,定义一个比较器类时最好的办法,如下代码所示,

代码9:只呈现定义比较器类的代码

//定义按照学生姓名升序排序的比较器类
class StudentNameComparator implements Comparator<Student>
{
	//姓名为主要条件,年龄为次要条件
	public int compare(Student stu1,Student stu2)
	{
		int value = stu1.getName().compareTo(stu2.getName());
 
		if(value== 0)
			return new Integer(stu1.getAge()).compareTo(new Integer(stu2.getAge()));
		return value;
	}
}
创建上述StudentNameComparator对象,并将其作为参数传递到TreeMap的构造函数中,再次执行代码8,结果为:

Peter = 23 : USA

Robert = 31 : Italy

Susan = 19 : France

=========================================

Peter = 23 : USA

Robert = 31 : Italy

Susan = 19 : France

Map集合中的元素是按照Student对象的姓名升序排序的。

4.3  练习三

需求:获取字符串“soihgrhrhfvdhguhfi”中每个字母出现的次数。要求打印结果的格式为:a(2)b(2)…。

思路:字符与个数之间存在着一一对应的关系,那么就可以使用Map集合将字符和与其对应的个数成对地存储起来。存储时,若该字符不存在于集合中,则存储字符和数字1;若已存在,就将字符个数自增再存储。

实现方式:

我们使用两种方法实现目标需求,这两种方法首先都要将字符串转换为字符数组,这样做的好处是避免了反复调用charAt方法来获取每个字符,以此提高代码执行效率。

方法一:

遍历字符数组。获取字符数组中的第一个字符,将其作为键,判断该键是否存在于集合中。此时集合为空,显然不存在任何元素,就将该字符作为键,1作为值(个数),存储到Map集合中。同理,此后遍历到的字符只要不存在于集合中,就将该字符和1成对地存储到集合中。相反,若遍历到的字符已存在于集合中,就将其对应的值(个数)取出,自增后再重新存储。我们知道,当向Map集合中存储同值不同键的元素时,新值将覆盖旧值,这样就可以起到计数的效果。最后,将键值对(字符与对应的个数)按照给定格式,打印到控制台。

方法二:

       根据方法一的执行流程可以观察到,无论字符对应的个数是否自增,最终都是要存储到集合中的。因此,只需要判断是否需要将值自增即可。由此,我们在对字符数组进行遍历以前需要定义一个计数器变量。执行流程同样要遍历字符数组。通过字符,从集合中获取到该字符对应的个数,判断该个数是否为空,若为空,则令计数器从初始化值(0)自增,最后存储字符和计数器记录的个数;若不为空,则将值赋给计数器,同样令计数器自增,成对存储字符和计数器数值。

代码10:

import java.util.*;
 
class MapTest4
{
	public static void main(String[] args)
	{
		String source = "soihgrhrhfvdhguhfi";
		char[] chs = source.toCharArray();//将字符串转成字符数组,以提高程序执行效率
		TreeMap<Character,Integer> tm = new TreeMap<Character,Integer>();
 
		//方法一:判断键(字符)是否存在于集合中
		char ch = 0;//将临时变量定义在循环体外,以避免占用过多的栈内存空间
		for(int x=0; x<chs.length; x++)
		{
			ch= chs[x];
 
			if(ch<'a'&&ch>'z' || ch<'A' &&ch>'Z')//过滤非字母字符
				continue;
 
			if(!tm.containsKey(ch))
			//如果集合中不存在指定字符,则存储该字符和1
				tm.put(ch,new Integer(1));
			else
			//如果集合中已存在指定字符,则存储该字符,并将个数自增再存储
				tm.put(ch,tm.get(ch)+1);
 
			ch= 0;
		}
             
		//方法二:判断值(个数)是否存在于集合中
		//将一下临时变量定义在循环体外,以避免占用过多的栈内存空间
		Integer value = null;//记录字符对应个数
		char ch = 0;//记录字符
		int count = 0;//字符个数计数器
		for(int x=0; x<chs.length; x++)
		{
			ch= chs[x];
 
			if(ch<'a'&&ch>'z' || ch<'A' &&ch>'Z')//过滤非字母字符
				continue;
 
			value= tm.get(ch);//获取到指定字符对应的个数
			if(value!= null)//若值(个数)为空,则将值赋给计数器
				count= value;
			count++;//计数器自增
 
			tm.put(ch,count);//存储字符和自增后的计数器
 
			count= 0;//将计数器清零
		}
             
		//打印结果
		printMapElements(tm);
	}
	//按照指定格式打印集合中的元素。
	public static void printMapElements(Map<Character,Integer> map)
	{
		Set<Map.Entry<Character,Integer>> entrySet= map.entrySet();
		Map.Entry<Character,Integer> me = null;
		for(Iterator<Map.Entry<Character,Integer>>it = entrySet.iterator(); it.hasNext(); )
		{
			me= it.next();
			System.out.print(me.getKey()+"("+me.getValue()+")");
		}
	}
}
上述代码使用了两种方法实现目标需求,执行结果均为:

d(1)f(2)g(2)h(5)i(2)o(1)r(2)s(1)u(1)v(1)

对于以上代码我们做出以下几点说明:

(1)   由于Map集合中只能存储引用数据类型,因此我们真正存储到集合中的是char和int类型的包装类Character和Integer对象,分别表示表示字符和其对应的个数。

(2)   由于Character类已经实现了Comparable接口,因此存储到TreeMap中时可以自动按照字母顺序排序。

(3)   用于存储字符和记录个数的变量,应定义在for循环体外,否则每执行一次循环体,就会在栈内存创建一个新的变量,导致短时间内占用过多的栈内存空间。

       通过上述例程,我们可以做出如下总结:需要存储相互之间存在映射关系的数据时,就要优先使用Map集合。

4.4  练习四

需求:假设有一个学校,学校中有两个班级——普通班和实验班,每个班各有两名学生,普通班中的学生分别是:PT01,Jack;PT02,Kate(前学号,后姓名);实验班中的学生分别是:SY01,Tom;SY02,Susan。要求使用Map集合来表示学校与班级、班级与学生之间的关系。

思路:

       存储过程:从下往上思考,首先创建上述四个学生对象,然后创建两个Map集合对象代表两个班级,将四个学生对象分别存储到两个班级中,存储时学号为键,姓名为值。最后再创建一个Map集合表示学校,将上述两个班级存储到学校Map中,班级名称为键,表示班级的Map对象为值。按照上述思路,实际编写代码时,应从上往下编写。

       获取元素过程:这里以keySet方法为例。首先获取到存有所有班级名称的Set集合对象,利用迭代器开启一层for循环,每获取一个班级名称,通过get方法获取对应的班级Map对象。获取到班级Map对象以后,获取存储所有学生学号的Set集合,同样利用迭代器再开启一层for循环,每获取一个学生学号,就通过get方法获取对应的学生姓名,最后按照班级、学号、姓名的顺序将信息打印至控制台。有兴趣的朋友也可以尝试使用entrySet方法来实现相同的功能。

代码11:

import java.util.*;
 
//为演示方便不再单独定义Student类
class MapTest5
{
	public static void main(String[] args)
	{
		//以下Map集合表示学校,用于存储班级
		HashMap<String, HashMap<String,String>> school = new HashMap<String, HashMap<String,String>>();
 
		//以下两个Map集合表示班级,用于存储学生
		HashMap<String,String> ptclass = new HashMap<String,String>();
		HashMap<String,String> syclass = new HashMap<String,String>();
 
		//向学校中存储班级
		school.put("普通班",ptclass);
		school.put("实验班",syclass);
 
		//向班级中存储学生
		ptclass.put("PT01","Jack");
		ptclass.put("PT02","Kate");
 
		syclass.put("SY01","Tom");
		syclass.put("SY02","Susan");
	}
	//打印所有班级所有学生的方法
	public static void printStudentInfo(HashMap<String,HashMap> school)
	{
		String classname =null;
		HashMap<String,String> classroom = null;
 
		//外层for循环获取到所有的班级
		for(Iterator<String>classit = school.keySet().iterator(); classit.hasNext(); )
		{    
			//班级名称
			classname= classit.next();
			//通过班级名称获取代表班级的Map对象
			classroom= school.get(classname);
 
			printSingleClassStudentInfo(classname,classroom);//打印指定班级
		}
	}
	public static void printSingleClassStudentInfo(String classname,HashMap<String,String> classroom)//打印一个班级所有学生的方法
	{
		String stunum = null;
		String stuname = null;
 
		//内层循环获取到班级中的所有学生
		for(Iterator<String>stuit = classroom.keySet().iterator(); stuit.hasNext(); )
		{
			stunum= stuit.next();//获取学号
			stuname= classroom.get(stunum);//获取姓名
			//打印班级、学号、姓名
			System.out.println(classname+":"+stunum+","+stuname);
		}
	}
}
运行结果:

普通班:PT01,Jack

普通班:PT02,Kate

实验班:SY01,Tom

实验班:SY02,Susan

通过上述代码就将学校中的所有学生的信息都获取到,并打印在了控制台上。需要说明的是,为便于今后需要时,单独打印某一个班级所有学生的信息,将这部分代码封装为了一个单独的方法。

       上述代码是为演示双重Map集合元素的存储与获取的方式,因而将学号与姓名作为键值对存储在了内层Map集合中,而在实际开发中通常会单独定义一个学生类,并通过创建Student对象来存储学号与姓名,因此内层集合应定义为单列集合,比如List集合,或者Set集合。相应的,获取元素的方法也应相应的做出修改,现将代码直接给出,

代码12:

import java.util.*;
 
class Student
{
	privateString num,name;
	Student(String num,String name)
	{
		this.num= num;
		this.name= name;
	}
	public String getNum()
	{
		return num;
	}
	public String getName()
	{
		return name;
	}
}
//为演示方便不再单独定义Student类
class MapTest6
{
	public static void main(String[] args)
	{
		//以下Map集合表示学校,用于存储班级
		HashMap<String,List<Student>> school = new HashMap<String,List<Student>>();
 
		//以下两个List集合表示班级,用于存储学生
		List<Student> ptclass = new ArrayList<Student>();
		List<Student> syclass = new ArrayList<Student>();
 
		//向学校中存储班级
		school.put("普通班",ptclass);
		school.put("实验班",syclass);
 
		//向班级中存储学生
		ptclass.add(newStudent("PT01","Jack"));
		ptclass.add(newStudent("PT02","Kate"));
 
		syclass.add(newStudent("SY01","Tom"));
		syclass.add(newStudent("SY02","Susan"));
 
		printStudentInfo(school);
	}
	//打印所有班级所有学生的方法,这里使用了entrySet方法
	public static void printStudentInfo(HashMap<String,List<Student>> school)
	{
		Map.Entry<String,List<Student>> me = null;
		String classname = null;
		List<Student> classroom = null;
 
		//外层for循环获取到所有的班级
		for(Iterator<Map.Entry<String,List<Student>>> classit = school.entrySet().iterator(); classit.hasNext(); )
		{    
			//键值对关系对象
			me= classit.next();
			//班级名称
			classname= me.getKey();
			//通过班级名称获取代表班级的Map对象
			classroom= me.getValue();
 
			printSingleClassStudentInfo(classname,classroom);//打印指定班级
		}
	}
       public static void printSingleClassStudentInfo(String classname,List<Student> classroom)//打印一个班级所有学生的方法,第一个参数为班级名称
	{
		Student stu = null;
		String stunum = null;
		Stringstuname = null;
 
		//内层循环获取到班级中的所有学生
		for(Iterator<Student>stuit = classroom.iterator(); stuit.hasNext(); )
		{
			stu= stuit.next();//获取学生对象
			stunum= stu.getNum();//获取学号
			stuname= stu.getName();//获取姓名
			//打印班级、学号、姓名
			System.out.println(classname+":"+stunum+","+stuname);
		}
	}
}
执行结果同样为:

普通班:PT01,Jack

普通班:PT02,Kate

实验班:SY01,Tom

实验班:SY02,Susan


小知识点2:

       实际上,无论是HashSet还是TreeSet,它们底层所使用的容器还是Map,只不过Set集合仅使用了Map的键,而没有使用值而已。我们可以看一看HashSet集合的源代码。以下是截取的一部分HashSet源代码。

代码13:

public class HashSet<E> extendsAbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
	private transient HashMap<E,Object>map;
 
	// Dummy value to associate with an Objectin the backing Map
	private static final Object PRESENT = newObject();
 
	/*
	 *
	 * Constructs a new, empty set; the backing<tt>HashMap</tt> instance has
	 * default initial capacity (16) and loadfactor (0.75).
	 */
	public HashSet() {
		map = new HashMap<>();
	}
 
	public boolean add(E e) {
		return map.put(e, PRESENT)==null;
	}
}
从以上代码可以看出,HashSet集合内部定义有一个HashMap类型的成员变量,并在调用空参数HashSet构造方法时,为该成员变量初始化一个HashMap对象。那么从名为map的成员变量的类型声明来看,第一个类型参数是E,这需要由调用者指定,而第二个类型参数被固定为Object,因此HashSet仅使用了HashMap键的部分,而无法使用值的部分。再来看看add方,在向HashSet中存储元素时,其实也就是向HashMap中存储元素,键的部分是外部调用者传递的有效元素,而值的部分是一个名为PRESENT的Object类型常量,该常量同样定义在HashSet集合的成员位置,专门用于填充HashMap集合值部分的空白。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值