《JAVA核心技术 卷I》 第四章 - 对象与类

第四章 - 对象与类

目录

1. 面向对象程序设计概述

1.1 类的相关概念

  • 由类构造对象的过程称为创建类的实例。对象中的数据称为实例字段,操作数据的过程称为方法

  • 如果想要熟练使用OOP,一定要弄清对象的三个主要特性:

    • 对象的行为——可以对对象完成哪些操作,或者可以对对象应用哪些方法?
    • 对象的状态——当调用那些方法的时候,对象会如何响应?
    • 对象的标识——如何区分具有相同行为与状态的不同对象

    若要设计一个类,可以先从识别类开始,然后再为各个类添加方法。识别类的一个简单经验是在分析问题的过程中寻找名词,而方法对应着动词。

1.2 类之间的关系

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

    • 依赖(uses - a):依赖是一种最明显的,最常见的关系。如果一个类的方法使用或操纵另一个类的对象,我们这就说一个类依赖于另一个类。应该尽可能的将互相依赖的类减少至最少,可就是尽量减少类之间的耦合
    • 聚合(has - a):聚合的对象之间具有包容关系,而包容关系意味着类A的对象包含类B的对象。聚合也可以理解为关联关系
    • 继承(is - a):表示一个更特殊的类与一个更一般的类之间的关系。
  • 表达类关系的UML符号

    《JAVA核心技术 卷I》 第四章 - 对象与类

2.类

2.1 字段

  • 实现封装的关键在于,绝对不能让类中的方法直接访问其它类的实例字段

    可以使用public标记实例字段(如成员变量),但这是一种很不好的做法。public数据字段允许程序中的任何方法对其进行读取和修改,这就完全破坏了封装。

  • 如果对一个对象变量初始化值为null,那么必须要特别小心。因为如果在未赋予新值的情况下,调用方法处理该对象变量,那么就会触发空指针错误。为了应对这种情况,有两种方法可供选择:

    • 宽容型:把null参数从转换为一个适当的非null值(在Java9中,Objects类提供了一个方便的方法)

      • public Employee(String n,double s,int year,int month,int day)
        {
            name = Objects.requireNonNullElse(n,"unknown");
            //如果传入的数值n是null的话,就会在赋值时自动转换为"unknown"字符串
        }
        
    • 严格型:直接拒绝null参数

      • public Employee(String n,double s,int year,int month,int day)
        {
            name = Objects.requireNonNullElse(n,"The name cannot be null");
            name = n;	//选择产生异常然后抛出/解决
        }
        
    • 如果要接受一个对象引用作为构造参数,就要问问自己:是不是真的希望接受可有可无的值。如果不是,那么"严格型"方法可能更加合适

  • 如果将一个字段定义为static,则每个类只有一个这样的字段。而对于非静态的实例字段,每个对象都有属于自己的一个副本。

2.2 构造器

  • 关于构造器,不能对一个已经存在的对象调用构造器来达到重新设置实例字段的目的

    james.Employee("James",2500,1950,1,1);	//不允许
    
  • 不要在构造器内定义与实例字段同名的局部变量。这些局部变量只能在构造器内部访问,它们会遮蔽同名的实例字段

  • 如果构造器的第一个语句形如this(...),这个构造器将调用同一个类的另一个构造器

  • 可以在类定义中直接为任何字段赋值。在执行构造器之前先完成这个赋值操作,如果一个类的所有构造器都希望把某个特定的实例字段设置为同一个值,这个语法就很有用。为避免循环定义,建议总是将初始化块放在字段定义之后

