java集合-面试考点

集合

1.请说明List、Map、Set三个接口存取元素时,各有什么特点?

答:List与Set都是单列元素的集合,它们有一个功共同的父接口Collection。Map是双列集合不能存储重复的key

  • List接口存取的元素是有序的(有索引),可重复的 add,get方法
  • Set接口存取元素是无序的,不可重复的 遍历引入Iterator(因为无序,没有索引不知道取哪一个元素 next,hasNext方法
  • Map集合存取元素采用hash表存储 put,get,keySet、entrySet方法
2.阐述ArrayList、Vector、LinkedList的存储性能和特性

答:存储性能

  • ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
  • LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
  • Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

1

3.请说明Collection 和 Collections的区别。

答:Collection是单列集合是接口下面是Collection的体系结构

1

Collections是一个类,以下是Collections常用的方法

Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

//sort排序参数是list集合
public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
}
//sort的重载,参数是List集合和Comparator接口 需要重写它的compare方法进行排序规则定义
 public static <T> void sort(List<T> list, Comparator<? super T> c) {
        list.sort(c);
}
//shuffle方法是对集合进行随机排序
public static void shuffle(List<?> list) {
        Random rnd = r;
        if (rnd == null)
            r = rnd = new Random(); // harmless race.
        shuffle(list, rnd);
}
public static void shuffle(List<?> list, Random rnd) {
        int size = list.size();
        if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=size; i>1; i--)
                swap(list, i-1, rnd.nextInt(i));
        } else {
            Object[] arr = list.toArray();

            // Shuffle array
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));

            // Dump array back into list
            // instead of using a raw type here, it's possible to capture
            // the wildcard but it will require a call to a supplementary
            // private method
            ListIterator it = list.listIterator();
            for (Object e : arr) {
                it.next();
                it.set(e);
            }
        }
}

Collections用法集合

public class CollectionsTest {

public static void main(String[] args) {
    List<Integer> list = new ArrayList<Integer>();
    list.add(34);
    list.add(55);
    list.add(56);
    list.add(89);
    list.add(12);
    list.add(23);
    list.add(126);
    System.out.println(list);

    //对集合进行排序
    Collections.sort(list);
    System.out.println(list);

    //对集合进行随机排序
    Collections.shuffle(list);
    System.out.println(list);

    //获取集合最大值、最小值
    int max = Collections.max(list);
    int min = Collections.min(list);
    System.out.println("Max:" + max + " Min: " + min);

    List<String> list2 = Arrays.asList("Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday".split(","));
    System.out.println(list2);

    //查找集合指定元素,返回元素所在索引
    //若元素不存在,n表示该元素最有可能存在的位置索引
    int index1 = Collections.binarySearch(list2, "Thursday");
    int index2 = Collections.binarySearch(list2, "TTTTTT");
    System.out.println(index1);
    int n = -index2 - 1;

    //查找子串在集合中首次出现的位置
    List<String> subList = Arrays.asList("Friday,Saturday".split(","));
    int index3 = Collections.indexOfSubList(list2, subList);
    System.out.println(index3);
    int index4 = Collections.lastIndexOfSubList(list2, subList);
    System.out.println(index4);

    //替换集合中指定的元素,若元素存在返回true,否则返回false
    boolean flag = Collections.replaceAll(list2, "Sunday", "tttttt");
    System.out.println(flag);
    System.out.println(list2);

    //反转集合中的元素的顺序
    Collections.reverse(list2);
    System.out.println(list2);

    //集合中的元素向后移动k位置,后面的元素出现在集合开始的位置
    Collections.rotate(list2, 3);
    System.out.println(list2);

    //将集合list3中的元素复制到list2中,并覆盖相应索引位置的元素
    List<String> list3 = Arrays.asList("copy1,copy2,copy3".split(","));
    Collections.copy(list2, list3);
    System.out.println(list2);

    //交换集合中指定元素的位置
    Collections.swap(list2, 0, 3);
    System.out.println(list2);

    //替换集合中的所有元素,用对象object
    Collections.fill(list2, "替换");
    System.out.println(list2);

    //生成一个指定大小与内容的集合
    List<String> list4 = Collections.nCopies(5, "哈哈");
    System.out.println(list4);

    //为集合生成一个Enumeration
    List<String> list5 = Arrays.asList("I love my country!".split(" "));
    System.out.println(list5);
    Enumeration<String> e = Collections.enumeration(list5);
    while (e.hasMoreElements()) {
        System.out.println(e.nextElement());
    	}
	}
}
4.请你说明HashMap和Hashtable的区别?

答:HashMap和Hashtable从线程安全与否、是否能存储空值、以及性能去比较

1

适用场景:

HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允许空键值,而HashTable不允许。

5.请说说快速失败(fail-fast)和安全失败(fail-safe)的区别?

答:快速失败与安全失败像是在比较java.util包和java.util.concurren下的集合安全性问题。显然java.util是不安全的会快速失败,而java.util.concurren下的工具类则是安全的会安全失败。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。此外Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。

快速失败例子: 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

public class FastFailTest {

    public static void main(String[] args) {
        //在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

        ArrayList<String> arrayList=new ArrayList<>();

        arrayList.add("one");
        arrayList.add("two");
        arrayList.add("three");
        arrayList.add("four");
        arrayList.add("five");

        for (String s : arrayList) {
            arrayList.remove("one");
            System.out.println(s);
        }

    }

}
//结果
Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1012)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:966)
	at Java集合.FastFailTest.main(FastFailTest.java:20)
一:快速失败(fail—fast)

          在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

          原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

      注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

      场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)。

    二:安全失败(fail—safe)

      采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

      原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。

      缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

          场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
