Java常见面试题及答案.docx

上传人:b****2 文档编号:24510037 上传时间:2023-05-28 格式:DOCX 页数:15 大小:610.93KB
下载 相关 举报
Java常见面试题及答案.docx_第1页
第1页 / 共15页
Java常见面试题及答案.docx_第2页
第2页 / 共15页
Java常见面试题及答案.docx_第3页
第3页 / 共15页
Java常见面试题及答案.docx_第4页
第4页 / 共15页
Java常见面试题及答案.docx_第5页
第5页 / 共15页
点击查看更多>>
下载资源
资源描述

Java常见面试题及答案.docx

《Java常见面试题及答案.docx》由会员分享,可在线阅读,更多相关《Java常见面试题及答案.docx(15页珍藏版)》请在冰豆网上搜索。

Java常见面试题及答案.docx

Java常见面试题及答案

1. ArrayList和Vector的区别。

相同点

1、ArrayList和Vector都是继承了相同的父类和实现了相同的接口。

2、底层都是数组实现的。

3、初始默认长度都为10。

不同点

1、同步性

Vector中的public方法多数添加了synchronized关键字,以确保方法同步,也即是Vector线程安全,ArrayList线程不安全。

2、扩容不同

内部属性不同,这可能是导致扩容方式不同的原因所在。

ArrayList有两个属性,存储数据的数组elementData,和存储记录数目的size。

 

Vector有三个属性,存储数据的数组elementData,存储记录数目的elementCount,还有扩展数组大小的扩展因子capacityIncrement。

 

ArrayList的扩展方法:

//jdk1.8.0_91

privatevoidgrow(intminCapacity){

//overflow-consciouscode

intoldCapacity=elementData.length;

intnewCapacity=oldCapacity+(oldCapacity>>1);

if(newCapacity-minCapacity<0)

newCapacity=minCapacity;

if(newCapacity-MAX_ARRAY_SIZE>0)

newCapacity=hugeCapacity(minCapacity);

//minCapacityisusuallyclosetosize,sothisisawin:

elementData=Arrays.copyOf(elementData,newCapacity);

}

可以看出,在满足扩容条件时,扩展后数组大小为(原数组长度的1.5倍)与传递参数中较大者。

Vector的扩展方法:

//jdk1.8.0_91

privatevoidgrow(intminCapacity){

//overflow-consciouscode

intoldCapacity=elementData.length;

intnewCapacity=oldCapacity+((capacityIncrement>0)?

capacityIncrement:

oldCapacity);

if(newCapacity-minCapacity<0)

newCapacity=minCapacity;

if(newCapacity-MAX_ARRAY_SIZE>0)

newCapacity=hugeCapacity(minCapacity);

elementData=Arrays.copyOf(elementData,newCapacity);

}

可以看出,当扩容因子大于0时,新数组长度为原数组长度+扩容因子,否则子新数组长度为原数组长度的2倍。

将上面生成的新数组长度与传递的参数长度作比较,较大者为最终的新长度。

 

2、Collection和Collections的区别。

考点

Collection 、Collections 。

解析Collection与Collections的根本区别是:

1、Collection是一个集合接口。

它提供了对集合对象进行基本操作的通用接口方法。

Collection接口在Java类库中有很多具体的实现。

Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

2、Collections是一个包装类。

它包含有各种有关集合操作的静态多态方法。

此类不能实例化,就像一个工具类,服务于Java的Collection框架。

Collections是一个包装类,Collection表示一组对象,这些对象也称为collection的元素。

一些collection允许有重复的元素,而另一些则不允许,一些collection是有序的,而另一些则是无序的。

扩展资料:

所有通用的Collection实现类(通常通过它的一个子接口间接实现Collection)应该提供两个“标准”构造方法:

一个是void(无参数)构造方法,用于创建空collection;

另一个是带有Collection类型单参数的构造方法,用于创建一个具有与其参数相同元素新的collection。

实际上,后者允许用户复制任何collection,以生成所需实现类型的一个等效collection。

尽管无法强制执行此约定(因为接口不能包含构造方法),但是Java平台库中所有通用的Collection实现都遵从它。

此接口中包含的“破坏性”方法,是指可修改其所操作的collection的那些方法,如果此collection不支持该操作,则指定这些方法抛出UnsupportedOperationException。

如果是这样,那么在调用对该collection无效时,这些方法可能,但并不一定抛出UnsupportedOperationException。

例如,如果要添加的collection为空且不可修改,则对该collection调用addAll(Collection)方法时,可能但并不一定抛出异常。

3、HashMap的工作原理是什么?

解析

HashMap是基于Hashing(散列法)的原理,以键值对(key-value)的形式存储元素的,我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。

Mark:

HashMap的键值对也叫做Entry,而每个Entry都是存储在数组当中,因此这个数组就是HashMap的主干。

