8、面向对象
1. 什么是面向对象
1.1 什么是面向过程?什么是面向对象?
面向过程(Procedure-Oriented Programming, OPP)
是一种以事物为中心的编程思想。主要关注“怎么做”,即完成任务的具体细节。
面向对象(Object-Oriented Programming, OOP)
是一种以对象为基础的编程思想。主要关注“谁来做”,即完成任务的对象。
1.2 面向对象和面向过程的优缺点对比
面向过程:
-
优点:效率高(不需要实例化对象)
-
缺点:耦合度高,扩展性差,不易维护
面向对象:
-
优点:耦合低(易复用),扩展性强,易维护
-
缺点:效率比面向过程低
大多数支持面向对象的语言,同时也支持面向过程。不论是 Java、PHP,还是 JS,它们都还无法完全面向对象,因为面向过程是必然的,面向过程代表着必要的程序流程,调动对象进行组合或对象内部能力的实现,都一定会存在“过程”,它最终还是需要通过拆分步骤来指导最具体的执行细节。
1.3 面向对象编程的本质
以类的方式组织代码,以对象的形式(组织 / 封装)数据。
1.4 面向对象三大特性
- 封装
- 继承
- 多态
2. 类与对象
类的五大成员:
- 属性
- 方法
- 构造器
- 代码块
- 内部类
2.1 类与对象的关系
类是一种抽象的数据类型,它是对某一类事物整体描述 / 定义,但是并不能代表某一个具体的事物。
对象是抽象概念的具体实例。
在 Java 中,一个项目可以有多个类,而每个类都可以有一个 main 方法。但是,只有一个 main 方法会成为程序的入口点,即只有一个 main 方法会被 JVM 执行。我们可以根据需要选择执行哪个类的 main 方法。
在一个 *.java
的文件中,只能有一个 public class
的声明,但是允许多个 class 的声明。
2.2 创建与初始化对象
使用 new
关键字创建对象
使用 new 关键字创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用。
IDEA 中使用快捷键快速创建对象:Alt + Enter
3. 构造器
类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的,是在创建对象的时候给成员变量进行初始化赋值的。
构造器特点:
- 必须和类的名字相同
- 必须没有返回类型,也不能写 void
- 没有具体的返回值
构造器类型:
- 无参构造方法(默认构造参数):根据类型为对象提供默认值。
- 有参构造方法:用于为不同对象提供不同初始化的值。
注意事项:
-
使用 new 关键字时,本质上是在调用构造器
-
一个类中可以有多个构造器,但是构造器的参数列表必须不同(重载)
-
如果没有手动定义构造器,则 Java 系统会提供一个默认的构造器,一旦定义了构造器,则系统会把默认的构造器收回
-
定义了有参构造之后,如果想使用无参构造,必须显式的定义一个无参构造
4. 创建对象内存分析
5. 封装
“高内聚,低耦合”
封装(数据的隐藏)
- 通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏。
记住这句话就够了:属性私有,get / set
封装的意义:
- 提高程序安全性,保护数据
- 隐藏代码的实现细节
- 统一接口
- 增加系统可维护性
6. 继承
继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模。
- 继承是类和类之间的一种关系。除此之外类和类之间的关系还有依赖、组合、聚合等。
- 继承关系的俩个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字
extends
来表示。
注意事项:
- Java 中类只有单继承,没有多继承!
- 被
final
修饰符修饰的类,不可以被继承。 - 父类中的 final 方法可以被子类继承,但是不能被子类重写。
利用 IDEA 快捷键
Ctrl + h
查看继承关系(hierarchy)。
访问修饰符:
修饰符 | 当前类 | 同一包内 | 子类(同一包) | 子类(不同包) | 其他包 |
---|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✓ | ✘ |
default | ✓ | ✓ | ✓ | ✘ | ✘ |
private | ✓ | ✘ | ✘ | ✘ | ✘ |
❗ 外部类只能被 public
和 default
两种权限修饰。
6.1 Object 类
Java 创建一个类时,如果没有明确继承一个父类,那么它就会自动继承 Object,成为 Object 的子类。
6.2 super
关键字
Java 中的 super 关键字在子类中用于访问父类成员(属性、方法和构造函数)
用法:
-
使用
super
关键字来访问父类的属性如果父类和子类具有相同的属性,则使用
super
来指定为父类的属性。 -
使用
super
关键字调用父类方法如果子类包含与父类相同的方法,则应使用
super
关键字指定父类的方法。 -
使用
super
关键字调用父类构造函数super()
只能在子类构造函数中使用,并且必须是第一条语句。即使在子类构造函数中没有使用
super()
,编译器也会隐式调用超类的默认构造函数。super()
和this()
不能够同时调用构造方法。
编译器可以自动调用无参数构造函数。但是,它不能调用有参构造函数。
如果必须从子类构造函数中调用父类的有参构造函数,则需要在子类构造函数中显式定义它。
6.3 方法重写
如果在父类和子类中都定义了相同的方法,则子类的方法将覆盖超类的方法,这称为方法重写。
重写规则:
- 父类和子类都必须具有相同的方法名称,相同的返回类型和相同的参数列表。
- 只能重写继承过来的方法,
private
修饰的方法无法被重写。 - 不能重写声明为 final 和 static 的方法。
- 父类的非静态方法不能被子类重写为静态方法。
- 访问权限不能比父类中被重写的方法的访问权限更低。
- 重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。
- Java 中的构造函数不会被继承。因此,在 Java 中不存在诸如构造函数重写之类的问题。
- 如果一个类继承了抽象类,抽象类中的抽象方法必须在子类中被重写。
- 返回类型可以相同,也可以不同,如果不同的话,子类重写后的方法返回类型必须是父类方法返回类型的子类型。
6.4 多态
所谓多态,就是指程序中定义的引用变量所指向的具体类型和通过该引用变量调用的方法在编程时并不确定,而是在程序运行期间才确定。
实现多态的条件:
- 继承:必须存在继承关系
- 重写:必须存在方法重写
- 向上转型:父类引用指向子类对象
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态成员的特点:
- 多态成员变量:编译运行看左边
- 多态成员方法:编译看左边,运行看右边
Java 多态有两种情况:重载和重写
1、在重载中,运用的是静态多分派,即根据静态类型确定对象,不是根据 new 的类型确定调用的方法;
2、在重写中,运用的是动态单分配,是根据 new 的类型确定对象,从而确定调用的方法
多态的转型分为向上转型和向下转型两种:
-
向上转型:多态本身就是向上转型的过程
使用格式:
父类类型 变量名 = new 子类类型();
-
向下转型:一个已经向上转型的子类对象可以使用强制类型转换的格式,将父类引用类型转为子类引用各类型
使用格式:
子类类型 变量名 = (子类类型) 父类类型的变量;
instanceof 关键字:
- 作用:测试它左边的对象是否是它右边的类的实例
object instanceof class
6.5 static
关键字
静态代码块
Java中,静态代码块是写在类中,使用 static
关键字修饰的一段代码块。
-
在类加载时执行,且只执行一次!
-
语法格式:
1
2
3static {
// 代码块
}
匿名代码块
匿名代码块是在类中一段代码。
-
每次 new 创建新的对象都会执行一次匿名代码块。
-
语法格式:
1
2
3{
// 代码块
}
在程序中,代码块与类的构造方法执行先后顺序:静态代码块 > 匿名代码块 > 类的构造方法
执行顺序:
(1)执行父类的静态成员(变量和代码块,执行顺序根据编写的代码顺序执行)
(2)执行子类的静态成员(变量和代码块,执行顺序根据编写的代码顺序执行)
(3)执行父类的非静态成员(变量和代码块,执行顺序同上)
(4)执行父类的构造函数
(5)执行子类的非静态成员(变量和代码块,执行顺序同上)
(6)执行子类的构造函数
静态导包
静态导包就是 Java 包的静态导入,是 JDK 1.5 中的新特性。
语法格式:
1 | import static package.className.fieldName|methodName; |
静态导入似乎可以帮助使用静态成员的简单名称来简化程序的编写和读取。示例如下:
1 | import static java.lang.System.*; |
6.6 抽象类
abstract
修饰符可以用来修饰方法也可以修饰类。
- 如果修饰方法,那么该方法就是抽象方法
- 如果修饰类,那么该类就是抽象类
注意事项:
- 抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类。
- 抽象类不能使用
new
关键字来创建对象,它是用来让子类继承的。 - 子类继承抽象类,那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类。
- 抽象方法只有方法的声明,没有方法的实现,它是用来让子类实现的。
抽象类中存在构造方法!
6.7 接口
接口是抽象方法的集合,接口通常以 interface
声明。
语法格式:
1 | [访问修饰符] interface 接口名称 [extends 其他接口] { |
接口特性:
- 接口没有构造方法。
- 接口无法被实例化,但是可以被实现。
- 接口是隐式抽象的,当声明一个接口的时候,不必使用
abstract
关键字。 - 接口中每一个方法也是隐式抽象的,方法会被隐式的指定为
public abstract
。 - 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。
- 一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。
- 接口中可以含有变量,但是接口中的变量会被隐式的指定为
public static final
。 - 一个接口不能实现(implements)另一个接口,但它可以继承多个其它的接口。
一个类可以通过 implements
关键字来实现一个或多个接口。
语法格式:
1 | [修饰符] class 类名 [extends 父类名] [implements 接口名1, 接口名2, ...] { |
6.8 内部类
在 Java 中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。
要访问内部类,可以通过创建外部类的对象,然后创建内部类的对象来实现。
内部类可以随意使用外部类的成员变量(包括私有)而不用生成外部类的对象,这也是内部类的唯一优点。
访问内部类本身的成员变量可用 this.属性名
,访问外部类的成员变量需要使用 Out.this.属性名
。
如果一个内部类只希望被外部类中的方法操作,那么可以使用 private
声明内部类。
内部类分类:
- 定义在外部类的局部位置上(如方法内):
- 局部内部类(有类名)
- 匿名内部类(没有类名)
- 定义在外部类的成员位置上:
- 成员内部类(没用
static
修饰) - 静态内部类(使用
static
修饰)
- 成员内部类(没用
-
局部内部类
局部内部类是定义在一个方法或者一个作用域中的类
语法格式:
1
2
3
4
5
6
7public class Outer {
public void method {
class Inner {
}
}
}特点:
- 可以直接访问外部类的所有成员,包括私有的
- 局部内部类不能有访问修饰符,不能被定义为
static
- 局部内部类不能定义
static
成员 - 局部内部类可以使用
Outer.this
语法指定访问外部类成员 - 局部内部类想要使用方法或域中的局部变量,该变量必须是
final
的 - 局部内部类只能在类所在的方法中用
-
匿名内部类
匿名内部类是定义在外部类的局部位置(比如方法中),并且没有类名。
特点:
-
匿名内部类既是一个类的定义,同时它本身也是一个对象
-
可以直接访问外部类的所有成员,包含私有的
-
不能添加访问修饰符和
static
修饰符 -
匿名内部类需要依托于其他类或者接口来创建
-
匿名内部类的声明必须是在使用new关键字的时候
-
-
成员内部类
成员内部类是定义在类内部,作为类的成员的类。
语法格式:
1
2
3
4
5public class Outer {
public class Inner {
}
}特点:
- 成员内部类可以被访问修饰符所修饰
- 成员内部类可以访问外部类的所有成员(包括
private
成员和静态成员) - 成员内部类不可以定义
static
成员
访问要求:
- 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员,可以使用
外部类.this.成员
指定访问外部类成员 - 在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问
- 如果要创建成员内部类的对象,前提是必须存在一个外部类的对象
创建语法:
1
2
3
4Outer o = new Outer();
Outer.Inner i = o.new Inner();
或
Outer.Inner i = new Outer().new Inner(); -
静态内部类
只有成员内部类才能加
static
变成静态内部类。特点:
- 直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
- 可以添加任意访问修饰符
- 因为内部类被静态化,因此
Outer.Inner
可以当做一个整体看,可以直接new
出内部类的对象
创建语法:
1
Outer.Inner i = new Outer.Inner();