☰
Java基础面试题精讲:122道真题集锦与避坑指南
2026/10/18 1:17:01 网站建设 项目流程

简介:这是为Java求职者整理的基础面试题库资源,包含基础题122道、代码题19道,覆盖面向对象抽象继承封装多态、String与StringBuffer差异、int与Integer装箱拆箱、运行时异常与检查异常、Servlet生命周期与CGI对比、EJB中SessionBean与EntityBean区别、ArrayList与Vector及LinkedList存储性能等高频考点,也对Collection相关基础进行了梳理。资源以单个PDF文档打包,文件数1个,压缩包大小267KB,便于下载后离线翻阅。目前已有1549人浏览学习,适合初、中级Java开发者在求职、跳槽或日常查漏补缺时使用。PDF不仅逐题给出答案,还对易混淆概念做了横向辨析,如String不可变时推荐StringBuilder、LinkedList插入快但随机访问慢、Vector线程安全带来性能损耗等,同时19道代码题可以帮助读者巩固语法细节、练习实际编码与输出分析,加深对Java基础知识的理解。整体内容覆盖校招笔试与一轮技术面常见范围,适合按题号系统刷题和考前集中复习。

1. 为什么122道Java基础面试题还能翻车:先看看这份集锦的边界在哪里

做Java面试准备的人,手上多半存过好几份“面试题大全”,但真到面试现场被追问“那你说说为什么这样设计”“换个场景还适用吗”,很多人就卡住了。这份《Java面试题及答案集锦(基础题122道,代码题19道)》,我完整拆过一遍,覆盖面确实老练:面向对象、String与基本类型、集合选型、异常、线程、Servlet生命周期,甚至还有一批现在已经边缘化的EJB和JSP题目。它最大的价值不是让你背下122个答案,而是给你一张完整的考点地图,让你知道哪些题常考、哪些题有坑、哪些题追问起来能区分出真假熟练度。适合准备校招或社招初、中级岗位的人,也适合平时带实习生、想快速摸底团队基础水平的面试官。

2. 面向对象与数据类型:从四特征到int和Integer的追问式拆解

2.1 面向对象四特征:背得出关键词不等于答得对追问

“抽象、继承、封装、多态”这八个字,几乎是每场Java面试的开场题。问题在于,很多人只背了结论,被追问一两层就露馅。原答案里有个细节容易被忽略:抽象包括过程抽象和数据抽象,过程抽象对应的是“方法”,数据抽象对应的是“类”。面试官如果问“抽象和封装到底有什么区别”,标准答法是——抽象解决的是“我要关注哪些特征”,封装解决的是“这些特征怎么对外隐藏”。所以抽象是设计层面的取舍,封装是实现层面的约束。

继承部分要能回答“子类能继承父类的私有成员吗”这类追问。准确说,私有成员可以被继承到子类对象里,但子类不能直接访问,需要通过父类提供的公有或受保护方法间接使用。经常有候选人答“不能继承私有成员”,这个说法在初面可能能混过去,但二面遇到较真的面试官就会被纠正。

多态部分值得多说两句。原答案提到“参数化多态性和包含多态性”,这个提法在普通面经里不常见。参数化多态一般指泛型,包含多态指继承和接口实现带来的运行时行为差异。回答这类题时,我习惯先给定义,再给一个具体例子:Shape接口有draw方法,Circle和Rectangle各自实现,调用方拿到Shape引用后调用draw,实际执行的是具体子类的版本。这就是“对同一消息作出不同响应”的直观解释。

答题框架建议按“定义 → 例子 → 追问”三层来准备,不要只背课本原句。面试官见过太多一模一样的书面答案,你多说一句“过程抽象和数据抽象的区别”,就能从“背过”的群体里跳出来。

2.2 int与Integer:基本类型和包装类型之间的四层差异

int和Integer,看起来是最基础的知识点,但这道题能挖的角度比想象中多。原答案给出了一个关键定位:Java提供两种不同的类型,引用类型和原始类型,int是原始数据类型,Integer是int的封装类。这两个类型的差异要从四个方面去说才完整。

第一是存储位置。原始类型变量直接存值,作为局部变量时在栈上;Integer是对象,作为局部变量时引用在栈上、对象在堆上。第二是默认值。对象引用实例变量的缺省值是null,原始类型实例变量的缺省值跟类型有关,int默认是0,boolean默认是false。这道题经常伴随“Integer能自动拆箱吗”的追问,Java 5之后有了自动装箱拆箱,Integer和int可以直接互相赋值,但要注意null拆箱会抛NullPointerException。第三是有无方法。Integer可以用parseInt、toString、compareTo等方法,int做不到。第四是性能,原始类型没有对象头,内存占用小、访问快。

