Java-2-类的介绍
Java 是最早的面向对象语言之一。我们先通过一个例子程序,初步分析了 Java 类 的组成部分:属性+方法,分析它们的常见修饰符,以及构造函数的写法。此外,还会简单提及了 嵌套类 以及 注释的写法。
A. 类的组成
在 java 中,一个类通常由属性和方法构成,我们以一个例子来分析类的组成。
1 | import java.util.Arrays; |
a. 包-java 类存放的位置
在 Java 中,我们会把相关的类放在同一个包中,通常包名的命名为个人、公司域名的倒序。例如书中作者将该书代码放在包 com.horstmann.corejava
中。
JDK 提供一个打包工具,可以将编译完成的 ”.class“ 文件打包成 archive files - JAR 文件。它的操作和 UNIX 中 tar 打包工具差不多。Java 库通常被打包成 JAR 格式的文件。
对于整个工程而言,在工程中使用 JAR 文件的时候,我们需要告诉编译器及虚拟机这些 JAR 文件的位置,这一过程,就通过指定 “class path” 完成。具体的,class path 可以运行时添加 -cp
参数来指定 classpath,也可以在系统环境变量 “CLASSPATH” 中添加。
对于具体的类而言,我们通过 import
指令可以将所依赖的包导入,从而不需要在使用对应包中的类的时候填写所用的类的全名(with 包名)。import
代码的位置在 包声明 代码之后,在 class 内容之前。
此外,我们可以通过通配符(wildcard)import 包中的所有类。e.g. import java.util.*;
需要注意的是 *
只能代表类,而不能代表 packages,即不能使用 import java.*;
.
对于import 类冲突的问题,例如 java.util.*
和 java.sql.*
中都含有 Date
类, 如果我们只使用其中的一个,可以在 import 的时候追加一行说明, e.g. import java.sql.Date;
。而如果两个都需要使用,那么只能在使用的时候注明所使用的类的全称。
更高级的写法,我们可以通过 static import 的方法,导入包中的所有类的 static 方法和变量,从而在实际使用中,不用写静态方法所属的类。i.e. (在使用静态类特别多的包的时候很实用,e.g. java.util.Comparator
, java.util.stream.Collectors
).
1 | import static java.lang.Math.*; |
b. 属性(变量,field)及其初始化
在 java 中,变量分为基本数据类型和引用数据类型, 在基本数据类型的变量中(共8种),直接存储 值;而在引用数据类型中,存储的是指向类的实例对象的地址,也就是存储的为一个 引用。 这一机制在赋值时需要我们格外注意,参考如下例子:
1 | //people 和 friends refer to the same object |
一般的,类中的变量声明为 private
权限,也就是只供类内部使用。
使用 final
修饰符表明变量为常量。对于基本数据类型,声明为 final 表示这个值不允许改变;而引用类型声明为 final 则表示这个引用不能改指向其他对象,但所指向对象中的内容,允许改变。(参考下面例子)
1 | //一般习惯 变量 所有字母大写,变量值不允许改变 |
需要特别注意的是, final 的 variables 必须在构造函数调用完成之前进行初始化。(ref)
使用 static
修饰变量,表示该变量是属于 类的公用元素,调用时需要使用 类名进行调用,而不能使用 类的实例进行调用。
类中的属性的初始化 除了在声明时进行,还可以使用 初始化代码块 进行(参看上方代码)。这个类中的声明赋值以及初始化块初始化,都是在调用 构造函数之前 发生。此外,还有一个 静态初始化代码块,它会在 类第一次被加载时执行一次,且优先于主函数被执行。(reflink)
如果在构造对象时不对类的实例变量进行赋值,系统会自动给这些变量赋给一些默认值,例如数值型都为 0, 布尔型为 false,对象默认为 null。因此,这里需要注意可能引发的 空指针异常。
c. 方法(函数)
Java 中的方法,分为 “accessor and mutator” 两种类型, accessor 方法仅仅读取,而不改变对象,而 mutator 方法会改变所引用的对象。
一个方法,主要包括方法的 权限,方法的返回值,方法的名称,方法的传入参数。
方法的权限: 方法通常声明为 public
权限,从而类的使用者可以调用(当然,一些用于自己类内部的 helper 方法,也应该声明为 private
类型)。
默认的,如果不声明为 static
, 方法都为 ‘instance method’,也就是调用时必须通过类的实例进行。所调用这个方法的类的实例,也叫这个函数调用的 receiver, e.g. fred.raiseSalary(5)
, 这里的 fred 为该方法的 receiver。而 Static Method静态方法, 则直接通过类名进行调用,而不是通过类的实例调用。
参数传递:本质上讲,Java 是一种值传递的语言,只不过这个值可以是基本数据类型的直接数值,还是对象的地址值。例如上面的 Manager
类的函数 giveRandomRaise()
它的接受参数是一个对象的引用,从而该函数可以通过传入的这个引用修改对象的参数。
需要注意的是,与对象的传递不同,基本数据类型的传递不是传递指向所存储数据的引用,而是直接传递存储的数值,即把数值复制给函数的参数变量,因此,无法通过传递基本数据类类型的变量,达到修改原变量的目的。
d. 创建实例
创建一个新的对象的实例,我们可以通过 new
调用类的构造方法完成;也可以使用 工厂方法,(一种设计模式)配合private 的构造方法 以及静态函数完成。
特殊的方法-构造函数
构造函数 一般为 public 类型,若为 private 类型,通常使用 工厂方法 进行类的构造。
构造方法的方法名与类的名字相同,没有返回值。当使用 new
生成对象的时候会进行调用,new
语句最终返回一个指向所生成的对象的引用。
每个类都至少一个构造函数(如果不写,默认为一个空的无参数函数,但如果你写了一个构造函数,就不会有空的无参默认构造函数)。
构造函数可进行多次重载,可以有多个具有不同参数表的构造函数,这一行为成为 “overloading”. 此外,为了方便,构造函数之间也可以互相调用,通过 this()
。
工厂方法
当构造函数为 private 权限时,(如 LocalDate
类)我们通常使用工厂方法,实现类的实例的构造。
使用工厂方法构造类的实例对象的方法,通常提供一些静态方法来获取类的不同的实例对象, 例如 NumberFormat
类,通过不同的工厂方法 NumberFormat.getCurrencyInstance()
, NumberFormat.getPercentInstant()
,可以返回一个类的不同样式的实例。
使用工厂类产生实例的好处:1- 相同参数的构造函数只能有一个,但是工厂函数可以构造不同样式的实例with 相同的参数表;2- new 方法只能生成本类的实例,而工厂方法更加自由,可以生成子类的实例;3- 工厂方法还可以返回共享的对象,而每 new 一次,必定生成新的对象。
this, super 关键字
This 关键字,指向调用这个方法的对象,例如上面的 raiseSalary()
方法中,this
指代调用该方法的对象。
super 这是指向一个该方法所在类的超类的对象。(距离子类最近的一个超类)
e. 重点总结
关键字
权限修饰符:public, protected, private
public
: 完全公开,其他所有类都可以访问。
protected
: 包访问权限,是默认权限, 仅在同一个包中的类可以访问。
private
: 类访问权限,仅仅在同一个类中可以访问。
static
关键字:可以修饰变量,方法,初始化代码块。表明所修饰的对象是 属于 类的公共内容,而不是 属于 类的实例。
final
关键字,用于修饰变量,表示所修饰的变量为 常量, (注意所修饰的对象 是基础数据类型 还是 引用数据类型的区别)
初始化顺序
对于实例变量(非静态变量)。
Step1: 类中的声明赋值以及初始化块初始化.
Step2: 构造函数 初始化。
Step3: 如果有变量在上述两步中都没有初始化, 进行系统默认初始化。(Note: 对于 final 类型,必须在上述两步进行初始化)
对于静态变量,它的初始化时随着类的构建而进行的,可以通过声明复制,静态初始化代码块进行。
B. 嵌套类
所谓的嵌套类也就是在 类的内部 声明的 类,根据这个类是否为 静态(static),我们分为 静态嵌套类 和 非静态嵌套类, 其中非静态嵌套类 又 称为 内部类(inner class)。(ref)
内部类根据声明的位置不同,又可以分为3种:
- 在外部类中直接定义的类;
- 在外部类中的一个方法中定义的类;
- 匿名内部类。
对于上面所有的三种情况的内部类,它们都归属于它的外部类的实例。可以获得他的外部类的 实例 的变量,方法。通常,可以直接在内部类中使用外部类的 非static 方法,如果可能有歧义时,还可以通过 外部类名.this
的形式,获得外部类的实例。第2种情况和 第3种 情况在下一篇介绍接口及 lambda表达式的笔记中会进一步介绍。
需要注意的是,inner class 在生成的时候,会 “记住” 与之生成相关的外部类的实例,也就是说对于内部类来说,它的 new 内部类名()
实际是 this.new 内部类名()
的简称。
C. Java 注释与文档
Java 提供了自动文档生成工具 javadoc,可以直接从 java 源文件生成HTML文档。它会将你的源文档中的 /** 部分的注释内容导出为文档。这样的好处是,将原文件与文档写在一起,保证了二者的同步。(如果二者分开存储,常常更新完程序而忘记更新文档)
一些格式
通常在代码的头部,我们使用如下格式撰写注释:
1 | /** |
通常对于静态常量,我们会添加注释说明他的含义。
在一些依赖于某些版本的代码前,我们使用 @since version 版本号
进行说明。
使用 @deprecated
tag 表明该部分代码不建议使用,并提供现在推荐使用的方案。(compiler 会检测这一标签,如果程序使用了 deprecated 的代码,编译时会做出提醒。)
使用 @see
和 @link
标签添加文档的参考超链接。
Package, Module 的comments:需要分别使用单独的文件来说明,package-info.java
和 module-info.java
。