面向对象概述

面向对象是一种编程思想,编程范式(方式),其本身与语言无关。“编程思想”其实指的是在分析问题、编写程序时采用的一种思维方式。我们之前的编码方式其实都是采用的“面向过程”的编程思想。

回忆一下在处理 ATM 机的需求时我们是如何操作的?

按照功能分别去书写登录、存款、取款、转账等功能,采用“把自己当作计算机”的方法,基于分析问题域的解决步骤,将各个单独功能使用一个主流程串起来。这种编程方式也被叫作 结构化编程

我们也知道计算机最初都是由科学家们在使用,随着计算机硬件的发展,信息时代使计算机进入千家万户,也使得计算机不单单再是只进行科学运算。软件的应用范围从科学运算走向各个领域,开发者对问题域的认知度需要从方法上发生了根本性的变化,另一种编程方式“面向对象编程”正悄悄在发展。

面向对象编程实则就是在进行 职责分配。 像针对 ATM 机那个需求,如果是使用面向对象的编程方式去进行编码,就可以将 ATM 构建成一个真实的“ATM 机对象”,ATM 所拥有的存取查退、登录验证等功能都应该是归属这个机子本身的行为。人去使用 ATM 时,只需要关心“哪里有 ATM 机?”、“如何使用 ATM”,而不再去关心各种验证。

所以:

  • 面向过程是基于分析问题域的解决步骤
  • 面向对象是基于分析问题域的参与角色

Java 在语法上直接引入了面向对象的概念,是第一门纯面向对象编程语言,它的设计完全是基于面向对象中所需要的概念,在语句层面上就能够表现面向对象的各种特征。其他语言也有模拟出面向对象的语法,比如说 js 在 ES6 中提出的 class 关键字。

面向过程 vs 面向对象

看到这里你可能开始在思考,那既然先有面向过程,后有面向对象,也就是说面向对象可以替代掉面向过程?或者说面向对象更好?
来思考这样一个场景,关键字给到两个:“一辆车”、“回家”。
千库网_法律禁止醉酒驾驶_元素编号13109763.png
排除掉饮酒的前提,那对于会开车的人的解决方式大概是:绕车一周检查情况 - 开车门上车 - 系安全带 - 打火 - 挂档 - 给油 ……
那对于不会开车的人,或因其他因素现在无法开车的人,他的解决方案就只有“找个会开车的人送我回家”了。无论是打出租还是找代驾,刚才的所有步骤都是由当前驾驶员完成的,“我”只需要使用驾驶员的“驾驶”方法就行了。 那我关心今天送我回家的是张阿姨还是李师傅吗?不关心。对不对?
那对于 “驾驶员” 而言呢?“上车 - 安全带 - 打火 - 挂档 - 给油” 的事,是不是仍然一步一步针对解决步骤来执行的?
由此能不能分析、总结出关于面向过程、面向对象的特点和区别?
面向过程 的设计思路是自顶向下,采用模块分解,按功能划分成多个关系简单、功能相对独立的基本模块(函数)。
面向对象 是将拥有相同行为的同类型的“东西”设计在一起,这些“东西”拥有自己应该拥有的方法,当“我”需要使用该方法,“叫” 这个 “东西” 自己去实现。就像是 “我” 使用驾驶员(张阿姨|李师傅|王老板)的驾驶方法。
所以,事无绝对,不能单方面的说某种编程范式更好,人们在解决自己擅长领域、能力范围内的事时,通常采取面向过程的方式分析解决问题。或是采取面向过程 + 面向对象的方式解决。

面向对象设计方案

面向对象设计方案核心在于:从问题场景中“找对象”,找出有哪些对象以及该对象所拥有的属性和行为:

  • 属性:对象身上的值数据
  • 行为:对象身上的功能

像我们常说的“找个对象吧”这个“对象”就是一个人。对于人,所拥有的“属性”,就像是个人信息表格中的数据,常见的姓名、年龄、性别、电话号、住址等等都是属于属性,都拥有对应的值。而像“特长”譬如说唱歌跳舞、写代码、开车等,都是“能做的事儿”,属于行为、方法。

