zoukankan      html  css  js  c++  java
  • java集合-Collection、Map接口

    什么是Java集合?

    一般我们在碰到多个数据或者对象的问题时,需要一个容器来存储多个对象或者数据,为了解决这一问题首先想到的是数组,但是数组有一些缺点,比如初始化之后存储长度就确定了,一个数组只能存储一种类型的对象,数组提供的属性和方法比较少,数组存储的数据是有序的,可以重复的。当我们在解决问题之前并不确定需要存储对象的个数和类型时,数组并不能很好的解决这一问题。但是集合可以,集合可以用于存储数量不等的多个对象。

    集合的特点:

    1. 集合只能存储对象。
    2. 集合存储的都是对象的引用。
    3. 集合可以存放不同类型,不限数量的数据类型。

    Java集合框架

    从上面这个框架图可以看出来Java集合核心部分就是Collection接口和Map接口。

    1.Collection接口是高度抽象出来的集合,它包含了集合的基本操作和属性,Collection接口又可以分为List接口和Set接口,List接口和Set接口继承Collection接口。

    • List是一个有序的队列,每个元素都有自己的索引,第一个索引值为0,List的实现类有LinkedList、ArrayList、Vector和Stack。
    • Set是一个不允许有重复元素的集合,有点像数学上的集合,Set的实现类有HastSet和TreeSet。HashSet依赖于HashMap,它实际上是通过HashMap实现的;TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。

    2.Map接口是一个映射接口,即key-value键值对。

    3.Iterator迭代器接口主要用于遍历Collection集合中的元素。

    4.Arrays和Collections。它们是操作数组、集合的两个工具类。

    Collection接口

    Collection接口是高度抽象出来的集合,它包含了集合的基本操作和属性,比如一些添加元素、清空、获取元素个数、删除、获取hash值等。

    LIst接口

    • List接口是Collection接口的子接口之一,之前也说到数组处理数据存在一些局限性,所以通常用List替代数字。
    • List集合类中元素有序,可重复,集合中每个元素都有其对应的索引,可以根据索引实现对元素的访问。
    • List的常用实现类包括:ArrayList、LinkedList和Vector

    (1)ArrayList类是List接口的典型实现类、主要实现类,本质上ArrayList是一个动态数组,它允许任何符合规则的元素插入甚至包括null。ArrayList创建时,默认容量是0,如果要添加元素时,将容量增加到10,该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作,ArrayList可以进行随机访问,但是是非同步的

    以下情况适合使用ArrayList:

    1. 频繁访问列表中的某一个元素。
    2. 只需要在列表末尾进行添加和删除操作。

    ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。ArrayList 是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。

    添加元素:add()方法

    import java.util.ArrayList;
    public class RunoobTest { public static void main(String[] args) { ArrayList<String> sites = new ArrayList<String>(); sites.add("Google"); sites.add("Runoob"); sites.add("Taobao"); sites.add("Weibo"); System.out.println(sites); } }

    访问元素:get()方法

    import java.util.ArrayList;
    public class RunoobTest { public static void main(String[] args) { ArrayList<String> sites = new ArrayList<String>(); sites.add("Google"); sites.add("Runoob"); sites.add("Taobao"); sites.add("Weibo"); System.out.println(sites.get(1)); // 访问第二个元素 } }

    修改元素:set()方法

    import java.util.ArrayList;
    
    public class RunoobTest {
        public static void main(String[] args) {
            ArrayList<String> sites = new ArrayList<String>();
            sites.add("Google");
            sites.add("Runoob");
            sites.add("Taobao");
            sites.add("Weibo");
            sites.set(2, "Wiki"); // 第一个参数为索引位置,第二个为要修改的值
            System.out.println(sites);
        }
    }

    删除元素:remove()方法

    import java.util.ArrayList;
    
    public class RunoobTest {
        public static void main(String[] args) {
            ArrayList<String> sites = new ArrayList<String>();
            sites.add("Google");
            sites.add("Runoob");
            sites.add("Taobao");
            sites.add("Weibo");
            sites.remove(3); // 删除第四个元素
            System.out.println(sites);
        }
    }
    

      

    迭代数组列表:for-each

    import java.util.ArrayList;
    
    public class RunoobTest {
        public static void main(String[] args) {
            ArrayList<String> sites = new ArrayList<String>();
            sites.add("Google");
            sites.add("Runoob");
            sites.add("Taobao");
            sites.add("Weibo");
            for (String i : sites) {
                System.out.println(i);
            }
        }
    }
    

      

    排序:Collections 类也是一个非常有用的类,位于 java.util 包中,提供的 sort() 方法可以对字符或数字列表进行排序。

    import java.util.ArrayList;
    import java.util.Collections;  // 引入 Collections 类
    
    public class RunoobTest {
        public static void main(String[] args) {
            ArrayList<String> sites = new ArrayList<String>();
            sites.add("Taobao");
            sites.add("Wiki");
            sites.add("Runoob");
            sites.add("Weibo");
            sites.add("Google");
            Collections.sort(sites);  // 字母排序
            for (String i : sites) {
                System.out.println(i);
            }
        }
    }
    

      (2)LinkedList类与ArrayList不同,LinkedList是一个链表,所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。

    以下情况适合使用LinkedList:

    1. 需要通过循环迭代来访问列表中的某些元素。
    2. 需要频繁的在列表开头、中间、末尾等位置进行添加和删除元素的操作。
    • LinkedList 继承了 AbstractSequentialList 类。
    • LinkedList 实现了 Queue 接口,可作为队列使用。
    • LinkedList 实现了 List 接口,可进行列表的相关操作。
    • LinkedList 实现了 Deque 接口,可作为队列使用。
    • LinkedList 实现了 Cloneable 接口,可实现克隆。
    • LinkedList 实现了 java.io.Serializable 接口,即可支持序列化,能通过序列化去传输。

    添加元素:add()、addFirst()、addLast()

    import java.util.LinkedList;
    
    public class ListTest {
        public static void main(String[] args) {
            LinkedList<String> s = new LinkedList<>();
            s.add("admin");
            s.add("root");
            s.addFirst("first");
            s.addLast("last");
            System.out.println(s);
        }
    }
    //输出结果:[first, admin, root, last]

    (3)Vector与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。在各种list中最好吧ArrayList作为缺省选择,当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。

    (4)Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。

    Set接口

    Set接口是Collection接口的子接口,set接口没有提供额外的方法,Set集合不允许包含相同的元素,如果试着把两个相同元素加入到同一个Set集合中,则添加失败。需要注意的是虽然Set中元素没有顺序,但是元素在set中的位置是由该元素的HashCode决定的,其具体位置其实是固定的。Set判断两个元素是否相同不是用“==”,而是用equals()。Set接口有三个具体实现类:HashSet、LinkedHsahSet和TreeSet。

    (1)HashSet

    • 不能保证元素排列顺序
    • 线程不安全
    • 集合元素可以是null

    Map接口

    1.  Map和Collection接口并列存在的,,用于保存具有映射关系的数据:key-value,key和value可以是任何引用类型的数据。
    2. Map接口的常用实现类有:HashMap、TreeMap、LinkedHashMap和Properties,其中HashMap是Map接口使用频率最高的实现类。

    常用方法:

    put():

    //添加元素
    put(K key, V value);
    
    //将此映射中的指定值与指定键关联,如果映射以前包含该键的映射,则旧值将被指定的值替换。
    //如果键是第一次存储,就直接存储元素,返回null
    //如果键不是第一次存在,就用值把以前的值替换掉,返回以前的值 

    get():

    //根据键获取值
    V get(Object key)

    下面这块代码主要演示了get()和put()的使用:

    remove():使用remove(key)方法来删除key对应的键值对key-value

    import java.util.HashMap;
    
    public class RunoobTest {
        public static void main(String[] args) {
            // 创建 HashMap 对象 Sites
            HashMap<Integer, String> Sites = new HashMap<Integer, String>();
            // 添加键值对
            Sites.put(1, "Google");
            Sites.put(2, "Runoob");
            Sites.put(3, "Taobao");
            Sites.put(4, "Zhihu");
            Sites.remove(4);
            System.out.println(Sites);
        }
    }
    //输出结果:{1=Google, 2=Runoob, 3=Taobao}
    

    迭代HashMap

    使用for-each来迭代HashMap中的元素,如果你只想获取 key,可以使用 keySet() 方法,如果你只想获取 value,可以使用 values() 方法。

    import java.util.HashMap;
    
    public class RunoobTest {
        public static void main(String[] args) {
            // 创建 HashMap 对象 Sites
            HashMap<Integer, String> Sites = new HashMap<Integer, String>();
            // 添加键值对
            Sites.put(1, "Google");
            Sites.put(2, "Runoob");
            Sites.put(3, "Taobao");
            Sites.put(4, "Zhihu");
            // 输出 key 和 value
            for (Integer i : Sites.keySet()) {
                System.out.println("key: " + i + " value: " + Sites.get(i));
            }
        }
    }
    /*输出结果
    key: 1 value: Google
    key: 2 value: Runoob
    key: 3 value: Taobao
    key: 4 value: Zhihu
    */

    除了这些方法还有其他方法:

    实现类一:HashMap

     在JDK1.7以及以前的版本中,HashMap的存储结构是数组+链表

    JDK1.8版本HashMap的存储结构是数组+链表+红黑树

    当一个值中要存储到 HashMap 中的时候会根据 Key 的值来计算出他的 hash,通过 hash 值来确认存放到数组中的位置,如果发生 hash 冲突就以链表的形式存储,当数组上的某个索引位置上的元素以链表形式存在的数据个数 > 8且当前数组的长度 > 64时,HashMap 会把这个链表转换成红黑树来存储。

    源码解析:

    DEFAULT_INITIAL_CAPACITY属性:表示初始容量为16

     MAXIMUM_CAPACITY属性:HashMap支持的最大容量为30

     DEFAUILT_LOAD_FACTOR属性:默认的负载因子为0.75,和扩容相关,主要表示当前hashmap的填充度

    put():调用putVal()方法,放入键值对

    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    
        /**
         * Implements Map.put and related methods.
         *
         * @param hash hash for key
         * @param key the key
         * @param value the value to put
         * @param onlyIfAbsent if true, don't change existing value
         * @param evict if false, the table is in creation mode.
         * @return previous value, or null if none
         */
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断当前hash表是否为空,如若为空,则需要进行扩容
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
         //根据当前key的hash算出当前元素应该放到hash表中的下标,如果该位置为null,则放入
            if ((p = tab[i = (n - 1) & hash]) == null)
                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);
                            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;
        }
    put的流程:
    
    首先根据要放入的key计算hash,然后根据hash获取table中的放入位置,如果当前table为空,则进行初始化
    判断放入位置是否为空,为空则直接放入,否则判断是否为红黑树节点,不是则为链表,则遍历链表查找是否存在相同的key,没找到则放入链表尾部并判断是否需要转为红黑树(TREEIFY_THRESHOLD)
    判断当前节点数是否大于阈值,如果大于阈值则需要进行扩容
     
    
    resize():扩充当前容量
    
    HashMap 每次扩容都是建立一个新的 table 数组,长度和容量阈值都变为原来的两倍,然后把原数组元素重新映射到新数组上,具体步骤如下:
    
    1. 首先会判断 table 数组长度,如果大于 0 说明已被初始化过,那么按当前 table 数组长度的 2 倍进行扩容,阈值也变为原来的 2 倍
    
    2. 若 table 数组未被初始化过,且 threshold(阈值)大于 0 说明调用了 HashMap(initialCapacity, loadFactor) 构造方法,那么就把数组大小设为 threshold
    
    3. 若 table 数组未被初始化,且 threshold 为 0 说明调用 HashMap() 构造方法,那么就把数组大小设为 16,threshold 设为 16*0.75
    
    4. 接着需要判断如果不是第一次初始化,那么扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去,如果节点是红黑树类型的话则需要进行红黑树的拆分。
    
    final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
           //如果容量大于最大容量,那么就将阈值设置为2的31次方-1
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
           //容量小于最大容量,那么将容量扩大一倍
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
              //如果旧的容量扩大一倍小于2的30次并且旧的容量大于默认的初始化容量大小16,阈值也变为原来的2倍
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
         //如果新的阈值为0,则重新计算阈值=表容量*负载因子
            if (newThr == 0) {
                float ft = (float)newCap * loadFactor;
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                          (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
            table = newTab;
            if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }

    HashMap在JDK1.8较之前版本的变化:

    1.HashMap map = new HashMap();//默认情况下,先不创建创度为16的数组

    2.档首次调用map.put()时,创建长度为16的数组

    3.数组为Node类型,在JDK7中成为Entry类型

    4.形成链表结构时,新添加的kay-value从尾部插入

    5.当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。

    实现类二:LinkedHashMap

    1. LinkedHashMap是HashMap的子类
    2. 在HashMap的存储结构基础上,使用一对双向链表来记录添加元素的顺序
    3. 在accessOrder为true的情况下(默认为false,即默认不按访问顺序排序),put,get方法取完的节点放到链表尾部,按照Lru(最近最长时间未使用的方法进行排列,也便于删除最老的节点),保证遍历顺序和插入顺序一致(后插入的一定在后面遍历到,先插入的先遍历)

    实现类三:TreeMap

    1. TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于 有序状态。
    2. TreeSet底层使用 红黑树结构存储数据

    实现类四:HashTable

    HashTable是线程安全的

    与HashMap不同的是,HashTable不允许使用null作为key和value

    与HashMap一样的是,HashTable也不能保证key-value对的顺序

    HashTable判断两个key或value相等的标准和HashMap一样

     参考

     从JDK源码学习HashMap

    https://www.javazhiyin.com/64568.html

    https://www.liaoxuefeng.com/wiki/1252599548343744/1265118019954528

  • 相关阅读:
    注册和登录与数据库内的链接
    数据访问
    马厩分配问题
    Codeforces Round #365 (Div. 2) D.Mishka and Interesting sum
    最优比例生成树模板
    01分数规划模板
    hiho一下第109周《Tower Defense Game》
    begin.BZOJ 1383: 三取方格数
    最小生成树
    Codeforces Round #364 (Div. 1)B. Connecting Universities
  • 原文地址:https://www.cnblogs.com/s1awwhy/p/13280083.html
Copyright © 2011-2022 走看看