2.1 变量
我们先来看下面的示例:
Dog dog1 = new Dog(); // 给他们状态 dog1.name = "Lucy"; dog1.color = "Black";
dog1是Dog类的实例(对象)名称,name和color是dog1字段的名称(字段存储了对象的状态),这些名称都代表了某种类型的值,在编程语言中被称为“变量”。通过变量,可以方便地找到内存中所存储的值。
Java里面的变量包含如下类型:
· 实例变量/非静态字段(Instance Variables/Non-Static Fields):从技术上讲,对象存储它们的个人状态在“非静态字段”,也就是没有static关键字声明的字段。非静态字段也被称为实例变量,因为它们的值对于类的每个实例来说是唯一的(换句话说,就是每个对象)。名字叫作Lucy的狗独立于另一条名字叫作Lily的狗。
· 类变量/静态字段(Class Variables/Static Fields):类变量是用static修饰符声明的字段,也就是告诉编译器无论类被实例化多少次,这个变量的存在只有一个副本。特定种类自行车的齿轮数目的字段可以被标记为static,因为相同的齿轮数量将适用于所有情况。代码“static intnumGears = 6;”将创建一个这样的静态字段。此外,关键字final也可以加入,以指示齿轮的数量不会改变。
· 局部变量(Local Variables):类似于对象存储状态在字段里,方法通常会存放临时状态在局部变量里。语法与局部变量的声明类似(例如,int count = 0;)。没有特殊的关键字来指定一个变量是否是局部变量,是由该变量声明的位置决定的。局部变量是类的方法中的变量。
· 参数(Parameters):在前文的例子中经常可以看到public static void main(String[] args),这里的args变量就是这个方法参数。需要记住的重要一点是,参数都归类为“变量(variable)”而不是“字段(field)”。
如果我们谈论的是“一般的字段”(不包括局部变量和参数),那么我们可以简单地说“字段”。如果讨论适用于上述所有情况,那么我们可以简单地说“变量”。如果上下文要求一个区别,我们将使用特定的术语(静态字段、局部变量等),也偶尔会使用术语“成员(member)”。类型的字段、方法和嵌套类型统称为它的成员。
2.1.1 命名
每一个编程语言都有它自己的一套规则和惯例的各种名目,Java编程语言对于命名变量的规则和惯例可以概括如下:
· 变量名称是区分大小写的。变量名可以是任何合法的标识符(无限长度的Unicode字母和数字),以字母、美元符号$或下画线_开头,但是推荐按照惯例以字母开头,而不是$或_。此外,按照惯例,美元符号从未使用过。在某些情况下,某些软件自动生成的名称会包含美元符号,但在实际编程中变量名应该始终避免使用美元符号。类似的约定还有下画线,不鼓励用“_”作为变量名开头。空格是不允许的。
· 随后的字符可以是字母、数字、美元符号或下画线字符。惯例同样适用于这一规则。为变量命名,尽量是完整的单词,而不是神秘的缩写。这样做会使你的代码更容易阅读和理解,比如name和color会比缩写n和c更直观。同时请记住,选择的名称不能是关键字或保留字。
· 如果选择的名称只包含一个单词,那么拼写单词全部为小写字母。如果它由一个以上的单词组成,那么每个后续单词的第一个字母大写,如gearRatio和currentGear。如果你的变量存储一个常量,如static final int NUM_GEARS = 6,那么每个字母大写,并用下画线分隔后续字符。按照惯例,下画线字符永远不会在其他地方使用。
详细的命名规范,可以参考笔者所著的《Java编码规范》(https://github.com/waylau/java-code-conventions)。
2.1.2 基本数据类型
Java是静态类型的语言,必须先声明再使用。基本数据类型之间不会共享。主要有8种基本数据类型:byte、short、int、long、char、float、double、boolean。其中,byte、short、int、long是整数类型,float、double是浮点数类型。
1.byte
byte由1个字节8位表示,是最小的整数类型。当操作来自网络、文件或者其他I/O的数据流时,byte类型特别有用。byte取值范围是[-128, 127],默认值为(byte)0。如果我们试图将取值范围外的值赋给byte类型的变量,则会出现编译错误,例如:
byte b = 128; // 编译错误
上面这个语句是无法通过编译的。
还有一个有趣的问题,如果有这样一个方法:
试图通过test(0)来调用这个方法是错误的,编译器会报错,因为类型不兼容!但是像下面这样赋值就完全没有问题:
byte b = 0; // 正常
这里涉及一个叫字面值(literal)的问题。字面值就是表面上的值,例如整型字面值在源代码中就是诸如5、0、-200这样的。如果整型字面值后面加上L或者l,那么这个字面值就是long类型,比如1000L代表一个long类型的值。
注意
l和1长得很像,所以表示一个long型时,以免肉眼看错,建议以L结尾。
若不加L或者l,则为int类型。基本类型当中的byte、short、int、long都可以通过不加L的整型字面值来创建,例如:
byte b = 100; short s = 5;
对于long类型,如果大小超出int所能表示的范围(32 bits),则必须使用L结尾来表示。整型字面值可以有不同的表示方式:十六进制(0X或者0x)、十进制、八进制(0)、二进制(0B或者0b)等。二进制字面值是JDK 7以后才有的功能。在赋值操作中,int字面值可以赋给byte、short、int、long等,Java语言会自动处理好这个过程。如果方法调用时不一样,比如调用test(0)的时候,它能匹配的方法是test(int),当然不能匹配test(byte)方法。
注意区别包装器与原始类型的自动转换,比如下面的赋值是允许的:
byte d = 'A'; // 正常
上面例子中的字符字面值可以自动转换成16位的整数。
对byte类型进行数学运算时,会自动提升为int类型。如果表达式中有double或者float等类型,也是会自动提升的。所以下面的代码是错误的:
byte t s1 = 100; byte s2 = 'a'; byte sum = s1 + s2; // 错误!不能将int转为byte
2.short
short用16位表示,取值范围为[- 2^15, 2^15 - 1]。short可能是最不常用的类型,可以通过整型字面值或者字符字面值赋值,前提是不超出范围。short类型参与运算的时候,一样会被提升为int或者更高的类型。
3.int
int用32位表示,取值范围为[- 2^31, 2^31 - 1]。Java 8以后,可以使用int类型表示无符号32位整数,数据范围为[0, 2^31 - 1]。
4.long
long用64位表示,取值范围为[- 2^63, 2^63 - 1],默认值为0L。当需要计算非常大的数时,如果int不足以容纳大小,可以使用long类型。如果long也不够,可以使用BigInteger类。
5.char
char用16位表示,其取值范围可以是[0, 65535]、[0, 2^16 -1]或者是从\u0000到\uffff。Java使用Unicode字符集表示字符,Unicode是完全国际化的字符集,可以表示全部人类语言中的字符。Unicode需要16位宽,所以Java中的char类型也使用16位表示。赋值可能是这样的:
char ch1 = 88; char ch2 = 'A';
ASCII字符集占用了Unicode的前127个值。之所以把char归入整型,是因为Java为char提供算术运算支持,例如运行“ch2++;”之后ch2就变成Y。当char进行加减乘除运算的时候,会被转换成int类型,必须显式转化回来。
6.float
float使用32位表示,对应单精度浮点数,遵循IEEE 754规范。运行速度相比double更快,占内存更小,但是当数值非常大或者非常小的时候会变得不精确。精度要求不高的时候可以使用float类型,声明赋值示例:
float f1 =10; f1 = 10L; f1 = 10.0f;
可以将byte、short、int、long、char赋给float类型,Java自动完成转换。
7.double
double使用64位表示,将浮点字面值赋给某个变量时,如果不显示在字面值后面加f或者F,则默认为double类型。比如下面的例子:
float f1 =10; f1 = 10.0; //为double类型
java.lang.Math中的函数都采用double类型。如果double和float都无法达到想要的精度,可以使用BigDecimal类。
8.boolean
boolean类型只有两个值true和false,默认为false。boolean与是否为0没有任何关系,但是可以根据想要的逻辑进行转换。许多地方都需要用到boolean类型。
除了上面列出的8种原始数据类型,Java编程语言还提供了java.lang.String,用于字符串的特殊支持。双引号包围的字符串会自动创建一个新的String对象,例如:
String s = "this is a string";
String对象是不可变的,这意味着一旦创建,它们的值不能改变。String类型不是技术上的原始数据类型,但考虑到语言所赋予的特殊支持,你可能会错误地倾向于认为它是这样的。
2.1.3 基本数据类型的默认值
在字段声明时,有时并不必要分配一个值。字段被声明但尚未初始化时,将会由编译器设置一个合理的默认值。一般而言,根据数据类型的不同,默认将为零或为null。良好的编程风格不应该依赖于这样的默认值。表2-1总结了上述数据类型的默认值。
表2-1 基本数据类型的默认值
局部变量(Local Variable)略有不同,编译器不会指定一个默认值未初始化的局部变量。如果你不能初始化你声明的局部变量,那么请确保使用之前给它分配一个值。访问一个未初始化的局部变量会导致编译时错误。
2.1.4 字面值
在Java源代码中,字面值(Literal)用于表示固定的值,直接展示在代码里,而不需要计算。数值型的字面值是最常见的,字符串字面值可以算是一种,当然也可以把特殊的null当作字面值。字面值大体上可以分为整型字面值、浮点字面值、字符和字符串字面值、特殊字面值。
1.整型字面值
从形式上看是整数的字面值归类为整型字面值。例如,10、100000L、'B'、0XFF这些都可以称为字面值。整型字面值可以用十进制、十六进制、八进制、二进制来表示。十进制很简单,二进制、八进制、十六进制的表示分别在最前面加上0B(0b)、0、0X(0x)即可。
当然基数不能超出进制的范围,比如在八进制里面09是不合法的,八进制的基数只能到7。一般情况下,字面值创建的是int类型,但是int字面值可以赋值给byte、short、int、long、char,只要字面值在目标范围以内,Java就会自动完成转换。如果试图将超出范围的字面值赋给某一类型(比如把128赋给byte类型),编译会通不过。如果想创建一个int类型无法表示的long类型,则需要在字面值最后面加上L或者l,通常建议使用容易区分的L。所以整型字面值包括int字面值和long字面值两种。
· 十进制:其位数由数字0~9组成,这是你每天使用的数字系统。
· 十六进制:其位数由数字0到9和字母A至F组成。
· 二进制:其位数由数字0和1组成。
下面是使用的语法:
// 十进制 int decVal = 26; // 十六进制 int hexVal = 0x1a; // 二进制 int binVal = 0b11010;
2.浮点字面值
浮点字面值可以简单理解为小数,分为float字面值和double字面值两种。如果在小数后面加上F或者f,就表示这是一个float字面值,如11.8F。如果小数后面不加F(f),如10.4,或者小数后面加上D(d),则表示这是一个double字面值。另外,浮点字面值支持科学记数法(E或e)表示。下面是一些例子:
double d1 = 123.4; // 科学记数法 double d2 = 1.234e2; float f1 = 123.4f;
3.字符和字符串字面值
在Java中,字符字面值用单引号括起来,如'@'、'1'。所有的UTF-16字符集都包含在字符字面值中。不能直接输入的字符可以使用转义字符,如\n为换行字符。也可以使用八进制或者十六进制表示字符,八进制使用反斜杠加3位数字表示,例如'\141'表示字母a。十六进制使用'\u'加上4为十六进制的数表示,如'\u0061'表示字符a。也就是说,通过使用转义字符,可以表示键盘上有的或者没有的所有字符。常见的转义字符序列有:
· \ddd(八进制)
· \uxxxx(十六进制Unicode字符)
· \'(单引号)
· \"(双引号)
· \\(反斜杠)
· \r(回车符)
· \n(换行符)
· \f(换页符)
· \t(制表符)
· \b(回格符)
字符串字面值使用双引号。字符串字面值中同样可以包含字符字面值中的转义字符序列。字符串必须位于同一行或者使用+运算符,因为Java没有续行转义序列。
4.特殊字面值
从Java SE 7开始,可以在数值型字面值中使用下画线,但是下画线只能用于分隔数字,不能分隔字符与字符,也不能分隔字符与数字。例如:
int x = 123_456_789;
在编译上面的代码时,下画线会自动去掉。
可以连续使用下画线,比如:
float f = 1.22___33__44
二进制或者十六进制的字面值也可以使用下画线。
切记,下画线只能用于数字与数字之间,除此以外都是非法的。例如,1._23是非法的,_123、11000_L都是非法的。
下面列出一些正确的用法:
long creditCardNumber = 1234_5678_9012_3456L; long socialSecurityNumber = 999_99_9999L; float pi = 3.14_15F; long hexBytes = 0xFF_EC_DE_5E; long hexWords = 0xCAFE_BABE; long maxLong = 0x7fff_ffff_ffff_ffffL; byte nybbles = 0b0010_0101; long bytes = 0b11010010_01101001_10010100_10010010;
下面列出一些非法的用法:
float pi1 = 3_.1415F; float pi2 = 3._1415F; long socialSecurityNumber1 = 999_99_9999_L; int x2 = 52_; int x4 = 0_x52; int x5 = 0x_52; int x7 = 0x52_;
2.1.5 基本数据类型之间的转换
在Java中,将一种类型的值赋给另一种类型是很常见的。同时要注意,boolean类型与其他7种类型不能进行转换,这一点很明确。对于其他7种数据类型,它们之间都可以进行转换,但是可能会存在精度损失或者其他一些变化。
转换分为自动转换和强制转换。对于自动转换(隐式),无须任何操作;而强制类型转换需要显式转换,即使用转换操作符“(类型)”。以下是一个示例:
int i =97; char c = (char)i; // int强制转换为char
首先将7种类型按下面的顺序排列一下:
byte <(short=char)< int < long < float < double
从小转换到大,可以自动完成;而从大到小,则必须强制转换。即使short和char类型相同,也必须强制转换。
图2-1形象地展示了类型转换之间的关系。小杯子的物品可以顺利倒入大杯子中(自动转换),但大杯子里面的物品则不能简单地倒入小杯子中(强制转化,可能会导致物品丢失)。
图2-1 基本数据类型之间的转换
1.自动转换
自动转换时发生扩宽转换(widening conversion)。因为较大的类型(如int)要保存较小的类型(如byte),内存总是足够的,不需要强制转换。将字面值保存到byte、short、char、long的时候,也会自动进行类型转换。注意,此时从int(没有带L的整型字面值为int)到byte、short、char也是自动完成的,虽然它们都比int小。在自动类型转化中,除了以下几种情况可能会导致精度损失以外,其他的转换都不能出现精度损失。
· int–> float
· long–> float
· long–> double
· float –>无符号double
除了可能的精度损失外,自动转换不会出现任何运行时异常。
2.强制类型转换
如果要把大的转成小的,或者在short与char之间进行转换,就必须强制转换,也被称作缩小转换(narrowing conversion),因为必须显式地使数值更小以适应目标类型。严格地说,将byte转为char不属于缩小转换,因为从byte到char的过程其实是byte→int→char,所以扩宽转换和缩小转换都有。
强制转换除了可能的精度损失外,还可能使模(overall magnitude)发生变化。强制转换示例如下:
int a = 257; byte b; b = (byte)a; // 1
如果整数的值超出了byte所能表示的范围,结果将对byte类型的范围取余数。例如,a=257超出了byte [-128,127]的范围,所以将257除以byte的范围(256)取余数得到b=1。需要注意的是,当a=200时,除以256取余数应该为-56,而不是200。
将浮点类型赋给整数类型的时候会发生截尾(truncation),也就是把小数的部分去掉,只留下整数部分。此时如果整数超出目标类型范围,一样将对目标类型的范围取余数。
7种基本类型转换总结如图2-2所示。
图2-2 7种基本类型转换总结
3.字面值赋值
在使用字面值对整数赋值的过程中,可以将int字面值赋给byte、short、char、int,只要不超出范围即可。这个过程中的类型转换是自动完成的,但是如果你试图将long字面值赋给byte,即使没有超出范围,也必须进行强制类型转换。例如,下面的例子是非法的:
byte b = 10L; // 错误!
如果想将long型转为byte,则需要进行强制转换。
4.表达式中的自动类型提升
除了赋值以外,表达式计算过程中也可能发生一些类型转换。在表达式中,类型提升规则如下:
· 所有byte、short、char都被提升为int。
· 如果有一个操作数为long,整个表达式提升为long。float和double情况也一样。
2.1.6 数组
数组(Array)是一个容器对象,保存一个固定数量的单一类型的值。当数组创建时,数组的长度就确定了。创建后,其长度是固定的。数据里面的每个项称为元素(element),每个元素都用一个数组下标(index)关联。下标从0开始,如图2-3所示,第9个元素的下标是8。
图2-3 数组示例
以下是一个数组的示例:
输出为:
Element at index 0: 100 Element at index 1: 200 Element at index 2: 300 Element at index 3: 400 Element at index 4: 500 Element at index 5: 600 Element at index 6: 700 Element at index 7: 800 Element at index 8: 900 Element at index 9: 1000
1.声明引用数组的变量
声明数组的类型:
byte[] anArrayOfBytes; short[] anArrayOfShorts; long[] anArrayOfLongs; float[] anArrayOfFloats; double[] anArrayOfDoubles; boolean[] anArrayOfBooleans; char[] anArrayOfChars; String[] anArrayOfStrings;
也可以将中括号放在数组名称后面(但不推荐):
// 合法,但不推荐使用 float anArrayOfFloats[];
2.创建、初始化和访问数组
ArrayDemo的示例说明了创建、初始化和访问数组的过程。可以用下面的方式简化创建、初始化数组:
int[] anArray = { 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 };
数组里面可以声明数组,即多维数组(multidimensional array)。如下面的例子就是一个多维数组MultiDimArrayDemo:
输出为:
Mr. Way Ms. Lau
最后,可以通过内建的length属性来确认数组的大小:
System.out.println(anArray.length);
3.复制数组
System类有一个arraycopy方法,用于数组的有效复制:
下面是一个例子(ArrayCopyDemo):
程序输出为:
way
4.数组操作
Java提供了一些数组有用的操作。观察下面的例子ArrayCopyOfDemo:
可以看到,相比于ArrayCopyDemo的例子,使用java.util.Arrays.copyOfRange方法,代码量减少了很多。
其他常用操作还包括:
· binarySearch:用于搜索。
· equals:比较两个数组是否相等。
· fill:填充数组。
· sort:数组排序,在Java 8以后,可以使用parallelSort方法,在多处理器系统的大数组并行排序比连续数组排序更快。