☰
Java基础第二阶段:运算符、流程控制、数组与字符串实战指南
2026/10/12 3:09:47 网站建设 项目流程

很多自学 Java 的朋友,学完变量、数据类型、控制台输入输出之后会突然进入一段迷茫期:语法似乎都眼熟,但真要写个东西出来又不知道该从哪里下手。这个位置恰好就是 java基础-2 要解决的问题。我见过太多人在这里反复翻书、反复看视频,进度反而越来越慢,原因很简单——不知道该练什么、练到什么程度才算过关。这篇内容就是把基础阶段最容易卡住的几块硬骨头拆开讲清楚:运算符、流程控制、数组、字符串、方法,以及那些初学必见的报错。每个部分我都会先说它解决什么问题,再给代码和思路,最后补上实际带人时见到的常见失误。适合刚学完第一阶段的读者,也适合基础不牢想回炉巩固的同学。

1. 从 Hello World 走出来,第二阶段到底该学什么

1.1 第一阶段通常覆盖什么,第二阶段要补什么

大多数入门教程的第一阶段会讲这些:JDK 的安装、环境变量配置、Hello World 的运行流程、变量声明、基本数据类型(int、double、boolean 等)、运算符的简单使用、Scanner 读取用户输入。这套组合拳打完之后,你能做的最复杂的程序,大概也就是"输入两个数求和""输入成绩判断是否及格"这种级别。

但这些其实只是"能看懂语法"的层面,离"会写程序"还有一段不小的距离。java基础-2 这个阶段要补的,是几块从语法走向逻辑的跳板:

  • 运算符的深水区:不只是加减乘除,而是整除、取模、自增自减、短路逻辑、三元表达式这些初学最容易写错的地方。
  • 流程控制:if、switch、for、while,让程序根据条件执行不同分支、重复做某件事,这是编程逻辑真正开始运转的地方。
  • 数组:第一个需要建立"引用"概念的数据结构,也是后续集合框架的认知基础。
  • 字符串:String 对象不可变的特性、== 和 equals 的区别,这些几乎每个面试题都会碰到。
  • 方法:把代码拆成积木、封装复用,这是程序设计的起点。

这一篇不会去碰类、对象、继承这些面向对象概念,那不是基础第二课该讲的。先把这些过程式的东西练扎实,后面学面向对象才不会被概念和语法的双重负担压垮。

1.2 为什么这个学习顺序几乎是固定的

有人可能会问:为什么非要按这个顺序学?先学数组再学循环行不行?先学方法再学字符串行不行?

技术上当然能看懂,但学习效率会差很多。原因在于这些知识点之间有明显的依赖关系:

  • 循环里通常会配合条件判断(比如求偶数和、统计个数),所以 if 和 for 最好连着学。
  • 数组的遍历方式天然就适合用 for 循环,不先学循环,数组的价值体现不出来。
  • 方法里会用到数组和字符串作为参数、返回值,放在数组之后讲,例子随手就能写。
  • 字符串的 equals 问题又涉及 == 这个运算符的语义理解,所以运算符必须早早打好基础。

这个顺序不是我发明的,是无数条学习路径验证过的。跳着学容易陷入"每个字都认识,组合起来看不懂"的状态,老老实实推进反而最快。

1.3 先给自己定一个看得见的输出目标

学完这一阶段,你应该能独立完成类似这样的程序:从控制台读取若干个学生的成绩,计算平均值、最高分、最低分,并输出每个成绩对应的等级(优、良、及格、不及格)。

这个任务虽然简单,但它把这一阶段几乎所有知识点都串起来了:Scanner 输入、数组存储、循环遍历、分支判断、方法封装。如果你能不看答案写出这个程序,并且能解释清楚每一步为什么这么写,那 java基础-2 就算真正过关了。

2. 运算符与表达式:初学阶段最容易写错的一组细节

2.1 整除和取模,别让 Java 的整数运算给你惊喜

先看一个很经典的问题:

int a = 7 / 2; System.out.println(a); // 输出多少?

很多新手以为会输出 3.5,实际输出的是 3。原因很简单:两个 int 做除法时,Java 不会自动帮你转成浮点数,结果还是 int,小数部分直接截断。这不是 bug,而是整数运算的规则。

