本帖最后由 ufof 于 2017-3-8 15:06 编辑



5.1 面向对象思想



5.1.1  面向过程思想概述

这一章我们不讲任何Java,就来讲讲编程思想。
面向过程思想是最传统的编程思想,思考方式与人的思考方式相违背。在面向过程编程中,“过程”或“函数”最大。程序的流程就是一个函数调用另外一个函数、另外一个函数又调用另一个函数。
上渣图:


简单地说,面向过程是把动词写在名词前面。例如“吃.西瓜”。

这种编程思想大家只是了解一下,我们要真正开始学习的是面向对象思想。

5.1.2 面向对象思想概述

在面向对象思想中,“类”最大。通过对类的实例化创建对象。类相当于图纸,对象相当于你通过这个图纸生产出来的事物。例如我的类是狗类。我实例化这个狗类,就得出了一只小狗来。这只小狗有许多行为,写法就是“小狗.吃骨头”、“小狗.睡觉”等。所以说面向对象思想中,名词在动词前,这与面向过程相对。

万物都有两个部分:属性和行为。狗类的属性有体型、皮肤颜色等;狗类的行为有吃骨头、看家、狗吠等。属性和行为都被封装在类当中。所以说类是图纸



但是真正使用的不是图纸,是你通过图纸创造出来的对象。只有实例化一个对象才可以调用其的属性以及行为。

我们人类说话的方式也是先说主语再说动词,例如“我写字”、“你画画”等。所以说,面向对象思想更加符合人类的原始思考方式。这样编程也更加优秀。

本章小结:
  • 面向过程注重于动作;面向对象注重于事物
  • 面向过程通过对函数的调用实现;面向对象通过实例化类来实现
  • 面向过程与人类思考方式违背;面向过程符合人类思考方式




5.2 类的成员



5.2.1  类和成员概述

上一节课已经阐明了,在面向对象思想中,类是图纸。也就是说明一种对象的属性以及方法都定义在类当中。
类的定义方法如下:
  1. 若干个修饰符 class  类名{
  2. }
复制代码
类名一般用大驼峰命名法。
那么类中有什么?对象的属性相对于代码中的成员变量/成员常量(统称字段);对象的行为对应代码中的方法

除了字段和方法之外,类还有以下成员:
  • 构造方法
  • 内部类
  • 初始化块

5.2.2 字段

我们之前学定义变量常量都是在方法中定义的。在方法中的变量常量称之为局部变量/局部常量。这两种量的生命周期仅存于方法的开始到方法的结束。

现在,我们要在类当中、方法之外定义变量常量。这种量叫做成员变量/成员常量,统称字段。他们的生命周期比局部变量长。

例如:
  1. class Dog{
  2.     String name;  //名称
  3.     int age;         //年龄
  4.     int height;      //身高
  5. }
复制代码

注:字符串String我们还没有学到。这里简单的说一下。字符串可以存储若干个字符。是引用数据类型。

5.2.3  方法

狗有看家、吃骨头、狗吠的行为。行为对应的是代码中的方法。这也同样被定义在类当中:
注意:这里的方法就不要声明为static了!理由我们在讲static静态的时候会讲。

  1. class Dog{
  2.     String name;  //名称
  3.     int age;         //年龄
  4.     int height;      //身高

  5.     void lookAfterHouse(){ //看家方法
  6.         System.out.println("看家");
  7.     }
  8.     void eatBone(){   //吃骨头
  9.         System.out.println("吃骨头");
  10.     }
  11.     void bark(){    //狗吠
  12.         System.out.println("汪");
  13.     }
  14. }
复制代码


好的,狗类基本上已经定义完了。图纸定义完了,如何通过这个图纸真正的制造出一个实实在在的对象呢?请看下一章。

本章小结:
  • 类的成员有:字段、方法、构造方法和内部类
  • 字段对应的是对象的属性;方法对应的是对象的行为



5.3 对象的实例化以及调用其成员



5.3.1 实例化方法

图纸已经画完了,如何通过这个图纸生产出一个实实在在的对象呢?这个过程叫做实例化。

实例化语法:
  1. 类名 实例名;
  2. 实例名 = new 类名();
复制代码

可简写为:
  1. 类名 实例名 = new 类名();
复制代码

实例名一般用小驼峰命名法。
这里我们实例化狗类。

  1. class Dog{
  2.     String name;  //名称
  3.     int age;         //年龄
  4.     int height;      //身高

  5.     void lookAfterHouse(){ //看家方法
  6.         System.out.println("看家");
  7.     }
  8.     void eatBone(){   //吃骨头
  9.         System.out.println("吃骨头");
  10.     }
  11.     void bark(){    //狗吠
  12.         System.out.println("汪");
  13.     }
  14. }

  15. class InstantiationDemo{
  16.     public static void main(String[] args){
  17.         Dog myDog = new Dog();  //实例化Dog类
  18.     }
  19. }
复制代码

注:对,一个.java文件可以放多个类。但是编译之后会有多个.class文件。

在这个程序中,myDog叫做对象的实例。
这个在内存中具体是什么样的请看下一章。

好的,现在我们已经有一个实例了,如何调用其的字段以及方法?

5.3.2  调用实例的字段以及方法

调用字段的语法:
  1. 实例.字段名;
复制代码

调用方法的语法:

  1. 实例.方法名(方法要求的参数);
复制代码

在这个例子当中:

  1. class Dog{
  2.     String name;  //名称
  3.     int age;         //年龄
  4.     int height;      //身高

  5.     void lookAfterHouse(){ //看家方法
  6.         System.out.println("看家");
  7.     }
  8.     void eatBone(){   //吃骨头
  9.         System.out.println("吃骨头");
  10.     }
  11.     void bark(){    //狗吠
  12.         System.out.println("汪");
  13.     }
  14. }

  15. class InstantiationDemo{
  16.     public static void main(String[] args){
  17.         Dog myDog = new Dog();     //实例化
  18.         myDog.name = "小狗狗";      //把myDog的字段name赋值为"狗狗"
  19.         System.out.println("我家的狗的名字是"+myDog.name);   //打印myDog.name
  20.         myDog.bark();   //让myDog叫一下
  21.     }
  22. }
复制代码
结果:



好的,这就是如何调用实例的成员了。
实例也称引用。两者完全相同。

5.3.3 引用数据类型

在第二章中,我们已经学习过了基本数据类型。我们今天学的实例化所得到的实例是引用数据类型的一种。这种通过实例化类的方式得到的实例叫做“类类型”。

引用数据类型还有两种,分别是数组和接口,我们以后会接触到。



5.3.4 null

null是一个关键字,也是一个字面量。
null可以赋给任意一种引用数据类型。当一个实例被赋为null时,其将不会指向堆内存中的任何对象。详见下一章。
  1. Dog d = null;
复制代码
当一个实例被置为null时,不能调用其的字段/方法,不然会抛出NullPointerException异常。


本章小结:
  • 实例化的方法为“类名 实例名 = new 类名()”
  • 可以通过“实例.字段”调用其的字段
  • 可以通过“实例.方法()”调用其的方法
  • 被null赋值的实例不指向任何对象




5.4 堆内存



5.4.1 实例化的具体过程

我们在讲方法的时候讲过了栈内存。相信也不难理解。现在我们再来学习另外一种内存的规划,叫做堆内存。

栈内存用于存储局部变量/实例,堆内存用于存储对象。

  1. Dog myDog = new Dog();
复制代码
我们来开始解析这一句话:

首先“Dog myDog”是经典的“类型 变量名”格式,这个和我们声明基本数据类型的方法也一样。myDog这个实例是在栈内存中被存储的:


这和局部变量/常量没有任何区别。

那么new Dog()在内存中是什么样子呢?这里就用到了堆内存。



在堆内存当中,新建了一个Dog类的对象。里面的内容其实就是Dog类中的成员。

那么这两者是如何关联起来的呢?大家可以发现“Dog myDog = new Dog();”中间有一个等号。

栈内存中的实例指向了堆内存中的对象。


这下就用该明白了实例化在内存中具体是如何实现的。
堆内存还有一个特点:里面的所有值都是由默认值的,所以类的字段一般不需要赋值。

本章小结
  • 堆内存用于存储对象
  • 当对类进行实例化时,该实例指向了堆内存中的对象



