Java面向对象与代码编写

Java面向对象与代码编写

面向过程的思想和面向对象的思想

       面向对象和面向过程的思想有着本质上的区别, 作为面向对象的思维来说,当你拿到一个问题时,你分析这个问题不再是第一步先做什么,第二步再做什么,这是面向过程的思维,你应该分析这个问题里面有哪些类和对象,这是第一点,然后再分析这些类和对象应该具有哪些属性和方法。这是第二点。最后分析类和类之间具体有什么关系,这是第三点。

      面向对象有一个非常重要的设计思维:合适的方法应该出现在合适的类里面

简单理解面向对象

  就是在程序里面首先分解出来的应该是注意不再是一步一步的过程了,而是首先考虑在这个问题域里面或者程序里面应该具有有哪些对象,所以从现在开始考虑任何问题脑子里不要再想着我实现这件事我第一步应该干什么,第二步应该干什么,如果这样想,那就是面向过程的思维了。面向对象的思维是,当我碰到这个问题域的时候,碰到这个程序的时候,我首先应该把这个问题里有哪些对象,对象与对象之间有什么关系抽象出来。

面向对象的设计思想

  面向对象的基本思想是,从现实世界中客观存在的事物出发来构造软件系统,并在系统的构造中尽可能运用人类的自然思维方式。

  面向对象更加强调运用人类在日常生活的逻辑思维中经常采用的思想方法与原则,如抽象、分类,继承、聚合、多态等。

  人在思考的时候,首先眼睛里看到的是一个一个的对象。

对象和类的概念

     对象是用于计算机语言对问题域中事物的描述,对象通过“属性(attribute)”和“方法(method)”来分别对应事物所具有的静态属性和动态属性。

     类是用于描述同一类的对象的一个抽象的概念,类中定义了这一类对象所具有的静态属性和动态属性

    类可以看成一类对象的模板,对象可以看成该类的一个具体实例。

    eg.什么叫瓶子?

     瓶子的定义:具有某些类特征的东西就是瓶子,比分说什么样的形状,比方说有个口,能倒水,能装水,一般有个盖等等。给瓶子下定义的过程,其实就是把瓶子里的某些东西抽象出来了,所以瓶子在这里是叫做一类事物的一个抽象,在你脑子里有瓶子的概念,可瓶子的概念在你脑子里到底是什么呢?瓶子的概念在你脑子里叫做一类事物的一个抽象。怎么抽象的呢?你往往抽象的是这两个方面:第一个方面我们叫它静态的属性,瓶子应该具有哪些特征,比分说瓶子应有个口,这是它的具有的一个静态属性,瓶子一般有一个盖,这也是它的具有一个静态属性,除此之外,你还可能给它总结动态的属性,什么动态的属性呢?比放说瓶子能倒水,这是它的动态属性。瓶子这个概念在你脑子里如果你细细的思维的话,其实你给它做了两方面的总结,一方面是静态的,一方面是动态的。反映到JAVA的类上,一个就是成员变量(静态属性),一个就是方法(动态属性)方法是可以执行的,可以动的。成员变量是某一个类的静态属性。所以你脑子里瓶子的概念实际上是一类事物的一个抽象,这种东西我们叫它类,椅子是类,桌子是类,学生是类。什么是对象呢?这一类事物的具体的某个实例就叫做对象。所以一类事物的具体的某一个东西,符合这类事物具体的特征的某个东西就叫做对象。瓶子是一个类,某个瓶子就是瓶子这个类里面的一个对象。

如何抽象出一个类?

       有两个方面,一方面是它的静态属性,另一方面是它的动态属性。反映到JAVA里面的类怎么包装它呢?一方面成员变量,另一方面是方法。

  eg.职员这个类该怎么抽象出来?也是从两个方面,一方面是它的静态属性,另一方面它的动态属性

  职员有哪些属性呢?有姓名,年龄,目前工资数额等属性,他有哪些方法呢?让这个职员来显示姓名,显示年龄,修改姓名,领取工资。当然显示姓名,显示年龄,修改姓名,领取工资这些也可以让别人来做,但面向对象的设计思维是最合适的方法应该出现在最合适的类里面。显示姓名,显示年龄,修改姓名,领取工资由谁来做更合适呢,那就是职员自己最合适。所以这些方法应该出现在职员这个类里面。

Java面向对象与代码编写_第1张图片

  对于类来说,它有一些属性或者称为成员变量,以后说属性或者成员变量指的是同一回事。具体的对象他有没有相关的一些属性或者叫成员变量呢?有,每一个人都有一份,只不过是取值不同而已。如从职员这个类实例化出来的两个职员:职员A和职员B,他们都有姓名,年龄,目前工资数额这些属性,但他们的名字,年龄,领取的工资数额都不一样。这样就能把职员A和职员B区分开来了,正是因为他们的属性值不一样,所以这个对象才能和另外的对象区分开来,所以通过属性是可以区分两个对象的。猫是一个类,这只猫是一个对象,这只猫和另外一只猫该怎么区分开来呢?那就得看你的猫这个类是怎么定义的了,猫有猫毛,毛有颜色。OK,这只猫是黑猫,另一只猫是白猫,这样通过猫毛的颜色区分开来了。如果只定义一个,如捉老鼠,白猫也能捉,黑猫也能捉,这样就没办法区分出黑猫和白猫了,所以根据方法是没办法区分两个对象的。所以每个对象都有自己的属性,属性值和另外一个对象一般是不一样的。

  一定要区分类和对象,什么叫做类?什么叫做对象?类是一类事物的一个抽象,具有共同特征的一类事物的一个抽象。对象是这个类具体的某一个实例,所以以后说实例(instance)或者说对象(object)指的是同一回事。

 

类(对象)之间的关系

关联关系(最弱的一种关系)

Java面向对象与代码编写_第2张图片

  类和类之间是有关系的,如学生和老师这两个类,老师可以教学生,学生可以向老师学习。这就是他们之间的关系。关系和关系之间是不同的,你和你老婆的关系和你和你其他女朋友的关系是不能混为一谈的。关系最弱的一种关系叫关联关系。关联关系反应到代码上往往是一个类的方法里面的参数是另一个类的具体的某一个对象,比如教授教研究生,教哪个研究生,教是教授这个类里面的一个方法,某个研究生是研究生这个类里面的一个具体的对象。关联关系是最弱的一种关系,咱们两个类之间有关系,或者两个对象之间有关系,但关系不是很紧密。

继承关系(比较强的一种关系)

Java面向对象与代码编写_第3张图片

  继承关系封装了这样一种逻辑:“XX是一种XX”,只要这东西能说通了,就可以考虑用继承关系来封装它。如:游泳运动员从运动员继承,游泳运动员是一种运动员,这能说得通,所以游泳运动员就是从运动员继承过来的,游泳运动员和运动员就是一种继承关系。学生是一个人,学生从人继承,老师是一个人,老师也从人继承,学生是一种老师,这说不通,所以学生和老师就不存在继承关系。所以将来做设计的时候要分清继承关系很简单,你只要说通这么一句话:“XX是一种XX”。OK,那他们之间就是继承关系。篮球运动员是一种球类运动员,这说得通,所以篮球运动员从球类运动员继承,这样继承很有可能会产生一棵继承树,运动员派生出来,派生出来的意思是游泳运动员这个类、球类运动员这个类、射击运动员类从它继承,相当于运动员派生出来了这些个不同的运动员,包括游泳的,球类的,射击的。球类的再派生足球的,篮球的,排球的。这就是一棵继承树,不过这棵树是比较理想化的情况,只有一个根节点。但实际当中,我们真实世界当中的继承关系不一定只从一个类继承,一个类可能从多个类继承过来,比如说:金丝猴从动物这个类继承,这很正常,但我还有另外一个专门的类:“应该受到保护的东西”,这也是一个类,金丝猴应该受到保护,所以金丝猴是一种应该受到保护的东西。所以金丝猴从“应该受到保护的东西”这个类继承过来。所以在现实情况当中,一个类完完全全有可能从多个不同的类继承,C++正是因为想封装这种继承关系,所以C++存在多重继承。

聚合关系(整体和部分)(比较强的一种关系)

Java面向对象与代码编写_第4张图片

  什么是聚合?聚合就是一个整体与部分的关系。我们说“XX是XX的一部分”,只要说得通,那么他们之间的关系就是聚合关系,队长是球队的一部分,队员是球队的一部分。所以队长和球队是聚合关系,队员和球队也是聚合关系。脑袋是人的以部分,身体和胳膊也是人的一部分,因此脑袋,身体和胳膊与人都是聚合关系。聚合关系分得再细一点的话就可以分成聚集关系和组合关系,比如球队、队长,队员,这三者是聚集关系,假如这个队长既是足球的队长,同时也是篮球的队长,一个人分属两个不同的球队,这是可以的,球队与队长之间没有我离不了你,你离不了我这种情况,所以如果分得更细的话,这种就叫做聚集关系。还有一种情况叫组合,组合说的是咱们俩密不可分,我是你必不可少的一部分。一个人的脑袋不可能既属于你又属于别人,身体也一样,不可能既属于你又属于别人。所以你的身体,脑袋和你是密不可分的,这是一种更加严格的聚合关系,专门给它取了个名字叫组合。

实现关系

