java_15:Java容器

Java集合

前言:
集合和数组都可以对多个数据进行存储操作,简称Java容器

数组存储的特点:

1.一旦初始化,长度不可变

2.一旦数组定义好,数据类型也就确定了

3.可以存储有序可重复的数据,对于无序不可重复的需求不能满足

4.数组提供的方法有限,对于添加、删除、插入数据等操作不方便,效率也不高

5.没有现成的方法获取数组中实际有效的元素个数

而集合可以灵活的处理数组的一些缺点


java_15:Java容器

一、Collection接口

单列集合,用来存储一个一个的对象

1.1 List接口

可以存储有序的可重复的数据,是动态的数组,可以存储null值

1. ArrayList
  1. jdk1.2实现,底层数据结构:Object[] elementData.

  2. 线程不安全,但效率高,查找元素的操作方便

  3. 源码分析

    (1)jdk7:
    *初始化:
    ArrayList list=new ArrayList();//底层创建一个长度为10的Object数组;
    *添加元素:
    add元素时,直接想数组的对应下标一次添加
    list.add(123)===elementData[0]=new Integer(123)
    *扩容:
    如果此次添加元素的操作导致底层数组容量不够,则触发扩容。扩容为原来的1.5倍。并将原数组的内容复制到新数组
    oldCapacity + (oldCapacity >> 1) 
    *注意:
    尽量使用带参数的构造器,指明初始化容量,尽量避免扩容,因为扩容用到Arrays.copyOf(),这个操作代价很高
    ArrayList list=new ArrayList(int capacity);
    (2)jdk1.8
    *初始化:
    ArrayList list=new ArrayList();//底层创建一个长度为0的数组Object
    *添加元素:
    第一次调用add()时,底层才创建长度为10的数组,并将数据加入的数组中
    list.add(123)
    *扩容:
    与jdk7无异
    总结:
    jdk7中的ArrayList的对象的创建类似于单例模式的饿汉式,
    jdk8中的ArrayList的对象的创建类似于单例模式的懒汉式,延迟数组创建,节省内存

2. LinkedList

  1. jdk 1.2实现,底层数据结构:双向链表

    //节点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;
        }
    }
    //LinkedList
    内部声明了Node类型的两个属性first和last,默认为null
    transient Node<E> first;
    transient Node<E> last; 

java_15:Java容器

  1. 线程不安全,对于频繁的插入和删除操作效率较高

  2. 源码分析

    *初始化:
    LinkedList lis=new LinkedList();//内部声明了Node类型的两个属性first和last,默认为null
    *添加元素:
    list.add(123);//将123封装到Node中,创建了Node对象
    其中,Node类的定义体现了LinkedList的双向链表的说法

3.Vector

  1. jdk1.0实现,List接口的古老实现类,底层数据结构:Object[] elementData

  2. 线程安全,但效率不高

    public synchronized boolean isEmpty() {
           return elementCount == 0;
       }
  3. 源码分析:

    *初始化:
    jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
    *扩容:
    默认扩容为原来的数组长度的2倍

4.List接口常用方法

    * 增:add(Object obj)
    * 删:remove(int index)/remove(Object obj)--remove(new Integer(2))
    * 改:set(int index,Object ele)
    * 查:indexOf(Obj obj)/lastIndexOf(Obj obj)/get(int index)
    * 插:add(int index,Object obj)
    * 长度:int size();
    * 遍历:
        Iterator 迭代器方式(hasnext(),next()){next:先下移指针,再返回元素。开始是在第一个元素的前一位置}
        foreach()
        普通的循环

5.怎样得到一个下线程安全的List

1.使用Vector--不建议,同步的,访问速度慢,开销大
2.Collections.synchronizedList(list)
    List<String> list = new ArrayList<>(); 
    List<String> synList = Collections.synchronizedList(list); 
3.concurrent并发包下的CopyOnWriteArrayList<>()
    List<String> list = new CopyOnWriteArrayList<>(); 

6.CopyOnWriteArrayList

1.读写分离:

写操作在一个复制的数组进行,读操作还是在原数组进行,互不影响

写操作需要加锁,防止并发写入时导致写入数据丢失

写操作结束后要把原始数组指向新的复制数组

2.特点:写时复制,读写分离。在写的时候,允许读操作,可以提高读操作的性能

