JAVA学习笔记——面向对象编程:类与对象1

简介

面向对象程序设计(Object Orientied Programming, OOP) 是当今主流的程序设计范型,它已经取代了 20 世纪 70 年代的 “结构化” 过程化程序设计开发技术。Java 是完全面向对象的,必须熟悉 OOP 才能够编写 Java 程序。

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

对于一些规模较小的问题,将其分解为过程的开发方式比较理想。而面向对象更加适用于解决规模较大的问题。要想实现一个简单的 Web 浏览器可能需要大约 2000 个过程,这些过程可能需要对一组全局数据进行操作。采用面向对象的设计风格,可能只需要大约 100 个类,每个类平均包含 20 个方法(如图1所示)后者更易于程序员掌握,也容易找到 bug 假设给定对象的数据出错了,在访问过这个数据项的 20 个方法中查找错误要比在 2000 个过程中查找容易得多。

OOP

图1 面向过程与面向对象的程序设计对比

类与对象

基本概念

”表示一系列具有相同属性和行为的事物的抽象,例如:人类、书类;而“对象”表示属于某一个“类”的具体的事物,又称为“实例”,例如:小明属于人类、《射雕英雄传》属于书类······

  • “类”规定了它的对象所具有的“属性”(实例域)和“方法”(操纵数据的过程)。
  • “对象”被实例化后,它具有一组自己特有的实例域值,这组值的集合称为该对象的“状态”。
  • 不同的对象可以属于同一个类,它们的“状态”可能不相同,但是却具有相同的“方法”,这些方法也只能操纵各自的“实例域”。

类与类之间的关系

  • 依赖 (use):如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。例:小明(人类)看《射雕英雄传》(书类)。
  • 聚合 (has):聚合关系意味着类 A 的对象包含类 B 的对象。例:XX学校(学校类)有体育馆(体育馆类)。
  • 继承 (is):是一种用于表示特殊与一般关系的。例:小明(人类)是哺乳动物(哺乳动物类)。

预定义类与自定义类

只有先将“类”实例化成“对象”后,才能对“对象”进行操作。在 Java 程序设计语言中,使用构造器 (constructor) 构造新实例。构造器是一种特殊的方法,用来构造并初始化对象,“类”的构造器与其类名相同。

Date birthday = new Date();  // birthday 引用一个 Date 对象
String s = birthday.toString();
Date deadline;  // deadline 为 null
deadline = birthday;  // deadline 和 birthday 引用同一个对象

在上例中,一共构造了 1 个 Date 对象。第 3 行定义了一个 Date 类的对象变量 deadline,但它没有引用任何对象,因此为 null。一般来说,用 new <类名>(); 构造新的对象,注意区分对象变量对象的区别。

对象与对象变量

图2 引用同一个对象的对象变量

预定义类(Date 和 LocalDate)

对于 Java 已封装的预定义类,按照根据文档说明构造对象即可。

关于日期,标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。

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

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

LocalDate.now();  // 根据当前时间构造一个 LocalDate 对象
LocalDate newYearsEve = LocalDate.of(1999, 12, 31);  // 构造特定时间的 LocalDate 对象
int year = newYearsEve.getYear(); // 1999
int month = newYearsEve.getMonthValue(); // 12
int day = newYearsEve.getDayOfMonth(); // 31

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

LocalDate aThousandDaysLater = newYearsEve.piusDays(1000):
year = aThousandDaysLater.getYear();  // 2002
month = aThousandDaysLater.getMonthValue();  // 09
day = aThousandDaysLater.getDayOfMonth();  // 26

注意:类库设计者意识到应当单独提供类来处理日历,不过在此之前这些方法已经是 Date 类的一部分了。Java 1.1 中引入较早的一组日历类时,Date 方法被标为废弃不用。虽然
仍然可以在程序中使用这些方法,不过如果这样做,编译时会出现警告。最好还是不要使用这些废弃不用的方法,因为将来的某个类库版本很有可能将它们完全删除。

  • 访问器方法:只访问对象而不修改对象的方法。
  • 更改器方法:访问对象后,同时更改对象状态的方法。