Java面向对象与代码编写_第5张图片

  作为父类来说,我觉得我应该具有这样一个方法,但我不知道怎么去实现,谁去实现,我的子类去实现,这就是实现关系。和实现关系息息相关的还有一种关系叫多态。

多态

Java面向对象与代码编写_第6张图片

Java与面向对象

Java面向对象与代码编写_第7张图片

  对象和类是分不开的,必须首先定义类才能有对象。首先定义方法才能调用。对象是JAVA里面的核心,做任何东西你首先都得给我造出一个对象才能做。静态属性简称属性,也叫成员变量,以后说属性或者说成员变量它们指的都是同一回事。

  整个类可以看作是静态的属性还有方法他们之间的一个综合。怎么抽象出一个类的概念,还是那句话,你必须抽象出两个方面来,第一方面是看他们的静态属性,他们有哪些成员变量,第二方面是看他们的有哪些方法。

  写JAVA程序时,我们一上来写的就是public class(声明一个类),在这个class里面我们写的是成员变量和方法。

  每一个JAVA里面的class(类)都对应了我们现实生活中某一类事物的一个抽象。比如说要在JAVA里面封装一只狗,具体怎么封装,如何写代码,代码如下:

/**
   * 一类事物封装到JAVA里面首先得写class,定义这个类,类名是什么可以自己取。
   * 这里把类名叫做Dog
   */
  public class Dog {
      /**
       * 接下来就是写这个狗这个类的属性或者叫成员变量,
       * 比如说狗这个类的毛的颜色,怎么定义这个属性呢,
      * 首先得定义毛的一个类型,如使用int来定义毛的颜色类型
      */
     int furcolor; //定义属性:毛的颜色
     float height; //定义属性:狗的高度
     float weight; //定义属性:狗的体重
     
     /**
      * 狗的颜色,高度,体重这些属性定义完了,接下来要定义的就是方法了。
      * 如写一个CatchMouse()方法,捉老鼠的方法。
      * CatchMouse这个方法里面有一个对象类型的参数,捉哪一只老鼠,这个对象参数是属于Mouse这个类的
      * @param m
      */
     void CatchMouse(Mouse m){
             //在方法体内写捉老鼠这个过程,怎么捉,跑着捉,走着捉
             System.out.println("我捉到老鼠了,汪汪!,老鼠要尖叫了!");
             /**
              * 老鼠尖叫一声,表示被狗咬到了,咬到了能不叫吗,很自然而然地想到,
              * 尖叫(scream())这个方法是属于Mouse这个类里面的某一个方法。
              * 老鼠自己调用它,让它自己尖叫。这就是面向对象的思维。
              */
            m.scream();
     }
     
    public static void main(String[] args) {
         Dog  d = new Dog();//首先用new关键字创建一只狗
         Mouse m=new Mouse();//造出一只老鼠。
         d.CatchMouse(m);//然后用这只狗去抓老鼠,让狗调用CatchMouse()方法去捉某只老鼠。
     }
 }
/**
  * 封装的老鼠类
  */
 public class Mouse {
     /**
      * 老鼠自己有一个发出尖叫的方法
       * 当被狗咬到时就会发出尖叫
      */
     public void scream() {
         System.out.println("我被狗咬到了,好痛啊!");
     }
 
 }

   从这个意义上来讲,JAVA里面的每定义一个类实际上就相当于一种新的数据类型。就跟int ,float, String等一样,不过是一种新定义的类型而已。

为什么使用面向对象编程?

  面向对象编程:一组对象互相配合通过沟通完成特定功能

  做软件苦苦追求的一种境界是可重用性(reusable),可扩展性。如果是面向过程,一般情况是属性和方法它们是分开的,他们不是聚合的关系,不是合在一起的,这样要复用起来比较麻烦,复用的层次只是局限于方法这个层次上,而面向对象则不同,它是把属性和方法综合在一个里面。综合在一起复用的时候是整个对象进行复用。所以面向对象和面向过程相比,前者更加容易让我们达到可重用性。

 

java类

JAVA类的定义

Java面向对象与代码编写_第8张图片

  JAVA里面有class关键字定义一个类,后面加上自定义的类名即可。如这里定义的person类,使用class person定义了一个person类,然后在person这个类的类体里面定义person这个类应该具有的成员变量(即属性)和方法,如这里定义的int  id和int  age这个两个成员变量,或者叫属性,这个id表示人的身份证号码,人应该具有这个属性,age表示人的年龄,这也是人应该具有的。这样就在person这个类里面定义了两个人应该有的属性,接下来就是定义方法了,这里定义了三个方法,分别是getAge()、setAge(int  i)和getId(),分别用来获取人的年龄,设置人的年龄,获取人的id,getAge()方法获取了人的年龄后,将获取到的值返回,所以使用了return  age语句,getId()方法也使用了return  id语句用于返回获取到的id的值。

成员变量

  Java面向对象与代码编写_第9张图片

  在JAVA里面的任何变量首先应该要声明,然后再赋值,然后再使用。成员变量和局部变量有一个重要区别:成员变量在类里面声明时如果不进行初始化,那么JAVA会默认给它初始化,而局部变量JAVA不会默认给它初始化,所以在方法里面声明一个局部变量如果不给它初始化时就会出错。默认初始化大多数都是0,boolean类型的为false,引用类型的为null,如过不记得JAVA对成员变量默认的初始化是多少的话,那就这样做,定义一个成员变量,不给它初始化,然后直接打印这个成员变量,打印出来的结果就是JAVA默认的初始化的值。

引用

Java面向对象与代码编写_第10张图片

  引用类型和基本类型有着巨大的区别,当声明一个int  i=0时,系统会马上给这个i分配一个内存空间(在栈内存里面分配一小块区域用来装数字0),里面装着一个值为0,以后使用i这个名字马上就可以访问这个内存空间里面的值,这就是基本数据类型,所以基础类型就只占一块内存。基础类型之外的类型全都叫引用类型,我们定义一个Mouse  m,这个m就是一个引用类型的数据。引用类型有什么重要的特征——引用类型占2块内存。我们定义好这个类之后,需要使用new关键字把这个类的对象实例化出来,也就是真真正正造出一个对象出来才能使用这个对象。

如何在内存中区分类和对象

  类是静态的概念,是位于代码区里面。对象是new出来的,它是位于堆内存,为什么对象要位于堆内存?因为堆内存是用来动态分配内存的,只有在运行当中才会new一个对象放堆内存里面,那这个对象到底有多大个,这个东西你不知道,你没有办法提前知道,所以你没有办法提前分配内存给这个对象,你只有在运行期间才能去分配它。什么叫运行期间?敲JAVAC这个命令那是在编译期间,编译完成后再敲JAVA命令,那就是运行期间了。只有在运行期间,才能够明白这个对象到底要分配多大的空间给它,所以把它放在堆内存里面,堆内存比较大,动态分配内存用它。如果这个对象不用了,那它就是垃圾,那么就等着垃圾收集器把它收集回去,释放掉占用的内存。

记住,以后一提到引用,脑子里马上浮现引用那就是一小块内存指向一大块内存。

对象的创建和使用

  Java面向对象与代码编写_第11张图片

  使用new关键字来创建一个新的对象。

类和对象的关系

  Java面向对象与代码编写_第12张图片

在内存中分析类和对象的关系

  假设这里有一个类C,我们定义了一个类class C,然后在这个类里面定义了两个成员变量: int  i和int  j。定义好了这两个成员变量以后,我们写了一个main()方法(public static void main(Strng[] args)),程序开始执行。第一句我们写了 C c1 = new C(),这句的代码是我们相当于在堆内存里创建了一个对象,同时也创建了这个对象的一个引用对象c1,c1位于栈内存中,c1这个引用对象指向堆中一大块内存,这一大块内存里面装着new出来的那个对象。这里面我们一般来说是new出来两个对象c1和c2,当然,实际上,严格来讲,c1和c2叫做对象的引用,有时候,简称new出来了两个对象,c1和c2。你脑子里马上要浮现出两块内存,c1指向一块,c2指向一块。局部变量是分配在栈内存里面的,main方法里面的c1和c2都是局部变量,所以在栈里面分配了两小块内存出来,一块是c1的,一块是c2的,c1这块内存里面装着一个值,或者叫装着一个地址,这个地址是什么,我们不知道,我们只知道根据这个值就能找到new出来的C这个类里面的一个对象,而在这个对象里面有它自己的成员变量i和j,里面的两小块内存是分别用来装i和j的值的,因为每一个对象都有自己不同的成员变量的值,所以c1指向的那块对内存里面又分成一小块一小块内存,每一个小块的内存都装着这个对象的成员变量(或者叫属性)。如这里的第一小块装着i的值,第二小块装着j的值,所以当我们去访问第一小块里面装着的成员变量时,我们应该这样写:c1.i,这样就拿到了i的值,c1.j,这样就拿到了j的值。同理,c2这个对象也指向了一个new出来的C这个类里面的另一个对象,这个对象也有成员变量i和j,只不过和c1指向的那个对象里的i和j的值不同而已。要访问这个这个对象的成员变量时 ,也是要c2.i,c2.j这样去访问。

