Java 册 · 配套教学 · 按《Java 2 实用教程》第 7 版第 1-3 章顺序

Java 基础语法教学

从"为什么这样写"讲到"怎么写",共八课。每课结构:概念讲解 → 可运行示例 → 要点提示 → 课堂练习(可展开看参考答案)。 覆盖教材前三章:程序结构、基本类型、类型转换、运算符、分支与循环、数组、输入输出、字符串。

8 课 · 含练习 讲解 + 示例 教材同序

第 1 课 程序骨架与运行方式

对应教材第 1 章。先搞清楚"一个 Java 程序是怎么跑起来的"。

Java 程序以类为单位组织,代码写在类的花括号内。源文件先由 javac 编译成字节码(.class), 再由 JVM 解释执行——这正是 Java 平台无关的原因:同一份字节码可以在任何装有 JVM 的机器上运行。 程序的入口是固定的 main 方法,JVM 从这里开始执行第一条语句。

// 文件名必须与 public 类名完全一致:Hello.java
public class Hello {
    public static void main(String[] args) {   // 程序入口,签名固定
        System.out.println("Hello, Java");     // 向控制台输出并换行
    }
}

// 编译与运行(命令行)
// javac Hello.java   → 生成 Hello.class(字节码)
// java  Hello        → JVM 执行(注意不带 .class 后缀)
要点
  • 一个源文件最多一个 public 类,且文件名必须与 public 类同名(大小写一致)。
  • main 方法签名固定:public static void main(String[] args)。
  • Java 区分大小写;语句以分号结尾;块用花括号划分。
课堂练习(3 题)
  1. 执行 java Hello.class 报"找不到或无法加载主类",原因是什么? 答:运行时应给出类名而不是文件名,正确写法是 java Hello。
  2. 一个文件里写了两个 public 类,编译会怎样? 答:编译报错。一个源文件只能有一个 public 类,且文件名必须与它同名。
  3. 简述 Java 实现平台无关的原理。 答:源码编译为与平台无关的字节码,各平台用自己的 JVM 解释执行,因此"一次编译,到处运行"。

第 2 课 标识符与基本数据类型

对应教材第 2 章。8 种基本类型,宽度固定、与平台无关。

变量是内存中的一块存储区域,类型决定它占多大空间、能放什么值。 Java 有 8 种基本类型,它们的字节数固定不变(不像 C/C++ 那样随平台变化)。 需要区分的是:成员变量有默认值,而局部变量必须先赋值才能使用。

// 标识符:由字母、$、_ 开头,后可跟数字;区分大小写;不能是关键字
// 习惯:类名首字母大写,变量/方法名首字母小写,常量全大写

boolean flag = true;        // true / false
byte    b = 127;            // 1 字节,-128 ~ 127
short   s = 32767;          // 2 字节
int     i = 2147483647;     // 4 字节,整数的默认类型
long    l = 100L;           // 8 字节,字面量加后缀 L
float   f = 3.14F;          // 4 字节,必须加 F,否则编译错误
double  d = 3.1415926;      // 8 字节,浮点的默认类型
char    c = 'A';            // 2 字节,单引号,Unicode

// 字面量写法
int hex = 0xFF, bin = 0b1010;      // 十六进制 / 二进制
long big = 1_000_000L;             // 下划线仅提高可读性
char nl = '\n', uni = '中';         // 转义字符与 Unicode 字符
要点
  • 成员变量默认值:int→0、double→0.0、boolean→false、引用→null;局部变量没有默认值。
  • float 字面量必须写 3.14F;较大的 long 值要加 L。
  • char 用单引号,String 用双引号,二者不可混用。
课堂练习(3 题)
  1. float f = 3.14; 为什么编译失败? 答:3.14 默认是 double 类型,赋给 float 会损失精度,必须写成 3.14F 或强制转换。
  2. byte b = 200; 能否通过编译? 答:不能,200 超出 byte 的取值范围(-128~127),需写成 byte b = (byte) 200;(会溢出)。
  3. 局部变量声明后直接使用会怎样? 答:编译错误(可能尚未初始化变量),必须先赋初值。

