Java是如何存储元素的(3)—Map集合存储数据原理(为什么HashMap集合的key部分的特点是无序,不可重复)

(一)Map集合结构图

(二)Map集合中常用的方法

 (1)往Map集合中添加元素

  • put(K key, V value)
  • 将指定的值与该映射中的指定键相关联(可选操作)。

这个和Collection不一样,Collection用的是add();

(2)清空

  • void

 

  • clear()
  • 从该地图中删除所有的映射(可选操作)。

 Collection中,也有这个方法。

(3)判断包含key和value

  • boolean
  • containsKey(Object key)
  • 如果此映射包含指定键的映射,则返回 true 。
  • boolean
  • containsValue(Object value)
  • 如果此地图将一个或多个键映射到指定的值,则返回 true 。

在Collection中是通过 Cotains,这和底层原理有关系了,因为Map是key-value存储的,Collection只是存储一个value。

(4)通过Key获取value值

  • get(Object key)
  • 返回到指定键所映射的值,或 null如果此映射包含该键的映射。

通过key值获取value值,这个和Colleciton中的List很像,然后Set中是没有下标的,所以Set没有。

(5)判断集合是否包含元素

  • boolean
  • isEmpty()
  • 如果此地图不包含键值映射,则返回 true 。

这个和Collection的一样。

(6) 获得集合中的元素数量

 
  • int
  • size()
  • 返回此地图中键值映射的数量。

这个和Colleciton中的一样。

(7) 删除集合中的某个元素

  • remove(Object key)
  • 如果存在(从可选的操作),从该地图中删除一个键的映射。

这个和Colleciton中的一模一样。

(8)获取所有的value

 Map<Integer,String> m1 = new HashMap<>();
        m1.put(1,"赵晓东");
        m1.put(2,"赵静瑶");
        m1.put(3,"赵景康");
        Collection<String> c1 = m1.values();
        for(String s : c1){
            System.out.println(s);
        }
赵晓东
赵静瑶
赵景康

(9) 获取Map集合中的所有Set集合,这个也很重要,可以循环Map

  • keySet()
  • 返回此地图中包含的键的Set视图。

 (10)将Map集合转换为set集合。这个很重要,可以循环Map

  • entrySet()
  • 返回此地图中包含的映射的Set视图。

(三)Map集合循环

Map集合的循环一共有两种方式

(1) 获取所有的key,通过遍历Key,来遍历value。这时候就需要了KeySet()方法进行转换了。

        Map<Integer,String> m1 = new HashMap<>();
        m1.put(1,"老师");
        m1.put(2,"校长");
        m1.put(3,"学生");
        /*通过将Map方法获取Set*/
        Set<Integer> key =m1.keySet();
        /*转换成Set方法后,通过获取iterrator进行遍历*/
        Iterator<Integer> I1=key.iterator();
        while (I1.hasNext()){
           Integer NumberKey = I1.next();
           String s1=m1.get(NumberKey);
            System.out.println(s1);
        }
老师
校长
学生

同时也可以通过增强for循环来获取Key

 for(Integer key1 : key){
            System.out.println(m1.get(key1));
        }

(2)把Map集合直接全部转化成Set集合。

这个时候需要调用的是Map的entrySet()方法了。

        Map<Integer,String> m1 = new HashMap<>();
        m1.put(1,"老师");
        m1.put(2,"校长");
        m1.put(3,"学生");
        /*将Map对象转换为Set*/
        Set<Map.Entry<Integer,String>> set = m1.entrySet();
        /*转换为Set之后,再调用Iterator*/
        Iterator<Map.Entry<Integer,String>> mset=set.iterator();
        /*接下来就是遍历*/
        while (mset.hasNext()){
            Map.Entry<Integer,String> m2=mset.next();
            Integer I1 =m2.getKey();
            String S1 = m2.getValue();
            System.out.println(I1+"--->"+S1);
        }
1--->老师
2--->校长
3--->学生

我们来看一下转换过程

其实这个时候,Map.Entry<Integer,String>为一个结点。 这个结点就是Set的形式,然后再通过set的getkey和getvalue获取元素即可。其实Map.Entry<k,v>为节点的类型。

