《Java核心技术》第四章 对象与类 学习笔记

第四章 对象与类

4.1 面向对象程序设计概述

Java是完全面向对象的,必须熟悉OOP才能够编写Java程序。
面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
在OOP中,不必关心对象的具体实现,只要能够满足用户的需求即可。

4.1.1 类

类(class)是构造对象的模板或蓝图。
由类构造(construct)对象的过程称为创建类的实例(instance)。

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

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

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

4.1.2 对象

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

  • 对象的行为(behavior)——可以对对象施加哪些操作,或可以对对象施加哪些方法?

  • 对象的状态(state)——当施加那些方法时,对象如何响应?

  • 对象标识(identity)——如何辨别具有相同行为与状态的不同对象?

    同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法定义的。此外,每个对象都保存着描述当前特征的信息。这就是对象的状态。对象的状态可能会随着时间而发生改变,但这种改变不会是自发的。对象状态的改变必须通过调用方法实现(如果不经过方法调用就可以改变对象状态,只能说明封装性遭到了破坏)。但是,对象的状态并不能完全描述一个对象。每个对象都有一个唯一的身份(identity)。需要注意,作为一个类的实例,每个对象的标识永远是不同的,状态常常也存在着差异。

对象的这些关键特性在彼此之间相互影响着。

4.1.4 类之间的关系

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

  • 依赖(“uses-a”) 最明显的、最常见的关系。应该尽可能地将相互依赖的类减至最少。
  • 聚合(“has-a”)是一种具体且易于理解的关系,
  • 继承(“is-a”)用于表示特殊与一般关系的。

4.2 使用预定义类

4.2.1 对象与对象变量

要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象应用方法。
在Java程序设计语言中,使用构造器(constructor)构造新实例。构造器是一种特殊的方法,用来构造并初始化对象。

Date birthday = new Date();

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

 LocalDate newYear = LocalDate.of(2021,1,4);
 int year = newYear.getYear();
 int month = newYear.getMonthValue();
 int day = newYear.getYear();
 LocalDate daysLater = newYear.plusDays(1000);

只访问对象而不修改对象的方法有时称为访问器方法

4.3 用户自定义类

在这个示例程序中包含两个类:Employee类和带有public访问修饰符的EmployeeTest类。EmployeeTest类包含了main方法,其中使用了前面介绍的指令。
Employee类中含有一个构造器和4个方法。三个属性。

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

关键在于name是一个只读域。一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样来确保name域不会受到外界的破坏。

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

import java.time.LocalDate;

public class EmployeeTest {
    
    
    public static void main(String[] args) {
    
    
        // 构造三个雇员
        Employee[] staff = new Employee[3];
        staff[0] = new Employee("mic", 20000, 2021, 1, 4);
        staff[1] = new Employee("rtg", 41484, 2021, 1, 4);
        staff[2] = new Employee("mic", 85988, 2021, 1, 4);

        // 涨薪
        for (Employee e: staff)
            e.raiseSalary(90);

        // 打印
        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)
    {
    
    
        name = n;
        salary = s;
        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 percent)
    {
    
    
        double raise = salary * percent / 100;
        salary += raise;
    }
}

4.4 静态域与静态方法

4.4.1 静态域

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

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

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

4.4.2 静态常量

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

public static final double PI = 3.1415956;

4.4.3 静态方法

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

Math.pow(x, a);

在运算时,不使用任何Math对象。换句话说,没有隐式的参数。可以认为静态方法是没有this参数的方法(在一个非静态的方法中,this参数表示这个方法的隐式参数)。

Employee类的静态方法不能访问Id实例域,因为它不能操作对象。但是,静态方法可以访问自身类中的静态域。下面是使用这种静态方法的一个示例:

public static int getNextId()
{
    
    
	return nextId;  // return static field
}
// 可以通过类名来调用该方法
int n = Employee.getNextId();

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

  • 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。
  • 一个方法只需要访问类的静态域(例如:Employee.getNextId)。

4.4.4 工厂方法

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

NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
NumberFormat percentFormatter = NumberFormat.getPercentInstance();

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

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

4.5 方法参数

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

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

double percent = 10;
harry.raiseSalary(percent);
// 在方法调用之后,percent的值还是10。 因为raiseSalary函数传入的是10的一个拷贝

然而,方法参数共有两种类型:

  1. 基本数据类型(数字、布尔值)。
  2. 对象引用。
    一个方法不可能修改一个基本数据类型的参数。而对象引用作为参数就不同了,可以很容易地利用下面这个方法实现将一个雇员的薪金提高两倍的操作:
