整数运算

Java 的整数运算遵循四则运算规则,可以使用任意嵌套的小括号。四则运算规则和初等数学一致。

  1. public class Main {
  2. public static void main(String[] args) {
  3. int i = (100 + 200) * (99 - 88); // 3300
  4. int n = 7 * (5 + (i - 9)); // 23072
  5. System.out.println(i);
  6. System.out.println(n);
  7. }
  8. }

整数的数值表示不但是精确的,而且整数运算永远是精确的,即使是除法也是精确的,因为两个整数相除只能得到结果的整数部分:

  1. int x = 12345 / 67; // 184

求余运算使用 %

  1. int y = 12345 % 67; // 12345÷67 的余数是 17

特别注意:整数的除法对于除数为 0 时运行时将报错,但编译不会报错。

溢出

要特别注意,整数由于存在范围限制,如果计算结果超出了范围,就会产生溢出,而溢出不会出错,却会得到一个奇怪的结果:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int x = 2147483640;
  4. int y = 15;
  5. int sum = x + y;
  6. System.out.println(sum); // -2147483641
  7. }
  8. }

要解释上述结果,我们把整数214748364015换成二进制做加法:

  1. 0111 1111 1111 1111 1111 1111 1111 1000
  2. + 0000 0000 0000 0000 0000 0000 0000 1111
  3. -----------------------------------------
  4. 1000 0000 0000 0000 0000 0000 0000 0111

由于最高位计算结果为1,因此,加法结果变成了一个负数。

要解决上面的问题,可以把 int 换成 long 类型,由于 long 可表示的整型范围更大,所以结果就不会溢出:

  1. long x = 2147483640;
  2. long y = 15;
  3. long sum = x + y;
  4. System.out.println(sum); // 2147483655

还有一种简写的运算符,即+=-=*=/=,它们的使用方法如下:

  1. n += 100; // 3409, 相当于 n = n + 100;
  2. n -= 100; // 3309, 相当于 n = n - 100;

自增和自减

Java 还提供了++运算和 -- 运算,它们可以对一个整数进行加 1 和减 1 的操作:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 3300;
  4. n++; // 3301, 相当于 n = n + 1;
  5. n--; // 3300, 相当于 n = n - 1;
  6. int y = 100 + (++n); // 不要这么写
  7. System.out.println(y);
  8. }
  9. }

注意 ++ 写在前面和后面计算结果是不同的:

  • ++n 表示先加 1 再引用 n
  • n++ 表示先引用 n 再加 1

不建议把 ++ 运算混入到常规运算中,容易自己把自己搞懵了。

移位运算

在计算机中,整数总是以二进制的形式表示。例如,int 类型的整数 7 使用 4 字节表示的二进制如下:

  1. 00000000 0000000 0000000 00000111

可以对整数进行移位运算。对整数 7 左移 1 位将得到整数 14,左移两位将得到整数 28

  1. int n = 7; // 00000000 00000000 00000000 00000111 = 7
  2. int a = n << 1; // 00000000 00000000 00000000 00001110 = 14
  3. int b = n << 2; // 00000000 00000000 00000000 00011100 = 28
  4. int c = n << 28; // 01110000 00000000 00000000 00000000 = 1879048192
  5. int d = n << 29; // 11100000 00000000 00000000 00000000 = -536870912

左移 29 位时,由于最高位变成 1,因此结果变成了负数。

类似的,对整数 28 进行右移,结果如下:

  1. int n = 7; // 00000000 00000000 00000000 00000111 = 7
  2. int a = n >> 1; // 00000000 00000000 00000000 00000011 = 3
  3. int b = n >> 2; // 00000000 00000000 00000000 00000001 = 1
  4. int c = n >> 3; // 00000000 00000000 00000000 00000000 = 0

