Java 核心技术 卷I 第4章 对象与类

第4章 对象与类

4.1 面向对象程序设计概述

面向对象程序设计(OOP)是当今主流的程序设计范型。

Java是完全面向对象的,必须熟悉OOP才能够编写Java程序。

面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏 的实现部分。程序中的很多对象来自标准库,还有一些是自定义的。究竟是自己构 造对象,还是从外界购买对象完全取决于开发项目的预算和时间。但是,从根本上 说,只要对象能够满足要求,就不必关心其功能的具体实现过程。在OOP中,不必关 心对象的具体实现,只要能够满足用户的需求即可。

传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了 这些过程,就要开始考虑存储数据的方式。这就是Pascal语言的设计者Niklaus Wirth将其著作命名为《算法+数据结构=程序》(Algorithms+Data Structures=Programs,Prentice Hall,1975)的原因。需要注意的是,在 Wirth命名的书名中,算法是第一位的,数据结构是第二位的,这就明确地表述了程 序员的工作方式。首先要确定如何操作数据,然后再决定如何组织数据,以便于数 据操作。而OOP却调换了这个次序,将数据放在第一位,然后再考虑操作数据的算 法。

4.1.1 类

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

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

实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通 过对象的方法与对象数据进行交互。封装给对象赋予了“黑盒”特征,这是提高重 用性和可靠性的关键。这意味着一个类可以全面地改变存储数据的方式,只要仍旧 使用同样的方法操作数据,其他对象就不会知道或介意所发生的变化。

OOP的另一个原则会让用户自定义Java类变得轻而易举,这就是:可以通过扩展一 个类来建立另外一个新的类。事实上,在Java中,所有的类都源自于一个“神通广 大的超类”,它就是Object。

在扩展一个已有的类时,这个扩展后的新类具有所扩展的类的全部属性和方法。在 新类中,只需提供适用于这个新类的新方法和数据域就可以了。通过扩展一个类来 建立另外一个类的过程称为继承(inheritance)。

4.1.2 对象

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

同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行 为是用可调用的方法定义的。

每个对象都保存着描述当前特征的信息。这就是对象的状态。

对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。对象状态的改变必须通过调 用方法实现(如果不经过方法调用就可以改变对象状态,只能说明封装性遭到了破坏)。

4.1.3 识别类

对于学习OOP的初学者来说常常会感觉无从下手。答案 是:首先从设计类开始,然后再往每个类中添加方法。

识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。

所谓“找名词与动词”原则只是一种经验,在创建类的时候,哪些名词和动 词是重要的完全取决于个人的开发经验。

4.1.4 类之间的关系

**依赖(dependence),即“uses-a”关系,是一种最明显的、最常见的关系。**例 如,**Order类使用Account类是因为Order对象需要访问Account对象查看信用状 态。**但是Item类不依赖于Account类,这是因为Item对象与客户账户无关。因此, 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。

应该尽可能地将相互依赖的类减至最少。如果类A不知道B的存在,它就不会关心B的 任何改变(这意味着B的改变不会导致A产生任何bug)。用软件工程的术语来说,就 是让类之间的耦合度最小。

聚合(aggregation),即“has-a”关系,是一种具体且易于理解的关系。例如, 一个Order对象包含一些Item对象。聚合关系意味着类A的对象包含类B的对象。

继承(inheritance),即“is-a”关系,是一种用于表示特殊与一般关系的。

例如,Rush Order类由Order类继承而来。在具有特殊性的RushOrder类中包含了一 些用于优先处理的特殊方法,以及一个计算运费的不同方法;而其他的方法,如添 加商品、生成账单等都是从Order类继承来的。一般而言,如果类A扩展类B,类A不 但包含从类B继承的方法,还会拥有一些额外的功能

很多程序员采用UML(Unified Modeling Language,统一建模语言)绘制类图, 用来描述类之间的关系。

很多程序员采用UML(Unified Modeling Language,统一建模语言)绘制类图, 用来描述类之间的关系。

4.2 使用预定义类

4.2.1 对象与对象变量

要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象应用方法。

在Java程序设计语言中,使用构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。

构造器的名字应该与类名相同。因此Date类的构造器名为Date。要想构造一个Date 对象,需要在构造器前面加上new操作符,如下所示:

new Date()

这个表达式构造了一个新对象。这个对象被初始化为当前的日期和时间。