那要是真想要精确的小数结果怎么办?两种办法:

double b = 7.0 / 2; // 有一个操作数是浮点数,结果就是浮点数,输出 3.5 double c = (double) 7 / 2; // 强转之后再除,也输出 3.5

取模运算符 % 也值得多说一句。它算的是整除后的余数,比如 7 % 2 结果是 1。初学阶段最容易搞混的是负数场景:-7 % 2 在 Java 里结果是 -1,因为 Java 的结果符号跟被除数一致。这个知识点平时用得不多,但一旦遇到,往往会被折腾一阵。

提示:如果你要判断一个整数 n 是不是偶数,正确写法是 n % 2 == 0,而不是拿 n / 2 再去判断。这个坑看着低级,我见不少人真踩过。

2.2 自增自减:++ 放在前面和后面完全是两回事

自增自减运算符是初学者绕不过去的一个坎,原因在于它有两个位置:i++和++i,还有两个效果:用旧值,还是用新值。我的记忆方法很粗暴:

  • i++:先拿去用,用完再加。
  • ++i:先加,加完再拿去用。

看代码:

int i = 3; int m = i++; // m = 3,i 变成 4 int n = ++i; // i 先变成 5,然后 n = 5

为什么我要单独拿出来讲?因为在 for 循环里,i++和++i对整体的循环结果几乎没影响,但在表达式里使用就完全不同。实际开发中,除了 for 循环的步进,我基本不推荐把自增自减混在复杂表达式里。这种代码写出来很炫,但下一次维护的人(很可能就是三个月后的你自己)读起来真的想骂人。

2.3 短路逻辑与三元表达式

&&和||是两个逻辑运算符,它们的行为和&、|不太一样。关键在于"短路"两个字:

int age = 10; boolean result = (age > 5) || (age++ > 20); System.out.println(age); // 猜猜输出是多少?

因为age > 5已经是 true,整个||表达式的结果已经确定,右边的age++ > 20根本不会执行,所以 age 还是 10。同理,&&左边为 false,右边就不执行。这个特性在初学阶段好像没啥用,但在后面的实际项目里,它就是"先检查对象是否是 null,再访问对象的属性"这种保护性语句的地基。

三元表达式是 if-else 的简写:

int score = 85; String level = score >= 60 ? "及格" : "不及格";

它的结构是"条件 ? 结果1 : 结果2",适合简单场景。但有个建议:如果条件分支里要执行多行逻辑,别用三元,老老实实写 if-else,可读性比那几行代码的"简洁"重要得多。

2.4 运算符优先级:记不住就多用括号

很多人会去背优先级表,什么算术运算符大于关系运算符大于逻辑运算符之类的。我的建议是:不用背,也用不着。真需要判断优先级的时候,直接加括号让意图一目了然。

boolean flag = (a + b > c) && (d != 0);

这段代码即使不查优先级表,读起来也没有任何歧义。与其在优先级上花时间,不如这几分钟拿去看循环的写法,收益高得多。不过有一个优先级相关的常识还是要记的:==的优先级低于算术运算符,所以a + b == c实际执行的是(a + b) == c,这个写多了自然就顺了。

3. 流程控制:从顺序执行到分支循环,编程逻辑开始真正运转

3.1 if-else 与 switch:什么场景该用哪个

if-else 是万能的,任何需要条件判断的地方都能用。但代码里如果出现"一个变量分别等于几个值,每个值对应不同的逻辑",那用 switch 会清晰得多。

老版本写法的 switch 有个著名的坑叫"穿透":每个 case 如果不写 break,程序会继续执行下一个 case 里的代码,直到遇到 break 或整个 switch 结束。这个特性早期经常被拿来面试,比如让你猜:

int week = 2; switch (week) { case 1: System.out.println("周一"); case 2: System.out.println("周二"); case 3: System.out.println("周三"); }

这段代码会同时输出"周二"和"周三",因为 case 2 没有 break。初学阶段记不住这个特性很正常,但一定要知道:写 case 时习惯性带上 break。如果你用的 JDK 版本比较新(14 之后),还能用箭头写法,完全绕开穿透问题:

int week = 2; switch (week) { case 1 -> System.out.println("周一"); case 2 -> System.out.println("周二"); case 3 -> System.out.println("周三"); default -> System.out.println("未知"); }