5.5 static静态



5.5.1  static概述

我们之前在定义字段或者方法时都必须要实例化其所在的类才可以调用。因为这些字段和方法属于实例。
在讲静态之前,先举一个例子:
每一个学生都有自己的课本和笔,但是他们都共用教室里的饮水机。假如说学生是一个类,课本和笔这两个字段是属于实例的、饮水机是大家公用的,所以属于类。

static是一个修饰符。如果一个类的字段/方法属于整个类而非单个实例,可以用static修饰。
没有被声明为static的成员叫做实例成员、反之称之为静态成员。
被声明为static的成员有以下特点:
  • 类一旦被加载、其也被加载
  • 静态成员优先于实例成员
  • 静态成员多了一种被调用的方式:类名.字段名/类名.方法名

5.5.2 static实例

  1. class Resource{
  2.     static int staticNum = 2;      //定义静态字段
  3.     int num = 7;                       //定义实例字段
  4. }

  5. class StaticDemo{
  6.     public static void main(String[] args){
  7.         //打印staticNum字段
  8.         System.out.println(Resource.staticNum);  //通过“类名.字段”格式访问静态字段staticNum
  9.         //打印num字段
  10.         Resource mc = new Resource();
  11.         System.out.println(mc.num);                   //通过实例mc调用实例字段num
  12.     }
  13. }
复制代码
结果:



在Resource类当中,定义了一个静态字段叫做staticNum以及一个实例字段num。在主方法中,使用了"Resource.staticNum"格式直接访问了staticNum这个静态字段。没有被声明为static的实例字段num只能通过传统的先实例化然后通过实例访问的方式来访问。

学生提问:既然被声明为static的字段/方法能如此方便的被调用,干脆全部声明为static可以吗?

答:不现实。再引用一下刚才学生的例子。如果每一个学生都给一个饮水机,会很占空间。所以说是否声明为static需要判断这个成员到底是共享的还是实例的。

再写一个静态方法的实例:

  1. class Resource{
  2.     static void staticMethod(){
  3.         System.out.println("我是静态方法");
  4.     }
  5.     void instanceMethod(){
  6.         System.out.println("我是实例方法");
  7.     }
  8. }

  9. class StaticDemo{
  10.     public static void main(String[] args){
  11.         Resource.staticMethod();  //通过“类名.方法”格式调用staticMethod
  12.         Resource r = new Resource();
  13.         r.instanceMethod();       //通过实例化类然后用实例调用instanceMethod方法
  14.     }
  15. }
复制代码
结果:



和刚才字段的例子一样。staticMethod()是静态方法,所以他可以直接被“Resource.staticMethod()”的方式调用。instanceMethod()是实例方法,必须实例化Resource类然后通过实例才能调用它。

5.5.3 内存中的静态

当一个成员被声明为静态后,其就不会出现在堆内存之中了。其会被调往内存共享区
而且,静态成员在内存中只有一个实体。不可能像实例成员那样每实例化一个对象就出现一个新的实体。

堆内存中的对象自动获得内存共享区的静态成员

  1. class Resource{
  2.     static int staticNum = 30;
  3.     int num = 20;
  4. }

  5. class StaticDemo{
  6.     public static void main(String[] args){
  7.         Resource r1 = new Resource();    //实例化
  8.         Resource r2 = new Resource();
  9.         Resource r3 = new Resource();
  10.     }
  11. }
复制代码
这个程序在内存中是这样的:



5.5.4 注意事项

  • 实例成员不能被“类名.成员”方式调用;
  • 静态成员也可以被“实例.成员”方式调用,但是不建议这样做。因为饮水机是大家的,不是你的;
  • 静态方法只能调用静态方法。因为静态方法更加早加载,那个时候实例方法还没有出生呢;(现在大家可以明白我们在讲方法的时候为什么直接被主方法调用的方法也得要是static了)
  • 实例方法既可以调用静态方法也可以调用实例方法。因为实例方法出生的晚,其他人早就都存在了;
  • 局部变量/常量(就是在方法中定义的变量/常量)不能被声明为静态。因为他的生命周期仅局限于方法当中。不足以被共享。

本章小结:
  • static是一个修饰符。如果一个字段或方法被static修饰,他就成为了一个静态字段/静态方法
  • 被声明为静态的成员多了一种被调用的方式:“类名.成员”
  • 被静态修饰的成员会被调往内存共享区
  • 静态这个知识点有许多的注意事项



5.6 权限修饰符:public和private



5.6.1 权限修饰符概述

我们人有许多的信息,这些信息有些是可以对外暴露的,有些是保密的。对外暴露的有姓名、性别等;保密的有年龄(对于女孩来讲)、工资等。

在Java中,也有对类成员进行权限设置的修饰符。权限修饰符一共有四种:private、default、protected、public。由于default和protected涉及到包以及子父类的概念,这里先不讲解。

5.6.2 权限修饰符应用

权限修饰符可以修饰类中的字段、方法、构造方法。
被声明为private的成员只能在本类中使用,称之为私有;
被声明为public的成员拥有最高的访问权限,称之为公有。
没有声明的话即是default。

  1. class PermissionDemo{
  2.     public static void main(String[] args){
  3.         Human h = new Human();
  4.         h.name = "Adam";
  5.         h.gender = "Man";
  6.         h.age = 28;            //非法
  7.         h.salary = 7000;    //非法
  8.     }
  9. }

  10. class Human{
  11.     public String name;     //公有字段name
  12.     public String gender;    //公有字段gender
  13.     private int age;        //私有字段age
  14.     private int salary;     //私有字段salary
  15. }
复制代码
结果:



由于被声明为private的字段只能在本类中使用,PermissionDemo主类不能访问被private修饰的age和salary字段,所以报错。

但是本类中就可以调用这两个私有字段:

  1. class Human{
  2.     public String name;     //公有字段name
  3.     public String gender;    //公有字段gender
  4.     private int age;        //私有字段age
  5.     private int salary;     //私有字段salary
  6.    
  7.     void introduce(){
  8.         System.out.println("我的名字是"+name);
  9.         System.out.println("我"+age+"岁了");          //合法
  10.         System.out.println("我的工资是"+salary);     //合法
  11.     }
  12. }
复制代码
由于introduce()方法和两个private的字段都是在同一个类中,所以说是合法的。
权限修饰符照样可以修饰方法,毕竟方法也是成员之一:

  1. class Human{
  2.     public String name;
  3.     private int salary;
  4.     private void showSalary(){
  5.         sallary = 7000;
  6.         System.out.println(salary);
  7.     }
  8.     public void showName(){
  9.         name = "Adam";
  10.         System.out.println(name);
  11.     }
  12. }

  13. class PermissionDemo{
  14.     public static void main(String[] args){
  15.         Human h = new Human();
  16.         h.showName();    //合法
  17.         h.showSalary();//非法
  18.     }
  19. }
复制代码
结果:




本章小结
  • 权限修饰符用于修饰类成员。可以对其的访问权限进行设置
  • 权限从小到达依次为“private、default、protected、public”
  • 被声明为private的成员仅能在本类中使用;public是最高权限,即使不同包的类也可以使用



5.7 匿名对象



5.7.1  匿名对象:访问成员

我们之前如果要访问类中的字段或者方法,必须要实例化这个类的一个对象。无论要访问多少次,都得要实例化。如果我只需要访问一次,可能稍微显得有一些麻烦。

假如说我想要访问Dog类的bark()方法:
  1. class AnonymousObjectDemo{
  2.         public static void main(String[] args){
  3.             Dog dog = new Dog();   //实例化
  4.             dog.bark();                   //访问方法
  5.         }
  6. }

  7. class Dog{
  8.     String name;
  9.     int age;
  10.     int height;

  11.     void lookAfterHouse(){
  12.         System.out.println("看家");
  13.     }
  14.     void eatBone(){
  15.         System.out.println("吃骨头");
  16.     }
  17.     void bark(){
  18.         System.out.println("汪");
  19.     }
  20. }
复制代码
结果:



仅仅访问一个方法,就得要实例化一个实例。这样的做法实在有一些麻烦。

