Java基础知识:运算符操作

  • 前言
  • 优先级
  • 赋值
  • 算术运算符
  • 一元加减运算符
  • 递增和递减
  • 关系运算符
  • 逻辑运算符
  • 短路
  • 位运算符
  • 与(&)
  • 或(|)
  • 异或(^)
  • 反(~)
  • 左移(<<)
  • 右移(>>)
  • 无符号右移(>>>)
  • 三元运算符
  • 字符串运算符
  • 类型转换
  • 截断和舍入
  • 装箱和拆箱
  • 类型提升
  • Java没有sizeof


前言

运算符指接受一个或多个参数并生成新值(操作数的运算方式)。这个参数与普通方法调用的形式不同,但效果是相同的。加法 +、减法 -、乘法 *、除法 / 以及赋值 = 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一些运算符可改变运算对象的值,这叫作“副作用”(Side Effect)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。

优先级

运算符的优先级决定了存在多个运算符时一个表达式各部分的运算顺序。Java 对运算顺序作出了特别的规定。其中,最简单的规则就是先乘除后加减。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定运算顺序。代码示例:

public static void main(String[] args) {
        int x = 1, y = 2, z = 3;
        int a = x + y - 2/2 + z; // [1]
        int b = x + (y - 2)/(2 + z); // [2]
        System.out.println("a = " + a);
        System.out.println("b = " + b);
        /* Output:
        a = 5
        b = 1
		*/
    }

在 System.out.println() 语句中使用了 + 运算符。 但是在这里 + 代表的意思是字符串连接符。编译器会将 + 连接的非字符串尝试转换为字符串。

赋值

赋值的符号是 = ,它代表着获取 = 右边的值并赋给左边的变量。右边可以是任何常量、变量或者可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量(A = 4),但不可将任何东西赋给一个常数(比如不能 4 = A)。

在万物皆对象的知识扩展中提到过:基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。当 a = b时,修改a的值是不会影响b的。

public class Subject {
    public Integer id;
    public String name;
}

public class Test {
    public static void main(String[] args) {
        Subject subject = new Subject();
        subject.id = 1;
        subject.name = "hello";
        System.out.println("subject.id="+subject.id +" subject.name="+subject.name);
        Subject subject1 = new Subject();
        subject1.id = 2;
        subject1.name = "world";
        System.out.println("subject1.id="+subject1.id +" subject1.name="+subject1.name);
        subject = subject1;
        System.out.println("subject.id="+subject.id +" subject.name="+subject.name);
        /* Output:
        subject.id=1 subject.name=hello
		subject1.id=2 subject1.name=world
		subject.id=2 subject.name=world
		*/

    }
}

在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时丢失,它引用的对象会在垃圾回收时被清理)。

这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。但是假若你不想出现这里的别名引起混淆的话,你可以这么做。代码示例:

subject.name = subject1.name;
        System.out.println("subject.id="+subject.id +" subject.name="+subject.name);
        /* Output:
        subject.id=1 subject.name=world
		*/

较之前的做法,这样做保留了两个单独的对象,而不是丢弃一个并将 t1 和 t2 绑定到同一个对象。但是这样的操作有点违背 Java 的设计原则。对象的赋值是个需要重视的环节,否则你可能收获意外的“惊喜”。

