java面试基础(一)容器

一、List

ArrayList

1、 ArrayList中的元素由底层数组承载
2、 如果使用午餐构造方法,那么默认调用this(10),即,若不指定list长度,默认为10

   public ArrayList(int initialCapacity) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this(10);
    }

3、ArrayList参数为Collection的构造方法底层调用的是Arrays.copyOf=>System.arraycopy方法
4、ArrayList通过size成员变量来记录元素个数
5、indexOf方法需要注意,如果传入的参数为null,那么会返回数组中第一个为null的索引,如果不存在,则返回-1

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

6、数组长度扩容,如果需要扩容,则默认扩容为原来的1.5倍

   private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //这里就是扩容1.5倍的地方
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

7、每次add或者remove,底层都会调用System.ayyayCopy方法,这也就是为什么ArrayList add和remove方法效率低下的原因
8、如果需要在循环的时候add或者remove,记住使用Iterator
至于为什么??以后再去深究
9、ArrayList没有加锁,并发时,会存在问题

LinkedList

1、底层由链表实现,维护了头指针和尾指针
2、add方法会将新Node加到链表的末尾

    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        //将新节点加到链表的结尾
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

3、indexOf 参数若是null,返回链表中第一个为null的索引,若不存在,返回-1
4、toArray使用的是new Object[size]结合for循环完成的
5、未使用synchronize,线程不安全。

vector

1、ArrayList的线程安全版,通过对方法加锁实现
2、扩容时的策略与ArrayList略有不同,vector维护了一个capacityIncrement变量用来控制每次扩容时扩容量,若为0,则默认扩充为原来的2倍,ArrayList选择默认扩容为原来的1.5倍

stack

继承了Vector,使用数组构造的堆结构

2、Set

HashSet

底层维护一个HashMap,只使用HashMap的key部分,value部分置为Object对象

TreeSet

底层维护一个TreeMap

3、Map

HashMap

1、底层由Entry数组和链表组成
2、实例化HashMap需要指定初始容量(默认16)和初始加载因子(默认0.75)
这两个参数是影响HashMap性能的重要参数,其中容量表示哈希表中桶的数量,初始容量是创建哈希表时的容量。
加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;
如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为0.75,一般情况下我们是无需修改的。
3、如果put方法的键值对中,key为null,对索引为0的链表进行for循环,如果没找到则将这个元素添加到talbe[0]链表的表头。
4、put方法(key非null)过程
计算key的hash值(通过扰动函数保留高位信息,避免h^(length-1)出现大概率碰撞)—>通过indexFor方法计算索引—>查找相关索引对应的链表是否含有key,若已存在,则覆盖value,若不存在,则将新键值对置于链表第一个元素之后
5、put扩容
若出现 size >= threshold的情况,默认会将容量扩容为原来的两倍;

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

扩容结束后,需要重新计算hash,并将键值对置于新的Entry数组的相应位置上。

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

6、解决hash碰撞常用的方法:链地址法(即HashMap所采用)、开放地址法(在原有hash结果的基础上+d,然后再取余)、再hash(即使用多个hash方法)

ConcurrentHashMap

1、ConcurrentHashMap没有锁住整个hash表,而是使用分段锁。其内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
2、有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁
3、对于一个key,需要经过三次hash操作,才能最终定位这个元素的位置,这三次hash分别为:

  • 对于一个key,先进行一次hash操作,得到hash值h1,也即h1 = hash1(key);
  • 将得到的h1的高几位进行第二次hash,得到hash值h2,也即h2 = hash2(h1高几位),通过h2能够确定该元素的放在哪个Segment;
  • 将得到的h1进行第三次hash,得到hash值h3,也即h3 = hash3(h1),通过h3能够确定该元素放置在哪个HashEntry。

4、concurrencyLevel、sshift、ssize、segmentShift 、segmentMask

  • concurrencyLevel 并发级别
    确定segment[]长度(ssize,ssize是大于等于concurrencyLevel的第一个2的n次方数)
  • sshift 即2的sshift次方 = ssize
  • segmentShift = 32-sshift
  • segmentMask = ssize - 1
    5、获取segment数组索引
    int j = (hash >>> segmentShift) & segmentMask;

6、put方法加锁,get方法不加锁(通过volatile关键字控制HashEntry中value的值来保证线程安全)
7、size、containsValue方法加锁机制:前面三次不对segment加锁,若出现modCount(s1) != modCount(s2) !=modCount(s3),则对所有segment加锁,来获取size或者进行其他操作
8、ConcurrentHashMap中的key和value值都不能为null

LinkedHashMap

1、LinkedHashMap可以看做是HashMap和LinkedList的融合体,它在自己实现的Entry中新增了befor和after两个成员,用来维护双向链表
2、可以用来实现LRU缓存
3、可以记录访问和插入的顺序(默认记录插入顺序)

HashTable

1、HashTable不支持null 键(若为null,会自动抛异常)
2、HashTable默认扩容为原来的2n+1,这是因为其与HashMap策略不同,HashTable选择素数,以降低碰撞率,但是这样会导致,求Entry[]索引时比HashTable效率低(因为除法运算速度远远小于位运算速度)
3、使用了synchronized保证了线程安全

TreeMap

1、TreeMap基于红黑树实现。红黑树是一种二叉搜索树,红黑树与AVL(平衡二叉树)相比,其旋转次数较少。
2、红黑树的时间复杂度为O(log(n)),因为给定n个节点,红黑树的高度最大为2(log(n+1))=>log(n)

猜你喜欢

转载自blog.csdn.net/qq_21955179/article/details/80842150
今日推荐