如果对一个负数进行右移,最高位的 1 不动,结果仍然是一个负数:

  1. int n = -536870912;
  2. int a = n >> 1; // 11110000 00000000 00000000 00000000 = -268435456
  3. int b = n >> 2; // 10111000 00000000 00000000 00000000 = -134217728
  4. int c = n >> 28; // 11111111 11111111 11111111 11111110 = -2
  5. int d = n >> 29; // 11111111 11111111 11111111 11111111 = -1

还有一种不带符号的右移运算,使用 >>>,它的特点是符号位跟着动,因此,对一个负数进行 >>> 右移,它会变成正数,原因是最高位的 1 变成了 0

  1. int n = -536870912;
  2. int a = n >>> 1; // 01110000 00000000 00000000 00000000 = 1879048192
  3. int b = n >>> 2; // 00111000 00000000 00000000 00000000 = 939524096
  4. int c = n >>> 29; // 00000000 00000000 00000000 00000111 = 7
  5. int d = n >>> 31; // 00000000 00000000 00000000 00000001 = 1

byteshort 类型进行移位时,会首先转换为 int 再进行位移。

仔细观察可发现,左移实际上就是不断地 ×2,右移实际上就是不断地 ÷2。

位运算

与、或、非、异或

位运算是按位进行与、或、非和异或的运算。

与运算的规则是,必须两个数同时为 1,结果才为 1

  1. n = 0 & 0; // 0
  2. n = 0 & 1; // 0
  3. n = 1 & 0; // 0
  4. n = 1 & 1; // 1

或运算的规则是,只要任意一个为 1,结果就为 1

  1. n = 0 | 0; // 0
  2. n = 0 | 1; // 1
  3. n = 1 | 0; // 1
  4. n = 1 | 1; // 1

非运算的规则是,01 互换:

  1. n = ~0; // 1
  2. n = ~1; // 0

异或运算的规则是,如果两个数不同,结果为 1,否则为 0

  1. n = 0 ^ 0; // 0
  2. n = 0 ^ 1; // 1
  3. n = 1 ^ 0; // 1
  4. n = 1 ^ 1; // 0

对两个整数进行位运算,实际上就是按位对齐,然后依次对每一位进行运算。例如:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int i = 167776589; // 00001010 00000000 00010001 01001101
  4. int n = 167776512; // 00001010 00000000 00010001 00000000
  5. System.out.println(i & n); // 167776512
  6. }
  7. }

运算优先级

在 Java 的计算表达式中,运算优先级从高到低依次是:

  • ()
  • ! ~ ++ --
  • * / %
  • + -
  • << >> >>>
  • &
  • |
  • += -= *= /=

记不住也没关系,只需要加括号就可以保证运算的优先级正确。

类型自动提升与强制转型

在运算过程中,如果参与运算的两个数类型不一致,那么计算结果为较大类型的整型。例如,shortint 计算,结果总是 int,原因是 short 首先自动被转型为 int

  1. public class Main {
  2. public static void main(String[] args) {
  3. short s = 1234;
  4. int i = 123456;
  5. int x = s + i; // s自动转型为int
  6. short y = s + i; // 编译错误!
  7. }
  8. }

也可以将结果强制转型,即将大范围的整数转型为小范围的整数。强制转型使用 (type),例如,将 int 强制转型为 short

  1. int i = 12345;
  2. short s = (short) i; // 12345

要注意,超出范围的强制转型会得到错误的结果,原因是转型时,int 的两个高位字节直接被扔掉,仅保留了低位的两个字节:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int i1 = 1234567;
  4. short s1 = (short) i1; // -10617
  5. System.out.println(s1);
  6. int i2 = 12345678;
  7. short s2 = (short) i2; // 24910
  8. System.out.println(s2);
  9. }
  10. }

因此,强制转型的结果很可能是错的。

浮点数的运算

浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做位运算和移位运算。

在计算机中,浮点数虽然表示的范围大,但是,浮点数有个非常重要的特点,就是浮点数常常无法精确表示。

