《Java编程思想》读书笔记之第3章-操作符

Posted 二木成林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java编程思想》读书笔记之第3章-操作符相关的知识,希望对你有一定的参考价值。

目录

 

第3章-操作符

3.1 更简单的打印语句

3.2 Java操作符

3.3优先级

3.4 赋值

3.4.1 方法调用中的别名问题

3.5 算术操作符

3.5.1 一元加、减操作符

3.6 自动递增和递减

3.7 关系操作符

3.7.1 测试对象的等价性

3.8 逻辑操作符

3.8.1 短路

3.9 直接常量

3.9.1 指数记数法

3.10 按位操作符

3.11 移位操作符

3.12 三元操作符if-else

3.13 字符串操作符+和+=

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

3.15 类型转换操作符

3.15.1 截尾和舍入

3.15.2 提升

3.16 Java没有sizeof


第3章-操作符

3.1 更简单的打印语句

本节使用了更简单的打印语句print(),而日常一般使用的是System.out.println()。

如果需要使用print(),需要静态导入第三方加包,该加包可通过源码获取。

package 第3章_操作符.第1节_更简单的打印语句;

import java.util.Date;

import static net.mindview.util.Print.*;

public class HelloDate {
    public static void main(String[] args) {
        // 原生的Java打印语句
        System.out.println("Rather a lot to type");

        // 使用print打印语句
        print("Hello, it's: ");
        print(new Date());
    }
}
/**
 * 打印结果:
 * Rather a lot to type
 * Hello, it's:
 * Sun Nov 01 21:21:59 CST 2020
 */

3.2 Java操作符

操作符如加号、正号(+)、减号、负号(-)、乘号(*)、除号(/)及赋值符号(=)用于操作数,生成一个新值,其会改变操作数自身的值,而且只能操作“基本类型”。

操作符如"="、"=="和"!="能操作所有的对象。

String类支持“+”和"+="操作符。

package 第3章_操作符.第2节_使用Java操作符;

public class ControlCharacter {
    public static void main(String[] args) {
        /* 加号、正号(+)、减号、负号(-)、乘号(*)、除号(/)及赋值符号(=) */
        System.out.println(1 + 1); // 加号
        System.out.println(+1); // 正号
        System.out.println(2 - 1);//减号
        System.out.println(-1);// 负号
        System.out.println(2 * 2);// 乘号
        System.out.println(4 / 2);// 除号
        int a = 5;
        System.out.println(a);// 赋值号

        /* "="、"=="和"!=" */
        Person p1 = new Person("张三");
        Person p2 = new Person("李四");
        Person p3;
        p3 = p1; // 赋值号
        System.out.println(p3.name);
        System.out.println(p1 == p2);// 判断是否相等的符号
        System.out.println(p1 != p2);// 不等号

        /* “+”和"+=" */
        String str1 = "Java";
        String str2 = "html";
        String str3 = str1 + str2;// +
        System.out.println(str3);
        str3 += str1; // +=
        System.out.println(str3);
    }
}

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }
}
/**
 * 打印结果:
 * 2
 * 1
 * 1
 * -1
 * 4
 * 2
 * 5
 * 张三
 * false
 * true
 * JavaHTML
 * JavaHTMLJava
 */

3.3优先级

如果一个表达式存在多个操作符,如x+y-z*m/n,那么操作符的优先级就决定了各部分的计算顺序,是先计算z*m还是先计算m/n呢,由操作符的优先级决定。Java对其作了规定,最简单的规则是先乘除后加减,但程序员可能会忘记优先级规则,所以使用括号明确计算顺序,如x+(y-z)*(m/n)。

package 第3章_操作符.第3节_优先级;

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 + x);
        System.out.println("a = " + a + " b = " + b);
    }
}
/**
 * 打印结果:
 * a = 5 b = 1
 */

注意:在System.out.println()语句中包含的"+"操作符,有连接字符串的作用,还有转换字符串的作用。

当编译器观察到一个String后面紧跟一个“+”,而这个“+”的后面又紧跟一个非String类型的元素时,就会尝试着将这个非String类型的元素转换为String。

package 第3章_操作符.第3节_优先级;

public class StringCharacter {
    public static void main(String[] args) {
        /* 1.加号在System.out.println()起连接字符串的作用 */
        System.out.println("I love " + "Java");

        /* 2.加号在System.out.println()起转换字符串的作用 */
        int num = 99;
        System.out.println("I love " + num);// 会把int类型的99转换成字符串输出
    }
}
/**
 * 打印结果:
 * I love Java
 * I love 99
 */

