生动形象地了解Java中封装、继承和多态的特性

1,550 阅读15分钟

一谈到Java肯定就要谈到面向对象,万物皆对象是每个Java学习者都听了不知道多少遍的话,然而很多刚学编程的人学到这里会一脸懵逼,就算跟着教程一步一步敲代码了,也就知道建个类要加class关键字,也知道类有属性和方法,也知道继承用extends关键字。至于属性和方法具体怎么声明和定义,继承后子类和父类的关系具体又是怎样的,就云里雾里了。

本文会尽量用最通俗易懂的语言来描述面向对象中的三大特性,这个东西比较抽象,学会的话就觉得在码代码的时候用起来特别方便,不会的话就会觉得太难了。任何一本Java书籍或者视频在讲到面向对象这一块的时候,都会花比较多的篇幅来讲解,本文也达不到将所有知识点全部讲出来,因为其中的细节确实挺多,而新手懵也就懵在了这么多细节上了。不过,最开始学习的时候你只需要掌握平常的基本使用就可以,至于其他细节在使用的过程中就会慢慢学会!

思路

了解一个东西怎么使用其实不难,就好像你要去了解一个杯子具体是怎样生产的和背后生产的原理,你肯定要懵,但是只让你拿一个杯子去装水却轻而易举,你在使用的过程中完全不需要关心杯子的细节,你只需要会用就行。

而很多人问题就出在这里,大部分人是“杯子”是什么都没弄清楚,脑袋里就压根没“杯子”这个概念,就更不用谈去使用“杯子”了。 所以,本文的重点就是让大家理清面向对象中 封装、继承和多态 这三个概念,代码会贴的比较少。让大家知道“杯子”大概是怎么就行了,后续的使用过程中会掌握更多细节的。

在讲解具体的概念之前,要和大家强调一点: 面向对象是为了方便我们构建代码,并不是增加我们构建代码的难度!!! 面向对象是为了方便我们构建代码,并不是增加我们构建代码的难度!!! 面向对象是为了方便我们构建代码,并不是增加我们构建代码的难度!!!

一定要以,啊我多了一个特别好用的工具,这种思维来去学习,而不是以,啊我多了一个负担这种思维

封装

封装只是一种概念,也是面向对象非常重要的一个特性。

其实封装在我们生活中无处不在,在平常构建代码的过程中也经常用到封装,封装就是隐藏细节: 你到银行取钱你只需提供银行卡和密码,柜员会将卡里的钱给你,至于柜员是在柜台后面如何验证你的密码、余额,又是如何从金库里拿到现金再给你,你都不知道也无需知道,这就是封装。银行封装了柜员在柜台后面操作的细节 再比如你到餐厅里去吃饭,你点好菜之后只需要等待服务员将菜端上给你,而不知道这个菜是如何做好的,这也是封装。餐厅封装了厨师在厨房里做菜的细节。

生活中封装的例子随处可见,在程序中封装也无处不在: 你调用Java库中的某个方法来实现某个功能,你只需要传入正确的参数即可让方法运行达到你想要的目的,至于方法内部进行了怎样的操作,具体是怎样完成这个功能的你都不知道也无需知道。方法封装了方法内部的细节。 你使用某个类的实例,你创建出一个对象,你如愿以偿拿到了对象的引用,但是这个对象具体是怎样构造出来的你不知道,构造函数里的细节你不清楚,你也不会知道有哪些私有数据,要操作这个对象一律得通过提供的方法来调用。构造函数封装了对象初始化流程。private封装了对象的私有数据成员

所以封装的主要目的就是隐藏细节,将对象当做黑箱进行操作。

继承

面向对象中子类继承父类,主要是为了避免重复的行为定义,比如Animal类你定义了 体重年龄等多个属性,又有 吃东西等多个行为,然后你又想弄出一个Dog类,你会发现Dog类我要定义的属性和行为Animal类都有,那我就无需再重新定义一遍了,直接将Dog类继承Animal类,就会让Dog类拥有Animal类的属性和行为。

