Java17-入门基础知识-四-

231 阅读1小时+

Java17 入门基础知识(四)

原文:Beginning Java 17 Fundamentals

协议:CC BY-NC-SA 4.0

六、语句

在本章中,您将学习:

  • 什么是语句

  • Java 中的表达式是什么,如何转换成表达式语句

  • 什么是块语句,块中声明的变量的范围是什么

  • 什么是控制流语句,如何使用if-elsefor-循环、while-循环和do-while循环语句

  • 如何使用break语句退出循环或程序块

  • 如何使用continue语句忽略循环语句体的其余部分,并继续下一次迭代

  • 什么是空语句以及在哪里使用它

  • 什么是开关表达式以及如何使用它

本章中的所有例子都在jdojo.statement模块中,其声明如清单 6-1 所示。

// module-info.java
module jdojo.statememnt {
    // No module statements
}

Listing 6-1The Declaration of a Module Named jdojo.statement

什么是语句?

一条语句指定了 Java 程序中的一个动作,比如将xy的和赋给z,将一条消息打印到标准输出,将数据写入文件,遍历一个值列表,有条件地执行一段代码,等等。语句是使用关键字、运算符和表达式编写的。

语句的类型

根据语句执行的动作,Java 中的语句可以大致分为三类:

  • 声明语句

  • 表达式语句

  • 控制流语句

后续部分详细描述了所有语句类型。

声明语句

声明语句用于声明变量。您已经使用了这种类型的语句。以下是 Java 中声明语句的几个例子:

int num;
int num2 = 100;
String str;

表达式语句

Java 中的表达式由文字、变量、运算符和方法调用组成;它们是 Java 程序的组成部分。对表达式求值;并且该评估可以产生变量、值或者什么都不产生。一个表达式总是有一个类型,如果是对返回类型为void的方法的方法调用,那么这个类型可能是void。以下是 Java 中表达式的几个例子:

  • 19 + 69

  • num + 2

  • num++

  • System.out.println("Hello")

  • new String("Hello")

末尾带分号的表达式称为表达式语句。然而,并不是所有的 Java 表达式都可以通过添加分号来转换成表达式语句。假设xy为两个int变量,下面是一个算术表达式,其计算结果为int值:

x + y

但是,以下不是 Java 中的有效表达式语句:

x + y;

允许这样的声明是没有意义的。它将xy的值相加,并且不对该值做任何处理。只有以下四种表达式可以通过在它们后面附加分号来转换为表达式语句:

  • 增量和减量表达式

  • 赋值表达式

  • 对象创建表达式

  • 方法调用表达式

增量和减量表达式语句的几个示例如下:

num++;
++num;
num--;
--num;

赋值表达式语句的几个例子如下:

num = 100;
num *= 10;

对象创建表达式语句的示例如下:

new String("This is a text");

注意,这条语句创建了一个新的String类对象。但是,新对象的引用不存储在任何引用变量中。所以这种说法用处不大。但是,在某些情况下,您可以以一种有用的方式使用这样的对象创建语句,例如,JDBC 驱动程序在加载驱动程序类时向驱动程序管理器注册自己,加载驱动程序类的一种方法是创建它的对象并丢弃已创建的对象。

您调用方法println()在控制台上打印一条消息。当你使用的println()方法末尾没有分号时,它就是一个表达式。当您在方法调用的末尾添加分号时,它就变成了一个语句。下面是一个方法调用表达式语句的示例:

System.out.println("This is a statement");

控制流语句

默认情况下,Java 程序中的所有语句都按照它们在程序中出现的顺序执行。但是,您可以使用控制流语句来更改执行顺序。有时,您可能希望仅在特定条件为真时才执行一条或一组语句。有时,您可能希望多次重复执行一组语句,或者只要特定条件为真。所有这些在 Java 中都可以使用控制流语句;iffor语句是控制流语句的例子。我们将很快讨论控制流语句。

块语句

block 语句是用大括号括起来的零个或多个语句的序列。block 语句通常用于将几个语句组合在一起,因此可以在需要使用单个语句的情况下使用它们。在某些情况下,您只能使用一条语句。如果您想在这些情况下使用多条语句,可以通过将所有语句放在大括号内来创建一条 block 语句,这将被视为一条语句。您可以将块语句视为一个复合语句,该语句被视为一个语句。以下是块语句的示例:

{ /* Start of a block statement. A block statement starts with { */
    int num1 = 20;
    num1++;
} /* End of the block statement. A block statement ends with } */
{
  // Another valid block statement with no statements inside
}

block 语句中声明的所有变量只能在该块中使用。换句话说,你可以说在一个块中声明的所有变量都有局部范围。考虑以下代码片段:

// Declare a variable num1
int num1;
{   // Start of a block statement
    // Declares a variable num2, which is a local variable for this block
    int num2;
    // num2 is local to this block, so it can be used here
    num2 = 200;
    // We can use num1 here because it is declared outside and before this block
    num1 = 100;
}   // End of the block statement
    // A compile-time error. num2 has been declared inside a block and
    // so it cannot be used outside that block
num2 = 50;

您也可以将一个 block 语句嵌套在另一个 block 语句中。封闭块(外部块)中声明的所有变量对于封闭块(内部块)都是可用的。但是,在封闭的内部块中声明的变量在封闭的外部块中不可用,例如:

// Start of the outer block
{
    int num1 = 10;
    // Start of the inner block
    {
        // num1 is available here because we are in an inner block
        num1 = 100;
        int num2 = 200; // Declared inside the inner block
        num2 = 678;     // OK. num2 is local to inner block
    }
    // End of the inner block
    // A compile-time error. num2 is local to the inner block.
    // So, it cannot be used outside the inner block.
    num2 = 200;
}
// End of the outer block

关于嵌套块语句,需要记住的重要一点是,如果在外部块中已经定义了同名的变量,则不能在内部块中定义该变量。这是因为在外部块中声明的变量总是可以在内部块中使用,如果在内部块中声明一个同名的变量,Java 就没有办法在内部块中区分这两个变量。以下代码片段无法编译:

int num1 = 10;
{
    // A Compile-time error. num1 is already in scope. Cannot redeclare num1
    float num1 = 10.5F;
    float num2 = 12.98F; // OK
    {
        // A compile-time error. num2 is already in scope.
        // You can use num2 already defined in the outer
        // block, but cannot redeclare it.
        float num2;
    }
}

if-else 语句

if-else语句的格式如下:

if (condition)
    statement1
else
    statement2

condition必须是一个boolean表达式。也就是说,它必须评估为truefalse。如果condition评估为true,则执行statement1。否则,执行statement2。一条if-else语句的流程图如图 6-1 所示。

img/323069_3_En_6_Fig1_HTML.png

图 6-1

if-else 语句的流程图

if-else语句中的else部分是可选的。如果缺少了else部分,该语句有时被简单地称为if语句。你可以写一个if声明如下:

if (condition)
    statement

一条if语句的流程图如图 6-2 所示。

img/323069_3_En_6_Fig2_HTML.png

图 6-2

if 语句的流程图

假设有两个名为num1num2int变量。假设您想在num1大于50的情况下将10加到num2上。否则你要从num2中减去10。您可以使用if-else语句来编写这个逻辑:

if (num1 > 50)
    num2 = num2 + 10;
else
    num2 = num2 - 10;

假设您有三个名为num1num2num3int变量。如果num1大于50,你想把10加到num2num3上。否则你要从num2num3中减去10。您可以尝试以下不正确的代码片段:

if (num1 > 50)
    num2 = num2 + 10;
    num3 = num3 + 10;
else
    num2 = num2 - 10;
    num3 = num3 - 10;

这段代码会生成一个编译时错误。这段代码有什么问题?在if-else语句中,只能在ifelse之间放置一条语句。这就是语句num3 = num3 + 10;导致编译时错误的原因。事实上,在一个if-else语句或一个简单的if语句中,您总是只能将一个语句与if部分相关联。对于else部分也是如此。在本例中,只有num2 = num2 - 10;else零件相关联;最后一条语句num3 = num3 - 10;else部分没有关联。无论num1是否大于50,你都要执行两条语句。在这种情况下,您需要将两条语句捆绑成一条块语句,如下所示:

if (num1 > 50) {
    num2 = num2 + 10;
    num3 = num3 + 10;
} else {
    num2 = num2 - 10;
    num3 = num3 - 10;
}

if-else语句可以嵌套,如下图所示:

if (num1 > 50) {
    if (num2 < 30) {
        num3 = num3 + 130;
    } else {
        num3 = num3 - 130;
    }
} else {
    num3 = num3 = 200;
}

有时,在嵌套的if-else语句中,很难确定哪个else与哪个if在一起。考虑下面这段代码:

int i = 10;
int j = 15;
if (i > 15)
if (j == 15)
    System.out.println("Thanks");
else
    System.out.println("Sorry");

这段代码的输出会是什么?它会打印"Thanks""Sorry",还是根本不打印任何东西?如果你猜到它不会打印任何东西,你已经了解了if-else协会。

您可以应用一个简单的规则来计算出在一个if-else语句中,哪个else对应哪个if。从else开始向上移动。如果您找不到任何其他的else语句,您找到的第一个if将与您开始的else一起使用。如果你在找到任何if之前找到一个else,那么第二个if将与你开始的else一起移动,以此类推。在本例中,从else开始,您找到的第一个ifif (j == 15),因此else与这个if一起出现。可以使用缩进和块语句重写前面的代码,如下所示:

int i = 10;
int j = 15;
if (i > 15) {
    if (j == 15) {
        System.out.println("Thanks");
    } else {
        System.out.println("Sorry");
    }
}

因为i等于 10,表达式i > 15将返回false,因此控件根本不会进入if语句。因此,不会有任何输出。

注意,if语句中的condition表达式必须是boolean类型。因此,如果您想比较两个int变量ij是否相等,您的if语句必须如下所示:

if (i == j)
    statement

你不能像这样写一个if语句:

if (i = 5) /* A compile-time error */
    statement

这个if语句不会被编译,因为i = 5是一个赋值表达式,它的值为int值 5。条件表达式必须返回一个boolean值:truefalse。因此,赋值表达式不能用作if语句中的条件表达式,除非您将boolean值赋给boolean变量,如下所示:

boolean b;
if (b = true) /* Always returns true */
    statement

这里,赋值表达式b = true总是在将true赋值给b后返回true。在这种情况下,允许在if语句中使用赋值表达式,因为表达式b = true的数据类型是boolean

您可以使用三元运算符来代替简单的if-else语句。假设,如果一个人是男性,你想将头衔设置为Mr.,如果不是,则设置为Ms.,你可以使用一个if-else语句和一个三元运算符来实现,如下所示:

String title;
boolean isMale = true;
// Using an if-else statement
if (isMale)
    title = "Mr.";
else
    title = "Ms.";
// Using a ternary operator
title = (isMale ? "Mr." : "Ms.");

您可以看到使用if-else语句和三元运算符的区别。使用三元运算符代码很紧凑。但是,您不能使用三元运算符来替换所有的if-else语句。只有当if-else语句中的ifelse部分只包含一个语句并且两个语句返回相同类型的值时,才可以使用三元运算符代替if-else语句。因为三元运算符是一个运算符,所以可以在表达式中使用。假设你想把ij中的最小值赋给k。您可以在变量k的以下声明语句中实现这一点:

int i = 10;
int j = 20;
int k = (i < j ? i : j); // Using a ternary operator in initialization

使用if-else语句也可以达到同样的效果,如下所示:

int i = 10;
int j = 20;
int k;
if (i < j)
    k = i;
else
    k = j;

使用三元运算符和if-else语句的另一个区别是,您可以使用将三元运算符作为方法参数的表达式。但是,您不能使用if-else语句作为方法的参数。假设您有一个接受一个int作为参数的calc()方法。你有两个整数,num1num2。如果您想将两个整数中的最小值传递给calc()方法,您应该编写如下所示的代码:

// Use an if-else statement
if (num1 < num2)
    calc(num1);
else
    calc(num2);
// Use a ternary operator
calc(num1 < num2 ? num1 : num2);

假设您想要打印消息"k is 15",如果变量int的值k等于15。否则,你要打印消息"k is not 15"。您可以使用三元运算符并编写一行代码来打印消息,如下所示:

System.out.println(k == 15 ? "k is 15" : "k is not 15");

switch 语句

switch语句的一般形式如下:

switch (switch-value) {
    case label1:
        statements
    case label2:
        statements
    case label3:
        statements
    default:
        statements
}

