Java核心技术速学版(第3版)

978-7-115-62609-7
作者: [美]凯·S.霍斯特曼(Cay S. Horstmann)
译者:
编辑: 陈灿然
分类: Java

图书目录:

详情

本书是经典Java开发基础书《Java核心技术》的速学版本。 本书首先介绍了Java语言的基础知识;其次介绍了流、输入和输出处理、并发、注解、日期和时间 API、国际化、编译和脚本、Java 平台模块系统等高级特性。 本书不仅可以让读者深入了解Java编程的基础知识和核心概念,还可以帮助读者掌握Java应用程序开发所需的基本技能。 本书涵盖了Java 17中更新的内容,提供了许多实用的示例代码,还给出了基于作者实际经验的提示、注意和警告。

图书摘要

版权信息

书名:Java核心技术速学版(第3版)

ISBN:978-7-115-62609-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

版  权

著    [美]凯·S.霍斯特曼(Cay S. Horstmann)

译    曹良亮

责任编辑 蒋 艳

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315

内容提要

本书是经典Java开发基础书《Java核心技术》的速学版本。本书首先介绍了Java语言的基础知识,包含接口、Lambda表达式、继承、反射、异常处理、断言、日志、泛型编程、容器等关键概念;其次介绍了流、输入和输出处理、并发、注解、日期和时间API、国际化、编译和脚本、Java平台模块系统等高级特性。本书不仅可以让读者深入了解Java编程的基础知识和核心概念,还可以帮助读者掌握Java应用程序开发所需的基本技能。本书涵盖了Java 17中更新的内容,提供了许多实用的示例代码,还给出了基于作者实际经验的提示、注意和警告。

本书可供希望深入掌握Java应用的初学者使用,也适合打算将Java应用到实际项目中的编程人员使用。

致  谢

首先,我要一如既往地感谢本书的编辑格雷格·多恩奇(Greg Doench),他热情地支持我完成了这本对Java进行全新介绍的新书。德米特里·基尔萨诺夫(Dmitry Kirsanov)和阿林娜·基尔萨诺娃(Alina Kirsanova)也再次以惊人的速度和严谨的态度将XHTML原稿变成了一本引人入胜的书。特别感谢所有版本的优秀评审团队,他们发现了许多错误并提出了改进建议。他们是安德烈斯·阿尔米雷(Andres Almiray)、盖尔·安德松(Gail Anderson)、保罗·安德松(Paul Anderson)、马库斯·比尔(Marcus Biel)、布里安·戈茨(Brian Goetz)、马克·劳伦斯(Mark Lawrence)、道格·利(Doug Lea)、西蒙·里特尔(Simon Ritter)、柴田佳樹(Yoshiki Shibata)和克里斯蒂安·乌伦布姆(Christian Ullenboom)。

凯·S. 霍斯特曼(Cay S. Horstmann)

2022年8月于德国柏林

作者简介

凯·S. 霍斯特曼(Cay S. Horstmann)是JavaScript for the ImpatientScala for the Impatient的作者,是Core Java, Volumes I and II, Twelfth Edition的主要作者,他还为专业编程人员和计算机科学专业的学生撰写了十多本书。他是美国圣何塞州立大学计算机科学专业的荣誉退休教授,也是一名Java Champion。

前  言

自1996年首次发布以来,Java语言一直在不断地改进。经典著作《Java核心技术》(Core Java)一书不仅详细介绍了Java的语言特性和所有核心库,还介绍了各个版本之间的大量更新之处。因此该书体量庞大,共分上下两卷,超过2000页。如果你只是想高效地使用现代Java,那么本书就是一个更快、更容易学习Java语言和核心库的途径。本书不回顾Java语言的发展历史,也不纠缠于过去版本的特点,只展示当前Java语言的优秀内容,以便你可以更快地把相关知识应用到实际工作中。

与之前的“Impatient”系列书籍类似,本书将会很快切入主题,向你展示解决编程问题所需的核心知识,而不会总是教条地告诉你一种范式如何优于另一种范式。本书还将相关的信息按照知识点进行碎片化处理,然后再把它们重新组织起来,这样更便于你在需要时快速检索。

假如你已经精通其他的编程语言,如C++、JavaScript、Swift、PHP或Ruby,那么在本书中,你将学习如何成为一名称职的Java编程人员。本书涵盖了目前开发人员需要了解的关于Java语言的方方面面,其中包括Lambda表达式和流这种强大的概念,以及记录类(record class)和封闭类(sealed class)等现代构造。

使用Java的一个关键原因是处理并发编程。由于Java库中提供了并行算法和线程安全的数据结构,因此应用编程人员处理并发编程的方式已经完全改变了。本书也提供了新的内容,向你展示如何使用强大的库特性,而不是使用容易出错的底层构造。

传统上,很多有关Java的书侧重于用户界面编程,但是现在,已经很少有开发人员在台式计算机上制作用户界面了。如果你打算将Java用于服务器端编程或Android编程,那么你将能够更加有效地使用本书,而不会被桌面GUI的代码干扰。

最后,本书是专门为应用编程人员编写的,而不是为大学的Java语言课程或者系统向导编写的,本书基本涵盖了应用编程人员在实践中需要解决的问题,例如记录日志和处理文件,但你将不会学习到如何手动实现链表或如何编写Web服务器。

衷心希望你能喜欢本书以这样的方式快速地介绍现代Java,并希望它能够让你的Java开发工作富有成效,且充满乐趣。

如果你发现本书有错误或对本书有改进建议,请访问异步社区,并前往勘误页面提交你的意见和建议。同时也请务必访问异步社区,下载本书配套的可运行的示例代码。

资源与支持

资源获取

本书提供思维导图等资源,要获得以上资源,您可以扫描下方二维码,根据指引领取。

