WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

Java 基础 #108

Open WangShuXian6 opened 3 years ago

WangShuXian6 commented 3 years ago

Java

https://www.java.com/zh-CN/

https://adoptopenjdk.net/

查看版本

java -version

vscode 插件

Java Extension Pack

Prettier Java [格式化 shift + alt + F]

可安装插件中的开源 jdk


开发环境JDK

www.oracle.com/technetwork/java/javase/downloads image

example

Welcome.java Java区分大小写


public class Welcome {
public static void main(String[] args) {
String greeting = "welcome to core java";
System.out.println(greeting);
for (int i = 0; i < greeting.length(); i++) {
System.out.print("=");
}
System.out.println();
}

}


>编译
>javac程序是一个Java编译器。它将文件Welcome.java编译成Welcome.class
```bash
javac Welcome.java

运行 java程序启动Java虚拟机。虚拟机执行编译器放在class文件中的字节码。

java Welcome

控制台输出

welcome to core java
====================

Mac安装多版本JDK

使用安装包安装各版本或者下载各版本压缩包手动解压 解压后将文件夹移动到目录 /Library/Java/JavaVirtualMachines (非必须,后面配置环境变量需要)。

下载

https://www.oracle.com/java/technologies/javase-downloads.html

https://docs.microsoft.com/zh-cn/java/openjdk/download

配置多个环境变量

打开zsh配置文件
vi ~/.zshrc

输入zshrc配置


export JAVA_8_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home
export JAVA_11_HOME=/Library/Java/JavaVirtualMachines/jdk-11.0.12.jdk/Contents/Home
export JAVA_16_HOME=/Library/Java/JavaVirtualMachines/jdk-16.0.2.jdk/Contents/Home
export JAVA_18_HOME=/Library/Java/JavaVirtualMachines/jdk-18.jdk/Contents/Home

export OPEN_JAVA_16_HOME=/Library/Java/JavaVirtualMachines/microsoft-11.jdk/Contents/Home

默认JDK版本

export JAVA_HOME=$JAVA_8_HOME


##### 多版本切换(取了别名,更方便切换)
>输入zshrc配置

alias jdk8="export JAVA_HOME=$JAVA_8_HOME" alias jdk11="export JAVA_HOME=$JAVA_11_HOME" alias jdk16="export JAVA_HOME=$JAVA_16_HOME" alias jdk18="export JAVA_HOME=$JAVA_18_HOME"

alias openjdk16="export JAVA_HOME=$OPEN_JAVA_16_HOME"

##### 重启终端 切换版本
>执行命令 切换到 java8
```bash
jdk8
WangShuXian6 commented 3 years ago

Java errors

Welcome.java is a non-project file, only syntax errors are reported

在vscode中直接打开一个 java 文件会出现此警告 打开一个java项目文件夹则不再提示该警告

WangShuXian6 commented 3 years ago

applet

构建并运行applet

现在很多浏览器并不提供Java支持,或者启用Java很困难

javac RoadApplet.java
jar cvfm RoadApplet.jar RoadApplet.mf *.class
appletviewer RoadApplet.html

第一条命令是调用Java编译器的命令。它将RoadApplet.java源文件编译成字节码文件RoadApplet.class。

不过这一次不要运行java程序。 首先,使用jar工具将类文件打包到一个“JAR文件”。 然后调用appletviewer程序,这是JDK自带的一个工具,可以用来快速测试applet。需要为这个程序指定一个HTML文件名,而不是一个Java类文件名

WangShuXian6 commented 3 years ago

Java的基本程序设计结构

一个最简单的Java应用程序,它只发送一条消息到控制台窗口中

FirstSample.java

// 类名必须和文件名相同
public class FirstSample {
public static void main(String[] args) {
System.out.println("hello world");
}
}

Java区分大小写

关键字public称为访问修饰符(access modifier),这些修饰符用于控制程序的其他部分对这段代码的访问级别

关键字class表明Java程序中的全部内容都包含在类中。这里,只需要将类作为一个加载程序逻辑的容器,程序逻辑定义了应用程序的行为。 类是构建所有Java应用程序和applet的构建块。Java应用程序中的全部内容都必须放置在类中。

关键字class后面紧跟类名。Java中定义类名的规则很宽松。名字必须以字母开头,后面可以跟字母和数字的任意组合。长度基本上没有限制。但是不能使用Java保留字(例如,public或class)作为类名

标准的命名规范为(类名FirstSample就遵循了这个规范):类名是以大写字母开头的名词。如果名字由多个单词组成,每个单词的第一个字母都应该大写(这种在一个单词中间使用大写字母的方式称为骆驼命名法。以其自身为例,应该写成CamelCase)。

源代码的文件名必须与公共类的名字相同,并用.java作为扩展名。因此,存储这段源代码的文件名必须为FirstSample.java(再次提醒大家注意,大小写是非常重要的,千万不能写成firstsample.java)。

如果已经正确地命名了这个文件,并且源代码中没有任何录入错误,在编译这段源代码之后就会得到一个包含这个类字节码的文件。Java编译器将字节码文件自动地命名为FirstSample.class,并与源文件存储在同一个目录下。最后,使用下面这行命令运行这个程序:

// 不要添加.class扩展名
java FirstSample

运行已编译的程序时,Java虚拟机将从指定类中的main方法开始执行(这里的“方法”就是Java中所说的“函数”),因此为了代码能够执行,在类的源文件中必须包含一个main方法。当然,也可以将用户自定义的方法添加到类中,并且在main方法中调用它们

根据Java语言规范,main方法必须声明为public 每个Java应用程序都必须有一个main方法

在Java中,像在C/C++中一样,用大括号划分程序的各个部分(通常称为块)。Java中任何方法的代码都用“{”开始,用“}”结束。

Java中的所有函数都属于某个类的方法(标准术语将其称为方法,而不是成员函数)。因此,Java中的main方法必须有一个外壳类

Java中的main方法必须是静态的

关键字void表示这个方法没有返回值,所不同的是main方法没有为操作系统返回“退出代码”。 如果main方法正常退出,那么Java应用程序的退出代码为0,表示成功地运行了程序。 如果希望在终止程序时返回其他的代码,那就需要调用System.exit方法。

在Java中,每个句子必须用分号结束。 特别需要说明,回车不是语句的结束标志,因此,如果需要可以将一条语句写在多行上。

Java与C/C++一样,都采用双引号分隔字符串

在Java的方法中,可以没有参数,也可以有一个或多个参数(有的程序员把参数叫做实参)。 对于一个方法,即使没有参数也需要使用空括号。例如,不带参数的println方法只打印一个空行

System.out还有一个print方法,它在输出之后不换行。例如,System.out.print(“Hello”)打印“Hello”之后不换行,后面的输出紧跟在字母“o”之后。


注释

Java中的注释也不会出现在可执行程序中。因此,可以在源程序中根据需要添加任意多的注释,而不必担心可执行代码会膨胀。 在Java中,有3种标记注释的方式。最常用的方式是使用//,其注释内容从//开始到本行结尾。

当需要长篇的注释时,既可以在每行的注释前面标记//,也可以使用/**/将一段比较长的注释括起来

第3种注释可以用来自动地生成文档。这种注释以/**开始,以*/结束。

在Java中,/**/注释不能嵌套。也就是说,不能简单地把代码用/**/括起来作为注释,因为这段代码本身可能也包含一个*/

/**
 * This is the first sample program
 * @version 1.01 1997-03-22
 * @author Gary Cornell
 */
public class FirstSample
{
   public static void main(String[] args)
   {
      System.out.println("We will not use 'Hello, World!'");
   }
}

数据类型

Java是一种强类型语言。这就意味着必须为每一个变量声明一种类型。 在Java中,一共有8种基本类型(primitive type), 其中有4种整型、2种浮点类型、1种用于表示Unicode编码的字符单元的字符类型char和1种用于表示真值的boolean类型。

Java有一个能够表示任意精度的算术包,通常称为“大数值”(big number)。 虽然被称为大数值,但它并不是一种新的Java类型,而是一个Java对象。

整型

整型用于表示没有小数部分的数值,它允许是负数。 Java提供了4种整型 image

在通常情况下,int类型最常用。 但如果表示星球上的居住人数,就需要使用long类型了。 byte和short类型主要用于特定的应用场合,例如,底层的文件处理或者需要控制占用存储空间量的大数组。

在Java中,整型的范围与运行Java代码的机器无关。这就解决了软件从一个平台移植到另一个平台,或者在同一个平台中的不同操作系统之间进行移植给程序员带来的诸多问题。 与此相反,C和C++程序需要针对不同的处理器选择最为高效的整型,这样就有可能造成一个在32位处理器上运行很好的C程序在16位系统上运行却发生整数溢出。 由于Java程序必须保证在所有机器上都能够得到相同的运行结果,所以各种数据类型的取值范围必须固定。

长整型数值有一个后缀L或l(如4000000000L)。 十六进制数值有一个前缀0x或0X(如0xCAFE)。 八进制有一个前缀0,例如,010对应八进制中的8。 很显然,八进制表示法比较容易混淆,所以建议最好不要使用八进制常数。