如果需要的话,也可以将这个对象传递给一个方法:

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

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

String s = new Date().toString();

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

Date birthday = new Date();

在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。new 操作符的返回值也是一个引用。下列语句:

Date deadline = new Date();

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

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

deadline = null;
...
if(deadline != null){
	System.out.println(deadline);
}

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

birthday = null;
String s =  birthday.toString(); // runtime error!

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

4.2.2 Java类库中的LocalDate类

Date类的实例有一个状态,即特定的时间点。

类库设计者决定将保存时间与给时间点命名分开。所以标准Java类库分别包含了两 个类:一个是用来表示时间点的Date类;另一个是用来表示大家熟悉的日历表示法 的LocalDate类。

将时间与日历分开是一种很好的面向对象设计。通常,最好使用不同的类表示不同 的概念。

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

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

程序清单4-1给出了完整的程序。

程序清单4-1 CalendarTest/CalendarTest.java

import java.time.DayOfWeek;
import java.time.LocalDate;

/**
 * @version 1.51 2023-05-12
 * @author Maxwell Pan
 */

public class CalendarTest {

    public static void main(String[] args) {
        LocalDate date = LocalDate.now();

        int month = date.getDayOfMonth();
        int today = date.getDayOfMonth();

        date = date.minusDays(today - 1); // Set to start of month
        DayOfWeek weekday = date.getDayOfWeek();

        int value = weekday.getValue();     // 1 = Monday, ... 7 = Sunday

        System.out.println("Mon Tue Wed Thu Fri Sat Sun");

        for (int i = 0; i < value; i++) {
            System.out.println("   ");
        }
        while (date.getMonthValue() == month){
            System.out.printf("%3d", date.getDayOfMonth());
            if (date.getDayOfMonth() == today){
                System.out.print("*");
            }
            else {
                System.out.print(" ");
            }
            date = date.plusDays(1);
            if (date.getDayOfWeek().getValue() == 1) {
                System.out.println();
            }
        }
        if (date.getDayOfWeek().getValue() != 1){
            System.out.println();
        }
    }
}

API java.time.LocalDate 8 构造一个表示当前日期的对象。 构造一个表示给定日期的对象。 得到当前日期的年、月和日。 得到当前日期是星期几,作为DayOfWeek类的一个实例返回。调用getValue来得到 1~7之间的一个数,表示这是星期几,1表示星期一,7表示星期日。 生成当前日期之后或之前n天的日期。

4.3 用户自定义类

现在开始学习如何设计复杂应用程序所需要的各种主力类(workhorse class)。通常,这些类没有main方法,却有自己的实例域和实例方法。要想创建 一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。

4.3.1 Employee类

程序清单4-2 EmployeeTest/EmployeeTest.java

/**
 * This program tests the Employee class
 * @version 1.12.1 2023-05-12
 * @author Maxwell Pan
 */
public class EmployeeTest {

    public static void main(String[] args) {

        // fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Carl Craker", 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.getSalaray() + ",hireDay=" + e.getHireDay());
        }
    }
}
import java.time.LocalDate;

public class Employee {
    // instance fields
    private String name;
    private double salary;
    private LocalDate hireDay;

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

    public String getName() {
        return name;
    }

    public double getSalaray() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent){

        double raise = salary * byPercent / 100;
        salary += raise;
    }

}

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

// fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Carl Craker", 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%:

// raise everyone's salary by 5%
        for (Employee e: staff) {
            e.raiseSalary(5);
        }

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

// print out information about all Employee objects
        for (Employee e: staff) {
            System.out.println("name=" + e.getName() + ",salary=" + e.getSalaray() + ",hireDay=" + e.getHireDay());
        }

4.3.2 多个源文件的使用

一个源文件包含了两个类。许多程序员习惯于将每一个类存在一 个单独的源文件中。例如,将Employee类存放在文件Employee.java中,将 EmployeeTest类存放在文件EmployeeTest.java中。

4.3.3 剖析Employee类

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

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

    public String getName() {
        return name;
    }
    // methods 2
    public double getSalaray() {
        return salary;
    }
    // methods 3
    public LocalDate getHireDay() {
        return hireDay;
    }
    // methods 4
    public void raiseSalary(double byPercent){

        double raise = salary * byPercent / 100;
        salary += raise;
    }