3.4 赋值

赋值使用操作符“=”。它的意思是“取右边的值.(即右值),把它复制给左边(即左值)”。右值可以是任何常数、变量或者表达式(只要它能生成一个值就行)。但左值必须是一个明确的并且已命名的变量。

例如:把一个常数赋给一个变量:

a=4;

但不能用常数作为左值,即不能说4=a。

  • 基本数据类型的赋值

为其赋值的时候,是直接将一个地方的内容复制到另一个地方。

例如:对基本类型使用a=b,那么就是把b的内容复制给a,若修改了a,那么b的内容还是不会发生改变。

package 第3章_操作符.第4节_赋值;

public class BasicDataTypeAssignment {
    public static void main(String[] args) {
        // 对基本类型使用a=b,那么就是把b的内容复制给a,若修改了a,那么b的内容还是不会发生改变。
        int a = 10;
        int b = 50;
        a = b; // 进行赋值操作
        System.out.println("a = " + a + " b = " + b);

        a = 100; // 修改a的值,打印发现b的值不变
        System.out.println("a = " + a + " b = " + b);
    }
}
/**
 * 打印结果:
 * a = 50 b = 50
 * a = 100 b = 50
 */
  • 对象的赋值

“将一个对象赋值给另一个对象”,实际是将“引用”从一个地方复制到另-一-个地方。这意味着假若对对象使用c=d,那么c和d都指向原本只有d指向的那个对象。

package 第3章_操作符.第4节_赋值;

public class ObjectTypeAssignment {
    public static void main(String[] args) {
        Tank t1 = new Tank();
        Tank t2 = new Tank();
        t1.level = 9;
        t2.level = 47;
        System.out.println("1: t1.level: " + t1.level + ", t2.level: " + t2.level);// 打印两个对象的值

        t1 = t2;// 把t2对象赋值给t1对象,那么t1也将指向t2所引用的对象(可能我们如此操作的本意是将t2.level的值赋给t1.level,即t1.level=t2.level)
        System.out.println("2: t1.level: " + t1.level + ", t2.level: " + t2.level);

        t1.level = 27;// 修改t1对象的属性值,但结果是两个对象的属性值都发生了变化
        System.out.println("3: t1.level: " + t1.level + ", t2.level: " + t2.level);
    }
}

class Tank {
    int level;
}
/**
 * 打印结果:
 * 1: t1.level: 9, t2.level: 47
 * 2: t1.level: 47, t2.level: 47
 * 3: t1.level: 27, t2.level: 27
 */

上面的代码很简单,大家应该都能看懂。但由于赋值操作的是一个对象的引用,那么修改t1的同时也将t2改变了。而我们原本希望它们是相互不受影响的。由于t1和t2包含的相同的应用,所以指向的是同一个对象。(原本t1包含的对对象的引用,是指向一个值为9的对象。在对t1赋值的时候,这个引用被覆盖,也就是丢失了﹔而那个不再被引用的对象会由“垃圾回收器”自动清理。)

上面的这种特殊现象称为“别名现象”,由将一个对象赋值给另一个对象引起的。

如果想要避免别名现象,即只是单纯的赋值而不是把对象复制过去,可以这样写:

t1.level=t2.level;

所以大家应该尽量注意不要为对象赋值。

3.4.1 方法调用中的别名问题

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

package 第3章_操作符.第4节_赋值.第1目_方法调用中的别名问题;

import static net.mindview.util.Print.print;

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

    public static void main(String[] args) {
        Letter x = new Letter(); // 实例化Letter对象
        x.c = 'a';// 为实例化对象的属性c赋值
        print("1: x.c: " + x.c);// 打印属性c的值

        f(x); // 调用f函数
        print("2: x.c: " + x.c);// 打印属性c的值,发现值被修改了
    }
}

class Letter {
    char c;
}
/**
 * 打印结果:
 * 1: x.c: a
 * 2: x.c: z
 */

在传递给方法f时,本意是复制参数Letter y作为一个副本,而不修改f()外面的对象,但实际上是传递的一个引用,所以y.c='z'修改了f()之外的对象。

总结:“别名问题”是由对象赋值给其他对象或作为方法参数进行操作而导致原对象发生改变的一种问题,简单的说,就是把对象b赋值给a,其中a修改自己对象中的属性时,那么b对象的属性必然也发生改变。

3.5 算术操作符

加号(+)、减号(-)、除号(/,直接去掉小数位,而不是四舍五入的结果,如7/4=1)、乘号(*)及取模运算符(%,从整数除法中产生余数,如7%3=4)。