从Java 7开始,加上前缀0b或0B就可以写二进制数。例如,0b1001就是9。 另外,同样是从Java 7开始,还可以为数字字面量加下划线,如用1_000_000(或0b1111_0100_0010_0100_0000)表示一百万。 这些下划线只是为了让人更易读。Java编译器会去除这些下划线。

Java没有任何无符号(unsigned)形式的int、long、short或byte类型。


浮点类型

浮点类型用于表示有小数部分的数值。在Java中有两种浮点类型

image

double表示这种类型的数值精度是float类型的两倍(有人称之为双精度数值)。 绝大部分应用程序都采用double类型。 在很多情况下,float类型的精度很难满足需求。实际上,只有很少的情况适合使用float类型,例如,需要单精度数据的库,或者需要存储大量数据。

float类型的数值有一个后缀F或f(例如,3.14F)。 没有后缀F的浮点数值(如3.14)默认为double类型。当然,也可以在浮点数值后面添加后缀D或d(例如,3.14D)。

可以使用十六进制表示浮点数值。例如,0.125=2-3可以表示成0x1.0p-3。 在十六进制表示法中,使用p表示指数,而不是e。 注意,尾数采用十六进制,指数采用十进制。 指数的基数是2,而不是10。

所有的浮点数值计算都遵循IEEE 754规范

常量Double.POSITIVE_INFINITY、Double.NEGATIVE_INFINITY和Double.NaN(以及相应的Float类型的常量)分别表示这三个特殊的值,但在实际应用中很少遇到

不能这样检测一个特定值是否等于Double.NaN:

if(x==Double.Nan) // is never true

所有“非数值”的值都认为是不相同的。然而,可以使用Double.isNaN方法:

if(Double.isNaN(x))

浮点数值不适用于无法接受舍入误差的金融计算中。 例如,命令System.out.println(2.0–1.1)将打印出0.8999999999999999,而不是人们想象的0.9。 这种舍入误差的主要原因是浮点数值采用二进制系统表示,而在二进制系统中无法精确地表示分数1/10。这就好像十进制无法精确地表示分数1/3一样。 如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal


char类型

har类型原本用于表示单个字符。 不过,现在情况已经有所变化。如今,有些Unicode字符可以用一个char值描述,另外一些Unicode字符则需要两个char值。有关的详细信息请阅读下一节。

char类型的字面量值要用单引号括起来。 例如:'A'是编码值为65所对应的字符常量。它与"A"不同,"A"是包含一个字符A的字符串。 char类型的值可以表示为十六进制值,其范围从\u0000到\Uffff。例如:\u2122表示注册符号(TM),\u03C0表示希腊字母π。

除了转义序列\u之外,还有一些用于表示特殊字符的转义序列。 所有这些转义序列都可以出现在加引号的字符字面量或字符串中。例如,'\u2122'或"Hello\n"。 转义序列\u还可以出现在加引号的字符常量或字符串之外(而其他所有转义序列不可以)。例如:

// 完全符合语法规则,\u005B和\u005D是[和]的编码。
public static void main(String\u005B\u005D args)

特殊字符的转义序列

image

Unicode转义序列会在解析代码之前得到处理。例如,"\u0022+\u0022"并不是一个由引号(U+0022)包围加号构成的字符串。实际上,\u0022会在解析之前转换为",这会得到""+"",也就是一个空串。

//更隐秘地,一定要当心注释中的\u。注释
// \u00A0 is a newline
//会产生一个语法错误,因为读程序时\u00A0会替换为一个换行符
//类似地,下面这个注释
// Look inside c:\users
//也会产生一个语法错误,因为\u后面并未跟着4个十六进制数。

Unicode和char类型

要想弄清char类型,就必须了解Unicode编码机制。Unicode打破了传统字符编码机制的限制。在Unicode出现之前,已经有许多种不同的标准:美国的ASCII、西欧语言中的ISO 8859-1、俄罗斯的KOI-8、中国的GB 18030和BIG-5等。这样就产生了下面两个问题:一个是对于任意给定的代码值,在不同的编码方案下有可能对应不同的字母;二是采用大字符集的语言其编码长度有可能不同。例如,有些常用的字符采用单字节编码,而另一些字符则需要两个或更多个字节。

设计Unicode编码的目的就是要解决这些问题。在20世纪80年代开始启动设计工作时,人们认为两个字节的代码宽度足以对世界上各种语言的所有字符进行编码,并有足够的空间留给未来的扩展。在1991年发布了Unicode 1.0,当时仅占用65536个代码值中不到一半的部分。在设计Java时决定采用16位的Unicode字符集,这样会比使用8位字符集的程序设计语言有很大的改进。

十分遗憾,经过一段时间,不可避免的事情发生了。Unicode字符超过了65536个,其主要原因是增加了大量的汉语、日语和韩语中的表意文字。现在,16位的char类型已经不能满足描述所有Unicode字符的需要了。

