Java集合,List,Set,Collection,Map实现原理

 一、集合概述

  • 为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java 提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java 所有的集合类都位于 java.util 包下,提供了一个表示和操作对象集合的统一构架,包含大量集合接口,以及这些接口的实现类和操作它们的算法。
  • Java 集合类型分为 Collection 和 Map,它们是 Java 集合的根接口,这两个接口又包含了一些子接口或实现类。

1.1集合与数组的区别

  • 数组可以保存基本类型的数据,也可以是引用类型的数据;而集合只能保存对象,比如你存入一个int型数据放入集合中,其实它是自动转换成Integer类后存入的,Java中每一种基本数据类型都有对应的引用类型。
  • 数组只能存储同一种数据类型的数据,集合可以存储不同类型的数据。(但是一般也只保存同一种数据类型的数据)。
  • 数组的长度一旦初始化就被定义好了,不可变;集合的长度是可变的.

1.2集合的大致接口实现

|----Collection接口:单列集合,用来存储一个一个的对象
     |----List接口:存储有序的、可重复的数据
           |----ArrayList:作为List接口的主要实现类,线程不安全的,效率高;底层采用Object[] elementData数组存储
           |----LinkedList:对于频繁的插入删除操作,使用此类效率比ArrayList效率高底层采用双向链表存储
           |----Vector:作为List的古老实现类,线程安全的,效率低;底层采用Object[]数组存储
           
     |----Set接口:存储无序的、不可重复的数据 
           |----HashSet:作为Set接口主要实现类;线程不安全;可以存null值
           		|----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加顺序遍历;对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
           |----TreeSet:可以按照添加对象的指定属性,进行排序。


|----Map:双列数据,存储key-value对的数据 
     |----HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value
          |----LinkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历,底层双向链表实现。
                    原因:在原的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。
     |----TreeMap:保证照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树
     |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
          |----Properties:常用来处理配置文件。key和value都是String类型


二、Collection接口

  • Collection 接口是 List、Set 和 Queue 接口的父接口,通常情况下不被直接使用。
  • Collection 接口定义了一些通用的方法,通过这些方法可以实现对集合的基本操作。定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。

2.1 Collection接口常用的方法

 Collection接口的常用方法
方法名称说明
boolean add(E e)向集合中添加一个元素,如果集合对象被添加操作改变了,则返回 true。E 是元素的数据类型
boolean addAll(Collection c)向集合中添加集合 c 中的所有元素,如果集合对象被添加操作改变了,则返回 true。
void clear()清除集合中的所有元素,将集合长度变为 0。
boolean contains(Object o)判断集合中是否存在指定元素
boolean containsAll(Collection c)判断集合中是否包含集合 c 中的所有元素
boolean isEmpty()判断集合是否为空
Iterator<E>iterator()返回一个 Iterator 对象,用于遍历集合中的元素
boolean remove(Object o)从集合中删除一个指定元素,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true。
boolean removeAll(Collection c)从集合中删除所有在集合 c 中出现的元素(相当于把调用该方法的集合减去集合 c)。如果该操作改变了调用该方法的集合,则该方法返回 true。
boolean retainAll(Collection c)从集合中删除集合 c 里不包含的元素(相当于把调用该方法的集合变成该集合和集合 c 的交集),如果该操作改变了调用该方法的集合,则该方法返回 true。
int size()返回集合中元素的个数
Object[] toArray()把集合转换为一个数组,所有的集合元素变成对应的数组元素。

三、Collection接口子接口:List

1.概述

  • List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引,第一个添加到 List 集合中的元素的索引为 0,第二个为 1,依此类推。
  • List 实现了 Collection 接口,它主要有两个常用的实现类:ArrayList 类和 LinkedList 类。

2.List接口常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:

方法        描述
void add(int index, Object o)在index位置插入ele元素
 boolean addAll(int index, Collection c)从index位置开始将c中的所有元素添加进来
                                Object get(int index)获取指定index位置的元素
                                Object remove(int index)       移除指定index位置(0是第一个元素)的元素,并返回此元素
                                Object set(int index, Object o)                                设置指定index位置的元素为o
 List subList(int fromIndex, int toIndex)返回从fromIndex到toIndex位置的子集合

3.List实现类(1)ArrayList

ArrayList 类实现了可变数组的大小,存储在内的数据称为元素。它还提供了快速基于索引访问元素的方式,对尾部成员的增加和删除支持较好。使用 ArrayList 创建的集合,允许对集合中的元素进行快速的随机访问,不过,向 ArrayList 中插入与删除元素的速度相对较慢。

ArrayList实现List接口后,未新增方法,我们就简单实现一下List新增方法。