算术运算符

Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 +、减号 -、除号 /、乘号 * 以及取模 %(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。

public static void main(String[] args) {
        int a = 2;
        int b = 12;
        System.out.println("a + b="+ (b + a));//Output: a + b=14
        System.out.println("a - b="+ (b - a));//Output: a - b=10
        System.out.println("a * b="+ (b * a));//Output: a * b=24
        System.out.println("a / b="+ (b / a));//Output: a / b=6
        System.out.println("a % b="+ (b % a));//Output: a % b=0
        System.out.println("a +="+ (b+=a));//Output: a +=14
        System.out.println("a -="+ (b-=a));//Output: a -=12
        System.out.println("a *="+ (b*=a));//Output: a *=24
        System.out.println("a /="+ (b/=a));//Output: a /=12
    }

Java 也用一种与 C++ 相同的简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。 可用 x += 4 来表示:将 x 的值加上4的结果再赋值给 x。

一元加减运算符

一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为 int 类型。

public static void main(String[] args) {
        int a = 2;
        int b = 12;
        int c= a * -b;
        System.out.println(c);//Output: -24
    }

递增和递减

和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 ++ 和递减 --,意为“增加或减少一个单位”。举个例子来说,假设 a 是一个 int 类型的值,则表达式 ++a 就等价于 a = a + 1。
每种类型的运算符,都有两个版本可供选用:“前递增”和“前递减”表示 运算符位于变量或表达式的前面,对于前递增和前递减(如 ++a 或 --a),会先执行递增/减运算,再返回值。而“后递增”和“后递减”表示运算符位于变量的后面,对于后递增和后递减(如 a++ 或 a–),会先返回值,再执行递增/减运算

public static void main(String[] args) {
        int a = 1;
        System.out.println("a="+a);//Output: a=1
        System.out.println("++a="+ ++a);//Output: ++a=2
        System.out.println("a++="+ a++);//Output: a++=2
        System.out.println("a="+a);//Output: a=3
        System.out.println("--a="+ --a);//Output: --a=2
        System.out.println("a--="+ a--);//Output: a--=2
        System.out.println("a="+a);//Output: a=1
    }

关系运算符

关系运算符会通过产生一个布尔boolean)结果来表示操作数之间的关系。如果关系为,则结果为 true,如果关系为,则结果为 false。关系运算符包括小于 <,大于 >,小于或等于 <=,大于或等于 >=,等于 == 和不等于 !=== 和 != 可用于所有基本类型,但其他运算符不能用于基本类型 boolean,因为布尔值只能表示 true 或 false,所以比较它们之间的“大于”或“小于”没有意义。

public static void main(String[] args) {
        Integer a = 99;
        Integer b = 99;
        System.out.println(a == b);
        System.out.println(a != b);
        /* Output: 
		true
		false
		*/
    }

如果你把 47 改成 128,那么打印的结果就是这样,因为 Integer 内部维护着一个 IntegerCache 的缓存,默认缓存范围是 [-128, 127],所以 [-128, 127] 之间的值用 == 和 != 比较也能能到正确的结果,但是不推荐用关系运算符比较,具体见 JDK 中的 Integer 类源码。

那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的 equals() 方法,下面是如何使用 equals() 方法的示例:

public static void main(String[] args) {
        Integer a = 99;
        Integer b = 99;
        System.out.println(a.equals(b));//Output: true
    }

equals() 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写 equals() 方法,否则我们将获取不到想要的结果。在万物皆对象中就有介绍 == 和 equals()的区别。

逻辑运算符

每个逻辑运算符 && (AND)、||(OR)和 !(非)根据参数的逻辑关系生成布尔值 true 或 false。下面的代码示例使用了关系运算符和逻辑运算符:

public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.println((a > 0) && (b >2));//Output: false
        System.out.println((a > 0) || (b >2));//Output: true
        System.out.println(!a.equals(b));//Output: true
    }

&& (AND)表示当两个条件都满足时才返回true, ||(OR)表示只要两个条件中一个满足即为true, !(非)表示当两个参数不匹配时即为true。请注意,如果在预期为 String 类型的位置使用 boolean 类型的值,则结果会自动转为适当的文本格式(即 “true” 或 “false” 字符串)。

短路

逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:

private static boolean test(int i){
        System.out.println("i"+i);
        return i>2;
    }
    private static boolean test1(int i){
        System.out.println("i"+i);
        return i>3;
    }
    private static boolean test2(int i){
        System.out.println("i"+i);
        return i>4;
    }
    public static void main(String[] args) {
        System.out.println(test(3) && test(2) && test(5));
        /*Output: 
		i3
		i2
		false
		*/
    }

可能你的预期是程序会执行 3 个 test 方法并返回。但第二个方法的返回结果是 false。这就代表这整个表达式的结果肯定为 false,所以就没有必要再判断剩下的表达式部分了。

位运算符

位运算符允许我们操作一个整型数字中的单个二进制位。包括按位取按位与(&)、按位或(|)异或(^)反(~)右移(>>)左移(<<)和无符号右移(>>>)。位运算符只能对整数型和字符型数据进行对应的位执行布尔代数,从而产生结果。

与(&)

如果两个二进制位都是1,结果才为1,否则结果为0。