3.适合场景:读多写少

//写操作加锁
public boolean add(E e) {     final ReentrantLock lock = this.lock;     lock.lock();     try {         Object[] elements = getArray();         int len = elements.length;         Object[] newElements = Arrays.copyOf(elements, len + 1);         newElements[len] = e;         setArray(newElements);         return true;     } finally {         lock.unlock();     } } 

final void setArray(Object[] a) {     array = a; } 
//读操作
@SuppressWarnings("unchecked") private E get(Object[] a, int index) {     return (E) a[index]; } 

缺陷:java_15:Java容器

1.2 Set接口

(1)存储无序的不可重复的数据

以HashSet为例说明:
(1)无序性:不等于随机性,存储的数据在底层数组中并非按照数组索引的顺序添加, 而是根据数组的哈希值进行添加
(2)不可重复性:保证添加的元素按照equals()判断时,不能返回true
     即相同的元素只能添加一个

(2)向Set中添加的数据,其所在的类一定要重写hashCode()和equals()

(3)重写的hashCode()和equals()尽可能保持一致性,相等的对象必须具有相等的散列码

1.HashSet

  1. 底层数据结构:HashMap(),数组+链表

  2. 线程不安全,可以存储null

  3. 源码分析

    添加元素的过程:以HashSet为例
    (1)调用元素a所在类的hashCode()方法,计算元素a的哈希值
    (2)此哈希值接着通过某种算法计算出HashSet底层数组的存放位置(即为:索引位置)
    (3)判断数组此位置是否已有元素:
        如果没有,则元素添加成功,---》情况1
    如果有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值;
    (4)如果hash值不相同,则元素a添成功---》情况2
    如果hash值相同,进而需要调用元素a所在equals()方法,
    (5)  equals()方法返回true,元素a添加失败
          equals()返回false,则元素a添加成功---》情况3
    说明:
       对于添加成功的情况2和3而言,元素a与已经存在指定索引位置上数据以链表的方式存储
    jdk7:元素a放到数组中,指向原来的元素,链表中a在首位
    jdk8:原来数组中的元素指向a,链表中a放在尾部
    总结:
       七上八下
    

2.LinkedHashSet

  1. 是HashSet的子类,因为添加了Linked,所以遍历内部数据时 可以按照添加的顺序遍历

  2. 对于频繁的遍历操作,效率高于HashSet

  3. 源码分析:底层是LinkedHashMap

    LinkedHashSet作为HashSet的子类,在添加数据的同时,
    每个数据还维护两个引用,记录此数据前一个数据和后一个数据
    优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
    

3.TreeSet

  1. 底层数据结构:红黑树,可以按照添加元素的指定属性进行排序

  2. 源码分析

    1.向TreeSet中添加的数据,要求是相同的类对象,不能添加不同类的对象
    2.两种排序方式:自然排序和定制排序
    3.自然排序中,比较两个对象是否相同的标准为compareTo()返回0,不再是equals()
    4.定制排序中,比较两个对象是否相同的标准为compa()返回0,不再是equals()
    5.构造器可以传入一个比较器做参数    
       public TreeSet(Comparator<? super E> comparator) {
           this(new TreeMap<>(comparator));
       }
    @Test
       public void test4(){
    
           Comparator comparator=new Comparator() {
               //按照年龄从小到大排列
               @Override
               public int compare(Object o1, Object o2) {
                   if (o1 instanceof Person && o2 instanceof Person) {
                       Person p1 = (Person) o1;
                       Person p2 = (Person) o2;
                       return Integer.compare(p1.age,p2.age);
                   }else {
                       throw new RuntimeException("输入的数据类型不匹配");
                   }
               }
           };
           TreeSet set1=new TreeSet(comparator);//将比较器传进去
           set1.add(new Person("ZhaoMin", 20));
           set1.add(new Person("WangYiBo", 22));
           set1.add(new Person("XiaoZhan", 28));
           set1.add(new Person("XiaoZhan", 26));
           Iterator iterator=set1.iterator();
           while (iterator.hasNext()) {
               System.out.println(iterator.next());
           }
       }
    
4.常用方法

使用的都是Collection中声明过的方法

5.TreeSet\HashSet\LinkedHashSet的区别

java_15:Java容器

