Java对象和类【Java技术核心卷1】

1. 面向对象概述

1.1 面向对象程序设计概述

面向对象程序设计(简称 OOP)是由对象组成的, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分。传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程, 就要开始考虑存储数据的方式。这就是 Pascal 语言的设计者 Niklaus Wirth 将其著作命名为《算法 + 数据结构 = 程序》(Algorithms + Data Structures = Programs, Prentice Hall, 1975 )的原因。需要注意的是,在 Wirth 命名的书名中, 算法是第一位的,数据结构是第二位的,这就明确地表述了程序员的 工作方式。先要确定如何操作数据, 然后再决定如何组织数据, 以便于数据操作。 而 OOP 却调换了这个次序, 将数据放在第一位,然后再考虑操作数据的算法。
对于一些规模较小的问题, 将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。耍想实现一个简单的 Web 浏览器可能需要大约 2000 个过程,这些过程可能需要对一组全局数据进行操作。采用面向对象的设计风格, 可能只需要大约 100 个类,每个类平均包含 20 个方法(如图所示) 后者更易于程序员掌握, 也容易找到 bug假设给定对象的数据出错了,在访问过这个数据项的 20 个方法中查找错误要比在 2000 个过程中查找容易得多
在这里插入图片描述

1.2 类

( class) 是构造对象的模板或蓝图。我们可以将类想象成制作小甜饼的切割机,将对象想象为小甜饼。由类构造(construct) 对象的过程称为创建类的实例 (instance ).

1.3 封 装

封装( encapsulation , 有时称为数据隐藏) 是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域( instance field ), 操纵数据的过程称为方法( method 。) 对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态( state )。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变

OOP 的另一个原则会让用户自定义 Java 类变得轻而易举,这就是:可以通过扩展一个 类来建立另外一个新的类。事实上, 在 Java 中,所有的类都源自于一个“ 神通广大的超类”, 它就是 Object。在扩展一个已有的类时, 这个扩展后的新类具有所扩展的类的全部属性和方法。在新类中,只需提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来建立另外一个 类的过程称为继承

1.4 对 象

要想使用 OOP,
—定要清楚对象的三个主要特性:

  • 对象的行为(behaviour)—可以对对象施加哪些操作,或可以对对象施加哪些方法?
  • 对象的状态(state )—当施加那些方法时,对象如何响应
  • 对象标识(identity )—如何辨别具有相同行为与状态的不同对象

同一个类的所有对象实例, 由于支持相同的行为而具有家族式的相似性。对象的行为是
用可调用的方法定义的。
此外,每个对象都保存着描述当前特征的信息。这就是对象的状态。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。对象状态的改变必须通过调用方法实现(如果不经过方法调用就可以改变对象状态,只能说明封装性遭到了破坏。)
但是,对象的状态并不能完全描述一个对象。每个对象都有一个唯一的身份( identity。) 例如,在一个订单处理系统中, 任何两个订单都存在着不同之处即使所订购的货物完全相同也是如此。需要注意作为一个类的实例, 每个对象的标识永远是不同的,状态常常也存在着差异。
对象的这些关键特性在彼此之间相互影响着。例如, 对象的状态影响它的行为(如果一个订单“ 已送货” 或“ 已付款”, 就应该拒绝调用具有增删订单中条目的方法。反过来, 如果订单是“ 空的”,即还没有加人预订的物品,这个订单就不应该进入“ 已送货” 状态。

1.5 识 别 类

传统的过程化程序设计, 必须从顶部的 main 函数开始编写程序。在面向对象程序设计时没有所谓的“ 顶部”。对于学习OOP 的初学者来说常常会感觉无从下手。答案是:首先从设计类开始,然后再往每个类中添加方法。
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。
例如, 在订单处理系统中,有这样一些名词:

  • 商品(Item)
  • 订单(Order)
  • 送货地址(Deliveryaddress)
  • 付 款 ( Payment )
  • 账户(Account)

这些名词很可能成为类 Item、 Order 等。
接下来, 查看动词:商品被添加到订单中, 订单被发送或取消, 订单货款被支付。对于每一个动词> > 如:“ 添加”、“ 发送”、“ 取消” 以及“ 支付”, 都要标识出主要负责完成相应动作的对象。例如,当一个> 新的商品添加到订单中时, 那个订单对象就是被指定的对象, 因为它 知道如何存储商品以及如何对> 商品进行排序。也就是说,add 应该是 Order 类的一个方法, 而 Item 对象是一个参数。
当然, 所谓“找名词与动词” 原则只是一种经验,在创建类的时候, 哪些名词和动词是 重要的完全取决于个人的开发经验

1.6 类之间的关系

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

  • 依赖(“ uses-a”)
  • 聚合(“ has-a”)
  • 继承(“ is-a”)

依赖( dependence ), 即“ uses-a” 关系, 是一种最明显的、 最常见的关系。例如,Order类使用 Account 类是因为 Order 对象需要访问 Account 对象查看信用状态。但是 Item 类不依赖于 Account 类, 这是因为 Item 对象与客户账户无关。因此, 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。
应该尽可能地将相互依赖的类减至最少。如果类 A 不知道 B 的存在, 它就不会关心 B的任何改变(这意味着 B 的改变不会导致 A 产生任何 bug )。用软件工程的术语来说,就是让类之间的耦合度最小。
聚合(aggregation ), 即“ has-a ” 关系, 是一种具体且易于理解的关系。例如, 一个Order 对象包含一些 Item 对象。聚合关系意味着类 A 的对象包含类 B 的对象。

表 达 类 关 系 的 UML 符 号
在这里插入图片描述
继承( inheritance ), 即“ is-a” 关系, 是一种用于表示特殊与一般关系的。例如,RushOrdei类由 Order 类继承而来。在具有特殊性的 RushOrder 类中包含了一些用于优先处理的特殊方法, 以及一个计算运费的不同方法;而其他的方法, 如添加商品、 生成账单等都是从Order 类继承来的。一般而言, 如果类 A 扩展类 B, 类 A 不但包含从类 B 继承的方法,还会拥有一些额外的功能

2. 使用预定义类

在 Java 中, 没有类就无法做任何事情, 我们前面曾经接触过几个类。然而,并不是所有的类都具有面向对象特征。例如,Math 类。在程序中,可以使用 Math 类的方法, 如 Math,random, 并只需要知道方法名和参数(如果有的话,) 而不必了解它的具体实现过程。这正是封装的关键所在,当然所有类都是这样。但遗憾的是,Math 类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域

2.1 对象与对象变量

要想使用对象,就必须首先构造对象, 并指定其初始状态。然后,Xt对象应用方法。在 Java 程序设计语言中, 使用构造器(constructor ) 构造新实例。构造器是一种特殊的方法, 用来构造并初始化对象。下面看一个例子。 在标准 Java 库中包含一个 Date 类。它的对象将描述一个时间点, 例如:“ December 31, 1999, 23:59:59 GMT”。
构造器的名字应该与类名相同。因此 Date 类的构造器名为 Date。要想构造一个 Date 对
象, 需要在构造器前面加上 new 操作符,如下所示:

new Date()

这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。
如果需要的话, 也可以将这个对象传递给一个方法:

System.out.printTn(new Date());

或者, 也可以将一个方法应用于刚刚创建的对象。Date 类中有一个 toString 方法。这
个方法将返回日期的字符串描述。下面的语句可以说明如何将 toString 方法应用于新构造的Date 对象上。

public class Test {
    
    
    public static void main(String[] args) {
    
    
        String s = new Date().toString();
        System.out.println(s);
    }
}
//Thu Aug 12 18:23:13 CST 2021

在这两个例子中, 构造的对象仅使用了一次。通常, 希望构造的对象可以多次使用, 因此,需要将对象存放在一个变量中:

Date birthday = new Date();

下图显示了引用新构造的对象变量 birthday
在这里插入图片描述
在对象与对象变量之间存在着一个重要的区别。例如, 语句:

Date deadline; // deadline doesn't refer to any object

定义了一个对象变量 deadline, 它 可 以 引 用 Date 类型的对象。但是,一定要认识到: 变量deadline 不是一个对象, 实际上也没有引用对象。此时,不能将任何 Date 方法应用于这个变
量上。语句

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Date deadline;
        String s = deadline.toString(); // not yet
    }
}
//java: 可能尚未初始化变量deadline

将产生编译错误
必须首先初始化变量 deadline, 这里有两个选择。当然,可以用新构造的对象初始化这个变量:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Date birthday = new Date();
        Date deadline;
        deadline = new Date();
        deadline = birthday;
    }
}

现在,这两个变量引用同一个对象
在这里插入图片描述
一定要认识到: 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。
在 Java 中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。下列语句:

Date deadline= new Date();

有两个部分。表达式 new Date() 构造了一个 Date 类型的象, 并且它的值是对新创建对象的 引用。这个引用存储在变量
deadline 中

可以显式地将对象变量设置为 null 表明这个对象变量目前没有引用任何对象。

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Date birthday = new Date();
        Date deadline;
        deadline = new Date();
        deadline = birthday;
        deadline = null;
        if (deadline != null) {
    
    
            System.out.println(deadline);
        }
    }
}
//没有任何输出

