类型、变量与运算符

hardwork
深入虚拟机去看待Java语法

前面的话

此文的编写参考了较多经典文献:《Java Rules》、《Java Language Specification 7》、《Java Virtual Machine Specification 7》、《Core Java》、《Java JDK8学习笔记》等,所以比较权威,不过鄙人才疏学浅,出错在所难免,望指出。

  • JVM:Java虚拟机

类型

Java是强类型编程语言,有助于在编译时检测错误。

JVM可以操作的数据类型可分为两类:

  • 原始类型(Primitive Types)
  • 引用类型(Reference Types)

与之对应的值有:

  • 原始值(Primitive Values)
  • 引用值(Reference Values)

值可用于变量赋值、参数传递、方法返回和运算操作。

原始类型与值

JVM所支持的原始数据类型包括:

  • 数值类型(Numeric Types)
  • 布尔类型(Boolean Types)
  • returnAddress类型

其中,数值类型又区为整数类型(Integral Types)和浮点类型(Floating-Point Types)。

整数类型

  • byte:值为8位有符号二进制补码,默认为0,1字节,-128~127
  • short:值为16位有符号二进制补码,默认为0,2字节,-32768~32767
  • int:值为32位有符号二进制补码,默认为0,4字节,-2147483648~2147483647
  • long:值为64位有符号二进制补码,默认为0,8字节
  • char:值为16位无符号整数表示的,指向BMP(Basic Multilingual Plane)的Unicode值,以UTF-16编码(在Java SE8中采用Unicode 6.2.0编码,JVM采用UTF-16 Big Endian编码),默认值为Unicode的null值(‘\u0000’)。

长整型数值有一个后缀l/L(如:1000000000000L)
十六进制数值有个前缀0x/0X(如:0xCAFE)
八进制有一个0,(如:010)
【从Java7开始】二进制有个0b/0B,(如:0b110)

浮点类型

  • float:值为单精度浮点数集合中的元素,或者(如果JVM支持的话)是单精度扩展指数(Float-Extended-Exponent)集合中的元素,默认值为整数0,4字节
  • double:值为双精度浮点数集合中的元素,或者(如果JVM支持的话)是双精度扩展指数(Double-Extended-Exponent)集合中的元素,默认值为整数0,8字节

float类型的数值有一个后缀f/F(如3.14f)。没有后缀的浮点数值(如3.14)默认为double类型。也可以在double类型添加后缀d/D(如3.14d)。

浮点数值的表示和计算都遵循IEEE 754规范,如有需要请参阅IEEE 754规范。不过JVM中定义的单精度扩展指数集合和双精度扩展指数集合中的元素和IEEE 754标准里面的单精度扩展格式和双精度扩展格式的表示并不完全一致。不过除了Class文件格式中必要的浮点数表示描述以外,并不特别要求表示浮点数值表示形式。

flaot在内存中的存储方式
float

溢出和出错情况的三个特殊浮点数值:

  • Infinity(正无穷大)
  • -Infinity(负无穷大)
  • NaN(Not-a-Number)

例如,1/0.0结果为Infinity,0/0.0或负数的平方根为NaN。

除了NaN以外,浮点数集合中的所有元素都是有序的,顺序是:负无穷、可数负数、正负零、可数正数、正无穷。

浮点数中,正负零数值相等,但是它们的运算有一定区别,例如:1.0除以0.0结果是无穷大,而1.0除以-0.0结果是负无穷大。

NaN是无序的,对它进行任何数值的比较和等值测试都会得到false。值得一提的是,有且仅有NaN一个数与其自身比较是否相等时会得到false,任何数值与NaN进行非等值比较都会返回true(等值比较返回false)。

布尔类型

  • boolean:取值范围为true和false,默认值为false。

returnAddress

  • returnAddress:表示一条字节码指令的操作码(Opcode)。在所有的虚拟机支持的原始类型中,只有returnAddress类型不能直接被Java语言的数据类型对应。

returnAddress类型会被JVM虚拟机的jsr、ret和jsr_指令所使用。这几条指令以前主要用来实现finally语句块,后来改为冗余finally块代码的方式来实现,甚至到了Java7时,虚拟机不允许Class文件内出现这几条指令。那相应地,returnAddress类型就处于名存实亡的状态。

引用类型和值

JVM中有三种引用类型:

  • 类类型(Class Types)
  • 数组类型(Array Types)
  • 接口类型(Interface Types)

这些引用类型的值分别由类实例、数组实例和实现了某个接口的类实例或数组实例动态创建。

