思想才是一切的起点——岛屿心情《寻找》
Java核心技术Ⅰ
第四章 对象与类
4.1 面向对象程序设计概述
传统的结构化程序设计通过设计一系列的过程(即算法)来求解问题。一旦确定了这些过程, 就要开始考虑存储数据的方式。这就是 Pascal 语言的设计者 Niklaus Wirth 将其著作命名为《算法 + 数据结构 = 程序》的原因。需要注意的是,在 Wirth 命名的书名中, 算法是第一位的,数据结构是第二位的。而 OOP 却调换了这个次序, 将数据放在第 •位,然后再考虑操作数
据的算法。
4.1.1 类
由类构造(construct) 对象的过程称为创建类的实例 (instance ).
封装( encapsulation , 有时称为数据隐藏) 是与对象有关的一个重要概念。从形式上看,封装不过是将数据和行为组合在一个包中, 并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域( instance field ), 操纵数据的过程称为方法( method 。) 实现封装的关键在于绝对不能让类中的方法直接地访问其他类的实例域。程序仅通过对象的方法与对象数据进行交互。
4.1.2 对象
- 对象的行为(behavior)—可以对对象施加哪些操作,或可以对对象施加哪些方法?
- 对象的状态(state )—当施加那些方法时,对象如何响应?
- 对象标识(identity )—如何辨别具有相同行为与状态的不同对象?
对象的这些关键特性在彼此之间相互影响着。例如, 对象的状态影响它的行为。
4.1.3 识别类
设计类的方法:首先从设计类开始,然后再往每个类中添加方法。
4.1.4 类之间的关系
- 依赖(“ uses-a”)
- 聚合(“ has-a”)
- 继承(“ is-a”)
依赖:依赖( dependence ), 即“ uses-a” 关系, 如果一个类的方法操纵另一个类的对象,我们就说一个类依赖于另一个类。
应该尽可能地将相互依赖的类减至最少。用软件工程的术语来说,就是
让类之间的耦合度最小。
聚合:聚合关系意味着类 A 的对象包含类 B 的对象。
继承:下一章着重介绍,一个类包含另外一个类里面的所有,例如Java中的所有类都是继承于超类Object
UML符号
4.2 使用预定义类
Java中提前定义好的类,前面几章中使用到的Math类。Math 类只封装了功能,它不需要也不必隐藏数据。由于没有数据,因此也不必担心生成对象以及初始化实例域。
4.2.1 对象与对象变量
在 Java 程序设计语言中, 使用构造器(constructor ) 构造新实例。
一定要认识到: 一个对象变量并没有实际包含一个对象,而仅仅引用一个对象。new 操作符的返回值也是一个引用。
Date deadline = new Date();
表达式 new Date() 构造了一个 Date 类型的对象, 并且它的值是对新创建对象的引用。
- 局部变量不会自动地初始化为 null,而必须通过调用 new 或将它们设置为 null 进行初始化。
- 所有的 Java 对象都存储在堆中。 当一个对象包含另一个对象变量时, 这个变量依然包含着指向另一个堆对象的指针。
- 在 Java中,必须使用 clone 方法获得对象的完整拷贝
4.2.2Java类库中的LocalDate类
类库设计者决定将保存时间与给时间点命名分开。所以标准 Java 类库分别包含了两个类:一个是用来表示时间点的 Date 类;另一个是用来表示大家熟悉的日历表示法的 LocalDate 类。
LocalDate使用静态工厂方法 (factory method) 代表你调用构造器。
实际上,Date 类也有 getDay、getMonth 以及 getYear 等方法, 然而并不推荐使用这些方法。 当类库设计者意识到某个方法不应该存在时, 就把它标记为不鼓励使用。
4.2.3 更改器方法与访问器方法
更改器方法:某个对象调用方法时对此对象进行修改
访问器方法:某个对象调用方法时对此对象不进行修改,重新生成一个对象,返回这个对象的引用。
4.3 用户自定义类
4.3.1 User类
public class User {
//Field
public String username;
private String password;
protected Integer age;
//Constructor
public User(){
System.out.println("无参构造器!");
}
public User(Integer age){
System.out.println("有参构造器 age="+age);
}
private User(String username){
System.out.println("私有构造器 username="+username);
}
protected User(char chr){
System.out.println("受保护的构造器 chr="+chr);
}
//Method
public Integer getAge() {
return this.age;
}
public String getPassword() {
return password;
}
protected void hello(){
System.out.println("hello");
}
private void printThings(String things,Integer count){
System.out.println("things="+things+",count="+count);
}
public static void main(String[] args) {
for (String str:args){
System.out.println(str);
}
}
}
源文件名是 User.java,这是因为文件名必须与 public 类的名字相匹配。在一个源文件中, 只能有一个公有类,但可以有任意数目的非公有类。
一个文件中包含两个类的时候,会创建两个.class文件,每个类对应一个class文件。
4.3.5 隐式参数与显式参数
User user=new User();
user.printThings("abc",12);
user是隐式参数
“abc”、12是显式参数。
在每一个方法中, 关键字 this 表示隐式参数。
4.3.6 封装的优点
注意不要编写返回引用可变对象的访问器方法。
什么意思?就是不要在get方法中返回实例域中的可变对象。举个例子
public class user{
private Info info;
public Info getInfo(){
return this.info;
}
}
class Info{
private String name;
private int age;
...
}
这个getInfo就是违反规则的,返回的info赋给另外一个变量的时候,这个变量可以直接操作info对象,破坏了封装性。
如果需要返回一个可变对象的引用, 应该首先对它进行克隆(clone )。对象 clone 是指存放在另一个位置上的对象副本。
public Info getInfo(){
return (Info)this.info.clone();
}
凭经验可知, 如果需要返回一个可变数据域的拷贝,就应该使用 clone。
4.3.7 基于类的访问权限
一个方法可以访问所属类的所有对象的私有数据。
class User{
private String name;
private int age;
public boolean equals(User user){
return this.name.equals(user.name);
}
}
这是合法的,user可以直接调用name。因为user是User的对象,而User的方法可以直接访问任何一个对象的私有域。
4.3.9 final实例域
可以将实例域定义为 final。 构建对象时必须初始化这样的域。也就是说, 必须确保在每一个构造器执行之后,这个域的值被设置, 并且在后面的操作中, 不能够再对它进行修改。
对于可变的类, 使用 final 修饰符可能会对读者造成混乱。
例如User类中的Info,如果将info使用final修饰,那么初始化之后info只能指向初始化后的Info对象就不能在变了,但是Info对象是可以变化的,可以操作里面的age。
引用不变,但是堆中的对象发生了变化。
4.4 静态域与静态方法
mian方法都被标识为static修饰符。
4.4.1 静态域
被static修饰,静态域属于类不属于任何的对象。
4.4.2 静态常量
静态变量使用的比较少,但是静态常量使用的比较多。
常用的静态常量是System.out。但是如果查看一下 System 类, 就会发现有一个 setOut 方法, 它可以将 System.out 设置为不同的流。。原因在于, setOut 方法是一个本地方法, 而不是用 Java 语言实现的。本地方法可以绕过 Java 语言的存取控制机制。
4.4.3 静态方法
静态方法是一种不能向对象实施操作的方法。
例如使用Math.pow(x,a), 不使用任何 Math对象。换句话说没有隐式的参数。
可以使用对象调用静态方法,但不推荐。
下面两种方法使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow) 。
- 一个方法只需要访问类的静态域(例如:Employee.getNextldh)
4.4.4 工厂方法
静态方法还有另外一种常见的用途。类似 LocalDate 和 NumberFormat 的类使用静态工厂方法 (factory method) 来构造对象。
为什么使用工程方法:
- 无法命名构造器。构造器的名字必须与类名相同。同一个类创建两种类型的对象。
- 当使用构造器时,无法改变所构造的对象类型。
4.4.5 main方法
main 方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main 方法将执行并创建程序所需要的对象。
4.5 方法参数
值传递和引用传递。
值传递是传值,引用传递是传地址。
Java不太一样。
值传递:拷贝一个新的值给变量
引用传递:将对象的地址传给变量。无法操作原对象的引用
String对象不可变是一个常量,使用+拼串的时候会产生一个新的String对象,如果原对象没有引用指向可能就会被垃圾回收器回收。所以当进行字符串的拼接的时候不推荐使用+操作String对象,而应该使用StringBuilder对象进行拼串,StringBuilder的对象是可变的。
基本类型的操作是在数值上进行操作,而不是新生成一个新的值。
参数使用情况:
- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
4.6 对象构造
构造器初始化对象的状态。
4.6.1 重载
有些类有多个构造器。例如, 可以如下构造一个空的 StringBuilder 对象:
StringBuilder messages = new StringBuilderO;
或者, 可以指定一个初始字符串:
StringBuilder todoList = new StringBuilderC'To do:\n";
这种特征叫做重载( overloading。) 如果多个方法(比如, StringBuilder 构造器方法)有相同的名字、 不同的参数,便产生了重载。既然有重载那肯定就有重载解析:编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。如果编译器找不到匹配的参数, 就会产生编译时错误,因为根本不存在匹配, 或者没有一个比其他的更好。
Java 允许重载任何方法, 而不只是构造器方法。
indexOf(int)
indexOf(int, int)
indexOf(String)
indexOf(String, int)
返回类型不是方法签名的一部分。也就是说, 不能有两个名字相同、 参数类型也相同却返回不同类型值的方法。
4.6.2 默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值: 数值为 0、布尔值为 false、 对象引用为 null。 如果不明确地对域进行初始化,就会影响程序代码的可读性。
4.6.3 无参数的构造器
如果在编写一个类时没有编写构造器, 那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是, 实例域中的数值型数据设置为 0、 布尔型数据设置为 false、 所有对象变量将设置为 null(这样搞很不好)。
如果类中提供了至少一个构造器, 但是没有提供无参数的构造器, 则在构造对象时如果没有提供参数就会被视为不合法。
请记住,仅当类没有提供任何构造器的时候, 系统才会提供一个默认的构造器如果在编写类的时候, 给出了一个构造器, 哪怕是很简单的, 要想让这个类的用户能够采用下列方式构造实例:
new ClassName()
就必须提供一个默认的构造器 ( 即不带参数的构造器)。
4.6.4 显式域初始化
通过重载类的构造器方法,可以采用多种形式设置类的实例域的初始状态。确保不管怎样调用构造器,每个实例域都可以被设置为一个有意义的初值,这是一种很好的设计习惯。
比较好的一种写法
class Employee
{
private static int nextld;
private int id = assignld();//每次初始化的时候这里调用
private static int assignld()
{
int r = nextld;
nextId++;
return r
}
...
}
4.6.6 调用另外一个构造器
如果构造器的第一个语句形如 this(…), 这个构造器将调用同一个类的另一个构造器。采用这种方式使用 this 关键字非常有用, 这样对公共的构造器代码部分只编写一次即可。
4.6.7初始化块
初始化数据域的方法
- 在声明的时候初始化
- 在构造器中设置值
- 使用初始化块
{
id = nextld;
nextld++;
}
首先运行初始化块,然后才运行构造器的主体部分。
每次创建对象的时候都会执行一次,但是如果为静态块,那就在加载类的时候执行一次。
初始化的顺序是按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块。
如果对类的静态域进行初始化的代码比较复杂,那么可以使用静态的初始化块。
4.6.8 对象析构域finalize方法
可以为任何一个类添加 finalize 方法。finalize 方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用 finalize 方法回收任何短缺的资源, 这是因为很难知道这个方法什么时候才能够调用。
== 有个名为 System.mnFinalizersOnExit(true) 的方法能够确保 finalizer 方法在 Java 关闭前被调用。不过,这个方法并不安全,也不鼓励大家使用。有一种代替的方法是使用方法 Runtime.addShutdownHook 添加“ 关闭钓” (shutdown hook), 详细内容请参看 API文档。==
4.7 包
Sun 公司建议将公司的因特网域名(这显然是独一无二的) 以逆序的形式作为包名,并且对于不同的项目使用不同的子包。例如, horstmann.com 是本书作者之一注册的域名。逆序形式为 com.horstmann。
从编译器的角度来看, 嵌套的包之间没有任何关系。例如,java.utU 包与java.util.jar 包毫无关系。每一个都拥有独立的类集合。
4.7.1 类的导入
使用 import
静态导入 import static
4.7.4 包作用域
前面已经接触过访问修饰符 public 和 private。标记为 public 的部分可以被任意的类使用;标记为 private 的部分只能被定义它们的类使用。如果没有指定 public 或 private , 这个部分(类、方法或变量)可以被同一个包中的所有方法访问。
4.8 类路径
类文件也可以存储在 JAR(Java 归档)文件中。在一个 JAR 文件中, 可以包含多个压缩形式的类文件和子目录, 这样既可以节省又可以改善性能。
JAR 文件使用 ZIP 格式组织文件和子目录。
4.9 注释
类注释必须放在 import 语句之后,类定义之前。
4.10 类设计技巧
- 一定要保证数据私有
- 一定要对数据初始化
- 不要在类中使用过多的基本类型,使用其他类代替
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 类名和方法名要能够体现它们的职责
- 优先使用不可变的类,例如LocalDate 类以及 java.time 包中的其他类是不可变的—没有方法能修改对象的状态。类似 plusDays 的方法并不是更改对象,而是返回状态已修改的新对象。选择使用LocalDate而不是Date