再补充一个高频追问:Integer的缓存范围。Integer缓存了-128到127之间的对象,所以两个Integer都用Integer.valueOf(100)赋值时,比较结果可能是true;一旦超过127,比如Integer a = 128; Integer b = 128; a == b就是false。这道题几乎年年出现,原题库里没有明说,但当你讨论int与Integer区别时,把它作为“面试官可能会追加的一层”主动讲出来,印象分会明显不一样。

2.3 String、StringBuffer、StringBuilder与经典对象计数题

String系列在基础题库里的地位非常牢固。首先是String是不是基本数据类型?不是。String类是final类型,不可继承、不可修改。原答案里提到“为了提高效率节省空间,我们应该用StringBuffer类”,这句话放到现在需要修正一下:StringBuffer是线程安全的,方法加了synchronized,单线程环境下反而有性能损耗;StringBuilder是Java 1.5引入的非同步版本,单线程字符串拼接首选。

三者的选择逻辑很好记:字符串内容不变,选String;单线程大量拼接,选StringBuilder;多线程共享可变字符串,选StringBuffer。面试时能说出“StringBuilder不是线程安全”这个点,说明你真的用过而不是只背过概念。

“String s = new String("xyz")创建了几个String Object”,原始答案是“两个”,但更严谨的说法是“一个或两个”。因为如果“xyz”这个字面量在字符串常量池里已经存在,那么只在堆上创建了一个新对象;如果常量池里没有,JVM会先把“xyz”放入常量池,再在堆上创建一个对象。面试时给出这个区分,比直接报“两个”更能体现底层理解。

还有一个极易混淆的考点:String有没有length()方法?数组有没有length()?Standard答案是String有length()方法,数组只有length属性。这两个写法在写代码时经常打反,后面避坑章节会专门讲。

3. 集合、异常与线程:ArrayList、HashMap、sleep与wait的考场送命题

3.1 List三兄弟:ArrayList、Vector、LinkedList的存储性能和选型

ArrayList、Vector、LinkedList这组对比,是集合体系最经典的入门题。原答案给得比较完整:ArrayList和Vector都用数组存储,数组预留了多余容量以便插入和扩容,所以按序号索引数据快、插入数据慢;Vector因为方法加了synchronized,线程安全但性能较差;LinkedList用双向链表实现,按序号索引需要从头或尾遍历,插入时只需修改前后节点的引用,所以插入快。

实际面试时,面试官更想听到的是“你怎么选型”。我一般这样答:绝大多数业务场景就是读多写少,用ArrayList;如果你明确知道会在列表中间频繁插入或删除,考虑LinkedList;如果涉及多线程并发访问,Vector不是首选,现代写法是CopyOnWriteArrayList——读操作不加锁,写操作复制一份新数组再改,适合读频率远高于写的场景。把这三个层次的差异说完,这道题就算过关了。

简单总结一张表:

实现类数据结构随机访问插入删除线程安全
ArrayList动态数组快,O(1)慢,涉及元素移动否
Vector动态数组快,O(1)慢,涉及元素移动是,方法级同步
LinkedList双向链表慢,O(n)快,O(1)否

注意一个细节:LinkedList插入虽然拿到节点后是O(1),但如果插入位置需要先遍历定位,整体复杂度还是O(n)。面试时主动点出这个限定条件,显得更严谨。

3.2 HashMap与Hashtable:null、线程安全和版本演变

HashMap和Hashtable的区别,原答案列了三四条,我梳理一下其中最稳定的几条。第一,HashMap允许null作为key或value,Hashtable不允许;第二,HashMap是非线程安全的,Hashtable方法是同步的;第三,HashMap是Java 1.2引进的Map接口实现,Hashtable继承自Dictionary类,属于JDK 1.0遗留;第四,Hashtable有contains方法,HashMap把它拆成了containsKey和containsValue,避免语义歧义。

有一个常踩的坑要提示:虽然Hashtable线程安全,但它用的是粗暴的全局方法锁,并发效率很低。现代Java并发场景下,优先考虑ConcurrentHashMap,它利用分段锁或CAS机制,锁粒度小得多。这里还可以补一句HashMap在并发下的风险:多线程同时put可能造成数据丢失,JDK 1.7以前甚至在扩容时会形成环形链表导致死循环,JDK 1.8改成尾插法后避免了这个问题。这些内容在基础题库里没有,但讲HashMap不提它们,显得像只背了对比表。

