• 5.1 类、超类和子类


      

    5.1 类、超类和子类

     

      

      子类比超类拥有的功能更加丰富。

      在本例中,Manager类比超类Employee封装了更多的数据,拥有更多的功能。

      在Manager类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的方法:

    class Manager extends Employee
    {
        private double bonus;
        ...
        public void setBouns(double b)
        {
            bonus = b;    
        }
    }
    

      如果有一个Manager对象,就可以使用setBonus方法。 

    Manager boss = ... ;
    boss.setBonus(5000);
    

      由于setBonus方法不是在Employee类中的定义,所以属于Employee类的对象不能使用它。

      从超类中还继承了name、salary和hireDay这3个域,所以现在就有了4个域(由于增加了bonus域)。

      在通过扩展超类定义子类的时候,仅需要指出子类和超类的不同之处,因此,会将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放在超类的方法,在面向对象程序设计中十分普遍。

      超类中的有些方法在子类Manager中不一定适用,比如说Manager中的getSalary方法应该返回薪水和奖金的总和,为此需要提供一个新的方法来覆盖(override)超类中的这个方法。

    class Manager extends Employee
    {
        ...
        //这个方法是错误的
        public double getSalary()
        {
            return salary + bonus;
        }
        ...
    }
    

      由于Manager类的getSalary方法不能直接地访问超类的私有域。尽管每个Manager对象都拥有一个名为salary的域,但在Manager类的getSalary方法中并不能直接访问salary域。只有Employee类的方法才能够访问私有部分。如果Manager类的方法一定要访问私有域,就必须借助公共的接口,Employee类中的公有方法getSalary就是这个公共接口。

    public double getSalary()
        {
            //通过子类的getSalary方法不能获得超类的salary域
            //所以以下这种方式也是错误的
            double baseSalary = getSalary();
            return baseSalary + bonus;    
        }

      为了获取salary域,就必须要调用超类Employee中的getSalary方法,而不是当前类的这个方法。

      为了调用超类Employee中的getSalary方法,而不是当前类的getSalary,必须使用super关键字来解决这个问题:

    public double getSalary()
        {
            double baseSalary = super.getSalary();
            return baseSalary + bonus;        
        }
    

      在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。 

    public Manager(String n, double s, int year, int month, int day)
        {
            super(n, s, year, month, day);
            bonus = 0;
        }
    

      这里的super语句super(n, s, year, month, day)是“调用超类Employee中含的n、s、year、month和day参数的构造器”的简写方式。

      由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。

      综上,super的作用:

      1、调用超类的方法;

      2、调用超类的构造器。(调用构造器的语句只能作为另一个构造器的第一个语句出现)

      

      重新定义Manager对象的getSalary方法之后,奖金就会自动添加到经理的薪水中。

    Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
    boss.setBonus(50000);
    

      接下来定义一个包含3个雇员的数组,将经理和雇员的信息都放在数组中:

    Employee[] staff = new Employee[3];
    staff[0] = boss;
    staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
    staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
    
    for(Employee e:staff)
    {
        System.out,println(e.getName() + " " + e.getSalary());
    }
    

      这里staff[1]和staff[2]仅输出基本薪水,这是因为它们对应的是Employee对象,而staff[0]对应的是Manager对象,它的getSalary方法将奖金和基本薪水加在了一起。

      在这里将e声明为Employee类型,但实际上它既可以引用Employee类型的对象,也可以引用Manager类型的对象。当e引用Employee对象的时候,e.getSalary()调用的是Manager类中getSalary方法,当e引用Manager对象的时候,e.getSalary()调用的是Manger类中的getSalary方法。

      虚拟机知道e实际引用的对象类型,因此能够正确地调用相应的方法。

      一个对象变量可以指示多种实际类型的现象被称为多态(polmorphism)。在运行时能够自动选择调用哪个方法的现象称为动态绑定(dynamic binding)。

    Employee.java

    package inheritance;
    
    import java.util.Date;
    import java.util.GregorianCalendar;
    
    public class Employee {
    	private String name;
    	private double salary;
    	private Date hireDay;
    	
    	public Employee(String n, double s, int year, int month, int day)
    	{
    		name = n;
    		salary = s;
    		GregorianCalendar calender = new GregorianCalendar(year, month - 1, day);
    		hireDay = calender.getTime();
    	}
    	
    	public String getName()
    	{
    		return name;
    	}
    
    	public double getSalary()
    	{
    		return salary;
    	}
    	
    	public Date getHireDay()
    	{
    		return hireDay;
    	}
    	
    	public void raiseSalary(double byPercent)
    	{
    		double raise = salary * byPercent/10;
    		salary += raise;
    	}
    }
    

    Manager.java

    package inheritance;
    
    public class Manager extends Employee 
    {
    	private double bonus;
    	//其余的参数会从Employee继承来
    	public Manager(String n, double s, int year, int month, int day)
    	{
    		super(n, s, year, month, day);
    		bonus = 0;
    	}
    	
    	public double getSalary()
    	{
    		double baseSalary = super.getSalary();
    		return baseSalary + bonus;
    	}
    	
    	public void setBonus(double b)
    	{
    		bonus = b;
    	}
    }
    

    ManagerTest.java 

    package inheritance;
    
    public class MangerTest {
    	public static void main(String args[])
    	{
    		Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
    		boss.setBonus(5000);
    		
    		Employee[] staff = new Employee[3];
    		
    		staff[0] = boss;
    		staff[1] = new Employee("Harry Cracker", 50000, 1989, 10, 1);
    		staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
    		
    		for(Employee e:staff)
    		{
    			System.out.println("name = " + e.getName() + ",salary = " + e.getSalary());
    		}
    	}
    
    }
    

    5.1.1 继承层次

      Java是不允许“实现多继承”,简称不允许“多继承”。但是Java支持“声明多继承”——Java的接口的多继承——一个类可以实现多个接口(“继承”了多个接口上的方法声明),而一个接口可以继承多个接口(同样是“继承”了多个接口上的方法声明)。

    5.1.2 多态

      可以将一个子类的对象赋值给超类变量。

      对象变量是多态的,一个Employee变量既可以引用一个Employee对象,也可以引用一个Employee类的任何一个子类的对象(例如Manager类)。

    Manager boss = new Manager(...);
    Employee[] staff = new Employee[3];
    staff[0] = boss;
    

      在这个例子中,staff[0]和boss引用的是同一个对象,但编译器将staff[0]看成Employee对象。所以以下调用方式是正确的:

      boss.setBonus(5000);

      但下面这种调用方式是错误的:

      staff[0].setBonus(5000);

      这是因为staff[0]声明的类型是Employee,而setBonus方法是Manager类的方法,不是Employee类的方法。

      可以将一个子类变量赋给一个超类变量,但不能将一个超类变量赋给一个子类变量。

    5.1.3 动态绑定

      调用方法的过程可以分解为:

      (1)编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。有可能存在多个名字为f,但参数不一样的方法。例如可能存在方法f(int)和f(Stirng),编译器会一一列举所有C类中名为f的方法和其超类中访问属性为private且名为f的方法(超类的私有方法不可访问)。至此,编译器已获得所有可能被调用的候选方法。

      (2)编译器查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法这个过程被称为重载解析(overloading resolution)。例如,对于调用x.f("Hello")来说,编译器将会挑选f(String),而不是f(int)。

      方法签名:方法的名字和参数列表称为方法的签名。

      (3)如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们讲这种方法称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定

      (4)程序运行而且采用动态绑定调用方法的时候,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类,如果D类定义了一个方法f(String),就直接调用它,否则就在D类的超类中寻找f(String)方法,以此类推。

      每次调用方法的时候都要进行搜索,时间开销很大,所以虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法,当需要调用方法的时候,虚拟机查找这个表就可以了。

      对于之前的e.getSalary(),e声明为Employee类型,由于这个方法没有参数,所以不需要担心方法重载的问题。

    方法重载(method overload)的具体规范
    
    如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。
    
    一.方法名一定要相同。
    二.方法的参数表必须不同,包括参数的类型或个数,以此区分不同的方法体。
    1.如果参数个数不同,就不管它的参数类型了!
    2.如果参数个数相同,那么参数的类型或者参数的顺序必须不同。
    三.方法的返回类型、修饰符可以相同,也可不同。
    四.main方法也可以被重载
    方法重载的作用:可以一个相同的方法传入不同的参数以达到想要的结果
    

      所以,e.getSalary()执行的过程如下:

      (1)虚拟机提取e的实际类型的方法表,即可能是Employee、Manager的方法表,也可能是Employee类的其他子类的方法表;

      (2)虚拟机搜索定义getSalary签名的类,此时,虚拟机已经知道应该调用哪个方法;

      (3)虚拟机调用方法。

      动态绑定有一个非常重要的特征无需对现存的代码进行修改,就可以对程序进行扩展。

      假设现在增加了一个新类Excutive,并且e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个Excutive类的对象,就会自动调用Excutive.getSalary()方法。

    5.1.4 阻止继承 final类和方法

      有时候,可能希望组织人们利用某个类定义子类,不允许扩张的类称为final类,如果在定义类的时候使用了final修饰符就表示这个类是final类。

      将方法或类声明为final的主要目的是:确保它们不会在子类中改变语义。例如,Calender类中的getTime和setTime方法都声明为final。这表明Calender类的设计者负责实现Date类与日历状态之间的转换,而不允许子类处理这些问题。

    5.1.5 强制类型转换

      将一个类型强制转换成另外一个类型的过程被称为类型转换。

      

    double x = 3.45;
    int nx = (int)x;
    

     

     将表达式x的值转换为int类型,丢弃了小数部分。

      有时候我们需要将某个类的对象引用转换为另外一个类的对象引用,对象引用的类型转换只需要用括号将目标类名称括起来,并放置在需要转换的对象引用之前就可以了。

      

    Manager boss = (Manager) staff[0];
    

       

      将一个值存入变量的时候,编译器将检查是否允许该操作。一个子类的引用赋给一个超类变量,编译器是允许的,但是将一个超类变量的引用赋给一个子类变量,必须进行类型转换,这样才能够通过运行时的检查。

      运行上述程序的时候,java运行系统会报告错误,并差生一个ClassCaseException异常,如果没有捕获这个异常,程序就会终止。所以在进行类型转换的时候,先查看一下是否成功进行地转换,这个过程只需要使用instanceof运算符即可实现。

      例如:

    if(staff[1] instanceof Manager)
    {
        boss = (Manager)staff[1];
        ...
    }    
    

      如果这个类型转换不成功,编译器就不会通过这个转换。

      例如:

      Date c = (Date)staff[1];

      将会产生编译错误,因为Date不是Employee的子类。

    综上所述:

    •   只有在继承层次上才能进行类型转换;
    •   在将超类转换成子类之前,需要使用instanceof进行检查。

      实际上,通过类型转换调整对象的类型不是一种好方法,在我们的实际过程中,大多数情况下并不需要将Employee对象转换成Manager对象,两个类的对象都能够正确地调用getSalary方法,因为实现多态性的动态绑定机制能够自动地找到相应的方法。

      只有在使用Manager中特有的方法的时候才需要进行类型转换,例如setBonus方法,如果鉴于某种原因,发现需要通过Employee对象调用setBonus方法,那么就应该检查一下超类的设计是否合理,重新设计并添加setBonus方法才是最正确的选择。

      只要没有捕获ClassCaseException异常,程序就会终止,在一般情况下,尽量少用类型转换和instanceof运算符。

    5.1.6 抽象类

      位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看,祖先类更加通用,人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

      

        学生和雇员都有姓名属性,因此可以将getName方法放置在继承关系较高层次的通用超类中。

      现在,再增加一个getDescription方法,它可以返回对一个人的简短描述。例如:

      an employee with a salary of $50000

      a student majoring in computer science

      在Employee类和Student类中实现这个方法很容易,但是在Person类中,除了姓名之外,Person类一无所知。所以,在这里更好的方法就是使用abstract关键字,这样就完全不需要实现这个方法了。

      

    public abstract String getDescription();
    

      

      为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象类。

    abstract class Person
        {
            ...
            public abstract String getDescription();
        }   
    

      

        除了抽象方法之外,抽象类还可以包含具体数据和具体方法。例如,Person类还保存着姓名和一个返回姓名的具体方法。

    abstract class Person
    {
        private String name;
        
        public Person(String n)
        {
            name = n;
        }    
    
        public abstract String getDescription();
    
        public String getName()
        {
             return name;
        }     
    }
        
        
    

      

      抽象方法充当着“占位”的角色,它们的具体实现是在子类中,抽象扩展类可以有两种选择。一种是在子类中定义部分抽象方法或抽象方法也不定义,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样子类就不是抽象的了。

      类即使不包含抽象方法,也可以将类声明为抽象类。

      抽象类不能被实例化,也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。例如,由于Person是抽象类,表达式

    new Person("Vince Vu")
    

      这种方法是错的,但可以创建一个具体子类的对象。

      虽然抽象类不能实例化,但是可以引用非抽象子类的对象。例如,

    Person p = new Student("Vince Vu", "Economics");
    

      这里的p是一个抽象类Person的变量,引用的是一个非抽象类Student的实例。

      

      在之前定义的抽象类Person中有两个具体的方法:Person(String n)和String getName()方法。

    class Student extends Person
    {
        private String major;
       
        public    Student(String n, String m)
        {
            super(n);
            major = m;    
        }
    
        public String getDescription()
        {
            return "a student majoring in" + major;    
        }
    }
    

      在Student类中定义了一getDescription方法,而且由于继承来的方法也是非抽象的,所以,Student类中的方法全部都是非抽象的,这个类也不再是抽象类。

    Person类

    package class_5_4;
    
    public abstract class Person
    {
    	public abstract String getDescription();
    	private String name;
    	
    	public Person(String n)
    	{
    		name = n;
    	}
    	
    	public String getName()
    	{
    		return name;
    	}
    }
    

    Employee类

    package class_5_4;
    
    import java.util.Date;
    import java.util.GregorianCalendar;
    
    public class Employee extends Person
    {
    	private double salary;
    	private Date hireDay;
    	
    	public Employee(String n, double s, int year, int month, int day)
    	{
    		super(n);
    		salary = s;
    		GregorianCalendar calender = new GregorianCalendar(year, month - 1, day);
    		hireDay = calender.getTime();	
    	}
    	
    	public double salary()
    	{
    		return salary;
    	}
    	
    	public Date getHireDay()
    	{
    		return hireDay;
    	}
    	
    	public String getDescription()
    	{
    		return String.format("an employee with a salary of %.2f", salary);
    	}
    	
    	public void raiseSalary(double byPercent)
    	{
    		double raise = salary*byPercent/100;
    		salary += raise;
    	}
    	
    }
    

    Student类

    package class_5_4;
    
    public class Student extends Person 
    {
    	private String major;
    	
    	public Student(String n, String m)
    	{
    		super(n);
    		major = m;
    	}
    	
    	public String getDescription()
    	{
    		return "a student majoring in " + major;
    	}
    	
    }
    

    PersonTest.java

     

    package class_5_4;
    
    public class PersonTest 
    {
    	public static void main(String args[])
    	{
    		Person[] people = new Person[2];
    		
    		people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
    		people[1] = new Student("Maria Morris", "computer");
    		
    		for(Person p:people)
    		{
    			System.out.println(p.getName() + "," + p.getDescription());
    		}
    	}
    }
    

    输出的结果为:

    Harry Hacker,an employee with a salary of 50000.00
    Maria Morris,a student majoring in computer
    

      

    能让一个男孩子热血的,不仅有梦想,还有姑娘。
  • 相关阅读:
    基础普及-Jar、War、Ear
    Guice 学习(五)多接口的实现( Many Interface Implementation)
    Foundation框架
    windowsclient开发--使用、屏蔽一些快捷键
    数据结构(Java语言)——BinaryHeap简单实现
    最小生成树之Prim(普里姆)算法
    LeetCode--Remove Element
    Java实现算法之--选择排序
    kvm云主机使用宿主机usb设备
    Oracle12c Client安装出现"[INS-30131]"错误“请确保当前用户具有访问临时位置所需的权限”解决办法之完整版
  • 原文地址:https://www.cnblogs.com/Mr24/p/6417150.html
Copyright © 2020-2023  润新知