1. 笔记2是什么:从“会敲代码”到“会写程序”的分水岭
如果你正在学Java基础,刚弄明白变量、数据类型、运算符这些概念,能在IDEA里敲出几个HelloWorld和简单的计算器,那么恭喜你,你马上要进入整个Java入门过程中最关键的一个阶段。这份笔记2要解决的,不再是“环境装没装好、语法认不认识”的问题,而是“怎么让代码真正跑起来解决实际问题”的问题。这三个字——流程、数组、方法,就是你从“照着抄代码”变成“自己设计逻辑”的分水岭。
我在带新人或者看初学者提问的时候,发现一个特别普遍的现象:很多人学完了环境搭建和基础语法,能看懂每一行代码,但一让他写一个完整的小程序,比如猜数字、统计成绩、排序数据,就完全不知道从哪下手。卡住的地方从来不是某个语法不会,而是脑子里没有“逻辑结构”这个概念。笔记2这个阶段,核心就是补上这个缺口:让代码学会做决定、学会重复干活、学会批量处理数据、学会把功能拆成一个个可复用的模块。
这个阶段学完,你能达到什么水平?至少可以独立写出一个带循环、判断、数组和方法的完整小程序,比如“输入五个学生成绩,自动算出平均分和最高分”这种工具。这些功能看起来简单,却是后面所有东西的地基。面向对象、集合框架、IO流,本质上都是在“数据怎么存、逻辑怎么走”这两件事上做扩展。地基打不牢,后面学框架的时候你会处处觉得别扭。
这份笔记适合谁?两类人。第一类是纯零基础,刚看完环境配置和HelloWorld的新手,你可以把笔记1里的环境准备、IDEA安装、变量和数据类型看完之后,直接拿这份笔记2作为下一阶段的学习主线。第二类是已经学过一点Java、但中间断过、或者想系统梳理一遍基础的人,这份笔记里大量“当年踩过的坑”和“为什么这样写”的解释,能帮你把零散的知识点串成线。
2. 流程控制:让代码学会“做决定”和“重复劳动”
2.1 if/else:程序员的“十字路口”
流程控制说白了,就是代码里的“十字路口”。计算机最擅长的事情之一,就是根据条件选择不同的路走。Java里最简单的选择结构就是if。
int score = 85; if (score >= 90) { System.out.println("优秀"); } else if (score >= 80) { System.out.println("良好"); } else if (score >= 60) { System.out.println("及格"); } else { System.out.println("不及格"); }这个代码的逻辑不难理解,但有几个细节新手特别容易翻车。第一个,if后面的括号里写的是“条件表达式”,结果是布尔值,true或者false,别在里边直接写一个整数然后再嘀咕“为什么不行”。第二个,判断相等要用==,不是=。=是赋值,==才是比较。我见过无数次新手在if条件里写if (score = 90),然后IDEA直接标红,编译器报错。第三个坑是if括号后面千万不要跟分号,写了分号代表这个判断体结束了,后面的代码块就变成了无条件执行。
判断条件本身也有讲究。比如判断一个年份是不是闰年,规则是“能被4整除但不能被100整除,或者能被400整除”。代码写出来是这样的:
int year = 2024; if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { System.out.println(year + " 是闰年"); } else { System.out.println(year + " 不是闰年"); }这里的新手难点是运算符优先级。%(取余)的优先级高于==,==高于&&,&&高于||,所以上面这个表达式的执行顺序是:先算year % 4 == 0,再算year % 100 != 0,然后组合&&得到一个结果,同时算year % 400 == 0,最后两个结果用||合并。如果你不确定优先级,最好的习惯是加括号,就像上面代码那样把每个独立的判断包起来,既清晰又安全。这个习惯在以后写复杂业务条件的时候特别重要。
还有一个很多人意识不到的点,叫作“早返回”思想。如果你在写一个方法,先判断异常或无效情况,直接在开头用if把不满足条件的挡掉,而不是写一长串嵌套的if else,代码会清爽很多。比如判断一个数是不是正数并且是否为偶数,两个条件拆开判断比嵌套判断更容易读:
if (num <= 0) { System.out.println("不是正数"); return; } if (num % 2 == 0) { System.out.println("是正偶数"); }很多刚从零开始学Java的人会觉得if else嵌套得越深越显技术,其实恰恰相反,平铺直叙的if才是工程里更受欢迎的风格。
2.2 switch语句:多重分支的另一种解法
当分支条件特别多,而且都是判断一个变量的具体值的时候,if else写起来就会又长又丑。这时候应该用switch。最经典的案例就是输入星期几的数字,输出对应的名称:
int day = 3; switch (day) { case 1: System.out.println("星期一"); break; case 2: System.out.println("星期二"); break; case 3: System.out.println("星期三"); break; default: System.out.println("未知"); break; }switch在入门阶段有两大拦路虎:case穿透和漏写break。所谓case穿透,就是如果某个case的break不写,程序执行到匹配的case之后不会停下来,会继续往下执行后面的case内容,直到遇到break或者整个switch结束。很多新手第一次遇到这种“诡异现象”都会蒙圈,然后百度搜半天才知道是break漏了。
实际上case穿透有时候是被故意利用的。比如多个case需要执行同一段代码,可以这么写:
switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': System.out.println("是元音字母"); break; default: System.out.println("是辅音字母"); break; }需要注意的一点是,switch在Java 7之前只能判断整数类型(byte、short、int、char),Java 7开始支持String。三更草堂这套2020年的课程应该是以经典语法为主,不会涉及太多新特性,但你要知道一件事:后来的Java版本里switch还出现了箭头写法——case 1 -> System.out.println("星期一");,这种写法自带break效果,代码更简洁。如果你用的是新版Java,可以了解一下,但入门阶段先把传统写法吃透就够了。
2.3 循环:让代码学会“偷懒”
如果说if是十字路口,循环就是跑步机。计算机最擅长做的事就是重复劳动,而且是乐此不疲地重复。Java里有三种循环:for、while、do-while。它们的共同点是都要有一个“循环条件”来控制什么时候停下来,否则就会变成死循环,程序一直跑不停,就像你在跑步机上忘了按停止键。
for循环是使用频率最高的,结构也最清晰:初始化、条件判断、步进操作三个部分都在括号里。统计1到100所有偶数的和,用for写是这样的:
int sum = 0; for (int i = 1; i <= 100; i++) { if (i % 2 == 0) { sum += i; } } System.out.println("1到100的偶数之和为:" + sum);这里i的声明在for括号里,所以它只能在这个循环内使用,循环结束之后没法访问。这个点我在后面讲作用域的时候还会再提。
while循环适合那种“不知道具体要循环多少次,只知道什么时候该停”的场景。比如让用户猜数字,直到猜对为止:
int target = 37; // 假设目标数字 Scanner scanner = new Scanner(System.in); int guess; while (guess != target) { System.out.print("请输入你猜的数字:"); guess = scanner.nextInt(); } System.out.println("恭喜你猜对了!");while和do-while的唯一区别是执行时机:while先判断条件再执行,do-while先执行一次再判断条件。所以do-while至少会执行一次。什么场景用do-while?比如弹一个菜单:“请选择功能,按1继续,按0退出”。你必须先展示菜单让用户输入一次,再根据输入决定要不要循环,这种“至少执行一次”的交互逻辑就是do-while的天然用例。
break和continue是两个辅助工具。break是直接终止整个循环,continue是跳过当前这一次循环、继续下一次。举个例子:循环打印1到10,遇到5就跳过不打印,遇到8就提前结束:
for (int i = 1; i <= 10; i++) { if (i == 5) { continue; } if (i == 8) { break; } System.out.println(i); }运行结果会打印1、2、3、4、6、7,到8的时候循环整体结束。这两个关键字别搞混,一个管“跳出去”,一个管“跳过一次”。写循环的时候我建议你脑子里想清楚:你是要“彻底停下来”还是“只是这次不做”。
3. 数组:Java里最基础的“容器”
3.1 数组的声明、初始化与内存布局
学完流程控制,你手上有了“走逻辑”的能力,但还缺一个东西:怎么一次性管理一批数据。总不能定义一百个变量来存一百个学生的成绩吧?这时候就该数组出场了。数组就是Java里最基础的容器,你可以把它理解成一排编了号的格子,每个格子里存一个同类型的数据。
数组的声明有两种常见方式:
int[] scores = new int[5]; // 动态初始化:开辟5个int空间的数组,默认值是0 int[] numbers = {10, 20, 30, 40}; // 静态初始化:直接给定元素动态初始化的意思是,你告诉Java“我要一个长度为5的int数组”,Java就在内存里开辟一块连续空间,每个位置有一个默认值。int的默认值是0,boolean是false,引用类型是null。静态初始化则是你直接把元素值写出来,由编译器推断数组长度。
这里有一个非常重要的点,Java里面数组是一个对象、是引用类型。int[] scores = new int[5]这行代码实际做了两件事:在堆内存里创建了一个长度为5的数组对象,在栈内存里声明了一个叫scores的引用变量,让它指向这个数组对象。新手不理解引用是什么没关系,你只需要记住:数组变量存的是数组的“地址”,不是数组本身。这个理解对后面学面向对象、学集合框架非常关键,因为那时候你天天和引用打交道。
访问数组元素用下标:scores[0]、scores[1],下标从0开始,这一点无数人栽过跟头。如果访问scores[5](长度为5的数组最大下标是4),运行时会抛出ArrayIndexOutOfBoundsException数组越界异常。这个异常是入门阶段最常见的几个运行时报错之一,看到它第一反应就是检查下标是不是超出了范围0到length-1。
3.2 数组遍历、最值与冒泡排序
拿到数组之后,最基础的操作就是遍历。所谓遍历,就是挨个访问数组里的每个元素。配合for循环,遍历数组的标准姿势是:
int[] scores = {67, 88, 95, 72, 83}; for (int i = 0; i < scores.length; i++) { System.out.println("第" + (i + 1) + "个成绩是:" + scores[i]); }注意,获取数组长度用的是scores.length,后面没有括号。这一点和String类的length()方法不一样,数组的length是属性,String的length是方法调用。这两者敢搞混的人不在少数,而且IDE不一定会在编译期报错,因为有些场景下两者都能编过,只是逻辑不对,排查起来特别费劲。
Java还有一种增强for循环,也叫foreach,专门用来遍历:
for (int score : scores) { System.out.println(score); }foreach的好处是不需要关心下标,代码简洁;坏处是拿不到当前元素的下标。所以如果你需要知道“第几个元素”,还是老老实实用普通for循环。
数组最经典的练习题就是求最大值。思路是先把第一个元素设为临时最大值,然后遍历剩下的元素,遇到比它大的就更新,注意遍历不是非得从0开始,可以从1开始,因为第0个元素已经被当作初始值了:
int max = scores[0]; for (int i = 1; i < scores.length; i++) { if (scores[i] > max) { max = scores[i]; } } System.out.println("最高分是:" + max);然后是排序。排序里最入门、也最适合用来建立“算法感觉”的是冒泡排序。冒泡排序的名字很形象:每一轮比较中,大的元素就像气泡一样慢慢“冒”到数组末尾。它的核心思想是,从头开始两两比较相邻元素,如果前一个比后一个大,就交换位置;跑完一整轮,最大的元素就沉到了末尾;下一轮只需要处理前面剩余的元素,依次类推。
int[] arr = {5, 2, 9, 1, 6}; // 外层循环控制比较的轮数,总共要比较 arr.length - 1 轮 for (int i = 0; i < arr.length - 1; i++) { // 内层循环控制每轮的比较次数,随着轮数增加,参与比较的元素越来越少 for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } }我说一下为什么要引入第三个临时变量temp。交换两个变量的值,就像倒两杯水,你不可能直接把这杯水倒进另一杯水里而不洒出来,必须拿一个空杯(temp)中转:先把a的水倒进temp,再把b的水倒进a,最后把temp的水倒进b。这个“中转站”思路在编程里经常出现,以后做很多操作都会用到。
冒泡排序的时间复杂度是O(n²),在大数据量下性能很差,但它是理解排序思想的绝佳入口。很多面试题里也会让你手写冒泡排序,所以这个代码不仅要能看懂,最好能闭着眼默写出来。写的时候注意外层循环条件arr.length - 1,内层循环条件arr.length - 1 - i,这里面的两个减一是很多人容易搞错的地方,减一是因为两两比较时最后一对的下标范围是n-2和n-1,减i是因为每结束一轮,末尾就多一个已经排好的元素,不需要再参与比较。
4. 方法:把逻辑装进“可复用的盒子”
4.1 方法的定义与调用:返回值与参数
写了几十行代码之后,你会发现代码开始变得臃肿,同样的逻辑你得复制粘贴好几遍。这时候就该引入了方法。方法是Java里组织代码的基本单位,你可以把它理解成一个“盒子”,把一段有逻辑的代码装进去,起个名字,以后想用就调用一次,不用重新写一遍。类似于厨房里的菜谱,你不需要每次做菜都研究一遍油盐酱醋的比例,照着菜谱直接做就行。
定义一个方法的基本结构是这样的:
public static int add(int a, int b) { int result = a + b; return result; }拆解开来看,public static是修饰符,int是返回值类型,add是方法名,括号里的int a, int b是参数列表,花括号里是方法体。如果一个方法不需要返回任何值,返回值类型就写void,比如打印输出,就不需要return。两数相加的方法写完之后,在main方法里直接调用:
public static void main(String[] args) { int sum = add(3, 5); System.out.println(sum); }方法的调用流程是:main方法执行到add(3, 5)这一行,程序暂停main的执行,跳转到add方法里,把3和5分别赋值给参数a和b,然后执行方法体内的代码,拿到返回值之后,回到main中继续往下走。
每个方法功能的单一性很重要,一个方法只做一件事,不要大杂烩。比如“计算平均值并打印”就是一个不好的方法设计,因为它既做了计算又做了输出,以后你只想拿平均值不想打印,还得硬着头皮接受那次输出。好的设计是“计算平均值”这个纯逻辑方法,输出交给调用方自己决定。这一点是很多从入门到进阶都会迷茫的地方,但基础阶段先有这样的意识,以后写代码会顺很多。
参数传递是新手另一个容易糊涂的地方,Java的参数传递本质是值传递。如果传的是基本类型(int、double这些),方法内部怎么修改都不会影响外面;如果传的是引用类型(比如数组),因为传递的是地址值,方法内部修改了数组的内容,外面的数组会跟着变。比如你写一个方法交换两个整数,很多人会发现根本交换不了:
public static void swap(int a, int b) { int temp = a; a = b; b = temp; } // main里调用后,原来的a和b并没有换过来原因就是上面说的,a和b在方法里只是外部变量的副本,你在副本上怎么折腾,原件都不会变。但如果你传的是数组,通过下标去修改元素,那修改就会真实生效。这是Java里一个非常经典也是非常重要的知识点,面试也经常考,建议拿代码实际跑一遍,印象会深得多。
4.2 方法重载与递归:方法的进阶玩法
方法重载是一个看起来简单但特别实用的小概念。重载的意思是:同一个类里,方法名相同,参数列表不同。参数列表不同可以是参数个数不同,也可以是参数类型不同。比如你想写一个求和的方法,既能加两个数,也能加三个数:
public static int sum(int a, int b) { return a + b; } public static int sum(int a, int b, int c) { return a + b + c; }这里的sum方法就构成了重载。调用的时候,Java会根据你传的参数个数和类型,自动找到匹配的那个方法。重载让代码变得非常优雅,你只需要记住一个方法名,就能处理多种不同类型的输入。println方法就是一个典型的重载,它可以接收int、String、double、char等任何类型,因为底层给你重载了一大堆版本。
递归是方法调用自己的技巧。很多人在第一次接触递归的时候完全转不过弯来,我当初也不例外。递归解决的是一个大型问题拆成相同结构的小问题的场景。最经典的是求阶乘:n! = n * (n-1)!,而1! = 1。这个描述本身就是递归的。
public static int factorial(int n) { if (n == 1) { return 1; } return n * factorial(n - 1); }递归必须有一个明确的终止条件,也就是所谓的“出口”,否则会无限调用自己,最终栈内存溢出(StackOverflowError)。上面例子里,if (n == 1) return 1;就是出口。调用factorial(5)的执行过程是:5乘以factorial(4) → 4乘以factorial(3) → 3乘以factorial(2) → 2乘以factorial(1) → 1。然后一层层往回带,结果是120。
递归很惊艳,但实际工作中用的时候要克制。因为每调用一次方法,就会在栈内存里压一层栈帧,递归次数一多,内存开销非常大,性能远不如循环。而且递归写起来如果逻辑不清晰,极其容易出bug,排查难度还高。我的建议是,入门阶段理解递归思想、能写阶乘和斐波那契数列就够了,真正生产环境里能用循环解决的问题尽量用循环。
5. IDEA实操:这阶段一定要掌握的“四个操作”
5.1 工程结构、代码模板与常用快捷键
再好的理论知识,最终都要落到IDE上。既然你用的是IDEA,就得把IDEA用熟练,因为它在你的学习效率里占的比重比想象中大得多。
首先弄清楚IDEA里的三层层级关系:Project(工程)包含Module(模块),Module包含Package(包),Package里面才是Class(类)。很多人刚开始分不清,直接在一个工程里乱建Class,短时间内没问题,但学到后面搞模块化开发就乱了。建议你现在就养成习惯:一个新工程对应一个大的学习项目,工程下可以有多个模块或者包,包名一般用公司域名倒写,比如com.example.study,然后按功能再分子包。
代码模板是最能提升打字效率的东西。在IDEA的Java文件里,输入psvm回车,自动生成public static void main(String[] args) {};输入sout回车,自动生成System.out.println();输入fori回车,自动生成标准for循环骨架。还有数组遍历的itar、普通遍历的iter。这些字母组合就是所谓的Live Templates,你不需要刻意去背,用多了手自然就记住了。实测下来,这些模板对新手特别友好,能让你把注意力集中在逻辑而不是纠结于语法拼写上。
还有几个快捷键是我强烈建议现在就练成肌肉记忆的:
| 快捷键 | 作用 |
|---|---|
| Ctrl + D | 复制当前行到下一行 |
| Ctrl + Y | 删除当前行 |
| Ctrl + Alt + L | 格式化代码,自动整理缩进和对齐 |
| Ctrl + / | 注释/取消注释当前行 |
| Shift + F6 | 批量重命名变量、方法、类 |
| Alt + Enter | 智能提示修复,红波浪线时的“万能钥匙” |
特别说一下Ctrl + Alt + L格式化,这个快捷键被很多人忽略了,却是保持代码整洁的最快途径。你可能觉得代码能跑就行,但从刚开始就学会格式化,以后跟团队协作的时候别人看你的代码会轻松很多。如果你发现格式化失效,多半是快捷键被别的软件(比如某些输入法)占用了,去IDEA的Settings里Keymap搜索“Reformat Code”,重新绑定一下就行。
IDEA还分社区版(Community)和旗舰版(Ultimate)。如果你只是学Java基础,社区版完全够用,而且它是免费开源,从IDEA官网就能直接下载,根本不需要去找什么激活资源。旗舰版主要是多了Spring、数据库工具等企业开发功能,那是以后做实际项目时再考虑的事。入门阶段没必要纠结版本差异,装好社区版,把基础学好才是正事。
5.2 断点调试:程序员的第一把“放大镜”
学习基础语法阶段,很多人出bug就是瞪着屏幕干看,然后反复在代码里加System.out.println来猜问题。这种方式不是不行,但效率太低。真正好用的是IDEA自带的调试神器——断点调试(Debug)。
调试的思路是人肉模拟计算机的执行过程,一步一步看每一行代码运行后变量的变化。操作步骤很简单:在你怀疑出问题的行的行号旁边,用鼠标左键单击,打上一个红点,这就是断点。然后不要点那个绿色三角的Run按钮,而是点旁边的虫子图标(Debug按钮),程序会运行到你打断点的那一行停下来,这时候行号旁边会出现一个蓝色箭头,表示当前执行到这一行,但还没有执行这一行。
停下来之后,IDEA下方会出现Debug工具窗口,里面有Variables面板,你能实时的看到当前方法内所有变量的值。然后你可以用调试窗口的按钮控制程序往下走。最常用的是两个:F8是Step Over,一步一步执行当前方法内的下一行代码;F7是Step Into,进入当前行调用的方法内部去查看里面的执行过程。如果后面数组学完之后你发现循环逻辑有问题,比如要求1到100的和,结果算出来对不上,这时候你就可以在循环体里打断点,看每一轮循环里i和sum这两个变量是怎么变的,问题一目了然。
我建议你从这一阶段开始,就强制自己用Debug代替println来排查逻辑错误。Debug的意义不只是找bug,更是帮助你理解程序的执行顺序和方法调用过程。很多人到了面向对象阶段对“对象之间如何互相调用”感觉很抽象,其实用Debug一步步看过一遍,比看十遍文档都管用。
6. 这一阶段最常见的坑,我当年都踩过
6.1 报错信息看不懂?先分清“编译期”和“运行期”
这一阶段你会遇到两类报错,处理思路完全不同。第一类是编译期错误,IDEA会在代码旁边直接标注红色波浪线,你还没运行就能看到问题。这类错误大多是语法层面的:少写了分号、方法名拼错、变量类型不匹配、花括号没配对。编译期错误不可怕,IDEA的报错提示已经很友好,鼠标悬停在红色波浪线上就能看到具体原因,跟着提示改就行。
第二类是运行期错误,代码能编译通过,但一运行就崩,控制台会打印一大串红色异常信息。这一个阶段最常见的运行期异常有三个:
| 异常 | 原因 | 解决方法 |
|---|---|---|
ArrayIndexOutOfBoundsException | 访问数组下标越界,比如长度为5你访问下标5 | 检查下标范围,确保在0到length-1之间 |
StackOverflowError | 递归没有出口,无限调用自己 | 检查递归的终止条件,确保能结束 |
NullPointerException | 对null的对象调用了方法或访问属性 | 检查对象是否初始化,不要对null进行操作 |
看到运行期异常不要害怕,控制台里会指明异常类型和出错的行号,比如at Test.main(Test.java:12),直接翻到第12行附近找问题就行。这个“定位技巧”太实用了,新手常犯的错误是一看到红色报错就慌了,从头到尾读代码,唯独不去看异常信息里的行号提示,结果绕了一大圈冤枉路。
还有一个特别常见的编译问题就是前面提过的变量作用域。循环里面定义的变量,循环外面访问不到;方法里面定义的变量,方法外面也访问不到。Java的局部变量遵循“先声明,后使用”的原则,而且必须初始化才能用,否则编译器会直接报错。很多初学者在for循环里定义一个变量,想在循环外面用它,发现报错就一脸懵,其实就是你把它限定在了循环这个“小圈子”里,圈子外面当然看不见。
6.2 学习建议:这几个习惯越早养成越好
因为这套课程是零基础入门用的,我最后再根据自己的实际经验,给几条只有踩过坑之后才懂的学习建议。
第一个建议是:每一段示例代码,都亲手在IDEA里敲一遍,不要复制粘贴。复制粘贴的后果是你看了三遍依然不会写,只有自己敲过,哪怕报错个三五次,手和脑才能同步记起来。我带新人时发现,凡是遇到“这个代码我好懂,但让我自己写就卡壳”的情况,十有八九是懒出来的,敲得不够,没有形成肌肉记忆。
第二个建议是:写代码之前,先画流程草图,哪怕只是在草稿纸上画几个框和箭头。比如写一个猜数字游戏,先想清楚游戏流程:生成随机数、循环接收用户输入、判断大小并提示、猜对退出。把流程用文字或图梳理一遍,再动手写代码,成功率会高非常多。你会发现后期写复杂一点的程序,这个习惯价值巨大,相当于先设计图纸再盖楼,而不是拿砖头硬码。
第三个建议是:别怕报错,报错是你的好朋友。每一个红色的异常,其实都精准地告诉了你“你的代码哪里违反了什么规则”。我刚学Java那年,遇到报错就跑去找人问,后来才醒悟过来,自己先看报错信息、看行号、尝试理解原因,这个过程本身就是学习。等你处理过的报错超过几十个,你就对常见的错误类型和排查套路形成了条件反射,那时候你才真正算入门了。
我后来带过不少新人,发现那些在数组、循环、方法这个阶段肯花时间、敢敲代码、习惯Debug的人,后面学面向对象、学集合框架、学框架都格外顺畅。反而是那些急着往后赶进度、总想“先看一遍混个脸熟”的人,越学越吃力,最后回来补基础。所以别嫌这个阶段的内容“太基础”,这几十个小时的功夫,是你整个Java学习路上性价比最高的投资。