初识Java 2-1 操作符

目录

优先级

赋值

递减和递增操作符

关系操作符

逻辑操作符

字面量

字面量中的下划线

科学记数法

按位操作符

移位操作符

三元操作符

字符串操作符+和+=

类型转换操作符

截尾和舍入


 

本笔记参考自: 《On Java 中文版》


        Java的操作符大多继承自C++,但Java对其进行了一些改进和简化。操作符是对数据进行操作,能接受一个或多个参数,生成一个新的值。

    有些操作符能修改操作数自身的值,这被称为“副作用”。由“副作用”生成的值也可供使用。

优先级

        当多个操作符存在时,就需要进行优先级的决定。最简单的规则就是先乘除,后加减。但复杂的规则往往难以记住,因此使用括号“()”是一个不错的选择。例如:

public class Precedence {
    public static void main(String[] args) {
        int x = 1, y = 2, z = 3;
        int a = x + y - 2 / 2 + z;
        int b = x + (y - 2) / (x - 2);

        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}

        上述程序使用了两条相似的语句,但语句的输出结果因为括号而不同:

        另外,在System.out.println()语句中使用的操作符+,这里的+代表着字符串的连接(如果必要,也会进行字符串的转换)。

赋值

        通过操作符=可以进行将符号右边的值赋给等号左边。其中左值必须是一个独特的命名变量(必须有一个可以储存值的物理空间)。

        对于基本类型而言,赋值很好理解,因为基本类型储存的是实际的值,赋值的过程就是将内容复制到了另一个地方。

int a = 10;
int b = a;

        但在为对象进行赋值的时候,情况就不一样了。因为操作对象,实际上是在操作这个对象的引用。如果“将一个对象赋值给另一个对象”,那么真正发生的是将一个引用复制到了另一个地方。例如:

class Tank {
    int level;
}

public class Assignment {
    public static void main(String[] args) {
        Tank t1 = new Tank();
        Tank t2 = new Tank();
        t1.level = 10;
        t2.level = 20;
        System.out.println("t1.level = " + t1.level + ", t2.level = " + t2.level);

        t1 = t2;        //此处t1的引用关联的是t2的指向的对象,t1原本的对象被覆盖了。
        System.out.println("t1.level = " + t1.level + ", t2.level = " + t2.level);

        t1.level = 30;
        System.out.println("t1.level = " + t1.level + ", t2.level = " + t2.level);
    }
}

        程序执行的结果如下:

        在上述的代码中,t1 = t2t1原本关联的对象覆盖了,此后t1关联的对象就变为了t2关联的对象。这种现象被称为“别名”,是Java操作对象的一种基本方式。若不想产生这个别名,可以这样做:

t1.level = t2.level;

    但是直接操作对象内部的字段违背了Java的设计原则,这是需要注意的。

别名陷阱

        将一个对象作为参数传递给方法时,也会产生别名。

class Letter {
    char c;
}

public class PassObject {
    static void f(Letter y) {
        y.c = 'z';
    }
}

        在上述代码中,方法f()会在其作用域的范围内生成一个参数Letter y的副本。由于实际上传递的是一个引用,所以若方法f()中的 y.c = 'z'; 实际上会改变f()之外的对象

递减和递增操作符

        Java中存在两种快捷运算符:递减操作符--(减少一个单位)和递增操作符++(增加一个单位)。这两种操作符都有两种用法,即前缀式后缀式,用法的区别在于操作符与变量的相对位置:在变量之前是前缀,反之是后缀。

        对于前缀的递增和递减,程序会先执行运算,再返回生成的结果;而对于后缀的二者而言,程序会优先返回变量的值,再执行运算。以递增为例:

public class AutoInc {
    public static void main(String[] args) {
        int i = 1;
        System.out.println("i = " + i);
        System.out.println("++i = " + ++i);
        System.out.println("i++ = " + i++);
        System.out.println("i = " + i);
    }
}

        程序的执行结果如下:

关系操作符

        关系操作符会根据操作数的值之间的关系生成一个布尔结果truefalse)。关系操作符有六种:<><=>===!=。在Java中,等于和不等于可用于所有类型,但是其他的比较操作不适用于boolean类型。因为boolean值只有真(true)和假(false)。

