java集合(容器)Map、Collection重点难点---2020迎战java集合

目录:

1. 什么是集合(容器)?
  • 为了满足常规的编程需要,我们要求能在任何时候,任何地点创建任意数量的对象,而这些对象用什么来容纳呢?这就产生了集合。Java集合类存放在java.util包中,是一个用来存放对象的容器。集合也叫容器,是用来存储对象的容器。
  • 1、集合只能存放对象。比如你存入一个int型数据 1 放入集合中,其实它是自动转换成Integer类后存入的,Java中每一种基本数据类型都有对应的引用类型。
  • 2、集合存放的都是对象的引用,而非对象本身。所以我们称集合中的对象就是集合中对象的引用。对象本身还是放在堆内存中。
  • 3、集合可以存放不同类型,不限数量的数据类型。

2. 集合(类)的特点有哪些?
  • 1、集合类这种框架是高性能的。对基本类集(动态数组,链接表,树和散列表)的实现是高效率的。一般人很少去改动这些已经很成熟并且高效的APl;
  • 2、集合类允许不同类型的集合以相同的方式和高度互操作方式工作;
  • 3、集合类容易扩展和修改,程序员可以很容易地稍加改造就能满足自己的数据结构需求。

3. 为什么要使用集合(类)?/集合类有哪些优势?
  • 1、降低编程难度:在编程中会经常需要链表、向量等集合类,如果自己动手写代码实现这些类,需要花费较多的时间和精力。调用Java中提供的这些接口和类,可以很容易的处理数据。
  • 2、提升程序的运行速度和质量:Java提供的集合类具有较高的质量,运行时速度也较快。使用这些集合类提供的数据结构,程序员可以从“重复造轮子”中解脱出来,将精力专注于提升程序的质量和性能。
  • 3、无需再学习新的APl:借助泛型,只要了解了这些类的使用方法,就可以将它们应用到很多数据类型中。如果知道了LinkedList的使用方法,也会知道LinkedList怎么用,则无需为每一种数据类型学习不同的API。
  • 4、增加代码重用性:也是借助泛型,就算对集合类中的元素类型进行了修改,集合类相关的代码也几乎不用修改。

4. 集合和数组的区别?
  • 1、数组是固定长度的;集合可变长度的。
  • 2、数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
  • 3、数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

5. java中有哪些常用的集合(容器)?
  • Java 容器分为 Collection 和 Map 两大类,各自都有很多子类。
    在这里插入图片描述
  • AbstractCollection : 对Collection接口的最小化抽象实现;
  • List : 有序集合;
  • AbstractList :有序集合的最小化抽象实现 ;
  • ArrayList :基于数组实现的有序集合;
  • LinkedList : 基于链表实现的有序集合;
  • Vector : 矢量队列;
  • Stack : 栈,先进后出;
  • Set :不重复集合;
  • AbstractSet :不重复集合的最小化抽象实现;
  • HashSet :基于hash实现的不重复集合,无序;
  • LinkedHashSet : 基于hash实现的不重复集合,有序;
  • SortedSet :可排序不重复集合;
  • NavigableSet :可导航搜索的不重复集合;
  • TreeSet :基于红黑树实现的可排序不重复集合;
  • Queue : 队列;
  • AbstractQueue : 队列的核心实现;
  • BlockingQueue : 阻塞队列;
  • Deque : 可两端操作线性集合;
  • Map : 键值映射集合
  • AbstractMap : 键值映射集合最小化抽象实现;
  • Hashtable :基于哈希表实现的键值映射集合,key、value均不可为null;
  • HashMap : 类似Hashtable,但方法不同步,key、value可为null;
  • LinkedHashMap : 根据插入顺序实现的键值映射集合;
  • IdentityHashMap : 基于哈希表实现的键值映射集合,两个key引用相等==,认为是同一个key;
  • SortedMap : 可排序键值映射集合;
  • NavigableMap : 可导航搜索的键值映射集合;
  • WeakHashMap : 弱引用键,不阻塞被垃圾回收器回收,key回收后自动移除键值对;

