java.util.Map
双列集合接口 键值对
K 和 V 都是泛型,根据传递进来的值的类型所决定
特点:
常用实现类:
null的
值和null
键。 ( HashMap
类大致相当于Hashtable
,除了它是不同步的,并允许null)。这个类不能保证地图的顺序; 特别是,它不能保证在一段时间内保持不变。Map接口定义了很多方法。
public abstract V put(K key, V value)
把键值存入到map集合中,返回该键的值,第一次插入该key,返回nullpublic abstract V remove(Object key)
把指定的键所对应的键值对删除,返回被删除元素的值,不存在返回nullpublic abstract V get(Object key)
获取指定的键的值,不存在返回nullpublic abstract boolean containsKey(Object key)
判断集合中是否包含指定的键Map集合的遍历:
public abstract Set keySet()
获取Map集合中的所有键,存储到Set集合中public abstarct Set> entrySet()
获取Map集合中所有的键值对对象的集合(Set集合)特点:
Map
map = new HashMap<>();
工作中不用hashmap。
该创建对象默认等于new HashMap<>(16,0.75);
16为数组默认长度,0.75为默认负载因子(当元素填满数组的75%的时候,才扩容数组长度)
public class Test{
public static void main(String[] args){
showOne();
showTwo();
}
public static void showOne(){
Map<String,String> map = new HashMap<>();
String s1 = map.put("a","1");
System.out.println(s1); // 输出 null 因为存放的key为a,在map中还没有a这个key,所以返回对应值就为null
String s2 = map.put("a","2");
System.out.println(s2); // 输出字符串 1 因为存放key为a,由于map中已经有a这个key了,key唯一,所以将旧value替换,将旧value值返回
}
public static void showTwo(){
Map<String,Integer> map = new HashMap<>();
map.put("a",1);
map.put("b",2);
map.put("c",3);
Integer in1 = map.remove("b");
System.out.println(in1); // 输出 2
int in1 = map.remove("b");
System.out.println(in1); // 抛出异常,因为在map中已经没有b这个key,所以会返回null,由于int类型不能接收null值,所以就会报错。建议使用对象类型接收返回值。 这里可以使用int是因为自动拆箱。
}
public static void showThree(){
Map<String,Integer> map = new HashMap<>();
map.put("a",1);
map.put("b",2);
map.put("c",3);
}
}
public calss Test{
public static void main(String[] args){
showOne();
}
public static void showOne(){
Map<String,Integer> map = new HashMap<>();
map.put("a",1);
map.put("b",2);
map.put("c",3);
Set<String> set = map.keySet();
// 迭代器遍历
Iterator<String> it = set.iterator();
while(it.hasNext()){
Integer value = map.get(it.next());
System.out.println(it.next()+" = "+value);
}
// 增强for循环遍历
for(String s: map.keySet()){
Integer value = map.get(s);
System.out.println(s+" = "+value);
}
}
}
Map
中存放的是两种对象,一种为key,一种为value,它们在Map
中是一一对应关系,这一对对象又称做Map
中的一个Entry(项)
。Entry
将键值对的对应关系封装成了一个对象。即键值对对象,这样我们在遍历Map
集合时,就可以从每一个键值对对象中获取对应的键与对应的值。
既然Entry表示了一对键值对,那么也同样提供了获取对应键值的方法:
public K getKey()
获取Entry对象中的键public V getValue()
获取Entry对象中的值在Map集合中提供了获取所有Entry对象的方法:
public Set
获取到Map中所有键值对对象的集合(Set集合)
public class Test{
public static void main(String[] args){
}
public static void showForEntry(){
Map<String,Integer> map = new HashMap<>();
map.put("a",1);
map.put("b",2);
map.put("c",3);
Set< Map.Entry<String,Integer> > set = map.entrySet();
// 迭代器遍历
Iterator< Map.Entry<String,Integer> > it = set.iterator();
while(it.hasNext()){
Map.Entry<String,Integer> entry = it.next();
String s = entry.getKey();
Integer i = entry.getValue();
System.out.println(s+" = "+v); // 输出每一个键值对
}
// for增强循环
for(Map.Entry<String,Integer> entry : set){
String s = entry.getKey();
Integer i = entry.getValue();
System.out.println(s+" = "+v); // 输出每一个键值对
}
}
}
练习:每位学生(姓名,年龄)都有自己的家庭住址。将学生对象和家庭住址存储到map集合中。学生作为key,家庭作为value。
注意,学生的姓名和年龄都相同,则为同一名学生。
Student.java
public class Student{
private String name;
private int age;
public Person() {
}
public Person(String name, String age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
// 因为不能存入同一名学生,所以必须重写hashCode和equals方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name) &&
Objects.equals(age, person.age);
}
}
Test.java
public class Test{
public static void main(String[] args){
show();
}
public static void show(){
Map<Student,String> map = new HashMap<>();
map.put(new Student("a",18), "成都市武侯区");
map.put(new Student("b",38), "成都市青羊区");
map.put(new Student("c",28), "成都市武侯区");
map.put(new Student("c",28), "成都市武侯区");
Set< Map.Entry<Student,String> > set = map.entrySet();
for(Map.Entry<Student,String> entry : set){
Student stu = entry.getKey();
String str = entry.getValue();
System.out.println(stu+" --- "+str);
}
}
}
LinkedHashMap类继承了HashMap类,该类可以保证元素的存取的顺序。底层原理是,Hash表+链表(记录元素的顺序)。
特点:
public class Test{
public static void main(String[] args){
test();
}
public static void test(){
HashMap<String,String> hashMap = new HashMap<>();
hashMap.put("a","1");
hashMap.put("c","3");
hashMap.put("b","2");
System.out.println("hashMap:"+hashMap); // 输出结果是没有任何顺序的
LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("a","1");
linkedHashMap.put("c","3");
linkedHashMap.put("b","2");
System.out.println("linkedHashMap:"+linkedHashMap); // 输出结果与存入的顺序一致
}
}
线程安全
java.util.Hashtable
最早的双列集合。它与新集合实现不同, Hashtable
是线程安全的集合,同步的(单线程)。键和值不能为null。在JDK1.2之后, 才改造为实现Map接口。
Hashtable存入null值,则会报异常:NullPointerException
public static void main(String[] args){
HashMap<String,String> hashMap = new HashMap<>();
hashMap.put(null,"a");
hashMap.put("c","d");
hashMap.put(null,null);
System.out.println(hashMap); // 输出 {null=null,c=d}
Hashtable<String,String> hashTable = new Hashtable<>();
hashTable.put(null,"a");
System.out.println(hashTable); // 报异常 NullPointerException 空指针异常
//hashTable.put("b",null);
//System.out.println(hashTable); // 报异常 NullPointerException 空指针异常
}
一个红黑树基于NavigableMap
实现。该类不同步,线程不安全。
LinkedHashMap是按照插入顺序排序,而TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序。在实现原理上LinkedHashMap是双向链表,TreeMap是红黑树。TreeMap还有个好兄弟叫TreeSet,实现原理是一样的。
负载因子
当我们将负载因子不定为0.75的时候(两种情况):
1、 假如负载因子定为1(最大值),那么只有当元素填满组长度的时候才会选择去扩容,虽然负载因子定为1可以最大程度的提高空间的利用率,但是会增加hash碰撞,以此可能会增加链表长度,因此查询效率会变得低下(因为链表查询比较慢)。hash表默认数组长度为16,好的情况下就是16个空间刚好一个坑一个,但是大多情况下是没有这么好的情况。
结论:所以当加载因子比较大的时候:节省空间资源,耗费时间资源
2、加入负载因子定为0.5(一个比较小的值),也就是说,直到到达数组空间的一半的时候就会去扩容。虽然说负载因子比较小可以最大可能的降低hash冲突,链表的长度也会越少,但是空间浪费会比较大。
结论:所以当加载因子比较小的时候:节省时间资源,耗费空间资源
但是我们设计程序的时候肯定是会在空间以及时间上做平衡,那么我们能就需要在时间复杂度和空间复杂度上做折中,选择最合适的负载因子以保证最优化。所以就选择了0.75这个值,Jdk那帮工程师一定是做了大量的测试,得出的这个值吧~
hash表的数组长度总在2的次方
1:
// WeakHashMap.java 源码:
/**
* Returns index for hash code h.
*/
private static int indexFor(int h, int length) {
return h & (length-1);
}
扩容也是以2的次方进行扩容,是因为2的次方的数的二进制是10…0,在二的次方数进行减1操作之后,二进制都是11…1,那么和hashcode进行与操作时,数组中的每一个空间都可能被使用到。
如果不是2的次方,比如数组长度为17,那么17的二进制是10001,在indexFor方法中,进行减1操作为16,16的二进制是10000,随着进行与操作,很明显,地址二进制数末尾为1的空间,不会得到使用,比如地址为10001,10011,11011这些地址空间永远不会得到使用。因此就会造成大量的空间浪费。
所以必须得是2的次方,可以合理使用数组空间。
2:
扩容临界值 = 负载因子 * 数组长度
负载因子是0.75即3/4,又因为数组长度为2的次方,那么相乘得到的扩容临界值必定是整数,这样更加方便获得一个方便操作的扩容临界值。
当链表长度>=8时构建成红黑树
利用泊松分布计算出当链表长度大于等于8时,几率很小很小
当put进来一个元素,通过hash算法,然后最后定位到同一个桶(链表)的概率会随着链表的长度的增加而减少,当这个链表长度为8的时候,这个概率几乎接近于0,所以我们才会将链表转红黑树的临界值定为8。
tips:了解红黑树,请移步至Java数据结构与算法:红黑树 AVL树.md
首先需要了解什么是红黑树,什么是AVL树。请移步至Java数据结构与算法:红黑树 AVL树.md
红黑树和AVL树增删改查的时间复杂度平均和最坏情况都是在O(lgN),包括但不超过。
红黑树性质:
特点:最长路径不会超过最短路径的2倍。
AVL性质:
在jdk8中hashmap的hash表桶中的链表长度大于8时,会将链表转为红黑树。虽然红黑树与AVL树的时间复杂度都为O(lgN),但是在调整树上面花费的时间相差很大。因为AVL树是平衡二叉树,要求严苛,任何节点的两个子树的高度最大差别为1,因此每次插入一个数或者删除一个数,最坏情况下,会使得AVL树进行很多次调整,为了保证符合AVL树的规则,调整时间花费较多。而红黑树,在时间复杂度上与AVL树相持平,但是在调整树上没有AVL树严苛,它允许局部很少的不完全平衡,但最长路径不会超过最短路径的2倍,这样以来,最多只需要旋转3次就可以使其达到平衡,调整时间花费较少。
最重要的一点,在JUC中有一个CurrentHashMap
类,该类为线程同步的hashmap类,当高并发时,需要在意的是时间,由于AVL树在调整树上花费的时间相对较多,因此在调整树的过程中,其他线程需要等待的时间就会增长,这样导致效率降低,所以会选择红黑树。
总结:在增加、删除的时间复杂度相同的情况下,调整时间相对花费较少的是红黑树,因此选择红黑树。
因为经过泊松定律知道,一个在负载因子为0.75时,出现的hash冲突,在一个桶中的链表长度大于8的几率是很少很少几乎为0,如果一来就使用红黑树,由于增删频繁,从而会调整树的结构,反而增加了负担,浪费时间,而直接使用链表增删反而比红黑树快很多,因此为了增加效率,而只是在长度大于8时使用红黑树。
public class Test{
public static void mian(String[] args){
forTest01();
}
public static void forTest01(){
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
char[] charArray = str.toCharArray();
HashMap<Character,Integer> hashMap = new HashMap<>();
for(int i = 0; i < charArray.length(); i++){
char c = charArray.get(i);
if( hashMap.containsKey(c) ){
Integer in = hashMap.get(c);
++in;
hashMap.put(c,in);
}else{
hashMap.put(c,1);
}
}
Set<Map.Entry<Character,Integer>> set = hashMap.entrySet();
for(Map.Entry<Character,Integer> entry : set){
System.out.println(entry.getKey()+"---"+entry.getValue());
}
}
public static void forTest02(){
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
HashMap<Character,Integer> hashMap = new HashMap<>();
for(char c : str.toCharArray()){
if( hashMap.containsKey(c) ){
Integer in = hashMap.get(c);
++in;
hashMap.put(c,in);
}else{
hashMap.put(c,1);
}
}
for(Character key : hashMap.keySet()){
Integer value = hashMap.get(key);
System.out.println(key+"---"+value);
}
}
}
JDK9的新特性:
List接口,Set接口,Map接口:里面增加了一个静态方法of,可以给集合一次性添加多个元素
当集合中存储的元素的个数已经确定了,要确定元素已经不能再改变的情况下使用。
注意:
UnsupportedOperationException
异常IllegalArgumentException
异常List<String> list = List.of("1","2","3");
Set<String> set = Set.of("a","b","c");
Map<String,Integer> map = Map.of("a",1,"b",2,"c",3);
package com.LFJava.doudizhu;
import java.util.*;
/**
* Created with IntelliJ IDEA
* User: heroC
* Date: 2020/2/24
* Time: 18:00
* Description:
* Version: V1.0
*/
public class Doudizhu {
public static void main(String[] args) {
// 准备牌的花色以及数字
List<String> listType = new ArrayList<>();
List<String> listNmuber = new ArrayList<>();
Collections.addAll(listType,"♣","♦","♥","♠");
Collections.addAll(listNmuber,"2","A","K","Q","J","10","9","8","7","6","5","4","3");
// 将花色与数字拼接在一起,存入到map双列集合中
Map<Integer,String> poker = new HashMap<>();
int index = 0;
poker.put(index,"大王");
++index;
poker.put(index,"小王");
++index;
for (String number : listNmuber) {
for(String type : listType){
poker.put(index, type + number);
++index;
}
}
//System.out.println(poker);
// 将map中的key值取出来,存入到list集合中,并将key值的顺序打乱
List<Integer> numbers = new ArrayList<>();
Set<Integer> set = poker.keySet();
for (Integer nums: set) {
numbers.add(nums);
}
Collections.shuffle(numbers);
//System.out.println(numbers);
// 将打乱的key值,存放到每个玩家的list集合中
List<Integer> diPai = new ArrayList<>();
List<Integer> player01 = new ArrayList<>();
List<Integer> player02 = new ArrayList<>();
List<Integer> player03 = new ArrayList<>();
for (int i = 0; i < numbers.size() ; i++) {
if( i >= 51){ // 一定要先将底牌存进去
diPai.add(numbers.get(i));
}else if( i % 3 == 0){
player01.add(numbers.get(i));
}else if( i % 3 == 1){
player02.add(numbers.get(i));
}else if( i % 3 == 2){
player03.add(numbers.get(i));
}
}
/*System.out.println(diPai);
System.out.println(player01);
System.out.println(player02);
System.out.println(player03);*/
// 发牌
show("heroC",poker,player01);
show("yikeX",poker,player02);
show("Vincent",poker,player03);
show("底牌",poker,diPai);
}
// 该方法,将每个玩家的存key值的list集合,先将key值排序,再将每个key值遍历出来,找到对应的map里的value值
public static void show(String playerName, Map<Integer,String> poker, List<Integer> playerList){
System.out.print(playerName+":");
Collections.sort(playerList);
for(Integer i : playerList){
String s = poker.get(i);
System.out.print(" "+s+" ");
}
System.out.println();
}
}
输出结果:
heroC: 大王 小王 ♦A ♠A ♣J ♥10 ♠10 ♦9 ♥9 ♣8 ♦7 ♣6 ♠6 ♣5 ♠5 ♣3 ♦3
yikeX: ♣2 ♥2 ♦K ♣Q ♦Q ♥Q ♠Q ♦J ♥J ♦10 ♣9 ♠9 ♦8 ♦5 ♠4 ♥3 ♠3
Vincent: ♦2 ♠2 ♣A ♥A ♥K ♠K ♠J ♣10 ♥8 ♣7 ♥7 ♠7 ♦6 ♥6 ♥5 ♦4 ♥4
底牌: ♣K ♠8 ♣4