浅谈类、抽象类、接口和继承之间的关系

本文探讨了类、抽象类、接口及继承的概念及其相互间的关系。解释了接口为何不实现方法却能在程序中被调用,以及抽象类的对象如何在JDK中存在等问题。

下面重点谈一谈类、抽象类、接口和继承之间的关系
不少细心的初学者在论坛上问类似这样的问题:
1、接口不实现方法,但我却在程序中可以调用接口的方法,这是为什么?比如 java.sql 包中的 Connection、Statement、ResultSet 等都是接口,怎么可以调用 它们的方法呢?
2、抽象类不能实例化,但是jdk中却有很多抽象类的对象,这是为什么?比如 System.in 是一个 InputStream 类型对象,但 InputStream 是抽象类,怎么可以得到它的对象呢?


不管怎么样,大家应该明白一点:不管是抽象类中的抽象方法,还是接口中定义的方法,都是需要被调用的,否则这些方法定义出来就没有意义了。


可能有很多书上没有提到,或者提到了而读者没有注意到这一点:
一个子类如果继承了它的基类,则表示这个类也是其基类的一种类型,这个子类的一个对象是子类类型,并且同时也是其基类的一个对象,它也具有基其类的类型;一个类如果实现了一个接口,则表示这个类的一个对象也是这个接口的一个对象。


可能这样说不太好懂,又是子类、基类、类型、接口什么的,容易搞混。其实举个现实的例子你就会觉得其实很简单:
如果“人”是一个基类,则“男人”是“人”的一个子类。如果“张三”是一个“男人”,也就是说“张三”是“男人”的一个对象,那么显然“张三”也是“人”这个基类的一个对象。


明白了这一点,就容易理解为什么我们可以得到抽象类的对象了:原来我们得到的抽象类的对象其实是它的已经实现了抽象方法的子类或子孙类的一个对象,但我们拿它当它的抽象类的基类来用。比如“人”这个类,每个人都会“悲伤”,男人悲伤的时候抽烟、喝酒,女人悲伤的时候哭泣、流泪。由于不同的子类在“悲伤”时所进行的动作不一样,因此这个动作(方法)在基类中不好实现,但基类中又需要有这个方法,因此,“人”这个类就可以定义一个抽象方法“悲伤”,由其子类“男人”和“女人”来实现“悲伤”这个方法。但是调用者只把男人和女人的对象当作其基类“人”的一个对象,调用它的“悲伤”方法。
读者可以去体验一下 jdk 的抽象类 java.lang.Process :
Runtime runtime = Rumtime.getRuntime();
Process process = rumtime.exec("notepad.exe");
Class cls = process.getClass();
System.out.println(cls.getName());
这时会打印出 process 类的名字,如果在 Windows 下它会是一个类似于 *Win32* 的名字,它是 Process 的一个子类。因为 process 类用于管理打开的进程,而在不同的操作系统上都有不同的实现,因此它把方法定义为 Process 的抽象方法,而具体的操作只能由对应在不同操作系统下的子实现。



下面来谈接口,我们知道接口只定义了一些方法,而没有实现这些方法。而其实,接口是一个规范,它规定了实现这个接口所要做的事情,或者说规定了实现接口的类必须具备的能力(也就是方法)。
那么我们可以这样对比:
某种类型的驾驶执照,规定了拿到这个驾照的人必须能够“开小汽车”和“开公共汽车”。那么我们认为这个驾照是一个接口,它规定了实现它的类所必须有的能力。
我们可以定义一个类 Driver,继承自 Human,然后实现“驾照持有者”这个接口:
public interface DriverHolder{
    public void driverCar();
    public void driverBus();
}
public class Driver extends Human implements DriverHolder{
    public void driverCar(){
        // ......
    }
    public void driverBus(){
        // ......
    }
}



这样一来,一个“Driver”对象,它同时也是一个 DrivreHolder 对象。即一个司机(Driver)同时是一个驾照执持有者对象。在程序中我们可以这样:
DriverHolder driverholder = new Driver();
driverholder.driverCar();


这样我们就解释了为什么“接口没有实现方法,却可以得到接口类的对象”的问题。


但是这样一来,肯定有人会问:为什么要定义一个接口呢,为什么不直接把这个方法定义到 Driver 类中去,然后 Driver driver = new Driver(); 一样可以调用它的 driverCar(); 和 driverBus() 方法,这样做岂不是方便得多?


