第 1 章 Java 程序设计概述
1.1 Java 程序设计平台
Java是一个完整的平台,有庞大的库,有大量可以直接抄的代码,有可以保证安全性、跨操作系统、内存垃圾回收的环境,集多种优势于一身。
1.2 Java 白皮书的关键术语
简单性:几乎所有编程语言的推出的目的都是优化上一代编程语言的不足,Java就是在C++的特性上剔除了许多难以理解的功能。而且刚刚推出时完整占用仅在Kb级别 面向对象:(一种程序设计技术)将重点放在数据上而非工具上。 分布式:网络功能强大,可以打开和访问云端对象。 健壮性:其指针模型可以消除重写内存和损坏数据的可能,编译器可以在运行前检测出问题。 安全性:具备沙箱功能,不可信代码隔离运行。 体系结构中立:编译后生成的字节码可以在任何有JRE的环境运行,和计算机体系无关。 可移植性:基本数据类型和运算行为是明确的,比如int类型的大小在任何机器上都是固定的。 解释性:不是纯编译型语言,先编译出字节码之后解释器翻译成机器码 高性能:靠JIT即时编译,把运行次数最多的代码片段直接编译成机器码。现代的优化直逼C/C++ 多线程:第一个在语言层面内置多线程,无需附加库。 动态性:按需动态加载新代码。
以上特性需要在后续学习中体会,纯看理解不了
1.3 Java applet 与 Internet
运行在网页上的java代码是applet,开发者在网页上设计java程序发布字节码,用户端直接用本地解释器运行,当时很牛,但马上被flash碾压。
1.4 Java 发展简史
Java 诞生于 1991 年 Sun 公司的 “Green 项目”。James Gosling 等人最初的设想是给嵌入式消费电子产品(电视机顶盒、遥控器之类)写一门语言,当时取名 Oak(橡树)。可惜产品没有找到市场,项目一度濒临放弃。
转机出现在 1995 年。互联网爆发式增长,Sun 发现网络上的设备五花八门、平台极不统一,而 Oak 的跨平台特性(编译成字节码、由各平台 JVM 解释执行)正好命中了 Web 时代的需求。于是 Oak 改名 Java 正式发布,并借助浏览器运行小程序(applet)一战成名。“一次编写,到处运行”(Write Once, Run Anywhere)成为 Java 最深入人心的口号。
此后 Java 一路快速迭代,几个关键节点:
- 1996 JDK 1.0 正式版
- 1997 JDK 1.1:内部类、反射、JDBC、RMI
- 1998 Java 2(1.2):Swing、集合框架、JIT 编译器
- 2004 Java 5:泛型、注解、枚举、自动装箱、并发包 —— 现代 Java 的基石
- 2006 Java 6:脚本引擎、JDBC 4.0
- 2009 Oracle 收购 Sun 公司
- 2011 Java 7:try-with-resources、switch 支持字符串、NIO.2
- 2014 Java 8:lambda、Stream、java.time、接口默认方法 —— 另一个里程碑
- 2017 Java 9:模块系统、JShell;此后改为每半年发一版
- 2018/2021/2023 Java 11 / 17 / 21:长期支持版本(LTS)
只要抓住两个关键版本就够了:Java 5 定下了泛型、注解、并发这些基础语法;Java 8 是写法的分水岭,lambda 与 Stream 让 Java 从”啰嗦的 C++ 改良版”进化成了现代化的语言。之后的版本大多是在这个地基上做增量。本书正是基于这一生态展开的。
1.5 关于 Java 的常见误解
“Java 很容易学” 语法确实比 C++ 温和,但”学语法”和”会写程序”是两码事。Java 真正的难度在生态:JVM、集合、并发、框架。入门容易,精通极难——就像 C 语法也不难,指针和内存才是坎。
“Java 能取代所有语言” 没有万能语言。C/C++ 统治系统编程与游戏引擎,Python 主导 AI,JavaScript 垄断浏览器,Go/Rust 在云原生崛起。Java 的天下是企业级后端,它再强也取代不了所有场景。
“Java 不过是换了个写法的 C/C++” 这是最值得掰扯的一条。两者的差异不在语法在运行模型:C++ 编译成机器码直接跑,Java 编译成字节码交给 JVM;C++ 手动管内存,Java 自动回收;C++ 指针能算数,Java 引用不行。同样的逻辑任何语言都能表达,但各自解决不同层面的问题——这正是本系列要慢慢体会的。
“Java 是闭源的” 反了。OpenJDK 是开源实现(GPL 许可),语言演进走公开的 JSR 流程。Oracle JDK 也只是基于 OpenJDK 的发行版。把「有大厂主导」说成「闭源」是个误会。
“JavaScript 是 Java 的简易版” 俩名字都带 “Java” 而已。Java 是 Sun 的静态类型面向对象语言,跑在 JVM;JavaScript 是 Netscape 为浏览器发明的脚本语言,跑在浏览器引擎。语法、类型系统、运行环境全都不一样,纯属当年蹭热度起的名字。
“Java 性能低下” 20 年前的刻板印象。早期纯解释执行确实慢,但 JIT 会把热点代码编成机器码,现代 JVM 峰值性能逼近 C++(大厂的高并发服务很多就是 Java)。真正的代价是启动慢、吃内存——它是「用启动速度和内存换运行性能」。
“Java 写程序容易有安全风险” 恰恰相反,Java 的安全设计是核心卖点:没有指针算术杜绝内存破坏、字节码校验、数组越界检查、沙箱机制。C/C++ 里缓冲区溢出、野指针这类经典漏洞,在 Java 里从机制上就没了。Java 的安全问题更多在应用层(框架配置、依赖漏洞),那是所有语言都躲不掉的。
第 2 章 Java 编程环境
Hello.java(源代码) │ ① javac Hello.java ▼ Hello.class(字节码 bytecode,磁盘文件) │ ② java Hello(启动 JVM) ▼ JVM:类加载 → 字节码校验 → 执行引擎 │ ③ 解释执行 + JIT 即时编译(热点代码) ▼ 机器码(二进制,内存中,CPU 真正执行)
相关名词解释
JDK 开发工具包,写代码必备,自带JRE JRE 运行时环境,可运行Java程序。 Java SE 标准版,用于客户端或服务器场景。 Java ME 微型版,用于小型设备,嵌入式场景。 Jakarta EE 企业版 Java 规范(原 Java EE / J2EE) OpenJDK 标准版的开源实现。 HotSpot OpenJDK 默认的 JVM 实现(内含 JIT 即时编译、垃圾回收、类加载等) LTS 长期支持版。很多软件都有长期支持版,包括Java。 JVM Java 虚拟机,字节码的执行引擎,JDK/JRE 的核心 javac Java 编译器,把 .java 源码编译成 .class 字节码 jar Java 归档工具,把多个 .class 和资源打包成一个文件 JIT 即时编译,JVM 运行时把热点字节码编译成机器码 JShell Java 9+ 的交互式命令行,边写边试
以下可以等用到再了解
javadoc 从源码注释生成 HTML 文档的工具 JPMS Java 平台模块系统(Java 9),JDK 与项目均可模块化 jlink Java 9 工具,把 JDK 模块与你的代码打包成精简自包含运行时 JMM Java 内存模型,规定多线程下共享变量的可见性规则 JMH 微基准测试框架,测 Java 代码真实性能 JFR Java Flight Recorder,JVM 内置的性能诊断记录器 JMC Java Mission Control,图形化查看 JFR 数据、监控 JVM JDBC Java 数据库连接标准 API JPA Java 持久化 API,ORM 标准 JSP Java Server Pages,老式网页模板,已过时
2.1 安装 Java 开发工具包
Java 的开源本体只有 OpenJDK 一份,Azul Zulu、Amazon Corretto、Eclipse Temurin、Oracle JDK 等全部是基于这同一份源码编译出的发行版——内核相同,差异只在打包方式、支持周期和性能优化。类比 Linux 内核 vs Ubuntu/Fedora。
运行安装程序一路下一步即可完成安装,除非你非要编译安装。记得勾选添加系统环境变量、Add to PATH一类的标识。
运行以下命令,即可查看java版本。(一般装cli软件这一步是必须做的)
javac --version如果报错未找到command not find,多半是你忘了勾选添加环境变量,win用户去设置内找编辑系统环境变量的对话框,手动把你安装java的根目录添加于此。unix-like用户(Linux、MacOS)去~/.zshrc或~/.bashrc文件加一行export PATH=jdk/bin:$PATH(jdk/bin是linux默认安装位置,如果你装在别处请改写)
2.2 使用命令行工具
学习Java需要具备一定的命令行基础,Java SE相关笔记中会体现一些基本用法。命令行主流教学基本都是 Unix‑like,相关深入用法在 Linux 基础与命令行 节点深入详解。
新建hw.java文件,输入
public class Hello { public static void main() { System.out.println("Hello World!"); }}在同级目录下,使用命令
javac Hello.java#先用编译器生成class字节码java Hello#再用JVM翻译成机器码,输出第一个hello world以上过程初学者可能会发现一些必须要遵守的写java程序必须要遵守的原则,简称语法。遵守这些规则怪谈才能不出bug。
2.3 使用集成开发环境
集成开发环境(IDE,Integrated Development Environment)把编辑、编译、运行、调试、代码补全等工具整合到一个图形界面里,省去手动敲命令的麻烦。
本系列使用 IntelliJ IDEA(JetBrains 出品),业界主流的 Java IDE。它分两个版本:
- Community(社区版):免费开源,本阶段够用
- Ultimate(旗舰版):付费,内置 Spring、数据库等企业开发支持——后面走后端路线时用得上
用 IDEA 跑 Hello World,本质就是把 2.2 的命令行两步(javac 编译、java 运行)搬进图形界面:新建项目 → 指定 JDK → 写代码 → 点运行。
2.4 JShell
JShell 是 Java 9 起自带的交互式命令行工具(REPL,Read-Eval-Print Loop),类似 Python 的交互式 shell:敲一行、立刻看到结果,不用建文件、编译、运行。
终端敲 jshell 进入,试几条:
jshell> System.out.println("Hello World!")Hello World!
jshell> 1 + 1$1 ==> 2
jshell> "abc".repeat(3)$2 ==> "abcabcabc"与 Python shell 的手感差异源于语言本身:Java 声明变量要写类型(int x = 5;),语句结尾要分号。而且 JShell 只是「试用台」——验证语法、算表达式、试 API 很方便,而且还能使用tab键补全查函数名,但正经程序还是走 2.2 的编译运行或 2.3 的 IDEA。对比 C:C 没有官方 REPL,想试个语法得写文件编译,Java 这一点更顺手。
第 3 章 Java 的基本程序设计结构
3.1 一个简单的 Java 程序
从 Hello World 起手,对照 C 最容易看清 Java 的程序骨架,很多新概念、新名词在这里先露个脸、留个印象,具体的深入剖析需要到之后的章节。
public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); }}类名与文件名:public class Hello 声明一个公共类,类名 Hello。类名习惯以大写字母开头;源文件名必须与公共类同名(Hello.java)。Java 里所有内容都必须放进类的容器中——C 可以在文件顶层写自由函数,Java 不行,这就是「万物皆类」的起点。
访问修饰符:public 表示「公开」,谁都能访问。Java 的类、方法、字段前面几乎都有修饰符(public / private 等)控制可见性,C 没有对应概念,只能靠文件作用域和头文件的约定来模拟。
main 方法——程序入口:public static void main(String[] args),逐词拆解:
public:JVM 要从外部调用它,必须公开static:静态方法,不需要创建对象就能调用void:不返回值String[] args:命令行参数数组,对应 C 的int argc, char *argv[](Java 只保留了参数那半,argc用args.length代替)
执行 java Hello 时,JVM 从 Hello 类的 main 方法开始运行。Java 里函数叫方法(method),等价于 C++ 的成员函数。
调用方法:通用语法是 对象.方法(参数)。System.out.println("Hello World!") 中,System 是一个类,out 是它内部代表标准输出的对象,println 是对象的方法——对应 C 的 printf。区别在于 C 调用的是库函数,Java 调用的是对象的方法,这是面向对象和面向过程的第一处体感差异。
大小写敏感:Java 严格区分大小写,Hello 和 hello 是两回事,main 必须小写。C 同样区分大小写,但 Java 的驼峰命名惯例(类名大写开头、方法变量小写开头)从一开始就要适应。
3.2 注释
几乎所有编程语言的注释都不会出现在可执行程序中——它们只是写给编译器和人看的说明,编译时被直接丢弃。
三种注释方式,一段代码全演示:
public class CommentDemo { /** * 文档注释:/** */ 之间的内容, * 可用 javadoc 工具生成 HTML 文档 */ public static void main(String[] args) { // 单行注释:// 到行尾都被忽略 System.out.println("Hello"); // 也可以跟在代码后面
/* * 多行注释:/* */ 之间的内容 * 跨越多行全部忽略 */ System.out.println("World!"); }}不能嵌套:三种注释都不能互相嵌套,尤其多行注释——/* 外层 /* 内层 */ 外层 */ 是错的:编译器遇到第一个 */ 就认为注释结束,后面剩的 外层 */ 会被当成代码,直接编译报错。
对比 C:// 和 /* */ 与 C 一致;/** */ 是 Java 新增的文档注释(C 没有),配合 javadoc 工具生成 API 文档。
小提示:示例中多行注释和文档注释每行开头的 * 并非语法要求,只是排版风格——Java 只认开头的 /* 或 /** 和结尾的 */,中间怎么写都行,IDEA 会自动帮你补 *。
3.3 数据类型
Java 有 8 种基本数据类型,每种的大小在所有平台上都固定不变——这是和 C 最根本的差异之一:C 的 int 在 16/32/64 位机器上是 2/4/4 字节,Java 的 int 永远是 4 字节,跨平台行为完全一致。
| 类型 | 存储 | 取值范围 | 字面量后缀 |
|---|---|---|---|
byte | 1 字节 | -128 ~ 127 | 无 |
short | 2 字节 | -32,768 ~ 32,767 | 无 |
int | 4 字节 | -21.4 亿 ~ 21.4 亿 | 无(整数默认) |
long | 8 字节 | -922 亿亿 ~ 922 亿亿 | L / l |
float | 4 字节 | 约 ±3.4E38(6~7 位有效数字) | F / f(必须写) |
double | 8 字节 | 约 ±1.8E308(15 位有效数字) | D / d(可省,浮点默认) |
char | 2 字节 | 0 ~ 65,535(无符号) | 单引号 'A' |
boolean | 不规定 | true / false | 无 |
(E 是科学计数法,3.4E38 = 3.4 × 10³⁸)
整型:整数默认是 int,超过范围加 L(后缀建议大写,小写 l 和数字 1 长得太像)。支持多种进制前缀:
int dec = 10; // 十进制(默认)int oct = 010; // 八进制:0 开头,值 = 8int hex = 0x10; // 十六进制:0x 开头,值 = 16int bin = 0b10; // 二进制:0b 开头(Java 7+),值 = 2长数字可以用
_分组(Java 7+,编译时忽略):1_000_000就是 100 万。_不能放在开头/结尾、小数点两侧、以及L/F后缀前,比如_100、100_.0都非法。
vs C:Java 没有无符号数。C 有
unsigned int,Java 所有整数类型都是带符号的,唯一的”无符号”是char——但它是字符类型,不做算术。
浮点型:带小数默认是 double,存 float 必须写 F 后缀。遵循 IEEE 754 标准,浮点运算有 3 种特殊结果:
Double.POSITIVE_INFINITY正无穷,如1.0 / 0Double.NEGATIVE_INFINITY负无穷,如-1.0 / 0Double.NaN非数字,如0.0 / 0
判断 NaN 必须用
Double.isNaN(x)——因为 NaN 不等于任何值,包括它自己,x == Double.NaN永远为 false。
字符型:char 是 2 字节无符号(C/C++是1字节),直接表示 Unicode 字符。有个著名大坑:Java 在解析代码之前就先处理 Unicode 转义(\uXXXX),所以写 Windows 路径 "c:\users" 会编译报错——\u 被当成 Unicode 转义开头,而 \users 不是合法的十六进制序列。改成 "c:\\users" 或 "c:/users" 就好。
顺带解释码点(code point):Unicode 给每个字符分配的唯一编号就是码点,范围 0 ~ 0x10FFFF(约 110 万)。
char只有 16 位,只能直接装 0~65535 的码点(基本多语言平面);更大的码点(如 emoji)要两个char拼成「代理对」表示。意思就是码点可能是一个char也可能是两个char拼起来的代理对。这就是 3.6 字符串「码点与代码单元」要展开的话题。
布尔型:boolean 只有 true / false,和 C/C++ 最大的区别是不能用数字代替——if (1) 在 Java 里是编译错误,必须写布尔表达式。这也顺带杜绝了 C 里”赋值写进条件”那类经典 bug。
3.4 变量与常量
声明变量:先声明后使用,命名规则:
- 以字母开头,后面可接字母、数字、
_、$,大小写敏感,注意不能用关键字当变量名(class、int、if…) - Java 的”字母”包含 Unicode 字符,支持中文变量名
- 惯例:驼峰命名、下划线命名,如
int totalCount; int total_count;
double salary; // 声明int i, j; // 一行声明多个-
不区分声明和定义:C/C++ 里声明(告诉编译器类型)和定义(分配存储)是两回事,于是有
extern、头文件、前置声明。Java 没有这套——每个声明天然就是定义,变量一旦声明就真实存在、占存储,因此也不需要头文件。 -
初始化:Java 编译期强制要求局部变量使用前必须赋值,否则直接编译报错:
int x;System.out.println(x); // 编译错误:变量 x 可能尚未初始化C/C++ 里这属于未定义行为(可能读到垃圾值),Java 把这种错误直接拦在编译期。
- 作用域(块作用域):变量在声明它的块
{ }内有效,块结束即失效,这点和 C 一致。但有个关键差异——Java 禁止遮蔽(shadowing):
int x = 0;{ int x = 1; // C/C++ 合法(内层遮蔽外层),Java 编译错误!}C/C++ 允许内层块重新声明同名变量(内层隐藏外层),Java 直接拒绝同名局部变量同时存在于同一作用域链。另外,Java 没有全局变量:C 有文件作用域的全局变量,Java 一切都在类内,最接近的等价物是类里的
static字段(详见后文对象与类)。
- 常量:用
final声明,只能赋值一次:
final double CM_PER_INCH = 2.54;对比 C:C 用
#define(预处理器文本替换,无类型检查)或const(编译器只读)。Java 的final语义更强——基本类型final变量的值不可变。类级常量用static final声明。
- 枚举类型:Java 5+ 引入,定义一组有限的值:
enum Size { SMALL, MEDIUM, LARGE }C 也有
enum,但 Java 的枚举是完整的类,能力远超 C(能带字段和方法),细节放到继承那篇展开。
3.5 运算符
- 算术运算符:
+ - * / %与 C 一致:
int a = 10, b = 3;a / b // 3:整数除法直接截断(向 0 取整)a % b // 1:取余一个 vs C 的差异:Java 里整数除零直接抛异常(ArithmeticException),浮点除零则是无穷或 NaN;C 的整数除零是未定义行为,程序可能悄悄崩掉。
- Math 类:Java 的数学函数都收在
Math类里,静态调用即可,不需要#include <math.h>也不用链接-lm:
Math.sin(x) // 正弦Math.cos(x) // 余弦Math.tan(x) // 正切Math.atan(x) // 反正切(弧度)Math.atan2(y, x) // 直角坐标 → 极坐标的角度Math.exp(x) // e^xMath.log(x) // 自然对数 ln(x)Math.log10(x) // 以 10 为底的对数Math.pow(a, b) // a 的 b 次方Math.PI // 圆周率 πMath.E // 自然常数 e注意:三角函数参数是弧度,不是角度。
- 隐式类型转换:小范围类型自动转大范围类型(隐式「拓宽」转换),合法转换如图:
byte → short → int → long → float → double ↑ char实线箭头无损、可隐式转换。另有几条虚线转换(int→float、long→float、long→double)虽合法但可能损失精度,因为 float/double 的尾数位有限。
隐式转换:
- 赋值:
double d = 3;(右侧自动拓宽成左侧类型)- 算术运算:
3 + 3.14(窄操作数自动提升成宽操作数)
二元运算时数值会自动提升:操作数里有 double 结果就是 double,其次 float、long,否则一律提升到 int——所以 byte + byte 的结果也是 int。原因是 JVM 的字节码指令没有 byte/short 级别的算术运算,一律按 int(或更大)宽度计算,这和 C 的整型提升(integer promotion)如出一辙。后果就是:
byte a = 10, b = 20;byte c = a + b; // 编译错误:a + b 是 int,不能直接赋给 bytebyte d = (byte) (a + b); // 正确:强转回来- 强制类型转换:大范围转小范围(「缩小」转换)必须显式强转,语法
(目标类型) 表达式:
double x = 9.997;int n = (int) x; // 9:直接截断小数,不是四舍五入- 想要四舍五入用
Math.round(x),不是强转 - 大转小可能溢出或截掉高位(如
int→byte),结果可能出乎意料 boolean不能转数值,数值也不能转boolean
- 赋值运算符:
=之外还有复合赋值+=-=*=/=%=&=|=^=<<=>>=>>>=:
x += 4; // 等价于 x = x + 4;一个小坑:复合赋值自带一次隐式强转——
byte b = 1; b += 1;合法(等价于b = (byte)(b + 1)),而b = b + 1;编译报错。
- 自增自减:
++--与 C 完全一致,注意前后缀的区别:
int x = 5;int y = x++; // y = 5,先用后加int z = ++x; // z = 7,先加后用- 关系与逻辑运算符(返回 boolean):全部列出来,按优先级从高到低:
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 1 | ! | 逻辑非 |
| 2 | < > <= >= | 关系比较 |
| 3 | == != | 相等判断 |
| 4 | & | 与(不短路) |
| 5 | ^ | 异或 |
| 6 | | | 或(不短路) |
| 7 | && | 与(短路) |
| 8 | || | 或(短路) |
vs C 的核心差异:
&&和||只能操作 boolean,没有 C 里「非 0 即真」那一套,if (1)直接编译错误。短路:&&||左边能决定结果时右边不会执行;&|不短路,两边都会算。
- 三目运算符:
条件 ? 值1 : 值2,与 C 一致:
int max = (a > b) ? a : b;- switch:传统写法和 C/C++ 差不多(会 fall-through,记得
break),Java 支持 4 种用法,逐一来看:
// ① 整数 / 字符:传统写法,会贯穿,记得 breakint day = 2;switch (day) { case 1: case 2: System.out.println("工作日"); break; case 6: case 7: System.out.println("周末"); break; default: System.out.println("非法");}
// ② 字符串(Java 7+)String fruit = "apple";switch (fruit) { case "apple": System.out.println("苹果"); break; case "banana": System.out.println("香蕉"); break; default: System.out.println("未知");}
// ③ 枚举enum Color { RED, GREEN, BLUE }Color c = Color.GREEN;switch (c) { case RED: System.out.println("红"); break; case GREEN: System.out.println("绿"); break; case BLUE: System.out.println("蓝"); break;}
// ④ switch 表达式(Java 14+):箭头写法,不贯穿,整体可作为表达式int month = 4;int days = switch (month) { case 2 -> 28; case 4, 6, 9, 11 -> 30; default -> 31;};要点:①②③是语句(执行完就完事),④是表达式(能整体返回一个值赋给变量);前三者会贯穿,④的箭头写法天然不贯穿。箭头分支只有一句表达式时,直接写在
->后面就是返回值;要写多句话就用{ }块,块内必须用yield给出值:
int days = switch (month) { case 2 -> 28; default -> { System.out.println("走默认分支"); yield 31; // 块内用 yield 返回值 }};- 位运算:
&|^~<<>>>>>。操作数是整数时是位运算;操作数是boolean时,&|^退化为逻辑运算(不短路)。
右移有 C/C++ 没有的明确行为:
>>算术右移,高位补符号位>>>无符号右移,高位补 0
C/C++ 对有符号数的右移是实现定义(不同编译器可能补符号位或补 0),Java 用 >> / >>> 明确区分,消除了这种不确定性。
- 括号与优先级:Java 的运算符优先级和 C 基本一致(
()最优先,* / %高于+ -,然后是移位、关系、逻辑、赋值)。拿不准就加括号,可读性永远第一,记优先级可维护性差。
3.6 字符串
- 子串:
substring(a, b)提取子串(左闭右开);C 没有直接等价物(只能指针 + 长度模拟) - 拼接:
+直接拼字符串,任何类型和字符串+都会自动转成字符串:
String s = "Hello " + 123; // "Hello 123"按分隔符拼一组字符串用 String.join:
String csv = String.join(", ", "a", "b", "c"); // "a, b, c""str".repeat(n)(Java 11+)重复字符串 n 次,方便方法,知道即可。
- 字符串不可变:
String一旦创建就不可改变,所有”修改”(拼接、replace、substring)都是创建新字符串对象,旧对象留给垃圾回收。好处是安全、可共享、天然线程安全;代价是每次修改都有开销——这就是第 9 条要用StringBuilder的原因。
手感:C++ 的
std::string可变,且有操作符重载(==比内容、[ ]访问),Java 没有操作符重载,一律走方法(.equals()、.charAt()),所以初看不如 C++ 直观。这是设计取舍:Java 用统一的方法调用换来了简单一致的模型。
- 检测字符串是否相等:内容相等用
equals,忽略大小写用equalsIgnoreCase。不能用==——==对引用比较的是”是不是同一个对象”,不是内容:
String a = "hello";String b = new String("hello");a.equals(b) // true,内容相等a == b // false,两个不同的对象
String c = "hello";a == c // true:字面量被"驻留"到常量池,指向同一对象最后这个例子是个陷阱:字符串 字面量(常量) 会被驻留(intern)到常量池,所以两个字面量 == 碰巧相等。但驻留只对字面量和编译期常量生效,运行时生成的新字符串不会自动共享:
String d = "he" + "llo"; // 编译期常量折叠 → 仍是字面量,和 a 同一个对象String x = "he";String y = x + "llo"; // 运行时拼接 → 新对象,和 a 不同a == d // truea == y // false所以任何依赖 == 判断内容的做法都是侥幸,内容判断一律用 equals,养成习惯。对比 C:C 用 strcmp 才比内容,Java 把”比内容”(equals)和”比身份”(==)拆成两个操作,反而更明确。
- 空串与 null 串:
""(空串,有对象、长度 0)和null(没有对象)是两回事:
String empty = "";String nothing = null;
empty.length() == 0 // truenothing.length() // 抛 NullPointerException!判空用 str.isEmpty() 或 str.length() == 0;判 null 用 str == null。两个都要防时,先判 null 再判空(&& 短路保证安全):
if (str != null && !str.isEmpty()) { // 既不是 null 也不是空串}null 可以类比 C++ 的 nullptr:表示引用没指向任何对象;它不是对象,所以对 null 调方法会抛 NullPointerException。
6. 码点与代码单元:length() 数的是 UTF-16 代码单元(char)个数,charAt(i) 取第 i 个 char;codePointCount(a, b) 数的是码点(真实字符)个数,codePointAt(i) 取第 i 个位置的码点:
String s = "A😀B";s.length() // 4:A + 代理对(占 2 个 char) + Bs.codePointCount(0, s.length()) // 3:A、😀、B 三个真实字符普通文本遍历用 for + charAt 遍历就够;要正确处理 emoji 这类增补字符才需要码点方法。codePoints() 会返回码点流,在 Stream API 的范畴再展开讲。
- String API:不用背。Java 标准库方法成千上万,没人全记得住。要做的是:
- 记住高频的几个:
lengthcharAtsubstringindexOfequalsequalsIgnoreCaseisEmptytoUpperCasereplace - 其余靠 IDEA 补全(敲
str.自动弹)+ 查文档,用多了自然熟
-
阅读联机 API 文档:Java 标准库的官方文档就是 Oracle 维护的 javadoc,后端开发也查它(docs.oracle.com 搜
java 17 api)。IDEA 里把鼠标悬停在类/方法上,或按Ctrl+Q(Mac 是Ctrl+J)直接看说明,不用每次开浏览器。 -
构建字符串:大量拼接别用
+(每次都会 new 一个新 String),改用StringBuilder:
StringBuilder builder = new StringBuilder();builder.append("Hello");builder.append(" ");builder.append("World");String result = builder.toString(); // "Hello World"顺带回答一个基础问题——类到底是什么? 类不是”容器”,是图纸(模板):它规定了对象长什么样(字段)和能干什么(方法)。StringBuilder 是一个类,new StringBuilder() 是按图纸造出一个对象,builder.append(...) 是调用这个对象的方法。一张图纸可以造出无数个对象,每个对象是独立的一份”实物”。这会在对象与类下一章细说。
- 文本块(Java 15+):用
"""包起来的多行字符串,写 HTML / JSON / SQL 不用再拼一堆\n:
String html = """ <html> <body>你好</body> </html> """;两个注意点:
- 内容里的单个
"不用转义,只有会影响到结束定界符"""时才需要\" - 行尾放一个
\表示忽略这个换行(\不进入字符串)
3.7 输入与输出
- 读取输入:从标准输入读数据,用
java.util.Scanner。注意System.in不是包,它是System类里的一个静态字段,代表标准输入流(对应 C 的stdin);Scanner负责把流里的字节解析成你要的类型:
import java.util.Scanner;//`java` 顶级包 → `util` 子包 → `Scanner` 类Scanner in = new Scanner(System.in);String name = in.nextLine(); // 读一整行int age = in.nextInt(); // 读一个整数double score = in.nextDouble(); // 读一个双精度浮点数对比 C:
scanf靠格式串 + 取地址符,类型不匹配很容易出问题;Scanner的方法是类型化的(nextInt()保证返回int),更安全但略啰嗦。还有next()(读一个单词)、hasNextInt()(先判断下一个是不是整数)等。
另外还有 java.io.Console(System.console() 获取),适合交互式控制台场景(如读密码不回显);但它在 IDE/重定向环境下可能拿不到,普通场景用 Scanner 就够。
- 格式化输出:和 C 的
printf几乎一模一样——Java 直接把格式串机制搬了过来:
System.out.printf("%d %f %s%n", age, score, name);对应 C 的 printf("%d %f %s\n", age, score, name);,占位符 %d %f %s 完全一致。完整格式串语法是:
%[参数索引$][标志][宽度][.精度]转换符| 部分 | 含义 |
|---|---|
% | 格式串起始符 |
参数索引$ | 指定用第几个参数(如 %1$d 用第 1 个参数) |
标志 | 对齐、符号、补零等控制 |
宽度 | 最小输出宽度,不足补空格或 0 |
.精度 | 浮点 = 小数位数;字符串 = 最大长度 |
转换符 | 决定输出格式与接受类型 |
全部标志:
| 标志 | 作用 | 示例 | 输出 |
|---|---|---|---|
- | 左对齐(默认右对齐) | printf("%-5d", 12) | 12 |
+ | 正数带 + 号 | printf("%+d", 12) | +12 |
0 | 数字前补 0 | printf("%05d", 12) | 00012 |
空格 | 正数前留一个空格 | printf("% d", 12) | 12 |
( | 负数用括号包起来 | printf("%(d", -12) | (12) |
, | 千位分隔符 | printf("%,d", 1234567) | 1,234,567 |
# | 备选形式(如 0x 前缀) | printf("%#x", 255) | 0xff |
$ | 参数索引结束符:%2$d 用第 2 个参数,可重排/复用 | printf("%2$s %1$s", "a", "b") | b a |
< | 复用上一个格式符的参数 | printf("%d %<x", 255) | 255 ff |
全部转换符:
| 转换符 | 含义 | 适用类型 | 示例(对 42) |
|---|---|---|---|
%d | 十进制整数 | byte / short / int / long / BigInteger | 42 |
%o | 八进制整数 | 同上 | 52 |
%x %X | 十六进制整数 | 同上 | 2a |
%f%F | 十进制浮点 | float / double / BigDecimal | 3.140000 |
%e %E | 科学计数法 | 同上 | 3.140000e+00 |
%g %G | 通用浮点(自动选 f 或 e) | 同上 | 3.14000 |
%a %A | 十六进制浮点数 | float / double / BigDecimal | 0x1.0p-1(对 0.5) |
%s %S | 字符串 | String / 任意对象 | hello |
%c%C | 字符 | char / byte / short / int | A |
%b %B | 布尔(%B 输出大写) | boolean | true / TRUE |
%h%H | 散列码 | 任意对象 | 5e9a2c |
%n | 平台换行 | — | 换行 |
%% | 字面量百分号 | — | % |
%tx %Tx | 日期时间(必须跟后缀,如 %tY 年份) | long / Long / Date / Calendar | 2026 |
%t 单独写不合法,它是日期时间的”前缀”,后面必须跟一个字母指定取哪部分:%tY 年、%tm 月、%td 日、%tH 时(24 小时制)、%tM 分、%tS 秒、%tF 完整日期 yyyy-MM-dd、%tT 完整时间 HH:mm:ss。
几个注意点:
%n输出平台原生行分隔符。C 的printf("\n")在 Windows 文本模式下会自动转成\r\n,但 Java 的\n永远是 LF、不做转换,想要平台分隔符就用%nString.format(...)是 Java 版的sprintf:格式化后返回字符串而不打印- 类型必须匹配:
%d配String直接抛异常——C 是未定义行为,Java 把错误拦在运行时 System.out.printf与System.out.format完全等价
- 文件输入与输出:套路和标准输入输出一模一样,只是把
System.in换成文件路径。
读文件用 Scanner,传一个 Path:
Scanner in = new Scanner(Path.of("data.txt"), StandardCharsets.UTF_8);while (in.hasNextLine()) { System.out.println(in.nextLine());}写文件用 PrintWriter:
PrintWriter out = new PrintWriter("output.txt", StandardCharsets.UTF_8);out.println("Hello 文件");out.close(); // 写完必须关,否则可能没落盘对比 C 的
FILE*:C 是fopen("file.txt", "r")+fscanf/fprintf,Java 是把文件包进Scanner/PrintWriter对象再读写——同样是面向对象的”构造对象、调方法”套路。
相对路径:相对的是 JVM 的工作目录(启动 java 命令时所在的目录),不是 .java / .class 文件所在的目录。在 IDEA 里默认是项目模块目录,容易和直觉不符——报 FileNotFoundException 时,先想想”当前工作目录到底在哪”。
两个注意点:
- 编码:建议显式传
StandardCharsets.UTF_8,否则用平台默认编码,跨机器容易乱码 - 关闭:用完要
close(),否则数据可能没写入磁盘、句柄泄漏(后面 ⑩ 文件 IO 会讲try-with-resources自动关闭)
3.8 控制流程
块作用域:和 C 一样,变量在声明它的 { } 块内有效。唯一区别在 3.4 提过:Java 不允许内层块遮蔽外层同名变量。
条件语句:if / else if / else 与 C 完全一致,唯一区别是条件必须是 boolean(if (1) 编译错误)。
循环:while、do-while、for 和 C 一模一样:
while (条件) { ... }do { ... } while (条件);for (int i = 0; i < n; i++) { ... }for 里声明的 int i 只在循环内有效,这点和 C99 之后一致。switch 已在 3.5 讲过,不重复。
中断控制流程:break 跳出当前循环/switch;continue 跳过本次循环剩余部分、进入下一轮。Java 还有 C 没有的带标签的 break/continue,专治多层嵌套循环:
outer:for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (j == 1) break outer; // 跳出整个外层循环,执行它后面的语句 }}带标签的 continue 则跳到外层循环的下一次迭代:
outer:for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { if (j == 1) continue outer; // 直接开始外层下一轮 i System.out.println(i + "," + j); }}// 输出:0,0 / 1,0 / 2,0break outer | continue outer | |
|---|---|---|
| 行为 | 退出整个外层循环(不再执行) | 跳到外层循环下一次迭代 |
| 类比 | 循环直接结束 | 外层跳过这一轮,继续 |
对比 C:C 想跳出多层循环只能靠 flag 标志位或 goto。Java 把
goto留作保留字但禁用(怕 goto 把结构搅成”意大利面条式代码”),用带标签的 break/continue 作为结构化替代——能跳出/跳过多层循环,但跳转方向被限制在循环的进出,不会乱跳。
带标签的 break 还能用在普通块上(不限于循环)——这是很实用的”提前退出嵌套逻辑”手法:
outer:{ if (条件A) { if (条件B) { break outer; // 满足条件,块里剩余代码全部跳过 } //这里是代码 } System.out.println("条件不满足时才执行到这里");}等价于 C 里用 goto 提前跳出,但只允许”从块内退出”,方向被限定,不会乱跳。
最后一条硬性规则:break / continue 只能跳到”包着它”的标签——标签标记的语句必须在词法上包含当前语句。站在块里面才能”跳出去”,站在外面就不能”跳出”这个块,编译器直接报错:
outer:{ break outer; // ✅ 合法:break 在块内部}break outer; // ❌ 编译错误:已经在块外面了这一节最容易踩的坑(都是相对 C 的新东西):
continue会跳过循环体里的更新语句——for安全(i++在循环头部,continue跳回时自动执行);while危险(更新写在循环体里,continue直接跳回条件判断,把更新跳过了 → 可能死循环)- 带标签的
continue不保存状态——跳到外层循环的下一轮,内层循环整体作废、从零开始,不会”接着上次” - 标签类型有限制——
continue的标签必须标在循环上;break的标签可以是循环/块/switch。标错当场编译报错 break outer跳到标签块之后,不是跳到标签那一行
3.9 大数
C 没有原生大数:long 上限也就 ±9.2×10¹⁸。Java 提供两个类处理超出范围的数:
BigInteger:任意精度整数BigDecimal:任意精度小数(适合金额,避免浮点误差)
不能直接用 + - * / 运算符,要调方法:
BigInteger a = BigInteger.valueOf(1234567890);BigInteger b = BigInteger.valueOf(9876543210L);BigInteger sum = a.add(b); // 加BigInteger diff = a.subtract(b); // 减BigInteger prod = a.multiply(b); // 乘BigInteger quo = a.divide(b); // 除构造要点:
- 小数值用
BigInteger.valueOf(long)转换 - 超大数用字符串构造:
new BigInteger("123456789012345678901234567890")——直接写数字字面量会因超出long范围编译报错
BigDecimal 常见于金额计算。0.1 + 0.2 用 double 算出来是 0.30000000000000004,用 BigDecimal 精确:
BigDecimal d = BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2));// 结果是精确的 0.3坑:用 BigDecimal.valueOf(0.1) 才是精确的 0.1;new BigDecimal(0.1) 会把 double 的二进制近似值原样带进去,反而”不精确”。
3.10 数组
声明与初始化:数组是对象,声明只是引用,必须 new 才有实体:
int[] a = new int[10]; // 10 个元素,默认 0int[] b = { 1, 2, 3, 4, 5 }; // 花括号直接初始化,不用 newint[] c = { 1, 2, 3, 4, 5, }; // 允许最后位置有逗号vs C:
int a[10]声明即分配,Java 必须new。
匿名数组与空数组:new int[] { 1, 2, 3 } 是匿名数组,不赋给变量,直接传给方法用:
int sum = sum(new int[] { 1, 2, 3 }); // 匿名数组做参数new int[0](或 new int[] {})是长度为 0 的数组——合法且有用:方法没结果时返回空数组而不是 null,调用方就不用判空了。
数组长度固定,但引用可以换:数组对象一旦创建长度就定死,但变量只是个引用,可以随时指向新数组——这就是”声明和构造分离”的灵活性:
int[] a; // 声明:只是引用,没分配内存a = new int[10]; // 构造a = new int[20]; // 换个更大的(旧数组交给垃圾回收)要”换大小还保留内容”用
Arrays.copyOf(a, 新长度)(自动拷贝并补零/截断)。对比 C:C 里int a[10]声明即定死,想换只能手动malloc+memcpy+free,还得自己管内存——Java 的”引用可换 + GC 回收”灵活得多。
访问与长度:下标从 0 开始;a.length 是属性(不是函数,C 得 sizeof(a)/sizeof(a[0]) 自己算)。越界抛 ArrayIndexOutOfBoundsException——C 越界是未定义行为,Java 直接拦下来。
for each 循环:遍历数组的简写:
for (int x : b) { System.out.println(x);}等价于 C 的下标遍历。注意拿到的是元素副本,改 x 不影响原数组;想改元素就用传统 for 加下标。
数组拷贝:= 赋值只是复制引用(两个变量指向同一个数组,改一个另一个也变);要独立副本用 Arrays.copyOf:
int[] same = b; // 只是多一个引用,同一数组int[] copy = Arrays.copyOf(b, b.length); // 深拷贝,新数组int[] first3 = Arrays.copyOf(b, 3); // 只拷前 3 个数组排序:Arrays.sort(a) 一行升序排序——C 要么手写排序要么 qsort。
命令行参数:main(String[] args) 对应 C 的 argc/argv。Java 不传 argc(用 args.length),且 args[0] 是第一个用户参数(C 的 argv[0] 是程序名):
public static void main(String[] args) { System.out.println(args.length); // 参数个数 System.out.println(args[0]); // 第一个参数}多维数组:本质是”数组的数组”。声明 int[][] grid = new int[3][4];,访问 grid[i][j]。
不规则数组(Java 的优势):每一行长度可以不同,C 的多维数组必须是矩形,Java 可以”锯齿”:
int[][] triangle = new int[5][];for (int i = 0; i < 5; i++) { triangle[i] = new int[i + 1]; // 第 i 行 i+1 个元素}C 想实现不规则数组得用”指针数组 + 手动管理”,Java 天然支持。
附录
日期
java.time 的 LocalDate(不可变日期类):
import java.time.LocalDate;
LocalDate today = LocalDate.now(); // 今天LocalDate bday = LocalDate.of(2000, 1, 1); // 指定日期
bday.plusDays(100); // 100 天后(返回新对象,原对象不变)bday.getDayOfWeek(); // 星期几bday.until(today); // 距离今天多少天和 String 一样,LocalDate 是不可变的:plusDays 返回新对象,不会改原来的。
对比 C:C 标准库没有好用的日期类型(time_t 是秒数,得自己转格式),Java 的 java.time 开箱即用。
小结:Java vs C 对照表
| 维度 | C | Java |
|---|---|---|
| 程序结构 | 自由函数 + main | 万物皆类,main 在类内 |
| 编译运行 | gcc → 可执行文件 → CPU 直接跑 | javac → 字节码 → JVM(解释 + JIT) |
| 内存 | 手动管理(malloc/free) | 自动垃圾回收 |
| 整型大小 | 随平台(int 可能是 2/4 字节) | 固定(int 永远 4 字节) |
| 无符号数 | 有 unsigned | 没有(唯一”无符号”的 char 不做算术) |
| 字符串 | char* / char[] | String 对象,不可变,equals 比内容 |
| 数组 | 下标遍历,sizeof 求长 | 对象,length 属性,for-each,越界抛异常 |
| 输入输出 | scanf / printf / FILE* | Scanner / System.out.printf / PrintWriter |
| 大数 | 无原生支持 | BigInteger / BigDecimal |
| 布尔 | 0 / 非 0 当真假 | 只有 true/false,if (1) 报错 |
| 内存安全 | 野指针、越界家常便饭 | 引用不能算数,越界即异常 |
| 等等 |
本章收获:C 给你完全控制权(和一堆踩坑机会),Java 帮你管好内存与边界,换来跨平台与安全。 学完这一章,可以用 Java 写出任何 C 能写的基础程序了。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!




