简介:这份Java编程面试题文档面向准备初、中高级Java开发岗位面试的学习者,围绕基础概念、循环与条件判断、数学运算及常见算法组织内容,可用于面试前自测、课堂练习与代码复盘。压缩包内共1个doc文件,约104KB,以题目说明和Java代码示例为主,涵盖斐波那契数列、101—200素数判断、水仙花数、正整数分解质因数、条件运算符嵌套成绩分级以及最大公约数与最小公倍数等经典题目。每道题既给出程序实现,也提示循环终止条件、取模除法、平方根缩小范围等分析思路,便于读者理解从题目到代码的转换过程。目前已有142人学习,适合希望系统梳理Java面试高频编程题、训练逻辑思维和现场编码能力的开发者查阅。
1. 从一对兔子开始:这套 Java 面试题集到底练什么
面试间里最容易被要求手写的一段代码,往往不是框架配置,而是「一对兔子从第三个月起每月生一对」这种看着像小学奥数的题。这套 Java 编程面试题集就围绕这类题展开:19 个独立的类,从lianxi01排到lianxi19,每题一个main方法,覆盖斐波那契递推、素数判断、水仙花数、质因数分解、最大公约数、字符统计、完数、猴子吃桃、菱形打印这些经典练手题。它不讲 Spring,也不碰 JVM 调优,适合两类人:刚学完 java 语法想找手感的新手,以及写了几年业务代码、发现自己遇到循环边界和取模运算还是会翻车的开发者。整套东西零第三方依赖,一个 JDK 就能全部跑起来,这也是它最舒服的地方。
2. 题目集整体结构:19 个 lianxi 类背后的六类基本功
2.1 文件组织:一个类一个 main,没有包声明
把这套题解压出来,看到的是一堆平铺的.java文件,lianxi01.java到lianxi19.java排列在同一层,没有src/main/java这层约定,也没有package声明,自然也就没有pom.xml或者build.gradle。每个文件的骨架都长一个样:一个public class lianxiXX,里面一个public static void main(String[] args),逻辑全写在 main 里,跑完直接System.out.println出结果。
这种写法在工程上当然是不合格的,但它对刷题有好处——打开一个文件就是一道完整的题,不用在多个类之间跳。有三个文件稍微特殊:程序 6 在lianxi06.java里额外定义了class deff,程序 14 和 15 各自定义了class input。这几个类没有加public,因为一个.java文件里只能有一个 public 类,且类名必须和文件名一致;非 public 的类可以随便塞在同一个文件里,编译时会生成独立的.class。
提示:这套代码不要直接拖进正在维护的业务工程里。没有包名的类属于默认包,默认包里的类无法被有包名的类
import,混进去之后只会给自己制造编译错误。
类名的写法也值得说一句:lianxi01全小写,按 Java 命名规范类名该用大驼峰,编译器不管这个,但 IDE 会给你一片黄色波浪线。真要长期用,改名比忍着更省事。
2.2 六类基本功与题号映射
19 道题看着散,其实能归成六类,摸清分类之后再刷,比从头到尾顺一遍效率高得多。
| 类别 | 题号 | 主要考察点 |
|---|---|---|
| 递推与数列 | 1、8、17 | 变量轮换、增量构造、逆向推导 |
| 素数、因数与约数 | 2、4、6、9 | 循环上界、整除判断、辗转相除 |
| 数位拆解 | 3、13 | 取模与整除取高位、平方根判定 |
| 条件与分段 | 5、12、15 | 三元嵌套、区间累进、多分支排序 |
| 输入与字符串 | 7、14、15 | Scanner 读取、字符分类、日期合法性 |
| 枚举与图形 | 11、16、18、19 | 多重循环、排列去重、按行控制空格与星号 |
数位拆解这一类特别能看出基本功。程序 3 求水仙花数,核心是三行:b3 = m / 100拿百位,b2 = m % 100 / 10拿十位,b1 = m % 10拿个位。这个「先取模再整除」的套路在程序 11 拼三位数、程序 13 判断完全平方数时都会反复出现,写熟了一遍就通。
枚举类的题则是练剪枝的思路。程序 11 用三重循环枚举 1 到 4 组成的三位数,靠x != y && y != z && x != z去重,最后得到 24 个;程序 18 的乒乓球队配对,本质也是枚举加排除条件,只是排除条件被硬编码成了continue分支。
2.3 从工程视角看这三处短板
第一处是没有断言。19 个类全靠肉眼比对控制台输出,没人告诉你正确答案是多少,改错了也看不出来。程序 1 里M=24是写死的,你想验证第 30 个月就得手动改常量,改完还得自己算对不对。
第二处是输入校验基本缺席。程序 14 判断某天是这一年的第几天,校验条件写的是year < 0 || month < 0 || month > 12 || day < 0 || day > 31,2 月 30 日、4 月 31 日都能顺利通过,算出个不存在的日期。这类漏洞在面试里未必扣分,但在真实系统里就是血泪经验。
第三处是条件硬编码。程序 18 原题只说了「a 不和 x 比,c 不和 x、z 比」,代码里却额外排除了a vs y、b vs z、b vs y,等于是把出题人没写的隐含前提偷偷补进了判断里。结果恰好正确(只剩 a-z、b-x、c-y 三组),但逻辑来源经不起追问。程序 19 的菱形同理,H = 7, W = 7直接写死,想画 9 行的菱形得改两处。
3. 核心代码拆解:递推、素数、质因数与分段条件的写法差异
3.1 递推题的变量轮换:兔子与猴子吃桃
程序 1 的原写法用了三个变量:
int f1 = 1, f2 = 1, f, M = 24; for (int i = 3; i <= M; i++) { f = f2; // 先把上个月的值暂存 f2 = f1 + f2; // 本月 = 前两月之和 f1 = f; // f1 前移一位 System.out.println("第" + i + "个月的兔子对数: " + f2); }这段代码的正确性依赖一个细节:f2 = f1 + f2执行时,右边的f2还是旧值,左边的f2已经被覆盖,所以必须先用f把旧值存下来。逻辑说明:f1始终代表「上上个月」,f2代表「上个月」,循环每转一圈两者一起前移。参数说明:M是模拟的总月数,取 24 时最后一行的输出是 46368;如果想把月数做成可配置,把它从args[0]里解析出来就行。
如果不想引入第三个变量,也可以写成等价的加法赋值,可读性更好一点:
for (int i = 3; i <= months; i++) { int cur = f1 + f2; // 本月 f1 = f2; // 上上月 <- 上个月 f2 = cur; // 上个月 <- 本月 System.out.println("第" + i + "个月:" + cur); }程序 17 的猴子吃桃是反着推的,思路刚好相反:已知第 10 天剩 1 个,每天吃掉前一天的一半再加一个,那么前一天就是(x + 1) * 2。原代码从i = 2循环到i <= 10,一共推 9 次,得到 1534。这里最容易翻车的是循环起点:从第 2 天开始推,是因为「第 10 天剩 1 个」已经是已知条件,推 9 次就回到第 1 天,多推一次就变成第 0 天了。我刚看这段的时候就把起点写成 1,结果算出翻倍的数字。
3.2 素数判断:循环上界 sqrt(n) 与标志位的初始化
程序 2 求 101 到 200 之间的素数,原来的标志位写法有点绕:
boolean b = false; for (int j = 2; j <= Math.sqrt(i); j++) { if (i % j == 0) { b = false; break; } else { b = true; } // 只要有一次除不尽就置 true }这个写法能跑,但它依赖「循环至少执行一次」和「最后一次迭代的赋值」两个隐含前提,换个场景就会失效。比如循环一次都没进去(i < 4),b仍然是初始的false,会把 2 和 3 判成非素数。习惯做法是先假设是素数,发现能整除再推翻:
boolean isPrime = true; for (int j = 2; j <= (int) Math.sqrt(i); j++) { if (i % j == 0) { isPrime = false; break; } } if (isPrime) { count++; System.out.println(i); }逻辑说明:Math.sqrt(i)作为上界,是因为如果i有一个大于sqrt(i)的因子,那必然对应一个小于sqrt(i)的因子,前半段没找到就不用再往后找了。参数说明:原代码外层写的是i += 2,从 101 开始只跑奇数,因为偶数除了 2 以外都不可能是素数。这个优化在这里成立,但如果把范围改成从 2 开始,就必须单独处理 2。最终结果是 101 到 200 之间共 21 个素数。
3.3 质因数分解:while 循环里的 k 与 n
程序 4 是这 19 题里唯一需要手动输入的题之一,写法挺典型:
Scanner s = new Scanner(System.in); int n = s.nextInt(); int k = 2; System.out.print(n + "="); while (k <= n) { if (k == n) { System.out.println(n); break; } // 分解到自身,结束 else if (n % k == 0) { System.out.print(k + "*"); n = n / k; } // 找到因子,n 缩小 else { k++; } // 除不尽,试下一个 }逻辑说明:k从最小的质数 2 开始,如果能整除就把它打印出来,并把n除以k的商赋回给n,注意这里k不递增,因为同一个质因数可能出现多次(90 里有 2 个 3)。除不尽才k++,去找下一个可能的因子。循环终止条件是k <= n,当k == n时说明剩下的n本身就是一个质数,直接输出并break。
参数说明:这里有个隐藏代价,n被就地覆盖了,原始输入值丢了。如果后面还想打印「输入的数是 90」,得提前存一份。另外输出语句System.out.print(n + "=")用的是print而不是println,因为后面的因子要接在同一行,最后那个println(n)负责换行。输入 90 的输出是90=2*3*3*5。
3.4 三元嵌套与分段提成:条件分支的两种写法
程序 5 的成绩等级判定用了一层三元嵌套:
grade = x >= 90 ? 'A' : x >= 60 ? 'B' : 'C';逻辑说明:三元运算符从右往左结合,第一层判 90 以上,不满足才进第二层判 60 以上,剩下的全是 C。参数说明:x是int,grade是char,三元表达式的两个分支都是字符字面量,类型能对上。这种写法在只有三档时还算清爽,档位再多就该换if-else或者表驱动了,嵌套超过三层基本没法读。
程序 12 的奖金分段是同一个问题的复杂版本,也是整套题里最容易算错的一道。原代码把前几档的提成合并成了系数:
} else if (x > 60 && x <= 100) { y = 20 * 0.175 + 20 * 0.05 + 20 * 0.03 + (x - 60) * 0.015; }问题在20 * 0.175:0.175 是 10% 和 7.5% 两个档位费率相加得来的,乘 20 等于把前 20 万又算了一遍,实际这块应该是10 * 0.1 + 10 * 0.075 = 1.75,而不是 3.5。这类「合并系数」的写法一旦档位调整就会全线崩。我一般改成区间累进:
static double bonus(double profit) { double[] limit = {10, 20, 40, 60, 100}; // 各档上限,单位万元 double[] rate = {0.10, 0.075, 0.05, 0.03, 0.015, 0.01}; // 对应区间费率 double sum = 0, prev = 0; for (int i = 0; i < limit.length; i++) { if (profit <= prev) break; // 利润落在更低的档,直接停 double part = Math.min(profit, limit[i]) - prev; // 本档实际计入的部分 sum += part * rate[i]; prev = limit[i]; } if (profit > 100) sum += (profit - 100) * rate[5]; // 超过 100 万的部分 return sum; }逻辑说明:prev记录上一档的上限,Math.min(profit, limit[i]) - prev就是当前利润在本档区间里占的长度。参数说明:limit和rate两个数组必须等长对应前五档,第六档(100 万以上)没有上限,所以单独处理。利润传 15 时结果是 1.375 万,和手工算的10*0.1 + 5*0.075一致;传 130 时,前 100 万拿 3.35 万,超出部分 30 万按 1% 拿 0.3 万,合计 3.65 万。
4. 跑通与验证:JDK 环境、编译命令和三组预期输出
4.1 最小运行环境与编译命令
跑这套题不需要构建工具,JDK 8 及以上都行,代码里用到的Scanner、Math.sqrt、String.toCharArray都是很早就有的 API,没有版本兼容问题。把 19 个文件放进同一个目录,在该目录下打开终端:
# 编译单个文件,生成 lianxi01.class javac lianxi01.java # 运行,注意这里写的是类名,不带 .class 后缀 java lianxi01 # 一次性编译全部,-encoding 防止中文输出乱码 javac -encoding UTF-8 *.java逻辑说明:javac只做编译,java负责启动 JVM 并加载指定类。参数说明:-encoding UTF-8在 Windows 默认 GBK 的终端里尤其重要,源码里有大量中文提示语,不指定编码会报「编码 GBK 的不可映射字符」。如果你的终端输出中文是乱码,那是终端代码页的问题,不是程序的问题,把终端切成 UTF-8 就行。
4.2 文件与类名对齐、多类同文件的编译产物
编译时最常见的报错是类 lianxi01 是公共的, 应在名为 lianxi01.java 的文件中声明。Java 要求 public 类的类名和文件名完全一致,包括大小写。lianxi01.java里的类叫lianxi01,看着没问题,但很多人习惯性把它改成Lianxi01,一改就编译不过。
程序 6、14、15 这三个文件里各有第二个类。编译lianxi06.java会一次生成lianxi06.class和deff.class两个文件,运行的时候只写java lianxi06,deff是给lianxi06用的,不需要单独启动。这里也有个容易忽略的点:deff和input这两个类名在 14、15 里都叫input,如果哪天想把这 19 个文件编译成 jar 或者放进同一个工程,两个同名类会打架,得先改名或者加包名。
4.3 用重定向喂输入做逐题验证
19 道题里有 6 道需要键盘输入(4、5、6、7、8、12、14、15 里的一部分),每次跑都手敲一遍很烦,也没法做回归。做法是把输入写进一个文本文件,用管道或者重定向喂给程序:
# 把输入写成一行,直接用 echo 管道喂进去 echo "90" | java lianxi04 # 输出:90=2*3*3*5 # 多个输入用换行分隔,写进文件更清楚 printf "12\n8\n" | java lianxi06 # 输出:最大公约数: 4 / 最小公倍数: 24逻辑说明:Scanner读的是标准输入,管道把echo的输出接到标准输入上,程序感知不到区别。参数说明:\n是Scanner.nextInt()的分隔符,如果两个数字用空格分隔也能读,但用换行更接近真实交互。这套办法在批量验证时特别省事,把每题的输入和期望输出写成一个脚本,改完代码跑一遍就知道有没有改坏。
4.4 几组可以直接对答案的预期输出
为了省去自己算的时间,我把几道题的正确答案列在这里,跑完对一下就知道代码有没有被改错。
| 题目 | 输入 | 预期输出 |
|---|---|---|
| 程序 1 兔子 | M=24 | 第 24 个月 46368 对 |
| 程序 2 素数 | 无 | 101 到 200 之间共 21 个素数 |
| 程序 3 水仙花 | 无 | 153、370、371、407 |
| 程序 4 质因数 | 90 | 90=233*5 |
| 程序 9 完数 | 无 | 6、28、496 |
| 程序 10 弹球 | 无 | 经过 299.609375 米,第 10 次反弹 0.09765625 米 |
| 程序 11 排列 | 无 | 24 个三位数 |
| 程序 17 猴子吃桃 | 无 | 1534 个 |
程序 10 的输出最容易被质疑,因为原代码最后打印的是h / 2,而循环里的h已经反复折半了。这行打印出来的值确实是对的,原因在下一节的第五条里会讲清楚。
4.5 放进 IDE 的目录组织建议
如果打算长期在这套题上做练习,建议单独建一个工程目录,别和其他项目混在一起:
java-lianxi/ ├── src/ │ ├── lianxi01.java │ ├── lianxi02.java │ └── ... └── out/ # 编译产物,IDE 自动生成逻辑说明:src放源码,out放.class,这是 IntelliJ IDEA 新建 Java 项目时的默认结构,把文件拖进src就能直接运行。参数说明:不要给这些文件加package,一加就得连带调整目录层级,而默认包的类互相调用本来是最省事的,练习阶段没必要引入这层复杂度。
5. 避坑与排查:这 19 个类里最容易翻车的五处地方
5.1 编译与运行期的问题
第一条:java lianxi04报「找不到或无法加载主类」。现象是javac明明没报错,运行却提示类加载失败。原因通常是文件名和 public 类名不一致,或者当前目录不在 classpath 上。javac lianxi04.java生成的.class落在源文件所在目录,如果终端还停在上一级目录,java lianxi04自然找不到。解决办法是先cd到.class所在目录再运行,或者用java -cp . lianxi04显式指定 classpath;顺带确认一下文件名大小写,Linux 下Lianxi04.java和lianxi04.java是两个文件。
第二条:连着读两个nextInt()之后,nextLine()读到一个空串。现象是程序 7 输入一行字符统计,结果数字个数、字母个数全是 0,一个字符都没读进去。原因是nextInt()只消费数字本身,不消费行尾的换行符,那个换行符留在缓冲区里,接着调用的nextLine()立刻读到空行就返回了。解决办法有两个:要么统一用nextLine()读整行再Integer.parseInt()转换,要么在nextInt()之后补一次nextLine()把换行符吃掉。我自己习惯第一种,混用nextInt和nextLine迟早还会踩同一个坑。
5.2 逻辑与输出对不上的问题
第三条:程序 7 统计英文字母时,大写 A 永远不被计入。现象是输入ABC得到字母个数 2。原因在判断条件ch > 'A' && ch <= 'Z',左边少了等号,'A'被排除在外;同理ch >= 'a' && ch <= 'z'是对的,只有大写那一支写漏了。这类 bug 很隐蔽,因为你测abc全对,只有输入里刚好带大写 A 才暴露。解决办法是别手写 ASCII 区间,直接Character.isLetter(ch)、Character.isDigit(ch)、Character.isWhitespace(ch),可读性和正确性都更好。另外digital这个变量名指的是 digits,拼错了但不影响运行。
第四条:程序 12 输入 100 万,奖金比输入 60 万还少一截。现象是利润 60 万算出 3.35 万,利润 100 万却算出 5.1 万,跳档的那一段数字明显偏大。原因是合并系数时把区间长度乘错了:20 * 0.175里 20 是前 20 万的额度,0.175 是两个档位费率之和,相乘等于把 10% 和 7.5% 各按 20 万算了一遍,凭空多出 1.75 万。解决办法是回到「每档单独算,额度用区间长度」的写法,也就是上一节那个limit/rate数组累进的版本。改完之后把 10、20、40、60、100、200 这几个边界值都跑一遍,确认每档的衔接处连续。
第五条:程序 10 的反弹高度看着像被除了两次。现象是代码里h = h / 2在循环体内,最后又打印h / 2,直觉上觉得算了两次。原因在于变量的语义在循环里漂移了:初始h = 100表示「第一次落地前的高度」,每轮循环s = s + h累加的是这一趟下落加反弹的总路程,然后h被折半,变成「下一次落地后的反弹高度」。循环跑 9 次之后,h是第 9 次反弹的高度,再除以 2 才是第 10 次反弹高度。解决办法不是改公式,而是把变量改名,h拆成dropHeight和bounceHeight两个,语义写在名字里,就没这种黑匣子感觉了。这种题在面试里报错不可怕,解释不清变量含义才尴尬。
6. 二次开发:把 main 方法改造成可测试的练习脚手架
这套题的原始形态是「改常量、看输出、肉眼对答案」,练手够用,但想验证自己有没有真的掌握,最好把每道题的计算逻辑从main里剥出来,变成可传参、可断言的静态方法。改造的收益很直接:能写断言、能批量跑、能对比不同写法。
以程序 1 为例,先抽方法:
public class Rabbit { // 返回第 month 个月的兔子对数,month 从 1 开始 static long pairs(int month) { if (month <= 0) throw new IllegalArgumentException("month must be >= 1"); if (month <= 2) return 1; long f1 = 1, f2 = 1; for (int i = 3; i <= month; i++) { long cur = f1 + f2; f1 = f2; f2 = cur; } return f2; } public static void main(String[] args) { // 用断言做回归,-ea 开启后不通过会抛 AssertionError assert pairs(1) == 1; assert pairs(2) == 1; assert pairs(3) == 2; assert pairs(24) == 46368; System.out.println("第 24 个月:" + pairs(24)); } }逻辑说明:返回值类型从int换成long,是因为月数一旦放大到 90 以上,int会溢出成负数,这种错误还不会报异常,只会安静地给出错误结果,比报错更难查。参数说明:month做了下界校验,抛IllegalArgumentException而不是返回 0,是为了让调用方立刻发现问题。assert默认不生效,运行时要加-ea参数:java -ea Rabbit。
同样的套路可以套到其余 18 题上:程序 2 抽成countPrimes(int from, int to),程序 4 抽成List<Integer> factorize(int n),程序 12 抽成bonus(double),程序 19 抽成printDiamond(int h)并把星号改成StringBuilder返回。全部抽完之后,19 个类就变成了一个可以互相组合的小工具库,程序 6 的deff类里那个辗转相除可以直接复用到程序 4 的质因数分解里,不用再写第二遍。
验证方法我一般分三步走:第一步跑边界值,month = 1、2、3,profit = 0、10、100,确认临界点不断层;第二步跑一组随机输入,和暴力解法对比结果,比如素数判断用试除法对拍;第三步才跑原题给的样例,看输出格式有没有被改坏。这三步走完,改造才算稳。
改造过程中还有两个小地方值得顺手处理。一是去掉默认包,给所有类加上package lianxi;,目录同步调整成src/lianxi/,这样才可能被其他工程引用;二是把System.out.println从计算逻辑里挪出来,方法只返回值,打印交给调用方,否则测试的时候控制台会被刷屏,真正的断言失败信息反而被淹没。
从那以后我每次重构这种「一堆 main 方法」的练习代码,都会先问自己一句:这段逻辑能不能在不启动程序的情况下被验证?不能就先抽方法,再谈别的。这套 19 题的面试题集本身不难,难的是用它养成把逻辑和输出分离的习惯,这个习惯带到真实项目里,省下的排查时间比刷完这 19 题多得多。希望帮到你。
本文还有配套的精品资源,点击获取