可以使用运算符后紧跟等号这样的简化符号,如x+=4实质上为x=x+4。

package 第3章_操作符.第5节_算术操作符;

import java.util.Random;

import static net.mindview.util.Print.print;

public class MathOps {
    public static void main(String[] args) {
        Random rand = new Random(47);

        /* 使用整数进行测试 */
        int i, j, k;
        j = rand.nextInt(100) + 1;// 选择一个值从1到100
        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 = j * k;// 乘号(*)
        print("j * k : " + i);

        i = k % j;// 取模(%)
        print("k % j : " + i);

        j %= k;
        print("j %= k : " + j);


        /* 使用浮点数进行测试 */
        float u, v, w;
        v = rand.nextFloat();
        print("v : " + v);
        w = rand.nextFloat();
        print("w : " + w);

        u = v + w;
        print("v + w : " + u);

        u = v - w;
        print("v - w : " + u);

        u = v * w;
        print("v * w : " + u);

        u = v / w;
        print("v / w : " + u);


        /* 使用简化符号 */
        u += v;
        print("u += v : " + u);

        u -= v;
        print("u -= v : " + u);

        u *= v;
        print("u *= v : " + u);

        u /= v;
        print("u /= v : " + u);
    }
}
/**
 * 打印结果:
 * j : 59
 * k : 56
 * j + k : 115
 * j - k : 3
 * k / j : 0
 * j * k : 3304
 * k % j : 56
 * j %= k : 3
 * v : 0.5309454
 * w : 0.0534122
 * v + w : 0.5843576
 * v - w : 0.47753322
 * v * w : 0.028358962
 * v / w : 9.940527
 * u += v : 10.471473
 * u -= v : 9.940527
 * u *= v : 5.2778773
 * u /= v : 9.940527
 */

3.5.1 一元加、减操作符

一元加操作符就是正号(+),一元减操作符就是负号(-)。

例如:x=-a。编译器也能识别x=a*-b,但程序员可能就会遇到麻烦,所以写成x=a*(-b)更好。

一元减号的作用是转变数据的符号,一元加号的唯一作用是将较小类型的操作数提升为int类型。

package 第3章_操作符.第5节_算术操作符.第1目_一元加减操作符;

public class MonadicOperator {
    public static void main(String[] args) {
        // 1.一元减运算符,即负号(-)
        int a = -5;
        System.out.println(a);

        // 2.一元加运算符,即正号(+)
        // char b='A';
        // b = b + 1;// 会报错
        // System.out.println(b);

        char c = 'B';
        System.out.println(+c);// 使用一元加运算符把c由char类型提升为了int类型
    }
}
/**
 * 打印结果:
 * -5
 * 66
 */

报错的那部分提示为:

甚至+c+1会参与运算输出67。

3.6 自动递增和递减

自增运算符“++”指的是增加一个单位;自减运算符“--”指的是减少一个单位。

又分为前缀式和后缀式。前缀递增/递减表示操作符位于变量或表达式前面,如++a;后缀递增/递减表示操作符位于变量或表达式后面,如a++。

其中对于前缀式(如++a或--a)会先运算,再生成值;而后缀式(如a++或a--)会先生成值,再执行运算。

package 第3章_操作符.第6节_自动递增和递减;

import static net.mindview.util.Print.print;

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);
    }
}
/**
 * 打印结果:
 * i : 1
 * ++i : 2
 * i++ : 2
 * i : 3
 * --i : 2
 * i-- : 2
 * i : 1
 */

3.7 关系操作符

关系操作符生成的是一个boolean(布尔)结果,它们计算的是操作数的值之间的关系。如果关系是真实的,关系表达式会生成true(真〉﹔如果关系不真实,则生成false(假)。

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

因为boolean值只能为true或false,“大于”和“小于”没有实际意义(例如a=true,b=false,那么比较a>b,毫无意义)。

3.7.1 测试对象的等价性

关系操作符==和!=也适用于所有对象。

package 第3章_操作符.第7节_关系操作符.第1目_测试对象的等价性;

public class Equivalence {
    public static void main(String[] args) {
        Integer n1 = new Integer(47);
        Integer n2 = new Integer(47);
        System.out.println(n1 == n2);
        System.out.println(n1 != n2);
    }
}
/**
 * 打印结果:
 * false
 * true
 */

开始我也以为该输入true,然后是false,因为两个Integer对象是相同的。

但实际上只是对象的内容是相同的,对象的引用却是不同的,而==和!=比较的就是对象的引用。

而对于对象的实际内容的比较,需要用方法equals(),这个方法不适用于“基本类型”,基本类型直接使用==和!=即可。

package 第3章_操作符.第7节_关系操作符.第1目_测试对象的等价性;

public class EqualsMethod {
    public static void main(String[] args) {
        /* equals比较的是两个对象的值内容 */
        Integer n1 = new Integer(47);
        Integer n2 = new Integer(47);
        System.out.println(n1.equals(n2));
    }
}
/**
 * 打印结果:
 * true
 */

但事情不是如此简单,如果你自己创建了类,那么结果又不一样了

package 第3章_操作符.第7节_关系操作符.第1目_测试对象的等价性;

public class EqualMethod2 {
    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));
    }
}

