这不是什么教材,笔者有时会在论坛上瞧瞧,看到不少初学者问到很多问题,这些问题是java程序员应该懂得的,而一般书上不会讲到或者一笔带过的知识。因此斗胆涂鸦一篇文章,把想说的在这里一口气说完。这也是本人第一次写技术性的文章,文笔不畅之外,还请各位见谅。
首先讲清楚类和对象的区别。
类是广泛的概念,表示一个有共同性质的群体,而对象指的是具体的一个实实在在的东西。例如,“人”是一个类,它可以表示地球上所有的人;而“张三”、“李四”、“爱因斯坦”等则是一个个的对象,或者说它们是“人”这个类的一个个实例。在 Java 中,我们可以定义类,然后创建类的对象。
例如:
// 声明一个类“Human”
class Human{
private String name;
public String getName(){
return name;
}
public void setName(String value){
this.name = value;
}
//......
}
创建一个类:
Human human = new Human();
其次,很多人对对象和对象的引用认识模糊
引用是程序操作对象的句柄,相当于C和C++中的指针。
前面说了,对象是一个实实在在的东西,比如前面的代码:
Human human = new Human();
程序执行到这里之后,java虚拟机将会在内存中创建一个 Human 对象,并将这个对象的引用赋给 human 变量。这里有两步,首先是创建 Human 对象,然后把创建的对象的引用赋给 human 变量。
如果声明了一个对象的引用,但没有将对象赋值给它,则这个引用指向了空的对象,或者说引用了不存在的对象。这时如果想通过这个引用访问对象,则会抛出空指针异常,例如:
Human human;
//......
human.setName("张三");
下面重点谈一谈类、抽象类、接口和继承之间的关系
不少细心的初学者在论坛上问类似这样的问题:
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 与具体的业务逻辑完全分离开了,它可以应用到所有的场合。