继承

1 类、超类及子类

1.1 定义子类

在java中,使用关键字extends进行继承,具体实现方式如下,这里Manager表示子类,Employee表示超类,下面所示关系是类Manager继承于类Employee

pubic Manager extends Employee
{
    
}

通过这种继承的方式,子类可以继承很多超类中允许继承的方法和变量,因此子类定义的对象能够直接使用这些方法和变量。

1.2 覆盖方法

当超类中的一些方法不能够满足子类的需求时,可以定义一个一样名字的方法,但是方法中实现内容有所不同,

1.3 子类构造器

同样是一个类,作为子类也是需要一个构造器进行构造对象,不过有一点需要明白的是,每次用子类创建对象的时,均会执行子类构造器之前先执行超类构造器,如此继续往上调用一直到Object的构造器,等Object构造完成之后,执行超类构造器,最后执行子类构造器。举个例子,如下

class Animal
{
    public Animal()
    {
        System.out.println("Making an animal");
    }
}

class Dog extends Animal
{
    public Dog()
    {
        System.out.println("Making a Dog");
    }
}

public class Test
{
    public static void main(String args[])
    {
        Dog a=new Dog();
    }
}

在上述例子中,当创建一个Dog类的对象时,首先调用Dog类的构造函数Dog(),并将其压入栈,接着Dog会调调用超类的构造函数,将超类的构造函数Animal()压入栈,最后会调用Object类的构造函数,并将其压入栈中;接下来就会依次按照先进后出的顺序将这些构造函数出栈,出一次栈就调用相应出栈函数,因此,会先调用超类的构造函数,其次再调用子类的构造函数。

在上面这段中说会调用超类的构造函数,这是怎么回事呢?子类的构造函数在执行的时候,首先均会调用超类的构造函数,具体可以分为两种:

  • 第一种,我们可以且必须在子类构造函数中的第一条语句,通过调用super()函数的形式调用超类的构造函数(可以是无参构造也可以是有参构造)
  • 第二种,如果不通过上述方式显式调用超类构造函数,系统会自动调用超类的无参构造函数。当然这种情况下一定需要超类有无参构造函数,如果没有无参构造函数系统就会报错

1.4 继承层次

在java中,支持一个超类具有多个子类,但是不支持一个子类继承于多个超类(这一点和C++有所不同)

1.5 多态

在java中,任何对象变量均是多态的,简单来说,就是一个超类的变量既可以引用超类对象也可以引用子类对象,相反,一个子类的变量不可以引用超类对象;如下述程序中的超类Animal与子类Dog,当超类定义的变量a引用子类对象时,这时候变量a是一个超类变量,所以会导致不能调用子类的特有函数bark(),如果想使用必须将变量a强制转换为子类;但是呢,变量a可以调用子类中覆盖超类中的函数,如下面程序中的函数make(),如果执行a.make(),则会自动调用子类中的make()函数,这正是多态神奇的地方!

  • 总结以上所说,多态的特点就是超类变量可以引用子类对象,但是在使用超类变量调用方法时,只能调用子类的对超类覆盖的方法,不能调用子类特有的方法
  • 如果要想一个超类变量能够使用子类特有的方法,可以将其强制转换为子类类型,如下面程序所示,将c[0]Animal类型强制转换为Dog之后(Dog e=(Dog)c[0]),就可以使用Dog类中的bark()方法了
class Animal
{
    public Animal()
    {
        System.out.println("Making an animal");
    }
    
    public void make()
    {
        System.out.println("Animal:make()");
    }
    
    public void eat()
    {
        System.out.println("Animal:eat()")
    }
    
}

class Dog extends Animal
{
    public Dog()
    {
        System.out.println("Making a Dog");
    }
    
    public void make()
    {
        System.out.println("Dog:make()");
    }
    
    public void bark()
    {
        System.out.println("Dog:bark()");
    }
    
    public void eat()
    {
        System.out.println("Dog:eat()")
    }  
}

class Cat extends Animal
{
    public Cat()
    {
        System.out.println("Making a cat");
    }
    
    
    
