面向对象程序设计(object oriented programming,opp)已经是当前程序设计的主流。

本文章讲述Java对象与类相关知识。

在OPP中,不必关心对象的具体实现,只要能满足用户的需求即可。

在OPP中 ,数据被放在第一位

在实现OPP时,不同于面向过程程序设计要从编写main函数开始,而是应该从设计类开始。

关于OPP,相信参与过中大型系统开发的同学都有自己的理解,这里就不再赘述。

本篇文章核心的内容为 Java的OPP,主角为Java而非opp。

类(class) —–> 由类构造对象 (construct) ——–> 构造结果(新对象) : 类的实例 (instance)

类的特点

继承 、封装 、多态。

  • 继承 (inheritance): 类可以通过扩展另一个类来建立,扩展后的新类具有所拓展类的全部方法和属性。(Java所有的类都源于超类Object)

  • 封装(encapsulation,数据隐藏):将数据和方法组合在一个包中,对使用者隐藏数据实现方式。对象中的数据称为实例域(instance fields),每个对象都有一组实例域值,这些值的集合就是这个对象当前的状态(state)。

    注意 :实现封装的关键在于绝对不能让类中的方法直接访问其他类的实例域。

  • 多态:

类之间的关系

最常见的类之间的关系又如下几种,注意区分:

  • 依赖(dopendence,uses-a)。如果b类的方法会操作a类的对象,那么就说b依赖a。

注意:我们应该尽可能的将相互依赖的类减至最小,以降低耦合度。

  • 聚合(aggregation,has-a)。又叫关联。如果A类的对象包含B类的对象,那么就说他们聚合。
  • 继承(inheritance,is-a)。暂先不多说,就是一种特殊与一般的关系,Java关键字extant。

使用现有类

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

  • 构造对象:使用构造器(constructor)创造新实例。构造器的名字与类名同名。

  • 对象与对象变量:

    Date deadline = new Data()

    new Data()表达式构造了一个新对象,并且它的值是新对象的引用。

    而deadline是一个变量,储存了new Data的值(值是引用)。

    一定要认识到:一个变量对象并没有实际包含一个对象,而仅仅是引用一个对象。

    关于这部分的内容,可以参考本博客的文章《浅谈js不同数据类型在拷贝与传参时的不同》,核心思想差不多,都是深浅拷贝。

    注意 : 所有的Java对象都储存在堆中。

    因此在Java中,必须使用clone方法来获得对象的完整拷贝。

自定义类

这里涉及到的内容有:

  • 构造器
  • 隐式参数与显式参数
  • 封装的优点
  • 基于类的访问权限
  • 私有方法
  • Final实例域

直接用一个简单的样例代码来讲述自定义类的使用细节吧 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import java.util.*;
/**
* 注意这里的public ,说明是公有类。只能有一个公有类。
* */
public class EmployeeTest
{
/**
*如果执行java EmloyeeTest,就会执行这个main函数。
*而如果执行javac Employee*.java 或 javac EmployeeTest.java 也都可以编译源程序。
*/
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());
}
}
/**
* 可以有n个私有类。
*/

class Employee
{
//构造器
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month - 1, day);
// GregorianCalendar uses 0 for January
hireDay = calendar.getTime();
}

public String getName()
{
return name;
}

public double getSalary()
{
return salary;
}

// 其实这里不该这么做,直接返回了对象,破坏了封装。
public Date getHireDay()
{
return hireDay;
}

public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
public void setId()
{
id = nextId;
nextId ++;
}
//静态方法的使用场景之一:只访问自身类内部的静态域
public static int getNextId(){
return nextId; //返回了静态域
}
/**
* 实例域。
* 实例域推荐使用private,public会破坏封装性。
* 另外,这里的name和hireDay,他们本身就是对象的实例域。
* 即 :类通常包括类型属于某个类的实例域。
* */
//final实例域,不可变(无set方法)
private final String name; //这里就是一个静态常量,如果设计成public也问题不大(无set)
private double salary;
private Date hireDay;
private int Id ;
//static关键字
private static int nextId = 1
}

  1. 首先是构造器。构造器的特点很简单:

    1. 构造器与类同名。
    2. 每个类可以有一个以上的构造器。(重载)
    3. 构造器可以有任意参数(0或多个)。
    4. 构造器没有返回值。
    5. 构造器总是和new一起使用。(也就是说不构造器总是伴随着new操作符的执行而被调用,而不能对一个已经存在的对象调用构造器来重新设置实例域。)
  2. 然后是隐式参数与显式参数

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

    比如,可以像下面这样调用:

    1
    number001.raiseSalary(5);

    这个调用语句中:

    number001是隐式参数。

    5是显式参数。

    其实在每个方法中,关键字this就是隐式参数。谁调用,this就指向谁。这个this也可以参考js中的this。

  3. 封装的优点。

    在上面的public class EmployeeTest代码中,可以看到:

    我们将name、salary、hireDay都标记为private,只有本类的方法可以读取。

    如果标记为public,会带来的缺点是 : 改变这个域值的方法可以出现在任何地方。

    因此强烈建议将域值的属性都设置为private,同时考虑到需要对域值进行获得或者设置,所以以下三个部分是必须的:

    1. 一个私有数据域。
    2. 一个公有的域访问器方法。
    3. 一个公有的域更改器方法。

    这样比直接提供public数据域复杂,但好处有:

    1. 可以改变内部实现,且除了该类的方法之外,不会改变别的方法。

    2. 更改器方法可以执行错误检查,然而直接对域进行赋值不会进行这些处理。

      ​ 注意:如果要返回一个可变对象的引用,应该首先对其进行clone。

  4. Final实例域(静态常量)

    如上面的代码 private final String name;,可以将实例域定义为final,并且构建对象时必须初始化。

    final修饰符应该应用于基本数据(primitive)类型域或不可变(immutable)类域。

  5. 静态域与静态方法(static关键字)

    静态方法不能向对象实施操作的方法(可以理解为是没有this的方法)

    因为静态方法不能操作对象,所以不能在静态方法中访问实例域(没有this关键字自然没有实例域)。

    但是,静态方法可以访问自身类中的静态域(比如下面的nextId)

    例如,在上面的代码中,同样也是 private static int nextId = 1;

    这样的话,如果new 10个Employee对象,每个Employee都有自己的id,但是10个Employee共享一个nextId。

    静态方法的使用场景:

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

    main方法也是静态方法。不对任何对象进行操作。(事实上在启动程序时还没有任何一个对象)

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

  6. 方法参数

    Java程序设计语言总是采用值调用。

    也就是说,方法得到的是参数值的一个拷贝。然后考虑到方法参数有基本数据类型(数字,bool)和对象引用两种类型,因此也会存在深浅拷贝的问题,和js里面一样,不多说,方法参数的使用总结一下就是下面三点:

    1. 一个方法不能修改一个基本数据类型的参数(数值和bool)
    2. 一个方法可以修改一个对象参数的状态。
    3. 一个方法不能实现让对象参数引用一个新的对象(就是和原值引用同一个对象)

