集合之双列集合

一.Map接口

Map接口表示的就是双列集合.双列集合的元素是按照键值对的方式来存储的.并且不能有重复的键,但是值可以重复.也就是说键值的对应关系必须是一对一,值键的对应关系可以是一对多.

Map接口中的常用方法

理论上Map跟List差不多,都可以存储任何类型的数据.但是为了后面操作集合方便,我们还是需要指定集合能存储的数据类型,这里包括键值型以及值类型.这里还需要使用到我们之前提到的泛型语法.这样规定了存储的数据类型之后,我们进行数据操作时就不会造成数据紊乱的情况.

这里我们通过一段代码看看Map接口下的常用方法:

public class MapDemo {

    public static void main(String[] args) {

        //父类引用指向子类对象
        Map<Integer,String> map = new HashMap<>();
        //向集合中添加元素
        map.put(6, "6");
        map.put(5, "5");
        //得到此集合中键对应的值
        System.out.println(map.get(6));
        //判断集合包不包含指定的键
        System.out.println(map.containsKey(6));
        //判断集合包不包含指定的值
        System.out.println(map.containsValue(5));
        //集合的长度
        map.size();
        //判断集合是不是空集
        System.out.println(map.isEmpty());
        //清空整个集合
        map.clear();

    }
}

二.Map的实现类

上个章节我们已经提到了Map的具体实现类

Map接口下主要有三个实现类:

1.HashMap.这个类的元素存储顺序是无序的.底层是数组,按照哈希算法进行判断是否是重复的以及排序.这个在这个章节后面我们再说.

2.Hashtable:这是一个线程安全的Map.

3.TreeMap:这个类的元素存储也是无序的,但是我们可以通过实现Comparable接口来实现元素的排列是有序的.底层是树结构.这个等我们了解了数据结构之后再介绍这种数据结构.

HashMap

HashMap集合是一个双列集合.并且**存储的元素不能重复.**那么为什么存储的元素不能重复呢?怎么判断元素是否重复?

判断元素是否重复

我们的add方法在底层进行操作的时候,就会判断集合中是否已经包含了此元素(只看键).在底层使用的是**hashCode()和equals()**这两个方法来判断内容是否重复.

在之前学习API的时候我们提到过Object类,那么其实这两个方法是Object类中的方法.值得注意的是,**hashCode()这个方法被native修饰.**代表就是此方法是本地方法,也就是调用操作系统的方法,java并没有实现.

在Object类中,HashCode的作用是获取对象在内存中的地址.equals的作用与"=="作用相同,也是比较对象的地址.

然而在实际除了我们的自建类,java中的所有类几乎都重写了这两种方法.重写过后的HashCode方法就不是获取对象的地址了,而是经过一系列简单计算计算出对象的**哈希值.这个哈希值是int类型.其次equals方法也被重写了,这个在之前的API章节中也有讲过.重写之后的equals方法作用也不是比较对象的地址是否相同了,而是比较对象的内容是否相同.很明显equals方法比HashCode方法效率要低一些.因此hash类型判断元素是否重复首先使用的是hashCode方法(因为效率高),但是有些时候尽管内容是不同的但是哈希值却一样,这个时候再调用equals方法(效率低但是准确).**这样做就会保证相同的元素不会加到集合中(之后的会把之前的覆盖).

当然hashMap是这样,我们上一节单列集合中的hashSet也是这样,毕竟是hashMap分割出去的,底层实现是完全一样的.

我们先看一下hashMap的常用方法

import java.util.HashMap;

public class HashDemo {

