面向对象概述
面向对象是一种编程思想,编程范式(方式),其本身与语言无关。“编程思想”其实指的是在分析问题、编写程序时采用的一种思维方式。我们之前的编码方式其实都是采用的“面向过程”的编程思想。
回忆一下在处理 ATM 机的需求时我们是如何操作的?
按照功能分别去书写登录、存款、取款、转账等功能,采用“把自己当作计算机”的方法,基于分析问题域的解决步骤,将各个单独功能使用一个主流程串起来。这种编程方式也被叫作 结构化编程。
我们也知道计算机最初都是由科学家们在使用,随着计算机硬件的发展,信息时代使计算机进入千家万户,也使得计算机不单单再是只进行科学运算。软件的应用范围从科学运算走向各个领域,开发者对问题域的认知度需要从方法上发生了根本性的变化,另一种编程方式“面向对象编程”正悄悄在发展。
面向对象编程实则就是在进行 职责分配。 像针对 ATM 机那个需求,如果是使用面向对象的编程方式去进行编码,就可以将 ATM 构建成一个真实的“ATM 机对象”,ATM 所拥有的存取查退、登录验证等功能都应该是归属这个机子本身的行为。人去使用 ATM 时,只需要关心“哪里有 ATM 机?”、“如何使用 ATM”,而不再去关心各种验证。
所以:
- 面向过程是基于分析问题域的解决步骤
- 面向对象是基于分析问题域的参与角色
Java 在语法上直接引入了面向对象的概念,是第一门纯面向对象编程语言,它的设计完全是基于面向对象中所需要的概念,在语句层面上就能够表现面向对象的各种特征。其他语言也有模拟出面向对象的语法,比如说 js 在 ES6 中提出的 class
关键字。
面向过程 vs 面向对象
看到这里你可能开始在思考,那既然先有面向过程,后有面向对象,也就是说面向对象可以替代掉面向过程?或者说面向对象更好?
来思考这样一个场景,关键字给到两个:“一辆车”、“回家”。
排除掉饮酒的前提,那对于会开车的人的解决方式大概是:绕车一周检查情况 - 开车门上车 - 系安全带 - 打火 - 挂档 - 给油 ……
那对于不会开车的人,或因其他因素现在无法开车的人,他的解决方案就只有“找个会开车的人送我回家”了。无论是打出租还是找代驾,刚才的所有步骤都是由当前驾驶员完成的,“我”只需要使用驾驶员的“驾驶”方法就行了。 那我关心今天送我回家的是张阿姨还是李师傅吗?不关心。对不对?
那对于 “驾驶员” 而言呢?“上车 - 安全带 - 打火 - 挂档 - 给油” 的事,是不是仍然一步一步针对解决步骤来执行的?
由此能不能分析、总结出关于面向过程、面向对象的特点和区别?
面向过程 的设计思路是自顶向下,采用模块分解,按功能划分成多个关系简单、功能相对独立的基本模块(函数)。
而 面向对象 是将拥有相同行为的同类型的“东西”设计在一起,这些“东西”拥有自己应该拥有的方法,当“我”需要使用该方法,“叫” 这个 “东西” 自己去实现。就像是 “我” 使用驾驶员(张阿姨|李师傅|王老板)的驾驶方法。
所以,事无绝对,不能单方面的说某种编程范式更好,人们在解决自己擅长领域、能力范围内的事时,通常采取面向过程的方式分析解决问题。或是采取面向过程 + 面向对象的方式解决。
面向对象设计方案
面向对象设计方案核心在于:从问题场景中“找对象”,找出有哪些对象以及该对象所拥有的属性和行为:
- 属性:对象身上的值数据
- 行为:对象身上的功能
像我们常说的“找个对象吧”这个“对象”就是一个人。对于人,所拥有的“属性”,就像是个人信息表格中的数据,常见的姓名、年龄、性别、电话号、住址等等都是属于属性,都拥有对应的值。而像“特长”譬如说唱歌跳舞、写代码、开车等,都是“能做的事儿”,属于行为、方法。
这也就是面向对象中常常提到的:
- 万物皆对象
- 对象因关注而产生
桌子板凳是对象,它们可以拥有长宽高、颜色、价格、产地等属性;猫狗老虎也是对象,它们有身高、体重、年龄等属性以及吃饭睡觉等方法。万物,都可以被看作是对象(object)。
那什么又叫“对象因关注而产生”呢?当在设计 ATM 的时候,你会关心孟加拉老虎的毛发颜色吗?
类
首先思考一个问题,“类” 是什么?在日常生活中的分类有什么呢:
- 人
- 书籍
- 动物
- 家具
类其实就是通过人脑的思维,把具有相同属性和行为的一组实体,进行归纳,抽取为共同的抽象概念。在一个问题域中我们会找到大量的同类型对象,根据类型进行编码,这就是类的概念。
像在上述问题域中我们就提到了来开车的可能是张阿姨,可能是李师傅,甚至于可能是自己的专人司机,但不管是谁,“驾驶”技能都是共有的,所以他们的类就应该是“驾驶员”或者“代驾”。
在对于 ATM 的操作时,以前的写法:
String [] username = {"zhangsan", "lisi", "wangwu"}; // 所有用户账号放一起
String [] password = {"123", "456", "666"}; // 所有用户密码放一起
double [] account = {500, 1000, 1500}; // 所有用户余额放一起
// 再通过 数组[下标] 方式操作数据
自定义类
现在有了新的解决方法能够让这些分散数据更有关联性、组织性。定义类,其实就是在定义一种数据类型。类是对象的抽象概念,对象是类的具体实现。
语法:
public class 类名{ // 一个公共类,类名首字母大写
// 属性 值数据
// 行为 功能
}
语法说明:
- 一个 .java 文件只定义一个类
- 类名和文件名保持一致,即
ATM.java
就只有public class ATM
- 不要在类里再嵌套声明类。嵌套的写法属于内部类,当前不做讨论
定义好的类是不能直接使用的,类相当于是一个“模板”,通过这个模板产生出具有属性和行为的东西。就好比是选择某个人问路,不能直接叫“人!”,得明确到实体某个人身上。
所以在面向对象中我们分析和编码的方式步骤是:
- 既然是值数据,本质上来说,所有属性值都是一个数据量,而数据量在编程语言中只有变量和常量,因而属性的语法就是变量或常量声明的语法
访问修饰符 数据类型 变量名;
- 如果该类的所有对象在该属性上的值是不同的,会发生变化的,那么就该声明为变量,例如每个人的姓名、年龄、身份证号等;反之该类所有对象在这个属性上的值是相同的,那么就该声明为常量
访问修饰符 final 数据类型 大写常量名 = 常量值;
public class Human {
public String name;
public int age;
public final boolean stillALive = true;
}
属性语法特点
由于定义的是“类”,所以 “变” 或 “不变” 应该基于全体对象进行考虑,而不能够只考虑单一的对象。
- 属性语法本质上就是变量或常量的语法
- 属性书写的位置是被放在 类 的 {} 中,而不是在某个方法的 {} 中。这种书写位置导致了属性可以在整个类的任意方法中使用。也被称为全局变量 或 成员。放在方法中变量只能在该方法中使用,即局部变量
- 属性是可以有 访问修饰符 的,目前统一写成 public;局部变量没有。这些修饰符的作用是用于控制属性是否能在外部被访问到。访问修饰符存在 4 种:
- public 公共的
- private 私有的
- 不写
- protected 受保护的
- 属性在时可以不赋初始值,产生对象时会自动进行初始化
}
与之前的函数声明方式相比,没有 static 关键字。static 是一个特殊关键字,由它声明的方法在 java 的面向对象概念中是代表特殊含义,表示静态方法,本章不涉及。
```java
// Student.java
public class Student{
// 属性
public String name;
public int age;
// 方法
public void talk(){ // 表示所有学生实例都可以拥有说话方法
}
}
命名规范
- 类名:首字母大写,大驼峰
- 变量名:小驼峰
- 常量名:所有字母全大写
- 方法名:不能用类名做方法名,帕斯卡命名法(小驼峰)
通过类产生对象
有了抽象的类模板后,就可以使用new
关键字产生出具体对象,专业上称为实例化对象:
语法:类型 实例名 = new 类名();
Student zhagsan = new Student();
zhangsan
是一个 Student 类型的变量,这个变量用来存放一个引用,指向真正的学生对象。真正的学生对象使用 new 语句产生,存放在一个独立的内存空间中。对象在创建好后,才能操作它的属性,赋值,取值、调用方法,否则会发生“空指针”的异常。
所以需要注意:
- 在通过变量访问对象属性或方法前,必须保证该变量指向了一个对象
- 对象变量是有数据类型的,比如 zhangsan 在声明时就定义好了 Student 这个类型去修饰它,所以只能够赋值为 Student 的对象
- 无法强转,就好比是无法将“人类”强转为“狗类”
-
对象的操作
创建了实例对象以后,就可以去操作对象的属性和方法。
对象.
点操作符的含义就是通过对象名的引用去访问真正的对象。 ```java // Student.java public class Student { public String name = “zhangsan”; public int age;public void talk() {
System.out.println(name + "我" + age + "岁");
} }
// TestMain.java public class TestMain { public static void main(String[] args) { Student zhangsan = new Student(); zhangsan.age = 18; // 为 age 赋值,可读作 zhangsan 实例的年龄为18 zhangsan.talk(); // 打印 zhangsan我18岁 } }
<a name="Z5JgZ"></a>
## 构造器 constructor
“器” 就是方法,是可以在类中书写除了属性、方法以外的第三种内容。
当我们在使用 `new 类名();` 其中, new 关键字表示 “新的”,后面的 `类名()` 的形式像是在调用一个方法。特殊之处则在于:
- 方法名就是类名,像刚才书写的 `new Student()`
- 对于这个方法我们没有传参
- 也可以传参,像是在 `new Scanner(System.in)` 也可以传参
由此可见,new 后面跟的就是一个方法的调用指令。这个方法就是构造方法。构造方法的作用在于两点:
1. 用于产生对象
1. 对实例对象的属性进行初始化
<a name="qWi4R"></a>
### 构造函数的声明方式
函数在调用之前,一定有声明,当打开 `Scanner.java`:
<a name="OCEjs"></a>
### ![sacnner.png](https://cdn.nlark.com/yuque/0/2022/png/1454005/1651731859107-9f5aabe3-40ec-4c40-b354-035fb477c0c6.png#clientId=u53b54383-b44c-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ued5872a3&margin=%5Bobject%20Object%5D&name=sacnner.png&originHeight=1444&originWidth=2848&originalType=binary&ratio=1&rotation=0&showTitle=false&size=654607&status=done&style=stroke&taskId=u85bf9867-12c8-47f4-b500-9f256583a3d&title=)
可以发现在语法上,没有 static 关键字和返回类型。那也就是说如果在自定义的类里,也只写 `public 类名` 就可以模拟出 Scanner 构造方法。
```java
// Person.java 中的构造函数
public Person(){
}
// Student.java 中的构造函数
public Student(){
}
构造方法的语法特性
当开发者没有在类中不主动书写构造方法,编译器会自动生成一个公共的无参的构造方法,以保证基本使用:
// Car.java
public class Car {
// java 文件中没有书写任何构造函数
}
// TestMain.java
public class TestMain {
public static void main(String[] args) {
Car BMW = new Car(); // 调用的是自动生成的公共无参构造
}
}
如果要自定义构造方法必须遵循:
- 构造函数名称与类名一致
- 构造函数没有返回类型,即连 void 都不能有
- 访问修饰符和参数列表根据需要去设计
一旦定义了构造方法,那么 java 将不再自动生成那个公共无参构造了,即必须传实参:
// Car.java
public class Car {
public String name;
// 显式声明了一个带参构造
public Car(String name) {
}
}
// TestMain.java
public class TestMain {
public static void main(String[] args) {
Car BMW = new Car(); // 报错
}
}
构造方法除了产生对象外,还可以实现作用的第二点:通过接收外部参数,对实例对象的属性进行初始化:
// Car.java
public class Car{
public String name;
public int price;
public Person(String name, int price){ // 声明时,接收形参
// 关于 this 关键字的使用,见下面
this.name = name;
this.price = price;
}
}
// TestMain.java
new Car("BMW", 200000);
初见 this
当一个类的实例有太多属性需要传值时,分别使用 实例.属性 = XX
的方式赋值就太复杂了:
// Student.java
public class Student {
public String name;
public int age;
/*
...
可能存在额外更多属性
*/
public void talk() {
System.out.println("我叫 " + name + "," + age + "岁");
}
}
// TestMain.java
public class TestMain {
public static void main(String[] args) {
// 创建 zhangsan 实例
Student zhangsan = new Student();
zhangsan.name = "zhangsan"; // 实例 zhangsan 的名字叫 zhangsan
zhangsan.age = 20; // 实例 zhangsan 年纪 20
/*
为该对象更多的属性赋值
zhangsan.a = a;
zhangsan.b = b;
zhangsan.c = c;
*/
zhangsan.talk();
}
}
既然对象的行为就是函数,那么是什么让函数变得更灵活的?是参数,对不对?
那实例又是如何生成的?是 new 类型()
出来的,对不对?那么在 类型()
后面不就有一个 ()
吗?可以用于传参。刚刚也提到了如果不显式书写构造函数,编译器会自动生成一个公共无参构造,那假设我在 new 类型()
时,就传入具体的参数,岂不是就能实现在 new 的同时为实例对象属性赋好值:
// Student.java
public class Student {
public String name;
public int age;
public int score;
public String address;
public String hobby;
// 构造函数,函数名同类名
public Student(String name, int age, int score, String address, String hobby){
this.name = name;
this.age = age;
this.score = score;
this.address = address;
this.hobby = hobby;
}
// 方法定义中指定接收来的实参数据设置给某个属性
public void talk() {
System.out.println("我叫 " + this.name + "," + this.age + "岁,家住" + this.address + ",这次考试 " + this.score + "分,我擅长" + this.hobby);
}
}
在构造方法中第 10 行,定义好需要接收来的实参, 这样一来,在 new
实例时,就只需要传入实参就可以对实例的属性进行赋值了。
在函数的声明中 11-15行里出现了关键字 this
。this 词语本身指 “这个”、“这是”,this 指向 new 出来的实例。
当出现 new Student()
就表示调用了构造函数创建一个实例,传入的各属性值就作为了该实例的属性的值。
// TestMain.java
public class TestMain {
public static void main(String[] args) {
// 创建 zhangsan 实例
Student zhangsan = new Student("zhangsan", 20, 80, "金牛区", "打篮球");// 传实参就只需要按照形参顺序给值
zhangsan.talk();
// 创建 lisi 实例
Student lisi = new Student("lisi", 19, 100, "高新区", "料理");
lisi.talk();
}
}