不过并非为了避免重复定义行为就要使用继承,滥用继承而导致程序维护上的问题时有耳闻。如何正确判断使用继承的时机,以及继承之后如何活用多态,才是学习继承的重点。而这些在我们没有理清继承的概念之前是无法做到的。

理清继承的概念,可以简单使用 “is-a”来判断子类和父类的关系,中文称为“是一种”的关系。简单来说,如果你可以这样描述:B是一种A,那么B继承了A。能通过“is-a”测试,在逻辑上和程序中就能顺利使用继承关系

比如现在Dog类和Cat类继承了Animal类:

/*创建两个对象,这肯定没问题*/
Dog dog = new Dog();
Cat cat = new Cat();

/*这两句也可以通过编译*/
Animal animal1 = new Dog(); // Dog是一种Animal
Animal animal2 = new Cat(); // Cat是一种Animal

/*但是下面两句就无法通过编译*/
Dog dog = new Animal(); // Animal不一定是Dog,所以编译失败
Cat cat = new Animal(); // Animal不一定是Cat,所以编译失败

前两段代码通过了“is-a”测试,自然是没什么问题,而第三段代码就没有通过“is-a”测试,所以自然编译失败。编译器就是语法检查器,检查的方式是从 = 号右边往左读:右边是不是一种左边呢(右边类是不是左边类的子类)?

上面代码不管是从逻辑上还是程序上都好理解,我们来看下面的代码:

Animal animal = new Dog(); // Dog是一种Animal,这一行可以通过编译
Dog dog = animal; // 这一句话编译失败

第一行没话说,都知道能通过编译,那么第二行是为啥编译失败呢? 编译器检查语法一次只看一行,它又无法联系上下文来做阅读理解,animal你是用Animal类来做声明的,自然而然编译器将就你认定为Aniaml类,尽管你指向的是子类Dog,但是animal现在的“身份”还是Aniaml类。所以,第二行无法通过“is-a”测试,编译失败。

编译器会检查父子类之间“is-a”关系,如果你不想要编译器啰嗦可以叫它住嘴:

Animal animal = new Dog();
Dog dog = (Dog)animal;

对于第二行本来编译器是要提示你,animal不一定是Dog类的对象哦,但是你加上强制类型转换让它住嘴了,编译器就让这段代码通过编译了,不过后果得自行负责

就上面这个代码来说,animal确实指向了Dog类实例,所以第二行让Dog实例转换成Dog并没有什么问题,执行期间也不会出错。但是下面代码可以通过编译,但是运行时却会报错:

Animal animal = new Dog();
Cat cat = (Cat)animal;

你让编译器闭嘴了,编译器让你通过了编译,不过在运行时发现animal明明是指向的Dog类,你让Dog转换成Cat那肯定是不行的!因为Dog并不是一种Cat

综上所述,使用“is-a”原则就可以判断何时编译成功,何时编译失败,并留意指向的实例类型,就可以判断何时扮演成功,何时会抛出异常! 然后在要使用继承前,也可以在逻辑上判断是否该使用继承: 狗是动物吗?是的,所以狗继承动物没有一点问题。猫是动物吗?是的,所以猫继承动物也没有一点问题。树是一种动物吗?不是,这在逻辑上说不过去,所以在程序中也不应该让树继承动物类,否则会导致程序的整体逻辑混乱。 在程序中,并没有任何限制可以防止你不继承某个类,你当然可以让Tree extends Animal,但是这样是完全不合理的,作为开发者应当避免这种乱继承的行为!