这个类的所有方法都被标记为public。关键字public意味着任何类的任何方法都可 以调用这些方法(共有4种访问级别

接下来,需要注意在Employee类的实例中有三个实例域用来存放将要操作的数据:

// instance fields
    private String name;
    private double salary;
    private LocalDate hireDay;

关键字private确保只有Employee类自身的方法能够访问这些实例域,而其他类的 方法不能够读写这些域。

可以用public标记实例域,但这是一种极为不提倡的做法。public数据 域允许程序中的任何方法对其进行读取和修改。这就完全破坏了封装。

这里强烈建议将实例域标记为private。

最后,请注意,有两个实例域本身就是对象:name域是String类对象,hireDay域 是LocalDate类对象。这种情形十分常见:类通常包括类型属于某个类类型的实例 域。

4.3.4 从构造器开始

警告:请注意,不要在构造器中定义与实例域重名的局部变量。

4.3.5 隐式参数与显示参数

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

在每一个方法中,关键字this表示隐式参数。

raiseSalary方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在 方法名前的Employee类对象。第二个参数位于方法名后面括号中的数值,这是一个 显式(explicit)参数。

4.3.6 封装的优点

    public String getName() {
        return name;
    }
    // methods 2
    public double getSalaray() {
        return salary;
    }
    // methods 3
    public LocalDate getHireDay() {
        return hireDay;
    }

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

4.3.7 基于类的访问权限

方法可以访问所调用对象的私有数据。一个方法可以访问所属类的所有对象的私有数据

4.3.8 私有方法

在Java中,为了实现一个私有的方法,只需将关键字public改为private即可。 对于私有方法,如果改用其他方法实现相应的操作,则不必保留原有的方法。如果 数据的表达方式发生了变化,这个方法可能会变得难以实现,或者不再需要。然 而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调 用,可以将其删去。如果方法是公有的,就不能将其删去,因为其他的代码很可能 依赖它。

4.3.9 final 实例域

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

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

4.4 静态域与静态方法

main方法都被标记为static修饰符。

4.4.1 静态域

如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实 例域却都有自己的一份拷贝。

例如,假定需要给每一个雇员赋予唯一的标识码。这 里给Employee类添加一个实例域id和一个静态域nextId:

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

4.4.2 静态常量

静态变量使用得比较少,但静态常量却使用得比较多。

在Math类中定义了一个静态常量:

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

在程序中,可以采用Math.PI的形式获得这个常量。

如果关键字static被省略,PI就变成了Math类的一个实例域。需要通过Math类的对 象访问PI,并且每一个Math对象都有它自己的一份PI拷贝。

4.4.3 静态方法

静态方法是一种不能向对象实施操作的方法。

Math类的pow方法就是一个静态方法。表达式

Math.pow(x, a)

4.4.4 工厂方法

静态方法还有另外一种常见的用途。类似LocalDate和NumberFormat的类使用静态工厂方法(factory method)来构造对象。

4.4.5 main 方法

需要注意,不需要使用对象调用静态方法。同理,main方法也是一个静态方法。

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

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

/**
 * This program demonstrates static methods
 * @version 1.02 2023-05-12
 * @author Maxwell Pan
 */
public class StatucTest {

    public static void main(String[] args) {
        // fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Tom", 40000);
        staff[1] = new Employee("Dick", 60000);
        staff[2] = new Employee("Harry", 65000);

        // print out information about all Employee objects
        for (Employee e: staff) {
            e.setId();
            System.out.println("name=" + e.getName() + ",id=" + e.getId() + ",salary=" + e.getSalary());
        }

        int n = Employee.getNextId(); // calls static method
        System.out.println("Next available id=" + n);
    }
}

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;
        id = 0;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public int getId() {
        return id;
    }

    public void setId() {
        id = nextId;  // set id to next available id
        nextId++;
    }

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

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

4.5 方法参数

按值调用(call by value)表示方法接收的是调用者提供的值。而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。

Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个 拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。

程序清单4-4 ParamTest/ParamTest.java

/**
 * This program demonstrate parameter passing in Java
 * @version 1.00 2000-01-27
 * @author Maxwell Pan
 */
public class ParamTest {
    public static void main(String[] args) {
        /**
         * Test 1 : Methods can't modify numeric parameters
         */
        System.out.println("Testing tripleValue:");
        double percent = 10;
        System.out.println("Before: percent=" + percent);
        tripleValue(percent);
        System.out.println("After: percent=" + percent);

        /**
         * Test 2: Methods can change the state of object parameters
         */
        System.out.println("\\nTesting tripleSalary:");
        Employee harry = new Employee("Harry", 50000);

        System.out.println("Before: salary=" + harry.getSalary());
        tripleSalary(harry);
        System.out.println("After: salary=" + harry.getSalary());

        /**
         * Test3: Methods can't attach new objects to object parameters
         */
        System.out.println("\\nTesting swap:");
        Employee a = new Employee("Alice", 70000);
        Employee b = new Employee("Bob", 60000);

        System.out.println("Before: a=" + a.getName());
        System.out.println("Before: b=" + b.getName());
        swap(a,b);
        System.out.println("After: a=" + a.getName());
        System.out.println("After: b=" + b.getName());
    }

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

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

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

class Employee{
    // simplified Employee class
    private String name;
    private double salary;

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

    public String getName() {
        return name;
    }

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

4.6 对象构造

4.6.1 重载

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

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

4.6.2 默认域初始化

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

4.6.3 无参数的构造器

4.6.4 显式域初始化

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

4.6.5 参数名

还一种常用的技巧,它基于这样的事实:参数变量用同样的名字将实例域屏蔽起 来。

4.6.6 调用另一个构造器

关键字this引用方法的隐式参数。然而,这个关键字还有另外一个含义。

4.6.7 初始化块

实际上,Java还有第三种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。

程序清单4-5中的程序展示了本节讨论的很多特性:

程序清单4-5 ConstructorTest/ConstructorTest.java

import java.util.Random;

/**
 * This program demonstrate object construction.
 * @version 1.02 2023-05-13
 * @author Maxwell Pan
 */

public class ConstructorTest {

    public static void main(String[] args) {

        // fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Harry", 40000);
        staff[1] = new Employee(60000);
        staff[2] = new Employee();

        // print out information about all Employee objects
        for (Employee e: staff
             ) {
            System.out.println("name=" + e.getName() + ",id=" + e.getId() + ",salary=" + e.getSalary());
        }
    }

}

class Employee{

    private static int nextId;

    private int id;
    private String name = "";// instance field initialization
    private double salary;

    // static initialization block

    static {
        Random generator = new Random();
        // set nextId to a random number between 0 and 999
        nextId = generator.nextInt(1000);
    }

    // object initialization block
    {
        id = nextId;
        nextId++;
    }

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

    public Employee(double s)
    {
        // calls the Employee(String,double) constructor
        this("Employee #" + nextId, s);
    }

    // the default constructor
    public Employee()
    {
        // name initialized to "" --see above
        // salary not explicitly set - initialized to 0
        // id initialized in initialization block
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }
}

java.util.Random 1.0 构造一个新的随机数生成器。 返回一个0~n-1之间的随机数。

4.6.8 对象析构与finalize方法

由于Java有自动的垃圾回收器,不需要人工回收内存,所以Java不支持析构器。

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

4.7 包

Java允许使用包(package)将类组织起来。借助于包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。

4.7.1 类的导入

标准的Java类库分布在多个包中,包括java.lang、java.util和java.net等。标 准的Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组 织包。所有标准的Java包都处于java和javax包层次中。 使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了 Employee类。只要将这些类放置在不同的包中,就不会产生冲突。

更简单且更常用的方式是使用import语句。import语句是一种引用 包含在包中的类的简明描述。一旦使用了import语句,在使用类时,就不必写出包 的全名了。

可以使用import语句导入一个特定的类或者整个包。import语句应该位于源文件的 顶部(但位于package语句的后面)。

Import语句的唯一的好处是简捷。可以使用简短的名字而不是完整的包名来引用一 个类。例如,在import java.util.*(或import java.util.Date)语句之后, 可以仅仅用Date引用java.util.Date类

4.7.2 静态导入

import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。

4.7.3 将类放入包中

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

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

import com.horstmann.corejava.Employee;

// The Employee class is defined in that package
import static java.lang.System.*;

/**
 * This program demonstates the use of packages
 * @version 1.12 2023-05-13
 * @author Maxwell Pan
 */

public class PackageTest {
    public static void main(String[] args) {
        // because of the import statement, we don't have to use
        // com.horstman.corejava.Employee here
        Employee harry = new Employee("Harry Hacker", 5000, 1989, 10, 1);

        harry.raiseSalary(5);

        // because of the static import statement, we don't have to use System.out here
        out.println("name=" + harry.getName() + ",salary=" + harry.getSalary());
    }
}
package com.horstmann.corejava;

/**
 * the classes in this file are part of this package
 *
 */

import java.time.LocalDate;

/**
 * @version 1.12 2023-05-13
 * @author Maxwell Pan
 */

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

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        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;
    }
}

4.7.4 包作用域

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

4.8 类路径

类存储在文件系统的子目录中。类的路径必须与包名匹配。

类文件也可以存储在JAR(Java归档)文件中。在一个JAR文件中,可以包含 多个压缩形式的类文件和子目录,这样既可以节省又可以改善性能。在程序中用到 第三方(third-party)的库文件时,通常会给出一个或多个需要包含的JAR文件。

4.8.1 设置类路径

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

4.9 文档注释

JDK包含一个很有用的工具,叫做javadoc,它可以由源文件生成一个HTML文档。事 实上,在第3章讲述的联机API文档就是通过对标准Java类库的源代码运行javadoc 生成的

4.9.1 注释的插入

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

应该为上面几部分编写注释。注释应该放置在所描述特性的前面。注释以/开始, 并以/结束。 每个/**.../文档注释在标记之后紧跟着自由格式文本(free-form text)。标 记由@开始,如@author或@param。

自由格式文本的第一句应该是一个概要性的句子。javadoc实用程序自动地将这些 句子抽取出来形成概要页。

4.9.2 类注释

类注释必须放在import语句之后,类定义之前。

4.9.3 方法注释

每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记: 这个标记将对当前方法的“param”(参数)部分添加一个条目。这个描述可以占据多行,并可以使用HTML标记。一个方法的所有@param标记必须放在一起。 这个标记将对当前方法添加“return”(返回)部分。这个描述可以跨越多行,并可以使用HTML标记。

4.9.4 域注释

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

/*
* The "Hearts" card suit
*/
public static final int HEARTS = 1;

4.9.5 通用注释

下面的标记可以用在类文档的注释中。 这个标记将产生一个“author”(作者)条目。可以使用多个@author标记,每个 @author标记对应一个作者。 这个标记将产生一个“version”(版本)条目。这里的文本可以是对当前版本的 任何描述。 下面的标记可以用于所有的文档注释中。 这个标记将产生一个“since”(始于)条目。这里的text可以是对引入特性的版 本描述。例如,@since version 1.7.1。 这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建 议。例如, @deprecated Use

setVisible(true)

instead 通过@see和@link标记,可以使用超级链接,链接到javadoc文档的相关部分或外部 文档。

4.9.6 包与概述注释

可以直接将类、方法和变量的注释放置在Java源文件中,只要用/**...*/文档注释 界定就可以了。但是,要想产生包注释,就需要在每一个包目录中添加一个单独的 文件。可以有如下两个选择: 1)提供一个以package.html命名的HTML文件。在标记...之间的 所有文本都会被抽取出来。

