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”
类型判断instanceofC 没有(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 scanfJava 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运算符、条件