文章目录
基本概念:对象、类、属性和方法
现实世界中的物体有两个共同的特征:它们都有状态和行为。识别现实世界对象的状态和行为是从OOP思考的好方法。
对于你看到的每一个物体,问自己两个问题,这些现实世界的观察结果都会转化为OOP的世界:
- 这个对象可能处于什么可能的状态?
- 这个对象可以执行什么可能的行为?
一个对象是一个状态和行为的集合。
状态:对象中包含的数据。在Java中,这些是对象的字段。
行为:对象支持的操作。在Java中,这些被称为方法。
每个对象都有一个类,类定义了类型和实现:
- 一个类定义了方法和字段
- 方法和字段统称为成员
简单地说,一个类的方法是它的应用程序编程接口(API)——定义用户如何与实例交互。
静态/实例变量(方法)
类变量:与类相关而不是与类实例相关的变量。您还可以将方法与一个类——类方法关联起来。
- 使用‘.’来引用静态变量/方法。
那些不是类方法或类变量的方法和变量被称为实例方法和实例变量。
- 引用类实例中的方法和变量来使用实例方法/变量。
类变量和类方法与一个类相关联,并且每个类出现一次。使用它们不需要创建对象。实例方法和变量在每个类的实例中出现一次。
静态方法不与类的任何特定实例相关联,而实例方法(没有静态关键字的声明)必须在特定对象上调用。
接口和枚举
接口
Java的接口是设计和表达ADT的一种有用的语言机制,其实现是一个实现该接口的类。
- Java中的接口是一个方法签名的列表,但没有方法体。
- 如果类在其实现子句中声明了接口,则会实现接口,并为接口的所有方法提供方法体。
- 一个接口可以扩展一个或多个其他接口。
- 一个类可以实现多个接口。
接口 vs. 类
-
接口:确定ADT规约
-
类:实现ADT
也可以不需要接口而直接使用类作为ADT(既有ADT定义也有ADT实现),但实际中更倾向于使用接口来定义变量。
对变量和参数使用接口类型,除非明确有一个实现就足够了。
好处:支持变更实施,防止依赖于实施细节
为什么需要不同的实现?
- 不同的性能
- 选择最适合您使用的实现
- 不同的行为
- 选择实现您想要的实现
- 行为必须符合接口规范
- 通常,表现和行为都会有所不同
- 提供了一个功能:性能权衡
- 示例: HashSet,TreeSet
静态工厂方法
直接使用具体类的构造函数的问题:打破了抽象边界
接口定义中没有包含constructor,也无法保证所有实现类中都包含了同样名字的constructor。故而,客户端需要知道该接口的某个具体实现类的名字。
使用静态工厂:
默认方法
接口中的每个方法在所有类中都要实现,而通过default方法,在接口中统一实现某些功能,无需在各个类中重复实现它。
接口中默认方法的最典型用法是逐步为给定类型提供额外的功能,而不分解实现类。
使用接口好处
- 防止错误
- ADT是由它的操作定义的,接口就是这样做的。
- 当客户端使用接口类型时,静态检查可以确保它们只使用由接口定义的方法。
- 如果实现类公开了其他方法,或者更糟的是,类具有可见的表示——客户端不会意外地看到或依赖它们。
- 当我们有一个数据类型的多个实现时,接口就会提供对方法签名的静态检查。
- 易于理解
- 客户端和维护人员确切地知道在哪里寻找ADT的规范。
- 由于该接口不包含实例字段或实例方法的实现,因此更容易将实现的详细信息保留在规范之外。
- 易于变更
- 通过添加实现接口的类,我们可以轻松地添加一种类型的新实现。
- 如果我们避免构造函数而支持静态工厂方法,客户端将只会看到接口。这意味着我们可以在不更改客户端代码的情况下切换客户端正在使用的实现类。
枚举
有时一个类型有一个小的,有限的不可变值集,例如:一年中的月:1月,2月,…,11月,12月;一周的日子:星期一,星期二,…,星期六,星期日;罗盘点:北,南,东,西;…
当值集很小且有限时,将所有值定义为命名常量是有意义的,称为枚举。Java有一个枚举类型。
向枚举添加行为
封装和隐藏信息
区分一个设计良好的模块和一个坏的模块的一个最重要的因素是它对其他模块隐藏内部数据和其他实现细节的程度。
精心设计的代码隐藏了所有的实现细节:
- 清晰地将API与实现分离
- 模块仅通过API进行通信
- 它们忽略了彼此的内部工作原理
这些被称为信息隐藏或封装,是软件设计的基本原则。
封装的好处
- 将系统各个类解耦合
- 允许它们被单独开发、测试、优化、使用、理解和修改
- 加快系统开发
- 类可以并行开发
- 减轻维护负担
- 类可以被更快地理解和调试,而不用担心伤害其他模块
- 可以进行有效的性能调整
- 表现不佳的类可以单独地进行优化
- 增加软件复用性
- 低耦合度的类在其他上下文中经常被证明是有用的
使用接口隐藏信息
- 使用接口类型声明变量
- 客户端仅使用接口中定义的方法
- 客户端代码无法直接访问属性
但是,目前客户端还是可以访问非接口成员——本质上,这不是强制的信息隐藏。
成员的可见性修饰符
-
private
仅可通过声明类进行访问 -
protected
可从声明类的子类(以及在包)内访问 -
public
从任何地方访问
仔细设计您的API。
只提供客户端所需的功能,所有其他成员都应该是私有。
您可以让私有成员稍后公开而不破坏客户端——但反之亦然!
继承和重写
重写(Overriding)
可重写的方法:一种允许重新实现的方法。
在Java中,默认情况下是可重写的,即没有特殊的关键字。
严格继承:子类只能添加新方法,无法重写超类中的方法
- 子类只能向超类添加新的方法,它不能覆盖它们
- 如果一个方法不能在Java程序中被覆盖,它必须以关键字final作为前缀。
final
-
final字段:防止在初始化后重新分配到字段
-
final方法:防止覆盖方法
-
final类:防止扩展类
方法重写
方法重写是一种语言特性,它允许子类或子类提供方法的特定实现,该方法已经由其超类或父类提供。
重写方法需要有完全同样的方法签名。
所执行的方法的版本将由用于调用它的对象决定。
如果使用父类的对象来调用该方法,则将执行父类中的版本;如果使用子类的对象来调用该方法,则将执行子类中的版本。
当一个子类包含一个覆盖超类的方法的方法时,它还可以通过使用关键字super
来调用超类方法。
-
this
:调用自身的方法 -
super
:调用超类的方法
重写的时候,不要改变原方法的本意。
抽象类
抽象方法:一种具有签名但没有实现的方法(也称为抽象操作),由关键字abstract来修饰
抽象类:包含至少一个抽象方法的类称为抽象类
接口:一个只有抽象方法的抽象类
接口主要用于系统或子系统的规范。该实现由一个子类或其他机制提供。
多态、子类型、重载
三种类型的多态
特殊多态:在大多编程语言靠函数**重载(Overloading)**实现。
参数化多态:当编写代码时没有提到任何特定类型时,因此可以与任意数量的新类型显式地使用。在面向对象的编程社区中,这通常被称为泛型编程。
子类型多态、包含多态:当一个父类类型引用不同子类型实例时。
特殊多态与重载(Overloading)
当一个函数适用于几种不同类型(可能不显示公共结构),并且可能以每种类型的方式运行时,就会获得特殊多态性。
重载的方法允许您在类中重用相同的方法名,但使用不同的参数(也可以使用不同的返回类型)。
重载的方法通常意味着对调用方法的人来说更好一些,因为您的代码承担了处理不同参数类型的负担,而不是强迫调用者在调用方法之前进行转换。
函数重载是指使用不同的实现创建多个同名的方法的能力。对重载函数的调用将运行适合于调用上下文的该函数的特定实现,允许一个函数调用根据上下文执行不同的任务。
函数重载是一个静态多态。
- 函数根据参数列表进行解析,根据参数列表进行最佳匹配
- 静态类型检查,对象类型作为参数时取决于引用类型
- 在编译阶段时决定要具体执行哪个方法 (static type checking)
相反,重写的方法则是在运行时进行动态检查!
函数重载中的规则:重载函数必须参数数量或者类型不同,及必须要有不同的参数列表。
- 可以有不同或相同的返回值类型
- 可以不同或相同的访问等级
- 可以有不同或相同的异常抛出
- 可以在一个类中重载,也可以在子类中重载(Overload也可以发生在父类和子类之间!)
不要将重载和重写混淆:
- 当一个方法被重写时,派生类中给出的新方法定义与基类中的参数的数量和类型完全相同
- 当派生类中的方法与基类中的方法的签名不同时,这就是重载
- 注意,当派生类重载原始方法时,它仍然从基类继承原始方法
参数多态性与泛型编程
参数多态性
当一个函数在一系列类型上工作时,得到参数多态性;这些类型通常表现出一些共同的结构。它能够以一种通用的方式定义函数和类型,以便它基于在运行时传递的参数工作,即,允许静态类型检查而不完全指定类型。这在Java中被称为“泛型”。
泛型编程
泛型编程是一种编程风格,其中数据类型和函数根据稍后指定的类型编写,然后在需要时对作为参数提供的特定类型进行实例化。
泛型编程围绕着从具体的、有效的算法中抽象出来以获得通用算法的思想,这些算法可以与不同的数据表示相结合,从而产生各种有用的程序。
Java中的泛型
一个类型变量是一个非限定的标识符类型变量。
它们由泛型类声明、泛型接口声明、泛型方法声明和泛型构造函数声明引入。
泛型类:其定义中包含了类型变量
-
这些类型变量被称为类的类型参数。
-
它定义了一个或多个作为参数的类型变量。
-
泛型类声明定义了一组参数化类型,为类型参数部分的每个可能调用定义一个。
-
所有这些参数化的类型在运行时共享同一个类。
泛型接口:其定义中包含了类型变量
- 这些类型变量被称为接口的类型参数。
- 它定义了一个或多个作为参数的类型变量。
- 通用接口声明定义了一组类型,每个类型参数部分的可能调用对应一个类型。
- 所有参数化的类型在运行时共享相同的接口。
泛型方法:声明了类型变量的方法
- 这些类型变量被称为该方法的形式化类型参数。
- 形式化类型参数列表的形式与类或接口的类型参数列表相同。
使用**菱形操作符<>**来帮助声明类型变量。
List<Integer> ints = new ArrayList<Integer>();
public interface List<E>
public class Entry<KeyType, ValueType>
集合是包含一些其他类型为E的元素的有限集的ADT
Set是泛型类型的一个示例:其规范是根据后面要填写的占位符类型进行的类型。
我们没有为Set<String>
、Set<Integer>
等编写单独的规范和实现,而是设计并实现了一个Set<E>
。
通配符,只在使用泛型的时候出现,不能在定义中出现;可以用作对泛型的修饰:
List<?> list = new ArrayList<String>();
List<? extends Animal>
List<? super Animal>
泛型类型信息在运行时将被擦除(即仅限编译时),因此不能使用instanceof
来检查泛型。
无法创建泛型数组。
子类型多态
类型是一组值,一个子类型只是该超类型的一个子集。
继承/子类型的好处:代码的重用,建模的灵活性
Java中每个类只能直接扩展一个父类;一个类可以实现多个接口
B是A的一个子类型,意思是,每个B都是A,每个B都满足A的Spec。
- 如果B的规范至少和A的规范一样强,那么B才是A的一个子类型。
- 当我们声明一个实现接口的类时,Java编译器会自动强制执行此要求的一部分:它确保A中的每个方法都出现在B中,并具有兼容的类型签名。
- 类B如果不实现在接口A中声明的所有方法,就无法实现接口A。
对子类型的静态检查
但是编译器不能检查我们是否没有以其他方式削弱了规范:
- 加强了对方法的某些输入的先决条件
- 削弱了后置条件
- 削弱了接口抽象类型向客户机发布广告的保证。
子类型多态:不同类型的对象可以统一的处理而无需区分
每个对象都根据其类型进行行为(例如,如果您添加了新的子类型,客户端代码不会改变)从而隔离了“变化”
LSP里氏转换原则
如果S是T的一个子类型,那么T类型的对象可以被S类型的对象替换(即T类型的对象可以被子类型S的任何对象替换),而不改变T的任何理想属性。
instanceof
测试一个对象是否属于一个给定的类的操作符
建议:如果可能的话,避免instanceof
,永远不要使用超类中的instanceof
来检查针对子类的类型。
原因:
- 违反面向对象的原则:
instanceof
操作符会违反面向对象编程中的封装原则,因为它需要访问对象的具体类型信息。面向对象编程鼓励将行为封装在对象内部,并通过方法调用来实现对象间的交互,而不是直接操作对象的类型信息。 - 导致代码脆弱:使用
instanceof
进行类型检查可能导致代码的脆弱性。当对象的类层次结构发生变化时,需要修改使用instanceof
的地方。 - 缺乏扩展性:使用
instanceof
进行类型检查通常会导致代码的可扩展性变差。如果需要添加新的子类或修改现有的类层次结构,就需要修改使用instanceof
的地方。 - 违反多态性原则:多态性是面向对象编程的重要特征之一。通过多态性,可以通过父类的引用来处理不同子类的对象,从而提高代码的灵活性和可复用性。使用
instanceof
操作符会导致代码中充斥着类型检查,使得多态性的好处无法充分体现。
Java中的一些重要的Object方法
equals()
如果这两个对象是“相等的”,则为真
hashCode()
一个用于哈希映射的哈希代码
toString()
一种可输出的字符串表示形式
toString()
:你知道你的对象是什么,这样你就可以更好地重写。建议总是重写这个方法,除非你知道它不会被调用equals ()
&hashCode()
: 如果您需要值语义,则必须重写;否则不需要重写
设计好的类
不可变的类的优点:
- 简单
- 固有线程安全
- 可以没有代价地共享
- 不需要防御拷贝
- 优秀的构建块
如何编写一个不可变的类:
- 不提供任何mutator方法
- 确保没有任何方法可能被Override
- 使所有字段final
- 使所有字段peivate
- 确保任何可变组件的安全性(避免代表暴露)
- 实现
toString()
,hashCode()
,clone()
,equals()
, 等等。
什么时候设计不可变的类:
- 除非迫不得已时
- 总是让小的“价值类”成为不可变的!
- 例如:颜色,电话号码,单位
Java中Date是可变类,因此尽量不使用Date,而尽量使用Long代替
什么时候设计可变的类:
- 类需要表示其状态发生变化的实体
- 具体的类,如银行账户,红绿灯
- 抽象的类,如迭代器,集合,匹配器
- 过程类,如线程,计时器
- 如果类必须是可变的,那么最小化可变性
- 构造函数应该完全初始化实例对象
- 避免重新初始化方法
面向对象编程的历史
-
20世纪60年代: Simula 67是由挪威计算中心的克里斯汀·尼加德和奥勒-约翰·达尔开发的第一个面向对象的语言,用于支持离散事件模拟。(类、对象、继承等)
-
术语“面向对象编程(OOP)”最初是由Xerox PARC在他们的Smalltalk语言中使用的。
-
20世纪80年代: OOP已经变得突出,其中的主要因素是C++。
-
Niklaus Wirth为模块化编程和数据抽象,与Oberon和Modula-2;
-
Eiffel 和 Java