《Java核心技术卷I》阅读笔记——基本结构与类
Java 概述
关键特性
- 简单性:剔除了 C++ 中许多很少使用、难以理解、易混淆的特性
- 面向对象:主要差别在于多继承
- 分布式:能够通过 URL 打开和访问网络上的对象
- 健壮性:指针模型可以避免重写内存和损坏数据
- 安全性:适用于网络/分布式环境,防范堆栈溢出、进程空间内存破坏、未授权文件读写
- 体系结构中立:生成与特定计算机体系无关的字节码指令——虚拟机
- 可移植性
- 解释性:Java 解释器可以在任何移植了解释器的机器执行字节码
- 高性能
- 多线程
- 动态性
Java applet 与 Internet
- 在网页中运行的 Java 程序称为 applet
- 使用 applet时,只需要启用 Java 的 Web 浏览器执行字节码
基本程序设计结构
最简单的示例
1 | public class FirstSample |
- Java 区分大小写
- public 称为访问修饰符,控制程序的其他部分对这段代码的访问级別
- class 表明 Java 程序中的全部内容都包含在类中
- Java 中的所有函数都属于某个类的方法——main 方法必须有一个外壳类
- 类名字必须以字母开头,后面可以跟字母和数字的任意组合
- 类名是以大写字母开头的名词
- 源代码的文件名必须与公共类的名字相同
- main 方法必须声明为 public
注释
- 使用
//
- 使用
/**/
——不能嵌套 - 使用
/***/
——自动生成文档
数据类型
整型
- byte 和 short 用于特定的应用场合,如底层的文件处理或者需要控制占用存储空间量的大数组
- 整型的范围与运行 Java 代码的机器无关(C 和 C++ 程序需要针对不同的处理器选择整型)
- 长整型数值有一个后缀 L 或 l
- 十六进制数值有一个前缀 0x 或 0X
- 二进制数值有一个前缀 0b 或 0B
- 可以为数字字面量加下划线
1_000_000
- Java 没有任何无符号(unsigned)形式的整数
浮点
- 很少的情况适合使用 float 类型
- float类型的数值有一个后缀 F 或 f
- 十六进制表示浮点数值:
0.125 = 0xl.0p-3
,基数为 2,且尾数用十六进制表示,指数用十进制表示 - 特殊常量:
- Double.POSITIVE_INFINITY
- Double.NEGATIVE_JNFINITY
- Double.NaN(
Double.isNaN(x)
)
- 如果在数值计算中不允许有任何舍入误差,就应该使用 BigDecimal 类
char
char 类型的字面量值要用单引号括起来
char 类型的值可以表示为十六进制值,其范围从 \u0000 到 \Uffff(\u 为特殊字符的转义,可以出现在加引号的字符常量或字符串之外,如
public static void main(String\u005B\u00SD args)
)Unicode 转义序列会在代码解析之前处理
Unicode
- 码点(codepoint)是指与一个编码表中的某个字符对应的代码值
- 码点采用十六进制书写,并加上前缀 U+——U+0041 是拉丁字母 A 的码点
- Java 中 char 类型描述了 UTF-16 编码中的一个代码单元,即用 16 位表示(UTF-16 编码采用不同长度的编码表示所有 Unicode 码点)
- 强烈建议不要在程序中使用 char 类型,而是将字符串作为抽象数据类型处理
bool
- true 与 false
- Java 的整型值和布尔值之间不能进行相互转换
变量
- Java 中,每个变量都有一个类型
- 变量名必须是一个以字母开头并由字母或数字构成的序列,字母包括’A’-’Z’、 ’a’-’z‘、’_‘、’$‘或在某种语言中表示字母的任何 Unicode字符(Character 类的
isJavaldentifierStart
和isJavaldentifierPart
方法检查) - 大小写敏感
- 必须用赋值语句对变量进行显式初始化
- 利用关键字 final 指示常量
final double CM_PER_INCH = 2.54;
(只能被赋值一次,且名字通常全大写) - 类常量可以在一个类中的多个方法中使用,定义于 main 方法外
public static final double CM_PER_INCH=2.54;
运算符
- 整数除以 0 会产生一个异常,浮点数除以 0 会得到无穷大或 NaN
- Math 类中,包含各种数学函数
- 幂运算必须借助 Math 类的 pow 方法
double y = Math.pow(x, a);
- 重要常量:
Math.PI
与Math.E
- 幂运算必须借助 Math 类的 pow 方法
类型转换
- 强制类型转换
- 最接近整数:
Math.round()
自增与自减
- 同 C++
- 前缀,则先返回后加1;后缀相反
关系
- 沿用 C++
&&
与||
具有“短路”特点- 支持三元操作符 ?:
位运算
- &、|、~、^
- 算数移位:
<<
与>>
,用符号位填充高位(右操作要完成模32的运算,即1<<35
等价于1<<3
) - 逻辑移位:
>>>
用0填充高位
运算符优先级
- 同级别从左到右
- Java 不使用逗号运算符
枚举类型
enum Size {SMALL, MEDIUM, LARGE, EXTRA, LARCE};
Size s = Size.MEDIUM;
- 只能存储某个枚举值,或者
null
字符串
每个用双引号括起来的字符串都是 String 类的一个实例
String.substring()
:提取一个子串允许用 + 拼接两个字符串,或将一个非字符串的值转化后拼接
不可变字符串
- String 类没有提供修改字符串的方法
- 必须提取需要的字符,再拼接替换的字符串
字符串相等
String.equals()
==
只能确定两个字符串是否来自同一内存地址
空串与Null
- 空串长度为零:
if (str.length() = 0)
或if (str.equals(""))
- 首先检查不为 null,再检查是否空串
if(str != null && str.length() != 0)
String的API
略,可通过浏览器指向安装 JDK 的 docs/api/index.html 子目录,查询 API 文档
StringBuilder类
1 | StringBuilder builder = new StringBuilder(); |
输入与输出
读取输入
实例化
Scannner
(需要import java.util
):Scanner in = new Scanner(System.in);
输入中有空格:
String name = in.nextLine();
输入以空格为分隔符:
String firstName = in.next();
读入整数:
int age = in.nextlnt();
读入浮点数:
double age = in.nextDouble();
从控制台读取:
1
2
3Console cons = System.console()r;
String username = cons.readLine( "User name: ");
char[] passwd = cons.readPassword( "Password:" );
格式化输出
沿用 C 的 printf
使用静态的 String.format 方法创建一个格式化的字符串
文件输入输出
- 读取文件:
- 用 File 对象构造一个 Scanner 对象:
Scanner in=new Scanner(Paths.get("myflle.txt"), "UTF-8");
- 目录的每一个反斜杠要加一个反斜杠
- 之后用前面的 Scanner 方法对文件读取
- 用 File 对象构造一个 Scanner 对象:
- 写入文件:
- 构造一个 PrintWriter 对象:
PrintWriter out=new PrintWriter("myfile.txt", "UTF-8");
- 若文件不存在则创建文件
- 之后用 print,printf,println 方法写文件
- 构造一个 PrintWriter 对象:
- 相对路径:文件位于 Java 虚拟机启动路径的相对位置
控制流程
与 C++ 的控制流程类似,但没有 goto 语句,且有变形的 for 循环以及带标签的 break 语句
块作用域
- 大括号之间的语句
- 不能在嵌套的两个块中声明同名变量
条件
- 同 C++
- 没有大括号,则 else 语句与最近邻的 if 是一组
循环
- while 语句同 C++,首先检查循环条件
- do-while 语句同 C++
确定循环
- for 类似 C++
- 循环中,检测两个浮点数是否相等需要格外小心——浮点数存在舍入误差,可能得不到精确值
switch语句
- 同 C++,存在 case 穿透
中断控制流程
break:
同 C++
带标签:标签必须放在希望跳出的最外
层循环之前——实际上,可以将标签应用到任何语句中,甚至可以应用到 if 语句或者块语句1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Scanner in = new Scanner(System.in);
int n;
read_data:
while (. . .)//this loop statement is tagged with the label
for (. ..)//this inner loop is not labeled
{
Systen.out.print("Enter a number>= 0: ");
n = in.nextlnt();
if (n<0)//should never happen-can’t go on
break read_data;
//break out of readjata loop
}
//this statement is executed immediately after the labeled break
if (n <0)//check for bad situation
{
//deal with bad situation
}
else
{
//carry out normal processing
}
continue:
- 同 C++
- 带标签
大数值
- 使用
java.Math
中的BigInteger
与BigDecimal
,处理任意长度数字值 Biglnteger a = Biglnteger.valueOf(100);
- 需要用
add
、multiply
、subtract
、divide
方法运算 - Java 不提供运算符重载功能
数组
- 必须用 new 运算符创建数组
int[] a = new int[100];
- 数组元素初始化为0、false、null
- 获得长度:
array.length
- 数组长度不可改变(数组列表 array list 长度可变)
for each
依次处理每个元素,不必考虑下标
for (variable : collection) statement
定义一个变量暂存集合的每一个元素,collection 必须是一个数组或一个有 Iterable 接口的对象
1
2for(int element:a)
System.out.println(element);
数组初始化与匿名数组
- 初始化:
int[] small Primes = {2, 3, 5, 7, 11, 13};
- 匿名数组:在不创建新变量的情况下重新初始化一个数组
small Primes = new int[] {17, 19, 23, 29, 31, 37};
数组拷贝
- 一个数组变狼拷贝给另一个数组变量,此时两个变量引用同一个数组
int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers, luckyNumbers.length);
将值拷贝到新的数组- 第二个参数为新数组长度
- 数组元素为数值,多余元素赋值为0;布尔,赋值为 false
命令行参数
- 每个 main 带有
String[] args
- main 方法接收一个字符串数组,即命令行参数
数组排序
java.util.Arrays
Arrays.sort(array)
无返回值,array 被排序Math.random()
返回一个 0-1 的浮点数
多维数组
double[][] balances = new double[10][100];
或者直接初始化(类似一维数组)
for each 语句只能按照一维数组处理
1
2
3for(double[] row: a)
for(double value: row)
// do sth with value
不规则数组
可以单独取出二维数组的一行,或者让某两行交换
构造不规则数组,即每一行长度不同
1
2
3int[][] odds = new int [max+1][];
for(int n = 0; n <= max; n++)
odds[n] = new int [n+1];
对象与类
OOP
类
- 由类构造(construct)对象的过程称为创建类的实例 (instance)
- Java 编写的所有代码都位于某个类的内部
- 封装是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式,实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域(对象中的数据)
- 所有的类都源自于 Object
对象
- 对象的行为(behavior)——可以对对象施加哪些操作
- 对象的状态(state)——对象如何响应这些方法
- 对象标识(identity)——如何辨别对象
- 对象的行为由可调用的方法定义
- 对象状态的改变必须通过调用方法实现
- 每个对象都有一个唯一的身份
识别类
- 找名词与动词——名词作为类,动词作为方法
- 以上只是原则上的经验
类的关系
- 依赖(uses-a):一个类的方法操作另一个类的对象
- 聚合(has-a):类 A 的对象包含类 B 的对象
- 集成(is-a):特殊与一般
预定义的类
对象与对象变量
- 用构造器(constructor)构造新的实例
- 构造器名字与类名相同,需要加 new 操作符
new Date()
- 将一个方法应用于新创建的对象
String s = new Data().toString();
- 定义对象变量
Date deadline;
- 可以引用一个对象
- 变量本身不是一个对象
- 要初始化或引用后才能使用对象的方法
- 任何对象变量的值都是对存储在另外一个地方的一个对象的引用,new 返回的也是一个引用
更改器方法与访问器方法
- 只访问对象,不更改对象
- 需要修改对象
- C++ 中,默认为更改器方法,除非加了 const
自定义类
- 完整程序需要组合若干个类,但只有一个类有 main 方法
示例
最简单的类定义形式:
1
2
3
4
5
6
7
8
9
10
11
12class ClassName
{
field1;
field2;
...
constructor1;
constructor2;
...
method1;
method2;
...
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/*
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);
*/
class Employee
{
// instance fields
private String name;
private double salary;
private Local Date hireDay;
// constructor
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
//a method
public String getName()
{
return name;
}
//...
}
多个源文件的使用
- 一般一个类放到一个源文件中
- 可以直接
javac EmployeeTest.java
编译,因为会自动搜索相关的文件
具体剖析
public
:任何类的任何方法都可以调用这些方法private
:确保只有类自身的方法能访问这些实例域,其他类的方法不能访问——不提倡用 public 标记实例域,因为破坏了封装- 类通常包括类型属于其他类类类型的实例域
构造器
- 构造器与类同名
- 总是伴随 new 操作符的执行被调用,不能对已存在的对象调用构造器重新设置实例域
- 不要在构造器中定义与实例域重名的局部变量
隐式参数与显式参数
- 类中方法有隐式参数和显示参数,显式参数、是明显地列在方法声明中
- 每一个方法中,关键字 this 表示隐式参数,可以将实例域与局部变量明显地区分开
- C++ 将定义在类内部的方法设为内联方法,但 Java 将此交给虚拟机完成——即时编译器会监视调用那些简洁、经常被调用、没有被重载以及可优化的方法
封装优点
- 如果要获得/设置实例域的值,需要设置:
- 一个私有的数据域
- 一个公有的域访问器方法
- 一个公有的域更改器方法
私有方法
- 设置方法为 private
- 不常见——一些辅助方法不应该成为公有接口的一部分,这些方法与当前的实现机制非常紧密
final 实例域
- 构建对象时必须初始化 final 实例域
- final 修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域——类中的每个方法都不会改变其对象
静态域与静态方法
静态域
- 每个类的所有实例都共享这一个静态域,但每个对象都会有自己的实例域
- 静态域属于类,不属于对象
静态常量
private static final double PI = 3.1415926
- 可以用 类名.PI 的方式获得常量
- 如果不为静态,则不能这样访问
静态方法
- 静态方法不能向对象实施操作
- 静态方法没有 this 参数
- 不能访问实例域,但可以访问自身类中的静态域
- 场景:
- 方法不需要访问对象状态,所需参数都来自显式参数
- 方法只需要访问类中的静态域
工厂方法
main
main 方法不对任何对象操作(程序启动时也没有任何一个对象)
每个类可以有一个 main 方法,常用于对类进行单元测试
方法参数
按值调用:方法接收调用者提供的值
按引用调用:方法接收调用者提供的变量地址
Java 总是按值调用,即得到参数值的一个拷贝
一个方法不能修改一个基本数据类型的参数,即下面执行后 a 仍然为10
1
2int a = 10;
increase_a(a);但可以将对象引用作为参数——方法得到对象引用的拷贝,而对象引用和其他的拷贝同时引用同一个对象
即 java 中:
- 一个方法不能修改一个基本数据类型的参数(数值型、布尔型)
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用一个新的对象
对象构造
重载
- 多个方法有相同的名字,但参数不同,则产生重载
- Java允许重载任何方法,而不只是构造器方法
- 完整描述一个方法,需要指明方法的签名,即方法名与参数类型
默认域初始化
- 如果构造器没有显式给域赋初值,则被自动赋为默认值
- 数值为 0,布尔为 false,对象引用为 null
无参数构造器
- 对象由无参数构造函数创建时,其状态会设置为适当的默认值
- 如果在编写一个类时没有编写构造器,系统就会提供一个无参数构造器,将所有的实例域设置为默认值——仅当类没有提供任何构造器的时候,系统才会提供一个默认的构造器
显式域初始化
可以在类定义中,直接将一个值赋给任何域
1
2
3
4class Employee
{
private String name = "";
}初始值不一定是常量值
1
2
3
4
5
6
7
8
9
10
11class Employee
{
private static int nextld;
private int id = assignld();
private static int assignld()
{
int r = nextld;
nextld++;
return r;
}
}
调用其他构造器
构造器的第一个语句形如 this(…),则调用同一个类的其他构造器
1
2
3
4
5
6public Employee(double s)
{
// calls Employee(String, double)
this("Employee #" + nextld, s);
nextld++;
}
初始化块
- 初始化数据域
- 构造器中设置值
- 声明中赋值
- 初始化块:只要构造类的对象,这些块就会被执行(不常见)
1 | class Employee |
对象析构
- Java 不支持析构器——自动的垃圾回收器,不需要人工回收内存
- 某些对象使用了内存之外的其他资源:可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用
包
- Java 允许使用包(package)将类组织起来,确保类名的唯一性
- 可以使用嵌套层次组织
- 编译器的角度来看,嵌套的包之间没有任何关系——java.util 包与 java.util.jar 包没有任何联系
类的导入
一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)
导入方法:
- 导入一个包的所有类
import java.util.*;
- 导入一个包的特定类
import java.util.LocalDate;
- 导入一个包的所有类
只能使用星号导入一个包,不能
import java.*;
当两个包都有同一个类,增加特定的语句
1
2
3import java.util.*;
import java.sql .*;
import java.util.Date;
静态导入
- 导入静态方法,即
import static java.lang.System.*;
可以使用类 System 的静态方法和静态域,不用添加类名前缀
类放入包中
必须将包的名字放在源文件的开头,包中定义类的代码之前
1
2
3
4
5package com.horstiann.corejava;
public class Employee
{
...
}如果没有在源文件中放置 package 语句,这个源文件中的类就被放置在一个默认包(default package)
放到不同的包中
1
2$ javac com/mycompany/PayrollApp.java
$ java com.mycompany.PayrollApp
包的作用域
- 标记为 public 的部分可以被任意的类使用;标记为 private 的部分只能被定义它们的类使用;如果没有指定 public 或 private,则这个部分(类、方法或变量)可以被同一个包中的所有方法访问
类路径
- 类存储在文件系统的子目录中,类的路径必须与包名匹配
文档注释
- javadoc 可以由源文件生成一个 HTML 文档
注释的插入
- 注释应该放置在所描述特性的前面
- 格式为
/***/
- 标记由 @ 开始
- 自由格式文本中,可以使用 HTML 修饰符
类注释
类注释必须放在 import 语句之后,类定义之前
方法注释
每一个方法注释必须放在所描述的方法之前
域注释
只需要对公有域(通常指的是静态常量)建立文档
通用注释
- @auther
- @version
- @since
- @deprecated
- @see
包与概述注释
- 产生包注释,需要在每一个包目录中添加一个单独的文件
- 以 package-info.java 命名的 Java 文件
- 文件必须包含一个初始的
/***/
注释
注释的抽取
- 切换到包含想要生成文档的源文件目录
- 运行
javadoc -d 目标目录 nameOfPackage
类的设计
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类