欢迎光临散文网 会员登陆 & 注册

Java学习记录:java的基本程序设计结构(二)

2022-12-15 19:16 作者:冰灵___Ling  | 我要投稿

输入与输出:

为了增加后面示例程序的趣味性,我们希望程序能够接受输入,并适当地格式化程序输出。当然,现代的程序都使用GUI收集用户输入,然而,编写这样一个界面需要使用更多工具与技术,目前还不具备这些条件。我们的第一要务是熟悉java程序设计语言,因此我们将使用基本的控制台来实现输入和输出。

读取输入:

前面已经看到,将输出打印到“标准输出流”(即控制台窗口)是一件非常容易的事情,只需要调用System.out.println。不过,读取“标准输入流”System.in就没有那么简单了。要想读取控制台输入,首先需要构造一个与“标准输入流”Sysem.in关联的Scanner对象。

Scanner in = new Scanner(System.in);

(构造器和new操作符以后会介绍。)

现在,就可以使用Scanner类的各种方法读取输入了。例如,nextLine方法将读取一行输入。

System.out.print("What is your name");
String name = in.nextLine();

在这里,使用nextLine方法是因为输入行中有可能包含空格。要想读取一个单词(以空白符作为分隔符),可以调用next方法。

String firstName = in.next();

要想读取一个整数,要使用nextInt方法。

System.out.println("How old are you?");
int age = in.nextInt();

与此类似,nextDouble方法可以读取下一个浮点数。

最后,在程序的最前面添加一行代码:

import java.util.*;

Scanner类在java.util包中定义。当使用的类不是定义在基本java.lang包中时,需要使用import指令导入相应的包。(有关于包与import指令的详细内容以后会讲)。

 

Scanner方法:

java.util.Scanner    5

·Scanner(InputStrem in)

用给定的输入流构造一个Scanner对象。

·String nextLine()

读取下一行输入。

·String next()

读取输入的下一个单词(以空白符作为分隔符)。

·int nextInt()

·double nextDouble()

读取并转换下一个表示整数或浮点数的字符序列。

·boolean hasNext()

检测输入中是否还有其他单词。

·boolean hasNextInt()

·boolean hasNextDouble()

检测下一个字符序列是否表示一个整数或浮点数。

 

【注释:因为输入对所有人都可见,所以Scanner类不适用于从控制台读取密码。可以使用Console类来达到这个目的。要想读取一个密码,可以使用以下代码:

Console cons = System.console();
String username = cons.readLine("User name: ");
char[] passwd = cons.readPassword("Password: ");

为安全起见,将返回的密码存放在一个字符数组中,而不是字符串中。完成对密码的处理之后,应该马上用一个填充值覆盖数组元素(数组处理以后会讲)。

使用Console对象处理输入不如使用Scanner方便。必须一次读取一行输入,而且Console类没有提供方法来读取单个单词或数字。】

 

Java.lang.System   1.0

·static Console console()   6

如果有可能进行交互操作,就通过控制台窗口为交互的用户返回一个console对象,否则返回null。对于任何一个在控制台窗口启动的程序,都可使用Console对象。否则,是否可用取决于所使用的系统。

 

Java.io.Console   6

·static char[] readPassword (String prompt, Object....args)

·static String readLine(String prompt, Object...args)

显示提示符(prompt)并读取用户输入,直到输入行结束。可选的args参数用来提供格式参数。(有关这部分内容之后会讲)

 

格式化输出:

可以使用System.out.print(x)语句将数值x输出到控制台。这个命令将以x的类型所允许的最大非0位数打印x。例如:

double x = 10000.0 / 3.0;
System.out.print(x)

会打印:

3333.3333333333335

如果希望显示美元、美分数,这就会有问题。

这个问题可以利用printf方法来解决,它沿用了C语言函数库中的古老约定。例如,以下调用:

System.out.printf("%8.2f",x);

打印x时字段宽度(field width)为8个字符,精度为2个字符。也就是说,结果包含一个前导的空格和7个字符,如下所示:

·3333.33(用·代替空格表示)

可以为printf提供多个参数,例如:

System.out.printf("Hello, %s. Next year, you'll be %d", name, age);

每一个以%字符开头的格式说明符(format specifiers)都替换为相应的参数。格式说明符末尾的转换字符(conversion character)指示要格式化的数值的类型:f表示浮点数,s表示字符串,d表示十进制整数。

大写形式会生成大写字母。例如,“%8.2E”将3333.33格式化为3.33E+03,这里有一个大写的E。

下面列出了用于printf的转换字符。

转换符

类型

举例

d

十进制整数

159

x或X

十六进制整数。要想对十六进制格式化有更多控制,可以使用HexFormat类

9f

o

八进制整数

237

f或F

定点浮点数

15.9

e或E

指数浮点数

1.59e+01

g

通用浮点数(e和f中较短的一个)

_____

a

十六进制浮点数

0x1.fccdp3

s

字符串

Hello

c

字符

H

 

b

 

布尔

True

h

散列码

42628b2

tx或Tx

