类在继承中的复用[转载]

本文介绍了面向对象编程中类的继承及复用方法,包括直接继承、调用父类方法和通过抽象方法实现逻辑关系复用。同时探讨了类的封闭性原则、合成关系以及.NET中的virtual关键字使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类在继承中的复用
面向对象编程的一个重要特征就是类的继承,通过 类的继承,子类可以获得
父类定义的公有功能特性。但 是实现复用的方法却有多种,通常利用类的继承来实现
功能复用的方法有三种:
直接继承父类的方法实现复用;
调用父类的方法构成自己的方法实现复用;
通过抽象方法来实现逻辑关系的复用;
这三种方法的根本原理是一致的,也就是利用类对象存在公有特性的特征,第 1
2 种方法是抽象出公有的功能特性,而第 3 种要更高级一些,它是抽象出公
有的逻辑关系特性。我们在实现和利用复用的时候,不管采用那种方法,都应该
遵循一个原则,叫做类的封闭性原则,这个原则有两个方面的含义:
完备性 : 一个类要完成一个独立的业务过程,该类的内部应该定义了这个业务
的整个过程,尽量不要在这个类定义了一些过程,而在另一个类中又定义了另外
一些过程。
透明性 : 一个类要提供一项功能给其他的类复用,该项功能对其使用者是透
明的,不但在实现上是透明的,而且在使用上也应是透明的。
前2种方式都比较简单,这边针对的种方式给个不错的例子
通过抽象方法来实现逻辑关系的复用能够较好地遵循类的封闭性原则。对车辆这个类做分析,可以发现任何车辆要行驶,都是由驱动带动轮子进行滚动向前行驶的,因此驱动带动轮子向前行驶是所有车辆公有的逻辑关系,因此比较好的方式是类在继承中能够继承这种共有的逻辑关系,而不是简单的轮子滚动的功能。这样我们可以定义一个车辆的抽象类,在车辆抽象类中定义了车辆行驶的方法,但是不同类型的车辆的行驶是不一样的(2轮或4轮回N轮驱动==),所以我们可以定义了两个方法,一个是公有的轮子滚动的方法,它实现了所有车辆的轮子滚动功能,另一个是驱动的抽象方法,它在车辆抽象类并不实现,因为抽象的车辆并不知道是哪种驱动。而在行驶方法中,先调驱动方法,然后调用滚动方法,这样就实现了前面所说的车辆行驶的逻辑关系。
代码如下:
public class abstract Vechile
{
  private void Roll
  {
   / / 实现滚动功能
   ......
   }
  / /
抽象的驱动方法
  protected abstract void Drive();
   / / 车辆的行驶逻辑方法
  public void Go()
  {
   / / 驱动
   Drive();
   // 滚动
   Roll();
  }
}
这样,在它的子类当中,就直接继承了行驶的方法,
但是在子类中必须实现驱动的方法,下面是汽车子类的定义:
public class Car extends Vechile
{
  //
实现父类的抽象的驱动方法
  public void Drive()
  {
   // 具体特有的驱动功能代码
   .......
  }
}
这样在汽车子类中,只需要关心它自己驱动功能的实现,而车辆是如何行驶,轮子是如何滚动都在车辆父
类中定义好了的,子类继承了这种逻辑关系,在子类中在这种复用方法当中,车辆行驶的业务全过程是在
车辆类中进行了完整的定义,要使用父类的公有功能轮子滚动方法对于子类是完全透明,在子类当中根本不会
出现对滚动方法的调用,轮子滚动的方法已经是父类的私有方法了。
如何调用这些功能呢?车辆类是一个抽象类,
是不能实例化,实际当中也是这样,并不存在一台抽象的车。但是我们在行驶车辆时,其实关注的是它的行驶功能,
因此我们可通过抽象的车辆类来调用具体子类车辆实例
Vechile vechiletest = new Car();
{
 / / 调用车辆行驶功能
 vechiletest.Go;
 .....
}
在调用过程中我们可以发现,使用抽象的父类定义了类变量,创建的的确是子类的实例,后面都是通过父
类的类变量进行调用。这样做有一个好处,如果车辆类型要改变了,只要把创建的实例改一下,不会影响后面
的调用代码。
在使用者的眼中,只有车辆类,具体如何行驶是由背后的具体实例来完成,这一点对于使用者是
透明的。
顺便分析一下功能的执行过程,首先调用父类的行驶方法,在父类行驶方法的执行中,发现驱动方法
是抽象方法,就会寻找该抽象方法的实现,这样就调用了子类驱动的实现方法,之后,再调用父类的轮子滚动
方法。这样就完成了整个行驶过程的调用。对于调用者来说,只有车辆的行驶方法。就好比坐着车子行驶,不
必知道车子是如何驱动的,轮子是如何滚动。
Arty:
面向对象中,我们一般通过对象的继承和合成来实现复用.
合成关系分为聚合关系和组合关系.聚合是整体/部分关系的抽象(has-a关系).组合关系是一种包含关系("contains-a"关系)部分和整体的生命周期是一样的.一个组合而成的新对象完全拥有对其组成部分的支配权,包括它们的创建和销毁等.
通过合成实现的复用,新对象的存取组成对象的唯一方法是通过组成对象的接口;有利于封装,因为新对象看不到组成对象的内部细节;依赖较少,不像继承中,派生类得依赖基类;这种复用可以在运行期内动态进行,新对象可以动态地引用与组成对象类型相同地对象.不过,这种复用造成系统需要管理的对象较多的局面.
我们在开发时应该优先考虑合成复用,而不是继承复用.
Timmy:
.Net 中还有一个比较好用的关键字virtual:

