Map接口可以将键(Key)映射到值(Value)的对象,一个键可以映射到最多一个值。这意味着在Map中,每个键 (K) 都是唯一的,但值 (V) 可以重复。
Set接口在内部也是使用键值对(K-V)的方式来存储元素的(这是基于它们底层使用的Map实现)。在Set接口中,键(K)就是Set中存储的元素本身,而值(V)通常是一个固定的占位符对象,通常是内部类的一个实例,例如HashMap中的PRESENT,它是一个静态的Object实例。
Map介绍
1、Map和Collection并列存在,用于保存具有映射关系的数据(K-V双列元素)。
2、Map中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
3、Map中的 key 不允许重复(value可重复),如有重复,则会将新的K-V替换掉原本的value值
4、Map中的K和V都可为空,但K只能有一个为空,V可以有多个为空
Map myMap = new HashMap();
myMap.put("key1","value1");
myMap.put(null,null);//k和v都为空
myMap.put(null,"value");//k也为空,v会替换之前的值
myMap.put("key2",null);//v为空
myMap.put("key3",null);//v再次为空
此时Map:{key1=value1, null=value, key2=null, key3=null}
5、一般使用String来作Map的key,其他类型的数据也可,但不推荐
6、一对k-v存放在一个HashMap$Node = newNode(hash, key, value, null)中
7、为方便遍历,还会创建EntrySet集合,该集合存放的元素类型为 Entry ,而每一个Entry实际存放的是HashMap$Node(因为static class Node<K,V> implements Map.Entry<K,V>),以便使用Entry的 getKey 和 getValue 方法,同时还提供了 KeySet (属Set接口) 和 values (属Collection接口) 类以便单独查看K和V
常用方法
1、put():添加元素,依次输入k和v的值
2、remove():删除指定元素,有两种重载版本
remove(Object key):删除该键下的所有元素,删除成功返回该k对应的v值,失败返回null;remove(Object key,Object value):只有当键存在且与给定的值相匹配时,才会从映射中移除该键值对。成功返回true,失败返回false
3、get():查看元素,输入k,返回对应的v值,不存在则返回null,返回的元素类型由value的类型确定
4、size():查看长度,返回该集合的元素个数
5、isEmpty():是否为空:查看该集合是否存在元素
6、clear():清空该集合内的所有元素
7、containsKey():查找该键是否存在;containsValue()查找该值是否存在
遍历方式
上文我们介绍了为方便遍历,还会创建EntrySet集合以及对应的KeySet和values,所以其至少包含三种遍历方式。
一、通过Key遍历
Map myMap = new HashMap();
//添加元素......
Set keySet = myMap.keySet();//创建Set类实例接收keyset集合
for (Object o :keySet) {//增强for循环
System.out.println(o+"-"+myMap.get(o));
}//o为key,get查看key对应的value值
Iterator iterator = keySet.iterator();//迭代器
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next+"-"+myMap.get(next));
}
二、遍历Value
Map myMap = new HashMap();
//添加元素......
Collection values = myMap.values();//创建Collection类实例接收values集合
for (Object o :values) {//增强for循环
System.out.println(o);
}
Iterator iterator = values.iterator();//迭代器
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
三、通过EntrySet遍历
这一类型略微复杂,上文我们提到系统创建的EntrySet集合存放的元素类型为 Entry ,而每一个Entry实际存放的是Node,以便使用Entry的 getKey 和 getValue 方法。
所以我们可以直接输出Node类型的数据,也可将其转为编译类型Entry,再使用Entry的getKey 和 getValue 方法来单独查看Key或Value。
Map myMap = new HashMap();
//添加元素......
Set entrySet = myMap.entrySet();//创建Set类实例接收entrySet集合
for (Object entry :entrySet) {//增强for循环
//此时entry编译类型为Object,运行类型为Node
System.out.println(entry);//直接输出Node类型元素
Map.Entry newEntry=(Map.Entry)entry;//将entry的编译类型由Object转为Entry向下转型
//此时entry编译类型为Entry,运行类型为Node,可调用getKey 和 getValue方法
System.out.println("[key="+newEntry.getKey()+" value="+newEntry.getValue()+"]");
}
Iterator iterator = entrySet.iterator();//迭代器
while (iterator.hasNext()) {
//同理
Object next = iterator.next();
System.out.println(next);
Map.Entry newEntry=(Map.Entry)next;
System.out.println("[key="+newEntry.getKey()+" value="+newEntry.getValue()+"]");
}
例题:使用HashMap添加多个员工对象,要求:
1、键(key)为员工id,值(value)为员工对象。
2、自定义员工类,包含姓名、工资、员工id。
3、使用至少两种遍历方式输出工资>18000的员工
public class Go {
public static void main(String[] args) {
HashMap myMap = new HashMap();
Employee staff1 = new Employee("一一", 3000, "1st");
Employee staff2 = new Employee("二二", 1000, "2nd");
Employee staff3 = new Employee("三三", 1500, "3rd");
Employee staff4 = new Employee("四四", 2000, "4th");
myMap.put(staff1.getId(), staff1);
myMap.put(staff2.getId(), staff2);
myMap.put(staff3.getId(), staff3);
myMap.put(staff4.getId(), staff4);
System.out.println("———————————————通过EntrySet遍历———————————————");
for (Object Entry : myMap.entrySet()) {
Map.Entry newEntry = (Map.Entry) Entry;
Employee emp = (Employee) newEntry.getValue();
if (emp.getSal() > 1800)
System.out.println(newEntry);
}
System.out.println("————————————————通过KeySet遍历————————————————");
for (Object key : myMap.keySet()) {
Employee emp=(Employee)myMap.get(key);
if (emp.getSal()>1800)
System.out.println(key+"="+myMap.get(key));
}
System.out.println("————————————————通过values遍历————————————————");
for (Object o : myMap.values()) {
Employee emp=(Employee)o;
if (emp.getSal()>1800)
System.out.println(emp.getId()+"="+o);
}
System.out.println("————————————————通过迭代器遍历————————————————");
Iterator iterator = myMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry newNext = (Map.Entry) iterator.next();
Employee employee = (Employee) newNext.getValue();
if (employee.getSal() > 1800)
System.out.println(newNext);
}
}
}
class Employee {省略......}
执行结果
———————————————通过EntrySet遍历———————————————
1st=Employee{name='一一', sal=3000.0, id='1st'}
4th=Employee{name='四四', sal=2000.0, id='4th'}
————————————————通过KeySet遍历————————————————
1st=Employee{name='一一', sal=3000.0, id='1st'}
4th=Employee{name='四四', sal=2000.0, id='4th'}
————————————————通过values遍历————————————————
1st=Employee{name='一一', sal=3000.0, id='1st'}
4th=Employee{name='四四', sal=2000.0, id='4th'}
————————————————通过迭代器遍历————————————————
1st=Employee{name='一一', sal=3000.0, id='1st'}
4th=Employee{name='四四', sal=2000.0, id='4th'}
HashMap
由于HashMap与Map的特点高度重和,因此仅简单总结一下:HashMap是Map接口使用频率最高的实现类;以K-V的方式来存储Node型数据,k不能重复但v可以重复,再次添加已有的k会覆盖原k-v,可添加null;HashMap未实现同步,因此线程是不安全的(无synchronized)。与HashSet相同,其取出的顺序也不能保证有序,因为其底层也是根据hash值来存储的。
1、HashMap的底层维护了Node类型的数组table,默认为null;
2、首次添加元素数组大小为16,加载因子为0.75(LoadFactor),临界值为16*0.75=12。之后再扩容则直接扩至2倍;当哈希桶(链表)添加第9个元素时,数组会进行2倍扩容,之后每多添加一个扩容一次,直至满足64个元素,开始树化。
3、添加k-v时,通过hash值得到索引,判断该索引处是否有元素,无则直接添加,有则先判断两k是否相等,相等则替换v,不相等则判断为树结构还是链表结构并进行相应操作,容量不足则扩容;
Hashtable
Hashtable和HashMap属同一级,都实现了Map接口,其大体也与HashMap相同:
1、存放的元素为键值对K-V。
2、常用方法基本相同。
但也有一定的区别:
1、Hashtable的键和值都不能为null,否则会抛出空指针异常。HashMap可以为null
2、Hashtable为线程安全、HashMap为非线程安全(无synchronized)
3、Hashtable的底层维护了Hashtable$Entry,初始大小为11,加载因子为0.75(LoadFactor),临界值为11*0.75=8,之后再扩容则直接扩至(2倍+1);
Properties
Properties为Hashtable的子类,也是使用键值段来保存数据。Properties还可用于从XXX.properties文件中加载数据到Properties类对象,并进行读取和修改。
XXX.properties文件通常为配置文件,后续IO流则会提及
比如在保存数据库密码时,如果将账号密码放入java文件中,那么每次修改都需重新编译,而保存在Properties文件中,修改相对简单
增/改:put()添加一对键值,如键已存在,则修改对应的值
删:remove()两种重载,1,remove(Object key)删除指定键所对应的所有对象;2 remove(Object key,Object value)删除指定键值对
查:两种方法:1、get()查找该键所对应的所有值,查找到则返回Object类,未找到返回null;2、pro.getProperty()两种重载:a、pro.getProperty(String key)同get方法,但只能查找String,返回值也为String,b、pro.getProperty(String key,String xxx)查找失败则返回String xxx,查找成功则等同上文
Properties pro = new Properties();
pro.put(111,"第一");
pro.put(222,"第二");
pro.put(333,"第三");
pro.remove(111);
pro.put(222,"新元素");
System.out.println(pro);
System.out.println(pro.get(222));
System.out.println(pro.getProperty("222","该元素不存在"));
执行结果:
{333=第三, 222=新元素}
新元素
该元素不存在
TreeMap
其与TreeSet极其相似,在此仅举例说明
//使用无参构造器
TreeMap ts = new TreeMap();
ts.put("jjjj", "aaaa");
ts.put("aaaa", "aaaa");
ts.put("cccc", "aaaa");
ts.put("kkkk", "aaaa");
ts.put("kkkk", "aaaa");//重复元素不添加,仅替换value
System.out.println(ts);
输出:{aaaa=aaaa, cccc=aaaa, jjjj=aaaa, kkkk=aaaa}
//自定义比较器-----------------------------
TreeMap ts = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length()-((String)o2).length();
}
});
ts.put("j", "aaaa");
ts.put("aa", "aaaa");
ts.put("ccc", "aaaa");
ts.put("kkkk", "aaaa");
ts.put("pppp", "aaaa");//长度重复不添加,仅替换value
System.out.println(ts);
输出:{j=aaaa, aa=aaaa, ccc=aaaa, kkkk=aaaa}
当添加第一个元素时,也会调用比较器,但此时数组内只有一个元素,所以系统会将两值的key相比较,此举只起检查是否为空值的作用,对结果无影响。
总结:如何选择类
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类的特性进行选择:
先判断存储的类型(一组对象[单列]还是一组键值对[双列])
一、一组对象[单列]:Collectionj接口
- 允许重复:List
- 增删少:LinkedList(底层维护了一个双向链表)
- 增删多:ArrayList(底层维护了Object类型的可变数组)
- 不允许重复:Set
- 无序:HashSet(底层为HashMap,维护了一个哈希表,即数组+链表+红黑树)
- 排序:TreeSet
- 插入取出顺序一致:LinkedHashSet(维护数组+链表,底层为LinkedHashMap)
二、一组键值对[双列]:Map
- 无序:HashMap(jdk8以上为数组+链表+红黑树)
- 排序:TreeMap
- 插入取出顺序一致:LinkedHashMap(维护数组+链表)
- 便于读取修改:Properties