ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java方法从入门到实战:参数传递、重载与递归全解析

Java方法从入门到实战:参数传递、重载与递归全解析 1. 为什么所有初学者都要先啃透方法这块硬骨头如果你正在从零开始学Java前面几天应该已经写过不少代码了。但凡代码写到一定程度一定会遇到一个尴尬的场景同一段逻辑需要在好几个地方重复使用。比如你要写一个“判断一个数是不是偶数”的功能第一处写一遍第二处再写一遍第三处还得复制粘贴一遍。代码越写越长改需求的时候更是灾难——你改了第一处忘了改第二处跑出来的结果前后矛盾根本不知道问题出在哪。这个时候就会非常自然地引出一个问题能不能把这段逻辑“提取”出来变成一个独立的小模块想用的时候就调用一次这就是方法Method存在的意义。我见过不少初学者对方法的态度两极分化。一类是觉得方法很简单不就是包一层的函数嘛结果一写程序就漏洞百出——返回值和实际返回内容对不上、参数类型传错、方法里面直接改了外面的变量导致一堆奇怪的bug。另一类是觉得自己反正前面几天的代码都是写在一个main方法里直接跑的方法这玩意儿学了也不知道干什么用产生了“我为什么要学这个”的困惑。其实方法就是Java程序的基本组织单元。在真实的项目开发中没有人会把所有业务逻辑堆在main方法里——那种代码叫“屎山代码”后期维护的人会崩溃。举一个最简单的例子假设你要开发一个电商系统有下单、退款、确认收货三个功能每个功能都需要计算这单的实付价格。如果没有方法你得在三块地方各写一遍价格计算逻辑如果有方法你只需要写一个getActualPrice()三个地方分别调用即可。从今天开始你的代码会从“能跑就行”进入“写得好不好”的阶段而方法就是你掌握“写得好”的第一步。2. 方法定义与调用语法背后的几个关键决策点2.1 方法定义六要素的核心逻辑一段最标准的Java方法长这样public static int add(int a, int b) { int sum a b; return sum; }很多初学者看到这样一个方法脑子里第一反应是“我要把这一长串语法背下来”。但实际上方法定义的每个部分都有它必须存在的理由理解了这些理由语法自然就记住了。方法定义可以拆成六个部分访问修饰符public对外部能访问到这种方法的权限范围做限定的。public意味着所有地方都能通过类名或对象来调用private意味着只有当前类内部才能调用。静态修饰符static表示这个方法属于类本身而不是属于某个具体对象。这是初学者最容易搞糊涂的一个点。简单说带有static的方法可以通过“类名.方法名”直接调用不带有static的方法必须先new一个对象出来然后通过“对象名.方法名”来调用。返回类型int这个方法最终会返回什么类型的数据。如果你不想返回任何数据就写void表示“这个方法执行完了就完了不往回带任何东西”。方法名add调用方法时用的名称。命名通常是动词或动词加名词的组合比如calculateTotal、getUserInfo、sendEmail。参数列表int a, int b调用这个方法时需要从外部传入什么类型、什么数量的数据。参数就像是方法这个机器加工时需要的原料。方法体花括号里面的部分这段方法的核心逻辑描述它具体做什么事情。2.2 主方法与自定义方法的分工关系在Java中程序执行的入口永远是main方法public static void main(String[] args) { int result add(10, 20); System.out.println(10 20 result); }各位初学者可以把main方法理解为“总调度员”而所有自定义方法都是“执行具体任务的员工”。总调度员接收到外部指令也就是程序启动然后逐条分配任务给员工去执行。这里有一个很常见的理解误区。有些初学者以为自定义方法只能写在main方法上面或者只能写在main方法下面其实不然。对于同一个类内的静态方法在类内哪个位置定义都没有影响甚至可以先调用后定义Java编译时会自动找到方法的定义。但要跟大家强调一个代码规范层面的问题不要因为语法允许就把方法位置乱摆。我见过一些初学项目里main方法和十几个自定义方法交错排列读代码的时候上下翻跳特别痛苦。按照Java社区的通用惯例应该是main方法放在最前面解决“程序从哪里开始运行”的问题自定义方法按功能逻辑排在后面。2.3 三条最容易出问题的调用规则方法定义好之后就是如何正确调用了。调用方法比定义方法容易踩坑我列出三条初学者几乎必踩的坑void方法直接写在表达式里。返回值类型是void的方法执行完就完了你不能强行把它赋值给某个变量。就像你跟朋友说“帮我把门关上”你不能紧接着问“顺便把这个结果给我算算”。比如public static void printMessage(String str) { System.out.println(str); } // 下面这样写会直接编译报错 // String s printMessage(hello);参数顺序和类型必须匹配。如果方法定义时参数是(int a, int b)调用的时候就不能传两个字符串进去也不能传一个int一个double——除非后面讲到的自动类型转换和重载规则允许。在初学阶段最容易犯的是把方法定义里的参数顺序记反调用的时候把b当a传了。return的执行顺序。如果你在return语句后面还写了其他语句编译器会直接报“无法访问的语句”错误。这背后的逻辑很好理解方法一旦碰到return就把值返回给调用方方法的生命周期在那一刻就结束了后面的代码永远不会执行那写它干嘛比如public static int test(int x) { if (x 0) { return x; } return 0; // System.out.println(永远不会执行); // 这行编译都无法通过 }3. 参数传递的本质为什么Java里其实只有值传递3.1 基本数据类型参数的内存表现“Java方法参数传递到底是值传递还是引用传递”这个问题几乎是各种Java面试里出现频率最高的问题之一但很多初学者还没到面试阶段就已经被绕晕了。我先直接给结论Java里只有值传递没有引用传递。这句话怎么理解当你把一个基本类型变量比如int类型作为参数传给方法时方法拿到的是这个变量值的“一份复印件”。方法内部对这个复印件做任何修改都影响不到原件。看代码public static void main(String[] args) { int num 10; changeValue(num); System.out.println(num); // 输出还是10而不是200 } public static void changeValue(int x) { x 200; }这个过程用生活场景类比就是你把一份重要文件的复印件递给了别人别人在复印件上乱涂乱画原件完好无损。对于这个现象从内存层面也很好解释——调用方法时Java会在栈内存中为新方法分配一块独立的栈帧空间参数x就在这块新空间里它和main方法里的num是两块完全独立的内存互不影响。3.2 引用数据类型参数的表现与常见误解现在到了很多书里最容易产生误导的地方。当我们把一个引用类型变量比如数组、对象作为参数传入方法时会出现这样的现象public static void main(String[] args) { int[] arr {1, 2, 3}; changeArray(arr); System.out.println(arr[0]); // 输出竟然是99 } public static void changeArray(int[] array) { array[0] 99; }很多初学者看到这里会得出“这不就说明Java存在引用传递吗你看方法里改了数组外面的数组也跟着变了”。这里有非常关键的一个认知环节。你传给方法的并不是对象本身——Java永远不把对象本身作为参数传递给它。你真正传过去的是“指向对象的那个地址值”也就是引用变量里存的地址。而这个地址值依然是按值传递的。实际上在调用changeArray(arr)时发生的是main方法里的引用变量arr的值一个地址被拷贝到了changeArray方法的参数array中。这时候arr和array指向了堆内存中的同一个数组对象。方法里通过array[0] 99修改的是堆里的那个真实对象所以main方法里通过arr再看数组内容当然变了。3.3 区分“修改引用指向”与“修改对象内容”很多初学者听了上面的解释之后又会产生一个看起来很合理的想法既然两个引用指向同一个对象那我如果在方法里把参数重新指向一个new出来的新对象外部是不是也会跟着换答案是不会。这正是值传递的典型证据。看这个例子public static void main(String[] args) { int[] arr {1, 2, 3}; rebindDemo(arr); System.out.println(arr[0]); // 输出还是1 } public static void rebindDemo(int[] array) { array new int[]{100, 200, 300}; }方法里array new int[]{100, 200, 300}这一行的本质是让局部变量array不再指向原来的对象而是指向了堆中一个新创建的对象。但这只是修改array这个局部变量里存的地址值它影响的只是方法内部的局部副本。外部的arr仍然指向那个老对象。你可以把引用变量理解成一个“带地址的便利贴”你复印了一张便利贴给别人别人在自己的便利贴上改成新地址你手上那张便利贴还是老地址。我把这两者对比放在一个表里方便大家记忆操作类型基本类型参数引用类型参数方法内重新赋值外部完全不受影响外部不受影响只是局部引用换指向方法内修改对象内容不存在这种操作外部能看到修改因为指向同一个对象方法内做计算再返回需要接收返回值才能拿到结果也需要接收返回值或者直接改对象内部搞懂了这套内存规则后面学习集合、泛型、甚至ORM框架时排查奇怪bug的能力会直接上一个台阶。4. 方法重载同一种动作不同的“打开方式”4.1 为什么要设计重载机制先设想一个没有重载的编程世界。假设你要写一个加法的工具方法但有时候是两个int相加有时候是两个double相加有时候是一个int一个double相加。没有重载的话你得给这些方法起不同的名字比如addIntInt、addDoubleDouble、addIntDouble调用方用起来还得时刻记住“我现在加的两个数是什么类型我应该调用哪个方法”。这个体验极其糟糕写代码的人累调用代码的人更累。重载机制的诞生就是解决这个矛盾的方法名不变参数列表不同由编译器根据调用时传入的参数类型和个数决定具体调用哪个版本的方法。这就是多态性在编译期的一种体现。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)编译器看到两个int参数自动匹配第一个方法写add(1.0, 2.0)自动匹配第二个写add(1, 2, 3)自动匹配第三个。你不需要操心方法名怎么区分代码的可读性大大提高。4.2 重载的判定规则与常见的编译陷阱重载的判定规则有三条初学者只需要记住参数个数不同可以构成重载。参数类型不同可以构成重载即使个数相同。参数顺序不同也可以构成重载理论上可行但实际开发中不建议这样写很容易让人混淆。重点注意仅返回值类型不同不构成重载。只改返回类型、不改变参数列表编译器会认为你在重复定义方法public static int calculate(int a, int b) { return a b; } // 编译报错同一个类中已有方法calculate(int, int) // public static double calculate(int a, int b) { // return a b; // }为什么Java不允许只靠返回值区分重载因为调用方法的时候存在一种场景我只关心方法执行过程根本不用接收返回值。例如calculate(1, 2); // 直接调用不接收结果这条调用语句本身没有上下文编译器无法判断用户到底想调用返回int的那个版本还是返回double的那个版本。因此Java语言设计者干脆禁止仅靠返回值类型区分重载避免编译器陷入二义性。4.3 重载与自动类型转换的顺序再给各位进阶读者讲一个踩坑概率很高的问题当我们调用一个重载方法而传入的参数类型正好“哪个版本都不完全匹配”时编译器会怎么选这里涉及一个默认的类型升级路径。举个实际例子public static void print(byte num) { System.out.println(byte 版本); } public static void print(int num) { System.out.println(int 版本); } public static void print(long num) { System.out.println(long 版本); } // 调用 print(10);这里10的默认类型是int所以编译器会直接匹配int版本。那如果只有byte版本和long版本没有int版本呢这时候编译器不会去“降级”匹配byte版本而会选择long版本。因为Java的自动类型转换只会把容量小的类型自动转成容量大的类型它遵循朝向int、long、float、double的自动提升规则。类似的坑还体现在实参传null调用重载方法时public static void show(String str) { System.out.println(String 版本); } public static void show(Object obj) { System.out.println(Object 版本); } // 调用 show(null);Java编译器会选择更具体的类型也就是String版本。但如果我再加一个public static void show(int[] arr)show(null)会直接变成编译错误——因为String和int[]不存在自动“谁更具体”的关系编译器无法判定。这种细节平时自己写着玩可能碰不到但真到了面笔试基础或者同事写的工具库出诡异问题时肚子里有点这种底层知识会很从容。5. 递归方法优雅但“烧栈”的写法5.1 递归的两个铁律递归方法是指方法体内直接或间接调用方法自身。它和前面所有内容有种完全不同的思考方式——你不再把问题看成一步步执行的过程而是把一个大问题拆解成“规模更小但结构相同”的子问题直到子问题小到可以直接给出答案。所有正确的递归都满足两条铁律有明确终止条件。这个条件通常叫递归出口当参数满足这个条件时方法直接返回结果不再调用自身。每次递归调用都在向终止条件靠拢。如果不满足这条程序就会陷入无限递归最终抛出StackOverflowError——栈内存被递归调用撑爆了。5.2 阶乘案例的完整执行拆解理解递归最有名的案例就是阶乘。一个非负整数n的阶乘记作n!定义为n * (n - 1) * (n - 2) * ... * 1且规定0!等于1。用递归来实现代码极其简洁public static int factorial(int n) { if (n 0) { return 1; } return n * factorial(n - 1); }为了让大家看懂这段代码到底怎么执行我以factorial(4)为例把完整的调用过程写在下面每一行缩进代表一层新的栈帧调用 factorial(4) 4 不等于 0进入 return 4 * factorial(3) 调用 factorial(3) 3 不等于 0进入 return 3 * factorial(2) 调用 factorial(2) 2 不等于 0进入 return 2 * factorial(1) 调用 factorial(1) 1 不等于 0进入 return 1 * factorial(0) 调用 factorial(0) 0 0返回 1 计算 1 * 1 1返回给上一层 计算 2 * 1 2返回给上一层 计算 3 * 2 6返回给上一层 计算 4 * 6 24返回给最外层整个过程类似于“从外到内一层层打入打到最深处拿到答案后再一层层返回来”。这个过程中每次方法调用都会在栈中分配一个栈帧直到触底再逐层返回释放。我给想加深这块理解的读者一个小建议自己拿纸笔画一下这种“栈式推进、栈式回退”的调用过程比看十遍代码都有用。很多刚开始学递归的同学觉得递归难难就难在脑中没有形成栈空间的具体画面。5.3 递归 vs 迭代别把优雅写成灾难说完原理必须聊一个很现实的话题递归并不是所有场景的最优解。以阶乘为例递归思路简洁、最容易理解但性能并不好。每次递归调用都要创建新的栈帧、保存现场的局部变量函数的调用是有成本的。而且一旦给的n比较大比如factorial(100000)无限递归当然非常危险就算没有无限递归正常递归也会在n达到几千上万时直接StackOverflowError栈默认大小有限不同JVM和操作系统有差异但远不足以支撑无限深的递归。同类问题用迭代写往往效率更高更安全public static int factorialLoop(int n) { int result 1; for (int i 1; i n; i) { result * i; } return result; }那递归有没有真正发挥价值的地方有。它的优势在于“代码结构清晰”尤其在树形结构遍历比如二叉树的前序、中序、后序遍历、目录结构遍历这类自然具有递归结构的问题上迭代写起来头发都要掉光递归则几行搞定。所以这作为一个经验的话能用迭代解决的简单数学问题优先用迭代遇到具有自然递归结构的数据处理树、图、嵌套目录应该毫不犹豫地选择递归。6. 可变参数与其他进阶参数形态6.1 可变参数的行为特性说完了递归再讲一个日常开发中出场率不低、但很多初学者压根不知道的功能可变参数。有时候方法需要接收的数据个数不确定比如写一个求任意多个整数之和的方法。Java在JDK 5之前只能传数组JDK 5之后引入了可变参数public static int sum(int... numbers) { int total 0; for (int num : numbers) { total num; } return total; }可变参数的本质其实是一个语法糖。int... numbers在编译后会变成一个int[] numbers参数。但这个语法糖让调用方式友好得多int a sum(1, 2); int b sum(1, 2, 3, 4, 5); int c sum(); // 可以不传任何值此时numbers是长度为0的数组total返回0可变参数左边必须是0到多个该类型参数。使用可变参数时有一个重要的放置规则每个方法只能有一个可变参数且必须放在参数列表的最后。比如public static void method(String str, int... nums)可变参数int... nums放在String str后面这样调用时能明确区分哪个参数对应到哪里。如果你写成public static void method(int... nums, String str)编译器直接报错。有了可变参数调用方法的体验确实方便很多。但内部实现依然是一个数组所以方法体内部对它的处理和对数组的处理没有任何区别。6.2 命令行传参程序“被外部指挥”的通道除了自己写在代码里调用方法还有一种特殊情景通过命令行向main方法传参。main方法的签名是public static void main(String[] args)其中args是一个字符串数组用来接收用户在命令行启动程序时携带的参数。假设你的类文件编译后叫TestApp.class在终端这样写java TestApp hello world 123那么args数组的内容就是args[0] helloargs[1] worldargs[2] 123注意所有命令行传入的值Java都会当作字符串处理。就算你传了123它也是字符串123想当数字用必须自己转。这种做法在初学阶段有点用主要让你理解程序如何接收“外部信息”作为输入。实际项目中命令行参数更多是被打包脚本、运维平台直接使用的初学者做个了解就好。6.3 方法体内部的变量生命周期每写一个方法方法体内部都会有一个自己的局部变量空间。这里要注意局部变量和类变量在存放位置上有着本质区别局部变量存放在栈内存中而且只能在当前方法内使用。方法一旦执行结束对应的栈帧就会被弹出局部变量也就随之销毁。所以以前面文章中学过的变量生命周期做一个延伸就会出现一个典型报错场景public static int test() { int x 100; // 方法执行完x就没了 } public static void another() { // System.out.println(x); // 编译报错找不到符号 }不同方法内部声明的变量即使同名也互不干扰。比如方法A里有个int count方法B里也有个int count两者完全独立各自在自己方法的栈帧里生存。这也印证了前面参数传递章节的内容——方法之间的数据流通只能通过参数传入和返回值带出两种途径。7. 方法级开发规范从“写得对”到“写得好”在Java中写一个能跑的方法很容易写一个让人一眼看懂、好维护的方法却不容易。这块内容在很多从0开始的教程里往往一笔带过但恰恰是初学者从“码代码的”走向“写软件的”的重要转折点我把自己在实际项目里总结出的几条朴素经验分享给大家。第一方法命名要动词开头、语义清晰。Java社区的命名习惯里方法名通常是“动词”或“动词名词”组合。比如getUserName、sendEmail、calculateTotalPrice看名字就能猜到它干了什么。不要用aaa、bbb、test1、test2这样的名字哪怕你能看懂自己三天前的代码别人接手时一定会想骂人。第二一个方法只做一件事。如果你的方法名里有“和”“并且”这类意思说明它承担了太多职责。典型例子一个名叫processDataAndSendEmail的方法既做数据处理又做邮件发送一旦线上出问题你很难定位到底是哪个环节挂了。拆成两个方法单一职责排查问题会容易得多。第三参数数量尽量精简。方法参数过多比如超过四五个调用时很容易传错或漏传。这在工程上有个经典替代方案把相关参数封装成一个对象或实体类作为参数传入。比如你有一个方法需要传商品名称、价格、库存、分类四个参数可以考虑定义一个Product内部类把这些字段封装起来方法只接收一个对象。第四方法体的代码长度要克制。我看过一些初学者的代码一个方法里有上百行有if分支、有switch、有本可以抽出来的循环逻辑。判断方法是否需要拆分的简单标准是如果你看这个方法需要来回滚动页面才能看到全貌就说明它应该被拆分成多个小方法了。每个小方法负责一个逻辑片段主方法只做“串流程”可读性会大幅提升。第五递归方法的栈深度问题在前面提到了写递归时一定要先想清楚终止条件能否覆盖所有输入。很多初学者只想到“正常的输入”而忽略了极端情况导致调用时直接栈溢出。养成“边界值思维”——0、负值、最小值、最大值、null、空数组这些情况在你的方法里会怎样表现这些通常比写对主流程更能体现一个人的编码功底。8. 从方法出发沿着Java学习路线继续向前到这里你已经掌握了Java方法的完整骨架方法的定义与调用、参数的值传递本质、重载与递归机制以及从“能写”到“会写”的方法级代码规范。但别把方法当做一个孤立的语法点来学。它和后面的很多知识点是强关联的。举个例子等学到面向对象部分你会看到方法这个概念会被反向“包装”进来——构造方法用于创建对象时初始化数据getter/setter方法用来读写对象私有属性静态方法属于类而不属于对象抽象方法和接口方法定义了子类必须实现的行为契约。如果今天的方法基础打不牢到面向对象阶段再补成本会成倍上升。我的建议是学完这篇内容先趁热手写几个综合小练习不用追求复杂重点是覆盖今天提到的所有知识点写一个方法接收一个整数数组返回数组中的最大值和最小值可以用int[]类型作为返回结果或者用可变参数。利用方法重载分别实现两个整数、三个整数、两个小数之间的求和并验证调用时的匹配顺序。写一个递归方法计算n个台阶的走法每次可以走1阶或2阶有多少种不同走法。这个问题的递归方向是f(n) f(n - 1) f(n - 2)出口是f(1) 1f(2) 2写完用笔和纸模拟一遍f(5)的完整递归过程。实现一个包含可变参数的工具方法可以接收任意多个人名然后打印出“你好xxx”。这些练习都是我自己带新人时反复用过的看着简单但能把参数传递、返回值、重载匹配、递归思维这些点全部串起来。把方法学透后面进入类和对象的世界你会感觉顺滑无比。
返回列表