简介:面向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 | 默认(无修饰) | protected | public |
|---|---|---|---|---|
| 同一个类内 | 可见 | 可见 | 可见 | 可见 |
| 同一个包内 | 不可见 | 可见 | 可见 | 可见 |
| 不同包子类 | 不可见 | 不可见 | 子类内部可见,但通过父类引用不可见 | 可见 |
| 不同包非子类 | 不可见 | 不可见 | 不可见 | 可见 |
第二行第四列的“子类内部可见”很容易误用。假设父类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 之后接口可以有default和static方法,这与题目语境下的“接口体不提供方法的实现”并不冲突,所以正确答案仍然是 D。
2.4 实例变量、类变量和局部变量的存续期
文档里的class A代码题非常有代表性:x是实例变量,y是类变量,s是方法参数,属于局部变量。三者的生命周期完全不同。
| 变量类型 | 定义位置 | 创建时机 | 销毁或回收时机 |
|---|---|---|---|
| 实例变量 | 类体内,方法外 | 对象创建时 | 对象不再被引用时 |
| 类变量(static) | 类体内,方法外 | 类加载时 | JVM 类卸载时 |
| 局部变量 | 方法内或代码块内 | 方法调用时 | 方法返回时 |
程序分析题最后有一道关于obj1和obj2输出的题,我建议直接手写验证。m是实例变量,n是静态变量,所以obj1.n *= 4和obj2.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=10、obj1.n=18、obj2.m=1、obj2.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 方法不走动态绑定
文档里的Parent和Child类题是经典的绑定陷阱。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:Pine、Birth Year:2000、Flower: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");的正确答案是true和false。要记住,字符串常量池中的内容在编译期就能确定,同样的字面量会复用同一个对象,而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.length和a[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,9和1,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 字节流、字符流和随机访问的选型
文档里反复出现FileInputStream、FileReader、BufferedReader的对比。一句话概括:处理图片、音频、压缩包用字节流,处理文本文件用字符流,需要来回跳读用RandomAccessFile。16 位 Unicode 字符流的读写对应FileReader和FileWriter,8 位字节流对应FileInputStream和FileOutputStream。
| 需求 | 推荐类 | 说明 |
|---|---|---|
| 文本按行读取 | BufferedReader | readLine()按行返回 |
| 文本按行写入 | BufferedWriter | newLine()跨平台换行 |
| 二进制文件读取 | 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(); } }运行前先确认javac和java命令可用,这就是 java 环境变量配置做完之后最基本的验证方式。写断言比直接打印更进一步,比如用if (!"obj1.m=10".equals(output)) throw new AssertionError();,这样错误可以自动暴露,不用肉眼逐行比对。
6.2 记录实际输出与预期不符的位置
我一般会准备一个文本文件,左边写手推输出,右边贴实际运行结果。第一次跑 Tree 和 PineTree 的题目时,我把super, printTree(str)原样复制进去,编译直接失败,这才意识到是逗号被 OCR 成了错误字符。这类问题在处理旧版习题资料时非常常见,字符串中的全角逗号、漏写分号、类名大小写不一致都会导致复现失败。
最后留一个实用技巧:把程序分析题的输出结果做成独立的 JUnit 测试类,每个测试方法对应一道题。这样当你修改了自己写的代码后,随时可以跑一遍测试,确认旧答案没有被破坏。断言通过不是终点,把错误输出贴回题目旁边,才是这份习题真正被吃透的时候。
本文还有配套的精品资源,点击获取