21 道 Java 基础面试题及答案,Dubbo SPI 及自适应扩展原理
4. Object 有哪些公用方法?
(1)clone
保护方法,实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常
(2)equals
在 Object 中与==是一样的,子类一般需要重写该方法
(3)hashCode
该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法。这个方法在一些具有哈希功能的 Collection 中用到
(4)getClass
final 方法,获得运行时类型
(5)wait
使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生:?
1. 其他线程调用了该对象的 notify 方法?
2. 其他线程调用了该对象的 notifyAll 方法?
3. 其他线程调用了 interrupt 中断该线程?
4. 时间间隔到了?
此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常
(6)notify
唤醒在该对象上等待的某个线程
(7)notifyAll
唤醒在该对象上等待的所有线程
(8)toString
转换成字符串,一般子类都有重写,否则打印句柄
5. Java 的四种引用,强弱软虚,用到的场景。
(1)强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
如下:
Object o=new Object(); // 强引用
当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:
o=null; // 帮助垃圾收集器回收此对象
显式地设置 o 为 null,或超出对象的生命周期范围,则 gc 认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于 gc 的算法。
举例:
public void test(){
Object o=new Object();
// 省略其他操作
}
在一个方法的内部有一个强引用,这个引用保存在栈中,而真正的引用内容(Object)保存在堆中。当这个方法运行完成后就会退出方法栈,则引用内容的引用不存在,这个 Object 会被回收。
但是如果这个 o 是全局的变量时,就需要在不用这个对象时赋值为 null,因为强引用不会被垃圾回收。
强引用在实际中有非常重要的用处,举个 ArrayList 的实现源代码:
private transient Object[] elementData;
public void clear() {
modCount++;
// Let gc do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
在 ArrayList 类中定义了一个私有的变量 elementData 数组,在调用方法清空数组时可以看到为每个数组内容赋值为 null。不同于 elementData=null,强引用仍然存在,避免在后续调用 add()等方法添加元素时进行重新的内存分配。使用如 clear()方法中释放内存的方法对数组中存放的引用类型特别适用,这样就可以及时释放内存。
(2)软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
String str=new String("abc"); // 强引用
SoftReference<String> softRef=new SoftReference<String>(str); // 软引用
当内存不足时,等价于:
If(JVM.内存不足()) {
str = null; // 转换为软引用
System.gc(); // 垃圾回收器进行回收
}
软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
(1)如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
(2)如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
这时候就可以使用软引用
Browser prev = new Browser(); // 获取页面进行浏览
SoftReference sr = new SoftReference(prev); // 浏览完毕后置为软引用
if(sr.get()!=null){
rev = (Browser) sr.get(); // 还没有被回收器回收,直接获取
}else{
prev = new Browser(); // 由于内存吃紧,所以对软引用的对象回收了
sr = new SoftReference(prev); // 重新构建
}
这样就很好的解决了实际的问题。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
3、弱引用
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。当你想引用一个对象,但是这个对象有自己的生命周期,你不想介入这个对象的生命周期,这时候你就是用弱引用。这个引用不会在对象的垃圾回收判断中产生任何附加的影响。比如说 Thread 中保存的 ThreadLocal 的全局映射,因为我们的 Thread 不想在 ThreadLocal 生命周期结束后还对其造成影响,所以应该使用弱引用,这个和缓存没有关系,只是为了防止内存泄漏所做的特殊操作。
4、幽灵引用(虚引用)
**虚引用主要用来跟踪对象被垃圾回收器回收的活动。**虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存后,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存回收后采取必要的行动。由于 Object.finalize()方法的不安全性、低效性,常常使用虚引用完成对象回收后的资源释放工作
。当你创建一个虚引用时要传入一个引用队列,如果引用队列中出现了你的虚引用,说明它已经被回收,那么你可以在其中做一些相关操作,主要是实现细粒度的内存控制。比如监视缓存,当缓存被回收后才申请新的缓存区。
6. hashcode 的作用。
答案:hashCode 用于返回对象的散列值,用于在散列函数中确定放置的桶的位置。
hashCode 的存在主要是用于查找的快捷性,如 Hashtable,HashMap 等,hashCode 是用来在散列存储结构中确定对象的存储地址的;
如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同;
如果对象的 equals 方法被重写,那么对象的 hashCode 也尽量重写,并且产生 hashCode 使用的对象,一定要和 equals 方法中使用的一致,否则就会违反上面提到的第 2 点;
两个对象的 hashCode 相同,并不一定表示两个对象就相同,也就是不一定适用于 equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如 Hashtable,他们**“存放在同一个篮子里”**。
7. ArrayList、LinkedList、Vector 的区别。
Arraylist 和 Vector 是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以插入数据慢,查找有下标,所以查询数据快,Vector 由于使用了 synchronized 方法-线程安全,所以性能上比 ArrayList 要差,LinkedList 使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,但是插入数据时只需要记录本项前后项即可,插入数据较快。
8. String、StringBuffer 与 StringBuilder 的区别。
答案:
**String 字符串常量
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全)**
简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。
String s1 = "aaaaa";
String s2 = "bbbbb";
String r = null;
int i = 3694;
r = s1 + i + s2;
通过查看字节码可以发现,JVM 内部使用的是创建了一个 StringBuilder 完成拼接工作
StringBuffer
Java.lang.StringBuffer 线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append("le") 会使字符串缓冲区包含“startle”,而 z.insert(4, "le") 将更改字符串缓冲区,使之包含“starlet”。
在大部分情况下 StringBuilder > StringBuffer
java.lang.StringBuilder
java.lang.StringBuilder 一个可变的字符序列是 5.0 新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。
9. Map、Set、List、Queue、Stack 的特点与用法。
10. HashMap 和 HashTable 的区别。
答案:Hashtable 和 HashMap 类有三个重要的不同之处。
1、第一个不同主要是历史原因。Hashtable 是基于陈旧的 Dictionary 类的,HashMap 是 Java 1.2 引进的Map接口的一个实现。
2、也许最重要的不同是 Hashtable 的方法是同步的,但是这也是 HashTable 效率低下的原因,任何方法都是同步的,而 HashMap 的方法不是。这就意味着,虽然你可以不用采取任何特殊的行为就可以在一个多线程的应用程序中用一个 Hashtable,但你必须同样地为一个 HashMap 提供外同步。一个方便的方法就是利用 Collections 类的静态的 synchronizedMap()方法,它创建一个线程安全的 Map 对象,并把它作为一个封装的对象来返回。这个对象的方法可以让你同步访问潜在的 HashMap。这么做的结果就是当你不需要同步时,你不能切断 Hashtable 中的同步(比如在一个单线程的应用程序中),而且同步增加了很多处理费用。
3、第三点不同是,**只有 HashMap 可以让你将空值作为一个表的条目的 key 或 value。**HashMap 中只有一条记录可以是一个空的 key,但任意数量的条目可以是空的 value。这就是说,如果在表中没有发现搜索键,或者如果发现了搜索键,但它是一个空的值,那么 get()将返回 null。如果有必要,用 containKey()方法来区别这两种情况。
一些资料建议,当需要同步时,用 Hashtable,反之用 HashMap。但是,因为在需要时,HashMap 可以被同步,HashMap 的功能比 Hashtable 的功能更多,而且它不是基于一个陈旧的类的,所以有人认为,在各种情况下,HashMap 都优先于 Hashtable。
11. HashMap 和 ConcurrentHashMap 的区别,HashMap 的底层源码。
(1)HashMap 的源码:
1、HashMap 中的 hash 函数实现:
详见:https://www.zhihu.com/question/20733617
2、HashMap 源码解读
详见:http://blog.csdn.net/ll530304349/article/details/53056346
(2)ConcurrentHashMap 的源码:
1、JDK1.7 版本的实现
ConcurrentHashMap 的锁分段技术:假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap 所使用的锁分段技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap 不允许 Key 或者 Value 的值为 NULL
![](https://static001.geekbang.org/infoq/39/39a5f45cb9e76d138ada5508dc43e39c.png)
第一:Segment 类
Put
将一个 HashEntry 放入到该 Segment 中,使用自旋机制,减少了加锁的可能性。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value); //如果加锁失败,则调用该方法
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash; //同 hashMap 相同的哈希定位方式
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
//若不为 null,则持续查找,知道找到 key 和 hash 值相同的节点,将其 value 更新
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else { //若头结点为 null
if (node != null) //在遍历 key 对应节点链时没有找到相应的节点
node.setNext(first);
//当前修改并不需要让其他线程知道,在锁退出时修改自然会
//更新到内存中,可提升性能
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); //如果超过阈值,则进行 rehash 操作
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut
该操作持续查找 key 对应的节点链中是否已存在该节点,如果没有找到已存在的节点,则预创建一个新节点,并且尝试 n 次,直到尝试次数超出限制,才真正进入等待状态,即所谓的?自旋等待。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//根据 hash 值找到 segment 中的 HashEntry 节点
HashEntry<K,V> first = entryForHash(this, hash); //首先获取头结点
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) { //持续遍历该哈希链
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) //若不存在要插入的节点,则创建一个新的节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
//尝试次数超出限制,则进行自旋等待
lock();
break;
}
/*当在自旋过程中发现节点链的链头发生了变化,则更新节点链的链头,
并重置 retries 值为-1,重新为尝试获取锁而自旋遍历*/
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
remove
用于移除某个节点,返回移除的节点值。
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
//根据这种哈希定位方式来定位对应的 HashEntry
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
Clear
要首先对整个 segment 加锁,然后将每一个 HashEntry 都设置为 null。
final void clear() {
lock();
try {
HashEntry<K,V>[] tab = table;
for (int i = 0; i < tab.length ; i++)
setEntryAt(tab, i, null);
++modCount;
count = 0;
} finally {
unlock();
}
}
Put
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key); //求出 key 的 hash 值
int j = (hash >>> segmentShift) & segmentMask;
//求出 key 在 segments 数组中的哪一个 segment 中
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j); //使用 unsafe 操作取出该 segment
return s.put(key, hash, value, false); //向 segment 中 put 元素
}
Get
public V get(Object key) {
Segment<K,V> s;
HashEntry<K,V>[] tab;
int h = hash(key); //找出对应的 segment 的位置
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) { //使用 Unsafe 获取对应的 Segmen
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) { //找出对应的 HashEntry,从头开始遍历
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
Size
求出所有的 HashEntry 的数目,先尝试的遍历查找、计算 2 遍,如果两遍遍历过程中整个 Map 没有发生修改(即两次所有 Segment 实例中 modCount 值的和一致),则可以认为整个查找、计算过程中 Map 没有发生改变。否则,需要对所有 segment 实例进行加锁、计算、解锁,然后返回。
public int size() {
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
(2)JDK1.8 实现
在 JDK1.8 中对 ConcurrentHashmap 做了两个改进:
取消 segments 字段,直接采用 transient volatile HashEntry<K,V>[] table 保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
将原先?table 数组+单向链表?的数据结构,变更为?table 数组+单向链表+红黑树?的结构。对于 hash 表来说,最核心的能力在于将 key hash 之后能均匀的分布在数组中。如果 hash 之后散列的很均匀,那么 table 数组中的每个队列长度主要为 0 或者 1。但实际情况并非总是如此理想,虽然 ConcurrentHashMap 类默认的加载因子为 0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,**那么查询某个节点的时间复杂度为 O(n);因此,对于个数超过 8(默认值)的列表,jdk1.8 中采用了红黑树的结构,那么查询的时间复杂度可以降低到 O(logN),**可以改进性能。
12. TreeMap、HashMap、LinkedHashMap 的区别。
Map 主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值
重复。Hashmap 是一个最常用的 Map,它根据键的 HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。HashMap 最多只允许一条记录的键为 Null;允许多条记录的值为 Null;HashMap 不支持线程的同步,即任一时刻可以有多个线程同时写 HashMap;可能会导致数据的不一致。如果需要同步,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有同步的能力,或者使用 ConcurrentHashMap。
HashMap:线程不同步。根据 key 的 hashcode 进行存储,内部使用静态内部类 Node 的数组进行存储,默认初始大小为 16,每次扩大一倍。当发生 Hash 冲突时,采用拉链法(链表)。可以接受为 null 的键值(key)和值(value)。JDK 1.8 中:当单个桶中元素个数大于等于 8 时,链表实现改为红黑树实现;当元素个数小于 6 时,变回链表实现。由此来防止 hashCode 攻击。
LinkedHashMap:保存了记录的插入顺序,在用 Iterator 遍历 LinkedHashMap 时,先得到的记录肯定是先插入的. 也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比 HashMap 慢,不过有种情况例外,当 HashMap 容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap 慢,因为 LinkedHashMap 的遍历速度只和实际数据有关,和容量无关,而 HashMap 的遍历速度和他的容量有关。LinkedHashMap 采用的 hash 算法和 HashMap 相同,但是它重新定义了数组中保存的元素 Entry,该 Entry 除了保存当前对象的引用外,还保存了其上一个元素 before 和下一个元素 after 的引用,从而在哈希表的基础上又构成了双向链接列表。
TreeMap:线程不同步,基于?红黑树?(Red-Black tree)的 NavigableMap 实现,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。
HashTable:线程安全,HashMap 的迭代器(Iterator)是 fail-fast 迭代器。HashTable 不能存储 NULL 的 key 和 value。
13. Collection 包结构,与 Collections 的区别。
(1)Collection 是单列集合
List?元素是有序的、可重复
有序的 collection,可以对列表中每个元素的插入位置进行精确地控制。
可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
可存放重复元素,元素存取是有序的。
List 接口中常用类
l Vector:线程安全,但速度慢,已被 ArrayList 替代。
评论