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

原文地址:https://www.cnblogs.com/s1awwhy/p/13280083.html