JAVA面试题整理

来源:互联网 发布:ubuntu命令 编辑:程序博客网 时间:2024/05/16 09:10

1.数组和链表的区别及各自优势,底层实现区别?

数组在内存空间内是连续的,初始化时需要确定数组的长度,利于遍历和查找,不利于插入和删除。查找时间复杂度O(1),插入O(n)
链表的地址不是连续的,可以随意存放,并不需要指定链表的长度,可以随意扩展,通过改变节点索引的指向来进行插入和删除操作,但是遍历的效率低。插入、删除操作的时间复杂度O(1),查找O(n).

链表的底层是通过操作内存中的索引来实现的,数组则是操作一片连续的内存空间。

2.数据库范式?乐观锁悲观锁区别?事务的四个特性。

数据库范式:
第一范式:列不可分,即一列数据不能分为其他几列;
第二范式:满足第一范式;必须存在主键,且非主键的列必须完全依赖于主键(不存在部分依赖)
第三范式:满足第二范式;非主键列必须直接依赖于主键,不能存在传递依赖。

悲观锁:在开始对对象进行,操作的时候就锁住对象,直到提交了事务才解锁,屏蔽了一切可能违反数据完整性的操作,但是并发性能差。

乐观锁:加锁的粒度相对悲观锁更细,在提交事务之前才将对象锁住。获取了较好的并发性能,但会出现脏读的现象。如果在A用户在提交对象修改之前,B用户获取了该对象,并在A提交事务之后,B再提交事务,会发现该对象已经变化了,不得不重新读取该对象,会增加并发用户读取对象。

事务的四个特性:
原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
一致性:数据库必须只包含成功提交事务的结果,未完成或被中断的事务不会写入数据库。(例:A给B转账100元,A-100后事务中断,B+100元没有执行,则事务回滚,数据库内数据A,B的数据都不变,不会出现A-100,B+0的情况。)
隔离性:出现并发时,一个事务的操作不能对其他并发事务出现干扰。
持久性:一个事务完成提交时,数据就应该被持久化到数据库,已经提交的事务不会再变化。后续的其他事务不会对已完成事务的结果又任何的影响。(例:A事务让小明存款+100元,并提交了事务,此时想让小明的存款只加50,并不能通过改变事务A的结果来实现。只能使用事务B让小明-50)

脏读:在一个事务中读取了另一个事务未提交的数据。
不可重复度:在一个事务中多次读取同一数据,出现了不同的值。
幻读:当处理一批数据时,可能发现有其他事务新增的数据没有处理。

Read uncommitted(读未提交):无法保证任何情况;
Read committed(读提交):避免脏读出现;

Repeatable read(可重复读):避免脏读、不可重复读
出现;
Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

3.发生哈希冲突的解决办法

①开放地址法:
线性探测法:当存放元素发生哈希冲突时,算法会从该槽向后循环遍历整个hash表,直到找到一个空槽。当查找元素时,会循环遍历hash表,直到

  1. 找到元素若是
  2. 发现空槽
  3. 整个hash表遍历完毕说明不存在该元素。

变形1:线性补偿探测法,将hash冲突的步长调整为Q,Q与hash数组的长度m互质
变形2:伪随机探测法,将hash冲突的步长调整为随机值,能避免或减少堆聚

缺点:

  1. 无法解决hash溢出问题,需要另外编程来维护一个溢出表;
  2. 删除工作很困难,当直接删除一个元素时,如果此元素占用了其他hash值的槽,会导致查找其他hash值的元素时首先找到一个空槽,即认为这个元素不存在(实际可能存在)。所以只能在已经删除的元素上加上删除标记。
  3. 容易出现堆聚现象,即存入的元素在hash数组中存放的位置连成一片,并且堆聚的发生会引起进一步的堆聚。

②拉链法(java实现为hashmap):通过维护一个存放链表的数组来实现hash表,链表通过entry内部类来实现, entry包含了hash值,hash表的key和value值,以及指向的下一个entry节点的next。若插入时,数组对应位置为空,直接插入,若已经存在了entry节点,即发生hash冲突的时候,首先判断key是重复,若重复就覆盖重复节点的value值,若不重复,会将元素的key,value信息存入entry对象,并且存入数组,并将该节点的next指向上一个entry节点对象。(相当于插入了链表的头部)