它需要一个hash函数,使用hashCode()和equals()方法来向集合/从集合添加和检索元素。

当调用put()方法的时候,HashMap会计算key的hash值,然后把键值对存储在集合中合适的索引上。

如果key已经存在了,value会被更新成新值。

HashMap数组中的每一个元素的初始值都是NULL。

HashMap的一些重要的特性是它的容量(capacity),负载因子(loadfactor)和扩容极限(thresholdresizing)。

1、Put方法的实现原理

HaspMap的一种重要的方法是put()方法,当我们调用put()方法时,比如hashMap.put("Java",0),此时要插入一个Key值为“Java”的元素,这时首先需要一个Hash函数来确定这个Entry的插入位置,设为index,即index=hash("Java"),假设求出的index值为2,那么这个Entry就会插入到数组索引为2的位置。

但是HaspMap的长度肯定是有限的,当插入的Entry越来越多时,不同的Key值通过哈希函数算出来的index值肯定会有冲突,此时就可以利用链表来解决。

其实HaspMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点,每一个Entry对象通过Next指针指向下一个Entry对象,这样,当新的Entry的hash值与之前的存在冲突时,只需要插入到对应点链表即可。

需要注意的是,新来的Entry节点采用的是“头插法”,而不是直接插入在链表的尾部,这是因为HashMap的发明者认为,新插入的节点被查找的可能性更大。

2、Get方法的实现原理

get()方法用来根据Key值来查找对应点Value,当调用get()方法时,比如hashMap.get("apple"),这时同样要对Key值做一次Hash映射,算出其对应的index值,即index=hash("apple")。

前面说到的可能存在Hash冲突,同一个位置可能存在多个Entry,这时就要从对应链表的头节点开始,一个个向下查找,直到找到对应的Key值,这样就获得到了所要查找的键值对。

例如假设我们要找的Key值是"apple":

第一步,算出Key值“apple”的hash值,假设为2。

第二步,在数组中查找索引为2的位置,此时找到头节点为Entry6,Entry6的Key值是banana,不是我们要找的值。

第三步,查找Entry6的Next节点,这里为Entry1,它的Key值为apple,是我们要查找的值,这样就找到了对应的键值对,结束。

4、heap和stack有什么区别。

解析

堆栈的概念:

堆栈是两种数据结构。

堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。

在单片机应用中,堆栈是个特殊的存储区,主要功能是暂时存放数据和地址,通常用来保护断点和现场。

要点:

堆,队列优先,先进先出(FIFO—firstinfirstout)。

栈,先进后出(FILO—First-In/Last-Out)。

堆和栈的区别

一、堆栈空间分配区别

1、栈(操作系统):

由操作系统自动分配释放,存放函数的参数值,局部变量的值等。

其操作方式类似于数据结构中的栈; 

2、堆(操作系统):

一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

二、堆栈缓存方式区别

1、栈使用的是一级缓存,他们通常都是被调用时处于存储空间中,调用完毕立即释放; 

2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。

所以调用这些对象的速度要相对来得低一些。

三、堆栈数据结构区别

堆(数据结构):

堆可以被看成是一棵树,如:

堆排序; 

栈(数据结构):

一种先进后出的数据结构。

Java中栈和堆的区别

栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方。

与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

 

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。

当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

 

堆内存用来存放由new创建的对象和数组,在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。

引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

 

Java中变量在内存中的分配

1、类变量(static修饰的变量):

在程序加载时系统就为它在堆中开辟了内存,堆中的内存地址存放于栈以便于高速访问。

静态变量的生命周期–一直持续到整个”系统”关闭。

 

2、实例变量:

当你使用java关键字new的时候,系统在堆中开辟并不一定是连续的空间分配给变量(比如说类实例),然后根据零散的堆内存地址,通过哈希算法换算为一长串数字以表征这个变量在堆中的”物理位置”。

 

实例变量的生命周期–当实例变量的引用丢失后,将被GC(垃圾回收器)列入可回收“名单”中,但并不是马上就释放堆中内存。

 

3、局部变量:

局部变量,由声明在某方法,或某代码段里(比如for循环),执行到它的时候在栈中开辟内存,当局部变量一但脱离作用域,内存立即释放。

5、Java类加载过程?

解析

当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现这个类的初始化。

1、加载

加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。

这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。

然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。

类加载阶段:

