Java基础知识回顾之三 ----- 封装、继承和多态

前言

在上一篇中回顾了java的修饰符和String类,这篇就来回顾下Java的三大特性:封装、继承、多态。

封装

什么是封装

在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部份包装、隐藏起来的方法。

封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。要访问该类的代码和数据,必须通过严格的接口控制。

封装最主要的功能在于我们能修改自己的实现代码,而不用修改那些调用我们代码的程序片段。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。

简单的来说,就是将Java中的经常用到的代码进行封装起来,形成一个方法。比如,我们常用的实体类,使用private修饰变量,用于保护数据;对外提供getter和setter方法,用于调用。这就是一种典型的封装。

代码示例:

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
public class packagingTest {
public static void main(String[] args) {
User user=new User();
//这里会报错,因为id和name是私有的,用于保护该数据
// user.id=10;
// user.name="张三";
user.setId(1);
user.setName("张三");
System.out.println(user.getId());
System.out.println(user.getName());
}
}
class User{
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

运行结果:

1
2
1
张三

使用封装的好处

  1. 良好的封装能够减少耦合。

  2. 类内部的结构可以自由修改。

  3. 可以对成员变量进行更精确的控制。

  4. 隐藏信息,实现细节。

继承

什么是继承

继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

继承的特性

  • 子类拥有父类非private的属性,方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。

为什么使用继承

继承主要目的是为了复用代码!简单的来说,就是将重复的代码抽出来,放到父类中,然后在再由子类继承使用,子类也是可以对父类进行扩展的。所以在继承关系中,可以这么理解,父类更通用,子类更具体。

打个比方,在动物世界中,猫和狮子是属于猫科,狗和狼是属于犬科。而它们也都是动物。猫和狮子有个共同的父类猫科,猫和狗有个共同的父类动物。所以它们是符合继承关系的, 只不过它们在行为上有所区别。猫和狗都有吃和睡,不过猫可以爬树,狗不可以。
这里,我们可以使用如下代码来进行说明。

代码示例:

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
public class extendTest {
public static void main(String[] args) {
Cat cat=new Cat();
Dog dog=new Dog();
cat.eat();
cat.sleep("cat");
cat.climbTree();
dog.eat("dog");
dog.sleep("dog");
}
}
class Animal{
public void eat(String name){
System.out.println(name+"正在吃东西...");
}
public void sleep(String name){
System.out.println(name+"正在睡觉...");
}
}
class Cat extends Animal{
private String name="Cat";
public void eat(){
super.eat(name);
System.out.println(name+"吃完了");
}
public void sleep(){
this.sleep(name);
}
public void sleep(String name){
System.out.println(name+"刚刚睡觉!");
}
public void climbTree(){
System.out.println(name+"正在爬树!");
}
}
class Dog extends Animal{
}

运行结果:

1
2
3
4
5
6
Cat正在吃东西...
Cat吃完了
cat刚刚睡觉!
Cat正在爬树!
dog正在吃东西...
dog正在睡觉...

在上述代码中,父类Animal实现了eat和sleep的方法,子类Cat和Dog使用了extends 关键字继承了父类Animal。子类Dog继承父类Animal之后什么都没做,而子类Cat不但继承了父类Animal,而且还增加了climbTree 方法,并且也重写了父类的eat和sleep方法。
在子类Cat中,出现了两个关键字:superthis
这两个关键字的意义如下:

  • super关键字:实现对父类成员的访问,用来引用当前对象的父类。
  • this关键字:指向自己的引用。

在上述代码中,子类Cat使用super关键字调用了父类Animal的eat方法,使用this关键字调用本类中的sleep方法。

说到继承,就不得不提这几个东西: final和protected修饰符、构造器、以及向上转型!
其中final和protected修饰符在上一篇java的修饰符和String类中已经讲解了,这里就简单的描述下。