这也就是面向对象中常常提到的:

  1. 万物皆对象
  2. 对象因关注而产生

桌子板凳是对象,它们可以拥有长宽高、颜色、价格、产地等属性;猫狗老虎也是对象,它们有身高、体重、年龄等属性以及吃饭睡觉等方法。万物,都可以被看作是对象(object)。

那什么又叫“对象因关注而产生”呢?当在设计 ATM 的时候,你会关心孟加拉老虎的毛发颜色吗?

首先思考一个问题,“类” 是什么?在日常生活中的分类有什么呢:

  • 书籍
  • 动物
  • 家具

类其实就是通过人脑的思维,把具有相同属性和行为的一组实体,进行归纳,抽取为共同的抽象概念。在一个问题域中我们会找到大量的同类型对象,根据类型进行编码,这就是类的概念。

像在上述问题域中我们就提到了来开车的可能是张阿姨,可能是李师傅,甚至于可能是自己的专人司机,但不管是谁,“驾驶”技能都是共有的,所以他们的类就应该是“驾驶员”或者“代驾”。

在对于 ATM 的操作时,以前的写法:

  1. String [] username = {"zhangsan", "lisi", "wangwu"}; // 所有用户账号放一起
  2. String [] password = {"123", "456", "666"}; // 所有用户密码放一起
  3. double [] account = {500, 1000, 1500}; // 所有用户余额放一起
  4. // 再通过 数组[下标] 方式操作数据

自定义类

现在有了新的解决方法能够让这些分散数据更有关联性、组织性。定义,其实就是在定义一种数据类型。类是对象的抽象概念,对象是类的具体实现。
语法:

  1. public class 类名{ // 一个公共类,类名首字母大写
  2. // 属性 值数据
  3. // 行为 功能
  4. }

语法说明:

  • 一个 .java 文件只定义一个类
  • 类名和文件名保持一致,即 ATM.java 就只有 public class ATM
  • 不要在类里再嵌套声明类。嵌套的写法属于内部类,当前不做讨论

定义好的类是不能直接使用的,类相当于是一个“模板”,通过这个模板产生出具有属性和行为的东西。就好比是选择某个人问路,不能直接叫“人!”,得明确到实体某个人身上。

所以在面向对象中我们分析和编码的方式步骤是:

  1. 从整个问题域中,找对象,包含对象的属性和行为
  2. 编码时先定义类,然后通过类产生对象,最后使用对象来产生效果

    属性

    属性:该类身上所拥有的值数据。
  • 既然是值数据,本质上来说,所有属性值都是一个数据量,而数据量在编程语言中只有变量和常量,因而属性的语法就是变量或常量声明的语法 访问修饰符 数据类型 变量名;
  • 如果该类的所有对象在该属性上的值是不同的,会发生变化的,那么就该声明为变量,例如每个人的姓名、年龄、身份证号等;反之该类所有对象在这个属性上的值是相同的,那么就该声明为常量 访问修饰符 final 数据类型 大写常量名 = 常量值;
    1. public class Human {
    2. public String name;
    3. public int age;
    4. public final boolean stillALive = true;
    5. }

    属性语法特点

    由于定义的是“类”,所以 “变” 或 “不变” 应该基于全体对象进行考虑,而不能够只考虑单一的对象。
  1. 属性语法本质上就是变量或常量的语法
  2. 属性书写的位置是被放在 类 的 {} 中,而不是在某个方法的 {} 中。这种书写位置导致了属性可以在整个类的任意方法中使用。也被称为全局变量 或 成员。放在方法中变量只能在该方法中使用,即局部变量
  3. 属性是可以有 访问修饰符 的,目前统一写成 public;局部变量没有。这些修饰符的作用是用于控制属性是否能在外部被访问到。访问修饰符存在 4 种:
    • public 公共的
    • private 私有的
    • 不写
    • protected 受保护的
  4. 属性在时可以不赋初始值,产生对象时会自动进行初始化
    • 基本数据类型,属性初始赋为 0
    • 引用数据类型,初始赋为 null
    • 局部变量在使用前必须初始值

      行为

      行为其实就是方法,是这个类的对象能够做的事情。所以定义行为其实就是在定义方法。
      语法: ```java 访问修饰符 返回类型 方法名(形参列表){ // 注意没有 static 关键字

}

  1. 与之前的函数声明方式相比,没有 static 关键字。static 是一个特殊关键字,由它声明的方法在 java 的面向对象概念中是代表特殊含义,表示静态方法,本章不涉及。
  2. ```java
  3. // Student.java
  4. public class Student{
  5. // 属性
  6. public String name;
  7. public int age;
  8. // 方法
  9. public void talk(){ // 表示所有学生实例都可以拥有说话方法
  10. }
  11. }

