概述
技术栈 | 传送门 |
JAVA 基础 | 手撸架构,Java基础面试100问_vincent-CSDN博客 |
JAVA 集合 | 手撸架构,JAVA集合面试60问_vincent-CSDN博客 |
JVM 虚拟机 | 手撸架构,JVM面试30问_vincent-CSDN博客 |
并发编程 | 手撸架构,并发编程面试123问_vincent-CSDN博客 |
Spring | 手撸架构,Spring面试63问_vincent-CSDN博客 |
Spring cloud | 手撸架构,Spring cloud面试45问_vincent-CSDN博客 |
SpringBoot | 手撸面试,Spring Boot面试41问_vincent-CSDN博客 |
Netty 与 RPC | 手撸架构,Netty 与 RPC面试48问_vincent-CSDN博客 |
Doubo | 手撸架构,Dubbo面试49问_vincent-CSDN博客 |
Redis | 手撸架构,Redis面试41问_vincent-CSDN博客 |
Zookeeper | 手撸架构,Zookeeper面试27问_vincent-CSDN博客 |
Mysql | 手撸架构,Mysql 面试126问_vincent-CSDN博客 |
MyBatis | 手撸架构,MyBatis面试42问_vincent-CSDN博客 |
MongoDB | 手撸架构,MongDB 面试50问_vincent-CSDN博客 |
Elasticsearch | 手撸架构,Elasticsearch 面试25问_vincent-CSDN博客 |
RabbitMQ | 手撸架构,RabbitMQ 面试49问_vincent-CSDN博客 |
Kafka | 手撸架构,Kafka 面试42问_vincent-CSDN博客 |
Docker | 手撸架构,Docker 面试25问_vincent-CSDN博客 |
Nginx | 手撸架构,Nginx 面试40问_vincent-CSDN博客 |
算法 | 常用排序算法总结(1)-- 比较排序_vincent-CSDN博客_比较排序 常用排序算法总结(2)-- 非比较排序算法_vincent-CSDN博客_非比较排序的算法有 |
分布式事务 | 分布式事务解决方案(总览)_vincent-CSDN博客 |
HTTP | 太厉害了,终于有人能把TCP/IP 协议讲的明明白白了_vincent-CSDN博客_tcp和ip |
接口继承关系和实现
集合类存放于 Java.util 包中,主要有 3 种:set(集)、list(列表包含 Queue)和 map(映射)。
- Collection:Collection 是集合 List、Set、Queue 的最基本的接口。
- Iterator:迭代器,可以通过迭代器遍历集合中的数据
- Map:是映射表的基础接口
Collection与Collections的区别是
Collection是Java集合框架中的基本接口,如List接口也是继承于它
public interface List<E> extends Collection<E>
Collections是Java集合框架提供的一个工具类,其中包含了大量用于操作或返回集合的静态方法。如下:
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
为何Collection不从Cloneable和Serializable接口继承?
Collection表示一个集合,包含了一组对象元素。如何维护它的元素对象是由具体实现来决定的。因为集合的具体形式多种多样,例如list允许重复,set则不允许。而克隆(clone)和序列化(serializable)只对于具体的实体,对象有意义,你不能说去把一个接口,抽象类克隆,序列化甚至反序列化。所以具体的collection实现类是否可以克隆,是否可以序列化应该由其自身决定,而不能由其超类强行赋予。
如果collection继承了clone和serializable,那么所有的集合实现都会实现这两个接口,而如果某个实现它不需要被克隆,甚至不允许它序列化(序列化有风险),那么就与collection矛盾了。
List
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:分别是 ArrayList、Vector 和 LinkedList。
ArrayList(数组)
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
Vector(数组实现、线程同步)
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
LinkList(链表)
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
Array 和 ArrayList 有何区别?
定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同;
ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。
Arraylist 与 LinkedList 区别
- ArrayList是数组的数据结构,LinkedList是链表的数据结构。
- 随机访问的时候,ArrayList的效率比较高,因为LinkedList要移动指针,而ArrayList是基于索引(index)的数据结构,可以直接映射到。
- 插入、删除数据时,LinkedList的效率比较高,因为ArrayList要移动数据。
- LinkedList比ArrayList开销更大,因为LinkedList的节点除了存储数据,还需要存储引用。
ArrayList和Vector的区别
ArrayList和Vector都实现了List接口,他们都是有序集合,并且存放的元素是允许重复的。它们的底层都是通过数组来实现的,因此列表这种数据结构检索数据速度快,但增删改速度慢。
而ArrayList和Vector的区别主要在两个方面:
- 线程安全。Vector是线程安全的,而ArrayList是线程不安全的。因此在如果集合数据只有单线程访问,那么使用ArrayList可以提高效率。而如果有多线程访问你的集合数据,那么就必须要用Vector,因为要保证数据安全。
- 数据增长。ArrayList和Vector都有一个初始的容量大小,当存储进它们里面的元素超过了容量时,就需要增加它们的存储容量。ArrayList每次增长原来的0.5倍,而Vector增长原来的一倍。ArrayList和Vector都可以设置初始空间的大小,Vector还可以设置增长的空间大小,而ArrayList没有提供设置增长空间的方法。
LinkedList是单向链表还是双向链表
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
ArrayList 的扩容机制
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
public boolean add(E e) {
//扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//如果传入的是个空数组则最小容量取默认容量与minCapacity之间的最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果最小需要空间比elementData的内存空间要大,则需要扩容
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// 获取elementData数组的内存空间长度
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//校验容量是否够
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//若预设值大于默认的最大值,检查是否溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间
//并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
打印数组
public static void main(String[] args) {
String[] jayArray = {"jay", "boy"};
// 方式一
Stream.of(jayArray).forEach(System.out::println);
// 方式二
System.out.println(Arrays.toString(jayArray));
}
如何实现数组和 List之间的转换
Array 转List
// 使用泛型,无需显式类型转换
String[] array = list.toArray(new String[list.size()]);
如果直接使用 toArray 无参方法,返回值只能是 Object[] 类,强转其他类型可能有问题:
String[] array = (String[]) list.toArray();
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at Test.main(Test.java:14)
Array 转List
使用Arrays.asList() 把数组转换成集合时,不能使用修改集合相关的方法:
String[] str = new String[] { "jay", "tianluo" };
List list = Arrays.asList(str);
list.add("boy");
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at Test.main(Test.java:13)
因为 Arrays.asList不是返回java.util.ArrayList,而是一个内部类ArrayList。
可以这样使用弥补这个缺点:
//方式一:
ArrayList< String> arrayList = new ArrayList<String>(strArray.length);
Collections.addAll(arrayList, strArray);
//方式二:
ArrayList<String> list = new ArrayList<String>(Arrays.asList(strArray)) ;
Collections.sort和Arrays.sort的实现原理
Collection.sort是对list进行排序,Arrays.sort是对数组进行排序。
Collections.sort底层实现
Collections.sort方法调用了list.sort方法
list.sort方法调用了Arrays.sort的方法
因此,Collections.sort方法底层就是调用的Array.sort方法
Arrays.sort底层实现
Arrays的sort方法,如下:
如果比较器为null,进入sort(a)方法。如下:
因此,Arrays的sort方法底层就是:
- legacyMergeSort(a),归并排序,
- ComparableTimSort.sort():即Timsort排序。
Timesort排序
Timsort排序是结合了合并排序(merge.sort)和插入排序(insertion sort)而得出的排序方法;
1.当数组长度小于某个值,采用的是二分插入排序算法,如下:
找到各个run,并入栈。
按规则合并run。
什么是Java优先级队列(Priority Queue)?
优先队列PriorityQueue是Queue接口的实现,可以对其中元素进行排序
优先队列中元素默认排列顺序是升序排列,但对于自己定义的类来说,需要自己定义比较器
public class PriorityQueue<E> extends AbstractQueue<E>
implements java.io.Serializable {
...
private final Comparator<? super E> comparator;
方法:
peek()//返回队首元素
poll()//返回队首元素,队首元素出队列
add()//添加元素
size()//返回队列元素个数
isEmpty()//判断队列是否为空,为空返回true,不空返回false
特点:
- 基于优先级堆
- 不允许null值
- 线程不安全
- 出入队时间复杂度O(log(n))
- 调用remove()返回堆内最小值
阻塞队列的实现,ArrayBlockingQueue的底层实现?
ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection接口。底层以数组的形式保存数据(实际上可看作一个循环数组)。常用的操作包括 add ,offer,put,remove,poll,take,peek。
BlockingQueue 使用详解_vincent-CSDN博客_blockingqueue使用
谈谈线程池阻塞队列
- ArrayBlockingQueue: (有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
- LinkedBlockingQueue: (可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列
- DelayQueue:(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
- PriorityBlockingQueue:(优先级队列)是具有优先级的无界阻塞队列;
- SynchronousQueue:(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
线程池都有哪几种工作队列
结合使用到对应队列的常用线程池(如newFixedThreadPool线程池使用LinkedBlockingQueue),进行展开阐述, 就可以
poll()方法和 remove()方法的区别
Queue队列中,poll() 和 remove() 都是从队列中取出一个元素,在队列元素为空的情况下,remove() 方法会抛出异常,poll() 方法只会返回 null 。
/**
* Retrieves and removes the head of this queue. This method differs
* from {@link #poll poll} only in that it throws an exception if this
* queue is empty.
*
* @return the head of this queue
* @throws NoSuchElementException if this queue is empty
*/
E remove();
/**
* Retrieves and removes the head of this queue,
* or returns {@code null} if this queue is empty.
*
* @return the head of this queue, or {@code null} if this queue is empty
*/
E poll();
Set
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
HashSet(Hash 表)
哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。
哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。如图 1 表示 hashCode 值不相同的情况;图 2 表示 hashCode 值相同,但 equals 不相同的情况。
HashSet 通过 hashCode 值来确定元素在内存中的位置。一个 hashCode 位置上可以存放多个元
素。
HashSet的add方法,
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
TreeSet(二叉树)
1. TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
2. Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。
3. 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序
4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
HashSet和TreeSet区别
- Hashset 的底层是由哈希表实现的,Treeset 底层是由红黑树实现的。
- HashSet中的元素没有顺序,TreeSet保存的元素有顺序性(实现Comparable接口)
- HashSet的add(),remove(),contains()方法的时间复杂度是O(1);TreeSet中,add(),remove(),contains()方法的时间复杂度是O(logn)
LinkHashSet(HashSet+LinkedHashMap)
对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。
Map
HashMap(数组+链表+红黑树)
HashMap 根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。我们用下面这张图来介绍HashMap 的结构。
JAVA7 实现
大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
- capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
- loadFactor:负载因子,默认为 0.75。
- threshold:扩容的阈值,等于 capacity * loadFactor
JAVA8 实现
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树组成。
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
ConcurrentHashMap
Segment 段
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个segment。
线程安全(Segment 继承 ReentrantLock 加锁)
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
并行度(默认 16)
concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。
Java8 实现 (引入了红黑树)
Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。
HashTable(线程安全)
Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。
HashMap 7 种遍历方式与性能分析
终于来了,最全的 HashMap 7 种遍历方式与性能分析_vincent-CSDN博客_hashmap 遍历方式
HashMap 和 HashTable 的区别
HashMap 和 HashTable 的区别_vincent-CSDN博客
1、继承的父类不同
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
2、线程安全性不同
HashTable 部分方法上有自己的 synchronize 同步,是线程安全的
3、 HashMap允许key,value为null,HashTable都不允许为null
4、是否提供contains方法
HashMap把contains方法去掉了,改成containsValue和containsKey。
Hashtable则保留了contains,containsValue和containsKey三个方法,其中containsKey和containsValue功能相同。
5、两个遍历方式的内部实现上不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
6、hash值不同
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。 hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。
Hashtable计算hash值,直接用key的hashCode(),在求hash值对应的位置索引时,用取模运算;
HashMap 上一章节详细阐述了用key的hashCode(),进行hash函数计算出的值,通过indexForh&(length-1)保证获取的index一定在数组范围内。
7、内部实现使用的数组初始化和扩容方式不同
HashTable在不指定容量的情况下的默认容量为11,增加的方式是 old*2+1;
HashMap在不指定容量的情况下的默认容量为16, 增加的方式要求一定为2的次幂。比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,
HashMap 的扩容过程
HashMap 基于JDK(1.7/1.8)实现原理及源码分析_vincent-CSDN博客_hashmap原理1.7和1.8
第一步把数组长度变为原来的两倍,
第二步把旧数组的元素重新计算hash插入到新数组中。
jdk8时,不用重新计算hash,只用看看原来的hash值新增的一位是零还是1,如果是1这个元素在新数组中的位置,是原数组的位置加原数组长度,如果是零就插入到原数组中。扩容过程第二步一个非常重要的方法是transfer方法,采用头插法,把旧数组的元素插入到新数组中。
HashMap 的长度为什么是2的幂次方
为了能让HashMap存取高效,数据分配均匀。
看着呢,以下等式相等,但是位移运算比取余效率高很多呢~
hash%length=hash&(length-1)
为什么HashMap中String、Integer这样的包装类适合作为key?
String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
- 因为它们都是final修饰的类,不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
- 它们内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范
如果想用Object作为hashMap的Key
重写hashCode()和equals()方法
讲讲红黑树的特点?
- 每个节点或者是黑色,或者是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。[注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
- 如果一个节点是红色的,则它的子节点必须是黑色的。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
变色:在不违反上述红黑树规则特点情况下,将红黑树某个node节点颜色由红变黑,或者由黑变红;
左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点
右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点
关于红黑树(R-B tree)原理,看这篇如何 - 工匠初心 - 博客园
无需调整 | 【变色】即可实现平衡 | 【旋转+变色】才可实现平衡 | |
情况1: | 当父节点为黑色时插入子节点 | 空树插入根节点,将根节点红色变为黑色 | 父节点为红色左节点,叔父节点为黑色,插入左子节点,那么通过【左左节点旋转】 |
情况2: | - | 父节点和叔父节点都为红色 | 父节点为红色左节点,叔父节点为黑色,插入右子节点,那么通过【左右节点旋转】 |
情况3: | - | - | 父节点为红色右节点,叔父节点为黑色,插入左子节点,那么通过【右左节点旋转】 |
情况4: | - | - | 父节点为红色右节点,叔父节点为黑色,插入右子节点,那么通过【右右节点旋转】 |
TreeMap(可排序)
TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
如果使用排序的映射,建议使用 TreeMap。在使用 TreeMap 时,key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。
TreeMap底层
- TreeMap实现了SotredMap接口,它是有序的集合
- TreeMap底层数据结构是一个红黑树,每个key-value都作为一个红黑树的节点
- 如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序
如何决定使用 HashMap 还是TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按key的升序排序,也可以指定排序的比较器。当用Iterator遍历TreeMap时,得到的记录是排过序的。
LinkHashMap(记录插入顺序)
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序序。
List,Set,Map三者的区别?
List: List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
Set: 不允许重复的集合。不会有多个元素引用相同的对象。
Map: 使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象
迭代器 Iterator 是什么?怎么用,有什么特点?
public interface Collection<E> extends Iterable<E> {
Iterator<E> iterator();
方法如下:
- next() 方法获得集合中的下一个元素
- hasNext() 检查集合中是否还有元素
- remove() 方法将迭代器新返回的元素删除
- forEachRemaining(Consumer<? super E> action) 方法,遍历所有元素
Iterator 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。
List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
String obj = it. next();
System. out. println(obj);
}
Iterator 和 ListIterator 区别
ListIterator 比 Iterator有更多的方法:
- ListIterator只能用于遍历List及其子类,Iterator可用来遍历所有集合,
- ListIterator遍历可以是逆向的,因为有previous()和hasPrevious()方法,而Iterator不可以。
- ListIterator有add()方法,可以向List添加对象,而Iterator却不能。
- ListIterator可以定位当前的索引位置,因为有nextIndex()和previousIndex()方法,而Iterator不可以。
- ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改哦。
Enumeration接口和Iterator接口的区别有哪些?
Enumeration速度是Iterator的2倍,同时占用更少的内存。但是,Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。同时,Iterator允许调用者删除底层集合里面的元素,这对Enumeration来说是不可能的。
快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
快速失败
在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
list.add(3);
System.out.println(list.size());
}
}
}
运行结果:
1
Exception in thread "main" java.util.ConcurrentModificationException
3
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at Test.main(Test.java:12)
安全失败
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
public class Test {
public static void main(String[] args) {
List<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
list.add(3);
System.out.println("list size:"+list.size());
}
}
}
运行结果:
1
list size:3
2
list size:4
其实,在java.util.concurrent 并发包的集合,如 ConcurrentHashMap, CopyOnWriteArrayList等,默认为都是安全失败的。
在迭代一个集合的时候,如何避免ConcurrentModificationException
在遍历一个集合的时候,我们可以使用并发集合类来避免ConcurrentModificationException,比如使用CopyOnWriteArrayList,而不是ArrayList。
Enumeration和Iterator接口的区别?
public interface Enumeration<E> {
boolean hasMoreElements();
E nextElement();
}
public interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
- 函数接口不同
- Enumeration速度快,占用内存少,但是不是快速失败的,线程不安全。
- Iterator允许删除底层数据,枚举不允许
- Iterator安全性高,因为其他线程不能够修改正在被Iterator遍历的集合里面的对象。
怎么确保一个集合不能被修改
很多朋友很可能想到用final关键字进行修饰,final修饰的这个成员变量,如果是基本数据类型,表示这个变量的值是不可改变的,如果是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变,如下:
public class Test {
//final 修饰
private static final Map<Integer, String> map = new HashMap<Integer, String>();
{
map.put(1, "jay");
map.put(2, "tianluo");
}
public static void main(String[] args) {
map.put(1, "boy");
System.out.println(map.get(1));
}
}
运行结果如下:
boy
那么,到底怎么确保一个集合不能被修改呢,可以使用以下三个方法:
- unmodifiableMap
- unmodifiableList
- unmodifiableSet
public class Test {
private static Map<Integer, String> map = new HashMap<Integer, String>();
{
map.put(1, "jay");
map.put(2, "tianluo");
}
public static void main(String[] args) {
map = Collections.unmodifiableMap(map);
map.put(1, "boy");
System.out.println(map.get(1));
}
}
运行结果:
// 可以发现,unmodifiableMap确保集合不能修改啦,抛异常了
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
at Test.main(Test.java:14)
对一组对象进行排序
Collections.sort()+ Comparator.comparing()
Collections.sort(studentList, Comparator.comparing(Student::getScore).reversed().thenComparing(Student::getName));
studentList.stream().forEach(p -> System.out.println(p.toString()));
Comparable 和 Comparator的区别
Comparable和Comparator接口被用来对对象集合或者数组进行排序。Comparable接口被用来提供对象的自然排序,我们可以使用它来提供基于单个逻辑的排序。
Comparator接口被用来提供不同的排序算法,我们可以选择需要使用的Comparator来对给定的对象集合进行排序。
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
public class Person implements Comparable<Person> {
private int age;
/**
* TODO重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) {
return 1;
} else if (this.age < o.getAge()) {
return -1;
}
return age;
}
}
集合类框架的最佳实践
结合平时工作:
- 根据应用需要正确选择要使用的集合类型对性能非常重要,比如:假如知道元素的大小是固定的,那么选用Array类型而不是ArrayList类型更为合适。
- 有些集合类型允许指定初始容量。因此,如果我们能估计出存储的元素的数目,我们可以指定初始容量来避免重新计算hash值或者扩容等。
- 为了类型安全、可读性和健壮性等原因总是要使用泛型。同时,使用泛型还可以避免运行时的ClassCastException。
- 使用JDK提供的不变类(immutable class)作为Map的键可以避免为我们自己的类实现hashCode()和equals()方法。
- 编程的时候接口优于实现
- 底层的集合实际上是空的情况下,返回为长度是0的集合或数组而不是null。
使用集合框架的好处
- 容量自增长;
- 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
- 允许不同 API 之间的互操作,API之间可以来回传递集合;
- 可以方便地扩展或改写集合,提高代码复用性和可操作性。
- 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
最后
以上就是安详蜡烛为你收集整理的手撸架构,JAVA集合 面试60问的全部内容,希望文章能够帮你解决手撸架构,JAVA集合 面试60问所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复