匿名对象是指,在创建对象的时候不指向任何实例。可以通过这个对象来直接访问方法。但是由于其没有指向任何实例,Java把它视为垃圾。其过一段时间后会被垃圾回收器清除掉。

上面这个例子可以简化为:
  1. class AnonymousObjectDemo{
  2.         public static void main(String[] args){
  3.             new Dog().bark();   //创建匿名对象
  4.         }
  5. }

  6. class Dog{
  7.     String name;
  8.     int age;
  9.     int height;

  10.     void lookAfterHouse(){
  11.         System.out.println("看家");
  12.     }
  13.     void eatBone(){
  14.         System.out.println("吃骨头");
  15.     }
  16.     void bark(){
  17.         System.out.println("汪");
  18.     }
  19. }
复制代码
结果:



在new Dog()后面直接加上.bark()访问bark方法。这样做不需要创建实例,所以可以节省栈内存中的空间。
如果要访问多个方法,还是得要实例化对象。

注意:
使用匿名对象来对字段进行赋值是没有意义的。比如说:
  1. new Dog().age = 5;
复制代码
好的,你把他的age字段给改了,但是你以后可以用得上吗?你刚刚new出来的对象因为没有实力已经在内存中找不到了或被垃圾处理器给清除了。所以说对字段进行赋值是没有意义的。


5.7.2 匿名对象:参入参数

如果说一个方法需要一个对象来作为参数的话,一般的做法是先实例化,然后将实例传入。例如:

  1. class AnonymousObjectDemo{
  2.     public static void main(String[] args){
  3.         MyClass mc = new MyClass();                //实例化
  4.         Demo.aMethod(mc);                        //传入实例
  5.     }
  6. }

  7. class Demo{
  8.     public static void aMethod(MyClass mc){        //创建静态方法,参数为MyClass对象
  9.         System.out.println("Method runs");
  10.     }
  11. }

  12. class MyClass{}
复制代码
结果:



可以在调用函数的时候不用传入实例。可以这样简化:

  1. class AnonymousObjectDemo{
  2.     public static void main(String[] args){
  3.         Demo.aMethod(new MyClass());            //传入匿名对象
  4.     }
  5. }

  6. class Demo{
  7.     public static void aMethod(MyClass mc){        //创建静态方法,参数为MyClass对象
  8.         System.out.println("Method runs");
  9.     }
  10. }

  11. class MyClass{}
复制代码
结果:



在主方法中调用aMethod()方法时,传入的是new MyClass(),这样做是将匿名对象传入方法中。这样的做法不需要创建实例,节省了栈内存。

本章小结
  • 匿名对象是指在创建对象的时候不让其指向任何实例
  • 匿名对象可以用于:
  • 调用方法/字段一次
  • 作为参数传入方法



5.8 面向对象三特点:封装



5.8.1  封装概述

封装是面向对象三个特点之一,在编程中十分重要,必须要掌握。
举一个例子:我们在用电脑的时候,只有显示器是可视化的。电脑中的硬件我们是不能访问的,而且我们也不知道。虽说不知道,不影响我们对电脑的使用。如果让用电脑的人自由去拆开电脑去鼓捣硬件的话,最大的问题是安全性。由于客户不了解硬件,电脑很可能会坏。
编程的时候,客户也不能直接访问类中的成员。隐藏对象的属性以及细节的过程,叫做封装。

我们通过一个实际例子来了解为什么一些细节不能被用户直接访问:

  1. class EncapsulationDemo{
  2.     public static void main(String[] args){
  3.         Dog d = new Dog();
  4.         d.age = -1;        //将-1赋给年龄
  5.     }
  6. }

  7. class Dog{
  8.     String name;
  9.     int age;
  10.     int height;

  11.     void lookAfterHouse(){
  12.         System.out.println("看家");
  13.     }
  14.     void eatBone(){
  15.         System.out.println("吃骨头");
  16.     }
  17.     void bark(){
  18.         System.out.println("汪");
  19.     }
  20. }
复制代码

在这个例子当中,用户实例化了Dog类之后将年龄设置成了-1。这样做不会引起任何错误,因为-1在int范围当中。可是从逻辑层面来讲,-1岁是不可能的。这样的漏洞是由于对字段的访问“太过自由”导致的。

5.8.2 实现封装

引用刚才的例子。解决这个漏洞最好的方法是不让用户通过实例来访问字段,而是通过一个方法来设置/获取字段。
首先,不让用户通过实例来访问字段的实现方法是将字段声明为private:
  1. class EncapsulationDemo{
  2.     public static void main(String[] args){
  3.         Dog d = new Dog();
  4.         d.age = -1;        //将-1赋给年龄
  5.     }
  6. }

  7. class Dog{
  8.     private String name;
  9.     private int age;
  10.     private int height;

  11.     void lookAfterHouse(){
  12.         System.out.println("看家");
  13.     }
  14.     void eatBone(){
  15.         System.out.println("吃骨头");
  16.     }
  17.     void bark(){
  18.         System.out.println("汪");
  19.     }
  20. }
复制代码
用private修饰完之后,现在这个字段已经完全不对外暴露了。用户永远无法访问它。现在我们要通过方法来获取/改变这三个字段。
一般来讲,用于获取/设置private字段的方法的名称一般为setXXX()或getXXX()。一般用public修饰,因为这毕竟是完全安全的。

对于setXXX()方法,一般会接收一个和XXX字段类型相同的参数。方法中把参数赋给字段即可。
对于getXXX()方法,直接把XXX字段return即可。

在Dog类后面加入这几个方法。
  1. public void setName(String str){   //接收String类型
  2.     name = str;                           //将参数str赋给name字段
  3. }
  4. public String getName(){
  5.     return name;                          //返回name字段
  6. }
  7.    
  8. public void setAge(int a){             //接收int类型
  9.     age = a;                                //将参数a赋给age字段
  10. }

  11. public int getAge(){
  12.     return age;                             //返回age字段
  13. }
  14. //下面的方法都是同理
  15. public void setHeight(int h){
  16.     height = h;
  17. }
  18. public int getHeight(){
  19.     return height;
  20. }
复制代码

在setXXX()方法中,是将方法的参数赋值个其所对应的字段。由于这些方法在本类当中,就算被声明为private,其也可以被访问。
在getXXX()方法中,是将字段返回。

不过问题还是没有解决。我这样做照样通过setAge()方法可以把年龄设置为-1。但是前面做的只是铺垫。我们可以在方法中对传进来的参数进行判断是否大于0,然后赋值:

  1.     public void setAge(int a){
  2.         if(a>0){        //比较是否大于0
  3.             age = a;    //如果满足就赋值
  4.         }
  5.         else
  6.         {
  7.             System.out.println("非法非法!必须大于0!赋值失败");
  8.         }
  9.     }
复制代码

好的!现在如果我们还将其赋值为-1:

  1. class EncapsulationDemo{
  2.     public static void main(String[] args){
  3.         Dog d = new Dog();
  4.         d.setAge(-1);    //调用setAge方法
  5.     }
  6. }

  7. //省略Dog类
复制代码
结果:



现在,这几个字段被我们封装完毕了!我们之后学了异常,还可以在年龄为负数的时候抛出异常呢!

本章小结
  • 封装是指隐藏对象的属性和实现细节
  • 一般对于类的字段不用实例来调用,而是通过方法来设置/获取
  • 一定要把字段声明为private!



5.9 this



5.9.1 this概述

this是一个关键字。this也是一个量,每一个非static的方法都会被分配一个这样的变量。
this这个变量的值是调用其所在的方法的实例

  1. class MyClass{
  2.     void method(){}
  3. }

  4. class ThisDemo{
  5.     public static void main(String[] args){
  6.         MyClass mc1 = new MyClass();
  7.         mc1.method();
  8.         
  9.         MyClass mc2 = new MyClass();
  10.         mc2.method();
  11.         
  12.         MyClass mc3 = new MyClass();
  13.         mc3.method();
  14.     }
  15. }
复制代码
在这个程序当中,MyClass类中的method的this是会变化的。mc1调用method()时,this的值会变成mc1;
mc2调用method()时,this的值会变成mc2......  以此类推。换句话说,this相当于是一个万能语句,谁调用该方法,它的值就会变成这调用这个方法的实例。

