从结构体走向对象
本文能够让你大致理解面向对象的一些概念, 但是以下一切内容均不适用于参加OO课程的考试, 如果爆零, 后果自负 .
部分语法的介绍
C++语言中, 定义结构体变量时, 类型中的struct可以省略, 如"struct 结构体名 变量名"可以简写为"结构体名 变量名 ". 以下均为简写形式.
Java语言中, 所有内置的大写开头的类型 都是类, 所有对象都是"引用变量" , 可以理解为不能做运算的指针 .
(指针运算指, 数组首项地址+1等于第二项地址的这种运算.)
如: 1 2 3 4 5 6 int a = 0 ;Integer b = 0 ;Integer c = 0 ;
上面的代码可以简单地理解为, a
是一个int
类型变量, 内容是0
.
b
是一个指向Integer
变量的指针, 他指向的值是0
.
c
同理.
因此, b==c
判断的是b
和c
是否指向同一个对象. 判断b
和c
相等应该用b.equals(c)
.
这篇推送只讲概念, 不需要完全理解代码是什么意思, 具体语法部分都做了注释. 之后大概 还会有推送来讲语法. 如果那里有代码不明白什么意思欢迎联系我或者后台给我留言.
类
啥是类? 最简单的来说, 就是 C 语言中的结构体.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Student { String name; int ID; }
在最开始非面向对象的编程中, 我们经常会用多个变量来描述同一对象的多个属性: 比如定义一个名字变量, 一个学号变量. 后来, 随着计算机科学的发展, 编程语言中支持了 结构体 这样一个特性, 能够让我们把 描述同一个东西不同属性 的多个变量统一管理. 结构体这一个概念, 到了面向对象的语言中, 结构体发展为了类的概念.
可以把类理解为C语言中的结构体+方法 .
方法
在很多情况下, 我们之前编写的一些函数是针对某一个结构体 提供的. 比如, 链表的删除某个节点的函数就可以认为是针对某个节点定义的, 效果是删除这个节点后面的一个节点 .
我们可以把这样的函数定义为一个全局的函数, 把一个结构体变量作为参数传入这个函数:
1 2 3 node* remove (node *now) { ... }
但是, 如果这样自定义我们就会在别的任何地方都没法再次定义一个 remove
函数了. 这个只针对 node*
的函数占据了一个全局的名字 (或者说, 符号). 无疑, 这样的实现非常 不优雅 . 因此, 我们更应该把进和 node*
这一个变量相关的函数定义成一个 node*
的 成员函数 , 也叫 方法 .
1 2 3 4 5 6 7 8 9 10 11 struct node { int data; node *; node* remove () { return this ->next; } };
如果这样定义remove
函数, 就不会污染全局的作用域, 同时也不用我们显示的声明参数, 只需要如下调用:
即可自动把 a
或 a的地址
作为 this指针
参数传入给 remove
函数.
因此, 方法就是针对某一个类的函数 , 为了方便我们把它放到类里面 . 构造一个对象的函数是一个特殊的成员函数, 叫做类的构造函数.
继承
其实, 编程语言演进的过程就是一代代程序程序员偷懒的过程 . 自从有了结构体的方法这东西之后, 人们就在想如果好几个结构体都有相同的方法, 能不能直接重复用一下?
比如对于如下几个结构体:
1 2 3 4 5 6 7 8 struct shirt { double price = 9.15 ; void printPrice () ; }; struct trousers { double price = 2.33 ; void printPrice () ; };
这两个结构体都分别有一个printPrice
函数, 内容也都是一样的, 但是要写两次, 非常的不优雅 , 于是人们引入了继承的概念:
May there be inheritance!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Clothes { double price; void printPrice () { } } class Shirt extends Clothes { double price = 9.15 ; } class Trousers extends Clothes { double price = 2.33 ; }
这样, printPrice
函数只需要写一次. 从某个类继承, 或者说派生出来的类, 会拥有父类的所有属性以及方法. 子类也可以对应的重写这些方法.
同时, 对于以上的代码中, 显然 Clothes
类和别的类有一个很大的区别. 我们会创建一个 Shirt
类的对象, 但是我们不会执行 new Clothes()
. Clothes
类存在的意义在于提供给别人继承, 我们不会实例化这个类, 因此我们把这个类定义为抽象类(abstract class) :
1 2 3 4 5 6 7 abstract class Clothes { double price; void printPrice () { System.out.println(this .price); } }
一个抽象类的意义是提供给别人来继承. 代码中的 abstract
标记了这是一个抽象类.
多态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 abstract class Animal { void bark () { System.out.println("bark" ); } } class Dog extends Animal { void bark () { System.out.println("Woof" ); } } class Cat extends Animal { void bark () { System.out.println("Meow~" ); } }
上述代码中, Animal
类规定了继承他的类要实现 bark()
方法. 那么, 对于如下的代码, 会输出什么呢?
1 2 3 4 5 Animal a = new Dog ();a.bark();
上面说到, Java中类的变量都是引用变量, 因此上面代码可以不严谨的理解为: 定义一个 指向Animal
类型的指针 a,
在这里, a
是一个Animal
类型的引用, 指向一个Dog
对象. 但是, 尽管他是 Animal
类型的引用, 调用 a.bark()
时仍然调用的是 Dog
的 bark()
方法.
多态, 狭义上指同一个名字(符号)指代多个物体. 在上面代码中, 如果不知道a
指向的类型是什么, 调用 a.bark()
有三种可能的情况. 这就利用了多态的性质.
例子以及面向对象的好处.
在C语言中, 由于没有这些特性, 极大地存在着代码冗余重复的现象, 如: printf
函数用于格式化并向控制台输出内容, sprintf
用于格式化并向字符串写入内容, fprintf
用于格式化并向文件写入内容.
上面的三个函数, 都完成了格式化这一个步骤, 但是代码被编写了三次. 如果需要向网络连接中格式化并写入内容, 则又需要重复一遍格式化的操作. 面向对象就能很好地解决这个问题.
如, 如果我们要向一个文件写入内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 PrintWriter out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter ( new FileOutputStream ("filename.txt" ) ) ) );
上面代码中, 几个类的构造函数分别为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 FileOutputStream(String name) OutputStreamWriter(OutputStream out) BufferedWriter(Writer out) PrintWriter(Writer out)
可以看到, FileOutputStream
接受文件名作为参数, 单纯负责向文件中写入内容.
OutputStreamWriter
接受任何 OutputStream
对象, 用于转换写入内容的编码.
BufferedWriter
接受一个 Writer
, 用于缓存即将写入 Writer
的内容.
PrintWriter
接受一个 Writer
, 用于格式化输入, 写入 Writer
中.
高内聚, 低耦合的设计模式就体现在了这里: 一个 OutputStream
类只需要实现 write
方法, 只能写入二进制字节数据, 而一个 Writer
负责处理编码问题, 可以写入字符串, 负责把写入的字符串编码为二进制字节数据, 而 BufferedWriter
则负责缓存上层写入的内容, 也同样只提供了写入字符串的方法. PrintWriter
负责提供格式化的方法, 可以格式化并写入 int
double
char
string
等多种类型.
这样的设计, 使得功能的拓展变得很方便, 比如我们要向一个自定义的东西中写入数据, 完全可以只实现一个 OutputStream
而复用 OutputStreamWriter
, BufferedWriter
, PrintWriter
等很多和写入数据有关的类.
所以, 这样的实际模式大概是被不断加需求改需求的产品经理逼出来的
感谢 ☁️学长,
(如果可以的话, 能关注一下这个公众号吗)