对象是否相等

        尽管操作符==!=适用范围广,但是在面对一些和对象相关的比较时,就会出现问题。例如:

public class Equivalence {
    static void show(String desc, Integer n1, Integer n2) {
        System.out.println(desc + ":");
        System.out.printf(
                "%d==%d %b %b%n", n1, n2, n1 == n2, n1.equals(n2));
    }    //%b表示布尔值

    public static void test(int value) {
        Integer i1 = value;
        Integer i2 = value;
        show("直接创建对象时", i1, i2);

        Integer r1 = Integer.valueOf(value);
        Integer r2 = Integer.valueOf(value);
        show("使用Integer.valueOf()创建对象时", r1, r2);

        int x = value;
        int y = value;
        System.out.println("使用原始的int时:");
        System.out.printf("%d==%d %b%n", x, y, x == y);
    }

    public static void main(String[] args) {
        test(127);
        System.out.println("\n");
        test(128);
    }
}

        该程序的输出结果是:

初识Java 2-1 操作符_第1张图片

        上述比较之所以出现差异,关键在于:操作符==比较的是Integer对象的引用。

    Integer会根据值的大小范围使用两种不同的缓存方式:

        · 若值的范围在-128~127之间,Integer会使用享元模式进行缓存,因此即使多次调用Integer.valueOf(127),其生成的也是同一个对象。

        · 若在上述范围之外,Integer每次产生的将是不同的对象,因此两次调用Integer.valueOf(127)返回的是不同的对象。

        上述程序中,在test(127)的执行过程中,之所以操作符==可以返回正确的布尔值,不是因为该操作符进行了正确的比较,而只是因为两者的引用恰好相同罢了。因此,如果要对Integer对象进行比较,就需要使用equals()

        但equals()也不是万能的, 该方法的默认行为是比较引用。这意味着,如果对一个自定义的类使用equals(),例如:

class ValA {
    int i;
}

public class EqualsMethod {
    public static void main(String[] args) {
        ValA va1 = new ValA();
        ValA va2 = new ValA();
        va1.i = va2.i = 100;
        System.out.println(va1.equals(va2));
    }
}

        上述程序的结果是false,这就是因为默认的equals()比较的是引用。所以为了能够正确比较对象的值,就需要通过重写equals()等方式。

        另一种相等比较问题发生在进行极小数的比较时。例如:

public class DoubleEquivalence {
    static void show(String desc, Double n1, Double n2) {
        System.out.println(desc + ":");
        System.out.printf(
                "%e==%e %b %b%n", n1, n2, n1 == n2, n1.equals(n2));
    }

    public static void test(double x1, double x2) {
        System.out.printf("%e==%e %b%n", x1, x2, x1 == x2); // %e表示科学记数法
        Double d1 = x1;
        Double d2 = x2;
        show("直接创建对象时", d1, d2);

        Double r1 = Double.valueOf(x1);
        Double r2 = Double.valueOf(x2);
        show("使用Double.valueOf()创建对象时", r1, r2);
    }

    public static void main(String[] args) {
        test(Double.MAX_VALUE, Double.MAX_VALUE - Double.MIN_VALUE * 1_000_000);
    }
}

        程序运行的结果如下:

        这种错误出现的原因是因为,当一个非常大的值减去一个较小的值时,非常大的值不会出现明显变化,这就是舍入误差。而这种误差的出现,则来自于机器无法储存足够的信息来表示这种大数值的小变化。

逻辑操作符

        逻辑操作符“与”(&&)、“或”(||)和“非”(!)会根据参数的逻辑关系,生成布尔值结果。但在Java中,“与”和“或”操作只能用于布尔值

短路

        逻辑操作符支持“短路”现象:一旦表达式当前部分的计算结果能准确表达整个表达式的值,那么表达式剩余的部分将不再被执行。

public class ShortCircuit {
    static boolean test1(int val) {
        System.out.println("test1(" + val + ")");
        System.out.println("结果:" + (val < 1));
        return val < 1;
    }

    static boolean test2(int val) {
        System.out.println("test2(" + val + ")");
        System.out.println("结果:" + (val < 2));
        return val < 2;
    }