优点:

  1. 处理冲突简单,无堆聚现象,也不会发生占用hash数组的情况,查询效率比较高。
  2. 各链表上的节点空间是动态申请的,适于处理未知表长度的数据
  3. 支持对hash数组动态的扩容,节约空间.
  4. 比较容易进行删除操作,只需要删除链表上的节点即可

缺点:维护指针需要额外空间

③再散列:发生冲突时,使用第二个、第三个hash函数来计算hash值,直到无冲突。缺点:计算时间长。

④维护公共溢出区:当hash冲突发生时,将元素存入另外维护的hash数组中。
缺点:需要更多额外的空间。

4.循环链表

循环链表是尾节点的指针域指向头结点的一种环形的链表数据结构。

优点:
从表中任一结点出发都能访问到整个链表,即使是单向的循环链表也可以遍历到指定节点之前的元素。尾节点的指针域指向头结点的一种环形的链表数据结构。循环链表里是没有空指针的,遍历一个循环链表的时候,循环终止的条件不能像其他链表一样使得节点指向为空,只能指向某一特定的指针。
缺点:
当对链表进行增删操作时,可能对链表结构发生破坏,造成环的断开,或者出现环外的元素,可能造成已经写下的循环语句出错,或者找不到元素的情况。

5.hashmap 和concurrenthashmap的区别?

hashmap不是线程安全的,concurrentHashMap是线程安全的。

concurrentHashMap实现:使用更细粒度的synchronized锁,分段锁(segment)将内部维护的map默认拆分成16个segment,对每个segment单独加上lock锁,根据要存入或删除元素的key的hash值来计算具体放入那个segment中的hashentry(对应到相同的hash值),这样既保证了线程安全又减少了并发访问时出现阻塞的概率,提高了高并发io性能。
下面分析concurrentHashMap在并发下的四种情况:
并发读:显而易见,因为concurrentHashMap使用的是读写分离的读写锁,并发的多个写线程之间是不加锁的。因此单纯的读操作可以实现完全的并发。

并发写:多个并发写操作发生时,第一个执行的写线程,会对整个数据结构加上写锁,使得其他写线程阻塞,保证写操作的一致性。但是添加,删除操作和更新操作的底层实现略有区别。后面有介绍

并发读写(结构性改变的写):在写入新元素的操作或者删除操作(结构性改变)时,将会对被改动节点前的节点进行一份复制,保留原节点的结构,不影响读的结果,并在复制的片段中进行更改或删除操作,当迭代器完成结构性改变操作(删除或新增)后,再将对应hash桶头指针指向新的数据,因此不会影响并发的读线程结果。

并发读写(非结构性改变的写):这种情况很特殊,是唯一可能出现读线程对数据结构加锁的情况。当读线程遍历hashentry链表时,如果发现有结点的value值为空,说明此时可能有并发的写线程正在修改此节点的值,此时读线程会对hashentry中的链表加上锁,防止写线程再次修改节点value值,保证不会再读出空值,并保证读出的是最新的节点值,保证读写一致性。注:(整个读操作过程只有在读写并发并出现二次读取的情况会加锁,并发性能很高)

hashtable,vector(同步容器):通过对整个方法加上synchronized锁的方式实现线程安全,并发io效率低,并且在一些复合操作时可能出现线程不安全的情况,而concurrent类的数据结构提供了一些整合了复合操作方法来保证操作的原子性和一致性。

6.有哪些加锁的方式?

synchronized(内部锁、互斥锁):
对象锁,静态对象锁(也称作类锁,即使用Class.XXX对代码块加锁或者岁静态方法加上synchironized关键字时加上的锁)

    **可重入(锁计数机制):**当线程1持有锁时再次获得锁时,锁计数+1,每次推出同步代码块

lock(实现读写锁分离):
抽象了对内部锁的操作,通过lock类实例对象的lock()方法加锁,unlock()方法解锁,实现了细粒度的读写锁分离。

ReadAndWriteLock:读锁共享锁(不隔离其他读锁),写锁为独占锁(隔离其他写锁)。

7.线程池和阻塞队列

