一:基本运算符

  • +,+=
  • -,-=
  • *,*=
  • /,/=
  • %,%=
  • -(取反)

基本运算符非常简单,涉及到的无非是加,减,乘,除,取模运算。

值得一提的是取模运算是带符号运算,初学者很容易踩到这个坑。

示例:

  1. // 判断一个整数是不是奇数
  2. public static boolean isOdd(int num){
  3. return num % 2 == 1;
  4. }

初学者很有可能写出这样的代码,对于判断一个整数是否为基数,我们还要考虑输入为负数的情况

如果 num 为负数,譬如 -3 ,那么 -3 % 2 的结果为 -1 !

所以,判断一个整数是否为奇数的代码应该改为:

// 判断一个整数是不是奇数
public static boolean isOdd(int num){
  return num % 2 != 0;
}

/% 运算符比较经典的应用是求一个数字的数位和

什么叫一个数字的数位和?譬如,有数字 921 ,该数字的数位和为:9 + 2 + 1 = 12

获取一个数字的数位和代码:

int getSum(int num){
  int res = 0;
  while(num != 0){
    res += num % 10;
    num = num / 10;
  }
  return num;
}

自增自减运算符

  • i++
  • ++i
  • i--
  • --i

i++++i 有什么区别呢?答案是:赋值的顺序不同。

i++ 是先赋值后加 1++i 则是先加 1 再赋值

我们来看一个程序:

public class Main {
  public static void main(String[] args) {
    int a = 0;
    int b = 0;
       System.out.println(a++);
    System.out.println(++b);
  }
}

该程序输出的结果为:

0
1

原因就在于,System.out.println(a++); 会先将表达式的结果打印,然后再执行 a + 1 这个操作,所以打印输出的结果为 0System.out.println(++b); 则会先执行 b + 1 ,然后打印结果,所以打印的结果值为 1

来看一道比较经典题目:

public class Main {
    public static void main(String[] args) {
        int i = 0;
        i = i++ + ++i;
        int j = 0;
        j = ++j + j++ + j++ + j++;
        System.out.println(i);
        System.out.println(j);
    }
}

请说出该程序输出的结果?

该程序输出的结果为:

2
7

没有做对的小伙伴不如仔细思考下,一定要明确的是,+ 运算符两侧连接的是两个表达式,我们只需要搞清楚表达式的值这个概念,本题就可以迎刃而解。

比较运算符

  • >
  • <
  • ==
  • >=
  • <=
  • !=

比较运算符非常简单,使用比较运算符进行两个变量的比较操作,其结果返回的是一个布尔值。

初学者一定要明确的是 === ,前者是赋值操作,后者才是比较变量值是否相等。

二:逻辑运算符与短路特性及断路特性

  • &&
  • ||
  • !

逻辑与:&&

逻辑或:||

非:!

逻辑运算符两侧连接的是两个布尔变量,返回的结果仍是布尔值。

当两个值均为真时,逻辑与的结果才返回 true,否则返回 false;当两个值有一个为真,逻辑或的结果就返回 true,只有两个值均为假,逻辑或才返回 false

逻辑运算符的短路特性

我们先来看两个示例程序

程序一:


public class Main {
    public static boolean trueThing() {
        System.out.println("this is true!");
        return true;
    }

    public static boolean falseThing() {
        System.out.println("this is false!");
        return false;
    }

    public static void main(String[] args) {
        if (trueThing() || falseThing()) {

        }
    }
}

该程序输出的结果为:

this is true!

我们看到,在 if 判断中,只执行了 trueThing() 的代码,并没有执行 falseThing() 的代码,这是因为 || 运算具有 短路特性,当判断出第一条语句为真时,后面的部分就没有执行的意义了,因为结果必然返回 true

程序二:

public class Main {
    public static boolean trueThing() {
        System.out.println("this is true!");
        return true;
    }

    public static boolean falseThing() {
        System.out.println("this is false!");
        return false;
    }

    public static void main(String[] args) {
        if (falseThing() && trueThing()) {

        }
    }
}

该程序输出的结果为:

this is false!

我们看到,在 if 判断中,又是只执行了前半段,没有执行后半段,其原因在于,&& 运算具有断路特性,当判断出第一条语句为假时,后面的部分就没有执行的意义了,因为结果必然返回 false !

三:三元运算符

三原运算符是软件编程中的一个固定格式,语法为:

条件表达式 ? 表达式1:表达式2;

如果条件表达式为 true 那么则调用表达式 1,否则就调用表达式 2

示例程序:

res 进行赋值

如果满足 a > b ,则 res 复制为 1

如果满足 a < b,则 res 赋值为 -1

如果a == b,则 res 赋值为 0

上述需求可以用三元运算符的方式简洁地实现:

int res = a > b ? 1 : (a < b ? -1 : 0);

四:位运算详解与实战

