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
张三18
com.code.day08.Person@4f3f5b24
lisi19
com.code.day08.Person@15aeb7ab
wangwu20
四、方法参数传递:基本类型 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,