如果面试中聊到这,最好还能说出HashMap在JDK 1.8后引入了红黑树优化:当链表长度超过TREEIFY_THRESHOLD(默认是8)且数组长度达到64时,链表会转成红黑树,把最坏情况下的查找时间从O(n)降到O(log n)。这个参数在原题库中完全没有,却是当前面试的高频追问点。

3.3 异常体系:运行时异常、受检异常和Error的三层边界

异常相关的题,核心是搞清楚“运行时异常和一般异常有什么区别”。原答案的说法是:运行时异常表示虚拟机的通常操作中可能遇到的异常,是常见运行错误;Java编译器要求方法必须声明抛出可能发生的非运行时异常,并不要求必须声明未被捕获的运行时异常。这句话反过来说就是:受检异常(checked exception)编译期强制处理,运行时异常(RuntimeException)编译期不强制。

常被问到的RuntimeException子类有一长串,最典型的几个要能脱口而出:NullPointerException、ArithmeticException、ClassCastException、IndexOutOfBoundsException、IllegalArgumentException。回答“给我一个你最常见到的runtime exception”时,给一个具体场景比单纯报名字好,比如“最常见的是NullPointerException,调用某个未初始化对象的实例方法时触发”。

Error也要单独说清楚。error表示恢复不是不可能但很困难的问题,比如内存溢出OutOfMemoryError、栈溢出StackOverflowError。程序不应该去捕获并试图处理这类问题,因为即使捕获了,系统状态也可能已经不可靠。一般性原则是:调用方可以恢复的状态,用受检异常;属于编程错误的,用运行时异常;系统级故障,让它以Error的形式暴露出来。

3.4 线程状态与同步:sleep、wait、notify的锁行为差异

sleep和wait的区别是线程题的必考点。原答案给出了两个关键结论:sleep是Thread类的静态方法,调用后线程暂停执行但不释放对象锁,到指定时间自动恢复;wait是Object类的方法,调用后线程释放对象锁,进入该对象的等待池,必须由其他线程调用notify或notifyAll才能唤醒。实际写并发代码时,这个区别直接影响死锁是否发生。如果持锁后调用sleep,其他线程拿不到锁只能干等;如果调用wait,锁被释放,其他线程有机会进入临界区。

同步的实现方面,经典做法是synchronized配合wait、notify,synchronized可以加在方法上,也可以加在代码块上。加在方法上时锁的是当前对象实例,静态方法锁的是Class对象。面试追问“notify唤醒的是哪个线程”要注意,规范说法是JVM决定唤醒哪个线程,并且不是按优先级,如果你想确保某个特定线程被唤醒,就得自己设计机制,比如用多个条件队列或显式Lock。

线程状态部分,老题库把Java线程归为“运行、就绪、挂起、结束”四种,这个说法在早期资料里常见,但现代JDK的Thread.State枚举里实际是六种:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。遇到较真版本号的面试官,用六状态回答更稳妥;如果他不讲究版本,用四状态概括再补充一句“不同版本划分方式略有差异”,也能体现出你懂底层状态机。

4. 类加载与三道代码题:ClassLoader机制、线程增减j与Singleton写法

4.1 ClassLoader加载class文件的机制:从查找路径到双亲委派

原答案对类加载的说明比较简短:JVM中类的装载由ClassLoader和它的子类实现,ClassLoader负责在运行时查找并装入类文件。这段描述方向正确,但作为面试答案信息量不够。一般我会补上三层加载器结构和双亲委派模型的理解。

JVM默认有三种类型的类加载器:启动类加载器(Bootstrap)、扩展类加载器(Extension)和应用类加载器(App)。启动类加载器负责加载JDK核心类库,比如rt.jar里的类;扩展类加载器负责加载扩展目录下的jar;应用类加载器负责加载classpath指定的类。三者不是继承关系,而是父子委派关系:类加载请求先交给父加载器尝试加载,父加载器加载不到,才轮到子加载器自己加载。

为什么要这样设计?根本目的是为了安全。如果应用类加载器自己写一个java.lang.String,双亲委派会让启动类加载器先加载JRE自带的String,从而防止核心类库被篡改。面试时把“双亲委派 + 安全性”这套逻辑说出来,比单纯列加载器名字强得多。另一个常见追问是“如何打破双亲委派”,典型做法是自定义类加载器并重写loadClass方法,不先请求父加载器。只把答案念出来而不讲动机。

