第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 数值类型之间的转换

在程序运行时,经常需要将一种数值类型转换为另一种数值类型。数值类型之间的合法转换如下图所示:

  

java 应用程序的结构 java应用程序的结构如何_System

图中的实心箭头,表示无信息丢失的转换;虚箭头,表示可能有精度损失的转换。

当使用两个不同基本类型的数值进行计算时,先要将两个操作数转换为同一种类型(类型提升),然后再进行计算。转换规则如下

  • 如果两个操作数有一个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;