浮点数 0.1 在计算机中就无法精确表示,因为十进制的 0.1 换算成二进制是一个无限循环小数,很显然,无论使用 float 还是 double,都只能存储一个 0.1 的近似值。但是,0.5 这个浮点数又可以精确地表示。

因为浮点数常常无法精确表示,因此,浮点数运算会产生误差:

  1. public class Main {
  2. public static void main(String[] args) {
  3. double x = 1.0 / 10;
  4. double y = 1 - 9.0 / 10;
  5. // 观察x和y是否相等:
  6. System.out.println(x);
  7. System.out.println(y);
  8. }
  9. }

由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数:

  1. // 比较x和y是否相等,先计算其差的绝对值:
  2. double r = Math.abs(x - y);
  3. // 再判断绝对值是否足够小:
  4. if (r < 0.00001) {
  5. // 可以认为相等
  6. } else {
  7. // 不相等
  8. }

浮点数在内存的表示方法和整数比更加复杂。Java 的浮点数完全遵循 IEEE-754 标准,这也是绝大多数计算机平台都支持的浮点数标准表示方法。

类型提升

如果参与运算的两个数其中一个是整型,那么整型可以自动提升到浮点型:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = 5;
  4. double d = 1.2 + 24.0 / n;
  5. System.out.println(d); // 6.0
  6. }
  7. }

需要特别注意,在一个复杂的四则运算中,两个整数的运算不会出现自动提升的情况。例如:

  1. double d = 1.2 + 24 / 5; // 5.2

计算结果为 5.2,原因是编译器计算 24 / 5 这个子表达式时,按两个整数进行运算,结果仍为整数 4

溢出

整数运算在除数为 0 时会报错,而浮点数运算在除数为 0 时,不会报错,但会返回几个特殊值:

  • NaN 表示 Not a Number
  • Infinity 表示无穷大
  • -Infinity表示负无穷大

例如:

  1. double d1 = 0.0 / 0; // NaN
  2. double d2 = 1.0 / 0; // Infinity
  3. double d3 = -1.0 / 0; // -Infinity

这三种特殊值在实际运算中很少碰到,我们只需要了解即可。

强制转型

可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值。例如:

  1. int n1 = (int) 12.3; // 12
  2. int n2 = (int) 12.7; // 12
  3. int n2 = (int) -12.7; // -12
  4. int n3 = (int) (12.7 + 0.5); // 13
  5. int n4 = (int) 1.2e20; // 2147483647

如果要进行四舍五入,可以对浮点数加上 0.5 再强制转型:

  1. public class Main {
  2. public static void main(String[] args) {
  3. double d = 2.6;
  4. int n = (int) (d + 0.5);
  5. System.out.println(n);
  6. }
  7. }

布尔运算

对于布尔类型 boolean,永远只有 truefalse 两个值。

布尔运算是一种关系运算,包括以下几类:

  • 比较运算符:>>=<<===!=
  • 与运算 &&
  • 或运算 ||
  • 非运算 !

下面是一些示例:

  1. boolean isGreater = 5 > 3; // true
  2. int age = 12;
  3. boolean isZero = age == 0; // false
  4. boolean isNonZero = !isZero; // true
  5. boolean isAdult = age >= 18; // false
  6. boolean isTeenager = age >6 && age <18; // true

关系运算符的优先级从高到低依次是:

  • !
  • >>=<<=
  • ==!=
  • &&
  • ||

运算规则见下表:

a b a&b a&&b a|b a||b !a a^b
true true true true true true false false
true false false false true true false true
false true false false true true true true
false false false false false false true false

短路运算

布尔运算的一个重要特点是短路运算。如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果。

因为 false && x 的结果总是 false,无论 xtrue 还是 false,因此,与运算在确定第一个值为 false 后,不再继续计算,而是直接返回 false

我们观察以下代码:

  1. public class Main {
  2. public static void main(String[] args) {
  3. boolean b = 5 < 3;
  4. boolean result = b && (5 / 0 > 0);
  5. System.out.println(result);
  6. }
  7. }

