本文介绍面向对象的三个基本特征:封装、继承、多态,以及常见问题如:父类子类初始化顺序,接口和抽象类,对象和引用等。
面向对象和面向过程的区别
面向过程
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
优点:性能比面向对象高。单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发
缺点:和面向对象相比,维护、复用、扩展比较困难。
面向对象
面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,这个盒子里有数据也有解决问题的方法。
需要什么功能直接使用就可以了,不需要关心这个功能是如何实现的
优点:面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:因为因为类调用时需要实例化,开销比较大。所以性能没有面向过程高。
封装
封装就是把客观的事物封装成抽象的类。隐藏属性和实现细节,仅对外提供访问方式。便于使用,也提高了安全性
继承
继承是以已经存在的类为基础,实现一个新类。新类可以增加新的数据和功能,也可以使用父类的功能,
还可以对父类功能的实现方式进行修改
多态
多态是指程序的引用变量所指向的具体类型和通过该引用变量调用的方法调用在编程时并不确定,
在程序运行期间才确定。这样,不用修改代码,就可以让引用变量绑定到各种不同的对象上,让该引用调用的具体方法随之改变,
让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,
通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的(继承和重写),也就是我们所说的多态性。
常见问题
接口和抽象类,继承和实现
区别 | 接口 | 抽象类 |
---|---|---|
自身控制符 | 可以为 public 和默认(包级) | 外部抽象类不能使用 static 声明,内部抽象类可以使用 static 声明 使用 static 声明的内部抽象类相当于一个外部抽象类 |
成员变量 | 只有静态变量,且只能用public final static修饰(默认)。 声明时,必须初始化 | 可以有普通变量和静态变量 |
成员方法 | 只能用 public abstract 修饰(默认) | 抽象方法不能用 private 修饰 抽象类可不含抽象方法 可以包含 main 方法,执行抽象类,但不可实例化 |
构造方法 | 不能有构造方法 | 可以定义构造方法 |
方法体 | 不能有 | 可以有 |
初始化块 | 不能有 | 可以有 |
继承 | 一个接口可以继承多个接口 | 抽象类可以继承实体类,实体类必须有明确的构造函数(默认构造方法就行) |
实现 | 一个类可以实现多个接口 | 抽象类可以可以实现多个接口,方法可加 abstract 修饰,不必全部实现 |
重写和重载
方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
重写和重载的区别:
区别 | 重载方法 | 重写方法 |
---|---|---|
参数列表 | 必须修改 | 不能修改 |
返回类型 | 可以修改 | 不 能修改 |
异常 | 可以修改 | 可以减少或删除,不能抛出新的或者更广的异常 |
访问 | 可以修改 | 一定不能做更严格的限制(可以降低限制) |
父类引用、子类引用、父类对象和子类对象
// 父类
class Person
{
public String name;
Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
// 子类
class Student extends Person
{
public String name;
public Student(String name, String name1)
{
super(name);
this.name = name1;
}
@Override
public String getName()
{
return name;
}
}
public class Test
{
public static void main(String[] args)
{
Student s1 = new Student("Father", "Child");
Person p1 = new Student("Father", "Child");
// 当父类引用变量指向子类对象时,优先调用子类对象的成员方法
System.out.println(p1.getName()); // Child
System.out.println(s1.getName()); // Child
// 成员变量不会覆盖,所以调用引用变量自身类型的,和引用对象的类型无关
System.out.println(p1.name); // Father
System.out.println(s1.name); // Child
}
}
我们可以得到以下结论:
成员变量:通过多态调用的成员变量,无论是普通类型,静态类型,常量类型,
仍是父类的成员变量(即和引用对象类型无关,和引用变量类型相关),因为成员变量不存在override(覆盖)问题
成员方法:通过多态调用的成员方法一般仍是父类中的方法,若在相应子类中被override(覆盖),则调用子类重写的方法。
静态方法:通过多态调用的静态方法仍是父类的静态方法(即和引用对象类型无关,和引用变量类型相关)。
父类子类中不同内容的初始化顺序
定义两个类,Father 和 Son,Son 继承了 Father:
class Father {
static {
System.out.println("Father的静态代码块");
}
{
System.out.println("Father的非静态代码块");
}
public Father() {
System.out.println("Father的构造方法");
}
}
public class Son extends Father{
static{
System.out.println("Son的静态代码块");
}
{
System.out.println("Son的非静态代码块");
}
public Son(){
System.out.println("Son的构造方法");
}
public static void main(String[] args) {
// 创建两个实例化对象
Son son = new Son();
Son son2 = new Son();
}
}
运行代码,可知,创建第一个实例化对象时,执行顺序为:
- Father的静态代码块
- Son的静态代码块
- Father的非静态代码块
- Father的构造方法
- Son的非静态代码块
- Son的构造方法
创建第二个实例化对象时,执行顺序为:
- Father的非静态代码块
- Father的构造方法
- Son的非静态代码块
- Son的构造方法
静态方法块只执行一次
静态方法块在类加载时被执行,所以,只要类含有main函数,不管是否生成生成相应的对象,静态代码块都会执行