java.util.concurrent.ThreadPoolExecutor:从1.5开始,通过线程池维护并行执行线程的数量,该类的构造函数接收如下参数:

corePoolSize:核心线程数,指保留的线程池大小
maximumPoolSize:指的是线程池的最大大小
keepAliveTime: 指的是空闲线程结束的超时时间(当一个线程不工作时,过keepAliveTime 长时间将停止该线程)。
unit: 是一个枚举类,表示keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值)。
workQueue: 表示存放任务的队列(存放需要被线程池执行的线程队列)。
handler: 阻塞策略(添加任务失败后如何处理该任务).

当调用execute()方法向线程池中加入一个任务时,
①如果线程数量小于corePoolSize,线程池会立即创建一条线程来执行该任务;
②如果此时的线程数已经超过corePoolSize并且小于maximumPoolSize时,任务会被存入队列中;
③如果队列满了,线程池会继续开启新的线程来执行这个任务;
④如果线程池的线程数达到了maximumpoolsize,且队列已满,则抛异常。任务丢弃。
⑤如果一条线程空闲达到keepAliveTime,线程会被杀掉。

其中阻塞队列有多种实现方式:

ArrayBlockingQueue:基于数组实现,初始化时确定了其容量;
LinkedBlockingQueue:线程安全的、容量默认Integer.MAX_VALUE即无上限,也可以指定容量;
PriorityBlockingQueue:与LinkedBlockingQueue类似,但内不部排序并不是先入先出顺序,自然排序或根据conparartor.
SynchronousQueue:存取必须交替完成。 DelayedQueue:延迟期满才能提取元素。
LinkedBlockingDque:双端队列,头尾都可以存取

何为阻塞队列:队列满时,写操作被阻塞,队列空时,读操作被阻塞的队列。

阻塞策略:

AbortPolicy:默认的阻塞策略,不执行任务,直接抛出一个运行时异常,需要通过trycatch来捕获处理。
DiscardPolicy:直接抛弃 DiscardOldestPolicy:抛弃头部的一个任务并且再次执行该任务。
CallerRunsPolicy:将由调用者线程去执行任务,同时阻塞入口。

8.常见的设计模式

(1)创建型模式:单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式(2)结构型模式:适配器模式、装饰器模式、代理模式、外观模式、组合模式、桥接模式、享元模式(3)行为模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

(1)①工厂模式:建立一个工厂类,对多个实现了相同接口的产品类进行封装,工厂类内部包含了创建各个产品类的实例的方法。在创建各类的实例的时候,首先创建工厂类实例,统一使用工厂类实例内部的方法来创建其他产品类的实例。
应用:适用于大量的不同产品需要创建,但是拥有相同的接口时。体现了设计模式的合成/复用原则。

②抽象工厂模式:普通的工厂模式扩展产品时,需要对工厂类进行修改,不符合设计模式的开闭原则。因此对工厂模式进行了改进,将工厂类进一步抽象,每一个产品类都对应于一个工厂类,这些不同的工厂类都统一实现一个生产接口,该生产接口声明了一个生产方法,由其不同实现类工厂分别实现。需要建立产品类的实例的时候,统一使用生产接口声明对象,并使用产品工厂类实例化对象,调用接口中的生产方法来创建产品实例。
应用:与工厂模式同。体现了开闭原则,可扩展性更强。

③单例模式:单例对象能保证在一个JVM中,该对象只有一个实例存在。使用私有构造函数,避免被外部类直接实例化,在内部持有私有静态实例,并赋值null,同时提供一个获取实例的静态方法暴露给外部类。在外部类需要用到单例实例的时候,可直接调用该静态方法获取该单例实例(若实例还未被创建,则创建实例,若已经创建实例,则直接返回已经创建的实例)。
应用:某些使用频繁的类,使用单例可以节约系统开销;
省去了new操作符,降低了内存压力;
对于某些类型,如系统日志,缓存类等,若使用多例,则逻辑混乱无法管理。

④建造者模式:与工厂模式类似,使用建造者类,建造者类内部维护了一个list,内部的生产方法接收生产数量的参数,循环调用生产者类,生产产品加入list中。
应用:用于批量生产产品,或者生产复合型对象。