我的建议是:分支不超过三个用 if-else;分支很多且判断的是同一个变量的取值,用 switch;如果判断的是一个范围(比如分数 60 到 80),那还是得用 if-else。没有谁更好,只有合不合适。

3.2 for、while 和 do-while 怎么选

for、while、do-while 是三种循环,但很多人学完都记不清它们的区别。最简单的理解方式:

  • for 循环适合"明确知道循环几次"的场景。比如遍历数组、打印 1 到 100,次数是定的。
  • while 循环适合"条件满足就继续"的场景。次数不确定,可能一次都不执行。比如读文件直到文件结束。
  • do-while 循环先执行一次,再判断条件。适合"无论如何都要先干一次"的场景。

我平时看到的初学代码里,最典型的问题是:明明是一个已知次数的任务,非要用 while,结果还要手动维护一个变量来计数。能用 for 的场景不用 while,能让代码结构更清晰。来看一个对比:

// 用 while 打印 1 到 10 int i = 1; while (i <= 10) { System.out.println(i); i++; } // 用 for 打印 1 到 10 for (int j = 1; j <= 10; j++) { System.out.println(j); }

for 的优势在于:初始化、条件判断、步进三个部分都写在第一行,循环逻辑一眼就能看全。while 则更适合条件本身才是重点的场景。

3.3 循环里最常见的死循环:条件永远为 true

新手在写 while 循环的时候,经常出现程序一运行就卡死,控制台什么都不输出。这种多半就是死循环。最常见的写法是:

int i = 1; while (i <= 10) { System.out.println(i); }

循环体里忘了让变量 i 往前走,条件永远成立。这种情况我在给初学者看代码时几乎每次都能遇到。解决的思路只有一个:循环开始前先想清楚"循环要结束,必须发生什么变化"。是计数器增加,还是条件本身依赖某个外部输入。每次循环体里都要朝这个方向推进,哪怕只是一行i++。

3.4 用一个小例子把流程控制串起来

单纯讲语法太干,我建议初学者用这个题目练手:打印九九乘法表。

for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { System.out.print(j + " * " + i + " = " + (i * j) + "\t"); } System.out.println(); }

这个题目有意思的地方在于:它需要嵌套循环,而且内层循环的次数依赖外层的当前值。你一旦能写出这段代码,对 for 循环、字符串拼接、转义字符\t的理解都会上一个台阶。想加难度的话,还可以试试在控制台打印一个等腰三角形或菱形,那会逼着你去思考空格和 * 的数量关系。

提示:控制台输出用System.out.print()不换行,用System.out.println()换行,这是初学阶段最容易被忽略的细节。打印图案类题目出错,一大半是没搞清这两个方法的区别。

4. 数组:第一个真正需要理解"引用"概念的数据结构

4.1 数组的声明、创建和默认值

数组是"同类型数据的有序集合",这句话每个教程都写,但真正理解它需要动手。创建一个能放 5 个整数的数组有两种常见写法:

int[] nums = new int[5]; // 创建一个长度 5 的数组,默认值是 0 int[] scores = { 90, 85, 78 }; // 直接用初始值创建

新手最容易不理解的是new int[5]里的"默认值"。你不赋值,数组里的每个元素也有值:int 类型的数组默认全是 0,double 默认 0.0,boolean 默认 false,引用类型(比如后面学的 String 数组)默认是 null。这个特性后面经常用到,比如统计时先初始化一个全 0 的计数器数组。

还有一个细节:数组长度用.length获取,注意这里不是方法调用,不需要写括号。这一点和 String 的.length()正好相反,我见过不少人把两者搞混,编译器直接报错。

4.2 下标越界:初学阶段必见的第一道异常

数组的下标从 0 开始,长度为 n 的数组合法下标是 0 到 n-1。这句话看着简单,但刚写代码时几乎人人都会踩一次:

int[] nums = new int[3]; nums[0] = 10; nums[1] = 20; nums[2] = 30; System.out.println(nums[3]); // 运行时报 ArrayIndexOutOfBoundsException

