Java核心卷1知识点整理——第四章 对象与类(4.1-4.3)

4 篇文章 0 订阅
3 篇文章 0 订阅
4.1 面向对象设计概述
4.1.1 类

Java所有编写的代码都位于某个类的内部。

OOP的一个原则是封装(encapsulation),封装就是将数据和方法组合在一个类中,并且对使用者隐藏数据的实现方式,不让使用者具有直接访问数据的权限,只能使用类中的方法对数据进行操作。

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

Java中,所有的类都源自于一个超类Object。

实例域:对象中的数据

方法:操纵数据的过程

状态:每个类的实例的一组特定的实例域值的集合

4.1.2 对象

对象的三个特性:

  • 对象的行为(behavior)
  • 对象的状态(state)
  • 对象标识(identity)

对象的行为是通过可调用的方法所定义的。

对象的状态是保存该对象描述当前特征的数据信息。状态是可以随着时间改变的,但是不会自发改变,必须通过调用方法来改变对象的状态。(如果不经过方法调用就可以改变对象状态,说明封装性遭到了破坏)

每个对象都具有唯一的身份,即使两个对象的状态完全一样,它们的身份也是不同的。

每个对象的身份标识永远是不同的,状态常常也存在差异。

4.1.4 类之间的关系

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

  • 依赖(“uses-a”)——一个类需要操纵另一个类的对象
  • 聚合(“has-a”)——一个类的对象包含另一个类的对象
  • 继承(“is-a”)——一个类扩展了另一个类
4.2 使用预定义类
4.2.1 对象与对象变量

使用对象,使用***构造器(constructor)***构造新实例。构造器是一种特殊的方法,用来构造并初始化对象,然后才可以对对象应用方法。

构造器名字和类名必须相同。构造对象在构造器前面加一个new操作符,如下:

new Date()

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

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

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

或者,也可以将一个方法应用于刚刚创建的对象Date类中有一个toString方法。这个方法将返回日期的字符串描述。

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

这两个例子中,构造的对象只用了一次。如果希望构造的对象多次使用,可以将对象存放在一个变量中

Date birthday = new Date();

在这里插入图片描述

定义一个对象变量,需要进行初始化才可以使用。否则会产生编译错误。

Date deadline; // deadline doesn't refer to any object
s = deadline.toString(); // not yet

初始化有两种选择,一种是使用新构造的对象初始化这个变量

deadline = new Date();

另一种是让这个变量引用一个已存在的对象

deadline = birthday;

现在两个变量引用同一个对象。

在这里插入图片描述

一个对象变量并没有包含一个对象,只是引用一个对象。

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

Date deadline = new Date();

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

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

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

局部变量不会自动初始化为null,而必须通过调用new或将它们设置为null进行初始化。(原因:为什么Java中全局变量不一定初始化,局部变量必须初始化?

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

更改器方法:修改对象的方法

访问器方法:只访问对象而不修改对象的方法

使用LocalDate类打印当前月的日历,并且在当前的日后面用*标记

Class CalendarTest {
  public static void main(String[] args) {
    LocalDate date = LocalDate.now();
    int month = date.getMonthValue();
    int today = date.getDayOfMonth();
    
    date = date.minusDays(today-1); // 将日期设置为本月的第一天
    int value = date.getDayOfWeek().getValue();
    for(int i = 1; i < value; i++) {
      System.out.print("    ");
    }
    // 不论一个月有多少天,只要判断当前日期还在本月内即可
    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(); // 如果到了新的一周就换行
    }
  }
}

该示例程序的重点是展示如何使用一个类的接口来完成任务,并无须了解实现细节。

4.3 用户自定义类

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

4.3.1 Employee类

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

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

下面是一个非常简单的Employee类:

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类的具体使用:

public class EmployeeTest {
  public static void main(String[] args) {
    // 创建一个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", 4000, 1980, 3, 15);
    
    // 给每个员工涨薪5%
    for(Employee e: staff) {
      e.raiseSalary(5);
    }
    
    // 打印所有雇员的信息
    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;
  }
}

一个源文件中只能有一个公有类,但可以有任意数目的非公有类。源文件的文件名必须和公有类的类名一致。

当编译这段代码的时候,编译器会在目录下创建两个文件:EmployeeTest.classEmployee.class

将程序中包含main方法的类名提供给字节码解释器,以便启动这个程序:

java EmployeeTest