public static void main(String[] args) {
        int a = 3;
        String s = Integer.toBinaryString(a);
        System.out.println(s);//Output: 11  即 四进制0011
        int b = 5;
        String s1 = Integer.toBinaryString(b);
        System.out.println(s1);//Output: 101  即 四进制0101
        System.out.println(a&b);//Output: 1 即 四进制 0001
    }

或(|)

参与运算的2个二进制位只要有其中1个是1 ,那么就是1,如果2个二进制位都是0则表示0

public static void main(String[] args) {
        int a = 3;
        String s = Integer.toBinaryString(a);
        System.out.println(s);//Output: 11  即 四进制0011
        int b = 5;
        String s1 = Integer.toBinaryString(b);
        System.out.println(s1);//Output: 101  即 四进制0101
        System.out.println(a|b);//Output: 1 即 四进制 0111
    }

异或(^)

参与运算的2个二进制进行异或运算,如果2个二进制都是0或者都是1,那么就是0,如果俩个二进制不同,则为1。

public static void main(String[] args) {
        int a = 3;
        String s = Integer.toBinaryString(a);
        System.out.println(s);//Output: 11  即 四进制	0011
        int b = 5;
        String s1 = Integer.toBinaryString(b);
        System.out.println(s1);//Output: 101  即 四进制 	0101
        System.out.println(a^b);//Output:6=110 即 四进制 	0110
    }

反(~)

万能公式:~x = -(x+1);这样可以计算出取反的结果。

取反的运算方式有两种:正数取反负数取反

正数取反:将初始数值转换成二进制数原码,即将0变为1、将1变为0,所有计算二进制的输入输出都是补码的形式。

public static void main(String[] args) {
        int a = 4;//0100 取反 1011 补码 1010 反码 1101=-5
        System.out.println(~a);//Output: -5
    }

最高位为1,表示是一个负数,因此符号位不变,然后减1,得到反码(补码 = 反码 + 1)。

负数取反:将初始数值转换成二进制数原码再取反运算:即将0变为1、将1变为0。得到的是最终结果的补码(到达这一步后所得的二进制数为正数,由于正数的原码、反码、补码相同,后面的运算可以忽略,视此步得到的为最终结果的二进制数)

public static void main(String[] args) {
        int a = -4;//1100 取反 0011=3
        System.out.println(~a);//Output: 3
    }

左移(<<)

运算规则:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。例如: 12345 << 1,则是将数字12345左移1位:

用new运算符创建的对象java new运算符的用法java_运算符

public static void main(String[] args) {
        System.out.println(12345<<1);//Output: 24690
    }

位移后十进制数值变成:24690,刚好是12345的二倍,所以有些人会用左位移运算符代替乘2的操作,但是这并不代表是真的就是乘以2,很多时候,我们可以这样使用,但是一定要知道并不代表两者是一样的。

注意:当左移的位数大于等于32位的时候,位数会先求余数,然后再进行左移,也就是说,如果真的左移32位 12345 << 32 的时候,会先进行位数求余数,即为 12345<<(32%32) 相当于 12345<< 0 ,所以12345<< 33 的值和12345<<1 是一样的,都是 24690。

右移(>>)

运算规则:右移位运算符有“正”、“负”值:若值为正,则在高位插入 0;若值为负,则在高位插入 1,低位舍去。同样,还是以12345这个数值为例,12345右移1位: 12345>>1:

用new运算符创建的对象java new运算符的用法java_java_02

public static void main(String[] args) {
       System.out.println(12345>>1);//Output: 6172
   }

右移后得到的值为 6172 和int 类型的数据12345除以2取整所得的值一样,所以有些时候也会被用来替代除2操作。另外,对于超过32位的位移,和左移运算符一样,,会先进行位数求余数。

无符号右移(>>>)

无符号右移运算符和右移运算符是一样的,不过无符号右移运算符在右移的时候是补0的。

用new运算符创建的对象java new运算符的用法java_用new运算符创建的对象java_03

public static void main(String[] args) {
        System.out.println(-12345>>>1);//Output: 2147477475
    }

三元运算符

三元运算符,也称为条件运算符。这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,简洁版的if else语句。表达式格式如下:

布尔表达式 ? 值 1 : 值 2

若表达式计算为 true,则返回结果 值 1 ;如果表达式的计算为 false,则返回结果 值 2。