下面利用一些专用术语解释一下Java语言解决这个问题的基本方法。从Java SE 5.0开始。码点(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码点采用十六进制书写,并加上前缀U+,例如U+0041就是拉丁字母A的码点。Unicode的码点可以分成17个代码级别(code plane)。第一个代码级别称为基本的多语言级别(basic multilingual plane),码点从U+0000到U+FFFF,其中包括经典的Unicode代码;其余的16个级别码点从U+10000到U+10FFFF,其中包括一些辅助字符(supplementary character)。

UTF-16编码采用不同长度的编码表示所有Unicode码点。在基本的多语言级别中,每个字符用16位表示,通常被称为代码单元(code unit);而辅助字符采用一对连续的代码单元进行编码。这样构成的编码值落入基本的多语言级别中空闲的2048字节内,通常被称为替代区域(surrogate area)[U+D800~U+DBFF用于第一个代码单元,U+DC00~U+DFFF用于第二个代码单元]。这样设计十分巧妙,我们可以从中迅速地知道一个代码单元是一个字符的编码,还是一个辅助字符的第一或第二部分。例如,是八元数集(http://math.ucr.edu/home/baez/octonions)的一个数学符号,码点为U+1D546,编码为两个代码单元U+D835和U+DD46。(关于编码算法的具体描述见http://en.wikipedia.org/wiki/UTF-16)。

在Java中,char类型描述了UTF-16编码中的一个代码单元。

我们强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元。最好将字符串作为抽象数据类型处理。


boolean类型

boolean(布尔)类型有两个值:false和true,用来判定逻辑条件。 整型值和布尔值之间不能进行相互转换。

在C++中,数值甚至指针可以代替boolean值。值0相当于布尔值false,非0值相当于布尔值true。在Java中则不是这样。


变量

在Java中,每个变量都有一个类型(type)。在声明变量时,变量的类型位于变量名之前

public class Print {
    // 每个声明以分号结束。由于声明是一条完整的Java语句,所以必须以分号结束。
    double salary;
    int vac;
    long earthPopulation;
    boolean done;

    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

变量名必须是一个以字母开头并由字母或数字构成的序列。 需要注意,与大多数程序设计语言相比,Java中“字母”和“数字”的范围更大。字母包括'A'~'Z'、'a'~'z'、'_'、'$'或在某种语言中表示字母的任何Unicode字符。 例如,德国的用户可以在变量名中使用字母‘’;希腊人可以用π。 同样,数字包括'0'~'9'和在某种语言中表示数字的任何Unicode字符。 但'+'和''这样的符号不能出现在变量名中,空格也不行。 变量名中所有的字符都是有意义的,并且大小写敏感。 变量名的长度基本上没有限制。

如果想要知道哪些Unicode字符属于Java中的“字母”,可以使用Character类的isJavaIdentifierStart和isJavaIdentifierPart方法来检查。

尽管$是一个合法的Java字符,但不要在你自己的代码中使用这个字符。它只用在Java编译器或其他工具生成的名字中

另外,不能使用Java保留字作为变量名

可以在一行中声明多个变量:

// 不过,不提倡使用这种风格。逐一声明每一个变量可以提高程序的可读性。
int i,j;

//许多程序员将变量名命名为类型名
Box box;

//还有一些程序员更加喜欢在变量名前加上前缀“a” Box aBox;


***
#### 变量初始化
>声明一个变量之后,必须用赋值语句对变量进行显式初始化,千万不要使用未初始化的变量。Java编译器认为语句序列是错误的

>要想对一个已经声明过的变量进行赋值,就需要将变量名放在等号(=)左侧,相应取值的Java表达式放在等号的右侧。
```java
int vac;
vac = 12;

也可以将变量的声明和初始化放在同一行中

int vac = 12;

在Java中可以将声明放在代码中的任何地方


常量

在Java中,利用关键字final指示常量。

public class Constants {
    public static void main(String[] args) {
        final double CM_PER_INCH = 2.54;
        double paperWidth = 8.5;
        double paperHeight = 11;
        System.out.println("papaer size is:" + paperWidth * CM_PER_INCH + "by" + paperHeight * CM_PER_INCH);
    }
}
papaer size is:21.59by27.94

关键字final表示这个变量只能被赋值一次。一旦被赋值之后,就不能够再更改了。 习惯上,常量名使用全大写。

在Java中,经常希望某个常量可以在一个类中的多个方法中使用,通常将这些常量称为类常量。可以使用关键字static final设置一个类常量。

类常量的定义位于main方法的外部。因此,在同一个类的其他方法中也可以使用这个常量。而且,如果一个常量被声明为public,那么其他类的方法也可以使用这个常量。

const是Java保留的关键字,但目前并没有使用。在Java中,必须使用final定义常量。


运算符

在Java中,使用算术运算符+、-、*、/表示加、减、乘、除运算。当参与/运算的两个操作数都是整数时,表示整数除法;否则,表示浮点除法。整数的求余操作(有时称为取模)用%表示。例如,15/2等于7,15%2等于1,15.0/2等于7.5。

整数被0除将会产生一个异常,而浮点数被0除将会得到无穷大或NaN结果。

可移植性是Java语言的设计目标之一。无论在哪个虚拟机上运行,同一运算应该得到同样的结果。对于浮点数的算术运算,实现这样的可移植性是相当困难的。double类型使用64位存储一个数值,而有些处理器使用80位浮点寄存器。这些寄存器增加了中间过程的计算精度。例如,以下运算: double w=x*y/z 很多Intel处理器计算x*y,并且将结果存储在80位的寄存器中,再除以z并将结果截断为64位。这样可以得到一个更加精确的计算结果,并且还能够避免产生指数溢出。但是,这个结果可能与始终在64位机器上计算的结果不一样。因此,Java虚拟机的最初规范规定所有的中间计算都必须进行截断。这种行为遭到了数值计算团体的反对。截断计算不仅可能导致溢出,而且由于截断操作需要消耗时间,所以在计算速度上实际上要比精确计算慢。为此,Java程序设计语言承认了最优性能与理想结果之间存在的冲突,并给予了改进。在默认情况下,虚拟机设计者允许对中间计算结果采用扩展的精度。但是,对于使用strictfp关键字标记的方法必须使用严格的浮点计算来生成可再生的结果。例如,可以把main方法标记为

public static strictfp void main(String[] args)

于是,在main方法中的所有指令都将使用严格的浮点计算。如果将一个类标记为strictfp,这个类中的所有方法都要使用严格的浮点计算。

实际的计算方式将取决于Intel处理器的行为。在默认情况下,中间结果允许使用扩展的指数,但不允许使用扩展的尾数(Intel芯片在截断尾数时并不损失性能)。因此,这两种方式的区别仅仅在于采用默认的方式不会产生溢出,而采用严格的计算有可能产生溢出。

如果没有仔细阅读这个注释,也没有什么关系。对大多数程序来说,浮点溢出不属于大问题。

数学函数与常量

在Math类中,包含了各种各样的数学函数

在Math类中,为了达到最快的性能,所有的方法都使用计算机浮点单元中的例程。如果得到一个完全可预测的结果比运行速度更重要的话,那么就应该使用StrictMath类。它使用“自由发布的Math库”(fdlibm)实现算法,以确保在所有平台上得到相同的结果。有关这些算法的源代码请参看www.netlib.org/fdlibm(当fdlibm为一个函数提供了多个定义时,StrictMath类就会遵循IEEE 754版本,它的名字将以“e”开头)。

数值类型之间的转换

例如,123456789是一个大整数,它所包含的位数比float类型所能够表达的位数多。当将这个整型数值转换为float类型时,将会得到同样大小的结果,但却失去了一定的精度。

int n = 123456789;
float f = n; // f is 1,234567892E8

当使用上面两个数值进行二元操作时(例如n+f,n是整数,f是浮点数),先要将两个操作数转换为同一种类型,然后再进行计算。

·如果两个操作数中有一个是double类型,另一个操作数就会转换为double类型。

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

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

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

数值类型之间的合法转换

image

强制类型转换

在必要的时候,int类型的值将会自动地转换为double类型。 但另一方面,有时也需要将double转换成int。在Java中,允许进行这种数值之间的类型转换。当然,有可能会丢失一些信息。 在这种情况下,需要通过强制类型转换(cast)实现这个操作。 强制类型转换的语法格式是在圆括号中给出想要转换的目标类型,后面紧跟待转换的变量名。例如:

double x = 9.997;
int nx = (int) x;

这样,变量nx的值为9。强制类型转换通过截断小数部分将浮点值转换为整型。

如果想对浮点数进行舍入运算,以便得到最接近的整数(在很多情况下,这种操作更有用),那就需要使用Math.round方法:

double x = 9.997;
int nx = (int) Math.round(x);

现在,变量nx的值为10。当调用round的时候,仍然需要使用强制类型转换(int)。其原因是round方法返回的结果为long类型,由于存在信息丢失的可能性,所以只有使用显式的强制类型转换才能够将long类型转换成int类型。

如果试图将一个数值从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值。例如,(byte)300的实际值为44。

不要在boolean类型与任何数值类型之间进行强制类型转换,这样可以防止发生错误。只有极少数的情况才需要将布尔类型转换为数值类型,这时可以使用条件表达式b?1:0

结合赋值和运算符

可以在赋值中使用二元运算符,这是一种很方便的简写形式 x +=4;等价于:x = x + 4; (一般地,要把运算符放在=号左边,如*=或%=)

如果运算符得到一个值,其类型与左侧操作数的类型不同,就会发生强制类型转换。例如,如果x是一个int,则以下语句 x+=3.5是合法的,将把x设置为(int)(x+3.5)。

自增与自减运算符

n++将变量n的当前值加1,n--则将n的值减1。例如,以下代码:

int n=12;
n++;

将n的值改为13。 由于这些运算符会改变变量的值,所以它们的操作数不能是数值。例如,4++就不是一个合法的语句。

这些运算符有两种形式;上面介绍的是运算符放在操作数后面的“后缀”形式。 还有一种“前缀”形式:++n。后缀和前缀形式都会使变量值加1或减1。但用在表达式中时,二者就有区别了。 前缀形式会先完成加1;而后缀形式会使用变量原来的值。

不要在表达式中使用++,因为这样的代码很容易让人困惑,而且会带来烦人的bug。

关系和boolean运算符

要检测相等性,可以使用两个等号== 3 == 7的值为false。

可以使用!=检测不相等 3!= 7的值为true。

还有经常使用的<(小于)、>(大于)、<=(小于等于)和>=(大于等于)运算符

Java沿用了C++的做法,使用&&表示逻辑“与”运算符,使用||表示逻辑“或”运算符。从!=运算符可以想到,感叹号!就是逻辑非运算符。&&和||运算符是按照“短路”方式来求值的:如果第一个操作数已经能够确定表达式的值,第二个操作数就不必计算了。如果用&&运算符合并两个表达式,exp1 && exp2而且已经计算得到第一个表达式的真值为false,那么结果就不可能为true。因此,第二个表达式就不必计算了。可以利用这一点来避免错误。

如果第一个表达式为true,expression1||expression2的值就自动为true,而无需计算第二个表达式。

Java支持三元操作符?: x<y?x:y会返回x和y中较小的一个

位运算符

处理整型类型时,可以直接对组成整型数值的各个位完成操作。这意味着可以使用掩码技术得到整数中的各个位。位运算符包括: image

这些运算符按位模式处理。例如,如果n是一个整数变量,而且用二进制表示的n从右边数第4位为1,则int f = (n & 0b1000) / 0b1000;会返回1,否则返回0。利用&并结合使用适当的2的幂,可以把其他位掩掉,而只保留其中的某一位。

应用在布尔值上时,&和|运算符也会得到一个布尔值。这些运算符与&&和||运算符很类似,不过&和|运算符不采用“短路”方式来求值,也就是说,得到计算结果之前两个操作数都需要计算。

另外,还有>>和<<运算符将位模式左移或右移。需要建立位模式来完成位掩码时,这两个运算符会很方便:int f = (n & (1 << 3)) >> 3; >>>运算符会用0填充高位,这与>>不同,它会用符号位填充高位。不存在<<<运算符。

移位运算符的右操作数要完成模32的运算(除非左操作数是long类型,在这种情况下需要对右操作数模64)。例如,1<<35的值等同于1<<3或8。

括号与运算符级别

image image

枚举类型

有时候,变量的取值只在一个有限的集合内。例如:销售的服装或比萨饼只有小、中、大和超大这四种尺寸。当然,可以将这些尺寸分别编码为1、2、3、4或S、M、L、X。但这样存在着一定的隐患。在变量中很可能保存的是一个错误的值(如0或m)。

针对这种情况,可以自定义枚举类型。枚举类型包括有限个命名的值

enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE}

现在,可以声明这种类型的变量:

Size s = Size.MEDIUM;

Size类型的变量只能存储这个类型声明中给定的某个枚举值,或者null值,null表示这个变量没有设置任何值。


字符串

从概念上讲,Java字符串就是Unicode字符序列。例如,串“Java\u2122”由5个Unicode字符J、a、v、a和TM。Java没有内置的字符串类型,而是在标准Java类库中提供了一个预定义类,很自然地叫做String。每个用双引号括起来的字符串都是String类的一个实例:

String e = "";

子串

String类的substring方法可以从一个较大的字符串提取出一个子串

String greeting = "Hello";
String s = greeting.substring(0, 3);

创建了一个由字符“Hel”组成的字符串

substring方法的第二个参数是不想复制的第一个位置。这里要复制位置为0、1和2(从0到2,包括0和2)的字符。在substring中从0开始计数,直到3为止,但不包含3。

substring的工作方式有一个优点:容易计算子串的长度。字符串s.substring(a,b)的长度为b-a。例如,子串“Hel”的长度为3-0=3。

拼接

Java语言允许使用+号连接(拼接)两个字符串 +号按照给定的次序将两个字符串拼接起来

当将一个字符串与一个非字符串的值进行拼接时,后者被转换成字符串 任何一个Java对象都可以转换成字符串

int age = 13;
String rating = "PG" + age;

rating设置为“PG13”。 这种特性通常用在输出语句中

如果需要把多个字符串放在一起,用一个定界符分隔,可以使用静态join方法:

String all =String.join("/","S","M","L","XL");
//all is the string "S / M / L / XL"

不可变字符串

String类没有提供用于修改字符串的方法。如果希望将greeting的内容修改为“Help!”,不能直接地将greeting的最后两个位置的字符修改为‘p’和‘!’。 要修改这个字符串,首先提取需要的字符,然后再拼接上替换的字符串:

// 这条语句将greeting当前值修改为“Help!” greeting = greeting.substring(0, 3) + "p!";


>由于不能修改Java字符串中的字符,所以在Java文档中将String类对象称为不可变字符串,如同数字3永远是数字3一样,字符串“Hello”永远包含字符H、e、l、l和o的代码单元序列,而不能修改其中的任何一个字符。当然,可以修改字符串变量greeting,让它引用另外一个字符串,这就如同可以将存放3的数值变量改成存放4一样。

>这样做是否会降低运行效率呢?看起来好像修改一个代码单元要比创建一个新字符串更加简洁。答案是:也对,也不对。的确,通过拼接“Hel”和“p!”来创建一个新字符串的效率确实不高。但是,不可变字符串却有一个优点:编译器可以让字符串共享。

>为了弄清具体的工作方式,可以想象将各种字符串存放在公共的存储池中。字符串变量指向存储池中相应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。

>总而言之,Java的设计者认为共享带来的高效率远远胜过于提取、拼接字符串所带来的低效率。查看一下程序会发现:很少需要修改字符串,而是往往需要对字符串进行比较(有一种例外情况,将来自于文件或键盘的单个字符或较短的字符串汇集成字符串。为此,Java提供了一个独立的类

#### 检测字符串是否相等
>可以使用equals方法检测两个字符串是否相等
```java
s.eauals(t);

如果字符串s与字符串t相等,则返回true;否则,返回false。需要注意,s与t可以是字符串变量,也可以是字符串字面量

"hello".equals(greeting);

要想检测两个字符串是否相等,而不区分大小写,可以使用equalsIgnoreCase方法

"hello".equalsIgnoreCase("hello");

一定不要使用==运算符检测两个字符串是否相等!这个运算符只能够确定两个字符串是否放置在同一个位置上。当然,如果字符串放置在同一个位置上,它们必然相等。但是,完全有可能将内容相同的多个字符串的拷贝放置在不同的位置上。

如果虚拟机始终将相同的字符串共享,就可以使用==运算符检测是否相等。但实际上只有字符串常量是共享的,而+或substring等操作产生的结果并不是共享的。因此,千万不要使用==运算符测试字符串的相等性,以免在程序中出现糟糕的bug。从表面上看,这种bug很像随机产生的间歇性错误。

 String greeting = "Hello";if(greeting=="Hello")
    {
        // 可能为 true
    }if(greeting.substring(0,3)=="Hel")
    {
        // 可能为 false
    }

空串与Null串

空串""是长度为0的字符串。可以调用以下代码检查一个字符串是否为空:

if (str.length() == 0)

if(str.equals(""))

空串是一个Java对象,有自己的串长度(0)和内容(空)。不过,String变量还可以存放一个特殊的值,名为null,这表示目前没有任何对象与该变量关联 要检查一个字符串是否为null,要使用以下条件:

if(str == null)

有时要检查一个字符串既不是null也不为空串,这种情况下就需要使用以下条件:

if(str != null && str.length() != 0)

首先要检查str不为null。如果在一个null值上调用方法,会出现错误

码点与代码单元

Java字符串由char值序列组成。char数据类型是一个采用UTF-16编码表示Unicode码点的代码单元。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。

length方法将返回采用UTF-16编码表示的给定字符串所需要的代码单元数量。例如

String greeting = "Hello";
int n = greeting.length();// 5

要想得到实际的长度,即码点数量,可以调用:

String greeting = "Hello";
int cpCount = greeting.codePointCount(0, greeting.length());

调用s.charAt(n)将返回位置n的代码单元,n介于0~s.length()-1之间。

String greeting = "Hello";
char first = greeting.charAt(0);// H
char last = greeting.charAt(4);// o

要想得到第i个码点,应该使用下列语句

String greeting = "Hello";
int i = 0;
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);

Java对字符串中的代码单元和码点从0开始计数。

String API

Java中的String类包含了50多个方法 每一个API的注释都以形如java.lang.String的类名开始。

·char charAt(int index)

返回给定位置的代码单元。除非对底层的代码单元感兴趣,否则不需要调用这个方法。

·int codePointAt(int index)5.0

返回从给定位置开始的码点。

·int offsetByCodePoints(int startIndex,int cpCount)5.0

返回从startIndex代码点开始,位移cpCount后的码点索引。

·int compareTo(String other)

按照字典顺序,如果字符串位于other之前,返回一个负数;如果字符串位于other之后,返回一个正数;如果两个字符串相等,返回0。

·IntStream codePoints()8

将这个字符串的码点作为一个流返回。调用toArray将它们放在一个数组中。

·new String(int[]codePoints,int offset,int count)5.0

用数组中从offset开始的count个码点构造一个字符串。

·boolean equals(Object other)

如果字符串与other相等,返回true。

·boolean equalsIgnoreCase(String other)

如果字符串与other相等(忽略大小写),返回true。

·boolean startsWith(String prefix)

·boolean endsWith(String suffix)

如果字符串以suffix开头或结尾,则返回true。

·int index0f(String str)

·int index0f(String str,int fromIndex)

·int index0f(int cp)

·int index0f(int cp,int fromIndex)

返回与字符串str或代码点cp匹配的第一个子串的开始位置。这个位置从索引0或fromIndex开始计算。如果在原始串中不存在str,返回-1。

·int lastIndex0f(String str)

·int lastIndex0f(String str,int fromIndex)

·int lastindex0f(int cp)

·int lastindex0f(int cp,int fromIndex)

返回与字符串str或代码点cp匹配的最后一个子串的开始位置。这个位置从原始串尾端或fromIndex开始计算。

·int length()

返回字符串的长度。

·int codePointCount(int startIndex,int endIndex)5.0

返回startIndex和endIndex-1之间的代码点数量。没有配成对的代用字符将计入代码点。

·String replace(CharSequence oldString,CharSequence newString)

返回一个新字符串。这个字符串用newString代替原始字符串中所有的oldString。可以用String或StringBuilder对象作为CharSequence参数。

·String substring(int beginIndex)

·String substring(int beginIndex,int endIndex)

返回一个新字符串。这个字符串包含原始字符串中从beginIndex到串尾或endIndex–1的所有代码单元。

·String toLowerCase()

·String toUpperCase()

返回一个新字符串。这个字符串将原始字符串中的大写字母改为小写,或者将原始字符串中的所有小写字母改成了大写字母。

·String trim()

返回一个新字符串。这个字符串将删除了原始字符串头部和尾部的空格。

·String join(CharSequence delimiter,CharSequence...elements)8

返回一个新字符串,用给定的定界符连接所有元素。

在API注释中,有一些CharSequence类型的参数。这是一种接口类型,所有字符串都属于这个接口 现在只需要知道只要看到一个CharSequence形参,完全可以传入String类型的实参。

构建字符串

每次连接字符串,都会构建一个新的String对象,既耗时,又浪费空间。使用StringBuilder类就可以避免这个问题的发生。

如果需要用许多小段的字符串构建一个字符串,那么应该按照下列步骤进行。首先,构建一个空的字符串构建器:

StringBuilder builder = new StringBuilder();

当每次需要添加一部分内容时,就调用append方法。

builder.append("ch");
builder.append("ch2");

在需要构建字符串时就调用toString方法,将可以得到一个String对象,其中包含了构建器中的字符序列。

String full = builder.toString();

在JDK5.0中引入StringBuilder类。这个类的前身是StringBuffer,其效率稍有些低,但允许采用多线程的方式执行添加或删除字符的操作。如果所有字符串在一个单线程中编辑(通常都是这样),则应该用StringBuilder替代它。这两个类的API是相同的。

java.lang.StringBuilder 5.0

·StringBuilder()

构造一个空的字符串构建器。

·int length()

返回构建器或缓冲器中的代码单元数量。

·StringBuilder append(String str)

追加一个字符串并返回this。

·StringBuilder append(char c)

追加一个代码单元并返回this。

·StringBuilder appendCodePoint(int cp)

追加一个代码点,并将其转换为一个或两个代码单元并返回this。

·void setCharAt(int i,char c)

将第i个代码单元设置为c。

·StringBuilder insert(int offset,String str)

在offset位置插入一个字符串并返回this。

·StringBuilder insert(int offset,Char c)

在offset位置插入一个代码单元并返回this。

·StringBuilder delete(int startIndex,int endIndex)

删除偏移量从startIndex到-endIndex-1的代码单元并返回this。

·String toString()

返回一个与构建器或缓冲器内容相同的字符串。


输入输出

读取输入

打印输出到“标准输出流”(即控制台窗口)是一件非常容易的事情,只要调用System.out.println即可

要想通过控制台进行输入,首先需要构造一个Scanner对象,并与“标准输入流”System.in关联。


import java.util.Scanner;

Scanner in = new Scanner(System.in);

>现在,就可以使用Scanner类的各种方法实现输入操作了。例如,nextLine方法将输入一行。
```java
        Scanner in = new Scanner(System.in);
        System.out.print("what is you name?");
        String name = in.nextLine();

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

String firstName = in.next();

要想读取一个整数,就调用nextInt方法

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

要想读取下一个浮点数,就调用nextDouble方法。

Scanner类定义在java.util包中。当使用的类不是定义在基本java.lang包中时,一定要使用import指示字将相应的包加载进来。

import java.util.*;

/**
 * This program demonstrates console input.
 * @version 1.10 2004-02-10
 * @author Cay Horstmann
 */
public class InputTest
{
   public static void main(String[] args)
   {
      Scanner in = new Scanner(System.in);

      // get first input
      System.out.print("What is your name? ");
      String name = in.nextLine();

      // get second input
      System.out.print("How old are you? ");
      int age = in.nextInt();

      // display output on console
      System.out.println("Hello, " + name + ". Next year, you'll be " + (age + 1));
   }
}

>因为输入是可见的,所以Scanner类不适用于从控制台读取密码。Java SE 6特别引入了Console类实现这个目的。要想读取一个密码,可以采用下列代码:
```java
import java.io.Console;

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

为了安全起见,返回的密码存放在一维字符数组中,而不是字符串中。在对密码进行处理之后,应该马上用一个填充值覆盖数组元素(数组处理将在3.10节介绍)。

采用Console对象处理输入不如采用Scanner方便。每次只能读取一行输入,而没有能够读取一个单词或一个数值的方法。

java.util.Scanner 5.0

·Scanner(InputStream in)

用给定的输入流创建一个Scanner对象。

·String nextLine()

读取输入的下一行内容。

·String next()

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

·int nextInt()

·double nextDouble()

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

·boolean hasNext()

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

·boolean hasNextInt()

·boolean hasNextDouble()

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

java.lang.System 1.0

·static Console console()6

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

java.io.Console 6

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

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

显示字符串prompt并且读取用户输入,直到输入行结束。args参数可以用来提供输入格式。有关这部分内容将在下一节中介绍。

格式化输出

System.out.print(x)将数值x输出到控制台上。这条命令将以x对应的数据类型所允许的最大非0数字位数打印输出x

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

printf方法,可以用8个字符的宽度和小数点后两个字符的精度打印x。也就是说,打印输出一个空格和7个字符

double x = 10000.0 / 3.0;
System.out.printf("%8.2f", x);// 3333.33

在printf中,可以使用多个参数 每一个以%字符开始的格式说明符都用相应的参数替换。格式说明符尾部的转换符将指示被格式化的数值类型:f表示浮点数,s表示字符串,d表示十进制整数。


double x = 10000.0 / 3.0;
    System.out.printf("Hello,%s,Next year,you well be %d", "harry", 20);//Hello,harry,Next year,you well be 20

>用于printf的转换符
![image](https://user-images.githubusercontent.com/30850497/118459205-1abe7480-b72e-11eb-916d-7baab1449bfe.png)

>还可以给出控制格式化输出的各种标志。例如,逗号标志增加了分组的分隔符
```java
System.out.printf("%,.2f", 10000.0 / 3.0);// 3,333.33

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

用于printf的标志

image

可以使用s转换符格式化任意的对象。对于任意实现了Formattable接口的对象都将调用formatTo方法;否则将调用toString方法,它可以将对象转换为字符串。

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

String message = String.format("Hello,%s . Next year,you well be %d", "harry", 20);

日期和时间的转换符 image image 可以看到,某些格式只给出了指定日期的部分信息。例如,只有日期或月份。如果需要多次对日期操作才能实现对每一部分进行格式化的目的就太笨拙了。为此,可以采用一个格式化的字符串指出要被格式化的参数索引。索引必须紧跟在%后面,并以$终止

// 在新代码中,应当使用java.time包的方法
System.out.printf("%1$s %2$tB %2$te, %2$tY", "due date:", new Date());

还可以选择使用<标志。它指示前面格式说明中的参数将被再次使用。也就是说,下列语句将产生与前面语句同样的输出结果:

// 在新代码中,应当使用java.time包的方法
System.out.printf("%s %tB %<te, %<tY", "due date:", new Date());

参数索引值从1开始,而不是从0开始,%1$...对第1个参数格式化。这就避免了与0标志混淆。

格式说明符语法 image

许多格式化规则是本地环境特有的。例如,在德国,组分隔符是句号而不是逗号,Monday被格式化为Montag。

文件输入与输出

对文件进行读取,就需要一个用File对象构造一个Scanner对象


import java.nio.file.Paths;
import java.util.Scanner;

public class File { public static void main() throws IOException { Scanner in = new Scanner(Paths.get("myfile.txt"), "UTF-8"); } }

>如果文件名中包含反斜杠符号,就要记住在每个反斜杠之前再加一个额外的反斜杠:```“c:\\mydirectory\\myfile.txt”```。
>在这里指定了UTF-8字符编码,这对于互联网上的文件很常见(不过并不是普遍适用)。读取一个文本文件时,要知道它的字符编码。如果省略字符编码,则会使用运行这个Java程序的机器的“默认编码”。这不是一个好主意,如果在不同的机器上运行这个程序,可能会有不同的表现。

>现在,就可以利用前面介绍的任何一个Scanner方法对文件进行读取。

>要想写入文件,就需要构造一个PrintWriter对象。在构造器中,只需要提供文件名:
```java
PrintWriter out = new PrintWriter("myfile.txt", "UTF-8");

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

可以构造一个带有字符串参数的Scanner,但这个Scanner将字符串解释为数据,而不是文件名。例如,如果调用:

Scanner in2 = new Scanner("myfile.txt"); // error

这个scanner会将参数作为包含10个字符的数据:‘m’,‘y’,‘f’等。在这个示例中所显示的并不是人们所期望的效果

当指定一个相对文件名时,例如,“myfile.txt”,“mydirectory/myfile.txt”或“../myfile.txt”,文件位于Java虚拟机启动路径的相对位置。如果在命令行方式下用下列命令启动程序: java myProg 启动路径就是命令解释器的当前路径。然而,如果使用集成开发环境,那么启动路径将由IDE控制。 可以使用下面的调用方式找到路径的位置:

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

如果觉得定位文件比较烦恼,则可以考虑使用绝对路径,例如:“c:\\mydirectory\\myfile.txt”或者“/home/me/mydirectory/myfile.txt”

正如读者所看到的,访问文件与使用System.in和System.out一样容易。要记住一点:如果用一个不存在的文件构造一个Scanner,或者用一个不能被创建的文件名构造一个PrintWriter,那么就会发生异常。Java编译器认为这些异常比“被零除”异常更严重。 正如读者所看到的,访问文件与使用System.in和System.out一样容易。要记住一点:如果用一个不存在的文件构造一个Scanner,或者用一个不能被创建的文件名构造一个PrintWriter,那么就会发生异常。Java编译器认为这些异常比“被零除”异常更严重。


import java.nio.file.Paths;
import java.util.Scanner;

public class File { public static void main() throws IOException { Scanner in = new Scanner(Paths.get("myfile.txt"), "UTF-8"); } }


>当采用命令行方式启动一个程序时,可以利用Shell的重定向语法将任意文件关联到System.in和System.out:

```bash
java MyProg < myfile > output.txt

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

java.util.Scanner 5.0

·Scanner(File f)

构造一个从给定文件读取数据的Scanner。

·Scanner(String data)

构造一个从给定字符串读取数据的Scanner。

java.io.PrintWriter 1.1

·PrintWriter(String fileName)

构造一个将数据写入文件的PrintWriter。文件名由参数指定。

java.nio.file.Paths 7

·static Path get(String pathname)

根据给定的路径名构造一个Path。


控制流程

Java使用条件语句和循环结构确定控制流程

块作用域

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


public class Block {
public static void main() {
int n;
{
int k;
}
}
}
不能在嵌套的两个块中声明同名的变量

条件语句

循环

当条件为true时,while循环执行一条语句(也可以是一个语句块)。一般格式为 while (condition) statement 如果开始循环条件的值就为false,则while循环体一次也不执行

如果希望循环体至少执行一次,则应该将检测条件放在最后。使用do/while循环语句可以实现这种操作方式。它的语法格式为: do statement while (condition) 这种循环语句先执行语句(通常是一个语句块),再检测循环条件;然后重复语句,再检测循环条件,以此类推。

确定循环

for循环语句是支持迭代的一种通用结构,利用每次迭代之后更新的计数器或类似的变量来控制迭代次数。

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

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

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

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

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

可能永远不会结束。由于舍入的误差,最终可能得不到精确值。例如,在上面的循环中,因为0.1无法精确地用二进制表示,所以,x将从9.99999999999998跳到10.09999999999998。

当在for语句的第1部分中声明了一个变量之后,这个变量的作用域就为for循环的整个循环体

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

可以在各自独立的不同for循环中定义同名的变量:

for循环语句只不过是while循环的一种简化形式。

多重选择:switch语句

switch语句将从与选项值相匹配的case标签处开始执行直到遇到break语句,或者执行到switch语句的结束处为止。如果没有相匹配的case标签,而有default子句,就执行这个子句。

有可能触发多个case分支。如果在case分支语句的末尾没有break语句,那么就会接着执行下一个case分支语句。这种情况相当危险,常常会引发错误。为此,我们在程序中从不使用switch语句。

如果你比我们更喜欢switch语句,编译代码时可以考虑加上-Xlint:fallthrough选项 java -Xlint:fallthrough Test.java

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

case标签可以是:

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

·枚举常量。

·从Java SE 7开始,case标签还可以是字符串字面量。

当在switch语句中使用枚举常量时,不必在每个标签中指明枚举名,可以由switch的表达式值确定。

中断控制流程语句

不带标签的break语句。与用于退出switch语句的break语句一样,它也可以用于退出循环语句。

带标签的break语句,用于跳出多重嵌套的循环语句。 标签必须放在希望跳出的最外层循环之前,并且必须紧跟一个冒号。 只能跳出语句块,而不能跳入语句块。

continue语句。与break语句一样,它将中断正常的控制流程。continue语句将控制转移到最内层循环的首部 continue语句越过了当前循环体的剩余部分,立刻跳到循环首部 如果将continue语句用于for循环中,就可以跳到for循环的“更新”部分 带标签的continue语句,将跳到与标签匹配的循环首部。


大数值

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

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


import java.math.BigInteger;
BigInteger a=BigInteger.valueOf(100);
>不能使用人们熟悉的算术运算符(如:+和*)处理大数值。而需要使用大数值类中的add和multiply方法。

```java
        BigInteger a = BigInteger.valueOf(100);
        BigInteger b = BigInteger.valueOf(10);
        BigInteger c = a.add(b);

java.math.BigInteger 1.1

·BigInteger add(BigInteger other)

·BigInteger subtract(BigInteger other)

·BigInteger multiply(BigInteger other)

·BigInteger divide(BigInteger other)

·BigInteger mod(BigInteger other)

返回这个大整数和另一个大整数other的和、差、积、商以及余数。

·int compareTo(BigInteger other)

如果这个大整数与另一个大整数other相等,返回0;如果这个大整数小于另一个大整数other,返回负数;否则,返回正数。

·static BigInteger valueOf(long x)

返回值等于x的大整数。

·BigDecimal add(BigDecimal other)

·BigDecimal subtract(BigDecimal other)

·BigDecimal multiply(BigDecimal other)

·BigDecimal divide(BigDecimal other,RoundingMode mode)5.0

返回这个大实数与另一个大实数other的和、差、积、商。要想计算商,必须给出舍入方式(rounding mode)。RoundingMode.HALF_UP是在学校中学习的四舍五入方式(即,数值0到4舍去,数值5到9进位)。它适用于常规的计算。有关其他的舍入方式请参看API文档。

·int compareTo(BigDecimal other)

如果这个大实数与另一个大实数相等,返回0;如果这个大实数小于另一个大实数,返回负数;否则,返回正数。

·static BigDecimal valueOf(long x)

·static BigDecimal valueOf(long x,int scale)

返回值为x或x/10scale的一个大实数。

数组

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

在声明数组变量时,需要指出数组类型(数据元素类型紧跟[])和数组变量的名字。

声明了整型数组a 只声明了变量a,并没有将a初始化为一个真正的数组

int[] a;

使用new运算符创建数组。 这条语句创建了一个可以存储100个整数的数组。数组长度不要求是常量:new int[n]会创建一个长度为n的数组。

int[] a= new int[100];

可以使用下面两种形式声明数组


int[] a; // 将类型int[](整型数组)与变量名分开

int b[];

>这个数组的下标从0~99(不是1~100)。一旦创建了数组,就可以给数组元素赋值。

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

>创建一个包含10个字符串的数组,所有字符串都为null
```java
String[] names = new String[10];

如果创建了一个100个元素的数组,并且试图访问元素a[100](或任何在0~99之外的下标),程序就会引发“array index out of bounds”异常而终止执行。

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

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

for each循环

Java有一种功能很强的循环结构,可以用来依次处理数组中的每个元素(其他类型的元素集合亦可)而不必为指定下标值而分心。

for (variable : collection) statement

定义一个变量用于暂存集合中的每一个元素,并执行相应的语句(当然,也可以是语句块)。>collection这一集合表达式必须是一个数组或者是一个实现了Iterable接口的类对象(例如ArrayList)。

循环a中的每一个元素”(for each element in a)

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

更加简单的方式打印数组中的所有值,即利用Arrays类的toString方法

数组初始化以及匿名数组

创建数组对象并同时赋予初始值的简化书写形式 在使用这种语句时,不需要调用new。

int[] small = { 1, 2, 3, 4, 5 };

可以初始化一个匿名的数组:

new int[] {1, 2, 3, 4, 5};

允许数组长度为0 数组长度为0与null不同

数组拷贝

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

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

int[] d=Arrays.copyOf(small, 10);

第2个参数是新数组的长度。这个方法通常用来增加数组的大小 如果数组元素是数值型,那么多余的元素将被赋值为0;如果数组元素是布尔型,则将赋值为false。相反,如果长度小于原始数组的长度,则只拷贝最前面的数据元素。

命令行参数

每一个Java应用程序都有一个带String arg[]参数的main方法。这个参数表明main方法将接收一个字符串数组,也就是命令行参数。

数组排序

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

Arrays.sort(a);

这个方法使用了优化的快速排序算法。快速排序算法对于大多数数据集合来说都是效率比较高的。

java.util.Arrays 1.2

·static String toString(type[]a)5.0

返回包含a中数据元素的字符串,这些数据元素被放在括号内,并用逗号分隔。

参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

·static type copyOf(type[]a,int length)6

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

返回与a类型相同的一个数组,其长度为length或者end-start,数组元素为a的值。

参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

start 起始下标(包含这个值)。

end 终止下标(不包含这个值)。这个值可能大于a.length。在这种情况下,结果为0或false。

length 拷贝的数据元素长度。如果length值大于a.length,结果为0或false;否则,数组中只有前面length个数据元素的拷贝值。

·static void sort(type[]a)

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

参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

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

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

采用二分搜索算法查找值v。如果查找成功,则返回相应的下标值;否则,返回一个负数值r。-r-1是为保持a有序v应插入的位置。

参数:a 类型为int、long、short、char、byte、boolean、float或double的有序数组。

start 起始下标(包含这个值)。

end 终止下标(不包含这个值)。

v 同a的数据元素类型相同的值。

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

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

参数:a 类型为int、long、short、char、byte、boolean、float或double的数组。

v 与a数据元素类型相同的一个值。

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

如果两个数组大小相同,并且下标相同的元素都对应相等,返回true。

参数:a、b 类型为int、long、short、char、byte、boolean、float或double的两个数组。

多维数组

多维数组将使用多个下标访问数组元素,它适用于表示表格或更加复杂的排列形式。

声明一个二维数组

double[][] b;

与一维数组一样,在调用new对多维数组进行初始化之前不能使用它

b = new double[2][3];

如果知道数组元素,就可以不调用new,而直接使用简化的书写形式对多维数组进行初始化

int[][] square=
{
{1,2},
{3,4}
};

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

for each循环语句不能自动处理二维数组的每一个元素。它是按照行,也就是一维数组处理的。要想访问二维数组a的所有元素,需要使用两个嵌套的循环

快速地打印一个二维数组的数据元素列表

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

不规则数组

Java实际上没有多维数组,只有一维数组。多维数组被解释为“数组的数组。”

可以方便地构造一个“不规则”数组,即数组的每一行有不同的长度。

WangShuXian6 commented 3 years ago

对象与类

面向对象程序设计概述

面向对象程序设计与面向过程程序设计在思维方式上存在着很大的差别。

面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。

传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。 一旦确定了这些过程,就要开始考虑存储数据的方式。 这就是Pascal语言的设计者Niklaus Wirth将其著作命名为《算法+数据结构=程序》(Algorithms+Data Structures=Programs,Prentice Hall,1975)的原因。 需要注意的是,在Wirth命名的书名中,算法是第一位的,数据结构是第二位的,这就明确地表述了程序员的工作方式。首先要确定如何操作数据,然后再决定如何组织数据,以便于数据操作。 而OOP却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算法。

对于一些规模较小的问题,将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。

类(class)是构造对象的模板或蓝图。 我们可以将类想象成制作小甜饼的切割机,将对象想象为小甜饼。 由类构造(construct)对象的过程称为创建类的实例(instance)。

用Java编写的所有代码都位于某个类的内部。

封装(encapsulation,有时称为数据隐藏)是与对象有关的一个重要概念。

对象中的数据称为实例域(instance field),操纵数据的过程称为方法(method)。对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态(state)。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。

实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。

对象

对象的三个主要特性:

·对象的行为(behavior)——可以对对象施加哪些操作,或可以对对象施加哪些方法?

·对象的状态(state)——当施加那些方法时,对象如何响应?

·对象标识(identity)——如何辨别具有相同行为与状态的不同对象?

每个对象都保存着描述当前特征的信息。这就是对象的状态。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。对象状态的改变必须通过调用方法实现

对象的状态影响它的行为(如果一个订单“已送货”或“已付款”,就应该拒绝调用具有增删订单中条目的方法。反过来,如果订单是“空的”,即还没有加入预订的物品,这个订单就不应该进入“已送货”状态)。

识别类

识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。

例如,在订单处理系统中,有这样一些名词:

·商品(Item)

·订单(Order)

·送货地址(Shipping address)

·付款(Payment)

·账户(Account)

这些名词很可能成为类Item、Order等。

接下来,查看动词:商品被添加到订单中,订单被发送或取消,订单货款被支付。对于每一个动词如:“添加”、“发送”、“取消”以及“支付”,都要标识出主要负责完成相应动作的对象。例如,当一个新的商品添加到订单中时,那个订单对象就是被指定的对象,因为它知道如何存储商品以及如何对商品进行排序。也就是说,add应该是Order类的一个方法,而Item对象是一个参数。

类之间的关系

在类之间,最常见的关系有

·依赖(“uses-a”)

·聚合(“has-a”)

·继承(“is-a”)

表达类关系的UML符号 image

依赖(dependence)

即“uses-a”关系,是一种最明显的、最常见的关系。 例如,Order类使用Account类是因为Order对象需要访问Account对象查看信用状态。 但是Item类不依赖于Account类,这是因为Item对象与客户账户无关。 因此,如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。

应该尽可能地将相互依赖的类减至最少。 如果类A不知道B的存在,它就不会关心B的任何改变(这意味着B的改变不会导致A产生任何bug)。用软件工程的术语来说,就是让类之间的耦合度最小。

聚合(aggregation)

即“has-a”关系,是一种具体且易于理解的关系。 例如,一个Order对象包含一些Item对象。聚合关系意味着类A的对象包含类B的对象。

有些方法学家不喜欢聚合这个概念,而更加喜欢使用“关联”这个术语。从建模的角度看,这是可以理解的。但对于程序员来说,“has-a”显得更加形象。 喜欢使用聚合的另一个理由是关联的标准符号不易区分

继承(inheritance)

即“is-a”关系,是一种用于表示特殊与一般关系的。 例如,Rush Order类由Order类继承而来。 在具有特殊性的RushOrder类中包含了一些用于优先处理的特殊方法,以及一个计算运费的不同方法;而其他的方法,如添加商品、生成账单等都是从Order类继承来的。 一般而言,如果类A扩展类B,类A不但包含从类B继承的方法,还会拥有一些额外的功能

类图 image

使用预定义类

不是所有的类都具有面向对象特征。 例如,Math类。在程序中,可以使用Math类的方法,如Math.random,并只需要知道方法名和参数(如果有的话),而不必了解它的具体实现过程。 这正是封装的关键所在,当然所有类都是这样。但遗憾的是,Math类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。

对象与对象变量

要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象应用方法。

在Java程序设计语言中,使用构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。

构造器的名字应该与类名相同。

一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。

可以显式地将对象变量设置为null,表明这个对象变量目前没有引用任何对象。

如果将一个方法应用于一个值为null的对象上,那么就会产生运行时错误。

局部变量不会自动地初始化为null,而必须通过调用new或将它们设置为null进行初始化。

Java类库中的LocalDate类

时间是用距离一个固定时间点的毫秒数(可正可负)表示的,这个点就是所谓的纪元(epoch),它是UTC时间1970年1月1日00:00:00。 UTC是Coordinated Universal Time的缩写,与大家熟悉的GMT(即Greenwich Mean Time,格林威治时间)一样,是一种具有实践意义的科学标准时间。

标准Java类库分别包含了两个类:一个是用来表示时间点的Date类;另一个是用来表示大家熟悉的日历表示法的LocalDate类。

不要使用构造器来构造LocalDate类的对象。实际上,应当使用静态工厂方法(factory method)代表你调用构造器。

import java.time.LocalDate;

LocalDate.now();

更改器方法与访问器方法

一个更改器方法(mutator method)。调用这个方法后,对象的状态会改变

只访问对象而不修改对象的方法有时称为访问器方法(accessor method)

java.time.LocalDate 8

·static LocalTime now()

构造一个表示当前日期的对象。

·static LocalTime of(int year,int month,int day)

构造一个表示给定日期的对象。

·int getYear()

·int getMonthValue()

·int getDayOfMonth()

得到当前日期的年、月和日。

·DayOfWeek getDayOfWeek

得到当前日期是星期几,作为DayOfWeek类的一个实例返回。调用getValue来得到1~7之间的一个数,表示这是星期几,1表示星期一,7表示星期日。

·LocalDate plusDays(int n)

·LocalDate minusDays(int n)

生成当前日期之后或之前n天的日期。


用户自定义类

主力类(workhorse class)。通常,这些类没有main方法,却有自己的实例域和实例方法。要想创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。

Employee类

在Java中,最简单的类定义形式为:

image

薪金管理系统


import java.time.*;

/**

class Employee { private String name; private double salary; private LocalDate hireDay;

public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); }

public String getName() { return name; }

public double getSalary() { return salary; }

public LocalDate getHireDay() { return hireDay; }

public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }

>示例程序中包含两个类:Employee类和带有public访问修饰符的EmployeeTest类。EmployeeTest类包含了main方法

>源文件名是EmployeeTest.java,这是因为文件名必须与public类的名字相匹配。
>在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。

>当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class和Employee.class。

>将程序中包含main方法的类名提供给字节码解释器,以便启动这个程序:```java EmployeeTest```
>字节码解释器开始运行EmployeeTest类的main方法中的代码。在这段代码中,先后构造了三个新Employee对象,并显示它们的状态。

### 多个源文件的使用
>在以上程序清单中,一个源文件包含了两个类。
>许多程序员习惯于将每一个类存在一个单独的源文件中。
>例如,将Employee类存放在文件Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。

>如果喜欢这样组织文件,将可以有两种编译源程序的方法。一种是使用通配符调用Java编译器:```javac Employee*.java```
>于是,所有与通配符匹配的源文件都将被编译成类文件

>或者键入下列命令:```javac EmployeeTest.java```
>使用第二种方式,并没有显式地编译Employee.java。
>然而,当Java编译器发现EmployeeTest.java使用了Employee类时会查找名为Employee.class的文件。
>如果没有找到这个文件,就会自动地搜索Employee.java,然后,对它进行编译。
>更重要的是:如果Employee.java版本较已有的Employee.class文件版本新,Java编译器就会自动地重新编译这个文件。

### 剖析Employee类
>这个类包含一个构造器和4个方法:
```java
public Employee(String n, double s, int year, int month, int day)

public String getName()

public double getSalary()

public LocalDate getHireDay()

public void raiseSalary(double byPercent)

这个类的所有方法都被标记为public。关键字public意味着任何类的任何方法都可以调用这些方法

在Employee类的实例中有三个实例域用来存放将要操作的数据:

private String name;
private double salary;
private LocalDate hireDay;

关键字private确保只有Employee类自身的方法能够访问这些实例域,而其他类的方法不能够读写这些域。

可以用public标记实例域,但这是一种极为不提倡的做法。public数据域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。任何类的任何方法都可以修改public域,从我们的经验来看,某些代码将使用这种存取权限,而这并不我们所希望的,因此,这里强烈建议将实例域标记为private。

有两个实例域本身就是对象:name域是String类对象,hireDay域是LocalDate类对象。这种情形十分常见:类通常包括类型属于某个类类型的实例域。

从构造器开始

Employee类的构造器:

public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}

可以看到,构造器与类同名。在构造Employee类的对象时,构造器会运行,以便将实例域初始化为所希望的状态。 构造器与其他的方法有一个重要的不同。构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。

·构造器与类同名

·每个类可以有一个以上的构造器

·构造器可以有0个、1个或多个参数

·构造器没有返回值

·构造器总是伴随着new操作一起调用

Java构造器的工作方式与C++一样。但是,要记住所有的Java对象都是在堆中构造的,构造器总是伴随着new操作符一起使用。

不要在构造器中定义与实例域重名的局部变量。

隐式参数与显式参数

方法用于操作对象以及存取它们的实例域。 例如,方法raiseSalary将调用这个方法的对象的salary实例域设置为新值。

public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}

raiseSalary方法有两个参数。 第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee类对象。 第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参数。(有些人把隐式参数称为方法调用的目标或接收者。) 显式参数是明显地列在方法声明中的,例如double byPercent。隐式参数没有出现在方法声明中。

在每一个方法中,关键字this表示隐式参数。如果需要的话,可以用下列方式编写raiseSalary方法: 这样可以将实例域与局部变量明显地区分开来。

public void raiseSalary(double byPercent)
{
double raise = this.salary * byPercent / 100;
this.salary += raise;
}

在Java中,所有的方法都必须在类的内部定义,但并不表示它们是内联方法。 是否将某个方法设置为内联方法是Java虚拟机的任务。即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法。

封装的优点

访问器方法。由于它们只返回实例域值,因此又称为域访问器。


public String getName()
{
return name;
}

public double getSalary() { return salary; }

public LocalDate getHireDay() { return hireDay; }

>在有些时候,需要获得或设置实例域的值。因此,应该提供下面三项内容:

·一个私有的数据域;

·一个公有的域访问器方法;

·一个公有的域更改器方法。

>这样做要比提供一个简单的公有数据域复杂些,但是却有着下列明显的好处:

>首先,可以改变内部实现,除了该类的方法之外,不会影响其他代码。
>第二点好处:更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。例如,setSalary方法可以检查薪金是否小于0。

>注意不要编写返回引用可变对象的访问器方法。在Employee类中就违反了这个设计原则,其中的getHireDay方法返回了一个Date类对象:
```java
class Employee
{
   private Date hireDay;

   public Date getHireDay()
   {
      return hireDay;
   }
}

LocalDate类没有更改器方法,与之不同,Date类有一个更改器方法setTime,可以在这里设置毫秒数。

Date对象是可变的,这一点就破坏了封装性!请看下面这段代码: image 出错的原因很微妙。d和harry.hireDay引用同一个对象。对d调用更改器方法就可以自动地改变这个雇员对象的私有状态!

返回可变数据域的引用 image

如果需要返回一个可变对象的引用,应该首先对它进行克隆(clone)。对象clone是指存放在另一个位置上的对象副本。有关对象clone的详细内容将在第6章中讨论。下面是修改后的代码:

class Employee
{
   private Date hireDay;

   public Date getHireDay()
   {
      return (Date) hireDay.clone();
   }
}

如果需要返回一个可变数据域的拷贝,就应该使用clone。

基于类的访问权限

方法可以访问所调用对象的私有数据。一个方法可以访问所属类的所有对象的私有数据

比较两个雇员的equals方法。


public class Employee {
private String name;
public boolean equals(Employee other) {
    return name.equals(other.name);
}

}

>典型的调用方式是
```java
if(harry.equals(boss)){};

这个方法访问harry的私有域,这点并不会让人奇怪,然而,它还访问了boss的私有域。这是合法的,其原因是boss是Employee类对象,而Employee类的方法可以访问Employee类的任何一个对象的私有域。

私有方法

在实现一个类时,由于公有数据非常危险,所以应该将所有的数据域都设置为私有的。然而,方法又应该如何设计呢?尽管绝大多数方法都被设计为公有的,但在某些特殊情况下,也可能将它们设计为私有的。有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为private的。

在Java中,为了实现一个私有的方法,只需将关键字public改为private即可。

对于私有方法,如果改用其他方法实现相应的操作,则不必保留原有的方法。如果数据的表达方式发生了变化,这个方法可能会变得难以实现,或者不再需要。然而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的,就不能将其删去,因为其他的代码很可能依赖它。

final实例域

可以将实例域定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。例如,可以将Employee类中的name域声明为final,因为在对象构建之后,这个值不会再被修改,即没有setName方法。

public class Employee {
    private final String name;

final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。

对于可变的类,使用final修饰符可能会对读者造成混乱。

private final StringBuilder eva;

在Employee构造器中会初始化为

eva = new StringBuilder();

final关键字只是表示存储在evaluations变量中的对象引用不会再指示其他StringBuilder对象。不过这个对象可以更改:

eva.append("demo");

静态域与静态方法

静态域

如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。 例如,假定需要给每一个雇员赋予唯一的标识码。这里给Employee类添加一个实例域id和一个静态域nextId:

class Employee {
private static int nextId = 1;
private int id;
}

现在,每一个雇员对象都有一个自己的id域,但这个类的所有实例将共享一个nextId域。换句话说,如果有1000个Employee类的对象,则有1000个实例域id。但是,只有一个静态域nextId。即使没有一个雇员对象,静态域nextId也存在。它属于类,而不属于任何独立的对象。

实现一个简单的方法:

public void setId()
{
id = nextId; // set id to next available id
nextId++;
}

假定为harry设定雇员标识码: harry.setId();

harry的id域被设置为静态域nextId当前的值,并且静态域nextId的值加1

静态常量

在Math类中定义了一个静态常量:

public class Math {
private static final double PI = 3.1415;
}

在程序中,可以采用Math.PI的形式获得这个常量。

如果关键字static被省略,PI就变成了Math类的一个实例域。需要通过Math类的对象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。

另一个多次使用的静态常量是System.out

由于每个类对象都可以对公有域进行修改,所以,最好不要将域设计为public。然而,公有常量(即final域)却没问题。因为out被声明为final,所以,不允许再将其他打印流赋给它

如果查看一下System类,就会发现有一个setOut方法,它可以将System.out设置为不同的流。读者可能会感到奇怪,为什么这个方法可以修改final变量的值。原因在于,setOut方法是一个本地方法,而不是用Java语言实现的。本地方法可以绕过Java语言的存取控制机制。这是一种特殊的方法,在自己编写程序时,不应该这样处理。

静态方法

静态方法是一种不能向对象实施操作的方法。例如,Math类的pow方法就是一个静态方法。表达式 Math.pow(x,a); 计算幂xa。在运算时,不使用任何Math对象。换句话说,没有隐式的参数。

可以认为静态方法是没有this参数的方法

Employee类的静态方法不能访问Id实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域。下面是使用这种静态方法的一个示例:

public static int getNextId()
{
return nextId; // returns static field
}

可以通过类名调用这个方法:

int n = Employee.getNextId();

可以使用对象调用静态方法。例如,如果harry是一个Employee对象,可以用harry.getNextId()代替Employee.getNextId()。不过,这种方式很容易造成混淆,其原因是getNextId方法计算的结果与harry毫无关系。我们建议使用类名,而不是对象来调用静态方法。

在下面两种情况下使用静态方法:

·一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。

·一个方法只需要访问类的静态域(例如:Employee.getNextId)。

工厂方法

静态方法还有另外一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。你已经见过工厂方法LocalDate.now和LocalDate.of。NumberFormat类如下使用工厂方法生成不同风格的格式化对象:

NumberFormat currencyFormater = NumberFormat.getCurrencyInstance();
NumberFormat percentFormater = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormater.format(x));
System.out.println(percentFormater.format(x));

为什么NumberFormat类不利用构造器完成这些操作呢?这主要有两个原因:

·无法命名构造器。构造器的名字必须与类名相同。但是,这里希望将得到的货币实例和百分比实例采用不用的名字。

·当使用构造器时,无法改变所构造的对象类型。而Factory方法将返回一个DecimalFormat类对象,这是NumberFormat的子类

main方法

不需要使用对象调用静态方法。例如,不需要构造Math类对象就可以调用Math.pow。

同理,main方法也是一个静态方法。


public class Employee {
public static void name(String[] args) {
}

}


>main方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。

>如果Employee类是一个更大型应用程序的一部分,就可以使用下面这条语句运行程序
```java App```
>Employee类的main方法永远不会执行。

## 方法参数
>在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。“按……调用”(call by)是一个标准的计算机科学术语,它用来描述各种程序设计语言(不只是Java)中方法参数的传递方式(事实上,以前还有按名调用(call by name),Algol程序设计语言是最古老的高级程序设计语言之一,它使用的就是这种参数传递方式。不过,对于今天,这种传递方式已经成为历史)。

>Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

>方法参数共有两种类型:

·基本数据类型(数字、布尔值)。

·对象引用。

>一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了

>有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。

>Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。

>Java中方法参数的使用情况:

·一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。

·一个方法可以改变一个对象参数的状态。

·一个方法不能让对象参数引用一个新的对象。

***
## 对象构造
>构造器,可以定义对象的初始状态。
>Java提供了多种编写构造器的机制

### 重载
>