Java集合

集合框架概述

 集合、数组都是对多个数据进行存储操作的结构,简称java容器

  1. 数组具有局限性:

    • 数组的长度是固定的

    • 数组无法同时存储多个不同的数据类型

    • 数组提供的方法非常有限,对于CRUD操作等操作非常不便且效率不高

  2. 集合的架构

    集合简单理解就是一个长度可以改变,可以保持任意数据类型的动态数组。Java中的集合不是由一个类来完成的,而是一组接口和类构成了一个框架体系,大致可分为3层,最上层时一组接口,继而是接口的实现类。

  3. 集合框架的接口

    • Collection:集合框架最基础的接口、最顶层的接口;单列合集,用来存储一个一个的对象;集合框架最基础的接口、最顶层的接口。

      • List(最常用的接口):Collection的子接口,存储有序的、可重复的数据(动态数组)

        • ArrayList

        • LinkedList

        • vetor

      • Set:Collection的子接口,存储有序的、不可重复的数据(数学上所讲的集合)

        • HashSet

        • LinkedHashSet

        • TreeSet

    • Map:独立于Collection的另外一个接口;双列集合,用来存储一对一对的数据(key - value)(函数:y = f(x));

      • HashMap

      • LinkedHashMap

      • TreeMap

      • Hashtable

      • Properties

    • Iterator:输出集合元素的接口,一般适用于无序集合,从前往后输出。

      • Listiterator:Iterator的子接口,可以双向输出

Collection接口

Collection是集合框架中最基础的父接口,可以存储一组无序、不唯一的对象;一般不直接使用该接口,也不能被实例化,只是用来提供规范。

Collection是Iterable接口的子接口。

Collection接口的方法:

方法描述
int Size() 获取添加的元素的个数
void clear() 清空集合元素
boolean isEmpty() 判断当前集合是否为空
boolean add(E e) 将元素e添加到集合coll中
boolean addAll(Collection coll) 将coll集合中的元素添加到当前的集合中
boolean remove(Object obj) 从当前集合中移除obj元素
boolean removeAll(Collection coll) 从当前集合中移除coll中所有元素
boolean contains(Object obj) 判断当前集合是否包含obj
boolean containsAll(Collection coll) 判断形参coll中的所有元素是否都存在于当前的集合中
coll.retainAll(Collection coll) 差集:从当前集合中移除coll中所有的元素
boolean equals(Object obj) 要想返回true,需要当前集合和形参集合的元素都相同
int hashCode() 返回当前对象的哈希值
Object[] toArray() 将集合转为数组
T[] toArray(T[] a) 将集合转换为一个指定数据类型的数组
iterator<E> iterator() 返回Iterator接口的实例,用于遍历集合元素

Collection的子接口:

  • List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素

  • Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用iterator接口取得所有元素,再逐一遍历各个元素

List接口

List是Collection最常用的一个子接口,可以存储有序,可重复的对象。

List常用的扩展方法:

方法描述
T get(int index) 通过下标返回集合中对应位置的元素
T set(int index, T element) 在集合中的指定位置存入对象
int indexOf(Object o) 从前向后,查找某个对象在集合中的位置
int lastIndexOf(Object o) 从后向前,查找某个对象在集合中的位置
Listiterator<E> listIterator() 实例化ListIterator接口,用来遍历List集合
List<E> subList(int fromIndex, int toIndex) 通过下标截取List集合

List接口的实现类:

  • ArrayList:

    ArrayList是开发中使用频率最高的List实现类,实现了长度可变的数组,在内存分配连续空间,底层使用Object[] elementData存储,所以读取快,增删慢;线程不安全,效率高;

    ArrayList的底层实现:

     // JDK8以前:
     ArrayList list = new ArrayList(); // 底层创建了长度是10的Object[] 数组 elementData
     list.add(123) // elementData[0] = new Integer(123);
     ...
     list.add(123) // 如果此次添加导致底层的elementData数组容量不够,则扩容
     // 默认情况下,扩容为原来的1.5倍,同时需要将原有数组的数据复制到新的数组中
     // 建议开发中使用带参的构造器:ArrayList() = new ArrayList(int capacity);
     // JDK8以后:
     ArrayList() list = new ArrayList(); // 底层Object[] elementData初始化为{},并没有创建长度为10的数组
     list.add(123); // 当第一次调用add()时,底层才创建了长度10的数组,并将数据添加到elementData数组中
     // 扩容机制同 jdk7

    jdk8之前的ArrayList的对象的创建类似于单例的饿汉式,而jdk8以后的ArrayList的对象的创建类似于单例的懒汉式延迟了数组的创建,节省内存

  • LinkedList:对于频繁地插入和删除操作,使用此类效率比ArrayList高;底层使用双向链表存储,实现了先进先出的队列

    LinkedList的底层实现:

     LinkedList list = new LinkedList();  // 内存声明了Node类型的first和last属性,默认值为null
     list.add(123) // 将123封装到Node中,创建了Node对象
     
     // 其中,node定义为:
     private static class Node<E>{
      E item;
      Node<E> next;
      Node<E> prev;
     
      Node(Node<E> prev, E element, Node<E> next){
      this.item = element;
      this.next = next;
      this.prev = prev;
      }
     }
  • Vector:作为List接口的古老实现类;线程安全,效率低

    • Stack:Vector的子类,实现类栈的数据结构

