一、你好世界
安装
在使用 Java 编程之前,您需要下载并安装一个 Java 开发工具包(JDK),例如 Oracle 网站上的标准版(JDK SE)。1JDK 包括 Java 编译器、类库以及运行 Java 应用所需的虚拟机等。您还应该下载一个集成开发环境(IDE ),因为它会使 Java 开发变得更加容易。一个这样的 Java IDE 是 Apache NetBeans,2,它可以在 Windows、macOS 和 Linux 上免费获得。如果您根本不想使用任何 IDE,使用常规文本编辑器也是一种选择。若要在没有 IDE 的情况下工作,可以使用。java 扩展—例如 myapp . Java—并在您选择的文本编辑器中打开它。
创建项目
如果您决定使用 ide(推荐),您需要创建一个项目,它将管理 Java 源文件和其他资源。要在 NetBeans 中创建项目,请单击“文件”“➤新建项目”。在对话框中,选择 Java with Ant 类别下的 Java 应用程序项目,然后单击 Next。在此对话框中,将项目名称设置为“MyProject ”,将主类的名称设置为“myproject”。MyApp”。如果需要,更改项目的位置,然后单击 Finish 生成项目。该项目的唯一文件 MyApp.java 将会打开,其中包含一些默认代码。您可以删除所有代码,以便从一个空的源文件开始。
你好世界
当您设置好项目和编程环境后,您将创建的第一个应用程序是 Hello World 程序。这个程序将教你如何编译和运行 Java 应用程序,以及如何输出一个字符串到一个命令窗口。
创建这个程序的第一步是向 MyApp.java 源文件添加一个公共类。该类的名称必须与物理源文件的名称相同,没有文件扩展名,在本例中为“MyApp”在 Java 中,每个文件有多个类是合法的,但是只允许有一个公共类,并且它的名字必须与文件名匹配。记住,Java 是区分大小写的。类名后面的花括号界定了属于该类并且必须包含的内容。括号及其内容被称为代码块,或简称为块。
public class MyApp {}
Java 类被组织成包,类似于其他语言中的名称空间。package 语句需要出现在文件的顶部,以指定文件属于哪个包。这个名称必须与文件所在的目录(相对于项目的源目录)相匹配,所以在这种情况下,包名是myproject
。
package myproject;
public class MyApp {}
接下来,在类中添加 main 方法。这是应用程序的起点,必须始终包含在如下代码所示的相同表单中。关键字本身将在后面的章节中讨论。
package myproject;
public class MyApp {
public static void main(String[] args) {}
}
完成 Hello World 程序的最后一步是通过调用print
方法输出文本。这个方法位于System
类中,然后在out
类中再往下一层。该方法采用单个参数——要打印的字符串——并以分号结束,Java 中的所有语句都是如此。
package myproject;
public class MyApp {
public static void main(String[] args) {
System.out.print("Hello World");
}
}
注意,点运算符(.
)用于访问类的成员。与print
类似,还有println
方法,它自动在打印字符串的末尾添加一个换行符。System
类属于 java.lang 包,它总是包含在 java 项目中。
代码提示
如果您不确定一个特定的类包含什么,或者一个方法接受什么参数,您可以利用一些 ide 中的代码提示,比如 NetBeans。代码提示窗口会在您键入代码的任何时候出现,并且有多个预先确定的选项。您也可以通过按 Ctrl+空格键手动调出它。这是一个强大的特性,它让您可以快速访问类库及其成员,以及描述。
www。甲骨文。com/Java/technologies/Java se-下载。html
2
二、编译并运行
从 IDE 运行
完成 Hello World 程序后,您可以用两种方式之一编译和运行它。第一种方法是从您正在使用的 IDE 的菜单栏中选择 Run。在 NetBeans 中,菜单命令是运行➤运行项目。然后,IDE 将编译并运行该应用程序,该应用程序将在 IDE 的输出窗口中显示文本“Hello World”。
从控制台窗口运行
另一种方式是使用控制台窗口手动编译程序,比如 Windows 下的 C:\Windows\System32\cmd.exe。最方便的方法是首先将 JDK bin 目录添加到PATH
环境变量中。在 Windows 中,您可以使用SET PATH
命令,然后将路径附加到您的 JDK 安装的 bin 文件夹,用分号隔开。请注意,确切的路径取决于您安装的 JDK 版本。
SET PATH=%PATH%;"C:\Program Files\Java\jdk-17.0.2\bin"
通过这样做,控制台将能够在这个控制台会话期间从任何文件夹中找到 Java 编译器。PATH
变量也可以永久改变。 1 接下来,导航到 Java 源文件所在的文件夹,通过键入 javac 后跟完整的文件名来运行编译器。
C:\MyProject\src\myproject> javac MyApp.java
程序将被编译成一个名为 MyApp.class 的类文件,这个类文件包含的是字节码而不是机器码,所以要执行它,你需要通过键入 java 后跟全限定类名来调用 Java 虚拟机,全限定类名包括包名。该命令需要从父文件夹(项目的源文件夹)中执行。请注意。编译文件时使用 java 扩展名,但。运行时不使用类扩展。
C:\MyProject\src> java myproject.MyApp
或者,从 Java 11 开始,您可以通过给 Java 命令提供完整的文件名来编译和运行源文件:
java MyApp.java
评论
注释用于在源代码中插入注释,对结束程序没有影响。Java 有标准的 C++注释符号,有单行注释和多行注释。
// single-line comment
/* multi-line
comment */
除了这些,还有 Javadoc 注释。该注释用于通过使用 JDK bin 文件夹中包含的实用程序生成文档,该实用程序也称为 Javadoc。
/** javadoc
comment */
预览功能
预览功能是一项新功能,在未来的 JDK 版本中可能会有所变化。若要编译包含预览功能的代码,必须为项目指定一个附加的命令行选项。在 NetBeans 中,这是通过首先打开文件➤项目属性窗口来完成的。在那里,从构建类别中选择编译选项卡,并在该窗口的底部,在标有“附加编译器选项”的输入框中添加“- enable-preview”。单击“确定”,将为此项目启用预览功能。
www . Java . com/en/download/help/path . XML
三、变量
变量用于在程序执行期间将数据存储在内存中。
数据类型
根据您需要存储的数据,有几种数据类型。Java 语言内置了八种类型,称为原语。整数类型有byte
、short
、int
和long
。float
和double
类型代表浮点数(实数)。char
类型保存 Unicode 字符,而boolean
类型包含 true 或 false 值。除了这些基本类型,Java 中的其他所有类型都由一个类、一个接口或一个数组来表示。
数据类型
|
大小(位)
|
描述
|
| — | — | — |
| byte``short``int``long
| eightSixteenThirty-twoSixty-four | 带符号整数 |
| float``double
| Thirty-twoSixty-four | 浮点数 |
| char
| Sixteen | Unicode 字符 |
| boolean
| one | 布尔值 |
声明变量
要声明(创建)一个变量,从你希望它保存的数据类型开始,后面跟着一个变量名。名称可以是您想要的任何名称,但是最好给变量起一个与它们所包含的值密切相关的名称。变量的标准命名约定是第一个单词应该小写,随后的所有单词都应该大写。
int myInt;
分配变量
要给变量赋值,可以使用赋值运算符(=
)后跟值。当一个变量被初始化(赋值)时,它就被定义(声明和赋值)。
myInt = 10;
声明和赋值可以合并成一条语句:
int myInt = 10;
如果您需要多个相同类型的变量,有一种使用逗号运算符(,)来声明或定义它们的简便方法:
int myInt = 10, myInt2 = 20, myInt3;
使用变量
一旦定义了变量,您就可以通过引用变量的名称来使用它,例如,打印它:
System.out.print(myInt);
整数类型
如前所述,有四种有符号整数类型可供使用,这取决于您需要变量保存多大的数字:
byte myInt8 = 2; // -128 to +127
short myInt16 = 1; // -32768 to +32767
int myInt32 = 0; // -2³¹ to +2³¹-1
long myInt64 = -1; // -2⁶³ to +2⁶³-1
除了标准的十进制记数法,整数也可以用八进制或十六进制记数法来赋值。从 Java 7 开始,也可以使用二进制表示法。
int myHex = 0xF; // hexadecimal (base 16)
int myOct = 07; // octal (base 8)
int myBin = 0b10; // binary (base 2)
数字中的数字可以用下划线(_)分隔。这个特性是在 Java 7 中引入的,提供它只是为了提高可读性。
int bigNumber = 10_000_000;
浮点类型
浮点类型可以存储整数,也可以存储浮点数。它们可以用十进制或指数记数法来赋值。
double myDouble = 3.14;
double myDouble2 = 3e2; // 3*10² = 300
注意,Java 中的常量浮点数在内部总是以双精度形式保存。因此,如果您试图将一个 double 赋值给一个 float,您将会得到一个错误,因为 double 比 float 具有更高的精度。为了正确赋值,您可以在常量后面添加一个字符 F ,这表示该数字实际上是一个浮点数。
float myFloat = 3.14; // error
float myFloat = 3.14F; // ok
更常见和有用的方法是使用显式强制转换。通过将所需的数据类型放在要转换的变量或常量之前的括号中,执行显式强制转换。这将在赋值发生之前将值转换为指定的类型—在本例中为float
。
float myFloat = (float)3.14;
字符类型
char
数据类型可以包含一个 Unicode 字符,用单引号分隔:
char myChar = 'A';
也可以使用特殊的十六进制表示法来分配字符,该表示法允许访问所有 Unicode 字符:
char myChar = '\u0000'; // \u0000 to \uFFFF
布尔型
boolean
类型可以存储一个布尔值,这个值只能是真或假。这些值由关键字true
和false
指定。
boolean myBool = false;
变量作用域
变量的范围指的是代码块,在这个代码块中可以无限制地使用该变量。例如,局部变量是在方法中声明的变量。这样的变量只有在声明后,才能在方法的代码块中使用。一旦方法的作用域(代码块)结束,局部变量将被销毁。
public static void main(String[] args)
{
int localVar; // local variable
}
除了局部变量,Java 还有字段和参数类型的变量,这将在后面的章节中介绍。但是 Java 不像 C++那样有全局变量。
匿名块
您可以使用一个匿名(未命名)代码块来限制局部变量的范围。这种结构很少使用,因为如果一个方法足够大,可以保证使用匿名块,那么更好的选择通常是将代码分成单独的方法。
public static void main(String[] args)
{
// Anonymous code block
{
int localVar = 10;
}
// localVar is unavailable from here
}
类型推理
从 Java 10 开始,可以用var
声明局部变量,让编译器根据变量的赋值自动确定变量的类型。因此,以下两个声明是等效的:
var i = 5; // Implicit type
int i = 5; // Explicit type
何时使用var
取决于个人喜好。如果变量的类型从赋值中显而易见,使用var
可能会更好地缩短声明并提高可读性。如本例所示,使用非基元类型的好处变得更加明显。
// No type inference
java.util.ArrayList a = new java.util.ArrayList();
// With type inference
var a = new java.util.ArrayList();
请记住,var
只能在局部变量同时被声明和初始化时使用。
四、运算符
运算符是用来对值进行运算的特殊符号。专门处理数字的运算符可以分为五种类型:算术、赋值、比较、逻辑和按位运算符。
算术运算符
算术运算符包括四种基本算术运算,以及用于获得除法余数的模数运算符(%
):
float x = 3+2; // addition (5)
x = 3-2; // subtraction (1)
x = 3*2; // multiplication (6)
x = 3/2; // division (1)
x = 3%2; // modulus (1)
请注意,除法符号给出了不正确的结果。这是因为它对两个整数值进行运算,因此会对结果进行舍入并返回一个整数。要获得正确的值,必须将其中一个数字显式转换为浮点类型。
float x = (float)3/2; // 1.5
赋值运算符
第二组是赋值操作符——最重要的是赋值操作符本身(=
),它给变量赋值:
int i = 0; // assignment
赋值运算符和算术运算符的一个常见用途是对变量进行运算,然后将结果保存回同一个变量中。使用组合赋值操作符可以缩短这些操作。
i += 5; // i = i+5;
i -= 5; // i = i-5;
i *= 5; // i = i*5;
i /= 5; // i = i/5;
i %= 5; // i = i%5;
递增和递减运算符
另一种常见的操作是将变量加 1 或减 1。这可以通过递增(++
)和递减()运算符来简化。
++i; // i = i+1
−−i; // i = i-1
这两者都可以用在变量之前或之后:
++i; // pre-increment
−−i; // pre-decrement
i++; // post-increment
i−−; // post-decrement
无论使用哪个变量,变量的结果都是相同的。不同的是,后运算符在改变变量之前返回原始值,而前运算符先改变变量,然后返回值。
int x, y;
x = 5; y = x++; // y=5, x=6
x = 5; y = ++x; // y=6, x=6
比较运算符
比较运算符比较两个值,并返回 true 或 false。它们主要用于指定条件,即评估为真或假的表达式。
boolean b = (2==3); // equal to (false)
b = (2!=3); // not equal to (true)
b = (2>3); // greater than (false)
b = (2<3); // less than (true)
b = (2>=3); // greater than or equal to (false)
b = (2<=3); // less than or equal to (true)
逻辑运算符
逻辑运算符通常与比较运算符一起使用。如果左侧和右侧都为真,则逻辑和 ( &&
)计算为真,如果左侧或右侧为真,则逻辑或 ( ||
)为真。为了反转布尔结果,有一个逻辑非 ( !
)运算符。请注意,对于逻辑和以及逻辑或,如果结果已经由左侧确定,则不会对右侧进行评估。
boolean b = (true && false); // logical and (false)
b = (true || false); // logical or (true)
b = !(true); // logical not (false)
按位运算符
按位运算符可以操作整数类型中的单个位。例如,右移位运算符(>>
)将除符号位之外的所有位向右移动,而零填充右移位(>>>
)将包括符号位在内的所有位向右移动。
byte b = 5 & 4; // 101 & 100 = 100 (4) // and
b = 5 | 4; // 101 | 100 = 101 (5) // or
b = 5 ^ 4; // 101 ^ 100 = 001 (1) // xor
b = 4 << 1; // 100 << 1 = 1000 (8) // left shift
b = 4 >> 1; // 100 >> 1 = 10 (2) // right shift
b = 4 >>>1; // 100 >>>1 = 10 (2) // zero-fill right shift
b = ~4; // ~00000100 = 11111011 (-5) // invert
这些位运算符有速记赋值运算符,就像算术运算符一样:
int i = 5;
i &= 4; // "and" and assign
i |= 4; // or and assign
i ^= 4; // xor and assign
i <<= 1; // left shift and assign
i >>= 1; // right shift and assign
i >>>= 1; // right shift and assign (move sign bit)
运算符优先级
在 Java 中,表达式通常从左到右计算。但是,当表达式包含多个运算符时,这些运算符的优先级决定了它们的求值顺序。下表显示了优先顺序。同样的顺序也适用于许多其他语言,如 C++和 C#。
|优先
|
操作员
|
优先
|
操作员
|
| — | — | — | — |
| one | ++ −− !~ | seven | & |
| Two | * / % | eight | ^ |
| three | + − | nine | | |
| four | << >> > > > | Ten | && |
| five | < <= > >= | Eleven | || |
| six | == != | Twelve | = 操作员 = |
例如,逻辑和 ( &&
)的绑定弱于关系运算符,而关系运算符又弱于算术运算符:
boolean b = 2+3 > 1*4 && 5/5 == 1; // true
为了避免学习所有运算符的先例,并阐明意图,可以使用括号来指定首先计算表达式的哪一部分:
boolean b = ( (2+3) > (1*4) ) && ( (5/5) == 1 ); // true
五、字符串
Java 中的String
类是一种可以保存字符串文字的数据类型。 String 是一种引用数据类型,所有非原始数据类型也是如此。这意味着变量包含内存中对象的地址,而不是对象本身。在内存中创建一个String
对象,并将该对象的地址返回给变量。如以下代码所示,字符串由双引号分隔:
String a = "Hello";
字符串存储在所谓的字符串池中,由 String 类维护。出于性能原因,任何等于先前创建的字符串的字符串文字都将引用池中的同一字符串对象。这是可行的,因为 Java 中的字符串是不可变的,因此如果不创建一个新的 String 对象就不能更改。
String a1 = "Hello";
String a2 = "Hello"; // refers to same object as a1
组合字符串
加号用于组合两个字符串。在这个上下文中称为串联操作符(+
),它有一个伴随的赋值操作符(+=
),将一个字符串追加到另一个字符串并创建一个新字符串。
String a = "Hello";
String b = " World";
String c = a+b; // "Hello World"
a += b; // "Hello World"
请注意,尽管一条语句可以分成多行,但一个字符串必须在一行中,除非使用串联运算符将其拆分:
String x
= "Hello " +
"World";
转义字符
对于向字符串本身添加新行,有一个转义字符(\n
)。这种反斜杠符号用于书写特殊字符,如反斜杠和双引号。在特殊字符中还有一个 Unicode 字符符号,用于书写任何字符。下表列出了所有的转义字符。
性格;角色;字母
|
意义
|
| — | — |
| \n
| 新行 |
| \t
| 横表 |
| \b
| 退格 |
| \r
| 回车 |
| \uFFFF
| Unicode 字符(四位十六进制数字) |
| \f
| 换页 |
| \’
| 单引号 |
| \”
| 双引号 |
| \\
| 反斜线符号 |
字符串比较
比较两个字符串的方法是使用String
类的equals
方法。如果使用相等运算符(==
),将会比较内存地址。
boolean x = a.equals(b); // compares string
boolean y = (a == b); // compares address
请记住,Java 中的所有字符串都是String
对象。因此,可以直接在常量字符串上调用方法,就像在变量上一样。
boolean z = "Hello".equals(a); // true
StringBuffer 类
String
类有大量可用的方法,但是它不包含任何操作字符串的方法。那是因为 Java 中的字符串是不可变的。一旦创建了一个String
对象,其内容就不能改变,除非整个字符串被完全替换。因为大多数字符串从未被修改过,所以这样做是为了让String
类更有效。对于需要可修改字符串的情况,可以使用StringBuffer
类,这是一个可变的字符串对象。
StringBuffer sb = new StringBuffer("Hello");
这个类有几个操作字符串的方法,包括append
、delete
和insert
:
sb.append(" World"); // add to end of string
sb.delete(0, 5); // remove 5 first characters
sb.insert(0, "Hello"); // insert string at beginning
您可以使用toString
方法将StringBuffer
对象转换回常规字符串,该方法返回对象的字符串表示。它存在于 Java 的每个类中,因为它是由所有类继承的Object
定义的。
String s = sb.toString();
文本块
文本块是由三个双引号("""
)分隔的多行字符串。它提供了一种简化的方法来编写跨越多行的字符串,而不必指定转义字符,如换行符或引号。
String textBlock = """
line 1
line 2""";
开始分隔符后的换行符是强制的,因此文本块必须跨越多行。文本块中使用的任何换行符都将被自动解释为换行符,因此前面的文本块相当于以下字符串:
String s = "line 1\nline 2";
相对于其他行缩进一行文本的空白将被保留。但是,任何用于缩进所有行的初始空格都将被删除。
String html = """
<div>
<p>Hi</p>
</div>""";
因此,该文本块与以下字符串相同:
String html = "<div>\n <p>Hi</p>\n</div>";
文本块作为预览特性在 Java 13 中引入,并在 Java 15 中成为标准特性。
六、数组
一个数组是一个固定大小的数据结构,用于存储一组单一类型的值。
数组声明
要声明一个数组,需要将一组方括号附加到数组将包含的数据类型上,后跟数组的名称。数组可以用任何数据类型声明,并且它的所有元素都必须是该类型。
int[] x;
或者,括号可以放在数组名称之后。但是,不鼓励这种形式。因为括号影响类型,所以它们应该出现在类型旁边。
int y[]; // discouraged form
数组分配
数组被分配了new
关键字,然后是数据类型和一组包含数组的长度的方括号——数组可以包含的固定数量的元素。一旦创建了数组,元素将自动分配给该数据类型的默认值,在 int 数组的情况下是零(0
)。
int[] y = new int[3]; // allocate 3 elements with value 0
数组赋值
要填充数组,可以通过将元素的数字索引放在方括号内,然后给它们赋值,一次引用一个元素。请注意,索引从零开始。
y[0] = 1;
y[1] = 2;
y[2] = 3;
或者,可以使用花括号符号一次性赋值。如果同时声明数组,可以选择省略new
关键字、数据类型和方括号。
int[] x = new int[] {1,2,3};
int[] x = {1,2,3};
初始化数组元素后,可以通过引用方括号内的元素索引来访问它们:
System.out.print(x[0] + x[1] + x[2]); // "6"
多维数组
多维数组的声明、创建和初始化类似于一维数组,只是它们有额外的方括号。它们可以有任意数量的维度,并且为每个维度添加另一组方括号。
String[][] x = {{"00","01"},{"10","11"}};
String[][] y = new String[2][2];
y[0][0] = "00";
y[0][1] = "01";
y[1][0] = "10";
y[1][1] = "11";
System.out.print(x[0][0] + x[1][1]); // "0011"
数组列表类
关于数组,需要记住的重要一点是,它们的长度是固定的,没有办法改变它们的大小。数组的大小可以通过数组的length
成员来获取。
Int[] x = new int[3];
int size = x.length; // 3
对于需要可调整大小的数组的情况,可以使用通用的ArrayList<T>
类,它位于 java.util 包中。该列表将保存的数据类型在尖括号(<>
)中指定。泛型类将在后面的章节中详细讨论。
import java.util.ArrayList;
// ...
// Create an ArrayList collection for strings
java.util.ArrayList<String> a = new java.util.ArrayList<>();
ArrayList
类有几个有用的方法来改变列表,比如add
、set
和remove
:
a.add("Hi"); // add an element
a.set(0, "Hello"); // change first element
a.remove(0); // remove first element
要从ArrayList<T>
中检索一个元素,可以使用get
方法。然后,必须将元素显式转换回其原始类型,因为它在内部存储为对象类型,可以保存任何引用数据类型。
a.add("Hello World");
String s = (String)a.get(0); // Hello World
七、条件语句
条件语句用于根据不同的条件执行不同的代码块。
如果语句
只有当括号内的条件被评估为真时,if
语句才会执行。条件可以包括任何比较和逻辑运算符。
int x = 1;
// ...
if (x == 1) {
System.out.println(x + " = 1");
}
为了测试其他条件,if
语句可以被任意数量的else-if
子句扩展。只有当所有先前的条件都为假时,才会测试每个附加条件。
else if (x > 1) {
System.out.println(x + " > 1");
}
对于处理所有其他情况,可以在末尾有一个else
子句,如果前面的所有条件都为假,则执行该子句:
else {
System.out.println(x + " < 1");
}
如果只需要有条件地执行一条语句,可以省去花括号。但是,包含它们被认为是一种好的做法,因为它们可以提高代码的可读性。
if (x == 1)
System.out.println(x + " = 1");
else if (x > 1)
System.out.println(x + " > 1");
else
System.out.println(x + " < 1");
交换语句
switch
语句检查一个值和一系列事例标签之间的相等性。然后,它执行匹配的案例。该语句可以包含任意数量的事例,并且可以以处理所有其他事例的默认标签结束。
switch (x)
{
case 0: System.out.println(x + " is 0"); break;
case 1: System.out.println(x + " is 1"); break;
default: System.out.println(x + " is something else");
}
注意,每个case
标签后面的语句没有用花括号括起来。相反,语句以关键字break
结束。没有了break
,死刑将会落到下一个案子。如果需要以相同的方式评估几个案例,这可能会很有用。
任何整数数据类型都可以与switch
语句一起使用,包括byte
、short
、int
和char
。从 Java 7 开始,String
类型也是允许的。
String fruit = "apple";
switch (fruit)
{
case "apple": System.out.println("apple"); break;
default: System.out.println("not an apple");
}
开关表达式
这个开关在 Java 12 中扩展了新的预览特性,成为 Java 14 中的标准特性。考虑以下用于模拟表达式(计算值的代码)的开关:
String result;
switch (x)
{
case 1: result = "one"; break;
case 2:
case 3: result = "two or three"; break;
default: result = "many";
}
使用箭头标签(->
)代替传统的 case 标签可以使代码更加简洁。使用这种形式时,箭头标签后只能出现一个表达式或语句,并且每个 case 可以包含多个常量,用逗号分隔。箭头标签不允许穿透,因此不使用 break 关键字。
String result;
switch (x)
{
case 1 -> result = "one";
case 2, 3 -> result = "two or three";
default -> result = "many";
}
这个 switch 语句可以进一步简化为一个 switch 表达式。在这种形式下,开关将计算匹配事例后面的表达式。请记住,默认标签随后会变成强制标签,这样所有可能的输入值都会产生一个有效的表达式。
String result = switch (x)
{
case 1 -> "one";
case 2, 3 -> "two or three";
default -> "many";
};
如果需要多个表达式,可以包含一个完整的代码块。在这样的块中,yield 语句用于指定 switch 表达式将计算的值。
String result = switch (x)
{
case 1 -> "one";
case 2, 3 -> "two or three";
default -> {
if (x == 4) yield "four";
else yield "many";
}
};
三元运算符
三元运算符(?:
)可以用要返回的值替换单个if-else
子句。运算符有三个表达式。如果第一个表达式的计算结果为真,则返回第二个表达式,如果为假,则计算并返回第三个表达式。它是 Java 中唯一接受三个操作数的运算符。
x = (x < 0.5) ? 0 : 1; // ternary operator (?:)
这个三元语句相当于下面的 if-else 子句:
if (x < 0.5) { x = 0; }
else { x = 1; }
八、循环
Java 中有四种循环结构。它们用于多次执行一个特定的代码块。与条件语句if
一样,如果代码块中只有一条语句,循环的花括号可以省去。
当循环
只有当指定的条件为真时,while
循环才会在代码块中运行,并且只要条件保持为真,循环就会继续。下面的循环将打印出数字 0 到 4:
int i = 0;
while (i < 5) {
System.out.print(i++); // "01234"
}
注意,循环的条件必须评估为一个boolean
值。仅在每次迭代(循环)开始时检查该条件。
Do While 循环
除了检查代码块之后的条件之外,do while
循环的工作方式与while
循环相同。因此,它将始终至少在代码块中运行一次。
int i = 0;
do {
System.out.print(i++);
} while (i < 5); // "01234"
For 循环
for
循环用于遍历一个代码块特定的次数。它使用三个参数。第一个参数初始化一个计数器,并且总是在循环之前执行一次。此计数器变量的范围仅限于 for 循环,并且在循环后不可访问。第二个参数保存循环的条件,并在每次迭代之前进行检查。最后,第三个参数包含计数器的增量,在每次迭代结束时执行。
for (int i = 0; i < 5; i++) {
System.out.print(i); // "01234"
}
for
回路可能有几种变化。例如,可以使用逗号运算符将第一个和第三个参数分成几个语句。
for (int k = 0, m = 0; k < 5; k++, m--) {
System.out.print(k + m); // "00000"
}
您也可以选择省略一个或多个参数。例如,第三个参数可以移到循环体中。
for (int k = 0, m = 0; k < 5;) {
System.out.print(k + m); // "00000"
k++; m--;
}
对于每个循环
“for
each”循环提供了一种简单的方法来遍历数组。在每次迭代中,数组中的下一个元素被赋给指定的变量,循环继续执行,直到遍历完整个数组。
int[] array = { 1,2,3 };
for (int element : array) {
System.out.print(element); // "123"
}
中断并继续
有两个特殊的关键字可以在循环中使用:break
和continue
。break
关键字结束循环结构,continue
跳过当前迭代的剩余部分,并在下一次迭代的开始处继续。
for (int i = 0; i < 10; i++)
{
if (i == 5) break; // end loop
if (i == 3) continue; // start next iteration
System.out.print(i); // "0124"
}
要中断当前循环之上的循环,必须首先通过在该循环前添加一个后跟冒号的名称来标记该循环。有了这个标签,现在可以将它用作break
语句的参数,告诉它从哪个循环中退出。这也适用于continue
关键字 d,以便跳到指定循环的下一次迭代。
myLoop: for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 10; j++)
{
break myLoop; // end outer for loop
}
}
标记块
一个标记为的块,也称为一个名为的块,通过在一个匿名代码块前放置一个标签来创建。关键字break
可以用来脱离这样的块,就像在带标签的循环中一样。这可能是有用的,例如,当执行验证时,如果一个验证步骤失败,整个过程必须中止。
validation:
{
if(true)
break validation;
}
标记块对于将大型方法组织成几个部分很有用。在大多数情况下,将方法拆分是一个更好的主意。但是,如果新方法需要很多参数,或者如果该方法仅在单个位置使用,则一个或多个标记块可能是优选的。
九、方法
方法是可重用的代码块,只在被调用时执行。
定义方法
您可以通过键入返回类型,后跟方法名、一组括号和代码块来创建方法。关键字void
可以用来指定该方法不返回值。方法的命名约定与变量的相同——一个描述性的名称,第一个单词小写,后面所有单词的第一个字母大写。
class MyApp
{
void myPrint()
{
System.out.println("Hello");
}
}
调用方法
前面的方法将简单地打印出一条文本消息。为了让从 main 方法中调用(调用),必须首先创建一个MyApp
类的实例。然后在实例名后面使用点操作符,以访问其成员,包括myPrint
方法。
public static void main(String[] args)
{
MyApp m = new MyApp();
m.myPrint(); // "Hello"
}
方法参数
方法名后面的括号用于向方法传递参数。为此,必须首先将相应的参数以逗号分隔列表的形式添加到方法声明中。
void myPrint(String s)
{
System.out.println(s);
}
一个方法可以被定义为接受任意数量的参数,并且它们可以有任意的数据类型。只要确保以正确的顺序使用相同类型和数量的参数调用该方法。
public static void main(String[] args)
{
MyApp m = new MyApp();
m.myPrint("Hello"); // "Hello"
}
准确地说,参数出现在方法定义中,而参数出现在方法调用中。然而,这两个术语有时会被错误地互换使用。
返回语句
方法可以返回值。然后用该方法将返回的数据类型替换void
关键字,并且用指定返回类型的参数将return
关键字添加到方法体中。
public class MyApp
{
String getString()
{
return "Hello";
}
}
Return
是一个跳转语句,它使方法退出,并将指定的值返回到调用该方法的地方。例如,前面的方法可以作为参数传递给println
方法,因为该方法的计算结果是一个字符串。
public static void main(String[] args)
{
MyApp m = new MyApp();
System.out.println( m.getString() ); // "Hello"
}
return
语句也可以在void
方法中使用,以便在到达结束块之前退出。在此上下文中使用时,不指定返回值。
void myPrint(String s)
{
if (s == "") { return; } // skip if string is empty
System.out.println(s);
}
方法重载
只要参数的类型或数量不同,就可以用相同的名称声明多个方法。称为方法重载,例如,这可以在System.out.println
方法的实现中看到。这是一个强大的特性,允许一个方法处理各种参数,而程序员不需要知道使用不同的方法。
void myPrint(String s)
{
System.out.println(s);
}
void myPrint(int i)
{
System.out.println(i);
}
传递参数
Java 与许多其他语言的不同之处在于,所有方法参数都是通过值传递的。事实上,它们不能通过引用来传递。对于值数据类型(基本类型),这意味着在方法中只有变量的本地副本被更改,所以更改不会影响原始变量。对于引用数据类型(类、接口和数组),这意味着只将内存地址的副本传递给方法。因此,如果整个对象被替换,更改不会传播回调用者,但是对对象的更改将影响原始对象,因为副本指向相同的内存位置。
public class MyApp
{
public static void main(String[] args)
{
MyApp m = new MyApp();
int x = 0; // value data type
m.set(x); // value is passed
System.out.println(x); // "0"
int[] y = {0}; // reference data type
m.set(y); // address is passed
System.out.println(y[0]); // "10"
}
void set(int a) { a = 10; }
void set(int[] a) { a[0] = 10; }
}
十、类
一个类是一个用来创建对象的模板。类由成员组成,其中主要的两个是字段和方法。字段是保存对象状态的变量,而方法定义了对象能做什么——所谓的对象行为。
class MyRectangle
{
int x, y;
int getArea() { return x * y; }
}
对象创建
要从定义类的外部访问(非静态)字段或方法,必须首先创建该类的对象。这是使用new
关键字完成的,它将在系统内存中创建一个新对象。
public class MyApp
{
public static void main(String[] args)
{
// Create an object of MyRectangle
MyRectangle r = new MyRectangle();
}
}
一个对象也被称为一个实例。该对象将包含自己的一组实例变量(非静态字段),这些变量可以保存与该类的其他实例不同的值。
访问对象成员
除了创建对象之外,需要在类定义中将超出其包可访问的类成员声明为public
。
class MyRectangle
{
public int x, y;
public int getArea() { return x * y; }
}
现在可以通过在实例名称后使用点运算符来访问该对象的成员:
public class MyApp
{
public static void main(String[] args)
{
MyRectangle r = new MyRectangle();
r.x = 10;
r.y = 5;
int area = r.getArea(); // 50 (5*10)
}
}
构造器
一个类可以有一个构造器,一种用于实例化(构造)对象的特殊方法。它总是与该类同名,并且没有返回类型,因为它隐式返回该类的一个新实例。为了能被不在其包中的另一个类访问,需要用public
访问修饰符来声明它。当使用new
语法创建了一个MyRectangle
类的新实例时,将调用构造器方法,在下面的示例中,该方法将字段设置为指定的默认值。
class MyRectangle
{
int x, y;
public MyRectangle() { x = 10; y = 20; }
}
像任何其他方法一样,构造器可以有一个参数列表。如下面的代码所示,这可用于使字段的初始值取决于创建对象时传递的参数。
class MyRectangle
{
int x, y;
public MyRectangle(int a, int b) { x = a; y = b; }
}
public class MyApp
{
public static void main(String[] args)
{
MyRectangle r = new MyRectangle(20, 15);
}
}
这个关键字
在构造器内部,以及在属于对象的其他方法中,可以使用一个名为this
的特殊关键字。this
关键字是对该类的当前实例的引用。例如,如果构造器的参数与相应的实例变量同名,那么实例变量仍然可以通过使用this
关键字来访问,即使它们被参数所掩盖。
class MyRectangle
{
int x, y;
public MyRectangle(int x, int y)
{
this.x = x;
this.y = y;
}
}
构造器重载
为了支持不同的参数列表,可以重载构造器。在下面的示例中,如果类在没有任何参数的情况下被实例化,则这些字段将被赋予指定的默认值。对于一个参数,两个字段都将被设置为所提供的值,而对于两个参数,每个字段都将被分配一个单独的值。
class MyRectangle
{
int x, y;
public MyRectangle() { x = 10; y = 20; }
public MyRectangle(int a) { x = a; y = a; }
public MyRectangle(int a, int b) { x = a; y = b; }
}
试图用错误的参数数量或错误的数据类型创建对象将导致编译时错误,就像任何其他方法一样。
构造器链接
还可以使用this
关键字从一个构造器调用另一个构造器。被称为构造器链接,这允许更大的代码重用。请注意,关键字以方法调用的形式出现,并且必须位于构造器的第一行。
public MyRectangle() { this(10, 20); }
public MyRectangle(int a) { this(a, a); }
public MyRectangle(int a, int b) { x = a; y = b; }
初始字段值
如果类中有需要分配默认值的字段,比如在刚刚显示的第一个构造器中,可以在声明字段的同时简单地分配它们。这些初始值将在调用构造器之前赋值。
class MyRectangle
{
int x = 10, y = 20;
}
默认构造器
即使没有定义构造器,也可以创建一个类。这是因为编译器会自动创建一个默认的无参数构造器。
public class MyApp
{
public static void main(String[] args)
{
// Default constructor used
MyApp a = new MyApp();
}
}
如果定义了任何自定义构造器,编译器将不会添加默认的无参数构造器。
空
内置常量null
用于表示未初始化的对象。它只能赋给对象,不能赋给基元类型的变量。等号运算符(==
)可以用来测试一个对象是否为空。
String s = null;
// ...
if (s == null) s = new String();
默认值
对象的默认值是null
。对于原始数据类型,默认值如下:整数类型变为0
,浮点类型变为 0.0,char 具有表示零的 Unicode 字符(\0000
),Boolean 为false
。缺省值将由编译器自动分配,但只适用于字段,不适用于局部变量。但是,显式指定字段的默认值被认为是好的编程方式,因为这使得代码更容易理解。对于局部变量,默认值不是由编译器设置的。取而代之的是,编译器强迫程序员给所使用的任何局部变量赋值,以避免与错误地使用未赋值变量相关的问题。
public class MyApp
{
int x; // field is assigned default value 0
int dummy() {
int x; // local variable must be assigned if used
}
}
垃圾收集工
Java 运行时环境有一个垃圾收集器,当不再需要对象时,它会定期释放对象使用的内存。这将程序员从繁琐且容易出错的内存管理任务中解放出来。当一个对象不再被引用时,它就有资格被销毁。例如,当对象超出范围时,就会出现这种情况。也可以通过将对象的引用设置为null
来显式删除对象。
public class MyApp
{
public static void main(String[] args)
{
MyApp a = new MyApp();
// Make object available for garbage collection
a = null;
}
}
十一、静态
关键字static
用于创建无需创建类实例就可以访问的字段和方法。静态(类)成员只存在于一个副本中,该副本属于类本身,而实例(非静态)成员是作为每个新对象的新副本创建的。这意味着静态方法不能使用实例成员,因为这些方法不是实例的一部分。另一方面,实例方法可以使用静态成员和实例成员。
class MyCircle
{
float r = 10; // instance field
static float pi = 3.14F; // static/class field
// Instance method
float getArea() { return newArea(r); }
// Static/class method
static float newArea(float a) { return pi*a*a; }
}
访问静态成员
要从类外部访问静态成员,先使用类名,然后使用点运算符。该操作符与用于访问实例成员的操作符相同,但是要访问它们,需要一个对象引用。试图通过使用对象引用(而不是类名)来访问静态成员将导致警告,因为这使得更难看到静态成员正在被使用。
public static void main(String[] args)
{
float f = MyCircle.pi;
MyCircle c = new MyCircle();
float g = c.r;
}
静态方法
静态成员的优点是它们可以被其他类使用,而不必创建该类的实例。因此,当只需要变量的一个实例时,应该将字段声明为静态的。如果方法执行独立于任何实例变量的通用函数,那么它们应该被声明为静态的。一个很好的例子是只包含静态方法和字段的Math
类。
double pi = Math.PI;
Math
是每个 Java 应用程序默认包含的类之一,因为它属于 java.lang 包,该包总是被导入。这个包包含 Java 语言的基础类,比如String
、Object
和System
。
静态字段
静态字段具有在应用程序的整个生命周期中保持不变的优势。这意味着它们可以用来记录一个方法在类的所有实例中被调用的次数。静态字段的初始值只设置一次,有时在使用类或字段之前。
class MyCircle
{
static void foo() { count++; }
static int count = 0;
}
静态初始化块
如果静态字段的初始化需要不止一行或一些其他逻辑,则可以使用静态 初始化块。与构造器不同,这个块只运行一次,与静态字段同时初始化。
class MyClass
{
static int[] array = new int[5];
// Static initialization block
static
{
int i = 0;
for(int element : array)
element = i++;
}
}
实例初始化块
一个初始化块提供了另一种分配实例字段的方法。这个块放在类级别,就像静态初始化块一样,但是没有使用static
关键字。任何放在括号中的代码都会被编译器复制到每个构造器的开头。
class MyClass
{
int[] array = new int[5];
// Initialization block
{
int i = 0;
for(int element : array) element = i++;
}
}
一个类可以有多个实例初始化块和静态初始化块。
十二、继承
继承允许一个类获得另一个类的成员。在下面的例子中,Apple
从Fruit
继承而来。这是用extends
关键字指定的。Fruit
然后成为苹果的超类,苹果又成为Fruit
的子类。除了自己的成员,Apple
还获得了Fruit
中所有可访问的成员,除了任何构造器。
// Superclass (parent class)
class Fruit
{
public String flavor;
}
// Subclass (child class)
class Apple extends Fruit
{
public String variety;
}
Java 中的一个类只能从一个超类继承,如果没有指定类,它将隐式地从Object
继承。因此,Object
是所有类的根类。
// Same as class MyClass {}
class MyClass extends Object {}
向上抛
从概念上讲,子类是超类的特化。这意味着Apple
是一种Fruit
,也是一种Object
,因此可以用在任何需要Fruit
或Object
的地方。例如,如果创建了一个Apple
的实例,它可以被向上转换为到Fruit
,因为子类包含了超类中的所有内容。
Apple a = new Apple();
Fruit f = a;
通过这个变量,Apple
被视为一个Fruit
,因此只有Fruit
成员可以被访问:
f.flavor = "Sweet";
向下铸造
当类被向下转换回到Apple
时,特定于Apple
的字段将被保留。那是因为Fruit
只包含了Apple
——它没有将 ?? 转化为苹果。向下转换必须使用 Java 转换格式显式进行,因为不允许将实际的Fruit
对象向下转换为Apple
。
Apple b = (Apple)f;
运算符的实例
作为一项安全预防措施,您可以在运行时进行测试,看看是否可以通过使用instanceof
操作符将一个对象转换为一个特定的类。如果左侧对象可以被转换为右侧类型而不会导致异常,则该操作符返回true
。
if (f instanceof Apple)
{
Apple myApple = (Apple)f;
// use myApple here
}
像这样使用 instanceof 操作符是很常见的,其中条件检查之后是类型转换。因此,我们添加了一个更简洁的语法,将指定的变量包含在条件中。变量的范围仅限于条件块。
if (f instanceof Apple myApple)
{
// use myApple here
}
这是 instanceof 操作符的模式匹配特性的一部分,它成为 Java 14 中的预览特性,然后成为 Java 16 中的标准特性。该操作符被扩展为不仅接受类型,还允许在单个表达式中提取和测试类型。
class Speed
{
public int velocity = 10;
}
public class MyApp
{
public static void main(String[] args) {
Object o = new Speed();
// ...
if ( (o instanceof Speed s) && (s.velocity > 5) ) {
System.out.println("Speed is " + s.velocity);
}
}
模式匹配开关
Java 17 增加了 switch 语句和表达式的模式匹配作为预览特性。这扩展了 switch,使其可以处理任何类型模式,而不像以前那样只处理数字、字符串和枚举类型。将事例标签与模式一起使用时,选择由模式匹配而不是相等检查来确定。在下面的代码中,object 变量的值与长模式匹配,并且将执行与该案例相关联的代码。
Object o = 5L; // L suffix means Long type
String myType = switch(o)
{
case null -> "null";
case Integer i -> "integer is " + i;
case Long l -> "long is " + l;
default -> o.toString();
}
System.out.println(myType) // "long is 5"
限制继承
可以将类声明为 final,以防止任何类继承它:
// Cannot be inherited
final class Fruit {}
一种限制较少的方法是使用 sealed 修饰符只允许某些类继承。这些类是在任何 extends 子句右侧的逗号分隔的许可证子句中指定的。
// Can be inherited only by Apple or Orange
sealed class Fruit permits Apple, Orange {}
从密封类继承的允许类必须依次声明为非密封的、密封的或最终的。非密封类可以被任何类继承,而最终类不允许再有子类。
// Can be inherited by any class
non-sealed class Lemon extends Fruit{}
// Can be inherited only by RedDelicious class
sealed class Apple extends Fruit permits RedDelicious{}
// Cannot be inherited
final class Orange extends Fruit {}
Java 15 中增加了密封类作为预览特性。sealed 和 final 修饰符也可以应用于接口和抽象类。
十三、覆盖
子类中的成员可以重定义超类中的成员。这通常是为了给实例方法新的实现。
覆盖方法
在下面的例子中,Rectangle
的getArea
方法在Triangle
中被覆盖,方法是用相同的方法签名重新声明它。签名包括方法的名称、参数和返回类型。但是,可以更改访问级别,以允许比被覆盖的方法更多的访问。
class Rectangle
{
public int w = 10, h = 10;
public int getArea() { return w * h; }
}
class Triangle extends Rectangle
{
public int getArea() { return w * h / 2; }
}
覆盖注释
为了表明这种覆盖是有意的,@Override
注释应该放在方法之前。这个注释是在 Java 5 中添加的,以防止意外覆盖并提高可读性。如果带注释的方法实际上没有覆盖任何东西,编译器也会给出警告,如果签名与父类中的方法不匹配,就会发生这种情况。
class Triangle extends Rectangle
{
@Override
public int getArea() {
return w * h / 2;
}
}
从Triangle
实例调用getArea
方法将调用Triangle
的方法版本:
Triangle o = new Triangle();
o.getArea(); // (50) calls Triangle's version
如果Triangle
的实例被向上转换为Rectangle
,那么Triangle
的方法版本仍然会被调用,因为Rectangle
的版本已经被覆盖:
Rectangle o = new Triangle();
o.getArea(); // (50) calls Triangle's version
隐藏方法
这只适用于实例方法,不适用于类(静态)方法。如果一个名为newArea
的类方法被添加到Rectangle
并在Triangle
中重新定义,那么Triangle
的方法版本将只隐藏Rectangle
的实现。因此,没有使用@Override
注释。
class Rectangle
{
public int w = 10, h = 10;
public static int newArea(int a, int b) {
return a * b;
}
}
class Triangle extends Rectangle
{
public static int newArea(int a, int b) {
return a * b / 2;
}
}
从Triangle
的类中调用newArea
将会调用Triangle
的版本,但是从Rectangle
的类中调用方法将会调用Rectangle
的实现:
Triangle o = new Triangle();
Triangle.newArea(10,10); // (50) calls Triangle's version
Rectangle r = o;
Rectangle.newArea(10,10); // (100) calls Rectangle's version
重定义的实例方法在 Java 中总是被覆盖,重定义的类方法总是被隐藏。没有办法改变这种行为,例如,在 C++或 C#中可以做到。
隐藏字段
在 Java 中不能覆盖字段,但是可以通过声明一个与继承字段同名的字段来隐藏它们。字段的类型及其访问级别可以不同于继承字段。通常不建议隐藏字段,因为这会使代码更难阅读。
class Rectangle
{
public int w = 10, h = 10;
}
class Triangle extends Rectangle
{
public int w = 5, h = 5; // hide inherited fields
}
public class MyApp
{
public static void main(String args[]) {
Triangle t = new Triangle();
Rectangle r = t;
System.out.println(t.w); // "5"
System.out.println(r.w); // "10"
}
}
访问重新定义的成员
被覆盖的方法(或隐藏的实例字段)仍然可以使用super
关键字从子类内部访问。这个关键字是对超类的当前实例的引用。
class Triangle extends Rectangle
{
@Override
public int getArea() {
return super.getArea() / 2;
}
}
调用父构造器
另一个可以使用super
关键字的地方是在构造器的第一行。在那里,它可以执行一个调用超类的构造器的方法调用。
public Triangle(int a, int b) { super(a,b); }
如果一个构造器的第一行不是对另一个构造器的调用,Java 编译器会自动添加对超类的无参数构造器的调用。这确保了所有的祖先类都被正确构造。
public Triangle() { super(); }
十四、包和导入
包用于避免命名冲突,并将代码文件组织到不同的目录中。到目前为止,在本书中,代码文件位于项目源目录的根目录下,因此属于所谓的默认包。在 Java 中,文件所属的目录(相对于项目的源目录)对应于包名。
要将代码文件分配给包(例如 mypackage ),必须将其移动到项目目录下以该名称命名的文件夹中。此外,文件必须使用关键字package
后跟包名(和路径)来指定它属于哪个包。每个源文件中只能有一个 package 语句,而且必须是第一行代码,任何注释除外。请注意,包的命名约定都是小写的。
// This file belongs to mypackage
package mypackage;
包可以是任意深度的目录层,层次结构中的层由点分隔。例如,如果包含代码文件的 mypackage 文件夹被放在一个名为 sub 的项目文件夹中,那么包声明应该如下所示。
package sub.mypackage;
访问包
为了演示如何访问包成员,在项目源目录下的 sub\mypackage 文件夹中放置了一个名为 MyClass.java 的文件。该文件包含一个名为MyClass
的公共类。
package sub.mypackage;
public class MyClass {}
MyClass
可以通过两种方式之一从另一个源文件访问。第一种方法是键入完全限定名。
// Fully qualified class name
sub.mypackage.MyClass m;
第二种选择是通过包含带有import
关键字的类来缩短完全限定名。在代码文件中,import
语句必须位于包声明语句之后,所有其他成员之前。除了让程序员不必键入完全限定名之外,它没有别的用途。
import mypackage.sub.MyClass;
// ...
MyClass m;
除了导入特定的类之外,包内的所有类型(类或接口)都可以通过使用星号(*
)来导入。注意,这并没有导入任何子包。
import java.util.*;
import
语句的第三种变化是静态导入,它导入一个类的所有静态成员。一旦静态成员被导入,就可以使用它们,而不必指定类名。
import static java.lang.Math.*;
// ...
double pi = PI; // Math.PI
十五、模块
**模块是一组可重用的相关包和资源文件以及模块描述符文件。它们应该是自给自足的,并且只公开接口来使用模块的功能。
*## 创建模块
NetBeans 有一个特殊的项目类型来管理多个模块。要创建这样一个项目,转到文件➤新项目,并从那里,选择 Java 与 Ant 类别下的 Java 模块化项目。单击 Next,将项目命名为 MyModules,然后单击 Finish 创建项目。继续,通过在“项目”窗口中右键单击 MyModules 项并选择“新建➤模块”,将名为“firstmodule”的模块添加到该项目中。
在“项目”窗口中可以看到,一个模块有一个名为 module-info.java 的特殊文件。这个模块描述符文件必须位于将要编译成模块的包的根文件夹中。文件中有一个模块描述符,它由模块关键字、模块名和一组花括号组成。
module firstmodule {
}
接下来,让我们创建一个包,其中包含一个要包含在该模块中的类。右键单击项目窗口中的“firstmodule”项,并选择“新建➤ Java 类”。将其命名为 util。MyClass 自动将它放在一个名为 util 的新包中。该包是强制性的,因为在模块中不允许将除模块描述符之外的文件放在默认包(顶级目录)中。在新的源文件中键入以下代码示例:
// util.MyClass.java
package util;
public class MyClass {
public static void sayHi() {
System.out.println("Hello Module");
}
}
返回到模块描述符文件,使用关键字exports
加上完全限定的包名(firstmodule.util)为 util 包添加一个导出语句。这将使该包对使用该模块的任何其他模块可见。任何其他没有被显式导出的包,包括子包,从模块外部都是不可访问的。
module firstmodule {
exports firstmodule.util; // make package visible
}
使用模块
我们现在将创建第二个模块来利用第一个模块。向项目中添加一个名为 secondmodule 的新模块。在其模块描述符文件中,导入 firstmodule,使其导出的包在这个新模块中可见。
module secondmodule {
requires firstmodule; // import module
}
添加一个名为 app 的类。MyApp 到模块,所以类文件被放在一个名为 App 的包中。在该文件中包含以下代码,它利用了第一个模块中公开的 util 包:
// app.MyApp.java
package app;
public class MyApp {
public static void main(String[] args) {
util.MyClass.sayHi(); // "Hello Module"
}
}
这是让第二个模块使用第一个模块公开的功能所需的全部代码。编译并运行项目,让 main 方法从导入的模块中调用函数,这将显示“Hello Module”文本字符串。*
十六、访问级别
Java 中有四个可用的访问级别:public
、protected
、private
和包私有。Package-private 没有使用关键字显式声明。相反,它是 Java 中每个成员的默认访问级别。
public int myPublic; // unrestricted access
protected int myProtected;// package or subclass access
int myPackage; // package access
private int myPrivate; // class access
私有访问
无论访问级别如何,所有成员都可以在声明它们的类(包含类)中进行访问。这是唯一可以访问私有成员的地方。
package mypackage;
public class MyApp
{
public int myPublic;
protected int myProtected;
int myPackage;
private int myPrivate;
void test()
{
myPublic = 0; // allowed
myProtected = 0; // allowed
myPackage = 0; // allowed
myPrivate = 0; // allowed
}
}
包-私人访问
可以在包含包的任何地方访问包私有成员,但不能从另一个包访问:
package mypackage;
public class MyClass
{
void test(MyApp m)
{
m.myPublic = 0; // allowed
m.myProtected = 0; // allowed
m.myPackage = 0; // allowed
m.myPrivate = 0; // inaccessible
}
}
受保护的访问
受保护的成员在子类中和包含包中是可访问的。在下面的代码中,可以访问受保护的成员,因为 MyChild 是定义该成员的 MyApp 的子类:
package newpackage;
import mypackage.MyApp;
public class MyChild extends MyApp
{
void test()
{
myPublic = 0; // allowed
myProtected = 0; // allowed (in subclass)
myPackage = 0; // inaccessible
myPrivate = 0; // inaccessible
}
}
请注意,除了子类之外,受保护的成员也可以在包含包的任何地方访问。这种行为不同于其他语言,如 C++和 C#,在这些语言中,受保护的成员只能从子类和包含类中访问。
package mypackage;
public class MyTest
{
void test(MyApp m)
{
m.myPublic = 0; // allowed
m.myProtected = 0; // allowed (same package)
m.myPackage = 0; // inaccessible
m.myPrivate = 0; // inaccessible
}
}
公共访问
public
修饰符允许从任何可以引用成员的地方进行无限制的访问:
package newpackage;
import mypackage.MyApp;
public class MyClass
{
void test(MyApp m)
{
m.myPublic = 0; // allowed
m.myProtected = 0; // inaccessible
m.myPackage = 0; // inaccessible
m.myPrivate = 0; // inaccessible
}
}
顶级访问
在包中直接声明的成员(顶级成员)只能在包私有和公共访问之间进行选择。例如,没有访问修饰符的顶级类将默认为 package-private。这样的类只能在包含的包中访问。相反,显式声明为public
的顶级类也可以从其他包中访问。
// Accessible only from containing package
class PackagePrivateClass {}
// Accessible from any package
public class PublicClass {}
嵌套类访问
Java 允许在其他类中定义类,这些被称为嵌套类。这样的类可以有四个访问级别中的任何一个。如果嵌套类不可访问,它就不能被实例化或继承。
public class MyClass
{
// Only accessible within MyClass
private class PrivateNestedClass {}
}
请记住,嵌套成员可以受到它们自己的访问级别和包含类的访问级别的限制。例如,包私有类中的公共嵌套类将不能从其他包中访问。
class MyClass
{
// Only accessible within containing package
public class PrivateNestedClass {}
}
访问级别指南
作为一项准则,在选择访问级别时,通常最好使用最严格的级别。这是因为一个成员可以被访问的位置越多,它可以被错误访问的位置就越多,这使得代码更难调试。使用限制性访问级别还使得修改该类变得更加容易,而不会破坏使用该类的任何其他开发人员的代码。