这是因为java是单继承的,它只能继承于一个类,这样它的类型就只限于其基类或者基类的基类。但是java可以实现多个接口,这样它就可以有很多个接口的类型。就象一个人,它继承自“脊椎动物”这个类,而“脊椎动物”又继承自“动物”这个类,因此“张三”是个人,他是一个“脊椎动物”,当然他也是一个“动物”。但他可以继承很多个接口,比如拿驾驶执照之后,他就是“驾照持有者”类型,他也可以拿英语六级证书,这样他就是一个六级证书持有者等等。


明白这一点之后,我们来看一看 java 的事件机制。
java.awt.Button 类有一个 addActionListener(ActionListener l);方法。这个方法传入的是一个接口类型:ActionListerner,在实际中,我们需要实现 ActionListener 接口,并且把实现这个接口的类的对象引用作为参数传入。这样,Button对象就得到了一个 ActionListener 对象,它知道这个 ActionListener 对象有一个 actionPerformed 方法,或者说它有处理 Action 事件的能力,当 Action 事件发生时,它就可以调用这个对象的 actionPerformed 方法。


比如一般我们会这样做:
public class TestButton extends Frame implements ActionListener{
    private Button btn1 = new Button();
    //......
   
    public TestButton(){
        btn.addActionListener(this);
 this.add(btn);
    }
   
    public void actionPerformed(ActionEvent e){
 
    }
}


现在我们假设 ActionListener 不是接口,而是一个类。那么我们只能继承 ActionListener 类,并且重写 actionPerformed 方法。但是java是单继承的,如果类继承了 ActionListener 类,那么它就不能继承其它的类(Frame 类)了,而不从 Frame 类继承的话,又怎么创建窗体,怎么把 Button 放到窗体中去呢?



其实接口不完全是为了解决 java 的单继承问题,它在某种程度上可以达到调用和实现细节的分离。
比如说,中国的民用电规范是一个接口:平均电压220V、50Hz、Sin 交流电,水力发电厂、火力发电厂、核电厂,还有小型的柴油发电机如果按这个规范发电,则表示它们实现了这个民用电源的接口;冰箱、电视、洗衣机等家用电器使用这些电源,表示它们在调用这个接口。在这里,家用电器不管电从哪里来,只要它符合民用电源的规范就好,电源也不管它发的电用于什么工作,只管提供电源。


再回过头来看看 Button 的事件机制。要知道,Button 要保证所有的 action 事件发生时,程序员都可以在他的代码中处理它,图书馆管理系统、收银系统、进销存等等等等等等。而接口就可以做到这一点:找一个类实现 ActionListener 接口,并且让 Button 得到这个类的对象的引用( 调用 addActionListener 方法),从而当Action事件发生时,button 创建一个包含了事件信息的对象(ActionEvent),然后调用这个接口对象的方法,到底怎么处理这次事件,这就是实现接口的类的事情了。在这里,Button 丝毫不了解 actionPerformed 方法中到底干了什么事情,也不应该知道。Button 与具体的业务逻辑完全分离开了,它可以应用到所有的场合。