如果将一个方法应用于一个值为 null 的对象上,那么就会产生运行时错误。

import java.util.Date;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Date birthday = new Date();
        Date deadline;
        deadline = new Date();
        deadline = birthday;
        deadline = null;
        if (deadline != null) {
    
    
            System.out.println(deadline);
        }
        birthday = null;
        String s = birthday.toString(); // runtime error!
    }
}
//Exception in thread "main" java.lang.NullPointerException at Test.main(Test.java:22)

局部变量不会自动地初始化为 null,而必须通过调用 new 或将它们设置为 null 进行初始化

很多人错误地认为 Java 对象变量与 C++ 的引用类似。然而,在 C++ 中没有空引用, 并且引用不能被赋值。可以将 Java
的对象变量看作C++ 的对象指针。
Date birthday; // Java
实际上,等同于
Date* birthday; // C++
一旦理解了这一点, 一切问题就迎刃而解了。 当然,一个Date* 指针只能通过调用new 进行初始化。就这一点而言,C++与 Java 的语法几乎是一样的 >
Date* birthday = new Date(); // C++
如果把一个变量的值賦给另一个变量, 两个变量就指向同一个日期,即它们是同一个对象的指针。 在 Java 中的 null 引用对应 C++ 中的 NULL 指针。
所有的 Java 对象都存储在堆中。 当一个对象包含另一个对象变量时, 这个变量依然包含着指向另一个堆对象的指针。
在 C++ 中, 指针十分令人头疼, 并常常导致程序错误。稍不小心就会创建一个错误的指针,或者造成内存溢出。在 Java 语言中,这些问题都不复存在。 如果使用一个没有初始化的指针, 运行系统将会产生一个运行时错误, 而不是生成一个随机的结果, 同时,不必担心内存管理问题,垃圾收集器将会处理相关的事宜。
C++ 确实做了很大的努力, 它通过拷贝型构造器和复制操作符来实现对象的自动拷贝。 例如,一个链表( linked list) 拷贝的结果将会得到一个新链表, 其内容与原始链表相同, 但却是一组独立的链接。这使得将同样的拷贝行为内置在类中成为可能。在 Java中,必须使用 clone 方法获得对象的完整拷贝

2.2 Java 类库中的 LocalDate 类

在前面的例子中, 已经使用了 Java 标准类库中的 Date 类。Date 类的实例有一个状态,即特定的时间点
尽管在使用 Date 类时不必知道这一点,但时间是用距离一个固定时间点的毫秒数(可正可负) 表示的, 这个点就是所谓的纪元( epoch), 它 是 UTC 时间 1970 年 1 月 1 日 00:00:00。UTC 是 Coordinated Universal Time 的缩写,与大家熟悉的 GMT ( 即GreenwichMeanTime,格林威治时间)一样,是一种具有实践意义的科学标准时间但是,Date 类所提供的日期处理并没有太大的用途。Java 类库的设计者认为: 像”December 31, 1999, 23:59:59" 这样的日期表示法只是阳历的固有习惯。这种特定的描述法遵循了世界上大多数地区使用的 Gregorian 阳历表示法。但是, 同一时间点采用中国的农历表示和采用希伯来的阴历表示就很不一样,对于火星历来说就更不可想象了。

类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类: 一个是用来表示时间点的 Date类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。Java SE 8弓丨入了另外一些类来处理日期和时间的不同方法。

不要使用构造器来构造 LocalDate 类的对象。实际上,应当使用静态工厂方法 (factory
method) 代表你调用构造器。下面的表达式:

import java.time.LocalDate;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        LocalDate.now();
    }
}

会构造一个新对象,表示构造这个对象时的日期。
可以提供年、 月和日来构造对应一个特定日期的对象:

import java.time.LocalDate;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        LocalDate.of(2021, 8, 12);
    }
}

一旦有 了一个 LocalDate 对象, 可以用方法 getYear、 getMonthValue 和 getDayOfMonth
得到年、月和日:

import java.time.LocalDate;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        LocalDate newYearsEve = LocalDate.of(2000, 2, 15);
        int year = newYearsEve.getYear();
        int month = newYearsEve.getMonthValue();
        int day = newYearsEve.getDayOfMonth();
        System.out.println(year);
        System.out.println(month);
        System.out.println(day);
    }
}
//2000
//2
//15

看起来这似乎没有多大的意义, 因为这正是构造对象时使用的那些值。不过,有时可能
某个日期是计算得到的,你希望调用这些方法来得到更多信息。例如, plusDays 方法会得到一个新的 LocalDate, 如果把应用这个方法的对象称为当前对象,这个新日期对象则是距当前对象指定天数的一个新日期

import java.time.LocalDate;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        LocalDate newYearsEve = LocalDate.of(2000,2,15);
        LocalDate aThousandDaysLaster = newYearsEve.plusDays(-10);
        int year = aThousandDaysLaster.getYear();
        int month = aThousandDaysLaster.getMonthValue();
        int day = aThousandDaysLaster.getDayOfMonth();
        System.out.println(year);
        System.out.println(month);
        System.out.println(day);
    }
}
//2000
//2
//5

LocalDate 类封装了实例域来维护所设置的日期。如果不查看源代码, 就不可能知道类内部的日期表示。当然, 封装的意义在于,这一点并不重要, 重要的是类对外提供的方法。

2.3 更改器方法与与访问器方法

再来看 plusDays 方法调用:

LocalDate aThousandDaysLaster = newYearsEve.plusDays(-10);

这个调用之后 newYeareEve 会有什么变化? 它会改为 -10天之后的日期吗? 事实上,并没有。plusDays 方法会生成一个新的 LocalDate 对象,然后把这个新对象赋给 aThousandDaysLater变量。原来的对象不做任何改动。 我们说 plusDays 方法没有更改调用这个方法的对象。(这类似于 String 类的 toUpperCase 方法。在一个字符串上调用toUpperCase 时,这个字符串仍保持不变,会返回一个将字符大写的新字符串。)

Java 库的一个较早版本曾经有另一个类来处理日历,名为 GregorianCalendar。 可以如下为这个类表示的一个日期增加 -10 天:

import java.util.Calendar;
import java.util.GregorianCalendar;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        GregorianCalendar someDay = new GregorianCalendar(2000,2,15);
        someDay.add(Calendar.DAY_OF_MONTH, -10);
        int year = someDay.get(Calendar.YEAR);
        int month = someDay.get(Calendar.MONTH);
        int day = someDay.get(Calendar.DAY_OF_MONTH);
        System.out.println(year);
        System.out.println(month);
        System.out.println(day);
    }
}
//2000
//2
//5

与 LocalDate.plusDays 方法不同,GregorianCalendar.add 方法是一个更改器方法 ( mutatormethod ) 调用这个方法后,someDay 对象的状态会改变
相 反, 只 访 问 对 象 而 不 修 改 对 象 的 方 法 有 时 称 为 访 问 器 方 法 例 如,
LocalDate.getYear 和 GregorianCalendar.get 就是访问器方法

2.4 LocalDate示例所用 API 汇总

类原型 简介
static LocalTime now( ) 构造一个表示当前日期的对象
static LocalTime of(int year, int month , int day ) 构造一个表示给定日期的对象
int getYear( )
int getMonthValue( )
int getDayOfMonth( ) 得到当前日期的年、 月和曰。
DayOfWeek getDayOfWeek 得到当前日期是星期几, 作为DayOfWeek 类的一个实例返回。 调用 getValue 来得到1 ~ 7 之间的一个数, 表示这是星期几, 1 表示星期一, 7 表示星期日
Local Date piusDays(int n )
Local Date minusDays(int n) 生成当前日期之后或之前 n 天的日期

3. 用户自定义类

要想创建一个完整的程序, 应该将若干类组合在一起, 其中只有一个类有 main 方法

3.1 创建 Employee 类

Java 中, 最简单的类定义形式为:
        class ClassName{
    
    
        field1
        field2
        constructor2
        constructor2
        method2
        method2
        }

下面看一个非常简单的 Employee 类。 在编写薪金管理系统时可能会用到。

import java.time.LocalDate;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        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);
        // raise everyone's salary by 5%
        for (Employee e : staff) {
    
    
            e.raiseSalary(5);
        }
        // print out information about all Employee objects
        for (Employee e : staff) {
    
    
            System.out.println("name=" + e.getName() +
                    ",salary=" + e.getSalary() +
                    ",hireDay=" + e.getHireDay());
        }
    }
}