日期时间(T强制大写)

已经过时,应该改为使用java.time;类

%

百分号

%

n

与平台有关的行分隔符

___

【注释:可以使用s转换字符格式化任意的对象。如果一个任意对象实现了Formattable接口,会调用这个对象的formatTo方法。否则,会调用toString方法将这个对象转换为一个字符串。(toString方法和接口以后会讲)】

另外,还可以指定控制格式化输出外观的各种标志(flag)。例如,逗号标志会增加分组分隔符。即:

System.out.printf("%,.2f",10000.0 / 3.0);

会打印:

3,333.33

可以使用多个标志,例如,”%,( .2f”会使用分组分隔符,并将负数包围在括号内。

下面列出了用于printf的标志:

 

可以使用静态的String.format方法创建一个格式化的字符串,而不打印输出:

String message = String.format("Hello, %s. Next year, you'll be %d", name, age+1);

【注释:在java15中,可以使用formatted方法,这样可以少敲5个字符:

String message =“Hello,%s. Next year, you'll be %d".formatted(name, age+1);

 【注释:格式化规则是特定于本地化环境的。例如,在德国,分组分隔符是点号而不是逗号。】

文件输入与输出:

要想读取一个文件,需要构造一个Scanner对象,如下所示:

Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);

如果文件名中包含反斜线符号,记住要在每个反斜线之前再加一个额外的反斜线转义:

”c:\\mydirectory\\myfile.txt”

现在就可以使用之前见过的Scanner方法读取这个文件了。

【注释:这里指定了UTF-8字符编码,这对于互联网上的文件很常见(不过并不是普通适用)。读取一个文本文件时,要知道它的字符编码(更多信息见下一本书)。如果省略字符编码,则会使用运行这个Java程序的机器的“默认编码”。这不是一个好主意,如果在不同的机器上运行这个程序,可能会有不同的表现。】

【警告:可以提供一个字符串参数来构造一个Scanner,但这个Scanner会把字符串解释为数据,而不是文件名。例如,如果调用:

canner in = new Scanner("myfile.txt");//ERROR?

这个scanner会将参数看作是包含10个字符(myf等)的数据。这可能不是我们的原意。】

要想写入文件,需要构造一个PrintWriter对象。在构造器(constructor)中,需要提供文件名和字符编码:

PrintWriter out = new PrintWriter("myfile.txt", StandardCharsets.UTF_8);

如果文件不存在,则创建该文件。可以像输出到System.out一样使用print、println以及printf命令。

【注释:当指定一个相对文件名,例如,myfile.txtmydirectory/myfile.txt../myfile.txt,文件将相对于启动Java虚拟机的那个目录放置。如果从一个命令shell执行以下命令启动程序:

java MyProg

启动目录就是命令shell的当前目录。不过,如果使用集成开发环境,那么启动目录将由IDE控制。可以使用下面的调用找到这个目录的位置:

String dir = System.getProperty("user.dir");

如果觉得文件定位太麻烦,可以考虑使用绝对路径名,例如:

c:\\mydirectory\\myfile.txt/home/me/mydirectory/myfile.txt

如你所见,访问文件和使用System.in和System.out一样容易。要记住一点:如果用一个不存在的文件构造一个Scanner,或者用一个无法创建的文件名构造一个PrintWriter,就会产生异常。Java编译器认为这些异常比“被零除”异常更严重。(之后会学习处理异常的方法)至于现在,只需要告诉编译器:你已经知道有可能出现“输入/输出”异常。为此要用一个throws子句标记main方法,如下所示:

public static void main(String[] args) throws IOException
{
    Scanner in = new Scanner(Path.of("myfile.txt"), StandardCharsets.UTF_8);
    ....
}

现在你已经学习了如何读写包含文本数据的文件了。对于更高级的内容,请见卷二。

【注释:从命令shell启动一个程序时,可以利用shell的重定向语法将任意文件关联到System.in和System.out:

java Myprog < myfile.txt > output.txt

这样就不必担心处理IOException异常了。】

 

以下是一个读写文本文件代码示例:

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.*;
public class haihaihai {
    public static void main(String[] args) throws IOException
    {
        Scanner in = new Scanner(Path.of("C:\\Users\\冰铃月\\IdeaProjects\\The first java project\\108宿舍档案.txt"), StandardCharsets.UTF_8);
        String User1 = in.nextLine();
        System.out.println(User1);
        PrintWriter out = new PrintWriter("C:\\Users\\冰铃月\\IdeaProjects\\The first java project\\test.txt",StandardCharsets.UTF_8);
        out.print("Hello,fuck you.");
        out.close();
    }
}

——

控制流程:

Java支持使用条件语句和循环结构来确定控制流程。这里首先讨论条件语句,然后介绍循环语句,最后介绍switch语句,它可以用来检测一个表达式的多个值。

【C++注释:java的控制流程结构与C/C++的控制流程结构基本相同,只有很少几个例外。Java中没有goto语句,但break语句可以带标签,可以利用它从嵌套循环中跳出(对于这种情况,C语言中可能就要使用goto语句了)。最后,还有以中国变形的for循环,有点类似于C++中基于范围的for循环和C#中的foreach循环。】

 