  • final:修饰的类不可以被继承。
  • protected:修饰的类仅对同一包内的类和所有子类可见。

构造器

虽然子类可以继承父类的属性和方法(private修饰的除外),但是还有一样是子类无法继承的,那就是是构造器!对于构造器而言,它只能够被调用,而不能被继承。如果子类想使用父类的构造器,那么只需使用super关键字调用即可。

注:如果父类的构造器被private所修饰,那么是无法被外部调用的,包括子类!

向上转型

将子类转换成父类,在继承关系上面是向上移动的,所以一般称之为向上转型。
之前的个例子中,猫和动物是属于继承关系,那么我们可以把猫当作动物就是向上转型!
例如:

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
public class extendTest {
public static void main(String[] args) {
Animal animal=new Cat();
animal.eat("cat");
animal.sleep("cat");
}
}
class Animal{
public void eat(String name){
System.out.println(name+"正在吃东西...");
}
public void sleep(String name){
System.out.println(name+"正在睡觉...");
}
}
class Cat extends Animal{
private String name="Cat";
public void eat(){
super.eat(name);
System.out.println(name+"吃完了");
}
public void sleep(){
this.sleep(name);
}
public void sleep(String name){
System.out.println(name+"刚刚睡觉!");
}
public void climbTree(){
System.out.println(name+"正在爬树!");
}
}

运行结果:

1
2
cat正在吃东西...
cat刚刚睡觉!

上述代码中完成了向上转型,但是在向上转型中是存在着一些缺憾的,那就是属性和方法的丢失。例如上述代码中就丢失了Cat类中的climbTree()方法和name属性。所以慎用向上转型!

多重继承

Java的继承是单继承,但是可以实现多重继承!
虽然一个子类只能继承一个父类,但是子类的子类也可以子类。
例如C类继承B类,B类继承A类,所以按照关系就是A类是B类的父类,B类是C类的父类。

继承的缺点

虽然继承大大提升了代码的复用性,但是也提高了类之间的耦合性!父类更改,子类就必须更改!因此可以说继承破坏了封装,因为对于父类而言,它的实现细节对与子类来说都是透明的。
所以慎用继承!!!

多态

什么是多态

多态是指事物在运行过程中存在不同的状态。

多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定。

使用多态的必要条件

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

  1. 要有继承关系;
  2. 子类要重写父类的方法;
  3. 父类引用指向子类对象,也就是向上转型。

多态使用的简单示例

在上面的继承讲解中,我们用到了猫和动物这两个之间的关系,这里我们也可以使用这,只需要稍微改下代码,就可以实现多态。

代码示例:

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
public class Test {
public static void main(String[] args) {
Animal animal=new Cat();
animal.eat();
}
}
class Animal{
private String name="Animal";
public void eat(){
System.out.println(name+"正在吃东西...");
sleep();
}
public void sleep(){
System.out.println(name+"正在睡觉...");
}
}
class Cat extends Animal{
private String name="Cat";
public void eat(String name){
System.out.println(name+"吃完了");
sleep();
}
public void sleep(){
System.out.println(name+"正在睡觉");
}
}

输出结果:

1
2
Animal正在吃东西...
Cat正在睡觉

看到了运行结果之后,如果不熟悉多态的话,是不是感觉有些奇怪呢?
打印的第一句应该好理解,为什么打印的第二句不是Animal的方法,而是Cat中的方法呢?

我们知道多态是指事物在运行过程中存在不同的状态。而这里,我们用到了继承、重写以及向上转型。
在这里顺便提一下:

在向上转型中,一个父类的引用是可以指向多种子类对象,那么在运行时对于同一个消息是由实际的被引用的对象的类型来决定。

根据上述这段理解,我们再来看刚刚的那个示例。
在Cat类中,重写了父类Animal的sleep方法,并重载了eat方法。重载之后的eat(String name)方法和父类Animal的eat()方法不是同一个方法,因为是会在向上转型丢失的。而Cat子类重写了sleep方法,因此在向上转型的时候是不会丢失的,并且因为指定对对象的引用类型是Cat,所以Animal在调用eat()方法的时候,先是调用本类中eat()方法,然后在调用子类中的sleep()方法!

结论:

当父类引用指向子类方法时,必须调用那些父类中存在的方法,如果子类中对该方法进行了重写,那么在运行时就会动态调用子类中的方法,这就是多态。

使用多态的优点

摘自:https://www.cnblogs.com/jack204/archive/2012/10/29/2745150.html