位运算符

  • ~,按位取反
  • &,按位与
  • |,按位或
  • ^,异或
  • <<,左移
  • >>,带符号右移
  • >>>,无符号右移(总是补 0

用最有效率的方法计算2乘以8

最有效率的方法即位运算。

  • << 左移;左移一位相当于乘以2
  • >>带符号右移;正数右移高位补0,负数右移高位补1
    • 右移一位相当于除以2
    • 4 >> 1 结果为2
    • -4 >> 1结果为-2
  • >>>无符号右移;无论正数负数,高位均补0
    • 对于正数而言>>>>>无区别
    • 对于负数而言,举例:-2 >>> 1结果为:2147483647(Integer.MAX_VALUE)

所以,本题要求使用最有效率的方法来计算2 * 8

我们可以:

int res = 8 << 1

即可。

&&& 的区别

  1. 首先 &&& 都可以作为逻辑与的运算符;区别是&&具有断路特性,如果&&符号左边的表达式为false,则不再计算第二个表达式
  2. &可以用作位运算符,当&两边的表达式不是boolean类型时,&表示按位与的操作
    1 & 1 = 1
    1 & 0 = 0
    0 & 0 = 0

如示例程序:

public class Test {
    public static void main(String[] args) {
        // 0x7A : 0111 1010
        // 0x53 : 0101 0011
        // 0x7A & 0x53 = 0101 0010 = 0x52
        System.out.println(Integer.toHexString(0x7A & 0x53));
    }
}

结果输出:

52

我们在上面也提到了,& 也可以作为逻辑与,和 && 不同的是,& 是不具有断路特性的。

如示例程序:

public class Main {
    public static boolean trueThing() {
        System.out.println("this is true!");
        return true;
    }

    public static boolean falseThing() {
        System.out.println("this is false!");
        return false;
    }

    public static void main(String[] args) {
        if (falseThing() & trueThing()) {

        }
    }
}

该程序输出的结果为:

this is false!
this is true!

如我们所想,即使第一个表达式为假,使用 & 运算符还是会执行第二个表达式。

||| 也是一样的;| 也可以用作逻辑或运算,其也不具有 || 的短路特性。

使用最快的方式,交换整型数组中任意两个位置的数字

我们可以使用异或运算快速交换整型数组中任意两个位置的数字。

异或运算是一种基于二进制的位运算,对于运算符^两侧数的每一个二进制位,同值取0,异值取1,形象地说就是不进位加法。

异或运算满足以下的几种性质:

  • 交换律
  • 结合律
  • 对于任何数x,都满足x ^ x = 0;x ^ 0 = x

所以,通过异或运算可以快速交换数组中任意两个位置的数字,代码如下:

public class Main {
    public static void swap(int[] nums,int i,int j){
        if(i == j || nums[i] == nums[j]){
            return;
        }
        nums[i] = nums[i] ^ nums[j];
        nums[j] = nums[i] ^ nums[j];
        nums[i] = nums[i] ^ nums[j];
    }
}

一个数组中,只有一个数出现了奇数次,其他数则均出现偶数次,如何找到这个数

利用异或^的运算性质可以轻松求解

异或(XOR)运算性质:

  • 两个bit位数字相同,异或结果为0
  • 两个bit位数字不同,异或结果为1

也就是说:

  • 0 ^ 0 = 0
  • 0 ^ 1 = 1
  • 1 ^ 0 = 1
  • 1 ^ 1 = 0

并且,异或运算满足交换律和结合律

  • a ^ b = b ^ a
  • (a ^ b) ^ c = a ^ (b ^ c)

回到本问题:

已知数组中,只有一个数出现了奇数次,其余数均出现偶数次

思路:

将数组中所有数异或起来,偶数次的数字异或起来结果必为0

最后就是出现奇数次的那个数和0异或,结果为这个数字

参考代码:

public class XORDemo {
    public int findOddTimesNum(int[] nums){
        int xor = 0;
        for(int n : nums){
            xor ^= n;
        }
        return xor;
    }
}

位运算中的几个小技巧

n & (n - 1)

n & (n - 1)是位运算中非常重要的一种技巧,它是将二进制数n最右边的1变成0的操作

示例:n = 10101000

n = 10101000
n - 1 = 10100111
n & (n - 1) = 10100000 // 将二进制数n最右边的那个1变成了0,其余不变

n & (~n + 1)

n & (n - 1) 可以找到一个二进制数 n 最右边的那个 1 代表的数

来看示例:

num = 10101000

我们知道num最右边的那个1代表数为00001000

~num = 01010111
~num + 1 = 01011000
num & (~num + 1) = 00001000

这也是一个涉及位运算的算法题中常用的小技巧,它可以找到一个二进制数 n 最右边的那个 1 代表的数

五:运算符优先级与字符串加法

关于运算符的优先级,我们只需要明确两点即可:

  • 乘除高于加减
  • 其他全部假括号,括号的优先级最高!

字符串加法

  • 字符串拼接调用 toString 方法或者原生类型对应的表示
  • JDK 偷偷把字符串连接转换成 StringBuilder 调用

来看一个程序:

4、Java的运算系统 - 图1

我们在 StringBuilder 类的构造器上打一个断点

4、Java的运算系统 - 图2

使用 IDEAdebug 模式运行程序,我们会发现,程序最终会停留在断点处。

这是因为:

JDK 会偷偷把字符串连接转换成 StringBuilder 类的调用,进行字符串拼接,以节省内存空间。

为什么要这样做呢?

因为字符串本身是一个不可变类。

String类中使用final关键字字符数组保存字符串

private final char value[];

所以String对象是不可变的,这就导致了每次对String对象添加,更改或删除字符的操作都会产生一个新的String对象。不仅效率低下,而且浪费了空间。

所以,JDK 偷偷帮我们调用了 StringBuilder 类,在我们对 StringBuilder 类的对象进操作时,不会像 String 类那样频繁创建新的对象,而是都在同一块内存上进行字符串的增删改操作,减少了零碎的对象带来的内存压力,大大节省了空间。