在Java语言中,继承的基本思想是可以从已有的类派生出新类。不同的类可能会有一些共同的特征和行为,可以将这些共同的特征和行为统一放在一个类中,使它们可以被其他类所共享。
例如,可以将人(person)定义为一个类,因为员工(employee)具有人的所有的特征和行为,则可以将员工类定义为人的子类,这就叫继承。
在类的层次结构中,被继承的类称为父类(parent class)或超类(super class),而继承得到的类称为子类(sub class)或派生类(derived class)。子类继承父类的状态和行为,同时也可以具有自己的特征。
public class a extends b {
//类体定义
}
在这里,a为子类,b为父类;
(1)子类继承父类中非private的成员变量和成员方法。例如,在Employee类中可以使用从父类继承来的name和age属性,还可以调用从父类继承来的方法,如sayHello()方法。子类还可以定义自己的成员变量和成员方法,如Employee类定义了一个表示工资的变量salary,还定义了computeSalary()方法。
(2)定义类时若缺省extends关键字,则所定义的类为java.lang.Object类的直接子类。在Java语言中,一切类都是Object类的直接或间接子类。例如,Person类是Object类的子类,也继承了Object类中定义的方法。Employee类、Person类和Object之间的类层次关系如图7-1所示。前面定义的所有类都是Object的子类。
(3)Java仅支持单重继承,即一个类至多只有一个直接父类。在Java中可以通过接口实现其他语言中的多重继承。
注意:
父类中定义的private成员变量和方法不能被子类继承,因此在子类中不能直接使用。如果父类中定义了公共的访问方法和修改方法,子类可以通过这些方法来访问或修改它们。
在子类中可以定义与父类中的名字、参数列表、返回值类型都相同的方法,这时子类的方法就叫作覆盖(overriding)或重写了父类的方法。
如果子类覆盖了父类的方法,在调用相同的方法时,调用的是子类的方法。
为了避免在覆盖方法时写错方法头,可以使用@Override注解语法,即在要覆盖的方法前面添加@Override。
@Override注解表示其后的方法必须是覆盖父类的一个方法。如果具有该注解的方法没有覆盖父类的方法,编译器将报告一个错误。例如,toString如果被错误地写成tosrting,将报告一个编译错误。如果没有使用注解,编译器不会报告错误。使用注解可以避免错误。
(1)private方法不能覆盖。只有非private的实例方法才可以覆盖,如果在子类中定义了一个方法在父类中是private的,则这两个方法无关。
(2)父类中static方法可以被继承,但不能被覆盖。如果子类中定义了与父类中的static方法完全一样的方法,那么父类中的方法被隐藏。父类中被隐藏的static方法仍然可以使用“类名.方法名()”形式调用。
方法重载是在一个类中定义多个名称相同但参数不同的方法。而方法覆盖是在子类中为父类中的同名方法提供一个不同的实现。要在子类中定义一个覆盖的方法,方法的参数和返回值类型都必须与父类中的方法相同。
public class Parent {
public void display(double i ){
System.out.println(i);
}
}
public class Child extends Parent{
public void display(double i ){
System.out.println(2*i);
}
}
public class Test {
public static void main(String[] args) {
Child child = new Child();
child.display(10.0);
child.display(10);
}
}
Parent类中定义了display()方法,Child类的display()与Parent类的display()参数和返回值类型都相同,是方法覆盖,但实现不同。Test类的main()方法中对Child类对象obj的display()方法的两次调用(参数类型不同)结果都为20.0,说明调用的都是Child类中覆盖的方法。
如果将Child类中display()方法的参数改为int i,再次执行程序,输出结果是20.0和10.0。这说明Child类中定义的display()方法不是对父类的方法覆盖,而是父类中继承来的display()方法的重载,因此当为display()方法传递一个double型参数时,将执行父类中的方法。
在子类中可以定义与父类中同名的成员变量,这时子类的成员变量会隐藏父类的成员变量。
子类不能继承父类的构造方法。要创建子类对象,需要使用默认构造方法或为子类定义构造方法。
4.1子类的构造方法:
Java语言规定,在创建子类对象时,必须先创建该类的所有父类对象。因此,在编写子类的构造方法时,必须保证它能够调用父类的构造方法。
在子类的构造方法中调用父类的构造方法有两种方式:
在任何情况下,创建一个类的实例时,将会沿着继承链调用所有父类的构造方法,这叫作构造方法链。下面代码定义了Vehicle类、Bicycle类和ElectricBicycle类,代码演示了子类和父类构造方法的调用。
封装性是面向对象的一个重要特征。在Java语言中,对象就是一组变量和方法的封装体。通过对象的封装,用户不必了解对象是如何实现的,只须通过对象提供的接口与对象进行交互就可以。封装性实现了模块化和信息隐藏,有利于程序的可移植性和对象的管理。
对象的封装是通过下面两种方式实现的。
(1)通过包实现封装性。在定义类时使用package语句指定类属于哪个包。包是Java语言最大的封装单位,定义了程序对类的访问权限。
(2)通过类或类的成员访问权限实现封装性。
类(包括接口和枚举等)的访问权限通过修饰符public实现,定义哪些类可以使用该类。public类可以被任何其他类使用,而缺省访问修饰符的类仅能被同一包中的类使用。
类成员的访问权限包括成员变量和成员方法的访问权限。共有4个修饰符,分别是private、缺省的、protected和public,这些修饰符控制成员可以在程序的哪些部分被访问。
用private修饰的成员称为私有成员,私有成员只能被这个类本身访问,外界不能访问。private修饰符最能体现对象的封装性,从而可以实现信息的隐藏。
缺省访问修饰符的成员,一般称为包可访问的。这样的成员可以被该类本身和同一个包中的类访问。其他包中的类不能访问这些成员。对于构造方法,如果没有加访问修饰符,也只能被同一个包的类产生实例。
当成员被声明为protected时,一般称为保护成员。该类成员可以被这个类本身、同一个包中的类以及该类的子类(包括同一个包以及不同包中的子类)访问。如果一个类有子类且子类可能处于不同的包中,为了使子类能直接访问父类的成员,那么应该将其声明为保护成员,而不应该声明为私有或默认的成员。
用public修饰的成员一般称为公共成员,公共成员可以被任何其他的类访问,但前提是类是可访问的。
如果一个类使用final修饰,则该类就为最终类(final class),最终类不能被继承。
定义为final的类隐含定义了其中的所有方法都是final的。因为类不能被继承,因此也就不能覆盖其中的方法。有时为了安全的考虑,防止类被继承,可以在类的定义时使用final修饰符。在Java类库中就有一些类声明为final类,如Math类和String类都是final类,它们都不能被继承。
如果一个方法使用final修饰,则该方法不能被子类覆盖。
class a{
public final void method(){}
}
class b extends a{
public void method(){}//该语句发生编译错误
}
用final修饰的变量包括类的成员变量、方法的局部变量和方法的参数。一个变量如果用final修饰,则该变量为常值变量,一旦赋值便不能改变。
对于类的成员变量一般使用static与final组合定义类常量。这种常量称为编译时常量,编译器可以将该常量值代入任何可能用到它的表达式中,这可以减轻运行时的负担。
如果使用final修饰方法的参数,则参数的值在方法体中只能被使用而不能被改变,
前面章节中定义的类可以创建对象,它们都是具体的类。在Java中,还可以定义抽象类。抽象类(abstract class)是包含抽象方法的类。
假设要开发一个图形绘制系统,需要定义圆类(Circle)、矩形类(Rectangle)和三角形类(Triangle)等,这些类都需要定义求周长和面积的方法,这些方法对不同的图形有不同的实现。这时就可以设计一个更一般的类,如几何形状类(Shape),在该类中定义求周长和面积的方法。由于Shape不是一个具体的形状,这些方法就不能实现,因此要定义为抽象方法(abstract method)。
定义抽象方法需要在方法前加上abstract修饰符。抽象方法只有方法的声明,没有方法的实现。包含抽象方法的类必须定义为抽象类,定义抽象类需要的类前加上abstract修饰符。下面定义的Shape类即为抽象类,其中定义了两个抽象方法。
在抽象类中可以定义非抽象的方法。可以创建抽象类的子类,抽象类的子类还可以是抽象类,只有非抽象的子类才能使用new创建该类的对象。抽象类中可以没有抽象方法,但仍然需要被子类继承,才能实例化。
注意:因为abstract类必须被继承而final类不能被继承,所以final和abstract不能在定义类时同时使用。
这里定义的Circle类继承了抽象类Shape类,由于Circle类不是抽象类,因此它必须实现抽象类中getArea()和getPerimeter()两个方法,此外,它还定义了构造方法和其他普通方法。
假设parent是一个父类型引用,child是一个子类型(直接或间接)引用,则下面的赋值语句是合法的:parent = child;//子类对象自动转换为父类对象。
这种转换称为向上转换(up casting)。向上转换指的是在类的层次结构图中,位于下方的类(或接口)对象都可以自动转换为位于上方的类(或接口)对象,但这种转换必须是直接或间接类(或接口)。
反过来,也可以将一个父类对象转换成子类对象,这时需要使用强制类型转换。强制类型转换需要使用转换运算符“()”。
instanceof运算符用来测试一个实例是否是某种类型的实例,这里的类型可以是类、抽象类、接口等。instanceof运算符的格式为:
该表达式返回逻辑值。如果variable是TypeName类型或父类型的实例,则返回true,否则返回false。
if (p instanceof Employee) {
Employee emp = (Employee) p;
// 进行后续操作
} else {
// 处理类型不匹配的情况
}
多态(polymorphism)就是多种形式,是指Java程序中一个类或多个类中可以定义多个同名方法,这多个同名方法完成的操作不同,这就是多态。多态性是指在运行时系统判断应该执行哪个方法的代码的能力。Java语言支持两种类型的多态:
(1)静态多态:也叫编译时多态,是通过方法重载实现的。
(2)动态多态:也叫运行时多态,是通过方法覆盖实现的。
将方法调用与方法体关联起来称方法绑定(binding)。若在程序执行前进行绑定,叫前期绑定,如C语言的函数调用都是前期绑定。若在程序运行时根据对象的类型进行绑定,则称后期绑定或动态绑定。Java中除static方法和final方法外都是后期绑定。
对重载的方法,Java运行时系统根据传递给方法的参数个数和类型确定调用哪个方法,而对覆盖的方法,运行时系统根据实例类型决定调用哪个方法。对子类的一个实例,如果子类覆盖了父类的方法,运行时系统调用子类的方法,如果子类继承了父类的方法,则运行时系统调用父类的方法。
有了方法的动态绑定,就可以编写只与基类交互的代码,并且这些代码对所有的子类都可以正确运行。假设抽象类Shape定义了getArea()方法,其子类Circle、Rectangle和Square都各自实现了getArea()方法。下面的例子说明了多态和方法动态绑定的概念。
在Java中,多态是面向对象编程的一个重要概念,它允许使用父类类型的引用来引用子类对象,并根据实际对象的类型来调用相应的方法。多态性使得我们可以编写更灵活、可扩展和可维护的代码。
多态性的实现依赖于两个关键概念:继承和方法重写。
继承:子类可以继承父类的属性和方法。子类可以扩展或重写父类的方法,也可以添加自己的方法。通过继承,子类可以具备父类的行为和特性。
方法重写:子类可以重写(覆盖)父类的方法,即在子类中重新定义与父类中具有相同名称和参数列表的方法。子类重写的方法可以具有不同的实现,但方法签名必须与父类方法相同。
多态性的实现方式有两种:编译时多态和运行时多态。
编译时多态:通过父类类型的引用指向子类对象,编译器在编译时只检查父类中定义的方法和属性,而不检查实际对象的类型。这意味着,通过父类引用只能调用父类中定义的方法和属性。
运行时多态:在运行时,Java虚拟机会根据实际对象的类型来调用相应的方法。即使使用父类类型的引用,如果实际对象是子类对象,那么调用的将是子类中重写的方法。这使得我们可以在不修改现有代码的情况下,通过替换实际对象来改变程序的行为。
多态性的优势在于它提高了代码的可扩展性和可维护性。通过使用多态,我们可以编写通用的代码,减少重复的代码,并且可以方便地添加新的子类来扩展程序的功能。此外,多态性还支持面向接口编程,使得代码更加灵活和可测试。
class Animal {
public void makeSound() {
System.out.println("Animal is making a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog is barking");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat is meowing");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound(); // 输出:Dog is barking
animal2.makeSound(); // 输出:Cat is meowing
}
}
在上面的示例中,我们定义了一个Animal
类作为父类,以及Dog
和Cat
类作为子类。父类Animal
有一个makeSound()
方法,子类Dog
和Cat
分别重写了这个方法。
在PolymorphismExample
类的main
方法中,我们创建了一个Animal
类型的引用animal1
,并将其指向一个Dog
对象。同样,我们创建了另一个Animal
类型的引用animal2
,并将其指向一个Cat
对象。
当我们调用animal1.makeSound()
时,由于animal1
引用指向的是Dog
对象,所以会调用Dog
类中重写的makeSound()
方法,输出"Dog is barking"。
同样地,当我们调用animal2.makeSound()
时,由于animal2
引用指向的是Cat
对象,所以会调用Cat
类中重写的makeSound()
方法,输出"Cat is meowing"。
这个示例展示了多态性的特性,即通过父类类型的引用调用子类对象的方法,根据实际对象的类型来决定调用哪个方法。这种灵活性使得我们可以编写更通用和可扩展的代码。