1. 类与对象Java的初始奥义1.1 从变量到类编程思维的第一次升维学完运算符、分支循环和数组后很多人会陷入一个困惑明明已经能用代码算出水仙花数、打印九九乘法表了为什么还要学类借用某位资深工程师培训新人的原话“写程序不是让计算机算数学题而是让计算机帮你管理真实世界”。真实世界里有学生、订单、电影票、银行账户。它们有属性比如学生的姓名、学号和成绩它们有行为比如选课、退课、打印成绩单。如果只用基本类型你得同时维护好几个平行的数组稍不留神下标就对不齐了。类就是用来把“属性行为”打包到一起的容器对象就是按照这个容器造出来的具体个体。新手最容易犯的认知误区是混淆“类”和“对象”。我平时讲课喜欢用月饼模子做类比类是模子规定了月饼应该有花纹属性和能被脱模行为对象是压出来的那块月饼有具体的花纹细节、重量和馅料。模子只有一个但月饼可以有无数个世界上不存在两块完全相同的月饼。类是设计图对象是实物类是模板对象是实例。在Java里定义一个学生类通常长这样public class Student { String name; int studentId; double score; public void printInfo() { System.out.println(姓名 name 学号 studentId 成绩 score); } }这段代码告诉Java虚拟机三件事这个类叫Student每个Student对象身上会随身携带name、studentId、score三个变量这个类提供了一种行为叫printInfo任何学生对象都可以调用它。变量和方法都定义在类里面这是一个标准的封装雏形。然后通过new关键字把模子里的“月饼”压出来Student s1 new Student(); s1.name 张三; s1.studentId 2024001; s1.score 87.5; s1.printInfo();从内存视角看new Student()在堆里开辟了一块空间s1是栈上的引用变量拿着那块空间的地址。很多初学者会纠结引用到底是什么我的建议是先别深挖只需记住一个核心模型**Java里除了基本类型一切变量都是引用引用的本质是地址的搬运工。**理解这一条后面学集合、学链表时会少掉很多头发。1.2 构造器与this关键字每个对象出生时的仪式定义好类之后接下来要面对的是“初始化”问题。直接给字段赋值当然可行但在真实项目中一个对象往往需要校验参数、初始化资源、甚至调用其他服务获取配置这些逻辑如果散落在外部代码里类的复用性会大打折扣。于是有了构造器。构造器的名字必须与类名完全一致没有返回类型在new时自动执行。如果没写编译器会送一个隐式的无参构造器。如果写了任何构造器编译器就不再赠送默认的了。这一条规则是新手基础面试中的高频考点。给Student类加上两个构造器public class Student { String name; int studentId; double score; public Student() { // 默认构造器什么都不做等待后续setter } public Student(String name, int studentId, double score) { this.name name; this.studentId studentId; this.score score; } }这里出现了this关键字。它的作用是在方法内部指代“当前调用这个方法的对象”。当参数名和字段名同名时this.name name就是在告诉编译器等号左边是对象的字段右边是外面传进来的变量。你可以把this理解为每个对象自带的镜子永远照到的是自己。构造器之间的互相调用用this(...)语法。比如public Student(String name) { this(name, 0, 0.0); }这种写法叫构造器重载好处是避免重复代码。一个常见经验如果字段很多而且有默认值逻辑尽量在构造器里做集中初始化不要把复杂的初始化逻辑抛给调用方。注意this(...)调用必须写在构造器第一行这是Java语法硬性规定。原因在于构造器执行时对象的内存分配已经完成子类构造器很可能需要父类先完成初始化而Java必须保证初始化顺序的确定性。1.3 封装的艺术为什么不能把所有字段都设成public很多刚从Python或JavaScript转过来的学习者都有一个习惯把字段直接暴露出去谁想改就改。初期爽快后期灾难。几天之后你根本不知道哪个模块偷偷改了某个字段排查问题如同大海捞针。Java用访问修饰符和getter/setter营造了一个更严苛但更可控的边界。核心原则很简单**字段私有化方法公开化。**把Student的字段改成private然后对外提供访问入口public class Student { private String name; private int studentId; private double score; public String getName() { return name; } public void setName(String name) { if (name null || name.trim().isEmpty()) { throw new IllegalArgumentException(姓名不能为空); } this.name name; } public double getScore() { return score; } public void setScore(double score) { if (score 0 || score 100) { throw new IllegalArgumentException(成绩必须在0到100之间); } this.score score; } }外部代码无法直接写s1.score -10只能通过setScore方法而方法内部有了校验的机会。说白了封装就是给字段配上一个安检门所有数据进出都要过一道程序。芯片行业有一句话叫“胶水逻辑导致bug”一个对象内部的数据若是到处裸奔改坏的成本比省下的那几行setter要多得多。实际开发中很多人偷懒用IDE生成getter/setter这没问题。但请保持清醒**getter/setter只是封装的底线不是封装的终点。**更高级的封装是提供业务方法比如student.enrollCourse(course)而不是暴露一个选课列表让外部随意add。2. 继承与多态把代码复用玩出体系感2.1 继承体系的设计什么时候才应该用继承面向对象三大特性里继承最容易被人滥用。教科书喜欢讲“猫继承动物”但到了真实项目里很多人不管三七二十一就建一个BaseService然后把所有公用的方法丢进去最终变成上帝类。真正的继承应该满足一个朴素的标准子类是一个更具体的父类并且子类和父类之间是“is-a”的关系。Java中用extends关键字实现继承。Student如果被设计为Person的子类意味着Student首先是一个Person然后额外拥有学号、选课等功能。下面是典型结构public class Person { protected String name; protected int age; public Person(String name, int age) { this.name name; this.age age; } public void sayHello() { System.out.println(你好我叫 name); } } public class Student extends Person { private int studentId; public Student(String name, int age, int studentId) { super(name, age); this.studentId studentId; } Override public void sayHello() { System.out.println(你好我是学生 name 学号 studentId); } }super关键字在这里有两个用途一是调用父类构造器必须放在子类构造器第一行二是调用被覆写的父类方法。很多初学者搞不懂为什么super()这行必写且必须第一行因为父类字段需要先于子类完成初始化否则子类方法执行时可能读到尚未初始化的父类状态Java通过语法强制保证了这一点。继承的访问权限有个值得一提的细节就是protected的语义。当前包内可以访问子类也可以访问——不管子类在哪个包。这在做框架代码时非常常用提供默认实现的同时允许子类定制。protected并不完美它会让类的封装边界变得模糊但比起包内可见的默认权限它给了跨包扩展一个体面的口子。2.2 方法覆写的规矩与注解的作用方法覆写是继承中最容易出错的点。Java要求覆写方法的签名必须一致返回类型可以是协变类型子类返回类型是父类返回类型的子类访问权限不能比父类更窄。比如父类是public的方法子类不能把它降级成protected因为这会破坏多态性——调用方只知道自己在调用Person万一实际对象是Student却不能按Person的契约访问public方法就乱套了。建议每位学习者养成写override注解的习惯。它的作用不是强制性的语法检查而是给编译器一个提示帮我校验这里到底有没有正确重写父类方法。如果方法签名写错了比如少写一个参数编译器会立刻报错而不是让你在运行期对着堆栈发呆。覆写与重载是两个截然不同的概念。覆写解决的是“子类替换父类行为”重载解决的是“同名方法不同参数”。覆写是父子类之间的纵向关系重载是同一类内部的横向多版本。还有个细节容易被忽略静态方法不能被覆写。子类里写了同名的静态方法只是把父类的静态方法影藏了调用时取决于引用类型而不是对象类型。我第一次带项目时就在这上面栽过跟头以为是多态派上了用场结果调用的是父类静态方法排查半天才明白原因。能不用继承去“复用”静态方法就别用静态方法用类名调用即可。2.3 多态的底层视角与转型风险多态是面向对象里最优雅也最抽象的机制。它允许父类引用指向子类对象Person p new Student(李四, 20, 2024002); p.sayHello();这里p的编译期类型是Person运行期类型是Student。调用sayHello时会根据实际对象类型去执行Student的覆写版本。这就是传说中的动态绑定也叫运行时多态。它的价值在于你可以写一套面向Person的代码不用关心具体是哪种人程序会在运行时自行决定。底层原理其实不玄乎。JVM的方法区里维护着一张方法表每个类继承结构会生成一张虚方法表子类覆写的方法在表中的槽位替换为子类实现的方法指针。调用p.sayHello()时JVM根据p的实际类型找到对应的一张方法表按偏移量取出目标方法地址。这是“一个接口多种实现”的核心支撑。多态有代价代价是转型时的风险。把Student向上转型为Person是安全的因为子类一定具备父类的全部能力反过来向下转型就不一定安全了你无法保证一个Person对象当初是Student造的。用instanceof可以做判断if (p instanceof Student) { Student s (Student) p; s.printStudentCard(); }Java 16以后instanceof模式匹配可以简化写法if (p instanceof Student s) { s.printStudentCard(); }经验写代码时要尽量面向父类编程把具体子类的细节关在门后。但永远不要盲目向下转型转型前必须用instanceof确认。转型一次是手段转型多了说明你的继承体系多半设计歪了。3. 抽象类与接口把复杂系统里的规矩立起来3.1 抽象类把公共骨架抽出来把细节留给子类在一个设计良好的系统里父类很少被直接实例化。比如动物这个父类你根本不知道该给它的“移动”方法写什么具体逻辑。此时可以让父类成为抽象类把不完整的部分交给子类去填空。抽象类用abstract关键字修饰。可以包含抽象方法只有声明没有实现也可以包含普通方法有完整实现、字段和构造器。抽象类的构造器无法直接new但子类构造时会先执行父类构造器来初始化公共字段。public abstract class Animal { protected String name; public Animal(String name) { this.name name; } public abstract void makeSound(); public void eat() { System.out.println(name 正在进食); } }子类继承抽象类后要么实现所有抽象方法要么把自己也声明为抽象类。这套机制解决的问题是把一堆子类中高度相似的字段和逻辑收拢到一处同时强制每个子类必须实现某些能力防止漏写。抽象类最典型的应用场景是模板方法模式。父类定义好整个业务骨架把某些步骤声明为抽象方法子类各自定制。比如public abstract class DataParser { public final void parse(String filePath) { openFile(filePath); String raw readContent(filePath); process(raw); closeFile(filePath); } protected abstract String readContent(String filePath); protected abstract void process(String raw); private void openFile(String filePath) { System.out.println(打开文件 filePath); } private void closeFile(String filePath) { System.out.println(关闭文件 filePath); } }这种写法把“文件解析的普遍流程”锁死在父类里每个具体解析器只需填两个抽象方法。模板方法模式在大数据处理、配置加载、报表生成等场景里遍地都是。3.2 接口把能力契约从实现中剥离开接口比抽象类更抽象。它定义的是“能做什么”而不是“是什么”。接口里的方法默认是public abstract字段默认是public static final即常量。Java 8以后接口允许default方法和static方法但这不改变接口的核心定位能力契约。public interface Flyable { void fly(); } public interface Soundable { void makeSound(); }一个类可以同时实现多个接口解决了单继承的局限public class Bird implements Flyable, Soundable { Override public void fly() { System.out.println(鸟儿展翅飞翔); } Override public void makeSound() { System.out.println(叽叽喳喳); } }接口之间还可以用extends实现继承链比如一个接口可以extends多个父接口。这比类的继承更灵活因为接口没有实现负担只传递方法签名。Java 8引入的default方法是个值得说道的细节。当初为了让现有集合框架接口增加新特性又不想让每个实现类都改代码Oracle给大家提供了默认实现public interface Flyable { void fly(); default void glide() { System.out.println(默认滑翔动作); } }这意味着实现类可以不实现glide()直接继承默认实现也可以按需覆写。default方法的价值是向后兼容让接口在新增能力时不破坏已有实现。但它也带来了菱形问题一个类实现了两个接口两个接口都有同名的default方法就会产生冲突编译期强制要求你覆写这个方法。3.3 抽象类还是接口一个老生常谈但值得再谈的选择题这是面试高频题也是设计时的经典选择难题。我的建议非常简单只要你能说出“这个父类有状态需要维护”就用抽象类。只要你说的是“具备某种能力谁来都要遵守这个契约”就用接口。抽象类适合承载公共字段、构造器、访问权限控制、模板方法骨架的场景。接口适合定义模块间的依赖边界、提供能力标记、实现多继承契约的场景。Java 8以后接口有了default方法但接口依然不能持有非静态字段依然没有构造器。所以咱们写代码时如果你发现自己想在接口里定义一个private字段来缓存什么说明这里应该用抽象类。具体项目的经验法则做框架、做组件库时优先定义接口让调用方只依赖契约做领域模型时优先使用抽象类组织公共逻辑。很多优秀框架比如各种ORM、MVC都是“抽象类接口”混着用的接口负责对外能力抽象类负责屏蔽实现细节。4. 异常处理程序崩溃前最后的防线4.1 异常体系梳理Error与Exception的区别Java的异常体系以Throwable为根。下面分两条主干Error和Exception。Error是JVM层面的严重问题比如OutOfMemoryError、StackOverflowError。这种错误通常不具备恢复价值你catch住了也没法正常继续跑最好的处理方式就是让程序结束、把日志打清楚。我见过不少新人试图去catch OutOfMemoryError以为可以优雅退出结果越catch越糟。别跟Error较劲它不是给应用层处理设计的。Exception又分两类受检异常checked和非受检异常unchecked即RuntimeException及其子类。受检异常在编译期强制你处理不处理就编译不过。典型的有IOException、SQLException。这类异常通常代表外部条件不稳定文件不存在、数据库连不上、网络断了。非受检异常是运行时才暴露的编译期不强制处理。典型的有NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException。它们代表程序逻辑本身的bug数组越界说明下标算错了空指针说明对象没初始化。4.2 try-catch-finally的完整形态与执行顺序处理异常的经典组合是try-catch-finally。基本形态FileReader reader null; try { reader new FileReader(data.txt); char[] buffer new char[1024]; int len reader.read(buffer); System.out.println(new String(buffer, 0, len)); } catch (FileNotFoundException e) { System.err.println(文件找不到 e.getMessage()); } catch (IOException e) { System.err.println(读取失败 e.getMessage()); } finally { if (reader ! null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } }执行顺序是先走try里的代码如果抛异常且类型匹配某个catch就进入对应catch块不管有没有异常finally一定会执行。try里return了finally仍然先执行这跟很多初学者的直觉相反。Java 7以后引入了try-with-resources语法只要资源实现了AutoCloseable接口就可以自动关闭try (FileReader reader new FileReader(data.txt)) { char[] buffer new char[1024]; int len reader.read(buffer); System.out.println(new String(buffer, 0, len)); } catch (IOException e) { System.err.println(IO异常 e.getMessage()); }这份代码简洁不少不需要手写finally关闭。底层编译器会帮你生成关闭代码并且关闭顺序跟创建顺序相反。建议日常开发凡是碰到资源类就别再手写finally了直接用try-with-resources省心且不容易漏关。4.3 异常设计的最佳实践异常处理是代码设计里最能看出工程师功力的部分。几条经验简单粗暴但非常有效。**不要吞异常。**catch块里只有一个e.printStackTrace()甚至空着不打印出了问题日志里一片安静排查起来极度痛苦。好的做法是记下上下文信息、参数、当前状态。如果真的确定这个异常可以忽略请在日志里明确说明忽略的原因。**不要用异常控制流程。**有人习惯用抛异常来跳出循环或做条件判断这在Java里是极其糟糕的实践。异常机制的构造、填充堆栈、捕获、解栈每一步都有不小的性能开销而且代码可读性极差。正经的判断逻辑用if解决异常只用于真正的异常状况。**自定义异常要区分受检和非受检。**如果异常是外部条件导致的而且调用方有责任处理设计成受检异常。如果异常是编程错误而且调用方根本来不及恢复设计成非受检异常继承RuntimeException。博主在团队里定的规矩是业务规则校验失败用非受检异常外部资源操作失败用受检异常。异常信息要带上下文。“文件不存在”这种信息基本没用。更合理的写法是“读取订单导出文件order_2024_08.csv时失败目标文件不存在目录/data/orders已确认”。我不推荐在日志里打敏感数据但业务上下文必须带。5. 集合框架与泛型搞定数据容器的武功秘籍5.1 集合框架的体系结构与选用指南数组的硬伤是长度固定。真实项目里数据量是运行时才知道的于是Java提供了集合框架Collection Framework。整个框架以Collection为根接口下面有两大分支List和Set另有独立的Map体系。List有序、可重复。ArrayList底层是Object数组查询快、增删慢。日常开发出场率最高。LinkedList底层是双向链表插入删除快特别是中间位置随机访问慢。实际项目中并不那么常用除非明确需要频繁在表头表尾操作且数据量大。Set无序、不可重复。HashSet基于HashMap存取效率高不保证顺序。LinkedHashSet保持插入顺序代价是略微增加内存。TreeSet元素有序自然顺序或自定义比较器基于红黑树插入删除是O(log n)。Map键值对。HashMap最常用基于数组链表红黑树允许一个null键和多个null值不保证顺序。LinkedHashMap保持插入顺序适合做LRU缓存。TreeMap键按自然顺序排序适合范围查询。菜单式选择指南需要按索引访问就选ArrayList需要快速去重就选HashSet需要排序且去重用TreeSet需要键值映射且不关心顺序闭眼选HashMap真的。5.2 ArrayList的扩容机制与迭代安全ArrayList的默认容量是10当元素个数超过当前容量时会自动扩到原来的1.5倍。它是怎么扩容的先new一个新数组长度是老数组的1.5倍再用System.arraycopy把老数组元素搬过去。这个操作是O(n)的如果频繁触发扩容性能会明显下降。一个减少扩容代价的小技巧如果能预估数据规模直接new ArrayList(capacity)让数组一步到位。博主在处理百万级数据导入时就因为少写了个容量参数多花了不少时间在扩容上。往集合里加元素时如果用for-each遍历的同时直接调用add或remove会抛ConcurrentModificationException。原因是迭代器内部有一个modCount计数器每次结构性修改都会加一迭代时发现计数器对不上就立刻抛出异常。这是保护机制防止你在遍历时破坏结构。正确做法是用迭代器的remove方法或者干脆用Java 8的removeIflist.removeIf(item - item.length() 10);5.3 泛型的边界通配符与类型擦除Java的泛型是编译期检查、运行期擦除的。写List 和List 在字节码层面其实是同一个类型只是编译器在插入检查和强制转换方面做了手脚。这带来的结果有两个运行期拿不到类型参数的具体类型用反射可以绕过泛型约束。泛型通配符是很多人的难点。三个符号记法? extends T表示T或T的子类适合读取称为协变。? super T表示T或T的父类适合写入称为逆变。?无界通配符表示任何类型。一个经典规则叫PECSProducer Extends Consumer Super如果你只往外读数据用extends如果你只往里写数据用super。这条规则来自Java集合框架设计者本尊的经验总结在Stream和Collections工具方法里尤为明显。public static void printAll(List? extends Number list) { for (Number n : list) { System.out.println(n); } } public static void addNumbers(List? super Integer list) { list.add(42); }泛型擦除让new T()和new T[]这种写法变得不可能因为运行期根本不知道T是什么。因此很多框架需要传入Class 参数来做运行时反射这是Java泛型在历史兼容性面前的妥协。5.4 集合与冒泡排序一把基本功好用的双刃剑数组与集合的互转是日常高频操作。数组转ListString[] arr {a, b, c}; ListString list Arrays.asList(arr);特别注意这个asList返回的List是固定大小的不支持add和remove但可以set修改。想得到一个真正完整的ArrayList得再包一层new ArrayList(Arrays.asList(arr))。List转数组String[] newArr list.toArray(new String[0]);至于排序JDK 8之后List本身提供了sort方法底层用的是归并排序的稳定版本TimSort。日常排序都是list.sort(Comparator.comparing(...))这种写法手动手写冒泡排序的场合已经很少了。但面试考查冒泡排序仍有价值它能检验你对基本算法流程、双层循环、边界条件的把握。建议手写时加上优化如果某趟扫描没有任何交换说明已经有序可以提前退出。6. Java 8新特性的实战装备Stream与Lambda6.1 Lambda表达式让函数像参数一样传递JDK 8是Java历史上最重要的一次版本升级其中Lambda让Java从“纯面向对象”转向“函数式辅助”。Lambda的本质是匿名函数可以简化那些只有一个抽象方法的接口函数式接口的实现。// 之前的写法 Runnable r1 new Runnable() { Override public void run() { System.out.println(hello); } }; // Lambda写法 Runnable r2 () - System.out.println(hello);对于带参数的方法箭头左边是参数列表右边是方法体ComparatorString comparator (s1, s2) - s1.length() - s2.length();Lambda表达式的类型由上下文推断。Java自带一套类型推断逻辑如果方法参数是Comparator 编译器会自动推断s1和s2都是String。不过Lambda只支持函数式接口——有且只有一个抽象方法的接口。为了避免误用Java提供了一个注解FunctionalInterface如果接口里定义了多个抽象方法编译器会直接报错。6.2 Stream流集合处理的管道式操作Stream是对集合进行批量操作的一套声明式API。它的核心思想是数据从源头流经一系列中间操作最后在终止操作里汇总结果。整个过程不会修改原有集合而是产生一个新的流对象。ListString names Arrays.asList(张三, 李四, 王五, 赵六); ListString result names.stream() .filter(name - name.startsWith(张)) .map(String::toUpperCase) .collect(Collectors.toList());filter是过滤map是映射转换collect是汇集成集合。方法引用String::toUpperCase是Lambda的一种简写形式表示调用String的toUpperCase实例方法。中间操作是惰性的它们只记录操作不会立即执行。只有遇到终止操作比如collect、forEach、count时才会真正触发执行。这是Stream性能设计上很重要的一点避免无谓的计算浪费。names.stream() .filter(name - { System.out.println(过滤 name); return name.length() 1; }) .limit(2) .forEach(System.out::println);执行这段代码会发现filter只输出了前两个名字第三个名字压根没有进入filter——这就是短路优化。Stream披着高大上的外衣实际上仍然是循环和判断的封装理解这一点就不会被奇奇怪怪的API名绕晕。6.3 收集器Collectors与并行流的避坑指南Collectors工具类提供了大量收集器。最常用的toList、toSet、toMap还有joining拼接字符串、groupingBy分组、partitioningBy分区。举个例子MapString, ListStudent byClass students.stream() .collect(Collectors.groupingBy(student - student.getClassName()));一个班级为键对应学生列表为值几行代码搞定原本要写一堆循环的分组逻辑。partitioningBy是一种特殊的分组键固定是true和false适合把数据切分为两拨。并行流parallelStream可以把操作分发到多个线程执行。听起来很美用起来凶险。并行流底层用的是Fork/Join框架公共池里维护着线程。如果操作里有共享可变状态比如多个线程往同一个List里add就会产生线程安全问题。此外频繁的小数据量任务根本不适合并行——创建线程、任务切分、结果合并的开销超过了收益。我的经验是数据超过十万级别且中间操作是无状态的才考虑并行流。7. 常见报错与排查实录7.1 最有代表性的五类报错实践中最常遇到的报错就是那么几类把它们吃透了学习效率能翻一倍。空指针NullPointerException排查口诀是先看日志定位哪一行代码再检查这行代码里哪个对象是null。最简单的办法是在关键位置打印对象或利用调试器查看变量值。养成习惯方法参数尽量不做非null断言而是记录日志后抛更明确的异常。下标越界ArrayIndexOutOfBoundsException常见于for循环的边界条件i arr.length是要命的写法。还有一个隐蔽点split结果是String[]如果末尾有空串length会比你预期的大。并发修改ConcurrentModificationException在遍历集合时直接增删元素上面已经提过。用迭代器的remove或者用removeIf。类转换异常ClassCastException几乎都是向下转型不当造成的。转型前做好instanceof检查别依赖运气。栈溢出StackOverflowError最典型的触发点是递归没有正确的基线条件。排查时看堆栈里反复出现的那个方法十有八九是它自己无限调用了自己。7.2 一条简洁有效的排查思路有效排查不是靠一遍遍读代码而是靠日志和断点。真实推荐的排查路径是看异常名与消息。大部分时候异常信息已经把线索说清楚了。看堆栈第一行。第一行指向的是异常抛出的具体文件行号九成问题就藏在这里。找到那行代码问自己这行里有哪些变量可能为null、可能越界、类型可能不符如果还是没头绪加日志或调试器断点逐步看状态变化。经验之谈新手常常一遍遍读代码找bug效率极低。我建议先怀疑简单原因——空指针、边界条件、对象状态未初始化再往复杂方向想。绝大多数异常的基础原因都是很简单的逻辑疏漏。8. 写在最后的个人心得这次把Java基础语法的下半部分整理了一遍类与对象、封装继承多态、抽象类与接口、异常处理、集合框架、泛型、Lambda与Stream流。这些知识点相互关联环环相扣就像一个体系的七根柱子缺一根面向对象的大厦都立不稳。我个人的体会是学Java基础语法最容易犯的错误是“看三遍书、一行不写”。语法是工具不是信条光看永远体会不到空指针的无奈、覆写不起作用的诡异、泛型通配符的绕脑。建议每位学习者给自己定一个不可商量的规则每学完一个主题就亲手敲一个几十行的小Demo再故意制造几个错误观察报错信息。还有一点初学阶段不必对着某一个概念死磕到底。比如深入理解抽象类和接口在设计模式里的差异需要实际写过几个项目才能融会贯通。先能写出能跑的代码再在业务复杂度的推动下回头精雕概念这是比较高效的学习路径。对Java而言理论很有价值但真正的回报从你第一次用继承改造重复代码那一刻开始。