块作用域:

在学习控制结构之前,需要了解块(block)的概念。

块(即复合语句)由若干条Java语句组成,并用一对大括号括起来。块确定了变量的作用域。一个块可以嵌套在另一个块中。下面就是嵌套在main方法块中的一个块。

public static void main(String[] args){
    int n;
    ...
    {
        int k;
        ...
    }//k is only defined up to here
}

但是,不能在嵌套的两个块中声明同名的变量。例如,下面的代码就有错误,而无法通过编译:

public static void main(String[] args){
    int n;
    ...
    {
        int k;
        int n; // ERROR--can't redeclare n in inner block
        ...
    }
}

【C++注释:在C++中,可以在嵌套的块中重定义一个变量。在内层定义的变量会遮蔽(shadow)在外层定义的变量。这就有可能带来编程错误,因此Java中不允许这样做。】

条件语句:

在Java中,条件语句的形式为:

if (condition) statement

这里的条件必须用小括号括起来。

与大多数程序设计语言一样,在java中,常常希望在某个条件为真时执行多条语句。在这种情况下,就可以使用块语句(block statement),形式如下:

{
    statement1
    statement2
    ...
}

例如:

if(yourSales >= target)
{
    performance = "Satisfactory";
    bonus = 100;
}

当yourSales大于或等于target时,将执行大括号中的所有语句。

【注释:使用块(有时称为复合语句)可以在Java程序结构中原本只能放置一条(简单)语句的地方放置多条语句。】

在Java中,更一般的条件语句如下所示:

if (condition) statement1 else statement2

例如:

if (yourSales >= target)
{
    performance = "Satisfactory";
    bonus = 100 + 0.01 * (yourSales - target);
}
else
{
    performance = "Unsatisfactory";
    bonus = 0;
}

其中else部分总是可选的。else子句与最邻近的if构成一组。因此,在以下语句中:

if(x <= 0) if(x == 0) sign = 0; else sign = -1;

else与第2个if配对。当然,使用大括号可以让这段代码更加清晰:

if(x <= 0) {if(x == 0) sign = 0; else sign = -1;}

反复使用if....else if....很常见。例如:

if (yourSales >= 2 * target)
{
    performance = "Excellent";
    bonus = 1000;
}
else if(yourSales >= 1.5 * target)
{
    performance = "Fine";
    bonus = 500;
}
else if(yourSales >= target)
{
    performance = "Satisfactory";
    bonus = 100;
}
else
{
    System.out.println("You're fired");
}

 

循环:

while循环会在条件为true时执行一个语句(也可以是一个块语句)。一般形式如下:

while (condition) statement

如果开始时循环条件就为false,那么while循环一次也不执行。

While循环语句在最前面检测循环条件。因此,循环体中的代码有可能一次都不执行。如果希望循环体至少执行一次,需要使用do/while循环将检测放在最后,它的语法如下:

do statement while (condition);

这种循环先执行语句(通常是一个语句块),然后再检查循环条件。如果条件为true,就重复执行语句,然后再次检测循环条件,依次类推。

 

确定性循环:

for循环语句是支持迭代的一种通用结构,它由一个计数器或类似的变量控制迭代次数,每次迭代后这个变量将会更新。下面的循环将在屏幕上显示出打印数字1~10:

for(int i = 1; i <=10; i++)
System.out.println(i);

for语句的第1部分通常是对计数器初始化;第2部分给出每次新一轮循环执行前要检测的循环条件;第3部分指定如何更新计数器。

与C++类似,尽管Java允许在for循环的各个部分放置任何表达式,但有一条不成文的规则:for语句的3个部分应该对同一个计数器变量进行初始化、检测和更新。若不遵守这个规则,所写的循环很可能晦涩难懂。(但是没成文就是没成文,该咋用咋用)

即使受这个规则所限,仍有无尽可能,你可以写各种各样的for循环。例如,可以编写下面这个倒计数的循环:

for(int i = 10; i > 0; i--)
    System.out.println("Counting down ..." + i);
System.out.println("Blastoff!")

【警告:在循环中,检测两个浮点数是否相等需要格外小心。下面的for循环:

for (double x = 0; x!= 10; x += 0.1)...

可能永远不会结束。由于存在舍入误差,可能永远达不到精度的最终值。在这个例子中,因为0.1无法精确地用二进制表示,所以,x将从9.99999999999998跳到10.09999999999998。】

在for语句的第1部分中声明一个变量之后,这个变量的作用域会扩展到这个for循环体的末尾。

for (int i = 1; i<= 10; i++)
{
    ...
}
// i no longer defined here

特别指出,如果在for语句内部定义一个变量,这个变量就不能在循环体之外使用。因此,如果希望在for循环体之外使用循环计数器的最终值,就要确保在循环体之外声明这个变量。

int i;
for (i = 1; i<= 10; i++)
{
    ...
}
// i is still defined here