如果没有短路运算,&& 后面的表达式会由于除数为 0 而报错,但实际上该语句并未报错,原因在于与运算是短路运算符,提前计算出了结果 false

如果变量 b 的值为 true,则表达式变为 true && (5 / 0 > 0)。因为无法进行短路运算,该表达式必定会由于除数为 0 而报错,可以自行测试。

类似的,对于 || 运算,只要能确定第一个值为 true,后续计算也不再进行,而是直接返回 true

  1. boolean result = true || (5 / 0 > 0); // true

三元运算符

Java 还提供一个三元运算符 b ? x : y,它根据第一个布尔表达式的结果,分别返回后续两个表达式之一的计算结果。示例:

  1. public class Main {
  2. public static void main(String[] args) {
  3. int n = -100;
  4. int x = n >= 0 ? n : -n;
  5. System.out.println(x);
  6. }
  7. }

上述语句的意思是,判断 n >= 0 是否成立,如果为 true,则返回 n,否则返回 -n。这实际上是一个求绝对值的表达式。

注意到三元运算 b ? x : y 会首先计算 b,如果 btrue,则只计算 x,否则,只计算 y。此外,xy的类型必须相同,因为返回值不是 boolean,而是 xy 之一。

字符和字符串

在 Java 中,字符和字符串是两个不同的类型。

字符类型

字符类型 char 是基本数据类型,它是 character 的缩写。一个 char 保存一个 Unicode 字符:

  1. char c1 = 'A';
  2. char c2 = '中';

因为 Java 在内存中总是使用 Unicode 表示字符,所以,一个英文字符和一个中文字符都用一个 char 类型表示,它们都占用两个字节。要显示一个字符的 Unicode 编码,只需将 char 类型直接赋值给 int 类型即可:

  1. int n1 = 'A'; // 字母“A”的Unicodde编码是65
  2. int n2 = '中'; // 汉字“中”的Unicode编码是20013

还可以直接用转义字符 \u+Unicode 编码来表示一个字符:

  1. // 注意是十六进制:
  2. char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65
  3. char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013

字符串类型

char 类型不同,字符串类型 String 是引用类型,我们用双引号 "..." 表示字符串。一个字符串可以存储 0 个到任意个字符:

  1. String s = ""; // 空字符串,包含0个字符
  2. String s1 = "A"; // 包含一个字符
  3. String s2 = "ABC"; // 包含3个字符
  4. String s3 = "中文 ABC"; // 包含6个字符,其中有一个空格

因为字符串使用双引号 "..." 表示开始和结束,那如果字符串本身恰好包含一个 " 字符怎么表示?例如,"abc"xyz",编译器就无法判断中间的引号究竟是字符串的一部分还是表示字符串结束。这个时候,我们需要借助转义字符 \

  1. String s = "abc\"xyz"; // 包含7个字符: a, b, c, ", x, y, z

因为 \ 是转义字符,所以,两个 \\ 表示一个 \ 字符:

  1. String s = "abc\\xyz"; // 包含7个字符: a, b, c, \, x, y, z

常见的转义字符包括:

  • \" 表示字符 "
  • \' 表示字符 '
  • \\ 表示字符 \
  • \n 表示换行符
  • \r 表示回车符
  • \t 表示Tab
  • \u#### 表示一个 Unicode 编码的字符

例如:

  1. String s = "ABC\n\u4e2d\u6587"; // 包含6个字符: A, B, C, 换行符, 中, 文

字符串连接

Java 的编译器对字符串做了特殊照顾,可以使用 + 连接任意字符串和其他数据类型,这样极大地方便了字符串的处理。例如:

  1. // 字符串连接
  2. public class Main {
  3. public static void main(String[] args) {
  4. String s1 = "Hello";
  5. String s2 = "world";
  6. String s = s1 + " " + s2 + "!";
  7. System.out.println(s);
  8. }
  9. }