    static boolean test3(int val) {
        System.out.println("test3(" + val + ")");
        System.out.println("结果:" + (val < 3));
        return val < 3;
    }

    public static void main(String[] args) {
        boolean b = test1(0) && test2(2) && test3(2);
        System.out.println("\n表达式的值是:" + b);
    }
}

        上述程序的运行结果是:

初识Java 2-1 操作符_第2张图片

        很明显,test3()没有被执行。这是因为test2()的结果是false,因此整个表达式的值肯定是false,剩下的表达式就没有必要执行了。通过这种短路的形式,可以减少资源的消耗。

字面量

        一般,编译器能够准确识别一个字面量的类型。但是在一些类型模糊的情况下,就必须使用与这些字面量相关的字符来进行引导。例如:

public class Literals {
    public static void main(String[] args) {
        int l1 = 0x2f; // 十六进制(小写)
        System.out.println("l1: " + Integer.toBinaryString(l1));

        int l2 = 0x2F; // 十六进制(大写)
        System.out.println("l2: " + Integer.toBinaryString(l2));

        int l3 = 0177; // 八进制(前置0)
        System.out.println("l3: " + Integer.toBinaryString(l3));

        char c = 0xffff; // char类型的最大十六进制值
        System.out.println("c: " + Integer.toBinaryString(c));

        byte b = 0x7f; // byte类型的最大十六进制值
        System.out.println("b: " + Integer.toBinaryString(b));

        long n1 = 200L; // L是long类型后缀
        long n2 = 200l; // L的大小写都可以
        long n3 = 200;

        float f1 = 2L;
        //以此类推...

        double d1 = 1D;
        //以此类推...
    }
}

        程序执行的结果如下:

        若试图将一个变量初始化为超出其范围的值,编译器会报错。上述程序展示了charbyte类型的最大十六进制值,当赋值超出了这一范围,编译器会自动将值转换为int类型,并说明本次赋值需要进行“窄化转型”。

    若将较小的类型传递给方法Integer.toBinaryString()方法,该类型会被自动转换为int类型。

字面量中的下划线

        在Java7之后,在数字字面量里使用下划线是被允许的。这在表示大数值时大有帮助。例如,可以这样初始化:

int bin = 0b0010_1111_1010_1111_1001_1000;

        在使用下划线时有几条规则:

  1. 只能使用单个的下划线,不能连续使用多个;
  2. 数字的开头和结尾不能存在下划线;
  3. 类似于FDL的后缀周围不能有下划线。
  4. 在二进制或十六进制标识符bx周围不能有下划线。

     在方法System.out.printf()中可以使用%n替代惯用的\n。这样替代的理由是在不同的平台上,换行符的表达是不同的。使用%n,Java会自动匹配各个平台的换行符,因此可以省去不必要的麻烦。


科学记数法

        在科学与工程领域,“e”表示自然对数的基数(约为2.718)。但是在一些程序语言中(Java也包含在内),e被用来表示“10的幂次”。所以如果在Java中看到类似于1.28e-43f的表达式时,其的含义实际上是:1.28\times 10^{-43}

        一般,若编译器能够准确识别类型,那么就不需要在数值后面添加后缀名。但是如果出现类似于下方展示的情况:

float f = 1e-23f;

在编译器中,指数通常被作为double类型处理,此时尾部的f就是必须的(否则我们将收到一条报错提示)。

按位操作符

        按位操作符可以操作整数基本类型中的单个二进制位(bit)。总共有四个操作符:按位与(&)、按位或(|)、按位异或(^)和按位非(~)。和加减乘除类似,按位操作符也可以和等号(=)联用:&=|=^=~是一元操作符,不能与等号联用)。

    布尔值只能进行按位与、或和异或操作,而不能执行按位非操作。另外,对于布尔值,按位操作符和逻辑操作符有相同的效果,但是按位操作符不会“短路”。

