背景:
Java之所以能风靡全球并且一直保持领先的原因之一:必定是他面向对象编程(OOP)的思想。诸如更快的开发和更廉价的维护成本,使建模变得更简单,能够很轻松应对大体量的工程项目,使得代码更加优雅等优势已经让人烂熟于心。
当然,对于初学者而言,面向对象是肯定没有面向过程那样容易理解的。首先,我们要明白,什么是对象?
类和对象
所谓对象,可谓是万物皆对象。比如一台电脑,某部手机等。要了解对象,就要先了解类。
类:类是一类事物的抽象,这类事物拥有相同的特征。在Java中,我们可以理解为拥有相同属性和方法的一类事物的抽象描述。举个例子。人可以抽象为一个类(Person),如果需要,我们可以继续将人这个类细化为 男人(Man)和 (Woman)两个子类。当然我们还能以其他维度去抽象出其他子类。而作为类,他的本质作用是定义这类事物所拥有的属性和方法,并不一定要给出具体的属性值和方法行为。 它仅仅只是做一个定义或者规范。
对象:我们在Java中经常说:实例化一个对象,或者类的实例化。其实本质就是从抽象的类中,去构造出一个具体的对象。比如我们可以实例化一个叫做张三,跑步很快的(男)人。这个过程叫做实例化,你可以理解为具体化的过程,相当于我们从(男)人这个类中去实例化一个具体的人张三,赋予他一个名字(属性)叫做张三,赋予它一个行为(方法)叫做跑步,并且跑步跑得很快。
三大特征
面向对象的三大特征:封装、继承、多态
封装:
在Java中,封装其实指的是将属性和实现功能的细节封装成抽象的类,仅仅提供给外界访问接口和功能说明。比如查询订单的接口:你会得到接口地址和这个接口的功能,而并不知道这个接口做了哪些逻辑。实际上,作为调用者,也确实不需要知道接口的实际实现功能的过程。并且,封装会把属性私有化(对每个属性提供getter和setter方法),以防外界非法修改。由此可见,封装的好处就是增强安全性和简化编程。
在对属性私有化一般使用private关键字,以下是各个关键字的访问范围,可以给我们提供不同级别的保护。
访问修饰符 | 同类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | |
default | √ | √ | ||
private | √ |
继承:
继承就是子类继承父类的行为,使得子类获得父类的属性和行为。子类也可以根据自身实际情况去重写父类属性和方法,或者拓展新的属性和方法。不过要注意的是:Java不支持多继承(但支持多级继承)。继承使用的关键字是extends。
在开发过程中,我们可以把公共的方法抽取到父类中去。有效的抽取能提高代码的复用率和代码维护性。继承的弊端是会增加代码耦合度(这边的耦合是相对的,有效的继承是利大于弊)。
继承的注意点:
1. 子类只能继承(重写)父类所有非私有的属性和方法(父类内部使用的属性依然可以封装起来)。
2. 继承无法继承构造方法,只能通过super访问父类构造方法。
3. 子类重写父类方法时,访问权限不能更低。
4. 父类的静态方法,子类也必须通过静态方法去进行“重写”(子类为静态,父类也必须为静态)。
5. 子类构所有构造方法默认都会访问父类的无参构造方法(子类每一个构造方法的第一条语句默认都是:super().只是没有显现出来),所以在设计父类的时候,最好保证父类有一个无参构造。
6. 子父类初始化顺序,先初始化父类,再初始化子类(分层初始化)。
7. 因为子类会调用父类的数据,所以默认会初始化父类。
/**
* 这边引入的一个知识点:
* 先是加载类,
* 加载类的时候,
* 会先初始化成员变量和成员方法,
* 然后实例化类,会调用构造方法,
* 所以就导致一个现象,
* 先初始化成员变量,再加载构造。
*
* 一个类的初始化过程:
* 成员变量的初始化过程
* 默认初始化
* 显示初始化
* 构造方法初始化
* 子父类的初始化(分层初始化)
* 先进行父类初始化,然后进行子类初始化。
*/
class Y{
public Y() {
System.out.print("Y ");
}
}
class X {
Y b = new Y();
public X() {
System.out.print("X ");
}
}
public class Z extends X{
Y y = new Y();
public Z() {
super();
System.out.print("Z ");
}
public static void main(String[] args){
new Z();
}
}
// 结果YXYZ
/**
* 解析:
* 在main函数中,new Z();的时候,因为Z继承了X,所以先初始化父类X。
* 而父类中,有成员变量和构造方法。而加载类的时候,又会先初始化成员变量和成员方法,再调用构造方法实例化类。
* 所以:
* 先执行了X 中的 Y y = new Y(); 打印了Y
* 然后执行X 中的构造方法实例化类,打印了X
* 执行完上面步骤,初始化父类完成,继而执行Z中的 Y y = new Y(); 打印了Y
* 最后,执行Z中的构造方法实例化Z,打印了Z
*/
/** 比较常见的继承父类方法,并拓展父类方法。
* 这边用到super调用父类被覆盖的方法。
*/
class Bird{
public void sing(){
System.out.println("Bird sing~");
}
}
class Parrot extends Bird{
public void sing(){
super.sing();
System.out.println("Parrot also can repeat words!");
}
}
public class BirdSing {
public static void main(String[] args) {
Parrot parrot = new Parrot();
parrot.sing();
}
}
/*
看程序写结果:
A:一个类的静态代码块,构造代码块,构造方法的执行流程
静态代码块 > 构造代码块 > 构造方法
B:静态的内容是随着类的加载而加载
静态代码块的内容会优先执行
C:子类初始化之前先会进行父类的初始化
结果是:
静态代码块Fu
静态代码块Zi
构造代码块Fu
构造方法Fu
构造代码块Zi
构造方法Zi
原因:
其实Zi z = new Zi();过程是: 先Zi z。先把Zi类加载到栈内存中(加载类),然后再去执行 = new Zi(),实例化Zi,在堆内存中给zi开辟空间。
在Zi z 时(加载类):要先加载Zi类,在加载子类的时候发现继承了Fu类,所以在加载父类的时候,先执行了Fu()的静态代码块
然后执行子类代码块。
在= new Zi();时(构造):发现有父类,所以先走父类的构造代码块,父类的构造方法,再走子类的构造代码块,子类的构造方法。
*/
class Fu {
static {
System.out.println("静态代码块Fu");
}
{
System.out.println("构造代码块Fu");
}
public Fu() {
System.out.println("构造方法Fu");
}
}
class Zi extends Fu {
static {
System.out.println("静态代码块Zi");
}
{
System.out.println("构造代码块Zi");
}
public Zi() {
System.out.println("构造方法Zi");
}
}
class Test2 {
public static void main(String[] args) {
Zi z = new Zi();
}
}
继承概念的实现方式有三类:实现继承、接口继承和可视继承。
1. 实现继承:直接使用父类的属性和方法,无需额外编码。
2. 接口继承:继承父类属性和方法的名称,子类需要提供实现能力。
3. 可视继承:子窗体(类)使用基窗体(类)的外观和实现代码的能力。
多态:
多态可以让同一个行为具有多个不同表现形式或形态的能力。比如打印机,最终可以有激光打印机和喷墨打印机,黑白打印机和彩色打印机等。我们常见的多态有List<Integer> list = new ArrayList<>();
多态是一种动态绑定(dynamic binding:指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法),他可以有效的解决继承带来的耦合问题。
多态实现必备条件:
1. 要有继承关系或实现关系
2. 要有方法重写
3. 要有父类引用指向子类对象: 父类 f = new 子类();
多态中的成员访问特点:
1. 成员变量
编译看等号左边(父类),运行看左边(父类)。
2. 构造方法
创建子类对象时,访问父类的构造方法,对父类的数据进行初始化
2. 成员方法
编译看等号左边(父类),运行看右边(子类,其实父类引用还是会调用自己的方法,然后因为是重写导致出现了子类的结果,并非父类调用到子类的方法)(出现重写(重写就是覆盖)) 父类不能使用子类特有的功能(Fu f=new zi() f是父类的对象)。
1. 静态方法
编译看、等号左边(父类),运行看左边(父类)。 静态和类相关,算不上重写,所以访问还是看左边。
静态的方法可以被继承,但是不能重写。如果父类中有一个静态的方法,子类也有一个与其方法名,参数类型,参数个数都一样的方法,并且也有static关键字修饰,那么该子类的方法会把原来继承过来的父类的方法隐藏,而不是重写。通俗的讲就是父类的方法和子类的方法是两个没有关系的方法,具体调用哪一个方法是看是哪个对象的引用;这种父子类方法也不在存在多态的性质。《Java编程思想》中这样提到“只有普通的方法调用可以是多态的”。
多态的好处:
1. 提高了代码的维护性(继承保证)
2. 提高代码的扩展性
多态的弊端:
1. 多态不能使用子类特有的功能。如需使用子类特有功能,需要向下转型。
在Java中,我们提供了解决多态弊端的方法:向下转型。
向上转型和向下转型是多态中常用的一些方式。向上转型:Fu f = new Zi(); 而向下转型:Zi z = (Zi)f; 向下转型,只能转自己或者自己的“父集”。
/*
多态的弊端:
不能使用子类的特有功能(子类有得方法,父类没有的话,可以编译通过,但是运行的时候,用父类的引用调用子类的方法时调用不到的。)。
我就想使用子类的特有功能?行不行?
行。
怎么用呢?
A:创建子类对象调用方法即可。(可以,但是很多时候不合理。而且,太占内存了)
B:把父类的引用强制转换为子类的引用。(向下转型)
对象间的转型问题:
向上转型:
Fu f = new Zi();
向下转型:
Zi z = (Zi)f; //要求该f必须是能够转换为Zi的。
*/
class Fu {
public void show() {
System.out.println("show fu");
}
}
class Zi extends Fu {
public void show() {
System.out.println("show zi");
}
public void method() {
System.out.println("method zi");
}
}
class DuoTaiDemo4 {
public static void main(String[] args) {
// 向上转型
Fu f = new Zi();
f.show();
//f.method();
//创建子类对象
//Zi z = new Zi();
//z.show();
//z.method();
//你能够把子的对象赋值给父亲,那么我能不能把父的引用赋值给子的引用呢?
//如果可以,但是如下
Zi z = (Zi)f;
z.show();
z.method();
}
}
五大基本原则
- 单一职责原则(Single Responsibility Principle)
单一职责原则的规则是:每一个类应该专注于做一件事情。可以降低类的复杂性和变更引起的风险,提高类的可读性和维护性。各司其职,有条不紊。此原则的核心就是解耦和增强内聚性。 - 开放封闭原则(Open Close Principle)
开闭原则的规则是:面向扩展开放,面向修改关闭。在设计一个模块时,应该使得这个模块不被修改的前提下被拓展。多态就是一种开放封闭原则的践行方式。 - 里氏替换原则(Liskov Substitution Principle)
里氏替换原则的规则是:超类存在的地方,子类是可以替换的。里氏替换原则的核心是:子类可以扩展父类的功能,但不能改变父类原有的功能。 里氏替换原则这样做,能够保证,核心公共功能都由父类保证,在需要修改这些功能时,只需要修改父类方法就行。如果存在子类覆盖,那么这些子类就不能及时获取到父类的修改。尤其是在项目比较庞大的时候。往往会忽略这些子类的存在,导致一些异常问题。
里氏替换原则强调的是不修改父类原有功能,主要是防止覆盖父类方法后,让父类失去“统一管理”的功能,在修改父类后,子类无法获取最新修改。这似乎和多态有点矛盾。这该怎么办呢?
其实我们可以这么理解,里氏替换原则其实是一种指导。在公共配置中,我们常会在父类里面写好实现方法,子类继承即可,这样可以避免代码重复性。这种情况下,尽量遵守里氏替换原则,因为公共方法需要“统筹安排”,而类似业务问题,本来就是存在各不相同的表现或者实现方式,这种情况下,一般的做法是,父类中仅定义抽象方法,子类去做特有实现,当然,这种情况下也可能会有小部分业务抽取,这种时候,我们需要拥抱变化,要注意尽量规避日后的重写。 - 依赖倒置原则(Dependence Inversion Principle)
里氏替换原则强调在设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。比如抽象不应该依赖细节,而细节应该依赖抽象。这样可以有效地降低耦合。增强代码可读性和可维护性。
这么说还是比较抽象,我们用代码来讲解。我们以前比较喜欢这么写代码:
public class Course {
public void javaCourse(){
System.out.println("Java Study");
}
public void pythonCourse(){
System.out.println("Python Study");
}
// ...
}
public class StudyDemo {
public static void main(String[] args) {
Course course = new Course();
course.javaCourse();
course.pythonCourse();
}
}
当新增课程时,就会去Course类里面新增方法,所以,每次新增课程,都需要从底层实现到高层调用依次地修改代码。这就意味着高层调用依赖着底层的实现。这样的新增,看上去不会动到原有代码,似乎是安全的。但是实际情况要比这个复杂很多,可能某次一不小心就没有hold住变化。
所以,最好的打算是,我们写好的代码,尽量不要去修改它。
我们仔细思考,课程是一个平行概念。这样我们就可以抽取一个接口,然后每次新增课程,实现它就行。
// 接口
public interface ICourse {
void study();
}
// 课程方法实现
public class JavaCourse implements ICourse{
@Override
public void study() {
System.out.println("Java Study!");
}
}
// 课程方法实现
public class PythonCourse implements ICourse{
@Override
public void study() {
System.out.println("Python Course");
}
}
// 提供调用工厂
public class StudyFactory {
public void study(ICourse course) {
course.study();
}
}
// 业务高层
public class StudyDemo {
public static void main(String[] args) {
StudyFactory factory = new StudyFactory();
factory.study(new JavaCourse());
factory.study(new PythonCourse());
}
}
- 接口隔离原则(Interface Segregation Principle)
接口隔离原则的规则是:客户端不应该依赖他们不需要的方法,依赖的接口应以最小为原则。
这就要求我们定义接口,也要以最小为原则,应将方法分类分组,不应该将毫无关系的方法塞进一个接口。所以我们可以将接口隔离理解为:我们可以利用接口定义来最小化分组功能方法。