Java从入门到放弃(七):整数运算

在Java中,整数运算严格遵守四则运算的基本规则,允许使用小括号来定义运算的优先级,从而确保运算顺序与初等数学中的规则一致。例如,我们可以很容易地进行加法、减法、乘法和除法等操作,并得到预期的结果。

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

Java中整数的数值表示是精确的,这意味着整数运算的结果也是精确的。特别需要注意的是,当进行整数除法时,结果只保留商的整数部分,小数部分会被丢弃。

int x = 12345 / 67; // 184
int y = 12345 % 67; // 12345÷67的余数是17

如果试图用零作为除数,Java会在运行时抛出ArithmeticException异常,这是因为在数学上,任何数除以零都是未定义的。

溢出

我们在使用整数运算时还需要警惕一个常见问题,那就是溢出。由于整数类型(如int)在Java中有其固定的范围,如果运算结果超出了这个范围,就会发生溢出。溢出并不会导致程序崩溃或抛出异常,但会得到一个错误的结果。

public class Main {
    public static void main(String[] args) {
        int x = 2147483640;
        int y = 15;
        int sum = x + y;
        System.out.println(sum); // -2147483641
    }
}

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

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

因此,在进行可能产生大数的运算时,我们应当格外小心,或者使用更大的整数类型(如long)来避免溢出。

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

Java还提供了几种简化运算的写法,如复合赋值运算符(+=-=*=/=)。这些运算符允许我们在一条语句中完成变量的更新和赋值操作,使代码更加简洁。

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

自增/自减

此外,Java还提供了自增(++)和自减(--)运算符,用于对整数变量进行加1或减1的操作。需要注意的是,自增和自减运算符的位置(前置或后置)会影响运算的结果。前置运算会先对变量进行加1或减1操作,然后再返回操作后的值;而后置运算则会先返回变量的原始值,然后再进行加1或减1操作。为了避免混淆和潜在的错误,建议在使用这些运算符时格外小心,并尽量避免在复杂的表达式中混用它们。

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

移位运算

在计算机内部,整数是以二进制的形式进行存储和计算的。以常见的int类型为例,一个整数如7在计算机中表示为四个字节的二进制数:

00000000 00000000 00000000 00000111

整数可以进行移位运算,这是一种对二进制位进行左移或右移的操作。当我们将整数7左移一位时,相当于将它的所有二进制位都向左移动一个位置,并在最右侧填充0,结果得到整数14:

int n = 7;       // 00000000 00000000 00000000 00000111 = 7  
int a = n << 1;  // 00000000 00000000 00000000 00001110 = 14

如果继续左移两位,则得到整数28:

int b = n << 2;  // 00000000 00000000 00000000 00011100 = 28

但是,当左移的位数较多时,例如左移28位或29位,最高位(符号位)可能会发生变化,导致整数的正负性发生改变:

int c = n << 28; // 01110000 00000000 00000000 00000000 = 1879048192  
int d = n << 29; // 11100000 00000000 00000000 00000000 = -536870912

在右移运算中,整数的二进制位会向右移动,左边的高位则根据整数的符号位(正数最高位为0,负数最高位为1)进行填充。对于正数,高位填充0;对于负数,高位保持为1,以保持其负数的特性。例如:

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

对负数进行右移时,高位保持为1:

int n = -536870912;  
int a = n >> 1;  // 11110000 00000000 00000000 00000000 = -268435456  
int b = n >> 2;  // 11111000 00000000 00000000 00000000 = -134217728

还有一种特殊的右移运算,即无符号右移(>>>),它在右移时不考虑符号位,总是用0来填充高位。因此,对负数进行无符号右移运算时,会得到一个正数:

int n = -536870912;  
int a = n >>> 1;  // 01110000 00000000 00000000 00000000 = 1879048192

位运算

位运算是一种在计算机科学中常用的操作,它涉及对整数的二进制表示进行逐位的逻辑运算。这些运算包括与(AND)、或(OR)、非(NOT)和异或(XOR)。下面是这些运算的基本规则:

与运算(AND):两个位都为1时,结果位才为1。例如:

n = 0 & 0; 结果为 0
n = 0 & 1; 结果为 0
n = 1 & 0; 结果为 0
n = 1 & 1; 结果为 1

