Java开发岗面试题--基础篇(二)

时间:2022-07-24
本文章向大家介绍Java开发岗面试题--基础篇(二),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

老哥们,接上篇《Java开发岗面试题--基础篇(一)》,本期推出Java开发岗面试题--基础篇(二),来看看Java中的集合、多线程、异常体系等知识在面试中是怎么体现的。

HashMap和HashTable的区别?

HashMap和HashTable是Map接口的实现类,它们大体有以下几个区别:

  • 继承的父类不同。HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。
  • 线程安全性不同。HashTable中的方法是Synchronize修饰的,而HashMap中的方法在缺省情况下是非Synchronize的。因此,HashTable是线程安全的,HashMap是非线程安全的。
  • key和value是否允许null值。HashTable中,key和value都不允许出现null值。但是如果在HashTable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,不过运行时会抛出NullPointerException异常,这是JDK的规范规定的。HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是HashMap中没有该键,也可能是该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。

Map集合有哪些实现类?

分别具有什么特征?

实现类

特征

HashMap

线程不安全的键值对集合。允许null值,key(最多只允许一个)和value都可以

HashTable

线程安全的键值对集合。不允许null值,key和value都不可以

TreeMap

能够把它保存的记录根据键排序的集合。默认是按升序排序

如何解决HashMap线程不安全问题?

  1. Collections.synchronizedMap() 方法。
  2. Java.util.concurrent.ConcurrentHashMap类。ConcurrentHashMap类的方法内部使用了synchronized保证线程安全。

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集合?

  1. 获取Map中key的set集合map.keySet(),遍历key集合,通过Map.get(key)获取value值。
  2. 获取Entry集合map.entrySet(),遍历entrySet分别获取key和value。
  3. 通过Iterator遍历。
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集合如何保证线程安全?

  • 使用Vector
  • 使用Collections中的方法synchronizedList将ArrayList转换为线程安全的List
  • 使用java.util.current包下的CopyOnWriteArrayList(推荐)

IO和NIO的区别?

NIO是JDK1.7以后有的,它们俩的主要区别是:

  • IO是面向流是阻塞IO,NIO是面向缓冲,非阻塞的IO。IO每次从流中读取一个或多个字节,直到读取完所有的字节,没有缓存到任何地方。NIO读取数据是有缓存,就是说它读取的数据是在缓冲里读的。
  • 另外的话,Java中的各种IO是阻塞的 。一个线程调用read()或者write()方法时,这个线程就已经被阻塞了,直到读取到一些数据为止,或者是完全写入。在这个过程中不能干其他的事情。NIO是非阻塞模式的,当发送一个读取数据的请求时,如果没有读取到可用的数据,就什么也不会获取,且不会让线程阻塞,写数据也是这样。非阻塞的IO空闲时间可用来做其它的操作。所以,一个单独的非阻塞线程可以管理多个输入和输出通道。
  • 另外NIO还有一个selector(选择器 ),它可以管理多个输入输出的通道。

在Java中实现多线程的三种手段?

  1. 一种是继承Thread类。
  2. 另一种是实现Runnable接口。
  3. 最后一种是实现Callable接口。
  4. 还有一种也是实现Callable接口,只不过有返回值而已。

简述线程、程序、进程的基本概念,

以及它们之间的关系?

线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小的多,也正因为如此,线程也被称为轻量级进程。

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源,如CPU、时间、内存空间、输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。

什么是多线程?

为什么程序的多线程功能是必要的?

多线程就是几乎同时执行多个线程。实际上多线程程序中的多个线程是一个线程执行一会然后其他的线程再执行,并不是同时执行(多个线程的核心可以同时执行)。这样可以带来以下的好处:

  • 使用线程可以把占据长时间的程序中的任务放到后台去处理。
  • 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
  • 程序的运行速度可能加快
  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源,如内存占用等等。

多线程与多任务的差异是什么?

多任务与多线程是两个不同的概念,它们区别如下:

  • 多任务是针对操作系统而言的,表示操作系统可以同时运行多个应用程序。
  • 而多线程是针对一个进程而言的,表示在一个进程内部可以几乎同时执行多个线程。

