7 面向对象的编程
7.1 基本概念
7.1.1 对象
真实世界的对象由状态和行为构成。状态是成员变量,行为是方法。
状态:对象包含的数据
行为:对象支持的动作(在Java中,被称为方法)
7.1.2 类
每个对象都有一个类。类是对一组对象的抽象。
一个类中定义了方法和字段,方法和字段统称为类的成员。
粗略地说,类的方法是它的应用程序编程接口。
静态/实例化的变量/方法
静态
类成员变量是与类关联的变量,而不是与类的实例关联的变量。同理有类方法。
类成员变量和类方法:对整个类都成立。声明时,在变量和方法前加上static即可;要引用类变量和方法,需要在类的名称与类方法或类变量的名称之间加上句点(‘.’)
实例化
不是类方法或类变量的方法和变量称为实例方法和实例成员变量。
类是一个模版,可以产生其他的实例对象。但要指定其成员变量,这个过程叫实例化。要引用实例方法和变量,必须先实例化,然后再引用类实例中的方法和变量
eg:
staticStr是静态的,在静态方法main中可以直接调用;
testStaticMethod方法是静态的,在main方法中可以直接调用;
testObjectMethod方法是非静态的,在main方法中需要先实例化对象msm然后再通过msm调用
7.2 接口和枚举
7.2.1 接口
Java的接口是设计和表达ADT的一种有用的语言机制,它的实现是实现该接口的类,如果类在其implements子句中声明接口,并为接口的所有方法提供方法体,则类将实现接口
-
一个接口可以扩展一个或多个其他接口(接口之间可以继承和扩展)
-
一个类可以实现一个或多个接口(一个接口可以有多个实现类)
-
类可以实现接口ADT,也可以不依赖接口直接自己定义并实现ADT
-
实际中,更倾向于用接口来定义变量,用类来实现接口
eg:
eg:
- set是不可变的,实现类中不可以有add方法
- Java接口没有构造方法
- 接口中不可以有接口的实现方法如arraySet,接口需与实现类独立
- 实现类中缺少contains方法的具体实现
接口和实现类的使用方法:
接口 实例 = new 实现类
但是这样封装不彻底,因为暴露了实现类给用户,因此,从Java8开始,接口中允许拥有静态方法,其将实现封装到函数体内部,这样用户就不会知道具体的实现类,如:
在接口中使用default方法
在基于抽象的典型设计中,一个接口有一个或多个实现,如果向接口添加一个或多个方法,所有实现也将被迫实现它们。否则,设计就会崩溃。
通过default方法,在接口中统一实现某些功能,无需在各个类中重复实现它。
default 方法的典型使用方式:以增量式的为接口增加额外的功能而不破坏已实现的类
eg:
7.2.2 枚举(略)
7.4 封装与信息隐藏
7.4.1 信息隐藏
信息隐藏是区分设计模块好坏的最重要的因素,也是软件设计的基本原则。
精心设计的代码隐藏了所有的实现细节,这种代码有以下特征:
- 清晰地将API与实现分离
- 模块间只能通过API打交道
- 它们之间相互独立
信息隐藏的好处:
- 允许类被单独开发、测试、优化、使用、理解和修改
- 加速系统开发过程(类可以被平行开发)
- 减轻维护负担(可以更快地理解和调试某个类,而不必担心损害其他模块)
- 实现有效的性能调整(某个关键的类可以进行单独优化)
- 增加软件复用
基于接口的信息隐藏
- 使用接口类型声明变量
- 客户端仅使用接口中定义的方法
- 客户端代码无法直接访问属性
但是即使这样,客户端仍然可以访问其他非接口的成员。因此 ,Java中规定了成员的可见性。分以下4类:public、protected、default、private
信息隐藏的最佳实践
- 小心地设计API
- 只提供客户端所需的功能,所有其他成员都设置为private
- 可以逐步放开(原来private后来变成public),但是不能反过来
7.5 继承和重写
继承是为了代码重用,其特点如下:
- 超类特性(公共、受保护)在子类中隐式可用
- 只写一次代码,子类使用时可以覆盖重写
eg:
7.5.1 重写
可重写方法是指允许重新实现的方法
在Java中,默认情况下方法是可重写的,即没有特殊的关键字。但是,如果是strict inheritance(严格继承),则子类无法覆盖重写父类中的方法,只能添加新的方法。如果无法覆盖某个方法,则必须在其前面加上关键字final。
eg:
覆盖/重写
方法重写是一种功能,它允许子类或子类提供已由其超类或父类之一提供的方法的特定实现,但是重写的时候不能改变方法的本意。
前最好在重写方法之前加上@override,这样编译器就会在编译时自动检查覆盖方法和被覆盖的方法签名是否完全一致。
如果子类想要覆盖重写父类中的函数,则要和父类保持相同的方法签名。实际执行时调用哪个方法,运行时根据调用对象决定:如果使用父类的对象调用该方法,则将执行父类中的版本;如果使用子类的对象调用该方法,则将执行子类中的版本。
父类型中的被重写函数体不为空:意味着对其大多数子类型来说,该方法是可以被直接复用的。对某些子类型来说,有特殊性,故重写父类型中的函数,实现自己的特殊要求。如果父类型中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写。
当子类包含重写超类方法的方法时,它还可以使用关键字super调用超类方法
在构造方法中,如果要调用父类的构造方法,则要将super()放在构造方法的第一句,否则会报错。
7.5.2 抽象类
至少包含一个抽象方法的类称为抽象类,抽象类只有定义没有实现,不能实例化
继承某个抽象类的子类在实例化时,所有父类中的抽象方法必须已经实现
eg:
抽象类和接口的关系
接口:只有抽象方法的抽象类
接口主要用于系统或子系统的规范。实现由子类或其他机制提供
具体类→抽象类→接口
7.6 多态、子类型、重载
7.6.1多态的3种形态
- 特殊多态
- 参数化多态
- 子类型多态、包含多态
7.6.2 特殊多态:
即重载(一个方法有多个重名的实现,参数列表不同或返回值不同)
除了名字相同之外,没有任何关系
重载使用静态类型检查,在编译阶段决定具体执行哪个方法(重写是在运行时进行动态检查的)
123是重载,45不是,形参不同不算重载
根据静态检查确定重载方法的具体方法
要调用的方法的哪个重写版本是在运行时根据对象类型决定的,但要调用的方法的哪个重载版本是根据编译时传递的参数的引用类型决定的
对于重写的方法,在子父类都有的情况下,会到运行时进行动态检查;对于重载的方法,会直接进行编译阶段检查
-
1.没有实现接口
-
4.b是animal,没有moo方法
重写vs重载
- 重写时父类和子类中的方法具有相同的签名
- 签名不同时则为重载
- 子类重载了父类的方法后,子类仍然继承了被重写的方法
7.6.3 参数化多态:泛型编程(一个类型名字可以代表多个类型)
参数多态性是指方法针对多种类型时具有同样的行为(这里的多种类型应具有通用结构),此时可使用统一的类型变量表达多种类型
泛型是一种以通用方式定义函数和类型的能力,因此它可以基于运行时传递的参数工作,也就是说,在不完全指定类型的情况下允许静态类型检查。在运行时根据具体指定类型确定具体类型(编译成class文件时,会用指定类型替换类型变量“擦除”)
泛型编程是一种编程风格,其中数据类型和函数是根据待指定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化
泛型编程围绕“从具体进行抽象”的思想,将采用不同数据表示的算法进行抽象,得到泛型化的算法,可以得到复用性、通用性更强的软件。
使用“<>”,帮助声明泛型变量,如:
使用泛型变量的三种形式 :泛型类、泛型接口和泛型方法
泛型类
类中如果声明了一个或多个泛型变量,则为泛型类
这些类型变量称为类型参数(类型参数)
eg:
泛型接口
如果接口声明了一个或多个类型变量,那么它就是泛型的。
Set是泛型类型的一个例子:
我们没有为Set和Set等编写单独的规范和实现,而是设计并实现了一个Set。
泛型接口有两种实现方法:
1.非泛型的实现类
在子类实现接口的时候明确给出类型
eg:
2.泛型的实现类
在子类定义的时候继续使用泛型
eg:
泛型方法(略)
7.6.4 子类型多态、包含多态:
Java集合API:
一个类只有一个父类,但可以实现多个接口。子类型多态主要说的是接口的具体实现类,如list接口有arraylist和linkedlist两种实现。
子类型
(一个变量名字可以代表多个类的子类)
个类只有一个父类,但可以实现多个接口
“B is a subtype of A” means “every B is an A.”
从规约角度看:如果B的规约至少和A的规约一样强大,那么B只是A的一个子类型,子类型的规约不能弱化父类型的规约。
子类型多态:不同类型的对象可以统一的处理而无需区分,从而隔离了开发者和用户之间的“变化”
eg:如果您添加了新类型的帐户,则客户端代码不会更改
7.10 Java中一些重要的对象方法
Java中最大的类Object类中提供了3个方法,可以进行重写:
- equals():判断两个对象是否相等
- hashcode():返回字符串的哈希码
- toString():将其他类型转换为可打印的字符串
equals方法是Object类的方法,其代码就是使用“==”进行地址的比较,但在某些类如String中进行了重写。
基本数据类型的比较直接使用“==”即可,对象数据类型才使用equals方法。基本数据类型和其包装类型比较时则将包装自动拆开,只进行值的比较。
对于字符串来说,使用“==”比较字符串的地址是否相同;使用equals方法比较字符串内容是否相同。
补充
1、 hashCode是object中的方法,但是是native方法,native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,无法看到源码的实现
2、一般情况下,对象不同(equals不同),hashCode一般不同,但hashcode可以相同。
3、但是对象相同(equals相同),hashCode一定相同。
4、HashMap和HashSet的存取都是通过Key的hash值来存取的
5、建议如果equals判断结果为false的情况下,hashCode最好不要相同
如果涉及到身份识别(一般使用hashmap或者hashset存储key信息),则必须重写equals和hashCode方法(如果只是重写equals方法的话,则由于每次比较new一个对象的时候都会引用新的内存地址,则会得到不同的hashcode值,因此无法找到对应的hashmap或者hashset桶,无法进行接下来的比较),如下面的例子:
eg:输出为false
解决方法:将重载的equals方法替换为重写的equals方法,输出为true
7.11 设计好的类
不可变的类的优点:
- 简单
- 天生线程安全
- 可以自由共享
- 不需要防御性拷贝
- 优秀的构建块
如何写一个不可变类:
- 不要提供任何变值器
- 确保没有方法被重写
- 所有成员变量都设置为private final
- 确保每个可变数据类型的安全(防止内存泄漏)
- 重写toString,hashCode,clone,equals等方法
何时编写一个不可变类:
- 几乎任何时候
- 要将最底层的类如颜色、电话号码单元等设置为不可变类
何时编写一个可变类:
- 类所代表的实体需要进行变化,如现实中的交通灯、银行账户等
- 抽象操作:迭代器(iterator)、匹配器(matcher)、容器(collections)等
- 进程类:thread,timer
如果类必须可变,则要:
- 最小化可变区域
- 构造函数应该完全初始化实例
- 避免重新初始化方法