有对象,不懂对象?
没对象,想要对象?
这里告诉你怎么正确打开你的对象。

引言

面向对象是什么?怎么去理解面向对象?我记得一开始接触Java的时候,老师对面向对象的解释举例都是阿猫阿狗,讲完之后,我们都知道了什么叫对象,但什么叫面向对象呢?其实并没有说清楚。学Java这么久了,关于面向对象还是有一点自己的理解的。这篇文章,将帮助你去理解什么叫面向对象。

对象

前面一直在说面向对象,那什么是对象呢?对象是系统中描述客观事物的一个实体,它是构成系统的一个基本单位。这么讲好理解吗?不好理解?那我们再简单一点,对象是一个类别中的某一个个体,学了数学的我们应该知道集合,对象就是我们集合中的元素,他是独一无二的。我们这里讲的对象也是独一无二的,为什么这么说,后面在作解释。那这样,我们能理解对象吗?地球上的人,是一个大的类别,我们每个人就是其中的一个个体,所以说,我们就是一个对象。

好,对象的理解完,我们再来看。我们每个人是一个对象,那怎么去分析这个对象呢?之前也讲了,对象是独一无二的存在。那我们来看看,为什么这么说?我们人这个对象实体,有名字,身高,性别,年龄等基本的描述信息。看个程序(以下程序都是Java版):

1
2
3
4
5
6
public class Person {
String name ;
int height ;
String sex ;
int age ;
}

这是不是就是对人的一个基本描述(我们不考虑太多的描述)?这里的 name , height , sex , age
称为成员变量,是Person类的属性。刚刚说了,对象是由一个类实例化出来的一个实体。那是不是这样?

1
Person person = new Person();

那这样,我们是不是就可以说 person 是一个对象了?是的,person 就是一个对象。好,我们的对象是独一无二的,那是不是要给对象加上它的基本信息,他叫什么?多高?年龄多大之类的信息?

成员变量的赋值

成员变量怎么赋值呢?一般的我们针对成员变量的赋值会有三种:

在定义的时候直接赋

1
2
3
4
5
6
public class Person {
String name = "Alan";
String height ;
String sex ;
int age ;
}

通过构造方法赋值

所谓构造方法,也就是我们常喜欢说的构造器,是指,我们根据这个类,去创建对象的一个途径。

1
2
3
4
5
6
7
8
9
10
public class Person {
String name = "Alan";
String height ;
String sex ;
int age ;

public Person(int height) {
this.height = height;
}
}

通过构造器,我们是在创建这个对象的时候就已经确定赋值了。

1
Person person = new Person("180cm");

通过方法赋值

