JavaSE之可变参数、递归、数组算法与参数传递全解析
JavaSE之可变参数、递归、数组算法与参数传递全解析
一、可变参数:灵活处理不确定个数的参数
1.1 可变参数概述
在实际开发中,经常会遇到「参数类型确定,但参数个数不确定」的场景(例如:实现多个整数相加、多字符串拼接)。可变参数正是为解决这类问题而生,它允许方法接收任意个数的同类型参数,底层本质是数组,但调用时无需手动创建数组,极大简化了代码。
核心要点:
- 定义格式:
数据类型... 变量名
(例如int... nums
、String... strs
) - 本质:编译后自动转换为数组(
String... strs
等价于String[] strs
) - 使用限制:
- 一个方法中只能定义一个可变参数
- 可变参数必须位于参数列表的最后一位(避免参数歧义)
1.2 可变参数实战练习:字符串拼接
需求:定义方法,用指定分隔符拼接多个字符串(如用 -
拼接 [\"张无忌\",\"赵敏\",\"小昭\",\"小华\"]
,结果为 张无忌-赵敏-小昭-小华
)。
public class Day08Train { public static void main(String[] args) { // 调用可变参数方法,直接传递多个字符串,无需手动创建数组 String str = concat(\"-\", \"张无忌\", \"赵敏\", \"小昭\", \"小華\"); System.out.println(str); // 输出结果:张无忌-赵敏-小昭-小華 } /** * 字符串拼接方法 * @param s 分隔符(固定参数,位于可变参数前) * @param arr 待拼接的字符串数组(可变参数) * @return 拼接后的完整字符串 */ public static String concat(String s, String... arr) { String str = \"\"; for (int i = 0; i < arr.length; i++) { // 最后一个元素后不添加分隔符 if (i == arr.length - 1) { str += arr[i]; } else { str += arr[i] + s; } } return str; }}
二、递归算法:自己调用自己的解题思路
2.1 递归概述
递归是一种特殊的编程思想,指方法内部直接或间接调用自身,核心是将复杂问题拆解为「与原问题结构相同但规模更小的子问题」,直到子问题可直接解决(即「递归出口」)。
递归分类:
- 直接递归:方法自身直接调用自己(如
method()
中调用method()
) - 间接递归:多个方法循环调用(如
A()
调用B()
,B()
调用C()
,C()
调用A()
)
关键注意事项:
- 必须有明确的递归出口:否则会陷入无限递归,导致「栈内存溢出(StackOverflowError)」
- 控制递归调用次数:即使有出口,若调用次数过多(如递归深度超过 1 万次),也会触发栈溢出
2.2 递归经典练习
练习 1:利用递归输出 3 到 1
需求:从指定数字 x
倒序输出到 1,递归出口为 x == 1
。
public class RecursionTest1 { public static void main(String[] args) { method(3); // 输出结果:3 2 1 } public static void method(int x) { if (x == 1) { System.out.println(x); return; // 递归出口:x=1时终止调用 } else { System.out.println(x); x--; method(x); // 递归调用:方法自身调用,参数规模减小 } }}
练习 2:求 n 的阶乘
阶乘定义:n! = n × (n-1) × (n-2) × ... × 1
(如 5! = 5×4×3×2×1 = 120
),递归出口为 n == 1
(因 1! = 1
)。
public class RecursionTest2 { public static void main(String[] args) { int result = factorial(5); System.out.println(result); // 输出结果:120 } public static int factorial(int n) { int sum = 0; if (n == 1) { return 1; // 递归出口:n=1时返回1 } else { sum = n * factorial(n - 1); // 递归逻辑:n! = n × (n-1)! } return sum; }}
练习 3:计算斐波那契数列第 n 个值
斐波那契数列规律:从第 3 项开始,每一项等于前两项之和(如 1, 1, 2, 3, 5, 8, 13...
),递归出口为 n == 1
或 n == 2
(前两项均为 1)。
public class RecursionTest3 { public static void main(String[] args) { int result = feibo(6); System.out.println(result); // 输出结果:8(第6项为8) } public static int feibo(int n) { int x = 0; if (n == 1 || n == 2) { return 1; // 递归出口:前两项均返回1 } else { x = feibo(n - 1) + feibo(n - 2); // 递归逻辑:第n项 = 第n-1项 + 第n-2项 return x; } }}
三、数组算法:反转、排序与查找实战
数组是 Java 中最常用的数据结构之一,反转、冒泡排序、二分查找是数组操作的经典算法,需熟练掌握其逻辑与实现。
3.1 数组反转
需求:将数组中「对称索引位置的元素互换」(如 [1,2,3,4,5]
反转后为 [5,4,3,2,1]
)。
public class ArrayReverse { public static void main(String[] args) { int[] arr = {1,2,3,4,5}; FanZhuan(arr); // 调用反转方法 } /** * 数组反转方法 * @param arr 待反转的数组 */ public static void FanZhuan(int[] arr) { int arr1[] = new int[arr.length]; // 创建新数组存储反转后的数据 for (int i = 0; i < arr.length; i++) { // 对称索引:原数组索引i 对应 新数组索引arr.length-1-i arr1[i] = arr[arr.length-1-i]; } // 方式1:普通for循环打印反转后数组 for (int i = 0; i < arr1.length; i++){ System.out.println(arr1[i]); // 输出:5 4 3 2 1 } // 方式2:增强for循环打印(更简洁) for (int i : arr1){ System.out.println(i); // 输出:5 4 3 2 1 } }}
3.2 冒泡排序
冒泡排序核心逻辑:通过相邻元素的多次比较与交换,将最大元素逐步“冒泡”到数组末尾,每轮排序后,未排序区间的最大元素确定位置。
关键规律:
- 排序轮数:
arr.length - 1
(n 个元素需 n-1 轮,最后一个元素无需排序) - 每轮比较次数:
arr.length - 1 - i
(i 为当前轮数,已排序的元素无需再比较)
public class BubbleSortDemo { public static void main(String[] args) { int[] arr = {3, 2, 1, 5, 4}; bubbleSort(arr); // 调用冒泡排序方法 } /** * 冒泡排序方法(升序) * @param arr 待排序的数组 */ public static void bubbleSort(int[] arr) { // 外层循环:控制排序轮数(共arr.length-1轮) for (int i = 0; i < arr.length; i++) { // 内层循环:控制每轮比较次数(每轮减少i次,因i个元素已排序) for (int j = 0; j < arr.length - 1 - i; j++) { int temp = 0; // 相邻元素比较:前元素 > 后元素则交换(升序逻辑) if (arr[j] > arr[j + 1]) { temp = arr[j + 1]; arr[j + 1] = arr[j]; arr[j] = temp; } } } // 增强for循环打印排序后数组 for (int i : arr) { System.out.println(i); // 输出:1 2 3 4 5 } }}
排序过程示例(数组 [3,2,1,5,4]
):
- 第 1 轮:
[2,1,3,4,5]
(比较 4 次,最大元素 5 冒泡到末尾) - 第 2 轮:
[1,2,3,4,5]
(比较 3 次,第二大元素 4 冒泡到倒数第二位) - 第 3 轮:无交换(比较 2 次,数组已有序)
- 第 4 轮:无交换(比较 1 次,排序结束)
3.3 二分查找
二分查找(又称折半查找)是高效的查找算法,适用于「已排序的数组」,时间复杂度为 O(log n)
(远优于线性查找的 O(n)
)。
核心注意事项:
- 前提条件:必须作用于已排序数组(升序/降序需与比较逻辑匹配)
- 边界计算:
- 中间索引:
mid = left + (right - left)/2
(避免left + right
导致的整数溢出) - 循环条件:
left <= right
(包含等于,确保不遗漏最后一个元素)
- 中间索引:
- 范围调整:
- 找到目标:立即返回
mid
(索引) - 目标更大:
left = mid + 1
(向右缩小查找范围) - 目标更小:
right = mid - 1
(向左缩小查找范围)
- 找到目标:立即返回
- 终止条件:循环结束返回
-1
(表示目标不存在) - mid 更新:每次循环必须重新计算
mid
(不能在循环外固定赋值)
public class BinarySearchDemo { public static void main(String[] args) { int[] arr = {1,2,3,4,5}; // 二分查找必须用有序数组 int index = midSort(arr, 5); // 查找目标值5 if (index != -1) { System.out.println(\"目标值找到,索引为:\" + index); // 输出:目标值找到,索引为:4 } else { System.out.println(\"目标值不存在于数组中\"); } } /** * 二分查找方法(升序数组) * @param arr 已排序的数组 * @param target 待查找的目标值 * @return 目标值的索引(未找到返回-1) */ public static int midSort(int[] arr, int target) { int left = 0; // 左边界索引 int right = arr.length - 1; // 右边界索引 // 先判断数组合法性(空数组或长度为0直接返回-1) if (arr.length == 0 || arr == null) { return -1; } // 循环查找:左边界 <= 右边界 while (left <= right) { int mid = (left + right) / 2; // 计算中间索引 if (arr[mid] == target) { return mid; // 找到目标,返回索引 } else if (arr[mid] < target) { left = mid + 1; // 目标在右侧,调整左边界 } else { right = mid - 1; // 目标在左侧,调整右边界 } } return -1; // 循环结束未找到,返回-1 }}
3.4 对象数组
对象数组是「存储对象引用的数组」,数组中的每个元素都是对象的地址,而非对象本身。通过对象数组可批量管理多个对象。
需求:定义数组存储 3 个 Person 对象,并遍历获取属性值
1. Person 类(封装姓名、年龄属性)
public class Person { // 私有属性(封装) private String name; private int age; // 无参构造方法 public Person() { } // 有参构造方法(用于初始化属性) public Person(String name, int age) { this.name = name; this.age = age; } // Getter方法(获取私有属性值) public String getName() { return name; } public int getAge() { return age; } // Setter方法(修改私有属性值,可选) public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; }}
2. 对象数组的创建与遍历
public class ObjectArrayDemo { public static void main(String[] args) { // 1. 创建对象数组(长度为3,存储3个Person对象) Person[] arr = new Person[3]; // 2. 创建Person对象 Person p1 = new Person(\"张三\", 18); Person p2 = new Person(\"lisi\", 19); Person p3 = new Person(\"wangwu\", 20); // 3. 将对象引用存入数组 arr[0] = p1; arr[1] = p2; arr[2] = p3; // 4. 遍历对象数组,获取属性值(需通过Getter方法) for (int i = 0; i < arr.length; i++) { // 直接打印数组元素:输出对象的地址(如com.code.Person@41629346) System.out.println(arr[i]); // 通过Getter方法获取属性值 System.out.println(arr[i].getName() + arr[i].getAge()); } }}
输出结果:
com.code.day08.Person@41629346张三18com.code.day08.Person@4f3f5b24lisi19com.code.day08.Person@15aeb7abwangwu20
四、方法参数传递:基本类型 vs 引用类型
Java 中方法参数传递的核心规则是「值传递」,但基本类型和引用类型的传递效果不同,本质是因为两者在内存中的存储方式不同。
4.1 基本数据类型做方法参数传递
基本类型(如 int
、double
、boolean
)存储的是「具体值」,传递时会复制一份值给方法参数,方法内部修改参数值不会影响原变量。
public class BasicParamDemo { public static void main(String[] args) { int a = 10; int b = 20; System.out.println(\"调用前:a=\" + a + \", b=\" + b); // 输出:调用前:a=10, b=20 // 传递的是a和b的值副本,而非原变量 method(a, b); System.out.println(\"调用后:a=\" + a + \", b=\" + b); // 输出:调用后:a=10, b=20(原变量未变) } private static void method(int a, int b) { a += 10; // a变为20(修改的是副本) b += 20; // b变为40(修改的是副本) System.out.println(\"方法内:a=\" + a + \", b=\" + b); // 输出:方法内:a=20,