报错信息已经说得很清楚了:下标 3 越界,超过数组长度 3 的合法范围。这个异常是初学阶段的"好朋友",遇到它不要慌,先把报错信息里提到的行号找到,看看是不是下标写越界了。讲个我实际遇到过的案例:有个同学写循环遍历数组,从i = 1开始到i <= nums.length结束,结果循环里访问nums[i]时,最后一次 i 恰好等于 length,越界了。正确的遍历应该是i = 0; i < nums.length; i++,这个"从 0 到 length-1"的规则,值得在心里多默念几遍。

4.3 遍历数组的两种方式

遍历是指把数组里的每个元素都过一遍。最基础的方式是普通 for 循环配合下标:

int[] nums = { 10, 20, 30 }; for (int i = 0; i < nums.length; i++) { System.out.println(nums[i]); }

Java 还提供了一种更简洁的写法,叫增强 for 循环(for-each):

for (int n : nums) { System.out.println(n); }

它的意思是"把 nums 里的每个元素依次取出来,赋值给 n"。这种写法非常适合单纯的读取操作,代码更短、不容易写错下标。但它有一个限制:拿不到当前元素的下标。如果你想同时知道"这是第几个元素",那还是用普通 for 循环。

4.4 二维数组和工具类 Arrays

二维数组可以理解为"数组的数组",最常见的使用场景是表格、矩阵。它的初始化有两种方式:

int[][] matrix = new int[3][3]; // 3 行 3 列,默认全 0 int[][] data = { { 1, 2 }, { 3, 4 }, { 5, 6 } }; // 三行两列

访问二维数组的元素要用两个下标:matrix[1][2]表示第 1 行(从 0 算起)第 2 列。遍历时需要两层循环嵌套,外层控制行,内层控制列。

另外,Java 的java.util.Arrays工具类有几个方法值得初学阶段就混个脸熟:

import java.util.Arrays; int[] nums = { 3, 1, 2 }; Arrays.sort(nums); // 排序,结果是 [1, 2, 3] System.out.println(Arrays.toString(nums)); // 输出 [1, 2, 3] Arrays.fill(nums, 0); // 把整个数组全部赋值为 0

