Java 基本语法
Java 基本语法:从注释到运算符
📖 本文是 Java 基础知识系列的第二篇,承接上一篇的核心概念,正式开始学习 Java 的语法细节。注释、标识符、关键字、运算符——这些是每一行 Java 代码的构成基础。掌握它们之后,你就可以真正开始写代码了。
一、注释
写好注释有助于自己和同事理解代码。注释是写给人看的,编译器在编译时会完全忽略它们——不会对生成的
.class文件产生任何影响。
1.1 单行注释
语法:以 // 开头,从 // 开始到本行结束的所有内容都是注释。
public void method() {
// age 用于表示年龄
int age = 18;
}阿里巴巴 Java 开发规约要求:单行注释必须写在被注释语句的上方另起一行,而不是写在行尾:
// ❌ 不推荐:注释写在行尾
int age = 18; // age 用于表示年龄
// ✅ 推荐:注释写在上方另起一行
// age 用于表示年龄
int age = 18;关于单行注释的内容,有一个重要的原则:注释应该解释"为什么这样做",而不是"做了什么"。代码本身已经清楚地表达了"做了什么",但代码无法解释背后的设计决策:
// ❌ 废话注释——代码已经说明了"做了什么"
// 将 price 乘以 0.8
double finalPrice = price * 0.8;
// ✅ 有价值的注释——解释了"为什么"
// 会员享受 8 折优惠
double finalPrice = price * 0.8;1.2 多行注释
语法:以 /* 开头,以 */ 结尾,中间的所有内容都是注释,可以跨多行。
/*
* age 用于表示年纪
* name 用于表示姓名
*/
int age = 18;
String name = "张三";多行注释有一个重要的限制:不能嵌套使用。
/* 外层注释
/* 内层注释 */ // ← 编译器认为注释到此处就结束了!
这部分代码会报错——因为它已经不在注释里了
*/编译器遇到第一个 */ 就认为注释结束了,后面的内容会被当作代码来解析。如果你需要临时注释掉一大段代码(包含多行注释的代码),建议使用 IDE 的快捷注释功能(Ctrl + /),或者使用单行注释逐行 //。
1.3 ⭐️ 文档注释(Javadoc)
文档注释是 Java 独有的一种注释形式,它不是写给程序员看的,而是 写给 javadoc 工具看的——可以自动生成专业的 HTML API 文档。
语法:以 /** 开头,以 */ 结尾。注意开头是两个星号,这是它和普通多行注释 /* */ 的关键区别。
文档注释可以用在三个地方:类、字段、方法。
/**
* 代码示例
*
* @author ;zy
* @date 2026/06/24
*/
public class Demo {
/** 姓名 */
private String name;
/** 年龄 */
private int age;
/**
* main 方法作为程序的入口
*
* @param args 命令行参数
*/
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}💡 IDEA 快捷键:在类、字段或方法的上方输入
/**然后按回车键,IDEA 会自动补全文档注释的格式(包括@param、@return等标签),*/也会自动生成。
常用 Javadoc 标签一览:
| 标签 | 用途 | 写在哪儿 |
|---|---|---|
@author | 作者标识 | 类 |
@version | 版本号 | 类 |
@since | 从哪个版本开始引入 | 类 / 方法 |
@see | 引用其他类或方法 | 类 / 方法 |
@param | 参数说明 | 方法 |
@return | 返回值说明 | 方法 |
@throws / @exception | 异常说明 | 方法 |
@deprecated | 标记已废弃,不推荐使用 | 类 / 方法 |
用 javadoc 命令生成 HTML 文档:
# 第一步:在类文件所在目录打开终端
# 第二步:执行 javadoc 命令
javadoc Demo.java -encoding utf-8
# 第三步:查看生成的文件
ls -l
# 会看到 index.html、Demo.html 以及一些 .js 和 .css 文件
# 第四步:用浏览器打开
open index.html # macOS
# 或者直接在文件管理器中双击 index.html生成的 HTML 页面和 Oracle 官方 Java API 文档是一样的风格。
1.4 ⭐️ 文档注释的注意事项
以下三条注意事项直接决定你生成的 javadoc 文档是否可用:
① 访问权限限制(最容易被忽略)
javadoc 命令只能为 public 和 protected 修饰的字段、方法和类生成文档。default(包访问权限)和 private 的注释会被 javadoc 静默忽略。
这不是 bug,而是有意设计:文档是给外部调用者看的,内部实现细节本来就不该暴露。如果你的类本身不是 public,javadoc 甚至会直接执行失败——连 index.html 都生成不了。
/**
* 这个注释会出现在 javadoc 中 —— 因为类是 public
*/
public class Person {
/** 这个注释也会出现 —— 字段是 public */
public String name;
/**
* 这个注释不会被 javadoc 生成 —— 方法是 private
* 因为调用者根本看不到这个方法,没必要生成文档
*/
private void internalHelper() {
// ...
}
}② HTML 标签使用规范
文档注释中可以嵌入 HTML 标记来排版,比如 <p> 分段、<a> 超链接、<ul> / <li> 列表等:
/**
* <p>这是一个用户服务类,提供用户的增删改查功能。</p>
*
* <ul>
* <li>支持分页查询</li>
* <li>支持按用户名模糊搜索</li>
* <li>密码使用 BCrypt 加密存储</li>
* </ul>
*
* 参考文档:<a href="https://example.com/api-guide">API 开发指南</a>
*/
public class UserService {
}但是不要使用标题标记(<h1> ~ <h6>)。因为 javadoc 工具会自己插入标题来组织页面结构,如果你写的 <h1> 和 javadoc 生成的标题重叠,页面排版就会乱掉。
③ 自定义 @ 注解
除了标准标签,你还可以在文档注释中定义并使用自己的 @ 注解。
1.5 注释规约与最佳实践
以下规约综合了阿里巴巴 Java 开发手册和行业通用实践:
✅ 类、字段、方法必须使用文档注释(/** */)而不是单行 // 或多行 /* */。因为 IDE 的鼠标悬浮提示可以直接显示文档注释的内容,大大提高开发效率——你在使用 String 类时,鼠标悬停在上面看到的说明文字,就是文档注释生成的。
✅ 所有抽象方法(包括接口方法)必须用 Javadoc 注释。除了说明返回值、参数和异常外,还必须清楚说明"这个方法做什么事、实现什么功能"——因为抽象方法没有方法体,调用者完全靠注释来理解它的行为。
✅ 所有类必须添加创建者和创建日期。可以在 IDEA 的 File and Code Templates 中预设模板,这样每次新建类时自动生成:
/**
* ${description}
*
* @author 你的名字
* @date ${DATE}
*/✅ 所有枚举类型字段必须有注释,说明每个数据项的含义和用途:
public enum OrderStatus {
/** 待支付 */
PENDING,
/** 已支付 */
PAID,
/** 已取消 */
CANCELLED,
/** 已退款 */
REFUNDED
}✅ 代码修改时,同步更新注释——过时的注释比没有注释更危险。一段写着"只支持正整数"的代码如果被改成了"也支持负数",后来的开发者会被误导。
❌ 注释的反模式:
// ❌ 废话注释
// 获取姓名
public String getName() { return name; } // 代码本身已经足够清晰
// ❌ 被注释掉的代码块(用 Git 管理历史,不要这么干)
// public void oldMethod() {
// // 几百行被注释的旧代码……
// }
// ❌ 与代码不一致的注释
// 返回用户年龄 —— 但这个方法的代码已经被改成了返回出生日期!
public int getAge() { return birthYear; }🎯 核心原则:注释是程序固有的一部分。好的注释能准确反映设计思想和业务含义,让继任者(包括几个月后的你自己)快速理解代码背后的信息。
二、标识符
标识符就是你给类、方法、变量、包起的名字。Java 对此既有"违法必究"的硬性规则,也有"不守规约会被同事鄙视"的软性规范。
2.1 命名规则(硬性要求——违反编译不通过,没有任何商量余地)
| 规则 | 说明 | 例子 |
|---|---|---|
| 组成字符 | 只能由字母、数字、下划线 _、美元符号 $ 组成 | myVar ✅ / my-var ❌ |
| 开头限制 | 不能以数字开头 | name1 ✅ / 1name ❌ |
| 关键字冲突 | 不能使用关键字和保留字 | class ❌ / myClass ✅ |
| 大小写敏感 | Name 和 name 是两个不同的标识符 | 两个完全独立的变量 |
| 长度不限 | 理论上没有长度限制 | 但别起 200 个字符的名字…… |
验证这些规则的简单示例:
// ✅ 合法标识符
int age;
String userName;
double $price; // $ 允许但应避免
int _count; // _ 单独使用从 Java 9 开始禁止
// ❌ 非法标识符
int 1stPlace; // 不能以数字开头
String my-name; // 连字符 - 不允许
int class; // class 是关键字
int my@email; // @ 不允许2.2 命名规范(软性建议——违反能编译,但 Java 社区会"另眼相看")
随着Java的演化,形成了统一的命名约定。违反这些约定虽然不会编译报错,但不合适。
| 元素 | 命名风格 | 示例 |
|---|---|---|
| 包名 | 全部小写,多级用 . 分隔 | com.example.myapp.util |
| 类名 / 接口名 | 大驼峰(PascalCase),名词或名词短语 | HelloWorld、ArrayList、UserService |
| 方法名 | 小驼峰(camelCase),动词或动词短语 | getName()、calculateTotal()、sendMessage() |
| 变量名 | 小驼峰,名词,见名知意 | userName、maxCount、isFinished |
| 常量名 | 全大写,下划线分隔 | MAX_VALUE、DEFAULT_PAGE_SIZE |
为什么 Java 选择驼峰命名而不是下划线?
驼峰命名的优势在于:每个单词的边界通过大写字母清晰区分(getUserName),比下划线(get_user_name)更紧凑,也更适合 Java 的面向对象风格。当你看到大驼峰 → 知道这是类/接口;看到小驼峰 → 知道这是方法/变量。
2.3 特殊标识符
Java 的标识符比大多数人想象的更宽松,但宽松不意味着应该滥用:
Unicode 字符可以做标识符(合法但强烈不建议):
// 😱 这竟然能编译通过!但永远不要这么写
String 姓名 = "张三";
int 年龄 = 18;
System.out.println(姓名 + "今年" + 年龄 + "岁");
// 理由:源代码可能在不同编码环境间传输,Unicode 标识符极易导致乱码问题$ 符号:Java 编译器在生成内部类时会使用 $ 作为分隔符(比如 Outer$Inner.class)。你手写的代码中应避免使用 $,以免和编译器生成的类名冲突。
_ 下划线:从 Java 9 开始,单独的 _ 变成了关键字,不能再用作标识符。这是为了给未来的语言特性(如模式匹配中的占位符)预留空间:
int _ = 10; // Java 8 可以编译,Java 9+ 报错三、关键字
关键字是 Java 语言保留的、有特殊含义的单词。你可以把它们理解为 Java 的"保留词汇"——它们有自己的本职工作,不能兼职做你的变量名。
3.1 关键字完整分类表
Java 共有约 50 个关键字(不同版本略有增减),按功能分为以下几类:
① 访问控制(3 个)
| 关键字 | 含义 |
|---|---|
private | 仅本类可见 |
protected | 本类 + 同包 + 子类可见 |
public | 所有类可见 |
② 类/方法/变量修饰符
| 关键字 | 含义 |
|---|---|
abstract | 抽象类 / 抽象方法 |
class | 声明一个类 |
extends | 继承父类 |
final | 不可变(类不可继承、方法不可重写、变量不可修改) |
implements | 实现接口 |
interface | 声明一个接口 |
new | 创建对象实例 |
static | 静态(属于类而非实例) |
native | 本地方法(JNI 调用 C/C++ 代码) |
strictfp | 严格浮点运算(Java 17 起已废弃) |
synchronized | 同步锁 |
transient | 序列化时忽略该字段 |
volatile | 保证多线程间的可见性 |
③ 程序控制(12 个)
if、else、switch、case、default、for、while、do、break、continue、return、instanceof
④ 错误处理(6 个)
try、catch、finally、throw、throws、assert
⑤ 包相关(2 个)
package(声明包路径)、import(导入其他包)
⑥ 基本数据类型(8 个)
byte、short、int、long、float、double、char、boolean
⑦ 其他
void(方法无返回值)、enum(枚举)、super(父类引用)、this(本对象引用)
3.2 两个被"架空了"的关键字——const 和 goto
const 和 goto 是 Java 关键字列表中两个特殊的存在:它们被保留了,但在 Java 中不起任何实际作用。
const:Java 用final代替了 C/C++ 的const功能goto:Java 保留了它但刻意不支持,因为goto是"结构化编程"的天敌(会导致意大利面条式的代码)。Java 使用break label和continue label来处理少数真正需要跳转的场景
虽然没有 goto,但 Java 提供了带标签的 break 和 continue 来应对少数真正需要跨层跳转的场景——这是结构化编程对 goto 的"合法替代":
// ============================================
// Java 标签跳转:goto 的结构化替代方案
// 标签声明在循环或代码块前,命名遵循标识符规则,后跟冒号
// ============================================
// 示例 1:break + 标签 —— 跳出多重嵌套循环
// 这是 goto 最常见的合法用途:在深层循环中直接退出到指定层级
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (i == 2 && j == 2) {
break outer; // 直接跳出 outer 标签所在的外层循环
}
System.out.printf("i=%d, j=%d%n", i, j);
}
}
// 输出:i=1,j=1 i=1,j=2 i=1,j=3 i=2,j=1
// 当 i=2, j=2 时,break outer 终止整个 outer 循环,内层循环也随之结束
// 示例 2:continue + 标签 —— 跳到外层循环的下一次迭代
// 比普通 continue 更强大:可以跨越多层直接控制外层循环的迭代
outer:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) {
continue outer; // 跳过内层循环的剩余迭代,外层 i++ 后继续
}
System.out.printf("i=%d, j=%d%n", i, j);
}
}
// 输出:i=1,j=1 i=2,j=1 i=3,j=1
// j 永远不会等于 3:每次 j=2 时直接跳回外层 i++,内层被重置
// 示例 3:标签 + 代码块 —— 模拟 C 语言中 goto 跳出任意代码块
// 注意:标签只能用于循环(for/while/do-while)或普通代码块,不能跨方法
initialize:
{
int port = 8080;
System.out.println("正在检查端口 " + port + " 的可用性...");
if (port < 1024) {
System.out.println("端口冲突,跳过初始化");
break initialize; // 直接跳出整个初始化代码块,类似 goto end_of_init
}
System.out.println("端口可用,继续初始化流程");
// 更多初始化逻辑...
}
System.out.println("初始化阶段结束");
// 输出:正在检查端口 8080 的可用性... 端口可用,继续初始化流程 初始化阶段结束
// 若 port < 1024,则输出"端口冲突"后直接跳到最后的 println
// ===== 关键区别 =====
// break label —— 终止标签所在循环/代码块,控制流跳到该块之后
// continue label —— 跳过当前迭代的剩余部分,直接进入标签所在循环的下一次迭代
// 与 goto 的本质不同:标签跳转严格遵循块作用域,禁止跨方法或跳入循环体内部3.3 三个特殊的"字面量"——true、false、null
严格来说,true、false、null 不是关键字,而是字面量(Literals):
boolean flag = true; // true 是 boolean 类型的字面量
String name = null; // null 是引用类型的空值字面量但它们和关键字一样不能用作标识符:
int true = 1; // ❌ 编译不通过
int null = 0; // ❌ 编译不通过四、运算符
运算符是执行计算、比较和逻辑判断的符号。这一节是整个 Java 基础语法中面试出镜率最高的知识点——尤其是自增自减和移位运算。
4.1 算术运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
+ | 加法 | 3 + 2 → 5 |
- | 减法 | 3 - 2 → 1 |
* | 乘法 | 3 * 2 → 6 |
/ | 除法 | 5 / 2 → 2(整型)/ 5.0 / 2 → 2.5(浮点) |
% | 取模(取余数) | 5 % 2 → 1 |
整数除法 vs 浮点数除法——这是新手最容易踩的坑:
int a = 5 / 2; // 结果是 2,不是 2.5!
// 两个整数相除,Java 会执行整数除法,直接截断小数部分
double b = 5.0 / 2; // 结果是 2.5
double c = 5 / 2.0; // 结果也是 2.5
// 只要有一个操作数是浮点数,就会执行浮点数除法% 取模——结果符号跟随被除数:
System.out.println(5 % 3); // 2
System.out.println(5 % -3); // 2 (被除数是正数,结果为正)
System.out.println(-5 % 3); // -2 (被除数是负数,结果为负)
System.out.println(-5 % -3); // -2+ 的双重身份——加法 vs 字符串拼接:
// 两边都是数字 → 加法
System.out.println(1 + 2); // 3
// 有一边是字符串 → 拼接
System.out.println("Hello " + "World"); // Hello World
System.out.println("age: " + 18); // age: 18
// 混合运算要小心执行顺序(从左到右)
System.out.println(1 + 2 + "abc"); // "3abc" 先做加法,再拼接
System.out.println("abc" + 1 + 2); // "abc12" 先拼接,再拼接4.2 关系运算符
关系运算符用来比较两个值的大小或相等关系,结果永远是 boolean 类型。
| 运算符 | 含义 | 示例 |
|---|---|---|
== | 等于 | 5 == 3 → false |
!= | 不等于 | 5 != 3 → true |
> | 大于 | 5 > 3 → true |
< | 小于 | 5 < 3 → false |
>= | 大于等于 | 5 >= 5 → true |
<= | 小于等于 | 3 <= 5 → true |
⚠️ 陷阱预告:
==在比较基本类型时比较的是值,在比较引用类型时比较的是内存地址(是不是同一个对象)。具体区别下一篇"基本数据类型"中会详细展开。
4.3 ⭐️ 逻辑运算符与短路求值
| 运算符 | 含义 |
|---|---|
&& | 逻辑与(AND),两边都为 true 结果才为 true |
|| | 逻辑或(OR),任意一边为 true 结果就为 true |
! | 逻辑非(NOT),取反 |
短路求值(Short-Circuit Evaluation) 是面试中的高频考点:
&&如果左边是false,右边根本不会执行(因为不管右边是什么,结果都是 false)||如果左边是true,右边根本不会执行(因为不管右边是什么,结果都是 true)
// 短路求值的实际效果
int a = 10;
// && 短路:左边为 false,右边不执行
boolean r1 = (a < 5) && (a++ > 0);
System.out.println(r1); // false
System.out.println(a); // 10 —— a++ 根本没有执行!
// || 短路:左边为 true,右边不执行
boolean r2 = (a > 5) || (a++ > 0);
System.out.println(r2); // true
System.out.println(a); // 10 —— a++ 还是没有执行!这个特性在实际开发中非常有用:
// ✅ 利用短路避免空指针异常
if (user != null && user.getName().equals("张三")) {
// 如果 user 为 null,&& 后面的代码不会执行,避免了 NullPointerException
}
// ✅ 利用短路做条件执行
if (cache == null || !cache.hasKey(key)) {
// 只在缓存不存在或无数据时加载
loadData();
}&& vs &、|| vs | 的区别:
int a = 10, b = 10;
// && 短路
boolean r1 = (a < 5) && (a++ > 0); // a++ 没执行,a 还是 10
// & 非短路(两边都会执行)
boolean r2 = (b < 5) & (b++ > 0); // b++ 执行了,b 变成了 11& 和 | 也可以做逻辑运算,但它们不短路,两边表达式都会执行。此外 & 和 | 还可以做位运算(见 4.8 节),而 && 和 || 只能做逻辑运算。
4.4 赋值运算符
| 运算符 | 等价于 |
|---|---|
= | 直接赋值 |
+= | a = a + b |
-= | a = a - b |
*= | a = a * b |
/= | a = a / b |
%= | a = a % b |
复合赋值运算符有一个隐蔽的特性:自动帮你做类型转换:
byte b = 10;
// ❌ 编译不通过
b = b + 1; // b + 1 的结果是 int 类型,无法赋值给 byte
// ✅ 编译通过
b += 1; // += 内部自动做了强制类型转换:b = (byte)(b + 1)这不是 bug,而是 JLS(Java 语言规范)明确规定 += 等价于 (type)((a) + (b))。
4.5 三元运算符
语法:条件 ? 表达式1 : 表达式2
如果条件为 true,返回表达式 1 的值;如果条件为 false,返回表达式 2 的值。
int score = 85;
String result = score >= 60 ? "及格" : "不及格";
System.out.println(result); // 及格什么时候用三元,什么时候用 if-else?
| 场景 | 推荐 |
|---|---|
| 简单的二选一赋值 | ?: 更简洁 |
| 需要执行多行语句 | if-else 更清晰 |
| 嵌套条件 | 别用三元嵌套——那是给自己找麻烦 |
// ❌ 别这样写——三元嵌套是代码可读性的灾难
String level = score >= 90 ? "优秀" : score >= 80 ? "良好" : score >= 60 ? "及格" : "不及格";
// ✅ 这种情况用 if-else 更清晰
String level;
if (score >= 90) {
level = "优秀";
} else if (score >= 80) {
level = "良好";
} else if (score >= 60) {
level = "及格";
} else {
level = "不及格";
}4.6 ⭐️ 自增自减运算符(面试高频)
这是 Java 面试中最常考的运算符陷阱,没有之一。
基本规则:
| 运算符 | 含义 |
|---|---|
++i | 先加后用——i 先自增 1,再参与外层表达式的计算 |
i++ | 先用后加——先取 i 的当前值参与外层表达式的计算,然后 i 再自增 1 |
--i | 先减后用 |
i-- | 先用后减 |
简单场景:
int i = 1;
int a = ++i; // i 先变成 2,再赋值给 a
System.out.println("i=" + i + ", a=" + a); // i=2, a=2
int j = 1;
int b = j++; // 先把 j 的值 1 赋给 b,j 再变成 2
System.out.println("j=" + j + ", b=" + b); // j=2, b=1深入理解——用 javap -c 反编译字节码验证:
源文件 HelloWorld.java:
public class HelloWorld {
public static void main(String[] args) {
int i = 1;
i = i++;
}
}用 javap -c HelloWorld.class 反编译,main 方法的字节码如下:
Code:
0: iconst_1 // 将 int 常量 1 压入操作数栈
1: istore_1 // 弹出栈顶值,存入局部变量表槽位 1(即 i = 1)
2: iload_1 // 将局部变量表槽位 1 的值(i = 1)压入操作数栈
3: iinc 1, 1 // 将局部变量表槽位 1 的值自增 1(此时 i 变为 2)
6: istore_1 // 弹出操作数栈顶的值(栈顶仍然是 1),存入局部变量表槽位 1
7: return // 方法返回逐条推演(操作数栈 + 局部变量表的变化):
| 字节码 | 指令含义 | 操作数栈 | 局部变量表 |
|---|---|---|---|
iconst_1 | 将常量 1 压栈 | [1] | i = 未初始化 |
istore_1 | 栈顶弹出 → 存入槽位 1 | [] | i = 1 |
iload_1 | 将槽位 1 的值压栈 | [1] | i = 1 |
iinc 1, 1 | 槽位 1 自增 1(只操作局部变量表,不碰栈) | [1] | i = 2 |
istore_1 | 栈顶弹出 → 存入槽位 1(用栈顶的 1 覆盖了 i) | [] | i = 1 |
return | 方法返回 | — | i = 1 |
三步看懂整个过程:
iload_1 → 栈 = [1], i = 1 "把 i 的值抄一份压入栈"
iinc → 栈 = [1], i = 2 "局部变量 i 自增,但栈里的副本不受影响"
istore_1 → 栈 = [], i = 1 "把栈顶的值 (1) 写回 i,i 的自增被覆盖了!"关键发现:iinc 指令直接操作局部变量表,不会修改操作数栈中已经压入的值。所以 istore_1 写入的仍然是 iload_1 时压入的那个 1——iinc 对局部变量表所做的自增(i = 2)被无情覆盖,最终 i 仍然是 1。
💡 为什么局部变量表从槽位 1 开始?
因为
main是静态方法,槽位 0 被String[] args参数占用。如果是实例方法(非static),槽位 0 则是this引用。
经典面试题变体对比:
// 题一:i = i++
int i = 1;
i = i++;
System.out.println(i); // 输出:1
// 原因见上方字节码推演
// 题二:i = ++i
int j = 1;
j = ++j;
System.out.println(j); // 输出:2
// ++j 的字节码顺序不同:iinc 先执行(j 变 2),iload 后执行(压入 2),istore 写入 2🎯 记忆诀窍:
i++是"先抄一份旧的备用,再自增,最后用备用的覆盖";++i是"先自增,再抄新的,最后用新的写入"。面试时画出操作数栈的推演过程,就永远不会算错。
4.7 ⭐️ 移位运算符(面试高频)
移位运算符直接操纵二进制位,是底层优化和高性能编程的基础。
| 运算符 | 含义 | 效果 |
|---|---|---|
<< | 左移 | 低位补 0,每左移 1 位相当于乘以 2 |
>> | 算术右移 | 高位用符号位填充(正数补 0,负数补 1) |
>>> | 逻辑右移 | 高位一律补 0(不关心符号) |
图解三种移位:
假设数字 -8(32 位表示):
二进制(补码):11111111 11111111 11111111 11111000
>> 1 位(算术右移):11111111 11111111 11111111 11111100 → -4
高位补符号位 1,保持负数
>>> 1 位(逻辑右移):01111111 11111111 11111111 11111100 → 2147483644
高位一律补 0,负数变巨大正数!代码验证:
int n = -8;
System.out.println(n >> 1); // -4 —— 算术右移:除以 2,保留符号
System.out.println(n >>> 1); // 2147483644 —— 逻辑右移:符号位也补 0<< 为什么相当于乘以 2 的 n 次方?
二进制中,左移一位就是在末尾加一个 0,这和十进制末尾加一个 0 相当于乘以 10 是同一个道理:
二进制:0010 (2) → 左移 1 位 → 0100 (4) = 2 × 2¹
二进制:0010 (2) → 左移 2 位 → 1000 (8) = 2 × 2²
二进制:0010 (2) → 左移 3 位 → 0001 0000 (16) = 2 × 2³实际应用场景:
// 1. HashMap 的哈希扰动函数
// 将高 16 位和低 16 位做异或,增加哈希值的随机性
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 2. 位图标记——用 int 的 32 个位记录 32 种状态
int flags = 0;
flags |= (1 << 3); // 设置第 3 位为 1
flags |= (1 << 7); // 设置第 7 位为 1
boolean hasFlag3 = (flags & (1 << 3)) != 0; // 检查第 3 位是否为 1
// 3. 高效计算——移位比乘除快得多
int half = n >> 1; // 等价于 n / 2,但更快
int doubleVal = n << 1; // 等价于 n * 2,但更快4.8 位运算符
| 运算符 | 含义 | 运算规则 |
|---|---|---|
& | 按位与 | 同为 1 才得 1 |
| | 按位或 | 只要有一个 1 就得 1 |
^ | 按位异或 | 不同为 1,相同为 0 |
~ | 按位取反 | 0 变 1,1 变 0 |
真值表:
int a = 0b1100; // 二进制 1100 = 十进制 12
int b = 0b1010; // 二进制 1010 = 十进制 10
System.out.println(Integer.toBinaryString(a & b)); // 1000 (8) —— 两位都是 1
System.out.println(Integer.toBinaryString(a | b)); // 1110 (14) —— 至少一位是 1
System.out.println(Integer.toBinaryString(a ^ b)); // 0110 (6) —— 两位不同
System.out.println(Integer.toBinaryString(~a)); // 1111...0011 (-13) —— 全部取反典型应用场景:
// 1. 奇偶判断——比 n % 2 更快
boolean isOdd = (n & 1) == 1;
// 2. 权限位掩码
int READ = 1 << 0; // 0001
int WRITE = 1 << 1; // 0010
int EXECUTE = 1 << 2; // 0100
int permission = READ | WRITE; // 0011 —— 拥有读写权限
boolean canRead = (permission & READ) != 0; // true
boolean canExecute = (permission & EXECUTE) != 0; // false
// 3. 交换两个整数(不借助第三个变量)
int x = 5, y = 10;
x = x ^ y;
y = x ^ y; // y 变成了原来的 x
x = x ^ y; // x 变成了原来的 y
System.out.println("x=" + x + ", y=" + y); // x=10, y=54.9 运算符优先级
当一行代码中出现多个运算符时,Java 按照优先级从高到低依次计算。
优先级速查表(从高到低):
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | () [] . | 左 → 右 |
| 2 | ! ~ ++ -- +(正号)-(负号) | 右 → 左 |
| 3 | * / % | 左 → 右 |
| 4 | + - | 左 → 右 |
| 5 | << >> >>> | 左 → 右 |
| 6 | < <= > >= instanceof | 左 → 右 |
| 7 | == != | 左 → 右 |
| 8 | & | 左 → 右 |
| 9 | ^ | 左 → 右 |
| 10 | | | 左 → 右 |
| 11 | && | 左 → 右 |
| 12 | || | 左 → 右 |
| 13 | ?: | 右 → 左 |
| 14(最低) | = += -= *= /= %= <<= >>= | 右 → 左 |
口诀记忆:"单目 > 算术 > 移位 > 关系 > 位 > 逻辑 > 三目 > 赋值"
最佳实践是:不确定优先级就加括号。括号不仅消除歧义,更重要的——它让看代码的人不需要在脑中模拟优先级。
// ❌ 让人头疼——谁会记得 & 和 && 哪个优先级高?
boolean result = a > 0 && b < 10 & c != 0;
// ✅ 加了括号一目了然
boolean result = (a > 0) && ((b < 10) & (c != 0));4.10 instanceof 运算符
instanceof 用来判断一个对象是否是某个类(或接口)的实例:
Object obj = "Hello, Java!";
System.out.println(obj instanceof String); // true
System.out.println(obj instanceof Integer); // false
System.out.println(obj instanceof Object); // true —— 所有类都是 Object 的子类Java 16 的 Pattern Matching 简化写法:
Java 16 之前,instanceof 判断后需要手动强转:
// Java 16 之前——啰嗦
if (obj instanceof String) {
String s = (String) obj; // 手动强转
System.out.println(s.length());
}
// Java 16+ —— 简洁
if (obj instanceof String s) { // 一步完成判断 + 赋值 + 类型转换
System.out.println(s.length());
}这种写法特别适合需要根据不同类型做不同处理的场景,减少了冗余代码。
五、分隔符
分隔符是 Java 代码的"骨架",它们定义了代码块、方法调用、数组访问等结构。没有它们,Java 代码就是一盘散沙。
| 分隔符 | 名称 | 用途 |
|---|---|---|
{ } | 花括号 | 定义代码块 / 类体 / 方法体 / 作用域 |
( ) | 圆括号 | 方法参数列表 / 表达式分组 / 类型转换 / if/for/while 条件 |
[ ] | 方括号 | 数组声明和元素访问 |
; | 分号 | 语句结束符——Java 中每条语句都必须以分号结尾 |
, | 逗号 | 分隔变量声明、方法参数、数组元素 |
. | 点号 | 访问类的成员(System.out)或包的层级(java.util) |
:: | 双冒号 | 方法引用(Java 8+),如 System.out::println |
// 分隔符使用的完整示例
public class SeparatorDemo { // { 开始类体
public static void main(String[] args) { // ( 参数列表 ),{ 开始方法体
int[] numbers = {1, 2, 3}; // [] 数组声明,{ 数组初始化,, 分隔元素
System.out.println(numbers[0]); // . 访问成员,() 方法调用,[] 数组访问,; 语句结束
} // } 结束方法体
} // } 结束类体六、总结
| 知识点 | 核心要点 |
|---|---|
| 注释 | // 单行 / /* */ 多行 / /** */ 文档(Javadoc),文档注释要注意 javadoc 的访问权限限制 |
| 标识符命名 | 规则(硬性要求,违反编译不过)vs 规范(软性建议,驼峰命名是 Java 的身份证) |
| 关键字 | 约 50 个保留词,const 和 goto 是历史遗物,true/false/null 是字面量 |
| 自增自减 | ++i 先加后用,i++ 先用后加——用操作数栈模型理解 i = i++ 的结果 |
| 短路求值 | && 和 ` |
| 移位运算 | << 左移(乘以 2)/ >> 算术右移(补符号位)/ >>> 逻辑右移(补 0) |
| 位运算 | & ` |
| 优先级 | 不确定就加括号——代码是写给人看的 |
掌握本文的内容后,你就拥有了阅读和编写 Java 代码的"识字能力"。下一篇我们将进入 基本数据类型——Java 中的 8 种基本类型、包装类型、自动装箱拆箱,以及那些会让你在面试中被问到的缓存机制。
参考
- Java Language Specification - Chapter 3: Lexical Structure — Java 词法结构官方规范
- Java Language Specification - Chapter 15: Expressions — Java 表达式官方规范
- javabetter.cn - 文档注释详解 — 二哥的注释教程
- JavaGuide — 常看的Java知识分享社群