switch-value必须评估为一种类型:byteshortcharintenumString。有关如何在switch语句中使用enum类型的详细信息,请参考关于枚举的第二十二章。关于如何在switch语句中使用字符串的详细信息,参见第十五章。label1label2等。是编译时常量表达式,其值必须在switch-value的类型范围内。一条switch语句被评估如下:

  • The switch-value被评估。

  • 如果switch-value的值匹配一个case标签,则从匹配的case标签开始执行,并执行所有语句,直到switch语句结束。

  • 如果switch-value的值与case标签不匹配,则从可选的default标签后面的语句开始执行,直到switch语句结束。

以下代码片段是使用switch语句的示例:

int i = 10;
switch (i) {
    case 10: // Found the match
        System.out.println("Ten");       // Execution starts here
    case 20:
        System.out.println("Twenty");    // Also executes this statement
    default:
        System.out.println ("No-match"); // Also executes this statement
}
Ten
Twenty
No-match

i的值是 10。执行从case 10:之后的第一条语句开始,经过case 20:default标签,执行这些标签下的语句。如果您将i的值更改为 50,那么case标签中将没有任何匹配,执行将从default标签后的第一条语句开始,这将打印"No-match"。以下示例说明了这一逻辑:

int i = 50;
switch (i) {
    case 10:
        System.out.println("Ten");
    case 20:
        System.out.println("Twenty");
    default:
        System.out.println("No-match"); // Execution starts here
}
No-match

default标签不必是出现在switch语句中的最后一个标签,它是可选的。下面是一个不是最后一个标签的default标签的例子:

int i = 50;
switch (i) {
    case 10:
        System.out.println("Ten");
    default:
        System.out.println("No-match"); // Execution starts here
    case 20:
        System.out.println("Twenty");
}
No-match
Twenty

因为i的值是 50,与任何一个case标签都不匹配,所以执行从default标签后的第一条语句开始。控制通过随后的标签case 20:并执行该 case 标签后的语句,打印Twenty。一般来说,如果i的值是 10,你要打印Ten,如果i的值是20,你要打印Twenty。如果i的值既不是10也不是20,你想打印No-match。使用break关键字可以做到这一点。

当在switch语句中执行break语句时,控制权被转移到switch语句之外。下面是一个在switch语句中使用break语句的例子:

int i = 10;
switch (i) {
    case 10:
        System.out.println("Ten");
        break; // Transfers control outside the switch statement
    case 20:
        System.out.println("Twenty");
        break; // Transfers control outside the switch statement
    default:
        System.out.println("No-match");
        break; // Transfers control outside the switch statement. It is not necessary.
}
Ten

请注意前面代码片段中对break语句的使用。事实上,switch语句中的break语句的执行会停止switch语句的执行,并将控制权转移给switch语句之后的第一条语句(如果有的话)。在前面的代码片段中,在default标签中使用break语句是不必要的,因为default标签是switch语句中的最后一个标签,并且switch语句的执行将在此之后停止。然而,我建议即使在最后一个标签中也使用一个break语句,以避免以后添加额外标签时出现错误。

用作case标签的常量表达式的值必须在switch-value的数据类型范围内。记住 Java 中的byte数据类型的范围是–128 到 127,下面的代码不会编译,因为第二个case标签是150,它在byte数据类型的范围之外:

byte b = 10;
switch (b) {
    case 5:
        b++;
    case 150: // A compile-time error. 150 is outside the range -128 to 127
        b--;
    default:
        b = 0;
}

switch语句中的两个 case 标签不能相同。下面这段代码无法编译,因为case标签10重复了:

int num = 10;
switch (num) {
    case 10:
        num++;
    case 10: // A compile-time error. Duplicate label 10
        num--;
    default:
        num = 100;
}

需要注意的是,switch语句中每个case的标签必须是编译时常量。也就是说,标签的值必须在编译时已知。否则,会发生编译时错误。例如,下面的代码不会编译:

int num1 = 10;
int num2 = 10;
switch (num1) {
    case 20:
        System.out.println("num1 is 20");
    case num2: // A Compile-time error. num2 is a variable and cannot be used as a label
        System.out.println("num1 is 10");
}

你可能会说,当执行switch语句时,你知道num2的值是 10。但是,所有变量都是在运行时计算的。变量的值在编译时是未知的。因此,case num2:导致了编译器错误。这是必要的,因为 Java 在编译时确保所有的case标签都在switch-value的数据类型范围内。否则,那些 case 标签后面的语句将永远不会在运行时执行。

Tip

default标签是可选的。一条switch语句中最多只能有一个default标签。

if-else语句中的条件表达式比较相同变量的值是否相等时,switch语句是编写if-else语句的一种更清晰的方式。例如,下面的if-elseswitch语句完成了同样的事情:

// Using an if-else statement
if (i == 10)
    System.out.println("i is 10");
else if (i == 20)
    System.out.println("i is 20");
else
    System.out.println("i is neither 10 nor 20");
// Using a switch statement
switch (i) {
    case 10:
        System.out.println(“i is 10");
        break;
    case 20:
        System.out.println("i is 20");
        break;
    default:
        System.out.println("i is neither 10 nor 20");
}

开关表达式

Switch 表达式是作为 Java 12 中的预览特性和 Java 14 中的核心特性引入的。switch 表达式产生单个值,并使用单个表达式、throw 语句或代码块,而不是依赖于 break 关键字。这就产生了一个更清晰、更不容易出错的语法。

例如,将前面的 switch 语句转换为 switch 表达式,如下所示:

switch (i) {
    case 10 -> System.out.println("i is 10");
    case 20 -> System.out.println("i is 20");
    default -> System.out.println("i is neither 10 nor 20");
}

与 switch 语句不同,switch 表达式只产生一个值,因此前面的示例可以重写如下:

String message = switch (i) {
    case 10 -> "i is 10";
    case 20 -> "i is 20";
    default -> "i is neither 10 nor 20";
}
System.out.println(message);

开关表达式使用 case 标签,后跟->和下列之一:

  • 表达式,包括但不限于常量值

  • throw 语句

  • 使用左右花括号的代码块

此外,每个 case 标签可以支持多个用逗号分隔的值。例如,以下开关表达式使用每种类型中的一种:

String message = switch (i) {
    case 10, 15 -> "i is ten or fifteen";
    case 20 -> {
        String str = "i is";
        yield str + " twenty";
    }
    default -> throw new RuntimeException("i is not 10, 15, or 20");
}

第一个 case 语句将匹配 10 或 15。

yield 语句在开关表达式中用于指定开关表达式返回的值。

由于异常脱离了正在执行的当前方法或执行上下文,因此可能会引发异常。我们将在第十三章中全面介绍异常情况。

使用 yield 语句,switch 表达式也支持旧式的 case 标签(case L:),但是不能在同一个 switch 表达式中混合 case 标签类型。换句话说,您必须使用所有旧式的案例标签,或者一个都不使用。

for 语句

for语句是一个迭代语句,用于根据某些条件多次循环一个语句。它也被称为for循环语句或简称为for循环。for循环语句的一般形式是

for (initialization; condition-expression; expression-list)
    statement

initializationcondition-expressionexpression-list用分号隔开。一条for -loop 语句由四部分组成:

  • 初始化

  • 条件表达式

  • 声明

  • 表达式列表

首先,执行初始化部分;然后,对条件表达式求值。如果条件表达式的计算结果为true,则执行与for -loop 语句相关的语句。之后,计算表达式列表中的所有表达式。再次评估条件表达式,如果评估结果为true,则执行与for -loop 语句相关的语句,然后执行表达式列表,依此类推。这个执行循环一直重复,直到条件表达式的值为false。图 6-3 显示了for循环语句的流程图。

img/323069_3_En_6_Fig3_HTML.png

图 6-3

for 循环语句的流程图

例如,下面的for -loop 语句将打印 1 到 10 之间的所有整数,包括 1 和 10:

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

首先,int num = 1被执行,它声明了一个名为numint变量,并将其初始化为 1。需要注意的是,在for -loop 语句的初始化部分声明的变量只能在那个for -loop 语句中使用。然后对条件表达式num <= 10求值,为1 <= 10;它第一次评估为true。现在,执行与for -loop 语句相关的语句,打印num的当前值。最后,对表达式列表中的表达式num++进行求值,这将使num的值增加 1。此时,num的值变为 2。对条件表达式2 <= 10求值,返回true,并打印num的当前值。此过程持续到num的值变为 10 并被打印。之后,num++num的值设置为 11,条件表达式11 <= 10返回false,停止执行for -loop 语句。

一个for -loop 语句中的三个部分(初始化、条件表达式和表达式列表)都是可选的。请注意,第四部分(语句)不是可选的。因此,如果在for -loop 语句中没有要执行的语句,则必须使用空块语句或分号来代替语句。被当作语句的分号被称为空语句空语句。使用for -loop 语句的无限循环可以写成如下:

for( ; ; ) {
    // An infinite loop
}

前面的for -loop 语句可以用一个空语句重写,该语句是一个分号,如下所示:

// An infinite loop. Note a semicolon as a statement
for( ; ; );

下面是对for -loop 语句各部分的详细讨论。

初始化

for -loop 语句的初始化部分可以有一个变量声明语句,它可以声明一个或多个相同类型的变量,或者它可以有一个由逗号分隔的表达式语句列表。请注意,初始化部分使用的语句不以分号结尾。以下代码片段显示了for -loop 语句中的初始化部分:

// Declares two variables i and j of the same type int
for(int i = 10, j = 20; ; );
// Declares one double variable salary
for(double salary = 3455.78F; ; );
// Attempts to declare two variables of different types
for(int i = 10, double d1 = 20.5; ; ); /* A compile-time error */
// Uses an expression i++
int i = 100;
for(i++; ; ); // OK
// Uses an expression to print a message on the console
for(System.out.println("Hello"); ; );  // OK
// Uses two expressions: to print a message and to increment num
int num = 100;
for(System.out.println("Hello"), num++; ; );

Tip

当执行for循环时,for循环的初始化部分只执行一次。

您可以在for -loop 语句的初始化部分声明一个新变量。但是,您不能重新声明已经在范围内的变量:

int i = 10;
for (int i = 0; ; ); // An error. Cannot re-declare i

可以在for -loop 语句中重新初始化变量i,如下图:

int i = 10;      // Initialize i to 10
i = 500;         // Value of i changes here to 500
/* Other statements go here... */
for (i = 0; ; ); // Reinitialize i to zero inside the for-loop loop

条件表达式

条件表达式必须计算出truefalseboolean值。否则,会发生编译时错误。条件表达式是可选的。如果它被省略,trueboolean值被假定为条件表达式,这将导致无限循环,除非使用break语句来停止循环。以下两个for -loop 语句导致无限循环,它们是相同的:

// An infinite loop - Implicitly condition-expression is true
for( ; ; );
// An infinite loop - An explicit true is used as the condition-expression
for( ; true; );

break语句用于停止执行for循环语句。当一条break语句被执行时,控制被转移到for循环语句之后的下一条语句,如果有的话。您可以重写for -loop 语句,使用break语句打印 1 到 10 之间的所有整数:

// A for-loop with no condition-expression
for(int num = 1;  ; num++) {
    System.out.println(num); // Print the number
    if (num == 10) {
        break;               // Break out of loop when i is 10
    }
}

这个for -loop 语句打印与前面的for -loop 语句相同的整数。但是,不推荐使用后者,因为您正在使用一个break语句,而不是使用条件表达式来跳出循环。尽可能使用条件表达式来中断for循环是一个很好的编程实践。

表达式列表

表达式列表部分是可选的。它可能包含一个或多个由逗号分隔的表达式。您只能使用可以通过在末尾附加分号来转换为语句的表达式。有关更多详细信息,请参考本章开头对表达式语句的讨论。您可以重写打印 1 到 10 之间所有整数的相同示例,如下所示:

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

注意这个for -loop 语句在表达式列表中使用了两个表达式,用逗号分隔。一个for -loop 语句给了你更多的能力来编写紧凑的代码。

您可以如下重写前面的for -loop 语句,使其更加简洁并完成相同的任务:

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

请注意,您将表达式列表中的两个表达式合并为一个。您使用了num++作为println()方法的参数,所以它首先打印num的值,然后将其值递增 1。如果用++num代替num++,能否预测前面for循环语句的输出?

也可以使用嵌套的for -loop 语句,即for -loop 语句在另一个for -loop 语句内部。假设您想要打印一个 3 × 3(读作三乘三)矩阵,如下所示:

11      12      13
21      22      23
31      32      33

打印 3 × 3 矩阵的代码可以写成如下:

// Outer for-loop statement
for(int i = 1; i <= 3; i++) {
    // Inner for-loop statement
    for(int j = 1; j <= 3; j++) {
        System.out.print(i + "" + j);
        // Prints a tab after each column value
        System.out.print("\t");
    }
    System.out.println(); // Prints a new line
}

可以使用以下步骤来解释前面的代码:

  1. 执行从外层for-循环语句的初始化部分(int i = 1)开始,其中i被初始化为 1。

  2. 外部for-循环语句(i <= 3)的条件表达式被评估为i等于 1,这是真的。

  3. 外部for循环的语句部分以内部for循环语句开始。

  4. 现在j被初始化为 1。

  5. 内部for-循环语句(j <= 3)的条件表达式被评估为j等于 1,这是真的。

  6. 执行与内部for -loop 语句相关的 block 语句,打印 11 和一个制表符。

  7. 执行内部for-循环语句(j++)的表达式列表,将j的值增加到 2。

  8. 内部for-循环语句(j <= 3)的条件表达式被评估为j等于 2,这是真的。

  9. 执行与内部for -loop 语句相关的 block 语句,打印 12 和一个制表符。在此阶段,打印文本如下所示:

  10. 执行内部for-循环语句(j++)的表达式列表,将j的值增加到 3。

  11. 内部for-循环语句(j <= 3)的条件表达式被评估为j等于 3,这是真的。

  12. 执行与内部for -loop 语句相关的 block 语句,打印 13 和一个制表符。在这一阶段,打印文本如下所示:

11  12

  1. 执行内部for-循环语句(j++)的表达式列表,将j的值增加到 4。

  2. 内部for-循环语句(j <= 3)的条件表达式被评估为j等于 4,这是假的。至此,内for循环完成。

  3. 执行外部for -loop 语句的 block 语句的最后一条语句,即System.out.println()。它打印系统相关的行分隔符。

  4. 执行外部for-循环语句(i++)的表达式列表,将i的值增加到 2。

  5. 现在,内部的for -loop 语句重新开始,其中i的值等于 2。对于等于 3 的i,也执行这一系列步骤。当i变为 4 时,外部的for-循环语句退出,此时,打印出来的矩阵会是这样:

    11   12   13
    21   22   23
    31   32   33
    
    
11  12  13

请注意,这段代码还会在每一行的末尾打印一个制表符,并在最后一行之后打印一个新行,这不是必需的。需要注意的重要一点是,变量j是在每次内部for循环语句启动时创建的,当内部for循环语句退出时被销毁。因此,变量j被创建和销毁三次。您不能在内部for -loop 语句之外使用变量j,因为它已经在内部for -loop 语句中声明,并且它的作用域是内部for -loop 语句的局部。清单 6-2 包含本节讨论的完整代码。该程序确保不打印额外的制表符和新的行字符。

// PrintMatrix.java
package com.jdojo.statement;
public class PrintMatrix {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                System.out.print(i + "" + j);
                // Print a tab, except for the last number in a row
                if (j < 3) {
                    System.out.print("\t");
                }
            }
            // Print a new line, except after the last line
            if (i < 3) {
                System.out.println();
            }
        }
    }
}
11    12      13
21    22      23
31    32      33