二、Map接口

  1. 双列集合,存储key-value数值对的数据。entry{key,value}

  2. Map中的key:
    无序的不可重复的,用Set存储所有的key;
    key所在类要重写equals和hashcode

3.Map中的value:
无序的、可重复的,使用Collection存储所有的value
value所在的类要重写equals

4.Map中的entry:
无序的、不可重复的
使用Set存储所有的entry

1.HashMap
  1. Map的主要实现类,线程不安全,效率高。

  2. 可以存储null的key和value,键值对

java_15:Java容器

  1. 底层数据结构:

    1.7:数组+链表

    1.8:数组+链表+红黑树

  2. 源码分析:
(1)jdk1.7
    *初始化:
    HashMap map=new HashMap();//在实例化以后,底层创建了长度为16的一维数组Entry[] table
    *底层结构:
        数组+链表
    *put(key,value):
    (1)首先调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法以后,得到在数组Entry中的存放位置;
   (2)如果此位置的上的数据为空,则key1-value1添加成功---情况1
   (3)如果此位置上的数据不为空,(意为着在此位置已经存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
      都不相同:key1-value1添加成功---情况2
      和某一数据(key2-value2)相同,继续比较:调用key1所在类的equals(key2):
         如果equals()返回false:key1-value1添加成功---情况3
         如果equals()返回true:使用value1替换value2
   补充:关于情况2、3:此时key1-value1和原来的数据以链表的形式存储
   *扩容:在不断添加的过程,会涉及到扩容问题,
        默认的扩容方式:当超出临界值12(且要存放放入位置非空)时, 扩容为原来容量的2倍,并将原有的数据复制过来
(2)jdk8:
*初始化:
    new HashMap();//底层没有创建一个长度为16的数组
*map.put(key,value):
    -首次调用put()方法创建一个长度为16的数组Node[]
    -与1.7类似
    -使用红黑树:
     当数组中的某一个索引位置上的元素以链表形式存在的数据个数>8,且当前数组的长度>64时,此时此索引位置上的索引数据改为使用红黑树存储,提高效率,
*底层结构:
        数组+链表+红黑树
*补充常量:
 * EFAULT_INITIAL_CAPACITY:HashMap的默认容量:16
 * DEFAULY_LOAD_FACTORY:HashMap的默认加载因子:0.75
 * threshold:扩容的临界值=容量*填充因子: 16*0.75》=12
 * TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
     当小于该值师,又转为链表。8--符合泊松分布
     红黑树使用的频率不高,到8的时候按照泊松分布,出现的概率非常小        0.000000006 
 * MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
 * 扩容是为了尽量减少链表的使用
  1. 负载因子值得大小对HashMap有什么影响

    (1)负载因子的大小决定了HashMap的数据密度
    (2)负载因子越大,密度越大,越容易碰撞,数组中链表越长,造成查询、插入时比较的次数增多,性能下降
    (3)负载因子越小,越容易触发扩容,数据密度越小,发生碰撞几率变小,数组中链表越短,查询和插入时比较次数越少,性能会更高。 但会浪累一定的内容空间,而且经常扩容影响性能。建议初始化时预设大一点的空间。
    (4)负载因子设置为0.7-0.75,此时平均检索长度接近于常数
    

6.实现细节:

(1)确定桶的下标(key在数组中的索引)

①取模:hash%capacity()---性能不高,如果hash为负值,则索引也为负,不可取
②位运算:hash&(length-1)--提高性能,解决负数问题,length=2^n
hash&(length-1),length-1=11111...,所以&之后得到的数组下标肯定在
    0--length-1(2^n-1)的范围内
 为什么是2^n,就是为了让它-1之后得到的是一个二进制表示全为1的结果

(2)扩容--动态扩容
java_15:Java容器
扩容时:capacity为原来的2倍,使用resize()扩容,需要将原数组中的键值对插入新的数组中。并重新计算桶下标

java_15:Java容器

2.LinkedHashMap

  1. 继承于HashMap,保证在遍历map元素时,可以按照添加的顺序实现遍历
原因:在原有的HashMap底层结构基础上,添加了一对指针head和tail,维护了一个双向链表指向前一个和后一个       
对于频繁的遍历操作,此类执行效率高于HashMap
  1. LinkedHashMap 重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

    void afterNodeAccess(Node<K,V> p) {
       //当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在 每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是近访问的节点,那么链表首部就是近久未 使用的节点
    } 
    void afterNodeInsertion(boolean evict) {
       //在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除晚的节点,也就是链表首部节点 first
       //removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现, 这在实现 LRU 的缓存中特别有用,通过移除近久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是 热点数据。
    } 
    

    3.LRU缓存

java_15:Java容器

3.Map中常用的方法:
添加:put(Object key,Object value)
删除:remove(Object key)\clear()
修改:put(Object key,Objec value)
查询:get(Object key)
长度:size()
遍历:keySet()/values()/entrySet()
     Set set=map.keySet();
        Iterator iterator=set.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
        //2.Collection values();//遍历所有的value
        Collection collection=map.values();
        for (Object obj : collection) {
            System.out.println(obj);
        }
        //3.entrySet();//遍历所有的键值对Entry
        //--方式一:
        Set set1=map.entrySet();
        for (Object obj : set1) {
            //entrySet集合中所有的元素都是entry
            Map.Entry entry=(Map.Entry)obj;
            System.out.println(entry.getKey()+"-->"+entry.getValue());

        }
        System.out.println("====方式二====");
        //--方式二:
        Set set2=map.keySet();
        Iterator iterator1=set2.iterator();
        while (iterator1.hasNext()) {
            Object key=iterator1.next();
            Object value=map.get(key);
            System.out.println(key+"-->"+value);
        }

4.TreeMap

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

向TreeMap中添加key-value,要求key必须是由同一个类创建的对象  因为要按照key进行排序:自然排序(默认)、定制排序 
 //定制排序
    @Test
    public void test1(){
        Person p1 = new Person("Tom", 12);
        Person p2 = new Person("Jerry", 10);
        Person p3 = new Person("Lion", 15);
        Person p4 = new Person("Jeff", 15);

        Map map=new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Person && o2 instanceof Person) {
                    Person p1 = (Person) o1;
                    Person p2 = (Person) o2;
                    return Integer.compare(p1.age,p2.age);
                }
                throw new RuntimeException("输入的类型不匹配");
            }

        });
        map.put(p1, 90);
        map.put(p2, 89);
        map.put(p3, 96);
        map.put(p4, 88);

        Set entry=map.entrySet();
        Iterator iterator=entry.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

}