移位操作符

        移位操作符也能操作二进制位,且只能用来处理基本类型中的整数类型。移位操作符包括了三种操作符:

  • 左移(<<):将操作符左侧的操作数向左移动,移动的位数在操作符右侧指定。
  • 有符号右移(>>):将操作符左侧的操作数向有移动,若符号为正,在高位补0,否则补1。
  • 无符号右移(>>>):无论符号正负,都在高位补0。

    当对charbyteshort这些较小的类型进行移位运算时,在开始运算前会自动将它们的类型转变为int,并且结果也是int类型。

        若对int类型进行移位操作,在进行运算的时候,操作符右侧的可移位数只会用到低5位。而如果是处理long类型,那么只会用到可移位数的低6位。通过这种方式,可以放置移位超出类型允许的范围。

        若使用的是<<=>>=>>>=,那么在对charbyteshort等类型进行移位时,可能发生截断(此时得到的结果会是-1):

public class URshift {
    public static void main(String[] args) {
        short s = -1;
        System.out.println("没有移位时: " + Integer.toBinaryString(s));
        System.out.println("正确的移位: " + Integer.toBinaryString(s >>> 10));

        s >>>= 10;
        System.out.println("错误的移位: " + Integer.toBinaryString(s));
    }
}

        程序执行的结果如下:

        语句Integer.toBinaryString(s >>> 10)之所以没有出错,就是因为在移位之后,结果没有重新赋值给short类型的变量s,而是在完成运算之后直接打印,因此没有发生截断。

    在程序中表示的数字的二进制形式是“有符号的二进制补码”。

三元操作符

        三元操作符,即条件操作符,它有三个操作数。其形式如下:

boolean-exp ? value0: value1

        若boolean-exp的结果是true,则执行value0,其结果就是该三元操作符的结果值;反之,则执行value1,同样得到一个来自value1的结果值。

    三元操作符的应用场景主要在于从两个值中选择一个给变量赋值。

字符串操作符+和+=

        在Java中,++=操作符可以用来连接字符串。

    C++可以通过操作符重载来达成这一功能,但由于这一特性被认为过于复杂,所以Java无法像C++这样进行操作符的重载。

        若一个表达式以字符串开头,则其后面的所以操作数都得是字符串类型(编译器会强制转换):

public class StringOperators {
    public static void main(String[] args) {
        int x = 0, y = 1, z = 2;
        String s = "x, y, z ";

        System.out.println(s + x + y + z); // x、y和z的内容会被转变为字符串
        System.out.println(x + " " + s); // 开头的x的内容会被转换为字符串

        s += "(总和) = ";
        System.out.println(s + (x + y + z)); // 使用括号控制先后顺序

        System.out.println("" + x);       //类似于Integer.toString()的用法,但更简单
    }
}

        程序执行的结果如下:

        对语句System.out.println(s + x + y + z);而言,编译器会先将xyz转换为对应的字符串形式,这也是为什么没有发生整数求和,得到一个结果3。而在语句System.out.println(s + (x + y + z));中,由于通过括号控制了字符串转换的先后顺序,所以能够得到一个最终的结果。

类型转换操作符

        在适合的时候,Java会自动将一种类型转变为另一个类型。例如将整数值赋值给浮点变量时,编译器会自动将int类型转换为float类型。

        编译器可以自动地将较小的类型提升为较大的类型,但是有时候,我们会需要将较大的类型转变为较小的类型,即窄化转型

int i = 200;
long lng = i; // 向上提升,可以省略强制类型转换
i = (int)lng; // 窄化转型,需要强制类型转换

        在Java中,类型转换是比较安全的,但是在进行窄化转型时,依旧存在丢失信息的风险。这是因为较小的数据类型无法容纳更多的信息。

    boolean是应该特殊的类型,它不允许任何的类型转换。除此之外,“类”类型也不被允许转换为其他类型。

截尾和舍入

        通常地,若将一个较大的类型转换为较小的类型,程序往往会对数值进行截尾:

public class CastingNumbers {
    public static void main(String[] args) {
        double above = 0.7, below = 0.4;

        System.out.println("(int)above: " + (int)above);
        System.out.println("(int)below: " + (int)below);
    }
}

        程序执行的结果是:

        在上述程序中,我们将double类型的数据强制转换为int类型,并且发生了截尾,double类型数值的小数位被截断了。

    若需要进行舍入,可以使用java.lang.Math中的round()方法。

你可能感兴趣的:(Java,java)