类设计技巧

  1. 一定将数据设计成私有(不要破坏封装性)
  2. 一定要对数据初始化
  3. 不要在类中使用太多基本数据类型(鼓励用其他类代替多个相关的基本数据类型的使用,是程序易读易改)
  4. 不是所有的域都需要独立的域访问器和域更改器(set get非必须)
  5. 使用标准格式定义类 :
    • 类的内容 :公有访问特性部分 —-> 包作用域访问特性部分 ——–> 私有访问特性部分
    • 每个部分中 : 实例方法 ——–> 静态方法 ——–> 实例域 ——–> 静态域
  6. 将职责过多的类进行分解。
  7. 类名和方法名应该体现他们的职责。

对象(的构造)

要OPP,对于对象的三个特性一定要清楚:

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

然后来基于此谈谈对象构造。

这里涉及到的知识有 :重载,默认域初始化域默认构造器&显示域初始化,参数名,调用另一个构造器,初始化块,对象析构与finalize方法。

重载

重载也没啥好说的,就是方法的重载 。

重载解析 :多个方法参数名字相同参数不同 ——-> 产生重载 ——> 编译器匹配方法 ( 若匹配失败 ——–> 编译错误)

Java允许重载任何方法,因此同样包括构造器方法。

初始化域/构造器

如果构造器中没有显式的给域赋予值,域就会被赋予默认值 。各类型默认值分别为:

  • 数值:0
  • 布尔 :false
  • 对象 : null

尽量避免需要默认初始化域 !

默认构造器就是没有参数的构造器。

如果在编写类的时候没有写构造器,系统就会提供一个默认构造器。(当然,里面的值都是默认值)

显式初始化域 : 这里就是主动在构造对象时对他们的域值进行初始化。

由于类的构造器方法可以重载,所以可以采用多种形式设置类的实例域初始化状态。(参数写对就行)

可以在类定义中,直接将一个值(不一定是常量)赋给任何域。

1
2
3
4
5
6
7
class E{
...
stactic int newId(){
...
}
private String id = newId();
}

上面实在构造器中设置值,当然也可以在声明中初始化值

还可以在初始化块中初始化,但是一般没人这么做,不如直接放到构造器里初始化了。

总的来说,初始化数据的步骤是 :

  1. 所有数据域被初始化为默认值
  2. 按在类声明中的次序,依次执行所有初始化语句和初始化块。
  3. 如果构造器第一行调用了第二个构造器,就去执行第二个构造器
  4. 执行这个构造器主题

this关键字的用法

  1. 将参数变量用同样的名字将实例域屏蔽起来

    1
    2
    3
    4
    5
    6
    7
    8
    //编写很小的构造器时,参数通常要有一些含义,因此一些代码通常这样写
    public E(String aName){
    name = aName;
    }
    //用this关键字将参数变量用同样的名字将实例域屏蔽起来,可以这样写
    public E(String name){
    this.name = name; //this指向被构造对象
    }
  2. 调用另一个构造器

    关键字this引用方法的隐式参数,这是一层含义。

    还有一层含义 : 如果构造器的第一个语句形如this(…) ,那么它将调用同一个类下的另一个构造器。

    1
    2
    3
    4
    5
    public E(double s){
    //调用另一个构造器
    this("E #"+nextId,s); //调用new E(6000.0),E(double)构造器会调用E(String ,double)构造器
    nextId ++;
    }

    优点 : 对公共的构造器代码部分只编写一次即可。

    对象析构与finalize()

    在析构器中,最常见的操作就是回收分配给对象的储存空间。

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

    当然,有些对象会使用内存之外的其他资源(比如文件使用了系统资源另一个对象的句柄),那么这种资源不再需要时,将其回收就非常重要。

    因此有了finalize方法。

    可以为任何一个类添加finalize方法 ,这个方法将在垃圾回收器清除对象之前调用。

    在实际使用中,最好还是不要依赖finalize方法,因为不知道这个方法啥时候调用。