Skip to content

Latest commit

 

History

History
1629 lines (1161 loc) · 67.7 KB

File metadata and controls

1629 lines (1161 loc) · 67.7 KB

五、Java 语言元素和类型

本章首先系统地介绍 Java,并定义语言元素标识符、变量、文本、关键字、分隔符和注释。它还描述了 Java 类型、基本类型和引用类型。特别注意的是String类、enum类型和数组。

在本章中,我们将介绍以下主题:

  • Java 语言元素是什么?
  • 评论
  • 标识符和变量
  • 保留和限制关键字
  • 分离器
  • 基本类型和文字
  • 引用类型和字符串
  • 阵列
  • 枚举类型
  • 练习–变量声明和初始化

Java 语言元素是什么?

与任何编程语言一样,Java 具有应用于语言元素的语法。这些元素是用于组成语言结构的构建块,允许程序员表达意图。元素本身具有不同的复杂程度。较低级别(较简单)的图元可以构建较高级别(较复杂)的图元。有关 Java 语法和语言元素的更详细、更系统的处理,请参阅 Java 规范(https://docs.oracle.com/javase/specs )。

在本书中,我们从属于最低级别之一的输入元素开始。它们被称为输入元素,因为它们充当 Java 编译器的输入。

输入元素

根据 Java 规范,Java 输入元素可以是以下三种元素之一:

  • 空白:这可以是这些 ASCII 字符之一——SP(空格)、HT(水平制表符)或 FF(换页符,也称为分页符)
  • 注释:一种自由格式的文本,未经编译器处理,而是按原样转换为字节码,因此程序员在编写代码时使用注释向代码中添加人类可读的解释。注释可以包含空格,但不能识别为输入元素;它仅作为注释的一部分进行处理。我们将描述注释的语法规则,并在注释部分展示一些示例。
  • 令牌:可以是以下之一:
    • 标识符:将在标识符和变量部分中描述。
    • 关键字:将在保留和限制关键字部分描述。
    • 分离器:将在分离器章节中描述。
    • 文字:将在原语类型和文字部分描述。某些文本可以包含空格,但不能将其识别为输入元素;空白仅作为文本的一部分进行处理。
    • 运算符:将在第 9 章运算符、表达式和语句**中描述。

输入元素用于组成更复杂的元素,包括类型。一些关键字用于表示类型,我们也将在本章中讨论它们。

类型

Java 是一种强类型语言,这意味着任何变量声明都必须包含其类型。类型限制变量可以保存的值以及该值的传递方式。

Java 中的所有类型分为两类:

  • 原语类型:在原语类型和文字部分中描述
  • 参考类型:在参考类型和字符串章节中描述

一些参考类型需要更多的注意,或者是因为它们的复杂性,或者是为了避免将来的混淆而必须解释的其他细节:

  • 阵列:在阵列章节中描述
  • 字符串(大写的第一个字符表示它是一个类的名称):在引用类型和字符串部分中描述
  • 枚举类型:在枚举类型章节中描述

评论

Java 规范提供了有关注释的以下信息:

“有两种注释: /文本/ 传统注释:从 ASCII 字符/到 ASCII 字符/的所有文本都被忽略(如在 C 和 C++中)。 //文本 行尾注释:从 ASCII 字符//到行尾的所有文本都被忽略(如在 C++中)。”

下面是我们已经编写的SimpleMath类中的注释示例:

public class SimpleMath {
  /*
    This method just multiplies any integer by 2
    and returns the result
  */
  public int multiplyByTwo(int i){        
    //Should we check if i is bigger than 1/2 of Integer.MAX_VALUE ?
    return i * 2; // The magic happens here
  }
}

注释不会以任何方式影响代码。它们只是程序员的笔记。另外,不要将它们与 JavaDoc 或其他文档生成系统混淆。

标识符和变量

标识符和变量是 Java 最常用的元素之一。它们紧密耦合,因为每个变量都有一个名称,而变量的名称是一个标识符。

标识符

标识符是 Java 标记列表中的第一个。它是一系列符号,每个符号可以是字母、美元符号$、下划线、_或任何数字 0-9。有关限制如下:

  • 标识符的第一个符号不能是数字
  • 单个符号标识符不能是下划线_
  • 标识符不能与关键字具有相同的拼写(请参见保留和限制关键字部分)
  • 标识符不能是布尔文本truefalse
  • 标识符不能拼写为特殊类型null

如果违反上述任何限制,编译器将生成错误。

实际上,用于标识符的字母通常取自英文字母表——小写或大写。但也可以使用其他字母表。您可以在 Java 规范(第 3.8 节)的标识符中找到字母的正式定义 https://docs.oracle.com/javase/specs )。以下是该部分的示例列表:

  • i3
  • αρετη
  • String
  • MAX_VALUE
  • isLetterOrDigit

为了展示各种可能性,我们可以再添加两个合法标识符示例:

  • $
  • _1

变量

变量是一个存储位置,Java 规范将其放在变量部分。它具有名称(标识符)和指定的类型。变量是指存储值的内存。

Java 规范规定了八种变量:

  • 类变量:无需创建对象即可使用的静态类成员
  • 实例变量:只能通过对象使用的非静态类成员
  • 阵列组件:一个阵列元素(参见阵列部分)
  • 方法参数:传递给方法的参数
  • 构造函数参数:创建对象时传递给构造函数的参数
  • Lambda 参数:传递给 Lambda 表达式的参数。我们将在第 17 章Lambda 表达式和函数式编程中讨论
  • 异常参数:捕获异常时创建,我们将在第 10 章控制流语句中讨论
  • 局部变量:方法内部声明的变量

从实际角度来看,所有八种变量可概括如下:

  • 类成员,静态或非静态
  • 数组成员(也称为组件或元素)
  • 方法、构造函数或 lambda 表达式的参数
  • catch 块的异常参数
  • 一种常规的局部代码变量,最常见的一种

大多数时候,当程序员谈论变量时,他们指的是最后一种变量。它可以引用类成员、类实例、参数、异常对象或编写代码所需的任何其他值。

变量声明、定义和初始化

让我们先看看例子。让我们假设这三行代码是连续的:

int x;  //declartion of variable x
x = 1;  //initialization of variable x
x = 2;  //assignment of variable x 

从前面的示例可以猜到,变量初始化是将第一个(初始)值分配给变量。所有后续分配都不能称为初始化。

在初始化之前,无法使用局部变量:

int x;
int result = x * 2;  //generates compilation error

前面代码的第二行将生成编译错误。如果变量是类的成员(静态或非静态)或数组的组件,并且没有显式初始化,则会根据变量的类型为其分配一个默认值(请参见基本类型和文字引用类型和字符串部分)。

声明将创建一个新变量。它包括变量类型和名称(标识符)。声明是 Java 规范第 6.1 节(中使用的技术术语 https://docs.oracle.com/javase/specs )。但是有些程序员使用 Word 定义作为声明的同义词,因为在 java 中不存在的语句类型中,单词定义被用在一些其他编程语言(例如 C++和 C++)中。所以,请注意这一点,并假设当您在这里定义应用于 Java 时,它们意味着声明。

大多数情况下,在编写 Java 代码时,程序员将声明和初始化语句组合在一起。例如,int类型的变量可以声明并初始化为保存整数1,如下所示:

int $ = 1;
int _1 = 1;
int i3 = 1;
int αρετη = 1;
int String = 1;
int MAX_VALUE = 1;
int isLetterOrDigit = 1;

相同的标识符可用于声明和初始化String类型的变量以保存abs

String $ = "abc";
String _1 = "abc";
String i3 = "abc";
String αρετη = "abc";
String String = "abc";
String MAX_VALUE = "abc";
String isLetterOrDigit = "abc";

您可能已经注意到,在前面的示例中,我们使用了来自标识符部分示例的标识符。

最终变量(常数)

最后一个变量是一个一旦初始化就不能分配给另一个值的变量。由final关键字表示:

void someMethod(){
  final int x = 1;
  x = 2; //generates compilation error
  //some other code
}

尽管如此,以下代码仍可以正常工作:

void someMethod(){
  final int x;
  //Any code that does not use variable x can be added here
  x = 2;
  //some other code 
}

前面的代码不会生成编译错误,因为局部变量没有在声明语句中自动初始化为默认值。如果未显式初始化变量,则仅将类、实例变量或数组组件初始化为默认值(请参见基元类型和文字引用类型和字符串部分)。

当最后一个变量引用一个对象时,不能将其分配给另一个对象,但分配的对象的状态可以随时更改(参见引用类型和字符串部分)。这同样适用于引用数组的变量,因为数组是一个对象(请参见数组部分)。

因为最后一个变量不能更改,所以它是一个常量。如果它具有基元类型或String类型,则称为常量变量。但是 Java 程序员通常将常量一词应用于类级的最终静态变量,并将局部最终变量称为 final 变量。按照约定,类级常量的标识符用大写字母书写。以下是几个例子:

static final String FEBRUARY = "February";
static final int DAYS_IN_DECEMBER = 31;

这些常量看起来与以下常量非常相似:

Month.FEBRUARY;
TimeUnit.DAYS;
DayOfWeek.FRIDAY;

但是前面的常量是在一种特殊的类中定义的,称为enum,尽管出于所有实际目的,所有常量的行为都是相似的,即它们不能更改。我们只需要检查常量的类型,就可以知道它的类(类型)提供了什么方法。

保留和限制关键字

关键字是输入类型部分中列出的第二个 Java 令牌。我们已经看到了几个 Java 关键字-abstractclassfinalimplementsintinterfacenewpackageprivatepublicreturnstaticvoid。现在我们将展示保留关键字的完整列表。这些关键字不能用作标识符。

保留关键字

以下是 Java 9 中所有 49 个关键字的列表:

| 摘要 | 班 | 最终的 | 工具 | int | | 界面 | 刚出现的 | 包裹 | 私有的 | 平民的 | | 回来 | 静止的 | 无效的 | 如果 | 这 | | 打破 | 双重的 | 违约 | 受保护的 | 投 | | 字节 | 其他的 | 进口 | 同步的 | 投掷 | | 案例 | 枚举 | 运算符 | 布尔值 | 转瞬即逝的 | | 接住 | 延伸 | 转换 | 短的 | 尝试 | | 烧焦 | 对于 | 明确肯定 | 做 | 最后 | | 持续 | 浮动 | 长的 | strictfp | 不稳定的 | | 出生地的 | 超级的 | 虽然 | _(下划线) | |

关键字用于不同的 Java 元素和语句,不能用作标识符。gotoconst_(下划线)关键字尚未用作关键字,但它们可能会在未来的 Java 版本中使用。目前,它们只是包含在保留关键字列表中,以防止它们被用作标识符。但它们可以是其他字符中标识符的一部分,例如:

int _ = 3; //Error, underscore is a reserved keyword
int __ = 3; //More than 1 underscore as an identifier is OK
int _1 = 3;
int y_ = 3;
int goto_x = 3;
int const1 = 3;

truefalse单词看起来像关键字,不能用作标识符,但实际上它们不是 Java 关键字。它们是布尔文字(值)。我们将在基本类型和文字部分中定义文字。

还有一个词看起来像关键字,但实际上是一种特殊类型-null(参见参考类型和字符串部分)。它也不能用作标识符。

限定关键字

有十个词被称为限制关键词:openmodulerequirestransitiveexportsopenstousesprovideswith。它们之所以被称为受限,是因为它们不能作为模块声明上下文中的标识符,本书将不讨论这一点。在所有其他地方,可以将它们用作标识符。以下是此类用法的一个示例:

int to = 1;
int open = 1;
int uses = 1;
int with = 1;
int opens =1;
int module = 1;
int exports =1;
int provides = 1;
int requires = 1;
int transitive = 1;

但是,最好不要在任何地方使用它们作为标识符。命名变量还有很多其他方法。

分离器

分隔符是输入类型部分中列出的第三个 Java 标记。以下是全部 12 个,没有特定顺序:

;  { }  ( )  [ ]  ,  .  ...  ::  @

分号“;”

现在,您已经非常熟悉分隔符;(分号)的用法。它在 Java 中的唯一任务是终止语句:

int i;  //declaration statement
i = 2;  //assignment statement
if(i == 3){    //flow control statement called if-statement
  //do something
}
for(int i = 0; i < 10; i++){  
  //do something with each value of i
}

大括号“{}”

你已经在课堂上看到了分隔符{}(大括号):

class SomeClass {
  //class body with code
}

您还看到了方法体周围的大括号:

void someMethod(int i){
  //...
  if(i == 2){
    //block of code
  } else {
    //another block of code
  }
  ...
}

大括号还用于表示控制流语句中的代码块(参见第 10 章控制流语句

void someMethod(int i){
  //...
  if(i == 2){
    //block of code
  } else {
    //another block of code
  }
  ...
}

用于初始化数组(参见数组部分):

int[] myArray = {2,3,5};

还有一些其他很少使用的构造使用了大括号。

括号“()”

您还看到了使用分隔符()(括号)在方法定义和方法调用中保留方法参数列表:

void someMethod(int i) {
  //...
  String s = anotherMethod();
  //...
}

它们也用于控制流报表(参见第 10 章控制流报表

if(i == 2){
  //...
}

在类型转换过程中(参见基本类型和文字部分),它们被放置在类型周围:

long v = 23;
int i = (int)v;

关于设置执行的优先级(参见第 9 章运算符、表达式和语句),您应该从基础代数开始熟悉:

x = (y + z) * (a + b).

括号“[]”

分隔符[](括号)用于阵列声明(见阵列部分):

int[] a = new int[23];

逗号“,”

逗号,用于分隔方法参数,括号中列出:

void someMethod(int i, String s, int j) {
  //...
  String s = anotherMethod(5, 6.1, "another param");
  //...
}

逗号也可用于分隔声明语句中相同类型的变量:

int i, j = 2; k;

在前面的示例中,所有三个变量ijk都被声明为int类型,但只有变量j被初始化为2

在循环语句中使用逗号与声明多个变量的目的相同(参见第 10 章控制流语句

for (int i = 0; i < 10; i++){
   //...
} 

句号”

分隔符.(句号)用于分隔包名的各个部分,如您在com.packt.javapath示例中所看到的。

您还看到了如何使用句点来分隔对象引用和该对象的方法:

int result = simpleMath.multiplyByTwo(i);

类似地,如果simpleMath对象具有a的公共属性,则可以将其称为simpleMath.a

省略号“…”

分隔符...(省略号)仅用于 varargs:

int someMethod(int i, String s, int... k){
  //k is an array with elements k[0], k[1], ...
}

可以通过以下任一方式调用前面的方法:

someMethod(42, "abc");          //array k = null
someMethod(42, "abc", 42, 43);  //k[0] = 42, k[1] = 43
int[] k = new int[2];
k[0] = 42;
k[1] = 43;
someMethod(42, "abc", k);       //k[0] = 42, k[1] = 43

第 2 章Java 语言基础中,在谈到main()方法时,我们解释了 Java 中varargs(变量参数)的概念。

冒号“:”

分隔符::(冒号)用于 lambda 表达式中的方法引用(参见第 17 章LambdAE表达式和函数编程

List<String> list = List.of("1", "32", "765");
list.stream().mapToInt(Integer::valueOf).sum();

在符号“@”处

分隔符@(符号处)用于表示注释:

@Override
int someMethod(String s){
  //...
}

当我们在第 4 章您的第一个 Java 项目中创建单元测试时,您已经看到了几个注释示例。Java 标准库中有几个预定义的注释(@Deprecated@Override@FunctionalInterface等等)。我们将在第 17 章中使用其中一个(@FunctionalInterfaceLambdAE表达式和函数编程

注释是元数据。它们描述类、字段和方法,但它们本身不被执行。Java 编译器和 JVM 读取它们,并根据注释以某种方式处理所描述的类、字段或方法。例如,在第 4 章您的第一个 Java 项目中,您看到了我们如何使用@Test注释。将它添加到公共非静态方法之前会告诉 JVM 它是一个必须运行的测试方法。因此,如果执行这个类,JVM 将只运行这个方法。

或者,如果在方法前面使用@Override注释,编译器将检查该方法是否实际重写父类中的方法。如果在任何类父类中都找不到非私有非静态类的匹配签名,编译器将引发错误。

也可以创建新的自定义注释(JUnit 框架正是这样做的),但本主题不在本书的范围之内。

基本类型和文字

Java 只有两种变量类型:基元类型和引用类型。基元类型定义变量可以保存的值的类型以及该值的大小。我们将在本节中讨论基本类型。

引用类型只允许我们为变量指定一种值——对存储对象的内存区域的引用。我们将在下一节中讨论引用类型,引用类型和字符串

基本类型可分为两组:布尔类型和数值类型。数字类型组可以进一步分为整数类型(byteshortintlongchar)和浮点类型(浮点和双精度)。

每个基元类型由相应的保留关键字定义,列在保留和限制关键字部分。

布尔型

布尔类型允许变量具有两个值之一:truefalse。正如我们在保留关键字一节中提到的,这些值是布尔文本,这意味着它们是直接表示自身的值,没有变量。我们将在原语类型文字一节中进一步讨论文字。

以下是一个b变量声明和初始化值true的示例:

boolean b = true;

下面是使用表达式将true值分配给b布尔变量的另一个示例:

 int x = 1, y = 1;
 boolean b = 2 == ( x + y );

在前面的示例中,在第一行中,声明了int原语类型的两个变量xy,并为每个变量分配了一个值1。在第二行中,声明了一个布尔变量,并为其赋值了2 == ( x + y )表达式的结果。括号按如下方式设置执行的优先级:

  • 计算分配给xy变量的值之和
  • 使用==布尔运算符将结果与2进行比较

我们将在第 9 章运算符、表达式和语句中学习运算符和表达式。

控制流语句中使用了布尔变量,我们将在第 10 章控制流语句中看到许多使用它们的示例。

整型

Java 整数类型的值占用不同的内存量:

  • 字节:8 位
  • 字符:16 位
  • 短:16 位
  • int:32 位
  • 长:64 位

char外,所有符号均为有符号整数。符号值(0表示负“【T2]”和1表示正“【T4]”)占据该值二进制表示的第一位。这就是为什么有符号整数只能作为正数保存无符号整数值的一半。但它允许有符号整数包含负数,而无符号整数不能这样做。例如,在byte类型(8 位)的情况下,如果它是一个无符号整数,它可以保存的值的范围将是 0 到 255(包括 0 和 255),因为 2 到 8 的幂是 256。但是,正如我们所说,byte类型是一个有符号整数,这意味着它可以保存的值的范围是-128 到 127(包括-128、127 和 0)。

char类型的情况下,它可以保存 0 到 65535(包括 0 和 65535)之间的值,因为它是一个无符号整数。这个整数(称为代码点)标识 Unicode 表(中的一条记录 https://en.wikipedia.org/wiki/List_of_Unicode_characters 。每个 Unicode 表记录都有以下列:

  • 代码点:十进制值–Unicode 记录的数字表示
  • Unicode 转义:前缀为\u的四位数字
  • 可打印符号:Unicode 记录的图形表示(不适用于控制代码)
  • **描述:**一种人类可读的符号描述

以下是 Unicode 表中的五条记录:

| 代码点 | Unicode 转义 | 可打印符号 | 说明 | | 8. | \u0008 | | 退格 | | 10 | \u000A | | 线路馈电 | | 36 | \u0024 | $ | 美元符号 | | 51 | \u0033 | 3 | 第三位 | | 97 | \u0061 | a | 拉丁文小写字母 A |

前两个是 Unicode 的示例,它表示不可打印的控制代码。控制代码用于向设备(例如显示器或打印机)发送命令。Unicode 集合中只有 66 个这样的代码。它们的代码点从 0 到 32(含 0 到 32),从 127 到 159(含 127)。65535 个 Unicode 记录的其余部分有一个可打印的符号,即记录所代表的字符。

char类型有趣(且经常令人困惑)的方面是,Unicode 转义码和代码点可以互换使用,除非char类型的变量涉及算术运算。在这种情况下,使用代码点的值。为了演示它,让我们看一下以下代码片段(在注释中,我们捕获了输出):

char a = '3';
System.out.println(a);         //  3
char b = '$';
System.out.println(b);         //  $
System.out.println(a + b);     //  87
System.out.println(a + 2);     //  53
a = 36;    
System.out.println(a);         //  $ 

如您所见,char类型的变量ab表示3$符号,只要它们不涉及算术运算,就显示为这些符号。否则,仅使用代码点值。

从五条 Unicode 记录中可以看到,3字符的代码点值为 51,$字符的代码点值为 36。这就是为什么添加ab会产生 87,而将2添加到a会产生 53。

在示例代码的最后一行中,我们为char类型的a变量指定了一个十进制值 36。这意味着我们已经指示 JVM 将代码点为 36 的字符(即$字符)分配给a变量。

这就是为什么char类型被包括在 Java 的整数类型组中的原因——因为它在算术运算中充当数字类型。

每个基元类型可以容纳的值范围如下:

  • byte:从-128 到 127,包括
  • short:从-32768 到 32767,包括
  • int:从-2.147.483.648 到 2.147.483.647,含-2.147.483.647
  • long:从-9223372036854775808 到 9223372036854775807,包括
  • char:从“\u0000”到“\uffff”,即从 0 到 65535,包括

您可以随时使用每个原语类型的对应包装类访问每个类型的最大值和最小值(我们将在第 9 章运算符、表达式和语句中更详细地讨论包装类)。下面是一种方法(在注释中,我们展示了输出):

byte b = Byte.MIN_VALUE;
System.out.println(b);     //  -127
b = Byte.MAX_VALUE;
System.out.println(b);     //   128

short s = Short.MIN_VALUE;
System.out.println(s);      // -32768 
s = Short.MAX_VALUE;
System.out.println(s);      //  32767

int i = Integer.MIN_VALUE;
System.out.println(i);      // -2147483648
i = Integer.MAX_VALUE;
System.out.println(i);      //  2147483647

long l = Long.MIN_VALUE;
System.out.println(l);      // -9223372036854775808
l = Long.MAX_VALUE;
System.out.println(l);      //  9223372036854775807 

char c = Character.MIN_VALUE;
System.out.println((int)c); // 0
c = Character.MAX_VALUE;
System.out.println((int)c); // 65535

你可能已经注意到了(int)c结构。这被称为选角,类似于电影制作过程中演员被试扮演某个特定角色时发生的情况。任何基元数值类型的值都可以转换为另一个基元数值类型的值,前提是该值不大于目标类型的最大值。否则,将在程序执行期间生成错误(这种错误称为运行时错误)。我们将在第 9 章运算符、表达式和语句中进一步讨论基本数字类型之间的转换。

无法在数字类型和boolean类型之间进行转换。如果您尝试这样做,将生成编译时错误。

浮点类型

在 Java 规范中,浮点类型(floatdouble定义为:

“单精度 32 位和双精度 64 位格式的 IEEE 754 值。”

这意味着float类型占用 32 位,double类型占用 64 位。它们表示正数值和负数值,在点“.”后面有一个小数部分:1.2345.5610.-1.34。默认情况下,在 Java 中,带点的数值被认为是double类型。因此,以下赋值会导致编译错误:

float r = 23.4;

为了避免错误,必须通过在值处添加fF字符来指示该值必须被视为float类型,如下所示:

float r = 23.4f;
or
float r = 23.4F;

值本身(23.4f23.4F称为文本。我们将在原语类型文字一节中详细讨论它们。

最小值和最大值的计算方法与整数的计算方法相同。只需运行以下代码片段(在注释中,我们捕获了计算机上的输出):

System.out.println(Float.MIN_VALUE);  //1.4E-45
System.out.println(Float.MAX_VALUE);  //3.4028235E38
System.out.println(Double.MIN_VALUE); //4.9E-324 
System.out.println(Double.MAX_VALUE); //1.7976931348623157E308

负值的范围与正数的范围相同,只是在每个数字前面有减号-。零可以是0.0-0.0

基元类型的默认值

声明变量后,在使用变量之前,必须为其赋值。正如我们在变量声明、定义和初始化一节中提到的,局部变量必须被初始化或显式赋值。例如:

int x;
int y = 0;
x = 1;

但是,如果变量声明为类字段(静态)、实例(非静态)属性或数组组件,并且未显式初始化,则会使用默认值自动初始化该变量。值本身取决于变量的类型:

  • 对于byteshortintlong类型,默认值为零,0
  • 对于floatdouble类型,默认值为正零,0.0
  • 对于char类型,默认值为\u0000,点代码为零
  • 对于boolean类型,默认值为false

基元类型文字

文字是在输入类型部分中列出的第四个 Java 标记。它是一个值的表示。我们将在引用类型和字符串部分讨论引用类型的文字。现在我们只讨论基本类型的文字。

为了演示原语类型的文本,我们将在com.packt.javapath.ch05demo包中使用LiteralsDemo程序。您可以通过右键单击com.packt.javapath.ch05demo包,然后选择新的【类】,并键入LiteralsDemo类名来创建它,正如我们在第 4 章中所述,您的第一个 Java 项目中所述。

在原始类型中,boolean类型的文本是最简单的。它们只有两个:truefalse。我们可以通过运行以下代码进行演示:

public class LiteralsDemo {
  public static void main(String[] args){
    System.out.println("boolean literal true: " + true);
    System.out.println("boolean literal false: " + false);
  }
}

结果如下:

这些都是可能的布尔文字(值)。

现在,让我们转到一个更复杂的主题char类型的文本。它们可以是:

  • 单个字符,用单引号括起来
  • 用单引号括起来的转义序列

单引号或撇号是带有 Unicode 转义\u0027(十进制代码点 39)的字符。在整型部分中,我们已经看到了char型文字的几个例子,当我们在算术运算中将char型行为演示为数值型时。

以下是作为单个字符的char类型文字的一些其他示例:

System.out.println("char literal 'a': " + 'a');
System.out.println("char literal '%': " + '%');
System.out.println("char literal '\u03a9': " + '\u03a9'); //Omega
System.out.println("char literal '™': " + '™'); //Trade mark sign

如果运行上述代码,输出将如下所示:

现在,让我们来讨论第二种类型的char文本–转义序列。它是一种字符组合,其作用类似于控制代码。事实上,一些转义序列包括控制代码。以下是完整的列表:

  • \ b(退格 BS,Unicode 转义\u0008
  • \ t(水平制表符 HT,Unicode 转义\u0009
  • \ n(换行 LF,Unicode 转义\u000a
  • \ f(表单提要 FF,Unicode 转义\u000c
  • \ r(回车符 CR,Unicode 转义符\u000d
  • \ "(双引号),Unicode 转义\u0022
  • \ '(单引号’,Unicode 转义\u0027
  • \ \(反斜杠\,Unicode 转义\u005c

如您所见,转义序列总是以反斜杠(\开头)。让我们演示一些转义序列用法:

System.out.println("The line breaks \nhere");
System.out.println("The tab is\there");
System.out.println("\"");
System.out.println('\'');
System.out.println('\\');

如果运行上述代码,输出将如下所示:

如您所见,\n\t转义序列仅作为控制代码。它们本身不可打印,但会影响文本的显示。其他转义序列允许在不允许打印符号的上下文中打印符号。一行中的三个双引号或单引号将被限定为编译器错误,如果在没有反斜杠的情况下使用,还将被限定为一个反斜杠字符。

char类型的文本相比,浮点文本更简单。正如我们前面提到的,默认情况下,23.45文本具有double类型,如果您希望它是double类型,则无需在文本后面附加字母dD。但是你可以,如果你想更明确的话。另一方面,float类型的文字要求在末尾追加字母fF。让我们运行以下示例(注意我们如何使用\n转义序列在输出之前添加换行符):

System.out.println("\nfloat literal 123.456f: " + 123.456f);
System.out.println("double literal 123.456d: " + 123.456d);

结果如下:

浮点型文字也可以使用eE表示科学符号(参见https://en.wikipedia.org/wiki/Scientific_notation

System.out.println("\nfloat literal 1.234560e+02f: " + 1.234560e+02f);
System.out.println("double literal 1.234560e+02d: " + 1.234560e+02d);

上述代码的结果如下所示:

如您所见,值保持不变,无论是以十进制格式还是科学格式显示。

byteshortintlong整数类型的文字默认为int类型。以下分配不会导致任何编译错误:

byte b = 10;
short s = 10;
int i = 10;
long l = 10;

但以下每一行都会生成一个错误:

byte b = 128;
short s = 32768;
int i = 2147483648;
long l = 2147483648;

这是因为byte类型可以容纳的最大值是 127,short类型可以容纳的最大值是 32767,int类型可以容纳的最大值是 2147483647。请注意,尽管long类型的值可以高达 9223372036854775807,但最后一次赋值仍然失败,因为 2147483648 文本默认为int类型,但超过了int类型的最大值。要创建long类型的文字,必须在末尾添加字母lL,因此以下赋值工作正常:

long l = 2147483648L;

使用大写字母L是一种很好的做法,因为小写字母l很容易与数字1混淆。

前面的整型文字示例以十进制表示。但byteshortintlong类型的文字也可以用二进制(以 2 为基数,数字 0-1)、八进制(以 8 为基数,数字 0-7)和十六进制(以 16 为基数,数字 0-9 和 a-f)数制表示。下面是演示代码:

System.out.println("\nPrint literal 12:");
System.out.println("- bin 0b1100: "+ 0b1100);
System.out.println("- oct    014: "+ 014);
System.out.println("- dec     12: "+ 12);
System.out.println("- hex    0xc: "+ 0xc);

如果我们运行前面的代码,输出将是:

如您所见,二进制文字以0b(或0B开头,然后是二进制表示的值1211002^0*0 + 2^1*0 + 2^2*1 + 2^3 *1)。八进制文字以0开头,后跟八进制系统中表示的值12148^0*4 + 8^1*1)。十进制文字仅为12。十六进制文字以0x(或0X)开头,然后是十六进制表示的值 12—c(因为在十六进制中,af(或AF)的符号映射到十进制值1015

在文字前面添加减号(-)会使值为负数,无论使用哪种数字系统。下面是一个演示代码:

System.out.println("\nPrint literal -12:");
System.out.println("- bin 0b1100: "+ -0b1100);
System.out.println("- oct    014: "+ -014);
System.out.println("- dec     12: "+ -12);
System.out.println("- hex    0xc: "+ -0xc);

如果运行上述代码,输出将如下所示:

为了完成对基本类型文字的讨论,我们想提到在基本类型文字中下划线(_的可能用法。对于一个很长的数字,将其分组有助于快速估计其大小。以下是几个例子:

int speedOfLightMilesSec = 299_792_458; 
float meanRadiusOfEarthMiles = 3_958.8f;
long creditCardNumber = 1234_5678_9012_3456L;

让我们看看运行以下代码时会发生什么:

long anotherCreditCardNumber = 9876____5678_____9012____1234L;
System.out.println("\n" + anotherCreditCardNumber);

前面代码的输出如下所示:

如您所见,如果将一个或多个下划线放在数字文字中的数字之间,则会忽略这些下划线。任何其他位置的下划线都会导致编译错误。

引用类型和字符串

将对象指定给变量时,该变量保存对对象所在内存的引用。从实用的角度来看,这样的变量在代码中被处理,就好像它是它所表示的对象一样。此类变量的类型可以是类、接口、数组或特殊的null类型。如果分配了null,则对对象的引用将丢失,并且变量不表示任何对象。如果一个对象不再使用,JVM 会在名为垃圾收集的过程中将其从内存中删除。我们将在第 11 章JVM 进程和垃圾收集中描述此过程。

还有一种称为 type variable 的引用类型,用于声明泛型类、接口、方法或构造函数的类型参数。它属于 Java 泛型编程领域,超出了本书的范围。

所有对象,包括数组,都继承了第 2 章Java 语言基础中描述的java.lang.Object类的所有方法。

引用java.lang.String类(或只是String类)对象的变量也是引用类型。但是,在某些方面,String对象表现为一种基本类型,这有时可能会令人困惑。这就是为什么我们将在本章的一节专门介绍String课程的原因。

此外,枚举类型(也是一种引用类型)需要特别注意,我们在本节末尾的枚举类型小节中对此进行了描述。

类类型

使用相应的类名声明类类型的变量:

<Class name> variableName;

它可以通过分配给它null或名称用于声明的类的对象(实例)来初始化。如果该类有一个从中继承(扩展)的超类(也称为父类),则该超类的名称可用于变量声明。这是可能的,因为 Java 多态性,如第 2 章Java 语言基础所述。例如,如果一个SomeClass类扩展了SomeBaseClass,则以下两种声明和初始化都是可能的:

SomeBaseClass someBaseClass = new SomeBaseClass();
someBaseClass = new SomeClass();

而且,由于默认情况下每个类都扩展了java.lang.Object类,因此也可以进行以下声明和初始化:

Object someBaseClass = new SomeBaseClass();
someBaseClass = new SomeClass();

我们将在第 9 章运算符、表达式和语句中详细介绍如何将子类对象分配给基类引用。

接口类型

使用相应的接口名称声明接口类型的变量:

<Interface name> variableName;

它可以通过分配给它null或实现接口的类的对象(实例)来初始化。以下是一个例子:

interface SomeInterface{
  void someMethod();
}
interface SomeOtherInterface{
  void someOtherMethod();
}
class SomeClass implements SomeInterface {
  void someMethod(){
    ...
  }
} 
class SomeOtherClass implements SomeOtherInterface{
  void someOtherMethod(){
    ...
  }
}
SomeInterface someInterface = new SomeClass();
someInterface = new SomeOtherClass(); //not possible, error
someInterface.someMethod();         //works just fine
someInterface.someOtherMethod();   //not possible, error

我们将在第 9 章运算符、表达式和语句中详细介绍如何将子类型分配给基类型引用。

阵列

Java 中的数组是引用类型,并且扩展(继承)了Object类。数组包含的组件的类型与声明的数组类型相同,或者其值可以分配给数组类型的类型相同。组件的数量可能为零,在这种情况下,数组为空数组。

数组组件没有名称,由索引引用,索引为正整数或零。一个具有n分量的数组被称为具有长度 n。创建阵列对象后,其长度永远不会更改。

数组声明以类型名称和空括号[]开头:

byte[] bs;
long[][] ls;
Object[][] os;
SomeClass[][][] scs; 

括号对数表示数组的尺寸数(或嵌套深度)。

有两种方法可以创建和初始化数组:

  • 通过创建表达式,使用new关键字、类型名称以及每对括号中每个维度长度的括号;例如:
        byte[] bs = new byte[100];
        long[][] ls = new long [2][3];
        Object[][] os = new Object[3][2];
        SomeClass[][][] scs = new SomeClass[3][2][1]; 
  • 通过数组初始值设定项,使用逗号分隔的每个维度的值列表,并用大括号括起来,例如:
        int[][] is = { { 1, 2, 3 }, { 10, 20 }, { 3, 4, 5, 6 } };
        float[][] fs = { { 1.1f, 2.2f, 3 }, { 10, 20.f, 30.f } };
        Object[] oss = { new Object(), new SomeClass(), null, "abc" };
        SomeInterface[] sis = { new SomeClass(), null, new SomeClass() };

从这些示例中可以看出,多维数组可以包含不同长度的数组(“T0”)数组。此外,组件类型值可以不同于数组类型,只要该值可以分配给数组类型的变量(即float[][] fsObject[] isSomeInterface[] sis数组)。

因为数组是一个对象,所以每次创建数组时都会初始化其组件。让我们来考虑这个例子:

int[][] is = new int[2][3];
System.out.println("\nis.length=" + is.length);
System.out.println("is[0].length=" + is[0].length);
System.out.println("is[0][0].length=" + is[0][0]);
System.out.println("is[0][1].length=" + is[0][1]);
System.out.println("is[0][2].length=" + is[0][2]);
System.out.println("is[1].length=" + is[0].length);
System.out.println("is[1][0].length=" + is[1][0]);
System.out.println("is[1][1].length=" + is[1][1]);
System.out.println("is[1][2].length=" + is[1][2]);

如果我们运行前面的代码段,输出将如下所示:

可以在不初始化某些维度的情况下创建多维数组:

int[][] is = new int[2][];
System.out.println("\nis.length=" + is.length);
System.out.println("is[0]=" + is[0]);
System.out.println("is[1]=" + is[1]);

此代码运行的结果如下所示:

可以在以后添加缺少的维度:

int[][] is = new int[2][];
is[0] = new int[3];
is[1] = new int[3];

重要的一点是,必须先初始化维度,然后才能使用它。

引用类型的默认值

引用类型的默认值为null。这意味着,如果引用类型是静态类成员或实例字段,并且没有显式地分配初始值,那么它将自动初始化并分配值null。请注意,对于数组,这适用于数组本身及其引用类型组件。

引用类型文字

null文本表示引用类型变量没有赋值。让我们看一下以下代码片段:

SomeClass someClass = new SomeClass();
someClass.someMethod();
someClass = null;
someClass.someMethod(); // throws NullPointerException

第一条语句声明了someClass变量,并为其分配了对SomeClass类对象的引用。然后使用该类的引用调用该类的方法。下一行将null文本分配给someClass变量。它从变量中删除参考值。因此,当在下一行中,我们再次尝试调用相同的方法时,我们返回NullPointerException,只有当所使用的引用被分配null值时才会发生这种情况。

String类型也是一个引用类型。这意味着String变量的默认值为nullString类继承java.lang.Object类的所有方法,就像任何其他引用类型一样。

但在某些方面,String类的对象的行为就好像它们是原始类型一样。我们将讨论这样一种情况,String对象在中用作方法参数,并将引用类型值作为方法参数部分传递。我们现在将讨论String行为类似于原始类型的其他情况。

String类型的另一个特点是它看起来像一个基本类型,它是唯一一个比null具有更多文本的引用类型。类型String也可以包含零个或多个字符的文字,并用双引号括起来-"""$""abc""12-34"String文字的字符也可能包括转义序列。以下是几个例子:

System.out.println("\nFirst line.\nSecond line.");
System.out.println("Tab space\tin the line");
System.out.println("It is called a \"String literal\".");
System.out.println("Latin Capital Letter Y with diaeresis: \u0178");

如果执行前面的代码段,则输出如下:

但是,和char类型的文字不同,String文字的行为不像算术运算中的数字。String类型唯一适用的算术运算是加法,其行为类似于串联:

System.out.println("s1" + "s2");
String s1 = "s1";
System.out.println(s1 + "s2");
String s2 = "s1";
System.out.println(s1 + s2);

运行前面的代码,您将看到以下内容:

String的另一个特殊特征是String类型的对象是不可变的。

字符串不变性

在不更改引用的情况下,无法更改分配给变量的String类型值。JVM 作者决定这样做有几个原因:

  • 所有String文本都存储在相同的公共内存区域中,称为字符串池。在存储新的String文本之前,JVM 会检查是否已经存储了这样的文本。如果这样的对象已经存在,则不会创建新对象,对现有对象的引用将作为对新对象的引用返回。以下代码演示了这种情况:
        System.out.println("s1" == "s1");
        System.out.println("s1" == "s2");
        String s1 = "s1";
        System.out.println(s1 == "s1");
        System.out.println(s1 == "s2");
        String s2 = "s1";
        System.out.println(s1 == s2);

在前面的代码中,我们使用了==关系运算符,它用于比较基元类型的值和引用类型的引用。如果我们运行此代码,结果将如下所示:

您可以看到,如果两个文本具有相同的拼写,则文本的各种比较(直接或通过变量)一致地产生true,如果拼写不同,则产生false。这样,长String文本就不会被复制,内存也得到了更好的利用。

为了避免通过不同的方法同时修改同一个文本,每次尝试更改String文本时,都会创建一个带有更改的文本副本,而原始String文本保持不变。下面是演示它的代码:

        String s1 = "\nthe original string";
        String s2 = s1.concat(" has been changed"); 
        System.out.println(s2);
        System.out.println(s1);

String类的concat()方法将另一个String文本添加到s1的原始值,并将结果赋给s1变量。此代码的输出如下所示:

如您所见,分配给s1的原始文字没有改变。

  • 做出这样一个设计决策的另一个原因是安全性——JVM 作者心目中的最高优先级目标之一。String文本被广泛用作访问应用程序、数据库和服务器的用户名和密码。String值的不变性使其不易受到未经授权的修改。
  • 另一个原因是,在长String值的情况下,某些计算密集型过程(例如,Object父类中的hashCode()方法)可能会非常繁重。通过使String对象不可变,如果已经对具有相同拼写的值执行了此类计算,则可以避免此类计算。

这就是为什么所有修改String值的方法都返回String类型,这是对携带结果的新String对象的引用。前面代码中的concat()方法就是这种方法的典型例子。

String对象不是从文本创建的,而是使用String构造函数new String("some literal")创建的时,问题就变得有点复杂了。在这种情况下,String对象存储在存储所有类的所有对象的相同区域中,并且每次使用new关键字时,分配另一块内存(具有另一个引用)。下面是演示它的代码:

String s3 = new String("s");
String s4 = new String("s");
System.out.println(s3 == s4);

如果运行它,输出将如下所示:

如您所见,尽管拼写相同,但对象具有不同的内存引用。为避免混淆和仅根据拼写比较String对象,请始终使用String类的equals()方法。下面是演示其用法的代码:

System.out.println("s5".equals("s5"));  //true
System.out.println("s5".equals("s6"));  //false
String s5 = "s5";
System.out.println(s5.equals("s5"));   //true
System.out.println(s5.equals("s6"));   //false
String s6 = "s6";
System.out.println(s5.equals(s5));     //true
System.out.println(s5.equals(s6));     //false
String s7 = "s6";
System.out.println(s7.equals(s6));     //true
String s8 = new String("s6");
System.out.println(s8.equals(s7));     //true
String s9 = new String("s9");
System.out.println(s8.equals(s9));     //false

如果运行它,结果将是:

为了您的方便,我们将结果作为注释添加到前面的代码中。如您所见,String类的equals()方法仅根据值的拼写返回truefalse,因此,当拼写比较是您的目标时,请始终使用它。

顺便说一下,您可能还记得,equals()方法是在Object类中定义的,String类的父类。String类有自己的equals()方法,该方法在父类中使用相同的签名覆盖该方法,正如我们在第 2 章Java 语言基础中所演示的。String类的equals()方法的源代码如下:

public boolean equals(Object anObject) {
  if (this == anObject) {
    return true;
  }
  if (anObject instanceof String) {
    String aString = (String)anObject;
    if (coder() == aString.coder()) {
      return isLatin1() ? 
             StringLatin1.equals(value, aString.value)
            : StringUTF16.equals(value, aString.value);
    }
  }
  return false;
}

如您所见,它首先比较引用,如果它们指向同一对象,则返回true。但是,如果引用不同,它会比较值的拼写,这实际上发生在StringLatin1StringUTF16类的equals()方法中。

我们想让您了解的一点是,String类的equals()方法是通过首先执行引用的比较来优化的,只有在不成功的情况下,才会比较值本身。这意味着不需要比较代码中的引用。相反,对于String类型的对象比较,始终仅使用equals()方法。

至此,我们将进入本章将讨论的最后一种引用类型enum类型。

枚举类型

在描述enum类型之前,让我们先看看其中一个用例,以此作为拥有此类类型的动机。假设我们想要创建一个描述TheBlows族的类:

public class TheBlows {
  private String name, relation, hobby = "biking";
  private int age;
  public TheBlows(String name, String relation, int age) {
    this.name = name;
    this.relation = relation;
    this.age = age;
  }
  public String getName() { return name; } 
  public String getRelation() { return relation; }
  public int getAge() { return age; }
  public String getHobby() { return hobby; }
  public void setHobby(String hobby) { this.hobby = hobby; }
}

我们已将默认嗜好设置为biking,并将允许稍后对其进行更改,但在对象构造期间必须设置其他属性。那就好了,不过我们不想让这个家族的成员超过四名,因为我们非常了解TheBlows家族的所有成员。

为了施加这些限制,我们决定预先创建TheBlows类的所有可能对象,并将构造函数设置为私有:

public class TheBlows {
  public static TheBlows BILL = new TheBlows("Bill", "father", 42);
  public static TheBlows BECKY = new TheBlows("BECKY", "mother", 37);
  public static TheBlows BEE = new TheBlows("Bee", "daughter", 5);
  public static TheBlows BOB = new TheBlows("Bob", "son", 3);
  private String name, relation, hobby = "biking";
  private int age;
  private TheBlows(String name, String relation, int age) {
    this.name = name;
    this.relation = relation;
    this.age = age;
  }
  public String getName() { return name; }
  public String getRelation() { return relation; }
  public int getAge() { return age; }
  public String getHobby() { return hobby; }
  public void setHobby(String hobby) { this.hobby = hobby; }
}

现在只有TheBlows类的四个实例存在,不能创建该类的其他对象。让我们看看如果运行以下代码会发生什么:

System.out.println(TheBlows.BILL.getName());
System.out.println(TheBlows.BILL.getHobby());
TheBlows.BILL.setHobby("fishing");
System.out.println(TheBlows.BILL.getHobby());

我们将获得以下输出:

同样,我们可以创建具有三个家庭成员的TheJohns家庭:

public class TheJohns {
  public static TheJohns JOE = new TheJohns("Joe", "father", 42);
  public static TheJohns JOAN = new TheJohns("Joan", "mother", 37);
  public static TheJohns JILL = new TheJohns("Jill", "daughter", 5);
  private String name, relation, hobby = "joggling";
  private int age;
  private TheJohns(String name, String relation, int age) {
    this.name = name;
    this.relation = relation;
    this.age = age;
  }
  public String getName() { return name; }
  public String getRelation() { return relation; }
  public int getAge() { return age; }
  public String getHobby() { return hobby; }
  public void setHobby(String hobby) { this.hobby = hobby; }
}

在这样做的过程中,我们注意到这两个类有很多共同之处,并决定创建一个Family基类:

public class Family {
  private String name, relation, hobby;
  private int age;
  protected Family(String name, String relation, int age, String hobby) {
    this.name = name;
    this.relation = relation;
    this.age = age;
    this.hobby = hobby;
  }
  public String getName() { return name; }
  public String getRelation() { return relation; }
  public int getAge() { return age; }
  public String getHobby() { return hobby; }
  public void setHobby(String hobby) { this.hobby = hobby; }
}

现在,在扩展了Family类之后,可以对TheBlowsTheJohns类进行实质性的简化。下面是TheBlows课程现在的样子:

public class TheBlows extends Family {
  public static TheBlows BILL = new TheBlows("Bill", "father", 42);
  public static TheBlows BECKY = new TheBlows("Becky", "mother", 37);
  public static TheBlows BEE = new TheBlows("Bee", "daughter", 5);
  public static TheBlows BOB = new TheBlows("Bob", "son", 3);
  private TheBlows(String name, String relation, int age) {
    super(name, relation, age, "biking");
  }
}

这就是enum类型背后的思想,它允许创建具有固定数量命名实例的类。

enum引用类型类扩展了java.lang.Enum类。它定义了一组常量,每个常量都是它所属的enum类型的实例。这样一个集合的声明以enum关键字开始。以下是一个例子:

enum Season { SPRING, SUMMER, AUTUMN, WINTER }

所列项目SPRINGSUMMERAUTUMNWINTER中的每一项都是Season的一个实例。它们是Season类在应用程序中唯一存在的四个实例。无法创建Season类的其他实例。这就是创建enum类型的原因:它可以用于一个类的实例列表必须限制为固定集合的情况,例如可能的季节列表。

enum声明也可以用驼峰式书写:

enum Season { Spring, Summer, Autumn, Winter }

但是更经常使用全大写样式,因为正如我们前面提到的,Java 编程中静态 final 常量的标识符是按照约定以这种方式编写的,以便将它们与非常量变量区分开来。enum常量是静态的,隐式地是最终的。

让我们回顾一下Season类用法的一个示例。以下是一种根据季节打印不同消息的方法:

void enumDemo(Season season){
  if(season == Season.WINTER){
    System.out.println("Dress up warmer");
  } else {
    System.out.println("You can drees up lighter now");
  }
}

让我们看看如果我们运行以下两行会发生什么:

enumDemo(Season.WINTER);
enumDemo(Season.SUMMER);

结果如下:

您可能已经注意到,我们使用了一个==操作符来比较引用。这是因为enum实例(作为所有静态变量)在内存中唯一存在。而equals()方法(在java.lang.Enum父类中实现)也带来了相同的结果。让我们运行以下代码:

Season season = Season.WINTER;
System.out.println(Season.WINTER == season);
System.out.println(Season.WINTER.equals(season));

结果将是:

原因是java.lang.Enum类的equals()方法实现如下:

public final boolean equals(Object other) {
  return this == other;
}

如您所见,它对两个对象引用进行了完全相同的比较—this(引用当前对象的保留关键字)和对另一个对象的引用。如果您想知道为什么参数有Object类型,我们想提醒您,所有引用类型,包括enumString都扩展了java.lang.Object。他们含蓄地这样做。

java.lang.Enum的其他有用方法如下:

  • name():按声明时的拼写返回枚举常量的标识符。
  • ordinal():返回声明时对应于枚举常量位置的整数(列表中的第一个序号值为零)。
  • valueOf():按名称返回enum常量对象。
  • toString():默认返回与name()方法相同的值,但可以覆盖返回任何其他String值。
  • values():在java.lang.Enum类的文档中找不到的静态方法。在 Java 规范中,第 8.9.3 节(https://docs.oracle.com/javase/specs ),它被描述为隐式声明和 Java 教程(https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html 表示编译器在创建枚举时会自动添加一些特殊方法

其中有一个静态的values()方法,该方法返回一个数组,该数组包含enum的所有值,这些值按声明顺序排列。

让我们看一个使用它们的示例。下面是我们将用于演示的enum类:

enum Season {
  SPRING, SUMMER, AUTUMN, WINTER;
}

下面是使用它的代码:

System.out.println(Season.SPRING.name());
System.out.println(Season.SUMMER.ordinal());
System.out.println(Enum.valueOf(Season.class, "AUTUMN"));
System.out.println(Season.WINTER.name());

前面代码段的输出如下所示:

第一行是name()方法的输出。第二个是ordinal()方法的返回值:SUMMER常量是列表中的第二个,所以它的顺序值是 1。第三行是应用于valueOf()方法返回的AUTUMNenum常数的toString()方法的结果。最后是toString()方法应用于WINTER常数的结果。

equals()name()ordinal()方法在java.lang.Enum中声明为final,因此它们不能被重写并按原样使用。valueOf()方法是静态的,不与任何类实例关联,因此无法重写。我们唯一可以覆盖的方法是toString()方法:

enum Season {
  SPRING, SUMMER, AUTUMN, WINTER;
  public String toString() {
    return "The best season";
  }
}

如果我们再次运行前面的代码,结果如下:

现在,您可以看到,toString()方法为每个常量返回相同的结果。如有必要,可以为每个常量重写toString()方法。让我们看看这个版本的Season类:

enum Season2 {
  SPRING,
  SUMMER,
  AUTUMN,
  WINTER { public String toString() { return "Winter"; } };
  public String toString() {
    return "The best season";
  }
}

我们仅针对WINTER常量重写了toString()方法。如果再次运行相同的代码段,结果如下:

如您所见,toString()的旧版本用于除WINTER之外的所有常量。

也可以将任何属性(以及 getter 和 setter)添加到enum常量中,并将它们与相应的值关联。以下是一个例子:

enum Season {
  SPRING("Spring", "warmer than winter", 60),
  SUMMER("Summer", "the hottest season", 100),
  AUTUMN("Autumn", "colder than summer", 70),
  WINTER("Winter", "the coldest season", 40);

  private String feel, toString;
  private int averageTemperature;
  Season(String toString, String feel, int t) {
    this.feel = feel;
    this.toString = toString;
    this.averageTemperature = t;
  }
  public String getFeel(){ return this.feel; }
  public int getAverageTemperature(){
    return this.averageTemperature;
  }
  public String toString() { return this.toString; }
}

在前面的示例中,我们向Season类添加了三个属性:feeltoStringaverageTemperature。我们还创建了一个构造函数(一种用于指定对象状态初始值的特殊方法),它接受这三个属性并添加 getter 和返回这些属性值的toString()方法。然后,在每个常量后面的括号中,我们设置了创建该常量时要传递给构造函数的值。

下面是我们将要使用的演示方法:

void enumDemo(Season season){
  System.out.println(season + " is " + season.getFeel());
  System.out.println(season + " has average temperature around " 
                               + season.getAverageTemperature());
}

enumDemo()方法采用enum Season常量,构造并显示两个句子。让我们为每个季节运行前面的代码,如下所示:

enumDemo2(Season3.SPRING);
enumDemo2(Season3.SUMMER);
enumDemo2(Season3.AUTUMN);
enumDemo2(Season3.WINTER);

结果如下:

enum类是一个非常强大的工具,它允许我们简化代码并使其更好地防止运行时错误,因为所有可能的值都是可预测的,并且可以提前测试。例如,我们可以使用以下单元测试来测试SPRING常量吸气剂:

@DisplayName("Enum Season tests")
public class EnumSeasonTest {
  @Test
  @DisplayName("Test Spring getters")
  void multiplyByTwo(){
    assertEquals("Spring", Season.SPRING.toString());
    assertEquals("warmer than winter", Season.SPRING.getFeel());
    assertEquals(60, Season.SPRING.getAverageTemperature());
  }
}

诚然,getter 没有太多代码可以出错。但是,如果enum类有更复杂的方法,或者固定值列表来自一些应用程序需求文档,这样的测试将确保我们已经按照要求编写了代码。

在标准 Java 库中,有几个enum类。下面是这些类中的一些常量示例,它们可以为您提供关于存在的内容的提示:

Month.FEBRUARY;
TimeUnit.DAYS;
TimeUnit.MINUTES;
DayOfWeek.FRIDAY;
Color.GREEN;
Color.green;

因此,在创建您自己的enum之前,请尝试检查并查看标准库是否已经为类提供了所需的值。

将引用类型值作为方法参数传递

引用类型和基元类型之间值得特别讨论的一个重要区别是它们的值在方法中的使用方式。让我们通过示例来了解差异。首先,我们创建SomeClass类:

class SomeClass{
  private int count;
  public int getCount() {
    return count;
  }
  public void setCount(int count) {
      this.count = count;
    }
}

然后我们创建一个使用它的类:

public class ReferenceTypeDemo {
  public static void main(String[] args) {
    float f = 1.0f;
    SomeClass someClass = new SomeClass();
    System.out.println("\nBefore demoMethod(): f = " + f + 
                             ", count = " + someClass.getCount());
    demoMethod(f, someClass);
    System.out.println("After demoMethod(): f = " + f 
                           + ", count = " + someClass.getCount());
  }
  private static void demoMethod(float f, SomeClass someClass){
    //... some code can be here
    f = 42.0f;
    someClass.setCount(42);
    someClass = new SomeClass();
    someClass.setCount(1001);
  }
}

我们先看看里面。为了演示的目的,我们将其变得非常简单,但假设它做得更多,然后为f变量(参数)分配一个新值,并在SomeClass类的对象上设置一个新的计数值。然后该方法尝试用一个新值替换传入的引用,该值指向另一个计数值的新SomeClass对象。

main()方法中,我们用一些值声明并初始化fsomeClass变量,并将它们打印出来,然后将它们作为参数传递给demoMethod()方法,并再次打印相同变量的值。让我们运行main()方法,看到如下结果:

要理解差异,我们需要考虑以下两个事实:

  • 通过复制将值传递给方法
  • 引用类型的值是对引用对象所在内存的引用

这就是为什么在传入原语值(或String,正如我们已经解释过的,它是不可变的)时,会创建实际值的副本,因此不会影响原始值。

类似地,如果传入对对象的引用,则方法中的代码只能访问其副本,因此无法更改原始引用。这就是为什么我们试图更改原始引用值并使其引用另一个对象也没有成功的原因。

但是方法中的代码能够访问原始对象并使用引用值的副本更改其计数值,因为该值仍然指向原始对象所在的同一内存区域。这就是为什么方法中的代码能够执行原始对象的任何方法,包括那些更改对象状态(实例字段的值)的方法。

当对象状态作为参数传入时,这种变化称为副作用,有时在以下情况下使用:

  • 一个方法必须返回多个值,但不可能通过返回的构造来实现
  • 程序员不够熟练
  • 第三方库或框架利用副作用作为返回结果的主要机制

但最佳实践和设计原则(本例中的单一责任原则,我们将在第 8 章面向对象设计(OOD)原则)中讨论)指导程序员尽可能避免副作用,因为副作用通常会导致不太可读(对于人而言)难以识别和修复的代码和微妙的运行时效果。

必须区分副作用和称为委托模式(的代码设计模式 https://en.wikipedia.org/wiki/Delegation_pattern ),当传入对象上调用的方法为无状态时。我们将在第 8 章面向对象设计(OOD)原则中讨论设计模式。

类似地,当数组作为参数传入时,可能会产生副作用。下面是演示它的代码:

public class ReferenceTypeDemo {
  public static void main(String[] args) {
    int[] someArray = {1, 2, 3};
    System.out.println("\nBefore demoMethod(): someArray[0] = " 
                                               + someArray[0]);
    demoMethod(someArray);
    System.out.println("After demoMethod(): someArray[0] = " 
                                                + someArray[0]);
  }
  private static void demoMethod(int[] someArray){
    someArray[0] = 42;
    someArray = new int[3];
    someArray[0] = 43;
  }
}

上述代码执行的结果如下所示:

您可以看到,尽管在方法内部,我们能够为传入的变量分配一个新数组,但赋值43只影响新创建的数组,而对原始数组没有影响。但是,使用传入的引用值副本更改数组组件是可能的,因为副本仍然指向相同的原始数组。

并且,为了结束关于引用类型作为方法参数及其可能的副作用的讨论,我们想证明,String类型参数由于String值的不变性,在作为参数传入时,其行为类似于原始类型。以下是演示代码:

public class ReferenceTypeDemo {
  public static void main(String[] args) {
    String someString = "Some string";
    System.out.println("\nBefore demoMethod(): string = " 
                                              + someString);
    demoMethod(someString);
    System.out.println("After demoMethod(): string = " 
                                              + someString);
  }
  private static void demoMethod(String someString){
    someString = "Some other string";
  }
}

前面的代码产生以下结果:

方法中的代码无法更改原始参数值。这样做的原因与原语类型不同,参数值在传递到方法之前是复制的。在本例中,副本仍然指向相同的原始String对象。实际原因是更改一个String值不会更改该值,但会使用更改的结果创建另一个String对象。这就是我们在字符串类型和文本部分中描述的String值不变性机制。对这个新的(更改的)String对象的引用分配给传入的引用值的副本,对仍然指向原始字符串对象的原始引用值没有影响。

至此,我们结束了关于 Java 引用类型和字符串的讨论。

练习–变量声明和初始化

以下哪项陈述是正确的:

  1. int x='x';
  2. int x1=“x”;
  3. char x2=“x”;
  4. 字符 x4=1;
  5. 字符串 x3=1;
  6. 5 月=5 个月;
  7. 月=月。四月;

答复

1, 4, 7

总结

本章为讨论更复杂的 java 语言构造提供了基础。对于 Java 编程来说,了解 Java 元素(如标识符、变量、文本、关键字、分隔符、注释和类型)以及原语和引用是必不可少的。您还有机会了解了一些如果理解不正确可能会引起混淆的方面,例如字符串类型的不变性以及引用类型用作方法参数时可能产生的副作用。还详细解释了数组和enum类型,使读者能够使用这些强大的结构并提高代码的质量。

在下一章中,读者将介绍 Java 编程最常见的术语和编码解决方案——应用程序编程接口API)、对象工厂、方法重写、隐藏和重载。然后,关于软件系统设计和聚合(相对于继承)优势的讨论将使读者进入最佳设计实践领域。Java 数据结构的概述将在本章结束,为读者提供实用的编程建议和建议。