我们都知道,直接打印一个实例,结果会是一个实例的“类名+@+地址值”。this由于指向的是实例,直接打印this也会是这样。为了更方便与演示,我把刚才的程序改变一下。

  1. class MyClass{
  2.     void method(){
  3.         System.out.println(this);                    //将this打印输出
  4.     }
  5. }

  6. class ThisDemo{
  7.     public static void main(String[] args){
  8.         MyClass mc1 = new MyClass();
  9.         mc1.method();
  10.         
  11.         MyClass mc2 = new MyClass();
  12.         mc2.method();
  13.         
  14.         MyClass mc3 = new MyClass();
  15.         mc3.method();
  16.     }
  17. }
复制代码
结果:



根据这个结果,我们可以看出this的值是变化的。第一行是打印mc1的结果;第二行是打印mc2的结果;第三行是打印mc3的结果。现在大家应该对this的意义有比较深刻的了解了。但是,this究竟有什么用呢?

5.9.2 调用this的成员

我们在讲实例化的时候,曾说过可以通过实例访问类的成员。既然this的值是实例,我们是否可以通过this来访问其对应的实例的成员呢?

this访问字段的语法如下(很容易!):
  1. this.字段名;
复制代码
访问方法的语法也不言而喻:
  1. this.方法(要求的参数);
复制代码

我们写一个程序验证:
  1. class MyClass{
  2.     int num;                                    //num字段
  3.    
  4.     void method(){
  5.         System.out.println(this.num);        //打印输出this的num字段
  6.     }
  7. }

  8. class ThisDemo{
  9.     public static void main(String[] args){
  10.         MyClass mc1 = new MyClass();
  11.         mc1.num = 1;        //将mc1实例的num字段设置为1
  12.         mc1.method();
  13.         
  14.         MyClass mc2 = new MyClass();
  15.         mc2.num = 2;        //将mc2实例的num字段设置为2
  16.         mc2.method();
  17.         
  18.         MyClass mc3 = new MyClass();
  19.         mc3.num = 3;        //将mc3实例的num字段设置为3
  20.         mc3.method();
  21.     }
  22. }
复制代码
结果:



可以发现,主方法中的三个实例的num都是不同的,分别为1、2、3。打印this.num就相当于是打印了mc1、mc2、mc3的num值。

一般来讲,开发中this最常用的用途是区分成员变量和局部变量。我们在上一节讲封装的时候,setXXX的参数名和其设置的字段不一样。

  1. public void setAge(int a){
  2.     age = a;
  3. }
复制代码

这样做可读性很差:我怎么知道你设置的字段是age而不是a!

不过,如果参数的名字是age,就会和字段age冲突。系统会认为你在把age赋给自己(age=age)。现在,我们可以用this.age指向调用setAge方法的实例的age字段,来解决这个问题。

  1. public void setAge(int age){
  2.     this.age = age;
  3. }
复制代码
这样做可读性可以提升,我们知道你设置的字段是age了,而且也不会冲突。这个问题就完美解决了。

大家要注意一点:this不能在静态方法中使用。我们已经重复很多遍了:this的值是调用其所在的方法的实例。静态方法是通过类调用的,而非实例,那么this在静态方法中有值吗?显而易见。

本章小结
  • this永远指向调用其所在的方法的实例
  • 可以使用this调用实例的字段:this.字段名/this.方法(要求的参数)
  • this不能在静态方法中使用



5.10 构造方法



5.10.1 构造方法概述

构造方法是指在对象被创建的时候(即new出来的时候)直接调用的方法。这种方法一般用于在创建对象时直接对字段进行设置。
我们再来看一下之前讲过的实例化语法:
  1. 类名 实例名 = new 类名();
复制代码
我相信大家早就很好奇括号是干什么用的了。括号中填的是构造方法的参数。
构造方法的定义:
  1. 权限修饰符 类名(参数){
  2.    
  3. }
复制代码

构造方法没有名字,在定义的时候类名是什么就写什么。
如果一个类没有构造方法,其会自动有一个无参数无代码的构造方法。

5.10.2 构造方法实例

我们为Dog类写一个构造方法。要求的参数是所有字段:
  1. class Dog{
  2.     private String name;
  3.     private int age;
  4.     private int height;

  5.     //省略其他方法
  6.     public Dog(String name, int age, int height){    //定义构造方法
  7.         this.name = name;
  8.         this.age = age;
  9.         this.height = height;
  10.     }
  11. }

  12. class ConstructorDemo{
  13.     public static void main(String[] args){
  14.         Dog d = new Dog("狗狗",2,1);
  15.         System.out.println(d.getName());
  16.         System.out.println(d.getAge());
  17.         System.out.println(d.getHeight());
  18.     }
  19. }
复制代码
结果:



学生提问:this关键字不是指向其所在的方法的实例吗?在构造方法中使用都还没有实例吧,怎么用this呢?

答:构造方法中的this是由特殊含义的。在构造方法中的this指向的是即将要被创建的对象。

在Dog类中定义了一个构造方法,参数类型是String、int和int。在主方法中实例化对象的时候括号中必须要填写其所要求的参数。因为字段已经被赋值了,直接打印也是有结果的。

构造方法照样可以重载。

  1. class Dog{
  2.     private String name;
  3.     private int age;
  4.     private int height;

  5.     //省略其他方法
  6.     public Dog(){
  7.         
  8.     }
  9.     public Dog(String name, int age, int height){
  10.         this.name = name;
  11.         this.age = age;
  12.         this.height = height;
  13.     }
  14. }

  15. class ConstructorDemo{
  16.     public static void main(String[] args){
  17.         Dog d = new Dog();
  18.         Dog d1 = new Dog("狗狗",2,1);
  19.     }
  20. }
复制代码

在Dog类中,有两个构造方法。一个是没有参数的,一个是有三个参数的。所以在实例化对象的时候,要么不写参数,要么写三个。

本章小结
  • 构造方法是在对象被创建时自动调用的方法
  • 一般用于提前设置字段
  • 构造方法中的this指向的是即将被创建的对象
  • 构造方法也可以重载



5.11 面向对象三特点:继承



5.11.1  生活中的继承

我们将生活中的任意两个事物放在一起,总能找到一些共性。例如苹果和桃子都有种子、铅笔和圆珠笔都可以用来写字、小说和散文都有文字......如果我们向上抽取,让苹果和桃子都在水果类下,“有种子”属性可以定义在水果类中,这样的话苹果和桃子自然而然也有了种子,而不需要在独立的定义一次。

在思考关于继承的问题时,应当明确哪些行为是继承行为,哪些是特有行为。例如动物类中有吃饭和睡觉行为,狗类自然而然也有这两个行为。这种行为叫做继承行为。狗可以看家、啃骨头,这两个行为是动物类没有的,这种行为叫做特有行为。



在编程当中,善用继承特性可以提高代码的复用性以及弹性。减少多余的代码。

5.11.2 Java中应用继承

  1. class Dog{
  2.     //继承行为
  3.     public void sleep(){
  4.         System.out.println("睡觉");
  5.     }
  6.     public void eat(){
  7.         System.out.println("吃");
  8.     }
  9.     //特有行为
  10.     public void lookAfterHouse(){
  11.         System.out.println("看家");
  12.     }
  13.     public void bark(){
  14.         System.out.println("汪");
  15.     }
  16.    
  17. }

  18. class Cat{
  19.     //继承行为
  20.     public void sleep(){
  21.         System.out.println("睡觉");
  22.     }
  23.     public void eat(){
  24.         System.out.println("吃");
  25.     }
  26.     //特有行为
  27.     public void catchMouse(){
  28.         System.out.println("抓老鼠");
  29.     }
  30. }
复制代码

可以发现,sleep()和eat()方法显得重复。我们可以定义一个Animal父类,在其中定义
sleep()和eat(),让Dog类和Cat类继承Animal类,这两个子类就自动拥有了Animal类中的方法。
在Java中,通过关键字extends来实现继承,格式为:
  1. class 子类名 extends 父类{
  2. //一些代码
  3. }
复制代码

