【面试题】-java集合框架和IO面试题汇总

免责声明:本篇内容来自网络汇总,部分回答既参考了网上的内容,也结合了自身所学.

目录

1.说说常见的集合有哪些吧?

2.HashMap与HashTable的区别?

3.HashMap的put方法具体流程

4.HashMap的扩容

5.什么是Hash冲突?Hash冲突怎么解决?

6.HashMap为什么不直接使用hashcode()后的值做为table的下标呢?

7.为什么HashMap中String、Integer这样的包装类适合作为Key?

8.如果我想要让自己的Object作为K应该怎么办呢?

9.ConcurrentHashMap和Hashtable的区别?

10.ConcurrentHashMap的实现.

11.Java集合的快速失败机制 “fail-fast”?

12.ArrayList 和 Vector 的区别?

13.ArrayList和LinkedList的区别?

14.HashSet是如何保证数据不可重复的?

15.Array和ArrayList有何区别?

16.BlockingQueue是什么?

17.当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?

18.java中有几种类型的流?

19.字符流和字节流有什么区别?

20.谈谈Java IO里面的常见类,字节流,字符流、接口、实现类、方法阻塞

21.请谈谈BIO,NIO,AIO.

22.递归读取文件夹的文件.


1.说说常见的集合有哪些吧?

Map:HashMap,TreeMap,LinkedHashMap,ConcurrenHashMap,HashTable,Properties.

List:ArrayList,LinkedList,CopyOnWriteArrayList,Stack,Vector.

Set:HashSet,LikedHashSet,TreeSet,CopyOnWriteArraySet.

2.HashMap与HashTable的区别?

HashTable出现于JDK1.0,HashMap是从JDK1.2版本开始出现.

HashTable使用了syncronized是线程安全的,HashMap线程不安全,正因如此,HashMap的性能要更高一些.

HashMap允许k,v均为Null,但HashTable不允许k,v为null.