2.3 方法

  • 方法有两个参数,第一个参数称为隐式参数,是出现在方法名前的对象。第二个参数是位于方法名方法名后面括号中的数值,这是一个显式参数(有人把隐式参数也称为方法调用的目标或接收者)。显式参数显式的列在方法声明中,隐式参数则没有出现在方法声明中。关键字this可以用于指示隐式参数

  • 每一个类都可以有一个main方法,这是常用于对类进行单元测试的一个技巧。

  • 只访问对象而不修改对象的方法称为访问器方法。反之,访问且修改对象的方法称为更改器方法

    注意不要编写 返回"可变对象引用"的访问器方法。

     class Employee
     {
         private Date hireDay;
         
         public Date getHireDay()
         {
             return hireDay; //Date对象是可变的,这一点就破坏了封装性
         }
     }
    //如果之后有成员变量想要通过getHireDay来获得hireDay的数值,他将直接连接到hireDay的地址上,直接对hireDay进行操作。这一点一般的数值是不用考虑的,但对于对象引用就需要特别考虑
    //如果需要返回一个可变对象的引用,首先应该对它进行"克隆(clone)",对象克隆是指存放在另一个新位置上的对象副本。如果需要返回一个可变数据字段的副本,就应该使用clone
    
  • 要完整的描述一个方法,需要指定方法名以及参数类型,这叫做方法的签名;返回值不是方法签名的一部分,也就是说不能有两个名字相同,参数类型相同,返回类型却不同的方法

  • 方法中的局部变量必须明确的初始化,但是在类中,如果没有初始化类中的字段,将会自动初始化为默认值

  • 按值使用表示方法接收的是调用者提供的值,按引用调用表示方法接收的调用者提供的变量地址。方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。

    Java对对象采用的不是按引用调用,实际上,对象引用是按值传递的

    //先来看下面这段程序
    public static void swap(Employee x, Employee y)
    {
        Employee temp = x;
        x = y;
        y = temp;
    }
    
    Employee a = new Employee("Alice");
    Employee b = new Employee("Bob");
    swap(a,b);
    
    //如果Java对对象采用的是引用调用,那么这个方法就应该能实现交换
    //然而事实是两个对象毫无变化,实际上swap方法的参数x和y被初始化为两个对象引用的副本,这个方法交换的是这两个副本。在这点上,Java和C有很大的不同
    //!注意:这和之前说的不要编写 返回"可变对象引用"的访问器方法 并不冲突,在return的时候,由于离开方法副本会被销毁,所以最终返回的还是引用对象的地址
    
2.3.1 静态方法
  • 静态方法是不在对象上执行的方法,它不能在对象上执行操作,但是静态方法可以访问静态字段。

    可以使用对象调用静态方法,但这种写法很容易造成混乱,因为方法计算的结果与对象毫无关系。建议使用类名而不是对象来调用静态方法。

  • 静态工厂方法

    • 可以使用静态工厂方法来构造对象。不直接使用构造器主要有以下两个原因:
      • 无法命名构造器,构造器的名字必须与类名相同。但是有时会希望有两个不同的名字,比如想要分别得到货币实例和百分比实例
      • 使用构造器时,无法改变所构造对象的类型。而工厂方法实际上将返回特定类的对象。

2.4 类的设计技巧

  1. 一定要保证数据私有

  2. 一定要对数据进行初始化

  3. 不要在单个类中使用过多的基本类型,要用其它的类替换使用多个相关的基本类型(模块化)

  4. 不是所有的字段都需要单独的的字段访问器和字段更改器

  5. 分解有过多职责的类

  6. 类名和方法名要能体现他们的职责

  7. 优先使用不可变的类0

3. 包名

  • 为了保证包名的绝对唯一性,要用一个因特网域名,以逆序的形式作为包名,然后对于不同的工程使用不同的子包。

    • 例如,考虑域名horstmann.com。如果逆序来写,就得到了包名com.horstmann,然后可以追加一个工程名,如com.horstmann.corejava。如果再把Employee类放在这个包里,那么这个类的"完全限定名"就是com.horstmann.corejava.Employee
  • 有一种import语句允许导入静态方法和静态字段,而不只是类

     //如果在源文件顶部添加一条指令
     import static java.lang.System.*;
     //就可以使用System类的静态方法和静态字段,而不必加类名前缀
     out.println("hi");
     
     //另外还可以导入特定的方法或字段,如
     import static java.lang.System.out;
     println("hi");
     
     //事实上这样的方法多用于Math类让表达式更加简洁,而非如上这样使用
    
  • 编译器在编译源文件的时候不检查目录结构。即使源文件不在对应目录下,也可以进行编译。如果它不依赖于其它包,那么就可以通过编译并且不会出现编译错误。但这样的程序最终依旧无法运行,因为如果包和目录不匹配,虚拟机就找不到类

4. JAR文件

  • 类文件可以储存在JAR文件中,在一个JAR文件中,可以包含多个压缩形式的类文件和子目录。一个JAR文件既可以包含类文件,有也可以包含诸如图像和声音等其它类型的文件。JAR文件是压缩的,它使用了ZIP压缩格式

  • 如果要让类被多个程序共享,需要做到以下几点:

    1. 把类文件放到一个目录中。这个目录是包树状结构的基目录
    2. 将JAR文件放在另一个目录中
    3. 设置类路径(classpath)。类路径应当包含所有 包含类文件的路径,类路径中的各项由分号(;)分隔
      • 类路径包括:
        • 基目录(如 /home/user/classdir)
        • 当前目录(.)
        • JAR文件(如 /home/user/archives/archive.jar)
      • 一个类路径例子: /home/user/classdir;.;/home/user/archives/archive.jar
    • 类路径所列出的目录和归档文件(JAR)是搜寻类的起始点。
      • 若虚拟机要寻找某一类文件,它首先要查看Java API类。若未找到,则查看类路径。
      • 若编译器要查找某一类文件,与虚拟机不同。若引用了一个类,而没有指定这个类的包,那么编译器将首先查找这个类的包。它将查看所有import指令,确定import的包中是否包含指定的类。然后他会在类路径所有位置搜索以上各个类。如果找到了一个以上的类,就会产生编译错误
    • 如果要设置类路径,可以使用-classpath,也可以通过设置CLASSPATH环境变量来指定
  • 除了类文件,图像和其它资源外,每个JAR文件还包含一个清单文件,用于描述归档文件的特殊特性。清单文件被命名为MANIFEST.MF,它位于JAR文件的一个特殊的META-INF子目录中。

     //以下是一个清单文件的示例
     //这一行是必要的
     Mainfest-Version: 1.0
     lines describing this archive
     
     //下面不是必要的
     Name: Woozle.class
     lines describing this class
     Name: com/mycompany/mypkg/
     lines describing this package
     
     //总的来说清单文件类似于说明书,简要的解释了某个class或包的内容
    
  • 更多JAR文件相关的内容此处不做笔记,请参考书P143~148