    public static void main(String[] args) {

        HashMap<Integer,String> hashMap = new HashMap<>();
        hashMap.put(1, "1");
        hashMap.put(2, "2");
        hashMap.put(1, "11");
        hashMap.put(4, "4");
        hashMap.put(5, "55");
        hashMap.put(3, "3");
        //hashMap可以存储为null的键
        hashMap.put(null, "0");
        //{null=0, 1=11, 2=2, 3=3, 4=4, 5=55}
        System.out.println(hashMap);

        //拿到键对应的值
        System.out.println(hashMap.get(1));
        //判断此集合包不包含对应的键
        System.out.println(hashMap.containsKey(6));
        //判断集合包不包含对应的值
        System.out.println(hashMap.containsValue("6"));
        //返回键那一列的Set集合
        System.out.println(hashMap.keySet());
        //返回值对应的那一列的Collection单列集合
        System.out.println(hashMap.values());
        //删除键对应的键值对,并返回键对应的值
        System.out.println(hashMap.remove(5));
        //更改键对应的值,返回更改前的值
        System.out.println(hashMap.replace(1, "111"));

    }
}

那么hashMap的底层结构到底是怎样的呢.

hashMap底层结构

在上面我们已经提到了hashMap存储元素的顺序是无序的,但是元素的排列是有序的,元素排列的顺序是按照计算出来的哈希值进行存储的,那么具体的底层结构是怎样的,我们具体来看看.

自java8之后,底层存储的数据结构有三种,分别是数组,链表,红黑树.

我们以hashSet为例介绍hashMap底层存储逻辑.

hashSet底层结构是哈希表也就是数组,存储元素的过程中会根据存储的元素计算出一个hash值,再进行运算计算出元素在数组的位置(一般是对哈希值进行取模).将元素封装到一个node对象中,将对象存储到对应的位置,如果之后有元素计算出来存储的位置相同的话,会加在这个元素之后,也就是说数组里面存储的是链表,当链表长度也超过一定数值,会转换为红黑树.

哈希数组底层数组初始长度为16,负载因子是0.75,当数组里面有个元素已经变为链表结构并且长度超过8时,会先扩容哈希数组,要么就是数组长度已经超过了0.75倍的长度,也会触发扩容,每次扩容后的长度是之前的2倍.直到到64,这个时候如果链表长度超过了8,那么链表就会转换为红黑树存储结构.

我们一起来看看put方法的源码

put 方法源码
            public V put(K key, V value) {
              return putVal(hash(key), key, value, false, true);
              //hash(key)这个方法就计算出来key的哈希值
              }
