你已经会保存、编译并运行一个 Hello 程序。接下来真正要解决的问题,是看懂并写对程序里的每个细节:名字为什么这样取,变量里究竟存了什么,7 / 2 为什么不是 3.5,字符串为什么不能用 == 判断内容,以及 Scanner 为什么偶尔会“跳过”一行输入。
这一篇会把这些问题连成一条完整路径。我们不靠死记语法表,而是始终追问三件事:这段代码产生什么值、这个值是什么类型、程序按什么顺序求值。只要这三件事能答清楚,大多数基础语法问题都会从“凭感觉”变成“可推演”。
代码约定:包含 public class 的围栏是可单独保存的完整示例;没有类声明的 Java 围栏均明确视为片段,应放在当前讲解指定的 main 方法或类体中。带 // 编译错误、// 非法 的行用于展示反例,默认保持注释状态。
下面的示例默认采用经典的类式写法,并使用 public static void main(String[] args) 作为入口。这种结构在不同 JDK、IDE 和在线判题环境中都很常见,也最适合建立完整的源文件心智模型。
先看一个可以直接保存、编译和运行的程序。文件名应为 SyntaxMap.java:
/**
* 演示一个最小但结构完整的 Java 程序。
*/
public class SyntaxMap {
public static void main(String[] args) {
String topic = "Java 基础语法";
int lessonCount = 10;
System.out.println("正在学习:" + topic);
System.out.println("计划完成 " + lessonCount + " 个知识点");
}
}把代码从外向内看,会发现它是一层层嵌套的:
package 声明、若干 import 声明,以及顶层类或接口声明。public class SyntaxMap { ... } 声明了一个公开类。采用文件系统保存源码时,公开顶层类通常与源文件同名,因此这里使用 SyntaxMap.java。main 是这个示例的程序入口。启动器找到类后,会把命令行参数整理成一个 String[],再传给参数 args。main 的大括号形成一个块。变量声明和输出语句都在这个块里,程序从上到下执行这些语句。当程序开始使用包和标准库时,完整轮廓通常是这样:
package demo.basics; // 如果有,必须位于普通编译单元开头
import java.util.Scanner; // 位于 package 之后、类型声明之前
public class InputDemo {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入昵称:");
String name = input.nextLine();
System.out.println("你好," + name);
package 决定这个类型属于哪个包;import 只是让我们能用简单名称 Scanner,不用每次都写完整名称 java.util.Scanner。import 不会把另一个类的源码复制进当前文件,也不会创建对象。
暂时不使用包时,可以省略 package。这会把类放进未命名包,适合单文件练习。项目一旦有多个目录和多个类,就应使用命名包来避免重名并表达组织关系。
main 的每个部分是什么意思经典入口的签名是:
public static void main(String[] args)可以先这样理解:
参数名 args 可以换成别的合法标识符,真正关键的是声明结构。课程里仍用 args,因为这是大家一眼就能识别的惯例。
Java 会先把源代码识别为关键字、标识符、字面量、分隔符和运算符等记号。空格、换行和注释通常不参与后续语法分析,但它们有两个实际作用:分开相邻记号,以及帮助人读懂结构。staticvoid 是一个标识符,static void 才是两个关键字。
标识符用来给类、变量、方法等命名。实用规则如下:
_ 和 $ 出现在某些位置;但单独的 _ 是保留字,$ 通常留给生成代码。团队代码优先使用易输入、易检索的英文名称。totalPrice、TotalPrice 和 totalprice 是三个名字。int class = 1;。OrderCalculator;变量和方法常用小驼峰,如 totalPrice、calculateTotal;常量常用全大写加下划线,如 MAX_RETRY_COUNT。Java 常见的注释有三种:
// 单行注释:到本行结束
/*
* 传统块注释:可以跨越多行,但不能嵌套
*/
/**
* 文档注释:可以被 Javadoc 工具处理
*/注释应该说明“为什么这样做”、输入约束或容易忽略的前提。像 count++; // count 加一 这种重复代码表面的注释,通常只会制造噪声。
缩进本身通常不改变 Java 的语义,大括号才决定块的边界。但糟糕缩进会让人误判边界。每写一个左大括号就立刻补上右大括号,再让编辑器统一格式化,能减少大量低级错误。
下面的交互会把一个源文件拆成可点击的结构区域。逐层点击时,留意“语法位置”和“运行时执行顺序”不是一回事:import 参与编译期名称解析,但不会在 main 里按行执行。
System.out 表示标准输出流。终端程序最常用三个方法:
print(value) 输出内容后不换行。println(value) 输出内容并结束当前行。printf(format, values...) 按格式字符串控制数字精度、字段宽度和换行。先看 print 与 println 的差别:
public class OutputDemo {
public static void main(String[] args) {
System.out.print("姓名:");
System.out.println("小林");
System.out.println();
System.out.println("欢迎开始练习");
}
}输出是:
姓名:小林
欢迎开始练习不带参数的 println() 只输出一个换行。需要先打印提示并让用户在同一行输入时,print 很合适。
字符串使用双引号,字符使用单引号。想在字符串里表示引号、反斜杠、制表符或换行,要用转义序列:
public class EscapeDemo {
public static void main(String[] args) {
System.out.println("他说:\"继续试一次。\"");
System.out.println("课程\t进度");
System.out.println("Java\n基础语法");
System.out.println("C:\\work\\
"A" 是长度为 1 的 String,'A' 是一个 char 值。它们看起来相似,类型和可用操作却不同。
格式字符串里的 % 开始一个格式说明符。最常用的几种是:
public class Receipt {
public static void main(String[] args) {
String item = "练习册";
int count = 3;
double unitPrice = 19.9;
double total = count * unitPrice;
System.out.printf("%-8s %3d × %6.2f = %7.2f 元%n",
item, count, unitPrice, total);
System.out.
printf 的精度会影响显示时的舍入,不会修改变量本身。unitPrice 仍然是原来的 double 值。格式符和参数类型不匹配,例如用 %d 输出 double,程序会在运行时抛出格式异常。
日志或数据交换如果要求完全固定的小数点、日期和分组规则,不应盲目依赖机器的默认区域设置。面向当前用户显示时,本地区域格式通常更自然;面向机器解析时,则应明确约定格式。
变量可以先理解成一个有名字、有类型的存储位置。类型规定这个位置能接受什么值;名字让代码可以再次找到它;赋值则把右侧表达式算出的结果写进这个位置。
int score = 80;
score = score + 5;第二行不是数学等式。执行时先读取 score 的旧值 80,计算 80 + 5,再把 85 写回 score。赋值号左边要表示可写的位置,右边是要计算的表达式。因此下面两行意义完全不同:
score = 100; // 合法:把 100 写进 score
// 100 = score; // 非法:字面量 100 不是可写位置声明告诉编译器变量的类型与名字;初始化是在变量创建时给出第一个值;再次赋值会替换当前值。
int attempts; // 声明
attempts = 1; // 第一次赋值,也完成初始化
attempts = 2; // 再次赋值
double price = 29.9; // 声明与初始化合在一起局部变量不会自动获得 0、false 或 null。编译器必须能证明某次读取发生前,变量沿所有可能执行路径都已赋值:
public class InitDemo {
public static void main(String[] args) {
int count;
// System.out.println(count); // 编译错误:count 还没有确定值
count = 3;
System.out.println(count); // 合法
}
}类字段和数组元素会获得默认值,但这不应被误套到局部变量上。初学时最稳妥的写法,是尽量在声明处给出清楚的初始值。
局部变量的作用域通常从声明处开始,到包含它的块结束:
public class ScopeDemo {
public static void main(String[] args) {
int outer = 10;
{
int inner = 20;
System.out.println(outer + inner); // 30
}
System.out.println(outer); // 合法
// System.out.println(inner); // 编译错误:inner 已离开作用域
}
}作用域讨论的是“源代码的哪一部分可以用这个简单名字”,生命周期讨论的是“运行时这个变量何时存在”。两者相关,但不是同一个概念。当前阶段先把块边界画清楚,已经能避免绝大多数局部变量问题。
在同一个方法里,也不要指望用嵌套块重新声明一个仍在作用域内的同名局部变量:
int value = 10;
{
// int value = 20; // 编译错误:外层 value 仍在作用域内
}当一个名字在初始化后不应再指向别的值,可以使用 final:
final double TAX_RATE = 0.06;
double subtotal = 200.0;
double tax = subtotal * TAX_RATE;
// TAX_RATE = 0.08; // 编译错误final 约束的是变量不能再次赋值。若变量保存的是对象引用,final 表示这根“指向对象的箭头”不能改指另一个对象,不等于对象内部一定不可变化。String 自身不可变,是 String 类的性质,不是因为引用变量写了 final。
对整个类都要复用的固定量,常见写法是类级常量:
public class ShippingRule {
private static final double FREE_SHIPPING_LIMIT = 99.0;
public static void main(String[] args) {
System.out.println(FREE_SHIPPING_LIMIT);
}
}变量名应该表达业务含义,而不是只表达类型。double p 很快会变得含糊,double unitPrice 能直接告诉读者这个值是什么。只有非常短的循环计数器等临时量,才适合使用 i、j 这类短名字。
Java 的类型分成两大类:基本类型和引用类型。基本类型变量直接保存该类型的值;引用类型变量保存一个引用,这个引用可以指向兼容类型的对象,也可能是 null。

基本类型变量直接保存值;引用变量保存通向对象的引用。两者都必须遵守声明类型。
char 经常被口语化地称为“字符类型”,但更精确的理解是一个 UTF-16 代码单元。常见汉字通常占一个 char,某些补充字符会由一对代理代码单元表示:
public class UnicodeDemo {
public static void main(String[] args) {
String text = "A😀";
System.out.println(text.length()); // 3 个 char 代码单元
System.out.println(text.codePointCount(0, text.length())); // 2 个码点
}
}String、数组以及你以后自己定义的类都属于引用类型。看下面的代码:
String title = "Java";
String backup = title;
title = "Java 基础";backup = title 复制的是引用值。随后让 title 指向另一个字符串,不会让 backup 自动跟着改变。引用还可以取特殊值 null,表示目前不指向任何对象:
String nickname = null;
// System.out.println(nickname.length()); // 运行时抛出 NullPointerException对 null 调用实例方法没有目标对象,因此会失败。能用空字符串 "" 表达“已知存在但内容为空”时,不要随意用 null 混在一起。
字面量让我们在代码中直接表示值:
int decimal = 1_000_000;
int binary = 0b1010_0101;
int hex = 0xFF;
long distance = 9_000_000_000L;
double rate = 0.075;
double scientific = 1.2e3; // 1200.0
float ratio = 0.5F;
char grade = 'A';
没有小数点的十进制整数字面量通常先按 int 处理,超出范围时需要使用 L 表示 long。带小数点或十进制指数的浮点字面量默认是 double,写入 float 时通常加 F。
下划线可以分组数字,提高可读性,但不能放在字面量开头或结尾,也不能紧贴小数点或进制前缀。1_000_000 合法,_100、100_、1_.5 和 0x_FF 都不合适或不合法。
整数还可以使用二进制 0b 和十六进制 0x。以 0 开头的旧式八进制写法容易让 010 被误读成十进制十,实际它表示八;普通业务代码里应避免这种含糊写法。
float 和 double 表示的是有限集合中的二进制浮点数,不是数学意义上的任意实数。很多看似简单的十进制小数只能保存成附近值:
public class FloatingPointDemo {
public static void main(String[] args) {
double result = 0.1 + 0.2;
System.out.println(result); // 常见输出:0.30000000000000004
}
}这不是随机误差,而是表示方式决定的结果。金额等需要明确十进制舍入规则的场景,后续应学习 BigDecimal,不要简单把所有金额都交给 double。
选择类型时先从含义出发:人数通常用 int,持续时间可能用 long,开关状态用 boolean,文本用 String。不要为了“省几个字节”过早把普通整数都改成 byte 或 short,类型转换带来的复杂度往往更高。
Java 的基础算术运算符是 +、-、*、/ 和 %。符号很熟悉,但运算结果不只由符号决定,还取决于两侧操作数经过数值提升后的类型。
public class ArithmeticDemo {
public static void main(String[] args) {
int a = 7;
int b = 2;
System.out.println(a + b); // 9
System.out.println(a - b); // 5
System.out.println(a * b); // 14
System.out.
两个整数相除时,结果仍是整数,小数部分向零方向截掉:
System.out.println( 7 / 2); // 3
System.out.println(-7 / 2); // -3要得到小数结果,至少让一个操作数在运算前成为浮点类型:
int total = 7;
int count = 2;
double wrong = total / count; // 先算出 int 3,再转为 3.0
double right = (double) total / count; // 先把 total 转为 7.0,结果 3.5变量接收结果的类型不会倒过来改变右侧已经发生的运算。double wrong 并不能让 total / count 自动采用浮点除法。
整数除数为 0 会抛出 ArithmeticException。浮点除法遵循 IEEE 754 规则,1.0 / 0.0 产生正无穷,0.0 / 0.0 产生 NaN,通常不会抛出同一种异常:
System.out.println(1.0 / 0.0); // Infinity
System.out.println(0.0 / 0.0); // NaN不要把“没有抛异常”理解成结果一定可用于业务计算。可以用 Double.isFinite(value) 或 Double.isNaN(value) 检查特殊值。
对整数 a 和非零 b,a / b 与 a % b 满足:
(a / b) * b + (a % b) == a由于整数除法向零截断,余数可能是负数,而且它的符号与被除数一致:
System.out.println( 7 % 3); // 1
System.out.println(-7 % 3); // -1
System.out.println( 7 % -3); // 1因此,若你想把任意整数映射到 0 到 m - 1,直接写 value % m 可能得到负数。标准库提供了更符合这一需求的 Math.floorMod(value, m):
System.out.println(-7 % 3); // -1
System.out.println(Math.floorMod(-7, 3)); // 2int 只保留 32 位有符号结果。超过范围时,普通算术保留二进制结果的低位,不会自动变成 long,也不会默认抛异常:
public class OverflowDemo {
public static void main(String[] args) {
int max = Integer.MAX_VALUE;
System.out.println(max); // 2147483647
System.out.println(max + 1); // -2147483648
}
}另一个常见陷阱是“结果最后接到 long,所以中间不会溢出”:
int width = 50_000;
int height = 50_000;
long wrongArea = width * height; // int 乘法先溢出
long rightArea = (long) width * height; // 先提升一个操作数,再做 long 乘法需要把溢出当作错误时,可以使用 Math.addExact、Math.multiplyExact 等方法,让越界转成明确异常:
int next = Math.addExact(Integer.MAX_VALUE, 1); // 抛出 ArithmeticException由于每一步都可能舍入,浮点加法和乘法一般不满足结合律:
double x = 1e16;
double y = -1e16;
double z = 1.0;
System.out.println((x + y) + z); // 1.0
System.out.println(x + (y + z)); // 0.0(常见结果)比较浮点结果时,也不要对计算值轻率使用 ==。常见做法是根据业务尺度选择容差:
double actual = 0.1 + 0.2;
double expected = 0.3;
double tolerance = 1e-9;
boolean closeEnough = Math.abs(actual - expected) < tolerance;
System.out.println(closeEnough); // true容差不是永远固定的魔法数字。数值范围很大时,还可能需要相对误差或专门的数值算法。
Math 位于自动可见的 java.lang 包,不需要显式导入。常用成员包括:
double hypotenuse = Math.sqrt(3 * 3 + 4 * 4); // 5.0
double power = Math.pow(2, 10); // 1024.0
double down = Math.floor(3.9); // 3.0
double up = Math.ceil(3.1); // 4.0
三角函数使用弧度。Math.random() 返回大于等于 0.0 且小于 1.0 的伪随机 double,适合简单演示;安全令牌、密码或密钥不能依赖它。
排查算错时,先把表达式按“每个中间结果的类型”拆开。很多问题不是公式本身错了,而是整数除法、运算前溢出或浮点舍入比你预想得更早发生。
表达式会产生值。字面量、变量读取、方法调用都可以是表达式,运算符还能把简单表达式组合成复杂表达式:
double total = unitPrice * count * (1.0 - discountRate);阅读复杂表达式时,要分开三个问题:
它们经常被混成“谁先算”,但并不是同一层规则。

先确定表达式树,再按 Java 规定计算操作数;短路运算符还可能跳过右侧。
从高到低,可以先掌握这组常用层级:
2 + 3 * 4 被分组为 2 + (3 * 4)。即使你记得优先级,也建议在表达业务公式时主动加括号:
double payable = subtotal * (1.0 - discountRate) + shippingFee;括号不是只为编译器服务。它把你打算表达的业务结构直接展示给读者。
确定分组后,Java 保证运算符的操作数表现为从左到右求值。左操作数如果抛异常,右操作数不会继续执行。下面的例子虽然合法,但非常不适合作为日常写法:
int i = 2;
int result = (i = 3) * i;
System.out.println(result); // 9左侧赋值先把 i 改成 3,右侧随后读取到 3。知道这个规则是为了能分析代码,不是为了把多个副作用塞进一行。
一条表达式最好只包含一个最外层副作用。把“修改变量”和“使用变量”混在同一表达式里,即使结果有明确规范,也会让调试和代码审查变得困难。
下面的实验允许你修改操作数和运算符,并逐步观察分组、数值提升、求值顺序与最终类型。
<、<=、>、>=、==、!= 会产生 boolean 值:
int age = 20;
boolean adult = age >= 18;
boolean exactTwenty = age == 20;
boolean notZero = age != 0;&& 表示两侧都为真,|| 表示至少一侧为真,! 取反。&& 与 || 会短路:
int divisor = 0;
int value = 10;
boolean safe = divisor != 0 && value / divisor > 2;
System.out.println(safe); // false,没有执行除法&& 左侧为 false 时,整体已经不可能为 true,右侧不求值;|| 左侧为 true 时,整体已经确定为 true,右侧不求值。短路既影响性能,也常被用来保护右侧操作的前置条件。
不要写数学式风格的连续比较:
// boolean inRange = 0 <= score <= 100; // 非法
boolean inRange = 0 <= score && score <= 100;条件运算符由 ? 和 : 组成,形式是:
条件 ? 条件为真时的值 : 条件为假时的值int score = 86;
String level = score >= 60 ? "通过" : "未通过";它是表达式,会产生一个值。适合简短、对称的二选一;分支里要做多个动作时,使用 if...else 更清楚。
x += y 通常可以理解为“取出 x,与 y 相加,再写回 x”,但它还会把结果转换回左侧变量的类型,并且左侧只求值一次:
short s = 3;
s += 4.6; // 合法,结果为 short 7
// s = s + 4.6; // 非法:右侧是 double,不能直接赋给 short
s = (short) (s + 4.6); // 与复合赋值的类型效果接近因此复合赋值不只是少打几个字符。类型较窄时,它可能悄悄丢弃小数或高位。代码依赖这种隐式窄化时,应把意图写清楚。
count++ 和 ++count 都会让变量增加 1,单独作为语句时效果相同:
count++;
++attempts;进入更大的表达式后,两者产生的值不同:
int x = 5;
int oldValue = x++; // oldValue 是 5,x 变成 6
int y = 5;
int newValue = ++y; // y 先变成 6,newValue 也是 6最容易误判的是:
int x = 5;
x = x++;
System.out.println(x); // 5x++ 先产生旧值 5,同时把 x 改成 6,随后外层赋值又把旧值 5 写回。实战中让 ++、-- 独立成句,代码会稳得多。
混合不同数值类型运算时,Java 会先把操作数转换到一个共同类型。理解这条规则,比背每一道“结果是什么”更可靠。
对常见二元数值运算,可以按下面顺序判断:
byte、short 和 char 参与普通算术时,先提升为 int。所以两个 byte 相加,结果通常也是 int,不是 byte。
如果任一操作数是 double,另一侧转换为 double,运算结果是 double。
否则,如果任一操作数是 float,另一侧转换为 float,结果是 float。
byte left = 10;
byte right = 20;
// byte sum = left + right; // 编译错误:结果是 int
int sum = left + right; // 30
long count = 5L;
double unit = 2.5;
double total = count * unit; // long 先转为 double从较窄整数类型转到较宽整数类型通常能精确保留值:
int count = 2_000_000_000;
long exact = count;但“宽化转换”并不保证所有最低有效位都保留。int 转 float、long 转 float,以及某些 long 转 double 都可能损失精度:
int original = 1_234_567_890;
float approximate = original;
int roundTrip = (int) approximate;
System.out.println(original); // 1234567890
System.out.println(roundTrip); // 可能不同判断转换是否安全,不能只看目标类型占几个字节,还要看它能精确表示哪些值。
把大范围类型强制转为小范围类型,需要在值前写目标类型:
double price = 19.99;
int whole = (int) price; // 19,向零截掉小数部分
int code = 130;
byte narrowed = (byte) code; // -126,只保留低 8 位强制转换的含义是“我明确要求按这条规则转换”,不是“请帮我安全地四舍五入”。从浮点数取最接近整数,应根据需求使用 Math.round;要检查整数是否超出范围,应先比较边界,或使用标准库的精确转换方法。
有一个实用的编译期特例:值在范围内的常量表达式,可以直接初始化 byte、short 或 char:
byte answer = 42; // 42 是 int 字面量,但编译器能确认它落在 byte 范围内
int value = 42;
// byte copy = value; // 非法:变量运行时可能是其他值
byte copy = (byte) value; // 显式窄化char 可以参与数值提升,因为它保存 UTF-16 代码单元的无符号数值:
char letter = 'A';
int codeUnit = letter; // 65
char next = (char) (letter + 1); // 'B'这种技巧适合处理明确限定的代码单元范围,不应被误当成通用的自然语言字符遍历方案。大小写、组合字符和补充码点需要标准 Unicode API。
数值转字符串,可以用 String.valueOf:
String countText = String.valueOf(42); // "42"
String priceText = String.valueOf(19.9); // "19.9"字符串转数值,需要解析:
int count = Integer.parseInt("42");
long distance = Long.parseLong("9000000000");
double rate = Double.parseDouble("0.075");输入不是合法格式时,解析方法会抛出 NumberFormatException。解析前是否允许前后空白、逗号、小数位或空字符串,应由程序明确决定:
String input = " 42 ";
int count = Integer.parseInt(input.trim());不要用强制转换处理 String 和基本数值类型。(int) "42" 不合法;字符串里的字符需要由 Integer.parseInt 等解析方法按数字语法解释。
String 不是基本类型,而是 java.lang 中的类。字符串字面量会得到 String 对象;变量保存的是对字符串对象的引用。

字符串方法通常返回新字符串;重新赋值改变的是变量保存的引用,不会修改旧字符串对象。
字符串创建后,其字符序列不能被修改。像 toUpperCase、replace、substring 这样的方法会返回一个结果字符串,不会原地改变原字符串:
String name = "java";
name.toUpperCase();
System.out.println(name); // java
name = name.toUpperCase();
System.out.println(name); // JAVA第二次赋值只是让变量 name 改为引用结果字符串。旧的 "java" 对象没有被改写。
字符串不可变让共享更安全,也使字符串适合作为很多 API 的稳定输入。代价是频繁连接或替换可能创建中间对象。循环中大量拼接时,可以使用可变的 StringBuilder;少量展示文本直接用 + 更清楚。
假设有:
String course = "Java 基础";常用操作包括:
int units = course.length(); // UTF-16 代码单元数量
char first = course.charAt(0); // 'J'
String part = course.substring(0, 4); // "Java"
int position = course.indexOf("基础"); // 找不到时返回 -1
boolean contains = course.contains("Java");
字符串索引从 0 开始。substring(begin, end) 包含 begin,不包含 end,可以记成左闭右开区间 [begin, end)。合法索引应满足:
0 <= begin <= end <= length()charAt(index) 则要求 0 <= index < length()。越界会抛出 StringIndexOutOfBoundsException。
length() 返回 char 代码单元数量,不一定等于用户看到的图形字符数量。只处理 ASCII、常见数字和普通中文时差异不明显;处理表情或复杂文字时,应该使用码点或更高层的文本边界 API。
对基本数值使用 == 是比较数值;对引用类型使用 ==,比较的是两个引用是否指向同一个对象:
String a = new String("Java");
String b = new String("Java");
System.out.println(a == b); // false,通常是两个对象
System.out.println(a.equals(b)); // true,内容相同判断字符串内容应使用 equals:
boolean same = input.equals("yes");
boolean sameIgnoreCase = input.equalsIgnoreCase("yes");如果变量可能是 null,把已知非空的常量放在前面更安全:
boolean confirmed = "yes".equals(input);equalsIgnoreCase 适合简单、不依赖特定地区规则的大小写忽略比较。面向自然语言的排序、搜索和身份标识规范化,不能只靠它完成。
只要 + 的任一侧已经是 String,另一侧会转成字符串再连接。+ 按左结合分组:
System.out.println(1 + 2 + " 个任务"); // "3 个任务"
System.out.println("任务 " + 1 + 2); // "任务 12"
System.out.println("任务 " + (1 + 2)); // "任务 3"第一行先完成整数加法,第二行从第一个 + 开始就进入字符串连接。业务文本中凡是想先算数值,主动加括号最清楚。
空字符串连接也能把值转成字符串,例如 "" + 42,但 String.valueOf(42) 更直接地表达了转换意图。
indexOf 找不到目标时返回 -1。把这个结果直接传给 substring 会越界:
String fullName = "Lin-Ming";
int separator = fullName.indexOf('-');
if (separator >= 0) {
String familyName = fullName.substring(0, separator);
String givenName = fullName.substring(separator + 1);
System.out.println(familyName + " / " + givenName);
}这段代码体现了一个通用习惯:先验证索引或输入条件,再执行依赖该条件的操作。
字符串池可能让某些字面量之间的 == 恰好得到 true,但这不改变 == 的含义。不要用偶然结果推导内容比较规则;内容相等始终写 equals。
标准输入 System.in 提供字节流,Scanner 把它包装成更容易使用的文本扫描器。使用前需要导入并创建对象:
import java.util.Scanner;
public class Greeting {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入姓名:");
String name = input.nextLine();
System.out.println("你好," + name);
}
}程序读取前应先输出明确提示,否则用户只会看到程序停住,不知道它在等待什么。

nextInt 等方法读取下一个 token,nextLine 读取光标到行末之间的全部内容。混用时要关注换行符是否仍留在输入中。
Scanner 默认把空白当作分隔符,将输入拆成 token。常用方法可以分成两类:
token 方法会先跳过分隔空白,再读取下一个 token;nextLine 不基于 token,它返回当前光标到行末之间的所有内容。
下面的交互把输入缓冲区和扫描位置画出来。依次尝试 nextInt → nextLine 与“全部用 nextLine”两条路径,观察那个空字符串从哪里来。
看这段程序:
import java.util.Scanner;
public class ProfileBug {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("年龄:");
int age = input.nextInt();
System.out.print("个人简介:");
String bio = input.nextLine
用户输入年龄后按回车,nextInt() 取走数字 token,但行结束符仍在当前位置附近。紧接着的 nextLine() 读取“当前位置到这一行结尾”的剩余内容,于是可能立刻得到空字符串。
修复方式一,是在 token 读取后显式消费当前行剩余部分:
int age = input.nextInt();
input.nextLine(); // 消费年龄所在行的剩余内容和行结束符
System.out.print("个人简介:");
String bio = input.nextLine();修复方式二,是交互式输入统一按整行读取,再解析需要的数值:
System.out.print("年龄:");
int age = Integer.parseInt(input.nextLine().trim());
System.out.print("个人简介:");
String bio = input.nextLine();第二种方式让每一次提示都对应完整一行,状态通常更容易推理,也是本篇更推荐的命令行表单写法。代价是你要明确处理空行和非法数字。
nextInt() 遇到不符合整数格式的 token,会抛出 InputMismatchException,而且错误 token 仍可能留在扫描器中。可以先用 hasNextInt() 检查:
import java.util.Scanner;
public class SafeNumberInput {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入整数:");
if (input.hasNextInt()) {
int value = input.nextInt();
System.out.println("你输入了 " +
hasNextInt() 和 nextInt() 都可能等待输入。检查只说明“下一个 token 是否可解析”,不会替你消费这个 token。
如果选择整行读取,可以捕获解析异常:
import java.util.Scanner;
public class ParseLineInput {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入整数:");
String line = input.nextLine().trim();
try {
int value = Integer.parseInt(line);
异常处理的完整设计会在后续展开,这里先建立最小认识:外部输入不受程序控制,解析失败是必须考虑的正常分支。
扫描文件时应及时关闭 Scanner,通常用 try-with-resources。对包装 System.in 的扫描器,关闭它也会关闭底层标准输入;如果程序其他部分还要继续读,就不应在中途关闭。小型单入口命令行程序可以在所有输入结束后关闭,或让进程退出时统一释放。
不要通过“再多写几个 nextLine”碰运气修复输入问题。把输入写在纸上,标出每个读取方法消费了哪些字符、扫描位置停在哪里,换行陷阱就会变成可确定的状态变化。
语法不只发生在方法内部。类放在哪个包、编译器从哪里找类型、启动器把哪些文本传给 main,共同决定程序能否被正确编译和启动。
假设文件 PriceTool.java 的开头是:
package demo.shop;
public class PriceTool {
public static void main(String[] args) {
System.out.println("价格工具");
}
}类的完整名称是 demo.shop.PriceTool。按常见文件布局,源码位于:
项目目录/
└── demo/
└── shop/
└── PriceTool.java在项目目录执行:
javac -d out demo/shop/PriceTool.java
java -cp out demo.shop.PriceTooljavac -d out 把类文件写入 out,并建立对应包层级;java -cp out 告诉启动器从 out 这个类路径根目录查找类。运行时写完整类名,用点连接包与类,不写 .class 后缀。
类路径应指向包目录的上一级根目录。若类文件位于 out/demo/shop/PriceTool.class,类路径是 out,要启动的类名是 demo.shop.PriceTool。
Scanner 的完整类型名是 java.util.Scanner。下面两种写法表达同一个类型:
java.util.Scanner input = new java.util.Scanner(System.in);import java.util.Scanner;
Scanner input = new Scanner(System.in);java.lang 中的公开类型会被普通编译单元隐式导入,所以 String、System、Math 不用显式 import。同一个包中的类型也能直接用简单名。
通配导入 import java.util.*; 只导入该包的可访问顶层类型,不会递归导入子包。它也不意味着运行时一次加载整个包。学习阶段显式写具体类型,通常更容易看出依赖来自哪里。
执行:
java Greeting 小林 3启动器会把 "小林" 和 "3" 放进 args。它们全都是字符串,数字需要解析:
public class Greeting {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("用法:java Greeting <姓名> <次数>");
return;
}
String name = args[0];
int times = Integer.parseInt(args[1]);
System.out.
数组索引从 0 开始。没有参数时,args 通常是长度为 0 的数组,不是 null。读取 args[0] 前先检查 args.length,否则会发生 ArrayIndexOutOfBoundsException。
命令行参数由终端先进行拆分。想把带空格的内容作为一个参数,通常要使用当前 shell 支持的引号:
java Greeting "小林 同学" 3Java 程序最终只看到拆分后的字符串数组,原命令行中的引号通常不会成为字符串内容。不同 shell 的转义规则可能不同,涉及路径和特殊字符时要在目标环境测试。
经典流程明确分成两步:
javac Hello.java
java Hello第一步读取 .java 并生成类文件,第二步按类名启动。现代 JDK 也支持直接把源文件交给 java 的源文件模式,例如:
java Hello.java两种方式的目标相似,但命令含义不同。排查类路径、包目录和类文件时,传统两步流程能把“编译是否成功”与“启动是否成功”分得更清楚。
基础语法阶段的错误可以按发生阶段分为三类:
“没有红字”只排除了其中一部分问题。可靠程序来自可重复的检查过程。
';' expected编译器在当前位置期待分号。真正遗漏处可能在本行前半段或上一行,先检查最近的完整语句,不要只在箭头位置盲目补符号。
cannot find symbol编译器无法解析某个名字。依次检查拼写和大小写、变量是否在作用域内、是否先声明、类型是否需要导入、包和类路径是否正确。
variable ... might not have been initialized局部变量在某条可能路径上尚未赋值。给出合理初值,或调整分支结构,让编译器能确认所有到达读取点的路径都完成了赋值。
incompatible types右侧表达式类型不能按赋值规则转换为左侧类型。先写出两边类型,再判断应修改数据模型、使用安全的宽化,还是在确认信息损失可接受后显式窄化。
Could not find or load main class编译可能已经成功,但启动器没在类路径中找到指定完整类名。检查当前目录、-cp 根目录、包声明、类文件路径和命令中的点号名称。
NumberFormatException传给 parseInt、parseDouble 等方法的字符串不符合目标格式,或数值超出类型范围。输出带边界的原始输入,例如 input=[...],能看出隐藏空白和空字符串。
固定一个最小可复现输入。记录运行命令、输入内容、预期结果和实际结果,不要一边改输入一边猜原因。
从第一条编译错误或第一处实际偏差开始。后续错误可能只是前一个括号、类型或名称问题引发的连锁反应。
把复杂表达式拆成有名字的中间值,并同时打印“值 + 类型语境 + 边界”。例如分别打印整数除法前的 total、count 和转换后的商。
每次只修改一个假设,重新保存、编译、运行同一个输入。通过后再加正常值、边界值和非法值,确认修复没有制造新问题。
假设平均分算错:
int total = 250;
int count = 3;
double average = total / count;不要直接把整条公式来回改。先输出中间信息:
System.out.printf("total=%d, count=%d%n", total, count);
System.out.println("integer quotient=" + (total / count));
System.out.println("floating quotient=" + ((double) total / count));这样可以迅速确认偏差发生在整数除法阶段,而不是 printf、变量接收或后续展示阶段。
错误位置不总等于根因位置:
System.out.println("第一行"
System.out.println("第二行");第二行可能被标红,但根因是上一行缺少右括号和分号。调试语法错误时,从箭头位置向前检查最近一个未闭合的字符串、括号、大括号和语句。
以“读取人数并计算人均值”为例,至少准备:
total=100、count=4,验证主路径。count=1、total=0、接近整数上限的值,验证除法与范围。count=0、空字符串、字母、超大数字,验证错误提示和解析策略。测试的目标不是证明“我刚才那次运行没出错”,而是让每条重要规则都有反例压力。
遇到问题时,把“源码文本 → 编译结果 → 启动命令 → 输入字符 → 中间值 → 最终输出”排成一条链。第一处与预期不同的位置,就是当前最有价值的调查点。
下面的练习把变量、类型、字符串、输入和调试连在一起。先自己写出预测,再展开思路。
下面的程序能编译,但平均单价会丢失小数;输入商品数量后,备注还可能被读成空字符串。请找出两个原因并修复。
import java.util.Scanner;
public class CostCalculator {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("总价:");
int total = input.nextInt();
System.out.print("数量:");
int count = input.
写一个 OrderSummary 程序,依次读取商品名、数量和单价。商品名允许空格;数量必须是 int,单价是 double。输出时数量使用 %d,总价保留两位小数。先只处理合法输入,再思考空行和非法数字应该怎样提示。
不运行程序,先写出每个输出,并标注关键中间表达式的类型:
public class Prediction {
public static void main(String[] args) {
byte a = 10;
byte b = 20;
int sum = a + b;
int count = 3;
double ratio = count / 2;
double
实现 DiscountTool,运行方式为:
java DiscountTool 199.9 0.85第一个参数是原价,第二个参数是折扣系数。程序要检查参数数量,解析为 double,计算折后价,并输出两位小数。参数缺失或格式非法时,给出清楚提示。
否则,如果任一操作数是 long,另一侧转换为 long,结果是 long;剩余情况按 int 运算。
下一步可以在解析周围加入 try...catch,并验证 itemName 非空、quantity 大于零、unitPrice 不为负数。
字符串连接按左结合进行,先得到 "结果 1",再连接 2,所以 label 是 "结果 12"。
这个实现还可以继续增加业务边界,例如折扣系数是否允许大于 1,以及金额是否改用明确十进制规则的类型。边界不是由语法替你决定的,必须根据实际需求写进程序。