文章目录
一、简介
面向对象(Object Oriented)是一种软件开发方法、编程范式。 面向对象是相对于面向过程来讲的,面向对象是指将事物相关的数据和方法组织为一个整体来看待,这种编程方式相较于面向过程,更贴近人的思维习惯。
面向对象的三大基本特征是:封装、继承、多态
- 封装:隐藏了类的内部实现,可以在不影响使用者的前提下改变类的内部结构,同时保护了数据。
- 继承:产生了子类和父类的关系,子类可以复用或扩展父类的代码,另外继承是实现多态的前提。
- 多态:指的是一个行为有多种形态,让对象在运行期自动决定执行何种行为,通过将子类对象引用赋值给父类对象来实现动态方法调用。
二、面向过程和面向对象
在编程语言中,面向过程和面向对象是两种不同的编程范式,在 Java 等编程语言中均有广泛应用,它们具有以下特点:
2.1 面向过程
面向过程编程的核心关注点在实现功能的步骤上,强调的是解决问题的步骤和顺序,把程序看作是一系列函数的集合,通过依次调用这些函数来完成任务。
注重程序的执行流程,按照顺序一步一步地执行代码。数据和操作数据的函数通常是分离的,数据在不同的函数之间传递。对于简单的任务,编程效率可能较高,因为代码直接围绕问题的解决步骤展开。
2.2 面向对象
面向对象编程的核心关注点在实现功能需要哪些对象的参与,将数据和操作数据的方法封装在对象中。把程序看作是一组相互交互的对象的集合,通过对象之间的消息传递来完成任务。
强调对象的封装性、继承性和多态性。
- 封装性使得对象的内部状态和行为被隐藏起来,外部只能通过特定的接口进行访问,提高了代码的安全性和可维护性。
- 继承性允许子类继承父类的属性和方法,实现代码复用。
- 多态性使得同一操作可以作用于不同类型的对象,产生不同的行为,提高了代码的灵活性和可扩展性。
2.3 总结
代码组织方式
面向过程编程通常以函数为单位组织代码,函数之间的调用关系较为明确。
面向对象编程以类为单位组织代码,类之间通过继承、组合等关系相互关联。
可维护性
面向对象编程由于封装性和继承性等特点,使得代码的可维护性更高。当需要修改代码时,只需要在相应的类中进行修改,而不会影响其他不相关的部分。
面向过程编程中,如果一个函数的修改可能会影响到其他多个函数,维护起来相对困难。
可扩展性
面向对象编程的继承性和多态性使得代码的可扩展性更强。可以通过继承现有类并添加新的功能来扩展程序,而不需要修改已有代码。
面向过程编程在扩展功能时,可能需要修改多个函数,容易引入新的错误。
适用场景
面向过程编程适用于简单的、流程较为固定的任务,例如,操作系统内核、嵌入式系统等。
面向对象编程适用于复杂的、需要不断扩展和维护的系统,例如,企业级应用、游戏开发等。
三、类和对象
在编程语言中,类和对象是面向对象编程的基本组成单元,它们是抽象和具体的关系,具体如下:
3.1 类
类指的是在现实世界中,将事物与事物之间的共同的状态和行为提取出来,形成了一个模版,这个模版就叫做类。
// 定义学生类
public class Student {
// 成员属性
public String name;
// 成员方法
public void printName() {
System.out.println("姓名:" + this.name);
}
}
3.2 对象
对象是通过类的实例化创建出来的,对象是类的具体个体,对象又叫做实例。
public class Student {
// 成员属性
public String name;
// 成员方法
public void printName() {
System.out.println("姓名:" + this.name);
}
// 测试
public static void main(String[] args) {
// 创建学生类
Student student = new Student();
// 调用属性
student.name = "张三";
// 调用方法
student.printName();
}
}
运行结果:
姓名:张三
四、扩展内容
4.1 成员变量
定义在方法的外部,类的内部,成员变量的作用范围是整个类,不需要初始值,在创建对象时,存储在堆内存当中。
4.2 局部变量
定义在方法的内部或者某一个语句块的内部,局部变量的作用范围仅限于方法内或者语句块内,必须有初始值,否则编译器报错,存储在栈内存中。
4.3 成员方法
成员方法也被称为方法,定义在类的内部,可以将实现的细节封装起来,方便调用,提高代码的复用性,成员方法存储在方法区中。
4.4 this
代表的是一个引用,该引用保存的是被引用对象的内存地址,通过这个内存地址可以访问成员变量和成员方法,在大部分的情况下都可以省略,但是在用于区分局部变量和成员变量时不能省略,不然编译器报错。
public class Student {
//成员变量
public String name;
public int age;
public Student(String name, int age) {
//s1调用了构造方法,this存的是s1的引用
this.name = name;
this.age = age;
}
// 测试
public static void main(String[] args) {
// 创建对象
Student s1 = new Student("张三", 18);
System.out.println("姓名:" + s1.name + "\n年龄:" + s1.age);
}
}
运行结果:
姓名:张三
年龄:18
- 还可以用来调用同一类中的其他构造方法,可以实现代码复用,只能在构造方法的第一行
public class Student {
//成员变量
public String name;
// 无参构造
public Student() {
System.out.println("无参构造执行了");
}
// 有参构造
public Student(String name) {
// 调用无参构造
this();
this.name = name;
}
// 测试
public static void main(String[] args) {
// 创建对象,这里是用有参构造初始化对象的
Student s1 = new Student("张三");
System.out.println("姓名:" + s1.name);
}
}
运行结果:
无参构造执行了
姓名:张三
4.5 构造方法
构造方法用于创建对象并初始化对象的状态。当创建对象时,如果没有有参构造方法的话,默认是直接调用对象的无参构造方法,并且会将该对象的成员属性初始化,具体特点如下:
- 方法名必须和类名相同;
- 没有返回值;
- 不能被 static 修饰;
- 可以具有不同的访问修饰符;
- 可以方法重载;
- 当一个类中没有写构造方法时,系统会默认给该类一个默认的无参构造方法。当自己定义构造方法后默认的构造方法就不存在了。
public class Student {
// 成员属性
public String name;
public int age;
// 无参构造方法
public Student() {
System.out.println("------无参构造方法------");
name = "张三";
age = 18;
}
// 有参构造方法
public Student(String name, int age) {
System.out.println("------有参构造方法------");
this.name = name;
this.age = age;
}
// 测试
public static void main(String[] args) {
//创建对象
Student s1 = new Student();
System.out.println("姓名:" + s1.name + "\n年龄:" + s1.age);
Student s2 = new Student("李四", 20);
System.out.println("姓名:" + s2.name + "\n年龄:" + s2.age);
}
}
运行结果:
------无参构造方法------
姓名:张三
年龄:18
------有参构造方法------
姓名:李四
年龄:20
五、封装
封装是 Java 面向对象编程的三大特性之一,它强调将数据和操作数据的方法封装在一个类中,对外隐藏内部实现细节。私有化属性是实现封装的关键步骤,同时限制这些属性的访问权限,只通过特定的方法来进行访问和修改。
5.1 实现步骤
- 将对象的属性私有化,隐藏内部的实现细节,防止外部类恶意的访问对象的属性;
- 由于上述属性被私有化了,所以必须提供一个对外的方法,来修改这些被私有化的属性,又因为是方法,所以可以在方法内添加访问逻辑和数据验证,这样即可以保证数据的完整性,也可以降低代码耦合度。
5.2 样例代码
对于下面的样例代码,希望大家认真思考,想想有封装和没有封装的区别,以及如何理解保证数据的完整性、降低代码耦合度。
public class Person {
// 第一步:将属性私有化
private String name;
private int age;
// 第二步:提供对外的方法,可以按 alt + insert 快速生成 getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
// 年龄要在 0 - 120 岁区间
if (age > 0 && age <= 120) {
this.age = age;
} else {
System.out.println("年龄不合法");
}
}
// 测试
public static void main(String[] args) {
// 创建对象p1
Person p1 = new Person();
p1.setName("张三");
p1.setAge(20);
System.out.println("姓名:" + p1.getName() + "\n年龄:" + p1.getAge());
System.out.println("==========");
// 创建对象p2
Person p2 = new Person();
p2.setName("李四");
p2.setAge(-20);
System.out.println("姓名:" + p2.getName() + "\n年龄:" + p2.getAge());
}
}
运行结果:
姓名:张三
年龄:20
==========
年龄不合法
姓名:李四
年龄:0
六、继承
继承是 Java 面向对象编程的三大特性之一,继承是一种机制,允许一个类(子类)继承另一个类(父类)的属性和方法,并且子类可以在父类的基础上进行扩展和特化,添加新的属性和方法,或者重写父类的方法以实现不同的行为。在 Java 中,通过 extends 关键字,实现继承关系,采用的是单继承。而在 C++ 中,采用的是多继承。
6.1 优缺点
继承的优点
-
代码复用:子类可以继承父类的属性和方法,避免了重复编写相同的代码。这大大提高了开发效率,减少了代码量。
-
层次结构清晰:继承可以建立清晰的类层次结构,使代码更易于理解和维护。相关的类可以组织在一起,形成一个有层次的体系。
-
多态性:继承是实现多态的前提之一,通过继承,不同的子类对象可以对同一方法有不同的实现,从而实现多态性。
-
扩展性强:子类可以在父类的基础上进行扩展,添加新的属性和方法,满足特定的需求。这种扩展性使得代码能够适应不断变化的需求。
继承的缺点
-
复杂性:当继承层次过多时,代码的复杂性会增加,理解和维护起来会变得困难。而且,深层次的继承关系可能会导致性能问题。
-
限制了灵活性:一旦一个类继承了另一个类,就受到了父类的约束。在某些情况下,这可能会限制子类的灵活性,使得子类难以进行独立的扩展和修改。
-
使用场景:并不是所有的类之间的关系都适合用继承来表示。
6.2 this 和 super
super 关键字体现了 Java 中类的继承层次结构。它使得子类能够在自身的实现中,有选择地调用父类的功能,同时进行扩展和定制。
关键字 | 对象不同 |
---|---|
this | 指代当前对象,可以使用 this 来访问该对象的成员变量和成员方法。 |
super | 指代父类对象,在子类中使用 super 可以访问父类的成员变量、成员方法和调用父类的构造方法。 |
关键字 | 用法不同 |
---|---|
this | 主要用于区分局部变量和成员变量,当方法中的局部变量与成员变量同名时。 |
super | 主要用于在子类中调用父类的构造方法,访问父类被重写的方法,访问父类的成员变量。 |
总之,this 和 super 在 Java 中分别用于指代当前对象和父类对象,它们在不同的场景下发挥着重要的作用,帮助开发者实现面向对象编程中的封装、继承等特性。
⚠️需要注意的是 super 并不是严格意义上指代父类对象的,它只是父类对象存储空间的一个标识。由于它不能像使用对象引用一样,来独立地调用父类中所有的方法或访问所有的成员,只能在特定的上下文中,如调用父类构造方法、访问被重写的方法或父类的成员变量。但为了方便理解,你可以认为 super 指代的是父类对象,因为大多情况,使用 super 的效果类似于在操作父类对象,但要清楚它的本质是在子类中用于特定目的的关键字,与真正的父类对象引用有很大的区别。
6.3 访问变量特点
在 Java 中,访问成员变量都是遵循就近原则,谁离得近就调用谁,如果子类没有定义 name 属性,就会调用父类的 name,以此类推。
// 父类:Animal
public class Animal {
public String name = "动物";
}
// 子类:Dog,通过 extends 关键字,实现继承关系
public class Dog extends Animal{
public String name = "中华田园犬";
public void show() {
name = "小土狗";
System.out.println(name);
}
}
// 创建测试类:Test,测试结果
public class Test {
public static void main(String[] args) {
// 创建对象
Dog dog = new Dog();
// 调用对象的方法
dog.show();
}
}
运行结果:
小土狗
6.4 访问成员方法特点
在 Java 中,访问成员方法都是遵循就近原则,谁离得近就调用谁,如果子类没有定义 show 方法,就会调用父类的 show,以此类推。
// 父类:Animal
public class Animal {
public void show() {
System.out.println("Animal 父类的 show() 执行了...");
}
}
// 子类:Dog
public class Dog extends Animal {
public void show() {
System.out.println("Dog 子类的 show() 方法执行了...");
}
}
// 测试类:Test
public class Test {
public static void main(String[] args) {
// 创建对象
Dog d1 = new Dog();
// 调用对象的成员方法
d1.show();
}
}
运行结果:
Dog 子类的 show() 方法执行了...
6.5 访问构造方法特点
在子类中,所有的构造方法都会隐式通过 super() 访问父类中无参的构造方法,并且必须在构造方法的第一行。因为子类可能会访问父类的属性,如果在父类还没有初始化就去访问的话,编译器就会报错,所以在子类初始化前必须先完成父类属性的初始化,这也是为什么要在子类构造方法第一行去调用父类的无参构造,目的就是为了初始化父类的数据。
// 父类:Animal
public class Animal {
public Animal() {
System.out.println("Animal 父类的无参构造被调用了...");
}
}
// 子类:Dog,通过 extends 关键字,实现继承关系
public class Dog extends Animal {
public String name;
public Dog() {
System.out.println("Dog 子类的无参方法被调用了...");
}
public Dog(String name) {
System.out.println("Dog 子类的有参方法被调用了...");
}
}
// 测试类:Test,测试结果
public class Test {
public static void main(String[] args) {
// 调用子类的无参构造创建对象
Dog d1 = new Dog();
System.out.println("==========");
// 调用子类的有参构造创建对象
Dog d2 = new Dog("小土狗");
}
}
运行结果:
Animal 父类的无参构造被调用了...
Dog 子类的无参方法被调用了...
==========
Animal 父类的无参构造被调用了...
Dog 子类的有参方法被调用了...
6.6 方法重写
当子类中出现了和父类中一模一样的方法签名(返回值、方法名、参数列表),用于当子类需要父类的方法时,而父类方法不满足子类的需求,就可以在子类中使用方法重写,对父类的方法进行扩展。
⚠️@Override:这是一个用于标识当前方法是被重写的方法的一个注解,可以帮助我们检查重写方法的方法声明的正确性。
// 父类:Animal
public class Animal {
public void show() {
System.out.println("我是一只动物");
}
}
// 子类:Dog
public class Dog extends Animal {
@Override
public void show() {
System.out.println("我是一只小狗");
}
}
// 测试类:Test
public class Test {
public static void main(String[] args) {
// 创建对象
Dog d1 = new Dog();
// 调用对象的成员方法
d1.show();
}
}
运行结果:
我是一只小狗
⚠️注意:私有方法不能被重写,子类方法访问权限不能更低比父类的访问权限更低。
6.7 继承中的注意事项
单一职责原则:在设计继承关系时,应遵循单一职责原则,即一个类应该只有一个引起它变化的原因。避免让一个类承担过多的职责,导致继承层次过于复杂。
避免过度继承:过度使用继承可能会导致代码的复杂性增加,并且可能会出现继承层次过深、难以理解和维护的问题。在某些情况下,可以考虑使用组合或接口来替代继承。
方法重写的规则:当子类重写父类的方法时,需要遵循方法签名一致,并且父类方法不能被私有化、子类方法的访问修饰符不能比父类方法更严格。
构造方法的调用顺序:在创建子类对象时,会先调用父类的构造方法,然后再调用子类的构造方法。这确保了父类的属性得到正确的初始化。
七、多态
多态是 Java 面向对象编程的三大特性之一,即同一操作对于不同的对象可以有不同的表现形式,简单来说,就是允许不同类型的对象对同一消息作出不同的响应。
7.1 优缺点
多态的优点
-
提高代码的可扩展性和可维护性:当需要增加新的子类时,只需要实现相应的方法,而不需要修改现有的代码。这使得代码更加灵活,能够适应不断变化的需求。
-
增强代码的通用性:可以使用父类类型的引用来处理不同子类的对象,提高了代码的通用性,这样可以减少代码的重复,提高开发效率。
-
降低代码的耦合度:多态使得代码依赖于抽象而不是具体的实现,减少了不同部分代码之间的依赖关系。这有助于提高代码的可维护性和可测试性。
-
实现开闭原则:开闭原则是面向对象编程中的一个重要原则,即对扩展开放,对修改关闭。多态可以帮助实现开闭原则,因为可以通过添加新的子类来扩展功能,而不需要修改现有的代码。
多态的缺点
-
运行时类型检查可能导致性能开销:由于多态是在运行时根据对象的实际类型来决定调用哪个方法,这可能会导致一些运行时类型检查的开销。特别是在频繁调用多态方法的情况下,可能会对性能产生一定的影响。不过,在大多数情况下,这种性能开销是可以忽略不计的,而且当前版本的 Java 虚拟机也在不断优化多态的实现,以减少性能损失。
-
代码理解和调试可能变得复杂:多态使得代码的行为更加动态和灵活,但也可能使得代码的理解和调试变得更加困难。特别是当代码中存在复杂的多态关系时,可能需要花费更多的时间来理解代码的执行流程。在调试多态代码时,需要注意对象的实际类型和方法的调用关系,以确保代码的正确性。
-
可能导致意外的行为:如果不恰当地使用多态,可能会导致意外的行为。例如,如果子类没有正确地重写父类的方法,或者在多态方法中没有正确地处理不同的对象类型,可能会导致程序出现错误或异常。
7.2 实现前提
实现多态的前提是,类与类之间存在继承关系,然后子类重写父类的方法,最后父类引用指向子类对象,当通过父类引用调用这个被重写的方法时,实际执行的是子类中的方法实现,这就是多态
7.3 访问成员变量
在多态中访问成员变量遵循编译看左边,运行也看左边。当代码被编译时,会看父类中有没有这个变量,如果有则编译成功,没有则编译失败,运行时,实际返回的是父类中的成员变量。
// 测试类
public class Test {
public static void main(String[] args) {
// 多态方式创建对象
Animal d = new Dog();
Animal c = new Cat();
// 访问成员变量
System.out.println(d.name);
System.out.println(c.name);
}
}
// 父类:Animal
class Animal {
String name = "动物";
}
// 子类:Dog
class Dog extends Animal {
String name = "小狗";
}
// 子类:Cat
class Cat extends Animal {
String name = "小猫";
}
运行结果:
动物
动物
7.4 访问成员方法
在多态中访问成员方法遵循编译看左边,运行也看右边。当代码被编译时,会看父类中有没有这个方法,如果有则编译成功,没有则编译失败,运行时,实际返回的是子类中的成员方法。
// 测试类
public class Test {
public static void main(String[] args) {
// 多态方式创建对象
Animal d = new Dog();
Animal c = new Cat();
// 访问成员方法
d.show();
c.show();
}
}
// 父类:Animal
class Animal {
public void show() {
System.out.println("我是一只动物");
}
}
// 子类:Dog
class Dog extends Animal {
@Override
public void show() {
System.out.println("我是一只小狗");
}
}
// 子类:Cat
class Cat extends Animal {
@Override
public void show() {
System.out.println("我是一只小猫");
}
}
运行结果:
我是一只小狗
我是一只小猫
7.5 多态的转型
向上转型
向上转型指的是子类对象赋值给父类引用,通过向上转型,可以实现多态。在运行时,根据实际对象的类型来决定调用哪个方法,向上转型是安全的,因为子类总是包含父类的所有成员和方法。但是,这种转型方式,只能访问父类中已有的成员,不能直接访问子类特有的成员。
// 测试类
public class Test {
public static void main(String[] args) {
// 多态方式创建对象
Animal d = new Dog();
Animal c = new Cat();
// 访问成员变量
System.out.println(d.name);
System.out.println(c.name);
}
}
// 父类:Animal
class Animal {
String name = "动物";
}
// 子类:Dog
class Dog extends Animal {
String name = "小狗";
}
// 子类:Cat
class Cat extends Animal {
String name = "小猫";
}
运行结果:
动物
动物
向下转型
向下转型指的是父类引用转换为子类引用,在某些情况下,我们需要调用子类特有的方法或访问子类特有的属性,这时就需要进行向下转型。但是向下转型是不安全的,因为父类引用不一定总是指向子类对象。如果进行了错误的向下转型,会在运行时抛出 ClassCastException 异常,在进行向下转型之前,应该先进行类型检查,以确保父类引用实际指向的是子类对象,使用 instanceof 运算符进行类型检查。
// 测试类
public class Test {
public static void main(String[] args) {
// 多态方式创建对象
Animal d = new Dog();
Animal c = new Cat();
// 访问子类特有成员方法
if (d instanceof Dog) {
Dog dog = (Dog) d;
dog.eat();
} else if (c instanceof Cat) {
Cat cat = (Cat) c;
cat.eat();
}
}
}
// 父类:Animal
class Animal {
public void show() {
System.out.println("我是一只动物");
}
}
// 子类:Dog
class Dog extends Animal {
@Override
public void show() {
System.out.println("我是一只小狗");
}
public void eat() {
System.out.println("狗狗吃骨头");
}
}
// 子类:Cat
class Cat extends Animal {
@Override
public void show() {
System.out.println("我是一只小猫");
}
public void eat() {
System.out.println("猫猫吃小鱼");
}
}
运行结果:
狗狗吃骨头
在 Java 14 中,instanceof 有一个新特性,建议简化上述代码,具体如下:
// 测试类
public class Test {
public static void main(String[] args) {
// 多态方式创建对象
Animal d = new Dog();
Animal c = new Cat();
// 访问子类特有成员方法
if (d instanceof Dog dog) {
dog.eat();
} else if (c instanceof Cat cat) {
cat.eat();
}
}
}
// 父类:Animal
class Animal {
public void show() {
System.out.println("我是一只动物");
}
}
// 子类:Dog
class Dog extends Animal {
@Override
public void show() {
System.out.println("我是一只小狗");
}
public void eat() {
System.out.println("狗狗吃骨头");
}
}
// 子类:Cat
class Cat extends Animal {
@Override
public void show() {
System.out.println("我是一只小猫");
}
public void eat() {
System.out.println("猫猫吃小鱼");
}
}
运行结果:
狗狗吃骨头
7.6 多态的绑定
静态绑定
静态多态也叫编译时多态 ,通常指的是方法重载(Overload),在编译时就能够确定要调用的具体方法。
public class Test {
public int sum(int a, int b) {
return a + b;
}
public double sum(double a, double b) {
return a + b;
}
public int sum(int a, int b, int c) {
return a + b + c;
}
}
动态绑定
动态多态也叫运行时多态,通常指的是方法重写(Override),在运行时根据对象的实际类型来决定调用哪个具体的方法实现,通过方法重写和父类引用指向子类对象来实现。
// 父类:Animal
class Animal {
public void show() {
System.out.println("Animal 父类的 show() 执行了...");
}
}
// 子类:Dog
class Dog extends Animal {
@Override
public void show() {
System.out.println("Dog 子类的 show() 执行了...");
}
}
// 子类:Cat
class Cat extends Animal {
@Override
public void show() {
System.out.println("Cat 子类的 show() 执行了...");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 创建Dog实现类
Animal dog = new Dog();
dog.show();
// 创建Cat实现类
Animal cat = new Cat();
cat.show();
}
}
运行结果:
Dog 子类的 show() 执行了...
Cat 子类的 show() 执行了...
尽管在编译时,dog 和 cat 的引用类型都是 Animal ,但在运行时,实际执行的 show() 方法分别是 Dog 类和 Cat 类中实现的方法,这就是动态多态的体现。