LocalDate 类常用方法

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

自定义类

在 Java 中,最简单的类定义形式为:

class ClassName
{
	field1;
	field2;
	......
	constructor1;
	constructor2;
	......
	method1;
	method2;
	......
}

例:Employee 类

import java.time.*;

/**
 * This program tests the Employee class.
 * @version 1.12 2015-05-08
 * @author Cay Horstmann
 */
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 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)
   {
      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 byPercent)
   {
      double raise = salary * byPercent / 100;
      salary += raise;
   }
}

上例中,我们自定义了一个 Employee 类,一共包括 3 个实例域、1 个构造器和 4 个方法,接下来将逐一分析。

构造器

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

类的构造器和类名同名,在构造 Employee 类的对象时,构造器会运行,以便将实例域初始化为所希望的状态,并且没有返回值。

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

注意:不能在构造器中使用与实例域相同变量名的参数,否则会报错。

方法

public LocalDate getHireDay()
{
   return hireDay;
}

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

方法用于操作对象以及存取它们的实例域,主要由 5 个部分组成:

  • 访问权限:上例中的 public
  • 返回值类型:方法运行结束后,返回数据的类型,使用 return 关键字
  • 方法名:方法的名称
  • 参数:括号中的变量,如 raiseSalary(double byPercent)
  • 方法的具体操作:方法的主体部分

返回值类型:方法的返回值类型可以是任意类型,如 intdouble 等,甚至可以是各种类,如 LocalDate。而关键字 void 表示没有返回值,则方法体内可以不出现 return,或直接使用 return;,表示没有任何返回值,方法运行结束。

参数:一个类方法的参数一共有两种:一种是显式参数,另一种是隐式参数。上例中,raiseSalary 的显式参数是double byPercent,隐式参数是 this

每个类方法都会有 this 参数,它引用的是调用该方法的对象本身。好处在于在方法内部,便于访问对象的实例域和其他方法,同时还能将局部变量和对象的实例域明显地区分开。

访问权限与封装性

  • 关键字 public:表示任何类的任何方法都可以调用该方法或者访问该实例域。
  • 关键字 private:确保了只有类自身的方法能够访问对象的实例域, 而其他类的方法不能够读写这些域。

类的实例域一般设置为 private,再单独编写访问实例域的各种方法,设置为 public。这样做的好处是,将类的实例域私有化,不能通过外部任意修改,最大程度地限制了外部对实例域的操作,避免了由外部任意修改实例域而产生的意外的错误,同时也体现了类的封装性的特点。

封装(encapsulation,有时称为数据隐藏)是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。

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

封装性的好处

  • 首先,可以改变内部实现,除了该类的方法之外,不会影响其他代码。
  • 更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。

当然,对于一些特定的方法,也同样可以根据需要设置为 private 权限,实现对外部封闭。

注意:不要编写返回引用可变对象的访问器方法。在 Employee 类中就违反了这个设计原则,其中的 getHireDay 方法返回了一个 Date 类对象。

Employee harry = ...;
Date d = harry.getHireDay();
double tenYearsInMilliSeconds = 10 * 365.25 * 24 * 60 * 60 * 1000;
d.setTime(d.getTime() - (long) tenYearsInMilliSeconds);

运行这段程序就会产生问题!因为外部定义的 dharry.getHireDay() 引用的是同一个对象,因此当在外部修改 d 的时候,同时也修改了 harry 的实例域,从而破坏了类的封装性。正确的方式应该是返回该私有实例域对象的克隆(clone)。

public Date getHireDayO
{
	return (Date) hireDay.clone(); // Ok
}

final 实例域

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

注意final 表示变量引用的对象一旦完成初始化后,该变量不能再引用其他的对象,但是已经引用的变量本身却是可以改变的。


参考资料

  1. 《Java核心技术 卷1 基础知识》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值