6.请你说说Iterator和ListIterator的区别?

答:Collections的静态方法能够创建Iterator和ListIterator对象 然后ListIterator继承Iterator接口

Iterator<Object> objectIterator = Collections.emptyIterator();
ListIterator<Object> objectListIterator = Collections.emptyListIterator();

public interface ListIterator<E> extends Iterator<E> 
  • 遍历范围:Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。

  • public interface List<E> extends Collection<E> 
         ListIterator<E> listIterator();
    	 Iterator<E> iterator();
    
    public interface Set<E> extends Collection<E> //set集合就只有iterator方法
         Iterator<E> iterator();
    
  • 遍历顺序:Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。

  • 后者优势::ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引…

引申一下,为什么每一个集合都具有遍历功能也就是都由iterator方法,以为Collection接口是实现了Iterable接口的,所以所有的集合都可以重写iterator方法定义自己的遍历规则。

7.请你说明一下ConcurrentHashMap的原理?

答:首先为什么要引入ConcurrentHashMap,因为hashmap是线程不安全的,其次虽然Hashtable是线程安全的但是因为他共用同一把锁,就会导致效率慢,所有我们考虑是否存在多把锁同时存在 同时对线程并发进行操作,每一部分的数据都用一把锁来锁住,从而在有效率的前提下又能做到线程安全。于是就引入了ConcurrentHashMap。

ConcurrentHashMap最重要的就是分段锁技术。当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。

  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁
  • 在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。
  • ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。其中Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。
  • 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
  • 在ConcurrentHashMap 中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry 对象链接成一个链表。由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。

1

JDK1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全。数据结构采用:数组+链表+红黑树。

8.请解释一下TreeMap?

答:因为TreeMap是基于红黑树实现的 排序规则是通过Comparator进行排序,具体取决于使用的构造方法。所以问什么是TreeMap也就是问什么是红黑树。所以TreeMap特性为:

  • 根节点是黑色
  • 每个节点都只能是红色或者黑色
  • 每个叶节点(NIL节点,空节点)是黑色的。
  • 如果一个节点是红色的,则它两个子节点都是黑色的,也就是说在一条路径上不能出现两个红色的节点。
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
9.请你说明concurrenthashmap有什么优势以及1.7和1.8区别?

答:优势也就是问相较于HashMap和HashTable的优势。主要体现在线程安全以及效率高。问1.7和1.8区别主要体现在锁机制和计数上

ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表

  • 1.7采用的是分段锁机制 采用的是链表时间复杂度为O(n)
  • 1.8采用的是CAS+Synchronized保证并发线程安全 采用的是红黑树时间复杂度为O(log(n))
  • jdk1.7中采用Segment + HashEntry的方式进行实现的,lock加在Segment上面。1.7size计算是先采用不加锁的方式,连续计算元素的个数,最多计算3次:1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
  • 1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数;
private transient volatile long baseCount;

private final void addCount(long x, int check) {
        CounterCell[] cs; long b, s;
        if ((cs = counterCells) != null ||
            !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell c; long v; int m;
            boolean uncontended = true;
            if (cs == null || (m = cs.length - 1) < 0 ||
                (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
                if (sc < 0) {
                    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                        (nt = nextTable) == null || transferIndex <= 0)
                        break;
                    if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }
10.请你解释HashMap的容量为什么是2的n次幂?

答:

  • 第一,因为2的次幂能够保证索引值肯定在HashMap的容量大小范围内均匀分布于每一个桶中。(tab[i = (n - 1) & hash]与运算决定的);

  • 第二为什么是2的次幂是因为HashMap的tableSizeFor方法做了处理,能保证n永远都是2的n次幂。

     static final int tableSizeFor(int cap) {
            // cap-1后,n的二进制最右一位肯定和cap的最右一位不同,即一个为0,一个为1,例如cap=17(00010001),n=cap-1=16(00010000)
            int n = cap - 1;
            // n = (00010000 | 00001000) = 00011000
            n |= n >>> 1;
            // n = (00011000 | 00000110) = 00011110
            n |= n >>> 2;
            // n = (00011110 | 00000001) = 00011111
            n |= n >>> 4;
            // n = (00011111 | 00000000) = 00011111
            n |= n >>> 8;
            // n = (00011111 | 00000000) = 00011111
            n |= n >>> 16;
            // n = 00011111 = 31
            // n = 31 + 1 = 32, 即最终的cap = 32 = 2 的 (n=5)次方
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? AXIMUM_CAPACITY : n + 1;
        }
    
  • 第三:HashMap中的hash也做了比较特别的处理,(h = key.hashCode()) ^ (h >>> 16)实质上是把一个数的低16位与他的高16位做异或运算,因为在前面 (n - 1) & hash 的计算中,hash变量只有末x位会参与到运算。使高16位也参与到hash的运算能减少冲突

11.hashmap什么时候扩容?

答:分为两种情况

  • 第一就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。

  • 第二个是有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(虽然hash冲突,但是这时元素个数小于阈值12,并没有同时满足扩容的两个条件。所以不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,也没有同时满足扩容的两个条件,所以也不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。

12.请你解释一下hashMap具体如何实现的?

答:从底层原理,从他的几个重要方法get、put

  • 底层原理:hashmap采用的是数组加链表或者数组加红黑树进行存储
  • jdk1.7下put时在多线程情况下,会形成环从而导致死循环。
  • HashMap在JDK1.8的版本中引入了红黑树结构做优化,当链表元素个数大于等于8时,链表转换成树结构;若桶中链表元素个数小于等于6时,树结构还原成链表。
  • 因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。至于为什么要有7,是因为防止频繁的put‘和get操作,使得效率低下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值