Java之路:类的封装、继承与多态

面向对象有三大特点:封装性、继承性和多态性。

一、封装

1、封装的含义

封装 (Encapsulation)是将描述某类事物的数据与处理这些数据的函数封装在一起,形成一个有机整体,称为类。

类所具有的封装性可使程序模块具有良好的独立性与可维护性,这对大型程序的开发是特别重要的。

类中的私有数据在类的外部不能直接使用,外部只能通过类的公共接口方法(函数)来处理类中的数据,从而使数据的安全性得到保证。

封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而仅需要通过外部接口,特定的访问权限来使用类的成员。

一旦设计好类,就可以实例化该类的对象。我们在形成一个对象的同时也界定了对象与外界的内外隔离。至于对象的属性、行为等实现的细节则被封装在对象的内部。外部的使用者和其他的对象只能经由原先规划好的接口和对象交互。

我们可用一个鸡蛋的三重构造来比拟一个对象,如下图所示。
在这里插入图片描述
属性(Attributes) 好比蛋黄,它隐藏于中心,不能直接接触,它代表的对象的状态(State)。
行为(Behaviors) 好比蛋白,它可以经由接口与外界交互而改变内部的属性值,并把这种改变通过接口呈现出来。
接口(Interface) 好比蛋壳,它可以与外界直接接触。外部也只能通过公开的接口方法来改变对象内部的属性(数据)值,从而使类中数据的安全性得到保证。

2、封装的实现

在Java中有四种访问权限:公有(public)、私有(private)、保护(protected)、默认(default)。但访问权限修饰符只有三种,因为默认访问权限没有访问权限修饰符

默认访问权限是包访问权限,即在没有任何修饰符的情况下定义的类,属性和方法在一个包内都是可访问的。

在这里插入图片描述
访问权限控制符是对类外而言的,而在同一类中,所有的类成员属性及方法都是相互可见的,也就是说,它们之间是可以相互访问的。

3、总结

(1)在Java中,最基本的封装单元是类

(2)类是基于面向对象思想编程语言的基础,程序员可以把具有相同业务性质的代码封装在一个类里,通过接口方法向外部代码提供服务,同时向外部代码屏蔽类里服务的具体实现方式。

(3)数据封装的最重要的目的是在于要实现“信息隐藏(Information Hidding)”。

(4)在类中的“数据成员(属性)”或者“方法成员”,可以使用关键字“public”、”private”、”protected”来设置各成员的访问权限。

(5)封装性是面向对象程序设计的原则之一。 它规定对象应对外部环境隐藏它们的内部工作方式。良好的封装可以提高代码的模块化程度,它防止了对象之间不良的相互影响。使程序达到强内聚(许多功能尽量在类的内部独立完成,不让外面干预),弱耦合(提供给外部尽量少的方法调用)的最终目标。

二、继承

1、继承的含义

对象(Object)是类(Class)的一个实例(Instance)。

如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。

继承性是面向对象的第二大特征。

继承(Inheritance)是面向对象程序设计中软件复用的关键技术,通过继承,可以进一步扩充新的特性,适应新的需求。这种可复用、可扩充技术在很大程度上降低了大型软件的开发难度,从而提高软件的开发效率。

当我们说某一个新类A继承某一既有类B时,表示这个新类A具有既有类B的所有成员,同时对既有类的成员做出修改,或是增加了新的成员。

保持已有类的特性而构造新类的过程称为继承。在已有类的基础上新增自己的特性而产生新类的过程称为派生。

我们把既有类称为基类(base class)、超类(super class)或者父类(parent class),而派生出的新类,称为派生类(derived class)或子类(subclass)。

继承可以使得子类自动具有父类的各种属性和方法,而不需要再次编写相同的代码,从而达到类的复用目的。

继承的目的在于实现代码重用,对已有的成熟的功能,子类从父类执行“拿来主义”。而派生的目的则在于,当新的问题出现时,原有代码无法解决(或不能完全解决)时,需要对原有代码进行全部(或部分)改造

2、继承的实现

在Java中,通过继承可以简化类的定义,扩展类的功能。在Java中支持类的单继承和多层继承,但是不支持多继承,即一个类只能继承一个类而不能继承多个类。
【语法】

class 子类名 extends 父类