命名规范

  • 类名:首字母大写,大驼峰
  • 变量名:小驼峰
  • 常量名:所有字母全大写
  • 方法名:不能用类名做方法名,帕斯卡命名法(小驼峰)

    通过类产生对象

    有了抽象的类模板后,就可以使用 new关键字产生出具体对象,专业上称为实例化对象
    语法:类型 实例名 = new 类名();
    1. Student zhagsan = new Student();
    zhangsan 是一个 Student 类型的变量,这个变量用来存放一个引用,指向真正的学生对象。真正的学生对象使用 new 语句产生,存放在一个独立的内存空间中。对象在创建好后,才能操作它的属性,赋值,取值、调用方法,否则会发生“空指针”的异常。
    image-20220415160631578.png
    所以需要注意:
  1. 在通过变量访问对象属性或方法前,必须保证该变量指向了一个对象
  2. 对象变量是有数据类型的,比如 zhangsan 在声明时就定义好了 Student 这个类型去修饰它,所以只能够赋值为 Student 的对象
  3. 无法强转,就好比是无法将“人类”强转为“狗类”
  4. 对象变量的赋值只能使用该变量对应的对象或赋值为 null

    对象的操作

    创建了实例对象以后,就可以去操作对象的属性和方法。对象. 点操作符的含义就是通过对象名的引用去访问真正的对象。 ```java // Student.java public class Student { public String name = “zhangsan”; public int age;

    public void talk() {

    1. 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岁 } }

  1. <a name="Z5JgZ"></a>
  2. ## 构造器 constructor
  3. “器” 就是方法,是可以在类中书写除了属性、方法以外的第三种内容。
  4. 当我们在使用 `new 类名();` 其中, new 关键字表示 “新的”,后面的 `类名()` 的形式像是在调用一个方法。特殊之处则在于:
  5. - 方法名就是类名,像刚才书写的 `new Student()`
  6. - 对于这个方法我们没有传参
  7. - 也可以传参,像是在 `new Scanner(System.in)` 也可以传参
  8. 由此可见,new 后面跟的就是一个方法的调用指令。这个方法就是构造方法。构造方法的作用在于两点:
  9. 1. 用于产生对象
  10. 1. 对实例对象的属性进行初始化
  11. <a name="qWi4R"></a>
  12. ### 构造函数的声明方式
  13. 函数在调用之前,一定有声明,当打开 `Scanner.java`:
  14. <a name="OCEjs"></a>
  15. ### ![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=)
  16. 可以发现在语法上,没有 static 关键字和返回类型。那也就是说如果在自定义的类里,也只写 `public 类名` 就可以模拟出 Scanner 构造方法。
  17. ```java
  18. // Person.java 中的构造函数
  19. public Person(){
  20. }
  21. // Student.java 中的构造函数
  22. public Student(){
  23. }

构造方法的语法特性

当开发者没有在类中不主动书写构造方法,编译器会自动生成一个公共的无参的构造方法,以保证基本使用:

  1. // Car.java
  2. public class Car {
  3. // java 文件中没有书写任何构造函数
  4. }
  5. // TestMain.java
  6. public class TestMain {
  7. public static void main(String[] args) {
  8. Car BMW = new Car(); // 调用的是自动生成的公共无参构造
  9. }
  10. }

如果要自定义构造方法必须遵循:

  • 构造函数名称与类名一致
  • 构造函数没有返回类型,即连 void 都不能有
  • 访问修饰符和参数列表根据需要去设计