Listing 6-2Using a for Loop to Print a 3 × 3 Matrix

for-each 语句

Java 5 引入了一个增强的for循环,称为for-each循环。它用于迭代数组和集合的元素。参考关于数组和集合的章节第十九章获得关于for-each循环的详细解释,以及通过集合元素循环的其他方法。for - each循环的一般语法如下:

for(Type element : a_collection_or_an_array) {
    // This code will be executed once for each element in
    // the collection/array.
    // Each time this code is executed, the element
    // variable holds the reference
    // of the current element in the collection/array
}

以下代码片段打印了一个int数组numList的所有元素:

// Create an array with 4 elements
int[] numList = {10, 20, 30, 40};
// Print each element of the array in a separate line
for(int num : numList) {
    System.out.println(num);
}
10
20
30
40

while 语句

while语句是另一个迭代(或循环)语句,用于在条件为真时重复执行一个语句。一条while语句也被称为while循环语句。while循环语句的一般形式是

while (condition-expression)
    statement

条件表达式必须是boolean表达式,语句可以是简单语句,也可以是 block 语句。首先计算条件表达式。如果它返回true,则执行该语句。再次计算条件表达式。如果返回true,则执行该语句。该循环继续,直到条件表达式返回false。图 6-4 显示了一条while语句的流程图。

img/323069_3_En_6_Fig4_HTML.png

图 6-4

while 语句的流程图

for -loop 语句不同,while -loop 语句中的条件表达式不是可选的。例如,要使while语句成为无限循环,需要使用boolean字面量true作为条件表达式:

while (true)
    System.out.println ("This is an infinite loop");

一般来说,for -loop 语句可以转换成while -loop 语句。然而,并不是所有的for循环语句都可以转换成while循环语句。这里显示了一个for循环和一个while循环语句之间的转换:

// A for-loop statement
for (initialization; condition-expression; expression-list)
    statement
// Equivalent while-loop Statements
initialization
while (condition-expression) {
    statement
    expression-list
}

您可以使用如下所示的while循环打印 1 到 10 之间的所有整数:

int i = 1;
while (i <= 10) {
    System.out.println(i);
    i++;
}

这个while loop可以用如下三种不同的方式重写:

// #1
int i = 0;
while (++i <= 10) {
    System.out.println(i);
}
// #2
int i = 1;
while (i <= 10) {
    System.out.println(i++);
}
// #3
int i = 1;
while (i <= 10) {
    System.out.println(i);
    i++;
}

break语句用于退出while循环语句中的循环。您可以使用break语句重写前面的示例,如下所示。请注意,下面这段代码只是为了说明在while循环中如何使用break语句;这不是使用break语句的好例子:

int i = 1;
while (true) { /* Cannot exit the loop from here because it is true */
    if (i <= 10) {
        System.out.println(i);
        i++;
    } else {
        break; // Exit the loop
    }
}

do-while 语句

do-while语句是另一个循环语句。它类似于while -loop 语句,但有一点不同。即使条件表达式第一次评估为false,与while循环语句相关的语句也不能执行一次。然而,与do-while语句相关的语句至少执行一次。do-while语句的一般形式是

do
    statement
while (condition-expression);

注意,do-while语句以分号结束。条件表达式必须是一个boolean表达式。该语句可以是简单语句,也可以是块语句。首先执行语句。然后对条件表达式求值。如果计算结果为true,则再次执行该语句。该循环继续,直到条件表达式评估为false。图 6-5 显示了一条do-while语句的流程图。

img/323069_3_En_6_Fig5_HTML.png

图 6-5

do-while 语句的流程图

像在for循环和while循环中一样,break语句可以用来退出do-while循环。一个do-while循环可以计算 1 到 10 之间的整数之和,如下所示:

int i = 1;
int sum = 0;
do {
    sum = sum + i; // Better to use sum += i
    i++;
}
while (i <= 10);
// Print the result
System.out.println("Sum = " + sum);
Sum = 55

什么时候用do-while语句代替while语句?您可以将每个do-while语句重写为while语句,反之亦然。然而,在某些用例中使用do-while语句会让你的代码更具可读性。考虑以下代码片段:

String filePath = "C:\\kishori\\poem.txt";
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line;
while((line = reader.readLine()) != null) {
    System.out.println(line);
}

该代码一次读取一行文件的内容,并将其打印在标准输出上。对于这段代码,我省略了错误检查和导入语句的细节。它使用一个while循环。下面的代码片段使用了一个do-while语句来做同样的事情:

String filePath = "C:\\kishori\\poem.txt";
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line;
do {
    line = reader.readLine();
    if (line != null) {
        System.out.println(line);
    }
 } while (line != null);

您可以看到,当您使用do-while语句时,逻辑并不流畅。在打印之前,您必须使用一个额外的if语句来检查一行之前是否被读取过。在这种情况下,使用while语句是更好的选择。

当循环的条件表达式依赖于循环内部计算的值时,您需要使用do-while语句。假设您需要要求用户输入一个介于 1 和 12 之间的月份值。程序会一直询问用户,直到输入一个有效值。在这种情况下,do-while语句更合适。清单 6-3 包含了完整的程序。我省略了错误检查,比如当用户输入文本而不是整数时。

// UserInput.java
package com.jdojo.statement;
import java.util.Scanner;
public class UserInput {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int month;
        do {
            System.out.print("Enter a month[1-12]: ");

            // Read an input from the user
            month = input.nextInt();
        } while (month < 1 || month > 12);
        System.out.println("You entered " + month);
    }
}
Enter a month[1-12]: 20
Enter a month[1-12]: -1
Enter a month[1-12]: 0
Enter a month[1-12]: 9
You entered 9

Listing 6-3Using a do-while Statement to Accept a Valid User Input

Scanner类用于从标准输入中读取输入。在这种情况下,键盘是标准输入。Scanner类的nextInt()方法从键盘中读取下一个整数。程序循环运行,直到用户输入 1 到 12 之间的整数。如果用户输入一个非整数值,程序将因出错而中止。

中断语句

break语句用于退出程序块。break语句有两种形式:

  • 未标记的break语句

  • 标记为break的语句

未标记的break语句的一个例子是

break;

带标签的break语句的一个例子是

break label;

您已经看到了未标记的break语句在switchfor-循环、while-循环和do-while语句中的使用。它将控制转移出它所在的switchfor-循环、while-循环或do-while语句。在这四种嵌套语句的情况下,如果在内部语句中使用了未标记的break语句,它只会将控制权转移到内部语句之外,而不会转移到外部语句之外。假设您想要打印 3 × 3 矩阵的下半部分,如下所示:

11
21      22
31      32      33

要仅打印 3 × 3 矩阵的下半部分,您可以编写以下代码片段:

for(int i = 1; i <= 3; i++) {
    for(int j = 1; j <= 3; j++) {
        System.out.print ( i + "" + j);
        if (i == j) {
            break; // Exit the inner for loop
        }
        System.out.print("\t");
    }
    System.out.println();
}
11
21    22
31    32      33

在内部for -loop 语句中使用了break语句。当外循环计数器(i)的值等于内循环计数器(j)的值时,执行break语句,内循环退出。如果你想从内for循环语句中退出外for循环语句,你必须使用带标签的break语句。Java 中的标签是后跟冒号的任何有效的 Java 标识符。以下是 Java 中的一些有效标签:

  • label1:

  • alabel:

  • Outer:

  • Hello:

  • IamALabel:

现在使用前一个例子中带标签的break语句,看看结果:

outer:  // Defines a label named outer
for(int i = 1; i <= 3; i++ ) {
    for(int j = 1; j <= 3; j++ ) {
        System.out.print(i + "" + j);
        if (i == j) {
            break outer;  // Exit the outer for loop
        }
        System.out.print("\t");
    }
    System.out.println();
}  // The outer label ends here

前面代码片段的输出如下:

11

为什么它只打印了 3 × 3 矩阵的一个元素?这次,您在内部的for循环语句中使用了一个带标签的break语句。当i == j第一次评估为true时,执行带标签的break语句。它将控制权从标记为outer的块中转移出来。请注意,outer标签正好出现在外部for循环语句之前。因此,与标签outer相关联的块是外部的for -loop 语句。带标签的语句不仅可以用在switchfor -loop、while -loop 和do-while语句中;相反,它可以用于任何类型的 block 语句。以下是一个带标签的break语句的简单示例:

blockLabel:
{
    int i = 10;
    if (i == 5) {
        break blockLabel; // Exits the block
    }

    if (i == 10) {
        System.out.println("i is not five");
    }
}

关于带标签的break语句,需要记住的重要一点是,与break语句一起使用的标签必须是使用带标签的break语句的块的标签。以下代码片段说明了带标签的break语句的不正确用法:

lab1:
{
    int i = 10;
    if (i == 10)
        break lab1; // Ok. lab1 can be used here
}
lab2:
{
    int i = 10;
    if (i == 10)
        // A compile-time error. lab1 cannot be used here
        // because this block is not associated with
        // lab1 label. We can use only lab2 in this block.
        break lab1;
}

continue 语句

continue语句只能在for循环、while循环和do-while语句中使用。continue报表有两种形式:

  • 未标记的continue语句

  • 标记为continue的语句

未标记的continue语句的一个例子是

continue;

带标签的continue语句的一个例子是

continue label;