构造方法

  Java面向对象与代码编写_第13张图片

  在面向对象里面有一个特殊的方法,叫构造方法。

  构造方法是用来创建一个新的对象的,与new组合在一起用,使用new+构造方法创建一个新的对象。你new一个东西的时候,实际上你调用的是一个构造方法,构造方法就是把自己构造成一个新的对象,所以叫构造方法,构造一个新对象用的方法叫构造方法。

  构造方法比较特殊,构造方法的名字必须和类的名字完全一模一样,包括大小写,并且没有返回值。如原来定义的一个person类,在类里面声明了两个成员变量id与age,这时候你可以再为这个person类定义一个它的构造方法person(int n,int i),这个方法的名字和类名完全相同,并且没有返回值,也就是在这个方法前面不能写任何的方法的返回类型修饰符,连void都不可以写。

 构造方法范例:

 public class Person {
     int id;  //在person这类里面定义两个成员变量id和age,
     int age=20;  //给成员变量age赋了初值为20
 
     /**这里就是person这个类的一个构造方法
      * 构造方法的规则很简单,和类名要完全一样,一点都不能错,包括大小写。
      * 并且没有返回值,不能写void在它前面修饰
      * @param _id
      * @param _age
      */
     public Person(int _id,int _age ) {
         id = _id;
         age = _age;
     }
 }

  构造方法写好后就和new组合在一起使用,new的作用是构建一个新对象,创造一个新对象,所以new的时候实际当中调用的是构造方法。只有调用了这个构造方法才能构造出一个新的对象。例如:

 public static void main(String[] args) {
     Person tom = new Person(1, 25); // 调用person这个构造方法创建一个新的对象,并给这个对象的成员变量赋初始值
 }

Java面向对象与代码编写_第14张图片

  下面是在main方法里面调用person构造方法时的内存分析情况:

Java面向对象与代码编写_第15张图片

Java面向对象与代码编写_第16张图片

 

 

  当方法调用完成之后,栈里面为它分配的空间全部都要消失,即把这个方法调用时分配给它的内存空间释放出来,所以这个构造方法person调用完成之后,栈内存里面分配的两小块内存_id和_age自动消失了。这样就把它们所占的空间让了出来,让其他的方法去占用。而new出来的对象则永远留在了堆内存里面。

  Java面向对象与代码编写_第17张图片

  声明一个类,若没有在类中指定其构造方法(构造函数)时,编译器会为这个类自动添加形如类名( ){ }的构造函数。

如:

 class  point{//这里声明一个类时并没有给它指定其构造方法。
    int  x;  
   int  y;
}

 但在main方法里面我们却可以这样使用:

 public static void main(String[] args){
     point  p = new  point();
}

  这里这样写是可以的,当没有给这个类指明构造方法时,系统会默认地给这个类加上point ( ) { }这样一个空的构造方法。所以才可以在main方法中使用
point  p = new  point(); 实际上你调用的就是编译器默认给它加上的point ( ) { }这个构造方法,在这个构造方法当中,默认地把类里面的成员变量x和y初始值设为0。正是因为系统给它默认加上这么一个构造方法,所以才能在main方法里面调用。但要记住一点,一旦给这个类里面指定了构造方法,那么系统就不会再给这个类添加构造方法了。

 

方法重载

 方法名一样,但参数不一样,这就是重载(overload)。

  所谓的参数不一样,主要有两点:第一是参数的个数不一样,第二是参数的类型不一样。只要这两方面有其中的一方面不一样就可以构成方法的重载了。

Java面向对象与代码编写_第18张图片

 public class TestOverLoad {
 
     void max(int a, int b) {
         System.out.println(a > b ? a : b);
    }
 
     /*
      * int max(int a, int b) { 
      *         return a > b ? a : b; 
      * }
     */

     void max(float a, float b) {
         System.out.println(a > b ? a : b);
     }
 }

  这里有void修饰符的两个max方法名字相同,但它们的参数类型不一样,所以可以构成重载。而int  max(int a , int b)方法与void  max(int a, int b)方法是不构成重载的,现在它们是重名的两个方法,在一个类中声明两个重名的方法是不允许的,编译会出错。方法名一样,参数类型一样,只有返回值不一样,这个不构成重载,因为方法可以这么调用,调用一个方法时可以不使用的它返回值,所以当调用这两个方法时,把整型的数传进去,由于两个方法都是一样的名字,一样的参数类型,所以编译器无法区分到底要调用的是哪一个方法。构成重载深层次的原因:只要这两个方法编译器能区分开来,在调用的时候知道要调用的是哪一个,不会产生混淆,这两个方法就构成重载。

再看下面这两个方法:

 int max(int a, int b) {
     System.out.println("调用的int max(int a, int b)方法");
     return a > b ? a : b;
 }
      
 int max(short a, short b) {
     System.out.println("调用的int max(short a, short b)方法");
     return a > b ? a : b;
 }

  这两个方法也是可以构成重载的,因为编译器一看到整数,就会把它当作int类型。所以当把整数传进来的时候,编译器首先调用的是max(int a , int b)这个方法。而要想调用max(short a ,short b)这个方法,那么就得在main方法里面这样写:

 public static void main(String[] args) {
     TestOverLoad t= new TestOverLoad();
     t.max(3,4);  //这里调用的是max(int a , int b)这个方法
     short a = 3;
     short b = 4;
     t.max(a, b);  //这里调用的是max(short a , short b)这个方法。
 }

构造方法的重载

与普通方法一样,构造方法也可以重载

  public class Person {
  
      int id;
      int age;
      
     /**
      * 构造方法
      */
     public Person() {
         id=0;
         age=20;
     }
     
     /**
      * 构造方法重载一
      * @param i
      */
     public Person(int i) {
         id=i;
         age=20;
     }
     
     /**
      * 构造方法重载二
     * @param i
     * @param j
      */
     public Person(int i,int j) {
         id=i;
         age=j;
     }
 }

 

对象转型

Java面向对象与代码编写_第19张图片

  对象转型分为两种:一种叫向上转型(父类对象的引用或者叫基类对象的引用指向子类对象,这就是向上转型),另一种叫向下转型。转型的意思是:如把float类型转成int类型,把double类型转成float类型,把long类型转成int类型,这些都叫转型。把一种形式转成另外一种形式就叫转型。除了基础数据类型的转型之外(基础数据类型的转型:大的可以转成小的,小的也可以转成大的。),对象领域里面也有对象之间的转型。

对象转型实例一

 1 package javastudy.summary;
  2 
  3 /**
  4  * 父类Animal
  7  */
  8 class Animal {
  9 
 10     public String name;
 11 
 12     public Animal(String name) {
 13         this.name = name;
 14     }
 15 }
 16 
 17 /**
 18  * 子类Cat继承Animal
 21  */
 22 class Cat extends Animal {
 23 
 24     /**
 25      * Cat添加自己独有的属性
 26      */
 27     public String eyeColor;
 28 
 29     public Cat(String n, String c) {
 30         super(n);//调用父类Animal的构造方法
 31         this.eyeColor = c;
 32     }
 33 }
 34 
 35 /**
 36  * 子类Dog继承Animal
 38  *
 39  */
 40 class Dog extends Animal {
 41     /**
 42      * Dog类添加自己特有的属性
 43      */
 44     public String furColor;
 45 
 46     public Dog(String n, String c) {
 47         super(n);//调用父类Animal的构造方法
 48         this.furColor = c;
 49     }
 50 
 51 }
 52 
 53 /**
 54  * 下面是这三个类的测试程序
 57  */
 58 public class TestClassCast {
 59 
 60     /**
 61      * @param args
 62      */
 63     public static void main(String[] args) {
 64 
 65         Animal a = new Animal("name");
 66         Cat c = new Cat("catname","blue");
 67         Dog d = new Dog("dogname", "black");
 68         /**
 69          * a instanceof Animal这句话的意思是a是一只动物吗?
 70          * a是Animal这个类里面的是一个实例对象,所以a当然是一只动物,其结果为true。
 71          */
 72         System.out.println(String.format("a instanceof Animal的结果是%s",a instanceof Animal));//true
 73         /**
 74          * c是Cat类的实例对象的引用,即c代表的就是这个实例对象,
 75          * 所以“c是一只动物”打印出来的结果也是true。
 76          * d也一样,所以“d是一只动物”打印出来的结果也是true。
 77          */
 78         System.out.println(String.format("c instanceof Animal的结果是%s",c instanceof Animal));//true
 79         System.out.println(String.format("d instanceof Animal的结果是%s",d instanceof Animal));//true
 80         /**
 81          * 这里判断说“动物是一只猫”,不符合逻辑,所以打印出来的结果是false。
 82          */
 83         System.out.println(String.format("a instanceof Cat的结果是%s",a instanceof Cat));
 84         /**
 85          * 这句话比较有意思了,a本身是Animal类的实例对象的引用,
 86          * 但现在这个引用不指向Animal类的实例对象了,而是指向了Dog这个类的一个实例对象了,
 87          * 这里也就是父类对象的引用指向了子类的一个实例对象。
 88          */
 89         a = new Dog("bigyellow", "yellow");
 90         System.out.println(a.name);//bigyellow
 91         /**
 92          * 这里的furColor属性是子类在继承父类的基础上新增加的一个属性,是父类没有的。
 93          * 因此这里使用父类的引用对象a去访问子类对象里面新增加的成员变量是不允许的,
 94          * 因为在编译器眼里,你a就是Animal类对象的一个引用对象,你只能去访问Animal类对象里面所具有的name属性,
 95          * 除了Animal类里面的属性可以访问以外,其它类里面的成员变量a都没办法访问。
 96          * 这里furColor属性是Dog类里面的属性,因此你一个Animal类的引用是无法去访问Dog类里面的成员变量的,
 97          * 尽管你a指向的是子类Dog的一个实例对象,但因为子类Dog从父类Animal继承下来,
 98          * 所以new出一个子类对象的时候,这个子类对象里面会包含有一个父类对象,
 99          * 因此这个a指向的正是这个子类对象里面的父类对象,因此尽管a是指向Dog类对象的一个引用,
100          * 但是在编译器眼里你a就是只是一个Animal类的引用对象,你a就是只能访问Animal类里面所具有的成员变量,
101          * 别的你都访问不了。
102          * 因此一个父类(基类)对象的引用是不可以访问其子类对象新增加的成员(属性和方法)的。
103          */
104         //System.out.println(a.furColor);
105         System.out.println(String.format("a指向了Dog,a instanceof Animal的结果是%s",a instanceof Animal));//true
106         /**
107          * 这里判断说“a是一只Dog”是true。
108          * 因为instanceof探索的是实际当中你整个对象到底是什么东西,
109          * 并不是根据你的引用把对象看出什么样来判断的。
110          */
111         System.out.println(String.format("a instanceof Dog的结果是%s",a instanceof Dog));//true
112         /**
113          * 这里使用强制转换,把指向Animal类的引用对象a转型成指向Dog类对象的引用,
114          * 这样转型后的引用对象d1就可以直接访问Dog类对象里面的新增的成员了。
115          */
116         Dog d1 = (Dog)a;
117         System.out.println(d1.furColor);//yellow
118     }
119 
120 }

