
本来这一篇是想直接接上一篇的内容继续写环境搭建之后的语法讲解但每次给新手讲JavaSE我发现大家最容易卡住的并不是“这个语法怎么写”而是“为什么这样写”“哪个环节理解错了会埋雷”。所以基础02我打算换个思路不只是罗列知识点而是按一条完整的学习主线来拆数据类型、运算符、流程控制、方法、数组每一块都会先讲“用来干嘛”再讲“怎么用”顺手把网上很少讲清楚的坑也一并填了。这一篇适合两类人。一类是刚装好JDK、写过HelloWorld但还没形成体系感的零基础学习者另一类是已经工作一两年、想回头把Java语法地基补扎实的开发者。如果你能把这篇里的内容都消化掉再去看面向对象、集合、IO会明显觉得轻松很多因为那些高级特性本质上都是在这一片语法土壤上长出来的。1. 先理清JavaSE的学习主线这一篇到底讲了什么1.1 为什么“基础02”要从语法规则讲起JavaSE系列如果按教学顺序排通常是环境搭建、基础语法、面向对象、常用API、集合、异常、IO、多线程、网络编程这样一条线。基础01是环境搭建和第一个HelloWorld让我们确认“代码能跑起来”。基础02就要解决下一个问题怎么让代码按照我们想要的逻辑去执行。有人可能会问为什么不直接跳去学面向对象因为面向对象里的类和对象本质上也是由变量、方法、控制流这些最基础的语法拼出来的。你连if和for都没掌握写出来的类也只是一个空壳子。就好比盖房子对象是户型设计语法是砖块水泥你不先把砖烧好户型图再漂亮也白搭。这一篇的核心知识范围包括五块变量与数据类型、运算符、流程控制、方法、数组。这五块共同构成一个Java程序的最小可用闭环——你能接收数据、对数据做运算、根据条件跳转分支、循环处理重复逻辑还能把一段逻辑封装成方法复用用数组批量管理同类型的数据。把这些串起来你就能写出真正意义上的“程序”而不只是hello world式的单行输出。1.2 JDK版本选择以及那个“下载账号”的坑先解决一个实战问题很多新手去Oracle官网下载JDK走到一半发现被要求登录Oracle账号没有账号就下不了直接卡住。这个“下载账号”问题几乎每个月都有人在技术群里问一遍。先说结论Oracle官网下载JDK确实需要注册一个Oracle账号注册是免费的用邮箱就能完成不会收钱。你只需要去Oracle官网点注册填一个常用的邮箱按邮件激活链接操作即可。这个账号只是用于下载授权不需要额外付费也不用担心绑定什么。如果实在不想注册也可以选择OpenJDK的官方构建或者Eclipse Adoptium项目下的Temurin发行版这两个都是免费开源、无需账号直接下载的。版本选择上我的建议是学习阶段直接用JDK 17或JDK 21二者都是长期支持版本前者生态最成熟后者是最新的LTS。不要一上来就追JDK 25之类的非LTS版本没必要为了新特性给自己找不痛快。JDK 8虽然还在大量老项目中使用但如果你是从零学习不建议再用它起步因为不少新语法特性比如后面要讲的switch增强在JDK 8里根本支持不了。下载安装完成之后在命令行里敲java -version和javac -version如果能正确输出版本号环境就算准备好了。这里额外提醒一句如果你的机器之前装过别的JDK版本多版本共存时一定要检查JAVA_HOME指向的是哪一个很多“编译报错但明明装了JDK”的诡异问题最后查出来都是环境变量指错了版本。2. 开发前的准备工作环境变量、IDE与命令行视角2.1 环境变量到底配了什么JAVA_HOME、PATH和CLASS_PATH环境变量这个话题教科书里讲得特别枯燥我尽量用大白话解释。为什么配了JAVA_HOME和PATH之后命令行的java和javac就能用因为操作系统在执行命令时会去PATH里记录的目录逐个查找。你把JDK的bin目录加进PATH系统才能找到java.exe和javac.exe这两个可执行文件。那JAVA_HOME是干嘛用的它不是给操作系统用的而是给后续部署的工具用的比如Maven、Gradle、Tomcat它们启动时要找到JDK的安装根目录就是靠解码JAVA_HOME这个变量。所以JAVA_HOME要指向JDK的根目录比如C:\Program Files\Java\jdk-17而PATH里要追加的是%JAVA_HOME%\bin这个细节千万别搞反。还有一个常见的CLASS_PATH概念这里多说一句。很多人问要不要配CLASS_PATH我的答案很干脆Java 9之前需要Java 9之后不要再配了。因为JDK 9开始引入了模块化类加载机制已经不需要手动维护classpath环境变量了配了反而容易出问题。很多旧教程里让人配.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar这在现代JDK上纯属画蛇添足别学。在实际命令行里验证配置时几个命令非常好用java -version看运行版本javac -version看编译器版本where javaWindows或which javaLinux/macOS查当前实际使用的是哪个路径下的Java。多版本切换时where java能把所有带java.exe的路径列出来看到底是哪一层的配置抢占了命令入口排查起来非常直观。2.2 用命令行还是用IDE两种方式建议互补现在很多新手一上来就装IDEA这没问题IDEA确实好用。但我的建议是初学阶段的前两周刻意多使用命令行编译运行哪怕麻烦一点也要坚持。原因是IDE帮我们隐藏了太多东西。你点一下绿色三角就能跑但背后经历了什么——源码文件编译成class文件、类路径加载、main方法入口查找这些过程全被吞掉了。一旦将来在工作中遇到构建工具、部署环境的问题没有命令行基础的人会非常被动。我在带新人的时候见过不少这样的例子代码在IDEA里跑得好好的部署到服务器上一编译就报“找不到主类”就是因为对classpath和编译过程完全没有概念。命令行用熟了再切回IDE感受完全不同。javac Hello.java生成的.class文件、java Hello执行的入口逻辑你会知道工具只是包装了这些命令。学习阶段我推荐一个组合用记事本或VS Code写代码然后用命令行编译运行等你把javac和java两个命令用得溜了再正式转到IDEA里写工程。工具使用上有两个细节值得注意。第一IDEA里新建Java类后快速生成main方法可以敲psvm回车生成输出语句敲sout回车这是高频快捷键一天能用几十次。第二无论用哪个编辑器源码文件的编码格式强烈建议统一为UTF-8。尤其在国内Windows环境下系统默认编码可能是GBK如果不统一第一次带中文的代码编译时就会出现乱码和报错这个问题我在第7部分会专门展开讲。3. 变量、数据类型与类型转换搞不清这些后面全是坑3.1 Java的8种基本类型与引用类型Java里的数据类型分成两大类基本类型和引用类型。基本类型就8种可以记成“四整两浮一字符一布尔”。四整是byte、short、int、long两浮是float、double一字符是char一布尔是boolean。每种基本类型的大小、取值范围和默认值对写代码影响很大。我整理了一个常用速查表类型占用字节默认值取值范围byte10-128 ~ 127short20-32768 ~ 32767int40-2^31 ~ 2^31-1long80L-2^63 ~ 2^63-1float40.0f约 ±3.4E38double80.0d约 ±1.8E308char2\u00000 ~ 65535boolean1falsetrue / false有两个容易忽略的点。第一Java给每个基本类型都设计了默认值局部变量不赋初值无法编译但成员变量有默认值这是Java语言的安全性设计避免出现其他语言里那种“变量没初始化就拿着脏数据乱跑”的情况。第二整数字面量可以用下划线分割比如1_000_000可读性很好Java 7开始支持写长数字时非常实用。引用类型和基本类型的核心区别在于存储内容。基本类型变量直接存数值引用类型变量存的是对象的地址引用。用生活化类比就是基本类型是你在便签上直接写了“3”引用类型是便签上写着“去第5排第3个储物柜取东西”。这个差异在方法传参时体现得淋漓尽致后面第6部分专门讲。String是最常用的引用类型但它有特殊待遇可以直接用双引号赋值还能用拼接看起来像基本类型。要小心的是String拼接在循环里会产生大量中间对象性能很差循环拼接建议用StringBuilder。3.2 类型转换的两种方式与精度陷阱类型转换分隐式转换和强制转换两种。隐式转换遵循“小转大”的原则byte转int、int转long都不会丢数据编译器直接放行。比如int i 5; long l i;没问题因为8个字节的long完全装得下4个字节的int。强制转换则是“大转小”需要显式写括号强转比如int i (int) someLong。这种转换有风险超出目标类型范围就会发生截断数据直接丢。最经典的笔试题就是byte b (byte) 130;结果等于多少答案是-126。原因简述一下130的int二进制是00000000 00000000 00000000 10000010强转成byte时只保留低8位10000010首位是1在补码表示法里就是负数的-126。这种题不是为了刁难人而是让我们真正理解“位”和“表示范围”。日常开发中还有一个特别常见的精度陷阱整数除法。int a 1; int b 2; double d a / b;结果不是0.5而是0.0。因为a / b在等号右侧时两个操作数都是int先按整数除法得到0然后才赋值给double。正确写法是double d (double) a / b;或double d 1.0 / 2;先让至少一边变成浮点数再做除法。这个坑初学的人基本都会踩一次踩过之后印象特别深。浮点数本身还有一个更隐蔽的坑二进制无法精确表示很多十进制小数比如0.1在浮点数里是近似值。所以千万别直接拿判断浮点数相等比如0.1 0.2 0.3结果是false。需要精确计算金钱等场景请使用BigDecimal类这是Java专门的精确十进制计算方案。如果只是判断两个浮点数是否“差不多”可以设定一个很小的误差范围比如Math.abs(a - b) 1e-6。一个容易被忽略的细节三元运算符也会触发类型提升。比如int x 1; long y 2L; Object obj true ? x : y;你以为是Object类型值为1实际结果是Long类型的1L因为编译器会把x提升为long再装箱。这种细节在写通用代码时容易引起意外记住一句话混合类型参与运算时结果会向“更大”的类型靠拢。4. 运算符的细节比你想的多短路、自增和位运算4.1 运算符全家桶与优先级Java的运算符分成六类算术、赋值、关系、逻辑、位、三元。大部分都很直白不需要死记但有几个关键点值得花时间理解。优先级上有一个简化记忆版口诀单目 算术 移位 关系 逻辑 三元 赋值。实际开发中我的建议是优先级记个大概写代码时多用括号把意图明确表达出来。比如(a b) * c和a b * c有括号一眼就懂没括号就得想一下。代码是写给人看的没必要用优先级秀智商这是新手要尽早建立的意识。关系运算符和逻辑运算符在判等场景有一个非常经典的坑在基本类型之间比较数值在引用类型之间比较的是内存地址。所以new String(abc) new String(abc)结果是false两个对象地址不同要用equals()方法判断内容相等。这个知识点在Java里出现频率极高网上有句话叫“Java程序员每天都在和equals做斗争”绝不是夸张。还有一个经常踩的坑是和搞混。if (a 1)在Java里编译都过不去因为是赋值赋值表达式的结果是int不是boolean编译器直接报错。这点倒是比很多语言安全不会出现把赋值当比较的bug。4.2 短路与自增让笔试题没有机会坑你逻辑运算符里和||是短路逻辑和|是非短路逻辑。短路的含义是根据左侧结果就能确定整个表达式结果时右侧直接不再求值。比如(a ! 0) (b / a 1)这个表达式如果a等于0左侧为false那右侧b / a根本不会执行也就不会抛出除零异常。这是短路运算非常典型的安全用法。反过来如果写成(a ! 0) (b / a 1)无论左侧是什么右侧都会执行除零异常照样抛。记住这个规则看代码时能避开很多潜在问题。自增自减运算i和i的差别也是高频考点。核心区别就一句话i先取旧值参与运算再自增i先自增再取新值参与运算。看这个例子int i 1; int a i; // a 1, i 2 int b i; // i 3, b 3 System.out.println(a b); // 输出 1 3写代码时不用刻意回避自增自减但一定要避免在一个表达式里对同一个变量多次使用自增比如x i i这种。这种代码的可读性极差而且不同语言的结果可能还不一样纯属给自己和别人添堵。可读性优先于一切技巧这是我在工程实践里最深的一条体会。位运算符、、和、|、^在实际业务中不如前几类常用但有两个经典应用值得知道。第一个是判断奇偶(n 1) 0为偶数因为奇数的二进制最低位一定是1。第二个是权限标志位用int类型的每一位表示一种权限通过判断是否拥有权限通过|追加权限这种设计在底层框架和Redis位图等场景中非常普遍。还有一个冷门但实用的小技巧n * 2可以写成n 1n / 2可以写成n 1。但需要强调的是现代JVM的优化器已经非常聪明了乘2除2写普通运算符就行不用手动改移位。了解原理可以别指望这种优化能提升多少性能它带来的可读性下降完全不值。5. 流程控制实战写出第一段有逻辑的程序5.1 if-else和switch怎么选流程控制是程序逻辑的核心骨架Java里主要就是分支和循环。分支有if-else和switch两种循环有for、while、do-while三种。选if-else还是switch我的判断标准是判断条件是范围比如成绩大于60分时用if判断条件是离散的具体值比如星期几、月份、枚举时用switch。switch的优势是结构清晰可读性好而且某些场景下编译器能优化成跳转表执行效率高。但传统的switch有个经典的坑叫case穿透。每个case块末尾如果不写break程序会继续执行下一个case的代码直到遇到break或switch结束。有时候这是有意的比如多个case要共享同一段处理逻辑但更多时候是无意的忘了写break程序就跑出了预期行为排查起来特别容易抓瞎。新的Java版本JDK 14正式支持提供了箭头语法极大改善了传统switch的体验// 传统写法 int score 85; String level; switch (score / 10) { case 10: case 9: level 优秀; break; case 8: level 良好; break; default: level 继续努力; } // 箭头语法 int score 85; String level switch (score / 10) { case 10, 9 - 优秀; case 8 - 良好; default - 继续努力; };看到区别了吧箭头语法天然规避了穿透问题而且能直接作为表达式返回值代码从六行缩到四行可读性提升明显。如果你用的是JDK 17或21强烈建议直接学新写法。分支判断里还有一个写代码的习惯问题当有多个条件时把最容易成立、能最快返回的条件放在前面可以减少无效判断。同时要避免写特别深的嵌套if三层以上就要考虑拆方法或提前return。版本控制工具和AI都救不了“意大利面条式”的嵌套逻辑优化可读性是对自己和同事最基本的善意。5.2 循环的三种姿势与跳出技巧for、while、do-while三种循环使用场景各不同。for适合循环次数已知的场景比如遍历数组、计数while适合循环次数未知但终止条件明确的场景比如读文件直到末尾do-while的特点是条件至少判断一次无论如何都会先执行一遍循环体。实际开发中do-while用得最少但也不是没有需求。比如你要弹出一个菜单至少显示一次然后根据用户输入决定是否继续这种场景do-while就最合适。说到跳出循环break是终止整个循环continue是跳过当前这一次迭代这两个简单。复杂一点的是带标签的breakJava支持在外层循环前加标签然后在内层循环里break 标签名一次性跳出所有嵌套循环。直接看代码outer: for (int i 0; i 10; i) { for (int j 0; j 10; j) { System.out.println(i j); if (i 3 j 3) { break outer; // 直接跳出两层循环 } } }这个语法本身不难但说实话日常开发里我看到的大多数多层嵌套循环更好的选择是抽方法加return或者直接重构逻辑。标签跳出是语法兜底不是最优设计能用简单逻辑替代就别上复杂度。增强for循环for (String s : list)是语法糖底层其实就是迭代器或下标遍历。用增强for时有一个大坑在循环体内不能直接通过list.remove()删除元素否则会抛ConcurrentModificationException。这个异常在很多新人眼中看起来非常吓人其实原理很简单——迭代过程中修改了集合结构迭代器的校验检测到不合法操作就抛异常了。如果想在遍历时删除元素应该用Iterator的remove()方法或者收集好要删除的元素循环结束后统一删除。这里给大家一个练手的经典题目打印九九乘法表。它要求双重循环、控制换行和格式化输出20行不到的代码能把for嵌套和流程控制用得滚瓜烂熟。写完乘法表再试试打印一个等腰三角形两个小练习做完循环基本就过关了。6. 方法与数组代码复用的第一步和内存视角6.1 方法的重载与值传递方法是Java里实现代码复用的最基本单元。定义方法时需要关注的几个部分包括访问修饰符、返回值类型、方法名、参数列表、方法体。其中方法名加参数列表合称为方法签名Java里判断一个方法是否重复看的就是方法签名返回值类型不算在签名里。重载是指同一个类里允许存在多个同名方法但参数列表不同。比如System.out.println就重载了无数次可以打印字符串、整数、浮点数、对象方法名一样参数类型不同。调用时会根据实参的类型自动匹配最合适的版本。这个特性在写工具类时特别有用一套方法名搞定多种类型参数比如写一个计算器工具类可以重载add(int, int)和add(double, double)。重载有一个比较容易踩的坑是可变参数的方法。可变参数void print(String... args)本质上是一个String[]数组参数它可以与同名非可变参数方法共存但调用时需要注意匹配规则。如果同时定义了print(String[] args)和print(String... args)直接编译报错因为这俩签名冲突所以选择其中一个实现就好。接下来是最重要的概念Java的方法参数传递严格来说是值传递。这里很容易被网上各种说法绕晕我用人话说明白。基本类型参数传递时复制的是值本身。方法内修改变量不影响外面的变量。public static void change(int x) { x 100; } public static void main(String[] args) { int n 10; change(n); System.out.println(n); // 还是10 }引用类型参数传递时复制的是引用变量里存的地址值。所以外面的引用变量和方法里的引用变量指向的是同一个堆对象方法内通过引用修改对象内容外面能看到变化。但方法内重新给引用变量赋值一个新对象外面的引用指向不会变。public static void change(StringBuilder sb) { sb.append( world); // 外面能看到 sb new StringBuilder(new); // 外面看不到这个变化 } public static void main(String[] args) { StringBuilder sb new StringBuilder(hello); change(sb); System.out.println(sb.toString()); // 输出 hello world }经典的swap问题就是由此而来——写一个方法交换两个int变量的值交换不了。原因很简单方法内交换的是两个副本和外面的变量没关系。如果要真正交换两个引用变量的内容需要在方法里通过对象状态去改或者把交换逻辑写在调用处。理解值传递是理解Java内存模型的第一步基础打不牢后面学集合、学并发很多现象都会觉得“玄学”。6.2 数组、常见初始化与内存布局数组是Java里最基础的数据容器它存储一组相同类型的数据长度一旦确定就不可改变。数组既可以存基本类型也可以存引用类型数组本身是引用类型。创建数组有三种方式int[] arr1 {1, 2, 3}; // 静态初始化声明同时赋值 int[] arr2 new int[5]; // 动态初始化长度为5默认值0 int[] arr3 new int[]{4, 5, 6}; // 匿名数组常用于方法传参为什么初始化和内存有关因为new int[5]会在堆内存中分配一块连续空间能存下5个int类型的值。数组变量arr2本身是一个引用存在栈上里面存的是堆中这块连续空间的起始地址每个元素通过“起始地址 索引 * 元素大小”来定位。这就是为什么数组下标从0开始——第一个元素的地址偏移是0这也是计算机科学史上的一个经典设计。理解了内存布局之后就能理解为什么数组访问越界会抛ArrayIndexOutOfBoundsExceptionJVM访问了超出了这块连续空间大小的地址安全机制直接挡住并抛异常。这个异常非常常见排查也很简单看异常信息里是第几行确认数组的索引值是多少再结合数组的长度就能定位。数组声明时有一个风格问题int[] arr和int arr[]在语法上都是合法的前者更符合Java惯例阅读代码时“int[]”作为一个整体类型更直观实际开发中统一用int[] arr的形式。数组之间的复制也是高频操作。最简单的是循环一个个复制但更高效的是System.arraycopy或Arrays.copyOf。其中System.arraycopy是native方法直接内存操作速度最快Arrays.copyOf底层也调用了System.arraycopy但它可以自动扩容并返回新数组。日常开发中涉及数组扩容时Arrays.copyOf用得多自己写数组扩容逻辑的情况其实很少因为集合类已经帮我们处理好了。用Arrays.toString(arr)可以直接输出数组内容不要再自己写循环打印了。Arrays.sort(arr)对数组进行升序排序底层是双基准快速排序性能很好。Arrays.binarySearch(arr, key)做二分查找时要注意前提是数组必须已排序。这几个工具类方法建议直接记住写代码效率高很多。7. 常见问题与排查技巧实录7.1 新手最常碰到的几个编译/运行问题技术文章如果不写报错排查总感觉少了灵魂。下面这几个问题是我在带新手时几乎每周都会遇到的整理出来给各位当作速查表用。问题现象常见原因解决方法javac不是内部或外部命令PATH未配置或配置错误检查JAVA_HOME和PATH确认%JAVA_HOME%\bin已添加找不到或无法加载主类classpath配置错误或类名大小写不匹配检查文件名、public类名、java命令导入的类名三者一致编码GBK不可映射字符源码文件编码与编译器默认编码不一致统一文件为UTF-8编译时加-encoding UTF-8参数数组越界异常访问了超出数组长度范围的索引检查循环边界确认i arr.length空指针异常引用变量为null却调用了它的方法或属性定位报错行检查引用是否已正确初始化其中“找不到或无法加载主类”这个问题原因五花八门。最常见的情况是初学者在命令行里输错了类名的大小写。Java类名区分大小写HelloWorld和helloworld是两个不同的类名系统找不到对应的类就报这个错。还有一个情况是运行时用了java Hello.java这种写法那是Java 11开始支持的“单文件源码运行”模式和老式的java Hello是两回事别混。中文乱码的问题特别值得细说。在Windows的cmd窗口里系统默认编码是GBK而Java源码通常保存为UTF-8。如果用IDEA保存的UTF-8源码拿到命令行用javac直接编译编译器会按系统默认编码读取UTF-8的中文在GBK视角下就是乱码编译直接报错。解决办法一是把所有文件都统一成UTF-8二是编译时显式指定javac -encoding UTF-8 Hello.java三是中文输出时把控制台编码调整一下。这属于典型的“环境编码不一致”问题提前知道比到时抓瞎好太多。7.2 几个容易被忽略的习惯建议教了语法最后再给几个很多人忽略但实际收益极高的习惯建议。第一个是命名规范。类名使用大驼峰UserService、OrderInfo方法名和变量名使用小驼峰getUserName、totalAmount常量全大写加下划线MAX_COUNT。代码首先是写给人看的命名清晰比写多几行注释更有价值。不要在命名上偷懒一个叫data的变量和一个叫pendingOrderList的变量维护成本差距十倍往上。第二个是文件组织。Java规定一个源文件只能有一个public类且文件名必须与这个public类的类名一致。这个规则在刚学的时候会觉得很严格但它是Java代码组织和编译器工作的基础。如果你写了public class User文件就必须叫User.java改类名之后记得同步改文件名IDEA里的重命名重构能自动完成这件事命令行手动操作时就要注意。第三个是报错信息阅读习惯。新手看异常栈时总喜欢从第一行开始读但真正的根因信息通常在最后面的Caused by部分。Java异常链的机制会把底层的真实原因包在里面养成从后往前读的习惯能快速定位到代码真正出错的位置。看到报错不要慌把异常栈的前几行和Caused by信息贴出来大多数问题群里一问就有人秒答。第四个建议是尽早学会追踪代码执行过程。IDE里Debug断点调试是必须掌握的技能但别忘了更朴素的System.out.println打印法。在关键变量输出一下中间结果看到哪个瞬间和预期不符问题就找到了。这个方法虽然“原始”但无数次排查中都是最快见效的手段。最后一个习惯也是我最想强调的每天动手写代码的时间一定要大于看教程的时间。JavaSE的视频课和文章多如牛毛但语法知识就像肌肉记忆看一百遍不如敲十遍。我自己练语法基础的时候每天找两三道OJ小题目用纯Java实现坚持了大概两周数据结构、运算符、流程控制这些内容就再也不会搞混了。写到这里基础02的核心内容就讲完了。我个人在学习和带新人的过程中最深的一点体会是Java语法的难点从来不在记忆而在理解背后的“为什么”。比如为什么整数除法会丢精度为什么switch会穿透为什么值传递改不了外面的值——每一个坑背后都对应着一个计算机设计上的基本原理。把这些原理串起来语法就不再是零散的规则而是一张逻辑自洽的知识网。之后再学面向对象、集合框架你就能带着这张网去“接住”新知识而不是每个知识点都单独背一遍。学习节奏上我的建议是可以慢一点但每一步都要亲手敲代码验证。这篇里的代码示例不算多每个都值得敲一遍再改一改改出几种不同的行为再想想为什么这比刷十遍教程都有用。