第一章 Java程序设计概述
1.1 Java程序设计平台
1.2 Java“白皮书”的关键术语
Java设计的初衷:
- 简单性
- 面向对象
- 分布式
- 健壮性
- 安全性
- 体系结构中立
- 可移植性
- 解释型
- 高性能
- 多线程
- 动态性
// 白皮书
// 11个关键术语的概述
1.2.1 简单性
易于理解。支持嵌入式。
1.2.2 面向对象
1.2.3 分布式
-
Java有丰富的例程库,用来处理TCP/IP协议。
-
Java程序能够通过URL打开访问网络上的对象,其便捷程度好像在访问本地文件一样。
1.2.4 健壮性
Java非常强调早期的问题检测、后期动态的(运行时)检测,以及消除容易出错的情况。
1.2.5 安全性
1.2.6 体系结构中立
编译器编译生成一个体系结构中立的目标文件格式,通过Java运行时系统可以在许多处理器上运行。编译器的字节码可以动态转换成本地机器代码。这种字节码不是“新思路”。
1.2.7 可移植性
基本数据类型的大小以及有关运算的行为都有明确的说明。系统组成的类库都定义了可移植的接口。除了与用户界面有关的部分外,所有其他Java库能很好的支持平台独立性。
1.2.8 解释型
Java解释器可以在任何移植了解释器的机器上执行Java字节码。
1.2.9 高性能
解释型虚拟机指令肯定会比全速运行机器指令慢很多。不过虚拟机有一个选项,可以将最频繁的字节码序列转成机器码,这个过程叫即时编译。
1.2.10 多线程
多线程可以带来更快的交互响应和实时行为。Java是第一个支持并发程序设计的语言。JavaSE8不再追求更快的处理器,而是着眼于获得更多的处理器,而且让他们一直保持工作。
1.2.11 动态性
Java能够适应不断发展的环境。库中可以自由地添加新方法和实例变量而对客户端没有影响。Java也可以为正在运行的代码增加代码。
1.3 Java applet与Internet
applet几乎被淘汰。
1.4 Java发展简史
1.5 关于Java的常见误解
-
Java是HTML的扩展 Java是程序设计语言;HTML是描述网页结构的方式。
-
使用XML,所以不需要Java XML是一种描述数据的方式。可以使用程序设计语言处理XML数据。
-
Java是一种非常容易学习的程序设计语言
-
Java将成为适用于所有平台的通用性 Java在服务器端编程和跨平台客户端应用领域很有优势。
-
Java只不过是另外一种程序设计语言 程序设计语言的成功更多取决于其支撑系统的能力,而不是语法的精巧性。人们主要关注的是:是否提供了有用、便捷和标准的库能实现需要的功能。是否有工具开发商能建立强大的编程和调试环境。语言和工具集是否与计算基础架构的其他部分整合在一起。
-
Java是专用的,应该避免使用 Java仅限于桌面和服务器平台开源,嵌入式系统很可能需要付费。
-
Java是解释型的,对于关键的应用程序太慢了 即时编译使得代码运行速度与C++相差无几,有些情况甚至更快。
-
所有Java程序都是在网页中运行的
-
Java程序存在重大安全风险
-
JavaScript是Java的简易版
-
使用Java可以用廉价的Internet设备取代计算机 现在大多数用户常用平台是Android设备。
第二章 Java程序设计环境
2.1 安装Java开发工具包
2.1.1 下载JDK
-
Java术语: | 术语名 |解释| | --- | --- | | JDK | 编写Java程序使用的App | | JRE | 运行Java程序的App | | Server JRE(服务器JRE) | 在Server上运行Java程序的App | | SE | 用于桌面或简单Server应用的Java平台 | | EE | 用于复杂Server应用的Java平台 | | ME | 用于小型设备的Java平台 | | Java FX | 图形化用户界面的一个备选工具包,Java11前某些SE发布版本中提供 | | OpenJDK | SE的一个免费开源实现 | | Java2(J2) | 一个过时术语,98
06年的Java版本 | | SDK | 一个过时术语,9806年的JDK | | Update(u) | Oracle的术语,Java8前的bug修正版本 | | NetBeans | Oracle的集成开发环境 | -
JRE只包含虚拟机,专门为不需要编译器的用户提供。
2.1.2 设置JDK
2.1.3 安装库源文件和文档
-
lib/src.zip包含了所有公共类库的源代码。
2.2 使用命令行工具
- javac.exe是一个Java编译器。java.exe启动虚拟机,虚拟机执行javac编译类文件中的字节码。
- 要注意以下几点:
- 输入源程序区分大小写。
- 运行javac.exe需要文件名。而运行java.exe只需要指定类名,不需要带扩展名。
- 如果javac报错指出无法找到文件,使用dir命令检查目录中是否存在该文件,不要使用图形界面。
2.3 使用集成开发环境
2.4 运行图形化应用程序
2.5 构建并运行applet
第三章 Java的基本程序设计结构
3.1 一个简单的Java应用程序
public class FirstSample{
public static void main(String[] args){
System.out.println("We will not use 'Hello,World!'");
}
}
Java区分大小写。源代码中关键字public称为访问修饰符(access modifier),这些修饰符用于控制程序的其他部分对这段代码的访问级别。关键字class表明程序中的全部内容都包含在类中,程序逻辑定义了应用程序的行为。
类是构建所有Java应用程序的的构建块,Java应用程序中的全部内容都必须放置在类中。
Java类名定义规则很宽松。必须以字母开头,后面可以跟字母和数字的任意组合。长度没有限制。不能使用Java保留字(保留字列表参看附录)。
标准命名规范:类名是以大写字母开头的名词。如果名字由多个单次组成,每个单词的首字母都要大写(骆驼命名法)。
源代码的文件名必须与公共类的名字相同,并用.java作为扩展名。
运行javac编译后生成一个包含这个类字节码的文件,将扩展名变更为.class,并储存在源文件(Source files)目录下。
运行java执行已编译的程序时,虚拟机总是从指定类中的main()开始执行,main()也叫main方法、main函数。因此源代码中必须包含一个main()。main()必须声明为public。
源代码中用{ }括起来的部分称为代码块。
{
System.out.println("We will not use 'Hello,World!'");
}
一对大括号表示方法体的开始与结束。每个语句必须用;结束。这里使用了System.out的对象调用
其println方法。.用于调用对象。
示例调用println()并传递其一个字符串参数。println()将传递给它的参数显示在控制台上,然后终止这个输出行。因此每次调用println()都会在新的一行显示输出。
Java采用" "界定字符串。Java的方法可以没有参数,也可以有一个或多个。即使没有参数也要使用空括号()。System.out.println();语句会打印一个空行。
3.2 注释
-
Java中的注释不会出现在可执行程序中,可以在源程序中添加任意多的注释。
-
Java中有3中注释的方式:
//。其注释内容从//开始到本行结尾。- 注释界定符
/* */。用/*和*/括起一段比较长的注释。 /** */。这种注释可以自动生成文档,以/**开始,以*/结束。
3.3 数据类型
Java是一种强类型语言。必须为每一个变量声明一种类型。Java中一共有8种基本类型(primitive type),包括4种整型、2种浮点类型、1种字符类型、1种用于表示真值的boolean类型。
3.3.1 整型
- 整型用于表示没有小数部分的数值,允许负数。
- Java整型
| 类型 | 存储需求 | 取值min | 取值max |
|---|---|---|---|
| int | 4字节 | -2147483648 | \2147483647 |
| short | 2字节 | -32768 | 32767 |
| long | 8字节 | -9223372036854775808 | \9223372036854775807 |
| byte | 1字节 | -128 | 127 |
-
整型的范围与运行的机器无关,可移植性。
-
long后缀L或l。
-
十六进制数前缀0X或0x。八进制前缀0,建议不使用8进制,容易混淆。
-
Java7开始,加上前缀0b或0B可以表示二进制数,同时还可以为数字字面量加下划线,这些下划线只是让人更易读,编译时javac会去除下划线。
-
Java没有任何无符号(unsigned)的int、long、short或byte。
3.3.2 浮点类型
- 浮点类型用于表示有小数部分的数值。有两种浮点类型。 | 类型 | 存储需求 | 取值范围 | | --- | --- | --- | | float | 4字节 | 大约±3.40282347E+38F(有效位数为6~7位) | | double | 8字节 | 大约±1.79769313486231570E+308(有效位数为15位) |
double的数值精度是float的两倍。实际上只有很少的情况适合使用float。
-
float后缀F或f。没有后缀默认double。
-
可以使用十六进制表示浮点数值。例,0.125=可以表示成0x1.0p-3。
步骤:
将0.125转化为二进制为0.001。
规范化为0b1.0*2^-3,注意此时整个数是二进制。
尾数为二进制0,转化为十六进制仍然为0。
用p表示指数,在加上0x前缀,得到结果0x1.0p-3。
注意:
尾数采用16进制,指数采用10进制。使用p表示指数而不是e(e是一个十六进制数位)。指数的基数是2而不是10。
规范化后二进制浮点数的尾数部分可能很长,所以把尾数部的每4位用一个十六进制数表示,使浮点数更加简洁。
-
-
用于表示溢出和出错情况的三个特殊的浮点数值:
-
正无穷大
-
负无穷大
-
NaN
-
常量Double.POSITIVE_INFINITY、Double.NEGATIVE_INFINITY和Double.NaN分别表示这三个特殊的值,但在实际应用中很少遇到。
可以使用Double.isNaN()来判断:if(Double.isNaN(x))
-
警告:浮点数值不适用于无法接受舍入误差的金融计算。如果在数值计算中不允许有任何舍入误差,就应该使用BigDecimal类。
3.3.3 char类型
-
char一般表示单个字符,有些Unicode值需要两个char值。char的字面量值要用
'括起来。/** 'A'是编码值位65的字符常量。 "A"是包含一个字符A的字符串。 */ -
char类型的值可以表示为十六进制,范围是转义序列\u0000~\uFFFF。
-
除了\u转义序列以外,还有一些表示特殊字符的转义序列:
| 转义序列 | 名称 | Unicode值 |
|---|---|---|
| \b | 退格 | \u0008 |
| \t | 制表 | \u0009 |
| \n | 换行 | \u000a |
| \r | 回车 | \u000d |
| " | 双引号 | \u0022 |
| ' | 单引号 | \u0027 |
| \ | 反斜杠 | \u005c |
警告:Unicode转义序列会在解析代码之前得到处理。
例如:
1."\u0022+\u0022"的输出结果不是“ "+" ”,而是一个空串。
2.
// \u000a is a newline会产生一个语法错误,因为编译运行时\u000a会被替换成换行符3.
//look inside c:\users也会发生语法错误,因为\u后面没有跟着4个16位进制数。
3.3.4 Unicode 和char类型
- 码点(code point)是指与一个编码表中的某个字符对应的代码值。Unicode的码点分成了17个代码级别(code plane)。第一个代码级别称为基本的多语言级别(basic multilingual plane)。在基本的多语言级别中,每个字符用16位表示,被称为代码单元。
- char类型描述了UTF-16编码中的一个代码单元(code unit)。强烈建议不要使用char,除非需要处理UTF-16代码单元。最好将字符串作为抽象数据类型处理。
3.3.5 boolean类型
boolean(布尔)类型有两个值,false和true,用来判定逻辑条件。整型和boolean之间不能进行相互转换。
3.4 变量
-
Java中每个变量都有一个类型(type)。声明变量时先指定变量的类型,然后是变量名。
-
变量名必须是一个以字母开头并由数字构成的序列。长度基本没有限制。不能用Java保留字作为变量名。Java中字母和数字的范围更大。
-
字母包括'A'-'Z'、'a'-'z'、'_'、'$'或者在某种语言中表示字母的任何Unicode字符。大小写敏感。
/** //例如德国的字母'ä'。 */数字包括'0'-'9'或者在某种语言中表示数字的任何Unicode字符。
-
Character类的isJavaIdentifierStart()和isJavaIdentifierPart()来检查哪些Unicode字符属于Java的“字母”。
不要在自己的代码中使用'$'。它只是用在Java编译器或其他工具生成的名字中。
-
3.4.1 变量初始化
声明一个变量后,必须用赋值语句进行显示初始化。Java的声明可以放在代码中的任何地方。
/**
int x = 12;
//声明x并把12赋给了x。
*/
3.4.2 常量
-
Java中用关键字final指示常量。final表示被指示变量只能被赋值一次。
/** final int X = 12; */ -
Java中类常量可以在一个类的多个方法中使用。用
static final声明在main()的外部。
public class Constants{
public static final double ACONSTANT = 10;
public static void main(String[] args){
System.out.println(ACONSTANT);
}
}
如果一个常量被声明为public,那么其他类的所有方法都可以使用这个常量。
3.5 运算符
-
Java中使用算术运算符+、-、*、/表示加、减、乘、除运算。当参与/运算的两个操作数都是整数时,表示整数除法;否则表示浮点除法。整数的求余用%表示。
-
整数除0会产生异常,浮点数除0会得到无穷大或NaN。
-
关键字strictfp:
如果用strictfp标记main(),则方法中所有指令都将使用严格的浮点计算。如果将一个类标记为strictfp,这个类中的所有方法都要使用严格的浮点计算。
/** public static strictfp void main(String[] args) */实际的计算方式取决于Intel处理器的行为。默认方式不会产生溢出,而采用严格的计算有可能产生溢出。
-
3.5.1 数学函数与常量
- Math包含各种数学函数。
-
计算数值的平方根,可以使用sqrt()。
/** double y = Math.sqrt(4); System.out.println(y); */- println()处理System.out对象。但sqrt()处理的不是对象,这样的方法称为静态方法。
-
Java中没有幂运算,需要借助pow()。
/** double y = Math.pow(x,a); //将y的值设置为x的a次幂(xa)。pow()有两个double参数,返回结果为double。 */ -
整数求余问题。floorMod(x,y)。
-
常用的三角函数:sin()、cos()、tan()、atan()、atan2()
-
指数函数以及它的反函数:exp()、log()、log10()
-
Java中提供了两个用于表示π和e常量的近似值:(1)PI(2)E
-
在源文件的顶部静态导入Math,就可以不用再方法名和常量名前加前缀“Math”
/** import static java.lang.Math.*; */ -
Math为了达到最快的性能,所有的方法都是用计算机浮点单元里的例程。如果想要完全可预测的结果而不是最快的运行速度可以使用StrictMath类。
-
3.5.2 数值类型之间的转换
6个实心箭头表示无信息丢失的转换。3个虚箭头表示可能有精度损失的转换。
- 浮点数和整数进行二元操作时。要将2个操作数转换为同一类型再计算。
- 如果有一个操作数是double,另一个操作数就会转为double。
- 如果有一个操作数是float,另一个操作数就会转为float。
- 如果有一个操作数是int,另一个操作数就会转为int。
- 否则两个操作数都转换为int。
3.5.3 强制类型转换
-
强制类型转换(cast)的语法格式是在圆括号里指出转换的目标类型,后面紧跟待转换的变量。
/** double x = 1.233; int y = (int)x; //截断小数部分将浮点值强制转换为整型。 */ -
round()可以进行四舍五入运算。
/** double x = 9.997; int y = (int)Math.round(x); //round()返回的是long,还需要强制int转换。 */
3.5.4 结合赋值和运算符
-
可以在赋值中使用二元运算符。
/** x %= 4;等价于x = x % 4; */-
int x = 3;
x += 3.5;
得到的值类型与左操作数类型不同,会强制转换。相当于x = (int)(x + 3.5);
-
3.5.5 自增与自减运算符
- 后缀形式自增、自减运算符:
n++;将n当前值加1。
n--;将n当前值减1。
- 前缀形式自增、自减运算符:
++n;将n当前值加1。
--n;将n当前值减1。
-
前缀形式先加1,后续再操作变量;后缀形式先操作变量,然后加一
/** int m = 7; int n = 7; int a = 2*++m; int b = 2*n++; //现在a=16,m=8。b=14,n=8; */
3.5.6 关系和boolean运算符
-
判断两个值的关系并生成boolean值: 相等
==、不相等!=、小于<、大于>、小于等于<=、大于等于>=/** 3==7的值是false 3!=7的值是true */ -
判断两个表达式的值并生成boolean值: 逻辑与运算符:
&&、逻辑或运算符:||
&&和||按照“短路方式”求值,如果第一个关系表达式的boolean值能确定,第二个表达式就不计算并生成第一个表达式的boolean值。
/**
expression1&&expression2
*/
-
逻辑非运算符:
! -
三元操作符:
?:如果条件为true,则计算第一个表达式的值,否则计算第二个。/** condition?expression1:expression2 */
3.5.7 位运算符
- 处理整数类型的二进制数,直接对组成整数类型数值的各个二进制位操作。除了
~为单目运算符,其他都是双目运算符。
-
&(and):两个数对应位同真且真/** 0b1100&&0b1000 //结果:0b1000 */ -
|(or):两个数对应位同假或假/** 0b1100||0b1000 //结果:0b1100 */&、|以关系运算符应用时,也会得到boolean值,但运算符两边的表达式都要求值进而生成整个表达式的boolean结果。
-
^(xor):同假异真/** 0b1100||0b1000 //结果:0b0100 */ -
~(not):按位取反/** ~0b1100 //结果:0b0011 */ -
左移
<<:将所有二进制位左移,溢出位舍弃,空出位补0。 -
右移
>>:将所有二进制位右移,溢出位舍弃,空出位补原数的符号位。 -
无符号右移
>>>:将所有二进制位右移,溢出位舍弃,空出位补0。/** 0b1100>>1 //结果:0b1110 */
移位运算符的右操作数要完成模32的运算(除非左操作数是long类型,这种情况要对右操作数模64)。
3.5.8 括号与运算符级别
不使用( )的情况下,参见运算符优先级表格:
| 运算符 | 结合性 |
|---|---|
. | 从左向右 |
! ~ ++ -- +(一元运算) -(一元运算) ()(强制类型转换) new | 从右向左 |
* / % | 从左向右 |
+ - | 从左向右 |
<< >> >>> | 从左向右 |
< <= > >= instanceof | 从左向右 |
== != | 从左向右 |
& | 从左向右 |
^ | 从左向右 |
| | 从左向右 |
&& | 从左向右 |
|| | 从左向右 |
?: | 从右向左 |
= += -= *= /= %= &= |= ^= <<= >>= >>>= | 从右向左 |
3.5.9 枚举类型
当变量的取值只在一个有限的集合内,可以自定义枚举类型。枚举类型包括有限个命名的值。
/**
例如:
{
enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE};
Size s = Size.SMALL;
}
*/
枚举类型的变量可以存储一个特殊值null,表示当前变量没有设置任何值。
3.6 字符串
Java字符串就是Unicode字符序列。Java没有字符串类型,而是在标准Java类库里提供了一个预定义类叫做String,每个用" "括起来的字符串都是String的一个实例。
/**
String e = "Hello";
*/
3.6.1 子串
String的substring()可以从一个较大的串提取出一个子串。
/**
String e = "Hello";
String e1 = e.substring(0,3);
//创建了一个“Hel”的子串。
*/
3.6.2 拼接
-
Java允许使用
+拼接两个串。/** String e = "Hello"; String s = "!"; String x = e + s; */ -
一个串与一个非串的值拼接时,后者被转换成串。任何一个Java对象都可以转换成为串。
/** String e = "page"; int s = "47"; String x = e + s; //x是page47 */ -
静态join()可以用一个定界符把多个串放在一起。
/** String all = String.join("\","p","i","g"); //all是“p\i\g” */
3.6.3 不可变字符串
-
String没有提供修改串的方法。想要修改串可以先substring()再拼接。所以Java文档称String对象为不可变字符串。
/** String e = "Hello"; String s = e.substring(0,3) + "p!"; //s是“Help!”。 */ -
Java的字符串逻辑上存放在公共的存储池中。串变量指向存储池中相应的位置,如果复制一个串给一个新变量,原串与新串共享相同的字符。
3.6.4 检测字符串是否相等
-
equals()检测两个串是否相等并生成boolean值。
/** s.equals(t) */ -
equalsIgnoreCase()可以忽略大小写检测两个串是否相等。
/** s.equalsIgnoreCase(t) */ -
不要使用
==检测两个串是否相等!==只能检测两个串是否在同一个位置上。如果虚拟机始终将相同的串共享,就可以使用==。但实际上只有串常量是共享的,而+或substring()的结果不是共享的。
3.6.5 空串与null串
-
空串是长度为0的串。可以用length()检测串的长度。
/** int x = s.length(); */空串是一个Java对象,有自己的串长度(0)和内容(空)。
-
String变量还可以存放特殊值null,表示目前没有任何对象与该变量关联。
要检测一个串是否为null可以用
==。/** if(s != null && s.length() != 0) */
3.6.6 码点与代码单元
-
length()可以返回给定串的代码单元数量。
/** String greeting = "Hello"; int n = greeting.length(); //n是5 */ -
codePointCount()可以返回码点数量。
/** String greeting = "Hello"; int cpCount = greeting.codePointCount(0,greeting.length()); */ -
charAt(n)可以返回位置n的代码单元。
/** String greeting = "Hello"; char first = greeting.charAt(0); //first是H char last = greeting.charAt(4); //last是o */ -
想要得到第i个码点需要下列语句。
/** String greeting = "Hello"; int index = greeting.offsetByCodePoints(0,i); */- Java中代码单元和码点从0开始计数。
-
如果要遍历一个串,并依次查看每一个码点,可以使用codePoints(),会生成一个int值的流,每个int值对应一个码点。可以将它转换为一个数组,再完成遍历。
/** int[] codePoints = str.codePoints().toArray(); */反之,要把一个码点数组转换为一个串,可以使用构造函数。
/** String str = new String(codePoints,0,codePoints.length()); */
3.6.7 StringAPI
最常用的API汇总。java.lang的String,表格里是方法的名字、解释和参数描述。有些CharSequence类型的参数,这是一种接口类型,所有字符串都属于这个接口。
- API java.lang.String
| 方法 | 解释 |
|---|---|
| char charAt(int index) | 返回给定位置的代码单元 |
| int codePointAt(int index) | 返回从给定位置的码点 |
| int offsetByCodePoints(int startIndex,int cpCount) | 返回从startIndex代码点开始,位移cpCount后的码点索引 |
| int compareTo(String other) | 按照字典顺序逐个比对,如果串位于other之前,返回一个负数;如果位于other之后,返回一个正数;相等返回0 |
| IntStream codePoints() | 将串的码点作为一个流返回 |
| new String(int[] codePoints,int offset,int count) | 从数组中从offset开始的count码点构造一个串 |
| boolean equals(Object other) | 如果串与other相等,返回true |
| boolean equalsIgnoreCase(String other) | 忽略大小写的情况下如果串与other相等,返回true |
| boolean startsWith(String prefix) | 如果串以prefix开头则返回true |
| boolean endsWith(String suffix) | 如果串以suffix结尾则返回true |
| int indexOf(String str) | 返回与串str匹配的第一个子串的开始位置。这个位置从索引0开始计算。如果原串中不存在str返回-1 |
| int indexOf(String str,int fromIndex) | 返回与串str匹配的第一个子串的开始位置。这个位置从索引fromIndex开始计算。如果原串中不存在str返回-1 |
| int indexOf(int cp) | 返回与码点cp匹配的第一个子串的开始位置。这个位置从索引0开始计算。如果原串中不存在cp返回-1 |
| int indexOf(int cp,int fromIndex) | 返回与码点cp匹配的第一个子串的开始位置。这个位置从索引fromIndex开始计算。如果原串中不存在cp返回-1 |
| int lastIndexOf(String str) | 返回与串str匹配的最后一个子串的开始位置。这个位置从原串尾端开始计算 |
| int lastIndexOf(String str,int fromIndex) | 返回与串str匹配的最后一个子串的开始位置。这个位置从fromIndex开始计算 |
| int lastIndexOf(int cp) | 返回与码点cp匹配的最后一个子串的开始位置。这个位置从原始串尾端开始计算 |
| int lastIndexOf(int cp,int fromIndex) | 返回与码点cp匹配的最后一个子串的开始位置。这个位置从fromIndex开始计算 |
| int length() | 返回串的长度 |
| int codePointCount(int startIndex,int endIndex) | 返回startIndex和endIndex之间的码点数。没有配成对的代用字符计入码点。 |
| String replace(CharSequence oldString,CharSequence newString) | 返回一个新串,用newString代替原串中的oldString。可以用String或StringBuilder对象作为CharSequence参数 |
| String substring(int beginIndex) | 返回一个新串,包含原串中从beginIndex到串尾的所有代码单元 |
| String substring(int beginIndex,int endIndex) | 返回一个新串,包含原串中从beginIndex到endIndex的所有代码单元 |
| String toLowerCase() | 返回一个将原串的大写字母改为小写的新串 |
| String toUpperCase() | 返回一个将原串的小写字母改为大写的新串 |
| String trim() | 返回一个删除原串中头部尾部空格的新串 |
| String join(CharSequence delimiter,CharSequence... elements) | 返回一个用所给的界定符连接的新串 |
3.6.8 阅读联机API文档
左上方的窗框显示可使用的所有包,在其下方列出所有的类。点击类名,这个类的API文档就会显示在右侧的窗框中。
3.6.9 构建字符串
-
使用StringBuilder把较短串组成新String实例,省时省内存。
/** //先构建一个内容为空的字符串构建器的实例,这个实例就具有了StringBuilder的所有方法。 String builder = new StringBuilder(); //调用append()添加内容 builder.append(char); builder.append(String); //向构建器添加好内容后,调用toString()连接builder里的内容成String。 String completedString = builder.toString(); */- JDK5.0引入StringBuilder,前身是StringBuffer。效率有些低,但允许以多线程的方式执行添加或删除字符的操作。如果在单线程中编辑,应该用StringBuilder替代它。这两个类的API相同。
- StringBuilder重要方法: | 方法 | 解释 | | --- | --- | | 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的代码单元并返回this | | String toString() | 返回一个与构建器或缓冲器内容相同的串 |
3.7 输入输出
3.7.1 读取输入
-
打印输出到标准输出流(控制台窗口)调用System.out.println。
-
要控制台读取输入的内容,先构造一个Scanner实例,并与标准输入流System.in关联。
/** Scanner in = new Scanner(System.in); */nextLine()可以读取一行。
/** Scanner in = new Scanner(System.in); String name = in.nextLine(); */next()可以读取一个单词。
/** Scanner in = new Scanner(System.in); String firstame = in.next(); */nextInt()可以读取一个整数。
/** Scanner in = new Scanner(System.in); int age = in.nextInt(); */nextDouble()可以读取一个浮点数。 Scanner定义在java.util中。当使用的类不是在基本java.lang中时要导包。
/** import java.util.*; *、-
Scanner类不适用于读取密码。可以使用Console类实现。
/** Console cons = System.console(); String username = cons.readLine("User name: "); char[] passwd = cons.reaPassword("Password: "); */返回的密码存放在一维字符数组中而不是串中。在对密码处理以后应该马上用一个填充值覆盖数组元素。
Console处理不如Scanner,每次只能读取一行,而没有读取一个单词或一个数的方法。
-
-
API java.util.Scanner | 方法 | 解释 | | --- | --- | | Scanner(InputStream in) | 用给定的的输入轮流创建一个Scanner对象 | | String nextLine() | 读取输入的下一行内容 | | String next() | 读取输入的下个单词(以空格作为分隔符) | | int nextInt() | 读取并转换下一个表示整数的字符序列 | | Double nextDouble() | 读取并转换下一个表示浮点数的字符序列 | | boolean hasNext() | 检测输入中是否还有其他单词 | | boolean hasNextInt() | 检测是否还有表示整数的下一个字符序列 | | boolean hasNextDouble() | 检测是否还有表示浮点数的下一个字符序列 |
-
API java.lang.System | 方法 | 解释 | | --- | --- | | static Console console() | 如果有可能进行交互操作,就通过控制台窗口位交互的用户返回一个console对象,否则返回null。对于任何通过控制台窗口启动的程序,都可以使用console对象。否则,其可用性与使用的系统有关。 |
-
API java.io.Console | 方法 | 解释 | | --- | --- | | static char[] reaPassword(String prompt,Object...args) | 显示串prompt并读取用户输入,直到输入行结束。args用来提供输入格式 | | static String readLine(String prompt,Object...args) | 显示串prompt并读取用户输入,直到输入行结束。args用来提供输入格式 |
3.7.2 格式化输出
-
System.out.print(x)将以x对应的数据类型所允许的最大非0数字位数打印输出x。/** double x = 10000.0/3.0; System.out.print(x); //输出3333.3333333333335 */ -
JavaSE8沿用了C的printf()。
/** System.out.printf("%8.2f",x); //用附带小数点后两个字符的精度一共8个字符的宽度打印x:3333.33(最高位是空格) */printf()可以使用多个参数。
/** System.out.print("Hello,%s.Next year,you'll be %d",name,age); */每个以%开始的格式说明符都用相应的参数替换。格式说明符尾部的转换符将指示被格式化的数值类型。
-
用于printf的转换符: | 转换符 | 类型 | | --- | --- | | d | 十进制整数 | | x | 十六进制整数 | | o | 八进制整数 | | f | 定点浮点数 | | e | 指数浮点数 | | g | 通用浮点数 | | a | 十六进制浮点数 | | s | 串 | | c | 字符 | | b | boolean | | h | 散列码 | | % | % | | n | 与平台有关的行分隔符 |
/** //例:散列码42628b2 */ 可以使用s转换符格式化任意对象。对于任意实现了Formattable接口的实例都将调用formatTo();否则调用toString()。
-
控制格式化输出的各种标志,可以使用多个.
/** //例:`"%,(.2f"` */
| 标志 | 目的 | 举例 | 解释 |
|---|---|---|---|
| + | 打印正数和负数的符号 | -3333.33 | |
| 空格 | 在正数之前添加空格 | | 3333.33| | |
| 0 | 数字前面补0 | 003333.33 | |
| - | 左对齐 | |3333.33 | | |
| ( | 将负数括在括号内 | (3333.33) | |
| , | 添加分组分隔符 | 3,333.33 | |
| #(对于f格式) | 不论小数位是否有值都包含小数点 | 3333. | |
| #(对于x或o格式) | 添加前缀0x或0 | 0xcafe | |
| $ | 给定被格式化的参数索引 | 159 9f(%1$d,%1$x将以十进制和十六进制打印输出第一个参数) | |
| < | 格式化前面说明的数值 | 159 9f(%d%<x将以十进制和十六进制打印同一个数值) |
-
可以使用静态的format()创建一个串而不打印输出。
/** String message = String.format("Hello,%s.Next year,you'll be %d",name,age); */ -
格式说明符的语法图:
参数索引值从1开始。许多本地化规则是本地环境特有的,可以控制应用的国际化行为。
3.7.3 文件输入与输出
-
要对文件读取,要用File实例构造一个Scanner实例,并用其方法读取。
/** Scanner in = new Scanner(Paths.get(myfile.txt"),"UTF-8"); //如果文件名中包含`\`,就要在每个`\`再加一个`\`转义。 //这里指定了UTF-8字符编码。如果省略,会使用当前机器的默认编码,不同机器会有不同表现。 */ -
写入文件要构造PrintWriter实例。
/** PrintWriter out = new PrintWriter("myfile.txt","UTF-8"); //如果文件不存在就创建该文件。 */ -
当指定一个相对文件名时,文件位于Java虚拟机启动路径的相对位置。如果在命令行方式下用
java XXX命令时,启动路径就是命令行解释器的当前路径。如果使用IDE,启动路径由IDE控制。可以用System.getProperty()找到路径位置。也可以使用绝对路径。/** String dir = System.getProperty("user.dir"); */ -
如果用不存在的文件构造Scanner,或用不能被创建的文件名构造一个PrintWriter,会发生IOexception异常。javac认为这些异常比“除0”异常更严重。要告知javac,在main()中用throw子句标记。
/** public static void main(String[] args) throws IOException{ Scanner in = new Scanner(Path.get("myfile.txt"),"UTF-8"); ... } */-
命令行方式启动程序时,可以用shell的重定向将任意文件关联到System.in和System.out,可以不用担心异常。
/** java MyProg<myfile.txt>output.txt */
-
-
API java.util.Scanner | 方法 | 解释 | | --- | --- | | Scanner(File f) | 构造一个读取给定文件的Scanner | | Scanner(String data) | 构造一个读取给定串的Scanner |
-
java.io.PrintWriter | 方法 | 解释 | | --- | --- | | PrintWriter(String fileName) | 构造一个向给定文件写入数据的PrintWriter |
-
java.nio.file.Paths | 方法 | 解释 | | --- | --- | | static Path get(String pathname) | 根据给定的路径名构造一个Path |
3.8 控制流程
Java使用条件语句和循环结构确定控制流程。
3.8.1 块作用域
块(block)是由{}括起来的若干条简单的Java语句。块确定了变量的作用域。可以嵌套但,不能在嵌套的块中声明同名的变量。
/**
public static void main(String[] args){
int n;
...
{
int k;
...
}
}
*/
3.8.2 条件语句
-
语句格式:if(condition) statement
-
一般的条件语句格式:if(condition)statement else statement2
else部分是可选的。else与最邻近的if构成一组。
3.8.3 循环
-
当条件为true时,while循环执行语句,直到条件为false退出循环。
一般格式为:while(condition) statement
-
do/while循环先执行语句。格式为:do statement while(condition)
3.8.4 确定循环
-
for循环是支持迭代的一种通用结构,利用每次迭代后更新的计数器或类似的变量来控制迭代次数。
/** for(int i = 1; i <= 10; i++) System.out.print(i); //将数字1~10输出。 */for的第一部分初始化计数器;第二部分给出每次新一轮执行循环前检测的条件;第三部分指示如何更新计数器;
-
Java允许在for的各个部分放置任何表达式,但不推荐使用。for的第一部分声明的变量作用域为for的整个循环体。
警告:在循环中,检测两个浮点数相等可能由于舍入误差而到不到精确值,循环永远不会结束
/**
for(double x = 0; x != 10; x +=0.1;)
//0.1无法用二进制精确表示,所以x将从9.999 999 999 999 98跳到10.099 999 999 999 98
*/
3.8.5 多重选择switch语句
- switch用于处理多个选项。 switch开始检测匹配,与condition匹配的case处执行statement直到
break;,中断并退出循环;否则检测到switch结束处,如果有default子句,就执行default。语法格式:
switch(condition){
case 1:
statement1;
...
break;
case 2:
statement1;
...
break;
case ...
default:
statement;
...
break;
}
警告:有可能触发多个case。如果case末尾没有
break;,就会在触发当前case执行后,继续执行接下来的case,直到遇到break;。这个错误相当危险。可以考虑加上
-Xlint:fallthrough选项。这样javac发现某分支缺少break就会警告。/**
例:javac -Xlint:fallthrough Test.java
*/
如果缺少break正是你想要的,可以为外围方法加标注
@SuppressWarnings("fallthrough")。这样就不会生成警告了标注是为javac或处理Java源文件或类文件的工具提供信息的一种机制。
- case可以是:
- char、byte、short或int的常量表达式
- 枚举常量
- JavaSE7开始,可以是串字面量 当在switch中使用枚举常量时,case不用指明枚举名直接调用常量。
3.8.6 中断控制流程语句
-
break;用于中断退出循环。Java还提供了一个带标签的break用于跳出多重嵌套循环。标签必须放在希望跳出的最外层循环前并紧跟一个冒号。在循环中通过执行带标签的break跳转到带标签的块尾。对于任何使用break的代码都要检测循环是正常结束还是break跳出。/** ... label: while(...){ ... break label; } //会执行带有label标签的break跳转到这里 */- 可以将标签应用到任何语句中,但不提倡使用这种方式。另外只能跳出块,不能跳入块。
-
continue;用于中断循环并转移到最内层循环的首部。但对于for来说会跳到for的"更新"部分。带标签的continue会跳转到带标签的块首。
3.9 大数值
-
java.math中的BigInteger实现了任意精度的整数运算,BigDecimal实现了任意精度的浮点数运算。
静态的valueOf()可以将普通的数值转换为大数值。
/** BigInteger a = BigInteger.valueOf(100); */大数值的加和乘运算需要使用add()和multiply()。
/** BigInteger c = a.add(b); BigInteger d = c.multiply(b.add(BigInteger.valueOf(2))); */ -
API java.math.BigInteger | 方法 | 解释 | | --- | --- | | BigInteger add(BigInteger other) | 返回当前大整数和大整数other的和 | | BigInteger substract(BigInteger other) | 返回当前大整数和大整数other的差 | | BigInteger multiply(BigInteger other) | 返回当前大整数和大整数other的积 | | BigInteger divide(BigInteger other) | 返回当前大整数和大整数other的商 | | BigInteger mod(BigInteger other) | 返回当前大整数和大整数other的余数 | | int compareTo(BigInteger other) | 如果当前大整数和other相等返回0;如果小于other返回负数;否则返回正数 | | static BigInteger valueOf(long x) | 返回值等于x的大整数 |
-
API java.math.BigDecimal | 方法 | 解释 | | --- | --- | | BigDecimal add(BigDecimal other) | 返回当前大实数和大实数other的和 | | BigDecimal substract(BigDecimal other) | 返回当前大实数和大实数other的差 | | BigDecimal multiply(BigDecimal other) | 返回当前大实数和大实数other的积 | | BigDecimal divide(BigDecimal other) | 返回当前大实数和大实数other的商,但要给出舍入方式
RoundingMode.HALF_UP| | int compareTo(BigDecimal other) | 如果当前大实数和other相等返回0;如果小于other返回负数;否则返回正数 | | static Bigcemal valueOf(long x) | 返回值等于x的大实数 | | static Bigcemal valueOf(long x,int scale) | 返回值等于x/10^scale的大实数 |
3.10 数组
-
数组是一种数据结构,用来存储同一类型值的集合。通过一个整型下标可以访问数组中的每一个值。在声明数组变量时要指出数组类型、长度和变量名,长度可以为n。
/** int[] a = new int[100]; //声明并创建一个可以存储100个int值的数组。 */数组下标从0开始。创建一个数值数组时所有元素都初始化为0。boolean数组会初始化为false。对象数组的元素初始化为null。
-
可以使用array.length获得数组元素个数。
/** int x = a.length; */数组大小不可变。另一种数据结构--数组列表(array list)可以扩展数组大小。
3.10.1 for each循环
-
foreach可以依次处理数组中每个元素而不必为下标值分心。
语句格式:for(variable : collection) statement
定义一个variable用于暂存collection中的每个元素,遍历每个元素同时并执行statement。collection必须是一个数组或者一个实现了Iterable接口的类实例。
-
使用Array.toString()来打印数组中的所有值更简单。
/** System.out.println(Arrays.toString(a)); */
-
3.10.2 数组初始化以及匿名数组
-
可以不需要调用new就可以创建数组对象并赋初值。
/** int[] a = {2,3,4,6}; */ -
可以创建一个匿名数组并赋初值。
/** new int[] = {2,3,4,6}; */这种语法可以重新初始化一个数组。
- Java允许数组长度为0,但长度为0与null不同。
3.10.3 数组拷贝
-
Java允许将一个数组变量拷贝给另一个数组变量,这时两个变量将引用一个数组。如果希望将一个数组的值拷贝到一个新的数组中,要使用Arrays的copyOf()。
/** int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers,luckyNumbers.length); */第二个参数是新数组的长度,这个方法常用来增加数组的大小。如果数组元素是数值型,多余的元素被赋值为0;如果是布尔型,多余的元素被赋值为false。如果长度小于原始数组的长度,则只拷贝最前面的元素。
- Java的
[]被预定义检查数组边界,而且没有指针运算,不能通过a+1来得到数组的下一个元素。
- Java的
3.10.4 命令行参数
-
每一个Java应用程序都带有一个String arg[]参数的main()。这个参数表明main方法将接收一个串数组,也就是命令行参数
/** public class Message{ public static void main(String[] args){ if(args.length == 0 || args[0].equals("-h")) { System.out.print("Hello,"); } else if(args[0].equals("-g")) { System.out.print("Goodbye,"); } for(int i = 1,i < args.length;i++) { //输出其他的命令行参数 System.out.print(" " + args[i]); } System.out.println("!"); } } //使用java Messa -g cruel world运行时,args[] = {"-g","cruel","world"} //程序运行结果:Goodbye, cruel world! */- Java的程序名不存储在args中。
3.10.5 数组排序
-
对数值型数组排序可以使用Arrays的sort()。
/** int[] a = new int[10000]; ... Arrays.sort(a); */ -
API java.util.Arrays | 方法 | 解释 | | --- | --- | | static String toString(type[] a) | 以串返回a中的元素,用
,分隔。a类型包括:数值型、char、byte、boolean| | static type copyOf(type[] a,int length) | 以长度为length的数组返回a中的元素,用,分隔。a类型包括:数值型、char、byte、boolean | | static type copyOfRange(type[] a,int start,int end) | 以长度为end-start的数组返回a中的元素,用,分隔。a类型包括:数值型、char、byte、boolean。start是起始下标 | | static void sort(type[] a) | 采用优化的快排对数组排序,a类型包括:数值型、char、byte、boolean | | static int binarySearch(type[] a,type v) | 在有序a中用二分查法找v,查找成功返回下标值,否则返回负数r。(“-r-1是为保持a有序v应插入的位置”)a类型包括:数值型、char、byte、boolean。v的数据类型与a的元素相同 | | static int binarySearch(type[] a,int start,int end,type v) | 在有序a中以起始下标b结束下标截取一段数组,用二分查法找v,查找成功返回下标值,否则返回负数r。(“-r-1是为保持a有序v应插入的位置”)a类型包括:数值型、char、byte、boolean。v的数据类型与a的元素相同 | | static void fill(type[] a,type v) | 将数组所有元素赋值为v。a类型包括:数值型、char、byte、boolean。v的数据类型与a的元素相同 | | static booleav equals(type[] a,type[] b) | 如果两个数组大小内容相同返回true,否则返回false。a类型包括:数值型、char、byte、boolean。b的数据类型与a的元素相同 |
3.10.6 多维数组
-
二维数组适用于表格或更加复杂的排版形式。二维数组的声明、构建与初始化与一维数组相同。
/** double[][] = balances; */数组被初始化后可以通过
[][]来访问元素。/** balances[i][j] //balances数组的第i行第j列 */-
foreach不能处理二维数组,需要嵌套。
-
可以调用deepToString()打印二维数组的元素列表。
/** System.out.println(Arrays.deepToString(a)); //输出格式为: //[[1,2,3][4,5,6][7,8,9]] */
-
3.10.7 不规则数组
-
Java实际上只有一维数组,多维数组被解释为“数组的数组”。多维数组是一个元素是数组的数组,可以单独存取某一层,所以可以让两行交换。
/** double[] temp = balances[i]; balances[i] = balances[i+1]; balances[i+1] = balances[temp]; */ -
也可以构造不规则数组,每一层都有不同长度。
第4章 对象与类
4.1 面向对象程序设计(OOP)概述
4.1.1 类
-
类(class)是构造对象的模板或蓝图。类构造(construct)对象的过程称为创建类的实例(instance)。
-
封装(encapsulation)是将数据和行为组合在一个包中,并对实例的使用者隐藏了数据的实现方式。实例中的数据称为实例域(instance field),操纵数据的过程称为方法(method)。对于每个实例都有一组特定的实例域值。这些值的集合就是实例的当前状态(state)。
实现封装的关键在于不能让类中的方法直接访问其他类的实例域。程序仅通过实例的方法与实例的数据交互。
-
通过扩展一个类来构造新类就是继承(inheritance)。Java所有的类都源于Object。
继承后,新类具有原类的全部属性和方法。在新类中只需提供适用于新类的新方法和数据域
4.1.2 对象
对象的三个特性:
-
行为(behavior)--可以对对象施加哪些操作,或可以对对象施加哪些方法?
-
状态(state)--施加方法时,对象如何响应?
-
标识(identity)--如何辨别具有相同行为与状态的不同对象?
对象的行为是由可调用的方法定义的。一个类的所有实例,由于支持相同的行为具有家族式的相似性。
对象的状态就是对象都保存着描述当前特征的信息。可能会改变,但并不会是自发的。对象状态的改变必须通过调用方法实现。但是对象的状态不能完全描述一个对象。每个对象都有一个唯一的身份。
作为类的实例,对象之间的标识永远是不同的。
对象的关键特性彼此影响。
4.1.3 识别类
在OOP的程序编写中,首先从设计类开始,识别在需求中要有哪些类,然后再向每个类中添加新方法。
4.1.4 类之间的关系
类之间的关系最常见的有:
-
依赖(“use-a”) 一个类的方法操纵另一个类的实例,就是一个类依赖(dependence)另一个类。尽可能将相互依赖的类减少,降低耦合度。
-
聚合(“has-a”) 聚合(aggregation)意味着实例A包含实例B。
-
继承 (“is-a”) 继承(inheritance)是一种特殊与一般的关系。
4.2 使用预定义类
并不是所有的类都具有OOP特征。Math只封装了功能,由于没有数据,因此不必担心构造实例并初始化实例域。
4.2.1 对象与对象变量
-
要想使用对象就要先使用构造器(constructor)构造实例,指定初始状态后使用实例。构造器是一种特殊的方法,用来构造并初始化实例。
构造器的名字应该与类名相同,构造实例时使用new操作符。
/** new Date() */可以将构造的实例传给方法,可以将一个方法应用于刚构造的实例,也可以将实例存放在一个变量中。
/** System.out.pringln(new Date()); //或 String s = new Date().toString(); //或 Date birthday = new Date(); */ -
在对象与对象变量之间有区别。
/** Date deadline; //deadline是一个对象变量,不是实例,可以引用Date类型的实例,但现在还没有引用被赋实例。 //可以初始化变量或者引用一个已存在的实例来使用deadline。 */ -
Java中任何对象变量的值都是对存储在另一个地方的实例的引用。new操作符的返回值也是一个引用。
/** Date deadline = new Date(); //New Date()构造了一个Date的实例,返回了一个值,这个值是对新实例的引用。 //把引用存储在deadline中。 */可以显式地将对象变量赋值为null。局部变量不会自动初始化为null,必须new或者显式赋值null进行初始化。
- 所有Java实例都存储在堆(heap)中。当一个实例包含另一个对象时,这个实例变量依然包含着指向另一个堆对象的指针。
4.2.2 Java类库中的LocalDate类
-
Data实例有个状态,即特定的时间点。而LocalDate用来表示日历表示法。
-
不要使用构造器来构造LocalDate实例,应该使用使用静态工厂方法(factory method)代表使用构造器。
/** LocalDate.now() //构造一个实例表示构造时的日期 LocalDate.of(1999,12,31) //提供特定日期构造一个实例 */ -
可以使用getYear()、getMonthValue()、getDayOfMonth()得到年、月、日。
/** LocalDate newYearsEve = LocalDate.of(1999,12,31); int year = newYearsEve.getYear(); int month = newYearsEve.getMonthValue(); int day = newYearsEve.getDayOfMonth(); */
4.2.3 更改器方法与访问器方法
-
访问对象后,对象状态改变的方法是更改器方法(mutator method)。相反访问对象后,对象状态不改变的的方法是访问器方法(accessor method)。
/** LocalDate.getYear()就是一个访问器方法。 */ -
API java.time.LocalDate | 方法 | 解释 | | --- | --- | | static LocalTime now() | 构造一个表示当前日期的实例 | | static LocalTime of(int year,int month.int day) | 构造一个表示给定日期的对象 | | int getYear() | 得到当前日期的年 | | int getMonthValue() | 得到当前日期的月 | | int getDayOfMonth() | 得到当前日期的日 | | DayOfWeek getDayOfWeek() | 得到当前日期为星期几并返回实例 | | DayOfWeek getValue() | 得到1~7之间的数表示星期几 | | LocalDate plusDays(int n) | 返回当前日期之后n天的日期 | | LocalDate minusDays(int n) | 返回当前日期之前n天的日期 |
4.3 用户自定义类
-
在设计类时包括主力类(workhorse class),这些类没有main(),但有自己的实例域和实例方法。要想创建一个完整的程序应该讲若干类组合在一起,其中只有一个类有main()。
- 程序清单4-3 EmployeeTest.java/Employee.java
import java.time.*;
/**
* This program tests the Employee class.
* @version 1.12 2015-05-08
* @author Cay Horstmann
*/
public class EmployeeTest {
public static void main(String[] args){
// fill the staff array with three Employee objects
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// raise everyone's salary by 5%
for (Employee e : staff) {
e.raiseSalary(5);
}
// print out information about all Employee objects
for (Employee e : staff) {
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay());
}
}
}
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;
}
}
4.3.1 Employee类
- Java最简单的类定义形式:
class ClassName{
field1
field2
...
constructor1
constructor2
...
method1
method2
...
}
在一个源文件中只能有一个public公有类,但可以有任意数目的非公有类。
4.3.2 多个源文件的使用
如果一个程序的类单独放在每一个源文件中,有两种编译源程序的方法。
-
使用通配符
/** javac Employee*.java //所有与通配符匹配的源文件都被编译成类文件。 */ -
javac带有main()的源文件
/** javac EmployeeTest.java //javac编译时发现使用了其他类就会自动查找其他类的.class编译文件, //如果没有找到就会自动搜索其他类的.java文件并对其编译。 //重要的是如果.java版本比已有的.class文件新,则会重新编译。 */
4.3.3 剖析Employee类
-
方法可以被标记为public,为其他类提供操作这个类的方法,任何类的任何方法都可以调用这些方法。
强烈建议用private标记实例域,防止破坏封装。
-
预定义类的实例域本身就是实例,并不是实例变量。
/** private String name; private LocalDate hireday; private double salary; */
4.3.4 从构造器开始
-
在构造实例时,构造器会运行将实例初始化到定义的状态。
/** public Employee(String n, double s, int year, int month, int day){ name = n; salary = s; hireDay = LocalDate.of(year, month, day); } */构造器与类同名,每个类可以有一个或者以上的构造器。
构造器可以有多个参数,并且没有返回值。
构造器总是随着new的执行被调用,不能对一个已经存在的实例调用构造器来重新对实例赋状态。
- Java中所有的实例都是在堆中构造的。
警告:不要在构造器中定义与实例域重名的局部变量,在外部构造实例时无法设置变量值,而且会屏蔽同名的实例域。
4.3.5 隐式参数与显示参数
-
方法用于操作对象以及存取它们的实例域。
/** public void raiseSalary(double byPercent){ double raise = salary * byPercent / 100; salary += raise; } */ -
方法有两个参数。第一个参数称为隐式(implicit)参数,是在方法名前的类实例,但不会出现在方法声明中。第二个显式(explicit)参数位于
()中。在每个方法中,
this关键字表示隐式参数,可以用其编写方法将实例域与局部变量区别开。/** public void raiseSalary(double byPercent){ double raise = this.salary * byPercent / 100; this.salary += raise; } */- Java中所有的方法都在类内定义,但不代表它们是内联方法。是否将某个方法设置为内联方法是Java虚拟机的任务。即使编译器会监视调用简洁、经常被调用、没有被重载以及可优化的方法。
4.3.6 封装的优点
- 访问器方法由于只返回实例域值,所以又被称为域访问器。
- 有些时候需要获得或设置实例域的值。因此应该提供下面三样内容:
- 一个私有的数据域
- 一个公有的域访问器方法
- 一个公有的域更改器方法 可以改变内部实现,而且不会影响其他代码。同时更改器可以执行错误检查。
警告:不要编写返回引用可变对象的域访问器。
/**
//程序清单4-3引用
//在Employee中getHierDay()返回了一个Date:
class Employee{ private Date hireDay; ... public Date getHireDay(){ return hireDay; } ... }//LocalDate没有域更改器。但Date有getTime()可以设置毫秒数,Date可变破坏了封装性!
Employee harry = ...; Date d = harry.getHireDay(); double tenYearsInMilliSeconds = 10*365.25*24*60*60*1000; d.setTime(d.getTime()-(long)tenYearsInMilliSeconds);//这段代码d和harry引用了同一个对象,对d调动域更改器就可以自动改变这个雇员的私有状态!
//如果需要返回一个可变对象的引用,应该先对其克隆(clone)。
//对象clone是指存放在另一个位置上的实例副本。
class Employee{ ... public Date getHireDay(){ return (Date)hireDay.clone(); } ... }*/
4.3.7 基于类的访问权限
一个方法可以访问所调用实例的私有数据,也可以访问所属类的所有实例的私有数据。
4.3.8 私有方法
大多数方法都设计为public。但有时希望将一个计算代码分为若干个独立的辅助方法,而这些辅助方法不该成为public接口的一部分,因为他们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序,将方法设置为私有private。
4.3.9 final实例域
可以将实例域定义为final。构建实例时必须初始化这样的域。final修饰符大都应用于基本类型(primitive)域,或不可变(immutable)类(类中的方法都不会改变其对象)的域。
/**
//String就是一个不可变类。
*/
4.4 静态域与静态方法
4.4.1 静态域
如果将域定义为static,则所有的实例对与这个域都会有自己的一份拷贝。即使没有实例,静态域也存在,它属于类。
4.4.2 静态常量
- out是一个静态常量,它在System中声明。
- System类中有一个setOut(),可以将out设置为不同的流。setOut()是一个本地方法,不是Java语言实现的。本地方法可以绕过Java的存取控制机制。这是一种特殊的方法,自己编程时不应该这样。
4.4.3 静态方法
- 静态方法是一种不能向对象实施操作的方法。但是静态方法可以访问类自身的静态域。
-
可以使用实例调用静态方法。但是建议用类名调用。在下面两种情况使用静态方法:
- 一个方法不需要访问对象状态,其所有参数都是通过显式参数提供
- 一个方法只需要访问类的静态域
-
4.4.4 工厂方法
静态方法还可以作为静态工厂方法来构造实例。
/**
//NumberFormat使用工厂方法生成不同风格的格式化对象
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
MunberFormat percentFormatter = NumberFormat.getPercentInstance();
double x = 0.1;
System.out.println(currencyFormatter.format(x));
System.out.println(percentFormatter.format(x));
//为什么不使用构造器完成:
//1. 无法命名构造器,这里希望得到得到的货币实例和百分比实例采用不同的名字
//2. 当使用构造器时无法改变返回的实例类型。而Factory方法可以返回DecimalFormat实例,这是NumberFormat的子类
*/
4.4.5 main()
- main()不对任何对象进行操作。在启动程序时还没有任何一个对象,main()将执行并创建程序所需要的对象。
- 每一个类可以有一个main()。这是一个常用与对类进行单元测试的技巧。如果想要测试一个类,就对其加入main()并执行:java Classname。如果这个类是更大型应用的一部分,可以执行:java Application,内部的各个类的main()不会执行。
4.5 方法参数
-
在程序设计语言中,按值调用(call by value)表示方法接收的是调用者的提供的值;按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
“按……调用”(call by)是一个标准的计算机科学术语,用来描述各种程序设计语言中方法参数的传递方式。一个方法可以修改\传递引用\所对应的变量值,但不能修改\传递值\调用所对应的变量值。 Java总是采用按值调用。方法得到的是所有参数值的一个拷贝,特别是不能修改传递给它的任何参数变量的内容。
-
方法参数一共有两种类型
-
基本数据类型(数字、布尔值)
-
对象引用 一个方法不能修改一个基本类型的参数,但可以修改一个对象引用的参数。
/** //不可以修改基本类型数据参数 public static void tripleValue(double x){ x = 3 * x; } double percent = 10; tripleValue(percent); //1.x初始化为一个percent的拷贝 //2.x乘3变成了30,但percent不变 //3.方法结束,x不再使用
//可以修改引用类型数据参数 public static void tripleSalary(Employee x){ x.raiseSalary(200); } harry = new Employee(...); tripleSalary(harry); //1.x初始化引用harry的对象 //2.raiseSalary()提高了x和harry引用的对象的薪水 //3.方法结束,x不再使用。harry继续引用着薪水增长的对象。 */
-
-
Java对对象采用的不是引用调用,对象引用是按值传递的。
/** //方法传递的是引用的值,引用的内容,而不是引用本身。 */ -
一个方法不能让对象参数引用一个新对象。
4.6 对象构造
Java提供了多种编写构造器的机制。
4.6.1 重载
-
有些类有多个构造器,这种特征叫重载(overloading)。如果多个方法同名但不同参就会产生重载。
javac必须挑选出具体执行哪个方法,通过各个方法给出的参数类型与指定方法所调用的值类型来匹配,挑选出相应的方法。
如果javac找不到匹配的参数,就会产生编译时错误,这个过程称为重载解析(overloading resolution)。
-
Java允许重载任何方法。要完整描述方法需要指出方法名以及参数类型,这叫做方法的签名(signature)。返回类型不是签名的一部分。
/** //String有4个公有的indexOf(),它们的签名是: //indexOf(int) //indexOf(int,int) //indexOf(String) //indexOf(String,int) */
-
4.6.2 默认域初始化
- 如果构造器中没有显式给域赋初值,域就会被自动赋默认值。不建议赋默认值。
- 域域局部变量的主要不同点在于默认初始化。局部变量必须显式初始化。
4.6.3 无参数的构造器
在编写一个类时没有编写构造器,系统会自动提供一个无参构造器,在创建实例时会将所有实例域赋以默认值。
如果类中至少提供了一个构造器,但是没有提供无参构造器。在构造实例时不提供参数被视为不合法。
4.6.4 显式域初始化
重载构造器方法,可以采用多种形式设置实例域的初始状态。
确保不管怎样调用构造器,每个实例域都可以初始化一个有意义的值,这是一种良好的设计习惯。初始值可以是能返回匹配实例域类型的方法。
在设计构造器之前先设计实例域赋初值。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
4.6.5 参数名
在设计参数名时有一个常见的技巧:参数变量用与实例域同样的名字将实例域屏蔽起来。
/**
public Employee(String name,double salary){
this.name = name;
this.salary = salary;
}
*/
4.6.6 调用另一个构造器
this关键字还有另外一个含义,可以调用同一个类的另一个构造器。
/**
public Employee(double s){
//使用Employee(String,double)
this("Employee #" + nextId,s);
nextId++;
}
*/
4.6.7 初始化块
-
初始化数据域的方法:
- 在构造器中设置
- 在声明中赋值
- 初始化块(initialization block) 在一个类的声明中可以包含多个代码块,只要构造实例,这些初始化块就会优先于构造器执行,然后才执行构造器。
这种机制非必需,也不常见。通常直接将初始化块放在构造器中。
- 建议将初始化块放在域定义后。具体规则查看Java语言规范8.3.2.3节。
-
调用构造器的具体处理步骤:
- 所有数据域被初始化为默认值
- 按照在类声明中的顺序,依次执行所有域初始化语句和初始化块
- 如果当前构造器第一行调用了第二个构造器,则执行第二个构造器主体
- 执行当前构造器主体
-
如果对类的静态域进行初始化的代码比较复杂可以使用静态的初始化块,将代码放进一个块中并用static标记。
在类第一次加载的时候会进行静态域的初始化,所有的静态初始化语句和静态初始化块按照类定义的顺序执行。
-
API java.util.Random | 方法 | 解释 | | --- | --- | | Random() | 构造一个新的随机数生成器 | | int nextInt(int n) | 返回一个0~n-1之间的数 |
4.6.8 对象析构与finalize()
- 可以为任何一个类添加finalize()。finalize()将在垃圾回收器清除对象之前调用。在实际应用中不要依赖finalize()回收任何短缺的资源,因为很难知道这个方法什么时候调用。
-
System.runFinalizersOnExit(true)能够确保finalize()在Java关闭前被调用。不过不安全,不推荐使用。
可以使用Runtime.addShutdownHook添加“关闭钩”(shutdown hook),详细内容查看API。 如果某个资源需要在使用完毕立刻被关闭,那么需要人工管理。对象用完后,可以使用close()来完成相应的清理操作。
-
4.7 包
- Java允许使用包(package)将类组织起来。使用包的主要原因是确保类名的唯一性,同名类放在不同名包中就不会产生冲突。借助包可以方便组织自己的代码,并将自己和别人提供的代码库分开管理。
- 标准的Java类库分隔在多个包中,包括java.lang、java.util和java.net等。标准的Java包具有层次结构。可以使用嵌套层次组织包,所有标准的Java包都处于java和javax包层次中。从编译器的角度看,嵌套的包之间没有任何关系。
4.7.1 类的导入
-
一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)。有两种方式访问另一个包中的公有类:
- 在每个类名前添加完整的包名,使用包名引用
- 更常用的方式是使用import语句
-
import语句是一种引用包含在包中的类的声明。使用import导入一个特定的类或整个包。import应该位于源文件的顶部,package语句的后面。
/** //导入java.util包中所有的类 import java.util.*; */只能用
*导入一个包,不能导入以java为前缀的所有包。在导入含有同名类的两个不同名包时,要单独导入代码中使用的类,如果两个类代码都使用了,那么只能在每个类前使用包名引用。
-
在包中定位类是编译器(compiler)的工作。类文件中的字节码肯定使用完整的包名来引用其他类。
4.7.2 静态导入
import还可以导入静态方法和静态域。
/**
//import System的静态方法和静态域,使用时可以不用加类名前缀
import static java.lang.System.*;
//也可以导入特定的静态方法和静态域
*/
4.7.3 将类放入包中
-
将一个类放入包中,要将包的名字放在源文件的开头,包中定义类的代码之前。
/** package com.horstmann.corejava; public class Employee{ ... } */如果没有在源文件中放置package语句,这个源文件的类就被放置在一个默认包(default package)中。默认包没有名字。
-
包中的文件应该被放到与完整的包名匹配的子目录中。javac将类文件也放在相同的目录结构中。编译运行类时都要从基目录开始。
/** com.horetmann.corejava中的所有源文件在Windows中应该被放置在子目录com/horstmann/corejava中 */ -
javac对文件进行操作,而解释器加载类。
警告:javac在编译源文件时不检查目录结构,即使源文件不在子目录下也可以编译。如果源文件不依赖其他包就不会出现编译错误,但生成的类文件无法运行,除非将所有类文件移到正确的位置上。如果包与目录不匹配,虚拟机就找不到类。
4.7.4 包作用域
-
public标记的部分可以被任意类使用;private标记的部分只能被定义它们的类使用;没有标记的部分可以被同一个包中的其他类使用。
变量必须显式地标记private,否则将默认为包可见,会破坏封装性。
-
JDK1.2开始,类加载器明确禁止加载用户自定义的、包名以
java.开头的类,防止java包的封装性被破坏。用户自定义的包可以通过包密封(package sealing)机制解决各种包混杂在一起的问题。如果将一个包密封起来,制作包含包含密封包的JAR文件,就不能再向这个密封包中添加类。
4.8 类路径
- 类文件也可以存储在JAR(java归档)文件中。一个JAR文件可以包含多个压缩的类文件和子目录。在程序中用到第三方(third-party)库文件时,通常会给出一个或多个需要包含的JAR。
- JAR使用ZIP格式组织文件和子目录。可以使用ZIP程序查看内部。
- 多个程序共享类的方法:
-
把类放到一个目录中
-
将JAR放在与类目录同级的一个目录中
-
设置类路径(class path)。类路径是包含类文件的路径的集合。在Windows中以
;分隔。/** c:\classdir;.;c:\archives\archive.jar */
-
- 类路径包括:
-
基目录
-
当前目录
-
JAR 可以在JAR目录中指定通配符,例如:
/** c:\classdir;.;c:\archives* */ 运行时相应的库文件(rt.jar和jre/lib与jre/lib/ext下的一些其他的JAR文件)会被自动搜索,不用显式列在类路径中。
-
警告:javac总是在当前目录查找文件,但虚拟机只在类路径中有
.时才查看当前目录。如果没有设置类路径则默认包含.,但设置了类路径但没有.,程序只能编译不能运行。
-
类路径所列出的目录和归档文件是搜寻类的起始点。
当虚拟机查找类文件时,首先查看相应库文件,否则会按照类路径依次查找文件。
javac定位类文件时,首先查看相应库文件,否则会按照类路径依次查找文件。当源文件中未使用包名来引用类时,javac首先查找包含这个类的包,并查询所有的import,确认是否包含被引用的类。当javac查询import时,会先查找默认导入的java.lang,其次是当前包和import显式导入的包。
javac在定位类文件时也要查看源文件是否比类文件新。如果源文件比类文件新,那么源文件就会被自动的重新编译。
4.8.1 设置类路径
-
最好采用
-classpath(或-cp)选项指定类路径,并将其放在一个shell脚本或一个批处理文件中。/** java -cp c:\classdir;.;c:\archives\* MyProg */ -
也可以设置CLASSPATH环境变量完成类路径,直到退出shell为止,类路径设置均有效。
/** //Windows shell中命令格式 set CLASSPATH=c:\classdir;.;c:\archives\* */
警告:
- 如果将CLASSPATH设置为永久不变的值,这总的来说是一个很糟的主意。人们有可能会忘记全局设置。
- 而绕开类路径将所有文件放进jre/lib/ext中是一个极坏的主意。当手动加载其他的类文件时,如果将其放在扩展路径上则不能正常的工作。
4.9 文档注释
JDK中的javadoc工具可以由源文件生成一个HTML文档。联机API文档就是对标准Java类库的源代码运行javadoc生成的。
4.9.1 注释的插入
-
javadoc实用程序(utility)从几个特性中抽取信息:
- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
应该为上面几部分编写注释。注释应该放在所描述特性前,以
/**开始,以*/结束。
-
每个文档注释中在标记后紧跟自由格式文本(free-form text)描述。标记以
@开始。自由格式文本的第一句应该是概要性的句子。javadoc自动抽取这些句子形成概要页。
在自由格式文本中可以使用HTML修饰符。
- 如果文档中有其他文件的链接,应该将这些文件放到子目录doc-files中,并且在链接中也应该使用doc-files目录。javadoc将从源目录中拷贝这些目录及其中文件到文档目录中。
4.9.2 类注释
类注释必须放在import后,类定义前。
4.9.3 方法注释
在方法注释中除了通用标记还可以使用下面的标记:
-
@param变量描述 对当前方法的“param”(参数)部分添加一个条目。描述可以占据多行,可以使用HTML标记。一个方法的所有@param必须放在一起。 -
@return描述 对当前方法添加一个“return”(返回)部分。描述可以占据多行,可以使用HTML标记。 -
@throws类描述 对当前方法添加一个注释表明这个方法有可能抛出异常。/** /** Raise the salary of an employee. @param byPercent the percentage by which to raise the salary(e.g. 10 mean 10%) @return the amount of the raise */ public double raiseSalary(double byPercent){
double raise = salary * byPercent / 100; salary += raise; return raise; }*/
4.9.4 域注释
只需要对公有域(通常指静态常量)建立文档。
4.9.5 通用注释
-
下面的标记可以用于类注释:
-
@author姓名 生成一个“author”(作者)条目。可以使用多个@author,每个@author对应一个作者。 -
@version文本 生成一个“version”(版本)条目。“文本”可以是对当前版本的任何描述。
-
-
下面的标记用于文档注释中:
-
@since文本 生成一个“since”(始于)条目。“文本”可以是对引入特性的版本描述。 -
@deprecated文本 对类、方法、变量添加一个不再使用的注释。“文本”中给出了取代的建议。
-
-
下面的标记可以使用超级链接链接到javadoc文档的相关部分或外部文档:
@see引用 在“see also”部分中增加一个超级链接,可以用于类和方法。可以为一个特性添加多个@see,但必须将他们放在一起。共有三种引用的形式:
(1)package.class#feature label
提供类、方法或变量的名字,可以指定一个可选标签(label)作为链接锚(link anchor),javadoc就在文档中插入一个超链接。省略包名时链接定位于当前包中,省略包名和类名时链接定位于当前类。如果省略label,用户看到的锚的名称就是目标代码名或URL。一定要使用
#分隔类名于方法名或类名于变量名。/** @see com.horstmann.corejava.Employee#raiseSalary(double) */(2)<a href="...">label</a>
指定一个超链接链接到任何URL。可以指定一个可选标签(label)作为链接锚(link anchor),如果省略label,用户看到的锚的名称就是目标代码名或URL。
/** @see <a href="www.horstmann.com/corejava.html">The Core Java home page</a> */(3)"text"
" "中的文本会显示在“see also”部分。/** @see "Core Java 2 volume 2" */2.
@link引用 可以在注释中的任意位置放置指向其他类或方法的超级链接。引用形式:{@link package.class#feature label}提供类、方法或变量的名字,可以指定一个可选标签(label)作为链接锚(link anchor),javadoc就在文档中插入一个超链接。省略包名时链接定位于当前包中,省略包名和类名时链接定位于当前类。如果省略label,用户看到的锚的名称就是目标代码名或URL。一定要使用
#分隔类名于方法名或类名于变量名。/** {@link com.horstmann.corejava.Employee#raiseSalary(double)} */
4.9.6 包与概述注释
- 可以直接将类、方法、变量的注释放在Java源文件中。但要想写入包注释,需要在每一个包目录中添加一个单独的文件,有2种方式:
- 提供一个以package.html命名的文件。在标记
<body>...</body>之间的所有文本都会被抽取出来。 - 提供一个package-info.java命名的文件。文件在包语句后必须包含一个初始的以
/**和*/界定的javadoc注释,不可以包含更多的代码或注释。
- 提供一个以package.html命名的文件。在标记
- 也可以为所有源文件提供一个概述性的注释。此注释将被放在overview.html中,overview.html位于包含所有源文件的父目录中。标记
<body>...</body>之间的所有文本会被抽取出来。在导航栏中选择“Overview”时就会显示这些注释。
4.9.7 注释的抽取
-
在抽取注释前,计划一个存储注释HTML文件的目录docDirectory。
-
切换到包含想要生成文档的源文件目录。如果想为嵌套的包生成文档,需要切换到包含嵌套最外层的包的目录。
-
如果是一个包,运行:javadoc -d docDirectory nameOfPackage
如果是多个包,运行:javadoc -d docDirectory nameOfPackage1,nameOfPackage2...
如果源文件属于默认包,运行:javadoc -d docDirectory *.java
如果省略-d docDirectory,那么HTML会被提取到当前目录下,会带来混乱,不提倡这种做法。
-
-
可以使用多种形式命令行选项对javadoc调整。
可以使用-author和-version指定文档中包含@author和@version(默认下会省略)。
可以使用-link为标准类库添加超链接。
/** javadoc -link http://docs.orcle.com/javase/8/docs/api *.java //所有的标准类库类都会自动链接到Oracle的文档 */使用-linksource转换每个源文件为HTML(不对代码着色,但包含行编号),每个类和方法名将变为指向源代码的超链接。
其他的选项可以查阅javadoc的联机文档。
- 如果需要进一步的定制,例如生成非HTML的文档,可以提供自定义的doclet来生成任何输出形式。定制需求可以查阅联机文档。
4.10 类设计技巧
-
一定要保证数据私有
绝对不要破坏封装性,数据的表示形式很可能会改变,而使用方式却不会经常变化。最好保持实例域的私有性,当他们的表示形式变化时不会对类的使用者产生影响,即使出现bug也易于检测。
-
一定要对数据初始化
Java不对局部变量初始化,但会对实例域初始化。最好显式初始化,可以提供默认值,也可以在所有构造器中设置默认值。
-
不要在类中使用过多的基本类型
用其他的类代替多个相关的基本类型的使用,会使类更易于理解且修改。
-
不是所有的域都需要独立的域访问器和域更改器
-
将职责过多的类进行分解
将一个方法过多复杂的类分解成两个更为简单的类。
-
类名和方法名要能体现他们的职责
命名的良好习惯是采用一个名词、有形容词修饰的名词或动名词修饰的名词。对于域访问器和域更改器来说,习惯域访问器使用get开头,更改器使用set开头。
-
优先使用不可变的类
LocalDate以及java.time中的其他类是不可变的——没有方法能修改对象的状态,累死plusDays()并不是修改对象,而是返回状态已修改的新对象。 更改对象的问题在于,如果多个线程同时更新一个对象会发生并发更改,其结果是不可预料的。当然,并不是所有的类都必须是不可变的。