07 运算符与表达式
前置知识:06 变量与数据类型
本章目标:掌握 Java 全部运算符,重点理解>>>无符号右移、短路求值、instanceof模式匹配、Math 类与 BigInteger。
概述
Java 的运算符 90% 与 C 相同,本章把笔墨集中在差异点:boolean 不能当整数用导致逻辑运算更严格、>>> 填补了 C 的空白、instanceof 走向模式匹配。另外介绍两个 C 程序员会嫉妒的东西:内置的 Math 工具类和 BigInteger 大数运算。
运算符全景与 C 的差异速览
| 类别 | 运算符 | 与 C 的差异 |
|---|---|---|
| 算术 | + - * / % | % 对浮点也可用(C99 起才有 fmod) |
| 关系 | == != > < >= <= | == 用于对象时比较引用(见下) |
| 逻辑 | && || ! | 操作数必须是 boolean,不能是 int |
| 位运算 | & | ^ ~ << >> >>> | 新增 >>> |
| 三元 | ?: | 相同 |
| 赋值 | = += -= *= /= %= &= |= ^= <<= >>= >>>= | 新增 >>>= |
| 自增自减 | ++ -- | 相同,前后缀语义一致 |
| 字符串拼接 | + | C 没有,"a" + 1 合法且结果是 “a1” |
| 类型判断 | instanceof | C 没有(RTTI 是 C++ 的概念) |
算术运算符
public class ArithmeticDemo {
public static void main(String[] args) {
// 除法:两个整数相除结果还是整数,直接截断
System.out.println(7 / 2); // 3
System.out.println(-7 / 2); // -3(向零取整,同 C99)
System.out.println(7.0 / 2); // 3.5(有 double 参与则提升)
// 取模:结果的符号跟随被除数
System.out.println(7 % 3); // 1
System.out.println(-7 % 3); // -1(不是 2!与 C 一致)
System.out.println(7 % -3); // 1
System.out.println(7.5 % 2); // 1.5 —— 浮点也能取模,C 需要调 fmod
// 字符串拼接是 + 的重载:任一侧是 String 就按拼接处理
String s = "得分: " + 95 + 5; // 注意!从左到右结合 -> "得分: 955"
String s2 = 95 + 5 + "分"; // 先算术后拼接 -> "100分"
System.out.println(s);
System.out.println(s2);
// 自增自减:前缀先加后用,后缀先用后加,语义与 C 完全相同
int i = 5;
System.out.println(i++); // 5,之后 i 为 6
System.out.println(++i); // 7
}
}关系与逻辑运算符:短路求值
public class ShortCircuitDemo {
static boolean check(String name, boolean result) {
System.out.println("执行了 check(" + name + ")");
return result;
}
public static void main(String[] args) {
// && 短路:左边为 false 时右边根本不执行
if (check("A", false) && check("B", true)) {
System.out.println("both true");
} else {
System.out.println("短路生效,B 未被检查");
}
// || 短路:左边为 true 时右边不执行
if (check("C", true) || check("D", false)) {
System.out.println("C 已足够");
}
// 经典应用——防御性判空顺序:
String s = null;
// s != null && s.length() > 0 // 安全:s 为 null 时不会调用 length()
// s.length() > 0 && s != null // 危险顺序,会 NPE
// & 和 | 不短路(两边都算),用于 boolean 时功能一样但无保护,
// 一般只在位运算场景使用它们
System.out.println(true & false); // false,但不短路
System.out.println(true ^ false); // true —— 异或可用于布尔翻转
}
}关键区别于 C:Java 中 if (x && y) 要求 x、y 都是 boolean。C 里 x && y 可以是任意整数,Java 编译器直接报错,杜绝了大量手误。
位运算与 >>> 无符号右移
public class BitDemo {
public static void main(String[] args) {
int a = 0b1100; // 12
int b = 0b1010; // 10
System.out.println(a & b); // 8 (1000)
System.out.println(a | b); // 14 (1110)
System.out.println(a ^ b); // 6 (0110)
System.out.println(~a); // -13 (按位取反)
// 左移 <<:低位补 0,等价乘 2^n
System.out.println(1 << 4); // 16
System.out.println(-1 << 30); // -1073741824
// 算术右移 >>:高位补符号位(负数补 1),等价除以 2^n 向下取整
System.out.println(16 >> 2); // 4
System.out.println(-16 >> 2); // -4(符号保持)
// ===== 重点:>>> 逻辑右移(无符号右移),Java 独有 =====
// 高位一律补 0,不管符号
int neg = -1; // ...1111 32 个 1
System.out.println(neg >> 28); // -1 (>> 补 1 还是负数)
System.out.println(neg >>> 28); // 15 (>>> 补 0 变正数)
// C 里想实现同样效果要写 (unsigned int)neg >> 28 这种强转;
// Java 没有 unsigned 类型,>>> 就是官方给出的替代方案。
// 典型用途:哈希函数中把符号位打散,如 HashMap 的 spread 函数
int h = 0xDEADBEEF;
int spread = h ^ (h >>> 16);
// 移位数会对 32 取模(long 则对 64 取模)——这点与 C 的未定义行为不同!
System.out.println(1 << 33); // 等价 1 << 1 = 2,明确定义
System.out.println(spread);
}
}| 表达式 | C 行为 | Java 行为 |
|---|---|---|
-1 >> 1 | 实现 defined(通常算术移位) | 定义为补符号位,得 -1 |
-1 >>> 1 | 语法错误(无此运算符) | 得 2147483647 |
1 << 33 | 未定义行为 | 明确定义等价 1 << 1 |
(int)x & 0xF 提取低位 | 常用 | 相同可用 |
instanceof 与模式匹配(Java 16+)
instanceof 判断对象的实际类型,传统写法需要强转两次:
public class InstanceOfDemo {
public static void main(String[] args) {
Object obj = "hello world";
// 传统写法:判断一次 + 强转一次,啰嗦
if (obj instanceof String) {
String s = (String) obj;
System.out.println("长度: " + s.length());
}
// 模式匹配写法(Java 16 正式):判断成功自动绑定变量
if (obj instanceof String s) { // s 只在条件成立时可用
System.out.println("长度: " + s.length());
}
// 结合否定时注意:s 在 else 分支不可用(可能未赋值)
// 但可以配合提前返回使用:
printLength(obj);
printLength(Integer.valueOf(42));
}
static void printLength(Object obj) {
if (!(obj instanceof String s)) {
System.out.println("不是字符串: " + obj);
return; // 卫语句风格提前退出
}
// 从这里开始编译器知道 s 一定有效
System.out.println("字符串长度: " + s.length());
}
}更强大的 switch 模式匹配在 08 条件语句 中详述。
三元运算符
public class TernaryDemo {
public static void main(String[] args) {
int score = 82;
// 条件 ? 真值 : 假值,用法与 C 相同
String level = score >= 60 ? "及格" : "不及格";
System.out.println(level);
// 可以嵌套但可读性差,两层以上建议改 if-else
String grade = score >= 90 ? "优"
: score >= 80 ? "良"
: score >= 60 ? "及格"
: "不及格";
System.out.println(grade);
// 一个 Java 特有的坑:两侧类型不一致时会触发自动提升
Object result = true ? 1 : 2.0; // 结果是 Double 1.0,不是 Integer!
System.out.println(result.getClass()); // class java.lang.Double
// 求绝对值的三种写法对比
int x = -7;
int abs1 = x < 0 ? -x : x;
int abs2 = Math.abs(x); // 推荐
System.out.println(abs1 + " " + abs2);
}
}运算符优先级表
从高到低(同一行内按结合方向计算)。不必死记,拿不准就加括号:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | () [] . 方法调用 | 左 |
| 2 | ! ~ ++ -- +(正) -(负) (强转) new | 右 |
| 3 | * / % | 左 |
| 4 | + - | 左 |
| 5 | << >> >>> | 左 |
| 6 | < <= > >= instanceof | 左 |
| 7 | == != | 左 |
| 8 | & | 左 |
| 9 | ^ | 左 |
| 10 | | | 左 |
| 11 | && | 左 |
| 12 | || | 左 |
| 13 | ?: | 右 |
| 14 | = += -= ... | 右 |
与 C 相比少了逗号运算符和取地址/解引用(&x/*p——Java 没有指针运算符)。
public class PriorityTrap {
public static void main(String[] args) {
// 经典陷阱 1:== 优先级高于 &,但位运算意图常被误读
int flags = 0b0101;
boolean b = (flags & 0b0001) != 0; // 必须加括号!
// 若写成 flags & 0b0001 != 0 会编译错误(int 与 boolean 做 &)
// 经典陷阱 2:移位优先级低于加减
System.out.println(1 << 2 + 3); // 是 1<<5 = 32,不是 4+3=7!
// 经典陷阱 3:三元优先级低于 +
int x = 5;
System.out.println("值=" + (x > 0 ? "正" : "负")); // 括号不能省
// 少括号会变成 "值=正"/"值=负" 拼接错误甚至编译错
System.out.println(b);
}
}Math 类常用方法
C 需要包含 <math.h> 并区分 abs/labs/fabs;Java 统一在 java.lang.Math,自动重载:
public class MathDemo {
public static void main(String[] args) {
// 常量
System.out.println(Math.PI);
System.out.println(Math.E);
// 绝对值 / 最值 —— 自动适配 int/long/float/double
System.out.println(Math.abs(-3.5)); // 3.5
System.out.println(Math.max(3, 7)); // 7
System.out.println(Math.min(3L, 7L)); // 3
// 取整三兄弟(注意与 C 的 round 差异)
System.out.println(Math.floor(3.7)); // 3.0 向下取整
System.out.println(Math.ceil(3.2)); // 4.0 向上取整
System.out.println(Math.round(3.5)); // 4 四舍五入(+0.5截断)
System.out.println(Math.round(-3.5)); // -3 !注意负数是"五入靠零"
// 幂与开方
System.out.println(Math.pow(2, 10)); // 1024.0
System.out.println(Math.sqrt(144)); // 12.0
System.out.println(Math.cbrt(27)); // 3.0 立方根(C 没有)
// 对数与三角
System.out.println(Math.log(Math.E)); // 1.0 自然对数
System.out.println(Math.log10(1000)); // 3.0
System.out.println(Math.sin(Math.PI / 6));
// 随机数:Math.random() 返回 [0.0, 1.0) 的 double
// 掷骰子 1~6:
int dice = (int) (Math.random() * 6) + 1;
System.out.println("掷出: " + dice);
// 精确算术(防溢出,溢出抛异常而不是回绕):
try {
Math.addExact(Integer.MAX_VALUE, 1);
} catch (ArithmeticException e) {
System.out.println("检测到溢出!");
}
// 还有 subtractExact/multiplyExact/floorDiv/floorMod
// floorMod(-7, 3) = 2 —— 数学上正确的取模,比 % 更适合循环数组
System.out.println(Math.floorMod(-7, 3));
}
}| 需求 | C 写法 | Java 写法 |
|---|---|---|
| 整数绝对值 | abs(x) (<stdlib.h>) | Math.abs(x) |
| 浮点四舍五入 | round 返回 long,负数行为不同 | Math.round |
| 随机整数 [0,n) | rand() % n(质量差) | (int)(Math.random()*n) 或 Random 类 |
| 防溢出加法 | 手写判断 | Math.addExact(a, b) |
Scanner 键盘输入
C 用 scanf 按格式串解析;Java 的 Scanner 是面向对象的流式读取器:
import java.util.Scanner;
public class ScannerDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入年龄: ");
int age = sc.nextInt(); // 类似 scanf("%d", &age),但无需取地址
System.out.print("请输入身高(米): ");
double height = sc.nextDouble();
sc.nextLine(); // 吃掉上一行残留的换行符(重要陷阱!)
System.out.print("请输入姓名: ");
String name = sc.nextLine(); // 读整行,可含空格
// sc.next() 则只读一个词(遇空格停止),类似 %s
System.out.printf("%s 今年 %d 岁,身高 %.2f 米%n", name, age, height);
// hasnextInt()/hasNextLine() 可先探测再读,避免输入耗尽异常
while (sc.hasNextInt()) {
int v = sc.nextInt();
System.out.println("读到: " + v);
}
sc.close(); // 用完关闭(Scanner 包装了 System.in)
}
}| 对比项 | C scanf | Java Scanner |
|---|---|---|
| 风格 | 格式化字符串 + 取地址 | 方法调用,类型安全 |
| 缓冲区溢出风险 | 有(%s 不限长) | 无,自动扩容 |
| 解析失败 | 返回值难检查,残留脏数据 | 抛 InputMismatchException,可捕获处理 |
| 读一行带空格 | fgets 配合 | nextLine() 直接支持 |
| 性能 | 快 | 较慢(竞赛大数据用 BufferedReader) |
BigInteger 大数运算
C 处理超出 long long 的整数要么手写高精度,要么引入 GMP 第三方库;Java 内置 BigInteger:
import java.math.BigInteger;
public class BigIntegerDemo {
public static void main(String[] args) {
// 创建:字符串构造(超过 long 的数只能这样给)
BigInteger a = new BigInteger("123456789012345678901234567890");
BigInteger b = BigInteger.valueOf(987654321L); // 小数值用 valueOf
// 运算不能用 +-*/,要用方法调用(运算符不可重载)
BigInteger sum = a.add(b);
BigInteger diff = a.subtract(b);
BigInteger prod = a.multiply(b);
BigInteger quo = a.divide(b);
BigInteger rem = a.remainder(b);
BigInteger pow = BigInteger.TEN.pow(20); // 10^20
System.out.println("和: " + sum);
System.out.println("积: " + prod);
System.out.println("商: " + quo);
System.out.println("余: " + rem);
System.out.println("10^20: " + pow);
// 比较:必须用 compareTo / equals,== 和 compareTo 要分清
System.out.println(a.compareTo(b) > 0); // true
System.out.println(BigInteger.TEN.equals(BigInteger.TEN)); // true
// 经典示例:计算阶乘 50!(约 65 位数字)
System.out.println("50! = " + factorial(50));
// 对应 C 需要 GMP: mpz_fac_ui(result, 50);
}
static BigInteger factorial(int n) {
BigInteger r = BigInteger.ONE; // 常量 ONE/TEN/ZERO
for (int i = 2; i <= n; i++) {
r = r.multiply(BigInteger.valueOf(i));
}
return r;
}
}注意:BigInteger 不可变,每次运算都产生新对象,性能远低于基本类型——能用 long 就别用 BigInteger。小数对应的是 BigDecimal(精确十进制,金融计算必备)。
综合示例
import java.util.Scanner;
/**
* 位运算综合演示:一个轻量的权限管理系统
* 每个 bit 代表一种权限,这是位运算最经典的应用场景
*/
public class PermissionSystem {
// 用位移定义权限位掩码
static final int READ = 1 << 0; // 0001
static final int WRITE = 1 << 1; // 0010
static final int EXECUTE = 1 << 2; // 0100
static final int ADMIN = 1 << 3; // 1000
public static void main(String[] args) {
int userPerms = READ | WRITE; // 授予读写权限 -> 0011
// 检查权限:与掩码做 &
System.out.println("可读: " + hasPerm(userPerms, READ));
System.out.println("可执行: " + hasPerm(userPerms, EXECUTE));
// 授予权限:|
userPerms |= EXECUTE;
System.out.println("授权后二进制: " + Integer.toBinaryString(userPerms));
// 撤销权限:& ~mask
userPerms &= ~WRITE;
System.out.println("撤销 WRITE 后可写吗: " + hasPerm(userPerms, WRITE));
// Scanner 交互:输入一个整数当作权限集合来解析
Scanner sc = new Scanner(System.in);
System.out.print("输入权限位数字(0-15): ");
if (sc.hasNextInt()) {
int input = sc.nextInt() & 0xF; // 保留低 4 位
StringBuilder sb = new StringBuilder(); // 拼接描述
if (hasPerm(input, READ)) sb.append("READ ");
if (hasPerm(input, WRITE)) sb.append("WRITE ");
if (hasPerm(input, EXECUTE)) sb.append("EXECUTE ");
if (hasPerm(input, ADMIN)) sb.append("ADMIN");
System.out.println("权限集: " + (sb.isEmpty() ? "(空)" : sb.toString().trim()));
}
sc.close();
}
static boolean hasPerm(int perms, int mask) {
return (perms & mask) == mask;
}
}本章要点回顾
- 逻辑运算符只接受 boolean 且
&&/||短路;&/|不短路 >>>是 Java 独有的无符号右移,弥补没有 unsigned 的缺口;移位数自动取模instanceof模式匹配(Java 16+)消除重复强转,配合卫语句很优雅- 优先级陷阱:
1 << 2 + 3是 32;不确定就加括号 - Math 类统一了 C 的 math.h 各函数并新增精确算术;Scanner 是类型安全的流式输入;BigInteger 让大数运算开箱即用
练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| P1004 | 方格取数 | https://www.luogu.com.cn/problem/P1004 | 运算符、条件 |