6. List、Set和Map的区别?
  • List:有序集合,元素可重复,可以插入多个null元素,元素都有索引;
  • Set:不重复集合,只允许存入一个null元素,必须保证元素唯一性。HashSet无序,SortedSet可排序,LinkedHashSet按照插入排序;
  • Map:键值对集合,存储键、值和之间的映射;Key无序,唯一;value 不要求有序,允许重复。

7. 集合底层数据结构是什么?
  • 一、Collection:
    1、List
    ①Arraylist: Object数组;
    ②Vector: Object数组;
    ③LinkedList: 双向循环链表。
    2、Set
    ①HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素;
    ②LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的;
    ③TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。
  • 二、Map
    1、HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间;
    2、LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑;
    3、HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
    4、TreeMap: 红黑树(自平衡的排序二叉树)。

8. 哪些集合类是线程安全的?
  • 1、Vector
  • 2、Stack
  • 3、Hashtable
  • 4、java.util.concurrent 包下所有的集合类 ArrayBlockingQueue、ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque等。

9. 如何确保一个集合不能被修改?
  • 可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

10. ArrayList和Vector的共同点和区别?
  • 一、 联系:
    1、底层都使用数组实现;
    2、功能相同,实现增删改查等操作的方法相似;
    3、长度可变的数组结构。
  • 二、 区别:
  • 1、这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合:
    ①线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的;
    ②性能:ArrayList 在性能方面要优于 Vector;
    ③扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%;
  • 2、Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间;
  • 3、 Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

11. ArrayList和LinkedList的区别是什么?
  • 1、数据结构实现:ArrayList 基于动态数组实现的非线程安全的集合;LinkedList 基于双向链表实现的非线程安全的集合。
  • 2、扩容问题:ArrayList 使用数组实现,无参构造函数默认初始化长度为 10,数组扩容是会将原数组中的元素重新拷贝到新数组中,长度为原来的 1.5 倍(扩容代价高);LinkedList 不存在扩容问题,新增元素放到集合尾部,修改相应的指针节点即可。
  • 3、内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用节点,一个指向前一个元素,一个指向下一个元素。
  • 4、随机访问效率:对于随机索引访问的 get 和 set 方法,一般 ArrayList 的速度要优于 LinkedList。因为 ArrayList 直接通过数组下标直接找到元素;LinkedList 要移动指针遍历每个元素直到找到为止。
  • 5、新增删除效率:新增和删除元素,一般 LinkedList 的速度要优于 ArrayList。因为 ArrayList 在新增和删除元素时,可能扩容和复制数组;LinkedList 实例化对象需要时间外,只需要修改节点指针即可。
  • 6、支持随机访问方面:LinkedList 集合不支持高效的随机访问(RandomAccess),ArrayList 支持。ArrayList 的空间浪费主要体现在在list列表的结尾预留一定的容量空间;LinkedList 的空间花费则体现在它的每一个元素都需要消耗存储指针节点对象的空间。
  • 相同点:都是非线程安全,允许存放 null

12. 什么是 Random Access ?
  • RandomAccess (随机访问):是一个标记接口,用于标明实现该接口的 List 支持快速随机访问,主要目的是使算法能够在随机和顺序访问的 List 中表现的更加高效。

13. ArrayList和Array有何区别?
  • ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素;
  • Array 即数组,定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。

14. 如何实现数组和 List 之间的转换?
  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。

15. Vector、ArrayList、LinkedList 的特点对比?
  • 1、ArrayList 和 Vector 都是使用数组存储数据;
    2、允许直接按序号索引元素;
    3、插入元素涉及数组扩容、元素移动等内存操作;
    4、根据下标找元素快,存在扩容的情况下插入慢;
    5、Vector 对元素的操作,使用了 synchronized 方法,性能比 ArrayList 差;
    6、Vector 属于遗留容器,早期的 JDK 中使用的容器;
  • 1、LinkedList 使用双向链表存储元素;
    2、LinkedList 按序号查找元素,需要进行前向或后向遍历,所以按下标查找元素,效率较低;
    3、LinkedList 非线程安全;
    4、LinkedList 使用的链式存储方式与数组的连续存储方式相比,对内存的利用率更高;
    5、LinkedList 插入数据时只需要移动指针即可,所以插入速度较快;