运行结果:

Java面向对象与代码编写_第20张图片

内存分析

 Java面向对象与代码编写_第21张图片

  在内存中可以看到,指向Dog类实例对象的引用对象a是一个Animal类型的引用类型,这就比较有意思了,Animal类型指向了Dog这个对象,那么,在程序的眼睛里会把这只Dog当成一只普通的Animal,既然是把Dog当成一只普通的Animal,那么Dog类里面声明的成员变量furColor就不能访问了,因为Animal类里面没有这个成员变量。因此,从严格意义上来讲,这个a眼里只看到了这个子类对象里面的父类对象Animal,因此能访问得到的也只是这个Animal对象里面的name属性,而这个Animal对象外面的furColor属性是访问不到的,虽然Dog对象确实有这个属性存在,但a就是看不到,a门缝里看Dog——把Dog看扁了,不知道Dog还有furColor这个属性存在,因此a访问不了furColor属性,因此从严格意义上来讲,a指向的只是这个Dog对象里面的Animal对象,也就是黄色箭头指向的那部分,a就只看到了Dog里面这部分,而Dog外面的部分都看不到了。这就是父类引用指向子类对象,父类引用指向子类对象的时候,它看到的只是作为父类的那部分所拥有的属性和方法,至于作为子类的那部分它没有看到。

  如果真的想访问Dog对象的furColor属性,那就采用对象转型的办法,把父类对象的引用转型成子类对象的引用。Dog d1 = (Dog)a;这里采用的就是对象转型的办法,把a强制转换成一只Dog对象的引用,然后将这个引用赋值给Dog型的引用变量d1,这样d1和a都是指向堆内存里面的Dog对象了,而且d1指向的就是这只Dog所有的部分了,通过这个d1就可以访问Dog对象里面所有的成员了。

 

对象转型实例二

  public class TestClassCast {
 
     public void  f(Animal a) {
         System.out.println(a.name);
         if (a instanceof Cat) {
             Cat cat = (Cat)a;
             System.out.println(cat.eyeColor+" eye");
         }else if (a instanceof Dog) {
             Dog dog = (Dog)a;
             System.out.println(dog.furColor+" fur");
         }
     }
     
     /**
      * @param args
      */
     public static void main(String[] args) {
         Animal a = new Animal("name");
         Cat c = new Cat("catname","blue");
         Dog d = new Dog("dogname", "black");
         TestClassCast testClassCast = new TestClassCast();
         testClassCast.f(a);
         testClassCast.f(c);
         testClassCast.f(d);
     }
 }

  这里的这些代码是对前面声明的三个类Animal,Dog,Cat测试的延续,这里我们在TestClassCast这里类里面测试这个三个类,这里我们在TestClassCast类里面new了一个testClassCast对象,为的是调用TestClassCast类里面声明的f(Animal  a)这个方法,这个f()方法里面的参数类型是Animal类型,如果是Animal类型的参数,那么我们可以把这个Animal类型的子类对象作为参数传进去,这是可以的。如把一只Dog或者是一只Cat丢进f()方法里面这都是可以的,因为Dog和Cat也是Animal。因此当程序执行到testClassCast.f(a);,testClassCast.f(c);,testClassCast.f(d);的时候,因为f()方法里面的参数是Animal类型的,所以我们可以把一个Animal对象传进去,除此之外,我们还可以直接把从Animal类继承下来的Dog类和Cat类里面的Dog对象和Cat对象作为实参传递过去,即是把Animal类型的子类对象作为参数传进去。这里就体现出了继承和父类对象的引用可以指向子类对象的好处了,如果说没有继承关系的存在,如果说父类的引用不可以指向子类对象,那么我们就得要在Test类里面定义三个f()方法了,即要定义这样的f()方法:
f(Animal a)、f(Dog d)、f(Cat c)分别用来处理Animal、Dog和Cat,使用三个方法来处理,将来程序的扩展起来就不是很容易了,因为面向对象可以帮助我们这些年来编程苦苦追求的一个境界是可扩展性比较好。可扩展性比较好的一个典型例子就是说当你建好一个建筑之后或者是你写好这个程序之后,把这个主建筑给建好了,将来你要加一些其他的功能的时候,尽量不要去修改主结构,这叫可扩展性好,你盖了一座大楼,你现在要在大楼的旁边添加一个厨房,那你在它旁边一盖就行了,如果有人告诉你,我添加一个厨房我需要把你整个大楼的主要柱子都给拆了然后再盖一遍,这你干吗,肯定不干。如果结构设计成这样,那就是设计得不好,可扩展性不好。所以这里如果要把f()方法写成三个重载的f()方法,那么将来我输出一只鸟的时候又得要添加一个f(Bird  b)方法来处理鸟。这样扩展起来就太麻烦了,因为每处理一只动物都要添加一个新的方法,但是如果存在继承关系,如果父类对象的引用可以指向子类对象,那扩展起来就简单了,你可以把处理动物的方法写在一个方法f(Animal  a)里面就够了,因为所有动物的种类都是从Animal类继承下来,因此给f()方法传递Animal类型的参数的时候可以直接把这个Animal类的子类对象传进去,这样不管是要增加什么动物的输出,我都可以调用f(Animal a)方法去处理,这种扩展性比每次都要增加一个新的处理方法的扩展性要好得多,这就是继承的一个好处,这就是对象转型对于可扩展性来的好处:“对象的引用可以指向子类对象”,是因为面向对象的编程里面存在这样的继承关系,使得程序的可扩展性比较好。

  对象转型可以使父类对象的引用可以指向子类对象,给程序带来了比较好的可扩展性:我们可以在一个方法的参数里面定义父类的引用,然后实际当中传的时候传的是子类的对象,然后我们再在方法里面判断这个传过来的子类对象到底属于哪个子类,然后再去执行这个子类里面的方法或者调用这个子类里面的成员变量,因此程序的可扩展性比单独定义好多个方法要好一些。不过这个可扩展性还没有达到最好,使用多态就可以让程序的扩展性达到极致。

 

面向对象最核心的机制——动态绑定,也叫多态

Java面向对象与代码编写_第22张图片

通过下面的例子理解动态绑定,即多态