(1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。

(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。

(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。

Student类加载过程图示:

2、链接

链接包括验证、准备以及解析三个阶段。

(1)验证阶段。

主要的目的是确保被加载的类(.class文件的字节流)满足Java虚拟机规范,不会造成安全错误。

(2)准备阶段。

负责为类的静态成员分配内存,并设置默认初始值。

(3)解析阶段。

将类的二进制数据中的符号引用替换为直接引用。

说明:

符号引用。

即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

直接引用。

可以理解为一个内存地址,或者一个偏移量。

比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。

举个例子来说,现在调用方法hello(),这个方法的地址是0xaabbccdd,那么hello就是符号引用,0xaabbccdd就是直接引用。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

3、初始化

初始化,则是为标记为常量值的字段赋值的过程。

换句话说,只对static修饰的变量或语句块进行初始化。

如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

4、小结

类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载(垃圾回收)。

5、扩展

常见问题:

在自己的项目里新建一个java.lang包,里面新建了一个String类,能代替系统String吗?

不能,因为根据类加载的双亲委派机制,会将请求转发给父类加载器,父类加载器发现冲突了String就不会加载了。

6、GC是什么?

为什么要有GC?

GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,

Java语言没有提供释放已分配内存的显示操作方法。

Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。

要请求垃圾收集,可以调用下面的方法之一:

System.gc()或Runtime.getRuntime().gc(),但JVM可以屏蔽掉显示的垃圾回收调用。

 

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。

垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。

在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。

移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

 

7、垃圾回收的优点和原理。

并考虑2种回收机制

1、java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。

2、由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。

3、垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。

4、垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。

5、程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。

垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。

回收机制有引用计数和对象引用遍历两种。

以下为扩展内容:

(选择记忆即可)

1)引用计数收集器

引用计数是垃圾收集器中的早期策略。

在这种方法中,堆中每个对象(不是引用)都有一个引用计数。

当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。

当任何其它变量被赋值为这个对象的引用时,计数加1,但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。

任何引用计数为0的对象可以被当作垃圾收集。

当一个对象被垃圾收集时,它引用的任何对象计数减1。

2)跟踪收集器

早期的JVM使用引用计数,现在大多数JVM采用对象引用遍历。

对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。

如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。

8、System.gc()和Runtime.gc()会做什么事情?

GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。

Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

System.gc();就是呼叫java虚拟机的垃圾回收器运行回收内存的垃圾。

每个Java应用程序都有一个Runtime类实例,使应用程序能够与其运行的环境相连接。

可以通过getRuntime方法获取当前运行时。

Runtime.getRuntime().gc();

java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同。

唯一的区别就是System.gc()写起来比Runtime.getRuntime().gc()简单点,在字节码层面上调用前者比调用后者短一点点,前者是1条字节码而后者是2条。

System.gc():

invokestaticjava/lang/System.gc()V

Runtime.getRuntime().gc():

invokestaticjava/lang/Runtime.getRuntime()Ljava/lang/Runtime;invokevirtualjava/lang/Runtime.gc()V

实际运行起来性能几乎一样。

不过如果对字节码大小非常非常敏感的话建议用System.gc()。

从通常的代码习惯说也是System.gc()用得多些。

其实基本没什么机会用得到这个命令,因为这个命令只是建议JVM安排GC运行,还有可能完全被拒绝。

 

GC本身是会周期性的自动运行的,由JVM决定运行的时机,而且现在的版本有多种更智能的模式可以选择,还会根据运行的机器自动去做选择,就算真的有性能上的需求,也应该去对GC的运行机制进行微调,而不是通过使用这个命令来实现性能的优化。

 

9、为什么说Synchronized是一个悲观锁?

乐观锁的原理又是什么?

什么是CAS,它有什么特性?

Synchronized显然是一个悲观锁,因为它的开发策略就是悲观的:

不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。

先进行操作,如果没有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。

这种乐观的并发策略的需要实现不需要线程挂起,所以被称为非阻塞同步。

乐观锁的核心算法是CAS(CompareandSwap,比较并交换,它涉及到三个操作数:

内存值、预期值、新值。

当日仅当预期值和内存值相等时才将内存值修改为新值。

这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。

CAS具有原子性,它的原子由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令,JDK中提供了Unsafe类执行这些操作。

10、线程池中的线程是怎么创建的?

是一开始就随着线程池的启动创建好的吗?

显然不是的。

线程池默认初始化后不启动Worker,等待有请求时才启动。

每当我们调用execute()方法添加一个任务时,线程池会做如下判断:

∙如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

∙如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

∙如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

∙如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

当一个线程完成任务时,它会从队列中取下一个任务来执行。

当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。

如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

 

11、很多人都说要慎用ThreadLocal,谈谈你的理解,使用ThreadLocal需要注意些什么?

ThreadLocal的实现是基于一个所谓的ThreadLocalMap,在ThreadLocalMap中,它的key 是一个弱引用。

通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例外,它并没有这么做。

这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应ThreadLocalMap!

这就是很多OOM的来源,所以通常都会建议,应用一定要自己负责remove,并且不要和线程池配合,因为worker线程往往是不会退出的。

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 党团工作 > 入党转正申请

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1