02Java容器模块面试题

1、Java容器有哪些

Java容器分别为CollectionMap两大类,其下右分为很多子类

2、Collection和Collections有什么区别

Collection是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如ListSet

Collections是一个包装类,包含了很多静态方法,不能被实例化,就像工具类

3、List、Set、Map之间的区别

List、Set、Map之间的区别主要体现在两个方面:元素是否有序是否允许重复元素

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Lpzb5F1-1581692496401)(images/07.png)]

4、HashMap与HashTable的区别

  • HashMap是继承自AbstractMap类,而HashTable继承自Dictionary类。不过它们都桶实现了map,clonaeable,Serializable(可序列化)这三个接口

  • HashMapkey可以是null;HashMapkey可以是null

  • 线程安全性:HashMap的方法没有使用sychronized关键字修饰;都是非线程安全的;

    HashTable的方法都是被synchronized修饰的,是线程安全的

    可以使用Collections.synchronized(hashMap)或者使用ConcurrentHashMMap

    • 初始容量大小和每次扩容容量大小的不同:HashTable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1

      HashMap的默认初始化大小为16,之后每次扩充,容量变为原来的2倍

    • 计算hash值的方法不同:HashTable直接使用对象的hashCode;HashMap是根据对象的地址或者字符串或者数字计算出来的int类型的数值

            int hash = key.hashCode(); //HashTable
    
    	  
    	    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//HashMap
    
    

5、说说如何决定使用HashMap还是TreeMap

Key有序遍历选择TreeMap

6、说一下HashMap的实现原理

HashMap基于hash算法实现的,我们通过put(key,value)存储,get(key)来获取。

当传入keyHashMap会根据key.hashCode()计算出hash值,根据hash值将value保存在bucket

当计算出的hash值相同时,我们称为hash冲突,HashMap的做法使用链表红黑树存储想用值的valye

hash冲突个数少时,使用链表否则使用红黑树

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }



7、说一下HashSet的实现原理?

HashSet是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet的实现比较简单,相关HashSet的操作,基本上都是底层直接调用HashMap的相关方法来完成

8、ArrayList和LinkedList的区别是什么?

9、如何实现数组和List之间的转换

数组转List:使用Arrays.asList(array)进行转换

List转换数组:使用List自带的toArray方法

10、ArrayList和Vector的区别是什么

线程安全:Vector使用了synchronized来实现线程同步,是线程安全的,而ArrayList是非线程安全的

性能:ArrayList在性能方面优于Vector

扩容:ArrayListVector都会根据实际情况需要动态的调整容量,只不过在Vector扩容每次会增加1倍,而ArrayList只会增加50%

11、Array和ArrayList有喝区别

Array可以存储基本数据类型和对象,ArrayList只能存储对象

Array是指定固定大小的,而ArrayList大小是自动扩展的

Array内置方法没有ArrayList多,比如addAllremoveAll,iteration等方法只有ArrayList

12、在Queue中poll()和remove()有什么区别//element()和peek()的区别

相同点:都是返回第一个元素,并在队列中删除返回的对象

不同点:如果没有元素remove()会直接抛出NoSuchElementException异常,而poll会返回异常

13、哪些集合是线程安全的?

VectorHashTableStack都是线程安全的,而像HashMap则是线程不安全的;

不过在JDK 1.5以后随着java.util.concurrent并发包的出现,他们也有了自己对应的线程安全类,比如HashMap对应的线程安全类就是ConcurrentHashMap

14、迭代器Iterator是什么??

Iterator接口提供遍历任何Collection的接口;

我们可以从Collection中使用迭代器方法来获取迭代器实例;

迭代器取代了Java集合框架中的Enumeration迭代器运行调用者在迭代过程中移除元素

15、Iterator和ListIterator有什么区别?

public interface ListIterator<E> extends Iterator<E> {

  • Iterator可以遍历SetList集合;而ListIterator只能遍历List
  • Iterator只能单向遍历,而ListIterator可以双向遍历(向前/后遍历)
  • ListIteratorIterator接口继承,然后添加了一些额外的功能,比如添加一个元素,替换一个元素,获取前面或后面元素的索引位置

16、怎么确保一个集合不被修改,

可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出UnsupportedOperationException异常****

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ZaEvHrR-1581692496402)(images/08.png)]

17、怎样确保一个集合成为线程安全的集合

可以使用Collections.synchronizedList(Collection c)方法来创键一个线程安全的集合

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PnYdwniR-1581692496403)(images/09.png)]

18、HashMap原理,内部数据结构?

HashMap的内部存储结构其实是对数组+链表的结合(1.8后加入红黑树,O(n)到O(logn)的时间开销)。

当实例化一个HashMap的时候,系统会创建一个长度为CapacityEntry数组,这个长度称为容量(Capacity),这个数组可以存放元素的位置我们诚挚为“桶(bucket)"",每个bucket有自己的索引,系统可以根据索引快速的查找bucket中的元素,每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带有一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一条Entry链