ArrayList和LinkedList的区别:

  1. ArrayList基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾插法插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedlist(需要创建大量的node对象)

  2. LinkedList基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询,需要逐一遍历;便利Linkedlist必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大;另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,当结果为空时会遍历整个列表

 

Set接口

跟List一样,Set也是Collection的子接口,Set集合是以散列的形式存储数据,所以元素时没有顺序的,可以存储一组无序且唯一的数据;Set接口中没有额外定义的新方法,都是Collection中声明过的方法;向Set中添加的数据,其所在类一定要重写hashCode()和equals(),且二者尽可能保持一致性(相等的对象必须具有相等的散列码)。

hashCode与equals:

hashcode()作用是获取哈希码,也称为散列码,它实际上是返回一个int整数。这个哈希码的作用是去确定该函数对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中,java中的任何类都包含由hashCode()函数。

在HashSet中判断两个值是否相等,会先判断二者的hashCode,若hashCode不等,则判定二者不等;若hashCode相等,再使用equals判断二者是否相等。这样能大大地减少equals的次数,提高执行速度。要注意的是,如果equals()方法被重写,则hashCode()方法也必须被重写。

Set接口的实现类:

  • HashSet

    HashSet是开发中经常使用的一个实现类,存储一组无序且唯一的对象;线程不安全;可以存储null值。

    • 无序:不等于随机性;存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的(即存储顺序和比那里顺序不同)

    • 不可重复性:保证添加的元素按照equals()判断时,不能返回true;即相同的元素只能添加一个

    HashSet元素添加过程:我们向HashSet添加元素a,首先调用a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种计算方法算出HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:

    如果此位置上没有其他元素,则元素a添加成功;→ 情况1

    如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:

     如果hash值不相同,则元素a添加成功;→ 情况2

    如果hash值相同,进而需要调用元素a所在类的equals()方法:

     equals()返回true,元素a添加失败

     equals()返回false,则元素a添加成功---> 情况3

    对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储:

    • jdk 7:元素a放到数组中,指向原来的元素

    • jdk 8:原来的元素在数组中,指向元素a

  • LinkedHashSet

    作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历;在添加数据的同时,每个数据还维护了两个引用,记录此数据前驱和后继;对于比较频繁的遍历操作,LinkedHashSet效率高于HashSet

  • TreeSet

    可以按照添加对象的指定属性,进行排序;所以向TreeSet中添加的数据,要求是相同类的对象;

    值得注意的是HashSet和TreeSet都是有序的,但是有序的含义不同;HashSet中有序的含义是存储顺序和遍历顺序一致,TreeSet的有序是指集合内部会自动对所有的元素按照升序进行排列,无论存入的顺序是什么,遍历的时候一定按照升序输出

Map接口

Map是本质上就是一个 key-value 数据字典。

Map中的key是无序的、不可重复的,使用Set存储所有的key;Map中的value是无序的、可重复的,使用Collection存储所有的value

一个键值对:key-value构成了一个entry对象,其是无序的、可重复的,使用Set存储所有的entry。

Map 接口定义时使用了泛型,并且定义两个泛型 K 和 V,K 表示 key,规定键元素的数据类型,V 表示 value,规定值元素的数据类型。

Map接口的方法:

方法描述
int size() 获取集合长度
boolean isEmpty() 判断集合是否为空
boolean containsKey(Object key) 判断集合中是否存在某个 key
boolean containsValue(Object value) 判断集合中是否存在某个 value
V get(Object key) 取出集合中 key 对应的 value
V put(K key,V value) 向集合中存入一组 key-value 的元素
V remove(Object key) 删除集合中 key 对应的 value
void putAll(Map map) 向集合中添加另外一个 Map
void clear() 清除集合中所有的元素
Set keySet() 取出集合中所有的 key,返回一个 Set
Collection values() 取出集合中所有的 value,返回一个 Collection
Set<Map.Entry<K,V>> entrySet() 将 Map 以 Set 的形式输出
int hashCode() 获取集合的散列值
boolean equals(Object o) 比较两个集合是否相等

