第一次学编程,最容易卡住的不是某个关键字,而是脑子里缺少一张完整地图:代码为什么能让硬件做事,javac 和 java 各自负责什么,JDK、JRE、JVM 又为什么总被放在一起讨论。
这一节先把这些关系接起来。读完后,你应该能独立解释一段 Java 源代码从保存到运行的全过程,也能在命令行中编译、运行并定位最常见的错误。
计算机不会“理解”程序的目的。它只会按照当前机器能够执行的指令,一步不漏地改变寄存器、内存和外部设备的状态。编程的核心,就是把人的目标逐层变成足够明确、可执行、可检查的步骤。
一台计算机可以剪视频、运行游戏,也可以控制生产设备。硬件并没有为每一种用途各造一套电路。真正让同一套硬件承担不同任务的,是被装入内存并由处理器执行的程序。
先看几个最基本的部件:
CPU 内部还可以继续拆分。算术逻辑单元(ALU)负责加减、比较和位运算;通用寄存器暂存操作数;程序计数器(PC)记录接下来要执行的指令位置。现代处理器还有多级缓存、流水线和多个核心,但这些优化没有改变最基本的抽象。
底层信息用二进制表示。一个二进制位(bit)只能取 0 或 1,八个位组成一个字节(byte)。内存可以看成许多带地址的位置,CPU 用地址定位要读写的内容。相同的位模式究竟表示整数、字符、颜色还是指令,不由位本身决定,而由当前指令和数据类型的解释规则决定。

程序载入内存后,CPU 按照“取指 → 译码 → 执行 → 更新 PC”的顺序持续运行。
如果把细节压缩到最小,CPU 会不断重复下面的过程:
根据程序计数器中的地址,从内存取出下一条指令。指令和数据在底层都是二进制位,具体含义由处理器的指令集决定。
译码电路判断这条指令需要什么操作数、要调用哪类执行单元,以及结果应写到哪里。
CPU 执行运算、数据搬移或跳转,并更新寄存器、内存或外部设备可见的状态。
程序计数器指向后续指令。普通情况下它向前移动;遇到分支、循环或方法调用时,它可能跳到另一个位置。
程序能做复杂工作,并不是因为单条机器指令很聪明,而是因为许多简单指令按严格顺序组合在一起。一个条件判断,底层可能包含比较和跳转;一个循环,则会让程序计数器反复回到某段指令。
下面的实验把程序计数器、内存和 ALU 放在同一块面板上。逐步执行几次,观察每一步究竟改变了什么。
取指—执行循环看起来是严格按顺序推进的,但键盘按下的时间、网络数据到达的时间、磁盘完成读取的时间都无法预先写死。这类在不可预测时刻发生的事情,叫作异步事件。
一种最直接的处理办法是轮询:程序不断询问“键盘有输入吗”“网卡有数据吗”。轮询容易实现,但如果设备大部分时间没有新消息,CPU 会把大量时间浪费在重复检查上。
设备通过总线或其他互连传递数据、地址和控制信号。不同设备的寄存器、协议和时序各不相同,设备驱动负责把操作系统的通用请求转换成设备能处理的操作。应用调用“读取文件”或“发送网络数据”时,背后往往经过了系统调用、驱动和硬件控制器多层协作。
中断提供了另一种机制。设备准备好数据后发送中断信号,CPU 保存当前执行现场,转去运行对应的中断处理程序;处理结束后,再恢复之前的程序。应用开发者通常不会亲自编写硬件中断处理程序,这部分工作由操作系统内核和设备驱动承担。
“中断”和 Java 的异常不是一回事。硬件中断主要用于通知 CPU 外部事件,Java 异常则是程序运行过程中传递错误或特殊情况的一套语言与运行时机制。两者都会改变控制流,但来源和处理层级不同。
操作系统负责管理处理器时间、内存、文件和设备。单核 CPU 在某一瞬间只能执行一个指令流,但调度器可以快速切换不同任务,让它们在一段时间内交替推进;多核 CPU 则能让多个指令流真正同时执行。
进程通常是资源和隔离的容器,线程是进程中的执行单元。一个 Java 应用启动后至少会有一个执行 main 的线程,JVM 还可能创建垃圾收集、编译等内部线程。当线程等待磁盘、网络或用户输入时,它可以进入阻塞状态,调度器便把 CPU 时间交给其他可运行线程。
定时器中断还能支持抢占式调度:即使某个线程不主动让出 CPU,操作系统也可以在时间片结束后暂停它。于是,“并发”描述多个任务在同一时间段内都有进展,“并行”则强调多个任务在同一时刻由不同核心实际执行。
机器语言与处理器架构绑定,形式接近二进制编码。直接用它开发大型程序,既难阅读,也难迁移。高级语言把常见概念提高到人能管理的层级:变量表示有名字的数据,表达式表示计算,if 表示选择,循环表示重复,方法表示可复用的任务。
不过,高级语言写成的源代码不能直接交给普通 CPU。它需要经过翻译。最常见的两种解释模型是:
这两种模型并不互斥。现代语言运行时经常先编译到中间表示,再解释执行其中一部分,并把频繁执行的热点代码即时编译为机器码。Java 就采用了这种组合思路。
一段程序可能遇到三类问题:
“能够编译”只说明代码通过了当前编译阶段的检查,不等于结果正确。编译器无法知道你心里想算的是含税价还是税前价,也无法替你证明所有输入下的业务规则都正确。
Java 没有把源码直接编译成某一种 CPU 的机器码,而是先生成面向 Java 虚拟机的类文件。类文件扩展名是 .class,其中包含字节码、常量池、类型和方法等结构化信息。

