类与对象 – Java学习(二)

弄清楚类与对象的本质与基本特征,是进一步学习面向对象编程语言的基本要求。面向对象程序设计与面向过程程序设计在思维上存在着很大差别,改变一种思维方式并不是一件容易的事情。

一、面向对象程序设计

程序由对象组成,对象包含对用户公开的特定功能部分,和隐藏在其内部的实现部分。从设计层面讲,我们只关心对象能否满足要求,而无需过多关注其功能的具体实现。面对规模较小的问题时,面向过程的开发方式是比较理想的,但面对解决规模较大的问题时,面向对象的程序设计往往更加合适。

对象是对客观事物的抽象,类是对对象的抽象,是构建对象的模板。由类构造(construct)对象的过程称为创建类的实例(instance)或类的实例化。

封装是将数据和行为组合在一个包中,并对使用者隐藏数据的实现方式。对象中的数据称为实例域(instance field)或属性、成员变量,操纵数据的过程称为方法(method)。对象一般有一组特定的实例域值,这些值的集合就是对象当前的状态。封装的关键在于不让类中的方法直接的访问其他类的实例域,程序仅通过对象的方法与对象数据进行交互。封装能够让我们通过简单的使用一个类的接口即可完成相当复杂的任务,而无需了解具体的细节实现。

对象的三个主要特征

  1. 对象的行为(behavior):可以对对象施加哪些操作,通过方法(method)实现。
  2. 对象的状态(state):存储对象的特征信息,通过实例域(instance field)实现。
  3. 对象的标识(identity):辨别具有不同行为与状态的不同对象。

设计类

传统的面向过程的程序设计,必须从顶部的 main 入口函数开始编写程序。面向对象程序设计没有所谓的顶部,我们要从设计类开始,然后再往每个类中添加方法。那么我们该具体定义什么样的类?定义多少个?每个类又该具备哪些方法呢?这里有一个简单的规则可以参考 —— “找名词与动词”原则。

我们需要在分析问题的过程中寻找名词和动词,这些名词很有可能成为类,而方法对应着动词。当然,所谓原则,只是一种经验,在创建类的时候,哪些名词和动词是重要的,完全取决于个人的开发经验(抽象能力)。

类之间的关系

最常见的关系有:依赖(use-a)、聚合(has-a)、继承(is-a)。可以使用UML(unified modeling language)绘制类图,用来可视化的描述类之间的关系。

二、预定义类与自定义类

在 Java 中没有类就无法做任何事情,Java 标准类库中提供了很多类,这里称其为预定义类,如 Math 类。要注意的是:并非所有类都具有面向对象的特征(如 Math 类),它只封装了功能,不需要也不必要隐藏数据,由于没有数据,因此也不必担心生成以及初始化实例域的相关操作。

要使用对象,就必须先构造对象,并指定其初始状态。我们可以使用构造器(constructor)构造新实例,本质上,构造器是一种特殊的方法,用以构造并初始化对象。构造器的名字与类名相同。如需构造一个类的对象,需要在构造器前面加上 new 操作符,如new Date()。通常,希望对象可以多次使用,因此,需要将对象存放在一个变量中,不过要注意,一个对象变量并没有实际包含一个对象,而仅仅是引用一个对象。

访问器与修改器 我们把只访问对象而不修改对象状态的方法称为 访问器方法(accessor method)。如果方法会对对象本身进行修改,我们称这样的方法称为 更改器方法(mutator method)。

用户自定义类

要想创建一个完成的程序,应该将若干类组合在一起,其中只有一个类有 main 方法。其它类( workhorse class)没有 main 方法,却有自己的实例域和实例方法,这些类往往需要我们自己设计和定义。

一个源文件中,最多只能有一个公有类(访问级别为public),但可以有任意数目的非公有类。尽管一个源文件可以包含多个类,但还是建议将每一个类存在一个单独的源文件中。 不提倡用public标记实例域(即对象的属性),public 数据域允许程序中的任何方法对其进行读取和修改。当实例域设置为 private 后,如果需要对其进行读取和修改,可以通过定义公有的域访问器或修改器来实现。这里要注意:不要编写返回引用可变对象的访问器方法,如:

class TestClass{
    private Date theDate;
    public getDate(){
        return theDate; // Bad
    }
}

上面的访问器返回的是对实例属性 theDate 的引用,这导致在后续可以随意修改当前实例的 theDate 属性,比如执行x.getDate().setTime(y),破坏了封装性!如果要返回一个可变对象的引用,应该首先对他进行克隆,如下:

class TestClass{
    private Date theDate;
    public getDate(){
        return (Date) theDate.clone(); // Ok
    }
}

构造器

构造器与类同名,当实例化某个类时,构造器会被执行,以便将实例域初始化为所需的状态。构造器总是伴随着 new 操作符的调用被执行,不能对一个已经存在的对象调用构造器来重置实例域。

  1. 构造器与类同名
  2. 每个类可以有多个构造器
  3. 构造器可以有 0 个或多个参数
  4. 构造器没有返回值
  5. 构造器总是伴随着 new 操作一起调用

基于类的访问权限

方法可以访问所属类的所有对象的私有数据。[*]

在实现一个类时,应将所有的数据域都设置为私有的。多数时候我们把方法设计为公有的,但有时我们希望将一个方法划分成若干个独立的辅助方法,通常这些辅助方法不应该设计成为公有接口的一部分,最好将其标记为 private 。只要方法是私有的,类的设计者就可以确信:他不会被外部的其他类操作调用,可以将其删去,如果是公有的,就不能将其删除,因为其他的代码可能依赖它。

final 实例域

在构建对象时必须对声明的 final 实例域进行初始化,就是说必须确保在构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对其进行修改。final 修饰符大都用于基本类型,或不可变类的域。