上面这个例子还非常好判断,但是将来开发的过程中各种类的关系错综复杂,就需要好好理清“is-a”关系了。 比如 酒店浴室是一种浴室吗?是的,那让酒店浴室类继承浴室没有问题。家用浴室是一种浴室吗?是滴,让家用浴室继承浴室类也没有问题。 那么,浴盆是一种浴室吗? 好像挺有关系的,不过稍微一琢磨倒也能理清,浴盆与其说 is a 浴室,倒不如说 浴室 has a 浴盆,这就牵扯到另一个原则了 “has-a” 原则,中文来说就是 “有某个”东西。 浴室里可以有浴盆,所以浴盆应当是浴室的一种属性,而不是一种子类。所以将浴盆定义成浴室的属性就要比浴盆继承浴室要好很多!

理清继承,用最简单的办法就是: “is-a”原则帮助你判断父子类的关系,“has-a”原则帮助你判断类与成员的关系!

多态

多态,即多种形态或者多种状态。听起来挺深奥,其实非常简单,多态在生活中也无处不在: 假设你现在开了一个宠物店,提供给宠物洗澡的服务,于是你吩咐店员贴一个公告,咱们店可以给所有哈士奇洗澡!于是附近的居民都带着哈士奇过来洗澡了,你高兴的不得了,因为赚了钱。 这个用程序的语言来描述就像你这个宠物店给大家提供了一个方法:

// 参数是哈士奇类,执行的功能洗澡
public static void shower(哈士奇 h);

但是好像有一个问题,有居民带着一个金毛过来准备给金毛洗澡,但是店员不允许金毛过来洗,因为你吩咐的就是让哈士奇洗,金毛并不是哈士奇,所以不行。 又有其它居民带着萨摩耶、柴犬啥的过来,店员都把他们拒之门外了,更有甚者还有人带着猫过来,那就更不用多说了,不准洗。 你失去了很多顾客,但是你还没办法生气,因为店员尽责得执行了你下达的指令:

shower(哈士奇类的对象);	 // 通过,因为传进来的参数是匹配的
shower(金毛类的对象); 	 // 报错,因为传进来的参数类型不是哈士奇
shower(萨摩耶类的对象); 	 // 报错,因为传进来的参数类型不是哈士奇
shower(猫类的对象); 		 // 报错,因为传进来的参数类型不是哈士奇

你发现自己好像做了一件傻事,自己明明是个宠物店却只限制让哈士奇过来洗澡,那自然生意就限制了很多很多。于是乎你决定让各种各样的宠物都可以过来洗澡,哈士奇,金毛也行,萨摩耶也行:

public static void shower(哈士奇 a); // 参数是哈士奇类,执行的功能洗澡
public static void shower(金毛 a);	// 参数是金毛类,执行的功能洗澡
public static void shower(萨摩耶 a); // 参数是萨摩耶类,执行的功能洗澡
public static void shower(猫 a);	// 参数是猫类,执行的功能洗澡
...
...
...

你定义了一万个方法,因为宠物品种实在是太多了,你必须要考虑到各种各样的品种。然而你发现,这样成本太高了也太麻烦了,问题虽然能够解决但是却让你精疲力尽。o(╥﹏╥)o

于是你决定改变,你重新吩咐店员,只要是宠物(或动物)就都可以过来享受洗澡服务!这个指令一下达,生意立马就好了,O(∩_∩)O~ 因为别人不管是带着哈士奇过来还是金毛、萨摩耶或者猫过来,都可以。用程序描述就是:

// 参数是宠物(动物)类,执行的功能是洗澡
public static void shower(Aniaml a);

public static void main(String[] args) {
	shower(哈士奇类的对象);	// 通过,哈士奇类继承了Animal类
	shower(金毛类的对象); 	// 通过,金毛类继承了Animal类
	shower(萨摩耶类的对象); 	// 通过,萨摩耶类继承了Animal类
	shower(猫类的对象); 		// 通过,猫类继承了Animal类
}

这就是多态的一种表现,你明明只限定了一个参数类型,但是传进来的参数却各种形态都有。 是不是方便到爆了?

