
最近后台收到不少私信问的全是同一个问题“想学 Java到底从哪里开始为什么我照着教程敲代码还是一堆报错” 我太懂这种感觉了当年第一次配环境变量的时候照着网上教程一步步弄结果java命令还是提示“不是内部或外部命令”折腾了一晚上最后发现是 Path 里少了\bin。为了不让新手在起步阶段就被这种破事劝退我决定开一个“Java基础笔记”系列把基础但最容易卡人的内容按自己的理解整理出来。第一篇就先从装环境、基础语法、数组排序、面向对象、集合入门和常见异常排查开始。这篇笔记适合两种人一种是完全零基础、准备认真学 Java 的入门者另一种是学了一段时间但基础不牢、想回来查漏补缺的人。我会把每个知识点背后“为什么要这么做”也讲清楚而不是只给结论。1. 装环境这一关JDK版本、环境变量配置的完整避坑指南1.1 先搞懂JDK、JRE和JVM再来选版本很多新人看到 JDK、JRE、JVM 这三个缩写第一反应是“这仨是不是同一个东西”其实不是。简单来说JVMJava Virtual Machine是整个 Java 跨平台特性的根子它负责把编译好的 .class 字节码翻译成当前操作系统能执行的指令。不同操作系统有各自的 JVM 实现但它们读的是同一套字节码这才有了“一次编写到处运行”。JREJava Runtime Environment是 Java 运行时的最小环境里面包含 JVM 和 Java 自带的核心类库。只是运行别人写好的 Java 程序装 JRE 就够了。JDKJava Development Kit是给开发者用的完整工具包它包含了 JRE 的全部内容额外提供了 javac 编译器、jar 工具、javadoc 文档工具等。要自己写代码、编译代码就必须装 JDK。用生活里的东西来类比JVM 像发动机JRE 是一台能开走的整车JDK 则是带全套维修工具的造车工厂。接下来是版本选择这也是新手问得最多的问题之一。网上教程一半用的是 JDK 8即 1.8另一半用的是 JDK 17 或 21。我的建议是如果完全没接触过 Java优先选 JDK 17。原因有三第一17 是长期支持版本LTS企业里用得越来越多学完去工作衔接更顺第二JDK 8 是老版本继续学迟早面临升级问题第三JDK 9 之后引入模块化系统很多老教程里的配置方式已经过时不如直接学新版本。但这里有一个现实矛盾不少学校教材和培训课程仍然以 JDK 8 为主。如果你是为了跟课堂进度或者应付考试就老老实实按课程要求来装别擅自用新版本否则环境不一致会出现各种奇怪的编译报错。环境版本不是越新越好而是以你的学习资料为准。1.2 环境变量三步配置JAVA_HOME、PATH、CLASSPATH怎么填安装 JDK 时尽量别装在带空格的路径下比如 Windows 默认的C:\Program Files\Java\jdk-17虽然能用但我个人更建议装到D:\Java\jdk-17这种路径短、没有空格的位置。后面配置环境变量、写脚本调 Java 命令时短路径能少踩很多坑。装完后配置环境变量这一步是初学者搜索“java环境变量配置”找教程最多的地方。我按 Windows 系统的完整流程讲一遍右键“此电脑” → 属性 → 高级系统设置 → 环境变量。在“系统变量”区域点击“新建”变量名填JAVA_HOME变量值填 JDK 安装路径比如D:\Java\jdk-17。找到系统变量中的Path点击编辑。Windows 10/11 会打开一个列表式窗口点击“新建”分别加入%JAVA_HOME%\bin和%JAVA_HOME%\jre\bin。如果是老版本 Windows需要在原值末尾用英文分号隔开再追加。新建CLASSPATH。这一步很多老教程都写但说句实话从 JDK 1.6 之后 CLASSPATH 已经不需要手动配了。我见过太多新手因为在 CLASSPATH 里写错一个点号或分号导致类加载异常排查半天。我的建议是JDK 8 及以上直接跳过 CLASSPATH 配置编译器会默认在当前目录查找类文件。配置完成后打开一个新的命令行窗口验证。注意一定要新开窗口旧窗口不会刷新环境变量。如果你执行java -version提示“java 不是内部或外部命令”大概率是 Path 配置没生效或者拼写有误。先确认JAVA_HOME的值是否就是 JDK 实际安装目录再确认Path里的%JAVA_HOME%\bin是否真的写进去了。一个小技巧在命令行输入echo %JAVA_HOME%如果返回的是空说明环境变量没有加载到需要检查变量名拼写。1.3 配置完怎么验证环境没问题环境搭好之后只看java -version能输出版本还不够我建议做一个完整的编译运行测试。随便找个目录新建一个Hello.java文件public class Hello { public static void main(String[] args) { System.out.println(Hello, Java!); } }然后在命令行里依次执行javac Hello.java和java Hello如果输出Hello, Java!说明整套环境闭环没问题。这里有一个新手普遍卡住的问题运行java Hello报“找不到或无法加载主类”。网上很多回答说是 CLASSPATH 的问题实际上 90% 的情况是命令行当前目录不对或者文件名和类名不一致。Java 对公共类名字的要求非常严格公共类的名字必须和文件名完全一致包括大小写。文件名是Hello.java公共类就必须是Hello运行java Hello时命令行必须停留在文件所在目录。我还遇到过另一种情况电脑上装过多个版本 JDK命令行调用的java是旧版的而javac是新版的编译和运行环境不一致会出现各类奇怪的报错。验证时可以执行where java查看系统到底调用了哪个路径下的java.exe确保它指向你刚配置的 JDK 目录。提示环境变量配置完务必新开命令行窗口再验证当前窗口读到的还是旧的环境变量缓存。2. 语法地基标识符、基础数据类型、运算符的易错点2.1 标识符命名规则代码质量的第一道关口进入写代码阶段第一个要记住的是标识符的命名规则。标识符就是你在代码里自己起的名字比如类名、方法名、变量名。硬性规则有四条只能由字母、数字、下划线_和美元符号$组成不能包含空格、连字符。不能以数字开头。不能是 Java 的关键字或保留字比如class、if、for、while、new都不能作为标识符。Java 严格区分大小写Name和name是两个完全不同的标识符。软性规范虽然没有强制性但实际开发中比硬性规则还重要类名使用大驼峰命名法例如UserService、OrderController每个单词首字母大写。方法名和变量名使用小驼峰命名法例如getUserName、orderCount。常量使用全大写加下划线例如MAX_SIZE、DEFAULT_TIMEOUT。我记得有一次代码评审看到有人用拼音缩写给变量命名比如int xjsfje 10没人知道这是啥意思。后来我们组立了规矩变量名至少要能“望文生义”哪怕用长一点的名字也没问题。代码是写给人看的然后才是给机器执行的。2.2 基本数据类型和运算符优先级写错一次就知道疼Java 里有 8 种基本数据类型按内存大小从低到高排是这样类型占用空间取值范围/特点byte1字节-128 到 127short2字节-32768 到 32767int4字节约 -21亿 到 21亿long8字节赋值时建议加L后缀float4字节赋值时必须加F后缀double8字节默认的小数字面量类型char2字节单个字符boolean不定只有 true 和 false新手最容易忽略的是默认值。比如int[] arr new int[5];数组里每个元素的默认值是 0而引用类型数组的默认值是null。很多 bug 都是因为没搞清楚默认值导致的。运算符这块我建议把自增自减和短路运算符单独拎出来掌握int a 5; int b a; // b5a6先赋值后自增 int c 5; int d c; // d6c6先自增后赋值这个考点在面试里的出现频率极高。另外短路运算符和||有个容易被忽略的特性如果左边的表达式已经能确定整个表达式的结果右边的表达式根本不会执行。如果你在右边写了一个带副作用的表达式比如if (x 0 || y 1)当x 0成立时y永远不会执行这个细节容易引发隐藏bug。2.3 字符串引用类型里最特殊的存在字符串String在 Java 里是引用类型不是基本类型但它的使用频率高到必须单独掌握几个关键知识点。第一个是不可变性。String 对象一旦创建内容就不能更改。你写的str abc底层并不是在修改原字符串而是新创建了一个 String 对象再把引用指向新对象。如果在一个循环里反复做字符串拼接会产生大量临时对象内存开销非常大。高频拼接应该用StringBuilder或StringBuffer。第二个是字符串常量池。直接用双引号创建的字符串会先检查常量池里有没有相同内容有就直接复用而通过new String(abc)创建的对象在堆中新建即使内容相同也不复用。这就是为什么String s1 abc; String s2 abc; System.out.println(s1 s2); // true常量池复用 String s3 new String(abc); System.out.println(s1 s3); // false堆中新对象面试里这个经典题的出镜率极高但只背结果不够要理解背后的设计原因这种设计是为了减少字符串对象的重复创建降低内存占用。第三个是文本块Text Block。从 Java 13 开始正式支持多行字符串写法用三个双引号包裹很适合写 JSON、SQL、HTML 这类长文本String json { name: 张三, age: 25 } ; System.out.println(json);这个特性在 JDK 8 里是不存在的如果你还在用旧版本编译会直接报错。这也是我前面建议新学者直接用新版本的一个实际原因。3. 从数组到排序算法基础里的硬核实战3.1 数组声明、初始化和边界问题数组是 Java 里最基础的数据容器也是算法题的常客。声明方式有两种写法int[] arr和int arr[]前者更符合阅读习惯推荐使用。数组一旦创建长度就固定了不能动态扩展。初始化分为动态初始化和静态初始化// 动态初始化先指定长度再逐个赋值 int[] arr1 new int[5]; arr1[0] 10; // 静态初始化创建时直接给元素 int[] arr2 {1, 2, 3, 4, 5}; int[] arr3 new int[]{1, 2, 3, 4, 5};这里有个细节int[] arr new int[3]{1,2,3};是编译错误不能同时指定长度又直接赋元素。遍历数组最常用的是增强 for 循环for (int x : arr2) { System.out.println(x); }但增强 for 循环拿不到当前下标。如果业务逻辑需要下标比如修改某个位置的元素还是要用普通 for 循环。说到边界问题数组下标范围是 0 到length-1访问arr[length]就会抛出ArrayIndexOutOfBoundsException。这是初学者最容易碰到的异常之一排查思路也很直接打印数组的length再打印你访问的下标对比一下就能定位问题。3.2 冒泡排序面试手写代码的保底技能冒泡排序为什么叫“冒泡”因为每一轮都会把当前未排序部分的最大值“冒泡”到最右边像气泡上浮一样。它虽然时间复杂度是 O(n²)不算高效但实现简单面试也经常考。基本版实现public static void bubbleSort(int[] arr) { int n arr.length; for (int i 0; i n - 1; i) { 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; } } } }注意内层循环的终止条件是n - 1 - i每循环一轮最后 i 个元素已经排好序不需要再比较。进阶一点可以加入一个swapped标志位如果某一轮里一次交换都没发生说明数组已经完全有序可以提前终止。这个优化在数据接近有序时效果很明显面试时能写出优化版会给面试官留下好印象。3.3 快速排序算法题的进阶敲门砖快速排序的平均时间复杂度是 O(n log n)核心思想是“分治”。选定一个基准值pivot把小于基准的放左边大于基准的放右边然后对左右两个分区递归执行同样操作。一个清晰简单的实现public static void quickSort(int[] arr, int left, int right) { if (left right) { return; } int pivot arr[left]; int i left; int j right; while (i j) { while (i j arr[j] pivot) { j--; } arr[i] arr[j]; while (i j arr[i] pivot) { i; } arr[j] arr[i]; } arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }这个实现用的是挖坑法选左边界做基准值。关键点在于两个内层 while 循环的条件里都带着i j漏掉就可能导致下标越界或死循环。快速排序的难点就在边界条件的把握新手第一次写的时候出错非常正常。我建议不要只看代码拿一个只有 6 个元素的小数组在纸上手动模拟一遍把每一步的 i、j 指针移动和元素覆盖都画出来。跑通一遍之后你对这个算法的理解会比看十遍视频都深刻。4. 面向对象从代码量到思维模式的关键转变4.1 类与对象图纸和实物的关系“万事万物皆对象”这句话听起来很玄其实类比一下就明白了。类就像一张设计图纸定义了对象有哪些属性字段和行为方法对象就是根据图纸生产出来的实际物品。图纸本身不能住人但按图纸盖出来的房子可以。public class Student { String name; int age; void study() { System.out.println(name 正在学习 Java); } } public class Main { public static void main(String[] args) { Student s new Student(); s.name 小明; s.age 18; s.study(); } }new Student()就是在创建对象s是对象的引用。很多人初期会混淆“对象本身”和“对象的引用”引用相当于遥控器对象相当于电视机你操作遥控器就是在操作电视。4.2 构造器、this与参数传递的秘密构造器是在创建对象时自动调用的特殊方法名字必须和类名一致不能写返回类型。如果不写构造器Java 会提供一个默认的无参构造器但一旦你写了自己的构造器默认构造器就消失了除非你显式写出来。public class Student { String name; public Student() { this(无名); } public Student(String name) { this.name name; } }上面代码里this(无名)是构造器之间的调用this.name name用来区分成员变量和局部变量。当一个构造器调用另一个构造器时this()必须放在第一行。关于参数传递Java 是纯值传递。基本数据类型传的是值的副本引用类型传的是引用的副本。所以方法外的一个引用变量传给方法后如果在方法内部重新赋值一个新的对象外面的引用不会变但如果方法内部修改了传入对象的字段外部看到的对象就变了。这也是一个高频面试题。4.3 封装不是“私有化”这么简单封装的直观操作是把字段设为private对外提供公开的 getter/setter。但封装的意义远不止可见性控制它是“隐藏实现细节”这套设计思想的基础。举例来说学生年龄字段如果直接公开外部代码可以写成s.age -1这明显不合逻辑。但通过 setter 方法可以在赋值前做校验public class Student { private int age; public void setAge(int age) { if (age 0 || age 150) { throw new IllegalArgumentException(年龄不合法); } this.age age; } }这就是“把数据和操作数据的方法绑定在一起”对外只暴露合理、安全的操作入口。以后学设计模式、架构设计时很多思想的源头都是这里。5. 集合框架入门数组之后的第一个实用武器5.1 为什么学完数组必须立刻学集合数组长度固定增删元素非常麻烦这是它在真实业务里不常用的主要原因。Java 集合框架Java Collections Framework就是为了解决这类问题出现的集合提供了一组可以动态扩展、自动收缩的数据容器并内置了常用的查找、去重、排序等功能。打个比方数组像一个固定座位的教室人数超过上限就只能换地方集合像一个能自动扩容的会议室人少了也不浪费太多空间人多了还能自动加椅子。5.2 List、Set、Map 三大接口初体验集合框架最常用的三大接口是 List、Set、Map。List是有序可重复的列表常用实现是ArrayList和LinkedList。ArrayList底层是动态数组查询快、增删慢因为增删涉及元素移动LinkedList底层是双向链表增删快、查询慢因为要从头遍历。日常开发里 95% 的场景直接用ArrayList就够了。Set是无序不可重复的集合常用实现是HashSet和TreeSet。HashSet基于哈希表存取效率高TreeSet基于红黑树元素会保持排序状态。往HashSet放自定义对象时必须正确重写hashCode()和equals()否则可能出现“内容相同但被当成两个对象”的问题。Map是键值对结构最常用的是HashMap。相比早期的HashtableHashMap允许null键和null值。JDK 8 之后HashMap在某个哈希桶的元素数量超过阈值时会把链表树化成红黑树将最坏情况下的查询时间复杂度从 O(n) 改善到 O(log n)。5.3 遍历集合的正确姿势List 遍历可以用普通 for、增强 for、Iterator 迭代器。Iterator 最灵活可以在遍历过程中安全地删除元素使用增强 for 在遍历时删除元素会抛出ConcurrentModificationException。ListString list new ArrayList(); list.add(Java); list.add(Python); // 增强 for只适合读取 for (String s : list) { System.out.println(s); } // Iterator适合一边遍历一边删除 IteratorString iterator list.iterator(); while (iterator.hasNext()) { String s iterator.next(); if (Python.equals(s)) { iterator.remove(); } }Map 遍历我用得最多的是entrySet()MapString, Integer map new HashMap(); map.put(Java, 1); map.put(Python, 2); for (Map.EntryString, Integer entry : map.entrySet()) { System.out.println(entry.getKey() : entry.getValue()); }JDK 8 以后也可以用 lambda 风格map.forEach((k, v) - System.out.println(k : v));lambda 在现在的 Java 开发里已经是默认技能所以“lambda函数 java”的搜索热度一直居高不下。不过这篇笔记不深入展开函数式编程你只需要知道forEach这种写法是怎么用的就够了。6. 新手最容易碰到的异常报错不可怕可怕的是不会看堆栈6.1 数组越界最基础的异常最经典的面试导入题ArrayIndexOutOfBoundsException的特点是非常直白报错信息会直接告诉你下标越界。比如Exception in thread main java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5这句话翻译过来是访问了下标 5但数组长度只有 5合法下标范围是 0 到 4。排查思路很简单先看这个数组怎么创建、length是多少再检查循环或下标计算逻辑。很多人犯这个错是因为循环里写了i arr.length而正确写法应该是i arr.length。6.2 OOMOutOfMemoryError 是怎么发生的JVM 的内存是有限制的。当你创建的对象太多、占用内存超过堆的最大限制时就会抛出OutOfMemoryError。比如无限往ArrayList里放数据或者死循环里不断new对象很快就会把内存撑爆。初学者遇到这个错先检查是不是创建了超大集合或者存在无限递归。等到学 JVM 调优之后可以在启动命令里调整堆大小java -Xmx1024m YourClass-Xmx1024m表示把最大堆内存设为 1024 兆。但第一课要记住的是OOM 是错误Error级别不是普通异常绝大多数情况说明程序本身出了问题不能靠简单的 try-catch 吞掉。6.3 NoClassDefFoundError 和 ClassNotFoundException看起来像本质不同这两个错误名字长得像但本质完全不同。ClassNotFoundException是运行时找不到类通常原因是依赖缺失或者某个 jar 包没加到 classpath 里。NoClassDefFoundError是类编译期存在但在运行期加载失败常见原因是某个类的静态初始化块抛了异常导致类初始化失败后续引用这个类时就会出现这个错误。举个例子高版本 JDK 里大量使用了模块化机制早期的一些类被移除了。如果你在较新的 JDK 上运行老项目就可能报类似java/applet/Applet找不到的错误。排查时要先看堆栈里到底加载哪个类失败再确认对应的 jar 包是否存在、版本是否匹配。6.4 几个搜索热度极高的业务异常速查除了上面三个基础异常还有几个高频搜索场景和 Java 环境密切相关这里用速查表方式整理一下报错场景常见原因排查方向RedisTemplate 的 increment() 报 not integer or out of rangeRedis 里同一个 key 的值被别的命令改成了非数字类型用TYPE key查看数据类型用GET确认值内容Lombok 报 not using a compiler supported by lombok新版本 JDK 发布后 Lombok 版本未适配升级 Lombok 版本或临时降 JDK 版本JSON 序列化时大写开头的 bean 字段变小写JavaBean 规范要求属性名小驼峰序列化工具会按规范处理尽量不用大写字母开头的字段名必要时用注解指定序列化名称找不到 sun.misc 相关类JDK 模块化后删除了部分内部 API升级代码改用官方公开 API这些异常看起来五花八门但排查思路都一样先读堆栈第一行定位是哪个类、哪一行代码出了问题再结合工具查数据状态。7. 关于学习路线的个人观察与下一步建议学 Java 很容易陷入“学完一个知识点又一个知识点但不知道怎么串起来”的迷惘。根据我带新人的经验比较顺的路线是第一阶段先把语言底子打牢语法基础、流程控制、数组、面向对象、常用集合、字符串、异常处理、IO 流、多线程基础。第二阶段学数据库包括 SQL 语法、JDBC 操作和简单的表设计。第三阶段进入 Web先学 Servlet 和 Tomcat理解一次 HTTP 请求从进入到返回的全过程。第四阶段再上框架Spring、Spring Boot、MyBatis用框架简化企业级开发。注意这个顺序不是说你每个阶段都要精通才能进下一阶段重点是“先跑通再加深”。很多人在第一阶段死磕细节一年了还在写控制台程序反而耽误了全局认知。更好做法是把基础部分过一遍尽快做一个小项目在项目里遇到的问题再回来补基础。这篇是“Java基础笔记”系列的第一篇后面我打算重点整理面向对象的三大特性——继承、多态、接口以及字符串和集合的源码级细节这些既是面试高频区也是日常开发必须扎实的部分。最后分享一条我自己最深的体会学 Java 基础不要只看视频或看书一定要动手敲代码更重要的是一定要养成读报错信息的习惯。很多新手一看到英文报错就慌其实每个报错信息都在精确地告诉你问题出在哪一行、是什么类型把报错读明白了比背十道面试题都有用。这一篇里的所有示例代码都建议亲手敲一遍改一改参数看看结果有什么变化报错是学习过程中最好的老师。