Java集合类面试总结:

 

1、String、StringBuffer、StringBuilder 的区别是什么?String为什么是不可变的?

         ①String是字符串常量,StringBuffer和StringBuilder都是字符串变量。后两者的字符内容可变,而因为在JDK中String类被声明为一个final类,创建后内容不可变。②StringBuffer是线程安全的,而StringBuilder是非线程安全的。

         ps:线程安全会带来额外的系统开销,所以StringBuilder的效率比StringBuffer高。如果对系统中的线程是否安全很掌握,可用StringBuffer,在线程不安全处加上关键字Synchronize。

        String为什么是不可变的:什么是不可变的?一个对象在创建完成后,不能再改变它的状态。(即不能改变对象内的成员变量)---String的属性被final修饰、私有的并且没有提供修改方法。 (主要字段是char数组,虽然被final修饰但数组是可变的,私有保证了不被修改但还是可以通过反射来改变String,但一般不这样做)

       ---为什么设计成不可变的?字符串常量池的需要,提升效率和减少内存分配;安全性考虑,防止被意外修改;作为hashmap、hashtable等hash类型的数据key的必要。

 

2.set接口:

         无序,不能重复。没有get方法,想要获取元素,需要使用迭代器来实现,常用实现类是HashSet。

        HashSet:实现Set接口,底层是一个hashmap,value存的是空的object;不能保证元素的排列顺序,顺序有可能发生变化,不是同步的,集合元素可以是null,但只能放入一个null

         LinkedHashSet:继承自HashSet,底层实现是LinkedHashMap。维护了一个双链表来记录插入的顺序,使元素按照插入顺序保存。基本方法的复杂度为O(1)。

         TreeSet:是SortedSet接口的唯一实现类,可以确保集合元素处于排序状态,底层实现是TreeMap。

 

3. HashSet与HashMap区别:

       HashSet底层是一个hashmap,value存的是空的object;

      HashMap实现了Map接口 ,HashSet实现了Set接口;

      HashMap储存键值对 ,HashSet仅仅存储对象;

      HashMap使用put()方法将元素放入map中 ,HashSet使用add()方法将元素放入set中;

      HashMap中使用键对象来计算hashcode值 ,HashSet使用成员对象来计算hashcode值;

      HashMap比较快,因为是使用唯一的键来获取对象 ,HashSet较HashMap来说比较慢;

 

4、Vector,ArrayList, LinkedList的区别是什么?

        List:有序的,可以重复的。

        ①Vector、ArrayList都是以类似数组的形式存储在内存中,LinkedList则以链表的形式进行存储。②List中的元素有序、允许有重复的元素,Set中的元素无序、不允许有重复元素。③Vector线程同步,ArrayList、LinkedList线程不同步。④LinkedList适合指定位置插入、删除操作,不适合查找;ArrayList、Vector适合查找,不适合指定位置的插入、删除操作。⑤ArrayList默认大小为10,在元素填满容器时会按1.5倍扩容【因为位运算】(扩容后的大小= 原始大小+原始大小/2 + 1),LinkedList维护的是链表,没有扩容机制,而Vector则是100%,因此ArrayList更节省空间。

/*ArrayLst的扩容操作*/
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);//默认的相当于1.5倍
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;//如果还是不够,就把需要的值赋值
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);//判断newCapacity大容量情况,考虑minCapacity
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }

  

//Vector扩容操作
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); //扩大1倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

  

        【LinkedList使用普通for循环遍历慢的原因,LinkedList在get任何一个位置的数据的时候,都会把前面的数据走一遍。遍历的时间复杂度为O(N2)】

        【Vector为什么是线程安全的?--->Vector在一些必要的方法上都加了 synchronized关键字,Vector中有一个 elements()方法用来返回一个 Enumeration,以匿名内部类的方式实现的:】