1 package javastudy.summary;
  2 
  3 class Animal {
  4     /**
  5      * 声明一个私有的成员变量name。
  6      */
  7     private String name;
  8 
  9     /**
 10      * 在Animal类自定义的构造方法
 11      * @param name
 12      */
 13     Animal(String name) {
 14         this.name = name;
 15     }
 16 
 17     /**
 18      * 在Animal类里面自定义一个方法enjoy
 19      */
 20     public void enjoy() {
 21         System.out.println("动物的叫声……");
 22     }
 23 }
 24 
 25 /**
 26  * 子类Cat从父类Animal继承下来,Cat类拥有了Animal类所有的属性和方法。
 27  * @author gacl
 28  *
 29  */
 30 class Cat extends Animal {
 31     /**
 32      * 在子类Cat里面定义自己的私有成员变量
 33      */
 34     private String eyesColor;
 35 
 36     /**
 37      * 在子类Cat里面定义Cat类的构造方法
 38      * @param n
 39      * @param c
 40      */
 41     Cat(String n, String c) {
 42         /**
 43          * 在构造方法的实现里面首先使用super调用父类Animal的构造方法Animal(String name)。
 44          * 把子类对象里面的父类对象先造出来。
 45          */
 46         super(n);
 47         eyesColor = c;
 48     }
 49 
 50     /**
 51      * 子类Cat对从父类Animal继承下来的enjoy方法不满意,在这里重写了enjoy方法。
 52      */
 53     public void enjoy() {
 54         System.out.println("我养的猫高兴地叫了一声……");
 55     }
 56 }
 57 
 58 /**
 59  * 子类Dog从父类Animal继承下来,Dog类拥有了Animal类所有的属性和方法。
 60  * @author gacl
 61  *
 62  */
 63 class Dog extends Animal {
 64     /**
 65      * 在子类Dog里面定义自己的私有成员变量
 66      */
 67     private String furColor;
 68 
 69     /**
 70      * 在子类Dog里面定义Dog类的构造方法
 71      * @param n
 72      * @param c
 73      */
 74     Dog(String n, String c) {
 75         /**
 76          * 在构造方法的实现里面首先使用super调用父类Animal的构造方法Animal(String name)。
 77          * 把子类对象里面的父类对象先造出来。
 78          */
 79         super(n);
 80         furColor = c;
 81     }
 82 
 83     /**
 84      * 子类Dog对从父类Animal继承下来的enjoy方法不满意,在这里重写了enjoy方法。
 85      */
 86     public void enjoy() {
 87         System.out.println("我养的狗高兴地叫了一声……");
 88     }
 89 }
 90 
 91 /**
 92  * 子类Bird从父类Animal继承下来,Bird类拥有Animal类所有的属性和方法
 93  * @author gacl
 94  *
 95  */
 96 class Bird extends Animal {
 97     /**
 98      * 在子类Bird里面定义Bird类的构造方法
 99      */
100     Bird() {
101         /**
102          * 在构造方法的实现里面首先使用super调用父类Animal的构造方法Animal(String name)。
103          * 把子类对象里面的父类对象先造出来。
104          */
105         super("bird");
106     }
107 
108     /**
109      * 子类Bird对从父类Animal继承下来的enjoy方法不满意,在这里重写了enjoy方法。
110      */
111     public void enjoy() {
112         System.out.println("我养的鸟高兴地叫了一声……");
113     }
114 }
115 
116 /**
117  * 定义一个类Lady(女士)
118  * @author gacl
119  *
120  */
121 class Lady {
122     /**
123      * 定义Lady类的私有成员变量name和pet
124      */
125     private String name;
126     private Animal pet;
127 
128     /**
129      * 在Lady类里面定义自己的构造方法Lady(),
130      * 这个构造方法有两个参数,分别为String类型的name和Animal类型的pet,
131      * 这里的第二个参数设置成Animal类型可以给我们的程序带来最大的灵活性,
132      * 因为作为养宠物来说,可以养猫,养狗,养鸟,只要是你喜欢的都可以养,
133      * 因此把它设置为父类对象的引用最为灵活。
134      * 因为这个Animal类型的参数是父类对象的引用类型,因此当我们传参数的时候,
135      * 可以把这个父类的子类对象传过去,即传Dog、Cat和Bird等都可以。
136      * @param name
137      * @param pet
138      */
139     Lady(String name, Animal pet) {
140         this.name = name;
141         this.pet = pet;
142     }
143 
144     /**
145      * 在Lady类里面自定义一个方法myPetEnjoy()
146      * 方法体内是让Lady对象养的宠物自己调用自己的enjoy()方法发出自己的叫声。
147      */
148     public void myPetEnjoy() {
149         pet.enjoy();
150     }
151 }
152 
153 public class TestPolymoph {
154     public static void main(String args[]) {
155         /**
156          * 在堆内存里面new了一只蓝猫对象出来,这个蓝猫对象里面包含有一个父类对象Animal。
157          */
158         Cat c = new Cat("Catname", "blue");
159         /**
160          * 在堆内存里面new了一只黑狗对象出来,这个黑狗对象里面包含有一个父类对象Animal。
161          */
162         Dog d = new Dog("Dogname", "black");
163         /**
164          * 在堆内存里面new了一只小鸟对象出来,这个小鸟对象里面包含有一个父类对象Animal。
165          */
166         Bird b = new Bird();
167 
168         /**
169          * 在堆内存里面new出来3个小姑娘,名字分别是l1,l2,l3。
170          * l1养了一只宠物是c(Cat),l2养了一只宠物是d(Dog),l3养了一只宠物是b(Bird)。
171          * 注意:调用Lady类的构造方法时,传递过来的c,d,b是当成Animal来传递的,
172          * 因此使用c,d,b这三个引用对象只能访问父类Animal里面的enjoy()方法。
173          */
174         Lady l1 = new Lady("l1", c);
175         Lady l2 = new Lady("l2", d);
176         Lady l3 = new Lady("l3", b);
177         /**
178          * 这三个小姑娘都调用myPetEnjoy()方法使自己养的宠物高兴地叫起来。
179          */
180         l1.myPetEnjoy();
181         l2.myPetEnjoy();
182         l3.myPetEnjoy();
183     }
184 }

运行结果:

  

 

画内存图理解动态绑定(多态)

  首先从main方法的第一句话开始分析:

    Cat c = new Cat("Catname","blue");

  程序执行到这里,栈空间里有一个变量c,c里面装着一系列的值,通过这些值可以找到位于堆内存里面new出来的Cat对象。因此c是Cat对象的一个引用,通过c可以看到这个Cat对象的全部。c指向new出来的Cat对象。在new这个Cat对象的时候,调用了Cat对象的构造方法Cat(String n,String c),定义如下:

    Cat(String n,String c){

      super(n);

      eyesColor=c;

    }

  因此在构造子类对象时首先使用父类对象的引用super调用父类的构造方法Animal(String name),定义如下:

    Animal(String name){

      this.name=name;

    }

  因此会把传过来的字符串“Catname”传递给父类对象的name属性。当Cat(String n,String c)构造方法调用结束后,真真正正在堆内存里面new出了一只Cat,这只Cat里面包含有父类对象Animal,这个Animal对象有自己的属性name,name属性的值为调用父类构造方法时传递过来的字符串Catname。除此之外,这只Cat还有自己的私有成员变量eyesColor,eyesColor属性的属性值为调用子类构造方法时传递过来的字符串blue。所以执行完这句话以后,内存中的布局是栈内存里面有一个引用c,c指向堆内存里面new出来的一只Cat,而这只Cat对象里面又包含有父类对象Animal,Animal对象有自己的属性name,属性值为Catname,Cat除了拥有从Animal类继承下来的name属性外,还拥有一个自己私有的属性eyesColor,属性值为blue。这就是执行完第一句话以后整个内存布局的情况如下图所示:

  Java面向对象与代码编写_第23张图片

