第3章 Java的基本程序设计结构
3.1 一个简单的Java应用程序
下面看一个最简单的Java应用程序,它只发送一条消息到控制台窗口中:
public class FirstSample {
public static void main(String[] args) {
System.out.println("we will not use 'Hello World!'");
}
}
所有的java应用程序都具有这种结构,还是值得花一些时间研究一下。首先,Java对大小写敏感。如果出现了拼写错误,那么程序将无法运行。例如,将main拼写成Main.
下面逐行分些一下这段源代码。
关键字public 称为访问修饰符,它将用于控制程序的其他部分对这段代码的访问级别。关键字class 表明Java程序中的全部内容都包含在类中。这里,只需要将类作为一个加载程序逻辑的容器,程序逻辑定义了应用程序的行为。关键字class的后面紧跟类名。
Java中类名的定义规则:
- 名字必须以字母开头的,后面可以跟字母和数字的任意组合;
- 长度上基本没有限制,但是不能使用Java保留字作为类名;
Java标准的命名规范:
- 类名是以大写字母开头的名词。如果名字由多个单词构成,每个单词的首写字母都应该大写。这种在一个单词中间使用大写字母的方式成为骆驼命名法。
源代码的文件名必须与公共类的名字相同,并以.java作为扩展名。这段源代码正常编译后,就会得到一个包含这个类字节码的文件。Java编译器将字节码文件自动命名为FirstSample.class,并与源文件存储在同一个目录下。运行编译程序时,Java虚拟机将从指定的类中的main方法开始执行。
注释:根据Java语言规范,main()方法必须声明为 public static.与C++不同的是main方法没有给操作系统返回“退出代码”。如果main方法正常退出,那么Java应用程序的退出代码为0,表示成功退出了程序;如果希望在终止程序时返回其他的代码,那么需要调用System.exit()方法。
接下来,研究一下这段代码:
{
System.out.println("we will not use 'Hello World!'");
}
Java 中“函数调用”的通用语法:object.method(paramters);
3.2 注释
- 第一种,单行注释使用 // .......;
- 第二种,多行注释使用 /* ........ */; 注意:在java中,/* */注释不能嵌套。
- 第三种,自动生成文档使用 /** ...........*/ ;
3.3 数据类型
Java 是一种强类型语言。这就是意味着必须为每一个变量声明一种数据类型。Java 不区分声明和定义!在Java 中,一共有8种基本类型,其中有4中整型,long (8字节)、int (4字节)、short (2字节)、byte (1字节); 2种浮点型,double (8字节)、float (4字节);1种用于表示Unicode编码的字符单元的字符类型char(2字节) 和 1种用于表示真值的boolean类型。
3.3.1 整型
整型用于表示没有小数部分的数值,它允许是负数。通常情况下,int类型最常用。
注:Java跨平台性表现之一:在Java中,整型的范围与运行Java代码的机器无关。这就解决了软件的“平台移植”问题。由于Java程序必须保证在所有的机器上都能够得到相同的运行结果,所以每一种数据类型的取值范围必须固定。
长整型数值有一个后缀L。十六进制数值有一个前缀0x,八进制有一个前缀0.从Java 7开始,加上前缀0b就可以写二进制数。例如 0b1001 就是 9.
注:Java 没有任何无符号类型(unsigned).
3.3.2 浮点类型
浮点类型常用于表示小数部分的数值。double(8字节) 表示的精度是float (4字节)的两倍(有人称之为双精度数值)。float类型的数值有一个后缀F,没有后缀F的浮点数值默认为double类型。
所有浮点数值计算都遵循 IEEE 754 规范。下面是用于溢出和出错情况的三个特殊的浮点数值:
正无穷大;负无穷大;NaN(不是一个数字);
注:不能这样检测一个特定值是否等于Double.NaN:
if (x==Double.NaN) // is never true
因为,所有“非数值”的值都认为是不相同的。但是可以使用Double.isNaN()方法:
if (Double.isNaN(x)) // check whether x is "not a number"
浮点数值不适合用于禁止出现舍入误差的金融计算中。主要是因为在java中浮点数值采用的是二进制系统表示,二进制系统无法精确表示分数 1/10。这就好像十进制无法精确表示 1/3 一样。如果需要在数值计算中,不含有任何舍入误差,就应该使用BigDecimal类。
3.3.3 char类型
char类型用于表示单个字符。通常用来表示字符常量。例如,‘A’ 是编码为65所对应的字符常量。
注:要想弄清楚char类型,就必须了解Unicode编码表。代码点(code point)就是指与一个编码表中的某个字符对应的代码值。
3.3.4 boolean 类型
boolean (布尔) 类型有两个值:false 和 true,用来判断逻辑条件。整型值和布尔值之间不能互相转换。
3.4 变量
在Java 中,每个变量属于一种类型。在声明变量时,变量所属的类型位于变量名之前。变量名必须是以字母开头的由字母或数字构成的序列。变量中的所有字符都是有意义的,并且大小写敏感。变量名的长度没有限制。
提示: 如果想要知道哪些Unicode字符属于Java中的“字母”,可以使用Character类的isJavadefierStart 和 isJavaIdenfierPart方法进行检测。
可以在一行中声明多个变量,逐一声明每个变量可以提高程序的可读性。许多程序员将变量名命名为类型名,例如:Box box;
3.4.1 变量初始化
声明一个变量后,必须用赋值语句对变量进行显示初始化,千万不能使用未被初始化的变量。否则报错如下:
int x;
System.out.println(x); // ERROR--The variable x may not have been initialized
在Java 中,变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的编程风格。
3.4.2 常量
在Java中,必须利用关键字 final 指示常量。关键字 final 指示变量只能赋值一次。习惯上,常量名使用全大写。
在Java 中,经常希望一个变量可以被一个类中的多个方法使用,这些常量被称为类常量。可以使用关键字 static final设置一个类常量。需要注意,类常量是定义在main方法的外部。因此,同一个类的其他方法中也可以使用这个常量,而且,如果一个常量被声明为 public 其他类中的方法 也可以使用这个常量。
3.5 运算符
当参与 / 运算的两个操作数都是整数时,表示整除法;否则,表示浮点除法。整数的求余操作用%表示。
注:为什么数值计算团体饭对Java虚拟机的最初规范规定所有的中间计算都必须进行截断?
因为截断不仅可能导致溢出,而且由于截断操作需要消耗时间,所以在计算速度上要比精确计算慢。为了解决最优性能与理想结果之间的冲突,在默认情况下,虚拟机设计者允许将中间计算结果采用扩展的精度。
3.5.1 自增运算符与自减运算符
前缀和后缀两种方式,但是在表达式中,这两种方式就有区别了:前缀方式先进行加1运算;后缀放式则使用变量原来的值。
3.5.2 关系运算符与 boolean 运算符
&& 表示逻辑 “与“; || 表示逻辑 ”或“;! 表示逻辑 ”非“。但是,&& 和 || 都是按照”短路“ 方式求值的。
3.5.3 位运算符
在处理整数时,可以直接对组成整型数值的各位进行操作。位运算符包括:&(“与”)、| (“或”)、^ (“异或“)、~(”非“)
例如:通过运用2的幂次方的&运算可以将其他位屏蔽掉,而只保留其中的一位。
int n = 0b11000; // 二进制表示n
int fourthBitFromRight = (n & 0b1000) / 0b1000;
System.out.println(fourthBitFromRight); // return 1
注:1. & 和 | 运算符应用于布尔值,得到的结果也是布尔值。但是,它们不是按照“短路”方式计算值。
>> 和 << 运算符将二进制位进行右移或左移操作。例如,
int fourthBitFromRight = (n & (1 << 3)) >> 3; // return 1
最后,>>> 运算符将用0 填充最高位; >> 运算符用符号位填充高位。
没有<<<运算符。
注意:对移位运算符右侧的参数需要进行模32的运算(如果左边的操作数是long类型,则运算符右侧的参数需要模64运算),否则:
System.out.println((1<<35) == 8); //true
System.out.println((1<<3) == 8); // true
3.5.5 数值类型之间的转换
在程序运行时,经常需要将一种数值类型转换为另一种数值类型。数值类型之间的合法转换如下图所示:
图中的实心箭头,表示无信息丢失的转换;虚箭头,表示可能有精度损失的转换。
当使用两个不同基本类型的数值进行计算时,先要将两个操作数转换为同一种类型(类型提升),然后再进行计算。转换规则如下:
- 如果两个操作数有一个double类型,另一个操作数就转换为double类型。
- 否则,如果其中一个操作数是float类型,另一个操作数转换为float类型。
- 否则,如果其中一个操作数是long类型,另一个操作数转换为long类型。
- 否则,两个操作数都将被转换为int类型。
3.5.6 强制类型转换
强制类转换的格式:在圆括号中给出要转换的目标类型,后面紧跟转换的变量名。例如,
double x = 9.997;
int nx = (int) Math.round(x); //对浮点类型x进行四舍五入运算,返回long类型,因此需要进行类型强转。 nx = 10
注意:类型强转会发生溢出,导致信息的丢失。
3.6 字符串
从概念上讲 ,Java字符串就是Unicode字符序列。Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类,叫做String类。每个用双引号括起来的字符串都是String类的一个实例。
String e = ""; // 一个空串
3.6.1 子串
String类的substring方法可以从一个较大的字符串中提取一个子串。例如:
String greeting = "Hello";
String s = greeting.substring(0,3); // 前闭后开区间[0,3)包含的字符序列构成它的一个子串, 子串的长度是3-0 = 3
3.6.2 拼接
Java允许使用+号连接两个字符串。这种特性经常用在输出语句中。例如:
- 两个字符串拼接
String str1 = "hungry_";
String str2 = "bug";
String str3 = str1 + str2; // str3 = "hungry_bug"
- 一个字符串与一个非字符串的值进行拼接时,后者被转换为字符串。任何一个Java对象都可以转换成字符串。
int time = 2016;
String str4 = str3 + time; //str4 = "hungry_bug2016"
3.6.3 不可变字符串
String类没有提供用于修改字符串的方法。如何修改一个字符串呢?在Java中实现该操作非常简单,首先提取需要的字符,然后再拼接上替换的字符串。例如:
greeting = greeting.substring(0,3)+"p!"; // greeting = "help!"
由于不能修改字符串中的任何一个字符,Java文档中将String类对象称为
不可变字符串,但是可以修改字符串变量,让它应用另外一个字符串。
虽然通过拼接的方式来创建一个新的字符串,效率不高,但是,不可变字符串有一个优点:编译器可以让字符串共享。共享如何实现?
可以想象将各种字符串放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符共享相同的字符。字符串(String类对象)存放在堆中。
总而言之,Java的设计者认为共享带来的高效率远远胜过于提取、拼接字符串所带来的低效率。
注: 与C不同的是,Java中的字符串不是字符数组,而更像char*指针,
char *greeting = "Hello";
当采用另一个字符串替换greeting的时候,Java代码主要发生以下操作:
char* temp = malloc(6);
strncpy(temp,greeting,3);
strncpy(temp+3,"p!",3);
greeting = temp;
3.6.4 检测字符串是否相等
可以使用equals方法检测两个字符串是否相等。对于表达式:
s.equals(t); //若字符串s与t相等,返回true;否则返回false
//注意:s与t可以是字符串变量,也可以是字符串常量。
要想检测两个字符串是否相等,而不区分大小写,可以使用equalsIgnoreCase()方法。
"Hello".equalsIgnoreCase("hello"); // true
为什么不能使用 == 运算符检测两个字符串是否相等呢?
这个运算符只能够确定两个字符串是否放在同一个位置上。当然,如果两个字符串放置在同一个位置上,它们必相等,但是,完全有可能将内容相同的多个字符串的拷贝放置在不同的位置上。例如:哈希表。
如果虚拟机始终将相同的字符串共享,就可以使用 == 运算符检测是否相等。但是,实际上只有字符串常量是共享的。而+ 或substring等操作产生的结果并不是共享的。因此,不要使用 == 运算符测试字符串的相等性,以免在程序中出现糟糕的bug。
3.6.5 空串与Null串
空串 "" 是长度为0的字符串。判断字符串为空的方法如下:
if(str.lenght() == 0) ; // 第一种判断方式
if(str.equals("")) ; // 第二种判断方式
空串是一个Java对象,有自己的串长度(0)和内容(空)。但是,String变量可以存放一个特殊的值null,此时,表示目前没有任何对象与该变量关联。要检查一个字符串是否为null,要使用以下条件:
if(str == null)
有时候 要检查一个字符串既不是null,也不是空串:
if(str != null && str.length() != 0) // 首先要检查str不为null,因为在一个null值上调用方法,会出错。
3.6.9 构建字符串
有些时候,需要由许多较短的字符串构建一个新的字符串。如果采用字符串连接的方式构建新的字符串,效率比较低。每次连接字符产,都会构建一个新的String对象,既耗时,又浪费空间。使用StringBuilder类(该类的前身是StringBuffer)就可以避免这个问题的发生。首先,构造一个空的字符串构建器:
StringBuilder builder = new StringBuilder(); // 创建一个空字符串的构建器
当每次需要添加一部分内容时,就调用append方法。
builder.append(str); // appends a string
在需要构建字符串时就调用toString 方法,就可以得到一个String对象,其中包括了构建器中的字符序列。
String completeSting = builder.toString(); // 返回字符串对象
注:StringBuilder类的前身是StringBuffer,其效率稍低,但是允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑(通常都是这样),则应该用StringBuilder类替代它。
3.7 输入输出
通过控制台进行“标准输入流”System.in,首项需要构造一个Scanner对象,并与“标准输入流”System.in关联。
Scanner in = new Scanner(System.in); // 构造一个Scanner对象,并与“标准输入流”System.in关联。
使用Scanner类的各种方法实现输入操作,例如,使用nextLine()方法将输入一行。
String name = in.nextLine(); // 在这里使用nextLine方法是因为输入行中可能含有空格。
要想读取一个以空白符作为分隔符的单词,就要调用:
String firstName = in.next(); // 读一个单词
int age = in.nextInt(); // 读一个整数float floatNumber = in.nextDouble(); // 读一个浮点数
测试结果如下:
Input:
hungry bug
hungry
Output:
String name = in.nextLine();
String firstName = in.next();
System.out.println(name); // hungry bug
System.out.println(firstName); // hungry
注释:因为输入是可见的,所以Scanner类不适用于从控制台读取密码。Java SE 6特别引入了Consloe 类实现这个目的。要想读取一个密码,可以采用下列代码:
Console cons = System.console();
String username = cons.readLine("User Name:"); //
char[] passwd = cons.readPassword("Password: "); // 返回的密码放在一维数组中
返回的密码放在一维数组中,而不是字符串中。因为Java中的字符串是不可变字符串,字符串常量是共享的,这样就会存在安全问题。
3.7.2 格式化输出
System.out.println(x);//
这条命令是将以x所对应的数据类型所允许的最大非0数字位数打印输出x。例如:
double y = 1000.0 / 3.0;
System.out.println(y); // 333.3333333333333 // 16位
Java SE 5.0沿用了C语言库函数的printf()方法。
System.out.printf("%tc",new Date()); // 星期一 十二月 05 19:32:16 CST 2016
可以使用静态的String.format()方法创建一个格式化的字符串,而不打印输出:
String name = "hungrybug";
int age = 25;String message = String.format("Hello,%s.Next year,you'll be %d.", name,age);
System.out.println(message); // Hello,hungrybug.Next year,you'll be 25.
3.7.3 文件输入与输出
要想读取文件,就需要一个用File对象构造一个Scanner对象。如下所示:
Scanner in = new Scanner(Paths.get("myfile.txt"))); // 正确的写法。根据给定的文件名构造一个Path作为Scanner的参数
//Scanner in2 = new Scanner("myfile.txt");//错误:Scanner将参数解释为数据而不是文件名。
如果文件名中包含反斜杠符号,就要记住在每个反斜杠之前再加一个额外的反斜杠(转义):"c:\\mydirectory\\myfile.txt"
要想写入文件,就需要构造一个PrintWriter对象。在构造器中,只需要提供文件名
PrintWriter out = new PrintWriter("myfile.txt"); // 如果文件不存在,就创建文件
要记住一点:如果用一个不存在的文件创建一个Scanner,或者用一个不能被创建的文件名够造一个PrintWriter,那么就要回很严重的异常。Java编译器认为这些异常比“被零整除”异常更严重。
3.8.1 块作用域
一个块可以嵌套在另一个块中。但是,不能在嵌套的两个块中声明同名变量。因为在内层的块中,内层定义的变量会覆盖外层定义的变量。
3.8.2 for循环
注:在循环中检测两个浮点数是是否相等需要格外小心。下面的for循环可能永远不会结束。
for(double x = 0; x != 10; x += 0.1){} // 因为0.1无法精确地用二进制表示。
可以比较两个float值的差,当他们的差的绝对值小于一个极小的数值时,比如说10的-6次方,则认为二者相等!或者涉及到浮点型数据运算的时候,最好用BigDecimal处理,避免出现不必要的麻烦。
特别的是,如果在for语句内部定义一个变量,这个变量就不能在循环体之外使用。因此,如果希望在for循环体之外使用循环计数的最终止值,就要确保这个变量在循环语句的前面且在外部声明。
3.8.3 Switch语句
switch语句从与选项值相匹配的case标签处开始执行直到break语句时,或者执行到switch语句的结束为止。如果没有相匹配的case语句,而有default子句,就执行这个子句。
3.10 数组
创建一个数组
int[] a = new int[100]; //数组a长度是常数:
int[] b = new int[n]; // 数组长度不要求是常数,创建一个长度为n的int数组b;
int[] c = new int[0]; //在Java中允许数组的长度为0,在编写一个结果为数组的方法时,如果结果为空,这种语法就显得非常有用。
创建一个数字数组时,所有元素都初始换为0,boolean 数组的元素会初始化为false。对象数组的元素则初始化为一个特殊的值null,表示这些元素还没有存放任何对象。例如:
Strign[] names = new String[10];//所有的字符串为null
数组一旦创建了,就不能再改变数组的大小。如果经常需要在运行时扩展数组的大小,就应该使用另一种数据结构--数组列表(arrayList)。
3.10.1 for each 循环
用来依次处理数组中的每个元素,增强的for循环的语句格式:
for(variable : collection) statement
定义一个变量用于暂存集合中的每一个元素,并执行相应的语句。collection这一集合必须是一个数组或者是一个实现了Iterable接口的类对象。
提示:有一个更简单的方式打印数组中的所有值,即利用Arrays类的toString()。Arrays.toString(a); //返回一个包含数组元素的字符串,这些元素被放置在括号内,并用逗号隔开。要想打印数组,可以调用 System.out.println(Arrays.toString(a));
int [] a = {1,2,3,4,5};
System.out.println(Arrays.toString(a)); // [1, 2, 3, 4, 5]
3.10.3 数组拷贝
数组变量的拷贝:在Java中,允许将一个数组变量拷贝给另一个数组变量。这时,两个变量将引用同一个数组:
int[] luckyNumbers = smallPrimes; // 将数组smallPrimes拷贝给luckyNumber数组
luckyNumers[5] = 12; // now smallPrimes[5] is also 5
数组元素的拷贝:如果希望将一个数组中的所有值拷贝到一个新的数组中去,就需要用Arrays类的copyOf()
int[] copiedLuckyNumber = Arrays.copyOf(luckyNumbers,luckyNumbers.length);//第二个参数是新数组的大小
下面这个方法通常用来增加数组的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers,2*luckyNumbers.length);//如果新数组的长度不够,则只拷贝最前面的数据元素
3.10.5 数组排序
int[] a = new int[100];
.....
Arrays.sort(a); // 这个方法使用了快速排序算法。
如何产生0到n-1之间的一个随机数?
先用Matn.randm()返回一个0到1之间(包含0不包含1)的随机浮点数。用n乘以这个浮点数,就可以得到从0到n-1之间的一个随机数。
int r = (int)(Math.random() * n); // 类型强转
3.10.6 多维数组
二维数组(矩阵)定义如下:
int[][] s = {
{16,3,2,13},
{5,10,11,8},
{9,6,7,12},
{4,15,14,1}
}; //
for each循环不能处理二维数组的每一个元素,它是按照一维数组进行处理的,也就是按照行进行处理的。要想访问二维数组的所有元素,需要使用二重循环:
for(double[] row:a)
for(double[] value:row)
do soming with value;
3.10.7 不规则数组
Java实际上没有多维数组,只有一维数组。多维数组被解释为"数组的数组"。由于可以单独地取数组的某一行,所以可以让两行交换。
double[] temp = balance[i];// 数组变量的拷贝
balance[i] = balance[i+1];
balance[i+1] = temp;