第 3 课 类型转换与字面量

对应教材 2.3。方向搞清楚,精度才不会莫名丢失。

小范围类型可以自动转换成大范围类型(不会丢精度);反过来必须强制转换,且可能丢精度或溢出。 表达式中不同类型混合运算时,整体会先提升为其中最高的类型。

// 自动转换方向:byte → short → char → int → long → float → double
int i = 10;
double d = i;             // 自动转换,安全

// 强制转换:可能丢失精度
double x = 3.99;
int n = (int) x;          // 直接截断,结果是 3(不是四舍五入)
byte bb = (byte) 300;     // 溢出,结果是 44

// 混合运算:整体提升为最高类型
int a = 5, b = 2;
System.out.println(a / b);        // 2,整数除法会截断
System.out.println(a / (double)b);// 2.5,先把其中一个提升为浮点
要点
  • 强制转换直接丢弃小数部分;四舍五入要用 Math.round()。
  • 整数与整数相除结果必为整数,求平均值前先转浮点。
课堂练习(2 题)
  1. 写出把 3.99 四舍五入为整数的表达式。 答:(int) Math.round(3.99),结果为 4。
  2. 为什么 1 / 2 * 3.0 的结果是 0.0? 答:先算 1 / 2,两个操作数都是 int,结果为 0,再乘 3.0 得 0.0。应写成 1.0 / 2 * 3。

第 4 课 运算符与表达式

对应教材 3.1。短路求值与自增是考试高频点。

运算符决定表达式如何求值。需要特别理解两点: ①短路求值——&& 左侧为 false 时右侧不再计算,|| 左侧为 true 时同理; ②自增位置——i++ 先用后加,++i 先加后用。

int a = 10, b = 3;
System.out.println(a / b);       // 3   整除
System.out.println(a % b);       // 1   取余
System.out.println(a++);         // 10  先用后加(此后 a 变为 11)
System.out.println(++a);         // 12  先加后用

boolean p = true, q = false;
System.out.println(p && q);      // false,左侧为 false 时右侧不求值
System.out.println(p || q);      // true ,左侧为 true 时右侧不求值

int x = 0b1100;
System.out.println(x & 0b1010);  // 8   按位与
System.out.println(x | 0b1010);  // 14  按位或
System.out.println(x ^ 0b1010);  // 6   按位异或
System.out.println(x << 2);      // 左移 2 位(相当于乘 4)

int max = (a > b) ? a : b;       // 三目运算符
a += 5;                          // 复合赋值(隐含强制转换)
要点
  • && / || 短路;& / | 两侧都求值(同时是位运算符)。
  • == 比较基本类型的值;比较引用类型时比较地址。
  • 优先级:算术 > 关系 > 逻辑 > 赋值,不确定就加括号。
课堂练习(3 题)
  1. int i = 5; int j = i++ + ++i; 求 i 与 j 的值。 答:i++ 取值 5 后 i 变 6;++i 先把 i 变 7 并取 7;j = 5 + 7 = 12,最终 i = 7。
  2. 什么情况下必须用短路与而不是 &? 答:当右侧表达式依赖左侧成立才有意义时,例如 s != null && s.length() > 0,用 & 会在 s 为 null 时抛异常。
  3. 比较两个字符串内容是否相同应该怎么写? 答:用 s1.equals(s2),不能用 ==(它比较引用地址)。

第 5 课 分支与循环

对应教材 3.2-3.6。程序"会判断、能重复"的关键。

顺序结构只能从上往下执行;分支让程序按条件选择路径,循环让一段代码重复执行。 switch 适合"一个变量取离散的若干值"的场景,注意每个 case 结束要写 break,否则会继续向下执行(穿透)。 for-each 是最简洁的只读遍历方式。

// 分支
if (score >= 90) {
    System.out.println("优秀");
} else if (score >= 60) {
    System.out.println("及格");
} else {
    System.out.println("不及格");
}