class Value {
    int i;
}
/**
 * 打印结果:
 * false
 */

为什么,结果又变成了false呢,这是由于equals()的默认行为是比较引用,所以除非在自己的新类中覆盖equals()方法,否则比较的仍然是引用,而不是我们希望的内容。

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

查看Integer的源码,就可以看到实现了equals()方法,覆盖了原有的。

3.8 逻辑操作符

逻辑操作符有:与(&&)、或(||)、非(!),生成一个布尔值(true或false)。

package 第3章_操作符.第8节_逻辑操作符;

import java.util.Random;

import static net.mindview.util.Print.print;

public class Bool {
    public static void main(String[] args) {
        Random rand = new Random(47);
        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)));
    }
}
/**
 * 打印结果:
 * i = 58
 * j = 55
 * i > j is true
 * i < j is false
 * i >= j is true
 * i <= j is false
 * i == j is false
 * i != j is true
 * (i < 10) && (j < 10) is false
 * (i < 10) || (j < 10) is false
 */

“与”表示当且仅当a和b都是true时才返回true;

“或”表示只要a或b任意一个是true就返回true;

“非”表示如果a是true,那么!a就是false,反之如果a是false,那么!a就是true。

运算符用法含义说明实例结果
&&a&&b短路与ab 全为 true 时,计算结果为 true,否则为 false。2>1&&3<4true
||a||b短路或ab 全为 false 时,计算结果为 false,否则为 true。2<1||3>4false
!!a逻辑非a 为 true 时,值为 false,a 为 false 时,值为 true!(2>4)true

3.8.1 短路

当使用逻辑操作符时,我们会遇到一种“短路”现象。即一旦能够明确无误地确定整个表达式的值,就不再计算表达式余下部分了。因此,整个逻辑表达式靠后的部分有可能不会被运算。

package 第3章_操作符.第8节_逻辑操作符.第1目_短路;

import static net.mindview.util.Print.print;

public class ShortCircuit {
    static boolean test1(int val) {
        print("test1(" + val + ")");
        print("result: " + (val < 1));
        return val < 1;
    }

    static boolean test2(int val) {
        print("test2(" + val + ")");
        print("result: " + (val < 2));
        return val < 2;
    }

    static boolean test3(int val) {
        print("test3(" + val + ")");
        print("result: " + (val < 3));
        return val < 3;
    }

    public static void main(String[] args) {
        boolean b = test1(0) && test1(2) && test3(2);
        print("expression is " + b);
    }
}
/**
 * 打印结果:
 * test1(0)
 * result: true
 * test1(2)
 * result: false
 * expression is false
 */

我们以为所有的这个三个测试都会执行,但并非如此,第一个表达式为true,所以会继续计算下去,而第二个测试产生了false,那么意味着这个表达式一定为false,所以没有必要计算剩余的表达式,这就是短路现象。

事实上,如果逻辑表达式发生短路现象,那么可能会获得潜在的性能提升。

3.9 直接常量

常量是指在程序运行过程中其值不能改变的量。常量分为直接常量和符号常量。

直接常量分为整型常量、实型常量、字符常量和字符串常量。

通常把整型常量和实型常量合称为数值常量。整型常量就是常整数,有十进制、八进制、十六进制三种表示形式。需要注意的是,八进制常整数在书写时以数字0作前缀;十六进制以0x作前缀。实型常量只采用十进制小数形式和指数形式表示,而不是八进制和十六进制形式表式。字符型常量字符型常量必须用单引号括起来。可以使用控制符、单引号、双引号、反斜线等。字符串常量用双引号括起来。符号常量是用标示符代表一个常量,使用之前必须定义。

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