<think>首先,用户的问题是:“我想了解C#中抽象类的概念用法 请问C# 抽象类定义及使用示例”。我需要根据系统级指令来回答。 系统级指令: 1. 所有行内数学表达式必须使用$...$格式。 2. 独立公式必须使用$$...$$格式并单独成段。 3. LaTeX语法正确。 4. 使用中文回答。 5. 生成相关问题。 6. 回答中引用的段落末尾自然地添加引用标识,例如:[^1] 用户可见层: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 - 参考站内引用,但用户提供的引用是关于C#抽象类的,所以我可以使用这些作为参考。 用户提供的引用: - [^1]: C#入门(8):抽象类介绍。解释抽象类不能被实例化,作为基,包含抽象成员必须在派生中被重写。 - [^2]: 浅谈C#之抽象类、抽象方法。给出抽象类定义示例:public abstract class Animal,包含非抽象方法抽象方法。 - [^3]: C#中抽象类的使用示例。提到abstract修饰符,用于抽象类抽象方法。 - [^4]: C#基础知识学习——抽象类定义与使用举例。定义抽象类,不能被实例化,普通继承时必须实现抽象方法。 - [^5]: C#中抽象类、抽象方法接口。强调抽象类继承后必须实现所有抽象方法,声明规则。 我的回答需要结构清晰: 1. 解释抽象类的概念。 2. 给出定义。 3. 提供使用示例。 4. 添加引用标识。 5. 生成相关问题。 由于问题不涉及数学表达式,我不需要强制添加LaTeX数学内容。但指令说“所有行内数学表达式必须使用$...$格式”,但这里没有数学表达式,所以我可以忽略,除非必要。但为了安全,我应该只在有数学时使用。 回答结构: - 开头介绍抽象类概念。 - 解释定义。 - 提供代码示例。 - 讨论用法。 - 结束生成相关问题。 使用中文回答。 步骤: 1. 定义抽象类:使用引用中的信息。 2. 示例:从引用中提取代码或创建似。 3. 添加引用:在相关段落末尾添加[^n]。 4. 生成相关问题。 引用标识:我需要自然地添加。例如,在解释概念时,如果基于某个引用,添加[^1]。 相关问题:在最后生成3-5个相关问题。 最终回答格式: - 标题:C#抽象类概念与使用示例 - 内容: - 什么是抽象类? - 如何定义抽象类? - 使用示例。 - 注意事项。 - §§相关问题§§ - 1. ... - 2. ... - 3. ...</think>### C#抽象类概念与使用示例 在C#中,抽象类(Abstract Class)是一种特殊的,它不能被直接实例化(即不能创建对象),而是作为其他的基使用。抽象类的主要目的是定义一组公共行为规范,要求派生(子)必须实现这些行为。抽象类可以包含: - **抽象成员**:如抽象方法、属性或事件,这些成员只有声明没有实现,必须在派生中被重写(override)。 - **非抽象成员**:如普通方法或属性,可以有具体实现,派生可以直接使用或重写。 抽象类常用于实现代码复用多态性,确保所有派生遵循统一的接口规范[^1]。 #### 抽象类的定义 在C#中,定义抽象类需使用 `abstract` 关键字修饰名。抽象方法也必须用 `abstract` 声明,且不能有方法体。声明时需注意: - 抽象方法必须位于抽象类中。 - 不能使用 `virtual`、`static` 或 `private` 修饰符[^5]。 定义示例: ```csharp public abstract class Animal // 抽象类定义 { // 非抽象方法:有具体实现 public void Eat() { Console.WriteLine("动物正在进食"); } // 抽象方法:无实现,派生必须重写 public abstract void MakeSound(); } ``` 此示例中,`Animal` 是一个抽象类,包含一个非抽象方法 `Eat()` 一个抽象方法 `MakeSound()`[^2]。 #### 使用示例 抽象类的核心用法是通过继承来实现多态。派生必须重写所有抽象成员,否则会编译错误。以下是完整示例: ```csharp using System; // 定义抽象类 public abstract class Shape { // 抽象属性:派生必须实现 public abstract double Area { get; } // 非抽象方法:通用实现 public void Display() { Console.WriteLine($"形状的面积是: {Area}"); } } // 派生:圆形 public class Circle : Shape { public double Radius { get; set; } public Circle(double radius) { Radius = radius; } // 重写抽象属性 public override double Area => Math.PI * Radius * Radius; // 计算圆面积 } // 派生:矩形 public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } public Rectangle(double width, double height) { Width = width; Height = height; } // 重写抽象属性 public override double Area => Width * Height; // 计算矩形面积 } // 主程序 class Program { static void Main() { Shape circle = new Circle(5.0); // 创建圆形对象 circle.Display(); // 输出: 形状的面积是: 78.53981633974483 Shape rectangle = new Rectangle(4.0, 6.0); // 创建矩形对象 rectangle.Display(); // 输出: 形状的面积是: 24 } } ``` 在此示例中: - `Shape` 是抽象基,定义了抽象属性 `Area` 非抽象方法 `Display()`。 - `Circle` `Rectangle` 是派生,它们重写了 `Area` 属性以提供具体实现。 - 通过抽象类,实现了多态:`Main` 方法中统一调用 `Display()`,但不同子计算面积的方式不同[^4]。 #### 关键注意事项 1. **实例化限制**:抽象类不能被直接实例化(如 `new Animal()` 会报错),只能通过派生使用[^1]。 2. **成员实现**:派生必须使用 `override` 关键字实现所有抽象成员,否则编译失败。 3. **构造函数**:抽象类可以有构造函数(用于初始化基状态),但不能直接调用[^4]。 4. **与接口的区别**:抽象类可以包含实现代码(如普通方法),而接口只定义契约;一个只能继承一个抽象类,但可实现多个接口[^5]。 抽象类适用于需要共享代码逻辑的场景(如游戏中的角色基),而接口更适合定义纯行为规范[^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值