其中,数组类型还包括一个单一维度(即长度不由其类型决定)的组件类型(Component Type),一个数组的组件类型也可以是数组。但从任意一个数组开始,如果发现其组件类型也是数组类型的话,继续重复取这个数组的组件类型,这样操作不断执行,最终一定可以遇到组建类型不是数组的情况,这时就把这种类型称为数组类型的元素类型(Element Type)。数组的元素类型不许是原始数组、类类型或者接口类型中的一种。

在引用类型中还有一个特殊的值:null。当一个引用不指向任何对象的时候,它的值就用null来表示。一个为null的引用,在没有上下文的情况下不具备任何实际的类型,但是有具有上下文时它可转型为任意的引用类型。引用类型的默认值就是null。

Java虚拟机规范并没有规定null在虚拟机中应当怎样编码表示

变量

Java中每一个变量都属于一种数据类型。

变量名必须是以字母开头的由字母或数字构成的序列。

字母包括’A’~’Z’、’a’~’z’、’_’、’$’或在某种语言中代表字母的任何Unicode字符(包括汉字)。如希腊人可以用π,德国人可以用α等。
数字包括’0’~’9’和在某种语言中代表数字的任何Unicode字符。但’+’和’®’这样的符号不能出现在变量中,空格也不行。
变量名中所有的字符都是有意义的,并且大小写敏感,长度一般没有限制。

Tips:

  • 如果想知道哪些Unicode字符属于Java中的字母,可以使用Character类的isJavaIdentifierStart和isJavaIdentifierPart方法进行检测。
  • 尽管$是合法的标识符,但不要在自己写的代码中使用,它只用在Java编译器或其它工具生成的名字中。
  • 变量名命名不能为Java保留字(关键字是保留字的子集)。

常量

在Java中,利用关键字final来修饰常量。

关键字final表示这个变量只能被赋值一次,一旦被赋值之后就不能再变更,否则会出现编译错误。

类常量

Java中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。
可以使用static final设置一个类常量,示例如下:

public class Constants2
{
    public static final CM_PER_INCH = 2.45;
    public static main(String[] args)
    {
        double paperWidth = 8.5;
        double paperHeight = 11;
        System.out.println("Paper size in centimeters:" +
            + paperWidth * CM_PER_INCH + " by " +
            paperHeight * CM_PER_INCH);
    }
}

字面常量

字面常量(Literal Constant)是原始类型、String类型或空类型的值的源代码表示。
包括如下:

  • IntegerLiteral
  • FloatingLiteral
  • BooleanLiteral
  • CharacterLiteral
  • StringLiteral
  • NullLiteral

整数字面常量表示:

int n1 = 1;     //十进制
int n2 = 0b1;    //二进制(Java7 later)
int n3 = 01;    //八进制
int n4 = 0x1;    //十六进制

浮点数字面常量表示

float f = 0.00123f;
double d = 1.23e-3;

字符字面常量表示

char c1 = 'a';
char c2 = '\"';
char c3 = '\u0000';

数字常量表示法

Java7之后,撰写整数或浮点数常量时可以使用下划线更清楚地表示某些数字。

如:

int n1 = 1234_5678;
int n2 = 0b1010_1010_1010;
double d = 3.141_592_653;

运算符

整数运算

Java编程语言提供了很多作用于整数的运算符:

  • 比较运算符,产生boolean类型的值:
    • 数值比较运算符:<<=>>=
    • 数值相等性运算符:==!=
  • 数值运算符,产生int或long类型的值:
    • 一元加、减运算符:+-
    • 乘法运算符:`/%`*
    • 加法运算符:+-
    • 递增运算符:++ ,同为前缀和后缀
    • 递减运算符:-- ,同为前缀和后缀
    • 带符号和无符号移位运算符:<<>>>>>
    • 逐位求补运算符:~
    • 整数逐位运算符:&|^
  • 条件运算符:?:
  • 强制转换运算符,可以把一个整形值转换成任何指定数值类型的值
  • 字符串串接运算符:+ 当给定一个String操作数和一个整型操作数时,该运算符将把整型操作数转换成以十进制形式表示其值的String,然后产生一个最新创建的String,它是两个字符串的串接。
  • 类Byte、Short、Integer、Long和Character中预定义了其它有用的构造函数、方法和常量

数值提升

如果一个二元运算符(而不是一个移位运算符)具有至少一个long类型的操作数,那么,将使用64位精度执行计算,并且数值运算符的结果是long类型。

如果另一个操作数不是long类型,则首先会通过数值提升将其加宽到类型long。否则将使用32位的精度执行运算,并且数值运算符的结果是int类型。