接着看这句话:Lady l1 = new Lady(“l1”,c);

  Java面向对象与代码编写_第24张图片

  程序执行到这里,首先在栈内存里面多了一个引用变量l1,l1里面装着一个值,通过这个值可以找到在堆内存里面new出来的Lady对象。l1就是这个Lady对象的引用,l1指向Lady对象。在创建Lady对象时,调用Lady类的构造方法:Lady(String name,Animal pet),其定义如下:

  Lady(String name,Animal pet){

    this.name=name;

    this.pet=pet;

  }

  这个构造方法有两个参数,分别是String类型的name和Animal类型的pet,pet参数是一个父类对象的引用类型,这里把l1和c作为实参传递给了构造方法,接着在构造方法里面执行this.name=name,把传递过来的l1由传给Lady对象的name属性,因此Lady对象的name属性值为l1,这里也把前面new出来的那只Cat的引用c传递给了构造方法里面的参数pet,接着在构造方法里面执行this.pet=pet,pet参数又把c传过来的内容传递给Lady对象的pet属性,因此pet属性的属性值就是可以找到Cat对象的地址,因此Lady对象的pet属性也成为了Cat对象的引用对象了,通过pet里面装着的值是可以找到Cat对象的,因此pet也指向了Cat,但并不是全部指向Cat,pet指向的只是位于Cat对象内部的Animal对象,这是因为在调用构造方法时,是把c当成一个Animal对象的引用传过来的,把c作为一个Animal对象传递给了pet,所以得到的pet也是一个Animal对象的引用,因此这个pet引用指向的只能是位于Cat对象里面的Animal对象。在我pet引用对象眼里,你Cat对象就是一只普通的Animal,访问你的时候只能访问得到你里面的name属性,而你的eyesColor属性我是访问不到的,我能访问到你的name属性,访问的是位于你内部里面的父对象的name属性,因为我pet引用本身就是一个父类对象的引用,因此我可以访问父类对象的全部属性,而你子类对象Cat自己新增加的成员我pet引用是访问不了的。不过现在我pet引用不去访问你父类对象的成员变量name了,而是去访问你的成员方法enjoy了。首先是使用Lady对象的引用l1去调用Lady对象的myPetEnjoy()方法,myPetEnjoy()方法定义如下:

  public void myPetEnjoy(){

    pet.enjoy();

  }

  然后在myPetEnjoy()方法体里面又使用pet引用对象去调用父类对象里面的enjoy方法。

  方法是放在代码区(code seg)里面的,里面的方法就是一句句代码。因此当使用pet引用去访问父类对象的方法时,首先是找到这个父类对象,然后看看它里面的方法到底在哪里存着,找到那个方法再去执行。这里头就比较有意思了,code seg里面有很多个enjoy方法,有父类的enjoy()方法,也有子类重写了从父类继续下来的enjoy()方法,那么调用的时候到底调用的是哪一个呢?是根据谁来确定呢?注意:这是根据你实际当中的对象来确定的,你实际当中new出来的是谁,就调用谁的enjoy方法,当你找这个方法的时候,通过pet引用能找得到这个方法,但调用代码区里面的哪一个enjoy方法不是通过引用类型来确定的,如果是通过引用类型pet来确定,那么调用的肯定是Animal的enjoy()方法,可是现在是根据实际的类型来确定,我们的程序运行以后才在堆内存里面创建出一只Cat,然后根据你实际当中new出来的类型来判断我到底应该调用哪一个enjoy()方法。如果是根据实际类型,那么调用的就应该是Cat的enjoy()方法。如果是根据引用类型,那么调用的就应该是Animal的enjoy()方法。现在动态绑定这种机制指的是实际当中new的是什么类型,就调用谁的enjoy方法。所以说虽然你是根据我父类里面的enjoy方法来调用,可是实际当中却是你new的是谁调用的就是谁的enjoy()方法。即实际当中调用的却是子类里面重写后的那个enjoy方法。当然,讲一点更深的机制,你实际当中找这个enjoy方法的时候,在父类对象的内部有一个enjoy方法的指针,指针指向代码区里面父类的Animal的enjoy方法,只不过当你new这个对象的时候,这个指针随之改变,你new的是什么对象,这个指针就指向这个对象重写后的那个enjoy方法,所以这就叫做动态绑定。只有在动起来的时候,也就是在程序运行期间,new出了这个对象了以后你才能确定到底要调用哪一个方法。我实际当中的地址才会绑定到相应的方法的地址上面,所以叫动态绑定。调这个方法的时候,只要你这个方法重写了,实际当中调哪一个,要看你实际当中new的是哪个对象,这就叫多态,也叫动态绑定。动态绑定带来莫大的好处是使程序的可扩展性达到了最好,我们原来做这个可扩展性的时候,首先都是要在方法里面判断一下这只动物是哪一类里面的动物,通过if (object instanceof class)这样的条件来判断这个new出来的对象到底是属于哪一个类里面的,如果是一只猫,就调用猫的enjoy方法,如果是一条狗,就调用狗的enjoy方法。如果我现在增加了一个Bird类,那么扩展的时候,你又得在方法里面写判断这只鸟属于哪一个类然后才能调用这只鸟的enjoy方法。每增加一个对象,你都要在方法里面增加一段判断这个对象到底属于哪个类里面的代码然后才能执行这个对象相应的方法。即每增加一个新的对象,都要改变方法里面的处理代码,而现在,你不需要再改变方法里面的处理代码了,因为有了动态绑定。你要增加哪一个对象,你实际当中把这个对象new出来就完了,不再用去修改对象的处理方法里面的代码了。也就是当你实际当中要增加别的东西的时候,很简单,你直接加上去就成了,不用去改原来的结构,你要在你们家大楼的旁边盖一个厨房,很简单,直接在旁边一盖就行了,大楼的主要支柱什么的你都不用动,这就可以让可扩展性达到了极致,这就为将来的可扩展打下了基础,也只有动态绑定(多态)这种机制能帮助我们做到这一点——让程序的可扩展性达到极致。因此动态绑定是面向对象的核心,如果没有动态绑定,那么面向对象绝对不可能发展得像现在这么流行,所以动态绑定是面向对象核心中的核心。

  总结动态绑定(多态):动态绑定是指在“执行期间”(而非编译期间)判断所引用的实际对象类型,根据其实际的类型调用其相应的方法。所以实际当中找要调用的方法时是动态的去找的,new的是谁就找谁的方法,这就叫动态绑定。动态绑定帮助我们的程序的可扩展性达到了极致。

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

  1. 要有继承(两个类之间存在继承关系,子类继承父类)
  2. 要有重写(在子类里面重写从父类继承下来的方法)
  3. 父类引用指向子类对象

  这三个条件一旦满足,当你调用父类里面被重写的方法的时候,实际当中new的是哪个子类对象,就调用子类对象的方法(这个方法是从父类继承下来后重写后的方法)。

  面向对象比较强调类和类之间,对象和对象之间的一种组织关系,如果能把这种组织关系组织得比较好的话,你的程序想扩展性比较好,比较健壮,维护性比较好这些都可以达到,关键看你的设计到底好还是不好。

 

抽象类

Java面向对象与代码编写_第25张图片

下面通过一下的小程序深入理解抽象类

Java面向对象与代码编写_第26张图片

  因此在类Animal里面只需要定义这个enjoy()方法就可以了,使用abstract关键字把enjoy()方法定义成一个抽象方法,定义如下:public abstract void enjoy(); 

  从某种意义上来说,抽象方法就是被用来重写的,所以在父类声明的抽象方法一定要在子类里面重写。如果真的不想在子类里面重写这个方法,那么可以再在子类里面把这个方法再定义为抽象方法,因为子类觉得我去实现也不合适,应该让继承我的子类去实现比较合适,因此也可以在继承这个子类的下一个子类里面重写在父类里面声明的抽象方法,这是可以的。

  这里有一个规则:既然父类里面的方法是抽象的,那么对于整个类来说,它就有一个没有实现的方法,这个方法不知道怎么去实现,那么这个类是就是残缺不全的,因此这个类应该被定义为一个抽象类。所以前面这样声明的声明的class Animal应该要在class的前面加上abstract,即声明成这样:abstract class Animal,这样Animal类就成了一个抽象类了。Animal类的最终定义代码如下:

 /**
  * 父类Animal
  * 在class的前面加上abstract,即声明成这样:abstract class Animal
  * 这样Animal类就成了一个抽象类了
  */
 abstract class Animal {
 
     public String name;
 
     public Animal(String name) {
         this.name = name;
     }
     
     /**
      * 抽象方法
      * 这里只有方法的定义,没有方法的实现。
      */
     public abstract void enjoy(); 
     
 }

 Java语言规定,当一个类里面有抽象方法的时候,这个类必须被声明为抽象类。

 子类继承父类时,如果这个父类里面有抽象方法,并且子类觉得可以去实现父类的所有抽象方法,那么子类必须去实现父类的所有抽象方法,如:

/**
  * 子类Dog继承抽象类Animal,并且实现了抽象方法enjoy  
  *
  */
 class Dog extends Animal {
     /**
      * Dog类添加自己特有的属性
      */
     public String furColor;
 
     public Dog(String n, String c) {
         super(n);//调用父类Animal的构造方法
         this.furColor = c;
     }
 
     @Override
     public void enjoy() {
         System.out.println("狗叫....");
     }
 
 }

  这个父类里面的抽象方法,子类如果觉得实现不了,那么把就子类也声明成一个抽象类,如:

 /**
  * 这里的子类Cat从抽象类Animal继承下来,自然也继承了Animal类里面声明的抽象方法enjoy(),
  * 但子类Cat觉得自己去实现这个enjoy()方法也不合适,因此它把它自己也声明成一个抽象的类,
  * 那么,谁去实现这个抽象的enjoy方法,谁继承了子类,那谁就去实现这个抽象方法enjoy()。
  * @author gacl
  *
  */
 abstract class Cat extends Animal {
 
     /**
      * Cat添加自己独有的属性
     */
    public String eyeColor;
 
     public Cat(String n, String c) {
         super(n);//调用父类Animal的构造方法
         this.eyeColor = c;
     }
 }

 这里的子类Cat从抽象类Animal继承下来,自然也继承了Animal类里面声明的抽象方法enjoy(),但子类Cat觉得自己去实现这个enjoy()方法也不合适,因此它把它自己也声明成一个抽象的类,那么,谁去实现这个抽象的enjoy方法,谁继承了子类,那谁就去实现这个抽象方法enjoy()。如:

  /**
  * 子类BlueCat继承抽象类Cat,并且实现了从父类Cat继承下来的抽象方法enjoy
  *
  */
 class BlueCat extends Cat {
 
     public BlueCat(String n, String c) {
         super(n, c);
     }
 
     /**
      * 实现了抽象方法enjoy
      */
     @Override
     public void enjoy() {
         System.out.println("蓝猫叫...");
     }
     
 }

完整的测试代码如下:

  1 package javastudy.summary;
  2 
  3 /**
  4  * 父类Animal
  5  * 在class的前面加上abstract,即声明成这样:abstract class Animal
  6  * 这样Animal类就成了一个抽象类了
  7  */
  8 abstract class Animal {
  9 
 10     public String name;
 11 
 12     public Animal(String name) {
 13         this.name = name;
 14     }
 15     
 16     /**
 17      * 抽象方法
 18      * 这里只有方法的定义,没有方法的实现。
 19      */
 20     public abstract void enjoy(); 
 21     
 22 }
 23 
 24 /**
 25  * 这里的子类Cat从抽象类Animal继承下来,自然也继承了Animal类里面声明的抽象方法enjoy(),
 26  * 但子类Cat觉得自己去实现这个enjoy()方法也不合适,因此它把它自己也声明成一个抽象的类,
 27  * 那么,谁去实现这个抽象的enjoy方法,谁继承了子类,那谁就去实现这个抽象方法enjoy()。
 28  * @author gacl
 29  *
 30  */
 31 abstract class Cat extends Animal {
 32 
 33     /**
 34      * Cat添加自己独有的属性
 35      */
 36     public String eyeColor;
 37 
 38     public Cat(String n, String c) {
 39         super(n);//调用父类Animal的构造方法
 40         this.eyeColor = c;
 41     }
 42 }
 43 
 44 /**
 45  * 子类BlueCat继承抽象类Cat,并且实现了从父类Cat继承下来的抽象方法enjoy
 46  * @author gacl
 47  *
 48  */
 49 class BlueCat extends Cat {
 50 
 51     public BlueCat(String n, String c) {
 52         super(n, c);
 53     }
 54 
 55     /**
 56      * 实现了抽象方法enjoy
 57      */
 58     @Override
 59     public void enjoy() {
 60         System.out.println("蓝猫叫...");
 61     }
 62     
 63 }
 64 
 65 /**
 66  * 子类Dog继承抽象类Animal,并且实现了抽象方法enjoy
 67  * @author gacl
 68  *
 69  */
 70 class Dog extends Animal {
 71     /**
 72      * Dog类添加自己特有的属性
 73      */
 74     public String furColor;
 75 
 76     public Dog(String n, String c) {
 77         super(n);//调用父类Animal的构造方法
 78         this.furColor = c;
 79     }
 80 
 81     @Override
 82     public void enjoy() {
 83         System.out.println("狗叫....");
 84     }
 85 
 86 }
 87 
 88 public class TestAbstract {
 89 
 90     /**
 91      * @param args
 92      */
 93     public static void main(String[] args) {
 94 
 95         /**
 96          * 把Cat类声明成一个抽象类以后,就不能再对Cat类进行实例化了,
 97          * 因为抽象类是残缺不全的,缺胳膊少腿的,因此抽象类不能被实例化。
 98          */
 99         //Cat c = new Cat("Catname","blue");
100         Dog d = new Dog("dogname","black");
101         d.enjoy();//调用自己实现了的enjoy方法
102         
103         BlueCat c = new BlueCat("BlueCatname","blue");
104         c.enjoy();//调用自己实现了的enjoy方法
105     }
106 }

 

