命令行参数与可变参数
本教程共 100 篇 · 第 28 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:搞懂 main 方法的 String[] args 从哪来、怎么传,并掌握可变参数(varargs)让一个方法能接收任意多个同类型实参的写法。
程序启动时常需要外部传点信息进来,比如要处理哪个文件、用什么模式跑。写死在代码里,改一次就得重新编译一次,显然不合适。
Java 用两套机制接收「数量不确定」的输入:命令行参数负责程序外部传入,可变参数负责方法之间传递。两者底层都是数组。
命令行参数:main 的 args
从第一天写 Hello World 起,你就在写 public static void main(String[] args),那个 args 一直没派上用场。它就是命令行参数,运行程序时跟在类名后面。
public class ArgsDemo {
public static void main(String[] args) {
System.out.println("收到 " + args.length + " 个参数");
for (String s : args) {
System.out.println(s);
}
}
}
编译后用空格把参数跟在类名后面:
javac ArgsDemo.java
java ArgsDemo hello world 2026
输出:
收到 3 个参数
hello
world
2026
参数按书写顺序装进数组,args[0] 是 hello,不是类名。这点和 C 语言不同——C 的 argv[0] 是程序名,Java 的 args[0] 直接就是第一个真实参数。
args 是空数组,不是 null
不传任何参数时,args 是一个长度为 0 的数组,不是 null:
java ArgsDemo
输出 收到 0 个参数,循环一次都不进,不会抛异常。
这是 JVM 的保证,所以下面这种防御性判空纯属多余:
if (args != null && args.length > 0) { } // 前半句白写
if (args.length > 0) { } // 够了
但下标访问必须判长度。用户没传参数时直接读 args[0]:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException:
Index 0 out of bounds for length 0
这是命令行程序最常见的崩溃。取参数前先检查个数,或者给个默认值。
参数永远是字符串
不管你在命令行里敲的是什么,进到 args 里全是 String。要当数字用必须自己转:
public class SumArgs {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("用法:java SumArgs 数字1 数字2");
return;
}
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
System.out.println(a + b);
}
}
Integer.parseInt 把字符串转成 int。转不了会抛异常,比如 java SumArgs abc 1:
Exception in thread "main" java.lang.NumberFormatException:
For input string: "abc"
Note转数字用
Integer.parseInt(要 int)或Integer.valueOf(要 Integer 对象)。new Integer("1")这种构造方法从 Java 9 起就被标记废弃,Java 16 之后更是明确不该再用,历史兼容,不推荐。
带空格的参数要加引号
参数之间靠空格分隔,所以参数自身含空格时必须用引号包起来:
java ArgsDemo "hello world" 2026
这样是 2 个参数;不加引号就变成 3 个。引号由操作系统的 shell 处理,Windows 的 cmd、PowerShell 和 Linux/macOS 的 bash 规则略有差异,但双引号在三者都能用。
单文件直接运行
Java 11 起可以跳过 javac,直接运行源文件,参数照传:
java ArgsDemo.java hello world
JVM 会在内存里编译再执行,不生成 .class 文件。写小脚本、做快速验证时很方便,正式项目还是走编译流程。
main 的几种合法写法
main 的签名并不像想象中那么死板。下面几种都能被 JVM 认作程序入口:
public static void main(String[] args) // 标准写法
public static void main(String... args) // 可变参数版,同样合法
static public void main(String[] args) // 修饰符顺序可换
public static void main(String[] 随便叫) // 参数名任意
关键是三点:public static、返回 void、参数是一个 String 数组。第二种能行,正是因为可变参数编译后就是数组——这两个概念在这里接上了头。
Java 25 LTS 转正的紧凑源文件特性(JEP 512)还允许省掉外层类和 static:
void main() {
System.out.println("Hello");
}
这是为降低初学门槛做的简化,写小程序很省事。但它拿不到 args,需要命令行参数时还是得写完整签名。工作中读到的绝大多数代码仍是标准写法,两种都要认识。
可变参数:方法自己收多个值
写一个求和方法,调用方有时传 2 个数,有时传 5 个,怎么办?
老办法是让调用方自己封数组,很别扭:
static int sum(int[] nums) { }
sum(new int[]{1, 2, 3}); // 每次都要 new,烦
Java 5 引入的**可变参数(varargs)**解决了这个问题,语法是在类型后加三个点 ...:
public class VarargsDemo {
static int sum(int... nums) { // nums 在方法内部就是 int[]
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(10, 20)); // 30
System.out.println(sum()); // 0,一个都不传也行
}
}
int... nums 在方法体里就是一个 int[],该怎么遍历怎么遍历。调用时直接把值列出来,编译器自动打包成数组塞进去。
一个都不传时,nums 是长度为 0 的空数组,不是 null。这和 main 的 args 是同一套逻辑。
可变参数的规则
规则只有两条,但都是硬性的。
一个方法最多一个可变参数,而且必须放在参数列表末尾:
static void print(String prefix, int... nums) { // 正确
System.out.print(prefix + ":");
for (int n : nums) {
System.out.print(n + " ");
}
}
放错位置直接编译不过:
static void bad(int... a, String b) { }
error: varargs parameter must be the last parameter
道理不难想:编译器要判断「从哪个实参开始该打包进数组」。如果可变参数后面还有别的参数,这个边界就没法确定了。
可变参数就是数组
理解了这一点,很多行为就不用死记。可变参数方法可以直接接收数组:
sum(1, 2, 3); // 列值,编译器帮你打包
sum(new int[]{1, 2, 3}); // 直接给数组,效果完全一样
反过来也成立:不能同时定义可变参数版和数组版的重载,它们编译后签名一模一样:
static int f(int... a) { return 0; }
static int f(int[] a) { return 0; }
error: cannot declare both f(int[]) and f(int...) in class Demo
Warning给
String...传null要当心。f(null)会把整个null当成数组本身传进去,方法里遍历就抛空指针,编译器只给一句 warning:non-varargs call of varargs method with inexact argument type for last parameter。想传「一个 null 元素」得写f(new String[]{null})。
重载时谁优先
同时存在精确匹配和可变参数版本时,编译器优先选精确匹配:
static void f(int a, int b) { System.out.println("两参数版"); }
static void f(int... a) { System.out.println("可变参数版"); }
f(1, 2); // 两参数版
f(1, 2, 3); // 可变参数版
可变参数是最后的兜底选项。编译器会先找参数个数和类型完全对上的,找不到才考虑装箱、再找不到才考虑可变参数。这个顺序是为了兼容 Java 5 之前的老代码——加了 varargs 重载不会悄悄改变已有调用的行为。
正因如此,重载里混用可变参数很容易让人读不懂调用了哪个。工具方法里用它很好,重载体系里尽量少掺和。
两者对照
| 项目 | 命令行参数 args | 可变参数 int... |
|---|---|---|
| 来源 | 程序外部,运行时传入 | 程序内部,方法调用传入 |
| 类型 | 固定是 String[] | 任意类型都行 |
| 数量为 0 时 | 空数组,不是 null | 空数组,不是 null |
| 本质 | 数组 | 数组 |
| 位置要求 | main 的唯一参数 | 必须是参数列表最后一个 |
常见疑问
问:可变参数会不会影响性能?
每次调用都会 new 一个数组,高频调用的热点路径上有一点开销。JDK 的做法是给最常见的 0~2 个参数场景加固定参数的重载,其余交给可变参数版兜底。日常业务代码不用为此纠结,可读性收益远大于这点开销。
问:泛型可变参数为什么会有警告?
写 static <T> void f(T... args) 时,编译器会提示 Possible heap pollution from parameterized vararg type T。因为泛型数组本身是不安全的,往里塞错类型编译期查不出来。确认自己的方法只读不写这个数组,就在方法上加 @SafeVarargs 消除警告。
问:JDK 里哪些常用方法用了可变参数?
非常多。String.format(fmt, ...)、Arrays.asList(...)、List.of(...)、String.join(分隔符, ...) 都是。看到方法签名里的三个点,就知道它接收任意多个同类型实参。