若为大写(或小写)的L,代表long(但是,使用小写字母l容易造成混淆,因为它看起来很像数字1)。大写(或小写)字母F,代表float,大写(或小写)字母D,则代表double。

十六进制数适用于所有整数数据类型,以前缀0x(或0X),后面跟随0-9或小写(或大写)的a-f来表示。如果试图将一个变量初始化成超出自身表示范围的值(无论这个值的数值形式如何),编译器都会向我们报告一条错误信息。注意在前面的代码中,已经给出了char、byte以及short所能表示的最大的十六进制值。如果超出范围,编译器会将值自动转换成int型,并告诉我们需要对这次赋值进行“窄化转型”这样我们就可清楚地知道自己的操作是否越界了。

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

在C、C++或者Java中,二进制数没有直接常量表示方法。但是,在使用十六进制和八进制记数法时,以二进制形式显示结果将非常有用。通过使用Integer和Long类的静态方法toBinaryString()可以很容易地实现这一点。请注意,如果将比较小的类型传递给Integer.toBinaryString0方法,则该类型将自动被转换为int。

package 第3章_操作符.第9节_直接常量;

import static net.mindview.util.Print.print;

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;
    }
}
/**
 * 打印结果:
 * i1: 101111
 * i2: 101111
 * i3: 1111111
 * c: 1111111111111111
 * b: 1111111
 * s: 111111111111111
 */

3.9.1 指数记数法

Java采用e来表示指数。

package 第3章_操作符.第9节_直接常量.第1目_指数记数法;

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

        double expDouble = 47e47d;
        double expDouble2 = 47e47;
        System.out.println(expDouble);
    }
}
/**
 * 打印结果:
 * 1.39E-43
 * 4.7E48
 */
  • 在自然科学领域中,"e"代表自然对数的基数,约等于2.718。例如1.39xe^(-43)这样的指数代表着1.39x2.718^(-43)。
  • 在如C、Java等编程语言中,“e"代表10的幂次,如1.39e^(-43)代表着1.39x10^(-43)。

如果编译器能够正确地识别类型,就不必在数值后附加字符,如long n3=200。

通常编译器会将指数作为双精度(double)来处理,如果没有附加字符f,就会报错,例如float f4=1e-43f。看下面的图可能会有更明确的认识

3.10 按位操作符

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

  • 按位与(&)

操作数1

0

0

1

1

操作数2

0

1

0

1

按位与

0

0

0

1

规则总结:只有两个操作数对应位同为1时,结果为1,其余全为0. (或者是只要有一个操作数为0,结果就为0)。

  • 按位或(|)

操作数1

0

0

1

1

操作数2

0

1

0

1

按位或

0

1

1

1

规则总结:只有两个操作数对应位同为0时,结果为0,其余全为1.(或者是只要有一个操作数为1,结果就为1)。

  • 按位异或(^)

操作数1

0

0

1

1

操作数2

0

1

0

1

按位异或

0

1

1

0

规则总结:相异为1,相同为0。

  • 按位非(~)

操作数

0

1

按位或

1

0

规则总结:取反即可,0取1,1取0。

按位操作符可与等号(=)联合使用,以便合并运算和赋值:&=、=和^=都是合法的(由于“~”是一元操作符,所以不可与“=”联合使用)。

package 第3章_操作符.第10节_按位操作符;

public class OperatorCharacterByBit {
    public static void main(String[] args) {
        int a = 4;
        int b = 6;
        System.out.println("a: " + Integer.toBinaryString(a));
        System.out.println("b: " + Integer.toBinaryString(b));

        // 1.按位与(&)
        System.out.println("a&b: " + Integer.toBinaryString(a & b));

        // 2.按位或(|)
        System.out.println("a|b: " + Integer.toBinaryString(a | b));

        // 3.按位异或(^)
        System.out.println("a^b: " + Integer.toBinaryString(a ^ b));

        // 4.按位非(~)
        System.out.println("~a: " + Integer.toBinaryString(~a));
    }
}
/**
 * 打印结果:
 * a: 100
 * b: 110
 * a&b: 100
 * a|b: 110
 * a^b: 10
 * ~a: 11111111111111111111111111111011
 */

3.11 移位操作符

移位操作符操作的运算对象也是二进制的“位”。移位操作符只可用来处理整数类型(基本类型的一种)。

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

“有符号”右移位操作符(>>)则按照操作符右侧指定的位数将操作符左边的操作数向右移动。“有符号”右移位操作符使用“符号扩展”。若符号为正,则在高位插入0,若符号为负,则在高位插入1。

