一. 谈谈你对Java平台的理解? "Java 是解释执行",这句话正确么?
典型回答:
Java本身是一种面向对象的语言,最显著的特点有两个方面,一个是所谓的"书写一次,到处运行";能够非常容易地获得跨平台能力;另外就是垃圾收集器(GC),Java通 过垃圾收集器回收分配内存,大部分情况下,程序员不需要自己操心内存的分配和回收。我们日常接触到JRE或者JDK。JRE也就是Java运行环境,包含了JVM和Java类库,以及一些模块等。而JDK可以看作是JRE的一个超集,提供了更多的工具,比如编译器各种诊断工具。
"对于Java是解释执行"这句话,这个说法不准确。我们开发的Java的源代码,首先通过Javac编译成为字节码,然后在运行时通过Java虚拟机内嵌的解释器将字节码转换为最终的机器码。但是常见的JVM,比如我们大数据情况使用的0racleJDK提供的HostpotJVM,提供了JIT编译器,就是通常所说的动态编译器,JIT能够在运行时将热点代码(高频调用的方法和代码块)编译成机器码,这种情况下部分热点就属于编译执行,而不是解释执行。这样类似于缓存技术,运行时在遇到热点代码可以直接执行,而不是先解释在执行。
知识扩展:
在运行时 , JVM通过类加载器加载字节码,解释或者编译执行。就像我们前面提到的,主流的Java版本中,如JDK8实际是解释和编译混合的一种模式,即所谓的混合模式。JIT编译器分为多种模式(Server模式C1ient模式AOT模式)通常运行在Server模式的JVM,会进行上万次调用以收集足够的信息进行高效编译,client模式这个门限是1500次。
Oracle Hostpot JVM内置了两个不同的JIT compiler,C1对应 前面说的client模式,适用于对于启动速度敏感的应用,比如普通Java桌面应用;C2对应Server模式,它的优点是为长时间运行的服务器端应用设计的。
Java虚拟机启动时, 可以指定不同的参数对运行模式进行选择。比如,执行"-Xint",就是告诉JVM只进行解释执行,不对代码进行编译,这种模式抛弃了JIT可能带来的性能优势。毕竟解释器是逐条读入,逐条解释运行的。与其相对性的,还有一个"Xcomp"参数,这是告诉JVM关闭解释器,不要进行解释执行,或者叫做最大优化级别。那你可能会问这种模式是不是最高效啊?简单来说,还真未必。"-Xcomp" 会导致JVM启动变慢非常多..
除了日常最常见的Java使用模式,其实还有一种新的编译方式,即所谓的AOT,直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销,比如Oracle JDK 9就引入了实性质的AOT特性,并且增加了jaotc工具。
另外,JVM作为一个强大的平台,不仅仅只有Java语言可以运行在JVM上,本质上合规的字节码都可以运行,Java语言自身也为此提供了便利,我们可以看到类似ClojureScala Groovy JRuby Jython等 大量JVM语言,活跃在不同的场景。
二. 请对比Exception和Error,另外,运行时异常与一般异常有什么区别?
典型回答:
Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出或者捕获,它是异常处理机制的基本组成类型。Exceptoin和Error体现了Java平台设计者对于不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可以并且应该被捕获,进行相应处理。Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致处于非正常的不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如.OutofMemoryError之类,都是Error的子类。
Exceptoin又分为可检查异常和不检查异常,可检查异常在代码里必须显示地进行捕获处理,这是编译器检查的一部分。不检查异常就是所谓的运行时异常,类似于Nul1PointerException ArrayIndex0utofBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来进行判断是否需要捕获,并不会在编译期强制要求。
知识扩展:
在开发中尽量不要捕获类似Exceptio这样的通用异常,而是应该捕获特定异常.这是因为我们在日常的开发和合作中,我们读代码的机会往往超过写代码,软,件工程是门协作的艺术,所以我们有义务让自己的代码能够直接地体现出尽量多的信息,而泛泛的Exception之类,恰恰隐藏了我们的目的。另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,你可能更希望RuntimeException 被扩散出来,而不是被捕获。
在开发中不要生吞异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设!如果我们不把异常抛出来,或者也没有输出日志之类,程序可能在后续代码以不可控的方式结束。没有人能够轻易判断究竟是哪里出了异常,以及是什么原因产生了异常。
在开发中不要输出标准错误(STERR),因为有时候你很难判断出到底输出到哪里去了。尤其是分布式系统,如果发生异常,但是无法找到堆栈轨迹,这纯属是为诊断设置障碍。所以最好使用产品日志,详细地输出到日志系统里。
Throw early,catch late。在开发中可能会出现各种情况,比如获取配置失败之类的。在发现问题的时候,第一时间抛出,能够更加清晰地反映问题,这是Throw early。 catch late就是 我们经常烦恼的问题,捕获异常后,需要怎么处理?最差的方式,就是我们前面提到的"生吞异常",本质上就是掩盖问题。如果实在不知道如何处理,可以选择保留原有异常的cause信息,直接再抛出或者构建新的异常抛出去。在更高层,因为有了清晰的(业务)逻辑,往往会更清楚合适的处理方式是什么。
有时候,我们会根据需要自己定义异常,这个时候除了保证提供足够的信息,还需要考虑两点。一是否需要定异常CheckedException,因为这种类型的设计初衷是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类。在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能会导致潜在的安全问题。如果我们看Java的标准类库,你可能注意到类似java. net. ConnectException,出错信息是类似"Connection refused", 而不包含具体的机器名IP端口等,一个重要的考量就是信息安全。类似的情况在日志系统中也有,比如,用户数据一般是不可以输出到日志里面的。
try-catch代码段会产生额外的性能开销,或者换个角度来说,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段.尽量不要一个大的try包住整段代码,与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句要低效。
额外:
NoClassDeF oundError和ClassNotFoundException的区别?
首先NoClassDeFoundError是一个错误,ClassNotFoundException是一个异常。ClassNotFoundException的产生原因,Java支持使用Class. froName方法来动态地加载类,任意一个类的类名如果被作为参数传递给这个方法都将导致该类被加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时拋出ClassNotFoundException异常。
另外还有一个导致ClassNotFoundException的原因就是,当一个类已经被某个类加载器加载到内存中,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。
NoClassDeFoundError产生的原因在于:如果JVM或者ClassLoader实例尝试加载类的时候找不到类的定义。例如要查找的类在编译的时候是存在的,运行的时候,找不到了。这个时候就会导致NoClassDefFoundError.造成该问题的原因可能是打包过程中漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法就是查找那些在开发期间存在与类路径下,但在运行期间却不在类路径下的类。
三. 对比Hashtable、HashMap、TreeMap有什么不同?
典型回答:
HashTable HashMap TreeMap都是最常见的一些Map实 现,是以键值对的形式存储和操作数据的容器类型。
HashTable是早期Java类库提供的一个哈希表实现,本身是同步的,不支持nu11键和值,由于同步导致的性能开销,所以已经很少被推荐使用。
HashMap是应用更加广泛的哈希表实现,行为大致.上与HashTable- -致,主要区别在于HashMap不是同步的,支持nul1键和值等。通常情况下, HashMap进行put或者get操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存储场景的首选,比如,实现一个用户ID和用户信息对应的运行时存储结构。
TreeMap则是基于红黑树的一种提供顺序访问的Map,和HashMap不同,它的getremove之类操作都是0(long(n)的时间复杂度,具体顺序可以由指定的Comparator来决定或者根据键的自然顺序来判断。
知识扩展:
HashMap实现原理是经常被问到的一个问题,以下基于JDK1.8分析。
1. 内部存储
HashMap的内部存储是一个数组,数组的元素Node实现了Map.Entry接口(hash, key, value, next) , next非空时指向定位相同的另一个Entry,如图:
JDK 8之前,其内部是由数组+链表来实现的,而JDK 8对于链表长度超过8的链表将转储为红黑树。
2. 容量(capacity)和负载因子(loadFactor)
简单的说, capacity就是数组的大小,loadFactory就是数组填满程度的最大比列。当数组中的元素的数目大于capaci ty*loadFactor时就需要扩容,调整数组的大小为当前的2 倍。同时初始化容量的大小也是2的次幂(大于等于设定容量的最小次幂),则数组的大小在扩容前后都将是2的次幂。默认的容量为16,负载因子为0.75。
3. put方法的大致的思路
- 如果key的值为null,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。
- 如果key不为null,获取key的hashCode值,经过indexFor()方法运算得到的值作为标识,但由于hashCode的值并不唯一,经过运算获取的值也不能保证唯一(哈希冲突),所以,经过以上运算得来的数值只能作为数组的索引。
- 当通过索引定位到这个节点时,在遍历该链表,判断是否存在相同的key对象,如果存在就用新的value覆盖旧的value
- 如果不存在,就创建一个Entry对象添加到table[i]处,如果table[i]已经存在其他元素,那么新Entry对象将会保存在链表的表头,通过next指针指向原有的Entry对象,形成链表结构。
- 当链表的结构太长时(默认超过8个元素),链表就会转为红黑树。
4. get方法的大致的思路
- 对key进行nu11检查。如果key是nu11,table[0]这个位置的元素将被返回
- key的hashcode() 方法被调用,然后计算hash值。
- indexFor (hash, table. length)用来计算要获取的Entry对象在table数组中的精确的位置(使用刚才计算的hash值)
- 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。
四. ArrayList Vector LinkedList的 区别?
典型回答:
这三者都是实现集合框架中的List,也就是所谓的有序集合,因此具体功能也比较类似,比如都提供按照位置进行定位添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别在行为性能线程安全等方面,表现又有很大不同。
Vector是Java早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector内 部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组以满时,会创建新的数组,并拷贝原有数组数据。
ArrayList是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector近似,ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区别, Vector在扩容时会提高1倍,而ArrayList则是增加50%。
LinkedList顾明思议是Java提供的双向链表,所以它不需要像.上面两种那样调整容量,它也不是线程安全的。
Vector和ArrayList作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随即访问的场合。除了尾部出入元素和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。而LinkedList进行节点插入删除却要高效得很多,但是随即访问性能则要比动态数组慢。
文末福利
限于篇幅,今日份大厂面试真题到此为止;需要的更多的Java后端面试题、视频学习资料、架构师成长路线图的朋友点击下方传送门, 即可免费领取面试资料和视频学习资料
传送门
笔者整理的面试题包含但不限于Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技术
以下是笔者整理的部分面试题截图