简介:这份《Java 简答题》文档面向 Java 初学者、在校学生及准备笔试面试的开发者,围绕常见简答题提供条理化参考答案,可用于课堂复习、考前背诵与面试自测。压缩包内共 1 个 doc 文件,约 60KB,内容按主题组织,涵盖 Java 跨平台特性、类与对象、构造方法与重载、访问修饰符、八种基本数据类型、引用类型、String 与 StringBuffer/StringBuilder、包装器类型、值传递、static 与 final、== 及逻辑运算符、break/continue,以及数组、Arrays 工具类和类之间的依赖、关联、继承等知识点。每道题以简明问答形式呈现,便于快速检索和对照记忆,也能帮助读者梳理面向对象基础概念、发现知识盲区。该文档已有 433 人学习下载,适合需要系统过一遍 Java 基础简答题、进行考前查漏补缺的读者。
1. 一份 Java 简答题库,为什么值得当成面试前的“黑匣子”拆开看
面试前一晚还在翻八股文的人,大概都干过同一件事:把一份几十页的 Java 简答题背到“String 不可变”能脱口而出,结果面试官一句“那你讲讲equals和hashCode为什么必须一起覆盖”就被问穿。这份 Java 简答题资源的价值不在于“答案多”,而在于它把类结构、类关系、异常、核心 API、其他特性五个部分串成了一条完整主线,从基础语法一路推到集合、线程、JVM 加载。适合两类人:一是准备笔试面试、需要一个能自检的知识地图的开发者;二是带新人、想找一份能直接改造成内部题库的老手。下面按“先看答案对不对,再看怎么用代码验证”的顺序拆。
2. 第一部分的四个高频答案:类成员、构造方法、重载与权限修饰符
这一部分看着最基础,实际是面试官最爱追问的入口。简答题给的是结论,但结论背后的边界才是拉分点,逐个用代码验证一遍,比背十遍都强。
2.1 类的五类元素要能对应到字节码层面
原文把类中元素归为属性、方法、构造方法、块、内部类,这个划分是对的,但很多人答完就停。属性对应字段表中的field_info,方法是method_info,构造方法和普通方法在字节码里同属method_info,只是<init>这个名字特殊;块分为静态块和实例块,编译后分别并入<clinit>和<init>;内部类在编译期会生成独立的 class 文件,并持有一个指向外部类实例的引用。
public class Outer { private int value = 1; // 属性 static { System.out.println("clinit"); } // 静态块,编译进 <clinit> { System.out.println("init"); } // 实例块,编译进 <init> public Outer() { } // 构造方法 -> <init> public void run() { } // 普通方法 class Inner { // 内部类,生成 Outer$Inner.class void read() { System.out.println(value); } // 隐式持有 Outer.this } }这段代码编译后用javap -p -c Outer能看到<clinit>和<init>两个方法,实例块的内容被复制进每个构造方法开头;Inner会生成单独的Outer$Inner.class,构造器第一个参数是Outer类型,这就是“内部类能访问外部类属性”的实现机制。面试时答到这一层,基本不会被继续压。
2.2 构造方法的特征与“不能被修饰”的边界
原文说构造方法不能声明返回值类型、不能使用static/final/abstract修饰,这点要讲准确:构造方法本身是一个特殊方法,它隐含返回对象实例,所以不能写void;static属于类而非实例,与“创建对象”的语义冲突;abstract要求子类实现,而构造方法不会被继承,所以矛盾。final用来禁止覆盖,构造方法根本不参与覆盖,也没意义。
public class Point { private final int x; public Point(int x) { this.x = x; } // 合法:初始化 final 字段 // public static Point() {} // 编译错误:构造方法不能 static // public final Point() {} // 编译错误:构造方法不能 final }这里有个常见误答:有人说“构造方法可以被private修饰吗”。可以,单例模式就是这么干的,私有构造器阻止外部实例化。答“构造方法只能用 public 修饰”是要扣分的。
2.3 方法重载的判定:参数列表才是唯一依据
重载的判定标准是方法名相同、参数列表不同,返回值类型不参与。虚拟机在调用时有一个“方法描述符”的概念,即(参数类型列表)返回值类型,但 Java 语言层面只允许靠参数区分,因为foo()调用时无法根据返回值推断你调的是哪个。
public class OverloadDemo { public void print(int a) { System.out.println("int"); } public void print(Integer a) { System.out.println("Integer"); } public void print(int a, int b) { System.out.println("two int"); } // public int print(int a) { return 0; } // 编译错误:仅返回值不同不算重载 public static void main(String[] args) { new OverloadDemo().print(1); // 输出 int,精确匹配优先 new OverloadDemo().print(Integer.valueOf(1)); // 输出 Integer } }参数说明:print(1)会走基本类型版本,因为第一阶段匹配不需要装箱;print(Integer.valueOf(1))精确匹配包装类型版本。如果只有包装类型版本,print(1)会触发自动装箱。这套“三阶段匹配”(不含装箱、允许装箱、允许可变参数)在面试里能答出来,重载题就稳了。
2.4 权限修饰符的可见性要用一张表钉死
原文给了四列可见性表,但最容易错的是protected的“不同包的子类”这一列。准确说法是:子类可以访问从父类继承来的protected成员,但不能通过父类引用直接访问其他实例的该成员。
| 修饰符 | 同一类 | 同一包 | 不同包的子类 | 任意位置 |
|---|---|---|---|---|
| public | 可见 | 可见 | 可见 | 可见 |
| protected | 可见 | 可见 | 可见(限继承成员) | 不可见 |
| 缺省 | 可见 | 可见 | 不可见 | 不可见 |
| private | 可见 | 不可见 | 不可见 | 不可见 |
package p1; public class Base { protected int num = 10; } package p2; import p1.Base; public class Sub extends Base { void test(Base other, Sub self) { System.out.println(this.num); // 合法:继承来的成员 System.out.println(self.num); // 合法:本类型实例 // System.out.println(other.num); // 编译错误:不能通过父类引用访问 } }注意最后那行被注释掉的代码,这就是“不同包子类可见”的精确边界,也是简答题里表述不准、面试时容易被抓的点。
3. 第二部分:关联、依赖、继承与多态,写代码比背定义有效
类关系这一部分原文用文字区分了依赖和关联,但概念题最怕“说不清”。这一章的处理方式是把每种关系写成能编译的代码,再看运行结果和内存结构。
3.1 关联与依赖的区别落在字段和参数上
关联是长久关系,B 作为 A 的属性存在;依赖是瞬时关系,B 作为 A 某个方法的参数或局部变量出现。抓住“是不是字段”这一个判据,几乎不会判断错。
public class B { public void serve() { System.out.println("B serve"); } } public class A { private B b; // 关联:长期持有 public void setB(B b) { this.b = b; } public void work() { b.serve(); } public void handle(B temp) { // 依赖:方法参数,调用结束即释放 temp.serve(); } }参数说明:A持有B的字段,生命周期与A实例绑定,是关联;handle(B temp)里的B只在方法栈帧里存在,是依赖。如果面试官再追问“聚合和组合”,可以补一句:关联里如果整体销毁时部分也必然销毁,就是组合;部分可以独立存在则是聚合。这两者原文没提,但属于同一知识链上的延伸。
3.2 覆盖的方法解析过程要能说清
原文说覆盖要求子类方法与父类同名、同参数、同返回值,实际到了 Java 5 之后允许协变返回类型,子类可以返回父类返回值的子类型。这个细节面试常考。
class Animal { public Animal create() { return new Animal(); } public void speak() { System.out.println("animal"); } } class Dog extends Animal { @Override public Dog create() { return new Dog(); } // 协变返回类型,合法 @Override public void speak() { System.out.println("dog"); } public static void main(String[] args) { Animal a = new Dog(); a.speak(); // 输出 dog:运行时动态绑定 a.create().speak(); // create 返回 Animal 类型,但实际对象是 Dog } }逻辑说明:speak()是虚方法调用,字节码里是invokevirtual,运行时按实际类型查虚方法表;create()虽然返回类型协变,但编译期方法描述符按父类签名的“桥接方法”处理。这正是重载(编译期静态分派)与覆盖(运行期动态分派)的分界线。
3.3 抽象类与接口的选择判断
原文对抽象类和接口的对比停留在“抽象类可以有变量、接口方法全是抽象”。现代 Java 里接口可以有default和static方法,这个对比要更新。
public interface Movable { int SPEED = 10; // 隐含 public static final void move(); default void stop() { System.out.println("stop"); } // Java 8 起允许 } public abstract class Vehicle { protected String name; // 抽象类可以有实例字段 public abstract void move(); // 抽象方法 public void show() { System.out.println(name); } }参数说明:接口字段默认是常量,不能有普通实例字段;抽象类可以有状态。选择上,我一般用一条判据:需要共享状态或默认实现骨架时用抽象类,只想约定行为契约、允许多实现时用接口。原文那一道“类继承父类与实现接口的区别”,答案就是单继承与多实现的差别,但补上这个判据才算完整。
3.4equals与hashCode的契约用代码跑一遍
原文提出x.equals(y)为 true 时hashCode必须相同,这条契约在HashSet、HashMap里是硬约束,违反会导致元素“放进去找不到”。
public class User { private final String id; public User(String id) { this.id = id; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User)) return false; return id.equals(((User) o).id); } @Override public int hashCode() { return id.hashCode(); } // 与 equals 使用同一字段 public static void main(String[] args) { java.util.Set<User> set = new java.util.HashSet<>(); set.add(new User("u1")); System.out.println(set.contains(new User("u1"))); // 输出 true } }如果删掉hashCode覆盖,输出会变 false。原因在于HashSet先按哈希值定位桶,再在桶内用equals比较,两个逻辑相等的对象却落到不同桶里,自然找不到。排错时先看这两个方法是否成对实现,是处理集合“元素丢失”问题的第一招。
4. 第三部分到第四部分:异常处理与核心 API 的落地写法
异常和集合这两块是简答题里很容易背成“名词解释”的部分,但只要落到代码,坑就全冒出来了。
4.1 异常体系要按“必须处理”还是“可以不处理”分
原文区分了异常和错误,也问了NullPointerException。实际要记住的是:NullPointerException属于运行时异常,编译器不强制捕获;IOException属于受检异常,必须处理或声明抛出。
public class ReadDemo { public static void read(java.io.File file) throws java.io.IOException { try (java.io.BufferedReader br = new java.io.BufferedReader(new java.io.FileReader(file))) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } catch (java.io.FileNotFoundException e) { System.out.println("文件不存在: " + e.getMessage()); } } }逻辑说明:try-with-resources保证流自动关闭,等价于在finally里调close();throws声明受检异常给调用方;catch只处理FileNotFoundException,其余IOException继续上抛。参数上要注意:FileReader使用平台默认编码,中文文件可能乱码,我一般换成InputStreamReader并显式指定UTF-8。这就是简答题不会告诉你、但实际会翻车的地方。
4.2throw与throws的区别用一次自定义异常演示
class ScoreException extends RuntimeException { public ScoreException(String msg) { super(msg); } } public class ScoreChecker { public static void check(int score) { if (score < 0 || score > 100) { throw new ScoreException("分数越界: " + score); // throw 抛出对象 } } public static void main(String[] args) { check(120); } }throw后面跟异常实例,是动作;throws写在方法签名后,是声明。自定义异常继承RuntimeException表示不强制调用方捕获,继承Exception则强制。选择哪种,看业务上这个异常是否属于“调用方本应能避免”的错误。
4.3 集合框架的接口关系与List、Set、Map的取舍
原文问了Collection与Map的区别,标准答法是Collection存单元素,Map存键值对。落到实际选型还要看读写特征。
| 实现类 | 底层结构 | 随机访问 | 插入删除 | 是否有序 | 线程安全 |
|---|---|---|---|---|---|
| ArrayList | 动态数组 | 快 O(1) | 尾部快、中间慢 | 按插入顺序 | 否 |
| LinkedList | 双向链表 | 慢 O(n) | 头尾快 | 按插入顺序 | 否 |
| HashSet | 哈希表 | 无索引 | 均摊 O(1) | 不保证 | 否 |
| TreeSet | 红黑树 | 无索引 | O(log n) | 自然排序 | 否 |
| HashMap | 数组+链表/红黑树 | 按 key O(1) | 均摊 O(1) | 不保证 | 否 |
import java.util.*; public class CollectionDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("b"); list.add("a"); Collections.sort(list); // 工具类 Collections 提供静态方法 for (String s : list) System.out.print(s + " "); Map<String, Integer> map = new HashMap<>(); map.put("a", 1); map.forEach((k, v) -> System.out.println(k + "=" + v)); } }注意两个易混点:Collection是接口,Collections是工具类,名字差一个 s;ArrayList的add在扩容时是 1.5 倍增长,容量可预估时用new ArrayList<>(100)减少复制。这些都不是简答题的答案,但会决定你代码的实际性能。
4.4 线程创建的两种方式与synchronized的最小示例
原文要求列出创建线程的常用两种方法:继承Thread和实现Runnable。实际项目里更常用线程池,但面试先把这两种说清。
public class ThreadDemo { public static void main(String[] args) { Thread t1 = new Thread(() -> System.out.println("runnable 方式")); t1.start(); // 启动线程,不是 run() Thread t2 = new Thread(() -> { synchronized (ThreadDemo.class) { // 类锁,全局唯一 System.out.println("同步块"); } }); t2.start(); } }参数说明:start()才会真正创建线程并触发run(),直接调run()只是普通方法调用;synchronized加在实例方法上是对象锁,加在静态方法上是类锁,两者互不阻塞。另一个常考差异是sleep()不释放锁,wait()会释放锁并进入等待队列,这决定了线程通信的写法。
5. 常见问题排查:这份题库里答案本身有瑕疵的地方
5.1 现象:答“String 是 final 类,所以不可变”,追问“final 数组为什么还能改”就答不上
原因:final修饰引用只保证引用指向不变,对象内部状态仍可变。String的不可变来自private final char[] value加不对外暴露数组、所有“修改”方法返回新对象,两层共同保证。解决:背答案时补一句“不可变的本质是没有提供修改内部状态的途径,final 只是必要条件之一”。
5.2 现象:char能否存中文,答“能,因为 16 位”,面试官反问“生僻字呢”
原因:char是 UTF-16 编码单元,基本平面内一个char够用,扩展平面的字符需要一对代理项(两个char)。解决:答“常用汉字可以,扩展区字符要用两个char或直接用String/codePointAt”,这道题就答全了。
5.3 现象:try里有return,finally会不会执行
原因:finally一定执行,且在return表达式求值之后、方法真正返回之前执行。若finally里也return,会覆盖前面的返回值。解决:记住“finally 会执行但不要在里面写 return”,这道题的正确答法是“会执行,在 return 之前”。
5.4 现象:switch能否作用在long和String上
原因:早期switch只支持int兼容类型(byte、short、char、int)和枚举,Java 7 起支持String(编译成hashCode加equals判断),long始终不支持。解决:按“byte可以、long不行、String从 Java 7 起可以”来答,版本号说清就不会被追问。
5.5 现象:float f = 3.4编译报错,但答不出为什么
原因:3.4字面量默认是double,double转float属于窄化转换,需要显式加f后缀或强转。解决:写成float f = 3.4f。顺带一个易错点:Math.round(11.5)返回 12,Math.round(-11.5)返回 -11,因为round的实现是floor(x + 0.5),负数方向容易记反。
6. 把这份题库改造成可自测的知识清单:一个具体做法
背简答题最大的问题是“看过就会、合上就忘”。我一般会把它转成一个可运行的验证脚本,每道题配一段能复现结论的代码,跑通即算掌握。做法很简单:按五个部分建目录,每题一个 Java 文件,类名用题号,main里打印结论并附断言。
public class Q3_2 { public static void main(String[] args) { String a = "xyz"; String b = "xyz"; String c = new String("xyz"); assert a == b : "字面量来自字符串常量池,应同一引用"; assert a != c : "new 每次都创建新对象"; System.out.println("字符串创建方式验证通过"); } }运行javac Q3_2.java && java -ea Q3_2,-ea开启断言。参数说明:不加-ea断言不生效,这是很多人以为断言“没用”的原因。逐个跑下来,哪些题你只知道结论、写不出验证代码,一目了然,那才是真正要补的。用表格做个进度登记也行:题号、结论一句话、验证方式、是否跑通。这个方法我从带新人开始一直沿用,比刷题 App 的“记住了”按钮靠谱得多。
最后说个血泪经验:我早期准备面试时把简答题答案整段背下来,结果被问到“HashMap扩容为什么是 2 的幂”时完全接不住,因为背的答案里没有“为什么”。从那以后我每次看这类题库,都强制自己给每道题补一句“所以然”,补不出来的就标红去查源码。这份 Java 简答题资源作为知识地图是够用的,把它当成起点而不是终点,面试时才不至于被一句追问打回原形。希望帮到你。
本文还有配套的精品资源,点击获取