简介:
- 面向对象的三个基本特征是:封装、继承和多态。
- 封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类)。封装和继承的目的都是:代码重用。而多态则是为了实现另一个目的 —— 接口重用!多态的作用,就是为了类在继承和派生的时候,保证使用“家谱”中任一类的实例的某一属性时的正确调用。
1. 封装
- 封装:就是隐藏对象的属性和实现细节,仅对外提供公共访问方式。关于封装的你只需要记住下面这张图即可
- 封装的意义
- 良好的封装能够减少耦合。
- 类内部的结构可以自由修改。
- 可以对成员进行更精确的控制。
- 隐藏信息,实现细节。
2. 继承
2.1 继承的基本概念
为了方便理解直接上代码
public class TestPersonStudentDemo {
public static void main(String[] args) {
Student s = new Student();
// 访问Person类中的name属性
s.name = "张三";
// 访问Person类中的age属性
s.age = 18;
// 访问Student类中的school属性
s.school = "哈佛大学";
System.out.println("姓名:" + s.name + ",年龄:" + s.age + ",学校:" + s.school);
}
}
class Person {
String name;
int age;
}
class Student extends Person {
String school;
}
输出结果:
姓名:张三,年龄:18,学校:哈佛大学
由上面的程序可以发现,在Student类中虽然并未定义name与age属性,但在程序外部却依然可以调用name或age,这是因为Student类直接继承自Person类,也就是说Student类直接继承了Person类中的属性,所以Student类的对象才可以访问到父类中的成员。
2.2 继承特性
- 子类拥有父类非private的属性和方法;
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展;
- 子类可以用自己的方式实现父类的方法;
- 在Java中只允许单继承,而不允许多重继承,也就是说一个子类只能有一个父类,但是Java中却允许多层继承,多层继承就是,例如类C继承类B,类B继承类A,所以按照关系就是类A是类B的父类,类B是类C的父类,这是Java继承区别于C++继承的一个特性;
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系)。
2.3 子类对象的实例化过程
为了方便理解直接上代码
public class TestPersonStudentDemo1 {
public static void main(String[] args) {
Student s = new Student();
}
}
class Person {
String name;
int age;
// 父类的构造方法
public Person() {
System.out.println("1.public Person(){}");
}
}
class Student extends Person {
String school;
// 子类的构造方法
public Student() {
System.out.println("2.public Student(){}");
}
}
输出结果:
1.public Person(){}
2.public Student(){}
从程序输出结果中可以发现,虽然程序第3行实例化的是子类的对象,但是程序却先去调用父类中的无参构造方法,之后再调用了子类本身的构造方法。所以由此可以得出结论,子类对象在实例化时会默认先去调用父类中的无参构造方法,之后再调用本类中的相应构造方法。
实际上,在子类构造方法的第一行默认隐含了一个super()语句,上面的程序如果改写成下面的形式,也是可以的:
class Student extends Person{
String school ;
// 子类的构造方法
public Student(){
super() ; //实际上在程序的这里隐含了这样一条语句
System.out.println("2.public Student(){}");
}
}
注意:继承条件下构造方法调用规则如下:
- 如果子类的构造方法中没有通过super显示调用父类的有参构造方法,也没有通过this显示调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法。在这种情况下写不写super()语句效果都是一样;
- 如果子类的构造方法中通过super显示调用父类的有参构造方法,那将执行父类相应构造方法,而不执行父类无参构造方法;
- 如果子类的构造方法中通过this显示调用自身的其他构造方法,在相应构造方法中应用以上两条规则;
- 特别注意的是,如果存在多级继承关系,在创建一个子类对象时,以上规则会多次向更高一级父类应用,一直到执行顶级父类Object类的无参构造方法为止。
3. super关键字的使用
super关键字的用法如下:
- super可以用来引用直接父类的实例变量。
- super可以用来调用直接父类方法。
- super()可以用于调用直接父类构造函数。
1. super用于引用直接父类实例变量
public class TestSuper1 {
public static void main(String args[]) {
Dog d = new Dog();
d.printColor();
}
}
class Animal {
String color = "white";
}
class Dog extends Animal {
String color = "black";
void printColor() {
System.out.println(color);// prints color of Dog class
System.out.println(super.color);// prints color of Animal class
}
}
输出结果:
black
white
在上面的例子中,Animal和Dog都有一个共同的属性:color。 如果我们打印color属性,它将默认打印当前类的颜色。要访问父属性,需要使用super关键字指定。
2. 通过super来调用父类方法
public class TestSuper2 {
public static void main(String args[]) {
Dog d = new Dog();
d.work();
}
}
class Animal {
void eat() {
System.out.println("eating...");
}
}
class Dog extends Animal {
void eat() {
System.out.println("eating bread...");
}
void bark() {
System.out.println("barking...");
}
void work() {
super.eat();
bark();
}
}
输出结果:
eating...
barking...
在上面的例子中,Animal和Dog两个类都有eat()方法,如果要调用Dog类中的eat()方法,它将默认调用Dog类的eat()方法,因为当前类的优先级比父类的高。所以要调用父类方法,需要使用super关键字指定。
3. 使用super来调用父类构造函数
public class TestSuper3 {
public static void main(String args[]) {
Dog d = new Dog();
}
}
class Animal {
Animal() {
System.out.println("animal is created");
}
}
class Dog extends Animal {
Dog() {
super();
System.out.println("dog is created");
}
}
输出结果:
animal is created
dog is created
注意:如果没有使用super()或this(),则super()在每个类构造函数中由编译器自动添加。
super与this关键字的比较:
- super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
- this关键字:指向自己的引用。
4. 方法的重写与重载
4.1 方法的重写(override)
1. 方法的重写(override)
子类从父类中继承方法,有时,子类需要修改父类中定义的方法的实现,这称做方法的重写(method overriding)。“重写”的概念与“重载”相似,它们均是Java“多态”的技术之一,所谓“重载”,即是方法名称相同,但却可在不同的场合做不同的事。当一个子类继承一父类,而子类中的方法与父类中的方法的名称、参数个数和类型都完全一致时,就称子类中的这个方法重写了父类中的方法。“重写”又称为“复写”、“覆盖”。
2. 如何使用重写
class Super {
访问权限 方法返回值类型 方法1(参数1) {
...
}
}
class Sub extends Super{
访问权限 方法返回值类型 方法1(参数1) —————>复写父类中的方法
{
...
}
}
注意:方法重写时必须遵循两个原则,否则编译器会指出程序出错。
- 重写的方法不能比被重写的方法有更严格的访问权限;
- 重写的方法不能比被重写的方法产生更多的异常(关于异常,在后面会介绍)。
4.2 方法的重载(overload)
1. 方法的重载
首先回顾一下前面所讲的方法的重载,方法重载是指多个方法可以享有相同的名字,但是参数的数量或类型不能完全相同。
调用方法时,编译器根据参数的个数和类型来决定当前所使用的方法。方法重载为程序的编写带来方便,是OOP多态性的具体变现。在Java系统的类库中,对许多重要的方法进行重载,为用户使用这些方法提供了方便。
2. 重载的规则
- 被重载的方法必须改变参数列表(参数个数或类型不一样);
- 被重载的方法可以改变返回类型;
- 被重载的方法可以改变访问修饰符;
- 被重载的方法可以声明新的或更广的检查异常;
- 方法能够在同一个类中或者在一个子类中被重载。
- 无法以返回值类型作为重载函数的区分标准。
4.3 重写与重载之间的区别
方法的重写和重载是Java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
- 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载;
- 方法重写是在子类存在方法与父类的方法的名字相同而且参数的个数与类型一样,返回值也一样的方法,就称为方法的重写;
- 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
5. 抽象类
5.1 抽象类的概念
前面对类的继承进行了初步的讲解。通过继承,可以从原有的类派生出新的类。原有的类称为基类或父类,而新的类则称为派生类或子类。通过这种机制,派生出的新的类不仅可以保留原有的类的功能,而且还可以拥有更多的功能。
除了上述的机制之外,Java也可以创建一种类专门用来当作父类,这种类称为“抽象类”。抽象类的作用有点类似“模版”,其目的是要设计者依据它的格式来修改并创建新的类。但是并不能直接由抽象类创建对象,只能通过抽象类派生出新的类,再由它来创建对象。
5.2 如何定义抽象类
抽象类的定义规则:
- 抽象类和抽象方法都必须用abstract关键字来修饰;
- 抽象类不能被实例化,也就是不能用new关键字去产生对象;
- 抽象方法只需声明,而不需实现;
- 含有抽象方法的类必须被声明为抽象类,抽象类的子类必须复写所有的抽象方法后才能被实例化,否则这个子类还是个抽象类。
抽象类的定义格式:
abstract class 类名称 // 定义抽象类
{
声明数据成员;
访问权限 返回值的数据类型 方法名称(参数...)//定义一般方法
{
...
}
abstract 返回值的数据类型 方法名称(参数...);
//定义抽象方法,在抽象方法里,没有定义方法体
}
注意:
- 在抽象类定义的语法中,方法的定义可分为两种:一种是一般的方法,它和先前介绍过的方法没有什么两样;另一种是“抽象方法”,它是以abstract关键字为开头的方法,此方法只声明了返回值的数据类型、方法名称与所需的参数,但没有定义方法体。
- 抽象类也可以像普通类一样,有构造方法、一般方法、属性,更重要的是还可以有一些抽象方法,留给子类去实现,而且在抽象类中声明构造方法后,在子类中必须明确调用。
6. final关键字的理解与使用
6.1 final关键字的使用
在Java中声明类、属性和方法时,可使用关键字final来修饰。
- final标记的类不能被继承;
- final标记的方法不能被子类复写;
- final标记的变量(成员变量或局部变量)即为常量,只能赋值一次。
6.2 final关键字修饰类、成员变量和成员方法
1.final类
final用来修饰一个类,意味着该类成为不能被继承的最终类。出于安全性的原因和效率上的考虑,有时候需要防止一个类被继承。例如,Java类库中的String类,它对编译器和解释器的正常运行有着很重要的作用,不能轻易改变它,因此把它修饰为final类,使它不能被继承,这就保证了String类的惟一性。同时,如果你认为一个类的定义己经很完美,不需要再生成它的子类,这时也应把它修饰为final类。
格式:
final class ClassName{
...
}
注意:声明为final的类隐含地声明了该类的所有方法为final方法。
2.final修饰成员变量
变量被声明为final后,成为常值变量(即常量),一旦被通过某种方式初始化或赋值,即不能再被修改。通常static与final一起使用来指定一个类常量。例如:
static final int SUNDAY=0;
把final变量用大写字母和下划线来表示,这是一种编码规定。
3.final修饰成员方法
用final修饰的方法为最终方法,不能再被子类重写,可以被重载。
尽管方法重写是Java非常有力的特征,但有时却需要避免这种情况的发生。为了不允许一个方法被重写,在方法的声明中指定final属性即可。例如:
class A{
final void method(){}
}
class B extends A{//定义A类的一个子类B
void method(){}//错误,method()不能被重写
}
方法被声明为final有时可以提高性能:编译器可以自由地内联调用它们,因为它“知道”它们不会被子类重写。
7. 接口
7.1 接口的定义
接口(interface)是Java所提供的另一种重要技术,它的结构和抽象类非常相似,也具有数据成员与抽象方法,但它与抽象类又有以下两点不同:
- 接口里的数据成员必须初始化,且数据成员均为常量;
- 接口里的方法必须全部声明为abstract,也就是说,接口不能像抽象类一样保有一般的方法,而必须全部是“抽象方法”。
接口定义的语法如下:
{
final 数据类型 成员名称 = 常量; //数据成员必须赋初值
abstract 返回值的数据类型 方法名称(参数...);
//抽象方法,注意在抽象方法里,没有定义方法主体
}
接口与一般类一样,本身也具有数据成员与方法,但数据成员一定要赋初值,且此值将不能再更改,方法也必须是“抽象方法”。也正因为方法必须是抽象方法,而没有一般的方法,所以抽象方法声明的关键字abstract是可以省略的。
相同的情况也发生在数据成员身上,因数据成员必须赋初值,且此值不能再被更改,所以声明数据成员的关键字final也可省略。
事实上只要记得:
- 接口里的“抽象方法”只要做声明即可,而不用定义其处理的方式;
- 数据成员必须赋初值。
7.2 接口的实现
在Java中接口是用于实现多继承的一种机制,也是Java设计中最重要的一个环节,每一个由接口实现的类必须在类内部复写接口中的抽象方法,且可自由地使用接口中的常量。
既然接口里只有抽象方法,它只要声明而不用定义处理方式,于是自然可以联想到接口也没有办法像一般类一样,再用它来创建对象。利用接口打造新的类的过程,称之为接口的实现(implementation)。
class 类名称 implements 接口A,接口B //接口的实现
{
...
}
7.3 接口的扩展
接口是Java实现多继承的一种机制,一个类只能继承一个父类,但如果需要一个类继承多个抽象方法的话,就明显无法实现,所以就出现了接口的概念。一个类只可以继承一个父类,但却可以实现多个接口。
接口与一般类一样,均可通过扩展的技术来派生出新的接口。原来的接口称为基本接口或父接口,派生出的接口称为派生接口或子接口。通过这种机制,派生接口不仅可以保留父接口的成员,同时也可加入新的成员以满足实际的需要。
同样的,接口的扩展(或继承)也是通过关键字extends来实现的。有趣的是,一个接口可以继承多个接口,这点与类的继承有所不同。
interface 子接口名称 extends 父接口1,父接口2...
{
...
}
8. 多态
8.1 多态的实现条件
多态的三个条件:
继承的存在(继承是多态的基础,没有继承就没有多态);
子类重写父类的方法(多态下调用子类重写的方法);
父类引用变量指向子类对象(子类到父类的类型转换)。
子类转换成父类时的规则:
将一个父类的引用指向一个子类的对象,称为向上转型(upcasting),自动进行类型转换。此时通过父类引用调用的方法是子类覆盖或继承父类的方法,不是父类的方法。 此时通过父类引用变量无法调用子类特有的方法;
如果父类要调用子类的特有方法就得将一个指向子类对象的父类引用赋给一个子类的引用,称为向下转型,此时必须进行强制类型转换。
下面是一个多态实例的演示,详细说明请看注释:
public class TestAnimalDemo {
public static void main(String[] args) {
show(new Cat()); // 以 Cat 对象调用 show 方法
show(new Dog()); // 以 Dog 对象调用 show 方法
Animal a = new Cat(); // 向上转型
a.eat(); // 调用的是 Cat 的 eat
Cat c = (Cat) a; // 向下转型
c.work(); // 调用的是 Cat 的 work
}
public static void show(Animal a) {
a.eat();
// 类型判断
if (a instanceof Cat) { // 猫做的事情
Cat c = (Cat) a;
c.work();
} else if (a instanceof Dog) { // 狗做的事情
Dog c = (Dog) a;
c.work();
}
}
}
abstract class Animal {
abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void work() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void work() {
System.out.println("看家");
}
}
输出结果:
吃鱼
抓老鼠
吃骨头
看家
吃鱼
抓老鼠
可以用 instanceof 判断一个类是否实现了某个接口,也可以用它来判断一个实例对象是否属于一个类。instanceof 的语法格式为:
对象 instanceof 类(或接口)
它的返回值是布尔型的,或真(true)、或假(false)。
8.2 在Java中有两种形式可以实现多态:继承和接口。
- 基于继承实现的多态
- 基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。
- 基于继承实现的多态可以总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。
- 如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异。这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。
- 基于接口实现的多态
- 继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可能是通过实现接口并覆盖接口中同一方法的几不同的类体现的。
- 在接口的多态中,指向接口的引用必须是指定实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
- 继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。