操作符

第3章 操作符

在最底层,Java中的数据是通过使用操作符来操作的。

Java是建立在C++的基础之上的,Java相比C和C++,在操作符上也做了一些改进与简化。

3.1 更简单的打印语句

在上一章中,我们介绍了Java的打印语句:

System.out.println("Rather a lot to type");

可以看到,这条语句不仅涉及多种类型,而且不便于阅读。所以,我们可以使用静态导入(static import)并创建一个小类库来简化上一章的程序:

import static com.jiao.util.Print.print;
import java.util.Date;

public class HelloDate {
    public static void main(String[] args) {
        print("Hello,it's:");
        print(new Date());
    }
}

3.2 使用Java操作符

操作符作用于操作数,生成一个新值。

有些操作符可能会改变操作数自身的值,这被称为副作用。

大部分操作符只能操作基本类型,例外的一些,如:=、==、!= 这些操作符能操作所有的对象。

String类支持 + 和 += 。

3.3 优先级

当一个表达式中存在多个操作符时,操作符的优先级就决定了各部分的计算顺序。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) / (2 + z);
        System.out.println("a=" + a + " b=" + b);
    }
}

3.4 赋值

赋值使用操作符 "=" 。它的意思是:取右值,赋予给左值。

  • 右值:可以是任何常数、变量或表达式。
  • 左值:必须是一个明确的、已命名的变量。

Java中的赋值,存在两种情况:

  • 基本数据类型:将右值赋予左值后,左值改变不影响右值。

  • 引用类型:将右值赋予左值后,对左值操作会影响右值。即两个变量存储的都是同一个对象的引用,无论对哪个变量操作,都是对该对象进行操作。

下面,我们通过例子说明上述情况:

class Tank {
    int level;
}

public class Assignment {
    public static void main(String[] args) {
        int a = 39;
        int b = 78;
        print("a = " + a + ",b = " + b);
        a = b;
        print("a = " + a + ",b = " + b);
        a = 99;
        print("a = " + a + ",b = " + b);

        Tank t1 = new Tank();
        Tank t2 = new Tank();
        t1.level = 8;
        t2.level = 88;
        print("t1.level:" + t1.level + ",t2.level:" + t2.level);
        t1 = t2;
        print("t1.level:" + t1.level + ",t2.level:" + t2.level);
        t1.level = 99;
        print("t1.level:" + t1.level + ",t2.level:" + t2.level);
    }
} /* Output:
    a = 78,b = 78
    a = 99,b = 78
    t1.level:8,t2.level:88
    t1.level:88,t2.level:88
    t1.level:99,t2.level:99 
    */

在上例中,将t2赋给t1,则两个变量持有同一个引用,此时对任一变量操作,均会改变引用所指向的对象信息,这种特殊的现象被称为:别名现象,是Java操作对象的一种基本方式。

如果我们需要避免别名问题,可以采取下面的做法:

t1.level = t2.level;

3.4.1 方法调用中的别名问题

将一个对象传递给方法时,也会产生别名问题:

class Letter {
    char c;
}

public class PassObject {
    static void f(Letter y) {
        y.c = 'z';
    }
    public static void main(String[] args) {
        Letter x = new Letter();
        x.c = 'a';
        print("x.c: " + x.c);
        f(x);
        print("x.c: " + x.c);
    }
}   

通过上例,我们发现,在方法调用过程中,形参实际上是接收到了实参传递的对象引用。

3.5 算数操作符

Java的基本算数操作符包括:加号(+)、减号(-)、除号(/)、乘号(*)以及取模操作符(%)。 并且,使用操作符后紧跟一个等号可以同时进行运算与赋值操作。 例如:

public class MathOps {
    public static void main(String[] args) {
        Random rand = new Random(88);
        int i, j, k;
        j = rand.nextInt(100) + 1;
        print("j: " + j);
        k = rand.nextInt(100) + 1;
        print("k: " + k);
        i = j + k;
        print("j + k : " + i);
        i = j - k;
        print("j - k : " + i);
        i = k / j;
        print("k / j : " + i);
        i = k % j;
        print("k % j : " + i);
        j %= k;
        print("j %= k : " + j);
    }
}

上例使用了Random类来帮助我们生成随机数,参数为随机数生成器的种子,随机数生成器对于特定的种子值总是产生相同的随机数序列。

3.5.1 一元加、减操作符

一元减号和加号与二元减号和加号都使用相同的符号,根据表达式的书写形式,编译器会自己判断出使用的是哪一种。 例如:

x = -a;
x = a * (-b);

一元减号用于转变数据的符号,而一元加号则是为了与一元减号相对应,它唯一的作用是将较小类型的操作数提升为int。

3.6 自动递增和递减

Java提供了大量的快捷运算,使得编码更方便,但有时可能使代码阅读起来更困难。