接口

Java面向对象与代码编写_第27张图片

  JAVA是只支持单继承的,但现实之中存在多重继承这种现象,如“金丝猴是一种动物”,金丝猴从动物这个类继承,同时“金丝猴是一种值钱的东西”,金丝猴从“值钱的东西”这个类继承,同时“金丝猴是一种应该受到保护的东西”,金丝猴从“应该受到保护的东西”这个类继承。这样金丝猴可以同时从 “动物类”、“值钱的东西类”、“应该受到保护的东西” 这三个类继承,但由于JAVA只支持单继承,因此金丝猴只能从这三个类中的一个来继承,不能同时继承这三个类。因此为了封装现实生活中存在的多重继承现象,为了实现多继承,可以把其中的两个类封装成接口。使用接口可以帮助我们实现多重继承。

Java面向对象与代码编写_第28张图片

  接口的本质——接口是一种特殊的抽象类,这种抽象类里面只包含常量和方法的定义,而没有变量和方法的实现。

  抽象类所具有的一些东西接口可以具有,假如一个抽象类里面所有的方法全都是抽象的,没有任何一个方法需要这个抽象类去实现,并且这个抽象类里面所有的变量都是静态(static)变量,都是不能改变(final)的变量,这时可以把这样的抽象类定义为一个接口(interface)。把一个类定义成一个接口的格式是把声明类的关键字class用声明接口的关键字interface替换掉即可。

 /**
  * java中定义接口
  */
 public interface JavaInterfaces {
 
 }

  接口(interface)是一种特殊的抽象类,在这种抽象类里面,所有的方法都是抽象方法,并且这个抽象类的属性(即成员变量)都是声明成“public static final 类型 属性名”这样的,默认也是声明成“public static final”即里面的成员变量都是公共的、静态的,不能改变的。因此在接口里面声明常量的时候,可以写成“public static final 类型 常量名=value(值)”这样的形式,也可以直接写成“类型 常量名=value(值)”如:“public static final int id=10”可以直接写成“int id=10”这样的形式,因为在接口里面默认的属性声明都是“public static final”的,因此“public static final”可以省略不写。在接口里面声明的抽象方法可以不写abstract关键字来标识,因为接口里面所有的方法都是抽象的,因此这个“abstract”关键字默认都是省略掉的,如在一个接口里面声明这样的三个方法:“public void start()”、“public void run()”、“public void stop()”这三个方法前面都没有使用abstract关键字来标识,可它们就是抽象方法,因为在接口里面的声明的方法都是抽象方法,因此在接口里面的抽象方法都会把abstract关键字省略掉,因为默认声明的方法都是抽象的,所以就没有必要再写“abstract”字了,这一点与在抽象类里面声明抽象方法时有所区别,在抽象类里面声明抽象方法是一定要使用“abstract”关键字的,而在接口里面声明抽象方法可以省略掉“abstract”。注意:在接口里面声明的抽象方法默认是“public(公共的)”的,也只能是“public(公共的)”之所以要这样声明是为了修正C++里面多重继承的时候容易出现问题的地方,C++的多继承容易出现问题,问题在于多继承的多个父类之间如果他们有相同的成员变量的时候,这个引用起来会相当地麻烦,并且运行的时候会产生各种各样的问题。JAVA为了修正这个问题,把接口里面所有的成员变量全都改成static final,成员变量是static类型,那么这个成员变量就是属于整个类里面的,而不是专属于某个对象。对于多重继承来说,在一个子类对象里面实际上包含有多个父类对象,而对于单继承来说,子类对象里面就只有一个父类对象。多继承子类对象就有多个父类对象,而这些父类对象之间可能又会存在有重复的成员变量,这就非常容易出现问题,因此在JAVA里面避免了这种问题的出现,采用了接口这种方式来实现多继承。作为接口来说,一个类可以从接口继承(或者叫实现接口),这也是多继承,接口里面的成员变量不专属于某个对象,都是静态的成员变量,是属于整个类的,因此一个类去实现多个接口也是无所谓的,不会存在对象之间互相冲突的问题。实现多个接口,也就实现了多重继承,而且又避免了多重继承容易出现问题的地方,这就是用接口实现多重继承的好处。

 

接口特性

Java面向对象与代码编写_第29张图片

接口举例

 1 package javastudy.summary;
  2 
  3 /**
  4  * 这里定义了接口:Painter。 在Painter接口里面定义了paint()和eat()这两个抽象方法。
  5  * 
  6  * @author gacl
  7  * 
  8  */
  9 interface Painter {
 10     public void eat();
 11 
 12     public void paint();
 13 }
 14 
 15 /**
 16  * 这里定义了两个接口:Singer 在Singer接口里面定义了sing()和sleep()这两个抽象方法。
 17  * 
 18  * @author gacl
 19  * 
 20  */
 21 interface Singer {
 22     public void sing();
 23 
 24     public void sleep();
 25 }
 26 
 27 /**
 28  * 类Student实现了Singer这个接口
 29  * 
 30  * @author gacl
 31  * 
 32  */
 33 class Student implements Singer {
 34 
 35     private String name;
 36 
 37     public Student(String name) {
 38         this.name = name;
 39     }
 40 
 41     public String getName() {
 42         return name;
 43     }
 44 
 45     public void setName(String name) {
 46         this.name = name;
 47     }
 48 
 49     /**
 50      * 实现接口中定义的sing方法
 51      */
 52     @Override
 53     public void sing() {
 54         System.out.println("student is singing");
 55     }
 56 
 57     /**
 58      * 实现接口中定义的sleep方法
 59      */
 60     @Override
 61     public void sleep() {
 62         System.out.println("student is sleeping");
 63     }
 64 
 65     public void study() {
 66         System.out.println("Studying...");
 67     }
 68 
 69 }
 70 
 71 /**
 72  * Teacher这个类实现了两个接口:Singer和Painter。 这里Teacher这个类通过实现两个不相关的接口而实现了多重继承。
 73  * 
 74  * @author gacl
 75  * 
 76  */
 77 class Teacher implements Singer, Painter {
 78 
 79     private String name;
 80 
 81     public Teacher(String name) {
 82         this.name = name;
 83     }
 84 
 85     /**
 86      * 在Teacher类里面重写了这两个接口里面的抽象方法,
 87      * 通过重写抽象方法实现了这两个接口里面的抽象方法。
 88      */
 89     @Override
 90     public void eat() {
 91         System.out.println("teacher is eating");
 92     }
 93 
 94     public String getName() {
 95         return name;
 96     }
 97 
 98     @Override
 99     public void paint() {
100         System.out.println("teacher is painting");
101     }
102 
103     public void setName(String name) {
104         this.name = name;
105     }
106 
107     @Override
108     public void sing() {
109         System.out.println("teacher is singing");
110     }
111 
112     @Override
113     public void sleep() {
114         System.out.println("teacher is sleeping");
115     }
116 
117     public void teach() {
118         System.out.println("teaching...");
119     }
120 }
121 
122 public class TestInterfaces {
123 
124     public static void main(String[] args) {
125         /**
126          * 这里定义了一个接口类型的变量s1
127          */
128         Singer s1 = new Student("le");
129         s1.sing();
130         s1.sleep();
131         Singer s2 = new Teacher("steven");
132         s2.sing();
133         s2.sleep();
134         Painter p1 = (Painter)s2;
135         p1.paint();
136         p1.eat();
137     }
138 }

  这里验证了两个规则,“一个类可以实现多个无关的接口”,Teacher类既实现了Singer接口,同时也实现了Painter接口,而Singer接口和Painter接口是无关系的两个接口。“多个无关的类可以实现同一接口”,Student类和Teacher类都实现了Singer接口,而Student类和Teacher类并不是关系很密切的两个类,可以说是无关的两个类。

运行结果:

Java面向对象与代码编写_第30张图片

 