5. 文档注释

5.1 概述
  • Javadoc是一个可以由源文件生成HTML文档的,非常方便的工具。只要在源代码中添加以特殊界定符/**开始的注释,就可以生成一个文档。这样的文档可以将代码与注释放在一个地方,这样在修改源代码的同时,重新运行javadoc就可以保持文件注释和代码的一致性

  • Javadoc工具从下面几项中抽取信息:

    • 模块
    • 公共类和接口
    • 公共的和受保护的字段
    • 公共的和受保护的构造器及方法

    应该为以上的各个特性编写注释,注释放置在所描述特性的前面,注释以/**开始,以 */ 结束。每个文档注释包含 标记 和*格式文本。标记以@开始(如@since)。*格式文本第一句应该是一个概要性的句子。javadoc工具会自动的将这些句子抽取出来生成概要页。在*格式文本中可以使用HTML修饰符

5.2 类注释
  • 类注释必须放在import语句之后,类定义之前
/**
	A <code>Card</code> object represents a playing card such
	as "Queen as Hearts". A card has a suit.............
*/
public class Card
{
    
}
5.3 方法注释
  • 每一个方法注释必须放在所描述的方法之前。除了通用标记以外,还可以使用下买你的标记

    • @param variable description

      这个标记将给当前方法的参数(parameters)部分添加一个条目。这个描述可以占据多行,并且可以使用HTML标记。一个方法的所有@param标记必须放在一起

    • @return description

      这个标记将给当前方法添加返回(return)部分,这个描述可以跨多行,并且可以使用HTML标记

    • @throws class description

      这个标记将添加一个注释,表示这个方法可能抛出异常。

/**
	Raises the salary of an employee
	@param byPercent the percentage by which to raise the salary
	@return the amount of the raise
*/
public double raiseSalary(double byPercent)
5.4 字段注释
  • 一般只需要对公共字段(如静态变量)建立文档
/**
	The "Hearts" card suit
*/
public static final int HEARTS = 1;
5.5 通用注释
  • @since text

    这个标记会建立一个since(始于)条目。text(文本)可以是引入这个特性的代码的版本的描述

  • 以下标记可以使用在类文档注释中:

    • @author name

      这个标记将会产生一个author(作者)条目。可以使用多个@author条目。

    • @version text

      这个标记将会产生一个version(版本)条目。这里的文本可以是对当前版本的任何描述

  • @see reference

    这个标记将在"see also"(参见)部分增加一个超链接。它既可以用于类中,也可以用于方法中。@see有两种写法:

    //写法一:这将会建立一个到指定类的指定方法的超链接
    //公式:package.class#feature label
    @see com.horstmann.corejava.Employee#raiseSalary(double)
        
    //写法二:这样会建立一个超链接,可以链接到任何URL
    //公式:<a href="..."label<>>    
    @see <a href = "www.horstmann.com/corejava.html">The Core Java home page</a>
        
    //在上述两种写法中都可以指定一个label作为链接锚。如果省略了label,用户看到的锚就是目标代码或URL。如果在@see标记后加双引号,文本就会显示在"see also"部分    
    
  • @link reference

    这个标记的功能和@see的类似,但是可以在文档注释中的任何位置使用它放置指向其它类或方法的超链接

    //公式:{@link package.class#feature label}
    
5.6 包注释
  • 要产生包注释,就需要在每一个包目录中添加一个单独的文件,可以有以下两种选择
    • 提供一个名为package-info.java的Java文件。这个文件必须包含一个初始的以/** 和 */界定的Javadoc注释,后面是一个package语句。它不能包含更多的代码或注释
    • 提供一个名为package.html的HTML文件。会抽取标记<body>...</body>之间的所有文本
上一篇:C# .NET 索引器的基本使用


下一篇:复习 C++ 类(二)拷贝构造,赋值运算符,析构(1)