本专题主要为观看韩顺平老师《零基础30天学会Java》课程笔记,同时也会阅读其他书籍、学习其他视频课程进行学习笔记总结。如有雷同,不是巧合!
有时使用单独定义变量解决/数组存储,不利于数据的管理,并且效率低;数据类型无法直接体现,只能通过下标获取,造成变量名字和内容对应关系不明确。
一、类和对象
-
类是自定义数据类型,具有属性和方法→对象是类的一个具体实例
-
⭐对象在内存中的存在形式:
-
🐈⬛栈、堆、方法区:
在计算机内存管理中,栈(Stack)、堆(Heap)和方法区(Method Area) 是三个核心的内存区域,各自承担不同的职责。以下是它们的详细说明:
1. 栈(Stack)
特点
- 后进先出(LIFO):最后进入的数据最先被移除。
- 线程私有:每个线程都有自己的栈,互不干扰。
- 存储局部变量和方法调用:
- 方法调用时,会创建一个栈帧(Stack Frame),存储:
- 局部变量(基本类型、对象引用)
- 操作数栈(用于计算)
- 方法返回地址
- 方法执行完毕后,栈帧自动弹出(内存自动回收)。
- 方法调用时,会创建一个栈帧(Stack Frame),存储:
- 内存分配和回收速度快(由编译器自动管理)。
- 内存较小(通常几MB,可调整JVM参数)。
示例
void foo() {int a = 10; // 局部变量,存储在栈String s = "hi"; // 引用变量(s在栈,"hi"在堆) }
2. 堆(Heap)
特点
- 存储对象实例(
new
创建的对象)。 - 线程共享:所有线程共用堆内存。
- 动态分配:内存大小不固定,可扩展(受物理内存限制)。
- 垃圾回收(GC)管理:不再使用的对象由GC自动回收。
- 内存较大(默认占JVM内存的大部分)。
堆内存结构(以JVM为例)
- 新生代(Young Generation)
- Eden区(新对象首先分配到这里)
- Survivor区(From/To,用于存活对象复制)
- 老年代(Old Generation)
- 长期存活的对象晋升到这里
- 元空间(Metaspace,JDK8+) / 永久代(PermGen,JDK7-)
- 存储类元数据、常量池等(不属于堆,但逻辑相关)
示例
String str = new String("Hello"); // "Hello"对象在堆,str引用在栈
3. 方法区(Method Area):存储rug静态的、与类相关的数据
- 存储类信息:
- 类结构(字段、方法、构造器)
- 运行时常量池(字符串常量、
static final
变量) - JIT编译后的代码
- 静态变量(
static
修饰的变量)
- 线程共享:所有线程共用方法区。
- 逻辑上属于堆,但具体实现因JVM版本不同:
- JDK7及之前:称为 永久代(PermGen),在堆中。
- JDK8+:改为 元空间(Metaspace),使用本地内存(不受JVM堆大小限制)。
- 垃圾回收较少:主要回收无用的类信息。
示例
class MyClass { // 类信息存储在方法区static int x = 10; // 静态变量在方法区final String s = "ABC"; // 常量在方法区的运行时常量池 }
三者的关系
内存区域 存储内容 线程安全 管理方式 生命周期 栈 局部变量、方法调用 线程私有 自动分配/释放 方法执行结束即销毁 堆 对象实例、数组 线程共享 GC回收 对象不再被引用时回收 方法区 类信息、常量池 线程共享 GC(部分) JVM关闭时释放
总结
- 栈:存储方法调用和局部变量,速度快但容量小。
- 堆:存储所有对象,由GC管理,容量大但访问稍慢。
- 方法区:存储类元数据和常量,JDK8+后改为元空间(本地内存)
- ⚠若类的一个属性为数组,则一个实例的数组的实际数据仍存储在堆中:
- 数组是动态创建的对象,所有的对象实例都存储在堆内存。数组对象包括:数组长度、元素数据。在运行时通过
new
关键字动态创建(如new int[10]
),其大小和生命周期在编译期无法确定。而方法区存储的是类加载时确定的静态数据(如类信息)。因此数组不符合方法区的存储目标,因为数组的大小和内容可能随时变化,无法静态分配。并且数组可能占用较大内存(如一个int[100000]
),但方法区通常较小(默认几十MB),且扩展成本高(元空间使用本地内存,但仍有上限);而堆可以动态扩容(受物理内存限制) - 即使数组被声明为
static final
,数组对象(实际数据)仍在堆中,只有它的引用存储在方法区(作为静态变量)。 - 数组的引用(即变量名)的存储位置取决于变量的声明位置:
- 实例变量(类的成员属性):引用存储在堆内存中(因为对象实例本身在堆中)。
- 局部变量(方法内的变量):引用存储在栈内存(Stack)中。
- 数组是动态创建的对象,所有的对象实例都存储在堆内存。数组对象包括:数组长度、元素数据。在运行时通过
-
-
属性/成员变量/field(字段):类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组等)
- 定义语法:
访问修饰符 属性类型 属性名;
- 访问修饰符用于控制属性的访问范围,包括:
public
,protected
,默认,private
- 访问修饰符用于控制属性的访问范围,包括:
- 属性如果不赋值,会有默认值,规则和数组一致,即:int-0, short-0, byte-0, long-0, float-0.0, double-0.0, char-\u0000, boolean-false, String-null。‼️给对象分配内存空间后才有默认值。
- 定义语法:
-
成员方法/方法:
- 优点:提高代码的复用性;可用将实现的细节封装起来,供其他用户调用
- 定义方法:
访问修饰符 返回数据类型 方法名(形参列表) {// 方法体}
public class Method {public static void main(String[] args) {Person p = new Person();p.speak();int sum1 = p.cal01(100)int sum2 = p.cal02(100, 200)} }class Person {String name;int age;public void speak() {System.out.println("我是人");}public int cal01(int n) {int sum = 0;for (int i = 1; i <= n; i++) sum += i;return sum;}public int cal02(int m, int n) {return m + n;} }
‼️一个方法最多有一个返回值。如果想返回多个值,可以返回一个数组。
‼️方法不能嵌套定义,即方法体中不能再定义方法。因为Java的变量和方法作用域是类级别的,而非方法级别的。嵌套方法会导致作用域规则混乱。
-
🐈⬛方法嵌套的替代方案:
(1) 使用Lambda表达式(Java 8+)
Lambda可以模拟“嵌套方法”的行为,尤其在需要函数式接口的地方:
void outerMethod() {Runnable nestedAction = () -> {System.out.println("嵌套逻辑(Lambda实现)");};nestedAction.run(); // 调用 }
(2) 通过内部类/跨类调用类似
定义一个方法内的局部内部类,包含“嵌套方法”:
void outerMethod() {class Nested {void nestedMethod() {System.out.println("嵌套逻辑");}}new Nested().nestedMethod(); // 调用 }
(3) 方法分离到同一类中(调用同一个类的其他方法)
将逻辑拆分为类的独立方法,通过参数传递数据:
void outerMethod() {int result = nestedHelper(10); // 调用辅助方法System.out.println(result); }private int nestedHelper(int param) { // 辅助方法return param * 2; }
(4) 匿名内部类
适用于接口或抽象类的快速实现:
void outerMethod() {new Object() {void nestedMethod() {System.out.println("匿名内部类中的嵌套逻辑");}}.nestedMethod(); // 立即调用 }
为什么其他语言(如Python、JavaScript)支持方法嵌套?
- 函数式编程特性:这些语言允许函数作为一等公民,嵌套函数可以捕获外部作用域的变量(闭包)。
- 动态类型系统:无需严格定义类结构,嵌套函数更灵活。
何时需要“方法嵌套”?如何选择替代方案?
场景 推荐替代方案 示例 简单逻辑复用 Lambda表达式 () -> System.out.println()
复杂逻辑封装 内部类或独立方法 局部内部类或私有辅助方法 回调或事件处理 匿名内部类 new Runnable() { run()... }
需要访问外部方法局部变量 Lambda(要求变量为 final
或等效不可变)int x=10; () -> System.out.println(x)
-
🐈⬛方法无法独立存在:
在 Java 中,方法不能单独定义在
public
类的外部。Java 的语法规则要求所有方法必须定义在类的内部。1. Java 的语法规则
- 方法必须属于某个类:Java 是纯粹的面向对象语言,所有方法(包括
main
)都必须定义在类或接口中。 - 不能有“游离”方法:类似 C/C++ 的全局函数在 Java 中是不允许的。
❌ 错误示例
// 错误!方法不能定义在类外部 public void freeMethod() {System.out.println("This is illegal in Java!"); }public class MyClass {public static void main(String[] args) {freeMethod(); // 编译报错:找不到符号} }
2. 如何实现类似功能?
(1) 将方法放入类中
public class MyClass {// 正确:方法定义在类内部public static void freeMethod() {System.out.println("This is legal!");}public static void main(String[] args) {freeMethod(); // 直接调用} }
(2) 通过对象调用实例方法
public class MyClass {// 实例方法public void freeMethod() {System.out.println("Call via object");}public static void main(String[] args) {MyClass obj = new MyClass();obj.freeMethod(); // 通过对象调用} }
(3) 使用工具类(静态方法)
public class Helper {// 工具类的静态方法public static void freeMethod() {System.out.println("Helper method");} }public class MyClass {public static void main(String[] args) {Helper.freeMethod(); // 通过类名调用} }
3. 为什么 Java 不允许外部方法?
- 面向对象设计:Java 强制用类封装数据和行为,保持代码结构清晰。
- 避免命名冲突:类作为命名空间,可以防止全局方法名污染。
- 封装性:通过类的访问控制(如
public/private
)管理方法可见性。
4. 其他语言的对比
语言 是否支持游离方法 替代方案 Java ❌ 类中的静态或实例方法 C++ ✅ 全局函数或命名空间 Python ✅ 模块级函数 C# ❌ 静态类或实例方法 - 方法必须属于某个类:Java 是纯粹的面向对象语言,所有方法(包括
🗒️方法定义时的参数称为形式参数(形参);方法调用时传入的参数称为实际参数(实参)。实参和形参的类型要一致或兼容,个数、顺序必须一致。
🗒️对于形参是基本数据类型时,进行值拷贝,形参的任何改变不影响实参。引用类型传递的是地址,可以通过形参改变实参。
-
💻克隆对象
编写一个方法,可以克隆对象,返回复制的对象。要求得到的新对象和原来的对象是两个独立的对象,但属性相同。
public class CopyObject {public static void main(String[] args) {Person p = new Person();p.name = "Cherry";p.age = 20;p.verify = new int[]{1, 2, 3, 4};Tools tool = new Tools();Person new_p = tool.copyPerson(p);System.out.println("新对象的姓名为:" + new_p.name + ",年龄为:" + new_p.age);System.out.print("新对象的验证数组为:");for (int i = 0; i < new_p.verify.length; i++){System.out. print(new_p.verify[i] + " ");}System.out.println();new_p.name = "Stars";new_p.age = 25;new_p.verify[0] = 10;System.out.println("修改后新对象的姓名为:" + new_p.name + ",年龄为:" + new_p.age);System.out.print("修改后新对象的验证数组为:");for (int i = 0; i < new_p.verify.length; i++){System.out. print(new_p.verify[i] + " ");}System.out.println();System.out.println("修改后原来对象的姓名为:" + p.name + ",年龄为:" + p.age);System.out.print("修改后原来对象的验证数组为:");for (int i = 0; i < p.verify.length; i++){System.out. print(p.verify[i] + " ");}System.out.println();} }class Person {String name;int age;int[] verify; }class Tools {public Person copyPerson (Person p) {Person new_p = new Person();new_p.name = p.name;new_p.age = p.age;new_p.verify = p.verify; // 传递数组的地址;否则可以重新new一个数组并拷贝return new_p;} }
-
🐈⬛String 和 数组 同样作为引用类型,表现不同的原因
在Java中,
String
和数组虽然都是引用数据类型,但它们在修改时的行为差异源于不可变性(Immutability)和引用指向的对象是否可变。1. String的不可变性
(1) String是不可变类
-
任何对
String
的修改(如拼接、替换)都会创建一个新的String对象,而原对象不变。 -
示例:
String a = "Hello"; String b = a; // b和a指向同一个对象 "Hello" b = b + " World"; // 创建新对象 "Hello World",b指向新对象,a仍指向"Hello"
a
仍为"Hello"
,b
变为"Hello World"
。
(2) 内存变化
堆内存: 1. "Hello" (a和b最初指向这里) 2. "Hello World" (b修改后指向这里)
2. 数组的可变性
(1) 数组是可变对象
-
数组的元素可以直接修改,不创建新对象。
-
示例:
int[] arr1 = {1, 2, 3}; int[] arr2 = arr1; // arr2和arr1指向同一个数组对象 arr2[0] = 99; // 修改数组元素,arr1[0]也会变为99
arr1[0]
和arr2[0]
都变为99
。
(2) 内存变化
堆内存: 1. [1, 2, 3] (arr1和arr2指向这里) 修改后: 1. [99, 2, 3] (arr1和arr2仍指向这里)
3. 关键区别总结
特性 String 数组 可变性 不可变(修改生成新对象) 可变(可直接修改元素) 赋值行为 b = a
→ 指向同一对象arr2 = arr1
→ 指向同一对象修改后的影响 b
修改后指向新对象,a
不变arr2
修改元素,arr1
同步变化内存分配 每次修改生成新对象 始终操作同一对象 4. 如何避免数组的“同步修改”问题?
如果需要独立副本,需显式复制数组:
int[] arr1 = {1, 2, 3}; int[] arr2 = arr1.clone(); // 或 Arrays.copyOf(arr1, arr1.length),并且导入类:java.util.Arrays,因为这是一个静态方法,必须通过类名调用 arr2[0] = 99; // arr1不受影响
-
-
二、递归
-
定义:方法自己调用自己,并且每次调用时传入不同的变量。
-
递归机制分析案例:
-
递归的重要规则:
-
💻猴子吃桃
有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个。以后每天猴子都吃其中的一半,然后再多吃一个。当到第10天时,想再吃时 (即还没吃)发现只有1个桃子了。问题:最初共多少个桃子?【逆推思想】
import java.util.Scanner;public class Recursion {public static void main(String[] args) {System.out.print("查询第几天的桃子数?:");Scanner scanner = new Scanner(System.in);int day = scanner.nextInt();int remains = eat_peach(day);if (remains == -1)System.out.println("输入天数错误!");elseSystem.out.println("第 " + day + " 天剩余 " + remains + " 个桃子");}public static int eat_peach(int day) {if (day == 10) return 1;else if (day >= 1 && day < 10) return (eat_peach(day + 1) + 1) * 2;else return -1;} }
-
💻迷宫问题
用二维数组表示迷宫,0表示没有障碍物,1表示有障碍物;
使用递归回溯,递归出口是最后一个坐标被标记为可以走通,或当前位置向任何方向移动都是死路(回溯,即返回给上一层递归
False
,然后上一个位置继续向其他方向探测);寻找路径时需要标记:2-已经走过,可以走 3-已经走过,但是死路;
目前先规定寻找顺序:下→右→上→左,如果探测的位置是0则可以下一步;
探测到一个位置时,先假定可以走通,并按顺序探测,如果四个方向都不能到终点,则重置为不能走通
import java.util.Scanner;public class Recursion {public static void main(String[] args) {// 用二维数组表示迷宫int[][] map = new int[8][7];for (int j = 0; j < 7; j++) {map[0][j] = 1;map[7][j] = 1;}for (int i = 1; i < 7; i++) {map[i][0] = 1;map[i][6] = 1;}map[2][2] = map[3][1] = map[3][2] = 1;// 输出原始迷宫System.out.println("=======迷宫地图=======");for (int i = 0; i < map.length; i++) {for (int j = 0; j < map[i].length; j++) {System.out.print(map[i][j] + " ");}System.out.println();}// 开始探测find_way(map, 1, 1);// 输出探测后标记的路径System.out.println("=======探测路径=======");for (int i = 0; i < map.length; i++) {for (int j = 0; j < map[i].length; j++) {System.out.print(map[i][j] + " ");}System.out.println();}}// 从map[i][j]开始查找路径public static boolean find_way(int[][] map, int i, int j) {if (map[6][5] == 2) {return true;}else {if (map[i][j] == 0) {// 没有被探测过map[i][j] = 2; // 先假设可以走通// 下->右->上->左if (find_way(map, i + 1, j)) return true;else if (find_way(map, i, j + 1)) return true;else if (find_way(map, i - 1, j)) return true;else if (find_way(map, i, j - 1)) return true;else {map[i][j] = 3; // 重置为不能走通return false;}}else {return false;}}} }
-
💻汉诺塔问题
有三个柱子,第一个柱子从小到大堆了一些盘子,要求把第一个柱子的盘子移动到第三个柱子,保持大小不变,并且每次只能移动一个盘子。【将最后一个盘子上面的所有盘子看成一个整体,如果只有两个盘子的话,将小盘子移到中间,再将大盘子放到第三个,最后把小盘子放到第三个大盘子上】
import java.util.Scanner;public class Recursion {public static void main(String[] args) {System.out.print("输入盘子的个数:");Scanner scanner = new Scanner(System.in);int num = scanner.nextInt(); // 盘子个数move_plate(num, 'A', 'B', 'C');}public static void move_plate(int num, char a, char b, char c) {// a,b,c代表三根柱子的编号,a为当前在的柱子,c为目标柱子if (num == 1) {// 递归出口System.out.println(a + "->" + c);}else {// 把上面的移到b,最后一个移到c,再把上面的移到cmove_plate(num - 1, a, c, b);System.out.println(a + "->" + c);move_plate(num - 1, b, a, c);}} }
-
💻⭐八皇后
在8*8的棋盘上摆放8个皇后,使其不能相互攻击,即:任意两个皇后不能处于同一行、同一列、同一斜线,有几种摆法?【用一维数组存储皇后的位置,下标为行,数值为该行皇后放置的列】
实现思路:
- 逐行放置皇后:
- 每一行放一个皇后,避免行冲突。
- 检查列和对角线冲突:
- 列冲突:当前列是否已有皇后。
- 对角线冲突:两个皇后是否在同一个主对角线(行差 = 列差)或副对角线(行差 = -列差)。
- 递归回溯:
- 如果当前行的某个位置可以放皇后,则递归处理下一行。
- 如果无法放置,则回溯到上一行,尝试其他位置。
public class Recursion {private static final int N = 8; // 棋盘大小// static:表示变量属于类,而非对象(所有对象共享同一份数据)// final 是一个关键字,用于表示 不可变性;如果修饰引用类型(如对象、数组),则引用不可变(但对象内部状态可能可变,例如数组元素可以修改)private static int[] queens = new int[N]; // queens[i] = j 表示第i行的皇后放在第j列private static int count = 0; // 记录解法总数public static void main(String[] args) {solve(0); // 从第0行开始放置System.out.println("Total solutions: " + count);}/*** 递归放置皇后* @param row 当前要放置的行*/private static void solve(int row) {if (row == N) { // 所有行都已放置,找到一个解printSolution();count++;return;}for (int col = 0; col < N; col++) { // 从第一列开始判断if (isSafe(row, col)) { // 检查当前位置是否安全queens[row] = col; // 放置皇后solve(row + 1); // 递归处理下一行}}}/*** 检查 (row, col) 是否可以放置皇后*/private static boolean isSafe(int row, int col) {for (int i = 0; i < row; i++) {// 检查列冲突或对角线冲突if (queens[i] == col || Math.abs(row - i) == Math.abs(col - queens[i])) {return false;}}return true;}/** 打印当前解法 */private static void printSolution() {System.out.println("Solution " + count + ":");for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {System.out.print(queens[i] == j ? "Q " : ". ");}System.out.println();}System.out.println();} }
其他结果省略…
- 逐行放置皇后:
三、重载
- 定义:Java中允许同一个类中,存在多个同名方法,但是要求形参列表不一致(参数类型/个数/顺序,至少有一个不同;与修饰符、返回类型无关)。例如
System.out.println()
可以输出多个类型数据,因为out
是PrintStream
类型的一个实例。利于接口编程,减轻起名负担。
可变参数
Java允许将同一个类中多个同名、同功能,但是**参数个数(0个~任意个)**不同的方法,封装成一个方法。使用可变参数时,可以当作数组使用。
访问修饰符 返回类型 方法名(数据类型... 形参名) {
}
-
可变参数的实参可以直接是数组
-
本质是数组
-
可一个普通类型的参数一起放在形参列表,但是可变参数必须放在最后
-
一个形参列表中只能有一个可变参数
-
🐈⬛编程语言中的可变参数
可变参数(Variable Arguments,简称 varargs)是一种允许函数接受不定数量参数的特性。以下是主流编程语言中的支持情况和使用方法:
1. Java
语法:
使用
类型... 参数名
声明可变参数,必须是方法的最后一个参数。public void printValues(String... values) {for (String s : values) {System.out.println(s);} } // 调用 printValues("A", "B", "C"); // 可传任意数量参数
特点:
- 编译后转为数组(
String[]
)。 - 可以传递数组:
printValues(new String[]{"A", "B"})
。
2. Python
语法:
使用
*args
接收可变位置参数,**kwargs
接收可变关键字参数。def print_values(*args, **kwargs):for arg in args: # 处理位置参数print(arg)for key, value in kwargs.items(): # 处理关键字参数print(f"{key}: {value}") # 调用 print_values(1, 2, 3, name="Alice", age=25)
特点:
args
将参数打包为元组(tuple)。*kwargs
将关键字参数打包为字典(dict)。
3. JavaScript
语法:
使用
arguments
对象或剩余参数(...rest
)。// 传统方式(arguments) function printValues() {for (let i = 0; i < arguments.length; i++) {console.log(arguments[i]);} }// ES6剩余参数 function printValues(...values) {values.forEach(v => console.log(v)); } // 调用 printValues("A", "B", "C");
特点:
arguments
是类数组对象(非真正的数组)。...rest
将参数转为真正的数组(支持数组方法如map
、filter
)。
4. C/C++
语法:
C 使用
<stdarg.h>
宏,C++11 支持模板和initializer_list
。// C语言(需固定至少一个参数) #include <stdarg.h> void printValues(int count, ...) {va_list args;va_start(args, count);for (int i = 0; i < count; i++) {int value = va_arg(args, int);printf("%d\\\\n", value);}va_end(args); } // 调用 printValues(3, 10, 20, 30);
C++实现案例:
1. 使用 C++11 变参模板(推荐)
变参模板是类型安全的,适合现代 C++ 开发。
示例:打印不定数量的参数
#include <iostream> using namespace std;// 基础情况:递归终止 void printArgs() {cout << endl; // 参数包为空时换行 }// 递归展开参数包 template<typename T, typename... Args> void printArgs(T first, Args... rest) {cout << first << " "; // 打印第一个参数printArgs(rest...); // 递归处理剩余参数 }int main() {printArgs(1, 3.14, "Hello", 'A'); // 输出: 1 3.14 Hello Areturn 0; }
typename... Args
:声明变参模板。Args... rest
:解包参数包。- 递归终止条件:无参数的
printArgs()
。
2. 使用
std::initializer_list
(同类型参数)适用于参数类型相同的情况(如全部为
int
)。示例:计算整数和
#include <iostream> #include <initializer_list> using namespace std;int sum(initializer_list<int> nums) {int total = 0;for (auto num : nums) {total += num;}return total; }int main() {cout << sum({1, 2, 3, 4}); // 输出: 10return 0; }
3. C 风格
va_list
(不推荐,仅兼容旧代码)C++ 兼容 C 的可变参数机制,但缺乏类型安全。
示例:打印不定数量参数
#include <iostream> #include <cstdarg> using namespace std;void printValues(int count, ...) {va_list args;va_start(args, count); // 初始化 va_listfor (int i = 0; i < count; i++) {int value = va_arg(args, int); // 按 int 类型提取参数cout << value << " ";}va_end(args); // 清理 va_list }int main() {printValues(3, 10, 20, 30); // 输出: 10 20 30return 0; }
- 需手动指定参数类型(如
va_arg(args, int)
)。 - 无法自动检测参数类型错误(如传递
double
但按int
读取)。
4. 变参模板进阶:完美转发(C++11)
结合
std::forward
实现参数完美转发。示例:构造对象
#include <iostream> #include <string> using namespace std;class Person { public:Person(const string& name, int age) : name(name), age(age) {cout << "Constructed: " << name << ", " << age << endl;} private:string name;int age; };template<typename... Args> void createPerson(Args&&... args) {Person person(std::forward<Args>(args)...); // 完美转发参数 }int main() {createPerson("Alice", 25); // 输出: Constructed: Alice, 25return 0; }
方法 类型安全 适用场景 复杂度 变参模板 ✅ 任意类型、现代 C++ 高(需递归) std::initializer_list
✅ 同类型参数 低 va_list
❌ 兼容 C 代码 中(易出错) 完美转发 ✅ 构造函数/函数参数转发 高 特点:
- C 中需手动管理参数类型和数量(易出错)。
- C++ 更推荐使用
std::initializer_list
或模板包(template <typename... Args>
)。
5. C#
语法:
使用
params
关键字。public void PrintValues(params string[] values) {foreach (string s in values) {Console.WriteLine(s);} } // 调用 PrintValues("A", "B", "C");
特点:
- 类似 Java,编译后转为数组。
- 必须是参数列表的最后一个。
6. Ruby
语法:
使用
*args
接收可变参数。def print_values(*values)values.each { |v| puts v } end # 调用 print_values("A", "B", "C")
特点:
values
将参数转为数组(Array
类型)。
7. Go
语法:
使用
...
前缀表示可变参数。func printValues(values ...int) {for _, v := range values {fmt.Println(v)} } // 调用 printValues(1, 2, 3) // 传多个值 printValues([]int{4, 5}...) // 传切片需解包
特点:
- 可变参数必须是同一类型。
- 函数内部视为切片(
slice
)。
8. Swift
语法:
使用
values: Int...
。func printValues(_ values: Int...) {for v in values {print(v)} } // 调用 printValues(1, 2, 3)
特点:
- 参数类型必须明确指定(如
Int...
)。 - 函数内部当作数组(
[Int]
)使用。
对比总结
语言 语法 内部类型 限制 Java String... values
数组 必须是最后一个参数 Python *args
,**kwargs
元组/字典 无 JS ...values
数组 无 C va_list
手动解析 需固定首个参数 C# params string[]
数组 必须是最后一个参数 Ruby *values
数组 无 Go values ...int
切片 必须同类型 Swift values: Int...
数组 必须指定类型
使用建议
- 安全性:优先选择类型安全的实现(如 Java/C# 的
params
,Go/Swift 的类型约束)。 - 灵活性:Python/JS 的
args
和*kwargs
适合动态场景。 - 性能:C/C++ 的
va_list
需谨慎使用(易引发未定义行为)。
- 编译后转为数组(