Java中增加了一种“无符号”右移位操作符(>>>),它使用“零扩展”:无论正负,都在高位插入0。这一操作符是C或C++中所没有的。

package 第3章_操作符.第11节_移位操作符;

public class MoveBit {
    public static void main(String[] args) {
        int a = 9;
        int b = -4;
        System.out.println("a: " + Integer.toBinaryString(a));
        System.out.println("b: " + Integer.toBinaryString(b));

        // 1.左位移(<<): 符号位不变,低位补0。
        System.out.println("a<<1: " + Integer.toBinaryString(a << 1));
        System.out.println("b<<1: " + Integer.toBinaryString(b << 1));

        // 2.右位移(>>):低位溢出,符号位不变,并用符号位补溢出的高位。
        System.out.println("a>>1: " + Integer.toBinaryString(a >> 1));
        System.out.println("b>>1: " + Integer.toBinaryString(b >> 1));

        // 3.无符号右移(>>>):低位溢出,高位补0。
        System.out.println("a>>>1: " + Integer.toBinaryString(a >>> 1));
        System.out.println("b>>>1: " + Integer.toBinaryString(b >>> 1));
    }
}
/**
 * 打印结果:
 * a: 1001
 * b: 11111111111111111111111111111100
 * a<<1: 10010
 * b<<1: 11111111111111111111111111111000
 * a>>1: 100
 * b>>1: 11111111111111111111111111111110
 * a>>>1: 100
 * b>>>1: 1111111111111111111111111111110
 */

总结

  • 左位移(<<): 符号位不变,低位补0。如:2<<2结果为8。
  • 右位移(>>):低位溢出,符号位不变,并用符号位补溢出的高位。如:-6>>2结果为-2。

  • 无符号右移(>>>):低位溢出,高位补0。注意,无符号右移(>>>)中的符号位(最高位)也跟着变,无符号的意思是将符号位当作数字位看待。如:-1>>>1结果为2147483647。

如果对char、byte或者short类型的数值进行移位处理,那么在移位进行之前,它们会被转换为int类型,并且得到的结果也是一个int类型的值。只有数值右端的低5位才有用。这样可防止我们移位超过int型值所具有的位数。(译注:因为2的5次方为32,而int型值只有32位。)若对一个long类型的数值进行处理,最后得到的结果也是long。此时只会用到数值右端的低6位,以防止移位超过long型数值具有的位数。

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

package 第3章_操作符.第11节_移位操作符;

public class URShift {
    public static void main(String[] args) {
        int i = -1;
        System.out.println(Integer.toBinaryString(i));
        i >>>= 10;
        System.out.println(Integer.toBinaryString(i));

        long l = -1;
        System.out.println(Long.toBinaryString(l));
        l >>>= 10;
        System.out.println(Long.toBinaryString(l));

        short s = -1;
        System.out.println(Integer.toBinaryString(s));
        s >>>= 10;
        System.out.println(Integer.toBinaryString(s));

        byte b = -1;
        System.out.println(Integer.toBinaryString(b));
        b >>>= 10;
        System.out.println(Integer.toBinaryString(b));

        b = -1;
        System.out.println(Integer.toBinaryString(b));
        System.out.println(Integer.toBinaryString(b >>> 10));
    }
}
/**
 * 打印结果:
 * 11111111111111111111111111111111
 * 1111111111111111111111
 * 1111111111111111111111111111111111111111111111111111111111111111
 * 111111111111111111111111111111111111111111111111111111
 * 11111111111111111111111111111111
 * 11111111111111111111111111111111
 * 11111111111111111111111111111111
 * 11111111111111111111111111111111
 * 11111111111111111111111111111111
 * 1111111111111111111111
 */

3.12 三元操作符if-else

三元操作符也称为条件操作符,它显得比较特别,因为它有三个操作数,但它确实属于操作符的一种,因为它最终也会生成一个值,这与普通if-else语句是不同的。

表达式格式:

boolean-exp ? value0 : value1

解释:

  • 如果boolean-exp(布尔表达式)的结果为true,就计算value0,而且这个计算结果也就是操作符最终产生的值。

  • 如果boolean-exp的结果为false,就计算value1,同样,它的结果也就成为了操作符最终产生的值。

优缺点:三元操作符简洁高效,但可读性不佳。

与if-else的区别:三元操作符会产生一个值,而if-else不会。

package 第3章_操作符.第12节_三元操作符;

import static net.mindview.util.Print.print;

public class TernaryIfElse {
    // 1.使用三元操作符,代码更加简洁
    static int ternary(int i) {
        return i < 10 ? i * 100 : i * 10;
    }