一旦定义了构造方法,那么 java 将不再自动生成那个公共无参构造了,即必须传实参:

  1. // Car.java
  2. public class Car {
  3. public String name;
  4. // 显式声明了一个带参构造
  5. public Car(String name) {
  6. }
  7. }
  8. // TestMain.java
  9. public class TestMain {
  10. public static void main(String[] args) {
  11. Car BMW = new Car(); // 报错
  12. }
  13. }

构造方法除了产生对象外,还可以实现作用的第二点:通过接收外部参数,对实例对象的属性进行初始化:

  1. // Car.java
  2. public class Car{
  3. public String name;
  4. public int price;
  5. public Person(String name, int price){ // 声明时,接收形参
  6. // 关于 this 关键字的使用,见下面
  7. this.name = name;
  8. this.price = price;
  9. }
  10. }
  11. // TestMain.java
  12. new Car("BMW", 200000);

方法可以重载,那么构造方法也支持重载。

初见 this

当一个类的实例有太多属性需要传值时,分别使用 实例.属性 = XX 的方式赋值就太复杂了:

  1. // Student.java
  2. public class Student {
  3. public String name;
  4. public int age;
  5. /*
  6. ...
  7. 可能存在额外更多属性
  8. */
  9. public void talk() {
  10. System.out.println("我叫 " + name + "," + age + "岁");
  11. }
  12. }
  13. // TestMain.java
  14. public class TestMain {
  15. public static void main(String[] args) {
  16. // 创建 zhangsan 实例
  17. Student zhangsan = new Student();
  18. zhangsan.name = "zhangsan"; // 实例 zhangsan 的名字叫 zhangsan
  19. zhangsan.age = 20; // 实例 zhangsan 年纪 20
  20. /*
  21. 为该对象更多的属性赋值
  22. zhangsan.a = a;
  23. zhangsan.b = b;
  24. zhangsan.c = c;
  25. */
  26. zhangsan.talk();
  27. }
  28. }

既然对象的行为就是函数,那么是什么让函数变得更灵活的?是参数,对不对?
那实例又是如何生成的?是 new 类型() 出来的,对不对?那么在 类型() 后面不就有一个 () 吗?可以用于传参。刚刚也提到了如果不显式书写构造函数,编译器会自动生成一个公共无参构造,那假设我在 new 类型() 时,就传入具体的参数,岂不是就能实现在 new 的同时为实例对象属性赋好值:

  1. // Student.java
  2. public class Student {
  3. public String name;
  4. public int age;
  5. public int score;
  6. public String address;
  7. public String hobby;
  8. // 构造函数,函数名同类名
  9. public Student(String name, int age, int score, String address, String hobby){
  10. this.name = name;
  11. this.age = age;
  12. this.score = score;
  13. this.address = address;
  14. this.hobby = hobby;
  15. }
  16. // 方法定义中指定接收来的实参数据设置给某个属性
  17. public void talk() {
  18. System.out.println("我叫 " + this.name + "," + this.age + "岁,家住" + this.address + ",这次考试 " + this.score + "分,我擅长" + this.hobby);
  19. }
  20. }

在构造方法中第 10 行,定义好需要接收来的实参, 这样一来,在 new实例时,就只需要传入实参就可以对实例的属性进行赋值了。
在函数的声明中 11-15行里出现了关键字 this。this 词语本身指 “这个”、“这是”,this 指向 new 出来的实例。
当出现 new Student() 就表示调用了构造函数创建一个实例,传入的各属性值就作为了该实例的属性的值。

  1. // TestMain.java
  2. public class TestMain {
  3. public static void main(String[] args) {
  4. // 创建 zhangsan 实例
  5. Student zhangsan = new Student("zhangsan", 20, 80, "金牛区", "打篮球");// 传实参就只需要按照形参顺序给值
  6. zhangsan.talk();
  7. // 创建 lisi 实例
  8. Student lisi = new Student("lisi", 19, 100, "高新区", "料理");
  9. lisi.talk();
  10. }
  11. }