另一方面,可以在不同的for循环中定义同名的变量:

for (int i = 1; i<= 10; i++)
{
    ...
}
...
for (int i = 11; i <=20; i++) // OK to define another variable named i
{
    ...
}

for循环语句只是while循环的一种简化形式。例如,

for (int i = 10; i > 0; i--)
    System.out.println("Counting down ..." + i);

可以重写为:

int i = 10;
while ( i > 0)
{
    System.out.println("Counting down ... " + i);
    i--;
}

【注释:之后会介绍“泛型for循环”(又称为for each 循环),利用这个循环可以很方便地访问一个数组或集合中的所有元素。】

 

多重选择:switch语句

在处理同一个表达式的多个选项时,使用if/else结构会显得有些笨拙。switch语句会让这个工作变得容易,特别采用Java14引入的形式会更简单。

例如,如果建立一个简易的菜单系统,其中包含4个选项,可以使用以下代码:

Scanner in = new Scanner(System.in);
System.out.print("Select an option (1, 2, 3, 4 ) ");
int choice = in.nextInt();
switch(choice)
{
    case 1 ->
        ...
    case 2 ->
        ...
    case 3 ->
        ...
    case 4 ->
        ...
    default ->
        System.out.println("Bad input");
}

case标签可以是:

·类型为char、byte、short或int的常量表达式

·枚举常量

·字符串字面量

·多个字符串,用逗号分隔

例如:

String input = ...;
switch (input.toLowerCase())
{
    case "yes", "y" ->
        ...
    case "no", "n" ->
        ...
    default ->
        ...
}

swicth语句的“经典”形式可以追溯到C语言,从Java 1.0开始就支持这种形式。具体形式如下:

int choice = ...;
switch(choice)
{
    case 1:
        ...
        break;
    case 2:
        ...
        break;
    case 3:
        ...
        break;
    case 4:
        ...
        break;
    default:
        // bad input
        ...
        break;
}

switch语句从与选项值相匹配的case标签开始执行,直到遇到下一个break语句,或者执行到switch语句结束。如果没有匹配的case标签,则执行default子句(如果有default子句)。

警告:有可能触发多个分支。如果忘记在一个分支末尾增加break语句,就会接着执行下一个分支!这种情况相当危险,常常会引发错误。

为了检测这种问题,编译代码时可以加上 -Xlint:fallthrough选项,如下所示:

javac -Xlint:fallthrouth Test.java

这样一来,如果某个分支最后缺少一个break语句,编译器就会给出一个警告。

如果你确实是想使用这种“直通式”(fallthrough)行为,可以为其外围方法加一个注解@Suppress Warnings(“fallthrough”)。(注解是为编译器或处理java源文件或类文件的工具提供信息的一种机制。卷II会深入介绍注解)】

 

这两种switch形式都是语句。之前我们已经见过一个switch表达式,它会生成一个值。switch表达式没有“直通式”行为。

为了对称,Java14还引入了一个有直通行为的switch表达式,所以总共有4种不同形式的switch:

 

表达式

语句

无直通行为

int numLetters = switch (seasonName)
{
    case "Spring" ->
        {
            System.out.println("spring time!");
            yield 6;
        }
    case "Summer", "Winter" -> 6;
    case "Fall" -> 4;
    default -> -1;
};

 

switch (seasonName)
{
    case "Spring" ->
        {
            System.out.println("spring time!");
            numLetters = 6;
        }
    case "Summer", "Winter" -> numLetters = 6;
    case "Fall" -> numLetters = 4;
    default -> numLetters = -1;
}

 

有直通行为

int numLetters = switch (seasonName)
{
    case "Spring":
        {
            System.out.println("spring time!");
        }
    case "Summer", "Winter" :
        yield 6;
    case "Fall":
        yield 4;
    default:
        yield -1;
};

 

switch (seasonName)
{
    case "Spring":
        {
            System.out.println("spring time!");
        }
    case "Summer", "Winter" :
        numLetters = 6;
        break;
    case "Fall":
        numLetters = 4;
        break;
    default:
        numLetters = -1;
}

 

在有直通行为的形式中,每个case以一个冒号结束。如果case以箭头”->”结束,则没有直通行为。不能在一个switch语句种混合使用冒号和箭头。

注意switch表达式种的yield关键字。与break类似,yield会终止执行。但与break不同的是,yield还会生成一个值,这就是表达式的值。

要在switch表达式的一个分支中使用语句而不想有直通行为,就必须使用大括号和yield。如下将为一个分支增加日志语句:

case "Spring" ->
    {
        System.out.println("spring tme!");
        yield 6;
    }

switch表达式的每个分支必须生成一个值。最常见的做法是,各个值跟在一个箭头”->”后面:

case "Summer", "Winter" -> 6;

如果无法做到,则使用yield语句。

【注释:完全可以在switch表达式的一个分支中抛出异常。例如:

default -> throw new ILLegaLArgumentException("Not a valid season");

(异常以后会讲)】