public static void main(String[] args) {
        System.out.println(ternary(5));//Output: 500
        System.out.println(standardIfElse(11));//Output: 110
    }
    static int ternary(int i) {
        return i < 10 ? i * 100 : i * 10;
    }

    static int standardIfElse(int i) {
        if(i < 10)
            return i * 100;
        else
            return i * 10;
    }

三元运算符的引入多半就是为了高效编程,如果对业务处理不频繁可以考虑这种方式。

字符串运算符

这个运算符在 Java 里有一项特殊用途:连接字符串。只要当中有一条数据是字符串类型,其他非字符串数据都将被转换为字符串形式并连接

public static void main(String[] args) {
        int a= 1; int b = 2; int c= 3;
        String str="a,b,c";
        System.out.println(str+a+b+c);
        str +="(total)= ";
        System.out.println(str+(a+b+c));
        String abc=a+b+c+"";
        /*Output:
        a,b,c123
        a,b,c(total)= 6
        */
    }

+= 运算符可以拼接其右侧的字符串连接结果并重赋值给自身变量 str,括号 () 可以控制表达式的计算顺序,以便在显示 int 之前对其进行实际求和。
请注意主方法中的最后一个例子:我们经常会看到一个空字符串 " " 跟着一个基本类型的数据。这样可以隐式地将其转换为字符串,以代替繁琐的显式调用方法(如这里可以使用 Integer.toString())。

类型转换

“类型转换”(Casting)的作用是“与一个模型匹配”。在适当的时候,Java 会将一种数据类型自动转换成另一种。例如,假设我们为 float 变量赋值一个整数值,计算机会将 int 自动转换成 float。我们可以在程序未自动转换时显式、强制地使此类型发生转换。

public static void main(String[] args) {
        int a= 1;
        float b =a;
        System.out.println(b);//Output: 1.0
    }

在 Java 里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(Narrowing Conversion)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。对于“向上转换”(Widening conversion),则不必进行显式的类型转换,因为较大类型的数据肯定能容纳较小类型的数据,不会造成任何信息的丢失。

截断和舍入

在执行“向下转换”时,必须注意数据的截断和舍入问题。若从浮点值转换为整型值,Java 会做什么呢?下面是代码示例:

public static void main(String[] args) {
        double a = 0.19;
        float b = 1.5f;
        System.out.println("int a = "+ (int)a);
        System.out.println("int b = "+ (int)b);
        /*Output:
		int a = 0
		int b = 1
		*/
    }

从 float 和 double 转换为整数值时,小数位将被截断。若你想对结果进行四舍五入,可以使用 java.lang.Math 的 round() 方法。

public static void main(String[] args) {
        double a = 0.19;
        float b = 1.5f;
        System.out.println("int a = "+ Math.round(a));
        System.out.println("int b = "+ Math.round(b));
        /*Output:
        int a = 0
		int b = 2
		*/
    }

我们无需通过 import 就可以使用,因为 round() 方法是 java.lang 的一部分。

装箱和拆箱

装箱就是自动将基本数据类型转换为包装器类型拆箱就是自动将包装器类型转换为基本数据类型

public static void main(String[] args) {
    	//自动装箱
        Integer install = 100;
        //自动拆箱
        int excrete = install;
    }

反编译class文件之后得到如下内容:

用new运算符创建的对象java new运算符的用法java_用new运算符创建的对象java_04


装箱的时候系统为我们执行了Integer.valueof()方法,始终会返回一个Integer对象。拆箱的时候系统执行了Integer.intValue()的方法,直接将全局变量value返回。(如果不了解可以查看Integer里面的源码)

类型提升

java中的自动类型提升由低字节向高字节自动转换:byte->short->int->long->float->double,若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度)。通常,表达式中最大的数据类型是决定表达式结果的数据类型。float 型和 double 型相乘。

public static void main(String[] args) {
        int a = 1;
        double b =1.5;
        long c = 2;
        System.out.println("a * b = " +(a*b));
        System.out.println("a * c = " +(b*c));
        /*Output:
		a * b = 1.5
		a * c = 3.0
		*/
    }

Java没有sizeof

Java 不需要 sizeof() 方法来满足这种需求,因为所有类型的大小在不同平台上是相同的。我们不必考虑这个层次的移植问题 —— Java 本身就是一种“与平台无关”的语言。