1. 面相对象程序概述
- 当今主流的程序设计范型;取代 20 世纪 70 年代的结构化或者过程式变成技术;Java 则是面向对象的;所以必须要熟悉 OOP;
- 程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分;只要对象能够满足要求,则不关心其功能如何实现;
- 传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题,一旦确定过程,即选择数据存储的适当方式;这也是为啥 Pascal 语言作者著作命名: 算法 + 数据结构 = 程序;但在 OOP;调换次序了;数据第一位,再考虑操作数据的算法;
- 面向过程是和规模小的问题;但是面向对象更加是和解决规模较大的问题;
1.1. 类
class
是构造对象的模板;类构造对象的过程称为创建类的实例;- 封装:处理对象的一个重要概念;
- 实例字段:对象中的数据;作为类的实例,特定对象都有一组特定的实例字段值,这些值的集合,就是这个对象的当前状态;
- 方法:操作数据的过程称为方法;
- 封装:不能让类中的方法直接访问其他类的实力字段,程序只能通过对象的方法与对象数据进行交互;封装给对象赋予了黑盒特征;
1.2. 对象
-
对象的三个主要特性:
-
行为:可以对对象完成哪些操作,或者可以对对象应用哪些方法;
-
状态:当调用那些方法的时候,对象会如何响应;
-
标识:如何区分具有相同行为与状态的不同对象;
-
1.3. 类之间的关系
-
常见关系:
-
依赖:user-a
-
聚合:has-a
-
继承:is-a
-
2. 预定义类
2.1. 对象与对象变量
- 要想使用对象,首先肯定需要先构造出来对象,并且指定其初始状态;
- 在 Java 中,需要使用构造器来进行构造新的实例,构造器是一种特殊的方法,用来构造并且初始化对象;
Java 中,有一个
Date
类,其对象可以描述一个时间点;为什么 Java 不像别地语言内置一个类型?比如说 vb有一个内置 date 类型; 程序员可以直接使用指定格式日期;看上去方便;但是 vb 适应性不高,在不同的地区,日期格式不同;所以说对于 java 的设计,如果类设计不完善,程序员完全可以增强或者替代系统提供的类;tips:java的日期类库开始时很混乱,现在已经重新设计了两次;
2.2. LocalDate 类
-
Date
实例有个状态,即 特定的时间点;即UTC
时间 1970 年 1 月 1 日 00:00:00; -
这种特定日期遵循了世界大多数地区的 Gregorian 阳历;但是同样的时间点采用中国或者希伯来阴历来描述就不一样了;
-
类库设计者决定将保存时间与给时间点命名分开,所以标准 Java 的类库分别包含了两个类:一个是用来表示时间点的
Date
类;还有一个是用大家熟悉的日历表示法表示日期的LocalDate
类; -
不要使用构造器来构造
LocalDate
类的对象,实际上,应当使用静态工厂方法,它回代表你调用构造器;例如:LocalDate.now()
; -
也可以提供日期来构造对应一个特定日期的对象:
LocalDate.of(1999, 12, 31)
;LocalDate date = LocalDate.of(1999, 12, 31); int year = date.getYear(); // 1999 int month = date.getMonthValue(); // 12 int day = date.getDayOfMonth(); // 31
-
看起来没有多大意义,但是有时可能有一个计算得到的日期;例如
plusDays
方法得到一个新的LocalDate
;他会告诉你距离当前对象指定天数的一个新日期; -
其实
Date
类也有得到日月年的方法,但是已经废弃;
Date
类 在设计之初,因为考虑不周到,只是硬生生的返回数据值,无扩展性,不易国际化等等诸多缺陷;已经被新的,更完善的类、方法进行替代了;例如getYear
方法获取的是 1900 年以后得年数;getMonth
是从 0 开始;等等很多问题的存在;
3. 用户自定义类
class ClassName {
field1;
field2;
...;
constructor1;
constructor2;
...;
method1;
method2;
...
}
3.1. 多个源文件的使用
- 如果一个源文件包含两个类;那么一般分成两个源文件;例如
Demo
和DemoTest
两个类会分成两份java
文件; - 如果需要编译;那么一种是
javac Demo.java
;还有一种是javac DemoTest.java
;虽然第二种未显示编译Demo.java
;但是 Java 编译器发现了DemoTest.java
使用Demo
的类之后;会去查找Demo.class
如果没有找到,那么会自动搜索Demo.java
文件,进行编译;且如果Demo.java
相较于已有的Demo.class
文件版本更新了,会自动重新编译文件;
如果熟悉 Unix 的 make 工具,或者 Windows 的 nmake 等工具;可以认为 Java 编译器内置了 make 功能;
3.2. 构造器
- 构造器总是会结合
new
运算符来进行调用; - 构造器与类同名;
- 每个类可以有一个以上的构造器;
- 构造器可以是 0 , 1 或者多个参数;
- 构造器无返回值;
3.3. 用 var 声明局部变量
- 可以使用
var
关键字声明局部变量,而无需指定类型;只能用于方法的局部变量;参数和字段的类型必须要进行声明;
3.4. 使用 null 引用
- 可以使用
null
复制给对象;但是如果对null
值应用一个方法,则会产生NullPointerException
异常;
3.5. 隐式参数与显示参数
- 隐式参数:出现在方法名签的类型的对象;调用函数的对象,在类的方法中调用了类的实例域,这个被调用的实例域就是隐式参数。或者说是当前方法的对象, 一般我们会使用
this
关键字来使用隐式参数,this
表示调用该方法的当前类的当前实例,使用this
关键字可以很好的把显式参数和隐式参数分离开。 - 显示参数:方、法名括号中间的参数,就是所谓能看得见的参数;
3.6. 封装的优点
-
有的时候,需要获取或者设置实例的字段的值;需要提供:
-
一个私有的数据字段
-
一个公共的字段访问器方法
-
一个公共的字段更改器方法
-
3.7. 基于类的访问权限
同一个类 | 同一个包 | 不同包的子类 | 不同包的非子类 | |
---|---|---|---|---|
private |
√ | |||
default |
√ | √ | ||
protected |
√ | √ | √ | |
public |
√ | √ | √ | √ |
3.8. final 实例字段
可以将字段定义为 final
;则构造对象时必须初始化;以后不能再进行修改了;
- 必须要初始化;但是可以初始化为
null
;
4. 静态字段与静态方法
4.1. 静态字段
- 将一个字段定义为
static
;则每个类只有一个这样的字段,每个对象共享这个字段;对于非静态的实例字段,每个对象都有自己的一个副本; - 在
C++
中它叫类字段;
4.2. 静态常量
-
静态变量使用的少,但是静态常量使用的多;
public class Math { ... public static final double PI = 3.14159265358979323846; ... }
-
在程序中可以通过
Math.PI
进行访问常量;
在 Java 中 System.out 其实也是一个静态常量;按理说不许修改 final 变量的值;但是为什么会有一个 setOut 方法去修改?因为这个方法是一个原生方法,并非在 Java 语言中实现的;原生方法可以绕过 Java 语言的访问控制机制;是一种特殊的解决方法;自己写的时候就不要模仿啦;
4.3. 静态方法
-
是不在对象上执行的方法;并不使用任何对象;没有隐式参数;
-
静态方法在静态方法之中可以直接进行调用;其余需要 类.静态方法() 调用;
-
下面两种情况可以使用静态方法;
-
方法不需要访问对象状态,因为它所需要的所有参数都通过显式参数来进行提供;
-
方法只需要访问类的静态字段;
-
4.4. 工厂方法
-
静态方法还有一种常见的用它;即静态工厂方法来构造对象;
-
为什么不直接用构造器完成静态工厂方法的实现创造对象呢?
-
无法命名构造器;这里希望两个不同的名字;比如说
LocalDate.now()
; -
使用构造器,无法改变所构造对象的类型;而构造方法可以返回其子类;例如说
NUmberFormat
类,其NumberFormat.getCurrencyInstance()
返回了DecimalFormat
类的对象;这是它的一个子类;
-
4.5. main 方法
- main 方法不对任何对象进行操作;事实上,是启动程序时还没有对象;静态的
main
方法将执行并构造程序所需要的对象; - 每个类都可以有一个
main
方法;这是常用于对类进行单元测试的一个小技巧;
5. 方法参数
-
回顾
-
按值调用:表示方法接收的是调用者提供的值;
-
按引用调用:表示方法接收的是调用者提供的变量地址;
-
-
Java 总是按照按值调用;Java 没有按引用调用的;
public class Main {
public static void main(String[] args) throws IOException {
Demo demo1 = new Demo(1);
Demo demo2 = new Demo(2);
swap(demo1, demo2);
System.out.println(demo1);
System.out.println(demo2);
}
// 如果java 对象是按照按引用调用;那么这个方法可以实现交换;
// 实际上并不能完成交换;交换的是两个对象引用的副本;
// 如果输出其 hashCode ;会发现;交换后的的 hashCode 值也对调了;但是其在主方法中输出的 demo1 的 hashCode 并没有变化;方法所得到的只是传入参数值的一个拷贝;
public static void swap(Demo demo1, Demo demo2) {
Demo tmp = demo1;
demo1 = demo2;
demo2 = tmp;
// 此时 demo1 如果修改对象状态;那么最后 main 方法中的 demo2 的内存变量状态就被修改了;但实际上 main 方法中的 demo1 的对象引用并未被修改;没有说被修改成了 demo2 的对象引用了;当这个方法结束;其 demo1,demo2 的变量被抛弃了; 原来的 main 方法中的 demo1 和 demo2 仍然引用了方法调用之前所引用的对象;
}
}
class Demo {
public Integer value;
public Demo(Integer value) {
this.value = value;
}
@Override
public String toString() {
return "Demo{" +
"value=" + value +
'}';
}
}
Java 即便传入的是对象变量;也是按值调用的;方法所得到的只是传入参数值的一个拷贝;但是对象变量所指向的对象的状态却可以进行改变;因为你可以把对象变量理解为 Java 指针;指向了一块内存区域,即便方法接收的只是传入值的拷贝;但是实际上这个拷贝和原变量指向的是同一块内存区域,通过拷贝只是修改了该内存区域的内容;当原变量再查看该内存区域时,其值已经改变了;
Java的对象传值实际上是按值调用;传递的是堆上的地址;就是在栈上开辟了一块空间;拷贝了这个对象引用;只不过这个对象引用和原来的对象指向的是同一块内存地址;你修改的是堆上的东西;也就是对象的状态;但是实际上并没有修改对象的引用;也就是说栈的地址其实是被拷贝的;你并不能修改原来的栈的值;
-
总结
-
方法不能修改基本数据类型的参数(即数值型或者布尔型);
-
方法可以修改对象参数的状态;
-
方法不能让一个对象参数引用一个新的对象;
-
6. 对象构造
6.1. 重载
- 类可以有多个构造器;
- Java 可以重载任何方法;不仅仅是构造器方法;但方法的签名要不同;
- 返回类型不属于方法签名;
6.2. 默认字段初始化
- 数值为 0,布尔值为
false
,对象引用为null
; - 字段与局部变量的一个重要区别,方法中的局部变量必须要明确的初始化;但是在类中,如果没有初始化类中字段,则会自动初始化默认值;
6.3. 无参的构造器
- 很多类都包含一个无参的构造器;对象的状态会设置为适当的默认值;
- 仅当类没有任何其他构造器的时候,才会得到一个默认的无参构造器;
6.4. 显示字段初始化
-
可以在类定义中直接为任何字段进行赋值;
// 如果一个类的所有构造器都希望把某个特定的实例字段设置为同一个值;则这个语法就特别有用; class Demo { private String name = "..."; ... }
6.5. 调用另一个构造器
- 构造器的第一个语句
this (...)
;这个构造器将会调用同一个类的另一个构造器;
6.6. 初始化块
-
除了在构造器中、声明中赋值外;还有初始化块;
class Demo { fields; { // object initialization block } functions(); }
- 类的初始化块可以有多个;
- 首先执行初始化块;再运行构造器;
- 但是这种机制并非必须;也不常见;通常会将初始化块代码放在构造器中;
-
调用构造器的处理步奏:
-
如果构造器第一行调用另外一个构造器;则基于所提供的参数去执行第二个构造器;
-
否则
- 所有数字字段初始化为默认值;
- 按照在类声明中出现的顺序,执行所有字段初始化方法和初始化块;
-
执行构造器主体代码:
public class Main { public static void main(String[] args) { Demo demo = new Demo(); Demo demo2 = new Demo(); } } class Demo { { System.out.println("初始化块"); } static { System.out.println("静态初始化块"); } public Demo() { System.out.println("构造方法"); } } /** 静态初始化块 初始化块 构造方法 初始化块 构造方法 */
public class Main { public static void main(String[] args) { Son son = new Son(); } } class Parent { { System.out.println("Parent 初始化块"); } static { System.out.println("Parent 静态初始化块"); } public Parent() { System.out.println("Parent 构造方法"); } } class Son extends Parent { { System.out.println("Son 初始化块"); } static { System.out.println("Son 静态初始化块"); } public Son() { System.out.println("Son 构造方法"); } } /** Parent 静态初始化块 Son 静态初始化块 Parent 初始化块 Parent 构造方法 Son 初始化块 Son 构造方法 */
-
-
总结:
-
静态初始化块只会执行一次;
-
执行顺序:静态初始化块->初始化块->构造方法
-
执行顺序:父类静态初始化块->子类静态初始化块->父类初始化块->父类构造方法->子类初始化块->子类构造方法;
-
当然;你应该精心的组织好初始化代码;有利于其他人理解;如果让类的构造器依赖于数据字段声明的顺序;会很奇怪,且容易引起错误;
-
在 jdk 6 之前; 甚至可以写一个没有 main 方法的 “Hello world”;只需要在静态初始化块中写即可;会先执行静态初始化块;再显示消息指出 main 未定义;但 Java 7 后,java会首先检查是否由 main 方法了;
6.7. 对象析构与 finalize 方法
- Java 会自动完成垃圾回收;不需要人工回收;所以不支持析构器;
- 当然某个对象使用内存只玩的其他资源;例如文件或使用了系统资源的另一个对象的句柄;这样;资源不在使用的时候;回收会显得很重要;那么应该提供
close
方法;
7. 包
7.1. 包名
- 使用包的主要原因:确保类名唯一性;
- 一般采用域名倒置;
7.2. 类的导入
-
一个类可以使用所属包的所有类;以及其他包中的公共类;
-
可以使用两种方式来访问另一个包的公共类;
-
使用完全限定名;
-
import
语句进行导入;
-
-
*
只能导入一个包;而不可使用import java.*.*
; -
如果两个包中有类名冲突;那么有一个就需要使用完全限定名;
-
包中定位类是编译器的事情;类文件的字节码总是用完整的包名引用其他类;
7.3. 静态导入
-
可以采用
import
进行导入静态方法和静态字段;而不仅仅是类;import static java.lang.System.*; // 可以直接使用 System 类的静态方法和静态字段;而不必采用类名前缀; out.println("111"); // 亦可以导入特定方法或字段 import static java.lang.System.out;
7.4. 在包中增加类
- 要想类放入包中;需要将包的名字放在源文件开头;即采用
package
; - 如果没有在源文件中放置
package
语句;则源文件中的类属于无名包;
编译器在编译源文件的时候不检查目录结构;如果他的 package 与目录不对应;也可以进行编译;如果它不依赖其他包;就可以通过编译;但是最终的程序将无法运行;除非将所有类文件移动到正确的位置;如果包合目录不匹配;虚拟机就找不到类;
7.5. 包访问
在默认情况下,包并非是封闭的实体;也就是说,任何人都可以向包中添加更多的类;当然有恶意的或者低水平的,可能利用包的可见性添加一些能修改变量的代码;例如:
package java.awt;
这样就可以把得到的类文件防止在类路径上某处的 java/awt
子目录下,这样就可以访问 java.awt
包的内部了;
从 1.2 版本开始,JDK 实现着修改了类加载,明确禁止了加载包名以 “java.” 开头的用户自定义类;当然用户自定义的类,是无法从这种保护中受益;
另外一种机制是让 JAR
文件声明包为密封的 sealed
;以防止第三方修改;但是这种机制已经过时;
7.6. 类路径
-
类存储在文件系统的字母中,类的路径必须要与包名匹配;
-
JAR 文件使用 zip 格式进行组织;
-
为了让类可以被多个程序共享;则应:
-
把类文件放在一个目录中;且注意这个目录是包树状结构的 基目录;
-
将 jar 文件放在一个目录中;
-
设置类路径;类路径是所有类文件的路径的集合;
-
Unix 中,类路径使用冒号分隔;Windows 中,采用分号分隔;都采用句点表示当前目录;
-
类路径包含:
-
基目录;
-
当前目录;
-
jar 文件
-
-
-
从 Java 6 开始,可以在 JAR 文件目录中指定通配符了;即:
/home/classdir:.:/home/classdir/*
(Unix 中,*必须要转义防止 shell 扩展);
7.7. 设置类路径
- 类路径所列出的目录和归档文件是搜寻类的起始点;
- 设置最好用
-classpath
;或者-cp
;或者 Java 9 中的-class-path
选项指定类路径;
Java 9 中,还可以从模块路径加载类;
8. JAR 文件
8.1. 创建 JAR 文件
可以采用 jar 工具进行制作 JAR 文件;
示例 1: 将两个类文件归档到一个名为 classes.jar 的档案中:
jar cvf classes.jar Foo.class Bar.class
示例 2: 使用现有的清单文件 ‘mymanifest’ 并将 foo/ 目录中的所有文件归档到 ‘classes.jar’ 中:
jar cvfm classes.jar mymanifest -C foo/
8.2. 清单文件
-
除了类文件、图像和其他资源外,每个 JAR 文件还包含一个清单文件 (manifest),用来描述归档文件的特殊特性;
-
清单文件被命名为 MAINFEST.MF;它位于 JAR 文件的一个特殊的 META-INF 子目录中;
-
符合标准的最小清单文件极其的简单:
Manifest-Version: 1.0
-
复杂的清单文件可能包含更多的条目;可以有很多的;
-
具体 JAR 文件及清单文件格式:https://docs.oracle.com/javase/10/docs/specs/jar/jar.html
8.3. 可执行 JAR 文件
-
可采用 jar 命令的 e 选项来指定程序的入口点;即通常需要在调用 java 程序启动器时指定的类;
jar cvfe MyProgram.jar com.mycompany.mypkg.MainApplicationClass
-
注意:清单文件最后一行需要以换行符结束;否则,无法被正确读取;常见错误:创建一个只包含 Main-Class 行而无行结束符的文本文件;
-
可以通过
java -jar Program.jar
来进行执行; -
也可以进行打包;例如 Launch4J 或 IzPack 等;
8.4. 多版本 JAR 文件
可能需要对 Java 8 和 Java 9 用户发布不同的应用程序;为了解决:Java 9 引入了多版本 Jar 可以包含不同 Java 版本的类文件;
为了保证向后兼容;额外的类文件放在 MATA-INF/versions 目录中:
Application.class
BuildingBlocks.class
Util.class
META-INF
|- MANIFEST.MF (with line Multi-Release: true)
|- versions
|- 9
|--|
| |-Application.class
| |-BuildingBlocks.class
|- 10
|-BuildingBlocks.class
-
Java 8 是完全不知道 META-INF/versions 目录的;只会加载一流的类;Java 9 读取这个 JAR 文件时,则会使用新版本;
-
要增加不同版本的类文件,可以使用 --release 标志:
jar uf Program.jar --release 9 Application.class
-
要从头构建一个多版本 JAR 文件,可以采用 -C 选项
jar cf Program.jar -C bin/8 . --release 9 -C bin/9 Application.class
-
面向不同版本编译时,要使用 --release 标志和 -d 表示来指定输出目录:
javac -d bin/8 --release 8 ...
-
Java 中, -d 会创建这个目录(如果原先不存在该目录)
-
–release 也是 Java 9 新增的;较早版本,需要使用 -source, -target 和 -boot-classpath 标志
-
JDK 现在为之前的两个 API 版本提供了符号文件;在 Java 9 中,编译时可以将 --release 设置为 9, 8, 7 等;
-
多版本 JAR 并不适用于不同版本的程序或库;对于不同版本,所有类的公共 API 都应该一样;多版本 JAR 的唯一目的是支持你的某个特定的程序或库能够在多个不同的 JDK 版本上运行;如果你增加了功能或者改变了一个 API,那么应该提供一个新版本的 JAR ;
-
Javap 之类的工具并没有改造为可以处理多版本 JAR 文件;如果调用
javap -classpath Program.jar Application.class
,则会得到类的基本版本;如果必须要查看更新的版本,可以调用:javap -classpath Program.jar\!/META-INF/versions/9/Application.class
8.5. 关于命令行选项的说明
-
jdk 的命令行选项一直以来都是用单个短横线加多字幕选项名的形式;如:
java -jar ... javac -Xlint:unchecked -classpath ...
-
但是 jar 命令是例外;遵循经典的 tar 命令选项格式;无短横线;
-
Java 9 之后,java 工具转向一种更加常用的选项格式,多字母选项名前面加两个短横线;另外对于常用的选项可以使用单字母快捷方式;
-
Java 9 中,可以使用 --version 而不是 -version 另外可以使用 -class-path 而不是 -classpath;
-
详细内容可以看 JEP 293;http://openjdk.java.net/jeps/293;
-
所有清理工作,作者也提出来要标准化选项参数;带 – 和多字幕的选项的参数用空格或者一个等号来进行分割;
javac -class-path /home/classdir javac -class-path=/home/classdir
-
单字母选项的参数可用空格分隔,或者直接跟在选项后面;
javac -p moduledir ... javac -pmoduledir ...
-
无参数的单字母选项可以组合:
jar -cvf Program.jar -e packeage.Program */*.class
-
目前确实有点混乱;希望可以解决;
-
可以安全的使用 jar 命令的长选项:
jar --create --verbose --file jarFileName file1 file2 ... # 单字母选项,不组合,也可以使用 jar -c -v -f jarFileName file1 file2 ...
9. 文档注释
9.1. 注释的插入
-
javadoc 使用工具会从下面几项中,抽取信息:
-
模块;
-
包;
-
公共类与接口;
-
公共的和受保护的字段;
-
公共的和受保护的构造器及方法;
-
可以为以上各个特性边写注释;以
/**
开始,*/
结束; -
文档注释包含标记以及之后跟着的自由格式文本;标记以 @ 开始;
-
自由格式文本第一句应该是概要行的句子;可以使用 HTML 修饰符;
如果文档由到其他文件的链接,如图像,就应该将这些文件放到包含源文件的目录下面的一个子目录 doc-files 中;再引用;如:
<img src="F:\workspace\notes\Java\JavaSE\Java 核心技术学习笔记\doc-files\url.png">
9.2. 类注释
- 类注释需要在
import
语句之后,类定义之前;
9.3. 方法注释
- 方法注释在所描述的方法之前;
9.4. 字段注释
- 只需要对公共字段(通常指的是静态常量)建立文档;
9.5. 通用注释
9.6. 包注释
-
包注释,需要在每一个包目录中添加一个单独的文件;可以试:
-
提供一个名为 package-info.java 的文件;这个文件必须要包含一个文档注释;后面是一个 package 语句;不能包含更多的代码或注释
-
提供一个名为 package.html 的 html 文件;会进行抽取 body 之间的所有文本;
-
9.7. 注释抽取
如果你希望 HTML 文件将放在 docDirectory 的目录下;执行以下步奏:
- 切换到包含想到生成文档的源文件的目录;如果有嵌套的包需要生成文档;例如:
com.demo.corejava
,则需要切换到包含子目录 com 的目录; - 如果是一个包;则:
javadoc -d docDirectory nameOfPackage
- 如果为多个包生成:
javadoc -d docDirectory nameOfPackage1 nameOfPackage2 ...
- 如果文件在无名的包中,就应该运行:
javadoc -d docDirectory *.java
- 如果省略
-d docDirectory
选项;HTML 文件会抽取到当前目录下;可能会引起混乱;不建议这么做;
10. 类设计技巧
- 数据私有;
- 数据进行初始化;
- 不在类中使用过多的基本数据类型;
- 不是所有字段都需要单独的字段访问器和字段更改器;
- 分解有过多职责的类;
- 类名和方法名要能够提现它们的职责;
- 优先使用不可变的类;