先聊点实在的。我见过不少Java新手,前几节课还跟着打印“Hello World”,一到方法和数组就开始蒙圈:明明代码照抄了,运行却报错;数组里存了5个数,遍历时却总越界;方法写了一大堆,调用时又说不存在。等我把这些代码拆开一看,本质上就两个问题没绕明白——方法到底是怎么调用的,数组在内存里又是怎么摆放的。这篇《初识java方法+数组从0开始》,就是专门帮你把这两座山一点点铲平。我会用最直白的方式,把方法定义、调用、参数返回值,数组初始化、遍历、越界这些关键点拆开讲清楚,还会配合完整的实战小例子,让你不光看得懂,还能自己动手写出来。不管你是完全零基础,还是学过一点但一直没打通,这篇都值得你花二十分钟认真过一遍。
1. 方法:把代码打包成可复用的积木
很多新手学方法之前,其实已经写过过程序,只是没有意识到方法早就参与了运行。比如在IDE里写下一句打印输出,本质上就是在调用别人写好的方法。方法这个词听起来抽象,但它的核心思想特别简单:把一段完成特定功能的代码装进一个“包装盒”,给它起个名字,以后想用这段代码的时候,直接喊名字就行,不用把盒子里面的东西重新写一遍。
1.1 为什么先学方法?从“重复写代码”说起
我先举个没有方法时的反面例子。假设你想计算三个长方形的面积,第一个长3宽5,第二个长4宽6,第三个长7宽2。在没有方法的情况下,代码会长成下面这样:
int area1 = 3 * 5; System.out.println("第一个面积:" + area1); int area2 = 4 * 6; System.out.println("第二个面积:" + area2); int area3 = 7 * 2; System.out.println("第三个面积:" + area3);这段代码能跑,但问题很明显:如果哪天老板说“面积计算规则改了,长和宽相乘之后还要除以2”,你就得去改三处,而且这三处散落在代码各个角落,漏改一处就出bug。这还只是三个长方形,如果是三十个、三百个呢?重复代码会变成一场灾难。
方法就是用来解决这个问题的。把“长乘宽”这条规则抽出来,放进一个方法里:
public static int calcArea(int length, int width) { return length * width; }以后不管在哪里需要计算面积,只要调用calcArea(3, 5),就能拿到15;规则变了,只需要改方法内部那一行,所有调用之处跟着一起生效。这就是方法最核心的价值:复用逻辑、隔离变化、让代码结构更清晰。
另外还有一个容易忽略的好处——方法给了代码一个名字。一段密密麻麻的代码如果没有方法名,别人阅读时只能一行行去猜逻辑。而当你写出calcArea、getMaxScore、printStudentInfo这样的方法名时,整个程序就像一本书的目录,可读性直接提高一个档次。
1.2 方法的定义与调用:基本语法拆解
方法定义的基本结构,从零开始拆解是这样的:
修饰符 返回值类型 方法名(参数列表) { 方法体; return 返回值; }我们对照一个例子来看:
public static int add(int a, int b) { int sum = a + b; return sum; }逐个部分说明一下。
public static是修饰符,现在先不深入理解它的全部含义,只需知道它是目前写主方法时最常见的组合,照着写就行。其中public表示方法对外可见,static意味着方法可以直接通过类名调用,不需要先创建对象。
int是返回值类型,表示这个方法运行结束后会返回一个整数。如果方法不需要给外界任何结果,返回值类型就要写void,比如:
public static void printHello() { System.out.println("Hello"); }add是方法名,建议用动词开头,看到名字就大概知道它做了什么。(int a, int b)是参数列表,相当于方法接收外部传入的数据。方法体里面写具体的处理逻辑,最后return把结果送出去。
调用方法也很简单,在另一个方法里写下方法名加括号,并传入实际的数据:
public static void main(String[] args) { int result = add(3, 4); System.out.println(result); }这里有个细节值得新手注意:定义方法时的参数叫形式参数,简称形参;调用时传入的具体值叫实际参数,简称实参。把实参传给形参的过程,就是参数的传递。
如果方法没有返回值,调用时直接写方法名加括号和分号即可。比如printHello();。千万不要画蛇添足写int x = printHello();,因为void方法根本没有值可以赋给变量,编译器会直接报错。
1.3 参数、返回值与重载:让方法真正灵活起来
一个方法光有固定逻辑还不够,它需要靠参数和返回值与外界的其他代码打交道。参数决定了方法的输入,返回值决定了方法的输出,有了这两样,方法才算是一个真正通用的“函数”。
先看多个参数的传递顺序。定义方法public static void showInfo(String name, int age),调用时showInfo("小明", 18),那么name就是"小明",age就是18。顺序必须一一对应,不能反过来。这看起来很简单,但真到了新手代码里,经常有人把两个参数位置搞反,导致逻辑表现非常诡异。
再看返回值。return有两个作用:一是结束当前方法,二是把后面的值返回给调用者。注意:return一执行,方法后面的代码全都不再运行。举个例子:
public static int check(int n) { if (n < 0) { return -1; } return n; }当传入-5时,执行到第一个return -1,方法立刻结束,后面的return n根本不会执行。这个特性在后面的代码逻辑里非常常用,可以当作提前退出条件来用。
方法重载也是一个重要概念。简单说,方法名相同,参数列表不同(类型不同、个数不同,或顺序不同),就可以定义多个同名方法。比如:
public static int add(int a, int b) { ... } public static double add(double a, double b) { ... } public static int add(int a, int b, int c) { ... }调用add(1, 2)的时候,编译器会根据传入实参的数量和类型,自动匹配最合适的那个方法。重载能减少起名负担,也让调用更自然。但注意,仅仅返回值类型不同,不算重载,编译器会判定为重复定义。
2. 数组:在内存里存一组同类型数据的容器
方法解决的是“逻辑复用”,数组解决的是“数据批量存储”。想象你手里有语文、数学、英语三科成绩,如果每次都单独定义score1、score2、score3,那当课程变成30门的时候,就无从下手了。数组的价值就是允许你用同一个变量名,顺着一组连续编号去访问多个值。
2.1 数组是什么?生活化类比
你可以把数组想象成一栋旅馆的连续客房。酒店名字就是数组名,门牌号从0开始编号,也就是下标或索引。每位客人的类型必须一致——要么全是单人间,要么全是双人间,不能混住。这个大家应该能理解:Java数组要求所有元素类型相同,像int[]里只能存整数,String[]里只能存字符串。
数组还有一个关键特征:长度固定。从创建那一刻起,它的容量就定了。就像一栋楼建好之后,房间数不会自动变多,你不能往里塞超出房间数的客人。这一点与后面学到的ArrayList形成鲜明对比,ArrayList可以自动扩容,但那是后话。至少现在,请牢牢记住数组长度不可变。
另一个容易混淆的点:数组变量名本身存放的是“对象的引用”,而不是数据本身。这句话现在不理解没关系,但你要知道一个现象:把数组变量赋给另一个变量,两个变量指向的是同一个数组,改其中一个,另一个也会跟着变。后面讲常见坑的时候,我会专门再强调。
2.2 数组的创建与初始化:三种常见方式
创建数组有三种最常见的姿势,我们一个一个来看。
第一种:先声明,再分配空间。
int[] nums = new int[5];这行代码做了两件事:声明了一个int[]类型的变量nums,同时用new int[5]在内存中开辟了一块能装5个整数的空间。此时数组里每个元素都是默认值0(int类型默认值是0,double是0.0,boolean是false,引用类型是null)。等后面有了具体数据,再逐个赋值:
nums[0] = 10; nums[1] = 20;第二种:创建时直接指定初始值。
int[] nums = new int[]{10, 20, 30, 40, 50};或者省略new int[],写成:
int[] nums = {10, 20, 30, 40, 50};这种写法最直观,适合创建数组时数据已经确定的情况。数组长度由大括号里值的个数自动决定,这里是5。
第三种:分步声明和初始化。
int[] nums; nums = new int[]{10, 20, 30};注意:只声明不初始化的情况下,一定不能用分步写法nums = {10, 20, 30};,这是语法错误。只有在大括号紧跟在声明语句的等号右边时,才能缩写。
还有一个小要点:数组下标从0开始,不是从1开始。所以new int[5]的合法下标范围是0到4。很多人第一次接触数组,脑子里的编号惯性还是“第一个是1号”,于是正好踩进越界陷阱。现在请立即建立条件反射:看到下标0,就想到它是数组里的第一项。
2.3 遍历、增删改查与数组长度陷阱
数组最重要的操作就是遍历,也就是把每个元素都访问一遍。最常见的写法是for循环配合下标:
int[] scores = {88, 76, 95, 60, 82}; for (int i = 0; i < scores.length; i++) { System.out.println("第" + i + "个成绩是:" + scores[i]); }这里有个老生常谈的坑:length是属性不是方法。字符串取长度要写str.length(),带括号;数组取长度却要写scores.length,不带括号。新手经常混用,编译器直接报红,但其实理解了就不容易混:数组是Java里的特殊对象,length是它的固有属性;字符串是普通对象,length()是方法。
遍历还可以用增强for,也叫for-each:
for (int score : scores) { System.out.println(score); }这种写法不需要管下标,代码更简洁,适合只需要读取元素、不需要修改元素位置的场景。但它也有短板:拿不到当前元素的下标,而且不能直接用循环变量给数组元素赋值——因为score只是数组元素值的一个拷贝。
“增删改查”里的“改”,通过下标直接赋值就能完成:
scores[0] = 99;“查”就是通过下标读取元素。而“增”和“删”对于数组来说,是最尴尬的部分,因为数组长度固定,不能真正追加或删除元素。实际开发中往往通过创建一个新数组并拷贝内容来模拟“增删”。这种操作逻辑初学者先不急着掌握,知道数组天生不适合频繁插入删除即可。
再强调一个实用小点:遍历时,循环条件的判断一定要用i < scores.length,而不是i <= scores.length。因为长度是5,下标最大值是4,一旦i等于5,再访问scores[5]就会触发ArrayIndexOutOfBoundsException,程序直接崩掉。这个问题几乎所有初学者都会遇到,我在后面专门给你列排查方法。
3. 方法与数组的实战组合:从小例子到完整代码
方法管逻辑,数组管数据,二者一旦组合起来,威力就开始显现了。下面我用一个模拟班级成绩统计的例子,把这两块知识串成一条完整的线。这个例子虽然小,但结构非常完整,足以让你看清楚一个合格的程序是怎么被拆分成多个小方法组织起来的。
3.1 场景:写一个统计班级成绩的小工具
假设有这样一个需求:一个班级有5个学生,成绩已经录入到一个int数组里,现在要分别打印出最高分、最低分、平均分和及格人数。看一眼需求,你会发现里面包含了四个相对独立的子任务:求最大值、求最小值、求平均值、统计及格人数。它们都可以写成单独的方法,然后由主方法统一调度。
这个场景特别适合初学者,原因在于它不涉及复杂的业务逻辑,数据载体就是数组,操作全是遍历加比较,正好能把前面讲过的数组操作语法和方法定义语法结合起来。而且,这种“把一个任务拆成多个方法”的思路,后面学面向对象、学项目开发时都会反复用到。
3.2 把实现拆成方法:每个方法只干一件事
遵循“单一职责”原则,我先把需求拆成四个方法。每个方法只做一件事,参数统一接收一个成绩数组,返回值根据需求不同有所区别。
第一个是求最高分的方法:
public static int getMax(int[] scores) { int max = scores[0]; for (int i = 1; i < scores.length; i++) { if (scores[i] > max) { max = scores[i]; } } return max; }第二个是求最低分的方法:
public static int getMin(int[] scores) { int min = scores[0]; for (int i = 1; i < scores.length; i++) { if (scores[i] < min) { min = scores[i]; } } return min; }第三个是求平均分的方法。因为平均分大概率是小数,返回值类型用double:
public static double getAverage(int[] scores) { int sum = 0; for (int score : scores) { sum += score; } return sum / (double) scores.length; }这里有一个新手容易忽略的细节:sum是int,scores.length也是int,两个整数相除的结果还是整数,60分和70分两个人平均,直接除会得到65没问题,但如果是三个人的成绩60、70、100,230 / 3会得到76而不是76.666...,因为整数除法会把小数部分直接丢掉。解决办法是先把其中一个操作数转为double,比如(double) scores.length,这样整个除法就是浮点运算了。
第四个方法负责统计及格人数,这里以60分为及格线:
public static int countPass(int[] scores) { int count = 0; for (int score : scores) { if (score >= 60) { count++; } } return count; }把逻辑拆开之后,主方法会非常清爽,读起来一目了然。这就是方法带来的代码组织能力,比把所有逻辑揉在一起强得多。
3.3 完整代码与逐行解读
把上面的方法汇总到一个类里,再写上main方法调用,整体如下:
public class ScoreStatistics { public static void main(String[] args) { int[] scores = {88, 76, 95, 60, 82}; int max = getMax(scores); int min = getMin(scores); double avg = getAverage(scores); int passCount = countPass(scores); System.out.println("最高分:" + max); System.out.println("最低分:" + min); System.out.println("平均分:" + avg); System.out.println("及格人数:" + passCount); } public static int getMax(int[] scores) { int max = scores[0]; for (int i = 1; i < scores.length; i++) { if (scores[i] > max) { max = scores[i]; } } return max; } public static int getMin(int[] scores) { int min = scores[0]; for (int i = 1; i < scores.length; i++) { if (scores[i] < min) { min = scores[i]; } } return min; } public static double getAverage(int[] scores) { int sum = 0; for (int score : scores) { sum += score; } return sum / (double) scores.length; } public static int countPass(int[] scores) { int count = 0; for (int score : scores) { if (score >= 60) { count++; } } return count; } }运行结果如下:
最高分:95 最低分:60 平均分:80.2 及格人数:4逐行解读一下关键位置。int[] scores = {88, 76, 95, 60, 82};创建数组并初始化。每次调用方法时,实参scores会被传递给方法的形参int[] scores。这里传递的是数组引用,但现阶段你可以简单想象成“把数组的地址递了过去”,方法内部操作的是同一个数组。getMax里面先把max初始化为第一个元素,然后从下标1开始遍历比较。为什么不是从0开始?因为没有比较的必要,第一个元素本身就已经是当前最大值了,从1开始能少一次无意义的比较。
getAverage中需要特别留意强制类型转换。return sum / (double) scores.length;如果漏掉(double),运行结果就会变成80.0,而不是80.2。初学者经常在这个地方踩坑,回头一看代码觉得没错,但结果就是不对。我把这个点单拎出来,是希望你能记住一个通用规律:只要涉及两整数相除,并且期望结果是小数,就一定先转成double再除。
这个完整的例子浓缩了方法和数组的大多数核心语法。建议你亲手把它敲一遍,然后自己修改数据,比如增加数组元素、换及格线、再加一个求总分的方法。动手改一遍,远比看十遍印象深刻。
4. 初学者最常踩的坑与排查技巧
方法和数组虽然基础,但坑一点都不少。下面这些问题是初学者出现频率最高的。我把它们集中整理出来,就是希望你在还没踩坑之前,先把这些雷的位置记住。
4.1 数组越界:最常见的运行期异常
先看一段经典错误代码:
int[] nums = {1, 2, 3}; for (int i = 0; i <= nums.length; i++) { System.out.println(nums[i]); }程序编译不报错,运行到i = 3访问nums[3]时,直接抛ArrayIndexOutOfBoundsException。异常信息往往会显示类似Index 3 out of bounds for length 3。看到这句话,第一反应就是下标越界。
排查思路很简单:找到报错行,确认访问的下标是几,再确认数组合法下标范围是0到length - 1。检查循环条件是<还是<=,这一点是最常见的元凶。另外,如果下标是从键盘输入或从别的方法传进来的,还要考虑输入值是否可能超出范围,必要时加个判断。防御性编程的思维,就是从这种小地方开始养成的。
4.2 方法返回值与无返回值混淆
新手容易犯两类错误。一是定义方法时写了void,却还在方法体里写return 5;,编译器立即报错。二是调用一个有返回值的方法,却把它当独立语句使用,比如:
public static int add(int a, int b) { return a + b; } // main方法里 add(3, 4);这样写语法没有错,但返回值没有被使用,等于白算。虽然程序能运行,但明显是设计上的疏漏。合理的写法应该是:
int result = add(3, 4); System.out.println(result);反过来,如果方法没有返回值,却强行赋值,比如:
int x = printHello();就一定会编译报错。看到类似incompatible types: void cannot be converted to int这样的提示,不要慌,先去看方法定义处的返回值类型是不是void,然后把赋值删掉或者改成正确调用。
4.3 引用传递与基本类型传递的误解
这是很多初学者最难绕的一个弯。先看两组代码。
第一组,方法接收一个基本类型int:
public static void change(int num) { num = 99; } public static void main(String[] args) { int x = 10; change(x); System.out.println(x); // 结果还是10 }第二组,方法接收一个数组:
public static void changeArray(int[] arr) { arr[0] = 99; } public static void main(String[] args) { int[] nums = {10, 20, 30}; changeArray(nums); System.out.println(nums[0]); // 结果是99 }同样的“传参”操作,为什么结果一个不变,一个变了?原因是Java中基本类型传的是值的拷贝,方法里怎么改都影响不到原变量;而数组传递的是引用,方法内部通过引用拿到了同一个数组对象,通过arr[0] = 99;修改的是数组本身,所以外部数据跟着变了。
这个知识点现在不一定能立刻理解透,但至少要知道这个现象。在此给你一个实用建议:如果方法需要修改数组内容,直接修改即可,因为改的就是原数组;如果方法需要修改基本类型变量,靠传参是做不到的,必须把修改后的结果作为返回值接收回来。
4.4 实操心得:调试方法和数组的好用小技巧
第一,善用println观察循环过程。在循环里加上输出语句,打印下标和当前值:
for (int i = 0; i < nums.length; i++) { System.out.println("i=" + i + ", nums[i]=" + nums[i]); }跑一遍就能很直观地看到数据走向,这种方法对付“循环不到头”和“结果莫名不对”非常有效。
第二,画一下内存盒子图。数组是引用类型,多画几遍盒子与下标的小图,能极大帮助理解越界和引用传递。我当年是从纸上画数组下标开始入门的,这比光看代码有效得多。
第三,把方法写短一点。一个方法最好只干一件事,行数控制在十几行以内,出了问题也最容易定位。如果你发现自己写的一个方法膨胀到几十行,先停下来想想能不能拆成几个小方法。
第四,尽量用增强for读取数组。只要不需要下标,用for-each能省掉很多越界风险,代码也更干净。需要修改元素或者需要下标时,才回到普通for循环。
第五,编译报错时先看错误行,再看错误类型。初学阶段遇到编译错误很正常,不要急着重写整段代码,先把鼠标移到红波浪线上,读一下提示。像cannot find symbol是在说你调的方法或变量名写错了,array required是在说你在普通变量上用了下标操作。把这些提示当成线索,比盲目复制他人代码有用得多。
我个人在实际学习和写代码的过程中发现,方法和数组这两块知识,最忌讳的就是只看不练。方法语法只有三条:定义、调用、参数传递;数组语法也只有几条:创建、初始化、遍历、下标操作。可一旦组合进真实场景,排列出来的问题就千奇百怪。把上面这个成绩统计例子亲手敲三遍,第一遍照抄,第二遍改数据,第三遍自己加一个“统计不及格人数”的方法,你对方法和数组的感觉就会完全不一样。等这两块地基打牢了,后面再学面向对象、集合框架、字符串处理,都会顺很多。