如果任一个操作数不是int类型,则首先会通过数值提升将其加宽到int类型。

数值溢出与计算异常

内置的整型运算符不会以任何方式指示上溢或下溢。

如果需要空引用的拆箱转换,则整型运算符会抛出NullPointerException。

此外,可以抛出一个异常的惟一整型运算符是:整型除法运算符/和整型求余运算符%,如果右边的操作数为0,那么它们会抛出一个ArithmeticException

递增和递减运算符++--,如果需要装箱转换且没有足够内存可用于该转换,那么它们将会抛出一个OutOfMemoryError。

示例:

public class Test {
    public static void main(String[] args) {
        int i = 1000000;
        System.out.println(i * i);
        long l = i;
        System.out.println(l * l);
        System.out.println(20296 / (l - i));
    }
}

产生如下输出:

-727379968
1000000000000

并且随后会由于l-i而在除法运算中遇到一个ArithmeticException,因为l-i等于0。第一个乘法是以32位精度执行的,而第二个乘法是一个long乘法。值-727379968是数学结果1000000000000的低32位十进制,1000000000000对于int是一个过大的值,出现上溢。

任何整型的任何值都可以被强制转换成任何数值类型,反之亦然。在整型和boolean型之间没有强制转换。

浮点运算

Java编程语言提供了很多作用于浮点数的运算符:

  • 比较运算符,产生boolean类型的值:
    • 数值比较运算符:<<=>>=
    • 数值相等性运算符:==!=
  • 数值运算符,产生float或double类型的值:
    • 一元加、减运算符:+-
    • 乘法运算符:`/%`*
    • 加法运算符:+-
    • 递增运算符:++ ,同为前缀和后缀
    • 递减运算符:-- ,同为前缀和后缀
    • 带符号和无符号移位运算符:<<>>>>>
    • 逐位求补运算符:~
    • 整数逐位运算符:&|^
  • 条件运算符:?:
  • 强制转换运算符,可以把一个整形值转换成任何指定数值类型的值
  • 字符串串接运算符:+ 当给定一个String操作数和一个浮点操作数时,该运算符将把浮点操作数转换成以十进制形式表示其值的String(不会丢失信息),然后产生一个最新创建的String,它是两个字符串的串接。
  • 类Float、Double和Math中预定义了其它有用的构造函数、方法和常量

数值提升

如果一个二元运算符(而不是一个移位运算符)至少有一个操作数时浮点型,那么该运算就是一个浮点运算,即使另一个操作数是整数也是如此。