而通过上述例子咱们也可以发现多态的达成条件: 要有继承 父类引用指向子类对象

你参数定义的是Animal类,传进来的参数虽然可以多种多样,但是也不能瞎传参数进来,得是继承了Animal类的子类才可以。 然后呢,参数类型是父类,传进来的却是子类,在方法里面我操作的实例也是子类对象,这就是前面我们演示“is-a”原则时所体现出来的:父类的引用指向子类的对象。

这个理解后,咱们再来深入一些: 你现在宠物店决定拓展业务了,不光要能提供洗澡的服务,还要提供喂食的服务,你帮主人照看宠物进食。宠物进食自然就是宠物的行为(方法),你是照看宠物进食,又不是你进食,所以宠物来到宠物店后,你就要让宠物自己进食(即调用宠物的进食方法)。 用程序语言描述就是:

// 宠物类
class Animal{
	// 进食方法
	public void eat() {
		System.out.println("本宠物吃东西啦~~~~~~");
	}
}

// 你学聪明了,参数一开始设置的就是宠物类,这样所有宠物都可以进来
public static void helpEat(Animal a) {
	// 然后再调用宠物本身的进食方法
	a.eat();	// 打印的结果自然就是 “本宠物吃东西啦~~~~~~”
}

这么一看好像特别完美啊,但是你立马就发现问题了。每个不同的宠物品种喜欢吃的东西都不一样,吃东西的方式也不一样,可是现在你调用的是单一的宠物进食方法,不管是啥宠物进来打印的都是 “本宠物吃东西啦~~~~~~”。这就尴尬了,哈士奇是那种吃法,金毛是这种吃法,在你这难道就都变成了同一种吃法了吗。 所以要解决这个问题,应该体现出每个宠物品种自身的特性! 自身的特性用程序语言描述是啥呢? 就是方法的重写嘛:

// 哈士奇类继承了宠物类
class 哈士奇 extends Aniaml {
	@Override
	public void eat() {
		// 重写了父类的方法
		System.out.println("本哈士奇开始犯二地吃东西啦~");
	}
}

// 金毛类继承了宠物类
class 金毛 extends Aniaml {
	@Override
	public void eat() {
		// 重写了父类的方法
		System.out.println("本金毛开始随和地吃东西啦~");
	}
}

// 猫类继承了宠物类
classextends Aniaml {
	@Override
	public void eat() {
		// 重写了父类的方法
		System.out.println("本猫开始高冷地吃东西啦~");
	}
}

各品种的宠物重写了方法后,你宠物店再进行服务的时候效果立马就不一样了:

helpEat(哈士奇对象);	// 传进来的参数类型是哈士奇类对象,打印结果是:"本哈士奇开始犯二地吃东西啦~"
helpEat(金毛对象);	// 传进来的参数类型是金毛类对象,打印结果是:"本金毛开始随和地吃东西啦~"
helpEat(猫对象);		// 传进来的参数类型是猫类对象,打印结果是:"本猫开始高冷地吃东西啦~"

这就更能体现多态了,当父类的引用指向子类的对象时,调用的方法是子类重写后的方法,既体现了多种类型的传递,又体现了不同类型的特性。

多态是面向对象的核心机制,它将可扩展性达到了最好!

总结

面向对象的编程方式更贴近我们人类的理解和描述,很多人觉得难就是因为觉得这概念特别抽象,其实只要思路理清后,用平常生活中的思维去理解会发现使用面向对象时非常得心应手。

大家发现没有,这些特性其实在我们生活中随处可见,也非常好理解,这也就是我开头所说的:面向对象是为了方便我们构建代码,并不是增加我们构建代码的难度。

我个人认为,面向对象将程序语言描述和我们人类自然语言描述完美得结合在了一起。所以,面向对象只是比较抽象,但是并不难!希望大家理清思路后,带着愉快的心情去“享受”面向对象带来的便利!