
JavaSE学到第三天我对这门语言最大的感受是知识点密度开始上来了。前两天还在跟变量、运算符、if和for搏斗今天要一次性接触两个重量级内容——数组和方法。这两个东西放在一天学压力不小但学完回头看它们恰恰是Java从“写点小逻辑”走向“组织复杂代码”的分水岭。这篇笔记适合两种人。一种是按JavaSE路线自学刚好学到数组和方法这一站的人另一种是学完了但觉得概念模糊、想系统复盘一遍的人。我会把Day03的核心内容完整梳理数组怎么声明、怎么初始化、内存里怎么存、常见操作怎么写方法怎么定义、参数怎么传递、重载怎么回事、递归怎么安全使用。另外把我自己踩过的坑和课堂上总结出来的易错点全部整理进来了当复习提纲完全够用。1. 数组让一批数据变得可控1.1 数组到底是什么为什么下标必须从0开始先从一个最简单的场景聊起。假设要存班里60个人的成绩按前两天学的思路就得定义60个变量score1、score2、score3……先不说定义的时候多手累后面要遍历、要排序、要传参连名字都起不明白。这就是数组出现的原因把同一类型的多个数据放到一块连续的内存空间里用统一的名字加下标访问。数组在Java里声明和初始化的方式有几种// 方式一声明并分配空间 int[] scores new int[60]; // 方式二声明并直接赋初始值 int[] scores2 {88, 92, 77, 65, 90}; // 方式三先声明后赋值new不能省 int[] scores3; scores3 new int[]{88, 92, 77};这里有几个细节新手特别容易忽略。第一数组的长度一旦确定就不能改变。这句话背起来容易真正踩坑是在后面想动态添加数据的时候动不动就下标越界。第二访问元素用的是下标而Java数组的下标从0开始。为什么从0开始因为数组名指向的是首元素地址访问第i个元素本质是“首地址 i × 元素占用字节数”从0开始就不需要额外做减法运算这是C语言时代就定下的规则Java沿用了。第三方式一里new出来的int数组60个位置默认全是0String数组默认全是nullboolean数组默认全是false。这个默认值规则后面判断空值的时候非常有用比如你new了一个对象数组还没赋值就能通过判断元素是否为null知道哪些位置是空的。1.2 数组在内存里是怎么放的栈、堆与引用很多人学数组最难理解的一环是数组变量到底存了什么。如果定义的是基本类型变量int a 10a的内存里直接存的就是数字10。但数组不一样。执行int[] scores new int[60]时实际发生了两件事。第一在堆内存里开辟一块连续空间能装60个int这块空间大小是60×4240字节。第二在栈内存里创建一个引用变量scores它存的不是数组内容而是堆空间的首地址。访问scores[0]是先拿地址找到这块堆空间再根据偏移量取值。这个“引用”的概念是很多Bug的根源。看这段代码int[] a {1, 2, 3}; int[] b a; b[0] 100; System.out.println(a[0]);执行完你会发现a[0]也变成了100。原因在于b a只是把地址复制了一份a和b指向的是同一块堆内存。这个坑几乎每个学员都会踩一次尤其在做数组复制、数组传参的时候。如果确实想得到内容一样的独立副本得用Arrays.copyOf或者手动for循环逐个复制。理解栈和堆的分工对后面学对象、学集合框架至关重要。数组是Java里第一个让你直观感受到“引用类型”的知识点现在把这个概念扎稳后面讲类、讲对象数组时就不会那么抽象。1.3 数组常规操作四件套遍历、求最值、逆序、查找遍历是数组最基础的操作一个for循环搞定for (int i 0; i scores.length; i) { System.out.println(scores[i]); } // 增强for循环只读场景更简洁 for (int score : scores) { System.out.println(score); }注意增强for循环里拿到的score是一个副本改它不会影响原数组。偶尔有同学在增强for里直接给元素赋值然后发现数组没变疑惑代码哪里错了这就是对副本机制不熟悉。求最值的思路也很经典。先假设数组第一个元素就是最大值然后从第二个开始依次比较遇到更大的就更新int max scores[0]; for (int i 1; i scores.length; i) { if (scores[i] max) { max scores[i]; } }为什么从i 1开始因为第0个元素已经作为初始值了再比一遍没有意义。虽然只省了一次比较但能让思路更清爽。逆序操作要注意的是交换时的临时变量。黄金三步int temp arr[i]; arr[i] arr[j]; arr[j] temp;。没有temp直接赋值会丢数据这个细节笔试里经常考。查找的话数据量小直接用线性查找一个一个比对数据量大要先排序再用二分查找。二分查找的思路不算难但边界条件很容易写成死循环这个放到后面的常见问题里细说。1.4 二维数组数组里套数组的正确写法二维数组可以理解为“数组的数组”常用于表格、矩阵这类场景。声明方式int[][] matrix new int[3][4]; // 3行4列 int[][] arr {{1,2,3}, {4,5}, {6}}; // 不规则数组重点想强调第二行这种不规则写法。Java的二维数组不要求每一行长度一样因为每一行实际上是一个独立的一维数组对象。这点跟很多其他语言不同C语言的二维数组是严格的行列矩形Java则允许每行各长各的这给存储稀疏数据提供了便利。遍历二维数组用双层forfor (int i 0; i matrix.length; i) { for (int j 0; j matrix[i].length; j) { System.out.print(matrix[i][j] ); } System.out.println(); }这里特别注意外层length是行数内层length是当前行的列数。我见过有人内层条件写成matrix.length遍历不规则数组时直接越界崩溃。还有一点二维数组在内存里也不是一整块矩形空间而是“一个装着多个一维数组引用的一维数组”这个理解后面用到ArrayList嵌套时会顺手很多。2. 方法把逻辑封装成可复用的积木2.1 方法的结构从main方法说起Java程序入口是main方法这大家都知道。但只会在main里写代码很快就会碰到问题代码一多main里全是逻辑一层套一层想改一个功能都不知道影响范围。方法解决的就是这个问题把一段有独立功能的代码块封装起来给它一个名字、定义好参数和返回值之后想用就调用名字。方法定义的标准格式修饰符 返回值类型 方法名(参数列表) { 方法体; return 返回值; }举个例子求两个整数和的方法public static int add(int a, int b) { return a b; }static是当前阶段必须带的因为在main方法里要直接调用它。等后面学到对象时你会理解static更完整的含义现在可以先把它理解为“属于类本身、可以直接用类名调用”的方法。方法名用驼峰命名动词开头比如calculateAvg、printArray这是Java社区的通用习惯。方法的本质是抽象。好比你去餐厅点菜不需要知道后厨怎么炒只需要告诉服务员菜名和忌口厨房给你端出成品。方法名就是菜名参数就是忌口要求返回值就是端上来的菜。把变化的东西设计成参数把结果设计成返回值方法就能在被复用的同时保持灵活性。2.2 值传递详解基本类型和引用类型的区别一张表看懂方法调用时参数到底怎么传这是Day03最容易被问懵的知识点。先说结论Java里只有值传递。基本数据类型传的是值的副本引用数据类型传的是引用的副本。这句话看起来不难但做题时错误率极高。看第一组例子public static void main(String[] args) { int num 10; change(num); System.out.println(num); // 输出10 } public static void change(int x) { x 20; }为什么输出还不是10因为调用change(num)时是把num里存的10复制了一份给xx 20只改了副本原num纹丝不动。再看第二组例子public static void main(String[] args) { int[] arr {1, 2, 3}; change(arr); System.out.println(arr[0]); // 输出100 } public static void change(int[] a) { a[0] 100; }这次数组内容变了。原因是a拿到的是arr的地址副本两个引用指向同一块堆内存通过a修改堆里数据arr自然看得到变化。这里有个经典误区如果change里写的是a new int[]{100, 200}那arr不会受影响因为此时a指向了新的空间和原来的数组失去了关联。传递类型方法内修改形参外部变量是否受影响原因基本类型x 20不受影响传递的是值的副本引用类型a[0] 100受影响传的是地址副本指向同一堆空间引用类型重新newa new int[]{...}不受影响引用指向了新对象这个表格建议抄在笔记本上。后面学面向对象时对象传参的规则和数组一模一样修改对象属性外部看得到变化把形参重新赋值为新对象外部看不到变化。2.3 方法重载同名方法为什么能并存方法重载的定义很简单同一个类里方法名相同、参数列表不同类型、个数、顺序任一不同就是重载。与返回值类型无关。这个“与返回值类型无关”是考核重点两个方法如果只有返回值类型不同编译器会直接报错因为单靠调用语句无法区分到底调哪个。看这个例子public static int add(int a, int b) { return a b; } public static double add(double a, double b) { return a b; } public static int add(int a, int b, int c) { return a b c; }三个方法同名但参数列表各不相同。调用add(1, 2)走第一个add(1.5, 2.5)走第二个add(1, 2, 3)走第三个。编译器根据实参的数量和类型在编译期就确定了该绑定哪个方法。重载最大的好处是调用方不用记一堆方法名。比如System.out.println为什么既能打印int又能打印double还能打印String因为它内部就是一堆print和println的重载。这给我们的启发是如果你的代码里出现了printInt、printDouble这类名字就该思考是否可以用重载统一。有一点要注意重载和重写是两个完全不同的概念重写是继承里的知识Day03先不用管但面试总喜欢把这两个放在一起问提前有个印象能少踩一次坑。2.4 递归方法三要素缺一不可递归是Day03最烧脑的内容没有之一。理解递归的关键是不要试图一层一层跟进去调试而是抓住两个要素递归公式和终止条件。再加上“递归调用要能向终止条件逼近”一共三要素。最经典的阶乘n!public static int factorial(int n) { // 终止条件 if (n 1) { return 1; } // 递归公式 return n * factorial(n - 1); }计算factorial(5)的过程是5×4×3×2×1。递归真正执行时是先一层层往下递递到终止条件再一层层往回归。所以递归需要栈来保存每一层的方法状态递归层数太多会栈溢出报错名叫StackOverflowError。写递归的第一个习惯就是先写终止条件没有终止条件就是死循环加栈溢出。第二个习惯是确认递归公式能逼近终止条件也就是每次递归调用都要让参数朝终止条件靠近。以阶乘为例每次调用n都减1最终一定会降到1触发终止。如果写成return n * factorial(n 1)那就是无限递归程序必然崩。递归适合处理天然有嵌套结构的数据比如文件目录、树形菜单、JSON嵌套解析。但线性问题能用循环解决的就用循环不要为了炫技用递归后面案例三会具体对比两者的性能差异。3. 实操环节数组与方法结合的三道经典题3.1 成绩统计案例方法封装让main清爽起来Day03的实操课老师让我们写一个成绩统计工具接收一个数组返回平均分、最高分和不及格人数。这个题目很好地串联了数组遍历、求最值、方法定义、数组传参。我的最终实现public class ScoreAnalyzer { public static void main(String[] args) { int[] scores {88, 92, 77, 65, 43, 90, 58}; double avg average(scores); int max max(scores); int failCount countFail(scores); System.out.println(平均分 avg); System.out.println(最高分 max); System.out.println(不及格人数 failCount); } public static double average(int[] arr) { int sum 0; for (int score : arr) { sum score; } return (double) sum / arr.length; } public static int max(int[] arr) { int max arr[0]; for (int i 1; i arr.length; i) { if (arr[i] max) { max arr[i]; } } return max; } public static int countFail(int[] arr) { int count 0; for (int score : arr) { if (score 60) { count; } } return count; } }这个案例里最值得说的是平均分的类型转换。sum是intarr.length是int如果直接写成sum / arr.length结果是整数除法比如305÷7得到43而不是43.57。必须先把其中一个转成double也就是代码里的(double) sum强制类型转换之后整个除法就变成浮点除法了。这是Day03最容易丢分的一个细节。另一个收获是main方法变得非常干净只负责接收结果和输出。每个功能独立成一个方法哪个逻辑有问题就单独调试哪个方法不用一头扎进几百行的main里找。把main写得清爽这个习惯从Day03就要养成后面学面向对象时一个类干一件事也是同一个思路。3.2 冒泡排序从能排对到排得高效排序是数组操作里绕不开的内容Day03一般先讲最简单的冒泡排序。冒泡排序的思路每一轮从头到尾两两比较相邻元素如果前一个比后一个大就交换这样一轮下来最大值会“冒”到最后面。外层循环控制总共要多少轮内层循环控制每轮比较到哪里。第一版我照着思路写for (int i 0; i arr.length - 1; i) { for (int j 0; j arr.length - 1; j) { if (arr[j] arr[j 1]) { int temp arr[j]; arr[j] arr[j 1]; arr[j 1] temp; } } }这个版本虽然能排对但每一轮都把所有的相邻元素比较了一遍。实际上第一轮结束最大的元素已经到了最后一位第二轮就不需要再比较最后两个了所以内层循环的上限应该是arr.length - 1 - i。优化后的代码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; } } }还可以再加一个标志位如果某一轮一次交换都没发生说明数组已经有序直接break。这种优化在面试里属于加分项虽然Day03不要求但理解了会让你对算法的感觉不一样。另外强烈建议自己动手推导一遍拿一个数组比如{5, 1, 4, 2, 8}把每一轮比较后数组长什么样都写在纸上。这个练习看着简单但对理解外层循环和内层循环的关系特别有效。纸上演算一遍比盯着代码看十遍都管用。3.3 斐波那契数列递归和循环同台对比斐波那契数列的定义是前两项是1从第三项开始每一项等于前两项之和。递归写法非常直观public static int fib(int n) { if (n 2) { return 1; } return fib(n - 1) fib(n - 2); }运行fib(10)能得到正确结果55但如果试一下fib(50)程序会卡到怀疑人生。原因很简单这个递归计算了大量重复子问题。算fib(50)需要fib(49)和fib(48)算fib(49)又需要fib(48)和fib(47)fib(48)被重复计算了好多次整个计算量是指数级增长。课堂上老师让我们试试用循环改写public static int fibLoop(int n) { if (n 2) { return 1; } int a 1, b 1; for (int i 3; i n; i) { int temp a b; a b; b temp; } return b; }两种写法对比一下你很快就能理解为什么说递归不一定是最优解。递归的优势是代码直观、符合数学定义但代价是额外的栈空间和重复计算。工程上能用循环解决的就用循环递归更适合处理树、目录这类天然有嵌套结构的数据。斐波那契还有第三种解法叫备忘录递归用一个数组把算过的值存起来避免重复计算。这个思路是动态规划的雏形哪怕Day03不要求掌握提前知道有这条路后面学算法时会轻松不少。4. 常见问题与踩坑实录4.1 最常遇到的五个报错现场第一个高频报错是ArrayIndexOutOfBoundsException数组下标越界异常。典型场景是定义int[3]却访问arr[3]因为合法下标范围是0到2。我自己就干过这种事定义了int[] arr new int[5]循环里一不注意写成i arr.length程序一跑就抛异常。后来养成了一个习惯凡是数组循环默认写i arr.length有明确理由才改成。第二个是NullPointerException空指针异常这个异常从Day03开始会大量出现。典型场景String[] names new String[3];然后直接调用names[0].length()此时names[0]是null对null调方法直接崩。遇到这个异常第一反应是检查引用有没有被赋值第二是检查数组对应位置是否为null。后面学到对象数组时这个检查习惯能救命很多次。第三个是编译错误提示“可能尚未初始化变量”。典型场景在for循环里定义了一个变量用来记录结果循环结束想在循环外访问但编译器认为循环可能一次都不执行所以变量可能没被初始化。解决方式是把变量声明挪到循环外面并给一个默认值。第四个是方法调用时实参和形参类型不匹配。这个一般编译期就能发现算是五类问题里最友好的一种。但要注意自动类型转换带来的隐形坑比如方法只定义了double参数你传int没问题编译器自动转了但反过来把double传给int参数就会报错因为大范围类型不能隐式转小范围。第五个是方法声明了返回值类型但某个分支没有写return。编译器提示“缺少返回语句”这是Java语言的强制要求。很多从其他语言转过来的同学不习惯这一点但这是好事它保证方法每个路径都有返回值调用方拿到的一定是有效数据。4.2 给Day03自学者的一套练习建议与自测清单Day03的内容量大但真正要练扎实的核心就三件事数组操作的手感、值传递的理解、递归的思路。我给自己的练习方法是每个知识点至少写两遍第一遍照着笔记敲第二遍完全合上书自己写第二遍卡壳的地方就是没真正掌握的地方。自测标准如下供参考能不能不看书写出数组求最大值、逆序、查找的代码能不能用一句话说清楚基本类型参数和引用类型参数在方法调用时的区别能不能在纸上画出方法调用时栈内存和堆内存的变化能不能一眼看出冒泡排序里每个变量的作用并说清楚为什么内层循环上限要减i能不能写完递归代码后立刻说出终止条件和递归公式如果这些都能做到Day03就算过关了。做不到也不用急数组和方法是整个JavaSE的地基后面学面向对象、集合框架时它们无处不在。特别是引用传递这个概念今天理解不到位后面学List传参、学对象数组会一直难受。最后再分享一个学习Day03时的小心得不要贪多不要想着一天把数组的所有操作都刷完。我第一次学这部分时一口气练了十多个小练习第二天全忘了。后来改成每个知识点配两个练习一个照着写、一个背着写反而扎实很多。数组和方法是Java新手第一次感受到“数据结构”和“代码组织”的地方慢慢磨比赶进度有用得多。