⑤原型模式:该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。原型类需要实现Cloneable接口(空接口),告知虚拟机这个类可以被安全的克隆,同时复写Object类中的clone方法(native),将其声明为public方法。

应用:用于对象的复制
延伸——**深浅克隆的实现和区别**        浅克隆:浅克隆是只会为基础类型重新开辟内存空间,对于引用类型只是复制了引用,不会新建对象,对克隆对象内部引用类型进行修改时,会影响到母体内部的应用类型对象,因为他们指向的是同一块内存区域。        深克隆:对所有对象都重新开辟内存空间,2个对象互相独立,互不影响。

(2)⑥适配器模型:
当一个类需要扩展加入另一个接口中的方法,可以新建一个适配器类,继承待适配类,同时实现扩展功能的接口,在适配器内部实现接口方法。原类得以和含有新的功能的接口完成适配,达到扩展;
当一个对象需要进行扩展,新建一个适配器类,内部调用待适配对象的方法,同时实现接口方法,完成新功能的适配。
当一个接口需要适配,使用抽象类实现多个接口中的所有方法,需要使用原接口中的方法时,继承抽象类即可。

应用:需要将一个或数个接口中的方法加入一个类,一个对象或另一个接口的时候。

⑦装饰模式:定义个装饰类,与被装饰类实现同一个接口,构造函数接收接口类型引用为参数,内部调用接口方法并对其进行扩展。实例化装饰类,并在实例化时传入被装饰对象的接口声明,调用装饰类中的方法来达到扩展被装饰类功能的目的。

应用:一个类的功能需要扩展的时候,并且可以动态的增加功能和撤销,不会对原类修改(通过继承扩展时,必须复写父类中的方法,无法实现多种扩展,装饰类中可以定义多种构造函数,接收不同的参数来对功能进行扩展。)

⑧代理模式:与装饰模式概念基本相同,区别在于装饰类实例化时需要将被已经实例化的装饰类接口引用作为参数传入构造函数,可以实现一对多。而代理类则和被代理类则是一对一的关系,在代理类内部直接实例化被代理类对象(而非接口引用),隐藏了被代理类,只对外暴露代理类。
应用:与装饰模式一样,但是一对一关系,功能划分更加清晰,不会出现多个功能不同的相似的对象。

⑨外观模式:将多个存在依赖关系类的实例化过程放入一个外观类。通过实例化外观类,完成了存在依赖关系的多个类的实例化,将这多个类解耦,在改动代码时,不会影响到其他存在依赖关系的类。(与工厂模式类似,只是不需要实现相同的接口)

⑩桥接模式:将抽象化和实现解耦,使得二者可以独立变化。通过一个桥接抽象类,持有接口的一个实例(该接口有多种实现类)。在桥接类的子类中调用接口方法。实例化桥接类子类时,使用set方法传入接口具体的实现类,再调用桥接类的接口方法。

    应用:便于调用接口多种实现。如数据库驱动有多种实现,实际使用时,只需要实例化JDBC的driverManager,并传入使用的数据库类型即可。

未完待续……….

9.JVM类加载机制

    BootStrap ClassLoader:启动类加载器,一些JVM可以识别的类    Extension ClassLoader:  扩展类加载器,JAVA_HOME/bin中的一些类包(这层开始用户可控)    Application ClassLoader: 应用类加载器,用户的classpath中的一些类包    User ClassLoader: 自定义类加载器

双亲委派模型:所有的类都是从类加载器的顶层开始加载(父子类通过组合来实现),当父加载器无法加载类包才从子类加载。保证了用户不能自定义系统内部类相同类名。

10.JVM内存划分

程序计数器(Program Counter Register):线程私有的。很小的一块内存空间,每条线程都有一个对应的计数器用于记录每条线程当前运行的位置当前线程所执行的字节码的行号(在进行多线程切换,循环,跳转,分支,异常处理时,它都是必要的)。
java虚拟机栈(Java Virtual Machine Stacks):线程私有的。包含局部变量表(存放各种基本数据类型,对象的引用),操作栈,动态链接,方法出口等信息。
本地方法栈(Native Method Stacks):与java虚拟机栈功能相同,只不过为Native方法服务。
JAVA堆(JAVA Heap):存放对象的实例,他们的内存地址可以是固定的或可扩展的(通过-Xms和-Xmx控制)
方法区(Method Area):与JAVA堆一样,是线程共享的区域。存放被虚拟机加载的类信息,常量,静态变量。注:逻辑上和JAVA堆属于一块区域,但是有一个别名为“Non-Heap”用于与java堆做功能区分。