extends 是Java中的关键词。Java继承只能直接继承父类中的公有属性和公有方法,而隐含的(不可见的)继承了私有属性。

3、继承的限制

(1)Java之中不允许多重继承,但是却可以使用多层继承。 一般情况下,在我们所编写的代码时,多层继承的层数之中不宜超过三层。

(2)从父类继承的私有成员,不能被子类直接使用。

子类在继承父类的时候会将父类之中的全部成员(包括属性及方法)继承下来,但是对于所有的非私有(private)成员属于显式继承,而对于所有的私有成员采用隐式继承(即对子类不可见)。 子类无法直接操作这些私有属性,必须通过设置Setter和Getter方法间接操作。

(3)子类在进行对象实例化时,从父类继承而来的数据成员需要先调用父类的构造方法来初始化,然后再用子类的构造方法来初始化本地的数据成员。

子类继承了父类的所有数据成员,同时子类也可以添加自己的数据成员。但是,需要注意的是,在调用构造方法实施数据成员初始化时,一定要“各司其职”,即来自父类的数据成员,需要调用父类的构造方法来初始化,而来自子类的数据成员初始化,要在本地构造方法中完成。

在调用次序上,子类的构造方法要遵循“长辈优先”的原则,先调用父类的构造方法(生成父类对象),然后再调用子类的构造方法(生成子类对象)。

(4)被final修饰的类不能再被继承。 final在Java之中称为终结器。通过在类的前面添加final关键字便可以阻止该类被继承。

4、构造方法调用顺序

首先介绍下默认构造器:
默认构造器,又叫无参构造器,是没有形式参数的构造器,它的作用是创建一个“默认对象”。

如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。 如下:

【示例1】:调用编译器创建默认构造器

package person;
class Person {}
class Student extends Person {	
	Student() {
		System.out.println("Student() constructor!");
	}
}
public class TestPerson {
	public static void main(String[] args) {
		Student s1 = new Student();
	}
}

【结果】
在这里插入图片描述
子类Student在进行对象实例化时,会先调用父类Person的构造方法,然而,在父类Person中没有提供构造器,则在Student对象s1调用构造方法Student()时,编译器会自动地创建一个默认构造器。

但是,如果在父类中自己定义了构造器,那么就调用自己定义的,如下:

【示例2】:调用自定义默认构造器

package person;
class Person {
	Person() {	// 自己定义的默认构造器
		System.out.println("Person() constructor!");
	}
}
class Student extends Person {	
	Student() {
		System.out.println("Student() constructor!");
	}
}
public class TestPerson {
	public static void main(String[] args) {
		Student s1 = new Student();
	}
}

【结果】
在这里插入图片描述
由此可以证明子类对象在实例化时会默认先去调用父类中的无参构造方法,之后再调用本类中的相应构造方法。

构造器还可以显示调用:

【示例3】:显示调用默认构造器

package person;
class Person {
	Person() {	// 自己定义的默认构造器
		System.out.println("Person() constructor!");
	}
}
class Student extends Person {	
	Student() {
		super();	// 显示调用默认构造器
		/* 如果用super显示地调用构造器,
		 * 那么它必须出现在这个子类构造方法中的第一行语句。
		 * 否则,会报错
		 */
		System.out.println("Student() constructor!");
	}
}
public class TestPerson {
	public static void main(String[] args) {
		Student s1 = new Student();
	}
}

【结果】
在这里插入图片描述
注:如果显式用super()去调用父类的构造方法,那么它必须出现在这个子类构造方法中的第一行语句,否则编译不通过。

5、super的使用

从英文本意来说,它表示“超级的”,从继承体系上,父类相对于子类是“超级的”,故此,有时候我们也称父类为超类(super-class)。

【示例4】:用super显示调用父类构造器

class Person {
	String name;
	int age;
	Person(String name, int age) {	// 有参构造
		System.out.println("Person(String name, int age) constructor!");
		this.name = name;
		this.age = age;
	}
}
class Student extends Person {
	String school;
	Student(String school, String name, int age) {
		super(name, age);	// 显示调用父类构造器
		System.out.println("Student(String school, String name, int age) constructor!");
		this.school = school;
	}
	
