我们在UI界面重绘系列(上)中,探讨了类与对象、继承的关系、方法重写;今天我们来看看接口和构造器在重绘中的使用。并探讨它们的相关知识,更重要的是,我们会去补充一些之前未聊到的内容。
首先,我们来认识接口。
接口是什么?
接口不是类,它是一种抽象类型。接口中只能有抽象方法,被隐式的指定为 public abstract,接口中还可以有变量,这些变量会被隐式的指定为 public static final 变量。
接口声明的语法格式如下:
[可见度] interface 接口名称 [extends 其他的接口名] {
// 声明变量
// 抽象方法
}
我们可以看到,一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。接下来,我们继续聊聊它和类有哪些异同。
类与接口的异同
相同之处
1、Java中类与接口都保存在 .java 结尾的文件中,文件名使用相同的类名或是接口名。
2、一个类或接口都可以有多个方法。
不同之处
普通类 | 接口 | |
---|---|---|
方法 | 构造方法 | 抽象方法 |
成员变量 | 任意指定 | 只被static和final修饰 |
实例化对象 | 能 | 否 |
声明关键字 | class | interface |
继承关键字 | extends | implements(类继承接口时) |
抽象类 | 接口 | |
方法体 | 有 | 无 |
抽象形式 | abstract修饰 | 隐式:public abstract方法和public static final变量 |
静态代码块和静态方法 | 可以含有 | 不能含有 |
除此之外:
1、一个类只能继承一个抽象类,而一个类却能同时实现多个接口。
例如:之前我们的监听类实现了鼠标和动作监听器接口。
public class DrawListener implements MouseListener,ActionListener{
但需要注意的是,类的多继承是不合法的,只有接口可以被多继承。多继承指的是,一个类分别继承了多个其他类,这里我们要区分extends和implements。类只能多重继承,即一个类被很多类所继承。
2、 一个接口又可以被多个接口所继承。
例如:鼠标、键盘、动作监听器接口都继承于事件监听器接口
public interface MouseListener extends EventListener{
public interface ActionListener extends EventListener{
public interface KeyListener extends EventListener{
3、一个实现接口的类,必须实现接口内所描述的所有方法,否则必须声明为抽象类。但如果实现接口的类是抽象类,那么就没必要实现该接口的方法。
例如:
public class DrawListener implements MouseListener{
public void mousePressed(MouseEvent e){}
public void mouseReleased(MouseEvent e){}
public void mouseClicked(MouseEvent e) {}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
}
4、标记接口是没有任何方法和属性的接口,它仅仅表明它的类属于一个特定的类型。供其他代码来测试允许做一些事情。
例如:
package java.util;
public interface EventListener {
}
标记接口目的有二:
一是建立一个公共的父接口,当一个接口去继承父接口时,Java虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。
二是向一个类添加数据类型,实现标记接口的类不需要定义任何接口方法,但是该类通过多态性变成一个接口类型。
接口的使用
我们来创建一个公共的父类接口
import java.awt.Graphics;
public interface ShapeInfo {
public void draw(Graphics g);
}
并让其他接口或类去继承它,这里我们让Shape类去继承这个接口
public class Shape implements ShapeInfo{
声明好的ShapeInfo作为底层的基本接口,将不被修改,我们只需通过增补代码,即继承和实现来完成我们想要的功能。
下面,我们来谈谈构造器。
为什么要有构造器?
Java中引入构造器,是确保每一个对象都得到初始化,Java在有能力操作对象之前,系统会自动调用相应的构造器,或是用new关键字调用构造器,保证初始化的进行。调用构造器是编译器的责任,所以必须让编译器知道调用的是哪一个方法。所以Java采取了与类同名的方法命名构造器。
实际上用new调用构造器时,系统会为该对象分配内存空间,并为这个对象执行默认初始化。如果这个对象已经产生了,那么这些操作在执行构造器之前就已经完成了。所以当系统开始构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外界程序访问,只能够在构造器中通过this来引用。当执行结束后,这个对象作为构造器的返回值被返回,通过赋值给另一个引用类型的变量,从而让外部程序访问。
例如:
Shape s=new Shape();
如果我们写一个没有构造器的类,编译的时候,系统会默认创建一个无参构造器,我们可以自定义一个构造器去改变系统的默认初始化。
构造器的应用
this的使用
之前我们提到过这两个关键字。构造器和方法使用关键字this有很大的区别。
1、方法引用this指向正在执行方法的类的实例。静态方法不能使用this关键字,因为静态方法不属于类的实例,所以this也就没有什么东西去指向。
2、构造器的this指向同一个类中,不同参数列表的另外一个构造器。
super的使用
构造器和方法,都用关键字super指向超类,但是用的方法不一样。
1、方法用这个关键字去执行被重载的超类中的方法。
2、构造器使用super去调用超类中的构造器。
和this一样,在构造器中使用关键字super,它必须被放在第一行,否则会编译报错。
例如:我们在重写绘制方法paint时,代码如下:
public void paint(Graphics g){
//引用父类的绘制方法
super.paint(g);
//重写绘制方法
for(int i=0;i<data.length;i++){
Shape s=data[i];
s.draw(g);
}
}
如果将super.paint(g);放在for循环语句执行完,那么编译则无法引用到父类的绘制方法。执行程序,变动窗体时,将会出现下面这种绘制不完全的情况:
自定义Shape构造器
之前在实现重绘功能时,在监听器中初始化了Shape类的对象s,源代码如下:
Shape s=new Shape();
s.color=Color.BLACK;
s.shape=ShapeStr;
s.x1=x1;
s.y1=y1;
s.x2=x2;
s.y2=y2;
现在我们将Shape类中的默认无参构造器如下:
public Shape(int x1,int x2,int y1,int y2,Color color,String shape){
this.x1=x1;
this.x2=x2;
this.y1=y1;
this.y2=y2;
this.color=color;
this.shape=shape;
}
并在监听器中直接创建对象,并完成成员变量的初始化:
Shape s=new Shape(x1,x2,y1,y2,Color.BLACK,ShapeStr);
其他补充
问题一:Add default serial version ID警告
DFrame extends JFrame时会有Add default serial version ID的提示,按要求添加,会多出一行莫名其妙的代码,此时不再提示:
private static final long serialVersionUID = 1L;
在类的继承过程中,如果增改属性或方法,可能会提示我们要显示serialVesionUID的声明。目的是Serializable(序列化)对象版本的控制,避免有关版本Deserialize(反序列化)时不兼容的情况发生。如果不声明,JVM就会为我们自动产生一个值,这个值和编译器的实现相关,并不稳定;可能在不同JVM环境下出现Deserialize时报InvalidClassException异常。
实现机理大致如下:当serialVersionUID相同时,它就会将不一样的field以type的预设值Deserialize,即可避开不兼容性的问题.
问题二:java.lang.NullPointerException报错
这是空指针报错信息,我们增添 if 语句,避免指针指向Null的情况。
//重写绘制方法
for(int i=0;i<data.length;i++){
if(data[i]==null)break;
Shape s=data[i];
s.draw(g);
}
同样,当绘制图形数超过Shape数组长度时,会出现如下报错信息:java.lang.ArrayIndexOutOfBoundsException
我们在使用数组时一定要小心数组溢出的现象,这会导致不可预判的情况发生,十分危险。关于数组的内容我们会在今后继续探讨。