内存泄露:因为某些循环引用导致,本应被回收的垃圾没有被JVM自动存回收,最终导致内存溢出异常(OutOfMenmoryError)
解决:排除循环引用
内存溢出:堆内存中的对象实例确实还都应该存活,可能是某些对象生命周期过长,持有的对象过多,JVM内存消耗过大导致。
解决:看是否可以调大JVM内存,尽量优化代码,减少内存开销。

11.aop是如何实现的?

OOP(Object-Oriented Programing):面向对象编程,引入封装、继承和多态性等概念来建立一种对象层次结构。将业务逻辑里按层次划分来抽象成对象,每个层次的对象都有自己的行为和属性,负责一部分业务逻辑(处理页面响应,处理业务逻辑,处理数据库连接)

AOP(Aspect Oriented Programming):面向切面编程。有一些无法按照层次划分的零散的公共业务逻辑,如日志,异常,事务管理等,它可能出现在OOP概念的任何一个层次中。AOP将这些零散的公共业务逻辑抽离出来,封装成一个可以复用的模块(称之为切面)。

核心关注点和横切关注点:
如果OOP解决的是真正的业务逻辑,我们把它称之为业务的核心关注点,把它比作纵向的,那么AOP则是关注于那些与核心业务无关的一些通用服务,我们把它称之为横切关注点,即是横向的,像一个个切面切入各个业务逻辑中。

AOP的实现方式:
①静态织入方式:通过特定的语法标记切入点,在编译期间完成静态代理,以.class的java二进制文件保存在JVM内存中。
②动态代理方式:
1.使用proxy代理,利用类加载和反射机制,在项目运行过程中动态的创建代理类。代理类需要实现InvocationHandler 接口,被代理需要实现业务接口,无法代理不能实现接口的类。
2.使用cglib代理,通过继承来实现代理,无需实现接口,但不能对final类进行代理。

12.java中哪些类是不可变的?

8个基础类型及其封装类加上String类型。
boolean,byte, char, double ,float, integer, long, short,Boolean, Byte, Character, Double, Float, Integer, Long, Short, String.
对这些对象的操作都不会改变原对象,而是会重新开辟一片内存,新建一个对象,原对象依然不变,存在于内存中。

13.将String类设计为不可变类的目的

①可以保证线程安全,每次对String对象的操作不会改变原操作。
②String类内部缓存了string的hash值,这样设计可以保证String类一旦被初始化,hashcode就不会变。
③当创建了2个String类型的引用,指向的字符串内容相同的话,JVM只会在内存保留一个字符串对象,将2个引用都指向它(这也保证了hash一致性),如果String可变,那么,对其中一个引用对象做修改时,就会影响到另外的String.

14.Spring的入口在哪?

在web项目中,Spring的入口是ServletDispatcher,在web.XML中配置,配置方法与普通的servlet一样。ServletDispatcher它继承了HttpServlet类,收到请求后,将信息装入HttpServletRequest和HttpServletResponse对象,转发给HandlerAdaptor,找到适配的Handler(Controller),处理HttpServletRequest,并将响应信息放入HttpServletResponse,最后放入ModelAndView对象传递给ViewResolver,渲染视图,最后通过ServletDispatcher返回响应到客户端。

15.Spring中javabean的生命周期?

Servlet的生命周期:
容器实例化
初始化Servlet信息init
接收请求,调用service
当容器销毁destroy
Spring上下文中的Bean的生命周期:

1、实例化一个Bean--也就是我们常说的new;2、按照Spring上下文对实例化的Bean进行配置--也就是IOC注入;3、如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值4、如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以);5、如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法);6、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;7、如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。8、如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法、;注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton,这里我们不做赘述。9、当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;10、最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

16.多线程相关

博客链接:15个顶级Java多线程面试题及回答 / Java程序员面试中的多线程问题

原创粉丝点击