zoukankan      html  css  js  c++  java
  • Java数据结构笔记

    本文通过MetaWeblog自动发布,原文及更新链接:https://extendswind.top/posts/technical/java_data_structure

    系统的看一下Java支持的数据结构,记一下从数据结构到java实现的一些基础笔记。以下内容主要参考《java核心技术》与jdk11源码。

    用于保存对象的数据结构一般称作容器类,也称作泛型集合(generic collection,由于容易和Collection接口混淆,因此有些书直接叫做容器类container library)。主要分为Collection和Map两种,Collection用于存储独立的元素,而Map用于存储“键值对”对象。

    一些细节

    在查找元素、查找位置、移除等操作中,判断对象是否相同的方式是调用equal函数。

    retainAll(Collection<?> c)求两个集合的交集。

    实现时接口与实现分离。使用时用满足需要的接口(如队列使用Queue),针对具体的场景new合适的实现。当需要自行实现对应的功能,为了降低实现接口中过多函数的复杂程度,可以直接扩展对应的Abastract类(如AbstractQueue),这种抽象接口加抽象类的方式在集合设计中经常遇到。

    集合类只能容纳对象句柄。集合在存储基本类型时,会通过封装器(Integer等)将基本类型转换成普通类,因此在处理效率上不如数组(直接存储基本类型)。

    迭代器 Iterator

    public interface Iterator<E>{
      E next();
      boolean hasNext();
      void remove();
    }
    // 并没有一个函数直接返回迭代器指向位置的值
    
    public interface Iterable<E>{
      Iterator<E> iterator();
    }
    

    for each循环可以针对任何实现了Iterable的对象(由编译器直接翻译成Iterator对应的代码)。for (String e: c){…}

    Collection接口实现了Iterable接口,可以返回遍历元素的迭代器。

    通过迭代器,能够用一套代码访问不同的容器类。

    和C++的迭代器指向具体位置的设计不同,java的迭代器指向的位置可以看作是两个元素的中间。当调用next时,迭代器会跳过下一个元素,并返回被跳过元素的引用。不能像C++那样直接取当前位置的元素。对于remove函数,删除的是上一次next函数返回的位置(由于经常需要通过此位置的值判断是否删除)。也因此,每次调用remove函数前必须调用一次next方法,因此连续调用两次remove会出错。

    List

    ArrayList相当于dynamic array,随机访问快,随机插入慢。类似的实现还有Vector,相当于一个线程安全的ArrayList。

    LinkedList,双向链表,随机访问慢,随机插入快。LinkedList比较特殊,除了List接口还是了双端队列的Deque接口。

    LinkedList无法获取到node的指针,但可以通过获取ListIterator控制前后的位置(相对于Iterator,添加了previous等向前访问的函数)。

    LikedList并没有缓存指针的位置,因此get(n)等随机访问和修改的操作效率不高。

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    

    Map

    《java核心技术》中将Map的介绍放在了Set之后,但是Hashset的实现直接使用了HashMap,此处先写写Map的实现。

    HashMap

    (下面的默认参数取自openjdk11源码)

    实现常量时间的数据get和put。

    HashMap用hashcode结合链表的实现。使用了桶(bucket)机制,将hashcode取余后放入一个链表,当一个桶中的数据达到8个时,会通过treeifyBin函数将链表的形式转成红黑树存储以加快检索效率。

    在构造函数中设置了两个参数。threshold(默认为16)和loadFactor(默认为0.75),初始化时桶的数量会用threshold,往后threshold会等于 桶的数量×loadFactor。添加元素时,HashMap中的元素个数达到threshold时会调用resize让桶的数量翻倍,此时会遍历先前的所有元素添加到扩容之后的数组中(rehash过程)。

    通过一个名为table的数组存储桶的节点,默认情况下的初始化桶的个数为16,每次会增加为之前的2倍,最大为Int型的最大值。

    transient Node<K,V>[] table;
    
    static class Node<K,V> implements Entry<K,V> {
     final int hash;
     final K key;
     V value;
     Node<K,V> next;
     // ...
    }
    

    resize过程使用的头插入(新的table数组中插入的元素插入到链表头部,避免遍历到尾部的开销)。

    当一个桶中的数据达到TREEIFY_THRESHOLD(8)个,并且桶的数量超过MIN_TREEIFY_CAPACITY(64)时,会在treeifyBin函数中将每个桶中的数据从链表转换成红黑树。

    通过hashCode()函数得到hash值,通过equal()函数判断对象是否相等。

    并发访问中,HashMap的实现中并没有考虑多线程的问题,在多线程结构化(structurally)修改HashMap时可能会出问题(结构化主要指添加和删除,修改某个key对应的值不算结构化修改)。

    TreeMap

    基于红黑树实现的Map,对于获取键值、插入、删除的操作时间复杂度为log(n)。put时将数据插入红黑树,get时从树中取数据。

    由于数据存储在红黑树有序排列,获得排序后的数据较快。

    将key-value键值对做为节点插入到红黑树中,由于需要排序,通过Comparator<? super K>接口让插入的键值可比较。

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
    	// ...
    }
    

    当类默认的比较函数不能满足需要时,可以另外定义新的comparator传入TreeMap。

    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    

    EnumMap 用于枚举类型

    所有的key必须是同一个enum类型中。内部通过数组的方式表达。每个类型对应数组中的一个元素。

    put时直接获取key对应的enum位置,直接对数组中的此位置赋值。

    private transient K[] keyUniverse;
    private transient Object[] vals;
    
    public V put(K key, V value) {
        typeCheck(key);
        int index = key.ordinal();
        Object oldValue = vals[index];
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);
    }
    

    在put函数中,直接获取key对应的enum索引位置

    LinkedHashMap实现原理

    LinkedHashMap通过一个额外的双向链表链接所有的元素,通过构造函数中的参数accessOrder决定记录元素添加顺序还是元素访问顺序。

    可以用来copy一个map,使新的map中的元素顺序与被赋值的map顺序相同(如下)。也适用于LRU cache类似的场景。

    void foo(Map m) {
      Map copy = new LinkedHashMap(m);
      // ...
    }
    

    源码实现

    总体逻辑为,每次添加新的元素,都回加入到链表的尾部。每次访问节点时,如果accessOrder为true(链表按照访问顺序),则将当前访问的节点放到链表尾部;当accessOrder为false(链表按照插入顺序),则不对链表操作。

    通过两个指针可以获得所有元素的链表。

    // The head (eldest) of the doubly linked list.
    transient LinkedHashMap.Entry<K,V> head;
    
    // The tail (youngest) of the doubly linked list.
    transient LinkedHashMap.Entry<K,V> tail;
    

    jdk11的实现上略微有点跳,不同于jdk8直接重写了关键函数,jdk11通过多态的形式插入了一些和HashMap不同的操作。

    想象中的让链表记录元素插入顺序的方式,直接在put新的元素后,直接将新的元素加入到链表(记录元素访问顺序在get函数中类似)。jdk11在实现时,在HashMap类中定义了三个空函数用于在LinkedHashMap中实现:

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
    

    三个函数分别在Node的访问、插入、删除三种情况后被调用,在HashMap中为空函数,LinkedHashMap中具体实现。 但是,链表的插入顺序并没有直接放在afterNodeInsertion函数中,而是重写了创建新节点的newNode函数:

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
         Entry<K,V> p =
             new Entry<>(hash, key, value, e);
         linkNodeLast(p); // 将p节点通过tail指针添加到链表尾部
         return p;
    }
    

    其它Map

    WeakHashMap 值不用后会被回收,利用了GC机制中的标记。

    IdentityHashMap 用==而非equals比较键值。

    Set

    接口和Colection基本相同,除了java9加了几个针对不可变集合的函数。

    public interface Set<E> extends Collection<E> {...}
    

    Set的内部很多都直接使用上面的Map实现,如HashSet内部直接使用了HashMap做为存储,在添加元素时,将加入的元素作为key,用一个常量作为value。

    private transient HashMap<E,Object> map; 
    private static final Object PRESENT = new Object();
    
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    

    LinkedHashSet 记录了插入的顺序。

    TreeSet使用了TreeMap,通过红黑树适用于需要排序的数据。

    EnumSet包含枚举类型

    Queue

    ArrayDeque and LinkedList

    Deque(double-ended queue)双端队列,发音为|deck|。

    常见的队列主要有ArrayDeque(数组实现的双端循环队列)和 LinkedList(链表实现的双端队列),其中LinkedList虽然名字是List,但实现了Queue的函数,定义如下)。

    通常情况下ArrayDeque的数组型实现效率会高于LinkedList的指针型实现。

    public class LinkedList<E>
        extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
    	//...
    }
    

    双端循环队列的addFirst函数实现如下

    // 其中Deque表示“double end queue”,发音为deck
    public interface Deque<E> extends Queue<E> {...}
    
    // ArrayDeque使用的是循环队列
    // addFirst函数会向队列头部加入一个新的元素
    // 当头部为0时,会通过dec函数将head指针移到数组尾部对应的位置
    public void addFirst(E e) {
        if (e == null)
            throw new NullPointerException();
        final Object[] es = elements;
        es[head = dec(head, es.length)] = e;
        if (head == tail)
            grow(1);
    }
    
    static final int dec(int i, int modulus) {
        if (--i < 0) i = modulus - 1;
        return i;
    }
    

    PriorityQueue

    通过一个数组形式的堆实现最小优先队列,transient Object[] queue;,默认将最小值放在堆顶,peek()函数返回堆中最小值。没有直接的更改顺序的方式,如果需要将最大值放在堆顶需要传入Comparator接口的实现。

    满足堆的性质,poll()函数返回队列中的最小值或者最大值O(log(n))。

    Stack

    public class Stack<E> extends Vector<E> {...}
    
    public class Vector<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
    

    LIFO (后进先出)

    源码中的一些操作

    位操作

    用按位与操作提高取余效率

    仅针对取余数为2^n次方的数。

    对2取余实质上就是取2进制的最后一位,对4取余实质上是取2进制数的最后两位。

    如 5 % 2 = (2进制)101 & 001。

    2^n - 1 = (2进制的n个1)11….1

    hash % n 等于 (n - 1) & hash

    移位表示2^n

    1 « 4 // 相当于2^4

    当数组为2的倍数时,向左移m位相当于乘2的m次方

    4 « m // 相当于 4*(2^m)

    移位表示乘2和除2

    22 » 1 // 除以二 22 « 1 // 乘2 22 »> 1 // 忽略符号位除以2

    通过-1向右移位»>

    对于8位的byte,-1 的源码为 10000001,因此可以通过»>得到一个00010000的2^n的数。

    在HashMap中,hash桶的数量只能为2^n。下面的函数能够获取最小的n使2^n > cap。

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    
  • 相关阅读:
    Django-Auth组件
    Django-choice用法
    Django-Cookie和session组件
    Django-DRF
    Django-DRF分页器
    Django-DRF全局异常捕获,响应封装,自动生成接口文档
    Java学习路线一张图足够
    Java基础内容总结
    java基础学习之反射反射的基本概念及使用
    Java基础的方法使用详解
  • 原文地址:https://www.cnblogs.com/fly2wind/p/13376186.html
Copyright © 2011-2022 走看看