virtual 关键字用于修改方法或属性的声明,在这种情况下,方法或属性被称作虚拟成员。虚拟成员的实现可由派生类中的重写成员更改。
调用虚方法时,将为重写成员检查该对象的运行时类型。将调用大部分派生类中的该重写成员,如果没有派生类重写该成员,则它可能是原始成员。

示例
在该示例中,Dimensions 类包含 x 和 y 两个坐标和 Area() 虚方法。不同的形状类,如 Circle、Cylinder 和 Sphere 继承 Dimensions 类,并为每个图形计算表面积。每个派生类都有各自的 Area() 重写实现。根据与此方法关联的对象,通过调用正确的 Area() 实现,该程序为每个图形计算并显示正确的面积。
// cs_virtual_keyword.cs
// Virtual and override
using System;
class TestClass
{
   public class Dimensions
   {
      public const double pi = Math.PI;
      protected double x, y;
      public Dimensions()
      {
      }
      public Dimensions (double x, double y)
      {
         this.x = x;
         this.y = y;
      }
      public virtual double Area()
      {
         return x*y;
      }
   }
   public class Circle: Dimensions
   {
      public Circle(double r): base(r, 0)
      {
      }
      public override double Area()
      {
         return pi * x * x;
      }
   }
   class Sphere: Dimensions
   {
      public Sphere(double r): base(r, 0)
      {
      }
      public override double Area()
      {
         return 4 * pi * x * x;
      }
   }
   class Cylinder: Dimensions
   {
      public Cylinder(double r, double h): base(r, h)
      {
      }
      public override double Area()
      {
         return 2*pi*x*x + 2*pi*x*y;
      }
   }
   public static void Main() 
   {
      double r = 3.0, h = 5.0;
      Dimensions c = new Circle(r);
      Dimensions s = new Sphere(r);
      Dimensions l = new Cylinder(r, h);
      // Display results:
      Console.WriteLine("Area of Circle   = {0:F2}", c.Area());
      Console.WriteLine("Area of Sphere   = {0:F2}", s.Area());
      Console.WriteLine("Area of Cylinder = {0:F2}", l.Area());
   }
}
输出
Area of Circle   = 28.27
Area of Sphere   = 113.10
Area of Cylinder = 150.80
这个应该也可以算作继承中的复用吧
Justin:
这里我还想谈谈关于构造函数的继承,子类的构造函数在默认的情况下会继承父类的无参构造函数,这一点一定要注意。
 
 
在 Java 的学习与开发过程中,总有一些概念容易让人混淆,从而在编程时出现各种问题。今天,我们就来深入剖析几对极易混淆的概念,通过代码示例与图示,帮助大家彻底理清它们之间的区别与联系。 目录 一、线程与进程 1.1 概念 1.2 区别 1.3 代码示例 二、值传递与引用传递 2.1 概念 2.2 区别 三、接口与抽象 3.1 概念 3.2 区别 3.3 代码示例 总结 一、线程与进程 1.1 概念 进程:是计算机中已运行的程序的实体,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间、系统资源等,进程之间相互隔离,安全性高。例如,当我们同时打开浏览器、音乐播放器和文档编辑器时,它们分别对应一个进程,彼此互不干扰。 线程:是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间、文件句柄等。线程之间的切换和通信开销相对较小,能提高程序的执行效率。 1.2 区别 我们可以通过一张表格直观地看出线程与进程的区别: 对比项 进程 线程 资源分配 独立分配资源,有独立内存空间 共享进程资源 执行开销 创建、撤销、切换开销大 创建、撤销、切换开销小 并发性能 进程间并发,开销大 线程间并发,开销小 安全性 进程间相互隔离,安全性高 共享资源,需考虑同步问题 1.3 代码示例 下面是一个简单的 Java 多线程示例,通过创建线程并启动线程,展示线程的使用: class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + " 正在执行:" + i); } } } public class ThreadAndProcess { public static void main(String[] args) { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); thread1.start(); thread2.start(); } } AI写代码 java 运行 在上述代码中,我们创建了MyThread继承自Thread,重写run方法定义线程执行的逻辑。在main方法中创建两个线程对象并启动,此时两个线程会并发执行run方法中的代码。 二、值传递与引用传递 2.1 概念 值传递:是指在方法调用时,实际参数将其值复制一份传递给形式参数,在方法内部对形式参数的修改不会影响到实际参数的值。简单来说,就是传递的是数据的副本。 引用传递:在方法调用时,实际参数将其引用(内存地址)传递给形式参数,形式参数和实际参数指向同一内存地址,在方法内部对形式参数的修改会直接影响到实际参数。 2.2 区别 在 Java 中,只有值传递。对于基本数据型,传递的是值本身;对于引用数据型,传递的是对象的引用(地址值)。虽然看起来像是引用传递,但本质上还是值传递,因为传递的是引用的副本。我们通过代码来验证: public class PassByValue { public static void changeInt(int num) { num = 100; } public static void changeArray(int[] arr) { arr[0] = 100; } public static void main(String[] args) { int num = 10; changeInt(num); System.out.println("修改后的num:" + num); int[] arr = {10}; changeArray(arr); System.out.println("修改后的arr[0]:" + arr[0]); } } AI写代码 java 运行 在上述代码中,changeInt方法接收一个基本数据型int的参数,在方法内部修改参数值后,不会影响到main方法中num的值。而changeArray方法接收一个引用数据型int[]的参数,在方法内部修改数组元素的值,会影响到main方法中arr数组对应元素的值。这是因为传递的是数组的引用副本,通过该引用副本依然可以访问和修改原数组。 三、接口与抽象 3.1 概念 抽象:使用abstract关键字修饰的,抽象中可以包含抽象方法(只有方法声明,没有方法体)和非抽象方法。抽象不能被实例化,只能被继承,子必须实现抽象中的抽象方法(除非子也是抽象)。抽象通常用于抽取相关的共性,作为父来定义一些通用的行为和属性。 接口:使用interface关键字定义,接口中的方法默认是public abstract的(可以省略不写),属性默认是public static final的常量。接口不能被实例化,通过implements关键字实现接口,一个可以实现多个接口。接口用于定义一种规范,实现接口的必须实现接口中定义的所有方法。 3.2 区别 同样,我们通过表格来对比接口与抽象: 对比项 抽象 接口 定义方式 使用abstract class定义 使用interface定义 继承 / 实现通过extends继承抽象 通过implements实现接口 方法 可以包含抽象方法和非抽象方法 只能包含抽象方法(JDK 8 后可添加默认方法和静态方法) 属性 可以包含各种型的属性 只能包含public static final常量 多继承 一个只能继承一个抽象 一个可以实现多个接口 3.3 代码示例 先看抽象的示例: abstract class Animal { protected String name; public Animal(String name) { this.name = name; } public abstract void speak(); } class Dog extends Animal { public Dog(String name) { super(name); } @Override public void speak() { System.out.println(name + " 汪汪叫"); } } AI写代码 java 运行 在上述代码中,Animal是抽象,包含一个抽象方法speak。Dog继承自Animal,并实现了speak方法。 再看接口的示例: interface Flyable { void fly(); } class Bird implements Flyable { @Override public void fly() { System.out.println("鸟儿在飞翔"); } } AI写代码 java 运行 总结 这里定义了Flyable接口,包含fly抽象方法,Bird实现了Flyable接口并实现了fly方法。 通过以上对线程与进程、值传递与引用传递、接口与抽象的深入剖析和对比,我们可以更清晰地理解它们之间的区别与联系。 在学习 Java 编程过程中,理解这些基础概念至关重要。线程与进程是操作系统中的重要概念,而值传递与引用传递则涉及到方法调用时参数传递的机制,对于理解 Java 中的数据传递有着重要意义。同时,接口与抽象是面向对象编程中的重要组成部分,能够帮助我们更好地设计和组织代码。 通过深入研究这些概念,结合代码示例和区别对比,我们可以更好地掌握 Java 编程中的关键知识点,避免混淆和错误的应用,提高编程效率和质量。希望以上内容能够帮助您更好地理解和运用这些概念,在 Java 学习与开发中取得更好的成果。 ———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2401_88952086/article/details/148977946补充说明
最新发布
06-29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值