Java基础教程之面向对象 · 第七讲
- 本节学习目标
- 1️⃣ 继承性
- 1.1 继承的限制
- 2️⃣ 覆写
- 2.1 方法的覆写
- 2.2 属性的覆盖
- 2.3 关键字 this与 super的区别
- 3️⃣ 继承案例
- 3.1 开发数组的父类
- 3.2 开发排序类
- 3.3 开发反转类
- 🌾 总结
本节学习目标
- 掌握继承性的主要作用、实现、使用限制;
- 掌握方法覆写的操作;
1️⃣ 继承性
继承是面向对象编程的第二个主要特性,它解决了代码重用的问题。通过继承,我们可以从现有类派生出新的子类,并且还可以在子类中添加额外的操作功能。
在我们详细讲解继承这一特性之前,通过按照之前所学知识对Person
类和 Student
类的开发,我们可以进一步分析一个问题。
// 范例 1: 定义两个描述人与学生的类—— Person.java
class Person{
private String name;
private int age;
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}
// 范例 1: 定义两个描述人与学生的类—— Student.java
class Student{
private String name;
private int age:
private String school;
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void setSchool(String school){
this.school = school;
}
public String getSchool(){
return this.school;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}
通过以上两段代码的比较,相信大家可以清楚地发现,如果按照之前学习的概念进行开发,程序中会出现大量重复代码。通过分析发现,从现实生活角度来讲,学生本来就属于人,但是学生所表示的范围要比人表示的范围更小,也更加具体。所以要想解决类似问题,只能依靠面向对象中的继承概念来完成。
继承性严格来讲就是指扩充一个类已有的功能。在Java 中,要实现继承关系可以使用如下语法:
class 子类 extends 父类{...}
对于继承的格式有以下说明:
- 对于
extends
而言,应该翻译为扩充,但是为了理解方便,统一将其称为继承; - 子类又被称为派生类;
- 父类又被称为超类 (
Super Class
)。
// 范例 2: 继承的基本实现
class Person{ //Person 类定义
private String name;
private int age;
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
}
class Student extends Person{ //Student 类继承了Person 类, 此类没有定义任何的操作方法
}
public class TestDemo {
public static void main(String args[]){
Student stu = new Student(); //实例化的是子类
stu.setName("张三");
stu.setAge(20);
System.out.println("姓名:"+ stu.getName()+", 年龄:" +stu.getAge());
}
}
程序运行结果:
姓名:张三, 年龄:20
通过上边范例代码可以发现,子类(Student
) 并没有定义任何操作,而在主类中使用的全部操作都是由 Person
类定义的,因此可以证明,子类即使不扩充父类,也属于维持功能的状态。
// 范例 3: 在子类中扩充方法
class Person{ //Person类定义
private String name;
private int age;
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public String getName(){
return this.name;
}
public int getAge(){
return this.age;
}
class Student extends Person{ //Student 类继承了Person 类
private String school; //子类扩充的属性
public void setSchool(String school){ //扩充的方法
this.school = school;
}
public String getSchool(){ //扩充的方法
return this.school;
}
}
public class TestDemo{
public static void main(String args[]){
Student stu = new Student(); //实例化的是子类
stu.setName("张三");
stu.setAge(20);
stu.setSchool("清华大学"); //Student类扩充方法
System.out.println("姓名:"+stu.getName()+",年龄:"+stu.getAge() +",学校:"+stu.getSchool());
}
}
程序运行结果:
姓名:张三,年龄:20,学校:清华大学
在此程序代码中,子类对父类的功能进行了扩充(扩充了一个属性和两个方法,如下图所示)。子类从外表上看是扩充了父类的功能,但是对于此程序的代码,子类还有一个特点,即子类实际上是将父类定义得更加具体化的一种手段。 父类表示的范围大,而子类表示的范围小。
1.1 继承的限制
虽然继承可以进行类功能的扩充,但是其在定义的时候也会存在若干种操作的限制。
限制一:Java 不允许多继承,但是允许多层继承。
在C++ 语言中具备一种概念——多继承,即一个子类可以同时继承多个父类。但是在Java 中是不允许存在多继承的。
// 范例 4: 错误的继承
class A {}
class B {}
class C extends A,B {} //一个子类继承了两个父类
此程序编写的目的是希望 C
类可以同时继承 A
和 B
两个类的操作,但是在Java 中是不允许存在多继承的,所以这样的代码在编译时不能通过。而之所以会存在多继承的概念实际上是因为希望一个子类可以同时继承多个父类的操作,而Java中存在接口、内部类等多种语法形式来支持,内部类前面已经介绍过,接口将在后面文章中介绍。
不允许多继承实际上也就代表着单继承的局限,这一操作是与 C++
语言彼此相反的部分,C++
允许多重继承。但是从面向对象、从现实的角度讲,一个人只能有一个父亲,这应该属于单继承的概念,而不能说一个人有多个亲生父亲,否则就违背逻辑了。
// 范例 5: 多层继承
class A {}
class B extends A {} //B 类继承A 类
class C extends B {) //C 类继承B 类
C
实际上属于(孙)子类,这样一来就相当于 B
类继承了 A
类的全部方法,而 C
类又继承了 A
和 B
类的方法,这种操作称为多层继承。所以 Java 中只允许多层继承,不允许多重继承, 也即Java 存在单继承局限。
需要注意的是,虽然 Java语言从自身讲并没有继承层数的限定,但从实际的开发角度讲,类之间的继承关系最好不要超过三层。也就是说开发人员所编写的代码如果出现了继承关系,三层就够了,如果太多层的继承关系会比较复杂。
限制二: 子类在继承父类时,严格来讲会继承父类中的全部操作,但是对于所有的私有操作属于隐式继承,而所有的非私有操作属于显式继承。
// 范例 6: 观察属性
class A{
private String msg;
public void setMsg(String msg){
this.msg = msg;
}
public String getMsg(){
return this.msg;
}
}
class B extends A{
}
public class Demo{
public static void main(String args[]){
B b = new B(); //继承自 A类
b.setMsg("Hello"); //设置msg 属性,属性通过A类继承
System.out.println(b.getMsg()); //通过子类对象取得msg 属性
}
}
程序执行结果:
Hello
通过程序可以发现,利用子类对象设置的 msg
属性可以正常取得,那么也就可以得出结论:在 B
类里面一定存在 msg
属性,并且此属性是通过父类继承而来的。
但是在 B
类里面不能针对 msg
属性进行直接访问,因为它在 A
类中属于私有声明,只能利用 setter
或 getter
方法间接地进行私有属性的访问。
限制三: 在子类对象构造前一定会默认调用父类的构造(默认使用无参构造),以保证父类的对象先实例化,子类对象后实例化。
// 范例 7: 观察实例化对象操作
class A{
public A(){ // 父类提供的无参构造方法
System.out.println("A 类的构造方法!");
}
}
class B extends A{ //B 是子类继承父类A
public B(){ //定义子类的构造方法
System.out.println("B 类的构造方法!");
}
}
public class Demo {
public static void main(String args[]){
new B(); //实例化子类对象
}
}
程序执行结果:
A 类的构造方法!
B 类的构造方法!
本程度虽然实例化的是子类对象,但是发现它会默认先执行父类构造,调用父类构造的方法体执行,再实例化子类对象并且调用子类的构造方法。而这时,对于子类的构造而言,就相当于隐含了 super()
的语句调用,由于 “super()
” 主要是调用父类的构造方法,所以必须放在子类构造方法的首行。
// 范例 8: 子类隐含语句
class B extends A{ //B 是子类继承父类A
public B(){ //定义子类的构造方法
super(); //父类中有无参构造时加与不加无区别,如果编写则必须出现在首行
System.out.println("B 类的构造方法!");
}
}
从此程序中可以发现,当父类中提供有无参构造方法时,是否编写 “super()
” 没有区别。但是如果父类中没有无参构造方法,则必须明确地使用 super()
调用父类指定参数的构造方法。
// 范例 9: 父类不提供无参构造方法
class A{
public A(String title){ // 父类提供的有参构造方法
System.out.println("A类的构造方法,title = "+ title);
}
}
class B extends A{ //定义子类B
public B(String title){ //子类提供有参构造
super(title); //明确调用父类构造,否则将出现编译错误
System.out.println("B类的构造方法!");
}
}
public class Demo{
public static void main(String args[]){
new B("Hello"); //实例化子类对象
}
}
程序执行结果:
A类的构造方法,title = Hello
B类的构造方法!
此程序在父类中由于没有提供无参构造方法,所以在子类中就必须明确地使用 super()
调用指定参数的构造方法,否则将出现语法错误。
下面我们思考一个问题,既然子类默认会自动调用父类构造方法,那么有没有可能性,不让子类去调用父类构造呢?
既然super()
和 this()
都是调用构造方法,而且都要放在构造方法的首行,那么如果 "this()
"出现了,则默认的"super()
"应该就不会出现,于是编写如下案例程序。
// 范例 10:疑问的程序
class A {
public A(String msg){ //父类无参构造
System.out.println("msg="+ msg);
}
}
class B extends A {
public B(String msg){ //子类构造
this("HELLO", 30); //调用本类构造,无法使用“super()”
}
public B(String msg, int age){ //子类构造
this(msg); //调用本类构造,无法使用“super()”
}
}
public class TestDemo {
public static void main(String args[]){
B b = new B("HELLO", 20); //实例化子类对象
}
}
运行发现此程序有编译错误。
在本程序中,子类 B
的每一个构造方法,都使用了this()
调用本类构造方法,这样就表示子类无法调用父类构造。而在之前讲解 this
关键字时强调过:如果一个类中有多个构造方法之间使用 this()
互相调用,那么至少要保留一个构造方法作为出口,而这个出口一定会去调用父类构造 。
从逻辑上说,我们每一个人都一定会有自己的父母,在正常情况下,父母一定都要比我们先出生。在程序中,实例化就表示对象的出生,所以子类出生前(实例化前)父类对象一定要先出生(默认调用父类构造,实例化父类对象)。也就是说,子类会默认调用父类的无参构造函数。如果父类没有无参构造函数,子类会无法正常实例化。
此时在某种程度上讲,有一个问题解释了一半—— 一个简单Java类为何一定要保留一个无参构造方法。而关于此问题的另外一部分解释在讲解完反射机制之后就可以得到答案。
2️⃣ 覆写
继承性的主要特征是子类可以根据父类已有的功能进行功能的扩展,但是在子类定义属性或方法时,有可能出现定义的属性或方法与父类同名的情况,这样的操作就称为覆写。
2.1 方法的覆写
当子类定义了和父类的方法名称、返回值类型、参数类型及个数完全相同的方法时,就称为方法的覆写。为了更好地帮助大家理解方法覆写的意义,下面编写两个案例程序进行说明。
// 范例 11: 没有实现方法覆写
class A{
public void fun(){ //在父类中定义的方法
System.out.println("A类中的fun()方法。");
}
}
class B extends A{ //定义子类,此时没有覆写任何方法
}
public class TestDemo{
public static void main(String args[]){
B b = new B(); //实例化子类对象
b.fun(); //调用fun0方法
}
}
程序执行结果:
A 类中的fun)方法。
此程序在定义子类 B
时没有定义任何方法,所以在主方法中利用 B
类的实例化对象调用的 fun()
方法是通过A
类继承而来的。
// 范例 12: 实现方法覆写
class A{
public void fun(){ //在父类中定义的方法
System.out.println("A类中的 fun(方法。");
}
}
class B extends A{ //定义子类,此时没有覆写任何方法
public void fun(){ //此处为覆写
System.out.println("B类中的fun()方法。");
}
}
public class TestDemo{
public static void main(String args[]){
B b= new B(); // 实例化子类对象
b.fun(); //调用fun()方法,此时方法被覆写,所以调用被覆写过的方法
}
}
程序执行结果:
B 类中的 fun(方法。
此程序在B
类中定义了一个与A
类完全一样的 fun()
方法,所以当实例化B
子类对象,调用 fun()
方法时,将不再执行父类的方法,而是直接调用已经被子类覆写过的方法。
一个类可能会产生多个子类,每个子类都可能会覆写父类中的方法,这样一个方法就会根据不同的子类有不同的实现效果。
// 范例 13: 定义更多的子类
class A{
public void fun(){ //在父类中定义的方法
System.out.println("A类中的 fun()方法。");
}
}
class B extends A{ //定义子类,此时没有覆写任何方法
public void fun(){ // 此处为覆写
System.out.println("B类中的fun()方法。");
}
}
class C extends A{
public void fun(){ // 此处为覆写
System.out.println("C类中的 fun()方法。");
}
}
public class TestDemo {
public static void main(String args[]){
B b = new B(); //实例化子类对象
b.fun(); //调用fun()方法,此时方法被覆写,所以调用被覆写过的方法
C c = new C(); //实例化子类对象
c.fun(); //调用fun()方法,此时方法被覆写所以调用被覆写过的方法
}
}
程序执行结果:
B类中的 fun()方法。
C类中的 fun()方法。
此程序为 A
类定义了两个子类:B
类 和 C
类,并且都覆写了A
类中的 fun()
方法,当实例化各自类对象并且调用 fun()
方法时,调用的一定是被覆写过的方法。
对于方法的覆写操作,记住一个原则:如果子类覆写了方法(如B
类中的 fun()
方法,或者可能有更多的子类也覆写了fun()
方法),并且实例化了子类对象(B b = new B();
)时,调用的一定是被覆写过的方法。
简单地讲,就是要注意以下覆写代码执行结果的分析要素。
(1)观察实例化的是哪个类;
(2)观察这个实例化的类里面调用的方法是否已经被覆写过,如果没被覆写过则调用父类的方法。
这一原则也与后续的对象多态性息息相关,所以大家应该认真领悟。
方法覆写的概念其实就是子类定义了与父类完全一样的方法,但是什么时候需要子类覆写方法,什么时候不需要呢?
答案就是当子类发现功能不足时可以考虑覆写。
如果现在发现父类中的方法名称功能不足(不适合本子类对象操作),但是又必须使用这个方法名称时,就需要采用覆写这一概念实现。
就好比假设我们有一个父类叫做"动物
",其中有一个方法叫做"移动
",现在我们需要创建一个子类叫做"狗
",并且希望它的移动方式不同于其他动物。在这种情况下,我们可以覆写(重写)父类的"move
"方法来实现不同的功能。通过在子类中覆写父类的"move
"方法,我们改变了狗对象的移动行为。通过覆写方法,我们可以根据子类的需求扩展或修改父类的功能。
需要注意的是,在覆写的过程中必须考虑到权限问题, 即:被子类所覆写的方法不能拥有比父类更严格的访问控制权限。
对于访问控制权限实际上我们已经接触了解过3种了,这3种权限由大到小的顺序是: public
> default
(默认,什么都不写) > private
, 也就是说 private
的访问权限是最严格的(只能被本类访问)。即如果父类的方法使用的是 public
声明,那么子类覆写此方法时只能是 public
; 如果父类的方法是 default
(默认),那么子类覆写方法时候只能使用 default
或 public
。
Java中一共分为4种访问权限,对于这些访问权限,后面文章中还会详细介绍。而且在实际的开发中,绝大多数情况下都使用 public
定义方法。
// 范例 14: 正确的覆写
class A{
public void fun(){ //在父类中定义的方法
System.out.println("A类中的 fun()方法。");
}
}
class B extends A{ //定义子类,此时没有覆写任何方法
public void fun(){ //此处为覆写.父类中的fun()方法权限为 public, 此时子类中的方法权限并没有变得严格,而是与父类一致
System.out.println("B类中的 fun()方法。");
}
}
public class TestDemo{
public static void main(String args[]){
B b = new B(); //实例化子类对象
b.fun(); //调用fun(方法,此时方法被覆写,所以调用被覆写过的方法
}
}
程序执行结果:
B类中的 fun()方法。
此程序的子类奉行着方法覆写的严格标准,父类中的 fun()
方法使用 public
访问权限,而子类中的 fun()
方法并没有将权限变得更加严格,依然使用 public
( 这也符合绝大多数方法都使用 public
定义的原则)。
// 范例 15: 正确的方法覆写
class A{
void fun(){ //在父类中定义的方法
System.out.println("A类中的 fun(方法。");
}
}
class B extends A{ // 定义子类,此时没有覆写任何方法
public void fun(){ // 此处为覆写. 父类中的fun()方法权限为 default,此时子类中的方法权限与父类相比更加宽松
System.out.println("B类中的fun(方法。");
}
}
此程序 A
类中的 fun()
方法使用了 default
权限声明,所以子类如果要覆写 fun()
方法只能使用 default
或 public
权限,在本程序中子类使用了public
权限进行覆写。
// 范例 16: 错误的覆写
class A{
public void fun(){
System.out.println("A类中的 fun()方法。");
}
}
class B extends A{
void fun(){ //此处不能覆写,因为权限更加严格
System.out.println("B类中的 fun()方法。");
}
}
此程序子类中的 fun()
方法使用了default
权限,相对于父类中的 public
权限更加严格,所以无法进行覆写。
需要注意的是,如果父类中方法使用了private
声明,则因 private
表示是私有的,既然是私有的就无法被外部看见,所以子类是不能覆写的。下面抛开 private
权限不看,先编写一个正常的覆写操作。
// 范例 17: 正常的覆写
class A{
public void fun(){
this.print(); //调用print()方法
}
public void print(){
System.out.println("更多文章请访问:...");
}
}
class B extends A{
public void print(){ // 覆写的是print()方法
System.out.println("更多文章请访问:https://blog.csdn.net/LVSONGTAO1225?type=blog");
}
}
public class TestDemo {
public static void main(String args[]){
B b = new B(); // 实例化子类对象
b.fun(); //调用父类继承来的fun()方法
}
}
程序执行结果:
更多课程请访问: https://blog.csdn.net/LVSONGTAO1225?type=blog
此程序子类成功覆写了父类中的 print()
方法,所以当利用子类对象调用 fun()
方法 时,里面调用的 print()
为子类覆写过的方法。下面将以上代码修改,使用 private
声明父类中的 print()
方法。
// 范例 18:使用 private 声明父类中的print()方法
class A {
public void fun(){
this.print(); //调用 print()方法
}
private void print(){ //此为private权限,无法覆写
System.out.println("更多课程请访问:...");
}
}
class B extends A {
public void print(){//不能覆写print(方法
System.out.println("更多课程请访问:https://blog.csdn.net/LVSONGTAO1225?type=blog");
}
}
public class TestDemo {
public static void main(String args[]){
B b = new B(); //实例化子类对象
b.fun(); //调用父类继承来的fun()方法
}
}
程序执行结果:
更多课程请访问:...
从概念上来讲,本程序子类符合覆写父类方法的要求。但是从本质上讲,由于父类 print()
方法使用的是 private
权限,所以此方法不能够被子类覆写,即对子类而言,就相当于定义了一个新的 print()
方法,而这一方法与父类方法无关。
大部分情况下开发者并不需要过多地关注 private
的限制,因为在实际的开发中,private
主要用于声明属性,而public
主要用于声明方法。
一旦有了覆写后会发现,默认情况下子类所能调用的一定是被覆写过的方法。为了能够明确地由子类调用父类中已经被覆写的方法,可以使用 super.方法()
来进行访问。
// 范例 19: 利用 super.方法()访问父类中方法
class A{
public void print(){
System.out.println("更多课程请访问:...");
}
}
class B extends A{
public void print(){ //覆写的是print()方法
super.print(); //访问父类中的 print()方法
System.out.println("更多课程请访问:https://blog.csdn.net/LVSONGTAO1225?type=blog");
}
}
public class TestDemo{
public static void main(String args[]){
B b = new B(); //实例化子类对象
b.print();
}
}
程序执行结果:
更多课程请访问:...
更多课程请访问:https://blog.csdn.net/LVSONGTAO1225?type=blog
此程序在子类的 print()
方法里编写了"super.print();
" 语句,表示在执行子类 print()
方法时会先调用父类中的 print()
方法,同时利用“super. 方法()
”的形式主要是子类调用父类指定方法的操作, super.方法()
可以放在子类方法的任意位置。
2.2 属性的覆盖
如果子类定义了和父类完全相同的属性名称时,就称为属性的覆盖。
// 范例 20: 观察属性覆盖
class A{
String info ="Hello";
}
//定义属性,暂不封装
class B extends A{
int info = 100; //名称相同,发生属性覆盖
public void print(){
System.out.println(super.info);
System.out.println(this.info);
}
}
public class TestDemo {
public static void main(String args[]){
B b = new B(); //实例化子类对象
b.print();
}
}
程序执行结果:
Hello
100
此程序在子类中定义了一个与父类同名的 info
属性,这样就发生了属性的覆盖,所以在子类中直接访问 info
属性时 (this.info
)会自动找到被覆盖的属性内容,也可以使用 “super.属性
”的形式调用父类中的指定属性 (super.info
)。
需要注意的是,属性覆盖的在实际开发中没有意义。因为在任何开发中,类中的属性必须使用 private
封装,那么一旦封装后属性覆盖是没有任何意义的,因为父类定义的私有属性子类根本就看不见,更不会互相影响了。
2.3 关键字 this与 super的区别
通过前面文章以及本文里的一系列介绍可以发现,this
与 super
在使用形式上很相似,下面进行两个关键字的操作对比。
- 功能上,
this
调用本类构造、本类方法、本类属性,super
用于子类调用父类构造、父类方法、父类属性; - 形式上,
this
先查找本类中是否存在有指定的调用结构,如果有则直接调用,如果没有则调用父类定义。super
则不查找子类,直接调用父类操作; - 特殊的是,
this
还直接表示了本类的当前对象。
在开发中,对于本类或父类中的操作,强烈建议加上"this.
“或者是"super.
” 的标记,这样的代码更加清晰、便于维护。
3️⃣ 继承案例
现在要求定义一个整型数组的操作类,数组的大小由外部决定,用户可以向数组中增加数据,以及 取得数组中的全部数据。随后在原本的数组上扩充指定的容量,另外,在此类上派生以下两个子类。
(1)排序类:取得的数组内容是经过排序出来的结果;
(2)反转类:取得的数组内容是反转出来的结果。
分析:本程序要求数组实现动态的内存分配,也就是说里面的数组的大小是由程序外部决定的,在本类的构造方法中应该为类中的数组进行初始化操作,之后每次增加数据时都应该判断数组的内容是否已经是满的,如果不满则可以向里面增加,如果满则不能增加。另外如果要增加数据时肯定需要有一个指向可以插入的下标,用于记录插入的位置,如下图所示。
在进行此程序开发的过程中,首先要完成的是定义父类,此时不需要考虑子类的实现问题。
3.1 开发数组的父类
要求定义一个数组操作类 (Array
类),在这个类里面可以进行整型数组的操作,由外部传入类可以操作数组的大小,并且要求实现数据的保存以及数据的输出操作。
// 范例 21: 基础实现
class Array{ //定义数组操作类
private int data[]; //定义一个数组对象,此数组由外部设置长度
private int foot; //表示数组的操作脚标
/**
* 开发父类
* 构造本类对象时需要设置大小,如果设置的长度小于0则维持一个大小
* @param len 数组开辟时的长度
*/
public Array(int len)(
if (len>0){ //设置的长度大于0
this.data = new int[len]; //开辟一个数组
}else{ //设置的长度小于等于0
this.data = new int[1]; //维持一个元素的大小
}
}
/**
* 向数组中增加元素
* @param num 要增加的数据
* @return 如果数据增加成功返回true,如果数组中保存数据已满则返回false
*/
public boolean add(int num){
if (this.foot < this.data.length){ //有空间保存
this.data[this.foot++] = num; //保存数据,修改脚标
return true; //保存成功
}
return false; //保存失败
}
/**
* 取得所有的数组内容
* @return 数组对象引用
*/
public int[] getData(){
return this.data;
}
}
public class TestDemo {
public static void main(String args[]){
Array arr = new Array(3); //实例化数组操作类对象,可操作数组长度为3
System.out.print(arr.add(20)+" 、"); //可以保存数据
System.out.print(arr.add(10)+" 、");
System.out.print(arr.add(30)+" 、");
System.out.println(arr.add(100)+" 、"); //不可以保存数据,返回false
int[] temp = arr.getData(); //取得全部数组数据
for (int x=0; x<temp.length; x++){ //循环输出数据
System.out.print(temp[x]+" 、");
}
}
}
程序执行结果:
true 、true 、true 、false、
20、10、30、
此程序首先定义了一个专门操作数组的 Array
类,然后在这个类对象实例化时必须传入要设置的数组大小,如果设置的数组大小小于等于0
, 则数组默认开辟一个空间的大小,即保证数组至少可以保存一个数据。最后使用 add()
方法向数组中保存数据,如果此时数组有空余空间,则可以进行保存,返回 true
, 如果没有空间则不能够保存,返回 false
。如果需要取出数组中全部保存的数据时,可以调用 getData()
方法取得。
范例 21的程序实际上只能算是一个最为基础的实现,因为本程序编写的目的并不是让大家去使用,而是要通过程序的设计帮助大家更好的理解继承。此程序的缺陷在于数据删除时的复杂处理,可以试想一下,如果数组有10
个元素,但是现在要将索引为3
、5
、7
的数据删除,就表示数组有空余空间,这个时候应该可以继续增加数据才对,但事实上这样的操作是非常复杂的。所以通过程序,也再一次提醒大家,数组的缺陷就在于长度固定,而这个问题可以利用链表或者后面文章将会介绍到的类集框架来解决。
对于上边范例 21的代码大家需要注意一个问题,主方法中进行数组操作时只使用了 add()
和 getData()
两个方法,而为了保持一致,即使 Array
类定义了其他子类(排序子类、反转子类), 也应该使用这两个方法增加、取得数据。类结构如下图所示。
3.2 开发排序类
排序的数组类只需要在进行数据取得时返回排序好的数据即可,而数据取得的操作方法是 getData()
, 也就是说如果要定义排序子类,只需要覆写父类中的 getData()
方法即可。
// 范例 22: 定义并测试排序子类
class SortArray extends Array{ //定义排序子类
public SortArray(int len){ // Array类里面没有无参构造方法
super(len); //明确调用父类的有参构造,为父类中的data属性初始化
}
/**
* 因为父类中getData()方法不能满足排序的操作要求,但为了保存这个方法名称,所以进行覆写
* 在本方法中要使用java.util.Arrays.sort()来实现数组的排序操作
*@return 排序后的数据
*/
public int[] getData(){
Arrays.sort(super.getData()); // 排序
return super.getData(); //返回排序后的数据
}
}
public class TestDemo{
public static void main(String args[]){
SortArray arr = new SortArray(3); //实例化数组操作类对象,可操作数组长度为3
System.out.print(arr.add(20)+" 、"); //可以保存数据
System.out.print(arr.add(10)+" 、");
System.out.print(arr.add(30)+" 、");
System.out.println(arr.add(100)+" 、"); //不可以保存数据,返回false
int[] temp = arr.getData(); //取得全部数组数据
for (int x=0; x<temp.length; x++){ //循环输出数据
System.out.print(temp[×]+" 、");
}
}
}
程序执行结果:
true 、true 、true 、false、
10、20、30、
此程序实例化的是 SortArray
子类,这样在使用 getData()
方法 (方法名称为父类定义,子类扩充) 时返回的数据就是排序后的数组,而整个程序的操作过程中,除了替换一个类名称外,与正常使用 Array
类没有区别。
3.3 开发反转类
反转类指的是在进行数组数据取得时,可以实现数据的首尾交换。在本类中依然需要针对父类的 getData()
方法进行覆写。
// 范例 23: 开发反转类并测试
class ReverseArray extends Array { //数组反转类
public ReverseArray(int len){ // Array 类里面没有无参构造方法
super(len); //调用父类有参构造
}
/**
*取得反转后的数组数据,在本方法中会将数据进行首尾交换
*@return 反转后的数据
*/
public int[] getData(){
int center = super.getData().length /2; //计算反转次数
int head = 0; //头部脚标
int tail = super.getData().length-1; //尾部脚标
for (int x=0; x<center; x++){ //反转
int temp = super.getData()[head]; //数据交换
super.getData()[head]= super.getData()[tail];
super.getData()[tail]= temp;
head++;
tail--;
}
return super.getData(); //返回反转后的数据
}
}
public class TestDemo{
public static void main(String args[]){
ReverseArray arr = new ReverseArray(3); //实例化数组操作类对象,可操作数组长度为3
System.out.print(arr.add(20)+" 、"); //可以保存数据
System.out.print(arr.add(10)+" 、");
System.out.print(arr.add(30)+" 、");
System.out.println(arr.add(100)+" 、"); //不可以保存数据,返回 false
int[] temp = arr.getData(); //取得全部数组数据
for (int x=0; x<temp.length; x++){ //循环输出数据
System.out.print(temp[x]+" 、");
}
}
}
程序执行结果:
true 、true 、true 、false、
30、10、20、
此程序在定义 ReverseArray
类时依然保存了 Array
类的 getData()
方法,并且在 ReverseArray
类中的 getData()
方法里进行了数组数据的反转操作。
🌾 总结
在本文中,我们深入探讨了Java中继承的概念以及与之相关的一些重要概念。我们首先了解了继承的基本原理,并学习了如何通过创建子类来实现继承。然后,我们详细介绍了继承的限制条件,包括单继承、构造方法和私有成员的访问限制等。
接下来,我们研究了覆写的概念,在子类中重新定义父类的方法或属性,从而改变其行为或值。我们了解到,方法覆写和属性覆盖是实现多态性的重要手段,能够提供灵活和可扩展的代码结构。
我们还特别介绍了关键字super
的使用,它使得在子类中能够访问父类的成员,尤其是当子类成员与父类成员同名时。通过super
关键字,我们可以调用父类的构造方法、访问父类的成员变量和方法,实现更加灵活的继承结构。
最后,我们通过一个继承案例进一步巩固了所学知识。通过这个案例,我们具体了解了如何创建继承关系,在子类中进行方法覆写和属性覆盖,以及如何利用super
关键字来访问父类成员。
继承是面向对象编程中非常重要的概念,它提供了代码重用、可扩展性和多态性等好处。但是,在应用继承时,我们需要注意其限制条件,并正确地使用覆写和super
关键字。通过深入学习和实践,我们可以充分发挥继承的优势,构建更加强大和灵活的程序结构。