这里,我们使用方法去进行赋值,一般也是我们常说的 setter/getter 方法,进行 赋值/取值 操作。看这个例子:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Person {
String name ;
String height ;
String sex ;
int age ;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getHeight() {
return height;
}

public void setHeight(String height) {
this.height = height;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

面对这个方法,我们怎么去用?

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Person tom = new Person();
tom.setName("Tom");
tom.setAge(18);
tom.setHeight("180cm");
tom.setSex("male");

System.out.println(tom);
}

结果是什么?

1
Person{name='Tom', height='180cm', sex='male', age=18}

我们一般会把这种在一个类中将方法包装起来,并对去提供一个接口的方式,叫做 封装 。那现在,我们对象相关点讲完了,开始讲面向对象了。

面向对象

首先,面向对象是一种程序设计的方法,是从现实世界中客观存在的事物(即对象)出发来构造软件系统,并且在系统构造中尽可能运用人类的自然思维方式。这句话怎么理解呢?举个例子:

在我们学习 C 语言的时候,会写很多的方法去做某一件事,比如说,开门这个动作,C 中就会直接执行开门这个动作,那么问题来了,谁来执行这个开门动作呢?C 语言强调的是怎么去开门,谁来开门,他不关心。那在Java里面呢?同样是开门,但是他的开门会有人去开门,而不是让门自己打开。所以说,面向对象语言更符合我们人类自然的思维方式,

基本特性

封装

这里我们前面的例子大家也看到了,封装的含义就是隐藏对象内部的细节,不对外暴露。前面的 tom 对象的内部属性 , 比如说 name 还是对外暴露的。封装的原则是使对象以外的部分不能随意的访问和操作对象的内部属性,从而避免了外界对对象内部属性的破坏。可以通过对类的成员设置一定的访问权限,实现类中成员的信息隐藏。我不要它对外暴露,怎么做呢?

范围 private default protected public
同一个类中 OK OK OK OK
同一个包中 NO OK OK OK
子类 NO NO OK OK
全局范围 NO NO NO OK

继承

继承关系,应该是这里面比较复杂的特性之一。首先,我们先理解什么叫继承,顾名思义继承就是从上一代手中去获取到他的一切并作为自己的东西。很好理解吧?类的继承也是这样。子类要继承父类的属性,继承你的方法,继承你的一切。比如说,人里面有好人,有坏人。怎么说,那我给好人坏人打个标签:

1
2
3
4
5
6
7
8
9
10
public class BadPerson extends Person {
private String tag = "我是个坏人" ;

@Override
public String toString() {
return "BadPerson{" +
"tag='" + tag + '\'' +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
public class GoodPerson {
private String tag = "我是个好人" ;

@Override
public String toString() {
return "GoodPerson{" +
"tag='" + tag + '\'' +
'}';
}
}

看看我们得到的结果,

1
2
GoodPerson{tag='我是个好人', name='Tom', height='180cm', sex='male', age=18}
BadPerson{tag='我是个坏人', name='Alan', height='170cm', sex='female', age=24}

大家可以看到,我这里并没有去对GoodPerson做多余的方法,只有一个添加的标签。很显然,这些其他的方法都是从父类继承下来的。这里,我们就需要注意几点问题了:

  • Java中父类可以拥有多个子类,但是子类只能继承一个父类,称为单继承。
  • 继承实现了代码的复用。
  • Java中所有的类都是通过直接或间接地继承java.lang.Object类得到的。
  • 子类不能继承父类中访问权限为private的成员变量和方法。
  • 子类可以重写父类的方法,即命名与父类同名的成员变量。

Java中通过super来实现对父类成员的访问,super用来引用当前对象的父类。super 的使用有三种情况:

  • 访问父类被隐藏的成员变量,如:super.variable;
  • 调用父类中被重写的方法,如:super.Method([paramlist]),super()调用父类构造方法;
  • 调用父类的构造函数,如:super([paramlist]);

上面的几个问题,大家伙就自己去验证吧。

多态

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

怎么理解这句话?举个例子。好人 Tom 在路上抓到了一个小偷 Alan , 现在两个人进警察局,那现在有一个警察,要来分辨谁是好人谁是坏人了。警察在他们进来的时候不知道谁是好人谁是坏人,必须要进来之后才能确定是吧。但是问题来了,第一个进来的可能是好人,也可能是坏人,那我这里怎么分析呢?看程序:

1
2
3
4
5
6
7
public String analysis(GoodPerson goodPerson){
Class<? extends GoodPerson> aClass = goodPerson.getClass();
if (aClass.equals(GoodPerson.class)){
return "是个好人" ;
}
return "不知道";
}

这里的解决方式有很多,我这里解决了面向对象的一个基本问题就是:我是谁 ?也有其它的写法。

如果我进来的是一个坏人,那我是不是还要一个方法来分析好人坏人?那这样是不是就造成了人力财力?显然不合适。我们可以看到一点,无论好人坏人,都有一个共同点,就是 你都继承成了Person .现在来看:

1
2
3
4
5
6
7
8
9
10
public String analysis(Person person){
if (person instanceof GoodPerson){
return "是个好人" ;
}else if (person instanceof BadPerson){
return "是个坏人" ;
}else if (person instanceof Police){
return "我是警察" ;
}
return "不知道";
}

这样来看,不管你是好人坏人还是警察,只要你敢来,我就敢分辨你是谁。这样,是不是就方便了很多。这就是多态。

多态存在的几个必要条件:

  • 继承
  • 重写
  • 父类引用子指向类的对象

多态的优势在于:

1、可替换性。

2、可扩充性。多态对代码具有可扩充性。

3、接口性。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。

4、灵活性。它在应用中体现了灵活多样的操作,提高了使用效率。

5、简化性。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

抽象

这里我将抽象作为面向对象的第四个特性,支持面向对象“四大特性”说法。抽象是程序员的核心素质之 ,体现出程序员对业务的建模能力,以及对架构的宏观掌控力。虽然面向过程也需要进行 定的抽象能力,但是相对来说,面向对象思维,以对象模型为核心富模型的内涵,扩展模型的外延,通过模型的行为组合去共同解决某一类问题,抽象能力显得尤为重要 封装是一种对象功能内聚的表现形式 使模块之间辑合度变低,更具有维护性,继承使子类能够继承父类,获得父类的部分属性和行为,使模块更有复用性 多态使模块在复用性基础上更加有扩展性,使运行期更有想象空间。

抽象是面向对象思想最基础的能力之一,正确而严谨的业务抽象和建模分析能力是后续的封装、继承、多态的基础 是软件大厦的基石。在面向对象的思维中,抽象分为归纳和演绎。前者是从具体到本质,从个性到共性,将一类对象的共同特征进行归一化的逻辑思维过程 后者则是从本质到具体,从共性到个性,逐步形象化的过程。在归纳的过程中,需要抽象出对象的属性和行为的共性,难度大于演绎。演绎是在已有问题解决方案的基础上,正确地找到合适的使用场景。演绎错误在使用集合时比较常见,比如针对查多改少的业务场景 使用链表是非常不合理的,底层框架技术选型时如果有错误,则有可能导致技术架构完全不适应业务的快速发展。

现在我们来看我们的 Person 类, 我们可以把把他抽象出来,看结果:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class Person {
String name ;
String height ;
String sex ;
int age ;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getHeight() {
return height;
}

public void setHeight(String height) {
this.height = height;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", height='" + height + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}

public String analysis(Person person){
return "分析" ;
}
}

这里需要注意几个问题:

  • 抽象类不能被实例化,由其子类进行实例化操作;
  • 类中有抽象方法,此类一定是抽象类;
  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  • 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
  • 父类是有抽象方法的抽象类时,子类必须实现抽象方法或者声明为抽象。

在抽象类中,还有一个概念,我们称为 接口 。在接口中,一般是供别人调用的方法,他是对对象行为的抽象。接口是要实现的。比如说 Person 类中的 analysis(Person person) , 可以实现一个接口的形式:

1
2
3
public interface Behavior {
String analysis(Person person) ;
}

现在用Person 去实现我们的接口:

1
public abstract class Person implements Behavior{}

由于我们这里的Person是一个抽象类,所以呢,我们这里的Behavior里面的接口可以不实现。但是,Person的子类是必须要实现这个接口的。如果说,这个接口可以选择性实现,那我们的接口就要这么写:

1
2
3
default void say() {
System.out.println("Hello");
}

接口里面的 say() 就是一个可以不实现的方法。

接口与抽象之间的关系

语法不同

  • 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;

  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;

  • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

设计层面不同

  • 抽象类时对类的抽象,接口是对行为的抽象;

  • 抽象类更倾向于一种模板,子类均按照这个模板来设计;接口更是一种行为上的规范或者说是一种约束。

总结

正所谓,“一树一菩提,一花一世界。”一切皆对象,万物有三问:我是谁?我从哪来?我要到哪去?

面向对象编程(Object-Oriented Programming,OOP)的封装,继承,多态,抽象的理念推动了软件大规模化的可能,也实践了软件工程的三个目标:可维护性、可重用性和可扩展性。Java中,有一位所有类的先祖 —— Object。在Object类中,我们能找到万物三问的答案:

  • 我是谁? getClass() 回答了你,你是谁。 toString() 是你对外的公开名片;
  • 我从哪里来? Object() 构造方法是生产对象的必要 , clone() 是繁殖对象的另一种方式;
  • 我到哪里去? finalize() 是在对对象销毁时触发的方法。

另外,Object还映射了社会科学领域的一些问题:

  • 世界是否因你而不同? hashCode() 和 equals() 判断是你和世界不同还是世界和你不同。
  • 与他人的协调合作? wait() 和 notify() 是对象建通信的一种方式?毕竟世界这么大,你我距离这么远,想你了,怎么办呢?是吧。