对象与类
三个重要概念:
-
封装
-
继承
-
多态
类之间的常见关系有:
-
依赖(uses-a):一个类的方法操纵另一个类的对象
-
聚合(has-a):一个类的对象包含另一个类的对象
-
继承(is-a):一个类是另一个类的拓展(子集)
在设计类时,应尽可能将相互依赖的类减至最少,用软件工程的术语来说,就是让类的之间的耦合度最小。
区分对象引用和对象实体
一个对象变量(引用)并没有实际包含一个对象,而仅仅存放了这个对象实体的内存地址,换句话说,任何对象变量的值都是对存储在堆(heap)中的一个对象的引用。可以将 Java 的对象变量看作 C++ 的对象指针(而不是 C++ 的引用),这个值可以是 null ,表明目前没有指向任何对象。
对引用的值进行拷贝,结果如下:
一、自定义类
一个源文件只能有一个公有类,但可以有任意数目的非公有类,源文件名必须与公有类的名字相同,否则无法编译。
关于多个源文件的编译顺序,“可以认为 Java 编译器内置了 make 功能。”
构造器
-
-
不能对一个已经存在的对象调用构造器
-
如果没有编写构造器,默认的无参数构造器会将实例域设置为默认值
-
一旦编写了构造器,就不再提供默认的无参数构造器
- 可以把构造器的访问权限设为 private,这样就无法在外部用 new 来创建实例,Math 类就是这么做的
方法
隐式参数是出现在实例方法名前的类对象,也就是方法调用的目标或接受者,可以在方法内部用 this 表示,与之对应,列在方法声明中的参数就属于显式参数。
不要编写返回引用可变对象的访问器方法,因为对这个引用进行操作会改变原有的实例。如果需要返回一个可变数据域的拷贝,应该使用 clone 方法。
一个类的方法可以访问这个类的私有特性,即所有对象的私有数据,而不局限于调用它的单一对象的私有数据。
重载
方法名以及参数类型(不包括返回类型)构成了方法的签名(signature)。如果多个方法有相同的名字、不同的参数类型或顺序,便产生了重载(overload),编译器通过用各个方法给出的参数类型,与特定方法调用所使用的值类型进行匹配,挑选出相应的方法。
final
用 final 修饰的实例域必须在构建对象时进行初始化,在后面的操作中不能再对它进行修改,大都应用于基本(primitive)类型域,或不可变(immutable)类的域(该类中的每个方法都不会改变其对象)。
对可变的类使用 final 修饰符可能会难以理清关系,例如:
public class Employee { private final StringBuilder evaluations; // ... Employee() { evaluations = new StringBuilder(); // evaluations 不可引用其他 StringBuilder 对象,但可以对当前对象的状态进行更改: evaluations.append(str); // ... } }
static
在绝大多数面向对象语言中,静态域被称为类域,由这个类的所有对象共享。
静态方法是一种没有隐式参数的方法,用类名进行调用(不提倡用其对象的引用),用于以下情况:
-
-
一个方法只需要访问类的静态域
-
工厂方法(factory method)
-
main 方法
- aa
二、按值调用
-
-
按引用调用(call by reference):方法接收的是调用者提供的变量地址
Java 总是按值调用,方法得到的是参数值的拷贝,不能修改传递参数变量的内容。为了说明这个问题,我们先做一个小实验:
public static void swap(Employee x, Employee y) { Employee temp = x; x = y; y = temp; }
Employee a = new Employee("Alice", ...); Employee b = new Employee("Bob", ...); swap(a, b); // x -> Bob, y -> Alice, a -> Alice, b -> Bob
下面两幅图能很好地分清“不可以修改变量”和“可以修改引用指向的对象”之间的不同:
总之,一个方法
-
-
可以改变一个对象参数的状态
-
不能让对象参数引用一个新的对象
三、构造对象
初始化
必须明确地初始化方法中的局部变量,但如果没有初始化类中的域,将会被自动初始化为默认值(0 / false / null)。
显式的域初始化方法:
-
定义实例域时直接赋值
-
用 this 关键字调用另一个构造器(节省代码)
-
初始化块
class Person { private int age; private boolean sex; private String name; private double asset; // initialization block { asset = 0; } // ... }
在一个类的声明中,可以包含多个代码块,这些块会在构造对象时执行,运行顺序先于构造器。
构建一个对象实例时,具体的处理步骤是:
-
-
按照类声明中的次序,依次执行所有域初始化语句和初始化块
-
如果构造器第一行调用了第二个构造器,执行第二个构造器主体
-
执行这个构造器主体
静态初始化
在初始化块之前使用 static 关键字可以对静态域进行初始化,适用于无法准确知道赋值内容的时候:
// static initialization block static { Random generator = new Random(); nextId = generator.nextInt(10000); }
类第一次加载时就会进行静态域的初始化,所有的静态域初始化语句、静态初始化块都将按照定义的顺序进行。
析构
Java 有自动的垃圾回收器(GC),不需要人工回收内存,因此 Java 不支持析构器。不过,如果对象使用了内存之外的其他资源(比如文件),那么当资源不再需要时,应该将其回收和再用。可以在类中添加 finalize 方法,它会在 GC 清除对象之前调用以回收资源。
由于难以预料 GC 什么时候会清除对象,所以不要依赖 finalize 方法来回收短缺的资源。想在某个资源使用后立刻关闭,可以在对象的方法中调用一个相应的 close 方法。
四、包
名字相同的类放置于不同的包中,就不会产生冲突。借助包可以方便组织自己的代码,并与别人提供的代码库分开管理。标准的 Java 包具有一个层次结构,它们都处于 java 和 javax 包层次中。不过从编译器的角度来看,嵌套的包之间没有任何关系,每一个包的类集合都是独立的。
导入
一个类可以使用所属包中的所有类,以及导入的其他包中的公有类。 import 语句可以导入一个特定的类或者整个包(用 *),不存在一条语句导入多个包的情况。Java 中的 package 和 import 语句类似于 C++ 中的 namespace 和 using 指令。
前面提到过静态导入,结合 static 关键字并在类名后再接上星号,就可以使用指定类的静态方法和静态域,不必再加类名前缀。例如:
import static java.lang.System.*; // ... out.println("good good study"); exit(0); // ...
作用域
如果一个类、方法或变量没有指定为 private 或 public,则这个部分可以被同一个包中的所有方法访问(默认访问权限)。对类来说,这种设计有一定的方便性,但对变量来说并不合适,有时会忘记加上修饰符,从而破坏封装性。因此,最好记得在声明变量时对访问权限作出显式标记。
五、类设计技巧
-
-
一定要对数据初始化
-
尽量用其他类替代多个相关的基本类型的使用
-
不是所有的域都需要独立的访问器和更改器
-
将职责过多的类进行分解
-
名字要体现职责
-
优先使用不可变的类
- 迪米特法则(LoD,一个对象应当对其他对象有尽可能少的了解,“talk only to your immediate friends”)