3.HashMap的put方法具体流程

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
    // 1.如果table为空或者长度为0,即没有元素,那么使用resize()方法扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.计算插入存储的数组索引i,此处计算方法同 1.7 中的indexFor()方法
    // 如果数组为空,即不存在Hash冲突,则直接插入数组
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 3.插入时,如果发生Hash冲突,则依次往下判断
    else {
        HashMap.Node<K,V> e; K k;
        // a.判断table[i]的元素的key是否与需要插入的key一样,若相同则直接用新的value覆盖掉旧的value
        // 判断原则equals() - 所以需要当key的对象重写该方法
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // b.继续判断:需要插入的数据结构是红黑树还是链表
        // 如果是红黑树,则直接在树中插入 or 更新键值对
        else if (p instanceof HashMap.TreeNode)
            e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 如果是链表,则在链表中插入 or 更新键值对
        else {
            // i .遍历table[i],判断key是否已存在:采用equals对比当前遍历结点的key与需要插入数据的key
            //    如果存在相同的,则直接覆盖
            // ii.遍历完毕后任务发现上述情况,则直接在链表尾部插入数据
            //    插入完成后判断链表长度是否 > 8:若是,则把链表转换成红黑树
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 对于i 情况的后续操作:发现key已存在,直接用新value覆盖旧value&返回旧value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 插入成功后,判断实际存在的键值对数量size > 最大容量
    // 如果大于则进行扩容
    if (++size > threshold)
        resize();
    // 插入成功时会调用的方法(默认实现为空)
    afterNodeInsertion(evict);
    return null;
}

用图片表述: 

4.HashMap的扩容

/**
 * 该函数有2中使用情况:1.初始化哈希表;2.当前数组容量过小,需要扩容
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;// 扩容前的数组(当前数组)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;// 扩容前的数组容量(数组长度)
    int oldThr = threshold;// 扩容前数组的阈值
    int newCap, newThr = 0;

    if (oldCap > 0) {
        // 针对情况2:若扩容前的数组容量超过最大值,则不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 针对情况2:若没有超过最大值,就扩容为原来的2倍(左移1位)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }

    // 针对情况1:初始化哈希表(采用指定或者使用默认值的方式)HashMap的初始大小默认为16 每次扩容增大2倍
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 计算新的resize上限
    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) {
        // 把每一个bucket都移动到新的bucket中去
        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)
                    ((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;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

 5.什么是Hash冲突?Hash冲突怎么解决?

Hash冲突是指两个不同的输入值,通过Hash运算得到了相同的散列值值,我们称之为Hash冲突.

在HashMap中,由于存放的是键值对k,v,所以当哈希冲突出现后,可以使用Key值做进一步判断,若key相同,则替换老的value为新value,若Key不同,可以进一步处理,处理方法详见4.

6.HashMap为什么不直接使用hashcode()后的值做为table的下标呢?

因为hashcode()的返回值是Int类型,其范围是2^-32 ~2^32,约40亿个空间,而HashMap的大小在16~2^30之间,可能会导致Hashcode()的返回值不在数组大小范围内,从而导致无法匹配存储位置.

那该如何解决上述问题?

HashMap实现了自己的hash()方法,通过两次扰动运算降低了哈希碰撞的几率,使数据分散更加均匀.

为什么在HashMap中,数组的长度必须为2的次幂?

因为只有当数组长度为2的次幂时,才有h&(length-1)==h%length实现了Key的定位,同时数组长度为2的次幂时可以减少哈希冲突.

7.为什么HashMap中String、Integer这样的包装类适合作为Key?

因为String和Integer这样的包装类都是不可变类,使用final关键字修饰,可以减少hash冲突,符合HashMap内部规范.

8.如果我想要让自己的Object作为K应该怎么办呢?

重写hashcode()和equals()方法,HashMap的put中,需要用到.

9.ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap出现于jdk1.5以后,Hashable出现jdk1.0

ConcurrentHashMap是Hashtable的增强版,Hashtable在同步操作时要锁住整个结构,ConcurrentHashMap只需要锁住部分结构,在粒度上更细,因此ConcurrentHashMap的性能要高于Hashtable.

10.ConcurrentHashMap的实现.

在jdk1.7中

3

ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

在JDK1.8中

2

 JDK1.8中的ConcurrentHashMap是通过数组+链表+红黑树+CAS乐观锁来实现的,具体的实现非常复杂难懂,看了一圈最终放弃了,以后有时间会单独开一篇博客深入研究一番再分享.

11.Java集合的快速失败机制 “fail-fast”?

fail-fast是一种集合类迭代器的错误检测机制,在多个线程对同一个集合进行结构上改变的操作时,会抛出ConcurrentModificationException,可以使用java.util.concurrent包下的集合解决,该包下的集合都是fail-safe的.

12.ArrayList 和 Vector 的区别?

Vector出现于jdk1.0,ArrayList出现于Jdk1.2.

Vector使用了synchronized关键字修饰,是线程安全的,ArrayList线程不安全,在性能上ArrayList要更高一些.

Vector的加载因子是1,在扩容是在数据存满时进行扩容,每次扩容为原容量的(1+1)倍,ArrayList的加载因子是0.5,也就是在数据存到容量的一半时进行扩容,每次扩容为(0.5+1)倍.

13.ArrayList和LinkedList的区别?

ArrayList的数据结构是数组,而LinkedList的数据结构是双向链表,因此在查询上,ArrayList更胜一筹,在插入和删除操作上,LinkedList的性能更胜一筹.

LinkedList由于存储了指向下一个节点和上一个节点的指针,在内存占用上要比ArrayList要多一些.

14.HashSet是如何保证数据不可重复的?

HashSet底层是代理HashMap实现的,只用了HashMap的Key,Value用一个固定的虚拟值代替,在HashMap中Key是唯一的,当put时发现key相同,会把旧的value用新的value代替,并返回旧的value.

public boolean add(E e) {
    return map.put(e, PRESENT)==null;// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
}

因此,在已经存在该key值的情况下,HashSet中调用put方法会始终返回false,所以不会重复插入.

15.Array和ArrayList有何区别?

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

Array需要指定大小,ArrayList不需要指定大小.

16.BlockingQueue是什么?

BlockingQueue是jdk提供的一种队列,主要用于实现生产者-消费者模式,在使用时我们不需要考虑生产者是否有足够空间,消费者是否有可用对象,这些已经被BlockingQueue的实现类中被实现了,BlockingQueue的实现类有ArrayBlockingQueue、LinkedBlockingQueue等...

17.当一个集合被作为参数传递给一个函数时,如何才可以确保函数不能修改它?

在集合被作为参数传递之前,我们可以定义一个只读集合:collections.unmodifiableCollection(Collection c),这将确保改变集合的任何操作都会抛出UnsupportedOperationException.


è¿éåå¾çæè¿°

18.java中有几种类型的流?

字节流和字符流.字节流继承于InputStream和OutputStream,字符流继承于Reader和Writer.

19.字符流和字节流有什么区别?

所有底层设备都只接受字节流,字节流更为通用.字符流是针对文字字符串设计的,可以简化将字节流转成字符串这个步骤.

20.谈谈Java IO里面的常见类,字节流,字符流、接口、实现类、方法阻塞

IO里有非常多的类,常见的如File,InputStream,OutputStream,Reader,Writer.

字节流主要有InputStream,OutputStream和它们的子类:FileInputStream,FileOutputStream,BufferedInputStream,BufferedOutputStream.

字符流主要有Reader,Writer和它们的子类:InputerStreamReader,InputStreamWriter,BufferedReader,BufferedWriter,

都实现了Closeable, Flushable, Appendable这些接口.在调用部分方法时会产生阻塞,直到检测到结束或者异常.比如read()和readLine()方法.

21.请谈谈BIO,NIO,AIO.

BIO同步阻塞IO,一个连接就占用一个线程,如果以前面举过的烧开水为例,有一排热水壶在烧开水,一个线程只能处理一个热水壶,直到这个热水壶的水烧开,该线程才能继续处理下一个热水壶.

NIO同时支持同步阻塞/非阻塞IO,一个请求占用一个线程,以非阻塞IO为例,一个请求可能会被多条连接来完成,在Nio中仅需要一个线程即可应对,该线程对多条连接进行轮询,当有连接的缓冲区数据已满时,线程才会参与进来进行操作,以此来节约线程开销.还以烧开水为例,NIO中线程不再死守着一个水壶直到它烧开了,而是对多个水壶进行轮询,如果有水壶水开了才会过来进行相应操作,这样就可以实现一个线程操作多个水壶了.

AIO异步非阻塞IO,一个有效请求占用一个线程,继续以烧开水为例,AIO相当于在每个水壶上装了一个提示装置,当水烧开时自动通知线程来处理.

22.递归读取文件夹的文件.

/**
 * 
 * 递归读取文件夹的文件
 */
public class ListFileDemo {
    public static void listFile(String path) {
        if (path == null) {
            return;// 因为下面的new File如果path为空,回报异常
        }
        File[] files = new File(path).listFiles();
        if (files == null) {
            return;
        }
        for(File file : files) {
            if (file.isFile()) {
                System.out.println(file.getName());
            } else if (file.isDirectory()) {
                System.out.println("Directory:"+file.getName());
                listFile(file.getPath());
            } else {
                System.out.println("Error");
            }
        }
    }

    public static void main(String[] args) {
        ListFileDemo.listFile("D:\\data");

    }
}

另外还看到一道面试题,把某盘下的某文件夹里的所有子文件夹和子文件递归拷贝到某指定路径下并压缩,有空会来实现一下并补上.

发布了89 篇原创文章 · 获赞 70 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/lovexiaotaozi/article/details/89492131