相同字节码可以交给不同平台的 JVM,但运行仍以 JVM、依赖库和类文件版本兼容为前提。
一个传统命令行程序会经历下面几步:
程序员把 Java 代码保存为 .java 源文件。源文件是文本,包含类、方法、语句和注释。
javac 读取源文件,执行词法、语法、类型和访问检查,并输出一个或多个 .class 类文件。
java 启动 JVM。类加载器找到需要的类,JVM 对其进行加载、链接和初始化;链接过程包含校验和准备,符号引用还会在适当时机解析。
JVM 解释执行字节码,并收集运行信息。频繁执行的方法可能被 JIT 编译器转换为当前 CPU 的机器码,之后直接执行优化后的本机代码。
Java 虚拟机规范定义的是类文件格式、字节码指令和运行时行为。它并不要求类文件一定来自 Java 源码。只要其他语言的编译器能生成合法类文件,也可以借助 JVM 运行。反过来,Java 源码也可以由其他工具转换为不同目标形式。
类文件自身带有版本信息。较新的编译器默认可能生成较高版本的类文件,旧 JVM 无法识别时会报 UnsupportedClassVersionError。面向旧运行环境发布时,应在构建阶段选择兼容的 JDK,并使用 javac --release 目标版本 约束语言特性、类文件版本和可用的标准 API。
更准确的说法是:同一份兼容的类文件,可以在提供相应 Java 平台实现的不同系统上运行。下面这些条件仍然会影响可移植性:
JVM 提供的是统一的执行抽象,不是“消除所有平台差异”的魔法层。只依赖标准 API 的纯 Java 逻辑通常更容易迁移;一旦调用本地代码或依赖操作系统行为,就要单独验证每个目标环境。
在下面的实验中切换目标系统和类文件版本,观察哪些内容保持不变,哪些部分必须由目标平台提供。
这三个词经常被画成套娃,但真正好用的理解方式是先看职责。