递增和递减是两种相当不错的快捷运算,通常称为"自动递增"和"自动递减":

  • 递减操作符为:--,意为减少一个单位。
  • 递增操作符为:++,意为增加一个单位。

这两个操作符各有两种使用方式,分别为前缀式和后缀式:

  • 前缀式:操作符位于变量或表达式的前面,先执行递增或递减。
  • 后缀式:操作符位于变量或表达式的后面,后执行递增或递减。

下面是一个例子:

public class AutoInc {
    public static void main(String[] args) {
        int i = 1;
        print("i: " + i);
        print("++i: " + ++i);
        print("i++: " + i++);
        print("i: " + i);
        print("--i: " + --i);
        print("i--: " + i--);
        print("i: " + i);
    }
}

递增和递减是除了那些涉及赋值的操作符以外,唯一具有副作用的操作符,即,它们在进行运算的同时会改变操作数。

3.7 关系操作符

关系操作符生成的是一个boolean(布尔)结果,它们计算的是操作数的值之间的关系。

关系操作符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)以及不等于(!=)。等于和不等于适用于所有的基本数据类型,而其他比较符不适用于boolean类型。

3.7.1 测试对象的等价性

关系操作符==和!=也适用于所有对象,下面是一个例子:

public class Equivalence {
    public static void main(String[] args) {
        Integer n1 = new Integer(88);
        Integer n2 = new Integer(88);
        System.out.println(n1 == n2);
        System.out.println(n1 != n2);
    }
}

我们发现,结果出乎我们意料,因为,==和!=作用于对象时,比较的是对象引用而非对象内容。

我们需要比较两个对象的实际内容时,可以使用equals()方法:

public class EqualsMethod {
    public static void main(String[] args) {
        Integer n1 = new Integer(88);
        Integer n2 = new Integer(88);
        System.out.println(n1.equals(n2));
    }
}

上例结果如我们所料,但比较的是自己的类型时:

class Value {
    int i;
}

public class EqualsMethod2 {
    public static void main(String[] args) {
        Value v1 = new Value();
        Value v2 = new Value();
        v1.i = v2.i = 100;
        System.out.println(v1.equals(v2));
    }
}

这次的结果却是false,这是由于equals()的默认行为是比较对象引用,所以除非在自定义的新类中覆盖equals()方法,否则无法表现出我们希望的行为。

大多数Java类型都重写了equals()方法,以便用来比较对象的内容而非引用。

3.8 逻辑操作符

逻辑操作符:与(&&)、或(||)、非(!)能根据参数的逻辑关系,生成一个布尔值(true或false)。 下面的例子使用了关系操作符和逻辑操作符:

public class Bool {
    public static void main(String[] args) {
        Random rand = new Random(66);
        int i = rand.nextInt(100);
        int j = rand.nextInt(100);
        print("i = " + i);
        print("j = " + j);
        print("i > j is " + (i > j));
        print("i < j is " + (i < j));
        print("i >= j is " + (i >= j));
        print("i <= j is " + (i <= j));
        print("i == j is " + (i == j));
        print("i != j is " + (i != j));
        print("(i < 10) && (j < 10) is " + ((i < 10) && (j < 10)));
        print("(i < 10) || (j < 10) is " + ((i < 10) || (j < 10)));
    }
}

逻辑操作符只能作用于布尔值,不可将一个非布尔值当作布尔值在逻辑表达式中使用。

3.8.1 短路

当使用逻辑操作符时,我们会遇到一种短路现象。即一旦能够明确无误地确定整个表达式的值,就不再计算表达式的余下部分了。 下面是演示短路现象的例子:

public class ShortCircuit {
    static boolean test1(int val) {
        print("test1(" + val + ")");
        print("result: " + (val < 1));
        return val < 1;
    }
    static boolean test2(int val) {
        print("test1(" + val + ")");
        print("result: " + (val < 2));
        return val < 2;
    }
    static boolean test3(int val) {
        print("test1(" + val + ")");
        print("result: " + (val < 3));
        return val < 3;
    }
    
    public static void main(String[] args) {
        boolean b = test1(0) && test2(2) && test3(2);
        print("expression is " + b);
    }
} 

在上例中,当方法被调用,则会输出信息。当第二个测试产生了false结果时,意味着整个表达式肯定为false,所以没必要继续计算剩余的表达式。如果所有的逻辑表达式都有一部分不必计算,则会获得潜在的性能提升。

3.9 直接常量

当我们在程序中使用直接常量时,有时编译器无法准确知道该常量类型,需要使用某些字符来额外增加一些信息帮助编译器得知其类型。 下面的代码展示了这些字符:

public class Literals {
    public static void main(String[] args) {
        int i1 = 0x2f;
        print("i1: " + Integer.toBinaryString(i1));
        int i2 = 0X2F;
        print("i2: " + Integer.toBinaryString(i2));
        int i3 = 0177;
        print("i3: " + Integer.toBinaryString(i3));

        char c = 0xffff;
        print("c: " + Integer.toBinaryString(c));
        byte b = 0x7f;
        print("b: " + Integer.toBinaryString(b));
        short s = 0x7fff;
        print("s: " + Integer.toBinaryString(s));

        long n1 = 200L;
        long n2 = 200l;
        long n3 = 200;
        float f1 = 1;
        float f2 = 1F;
        float f3 = 1f;
        double d1 = 1d;
        double d2 = 1D;
    }
}

直接常量后面的后缀字符标志了它的类型:

  • 大写或小写的L,代表long。
  • 大写或小写的F,代表float。
  • 大写或小写的D,代表double。

十六进制数适用于所有的整数数据类型,以前缀Ox或0X,后面跟随数字0~9或字符a~f来表示。 在上面的代码中已经给出了char、byte以及short所能表示的最大十六位进制值,如果试图将一个变量初始化成超出自身表示范围的值,编译器则会该值作为int型处理,并报告一条错误信息,告诉我们需要对将int型进行窄化转换。

八进制数由前缀0以及后续的0~7的数字来表示。

二进制则没有直接常量的表示方式,但是在使用十六进制和八进制记数法时,以二进制形式显示结果非常有用。通过使用Integer和Long的静态方法toBinaryString()可以很容易地实现。不过将较小类型传递给Integer.toBinaryString(),则该类型会自动被转换为int。

数字的二进制表示形式称为:有符号的二进制补码。

3.9.1 指数记数法

Java采用了一种不直观的记数法来表示指数,例如:

public class Exponents {
    public static void main(String[] args) {
        float expFloat = 1.39e-43f;
        System.out.println(expFloat);
        double expDouble = 66e66d;
        System.out.println(expDouble);
    }
}

在科学与工程领域上,e代表自然对数的基数,约等于2.718,Java中的Math.E给出更精确的double型的值。

在编程语言中,e则代表10的幂次。

编译器会将小数默认作为双精度数(double)处理,上例中,如果不在变量expFloat后加上f,则会收到一条出错提示,告诉我们必须使用强制类型转换将double转型为float。

3.10 按位操作符

按位操作符用来操作整数基本数据类型中的单个比特,即二进制位。按位操作符会对两个参数中对应的位执行布尔代数运算,并最终生成一个结果。

按位操作符有以下几种:

  • 按位与操作符(&):两个输入位都为1,则生成一个输出位1。
  • 按位或操作符(|):两个输出位中有一个1,则生成一个输出位1;都为0时,输出0。
  • 按位异或操作符(^):输入位的某一个是1,但不全是1,则生成一个输出位1。
  • 按位非操作符(~):也称为取反操作符,它属于一元操作符,只对一个操作数进行操作。

3.11 移位操作符

移位操作符操作的运算对象也是二进制的位,只可用来处理整数类型。

  • 左移位操作符(<<):按照操作符右侧指定的位数将操作数向左移动(低位补0)。

  • 有符号右移位操作符(>>):按照操作符右侧指定的位数将操作数向右移动,若符号为正,则在高位插入0,符号为负,则在高位插入1。

  • 无符号右移位操作符(>>>):无论正负,都在高位插入0。

如果对于char、byte或者short类型的数值进行移位处理,那么在移位前,它们会被转换位int类型,并且得到的结果也是一个int类型的值。

移位可与等号组合使用(<<=、>>=、>>>=),此时,操作符左边的值会移动由右边指定的位数,再将得到的结果赋给左边的变量。但在进行无符号右移位结合赋值时,操作数如果是byte或short类型,它们会先被转为int类型,再进行右移操作,然后被截断,赋值给原来的类型,这种情况下可能得到-1的结果。 下面的例子演示了这种情况:

public class URShirft {
    public static void main(String[] args) {
        int i = -1;
        print(Integer.toBinaryString(i));
        i >>>= 10;
        print(Integer.toBinaryString(i));
        long l = -1;
        print(Long.toBinaryString(l));
        l >>>= 10;
        print(Long.toBinaryString(l));
        short s = -1;
        print(Integer.toBinaryString(s));
        s >>>= 10;
        print(Integer.toBinaryString(s));
        byte b = -1;
        print(Integer.toBinaryString(b));
        b >>>= 10;
        print(Integer.toBinaryString(b));
        b = -1;
        print(Integer.toBinaryString(b));
        print(Integer.toBinaryString(b >>> 10));
    }
}

在最后一个移位运算中,结果没有赋给b,而是直接打印出来,所以其结果是正确的。

3.12 三元操作符 if-else

三元操作符也称为条件操作符,其表达式采取下述形式:

boolean-exp ? value0 : value1
  • boolean-exp的结果为true,则计算value0,并且将其作为表达式的结果。
  • boolean-exp的结果为true,则计算value1,并且将其作为表达式的结果。

下面通过示例说明条件操作符的用法:

public class TernaryIfElse {
    static int ternary(int i) {
        return i < 10 ? i * 100 : i * 10;
    }

    public static void main(String[] args) {
        print(ternary(9));
        print(ternary(90));
    }
}

3.13 字符串操作符 + 和 +=

为了更方便地操作字符串,Java对+和+=进行了操作符重载(operator overloading),使得它们可以用于连接字符串。并且,如果表达式以字符串开头,那么后续的+操作符均被当成字符串操作符使用,操作数也均被转型为String:

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

3.14 使用操作符时常犯的错误

程序员在使用操作符时,经常会犯如下错误:

  • 对表达式如何计算有点不确定,也不愿意使用括号。
  • 将&和| 与 &&和||使用错误。

3.15 类型转换操作符

在适当的时候,Java会将一种数据类型自动转换成另一种。例如,将浮点变量赋以以一个整数值,编译器会将int自动转换成float。类型转换(cast)运算符,允许我们在不能进行自动转换的时候强制进行类型转换。

进行类型转换时,将转换后类型置于圆括号内,放在要进行类型转换的值左边,如下例所示:

public class Casting {
    public static void main(String[] args) {
        int i = 200;
        long lng = (long) i;
        lng = i;
        long lng2 = (long) 200;
        i = (int) lng2;
    }
}

在Java中,一般有两种类型转换方式:

  • 窄化转换:将能容纳更多信息的数据类型转换成容纳较少信息的数据类型,面临信息丢失的危险。此时,编译器会强制进行类型转换。 如:将int型变量赋以long类型数值。

  • 扩展转换:将能容纳较少信息的数据类型转换成容纳更多信息的数据类型,该行为是安全的,不必显式地进行类型转换。

Java允许除了布尔型之外的任何基本数据类型之间相互转换。

3.15.1 截尾和舍入

在执行窄化转换时,必须注意截尾和舍入的问题。例如,将一个浮点值转换为整型值。下例展示了Java的处理结果:

public class CastingNumbers {
    public static void main(String[] args) {
        double above = 0.7, below = 0.4;
        float fabove = 0.7f, fbelow = 0.4f;
        print("(int)above: " + (int) above);
        print("(int)below: " + (int) below);
        print("(int)fabove: " + (int) fabove);
        print("(int)fbelow: " + (int) fbelow);
    }
}

从上例的输出结果中可以看出,在将double或float转型位整型值时,总是对该数字进行截尾。如果想要得到舍入的结果,则需要使用java.lang.Math中的round()方法:

public class RoundingNumbers {
    public static void main(String[] args) {
        double above = 0.7, below = 0.4;
        float fabove = 0.7f, fbelow = 0.4f;
        print("Math.round(above): " + Math.round(above));
        print("Math.round(below): " + Math.round(below));
        print("Math.round(fabove): " + Math.round(fabove));
        print("Math.round(fbelow): " + Math.round(fbelow));
    }
}

3.15.2 提升

在对基本数据类型执行算术运算或按位运算时,只要类型比int小,那么在运算之前会自动转换成int,最终生成的结果也就是int类型。如果将结果赋值给较小类型,就会发生窄化转换,可能丢失信息。 通常,表达式中出现的最大的数据类型决定了表达式最终结果的数据类型。 例如,一个float值与一个double值相乘,则结果为double类型。

3.16 Java没有sizeof

在C和C++中,由于不同的数据类型在不同的机器上可能有不同的大小,在进行一些与存储空间有关的问题时,需要知道那些类型的大小,sizeof()用于查看数据类型的字节数。

在Java中,所有的数据类型在所有的机器中的大小是相同的,所以不需要sizeof()。

3.17 操作符小结

能够对布尔值进行的运算非常有限,我们只赋予它true和false值,并测试它们为真还是为假。

在char、byte和short中,我们可看到使用算术操作符中数据类型提升的效果。对于这些类型的任何一个进行算术运算,都会获得一个int结果,必须将其显式地类型转换回原来的类型,以将该值赋予原类型的变量。

而对于int类型,我们需要注意运算是否会造成溢出,如下例所示:

public class Overflow {
    public static void main(String[] args) {
        int big = Integer.MAX_VALUE;
        System.out.println("big = " + big);
        int bigger = big * 4;
        System.out.println("bigger = " + bigger);
    }
}
/* Output:
big = 2147483647
bigger = -4
*/

从上例中看出,当出现溢出时,编译器不会提示错误和警告,运行时也没有出现异常,结果发生了错误。

通过前面的学习,我们可以发现:除boolean以外,任何一种基本类型都可通过类型转换变为其他基本类型,当类型转换成一种较小的类型时,可能会丢失信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值