或运算(OR):至少有一个位为1时,结果位就为1。例如:

n = 0 | 0; 结果为 0
n = 0 | 1; 结果为 1
n = 1 | 0; 结果为 1
n = 1 | 1; 结果为 1

非运算(NOT):将所有位的0和1互换。例如:

n = ~0; 结果为 1(因为0的非是1)
n = ~1; 结果为 0(因为1的非是0)

异或运算(XOR):两个位不相同时,结果位为1;相同则为0。例如:

n = 0 ^ 0; 结果为 0
n = 0 ^ 1; 结果为 1
n = 1 ^ 0; 结果为 1
n = 1 ^ 1; 结果为 0

在实际应用中,位运算可以用来处理二进制数据,例如在网络编程中,通过位运算可以快速判断一个IP地址是否属于特定的子网。例如,给定两个整数i和n,它们分别代表两个IP地址的二进制形式:

int i = 167776589; // 二进制表示为 00001010 00000000 00010001 01001101,对应IP地址10.0.17.77
int n = 167776512; // 二进制表示为 00001010 00000000 00010001 00000000,对应IP地址10.0.17.0

通过按位与运算(AND),我们可以检查IP地址10.0.17.77是否在10.0.17.0的子网内:

System.out.println(i & n); // 结果为 167776512,表示IP地址10.0.17.77确实在10.0.17.0的子网内

运算优先级

在Java编程语言中,表达式的计算顺序受到运算符优先级的影响。以下是Java中运算符的优先级列表,从最高优先级到最低优先级排列:

  1. 括号 ():用于明确表达式的计算顺序。
  2. 逻辑非 ! 和一元运算符 ~(按位取反)以及递增 ++ 和递减 --:这些是一元运算符,作用于单个操作数。
  3. 乘法 *、除法 / 和取模 %:这些是二元运算符,用于两个操作数的乘、除和取余操作。
  4. 加法 + 和减法 -:同样是二元运算符,用于两个操作数的加法和减法。
  5. 左移 <<、右移 >> 和无符号右移 >>>:这些是二元运算符,用于按位移动操作数的二进制表示。
  6. 按位与 &:二元运算符,用于按位与操作。
  7. 按位或 |:二元运算符,用于按位或操作。
  8. 赋值运算符 +=-=*=/=:这些是复合赋值运算符,用于将左侧变量的值与右侧表达式的结果进行相应的运算,并将结果赋值给左侧变量。

如果你对这些优先级不太熟悉,不必担心。在编写表达式时,可以通过添加括号 () 来明确指定你希望首先计算的部分,从而确保表达式的计算顺序符合你的预期。

类型自动提升与强制转型

在Java中进行算术运算时,如果操作数的类型不同,会发生类型提升(type promotion),以确保运算结果的类型能够容纳所有操作数的值。例如,当一个short类型的变量与一个int类型的变量进行运算时,short类型的变量会被提升为int类型,运算结果也是int类型。这是因为int类型比short类型更大,能够包含更广泛的整数值。

以下是一个示例,展示了类型提升的过程:

public class Main {
    public static void main(String[] args) {
        short s = 1234;
        int i = 123456;
        int x = s + i; // s被提升为int类型,结果x为int类型
        // short y = s + i; // 这将导致编译错误,因为结果类型是int,不能直接赋值给short类型的变量
    }
}

在某些情况下,你可能需要将一个较大范围的整数类型强制转换为一个较小范围的整数类型。这种转换称为类型转换(type casting)。例如,你可以将int类型的值转换为short类型:

int i = 12345;
short s = (short) i; // i被强制转换为short类型

然而,需要注意的是,强制转换可能会导致数据丢失,特别是当转换的值超出了目标类型的范围时。在这种情况下,超出部分的位会被丢弃,只保留低位的字节。这可能导致结果与预期不符。例如:

public class Main {
    public static void main(String[] args) {
        int i1 = 1234567;
        short s1 = (short) i1; // 结果为-10617,因为高位字节被丢弃,只保留了低位字节
        System.out.println(s1);

        int i2 = 12345678;
        short s2 = (short) i2; // 结果为24910,同样只保留了低位字节
        System.out.println(s2);
    }
}

在进行强制转换时,务必要意识到这种风险,确保转换后的值在目标类型的有效范围内,或者在转换前进行适当的处理。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