当在for循环中执行continue语句时,循环体中的其余语句被跳过,表达式列表中的表达式被执行。您可以使用for -loop 语句打印 1 到 10 之间的所有奇数,如下所示:

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

在这个for -loop 语句中,您在表达式列表中将i的值增加 2。你可以用一条continue语句重写之前的for -loop 语句,如图 6-6 所示。

img/323069_3_En_6_Fig6_HTML.png

图 6-6

在 for-loop 语句中使用 continue 语句

对于是 2 的倍数的i的值,表达式i % 2返回 0,表达式i % 2 == 0返回true。在这种情况下,执行continue语句,跳过最后一条语句System.out.println(i)。增量语句i++continue语句执行后执行。前面的代码片段肯定不是使用continue语句的最佳例子;然而,它的目的是说明其用途。

当在while循环或do-while循环中执行未标记的continue语句时,循环中剩余的语句将被跳过,条件表达式将在下一次迭代中进行计算。例如,图 6-7 中的代码片段将使用while循环中的continue语句打印 1 到 10 之间的所有奇数。

img/323069_3_En_6_Fig7_HTML.png

图 6-7

在 while-loop 语句中使用 continue 语句

for循环和while循环中使用continue语句的主要区别在于控制转移的位置。在一个for循环中,控制被转移到表达式列表,而在一个while循环中,控制被转移到条件表达式。这就是为什么在不修改一些逻辑的情况下,for -loop 语句不能总是被转换成while -loop 语句。

未标记的continue语句总是继续最内层的for循环、while循环和do-while循环。如果你正在使用嵌套循环语句,你需要使用一个带标签的continue语句来继续外层循环。例如,您可以使用如下所示的continue语句重写打印 3 × 3 矩阵下半部分的代码片段:

outer: // The label "outer" starts here
for(int i = 1; i <= 3; i++) {
    for(int j = 1; j <= 3; j++) {
        System.out.print(i + "" + j);
        System.out.print("\t");
        if (i == j) {
            System.out.println(); // Print a new line
            continue outer;       // Continue the outer loop
        }
    }
}  // The label "outer" ends here

空洞的声明

空语句本身就是一个分号。一个空的语句没有任何作用。如果一个空的声明没有任何作用,我们为什么要有它?有时,语句是结构语法的一部分。然而,你可能不需要做任何有意义的事情。在这种情况下,使用空语句。一个for循环必须有一个与之相关联的语句。然而,要打印 1 到 10 之间的所有整数,您只能使用一个for -loop 语句的初始化、条件表达式和表达式列表部分。在这种情况下,您没有与for循环语句相关联的语句。因此,在这种情况下使用空语句,如下所示:

for(int i = 1; i <= 10; System.out.println(i++))
;  // This semicolon is an empty statement for the for loop

有时,空语句用于避免代码中的双重否定逻辑。假设noDataFound是一个boolean变量。您可以编写如下所示的代码片段:

if (noDataFound)
    ; // An empty statement
else {
      // Do some processing
}

前面的if-else语句可以不使用空语句来编写,如下所示:

if (!noDataFound) {
    // Do some processing
}

使用哪种代码是个人的选择。最后,请注意,如果您在只需要一个分号的地方键入两个或更多分号,这不会导致任何错误,因为每个多余的分号都被视为一个空语句,例如:

i++;  // Ok. Here, semicolon is part of statement
i++;; // Still Ok. The second semicolon is considered an empty statement.

在不允许使用语句的地方,不能使用空语句。例如,当只允许一个语句时,添加额外的空语句将导致错误,如下面的代码片段所示。它将两个语句i++;和一个空语句(;)关联到一个if语句,其中只允许一个语句:

if (i == 10)
    i++;; // A compile-time error. Cannot use two statements before an else statement
else
    i--;

摘要

Java 程序中的语句指定一个动作。Java 中的语句可以大致分为三类:声明语句、表达式语句和控制流语句。声明语句用于声明变量。表达式语句用于计算表达式的值。控制流语句控制其他语句的执行顺序。控制流语句包括ifif-else和循环语句。循环语句重复执行语句块,直到某个条件变为假。Java 提供了四个循环语句:for循环、for-each循环、while循环和do-while循环。break语句用于将控制转移到 block 语句或循环之外。continue语句用于忽略执行循环的剩余代码,并继续下一次迭代。Java 也有一个空语句,它本身就是一个分号。

EXERCISES

  1. 什么是语句?

  2. 什么是表达式?Java 中如何把一个表达式转换成表达式语句?可以把 Java 中所有类型的表达式都转换成表达式语句吗?

  3. 什么是控制语句,你为什么使用它们?

  4. 什么是 block 语句,如何创建 block 语句?

  5. 什么是空言?

  6. while -loop 和do-while语句有什么区别?

  7. 一个switch语句包含一个switch-value。列出一个switch-value必须评估的所有类型。

  8. 什么时候可以用switch语句代替if-else语句?

  9. 考虑下面的代码片段。count变量的有效值必须在 11(含)到 20(含)的范围内。编写if-else语句的条件,以便打印出正确的消息:

    int count = 20;
    if(<your-code-goes-here>)
        System.out.println("Count is valid.");
    else
        System.out.println("Count is invalid");
    
    
  10. 修复以下代码片段中的编译时错误。确保固定代码打印出y :

```java
int x = 10;
int y = 20;
if (x = 10)
    y++;
    System.out.println("y = " + y);
else
    y--;
    System.out.println("y = " + y);

```

的值

11. 使用if-else语句重写以下代码片段。当您将变量x初始化为另一个值时,确保switchif-else语句具有相同的输出。(提示:这是一个棘手的问题,因为在任何case标签中都没有break语句。)

```java
int x = 50;
switch (x) {
    case 10:
        System.out.println("Ten");
    default:
        System.out.println("No-match");
    case 20:
        System.out.println("Twenty");
}

```

12. 下面的代码片段是上一个代码片段的修改版本。使用 if-else 语句重写它。当您将变量 x 初始化为另一个值:

```java
int x = 50;
switch (x) {
    case 10:
        System.out.println("Ten");
        break;
    default:
        System.out.println("No-match");
        break;
    case 20:
        System.out.println("Twenty");
        break;
}

```

时,确保 switchif-else 语句具有相同的输出

13. 一名程序员正在学习switch语句,他们试图在任何可能的地方使用它。下面的代码片段是在不需要的地方强制使用的一个例子。不使用控制流语句重写以下代码片段。也就是说,你需要去掉switch语句,让程序逻辑保持不变:

```java
int x = 10;
// Some logic goes here...
switch(x) {
    default:
    x++;
}

```

14. 如何使用forwhiledo-while语句编写一个无限循环?举一个例子。

  1. 下面的for语句的目的是以相反的顺序打印从 1 到 10 的整数。代码没有按预期打印数字。识别逻辑错误并修复代码,这样它会输出 10,9,8,…1:
```java
for(byte b = 10; b >= 1; b++)
    System.out.println(b);

```