当然这个也能使用增强for循环。

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

(四)HashMap(哈希表)

  (1)HashMap集合底层是哈希表/散列表的数据结构

(2)哈希表是一个怎样的数据结构?

哈希表是一个数组和单向链表的结合体。数组:在查询方面效率很高,在随机增删方面效率很低。单向链表:在随机增删方面效率很高,在查询方面效率很低。哈希表将以上的两种数据结构融合在一起,充分发挥他们的各自优点

(3)HashMap集合底层源代码(哈希表在代码的原理实现)

	Public class HashMap{
		//hashMap底层实际上就是一个数组(一维数组)
		Node<k,v> table;
		//静态内部类
		static class Node<K,V> implements Map.Entry<K,V>{
		finalinthash;	//哈希值(哈希值是key()和hashCode()方法执行结果,hash值通过哈希函数/算法)
		finalKkey;	存储到Map集合中的那个Key
		Vvalue;	存储到map集合中的那个value
		Node<K,V> next;	下一个节点的内存地址
		
		Node(inthash,Kkey,Vvalue,Node<K,V>next){
		this.hash=hash;
		this.key=key;
		this.value=value;
		this.next=next;
		}
		
	}

从以上的源代码我们可以看出,HashMap里面有Node数组,数组里面有hash,key,value,next。其中hash是哈希值(哈希值是key的hashCode()方法的执行结果,hash值通过哈希函数/算法。可以转换存储成数组的下标),key,value就是存储的键值对,next就是下一个节点的内存地址。

综上所述:哈希表/散列表,一维数组,这个数组中每一个元素是一个单向链表(数组和链表的结合体)。

很明显的看出来,里面是结点的方式。所以,它到底是怎么样子的呢?

(4)map.put(k,v)和v=map.get(k)的原理

这个是特别重要的,也就是说,存是如何存的,取是如何取的。

(I)先将k,v封装到Node对象当中

(II)底层会调用k的hashCode()方法得到hash值,然后通过哈希函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上,如果说下标对应的位置上有链表,此时就会拿着k和链表上每一个节点的k进行equals,如果所有的equals方法都返回false,那么这个新节点将会被添加到链表的末尾。如果其中有一个equals返回了true,那么会将这个节点的value将会被覆盖。(hashCode是Object自带的,如果不重写,则返回地址。)其实这就是数据结构里面学过的。

v=map.get(k)实现原理:

(III)先调用k的hashCode()方法得出哈希值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个位置上,如果这个位置上什么也没有,返回Null,如果这个位置上有单向链表,那么会拿着参数k和单向链表上的每个结点中的k进行equals,如果所有equals方法返回false,那么get方法返回null,只有其中有一个结点k和参数k的equals返回true,那么此时这个节点的value就是我们要找的value。get方法最终返回这个要找的value。

(a)为什么哈希表的随机增删,以及查询效率都很高?

因为增删是在链表上完成,查询也不需要扫描,只需要部分扫描。

所以说HashMap上面的k会先后调用两个方法,一个是hashCode()另一个是equals,所以这两个都需要重写。equals默认是比较内存地址,但是我们要比较的是里面的内容。

这里可以联想为什么HashMap集合的key部分的特点是无序,不可重复?

无序:因为调用了hashCode()方法,通过哈希算法得出来的值不一定是连续的,可能是随便的到哪一个单向链表上的,学过数据结构的人都知道,计算位置的方法会有很多,其中有一个%取余数。所以说,不可能是连续的,那只能是无序的。

不可重复:不可重复是因为,k值调用了equals方法。这个方法会对内容进行比较,如果内容相同的话,会覆盖掉value,所以说HashMap存储的数据时不可重复的。

放在HashMap集合key部分的元素其实就是放在了HashSet集合中了,所以HashSet集合中的元素也要需要重写HashCode()和equals。

所以,同一个单向链表上的所有结点的Hash是相同的。同一个链表上一个key,equals一定是false。无序不可重复。

(b)如果HashCode()重写的时候,写成一个固定值,那么就会成为单向链表。就会发挥不出功能。这种情况称为散列分布不均匀。

(c)如何HashCode()的返回值都不一样,行吗?不行那么就称为一维数组了。就没有链表的概念了,也是散列分布不均匀。

所以说HashCode()重写需要有一定的原理。

(5)同时重写HashCode()和equals()

我们上面说了,为什么要重写HashCode()和equals(),那么equals方法有可能调用,也有可能不调用。

拿put(k,v)举例,什么时候equals不会调用?

哈希值经过哈希算法转换为数组下标,数组下标位置上如果是null,equals不需要执行。

拿get(k)举例,什么时候equals不会调用?

K.hashCode()方法返回哈希值,哈希值经过哈希算法转换数组下标。

数组下标位置上如果是Null,equals不需要执行。

我们重写了equals,但是没有重写hashCode()

        Student s1 = new Student("zhangsan");
        Student s2 = new Student("zhangsan");
        /*查看equals和hashCode*/
        /*没有重写equals方法之前*/
//        System.out.println(s1.equals(s2));//false
        /*重写了equals方法之后*/
        System.out.println(s1.equals(s2));//true

        System.out.println(s1.hashCode());//-1432604525
        System.out.println(s2.hashCode());//-795136960

        Set<Student> students = new HashSet<>();
        students.add(s1);
        students.add(s2);
        System.out.println(students.size());
true
460141958
1163157884
2

按理说student.size()应该是1,但是结果是2,现在我们重写hashCode方法,再进行测试。

true
-1432604525
-1432604525
1

student.size()的结果就变成了1.

所以说最终结论:放在HashMap集合中的key部分的,以及放在HashSet集合中的元素,需要同时重写HashCode方法和equals方法。

但是在JDK8之后,如果哈希表单向链表中元素超过8个,单向链表这种结构会变成红黑树数据结构,当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表数据结构。

(五)HashMap和HashTable的区别

(1)HashMap中可以存放Null,而HashTable中不可以。

HashMap存放Null值

    public static void main(String[] args) {
        Map m1 = new HashMap();
        m1.put(null,null);
        System.out.println(m1.size());
    }
1

HashTable存放 

        Map m2 = new Hashtable();
        m2.put(null,null);
Exception in thread "main" java.lang.NullPointerException
	at java.util.Hashtable.put(Hashtable.java:459)
	at com.javase.Map.MapTable.main(MapTable.java:13)

(六)Properties

Properties是一个Map集合,继承了Hashtable,properties的key和value都是String类型。Properties被称为属性类对象。并且是线程安全的。

         Properties pro = new Properties();
        /*需要掌握Properties的两个方法,一个存,一个取*/
        pro.setProperty("url","zcdee");
        pro.setProperty("gdsa","dsarf");
        pro.setProperty("username","root");
        /*通过key值进行取*/
        String s1 = pro.getProperty("url");
        String s2 = pro.getProperty("username");

        System.out.println(s1);
        System.out.println(s2);
zcdee
root

(七)TreeSet

(1)TreeSet集合底层实际上是一个TreeMap

(2)TreeMap集合底层是一个二叉树

(3)放到TreeSet集合中的元素,等同于放到了TreeMap集合Key部分了。

(4)TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。称为可排序集合。

(5)那么什么时候可能用到TreeSet呢?

编写程序从数据库当中取出数据,在页面展示用户信息的时候按照生日升序或者降序。这个时候可以使用TreeSet集合,因为TreeSet集合放进去,拿出来就是有序的。

        TreeSet<Integer> t1 = new TreeSet<>();
        t1.add(10000);
        t1.add(100);
        t1.add(1000);
        for(Integer i1 : t1){
            System.out.println(i1);
        }

运行结果为 

100
1000
10000

 (6)对自定义的类型来说,TreeSet可以排序吗?

不可以,以下程序中对于Person类型来说,无法排序,

为什么不可以排序呢,因为美音指定Customer对象之家你的比较规则。谁大谁小并没有说明。当然出现这个异常的原因是因为没有实现Comparable接口。

(7)那么怎么进行排序呢?

(a)让Customer实现这个Comparable接口

(b)重写方法

之所以String,Integer能进行排序,是因为他们实现了Comparable接口和方法。 

 所以说CompareTo方法的返回值很重要:

返回0表示相同,value会覆盖,返回>0,会继续在右子树上找。返回<0,会继续在左子树上找。

(八)自平衡二叉树

(1)遵循左小右大原则存放。

(2)遍历二叉树的时候三种方式:

前序遍历:根左右。 中序遍历:左根右 。 后序遍历:左右根。

(3)TreeSet集合/TreeMap集合采用的是:中序遍历方式Iterator迭代器采用的是中序遍历方式,左根右。

(九)TreeSet的第二种比较方式

public static void main(String[] args) {
        /*创建TreeSet集合的时候,需要使用这个比较器*/
        TreeSet<WuGui> Wuguis = new TreeSet<>(new WuGuiComparator());
        WuGui w1 = new WuGui(800);
        WuGui w2 =new WuGui(521);
        WuGui w3 = new WuGui(888);
        Wuguis.add(w1);
        Wuguis.add(w2);
        Wuguis.add(w3);

        for(WuGui s : Wuguis){
            System.out.println(s);
        }

    }
}
class WuGui{
    private  int age;