java_15:Java容器

5.Hashtable

(1)古老的实现类,线程安全,效率低

(2)不能存储null的key和value

6.Properties

常用来处理配置文件,key-value都是String类型

7.HashMap和Hashtable的区别:

(1)线程安全:
    HashMap线程不安全效率高,Hashtable线程安全效率低
(2)存储值:
    HashMap:可以存储null的key和value
    Hashtable:不能存储null的key和value
(3)元素次序:
    HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。
(4)HashMap 的迭代器是 fail-fast 迭代器。每次检查一下结构有没有变化,如果发生变化就抛出异常ConcurrentModificationException
  结构发生变化是指添加或者删除至少一个元素的所有操作,或 者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

8.ConcurrentHashMap

  1. ConcurrentHashMap 和 HashMap 实现上类似,主要的差别:ConcurrentHashMap 采用了分段锁 (Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发 度更高(并发度就是 Segment 的个数)。

  2. Segment 继承自 ReentrantLock

    static final class Segment<K,V> extends ReentrantLock implements Serializable { 
    
       private static final long serialVersionUID = 2249069246763182397L; 
    
       static final int MAX_SCAN_RETRIES =         Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; 
    
       transient volatile HashEntry<K,V>[] table; 
    
       transient int count; 
    
       transient int modCount; 
    
       transient int threshold; 
    
       final float loadFactor; } 
    final 
    
  3. 默认的并发级别为 16,也就是说默认创建 16 个 Segment。

    static final int DEFAULT_CONCURRENCY_LEVEL = 16; 
    

    4.size操作
    每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。

    /**  * The number of elements. Accessed only either within locks  * or among other volatile reads that maintain visibility.  */ 
    transient int count; 
    

    java_15:Java容器

    java_15:Java容器

8.TreeMap/HashMap/LinkedHashMap/ConcurrentHashMap

java_15:Java容器

猜你喜欢

转载自blog.51cto.com/14234228/2492095
今日推荐