Java 对象与类

面向对象程序

  面向对象程序设计(简称 OOPObject Oriented Programming ) 是当今主流的程序设计范型 。
  面向对象的程序由对象组成, 每个对象包含对用户公开的特定功能部分和隐藏的实现部分。
  面向对象包含以下 3 个最重要的特性:

  • 封装
  • 继承
  • 多态

  类 ( class ) 是构造对象的模板或蓝图。

  你可以将类想象成制作枪具的设计图, 将对象想象为枪的实体。

  由类构造(construct) 对象的过程称为创建类的实例(instance)。

对象

  Java 中的一切都被视为对象。
  尽管一切都看作对象,但操纵的标识符(变量)实际上是对象的一个“引用”(reference)。
  你可以将这一情形想象成在用遥控器(引用)操纵电视机(对象),只要握住这个遥控器,就能保持与电视机的连接。当你想改变频道或减少音量时,实际操纵的是遥控器(引用),再由遥控器来操纵电视机(对象)。若你想在房间里四处走走的同时仍能遥控电视机,那么只需携带遥控器(引用)而不是电视机(对象)。
  此外,即使没有电视机,遥控器亦可独立存在。也就是说,你必须拥有一个引用,但并不一定需要有一个对象与它关联。
  若想操纵一个词或句子,则可以创建一个String引用:

1
String s;

  但这里所创建的只是引用,并不是对象。
  若此时向s发送一个消息,就会返回一个运行时错误,因为此时s实际上没有与任何事务相关联(即没有电视机)。
  所以,一种安全的做法是:创建一个引用的同时便进行初始化:

1
String s = "abcd";

  当然,字符串对象有点特殊,它用带引号的文本进行初始化。通常对其他对象而言,一般是使用构造器来初始化

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

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

  同一个类的所有对象实例,由于支持相同的行为而具有家族式的相似性。对象的行为是用可调用的方法定义的。
  此外, 每个对象都保存着描述当前特征的信息,这就是对象的状态。
  对象的状态可能会随着时间而发生改变, 但这种改变不会是自发的。 对象状态的改变必须通过调用方法实现 (若不经过方法调用就可以改变对象状态, 只能说明封装性遭到了破坏 )。
  但是,对象的状态并不能完全描述一个对象,每个对象都有一个唯一的身份。
  例如, 在一个订单处理系统中,任何两个订单都存在着不同之处,即使所订购的货物完全相同也是如此。需要注意对象作为一个类的实例, 每个对象的标识永远是不同的,状态常常也存在着差异。
  对象的这些关键特性在彼此之间相互影响着。
  比如,对象的状态影响着它的行为(若一个订单“ 已送货” 或“ 已付款”, 就应该拒绝调用具有增删订单中条目的方法。反过来, 如果订单是“空的”, 即还没有加入预订的物品, 这个订单就不应该进入“已送货” 状态)