【警告:switch表达式的关键是生成一个值(或者产生一个异常而失败)。不允许“跳出”switch表达式:

default -> { return -1;} // ERROR

具体来讲,不能在switch表达式中使用return、break或continue语句。】

switch有这么多种形式,要如何选择呢?

swicth表达式优于语句。如果每个分支会为一个变量赋值或方法调用计算值,则用一个表达式生成值,然后使用这个值。例如:

  numLetters = switch (seasonName)
{
    case "Spring", "Summer", "Winter" -> 6;
    case "Fall" -> 4;
    default -> -1;
};

要优于:

switch (seasonName)
{
    case "Spring", "Summer", "Winter" ->
        numLetters = 6;
    case "Fall" ->
        numLetters = 4;
    default ->
        numLetters =-1;
}

只要在确实需要直通行为时,或者必须为一个switch表达式增加语法时,才需要使用break或yield。不过这些情况非常少见。

 

中断控制流程的语句:

尽管Java的设计者将goto仍作为一个保留字,但实际上并不打算在语言中包含goto。通常,使用goto语句会被认为是一种拙劣的程序设计风格。但在有些情况下,偶尔使用goto跳出循环还是有益处的。Java设计者同意这种看法,甚至在Java语言中增加了一条新的语句:带标签的break,以此来支持这种程序设计风格。

下面首先来看不带标签的break语句。与用于退出switch语句的break语句一样,它也可以用于退出循环。例如:

while (years <= 100)
{
    balance += payment;
    double interest = balance * interestRate / 100;
    balance += interest;
    if (balance >= goal) break;
    years++;
}

循环开始时,如果years > 100,或者如果循环中间balance >= goal,则退出循环。当然,也可以在不使用break的情况下计算years的值,如下所示:

while (years <= 100 && balance < goal)
{
    balance += payment;
    double interest = balance * interestRate / 100;
    balance += interest;
    if (balance < goal) {
        years++
    }
}

但是需要注意,在这个版本中,检测了两次balance < goal。为了避免重复检测,有些程序员更偏爱使用break语句。

与C++不同,Java还提供了一种带标签的break语句,允许跳出多重嵌套的循环。有时候,在嵌套很深的循环语句中会发生一些不可预料的事情。此时你可能希望完全跳出所有嵌套循环。如果只是为各层循环检测添加一些额外的条件,这会很不方便。

下面的例子展示了break语句如何工作。请注意,标签必须放在你想跳出的最外层循环之前,并且必须紧跟一个冒号。

Scanner in = new Scanner(System.in);
int n;
read_data:
while(...) // this loop statement is tagged with the label
{
    ...
    for(...) // this inner loop is not labeled
    {
        System.out.print("Enter a number >= 0: ");
        n = n.nextInt();
        if (n < 0) // should never happen-can't go on
            break read_data;
            // break out of read_data loop
        ...
    }
}
// this statement is executed immediately after the labeled break
if (n < 0) // check for bad situation
{
    // deal with bad situation
}
else
{
    // carry out normal processing
}

如果输入有误,执行带标签的break会跳转到带标签的语句块末尾。与任何使用break语句的代码一样,接下来需要检测循环是正常退出,还是由于break提前退出。

【注释:有意思的是,可以将标签应用到任何语句,甚至可以应用到if语句或者块语句,如下所示:

label:
{
    ...
    if (condition) break label; // exits block
    ...
}
// jump here when the break statement executes

因此,如果确实希望使用goto语句,而且一个代码块恰好在你想要跳转到的位置之前结束,就可以使用break语句!当然,我并不提倡使用这种方法。另外需要注意,只能跳出语句块,而不能跳入语句块。】

最后,还有一个continue语句。与break语句一样,它将中断正常的控制流程。continue语句将控制到最内层外围循环的首部。例如:

 

Scanner in = new Scanner(System.in);
while (sum < goal)
{
    System.out.print("Enter a number: ");
    n = in.nextInt();
    if (n < 0) continue;
    sum += n; // not executed if n < 0
}

如果n < 0,则continue语句会越过当前循环体的剩余部分,直接跳到循环首部。

如果在for循环中使用continue语句,会跳转到for循环的“更新”部分。例如:

for ( count = 1; count <= 100; count++)
{
    System.out.print("Enter a number, -1 to quit: ");
    n = in.nextInt();
    if (n < 0) continue;
    sum += n; // not executed if n < 0
}

如果n < 0,则continue语句将跳转到count++语句。

还有一种带标签的continue语句,将跳转到有匹配标签的循环的首部。

【提示:许多程序员发现break和continue语句很容易混淆。这些语句完全是可选的,即不使用这些语句也能表达同样的逻辑。在本书中,所有程序都不会使用break和continue。】

 

大数:

如果基本的整数和浮点数精度不足以满足需求,那么可以使用java.math包中两个很有用的类:BigInteger和BigDecimal。这两个类可以处理包含任意长度数字序列的数值。BigInteger类实现任意精度的整数运算,BigDecimal实现任意精度的浮点数运算。