16. 写一个for语句,以逆序打印从 13 到 1 的所有奇数。for语句的主体必须是空语句。也就是说,您可以只使用for语句的初始化、条件表达式和表达式列表来编写您的所有逻辑。您的for声明模板如下:

  1. 使用for语句编写一段代码,计算从 1 到 10 的所有整数之和,并在标准输出中打印出来。您的代码模板如下:

    int sum = 0;
    for(<your-code>; <your-code>; <your-code>);
    System.out.println("Sum = " + sum);
    
    
  2. 使用嵌套的for语句打印下面的金字塔。

        *
       ***
      *****
     *******
    
    
  3. 编写一个嵌套的for语句,它将打印以下内容:

         1
        22
       333
      4444
     55555
    666666
    
    
  4. 完成以下代码片段。它应该打印从lowerupper的所有整数的逗号分隔列表。比如lower是 1,upper是 4,就应该打印1, 2, 3, 4。(提示:使用System.out.print()打印不带新行的消息。)

    int lower = 1;
    int upper = 4;
    for(<your-code-goes-here>) {
        <your-code-goes-here>
    
    
for(<your-code>; <your-code>; <your-code>);

七、类

在本章中,您将学习:

  • Java 中有哪些类

  • 如何在 Java 中定义类

  • 如何声明类成员,如字段

  • 如何创建一个类的对象

  • 如何在编译单元中声明 import 语句

  • 如何在 Java 中定义记录

什么是课?

类是面向对象范例中编程的基本单位。在第三章中,你看到了 Java 中一个类的一些基本方面,例如,使用class关键字声明一个类,声明main()方法运行一个类,等等。本章详细解释了如何声明和使用一个类。

让我们从现实世界中一个简单的类的例子开始,来构建 Java 中一个类的技术概念。当你环顾四周,你会看到许多物体,比如书、电脑、键盘、桌子、椅子、人等等。您看到的每个对象都属于一个类。问自己一个简单的问题,“我是谁?”你显然会回答:我是人。你说你是人类是什么意思?你的意思是世界上存在一个人类阶级,而你是那个阶级的一个实例(“存在”)。你也明白其他人类(人类类的其他实例)也存在,他们与你相似,但不相同。你和你的朋友都是同一人类类的实例,具有相同的属性,如姓名、性别、身高、体重和行为,如思考、说话、行走的能力等。然而,对你和你的朋友来说,属性和行为在价值、质量或两者方面都不同。例如,两者都有名字和说话的能力。然而,你的名字可能是理查德,你的朋友的名字可能是格雷格。你可能说得慢,而你的朋友可能说得快。如果你想为你和你的朋友准备一个模型来检查你的行为,有两个选择:

  • 你可以分别列出你和你的朋友的所有属性和行为,并分别检查它们,就好像你和你的朋友之间没有联系一样。

  • 您可以列出您和您的朋友共有的属性和行为,然后在不指明您和您的朋友的情况下,将它们作为实体的属性和行为进行检查。该模型假设所有列出的属性和行为都将出现在一个实体中(没有命名),尽管它们可能因实体而异。您可能希望将您和您朋友的所有属性和行为作为一个类的属性和行为列出,比如说 human,并将您和您的朋友视为该 human 类的两个不同实例。本质上,您已经将具有相似属性和行为的实体(例如,您和您的朋友)分组在一起,并将该组称为类。然后,您将把所有对象(同样,您和您的朋友)视为该类的实例。

第一种方法将每个对象视为一个独立的实体。在第二种方法中,基于属性和行为的相似性对对象进行分类,其中对象总是属于一个类;类成为编程的基本部分。要确定对象的任何属性或行为,您需要查找它的类定义。例如,你是人类类的一个对象。你会飞吗?这个问题可以通过一系列步骤来回答。首先,你需要回答这个问题,“你属于哪个阶层?”答案是你属于人类阶级。人类类是否定义了一种飞行行为?答案是否定的。因为你是没有定义飞行行为的人类类的实例,所以你不能飞行。如果你仔细观察你得出答案的方式,你会发现这个问题是对一个物体(你)提出的,但答案是由这个物体所属的类(人)提供的。

类是必不可少的,它们是面向对象编程中程序的基本部分。它们被用作创建对象的模板。如何在 Java 中定义一个类?Java 中的一个类可能由五个部分组成:

  • 菲尔茨

  • 方法

  • 构造器

  • 静态初始化器

  • 实例初始化器

字段和方法也称为类的成员。类和接口也可以是类的成员。本章只关注字段。一个类可以有零个或多个类成员。一个类的类成员也称为嵌套类

类似于在婴儿出生时给出人的初始特征,如名字、性别、身高和体重,新创建的对象的属性在对象被创建时被初始化。在 Java 中,给对象的属性赋予初始值叫做初始化一个对象。构造器用于初始化一个类的对象。一个类必须至少有一个构造器。

初始化器用于初始化类的字段。您可以有零个或多个静态或实例类型的初始值设定项。初始化器和构造器执行相同的任务。初始化器也可以用来初始化类级别的字段,而构造器只能初始化对象级别的字段。

本章的其余部分将讨论如何声明和使用一个类的字段。

声明类

用 Java 声明类的一般语法如下:

[modifiers] class <class-name> {
    // Body of the class goes here
}

这里

  • modifiers是可选的;它们是将特殊含义与类声明相关联的关键字。一个类声明可以有零个或多个修饰符。

  • 关键字class用于声明一个类。

  • class-name是用户定义的类名,应该是有效的 Java 标识符。

  • 每个类都有一个主体,在一对大括号({})中指定。一个类的主体包含它的不同组成部分,例如,字段、方法等。

下面的代码片段定义了一个名为Human的类,其主体为空。注意Human类不使用任何修饰符:

// Human.java
class Human {
    // An empty body for now
}

下面的代码片段定义了一个名为Human的公共类,其主体为空。注意,这个声明使用了一个public修饰符:

// Human.java
public class Human {
    // An empty body for now
}

我将在本章后面详细解释公共类和其他类型类的区别。

在类中声明字段

类的字段表示该类对象的属性(也称为特性)。假设一个Human类的每个对象都有两个属性:一个名字和一个性别。Human类应该包含两个字段的声明:一个表示姓名,一个表示性别。

这些字段是在类体中声明的。在类中声明字段的一般语法是

[modifiers] class <class-name> {
    // A field declaration
    [modifiers] <data-type> <field-name> [= <initial-value>];
}

一个字段声明可以使用零个或多个modifiers。字段的数据类型位于其名称之前。或者,您也可以用一个值初始化每个字段。如果你不想初始化一个字段,它的声明应该在它的名字后面用分号结束。

有了两个字段namegender,Human类的声明如下所示:

// Human.java
class Human {
    String name;
    String gender;
}

Tip

Java 中有一个约定(不是规则或要求),以大写字母开始类名,后面的单词大写,例如,HumanTableColorMonitor等。字段和方法的名称要以小写字母开头,后面的单词要大写,例如namefirstNamemaxDebitAmount等。

Human类声明了两个字段:namegender。两个字段都是String类型。Human类的每个实例(或对象)都有这两个字段的副本。

有时属性属于类本身,而不属于该类的任何特定实例。例如,所有人类的计数不是任何特定人类的属性。相反,它属于人类阶级本身。人类计数的存在不依赖于人类类的任何特定实例,即使人类类的每个实例都对 count 属性的值有贡献。不管该类有多少个实例,都只存在一个类属性副本。但是,类的每个实例都有一个单独的实例属性副本。例如,namegender属性的单独副本存在于Human类的每个实例中。你总是指定一个人的名字和性别。然而,即使没有Human类的实例,也可以说Human类实例的数量为零。

Java 允许您为一个类声明两种类型的字段:

  • 类别字段

  • 实例字段

类字段也被称为类变量。实例字段也被称为实例变量。在前面的代码片段中,namegenderHuman类的两个实例变量。Java 有一种不同的方法来声明类变量。所有的类变量都必须使用关键字static作为修饰符来声明。清单 7-1 中Human类的声明增加了一个count类变量。

// Human.java
package com.jdojo.cls;
class Human {
    String name;        // An instance variable
    String gender;      // An instance variable
    static long count;  // A class variable because of the static modifier
}

Listing 7-1Declaration of a Human Class with One Class Variable and Two Instance Variables

Tip

一个类变量也被称为静态变量。实例变量也被称为非静态变量

创建类的实例

下面是创建类实例的一般语法:

new <Call-to-Class-Constructor>;

new操作符之后是对正在创建实例的类的构造器的调用。new操作符通过在堆上分配内存来创建一个类的实例。下面的语句创建了一个Human类的实例:

new Human();

这里,Human()是对Human类的构造器的调用。你给你的Human类添加了构造器了吗?不,你没有添加任何构造器到你的Human类中。您只添加了三个字段。你怎么能为一个没有添加的类使用构造器呢?当您没有向类中添加构造器时,Java 编译器会为您添加一个。Java 编译器添加的构造器称为默认构造器。默认构造器不接受任何参数。类的构造器的名称与类名相同。我们将在第九章中详细讨论构造器。

当一个类的实例被创建时会发生什么?new操作符为类的每个实例字段分配内存。回想一下,在创建类的实例时,没有给类变量分配内存。图 7-1 描述了内存中Human类的一个实例。

img/323069_3_En_7_Fig1_HTML.png

图 7-1

由 new Human()实例创建表达式在内存中创建的 Human 类的实例

图 7-1 显示内存是为实例变量namegender分配的。您可以创建任意多的Human类的实例。每次创建Human类的实例时,Java 运行时都会为namegender实例变量分配内存。为一个Human类的实例分配了多少内存?简单的答案是,您不知道一个类的实例使用了多少内存,事实上,您也不需要知道这一点。Java 运行时会自动为您处理内存分配和释放。

现在,您想要向前移动一步,并为新创建的Human类实例的namegender实例变量赋值。您能给新创建的Human类实例的namegender实例变量赋值吗?答案是否定的。您不能访问namegender实例变量,即使它们存在于内存中。要访问一个类实例的实例变量,你必须有它的引用(或句柄)。表达式new Human()在内存中创建了一个Human类的新实例。新创建的实例就像一个充满氦气的气球留在空中。当你在空中释放一个充满氦气的气球时,你就失去了对气球的控制。如果在气球释放到空中之前给它系上一根绳子,你可以用这根绳子控制气球。类似地,如果您想要控制(或访问)一个类的实例,您必须将该实例的引用存储在一个引用变量中。你用一根绳子控制一个气球;你用遥控器控制电视。控制设备的类型取决于您想要控制的对象的类型。类似地,您需要使用不同类型的引用变量来引用(或处理或使用)不同类的实例。

在 Java 中,类名定义了一个新的引用类型。特定引用类型的变量可以在内存中存储相同引用类型的实例的引用。假设您想要声明一个引用变量,它将存储一个对Human类实例的引用。您将如下所示声明变量:

Human jack;

这里,Human是类名,也是引用类型,jack是该类型的变量。换句话说,jack是一个Human类型的参考变量。jack变量可以用来存储Human类实例的引用。

操作符为一个类的新实例分配内存,并返回对该实例的引用(或间接指针)。您需要将由new操作符返回的引用存储在一个引用变量中:

jack = new Human();

注意jack本身是一个变量,它会被单独分配内存。jack变量的内存位置将存储新创建的Human类实例的内存位置的引用。图 7-2 描述了当引用变量jack被声明时,以及当Human类的实例被创建且其引用被分配给jack变量时的内存状态。

img/323069_3_En_7_Fig2_HTML.png

图 7-2

当引用变量被声明时,以及当引用变量被赋予一个类的实例的引用时,内存状态

您可以将jack变量视为内存中Human实例的远程控制器。您可以使用jack变量引用内存中的Human实例。我们将在下一节讨论如何使用引用变量。您也可以将两个语句合并成一个:

Human jack = new Human();

空引用类型

Java 中的每个类都定义了一个新的引用类型。Java 有一种特殊的引用类型,称为空类型。它没有名字。因此,不能定义空引用类型的变量。空引用类型只有一个由 Java 定义的值,即null文字。简直就是null。null 引用类型与所有其他引用类型都是赋值兼容的。也就是说,你可以将null赋给任何引用类型的变量。实际上,null存储在引用变量中意味着引用变量不引用任何对象。你可以把null存储在一个引用变量中,作为一个不带气球的字符串,其中气球是一个有效对象,字符串是一个引用变量。例如,您可以编写如下代码:

// Assign null to john
Human john = null;  // john is not referring to any object
john = new Human(); // Now, john is referring to a valid Human object

您可以使用带有比较运算符的null来检查相等和不相等:

if (john == null) {
    // john is referring to null. Cannot use john for anything
} else {
    // Do something with john
}

如果你对一个null引用执行一个操作,一个NullPointerException被抛出:

Human john = null;
// The following statement throws a NullPointerException because john is null and you
// cannot use any operation on a null reference variable
String name = john.name;

注意null是 null 类型的文字。Java 不允许混合引用类型和原始类型。不能将null赋给原始类型的变量。以下赋值语句将生成编译时错误:

// A compile-time error. A reference type value, null, cannot be assigned to
// a primitive type variable num
int num = null;

因为null(或任何引用类型的值)不能赋给一个原始类型的变量,所以 Java 编译器不允许您将原始值与null值进行比较。下面的比较将产生一个编译时错误。换句话说,您可以将引用类型与其他引用类型进行比较,并将基元类型与其他基元类型进行比较:

int num = 0;
// A compile-time error. Cannot compare a primitive type to a reference type
if (num == null) {
}

Tip

Java 有一个特殊的引用类型,叫做 null 类型。空类型没有名称。null 类型有一个文字值,用null表示。null 类型与所有其他引用类型都是赋值兼容的。您可以为任何引用类型变量分配一个null值。您可以将null值转换为任何引用类型。需要强调的是,null是“空引用类型”的文字值,而不是关键字。

使用点符号访问类的字段

点符号用于引用实例变量。点符号语法的一般形式如下:

<reference-variable-name>.<instance-variable-name>

例如,使用jack.name来引用jack引用变量所引用的实例的name实例变量。如果您想给name实例变量赋值,您可以使用下面的方法:

jack.name = "Jack Parker";

以下语句将name实例变量的值赋给String变量aName:

String aName = jack.name;

如何引用类变量?使用点符号有两种方法引用类变量:

  • 使用类的名称

  • 使用类实例的引用

您可以使用类的名称来引用类变量:

<class-name>.<class-variable-name>

例如,你可以使用Human.count来引用Human类的count类变量。要给count类变量赋一个新值,比如 101,可以这样写:

Human.count = 101;

要将count类变量的值读入一个名为population的变量,您可以使用:

long population = Human.count;

还可以使用引用变量来引用类的类变量。例如,你可以使用jack.count来引用Human类的count类变量。您可以使用下面的语句给count类变量赋值,比如 101:

jack.count = 101;

以下语句将count类变量的值读入名为population的变量:

long population = jack.count;

这两个语句都假设jack是一个Human类型的引用变量,并且它引用一个有效的Human实例。

Tip

您可以使用类名或类类型的引用变量来引用类变量。因为类变量属于类,并且由类的所有实例共享,所以使用类名引用它是合乎逻辑的。但是,您必须始终使用类类型的引用变量来引用实例变量。

是时候看看Human类中的字段了。本章中的大多数类都是jdojo.cls模块的一部分,如清单 7-2 中所声明的。模块名中的cls是 class 的简称。你不能使用jdojo.class作为模块名,因为class是一个关键字。该模块输出一个com.jdojo.cls包。您还没有学习模块声明中的exports语句。我在这一章解释它。

// module-info.class
module jdojo.cls {
    exports com.jdojo.cls;
}

Listing 7-2Declaration of the jdojo.cls Module

清单 7-3 有一个完整的程序,演示了如何访问一个类的类变量和实例变量。

// FieldAccessTest.java
package com.jdojo.cls;
class FieldAccessTest {
    public static void main(String[] args) {
        // Create an instance of the Human class
        Human jack = new Human();
        // Increase count by one
        Human.count++;
        // Assign values to name and gender
        jack.name = "Jack Parker";
        jack.gender = "Male";
        // Read and print the values of name, gender and count
        String jackName = jack.name;
        String jackGender = jack.gender;
        long population = Human.count;
        System.out.println("Name: " + jackName);
        System.out.println("Gender: " + jackGender);
        System.out.println("Population: " + population);
        // Change the name
        jack.name = "Jackie Parker";
        // Read and print the changed name
        String changedName = jack.name;
        System.out.println("Changed Name: " + changedName);
    }
}
Name: Jack Parker
Gender: Male
Population: 1
Changed Name: Jackie Parker

Listing 7-3Using Fields in a Class Declaration

该程序中的以下语句需要一些解释:

// Increase count by one
Human.count++;

它在count类变量上使用增量运算符(++)。在count类变量增加 1 后,您读取并打印它的值。输出显示其值增加 1 后,其值变为1。这意味着在执行Human.count++语句之前,它的值为零。但是,您从未将其值设置为零。其声明如下:

static long count;

当如前所示声明count类变量时,默认情况下它被初始化为零。如果没有给一个类的所有字段(类变量和实例变量)赋一个初始值,那么它们都被初始化为默认值。下一节描述用于初始化类的字段的规则。

字段的默认初始化

一个类的所有字段,静态的和非静态的,都被初始化为默认值。字段的默认值取决于其数据类型:

  • 一个数值字段(byteshortcharintlongfloatdouble)被初始化为零。

  • 一个boolean字段被初始化为false

  • 引用类型字段被初始化为null

根据这些规则,Human类的字段将被初始化如下:

  • count类变量被初始化为零,因为它是数字类型。这就是Human.count++评估为1 ( 0 + 1 = 1)的原因,如清单 7-3 的输出所示。

  • namegender实例变量属于String类型。String是引用类型。它们被初始化为null。回想一下,namegender字段的副本存在于Human类的每个对象中,并且namegender的每个副本被初始化为null

如果您考虑默认初始化Human类的字段,它的行为就好像您已经声明了Human类,如下所示。这个Human类的声明和清单 7-1 中显示的声明是相同的:

class Human {
    String name = null;
    String gender = null;
    static long count = 0;
}

清单 7-4 展示了字段的默认初始化。DefaultInit类只包含实例变量。类字段使用与实例字段相同的默认值进行初始化。如果将DefaultInit类的所有字段声明为static,输出将是相同的。该类包括两个引用类型的实例变量,strjack,它们是StringHuman类型。注意StringHuman都是引用类型,默认情况下null被分配给它们的引用。

// DefaultInit.java
package com.jdojo.cls;
class DefaultInit {
    byte b;
    short s;
    int i;
    long l;
    float f;
    double d;
    boolean bool;
    String str;
    Human jack;

    public static void main(String[] args) {
        // Create an object of DefaultInit class
        DefaultInit obj = new DefaultInit();
        // Print the default values for all instance variables
        System.out.println("byte is initialized to " + obj.b);
        System.out.println("short is initialized to " + obj.s);
        System.out.println("int is initialized to " + obj.i);
        System.out.println("long is initialized to " + obj.l);
        System.out.println("float is initialized to " + obj.f);
        System.out.println("double is initialized to " + obj.d);
        System.out.println("boolean is initialized to " + obj.bool);
        System.out.println("String is initialized to " + obj.str);
        System.out.println("Human is initialized to " + obj.jack);
    }
}
byte is initialized to 0
short is initialized to 0
int is initialized to 0
long is initialized to 0
float is initialized to 0.0
double is initialized to 0.0
boolean is initialized to false
String is initialized to null
Human is initialized to null

Listing 7-4Default Initialization of Class Fields

类的访问级别修饰符

在清单 7-1 中,您在com.jdojo.cls包中创建了Human类。您使用了清单 7-3 中的Human类来创建它在FieldAccessTest类中的对象,该类与Human类在同一个模块和同一个包中。编译和运行清单 7-3 中的以下语句没有问题:

Human jack = new Human();

让我们在jdojo.cls模块的com.jdojo.common包中创建一个名为ClassAccessTest的类。注意ClassAccessTestHuman类在不同的包中。ClassAccessTest类声明如下:

// ClassAccessTest.java
package com.jdojo.common;
public class ClassAccessTest {
    public static void main(String[] args) {
        Human jack;
    }
}

ClassAccessTest类的代码非常简单。它只做一件事——在其main()方法中声明一个Human类型的引用变量。编译ClassAccessTest类。哎呀!您得到了一个编译时错误:

ClassAccessTest.java:6: error: cannot find symbol
     Human jack;
        ^
  symbol:   class Human
  location: class ClassAccessTest
1 error

如果您仔细阅读错误,编译器会抱怨以下变量声明中的类型Human:

Human jack;

编译器声明它找不到术语Human的定义。用jack变量声明的ClassAccessTest类有什么问题?当您通过类的简单名称来引用一个类时,编译器会在引用类所在的同一个包中查找该类声明。在您的例子中,引用的类ClassAccessTestcom.jdojo.common包中;它使用简单的名字Human来引用Human类。因此,编译器在com.jdojo.common包中寻找Human类。编译器正在寻找一个不存在的com.jdojo.common.Human类。这是您收到错误的原因。

通过在ClassAccessTest中使用简单的名称Human,您的意思是指com.jdojo.cls包中的Human类,而不是com.jdojo.common包中的类。如果在com.jdojo.common包中有Human类,那么ClassAccessTest的代码就会被编译。让我们假设您没有一个com.jdojo.common.Human类,并且您想要修复这个错误。您可以通过使用Human类的完全限定名来修复它,就像这样:

// ClassAccessTest.java
package com.jdojo.common;
public class ClassAccessTest {
    public static void main(String[] args) {
        com.jdojo.cls.Human jack;
    }
}

现在编译ClassAccessTest类。哎呀!您又遇到了编译时错误。然而,这一次,错误不同了:

ClassAccessTest.java:6: error: Human is not public in com.jdojo.cls; cannot be accessed from outside package
        com.jdojo.cls.Human jack;
                     ^
1 error

这一次,编译器并不是说它不理解Human类型。是说它知道什么是com.jdojo.cls.Human型;然而,它只能在声明它的com.jdojo.cls包中访问。换句话说,Human类型在com.jdojo.common包中是不可访问的。这里出现了类的访问级别的概念。

当声明一个类时,还可以指定该类是可以从任何包中访问(或使用或引用),还是只能从声明该类的包中访问。例如,您可以在Human类的声明中指定是只能从com.jdojo.cls包中访问它,还是可以从任何包中访问它,包括com.jdojo.common包。指定类的访问级别的一般语法如下:

[access-level-modifier] class <class-name> {
    // Body of the class goes here
}

类声明中的访问级别修饰符只有两个有效值:无值和public:

  • 无值:与没有访问级别修饰符相同。它也被称为包级访问。如果一个类具有包级访问权限,那么它只能在声明它的包中被访问。清单 7-1 中的Human类拥有包级访问权限。这就是您能够使用(或访问)清单 7-3 中的FieldAccessTest类中的Human类的原因。注意,Human类和FieldAccessTest类在同一个包中,并且都有包级访问。因此,它们可以相互参照。Human类在com.jdojo.cls包中,它有包级访问。因此,不能从任何其他包访问它,例如com.jdojo.common。这就是你试图编译ClassAccessTest类时收到错误的原因。

  • 公共:带有public访问级别修饰符的类可以从同一个模块中的任何包中访问。如果您想让Human类可以从任何包(例如com.jdojo.common)中访问,您需要将它声明为public

模块M中声明的类C可以在模块N中访问吗?这个问题的答案取决于类C的访问级别修饰符和模块MN的声明。要使模块N中的包中的C类可访问,必须满足以下标准:

  • 模块M中的类C必须声明为public

  • 模块M必须将类C的包导出到所有其他模块,或者至少导出到模块N。通过导出包,模块声明包中的公共类(或任何类型)可以被所有或一些其他模块使用。

  • 模块N的声明必须需要模块M

模块依赖是一个很大的话题。我们在第十章中详细讨论。在这一章中,我们限制在同一个模块中讨论一个类型的可访问性,除非必须提到模块依赖。

让我们重新定义Human类,如清单 7-5 所示。这一次,您已经将它的访问级别指定为public,因此可以从任何包中访问它。

// Human.java
package com.jdojo.cls;
public class Human {
    String name;        // Instance variable
    String gender;      // Instance variable
    static long count;  // Class variable
}

Listing 7-5Redefined Human Class with the Public Access Level Modifier

重新编译Human类,然后编译ClassAccessTest类。这一次,ClassAccessTest类编译没有任何错误。

Tip

当我说一个类可以从一个包中访问时,这意味着什么?类定义了一个新的引用类型。引用类型可用于声明变量。当类在包中可访问时,类名可以用作引用类型,例如,在驻留在该包中的代码中声明变量。

进口申报

在上一节中,您学习了两条规则:

  • 您必须声明一个类public才能在声明它的包之外的包中使用它。如果另一个包在另一个模块中,那么在两个模块声明中都需要额外的工作来使public类可被访问。

  • 您需要使用类的完全限定名,以便在声明它的包之外的包中使用它。可以在声明类的包中使用它的简单名称来引用它。

第一条规则无可替代。也就是说,如果一个类需要从它的包外部访问,它必须被声明为public

还有另一种方法来处理第二条规则。通过使用导入声明,可以在包外通过简单名称引用类。导入声明用于将类从编译单元的包外部导入到编译单元中。从技术上讲,导入声明用于将任何类型导入编译单元,而不仅仅是类。导入声明出现在包声明之后,第一个类型声明之前。图 7-3 显示了进口申报出现的地方。在一个编译单元中可以有零个或多个导入声明。

img/323069_3_En_7_Fig3_HTML.png

图 7-3

Java 中编译单元的结构

本节只提到导入类。但是,同样的规则适用于导入任何其他类型,例如,接口、注释或枚举。因为到目前为止我只讨论了类类型,所以在这个讨论中我没有提到任何其他类型。

有两种类型的进口申报:

  • 单一类型进口报关单

  • 按需进口申报

单一类型进口报关单

单一类型导入声明用于从包中导入单一类型(例如,一个类)。它采取以下形式:

import <fully-qualified-name-of-a-type>;

下面的导入声明从com.jdojo.cls包中导入了Human类:

import com.jdojo.cls.Human;

单一类型导入声明仅从包中导入一种类型。如果您想要从一个包中导入多个类型(例如,三个类),您需要为每个类型使用一个单独的导入声明。以下进口报关单从pkg1包进口Class11,从pkg2包进口Class21Class22,从pkg3包进口Class33:

import pkg1.Class11;
import pkg2.Class21;
import pkg2.Class22;
import pkg3.Class33;

让我们重新看看com.jdojo.common.ClassAccessTest类,它有一个编译时错误:

// ClassAccessTest.java
package com.jdojo.common;
public class ClassAccessTest {
    public static void main(String[] args) {
        Human jack;
    }
}

当您使用简单名称的Human类时,您会收到一个编译时错误,因为编译器在com.jdojo.common包中找不到Human类。您通过使用Human类的完全限定名解决了这个错误,如下所示:

// ClassAccessTest.java
package com.jdojo.common;
public class ClassAccessTest {
    public static void main(String[] args) {
        com.jdojo.cls.Human jack; // Uses full qualified name for the Human class
    }
}

您还有另一种方法来解决这个错误,那就是使用单一类型的导入声明。您可以导入com.jdojo.cls.Human类来使用它的简单名称。修改后的ClassAccessTest类声明如下:

// ClassAccessTest.java – Modified version
package com.jdojo.common;
import com.jdojo.cls.Human; // Import the Human class
public class ClassAccessTest {
    public static void main(String[] args) {
        Human jack;         // Use simple name of the Human class
    }
}

修改后的ClassAccessTest类编译良好。当编译器在语句中遇到简单的类名Human时,比如

Human jack;

它遍历所有的导入声明,将简单名称解析为完全限定的名称。当它试图解析简单名称Human时,它会找到导入声明import com.jdojo.cls.Human,该声明导入了Human类。它假设您在前面的语句中使用简单名称Human时打算使用com.jdojo.cls.Human类。编译器用下面的语句替换前面的语句:

com.jdojo.cls.Human jack;

Tip

导入声明允许您在代码中使用简单的类型名称,从而使代码更具可读性。编译代码时,编译器用完全限定名替换类型的简单名称。它使用导入声明将类型的简单名称转换为它们的完全限定名称。需要强调的是,在 Java 程序中使用导入声明不会影响编译代码的大小或运行时性能。使用导入声明只是在源代码中使用简单类名的一种方式。

使用导入声明时,有许多微妙的地方需要记住。我们将很快讨论它们。

按需进口申报

有时您可能需要从同一个包中导入多种类型。您需要使用与需要从包中导入的类型数量一样多的单类型导入声明。按需导入声明用于使用一个import声明从包中导入多种类型。按需导入声明的语法是

import <package-name>.*;

这里,包名后面是一个点和一个星号(*)。例如,下面的按需导入声明从com.jdojo.cls包中导入所有类型:

import com.jdojo.cls.*;

有时,在按需导入声明中使用星号会导致对导入类型的错误假设。假设有两个类,C1C2。他们分别在p1p1.p2包里。也就是他们的全限定名是p1.C1p1.p2.C2。您可以将按需进口声明编写为

import p1.*;

认为它将导入两个类,p1.C1p1.p2.C2。这个假设是错误的。宣言

import p1.*;

仅从p1包中导入所有类型。它不会导入p1.p2.C2类,因为C2类不在p1包中;而是在p2包里,是p1的子包。按需导入声明末尾的星号表示仅来自指定包的所有类型。星号并不意味着子包和这些子包中的类型。有时,开发人员试图在按需导入声明中使用多个星号,认为它也会从所有的子包中导入类型:

import p1.*.*; // A compile-time error

这个按需导入声明会导致编译时错误,因为它使用了多个星号。它不遵循按需导入声明的语法。在按需进口申报单中,申报单必须以点号结尾,后跟一个且只能有一个星号。

如果您想要导入类C1C2,您需要使用两个按需导入声明:

import p1.*;
import p1.p2.*;

您可以使用按需导入声明重写ClassAccessTest类的代码:

// ClassAccessTest.java – Modified version uses import-on-demand
package com.jdojo.common;
// Import all types from the com.jdojo.cls package including the Human class
import com.jdojo.cls.*;
public class ClassAccessTest {
    public static void main(String[] args) {
        Human jack; // Use simple name of the Human class
    }
}

当编译器试图解析前面代码中的简单名称Human时,它将使用按需导入声明来查看Human类是否存在于com.jdojo.cls包中。实际上,import声明中的星号会被替换为Human,然后编译器检查com.jdojo.cls.Human类是否存在。假设在com.jdojo.cls包中有两个名为HumanTable的类。以下代码将通过一个按需导入声明进行编译:

// ClassAccessTest.java – Modified version uses import-on-demand
package com.jdojo.common;
// Import all types from com.jdojo.cls package including Human and Table classes
import com.jdojo.cls.*;
public class ClassAccessTest {
    public static void main(String[] args) {
        Human jack; // Use simple name of the Human class
        Table t1;   // Use simple name of the Table class
    }
}

前面代码中的一个按需导入声明与下面两个单一类型导入声明具有相同的效果:

import com.jdojo.cls.Human; // Import Human class
import com.jdojo.cls.Table; // Import Table class

在 Java 程序中使用哪种类型的导入声明更好:单一类型导入还是按需导入?使用按需进口申报很简单。但是,它不可读。让我们看看下面的代码,它编译得很好。假设类AB不在com.jdojo.cls包中:

// ImportOnDemandTest.java
package com.jdojo.cls;
import p1.*;
import p2.*;
public class ImportOnDemandTest {
    public static void main(String[] args) {
        A a; // Declare a variable of class A type
        B b; // Declare a variable of class B type
    }
}

通过查看这段代码,你能说出类AB的完全限定名吗?包里的Ap1还是p2?仅仅通过查看代码来判断包属于哪个类AB是不可能的,因为您已经使用了按需导入声明。让我们使用两个单一类型导入声明重写前面的代码:

// ImportOnDemandTest.java
package com.jdojo.cls;
import p1.A;
import p2.B;
public class ImportOnDemandTest {
    public static void main(String[] args) {
        A a; // Declare a variable of class A type
        B b; // Declare a variable of class B type
    }
}

通过查看导入声明,现在可以看出类A在包p1中,类B在包p2中。单一类型的导入声明使读者很容易知道哪个类是从哪个包中导入的。这也使得知道程序中其他包使用的类的数量和名称变得容易。本书在所有的例子中都使用单一类型的导入声明,除了我们讨论按需导入声明的例子。

尽管建议您在程序中使用单一类型导入声明,但您需要了解在同一程序中同时使用单一类型导入和按需导入声明的一些技巧性用法和含义。后续部分将详细讨论它们。

导入声明和类型搜索顺序

导入声明用于在编译期间将类型的简单名称解析为它们的完全限定名称。编译器使用预定义的规则来解析简单名称。假设以下语句出现在使用简单名称A的 Java 程序中:

A var;

在编译过程中,Java 编译器必须将简单名称A解析为完全限定名称。它按以下顺序搜索程序中引用的类型:

  • 当前编译单元

  • 单一类型进口申报

  • 在同一包中声明的类型

  • 按需进口申报

此类型搜索列表不完整。如果某个类型有嵌套类型,则在查找当前编译单元之前会先搜索嵌套类型。我们将推迟对嵌套类型的讨论,直到在本系列的第二本书中讨论内部类。

让我们用几个例子来讨论类型搜索的规则。假设您有一个名为B.java的 Java 源文件(编译单元),其内容如下。注意文件B.java包含两个类AB的声明:

// B.java
package p1;
class B {
    A var;
}
class A {
    // Code goes here
}

当类B声明类型A的实例变量var时,它使用简单名称引用类A。编译B.java文件时,编译器会在当前编译单元(B.java文件)中寻找简单名称为A的类型。它会在当前编译单元中找到一个简单名称为A的类声明。简单名称A将被替换为完全限定名称p1.A。注意,两个类AB是在同一个编译单元中声明的,因此它们在同一个包p1中。编译器将对类B的定义进行如下更改:

package p1;
class B {
    p1.A var; // A has been replaced by p1.A by the compiler
}

假设您想要使用前面示例中包p2中的类A。也就是有一个类p2.A,你想在类B中声明p2.A类型的实例变量var,而不是p1.A。让我们通过使用单一类型导入声明导入类p2.A来解决这个问题,如下所示:

// B.java – Includes a new import declaration
package p1;
import p2.A;
class B {
    A var; // You want to use p2.A when you use A here
}
class A {
    // Code goes here
}

当你编译修改后的B.java文件时,你会得到如下编译错误:

"B.java": p1.A is already defined in this compilation unit at line 2, column 1

修改后的源代码有什么问题?当您从其中移除单一类型导入声明时,它可以正常编译。这意味着是单一类型导入声明导致了错误。在解决这个错误之前,您需要了解一个关于单一类型导入声明的新规则。规则是

使用多个单一类型导入声明导入多个具有相同简单名称的类型是一个编译时错误。

假设你有两个类,p1.Ap2.A。请注意,这两个类有相同的简单名称A,放在两个不同的包中。根据这个规则,如果您想在同一个编译单元中使用两个类p1.Ap2.A,您不能使用两个单一类型的导入声明:

// Test.java
package pkg;
import p1.A;
import p2.A; // A compile-time error
class Test {
    A var1; // Which A to use p1.A or p2.A?
    A var2; // Which A to use p1.A or p2.A?
}

这条规则背后的原因是,当你在代码中使用简单的名字A时,编译器无法知道使用哪个类(p1.Ap2.A)。Java 可能已经通过使用第一个导入的类或最后一个导入的类解决了这个问题,这很容易出错。Java 决定将问题扼杀在萌芽状态,当您导入两个具有相同简单名称的类时,它会给出一个编译时错误,这样您就不会犯这样愚蠢的错误,并最终花费数小时来解决它们。

让我们回到在一个编译单元中导入p2.A类的问题,这个编译单元已经声明了一个类A。以下代码会产生编译时错误:

// B.java – Includes a new import declaration
package p1;
import p2.A;
class B {
    A var1; // You want to use p2.A when you use A
}
class A {
    // Code goes here
}

这一次,您只使用了一个单一类型的导入声明,而不是两个。为什么会出现错误?当您在同一个编译单元中声明多个类时,它们很可能是紧密相关的,并且会相互引用。您需要认为 Java 使用单一类型的导入声明导入了在同一个编译单元中声明的每个类。您可以将前面的代码看作是由 Java 转换的,如下所示:

// B.java – Includes a new import declaration
package p1;
import p1.A; // Think of it being added by Java
import p1.B; // Think of it being added by Java
import p2.A;
class B {
    A var; // We want to use p2.A when you use A
}
class A {
           // Code goes here
}

你现在能看出问题了吗?类A被导入了两次,一次由 Java 导入,一次由您导入,这就是错误的原因。你如何在你的代码中引用p2.A?很简单。每当你想在你的编译单元中使用p2.A时,使用完全限定名p2.A;

// B.java – Uses fully qualified name p2.A in class B
package p1;
class B {
    p2.A var; // Use fully qualified name of A
}
class A {
              // Code goes here
}

Tip

如果在同一个编译单元中存在具有相同简单名称的类型,则使用单类型导入声明将类型导入到编译单元是一个编译时错误。

让我们用代码解决编译时错误,该代码需要使用来自不同包的具有相同简单名称的类。代码如下:

// Test.java
package pkg;
import p1.A;
import p2.A; // A compile-time error
class Test {
    A var1;  // Which A to use p1.A or p2.A?
    A var2;  // Which A to use p1.A or p2.A?
}

您可以使用以下两种方法之一来解决该错误。第一种方法是删除两个导入声明,并使用类A的完全限定名,如下所示:

// Test.java
package pkg;
class Test {
    p1.A var1; // Use p1.A
    p2.A var2; // Use p2.A
}

第二种方法是只使用一个导入声明从一个包中导入类A,比如说p1,并从p2包中使用类A的完全限定名,如下所示:

// Test.java
package pkg;
import p1.A;
class Test {
    A var1;    // Refers to p1.A
    p2.A var2; // Uses the fully qualified name p2.A
}

Tip

如果您想要在一个编译单元中使用多个具有相同简单名称的类,但是来自不同的包,那么您最多可以导入一个类。对于其余的类,您必须使用完全限定名。您可以选择对所有类使用完全限定名。

让我们讨论一些关于使用按需导入声明的规则。在使用所有其他方法解析简单名称之后,编译器使用按需导入声明来解析类型的简单名称。使用单一类型导入声明和按需导入声明导入具有相同简单名称的类是有效的。在这种情况下,使用单一类型导入声明。假设您有三个类:p1.Ap2.Ap2.B。假设您有一个编译单元,如下所示:

// C.java
package p3;
import p1.A;
import p2.*;
class C {
    A var; // Will always use p1.A (not p2.A)
}

在这个例子中,类A被导入了两次:一次使用包p1中的简单类型导入声明,另一次使用包p2中的按需导入声明。简单名称A被解析为p1.A,因为单一类型的导入声明总是优先于按需导入声明。一旦编译器找到使用单一类型导入声明的类,它就停止搜索,而不使用任何按需导入声明来查找该类。

让我们将前面示例中的导入声明更改为使用按需导入声明,如下所示:

// C.java
package p3;
import p1.*;
import p2.*;
class  C {
    A var; // A compile-time error. Which A to use p1.A or p2.A?
}

编译类C产生以下错误:

"C.java": reference to A is ambiguous, both class p2.A in p2 and class p1.A in p1 match at line 8, column 5

错误信息清晰明了。当编译器使用按需导入声明找到一个类时,它会继续在所有按需导入声明中搜索该类。如果它使用多个按需导入声明找到具有相同简单名称的类,它将生成一个错误。您可以用几种方法解决此错误:

  • 使用两个单一类型的导入声明。

  • 使用一个单一类型导入和一个按需导入声明。

  • 对两个类都使用完全限定名。

下面的列表包含了更多关于导入声明的规则:

  • 忽略重复的单一类型导入和按需导入声明。以下代码是有效的:

  • 使用单一类型导入声明或按需导入声明从同一个包中导入类是合法的,尽管不是必需的。下面的代码从同一个包p5中导入类F。注意,在同一个包中声明的所有类都是自动导入的。在这种情况下,将忽略导入声明:

// D.java
package p4;
import p1.A;
import p1.A; // Ignored. A duplicate import declaration.
import p2.*;
import p2.*; // Ignored. A duplicate import declaration.
class D {
             // Code goes here
}

// E.java
package p5;
import p5.F; // Will be ignored
class E {
             // Code goes here
}
// F.java
package p5;
import p5.*; // Will be ignored
class F {
             // Code goes here
}

自动进口申报

您一直用简单的名字使用String类和System类,并且您从来没有想过在您的任何程序中导入它们。这些类的全限定名是java.lang.Stringjava.lang.System。Java 总是自动导入在java.lang包中声明的所有类型。想象以下按需导入声明在编译前被添加到源代码中:

import java.lang.*;

这就是为什么您能够在代码中使用简单的名称StringSystem而不用导入它们。你可以在你的程序中使用java.lang包中的任何类型,只要有简单的名字。

使用导入声明从java.lang包中导入类型不是错误。编译器会简单地忽略它们。以下代码将编译无误:

package p1;
import java.lang.*;      // Will be ignored because it is automatically done for you
public class G {
    String anythingGoes; // Refers to java.lang.String
}

使用类型的简单名称时需要小心,它与在java.lang包中定义的类型相同。假设您声明了一个p1.String类:

// String.java
package p1;
public class String {
    // Code goes here
}

假设在同一个包中有一个Test类,p1:

// Test.java
package p1;
public class Test {
    // Which String class will be used: p1.String or java.lang.String
    String myStr;
}

Test级中所指的String级是:p1.String还是java.lang.String?它将引用p1.String,而不是java.lang.String,因为编译单元的包(在本例中是p1)在任何导入声明之前被搜索,以解析类型的简单名称。编译器在包p1中找到String类。它不会在java.lang包中搜索String类。如果您想在这个例子中使用java.lang.String类,您必须使用它的完全限定名,如下所示:

// Test.java
package p1;
public class Test {
    java.lang.String s1; // Use java.lang.String
    p1.String s2;        // Use p1.String
    String s3;           // Will use p1.String
}

静态进口申报

静态导入声明顾名思义。它将某个类型的静态成员(静态变量/方法)导入到编译单元中。您在前面的章节中学习了静态变量(或类变量)。我们将在下一节讨论静态方法。静态导入声明有两种形式:

  • 单一静态导入

  • 静态按需导入

单静态导入声明导入一个类型的静态成员。静态按需导入声明导入一个类型的所有静态成员。静态导入声明的一般语法如下:

// Single-static-import declaration:
import static <package-name>.<type-name>.<static-member-name>;
//Static-import-on-demand declaration:
import static <package-name>.<type-name>.*;

您已经使用System.out.println()方法在标准输出上打印了消息。Systemjava.lang包中的一个类,它有一个名为out的静态变量。当你使用System.out时,你指的是System类中名为out的静态变量。您可以使用静态导入声明从System类导入out static变量,如下所示:

import static java.lang.System.out;

你的程序现在不需要用类名System作为System.out来限定out变量。相反,它可以在你的程序中用名字out来表示System.out。编译器将使用静态导入声明将名称out解析为System.out

清单 7-6 展示了如何使用静态导入声明。它导入了System类的out静态变量。注意,main()方法使用的是out.println()方法,而不是System.out.println()。编译器会用System.out.println()调用替换out.println()调用。

// StaticImportTest.java
package com.jdojo.cls;
import static java.lang.System.out;
public class StaticImportTest {
    public static void main(String[] args) {
        out.println("Hello static import!");
    }
}
Hello static import!

Listing 7-6Using Static Import Declarations

Tip

导入声明导入类型名,并允许您在程序中使用该类型的简单名称。导入声明对类型做什么,静态导入声明对类型的静态成员做什么。静态导入声明允许您使用某个类型的静态成员(静态变量/方法)的名称,而不用类型名称来限定它。

让我们看另一个使用静态导入声明的例子。java.lang包中的Math类包含许多实用常量和静态方法。例如,它包含一个名为PI的类变量,其值等于22/7(数学中的圆周率)。如果您想使用Math类的任何静态变量或方法,您需要用类名Math来限定它们。例如,您可以将PI静态变量称为Math.PI,将sqrt()方法称为Math.sqrt()。您可以使用下面的 static-import-on-demand 声明来导入Math类的所有静态成员:

import static java.lang.Math.*;

现在,您可以使用静态成员的名称,而不用类名Math来限定它。清单 7-7 演示了通过导入Math类的static成员来使用它。

// StaticImportTest2.java
package com.jdojo.cls;
import static java.lang.System.out;
import static java.lang.Math.*;
public class StaticImportTest2 {
    public static void main(String[] args) {
        double radius = 2.9;
        double area = PI * radius * radius;
        out.println("Value of PI is: " + PI);
        out.println("Radius of circle: " + radius);
        out.println("Area of circle: " + area);
        out.println("Square root of 2.0: " + sqrt(2.0));
    }
}
Value of PI is: 3.141592653589793
Radius of circle: 2.9
Area of circle: 26.420794216690158
Square root of 2.0: 1.4142135623730951

Listing 7-7Using Static Imports to Import Multiple Static Members of a Type

以下是一些关于静态导入声明的重要规则。

静态导入规则#1

如果导入两个具有相同简单名称的静态成员,一个使用单静态导入声明,另一个使用静态按需导入声明,则使用单静态导入声明导入的成员优先。假设有两个类,p1.C1p2.C2。这两个类都有一个名为m1的静态方法。下面的代码将使用p1.C1.m1()方法,因为它是使用单静态导入声明导入的:

// Test.java
package com.jdojo.cls;
import static p1.C1.m1; // Imports C1.m1() method
import static p2.C2.*;  // Imports C2.m1() method too
public class Test {
    public static void main(String[] args) {
        m1();           // C1.m1() will be called
    }
}

静态导入规则#2

不允许使用单一静态导入声明来导入具有相同简单名称的两个静态成员。以下静态导入声明生成了一个编译时错误,因为它们都导入了一个具有相同简单名称m1的静态成员:

import static p1.C1.m1;
import static p1.C2.m1; // A compile-time error

静态导入规则#3

如果静态成员是使用单静态导入声明导入的,并且在同一个类中存在同名的静态成员,则使用该类中的静态成员。下面是两个类的代码,p1.Ap2.Test:

// A.java package p1;
public class A {
    public static void test() {
        System.out.println("p1.A.test()");
    }
}
// Test.java
package p2;
import static p1.A.test;
public class Test {
    public static void main(String[] args) {
        test(); // Will use p2.Test.test() method, not p1.A.test() method
    }
    public static void test() {
        System.out.println("p2.Test.test()");
    }
}
p2.Test.test()

Test类使用单一静态导入声明从p1.A类导入静态方法test()Test类还定义了一个静态方法test()。当你用简单名test调用main()方法中的test()方法时,指的是p2.Test.test()方法,而不是静态导入导入的方法。

在这种情况下使用静态导入声明有一个隐藏的危险。假设在p2.Test类中没有test()静态方法。一开始,test()方法调用将调用p1.A.test()方法。稍后,您将在Test类中添加一个test()方法。现在test()方法调用将开始调用p2.Test.test(),这将在您的程序中引入一个难以发现的 bug。

Tip

似乎静态导入帮助您使用静态成员的简单名称来简化程序的编写和阅读。有时,静态导入可能会在您的程序中引入微妙的错误,这可能很难调试。建议您仅在极少数情况下使用静态导入。

申报记录

Java 中的记录是一种特殊类型的类,它具有不可变的字段(意味着它们不能被更改),具有由编译器自动为其生成的多个方法,并扩展了 java.lang.Record。它在 Java 14 中作为预览功能引入,在 Java 16 中最终确定。

记录类型允许 Java 编译器和运行时进行大量的性能改进,这是其他方法无法做到的。Java 记录的主要特征是它们是不可变的——一旦实例被创建,它的字段值就不能被更改——并且它们具有与记录定义中定义的字段名称相匹配的访问器方法。

用 Java 声明记录的一般语法如下:

[modifiers] record <record-name>( <field-definitions> ) {
    // Body of the record class goes here
}

这里

  • modifiers是可选的;它们是将特殊含义与记录声明相关联的关键字。一个记录声明可能有零个或多个修饰符,就像类声明一样。

  • 单词 record 用于声明一个记录。它不是关键字,仍然可以用作变量名。

  • record-name是用户定义的记录名,它应该是一个有效的 Java 标识符。

  • 每个记录都有一个主体,在一对大括号({})中指定。记录的主体可以包含不同的组件,例如,字段、方法等。,也可以是空的。

  • 字段定义是一个逗号分隔的<data-type> <field-name>声明列表,它定义了记录的immutable fields

例如,让我们重新创建人类类作为记录(清单 7-8 )。

// Human.java
package com.jdojo.cls;
public record Human (String name, String gender) {
    static long count;  // Class variable
}

Listing 7-8Redefined Human Class as a Record

现在,当创建一个人的实例时,必须提供“姓名”和“性别”字段,并且不能对该实例进行更改。与类不同,没有默认的字段初始化。如示例所示,记录中仍然可以有静态变量,它们不是不可变的。类变量(也称为静态变量)与类(在本例中是人类)相关联,而不是该类的特定实例。

要创建记录,可以使用如下所示的构造器,例如:

Human bob = new Human("Bob", "male")

您可以像这样使用方法调用来访问这些值(下一章将解释所有关于方法的内容):

String name = bob.name() //Bob

记录还有几个自动生成的方法(equals、hashCode 和 toString),我们将在后续章节中了解更多。

摘要

类是面向对象编程的基本构件。在 Java 中,类代表一个引用类型。类充当创建对象的模板。一个类由四部分组成:字段、初始化器、构造器和方法。字段表示该类对象的状态。初始值设定项和构造器用于初始化类的字段。new操作符用于创建一个类的对象。方法表示该类的对象的行为。

字段和方法被称为类的成员。构造器不是类的成员。顶级类有一个访问级别,它决定了从程序的哪个部分可以访问它。顶级类可以具有公共级或包级访问权限。可以从同一个模块中的任何地方访问公共类。如果该类的模块导出该类的包,如果其他模块声明依赖于该类的模块,则也可以从这些模块内部访问该公共类。顶级类上缺少访问级别修饰符,这使得该类具有包级别的访问权限,这使得该类可以在其包内进行访问。

Java 中的每个类都定义了一个新的引用类型。Java 有一种特殊的引用类型,称为空类型。它没有名字。因此,不能定义空引用类型的变量。空引用类型只有一个由 Java 定义的值,即null文字。简直就是null。空引用类型与所有其他引用类型都是赋值兼容的。

一个类可以有两种类型的字段。它们被称为实例变量和类变量,也分别称为非静态变量和静态变量。实例变量代表类的对象的状态。该类的每个对象都有一个所有实例变量的副本。类变量代表类本身的状态。一个类只存在一个类变量副本。可以使用点符号访问类的字段,其形式如下:

<qualifier>.<field-name>

对于实例变量,限定符是对该类实例的引用。对于类变量,限定符可以是类实例的引用或类名。

一个类的所有字段,静态的和非静态的,都被初始化为默认值。字段的默认值取决于其数据类型。一个数值字段(byteshortcharintlongfloatdouble)被初始化为零。一个boolean字段被初始化为false。引用类型字段被初始化为null

编译单元中的 Import 语句用于从其他包中导入类型。它们允许使用其他包中的简单类型名。编译器使用导入语句将简单名称解析为完全限定的名称。静态导入语句用于从其他包中导入类型的静态成员。

Java 16 引入了记录,记录是具有自动生成方法的不可变类,使用单词“record”和括号内的字段定义列表来定义。这些字段没有默认值,必须在创建记录实例时提供。

EXERCISES

  1. 什么是类的实例变量?实例变量的另一个名字是什么?

  2. 什么是类的类变量?类变量的另一个名字是什么?

  3. 一个类的不同类型字段的默认值是什么?

  4. 用两个名为xyint实例变量创建一个名为Point的类。两个实例变量都应该声明为公共的。不要初始化这两个实例变量。

  5. 将一个main()方法添加到您在前一个练习中创建的Point类中。创建一个Point类的对象,并打印xy实例变量的默认值。将xy的值分别设置为 5 和 10,并通过在程序中读回它们来打印它们的值。

  6. 假设Point是您在前面的练习中创建的类名,那么当下面的代码片段运行时会发生什么呢?

    Point p = null;
    int x = p.x;
    
    
  7. 以下代码的输出是什么?

    public class Employee {
        String name;
        boolean retired;
        double salary;
        public static void main(String[] args) {
            Employee emp = new Employee();
            System.out.println(emp.name);
            System.out.println(emp.retired);
            System.out.println(emp.salary);
        }
    }
    
    
  8. java.time包包含一个LocalDate类。该类包含一个返回当前本地日期的now()方法。CurrentDate类在它的 main 方法中使用了类的简单名称LocalDate。当前形式的代码无法编译。通过添加导入语句(首先是单一类型导入语句,然后是按需导入语句)来导入LocalDate类,完成并运行下面的代码。运行CurrentDate类时,它会以 ISO 格式打印当前的本地日期,比如 2017-08-27:

    // CurrentDate.java
    package com.jdojo.cls.excercise;
    /* Add an import statement here. */
    public class CurrentDate {
        public static void main(String[] args) {
            LocalDate today = LocalDate.now();
            System.out.println(today);
        }
    }
    
    
  9. 考虑下面这个名为StaticImport的类的代码。代码不能编译,因为它在它的main()方法中使用了out.println()而不是System.out.println()方法。通过添加静态导入语句来完成代码。System类在java.lang包中,outSystem类中的静态变量:

    // StaticImport.java
    package com.jdojo.cls.excercise;
    /* Add a static import statement here. */
    public class StaticImport {
        public static void main(String[] args) {
            out.println("Hello static import");
        }
    }
    
    
  10. 以下名为MathStaticImport的类的代码无法编译。添加一个 static-import-on-demand 语句来完成代码,这样它就可以编译了。java.lang.Math类包含名为PI的静态变量和名为sqrt() :

```java
// MathStaticImport.java
package com.jdojo.cls.excercise;
/* Add a static-import-on-demand statement here. */
public class MathStaticImport {
    public static void main(String[] args) {
        double radius = 2.0;
        double perimeter = 2 * PI * radius;
        System.out.println("Value of PI is " + PI);
        System.out.println("Square Root of 2 is " + sqrt(2));
        System.out.println("Perimeter of a circle of radius 2.0 is "
                           + perimeter);
    }

```

的静态方法

11. 定义一个名为 Computer 的公共记录类,具有以下字段和类型:String name、int numberOfProcessors、int memory、int diskSpace 和 String brand。