高级、资深Java面试题,附答案!需要高薪的你请拿去
有一段时间没更新了,在这个金九银十的跳槽季,献给大伙奉上一套热腾腾的互联网面试题,来自朋友的面试经历(2面技术+1面hr),由于朋友还没入职,不让我透露具体公司,但是绝对是大厂不假(想知道可以先下偷偷告诉你,或者让朋友给你内推),笔者收集整理了一下,废话不多说,直接进入正题。
以下给出了每一题思路及方向,后面还需要小伙伴们自己用户揣摩,希望能给你们一下帮助。
java基础篇
1、有哪些集合实现,对应的线程安全集合?
主要考察集合框架的两大类:集合(collection)和图(map),集合下面主要了解list和set,如ArrayList、LinkedList、HashSet、TreeSet;map下主要了解HashMap、TreeMap。
划重点(加分项):
① 线程安全集合选两个代表性的:ConcurrentHashMap和CopyOnWriteArrayList,了解它们是如何做到线程安全的。
2、hashmap底层结构,为什么使用链表?(hashmap是面试必考题)
HashMap的底层结构(数组)实现,一定要了解透彻,如put()、get()两块源码花点心思;
hash碰撞后产生的链表、红黑树顺带了解,有能力的最好看下源码实现(加分项)。
3、线程池常用参数、如何设置?
核心线程数、最大线程数、阻塞队列容量、线程空闲时间及单位、拒绝策略,每个的具体含义一定要了解。
划重点:
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满:
若线程数小于最大线程数,创建线程若线程数等于最大线程数,看拒绝策略
如何设置具体参数值(个人经验及看法,供参考):
核心线程数 = 每秒需要处理的最大任务数量 * 处理一个任务需要的时间,业界有常用的8020原则、6040原则,实际还是要根据自己应用的承载能力来看,假设系统每秒任务数为10~100,每个任务耗时0.1秒,则需要10*0.1~100*0.1,即1~10个线程。那么corePoolSize应该设置为大于1,若根据8020原则,即80%情况下系统每秒任务数不超过20,则corePoolSize=0.1*20=2。
阻塞队列容量 = 核心线程数 / 处理一个任务需要的时间 * 系统允许任务最大的响应时间
最大线程数 = (每秒需要处理的最大任务数量 – 队列容量)*每秒的线程处理任务能力,假设每秒200个任务需要20个线程,那么当每秒达到1000个任务时,则需要(1000-队列容量)*(20/200),即60个线程。
4、阻塞队列的实现原理?
lock锁的多条件(condition)阻塞控制,基础薄弱的需要提前了解一下同步锁synchronized、lock及Condition类,这一块考验高级和中级研发人员的界限,需要挖一下源码,多用心。
加分项:另外辐射出来可能会顺带直接问你AQS,建议仔细了解其实现原理,如其内部如何利用双向链表的,重点看acquire()方法实现。
JVM篇
1、内存模型(必考题)
无论你关注jdk1.8之前还是之后,不重要,关键是能正常理解每一块的作用、是否线程共享/私有。
堆、虚拟机栈、本地方法栈、程序计数器、方法区,需要了解每一部分的作用。
划重点:
虚拟机栈包含哪些部分、程序计数器的作用这两块着重了解。
2、GC发生在哪一块,GC算法?
发生在堆内存,GC算法主要了解:标记清除、复制、标记整理,需要知道其执行步骤及各自的区别。
划重点:
年轻代划分成哪几部分(E、S0、S1),使用哪种算法(复制),老年代使用哪种算法(标记清除),为什么?这里需要耐心理解透彻,很容易辐射到此类问题。
MySql篇
1、事务隔离级别,默认哪一个,解释幻读、不可重复读及二者区别?
隔离级别:读未提交、读提交、不可重复读、串行化,默认是不可重复读,具体幻读问题,请参考笔者此前文章幻读、不可重复读,其中进行了详细说明。
2、索引失效场景、索引数据结构?
主要分为非组合索引和组合索引:
1、非组合索引主要注意or、like(若like非左模糊情形,如xxx%,则可以使用索引),索引列存在表达式、聚合函数等等;
2、组合索引主要注意是否遵循最左原则。
还有些其它小点靠大家自己去积累,笔者这会是蒙圈状态,没反应过来。
索引数据结构:主要关注B+树,花心思了解透,且需要了解聚簇索引和非聚簇索引。
加分项:InnoDB与MyisAM索引方面的区别。
3、数据库引擎
主要可以说说InnoDB、MyISAM这两个常用的即可,需要了解二者的区别,InnoDB支持事务,二者使用场景,二者之间如何转变。
4、mysql分表中间件、如何监听binlog
中间件:tddl,MyCat(其实考验你的知识储备广度,一般不要求细说中间件的实现)。
监听时可以使用duckula中间件或者binlog监听独立jar包(common-binlog-alone)。
5、笔者认为高级或资深人员必须要了解的知识储备:InnoDB的MVCC机制
mysql自身的并发控制如何实现的,原理主要从其新增、更新、删除三方面去了解其内部的操作过程,其版本号如何控制等等。
中间件篇
1、消息队列用过哪些:kafka、rabbitMq、rocketMq?
以kafka为例,一定要能手绘kafka的逻辑结构图,知识点主要被问到两方面:
零拷贝:磁盘文件的数据复制到页面缓存中,然后将数据从页面缓存直接发送到网络中,不用重复从磁盘取数据。
kafka延时队列怎么实现:时间轮,需要细致了解其如何将消息遍布其中并且延时取出。
怎么保证消息有序:这个有序的前提是在partition层面,其中的消息在写入时都是有序的,消费时,每个partition只能被每一个group中的一个消费者消费,进而保证了其有序性。
kafka如何保证消息高可用:着重去了解ISR机制和复制机制,很难细说清楚,需要花时间了解。
kafka的事务是如何实现:三两句是说不清的,主要从幂等性发送和事务性保证两方面来回答,具体给几个小点供参考(1、引入内部Topic作为事务log;2、引入transactionId做同一事务判断;3、引入事务协调者;4、引入控制消息让broker通知消费者消息是否被原子性提交,并对使用者透明,不可见),参考点并不完全,需要读者用心自行完善。
加分项:
若使用过不止一种消息队列,最好能知晓其之间的差异或者优劣势。
2、搜索中间件使用过哪些,ES?
ES索引结构:index/type/id,类比关系型数据库:index=database,type=table,id=id。
ES分片:主要了解主分片(不可修改)与副分片(可以修改),主分片与对应的副分片不能存放同一个节点下,查询时主、副都是查询,es会择优返回结果。另外,分片路由算法了解一下:分片=hash(routing)%主分片数,routing一般可以使用id。
笔者建议还要了解下ES中的删除其实不是物理删除,具体如何删的有兴趣可以自行了解,属于加分项
3、缓存使用过什么,redis?
redis常用数据类型:String、List、hash、set、zset等等,使用场景最好也事先想清楚,100%会被问到。
redis集群架构模式:哨兵模式、主从模式、cluster模式,具体原理需要细致了解,大概率会被问到。
redis如何实现延时消息:此题考验临时给定场景的解决问题能力,其实笔者还是觉得考察的是对redis数据类型的熟悉程度。笔者给的思路是使用zset类型,将延时的时间点作为score进行存放,使用时启用一个轮询任务按照允许的时间间隔进行轮询取出一定量的数据,使用zrangeByScore方法根据score排好序的结果,使用完记得zrem删除避免重复消费,当然其中需要你控制好如何在删除之前避免并发的重复消费。
1、对于分表之后的业务常用数据,比如订单分表后,要查某段时间内的订单数据,如何实现?
本题不能直接将一张张分表查询一遍,效率太低,偏离实际使用场景的时效性和性能,所以考虑引入中间件(比如ES),可以将业务热度较高的数据抽取出来按照用户常用查询维度进行组合存放,当实际请求过来时,直接通过es即可获取相应结果。
注:这种方式也适用于DB的减压,若问到DB如何减压也可以这么回答。
2、比如下单之后有很多相应的数据都要同步修改,但是这类数据都不在同一个应用服务和数据库中,有什么方式可以达到数据的最终一致性?
这一题笔者给出两种思路(笔者倾向于后一种方式):
①使用消息,不同数据对应其所在的服务进行消息监听,进而获取消息后进一步修改,当然不能排除消息丢失的情况,所以需要进一步完善;
②可以监听binlog,当下单数据变更后,直接通过监听到的binlog的数据变更结果之后,根据具体需要进行对应的后续处理。
3、cup飙升如何排查?
具体说下思路:
① 先要ps -ef | grep java获得java进程,然后使用top命令获取cpu使用较高的线程;
② 使用jstack -l pid > /tmp/xxxx.log将堆栈情况输出到文件中便于后续查看;
③将top中cpu使用率较高的线程id转换成16进制去上一步的文件中查找,大多数情况下可以定位出一些眉目;
④如果仍然没有头绪,接着使用jmap导出堆的dump文件,并使用Eclipse的插件进行查看,比如找到问题对象进行实际代码分析。
现场编程
1、将单链表逆序输出,不能改变单链表的结构(比如不允许将单链表改造成双向链表)
这一题笔者给出两个思路:
① 简单点考虑可以借助栈来实现,顺序压栈后直接栈顶顺输出即可;
② 当然,如果要求不允许借助栈来实现,那就需要递归的思想,设置两个指针变量a和b(a->b),分别指向表头前两个节点,使得a指向的节点为b指向的节点的后继,再利用一个临时变量,逐步后移a、b直到b为空即可。
2、电影院选座位,共n个座位,每个座位票价不一样,找出连续的m个座位,使其票价总和最小。
这一题笔者直接理解为那个元素的数组(元素都是数值),找出m个连续的数字使其总和最小,给出起始点的下标即可。
可以直接顺序查找,每次计算出的m个元素的总和值保存在临时变量中,例如若使用map,则key为下标,value为sum值,记住不要过多浪费空间,并不需要每个下标都存放进去,只要存放一个即可,然后依次遍历数组下标,比较sum值(较小的放进map,同时为了保证节省空间,放进map前进行清空map,使map始终只有一个元素),最终能获取到最小的sum,拿出map的为一个键值对,即获取到起始下标。
一般现场编程题不会太难,毕竟不是让你现场ACM,而是着重考察面试者是否保留了过硬的动手能力,而不是只会CURD,但是算法还是需要日常积累才能自如应付。
这里只是笔者个人意见,重点还是关注和事先准备好以下几个方面:
1、职业生涯规划;
2、自身优缺点;
3、离职原因。
最后,希望本文能带给大家一些帮助,斩获心中理想的offer!
20个高级Java面试题汇总
这是一个高级Java面试系列题中的第一部分。这一部分论述了可变参数,断言,垃圾回收,初始化器,令牌化,日期,日历等等Java核心问题。
- 什么是可变参数?
- 断言的用途?
- 什么时候使用断言?
- 什么是垃圾回收?
- 用一个例子解释垃圾回收?
- 什么时候运行垃圾回收?
- 垃圾回收的最佳做法?
- 什么是初始化数据块?
- 什么是静态初始化器?
- 什么是实例初始化块?
- 什么是正则表达式?
- 什么是令牌化?
- 给出令牌化的例子?
- 如何使用扫描器类(Scanner Class)令牌化?
- 如何添加小时(hour)到一个日期对象(Date Objects)?
- 如何格式化日期对象?
- Java中日历类(Calendar Class)的用途?
- 如何在Java中获取日历类的实例?
- 解释一些日历类中的重要方法?
- 数字格式化类(Number Format Class)的用途?
可变参数允许调用参数数量不同的方法。请看下面例子中的求和方法。此方法可以调用1个int参数,或2个int参数,或多个int参数。
断言是在Java 1.4中引入的。它能让你验证假设。如果断言失败(即返回false),就会抛出AssertionError(如果启用断言)。基本断言如下所示。
断言不应该用于验证输入数据到一个public方法或命令行参数。IllegalArgumentException会是一个更好的选择。在public方法中,只用断言来检查它们根本不应该发生的情况。
垃圾回收是Java中自动内存管理的另一种叫法。垃圾回收的目的是为程序保持尽可能多的可用堆(heap)。 JVM会删除堆上不再需要从堆引用的对象。
比方说,下面这个方法就会从函数调用。
通过函数第一行代码中参考变量calendar,在堆上创建了GregorianCalendar类的一个对象。
函数结束执行后,引用变量calendar不再有效。因此,在方法中没有创建引用到对象。
JVM认识到这一点,会从堆中删除对象。这就是所谓的垃圾回收。
垃圾回收在JVM突发奇想和心血来潮时运行(没有那么糟糕)。运行垃圾收集的可能情况是:
- 堆可用内存不足
- CPU空闲
用编程的方式,我们可以要求(记住这只是一个请求——不是一个命令)JVM通过调用System.gc()方法来运行垃圾回收。
当内存已满,且堆上没有对象可用于垃圾回收时,JVM可能会抛出OutOfMemoryException。
对象在被垃圾回收从堆上删除之前,会运行finalize()方法。我们建议不要用finalize()方法写任何代码。
初始化数据块——当创建对象或加载类时运行的代码。
有两种类型的初始化数据块:
静态初始化器:加载类时运行的的代码
实例初始化器:创建新对象时运行的代码
请看下面的例子:static{ 和 }之间的代码被称为静态初始化器。它只有在第一次加载类时运行。只有静态变量才可以在静态初始化器中进行访问。虽然创建了三个实例,但静态初始化器只运行一次。
示例输出
让我们来看一个例子:每次创建类的实例时,实例初始化器中的代码都会运行。
示例输出
正则表达式能让解析、扫描和分割字符串变得非常容易。Java中常用的正则表达式——Patter,Matcher和Scanner类。
令牌化是指在分隔符的基础上将一个字符串分割为若干个子字符串。例如,分隔符;分割字符串ac;bd;def;e为四个子字符串ac,bd,def和e。
分隔符自身也可以是一个常见正则表达式。
String.split(regex)函数将regex作为参数。
如何使用扫描器类(Scanner Class)令牌化?
现在,让我们如何看看添加小时到一个date对象。所有在date上的日期操作都需要通过添加毫秒到date才能完成。例如,如果我们想增加6个小时,那么我们需要将6小时换算成毫秒。6小时= 6 * 60 * 60 * 1000毫秒。请看以下的例子。
格式化日期需要使用DateFormat类完成。让我们看几个例子。
带有区域设置的格式化日期如下所示:
Calendar类(Youtube视频链接 – https://www.youtube.com/watch?v=hvnlYbt1ve0)在Java中用于处理日期。Calendar类提供了增加和减少天数、月数和年数的简便方法。它还提供了很多与日期有关的细节(这一年的哪一天?哪一周?等等)
Calendar类不能通过使用new Calendar创建。得到Calendar类实例的最好办法是在Calendar中使用getInstance() static方法。
在Calendar对象上设置日(day),月(month)或年(year)不难。对Day,Month或Year调用恰当Constant的set方法。下一个参数就是值。
calendar get方法
要获取一个特定日期的信息——2010年9月24日。我们可以使用calendar get方法。已被传递的参数表示我们希望从calendar中获得的值—— 天或月或年或……你可以从calendar获取的值举例如下:
数字格式用于格式化数字到不同的区域和不同格式中。
使用默认语言环境的数字格式
使用区域设置的数字格式
使用荷兰语言环境格式化数字:
使用德国语言环境格式化数字:
使用默认语言环境格式化货币
使用区域设置格式化货币
使用荷兰语言环境格式化货币:
挑战10个最难的Java面试题(附答案)【上】
这是收集的10个最棘手的Java面试问题列表。这些问题主要来自 Java 核心部分 ,不涉及 Java EE 相关问题。你可能知道这些棘手的 Java 问题的答案,或者觉得这些不足以挑战你的 Java 知识,但这些问题都是容易在各种 Java 面试中被问到的,而且包括我的朋友和同事在内的许多程序员都觉得很难回答。
一个棘手的 Java 问题,如果 Java编程语言不是你设计的,你怎么能回答这个问题呢。Java编程的常识和深入了解有助于回答这种棘手的 Java 核心方面的面试问题。
为什么 wait,notify 和 notifyAll 是在 Object 类中定义的而不是在 Thread 类中定义
这是有名的 Java 面试问题,招2~4年经验的到高级 Java 开发人员面试都可能碰到。这个问题的好在它能反映了面试者对等待通知机制的了解, 以及他对此主题的理解是否明确。就像为什么 Java 中不支持多继承或者为什么 String 在 Java 中是 final 的问题一样,这个问题也可能有多个答案。
为什么在 Object 类中定义 wait 和 notify 方法,每个人都能说出一些理由。从我的面试经验来看, wait 和 nofity 仍然是大多数Java 程序员最困惑的,特别是2到3年的开发人员,如果他们要求使用 wait 和 notify, 他们会很困惑。因此,如果你去参加 Java 面试,请确保对 wait 和 notify 机制有充分的了解,并且可以轻松地使用 wait 来编写代码,并通过生产者-消费者问题或实现阻塞队列等了解通知的机制。
为什么等待和通知需要从同步块或方法中调用, 以及 Java 中的 wait,sleep 和 yield 方法之间的差异,如果你还没有读过,你会觉得有趣。为何 wait,notify 和 notifyAll 属于 Object 类? 为什么它们不应该在 Thread 类中? 以下是我认为有意义的一些想法:
1) wait 和 notify 不仅仅是普通方法或同步工具,更重要的是它们是 Java 中两个线程之间的通信机制。对语言设计者而言, 如果不能通过 Java 关键字(例如 synchronized)实现通信此机制,同时又要确保这个机制对每个对象可用, 那么 Object 类则是的正确声明位置。记住同步和等待通知是两个不同的领域,不要把它们看成是相同的或相关的。同步是提供互斥并确保 Java 类的线程安全,而 wait 和 notify 是两个线程之间的通信机制。
2) 每个对象都可上锁,这是在 Object 类而不是 Thread 类中声明 wait 和 notify 的另一个原因。
3) 在 Java 中为了进入代码的临界区,线程需要锁定并等待锁定,他们不知道哪些线程持有锁,而只是知道锁被某个线程持有, 并且他们应该等待取得锁, 而不是去了解哪个线程在同步块内,并请求它们释放锁定。
4) Java 是基于 Hoare 的监视器的思想。在Java中,所有对象都有一个监视器。线程在监视器上等待,为执行等待,我们需要2个参数:一个线程、一个监视器(任何对象)
在 Java 设计中,线程不能被指定,它总是运行当前代码的线程。但是,我们可以指定监视器(这是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其他线程在所需的监视器上等待,这将导致“入侵”,导致在设计并发程序时会遇到困难。请记住,在 Java 中,所有在另一个线程的执行中侵入的操作都被弃用了(例如 stop 方法)。
我发现这个 Java 核心问题很难回答,因为你的答案可能不会让面试官满意,在大多数情况下,面试官正在寻找答案中的关键点,如果你提到这些关键点,面试官会很高兴。在 Java 中回答这种棘手问题的关键是准备好相关主题, 以应对后续的各种可能的问题。
这是非常经典的问题,与为什么 String 在 Java 中是不可变的很类似; 这两个问题之间的相似之处在于它们主要是由 Java 创作者的设计决策使然。
为什么Java不支持多重继承, 可以考虑以下两点:
1)第一个原因是围绕钻石形继承问题产生的歧义,考虑一个类 A 有 foo() 方法, 然后 B 和 C 派生自 A, 并且有自己的 foo() 实现,现在 D 类使用多个继承派生自 B 和C,如果我们只引用 foo(), 编译器将无法决定它应该调用哪个 foo()。这也称为 Diamond 问题,因为这个继承方案的结构类似于菱形,见下图:
即使我们删除钻石的顶部 A 类并允许多重继承,我们也将看到这个问题含糊性的一面。如果你把这个理由告诉面试官,他会问为什么 C++ 可以支持多重继承而 Java不行。嗯,在这种情况下,我会试着向他解释我下面给出的第二个原因,它不是因为技术难度, 而是更多的可维护和更清晰的设计是驱动因素, 虽然这只能由 Java 言语设计师确认,我们只是推测。维基百科链接有一些很好的解释,说明在使用多重继承时,由于钻石问题,不同的语言地址问题是如何产生的。
2)对我来说第二个也是更有说服力的理由是,多重继承确实使设计复杂化并在转换、构造函数链接等过程中产生问题。假设你需要多重继承的情况并不多,简单起见,明智的决定是省略它。此外,Java 可以通过使用接口支持单继承来避免这种歧义。由于接口只有方法声明而且没有提供任何实现,因此只有一个特定方法的实现,因此不会有任何歧义。
另一个类似棘手的Java问题。为什么 C++ 支持运算符重载而 Java 不支持? 有人可能会说+运算符在 Java 中已被重载用于字符串连接,不要被这些论据所欺骗。
与 C++ 不同,Java 不支持运算符重载。Java 不能为程序员提供自由的标准算术运算符重载,例如+, – ,*和/等。如果你以前用过 C++,那么 Java 与 C++ 相比少了很多功能,例如 Java 不支持多重继承,Java中没有指针,Java中没有引用传递。另一个类似的问题是关于 Java 通过引用传递,这主要表现为 Java 是通过值还是引用传参。虽然我不知道背后的真正原因,但我认为以下说法有些道理,为什么 Java 不支持运算符重载。
1)简单性和清晰性。清晰性是Java设计者的目标之一。设计者不是只想复制语言,而是希望拥有一种清晰,真正面向对象的语言。添加运算符重载比没有它肯定会使设计更复杂,并且它可能导致更复杂的编译器, 或减慢 JVM,因为它需要做额外的工作来识别运算符的实际含义,并减少优化的机会, 以保证 Java 中运算符的行为。
2)避免编程错误。Java 不允许用户定义的运算符重载,因为如果允许程序员进行运算符重载,将为同一运算符赋予多种含义,这将使任何开发人员的学习曲线变得陡峭,事情变得更加混乱。据观察,当语言支持运算符重载时,编程错误会增加,从而增加了开发和交付时间。由于 Java 和 JVM 已经承担了大多数开发人员的责任,如在通过提供垃圾收集器进行内存管理时,因为这个功能增加污染代码的机会, 成为编程错误之源, 因此没有多大意义。
3)JVM复杂性。从JVM的角度来看,支持运算符重载使问题变得更加困难。通过更直观,更干净的方式使用方法重载也能实现同样的事情,因此不支持 Java 中的运算符重载是有意义的。与相对简单的 JVM 相比,复杂的 JVM 可能导致 JVM 更慢,并为保证在 Java 中运算符行为的确定性从而减少了优化代码的机会。
4)让开发工具处理更容易。这是在 Java 中不支持运算符重载的另一个好处。省略运算符重载使语言更容易处理,这反过来又更容易开发处理语言的工具,例如 IDE 或重构工具。Java 中的重构工具远胜于 C++。
我最喜欢的 Java 面试问题,很棘手,但同时也非常有用。一些面试者也常问这个问题,为什么 String 在 Java 中是 final 的。
字符串在 Java 中是不可变的,因为 String 对象缓存在 String 池中。由于缓存的字符串在多个客户之间共享,因此始终存在风险,其中一个客户的操作会影响所有其他客户。例如,如果一段代码将 String “Test” 的值更改为 “TEST”,则所有其他客户也将看到该值。由于 String 对象的缓存性能是很重要的一方面,因此通过使 String 类不可变来避免这种风险。
同时,String 是 final 的,因此没有人可以通过扩展和覆盖行为来破坏 String 类的不变性、缓存、散列值的计算等。String 类不可变的另一个原因可能是由于 HashMap。
由于把字符串作为 HashMap 键很受欢迎。对于键值来说,重要的是它们是不可变的,以便用它们检索存储在 HashMap 中的值对象。
由于 HashMap 的工作原理是散列,因此需要具有相同的值才能正常运行。如果在插入后修改了 String 的内容,可变的 String将在插入和检索时生成两个不同的哈希码,可能会丢失 Map 中的值对象。
如果你是印度板球迷,你可能能够与我的下一句话联系起来。字符串是Java的 VVS Laxman,即非常特殊的类。我还没有看到一个没有使用 String 编写的 Java 程序。这就是为什么对 String 的充分理解对于 Java 开发人员来说非常重要。
String 作为数据类型,传输对象和中间人角色的重要性和流行性也使这个问题在 Java 面试中很常见。
为什么 String 在 Java 中是不可变的是 Java 中最常被问到的字符串访问问题之一,它首先讨论了什么是 String,Java 中的 String 如何与 C 和 C++ 中的 String 不同,然后转向在Java中什么是不可变对象,不可变对象有什么好处,为什么要使用它们以及应该使用哪些场景。
这个问题有时也会问:“为什么 String 在 Java 中是 final 的”。在类似的说明中,如果你正在准备Java 面试,我建议你看看《Java程序员面试宝典(第4版) 》,这是高级和中级Java程序员的优秀资源。它包含来自所有重要 Java 主题的问题,包括多线程,集合,GC,JVM内部以及 Spring和 Hibernate 框架等。
正如我所说,这个问题可能有很多可能的答案,而 String 类的唯一设计者可以放心地回答它。我在 Joshua Bloch 的 Effective Java 书中期待一些线索,但他也没有提到它。我认为以下几点解释了为什么 String 类在 Java 中是不可变的或 final 的:
1)想象字符串池没有使字符串不可变,它根本不可能,因为在字符串池的情况下,一个字符串对象/文字,例如 “Test” 已被许多参考变量引用,因此如果其中任何一个更改了值,其他参数将自动受到影响,即假设
现在字符串 B 调用 \”Test\”.toUpperCase(), 将同一个对象改为“TEST”,所以 A 也是 “TEST”,这不是期望的结果。
下图显示了如何在堆内存和字符串池中创建字符串。
2)字符串已被广泛用作许多 Java 类的参数,例如,为了打开网络连接,你可以将主机名和端口号作为字符串传递,你可以将数据库 URL 作为字符串传递, 以打开数据库连接,你可以通过将文件名作为参数传递给 File I/O 类来打开 Java 中的任何文件。如果 String 不是不可变的,这将导致严重的安全威胁,我的意思是有人可以访问他有权授权的任何文件,然后可以故意或意外地更改文件名并获得对该文件的访问权限。由于不变性,你无需担心这种威胁。这个原因也说明了,为什么 String 在 Java 中是最终的,通过使 java.lang.String final,Java设计者确保没有人覆盖 String 类的任何行为。
3)由于 String 是不可变的,它可以安全地共享许多线程,这对于多线程编程非常重要. 并且避免了 Java 中的同步问题,不变性也使得String 实例在 Java 中是线程安全的,这意味着你不需要从外部同步 String 操作。关于 String 的另一个要点是由截取字符串 SubString 引起的内存泄漏,这不是与线程相关的问题,但也是需要注意的。
4)为什么 String 在 Java 中是不可变的另一个原因是允许 String 缓存其哈希码,Java 中的不可变 String 缓存其哈希码,并且不会在每次调用 String 的 hashcode 方法时重新计算,这使得它在 Java 中的 HashMap 中使用的 HashMap 键非常快。简而言之,因为 String 是不可变的,所以没有人可以在创建后更改其内容,这保证了 String 的 hashCode 在多次调用时是相同的。
5)String 不可变的绝对最重要的原因是它被类加载机制使用,因此具有深刻和基本的安全考虑。如果 String 是可变的,加载“java.io.Writer” 的请求可能已被更改为加载 “mil.vogoon.DiskErasingWriter”. 安全性和字符串池是使字符串不可变的主要原因。顺便说一句,上面的理由很好回答另一个Java面试问题: “为什么String在Java中是最终的”。要想是不可变的,你必须是最终的,这样你的子类不会破坏不变性。你怎么看?
另一个基于 String 的棘手 Java 问题,相信我只有很少的 Java 程序员可以正确回答这个问题。这是一个真正艰难的核心Java面试问题,并且需要对 String 的扎实知识才能回答这个问题。
这是最近在 Java 面试中向我的一位朋友询问的问题。他正在接受技术主管职位的面试,并且有超过6年的经验。如果你还没有遇到过这种情况,那么字符数组和字符串可以用来存储文本数据,但是选择一个而不是另一个很难。但正如我的朋友所说,任何与 String 相关的问题都必须对字符串的特殊属性有一些线索,比如不变性,他用它来说服访提问的人。在这里,我们将探讨为什么你应该使用char[]存储密码而不是String的一些原因。
字符串:
1)由于字符串在 Java 中是不可变的,如果你将密码存储为纯文本,它将在内存中可用,直到垃圾收集器清除它. 并且为了可重用性,会存在 String 在字符串池中, 它很可能会保留在内存中持续很长时间,从而构成安全威胁。
由于任何有权访问内存转储的人都可以以明文形式找到密码,这是另一个原因,你应该始终使用加密密码而不是纯文本。由于字符串是不可变的,所以不能更改字符串的内容,因为任何更改都会产生新的字符串,而如果你使用char[],你就可以将所有元素设置为空白或零。因此,在字符数组中存储密码可以明显降低窃取密码的安全风险。
2)Java 本身建议使用 JPasswordField 的 getPassword() 方法,该方法返回一个 char[] 和不推荐使用的getTex() 方法,该方法以明文形式返回密码,由于安全原因。应遵循 Java 团队的建议, 坚持标准而不是反对它。
3)使用 String 时,总是存在在日志文件或控制台中打印纯文本的风险,但如果使用 Array,则不会打印数组的内容而是打印其内存位置。虽然不是一个真正的原因,但仍然有道理。
输出
我还建议使用散列或加密的密码而不是纯文本,并在验证完成后立即从内存中清除它。因此,在Java中,用字符数组用存储密码比字符串是更好的选择。虽然仅使用char[]还不够,还你需要擦除内容才能更安全。
这个 Java 问题也常被问: 什么是线程安全的单例,你怎么创建它。好吧,在Java 5之前的版本, 使用双重检查锁定创建单例 Singleton 时,如果多个线程试图同时创建 Singleton 实例,则可能有多个 Singleton 实例被创建。从 Java 5 开始,使用 Enum 创建线程安全的Singleton很容易。但如果面试官坚持双重检查锁定,那么你必须为他们编写代码。记得使用volatile变量。
为什么枚举单例在 Java 中更好
枚举单例是使用一个实例在 Java 中实现单例模式的新方法。虽然Java中的单例模式存在很长时间,但枚举单例是相对较新的概念,在引入Enum作为关键字和功能之后,从Java5开始在实践中。本文与之前关于 Singleton 的内容有些相关, 其中讨论了有关 Singleton 模式的面试中的常见问题, 以及 10 个 Java 枚举示例, 其中我们看到了如何通用枚举可以。这篇文章是关于为什么我们应该使用Eeame作为Java中的单例,它比传统的单例方法相比有什么好处等等。
Java 枚举和单例模式
Java 中的枚举单例模式是使用枚举在 Java 中实现单例模式。单例模式在 Java 中早有应用, 但使用枚举类型创建单例模式时间却不长. 如果感兴趣, 你可以了解下构建者设计模式和装饰器设计模式。
1) 枚举单例易于书写
这是迄今为止最大的优势,如果你在Java 5之前一直在编写单例, 你知道, 即使双检查锁定, 你仍可以有多个实例。虽然这个问题通过 Java 内存模型的改进已经解决了, 从 Java 5 开始的 volatile 类型变量提供了保证, 但是对于许多初学者来说, 编写起来仍然很棘手。与同步双检查锁定相比,枚举单例实在是太简单了。如果你不相信, 那就比较一下下面的传统双检查锁定单例和枚举单例的代码:
在 Java 中使用枚举的单例
这是我们通常声明枚举的单例的方式,它可能包含实例变量和实例方法,但为了简单起见,我没有使用任何实例方法,只是要注意,如果你使用的实例方法且该方法能改变对象的状态的话, 则需要确保该方法的线程安全。默认情况下,创建枚举实例是线程安全的,但 Enum 上的任何其他方法是否线程安全都是程序员的责任
你可以通过EasySingleton.INSTANCE来处理它,这比在单例上调用getInstance()方法容易得多。
具有双检查锁定的单例示例
下面的代码是单例模式中双重检查锁定的示例,此处的 getInstance() 方法检查两次,以查看 INSTANCE 是否为空,这就是为什么它被称为双检查锁定模式,请记住,双检查锁定是代理之前Java 5,但Java5内存模型中易失变量的干扰,它应该工作完美。
你可以调用DoubleCheckedLockingSingleton.getInstance() 来获取此单例类的访问权限。
现在,只需查看创建延迟加载的线程安全的 Singleton 所需的代码量。使用枚举单例模式, 你可以在一行中具有该模式, 因为创建枚举实例是线程安全的, 并且由 JVM 进行。
人们可能会争辩说,有更好的方法来编写 Singleton 而不是双检查锁定方法, 但每种方法都有自己的优点和缺点, 就像我最喜欢在类加载时创建的静态字段 Singleton, 如下面所示, 但请记住, 这不是一个延迟加载单例:
单例模式用静态工厂方法
这是我最喜欢的在 Java 中影响 Singleton 模式的方法之一,因为 Singleton 实例是静态的,并且最后一个变量在类首次加载到内存时初始化,因此实例的创建本质上是线程安全的。
你可以调用 Singleton.getSingleton() 来获取此类的访问权限。
2) 枚举单例自行处理序列化
传统单例的另一个问题是,一旦实现可序列化接口,它们就不再是 Singleton, 因为 readObject() 方法总是返回一个新实例, 就像 Java 中的构造函数一样。通过使用 readResolve() 方法, 通过在以下示例中替换 Singeton 来避免这种情况:
如果 Singleton 类保持内部状态, 这将变得更加复杂, 因为你需要标记为 transient(不被序列化),但使用枚举单例, 序列化由 JVM 进行。
3) 创建枚举实例是线程安全的
如第 1 点所述,因为 Enum 实例的创建在默认情况下是线程安全的, 你无需担心是否要做双重检查锁定。
总之, 在保证序列化和线程安全的情况下,使用两行代码枚举单例模式是在 Java 5 以后的世界中创建 Singleton 的最佳方式。你仍然可以使用其他流行的方法, 如你觉得更好, 欢迎讨论。
经典但核心Java面试问题之一。
如果你没有参与过多线程并发 Java 应用程序的编码,你可能会失败。
如何避免 Java 线程死锁?
如何避免 Java 中的死锁?是 Java 面试的热门问题之一, 也是多线程的编程中的重口味之一, 主要在招高级程序员时容易被问到, 且有很多后续问题。尽管问题看起来非常基本, 但大多数 Java 开发人员一旦你开始深入, 就会陷入困境。
面试问题总是以“什么是死锁?”开始
当两个或多个线程在等待彼此释放所需的资源(锁定)并陷入无限等待即是死锁。它仅在多任务或多线程的情况下发生。
如何检测 Java 中的死锁?
虽然这可以有很多答案, 但我的版本是首先我会看看代码, 如果我看到一个嵌套的同步块,或从一个同步的方法调用其他同步方法, 或试图在不同的对象上获取锁, 如果开发人员不是非常小心,就很容易造成死锁。
另一种方法是在运行应用程序时实际锁定时找到它, 尝试采取线程转储,在 Linux 中,你可以通过kill -3命令执行此操作, 这将打印应用程序日志文件中所有线程的状态, 并且你可以看到哪个线程被锁定在哪个线程对象上。
你可以使用 fastthread.io 网站等工具分析该线程转储, 这些工具允许你上载线程转储并对其进行分析。
另一种方法是使用 jConsole 或 VisualVM, 它将显示哪些线程被锁定以及哪些对象被锁定。
如果你有兴趣了解故障排除工具和分析线程转储的过程, 我建议你看看 Uriah Levy 在多元视觉(PluraIsight)上《分析 Java 线程转储》课程。旨在详细了解 Java 线程转储, 并熟悉其他流行的高级故障排除工具。
编写一个将导致死锁的Java程序?
一旦你回答了前面的问题,他们可能会要求你编写代码,这将导致Java死锁。
这是我的版本之一
如果 method1() 和 method2() 都由两个或多个线程调用,则存在死锁的可能性, 因为如果线程 1 在执行 method1() 时在 Sting 对象上获取锁, 线程 2 在执行 method2() 时在 Integer 对象上获取锁, 等待彼此释放 Integer 和 String 上的锁以继续进行一步, 但这永远不会发生。
此图精确演示了我们的程序, 其中一个线程在一个对象上持有锁, 并等待其他线程持有的其他对象锁。
你可以看到, Thread1 需要 Thread2 持有的 Object2 上的锁,而 Thread2 希望获得 Thread1 持有的 Object1 上的锁。由于没有线程愿意放弃, 因此存在死锁, Java 程序被卡住。
其理念是, 你应该知道使用常见并发模式的正确方法, 如果你不熟悉这些模式,那么 Jose Paumard 《应用于并发和多线程的常见 Java 模式》是学习的好起点。
如何避免Java中的死锁?
现在面试官来到最后一部分, 在我看来, 最重要的部分之一; 如何修复代码中的死锁?或如何避免Java中的死锁?
如果你仔细查看了上面的代码,那么你可能已经发现死锁的真正原因不是多个线程, 而是它们请求锁的方式, 如果你提供有序访问, 则问题将得到解决。
下面是我的修复版本,它通过避免循环等待,而避免死锁, 而不需要抢占, 这是需要死锁的四个条件之一。
现在没有任何死锁,因为两种方法都按相同的顺序访问 Integer 和 String 类文本上的锁。因此,如果线程 A 在 Integer 对象上获取锁, 则线程 B 不会继续, 直到线程 A 释放 Integer 锁, 即使线程 B 持有 String 锁, 线程 A 也不会被阻止, 因为现在线程 B 不会期望线程 A 释放 Integer 锁以继续。
任何序列化该类的尝试都会因NotSerializableException而失败,但这可以通过在 Java中 为 static 设置瞬态(trancient)变量来轻松解决。
Java 序列化相关的常见问题
Java 序列化是一个重要概念, 但它很少用作持久性解决方案, 开发人员大多忽略了 Java 序列化 API。根据我的经验, Java 序列化在任何 Java核心内容面试中都是一个相当重要的话题, 在几乎所有的网面试中, 我都遇到过一两个 Java 序列化问题, 我看过一次面试, 在问几个关于序列化的问题之后候选人开始感到不自在, 因为缺乏这方面的经验。
他们不知道如何在 Java 中序列化对象, 或者他们不熟悉任何 Java 示例来解释序列化, 忘记了诸如序列化在 Java 中如何工作, 什么是标记接口, 标记接口的目的是什么, 瞬态变量和可变变量之间的差异, 可序列化接口具有多少种方法, 在 Java 中,Serializable 和 Externalizable 有什么区别, 或者在引入注解之后, 为什么不用 @Serializable 注解或替换 Serializalbe 接口。
关注 点击下方,第一时间了解华为云新鲜技术~
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。