// switch:支持 byte/short/char/int、String、枚举
switch (grade) {
    case 'A': System.out.println("90 以上"); break;   // 漏写 break 会穿透
    case 'B': System.out.println("80 以上"); break;
    default : System.out.println("其它");
}

// while / do-while
while (n > 0) { sum += n; n--; }
do { sum += n; n--; } while (n > 0);        // 至少执行一次

// for 与增强 for
for (int k = 0; k < arr.length; k++) { sum += arr[k]; }
for (int v : arr) { sum += v; }             // for-each,只读遍历最方便

// break / continue
for (int k = 0; k < 10; k++) {
    if (k == 3) continue;                   // 跳过本次循环
    if (k == 8) break;                      // 结束整个循环
}
outer:                                       // 标签:跳出多层循环
for (int i = 0; i < 3; i++)
    for (int j = 0; j < 3; j++)
        if (i * j > 2) break outer;
要点
  • switch 中漏写 break 会继续执行下一个 case。
  • if / while 后不加花括号时只有第一条语句受控,建议始终写花括号。
  • for-each 中的循环变量是副本,不能用来修改数组元素。
  • do-while 至少执行一次,注意末尾分号。
课堂练习(3 题)
  1. 写出判断闰年的条件(能被 400 整除,或能被 4 整除但不能被 100 整除)。 答:year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
  2. switch 中故意不写 break 会发生什么? 答:发生穿透,从匹配的 case 开始依次执行后续所有 case 的语句,直到遇到 break 或结束。
  3. 如何用带标签的 break 跳出两层循环? 答:在外层循环前写标签(如 outer:),内层满足条件时执行 break outer;。

第 6 课 数组

对应教材 2.5。数组是引用类型,创建后长度不可变。

数组用来存放一组同类型数据。声明时用 int[] a,创建时用 new int[5] 指定长度, 创建后长度固定不变。数组变量保存的是引用,把它赋给另一个变量只是复制地址,两个变量指向同一块数据。 二维数组本质是"数组的数组",所以每行的长度可以不同。

int[] a = new int[5];          // 创建,元素默认值为 0
int[] b = {1, 2, 3, 4, 5};     // 静态初始化
int[][] m = new int[3][4];     // 3 行 4 列
int[][] m2 = {{1,2},{3,4,5}};  // 每行长度可不同

System.out.println(a.length);    // 5,length 是属性不是方法
System.out.println(m.length);    // 3(行数)
System.out.println(m[0].length); // 4(第一行的列数)

for (int v : b) { System.out.print(v + " "); }
System.out.println(Arrays.toString(b));   // [1, 2, 3, 4, 5]
Arrays.sort(b);                            // 升序排序
int[] copy = Arrays.copyOf(b, b.length);   // 复制(新数组)
要点
  • 下标从 0 开始,越界抛 ArrayIndexOutOfBoundsException。
  • int[] c = b; 只是复制引用,改 c 会影响 b;要独立副本用 Arrays.copyOf。
  • 遍历时 arr.length 是属性,不能写成 arr.length()。
课堂练习(3 题)
  1. 如何求二维数组所有元素之和? 答:双重循环 for (int[] row : m) for (int v : row) sum += v;
  2. 把数组 b 赋给 c 后修改 c[0],b[0] 会变化吗? 答:会。二者指向同一个数组对象,修改互相可见;需要独立副本应使用 Arrays.copyOf 或 clone。
  3. 如何对数组降序排序? 答:基本类型数组可先 Arrays.sort(b) 再倒序遍历;对象数组可用 Arrays.sort(arr, Comparator.reverseOrder())。

第 7 课 输入与输出

对应教材 2.4。让程序能与用户交互。

输入用 Scanner(在 java.util 包中,需要 import),输出用 System.out。 注意 next() 读到空白符为止,nextLine() 读一整行——二者混用是初学者的经典坑: nextInt() 之后紧跟 nextLine() 会读到空行。

import java.util.Scanner;

