由于时间原因,不能像讲课一样给大家一一列出所有的要点,故在此篇博客中,仅记录一些个人之前忽略的点和常见易错点,将不展开全面介绍,各位读者可以当做闲暇阅读,查漏补缺。
第三章 Java的基本程序设计结构
3.4 变量
- Java变量名是字母、’_‘或’$'开头的字母数字串,没有长度限制,大小写敏感。$常用于编译器或其他工具生成的名字中,通常不使用。
- 变量名最好不要只存在大小写上的差异。
- 访问权限修饰符、static修饰符、final修饰符位置都可以随意交换,但通常为
public static final DataType varName;
3.4.1 变量初始化
- Java中声明变量后必须进行确定的显式初始化,否则无法通过编译。
- Java可以将变量声明放在代码中任意位置,但变量的声明应尽可能靠近第一次使用的地方,这是一种良好的程序编写风格。
- Java不区分变量的声明和定义。所有的Java变量声明都会开辟空间,而C/C++只有定义时会开辟空间,声明不会。
3.4.2 常量
- 关键字
final
表示该变量只能赋值一次,一旦赋值便不可修改。习惯上,常量名使用全大写+下划线。 - Java常常希望某个常量在一个类的多个方法使用,称之为类常量。
- const是Java保留的关键字,但目前并没有使用。
3.5 运算符
- 整数除零会产生
java.lang.ArithmeticException: / by zero
异常;浮点除零会产生无穷大或NaN结果。 - Java为了实现可移植性,需要定义统一的运算规范。但问题是,例如double计算,很多Intel处理器会将中间计算结果保存在80位的寄存器中,这无疑会导致精度的增加。Java中默认允许对中间结果采用扩展的精度,如果不允许,要使用
strictfp
关键字标记该方法。
3.5.1 数学函数与常量
常用的数学函数都包含在Math
类中,可以直接调用。
- 例如:
Math.sqrt(x)
求平方根;Math.pow(x,a)
求xa;Math.floorMod(x, y)
求非负余数等。 - Math类也提供了常用的三角函数:sin/cos/tan;以及指数函数exp/log/log10等;还提供了π和e的近似值:
Math.PI
和Math.E
。
注意:
- Math类为了达到最快性能,所有方法都使用本地计算机浮点单元中的例程(机器有关)。如果要在所有平台上得到相同结果,要使用StrictMath类。
3.5.2 数值类型之间的转换
当有类型转换不会造成数据溢出(Java允许丢失精度)时,或使用结合赋值运算符时,Java会根据计算需要自动进行类型转换。
- int -> float、long -> float、long -> double 可能丢失精度
- 二元操作需要将两个操作数转为同一类型。类型转换规则:
- 如果有一个数是double,则另一个转double。
- 如果有一个数是float,则另一个转float。
- 如果有一个数是long,则另一个转long。
- 如果有一个数是int,则另一个转int。
3.5.3 强制类型转换
当类型转换可能造成数据溢出从而损坏数据时(结合赋值运算符除外),Java不会自动进行类型转换。这时,就需要用小括号进行强制类型转换,强制类型转换会直接截断,不会进行舍入运算,而且如果超过目标类型的表示范围,就可能产生一个完全不同的值。
可以调用Math.round(x)产生一个四舍五入的long类型的值。
注意:
- 不要在boolean类型与任何其他类型之间进行强制类型转换!如有需要,可以使用表达式
b? 1 : 0
。
3.5.4 结合赋值和运算符
注意:
- 如果运算符得到的值与左侧操作数类型不同,会发生强制类型转换。例如x是一个int,而后调用
x += 3.5;
是合法的,这等价于x = (int) (x + 3.5);
。
3.5.5 自增与自减运算符
注意:
- 自增自减运算符的操作数是变量不能是常量。
- 建议不要在表达式中使用++,这样的代码阅读起来比较困难,且易错。
3.5.6 关系和boolean运算符
- !、==、!=、>、<、=、>=、<=
- 常常利用&&和||运算符的短路特点来避免出错。例如
if(obj != null && obj.xxx);
。 - Java支持三元操作符
?:
。
3.5.7 位运算符
处理整型类型时,可以直接进行位操作。位运算符包括:
- &,与
- |,或
- ^,异或
- ~,按位取反
- <<,算数(逻辑)左移(低位填零)
- >>,算数右移(高位填充符号位)
- >>>,算数右移(高位填充0)
利用掩码技术可以得到整数中的各个位。例如:int forthBitFromRight = (n & 0b1000) / 0b1000
或int forthBitFromRight = (n & (1<<3)) >> 3
。利用&并结合使用2的适当的幂,可以将其他位“掩盖掉”,从而只保留某一位。
注意:
- 将
&
和|
应用在boolean上时,也会得到一个boolean值,但是不会采用短路的方式,左右的表达式都需要计算。 - >>>会用0填充高位,>>用符号位填充高位。如果做除以2的操作,应该使用算数右移。
- 在C/C++中,>>是进行逻辑右移(通常强转为unsigned类型然后右移从而保证是高位填0)还是算数右移(通常是算数右移)依赖具体实现,而Java则消除了这种不确定性。
3.5.8 括号与运算符级别
- 分不清时候可以使用
()
运算符 - Java不使用
,
运算符。不过可以在for语句的第1部分和第3部分使用逗号分隔表达式列表。
3.5.9 枚举类型
- 枚举类型是一种自定义的类型,枚举类型的声明有点像类的声明。
- 枚举类型只包括有限个命名的值。
3.6 字符串
Java中,字符串的本质就是一串Unicode序列。Java没有内置的字符串基本类型,而是在Java类库中提供了一个预定义类java.lang.String
3.6.1 子串
String对象可以调用其substring(beginIndex, endIndex)
方法获得子串(起始下标为0)。
注意:
- substring的第二个参数是不想复制的第一个位置。
- 这样做的优点是容易计算子串的长度。长度 = endIndex-beginIndex。
3.6.2 拼接
- String 可以使用
+
拼接字符串。当一个字符串与非字符串拼接时,后者将被转换成字符串。 - 如果要使用分隔符拼接一个字符串数组,可以使用
String.join("/", “s”, “m”, “l”, “xl”)
方法。
3.6.3 不可变字符串
相比C/C++可以修改单个字符而言,String类没有提供用于修改字符串的方法。
由于不能修改Java字符串中的字符,所以Java文档中将String类对象称为“不可变字符串”。如果需要修改某个字符串变量,通常直接让它引用另一个字符串。
虽然重新建立一个字符串的效率并不高,但是不可变字符串带来一个优点:编译器可以让字符串共享。Java中有一个字符串池,每个字符串变量分别指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串可以共享相同的字符串,而不必担心字符串被莫名其妙修改的问题。
Java设计者认为共享带来的高效远远胜于提取、拼接字符串带来的低效。因为更多时候我们做的不是修改而是比较。(有一种例外情况,将来自输入流的单个字符或较短的字符串拼接成长串,不过为此有专门的类StringBuilder
来负责)
此外,不必担心过多的字符串导致堆内存遗漏,Java有GC机制。
3.6.4 检测字符串是否相等
可以使用equals
方法检测两个字符串(变量或字面量)是否相等。(如果忽略大小写可以使用equalsIgnoreCase
方法)。
注意:
一定不要使用 == 检查两个字符串是否相等!这只能判断两个字符串是否放在同一内存位置上。虽然放在同一位置上的字符串必然相等,但是完全有可能将内容相同的多个字符串拷贝防止在不同的位置上!“==”返回true或false与两个字符串相等与否并非等价。
如果虚拟机始终将所有相同的字符串共享,那么是可以使用==来判断的。但实际上只有字符串常量是共享的,而 + 或substring等操作产生的结果是不共享的。
C++中的string类重载了==运算符,以便检测字符串内容的相等性。C语言中则通常使用strcmp()方法。这类似于Java中的compareTo()方法,但是Java中的compareTo()通常用于比较字典序,判断String相等还是使用equals()最为清晰。
3.6.5 空串与null
注意,如果要检查一个String既不是null也不是空串,要先判断是否为null,再调用它身上的方法判断是否为空串。
3.6.6 码点与代码单元
Java字符串由char值序列组成。char类型是一个采用UTF-16编码表示的Unicode码点的代码单元。大多数Unicode使用一个代码单元,某些特殊符号则需要两个。
- 求长度:
str.length()
返回代码单元数量;str.codePointCount(0, str.length())
返回码点数量。 - 根据下标找char / int码点:
char ch = str.charAt(index);
返回对应的char(太底层,不建议使用,可能返回一个无意义的代码单元);int cp = str.codePointAt(index);
返回Unicode码点值。 - 根据字符偏移量找下标:
int index = str.offsetByCodePoints(0, i);
,即从下标0开始,向后偏移i个码点对应码点的下标。 - 如果需要遍历所有码点,可以使用
int[] codePoints = str.codePoints().toArray()
方法,得到一个int[]数组;反之使用new String(codePoints, 0, codePoints.length)
得到原来的字符串。
3.6.7 String API(略)
3.6.8 阅读联机API文档(略)
3.6.9 构建字符串
StringBuilder(JDK 5引入)和StringBuffer的区别:
- StringBuilder效率高,但是只支持单线程;
- StringBuffer效率较低,但是支持多线程添加和删除字符;
3.7 输入输出
3.7.1 读取输入(控制台输入)
- 标准打印流:System.out
- 标准字节输入流:System.in
- 因为System.in是字节输入流,所以只能以字节为单位从控制台读取,所以如果要把一串字节(byte[])转换成字符串、数字等等,就非常非常麻烦,因此,需要有一个包装类Scanner帮我们处理这类繁琐无聊的事情。
- 如果需要控制台输入,最好将一个Scanner对象与标准输入流关联:
new Scanner(System.in);
,直接调用它的next()、nextLine()、nextInt()、nextDouble()等方法,这个Scanner便会帮我们处理这些琐碎无聊且易错的事情。
3.7.2 格式化输出
幸运的是,Java沿用了C/C++的标准输入输出printf(),在Java中是System.out.printf()方法。转换符、格式控制符基本沿用了C/C++的风格。
此外Java还给出了很多扩展功能的printf()标志,以及用于Date类对象的日期和时间的转换符。
可以使用静态的String.format(String… args)
方法创建一个格式化的字符串。
3.7.3 文件的输入与输出
- 文件输入:只需要在构造Scanner时,传输一个File对象作为输入即可(不能直接使用字符串),根据源码,Scanner会自动将File装入FileInputStream。
- 文件输出:同理,构造一个打印流对象即可。
补充点:PrintStream和PrintWriter的区别
- System.out使用的是PrintStream,其工作原理是将字符以系统默认编码转换成字节流送给控制台,不支持指定编码,这就导致在将数据传输给另一个平台时,解码出现错误。因为我们常用的控制台是在本地机器的,所以一般没有问题。PrintStream类出现较早,所以为了兼容性,System.out沿用了PrintStream。
- 我们通常使用的是较晚出现的PrintWriter,其基本功能同PrintStream一样,主要区别在于PrintWriter支持指定的编码方式
java.nio.charset.Charset
,使得在跨平台时,兼容性和可控性会更好。
3.8 控制流程
3.8.1 块作用域
- 块作用域:用一对大括号括起来的若干条Java语句。
- Java中嵌套的块作用域不允许声明同名的变量(C++中是允许的,并且内层变量覆盖外层变量,但是容易出错)。
- 使用块(复合语句)可以在Java程序结构中原本只能放置一条(简单)语句的地方放置多条语句。
3.8.2 条件语句(略)
- if () {} else if () {} else {}
3.8.3 循环(略)
- while(){}
- do{}while();
3.8.4 确定循环(略)
- for(int i = 0; i < n; ++i) {}
3.8.5 多重选择
- 在程序中不建议使用switch,因为一旦忘记break就容易引发错误。
- case标签可以是:
- char、byte、short、int的常量表达式。
- 枚举常量。
- 从JavaSE7开始可以是字符串字面量。
3.8.6 中断控制流程语句
- Java除了支持普通的break之外,还额外支持一种带标签的break,例如:
read_data:
while(...) {
for(...;...;...) {
......
break read_data;
......
}
}
if(...) {
} else {
}
- 注意,对于任何使用break语句的代码,都最好检测一下循环是否正常结束,还是break跳出。
- 实际上,continue也有带标签的continue,因为不常用且导致代码难以阅读,故不再展开。
3.9 大数值
如果基本的整数和浮点数运算无法满足精度需要,则可以使用BigInteger和BigDecimal类,它们分别实现了任意长度的整数运算和任意精度的浮点运算。
注意:
与C++不同,Java没有重载运算符的功能。虽然Java设计者为String重载了 + 运算符,但没有重载其他运算符,也不支持程序员重载运算符。
3.10 数组
数组是一种顺序存储的数据结构。优点在于支持随机访问,缺点在于增删元素的时间复杂度较高。
有关数组初始化问题:
- 在Java中,创建一个数组时,所有元素都初始化为0、false或null,表示没有存放任何对象。
有关数组长度问题:
- 在Java中,数组的长度不要求是常量。假设n已经被显式初始化,则new int[n]是合法的语句。
- 可以通过数组的
public final int length
属性可以获得数组的长度,一旦访问越界则会抛出异常。 - 如果需要可扩展长度的数组,可以使用ArrayList类。
3.10.1 for each 循环
Java有一种简洁不易错且功能很强的循环结构:for (variable : collection) statement
,这样我们就无需担心集合长度以及下标问题辣~~~
注意:
- collection必须是数组或实现了Iterable接口的类对象
- 其实每次迭代都是把一个collection中的变量赋值给了variable,所以如果要对基本数据类型进行遍历,则仅仅支持访问,而不支持修改,因为修改的仅仅是临时变量,而非集合中的真正值。
- 如果需要在循环中使用下标值,或者仅仅访问集合中的个别元素,则需要使用传统的for循环。
- 如果仅仅需要打印所有值,可以使用
Arrays.toString()
方法。它会调用数组中每个对象的toString方法(基本数据类型是直接转换成字符串),然后加一个方括号,每个元素用逗号分隔,将集合中的元素全部打印出来。
3.10.2 数组初始化以及匿名数组
- 初始化数组:int[] arr = {1, 2, 3} 。其效果等价于 int[] arr = new int[]{1, 2, 3}。
- 创建匿名数组:
new int[N]
或new int[] {1, 2, 3}
,其优点在于不创建新变量的情况下创建一个数组对象。
注意:Java中,允许数组的长度为零。长度为零的数组与null不同,是占用空间的。
3.10.3 数组拷贝
- 如果直接使用“=”赋值,则仅仅拷贝数组的引用(浅拷贝)。
- 如果需要“深拷贝”,将整个数组再拷贝一份副本,则需要使用
Arrays.copyOf(arr, arr.length)
方法,这将返回拷贝后新数组的引用。长度小于原数组则截断,大于原数组则补0/false。 - Java中的数组变量没有重载+/-运算符,所以不能像C++的指针一样通过加减来得到下一个元素。
- Java的数组是对象,因此是在堆内存中保存的(GC回收)。而C++中,
int arr[100];
是保存在栈内存中(随着代码块结束自动回收);int* a = new int[100];
才是保存在堆中(且需要手动delete)。
3.10.4 命令行参数
在Java中,main方法固定带有一个字符数组String[] args
作为参数。
当在命令行键入如下字符串并会车时,会调用Message类的main入口方法,并将"-g"、“cruel”、"world"作为参数传入String[] args
中,并可以在程序中使用。
java Message -g cruel world
3.10.5 数组排序
可以直接调用Arrays.sort()方法,进行优化的快速排序,快排对于大多数数据集合来说效率还是比较高的。
程序清单3-7给出一个非常巧妙的不重复抽签办法。每次只随机产生下标,然后找出该元素后,用最后一个元素覆盖之,然后n–,使得下一次抽签的范围变成0 ~ (n-1),然后不断迭代这一过程即可。
3.10.6 多维数组
Java中,N维数组的定义和初始化大体与之前的一维数组类似,只不过多了几个维度。
注意:
- for each 不能直接遍历二维数组中的每一个元素,它是按照一位数组处理的。可以使用下面嵌套的foreach语句:
for(double[] row : arr)
for(double value : row)
do something with value......
- 如果要快速打印一个二维数组的数据元素列表,可以调用
System.out.println(Arrays.deepToString(arr));
3.10.7 不规则数组
Java的多维数组与C/C++在使用上大同小异,但是在存储结构上存在着很大差别,这正是Java的优势所在。
C/C++中,多维数组中的所有数据通常也是连续摆放在内存的一片区域中的,而Java中的数组更像是“数组的数组”,例如二维数组a引用的内存中,其实保存的是row个一位数组的引用。
这样带来的好处是:
第一,我们可以很轻易的将两行进行交换:
double[] tmp = balances[i];
balances[i] = balances[j];
balances[j] = tmp;
第二,我们可以创建一个每行长度不等的不规则数组(例如对角矩阵):
int[][] odds = new int[rowCount][]; // odds数组中均为null
// 依次为每一行创建一个新数组
for(int i = 0; i < rowCount; ++i) {
odds[i] = new int[i+1];
}
注意:
- 由于Java多维数组的内存分布与C和C++有显著差异,所以在Java的二维数组声明中,往往“行”数比“列”数重要。“列”数可以省略(因为列数的长度可以是任意的),而“行”数不能。这一点与C/C++恰好相反。
- 在C++中,Java声明
double[][] balances = new double[10][6];
等价于double **balances = new double*[10];
,然后为指针数组中的每个元素申请堆中的空间:
for(int i = 0; i < 10; ++i) {
balances[i] = new double[6];
}
庆幸的是在Java中会自动进行这样的空间开辟,除非建立的是不规则数组。
小博同学,2020年7月8日凌晨00:32,于小破邮。