
1. 第3、4天课程在Java学习路线中的真正定位很多人把Java学习当成语法堆砌今天学个变量、明天学个循环两周之后发现全都对不上号。实际上day3和day4在整个人Java入门路线中属于一个非常关键的分水岭语法基础已经铺完开始进入用Java的方式思考问题的阶段。这个过渡做得好不好直接决定你后面学集合、IO、多线程、框架时是越学越顺还是越学越懵。day3、day4的内容通常在各大教程和课程中被安排为面向对象编程入门和常用核心API的初探。我不是培训机构的人但我看过很多自学者的学习记录也从自己带新人、改代码的经历出发想把这部分真正重要的东西掰开揉碎了讲一遍。你会发现这两天的内容看上去只是类对象String数组排序这些零散知识点但它们实际上共同构成了一套完整的编程思维底座——你后面写的每一行业务代码、每一个设计模式、每一道面试题都绕不开这里。换句话说day1、day2教你的是Java长什么样day3、day4教的是Java怎么想问题。如果你只是记住了语法没转过弯来那后面的路会特别难受。这篇文章不打算按课程PPT复述知识点我想以课程笔记之外的角度把这几天真正值得反复琢磨的内容按照一个老程序员的思路重新梳理一遍。顺便说一句光看我的文章是不够的你需要打开IDEA或者Eclipse把文中的代码敲一遍改一改参数看一眼输出结果那才算真正过了一遍。2. 从变量和循环到类与对象为什么很多人在这个节点放弃2.1 面向对象不是语法是一种分配职责的思维方式我先说一个直观的感受几乎所有零基础学员第一次接触类和对象时都会觉得这有什么实际意义。写出一个Student类里面放几个字段和方法然后new一个对象出来调用一下看起来比直接用变量存数据麻烦得多。但问题在于编程的复杂度一上来变量堆的方案就崩了。举个例子如果你要管理50个学生的信息用基本类型你会写String stu1Name到String stu50Name吗显然不会你会用数组或集合。但如果学生不仅有姓名还有学号、成绩、选课列表并且不同角色的操作不同学生可以选课、老师可以录入成绩、管理员可以调整班级所有的数据和操作全部混在一起几周之后你自己都看不懂代码。面向对象做的事情本质上是把数据和操作数据的方法绑定在一起并且为不同角色分配不同的职责。Student类管学生的属性、Teacher类管成绩录入和课程管理、Admin类管班级调度这就是一种模拟现实分工的方式。你不需要在调用方侧写一大堆if判断来处理各种学生操作你只需要拿到一个对象调用对应的方法即可。2.2 用一个案例说清楚类、对象、构造器和重载我在课程中见过太多人会背定义但不会写代码。为了让面向对象这几个核心概念有一根主线串起来我建议所有初学者都亲手写一个课程表系统的小案例。麻雀虽小五脏俱全类、对象、构造器、重载都能覆盖到。我给出一个极简版本你可以在自己的电脑上敲一遍public class Course { private String name; // 课程名 private int credit; // 学分 private String teacher; // 授课教师 // 无参构造器 public Course() { this.name 未命名课程; this.credit 0; this.teacher 待定; } // 带部分参数的构造器重载1 public Course(String name, int credit) { this.name name; this.credit credit; this.teacher 待定; } // 带全部参数的构造器重载2 public Course(String name, int credit, String teacher) { this.name name; this.credit credit; this.teacher teacher; } public String getInfo() { return 课程: name , 学分: credit , 教师: teacher; } }这段代码里你至少能看到三个必须搞懂的东西private字段把数据藏起来外部不能直接改只能通过方法来访问或修改。这叫封装是为了防止数据被任意改成非法状态。比如学分的值如果被改成负数整个逻辑就乱了所以一般还要在setter里加校验。构造器重载Course()、Course(String, int)、Course(String, int, String)本质上是提供给使用方的三种初始化配方。你可以在只知道部分信息时先创建一个对象之后再逐步补全。this关键字它指代当前正在创建或调用方法的那个对象用于区分成员变量和参数变量。然后你在主方法里这样用public class Main { public static void main(String[] args) { Course c1 new Course(); Course c2 new Course(Java程序设计, 3); Course c3 new Course(数据结构, 4, 张老师); System.out.println(c1.getInfo()); System.out.println(c2.getInfo()); System.out.println(c3.getInfo()); } }运行之后你会看到三条输出记录。从代码层面说你已经把类、对象、构造器、重载、封装全用了一遍。这里请务必亲自动手不要偷懒只看文章。如果条件允许试着给Course类加一个setCredit方法并在里面判断学分是否小于0小于0就抛出异常——这一步会让你对封装的意义有非常具体的感知。2.3 继承和多态在初学阶段应掌握到什么程度day3、day4的课纲里继承和多态一般只是初识不需要你立刻写一个复杂的继承体系。但我发现大家容易走向两个极端要么完全没概念要么试图过度设计一个层级很深的对象模型。我的建议是初学阶段你只需要掌握三句话继承是is-a关系。Student是一种PersonTeacher也是一种Person。子类自动拥有父类的非私有成员但可以覆盖父类的方法。多态的核心是父类型引用指向子类对象。声明类型和实际类型不必完全一致调用方法时执行的是实际类型那一版。Override注解帮你检查覆盖是否真的成功。方法签名写错了编译器会提示避免你以为覆盖了结果根本没覆盖。我见过很多人在初学阶段纠结为什么要用父类引用指向子类对象直接用子类型不好吗。原因是统一处理同类事物。假设你有一个Person数组里面既放了Student又放了Teacher你不需要instanceof判断直接调用printRole()方法每个对象自己知道该输出什么。class Person { public void printRole() { System.out.println(我是普通人); } } class Student extends Person { Override public void printRole() { System.out.println(我是学生我的天职是学习); } } class Teacher extends Person { Override public void printRole() { System.out.println(我是老师我的天职是教书育人); } }主方法写一个循环Person[] people {new Student(), new Teacher(), new Person()}; for (Person p : people) { p.printRole(); }你不需要写一堆if去判断每个对象是谁这就是多态带来的代码可扩展性。以后新增一个Doctor类你只需要继承Person并覆盖方法即可老代码一行都不用改。这个思想在后端框架里会被用到极致比如Spring里处理不同类型请求的分发底层本质上就是这个思路。3. 几天内必须吃透的几个核心APIString、StringBuilder和包装类3.1 String的不可变性你每天都在创建字符串但可能没意识到几乎所有的day3、day4课程都会讲String但大多数教程讲得太浅只给一个字符串是不可变的概念就结束了。我来说说这个不可变性到底意味着什么以及它会在新手的代码里埋下什么雷。String对象一旦创建它的字符序列就固定了。你执行s s abc时Java并不是在原来的字符串后面追加内容而是新建了一个String对象然后把引用s指向新对象。原来的那个字符串对象如果没有其他地方引用就会被垃圾回收。所以在循环体里做字符串拼接是性能大忌。下面这段代码是很多新手写过的String result ; for (int i 0; i 10000; i) { result result i ,; }这段代码在循环中创建了成千上万个中间字符串对象既耗时又浪费内存。如果面试官让你优化标准答案就是改用StringBuilder。3.2 StringBuilder与StringBuffer选型不是随便选的StringBuilder和StringBuffer的API几乎一样核心区别是StringBuffer的方法是同步的线程安全StringBuilder不是。在单线程环境下StringBuilder性能更好因为它不需要加锁开销。我建议日常开发无脑选StringBuilder只有在明确多线程共享同一个可变字符串对象时才考虑StringBuffer。使用方式非常简单StringBuilder sb new StringBuilder(); sb.append(Java).append(程序设计).append(day3); String result sb.toString();append方法支持链式调用内部维护一个可变字符数组容量不够时自动扩容。这也是面试里喜欢问的一点StringBuilder默认初始容量是16超过后会按照原容量*22的规则扩容不同JDK版本可能略有差异。你可以用new StringBuilder(100)显式指定初始容量避免频繁扩容。3.3 包装类、自动装箱与空指针隐患Java是面向对象语言但int、double这些基本类型不是对象。为了让它们也能被放进容器、参与泛型就有了包装类Integer、Double、Boolean等。你平时写的Integer n 10;其实是编译器帮你做了Integer.valueOf(10)这叫自动装箱int x n;其实是调用了n.intValue()这叫自动拆箱。这套机制用起来很爽但有一个经典坑包装类对象可能为null拆箱时会抛NullPointerException。Integer count null; int total count 1; // 这里会抛空指针还有一种情况是Integer的缓存问题。Integer a 127; Integer b 127; a b结果是true但Integer a 128; Integer b 128; a b结果是false。原因在于Integer默认缓存了-128~127之间的对象在这个范围内valueOf返回的是同一个缓存对象超出范围就会新建对象。所以比较包装类数值永远用equals()不要用。4. 容器初探ArrayList和HashMap是day4最值得深挖的两个类4.1 ArrayList一个动态数组的自我修养Java的集合框架庞大繁杂但day3、day4阶段你不需要全面铺开。说实话先把ArrayList和HashMap彻底搞明白后面学LinkedList、HashSet、TreeMap都会快很多。ArrayList的底层就是数组。它之所以能动态扩容是因为当数组容量不够时它会新建一个更大的数组然后把老数据System.arraycopy到新数组中。默认扩容因子是1.5倍即新容量旧容量旧容量右移1位所以如果你大概知道数据量级别最好在创建时就指定初始容量ArrayListString list new ArrayList(10000);频繁扩容会复制数组数据量大时耗时明显。你可以在实际项目中用jvisualvm或者JProfiler看一眼分配情况会相当直观。ArrayList的常用操作就那几个add、get、set、remove、contains、size。但要注意contains、remove(Object)底层都是遍历数组然后equals时间复杂度是O(n)。如果数据量很大且频繁做包含判断应该考虑用HashSet而不是ArrayList。还有一个细节remove(int index)和remove(Object o)是重载方法。如果你有一个ArrayListInteger想删除值为2的元素绝不能写list.remove(2)——这会按索引删除第2个位置的元素。正确的写法是list.remove(Integer.valueOf(2))。4.2 HashMap为什么hashCode()和equals()必须一起重写HashMap可以说是Java面试的顶流话题但day4阶段你只需要建立三个基本认知。第一HashMap的存储结构是数组链表红黑树。每个数组元素叫Nodekey经过hash后定位到数组下标。如果多个key哈希到同一个下标就形成链表当链表长度超过阈值默认8且数组容量达到64时链表会转成红黑树把查询复杂度从O(n)降到O(log n)。第二HashMap通过key的hashCode()决定存储位置通过equals()判断两个key是否相等。如果你用自定义对象作为key比如Person但只重写了hashCode()而没重写equals()或者反过来了就会出现明明看起来是同一个对象却取不到值的诡异问题。我提供一个极简的规范写法public class Person { private String id; private String name; Override public boolean equals(Object o) { if (this o) return true; if (!(o instanceof Person)) return false; Person person (Person) o; return Objects.equals(id, person.id) Objects.equals(name, person.name); } Override public int hashCode() { return Objects.hash(id, name); } }第三初始容量和负载因子。HashMap的默认初始容量是16默认负载因子是0.75。意思是当元素数量超过容量 * 0.75时就会触发扩容。扩容需要重新计算所有key的位置非常耗时。如果你能预估数据量比如要存1000条数据最好用new HashMap(1000 / 0.75 1)或者直接new HashMap(1340)减少扩容次数。4.3 遍历Map的两种姿势Map的遍历方式很多但对初学者我推荐两种最不容易出错的方式一entrySet()HashMapString, Integer map new HashMap(); for (Map.EntryString, Integer entry : map.entrySet()) { System.out.println(entry.getKey() : entry.getValue()); }方式二forEachJava 8以后map.forEach((key, value) - System.out.println(key : value));这两种写法都能拿到key和value。我不建议在初学阶段用keySet()再get(key)原因是多了一次查询而且看起来也啰嗦。养成良好的遍历习惯后面写业务代码会顺手很多。5. 排序和数组操作从背代码到真正理解冒泡、Comparator与Arrays工具类5.1 冒泡排序你不该只会背循环要能看到交换的边界热搜词里冒泡排序java赫然在列可见这是所有Java学习者绕不过去的一道坎。很多培训课程喜欢让学员背冒泡循环但我要说的是排序算法是思维训练不是背诵内容。冒泡排序的思想是每一轮把最大的元素像气泡一样浮到数组末端。外层循环控制轮数内层循环控制相邻比较交换复杂度是O(n²)。但如果你真理解了内层循环的边界为什么是j arr.length - 1 - i而不是j arr.length - 1你就已经超过了大部分背代码选手。public static void bubbleSort(int[] arr) { int n arr.length; for (int i 0; i n - 1; i) { boolean swapped false; // 优化如果某一轮没有交换说明已经有序 for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; swapped true; } } if (!swapped) { break; } } }这里的- i是有意义的每一轮结束数组末尾都多了一个已经就位的大元素下一轮就不用再管它。而swapped标志是经典的优化手段如果一个有序数组传入循环一轮就能提前结束时间复杂度降到O(n)。这个细节在面试中很加分也让你真正理解算法不是死代码。5.2 Arrays工具类开发中最常用的排序和查找方式实际开发中你不会自己写排序。java.util.Arrays提供了一堆静态方法你只需要会用。int[] arr {5, 3, 8, 1, 9}; Arrays.sort(arr); // 对整个数组升序排序 System.out.println(Arrays.toString(arr)); int index Arrays.binarySearch(arr, 5); // 二分查找前提是数组已有序 System.out.println(5的位置是: index);Arrays.sort对基本类型数组使用双轴快速排序对对象数组使用TimSort归并插入的组合稳定且高效。你背一百行冒泡排序的实际收益率远不如把Arrays.sort的用法和原理吃透。但面试或考试中手写排序依然可能被问到所以我建议你把归并排序和快速排序各手写一遍而不是把冒泡当成唯一技能。5.3 Comparable与Comparator让对象也能排序如果你有一个Student数组或ListStudent想按年龄排序就不是Arrays.sort直接能搞定的事了。你需要告诉Java按什么规则排序这就引出了Comparable和Comparator。Comparable是类自己知道自己怎么比实现它的类要写一个compareTo方法public class Student implements ComparableStudent { private String name; private int age; Override public int compareTo(Student other) { return Integer.compare(this.age, other.age); // 按年龄升序 } }然后直接Arrays.sort(students);Comparator是外部定义一个比较器你不想改Student类本身时用这个Arrays.sort(students, new ComparatorStudent() { Override public int compare(Student s1, Student s2) { return s1.getName().compareTo(s2.getName()); } });Java 8以后可以写成Lambda表达式Arrays.sort(students, (s1, s2) - s1.getName().compareTo(s2.getName()));你会发现这玩意儿比Comparable灵活得多一个类可以同时有多种排序方式按年龄、按姓名、按成绩只需要定义多个Comparator即可。这是day4最实用、最贴业务场景的知识点没有之一。以后写Web项目你很可能需要实现查询结果按时间倒序排列核心就是Comparator的那点事。6. day3、day4之后必须补的几个底子环境变量、编译执行和常见报错6.1 JDK安装与环境变量配置其实没你想的那么可怕java环境变量配置是热搜里的高频词。很多新手在第一步就被吓住了其实核心就两个变量。JAVA_HOME指向JDK安装目录比如C:\Program Files\Java\jdk-17方便其他工具Maven、Tomcat等找到JDK。PATH追加%JAVA_HOME%\bin让命令行可以直接使用java和javac命令。配置完成后在命令行运行java -version能输出版本号就算成功了。我遇到过很多人配置完还是报不是内部或外部命令绝大多数是配置完没有重新打开命令行窗口或者JAVA_HOME路径写成了bin目录里面。路径应该写到JDK的根目录不是bin目录。6.2 源发行版17需要目标发行版17这类报错是怎么回事热搜词里的java: 警告: 源发行版 17 需要目标发行版 17是IntelliJ IDEA里非常常见的编译警告。说白了就是项目配置的Java语言级别是17但你的JDK版本不是17或者模块的target版本设置不一致。解决方案一般是在Project Structure里统一三处配置Project标签下的SDK和Language levelModules标签下每个模块的Language levelSettings - Build - Compiler - Java Compiler的Per-module bytecode version我见过很多初级开发者被这个报错卡住几小时其实原因就是三处版本不一致。只要你把项目结构里的SDK、语言级别、字节码版本全部对齐这类报错基本消失。如果你用的是JDK 8但项目语言级别是17代码里用了var、switch表达式这些新语法也会编译失败。此时建议要么升级JDK要么把语言级别降回8。6.3 从看教程到能debug这是质的飞跃day3、day4读完你应该学会一个工具调试器Debugger。很多人只看打印语句System.out.println定位问题这也能用但效率太低。在IDEA里你只需要在行号左侧点击一下设置断点然后按Debug按钮运行。程序会在断点处暂停你可以按F7Step Into进入方法内部按F8Step Over一行一行执行当前方法按F9Resume跳到下一个断点在Variables窗口查看当前所有变量的值调试器给你的信息量远超打印语句你能看到方法调用栈、每个变量的实时状态、异常发生的确切位置。我强烈建议在学完day4之后专门拿出半天时间把前面写的所有代码全部用Debug方式跑一遍每个断点都加一加、每步都走一走。这一步做完你对代码执行过程的理解会突飞猛进。7. 这几天的学习避坑清单个人经验向写到这儿我不想再来一次总结只把我见过的、自己踩过的几个典型坑列出来全部是真实教训。第一不要为了记笔记而记笔记。很多人的笔记本上抄满了String方法的用法但实际上一年也未必用到substring的某个冷门重载。真正应该记的是你运行代码时遇到的报错、排查的思路、以及一个知识点能用在什么业务场景里。比如为什么循环拼接字符串慢用StringBuilder快这类有对比、有场景的记录才有价值。第二尽早养成看文档的习惯。Oracle官方Java文档虽然全英文但语法并不复杂。学会查String、ArrayList、HashMap的文档说明比在任何博客上找答案都靠谱。中文博客容易过时而官方文档永远跟JDK版本同步。第三写代码时别急着一下写完。我的习惯是先写伪代码再翻译成Java。举个例子你想实现统计一个字符串中每个字符出现的次数1. 创建HashMapkey是字符value是次数 2. 遍历字符串的每个字符 3. 如果map里有这个字符次数1 4. 如果没有put进去次数1 5. 遍历map输出伪代码写完翻译Java就是水到渠成的事。初学者最容易犯的错误是一上来就写HashMapCharacter, Integer map new HashMap()然后在循环里卡壳——因为你还没想清楚整体流程。先动脑再动手这不是空话是真能救你命的习惯。第四保持代码整洁。初学阶段的代码短没人会注意缩进和命名但到了day4开始写类、写多个方法混乱的格式会极大提高理解成本。我要求自己所有变量名都表达含义flag这种名字能不用就不用方法名尽量以动词开头。这个习惯从学Java的第一天就该养成因为它比任何奇技淫巧都影响你后面的成长速度。第五面向报错编程。遇到异常别慌先把异常信息从下往上读完绝大多数问题出在Caused by那一段。初级阶段的报错基本就那几类空指针、数组越界、类型不匹配、编译错误。每解决一个就顺手记下来积累一个月之后你就是周围人眼里的报错杀手。如果你正在自学Java我的建议是把这一篇和你的课程对照着看然后动手把示例代码敲一遍、改一遍、debug一遍。day3、day4的内容不算多但它是一个台阶。跨过去后面的Java世界会特别开阔跨不过去你大概率会在集合和框架那里卡很久。编程学习没有捷径但一定有更聪明的路径——找对重点死磕到底你就比大多数同行者快了一步。