	public void print() {
		System.out.println("name : " + name);
		System.out.println("age : " + age);
		System.out.println("school : " + school);
	}
}
public class TestPerson {
	public static void main(String[] args) {
		Student s = new Student("NJTech", "张三", 20);
		s.print();
	}
}

【结果】
在这里插入图片描述

事实上,super关键字不仅可用于调用父类中的构造方法,也可用于调用父类中的属性或方法,如下面的格式所示。

super.父类中的属性 ;
super.父类中的方法() ;

【示例5】:用super调用父类属性或方法

class Person {
	String name;
	int age;
	
	Person() {}
	
	public void talk() {
		System.out.println("我的名字是:" + this.name + "\n" + "我的年龄是:" + this.age);
	}
}
class Student extends Person {
	String school;
	Student(String school, String name, int age) {
		// 其实在此隐式地调用了super(); 
		super.name = name;	// 用super调用父类属性
		super.age = age;
		super.talk();	// 用super调用父类方法
		this.school = school;
	}
}
public class TestPerson {
	public static void main(String[] args) {
		Student s = new Student("NJTech", "张三", 20);
	}
}

【结果】
在这里插入图片描述

【注】
super 是相对于继承而言的。super 代表的是当前类的父类,而this 这是代表当前类。

如果父类的属性和方法的访问权限不是private(私有的),那么这些属性和方法在子类中是可视的,换句话说,这些属性和方法也可视为当前类所有的,那么用“this.”来访问也是理所当然的。

但如果父类的属性和方法是private的,那么就不能再用(super.)来访问父类中的属性和方法。如下:

class Person {
	private String name; // 私有类型
	private int age;
	
	Person() {}
	
	private void talk() {
		System.out.println("我的名字是:" + this.name + "\n" + "我的年龄是:" + this.age);
	}
}
class Student extends Person {
	String school;
	Student(String school, String name, int age) {
		// 其实在此隐式地调用了super(); 
		super.name = name;	// 用super调用父类私有属性会出现错误
		super.age = age;	// 用super调用父类私有属性会出现错误
		super.talk();	// 用super调用父类私有方法会出现错误
		this.school = school;
	}
}
public class TestPerson {
	public static void main(String[] args) {
		Student s = new Student("NJTech", "张三", 20);
	}
}

【结果】
加粗样式

三、多态

1、多态的含义

多态(Polymorphisn),从字面上理解,多态就是一种类型表现出多种状态。

这也是人类思维方式的一种直接模拟,可以利用多态的特征,用统一的标识来完成这些功能。在Java中,多态性分为两类:

(1)方法多态性,体现在方法的重载与覆写上。

方法的重载是指同一个方法名称,根据其传入的参数类型、个数和顺序的不同,所调用的方法体也不同,即同一个方法名称在一个类中有不同的功能实现。

方法的覆写是指父类之中的一个方法名称,在不同的子类有不同的功能实现,而后依据实例化子类的不同,同一个方法,可以完成不同的功能。

(2)对象多态性,体现在父、子对象之间的转型上。

在这个层面上,多态性是允许将父对象设置成为与一个或更多的子对象相等的技术,通过赋值之后,父对象就可以根据当前赋值给的不同子对象,以子对象的特性加以运作。

多态意味着相同的(父类)信息发送给不同的(子)对象,每个子对象表现出不同的形态。

父类对象依据被赋值的每个子类对象的类型,做出恰当的响应(即与对象具体类别相适应的反应),这就是对象多态性的关键思想。 同样的消息或接口在发送给不同的对象时,会产生多种形式的结果,这就是多态性本质。

多态中的一个核心概念就是,子类(派生类)对象可以视为父类(基类)对象。

父、子类间的转型分为向上转型或向下转型:

(1) 向上转型(Upcast)(自动转型)

父类 父类对象 = 子类实例。

将子类对象赋值给父类对象,这样将子类对象自动转换为父类对象。这种转换方式是安全的。但是向上转型会丢失精度。
例如,我们可以说鱼是动物,鸟是动物,马是动物。这种向上转型在多态中应用得很广泛。
(2)向下转型(Downcast)(强制转型)

子类 子类对象 = (子类) 父类对象;

将父类对象赋值给子类对象。这种转换方式是非安全的。必须强制转换。 例如,如果我们说动物是鱼,动物是鸟,动物是马,这类描述是不全面的。因此,在特定背景下如果需要父类对象转换为子类对象,就必须使用强制类型转换。 这种向下转型用的比较少。