线程的几种状态?

线程一般具有五种状态。即创建、就绪、运行、阻塞、终止。

  • 新建(new):新创建了一个线程对象。
  • 可运行(runnable ):线程对象创建后,其它线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权 。
  • 运行(running ):可运行状态(runnable)的线程获得了CPU时间片(timeslice),执行程序代码。
  • 阻塞(block):阻塞状态是指线程因为某种原因放弃了CPU使用权,即让出了CPU timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得CPU timeslice转到运行(running)状态。阻塞的情况分三种:
    1. 等待阻塞:运行(running)的线程执行o.wait ()方法,JVM会把该线程放入等待队列(waitting queue)中。
    2. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    3. 其他阻塞:运行(running)的线程执行sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  • 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

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修饰之后,那么就具备了两层含义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序。但是它并不能保证操作的原子性。

应用场景:在只涉及可见性,针对变量的操作只是简单的读写(保证操作的原子性)的情况下可以使用volatile来解决高并发问题,如果这时针对变量的操作是非原子的操作,这时如果只是简单的i++式的操作,可以使用原子类atomic类来保证操作的原子性(采用CAS实现),如果是复杂的业务操作,那么舍弃volatile,采用锁来解决并发问题(synchronized或者Lock)。

实现线程同步有三种方式?

  • 同步代码块:在代码块上加上“synchronized”关键字,则此代码块就称为同步代码块。
同步代码块格式: 
synchronized(监视对象){
 需要同步的代码 ;
}
  • 同步方法
同步方法定义格式: 
synchronized 方法返回值 方法名称(参数列表){
}
在方法上加 synchronized,是把当前对象做为监视器
  • 同步锁
 Lock lock = new ReentrantLock();(可以在类中直接 new) 
 lock.lock(); 中间的代码块进行加锁 lock.unlock();

Java中的锁有几种方式?

  • Synchronized
  • Lock

Synchronized的局限性:如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep()方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待(不能主动释放锁)。当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作(不分情况,一律锁死)。

Lock的几个实现类?

  • ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。
  • ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 ——“读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。他的两个实现类读锁readerLock和写锁writerLock。

线程间通信的几种实现方式?

  1. 使用volatile关键字。基于volatile关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候,线程能够感知并执行相应的业务。这也是最简单的一种实现方式。
  2. 使用Object类的wait()和notify()方法。Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。 注意:wait和notify必须配合synchronized使用,wait()方法释放锁,notify()方法不释放锁

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返回的线程池对象的弊端如下:

  • FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  • CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。 注:建议自己通过new关键字创建newThreadPoolExecutor。

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 及其之类才能被捕获或抛出,它们的区别如下:

  • Exception(异常)是程序正常运行中,可以预期的意外情况,并且可以使用 try/catch 进行捕获处理的。
  • Exception又分为运行时异常(Runtime Exception)和受检查异常(Checked Exception),运行时异常编译能通过,但如果运行过程中出现这类未处理的异常,程序会终止运行;而受检查的异常,要么用try/catch捕获,要么用throws字句声明抛出,否则编译不会通过。
  • Error(错误)是指突发的非正常情况,通常是不可以恢复的,比如Java虚拟机内存溢出,诸如此类的问题叫做Error。

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 个常见的异常?

  1. NullpointException:空指针异常,null值导致。
  2. IOException:IO异常,IO流常见编译异常。
  3. SQLException:SQL拼写异常。
  4. ClassNotFoundException:类找不到异常,一般为Jar包引入失败或者忘写Spring注解。
  5. ClassCastException:类型转换异常。

常见的OOM原因有哪些?

常见的OOM原因有以下几个:

  • 数据库资源没有关闭
  • 加载特别大的图片
  • 递归次数过多,并一直操作未释放的变量

本期分享就到这里,下期将继续分享Java开发岗面试题,敬请期待!创作不易,大家多多转发点赞,感谢。搬砖的路上一起努力!

往期推荐

Java开发岗面试题--基础篇(一)

微信扫一扫,获取更多

个人博客:www.cyouagain.cn