hash(key)方法源码
            static final int hash(Object key) {
            int h;
            //在这里就可以看出hashMap可以存储一个为null的键,返回key的hash值
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            }
putVal方法源码
        final V putVal ( int hash, K key, V value,boolean onlyIfAbsent,
        boolean evict){
            Node<K, V>[] tab;//这就是底层的hash数组
            Node<K, V> p;//一个node对象
            int n, i;//n就是hash数组长度,
            //在我们new出来hashMap对象时,并没有创建出node对象,在添加元素的时候才会创建数组
            if ((tab = table) == null || (n = tab.length) == 0)
                //resize()是hash表扩容方法
                n = (tab = resize()).length;
            //i = (n - 1) & hash就是取模运算,计算出值应该在数组的位置
            if ((p = tab[i = (n - 1) & hash]) == null)
                //如果数组没有node对象,new出来一个node对象
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K, V> e;
                K k;
                if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                //如果已经是红黑树,那么调用红黑树的添加元素方法
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                //要不然就还是链表结构
                else {
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {
                            //往链表添加元素
                            p.next = newNode(hash, key, value, null);
                            //如果链表长度>=8,会调用转换为红黑树的方法,但是哈希数组长度也得满足长度为64的限制条件
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                if (e != null) { // existing mapping for key
                    //如果添加相同元素,覆盖之前的值
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }

那我们现在来总结一下hashMap底层put方法的存储逻辑

哈希表的每个空间存储的都是一个node对象.底层是对node对象进行操作的,存储的过程与之前的hashSet也是一模一样.就是三种数据结构的综合利用.

这里需要提一下遍历的问题.因为哈希表底层存储的是node对象,因此遍历起来不是很方便,要是自己来的话还得解封装,非常麻烦.hashMap为了方便起见给我们提供了一个内部类Entry.我们可以使用Map接口里的方法**entrySet().**将hash表所有的node对象里的键值对内容封装在一个Set集合中.然后可以对这个Set集合进行遍历,遍历的方法在上节内容也提到了,推荐使用迭代器遍历.同时这个类也为我们提供了getKey()和getValue()等方法.

遍历

import java.util.HashMap;

public class BianLiDemo {

    public static void main(String[] args) {

        HashMap<Integer,String> hashMap = new HashMap<>();
        hashMap.put(1, "1");
        hashMap.put(2, "2");
        hashMap.put(3, "3");
        hashMap.put(4, "4");
        hashMap.put(6, "6");
        hashMap.put(5, "5");

        for (Integer i :hashMap.keySet()) {
            //输出键那一列
            System.out.println(i);
        }
        for (String s:hashMap.values()) {
            //输出值那一列
            System.out.println(s);
        }
    }
}
import java.util.HashMap;
import java.util.Set;

import static java.util.Map.*;

public class BianLiDemo1 {

    public static void main(String[] args) {

        HashMap<Integer,String> hashMap = new HashMap<>();
        hashMap.put(1, "1");
        hashMap.put(2, "2");
        hashMap.put(3, "3");
        hashMap.put(4, "4");
        hashMap.put(6, "6");
        hashMap.put(5, "5");

        //返回Entry的Set集合
        Set<Entry<Integer, String>> entry =  hashMap.entrySet();

        for (Entry<Integer, String> s:entry) {
            //需要什么就调用什么
            s.getKey();
            System.out.println(s);
        }
    }
}

HashTable

HashTable类与HashMap的主要区别是:

HashTable不可以存储一个为null的键,但是HashMap可以.

HashTable类存储的不再是node对象,而是Entry对象.

HashTable是线程安全的.

TreeMap

这个类在之后我们再详细介绍

三.Collections工具类

注意我们之前上节说到单列集合的时候提到了Collection接口.那个接口是单列集合的根接口,跟我们这个有很大区别.这个类是为集合提供工具,也可以说是集合工具类.

Collections类与Arrays类相似,Arrays是为数组提供的工具类.Collections包含对集合进行操作的多态算法.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CSDemo {

    public static void main(String[] args) {

        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(1);

        /*
            将这些元素全部加到集合里面
            这个方法里面有两个参数,Collection<? super T> c,T...elements
            第一个参数c是集合,使用到了泛型通配符,通配符下限
            第二个参数代表可变长度参数,是利用数组数组实现的,也就是elements是一个数组
            一个参数列表中最多只能有一个可变长度参数,并且必须在末尾位置
         */
        Collections.addAll(arrayList,5,6,1,3,4);
        //与Arrays类似,必须先排序才能使用二分查找
        Collections.sort(arrayList);
        System.out.println(Collections.binarySearch(arrayList, 4));//3

        //逆序排序
        Collections.reverse(arrayList);

        //交换两个索引元素的位置
        Collections.swap(arrayList, 0, 3);
        //打乱元素顺序
        Collections.shuffle(arrayList);
        //list里的最大值
        System.out.println(Collections.max(arrayList));
        //用指定元素将集合填满
        Collections.fill(arrayList, 9);
        //将集合中所有为5的元素替换为4
        Collections.replaceAll(arrayList, 5, 4);

        //返回一个为空的List集合,其实是一个内部类emptyList对象,这个集合不能操作使用,用于逻辑处理
        List list = Collections.emptyList();

    }
}
import java.util.ArrayList;
import java.util.Collections;

public class CSDemo1 {

    public static void main(String[] args) {

        ArrayList<Integer> arrayList = new ArrayList<>();
        Collections.addAll(arrayList, 1,2,3,5,4,6);

        ArrayList<Integer> arrayList1 = new ArrayList<>();
        Collections.addAll(arrayList1, 5,6,1,3,2);

        /*
            我们要想将集合1中的内容复制到集合2中,可以使用copy方法
            但是要注意使用条件:
            被复制的集合(也就是第一个参数)泛型类型是第二个集合的父类
            下面那个copy方法使用会报错,因为arrayList1长度比arrayList小,存不进去那么多数据
         */
        Collections.copy(arrayList, arrayList1);
        Collections.copy(arrayList1, arrayList);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值