面试Java基础问题汇总 part1
编译时多态、运行时多态
c++要更复杂,Java相对而言更容易回答。
多态按执行过程分为两种情况,编译时多态和运行时多态。
运行时多态的概念也可以被说成“一个接口,多个方法”。
方法重载都是编译时多态。根据参数列表(数据类型、个数和次序),Java在编译时能够确定执行重载方法的哪一个。
方法覆盖会表现出两种不同的多态性,当对象引用本类实例时,为编译时多态,否则(例:父类对象引用子类实例)则为运行时多态。
在性能要求较高的代码中不提倡运用运行时多态,运行时多态方法较普通方法而言系统开销更大。
补充:泛型也是多态性的一种体现,是编译时多态。
equals()
==就不介绍了,它永远比较值。基础数据类型比较大小,引用数据类型比较地址值是否相同。
equals()
判断两个对象是否相等:
- 类没有覆盖equals方法,则等价于通过"=="比较对象。
- 类覆盖了equal是方法。一般会去比较两个对象的内容是否相等。
Java String equals方法实例
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
即先比较两个对象地址是否相等,如果地址相等,则他们的内容一定相等;如果地址不相等,会比较String的内容是否相等,比较方法就是遍历存储string的value数组(char类型,JDK9变为byte类型)。
hashCode()
hashCode()
函数的作用是获取散列码,它只在散列表中有用,在其他情况下没用。在散列表中,hashCode()
的作⽤是获取对象的散列码,进⽽确定该对象在散列表中的位置。如果两个对象的hashcode不相等,则它们一定不相等;如果hashcode相等,两个对象也不一定相等(hash collision),再调用equals
方法判断两者是否相等。这样可以减少equals()
的调用次数,大大提高执行速度。
下面是String类里面的hashCode()函数。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
线程基本状态
Java线程只有这六种状态。
- NEW
- RUNNABLE (就绪和运行统称)
- BLOCKED
- WAITING
- TIME_WAITING
- TERMINATED
Java IO
详细请去其他地方看,这里只总结一些规律。
特点 | 划分 |
---|---|
Reader/Writer结尾 | 字符流 |
Stream结尾 | 字节流 |
BIO\NIO\AIO (重点、必考)
再去其他地方看看,面试只懂下面这些肯定不行。BIO(Blocking I/O): 同步阻塞I/O模型,数据的读取写入必须阻塞在一个线程内等待其完成。在连接数不是很高的情况下,还是不错的,每一个连接专注于自己的I/O并且编程模型简单,不用考虑系统的过载,限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接请求。但是,十万级/百万级连接的时候,传统的BIO模型是无能为力的。
NIO(Non-blocking/New I/O): 同步非阻塞I/O模型,在Java1.4中引入了NIO框架,对应java.nio包,提供了Channel
, Selector
, Buffer
等抽象。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统BIO模型中的Socket
和SeverSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式,阻塞模型使用就像传统中的支持一样,简单,但是性能和可靠性都不好;非阻塞模型刚好相反。
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式开发。
AIO(Asynchronous I/O): AIO即NIO2。于Java 7引入,它是异步非阻塞的IO模型。异步IO基于事件和回调机制实现,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知线程进行后续的操作。
深拷贝 vs 浅拷贝
对于基本数据类型来讲,都是值传递。
对引用数据来讲,对于引用值进行传递的拷贝,为浅拷贝;创建新的对象,复制其内容,返回新对象的地址,为深拷贝。
C++、Python都有这个概念。
import copy
nums = [1, 2, 3, 4]
c1 = copy.copy(nums) # 浅拷贝
c2 = copy.deepcopy(nums) # 深拷贝
ArrayList扩容
初始化ArrayList,不加参数的时候,默认生成了一个空数组。
加入第一个元素的时候,add()
方法首先调用ensureCapacityInternal(size + 1)
,将minCapacity
设置为传入参数和默认容量(10),此时minCapacity = 10。
然后调用ensureExplicitCapacity
方法。
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
加入第一个元素时,minCapacity
-elementData.length
= 10- 0 = 10 > 0,调用grow()
扩容。
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
newCapacity(1) - minCapacity(10) < 0,newCapacity被置为10。
下次容量超过10的时候再次扩容,扩容之后为原来的.大约1.5倍,偶数正好,奇数抹去了小数部分。
HashMap和HashTable的区别
HashTable已经基本被淘汰,建议使用CocurrentHashMap。
- HashMap是非线程安全的,HashTable是线程安全的。HashTable内部的方法基本都经过
synchronized
修饰。 - HashMap的效率更好(因为不需要考虑线程安全)。
- HashMap,
null
可以作为键,这样的键只有一个,可以有一个或者多个键所对应的值为null
。HashTable中如果put进null
作键值,会报NullPointerException
。 - 初始容量大小和每次扩充容量大小的不同:(1) 创建时如果不指定容量初始值,HashTable默认初始大小为11,每次扩充变为原来的
2n+1
。HashMap默认的初始化大小为16,每次扩容变为原来2倍。
(2) 创建时如果给定了容量的初始值,那么HashTable会直接使用你给的大小,而HashMap会将其扩充为2的幂次方大小(HashMap
中的tableSizeFor()
方法保证,HashMap总是使用2的幂作为哈希表的大小,这是因为indexFor
方法使用(h&(length-1))来决定索引位置,是因为length是2的幂次方时,&运算接近%运算,能够均分,不会冲突。 - 底层数据结构:JDK1.8以后HashMap解决哈希冲突时,链表长度大于阀值(默认为8),会将链表转化为红黑树,以减少搜索时间。HashTable没有这个机制。
hash % length == hash & (length-1)
该等式仅在length是2的幂的时候成立,使用&运算比%运算效率高得多。
HashMap和HashSet的区别
HashSet底层基于HashMap实现。HashSet的源码中,除了clone()
、writeObject()
、readObject()
是HashSet自己不得不实现之外,其他都是直接调用HashMap中的方法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
put() 添加元素 | add() 添加元素 |
HashMap使用键(Key)计算hashcode。 | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以使用equals() 方法来判断对象的相等性。 |
HashMap底层实现(重要)
JDK1.8之前HashMap底层是数组和链表结合在一起(链表散列)。HashMap经过扰动函数处理后得到hash值,然后通过(n-1) & hash判断当前元素存放的位置。如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同。如果相同的话,直接覆盖,不相同就使用拉链法解决冲突。
扰动函数,使用key.hashCode()
得到h
之后再与h>>>16
取异或,目的是为了尽可能减少碰撞。
ConcurrentHashMap和HashTable的区别(重要)
主要在于实现线程安全的方式上不同。
底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表。JDK1.8采用的数据结构跟HashMap相同,都是数组+链表/红黑树。HashTable是数组加链表。
实现线程安全的方式:
(1) JDK1.7,ConcurrentHashMap使用分段锁对整个桶数组进行了分割分段(segment)每一把锁只锁容器的其中一部分数据,多线程访问容器里不同分段的数据,就不存在锁竞争,提高并发效率。JDK1.8时已经摒弃了segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现。并发控制使用synchronized和CAS来操作。
CAS:Compare and Swap,即比较再交换。
CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
(2) HashTable使用synchronized来保证线程安全(同一把锁)。这种方式效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态。
详见这里
参考资料
JavaGuide面试突击版,百度可得最新版。
编译时多态、运行时多态