Skip to content

Java 基础语法与程序结构

基础语法不是只会写 ifforclass,而是要理解 Java 程序从源码到运行时到底经历了什么。后面学习 Spring、MyBatis、JVM、多线程时,很多问题都会回到这里。

为什么要先学程序结构

Java 是强类型、面向对象、先编译后运行的语言。一个 .java 文件不会直接被操作系统执行,而是先编译成 .class 字节码,再由 JVM 加载和执行。

如果不理解这条链路,遇到下面问题时就容易只靠猜:

问题背后原因
main 方法写错后程序启动不了JVM 找不到固定签名的入口方法
包名和目录不一致导致类找不到编译器和类加载器按包名组织类
改了代码运行结果没变运行的可能不是刚编译出来的 class
方法参数改了调用方报错Java 方法调用由方法名和参数列表共同决定

Java 程序运行流程

mermaid
flowchart TD
    A["编写 .java 源文件"] --> B["javac 编译"]
    B --> C["生成 .class 字节码"]
    C --> D["JVM 类加载"]
    D --> E["字节码验证"]
    E --> F["解释执行或 JIT 编译"]
    F --> G["程序输出结果"]

关键点:

  1. 编译期检查语法、类型、方法是否存在。
  2. 运行期由 JVM 负责加载 class、创建对象、调用方法。
  3. Java 程序跨平台的原因是 .class 面向 JVM,而不是直接面向 Windows 或 Linux。

一个 Java 文件由什么组成

java
package com.company.order;

import java.math.BigDecimal;

public class OrderAmountCalculator {
    public static void main(String[] args) {
        BigDecimal price = new BigDecimal("99.90");
        int count = 3;
        BigDecimal total = calculateTotal(price, count);
        System.out.println(total);
    }

    public static BigDecimal calculateTotal(BigDecimal price, int count) {
        return price.multiply(BigDecimal.valueOf(count));
    }
}

结构解释:

结构作用
package声明这个类属于哪个包,用于避免类名冲突
import引入其他包里的类,减少全限定类名书写
class定义一个类,Java 代码通常写在类里
main程序入口,JVM 从这里开始执行
方法封装一段可复用逻辑
变量保存运行过程中的数据

main 方法为什么必须这样写

java
public static void main(String[] args)

这不是随便约定出来的写法,而是 JVM 启动普通 Java 程序时要查找的入口签名。

关键字为什么需要
publicJVM 需要从类外部调用它
staticJVM 不需要先创建对象就能调用
void程序入口不通过返回值给操作系统传业务数据
String[] args接收命令行参数

命令行参数 Demo:

java
public class ArgsDemo {
    public static void main(String[] args) {
        for (String arg : args) {
            System.out.println("参数:" + arg);
        }
    }
}

运行:

bash
javac ArgsDemo.java
java ArgsDemo dev 8080

变量和作用域

变量不是只有“能不能赋值”这么简单,还要看它在哪个范围内有效。

java
public class ScopeDemo {
    private static int classLevel = 10;

    public static void main(String[] args) {
        int methodLevel = 20;

        if (methodLevel > 10) {
            int blockLevel = 30;
            System.out.println(blockLevel);
        }

        // System.out.println(blockLevel); // 编译失败,blockLevel 只在 if 块内有效
    }
}

作用域原则:

变量位置生命周期和可见范围
成员变量对象存在期间存在,整个对象内部可访问
静态变量类加载后存在,属于类
局部变量方法执行期间存在,只在方法内部可访问
代码块变量只在 {} 内可访问

方法和重载

方法用于把一段逻辑命名。方法设计不好,业务代码就会变成一坨难以维护的流程。

java
public class MethodDemo {
    public static int add(int a, int b) {
        return a + b;
    }

    public static double add(double a, double b) {
        return a + b;
    }
}

这叫方法重载。Java 判断重载看的是:

  1. 方法名相同。
  2. 参数个数、参数类型或参数顺序不同。
  3. 只改变返回值类型不算重载。

流程控制

流程控制决定代码按什么路径执行。

java
public class OrderStatusDemo {
    public static String getStatusText(int status) {
        switch (status) {
            case 0:
                return "待支付";
            case 1:
                return "已支付";
            case 2:
                return "已发货";
            case 3:
                return "已完成";
            default:
                return "未知状态";
        }
    }
}

循环 Demo:

java
import java.math.BigDecimal;
import java.util.List;

public class OrderSumDemo {
    public static BigDecimal sum(List<BigDecimal> amounts) {
        BigDecimal total = BigDecimal.ZERO;
        for (BigDecimal amount : amounts) {
            if (amount == null) {
                continue;
            }
            total = total.add(amount);
        }
        return total;
    }
}

这里的 continue 表示跳过当前循环,break 表示结束整个循环。

