大家好,我是烤鸭:
今天分享一些Java面试题和答案。
这些答案都是自己想的,如果有理解不一样的,欢迎交流。
部分原题来自:
https://blog.csdn.net/qq_41790443/article/details/80694415
1. HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化
首先说一下HashMap是什么,大部分人会说数组+链表,键值允许为null。
我说说我的想法。HashMap内部类Node实现了Entry。四个属性:hash值,next节点地址值,key和value。
是存放在数组中。
HashMap初始化的时候会创建一个默认大小为16的数组,默认负载因子0.75,当数组大小为16*0.75=12时,会发生扩容。
JDK8 优化,HashMap数组初始化会在put方法时检查,如果数组为空,再去创建数组。JDK7 是初始化时都创建好了。
putVal方法中的hash()方法是key的高16位异或低16位实现的,比JDK7 更简单有效。
JDK8 优化,相同hashcode的会接在链表的末尾。当链表长度大于8时,会变成红黑树。JDK7 一直是链表。
JDK8 优化,resize()方法,遍历旧数组,oldTab[j]放入newTab中e.hash & (newCap - 1)的位置,链表重排时,原数组[j]位置上的桶移到了新数组[j+原数组长度]。JDK7 resize(),扩容后链表的顺序与原来相反。
上面红色部分,移位运算:a % (2^n) 等价于 a & (2^n - 1),由于(hashCode中)新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
更多有关于HashMap的内容,推荐两篇博客:
https://www.jianshu.com/p/17177c12f849
https://tech.meituan.com/java_hashmap.html
2. HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小。
发生条件:当 数组中元素个数 超过 数组大小 * 负载因子时,就会发生扩容。
过程简述(JDK 8 为例):源码就不贴了,只是简单说一下过程。
如果原数组大小是0,创建新数组,初始化数组大小和负载因子以及,当前负载因子发生扩容时的数组大小。如果原数组已经超过数组的大小的最大值(2的30次方),就将数组大小置成Integer最大值(2的31次方 - 1)。 如果是 0 - 2的30次方 之间的,扩容2倍。
遍历数组,判断是否是末节点,如果是的话,就把当前元素放到新数组[e.hash & (newCap - 1)]位置上(等价于a % (2^n))。如果不是,判断是否是红黑树节点,如果是树节点,遍历当前节点及后续节点,判断是否 (e.hash & bit == 0) (bit是原数组大小,添加节点时 e.hash & bit 完全是随机的,所以判断末尾是0还是1,来决定是高位还是低位节点)。不是树节点的情况(其实类似树节点),也是判断(e.hash & oldCap == 0),原数组[j]位置上的桶移到了新数组[j]。(e.hash & oldCap != 0),原数组[j]位置上的桶移到了新数组[j+原数组长度]。
关于2的n次幂:a % (2^n) 等价于 a & (2^n - 1)。
HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
3. HashMap,HashTable,ConcurrentHashMap的区别。
HashMap 允许一个NULL键和多个NULL值。非线程安全。HashMap实现线程安全可以采用:Collections.synchronizedMap(map)
HashTable 不允许NULL键和NULL值。线程安全。使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。
ConcurrentHashMap(jdk7) 允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
(jdk8) 摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。
并发的多线程使用场景中使用HashMap可能造成死循环(jdk7),HashTable效率太低不适合在高并发情况下使用,应该使用线程安全的ConcurrentHashMap。
https://www.cnblogs.com/zq-boke/p/8654539.html
4. 极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
ConcurrentHashMap 性能更好。
jdk 1.8以后,HashTable已经淘汰了,并发时使用一把锁处理并发问题,当有多个线程访问时,需要多个线程竞争一把锁,导致阻塞。
jdk 1.7 ConcurrentHashMap则使用分段,相当于把一个HashMap分成多个,然后每个Segment分配一把锁,这样就可以支持多线程访问。
jdk 1.8 ConcurrentHashMap取消segments字段,直接采用transient volatile HashEntry<K,V> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
并发控制使用Synchronized和CAS来操作。JDK8中的实现也是锁分离思想,只是锁住的是一个node,而不是JDK7中的Segment;锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程安全的。
5. HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。
jdk1.7 在并发的多线程使用场景中使用HashMap可能造成死循环,put过程中的resize方法在调用transfer方法的时候导致的死锁。
jdk1.8 会将原来的链表结构保存在节点e中,然后依次遍历e,根据hash&n是否等于0,分成两条支链,保存在新数组中。
但是有可能出现数据丢失的情况。
6. java中四种修饰符的限制范围。
private 本类 default 本包下其他类 protected 不同包下子类 public 不同包下非子类
7. Object类中的方法
equals
hashCode
toString
getClass
notify
notifyAll
wait * 3
8. 接口和抽象类的区别,注意JDK8的接口可以有实现。
抽象类,子类继承,关系是“是一种”。接口,子类实现,关系是“有一种”。
以jdk 1.8为例:
抽象类的方法可以声明default修饰方法,interface只能用public static修饰。
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
接口中不能含有静态代码块,而抽象类可以有静态代码块。
一个类只能继承一个抽象类,而一个类却可以实现多个接口。
9. 动态代理的两种方式,以及区别。
JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
CGlib动态代理:利用ASM(开源的Java字节码编辑库,操作字节码)开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
区别:JDK代理只能对实现接口的类生成代理;CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,这种通过继承类的实现方式,不能代理final修饰的类。
1. JDK代理使用的是反射机制实现aop的动态代理,CGLIB代理使用字节码处理框架asm,通过修改字节码生成子类。
所以jdk动态代理的方式创建代理对象效率较高,执行效率较低,cglib创建效率较低,执行效率高;
2. JDK动态代理机制是委托机制,具体说动态实现接口类,在动态生成的实现类里面委托hanlder去调用原始实现类方法。
CGLIB则使用的继承机制,具体说被代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。
https://blog.csdn.net/weixin_36759405/article/details/82770422
10. Java序列化的方式。
序列化就是把Java对象储存在某一地方(硬盘、网络),也就是将对象的内容进行流化(二进制)。
Java Serialization(主要是采用JDK自带的Java序列化实现,性能很不理想)
Json(目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,还有谷歌的Gson)
Hession(它基于HTTP协议传输,使用Hessian二进制序列化,对于数据包比较大的情况比较友好。)
Dubbo Serialization(阿里dubbo序列化)
FST(高性能、序列化速度大概是JDK的4-10倍,大小是JDK大小的1/3左右)
Protocol Buffer(Google出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 强)
kryo(比kyro更高效的序列化库就只有google的protobuf了)
关于kryo,https://blog.csdn.net/eguid_1/article/details/79316403
11. 传值和传引用的区别,Java是怎么样的,有没有传值引用。
java函数中的参数都是传递值的,所不同的是对于基本数据类型传递的是参数的一份拷贝,对于类类型传递的是该类参数的引用的拷贝。
当在函数体中修改参数值时,无论是基本类型的参数还是引用类型的参数,修改的只是该参数的拷贝,不影响函数实参的值,如果修改的是引用类型的成员值,则该实参引用的成员值是可以改变的。
https://www.cnblogs.com/zhangj95/p/4184180.html
12. 一个ArrayList在循环过程中删除,会不会出问题,为什么。
这里只考虑单线程的问题,ArrayList线程不安全。
循环方式不同,结果不同:
增强for循环,它对索引的边界值只会计算一次,使用list的remove方法会改变modCount值,校验和expectedModCount不一 样,有可能抛出ConcurrentModificationException。
普通for循环,由于remove方法会调用System.arraycopy,改变数组元素排序,有可能抛出IndexOutOfBoundsException。
iterator循环,iterator.remove()方法来移除元素是没有问题的。迭代器的remove方法每次会expectedModCount值改成modCount。
参考:https://www.cnblogs.com/hupu-jr/p/7891844.html
13. @Transactional注解在什么情况下会失效,为什么。
方法不是public的
异常类型是不是unchecked异常(解决方案:rollbackFor=Exception.class)
数据库引擎要支持事务,如果是MySQL,注意表要使用支持事务的引擎,比如innodb,如果是myisam,事务是不起作用的
是否开启了对注解的解析
spring是否扫描这个包
同一个类中的方法调用
异常被catch
14. List 和 Set 的区别
都是实现Collection接口的。
List:1.可以允许重复的对象。
2.可以插入多个null元素。
3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set:1.不允许重复对象
2. 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
3. 只允许一个 null 元素
4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
详细的解释:
https://www.cnblogs.com/IvesHe/p/6108933.html
15. HashSet 是如何保证不重复的
HashSet内部是HashMap,调用set的add方法就是调用map的put方法。
我们知道HashMap的key是不重复的。
根据hashCode和equals判断是否重复。
16. Java反射机制?功能?实际使用?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
部分信息是source阶段不清晰,需要在runtime阶段动态临时加载。
获取class对象:
1 ClassName.getClass() 2 ClassName.class 3 Class.forName(String className);
获取构造方法和创建实例:
clazz.getConstructors()(构造方法)
constructor.newInstance()(创建对象)。
获取共有或私有方法:
clazz.getDeclaredMethod获取方法,invoke(newInstance,"")调用方法。
获取注解:
clazz.getAnnotations();
最常见应用:
利用反射获取配置文件内容。
对象转Map,对象转json。
文件复制。
spring(IOC 和 aop),Hibernate 和mybatis 很多框架用到了。
反射可以跳过泛型检查。
https://blog.csdn.net/yongjian1092/article/details/7364451
17. Arrays.sort 实现原理和 Collection 实现原理
当调用Arrays.sort(Object[] objects)时,先调用的是归并的sort方法。
对于归并排序的改进:
以上方法对给定数组的指定区间内的数据进行排序,同时允许调用者提供用于归并排序的辅助空间。
实现思路为:首先检查数组的大小,如果数组比较小(286),则直接调用改进后的快速排序完成排序,
如果数组较大,则评估数组的无序程度,如果这个数组几乎是无序的,那么同样调用改进后的快速排序算法排序;
如果数组基本有序,那么采用归并排序算法对数组进行排序。
·对于快速排序的改进:
该算法的实现了一种称为“DualPivotQuicksort”的排序算法,中文可以翻译为“双枢轴快速排序”,可以看作是经典快速排序算法的变体。
算法的基本思路是:如果数组的数据量较少(47),则执行插入排序就可以达到很好的效果,如果数据量较大,
那么确定数组的5个分位点,选择一个或两个分位点作为“枢轴”,然后根据快速排序的思想进行排序。
Collections.sort时:
调用的也是Arrays.sort,再将排好序的值set进去。
Array.sort调用的是TimSort,TimSort算法是一种起源于归并排序和插入排序的混合排序算法。
如果数组小于MIN_MERGE(32),则调用binarySort,使用二分查找的方法将后续的数插入之前的已排序数组。
大于MIN_MERGE:
选取minRun(数组长度右移,直到小于 MIN_MERGE)。
找到初始升序序列,如果降序,会对其翻转。
若这组区块大小小于minRun,则将后续的数补足,利用binarySort 对run 进行扩展。
入栈需要合并的数组。
合并数组,重复以上的步骤。
Arrays.sort :
https://blog.csdn.net/octopusflying/article/details/52388012
Collections.sort:
https://blog.csdn.net/bruce_6/article/details/38299199
https://www.jianshu.com/p/1efc3aa1507b
18. LinkedHashMap的应用
HashMap是无序的,LinkedHashMap是有序的,且默认为插入顺序。
LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。
HashMap无序;LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。
LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
LinkedHashMap是线程不安全的。
https://www.jianshu.com/p/8f4f58b4b8ab
19. cloneable接口实现原理
实现clone接口的类,调用clone方法,属于深拷贝。
复制出来的对象属于不同的地址,改变复制对象的属性值不会影响原对象。
浅拷贝的话,拷贝的对象和原有的对象指向同一个地址值,改变其中的属性值,
另一个也会改变。
https://blog.csdn.net/u013916933/article/details/51590332
20. 数组在内存中如何分配
数组引用变量是存放在栈内存(stack)中,数组元素是存放在堆内存(heap)中。
数组初始化分为静态初始化(在定义时就指定数组元素的值,此时不能指定数组长度,否则就出现了静态加动态混搭初始化数组了)
动态初始化(只指定数组长度,由系统分配初始值,初始值根据定义的数据类型来)。
堆中变量没有引用会等待垃圾回收。
栈中变量会在脱离作用域后释放。
https://blog.csdn.net/lcl19970203/article/details/54428358
https://www.cnblogs.com/duanxz/p/6102583.html
21. BlockingQueue的使用及实现
利用 LinkedBlockingQueue 实现生产者和消费者队列,伪代码如下:
//消费者
class Consumer implements Runnable{private BlockingQueue<String> queue;public Consumer(BlockingQueue<String> queue) {this.queue = queue;}public void run() {while (true) {String data = queue.poll(2, TimeUnit.SECONDS);}}
}
//生产者
class Producer implements Runnable{private BlockingQueue queue;public Producer(BlockingQueue queue) {this.queue = queue;}public void run() {while (true) {//延迟2s入数据queue.offer(data, 2, TimeUnit.SECONDS);}}
}
//测试类
TestBlockingQueue{@Testpublic void test(){BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);Producer producer1 = new Producer(queue);Producer producer2 = new Producer(queue);Producer producer3 = new Producer(queue);Consumer consumer = new Consumer(queue);// 借助ExecutorsExecutorService service = Executors.newCachedThreadPool();// 启动线程service.execute(producer1);service.execute(producer2);service.execute(producer3);service.execute(consumer);}
}
http://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html
22. BIO、NIO、AIO区别
Blocking IO(同步阻塞)
面向流,阻塞。
Non-Blocking IO(同步非阻塞)
面向块(buffer),非阻塞。
非阻塞IO模型:
如果没有数据,立即返回EWOULDBLOCK。
用户线程不断轮询(polling)内核,是否有数据。
有数据后,由OS拷贝数据到内核缓冲区,再由内核缓冲区拷贝到用户线程缓冲区。
IO复用模型:
(select + pool 无差别的轮询方式)
多个I/O的阻塞复用到同一个select阻塞上。
由多路复用器不断轮询(poll) I/O线程,看看是否有数据。
如果没有数据,就把当前线程阻塞,如果有数据,就唤醒,轮询一遍所有的流。
(epool 最小轮询方式)
通过epoll方式来观察多个流,epoll只会把发生了I/O事件的流通知我们。
时间复杂度降低为O(k),k为发生事件的流的个数。
如果所有的IO都是短连接且事件发生的比较快,epoll和select + poll效率差不多。
epoll相比于select/poll的优势:
监视的描述符数量不受限制,所支持的FD上限是最大可以打开文件的数目;
I/O效率不会随着监视fd的数量增长而下降。
epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。
信号驱动模型:
类似epoll,需要开启Socket的信号驱动式I/O功能,通过sigaction系统调用来安装一个信号处理函数。
当有数据的时候,内核就为该进程产生一个SIGIO信号,通过该信号的值做处理。
Asynchronous IO
收到用户请求,立刻返回,不会阻塞用户进程。
等待数据完成后,将数据拷贝到用户内存,然后发出一个信号。
https://blog.csdn.net/historyasamirror/article/details/5778378
https://www.jianshu.com/p/db5da880154a
https://www.jianshu.com/p/439e8b349f48
23. 事务隔离级别 和 事务传播级别
五大隔离级别:
ISOLATION_DEFAULT(默认级别)
ISOLATION_READ_UNCOMMITTED(读未提交,可能导致脏读、幻读、重复读)
ISOLATION_READ_COMMITTED (读已提交,可能导致幻读、重复读)
ISOLATION_REPEATABLE_READ(不可重复读,可能导致幻读)
ISOLATION_SERIALIZABLE (通过锁表避免以上情况)
七个传播级别:
PROPAGATION_REQUIRED:如果没有事务就新建一个。
PROPAGATION_SUPPORTS:按当前事务执行,如果当前没有事务,就按没事务的方式执行。
PROPAGATION_MANDATORY:表示该方法必须运行在一个事务中。如果当前没有事务正在发生,将抛出一个异常。
PROPAGATION_REQUIRES_NEW:创建新的事务,如果存在事务,旧的事务会挂起。
PROPAGATION_NOT_SUPPORTED:以无事务的方式运行。
PROPAGATION_NESTED: 如果已有事务,就嵌套事务。没有的话,按 PROPAGATION_REQUIRED 处理。
ISOLATION_DEFAULT:使用数据库的默认隔离级别。
24. http的七层协议
网络七层协议由下往上分别为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
TCP/IP五层模型:应用层、传输层、网络层、数据链路层和物理层。
https://blog.csdn.net/a5582ddff/article/details/77731537
25. ArrayList怎么实现扩容
新的容量,原来容量的1.5倍。
调用Arrays.copyOf()。
https://blog.csdn.net/eases_stone/article/details/79843851