查看原文
其他

这 5 道 Java 面试题,你还真不一定懂。

帅地 苦逼的码农 2019-03-29

回顾之前的一些面试题:

Java集合、数组与泛型中的几个陷阱,你掉进了几个?

关于集合中一些常考的重点知识点总结

Java面试题(二):你真的懂这几道题了吗?

Java面试题及其解答(一)

String 和 StringBuffer,StringBuilder 的区别是什么?

1. 可变性上

String 字符串的本质,就是在 String 类内部维护了一个字符数组

并且这个数组被 final 修饰,因此 String 是一个不可变对象

而 StringBuffer  和 StringBuilder 都继承于 AbstractStringBuilder,不过在 AbstractStringBuilder 里面,存放字符串的字符数组并没有被 final 修饰


因此 StringBuffer 和 StringBuilder 对象是可变的。

2. 线程安全上

由于String 是不可变的,很显然 String 是线程安全的。StringBuffer 和 StringBuilder 都继承了 AbstractStringBuilder 的一些公共方法,不过在在重写的时候,StringBuffer 对这些方法加了同步锁

所以 StringBuffer 是线程安全的,而 StringBuilder 则没有加同步锁,所以 StringBuilder 是线程不安全的。

3. 性能上

由于 String 是不可变对象,所以每次我们对 String 对象内容进行修改的时候,例如

1String a = "a";
2a = a + "bc";

那么,都行生成一个新的对象,新对象的内容为"abc"。这里我给大家解释下

1a = a + "bc";

这段代码究竟发生了什么,它是怎么生成 "abc"这个对象的。

其实是这样的,这段代码运行的时候,编译器会创建一个 java.lang.StringBuilder 对象,然后会调用 StringBuilder 对象的 append 方法,把 "a" 和 "bc" 链接起来,最后在调用 toString() 方法,转为 String 对象,调用 toString 的时候,会生成一个新的对象。

所以,执行 a = a + "bc" 这样的操作是花销相对大了点的,要特别注意在一个循环里避免出现这样的操作,例如:

1String a = "a";
2for(int i = 0; i < 100; i++){
3    a = a + i;
4}

这个100次循环,会创建 100 个 StringBuilder对象。

在性能方面,StringBuffer 和 StringBuilder 则可以对自身内容进行操作,不过由于 StringBuffer 是线程安全的,所以性能会差点,而 StringBUilder 性能会好点。

Object 祖先的一些常见方法总结(有时候会问,常见方法最好知道)

1、

1public final native Class<?> getClass()

native方法,用于返回当前运行时对象的Class对象。由于final修饰,所以不可以被重写。

2、

1public boolean equals(Object obj)

用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

3、

1protected native Object clone() throws CloneNotSupportedException
2

naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,拷贝的对象和原对象内容完全相同,只是内存地址不一样,并且对于任何对象 x,表达式 x.clone() != x 为true,
x.clone().getClass() == x.getClass() 为true。

Object 本身没有实现 Cloneabl e接口,所以不重写clone
方法并且进行调用的话会发生 CloneNotSupportedException 异常。这里要特别注意,重写的时候,如果该对象的成员含有成员对象的话,如果仅仅是调用 super.clone 的话,拷贝的对象中,里面的成员对象并没有被拷贝到,还是共用同一个成员对象,所以我们一般需要进行深度克隆

4、

1public String toString()

返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

5、

1public final native void notify()
2

native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
6、

1public final native void notifyAll()
2

native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

7、

1public final native void wait(long timeout) throws InterruptedException
2
3public final void wait(long timeout, int nanos) throws InterruptedException
4
5public final void wait() throws InterruptedException
6

native方法,并且不能重写。暂停线程的执行。一般采用 wait 和 notify 这两个方法互相配合的方式,来实现线程的 等待/唤醒继续机制。

其中,没有参数的表示一直等待,有参数 timeout 的表示等到一定的时间,就自动不在等待了。

nanos 这个参数,表示额外要等待的时间。并且需要注意都是timeout的单位为毫秒, 参数nanos 的单位为纳秒,提供nanos这个参数,主要是为了能够更加精确着控制时间。

8、

1protected void finalize() throws Throwable { }作

实例被垃圾回收器回收的时候触发该方法,主要用来对象要被回收时,需要做的一些后续处理。

异常体系梳理

先来个整体图观摩一下

在Java 中,异常主要分为两类:ExceptionError,这两类都有一个共同的祖先 Throwable类。

Error:该异常往往是一些比较严重的异常,并且这种异常不可捕获,是一种程序无法处理的错误。例如内存不够用时的 OutOfMemoryError、Java 虚拟机运行时错误(VirtualMachineError)。

Exception:这种异常是可以捕捉的,写程序时能够预测到的,所以这种异常是程序本身可以处理的。Exception 有一个比较重要的子类估计就是 RuntimeException,常见的具体错误有 空指针(NullPointerException),数组访问越界等。

总结:Error 错误是程序不可处理的,而 Exception 是程序可以处理的。

try-catch-finally总结

  • try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。

  • catch 块:用于处理try捕获到的异常。

  • finally 块:无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return语句
    时,finally语句块将在方法返回之前被执行。

特别注意,finally块不会被执行:

  1. 在finally语句块中发生了异常。

  2. 在前面的代码中用了System.exit()退出程序。

  3. 程序所在的线程死亡。

  4. 关闭CPU。

HashMap 的容量为什么是 2 的幂次方

HashMap 的底层原理是 数组 + 链表,当我们进行 put() 操作的时候,需要根据 key 来获取哈希码,一般获取的操作如下

1int hash = hash(key.hashCode())

注意,key.hashCode() 获得该 key 的哈希码之后,我们还必须做一次 hash() 操作,这样能够减少冲突。

当我们获得 hash 之后,可以通过取余的方式来决定这个 key 是放在数组的哪个位置,即 hash%length,其中length表示数组的长度。

不过,可能是这种方式效率不高, SUN大师们发现, “当容量一定是2^n时,存在 h & (length - 1) == h % length”,并且这种 按位运算特别快,所以,容量为 n 的幂次方,可以提高运算速度。当然,加快运算可能只是其中原因之后,可能还有其他原因。

拓展
当我们指定了初始容量为 initCapatity 时,那么系统就会把初始容量设置为比 initCapatity 大并且这个数是 2 的幂次方。而且实现发方法值得学习,源码如下:

1/** 
2 * Returns a power of two size for the given target capacity. 
3 */
  
4static final int tableSizeFor(int cap) {  
5    int n = cap - 1;  
6    n |= n >>> 1;  
7    n |= n >>> 2;  
8    n |= n >>> 4;  
9    n |= n >>> 8;  
10    n |= n >>> 16;  
11    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  
12

ConcurrentHashMap 在 JDK1.7 与 JDK 1.8 的区别

1、底层数据结构:JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树

这里我说一下JDK1.8之后为何会出现红黑树,其实是这样的,当链表很多之后,就会影响查询操作,所以到了 JDK1.8之后,当链表的长度到了一定的阈值,就会把链表转换为红黑树,默认阈值为 8。

2、实现线程安全的方式(重要):在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap。

图片展示:
JKD1.7

JDK1.8


往期:

一文读懂一台计算机是如何把数据发送给另一台计算机的

速领!!送54本实体书,包邮!


听说爱学习的人都关注了

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存