编译期和运行期的区别

mermaid
flowchart TD
    A["编译期"] --> B["检查语法"]
    A --> C["检查类型"]
    A --> D["检查方法是否存在"]
    E["运行期"] --> F["创建对象"]
    E --> G["执行分支和循环"]
    E --> H["抛出运行时异常"]

举例:

java
String name = "Tom";
// name = 100; // 编译期报错,类型不匹配

String value = null;
// value.length(); // 运行期报错,NullPointerException

理解这个区别很重要。编译期能发现的问题越多,线上风险越低;运行期才暴露的问题,需要靠测试、日志、监控和异常处理兜底。

package、目录和 classpath

零基础最容易卡在“明明有这个类,为什么运行说找不到”。这通常不是语法问题,而是包名、目录、编译输出目录和 classpath 没对上。

Java 类的完整名字不是文件名,而是“包名 + 类名”。例如:

java
package com.company.order;

public class OrderApp {
    public static void main(String[] args) {
        System.out.println("order app");
    }
}

它的全限定名是:

text
com.company.order.OrderApp

源文件通常放在:

text
src/com/company/order/OrderApp.java

编译后 class 通常放在:

text
out/com/company/order/OrderApp.class

运行时不要写文件路径,而要写类的全限定名:

bash
javac -encoding UTF-8 -d out src/com/company/order/OrderApp.java
java -cp out com.company.order.OrderApp

流程可以这样理解:

mermaid
flowchart TD
    A["源文件 src/com/company/order/OrderApp.java"] --> B["javac 编译"]
    B --> C["输出 out/com/company/order/OrderApp.class"]
    C --> D["java -cp out 指定类搜索根目录"]
    D --> E["JVM 按包名拼路径查找 class"]
    E --> F["加载 com.company.order.OrderApp"]

-cp out 指定的是 classpath 根目录,不是 class 文件所在目录。JVM 会用类名 com.company.order.OrderApp 转换成 com/company/order/OrderApp.class,再到 classpath 根目录下面找。

常见错误怎么排

报错常见原因排查方式
Could not find or load main class运行类名写错、classpath 根目录写错、包名和目录不一致先确认 class 文件实际输出路径,再确认 java -cp 指向输出根目录
ClassNotFoundException运行期类加载器找不到类检查依赖 jar 是否在 classpath 或 Maven 依赖中
NoClassDefFoundError编译时有类,运行时缺类,或类初始化失败检查运行环境依赖、版本冲突、静态初始化异常
中文乱码源文件编码、编译编码、控制台编码不一致源文件 UTF-8,javac -encoding UTF-8,文件 IO 显式指定 UTF-8

ClassNotFoundExceptionNoClassDefFoundError 很像,但不一样。前者通常是代码主动加载类时找不到,例如 Class.forName("xxx")。后者常见于程序本来引用了某个类,编译时存在,运行时却缺依赖。

源码到字节码再到机器码

Java 不是简单“编译一次就变成本地机器码”。它的执行链路分层:

mermaid
flowchart TD
    A[".java 源码"] --> B["javac"]
    B --> C[".class 字节码"]
    C --> D["类加载器加载"]
    D --> E["字节码验证"]
    E --> F{"是否热点代码"}
    F -- "否" --> G["解释器逐条解释执行"]
    F -- "是" --> H["JIT 编译成本地机器码"]
    G --> I["程序继续运行"]
    H --> I

为什么要这样设计:

  1. .class 字节码让 Java 可以跨平台,同一份 class 可以在不同系统上的 JVM 中运行。
  2. 解释器让程序启动后可以较快开始执行。
  3. JIT 会把频繁执行的热点代码编译成本地机器码,提高长期运行性能。
  4. JVM 可以在运行过程中根据真实执行情况做优化,比如方法内联、逃逸分析、锁消除。

这也解释了为什么 Java 服务刚启动时性能可能还没到峰值,运行一段时间后热点代码被 JIT 优化,吞吐会更稳定。

方法签名和调用匹配

Java 方法不是只按名字查找,还会看参数列表。方法签名由方法名和参数类型组成,不包含返回值。

java
public class MethodMatchDemo {
    public static void print(int value) {
        System.out.println("int: " + value);
    }

    public static void print(long value) {
        System.out.println("long: " + value);
    }

    public static void print(Integer value) {
        System.out.println("Integer: " + value);
    }

    public static void main(String[] args) {
        print(1);              // 优先匹配 int
        print(1L);             // 匹配 long
        print(Integer.valueOf(1)); // 匹配 Integer
    }
}

重载匹配的大方向是:能精确匹配就精确匹配,其次才考虑类型提升、装箱、可变参数。业务代码不要设计一堆容易混淆的重载方法,否则调用方看不出最终会走哪个分支。