class Employee {
    
    
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String n, double s, int year, int month, int day) {
    
    
        this.name = n;
        this.salary = s;
        this.hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {
    
    
        return name;

    }

    public double getSalary() {
    
    
        return salary;
    }

    public LocalDate getHireDay() {
    
    
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
    
    
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

//name=Carl Cracker,salary=78750.0,hireDay=1987-12-15
//name=Harry Hacker,salary=52500.0,hireDay=1989-10-01
//name=Tony Tester,salary=42000.0,hireDay=1990-03-15

在这个程序中,构造了一个 Employee 数组, 并填人了三个雇员对象:

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);

接下来,利用 Employee 类的 raiseSalary方法将每个雇员的薪水提高 5%:

public void raiseSalary(double byPercent) {
    
    
    double raise = this.salary * byPercent / 100;
    this.salary += raise;
}

最后,调用 getName 方法、getSalary方法和 getHireDay 方法将每个雇员的信息打印出来:

for (Employee e : staff) {
    
    
    System.out.println("name=" + e.getName() +
            ",salary=" + e.getSalary() +
            ",hireDay=" + e.getHireDay());
}

注意,在这个示例程序中包含两个类:Employee类和带有 public 访问修饰符的 Test类。Test 类包含了 main方法,其中使用了前面介绍的指令。
源文件名是 Test.java,这是因为文件名必须与 public 类的名字相匹配。在一个源文件中, 只能有一个公有类,但可以有任意数目的非公有类。
接下来,当编译这段源代码的时候, 编译器将在目录下创建两个类文件:Test.class 和 Employee.class
在这里插入图片描述
在这里插入图片描述
将程序中包含 main方法的类名提供给字节码解释器, 以便启动这个程序

java Test

在这里插入图片描述
字节码解释器开始运行 Test 类的 main方法中的代码。在这段代码中,先后构造了三个新 Employee 对象, 并显示它们的状态

3.2 多个源文件的使用

一个源文件包含了两个类。许多程序员习惯于将每一个类存在一个单独的源文件中。例如,将 Employee 类存放在文件 Employee.java 中, 将 Test 类存放在文件 EmployeeTest.java 中。
在这里插入图片描述
编译器:

javac Employee*.java

在这里插入图片描述
于是,所有与通配符匹配的源文件都将被编译成类文件。或者键人下列命令:

javac EmployeeTest.java

可能会感到惊讶,使用第二种方式,并没有显式地编译 Employeejava 然而,当 Java编译器发现 EmployeeTestjava 使用 Employee 类时会查找名为 Employee.class 的文件。如果没有找 到这个文件,就会自动地搜索 Employee.java, 然后,对它进行编译。更重要的是:如果 Employee.java 版本较已有的Employee.dass 文件版本新,Java 编译器就会自动地重新编译这个文件。
如果熟悉 UNIX 的“ make” 工具(或者是 Windows 中的“ nmake” 等工具,) 可以认为Java 编译器内置了“ make” 功能。

3.3 剖析 Employee 类

下面对 Employee 类进行剖析。首先从这个类的方法开始。 通过查看源代码会发现,这
个类包含1个构造器和 4 个方法:

public Employee(String n, double s, int year, int month, int day)

public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)
  • 这个类的所有方法都被标记为 public。 关键字 public 意味着任何类的任何方法都可以调用这些方法
  • 接下来,需要注意在 Employee 类的实例中有三个实例域用来存放将要操作的数据:
private String name;
private double salary;
private Local Date hireDay;
  • 关键字 private 确保只有 Employee 类自身的方法能够访问这些实例域, 而其他类的方法不能够读写这些域。

可以用 public 标记实例域, 但这是一种极为不提倡的做法, public 數据域允许程序中的任何方法对其进行读取和修改。, 这就完全破坏了封装。 任何类的任何方法都可以修改 public 域, 从我们的经验来看, 某些代码将使用这种存取权限, 而这并不我们所希望的, 因此, 这里强烈建议将实例域标记为 private
最后, 请注意, 有两个实例域本身就是对象: name 域是 String 类对象, hireDay 域是

  • LocalDate 类对象。这种情形十分常见:类通常包括类型属于某个类类型的实例域

3.4 从构造器开始

下面先看看 Employee 类的构造器

public Employee(String n, double s, int year, int month, int day) {
    
    
        this.name = n;
        this.salary = s;
        this.hireDay = LocalDate.of(year, month, day);
    }

可以看到, 构造器与类同名。在构造 Employee 类的对象时, 构造器会运行,以便将实例域初始化为所希望的状态。
例如, 当使用下面这条代码创建 Employee 类实例时:

new Employee("]ames Bond", 100000, 1950, 1, 1);

将会把实例域设置为:

name = “James Bond”;
salary = 100000;
hireDay = LocalDate.of(1950, 1, 1); // January 1, 1950

构造器与其他的方法有一个重要的不同。构造器总是伴随着 new 操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。例如将产生编译错误

new Employee("James Bond", 100000, 1950, 1, 1);//Yes
james.Employee("James Bond", 250000, 1950, 1, 1);// Error

现在只需要先了解:

  • 构造器与类同名
  • 每个类可以有一个以上的构造器
  • 构造器可以有 0 个、1 个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着 new 操作一起调用

Java 构造器的工作方式与 C++—样。但是, 要记住所有的 Java 对象都是在堆中构造的, 构造器总是伴随着 new操作符一起使用。C++ 程序员最易犯的错误就是忘记 new 操作符:
Employee number007("]anie5 Bond", 100000, 1950, 1, 1);// C++, not Java
这条语句在 C++ 中能够正常运行,但在 Java 中却不行

请注意, 不要在构造器中定义与实例域重名的局部变量。例如, 下面的构造器将
无法设置 salary

public Employee(String n, double s, . . .){
    
    
    String name = n; // Error
    double salary = s; // Error
}

这个构造器声明了局部变量 name 和 salary。这些变量只能在构造器内部访问。这些 变量屏蔽了同名的实例域有些程序设计者常常不假思索地写出 这类代码, 因为他们已经习惯增加这类数据类型。这种错误很难被检查出来, 因此, 必须注意在所有的方法中不要命名与实例域同名的变量

3.5 隐式参数与显式参数

方法用于操作对象以及存取它们的实例域。例如,方法:

public void raiseSalary(double byPercent) {
    
    
        double raise = this.salary * byPercent / 100;
        this.salary += raise;
    }
