Java自考习题解析:访问修饰符、String与继承的面试陷阱
2026/9/19 18:03:19 网站建设 项目流程

简介:面向Java自考考生与刚接触Java的初学者,这套成套习题文档以题带点梳理Java语言核心基础:访问修饰符public/private/protected/default的可见范围,final修饰类、方法与变量的不同含义,实例变量与类变量及局部变量的区别,接口默认公共属性与静态常量特性,以及继承、多态、消息传递等面向对象概念,并针对内存回收中引用赋值是否产生垃圾对象作了细致辨析。题量上覆盖单选、填空、简答与程序分析题,还包含Applet生命周期、静态方法调用非静态方法、父类子类引用合法性判断等易错题型;简答附有参考答案,程序分析可通过运行代码验证输出,适合自测与考前突击。压缩包内为1个doc文档,整包约500KB,内容紧凑、便于打印。当前已有132人学习浏览,对需要系统过一遍Java考点、避开常见概念混淆的学习者很实用。

1. 从自考习题到 Java 面试八股:这份 doc 为什么值得重做一遍

最近帮人看简历,聊到String s1 = "123"; String s2 = "123"; s1 == s2 输出什么,一位自称有两年经验的候选人犹豫了十几秒。其实这种题目在旧版 Java 自考习题里反复出现,只是很多人把它当成“过时题库”扔掉了。拿到这份java自考习题全套.doc时,我第一反应也是老教材,但翻完第一章就被里面的题目密度惊到了:访问修饰符、final 语义、实例变量和类变量的存续期、接口隐含修饰符,几乎每一道题都能在现在的 java 面试题中找到对应版本。这不是一份背答案就能过关的资料,它要求你把 Java 语言基础里最容易含糊的概念逐条拆清楚。

这份文档适合两类人:一类是准备自考或期末考的在校生,需要把零散知识点串成体系;另一类是准备 java 基础面试的从业者,想用一套带程序分析题的题库做自测。后续章节我会把题目重新组织成可验证的代码和参数表,并把教材里排版错误、容易产生歧义的地方一并指出来。先从前几道选择题讲起,它们决定了你写出来的类在多大范围内可见、在什么时候被回收。

2. 访问权限、final 与变量存续期:先分清这三类变量再做题

2.1 protected 的可见边界:一张表解决选择题

文档第一题问的是“只限子类或者同一包中的类的方法能访问的访问权限”,答案是 C,protected。但题目只考了“子类和同包”,实际编码中真正容易踩坑的是“不同包子类”这个场景。

访问位置private默认(无修饰)protectedpublic
同一个类内可见可见可见可见
同一个包内不可见可见可见可见
不同包子类不可见不可见子类内部可见,但通过父类引用不可见可见
不同包非子类不可见不可见不可见可见

第二行第四列的“子类内部可见”很容易误用。假设父类Base里有一个protected int id,子类Child可以写this.id = 10,但如果写Base b = new Child(); b.id = 10;,编译器会直接报错,因为b的静态类型是Base,当前类不在Base所在包内,也不是Base本身。理解这个区别,比记住“protected 是给子类用的”更能解释日常报错。