类加载的过程还包括“加载、验证、准备、解析、初始化”五个阶段。初始化阶段对应静态变量赋值和静态代码块执行,这也是为什么静态块里引用前向声明的静态变量会出问题。这个五阶段在基础题里不常被直接问,但被追问时能展开,就是明显加分。

4.2 代码题实战:4个线程对j增减,内部类与synchronized配合

这道题原文给了一份可运行的示例代码,整理后如下:

public class ThreadTest1 { private int j; public static void main(String[] args) { ThreadTest1 tt = new ThreadTest1(); Inc inc = tt.new Inc(); Dec dec = tt.new Dec(); for (int i = 0; i < 2; i++) { Thread t = new Thread(inc); t.start(); t = new Thread(dec); t.start(); } } private synchronized void inc() { j++; System.out.println(Thread.currentThread().getName() + "-inc:" + j); } private synchronized void dec() { j--; System.out.println(Thread.currentThread().getName() + "-dec:" + j); } class Inc implements Runnable { public void run() { for (int i = 0; i < 100; i++) { inc(); } } } class Dec implements Runnable { public void run() { for (int i = 0; i < 100; i++) { dec(); } } } }

这段代码的关键在于两个方法都加了synchronized,锁是当前ThreadTest1实例tt。四个子线程通过Runnable内部类共享同一个tt对象,所以inc和dec对j的读写是串行化的,j不会出现丢失更新的问题。输出顺序不确定,因为线程调度由JVM决定,inc和dec谁先抢到锁都可能,最后j理论上回到0,但中间打印的j值序列每次运行都不一样。

有个容易忽略的点:为什么不用volatile修饰j?因为synchronized的进入和退出本身就建立了happens-before关系,线程释放锁时对共享变量的修改,对后续获取同一把锁的线程是可见的。加volatile不是必需,反而是synchronized已经覆盖了可见性和原子性两重保障。

写这道题时,面试官通常还会问“如果要保证每个线程每次增减后j不重复读旧值,你怎么改”。常见做法是把循环次数提到更大、增加线程数,或者改用ReentrantLock显式控制。建议把原题的内部类版本和Lambda版本都练一遍,Lambda版本里同一个runnable实例同样可以被多个线程复用,但写法更简洁。

4.3 Singleton两种写法与“2乘以8等于几”

Singleton是代码题里的老面孔,原资料给出了两种基础写法:饿汉式和懒汉式。饿汉式就是类初始化时直接创建唯一实例:

public class Singleton { private Singleton() {} private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } }

这种写法线程安全,因为静态字段在类加载初始化阶段就被赋值,JVM保证这个过程的线程安全。缺点是无论有没有用到实例,类一加载就把对象创建了,浪费资源。懒汉式原先把getInstance方法整体加synchronized,保证了延迟加载和线程安全,但每次获取实例都要走一遍方法锁,性能差。结合面试现状,更推荐的版本是双重检查锁加volatile:

public class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }

volatile在这里的作用是防止指令重排序。instance = new Singleton()这条语句在字节码层面会拆成三步:分配内存、调用构造器、把引用赋值给字段。如果后两步被重排,另一个线程可能拿到一个构造未完成的对象。volatile禁止了这个重排,双检锁才真正可靠。

“用最有效率的方法算出2乘以8等于几”是另一道送分题,答案是2 << 3。左移三位相当于乘以2的3次方。老面试官视角里,位移运算比乘法指令更快,所以是“最有效率”的解法。不过现在JVM的JIT编译器对乘以常量会做强度削减,自动变成移位或加法指令,直接写2 * 8代码上差距已经很小。回答这道题时先给2 << 3,再补一句“现代编译器已能自动优化乘法指令”,显得你既熟悉位运算,又了解JVM行为。

5. 避坑指南:122道题里最容易答错的六个细节(含原因与解决)

5.1 现象:short s1 = s1 + 1编译报错,s1 += 1却能通过

不少人在白板写代码时栽在这道题上。原因在于表达式s1 + 1中,s1是short类型,1是int常量,Java的二元数值运算会把short自动提升到int,结果就是int类型,直接赋值给short需要强制窄化转换,编译必然报错。而s1 += 1是复合赋值运算符,语言规范规定它自带隐式强制转换,等价于s1 = (short)(s1 + 1),所以能编译通过。

解决这个问题,理解“Java自动类型提升发生在运算前,赋值时的窄化必须显式”就够了。面试时如果能把这个现象背后的JLS规则说出来,比只记结论更有说服力。

5.2 现象:Math.round(-11.5)被写成-12,实际结果是-11