提交勘误

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区(https://www.epubit.com/),按书名搜索,进入本书页面,点击“发表勘误”,输入勘误信息,点击“提交勘误”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

与我们联系

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。

如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

关于异步社区和异步图书

异步社区”(www.epubit.com)是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作者与读者在线交流互动,以及传统出版与数字出版的融合发展。

异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社在计算机图书领域30余年的发展与积淀。异步图书面向IT行业以及各行业使用IT技术的用户。

第1章 基本编程结构

在本章中,你将学习Java语言的基本数据类型和控制结构方面的知识。假设你熟悉其他的程序设计语言,已是一名经验丰富的编程人员。也许你已经掌握了一些关于变量、循环、函数调用和数组的概念,但是你熟悉的那些概念可能和Java语言相比,有一些语法方面的差异。本章将帮助你快速了解Java语言的基础知识。本书也会为你提供一些Java API中处理常见数据类型的非常有用的技巧。

本章重点如下:

1.在Java中,所有方法都在类中声明。当你调用一个非静态方法时,需要通过该方法所属类的对象来进行调用。

2.静态方法的调用不需要对象。程序从静态的main方法开始执行。

3. Java有8种基本数据类型:4种有符号整数类型、两种浮点类型,以及char类型和boolean(布尔)类型。

4.Java的运算符和控制结构与C或JavaScript非常相似。

5.共有4种形式的switch,分别是带有和不带有直通式(fall-through)的表达式和语句。

6.Math类提供通用的数学函数。

7.String对象是字符序列,更准确地说,它们是UTF-16 编码中的Unicode码点。

8.使用文本块语法来声明多行的字符串字面量。

9.使用System.out对象,可以在终端窗口中显示输出。通过与System.in绑定的Scanner可以读取终端输入。

10.数组和容器可以用于收集相同类型的元素。

1.1 我们的第一个程序

当学习任何新的编程语言时,传统做法是从一个能够显示“Hello, World!”消息的程序开始,这也是在下面的小节中我们将要做的事情。

1.1.1 剖析“Hello, World”程序

话不多说,下面就是Java中的“Hello, world”程序。

package ch01.sec01;
 
// Our first Java program 
 
public class HelloWorld {
     public static void main(String[] args) {
          System.out.println("Hello, World!");
     }
}

让我们一起来看看这个程序。

Java是一种面向对象的语言。在程序中,通常需要控制对象(object)来让它们完成具体工作。操作的每个对象都属于特定的(class),通常也称这个对象是该类的一个实例(instance)。类定义了对象的状态是什么,以及对象能做什么。在Java中,所有代码都是在类中定义的。第2章将详细介绍对象和类。这个程序是由一个名为HelloWorld的单一类组成的。

main是一个方法(method),也就是在类中声明的一个函数。main方法是程序运行时调用的第一个方法。main方法声明为static,以表示该方法不作用于任何对象。(当调用main方法时,只有少数的预定义对象,并且它们都不是HelloWorld类的实例。)main方法也声明为void,以表示它不返回任何值。关于main方法中参数声明String[ ] args的含义,参见1.8.8小节。

在Java中,你可以将许多特性声明为publicprivate。除此之外,Java中还有一些其他可见性级别。这里将HelloWorld类和main方法都声明为public,这是类和方法中最常见的定义形式。

包(package是一组相关类的集合。把相关类放在一个包中是一个很好的做法,这样可以将相关类组合在一起,并避免多个类在具有相同名称时可能会发生的冲突。本书使用章(Chapter)和节(Section)的编号作为包名。因此,示例中类的全名就是ch01.sec01.Helloworld。第2章会有更多关于包和包命名规范的内容。

以 // 开头的行是注释。编译器会忽略从 // 到行末的所有字符,这些字符仅仅用来辅助编程人员阅读程序。

最后,来看看main方法的主体。在示例中,它由一行命令组成,该命令的功能是向System.out输出一个消息,System.out对象代表Java程序的“标准输出”。

正如你所见,Java不是一种可以用来快速执行一些简单命令的脚本语言。它的类、包和模块(模块在第15章中介绍)等特性使得它更适合用于编写大型程序。

Java也非常简单和统一。一些编程语言不仅有全局变量和全局函数,还有类内部的变量和方法。在Java中,所有东西都在类中声明,这种统一性可能会导致代码有些冗长,但也使得理解程序的含义变得容易。

注意:你刚刚看到了一个 // 形式的注释,它的注释效果是延伸到行末的。还可以在 /**/ 分隔符之间添加多行注释。例如:

/*
    This is the first sample program in Core Java for the Impatient.
    The program displays the traditional greeting "Hello, World!".
*/

还有第三种注释样式,称为文档注释(documentation comment),使用/***/作为分隔符。下一章中将会介绍。

1.1.2 编译和运行Java程序

要编译和运行一个Java程序,需要安装Java开发工具包(Java Development Kit,JDK),此外,也可以安装集成开发环境(Integrated Development Enviroment,IDE)。可以在异步社区中下载本书的示例代码。

一旦安装了JDK,就可以打开一个终端窗口,并切换到包含ch01目录的目录,然后运行以下命令:

javac ch01/sec01/HelloWorld.java 
java ch01.sec01.HelloWorld

然后,那条熟悉的问候语就将出现在终端窗口中,如图1-1所示。

图1-1 在控制台窗口运行Java程序

需要注意的是,执行程序需要两个步骤。首先,javac命令将Java源代码编译(compile)成一个与机器无关的中间表示,称为字节码(byte code),并将它们保存在类文件(class file)中;然后,java命令启动一个虚拟机(virtual machine,该虚拟机会加载类文件并执行编译后的字节码。

一旦编译完成,字节码可以在任意一个Java虚拟机中运行,无论是在你的台式计算机上或者是在遥远银河系中的某个设备上。这个“一次编写,处处运行”的承诺是Java的一个重要设计标准。

注意:javac编译器是通过文件名进行调用的,使用斜杠分隔文件路径段,文件扩展名为.javajava虚拟机的启动器是通过类名进行调用的,使用点号来分隔包的名称段,并且没有扩展名。

注意:如果程序由单个源文件组成,那么可以跳过编译的步骤,直接使用以下命令运行程序:

java ch01/sec01/HelloWorld.java

在后台,程序将会在运行之前进行编译,但不会生成类文件。

注意:在类Unix操作系统上,你可以按照以下步骤将Java文件转换为可执行程序。

(1)重命名文件,删除其扩展名.java

mv HelloWorld.java hello

(2)使文件可执行。

chmod +x hello

(3)在文件顶部添加一行bash的运行标记。

#!/path/to/jdk/bin/java --source 17

现在,你就可以通过以下方式运行程序了。

./hello

如果要在 IDE 中运行程序,首先需要按照 IDE 安装说明中描述的方式创建一个项目。然后,选择HelloWorld类并通过IDE运行它。图1-2显示了程序在Eclipse中的运行情况。Eclipse是一个非常流行的IDE。除此之外,还可以选择许多其他优秀的IDE。随着对Java编程的不断学习和深入了解,还是应该多尝试几种IDE,再从中选择一个自己喜欢的。

图1-2 在Eclipse IDE中运行Java程序

好了,恭喜你刚刚完成了使用Java运行“Hello,World!”程序这一古老的传统,下面我们就准备开始学习Java语言的基础知识。

提示:在异步社区官网中可以下载本书所有章节的示例代码。这些代码经过精心编排和组织,你可以很方便地创建一个包含所有示例程序的单个项目。建议你在仔细阅读本书中内容的同时,下载、运行和学习这些配套代码。

1.1.3 方法调用

让我们更仔细地看看main方法中唯一的语句:

System.out.println("Hello, World!");

System.out是一个对象,它是一个名为PrintStream的类的实例(instancePrintStream类有printlnprint等方法。这些方法被称为实例方法(instance method),因为它们对类的对象或实例进行操作。

若要在对象上调用实例方法,请使用点符号(dot notation)来表示:

object.methodName(arguments)

在这个例子中,main方法只有一个参数,即字符串"Hello, World!"

让我们用另一个例子来试试,像"Hello, World!" 这样的字符串是String类的实例。String类有一个返回String对象长度的length方法。若要调用该方法,则需要再次使用点符号:

"Hello, World!".length()

length方法是通过对象"Hello, World!"调用的,且该方法没有参数。与println方法不同,length方法会返回一个结果。使用该返回结果的一种方法就是将它输出到屏幕:

System.out.println("Hello, World!".length());

一起来试试看。用这个语句来编写一个Java程序并运行它,看看字符串的长度是多少。

在Java中,需要自己构造(construct)大多数对象(不像System.out"Hello, World!"这些对象,它们是已经存在的,可以直接使用)。下面是一个简单的示例。

Random类的对象可以生成随机数。可以使用new运算符来构造一个Random对象:

new Random()

在类名之后的是构造参数列表,在这个例子中该列表是空的。

你可以在构造的对象上调用方法。例如:

new Random().nextInt()

这样就可以通过这个新构造的随机数生成器,生成下一个随机整数。

如果想在一个对象上调用多个方法,那么需要将对象存储在变量中(参见1.3节)。这里我们打印两个随机数:

Random generator = new Random(); 
System.out.println(generator.nextInt()); 
System.out.println(generator.nextInt());

注意:Random类是在java.util包中声明的。为了在程序中使用这个类,需要添加import语句,示例如下。

package ch01.sec01; 
 
import java.util.Random; 
 
public class MethodDemo {
    ...
}

我们将在第2章中更详细地了解包和import语句。

1.1.4 JShell

在1.1.2小节中,你看到了如何编译和运行一个Java程序。JShell程序提供了一个“读取—评估—打印循环”(read-evaluate-print loop,REPL)的方式,它允许你尝试Java代码而无须编译和运行程序。当输入Java表达式时,JShell会评估输入,并打印结果,然后等待下一次输入。如果要启动JShell,只须在终端窗口中输入jshell,如图1-3所示。

图1-3 运行JShell

JShell以问候语开头,然后显示提示符:

|  Welcome to JShell -- Version 17
|  For an introduction type: /help intro
 
jshell>

现在可以输入一个Java表达式,例如:

"Hello, World!".length()

JShell会给你一个反馈,表示运行结果和下一个提示符:

$1 ==> 13
 
jshell>

需要注意的是,你并没有输入System.out.println。JShell会自动打印输入的每个表达式的值。

输出中的$1表示该结果可用于进一步的计算。例如,如果继续输入:

3 * $1 + 3

则JShell的反馈为:

$2 ==> 42

如果需要多次使用一个变量,那么可以给它指定一个更加容易记忆的名字。一定要遵循Java语法,同时指定类型和变量名(参见1.3节)。例如:

jshell> int answer = 42 
answer ==> 42

可以让JShell替你填写类型。具体操作是,输入一个表达式,不要按Enter键,而是按住Shift+Tab组合键,然后按V键。例如,当输入:

new Random()

然后按Shift + Tab组合键和V键,你会看到:

jshell> Random = new Random()

这时的光标位于 = 符号之前。此时可以输入一个变量名,然后按Enter键:

jshell> Random generator = new Random() 
generator ==> java.util.Random@3fee9989

此外,还有一个很棒的功能是Tab补全功能,例如输入:

generator.

随后按Tab键,你将会得到一个可以在generator变量上调用的所有方法的列表:

jshell > generator.
doubles(                   equals(                 getClass()         
hashCode()                 ints(                   isDeprecated()
longs(                     nextBoolean()           nextBytes(          
nextDouble(                nextExponential()       nextFloat(
nextGaussian(              nextInt(                nextLong(           
notify()                   notifyAll()             setSeed(
toString()                 wait(

现在输入ne再按Tab键。方法名会补全为next,并且你会得到一个更加简短的列表:

jshell> gengerator.next
nextBoolean()              nextBytes(              nextDouble(         
nextExponential()          nextFloat(              nextGaussian(
nextInt(                   nextLong(

如果再按D键和Tab键,方法名会自动补全为nextDouble(。再次按Tab键,将会看见3个备选:

Signatures:
double java.util.random.RandomGenerator.nextDouble(double bound)
double java.util.random.RandomGenerator.nextDouble(double origin, double bound) 
double Random.nextDouble()
  <press tab again to see documentation>

)键可以选择第3个版本:

jshell> gengerator.nextDouble()
$3 ==> 0.9560346568377398

注意:在自动完成列表中,需要参数的方法只能后跟左括号,如nextDouble( ,而不需要参数的方法有一对完整括号,如nextBoolean()

如果要重复运行命令,可以按 ↑ 键,直到看到要重新运行或编辑的行。可以用 ← 键和 → 键移动命令行中的光标,并添加或删除字符,完成后按Enter键。例如,按 ↑ 键并用Int替换Double,然后按Enter键:

jshell> generator.nextInt()
$4 ==> -352355569

默认情况下,JShell将会导入以下包:

java.io
java.math
java.net
java.nio.file 
java.util
java.util.concurrent 
java.util.function 
java.util.prefs 
java.util.regex 
java.util.stream

这就是可以在JShell中使用Random类而不需要任何导入语句的原因。如果需要导入其他类,可以在JShell提示符下输入导入语句。或者,更方便的是,通过按住Shift+Tab组合键和I键,可以让JShell搜索它。例如,输入Duration,然后按住Shift+Tab组合键和I键,你将获得一个潜在操作的列表:

jshell> Duration
0: Do nothing
1: import: java.time.Duration
2: import: javafx.util.Duration
3: import: javax.xml.datatype.Duration 
Choice:

输入1,然后你将收到一个确认信息:

Imported: java.time.Duration

随后显示:

jshell> Duration

这样就完成了导入工作,然后就可以继续工作了。这些命令足以让你开始使用 JShell。要获得更加详细的帮助,输入/help并按Enter键。如果要退出JShell环境,输入/exit并按Enter键,或者只须按Ctrl+D组合键。

JShell 使得 Java 语言和相关库的学习变得轻松而有趣,且无须启用庞大的开发环境和编写复杂的public static void main等代码。

1.2 基本类型

尽管Java是一种面向对象的编程语言,但这也并不代表所有Java的值都是对象。Java中的一些值依然属于基本类型(primitive type)。基本类型中有4种类型是有符号整数类型;两种是浮点类型;一种是在字符串编码中使用的char类型;另一种是表示真值的boolean(布尔)类型。在下面的小节中我们将详细学习这些类型。

1.2.1 有符号整数类型

有符号整数类型适用于没有小数部分的数,可以是负数。Java提供了4种有符号整数类型,如表1-1所示。

表1-1 Java中的有符号整数类型

类型

存储容量

取值范围(含)

byte

1字节

−128~127

short

2字节

−32 768~32 767

int

4字节

−2 147 483 648~2 147 483 647 ( 刚好超过20亿)

long

8字节

−9 223 372 036 854 775 808~9 223 372 036 854 775 807

注意:常量Integer.MIN_VALUEInteger.MAX_VALUE分别是int类型的最小值和最大值。此外,LongShortByte类也分别都有MIN_VALUEMAX_VALUE常量。

在大多数情况下,int类型是最实用的。但是如果想表示整个地球的居住人口数量,就需要使用long类型了。byteshort类型主要用于特定的应用场合,例如,底层文件处理,或者存储空间有限的大数组。

注意:如果long类型依然不够,那么可以使用BigInteger类。参见1.4.6小节了解详细信息。

在Java中,整数类型的范围不取决于运行程序的机器。毕竟,Java是被设计为“一次编写,处处运行”的语言。相比之下,C和C++程序中的整数类型的大小还取决于编译该程序的处理器。

可以使用后缀L来表示长整型字面量(例如,4000000000L)。但是,byte类型或short类型的字面量无法通过后缀区分。这时需要使用强制转换符号(参见1.4.4小节)。例如,(byte)127表示byte类型。十六进制字面量具有前缀0x(例如,0xCAFEBABE)。二进制数值具有前缀0b,例如,0b10019

警告:八进制数值具有前缀0,例如,0119。但这样的形式可能会容易混淆,因此最好远离八进制字面量和0开头的数值。

你可以在数字字面量中添加下划线进行长数字的分组,例如,使用1_000_000(或0b1111_0100_ 0010_0100_0000)来表示100万。但这里的下划线仅仅是为了让人更易阅读,Java编译器会删除它们。

注意:如果使用的整数值永远不会是负数,并且确实需要一个额外的数位来存储数据,那么可以将有符号整数值解释为无符号数,但是需要非常仔细。例如,一个byte类型的值b的表示范围通常是−128~127。如果想表示0~255的范围,仍然可以将其存储在byte类型中。由于二进制算术运算的性质,如果不发生溢出,那么加法、减法和乘法都是可以正常工作的。对于其他运算,可以调用Byte.toUnsignedInt(b)来获得0~255的int类型的值,然后就可以处理整数值,并将结果强制转换回byte类型。IntegerLong类也有用于处理无符号数的除数和余数的方法。

1.2.2 浮点类型

浮点类型表示有小数部分的数值。Java中的两种浮点类型如表1-2所示。

表1-2  浮点类型

类型

存储容量

取值范围

float

4字节

−3.40282347E+38F~+3.40282347E+38F(6~7位十进制有效数字)

double

8字节

−1.79769313486231570E+308~+1.79769313486231570E+308(15位十进制有效数字)

很多年前,当内存还是一种稀缺资源时,4 字节的浮点数是最常用的。但现在 7 位有效数字已经不太适用了,因此“双精度”数是系统的默认值。只有当需要存储大量的浮点数时,使用float类型才有意义。

float类型的数值有一个后缀F(例如,3.14F);没有后缀F的浮点数(例如,3.14)是double类型的。当然,你可以选择使用后缀D(例如,3.14D)来表示double类型的数值。

注意:你可以用十六进制来表示浮点数。例如,0.0009765625 = 2−10,也可以写成0x1.0p-10。在十六进制符号中,你需要使用p而不是e来表示指数。(因为e是一个十六进制数字。)请注意,即使数字是十六进制的,但指数(即2的幂)也需要使用十进制。

Java中有一些特殊的浮点值:Double.POSITIVE_INFINITY表示∞;Double.NEGATIVE_INFINIY表示Double.NaN表示“非数值”。例如,算式1.0/0.0的结果是正无穷大。算式0.0/0.0或负数的平方根会生成NaN。

警告:所有“非数值”都会被认为是各不相同的。因此,你不能使用条件测试语句if (x == Double.NaN) 来检查 x 是否为 NaN。而是应该使用 if (Double.isNaN(x))来判断。此外,也应当使用Double.isInfinite来测试±∞,用Double.isFinite来检查一个浮点数既不是无穷也不是NaN。

浮点数并不适用于金融计算的场景,因为它在计算中发生的一些舍入误差对金融领域来讲可能是无法容忍的。例如,System.out.println(2.0 - 1.7)将会打印出0.30000000000000004,而不是你所期望的0.3。这种舍入误差是由浮点数在二进制系统中的表示规则造成的。此外,小数3/10也没有精确的二进制表示,就像十进制系统中1/3没有精确的表示一样。如果你需要任意精度且没有舍入误差的数值计算,可以使用1.4.6小节中介绍的BigDecimal类。

1.2.3 char类型

char类型描述了Java使用的UTF-16字符编码中的“代码单元”。有关的详细信息颇有一些技术难度,请参见1.5节。你可能不会经常使用char类型。

偶尔你可能会遇到用单引号括起来的字符字面量。例如,'J'是值为 74(或十六进制4A)的字符字面量,表示Unicode字符“U+004A,拉丁大写字母J”的代码单元。这里的代码单元可以用十六进制表示,并使用\u作为前缀。例如,'\u004A''J'相同。更奇特的例子是'\u263A',☺的代码单元,“u+263A白色笑脸”。

此外一些特殊的代码,例如'\n''\r''\t''\b'分别表示换行、回车、制表和退格。

如果需要使用'\'则需要使用两个斜杠表示'\\'

1.2.4 boolean类型

boolean(布尔)类型有两个值:falsetrue

在Java中,boolean类型不是数值类型。boolean类型的值与整数类型中的01并没有任何关系。

1.3 变量

在下面的小节中,你将学习如何声明和初始化变量和常量。

1.3.1 变量声明

Java是一种强类型语言。每个变量只能保存一种特定类型的值。声明变量时,需要指定变量的类型、名称和一个可选的初始值。例如:

int total = 0;

你可以在一个语句中声明相同类型的多个变量:

int total = 0, count; // count is an uninitialized integer

但是,大多数的Java程序员都喜欢单独声明每个变量。

一起来看下面这个变量声明:

Random generator = new Random();

在这个声明中,对象的类的名称出现了两次。其中,第一个Random表示的是变量generator的类型;第二个Random是构造该类的对象的new表达式的组成部分。

为了避免这种重复,可以使用var关键字:

var generator = new Random();

现在,变量的类型是初始化该变量的表达式的类型。在这个例子中,generator是一个类型为Random的变量。

当声明变量的类型非常明显时,本书会使用var关键字。

1.3.2 标识符

变量、方法或类的名称统称为标识符(identifier)。在Java中,标识符必须以字母开头,由任意字母、数字、_符号和$符号组成。但是,$符号用于自动生成的标识符,因此,你不应该直接使用它。最后,_符号本身并不是有效的标识符。

在Java中,字母和数字可以来自任何字母,而不仅仅是拉丁字母。例如,πélévation也是有效的标识符。此外,标识符区分字母的大小写,countCount是不同的标识符。

你不能在标识符中使用空格或符号。最后,也不能使用double等关键字作为标识符。

按照惯例,变量和方法的名称以小写字母开头,类的名称以大写字母开头。Java程序员喜欢使用“驼峰式拼写法”(camel case,也称为骆驼式拼写法),即当名称由多个单词组成时,使用大写字母标识每一个单词的首字母,如countOfValidInputs

1.3.3 初始化

当在一个方法中声明变量时,必须先对其进行初始化,然后才能使用它。例如,以下代码会导致编译时错误:

int count;
count++; // Error—uses an uninitialized variable

编译器必须能够验证变量在使用之前是否已经初始化。例如,以下代码也是一种错误:

int count;
if (total == 0) {
     count = 0;
} else {
     count++; // Error—count might not be initialized
} 

Java允许在方法中的任何位置声明变量。在第一次需要使用变量之前,尽可能晚地声明变量被认为是一种较好的编程的风格。例如:

var in = new Scanner(System.in); // See Section 1.6.1 for reading input 
System.out.println("How old are you?");
int age = in.nextInt(); 

变量在其初始值可用时声明即可。

1.3.4 常量

final关键字表示赋值后不能被再次更改的值。在其他的语言中,通常可以将这样的值称为常量(constant)。例如:

final int DAYS_PER_WEEK = 7;

按照惯例,常量的名称应当全部使用大写字母。

你也可以使用static关键字来声明一个在方法外的常量:

public class Calendar {
     public static final int DAYS_PER_WEEK = 7;
     ...
}

这样一来,该常量就可以在多个方法中被使用。在Calendar内部,你可以通过DAYS_PER_WEEK来表示该常量。但是,若要在另一个类中使用该常量,需要在该常量之前加上类名,即Calendar. DAYS_PER_WEEK

注意:System类中声明了一个常量,如下所示。

public static final PrintStream out

这样,可以在任何地方通过System.out的形式使用它。它也是少数几个没有用大写字母表示的常量之一。

延迟final变量的初始化是合法的,只需要在首次使用它之前初始化即可。例如,以下代码是合法的:

final   int DAYS_IN_FEBRUARY;
if (leapYear) {
     DAYS_IN_FEBRUARY = 29;
} else {
     DAYS_IN_FEBRUARY = 28;
}

这也就是称它为“最终”变量的原因。一旦赋值,它就是最终变量,永远无法更改。

注意:有时,你需要一组相关的常量,示例如下。

public static final int MONDAY = 0; 
public static final int TUESDAY = 1; 
...

在这种情况下,你可以定义一个枚举(enumeration)。

enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
     SATURDAY, SUNDAY };

这样,Weekday就是一种带有Weekday.MONDAY等数值的类型。下面是如何声明和初始化Weekday变量。

Weekday startDay = Weekday.MONDAY;

我们将在第4章详细讨论枚举。

1.4 算术运算

Java使用任何基于C的语言中常见的运算符,如表1-3所示,我们将在下面的小节学习如何使用它们。

表1-3  Java运算符

运算符

结合性

[] . () (方法调用)

左结合

! ~ ++ −− +(一元运算符)−(一元运算符)()(强制转换)new

右结合

* / %(模运算)

左结合

+ −

左结合

<< >> >>>(算术移位)

左结合

< > <= >= instanceof

左结合

== !=

左结合

& (位与)

左结合

^(位异或)

左结合

|(位或)

左结合

&& (逻辑与)

左结合

||(逻辑或)

左结合

? : (条件运算符)

左结合

= += −= *= /= %= <<= >>= >>>= &= ^= |=

右结合

注意:在本表中,运算符是按照优先级递减的顺序排列的。例如,由于 + 的优先级高于 <<,因此3 + 4 <<5 等价于(3 + 4) << 5。当一个运算符是从左向右进行分组时,我们称它是左结合的。例如,3 – 4 − 5表示(3 − 4) − 5。但 −= 是右结合的,例如I −= j −= k表示 i −= (j −= k)

1.4.1 赋值

表1-3中的最后一行表示赋值运算符,例如:

x = expression;

以上语句会将x的值设置为右侧表达式的值,同时替换掉x之前的数值。

赋值是一个带有值的运算,具体来讲就是所赋的那个值。因此,在另一个表达式中使用赋值运算是合法的。例如:

(next = in.read()) != -1

以上语句中,nextin.read()的返回值赋值,如果该值不是−1,则赋值运算的值就不是−1,最后整个表达式的值为true

= 前面有另一个运算符时,该运算符将左侧和右侧组合起来,并计算得到结果。例如:

amount -= fee

等同于

amount = amount - fee;

1.4.2 基本算术运算符

加法、减法、乘法和除法分别用 + */ 表示。例如,2 * n + 1表示将2n相乘再加1

使用 / 运算符时,一定要小心。如果两个操作数都是整数类型,它表示整数除法,将得到整数结果并丢弃余数。例如,17 / 53,而17.0 / 53.4

整数除以零会产生一个异常,如果未捕捉到该异常,则程序会终止运行。(有关异常处理的更多信息,参见第5章。)一个浮点数除以零会生成一个无穷或NaN(参见1.2.2小节),并且不会导致异常。

使用 % 运算符将会得到余数。例如,17 % 5的结果是2,即17减去15(5的最接近17的整数倍)后的余数。如果a % b的结果为零,则ab的整数倍。

% 运算符的一个基本用途就是测试整数是否为偶数。如果n为偶数,则表达式n % 2的结果为0。如果n是奇数呢?这时,如果n为正,则n % 21;如果n为负,则n % 2−1。在实践中,处理负数是比较复杂的。当 % 运算符与那些可能为负的操作数一起使用时,一定要注意这些问题。

考虑一下这个问题。你需要计算一个时钟的时针位置。你需要调整时针,并将其标准化为一个介于0~11的数字。那么处理起来很简单:使用(position + adjustment) % 12即可。但是如果adjustment使时针的位置为负呢?那么你可能会得到一个负数。所以这时你必须引入一个分支,或者使用((position + adjustment) % 12 + 12) % 12。不管怎样,这样处理都很麻烦。

提示:在这种情况下,使用Math.floorMod方法会更容易。

Math.floorMod(position + adjustment, 12)将总是产生一个介于011之间的值。但遗憾的是,floorMod针对负除数的运算也会给出负数的结果,但这种情况在实际应用中并不常见。

Java有递增和递减运算符:

n++; // Adds one to n
n--; // Subtracts one from n

和其他基于C的语言类似,这些运算符也有前缀形式。n++++n都会使变量n的值递增,但在表达式中使用时,它们可能会有不同的值。第一种形式在n递增之前生成表达式的值,第二种形式则在n递增之后生成表达式的值。例如:

String arg = args[n++];

以上语句将arg设置为args[n],然后再使n递增。大概30年前,当编译器不能很好地优化代码时,这样做是有意义的。但是如今,使用两条单独的语句并不会产生性能上的差异,并且许多程序员认为显式的形式更容易阅读。

1.4.3 数学方法

在Java中,没有运算符能够实现幂运算。因此需要调用Math.pow()方法来实现:Math.pow(x, y) 将得到xy。如果要计算x的平方根,则需要调用Math.sqrt(x)。这些方法都是静态方法,因此与static常量一样,只需要在方法前添加类名,并不需要通过对象来调用这些方法。

此外,比较常用的还有Math.min()Math.max()方法,可用于计算两个值的最小值和最大值。

此外,Math类还提供了三角函数、对数函数,还有常量Math.PI和常量Math.E

注意:Math类提供了几种方法以确保整数的算术运算更安全。当计算溢出时,算术运算符会悄悄返回一个错误的结果。例如,10亿乘以3(1000000000 * 3)计算得到的结果为−1294967296,这是因为最大的int值恰好刚刚超过20亿。如果你调用Math.multiplyExact(1000000000, 3),那么将会产生一个异常。你可以捕获该异常,或者让程序终止,而不是使用错误的计算结果,并让程序继续运行。此外,还有addExactsubtractExactincrementExactdecrementExactnegateExact等方法,它们都使用intlong作为参数。

在其他类中也有一些数学方法。例如,Integer类和Long类中有compareUnsigneddivideUnsignedremainderUnsigned方法来处理无符号数。

1.4.4 数值的类型转换

当运算符的操作数是不同的数值类型时,在运算之前,这些数值会自动转换为一个通用的类型。转换是按照以下顺序进行的。

如果两个操作数中有一个为double类型,则另一个将转换为double类型。

如果其中一个操作数是float类型,则另一个将转换为float类型。

如果其中一个操作数是long类型,则另一个将转换为long类型。

否则,两个操作数都转换为int类型。

例如,如果计算3.14 + 42,那么第二个操作数将会从int类型转换为double类型的值42.0,然后进行加法计算,得到double类型的值45.14

如果计算 'J' + 1,则char类型的值'J'将被转换成为int类型的值74,最后结果为int类型的值75。至于如何将该值转换回char类型,还需要继续阅读后面的内容。

当你将数值类型的值赋值给一个变量,或将其作为参数传递给一个方法时,如果类型不匹配,则必须转换该值的类型。例如,在以下赋值中,值42会自动从int类型转换为double类型。

double x = 42;

在Java中,如果没有精度损失,那么将发生以下形式的转换:

byte类型到short类型、int类型、long类型,或者double类型;

short类型和char类型到int类型、long类型,或者double类型;

int类型到long类型或者double类型。

所有的整数类型都会被转换成为浮点类型。

警告:以下转换可能会丢失精度信息。

int类型到float类型。

long类型到float类型或double类型。

例如,考虑以下赋值:

float f = 123456789;

因为float类型只有7位有效数字,所以f实际上是1.23456792E8

为了实现这些自动转换之外的类型转换,需要使用强制类型转换运算符,强制类型转换的语法格式是在圆括号中指定目标类型的名称。例如:

double x = 3.75; 
int n = (int) x;

在这种情况下,小数部分将会被舍弃,n会被设置为3

如果想四舍五入到最接近的整数,可以使用Math.round方法,该方法返回一个long类型的值。如果明确知道结果更加适合int类型,可以调用:

int n = (int) Math.round(x);

在这个示例中,x3.75n被设置为4

如果需要将整数类型转换为另一个字节更少的类型,也需要使用强制转换:

int n = 1;
char next = (char)('J' + n); // Converts 75 to 'K'

在这种强制转换中,只保留最后的字节,例如:

int n = (int) 3000000000L; // Sets n to -1294967296

注意:如果担心强制类型转换会无警告地丢弃数值的重要部分,那么可以使用Math.toIntExact方法。当该方法无法将long类型转换为int类型时,就会产生异常。

1.4.5 关系运算符和逻辑运算符

==!= 运算符的功能是测试相等性。例如,当n不为0时,表达式n != 0的值为true。此外,<(小于)、>(大于)、<=(小于或等于)和 >=(大于或等于)运算符都是常用的运算符。

你也可以将boolean类型的表达式与 &&(与)、||(或)和 !(非)运算符组合。例如:

0 <= n && n < length

n介于0(包含0)和length(不包含length)之间时,表达式为真。

以上表达式中,如果第一个条件为false,则第二个条件不会被计算。如果第二个条件可能会产生错误,那么这种“短路”测试的方式非常有用。考虑以下条件:

n != 0 && s + (100 - s) / n < 50

如果n0,那么第二个条件,即其中包含除n运算的条件永远不会被求值,因此也不会出现除数为0的计算错误。

短路测试也可以用于“或”运算,只要一个操作数为true时,其余的求值就会停止,即不计算第二个条件。例如:

n == 0 || s + (100 - s) / n >= 50

如果n0,则以上表达式将得到true,并且第二个条件不会被计算。

最后,条件(conditional)运算符接受3个操作数:一个条件和两个值。如果条件为true,整个表达式的结果是第一个值,否则是第二个值。例如:

time < 12 ? "am" : "pm" 

表示如果time < 12 为真,则得到字符串"am",否则得到字符串"pm"

注意:Java还有位运算符 & (位与)、|(位或) 、^(位异或)。它们是针对整数,按照位模式进行运算的。例如,由于0xF的二进制数字是0...01111,因此n & 0xF就会得到n的二进制数字中的最低4位; n = n | 0xF 将会将n的二进制值的最低4位设置为1n = n ^ 0xF则将翻转n的最低4位。与 ! 运算符类似的位运算符是 ~ ,它的功能是翻转操作数的所有位,即~0xF的结果是1...10000

此外,还有在位模式下向左或向右移位的运算符。例如,0xF << 2将得到二进制数字为0...0111100。Java中共有两个右移运算符,其中 >> 是将符号位扩展到顶部,而 >>> 则会用0来填充顶部的符号位。如果你在程序中进行移位运算,那么你必须要知道这意味着什么;如果你并不了解移位运算,那么也就表明你可能不需要使用这些运算符了。

警告:如果移位运算符号的左操作数是int类型,那么右操作数的模是32;如果左操作数是long类型,那么右操作数的模是64。例如,1 << 35的值与1 << 38 相同。

提示:&(位与)和 |(位或)运算符应用于boolean值时,在计算结果之前将会对两个操作数进行强制求值。当然,这种用法非常罕见。加入右操作数没有副作用,它们就像 &&|| 一样,只是效率较低。除非确实需要强制对第二个操作数求值,并将其赋值给一个boolean变量,以使得执行流程清晰可见。

1.4.6 大数

如果基本类型的整数和浮点数的精度无法满足实际需求,那么可以使用java.math包中的BigInteger类和BigDecimal类。这些类的对象可以表示具有任意长度数字序列的数值。BigInteger类可以实现任意精度的整数运算,BigDecimal也可以对浮点数实现同样的功能。当然,使用大数的计算效率远远低于使用基本数据类型的计算效率。

静态方法valueOf可以将long类型转换为BigInteger

BigInteger n = BigInteger.valueOf(876543210123456789L);

你还可以从数字形式的字符串中构造一个BigInteger的对象:

var k = new BigInteger("9876543210123456789");

此外还有一些预定义的常量,例如,BigInteger.ZEROBigInteger.ONEBigInteger.TWOBigInteger.TEN

Java不允许对象使用运算符,因此必须调用对应的方法来处理大数的运算。

BigInteger r = BigInteger.valueOf(5).multiply(n.add(k)); // r = 5 * (n + k)

1.2.2 小节中,你看到了浮点数减法 2.0 - 1.7的结果为0.30000000000000004。使用BigDecimal类可以计算出精确结果。

调用BigDecimal.valueOf(n, e)将返回一个值为n×10−eBigDecimal实例。例如,以下方法调用将准确地得到结果0.3

BigDecimal.valueOf(2, 0).subtract(BigDecimal.valueOf(17, 1))

1.5 字符串

字符串是一个字符的序列。在Java中,字符串可以包含任意的Unicode字符。例如,字符串"Java™""Java\u2122"由5个字符构成,分别是Java。其中最后一个字符是“U+2122,注册商标”。

1.5.1 拼接

使用 + 运算符可以拼接两个字符串。例如:

String location = "Java";
String greeting = "Hello " + location;

以上两条语句将greeting设置为字符串"Hello Java"。(注意第一个操作数末尾的空格。)

当你将一个字符串与另一个值拼接时,该值将会转换为字符串:

int age = 42;
String output = age + " years";

现在字符串output"42 years"

警告:如果混合使用拼接和加法运算,那么可能会得到意想不到的结果。示例如下。

"Next year, you will be " + age + 1 // Error

首先,字符串拼接age,然后再拼接1,因此,最后得到的结果是"Next year, you will be 421"。在这种情况下,需要使用括号。

"Next year, you will be " + (age + 1) // OK

如果要组合多个字符串,并使用分隔符将他们分隔开,请使用join方法:

String names = String.join(", ", "Peter", "Paul", "Mary");
    // Sets names to "Peter, Paul, Mary"

join方法的第一个参数是分隔符字符串,后面是要拼接的字符串。它们的数量可以是任意多个,你也可以以字符串数组传递参数(数组在1.8节中有介绍)。如果需要连接大量的字符串,那么这种方法的效率会有些低。在这种情况下,请使用StringBuilder来代替join方法:

var builder = new StringBuilder(); 
while (more strings) {
     builder.append(next string);
}
String result = builder.toString();

1.5.2 子串

如果拆分字符串,可以使用substring方法。例如:

String greeting = "Hello, World!";
String location = greeting.substring(7, 12); // Sets location to "World"

substring方法的第一个参数是要提取子串的起始位置,位置从0开始表示。

第二个参数是不包含子串的第一个位置。在以上的示例中,greeting的第12个位置是 ! ,这个是我们不需要的字符的位置。该方法需要指定一个不需要的字符的位置,这可能看起来很奇怪,但这样做有一个优点:12 – 7将会是这个子串的长度。

有时,你可能希望从一个由分隔符分隔的字符串中提取所有子串。split方法将能够实现这个功能,并返回一个由子串组成的数组。

String names = "Peter, Paul, Mary"; 
String[] result = names.split(", ");
    // An array of three strings ["Peter", "Paul", "Mary"]

这里的分隔符可以是任何正则表达式(参见第9章)。例如,input.split("\\s+") 将在空白处拆分input字符串。

1.5.3 字符串比较

如果要比较两个字符串是否相等,请使用equals方法。例如:

location.equals("World")

location字符串恰好为"World"时,表达式将返回true

警告:永远不要使用 == 运算符来比较字符串。在下面的比较中,仅当location"World"在内存中是完全相同的对象时才能返回true

location == "World" // Don’t do that!

即在虚拟机中,每个字符串字面量只用一个实例,因此只有"World" == "World" 才能为true。但如果location是被计算得到的,例如,

String location = greeting.substring(7, 12);

那么结果将会被放置到一个单独的String对象中,location == "World" 将返回false

与其他任何对象一样,String变量可以是nullnull表示这个变量不指向任何对象,甚至不指向一个空字符串。

String middleName = null;

如果要测试一个对象是否为null,可以使用 == 运算符:

if (middleName == null) ...

需要注意的是,null与空字符串""不同。空字符串是长度为零的字符串,而null表示根本不存在任何字符串。

警告:针对null调用任何方法都会导致“空指针异常”。和所有异常一样,如果你不显式地处理它,该异常会中断程序的运行。

提示:当将字符串与字符串字面量进行比较时,最好将字符串字面量放在前面,示例如下。

if ("World".equals(location)) ...

这样的优势在于,即使locationnull,该测试也能正常工作。

如果在比较两个字符串时需要忽略字符的大小写,可以使用equalsIgnoreCase方法。例如:

"world".equalsIgnoreCase(location);

location"World""world"或者"WORLD"等情况时,都会返回true

有时,你可能需要将字符串按顺序排列。调用compareTo方法可以判断两个字符串是否按字典顺序排列:

first.compareTo(second)

如果firstsecond之前,那么该方法返回一个负整数(不一定是−1);如果firstsecond之后,则返回正整数(不一定是1);如果两者相等,则返回0

compareTo方法会依次比较每一个字符,直到其中一个字符串到达末尾,或者两个字符串不匹配。例如,当比较"word""world"时,前3个字符是匹配的,第4个字符d的Unicode值小于l。因此,"word"字符串在前。所以"word".compareTo("world") 返回−8,该值是dl的Unicode值之间的差。

这种比较方式对很多人来说可能不是很直观,因为它取决于字符的 Unicode 值的大小。例如,"blue/green""bluegreen"之前,因为字符 / 的Unicode值恰好小于g的Unicode值。

提示:在对相对比较容易阅读的字符串进行排序时,可以使用支持特定语言排序规则的Collator对象。有关更多信息参见第13章。

1.5.4 数值和字符串的相互转换

要将整数转换为字符串,可以调用静态Integer.toString方法:

int n = 42;
String str = Integer.toString(n); // Sets str to "42"

这个方法也可以有第二个参数,即一个基数(范围为2~36):

String str2 = Integer.toString(n, 2); // Sets str2 to "101010"

注意:更简单地将整数转换为字符串的方法是用空字符串和整数拼接,例如:"" + n 。但是有些人认为这样的代码很不美观,且效率稍低。

相反地,如果要将包含整数的字符串转换成为数值,那么可以使用Integer.parseInt方法:

String str = "101010";
int n = Integer.parseInt(str); // Sets n to 101010

同样地,该方法也可以指定转换基数:

int n2 = Integer.parseInt(str, 2); // Sets n2 to 42

对于浮点数和字符串之间的相互转换,可以使用Double.toStringDouble.parseDouble方法:

String str = Double.toString(3.14); // Sets str to "3.14" 
double x = Double.parseDouble(str); // Sets x to 3.14

1.5.5 字符串API

就像你期望的那样,String类定义了大量的方法。表1-4列出了一些经常使用的方法及其功能。

表1-4 String类常用方法

方法

功能

boolean startsWith(String str)

boolean endsWith(String str)

boolean contains(CharSequence str)

检查字符串是否以给定的字符串开头、结尾,或是否包含给定字符串

int indexOf(String str)

int lastIndexOf(String str)

int indexOf(String str, int fromIndex)

int lastIndexOf(String str, int fromIndex)

获取str的第一个或最后一个出现的位置,搜索整个字符串或从fromIndex开始的子串,如果未找到匹配项,则返回−1

String replace(CharSequence oldString,

   CharSequence newString)

将所有出现的oldString替换为newString,并返回新字符串

String toUpperCase()

String toLowerCase()

将原始字符串中的所有字符转换为大写或小写,并返回新字符串

String strip()

返回通过删除所有前导空格和末尾空格获得的新字符串

需要注意的是,在Java中,String类是不可变(immutable)的。也就是说,String的众多方法中没有一个方法能够修改字符串本身内容。例如,

greeting.toUpperCase()

将返回一个新字符串"HELLO,WORLD!",但并不会改变greeting。同样需要注意的是,有些方法具有CharSequence类型的参数。这是StringStringBuilder和其他字符序列的通用超类。如果需要查询每个String方法的详细描述,请参阅在线Java API文档。在搜索框中输入类名并选择匹配的类型即可得到如图1-4所示的信息(在本例中为java.lang.String)。

图1-4 检索API文档

随后,你将会获得一个记录每个方法的页面,如图1-5所示。当然,如果你碰巧知道某个方法的名称,可以直接在搜索框中输入方法的名称进行检索。

图1-5 API文档中的String方法

本书没有详细介绍API的具体细节,因为很多时候直接浏览API文档会更快捷。如果你不能保证总是可以连接到互联网,那么你可以下载并解压离线文档,进行脱机浏览。

1.5.6 码点和代码单元

Java第一次发布时,就非常自豪地采纳了同样是新生事物的Unicode标准。Unicode标准旨在解决字符编码这个非常棘手的问题。在Unicode之前,有许多互相不兼容的字符编码。以英语为例,有几乎可以作为通用标准的7位ASCII编码标准,该标准为所有英文字母、十进制数字和许多符号分配了介于0~127的编码。在西欧,ASCII还被扩展为8位代码,用来容纳类似ä和é等重音字符。在俄罗斯,ASCII也同样被扩展,俄罗斯使用128~255的位置表示一些斯拉夫字符。在日本,通常使用可变长度编码对英语和日语字符进行编码。此外,还有多种不兼容的中文字符编码也在被广泛使用。总之,在使用不同编码的情况下交换文件是一个很困难的问题。

Unicode通过介于0~65535的唯一的16位编码,来对所有书写系统的每个字符分配唯一的编码,来解决困扰大家已久的字符编码问题。1991年,Unicode 1.0发布,该标准使用了略少于半数的有效65536编码。Java从一开始就被设计成使用16位Unicode字符的系统,这一点对比其他使用传统8位字符编码的编程语言,是一个重大进步。但随后又发生了一些尴尬的事情,即汉字的数量远远超过了之前的预估值,这就迫使Unicode必须使用超过16位的编码方案。

如今,Unicode需要21位进行编码。每个有效的Unicode值称为码点(code point),其基本形式为U+与其后的4个或多个十六进制的数字。例如,字符A的码点是U+0041,而表示八元数集合的数学符号的码点是U+1D546 。

还有一种更加清楚的方式来表示码点,例如使用int值,但这显然是非常浪费的。Java使用一种变长的编码形式,称为UTF-16,它将所有“经典”的Unicode字符表示为单个16位的值,此外对于所有超过U+FFFF的字符编码,都需要通过一个16位的值组合配对表示,这个16位的值表示一个特殊的代码区域,通常被称为“代理字符”。在UTF-16编码中,字符A可以通过一个char值来表示,记作\u0041;而会被记作一对char\ud835\udd46

换句话说,char并不是Unicode字符或码点。它只是一个代码单元(code unit),是UTF-16编码中所使用的一个16位的量。

如果你并不使用中国的汉字,并且愿意把等特殊字符抛在脑后的话,那么字符串是一个Unicode字符序列的事情就对你没有太大影响,你当它是一个神话传说就行。在这种情况下,可以这样获得第 i个字符:

char ch = str.charAt(i);

也可以这样获取字符串的长度:

int length = str.length();

但是如果你想正确地处理字符串,那么必须工作得更加辛苦一些。例如,要获取Unicode的第i个码点,需要调用:

int codePoint = str.codePointAt(str.offsetByCodePoints(0, i));

码点总数为:

int length = str.codePointCount(0, str.length());

循环提取每一个码点:

int i = 0;
while (i < s.length()) {
     int cp = sentence.codePointAt(i);
     i += Character.charCount(cp);
     ... // Do something with cp
}

或者,也可以使用codePoints方法来生成一个int值的(stream),这样每个int值都对应一个码点。我们将在第8章中讨论流。你也可以将流转换为一个数组,如:

int[] codePoints = str.codePoints().toArray();

注意:过去,字符串总是在内部采用UTF-16编码表示,以char值数组的形式来表示。现在,String对象会尽可能地以ISO-8859-1字符的byte数组的形式来表示。未来版本的Java内部可能会改用UTF-8。

1.5.7 文本块

使用文本块语法可以更加方便地提供跨行形式的字符串文本。文本块以"""开头,后面可以直接使用换行符,结尾则使用另一个"""来标记:

String greeting = """ 
Hello
World
""";

greeting字符串包含两个换行符:一个在Hello之后,另一个在World之后。字符串文本中不包含起始的"""标记后的换行符。

如果你不希望在最后一行使用换行符,那么可以将终止标记符"""放在最后一个字符之后:

String prompt = """ 
Hello, my name is Hal. \ 
Please enter your name:""";

在任何一行的末尾,你都可以在行末添加反斜杠作为禁止换行的标志:

String prompt = """
Hello, my name is Hal. \
Please enter your name:""";

这样,字符串内就不包含任何换行符了。

文本块特别适用于一些包含其他语言代码的情况,例如SQL或HTML。只须将其粘贴在一对三重引号之内:

String html = """
<div class="Warning">
    Beware of those who say "Hello" to the world 
</div>
""";

需要注意的是,使用文本块时,你可以不用转义引号。但是,文本块中还是有两种特殊情况需要转义引号:

文本块以引号结尾

文本块包含3个或更多引号。

遗憾的是,文本块中你仍然需要转义所有反斜杠。

常规字符串中的所有转义序列在文本块中的使用方式都相同。

可以通过删除末尾的空格,或者将Windows系统的换行符(\r\n)更改为更加简单的换行符(\n)的方式来规范文本的换行符。如果你仍旧需要保留末尾的空格,请将最后一个空格转换为\s转义符。以下字符串就以两个空格结尾:

String prompt = """
Hello, my name is Hal. 
Please enter your name: \s""";

对于前导空格来说,事情就更加复杂了。考虑一个典型的变量声明,需要从左边距进行缩进。可以缩进文本块:

String html = """
     <div class="Warning">
         Beware of those who say "Hello" to the world
     </div>
     """;

这样就会去除文本块中所有行共有的最长前导空格序列。实际字符串是:

"<div class=\"Warning\">\n   Beware of those who say \"Hello\" to the world\n</div>\n"

注意,第一行和第三行中没有缩进。

文本块终止标记符"""之前的空格非常重要。但是,在删除缩进的过程中,整行的空格并不会被压缩。

警告:作为前缀的空格必须与文本块中的所有行完全匹配。如果混合使用制表符和空格,你可能会发现删减的空格会比预期的少。

1.6 输入和输出

为了让示例程序更加生动有趣,它们应该能够与用户进行交互。在下面的小节中,你将了解如何读取终端输入,以及如何实现格式化输出。

1.6.1 读取输入

当调用System.out.println时,输出被发送到“标准输出流”,从而在终端窗口中显示出来。如果要从“标准输入流”读取信息则没那么简单,因为对应的System.in对象只有一些读取单个字节的方法。为了读取字符串和数字,还需要构造一个能够连接到System.in对象的Scanner

var in = new Scanner(System.in);

nextLine方法能够读取一整行输入:

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

这里使用nextLine方法的主要原因是输入中可能包含空格。如果要读取由空格分隔的单个单词,需要调用:

String firstName = in.next();

如果要读取整数,可以使用nextInt方法:

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

类似地,你也可以使用nextDouble方法读取下一个浮点数。可以使用hasNextLinehasNexthasNextInthasNextDouble方法检查是否有新的行、单词、整数或浮点数可用:

if (in.hasNextInt()) {
    int age = in.nextInt();
    ...
}

Scanner类位于java.util包中,为了使用这个类,应当在程序的开头添加以下代码:

import java.util.Scanner;

提示:如果要读取密码,你可能就不会想使用Scanner类了,因为Scanner类会使得输入在终端中可见。建议使用Console类,示例如下。

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

这样用户输入的密码将以字符数组形式返回。这比将密码存储在String中更安全,因为可以在读取操作完成后重新处理数组。

提示:如果你想从文件中读取输入或将输出写入文件,可以使用shell的重定向语法。

java mypackage.MainClass < input.txt > output.txt

现在System.in将会从input.txt中读取信息,System.outoutput.txt中写入信息。你将在第9章中看到如何执行更加通用的文件输入和输出操作。

1.6.2 格式化输出

你已经看到了System.out对象中的println方法,可以用于编写一行输出。此外还有一种print方法,该方法的输出不会每次输出都开始新的一行。该方法通常用于输入提示:

System.out.print("Your age: "); // Not println 
int age = in.nextInt();

这样光标将会停留在提示信息之后,而不是下一行。

当你使用printprintln方法打印一个小数时,除末尾的零以外的所有数字将会被显示。例如:

System.out.print(1000.0 / 3.0);

将会打印:

333.333333333333

但是,如果你想显示美元和美分,这就会是一个问题了。为了限制输出位数,可以这样使用printf方法:

System.out.printf("%8.2f", 1000.0 / 3.0);

格式化串(format string)"%8.2f"表示的含义是,以8个字段宽度(field width)和2位精度(precision)的形式打印浮点数。也就是说,最终打印输出中会包含2个前导空格和6个字符:

333.33

你还可以为printf提供多个参数。例如:

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

每个以%字符开头的格式说明符(format specifier)都将被替换为相应的参数。格式说明符的末尾是转换说明符(conversion character),表示要格式化的值的类型:f表示浮点数,s表示字符串,d表示十进制整数。表1-5列出了所有转换说明符。

表1-5  格式化输出的转换说明符

转换说明符

功能

示例

d

十进制整数

159

x 或者X

十六进制整数(有关十六进制格式的更多信息,请使用Hexformat类)

9f或者9F

o

八进制整数

237

f

固定型浮点数

15.9

e或者E

指数型浮点数

1.59e+01或者1.59E+01

g或者G

通用型浮点数:如果指数大于精度要求或指数<−4,则为e/E,否则为f/F

15.9000,默认精度6位;2e+01,精度为1

a或者A

十六进制浮点数

0x1.fccdp3或者0X1.FCCDP3

s或者S

字符串

Java或者JAVA

c或者C

字符

j或者J

b或者 B

boolean

false或者FALSE

h或者 H

哈希码(参见第4章)

42628b2或者42628B2

t或者T

日期和时间(已过时,参见第12章)

%

百分号

%

n

平台相关的行分隔符

此外,你可以指定标志符来控制格式化输出的外观。表1-6列出了所有标志符。

表1-6  格式化输出标志

标志符

功能

示例

+

打印正数或者负数的标志

+3333.33

空格

在正数前添加空格

_3333.33

-

左对齐标记

3333.33---

0

添加前导0

003333.33

(

将负值括在括号中

(3333.33)

,

使用分组符号

3,333.33

#(针对f或者e格式)

始终包含小数点

3333.

#(针对x或者o格式)

添加0x或者0前缀

0xcafe

$

指定要格式化的参数的索引。例如,%1$d %1$x将以十进制和十六进制打印第一个参数

159 9f

<

格式化前面说明的数值。例如,%d %<x表示以十进制和十六进制打印同一个数字

159 9f

例如,逗号标志可以添加分组分隔符,+ 符号会为正数添加正数符号。以下语句:

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

将会打印

+33,333.33

你也可以使用formatted方法创建格式化字符串,而无须打印它:

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

1.7 控制流

在以下几小节中,你将学习如何实现分支和循环。Java 语言的这部分语法与其他常用语言(特别是C/C++和JavaScript)非常相似。

1.7.1 分支

if语句在圆括号内有一个分支条件,后面会有一个语句或一组括在花括号中的语句:

if (count > 0){
     double average = sum / count; 
     System.out.println(average);
}

你也可以添加一个else分支以在条件不满足时运行该分支:

if (count > 0) {
     double average = sum / count;
     System.out.println(average); 
} else {
     System.out.println(0);
}

else分支的语句中也可以再添加另外一个if语句:

if (count > 0) {
     double average = sum / count; 
     System.out.println(average); 
} else if (count == 0) {
     System.out.println(0);
} else {
     System.out.println ("Huh?");
}

1.7.2 switch语句

switch表达式的功能是将一个操作数与多个选项进行比较,并为每个具体情况生成一个值:

String seasonName = switch (seasonCode) { // switch expression
     case 0 -> "Spring";
     case 1 -> "Summer";
     case 2 -> "Fall";
     case 3 -> "Winter";
     default -> {
          System.out.println("???");
          yield "";
     }
};

需要注意的是,switch在这里是一个表达式(expression),并且有一个值,即5个字符串"Spring" "Summer""Fall""Winter"""中的一个。这个switch表达式的值被赋值给seasonName变量。

其实最常见的情况是,一个case后面跟着一个表达式。你也可以在一个花括号括起来的语句块中做一些其他的额外工作,就像前面示例中的default部分一样。然后,你需要在语句块中使用yield语句来生成一个值。

switch还有一种语句形式,如下所示:

switch (seasonCode) { // switch statement
     case 0 -> seasonName = "Spring";
     case 1 -> seasonName = "Summer";
     case 2 -> seasonName = "Fall";
     case 3 -> seasonName = "Winter";
     default -> {
          System.out.println("???");
          seasonName = "";
     }
}

在前面的示例中,case标签是整数。你可以使用以下任意类型的值:

char类型、byte类型、short类型或int类型的常量表达式(或与其相对应的封装类CharacterByteShortInteger,将在1.8.3小节中介绍);

字符串字面量;

枚举的值(参见第4章)。

每个case都可以有多个标签,并用逗号分隔:

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

注意:整数或String上的switch表达式总是有一个default部分。无论操作数值是什么,switch表达式都必须生成一个值。此外,如前一个示例所示,大小写的区别可能会引发异常。异常将在第5章中具体介绍。

警告:如果switch的操作数值为null,那么一个NullPointerException异常将会被抛出。当操作数类型为String或枚举时,会发生这种情况。

在前面的示例中,switch表达式和语句中,对于给定的操作数值只有一个case分支被执行。当然有时也可能会发生一些例外,这种情况通常被称作直通(fall-through,也称贯通)。即其从匹配的case分支开始执行,然后继续执行下一个case,除非被yieldbreak语句打断。switch的直通式变体同样也具有表达式和语句形式。在下面的示例中,当seasonName"Spring"时会发生这种直通。

int numLetters = switch (seasonName) { // switch expression with fall-through
    case "Spring":
        System.out.println("spring time!");
    case "Summer", "Winter":
        yield 6;
    case "Fall":
        yield 4;
    default:
        throw new IllegalArgumentException();
};
 
switch (seasonName) { // switch statement with fall-through
    case "Spring":
        System.out.println("spring time!");
    case "Summer", "Winter":
        numLetters = 6;
        break;
     case "Fall":
        numLetters = 4;
        break;
    default:
        throw new IllegalArgumentException();
}

需要注意的是,在直通式变体中,每个 case后面都跟一个冒号,而不是一个 ->。这样可以在冒号后跟任意数量的语句,并且不需要花括号。此外,在带有直通的switch表达式中,必须使用yield来生成一个值。

警告:在直通式变体中,忘记yieldbreak是一个常见的错误。除非真的需要直通行为,否则请避免使用这种变体。

1.7.3 循环

while循环会依据具体的条件,反复执行其循环体的语句。例如,假定有一个对数值求和的任务,直到数值的总和达到目标值。我们将使用随机数生成器作为数值的来源,其由java.util包中的Random类提供:

var generator = new Random();

下面的调用将会生成0~9的一个随机整数:

int next = generator.nextInt(10);

以下是用于求和的循环:

while(sum < target) {
     int next = generator.nextInt(10);
     sum += next;
     count++;
}

这是while循环的典型用法。当总和小于目标值时,循环会持续执行。

有时你需要先执行循环体,然后才能评估循环条件。假设你想知道达到特定值所需的具体时间,那么在测试循环条件之前,需要先进入循环并获取到那个测试值。在这种情况下,要使用do/while循环:

int next;
do {
     next = generator.nextInt(10);
     count++;
} while (next != target);

这样就可以先进入循环体,再设定next的值,然后再评估是否满足循环条件。只要满足循环条件,循环体就会重复执行。

在前面的示例中,循环迭代的次数都是未知的。然而,在实践中的许多循环中,循环迭代的次数都是固定的。在这些情况下,最好使用for循环。

例如,下面示例中的循环计算固定数量的随机值之和:

for (int i = 1; i <= 20; i++){
     int next = generator.nextInt(10);
     sum += next;
}

这个循环将会执行20次,每次循环迭代中,i分别会被设置为1、2、……、20。

可以将任何一个for循环重写为while循环。上面的循环等效于:

int i = 1;
while (i <= 20){
     int next = generator.nextInt(10);
     sum += next;
     i++;
}

while循环中,变量i的初始化、测试和更新分散在循环体的不同位置。而使用for循环,变量i的初始化、测试和更新可以很紧凑地聚集在一起。此外,for循环中变量的初始化、测试和更新可以采用任意形式。例如,当一个值小于目标值时,可以将其加倍:

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

也可以不在for循环的头部声明变量,而是初始化现有变量:

for (i = 1; i <= target; i++) // Uses existing variable i

或者可以声明或初始化多个变量并提供多个变量的更新,用逗号分隔。例如:

for (int i = 0, j = n - 1; i < j; i++, j--)

如果不需要初始化或更新,那么也可以将其留空。如果忽略该条件,则认为该条件总是为true

for (;;) // An infinite loop

你将在下一小节中看到如何退出这种无限循环。

1.7.4 breakcontinue

如果想从循环迭代的过程中退出,可以使用break语句。例如,假设你想处理用户输入的单词,直到用户输入字母Q为止。下面是一个使用boolean变量来控制循环的解决方案:

boolean done = false; 
while (!done) {
     String input = in.next();
     if ("Q".equals(input)) {
          done = true;
     } else {
          Process input
     }
}

下面的循环使用break语句执行相同的任务:

while(true) {
     String input = in.next();
     if ("Q".equals(input)) break; // Exits loop 
     Process input
}
// break jumps here

当到达break语句时,循环将立即退出。

continue语句类似于break,但它不会跳到循环的终点,而是跳到当前循环迭代的终点。可以使用它来略过不需要的输入,例如:

while (in.hasNextInt()) {
     int input = in.nextInt();
     if (input < 0) continue; // Jumps to test of in.hasNextInt()
     Process input
}

for循环中,continue语句将会跳转到下一个更新语句处:

for (int i = 1; i <= target; i++) {
     int input = in.nextInt();
     if (n < 0) continue; // Jumps to i++
     Process input
}

break语句仅从紧邻着的封闭循环或switch中跳转出来。如果要跳转到另一个封闭语句的末尾,请使用带标签的 break语句。在需要退出的语句处打上标签,例如:

outer:
while(...){
     ...
     while (...) {
          ...
          if (...) break outer:
          ...
      }
      ...
}
// Labeled break jumps here

标签可以是任何名称。

警告:虽然你在语句的顶部打上了标签,但break语句将跳转到末尾

常规break语句只能用于退出循环或switch,但带标签的break语句可以将控制转移到任何语句的末尾,甚至是块语句:

exit: {
     ...
     if(...)break exit;
     ...
}
// Labeled break jumps here

还有一个带标签的continue语句,它跳转到标签处开始下一次迭代。

提示:许多编程人员发现break语句和continue语句令人困惑。需要知道的是,这些语句完全是可选的,没有它们也是可以表达相同的逻辑的。本书不会使用break语句或continue语句。

1.7.5 局部变量的作用域

现在,你已经看到了使用了嵌套形式的语句块的示例。这是一个很好的开始,下面我们即将开始学习变量作用域的一些基本规则。局部变量(local variable)就是在方法中声明的任何变量,甚至包括方法的参数变量。变量的作用域(scope)就是可以在程序中访问该变量的范围。局部变量的作用域是从变量声明处开始,一直延伸到当前的封闭块的末尾:

while (...) {
     System.out.println(...);
     String input = in.next(); // Scope of input starts here
     ...
     // Scope of input ends here
}

换言之,每个循环在迭代时,都会创建一个新的input变量的副本,并且该变量在循环之外并不存在。

参数变量的作用域是整个方法:

public static void main(String[] args) { // Scope of args starts here
     ...
     // Scope of args ends here
}

这里还有一种需要理解作用域规则的情况。以下的循环计算了获取特定随机数字需要尝试的次数:

int count = 0;
int next;
do {
     next = generator.nextInt(10);
     count++;
} while (next != target);

这里的next变量必须在循环外部声明,以便在循环中实现条件判断。如果在循环内部声明,那么它的作用域将只延伸到循环体的结尾。

当你在for循环中声明变量时,它的作用域将延伸到循环的结尾,包括测试和更新语句:

for (int i = 0; i < n; i++) { // i is in scope for the test and update
     ...
}
// i not defined here

如果需要循环后的i值,就请在外部声明变量:

int i;
for (i = 0; !found && i < n; i++) {
     ...
}
// i still available

在Java中,不能在重叠的作用域内有名称相同的局部变量:

int i = 0; 
while (...) {
     String i = in.next(); // Error to declare another variable i
     ...
}

但是,如果作用域不重叠,则变量名可以相同:

for (int i = 0; i < n / 2; i++) { ... }
for (int i = n / 2; i < n; i++) { ... } // OK to redefine i

1.8 数组和数组列表

数组是一种能够容纳相同类型的多个数据的基本程序结构。Java语言中内置了数组类型,并且提供了一个ArrayList类,该类实现了数组按需增、减的操作。ArrayList类是Java语言中庞大的容器框架的一部分,该框架将在第7章中详细介绍。

1.8.1 使用数组

每种数据类型都有一个对应的数组类型。一个整数数组的类型是int[],一个String对象数组的类型是String[],以此类推。下面是一个保存字符串的数组:

String[] names;

以上语句中的变量尚未初始化。因此我们需要先用一个新的数组初始化这个变量。为此,需要使用new运算符:

names =  new String[100];

当然,可以将这两个语句组合在一起:

String[] names = new String[100];

现在names就成为了一个包含100个元素的数组,可以通过names[0]...names[99]的形式来访问数组中的这些元素。

警告:如果试图访问不存在的元素,例如names[-1]names[100],则会发生ArrayIndexOutOfBoundsException异常。

数组的长度可通过array.length获得。例如,以下循环使用空字符串填充字符串数组:

for (int i = 0; i < names.length; i++) {
     names[i] = "";
}

注意:使用C风格的语法形式来声明数组变量也是合法的,即将数组的[]跟在变量名后,而不是数据类型后。

int  numbers[] ;

但是,这种语法形式并不友好,因为这样的声明形式很容易混淆了变量名numbers和类型int[]。因此,几乎没有Java编程人员这样定义数组。

1.8.2 数组构造

当你使用new运算符构造数组时,它会使用默认值来填充数组:

数值类型(包括char)的数组用0填充;

boolean数组用false填充;

对象数组用null引用填充。

警告:在构造对象数组时,需要用对象进行填充,示例如下:

BigInteger[] numbers = new BigInteger[100];

此时,数组中还没有任何BigInteger对象,只有100个null引用。需要将它们替换为对BigInteger对象的引用。

for (int i = 0; i < 100; i++)
     numbers[i] = BigInteger.valueOf(i);

如前一小节所述,也可以通过编写一个循环语句来用值填充数组。然而,如果知道数组元素确切的值,就可以直接在花括号内列出它们:

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

如果不使用new运算符,也不指定数组长度,那么可以在末尾用逗号表示,这样可以方便随时添加数组值:

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

如果不想为数组指定名称,那么可以使用类似初始化的语法。例如,下面的示例将数组赋值给现有数组变量:

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

注意:长度为0的数组是合法的。可以使用new int[0]new int[]{}这种形式来构造一个数组。例如,如果一个方法返回一个匹配的数组,但是没有特定的输入,那么可以返回一个长度为 0 的数组。需要注意的是,长度为0的数组与null不同。如果a是长度为0的数组,那么a.length0;如果anull,则a.length将会导致NullPointerException异常。

1.8.3 数组列表

当构造一个数组时,需要明确知道它的长度。一旦数组构造好,长度就永远不会改变了。这在许多实际应用中是不方便的。有一种补救方法是使用java.util包中的ArrayList类。ArrayList对象在内部管理一个数组。当该数组太小或不够用时,另一个内部数组会被自动创建,并且元素会被移入其中。使用数组列表的编程人员看不到这个过程。

数组和数组列表的语法完全不同。数组使用特殊语法——[]运算符来访问元素,通过Type[]语法来标记数组的类型,以及通过new Type[n]的语法来构造数组。相反,数组列表是类,需要使用正常的语法来构造实例和调用方法。

此外,与目前看到的类不同,ArrayList类是一个带有类型参数的类——泛型类(generic class),第6章将详细介绍泛型类。

对于泛型类,需要使用尖括号来指定数组列表内元素的类型。例如,保存String对象的数组列表的类型应当表示为ArrayList<String>

为了声明和初始化这种类型的变量,可以使用以下3个语句中的任意一个:

ArrayList<String> friends = new ArrayList<String>(); 
var friends = new ArrayList<String>();
ArrayList<String> friends = new ArrayList<>();

注意,最后一个声明使用了空的<>,编译器会根据变量的类型推断其类型。[此快捷方式称为菱形语法(diamond syntax,因为空的尖括号具有菱形形状。]

此调用中没有构造参数,但仍需要在末尾提供()

结果可以生成长度为0的数组列表。可以使用add 方法在末尾添加元素:

friends.add("Peter");
friends.add("Paul");

由于数组列表没有设定初始值的语法,因此最好的方式是通过以下途径构造一个数组列表:

var friends = new ArrayList<>(List.of("Peter", "Paul"));

这里的List.of方法生成一个不可修改的给定元素列表,然后可以再使用该列表来构造一个ArrayList实例。

可以在ArrayList中的任何位置添加和删除元素:

friends.remove(1);
friends.add(0, "Paul"); // Adds before index 0

为了访问元素,必须调用对应的方法,而不能使用[]语法。数组列表使用 get方 法读取元素,使用set方 法修改元素:

String first = friends.get(0); 
friends.set(1, "Mary");

size方法可以获取数组列表当前的大小。以下示例使用循环来遍历所有元素:

for (int i = 0; i < friends.size(); i++) {
     System.out.println(friends.get(i)); 
}

1.8.4 基本类型的封装类

泛型类在一些方面存在限制,即不能将基本类型用作泛型类的类型参数。例如,ArrayList<int>是非法的。因此最好使用基本类型的封装类。每个基本类型,都有一个相应的封装类:IntegerByteShortLongCharacterFloatDoubleBoolean。如果要创建整数的数组列表,可以使用ArrayList<Integer>

var numbers = new ArrayList<Integer>(); 
numbers.add(42);
int first = numbers.get(0);

基本类型与其对应的封装类之间的类型转换是自动实现的。在调用add方法的过程中,一个保存了值42Integer对象会被自动构造,这种对象的自动构造过程叫作自动装箱(autoboxing)。

在以上示例代码中的最后一行,调用get方法将会返回一个Integer对象。在赋值给int变量之前,该对象会被拆箱(unboxing)以转换生成int值。

警告:基本类型和封装类之间的关系对编程人员几乎完全透明。只有一个例外,==!= 运算符比较的是对象的引用,而不是对象的内容。if(numbers.get(i) == numbers.get(j)) 条件并不会测试索引ij处的数值是否相同。就像字符串一样,你需要记住使用包装对象调用equals方法来判断两者是否相等。

1.8.5 增强for循环

你经常会希望访问数组的所有元素。例如,以下是计算数字数组中所有元素总和的方法:

int sum = 0;
for (int i = 0; i < numbers.length; i++) {
     sum += numbers[i];
}

由于这种循环的使用场景非常多,因此Java有一种更加方便的快捷方式来实现这种循环,通常称为增强for(enhanced for循环:

int sum = 0;
for (int n : numbers) {
     sum += n;
}

增强for循环的循环变量会遍历数组的元素而不是索引值。变量n会被numbers[0]numbers[1]等元素依次赋值。

也可以将增强for循环与数组列表一起使用。如果friends是字符串数组列表,则可以使用这样的循环打印所有元素:

for (String name : friends) {
     System.out.println(name); 
}

1.8.6 复制数组和数组列表

可以将一个数组变量复制到另一个数组中,但实际情况是这两个变量将引用相同的数组,如图 1-6所示。

int[] numbers = primes;
numbers[5] = 42; // Now primes[5] is also 42

图1-6 两个变量引用同一个数组

如果不想这样让两个数组变量共享一份数据,那么就需要创建一个数组的副本。以下示例使用静态方法Arrays.copyOf进行复制:

int[] copiedPrimes = Arrays.copyOf(primes, primes.length);

Arrays.copyOf方法构造了一个长度为所需值的新数组,并将原始数组的元素复制到新数组中。

数组列表引用的工作方式也和数组复制的情况类似:

ArrayList<String> people = friends;
people.set(0, "Mary"); // Now friends.get(0) is also "Mary"

为了复制数组列表,可以从现有的数组列表中构造一个新的数组列表:

var copiedFriends = new ArrayList<>(friends);

该构造器还可以用于将数组复制到数组列表中。使用List.of方法可以将数组封装到一个不可变的列表中,然后构造一个ArrayList

String[] names = ...;
var friends = new ArrayList<>(List.of(names));

同样可以将数组列表复制到数组中。出于兼容性这样令人失望的原因,你必须提供一个正确类型的数组。兼容性问题将在第6章中解释。

String[] names = friends.toArray(new String[0]);

注意:基本类型数组和对应的封装类数组列表之间没有简单的方法。例如,要在int[]ArrayList<Integer>之间进行转换,需要使用显式循环或intStream(参见第8章)。

1.8.7 数组算法

ArraysCollections类都为数组和数组列表提供通用算法的实现。下面是填充数组或数组列表的方法:

Arrays.fill(numbers, 0); // int[] array 
Collections.fill(friends, ""); // ArrayList<String>

要对数组或数组列表进行排序,请使用sort方法:

Arrays.sort(names); 
Collections.sort(friends); 

注意:对于数组(而不是数组列表)来说,如果数组较大,可以使用parallelSort方法将任务分配到多个处理器上。

Arrays.toString方法可以生成数组的字符串表示。这对打印调试系统的数组非常有用:

System.out.println(Arrays.toString(primes));
     // Prints [2, 3, 5, 7, 11, 13]

数组列表也有一个toString方法,该方法也能实现同样的功能:

String elements = friends.toString(); 
     // Sets elements to "[Peter, Paul, Mary]"

如果只是为了打印,你甚至不需要调用它,println方法会自动处理:

System.out.println(friends); 
     // Calls friends.toString() and prints the result

对于数组列表而言,还有一些有用的算法,但是数组无法使用这些算法:

Collections.reverse(names); // Reverses the elements 
Collections.shuffle(names); // Randomly shuffles the elements

1.8.8 命令行参数

正如你已经看到的那样,每个Java程序的main方法都有一个字符串数组作为参数:

public static void main(String[] args)

当程序被执行时,这个参数会被设置为命令行中指定的参数。例如,下面这个程序:

public class Greeting {
     public static void main(String[] args) {
          for (int i = 0; i < args.length; i++) {
               String arg = args[i];
               if (arg.equals("-h")) arg = "Hello";
               else if (arg.equals("-g")) arg = "Goodbye"; 
               System.out.println(arg);
          }
     }
}

如果这个程序被这样调用:

java Greeting -g cruel world

那么args[0]就是"-g"args[1]就是"cruel"args[2]就是"world"

请注意,命令行中的"java""Greeting"是不会被传递到main方法内的。

1.8.9 多维数组

Java语言中并没有真正的多维数组,它们被实现为数组的数组。例如,以下是声明和实现二维整数数组的方法:

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

从技术上讲,这是一个一维的int[]数组,见图1-7。

要访问一个元素,请使用两对方括号:

int element = square[1][2]; // Sets element to 11

其中,第一个索引选择行数组square[1],第二个索引表示从该行中选取元素。

你甚至可以交换一整行,例如:

int[] temp = square[0];
square[0] = square[1];
square[1] = temp;

图1-7 二维数组

如果未提供初始值,则必须使用new运算符并指定行数和列数:

int[][] square = new int[4][4]; // First rows, then columns

在之后的应用场景中,行数组的每一行都由一个数组填充。这里并不要求行数组具有相等的长度。例如,你可以存储帕斯卡三角形:

1
1  1
1  2  1
1  3  3  1
1  4  6  4  1
...

首先构造一个n行的数组:

int[][] triangle = new int[n][];

然后使用循环构造每一行,并填充:

for (int i = 0; i < n; i++) {
     triangle[i] = new int[i + 1];
     triangle[i][0] = 1;
     triangle[i][i] = 1;
     for (int j = 1; j < i; j++) {
          triangle[i][j] = triangle[i - 1][j - 1] + triangle[i - 1][j];
     }
}

要遍历一个二维数组,需要使用两个循环,一个用于行,另一个用于列:

for (int r = 0; r < triangle.length; r++) {
     for (int c = 0; c < triangle[r].length; c++) {
          System.out.printf("%4d", triangle[r][c]);
     }
     System.out.println();
}

也可以使用增强for循环:

for (int[] row : triangle) {
     for (int element: row) {
          System.out.printf("%4d", element);
     }
     System.out.println();
}

这些循环适用于矩阵数组以及具有不同行长度的数组。

提示:打印二维数组的元素列表用于调试,可以调用以下方法。

System.out.println(Arrays.deepToString(triangle));
     // Prints [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1], ...]

注意:Java 中没有二维数组列表,但可以声明一个 ArrayList<ArrayList><Integer>类型的变量,并自己构建每一行。

1.9 功能分解

如果main方法太长,那么可以将程序分解到多个类中,这将在第2章中介绍。对于一些简单的程序,可以将所有程序代码放入同一个类的不同方法中。具体细节将在第 2 章中详细讲解,这些方法必须像main方法本身一样,使用static修饰符声明。

1.9.1 声明和调用静态方法

当声明一个方法时,需要在方法头(method header)中提供返回值的类型(如果该方法不返回任何值,则为void)、方法名以及参数的类型和名称。然后在方法体(method body)部分提供说明,并使用return语句返回结果:

public static double average(double x,double y){
     double sum = x + y;
     return sum / 2;
}

请将该方法与main方法放在同一个类中。具体位置是在main方法之前还是之后均可。然后,可以这样调用该方法:

public static void main(String[] args){
     double a = ...;
     double b = ...;
     double result = average(a, b);
     ...
}

1.9.2 数组参数和返回值

可以将数组传递到方法中。这时方法只须接收一个数组的引用即可,方法可以通过该引用修改参数数组。以下示例中的方法交换了数组中的两个元素:

public static void swap(int[] values, int i, int j) {
     int temp = values[i];
     values[i] = values[j];
     values[j] = temp;
}

方法也可以返回数组。以下方法返回由给定数组的第一个和最后一个值组成的新数组(不对原数组进行修改):

public static int[] firstLast(int[] values) {
     if (values.length == 0) return new int[0];
     else return new int[] { values[0], values[values.length - 1] };
}

1.9.3 可变参数

某些方法允许调用者提供数量可变的参数。其实在编程中你已经见过这样的方法——printf方法。例如以下调用:

System.out.printf("%d", n);

System.out.printf("%d %s", n, "widgets");

这两个语句都调用了相同的printf方法,但是一个调用有2个参数,另一个调用则有3个参数。

现在让我们使用可变参数的形式重新定义average方法,这样就可以计算任意多个参数的平均数了。例如,average(3, 4.5, -5, 0)。声明可变参数的方法是在类型后使用...符号:

public static double average(double... values)

这时的参数实际上是一个double类型的数组。当调用该方法时,一个数组会被创建并用参数填充,在方法体内,你可以像使用任何其他数组一样使用它。

public static double average(double... values) {
     double sum = 0;
     for (double v : values) sum += v;
     return values.length == 0 ? 0 : sum / values.length;
}

现在可以调用:

double avg = average (3, 4.5, -5, 0);

如果已经将参数存储在一个数组中,那么也没必要对它们进行解包。可以直接传递该数组,而不是传递数组列表:

double[] scores = { 3, 4.5, -5, 0 }; 
double avg = average(scores);

变量参数必须是方法的最后一个参数,但它之前可以有其他参数。例如,以下示例中的方法确保至少有一个参数:

public static double max(double first, double... rest) {
     double result = first;
     for (double v : rest) result = Math.max(v, result);
     return result;
}

练习

1.编写一个程序,读取一个整数并将其以二进制、八进制和十六进制形式打印出来。计算该整数的倒数,并以十六进制浮点数形式打印。

2.编写一个程序,读取一个整数角度值(可能为正或负),并将其标准化为0度~359度的值。请先使用 % 运算符计算,然后使用floorMod重复实现该功能。

3.只使用条件运算符,编写一个读取3个整数,并打印其中的最大值的程序。然后请使用Math.max方法重复该功能。

4.编写一个程序,打印double类型的正的最小值和最大值。(提示:查找Java API中的Math.nextUp方法。)

5.当将一个double类型的值转换为int类型的值时,如果该值可能大于最大int值时会发生什么?试试看。

6.编写一个计算阶乘n! = 1 × 2 × ... × n的程序,使用BigInteger。计算1000的阶乘。

7.编写一个程序,读入0~4294967295的两个整数,将它们存储在int变量中,并计算和显示它们的无符号数的和、差、积、商和余数。不要将它们转换为long类型的值。

8.编写一个程序,读取字符串并打印其所有非空子串。

9.1.5.3小节的示例中,使用s.equals(t)比较两个字符串st,而不能使用s != t。提出一个不使用substring的不同示例。

10.编写一个程序,通过生成一个随机的long值并将其以36进制输出,生成一个随机的字母和数字字符串。

11.编写一个程序,读取一行文本并打印所有非ASCII字符及其Unicode值。

12.编写一个switch表达式,当给定一个指南针方向为"N""S""E""W"的字符串时,会生成一个x-偏移和y-偏移的数组。例如,"W"应生成 new int[] { -1, 0 }

13.编写一个switch语句,当给定一个指南针方向为"N""S""E""W"的字符串时,调整变量xy。例如,"W"会让x减少1。

14.能在switch语句中使用break而不使用直通吗?在switch表达式中呢?为什么?

15.提出一个有用的场景,其中直通行为对于switch表达式或switch语句是有益的。大多数网络搜索的结果都是针对C或C++的示例,其中执行会从case A跳转到case B,而不执行任何操作。在Java中,这样的操作并没有什么意义,因为可以直接使用case A, B

16.“Quine”是一个程序,它不需要读取任何输入或文件,就可以打印自己的源代码。使用Java文本块编写这样的程序。

17.Java开发工具包包含一个Java库的源代码文件src.zip。将其解压缩并使用你最喜爱的文本搜索工具,查找带标记的breakcontinue序列的用法。选择一个用法并重新编写它,不使用带标签的语句。

18.编写一个程序,打印一组彩票号码,选择1~49的6个不同的数字。为了选择6个不同的数字,请从填充由1到49的数组列表开始,然后随机选择一个索引并删除该元素。重复6次,最后按顺序打印结果。

19.编写一个程序,读取二维整数数组,并确定它是否为幻方(即所有行、所有列和对角线的总和是否相等,相等的即为幻方)。二维数组按照行输入,并在用户输入空行时停止。例如,输入:

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

(空行)

你的程序应该判断出该二维数组为幻方。

20.编写一个程序,将给定n的一个帕斯卡三角形存储到ArrayList<ArrayList<Integer>>中。

21.改进average方法,以便至少通过一个参数来调用它。

相关图书

Effective Java中文版(原书第3版)
Effective Java中文版(原书第3版)
Java编程动手学
Java编程动手学
Java研发自测入门与进阶
Java研发自测入门与进阶
Java开发坑点解析:从根因分析到最佳实践
Java开发坑点解析:从根因分析到最佳实践
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Java EE企业级应用开发实战(Spring Boot+Vue+Element)
Effective Java (第3版 英文版)
Effective Java (第3版 英文版)

相关文章

相关课程