public static void tripleSalary(Employee x)
{
    
    
    x.raiseSalary(200);
}
Employee harry = new Employee("3",1,2,3,4);
tripleSalary(harry);

具体的执行过程为

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

4.6 对象构造

4.6.1 重载

有些类有多个构造器。例如,可以如下构造一个空的StringBuilder对象:或者,可以指定一个初始字符串:

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

这种特征叫做重载(overloading)。
如果多个方法(比如,StringBuilder构造器方法)有相同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。

Java允许重载任何方法,而不只是构造器方法。因此,要完整地描述一个方法,需要指出方法名以及参数类型。这叫做方法的签名(signature)

4.6.2 默认域初始化

如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。

4.6.3 无参数的构造器

很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时,其状态会设置为适当的默认值。
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。

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

4.6.5 参数名

它基于这样的事实:参数变量用同样的名字将实例域屏蔽起来。例如,如果将参数命名为salary, salary将引用这个参数,而不是实例域。但是,可以采用this. salary的形式访问实例域。回想一下,this指示隐式参数,也就是所构造的对象。下面是一个示例:

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

4.6.6 调用另一个构造器

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

public Employee(double s)
{
    
    
	// 调用另一个构造函数
    this("1",1,2,3,4);
    salary += s;
}

4.6.7 初始化块

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

  • 在构造器中设置值
  • 在声明中赋值
    实际上,Java还有第三种机制,称为初始化块(initialization block)。在一个类的声明中,可以包含多个代码块。只要构造类的对象,这些块就会被执行。
    在这个示例中,无论使用哪个构造器构造对象,id域都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
class Employee
{
    
    
    private static int nextId = 1;
    private int id;

    private String name;
    private double salary;
    private LocalDate hireDay;
    
    // 初始化块
    {
    
    
        id = nextId;
        nextId++;
    }

如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。将代码放在一个块中,并标记关键字static。下面是一个示例。其功能是将雇员ID的起始值赋予一个小于10000的随机整数。

static
{
    
    
    Random generator = new Random();
    nextId = generator.nextInt(10000);
}

在类第一次加载的时候,将会进行静态域的初始化。与实例域一样,除非将它们显式地设置成其他值,否则默认的初始值是0、false或null。所有的静态初始化语句以及静态初始化块都将依照类定义的顺序执行。

4.6.8 对象析构与finalize方法

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

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

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

@Override
protected void finalize() throws Throwable {
    
    
    nextId--;
    super.finalize();
}

4.7 包

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

标准的Java类库分布在多个包中,包括java.lang、java.util和java.net等。标准的Java包具有一个层次结构。如同硬盘的目录嵌套一样,也可以使用嵌套层次组织包。所有标准的Java包都处于java和javax包层次中。

**使用包的主要原因是确保类名的唯一性。**假如两个程序员不约而同地建立了Employee类。只要将这些类放置在不同的包中,就不会产生冲突。事实上,为了保证包名的绝对唯一性,Sun公司建议将公司的因特网域名(这显然是独一无二的)以逆序的形式作为包名,并且对于不同的项目使用不同的子包。

从编译器的角度来看,嵌套的包之间没有任何关系。

4.7.1 类的导入

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

import java.time.LocalDate;
import java.util.*;

import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。
如,如果在源文件的顶部,添加一条指令:就可以使用System类的静态方法和静态域,而不必加类名前缀:

import static java.lang.System.*;
out.println(age);

4.7.4 包作用域

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

4.8 类路径

在前面已经看到,类存储在文件系统的子目录中。类的路径必须与包名匹配。

4.10 类设计技巧

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

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

3.不要在类中使用过多的基本类型
就是说,用其他的类代替多个相关的基本类型的使用。这样会使类更加易于理解且易于修改。

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

6.类名和方法名要能够体现它们的职责
与变量应该有一个能够反映其含义的名字一样,类也应该如此(在标准类库中,也存在着一些含义不明确的例子,如:Date类实际上是一个用于描述时间的类)。命名类名的良好习惯是采用一个名词(Order)、前面有形容词修饰的名词(RushOrder)或动名词(有“-ing”后缀)修饰名词(例如,BillingAddress)。对于方法来说,习惯是访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。

7.优先使用不可变的类
LocalDate类以及java.time包中的其他类是不可变的——没有方法能修改对象的状态。类似plusDays的方法并不是更改对象,而是返回状态已修改的新对象。更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。因此,要尽可能让类是不可变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。

猜你喜欢

转载自blog.csdn.net/qq_17677907/article/details/112168691