    // 2.使用普通的if-else,代码可读性更强
    static int standardIfElse(int i) {
        if (i < 10) {
            return i * 100;
        } else {
            return i * 10;
        }
    }

    public static void main(String[] args) {
        print(ternary(9));
        print(ternary(10));
        print(standardIfElse(9));
        print(standardIfElse(10));
    }
}
/**
 * 打印结果:
 * 900
 * 100
 * 900
 * 100
 */

3.13 字符串操作符+和+=

字符串操作符在Java中用于连接不同的字符串。

如果一个表达式以字符串开头,那么后续所有操作都必须是字符串类型。

package 第3章_操作符.第13节_字符串操作符;

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);
        System.out.println(x + " " + s);// x会转换成字符串输出

        s += "(summed) = "; // +=字符串操作符
        System.out.println(s + (x + y + z));

        System.out.println("" + x);// 等价于Integer.toString()
//        System.out.println(Integer.toString(x));
    }
}
/**
 * 打印结果:
 * x, y, z 012
 * 0 x, y, z
 * x, y, z (summed) = 3
 * 0
 */

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

Java中有一个与C和C++中类似的问题,即使用按位“与”和按位“或”代替逻辑“与”和逻辑“或”。按位“与”和按位“或”使用单字符(&或l),而逻辑“与”和逻辑“或”使用双字符(&&或1I)。就像“=”和“==”一样,键入一个字符当然要比键入两个简单。Java编译器可防止这个错误发生,因为它不允许我们随便把一种类型当作另一种类型来用。

3.15 类型转换操作符

Java可以将一种数据类型自动转换成另一种数据类型。

例如为一个浮点变量赋予一个整数值,编译器会将int类型自动转换成float。

类型转换符允许我们显示地进行这种类型的转换,或者在不能自动进行转换的时候强制进行类型转换。

要想执行类型转换,需要将希望得到的数据类型置于圆括号内,放在要进行类型转换的值的左边,例如:

package 第3章_操作符.第15节_类型转换操作符;

public class Casting {
    public static void main(String[] args) {
        int i = 200;
        System.out.println("i: " + getType(i));
        long lng = (long) i;// 将int类型转换成long类型
        System.out.println("lng: " + getType(lng));
        lng = i;// 也会将int类型转换成long类型
        System.out.println("lng: " + getType(lng));

        long lng2 = (long) 200;// 也可以直接对数值进行类型转换
        System.out.println("lng2: " + getType(lng2));
        lng2 = 200;// 也会直接将int类型的数值转换成long类型
        System.out.println("lng2: " + getType(lng2));
        i = (int) lng2;// 将long类型转换成int类型
        System.out.println("i: " + getType(i));
    }

    public static String getType(Object o) { //获取变量类型方法
        return o.getClass().toString(); //使用int类型的getClass()方法
    }
}
/**
 * 打印结果:
 * i: class java.lang.Integer
 * lng: class java.lang.Long
 * lng: class java.lang.Long
 * lng2: class java.lang.Long
 * lng2: class java.lang.Long
 * i: class java.lang.Integer
 */

既可以对数值进行类型转换,也可以对变量进行类型转换。

Java允许我们把任何基本数据类型转换成别的基本数据类型,但布尔型除外,后者根本不允许进行任何类型的转换处理。“类”数据类型不允许进行类型转换。为了将一种类转换成另一种,必须采用特殊的方法。

3.15.1 截尾和舍入

如将一个浮点数转换成一个整数值时,就会面对截尾和舍入问题,那么Java如何处理的呢?

package 第3章_操作符.第15节_类型转换操作符.第1目_截尾和舍入;

public class CastingNumbers {
    public static void main(String[] args) {
        double above = 1.7, below = 1.4;
        float fabove = 0.7f, fbelow = 0.4f;
        System.out.println("(int)above: " + (int) above);
        System.out.println("(int)below: " + (int) below);
        System.out.println("(int)fabove: " + (int) fabove);
        System.out.println("(int)fbelow: " + (int) fbelow);
    }
}
/**
 * 打印结果:
 * (int)above: 1
 * (int)below: 1
 * (int)fabove: 0
 * (int)fbelow: 0
 */

即Java将float或double转型为int整型数值时,总是对该数字执行截尾,即11.7取11,23.5取23。

如果要得到舍入的结果,那么就需要使用java.lang.Math中的round()方法。

package 第3章_操作符.第15节_类型转换操作符.第1目_截尾和舍入;