16. ArrayList list=new ArrayList(10);中的list扩容了吗?
  • 该语句只是申明和实例了一个 ArrayList,指定了容量为 10,未扩容,ArrayList 使用数组实现,无参构造函数默认初始化长度为 10,数组扩容是会将原数组中的元素重新拷贝到新数组中,长度为原来的 1.5 倍(扩容代价高)。

17. ArrayList 的优缺点?
  • 1、优点:ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快,ArrayList 在顺序添加一个元素的时候非常方便;
  • 2、ArrayList 的缺点如下:删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能;插入元素的时候,也需要做一次元素复制操作;
    3、总结:ArrayList 比较适合顺序添加、随机访问的场景。

18. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?
  • 1、for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止;
  • 2、迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式;
  • 3、foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

19. Java 中遍历 List 的最佳方式是什么?
  • 支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

20. 什么是迭代器Iterator?
  • Iterator 用于顺序访问集合对象的元素,它可以遍历集合中的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦。
  • 迭代器允许调用者在迭代过程中移除元素。
  • 缺点:增加新的集合类需要对应增加新的迭代器类,迭代器类与集合类成对增加。

21. Iterator 怎么使用?有什么特点?
# Iterator 使用代码如下:
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
  String obj = it. next();
  System. out. println(obj);
}
# java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象;
# next() 方法获得集合中的下一个元素;
# hasNext() 检查集合中是否还有元素;
# remove() 方法将迭代器新返回的元素删除;
# forEachRemaining(Consumer<? super E> action) 方法,遍历所有元素;
  • 特点:只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

22. 如何边遍历边移除 Collection 中的元素?
  • 边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法:
# 边遍历边移除List中的元素

List<Integer> list3 = new ArrayList<>();
        list3.add(1);
        list3.add(2);
        list3.add(3);
        list3.add(4);
 
        Iterator<Integer> iterator = list3.iterator();    
        while (iterator.hasNext()){            
            if(iterator.next() % 2 == 0){                
                iterator.remove();
            }
        }
        System.out.println(list3);
        
# 输出为[1, 3]
# 边遍历边移除set中的元素同List;

23. 如何边遍历边移除 Map 中的元素?
  • 使用 Iterator.remove() 方法:
# 边遍历边移除Map中的元素:

for (Iterator<Map.entry<K, V>> it = myHashMap.entrySet().iterator; it.hasNext();){
    Map.Entry<K, V> item = it.next();
    K key = item.getKey();
    V val = item.getValue();
}

# 例:
HashMap<Integer, String> hmap = new HashMap<>();
hmap.put(1, "hmap1");
hmap.put(2, "hmap2");
hmap.put(3, "hmap3");
hmap.put(4, "hmap4");
Iterator<Map.entry<Integer, String>> it = hmap.entrySet().iterator();
System.out.println("删除元素前:" + hmap);
while (it.hasNext()){
	Map.Entry<Integer, String> next = it.next();
	Integer key = next.getKey();
	String v = next.getValue();
	if("hmap1".equals(v)){
		it.remove();
	}
}
System.out.println("删除hmap1后:" + hmap);

# 输出为:删除lucy后:{17=h_nancy, 18=h_jerry, 12=h_henrly}

24. 为什么使用下面的代码实现边遍历边删除是错误的?
for(Integer i : list){
   list.remove(i)
}
  • 运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

25. Iterator 和 ListIterator 有什么联系和区别?
  • 1、联系:ListIterator 继承 Iterator;
  • 2、区别:
    ①Iterator可以迭代所有集合;ListIterator 只能用于List及其子类;
    ②ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能;
    ③ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不可以;
    ④ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不可以;
    ⑤ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改;

26. 多线程场景下如何使用 ArrayList?
  • ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如:
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
    System.out.println(synchronizedList.get(i));
}