使用静态的valueOf方法可以将一个普通的数转换为大数:

BigInteger a = BigInteger.valueOf(100);

对于更长的数,可以使用一个带字符串参数的构造器:

BigInteger reallyBig = new BigInteger("2222272727282972838292928928923823782792392839283871799819");

另外还有一些常量:BigInteger.ZERO、BigInteger.ONE和BigInteger.TEN,java9之后还增加了BigInteger.TWO。

【警告:对于BIgDecimal类,总是应当使用带一个字符串参数的构造器。还有一个Bigdecimal(double)构造器,不过这个构造器本质上很容易产生舍入误差,例如,new BigDecimal(0.1)会得到以下数位:

0.1000000000000000055511151231257827021181583404541015625】

遗憾的是,不能使用人们熟悉的算数运算符(如:+和*)来组合大数,而需要使用大数类的add和multiply方法。

BigInteger c = a.add(b); // c = a + b
BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); // d = c*(b+2)

【C++注释:与C++不同,Java不能通过编程实现运算符重载。使用BigInteger类的程序员无法重定义+和*运算符来提供BigInteger类的add和multiply运算。Java语言的设计者重载了+运算符来完成字符串的拼接,但没有重载其他的运算符,也没有给Java程序员提供机会在他们自己的类中重载运算符。】

 

数组:

数组是一种数据结构,用来存储同一类型值的集合。通过一个整型索引(index,或称下标)可以访问数组中的每一个值。例如,如果a是一个整型数组,a[i]就是数组中索引为i的整数。

在声明数组变量时,需要指出数组类型(元素类型后面紧跟[])和数组变量名。例如,下面声明了整型数组a:

int[] a;

不过,这条语句只声明了变量a,并没有将a初始化为一个真正的数组。应该使用new操作符创建数组。

int[] a = new int[100]; // or var a = new int[100];

这条语句声明并初始化了一个可以存储100个整数的数组。

数组长度不要求是常量:new int[n]会创建一个长度为n的数组。

一旦创建了数组,就不能再改变它的长度(不过,当然可以改变单个数组元素)。如果程序运行中需要经常扩展数组的大小,就应该使用另一种数据结构——数组列表(array list)。(以后会讲)

【注释:可以使用下面两种形式定义一个数组变量:

int[] a;

int a[];

大多数Java程序员喜欢使用第一种风格,因为它可以将类型int[](整型数组)与变量名清晰地分开。】

在Java中,提供了一种创建数组对象并同时提供初始值的简写形式。下面是一个例子:(primes:素数、质数)

int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };

请注意,这个语法中不需要使用new,甚至不用指定长度。

最后一个值后面允许有逗号,如果你要不断为数组增加值,这样会很方便:

String[] authors =
    {
        "James Gosling",
        "Bill Joy",
        "Guy Steele",
        // add more names here and put a comma after each name
    };

还可以声明一个匿名数组(anoymous array):

new int[] { 17, 19, 23, 29, 31, 37};

这个表达式会分配一个新数组并填入大括号中提供的值。它会统计初始值个数,并相应地设置数组大小。可以使用这种语法重新初始化一个数组而无须创建新变量。例如:

smallPrimes = new int[] { 17, 19, 23, 29, 31, 37 };

这是以下语句的简写形式:

int[] anonymous = { 17, 19, 23, 29, 31, 37 };
smallPrimes = anonymous;

【注释:在Java中,允许有长度为0的数组。编写一个结果为数组的方法时,如果碰巧结果为空,这样一个长度为0的数组就很有用。可以如下构造一个长度为0的数组:

new elementType[0]

new elementType[] {}

注意,长度为0的数组与null并不相同。】

 

访问数组元素:

数组元素从0开始编号。最后一个合法的索引为数组长度减1.在下面的例子中,索引值为0~99。一旦创建了数组,就可以在数组中填入元素。例如,可以使用一个循环:

int[] a = new int[100];
for (int i = 0; i < 100; i++)
    a[i] = i; // fills the array with numbers 0 to 99

创建一个数字数组时,所有元素都初始化为0。boolen数组的元素会初始化为false。对象数组的元素则初始化为一个特殊值null,表示这些元素(还)未存放任何对象。例如:

String[] names = new String[10];

会创建一个包含10个字符串的数组,所有字符串都为null。如果希望这个数组包含空串,则必须为元素提供空串:

for (int i = 0; i < 10; i++)
    names[i] = "";

【警告:如果创建了一个包含100个元素的数组,然后试图访问元素a[100](或在0~99之外的任何其他索引),就会出现“array index out of bounds”(数组索引越界)异常。】

要想获得数组中的元素个数,可以使用array.length。例如,

for (int i = 0; i < a.length; i++)
    System.out.println(a[i]);

 

for each 循环:

Java有一种功能很强的循环结构,可以用来依次处理数组(或者其他元素集合)中的每个元素,而不必考虑指定索引值。

这种增强的for循环形式如下:

for (variable : collection) statement