    public void eat()
    {
        System.out.println("Cat:eat()");
    }
        
}


public class Test
{
    public static void main(String args[])
    {
        Animal a=new Animal();//超类变量引用超类对象
        Animal b=new Dog();//超类变量引用子类对象
        a=b;//超类变量引用子类对象
        
        Animal[] c=new Animal[2];
        c[0]=new Dog();
        c[1]=new Cat();
        c[0].eat();//执行Dog类中的eat()函数
        c[0].bark();//不能执行,因为c[0]还只是一个Animal变量
        c[1].eat();//执行Cat类中的eat()函数
        Dog e=(Dog)c[0];
        e.bark();//将c[0]类型转换为Dog后就可以使用其特有的方法了
    }
}

1.6 理解方法调用

当如果出现类c继承于类b,类b继承于类a时,这时候具体的方法调用情况是如何的呢?举例说明。

class Animal
{
    void makenoise()
    {
        System.out.println("Animal:makenoise()");
    }
    
    void eat()
    {
        System.out.println("Animal:eat()");
    }
    
    void sleep()
    {
        System.out.println("Animal:sleep()");
    }
    
    void roam()
    {
        System.out.println("Animal:roam()");
    }
}

class Canine extends Animal
{
    void roam()
    {
        System.out.println("Canine:roam()");
    }
}

class Wolf extends Canine
{
    void makenoise()
    {
         System.out.println("Wolf:makenoise()");
    }
    
    void eat()
    {
         System.out.println("Wolf:eat()");
    }
}

public class Test
{
    public static void main(String args[])
    {
        Wolf a=new Wolf();
        a.makenoise();//调用Wolf类中的makenoise()函数
        a.roam();//调用Canine类中的roam()函数
        a.eat();//调用Wolf类中的eat()函数
        a.sleep();//调用Animal类中的sleep()函数
    }
}

上述例子表明,当出现多阶继承时,由于子类会继承来自超类的一些方法,所以会从继承级别的最低阶开始往最高阶寻找调用的方法,直到寻找到为止。

1.7 阻止继承

当我们想要使的自己写的类不允许被继承时,可以使用final对类进行声明,一旦类被声明了final,就表示该类不允许衍生出任何子类。如下所示。

public final class Excutive extends Manager
{
    
}
//声明final类的方式如上所示

此外,也可以对类中的某个具体方法声明为final,这样的方法具有什么含义呢?其含义就是,子类中不能覆盖该方法,并且需要说的时final类中的任何方法均自动的成为final方法,但是需要注意的是变量不会成为final

通过这种方法,可以知道,任何final类的引用一定是该final类对象,不可能是其子类对象。如java中的String类,其也是final类,因此String类的引用一定是String类本身的对象。

1.8 抽象类与方法

  • 抽象类是一种奇怪的类,为什么这么说呢?因为其不能被初始化,没有这种类的对象可以产生,简单来说,就是不能new出来。但是,还是可以将这种类作为引用类型,来引用一些子类对象。抽象类的定义如下。
abstract class Canine extends Animal
{
    
}

class Dog extends Canine
{
    
}

public static void main(String[] args)
{
    Canine a=new Canine();//编译错误,因为等号右边不能创建抽象类的实例对象
    Canine b=new Dog();//编译成功,虽然不能创建抽象类的对象,但是可以有抽象类的引用
}

含有大于一个抽象方法的类必须被标记为抽象的,抽象类中可以包含抽象方法和非抽象方法(抽象类中也可以不含有抽象方法);通过上面可以发现,其实抽象类除了继承之外没有其他任何用处。

  • 除了抽象类之外,还有抽象方法,抽象方法是没有实体的!如下所示是抽象类的定义方式。另外需要注意的是一旦声明了一个抽象方法,则必须同时将类标记为抽象类(不能在非抽象类中定义抽象方法)
public abstract void eat();

1.9 受保护访问

为了保护变量与方法,最好是将类中的方法或者变量声明为private,但是这样不利于在继承的时候使得子类利用超类的变量或者方法,所以为了既能使其他类不能使用超类中的一些方法或者变量,且又能使得在子类中能使用一些方法或者变量,可以将这些方法或者变量设置为protected类型,设置为protected类型的变量或者方法属于受保护的,其只能在子类中被访问,其他类不能使用。