27. 为什么 ArrayList 的 elementData 加上 transient 修饰?
  • 1、ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现;
  • 2、每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。

28. List里如何剔除相同的对象?
  • 1、List、List对象去重复值:
    这种情况的话,处理起来比较简单,通过JDK1.8新特性stream的distinct方法,可以直接处理。
  • List对象去重复值:
    这种的话,不能直接比较List中的对象,需要重写bean对象的equals和hashCode方法,然后通过放入Set集合来自动去重。

29. Collection和Collections有什么区别?
  • Collection是JDK中集合层次结构中的最根本的接口。定义了集合类的基本方法;
  • Collections是一个包装类。它包含有各种有关集合操作的静态多态方法,不能实例化,像一个Collection集合框架中的工具类。

30. Queue的add()和offer()方法有什么区别?
  • Queue 中 add() 和 offer() 都是用来向队列添加一个元素。在容量已满的情况下,add() 方法会抛出IllegalStateException异常,offer() 方法只会返回 false 。

31. Queue的remove()和poll()方法有什么区别?
  • Queue 中 remove() 和 poll() 都是用来从队列头部删除一个元素。在队列元素为空的情况下,remove() 方法会抛出NoSuchElementException异常,poll() 方法只会返回 null 。

32. Queue的element()和peek()方法有什么区别?
  • Queue 中 element() 和 peek() 都是用来返回队列的头元素,不删除。在队列元素为空的情况下,element() 方法会抛出NoSuchElementException异常,peek() 方法只会返回 null。

33. HashSet实现原理是什么?
  • 1、HashSet 是基于 HashMap 实现的,查询速度特别快;
  • 2、HashMap 是支持 key 为 null 值的,所以 HashSet 支持添加 null 值;
  • 3、HashSet 存放自定义类时,自定义类需要重写hashCode() 和 equals() 方法,确保集合对自定义类的对象的唯一性判断;
  • 4、无序、不可重复;

34. HashSet是如何保证数据不可重复的?
  • 1、向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equals 方法比较。
  • 2、HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

35. 为什么重写equals方法时必须重写hashCode方法?
  • 1、如果两个对象相等,则hashcode一定也是相同的
    2、两个对象相等,对两个equals方法返回true
    3、两个对象有相同的hashcode值,它们也不一定是相等的
    4、综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
    5、hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

36. TreeSet的原理是什么?特点?
  • 原理:TreeSet 基于 TreeMap 实现,TreeMap 基于红黑树实现;
  • 特点:有序;无重复;添加、删除元素、判断元素是否存在,效率比较高,时间复杂度为 O(log(N));

37. Java.util.Map的常用实现类有哪些?
  • HashMap、LinkedHashMap
  • Hashtable
  • TreeMap
  • IdentityHashMap

38. Map的遍历方式有哪些?
  • 1、Map 的 keySet() 方法,单纯拿到所有 Key 的 Set;
  • 2、Map 的 values() 方法,单纯拿到所有值的 Collection;
  • 3、keySet() 获取到 key 的 Set,遍历 Set 根据 key 找值(不推荐使用,效率比下面的方式低,原因是多出了根据 key 找值的消耗);
  • 4、获取所有的键值对集合,迭代器遍历;
  • 5、获取所有的键值对集合,for 循环遍历。

39. 什么是HashMap?HashMap的数据结构是什么?
  • 什么是HashMap:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  • HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

40. HashMap的实现原理?
  • HashMap 基于 Hash 算法实现,通过 put(key,value) 存储,get(key) 来获取 value;
  • 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标;
  • 存储时,如果出现hash值相同的key,此时有两种情况。①如果key相同,则覆盖原始值;②如果key不同(出现冲突),则将当前的key-value放入链表中;
  • 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值;
  • 当计算出的 hash 值相同时,称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value;
  • 当 hash 冲突的个数:小于等于 8 使用链表;大于 8 时,使用红黑树解决链表查询慢的问题。