【示例】

class Animal {}
class Fish extends Person {}
public class TestPerson {
	public static void main(String[] args) {
		Animal a = new Fish();	// 向上转型,安全
		Animal a1 = new Animal();
		Fish f = (Fish)a1;	// 向下转型,不安全	
		// 
	}
}

2、多态的实现

重载的表现形式就是调用一系列具有相同名称的方法,这些方法可根据传入参数的不同而得到不同的处理结果,这其实就是多态性的一种体现,这属于静态多态,即同一种接口,不同的实现方式。 这种多态是在代码编译阶段就确定下来的。

还有一种多态形式,在程序运行阶段才能体现出来,这种方式称为动态联编,也称为晚期联编(late bingding)。

当子类覆写了父类的同名方法时,如果用子类实例化父类对象,会发生向上类型转换,这时调用该方法时,会自动调用子类的方法,这是实现多态的基础。

【示例】

package ploy;
class Animal {
	public void eat() { System.out.println("Animal eat()"); }
	public void sleep() { System.out.println("Animal sleep()"); }
	public void move() { System.out.println("Animal move()"); }
	public void run() { System.out.println("Animal run()"); }
}
class Dog extends Animal {
	public void eat() { System.out.println("Dog eat()"); }
	public void sleep() { System.out.println("Dog sleep()"); }
	public void move() { System.out.println("Dog move()"); }
}
class Cat extends Animal {
	public void eat() { System.out.println("Cat eat()"); }
	public void sleep() { System.out.println("Cat sleep()"); }
	public void move() { System.out.println("Cat move()"); }
}
class Fish extends Animal {
	public void eat() { System.out.println("Fish eat()"); }
	public void sleep() { System.out.println("Fish sleep()"); }
	public void move() { System.out.println("Fish move()"); }
}
public class Poly {

	public static void main(String[] args) {
		
		Animal a1 = new Dog();
		a1.move();	
		a1.run();
		Animal a2 = new Cat();
		a2.move();  
		a2.run();
		Animal a3 = new Fish();
		a3.move();	
		a3.run();
	}
}

【结果】
在这里插入图片描述

四、继承与多态的关系

简单来说,继承是子类使用父类的方法,而多态则是父类使用子类的方法。 但更为确切来说,多态是父类使用被子类覆盖的同名方法,如果子类的方法是全新的,父类不存在同名的方法,则父类也不能使用子类自己独有的“个性化”方法。

注意:即使实施向上转型,父类对象所能够看见的方法依然还是本类之中所定义的方法(即被子类覆盖的方法)。如果子类扩充了一些新方法的话,那么父类对象是无法找到的。

五、隐藏

当子类覆写了父类的同名方法时,如果用子类实例化父类对象,会发生向上类型转换,这时调用该方法时,会自动调用子类的方法,这是实现多态的基础。

但是,在某些场景下,我们不希望父类的方法被子类方法覆写,即子类实例化后会调用父类的方法而不是子类的方法,这种情况下该怎么办?

这就需要用到另外一个概念——隐藏(Hide)。被关键词static修饰的静态方法是不能被覆盖的, Java就是利用这一个特性达到隐藏的效果。如下:

package ploy;
class Animal {
	public static void eat() { System.out.println("Animal eat()"); }
}
class Dog extends Animal {
	// 注意,如果父类方法是static,
	// 那么子类覆写的方法也必须是static的,否则编译不通过。
	public static void eat() { 	
		System.out.println("Dog eat()"); 
	}
}
public class Poly {

	public static void main(String[] args) {
		
		Animal a1 = new Dog();
		a1.eat();
		
		Animal.eat();
		Dog.eat();
		
	}
}

【结果】
在这里插入图片描述

从运行结果可以看出,这时调用的父类的方法,没有被子类所覆盖,这就是说父类“隐藏”了子类的同名方法。

而事实上,所有的静态方法都隶属于类,而非对象。所以,可以通过“类名.静态方法名”的方法来直接访问静态方法,“父类”与“子类”之间的方法就不会存在谁隐藏谁的问题。

猜你喜欢

转载自blog.csdn.net/qq_43555323/article/details/84788284