2)提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始 的以/*/界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代 码或注释。

4.9.7 注释的抽取

4.10 类设计技巧

简单地介绍几点技巧。应用这些技巧可以使得设计出来的类更具有OOP的专业水准。

1.一定要保证数据私有

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

2.一定要对数据初始化

Java不对局部变量进行初始化,但是会对对象的实例域进行初始化。最好不要依赖 于系统的默认值,而是应该显式地初始化所有的数据,具体的初始化方式可以是提 供默认值,也可以是在所有构造器中设置默认值。

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

用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且 易于修改。

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

或许,需要获得或设置雇员的薪金。而一旦构造了雇员对象,就应该禁止更改雇用 日期,并且在对象中,常常包含一些不希望别人获得或设置的实例域,例如,在 Address类中,存放州缩写的数组。

5.将职责过多的类进行分解

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

对于方法来说,习惯是访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。

命名类名的良好习惯是采用一个名词(Order)、前面有形容词修饰的名词 (RushOrder)或动名词(有“-ing”后缀)修饰名词

7.优先使用不可变的类

LocalDate类以及java.time包中的其他类是不可变的——没有方法能修改对象的 状态。类似plusDays的方法并不是更改对象,而是返回状态已修改的新对象。

你可能感兴趣的:(JAVA,java)