public class RoundingNumbers {
    public static void main(String[] args) {
        double above = 1.7, below = 1.4;
        float fabove = 0.7f, fbelow = 0.4f;
        System.out.println("Math.round(above): " + Math.round(above));
        System.out.println("Math.round(below): " + Math.round(below));
        System.out.println("Math.round(fabove): " + Math.round(fabove));
        System.out.println("Math.round(fbelow): " + Math.round(fbelow));
    }
}
/**
 * 打印结果:
 * Math.round(above): 2
 * Math.round(below): 1
 * Math.round(fabove): 1
 * Math.round(fbelow): 0
 */

round()方法即四舍五入,如round(0.7)是1,而round(0.4)是0。

round()是java.lang的一部分,不需要额外导入。

3.15.2 提升

如果对基本数据类型执行算术运算或按位运算,大家会发现,只要类型比int小(即char,byte或者short),那么在运算之前,这些值会自动转换成int。这样一来,最终生成的结果就是int类型。

如果想把结果赋值给较小的类型,就必须使用类型转换(既然把结果赋给了较小的类型,就可能出现信息丢失)。通常,表达式中出现的最大的数据类型决定了表达式最终结果的数据类型。

如果将一个float值与一个double值相乘,结果就是double,如果将一个int和一个long值相加,则结果为long。

package 第3章_操作符.第15节_类型转换操作符.第2目_提升;

public class Imporve {
    public static void main(String[] args) {
        // 如果将一个float值与一个double值相乘,结果就是double
        float a = 10.5f;
        double b = 0.2d;
        System.out.println("a: " + getType(a));
        System.out.println("b: " + getType(b));
        System.out.println("a*b: " + getType(a * b));

        // 如果将一个int和一个long值相加,则结果为long
        int c = 5;
        long d = 15L;
        System.out.println("c: " + getType(c));
        System.out.println("d: " + getType(d));
        System.out.println("c+d: " + getType(c + d));
    }

    public static String getType(Object o) { //获取变量类型方法
        return o.getClass().toString(); //使用int类型的getClass()方法
    }
}
/**
 * 打印结果:
 * a: class java.lang.Float
 * b: class java.lang.Double
 * a*b: class java.lang.Double
 * c: class java.lang.Integer
 * d: class java.lang.Long
 * c+d: class java.lang.Long
 */

3.16 Java没有sizeof

在C和C++中,sizeof)操作符可以告诉你为数据项分配的字节数。在C和C++中,需要使用sizeof(的最大原因是为了“移植”。不同的数据类型在不同的机器上可能有不同的大小,所以在进行一些与存储空间有关的运算时,程序员必须获悉那些类型具体有多大。例如,一台计算机可用32位来保存整数,而另一台只用16位保存。显然,在第一-台机器中,程序可保存更大的值。可以想像,移植是令C和C++程序员颇为头痛的一个问题。

Java不需要sizeofO操作符来满足这方面的需要,因为所有数据类型在所有机器中的大小都是相同的。我们不必考虑移植问题—它已经被设计在语言中了。

package 第3章_操作符.第16节_Java没有sizeof;

public class JavaSizeOf {
    public static void main(String[] args) {
        // char类型
        System.out.println("char size: " + Character.BYTES + "Byte");

        // int类型
        System.out.println("int size: " + Integer.BYTES + "Byte");

        // short类型
        System.out.println("short size: " + Short.BYTES + "Byte");

        // long类型
        System.out.println("long size: " + Long.BYTES + "Byte");

        // byte类型
        System.out.println("byte size: " + Byte.BYTES + "Byte");

        // float类型
        System.out.println("float size: " + Float.BYTES + "Byte");

        // double类型
        System.out.println("double size: " + Double.BYTES + "Byte");
    }
}
/**
 * 打印结果:
 * char size: 2Byte
 * int size: 4Byte
 * short size: 2Byte
 * long size: 8Byte
 * byte size: 1Byte
 * float size: 4Byte
 * double size: 8Byte
 * boolean size: boolean
 */

注意:boolean类型占用一位二进制位数。

以上是关于《Java编程思想》读书笔记之第3章-操作符的主要内容,如果未能解决你的问题,请参考以下文章

《Java编程思想》读书笔记之第8章-多态

《Java编程思想》阅读笔记之第4章-控制执行流程

[读书笔记]Java编程思想

《Java编程思想》阅读笔记之第11章-持有对象

Java编程思想读书笔记--第14章类型信息

<java编程思想>第一章读书笔记二