简介:金航数码Java初级技术笔试题,是一套面向Java初学者及初级开发岗位求职者的自测与复习题集。试卷围绕循环控制、字符串运算、变量命名规则、抽象方法、继承关系、数组与对象转换、异常处理等高频考点设计,题型贴近真实企业笔试风格,既可用于考前突击,也可用于检验基础知识掌握程度。
资源包共包含1个文件,采用doc文档格式,压缩后大小约98KB,内容为可直接打印或编辑的完整试卷,且每道选择题均标出正确答案,便于读者随时自测与核对。题目覆盖范围典型,比如do-while循环的边界判断、整数与字符串拼接结果、父类子类构造器调用顺序、try-catch-finally执行时机、数组与Object的强制转换等,能帮助初学者系统梳理Java语法与面向对象的核心易错点,并提升实际解题能力。
目前已有395人学习下载。对准备Java技术面试或刚入门的开发者而言,这是一份轻量但实用的刷题资料,能快速定位薄弱环节,建议结合官方文档或入门教材边练边补。
1. 金航数码 Java 笔试题的考点构成:初级难度、边界陷阱与编译器视角
金航数码这套 Java 笔试题,第一题就把很多人问懵了:do { i = i/2; } while( i-- > 1 )最后输出的是 -1,不是 1、5、2 里的任何一个。20 道单选题看着是初级水平,实际全在考三件事:编译能否通过、边界条件怎么走、对象初始化顺序是什么。你光会写 CRUD、背几个概念,上机照样卡壳。拿分的关键不是题海战术,而是先练成"编译器视角"——看到代码先判断能不能编译,再算执行结果。这套题适合正在准备 Java 初级岗笔试、校招机考,或者想检验自己基础是否扎实的人;把每道题的考点拆开看,你会发现它的出题逻辑很固定,完全可以在考前针对性地练。
2. 高频考点:流程控制、字符串连接与标识符规则
这一章集中解决选择题里最靠前的几个基础语法题。别看它们分值小,恰恰是笔试中失误率最高的区域。很多考生不是不会,而是被"看起来很简单"骗了,忽略边界、忘记作用域、搞错结合顺序。下面按题目逐道拆。
2.1 do-while 与 i-- 的组合边界:第一题为什么输出 -1
先看原题代码:
int i = 10; do { i = i / 2; } while ( i-- > 1 ); System.out.println(i);很多人凭直觉选了 C(2)或 A(1),因为看到i = i/2就以为循环到 1 就停了。关键在i--这个后置自减:先拿 i 的当前值去比较,比较完再减 1。推演一遍:i=10 进循环,i 变成 5,判断 5 > 1 成立,i 变成 4;下一轮 i 变成 2,判断 2 > 1 成立,i 变成 1;再下一轮 i 变成 0,判断 0 > 1 不成立,i 变成 -1,循环结束。所以输出 -1,选 D。
这类题面试官真正想看的不是你会不会除 2,而是你知不知道"先判断再自减"和"先自减再判断"的区别。如果改成while (--i > 1),结果会完全不同。答题时养成习惯:把每一轮的 i 值写在草稿纸上,不要心算到底。手写推演比脑子里"凭感觉"可靠得多。
2.2 字符串 + 运算:为什么 "Hello" + 9 + 1 是 Hello91
题目第二题几乎是一道送分题,但每年都有人错选 A(Hello10)。代码是String s = "Hello" + 9 + 1;,输出 Hello91。原因是 Java 的加法运算符从左到右结合,字符串遇到第一个加号时会把 9 转成字符串"9",整个表达式变成字符串拼接,后面的 1 也一样。如果你想先算 9+1 再拼接,必须写成"Hello" + (9 + 1),结果才是 Hello10。
这个考点的变形很多:"Hello" + 1 + 2 + 3是 "Hello123",而1 + 2 + "Hello"是 "3Hello",因为前面两个 int 先做了加法。笔试里遇到字符串拼接 + 数字的题,先扫一眼运算符顺序和左边的数据类型,不要默认先算数值。面试官就爱在追问环节加一道System.out.println(1 + 2 + "3")考你,答案是 "33"。
2.3 标识符规则:$、_ 合法,#、~ 非法
第三题给的四个变量声明是:
String #name = "Jane Doe"; int $age = 24; Double _height = 123.5; double ~temp = 37.5;Java 标识符的规则是:以字母、下划线_、美元符$或 Unicode 字符开头,后面可以跟字母、数字、下划线和美元符,但不能以数字开头,也不能包含#、~这类特殊符号。$age合法,_height合法,#name和~temp各是一处编译错误,所以答案是 2 处,选 C。
这里有个隐藏考点:Double _height = 123.5;用的是包装类型 Double,不是原始类型 double,自动装箱是合法的,所以这一行没有问题。很多考生把合法的$age当成错误、把合法的_height当成错误,反而漏掉真正的非法项。记规则的时候只需要记一条反面清单:不能有#、~、空格、-、.等符号,不能以数字开头,不能是关键字,剩下的基本都合法。
2.4 for 循环与变量作用域:break 之后为什么访问不到 i
第六题是个隐藏题,因为题干里没写答案字母:
for (int i=0; i<= 10; i++) { if( i>6) break; } System.out.println(i);这里不能直接去算"i 从 0 加到 7,break 之后输出 7"——因为 i 声明在 for 循环的初始化部分,作用域只限于 for 语句块内部。循环结束后,System.out.println(i)里的 i 根本不存在,所以是编译失败,选 D。这道题表面考 break,实际考变量作用域。
如果题目把 i 声明在循环外面,答案才是 7:i 自增到 7 时满足i > 6,break 跳出循环,此时 i 保留为 7。所以看到这类题先看变量声明的位置,再看 break 条件的触发时机。这道题也是很多考生在选择题里丢掉的第一分,因为人脑默认把 i 当成"后面还能用"。
2.5 数组与 Object 转换、方法传值:两个容易被误判的选择题
第七题的正确思路需要理解数组也是对象。代码三行分别是:Object obj = new int[]{1,2,3};、int[] someArray = (int[])obj;、增强 for 遍历输出。int[] 是 Object 的子类型,向上转型给 Object 变量再强制转回 int[],这是合法的,没有编译错误,最终输出 "1 2 3",选 A。这道题很多人纠结 line 3 的增强 for 会不会报错,其实for (int i : someArray)语法完全正确,只要 someArray 是 int[] 就能迭代。
第九题考的是 Java 方法参数传递:private static int a;初始为 0,main 里调用modify(a),方法内部a++改变的是形参 a 的拷贝,不是外面静态变量 a 的值,所以 main 打印的仍然是 0,选 A。这类题的加强版是把参数改成数组或对象,那时引用传递就会影响原对象内容。笔试遇到"方法里改了参数,外面影响吗"的题,先分清是基本类型还是引用类型。
提示:选择题里只要出现"这段代码有几处编译错误""程序运行结果"这类字眼,优先检查变量作用域和类型转换,再开始模拟执行。
3. 高频考点:继承、初始化顺序、抽象类与设计原则
这一章的题占了选择题的大头,也是面试官在追问环节最愿意深挖的部分。你不仅要能选出答案,还要能说清楚"为什么是这个顺序"。下面按题目把继承和初始化的逻辑一次讲透。
3.1 抽象方法没有大括号,声明以分号结束
第四题的正确答案是 D:声明抽象方法不可写出大括号。抽象方法只有方法签名,没有方法体,结尾必须是分号。A 错在 abstract 不能修饰字段,只能修饰类和实例方法;B、C 描述的方法体大括号完全不存在,所以谈不上"必须用"或"可有可无"。这个知识点在 Java 初级笔试里几乎必考,因为它最容易和接口默认方法混淆。接口的默认方法有方法体,抽象类里的抽象方法没有方法体,这两者一旦记混,续写题目必错。
回答这类题的最好方式是直接背一句标准表述:抽象方法声明以分号结束,子类若非抽象类则必须实现它。如果面试官追问"抽象类能不能没有抽象方法",答"能,但没什么意义;抽象类不一定有抽象方法,但有抽象方法的类一定是抽象类"。
3.2 单继承:一个子类一个父类,一个父类多个子类
第五题考的是继承的基本规则:Java 是单继承,一个子类只能有一个直接父类,但是一个父类可以有多个子类,选 C。A 说"一个子类可以有多个父类"直接违反单继承;B 说"一个父类只可以有一个子类"也不对。这里有个容易踩的坑:子类只能有一个直接父类,但通过多级继承,它可以拥有多个间接父类,所以严格说"一个子类可以有多个父类"在某些表述下是有歧义的。笔试遇到这种题,优先看"直接父类"这个词,只有 C 的表述完全严谨。
面试追问通常是这样:为什么 Java 不支持多继承?答题要点是"菱形问题"——如果两个父类有同名方法,子类不知道该继承哪一个。C++ 用虚继承解决,Java 直接砍掉多继承,改用接口多实现。
3.3 构造器调用链与初始化顺序:BaseBase 与 Super/Sub 的输出
第八题很简单,但也最容易因为忽略继承而答错:
class Base { Base() { System.out.print("Base"); } } public class Alpha extends Base { public static void main( String[] args ) { new Alpha(); new Base(); } }new Alpha()创建子类对象时,子类构造器第一行会隐式调用父类无参构造器,所以先打印 Base,再执行 Alpha 自己的构造器(这里没有显式内容);紧接着new Base()又打印一次 Base。最终输出 BaseBase,选 B。这里的关键是:子类构造器一定先执行父类构造器,和你写了super()与否无关。
第十二题是本章最值钱的一道,因为它把静态初始化、实例初始化、构造器调用链串在一起:
class Super { Super(int i){ System.out.print("Super" + i + ","); } } public class Sub extends Super { static Super s1 = new Super(1); Super s2 = new Super(3); Sub(int i){ super(i); System.out.print("Sub" + i + ","); } public static void main(String[] args) { Super c1 = new Sub(2); } }推演顺序分三步:第一步,类加载时执行静态初始化,static Super s1 = new Super(1)输出 Super1;第二步,new Sub(2)创建对象,先初始化实例变量s2 = new Super(3),输出 Super3;第三步,进入 Sub 构造器,super(2)调用父类构造器,输出 Super2,再输出 Sub2。整体答案就是 Super1,Super3,Super2,Sub2,选 C。
这里有个通用的记忆顺序:类加载静态初始化 → 对象创建时先父类成员初始化再父类构造器 → 再子类成员初始化再子类构造器。简化口诀是"静态先行、父类先造、成员先于构造"。如果面试官继续追问,通常会把 static 块、实例块、构造器三者的执行顺序混在一起考,记住"静态块只在类加载时执行一次,实例块每次 new 都执行,且实例块在构造器之前"。
3.4 抽象类子类必须实现抽象方法:第十三题的编译错误
第十三题表面在考数组默认值,实际考的是抽象方法的实现义务:
abstract class MineBase { abstract void amethod(); static int i = 1; } public class Sub extends MineBase { public static void main(String[] args) { int[] ar = new int[5]; for(i=0; i<ar.length; i++) { System.out.print(ar[i]); } } }amethod()是个抽象方法,Sub 类继承 MineBase 却没有实现它,同时 Sub 自己也不是抽象类,所以整个 Sub 类编译失败,选 C。很多考生看到 static int i=1 就开始算循环,以为输出五个 0,完全忽略编译错误。这个考点的出题手法很固定:放一段能正常执行的循环代码,但给类留一个未实现的抽象方法,看你会不会先检查编译。答题顺序永远是先看能否编译,再看输出结果。
如果 Sub 补上了void amethod(){},输出确实会是五个 0,因为 int[] 数组默认全是 0。面试追问时可以说:把 i 声明为 static 意味着所有实例共享同一个计数器,循环结束后 i 的值会影响后续使用,这也是设计上的坑。
3.5 Open-Close 原则与设计模式两大主题
第十四题考软件设计的 Open-Close 原则,选 A:对扩展开放,对修改关闭。这个原则是面向对象设计里最常考的一条,它的意思是:新增功能时应该通过扩展(新增类、新增方法)来实现,而不是去改已经写好的、稳定运行的代码。B、C 把"开放""关闭"的指向换了,D 更是无中生有。
第十五题考设计模式的两大主题,选 B:对象组合与类的继承。这两条是实现代码复用的两条根本路径:继承是白盒复用,组合是黑盒复用。设计模式里大量模式都是在二者之间做取舍,比如策略模式优先组合,模板方法模式用继承。面试官问"你用过哪些设计模式,为什么用组合而不是继承",回答时就可以把这两条作为理论根基:组合更灵活,可以在运行时替换行为,而继承关系在编译期就固定了。
3.6 继承是白盒复用而不是黑盒:第十六题的错误选项
第十六题问"关于继承表述错误的是",选 D。A 正确,继承是扩展已有对象实现来获得新功能;B 正确,超类捕获公共属性和方法,子类附加新内容;C 正确,继承会破坏封装性,因为父类实现细节暴露给子类;D 错误,它说"继承本质上是白盒复用,对父类的修改不会影响到子类"。白盒复用恰恰意味着父类改动会传导到子类,所以 D 前后矛盾。
这类题在笔试里属于"概念判断型",四个选项里通常有两个是原文互搏。答题技巧是先圈出"错误""正确"这类词,再把每个选项的主语和谓语拆分。D 的错误不在于"白盒复用"这个词,而在于"不会影响到子类"这一句。黑盒复用才是对外部透明、互不影响。
3.7 构造函数与 static 的冷门考法
第十九题考构造函数描述错误的是,选 A:构造函数的返回类型只能是 void 型。Java 中构造函数根本没有返回类型,写 void 反而不叫构造函数了,而是普通方法。B 说方法名与类名相同,C 说完成对象初始化,D 说创建对象时自动调用,全对。这类题是从 C++ 迁移过来的经典陷阱:C++ 构造函数也没有返回类型,更不是 void,而是无返回类型声明。
第二十题考 static 修饰的方法可以直接通过类名调用,选 A。这是 static 最核心的特征:它属于类而不属于实例。面试追问通常会结合前面的题目问"static 方法能不能访问实例变量",答案是能吗?不能——因为 static 方法不依赖实例存在,而实例变量依赖实例。笔试如果出了变体题,比如"static 方法里能不能用 this",答案也是不能。
4. 高频考点:异常处理与多线程的选择压轴
选择题最后几道集中在异常处理和多线程上。这部分知识点不多,但坑很密:异常路径的顺序、catch 的匹配规则、线程的启动方式。下面把这三类题型一次讲透。
4.1 try-catch-finally 的执行路径:除零异常后的输出
第十题的代码结构是 try 里调用method1(),method1()里做了10/j,当 j=0 时抛出 ArithmeticException:
try { method1(); System.out.print("A"); } catch (Exception ex) { System.out.print("B"); } finally { System.out.print("C"); } System.out.print("D");执行路径是:try 里抛异常,A 不打印;catch 捕获,打印 B;finally 始终执行,打印 C;异常已被处理,catch 后续代码继续,打印 D。最终输出 BCD,选 D。这道题有两层陷阱:第一层,A 会被跳过,因为异常发生点之后的 try 语句都不执行;第二层,D 会打印,因为异常被捕获后程序不会终止。
笔试改卷时最容易出现 BC 这个错误答案,原因是考生忘了异常被 catch 之后程序还会继续执行。记一条原则:try-catch 就像海绵,异常被 catch 接住后,后面的代码照常跑;只有异常没被捕获,方法才会提前退出。面试追问的变形题是:把 catch 去掉,只留 try-finally,输出什么?答案是没有捕获时 B 不打印,finally 打印 C,然后异常继续向上抛,D 不打印,程序在 D 之前崩溃。
4.2 异常匹配顺序:子类异常会被父类 catch 捕获
第十一题定义了Exc0 extends Exception、Exc1 extends Exc0,main 里throw new Exc1():
try { throw new Exc1(); } catch (Exc0 e0) { System.out.println("Ex0 caught"); } catch (Exception e) { System.out.println("exception caught"); }Java 的异常匹配机制是:抛出异常后,从第一个 catch 开始按顺序检查,看这个异常对象是否能赋值给 catch 的参数类型。Exc1 是 Exc0 的子类,能赋值给 Exc0 变量,所以第一个 catch 命中,输出 Ex0 caught,选 A。很多人以为会匹配 Exception,忘了子类异常一定能被父类引用捕获。
这里有个延伸考点是 catch 的书写顺序:如果把catch (Exception e)放在前面,catch (Exc0 e0)放后面,编译会直接失败,因为编译器发现 Exc0 的异常已经被前面的 Exception 捕获,后面的 Exc0 分支永远不可达。答题时看到多个 catch,先看异常类之间的继承关系,再按"谁在上、谁在下"判断。面试官最常追问的也是这个:为什么 catch 必须子类在前、父类在后?
4.3 run() 与 start()、synchronized 的作用范围
第十七题问"哪个方法用于定义线程的执行体",选 C:run()。这里有个高频混淆点:start()是启动线程、让它进入就绪状态,真正要执行的逻辑写在run()里。如果你直接调用run(),它只是在当前线程里当普通方法执行,并没有新开线程。笔试喜欢考这个,面试追问更爱问"直接调 run() 和调 start() 有什么区别",标准答案是:start() 会创建新线程并自动调用 run(),直接调 run() 就是普通方法调用,不并发。
第十八题问"哪个关键字可以用来为对象加互斥锁",选 D:synchronized。A 的 transient 是序列化时跳过字段,B 的 static 是类级别修饰,C 的 serialize 在 Java 标准写法里是 implements Serializable,根本不是关键字。synchronized 可以修饰方法,也可以修饰代码块,它保证同一时刻只有一个线程能进入临界区。面试追问通常会问 synchronized 锁的是对象还是类:静态方法锁的是 Class 对象,实例方法锁的是当前实例 this,代码块锁的是括号里指定的对象。
5. 易错点与失分点:上机编程与 SQL 答题陷阱
上机题才是这张卷子拉开差距的地方。前面选择题还能靠排除法,上机题一旦思路错了,一分都拿不到。这里把四道上机题和选择题里最容易误判的两道放在一起,按"错在哪、为什么错、怎么答对"的套路拆开讲。
5.1 字符串数字排序:是数字排序,不是字符串排序
上机第 21 题要求对"100,10,1,0,15,20"升序排列,并给出方法签名public static String sort(String args)。最常见的错法是把逗号分隔后的字符串直接丢进Arrays.sort(String[]),得到的结果是"0,1,10,100,15,20"——因为字符串排序按字典序,10 会排在 15 之前,100 会排在 15 之前,完全不符合数字大小。
import java.util.Arrays; public class Test1 { public static String sort(String args) { String[] parts = args.split(","); int[] nums = new int[parts.length]; for (int i = 0; i < parts.length; i++) { nums[i] = Integer.parseInt(parts[i].trim()); } Arrays.sort(nums); StringBuilder sb = new StringBuilder(); for (int num : nums) { sb.append(num).append(","); } return sb.toString(); } public static void main(String[] args) { String s = "100,10,1,0,15,20"; System.out.print(sort(s)); // 输出 0,1,10,15,20,100, } }这里有三处细节值得注意。第一,split(",")按逗号切分,但如果输入里有空格,需要先trim()再转换,否则Integer.parseInt会抛 NumberFormatException。第二,转换成 int[] 数组后排序,才能得到真正的数字升序。第三,题面的注释里输出末尾带一个逗号,说明它期望每拼一个数字后面加逗号,不需要特地去掉末尾逗号。如果题目要求不带末尾逗号,就用"不是最后一个元素才加逗号"的判断。上机阅卷时最容易因为格式不符扣分,宁可多写一个逗号,也不要输出成"0,1,10,15,20,100"这样和预期不一致。
面试官如果追问,通常会让写"用 Comparator 对字符串数组按数字大小排序"的解法,目的是避免 parseInt 后丢失原有字符串格式。那就在排序时用Arrays.sort(parts, (a, b) -> Integer.parseInt(a) - Integer.parseInt(b)),注意减法可能溢出,稳妥写法是用Integer.compare或Comparator.comparingInt(Integer::parseInt)。
5.2 斐波那契递归:索引从 1 开始与终止条件
上机第 22 题要求用递归求第 30 位数是多少,数列是 1、1、2、3、5、8、13、21、34…… 这里最经典的错法是终止条件写错。常见错误写法是index <= 1 ? 0 : ...,把第 1 位数当成 0,结果整体偏移一位;还有人把第 0 位数当成 1,导致输出比正确答案多一位。
public class TestNum { static int ComputNum(int index) { if (index <= 2) { return 1; } return ComputNum(index - 1) + ComputNum(index - 2); } public static void main(String[] args) { System.out.println(ComputNum(30)); // 832040 } }终止条件index <= 2返回 1,是因为前两位都是 1;从第 3 位开始,每一位等于前两位之和。第 30 位的结果是 832040。三个容易丢分的地方:一是终止条件写成index <= 1会导致第 2 位数计算错误;二是把数组下标从 0 开始记,问第 30 位时却数到下标 30;三是递归实现虽然能跑,但重复计算量巨大,如果面试官追问性能,可以补充用数组缓存或迭代实现。
注意:笔试卷子只要没有限制"必须用递归",斐波那契都可以用迭代写,时间复杂度 O(n)。但如果题目明确写了"用递归算法实现",就按递归写,不要自作主张改成迭代,否则可能被判定为没按要求作答。
5.3 SQL 三连问:子查询、GROUP BY HAVING 与 JOIN
SQL 题是上机题的压轴,三问覆盖了子查询、分组聚合、多表连接三大必考点。这里的易错点非常多,逐个拆开。
第一问:列出薪金比张平多的所有员工姓名。标准写法是:
SELECT Ename FROM EMP WHERE Sal > (SELECT Sal FROM EMP WHERE Ename = '张平');子查询SELECT Sal FROM EMP WHERE Ename = '张平'返回单行单列值 2800,外层查询筛选所有 Sal 大于 2800 的员工,结果是李淑华、马生。最典型的错法是直接把 2800 写死在条件里,或把单引号写成中文引号。如果员工表里有同名的人,内层子查询会返回多行,外层>就会报错;这时候要改成Sal > ANY (...),或者先按主键取唯一记录。张平是部门经理 2800,孙小平 2100、王一平 1850 都比 2800 少,所以不会出现在结果里。
第二问:列出最低薪金大于 2000 的各种职位。注意题目说的是"各种职位",也就是按 job 分组后,看这个组的最低薪金是否大于 2000:
SELECT job FROM EMP GROUP BY job HAVING MIN(sal) > 2000;这句的意思是:对 job 分组,筛出每组最低薪金大于 2000 的组,输出这些组的 job。数据里"部门经理"组最低 2800,"公司领导"组最低 3600,都满足;"业务员"组最低 1850,不满足。所以结果是"部门经理、公司领导"。最多人踩的坑是写成SELECT job FROM EMP WHERE sal > 2000,这筛选的是"每个员工薪金大于 2000",和"某种职位最低薪金"完全是两回事。回去看原卷的回答,第二问确实给的是where sal > 2000,这是错误的——没有按 job 分组,也没有用 MIN。这类 SQL 笔试的最高频失分点就在这:看不清"各种职位"这个分组词。
第三问:列出每个部门的部门名称、员工数量和平均薪水。这里有两版写法,差别在要不要保留没有员工的部门:
SELECT d.dname AS 部门名称, COUNT(e.empno) AS 员工数量, AVG(e.sal) AS 平均薪水 FROM DEPT d LEFT JOIN EMP e ON e.deptno = d.deptno GROUP BY d.deptno, d.dname ORDER BY d.deptno;题目说"每个部门",而 DEPT 表里一共有四个部门,其中市场部没有员工。如果使用 INNER JOIN,市场部会被直接丢弃,这就不叫"每个部门"了。只有 LEFT JOIN 能从 dept 表保留所有部门,市场部那一行员工数量是 0,平均薪水是 NULL。同时注意,AVG的括号里写的是e.sal员工薪金,而不是d.deptno;GROUP BY后面必须带上 SELECT 里出现的非聚合列 dname,否则在 Oracle 里直接报 ORA-00979,在 MySQL 里虽然能跑,但逻辑上 dname 没有对应到正确的 deptno。
选择题第 6 题和第 7 题,放在这里一起说。第 6 题最容易误判成"输出 7":错在哪——直接心算 break 的触发时机,忘了 i 声明在 for 循环内;为什么错——变量作用域只覆盖 for 语句,循环外访问不到;怎么答对——先看变量声明位置,再决定要不要算。第 7 题最容易误判成"编译失败":错在哪——以为int[]转Object再转回int[]不合法;为什么错——数组是对象,Object 是所有引用类型的父类,向下转型合法且运行时类型一致,不会抛 ClassCastException;怎么答对——确认转型方向后直接模拟增强 for 输出,答案是 1 2 3。
5.4 单例模式:缺括号、线程安全与面试追问
上机第 24 题要求写一个单例类,题面给的参考代码有一个隐藏问题:public static Singleton getInstance{少了括号,写成这样根本编译不过。正确的基本写法是:
class Singleton { private static Singleton s; private Singleton() {} public static Singleton getInstance() { if (s == null) { s = new Singleton(); } return s; } }这是懒汉式单例:第一次调用 getInstance 时才创建对象。它最大的问题是线程不安全——两个线程同时进入 if 判断时,可能各创建一个实例。笔试能写对这个已经及格,但面试官几乎必问"你这个写法线程安全吗"。加分回答是给出双重检查锁版本:
class Singleton { private static volatile Singleton s; private Singleton() {} public static Singleton getInstance() { if (s == null) { synchronized (Singleton.class) { if (s == null) { s = new Singleton(); } } } return s; } }这里 volatile 不是随便加的。new Singleton()在底层分三步:分配内存、初始化对象、把引用赋给变量。如果不加 volatile,第三步可能先于第二步执行,另一个线程会拿到一个"半初始化"的对象。面试官问"为什么要 double-check",标准答案是:第一次判空避免无谓加锁,第二次判空防止多线程重复创建。如果继续追问"还能怎么写",可以答静态内部类式:利用 JVM 的类加载机制保证线程安全;或者枚举式:Joshua Bloch 推荐的《Effective Java》写法,天然防反射、防序列化。
注意:考场里先写最基本的懒汉式,确保编译通过、逻辑正确;写完还有时间,再补线程安全注释或升级写法。不要一上来就写双重检查锁,万一漏了 volatile,反而暴露出对指令重排不了解。
6. 答题技巧与押题思路:三遍答题法与两道必背模板
这套卷子 20 道选择加 4 道上机,时间压力并不均匀。上机题里的 SQL 和单例要写完整输出,选择题里有些是概念秒杀、有些需要演算。我的建议是压缩做题顺序,不做"从第一题顺到最后一题"的匀速运动,而是按题型分三遍过:第一遍,把所有选择题快速扫一遍,碰到要推演的先跳过;第二遍,集中算流程控制、初始化顺序、异常路径这三类计算型题目,每一题都在草稿纸上写变量值和输出顺序;第三遍,专门检查"编译失败"选项——把代码里的抽象方法、变量作用域、catch 顺序、类型转换逐一核对,发现任何一个隐患就优先选编译错误类答案。
押题方向上,这套卷子反复出现四个套路。第一个是"看起来会执行、实际编译失败",抽象方法未实现加 for 循环、for 内部变量被外部引用,这类题每年都有;第二个是"字符串连接从左到右、数字拼接不先算",遇到"字符串" + 数字 + 数字一律按字符串处理;第三个是"SQL 分组统计必考 GROUP BY 和 HAVING",只要题目出现"各种""每个""最低薪金"里带"各种"或"每个"的词,就是分组题,原卷第二问的 where 写法就是要你纠正的错误示范;第四个是"单例必追问线程安全",不要再交只有 getInstance 没有 synchronized 的裸写法,就算不上锁,也要知道为什么需要锁。
两道必背模板值得考前默写。第一道是递归求斐波那契:"if (index <= 2) return 1; return ComputNum(index - 1) + ComputNum(index - 2);",注意第 30 位是 832040,不是 832039,很多人栽在这个数上。第二道是单例双重检查锁:私有构造、volatile 静态字段、getInstance 里先判空再同步再判空,这三行不能少。SQL 三连问的核心句是"子查询返回单值用 WHERE Sal >、分组统计用 HAVING MIN、显示所有部门用 LEFT JOIN",把这三句话记熟,遇到同类型的表和题目可以套一半分。从那以后我每次拿到 Java 笔试卷,第一件事不再是读题干,而是把所有选项里"编译失败""运行时报错"的字样先圈出来,再决定这道题要不要细致推演——因为这套卷子最狡猾的地方就是把你往执行结果的坑里带,真正的答案却藏在编译期。这个习惯帮我稳住了大部分选择题,希望帮到你。
本文还有配套的精品资源,点击获取