1. Java区分大小写;代码编写风格是自由的,只要满足语法要求即可
2. Java应用程序中的全部内容必须放置在类中。
Java是纯面向对象语言,对象是数据和操作的集合,所以所有的数据和接口定义都是在类(class)内的。
3.Java以;作为语句分隔符。
4.Java是强类型语言,每一个变量都必须声明一个类型。
a.Java变量可以是由字母或下划线开头,并且以数字字母和下划线随意组合来声明(但不能是Java保留字)。
b.字母范围除了英文字母外,也可以时其他语言中的字母,如希腊语中的α,如日文中的あ
c.数字除了0-9,也可以是其他语言中对应数字的字母,如I,II,III等。
可以使用Character.isJavaIdentifierStart和Character.isJavaIdentifierPart两个方法来判断某个字符是否是Java语言中可以用来声明变量的字母,其中start判断是否可以用作首字母。
注意:不要使用$作为变量命名,因为编译器等自动生成的类名等中包含了该符号,需要避免混淆。
声明变量如下:
int i;
short j;
- 第一个简单的Java程序
public class Demo {
public static void main(String[] args) {
String s = "Hello World !";
System.out.println(s);
}
}
1.main方法是Java程序的入口,写法是固定的。
2.某个文件中包含了一个public的类,其文件名必须和类名相同,并以.java结尾,即上述类对应的文件名是Demo.java。
下述的论述中“字节码命令的具体含义和操作过程”以及“内存模型的内容”请参考“学习JVM”
-
一、数据类型
-
1.1基本类型
| 基本类型 | 内存大小 | 最小值 | 最大值 | 包装器类型 | 默认值 |
|---|---|---|---|---|---|
| boolean | - | - | - | Boolean | false |
| char | 16 bits | Unicode 0 | Unicode - 1 | Character | '\u0000' |
| byte | 8 bits | -128 | +127 | Byte | (type)0 |
| short | 16 bits | -(-32768) | + - 1(32767) | Short | (short)0 |
| int | 32 bits | -(-2147483648) | + - 1(2147483647) | Integer | 0 |
| long | 64 bits | -(-9223372036854775808) | + - 1(9223372036854775807) | Long | 0L |
| float | 32 bits | Float | 0.0f | ||
| double | 64 bits | Double | 0.0d | ||
| void | - | - | - | Void | - |
上表中提到的初始值是指,对象被实例化时,实例变量(域、字段 —— field)会被赋予的默认值。
基本类型的数组在默认初始化时也是按此默认值赋值,见下“数组”一节。
特别注意:
1.基本类型的变量的值是存在在堆栈(stack)中的。
2.一个基本类型直接量必须被赋予一个变量或用于表达式中,否则编译器就会报错。
-
1.1.1整型
1.整型是指byte、short、int和long,其与运行Java的机器无关。并且整型都是有符号数。
2.整型可以有不同的表示方式:十进制(默认),十六进制(0x或0X),八进制(0),二进制(0b或0B,从Java 7),并且可以用“_”(下划线)来分割数字以便于阅读(从Java 7)。
3.给出一个数值(常量或字面量),其默认的类型是int,即占32 bits,但如果一个数值过大(超过了int的最大值),那么必须主动设置为long,即在数字后面加l或L。
否则,编译器会报错,内容为“过大的整数”。
一个数值字面量的默认类型是int,其深层次的原因是JVM命令中,对于整型的操作只有两种数据类型,int和long。
但是,在编译和代码层面,只要是byte和short的数值范围内,就可以直接可以赋值给对应的变量,如
byte b = 127;
short sh = 65535
int aInt = 3;
然而上述代码编译后,产生的字节码都是
iconst_1
istore_1
如果将超出范围的数值赋值给byte或short,会编译报错:“不兼容的类型: 从int转换到byte可能会有损失”。
如:byte b = 128;
另外整型的其他进制表示法也从侧面反映出默认的数值字面量是32位的int型,如
byte b_x = 0X7F; //十六进制
byte b_o = 0177; //八进制
byte b_b = 0B0111_1111; //二进制
上述通过不同的方式给byte类型的变量赋值都是允许的(都是127)。
但这里和负十进制可以赋值不同,十进制语句“byte b = -128”是允许的,根据8 bits的补码表示,十进制的-1等于二进制的1111_1111,但是不能编译如下语句:
byte b_b = 0B1111_1111; //尝试将8 bits的-1赋值给byte型
上述编译报错,int无法转换成byte。
要用二进制给byte赋值-1,需要以下语句:
byte a = 0b1111_1111_1111_1111_1111_1111_1111_1111;
即需要使用32 bits的补码形式。
另外二进制表示时数值范围超过的int类型的范围而不指定l或L时,报错内容为“过大的整数”,这和十进制的表述是一致的。
-
低于32的整型,在存储时其实都是32 bits的。
-
1.1.2 boolean类型
只有两个值:false和true,用来判断逻辑条件。其和数值型类型无法相互转换。其所占空间没有明确指定大小。
-
1.1.3浮点类型
1.浮点类型包括float和double,一个浮点类型的数值,默认就是double类型的。
2.要指定float类型的数值,需要在数值末尾添加f或F。
3.浮点类型遵循IEEE 754标准,采用二进制形式的科学计数法:float,首位符号位,2-9位是指数,10-32位是尾数;double,首位符号位,2-12位是指数,13-64位是尾数。
4.除了正常的小数表示外,可以使用两种科学计数法:
e(E)为底数:5.21e2或1.67e10(分别表示512.0和167.0),底数为10。
p(P)为底数:0X1.0p-2(0.25),底数为2,必须使用十六进制0x或0X。
5.浮点特殊值
- 正无穷大 Double.POSITIVE_INFINITY或Float.POSITIVE_INFINITY
- 负无穷大 Double.NEGATIVE_INFINITY或Float.NEGATIVE_INFINITY
- 非数(NaN) Double.NaN或Float.NaN
这三个特殊值是浮点数的特殊值,所以浮点运算才会产生的结果。其中任意正浮点数a:
a / 0 = Infinity or a / 0.0 = Infinity
-a / 0 = -Infinity or -a / 0.0 = -Infinity
0.0 / 0 = NaN or 0.0 / 0.0 = NaN
Math.sqrt(-4) = NaN //负数开方根
注意,非数NaN和任何值都不相等,也不等于自身,即NaN != NaN。可以通过Double.isNaN()或Double.isNaN()方法来判断一个数是否是NaN。
另外浮点数存在0.0和-0.0,但两者相等。
6.浮点数不能用于精确计算,因为其实用二进制的科学计数法表示的,某些小数只能无限逼近。精确计算可用BigDecimal。
-
1.1.4 char和String
-
1.1.4.1 char
1.char类型的大小为两个字节,即16 bits。其范围为'\u0000' ~ '\0uFFFF',共65536中表示。
2.定义变量时,将某个字符用单引号包起来,就是单个字符,如
char c = 'A';
3.Java中的char类型描述了UTF-16编码中的一个代码单元。
由于16 bits可以表示65536种可能,所以理论上char可以表示这么多数量的字符,但是现实生活中的符号已经超过这个数量,故需要编码让16 bits长度表示超出65536的部分。
Unicode标准中,码点使用十六进制表示,并加上前缀“U+”,如字母A的Java表示时'\u0041',其对应的Unicode的码点就是U+0041。
Unicode将码点分为17个代码级别(code plane)。
第一代码级别为基本的多语言级别(basic multilingual plane),代码点从U+0000 ~ U+FFFF。
剩余的16个级别的代码点从U+10000 ~ U+10FFFF,这些码点钟包含了一些辅助字符(supplementary character)。
UTF-16使用不同长度的编码表示所有的Unicode码点(code point,其实可以理解为“一个码点就是一个字符”)。
对于Unicode的第一代码级别,UTF-16用两个字节(即16 bits)来表示每一个字符,这个被称之为“代码单元(code unit)”。
而辅助字符则用一对连续的“代码单元”进行编码。
上述提到UTF-16用两个字节表示一个代码点,用此来表示第一级别的字符,
但是其中的U+D800 ~ U+DBFF和U+DC00 ~ U+DFFF不是用来直接表示字符的,这两个范围被称为辅助字符的第一部分和第二部分,即将第一部分和第二部分的码点编码,从而表达Unicode标准中的辅助字符。
这两个其余被称为“替代区域(surrogate area)”。
4.char类型与整型间的关系
char用两个字节表示,故其长度和short是一致的,但两者并直接赋值。
因为Java中的整型都是有符号的,而char表示的值却是从0 ~ 65536。所以理论上只有int和long符合。但是long又过长了。
对于
char c = 65535;
其字节码和赋值给short和byte一样,都是
iconst_1
istore_1
对于
int c1 = '\u01F3';
int c2 = '\u01F3';
字节码都是
sipush 499
istore_1
如果字符是ASCII中的字符,则第一个字节码改为“bipush ***”,其中***表示字符表示的数值。
—— —— —— —— —— —— —— —— —— —— —— —— —— ——
其实bipush和sipush分别表示将“-128 ~ 127”和“-32768 ~ 32767”推至栈顶。
但给byte和short赋值时并不是用的这两个字节码
5.转义序列
\u是通用的转义序列,其后跟一个4位十六进制数,例如 '\u0041'表示A
| 转义序列 | 名称 | Unicode |
|---|---|---|
| \b | 退格 | '\u0008' |
| \t | 制表 | '\u0009' |
| \n | 换行 | '\u000a' |
| \r | 回车 | '\u000d' |
| " | 双引号 | '\u0022' |
| ' | 单引号 | '\u0027' |
| \ | 反斜杠 | '\u005c' |
-
1.1.4.2 String
1.多个字符组成的就是一个字符串,在Java中没有将字符串定义基本类型,String是一个类(class),但有字符串直接量。如
String str = "ABC"; //这里使用"",单一的字符使用''
2.char类型中表示的代码单元都可以放在String直接量中,如
String str_1 = "\u0041\u0042\u0043"; //其就是字符串 "ABC"
3.char类型中的\u转义字符可以直接用于代码
public static void main(String\u005B\u005D args)
4.运算符+可以用来拼接字符串,另见2.10
"Hello" + 7 + 'a' => Hello7a
7 + 'a' + "Hello" => 104Hello
上述两种拼接结果不同,是因为+是自左向右结合的。
5.Unicode的辅助字符无法通过一个单一的char表示,所以无法用一个char来表示一个辅助字符。而String类型是可以打印出特殊字符的。
如果使用+运算符
String str = '\uD83D' + '\uDC8D';
并不能打印出特殊字符,而是打印出111818,即先计算了'\uD83D' + '\uDC8D'的数值,然后赋值给str。
但可以
String str = '\uD83D' + '\uDC8D';
或
String str = "\uD83D\uDC8D";
打印得到的是💍。
String底层其实就是用一个char[]来保存char,关于数组见引用类型。
注意str.length()返回2,故String的length()返回的不是字符(code point)的数量,而是返回的代码单元(code unit)的数量。
6.遍历字符串时,使用length()来进行字符(code point)数量的判断是不对的。
//\uD83D和\uDC8D分别是💍的high替换区和low替换区的代码单元
String aString = "\uD83DABC\uDC8D\uD83D\uDC8D";
//获取字符串对应的码点(code point)流,将流转成int[]数组
int[] ints = aString.codePoints().toArray();
//输出结果[55357, 65, 66, 67, 56461, 128141]
//其中单个不成对的替换区的代码单元仍旧直接输出
//组成Unicode辅助字符的“代码单元对”则以正确的码点int表示,如💍的码点是128141
System.out.println(Arrays.toString(ints));
//遍历int数组,重新将int型的码点(code point)包装成字符串,再单个输出
//输出结果为
/**
?
A
B
C
?
💍
*/
for(int x = 0; x < ints.length; x++) {
System.out.println(new String(new int[]{ints[x]}, 0 ,1));
}
//上述遍历也可以用如下方式
int length = aString.length();
//正向遍历,所以索引最小是0
int index = 0;
while (index < length) {
//codePointAt()方法会判断当前的索引位置与后一个索引位置是否是一个有效的“代码单元对”
//如果是单个的“替换区域”,则直接转成Unicode码点对应十进制int
int cp = aString.codePointAt(index);
//用isSupplementaryCodePoint()判断cp的int值所对应的码点(code point)是否是替换区域字符的码点 —— 简单的判断是否在1FFFF和10FFFF之间
if(Character.isSupplementaryCodePoint(cp)){
index += 2;
}
index++;
System.out.println(new String(new int[]{cp}, 0 ,1));
}
//循环结束时,index = length or index = length + 1
//下面的输出语句也是💍,\uD83D = 55357 \uDC8D = 56461
//所以该构造器中的第二个参数表示int[]的其实位置,第三个表示从int[]中取出的数量
System.out.println(new String(new int[]{55357, 56461}, 0 ,2));
//如果需要将字符串中的字符反转输出,一个简单的办法是将ints反转,然后输出
//ints中数据的反转需要自己处理,没有现成的Java方法
//另一种倒序的输出是用isHighSurrogate和isLowSurrogate
int length = aString.length();
int index = length - 1;
//index表示正在遍历的索引位置
while(index > -1) {
//用局部变量保存当前索引位置和前一个索引位置
int current = index;
int previous = index - 1;
//判断前一个索引位置是否存在
if(previous > -1) {
//在前一个索引位置存在的情况下,判断前一个位置和当前位置的代码单元是否可以组成一个“辅助字符”
//这里可以将此处的if与“previous > -1”合并,这样写思路更容易理解
if(Character.isHighSurrogate(aString.charAt(previous)) && Character.isLowSurrogate(aString.charAt(current))) {
System.out.println(new String(new int[]{aString.charAt(previous), aString.charAt(current)}, 0 ,2));
index -= 2;
} else {
System.out.println(new String(new int[]{aString.charAt(current)}, 0 ,1));
index--;
}
} else {
System.out.println(new String(new int[]{aString.charAt(current)}, 0 ,1));
index--;
}
}
//使用String.offsetByCodePoints(int index, int codePointOffset)可以获取偏移codePointOffset个字符(码点)的后一位索引
//其原理也是使用了isHighSurrogate和isLowSurrogate,同时实现正向和逆向的判断
//但是正向存在一个bug,
String aString = "ABCD\uD83D\uDC8DEFG";
//i是从8号索引位置开始,偏移一个字符(码点)后的索引
int i = aString.offsetByCodePoints(8, 1);
//输出9,但aString.length() == 9,所以索引最大是8,但这里返回了9
System.out.println(i);
//用此返回值去取字符(码点,调用codePointAt)或代码单元(char,调用charAt)时都会有运行时异常StringIndexOutOfBoundsException
System.out.println(aString.codePointAt(i));
//出现上述异常的原因在于,其代码实现中使用的是自增++,其有副作用,导致在逻辑判断时影响了用于方法返回结果的索引变量的值,对于上述的情况应该直接报错
注意,一般处理程序员之间的字符串用length()是没有问题的;而如果处理保存在数据库中的客户信息时,不能用length()来遍历,因为其中可能存在各种emoji表情等。
-
1.1.5 基本类型的相互转换
1.基本类型间,除了boolean类型,其余的数字类类型都可以相互转换:放大转换和缩小转换(强制转换)。下图表示了放大转换的方向,虚线表示可能存在精度丢失。
2.缩小转换(强制类型转换 cast)
强制类型转换即在需要的地方使用“()”,如
short s = (short) 0xffff; // s = -1
char c = (char) 123123123; // c = '\uB5B3'
int i = (int) 1.23; // i = 1;
byte b = 65;
char aChar = (char) b; //aChar = 'A'
char another = (char) 66.6; // another = 'B'
//不符合上述箭头方向的转换都是需要强制转换的
//其中整型和char之间的转换,都是先转换成二进制,然后再直接截取后面的相应位数
//如0xFFFF直接量是一个大于short范围的int值,其二进制是0000_0000_0000_0000_1111_1111_1111_1111,截取后16位,即1111_1111_1111_1111,此就是short=-1时的补码形式。
//即使放大转换没有途径,也可以强制转换,如上述的aChar
//浮点型转成整型时,小数部分直接截断,不会进行相关舍入,超出范围的也是截取相应的位数。
//浮点型转换成char型,可以看出先将浮点型转成整型,再转成对应码点的char型。
3.数值型和char型在进行二元算数运算时,整个计算结果会根据算术表达式中的最高类型转换。即
- 如果两个操作数中存在double,则另一个操作数会转换为double;
- 如果两个操作数中存在float,则另一个操作数会转换为float;
- 如果两个操作数中存在long,则另一个操作数会转换为long;
- 否则两个操作数都转换为int.
byte b1 = 1, b2 = 2;
short s1 = 3, s2 = 4;
short result1 = b1 + b2;
short result2 = s1 + s2;
short result3 = b1 + 1;
short result4 = s1 + 1;
short result5 = b1 + s1;
//上述代码都是错误的,编译器报错:
//java: 不兼容的类型: 从int转换到short可能会有损失
//上述可以进一步从侧面证明,整型的默认大小是int的
//但是区分数值字面量间的二元算数运算与上述示例的区别
byte b3 = 1 + 127; // 从int转换到byte可能会有损失
byte b4 = 1 + 126; // 这样的定义是可行的
//上述结果的原因在于,编译器可以对这样的赋值直接优化
//语句byte b4 = 127;和byte b4 = 1 + 126;产生的字节码都是一样的,都是
// bipush 127
// istore_1
//另外,整型运算的结果超过了范围,则直接回绕,而不是上溢或者下溢。如
byte b = (byte) 1 + 127; // b = -128
byte b = (byte)2 + 127; // b = -127
//这种回绕机制其实就是“补码”的优点
-
1.2 引用类型
在Java中,一切可被视为对象,但操作的标识符(变量)实际是一个对象的“引用”(reference)。
就像遥控器(引用)操作电视机(对象)一样,我们用遥控器来调节电视机亮度等,同样可以通过引用来达到改变对象状态的目的。
引用和对象并不是一一对应的,同一个引用(变量)可以在不同时刻操作不同的对象(引用的类型和对象可以对应即可),用一个对象也可以被不同的引用操作。这里其实就是万能遥控器和电视机的关系。
***Java中一切都是对象,引用是唯一可以操作操作对象的途径。***在表现上,引用分为两种:对象和数组。(其中数组也是对象,理解上其实是一致的)。
-
1.2.1 引用类型的默认值 - null
声明一个引用类型的变量,在给该变量赋值时必须是一个对象。但是如果不给该变量赋值,可以给指定其为null,表示该引用类型的变量还未明确的“引用”一个对象。和基本类型的默认值类似,引用类型的默认值也有默认值,是null。
实际上,基本类型和引用类型的默认值都是二进制零值。
-
1.2.2 Java中的类和对象实例化
关于“面向对象”的相关概念参考面向对象入门
public class Point { //定义一个类,名字叫Point,用来表示坐标轴上的某个点
private double x;
private double y; //分别用变量表示x,y来表示横坐标和纵坐标
public Point(double createX, double createY) { //构造器,用来创建一个Point对象
x = createX;
y = createY;
}
//两个get方法,分别用来返回x和y
public double getX() {
return x;
}
public double getY() {
return y;
}
//两个set方法,分别用来设置一个Point对象的x或y
public void setX(double newX) {
x = newX;
}
public void setY(double newY) {
y = newY;
}
public static void print(Point aPoint) {
System.out.println("x:" + aPoint.x + ",y:" + aPoint.y); //拼接语法可以参考“1.1.4.2 String”和2.10
}
public static void main(String[] args) {
Point p; // 声明了一个变量p,它的类型是Point,用来表示坐标轴上的某个点
p = null; // 将引用类型的默认值赋值给了变量p
p = new Point(1.0, 1.0); //创建了一个Point对象,并将这个对象赋值给p,也就是是说p引用了一个Point对象
Point anotherPoint ap = p; //重新声明了一个变量anotherPoint,类型同样是Point
print(p);
print(ap); //分别打印两个变量,可以看到打印结果相同
}
}
上面的打印结果在输出上似乎和下面代码是一样的
int a;
a = 1;
int b = a;
a = 2;
b = 3;
上述基本类型的声明和赋值方式在运行时的过程如下图所示:
1.声明变量a
2.将a赋值为1
3.声明变量b,同时将a的值赋值给b
4.a和b分别重新赋值
从图可以看出,在运行时a和b是两个完全不同的空间(即内存),后续的赋值时不相关的。
但是上述Point的例子在运行时如下:
1.声明变量p,类型为Point
2.给p赋值null
3.先通过new Point(1.0, 1.0)的形式,在堆上创建了一个Point对象,然后让变量p引用这个Point对象
4.声明变量ap,同时将p赋值给ap —— 注意,此时并不会赋值一个出一个新的Point对象,只是让ap也引用了在堆中的同一个对象 —— 所以在打印时相同的结果
注意:
1.根据上图,在打印前a和ap通过上述逻辑指向了同一个对象,由此可以推测在调用print方法时,也是分别将p和ap的引用赋值给了方法参数aPoint。
2.从运行过程看,对于“Point ap = p;”,无法改变p指向堆中对象的这一事实。引出的一个重点就是,此时如果在堆中再新建一个对象,让ap引用了新对象,但是不会影响p引用原来对象。
3.根据1和2可知,如果在方法print中,将参数aPoint重新指向一个新的堆中对象,其效果就是使得aPoint不再引用传入的对象,同样无法改变方法外部a和ap引用原来同一对象的事实。
//由于a和ap引用了同一对象,引用是操作对象的唯一途径,那么可以有以下调用
a.setX(2.0);
ap.setY(3.0);
print(a);
print(ap);
//打印结果仍旧是一样的,通过上图也很容易理解,因为a和ap操作的其实是同一对象
//所以引用变量之间的赋值或方法参数是引用时,其复制的不是对象本身,而是复制了引用,使变量引用了堆中同一对象。
“按值传递”可以理解为在进行参数传递时,是复制了一份放入入参中,此时给这个参数重新赋值并不会影响外部的值,例如
int a = 1;
f(a);
public void f(int x) { x = 2; }
上述伪码中,执行f时将x重新赋值了,那么在x=2;语句执行完后,x就是等于2,但是此时方法外部的变量a仍旧是1。
上述Point例子的分析可知,Java中如果方法的参数是引用类型,其也是一种“值”的复制,只是现在的“值”是引用 —— 引用在运行时也是一种用二进制表示的数值
Java中所有的参数传递方式是“按值传递”的。
而C和C++中还有一种“按址传递”,址就是指内存空间的位置
“按址传递”是将外部变量的值所在的空间位置传递给了方法,这种方式也可以使用一些特殊操作符来读取内存中的值。
但是当尝试修改时,同时会影响方法外部变量所代表的数据,类比就是:当a和ap通过“址传递”调用print后,在print内通过对aPoint操作,让堆中的原有对象回收,将内存释放出来(可以理解为清除了内存块所存储的数据),然后在相同的内存位置创建一个新的Point对象,由于方法print外的a和ap指向的地址未变,直接就是导致外部a和ap不再引用原来的Point对象
-
1.2.3 数组
1.数组是一种特殊的引用类型和对象(代码语法规则上的类型名称和在堆中创建的实例,都用“数组”这一术语)。
2.数组是一种数据结构,是存储同一类型的值的集合。—— 可以理解为“数组是若干个变量放在一起,并为该整体进行了命名”
3.Java的数组是固定长度的,在编译期已经决定,无法在运行时改变数组的大小(数组的长度和大小都是指该数组可以存放多少个值)。
//声明一个数组就是在一个具体类型后面加上[]
int[] aInts;
String[] aStrings;
Point[] aPoints;
//由于数组是引用类型,所以也可以被赋值为null,其含义和普通的引用变量一致
aInts = null;
//由于数组也是对象,所以数组实例的创建和类实例的创建是类似的 —— 关于实例和对象的关系可以参考“面向对象入门”
aInts = new int[5]; //创建了一个int[]的实例,其长度是5
//创建了数组实例后,可以理解为该实例在堆中开辟了内存空间,Java会为每一个数组元素自动填充默认值
//基本类型的默认值请参考上述“基本类型”
//而在C和C++中,并不会执行这种初始化
//另一种创建数组实例时,直接指定初始化的值
aInts = new int[]{1, 2, 3, 4, 5,}; // 元素5后面的,不是必须的
//这种初始化过程,是隐式指定了数组大小(即初始化值的数量),同时指定了数组的初始化值
//注意,两种初始化方式不能一起使用
// aInts = new int[3]{1, 2, 3}; 编译报错
//第二种初始化方式有一个简化版本
aInts = {6, 7, 8, 9, 10};
//注意,通过new来创建的实例在语法上就是创建了一个对象,对象可以是匿名的
public static void f(int[] ints) {}
f(new int[1]);
f(new int[]{1, 2}); //这两种调用方式都是可以的,引用类型作为方法参数的传值方式参考“上一节”
上述对于ints初始化的内存模型如下图(该图只是表明了数组作为特殊对象在堆中的最后表示,请阅读下述代码示例):
//每个数组都有一个length的属性,其表示数组的大小 —— 可以类比于Point类的x和y
System.out.println(ints.length); // 5
//数组类型通过“索引”访问某个位置的数据,索引从0开始,所以索引的最大值是length - 1
//数组其实是多个变量的一个集合,所以用索引访问后,可以向对待简单变量一样进行操作
int result = ints[0] + 1; // result = 7
ints[0] = 123; // 此时数组ints中的值是 [123, 7, 8, 9, 10]
//数组的长度最大是int的最大值,所以索引的值可以是byte、short、int和char
System.out.println(a[1L]); // 编译报错,要求int
byte b = 0;
System.out.println(ints[b]); // 输出0索引的值
//遍历
for(int i : ints) { System.out.println(i); }
for(int i = 0; i < ints.length; i++) { System.out.println(ints[i]); } // 参考“循坏”
//特别注意:数组的初始化是在运行时发生的
// 因为数组是对象,所以其是在运行时才在堆中被创建
ints = {1, 2};
ints = new int[2];
ints[0] = 1;
ints[1] = 2;
//上述两种方式,在字节码层面是一样的
//从而可知,在语法层面指定初始值也是先创建了数组实例,完成默认初始化,然后再将指定的值赋值在数组的相应位置
//其实这种语法方式只是在代码层面减少了遍历数组赋值的过程
//所以形如下述的方式也是可行的
double[] doubles = {Math.random(), Math.random()}; //方法Math.random()返回[0,1)的一个double随机数
double[] otherDoubles = new double[Random.nextInt(20)]; // 数组指定长度的初始化方式,其长度也可在运行时决定,更进一步说明组数的实例化是在运行时发生的
-
1.2.3.1 对象数组
// 引用类型的数据也可以用来定义数组
Point[] points = new Point[2]; // 定义了长度为2的Point数组
//上文提到,在创建数组的实例时,会在堆中分配开辟空间,为其赋予默认值,而引用类型的默认值是null
points[0] = new Point(1.0, 1.0);
points[1] = new Point(5.0, 6.0);
下图是对象数组运行时的模型:
-
1.2.3.2 多维数组
//由于数组是一个引用类型,既然是一个类型,那也可以用来定义数组
int[][] ints = new int[2][3]; //声明并初始化了一个2*3的二维数组,第一位长度为2,第二位长度为3
//类比于基础类型数组的取值方式
int[] one = ints[0]; // 将ints 0索引位置赋值给了变量one,类型是int[]
// one即使一个普通的数组,其的用法和遍历与上述没有任何区别
// 由于数组是引用类型,可以引用其他的数组
ints[1] = new int[]{1, 2, 3, 4}; // 此时ints不在是一个2*3的形式,由于数组是引用类型,所以根据内存模拟很容易理解
// 也可以直接取出多维数组中某个值
int value = ints[1][2]; // 首先获取第一维的1索引数组,然后通过[2]来获取第二维的2索引数值
//等效于
int[] another = ints[1];
int anotherValue = another[2];
//多维数组的初始化,指定长度的初始化,必须从左侧开始,根据引用类型的内存模拟可知,多维数组是一层层“向下”引用的
int[][] two = new int[3][];
int[][][] three = new int[8][][];
int[][][] three_1 = new int[2][5][];
//也可以指定初始化值 —— 其实是先初始化0然后根据指定值重新赋值
int[][] two_1 = {
{1, 2, 3},
{4, 5, 6, 7}
};
int[][] two_2 = new int[][]{
new int[]{0, 1},
{2, 3, 4}
};
注意:C和C++中数组是一个内存块,多维数组也是规则的内存块,是m*n的,但是由于Java中的数组是一种引用类型,所以是可以存在不规则的多维数组的。
-
二、运算符
-
2.1 优先级和结合性
1.优先级指定运算符执行的顺序,优先级高的先执行。
2.结合性是运算符的一个属性,如果表达式中有多个优先级相同的运算符,指定运算符的结合规则(由左至右或由右至左)。
副作用 —— 操作符作用于一个至三个操作数,生成一个新值;有一些操作符会改变操作数本身,这一情况即为“副作用”。具体可以参考下面的介绍。
-
2.2 赋值运算符
//在Java中赋值运算符使用“=”,把值存储在某个变量中
//上述关于基本类型的示例中所述的,就是赋值的过程
int i; //声明一个变量i
i = 10; //将10赋值给i
int j = 11; // 声明一个变量j,同时赋值11
i = i + j; // 将i与j相加,将结果赋值再赋值给i —— 这里的基本类型在运算时的提升参见“1.1.5 基本类型的相互转换”
//赋值运算符是有一个结果的,就像“+”是将两数相加,结果就是“和”,而赋值运算符的结果就是“赋予变量的值”
//所以 i = 10; 语句的结果就是10,故可以有如下的赋值方式,但是在可读性上有所欠缺
int a, b, c; // 同时声明a,b,c
a = b = c = 1; // 等同于 a = (b = (c = 1)),故三个变量存储的值都是1
-
2.3 算术运算符
//算术运算符有五个:+,-,*,/,%
//其中+,-,*的逻辑与数学是一致的,但如果操作数是浮点型,那么就存在精度问题(后一个操作数中有个小数位是9)
System.out.println(5.1000000000000000000000000000000000000001 + 5.1000000000000000000000000000000009000001); // 10.2
System.out.println(5.1000000000000000000000000000000000000001 - 5.1000000000000000000000000000000009000001); // 0.0
System.out.println(5.1000000000000000000000000000000000000001 * 5.1000000000000000000000000000000009000001); //26.009999999999998,5.1*5.1=26.01
//除法“/”和除余“%”在操作整型和浮点型时有一定差异
//如果被除数和除数都是整型,那么除法“/”是取商,而除余“%”则取余
System.out.println(19 / 5); // 3
System.out.println(19 % 5); // 4
//都是整型的情况下,除数不能为0,否则在运行时会报错:Exception in thread "main" java.lang.ArithmeticException: / by zero
//但是,请注意,/和%仍旧是有区别的
System.out.println(19 / -5); // -3
System.out.println(19 % -5); // 4
System.out.println(-19 % -5); // -4
System.out.println(-19 % 5); // -4
//除余%结果的正负情况只取决于第一个操作数,在浮点型的/和%也适用
//如果存在一个浮点数,那么就会将另一个操作数提升为对应的浮点数类型,参见“1.1.5基本类型的相互转换”和“1.1.3浮点类型”
System.out.println(19.0 / - 5); // 3.8
System.out.println(19.0 % -5); // 4.0
System.out.println(-19.0 % -5); // -4.0
System.out.println(-19.0 % 5); // -4.0
System.out.println(-19.0 % 0); // NaN,浮点型除余如果第二个操作数等于0(0或0.0或+0.0或-0.0)
//如果第一个操作数是无穷大,那么除余操作就是NaN
System.out.println(Double.POSITIVE_INFINITY % Double.NEGATIVE_INFINITY);
System.out.println(Double.POSITIVE_INFINITY % 1);
System.out.println(Double.POSITIVE_INFINITY % 1.0);
//0和0.0对任何数取余等于0或0.0
System.out.println(0 % Integer.MAX_VALUE); // 0
System.out.println(0 % Double.MAX_VALUE); // 0.0 这里是先将0提升为了double的0.0
System.out.println(0.0 % Integer.MIN_VALUE); // 0.0
System.out.println(0.0 % Double.MIN_VALUE); // 0.0
//非数NaN只有结果,没有常量,若需要第二个操作数是NaN,可以如下,结果全是NaN
System.out.println(1 / (Double.POSITIVE_INFINITY % 1.0));
System.out.println(1 % (Double.POSITIVE_INFINITY % 1.0));
System.out.println(1.0 / (Double.POSITIVE_INFINITY % 1.0));
System.out.println(1.0 % (Double.POSITIVE_INFINITY % 1.0));
-
2.4 位运算符
//分为按未操作符操作符和移位操作符
//这两种运算都用于处理整型二进制的单个bit,一般用于校验程序中的校验位,所以要求运算符两边的操作数都是整型
// 1.char型的数据也可以进行操作,只是会先转成对应的int型码点
// 2.操作byte和short的整型时,会想转换成int型
// 这里的char、byte和short转成int的情况在“1.1.5基本类型的相互转换”中已提到
//按未操作符
//1.按位与 & 2.按位或 | 3.非 ~ 4.按位异或 ^
//按位操作&、|和^就是讲两个操作数提升为同一级别(int或long),然后两个二进制数对齐
//对每一位进行对应操作,从而得到一个新值
//非按位运算是一个单目运算符,即将该整型的所有位都取反(包括符号位)
int i = 10; // 1010 省略前面28位0
int j = 1; // 0001 省略前面28位0
System.out.println(i & j); // &即对应位都是1时结果为1,结果为0000,即0
System.out.println(i | j); // |即对应位只要有一个是1时结果为1,结果为1011,即11
System.out.println(i ^ j); // ^即对应位不相同时结果为1,结果为1011,即11
System.out.println(~j); // 全部位取反,即1111_1111_1111_1111_1111_1111_1111_1110,负数是补码,即-2
//移位操作符
//1.左移 << 2.右移 >> 3.无符号右移 >>>
byte b = 12;
short s = 1222;
int i = 100_0000;
System.out.println(b << 2); // 48
System.out.println(s << 4); // 19552
System.out.println(i << 6); // 64000000
System.out.println(b >> 2); // 3
System.out.println(s >> 4); // 76
System.out.println(i >> 6); // 15625
//上述结果表明,移位操作不会后不会影响原来的操作数,也就是说它们是没有副作用的
//另外,有如下的规律
// 1.第二个操作数无论多大,只会根据第一个操作数的类型分别取后5位(int)或6位(long)
// 因为int的长度是32 bits = 2^5,64 bits = 2^6
// 表现就是 (b << 2) == (b << 34)是true,即34%32=2
// 或者 (aLong << 3) == (aLong << 67)
// 2.一个整型>>时,可以将该数先转成32 bits或64 bits的二进制表示,然后在二进制前
//添加若干1(负数)或0(正数),然后得到一个大于32 bits或64 bits的二进制,然后截取前
//32或64 bits,得到的即是>>结果
// 3.在精度不丢失时,一个正整型左移n位即原值 * 2ⁿ,右移n位原值 / 2ⁿ
// —— 无精度丢失是指左移时最高位(符号位)仍旧是0且有效位不丢失(移除的位上没有1);右移时有效位不丢失(移除的位上没有1)
// 4.在精度不丢失时,一个负整数左移n位即原值 * 2ⁿ,但右移不会有相同效果
// —— 无精度丢失是指,在左移过程中被移除的高位都是1;负数是补码表示的,所以右移出去的0和1都是有效数
// 5.一个正整型可以被左移成一个负整型,右移最小是0;一个负整型可以左移成正整型,右移永远是负数,最大是-1(负数是补码)
//无符号右移是指在右移过程中,高位全部用0替代,而非符号位
//所以一个负数发生无符号右移,一定是个非负数
//最高位都用0填补,会给人一种无符号右移一定是正整型的错觉
//byte、short是先转成int,然后再移位的,那么仍旧得到负数结果
byte b = -1;
b >>>= 10; // b仍旧是-1,因为-1的补码是1111_1111_1111_1111_1111_1111_1111_1111,无符号右移10后,后8位是1111_1111,结果仍旧是8 bits形式的-1
//这里的>>>=等效于 b = b >>> 10; 但该语句会有编译错误,int到byte会丢失
-
2.5 算术赋值和位运算赋值
//算数运算符和位运算符可以和赋值运算符结合形成新的运算符
//形式为 var op= value,其等效为 var = var op value,如
int i = 20;
int j = 10;
System.out.println(i += 5); // 15,等效于 i = i + 5,赋值表达式的值就是被赋予的值,即打印2
System.out.println(j -= 5); // 15,等效于 j = j - 5,赋值表达式的值就是被赋予的值,即打印0
System.out.println(i += j); // 30,等效于 i = i + j,赋值表达式的值就是被赋予的值,且上述语句中i和j的值已经改变,即打印30,此时i也被重新赋值为30
//从上述的结果可以看出,这些运算符是有“副作用”的,即会改变左侧操作数的值
//上文提到,基本类型会在计算式中向上提升
byte b = 1;
b = b + 1; //编译器会报错,从int到byte会丢失,
//这里运算结果虽然是2,在byte范围内,但仍旧需要强制转换
b += 200; //这种写法不会报错,由于b计算所得已经超过byte的范围,所以会被截断
//上面+=写法虽然不需要强制转换,更加简洁,但在实际应用中需要注意,需要评估程序是否允许溢出时截断;强制转换在某种程度上来说也是一种警告
//对于下面两种写法,其字节码命令是一样的
b += 200;
b = (byte) (b + 200);
//都是
0: iconst_0
1: istore_1
2: iload_1
3: sipush 200
6: iadd
7: i2b
8: istore_1
9: return
//仅从简单的语句的编译结果来看,+=并不会提高性能,且可能会让人忽略“溢出”问题
//注意:由于位运算符~是单目运算符,它不能和赋值=组合
-
2.6 条件运算符 - 唯一的三目运算符
//条件运算符形式如下
// boolean-expression ? value0 : value1;
//条件运算符的结果value0或value1。注意:value可以是一个具体的字面值,也可以是一个表达式
//另外,value为表达式时,value0和value永远不会被同时计算
//要求value0和value1的结果需要可以转换为同一种数据类型(基本类型和引用类型都可)
//条件表达式在一定程度上等效于if-else选择,但if-else执行时可以是某一个block,但条件运算符不支持多个语句
int i = 0;
int j = 1;
int result = i > j ? i : j; //该语句用来表示获取i和j中的大值
int result = i > j ? ++i : ++j;
System.out.println(result); // 2
System.out.println(i); // 0
System.out.println(j); // 2
i = 4;
j = 3;
result = i > j ? ++i : ++j;
// 第二和第三操作数也可以是其他表达式,如“x += 5”
// 也可以是一个其他的int(byte/short/char)的变量
// **只要表达式的值和变量的值能赋值给result即可**
System.out.println(result); // 5
System.out.println(i); // 5
System.out.println(j); // 4
-
2.7 递增和递减
//递增和自减是单目运算符,其表示让操作数自增1或自减1,等效于+=1和-=1
//操作数的类型可以是整型、浮点型或char型(对应的包装类也可)
//由于递增和递减是有副作用的,所以其无法作用于一个具体的表达式
//它们可以对变量和数组元素操作
int i_1 = 1
int i_2 = 2;
++i_1++;
++++i_1;
i_1++++;
(i_1 + i_2)++;
/*
这些使用方式都是错误的,编译器报错
java: 意外的类型
需要: 变量
找到: 值
由此可知,自增和自减的操作数只能是变量,不能是一个值,而表达值本身是有“值”的
*/
int[] ints = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int i = 0;
System.out.println(ints[i]); // 1,即0位置的值
i++; // i自增,即i变为1
ints[i]++; //即将数组1位置的数递增1
System.out.println(Arrays.toString(ints)); // [1, 3, 3, 4, 5, 6, 7, 8, 9],1位置的值变为了3
//自增与自减的副作用都是使操作数自增或自减,但何时自增或何时自减是用操作符的位置确定的
ints[i++] = 100; //该语句的效果是先使用i的值,然后让i自增
System.out.println(Arrays.toString(ints)); // [1, 100, 3, 4, 5, 6, 7, 8, 9]
System.out.println(i); // 2 所以现将1位置赋值为100,然后i自增为2
ints[++i] = 100; //该语句的效果是先让i自增,然后使用i的值
System.out.println(Arrays.toString(ints)); // [1, 100, 3, 100, 5, 6, 7, 8, 9]
System.out.println(i); // 3,因为是i先自增,所以是将3位置赋值为100
//这里就会产生一个疑惑:
// 1.《疯狂Java》和《核心技术I》中的优先级都是 [] > ++ > =
// 2.《Java技术手册》中 [] = 后++ > 前++ > =
// 通过上述的赋值过程来说,1中笼统的将[]的优先级高于++有点模糊
// 而2中将[]的优先级与"后++"等同,在ints[i++] = 100;上根据从左至右的结合性看好像没问题
// 但是其将[]和"后++"的优先级高于"前++"后不符合上述的赋值逻辑
// 所以一个可行的方案是,优先级++ > [],而++的副作用是其自身的问题,和[]无关
// 另外在《Java 8 语言规范》3.11和3.12中,将[]归为“分隔符”(Separators),而++为“操作符”(Operators)
// 根据下述情况6和7,的确会有一种[]优先于++的假象,但应该清楚这两种符号的性质是不同的
//*******************下面的使用方式需要特别注意*****************
//情况1:
ints[i++] += 2;
System.out.println(Arrays.toString(ints)); // [1, 100, 3, 102, 5, 6, 7, 8, 9]
System.out.println(i); // 4,因为是i先使用,所以是将3位置加上2,即102
//情况2:
//根据 += 运算符的等效性,ints[i++] += 2; => ints[i++] = 2 + ints[i++];
ints[i++] = 2 + ints[i++];
System.out.println(Arrays.toString(ints)); // [1, 100, 3, 102, 8, 6, 7, 8, 9]
System.out.println(i); // 6,因为i有两次++,所以i=6; 根据从左至右的结合性来说,先计算=左边的++,从而上述的等效是 ints[4] = 2 + ints[5];
//情况3:
ints[i++]++;
System.out.println(Arrays.toString(ints)); // [1, 100, 3, 102, 8, 6, 8, 8, 9]
System.out.println(i); // 7,根据从左至右的结合性来说,先计算i++,从而上述的等效是 ints[6]++
//情况4:
ints[++i]++;
System.out.println(Arrays.toString(ints)); // [1, 100, 3, 102, 8, 6, 8, 8, 10]
System.out.println(i); // 8,根据从左至右的结合性来说,先计算i++,从而上述的等效是 ints[8]++
i = 0; //i重新赋值
//情况6:
++ints[i++];
System.out.println(Arrays.toString(ints)); // [2, 100, 3, 102, 8, 6, 8, 8, 10]
System.out.println(i); // 1,从结合性来说,应该先计算ints某个元素的自增,但是这个元素具体是哪一个又和i的自增性有关,结果等效是 ++ints[0],从而可以认为[]虽然不是操作符,但其作为分隔符作用优先于++
//情况7:
++ints[++i];
System.out.println(Arrays.toString(ints)); // [2, 100, 4, 102, 8, 6, 8, 8, 10]
System.out.println(i); // 2,与上述同理,从而上述的等效是 ++ints[2]
//情况8:
//自增等效于 +=1,如果将++转为等效的运算方式,是和情况2一致的
-
2.8 逻辑运算符
//逻辑运算符的操作数必须是boolean类型的变量或值为boolean的表达式
//逻辑操作符的值就是两个boolean运算后的结果,所以形如 firstBoolean && secondBoolean && thirdBoolean也是可行的
// 短路 —— 条件与“&&”、条件或“||”,这两种操作符会视情况决定是否计算第二个操作数
// 不短路 —— 逻辑与“&”、逻辑或“|” 逻辑异或“^”,两个操作数都会进行计算
// 1.“与”即两者都是true时结果才为true;2.“或”即两者只要一个为true结果就是true;3.“异或”就是两者不同是结果为true。
//假设给出一个组数int[] ints,是否初始化未知,要求获取i位置的数值,为了程序的健壮性,有如下判断:
//if(ints已经引用一个数组对象 并且 i小于数组ints的长度 ) { ... }
//上述假设中的逻辑需要用到与操作,可以有两种形式
if(ints != null && i < ints.length) {
...
}
//这里用逻辑与“&”是不行的,假设ints未初始化,那么ints != null是false
//根据与运算的逻辑,整个表达式已经确定是false了,所以i<ints.length的结果已经无关紧要
// 条件与“&&”就由此判断不需要计算第二个表达式;
// 逻辑与“&”要求两个操作数都需要被计算,那么仍旧会计算第二个操作数的值,但ints未初始化,会在运行时报异常:NullPointerException
if(ints != null & i < ints.length) {
...
}
//逻辑非“!”是一个一元操作符,其表示取反
//逻辑与“&”,逻辑或“|”和逻辑异或“^”和位运算符的一些操作虽然用的是同一个操作符,但根据操作数的操作类型可以很好的区别出表达式的含义
//根据逻辑异或“^”的逻辑,两个操作数不同时结果返回为true,那么骑在运算结果上等效于“!=”
-
2.9 比较运算符
// >= <= > < 这四种比较运算符比较简单,其就是数学层面的数值比较
// 但是仍旧需要注意,浮点型具有精度的问题,所以如果判断浮点数大小需注意和避免
System.out.println(5 < 5.0000000000000000000000000001); // false,精度问题
//另外浮点数还有其他的比较问题,如0.0 == -0.0为true,非数NaN和任何数都不相等
// == 和 != 需要区别对待,如果比较的两个操作数是基础类型,其就是比较操作数是否相等(boolean类型的值也可以使用)
//如果用==和!=比较两个引用类型的数据是否相等,那么其表示的是两个操作数是否指向了同一个对象
//即使两个对象(a和b)的所有成员变量都是一致的,两个对象间分别用A和B引用(A = a; B = b;),
//用A==B比较总返回false,A和B指向的是不同的对象,只有B = a;时A==B才是true。
//**用==和!=比较引用类型的数据时,两个引用必须是同一类型(接口或类,关于类型的表述参考“对象入门”)**
//另外,需要注意的是,String或Integer(int)这样的包装类,在用==比较时可能会产生一些困惑的行为
//下面是5个Integer对象
Integer i_1 = Integer.valueOf(1);
Integer i_2 = Integer.valueOf(1);
Integer i_3 = new Integer(1);
Integer i_4 = Integer.valueOf(128);
Integer i_5 = Integer.valueOf(128);
System.out.println(i_1 == i_2); // true
System.out.println(i_1 == i_3); // false
System.out.println(i_4 == i_5); // false
//通过new实例化的对象都是在堆中的独立对象,所以i_1 != i_3;
//Integer有一个缓存机制,默认缓存-128~127的Integer型对象,通过valueOf时,如果是范围内的值,返回的是保存在静态变量中的同一个对象,
//所以上述i_1 == i_2但是i_4 != i_5
// 另外经过自动包装,将-128~127的数值字面量赋给Integer引用,其得到的也是缓存的Integer对象
//关于String字面量相关的判断请参考“Java面向对象”
-
2.10 =和+=用于拼接字符串
//可以用+运算符拼接字符串
//由于运算符+的结合性是从左向右
System.out.println("Hello" + 7 + 'a'); // Hello7a
System.out.println(7 + 'a' + "Hello"); // 104Hello
//从输出可以看出,拼接字符串时,+后面的的操作数都会被转换成字符串
-
2.11 操作符优先级和结合性
| 运算符 | 结合性 |
|---|---|
| ++ -- ~ ! +(取正) -(取反) | 从右向左 |
- / % | 从左向右 +(加号) -(减号) | 从左向右 << >> >>> | 从左向右 < <= >= > instanceof | 从左向右 == != | 从左向右 & | 从左向右 ^ | 从左向右 | | 从左向右 && | 从左向右 || | 从左向右 ?: | 从右向左 = += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左
//赋值语句的从右向左结合性很好理解,即先得到=右边的值,然后赋值给左边的变量,赋值语句的值就是=右边计算所得到的的值
//一元运算符一般不会操作同一个操作数,但也可以通过特殊的举例来说明
int i = 111;
System.out.println(~i); // -112
System.out.println(-~i); // 112
System.out.println(-i); // -111
System.out.println(~-i); // 110
//但条件运算符“?:”,通过例子看不出从右向左的结合性
int i = 1;
int j = 2;
int result = ( (i+=10) < (j+=8)) ? (i *= 10) : (j *= 20);
System.out.println(result); // 200
System.out.println(i); // 11
System.out.println(j); // 200
//从右向左计算理解上应该是先算后面的表达式 i*=10和j*=20,此时i=10和j=40(此时第二操作数的值=10,第三操作数的值=40);
//然后再计算i+=10和j+=8,即i=20和j=48,则条件成立,result=10
//但根据输出可知,先计算i+=10和j+=8,则i=11和j=10,条件不成立,计算第三操作数得200 —— 条件运算符的第二操作数和第三操作数不会同时执行,见2.6
-
三、流程控制
-
3.1 变量作用域
block —— 若干个语句可以被一对大括号{}包含起来。
从最简单的Java程序中可以看出,一个类就是一个block,一个方法就是嵌套在类中的block。
block确定了变量的作用域(scope)。
例如如下
{
....
{
....
}
....
}
在同一作用作用域中,变量名称唯一。 但是,更深层次的block中的变量名称是否能与外部相同视情况而定。
1.实例域与局部变量可以相同,具体参考“Java面向对象语法知识”。
2.某个方法中的局部变量见下述举例。
举例如下:
public class Test {
private int aInt; //实例域
//方法内部中定义的变量都是局部变量
public static void f() {
int aInt = 1; // 可行
// int aInt = 2; 这里不能重复定义
//内部block —— A
{
// int aInt = 2; 这里不能这么定义
int second = 3; //可行
}
int second = 4; //可行
// 当方法f()自上至下运行时,运行到内部block A时,
// 前面已经定义的aInt,在A中认识可见的,不不能再次申明一个相同的变量aInt;
// 但是,在A执行完后,退出了block,此时A中的second变量已经被回收,故下面仍旧可以再次定义second。
}
}
-
3.2 条件语句
-
if语句
if(condition) {
...
} else if(another condition) {
...
} else {
...
}
1.if语句表示一个分支选择,条件满足时执行相应的代码块。
2.后续的else和else-if是可选的。
3.每个else都是前一个if的取反,这是个隐藏条件。故需将范围小的判断条件放在前面。
if(age > 20) {
... // block one
} else if(age > 40) {
... // block two
} else if(age > 60) {
... // block three
}
one的条件是age > 20
two的条件是age <= 20 && age > 40
three的条件是20 < age <= 40 && age > 60
从上可是,two和three的条件永远不成立。如改为
if(age > 60) {
... // block one
} else if(age > 40) {
... // block two
} else if(age > 20) {
... // block three
}
则正确。
one的条件是age > 60
two的条件是40 < age <= 60
three的条件是 !(40 < age <= 60) && age > 20
---> (age <= 40 || age > 60) && age > 20
---> 20 < age <= 40 (age > 60 && age > 20就是age > 60,one已经满足)
-
switch语句
1.switch语句表示值选择,该值的数量是确定的。 2.选择的类型可以是byte、short、int、char、String和enum(枚举)。
switch(choice) { // choice是某一个变量名或者符合上述类型的一个表达式或方法调用
case value_1:
...
break;
case value_2:
...
break;
case value_3:
...
break;
default:
... // 该选择表示默认,即不符合上述任何一个值,则执行此处
break;
}
例如:
int i = 1;
int j = 3;
switch(i) { // 也可以是 switch(i + j)
case 1:
...
break;
case 2:
...
break;
case 3:
...
break;
default:
...
break;
}
如果去掉break,那么程序就会将符合值后续的代码全部执行,知道再次碰到break。
int i = 1;
switch(i) {
case 1:
...
// break;
case 2:
...
break; // A
case 3:
...
break;
default:
...
break;
此时i=1,所以符合“case 1”,故一直会执行到A结束。
实际应用上,假设应用系统中一共有三个状态:a、b、c,三个情况分别执行如下
if(state == a) {
f();
g();
h();
} else if(state == c) {
g();
h();
} else if(state == c) {
h();
}
此时就可以使用去掉break的选择方式
switch(state) {
case a:
f();
case b:
g();
case c:
h();
}
上述的switch调用会显得不好理解,但相比较if中的多次调用,switch的写法更加简洁。
使用switch时,还有一种方式,即
switch(state) {
case a:
case b:
f1();
f2();
break;
case c:
g();
break;
default: break; //其实default中的break是可以省略,加上break只是编码风格的差异
}
表示在a和b的情况下,执行相同的逻辑
-
3.3 循坏语句
即在满足条件时,重复执行某个block,直到条件不满足。
从中可知,循坏语句存在三个部分:1.循坏条件;2.循坏体;3.使循坏条件为false的迭代语句。
-
3.3.1 while
while(condition) {
some java sentence;
iteration;
}
int i = 0;
while(i < 3) { //循坏判断条件
System.out.println(i);
i++; //迭代语句,i自增
}
//循坏输出0,1,2
-
3.3.2 do-while
do {
some java sentence;
iteration;
} while(condition); //最后的“;”不能少,语法要求
int i = 0;
do {
System.out.println(i);
i++; //迭代语句,i自增
}
while(i < 3);
//循坏输出0,1,2
//从上述输出可知,在初始条件和迭代条件一直的情况下,while和do-while的效果是一致的
//而两者区别在于,do-while是先执行方法体,所以无论条件语句是否正确,一定会有一条输出;而while则先判断条件
-
3.3.3 for
for(java init sentence; condition; iteration) {
some java sentence;
}
//for循坏可以看出是一种while的变体,其用两个“;”分开三个语句,整体逻辑仍是condition=true则执行循环体,执行迭代语句
for(int i = 0; i < 3; i++) {
System.out.println(i);
}
// System.out.println(i); 编译报错,未定义的i
//该for循坏等同于上述的while和do-while循坏
//输出的结果是0,1,2,
//这里是do-while和while的明显区别是,变量i在循坏结束后不能被访问。
//for循坏中的两个“;”是必须的,但语句可以没有,如果不存在则表示一个死循环,当condition不存在时,默认为true。
//上述可以转换成
int i = 0
for( ; i < 3; i++) {
System.out.println(i);
}
System.out.println(i); //此时i可以访问,输出3
//从而可知,for循坏中的“循坏计数器i”只在for内有效,这个和上文中的block作用域不同,是特殊的情况。
//那么可以让多个不同的for循坏的计数器变量名相同,因为它们在不同的作用域
//将“java init sentence”提到for外面,则循坏计数器i的范围就会变大
//请注意,for循坏的循坏计数器的初始化、检测和迭代都应该放在()内。
//另外一方面,for不应该在循环体中再次操作循坏计数器
//另外for循坏中,多个循坏计数器的初始化和迭代可以放一起
for(int i = 0, j = i + 10; i < 3 && j < 10; i++, j+=2) {
}
//上述的for循坏完全可以用while替换,for循坏是将循坏计数器的定义和迭代提到了()内
循坏计数器不要使用浮点型,因为浮点型有精度问题,可以永远达不到condition=false的情况
//该循环可能会是死循环,这个JVM运行有关,如果循坏计数器的范围更大,出错的可能越大
for(double i = 0.0; i < 10.0; i+=0.1) {
}
-
3.3.4 foreach
在Java 5中增加的语法,其用于遍历一个数组或集合中的每一个元素,其没有循坏计数器。(如果是集合,就要集合类实现Iterable,见集合相关内容)
//循坏strings中的每一个元素 —— for each element in strings
String[] strings = {"a", "b", "c", "d", "e"};
for(String s : strings){
System.out.println(s);
}
//foreach中的变量s是一个临时变量
//用它来表示在遍历中,数组中的当前值或集合中的当前元素,这里就是某一个字符串。
//对变量s重新赋值,并不会影响strings的本身的数据,这是“按值传递”的另一个证明
//foreach无法做到:
// 1.反向迭代数组或集合中的元素
// 2.使用同一个循坏计数器取出两个数组或集合同一个位置的元素
-
3.3.5 嵌套循坏
循坏的嵌套很简单,就是一个循坏作为另一个循坏的循坏体。
for(int i = 0; i < 5; i++) {
System.out.println(i); // A
for(int j = 0; j < 5; j++) {
System.out.println(j); // B
}
}
//上述循坏中,A执行5次,B执行25(5 × 5)次。
-
3.4 跳转
break和continue关键字都可以用来控制程序流程,break已经在switch选择语句中提到过,这里我们来看看这两个关键字在循坏中的作用。
-
3.4.1 break和continue
//break在循环中用于退出循环 —— 不执行当前循坏的循环体中的后续代码,且可以在循坏计数器还满足condition时退出循坏
int i = 0;
while(i < 10) {
if(i % 8 == 0) {
break;
}
System.out.println(i);
i++;
}
System.out.println(i); // i = 7
//上述输出结果是1,2,3,4,5,6,7
//在i == 8时循环break,退出了循环,并且循环计数器是8
//等效于
int j = 1;
for(; j < 10; j++) {
if(j % 8 == 0) {
break;
}
System.out.println(j);
}
System.out.println(j);
//continue在循环中用于用于结束当前循环 —— 其会再次判断condition,为true时再次执行循环体
int i = 1;
while(i < 10) {
if(i % 8 == 0) {
continue;
}
System.out.println(i);
i++;
}
System.out.println(i);
//上述在输出1,2,3,4,5,6,7后陷入死循环,因为i==8时尝试跳出本地循坏,但一直满足condition条件
//修改如下,输出1,2,3,4,5,6,7,9
int i = 1;
while(i < 10) {
int tep = i; //用局部变量存储循坏计数器的值
i++; //迭代
if(tep % 8 == 0) { //判断
continue;
}
System.out.println(tep);
}
System.out.println(i); // i = 10
//等效for循坏如下
int j = 1;
for(; j < 10; j++) {
if(j % 8 == 0) {
continue;
}
System.out.println(j);
}
System.out.println(j);
//上述for循坏可以正确输出,并不会形成死循环
//所以continue在while,do-while和for中有不同表现
// 在while和do-while中,如果continue语句在迭代语句之前就会形成死循环
// 而for循坏在结束当前这次循坏时,会先执行迭代语句,在判断condition
-
3.4.2 return
//return有两个两个方面的用途
// 1.表示方法已经结束,退出该方法
// 2.在需要返回值的方法中,返回该返回值,例如返回一个Shape的面积
//return可以放在上述break和continue的位置上,
//其不仅会退出循坏,更会退出方法,所以循坏的输出循坏计数器值的语句也不会被执行
//其中return可以在方法内部的任何地方,比如出行异常时直接返回一个非法值(如-1),表示调用异常
//对于方法有返回值的情况,方法体内部一定要有return语句(即使返回值是非法值)
//所以如果在分支判断中,不同的condition,方法返回不同的值,那就要确保所有的block都有返回语句,或者在分支外部、方法末尾添加默认的return语句。
-
3.4.3 标签语法
//标签语法主要用于嵌套循坏中,和break和continue结合使用,用来控制循坏
outer: //给外部循坏打个标签,可以理解为给循坏命名
for(int i = 0; i < 5; i++) {
if(i == 1) {
continue;
} else if(i == 3) {
break;
}
System.out.println(i); // A
for(int j = 0; j < 10; j++) {
if(j == 1) {
continue;
} else if(j == 5) {
break;
}
System.out.println(j); // B
}
}
//上述循坏的输出是 i=0,j=0,2,3,4; i=2,j=0,2,3,4
//所以break和continue默认是和本身的循坏绑定的
//若将上述j的for循坏中,修改为
if(j == 1) {
continue outer; //continue到outer标注的循坏
} else if(j == 5) {
break outer; //退出outer标注的循坏
}
//修改后的输出 i=0,j=0; i=2,j=0
//上述的输出可以看出,break outer语句不会被执行,因为j == 1时已经结束i的当前循坏
//上述讲到for循坏中,使用continue时,迭代语句仍旧会被执行,
//但是在continue outer;语句执行后,不会再执行j++。
//嵌套循坏其实就是一个循环作为另一个循环的循环体的一部分,那么内部循坏怎么执行,其都是外部循坏的一个普通语句,当continue outer;执行时,是直接回到了i的for循坏。
//break和continue的运行机制和不打标签是一致的,区别是处理的是哪一层循坏
//break的标签形式可以与if合用
int i = 0;
outIf:
if(i < 5) {
System.out.println(i);
i++;
if(i < 5) {
break outIf; // 不能使用continue,编译器会报错“不是 loop 标签: outIf”
}
System.out.println(i);
}
// 上述代码似乎会在break outIf;语句执行后,跳转到外层的if判断
// 分析上似乎会形成一个循坏,但并不会,执行break outIf是直接退出了if判断
// 这和break在循坏和switch的效果是一致的,直接退出
// 上述的字节码命令是(代码直接放在main中)
0: iconst_0
1: istore_1
2: iload_1
3: iconst_5
4: if_icmpge 32
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: iinc 1, 1
17: iload_1
18: iconst_5
19: if_icmpge 25
22: goto 32
25: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_1
29: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
32: return
//可以看到,在22:的命令中,goto的参数是32,意味着直接跳转到32::处,即直接return。
//如果去掉最后的一个打印语句,其字节码后半段是
19: if_icmpge 22
22: return
//即此时即使有break语句,但在编译后不会生成goto命令
//将外层的if改为while且去掉最后的打印语句,其字节码命令是
0: iconst_0
1: istore_1
2: iload_1
3: iconst_5
4: if_icmpge 25
7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
14: iinc 1, 1
17: iload_1
18: iconst_3
19: if_icmple 2
22: goto 25
25: return
//可以看到,“19: if_icmple 2”即if为true时,跳转到2,否则调到“22: goto 25”