JDK 提供开发工具和运行能力;运行环境提供启动器、标准类库与 JVM;JVM 负责执行类文件。
早期常把 JRE 作为一个单独安装包:它是 JDK 的运行子集,能运行程序但没有 javac。现在,OpenJDK 和 Oracle 的常规下载以 JDK 为主,不再单独分发传统 JRE。生产程序还可以借助 jlink 等工具制作只包含所需模块的运行时镜像。因此,初学阶段直接安装 JDK 最省心。
不要用“JVM 里面有 JDK”来记忆。开发时使用 JDK 的工具;运行时由 java 启动 JVM,并向它提供标准库、应用类和配置。JVM 是执行引擎,JDK 是完整开发工具包。
安装后先执行:
java -version
javac -version两个命令都应存在,并且主版本通常应与你准备使用的版本一致。命令行究竟找到哪个程序,主要由 PATH 决定;JAVA_HOME 常被 Maven、Gradle、IDE 或脚本用来定位 JDK。只设置 JAVA_HOME 却没有正确配置 PATH,终端仍可能找不到 java 或调用另一套旧版本。
选择版本时,优先服从课程或项目的明确要求;没有约束时,选择当前受支持的 LTS 版本通常更稳妥。不要为了“数字更大”就启用预览特性,因为预览类文件需要在编译和运行两端显式开启,而且通常绑定特定版本。
从机器指令到大型应用,中间并没有突然出现“魔法”。程序只是把少数基本构件层层组合起来。
变量给一块可使用的数据起名字,类型则约束它能保存什么值、可以参与什么操作。初学时可以把变量想成有标签的盒子,但要记住这只是模型:编译器和 JVM 可能把值放在寄存器、栈帧或堆中,实际位置由实现决定。
int principal = 12_000;
double rate = 0.05;
double interest = principal * rate;这里的 int 表示整数,double 表示双精度浮点数,赋值语句把右侧表达式的结果存入左侧变量。12_000 中的下划线只用于提高数字可读性,不改变数值。
默认控制流会按书写顺序执行语句。分支让程序根据条件选择路径,循环让一段逻辑重复执行。
下面的示例可以直接编译运行:
public class InterestPreview {
public static void main(String[] args) {
int principal = 12_000;
double rate;
if (principal > 10_000) {
rate = 0.05;
} else {
rate = 0.04;
}
double interest =
如果需要处理一批账户,程序不会复制粘贴一百次计算语句,而会用循环逐个处理。这里揭示了高级语言的价值:我们表达的是“条件”和“重复”,编译器再把它们转换成比较、跳转和数据操作。
方法把完成某个任务的语句组织成一个有名字的单元。调用者只需要知道输入、输出和约定,不必每次展开内部步骤。大型程序之所以还能维护,靠的不是写得快,而是把问题拆成边界清楚的小块。
public class Greeting {
static String buildMessage(String name) {
return "你好," + name + "!";
}
public static void main(String[] args) {
System.out.println(buildMessage("小明"));
}
}方法 buildMessage 接收一个字符串并返回新字符串。main 不需要知道拼接细节,只使用它的结果。这种“先定义任务,再按名字使用”的能力,是后续模块化和面向对象设计的基础。
当程序规模变大,只靠从上到下罗列步骤很快会失控。软件设计通常同时使用两种视角:自顶向下把大问题拆成子问题,自底向上复用已经可靠的小组件。一个组件最好通过清楚的接口与外部协作,并把内部表示隐藏起来;这样内部实现改变时,使用者不必全部重写。
对象可以理解为拥有内部状态、并能响应方法调用的实体。类描述一类对象共有的数据结构和行为,对象则是按类创建的具体实例。例如,银行账户对象可以保存余额,并提供存款、取款和查询方法;外部代码不应随意破坏余额规则,而应通过对象提供的操作完成修改。
继承用于表达类型之间的扩展关系,子类可以继承并调整父类行为。多态则允许同一方法调用面对不同实际对象时表现不同。例如,同样调用 draw(),直线和圆形对象可以各自完成自己的绘制逻辑。
“Java 支持面向对象”不等于“Java 中所有东西都是对象”。int、double、boolean 等是基本类型。学习时更重要的是理解封装、职责和接口,而不是把任何代码都强行塞进复杂继承树。
按钮、文本框和窗口可以建模为对象。用户点击按钮会产生事件,事件处理方法随后被调用。它和前面的硬件中断不是同一层机制,但思维相通:程序不再只是一条从头走到尾的固定脚本,而是在等待事件,并在事件发生时执行对应处理逻辑。
命令行程序仍然很重要,因为它最容易观察输入、输出和错误;图形界面通常还要处理布局、事件线程和组件生命周期。先掌握顺序程序,再进入事件驱动开发,会少很多混乱。
网络通信也需要清楚约定。数据会被拆成适合传输的单元,地址帮助网络把它送到目标设备;域名需要解析为网络可用的地址;TCP、HTTP 等协议分别约定不同层面的通信方式。Java 提供网络、并发、加密和数据处理 API,但 API 只是工具,程序仍要正确处理超时、失败、权限和数据格式。
早期浏览器中的 Java Applet 已经退出主流并被移除,不应作为现代 Java Web 开发路线。今天常见的 Java 应用更多运行在服务器、桌面工具、构建系统和各类后端服务中。
把下面内容保存为 HelloJava.java。文件必须是纯文本,不要用会偷偷加入排版格式的文字处理软件保存。
public class HelloJava {
public static void main(String[] args) {
System.out.println("你好,Java!");
}
}运行后输出:
你好,Java!public class HelloJavaclass 声明一个类,HelloJava 是类名,public 表示这个顶层类可以被其他包中的代码访问。使用 javac 的常规文件组织方式时,公开顶层类 HelloJava 应保存在同名文件 HelloJava.java 中。Java 区分大小写,因此 HelloJava、hellojava 和 HelloJAVA 是三个不同标识符。
Java 标识符支持 Unicode 字符,首字符必须满足 Java 标识符起始规则,后续还可以包含合法数字;保留关键字不能拿来命名。工程中通常仍优先使用团队一致、易输入和易检索的命名。单独的下划线 _ 不是可用变量名,美元符号 $ 虽然在部分标识符中合法,通常留给生成代码或兼容场景。
public static void main(String[] args)这是最传统、兼容范围最广的应用入口签名:
public:启动器需要能够调用这个方法。static:不必先创建 HelloJava 对象,就能调用它。void:方法不向调用者返回值。main:启动器寻找的方法名。String[] args:接收命令行参数的字符串数组。参数名 args 可以改名,类型和入口结构才是关键。较新的 Java 版本还在降低小程序的入口样板,但学习经典形式有两个好处:它适用于大量现有项目,也能让类、方法、数组和静态成员尽早进入你的视野。
System.out.println(...)这行可以从左向右读:System 是 java.lang 包中的类,out 是它提供的标准输出流,println 是输出一个值并结束当前行的方法。java.lang 中的公开类型会被普通源文件自动导入,因此不需要单独写 import java.lang.System;。
括号中的 "你好,Java!" 是字符串字面量。右括号后的分号结束这条语句。两层大括号分别界定类体和 main 方法体,缩进本身通常不改变语义,却直接影响人能否快速看懂结构。
Java 常用两种普通注释:
// 单行注释:到这一行结束
/*
* 多行注释:可以跨越多行
*/注释会在编译阶段被当作非执行内容处理。它适合解释“为什么这样做”、边界条件和不明显的约束,不适合重复翻译每一行显而易见的代码。
先用命令行走通一次完整流程。IDE 当然可以使用,但它会自动替你保存、编译、设置类路径和启动 JVM。先看清底层动作,之后遇到 IDE 配置问题会更容易判断是哪一层出错。

编译失败或结果不符合预期时,都要回到源代码修改、保存并重新验证;运行命令不带 .class。
在终端分别执行 java -version 和 javac -version。如果前者存在而后者不存在,通常说明当前环境没有完整 JDK,或者 PATH 指向了错误位置。
新建一个练习目录并进入它。Windows PowerShell、macOS 和 Linux 都可以使用 mkdir java-start 与 cd java-start;查看当前文件时,Windows 常用 dir,macOS/Linux 常用 ls。
用纯文本编辑器创建 HelloJava.java,输入上一节的完整代码并保存。编译器只读取磁盘上已经保存的内容,不会读取编辑器里尚未保存的修改。
完整命令如下:
javac -encoding UTF-8 HelloJava.java
java HelloJava想把编译产物与源码分开,可以使用:
javac -encoding UTF-8 -d out HelloJava.java
java -cp out HelloJava-d out 把类文件写入 out 目录,-cp out 告诉启动器从该目录查找应用类。进入包和多文件项目后,这种目录意识会非常重要。
从 JDK 11 开始,简单的单文件程序还可以直接执行:
java HelloJava.java启动器会在内存中编译源文件并运行其中的入口类。它适合练习和小工具,但不会替代对 javac、类文件、类路径和构建流程的理解。课程刚开始时,建议至少手动走通几次“两步法”。
编译错误多时,不必从最后一条看起。后面的错误可能只是第一处语法破坏引起的连锁反应。先修最前面的一个或几个错误,保存并重新编译,错误列表往往会明显缩短。
下面的诊断实验准备了环境、编译、启动、运行和逻辑阶段的典型故障。先判断错误发生在哪一层,再选择最小修复动作。
Java 的知识很多,但起步顺序可以很朴素。每一步都应该有可运行的结果,而不是只背术语。
面对一个短程序,先预测输出,再亲自运行;然后解释每一步为什么发生;最后只改一个条件或输入,观察结果是否符合新预测。这个循环比抄十遍示例有效,因为它迫使你不断校准自己的执行模型。
建议保留一份错误记录,只写四项:原命令、完整报错、根因、最小修复。不要只截最后一行,也不要把“重装环境”当成默认答案。几周后你会发现,常见错误其实集中在文件名、保存状态、路径、类型和边界条件上。
这一阶段的合格标准不是“从不报错”,而是看到错误后能判断它属于环境、编译、启动还是程序逻辑,并用一个小实验验证判断。
先独立作答,再展开参考思路。综合题刻意把硬件、工具链和代码放在一起,因为真实开发中的错误通常不会按概念边界排队出现。
不用术语堆砌,用自己的话解释:当你执行 java HelloJava 并看到“你好,Java!”时,从类文件到屏幕之间至少发生了哪些关键步骤?
文件名是 hello.java,内容如下:
public class HelloJava {
public static void main(String[] args) {
System.out.println("你好,Java!")
}
}同学先执行 javac hello.java,修好后又执行 java HelloJava.class。请找出三个互相独立的问题。
一个程序在新 JDK 上编译,使用了仅提供 Windows 版本的本地动态库。团队把类文件和这个动态库复制到一台安装旧 JDK 的 Linux 服务器上,程序没有启动。请列出至少两个需要分别处理的问题。
走到这里,你应该能把“代码—编译器—类文件—JVM—操作系统—CPU”连成一条完整链路。下一次出现错误时,先判断它落在哪一层,再设计一个最小实验验证;这会成为后续学习 Java 语法、对象和工程工具时最可靠的排错习惯。
执行 javac -encoding UTF-8 HelloJava.java。成功时 javac 通常保持安静,并在当前目录生成 HelloJava.class;出现错误时,从第一条错误开始修复,保存后重新编译。
执行 java HelloJava。这里传的是类名,不带 .class。java 会启动 JVM、查找类并调用入口方法。