它将给定变量(variable)设置为集合中的每一个元素,然后执行语句(statement)(当然,也可以是语句块)。collection表达式必须是一个数组或者是一个实现了Iterable接口的类对象(例如ArrayList)。(数组列表和Iterable接口以后会讲)

例如:

for (int element : a)
    System.out.println(element);

会打印数组a的每一个元素,一个元素占一行。

这个循环应该读作“循环a中的每一个元素”(for each element in a)。Java语言的设计者也曾考虑过使用诸如foreach和in这样的关键字,但这种循环并不是最初就包含在Java语言中国,而是后来添加的,没有人希望破坏已经包含同名方法或变量(例如System.in)的老代码。

当然,使用传统的for循环也可以获得同样的效果:

for (int i = 0; i < a.length; i++)
    System.out.println(a[i]);

但是,“for each”循环更加简洁、更不易出错,因为你不必为起始或终止索引值而操心。

【注释:“for each”循环的循环变量将会遍历数组中的每个元素,而不是索引值。】

如果需要处理一个集合中的所有元素,相比传统循环,“for each”循环是个让人欣喜的改进。不过,很多情况下还是需要使用传统的for循环。例如,可能不希望遍历整个集合,或者可能需要在循环内部使用索引值。

【提示:有一个更容易的方法可以打印数组中的所有值,即利用Arrays类的toString方法。调用Arrays.toString(a)会返回一个包含数组元素的字符串,这些元素包围在中括号内,并用逗号分隔,例如,”[2,3,5,7,11,13]”。要想打印数组,只需要调用:

System.out.println(Arrays.toString(a));

 

数组拷贝:

在Java中,允许将一个数组变量拷贝到另一个数组变量。这时,两个变量将引用同一个数组

int[] luckNumbers = smallPrimes;
luckyNumbers[5] = 12; // now smallPrimes[5] is also 12

如果确实希望将一个数组的所有值拷贝到一个新的数组中,就要使用Arrays类的copyOf方法:

int[] copiedLuckyNumbers = Arrays.copyOf(luckNumbers, luckyNumbers.length);

第2个参数是新数组的长度。

这个方法也通常用来增加数组的大小:

luckyNumbers = Arrays.copyOf(luckyNumbers, 2 * luckyNumbers.length);

如果数组元素是数值型,那么新增的元素将填入0;如果数组元素是布尔型,则填入false。相反,如果长度小于原数组的长度,则只拷贝前面的值。

【C++注释:Java数组与堆栈上的C++数组有很大不同,但基本上与在堆(heap)上分配的数组指针一样。也就是说,

int[] a = new int[100]; // Java

不同于:

int a[100]; // C++

而等同于:

int* a = new int[100]; // C++

Java中的[ ]运算符预定义为会完成越界检查(bounds checking)。另外,没有指针运算,就意味着不能通过a加1得到数组中的下一个元素。】

 

命令行参数:

前面已经看到一个例子,其中一个Java数组重复出现过很多次。每一个Java程序都有一个带String arg[]参数的main方法。这个参数表明main方法将接受一个字符串数组,也就是命令行上指定的参数。

例如,来看下面这个程序:

public class firstWar {
    public static void main(String[] args){
        if (args.length == 0 || args[0].equals("-h"))
            System.out.print("Hello,");
        else if(args[0].equals("-g"))
            System.out.print("Goodbye,");
        // print the other command-line arguments
        for(int i = 1; i < args.length; i++)
            System.out.print(" "+args[i]);
    }
}

如果如下调用这个程序:

java firstWar -g cruel world

args数组将包含以下内容:

args[0]:"-g"
args[1]:"cruel"
args[2]:"world"

这个程序会显示下面这个消息:

Goodbye, cruel world!

【C++注释:在Java程序的main方法中,程序名并不存储在args中。例如,从命令行如下运行一个程序时:

java Message -h world

args[0]是”-h”,而不是“Message”或”java“。】

 

数组排序:

要想对数值型数组进行排序,可以使用Arrays类中的sort方法:

int[] a = new int[100];

。。。

Arrays.sort(a);

这个方法使用了优化的快速排序(QuickSort)算法。快速排序算法对于大多数数据集都很高效。Arrays类还提供了另外一些很便捷的方法,在API注释中将介绍这些方法。

Math.random方法将返回一个0到1之间(包含0、不包含1)的随机浮点数。用n乘以这个浮点数,可以得到从0到n-1之间的一个随机数。

int r = (int) (Math.random() * n);

如果想确保不会再次抽到一个数,可以创建一个范围数组,当抽到一个数时,用数组中的最后一个数覆盖这个元素,并将n减一。

numbers[r] = numbers[n - 1];
n--;

关键在于每次抽取的都是索引,而不是实际的值。这个索引指向一个数组,其中包含尚未抽取过的值。

 

Arrays类

java.util.Arrays  1.2

·static String toString(xxx[] a)  5

返回一个字符串,其中包含a中的元素,这些元素用中括号包围,并用逗号分隔。在这个方法以及后面的方法中,数组元素类型xxx可以是int、long、short、char、byte、boolean、float或double。