package p1; public class Base { protected int id; public void setId(int id) { this.id = id; } }
package p2; import p1.Base; public class Child extends Base { public void test() { this.id = 1; // 合法:子类内部可以直接访问 // Base b = new Base(); // b.id = 1; // 不合法:不同包中无法通过父类引用访问 protected } }

第一段代码中setId是 public 方法,它把受保护字段的修改操作封装在父类内部,这是更稳妥的做法。很多 java 基础题不直接考 protected 的表格,而是把它藏在“哪个访问修饰符可以让子类访问”的判断里,表格一行一行对照就不会错。

2.2 final 修饰类、方法和变量的三种语义

文档第二题说“类定义时用 final 修饰是指这个类不能被继承”,这道题的四个选项比较基础,但 final 经常被混在一起问:final class禁止继承,final method禁止子类覆盖,final variable一旦赋值就不能再改变。

final class FinalClass { public final void print() { System.out.println("final method"); } }

如果一个类已经是 final,它的方法再写 final 没有额外意义。实际开发中更常见的是用 final 修饰方法参数和局部变量,表明该方法内部不该对引用重新赋值。String 类就是一个典型例子,String被声明为 final,所以任何自定义类都不能覆盖它的核心行为,这也是字符串常量池能安全工作的前提之一。

2.3 接口隐含修饰符:变量不是 abstract

文档第四题说“接口中所有的变量都默认为 public abstract 属性”,这个说法是错的。接口里写int MAX = 100;,它的完整语义是public static final int MAX = 100;,也就是说接口变量默认是公有静态常量,不存在 abstract 变量的概念。接口方法才默认是public abstract

public interface Calculator { int MAX_VALUE = 100; // 实际是 public static final int add(int a, int b); // 实际是 public abstract }

这道题的考点是区分“接口和抽象类”的边界:抽象类可以有实例变量和构造方法,接口不能有实例变量,也不能有构造方法。不过要注意,Java 8 之后接口可以有defaultstatic方法,这与题目语境下的“接口体不提供方法的实现”并不冲突,所以正确答案仍然是 D。

2.4 实例变量、类变量和局部变量的存续期

文档里的class A代码题非常有代表性:x是实例变量,y是类变量,s是方法参数,属于局部变量。三者的生命周期完全不同。

变量类型定义位置创建时机销毁或回收时机
实例变量类体内,方法外对象创建时对象不再被引用时
类变量(static)类体内,方法外类加载时JVM 类卸载时
局部变量方法内或代码块内方法调用时方法返回时

程序分析题最后有一道关于obj1obj2输出的题,我建议直接手写验证。m是实例变量,n是静态变量,所以obj1.n *= 4obj2.n += 6都在修改同一个n

public class A { int m = 5; static int n = 3; public static void main(String[] args) { A obj1 = new A(); A obj2 = new A(); obj1.m *= 2; obj1.n *= 4; obj2.m += 1; obj2.n += 6; System.out.println("obj1.m=" + obj1.m); System.out.println("obj1.n=" + obj1.n); System.out.println("obj2.m=" + obj2.m); System.out.println("obj2.n=" + obj2.n); } }

输出结果为obj1.m=10obj1.n=18obj2.m=1obj2.n=18。注意obj2.m是 1,不是 11,因为obj1.m *= 2只影响obj1自己的实例变量。理解了这一点,再看内存回收那道题:u = v;会把u原来指向的对象引用覆盖,如果该对象没有其他引用,它就进入可回收状态,所以答案是 B。

3. 继承构造链与方法绑定:把程序分析题一次做对

3.1 父类构造器先于子类字段赋值

文档里有一段class A extends B的题目,子类构造器第一行调用super(x),这行决定了父类构造器必须先执行。下面这段是整理过的可运行版本,原题里的全角逗号已替换为合法语法。

class B { int b; B(int x) { b = x; System.out.println("b=" + b); } } class A extends B { int a; A(int x, int y) { super(x); a = y; System.out.println("b=" + b + ", a=" + a); } } public class Main { public static void main(String[] args) { A obj = new A(1, 2); } }

输出是两行:第一行b=1,来自父类构造器;第二行b=1, a=2,来自子类构造器。这里有一个容易忽略的顺序问题:子类实例变量的赋值动作会排在super(x)之后,也就是在进入子类构造器主体前,a还是默认值 0。如果面试官问“子类构造器里能不能访问a”,答案是在super()调用之前不能,因为那行只能是构造器链的起始语句。

3.2 private 方法不走动态绑定

文档里的ParentChild类题是经典的绑定陷阱。method1()Parent里是 private,Child里虽然定义了一个同名方法,但因为父类方法是 private,它不会参与覆盖,也就不存在多态绑定。

class Parent { private void method1() { System.out.println("Parent's method1()"); } public void method2() { System.out.println("Parent's method2()"); method1(); } } class Child extends Parent { public void method1() { System.out.println("Child's method1()"); } public static void main(String[] args) { Parent p = new Child(); p.method2(); } }

输出结果是Parent's method2()Parent's method1()。原因是method2()在父类中定义,它内部调用method1()时绑定到父类自己的 private 方法。如果把Parent.method1()改成 public 或 protected,输出才会变成Child's method1()。这个例子说明了方法覆盖的一个先决条件:父类方法必须对子类可见。

3.3 向上转型的合法性判据

文档给了一条继承链:Person->Student->Undergraduate。判断哪行合法只需要一个规则:父类引用 = 子类对象合法,子类引用 = 父类对象不合法。

代码行合法性原因
Person p1 = new Student();合法父类引用指向子类对象
Person p2 = new Undergraduate();合法也是向上转型
Student s1 = new Person();不合法父类对象不保证具有子类行为
Student s2 = new Undergraduate();合法Student 是 Undergraduate 的父类
Undergraduate ug1 = new Person();不合法父类对象不能直接转成孙子类
Undergraduate ug2 = new Student();不合法Student 未必是 Undergraduate
Object ob = new Student();合法所有类都继承自 Object

这题在 java 基础面试里很常见,换了个包装就是“下面哪一行会编译失败”。核心不在背表,而是记牢“编译期看引用类型,运行期看对象类型”。

3.4 引用共享与字段隐藏

Tree 和 PineTree 那道题里有一个明显的排版错误:super, printTree(str)实际应为super.printTree(str)。把代码修正后,sOb.printTree("sOb:")会先调用父类的printTree,打印Name:PineBirth Year:2000Flower:false,然后再打印子类新增的Coniferous:true

class PineTree extends Tree { public boolean coniferous = true; PineTree(String n, boolean f, int y, boolean c) { super(n, f, y); coniferous = c; } public void printTree(String str) { super.printTree(str); System.out.println("Coniferous:" + coniferous); } }

这里真正有意思的是字段隐藏:如果子类和父类定义了同名字段,super只能访问父类版本,直接写字段名则是子类版本。这种写法在工程中应当尽量避免,因为它会让代码阅读者很难判断某个字段到底属于哪一层。

4. 数组初始化、String 比较与数组算法:把易错点变成得分点

4.1 数组声明的两种合法姿势

文档里“错误的创建数组方法”一题,答案是 D:int intArray[5] = {1,2,3,4,5};。Java 不允许在声明数组时把长度写进中括号,长度只在new时或初始化器中给出。

int[] a1 = new int[5]; // 合法 int[] a2 = {1, 2, 3}; // 合法 int[] a3 = new int[]{1, 2, 3}; // 合法 // int[3] a4 = {1, 2, 3}; // 不合法

二维数组的length也有坑。int[][] x = {{1,2},{3,4,5},{6},{}};x.length是 4,不是 7,因为外层数组只有 4 个元素,每个元素是 int[] 类型。x[0].length才是 2,x[3].length是 0。很多遍历二维数组的代码写成for (int i = 0; i < x[i].length; i++),当某一行是空数组时不会出错,但当外层长度大于某行长度时就会越界。

4.2 String 的 == 比较与 substring 边界

String s = "0123456789";的题目里,s1 = s.substring(2)得到"23456789"s2 = s.substring(2,5)得到"234"。注意 substring 的结束索引是开区间,不包含位置 5 的字符'5',所以输出是23456789234

另一道题s.indexOf("a",4)"JavaWorld"中字母a在索引 1 处,从索引 4 开始向后找找不到,输出-1。这提醒了两个常见误区:indexOf返回的是第一次出现的下标,传入起始位置表示跳过前面的字符,找不到返回 -1。

比较方式比较内容典型结果
s1 == s2引用是否指向同一个字符串对象字面量相同时为 true
s1.equals(s2)字符串内容是否相等内容相同时为 true
s1 == new String("123")引用比较false
new String("123").intern() == "123"引用比较常量池对象true

文档判断题里String s1="123"; String s2="123"; String s3=new String("123");的正确答案是truefalse。要记住,字符串常量池中的内容在编译期就能确定,同样的字面量会复用同一个对象,而new则强制创建新对象。这个知识点是 java 面试八股题中的常客,比“String 不可变”更值得展开。

4.3 StringTokenizer 和两道数组程序填空

StringTokenizer 用于按分隔符拆分字符串,常见用法是先countTokens()获取数量,再循环nextToken()取出每个单词。

import java.util.StringTokenizer; public class TokenDemo { public static void main(String[] args) { StringTokenizer pas = new StringTokenizer("A,42,B83,C2,D", ","); int n = pas.countTokens(); System.out.println("单词数:" + n); while (pas.hasMoreTokens()) { System.out.println(pas.nextToken()); } } }

数组填空第一题是统计偶数个数,补全条件为i < a.lengtha[i] % 2 == 0。第二题是把奇数移到偶数之前,需要两个指针,odd记录奇数应该放到的位置,发现奇数就交换。

static int sigmaEvenNum(int[] a) { int s = 0; for (int i = 0; i < a.length; i++) { if (a[i] % 2 == 0) { s++; } } return s; } static void moveOddForword(int[] a) { for (int i = 0, odd = 0; i < a.length; i++) { if (a[i] % 2 == 1) { int t = a[i]; a[i] = a[odd]; a[odd] = t; odd++; } } }

第二段代码中odd始终指向下一个奇数应放的位置,每次交换后自增。需要仔细想的一个边界是:当i == odd时,交换的是同一个位置,不会产生副作用;只有当遇到偶数之后的奇数时,交换才真正发生。

4.4 合并与插入排序的输出验证

merger函数将两个有序数组合并到c中,前两个数分别是3,6,91,2,5。归并过程先比较两个数组头部较小值,最后拼接剩余元素,输出是1 2 3 5 6 9

public class MergeDemo { static int merger(int[] a, int[] b, int[] c) { int i = 0, j = 0, k = 0; while (i < a.length && j < b.length) { if (a[i] < b[j]) { c[k++] = a[i++]; } else { c[k++] = b[j++]; } } while (i < a.length) { c[k++] = a[i++]; } while (j < b.length) { c[k++] = b[j++]; } return k; } public static void main(String[] args) { int[] a = {3, 6, 9}; int[] b = {1, 2, 5}; int[] c = new int[100]; int p = merger(a, b, c); for (int k = 0; k < p; k++) { System.out.print(c[k] + (k < p - 1 ? " " : "\n")); } } }

插入排序s(a, 5)的输出是5 6 7 11 12。它的思路是从第二个元素开始,把当前值t插入到前面已排序部分的正确位置,内部循环j--不断把比t大的元素后移。这和常见的“冒泡排序 java 写法”最大区别是:冒泡通过相邻交换把大值浮到尾部,插入排序则是通过平移给新元素腾位置。

5. 文件 IO 与 Swing 事件:两个高频业务场景的易错源码

5.1 字节流、字符流和随机访问的选型

文档里反复出现FileInputStreamFileReaderBufferedReader的对比。一句话概括:处理图片、音频、压缩包用字节流,处理文本文件用字符流,需要来回跳读用RandomAccessFile。16 位 Unicode 字符流的读写对应FileReaderFileWriter,8 位字节流对应FileInputStreamFileOutputStream

需求推荐类说明
文本按行读取BufferedReaderreadLine()按行返回
文本按行写入BufferedWriternewLine()跨平台换行
二进制文件读取FileInputStream原始字节
随机读写 long 型数据RandomAccessFile支持readLong()writeChars()
打开文件对话框JFileChooser只能得到文件路径和对象,不能直接得到内容

程序填空题里提到FileFilter预设的两个方法,一个是accept(File f),用于判断文件是否满足筛选条件;另一个是getDescription(),用于返回筛选器的描述文本,比如“文本文件 (*.txt)”。用JFileChooser时,用户不可能通过文件对话框直接获得文件内容,能获得的是文件名、路径和文件对象,这也是选择题的答案。

5.2 FileOutputStream 写完必须要 close

原题在FileOutputStream构造后直接write(buffer, 0, n),程序填空题需要补上关闭流的逻辑。

public void actionPerformed(ActionEvent e) { try { int n = txtFld.getText().length(); byte buffer[] = new byte[n]; buffer = txtFld.getText().getBytes(); FileOutputStream wf = new FileOutputStream("myText.txt"); wf.write(buffer, 0, n); wf.close(); } catch (IOException ioe) { txtFld.setText(ioe.toString()); } }

这段代码里的getBytes()使用平台默认字符集,不同环境下可能产生不一样的结果。工程上更推荐显式指定字符集,例如getBytes(StandardCharsets.UTF_8)write(buffer, 0, n)的第三个参数是写入长度,这里n是字节数组长度,不是字符串字符数。如果文本包含中文,一个字符可能占多个字节,txtFld.getText().length()buffer.length不一定相等,更稳妥的做法是直接写wf.write(buffer)

5.3 布局管理器与滚动面板

FlowLayout从左到右排列组件,这是文档里的基础题。JFrame 的内容面板默认布局是 BorderLayout,JDialog 默认也是 BorderLayout。如果希望多个组件共享同一个显示空间、某一时刻只显示一个,用的是CardLayout。需要滚动条时,把JTextArea放进JScrollPane,再把滚动面板加入窗口。

JTextArea text = new JTextArea(8, 10); JScrollPane jsp = new JScrollPane(text); JFrame myFrame = new JFrame("示例"); myFrame.getContentPane().add(jsp);

JTextArea(8, 10)表示 8 行 10 列,但它本身没有滚动条,必须借助JScrollPane。很多初学者把滚动条直接放到 JFrame 上,结果文本一多就显示不全。

5.4 ActionEvent 的两种取事件源方式

文档的程序填空题里,按钮事件处理方法需要判断事件源。getSource()返回产生事件的对象,getActionCommand()返回按钮的文本或组件设置的动作命令。

public void actionPerformed(ActionEvent e) { if (e.getSource() == button) { String s = text1.getText(); StringTokenizer fenxi = new StringTokenizer(s); int n = fenxi.countTokens(); double a[] = new double[n]; for (int i = 0; i < n; i++) { String temp = fenxi.nextToken(); a[i] = Double.valueOf(temp).doubleValue(); } int sum = 0; for (int i = 0; i < n; i++) { sum += a[i]; } text2.setText(" " + sum); } }

这里有一个隐蔽问题:Double.valueOf(temp).doubleValue()返回 double,但sum声明为 int,如果输入是小数,小数部分会被丢弃。更合理的做法是把sum也声明为 double。按钮计数程序则用setText(String.valueOf(++i))更新标题,注意++i先自增再转字符串,这样第一次点击后显示的是 1 而不是 0。

6. 把程序分析题改成可运行断言:自查答案的最后一公里

6.1 用一个小 main 批量验证输出

文档后半部分有大量“阅读程序,写出输出结果”的题。我的建议是不要对着答案口算,而是把每道题的类复制到独立文件中,写一个统一的验证入口,跑一遍再对照手推结果。这样既能验证你理解的绑定规则,也能发现教材排版里隐藏的语法错误。

public class OutputChecker { static void checkClassA() { A obj1 = new A(); A obj2 = new A(); obj1.m *= 2; obj1.n *= 4; obj2.m += 1; obj2.n += 6; System.out.println("obj1.m=" + obj1.m); System.out.println("obj1.n=" + obj1.n); System.out.println("obj2.m=" + obj2.m); System.out.println("obj2.n=" + obj2.n); } public static void main(String[] args) { checkClassA(); } }

运行前先确认javacjava命令可用,这就是 java 环境变量配置做完之后最基本的验证方式。写断言比直接打印更进一步,比如用if (!"obj1.m=10".equals(output)) throw new AssertionError();,这样错误可以自动暴露,不用肉眼逐行比对。

6.2 记录实际输出与预期不符的位置

我一般会准备一个文本文件,左边写手推输出,右边贴实际运行结果。第一次跑 Tree 和 PineTree 的题目时,我把super, printTree(str)原样复制进去,编译直接失败,这才意识到是逗号被 OCR 成了错误字符。这类问题在处理旧版习题资料时非常常见,字符串中的全角逗号、漏写分号、类名大小写不一致都会导致复现失败。

最后留一个实用技巧:把程序分析题的输出结果做成独立的 JUnit 测试类,每个测试方法对应一道题。这样当你修改了自己写的代码后,随时可以跑一遍测试,确认旧答案没有被破坏。断言通过不是终点,把错误输出贴回题目旁边,才是这份习题真正被吃透的时候。

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

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

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

立即咨询