大家好,又见面了,我是你们的朋友全栈君。
本人发现网上虽然有不少Java相关的面试题,但第一未必全,第二未必有答案,第三虽然有答案,但未必能在面试中说,所以在本文里,会不断收集各种面试题,并站在面试官的立场上,给出我自己的答案。
第一部分、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、AIO 有什么区别?
- 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分布式集群。
- 实际应用场景里,一般是考虑多并发问题,而不是多并行问题。
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(),但是此方法无法复制函数类型)
这是我的公众号,如果可以,请大家关注下,多多捧场,谢谢了。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/151877.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...