"白歌在代码审查时指着小崔的代码:'
if (employeeName != null & employeeName.equals("大翔"))—— 这里用&是不安全的,如果employeeName为 null,equals()会抛 NullPointerException。改成&&短路与。'小崔恍然大悟:'原来&&比&多一层空指针防护!'黄俪补充道:'还有||也是同理——日常开发几乎总是用短路版本。'"
逻辑运算符
概述
逻辑运算符用于组合多个 boolean 表达式,构成复杂的条件判断。Java SE 8 提供5种逻辑运算符:短路版本(&&、||)和非短路版本(&、|),以及逻辑非 !。短路版本在日常开发中占绝对主导地位,因为它们不仅能提升性能,更是空指针防御的核心工具。
操作符行为表
| 操作符 | 含义 | 示例(boolean x=true, y=false) | 结果 | 短路? |
|---|---|---|---|---|
&& | 逻辑与(短路) | x && y | false | 是 |
|| | 逻辑或(短路) | x || y | true | 是 |
! | 逻辑非 | !x | false | —— |
& | 逻辑与(不短路) | x & y | false | 否 |
| | 逻辑或(不短路) | x | y | true | 否 |
核心区分:
&&vs&:前者短路(左侧为false时不执行右侧),后者不短路(两侧都执行)。||vs|同理。日常开发几乎总是使用短路版本&&和||,除非你有特殊理由需要两侧都执行。
深度原理分析
一、短路求值(Short-Circuit Evaluation)机制
短路求值是Java编译器的一项重要优化,也是防御性编程的重要工具。其原理如下:
短路与 &&
表达式: left && right
执行流程:
1. 先求值 left
2. 如果 left == false → 整个表达式必为 false,跳过 right 的求值(短路)
3. 如果 left == true → 必须求值 right,表达式结果 = right
短路或 ||
表达式: left || right
执行流程:
1. 先求值 left
2. 如果 left == true → 整个表达式必为 true,跳过 right 的求值(短路)
3. 如果 left == false → 必须求值 right,表达式结果 = right
二、短路求值的工程价值——空指针防御
短路求值最常见的应用是空指针检查前置:
// ✅ 利用 && 短路:如果 name 为 null,右侧不执行,避免 NullPointerException
if (name != null && name.equals("大翔")) {
System.out.println("欢迎大翔登录飞翔科技系统");
}
// ❌ 如果写成 & (不短路),name == null 时仍会执行 name.equals(),抛 NullPointerException
if (name != null & name.equals("大翔")) { // 危险!
完整示例一:飞翔科技员工登录与权限验证
场景
白歌负责飞翔科技内部系统的登录模块,用短路逻辑运算符实现多重条件检查与部门权限判断。
/**
* 飞翔科技员工登录验证系统
* 演示短路逻辑运算符
*/
public class EmployeeLogin {
public static void main(String[] args) {
String inputUsername = "大翔"; // 用户输入的用户名
String inputPassword = "feixiang888"; // 用户输入的密码
String correctPassword = "feixiang888";// 正确密码
boolean isAccountLocked = false; // 账号是否被锁定
String department = "技术部"; // 部门
int loginAttempts = 2; // 已尝试登录次数
int maxAttempts = 5; // 最大允许尝试次数
System.out.println("===== 飞翔科技内部系统 feixiang.net =====");
// --- 短路与 &&:多重条件检查 ---
// 利用短路:如果账号被锁,后续检查直接跳过
if (!isAccountLocked
&& inputPassword.equals(correctPassword)
&& loginAttempts < maxAttempts) {
System.out.println("登录成功!欢迎 " + inputUsername);
} else {
System.out.println("登录失败!请检查账号状态或密码");
}
// --- 逻辑或 ||:部门权限判断 ---
// 技术部 或 架构组 成员可访问代码仓库
boolean canAccessRepo = department.equals("技术部")
|| department.equals("架构组")
|| department.equals("测试部");
System.out.println("\n===== 代码仓库权限 =====");
System.out.println("部门: " + department);
System.out.println("可访问Git仓库: " + canAccessRepo);
// --- 逻辑非 !:取反 ---
boolean isWeekend = false;
System.out.println("\n===== 加班判定 =====");
if (!isWeekend) { // 不是周末 → 工作日
System.out.println("工作日,正常考勤");
}
}
}
运行输出:
===== 飞翔科技内部系统 feixiang.net =====
登录成功!欢迎 大翔
===== 代码仓库权限 =====
部门: 技术部
可访问Git仓库: true
===== 加班判定 =====
工作日,正常考勤
完整示例二:短路 vs 不短路的本质差异
场景
小崔想验证 & 和 && 的区别,白歌让他写一段代码来观察右侧表达式是否被执行。
/**
* 短路(&&/||) vs 不短路(&/|) 的行为对比
*/
public class ShortCircuitDemo {
public static void main(String[] args) {
System.out.println("===== 短路与 && vs 不短路与 & =====\n");
// --- 测试1:左侧为 false ---
System.out.println("【测试1】左侧为 false 时:");
int x = 0;
// 短路与 &&:右侧不执行
System.out.print("false && (++x > 0) → ");
boolean r1 = false && (++x > 0);
System.out.println(r1 + ",x = " + x + "(x未变,证明右侧未执行)");
// 不短路与 &:右侧依然执行
System.out.print("false & (++x > 0) → ");
boolean r2 = false & (++x > 0);
System.out.println(r2 + ",x = " + x + "(x被++了,证明右侧执行了)");
// --- 测试2:左侧为 true ---
System.out.println("\n【测试2】左侧为 true 时:");
int y = 0;
// 短路或 ||:右侧不执行
System.out.print("true || (++y > 0) → ");
boolean r3 = true || (++y > 0);
System.out.println(r3 + ",y = " + y + "(y未变,证明右侧未执行)");
// 不短路或 |:右侧依然执行
System.out.print("true | (++y > 0) → ");
boolean r4 = true | (++y > 0);
System.out.println(r4 + ",y = " + y + "(y被++了,证明右侧执行了)");
// --- 实战意义:空指针防御 ---
System.out.println("\n===== 实战:空指针防御 =====");
String name = null;
int z = 0;
// && 短路:name为null时不执行 name.equals()
if (name != null && name.equals("大翔")) {
System.out.println("匹配到 大翔");
} else {
System.out.println("安全通过(&&短路避免了NullPointerException)");
}
// 去掉短路保护,尝试 & (注释掉以避免实际崩溃,仅供理解)
// if (name != null & name.equals("大翔")) // ← 这会抛 NullPointerException!
}
}
运行输出:
===== 短路与 && vs 不短路与 & =====
【测试1】左侧为 false 时:
false && (++x > 0) → false,x = 0(x未变,证明右侧未执行)
false & (++x > 0) → false,x = 1(x被++了,证明右侧执行了)
【测试2】左侧为 true 时:
true || (++y > 0) → true,y = 0(y未变,证明右侧未执行)
true | (++y > 0) → true,y = 1(y被++了,证明右侧执行了)
===== 实战:空指针防御 =====
安全通过(&&短路避免了NullPointerException)
易错场景
反例:&& 右侧有副作用且依赖其执行
// ⚠️ 危险:依赖 && 右侧的副作用
int count = 0;
if (true || (++count > 0)) { // || 短路,右侧不执行!
System.out.println(count); // 期望 1,实际 0
}
纠正:如果必须执行右侧,应将其提前到判断之前,或使用不短路版本:
// ✅ 方案一:提前执行
int count = 0;
count++; // 先执行副作用
if (true || (count > 0)) {
System.out.println(count); // 1
}
// ✅ 方案二:使用不短路版本(确认你确实需要右侧执行)
int count = 0;
if (true | (++count > 0)) { // & 不短路,右侧一定执行
System.out.println(count); // 1
}
面试考点
Q1:&& 和 & 有什么区别?什么时候用哪个?
&&是短路逻辑与:如果左侧为false,右侧不执行,直接返回false。&是不短路逻辑与:无论左侧是什么,右侧都会执行。使用建议:日常开发中99%的场景应使用
&&(短路版本),因为(1)性能更优(跳过不必要的计算);(2)可以做空指针防御(obj != null && obj.method())。仅当你必须让右侧表达式执行(例如右侧有方法调用产生必要的副作用)时才使用&。此外,&也可作位运算符(按位与),这是另一层含义。
Q2:! 运算符后面能跟什么?优先级如何?
!是一元逻辑非运算符,后面必须跟boolean表达式或boolean变量。它的优先级很高(一元运算符级别),高于关系运算符和逻辑运算符。因此!a > b是编译错误(!a的结果是boolean,不能与int b比较),正确写法是!(a > b)。
Q3:|| 和 | 的性能差异在什么场景下最明显?
当右侧表达式是一个开销较大的操作时(如数据库查询、网络调用、复杂计算),
||的短路特性可以避免不必要的执行,性能优势显著。例如if (cacheHit || expensiveDbLookup())——缓存命中时直接跳过数据库查询。此外,当右侧表达式可能抛出异常时(如空指针),||提供安全保障。唯一的代价是:如果你的代码依赖右侧的副作用(如计数器++),短路会破坏预期行为——但这是设计问题,不是||的问题。