我们刚才的例子可以写成:
  1. class Animal{
  2.     public void sleep(){
  3.         System.out.println("睡觉");
  4.     }
  5.     public void eat(){
  6.         System.out.println("吃");
  7.     }
  8. }

  9. class Dog extends Animal{
  10.     //特有行为
  11.     public void lookAfterHouse(){
  12.         System.out.println("看家");
  13.     }
  14.     public void bark(){
  15.         System.out.println("汪");
  16.     }
  17.    
  18. }

  19. class Cat extends Animal{
  20.     //特有行为
  21.     public void catchMouse(){
  22.         System.out.println("抓老鼠");
  23.     }
  24. }
复制代码
这样一做,只需要在类中定义特有行为就行了,继承行为自动就会有了。
我们在主方法中创建Dog和Cat的匿名对象,调用继承行为:
  1. class InheritanceDemo{
  2.     public static void main(String[] args){
  3.         new Dog().sleep();
  4.         new Cat().eat();
  5.     }
  6. }
  7. class Animal{
  8.     public void sleep(){
  9.         System.out.println("睡觉");
  10.     }
  11.     public void eat(){
  12.         System.out.println("吃");
  13.     }
  14. }

  15. class Dog extends Animal{
  16.     //特有行为
  17.     public void lookAfterHouse(){
  18.         System.out.println("看家");
  19.     }
  20.     public void bark(){
  21.         System.out.println("汪");
  22.     }
  23.    
  24. }

  25. class Cat extends Animal{
  26.     //特有行为
  27.     public void catchMouse(){
  28.         System.out.println("抓老鼠");
  29.     }
  30. }
复制代码
结果:



虽然说Cat和Dog类没有定义这两个方法,但是因为其继承了Animal方法,自动拥有了sleep()和eat()类,所以调用完全不成问题。
字段也一样会被继承过来:
  1. class InheritanceDemo{
  2.     public static void main(String[] args){
  3.         System.out.println(new Dog().x);
  4.         new Cat().eat();
  5.     }
  6. }
  7. class Animal{
  8.     int x = 5;
  9.     public void sleep(){
  10.         System.out.println("睡觉");
  11.     }
  12.     public void eat(){
  13.         System.out.println("吃");
  14.     }
  15. }

  16. class Dog extends Animal{
  17.     //特有行为
  18.     public void lookAfterHouse(){
  19.         System.out.println("看家");
  20.     }
  21.     public void bark(){
  22.         System.out.println("汪");
  23.     }
  24.    
  25. }

  26. class Cat extends Animal{
  27.     //特有行为
  28.     public void catchMouse(){
  29.         System.out.println("抓老鼠");
  30.     }
  31. }
复制代码
结果:



Dog类中并没有定义字段x,而是从Animal类中继承过来的。因此可以通过Dog实例访问这个字段。

本章小结
  • 两个或多个有相似行为的类可以被提取成一个父类,令其子类继承父类及自动拥有父类有的方法和字段
  • 通过extends关键字实现继承
  • 继承可以提高代码的复用性和弹性



5.12 继承的成员特点



5.12.1 继承中的字段和方法

如果子类和父类的方法或者字段名称冲突,若为父类实例执行父类的字段/方法;如果子类实例执行子类的字段/方法。相当于子类把父类的字段/方法覆盖了。

  1. class InheritanceConflict{
  2.     public static void main(String[] args){
  3.         SuperClass superclass = new SuperClass();        //实例化父类
  4.         System.out.println(superclass.num);                //打印父类的num字段
  5.         superclass.print();                                //调用父类的print()方法
  6.         
  7.         SubClass subclass = new SubClass();                //实例化子类
  8.         System.out.println(subclass.num);                //打印子类的num字段
  9.         subclass.print();                                //调用子类的print()方法
  10.     }
  11. }

  12. class SuperClass{
  13.     int num = 7;
  14.     void print(){
  15.         System.out.println("我是父类");
  16.     }
  17. }

  18. class SubClass extends SuperClass{
  19.     int num = 8;
  20.     void print(){
  21.         System.out.println("我是子类");
  22.     }
  23. }
复制代码
结果:



在这个小实例当中,可以发现如果通过父类实例调用字段/方法则执行父类的;如果通过子类实例调用字段/方法则执行子类的。

这种机制叫做复写。在开发当中,永远永远不要去改源代码。应当是通过写一个子类继承原本的类来复写其的字段/方法。因为如果你直接在原本的类当中改代码,其他的类会受到牵连。

而且,有的时候子类虽然继承了父类,但是执行的结果是不一样的。比如说鸟类都可以飞,企鹅是鸟的子类,但是它就不能飞。我们可以在企鹅类当中复写鸟类的飞方法,已达到重写。

  1. class OverrideDemo{
  2.     public static void main(String[] args){
  3.         Penguin p = new Penguin();
  4.         p.fly();
  5.     }
  6. }

  7. class Bird{
  8.     void fly(){
  9.         System.out.println("飞");
  10.     }
  11. }

  12. class Penguin extends Bird{
  13.     void fly(){
  14.         System.out.println("我不能飞");
  15.     }
  16. }
复制代码
结果:



注意:如果子类要重写父类的方法,返回值、方法名和参数列表必须完全一样。
不然编译器会认为你是在子类中建立一个特有的方法,而非复写。而且,复写的方法的访问权限必须大于等于被复写的方法。

5.12.2 构造方法

我们都知道,要先有父类才有子类。比如说要先有父亲才会有儿子。所以说在Java当中,创建一个子类对象时,会先调用父类的构造方法,然后再调用自己的构造方法。

  1. class InheritanceConstructor{
  2.     public static void main(String[] args){
  3.         new SubClass();
  4.     }
  5. }

  6. class SuperClass{
  7.     SuperClass(){
  8.         System.out.println("父类构造方法执行");
  9.     }
  10. }

  11. class SubClass extends SuperClass{
  12.     SubClass(){
  13.         System.out.println("子类构造方法执行");
  14.     }
  15. }
复制代码
结果:



本章小结
  • 父类对象调用父类字段/方法;子类对象调用子类字段/方法
  • 子类对父类的成员的更改叫做重写
  • 创建子类对象时先调用父类构造方法,然后调用本身的构造方法



5.13 super



5.13.1 super的使用

我们在上一章中学到了子类对父类的字段/方法复写。复写之后如果我想要调用父类的未被复写的方法/字段是没有办法的。Java为此给我们提供一个叫做super的关键字。

super用于调用父类的方法、字段和构造方法。
语法如下:
  1. super.字段
  2. super.方法
  3. super(父类构造方法参数)  //调用父类的构造方法
复制代码
注:super()必须是构造方法中的第一个语句。

实例如下:
  1. class SuperDemo{
  2.     public static void main(String[] args){
  3.         new SubClass().show();
  4.     }
  5. }

  6. class SuperClass{
  7.     int num = 10;
  8. }

  9. class SubClass extends SuperClass{
  10.     int num = 5;
  11.    
  12.     void show(){
  13.         System.out.println(super.num);
  14.     }
  15. }
复制代码
结果:



可以看出来,super.num调用的不是SubClass中的字段num,而是SuperClass中的字段num。

super一样可以调用方法:
  1. class SuperDemo{
  2.     public static void main(String[] args){
  3.         new SubClass().print();
  4.     }
  5. }

  6. class SuperClass{
  7.     int num = 10;
  8.     void show(){
  9.         System.out.println("我是父类");
  10.     }
  11. }

  12. class SubClass extends SuperClass{
  13.     int num = 5;
  14.    
  15.     void show(){
  16.         System.out.println("我是子类");
  17.     }
  18.     void print(){
  19.         super.show();
  20.     }
  21. }
复制代码
结果:



虽然子类复写了show()方法,但是print方法中通过super调用了父类的show()方法。所以打印出来的是“我是父类”。

  1. class SuperDemo{
  2.     public static void main(String[] args){
  3.         new SubClass();
  4.     }
  5. }

  6. class SuperClass{
  7.     SuperClass(){
  8.         System.out.println("父类构造方法");
  9.     }
  10. }

  11. class SubClass extends SuperClass{
  12.     SubClass(){
  13.         super();
  14.         System.out.println("子类构造方法");
  15.     }
  16. }
复制代码
结果:



我们已经讲过了,子类中的构造方法是先调用父类的构造方法的。所以说其实在SubClass类的构造方法中的第一句加入了一个super()。如果你不写这个super(),依旧会给你写上一个。