错误示例:

java
public void query(Long id) {}
public void query(String id) {}

// query(null); // 编译失败,null 同时可以匹配 Long 和 String,编译器无法决定

语句、表达式和短路

表达式会产生值,语句表示一条执行动作。初学者常见问题是把赋值、比较、逻辑判断混在一起,最后条件判断和自己想的不一样。

java
int stock = 10;
boolean enough = stock > 0; // stock > 0 是表达式

if (enough) {               // if 是控制语句
    stock = stock - 1;      // 赋值语句
}

逻辑运算里的 &&|| 有短路特性:

java
public static boolean isValid(String name) {
    return name != null && name.length() > 0;
}

如果 name == null,后面的 name.length() 不会执行,所以不会空指针。不要把顺序写反:

java
// return name.length() > 0 && name != null; // 可能空指针

最小可运行商业 Demo:订单导入参数校验

这个 Demo 把变量、方法、循环、分支、返回值和异常串起来,模拟批量导入订单时的基础校验。

java
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class OrderImportValidator {
    public static void main(String[] args) {
        List<String> rows = new ArrayList<String>();
        rows.add("1001,99.90");
        rows.add("1002,-1");
        rows.add("bad,row");

        for (String row : rows) {
            String error = validate(row);
            if (error == null) {
                System.out.println("导入成功:" + row);
            } else {
                System.out.println("导入失败:" + row + ",原因:" + error);
            }
        }
    }

    public static String validate(String row) {
        if (row == null || row.trim().length() == 0) {
            return "行内容为空";
        }

        String[] parts = row.split(",");
        if (parts.length != 2) {
            return "列数不正确";
        }

        try {
            Long.parseLong(parts[0]);
            BigDecimal amount = new BigDecimal(parts[1]);
            if (amount.compareTo(BigDecimal.ZERO) <= 0) {
                return "金额必须大于 0";
            }
            return null;
        } catch (NumberFormatException e) {
            return "订单号或金额格式错误";
        }
    }
}

这个例子为什么有学习价值:

  1. main 是入口,模拟程序启动。
  2. List<String> 保存多行导入数据。
  3. for 循环逐行处理。
  4. validate 方法把校验逻辑独立出来,主流程更清晰。
  5. if 提前返回错误,避免后面代码处理非法数据。
  6. try-catch 把格式错误转换成用户能看懂的失败原因。

如果不做这些基础校验,非法金额、错误行格式会继续进入数据库或后续流程,线上问题会从“导入失败”变成“脏数据修复”。

面试常问

Java 为什么要先编译成 class 再运行?
因为 Java 追求跨平台和运行期优化。源码先编译成平台无关的字节码,不同操作系统只要有对应 JVM 就能运行;JVM 运行时还能做类加载、安全校验、解释执行和 JIT 优化。

classpath 是什么?
classpath 是 JVM 查找 class 和资源文件的搜索路径。它通常指向编译输出目录或 jar 包。运行带包名的类时,-cp 要写包结构的根目录,后面运行类名要写全限定名。

编译期错误和运行期错误有什么区别?
编译期错误在 javac 阶段就能发现,例如语法错误、类型不匹配、方法不存在;运行期错误发生在程序执行时,例如空指针、数组越界、除零、类找不到、数据库连接失败。编译期问题靠类型系统解决,运行期问题靠校验、异常、日志、监控和测试兜底。

为什么局部变量没有默认值?
局部变量在方法栈帧里使用,Java 要求先明确赋值再读取,避免读到不确定值。成员变量属于对象或类,创建对象或加载类时会被初始化为零值。

商业开发常见场景

场景Java 基础语法怎么发挥作用
订单状态流转ifswitch 控制不同状态处理逻辑
金额计算方法封装计算规则,避免重复代码
参数校验分支判断非法输入,提前返回
批量导入循环处理每一行数据,并记录错误
启动参数配置main(String[] args) 接收环境参数

常见问题

为什么 Java 一个文件通常只写一个 public 类

一个 .java 文件最多只能有一个 public 顶级类,并且文件名要和这个 public 类名一致。这样编译器和类加载器可以更容易根据类名定位文件。

为什么局部变量必须先赋值才能用

局部变量存在线程栈里,Java 不允许读取未初始化的局部变量,目的是避免读取到不确定值。成员变量有默认值,局部变量没有默认可读值。

为什么不要写特别长的方法

长方法通常混合了校验、计算、查询、保存、返回等多个职责。后果是难测试、难复用、难排查。业务系统里建议一个方法只表达一个清晰动作。

本章小结

Java 基础语法的核心是理解程序结构、编译运行流程、变量作用域、方法封装和流程控制。后续所有高级能力,本质上都是在这些基础上继续叠加类型、对象、内存、并发和框架规则。