  1. 可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
  2. 可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
  3. 接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
  4. 灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
  5. 简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

在对多态有一定的认识之后,可以尝试看看如下代码。
这是一个经典的多态问题,摘自:
http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx

代码示例:

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
public class extendsTest {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
class C extends B{
}
class D extends B{
}

运行结果:

1
2
3
4
5
6
7
8
9
1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D

分析

①②③比较好理解,一般不会出错。④⑤就有点糊涂了,为什么输出的不是”B and B”呢?!!先来回顾一下多态性。

运行时多态性是面向对象程序设计代码重用的一个最强大机制,动态性的概念也可以被说成“一个接口,多个方法”。Java实现运行时多态性的基础是动态方法调度,它是一种在运行时而不是在编译期调用重载方法的机制。

方法的重写Overriding和重载Overloading是Java多态性的不同表现。

重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloaded的方法是可以改变返回值的类型。

当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。(但是如果强制把超类转换成子类的话,就可以调用子类中新添加而超类没有的方法了。)

好了,先温习到这里,言归正传!实际上这里涉及方法调用的优先问题 ,优先级由高到低依次为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。让我们来看看它是怎么工作的。

比如④,a2.show(b),a2是一个引用变量,类型为A,则this为a2,b是B的一个实例,于是它到类A里面找show(B obj)方法,没有找到,于是到A的super(超类)找,而A没有超类,因此转到第三优先级this.show((super)O),this仍然是a2,这里O为B,(super)O即(super)B即A,因此它到类A里面找show(A obj)的方法,类A有这个方法,但是由于a2引用的是类B的一个对象,B覆盖了A的show(A obj)方法,因此最终锁定到类B的show(Aobj),输出为”B and A”。

再比如⑧,b.show(c),b是一个引用变量,类型为B,则this为b,c是C的一个实例,于是它到类B找show(C obj)方法,没有找到,转而到B的超类A里面找,A里面也没有,因此也转到第三优先级this.show((super)O),this为b,O为C,(super)O即(super)C即B,因此它到B里面找show(Bobj)方法,找到了,由于b引用的是类B的一个对象,因此直接锁定到类B的show(B obj),输出为”B and B”。

按照上面的方法,可以正确得到其他的结果。
问题还要继续,现在我们再来看上面的分析过程是怎么体现出蓝色字体那句话的内涵的。它说:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。还是拿a2.show(b)来说吧。

a2是一个引用变量,类型为A,它引用的是B的一个对象,因此这句话的意思是由B来决定调用的是哪个方法。因此应该调用B的show(B obj)从而输出”B and B”才对。但是为什么跟前面的分析得到的结果不相符呢?!问题在于我们不要忽略了蓝色字体的后半部分,那里特别指明:这个被调用的方法必须是在超类中定义过的,也就是被子类覆盖的方法。

B里面的show(B obj)在超类A中有定义吗?没有!那就更谈不上被覆盖了。实际上这句话隐藏了一条信息:它仍然是按照方法调用的优先级来确定的。它在类A中找到了show(Aobj),如果子类B没有覆盖show(A obj)方法,那么它就调用A的show(Aobj)(由于B继承A,虽然没有覆盖这个方法,但从超类A那里继承了这个方法,从某种意义上说,还是由B确定调用的方法,只是方法是在A中实现而已);现在子类B覆盖了show(A obj),因此它最终锁定到B的show(A obj)。这就是那句话的意义所在。

其它

参考:
http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx
https://www.cnblogs.com/jack204/archive/2012/10/29/2745150.html
https://blog.csdn.net/chenssy/article/details/12786385

到此,本文就结束了,谢谢阅读!欢迎留言和点赞,你的支持是我写作最大的动力!
版权声明:
作者:虚无境
博客园出处:http://www.cnblogs.com/xuwujing
CSDN出处:http://blog.csdn.net/qazwsxpcm    
个人博客出处:http://www.panchengming.com

+
------ 本文结束 ------