本章小结
  • super用于调用父类的字段、方法、构造方法
  • 格式如下:super.字段、super.方法、super()



5.14 用final修饰类和方法



5.14.1  继承机制造成的问题

继承的复写特征为程序带来了许多益处。不过这也带来了一个安全隐患:你写一个很重要的方法,别人一复写,你这个方法就可以被别人随意的更改。

  1. class FinalDemo{
  2.     public static void main(String[] args){
  3.         new SubClass().aMethod();
  4.     }
  5. }

  6. class SuperClass{
  7.     void aMethod(){
  8.         System.out.println("这是一个重要的方法");
  9.     }
  10. }

  11. class SubClass extends SuperClass{
  12.     void aMethod(){        //重写aMethod()
  13.         System.out.println("呵呵");
  14.     }
  15. }
复制代码
结果:



在这个例子当中,SuperClass中的aMethod()方法可以被子类复写。如果aMethod()被认为很重要,不应该被复写,怎么办呢?

所以说可以从中得出结论:继承性是对安全性的一个挑战。
我们要解决的方式就是不让一个类被继承或不让一个方法被复写。

5.14.2 通过final修饰类

final关键字不仅仅可以修饰变量,也可以修饰类和方法。
被final修饰的类不能被继承。

  1. class FinalDemo{
  2.     public static void main(String[] args){
  3.         new SubClass().aMethod();
  4.     }
  5. }

  6. final class SuperClass{
  7.     void aMethod(){
  8.         System.out.println("这是一个重要的方法");
  9.     }
  10. }

  11. class SubClass extends SuperClass{
  12.     void aMethod(){        //重写aMethod()
  13.         System.out.println("呵呵");
  14.     }
  15. }
复制代码
结果:



在这个实例当中,SuperClass被声明为final。当SubClass继承了SuperClass时,会抛出错误。
所以说final类会强制其不被继承。如果连子类都没有,我怎么复写呢?这样一来,安全性被提高了。

5.14.3 用final修饰方法

再上一节当中,被final修饰的类不能被继承。但是我希望一个类里面有一些方法可以被复写,有一些不可以。解决方法是通过final修饰方法。
当一个方法被修饰为final时,其不能被复写。

  1. class FinalDemo{
  2.     public static void main(String[] args){
  3.         SubClass sc = new SubClass();
  4.         sc.aMethod();
  5.         sc.anotherMethod();
  6.     }
  7. }

  8. class SuperClass{
  9.     final void aMethod(){
  10.         System.out.println("这是一个重要的方法");
  11.     }
  12.    
  13.     void anotherMethod(){
  14.         System.out.println("这是另一个方法");
  15.     }
  16. }

  17. class SubClass extends SuperClass{
  18.     void aMethod(){        //重写aMethod()
  19.         System.out.println("呵呵");
  20.     }
  21.    
  22.     void anotherMethod(){
  23.         System.out.println("呵呵");
  24.     }
  25. }
复制代码
结果:



在SuperClass类中,定义了一个final方法aMethod(),以及一个非final方法anotherMethod()。当SubClass继承SuperClass之后对两个方法都进行重写,anotherMethod()是没事的,不过由于aMethod()是final方法,对其进行复写会抛出错误。

本章小结
  • final除了可以修饰变量外,可以修饰方法和类
  • 继承带来了安全隐患,随时随地都可以对方法进行复写
  • 被final声明的类不能被继承
  • 被final声明的方法不能被复写



5.15 抽象类



5.15.1 抽象类概述

假如说有一个玩家类,玩家类有两个子类:一个生存玩家,一个红石玩家。两个子类都有玩的方法,但是玩的内容具体不一样。这该如何在父类中定义呢?

可以在父类中写一个play()方法,方法体内什么代码都没有。但是这样做子类不复写也行,所以说有一定问题的。

为了解决“有这个方法但是内容不一样”的问题,Java为我们提供抽象机制。

5.15.2  定义抽象方法和类

抽象方法通过修饰符声明,修饰符为abstract。
格式如下:
  1. 其他修饰符 abstract 方法名(参数列表);
复制代码
要注意,最后方法是通过分号结尾的。
有抽象方法的类也必须被abstract修饰。
  1. 其他修饰符 abstract 类名{
  2. //代码
  3. }
复制代码
abstract类不能创建对象。因为我如果创建对象调用抽象方法是没有意义的。

好的,现在我们会定义抽象方法和类了,我们来解决刚才玩家类的问题:
  1. class AbstractDemo{
  2.     public static void main(String[] args){
  3.         new SurvivalPlayer().play();
  4.         new RedstonePlayer().play();
  5.     }
  6. }

  7. abstract class Player{
  8.     abstract void play();
  9. }

  10. class SurvivalPlayer extends Player{
  11.     void play(){            //复写抽象方法play()
  12.         System.out.println("玩生存模式");
  13.     }
  14. }

  15. class RedstonePlayer extends Player{
  16.     void play(){            //复写抽象方法play()
  17.         System.out.println("玩红石");
  18.     }
  19. }
复制代码
结果:



在这个实例中,Player类被声明为abstract,其中有abstract方法play()。SurvivalPlayer和RedstonePlayer分别继承Player类并复写play()方法。

如果一个类没有复写其父类的所有抽象方法,子类成为抽象类。这样就可以“逼着”程序员必须复写抽象方法,子类必须复写,问题就解决了。

本章小结
  • 抽象机制用于解决“子类都有这个方法但是内容不一样”的问题
  • 抽象方法和抽象类通过abstract修饰
  • 子类必须复写其父类的所有抽象方法,不然子类也成为抽象方法



5.16 接口



5.16.1 接口概述

大家对于接口的理解有可能只是“插槽”,但是并非这样片面。严格意义来讲,接口是插槽的规范。如果你的插槽是我的这个接口,你就得要遵循我的规范。

例如现实生活中的接口有PCI接口、AGP接口等。PCI接口是给插槽的规范。比方说这个插槽长度、宽度等属性,都由PCI接口定义。AGP接口是对显卡的规范。比方说AGP接口的显卡都有固定的工作频率等。

上述的两种接口大家不需要很深的了解。但是总结的来讲接口是为一类事物提供一个规范。

5.16.2  定义接口

在Java中,通过这样的语法定义接口:

  1. 若干个修饰符 interface 接口名{
  2.     //some codes
  3. }
复制代码
那么,interface里究竟该定义什么?其和类有一样,可以定义字段和方法。但是接口中的字段和方法有固定的修饰符

  1. public static final(字段的固定修饰符)
  2. public abstract(方法的固定修饰符)
复制代码
在定义字段/方法时,如果漏掉了若干个修饰符没有关系。会自动帮你补上。
而且千万注意,字段一定要被赋值过。因为既然接口提供的是一个规范,所以其的值一定要是固定的。

我们来定义一个接口,接口中有一个字段和抽象方法:
  1. interface MyInterface{
  2.     public static final int NUM = 10;
  3.     public abstract void print();
  4. }
复制代码
在这个小接口当中,NUM字段被赋值为10。而且其中定义了一个抽象方法print()。
好的,现在我的小接口定义完了。现在我想要让我的一类事物去用这个接口。这该怎么做呢?

5.16.3  类对接口的实现

类与类之间的关系叫做继承,类与接口之间的关系叫做实现。
实现的方式如下:
  1. 若干修饰符 class 类名 implements 接口1,接口2,接口n{
  2.    
  3. }
复制代码
注:对,虽说一个类只能继承一个类,但是多实现是可以的。

当一个类实现一个接口后,其必须复写其中的所有抽象方法。
  1. class InterfaceDemo {
  2.     public static void main(String[] args) {
  3.         new MyClass().print();
  4.     }
  5. }

  6. class MyClass implements MyInterface{
  7.     public void print(){
  8.         System.out.println("Hello, interface");
  9.     }
  10. }

  11. interface MyInterface{
  12.     public static final int NUM = 10;
  13.     public abstract void print();
  14. }
复制代码
结果:



在这个小例子当中,MyClass类实现了MyInterface接口。其复写了print()方法。在主方法中创建匿名对象调用MyClass类的print()方法,输出结果即为Hello, interface。