类间关系

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

  • 依赖(uses-a
  • 聚合(has-a
  • 继承(is-a

依赖

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

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

聚合

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

继承

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

  如人类对象继承动物对象,可以说人类对象继承动物对象,人类对象为子类,动物对象为父类。

:Java 中Object对象是任何对象的父类。

静态域和静态方法

  Java中的main方法都被标记为static修饰符,下面讨论一下。

静态域(变量)

  若将域(变量)定义为static , 则每个类中只有一个这样的域(变量)。而对于其他所有不是static修饰的实例域(变量),每一个对象都有自己的一份拷贝。
  换而言之,类变量是根据该类创建的所有对象所共有的,所有对象都共享这一个类变量。
  例如,对某个大学的学生而言:

1
2
3
4
public class Student{
public static String schoolName = "某个学校";
...
}

  对于每个创建的 Student 对象而言,都是同一个学校的,所以它们的学校名是一样的,即使没有一个学生对象,静态变量schoolName也存在。 它属于类, 而不属于任何独立的对象。
  因此,即使创建了 1000 个学生对象,也只有一个静态的类变量schoolName

静态常量

  静态变量使用得比较少, 但静态常量却使用得比较多。
  例如, 在 Java 自带的Math类中定义了一个静态常量:

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

  在程序中, 可以采用Math.PI的形式获得这个常量。
  若关键字 static 被省略,PI 就变成了 Math类的一个实例域(变量)。 需要通过 Math 类的对象访问 PI 并且每一个Math对象都有它自己的一份 PI 拷贝。

静态方法

  静态方法是一种不能向对象实施操作的方法。
  例如,Math类的 pow方法就是一静态方法。
  表达式Math.pow(x,a);将计算幂xa次方。
  在运算时,不使用任何 Math 对象,换句话说,没有隐式的参数,所以可认为静态方法是没有this参数的方法。

main 方法

  需要注意, 不需要创建对象来调用静态方法。例如, 不需要构造 Math 类对象就可以调用 Math.pow
  同理,main方法也是一个静态方法。main 方法不对任何对象进行操作。
  事实上,在启动程序时还没有任何一个对象。静态的main方法就将执行并创建程序所需要的对象。

方法参数

  首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语:

  • 按值调用:表示方法接收的是调用者提供的值
  • 按引用调用:表示方法接收的是调用者提供的变量地址

  一个方法可以修改传递引用所对应的变量值, 而不能修改传递值调用所对应的变量值。
  Java 程序设计语言总是采用按值调用
  换而言之, 方法得到的是所有参数值的一个拷贝!拷贝!!拷贝!!!,特别是, 方法不能修改传递给它的任何参数变量的内容。

  例如,考虑下面的一个方法,该方法试图将一个参数值增加至3倍:

1
2
3
4
// doesn't work
public static void tripieValue(double x) {
x = 3 * x;
}

  下面调用这个方法:

1
2
double percent = 10; 
tripieValue(percent) ;

  不过, 理想中的30并未出现,调用这个方法后,percent的值还是10
  为什么呢?
  下面对其执行过程具体分析一下:

  • x 被初始化为percent值的一个拷贝 (也就 是10
  • x被乘以3后等于30,但是percent的值仍为10(如图所示)
  • 该方法结束之后, 参数变量x不再使用

对值参数的修改没有保留下来
  然而, 方法参数共有两种类型:

  • 基本数据类型
  • 对象引用类型

  虽然一个方法不可能修改一个基本数据类型的参数,但若是用对象引用作为参数就不一样了,我们可以很容易地利用下面这个方法将一个雇员的薪金提高两倍:

1
2
3
4
5
6
7
8
// works
public static void tripieSalary(Employee x) {
x.raiseSa1ary(200);
}

// 当调用
Employee harry = new Employee();
tripieSalary(harry) ;

  具体的执行过程为:

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

对对象参数的修改保留了下来

  可以看到,实现一个改变对象参数状态的方法并不是一件难事。 理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
  所以有些程序员认为 Java 程序设计语言对对象采用的是引用调用,但实际上,这种理解是不对的
  下面给出一个反例来详细地阐述一下这个问题。
  首先, 编写一个交换两个雇员对象的方法:

1
2
3
4
5
public static void swap(Employee x, Employee y){
Employee temp = x;
x = y;
y = temp;
}

  若 Java 中对象采用的是按引用调用, 那么这个方法就应该能够实现交换数据的效果:

1
2
3
Employee a = new Employee("Alice", . . .); 
Employee b = new Employee("Bob", . . .);
swap(a, b);

  但是,方法并没有改变存储在变量ab中的对象引用。swap方法的参数xy被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝,即xy被交换了。
  最终,白费力气。 在方法结束时参数变量xy被丢弃了。 原来的变量 ab仍然引用这个方法调用之前所引用的对象,如下图:
交换对象参数的结果没有保留下来

  下面总结一下 Java 中方法参数的使用情况:

  • 一个方法不能修改一个基本数据类型的参数。
  • 一个方法可以改变一个对象参数的状态(变量)
  • 一个方法不能让对象参数引用一个新的对象

对象构造

重载

  方法重载:指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数

  方法重载原则如下:

  • 方法名必须相同
  • 参数类型或个数必须不同

:在重载中,方法的返回类型、修饰符可以相同,也可不同

  换而言之:

  • 若方法名相同且参数个数不同,无论参数类型如何,必然重载
  • 若方法名相同且参数个数相同,参数类型或参数顺序不同时才为重载。

重写

  方法重写是存在子类与父类之间的,子类定义的方法与父类中的方法具有相同的方法名字,相同的参数列表和相同的返回类型,方法的修饰权限不能缩小;

  重写方法的规则(遵循两同两小一大原则):

  • 方法签名相同(方法名相同,参数列表相同);
  • 子类返回类型小于等于父类方法返回类型(返回类型是基本类型和 void 的要和父类保持一致,只有引用类型返回类型小于等于父类的);
  • 子类抛出异常小于等于父类方法抛出异常。例如:父类的一个方法申明了一个受查异常;IOException,在重写这个方法是就不能抛出Exception,只能抛出IOException的子类异常,可以抛出非受查异常;
  • 子类访问权限大于等于父类方法访问权限(public>protected>default>private

默认域初始化

  若在构造器中没有显式地给成员变量赋值, 那么其将被自动地赋为默认值:

  • 数值则为0
  • 布尔值则为false
  • 对象引用则为null

无参构造器

  很多类都包含一个无参数的构造函数,对象由无参数构造函数创建时, 其状态会设置为适当的默认值。
  若在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。
  若类中提供了至少一个构造器,但是没有提供无参数的构造器, 则在构造对象时若 没有提供参数就会被视为不合法。

调用另一个构造器

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

初始化块

  前面写过两种初始化数据域的方法:

  • 在声明中赋值;
  • 在构造器中赋值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Employee{
private static int nextld;
private int id;
private String name;
private double salary;
//初始化块
{
id = nextld;
nextld++;
}
public Employee(String n, double s){
salary = s;
}
public Employee() {
name = "";
salary = 0;
}
...
}

  在这个示例中, 无论使用哪个构造器构造对象,id变量都在对象初始化块中被初始化。首先运行初始化块,然后才运行构造器的主体部分。
  这种机制不是必需的,也不常见。通常会直接将初始化代码放在构造器中。

final 应用场景

  • 对类:若一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为abstract的,又被声明为final的。
  • 对变量:将变量被声明为final,可以保证其在使用中不被改变。声明为final的变量必须在new一个对象时初始化(即只能在声明变量或构造器或代码块内初始化),而在以后的引用中只能读取,不可修改。
  • 对方法:将方法声明为final,可以保证其在使用中不被改变。被声明为 final 的方法只能使用,不能在子类中覆盖(重写)。

  Java允许使用包 (package) 将类组织起来。 借助于包可以方便地组织自己的代码, 并将自己的代码与别人提供的代码库分开管理。
  使用包的主要原因是确保类名的唯一性。假如两个程序员不约而同地建立了 相同名字的类,只要将这些类放置在不同的包中, 就不会产生冲突。

类的导入

  类的导入有两种方式:

  • 在每个类名之前添加完整的包名:java.time.LocalDate
  • 使用import语句导人一个特定的类或者整个包:import java.util.*;

  推荐使用第二种。

静态导入(了解)

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

1
import static java.lang.System.*;

  这样在代码中就可以直接使用System类的静态方法和静态域, 而不必加类名前缀:

1
2
out.println("Hello World");
exit(0);

包作用域

  Java中的修饰符共有 4 个,各自范围不同:

类别\范围 类内部 本包 子类 外部包
public 1 1 1 1
protected 1 1 1 0
default 1 1 0 0
private 1 0 0 0

  default修饰符默认不写。

参考

  • Cay S. Horstmann. Java 核心技术卷一 [M]. 机械工业出版社,2016
0%