第 2 章 · 基本程序设计

有了第 1 章的环境,这一章开始写真正的代码:变量、八种基本类型、运算符、类型转换、控制台输入输出。这些是所有语言共有的地基,但 Java 的地基有几块和 C 长得不一样:类型长度跨平台定死、收窄转换必须显式、boolean 和整数彻底分家。本章把这些差异点讲透,它们日后都是编译器替你拦 bug 的本钱。

2.1 八种基本类型:长度是承诺

类型字节范围/取值典型用途
byte1-128 ~ 127字节流、文件
short2-32768 ~ 32767少见(兼容遗留)
int4±21 亿默认整数
long8±922 京时间戳、大数
float4IEEE 754 单精度少见(精度不够用)
double8IEEE 754 双精度默认浮点
char2UTF-16 码元单字符
boolean-true / false逻辑

两个和 C 的关键差异(计组课第 2 章的知识在这直接兑现)。其一,长度是规范承诺而非实现选择:C 的 int「至少 16 位,通常与机器字长相关」,Java 的 int 永远 4 字节,跨平台行为可预测。其二,boolean 独立于整数:C 里 if (1) 合法(非零即真),Java 里 if (1) 直接编译错误,「1 和 true 混用」这类 C 的经典笔误在 Java 不可能发生。

int width = 10;              // 变量
final int MAX = 100;         // 常量:final 修饰,再赋值编译错误

变量必须先声明后使用,局部变量还要先初始化(C 的「未初始化读是 UB」,计组第 14 章,在 Java 里直接编译错误拦下:variable x might not have been initialized)。

2.2 运算符:熟悉的配方,两处留神

算术 + - * / %、赋值 = += -=、比较、逻辑 && || ! 都和 C 同款。留神两处:

整数除法向零截断:7 / 2 == 3,-7 / 2 == -3(C 同款);除零是运行时异常 ArithmeticException: / by zero,不是 C 的 UB(计组第 14 章)。浮点比较别用 ==:计组第 2 章讲过 0.1 + 0.2 的故事,Java 照样中招,判断相等用 Math.abs(a - b) < 1e-9,金额计算用 BigDecimal。

前缀后缀 ++ 的坑:单独成句没区别,混进表达式里 a[i++] 和 a[++i] 的可读性灾难,第 5 章(数组)再展开。风格建议:自增自减永远单独成句,别炫技。

2.3 类型转换:宽转窄必须明说

int i = 42;
long l = i;          // 自动(拓宽):4 字节装进 8 字节,无损
double d = i;        // 自动:int → double
int j = (int) d;     // 强制(收窄):必须写 (int),丢小数
byte b = (byte) 200; // 强制:溢出回绕,得 -56(补码回绕,计组第 2 章)

Java 的立场:拓宽自动,收窄必须显式。C 会给警告但放行,Java 直接编译错误强制你表态。这个设计省掉一整类事故:C 代码里「double 赋给 int 丢了小数没人发现」的 bug,Java 里根本写不出来。long 字面量要带 L 后缀(9999999999L,不带会被当 int 编译错误:integer number too large),这个报错初学每周都会见一次,见到就检查数字后面有没有 L。

2.4 控制台输入输出:Scanner 与那三个 println

import java.util.Scanner;

public class Greet {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("你叫什么?");
        String name = sc.nextLine();        // 读一整行
        System.out.print("几岁?");
        int age = sc.nextInt();             // 读一个 int
        System.out.printf("%s 今年 %d 岁%n", name, age);
    }
}

输出三兄弟:print 不换行、println 换行、printf 格式化(和 C 的 printf 同一套 %d %s 模板,但 %n 是跨平台换行)。输入靠 Scanner 类(new Scanner(System.in),语法细节第 6、8 章补):nextInt 读整数、nextLine 读整行、hasNextInt 判断下一个是不是整数。

一个真实的坑我先替你踩了:nextInt 之后紧跟 nextLine 会读到空串。nextInt 只取走数字,留下换行符在缓冲区,下一个 nextLine 一口吞掉它,你得到空字符串。修法:nextInt 后补一个 sc.nextLine(); 把残留在往掉,或统一用 nextLine 读、再 Integer.parseInt 转换。

2.5 实例:从题到代码的完整走一遍

经典题「求三位数各位之和」,演示把数学翻译成代码的完整过程:

public class DigitSum {
    public static void main(String[] args) {
        int n = 472;
        int ones = n % 10;           // 个位:472 % 10 = 2
        int tens = n / 10 % 10;      // 十位:47 / 10 = 47,再 % 10 = 7
        int hunds = n / 100;         // 百位:472 / 100 = 4
        System.out.println(ones + tens + hunds);   // 13
    }
}

取余拆位、整除去尾,是这两章最常用的组合拳。那道「不借助临时变量交换两个数」的智力题(a=a+b; b=a-b; a=a-b;)可以玩,但工程里请老老实实用临时变量(可读性第一,且加法可能溢出),Java 还有更简单的选择,第 5 章讲到数组时再提。

常见错误

  • long 字面量忘写 L。报错 integer number too large,十之八九是这个。
  • 浮点当钱用。0.1 的无限循环二进制(计组第 2 章)在 Java 照样存在,金额一律 long(分)或 BigDecimal。
  • 整数溢出没察觉。int 在 ±21 亿封顶,阶乘 13! 就爆了;要大数换 long,再大换 BigInteger。
  • = 写成 ==(或反过来)。Java 里 if (x = 5) 编译不过(x 是 int 不是 boolean),这是 Java 静态类型送你的礼物;但 boolean 变量的 if (flag = true) 依然合法,手滑照样有。

练习

  1. 动手:验证 2.1 的表:System.out.println(Integer.MAX_VALUE) 和 Integer.MIN_VALUE 打出来是多少;Integer.MAX_VALUE + 1 的结果是什么(溢出回绕,和计组第 2 章对答案)?
  2. 动手:复现 nextInt/nextLine 的空串坑,再用「先读行再 parseInt」的方案修复。
  3. 手算:(int) 3.99、(int) -3.99、(byte) 130、(byte) -130 各是多少?对照计组课第 2 章的截断与回绕规则。
  4. 思考:Java 把「收窄必须显式」定成语言规范,C 只给警告。从「代码评审里警告堆积如山」的现实出发,评价「把警告升级为错误」这个设计决策的取舍,再想想 Kotlin、Go 这些后辈语言继承了谁的立场。