创建一个汽车类,在该类中定义 2 个属性和 toString() 方法,分别实现 setter/getter 方法。代码的实现如下:

 class Car {
        private String name;
        private Integer price;
        public Car(String name, Integer price) {
            this.name = name;
            this.price = price;
        }
        @Override
        public String toString() {
            return "Car{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }

接下来就是主函数:

public static void main(String[] args) {
        Car car = new Car("宝马", 400000);
        Car car1 = new Car("宾利", 5000000);
        ArrayList<Car> cars = new ArrayList<>();
        cars.add(car);
        cars.add(car1);
        // 刚开始遍历
        for (Car car2 : cars) {
            System.out.println(car2);
        }
        System.out.println("========");
        Car bens = new Car("bens", 1000000);
        // void add(int index, Object o)
        cars.add(0, bens);
        System.out.println("加入一个对象后集合的size: " + cars.size());
        // Object remove(int index)
        System.out.println("被移除的对象是: " + cars.remove(2));
        System.out.println("移除一个对象后集合的size: " + cars.size());
        // Object get(int index)
        System.out.println("根据索引0取到的对象: " + cars.get(0));
        // Object set(int index, Object o)
        Car car3 = new Car("三轮", 100);
        System.out.println("======== ========");
        // 最后遍历一下全部
        for (Car car2 : cars) {
            System.out.println(car2);
        }
   
    }

输出结果如下:

Car{name='宝马', price=400000}
Car{name='宾利', price=5000000}
========
加入一个对象后集合的size: 3
被移除的对象是: Car{name='宾利', price=5000000}
移除一个对象后集合的size: 2
根据索引0取到的对象: Car{name='bens', price=1000000}
======== ========
Car{name='bens', price=1000000}
Car{name='宝马', price=400000}

4.List实现类(2)LinkedList

LinkedList 类采用链表结构保存对象,这种结构的优点是便于向集合中插入或者删除元素。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高,但是 LinkedList 类随机访问元素的速度则相对较慢。这里的随机访问是指检索集合中特定索引位置的元素。

LinkedList 类除了包含 Collection 接口和 List 接口中的所有方法之外,还特别新增了以下表中所示的方法。

       

LinkList类中的方法

方法名称 说明 void addFirst(E e) 将指定元素添加到此集合的开头 void addLast(E e) 将指定元素添加到此集合的末尾 E getFirst() 返回此集合的第一个元素 E getLast() 返回此集合的最后一个元素 E removeFirst() 删除此集合中的第一个元素 E removeLast() 删除此集合中的最后一个素

用一个简答的例子来实现一下LinkedList新增的方法.代码的实现如下:

  public static void main(String[] args) {
        LinkedList<String> fruit = new LinkedList<>();
        fruit.add("苹果");
        fruit.add("香蕉");
        fruit.add("菠萝");
        fruit.add("山竹");
        fruit.add("草莓");
        System.out.println("        ******** 目前水果店中的类型有 ********");
        for (String s : fruit) {
            System.out.printf(s + "\t");
        }
        System.out.println();
        String s1 = new String("橘子");
        String s2 = new String("甘蔗");

        fruit.addFirst(s1);
        fruit.addLast(s2);
        System.out.println("****    在集合头部添加了\'橘子\'和尾部添加了\'甘蔗\'后水果店类型 
   ****");
        for (String s : fruit) {
            System.out.printf(s + "\t");
        }
        System.out.println();
        String s = fruit.removeLast();
        String last = fruit.getLast();
        System.out.println("删除的最后一个对象为:  " + s);
        System.out.println("集合现最后一个对象为:  " + last);
    }

输出结果如下:

        ******** 目前水果店中的类型有 ********
苹果	香蕉	菠萝	山竹	草莓	
****    在集合头部添加了'橘子'和尾部添加了'甘蔗'后水果店类型    ****
橘子	苹果	香蕉	菠萝	山竹	草莓	甘蔗	
删除的最后一个对象为:  甘蔗
集合现最后一个对象为:  草莓

5.List实现类(3)Vector

Vector的内部实现类似于ArrayList,Vector也是基于一个容量能够动态增长的数组来实现的,该类是JDK1.0版本添加的类,它的很多实现方法都加入了同步语句,因此是线程安全的。

但因为他的线程安全,因此效率很慢,所有不推荐使用,现在有更好的实现线程安全的替代方法,比如CopyOnWriteArrayList。

由于Vector是JDK1.0版本的类,所以方法名可能有所不同,但实际实现的效果都差不多。下面列举了几个方法:

public void addE1ement (E obj)相当于add()
        public E elementAt(int index) 相当于get()
      public Enumeration<E> elements()iterator()

6.ArrayList,LinkedList,Vector的区别

ArrayList   Vector     LinkedList
底层实现方式Object数组Object数组双向链表
读写机制插入:超过当前数组预定义的最大值时,数组需要扩容,扩容为原数组大小的1.5倍,扩容过程需要调用底层System.arraycopy()方法进行数组复制操作
删:删除元素时并不会减少数组的容量,如需缩小容量,可调用trimToSize()方法
读写机制与ArrayList基本一致,区别是Vector在扩容时,容量会提高一倍在查找元素时,需遍历链表
在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除
读写效率 ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化,对其进行插入和删除速度较慢,但检索速度快 效率低 LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但检索速度较慢
线程安全性 非线程安全线程安全非线程安全

四、Collection接口子接口:Set

1.概述

  • Set 集合类似于一个罐子,程序可以依次把多个对象“丢进”Set 集合,而 Set 集合通常不能记住元素的添加顺序。也就是说 Set 集合中的对象不按特定的方式排序,只是简单地把对象加入集合。Set 集合中不能包含重复的对象,并且最多只允许包含一个 null 元素。
  • 它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,但是比 Collection 接口更加严格。
  • Set 实现了 Collection 接口,它主要有两个常用的实现类:HashSet 类和 TreeSet类。

2.Set实现类(1)HashSet

  • hashSet底层其实是通过hashMap实现的。它将要存储的对象直接做为map的key,并且创建一个空的object对象作为map的值。
  • 因为HashMap的Key不可重复,而我们只关心Key,所以HashSet元素不可重复,元素无序
  • 要想真正了解HashSet的实现,那你就得了解HashMap的具体实现。

3.Set实现类(2)LinkedHashSet

LinkedHashSet继承了HashSet。与HashMap有所不同的是它底层使用LinkedHashMap来实现,即在HashMap的基础上新增加了一个双向链表来确保迭代有序(迭代的元素顺序和插入的元素顺序相同)。因此性能略低于HashSet,但是迭代访问元素时将具有很好的性能,因为它的内部是以链表来实现。

4.Set实现类(3)TreeSet

  • 它存储唯一的元素
  • 它不保留元素的插入顺序
  • 它按升序对元素进行排序
  • 它不是线程安全的

与 HashSet 完全类似的是,TreeSet 里绝大部分方法都是直接调用 TreeMap 的方法来实现的。

对象根据其自然顺序以升序排序和存储。该TreeSet中使用红黑树。

五、Map

1.概述

  • Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象。用于保存具有映射关系的数据。
  • Map 集合里保存着两组值,一组值用于保存 Map 里的 key,另外一组值用于保存 Map 里的 value,key 和 value 都可以是任何引用类型的数据。Map 的 key 不允许重复,value 可以重复,即同一个 Map 对象的任何两个 key 通过 equals 方法比较总是返回 false。

2.Map常用方法

Map接口的常用方法
方法名称说明
void clear()删除该 Map 对象中的所有 key-value 对。
boolean containsKey(Object key)查询 Map 中是否包含指定的 key,如果包含则返回 true。
boolean containsValue(Object value)查询 Map 中是否包含一个或多个 value,如果包含则返回 true。
V get(Object key)返回 Map 集合中指定键对象所对应的值。V 表示值的数据类型
V put(K key, V value)向 Map 集合中添加键-值对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。
void putAll(Map m)将指定 Map 中的 key-value 对复制到本 Map 中。
V remove(Object key)从 Map 集合中删除 key 对应的键-值对,返回 key 对应的 value,如果该 key 不存在,则返回 null
boolean remove(Object key, Object value)这是 Java 8 新增的方法,删除指定 key、value 所对应的 key-value 对。如果从该 Map 中成功地删除该 key-value 对,该方法返回 true,否则返回 false。
Set entrySet()返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry
Set keySet()返回 Map 集合中所有键对象的 Set 集合
boolean isEmpty()查询该 Map 是否为空(即不包含任何 key-value 对),如果为空则返回 true。
int size()返回该 Map 里 key-value 对的个数
Collection values()返回该 Map 里所有 value 组成的 Collection

3.实现类HashMap

  • HashMap是Java语言中用的最频繁的一种数据结构。
  • HashMap 在jdk1.7的底层数据结构是数组加链表。
  • HashMap 在jdk1.8的底层数据结构是数组+链表+红黑树。

我们以1.8版本为例:

put方法源码解析

put方法其实是调用的putVal方法 

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

putVal方法 

  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        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;
    }

       (⊙o⊙)…    没错,它就是长这么恶心,那我们就分块分别来解析吧。

(1)Node<K,V>[] tab中tab表示的就是数组。Node<K,V> p中p表示的就是当前插入的节点 

(2)

 if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

如果数组为空,就调用resize 函数初始化数组;

(3)

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

如果要插入的位置p为空,也就是没有节点,那么直接插入到该位置

(4)

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);

如果hash冲突,要插入的位置的节点的键与传入的节点的键相同。那么直接替换掉原来位置上的节点。如果被插入的位置是红黑树,那么转化为树节点插入到树中。

(5)

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;
            }

 遍历链表,如果遍历到链表最后,没有找到和要插入的节点的key相同的节点,就将要插入的节点追加到链表的最后,并且此时如果链表的长度大于8了,就应该将链表转化为红黑树,退出循环;如果在链表中找到了和要插入的节点的key相等的节点,直接退出循环。

后续操作,如果e不为null,说明找到了和要插入的节点的key相等的节点,就将要插入的节点的value替换给老节点e的value,并对e节点进行afterNodeAccess(e)操作。

(6) 

 ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

插入成功之后,还要判断一下实际存在的键值对数量size是否大于阈值threshold。如果大于那就开始扩容了。

4.实现类TreeMap

TreeMap 类的使用方法与 HashMap 类相同,唯一不同的是 TreeMap 类可以对键对象进行排序,这里不再赘述。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值