第8章 方法定义、重载与递归把重复的逻辑封装成方法函数是代码复用的第一步。本节学习方法的定义、参数传递、重载和递归。一、为什么需要方法写程序最怕重复代码。下面的代码求了两次和逻辑完全一样inta10,b20,c30;intsum1abc;intx1,y2,z3;intsum2xyz;// 又是同样的加法方法把这些逻辑封装起来一次定义处处调用intsum1sum(10,20,30);intsum2sum(1,2,3);重复代码的危害不只是多敲几行需求一变比如改为求和后再乘 2你得记得把所有重复的地方都改一遍漏一处就是 bug而用方法只需改一处。一个方法 输入参数处理方法体输出返回值想清楚这三件事方法就设计好了。二、方法的定义与调用语法修饰符 返回值类型 方法名(参数列表){// 方法体return返回值;}publicclassMethodDemo{publicstaticvoidmain(String[]args){intresultadd(3,5);// 调用方法传入实参System.out.println(result);// 8}// 定义一个加法方法staticintadd(inta,intb){// a、b 是形参intsumab;returnsum;// 返回结果}}关键点staticmain 中直接调用方法也要是 static静态方法后面学面向对象后大部分方法不用 static返回值类型int表示返回整数不需要返回值用void形参定义时的参数实参调用时传入的参数return结束方法并返回值void 方法中return;表示提前结束不需要返回值的场景用void方法只负责做事。return;可以提前结束 void 方法后面的代码不再执行staticvoidcheck(intscore){if(score0){System.out.println(分数不合法);return;// 提前结束下面不执行}System.out.println(分数score);}check(-5);// 分数不合法check(88);// 分数88命名建议方法名用小驼峰、以动词开头如addScore、printInfo。顺带一提方法体里声明局部变量时JDK 8 要求写出完整类型如果你用的是 JDK 11 或 JDK 17可以写var让编译器自动推断类型比如varresultadd(3,5);// JDK 11/17 写法等价于 int result add(3, 5);var只是少敲几个字类型在编译期就推断好了运行时没有任何区别。注意var只能用于局部变量方法参数、返回值类型这些位置还是要写明确类型。三、方法的调用过程调用方法时程序会暂停当前执行位置把实参的值传给形参执行方法体返回结果如有继续执行调用点后面的代码publicstaticvoidmain(String[]args){System.out.println(A);// Aintradd(3,5);// 跳到 add 执行System.out.println(r);// 回到这里输出 8System.out.println(B);// B}输出顺序A → 8 → B。调用栈Call Stack概念初学可以简单理解为谁调用我我执行完回谁那儿。方法里还可以再调方法一层层进去、再一层层回来最后从最里层开始逐层返回。四、参数传递值传递Java 只有值传递——传递的是值的拷贝staticvoidchange(intx){x100;// 修改的是拷贝}publicstaticvoidmain(String[]args){inta10;change(a);System.out.println(a);// 10a 没有被改变}基本类型传值方法内修改不影响原变量。但引用类型传的是地址值方法内通过地址修改对象内容原对象会变staticvoidchangeArr(int[]arr){arr[0]100;// 通过拷贝的地址找到了同一个数组}publicstaticvoidmain(String[]args){int[]a{1,2,3};changeArr(a);System.out.println(a[0]);// 100}简单记忆传基本类型原值不变传对象/数组内部属性可能被改。再深一层形参拿到的是实参的拷贝永远拿不到实参变量本身。基本类型拷贝数据引用类型拷贝地址——所以引用类型也有改了没用的情况当方法只是让形参换个指向时staticvoidchangeString(Strings){ss 后缀;// 形参指向了一个新字符串原变量不受影响}Stringname张三;changeString(name);System.out.println(name);// 张三没变区分改内容和换指向是关键通过拷贝的地址改内容原对象会变让形参指向别处和外部无关。五、方法重载Overload同一个类中方法名相同、参数列表不同类型、个数、顺序就是重载staticintadd(inta,intb){returnab;}staticintadd(inta,intb,intc){// 参数个数不同returnabc;}staticdoubleadd(doublea,doubleb){// 参数类型不同returnab;}调用时 JVM 根据实参的类型和个数自动匹配add(1,2);// 匹配 add(int, int)add(1,2,3);// 匹配 add(int, int, int)add(1.5,2.5);// 匹配 add(double, double)重载规则只看参数列表与返回值无关光返回值不同不算重载编译错误System.out.println()能打印各种类型靠的就是重载——传 int、double、String、boolean……编译器按实参类型自动挑一个合适的版本打印匹配时精确匹配优先找不到才尝试自动类型提升比如没有 long 版本时show(10L)会匹配 double 版本。六、可变参数参数个数不确定时用类型... 名字staticintsum(int...nums){inttotal0;for(intn:nums){totaln;}returntotal;}sum(1);// 1sum(1,2,3,4);// 10sum();// 0可变参数本质是数组可以传数组也可以传 0 到多个值sum(1, 2, 3)等价于sum(new int[]{1, 2, 3})编译器会自动把散落的实参打包成数组。可变参数可以和其他普通参数混用但必须是参数列表的最后一个。七、递归方法自己调用自己就是递归。递归必须有出口否则栈溢出StackOverflowError。经典例子n 的阶乘n! n * (n-1)!staticintfactorial(intn){if(n1){return1;// 出口1! 1}returnn*factorial(n-1);// 递归调用}factorial(5)5*factorial(4)5*4*factorial(3)5*4*3*factorial(2)5*4*3*2*factorial(1)5*4*3*2*1120递归的执行过程是先一路递下去再一路归上来以 factorial(3) 为例先算到 factorial(1) 触底返回 1再依次回代得到 2、6。斐波那契数列staticintfib(intn){if(n2){return1;}returnfib(n-1)fib(n-2);}递归的缺点重复计算多、栈深度有限。以 fib 为例fib(40)递归版要算几千万次而循环版瞬间出结果staticintfibLoop(intn){if(n2){return1;}inta1,b1;for(inti3;in;i){inttab;ab;bt;}returnb;}阶乘、斐波那契都能用循环改写性能更好。递归适合树形结构遍历、分治算法等天然递归的问题——写之前先想清楚出口是什么、每次调用是否向出口靠近。八、扩展知识1. 值传递 vs 引用传递的本质先看变量在内存里存的是什么基本类型int a 10; 栈a ──→ 10 数据直接存在变量里 引用类型int[] arr {1, 2, 3}; 栈arr ──→ 0x7f3a ──→ 堆数组对象 变量存的是地址调用方法时实参把自己存的东西复制一份交给形参a存的是 10 → 形参得到 10arr存的是地址 0x7f3a → 形参也得到 0x7f3a和arr指向同一个数组对象网上常说的引用类型传引用准确说是传了引用的拷贝。结论一句话形参永远拿不到实参变量本身只拿到它的拷贝。改拷贝不影响外部但引用拷贝是地址通过地址改对象内容外部看得到。2. 重载与返回值无关JVM 区分两个方法靠方法签名 方法名 参数列表返回值不参与所以只改返回值不可能产生新方法// ❌ 编译错误方法 get() 已定义只差返回值不算重载staticintget(){return1;}staticdoubleget(){return1.0;}3. 递归栈溢出StackOverflowError每次方法调用都会在内存栈上占一块栈帧空间栈的大小是有限的通常几 MB。递归没有出口栈帧无限增加最终把栈撑爆// ❌ 没有出口的递归staticvoidforever(){forever();}// 运行时报Exception in thread main java.lang.StackOverflowError就算有出口层数太深比如 factorial(100000)同样会栈溢出。所以写递归先想出口可能很深的问题用循环。4. 可变参数的本质int... nums在编译期就是一个int[] nums编译器负责把散落的实参打包成数组。三个要点传 0 个参数时得到的是一个长度 0 的数组不是 null必须放在参数列表最后不能和同类型的数组参数同时重载签名相同九、易错点1. 递归忘记出口 → 栈溢出// ❌ 没有出口无限递归staticintsum(intn){returnnsum(n-1);}// 运行时报 StackOverflowError// ✅ 先想好出口n 减到 1 就停staticintsum(intn){if(n1){return1;// 出口}returnnsum(n-1);}System.out.println(sum(5));// 152. 重载只改返回值 → 编译错误// ❌ 只有返回值不同编译器认为方法重复定义staticintmax(inta,intb){returnab?a:b;}staticdoublemax(inta,intb){returnab?a:b;}// 编译错误// ✅ 参数列表不同才算重载staticdoublemax(doublea,doubleb){returnab?a:b;}3. 局部变量与参数同名就近原则publicclassDemo{staticintcount100;// 成员变量staticvoidtest(intcount){// 参数也叫 count把成员变量遮住了System.out.println(count);// 这里用的是参数不是成员变量}publicstaticvoidmain(String[]args){test(5);// 5不是 100}}方法内出现同名时局部变量/参数优先。真想用成员变量写成Demo.count后面学了 this 还有别的写法。4. 基本类型实参传的是拷贝改了没用// ❌ 改的是拷贝外面不变staticvoidchange(intx){x100;}inta10;change(a);System.out.println(a);// 10没变// ✅ 用返回值把结果带出来staticinttenTimes(intx){returnx*10;}atenTimes(a);System.out.println(a);// 1005. int 方法必须有返回值路径// ❌ 有分支没 return编译错误staticintmax(inta,intb){if(ab){returna;}// b a 时没有返回值}// ✅ 所有路径都返回staticintmax(inta,intb){if(ab){returna;}returnb;}十、小结方法封装重复逻辑定义一次调用多次Java 是值传递形参拿到的永远是实参的拷贝基本类型改拷贝不影响外部引用类型通过拷贝的地址可以改对象内容重载 同名不同参数列表与返回值无关精确匹配优先其次自动类型提升可变参数本质是数组可以传 0 到多个值必须放参数列表最后递归必须要有出口注意性能与栈溢出问题能用循环改写就优先用循环方法签名 方法名 参数列表是 JVM 区分方法的依据下一节进入面向对象类与对象。下一篇第9章 面向对象类与对象