Java中的容器

525 阅读8分钟

1容器的概念

在Java当中,如果有一个类专门用来存放其它类的对象,这个类就叫做容器,或者就叫做集合,集合就是将若干性质相同或相近的类对象组合在一起而形成的一个整体。 image.png

上图是JAVA常见的各个容器的继承关系。

下图为集合容器中的主要类别。 image.png

Set下各种实现类对比

HashSet

1.底层结构

  1. HashSet底层是直接调用HashMap类实现
  2. HashMap的基本单元Entry对象是key-value,HashSet运用HashMap满足自身要求时,所有key对应的value都是用一个final的Object的。
  3. 因为set里面是用的HashMap<E,Object> map,K, V要求是泛型所以必须传一个对象,这个Object字段是静态常量,set的每个元素都共享这个值, 已经比较省空间了。

举个例子:Hashset 中的Add()方法

HashSet<String> objects = new HashSet<>();
objects.add("3");


public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}


private static final Object PRESENT = new Object();

PRESENT为HashSet类中定义的一个使用static final修饰的常量,其实无实际意义,HashSet的add()方法调用HashMap的put()方法实现,如果键已经存在,map.put()放回的是旧值,添加失败。如果添加成功map.put()方法返回的是null,HashSet.add()方法返回的true,则添加的元素可以作为map中的key。

在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,(先判断table的长度是否大于64,如果小于64,就通过扩容的方式来解决)如果大于64,链表长度大于8,将链表转换为红黑树,这样大大减少了查找时间。

HashMap1.8底层是数组+链表+红黑树 image.png

LinkedHashSet

底层实现

在LinkedHashMap中可以保持两种顺序,分别是插入顺序和访问顺序,这个是可以在LinkedHashMap的初始化方法中进行指定的。相对于访问顺序,按照插入顺序进行编排被使用到的场景更多一些,所以默认是按照插入顺序进行编排。

Map<String, String> test = new LinkedHashMap<String, String>(9); 

        test.put("化学","93"); 
        test.put("数学","98"); 
        test.put("生物","92"); 
        test.put("英语","97"); 
        test.put("物理","94"); 
        test.put("历史","96"); 
        test.put("语文","99"); 
        test.put("地理","95"); 

image.png
在LinkedHashMap中,是通过双联表的结构来维护节点的顺序的。上文中的程序,实际上在内存中的情况如下图所示,每个节点都进行了双向的连接,维持插入的顺序(默认)。head指向第一个插入的节点,tail指向最后一个节点。

image.png

TreeSet

TreeSet的底层是TreeMap,添加的数据存入了map的key的位置,而value则固定是PRESENT。TreeSet中的元素是有序且不重复的,因为TreeMap中的key是有序且不重复的。

对于 TreeMap 而言,它采用一种被称为红黑树的排序二叉树来保存 Map 中每个 Entry —— 每个 Entry 都被当成“红黑树”的一个节点对待。例如对于如下程序而言:

public class TreeMapTest 
 { 
    public static void main(String[] args) 
    { 
        TreeMap<String , Double> map = 
            new TreeMap<String , Double>(); 
        map.put("ccc" , 89.0); 
        map.put("aaa" , 80.0); 
        map.put("zzz" , 80.0); 
        map.put("bbb" , 89.0); 
        System.out.println(map); 
    } 
 }

当程序执行 map.put("ccc" , 89.0); 时,系统将直接把 "ccc"-89.0 这个 Entry 放入 Map 中,这个 Entry 就是该“红黑树”的根节点。接着程序执行 map.put("aaa" , 80.0); 时,程序会将 "aaa"-80.0 作为新节点添加到已有的红黑树中。

以后每向 TreeMap 中放入一个 key-value 对,系统都需要将该 Entry 当成一个新节点,添加成已有红黑树中,通过这种方式就可保证 TreeMap 中所有 key 总是由小到大地排列。例如我们输出上面程序,将看到如下结果(所有 key 由小到大地排列):

 {aaa=80.0, bbb=89.0, ccc=89.0, zzz=80.0} 

对比

HashSet基于哈希表实现,有以下特点:

  • 1.不允许重复
  • 2.允许值为null,但是只能有一个
  • 3.无序的。
  • 4.没有索引,所以不包含索引操作的方法 LinkedHashSet跟HashSet一样都是基于哈希表实现。只不过linkedHashSet在hashSet的基础上多了一个链表,这个链表就是用来维护容器中每个元素的顺序的。有以下特点:
  • 1.不允许重复
  • 2.允许值为null,但是只能有一个
  • 3.按照插入或者访问顺序有序的。
  • 4.没有索引,所以不包含索引操作的方法 TreeSet是SortedSet接口的唯一实现类,是基于二叉树实现的。TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序 和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。有以下特点:
  • 1.不允许重复
  • 2.不允许null值
  • 3.值有序
  • 4.没有索引,所以不包含索引操作的方法

List下各种实现类对比。(这几个类都是有序的,允许重复的)

Vector、ArrayList、LinkedList的区别

这三者都是实现集合框架中的List,也就是所谓的有序集合

Vector是Java早期提供的线程安全的动态数组, 如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。

ArrayList是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector近似, ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector在扩容时会提高1倍,而ArrayList则是增加50%。

LinkedList顾名思义是Java提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

image.png

不同容器类型适合的应用场景

  • Vector和ArrayList作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。
  • LinkedList进行节点插入、 删除却要高效得多,但是随机访问性能则要比动态数组慢
  • 在应用开发中,如果事先可以估计到,应用操作是偏向于插入、删除,还是随机访问较多,就可以针对性的进行选择

Map下各种实现类对比。

对于HashMap,LinkedHashMap,treeMap我们在前面已经讲了。

HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。

  1. 元素特性 HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只能有一个key为null的键值对,但是允许有多个值为null的键值对;\
  2. 顺序特性 HashTable、HashMap具有无序特性。\
  3. 初始化与增长方式
  • 初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂; HashMap默认容量为16,且要求容量一定为2的整数次幂。
  • 扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍。
  1. 线程安全性 HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。

HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步(1)可以用 Collections的synchronizedMap方法;(2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体, ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升。\

  1. hashMap的实现原理 HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。

LinkedHashMap和hashMap的区别在于多维护了一个链表,用来存储每一个元素的顺序,就跟HashSet和LinkedHashSet差不多。

HashMap通常比TreeMap快一点(树和哈希表的数据结构使然),建议多使用HashMap,在需要排序的Map时候才用TreeMap。

java容器中三者的区别

image.png