§ 对象
面向对象编程是一种编程思维方式和编码结构。
抽象
所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。
“纯粹”的面向对象程序设计方法:
- 万物皆对象。
- 程序是一组对象,通过消息传递来告知彼此该做什么。
- 每个对象都有自己的存储空间,可容纳其他对象。
- 每个对象都有一种类型。
- 同一类所有对象都能接收相同的消息。
对象操纵
Java 操纵的标识符实际只是对象的引用。
1 | String s; |
这里只是创建了一个 String 对象的引用,而非对象。直接使用会出现错误,通常更安全的做法是:创建一个引用的同时进行初始化。如:
1 | String s = "asdf"; |
通常使用 new 操作符来创建一个新对象,new
表示:创建一个新的对象实例:
1 | String s = new String("asdf"); |
数据存储
寄存器
。最快的存储区域,位于 CPU 内部。Java 对其没有直接的控制权。栈内存
。存在常规内存 RAM 区域中,可通过栈指针获得处理器的直接支持。速度仅次于寄存器。创建程序时,Java 必须准确知道栈内保存的所有项的生命周期,虽然栈内存上存在一些 Java数据(如对象引用),但 Java 对象是保存在 堆内存 中的。堆内存
。通用的内存池(也在 RAM 区域中),与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。执行代码时会自动在堆中进行内存分配(代价是:分配和清理内存要比栈内存需要更多的时间)常量存储
。非 RAM 存储
。数据完全存在程序之外,如外置硬盘。主要有序列化对象、持久化对象等。
基本类型的存储
基本类型不是通过 new
来创建的,它使用一个 “自动”变量,这个变量直接存储 “值”,并且位于 栈内存
中,所以更加高效。
数组的存储
Java 的数组使用前需要初始化,并且不能访问数组长度以外的数据。这种范围检查,是以每个数组上少量的内存开销以及运行时检查下标的额外时间为代价的。
当我们创建对象数组时,实际上时创建了一个引用数组,并且每个引用的初始值都是 null
。
基本类型默认值
如果类的成员变量(字段)是 基本类型,当类初始化时,这些类型将被赋予一个 初始值。
1 | boolean -> false |
所以,为了安全最好进行显式初始化。
但是,局部变量不会进行初始化。Java 会直接报 编译错误。
static 关键字
类是对象的外观及行为方式的描述。只有在通过 new 创建那个类的对象后,数据存储空间才被分配。所以这种方式有两种情况不足:
- 有时你只想为特定字段分配一个共享存储空间,不去考虑究竟要创建多少对象,甚至根本不想创建对象。
- 创建一个与此类的任何对象无关的方法。即使没有创建对象,也能调用该方法。
我们可以在类的字段或方法前增加 static 关键字,表示这是一个静态字段或静态方法。
1 | class StaticTest { |
§ 封装
包
包内含有一组类,它们被组织在一个单独的命名空间下。如 java.util.ArrayList
在 Java 中,可运行程序是一组 .class
文件,它们可以打包压缩成一个 Java 文档文件(JAR)
,Java 解释器 负责查找、加载和解释这些文件。
如果使用 package
语句,它必须是文件中除了注释之外的 第一行代码。
1 | package hiding; |
为了清晰可见,public 成员放在类开头,接着是 protected 成员,包访问权限成员,最后是 private 成员。
§ 复用
- 在新类中创建现有类的对象,这种方式叫 组合。
- 创建现有类类型的新类,这种方式叫 继承。
Java 除非显式继承其他类,否则隐式继承 Java 标准根类对象 Object
是一个
是用继承来表达的,有一个
是用组合来表达的。
当你想在新类中包含一个已有类的功能时,使用组合,而非继承。在新类中嵌入一个对象,以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。
当使用继承时,使用一个现有类开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。
继承最重要的不是为新类提供方法,它是新类与基类的一种关系。表述为:新类是已有类的一种类型。
继承图中派生类转型为基类是向上的,通常称作向上转型。因为是从一个更具体的类转化为一个更一般的类。
所以 向上转型永远是安全的。
也就是说,派生类是基类的超集,它可能比基类包含更多的方法,但它必须至少具有基类一样的方法。在向上转型期间,类接口之可能失去方法,不会增加方法。
一种判断使用组合还是继承的最清晰方法是:问问自己是否需要把新类向上转型为基类,如果必须向上转型,那么继承就是必要的。
final 关键字
编译时常量,在 Java 中,必须是基本类型,用关键字 final
修饰。你必须在定义常量的时候进行赋值。
一个被 static
和 final
同时修饰的属性只会占用一段 不能改变的存储空间。
当用 final
修饰对象引用时,final
使引用恒定不变。
final 关键词关闭了动态绑定,但是大部分情况下不会对程序整体性能带来什么改变。
因此:最好是为了设计使用 final,而不是为了提升性能而使用。
1 | private final int valueOne = 9; |
按照惯例,final static 基本变量(编译时常量)命名全部使用 大写。
1 | // 空白 final |
空白 final 是指没有初始化值的 final 属性。必须在 定义时或者每个构造器 中执行 final 变量的赋值操作。
final 参数
方法的参数列表中,参数声明为 final 意味着在方法中不能改变参数指向的对象或基本变量。
final 方法
- 作用一:给方法上锁,防止子类通过覆写改变方法的行为,这是出于继承的考虑。
- 作用二:提高效率,在新 Java 版本中已经可以自动探测优化了。
final 类
意味着它不能被继承,目的是类的设计永远不需要改动,或者出于安全考虑不希望有子类。
类初始化和加载
Java 中类的代码在 首次使用时加载,通常是创建类的第一个对象,或者访问了类的 static
属性或方法。构造器也是一个 static
方法(隐式的)。当一个类它任意一个 static
成员被访问时,就会被加载。
§ 多态
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。
多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,还能创建有扩展性的程序。
将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前,叫做 前期绑定
。
而运行时根据对象的类型进行绑定。叫做后期绑定。也称为 动态绑定
或 运行时绑定
。
Java 中除了 static
和 final
方法外,其他所有方法都是后期绑定。
陷阱
private 方法可以当作是 final 的,只有非 private 方法才能被重写,要小心重写 private 方法的现象,因为编译器不报错。
建议使用 @Override
注解
协变返回类型
Java 5 引入协变返回类型。表示派生类的被重写方法可以返回基类方法返回类型的派生类型。
向下转型与运行时类型信息
1 | (MoreUseful) x).u(); |
如果不成功会得到 ClassCastException
异常。
§ 接口
包含抽象方法的类叫做 抽象类
。如果一个类包含一个或多个抽象方法,那么类本身也必须是抽象的。
抽象方法只有声明,没有方法体。
1 | abstract class Basic { |
如果创建一个继承抽象类的新类,那么必须为基类的所有抽象方法提供方法定义。否则新类仍然是一个抽象类。
使用关键字 interface
来创建接口,接口可以包含属性,这些属性被隐式指明为 static
和 final
。
Java8 的接口允许包含默认方法(default
关键字):
1 | interface InterfaceWithDefault { |
默认方法的接口意味着 Java8 开始具有了结合多个基类的行为。
Java8 允许在接口中添加静态方法:
1 | public interface Operations { |
这是 模版方法设计模式 的一个版本,runOps() 是一个模版方法。
Java8 引入 default
方法后,选择使用抽象类还是使用接口,变得更加困惑。
实际经验:尽可能的抽象。更倾向使用接口而不是抽象类。必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。
§ 内部类
this
与 new
如果你需要生成对外部类的引用,可以使用外部类名字与 this
。
1 | public class DotThis{ |
有时你需要去创建某个内部类的对象,必须在 new
表达式中提供其他外部类对象的引用。
1 | public static void main(String[] args) { |
因此,必须使用 外部类的对象 来创建 内部类的对象。