·static xxx[] copyOf(xxx[] a, int end)  6

·static xxx[] copyOfRange(xxx[] a, int start, int end)  6

返回与a类型相同的数组,其长度为end或者end-start,并填入a的值。如果end大于a.length,结果会填充0或false值。

·static void short(xxx[] a)

使用优化的快速排序算法对数组进行排序。

·static int binarySearch(xxx[] a, xxx v)

·static int binarySearch(xxx[] a, int start, int end, xxx v)  6

使用二分查找算法在有序数组a中查找值v。如果找到v,则返回相应的索引;否则,返回一个负数值r。-r-1是v应插入的地方(为保持a有序)。

·static void fill(xxx[] a, xxx v)

将数组的所有元素设置为v。

·static boolean equals(xxx[] a, xxx[] b)

如果两个数组长度相同,并且相同索引对应的元素都相同,则返回true。

多维数组:

多维数组使用多个索引访问数组元素,它适用于表格或其他更复杂的排列形式。

假设需要建立一个数值表格,用来显示在不同利率下投资10000美元有多少收益,利息每年兑换并复投。

可以使用一个二维数组(也称为矩阵)来存储这些信息,名为balances。

在Java中,声明一个二维数组相当简单。例如:

double[][] balances;

其他情况下,如果知道数组元素,可以不调用new,而直接使用一种简写形式对多维数组进行初始化。例如:

int[][] magicSquare =
    {
        {16, 3, 2, 13},
        {5, 10, 11, 8},
        {9, 6, 7, 12},
        {4, 15, 14, 1}
    };

一旦初始化数组,就可以利用两对中括号访问单个元素,例如,balances[i][j]。

【注释:“for each”循环语句不会自动循环处理二维数组的所有元素。它会循环处理行,而这些行本身就是一维数组。要想访问二维数组a的所有元素。它会循环处理行,而这些行本身就是一维数组。要想访问二维数组a的所有元素,需要使用两个嵌套循环,如下所示:

for (double[] row : a)
    for (double value : row)
        do something with value

【提示:要想快速地打印一个二维数组的元素列表,可以调用:

System.out.println(Arrays.deepToString(a));

输出格式为:

[[16, 3, 2, 13], [5, 10, 11, 8], [9, 6, 7, 12], [4, 15, 14, 1]]

 

不规则数组:

到目前为止,我们看到的数组与其他程序设计语言中的数组没有多大区别。但在底层实际存在着一些细微的差异,有时你可以充分利用这一点:Java实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组”

例如,在前面的示例中,balances数组实际上是一个包含10个元素的数组,而每个元素又是一个由6个浮点数组成的数组。

表达式balances[i]指示第i个子数组,也就是表格的第i行,它本身也是一个数组,balances[i][j]指示这个数组的第j个元素。

由于可以单独地访问数组的某一行,所以可以让两行交换。

double[] temp = balances[i];
balances[i] = balances[i + 1];
balances[i + 1] = temp;

还可以很容易地构建一个“不规则”数组,即数组的每一行有不同的长度。下面是一个标准的示例。我们将创建一个数组,第i行第j列的元素将存放“从i个数中抽取j个数”可能的结果数。

1

1 1

1 2 1

1 3 3 1

1 4 6 4 1

1 5 10 10 5 1

1 6 15 20 15 6 1

由于j不可能大于i,所以矩阵是三角形的。第i行有i+1个元素(允许抽取0个元素,这种选择只有一种可能)。要想创建这样一个不规则的数组,首先需要分配一个数组包含这些行:

final int NMAX = 10;
int[][] odds = new int[NMAX + 1][];

接下来,分配这些行:

for (int n =0; n <= NMAX; n++)
    odds[n] = new int[n + 1];

分配了数组之后,可以采用通常的方式访问其中的元素(前提是没有超出边界):

for (int n =0; n <= NMAX; n++)
    for (int k = 0; k < odds[n].length; k++)
    {
        // compute lotteryOdds
        ...
        odds[n][k] = lotteryOdds;
    }

C++注释:在C++中,Java声明

 double[][] balances = new double[10][6]; // Java

不同于

double balances[10][6]; // C++

也不同于

double (*balances)[6] = new double[10][6]; // C++

【double (*balances)[6]的意思是指向数组(这里是每个一维数组含6个元素)的指针。

new了一个二维数组,那*balances应该是一个一维指针数组,包含10个元素】

而是分配了一个包含10个指针的数组:

double** balances=  new double*[10]; // C++

然后,这个指针数组的每一个元素会填充一个包含6个数字的数组:

for (i = 0; i < 10; i++)
    balances[i] = new double[6];

庆幸的是,当调用new double[10][6]时,这个循环是自动的。需要不规则的数组时,只能单独地分配行数组。】

现在,我们已经了解了Java语言地基本程序结构,下一章将介绍Java面向对象程序设计。


【学习参考书籍:《Java核心技术卷I》】

Java学习记录:java的基本程序设计结构(二)的评论 (共 条)

分享到微博请遵守国家法律