特别是Arrays.toString(),它能直接以人能看懂的方式把数组内容打印出来,调试的时候非常实用。很多人在初学阶段直接打印数组名System.out.println(nums),结果输出一串看不懂的东西[I@15db9742,然后怀疑自己写错了。其实那只是数组对象的地址表示,想看得用Arrays.toString()。

5. 字符串:一个让你重新认识 == 和 equals 的重要对象

5.1 String 的不可变性和字符串常量池

Java 里的 String 虽然写起来像基本类型,但它是一个类,是一个对象。它有一个重要特性:不可变。简单说,一个字符串对象创建之后,内容就不会再变了。你看到的"字符串拼接"生成的新字符串,其实是创建了一个全新的对象,而不是修改了原来的对象。

String s = "hello"; s = s + " world"; // s 现在指向一个新的字符串对象,内容是 "hello world"

因为不可变,Java 还做了一个优化叫"字符串常量池"。代码里直接写的字符串字面量会被复用在池子里。比如:

String s1 = "hello"; String s2 = "hello";

s1 和 s2 会不会指向同一个对象?答案是会,因为两个字面量相同,常量池里只需要保存一份。这个机制是理解后面 == 和 equals 区别的关键前提之一。

5.2 == 和 equals:每次面试都跑不掉的题

初学阶段最容易被问懵的一个问题:判断两个字符串内容是否相等,用 == 行不行?我直接说结论:不行,请用 equals。

String s1 = "hello"; String s2 = new String("hello"); System.out.println(s1 == s2); // false,比较的是对象地址 System.out.println(s1.equals(s2)); // true,比较的是内容

==对于引用类型的含义是比较两个引用是否指向同一个对象,也就是地址是否相同。s1 在常量池里,s2 是 new 出来的新对象,地址当然不一样。而equals()是被 String 类重写过的方法,它专门用来比较内容。

这也是我在第 2 章强调运算符的一个原因:很多人不理解为什么s1 == s2有时候是 true 有时候是 false,根源在于没有区分"值比较"和"引用比较"。这里建议养成一个习惯:只要是用来比较字符串内容,一律用equals(),别管它是不是同一个地址。这样既省心,也不会踩到常量池在不同 JDK 版本下表现不一致的边角问题。

5.3 常用字符串方法,初学记这几个就够了

String 的方法非常多,但基础阶段真正高频用到的其实就这几个:

String s = "Hello World"; s.length(); // 11,注意这是方法调用,要加括号 s.charAt(1); // 'e',取第 1 个字符 s.indexOf("World"); // 6,找不到返回 -1 s.substring(0, 5); // "Hello",从下标 0 截到 5,不包括 5 s.toUpperCase(); // "HELLO WORLD" s.toLowerCase(); // "hello world" s.contains("World"); // true s.trim(); // 去掉字符串两端的空白

这些方法都比较直观,唯一容易记错的是substring()的参数范围"含头不含尾"。初学阶段编程或者面试时,这种边界问题是高频失分点。你在练的时候可以随手写个小程序把这些方法都跑一遍,输出的结果和预期对一下,印象会深刻很多。

5.4 字符串拼接的几个选项

初学阶段通过+拼接字符串是最自然的写法:

String name = "张三"; int age = 18; System.out.println("姓名:" + name + ",年龄:" + age);

这种写法在开发中占大多数场景,够用且清晰。后面如果涉及大量拼接操作(比如循环里反复给字符串加内容),就需要考虑StringBuilder这类可变字符序列,因为它不会每次都创建新的字符串对象。这个问题在基础阶段不用深究,但可以留个印象:字符串拼接的效率问题会在你写更复杂的程序时慢慢浮现出来。

6. 方法:拆分逻辑、封装复用,值传递是绕不开的坎

6.1 方法的结构

方法就是一段可以被反复调用的代码块。定义一个方法有几个固定部分:修饰符、返回类型、方法名、参数列表、方法体。基础阶段最常见的形式是这样的:

public static int add(int a, int b) { int sum = a + b; return sum; }

这里public是访问修饰符,暂时先照抄;static暂时也先照抄,等到学面向对象时才会真正理解它的含义。int是返回类型,方法算完了一个整数结果要返回出去;a和b是参数,调用方传入的数据。

调用方式也很直接:

int result = add(3, 5); System.out.println(result); // 8

方法的意义在于:把一段具体逻辑封装起来,名字起得好,调用者和维护者都不需要关心内部细节。这不仅是代码复用的问题,更是程序设计的起点。

6.2 值传递:传进去的是值,不是变量

这是初学方法时最大的一个坑。看代码:

public static void change(int x) { x = 100; } int a = 5; change(a); System.out.println(a); // 输出还是 5

为什么不是 100?因为 Java 的方法参数传递是"值传递"。change(a)传入的是变量 a 的值的拷贝,方法里修改的是拷贝,不影响原来的变量。这个规则可以用一句话概括:方法里的参数变来变去,外面的变量但凡类型是基本类型,就不会受影响。

那如果是数组呢?数组是引用类型,情况就不同了:

public static void modify(int[] arr) { arr[0] = 999; } int[] nums = { 1, 2, 3 }; modify(nums); System.out.println(nums[0]); // 输出 999

这里传入的是"指向数组对象的引用"的拷贝。拷贝和原引用指向同一个数组对象,所以方法里通过引用修改数组内容,外面的数组确实会变。理解这二者的区别,是初学阶段的一道分水岭。我的建议是:遇到这样的代码,别去硬背"基本类型不受影响、引用类型受影响",而是画一张图,把变量、引用、对象之间的关系画出来,一目了然。

6.3 方法重载:同名不同参

方法重载是指多个方法名字相同,但参数列表不同(参数个数、类型不同)。比如:

public static int add(int a, int b) { return a + b; } public static double add(double a, double b) { return a + b; }

调用的时候,Java 会根据你传入的参数类型自动选择对应的方法。add(1, 2)走第一个,add(1.5, 2.5)走第二个。方法重载的意义是让调用方不用记住一堆不一样的名字,统一名称、统一语义,由编译器根据参数区分。

这里有一个新手经常问的问题:两个方法只是返回类型不同,能不能构成重载?不能。方法重载的判断标准是参数列表,不包括返回类型。因为调用时你可能没接返回值,编译器根本没法靠返回类型区分该调用哪个。

6.4 递归:方法自己调用自己

递归是方法里比较神奇的一种用法:方法在方法体中调用自己。初学最容易理解的例子是阶乘:

public static long factorial(int n) { if (n == 0 || n == 1) { return 1; } return n * factorial(n - 1); }

递归的核心是两件事:找出口(终止条件)和找递推公式。没有出口的递归会一直调用下去,最终把栈内存耗尽,抛StackOverflowError。这是初学阶段一个非常经典的程序崩溃方式,运行的时候控制台会刷出密密麻麻的调用栈信息,看着吓人,其实就是递归没有收敛。

我对初学者的建议是:递归很有意思,但不要过度执着。能用循环解决的优先用循环。递归的思考方式适合二叉树遍历、目录遍历等后面才会接触的场景,基础阶段理解它的结构和出口思想就够了。

7. 新手最常见的报错信息,每一句都值得认真读

7.1 编译错误和运行时错误,先分清两件事

初学阶段写代码,报错主要分两类:

编译错误发生在程序运行之前,通常是语法写错了、变量没声明、括号没配对、用了未定义的方法。这类错误最好解决,因为编译器会明确告诉你哪个文件的哪一行有问题。IDE 里一般会在错误代码下面划红线,鼠标移过去就有提示。

运行时错误是编译通过了,但程序跑起来之后才崩溃。这类错误对新手来说更棘手,因为它们往往和逻辑有关,而不仅仅是语法。看一条典型的异常信息:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 at Test.main(Test.java:9)

最重要的是最后一行:Test.java:9,它告诉你出错的位置在第 9 行。往上一点,异常类型是ArrayIndexOutOfBoundsException,意思很直白:数组下标越界。

7.2 三大高频异常,见多了就不慌了

初学阶段遇到的运行时异常,90% 落在下面这三类里。我把它们的特点和排查思路整理成一张表:

异常类型什么时候出现怎么排查
ArrayIndexOutOfBoundsException数组下标越界找到报错行,看循环的终止条件是否写成了<= length,或者直接访问了arr[length]
NullPointerException调用了 null 对象的方法或属性找到报错行,看哪个对象可能没初始化,打印出来确认一下
NumberFormatException把字符串转数字时格式不对例如Integer.parseInt("abc")就会抛这个异常,检查输入内容是否真的全是数字

空指针异常尤其值得多说一句。它通常不发生在代码刚刚写好的那一刻,而在程序运行到某个分支时才出现。比如你根据条件创建了一个对象,某个分支里忘了赋值,后面再调用对象的方法,就抛异常了。排查思路永远是:报错行里出现的那个"."前面是什么对象,它有没有可能是 null。

7.3 最简单的调试方法:输出打印观察法

我刚学编程的时候,还没有现在这么成熟的 IDE 调试工具,前辈教我的第一个调试手段就是"在关键位置打印变量"。

for (int i = 0; i < nums.length; i++) { System.out.println("i = " + i + ", nums[i] = " + nums[i]); // 其他逻辑 }

这种做法虽然原始,但对初学阶段特别有效。原因在于,你不需要打断点、不需要理解"单步执行"这个概念,只需要把变量的中间值打到控制台,人脑顺着输出就能还原程序运行的轨迹。大量报错的根本原因是"程序运行的路径和你以为的路径不一致",打印变量能最快地暴露这个偏差。

等你熟悉了变量值的变化规律,再去学 IDE 里的断点调试,会轻松很多。不必一开始就用高级玩法,先学会让程序告诉你在哪一步出了问题,这个能力比任何工具都重要。

带过好几轮零基础的同学之后,我越来越觉得,基础阶段最应该练的不是背语法,而是用代码描述日常逻辑。有一次一位刚学完基础的同学跟我说,他用数组和循环把每天喝水的次数和时间点记下来了,虽然只是最简单的几行代码,但那种"把想法变成程序"的感觉,比做完一整章习题都有价值。所以这篇基础篇的最后,我不留一堆作业,只给你一个建议:今天就用运算符、流程控制、数组这三样东西,写一个你生活中真实会用到的小工具,比如统计一周的步数、算某门课程的平均分、判断一个数是不是质数。卡住了就回头翻这篇里的代码,动手写一遍,比盯着屏幕看十遍都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询