八股文指的是什么(程序员八股文指的是什么)
最近发现网上虽然有不少 Java 相关的面试题,但第一未必全,第二未必有答案,第三虽然有答案,但未必能在面试中说,所以在本文里,会不断收集各种面试题,并站在面试官的立场上,给出我自己的答案。
文章中面试题均整理成 PDF,需要的学习的小伙伴可关注小编后私信的自取! 创作不易请各位看官多多支持点赞 ~
第一部分、Java 基础
1.JDK 和 JRE 有什么区别?
JDK 是 java 的开发工具包,有 JDK8,9 甚至到 14 的差别,安装以后,不仅包含了 java 的开发环境,比如 java.exe,还包含了运行环境(jre)相关包。
JRE 是 java 运行环境,一般装好 JDK 后,系统里会有对应的 JRE 环境。
2.说下你对== 和 equals 的认识,它们有什么差别?
对于==
基本类型,比如 int 等,==比较的是值是否相同;
引用类型,比如自定义对象:比较地址是否相同;
尤其地,对常量,由于常量被放在常量池里管理,所以对 String 等常量,==也是比较值
对于 equals 方法
对于 String,ArrayList 等,equals 方法是比较值;
但在 Object 里,equals 还是比较地址;
如果自己创建了一个类,但没有重写 equals 方法,还是会比较地址
3.如果两个对象的 hashCode 值一样,则它们用 equals()比较也是为 true,是不是?
不是
hashCode 是定义在 HashMap 里,用以快速索引;
Object 里,hashCode 和 equals 是两个不同的方法,默认 hashCode 是返回对象地址,equals 方法也是对比地址;
两者不是一回事,可以通过重写对象的 hashCode 方法,让不同值的对象有相同的 hashCode,但它们的 equals 方法未必相同
4.综合说下 final 的作用
修饰在类上,该类不能被继承。
修饰在方法上,该方法不能被重写。
修饰在变量上,叫常量,该常量必须初始化,初始化之后值就不能被修改,而常量一般全都是用大写来命名。
5.Math.round(-2.5) 等于多少?
结果是-2,因为该函数在数轴上,表现是向右取整,由此 Math.round(1.3) = 2。
6.String 是基本数据类型吗?
String 不是基础类型,基础类型有 8 种:byte、boolean、char、short、int、float、long、double,而 String 是对象。
但说到这里,你要多说句。
String s = "abc";,这是常量,放常量池管理。
不建议频繁对 String 修改,因为会产生内存碎片。
7.对字符串的都有哪些方法?详细说明下。
具体有 String、StringBuffer 和 StringBuilder 这三个类。
String 是不可变类,每次操作都会生成新的 String 对象,并将结果指针指向新的对象,由此会产生内存碎片。
如果要频繁对字符串修改,建议采用 StringBuffer 和 StringBuilder。
StringBuffer 和 StringBuilder 的差别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,由于无需维护线程安全的操作,所以 StringBuilder 的性能要高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。由于大多数环境下是单线程,所以大多是用 StringBuilder。
8.String str="abc"与 String str=new String("abc")的定义方法一样吗?
不一样,String str="abc"的方式,java 虚拟机会将其分配到常量池中;所以建议这种写法。
而 String str=new String("abc") 则会被分到堆内存中,如果再频繁修改,会导致内存碎片。
9.如何将字符串反转?
使用 StringBuilder 或 stringBuffer 的 reverse() 方法。
10.String 类的常用方法都有那些?
indexOf():返回指定字符的索引。
length():返回字符串长度。
equals():字符串比较。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring():截取字符串。
你面试时,说出其中的一两个即可,但需要说明如下的意思。
String s = "abc";,这是常量,放常量池管理。
不建议频繁对 String 修改,因为会产生内存碎片。
如果要频繁对字符串修改,建议采用 StringBuffer 和 StringBuilder
11.抽象类必须要有抽象方法吗?
不需要的,抽象类不一定非要有抽象方法。但从面向对象思想角度来分析,不建议这样做。
因为在设计的时候,会把逻辑上存在但实际不存在的类设置成抽象类,比如动物类,毕竟不能直接展示动物。
正因为不存在,所以里面的方法未必能实现,比如奔跑方法,所以此类方法需要设置成没方法体的抽象方法。
如果在抽象类里方法,全都有方法体,那么要么是抽象类设计不当,或者实现了未必能实现的方法,所以建议修改。
12.一般的类和抽象类有哪些区别?
一般的类不能包含没有方法体的抽象方法,而抽象类可以包含抽象方法。
抽象类不能直接用 new 来实例化,普通类可以直接实例化。
13.抽象类能使用 final 修饰吗?
首先说明,语法上不能,然后再进一步从面向对象思想角度来说明。
定义抽象类的本意是,让其它类继承的,从而进一步完善对象。如果定义为 final 该类就不能被继承,这样就会有矛盾,所以 final 不能修饰抽象类。
14.接口和抽象类有什么区别?
抽象类的子类要用 extends 来继承;而实现接口要用 implements 。
抽象类可以定义构造函数,而接口不能。
抽象类里可以定义 main 方法,但接口不能有 main 方法。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
上述是从语法上来归纳,然后建议大家再从面向对象思想的角度来说明
抽象类是对逻辑的归纳,比如动物类可以是抽象类,人类可以 extends 动物这个抽象类。
而接口是对功能的归纳,比如可以定义一个提供数据库访问功能的 接口,在其中封装若干操作数据库的方法。
15.java 中 IO 流分为几种?
按功能来分可以分输入流(input)和输出流(output)。从类型来分可以是字节流和字符流。
16.BIO、NIO、1AIO 有什么区别?
BIO 的英语全称是 Block IO, 同步阻塞式 IO,就是平常经常使用的传统 IO,特点是简单方便,但并发处理能力低。
NIO,叫 New IO, 同步非阻塞 IO,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
AIO,Asynchronous IO, 是 NIO 的升级,实现了异步非堵塞 IO ,它是基于事件和回调机制。
17.Files 的常用方法都有哪些?
Files.exists():检测路径是否存在。
Files.createFile():创建文件。
Files.createDirectory():创建文件夹。
Files.delete():删除文件或文件夹。
Files.copy():复制文件。
Files.move():移动文件,即复制后删除。
Files.size():查看文件的个数。
Files.read():读取文件。
Files.write():写入文件。
第二部分,Java 的集合,也叫容器
18.java 的集合容器都有哪些?
如下给出了大致的结构
所有线性表对象的父类是 Collection
有线性表类,比如 ArrayList 和 Set 等。
有键值对类,比如 HashMap。
19.Collection 和 Collections 有什么区别?
Collection 是一个集合接口,是所有线性表对象的父类。
Collections 是集合类的一个工具类,包含了对集合元素进行排序和线程安全等各种操作方法。
20.List、Set、Map 之间的区别是什么?
21.HashMap 和 Hashtable 有什么区别?
首先说,两者都是键值类的对象
HashTable 线程安全的,而 HashMap 线程不安全的,大多数的场景是单线程环境,在单线程环境下,HashMap 效率上比 hashTable 要高。
HashMap 允许空键值,而 hashTable 不允许。
22.如何决定使用 HashMap 还是 TreeMap?
对于在 Map 中进行插入、删除和定位元素这类操作,可以选 HashMap。但如果你要对一个有序的 key 集合进行遍历,需要选 TreeMap。
23.说一下 HashMap 的实现原理?
HashMap 是基于数据结构里的散列表,在大数据情况下,能保证 get 的高效性。
HashMap 不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap 实际上是一个链表散列的数据结构,即数组和链表的结合体。
当向 Hashmap 对象里 put 元素时,会根据 key 的 hashcode 计算 hash 值,根据 hash 值得到这个元素在数组中的位置,如果该数组在该位置上已经存放了其他元素,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放入链尾.如果数组中该位置没有元素,就直接将该元素放到数组的该位置上。
注意 Jdk 1.8 中对 HashMap 的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的 O(n)到 O(logn)
24.说一下 HashSet 的实现原理?
HashSet 在底层上,是由 HashMap 实现的
HashSet 的值放在 HashMap 的 key 上
HashMap 的 value 统一为 PRESENT
25.ArrayList 和 LinkedList 的区别是什么?
ArrrayList 底层实现的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是双向循环链表,不支持随机访问。
使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
26.如何做到数组和 List 之间的转换?
List 对象转换成为数组:可以调用 ArrayList(或其它 List)的 toArray 方法。
数组转换成为 List:调用 Arrays 的 asList 方法。
27.ArrayList 和 Vector 的区别是什么?(面试大概率会问)
Vector 是线程安全的,而 ArrayList 不是。所以在单线程情况下,建议使用 ArrayList
在扩容时,Vector 是扩容 100%,但 ArrayList 是 50%,后者更节省内存
结论:大多数开发场景是单线程环境,所以建议使用 ArrayList
28.Array 和 ArrayList 有何区别?
Array 能容纳基本数据类型和自定义对象,而 ArrayList 只能容纳自定义的对象,对于基本数据类型,需要转换成封装类才能存储。
Array 是指定大小的,要手动扩容,而 ArrayList 大小虽然可以在定义时指定,但遇到容量满时会自动扩容。
Array 没有提供 ArrayList 那么多功能,比如 addAll、removeAll 和 iterator 等。
所以建议使用 ArrayList
29.在 Queue 中 poll()和 remove()有什么区别?
poll() 和 remove() 都是从队列中取出一个元素,但是 poll() 在获取元素失败的时候会返回空,但是 remove() 失败的时候会抛出异常。
30.哪些集合类是线程安全的?
Vector:就比 arraylist 多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在 web 应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
Statck:堆栈类,先进后出,项目中用得并不多。
Hashtable:就比 hashmap 多了个线程安全,所以建议使用 HashMap。
enumeration:枚举,所以现在建议用 Iterator 来迭代。
结论是,如果在单线程情况下,不建议使用这些线程安全对象。
31.迭代器 Iterator 是什么?
迭代器是一种设计模式,也是一个对象,可以用来遍历并选择序列(比如 ArrayList 或 HashMap)中的对象,而开发人员不需要了解该序列的底层结构。
迭代器通常被称为轻量级对象,因为创建它的代价小。
32.Iterator 怎么用?有什么特点?
Iterator 比较好用,而且只能单向移动:
(1) 使用方法 iterator()要求容器返回一个 Iterator。第一次调用 Iterator 的 next()方法时,它返回序列的第一个元素。比如 list.iterator()
(2) 用 next()得到序列中的下一个元素。
(3) 使用 hasNext()检查是否还有其它元素。
(4) 使用 remove()将迭代器新返回的元素删除。但不建议一遍迭代一边删除,有可能引发并发问题。
Iterator 是 Java 迭代器最简单的实现,为 List 设计的 ListIterator 具有更多的功能,它可以从两个方向遍历 List,也可以从 List 中插入和删除元素。
33.Iterator 和 ListIterator 有什么区别?
Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历 List。
Iterator 对集合只能是前向遍历,ListIterator 既可以前向也可以后向。
ListIterator 实现了 Iterator 接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引等等,但在实际用的时候,大多也是只用到迭代的功能。
一般只建议使用 Iterator,别区分地对 List 对象用 ListIterator。
第三部分、多线程
35.并行和并发有什么区别?
并行是指两个或多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上同时处理多个任务,在多台处理器上同时处理多个任务。如 hadoop 分布式集群。
和饺子面的窍门:在1斤面粉里掺入6个蛋清,使面里蛋白质增加,包的饺子下锅后蛋白质会很快凝固收缩,饺子起锅后收水快,不易粘连。
实际应用场景里,一般是考虑多并发问题,而不是多并行问题。
36.线程和进程的区别?
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程,但一个进程一般有多个线程。
进程在运行过程中,需要拥有独立的内存单元,否则如果申请不到,就会挂起。而多个线程能共享内存资源,这样就能降低运行的门槛,从而效率更高。
线程是是 cpu 调度和分派的基本单位,在实际开发过程中,一般是考虑多线程并发。
37.守护线程是什么?
守护线程(daemon thread),是个服务线程,用来监视和服务其它线程。
38.创建线程有哪几种方式?
①. 继承 Thread 类创建线程类
通过 extends Thread 定义 Thread 类的子类,并重写该类的 run 方法。
创建 Thread 子类的实例,并调用线程对象的 start()方法来启动该线程。
②. 通过 Runnable 接口创建线程类
implements Runnable 接口的实现类,并重写该接口的 run()方法。
创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。
调用线程对象的 start()方法来启动该线程。
③. 通过 Callable 和 Future 创建线程
创建 Callable 接口的实现类,并实现 call()方法,该 call()方法将作为线程执行体,并且有返回值。
创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call()方法的返回值。
使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值。
另外,还有通过线程池来创建线程
39.说一下 runnable 和 callable 有什么区别?
Runnable 接口中的 run()方法的返回值是 void,在其中可以定义线程的工作任务,但无法返回值。
Callable 接口中的 call()方法是有返回值的,是一个泛型,一般会和 Future、FutureTask 配合,能异步地得到线程的执行结果。
40.线程有哪些状态?
线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。
创建状态。创建好线程对象,并没有调用该对象的 start 方法,此时线程处于创建状态。
就绪状态。当调用线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,也就是说还没进入运行状态。或者在线程运行之后,从等待或者睡眠状态中回来之后,也会处于就绪状态,等待被调度进入运行状态。
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个实践的发生(比如说某项资源就绪)之后再继续运行。wait 方法都可以导致线程阻塞。
死亡状态。如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪
41.sleep() 和 wait() 有什么区别?
sleep():这是线程类(Thread)的静态方法,让线程进入睡眠状态,等休眠时间结束后,线程进入就绪状态,和其他线程一起竞争 cpu 的执行时间。
因为 sleep() 是 static 静态的方法,他不能改变对象的机锁,当一个 synchronized 块中调用了 sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象,这时就会引发问题,此类现象请注意。
wait():wait()是 Object 类的方法,当一个线程执行到 wait 方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过 notify,notifyAll 方法来唤醒等待的线程。
42.notify()和 notifyAll()有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁,而且被 wait 的线程,无法自动再进入到唤醒状态。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了 notify 后只要一个线程会由等待池进入锁池,而 notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
43.线程的 run()和 start()有什么区别?
每个线程都是通过运行自身 run()来完成其操作的。而一般要通过调用 Thread 类的 start()方法(不是 run 方法)来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。
然后通过此 Thread 类调用方法 run()来完成其运行状态, Run 方法运行结束后, 此线程终止。然后 CPU 再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用 run(),其实就相当于是调用了一个普通函数而已,而不是以多线程的方式来运行。
总之,在多线程执行时要使用 start()方法而不是 run()方法。
44.创建线程池有哪几种方式?
①. newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
②. newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。
③. newSingleThreadExecutor()
这是一个单线程的 Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
④. newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer。
45.线程池都有哪些状态?
线程池有 5 种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:
46.线程池中 submit()和 execute()方法有什么区别?
接收的参数不一样
submit 有返回值,而 execute 没有
submit 方法能进行 Exception 处理
47.在 java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before 原则)。
48.多线程锁的升级原理是什么?
在 Java 中,锁共有 4 种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
但是在实际开发过程中,宁可用到类或者组件自身带的锁管理机制,因为这经历过其它项目的考验,比较可靠,别自己定义各种锁,更别自己定义锁的升级策略,因为这部分的代码没完整测试过,很容易引发问题。
49.什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而导致相互等待,由此代码无法继续下。此时称系统处于死锁状态或系统产生了死锁。
50.怎么防止死锁?
死锁的四个必要条件:
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。
如上是万金油类的正确的废话,理论层面这样说总不会错,那么在实际操作中是怎么做的?
1 比如在数据库的数据隔离级别方面,别设太高,否则很容易引发数据库里的等待,乃至死锁。
2 预防死锁的代价要比监控死锁的代价大很多,所以系统里一般是监控+解决,比如用监控系统(Cat 等),看是否有长时间运行的 SQL 语句或线程,这可以预先设置,比如运行时间超过 60 秒就报警,然后人工介入。
3 代码在上线前,在测试环境充分压力测试,发现死锁点再解决。上线后,不能保证一定没死锁,一般也是采用监控+人工解决的方式。
51.ThreadLocal 是什么?有哪些使用场景?
这是线程局部变量,属于线程自身私有,不在多个线程间共享。
Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。
请注意,任何线程局部变量一旦在工作完成后没有释放,Java 就会有内存泄露乃至 OOM 的风险。
52.说一下 synchronized 底层实现原理?
synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。
Java 中每一个对象都可以作为锁,这是 synchronized 实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的 class 对象
同步方法块,锁是括号里面的对象
一旦有线程对上述对象加锁,那么其它线程进入前就会检查并等待,等到锁释放后再进入。
53.synchronized 和 volatile 的区别是什么?
volatile 是在告诉 jvm 当前变量在自身线程的内存区里,值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。
volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性。
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
54.synchronized 和 Lock 有什么区别?
synchronized 是 java 关键字,Lock 是个 java 类;
synchronized 无法判断是否获取锁的状态,Lock 可以判断是否获取到锁;
synchronized 会自动释放锁,Lock 需在 finally 中手工释放锁(unlock()方法释放锁);
用 synchronized 关键字的两个线程 1 和线程 2,如果当前线程 1 获得锁,线程 2 线程等待。如果线程 1 阻塞,线程 2 则会一直等待下去,而 Lock 锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized 的锁可重入、不可中断、非公平,而 Lock 锁可重入、可判断、可公平(两者皆可);
Lock 锁适合大量同步的代码的同步问题,synchronized 锁适合代码少量的同步问题。
结论:synchronized 很重,而且大多只能加在单个方法上,而 Lock 可以作用在调用多个业务的方法上,使用起来比较简便。
55.synchronized 和 ReentrantLock 区别是什么?
synchronized 是关键字,ReentrantLock 是类,这是二者的本质区别。
ReentrantLock 是类,所以 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
ReentrantLock 比 synchronized 的扩展性体现在几点上:
ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
ReentrantLock 可以获取各种锁的信息
ReentrantLock 可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的:ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word。
56.说一下 atomic 的原理?
Atomic 包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型封装类)变量进行操作时,具有排斥性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic 系列的类中的核心方法都会调用 unsafe 类中的几个本地方法。我们需要先知道一个东西就是 Unsafe 类,全名为:sun.misc.Unsafe,这个类包含了大量的对 C 代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的
这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过 unsafe 分配内存的时候,如果自己指定某些区域可能会导致一些类似 C++一样的指针越界到其他进程的问题。
结论,你面试时说说就行了,实际项目里用的话需要非常谨慎。
第四部分、反射
57.什么是反射?
反射主要是指程序可以访问、检测和修改它本身状态或行为的一种能力
反射的物质基础是 Class 类,其中 C 是大写的。
Java 反射:
在 Java 运行时环境中,对于任意一个类,能否知道这个类有哪些属性和方法?对于任意一个对象,能否调用它的任意一个方法
Java 反射机制主要提供了以下功能:
在运行时判断任意一个对象所属的类。
烙饼不粘锅窍门:烙饼时,在平底锅上撒点盐,就不容易粘锅。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法。
58.什么是 java 序列化?什么情况下需要序列化?
序列化是指,把 Java 对象转换成一个字节序列,以便传输。
什么情况下需要序列化:
a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)想在网上传输时,比如想用套接字传输或用 RMI 或 Dubbo 调用时;
59.动态代理是什么?有哪些应用?
动态代理:
当想要给实现了某个接口的类中的方法,加一些额外的处理。比如说加日志,加事务等,就可以给这个类创建一个代理,这个代理类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新类。这个代理类并不是定义好的,是动态生成的。具有解耦合性。
动态代理的应用,非常典型就 Spring 里的 AOP,以及各种注解
60.怎么实现动态代理?
1 必须定义一个接口
2 定义 InvocationHandler(将实现接口的类的对象传递给它)处理类。
3 定义一个代理类 Proxy(因为调用 newInstance()可以产生代理对象,其实他只是一个产生代理对象的工具类)。
4 利用到 InvocationHandler,拼接代理类源码,将其编译生成代理类的二进制码,利用加载器加载,并将其实例化产生代理对象,最后返回。
五、对象拷贝
61.为什么要使用克隆?
用一个对象得到了大量的数据,需要对此处理,但同时又想保存原来的数据,就需要对原数据进行克隆操作。
62.如何实现对象克隆?
有两种方式:
1). 实现 Cloneable 接口,并重写其中的 clone()方法,在里面定义克隆动作;
2). 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
请注意,基于序列化和反序列化实现的克隆不仅是深度克隆,更重要的是通过泛型限定,把对象里包含的子对象也克隆出来,同时检查克隆出来的对象是否支持序列化,而这种检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对象,毕竟让问题在编译的时候暴露出来总是好过把问题留到运行时。
63.深拷贝和浅拷贝区别是什么?
浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝,浅拷贝可能会引发潜在的数据修改问题
深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()和 JSON.stringify(),但是此方法无法复制函数类型)
举报/反馈
桔皮妙用:将粥煮至半熟时加入两块鲜桔皮,煮熟的粥生津开胃,格外香甜。沏茶时加入几丝桔皮条,饮服时清香爽口,润肺祛痰。