1. JavaSE第三阶段的定位:从语法到面向对象思维的关键一跃
很多人学JavaSE容易陷入一种误区——翻完一遍基础语法,数据类型、运算符、流程控制、数组都过了一遍,看着自己写的循环嵌套、字符串拼接也能跑起来,就觉得自己"基础已经掌握得差不多了"。但是真正来到面向对象这一块,尤其是继承、多态、抽象、接口这几个核心点的时候,很多人会明显感到吃力,甚至开始怀疑自己前面学的那些语法是不是白学了。
其实这不是你的问题,而是学习节奏本身就该如此。JavaSE(三)这阶段的核心,我从一开始就想明确告诉你:这一阶段的意义不在"学会几个新关键字",而在于让你的编程思维从"面向过程"切换到"面向对象",从"用代码实现功能"切换为"用代码描述现实关系"。继承让你学会抽取共性,多态让你学会面向抽象编程,接口让你学会约定行为规范。这种思维方式的转变,才是JavaSE(三)真正要训练的东西。
所以这篇内容我不打算再给你罗列一遍"继承是什么、多态是什么"这类教科书式的解释,而是想结合我实际开发中高频用到的场景,把这几个核心知识点拆开揉碎,讲讲它们之间是怎么串起来的,以及你在学习和实际写代码中特别容易踩的坑。等你看完这部分内容,再回头看那些重载、重写、向上转型、动态绑定之类的术语,你就不会觉得它们是一堆孤立的语法点,而是一个可以互相配合的完整体系。
2. 继承体系里的构造链与初始化顺序:很多隐性Bug的源头
2.1 继承的真正价值是"抽取共性",而不是"复用方法"
很多初学者接触继承时,第一反应是"父类里写好的方法,子类可以拿来直接用,这功能太方便了"。这个理解没错,但它太浅了。如果你只是把继承当作省代码的工具,那你在设计继承结构时很容易走偏——比如为了偷懒,把几个不相干的类硬塞进一个继承体系里,结果后面维护代码时痛苦不堪。
我在实际写代码时更倾向于把继承理解为"对现实世界分类关系的建模"。比如狗、猫、鸟都属于动物,它们都有名称、年龄、吃东西的行为,这就是共性,可以抽到Animal这个父类里;但它们的叫声不一样,走路方式不一样,这就需要用方法重写来让子类表现自己的差异。
class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(name + " 正在吃东西"); } public void sound() { System.out.println("动物发出声音"); } } class Dog extends Animal { public Dog(String name, int age) { super(name, age); } // 重写父类的sound方法 @Override public void sound() { System.out.println(name + " 汪汪叫"); } } class Cat extends Animal { public Cat(String name, int age) { super(name, age); } @Override public void sound() { System.out.println(name + " 喵喵叫"); } }你看,Animal里定义的eat方法是所有子类共享的行为,Dog和Cat不需要重复写;而sound方法虽然父类里有默认实现,但每个子类都有自己的声音表现,于是通过重写的方式各自实现。这个设计才是继承的正确打开方式——它帮你把"是什么"和"做什么"分离,让代码结构更贴近真实世界的分类逻辑。
2.2 子类构造器为什么第一行必须是super()?
这是继承中第一个容易让人困惑的点。我在教初学者时经常收到这样的问题:"我创建子类对象时,父类没有无参构造器,为什么编译都过不去?"
这里的底层逻辑其实非常简单:创建子类对象时,JVM会先完成父类部分的初始化,再初始化子类特有的部分。你可以把子类对象想象成一个套娃,外层是子类自己定义的属性和方法,内层是继承自父类的属性和方法。既然内层的父类部分还没创建好,外层子类部分怎么能安全地使用它继承来的那些属性呢?
所以Java语言层面做了一个强制约束:子类构造器的第一行必须调用父类构造器,如果没写,编译器会默认加上super()调用无参构造器。一旦父类没有无参构造器(也就是你定义了带参构造器并且没写无参构造器),那个默认的super()就会找不到目标而报错。
解决方式有两种。第一种是在子类构造器第一行显式调用父类的带参构造器;第二种是给父类补充一个无参构造器。我本人推荐第一种方式:
class Dog extends Animal { public Dog(String name, int age) { super(name, age); // 显式调用父类带参构造器 } }原因很简单:显式传参意味着你明确知道自己给父类注入了什么数据,整个对象的初始化链路是清晰可见的。如果随意加个无参构造器,父类里的name、age就可能处于默认值null和0的状态,后面用到这些属性时容易产生隐藏的空指针或逻辑错误。
2.3 初始化顺序:静态块、实例块、构造器的执行流程
关于对象初始化顺序,很多面试题爱考,但实际开发中它的意义更加重要——它直接决定了你在哪里做初始化才能保证数据的正确性。Java里一个类从加载到创建对象的完整流程是:
- 父类静态成员变量初始化、静态代码块执行(类加载阶段,只执行一次)
- 子类静态成员变量初始化、静态代码块执行
- 父类实例成员变量初始化、实例代码块执行
- 父类构造器执行
- 子类实例成员变量初始化、实例代码块执行
- 子类构造器执行
这里有个很容易忽略的细节:实例代码块和实例成员变量初始化的顺序,取决于它们在代码中出现的先后顺序,而不是"实例变量先执行、代码块后执行"这种固定的规则。如果你在实例代码块里访问了后面才声明的变量,会拿到默认值而不是你预设的值。
class Parent { static { System.out.println("1. Parent静态代码块"); } { System.out.println("3. Parent实例代码块"); } public Parent() { System.out.println("4. Parent构造器"); } } class Child extends Parent { static { System.out.println("2. Child静态代码块"); } { System.out.println("5. Child实例代码块"); } public Child() { System.out.println("6. Child构造器"); } }执行new Child()时,输出顺序就是你看到的1到6。理解这个顺序有什么用?举个实际场景:如果你的父类构造器里调用了某个实例方法,而这个实例方法被子类重写了,那么创建子类对象时,父类构造器里调用的那个方法实际上会执行子类重写后的版本。此时子类自己的字段还没初始化完毕,如果方法里访问了子类字段,就会拿到默认值。这就是所谓的"构造器里调用可重写方法"的坑,在实际开发里一定要避免。
3. 多态背后的动态绑定机制:为什么编译看左边,运行看右边
3.1 向上转型到底发生了什么?
多态是面向对象三大特性中最抽象也最迷人的一个,但它也是很多人学了很久仍然似懂非懂的地方。在Java里多态的实现依赖三个条件:继承(或实现接口)、方法重写、父类引用指向子类对象。
先说说向上转型。所谓向上转型,就是用一个父类类型的引用变量去指向一个子类对象:
Animal a = new Dog("旺财", 3); a.sound(); // 实际调用的是Dog重写后的sound方法,输出"旺财 汪汪叫"这里有个核心认知点:编译阶段编译器看到的是a的声明类型是Animal,所以它只允许你调用Animal类中定义过的方法,如果你试图调用一个Dog类独有的方法(比如fetchBall()),编译器会直接报错,即使a实际指向的对象确实是一个Dog。这就是那句顺口溜的由来——"编译看左边,运行看右边"。
在设计层面,向上转型的核心意义在于:你不再关心具体对象是Dog还是Cat,你只需要把它当作一个Animal来对待,调用Animal层面的行为即可。这一点在参数传递上体现得最明显。比如你写一个方法,接收一个Animal类型的参数,那Dog和Cat的实例都可以传进去;如果哪天又新增了一个Bird类,只要它继承Animal,这个方法依然能处理它,完全不用修改。
3.2 动态绑定:JVM是怎么找到真正要调用的方法的?
很多人知道多态的结果,但不清楚内部机制。这里我尽量用通俗的方式讲明白。
Java中方法的调用默认是动态绑定的,也就是在运行时,根据对象的实际类型来决定调用哪个方法。JVM的调用机制大致是这样的:每个类在加载时都会生成一个方法表(Method Table),里面记录了该类所有可调用方法的实际入口地址。当代码执行到a.sound()时,JVM先拿到a实际指向对象的类型——Dog类,然后去Dog的方法表中查找sound方法对应的入口地址。
这一查找过程对JVM来说成本是很低的,因为它不需要在继承链上层层遍历,方法表已经把"重写后的方法入口"记录好了。换句话说,动态绑定是JVM为了支持多态而专门设计的一套高效机制。
相对而言,static方法和private方法属于静态绑定,它们在编译阶段就确定了调用目标,不存在"运行时根据对象实际类型决定"的问题。这也解释了为什么static方法不能被重写——它根本不属于动态绑定那一套体系,你在子类里写一个与父类static方法签名完全相同的方法,充其量叫"隐藏",而不是重写。
Animal a = new Dog("旺财", 3); a.sound(); // 动态绑定:输出汪汪叫 // 如果sound方法是static,上面这行调用的就是Animal里的版本,因为编译期就绑定了3.3 向下转型与instanceof:什么时候需要"还原真实类型"
有了向上转型,自然就有向下转型。向下转型就是把父类引用强制转回子类类型,前提是这个父类引用实际指向的对象确实是那个子类类型。如果转错了,运行时会抛出ClassCastException。
我在实际工作中见过不少这种错误,根本原因就是没搞清楚"引用类型"和"对象真实类型"的区别。举个典型场景:
Animal a = new Dog("旺财", 3); Cat c = (Cat) a; // 编译不报错,但运行时抛出ClassCastException为什么编译不报错?因为从Animal转型为Cat,是父类类型转子类类型,编译器无法确定a实际指向的对象是Dog还是Cat,所以它不做拦截;但运行时JVM一检查,发现真实对象是Dog,和Cat没有任何实际类型关系,于是直接抛异常。
正确的做法是先用instanceof做类型判断:
if (a instanceof Cat) { Cat c = (Cat) a; c.sound(); } else if (a instanceof Dog) { Dog d = (Dog) a; d.fetchBall(); }这里还有一个Java新版本的细节值得注意:从Java 16开始,你可以使用模式匹配的instanceof写法,省去显式强转的步骤:
if (a instanceof Dog d) { d.fetchBall(); // d已经就是Dog类型,直接使用 }这种写法既简洁又安全,有条件的话我建议尽快用起来。
4. 重写与重载:同名方法的两种命运
4.1 重写的约束条件,远比你想象的严格
方法重写是Java中高频使用的基础能力,但偏偏有很多细节容易出错。我帮别人review代码时,最常见的问题除了忘了加@Override注解之外,就是重写方法的访问权限和返回类型不匹配。
先明确重写的核心约束:
- 方法签名必须一致(方法名、参数列表完全相同)
- 返回值类型可以相同,也可以是父类返回类型的子类型(这叫协变返回类型)
- 访问权限不能比父类方法更严格。比如父类是public,子类就不能改成protected或private
- 不能重写被final修饰的方法
- 构造器和static方法不能被重写
关于第3点,可能有些初学者不理解"为什么访问权限只能放大不能缩小"。你要换个角度想:既然子类对象"是一个"父类对象,那么所有通过父类引用能调用的public方法,在子类对象上也必须能调用。如果子类把父类的public方法改成private,那就意味着同一个对象,你在父类类型下能调用这个方法,换成子类类型反而不让调了,这显然破坏了多态的规则。所以Java从语法层面直接禁掉了这种写法。
4.2 重载:编译期就决定好调用哪一个
重载与重写最大的区别在于:重载是在同一个类中定义多个同名但参数列表不同的方法,重写是子类对父类方法的替换。重载在编译期就能确定调用哪一个(静态绑定),重写则在运行时通过动态绑定决定。
举一个实际开发中最常见的重载场景——构造器重载:
class User { private String username; private String email; public User(String username) { this(username, ""); } public User(String username, String email) { this.username = username; this.email = email; } }这个写法不仅体现了重载,还展示了一个我特别推荐的习惯:构造器之间用this()互相调用,让参数最全的构造器成为唯一的数据初始化入口。这样既避免了重复代码,也保证了无论走哪个构造器创建对象,最终设置字段的逻辑是统一的。
重载时有个很容易踩的坑是参数类型之间的自动类型提升。比如你定义了method(int a)和method(double a)两个重载方法,调用method(3)时,编译器会优先选择int版本,而不是把3自动提升为double再调用double版本。如果只有一个method(double a)方法,调用method(3)时编译器才会把int自动转成double来匹配。这种优先级机制可以帮助你理解为什么"最精确匹配"的规则在实际开发里这么重要。
4.3 @Override注解:不是摆设,是你的保护网
很多初学者写重写方法时习惯性不写@Override注解,还振振有词"不写也能实现重写功能"。这个说法本身没错,但不写注解意味着你失去了一道极其重要的编译期保护。
举个例子:你想重写父类的equals方法,结果不小心把参数类型写成了String而不是Object。如果有@Override注解,编译器会立刻报错"这个方法并没有重写父类的任何方法";如果你没写注解,这段代码会当作一个新定义的方法正常编译通过。你的本意是重写,实际却变成了重载,程序运行时的行为就会和你预期完全不一致,而且这类Bug很难通过阅读代码发现。
所以我一直强调:凡是意图重写父类方法,一律加上@Override注解。这是最低成本的安全防护,也是我们团队代码规范里明确要求的一条。
5. 抽象类与接口:从语法差异到设计哲学
5.1 抽象类:把"必须有但没默认实现"的行为说出来
抽象类是用来描述一类对象的"抽象模板"的。它不能被实例化,可以包含抽象方法和具体方法,子类必须实现所有抽象方法(除非子类自己也是抽象类)。
我常用的判断标准是:如果父类里有一个方法,无法给出合理的默认实现,必须由子类根据自己的情况来实现,那就把它定义成抽象方法,对应的类就标记为abstract。
拿之前Animal的例子来改:所有动物都会发出声音,但Animal这个类本身根本没法定义声音到底是什么——是汪汪、喵喵还是哼哼?那sound()就应该定义为抽象方法:
abstract class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(name + " 正在吃东西"); } // 抽象方法:子类必须实现 public abstract void sound(); }这样设计的好处是强制所有子类必须提供sound的实现,如果某个子类忘了写,编译器会直接报错,而不是让这个错误在运行时才暴露出来。
5.2 接口:从"是不是"到"能不能"的思维转换
如果说抽象类描述的是"是什么"(is-a关系),接口描述的则是"能做什么"(can-do关系)。一个经典的说法是:接口定义的是一个能力契约,任何一个类(不管它继承自谁)只要实现了这个接口,就承诺了具备这些能力。
以Java 8为分界,接口的语法演进很明显。Java 8之前接口里只能有抽象方法和常量;Java 8增加了default方法和static方法;Java 9又引入了private方法。default方法的出现解决了接口演进兼容性的问题——当你给一个已经发布并被很多地方实现的接口新增方法时,如果定义成抽象方法,所有实现类都必须实现它;如果定义成default方法,则不影响已有实现类。
但我要提醒一句:default方法不是让你"在接口里写业务逻辑"的绿灯。它最大的应用场景是在不破坏现有实现的前提下扩展接口能力,比如JDK里Iterable接口新增forEach就是典型的default方法应用。平时自己定义接口时,如果多个实现类都要用到某段公共逻辑,我更倾向于设计成抽象类,或者提供工具类,而不是塞进接口里。
5.3 抽象类还是接口?我的一套判断标准
面对实际需求时,很多初学者卡在"不知道该用抽象类还是接口"这个问题上。我这里给出一个可以直接套用的判断标准,是我这些年用下来最顺手的一套逻辑:
- 如果多个类之间存在明显的"父子血缘关系"(is-a),且有共享的成员变量或通用方法,考虑抽象类
- 如果多个类之间没有血缘关系,但都具备某种共同行为(can-do),考虑接口
- 如果既需要共享代码,又需要多能力组合,就用"抽象类 + 多个接口"的组合方式
- 优先考虑接口,因为Java单继承多实现的特性决定了接口在组合上有天然优势
关于"优先考虑接口"这一点,我多说两句。接口让你在解耦上有更大的自由度。调用的代码依赖接口而不是某个具体实现类,意味着将来替换实现(比如把数据库从MySQL换成Oracle)时,调用方代码可以完全不动。这种面向接口编程的思想,在很多框架中体现得淋漓尽致,Spring的AOP就是建立在接口代理之上的。
6. Object类与通用方法:所有类的隐藏父类
6.1 为什么所有类都能调用toString和equals?
Java中所有类都直接或间接继承自Object类,这是隐式的,不需要你在类声明里写extends Object,编译器会自动加上。这意味着Object里定义的方法,比如toString()、equals()、hashCode()、getClass()等,成为每个对象都具备的通用能力。
这个设计的巧妙之处在于:它定义了一套所有对象共通的行为标准。比如System.out.println(obj)里,println方法接收一个Object类型的参数,调用的是obj.toString()方法。你传入什么对象不重要,只要它是Object的子类(java里所有类都是),就能调用toString()。这就是"所有类都继承Object"最大的工程意义——栈、集合、日志框架等通用组件可以统一处理任何类型的对象。
6.2 重写equals必须同时重写hashCode的原因
Object里的equals默认实现是基于引用的,也就是两个引用指向同一个对象才返回true。很多场景下我们希望"值相等就算同一个对象",比如User类的两个实例,只要username和email一样,就认为是同一个用户。这种时候就必须重写equals方法。
但只重写equals是不够的,原因藏在HashSet、HashMap这类散列集合的工作机制里:它们先通过hashCode方法算出对象的散列值,定位到可能存储的桶;再通过equals与桶内已有元素比较,确定是否真的相等。如果两个对象equals返回true,但hashCode不同,它们会被散列到不同的桶里,即使内容一样,HashMap也可能把它们当作两个不同的键。
这里有一个硬性约定你必须记住:如果两个对象equals比较结果为true,那么它们的hashCode返回值必须相同。反过来不成立——hashCode相同,equals不一定为true,因为不同对象可能产生相同散列值(哈希冲突)。
因此,重写equals几乎总是要同时重写hashCode。IDE生成的equals+hashCode代码通常是把equals中用到的关键字段也作为hashCode计算的依据,这是最稳妥的做法:
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(username, user.username) && Objects.equals(email, user.email); } @Override public int hashCode() { return Objects.hash(username, email); }6.3 equals用法上的一个细节陷阱
有一个很小的细节特别容易踩坑,就是equals和hashCode方法中getClass()与instanceof的选择。getClass() != o.getClass()要求比较的两个对象运行时类型完全一致;而instanceof则允许子类对象与父类对象相比较(只要满足继承关系就返回true)。
如果你的业务规则是"只要是User及其子类,只要关键字段相同就算同一个用户",那用instanceof更合适;如果你的规则是"必须是完全相同的类型才算同一个用户",那应该用getClass()比较。实际项目里,两者如何选择取决于业务语义,没有绝对标准,但有一点可以确定:不明所以地混用会导致equals的不对称性风险。
7. final关键字在类设计中的三种角色
7.1 final修饰数据:常量与不可变引用的区别
final修饰基本类型变量时,表示这个值一旦赋值就不能再修改;final修饰引用类型变量时,表示这个引用不能再指向其他对象,但引用指向的对象本身的内容可以改变。
很多人混淆了"引用不可变"和"对象不可变"这两个概念。看这个例子:
final List<String> list = new ArrayList<>(); list.add("A"); // 这是允许的 list = new ArrayList<>(); // 编译报错,不能重新赋值你定了一个final的List引用,不能让它重新指向另一个List对象,但可以通过add方法往这个List里添加元素。如果需要真正"不可变"的列表,应该使用Collections.unmodifiableList()或者Java 9之后的List.of()来创建。
7.2 final修饰方法:禁止重写,但不是"性能优化"
final方法不能被重写。关于final方法有一个很古老的提法叫"性能优化",认为final方法跳过了动态绑定,性能更好。但在现代JVM中,这个论点几乎没有意义了——JVM的运行时优化(比如内联)会自行判断哪些方法可以安全地内联,与是否加final关系不大。
因此对我来说,final方法的最大价值是设计层面的:当某个方法的逻辑是固定的、不希望子类改动时,用final明确表达这个意图。比如模板方法模式中,模板方法本身通常声明为final,防止子类破坏整体流程。
7.3 final修饰类:不可继承的边界约束
final类不能被继承。String就是典型的final类,ArrayList也是。为什么这些类要设计成final?核心原因是为了安全性和正确性。以String为例,它被广泛用于类加载、网络协议、哈希表键等场景,如果允许继承,子类就可能override方法改变字符串的语义,造成难以预料的后果。String是不可变类,而final让不可变性从"设计意图"上升为"语言强制"。
在实际开发中,当你设计一个工具类(全部由静态方法组成)或者一个值对象且不希望别人通过继承扩展时,可以考虑把它声明为final类。
8. 综合实战:一个完整练习打通继承、多态、抽象、接口
前面对每个知识点的单点分析,现在我们把它们全部串起来做一个综合练习。这个练习我建议你亲自动手敲一遍,因为它的设计逻辑非常接近真实项目中的模块划分方式。
需求很简单:设计一个"动物园"系统,能管理多种动物,每种动物都会发出叫声,同时具备清洗和喂食两种日常维护动作。
先定义抽象基类,把所有动物共有的属性和行为放进去:
abstract class Animal { private String name; public Animal(String name) { this.name = name; } public String getName() { return name; } public abstract void makeSound(); public void feed() { System.out.println(name + " 正在进食"); } }再定义两个能力接口,把"能清洗"和"能表演"这些跨类的能力独立出来:
interface Washable { void wash(); } interface Performable { void perform(); }然后定义具体动物类。注意组合方式:一个类既可以继承抽象类,又可以同时实现多个接口:
class Lion extends Animal implements Washable { public Lion(String name) { super(name); } @Override public void makeSound() { System.out.println(getName() + " 吼叫"); } @Override public void wash() { System.out.println(getName() + " 正在水洗"); } } class Parrot extends Animal implements Washable, Performable { public Parrot(String name) { super(name); } @Override public void makeSound() { System.out.println(getName() + " 模仿人声"); } @Override public void wash() { System.out.println(getName() + " 正在喷雾清洗"); } @Override public void perform() { System.out.println(getName() + " 正在表演说话"); } }最后模拟一个动物园维护场景,体现多态的解耦效果:
public class Zoo { public static void main(String[] args) { Animal lion = new Lion("辛巴"); Animal parrot = new Parrot("波利"); // 面向Animal父类编程 List<Animal> animals = List.of(lion, parrot); for (Animal animal : animals) { animal.makeSound(); animal.feed(); } // 面向接口编程 List<Washable> washables = List.of((Washable) lion, (Washable) parrot); for (Washable washable : washables) { washable.wash(); } // 接口单独调用 Performable performable = (Performable) parrot; performable.perform(); } }这个例子有几个值得仔细体会的点。第一,List 里同时装了两个不同动物,遍历时调用makeSound(),展现的正是多态的动态绑定特性。第二,Washable接口定义清洗能力,Lion和Parrot虽然血缘关系不同,但都实现了这个能力,用接口就能统一处理。第三,抽象类负责抽取公共属性(name、feed方法)和强制行为(makeSound),接口负责扩展附加能力(可清洗、可表演),彼此分工明确,互不干扰。
如果以后再增加一头新的动物,比如老虎Tiger,只要继承Animal并实现需要的接口即可,Zoo类中的逻辑完全不用改动。这种扩展性正是面向对象设计追求的目标。
9. JavaSE(三)学完后,你应该具备的自我检查清单
这一路知识点比较多,我最后整理一份自查清单,你可以拿它来检验自己的掌握程度。这里每一栏都不是"知道概念"就行,而是要能当场手写出代码、能解释清楚原因的。
继承设计:能独立写出一个合理的继承结构,清楚地区分哪些成员应该放到父类,哪些应该由子类自己定义;能解释super()在构造链中的作用。
初始化顺序:能从"类加载"开始,完整说出一个子类对象创建过程中,静态块、实例块、构造器的执行顺序,并且能说明原因。
多态原理:能用"编译看左边,运行看右边"解释向上转型后的方法调用行为;能写出一个使用instanceof做安全向下转型的示例。
重写与重载:能列出重写的全部约束;能说出重载与重写在绑定时机上的本质区别;所有重写方法都养成了加@Override注解的习惯。
抽象类与接口:能用自己的语言解释两者的定位差异(is-a vs can-do);能手写一个抽象类和一个接口,并对三者(父类、抽象类、接口)做出正确设计决策。
Object方法:能重写equals/hashCode方法,并正确解释为什么两者必须同时重写;知道toString在日志输出中的重要性。
final关键字:能说清楚final修饰变量、方法、类的不同效果;知道"引用不可变"与"对象不可变"的区别。
综合运用:能独立完成一个涉及多个类、继承体系、接口实现的小项目(比如动物园管理、订单系统),并通过多态简化代码结构。
如果你能不打磕绊地完成以上项目的内容,那么JavaSE(三)这部分就算真正掌握了,后面的集合框架、泛型、异常处理、IO流等进阶内容,学起来就会顺畅很多。反过来,如果哪一项还需要翻笔记才能答出来,我建议你先停下往前走,回去把对应的知识点再看一遍,这一步的基础没打牢,后面的知识体系很容易出问题。