1.Collection下的常用子类集合有那一些?
有List,Set。
List:有序(按添加顺序显示),可重复,set:无序(按大小排序显示),不可重复,map:以键值对方式存储
List分别有:
ArrayList:是List的主要实现类,ArrayList的底层是数组。线程不安全的。查找快,增删慢(通过首地址+下标获取元素位置,增删要移动某一些元素)”
LinkedList:底层是一个双向链表(是一个带头/尾指针的双向链表,)。插入,删除快(增删快因为直接就在对应的位置设置新的元素指向前后指针或删除)。查找慢(因为链表都是next指向下一个next.只能从首元素开始,依次获得下一个元素的地址。)
Vector:底层是一个object数组,效率低。它是线程安全的
2.ArrayList1.7和1.8的底层扩容实现原理说一下
通过一个空参的构造器创建对象时1.7底层创建了长度是10的数组。当我们向集合中添加第11个元素时,底层会进行扩容,扩容为原来数组长度的1.5倍。同时将原数组中的内容复制新的数组中。 元素要求所在的类必须重写equals方法(饿汉式)
1.8中并没有直接就创建长度为10的数组,调用add的时候才创建。(懒汉式),可以自己测试。
public static void main(String[] args)throws Exception {
ArrayList list=new ArrayList();
Integer integer = getCapacity(list);
System.out.println("初始容量为"+integer);
list.add("1");
Integer integer2 = getCapacity(list);
System.out.println("初始容量为"+integer2);
}
public static Integer getCapacity(ArrayList list) {
Integer length = null;
Class clazz = list.getClass();
Field field;
try {
field = clazz.getDeclaredField("elementData");
field.setAccessible(true);
Object[] object = (Object[]) field.get(list);
length = object.length;
return length;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return length;
}
扩容机制也可以看源码
第一次添加元素的时候this.size+1 的值是1,所以第一次添加的时候会将当前elementData数组的长度变为10。
将修改次数(modCount)自增1,判断是否需要扩充数组长度,判断条件就是用当前所需的数组最小长度与数组的长度对比,如果大于0,则增长数组长度。
我们来打个断点测试
当前数组还没有创建长度为0,所以就到下面的扩容方法中去创建数组
第二次进来的时候就有数组的长度了所以不会扩容,mincapacity达到11的时候就会继续触发扩容机制
public boolean add(E e) {
//确认list容量,如果不够,容量加1。注意:只加1,保证资源不被浪费
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素e放在size的位置上,并且size++
elementData[size++] = e;
return true;
}
//数组容量检查,不够时则进行扩容,只供类内部使用
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 若elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则取minCapacity为DEFAULT_CAPACITY和参数minCapacity之间的最大值。DEFAULT_CAPACITY在此之前已经定义为默认的初始化容量是10。
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//数组容量检查,不够时则进行扩容,只供类内部使用
// minCapacity 想要的最小容量
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//最小容量>数组缓冲区当前长度
if (minCapacity - elementData.length > 0)
grow(minCapacity);//扩容
}
3.Set的常用子集合有哪些?以及hashset的底层实现原理?
HashSet:是set的主实现类,线程不安全,可以存null值。底层是hashmap集合。创始容量和加载因子和hashmap一样。会根据数字或者字母来进行排序。
HashSet的底层原理:向set中添加对象时,会调用改对象的hashcod方法来计算存储的位置,如果该位置上没有其他元素,则直接存放,已经存在其他元素就调用该对象的equals方法进行内容比较,如果是true则不能存放,是false以链表的形式存放该位置上。
LinkedHashSet:是按照添加元素的顺序进行遍历,因为底层维护了一张链表用来记录添加的顺序。底层实现原理和HashSet一样,是hashset的子类
TreeSet:可以按照添加的对象的指定属性进行排序。AbstractSet子类。AbstractSet是set的子类。
什么是链表?:链表是一种根据元素节点逻辑关系排列起来的一种数据结构。它的主要目的是依靠引用关系来实现多个数据的保存
4.说一下Map接口中常用的子类,以及他的底层实现原理
Map分为hashmap和linkedhashmap,hashtable,treemap,
key是无序的不可重复的,使用Set存储方式的,Key所在的类想要重写equals方法和Hashcod方法,value无序可重复的,vaule所在的类要重写equals方法。
HashMap:是线程不安全的,可以存放null的key和vaule,如果是null默认从第0个查找。
LinkedHashMap:和Hashmap一样,只是可以按照元素添加的顺序进行遍历。底层用双向链表来记录元素添加顺序。
HashTable:是线程安全的。不可能存null。但是Hashtable是表级锁底层方法都是使用synchronized来保证线程安全,而ConcurrentHashMap是段级锁
TreeMap:添加的key-vaule对进行排序,可以考虑订制排序和自然排序,实现Comparator和 Comparable接口。
5.HashMap的底层实现原理,也就是Put方法底层是怎么实现的1.8中
1.new HashMap() : 底层默认创建了一个长度为16的数组为Node[16]类型(1.7中为Entry[]数组),加载因子为0.75。
当我们添加元素的数量一旦达到 (16 * 0.75)12时就会进行扩容。扩容为原来的2倍,, 初始容量与负载因子影响性能。
为什么是扩容2倍?根据计算机二进制的算法,在范围内的所索引值都可以得到
2.put添加数据的时候(k1,v1),先调用k1的hashCode方法算出哈希值,再根据哈希值算出该对象在数组中存放的位置。如果该位置上没其它元素,如果没其它元素则直接将该对象存放到数组中。如果该位置上已经存在其它元素(K2,V2)。则调用k1的equals方法和k2进行比较。如果结果为true,则说明key是相同的。则v1覆盖v2。
如果结果为false,以链表的形式将该对象存在该 位置上。如果链表上的元素的个数达到8时,
将链表改成红黑树(jdk1.8。
代码测试通过反射获取HashMap容量
public static void main(String[] args) throws Exception {
//指定初始容量15来创建一个HashMap
HashMap m = new HashMap(15);
//获取HashMap整个类
Class mapType = m.getClass();
//获取指定属性,也可以调用getDeclaredFields()方法获取属性数组
Field threshold = mapType.getDeclaredField("threshold");
//将目标属性设置为可以访问
threshold.setAccessible(true);
//获取指定方法,因为HashMap没有容量这个属性,但是capacity方法会返回容量值
Method capacity = mapType.getDeclaredMethod("capacity");
//设置目标方法为可访问
capacity.setAccessible(true);
//打印刚初始化的HashMap的容量、阈值和元素数量
System.out.println("容量:"+capacity.invoke(m)+" 阈值:"+threshold.get(m)+" 元素数量:"+m.size());
for (int i = 0;i<17;i++){
m.put(i,i);
//动态监测HashMap的容量、阈值和元素数量
System.out.println("容量:"+capacity.invoke(m)+" 阈值:"+threshold.get(m)+" 元素数量:"+m.size());
}
}
6.Hashmap为什么使用红黑树,红黑树是什么?
因为链表长了就会查询慢的问题,红黑树相当于排序数据。可以自动的使用二分法进行定位。性能较高。
红黑树是一种平衡二叉查找树,他的节点是红色或者黑色,根节点是黑色,每个叶子节点都是黑色的空节点,红色的节点他的子节点都是黑色,他可以用变色和旋转来调整平衡。
7.Hashmap的加载因子为什么是0.75为什么不是别的?
提高空间利用率,冲突少0.75比较合适。(我这里简单说了)
8.Hashmap如何解决hash冲突问题?1.7扩容死循环问题?
使用链表存放hash值相等且内容不等,存放到同一个链表中。
因为采用了头插法,在多线程的情况下操作hashmap的时候,导致死循环的问题。
9.HashMap1.8和1.7改进了那些, 红黑树和链表的时间复杂度
(一)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,因为1.7用单链表就行纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
(二)扩容后数据存储位置的计算方式也不一样
(三)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
Ologn 和 o(n)
10.Iterator和ListIterator有什么区别?
ListIterator 继承 Iterator,ListIterator中操作list集合的方法多,比如遍历的时候Iterator遍历做添加操作报并发修改异常错误,这个时候可以使用ListIterator中的add方法做添加,还有set方法做修改等。
11.你了解多线程吗?说一下多线程。
多线程原理:同一时间内,CPU只能处理1条线程,只有1条线程在工作(执行),而多线程…
为了充分的利用资源,让程序响应更快,使用了多线程。线程越多,cpu开销就越大。
开启线程的4种方式,继承Thred,实现Runnable接口,实现Callble接口和使用线程池。
生命周期->新建->就绪->阻塞->运行->死亡 ,阻塞到运行过程是循环的主要看线程
线程的同步机制:同步方法和同步代码块。
12.sleep和wait的区别?手写一下买票交替的线程实现。
1.sleep不会释放锁。Wait会释放锁
2.sleep会自动唤醒。wait需要其它线程唤醒。
3.sleep是Thread中的方法。wait是Object中的方法。
线程通信的wait,notfiy,notfiyall只能在同步方法和代码块中使用
13.为什么使用多线程?你对锁有了解吗?什么时候会出现线程安全问题?
因为可以提高程序效率,重入锁(Synchronized )、(数据库锁)悲观锁和乐观锁,分段锁(ConcurrentHashMap),读写锁(ReadWriteLock)。,分布式锁 在有共享数据,当多个线程访问全局变量的时候。
14.TCP的3次握手和四次挥手?为什么是四次挥手,而不是三次或是五次、六次?
三次握手:要建立连接时,1客户端发送一个SYN报文给服务器,2服务器收到请求后,向客户端响应一个SYN+ACK连接数据报文,3然后客户端收到报文,回应ACK报文,服务器收的到ACK完成3次握手。
四次挥手:双方关闭连接要经过双方都同意。所以,1首先是客服端给服务器发送FIN,要求关闭连接,2服务器收到后会发送一个ACK进行确认。3服务器关闭连接然后再发送一个FIN,4客户端收到FIN发送ACK确认,并进入TIME_WAIT状态。等待2MSL后自动关闭。服务器收到ACK就关闭连接了。
为什么是3次握手:因为没有收到服务器返回确认报文,这时会放弃连接,重新启动一条连接请求,因为他们之间要相互确认是否收到ACK。
15.为什么ArrayList集合不是安全的?并发(修改)异常是什么?
就是在用迭代器Iterator的遍历的时候,同时对集合元素进行操作,但是迭代器并不知道集合发生变化,会产生并发修改异常。
1.(删除操作)迭代的过程中可以使用Iterator的删除方法删除。2.(新增操作)使用listIterator 中有add方法,因为在迭代的过程对数组的修改。只有List有这特性,因为List有索引。2.使用for循环集合的siez去删除(遍历删除数据异常) 使用迭代器iterator遍历删除,不使用while(遍历删除数据异常)
16.TCP与UDP的区别:
1.基于连接与无连接;
2.对系统资源的要求(TCP较多,UDP少);
3.UDP程序结构较简单;
4.流模式与数据报模式 ;
5.TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。
17.什么是浅拷贝和深拷贝?
浅拷贝(Shallow Copy):指向被复制的内存地址,如果地址发生改变,浅拷贝出来的对象也会相应改变
深拷贝:为对象都开辟了新的内存空间地址,使这个增加的指针指向新的内存
总结:实则浅拷贝和深拷贝只是相对的,如果一个对象内部只有基本数据类型,那用 clone() 方法获取到的就是这个对象的深拷贝,而如果其内部还有引用数据类型,那用 clone() 方法就是一次浅拷贝的操作。
18.说一下BIO和NIO和AIO?
Java BIO: 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
19.什么是序列化与反序列化?
作用:在网络上传输对象字节序列,把对象字节保存到硬盘上。减少内存消耗。或者在远程rpc调用对象的时候需要。
序列化就是把对象存储到硬盘上,反序列化就是获取对象文件中的数据,转成后台对象类的数据。
20. 快速失败 (fail-fast) 和安全失败 (fail-safe) 的区别是什么?
java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。
21.ConcurrentHashMap分段锁的实现原理
ConcurrentHashMap是同步容器类jdk5增加的一个线程安全的哈希表。在hashmap和hashtable之间,内部采用锁分段机制代替hashtable的独占锁。提高性能。默认把表分成16个段(hashtable),每段中分出一个数组加链表。内部采用大量了CAS操作,他的数据结构很接近hashmap,只是增加了同步操作来控制并发,保证了线程的安全。(分段锁)
22.CAS算法是怎么操作的? ABA问题?
比较并替换。cas是一种基于锁的操作,而且是乐观锁。CAS包含了三个操作值,内存值,预估值,更新值,当内存值==预估值,才将更新值赋值给 内存值==更新值,否则将不做任何操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
ABA问题是: 可能发生内存值原来是A,中间被改成B,后来又被改成A,此时再使用CAS进行检查时发现没有变化,但是实际上发生了变化,这就是ABA问题。 解决方式: Compartmentalization