19、讲一下HashMap中的put方法?(如何存储,发生Hash冲突怎么办?)

  • 对key求hash值然后计算下标
  • 如果没有哈希碰撞则直接放入槽中
  • 如果碰撞以后以链表的形式连接到后面
  • 如果链表长度超过阈值(默认阈值为8),就把链表转为红黑树
  • 如果节点已存在旧的值,就替换
  • 如果槽满了(容量*加载因子),就需要resize

20、HashMap中哈希函数是怎么样实现的?还有哪些hash实现方法?

static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

高16bit不变,低16bit和高16位做异或获得hash值,然后(n-1)&hash获得下标

直接地址发:直接以关键字k或者k加上某个常数(k+c)作为hash地址

数字分析法:提取关键字中取值比较均匀的数字作为哈希地址

21、为什么HashMap中的&位必须为奇数(为什么数组容量为2的幂次)?

长度是16或者其他2的幂次,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HasCode后几位的值。只要输入的HashCode本身分布均匀,HASH算法的结果就是均匀的。然后使用(length-1)&hashCde相当于求出余数,比%快,确定在桶中的位置tab[i=(n-1)&hash]&

22、如何解决hash冲突(什么叫hash冲突)

两个key值计算的hash值一样 -----> 发生hash冲突如何解决

Hash冲突:hASHcODE值一样,但key不一样,就导致哦了冲突(如果Key一样,就是相同的值覆盖了)

HashMap的冲突处理是用的是链表地址发,将所有哈希地址相同的记录都连接在同一个链表中(同一个桶的链表中)。

当然:还有其他解决hash冲突的方法,例如:开放地址(外加增量d)、再哈希(hash函数不同)、链地址发、建立公共溢出区

23、HashMap线程并发安全性问题

HashMap在并发时可能常出现的问题主要是两方面?

  • 如果在多线程同时使用put方法添加元素,而假设正好存在两个put的key发生了碰撞,那么根据hashMao的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程put的数据被覆盖**

  • 如果多个线程同时检测到元素个数超过数组大小* loadFactor,这样就会发生多个线程同时对Node数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋值给table,也就是说其他线程都会全丢失

    如何实现一个并发安全的HashMap?参考ConncurrentHashMap底层原理

25、HashMap的扩容优化,是否会重算hash值?

  • jdk1.7扩容就是 当size>threesold=length*loadFactor时,将HashmAP的数组扩大两倍,所有元素再次进行Hash放入到新的HashMap中
  • JDK1.8扩容 当位桶中为链表时, 它会将原来的链表同过计算e.hash&oldCap==0分成两条链表,再将两条链表散列到新数组的不同位置上
  1. 1.7扩容后会重新计算hash值,这个key值(key%新的长度)在新桶的什么位置,这样就会有大量的重复计算

  2. 1.8中进行优化。桶中的链表不需要向JDK1.7中实现的那样重新计算hash值,只需要将原来的hash值新增的那个bithi1还是0.是0的话索引没变,是1的话索引编程元索引+oldCap

jdk1.7扩容代码

 //调用resize方法对table进行扩展,扩展策略是乘2
            resize(2 * table.length);

//resize该方法///
    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方法将原来的键值对移植过来
        transfer(newTable, initHashSeedAsNeeded(newCapacity));

        table = newTable;
        //计算新的阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


//这段代码遍历原来的每个键值对,计算新位置,并保存到新位置
//注意对源hAshMap中的每个元素再次进行hash,然后放入x
    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;
            }
        }



JDK1.8扩容

  final Node<K,V>[] resize() {
        Node<K, V>[] oldTab = table;//首次初始化后table为Null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//默认构造器的情况下为0
        int newCap, newThr = 0;
        if (oldCap > 0) {//table扩容过
            //当前table容量大于最大值得时候返回当前table
             if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                //table的容量乘以2,threshold的值也乘以2           
                newThr = oldThr << 1; // double threshold
        } else if (oldThr > 0) // initial capacity was placed in threshold
        //使用带有初始容量的构造器时,table容量为初始化得到的threshold
        newCap = oldThr;
        else{  //默认构造器下进行扩容  
            // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //使用带有初始容量的构造器在此处进行扩容
            float ft = (float) newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                    (int) ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes", "unchecked"})
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;
      if (oldTab != null) {
            //对新扩容后的table进行赋值,条件中的代码删减
        }
        return newTab;
    }

///
if (oldTab != null) {
            //对新扩容后的table进行赋值,条件中的代码删减

            //oldCap为原数组的长度
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //将原数组位置置为空,应该是便于垃圾回收吧
                    oldTab[j] = null;
                    //如果数组中只存放了一个元素,即不是红黑树
                    //结构也不是链表结构
                    if (e.next == null)
                        //直接通过&算法找到数组中的位置
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //如果是红黑树结构,就调用split
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //如果数组中存放的是链表,会将原来的链分成两条链表
                        Node<K,V> loHead =null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //通过计算e.hash&oldCap==0构造一条链
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            } //通过e.hash&oldCap!=0构造另外一条链
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //遍历结束以后,将tail指针指向null
                        //e.hash&oldCap==0构造而来的链的位置不变
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //e.hash&oldCap!=0构造而来的链的位置在数
                        //组j+oldCap位置处
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }





        }
————————————————
版权声明:本文为CSDN博主「alex-zhou96」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ZHOUJIAN_TANK/article/details/104463188


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值