Math.round的规则是返回与参数最接近的long,底层实现是floor(x + 0.5)。-11.5加0.5等于-11,floor(-11)就是-11。正数大家都不会错,负数因为忽略了floor方法的取整方向而发生误判。解决方法是把公式记牢:round(x) = floor(x + 0.5),以后遇到负数,先加0.5再向下取整,就不会再踩坑。

5.3 现象:数组的length和String的length()经常写反

现场写代码时,有人写arr.length()报错,也有人写str.length不报错但结果不对。原因是数组是JVM内建对象,length是它的公开字段属性,不是方法;String是普通类,通过length()方法获取字符数。数组没有length()方法,String没有length属性,两者刚好相反。解决方法是形成条件反射:看到数组就在脑子里想length字段,看到String就想length()方法调用。

5.4 现象:原题答案说“switch不能作用在String上”,放到现在会被打问号

这道题的表述有版本时效性。早期Java确实只支持int、short、char、byte以及枚举,String不行。但Java 7开始switch支持String作为条件表达式,通过hashCode和equals实现分支判断。long类型至今不受支持。所以回答时要分版本:如果你在Java 7之前,答案是不能用String;在当今开发环境,答案是“可以作用于String,但不能作用于long”。有版本意识的答案,比照搬老题库强得多。

5.5 现象:说“对象作为参数传递是引用传递”,被面试官当场纠正

标准的Java语语义只有值传递。当一个对象实例作为参数传入方法时,传递的是对象引用的一个拷贝,这个拷贝的值指向同一个对象。所以在方法内修改对象的属性,会反映到原对象上;但如果在方法内给参数重新赋值一个新的对象,原对象引用不会变。正确的表述是“对象引用按值传递”,而不是“对象按引用传递”。后一种说法容易让面试官认为你没有理解引用和值的区别。

5.6 现象:final、finally、finalize三兄弟傻傻分不清

这三个词拼写相近,含义却完全不同。final是修饰符,修饰类表示类不可继承,修饰方法表示方法不可覆盖,修饰变量表示变量引用不可再赋值。finally是异常处理结构的一部分,无论try块中是正常结束还是抛出异常,finally里的代码都会执行,常用于释放资源。finalize是Object类里的一个方法,垃圾回收器在回收对象前会调用它,但现代Java开发已经明确不推荐依赖finalize做资源清理,因为它执行时机不确定甚至可能不执行。回答时把三者放在“修饰符、异常关键字、Object回调方法”三个维度上区分清楚即可。

6. 把122道题刷进自己的答案里:三遍刷题法就够了

6.1 从背题到复述:一套人人能用的面试复习流程

我用这套方法帮过不下十个准备Java面试的开发者,核心就是下面三步。第一步,按专题分组刷题,不要从第1题线性刷到第122题。这份集锦本身是按“面向对象 → 数据类型 → 集合 → 异常 → 线程 → Web与容器”的自然顺序排列的,把同专题的题放一天集中消化,比如周一专门刷集合,周二专门刷异常和线程,每组不超过12道。每道题先合上答案,用自己的话在纸上写出关键词,再打开答案对照,遗漏的点就是你的弱点记录。

第二步,给每道题准备一个追问清单。基础题库的答案往往是结论性的,面试官一定会追问“为什么”或“换一种场景怎么做”。对每道题,试着自己问自己两个问题:这个设计解决了什么问题?对比方案有什么代价?比如学ArrayList和LinkedList时,追问“读写比多少时该切换选型”,学String和StringBuffer时,追问“为什么单线程推荐StringBuilder而不是StringBuffer”。能自问自答的题,才真正算理解了。

第三步,代码题手写三遍。打包里的19道代码题不算多,但每一道都值得白板演示。第一遍照着答案抄一遍,目标是看懂每行代码的作用,比如4线程增减j里的synchronized为什么加在方法上而不是run方法上;第二遍合上答案,凭记忆写出来,写不出来就回到第一步;第三遍完全独立写,写完后主动思考有没有更优方案,例如把懒汉Singleton改成双重检查锁加volatile。三道题下来,手感和只背答案完全不同。

我以前带过一个开发者,把122道题背了整整三周,结果面试时被“Math.round(-11.5)等于多少”打回原形,因为他背的答案是“-11”,却不明白为什么。从那以后,我每次复习这套题都强制自己走一遍“复述 → 追问 → 手写”的流程,不再碰纯背诵的复习方式。这份集锦适合当题库底稿,但要真正在面试里把答案变成自己的话,一定要多追问自己一层为什么。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询