现在想必大家可能稍微理解了为什么接口是给予一个规范了。这是因为只要是实现了我这个接口的类,必须复写我的方法。我就为这个类提供了一个规范。但是只要是我的范围之内,你这个方法想怎么写都行,反正要复写就是了。

5.16.4  接口与接口的继承

接口与接口之间可以继承,还可以多继承。语法和类的继承差不多:
  1. 若干个修饰符 interface 接口名 extends 接口1,接口2,接口n{
  2.    
  3. }
复制代码

子接口自动获得到父接口中的所有字段和方法。所以说如果类中实现了有父接口的子接口,其也得要复写父接口中的所有方法。

  1. interface SubInterface extends SuperInterface{
  2.     void method1();
  3. }

  4. interface SuperInterface{
  5.     void method2();
  6. }

  7. class Demo implements SubInterface{
  8.     public void method1(){
  9.         System.out.println("method 1");
  10.     }
  11.     public void method2(){
  12.         System.out.println("method 2");
  13.     }
  14. }
复制代码
在这个例子当中,SubInteface继承了SuperInterface。SubInterface中定义了method1(),SuperInterface定义了method2()。由于Demo类实现了SubInterface且其继承了SuperInteface,其必须复写method1()和method2()。

接口中的字段照样可以复写。(方法当然也可以复写但是没有意义,反正也没有方法体)
  1. class InterfaceInheritanceDemo {
  2.     public static void main(String[] args) {
  3.         new Demo().method1();
  4.     }
  5. }

  6. interface SubInterface extends SuperInterface{
  7.     int NUM = 0;
  8.     void method1();
  9. }

  10. interface SuperInterface{
  11.     int NUM = 1;
  12. }

  13. class Demo implements SubInterface{
  14.     public void method1(){
  15.         System.out.println(SubInterface.NUM);
  16.     }
  17. }
复制代码
结果:



在这个例子当中,SuperInterface和SubInterface同时拥有NUM字段。在Demo类中复写method1()输出SubInterface中的NUM,在主方法中创建匿名对象调用method1(),结果为0。可以看出来,子接口中的NUM复写了父接口中的NUM。

本章小结
  • 接口对类提供规范
  • 类要用一个接口需要实现这个接口,通过implements关键字实现
  • 如果一个类实现一个接口,其必须复写其所有方法
  • 类可以多实现接口
  • 接口可以继承其他接口,子接口自动获得父接口中的方法
  • 接口可以多继承接口

(两天没更新了抱歉)



5.17 面向对象三特点:多态



5.17.1 多态概述

多态是面向对象编程中最后一个特征。在程序中运用多态可以提高程序的扩展性。

假如说狗类继承了动物类。此时大街上跑来一只小狗,你可以说“这只小狗好可爱”、或者说“这只动物好可爱”、或者更加丧心病狂“这个生物好可爱”,从逻辑层面来讲,都可以。可以说狗既具备狗的形态,也具备动物的形态,也具备生物的形态。

这就是多态一个简单的了解。多态的定义是一个类具有多个形态。

5.17.2 子类对象指向父类引用

我们以前在实例化类的时候,都是“类名 实例名 = new 类名()”。一般情况来说,两边的类名必须完全一致。但是根据多态,可以子类对象指向父类引用

  1. class PolymorphismDemo {
  2.     public static void main(String[] args) {
  3.         Animal a = new Dog();    //子类对象指向父类引用
  4.     }
  5. }

  6. class Animal{
  7.     void sleep(){
  8.         System.out.println("睡觉");
  9.     }
  10.     void eat(){
  11.         System.out.println("吃东西");
  12.     }
  13. }

  14. class Dog extends Animal{
  15.     void bark(){
  16.         System.out.println("汪");
  17.     }
  18. }
复制代码
请大家注意程序的第三行。“Animal a = new Dog()”,很明显,左边的类名是父类,右边的是子类。这样做是合法的。

那么这样做具体有什么用呢?请看下一节。

5.17.3 多态的具体应用

我们先定义一个体系:
  1. abstract class Animal{
  2.     void sleep(){
  3.         System.out.println("睡觉");
  4.     }
  5.     void eat(){
  6.         System.out.println("吃东西");
  7.     }
  8.     abstract void sound();
  9. }

  10. class Dog extends Animal{
  11.     void sound(){
  12.         System.out.println("汪");
  13.     }
  14. }

  15. class Cat extends Animal{
  16.     void sound(){
  17.         System.out.println("喵");
  18.     }
  19. }
复制代码
由于Animal类的子类的叫法都不一样,但是都得叫,所以把sound()方法定义为抽象。让Dog和Cat复写sound()方法。
现在,我们在主方法中新建Dog和Cat匿名对象调用sound()方法。

  1. class PolymorphismDemo {
  2.     public static void main(String[] args) {
  3.         new Dog().sound();
  4.         new Cat().sound();
  5.     }
  6. }

  7. abstract class Animal{
  8.     void sleep(){
  9.         System.out.println("睡觉");
  10.     }
  11.     void eat(){
  12.         System.out.println("吃东西");
  13.     }
  14.     abstract void sound();
  15. }

  16. class Dog extends Animal{
  17.     void sound(){
  18.         System.out.println("汪");
  19.     }
  20. }

  21. class Cat extends Animal{
  22.     void sound(){
  23.         System.out.println("喵");
  24.     }
  25. }
复制代码
结果:



因为Dog和Cat的sound()方法内容不一样,所以输出结果必定不同。
但是发现sound()方法被调用过多。为了提高复用性,定义letsHear()方法。

  1. class PolymorphismDemo {
  2.     public static void main(String[] args) {
  3.         letsHear(new Dog());
  4.         letsHear(new Cat());
  5.     }
  6.     public static void letsHear(Dog d){
  7.         d.sound();
  8.     }
  9.     public static void letsHear(Cat c){
  10.         c.sound();
  11.     }
  12. }
  13. //省略定义Animal、Dog、Cat类
复制代码
在这个例子当中,letsHear方法被定义两次重载。一个接收的是Dog类,一个接收的是Cat类。但是如果过了一段时间我有了仓鼠类,我是不是又得要定义另一个letsHear()方法来接收仓鼠类?这样做程序的扩展性很差

此时,多态的特性就可以使用了。
  1. class PolymorphismDemo {
  2.     public static void main(String[] args) {
  3.         letsHear(new Dog());
  4.         letsHear(new Cat());
  5.     }
  6.     public static void letsHear(Animal a){
  7.         a.sound();
  8.     }
  9. }
复制代码
结果:



在这里,letsHear方法接受的参数是Animal类!就算来了仓鼠,因为其也是Animal类的子类,letsHear()照样可以接收!这样程序的扩展性就被大大的提高了。

再传参数的时候,其实就是把方法参数中的new Dog()或new Cat()指向了Animal a实例。这就是我刚才说的“子类对象指向父类引用”的用法。

可以发现,在编译时期,letsHear()方法中的Animal a实例拥有Animal的特点;在运行时期可以使用Cat/Dog的方法。这个实例既具备Animal的特点,也具备Cat/Dog的特点。这就是多态。由于实例a在编译时期拥有Animal特点,Animal叫做a的编译时类型。Cat/Dog即为a的运行时类型。

5.17.4 接口的多态

父类实例可以指向子类对象。虽说接口不能被实例化,但是接口的实例也可以指向其实现类的对象。
  1. 接口 实例名 = new 实现类(构造方法参数);
复制代码
我们可以用这个接口的实例来调用实现类的成员。我们写一个示范:

  1. class InterfacePolymorphism {
  2.     public static void main(String[] args){
  3.         MyInterface m = new MyClass();           //接口多态
  4.         m.method();                                       //用接口的实例调用MyClass的method()方法
  5.     }
  6. }

  7. interface MyInterface{
  8.     void method();
  9. }

  10. class MyClass implements MyInterface{
  11.     public void method(){
  12.         System.out.println("method() runs");   //复写
  13.     }
  14. }
复制代码
结果:



可以看出来,虽说是接口的实例,但是真正调用的是MyClass的method()方法。

本章小结
  • 多态是面向对象编程的最后一个特征,是指一类的事物可以有多个形态
  • 子类对象可以指向父类实例
  • 在定义方法时,其要求的引用类型参数可以传入其子类的对象
  • “父类 实例名 = new 子类()”当中,父类是实例的编译时类型;子类是实例的运行时类型
  • 接口实例也可以指向实现类对象