如果数值运算符两侧的操作数至少有一个是double类型,那么将使用64位浮点运算执行该运算,并且数值运算符的结果是double类型的值(如果另一个数不是double类型,则首先会通过数值提升将其加宽到double类型。否则,将使用32位浮点运算执行该运算,并且数值运算符的结果是float类型。如果另一个操作符不是float类型,则会首先通过数值提升将其加宽到float类型。

如果需要空引用的拆箱转换,则浮点运算符会抛出NullPointerException。递增和递减运算符++--,如果需要装箱转换且没有足够内存可用于该转换,那么它们将会抛出一个OutOfMemoryError。

上溢运算会产生一个带符号的无穷大,下溢运算会产生一个非规范的值或者一个带符号的0,不具有数学限定的结果会产生一个NaN。把NaN作为一个操作数的所有数值运算都会产生一个NaN的结果。

Java需要支持IEEE 754非规范化的浮点数和逐渐下溢(gradual underflow),这使得证明人们期待的特殊值算法的性质变得更容易,如果计算结果是一个非规范数,则浮点运算不会刷新为0(flush to zero)。Java要求浮点运算按如下方式工作:就像每个浮点运算符将其浮点运算结果四舍五入为结果精度一样。不精确的结果必须被四舍五入为最接近无限精确结果的有代表性的值;如果两个最接近的有代表性的值近似相等,那么将会选择其最低有效位为0的那个值。这是IEEE 754标准默认的四舍五入方式,称为四舍五入至最接近的值。当把浮点值转换为整数时,Java使用四舍五入到0,在这种情况下就好像把数截断一样,丢弃尾数位。选择四舍五入到0,可以使用这种格式的值在数量上最接近于并且不大于无限精确的结果。

示例:

public class Test {
    public static void main(String[] args) {
        // An example of overflow:
        double d = 1e308;
        System.out.print("overflow produces infinity:");
        System.out.println(d + "*10==" + d*10);
        // an example of gradual underflow:
        d = 1e-305 * Math.PI;
        System.out.println("gradual underflow:" + d);
        for (int i = 0; i < 4; i++) {
            System.out.print("  " + (d /= 100000));
        }
        System.out.println();
        // An example of NaN:
        System.out.println("0.0 / 0.0 == " + 0.0/0.0);
        // An example of inexact results and rounding:
        System.out.println("inexact results with float:");
        for (int i = 0; i < 100; i++) {
            float z = 1.0f / i;
            if (z * i != 1.0f) {
                System.out.print("  " + i);
            }
        }
        System.out.println();
        // Another example of inexact result and rounding:
        System.out.println("inexact result with double:");
        for (int i = 0; i < 100; i++) {
            double z = 1.0 / i;
            if (z * i != 1.0)
                System.out.print("  " +i);
        }
        System.out.println();
        //An example of cast to integer rounding:
        System.out.print("cast to int rounds toward 0:");
        d = 12345.6;
        System.out.println((int)d + " " + (int)-d);
    }
}

产生结果如下:

overflow produces infinity:1.0E308*10==Infinity
gradual underflow:3.141592653589793E-305
  3.1415926535898E-310  3.141592653E-315  3.142E-320  0.0
0.0 / 0.0 == NaN
inexact results with float:
  0  41  47  55  61  82  83  94  97
inexact result with double:
  0  49  98
cast to int rounds toward 0:12345 -12345

值得一提的是,这个示例证实逐渐下溢可能导致精度逐渐损失。
当i为0时,结果涉及到除以0,因此z变为正无穷大,并且z*0是NaN,它不等于1.0。

strictfp关键字

可移植性是Java的设计目标之一。无论在哪个虚拟机上运行,同一运算应该得到同样的结果。对于浮点数的算数运算,实现这样的可移植性是相当困难的。double类型使用64位存储一个double数值,而有些处理器使用80位浮点寄存器。这些寄存器增加了中间过程的计算精度。例如,下列运算:

double w = x * y / z;

很多Intel处理器计算x * y,并且将结果存储在80位寄存器中,再
除以z并将结果截断为64位。这样可以得到一个更加精确的计算结果,并且还能够避免产生指数溢出。但是,这个结果可能与始终在64位机器上计算的结果不一样。因此JVM的最初规范规定所有中间计算都必须进行截断。这种行为遭到了数值计算团体的反对。截断计算不仅可能导致溢出,而且由于阶段操作需要消耗时间,所以在计算速度上要比精确计算慢。为此Java承认了最优性能和理想结果之间存在的冲突,并给予了改进。在默认情况下,虚拟机设计者允许将中间结果采用扩展的精度。但是,对于使用strictfp关键字标记的方法必须使用严格的浮点计算来产生理想的结果。例如,可以把main方法标记为

public static strictfp void main(String[] args)

于是,在main方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为strictfp,这个类中所有方法都要使用严格的浮点计算。

实际的计算方式将取决于Intel处理器。在默认情况下,中间结果允许使用扩展的指数,但不允许使用扩展的尾数(Intel芯片在截断尾数时并不损失性能)。因此这两种方式的区别仅仅在于采用默认的方式不会产生溢出,而采用严格的计算有可能产生溢出。

布尔类型的运算符

布尔运算符有:

  • 关系运算符==!=
  • 逻辑求补运算符!
  • 逻辑运算符&^!
  • “条件与”运算符&&和“条件或”运算符||
  • 条件运算符?:
  • 字符串串接运算符+,当给定一个String操作数和一个布尔操作数时,该运算符将把布尔操作数转换成String(truefalse),然后产生一个最新创建的String,它是两个字符串的串接

布尔表达式确定了多种语句的控制流程:

  • if语句
  • while语句
  • do语句
  • for语句

boolean表达式还确定了哪个子表达式是通过?:运算符求值的。
只有boolean或Boolean表达式可以用于流程控制语句中,并且作为条件运算符?:的第一个操作数。依据C语言的约定,任何非0值都是true,通过表达式x!=0,可以把整数x转换成一个boolean。依据C语言的约定,不同于null的任何引用都是true,通过表达式obj!=null,可以把对象引用obj转换成一个boolean。

允许把boolean值强制转换成类型boolean或Boolean;不允许在类型boolean上执行其它强制转换。可以通过字符串转换把boolean转换成一个字符串。

引用类型的运算符

引用对象的运算符有:

  • 字段访问,使用限定名称或字段访问表达式
  • 方法调用
  • 强制类型转换运算符
  • 字符串串接运算符+
  • instanceof运算符
  • 引用相等性运算符==!=
  • 条件运算符?:
坚持原创技术分享,您的支持将鼓励我继续创作