这 5 道 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 中,异常主要分为两类:Exception 和 Error,这两类都有一个共同的祖先 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块不会被执行:
在finally语句块中发生了异常。
在前面的代码中用了System.exit()退出程序。
程序所在的线程死亡。
关闭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
往期:
听说爱学习的人都关注了