Scanner sc = new Scanner(System.in);
int    n  = sc.nextInt();
double d  = sc.nextDouble();
String s  = sc.next();        // 读到空白符为止
String ln = sc.nextLine();    // 读一整行
sc.close();

// 输出
System.out.print("不换行");
System.out.println("换行");
System.out.printf("%d %.2f %s%n", n, d, s);   // 格式化输出
// 格式符:%d 整数  %f 浮点  %.2f 保留两位  %s 字符串  %c 字符  %b 布尔
要点
  • 使用 Scanner 前需要 import java.util.Scanner;。
  • 格式符与参数必须按顺序、按类型对应,否则运行时报错。
课堂练习(2 题)
  1. 为什么 nextInt() 之后再调 nextLine() 读到的是空字符串? 答:nextInt() 只读数字,把换行符留在缓冲区,紧接着的 nextLine() 直接读到这个换行符并返回空串。解决办法是在中间多调用一次 nextLine() 消耗掉换行。
  2. 把 double 值 3.14159 按保留两位输出。 答:System.out.printf("%.2f%n", 3.14159); 输出 3.14。

第 8 课 字符串与综合练习

String 属于第 8 章,但基础阶段每段程序都会用到。

String 是不可变对象:任何"修改"都会产生新的字符串,原对象不变。 因此在循环中反复拼接字符串会产生大量临时对象,这种场景应使用 StringBuilder。 比较内容用 equals,== 只比较引用地址。

String s = "Hello";
s.length();                    // 5
s.equals("Hello");             // true,比较内容用 equals
s.equalsIgnoreCase("hello");   // 忽略大小写
s.charAt(0);                   // 'H'
s.substring(1, 3);             // "el",左闭右开
s.indexOf("ll");               // 2,未找到返回 -1
s.trim();                      // 去掉首尾空白
s.toUpperCase();               // "HELLO",原串不变
String[] parts = "a,b,c".split(",");
String.valueOf(123);           // "123"

StringBuilder sb = new StringBuilder();   // 频繁拼接时用
sb.append("a").append(1).reverse();
要点
  • String 不可变;循环拼接用 StringBuilder。
  • substring(a, b) 是左闭右开区间。
  • split 的参数是正则表达式,特殊字符需转义。
综合练习(3 题)
  1. 读入若干整数存入数组,输出最大值、最小值与平均值(保留两位)。 答:用 Scanner 读入 n 与 n 个整数存入 int[],遍历求 sum/max/min,最后 System.out.printf("avg=%.2f%n", sum / (double) n);
  2. 统计一行字符串中某个单词出现的次数。 答:用 split 切分后遍历计数,或用 indexOf(fromIndex) 循环查找。
  3. 为什么循环里写 s += "a" 效率低? 答:String 不可变,每次 += 都会新建 String 对象,n 次拼接产生 n 个临时对象;应改用 StringBuilder 的 append。

阶段自测

五道综合题,覆盖前八课的语法点。先自己写,再展开对照。

查看题目与参考答案(点击折叠)
  1. 读入 n 与 n 个整数,输出最大值、最小值与平均值(保留两位)。 答:用 int[] arr = new int[n] 存储,遍历求 sum/max/min,输出时用 System.out.printf("avg=%.2f%n", sum / (double) n);
  2. 判断一个正整数是否为素数。 答:循环到 Math.sqrt(n),若存在能整除的因子则不是素数:for (int i = 2; i * i <= n; i++) if (n % i == 0) return false;
  3. 去掉数组中的重复元素(保留首次出现的顺序)。 答:借助 LinkedHashSet:new ArrayList<>(new LinkedHashSet<>(Arrays.asList(arr))),或双重循环标记后复制到新数组。
  4. 统计一行字符串中字母、数字与其它字符的个数。 答:逐字符判断:Character.isLetter(c)、Character.isDigit(c),其余计入其它。
  5. 输出九九乘法表。 答:双重循环 for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) System.out.printf("%d×%d=%d\t", j, i, i * j); System.out.println(); }