Map的实现类:

  • HashMap:为Map的主要实现类,存储一组无序,key不可以重复,value可以重复;线程不安全,效率高;可以存储null的key和value

    HahsMap底层原理:

     // jdk8之前
     HashMap map = new HashMap() // 在实例化后,底层创建了长度是16的一维数组Entry[] table
     //...可能已经执行过多次put...
     map.put(key1, value1);  
     // 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算后,得到在Entry数组中的存放位置
     // 如果此位置上的数据为空,此时的key1-value1添加成功 ---> 情况1
     // 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在额一个或多个数据的哈希值:
      // 如果key1的哈希值与已经存在的哈希值都不相同,此时key1-value1添加成功 ---> 情况2
      // 如果key1的哈希值和已经存在的某个数据(key1-value2)的哈希值相同,继续比较,调用key1所在类的equals()方法,比较:
      // 如果equals()返回false,此时key1-value1添加成功 ---> 情况3
      // 如果equals()返回true,使用value1替换value2
     // 情况2和情况3,key1-value1和原来的数据以链表方式存储
     // 默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来
     // jdk8之后
     HashMap map = new HashMap(); // 底层没有创建一个长度为16的数组
     map.put(key1, value1); // 首次调用put()方法时,底层创建长度为16的 Node[] 数组
     // 底层结构为:数组 + 链表 + 红黑树
     // 当数组的某一个索引位置上的元素以链表形式的存在数据个数 > 8,且当前数组的长度 > 64时,此时此索引位置上的所有的数据改为使用红黑树
     
     // DEFAULT_INITAL_CAPACITY :HashMap的默认容量,16
     // DEFAULT_LOAD_FACTOR :HashMap的默认加载因子,0.75
     // threshold :扩容的临界值,= 容量 * 加载因子,16 * 0.75 = 12
     // TREEIFY_THRESHOLD :Bucket中链表长度大于该默认值,转化为红黑树
     // MIN_TREEIFY_CAPACITY :桶中的Node被树化时最小的hash表容量,64
  • LinkedHashMap:在原有的HashMap底层结构基础上,添加了一对指针,指向前驱和后继;保证在遍历map元素时,可以按照添加的顺序实现遍历;对于频繁的遍历操作,此类执行效率要高于HashMap

  • TreeMap:底层使用的是红黑树;保证按照添加的key-value对进行排序,实现排序遍历;此时考虑key的自然排序和定制排序

  • HashTable:

    • HashTable:作为古老的实现类;线程安全,效率低;不能存储null的key和value

    • Properties:常用来处理配置文件;key和value都是String类型

       Properties pros = new Properties();
       
       FileInputStream fis = new FileInputStream("jdbc.properties");
       pros.load(fis) // 加载对应的流文件
       
       String name = pros.getProperty("name");
       
       fis.close();

HashMap和HashTable的区别:

  • 区别:

    1. HashMap方法没有synchronized修饰,线程不安全,HahsTable线程安全

    2. HashMap允许key和value为null,HashTable不允许

  • 底层实现:数组 + 链表

    jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在;

    存储元素的步骤:

    • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标

    • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组

    • 如果产生hash冲突,先进行equal比较,相同则取代该元素;不同则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表

    • key为null,存在下标0的位置

    数组扩容:默认扩容方式为扩容为原来容量的2倍,并将原有的数据复制过来

ConcurrentHashMap原理,jdk7和jdk8版本的区别:

  • jdk7:

    数据结构:ReentranLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构

    元素查询:二次hash,第一次查询定位到Segment,第二次Hash定位到元素所在的链表的头部

    锁:Segment分段锁,Segment继承了ReentranLock,锁定操作的Segment,其他的Segment不受影响,并发度为segment个数,可以通过构造函数指定,数组扩容不会影响其他的Segment

    get方法无需加锁,volatile保证

  • jdk8

    数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性

    查找,替换,赋值操作都是使用CAS

    锁:锁链表的head节点,不影响其他元素的读写,微粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容

    读操作无锁:

    Node的val和next使用volatile修饰,读写线程对该变量互相可见

    数组用volatile修饰,保证扩容时被读线程感知

Collections 工具类

Collections不是接口,它是一个工具类,专门提供了一些对集合的操作,方便开发者去使用,完成相应的业务功能。

Colletions 针对集合的工具类,Collection

Arrays 针对数组的工具类,Array

方法描述
public static sort() 对集合进行排序
public static int binarySearch(List list,Object v) 查找 v 在 list 中的位置,集合必须是升序排列
public static get(List list,int index) 返回 list 中 index 位置的值
public static void reverse(List list) 对 list 进行反序输出
public static void swap(List list,int i,int j) 交换集合中指定位置的两个元素
public static void fill(List list,Object obj) 将集合中所有元素替换成 obj
public static Object min(List list) 返回集合中的最小值
public static Object max(List list) 返回集合中的最大值
public static boolean replaceAll(List list,Object old,Object new) 在 list 集合中用 new 替换 old
ublic static boolean addAll(List list,Object… obj) 向集合中添加元素



posted @ 2021-05-06 00:38  离渊灬  阅读(73)  评论(0)    收藏  举报