    public WuGui(int age) {
        this.age = age;
    }

    public WuGui() {
    }

    @Override
    public String toString() {
        return "WuGui{" +
                "age=" + age +
                '}';
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
class WuGuiComparator implements Comparator<WuGui>{
    @Override
    public int compare(WuGui o1, WuGui o2) {
        return o1.getAge() - o2.getAge();
    }
WuGui{age=521}
WuGui{age=800}
WuGui{age=888}

最终结论:放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:

第一种:放在集合中的元素实现java.lang.Comparable接口。

第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。

Comparable和Comparator怎么选择呢?

当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。

如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

(九)Collections工具类

(1)Collections实现对List进行排序

编程线程安全的和排序

        /*ArrayList集合不是线程安全的*/
        List<String > list1 = new ArrayList<>();

        /*变成线程安全的*/
        Collections.synchronizedList(list1);

        /*排序*/
        list1.add("abf");
        list1.add("abx");
        list1.add("abc");
        list1.add("abe");

        Collections.sort(list1);
        for (String l1 : list1){
            System.out.println(l1);
        }
abc
abe
abf
abx

当我们往list里面传入WuGui,并且没有实现comparable方法的时候

就会报错,所以使用Collections的时候必须要对Wugui2进行实现Comparable接口

class WuGui2 implements Comparable<WuGui2>{
    int age;

    public WuGui2(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(WuGui2 o) {
        return this.age -o.age;
    }

    @Override
    public String toString() {
        return "WuGui2{" +
                "age=" + age +
                '}';
    }

        List<WuGui2> WuGuis = new ArrayList<>();
        WuGuis.add(new WuGui2(1000));
        WuGuis.add(new WuGui2(2999));
        Collections.sort(WuGuis);
        for(WuGui2 wg : WuGuis){
            System.out.println(wg);
        }

对List集合中元素排序,需要保证List集合中的元素实现了Comparable接口。

(2)Collecitons实现对Set进行排序

Collections是不能直接对Set进行排序的,这时候需要将Set集合转换成List集合。

        Set<String > s1 =new HashSet<>();
        s1.add("abd");
        s1.add("abc");
        s1.add("abe");
        List<String> l1 = new ArrayList(s1);
        Collections.sort(l1);
        for(String l2 :l1){
            System.out.println(l2);
        }
abc
abd
abe

总结:现在终于对集合有了大致的了解,首先是集合是干什么的,既然集合是存储数据的,那么和数组有什么关系,有Map和Collection他们都有什么特点,他们都是怎么进行遍历的,在Map集合 中HashMap为什么要重写hashCode和equals方法,以及get和put方法存储数据的原理是什么等等,还有最大的感受是在学Java之前一定要把数据结构学好了。

猜你喜欢

转载自blog.csdn.net/MyxZxd/article/details/106217614