``
将调用这个方法的对象的 salary 实例域设置为新值。看看下面这个调用:
```java
Employee number007 = new Employee("James Bond", 100000, 1950, 1, 1);
number007.raiseSalary(5);

它的结果将 number007.salary 域的值增加 5%。具体地说,这个调用将执行下列指令:

double raise = number007.salary * 5 / 100;
number007.salary += raise;

raiseSalary 方法有两个参数。 第一个参数称为隐式 ( implicit ) 参数, 是出现在方法名前的 Employee类对象。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit) 参 数( 有些人把隐式参数称为方法调用的目标或接收者。

可以看到,显式参数是明显地列在方法声明中的, 例如 double byPercent。隐式参数没有出现在方法声明中。

在每一个方法中, 关键字 this 表示隐式参数。 如果需要的话,可以用下列方式编写
raiseSalary 方法:

public void raiseSalary(double byPercent) {
    
    
    double raise = this.salary * byPercent / 100;
    this.salary += raise;
}

有些程序员更偏爱这样的风格,因为这样可以将实例域与局部变量明显地区分开来。
C++ 注释:在 C++ 中, 通常在类的外面定义方法:

void Employee::raiseSalary(double byPercent) { // C++, not Java
. . .
}
如果在类的内部定义方法, 这个方法将自动地成为内联(inline) 方法
class Employee {
. . .
int getName() { return name; } // inline in C++
}

在 Java 中, 所有的方法都必须在类的内部定义, 但并不表示它们是内联方法。是否将某个方法设置为内联方法是 Java 虚拟机的任务。即时编译器会监视调用那些简洁、经常被调用、 没有被重载以及可优化的方法。

3.6 封装的优点

最后,再仔细地看一下非常简单的 getName 方法、 getSalary 方法和 getHireDay 方法

    public String getName() {
    
    
        return this.name;
    }

    public double getSalary() {
    
    
        return this.salary;
    }

    public LocalDate getHireDay() {
    
    
        return this.hireDay;
    }

这些都是典型的访问器方法。由于它们只返回实例域值, 因此又称为域访问器。
思考

将 name、 salary 和 hireDay 域标记为 public , 以此来取代独立的访问器方法会不会更容易些呢?

关键在于 name 是一个只读域。一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样来确保 name 域不会受到外界的破坏。虽然 salary 不是只读域,但是它只能用 raiseSalary 方法修改。特别是一旦这个域值出现了错误, 只要调试这个方法就可以了。如果 salary 域是 public 的,破坏这个域值的捣乱者有可能会出没在任何地方。
在有些时候, 需要获得或设置实例域的值。因此,应该提供下面三项内容:

  • 一个私有的数据域;
  • 一个公有的域访问器方法;
  • 一个公有的域更改器方法

这样做要比提供一个简单的公有数据域复杂些, 但是却有着下列明显的好处:首先, 可以改变内部实现,除了该类的方法之外,不会影响其他代码。例如,如果将存储名字的域改为:

String firstName;
String lastName;

那么 getName 方法可以改为返回

firstName + " " + lastName

对于这点改变, 程序的其他部分完全不可见。当然, 为了进行新旧数据表示之间的转换,访问器方法和更改器方法有可能需要做许多工作。但是, 这将为我们带来了第二点好处:更改器方法可以执行错误检查, 然而直接对域进行赋值将不会进行这些处理。例如, setSalary 方法可以检查薪金是否小于 0。
**警告:**注意不要编写返回引用可变对象的访问器方法。在 Employee 类中就违反了这个设计原则, 其中的 getHireDay 方法返回了一个 Date 类对象:

class Employee {
    
    
    private Date hireDay;
    public Date getHireDay() {
    
    
        return this.hireDay;
    }
}

LocalDate 类没有更改器方法, 与之不同, Date 类有一个更改器方法 setTime, 可以在这里设置毫秒数。Date 对象是可变的, 这一点就破坏了封装性! 请看下面这段代码:

Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);
// let's give Harry ten years of added seniority

出错的原因很微妙。d 和 harry.hireDay 引用同一个对象(请参见图)。更改器方法就可以自动地改变这个雇员对象的私有状态!
在这里插入图片描述
如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone )。对象 clone 是指存放在另一个位置上的对象副本。下面是修改后的代码:

class Employee {
    
    
    private Date hireDay;
    public Date getHireDay() {
    
    
        return (Date)(this.hireDay.clone());
    }
}

3.7 基于类的访问权限

从前面已经知道,方法可以访问所调用对象的私有数据。一个方法可以访问所属类的所有对象的私有数据,这令很多人感到奇怪!例如,下面看一下用来比较两个雇员的 equals方法。

class Employee {
    
    
    public boolean equals(Employee other) {
    
    
        return name.equals(other.name);
    }
}

典型的调用方式是

if (harry,equals(boss)) . . .

这个方法访问 harry 的私有域, 这点并不会让人奇怪,然而, 它还访问了 boss 的私有域。这是合法的, 其原因是 boss 是 Employee 类对象, 而 Employee 类的方法可以访问Employee 类的任何一个对象的私有域。
C++ 注释: C++ 也有同样的原则。方法可以访问所属类的私有特性( feature ), 而不仅限于访问隐式参数的私有特性

3.8 私有方法

在实现一个类时,由于公有数据非常危险, 所以应该将所有的数据域都设置为私有的。然而,方法又应该如何设计呢? 尽管绝大多数方法都被设计为公有的,但在某些特殊情况下,也可能将它们设计为私有的。有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常, 这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密, 或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为private 的。在 Java 中,为了实现一个私有的方法, 只需将关键字 public 改为 private 即可。
对于私有方法, 如果改用其他方法实现相应的操作, 则不必保留原有的方法。如果数据的表达方式发生了变化,这个方法可能会变得难以实现, 或者不再需要。然而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的, 就不能将其删去,因为其他的代码很可能依赖它。

3.9 final 实例域

可以将实例域定义为 final。 构建对象时必须初始化这样的域。也就是说, 必须确保在每一个构造器执行之后,这个域的值被设置, 并且在后面的操作中, 不能够再对它进行修改。例如,可以将 Employee 类中的 name 域声明为 final, 因为在对象构建之后,这个值不会再被修改, 即没有 setName 方法。

class Employee {
    
    
    private final String name;
}

final 修饰符大都应用于基本 (primitive ) 类型域,或不可变(immutable) 类的域(如果类中的每个方法都不会改变其对象, 这种类就是不可变的类。例如,String类就是一个不可变的类)。
对于可变的类, 使用 final 修饰符可能会对读者造成混乱。例如:

private final StringBuilder evaluations;

在 Employee 构造器中会初始化为

evaluations = new StringBuilder();

final 关键字只是表示存储在 evaluations 变量中的对象引用不会再指示其他StringBuilder对象。不过这个对象可以更改:

public void giveGoldStar(){
    
    
    evaluations.append(LocalDate.now() + ": Gold star!\n");
}

4. 静态域与静态方法

在前面给出的示例程序中,main 方法都被标记为 static 修饰符。下面讨论一下这个修饰
符的含义

4.1 静态域

如果将域定义为 static, 每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。例如, 假定需要给每一个雇员賦予唯一的标识码。这里给Employee类添加一个实例域 id 和一个静态域 nextld:

class Employee {
    
    
    private static int nextld = 1;
    private int id;
}

现在, 每一个雇员对象都有一个自己的 id 域, 但这个类的所有实例将共享一个 iiextld域。换句话说, 如果有 1000 个 Employee 类的对象, 则有 1000 个实例域 id。但是,只有一个静态域 nextld。即使没有一个雇员对象, 静态域 nextld 也存在。它属于类,而不属于任何独立的对象

注释:在绝大多数的面向对象程序设计语言中, 静态域被称为类域。术语“ static” 只是沿用了 C++ 的叫法, 并无实际意义。
下面实现一个简单的方法

public void setId(){
    
    
    id = nextld;
    nextld++;
}

假定为 harry 设定雇员标识码:

harry.setId();

harry 的 id 域被设置为静态域 nextld 当前的值,并且静态域 nextld 的值加 1:

harry.id = Employee.nextId;
Employee.nextId++;

4.2 静态常量

静态变量使用得比较少,但静态常量却使用得比较多。例如, 在 Math 类中定义了一个静态常量:

public class Hath {
    
    
    public static final double PI = 3.14159265358979323846;
}

在程序中,可以采用 Math.PI 的形式获得这个常量。
如果关键字 static 被省略, PI 就变成了 Math 类的一个实例域。需要通过 Math 类的对象访问 PI,并且每一个 Math 对象都有它自己的一份 PI 拷贝。
另一个多次使用的静态常量是 System.out。它在 System 类中声明:

public class System {
    
    
    public static final PrintStream out = . . .;
    . . .
}

前面曾经提到过,由于每个类对象都可以对公有域进行修改,所以,最好不要将域设计为 public。然而, 公有常量(即 final 域)却没问题。因为 out 被声明为 final, 所以,不允许再将其他打印流陚给它:

System.out = new PrintStrean(. . .); // Error out is final

如果查看一下 System 类, 就会发现有一个 setOut 方法, 它可以将 System.out 设置为不同的流。 读者可能会感到奇怪, 为什么这个方法可以修改 final 变量的值。原因在于,setOut 方法是一个本地方法, 而不是用 Java 语言实现的。本地方法可以绕过Java 语言的存取控制机制。这是一种特殊的方法, 在自己编写程序时, 不应该这样处理
在这里插入图片描述

4.3 静态方法

静态方法是一种不能向对象实施操作的方法。例如, Math 类的 pow 方法就是一个 静态
方法。表达式

Math.pow(x, a)

计算X^a, 不使用任何 Math 对象。换句话说,没有隐式的参数:
可以认为静态方法是没有 this 参数的方法(在一个非静态的方法中, this 参数表示这个方法的隐式参数,参见目录3.5)
Employee 类的静态方法不能访问 Id 例域, 因为它不能操作对象。但是,静态方法可
以访问自身类中的静态域。下面是使用这种静态方法的一个示例:

public static int getNextId() {
    
    
return nextId; // returns static field
}

可以通过类名调用这个方法:

int n = Employee.getNextId();

这个方法可以省略关键字 static 吗? 答案是肯定的。但是,需要通过 Employee 类对象的引用调用这个方法
注释: 可以使用对象调用静态方法。例如, 如果 harry 是一个 Employee 对象, 可以用 harry.getNextId( ) 代替 Employee.getNextId() 不过,这种方式很容易造成混淆,其原因是 getNextld 方法计算的结果与 harry 毫无关系。建议使用类名, 而不是对象来调用静态方法。

在下面两种情况下使用静态方法:

  • 一 方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)
    在这里插入图片描述
  • 一个方法只需要访问类的静态域(例如:Employee.getNextld)
    在这里插入图片描述
    C++ 注释: :Java 中的静态域与静态方法在功能上与 C++ 相同。但是, 语法书写上却稍有所不同。在 C++ 中,使用::操作符访问自身作用域之外的静态域和静态方法, 如Math::PI

术语“ static” 有一段不寻常的历史。起初,C 引入关键字 static 是为了表示退出一个块后依然存在的局部变量在这种情况下, 术语“ static” 是有意义的: 变量一直存在,当再次进入该块时仍然存在
随后, static 在 C 中有了第二种含义, 表示不能被其他文件访问的全局变量和函数。为了避免引入一个新的关键字, 关键字 static 被重用了
最后,C++ 第三次重用了这个关键字,与前面赋予的含义完全不一样, 这里将其解释为:属于类且不属于类对象的变量和函数。这个含义与 Java 相同

4.4 工厂方法

静态方法还有另外一种常见的用途。类似 LocalDate 和 NumberFormat 的类使用静态工厂方法 (factory methocO 来构造对象。你已经见过工厂方法 LocalDate.now 和LocalDate.of。NumberFormat 类如下使用工厂方法生成不同风格的格式化对象:

import java.text.NumberFormat;

public class Test {
    
    
    public static void main(String[] args) {
    
    
        NumberFormat currentFormatter = NumberFormat.getCurrencyInstance();
        NumberFormat percentFormatter = NumberFormat.getPercentInstance();
        double x = 0.1;
        System.out.println(currentFormatter.format(x));
        System.out.println(percentFormatter.format(x));
    }
}
//¥0.10
//10%

为什么 NumberFormat 类不利用构造器完成这些操作呢? 这主要有两个原因:

  • 无法命名构造器。构造器的名字必须与类名相同。但是, 这里希望将得到的货币实例和百分比实例采用不用的名字。
  • 当使用构造器时,无法改变所构造的对象类型。而 Factory 方法将返回一个DecimalFormat类对象,这是 NumberFormat 的子类

4.5 main 方法

需要注意,不需要使用对象调用静态方法。例如,不需要构造 Math 类对象就可以调用
Math.pow
同理, main 方法也是一个静态方法。

public class Test {
    
    
    public static void main(String[] args) {
    
    
    }
}

main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main 方法将执行并创建程序所需要的对象。

**提示:**每一个类可以有一个 main 方法。这是一个常用于对类进行单元测试的技巧。例如, 可以在 Employee 类中添加一个 main 方法:

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("Tom", 40000);
        staff[1] = new Employee("Dick", 60000);
        staff[2] = new Employee("Harry", 65000);
        for (Employee e : staff) {
    
    
            e.setId();
            System.out.println("name=" + e.getName() + ",id=" + e.getId() + ", salary=" + e.getSalary());
        }
    }
}

class Employee {
    
    
    private static int nextId = 1;
    private String name;
    private double salary;
    private int id;

    public Employee(String n, double s) {
    
    
        this.name = n;
        this.salary = s;
        this.id = 0;
    }

    public static int getNextId() {
    
    
        return nextId;
    }

    public String getName() {
    
    
        return this.name;
    }

    public double getSalary() {
    
    
        return this.salary;
    }

    public int getId() {
    
    
        return this.id;
    }

    public void setId() {
    
    
        this.id = nextId;
        ++nextId;
    }

    public static void main(String[] args) {
    
    //unit test
        Employee e = new Employee("Harry", 50000);
        System.out.println(e.getName() + " " + e.getSalary());
    }
}

如果想要独立地测试 Employee 类, 只需要执行
java Employee
在这里插入图片描述
在这里插入图片描述

如果 Employee 类是一个更大型应用程序的一部分, 就可以使用下面这条语句运行程序
java Test
Employee 类的 main 方法永远不会执行。
在这里插入图片描述

5. 方法参数

首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用 (call by value) 表示方法接收的是调用者提供的值。而按引用调用 ( call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。“ 按… … 调用”(call by) 是一个标准的计算机科学术语,它用来描述各种程序设计语言(不只是 Java ) 中方法参数的传递方式
Java 程序设计语言总是采用按值调用。也就是说, 方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

double percent = 10;
harry.raiseSalary(percent):

不必理睬这个方法的具体实现, 在方法调用之后, percent 的值还是 10。
下面再仔细地研究一下这种情况。假定一个方法试图将一个参数值增加至 3 倍:

public class Test {
    
    
    public static void tripleValue(double x) {
    
    
        x = x * 3;
    }

    public static void main(String[] args) {
    
    
        double percent = 10;
        tripleValue(percent);
        System.out.println(percent);
    }
}
//10.0

不过,并没有做到这一点调用这个方法之后,percent 的值还是 10。下面看一下具体的执行过程:

  1. x 被初始化为 percent 值的一个拷贝(也就是 10 )
  2. x 被乘以 3后等于 30。 但是 percent 仍然是 10
  3. 这个方法结束之后,参数变量 X 不再使用。然而,方法参数共有两种类型:
    基本数据类型(数字、布尔值)
    对象引用
    在这里插入图片描述

现在已经看到,一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了,可以很容易地利用下面这个方法实现将一个雇员的薪金提高两倍的操作:

public static void tripleSalary(Employee x) {
    
    // works
    x.raiseSalary(200);
    System.out.println("End of method: salary=" + x.getSalary());
}

当调用

harry = new Employee(. . .);
tripleSalary(harry);

时,具体的执行过程为:

  1. X 被初始化为 harry 值的拷贝,这里是一个对象的引用。
  2. raiseSalary 方法应用于这个对象引用。x 和 harry 同时引用的那个 Employee 对象的薪金提高了 200%。
  3. 方法结束后,参数变量 x 不再使用。当然,对象变量 harry 继续引用那个薪金增至 3倍的雇员对象
    在这里插入图片描述

实现一个改变对象参数状态的方法并不是一件难事。理由很简单, 方法 得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。很多程序设计语言(特别是, C++ 和 Pascal) 提供了两种参数传递的方式:值调用和引用调用。有些程序员认为Java 程序设计语言对对象采用的是引用调用, 实际上, 这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题

首先,编写一个交换两个雇员对象的方法:
如果 Java 对对象采用的是按引用调用,那么这个方法就应该能够实现交换数据的效果;但是,方法并没有改变存储在变量 a 和 b 中的对象引用。swap 方法的参数 x 和 y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

public static void main(String[] args) {
    
    
        Employee a = new Employee("Alice", 1);
        Employee b = new Employee("Bob", 2);
        System.out.println("Before:");
        System.out.println(a.getName());
        System.out.println(b.getName());
        swap(a, b);
        System.out.println("After:");
        System.out.println(a.getName());
        System.out.println(b.getName());
}
    
public static void swap(Employee x, Employee y) {
    
    
    Employee tmp = x;
    x = y;
    y = tmp;
    System.out.println("End of method: x=" + x.getName());
    System.out.println("End of method: y=" + y.getName());
}
Before:
Alice
Bob
End of method: x=Bob
End of method: y=Alice
After:
Alice
Bob

最终,白费力气。在方法结束时参数变量 X 和 y 被丢弃了。原来的变量 a 和 b 仍然引用
这个方法调用之前所引用的对象
在这里插入图片描述
这个过程说明:Java 程序设计语言对对象采用的不是引用调用,实际上, 对象引用是按值传递的。
下面总结一下 Java 中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
  • 一个方法可以改变一个对象参数的状态。
  • 一个方法不能让对象参数引用一个新的对象。

完整的代码如下: 可以测试参数如何影响引用的对象状态

public class Test {
    
    
    public static void main(String[] args) {
    
    
        Employee a = new Employee("Alice", 1);
        Employee b = new Employee("Bob", 2);
        System.out.println("Before:");
        System.out.println(a.getName());
        System.out.println(b.getName());
        swap(a, b);
        System.out.println("After:");
        System.out.println(a.getName());
        System.out.println(b.getName());
    }

    private static void tripleValue(double x) {
    
    //doesn't work
        x = 3 * x;
        System.out.println("End of method: x=" + x);
    }

    public static void tripleSalary(Employee x) {
    
    // works
        x.raiseSalary(200);
        System.out.println("End of method: salary=" + x.getSalary());
    }

    public static void swap(Employee x, Employee y) {
    
    
        Employee tmp = x;
        x = y;
        y = tmp;
        System.out.println("End of method: x=" + x.getName());
        System.out.println("End of method: y=" + y.getName());
    }
}

class Employee {
    
    
    private String name;
    private double salary;

    public Employee(String n, double s) {
    
    
        this.name = n;
        this.salary = s;
    }

    public String getName() {
    
    
        return this.name;
    }

    public double getSalary() {
    
    
        return this.salary;
    }

    public void raiseSalary(double byPercent) {
    
    
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

6. 对象构造

前面已经学习了编写简单的构造器,可以定义对象的初始状态。但是,由于对象构造非
常重要,所以 Java 提供了多种编写构造器的机制。下面将详细地介绍这些机制。
有些类有多个构造器。例如, 可以如下构造一个空的 StringBuilder 对象:

6.1 重载

StringBuilder messages = new StringBuilder();
StringBuilder todoList = new StringBuilder("To do:\n");

这种特征叫做重载( overloading。) 如果多个方法(比如, StringBuilder 构造器方法)有相同的名字、 不同的参数,与返回值无关,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数, 就会产生编译时错误,因为根本不存在匹配, 或者没有一个其他的更好。(这个过程被称为重载解析(overloading resolution)。)

注释:Java 允许重载任何方法, 而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)。例如, String 类有 4个称为 indexOf 的公有方法。它们的签名是:

indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)

6.2 默认域初始化

如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值: 数值为 0、布尔值为 false、 对象引用为 null。然而,只有缺少程序设计经验的人才会这样做。确实, 如果不明确地对域进行初始化,就会影响程序代码的可读性。

注释:这是域与局部变量的主要不同点。 必须明确地初始化方法中的局部变量。但是,如果没有初始化类中的域, 将会被自动初始化为默认值( 0、false 或 null )。

例如, 仔细看一下 Employee 类。 假定没有在构造器中对某些域进行初始化, 就会默认地将 salary 域初始化为 0, 将 name 和 hireDay 域初始化为 null。

但是,这并不是一种良好的编程习惯。 如果此时调用 getName 方法或 getHireDay 方法,则会得到一个 null 引用

6.3 无参数的构造器

很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时, 其状态会设置为适当的默认值。 例如, 以下是 Employee 类的无参数构造函数:

    public Employee() {
    
    
        this.name = "";
        this.salary = 0;
        this.hireDay = LocalDate.now();
    }

如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是, 实例域中的数值型数据设置为 0、 布尔型数据设置为 false、 所有对象变量将设置为 null。
如果类中提供了至少一个构造器, 但是没有提供无参数的构造器, 则在构造对象时如果没有提供参数就会被视为不合法。例如, 在程序清单 4-2 中的 Employee 类提供了一个简单的构造器:

Employee(String name, double salary, int y, int ra , int d)

对于这个类,构造默认的雇员属于不合法。也就是, 调用

e = new Eraployee() ;

**警告:**请记住,仅当类没有提供任何构造器的时候, 系统才会提供一个默认的构造器如果在编写类的时候, 给出了一个构造器, 哪怕是很简单的, 要想让这个类的用户能够采用下列方式构造实例:

new ClassName()

就必须提供一个默认的构造器 ( 即不带参数的构造器)。 当然, 如果希望所有域被赋予默认值, 可以采用下列格式:

public ClassName(){
    
    
}

6.4 显式域初始化

通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯。
可以在类定义中, 直接将一个值赋给任何域。例如:

class Employee{
    
    
    private String name = "";
}

在执行构造器之前,先执行赋值操作。当一个类的所有构造器都希望把相同的值赋予某个特定的实例域时,这种方式特别有用。
初始值不一定是常量值。在下面的例子中, 可以调用方法对域进行初始化。仔细看一下Employee 类,其中每个雇员有一个 id 域。可以使用下列方式进行初始化:

    private static int assignId() {
    
    
        int r = nexId;
        ++nexId;
        return r;
    }

**C++ 注释:**在 C++ 中, 不能直接初始化类的实例域。所有的域必须在构造器中设置。但是,有一个特殊的初始化器列表语法,如下所示:

Employee::Employee(String n,double s, int y, int m, int d) // C++
: name(n),
  salary(s),
  hireDay(y, m, d)
{
    
    
}

C++ 使用这种特殊的语法来调用域构造器。在 Java 中没有这种必要, 因为对象没有子对象, 只有指向其他对象的指针。

6.5 参数名

在编写很小的构造器时(这是十分常见的,) 常常在参数命名上出现错误。通常, 参数用单个字符命名:

    public Employee(String n, double s) {
    
    
        name = n;
        salary = s;
    }

但这样做有一个缺陷:只有阅读代码才能够了解参数 n 和参数 s 的含义。
有些程序员在每个参数前面加上一个前缀“ a”:

    public Employee(String aName, double aSalary) {
    
    
        name = aName;
        salary = aSalary;
    }

这样很清晰。每一个读者一眼就能够看懂参数的含义。
还有一种常用的技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。例如,如果将参数命名为 salary, salary 将引用这个参数, 而不是实例域。 但是,可以采用 this.salary 的形式访问实例域。回想一下,this 指示隐式参数, 也就是所构造的对象。下面是一个示例:

    public Employee(String aName, double aSalary) {
    
    
        this.name = aName;
        this.salary = aSalary;
    }

**C++ 注释:**在 C++ 中, 经常用下划线或某个固定的字母(一般选用 m 或 x ) 作为实例域的前缀例如,salary 域可能被命名为 salary、mSalary 或 xSalary Java 程序员通常不这样做

6.6 调用另一个构造器

关键字 this 引用方法的隐式参数。然而,这个关键字还有另外一个含义。如果构造器的第一个语句形如 this(…), 这个构造器将调用同一个类的另一个构造器。下面是一个典型的例子:

    public Employee(double s) {
    
    
        this("Employee #" + nexId, s);
        nexId++;
    }

当调用 new Employee(60000) 时, Employee(double) 构造器将调用Employee(String,double)构造器。
采用这种方式使用 this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可。
C++ 注释: 在 Java 中, this 引用等价于 C++ 的 this 指针。但是, 在 C++ 中, 一个构造器不能调用另一个构造器 ,, 在 C++ 中, 必须将抽取出的公共初始化代码编写成一个独立的方法。

6.7 初始化块

前面已经讲过两种初始化数据域的方法:

  • 在构造器中设置值
  • 在声明中赋值
    实际上,Java 还有第三种机制, 称为初始化块(initializationblock)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。例如:
    public Employee() {
    
    
        this.name = "";
        this.salary = 0;
        this.hireDay = LocalDate.now();
    }

    public Employee(double s) {
    
    
        this("Employee #" + nexId, s);
        nexId++;
    }

    public Employee(String aName, double aSalary) {
    
    
        this.name = aName;
        this.salary = aSalary;
    }

    {
    
    
        id = nexId;
        ++nexId;
    }

在这个示例中,无论使用哪个构造器构造对象,id 域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。

这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。

注释: 即使在类的后面定义, 仍然可以在初始化块中设置域。但是, 为了避免循环定义,不要读取在后面初始化的域。这个规则的复杂度足以使编译器的实现者头疼, 因此建议将初始化块放在域定义之后。

6.8 对象析构与 finalize 方法

有些面向对象的程序设计语言,特别是 C++, 有显式的析构器方法,其中放置一些当对象不再使用时需要执行的清理代码。在析构器中, 最常见的操作是回收分配给对象的存储空间。由于 Java 有自动的垃圾回收器,不需要人工回收内存, 所以 Java 不支持析构器。

当然,某些对象使用了内存之外的其他资源, 例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时, 将其回收和再利用将显得十分重要。

可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用 finalize 方法回收任何短缺的资源, 这是因为很难知道这个方法什么时候才能够调用。

如果某个资源需要在使用完毕后立刻被关闭, 那么就需要由人工来管理。对象用完时,可以应用一个 close 方法来完成相应的清理操作

7. 包

Java 允许使用包( package ) 将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
标准的 Java 类库分布在多个包中,包括 java.lang、java.util 和java.net 等。标准的Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的Java 包都处于java 和 javax 包层次中。
使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了 Employee类。只要将这些类放置在不同的包中, 就不会产生冲突。事实上,为了保证包名的绝对唯一性, Sun 公司建议将公司的因特网域名(这显然是独一无二的) 以逆序的形式作为包名,并且对于不同的项目使用不同的子包。例如, horstmann.com 是本书作者之一注册的域名。逆序形式为 com.horstmann。这个包还可以被进一步地划分成子包, 如com.horstmann.corejava。从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.utU 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合。

7.1 类的导入

一个类可以使用所属包中的所有类, 以及其他包中的公有类( public class。) 我们可以采用两种方式访问另一个包中的公有类。第一种方式是在每个类名之前添加完整的包名。例如:

java.time.LocalDate today = java.time.LocalDate.now();

这显然很繁琐。更简单且更常用的方式是使用 import 语句。import 语句是一种引用包含在包中的类的简明描述。一旦使用了 import 语句,在使用类时,就不必写出包的全名了。
可以使用 import 语句导人一个特定的类或者整个包。import 语句应该位于源文件的顶部(但位于 package 语句的后面)。例如, 可以使用下面这条语句导人 java.util 包中所有的类。

import java.util.*;

然后, 就可以使用

LocalDate today = LocalDate.now();

而无须在前面加上包前缀。还可以导人一个包中的特定类:

import java.time.LocalDate;

java.time.* 的语法比较简单,对代码的大小也没有任何负面影响。当然, 如果能够明确地指出所导人的类, 将会使代码的读者更加准确地知道加载了哪些类。

**提示:*在 Eclipse 中, 可以使用菜单选项 Source— Organize Imports Package 语句, 如 import java.util.*; 将会自动地扩展指定的导入列表,如:
import java.util .ArrayList;
import java.util .Date;
这是一个十分便捷的特性。
但是, 需要注意的是, 只能使用星号(*) 导入一个包, 而不能使用 import java.
或import java.. 导入以 java 为前缀的所有包。
在大多数情况下, 只导入所需的包, 并不必过多地理睬它们。但在发生命名冲突的时候,就不能不注意包的名字了。例如,java.util 和 java.sql 包都有日期( Date) 类。如果在程序中导入了这两个包:

import java.util.*;
import java.sql.*;

在程序使用 Date 类的时候, 就会出现一个编译错误:

Date today;//Variable 'today' is already defined in the scope

此时编译器无法确定程序使用的是哪一个 Date 类。可以采用增加一个特定的 import 语句来解决这个问题:

import java.util.*;
import java.sql.*;
import java.util.Date;

如果这两个 Date 类都需要使用,又该怎么办呢? 答案是,在每个类名的前面加上完整的包名

        java.util.Date deadline = new java.util.Date();
        java.sql.Date today2 = new java.sql.Date(99999999);

在包中定位类是编译器 ( compiler) 的工作。类文件中的字节码肯定使用完整的包名来引用其他类

C++ 注释: C++ 程序员经常将 import 与 #include 弄混。 实际上, 这两者之间并没有共同之处。在 C++ 中,必须使用 include 将外部特性的声明加栽进来,这是因为 C++ 编译器无法查看任何文件的内部,除了正在编译的文件以及在头文件中明确包含的文件。Java编译器可以查看其他文件的内部,只要告诉它到哪里去查看就可以了。

在 Java 中, 通过显式地给出包名, 如java.util.Date, 就可以不使用 import ; 而在C++ 中, 无法避免使用 include 指令。

Import 语句的唯一的好处是简捷。 可以使用简短的名字而不是完整的包名来引用一个类。例如, 在 import java.util.* (或 import java.util.Date) 语句之后, 可以仅仅用Date引用 java.util.Date 类。
在 C++中, 与 包 机 制 类 似 的 是 命 名 空 间(namespace)。 在 Java 中, package与import 语句类似于 C++中的 namespace 和 using 指令。

7.2 静态导入

import 语句不仅可以导人类,还增加了导人静态方法和静态域的功能。例如,如果在源文件的顶部, 添加一条指令:

import static java.lang.System.*;
就可以使用 System 类的静态方法和静态域,而不必加类名前缀:
out.println("Goodbye, World!"); // i.e., System.out
exit(0); //i.e., System.exit
//Goodbye, World!

另外,还可以导入特定的方法或域:

import static java.lang.System.out;

实际上,是否有更多的程序员采用 System.out 或 System.exit 的简写形式,似乎是一件值得怀疑的事情。这种编写形式不利于代码的清晰度。不过,

sqrt(pow(2, 3) + pow(3, 4));

看起来比

 Math.sqrt(Math.pow(2,3) + Math.pow(3,4));

清晰得多

7.3 将类放入包中

要想将一个类放人包中, 就必须将包的名字放在源文件的开头,包中定义类的代码之

package corejava.horstman.com;

public class Employee {
    
    
    ...
}

如果没有在源文件中放置 package 语句, 这个源文件中的类就被放置在一个默认包( defaulf package ) 中。默认包是一个没有名字的包。在此之前,我们定义的所有类都在默认包中。
将包中的文件放到与完整的包名匹配的子目录中。例如,corejava.horstman.com 包
中的所有源文件应该被放置在子目录 corejava\horstman\com ( Windows 中 corejava\horstman\com
corejava) 中。编译器将类文件也放在相同的目录结构中
在这里插入图片描述
要想编译这个程序, 只需改变基目录,并运行命令

javac PackageTest.java

编译器就会自动地查找文件corejava\horstmann\com\Employee.java 并进行编译
下面看一个更加实际的例子。在这里不使用默认包, 而是将类分别放在不同的包中
( corejava.horstmann.com 和 mycompany.com)
在这里插入图片描述
在这种情况下,仍然要从基目录编译和运行类,即包含 com 目录:

javac com\mycompany\PayrollApp.java
java com.mycompany.PayrollApp

需要注意,编译器对文件 (带有文件分隔符和扩展名 .java 的文件)进行操作。而 Java解释器加载类(带有 . 分隔符 )

警告: 编译器在编译源文件的时候不检查目录结构。例如,假定有一个源文件开头有下列语句:
package con.myconpany;
即使这个源文件没有在子目录 com/mycompany 下, 也可以进行编译。如果它不依赖于其他包, 就不会出现编译错误。但是, 最终的程序将无法运行, 除非先将所有类文件移到正确的位置上。 如果包与目录不匹配, 虚拟机就找不到类

7.4 包作用域

前面已经接触过访问修饰符 public 和 private。标记为 public 的部分可以被任意的类使用;标记为 private 的部分只能被定义它们的类使用。如果没有指定 public 或 private ,这个部分(类、方法或变量)可以被同一个包中的所有方法访问。

No 范围 private default protected public
1 同一包中的同一类
2 同一包中的不同类
3 不同包中的子类
4 不同包中的非子类

8. 类路径

在前面已经看到,类存储在文件系统的子目录中。类的路径必须与包名匹配。
另外, 类文件也可以存储在 JAR(Java 归档)文件中。在一个 JAR 文件中, 可以包含多个压缩形式的类文件和子目录, 这样既可以节省又可以改善性能。在程序中用到第三方( third-party ) 的库文件时,通常会给出一个或多个需要包含的 JAR 文件。JDK 也提供了许多的 JAR 文件, 例如,在 jre/lib/rt.jar 中包含数千个类库文件

**提示:**JAR 文件使用 ZIP 格式组织文件和子目录。可以使用所有 ZIP 实用程序查看内部的 rt.jar 以及其他的 JAR 文件。
为了使类能够被多个程序共享,需要做到下面几点

  1. 把类放到一个目录中, 例如 /home/user/classdir。需要注意, 这个目录是包树状结构的基目录。如果希望将 com.horstmann.corejava.Employee 类添加到其中,这个Employee.class类文件就必须位于子目录 /home/user/classdir/com/horstmann/corejava中
  2. 将 JAR 文件放在一个目录中,例如:/home/user/archives
  3. 设置类路径(class path)。类路径是所有包含类文件的路径的集合
    在 UNIX 环境中, 类路径中的不同项目之间采用冒号 ㈠ 分隔:/home/user/classdir:.:/home/use r/archives/archive.jar;而在 Windows 环境中,则以分号(;)分隔:c:\classdir;.;c:\archives\archive.jar

警告:javac 编译器总是在当前的目录中查找文件, 但 Java 虚拟机仅在类路径中有目录的时候才查看当前目录如果没有设置类路径, 那也并不会产生什么问题, 默认的类路径包含目录.然而如果设置了类路径却忘记了包含目录, 则程序仍然 可以通过编译, 但不能运行

类路径所列出的目录和归档文件是搜寻类的起始点。下面看一个类路径示例:
/home/user/classdir:.:/home/user/archives/archive.jar假定虚拟机要搜寻 com.horstmann.corejava.Employee 类文件。它首先要查看存储在jre/lib 和jre/lib/ext 目录下的归档文件中所存放的系统类文件。显然,在那里找不到相应的类文件,然后再查看类路径。然后查找以下文件:

  • /home/user/classdir/com/horstmann/corejava/Employee.class
  • com/horstmann/corejava/Employee.class 从当前目录开始
  • com/horstmann/corejava/Employee.class inside /home/user/archives/archive.jar

编译器定位文件要比虚拟机复杂得多。如果引用了一个类,而没有指出这个类所在的包, 那么编译器将首先查找包含这个类的包,并询查所有的 import 指令,确定其中是否包含了被引用的类。例如, 假定源文件包含指令:

package corejava.horstman.com;
import java.util.*;

并且源代码引用了 Employee 类。 编译器将试图查找 java.lang.Employee (因为java.lang 包被默认导入)、java.util.Employee、 com.horstmann.corejava.Employee 和当前包中的Employee。对这个类路径的所有位置中所列出的每一个类进行逐一查看。如果找到了一个以上的类, 就会产生编译错误(因为类必须是唯一的,而 import 语句的次序却无关紧要)。
编译器的任务不止这些,它还要查看源文件( Source files) 是否比类文件新。如果是这样的话,那么源文件就会被自动地重新编译。在前面已经知道,仅可以导人其他包中的公有类。一个源文件只能包含一个公有类,并且文件名必须与公有类匹配。因此, 编译器很容易定位公有类所在的源文件。当然, 也可以从当前包中导入非公有类。这些类有可能定义在与类名不同的源文件中。如果从当前包中导人一个类, 编译器就要搜索当前包中的所有源文件, 以便确定哪个源文件定义了这个类。

8.1 设置类路径

最好采用 -classpath (或 -cp) 选项指定类路径:

ava -classpath /home/user/dassdir: .:/home/user/archives/archive.jar HyProg

或者

java -classpath c:\classdir; .;c:\archives\archive.jar MyProg

整个指令应该书写在一行中。将这样一个长的命令行放在一个 shell 脚本或一个批处理文件中是一个不错的主意。
利用 -dasspath 选项设置类路径是首选的方法, 也可以通过设置 CLASSPATH 环境变量完成这个操作。其详细情况依赖于所使用的 shell。在 Bourne Again shell ( bash) 中,命令格式如下:
export CLASSPATH=/home/user/classdir:.:/ home/user/archives/archive.jar
在 Windows shell, 命令格式如下:
set CLASSPATH=c:\classdir;.;c:\archives\archive.jar
直到退出 shell 为止,类路径设置均有效。

**警告:**有人建议将 CLASSPATH 环境变量设置为永久不变的值。 总的来说这是一个很糟糕的主意。人们有可能会忘记全局设置, 因此, 当使用的类没有正确地加载进来时,会感到很奇怪。一个应该受到it责的示例是 Windows 中 Apple 的 QuickTime 安装程序。它进行了全局设置, CLASSPATH 指向一个所需要的 JAR 文件, 但并没有在类路径上包含当前路径。 因此, 当程序编译后却不能运行时

警告: 有人建议绕开类路径, 将所有的文件放在 jre/lib/ext 路径。这是一个极坏的主意,其原因主要有两个: 当手工地加载其他的类文件时, 如果将它们存放在扩展路径上,则不能正常地工作(有关类加栽器的详细信息, 请参看卷 n 第 9 章)。此外, 程序员经常会忘记 3 个月前所存放文件的位置。 当类加载器忽略了曾经仔细设计的类路径时, 程序员会毫无头绪地在头文件中查找。事实上,加栽的是扩展路径上已长时间遗忘的类。

9. 文档注释

JDK 包含一个很有用的工具,叫做javadoc, 它可以由源文件生成一个 HTML 文档。
如果在源代码中添加以专用的定界符 /**开始的注释, 那么可以很容易地生成一个看上去具有专业水准的文档。这是一种很好的方式,因为这种方式可以将代码与注释保存在一个地方。如果将文档存人一个独立的文件中, 就有可能会随着时间的推移, 出现代码和注释不一致的问题。然而,由于文档注释与源代码在同一个文件中,在修改源代码的同时, 重新运行 javadoc 就可以轻而易举地保持两者的一致性

9.1 注释的插入

javadoc 实用程序(utility) 从下面几个特性中抽取信息:

  • 公有类与接口
  • 公有的和受保护的构造器及方法
  • 公有的和受保护的域

应该为上面几部分编写注释、 注释应该放置在所描述特性的前面。注释以 /**开始,并以*/ 结束。每个 /** . . . */ 文档注释在标记之后紧跟着自由格式文本( free-form text )。标记由 @开始, 如@author 或@param。自由格式文本的第一句应该是一个概要性的句子。javadoc 实用程序自动地将这些句子抽取出来形成概要页。在自由格式文本中,可以使用 HTML 修饰符, 例如,用于强调的 <em>…</em>、 用于着重强调的 <strong>…</stroiig> 以及包含图像的 <img …> 等。不过,一定不要使用 <hl>或<hr>, 因为它们会与文档的格式产生冲突。若要键入等宽代码, 需使用 {@code … } 而不是<code>…</code>—这样一来, 就不用操心对代码中的 < 字符转义了。

注释: 如果文档中有到其他文件的链接, 例如, 图像文件(用户界面的组件的图表或图像等), 就应该将这些文件放到子目录 doc-files 中。javadoc 实用程序将从源目录拷贝这些目录及其中的文件到文档目录中。在链接中需要使用 doc-files 目录, 例如:<img src=“ doc-files/uml_ png” alt=“ UML diagram”>

9.2 类注释

类注释必须放在 import 语句之后,类定义之前。
下面是一个类注释的例子:

/**
 * A {©code Card} object represents a playing card , such
 * as "Queen of Hearts". A card has a suit (Diamond , Heart ,
 * Spade or Club) and a value (1 = Ace , 2 . . . 10, 11 = Jack,
 * 12 = Queen , 13 = King)

 */
public class Card {
    
    
}

**注释:**没有必要在每一行的开始用星号 *, 例如, 以下注释同样是合法的:

/**
 A {©code Card} object represents a playing card , such
 as "Queen of Hearts". A card has a suit (Diamond , Heart ,
 Spade or Club) and a value (1 = Ace , 2 . . . 10, 11 = Jack,
 12 = Queen , 13 = King)
 */
public class Card {
    
    
}

然而, 大部分 IDE 提供了自动添加星号 *, 并且当注释行改变时, 自动重新排列这
些星号的功能。

9.3 方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外, 还可以使用下面的标记:

  • @param 变量描述
    这个标记将对当前方法的“ param” (参数)部分添加一个条目。这个描述可以占据多行, 并可以使用 HTML 标记。一个方法的所有 @param 标记必须放在一起。
  • @return 描述
    这个标记将对当前方法添加“ return” (返回)部分。这个描述可以跨越多行, 并可以使用 HTML 标记。
  • ©throws 类描述
    这个标记将添加一个注释, 用于表示这个方法有可能抛出异常

9.4 域注释

只需要对公有域(通常指的是静态常量)建立文档。 例如:

/**
 * The "Hearts" card suit
 */
public class Card {
    
    
    public static final int HEARTS = 1;
}

9.5 通用注释

下面的标记可以用在类文档的注释中。

  • @author 姓名
    这个标记将产生一个 “author” (作者)条目。可以使用多个 @author 标记,每个@author 标记对应一个作者
  • @version
    这个标记将产生一个“ version”(版本)条目。这里的文本可以是对当前版本的任何描述。
    下面的标记可以用于所有的文档注释中。
  • @since 文本
    这个标记将产生一个“ since” (始于)条目。这里的 text 可以是对引人特性的版本描述。例如, ©since version 1.7.10
  • @deprecated 文本
    这个标记将对类、 方法或变量添加一个不再使用的注释。文本中给出了取代的建议。例如:
    @deprecated Use <code> setVIsible(true) </code> instead
    通过 @see 和@link标记,可以使用超级链接, 链接到 javadoc 文档的相关部分或外部文档。
  • @see 引用
    这个标记将在“ see also” 部分增加一个超级链接。它可以用于类中,也可以用于方法中。这里的引用可以选择下列情形之一:
    package.class#feature label
    <a href="…"> label </a>
    “text”
    第一种情况是最常见的。只要提供类、 方法或变量的名字,javadoc 就在文档中插入
    一个超链接。例如:
    @see com.horstraann.corejava.Employee#raiseSalary(double)
    建立一个链接到 com.horstmann.corejava.Employee 类的 raiseSalary(double) 方法的超链接。 可以省略包名, 甚至把包名和类名都省去,此时,链接将定位于当前包或当前类

需要注意,一定要使用井号(#,) 而不要使用句号(.)分隔类名与方法名,或类名与变量名。Java 编译器本身可以熟练地断定句点在分隔包、 子包、 类、内部类与方法和变量时的不同含义。但是 javadoc 实用程序就没有这么聪明了,因此必须对它提供帮助。

如果 @see 标记后面有一个 < 字符,就需要指定一个超链接。可以超链接到任何URL。例如:

@see <a href="www.horstmann.com/corejava.html">The Core Java home page</a>

在上述各种情况下, 都可以指定一个可选的标签( label ) 作为链接锚(link anchor) 如果省略了 label , 用户看到的锚的名称就是目标代码名或 URL。
如果@see 标记后面有一个双引号(")字符,文本就会显示在 “see also” 部分。例如:

@see "Core Java 2 volume 2"

可以为一个特性添加多个 @see 标记,但必须将它们放在一起

  • 如果愿意的话, 还可以在注释中的任何位置放置指向其他类或方法的超级链接, 以及插人一个专用的标记, 例如:
{
    
    @link package.class#feature label}

这里的特性描述规则与@see 标记规则一样

9.6 包与概述注释

可以直接将类、 方法和变量的注释放置在 Java 源文件中, 只要用 /** . . . */ 文档注释界定就可以了。但是, 要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:

  1. ) 提供一个以 package.html 命名的 HTML 文件。在标记 <body> … </body> 之间的所有文本都会被抽取出来。
  2. 提供一个以 package-info.java 命名的 Java 文件。这个文件必须包含一个初始的以/**和 */ 界定的 Javadoc 注释, 跟随在一个包语句之后。它不应该包含更多的代码或注释
    还可以为所有的源文件提供一个概述性的注释。这个注释将被放置在一个名为overview,html 的文件中,这个文件位于包含所有源文件的父目录中。标记 <body>… </body> 2间的所有文本将被抽取出来。当用户从导航栏中选择“ Overview” 时,就会显示出这些注释内容

9.7 注释的抽取

这里,假设 HTML 文件将被存放在目录 docDirectory 下。执行以下步骤:

  1. 切换到包含想要生成文档的源文件目录。 如果有嵌套的包要生成文档, 例如com.horstmann.corejava, 就必须切换到包含子目录 com 的目录(如果存在 overview.html 文件的话, 这也是它的所在目录)
  2. 如果是一个包,应该运行命令:
    javadoc -d docDirectory nameOfPackage
    或对于多个包生成文档,运行:
    javadoc -d docDirectory nameOfPackage\ nameOfPackage . . .
    如果文件在默认包中, 就应该运行:
    javadoc -d docDirectory *.java
    如果省略了 -d docDirectory 选项, 那 HTML 文件就会被提取到当前目录下。这样有可能会带来混乱,因此不提倡这种做法。
    可以使用多种形式的命令行选项对 javadoc 程序进行调整。例如, 可以使用 -author 和-version 选项在文档中包含@author 和@version 标记(默认情况下,这些标记会被省略)。另一个很有用的选项是 -link, 用来为标准类添加超链接。例如, 如果使用命令:
    javadoc -link http://docs.oracle.com/javase/8/docs/api *.java
    那么,所有的标准类库类都会自动地链接到 Oracle 网站的文档
    如果使用 -linksource 选项,则每个源文件被转换为 HTML (不对代码着色,但包含行编号,) 并且每个类和方法名将转变为指向源代码的超链接

注释: 如果需要进一步的定制,例如, 生成非 HTML 格式的文档, 可以提供自定义的doclet, 以便生成想要的任何输出形式。显然, 这是一种特殊的需求。

10. 类设计技巧

  1. 一定要保证数据私有

这是最重要的;绝对不要破坏封装性。需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。很多惨痛的经验告诉我们, 数据的表示形式很可能会改变, 但它们的使用方式却不会经常发生变化。当数据保持私有时, 它们的表示形式的变化不对类的使用者产生影响, 即使出现 bug 也易于检测

  1. 一定要对数据初始化

需要编写一个访问器方法或更改器方法,但是最好还是保持实例域的私有性。很多惨痛的经验告诉我们, 数据的表示形式很可能会改变, 但它们的使用方式却不会经常发生变化。当数据保持私有时, 它们的表示形式的变化不会对类的使用者产生影响, 即使出现 bug 也易于检测

  1. 不要在类中使用过多的基本类

用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于
修改。例如:用一个称为 Address 的新的类替换一个 Customer 类中以下的实例域:
private String street;
private String city;
private String state;
private int zip;

  1. 不是所有的域都需要独立的域访问器和域更改器
  2. 将职责过多的类进行分解
  3. 类名和方法名要能够体现它们的职责
  4. 优先使用不可变的类

更改对象的问题在于, 如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。因此, 要尽可能让类是不可变的, 这是一个很好的想法。对于表示值的类, 如一个字符串或一个时间点,这尤其容易。计算会生成新值, 而不是更新原来的值

猜你喜欢

转载自blog.csdn.net/weixin_45364220/article/details/119639949