1. 面向过程与面向对象
1.1 面向过程程序设计
在传统的面向过程程序设计中,算法 + 数据结构 = 程序,首先要决定如何操作数据,而后在决定以何种形式组织数据,以便于数据的操作。
对于一些规模较小的问题,将其分解为过程的设计方式比较理想。
但如果所需解决的问题规模较大,面向过程程序设计就显得比较复杂,可能需要将其分解为很多个过程,每个过程可能都需要对一组数据进行操作
1.2 面向对象程序设计
面向对象程序设计(简称OPP,Object Oriented Programming)是当今主流的程序设计规范,十分适合一些大项目的开发。在面向过程程序设计中需要2000个程序才能完成的功能,采用面向对象的设计风格,可能只需要100个类,平均每个类包含20个方法,管理100个类显然比管理2000个过程简单的多。
Java是一种完全面向对象的语言,在程序设计的过程中,我们不必关心对象的具体实现,而是聚焦于用户的需求。
面向对象程序设计有三大特征——封装、继承、多态。
2. 面向对象的三大特征
2.1 封装
2.1.1 什么是封装
很多人喜欢把封装比喻成一个多功能的黑盒子,我们可以使用黑盒子提供给我们的功能,但却不知道这些功能在黑盒子里是如何具体实现的。即封装隐藏了对象的属性和实现细节,仅对外提供公共访问方式。
封装的关键在于不能让类中的方法直接地访问其他类的实例域(对象中的数据称为实例域)。
2.1.2 Java是如何实现封装的
Java通过类来实现这一特性,它将一类事物的全部属性都封装在类中,并由类对外提供公共的访问方式。在类中,利用private关键字可以将变量或方法申明为私有的(事实上我们应该尽可能的将类内部的数据定义成私有的),一旦某变量或方法被声明为private,就意味着在其他的类中将无法直接访问此变量或方法。
关于private的更多解释可查看:Java访问权限修饰词——private、public和protected.
2.1.3 如何获取私有对象的信息
当我们在一个类中将某一变量设置成私有的之后,如果在其他方法中需要用到这一变量该怎么办?常用的做法是在该类中添加get()
和set()
方法,并让他们是公有(public)的,这样就可以对私有的对象进行间接的操作了。
2.1.4 封装的优点
可能有人会觉得这么做不是多此一举吗?为什么要将变量设置成私有的呢?直接让它是公有的不就行了,实则不然。
-
首先,这么做可以可以隔离变化,便于改变内部实现
例如原本有一个类它有一个
name
变量和一个toString()
方法:
private String name = "";
public String toString() {
return name;
}
然而之后我们想要把姓名分别用姓和名表示,即需要两个变量firstName
和lastName
,只需要在类内部稍作修改即可。
private String firstName = "";
private String lastName = "";
public String toString() {
return firstName + lastName;
}
显然如果是将name
设置为公有的,并在其他类中调用了此对象,就无法实现如此便捷的修改。
-
其次,更改器方法可以执行错误检查
当涉及到需要修改私有变量的值时,应使用方法更改器(
set()
方法),这样做的好处是我们可以在方法中就对此操作进行检查,如修改年龄时首先检查年龄是否小于0。 -
最后,这样可以保证数据安全性
在编写
get()
方法时,应该注意不要直接返回可变对象。如果直接返回可变对象,在外部类中就可以对其直接做出任何修改,这将是不可控的,我们不能对其安全性做出任何保证,同时排查错误也将变得更加的困难,将对象变量设置成公有的亦然。因此我们最好只返回可变对象的某些信息,而不要直接将对象作为返回值(对8大基本类型则无此限制)。
2.2 继承
继承使得人们可以基于已经存在的类构造一个新类,给程序设计带来了极大的可扩展性。“子承父类”,子类可以通过继承获得父类的方法和域,在此基础上子类可以添加一些新的方法和域,或者重新定义(覆盖,override)从父类中继承的方法。
2.2.1 Java中的继承
Java中用关键字extends
表示继承,如:
public class Student extends Person {
...
}
表示Student类继承了Person类,它将拥有Person类的所有方法和域。
2.2.2 覆盖(override)
如果想在子类中重新定义父类中的某个方法,可以利用覆盖来实现,如重新定义Person类中的toString()
方法:
public class Person {
private String name = "";
// 所有的类都继承自Object类,覆盖其toString()方法
@Override
public String toString() {
return name;
}
}
public class Student extends Person {
private int studentNumber = 0;
// 应使用@override对覆盖父类的方法进行标记
@Override
public String toString() {
return super.toString() + studentNumber;
}
}
当我们在子类中覆盖了父类的toString()
方法之后,再用子类的对象调用此方法,调用的就将是子类中的toString()
方法。
注:在覆盖父类方法时,可能会出现因方法的参数类型与父类不匹配,其结果并没有覆盖父类的方法(本意是想要覆盖的)。使用@override
对覆盖超类的方法进行标记后,就可以检查这种错误,如果我们定义的是一个新方法,编译器就会给出错误报告。
当一个方法覆盖另一个方法时,还可以指定一个更严格的返回类型,这里的严格指的是其子类。例如:
public class Person {
private Object name = "";
public Object getName() {
return name;
}
}
public class Student extends Person {
// 覆盖父类的 toString 方法时,指定了一个更严格的返回类型 String(继承于Object)
@Override
public String getName() {
return ((String) super.getName());
}
}
并且,在这种情况下,getName 方法依旧具有多态性(其实现依赖于桥方法)。
2.2.3 什么时候该使用继承
“is-a”是判断是否应该设计为继承关系的简单原则,它表明子类的每个对象也是父类的对象,即被包含的关系。
例如,动物和狮子,动物是个更大的范围,狮子属于动物的一种。在程序设计中我们就可以将动物定义成父类,让狮子是继承它,成为它的子类。
2.2.4 接口(interface)
Java不支持多继承,而是通过接口来实现多继承的功能。Java的类只能有一个父类,即只能继承一个类,但它可以实现(implements
)一个或多个接口。 而接口与类不同,接口可以继承(extends
)多个的接口。
2.2.5 为什么要用接口代替多继承
C++具有多重继承特性,但随之也带来了一些注入虚基类、控制规则等复杂特性。Java的设计者选择了不支持多继承,其主要原因就是多继承会让语言变得十分复杂。
实际上,接口可以提供多重继承的大多数好处,同时还能避免多继承的复杂性。
2.3 多态
多态可以理解为同一行为具有不同的表现形式。即同一接口,因具体的实例不同,接口的行为也有所不同。
2.3.1 对象变量是多态的
在Java中,对象变量是多态的,父类变量除了可以引用它自身的对象之外,还可以引用继承了它的任何一个子类的对象。例如
Student student = new Student();
Person person = student; // OK
在这里一个Person类型的变量引用了一个Student的对象,这是合法的,因为Student类继承了Person类,但反之则不行:
Student student1 = new Person(); // error
这就像是前面继承中所说的动物和狮子,我们可以说狮子是一种动物,但显然说动物是一种狮子是不合理的。
2.3.2 方法调用是多态的
再来看下面的代码:
Person person = new Person();
person.toString();
这里就会有疑问了,我用Person类型的变量引用了一个Student的对象,又调用了它的toString()
方法,那么此时调用的是Person类的toString()
方法还是Student类的toString()
方法呢?
答案是Student类的toString()
方法,即Java的方法调用是多态的,同一个Person变量,如果它引用了Person类的对象,那么它调用的将是Person类的toString()
方法,而如果它引用了Student类的对象,那么调用的就将会是Student类中的toString()
方法。
Java方法调用的多态性的实现,依赖于动态绑定。
2.3.3 动态绑定
当程序运行,并且采用动态绑定调用方法时,虚拟机会调用与变量所引用对象的实际类型最匹配的那个类的方法。
就如上节的例子所示,变量person
所引用的对象的实际类型是Student类,当执行person.toString()
时,会首先在Student类中搜索,如果Student类中定义了此方法,则直接调用;如果没有,就到它的父类中搜索,以此类推。
每次调用方法都要进行搜索的话,时间开销会非常大。因此虚拟机会预先为每个类创建一个方法表,其中列出了所有方法的签名和实际调用方法。这样一来,在真正调用方法的时候,虚拟机直接查表即可。
2.2.4 静态绑定
如果是private
、static
、final
方法或者构造器,编译器可以准确的知道应该调用哪个方法,我们将这种调用方式称为静态绑定。
2.2.5 多态的实现方法
方法一:重写(上述例子就是利用了重写)
方法二:抽象类和抽象方法
方法三:接口