静态域和静态方法

静态域和静态方法,是属于类且不属于对象的变量和函数。

通过 static 修饰符,可以标注一个域为静态的,静态域属于类,而不属于任何独立的对象,但是每个对象都会有一份这个静态域的拷贝。静态方法是一种不能对对象施加操作的方法,它可以访问自身类的静态域,类的对象也可以调用类的静态方法,但更建议直接使用类名调用静态方法。

使用静态方法的场景 : 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供;一个方法只需要访问类的静态域。

静态方法还有另外一种常见用途,作为工厂方法用以构造对象。之所已使用工厂方法,两个原因:一是无法命名构造器,因为构造器必须与类名相同;二是当时用构造器时无法改变构造的对象类型。

程序入口 main 方法就是一个典型的静态方法,其不对任何对象进行操作。在启动程序时还没有任何一个对象,静态的 main 方法将执行并创建程序所需要的对象。每个类都可以有一个 main 方法,作为一个小技巧,我们可以通过这个方法对类进行单元测试。

三、方法参数

Java 中的方法参数总是按值调用,也就是说,方法得到的是所有参数的值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。然而,方法参数有两种类型:基本数据类型和对象引用。

四、对象构造

如果在构造器中没有显式的为域赋值,那么域会被自动的赋予默认值:数值为 0、布尔之为 false、对象引用为 null。在类没有提供任何构造器的时候,系统会提供一个默认的构造器。

有些类有多个构造器,这种特征叫做重载(overloading)。如果多个方法有相同的名字、不同的参数,便产生了重载。 Java 中允许重载任何方法,而不仅是构造器方法。要完整的描述一个方法,需要指出方法名以及其参数类型,这个描述被称作方法的签名。

通过重载类的构造器方法,可以采用多种形式设置类的实例的初始状态。当存在多个构造器的时候,也可以在构造器内部通过 this 调用另一个构造器,要注意的是这个调用必须在当前构造器的第一行:

class Test{
    Test(int number) {
        this(number, (String)number);   // 位于当前构造器的第一行
    }

    Test(int number, String str) {
        _number = number;
        _string = str;
    }
}

初始化块

在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如:

class Test{
    private int number;
    private String name;

    /**
     * 初始化块
     */
    {
        number = 5;
    }

    Test(){
        name = 'Kelsen'
    }

    public void pring(){
        System.out.println(name + "-" + number);
    }
}

执行顺序为,首先运行初始化块,然后再运行构造器的主体部分。这种机制不是必须的,也不常见。通常会直接将初始化代码放在构造器中。

Java 中不支持析构器,它有自动的垃圾回收器,不需要人工进行内存回收。但,如果某个资源需要在使用完毕后立刻被关闭,那么就需要人工来管理。对象用完时可以应用一个 close 方法来完成相应的清理操作。

五、包

借助于包,可以方便的组织我们的类代码,并将自己的代码与别人提供的代码库区分管理。标准的 Java 类库分布在多个包中,包括 java.lang、java.util 和 java.net 等。标准的 Java 包具有一个层次结构。如同硬盘文件目录嵌套一样,也可以使用嵌套层次组织包。所有的标准 Java 包都处于 javajavax 包层次中。从编译器角度看,嵌套的包之间没有任何关系,每一个都拥有独立的类集合。

一个类可以使用所属包中的所有类,以及其他包中的公有类(pbulic class)。 import 语句是一种引用包含在包中的类的简明描述。packageimport 语句类似 C++ 中的 namespaceusing 指令。

import 语句还可以用来导入类的静态方法和静态域。

如果要将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。如:


package com.kelsem.learnjava;

public class Test{
    // ...
}

如果没有在源文件中放置 package 语句,这个源文件中的类就被放置在一个默认包中。

包作用域

标记为 private 的部分只能被定义他们的类访问,标记为 public 的部分可以被任何类访问;如果没有指定访问级别,这个部分(类/方法/变量)可以被同一个包中的所有方法访问。

类路径

类存储在文件系统的目录中,路径与包名匹配。另外,类文件也可以存储在 JAR 文件中。为了使类能够被多个程序共享,通常把类放到一个目录中,将 JAR 文件放到一个目录中,然后设置类路径。类路径是所有包含类文件的路径的集合,设置类路径时,首选使用 -calsspath 选项设置,不建议通过设置 CLASSPATH 这个环境变量完成该操作。

六、文档注释

JDK 包含一个非常有用的工具,叫做 javadoc 。它通过分析我们的代码文件注释,自动生成 HTML 文档。每次修源码后,通过运行 javadoc 就可以轻松更新代码文档。Javadoc 功能包括:Javadoc搜索,支持生成HTML5输出,支持模块系统中的文档注释,以及简化的Doclet API。详细使用说明可参考 https://docs.oracle.com/en/java/javase/11/javadoc/javadoc.html

七、类的设计

一定要保证数据私有 务必确保封装性不被破坏。

一定要对数据初始化 Java 不会对局部变量进行初始化,但会对对象的实例域进行初始化。最好不要依赖于系统默认值,而是显式的对实例域进行初始化。

不要在类中使用过多的基本类型 通过定义一个新的类,来代替多个相关的基本类型的使用。

不是所有的域都需要独立的域访问器和域更改器

将职责过多的类进行分解 如果明显的可以将一个复杂的类分解为两个更简单的类,就应该将其分解。

类名和方法名要能够体现他们的职责 对于方法名,建议:访问器以小写 get 开头,修改器以小写 set 开头;对于类名,建议类名是采用一个名词(Order)、前面有形容词修饰的名词(RushOrder)或动名词(ing后缀)修饰名词(BillingAddress)。

优先使用不可变的类 要尽可能让类是不可变的,当然,也并不是所有类都应当是不可变的。