如果用 + 连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接:

  1. // 字符串连接
  2. public class Main {
  3. public static void main(String[] args) {
  4. int age = 25;
  5. String s = "age is " + age;
  6. System.out.println(s);
  7. }
  8. }

多行字符串

如果我们要表示多行字符串,使用 + 号连接会非常不方便:

  1. String s = "first line \n"
  2. + "second line \n"
  3. + "end";

从 Java 13 开始,字符串可以用 """...""" 表示多行字符串(Text Blocks)了。举个例子:

  1. // 多行字符串
  2. public class Main {
  3. public static void main(String[] args) {
  4. String s = """
  5. SELECT * FROM
  6. users
  7. WHERE id > 100
  8. ORDER BY name DESC
  9. """;
  10. System.out.println(s);
  11. }
  12. }

上述多行字符串实际上是 5 行,在最后一个 DESC 后面还有一个 \n。如果我们不想在字符串末尾加一个 \n,就需要这么写:

  1. String s = """
  2. SELECT * FROM
  3. users
  4. WHERE id > 100
  5. ORDER BY name DESC""";

还需要注意到,多行字符串前面共同的空格会被去掉,即:

  1. String s = """
  2. ...........SELECT * FROM
  3. ........... users
  4. ...........WHERE id > 100
  5. ...........ORDER BY name DESC
  6. ...........""";

. 标注的空格都会被去掉。

如果多行字符串的排版不规则,那么,去掉的空格就会变成这样:

  1. String s = """
  2. ......... SELECT * FROM
  3. ......... users
  4. .........WHERE id > 100
  5. ......... ORDER BY name DESC
  6. ......... """;

即总是以最短的行首空格为基准。

最后,由于多行字符串是作为 Java 13 的预览特性(Preview Language Features)实现的,编译的时候,我们还需要给编译器加上参数:

  1. javac --source 13 --enable-preview Main.java

不可变特性

Java 的字符串除了是一个引用类型外,还有个重要特点,就是字符串不可变。考察以下代码:

  1. // 字符串不可变
  2. public class Main {
  3. public static void main(String[] args) {
  4. String s = "hello";
  5. System.out.println(s); // 显示 hello
  6. s = "world";
  7. System.out.println(s); // 显示 world
  8. }
  9. }

观察执行结果,难道字符串 s 变了吗?其实变的不是字符串,而是变量 s 的“指向”。

执行 String s = "hello"; 时,JVM 虚拟机先创建字符串 "hello",然后,把字符串变量 s 指向它:

  1. s
  2. ┌───┬───────────┬───┐
  3. "hello"
  4. └───┴───────────┴───┘

紧接着,执行 s = "world"; 时,JVM 虚拟机先创建字符串 "world",然后,把字符串变量 s 指向它:

  1. s ──────────────┐
  2. ┌───┬───────────┬───┬───────────┬───┐
  3. "hello" "world"
  4. └───┴───────────┴───┴───────────┴───┘

原来的字符串 "hello" 还在,只是我们无法通过变量 s 访问它而已。因此,字符串的不可变是指字符串内容不可变。

理解了引用类型的“指向”后,试解释下面的代码输出:

  1. // 字符串不可变
  2. public class Main {
  3. public static void main(String[] args) {
  4. String s = "hello";
  5. String t = s;
  6. s = "world";
  7. System.out.println(t); // t是"hello"还是"world"?
  8. }
  9. }

空值 null

引用类型的变量可以指向一个空值 null,它表示不存在,即该变量不指向任何对象。例如:

  1. String s1 = null; // s1是null
  2. String s2; // 没有赋初值值,s2也是null
  3. String s3 = s1; // s3也是null
  4. String s4 = ""; // s4指向空字符串,不是null

注意要区分空值 null和空字符串 "",空字符串是一个有效的字符串对象,它不等于 null