2 Object:所有类的超类

在java中,Object类是所有类的超类,也就是说所有的类都是扩展而来的。在我们定义一个类的时候,如果没有没有明确指出其超类,那么这个类的超类默认就是Object类,但是通常会省略掉。因此可以使用Object类的变量引用任何类的变量。

public class Cat
{
    
}
//相当于
public class extends Object
{
    
}

其次,在java中只有基本类型不是对象,如数值、字符以及布尔类型的值这些均不是对象。

2.1 equals方法

在java中,Object类中提供了比较两个对象是否相等的方法,即equals(),该方法在检测时,如果两个对象具有相同的引用,则认为是相等的。

2.2 hashCode方法

hashCode称为散列码,其是由一个对象导出的整型值,并且这个值是对象的存储地址。

2.3 toString方法

Object类中,toString()可以有效返回对象值的字符串。

3 泛型数组列表

为了能够动态更改数组大小的问题,在java中定义了一个称为ArrayList的类,这种类能够实现动态的添加于删除数组中的元素。ArrayList类是一个采用类型参数的泛型类,使用方式如下。

ArrayList<Employee> staff=new ArrayList<>();

这种泛型类中具有很多操作方法,用于添加元素,删除元素等等。

  • add(int index,E obj),在指定位置添加元素
  • size(),返回数组中的元素个数
  • ensureCapacity(),用于确定数组的大小
  • 还可以和定义一般数组一样,定义泛型数组,如ArrayList<Employee> staff=new ArrayList<>(50),但是这里和一般的定义数组不同,这里定义的大小表明其具有存储50个数据的能力,在这些空间使用完后,还会继续分配空间使用;相反,一般的定义数组不同,定义多大的空间,只能使用这么大的空间,一旦超过了就不行
  • set(int index,E obj),用于设置指定位置的元素值,将覆盖原来的元素
  • E get(int index),将返回指定位置的元素值
  • E remove(int index),删除指定位置上的元素,后面的元素会向前移动一个位置

4 对象包装器与自动装箱

有时候我们想要将基本数据类型当做对象进行处理,因此为了实现这种转换。在java中,所有的基本数据类型均有个与之相对应的包装类,这些类也称为包装器。如下所示是基本数据类型的包装器

  • Boolean
  • Character
  • Byte
  • Short
  • Integer
  • Long
  • Float
  • Double

如下述所示,包装与解开包装

Integer data=new Integer(99);//将改值99进行包装
int d=data.intValue();//解开包装,获得d等于99
  • 在自动包装出现之前,基本数据类型与包装器是是严格区分开的,如下面所示
ArrayList array=new ArrayList();
array.add(new Integer(999));//必须将999转换为Object类型
Integer one=(Integer)array.get(0);//取出的时候必须将其转换为Integer
int oneint=one.intValue();//将Integer转换为int

有了自动包装之后,就没有必要按照上面严格讲数据与类型区分开,如下所示

ArrayList<Integer> listnum=new ArrayList<Integer>();
listnum.add(1999);//int数据自动转换为Integer
int listNumber=listnum.get(0);//Integer自动转换为int
  • 在上面程序中,listnum.add(1999)这条语句将自动转换为listnum.add(Integer.valueOf(1999)),这就叫做自动装箱
  • 在上面程序中,listnum.get(0)这条语句将自动转换为listnum.get(0).intValue(),这就叫做自动拆箱

此外还有一些包装类中还有一些静态方法,如parseInt()parseDouble()等等,这些方法可以将字符串转换为int或者double类型,如下面所示

String	str="9";
int strNum=Integer.parseInt(str);
String str1="123.45";
double strNum1=Double.parseDouble(str1);

反过来,我们也可以将int或者double转换为字符串类型,如下面所示

double dou0=123.45;
String str0=""+dou0;//第一种
String str1=Double.toString(dou0);//第二种使用Double类中的静态成员方法