public Enumeration elements() {
        return new Enumeration() {
            int count = 0;
            public boolean hasMoreElements() {
                return count < elementCount;
            }
            public E nextElement() {
                synchronized (Vector.this) {
                    if (count < elementCount) {
                        return elementData(count++);
                    }
                }
                throw new NoSuchElementException("Vector Enumeration");
            }
        };
    }

       【ArrayList会自动扩容,但不会自动缩容,如何缩容? ArrayList里面有一个方法可以缩小容积,trimToSize() 】

/**
     * Trims the capacity of this ArrayList instance to be the
     * list's current size.  An application can use this operation to minimize
     * the storage of an ArrayList instance.
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = Arrays.copyOf(elementData, size);
        }
    }

 

5.HashTable, HashMap,TreeMap区别?

        Map是Java.util包中的另一个接口,属于集合类,键值对方式存储,键唯一,值不唯一,对map集合遍历时先得到键的set集合,对set集合遍历,得到相应的值。常用实现类:HashMap、Hashtable、LinkedHashMap、TreeMap

        哈希表的实现,存储方式:哈希表,也叫散列表, 是根据关键码值(Key value)而直接进行访问的数据结构。具体通过把Key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

       哈希冲突:当关键字集合很大时,关键字值不同的元素可能会映像到哈希表的同一地址上。解决冲突的方法有链地址法(将所有哈希地址为i的元素构成一个同义词的单链表,查找、插入、删除都在同义链表中进行)、再哈希法(同时构造多个不同的哈希函数)、开放定址法(再散列法,当管关键字key的哈希地址p出现冲突时,以此p为基础,产生另一个哈希地址p1,如果仍然有冲突,则重复寻找直到找出一个不冲突的哈希地址)。

       HashTable、HashMap区别:①HashTable线程安全,HashMap非线程同步。②HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值。③HashTable使用Enumeration,HashMap使用Iterator。④HashTable中hash数组默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小是16,增长方式一定是2的指数倍。

     TreeMap:TreeMap是一个有序的key-value集合,基于红黑树实现,继承于AbstractMap,实现了Cloneable、Serializable接口,可以被克隆,支持序列化;默认升序排列,可以通过重写compare方法来改变,线程不安全。

      HashMap如何工作:HashMap基于哈希原理,通过put()和get()方法储存和获取对象。调用put()方法时,首先判断key是否为空,如果为空,放入到table[0]的位置(遍历table[0]的链表的每个节点Entry,如果发现其中存在节点Entry的key为null,就替换新的value,然后返回旧的value,如果没发现key等于null的节点Entry,就增加新的节点),然后调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来储存值对象。【根据hash值和数组长度算出索引值 h & (length-1);,确保算出来的索引是在数组大小范围内,不会超出;比使用取模方式效率更高】,如果该位置key存在,替换新的value值,如果不存在,插入到头结点中。这里会涉及到扩容问题,容量扩大两倍。使用LinkedList来解决碰撞问题,当发生碰撞了,对象将会储存在LinkedList 的下一个节点中。 HashMap在每个LinkedList节点中储存键值对对象。获取对象时,调用get()方法,通过计算键对象的hash值,找到bucket位置,通过键对象的equals()方法找到正确的键值对,然后返回值对象。

      HashMap,TreeMap区别:①TreeMap,SortMap接口,基于红黑树,HashMap基于哈希散列表实现;②TreeMap 默认按键的升序排序,HashMap随机存储;③TreeMap键、值都不能为null,HashMap只允许键、值均为null;④两者都是线程不安全;5.HashMap效率要比TreeMap高。

      HashMap的实现机制:①维护一个每个元素是一个链表的数组,而且链表中的每个节点是一个Entry[]键值对的数据结构。②“链表散列”的数据结构,实现了数组+链表的特性,底层结构是数组,数组中的每一项是一条链表;查找快,插入删除也快。 ③对于每个key,他对应的数组索引下标是 int i = hash(key.hashcode)&(len-1); ④每个新加入的节点放在链表首,然后该新加入的节点指向原链表首。

      源码分析,参考:https://www.cnblogs.com/ITtangtang/p/3948406.html#a6

      HashMap的内部结构是hash+链表,JDK1.8 以后当超过8 时转化为红黑树,当小于6 时重新变为链表。当插入新元素时,对于红黑树的判断如下:判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向下面;遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

public V put(K key, V value) {
     // 若“key为null”,则将该键值对添加到table[0]中。
         if (key == null) 
            return putForNullKey(value);
     // 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
         int hash = hash(key.hashCode());
     //搜索指定hash值在对应table中的索引
         int i = indexFor(hash, table.length);
     // 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
         for (Entry e = table[i]; e != null; e = e.next) { 
             Object k;
              if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值
                  V oldValue = e.value;
                 e.value = value;
                 e.recordAccess(this);
                 return oldValue;
              }
         }
     //修改次数+1
         modCount++;
     //将key-value添加到table[i]处
     addEntry(hash, key, value, i);
     return null;
}

  HashMap 包含如下几个构造器:

   HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。

   HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

   HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。

   initialCapacity:HashMap的最大容量,即为底层数组的长度。

   loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。

   【hashmap 容量为什么是2 的幂次? --->方便取模和扩容,这样可以直接进行位操作计算。(首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。)】

      使用的是2次幂的扩展(指长度扩为原来2倍),所以,经过rehash之后,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。1.8中不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”  ( JDK1.8)

Java集合类面试总结:_第1张图片

     【 当两个不同的键对象的hashcode相同时会发生什么?--->它们会储存在同一个bucket位置的LinkedList中。键对象的equals()方法用来找到键值对。】

     【如果两个键的hashcode相同,你如何获取值对象?---> 调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象。】

     【如果HashMap的大小超过了负载因子定义的容量,怎么办?----->HashMap默认的初始容量是16,负荷系数是0.75(why?---如果负载因子越大,对空间的利用更充分,后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费),阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂。】

      【重新调整HashMap大小存在什么问题?--->resize后的HashMap容量是容量的两倍;当多线程的情况下,当两个线程同时开始扩容时,有可能出现环形链表,陷入死循环。(HashMap在高并发下如果没有处理线程安全会有陷入死循环这样的安全隐患)】

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];
         transfer(newTable);//用来将原先table的元素全部移到newTable里面
         table = newTable;  //再将newTable赋值给table
         threshold = (int)(newCapacity * loadFactor);//重新计算临界值
     }

    【可以使用自定义的对象作为键吗?---可以使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。】

    【可以使用CocurrentHashMap代替HashTable吗?----ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性】

     【ConcurrentHashMap多线程下比HashTable效率更高-- HashTable使用一把锁处理并发问题,当有多个线程访问时会导致阻塞;ConcurrentHashMap使用分段,每个部分分配一把锁,这样就可以支持多线程访问。】

    【一致性hash算法--将整个哈希值空间组织成一个虚拟的圆环,解决了增减服务器导致的数据散列问题、分布式环境下负载均衡问题。】

      【为什么重写hashcode、equals方法?--Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也需要改写。】

ConcurrentHashMap源码分析:

     JDK1.7版本中Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;HashEntry 用来封装映射表的键 / 值对;底层采用 数组+链表 的存储结构。

    JDK1.8版本中已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全, Node用来存储键值对,底层采用 数组+链表+红黑树 的存储结构。

 

4.fail-fast:

        java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。

        例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

        这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。

 

5.Arrays.sort()与Collections.sort():

       Arrays.sort() 算法是一个经过调优的快速排序,此算法在很多数据集上提供N*log(N)的性能,这导致其他快速排序会降低二次型性能。

       Collections.sort()算法是一个经过修改的合并排序算法。此算法可提供保证的N*log(N)的性能,此实现将指定列表转储到一个数组中,然后再对数组进行排序,在重置数组中相应位置处每个元素的列表上进行迭代。这避免了由于试图原地对链接列表进行排序而产生的n2 log(n)性能。

 

 

你可能感兴趣的:(集合类)