所谓程序基础就是学习一门语言的惯用思路:数据类型,计算方法,流程控制,编码规范等等。掌握这些,基本上可以号称“学会了”一门语言。 强类型语言——是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。本篇的内容对于掌握了一门编程语言的人,基本可以快速过。不过本文还是足够详细介绍了Java的数据类型,尤其是编码过程背后,究竟发生了什么。借此学习更多,也是不错的事。
我们先剖析一个完整的Java程序,它的基本结构是什么:
public class Hello { public static void main(String[] args) { // 向屏幕输出文本: System.out.println("Hello, world!"); }} // class定义结束
因为Java是面向对象的语言,一个程序的基本单位就是class
,class
是关键字,这里定义的class
名字就是Hello
:
public class Hello { // 类名是Hello // ...} // class定义结束
•类名必须以英文字母开头,后接字母,数字和下划线的组合•习惯以大写字母开头
要注意遵守命名习惯,
好的类名 | 不好的类名 |
---|---|
Hello | hello |
NoteBook | Good123 |
VRPlayer | _World |
注意到public
是访问修饰符,表示该class
是公开的。
不写public
,也能正确编译,但是这个类将无法从命令行执行。
在class
内部,可以定义若干方法(method):
public class Hello { public static void main(String[] args) { // 方法名是main // 方法代码... } // 方法定义结束}
方法定义了一组执行语句,方法内部的代码将会被依次顺序执行。
这里的方法名是main
,返回值是void
,表示没有任何返回值。
我们注意到public
除了可以修饰class
外,也可以修饰方法。而关键字static
是另一个修饰符,表示静态方法,后面我们会讲解方法的类型,目前,我们只需要知道,Java入口程序规定的方法必须是静态方法,方法名必须为main
,括号内的参数必须是String数组。
方法名也有命名规则,命名和class
一样,但是首字母小写:
好的方法名 | 不好的方法名 |
---|---|
Hello | hello |
goodMorning | good123 |
playVR | _World |
在方法内部,语句才是真正的执行代码。Java的每一行语句必须以分号结束:
public class Hello { public static void main(String[] args) { System.out.println("Hello, world!"); // 语句 }}
在Java程序中,注释是一种给人阅读的文本,不是程序的一部分,所以编译器会自动忽略注释。
Java有3种注释,第一种是单行注释,以双斜线开头,直到这一行的结尾结束:
// 这是注释...
而多行注释以/*
星号开头,以*/
结束,可以有多行:
/*这是多行注释*/
还有一种特殊的多行注释,以/**
开头,以*/
结束,如果有多行,每行通常以星号开头:
/** * 可以用来自动创建文档的注释 * @auther djtao */public class Hello { public static void main(String[] args) { System.out.println("Hello, world!"); }}
这种特殊的多行注释需要写在类和方法的定义处,可以用于自动创建文档。
Java程序对格式没有明确的要求,多几个空格或者回车不影响程序的正确性,但是我们要养成良好的编程习惯,注意遵守Java社区约定的编码格式。
那约定的编码格式有哪些要求呢?其实我们在前面介绍的Eclipse IDE提供了快捷键Ctrl+Shift+F
(macOS是⌘+⇧+F
)帮助我们快速格式化代码的功能,Eclipse就是按照约定的编码格式对代码进行格式化的,所以只需要看看格式化后的代码长啥样就行了。具体的代码格式要求可以在Eclipse的设置中Java
-Code Style
查看。
什么是变量?变量是初中代数的概念,例如一个简单的方程 y=x2+1 中,x,y都是变量。
在Java中,变量分为两种:基本类型的变量和引用类型的变量。
我们先讨论基本类型的变量。
// 赋值int x = 1;
上述语句定义了一个整型int
类型的变量,名称为x
,初始值为1
。
不写初始值,就相当于给它指定了默认值。默认值总是0
。
如果int是class的属性那么它会有一个默认值0. 如果int定义在方法里,是局部变量的话会没有初始值。
变量的一个重要特点是可以重新赋值。还可以赋值给其他变量。
// 赋值int x = 1;x = 2 // 2int y = x //2y = y + 1 // x->2 y->3
上面这些但凡学过高中伪代码的人都知道。我们一行一行地分析代码在机器中是如何执行的:
执行int x = 1;
,该语句定义了变量x,同时赋值为1,因此,JVM在内存中为变量x分配一个“存储单元”,填入值1:
x │ ▼┌───┬───┬───┬───┬───┬───┬───┐│ │ 1 │ │ │ │ │ │└───┴───┴───┴───┴───┴───┴───┘
执行x = 2;
时,JVM把2
写入变量x
的存储单元,因此,原有的值被覆盖,现在n
的值为200
:
x │ ▼┌───┬───┬───┬───┬───┬───┬───┐│ │ 2 │ │ │ │ │ │└───┴───┴───┴───┴───┴───┴───┘
执行int y = x;
时,定义了一个新的变量y
,同时对y
赋值,因此,JVM需要新分配一个存储单元给变量y
,并写入和变量y
一样的值,结果是变量x
的值也变为2
:
y x │ │ ▼ ▼┌───┬───┬───┬───┬───┬───┬───┐│ │ 2 │ │ │ 2 │ │ │└───┴───┴───┴───┴───┴───┴───┘
执行y = y + 1;
时,JVM首先计算等式右边的值y + 1
,结果为3
(因为此刻y
的值为2
),然后,将结果3
写入x
的存储单元,因此,变量x
最终的值变为300
:
2+1 x │ │ ▼ ▼┌───┬───┬───┬───┬───┬───┬───┐│ │ 2 │ │ │ 2 │ │ │└───┴───┴───┴───┴───┴───┴───┘ y
可见,变量可以反复赋值。
基本数据类型是CPU可以直接进行运算的类型。Java定义了以下几种基本数据类型:
•整数类型:byte,short,int,long•浮点数类型:float,double•字符类型:char•布尔类型:boolean
Java定义的这些基本数据类型有什么区别呢?要了解这些区别,我们就必须简单了解一下计算机内存的基本结构。
计算机内存的最小存储单元是字节(byte),一个字节就是一个8位二进制数,即8个bit。它的二进制表示范围从00000000
11111111,换算成十进制是0255,换算成十六进制是00
~ff
。
内存单元从0开始编号,称为内存地址。每个内存单元可以看作一间房间,内存地址就是门牌号。
0 1 2 3 4 5 6 ...┌───┬───┬───┬───┬───┬───┬───┐│ │ │ │ │ │ │ │...└───┴───┴───┴───┴───┴───┴───┘
一个字节是1byte,1024字节是1K,1024K是1M,1024M是1G,1024G是1T。一个拥有4T内存的计算机的字节数量就是:
4T = 4 x 1024G = 4 x 1024 x 1024M = 4 x 1024 x 1024 x 1024K = 4 x 1024 x 1024 x 1024 x 1024 = 4398046511104
不同的数据类型占用的字节数不一样。我们看一下Java基本数据类型占用的字节数:
┌───┐ byte │ │ └───┘ ┌───┬───┐ short │ │ │ └───┴───┘ ┌───┬───┬───┬───┐ int │ │ │ │ │ └───┴───┴───┴───┘ ┌───┬───┬───┬───┬───┬───┬───┬───┐ long │ │ │ │ │ │ │ │ │ └───┴───┴───┴───┴───┴───┴───┴───┘ ┌───┬───┬───┬───┐ float │ │ │ │ │ └───┴───┴───┴───┘ ┌───┬───┬───┬───┬───┬───┬───┬───┐double │ │ │ │ │ │ │ │ │ └───┴───┴───┴───┴───┴───┴───┴───┘ ┌───┬───┐ char │ │ │ └───┴───┘
byte
恰好就是一个字节,而long
和double
需要8个字节。
对于整型类型,Java只定义了带符号的整型,因此,最高位的bit表示符号位(0表示正数,1表示负数)。各种整型能表示的最大范围如下:
•byte:-128 ~ 127•short: -32768 ~ 32767•int: -2147483648 ~ 2147483647•long: -9223372036854775808 ~ 9223372036854775807
public class Main { public static void main(String[] args) { int i = 2147483647; int i2 = -2147483648; int i3 = 2_000_000_000; // 加下划线更容易识别 int i4 = 0xff0000; // 十六进制表示的16711680 int i5 = 0b1000000000; // 二进制表示的512 long l = 9000000000000000000L; // long型的结尾需要加L }}
特别注意:同一个数的不同进制的表示是完全相同的,例如15
=0xf
=0b1111
。
浮点类型的数就是小数,因为小数用科学计数法表示的时候,小数点是可以“浮动”的,如1234.5可以表示成12.345x102,也可以表示成1.2345x103,所以称为浮点数。
下面是定义浮点数的例子:
float f1 = 3.14f;float f2 = 3.14e38f; // 科学计数法表示的3.14x10^38double d = 1.79e308;double d2 = -1.79e308;double d3 = 4.9e-324; // 科学计数法表示的4.9x10^-324
对于float
类型,需要加上f
后缀。
浮点数可表示的范围非常大,float
类型可最大表示3.4x1038,而double
类型可最大表示1.79x10308。
布尔类型boolean
只有true
和false
两个值,布尔类型总是关系运算的计算结果:
boolean b1 = true;boolean b2 = false;boolean isGreater = 5 > 3; // 计算结果为trueint age = 12;boolean isAdult = age >= 18; // 计算结果为false
Java语言对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要1 bit,但是通常JVM内部会把boolean
表示为4字节整数。
字符类型char
表示一个字符。Java的char
类型除了可表示标准的ASCII外,还可以表示一个Unicode字符(如中文)。
注意char
类型使用单引号'
,且仅有一个字符,要和双引号"
的字符串类型区分开。
定义变量的时候,如果加上final
修饰符,这个变量就变成了常量:
final double PI = 3.14; // PI是一个常量double r = 5.0;double area = PI * r * r;PI = 300; // compile error!
常量在定义时进行初始化后就不可再次赋值,再次赋值会导致编译错误。
常量的作用是用有意义的变量名来避免魔术数字(Magic number),例如,不要在代码中到处写3.14
,而是定义一个常量。如果将来需要提高计算精度,我们只需要在常量的定义处修改,例如,改成3.1416
,而不必在所有地方替换3.14
。
根据习惯,常量名通常全部大写。
有些时候,类型的名字太长,写起来比较麻烦。例如:
StringBuilder sb = new StringBuilder();
这个时候,如果想省略变量类型,可以使用var
关键字:
var sb = new StringBuilder();
编译器会根据赋值语句自动推断出变量sb
的类型是StringBuilder
。对编译器来说,语句:
var sb = new StringBuilder();
实际上会自动变成:
StringBuilder sb = new StringBuilder();
因此,使用var
定义变量,仅仅是少写了变量类型而已。
在Java中,多行语句用{ }括起来。很多控制语句,例如条件判断和循环,都以{ }作为它们自身的范围,例如:
if (...) { // if开始 ... while (...) { while 开始 ... if (...) { // if开始 ... } // if结束 ... } // while结束 ...} // if结束
只要正确地嵌套这些{ },编译器就能识别出语句块的开始和结束。而在语句块中定义的变量,它有一个作用域,就是从定义处开始,到语句块结束。超出了作用域引用这些变量,编译器会报错。举个例子:
{ ... int i = 0; // 变量i从这里开始定义 ... { ... int x = 1; // 变量x从这里开始定义 ... { ... String s = "hello"; // 变量s从这里开始定义 ... } // 变量s作用域到此结束 ... // 注意,这是一个新的变量s,它和上面的变量同名, // 但是因为作用域不同,它们是两个不同的变量: String s = "hi"; ... } // 变量x和s作用域到此结束 ...} // 变量i作用域到此结束
定义变量时,要遵循作用域最小化原则,尽量将变量定义在尽可能小的作用域,并且,不要重复使用变量名。
•Java提供了两种变量类型:基本类型和引用类型-•基本类型包括整型,浮点型,布尔型,字符型。•变量可重新赋值,等号是赋值语句,不是数学意义的等号。•常量在初始化后不可重新赋值,使用常量便于理解程序意图。
Java的整数运算遵循四则运算规则,可以使用任意嵌套的小括号。四则运算规则和初等数学一致。
整数的数值表示不但是精确的,而且整数运算永远是精确的,即使是除法也是精确的,因为两个整数相除只能得到结果的整数部分:
int x = 12345 / 67; // 184
求余运算使用%
:
int y = 12345 % 67; // 12345÷67的余数是17
特别注意:整数的除法对于除数为0时运行时将报错,但编译不会报错。
public class Main { public static void main(String[] args) { int x = 2147483640; int y = 15; int sum = x + y; System.out.println(sum); // -2147483641 }}
要解释上述结果,我们把整数2147483640
和15
换成二进制做加法:
0111 1111 1111 1111 1111 1111 1111 1000+ 0000 0000 0000 0000 0000 0000 0000 1111----------------------------------------- 1000 0000 0000 0000 0000 0000 0000 0111
由于最高位计算结果为1
,刚好是二进制负号表示,因此,加法结果变成了一个负数。
要解决上面的问题,可以把int
换成long
类型,由于long
可表示的整型范围更大,所以结果就不会溢出:
long x = 2147483640;long y = 15;long sum = x + y;System.out.println(sum); // 2147483655
还有一种简写的运算符,即+=
,-=
,*=
,/=
,++
,--
它们的使用方法如下:
n += 100; // 3409, 相当于 n = n + 100;n -= 100; // 3309, 相当于 n = n - 100;
注意++
写在前面和后面计算结果是不同的,++n
表示先加1再引用n,n++
表示先引用n再加1。不建议把++
运算混入到常规运算中,容易自己把自己搞懵了。
浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做位运算和移位运算。
在计算机中,浮点数虽然表示的范围大,但是,浮点数有个非常重要的特点,就是浮点数常常无法精确表示。
举个栗子:
浮点数0.1
在计算机中就无法精确表示,因为十进制的0.1
换算成二进制是一个无限循环小数,很显然,无论使用float
还是double
,都只能存储一个0.1
的近似值。但是,0.5
这个浮点数又可以精确地表示。
因为浮点数常常无法精确表示,因此,浮点数运算会产生误差:
public class Main { public static void main(String[] args) { double a = 1.0 - 1.0 / 3; System.out.println(a); }}// 0.6666666666666667
由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数:
// 比较x和y是否相等,先计算其差的绝对值:double r = Math.abs(x - y);// 再判断绝对值是否足够小:if (r < 0.00001) { // 可以认为相等} else { // 不相等}
浮点数在内存的表示方法和整数比更加复杂。Java的浮点数完全遵循IEEE-754[1]标准,这也是绝大多数计算机平台都支持的浮点数标准表示方法。
如果参与运算的两个数其中一个是整型,那么整型可以自动提升到浮点型:
double a = 1 - 1.0 / 3; // 0.6666666666666667
需要特别注意,在一个复杂的四则运算中,两个整数的运算不会出现自动提升的情况。例如:
double d = 1.2 + 24 / 5; // 5.2
计算结果为5.2
,原因是编译器计算24 / 5
这个子表达式时,按两个整数进行运算,结果仍为整数4
。
整数运算在除数为0
时会报错,而浮点数运算在除数为0
时,不会报错,但会返回几个特殊值:
•NaN
表示Not a Number•Infinity
表示无穷大•-Infinity
表示负无穷大
例如:
double d1 = 0.0 / 0; // NaNdouble d2 = 1.0 / 0; // Infinitydouble d3 = -1.0 / 0; // -Infinity
这三种特殊值在实际运算中很少碰到,我们只需要了解即可。
可以将浮点数强制转型为整数。在转型时,浮点数的小数部分会被丢掉。如果转型后超过了整型能表示的最大范围,将返回整型的最大值。例如:
int n1 = (int) 12.3; // 12int n2 = (int) 12.7; // 12int n2 = (int) -12.7; // -12int n3 = (int) (12.7 + 0.5); // 13int n4 = (int) 1.2e20; // 2147483647
如果要进行四舍五入,可以对浮点数加上0.5再强制转型:
public class Main { public static void main(String[] args) { double d = 2.6; int n = (int) (d + 0.5); System.out.println(n); }}
根据一元二次方程 ax2+bx+c=0 (a!=0)的求根公式:
计算出a=1,b=3,c=-4的两个解:
public class Main { public static void main(String[] args) { double a = 1.0; double b = 3.0; double c = -4.0;
double delta = Math.pow(b, 2) - 4 * a * c; double r1 = (-b + Math.sqrt(delta)) / (2 * a); double r2 = (-b - Math.sqrt(delta)) / (2 * a); System.out.println(r1); // 1.0 System.out.println(r2); // -4.0 }}
浮点数常常无法精确表示,并且浮点数的运算结果可能有误差;
比较两个浮点数通常比较它们的绝对值之差是否小于一个特定值;
整型和浮点型运算时,整型会自动提升为浮点型;
对于布尔类型boolean
,永远只有true
和false
两个值。
布尔运算是一种关系运算,包括以下几类:
•比较运算符:>
,>=
,<
,<=
,==
,!=
•与运算 &&
•或运算 ||
•非运算 !
下面是一些示例:
boolean isGreater = 5 > 3; // trueint age = 12;boolean isZero = age == 0; // falseboolean isNonZero = !isZero; // trueboolean isAdult = age >= 18; // falseboolean isTeenager = age >6 && age <18; // true
关系运算符的优先级从高到低依次是:
•!
•>
,>=
,<
,<=
•==
,!=
•&&
•||
布尔运算的一个重要特点是“短路运算”。如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果。
因为false && x
的结果总是false
,无论x
是true
还是false
,因此,与运算在确定第一个值为false
后,不再继续计算,而是直接返回false
。
我们考察以下代码:
public class Main { public static void main(String[] args) { boolean b = 5 < 3; boolean result = b && (5 / 0 > 0); System.out.println(result); }}
如果没有短路运算,&&
后面的表达式会由于除数为0
而报错,但实际上该语句并未报错,原因在于与运算是短路运算符,提前计算出了结果false
。
如果变量b
的值为true
,则表达式变为true && (5 / 0 > 0)
。因为无法进行短路运算,该表达式必定会由于除数为0
而报错,可以自行测试。
类似的,对于||
运算,只要能确定第一个值为true
,后续计算也不再进行,而是直接返回true
:
boolean result = true || (5 / 0 > 0); // true
Java还提供一个三元运算符b ? x : y
,它根据第一个布尔表达式的结果,分别返回后续两个表达式之一的计算结果。示例:
int x = n >= 0 ? n : -n;
上述语句的意思是,判断n >= 0
是否成立,如果为true
,则返回n
,否则返回-n
。这实际上是一个求绝对值的表达式。
注意到三元运算b ? x : y
会首先计算b
,如果b
为true
,则只计算x
,否则,只计算y
。此外,x
和y
的类型必须相同,因为返回值不是boolean
,而是x
和y
之一。
判断指定年龄是否是小学生(6~12岁):
public class Main { public static void main(String[] args) { int age = 7; // primary student的定义: 6~12岁 boolean isPrimaryStudent = (age >= 6 && age <= 12) ? true : false; System.out.println(isPrimaryStudent ? "Yes" : "No"); }}// Yes
与运算(&)和或(||)运算是短路运算;
三元运算b ? x : y
后面的类型必须相同,三元运算也是“短路运算”。
在Java中,字符和字符串是两个不同的类型。
字符类型char
是基本数据类型,它是character
的缩写。一个char
保存一个Unicode字符:
char c1 = 'A';char c2 = '中';
因为Java在内存中总是使用Unicode表示字符,所以,一个英文字符和一个中文字符都用一个char
类型表示,它们都占用两个字节。要显示一个字符的Unicode编码,只需将char
类型直接赋值给int
类型即可:
int n1 = 'A'; // 字母“A”的Unicodde编码是65int n2 = '中'; // 汉字“中”的Unicode编码是20013
还可以直接用转义字符\u
+Unicode编码来表示一个字符:
// 注意是十六进制:char c3 = '\u0041'; // 'A',因为十六进制0041 = 十进制65char c4 = '\u4e2d'; // '中',因为十六进制4e2d = 十进制20013
和char
类型不同,字符串类型String
是引用类型,(注意,引用类型的定义稍后还会着重强调)我们用双引号""
表示字符串。一个字符串可以存储0个到任意个字符:
String s = ""; // 空字符串,包含0个字符String s1 = "A"; // 包含一个字符String s2 = "ABC"; // 包含3个字符String s3 = "中文 ABC"; // 包含6个字符,其中有一个空格
因为字符串使用双引号""
表示开始和结束,那如果字符串本身恰好包含一个"
字符怎么表示?例如,"abc"xyz"
,编译器就无法判断中间的引号究竟是字符串的一部分还是表示字符串结束。这个时候,我们需要借助转义字符\
:
String s = "abc\"xyz"; // 包含7个字符: a, b, c, ", x, y, z
因为\
是转义字符,所以,两个\\
表示一个\
字符:
String s = "abc\\xyz"; // 包含7个字符: a, b, c, \, x, y, z
常见的转义字符包括:
•\"
表示字符"
•\'
表示字符'
•\\
表示字符\
•\n
表示换行符•\r
表示回车符•\t
表示Tab•\u####
表示一个Unicode编码的字符
例如:
String s = "ABC\n\u4e2d\u6587"; // 包含6个字符: A, B, C, 换行符, 中, 文
Java的编译器对字符串做了特殊照顾,可以使用+
连接任意字符串和其他数据类型,这样极大地方便了字符串的处理。例如:
public class Main { public static void main(String[] args) { String s1 = "Hello"; String s2 = "world"; String s = s1 + " " + s2 + "!"; System.out.println(s); }}
如果用+
连接字符串和其他数据类型,会将其他数据类型先自动转型为字符串,再连接:
public class Main { public static void main(String[] args) { int age = 25; String s = "age is " + age; System.out.println(s); // "25" }}
如果我们要表示多行字符串,使用+号连接会非常不方便:
String s = "first line \n" + "second line \n" + "end";
从Java 13开始,字符串可以用"""..."""
表示多行字符串(Text Blocks)了。举个例子:
public class Main { public static void main(String[] args) { String s = """ SELECT * FROM users WHERE id > 100 ORDER BY name DESC """; System.out.println(s); }}
上述多行字符串实际上是5行,在最后一个DESC
后面还有一个\n
。如果我们不想在字符串末尾加一个\n
,就需要这么写:
String s = """ SELECT * FROM users WHERE id > 100 ORDER BY name DESC""";
还需要注意到,多行字符串前面共同的空格会被去掉,即:
String s = """...........SELECT * FROM........... users...........WHERE id > 100...........ORDER BY name DESC...........""";
用.
标注的空格都会被去掉。
如果多行字符串的排版不规则,那么,去掉的空格就会变成这样:
String s = """......... SELECT * FROM......... users.........WHERE id > 100......... ORDER BY name DESC......... """;
即总是以最短的行首空格为基准。
最后,由于多行字符串是作为Java 13的预览特性(Preview Language Features)实现的,编译的时候,我们还需要给编译器加上参数:
javac --source 13 --enable-preview Main.java
Java的字符串除了是一个引用类型外,还有个重要特点,就是字符串不可变。考察以下代码:
public class Main { public static void main(String[] args) { String s = "hello"; System.out.println(s); // 显示 hello s = "world"; System.out.println(s); // 显示 world }}
观察执行结果,难道字符串s
变了吗?其实变的不是字符串,而是变量s
的“指向”。
执行String s = "hello";
时,JVM虚拟机先创建字符串"hello"
,然后,把字符串变量s
指向它:
s │ ▼┌───┬───────────┬───┐│ │ "hello" │ │└───┴───────────┴───┘
紧接着,执行s = "world";
时,JVM虚拟机先创建字符串"world"
,然后,把字符串变量s
指向它:
s ──────────────┐ │ ▼┌───┬───────────┬───┬───────────┬───┐│ │ "hello" │ │ "world" │ │└───┴───────────┴───┴───────────┴───┘
原来的字符串"hello"
还在,只是我们无法通过变量s
访问它而已。因此,字符串的不可变是指字符串内容不可变。
理解了引用类型的“指向”后,可以解释下面的代码输出:
public class Main { public static void main(String[] args) { String s = "hello"; String t = s; s = "world"; System.out.println(t); // "hello" }}
引用类型的变量可以指向一个空值null
,它表示不存在,即该变量不指向任何对象。例如:
String s1 = null; // s1是nullString s2; // 没有赋初值值,s2也是nullString s3 = s1; // s3也是nullString s4 = ""; // s4指向空字符串,不是null
注意要区分空值null
和空字符串""
,空字符串是一个有效的字符串对象,它不等于null
。
请将一组int值视为字符的Unicode编码,然后将它们拼成一个字符串:
public class Main { public static void main(String[] args) { int a = 72; int b = 105; int c = 65281; String s = "" + (char)a + (char)b + (char)c; System.out.println(s); // Hi! }}
Java的字符类型char
是基本类型,字符串类型String
是引用类型;
基本类型的变量是“持有”某个数值,引用类型的变量是“指向”某个对象;
引用类型的变量可以是空值null
;
要区分空值null
和空字符串""
。
如果我们有一组类型相同的变量,例如,5位同学的成绩,可以这么写:
public class Main { public static void main(String[] args) { // 5位同学的成绩: int n1 = 68; int n2 = 79; int n3 = 91; int n4 = 85; int n5 = 62; }}
但其实没有必要int5个变量。可以使用数组来表示“一组”int
类型。代码如下:
public class Main { public static void main(String[] args) { // 5位同学的成绩: int[] ns = new int[5]; ns[0] = 68; ns[1] = 79; ns[2] = 91; ns[3] = 85; ns[4] = 62; }}
定义一个数组类型的变量,使用数组类型类型[]
,例如,int[]
。和单个基本类型变量不同,数组变量初始化必须使用new int[5]
表示创建一个可容纳5个int
元素的数组。
Java的数组有几个特点:
•数组所有元素初始化为默认值,整型都是0
,浮点型是0.0
,布尔型是false
;•数组一旦创建后,大小就不可改变。
要访问数组中的某一个元素,需要使用索引。数组索引从0
开始,例如,5个元素的数组,索引范围是0
~4
。
可以修改数组中的某一个元素,使用赋值语句,例如,ns[1] = 79;
。
可以用数组变量.length
获取数组大小:
public class Main { public static void main(String[] args) { // 5位同学的成绩: int[] ns = new int[5]; System.out.println(ns.length); // 5 }}
数组是引用类型,在使用索引访问数组元素时,如果索引超出范围,运行时将报错。
也可以在定义数组时直接指定初始化的元素,这样就不必写出数组大小,而是由编译器自动推算数组大小。例如:
public class Main { public static void main(String[] args) { // 5位同学的成绩: int[] ns = new int[] { 68, 79, 91, 85, 62 }; System.out.println(ns.length); // 编译器自动推算数组大小为5 }}
还可以进一步简写为:
int[] ns = { 68, 79, 91, 85, 62 };
注意数组是引用类型,并且数组大小不可变。我们观察下面的代码:
public class Main { public static void main(String[] args) { // 5位同学的成绩: int[] ns; ns = new int[] { 68, 79, 91, 85, 62 }; System.out.println(ns.length); // 5 ns = new int[] { 1, 2, 3 }; System.out.println(ns.length); // 3 }}
数组大小变了吗?看上去好像是变了,但其实根本没变。
对于数组ns
来说,执行ns = new int[] { 68, 79, 91, 85, 62 };
时,它指向一个5个元素的数组:
ns │ ▼┌───┬───┬───┬───┬───┬───┬───┐│ │68 │79 │91 │85 │62 │ │└───┴───┴───┴───┴───┴───┴───┘
执行ns = new int[] { 1, 2, 3 };
时,它指向一个
新的
3个元素的数组:
ns ──────────────────────┐ │ ▼┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐│ │68 │79 │91 │85 │62 │ │ 1 │ 2 │ 3 │ │└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
但是,原有的5个元素的数组并没有改变,只是无法通过变量ns
引用到它们而已。
如果数组元素不是基本类型,而是一个引用类型,那么,修改数组元素会有哪些不同?
字符串是引用类型,因此我们先定义一个字符串数组:
String[] names = { "ABC", "XYZ", "zoo"};
对于String[]
类型的数组变量names
,它实际上包含3个元素,但每个元素都指向某个字符串对象:
┌─────────────────────────┐ names │ ┌─────────────────────┼───────────┐ │ │ │ │ │ ▼ │ │ ▼ ▼┌───┬───┬─┴─┬─┴─┬───┬───────┬───┬───────┬───┬───────┬───┐│ │░░░│░░░│░░░│ │ "ABC" │ │ "XYZ" │ │ "zoo" │ │└───┴─┬─┴───┴───┴───┴───────┴───┴───────┴───┴───────┴───┘ │ ▲ └─────────────────┘
对names[1]
进行赋值,例如names[1] = "cat";
,效果如下:
┌─────────────────────────────────────────────────┐ names │ ┌─────────────────────────────────┐ │ │ │ │ │ │ ▼ │ │ ▼ ▼┌───┬───┬─┴─┬─┴─┬───┬───────┬───┬───────┬───┬───────┬───┬───────┬───┐│ │░░░│░░░│░░░│ │ "ABC" │ │ "XYZ" │ │ "zoo" │ │ "cat" │ │└───┴─┬─┴───┴───┴───┴───────┴───┴───────┴───┴───────┴───┴───────┴───┘ │ ▲ └─────────────────┘
这里注意到原来names[1]
指向的字符串"XYZ"
并没有改变,仅仅是将names[1]
的引用从指向"XYZ"
改成了指向"cat"
,其结果是字符串"XYZ"
再也无法通过names[1]
访问到了。
对“指向”有了更深入的理解后,试解释如下代码:
public class Main { public static void main(String[] args) { String[] names = {"ABC", "XYZ", "zoo"}; String s = names[1]; names[1] = "cat"; System.out.println(s); // XYZ }}
给数组赋值,本质是创建了新的引用。
•数组是同一数据类型的集合,数组一旦创建后,大小就不可变;•可以通过索引访问数组元素,但索引超出范围将报错;•数组元素可以是值类型(如int)或引用类型(如String),但数组本身是引用类型;
[1]
IEEE-754: https://web.archive.org/web/20070505021348/http://babbage.cs.qc.edu/courses/cs341/IEEE-754references.html