在编程学习的早期阶段,许多人首先接触的是C语言。作为一种经典的面向过程语言,C语言为理解编程基础概念提供了坚实的基础。然而,在处理大型或复杂的项目时,面向过程的语言可能会导致诸多挑战,尤其是在代码管理和维护方面。
随着项目规模的扩大,面向对象的编程语言成为了更合适的选择。这类语言通过封装、继承和多态等概念,提供了更高的代码可重用性和更强的模块化能力。Java语言就是其中的典型代表。它不仅支持面向对象的核心原则,而且广泛应用于各种规模的软件开发项目中。因此,在本节课中,我们将重点讲解Java语言中的面向对象编程。这将帮助大家深入理解并熟练运用面向对象的概念和技巧。
在开始本课程之前,请确保你已经熟悉Java的基本语法,这将为理解更高级的概念奠定基础。通过这种方式,我们可以更有效地掌握面向对象编程,进而提高软件开发的质量和效率。
面向对象引入
想要学习面向对象,首先要清楚地认识到面向对象与面向过程有什么区别,弄明白了这一点,才能彻底理解面向对象到底好在哪
面向对象与面向过程
此处将通过一个例子带大家了解何为面向过程,何为面向对象。
大家想想这样一个场景,早上起床从宿舍骑自行车前往教学楼,这个场景如何通过编程语言进行描述呢?
面向过程:
- 首先实现人从床上起来的逻辑 – 需要很多行代码
- 实现下楼的逻辑 – 需要很多行代码
- 接下来实现上车逻辑 – 需要很多行代码
- 实现骑自行车逻辑 – 需要很多行代码
- 然后实现下车逻辑 – 需要很多行代码
- 实现上楼逻辑 – 需要很多行代码
- 最后实现进入教室逻辑 – 需要很多行代码
面向对象:
- 分别制造一个人对象,床对象,楼梯对象,自行车对象,教室对象 – 需要很多行代码
- 床对象.下床(人对象) – 仅需一行代码
- 楼梯对象.下楼(人对象) – 仅需一行代码
- 自行车对象.上车(人对象) – 仅需一行代码
- 自行车对象.骑行(人对象) – 仅需一行代码
- 自行车对象.下车(人对象) – 仅需一行代码
- 楼梯对象.上楼(人对象) – 仅需一行代码
- 教室对象.进入(人对象) – 仅需一行代码
上面的例子很直观的描述了什么是面向过程,什么又是面向对象,同时我们可以明显的感受到面向对象的优势,实际上面向过程就类似于我们啥都不干,直接拿砖块盖房子,而面向对象就类似于先画好图纸,以后盖房子的时候都直接根据图纸来盖房子,不需要再思考怎么盖了。
从更加本质的角度上来说,面向过程是将一件事情分成了很多步骤,把这些步骤都完成,整件事情也就完成了,而面向对象是把整件事情中包含的所有物体都抽象为一个个的对象,每个对象都拥有自己的属性和能做的事情,通过这些对象,对于整件事情进行模拟,整件事情也就完成了。由此可见,面向过程更加繁琐,而面向对象更加简单易懂。
面向对象基础概念
类和对象
何为类
盖房子使用的图纸
何为对象
根据图纸盖出来的房子
类与对象的关系
类中都有什么
- 属性:比如学生的姓名,年龄,学号等该类型的固有信息
- 方法:比如吃饭,睡觉,学习等该类型的动态行为
类的特性
封装性
封装是面向对象的核心思想。它有两层含义;一层含义是把对象的属性和行为看成一个密不可分的整体,将这两者“组合”在一起(即封装在对象中);另一层含义指信息隐藏,将不想让外界知道的信息隐藏起来,例如,学开车时只需要知道如何操作汽车,不必知道汽车内部是如何工作的。
继承性
我们把任何动物都拥有的属性和行为放到动物类中(比如【属性:年龄、体重】【行为:吃饭、睡觉】),然后我们让猫猫类、狗狗类和兔兔类都继承动物类,此时猫猫类、狗狗类和兔兔类都拥有了任何动物共有的属性和行为,因此在猫猫类、狗狗类和兔兔类中,我们不需要把动物共有的属性和行为再写一遍了,我们只需要在其中定义他们所特有的属性和行为。而金毛、二哈和柯基都继承了狗狗类,因此他们都具有狗狗所共有的属性和行为,我们只需要定义他们所特有的属性和行为即可。
多态性
同样是吃饭,猫猫一般吃鱼,狗狗一般吃骨头,而兔兔一般吃胡萝卜,如果一个数组中存放了几种不同的动物,我们让这些动物一起去食堂吃饭,我们让不同种类的对象执行相同的行为(吃饭),但是最终他们表现出来的结果却不一样(猫吃鱼、狗吃骨头、兔兔吃胡萝卜),这种现象就叫多态,就是同种行为表现出不同状态。
面向对象基础
类的定义
class 类名 {
成员变量;
成员方法;
}
示例
/**
* 学生类
*/
public class Student {
/**
* 姓名
*/
String name;
/**
* 年龄
*/
int age;
/**
* 性别
*/
String sex;
/**
* 读书方法
*/
void read(){
System.out.println("大家好,我是"+name+",我在看书!");
}
}
对象的创建与使用
创建对象
类名 对象名 = new 类名();
Student stu = new Student();
使用对象
对象名.属性名
对象名.方法名
stu.name = "张三";
System.out.println("姓名:" + stu.name);
stu.read();
完整示例
public class Main {
public static void main(String[] args) {
Student stu = new Student();
stu.name = "张三";
System.out.println("姓名:" + stu.name);
stu.read();
}
}
对象的内存结构
思考一下下面这段代码的输出
public class Main {
public static void main(String[] args) {
// 值类型
int a=10;
int b=a;
System.out.println("a="+a);
System.out.println("b="+b);
System.out.println("修改b的值后:");
b=20;
System.out.println("a="+a);
System.out.println("b="+b);
// 引用类型(对象)
Student stu1=new Student();
Student stu2=null;
stu2=stu1;
stu1.name="张三";
stu1.age=10;
stu2.age=20;
System.out.println("修改stu2的值后:");
System.out.println("stu1.name="+stu1.name);
System.out.println("stu1.age="+stu1.age);
System.out.println("stu2.name="+stu2.name);
System.out.println("stu2.age="+stu2.age);
}
}
搞懂了上面这段代码输出结果的原理,对象的内存结构自然也就明白啦。
访问控制权限
private < default < protected < public
/**
* 测试权限修饰符
*/
public class Test {
/**
* 私有变量
*/
private int privateInt;
/**
* 默认变量
*/
int defaultInt;
/**
* 受保护变量
*/
protected int protectedInt;
/**
* 公共变量
*/
public int publicInt;
}
封装
为什么要封装
我们现在可以随意的访问对象中的属性和方法,但是很多时候我们并不希望类的外部能够访问某些属性和方法,就比如人类拥有心跳方法,但心跳方法我们不希望外界能够使用,外界随意使用会很危险,因此我们就需要使用权限修饰符private,使心跳方法变成一个私有的方法,只能在类的内部使用。
this关键字的使用
调用本类中的属性、方法、构造方法
如何实现封装
使用权限修饰符private
/**
* 学生类
*/
public class Student {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0) {
System.out.println("年龄不能为负数!");
return;
}
this.age = age;
}
/**
* 读书方法
*/
void read() {
System.out.println("大家好,我是" + name + ",刚满" + age + "岁~~~" + ",我在看书!");
}
}
public class Main {
public static void main(String[] args) {
Student stu = new Student();
stu.setName("张三");
stu.setAge(-20);
stu.setAge(18);
stu.read();
}
}
思考一下,如何让某个属性只读或只写
构造方法
在此之前,我们初始化对象中的属性,都是先创建一个对象,然后逐个为其中的属性赋值,这种方法并不是很方便,那我们有没有办法在创建对象的时候就给他赋上初值呢?
/**
* 学生类
*/
public class Student {
/**
* 无参构造方法
*/
public Student() {
}
/**
* 有参构造方法
* @param name 姓名
* @param age 年龄
*/
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0) {
System.out.println("年龄不能为负数!");
return;
}
this.age = age;
}
/**
* 读书方法
*/
void read() {
System.out.println("大家好,我是" + name + ",刚满" + age + "岁~~~" + ",我在看书!");
}
}
public class Main {
public static void main(String[] args) {
Student stu = new Student("张三", 18);
stu.read();
}
}
注意事项
- 构造方法的名称必须与类名一致
- 构造方法名称前不能有任何返回值类型的声明
- 不能在构造方法中使用return返回一个值,但可以单独写return语句作为方法的结束
构造块
用{}包裹,与构造方法同级,会在构造方法执行前(创建对象之前)执行
static关键字
前面讲到的类中的属性,都是需要创建对象后,通过 对象.属性 的方式使用,也就是这些属性都是属于对象的,而如果某个属性通过static关键字修饰,那么它将会属于类(通过 类.属性 使用),不仅是属性,方法也可以通过static修饰,效果一样。
示例
// 静态属性
类名.属性名
// 静态方法
类名.方法名
/**
* 学生类
*/
public class Student {
/**
* 无参构造方法
*/
public Student() {
}
/**
* 有参构造方法
*
* @param name 姓名
* @param age 年龄
*/
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 学校
*/
String school = "A大学";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0) {
System.out.println("年龄不能为负数!");
return;
}
this.age = age;
}
/**
* 读书方法
*/
void info() {
System.out.println("大家好,我是" + name + ",刚满" + age + "岁~~~,在" + school + "上大学");
}
}
public class Main {
public static void main(String[] args) {
Student stu1 = new Student("张三", 18);
Student stu2 = new Student("李四", 20);
Student stu3 = new Student("王五", 22);
stu1.info();
stu2.info();
stu3.info();
stu1.school= "B大学";
stu1.info();
stu2.info();
stu3.info();
}
}
上述示例中,张三、李四和王五是最开始是同一所大学 A大学 的学生,现在他们的大学改名叫 B大学 ,此时这里只改动了stu1的大学名称,而stu2和stu3的大学名称依然是 A大学 ,由此可见,我们想要让大学改名,就要把所有学生对象的大学名称全部更改成 B大学 ,假设有一千亿个学生,我们就要修改一千亿次。
上述这种情况肯定是不合理的,究其原因,其实是因为每个对象的大学属性是独立的,但如果大学属性 属于类 ,那么所有的学生对象都是共享大学属性的,只要使用 类名.属性名 修改了大学属性,那么属于该类型的所有对象的大学属性也就同步更新了。
使用静态属性后的代码
/**
* 学生类
*/
public class Student {
/**
* 无参构造方法
*/
public Student() {
}
/**
* 有参构造方法
*
* @param name 姓名
* @param age 年龄
*/
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private int age;
/**
* 学校
*/
static String school = "A大学";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0) {
System.out.println("年龄不能为负数!");
return;
}
this.age = age;
}
/**
* 读书方法
*/
void info() {
System.out.println("大家好,我是" + name + ",刚满" + age + "岁~~~" + ",在" + school + "上学。");
}
}
静态属性和方法的内存原理
静态代码块
在类第一次被使用的时候执行
静态代码块 -> 构造代码块 -> 构造方法
继承
基础语法
class 父类{
...
}
class 子类 extends 父类{
...
}
示例
/**
* 动物类
*/
public class Animal {
private String name;
private int age;
public final String COLOR = "白色";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
/**
* 狗狗类
*/
public class Dog extends Animal{
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("旺财");
dog.setAge(3);
System.out.println(dog.getName() + "的年龄是" + dog.getAge() + "岁,颜色是" + dog.COLOR);
}
}
注意事项
- Java仅支持单继承,不支持多继承
- 多个类可以继承同一个父类,比如狗狗、猫猫和兔兔都继承动物
- 允许多层继承,比如狗狗继承了动物,二哈继承了狗狗
方法重写
/**
* 动物类
*/
public class Animal {
void shout() {
System.out.println("动物叫!");
}
}
/**
* 狗狗类
*/
public class Dog extends Animal{
@Override
void shout() {
System.out.println("汪汪汪!");
}
}
super关键字
super.属性
super.方法(参数1, 参数2, ...)
/**
* 狗狗类
*/
public class Dog extends Animal{
@Override
void shout() {
super.shout();
System.out.println("汪汪汪!");
}
}
final关键字
- 修饰类,不可被继承
- 修饰方法,不可被重写
- 修饰变量,变为常量,只能赋初值
抽象类
有些时候,父类中某些方法的逻辑可以确定,有些方法的逻辑只有到了子类中才能确定,比如说叫声方法,动物类没法确定具体的叫声,而子类中可以确定,比如狗狗汪汪汪,猫猫喵喵喵,此时我们就需要用到抽象类,抽象类不能直接创建对象。
abstract class 抽象类名称 {
属性;
// 普通方法
访问权限 返回值类型 方法名称(参数) {
return 返回值;
}
// 抽象方法
访问权限 abstract 返回值类型 抽象方法名称(参数);
}
示例
/**
* 动物类
*/
public abstract class Animal {
void eat() {
System.out.println("吃东西!");
}
abstract void shout();
}
/**
* 狗狗类
*/
public class Dog extends Animal{
@Override
void shout() {
System.out.println("汪汪汪!");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
dog.shout();
}
}
接口
我们在打游戏的时候,很多东西都有加血和扣血方法,怪物可以加血扣血,角色可以加血扣血,建筑也可以加血扣血,他们是截然不同的东西,父类是不一样的,很难找到上述几种类型的公共父类,但他们却拥有相同的几种行为,接口就是为了这种情况而存在的,任何具备加血和扣血操作的类型,都可以实现血量控制接口,以实现所有拥有加血扣血操作的类型的规范统一。
我们在生活中其实也常常用到接口,比如电脑的USB接口,鼠标,键盘,手机,U盘,等等各种毫不相关的东西都可以使用同样的USB接口,这是因为这些设备都遵循了USB的协议,就像是上面那个例子中截然不同的东西都具备加血和扣血操作一样。
定义接口
[public] interface 接口名 [implements 接口1, 接口2, ...] {
[public] [static] [final] 数据类型 常量名 = 常量;
[public] [abstract] 返回值的数据类型 方法名(参数列表);
[public] static 返回值的数据类型 方法名(参数列表){}
[public] default 返回值的数据类型 方法名(参数列表){}
}
// 其实接口中最常用的一般是之定义抽象方法,也就是第二条 [public] [abstract] 返回值的数据类型 方法名(参数列表); 其他几条了解一下即可
实现接口
修饰符 class 类名 implements 接口1, 接口2,...{
...
}
接口支持多实现,比如有一个血量控制接口,一个飞行控制接口,对于只有加血扣血操作的类型,只需要实现血量控制接口,对于没有生命值但是能飞的小精灵,只需要实现飞行控制接口,而对于即拥有生命值,又能飞的主角或者载具来说,就既要实现血量控制接口,也要实现飞行控制接口。
示例
/**
* 血量控制接口
*/
public interface IBloodControl {
void addBlood(int blood);
void reduceBlood(int blood);
}
/**
* 飞行控制接口
*/
public interface IFlyControl {
void fly();
}
public class Bird implements IFlyControl, IBloodControl{
private int blood;
@Override
public void addBlood(int blood) {
this.blood += blood;
}
@Override
public void reduceBlood(int blood) {
this.blood -= blood;
}
@Override
public void fly() {
System.out.println("鸟儿飞行!");
}
}
抽象类和接口
从本质上来讲,接口就像是文章的大纲,等着我们去填充(实现接口),而抽象类更像是写了一半的文章,但是还没完全写完,没写完的部分先列好大纲,等之后在子类中填充。
多态
对象类型转换
向上转型
子类对象 -> 父类对象
父类类型 父类对象 = 子类实例;
因为子类对象一定属于其父类,就比如二哈一定属于狗狗,狗狗一定属于动物一样,所以能够直接隐式转换
向下转型
父类对象 -> 子类对象
子类类型 子类对象 = (子类)父类对象;
父类对象不一定属于子类,就比如动物对象不一定是狗狗对象,所以想要进行类型转换需要强制类型转换
多态的使用
/**
* 动物类
*/
public abstract class Animal {
abstract void shout();
}
/**
* 狗狗类
*/
public class Dog extends Animal{
@Override
void shout() {
System.out.println("汪汪汪!");
}
}
/**
* 猫猫类
*/
public class Cat extends Animal{
@Override
void shout() {
System.out.println("喵喵喵!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.shout();
Animal cat = new Cat();
cat.shout();
}
}
多态的好处
小动物们开了一家KTV,猫猫狗狗和兔兔一起去唱歌,通过代码实现上述场景。
其实上述场景中,我们可以实例化一个猫猫对象,一个狗狗对象,一个兔兔对象,然后依次调用他们的叫声方法,但是这样很麻烦,如果有一千个动物,我们就要调用不同对象的叫声方法一千次,有没有更好的办法呢?
猫猫狗狗和兔兔其实都属于动物,我们可以让他们都转型成动物类型,然后放到一个动物类型的列表(List)里面,然后遍历这个列表,在循环体中调用每一个元素的叫声方法。
/**
* 动物类
*/
public abstract class Animal {
abstract void shout();
}
/**
* 猫猫类
*/
public class Cat extends Animal{
@Override
void shout() {
System.out.println("喵喵喵!");
}
}
/**
* 狗狗类
*/
public class Dog extends Animal{
@Override
void shout() {
System.out.println("汪汪汪!");
}
}
/**
* 兔兔类
*/
public class Rabbit extends Animal{
@Override
void shout() {
System.out.println("哞哞哞!");
}
}
public class Main {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Rabbit());
animals.forEach(Animal::shout);
}
}
instanceof关键字
instanceof关键字用于判断一个对象是否属于某个类(或接口)的实例,语法格式如下:
对象 instanceof 类(或接口);
Object类(万类之祖)
Object是任何类的父类,所有的类都会直接或间接的继承Object类,其中有一些通用的方法我们可以重写,例如输出形式,比较,哈希值等。