面试Java基础问题汇总 part1

tech2022-12-05  99

编译时多态、运行时多态

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线程只有这六种状态。

NEWRUNNABLE (就绪和运行统称)BLOCKEDWAITINGTIME_WAITINGTERMINATED

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),且数组长度大于等于64时,会将链表转化为红黑树,以减少搜索时间。HashTable没有这个机制。 hash % length == hash & (length-1)

该等式仅在length是2的幂的时候成立,使用&运算比%运算效率高得多。

HashMap和HashSet的区别

HashSet底层基于HashMap实现。HashSet的源码中,除了clone()、writeObject()、readObject()是HashSet自己不得不实现之外,其他都是直接调用HashMap中的方法。

HashMapHashSet实现了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面试突击版,百度可得最新版。 编译时多态、运行时多态

最新回复(0)