面向对象
概述
历史:
面向对象编程(
object-oriented *programming: oop
)取代 面向过程编程(结构化)注:面向对象是一种思想而不是技术:
Java
是面向对象的语言但是不代表不能按照面向过程的思想写代码,C
是面向过程的语言同样不代表不能按照面向对象的思想写代码==程序 = 算法 + 数据结构==(
Pascal
语言的设计者Niklaus Wirth
提出)(1) 面向过程的编程思想中认为 算法 是优先级最高的元素:程序员需要先明确如何操作数据之后才去定义具体的数据结构
(2) 面向对象的编程思想中认为 数据结构 是优先级最高的元素
面向对象编程更利于大型项目的开发,面向过程编程相对于适合小型项目的开发
类
- 定义:用于创建具体实例或者对象的 模板
- 基本组成:(1) 类变量 (
static
修饰的成员变量) (2) 成员变量(实例变量)(3) 构造方法 (4) 方法 - 特性:(1) ==封装== (2) ==继承== (3) ==多态==
- 关系:(1) 泛化 (2) 实现 (3) 依赖 (4) 组合 (5) 关联 (6) 聚合
对象
- 定义:利用类模板创建多个具有 相同行为 的实例
- 特点:
- 每个对象都在虚拟机的堆区中 占有相应的空间 并且 具有相应的地址
- 对象的比较:(1)
==
比较对象比较的是每个对象在堆区中的地址 (2)equals()
方法只有在 重写 之后才比较的是对象包含内容
类
组成
成员变量:
定义:属于每个对象实例的变量
特点:
细节:每个对象中都隐含一个 “成员变量” :
this
表示当前对象的 引用(1) 方法的形式参数和成员变量名字相同时可以调用
this
加以区分public MyObject (int count){
this.count = count;
}(2) 每个对象在初始化完成时第一个变量一定是 对象的引用 (
this
)private MyObject this = referrence; // 模仿变量的声明,实际情况不是这样的
类变量:
- 定义:
static
关键字修饰的成员变量 - 特点:
- 类变量只属于类而不属于任何一个对象:只能使用 类名调用类变量 不可以使用对象调用
- 类变量在 连接过程的准备阶段 中被 分配空间 并且设置为 默认值,在初始化过程中赋予指定的值 (
JVM
相关)
- 定义:
构造方法
定义:创建对象实例时默认调用用于初始化成员变量的方法
特点:构造器默认是静态的 (
static
)==数量==:每个类都拥有 至少一个 构造器,可以利用方法 重载 拥有多个不同的构造器
注:如果程序员没有显式地提供任何构造器,那么编译器会默认生成一个无参构造器;如果程序员编写了相应的构造器,那么这个无参构造器就会被 覆盖,需要自己手动编写无参构造器
==相互调用==:每个构造器之间可以利用
this
关键字 相互调用==访问权限==:
- 编译器自动提供的构造器的访问权限取决于类的访问权限
- 构造器访问权限为包可见性:该类只可以在隶属于同一个包下的类中创建对象
- 构造器访问权限为私有的:该类不可以被在其他类中 直接创建 对象(单例模式)
- 构造器访问权限为受保护的:该类只可以在隶属于同一个包下的类创建对象,子类是不可以创建父类对象的,仅可以借助
super
调用
==初始化顺序==
方法
特性
封装
- 定义:合并数据和行为并创建新的数据类型,将接口和实现分离,实现细节隐藏
继承
定义:采用关键字
extends
实现的类与类之间的关系特点:
无论访问权限如何,子类可以继承父类 所有成员变量和方法(包括构造方法):私有变量和方法可以称为隐式继承,非私有变量和方法可以称为显示继承
实质:
(1) 虚拟机会 ==默认 调用父类的构造方法 对父类的所有成员变量进行初始化== 并且分配到 ==子类的内存空间==
(2) 虚拟机继续调用子类自身的构造方法并将所有的成员变量初始化添加到子类的内存空间中
==调用构造方法 ≠ 创建对象==:调用构造方法仅仅只是初始化成员变量,
new
关键字才是真正在内存中开辟空间创建对象==继承 ≠ 可访问==:
(1) 子类虽然继承了父类的成员变量和方法,但是访问权限可能是私有的,所以子类无法访问
(2) 子类无法访问到父类私有的成员变量和方法,也就无法对其进行覆盖,相当于子类内存区域中存在两个方法名一样的方法
this & super
定义:两者都指向子类对象内存区域,前者用于引用子类的属性,后者用于引用父类的属性
《
Java
核心技术 卷一》:“super
不是一个对象的引用,例如,不能将值super
赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字”注:从核心技术这句话中也可以看出,子类创建时并没有在内存中创建父类对象,也证明两个引用确实都是指向的子类的内存区域
特点:
- 两者都无法在静态代码块、静态方法中使用
- 两者都无法调用静态变量、静态方法
super()
调用父类构造器方法会被虚拟机 ==默认调用==super()
调用父类构造器方法 ==只能够放在子类构造器中第一行==,不可以放在其他任何方法中
向上转型 & 向下转型
向上转型
定义:子类引用向父类引用进行转换被称为向上转型
Father father = new Son();
特点:
(1) 子类向上转型后只可以调用父类拥有的成员变量和方法,无法调用子类自身的成员变量和方法,但是仍然拥有自身的成员变量
(2) 向上转型可以通过隐式的强制转换就可以执行:意味着向上转型始终都是安全的
解释:子类一定拥有父类的所有属性,所以即使转换成父类也是依然可以正常调用的
向下转型
定义:父类引用向子类引用进行转换被称为向下转型
Son son = (Father) new Father
特点:
(2) 向下转型可以通过编译器检查但是无法调用任何成员变量和方法:会直接抛出
java.lang.ClassCastException
异常
重写 & 重载
重写(
Override
)定义:子类对从父类继承而来的方法进行 重新定义:子类重新实现一个方法,这个方法和父类的 方法声明完全相同,实现不同
/* LinkedHashMap 继承 HashMap 并且重写了 newNode 方法 */
// HashMap 中创建节点的方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
// LinkedHashMap 中创建节点的方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}规则:
- 子类重写的方法的访问权限必须 大于等于 父类的访问权限
- 子类重写的方法的返回值必须是父类方法的返回值的子类或者其本身:如果子类重写方法的返回值是父类方法返回值的子类,则称这两个方法拥有可协变的返回类型
- 子类重写的方法的抛出的异常不能比父类更加宽泛,也不能够抛出新的异常
- 可以采用注解
@Override
检查是否符合其规范
细节:
如果子类只是想要在父类的方法上进行扩展,那么就需要调用父类的方法,由于两者名称相同,不能直接调用,使用关键字
super
解释:
super
和this
类似,都是指向对象的引用真正的实现原理见虚拟机
重载(
Overload
)定义:类中多个 具有相同方法名 但是 具有不同参数列表 的方法同时存在
规则:
解析过程:(==重载解析==)
编译器查看对象的声明类型以及需要调用的方法名:列出所有方法名相同但是参数列表不同的方法(==方法是子类和父类中可访问的==)
编译器根据调用方法提供的参数列表进行查找:在所有可能的方法中匹配参数列表相同的方法
(1) 如果子类中没有匹配到相应的方法,那么将会在父类中继续匹配
(2) 如果父类中依然没有匹配到相应的方法,则继续重复上述过程,直到找到为止
每次都匹配搜索实际需要调用的方法显然是非常低效的:虚拟机为每个类计算得到了 方法表,每次调用方法只需要查看方法表就行
注:方法表 = 方法签名 + 实际调用方法 (方法签名 = 方法名 + 参数列表)
细节:
- 体现方法的多态性
- 仅有返回值不同的方法不会被编译器视为重载,但是虚拟机中仍然认为是方法重载:仅有返回值不同是无法通过编译器检查的,即使在虚拟机中可行
- 真正的实现原理见虚拟机
多态
定义:某个对象引用可以指向多种不同的实际类型的现象(==多态又被称为后期绑定、动态绑定、运行时绑定==)
体现:重载可以体现方法的多态性 、重写和向上转型可以体现类的多态性;成员变量无法体现多态性
/*向上转型体现多态*/
Father father = new Son();
/*方法重写*/
public void play(){ // 父类中的方法
System.out.println("Father Play...");
}
public void play(){
System.out.println("Son Play..."); // 子类中的方法:重写
}
public static void test(Father father){
father.play(); // 最后调用的一定是子类重写的方法而不是父类的方法,重写就在这里体现了多态性:编译器是如何知道最后调用的是子类的实现呢?明明这里是父类的引用
}
public static void main(String[] args){
test(new Son()); // 方法参数给定的类型是父类,但是传入的参数却是子类:发生向上转型
}
/*方法重载*/
/*成员变量无法体现多态性*/
Father father = new Son(); // 假定父类和子类中都存在变量 field
System.out.println(father.field); // 调用的一定是父类自身的那个变量而一定不会是子类解释:
编译器在编译器的确不知道最后父类引用的实际指向类型究竟是什么,需要等到方法 真正被调用执行的时候 才会将父类引用替换成实际类型的引用
所以被称为动态绑定或者运行时绑定
编译器仅仅负责在编译期间就能够确定的东西,剩下无法确定的东西全部交付给虚拟机在运行期间来判断,虚拟机就采用 ==动态分派== 来完成
特点:
- 消除类型之间的耦合关系
static
、final
、private
修饰的方法没有多态性- 继承 不是实现多态的唯一手段,可以采用 接口实现同样可以实现多态
关系
泛化(
Generalization
)定义:从特殊到一般抽象出更加通用的类
细节:
(1) 泛化和继承是从 不同的角度描述的同一种关系:泛化是从特殊到一般,继承是从一般到特殊
(2) 实际实现时通常 不推荐使用 泛化/继承 这种高耦合的关系
实现(
Realization
)定义:类实现接口、抽象类中的抽象方法
细节:接口中的所有方法都必须实现,抽象类中则只需要实现抽象类方法
聚合(
Aggregation
)定义:整体由部分组成,部分和整体不是 强依赖,整体不存在但是部分依然存在
细节:
(1) 实际实现时不会在构造器中创建部分的引用而是借助方法创建
(2) 整体对象生命周期结束 不代表 部分对象的生命周期也结束
class Person{
private Computer computer;
public Person(){
// 不会在构造器中创建
}
public void setComputer(Computer computer){
this.computer = computer; // 等待外界的传入
}
}
组合(
Composition
)定义:整体由部分组成,部分和整体是 强依赖,整体不存在部分也不存在
细节:
(1) 实际实现时借助构造器创建部分的引用
(2) 整体对象生命周期结束 代表 部分对象的生命周期也结束
(3) 实际实现是更加推荐使用组合、聚合的方式来建立类与类之间的联系
class Person{
private Hand hand;
private Foot foot;
private Head head;
public Person(Hand hand, Foot foot, Head head){
this.hand = hand;
this.foot = foot;
this.head = head;
}
}
关联(
Association
)- 定义:类与类之间的联系,联系是强依赖的,具有长期性的
- 分类:
- 按照方向分类:(1) 双向关联 (2) 单向关联 (3) 自关联
- 按照联系重数分类:(1) 一对一 (2) 一对多 (3) 多对多
- 细节:
- 实际实现只要其他类出现在当前类中都算作是关联关系
- 关联关系可以和其他的关系叠加
依赖(
Dependency
)定义:类与类之间的联系,联系是偶然性的,非常弱
细节:实际实现时在方法的参数中使用到另外一个类的引用
class ClassA{
public void move(ClassB classb){ // 仅仅只是在方法中使用到其他类的引用不是长期性的
...
}
}
访问权限
包可见性
- 定义:被修饰的属性只有处于 ==同一个包== 下的类才可以直接访问
- 特点:属性不添加任何访问权限关键字修饰,默认就是包可见性
- 细节:
- 阅读源码过程中可以发现,不少类的内部成员变量都是采用包可见性:目的在于简化类之间的访问
- 编译器自动提供的构造器的访问权限取决于类的访问权限
- 构造器访问权限为包可见性:该类只可以在隶属于同一个包下的类中创建对象
public
- 定义:被修饰的属性可以被 ==任何类== 直接访问
- 特点:
- 细节:接口中默认所有方法都是公共的
private
定义:被修饰的属性只有在 ==类的内部== 可以直接访问
特点:
-
别名机制:多个引用指向堆中同一个对象,私有的引用不可以被直接访问,但是公共的引用可以直接访问,并且操作对象
细节:
private
不可以修饰普通类;可以修饰内部类private
修饰的方法默认是final
:不可以被重写也不可以被继承- 构造器访问权限为私有的:该类不可以被在其他类中 直接创建 对象(单例模式)
protected
- 定义:被修饰的属性可以被 ==子类== 或者 ==同一个包== 下的类直接访问
- 细节:
protected
不可以修饰普通类;可以修饰内部类- 构造器访问权限为受保护的:该类只可以在隶属于同一个包下的类创建对象,子类是不可以创建父类对象的,仅可以借助
super
调用
private |
protected |
friendly |
public |
|
---|---|---|---|---|
范围 | ==类内部可访问== | ==子类及其同一个包下的类== | ==同一个包下的类== | ==任何类== |
修饰 | 变量、方法、内部类 | 变量、方法、内部类 | 变量、方法、任何类 | 变量、方法、任何类(唯一) |
继承 | 可以 | 可以 | 可以 | 可以 |
构造器 | 不可以直接创建(单例模式) | 同一个包中类可以直接创建 | 同一个包中类可以直接创建 | 可以随意创建 |
关键字
- 前提:仅列出和类相关的常用关键字,并发和其他相关的关键字不在此列出
static
静态变量
定义:采用关键字
static
修饰的变量 (静态变量又被称为类变量)特点:
- 被修饰的变量被类的所有实例对象共享 / 被修饰的变量仅有一份且属于类:直接通过类名调用而不能够创建对象调用;类的内部不可以使用
this
调用 - 被修饰的变量仅会在 类加载阶段 被初始化并赋值 / 仅会进行一次初始化和赋值:类仅加载一次,所以静态变量也只会被初始化一次
- 被修饰的变量不会被垃圾回收机制回收:将会永久存在于虚拟机的内存中
- 被修饰的变量被类的所有实例对象共享 / 被修饰的变量仅有一份且属于类:直接通过类名调用而不能够创建对象调用;类的内部不可以使用
细节:
static
关键字仅可以 修饰成员变量,不可以 修饰方法内的局部变量static
关键字修饰的变量是可以修改的(不要和final
关键字搞混)System.out
是 静态常量:理论上 是不可以对静态常量进行修改的,实际Java
中存在setOut
方法可以更换输出流public static void setOut(PrintStream out) {
checkIO();
setOut0(out);
}
private static native void setOut0(PrintStream out); // 可以修改静态常量的原因是因为该方法是本地方法,采用 C++ 编写的
静态方法
- 定义:采用关键字
static
修饰的方法 - 特点:
- 被修饰的方法被类的所有实例对象共享 / 被修饰的方法仅有一份且属于类:直接通过类名调用也可以创建对象调用;类的内部不可以使用
this
调用 - 被修饰的方法仅可以调用静态方法、静态变量:不可以调用非静态的变量、非静态的方法
- 被修饰的方法不会被垃圾回收机制回收:将会永久存在于虚拟机的内存中
- 被修饰的方法不可以是抽象方法:静态方法不依赖于任何对象,所以必须有实现,因此不可以是抽象方法
- 实例方法既可以访问静态变量、静态方法,也可访问实例方法、实例变量
- 被修饰的方法被类的所有实例对象共享 / 被修饰的方法仅有一份且属于类:直接通过类名调用也可以创建对象调用;类的内部不可以使用
静态代码块
定义:采用关键字
static
修饰的代码块static {
InputStream in = JDBCUtils.class.getClassLoader().getResourceAsStream("db.properties");
Properties properties = new Properties();
...
}特点:
- 代码块是不可以被调用的
- 被修饰的代码块仅会在类加载阶段执行且仅执行一次:类仅加载一次,所以静态代码块也只会执行一次
- 被修饰的代码块仅可以调用静态方法,静态变量:不可以调用非静态的变量、非静态的方法
细节:
提高程序性能
解释:多次调用方法中反复初始化内容不变的对象,可以把对象的初始化过程放入静态代码块中减少反复初始的过程
boolean isBornBoomer() { // 每次调用这个方法都会创建对象
Date startDate = Date.valueOf("1946"); // 创建的对象具有内容还完全一样
Date endDate = Date.valueOf("1964"); // 最好抽取出来成为静态代码块
return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
}多个静态代码块的执行顺序是按照 ==从上至下== 的顺序执行的:包括
main
方法在内public class Test {
static{
System.out.println("test static 1"); // 第一个执行的静态代码块
}
public static void main(String[] args) {
// 第二个执行的静态代码块:虽然什么内容都没有但是依然会执行
}
static{
System.out.println("test static 2"); // 第三个执行的静态代码块
}
}
静态内部类
定义:采用关键字
static
修饰的内部类特点:
静态内部类可以直接在外部通过
new
创建对象:内部类只能够借助外部类后再利用new
创建对象public static void main(String[] args){
MyTest myTest = new MyTest(); // 外部类
InnerClass innerClass = myTest.new InnerClass(); // 内部类只能够借助外部类创建对象
StaticInnerClass staticInnerClass = new StaticInnerClass(); // 静态内部类直接创建对象
}
静态导包
定义:采用关键字
static
进行导包import static java.lang.System.*; // 静态导包
特点:
- 采用静态导包之后,该类中的所有静态变量、静态方法都可以直接使用,不需要再使用类名调用了
细节:通常不会这样做,因为代码可读性非常低
初始化顺序
初始化优先级排序:
- 父类静态代码块、静态变量
- 子类静态代码块、静态变量
- 父类实例变量、普通代码块
- 父类构造方法
- 子类实例变量、普通代码块
- 子类构造方法
细节:
- 静态修饰的内容的初始化顺序取决于声明的顺序;实例变量和普通代码初始化顺序也取决于声明的顺讯
- 如果静态变量是引用类型:那么将会 完整地初始化 引用的对象,而不是仅执行静态代码块、静态变量
例子:尝试判断一下下面所有的语句执行顺序
class Father{
{
System.out.println("父类代码块初始化...");
}
static {
System.out.println("父类静态代码块初始化...");
}
public Father()
{
System.out.println("父类初始化...");
}
}
class Brother{
public Brother()
{
System.out.println("兄弟类被初始化...");
}
}
class Son extends Father{
private static Brother brother = new Brother();
{
System.out.println("子类代码块初始化");
}
static {
System.out.println("子类静态代码块初始化...");
}
public Son() {
System.out.println("子类初始化...");
}
}
final
- 提高程序性能:
JVM
会将所有常量值存放在运行时常量池中(缓存) - 可以在多线程并发状态向安全地共享,不需要额外的同步机制:?
- 所有匿名内部类 、Lambda 表达式中的变量必须是常量:?
不可变变量
定义:采用关键字
final
修饰的变量特点:不可以对被修饰的变量进行 任何的修改(常量值)
如果不可变变量是 ==基本数据类型==:不可以进行任何修改
如果不可变变量是 ==引用类型==:引用不可以再指向其他的对象,对象本身是可以修改的
final int constant = 0;
final MyObject constantRef = new MyObject();/* constantRef 不可以指向其他对象了,但是依然可以调用类中的方法对对象进行修改 */
分类: 根据常量赋值的不同时期进行区分
编译时常量:定义常量时赋予其固定值
注:编译时常量在 编译阶段设置默认值,在连接过程的准备阶段赋值 (
JVM
相关)final int constant = 0; // 编译时常量
运行时常量:定义常量时 调用方法赋值 或者 构造方法 中赋值
(2) 利用构造器初始化常量值,该常量也被称为空白
final
final int number = new Random().nextInt(); // 等待随机方法被调用时才会真正为常量赋值:不会在类加载阶段就赋值,而是等到运行时才会
class MyObject{
private final int constant;
public MyObject(){
constant = 120;
}
}细节:
(1) 无论常量在什么时候赋值,在使用常量之前 必须被初始化并且赋值,否则编译报错
final int consVal; // 这种情况是不被编译器允许的:必须赋值
(2) 常量值既不属于 当前 类也不属于对象,属于运行该类方法的主类 (
JVM
相关)class ConstantClass
{
// 注意:这里是公共属性
public final static constantValue = "Hello World";
}
public class NotInitializationClass
{
public static void main(String[] args)
{
// 引用类的常量值
System.out.println(Constant.constantValue);
// 编译阶段通过常量传播优化,常量值并没有存储在 ConstantClass 的常量池中,而是存储在 NotInitializationClass 类中
}
}
静态常量:
定义:
static final
共同修饰的变量特点:普通常量值属于每个对象,静态常量仅属于类 / 每个对象的普通常量值都可以不同,每个对象的静态常量值一定相同
原因:静态变量在类加载阶段就已经被初始化完成了,但是对象还没有初始化所以所有的对象的拥有的静态常量一定相同(即使采用
random()
方法)
访问权限:
private
修饰的常量:仅可以在类的内部使用或者外部利用公共方法调用public
修饰的常量:这种常见于工具类中,便于其他的类使用(这种更加常见)
细节:编译时静态常量的命名方式必须采用 全部字母大写 和 下划线隔开 的方式
final static int CONSTANT_VALUE = 10;
不可变方法
定义:采用关键字
final
修饰的方法特点:
- 被修饰的方法不可以被 重写
- 被修饰的方法可以进行 重载
- 早期实现中
final
修饰的方法会允许编译器采用 内嵌调用 方式对其进行优化以提升效率(现在的实现中应该把优化交给 编译器和虚拟机 去执行)
细节:
private
方法被隐式地指定为final
方法解释:子类中定义的方法和父类中的一个
private
方法签名相同,那么子类方法不是重写父类方法,而是在子类中定义了一个新的方法方法中的形式参数也可以采用
final
修饰,相当于传入一个常量:常用于向匿名内部类传递参数
不可变类
- 定义:采用关键字
final
修饰的类 - 特点:
- 被修饰的类不可以被任何类继承,但是自己可以继承其他类
- 被修饰的类中的方法全部默认为
final
,成员变量可以采用final
修饰也可以不采用
- 细节:
String
类就是final
修饰的类
如果某个类被
fianl
修饰,但是我们依然需要 使用 这个类中的所有方法,那么应该怎么实现?
重排序
Object
定义:==所有类的父类==:无论是自定义类还是库中提供的类都会继承它
工具类:
Objects
方法
equals()
(1) 采用等号比较对象是否相同:实际比较的是两个对象的地址是否相同,大多数情况都是不相同的,一般需要重写
(2) 调用方法的对象可能为空:
Objects
中提供了equals
方法用于比较两个对象,两个对象都可以为空public boolean equals(Object obj) {
return (this == obj);
}
/*Objects 提供的方法 */
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}hashCode
(1) 计算哈希值的方法是本地方法:各种哈希表和类基本都是重写了这个方法
(2) 重写
hashCode()
方法时通常需要重写equals()
,否则很容易带来不一致的问题(详情见哈希表)(3) 等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价
public native int hashCode();
notify() & notifyAll() & wait()
public final native void notify(); // 唤醒线程池中的某个线程
public final native void notifyAll(); // 唤醒线程池中的所有线程
public final void wait() throws InterruptedException { // 让某个线程进入线程池等待
wait(0);
}clone
protected native Object clone() throws CloneNotSupportedException;