41. HashMap的put方法的具体流程(步骤)?
  • 当我们put的时候,首先计算 key的hash值,这里调用了 hash方法,hash方法实际是让key.hashCode()与key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length - 1) & hash,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。
  • ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
  • ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
  • ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
  • ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
  • ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  • ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

42. HashMap的扩容操作是怎么实现的?
  • ①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
  • ②.每次扩展的时候,都是扩展2倍;
  • ③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
  • 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。

43. 为什么基本类型不能做为HashMap的键值?
  • Java中是使用泛型来约束 HashMap 中的key和value的类型的,HashMap<K, V>;
  • 泛型在Java的规定中必须是对象Object类型的,基本数据类型不是Object类型,不能作为键值;
  • map.put(0, “ConstXiong”)中编译器已将 key 值 0 进行了自动装箱,变为了 Integer 类型;

44. 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的?
  • 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
  • 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
  • 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

45. 能否使用任何类作为 Map 的 key?
  • 可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
    1、如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
    2、类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
    3、如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
    4、用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

46. 为什么HashMap中String、Integer这样的包装类适合作为Key?
  • String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率:
    1、都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
    2、内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

47. 如果使用Object作为HashMap的Key,应该怎么办呢?
  • 重写hashCode()和equals()方法
    1、重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
    2、重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

48. HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
  • hashCode()方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

49. 如何解决通过hashCode()计算出的哈希值可能不在数组大小范围内的问题?
  • 1、HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
  • 2、在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

50. HashMap 的长度为什么是2的幂次方?
  • 为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
  • 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

51. HashMap 为什么使用两次扰动?
  • 这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。

52. HashSet和HashMap有什么区别?
  • HashMap
    ①实现 Map 接口;
    ②键值对的方式存储;
    ③新增元素使用 put(K key, V value) 方法;
    ④底层通过对 key 进行 hash,使用数组 + 链表或红黑树对 key、value 存储;
  • HashSet
    ①实现 Set 接口;
    ②存储元素对象;
    ③新增元素使用 add(E e) 方法;
    ④底层是采用 HashMap 实现,大部分方法都是通过调用 HashMap 的方法来实现;

53. TreeSet和TreeMap在排序时如何比较元素?
  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo() 方法,当插入元素时会回调该方法比较元素的大小;
  • TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序;

54. HashMap 与 HashTable 有什么区别?
  • 1、线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  • 2、效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  • 3、对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException;
  • 4、初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方;
  • 5、底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制;
  • 6、推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代;

55. 如何决定使用 HashMap 还是 TreeMap?
  • 对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

56. ConcurrentHashMap的实现原理?
  • HashMap 是线程不安全的,效率高;HashTable 是线程安全的,效率低。ConcurrentHashMap 可以做到既是线程安全的,同时也可以有很高的效率,得益于使用了分段锁。
  • JDK1.7实现原理:
  • ConcurrentHashMap 是通过数组 + 链表实现,由 Segment 数组和 Segment 元素里对应多个 HashEntry 组成;
  • value 和链表都是 volatile 修饰,保证可见性;
  • ConcurrentHashMap 采用了分段锁技术,分段指的就是 Segment 数组,其中 Segment 继承于 ReentrantLock;
  • 理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment;
  • JDK1.8实现原理:
  • 抛弃了原有的 Segment 分段锁,采用了 CAS + synchronized 来保证并发安全性;
  • HashEntry 改为 Node,作用相同;
  • val next 都用了 volatile 修饰;

57. HashMap 和 ConcurrentHashMap 的区别?
  • 1、ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
  • 2、HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

58. ConcurrentHashMap 和 Hashtable 的区别?
  • ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
  • 1、底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 2、实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

59. comparable 和 comparator的区别?
  • comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序;
  • comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序;
  • 一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort()。

60. TreeMap 和 TreeSet 在排序时如何比较元素?
  • TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。

61. Collections 工具类中的 sort()方法如何比较元素?
  • Collections 工具类的 sort 方法有两种重载的形式,
  • 1、要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
  • 2、不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值