画内存分析图体会接口与实现类之间存在的多态性

  Java面向对象与代码编写_第31张图片

  首先分析main方法的第一句话

   Singer s1 = new Student(“le”);  

  这里首先定义了一个接口类型的变量s1,接口Singer是Student类实现的,即相当于Student类从Singer接口继承,Singer接口的本质是一个特殊的抽象类,所以这里Singer接口就是Student类的父类,因此s1就是父类对象的一个引用,即这里这句话执行完后就是一个父类对象s1的引用指向子类对象Student。所以内存里面的布局应该是这样:栈空间里面有一个父类对象的引用s1,堆空间里面new出了一个Student对象,创造这个Student对象的时候调用了Student类的构造方法Student(String name),其定义如下:

  Student(String name){

    this.name = name;

  }

  通过调用构造方法使得这个Student对象有了一个自己的名字“le”,因此堆内存里面的Student对象的name属性值为“le”。

  这个Student对象能够访问位于代码区里面的sleep()方法和sing()方法,因为Student类从父类Sing继承而来,因此自然可以访问到这两个方法,除此之外,还能访问Student类里面自定义的Study()方法。因此代码区里面存放着这三个方法等待着Student类的对象去访问,也就是去调用。一个正常的Student可以直接调用这三个方法。那么怎么找得到位于代码区的这三个方法呢?Student对象里面存在着能找得到这个三个方法的函数指针,引用对象通过这个指针的索引指向就能找到代码区里面的这三个方法。

  s1是父类对象的索引,但此时s1指向的却是子类对象,即一个父类对象的索引指向了子类对象。这里很不幸的是,由于这个s1是一个父类对象的引用,站在s1的角度上,它就是只把你这个子类对象Student当成是一个Singer,s1只能看到Student对象里面的sing()和sleep这两个方法的方法指针,因此使用这个s1引用对象只能去访问从父类继承下来的sleep()和sing()这两个方法,但由于这两个方法在子类Student里面被重写了,那么现在就是这种情况了,子类Student从父类Singer继承,在子类里面重写了从父类继承下来的sing()和sleep()这两个方法,父类对象的引用指向了子类对象,这三种情况加在一起就使得多态可以存在了,这样调用位于代码区里面的方法时,会根据new出来的实际对象去调用代码区里面的方法,因此这里在s1眼里虽然是把这个new出的Student当成一个Singer,但这个对象实际上就是一个Student,因此使用父类对象的引用s1调用代码区里面的sleep()和sing()方法时,调用的是在子类里面重写过后的sing()和sleep()方法。

  接着分析第二句话

    Singer s2 = new Teacher(“steven”);  

  Teacher这个类实现了Singer接口和Painter接口,即相当于从两个父类继承,一个父类是Singer,另一个父类是Painter。

  Java面向对象与代码编写_第32张图片

  这里的s2也是父类对象Singer的引用,指向的却是子类对象Teacher,因此也是一个父类对象的引用指向子类对象。

  创造这个Teacher对象的时候,调用Teacher(String name)构造方法,其定义如下:

    Teacher(String name){

      this.name=name;

    }

  调用构造方法后,Teacher有了自己的名字steven,所以Teacher的name属性值为steven,由于这个Teacher实现了Painter接口和Singer接口,因此也继承这两个接口里面的方法,因此一个正常的Teacher可以访问的方法有:paint()、eat()和sing()、sleep。前面两个方法是从Painter类继承过来的,后面两个方法是从Singer类继承过来的。除了这四个方法外,还有自己定义的Teach()方法。可是很不幸的是,由于s2是一个Singer类对象的引用,因此站在s2的角度来看,它只把Teacher当成是一个普通的Singer,因此它看到的只是Teacher对象里面的sing()和sleep()这两方法,然后要调用时就通过Teacher对象里面的函数指针找到位于代码区的sleep()和sing()这两个方法。别的方法s2是看不到的,因此也调用不了。

  Painter p1=(Painter)s2;

  这里把s2强制转换成Painter,s2对象实际是指向Teacher的,把s2强制转换成Painter以后,就可以把Teacher当成Painter来用,所以p1会把Teacher当成Painter来看待,因此p1只能看到Teacher里面的painter()方法和eat()方法,因此能够访问到的也只有这两个方法。所以接口对于我们实际当中的对象来说,每一个接口暴露了我们这个实际对象的一部分方法。你使用什么样的接口,就只能访问这个接口里面定义的方法,别的接口定义的方法就没办法访问得到。

  接口可以帮助我们实现多重继承这种逻辑,接口和它的实现类之间存在多态性。

 

通过下面这些代码验证接口更进一步的特性

 1 package javastudy.summary;
  2 
  3 /**
  4  * 把“值钱的东西”这个类定义成一个接口Valuable。在接口里面定义了一个抽象方法getMoney()
  5  * @author gacl
  6  *
  7  */
  8 interface Valuable {
  9     public double getMoney();
 10 }
 11 
 12 /**
 13  * 把“应该受到保护的东西”这个类定义成一个接口Protectable。
 14  * 在接口里面定义了一个抽象方法beProtected();
 15  * @author gacl
 16  *
 17  */
 18 interface Protectable {
 19     public void beProteced();
 20 }
 21 
 22 /**
 23  * 这里是接口与接口之间的继承,接口A继承了接口Protectable,
 24  * 因此自然而然地继承了接口Protectable里面的抽象方法beProtected()。
 25  * 因此某一类去实现接口A时,除了要实现接口A里面定义的抽象方法m()以外,
 26  * 还要实现接口A从它的父接口继承下来的抽象方法beProtected()。
 27  * 只有把这两个抽象方法都实现了才算是实现了接口A。
 28  * @author gacl
 29  *
 30  */
 31 interface A extends Protectable {
 32     void m();
 33 }
 34 
 35 /**
 36  * 这里定义了一个抽象类Animal。
 37  * @author gacl
 38  *
 39  */
 40 abstract class Animal {
 41     private String name;
 42     /**
 43      * 在Animal类里面声明了一个抽象方法enjoy()
 44      */
 45     abstract void enjoy();
 46 }
 47 
 48 /**
 49  * 这里是为了实现了我们原来的语义:
 50  * “金丝猴是一种动物”同时“他也是一种值钱的东西”同时“他也是应该受到保护的东西”。而定义的一个类GoldenMonKey。
 51  * 为了实现上面的语义,这里把“值钱的东西”这个类定义成了一个接口Valuable,
 52  * 把“应该受到保护的东西”这个类也定义成了一个接口Protectable。这样就可以实现多继承了。
 53  * GoldenMonKey类首先从Animal类继承,然后GoldenMonKey类再去实现Valuable接口和Protectable接口,
 54  * 这样就可以实现GoldenMonKey类同时从Animal类,Valuable类,Protectable类继承了,即实现了多重继承,
 55  * 实现了原来的语义。
 56  * @author gacl
 57  *
 58  */
 59 class GoldenMonKey extends Animal implements Valuable,Protectable {
 60 
 61     /**
 62      * 在GoldenMoKey类里面重写了接口Protectable里面的beProtected()这个抽象方法,
 63      * 实现了接口Protectable。
 64      */
 65     @Override
 66     public void beProteced() {
 67         System.out.println("live in the Room");
 68     }
 69 
 70     /**
 71      * 在GoldenMoKey类里面重写了接口Valuable里面的getMoney()这个抽象方法,实现了接口Valuable。
 72      */
 73     @Override
 74     public double getMoney() {
 75         return 10000;
 76     }
 77 
 78     /**
 79      * 这里重写了从抽象类Animal继承下来的抽象方法enjoy()。
 80      * 实现了这抽象方法,不过这里是空实现,空实现也是一种实现。
 81      */
 82     @Override
 83     void enjoy() {
 84         
 85     }
 86     
 87     public static void test() {
 88         /**
 89          * 实际当中在内存里面我们new的是金丝猴,在金丝猴里面有很多的方法,
 90          * 但是接口的引用对象v能看到的就只有在接口Valuable里面声明的getMoney()方法,
 91          * 因此可以使用v.getMoney()来调用方法。而别的方法v都看不到,自然也调用不到了。
 92          */
 93         Valuable v = new GoldenMonKey();
 94         System.out.println(v.getMoney());
 95         /**
 96          * 把v强制转换成p,相当于换了一个窗口,通过这个窗口只能看得到接口Protectable里面的beProtected()方法
 97          */
 98         Protectable p = (Protectable)v;
 99         p.beProteced();
100     } 
101 }
102 
103 /**
104  * 这里让Hen类去实现接口A,接口A又是从接口Protectable继承而来,接口A自己又定义了一个抽象方法m(),
105  * 所以此时相当于接口A里面有两个抽象方法:m()和beProtected()。
106  * 因此Hen类要去实现接口A,就要重写A里面的两个抽象方法,实现了这两个抽象方法后才算是实现了接口A。
107  * @author gacl
108  *
109  */
110 class Hen implements A {
111 
112     @Override
113     public void beProteced() {
114         
115     }
116 
117     @Override
118     public void m() {
119         
120     }
121     
122 }
123 
124 /**
125  * java中定义接口
126  */
127 public class JavaInterfacesTest {
128 
129     public static void main(String[] args) {
130         GoldenMonKey.test();
131     }
132 }

接口总结:接口和接口之间可以相互继承,类和类之间可以相互继承,类和接口之间,只能是类来实现接口。

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(如何编写)