4.3.2 多个源文件的使用

在上面的代码中,一个源文件包含了两个类。通常程序员习惯将每一个类存在一个单独的源文件中。例如,将Employee类存放在Employee.java中,将EmployeeTest类存放在EmployeeTest.java中。

如果这样组织文件,可以有两种编译程序的方法。

一种是使用通配符调用Java编译器:

javac Employee*.java

所有与通配符匹配的源文件都将被编译成类文件。

或者:

javac EmployeeTest.java

第二种方式,并没有显式地编译Employee.java。因为,当Java编译器发现EmployeeTest.java使用了Employee类时,会查找名为Employee.class的文件。如果没有找到,就会自动搜索Employee.java,并且进行编译。更重要的是,如果Employee.java的版本比已有的Employee.class 文件版本更新,Java编译器会自动重新编译这个文件。

4.3.3 剖析Employee类

先看这个类的方法:

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 意味着任何类的任何方法都可以调用这些方法

接下来看这个类的实例域:

private String name;
private double salary;
private LocalDate hireDay;

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

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

4.3.4 构造器

Employee类的构造器:

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

可以看到,构造器与类同名。在构造Employee类的对象时,构造器会运行,将实例域初始化为所希望的状态。

例如,使用下面这条代码创建Employee类实例时:

new Employee("James Bond", 100000, 1950, 1, 1)

将会把实例域设置为:

name = "James Bond";
salary = 100000;
hireDay = LocalDate.of(1950, 1, 1); // 1950年1月1日

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

james.Employee("James Bond", 250000, 1950, 1, 1) // Error

这样做将会产生编译错误。

需要记住:

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

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

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

这个构造器声明了局部变量 namesalary。这些变量只能在构造器内部访问。这些变量屏蔽了同名的实例域。

4.3.5 隐式参数与显式参数

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

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

将调用这个方法的对象的salary实例域设为新值。例如:

number007.raiseSalary(5);

这个调用将执行以下指令:

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

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

在每一个方法中,关键字this表示隐式参数,可以用以下方式编写raiseSalary方法:

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

这样可以将实例域和局部变量明显区分开。

4.3.6 封装的优点

最后,再看一下getNamegetSalarygetHireDay方法:

public String getName() {
  return name;
}
public double getSalary() {
  return salary;
}
public LocalDate getHireDay() {
  return hireDay;
}

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

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

虽然salary不是只读域,但是它只能通过raiseSalary方法进行修改。如果一旦这个域值出现了错误,只要调试这个方法就可以了。如果salarypublic的,那么破坏这个域值的捣乱者可能出现在任何地方。

如果需要获得或者设置实例域的值,需要提供下面三项内容:

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

这样做的好处:

  • 可以改变内部的实现,除了该类的方法,而不会影响其他代码。例如,将存储名字改为:

    String firstName

    String lastName

    那么getName方法可以改为返回 firstName+ " " + lastName

    对于这点改变,程序的其他部分是完全不可见的。

  • 在更改器方法中可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。例如,setSalary方法可以检查薪金是否小于0。

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

class Employee {
  private Date hireDay;
  ...
  public Date getHireDay() {
    return hireDay(); // Bad!!
  }
}

LocalDate 类没有更改器方法, 与之不同, Date 类有一个更改器方法 setTime, 可以在这里设置毫秒数。

Date对象是可变的,这一点破坏了封装性。

如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone )。对象 clone 是指存放在另一个位置上的对象副本。 下面是修改后的代码:

class Employee {
  ...
  public Date getHireDay() {
    return (Date) hireDay.clone(); // Ok
  }
}
4.3.7 基于类的访问权限

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

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

典型的调用方式:

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

boss是Employee类对象,而Employee类的方法可以访问Employee类的任何一个对象的私有域

4.3.8 私有方法

尽管绝大多数方法都被设计为公有的,但在某些特殊情况下,也可能将它们设计为私有的。有时,可能希望将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,最好将这样的方法设计为 private 的。

4.3.9 final实例域

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

final修饰符大都应用于**基本(primitive)类型域,或不可变(immutable)**类域

对于可变的类, 使用final修饰符可能会对读者造成混乱。例如

private final StringBuilder evaluations;

Employee构造器中会初始化为

evaluations = new StringBuilder();

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

public void giveGoldStar() {
    evaluations.append(LocalDate.now() + ": Gold star! \n");
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值