5.18 向上转型以及向下转型



5.18.1 向上转型

上一小节我们讲述了多态的应用。这一节我们来学习多态的具体。
大家还记不记得第二章讲的数据转换?那是针对基本数据类型的。现在我们介绍引用数据类型的转换。


“父类 实例名 = new 子类()”是不是很像基本数据类型的隐式转换?就像是把short赋给int,永远不会溢出。在这个例子当中,是把子类的对象赋给了父类的实例。其实在这个过程中,系统自动帮你把子类转换成父类了。在继承树上,父类在子类上面,所以说这种转型叫做向上转型。



但是在上一节的例子中,sound()方法在Animal类中是抽象方法。那么Animal实例是怎么调用抽象方法的呢?
其实,在运行的时候,调用的是其的子类的sound(),而非父类的抽象方法sound(),不然肯定会报错。在编译的时候是侧重于父类的。简单的来说,编译看实例化的左边,运行看右边

5.18.2 向下转型

向下转型就会稍微有一些出错的可能。相当于基本数据类型中的显式转换。
我们先稍微修改一下我们之前定义过的体系。给Dog类和Cat类定义一个特有的方法:

  1. abstract class Animal{
  2.     void sleep(){
  3.         System.out.println("睡觉");
  4.     }
  5.     void eat(){
  6.         System.out.println("吃东西");
  7.     }
  8.     abstract void sound();
  9. }

  10. class Dog extends Animal{
  11.     void sound(){
  12.         System.out.println("汪");
  13.     }
  14.     void lookAfterHouse(){
  15.         System.out.println("看家");
  16.     }
  17. }

  18. class Cat extends Animal{
  19.     void sound(){
  20.         System.out.println("喵");
  21.     }
  22.     void catchMouse(){
  23.         System.out.println("抓老鼠");
  24.     }
  25. }
复制代码
Dog类中有特有方法lookAfterHouse(),Cat类有特有方法catchMouse()。
现在,我想要在letsHear()方法中不止调用sound()方法,还想要调用两个子类的特有方法。这里就涉及到向下转换。我们需要把方法中的Animal实例a强制转换成Cat/Dog:

  1. public static void main(String[] args) {
  2.         letsHear(new Dog());
  3. }
  4. public static void letsHear(Animal a){
  5.     a.sound();
  6.     Dog d = (Dog)a;
  7.     d.lookAfterHouse();
  8. }
复制代码
结果:



在本程序中的第六行,我把方法中的参数a强制转换成了Dog类。其语法和基本数据类型的强制转换一样。
转换之后,可以用Dog实例d来调用Dog类的特有方法lookAfterHouse()。

但是猫呢?如果我给里面传一个猫会怎么样?
  1. public static void main(String[] args) {
  2.     letsHear(new Cat());
  3. }
  4. public static void letsHear(Animal a){
  5.     a.sound();
  6.     Dog d = (Dog)a;
  7.     d.lookAfterHouse();
  8. }
复制代码
结果:



喵是正常被打印了。但是在强制转换的时候就不干了。因为方法体中是强制转换成狗类,而我传进去的参数是猫类。当然会报出错误。

如果可以先判断参数Animal a实例是哪一类的对象,是不是就好办了呢?上伪代码:

  1. public static void letsHear(Animal a){
  2.     a.sound();
  3.     if(a是Dog类的对象的引用){
  4.         Dog d = (Dog)a;
  5.     }
  6.     else{
  7.         Cat c = (Cat)a;
  8.     }
  9. }
复制代码

Java中的确给我们提供判断实例是哪个类的对象的运算符。我就先留一个悬念,下一节讲。

本章小结
  • 向上转型是将子类的对象转换成父类的引用。在使用多态的实例化方法时,系统会自动帮你转换
  • 向下转型是将父类强制转换成子类实例



5.19 instanceof



5.19.1 instanceof概述

instanceof是一个二元运算符。参与运算的是一个实例和一个类。简单的来说如果这个实例是这个类的实例,其会返回true;反之亦然。

我们在上一节中提到了,要对实例进行判断是哪个类的,然后才能向下转型,不然会报出ClassCastException。这节课我们先学习instanceof的用法,然后在把上一节中的问题解决了。

5.19.2 instanceof用法

insanceof语法如下:
  1. 实例 instanceof 类
复制代码
其返回的是布尔类型。
具体结果如下:
  • 如果实例就是这个类的实例,返回true
  • 如果实例是参与运算的类的父类的实例,返回false
  • 如果实例是参与运算的类的子类的实例,返回true
  • 如果参与运算的实例和类一点关系都没有,报错

注:在多态中,无论用实例和编译时还是运行时类型比较,都返回true。

上述第一种情况实例:

  1. <font size="3">class InstanceofDemo {
  2.     public static void main(String[] args) {
  3.         MyClass mc = new MyClass();
  4.         System.out.println(mc instanceof MyClass);
  5.     }
  6. }

  7. class MyClass{}</font>
复制代码
在这个例子当中,实例化了MyClass类。用实例mc与MyClass类进行instanceof比较,返回的必定是true。因为mc就是MyClass的实例。

第二种情况:
  1. public class InstanceofDemo {
  2.     public static void main(String[] args) {
  3.         SuperClass sc = new SuperClass();
  4.         System.out.println(sc instanceof SubClass);
  5.     }
  6. }

  7. class SuperClass{}
  8. class SubClass extends SuperClass{}
复制代码
SuperClass有子类SubClass。实例化SuperClass后,用其与SubClass做instanceof运算,符合第二种情况,返回false。

  1. public class InstanceofDemo {
  2.     public static void main(String[] args) {
  3.         SubClass sc = new SubClass();
  4.         System.out.println(sc instanceof SuperClass);
  5.     }
  6. }

  7. class SuperClass{}
  8. class SubClass extends SuperClass{}
复制代码
体系没有变化。这次是用子类的实例和父类进行instanceof运算。符合第三种情况,输出true。

  1. public class InstanceofDemo {
  2.     public static void main(String[] args) {
  3.         Class1 sc = new Class1();
  4.         System.out.println(sc instanceof Class2);
  5.     }
  6. }

  7. class Class1{}
  8. class Class2{}
复制代码

因为Class1和Class2没有任何关系,实例化Class1让其与Class2做instanceof运算,将会报错。

5.19.3  用instanceof解决多态向下转型问题

现在我们基本上会用instanceof运算符了。所以说我们可以用它来解决我们的问题。
我们的问题是什么来着?那就是要对letsHear()方法中的Animal类实例a判断其运行时类型。我们可以用instanceof解决:

  1. public static void letsHear(Animal a){
  2.     a.sound();
  3.     if(a instanceof Dog){
  4.         Dog d = (Dog)a;
  5.         d.lookAfterHouse();
  6.     }
  7.     else{
  8.         Cat c = (Cat)c;
  9.         c.catchMouse();
  10.     }
  11. }

  12. public static void main(String[] args){
  13.     letsHear(new Dog());
  14.     letsHear(new Cat());
  15. }
复制代码
结果:



好的,这个问题被我们圆满的解决了!以后我们在向下转型时,一定要对实例进行instanceof运算,以避免ClassCastException。

5.19.4 用instanceof判断实例的类是否实现一个接口

instanceof也可以用来判断一个实例的类是否实现了一个接口。显而易见,如果实现了,返回true;反之亦然。格式也一样:

  1. 实例 instanceof 接口
复制代码
我们写一个小程序:

  1. class InstanceOfDemo{
  2.     public static void main(String[] args){
  3.         MyClass mc = new MyClass();        //实例化MyClass
  4.         System.out.println(mc instanceof MyInterface);        //判断实例mc的类,也就是MyClass是否实现了MyInterface接口
  5.     }
  6. }

  7. class MyClass implements MyInterface{}

  8. interface MyInterface{}
复制代码
结果:



本章小结
  • instanceof是一个运算符,一个实例和一个类会参与运算
  • 如果实例为这个类的实例,返回true
  • 在向下转型时要对实例进行instanceof运算来避免出错





[groupid=546]Command Block Logic[/groupid]