老哥们,接上篇《Java开发岗面试题--基础篇(一)》,本期推出Java开发岗面试题--基础篇(二),来看看Java中的集合、多线程、异常体系等知识在面试中是怎么体现的。
HashMap和HashTable的区别?
HashMap和HashTable是Map接口的实现类,它们大体有以下几个区别:
Map集合有哪些实现类?
分别具有什么特征?
实现类 | 特征 |
---|---|
HashMap | 线程不安全的键值对集合。允许null值,key(最多只允许一个)和value都可以 |
HashTable | 线程安全的键值对集合。不允许null值,key和value都不可以 |
TreeMap | 能够把它保存的记录根据键排序的集合。默认是按升序排序 |
如何解决HashMap线程不安全问题?
HashMap的底层实现原理?
在JDK1.6、JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理hash冲突,同一hash值的键值对会被放在同一个位桶里,当桶中元素较多时,通过key值查找的效率较低。
而在JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值8时,将链表转换为红黑树,这样大大减少了查找时间。
当创建HashMap时会先创建一个数组,调用put()方法存数据时,先根据key的hashcode值计算出hash值,然后用这个哈希值确定在数组中存放的位置,再把value值放进去,如果这个位置本来没放东西,就会直接放进去,如果之前就有,就会生成一个链表,把新放入的值放在头部,当用get方法取值时,会先根据key的hashcode值计算出hash值,确定位置,再根据equals方法从该位置上的链表中取出该value值。
Hash碰撞怎么产生,怎么解决?
对象进行hash运算的前提是实现equals()和hashCode()两个方法,那么hashCode()的作用就是保证对象返回唯一hash值,但当两个对象计算值一样时,这就发生了碰撞冲突。下面将介绍如何处理冲突,当然其前提是一致性hash。
解决hash碰撞有以下几种方法:
开放地址法有一个公式:Hi=(H(key)+Di)%m。i=1,2,...,k(k<=m-1) 其中,m为哈希表的表长。Di是产生冲突时候的增量序列。Di值可能为1,2,3,…m-1,称线性探测再散列。如果Di取1,则每次冲突之后,向后移动1个位置。Di取值也可能为1,-1,2,-2,4,-4,9,-9,16,-16,…k,-k(k<=m/2),称二次探测再散列。如果Di取值可能为伪随机数列,称伪随机探测再散列。
当发生冲突时,使用第二个、第三个哈希函数计算地址,直到无冲突时。缺点:计算时间增加。比如上面第一次按照姓首字母进行哈希,如果产生冲突可以按照姓字母首字母第二位进行哈希,再冲突,直到不冲突为止。
将所有关键字为同义词的记录存储在同一线性链表中。如下:
HashMap为什么需要扩容?
当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为16*2=32,即扩大一倍。然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。比如说,我们有1000个元素就是new HashMap(1000),但是理论上来讲new HashMap(1024)更合适,不过即使是1000,HashMap也自动会将其设置为1024。但是newHashMap(1024)还不是更合适的,因为 0.75*1000<1000,也就是说为了让0.75*size>1000,newHashMap(2048)才最合适,避免了resize的问题。
如何遍历Map集合?
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class Test {
public static void main(String []args){
Map hashMap=new HashMap();
hashMap.put(1,"a");
hashMap.put(2,"b");
hashMap.put(3,"c");
//通过map.keySet()遍历
method1(hashMap);
//通过hashMap.entrySet()遍历
method2(hashMap);
//通过Iterator遍历
method3(hashMap);
}
public static void method1(Map hashMap){
System.out.println("通过map.keySet()遍历");
Set<Integer> set=hashMap.keySet();
for (Integer temp: set) {
System.out.println("key="+temp+",value="+hashMap.get(temp));
}
}
public static void method2(Map hashMap){
System.out.println("通过hashMap.entrySet()遍历");
Set<Map.Entry<Integer,String>> entry=hashMap.entrySet();
for (Map.Entry temp:entry) {
System.out.println("key="+temp.getKey()+",value="+temp.getValue());
}
}
public static void method3(Map hashMap){
System.out.println("通过Iterator遍历");
Iterator<Map.Entry<Integer,String>> iterator=hashMap.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<Integer,String> entry= iterator.next();
System.out.println("key="+entry.getKey()+",value="+entry.getValue());
}
}
}
运行结果:
ArrayList与LinkedList区别?
ArrayList使用数组方式存储数据,所以根据索引查询数据速度快,而新增或者删除元素时需要设计到位移操作,所以比较慢。
LinkedList使用双向链接方式存储数据,每个元素都记录前后元素的指针,所以插入、删除数据时只是更改前后元素的指针指向即可,速度非常快,然后通过下标查询元素时需要从头开始索引,所以比较慢,但是如果查询前几个元素或后几个元素速度比较快。
ArrayList与LinkedList都是线程不安全的。
Java中的ArrayList的初始容量和容量分配?
ArrayList是经常会被用到的,一般情况下,使用的时候会像这样进行声明:List arrayList=new ArrayList()。
如果像上面这样使用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,会重新分配内存空间,使数组的大小增长到16。
可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->…
也可以使用下面的方式进行声明:List arrayList=new ArrayList(4)。将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。
可以通过调试看到动态增长的数量变化:4->7->11->17->26->…
那么容量变化的规则是什么呢?请看下面的公式:
((旧容量 * 3 ) / 2) + 1
使用List集合如何保证线程安全?
IO和NIO的区别?
NIO是JDK1.7以后有的,它们俩的主要区别是:
在Java中实现多线程的三种手段?
简述线程、程序、进程的基本概念,
以及它们之间的关系?
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小的多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源,如CPU、时间、内存空间、输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。
什么是多线程?
为什么程序的多线程功能是必要的?
多线程就是几乎同时执行多个线程。实际上多线程程序中的多个线程是一个线程执行一会然后其他的线程再执行,并不是同时执行(多个线程的核心可以同时执行)。这样可以带来以下的好处:
多线程与多任务的差异是什么?
多任务与多线程是两个不同的概念,它们区别如下:
线程的几种状态?
线程一般具有五种状态。即创建、就绪、运行、阻塞、终止。
Thread类中的
start()和run()方法有什么区别?
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
Java中的notify和notifyAll有什么区别?
notify()方法不能唤醒某个具体的线程,所以只有一个线程在等待的时候它才有用武之地。而notifyAll()唤醒所有线程并允许它们争夺锁确保了至少有一个线程能继续运行。
Java多线程中调用
wait()和sleep()方法有什么不同?
Java程序中wait()和sleep()都会造成某种形式的暂停,它们可以满足不同的需要。wait()方法用于线程间通信,如果等待条件为真且其它线程被唤醒时它会释放锁,而sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间,但不会释放锁。
什么是线程安全?
多个线程同时运行一段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。同一个实例对象在被多个线程使用的情况下也不会出现计算失误,也是线程安全的,反之则是线程不安全的。
Java中的volatile变量是什么?
一个共享变量(类的成员变量、类的静态成员量)被volatile修饰之后,那么就具备了两层含义:
应用场景:在只涉及可见性,针对变量的操作只是简单的读写(保证操作的原子性)的情况下可以使用volatile来解决高并发问题,如果这时针对变量的操作是非原子的操作,这时如果只是简单的i++式的操作,可以使用原子类atomic类来保证操作的原子性(采用CAS实现),如果是复杂的业务操作,那么舍弃volatile,采用锁来解决并发问题(synchronized或者Lock)。
实现线程同步有三种方式?
同步代码块格式:
synchronized(监视对象){
需要同步的代码 ;
}
同步方法定义格式:
synchronized 方法返回值 方法名称(参数列表){
}
在方法上加 synchronized,是把当前对象做为监视器
Lock lock = new ReentrantLock();(可以在类中直接 new)
lock.lock(); 中间的代码块进行加锁 lock.unlock();
Java中的锁有几种方式?
Synchronized的局限性:如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep()方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待(不能主动释放锁)。当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作(不分情况,一律锁死)。
Lock的几个实现类?
线程间通信的几种实现方式?
synchronized和Lock的区别和应用场景?
Lock是接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
Lock可以提高多个线程进行读操作的效率。
Lock能完成synchronized所实现的所有功能,而且在性能上来说,如果竞争资源不激烈,synchronized要优于Lock,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
为什么要用线程池?
线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。Executors:线程池创建工厂类
自己根据创建线程池的需求来new对象(使用)
注意:线程池不允许使Executors去创建,而是通过ThreadPoolExecutor的方式。
说明:Executors返回的线程池对象的弊端如下:
Java中的异常体系?
什么是异常?分哪几种?有什么特点?
异常是发生在程序执行过程中阻碍程序正常执行的错误操作,只要在Java语句执行中产生异常则一个异常对象就会被创建。
Throwable是所有异常的父类,它有两个直接子类Error和Exception,其中Exception又被继续划分为被检查的异常(checked exception)和运行时的异常(runtime exception,即不受检查的异常)。
Error表示系统错误,通常不能预期和恢复(如JVM崩溃、内存不足等)。
被检查的异常(checked exception)在程序中能预期且要尝试修复(如我们必须捕获FileNotFoundException异常并为用户提供有用信息和合适日志来进行调试,Exception是所有被检查的异常的父类)。
运行时异常(Runtime Exception)又称为不受检查异常,如我们检索数组元素之前必须确认数组的长度,否则就可能会抛出ArrayIndexOutOfBoundException运行时异常,RuntimeException是所有运行时异常的父类。
try可以单独使用吗?
try不能单独使用,否则就失去了try的意义和价值。
以下try-finally可以正常运行吗?
try {
int i = 10 / 0;
} finally {
System.out.println("last");
}
可以正常运行。
Exception和Error有什么区别?
Exception和Error都属于Throwable的子类,在Java中只有Throwable 及其之类才能被捕获或抛出,它们的区别如下:
throw和throws的区别?
throws用在函数上,后面跟的是异常类,可以跟多个;而throw用在函数内,后面跟的是异常对象。
throws用来声明异常,让调用者知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说throw语句独立存在时,下面不要定义其他语句,因为执行不到。
throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常, 执行 throw则一定抛出了某种异常对象。
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
NoClassDefFoundError和
ClassNoFoundException有什么区别?
NoClassDefFoundError是Error(错误)类型,而ClassNoFoundExcept是Exception(异常)类型;
ClassNoFoundExcept是Java使用Class.forName方法动态加载类,没有加载到,就会抛出ClassNoFoundExcept异常;
NoClassDefFoundError是Java虚拟机或者ClassLoader尝试加载类的时候却找不到类订阅导致的,也就是说要查找的类在编译的时候是存在的,运行的时候却找不到,这个时候就会出现NoClassDefFoundError的错误。
使用try-catch为什么比较耗费性能?
这个问题要从JVM(Java 虚拟机)层面找答案了。首先Java虚拟机在构造异常实例的时候需要生成该异常的栈轨迹,这个操作会逐一访问当前线程的栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常等信息,这就是使用异常捕获耗时的主要原因了。
为什么finally总能被执行?
finally总会被执行,都是编译器的作用,因为编译器在编译Java代码时,会复制finally代码块的内容,然后分别放在try-catch代码块所有的正常执行路径及异常执行路径的出口中,这样finally才会不管发生什么情况都会执行。
说出 5 个常见的异常?
常见的OOM原因有哪些?
常见的OOM原因有以下几个:
本期分享就到这里,下期将继续分享Java开发岗面试题,敬请期待!创作不易,大家多多转发点赞,感谢。搬砖的路上一起努力!
往期推荐
微信扫一扫,获取更多
个人博客:www.cyouagain.cn