文章目录
1 抽象类
当编写一个类时,常常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确地知道这些子类如何实现这些方法。例如定义了一个Shape类,这个类应该提供一个计算周长的方法calPerimeter(),但不同Shape子类对周长的计算方法是不一样的,即Shape类无法准确地知道其子类计算周长的方法。
如何既能让Shape类里包含calPerimeter()方法,又无须提供其方法实现呢?使用抽象方法即可满足该要求:抽象方法是只有方法签名,没有方法实现的方法。
1.1 抽象方法和抽象类
抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。
抽象方法和抽象类的规则如下。
- 抽象类必须使用
abstract
修饰符来修饰,抽象方法也必须使用abstract
修饰符来修饰,抽象方法不能有方法体。 - 抽象类
不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例
。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。 - 抽象类可以包含Field、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类、枚举类6种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
- 含有抽象方法的类(包括直接定义了一个抽象方法;继承了一个抽象父类,但没有完全实现父类包含的抽象方法,以及实现了一个接口,但没有完全实现接口包含的抽象方法3种情况)只能被定义成抽象类。
注意
归纳起来,可以用“有得有失”4个字来描述抽象类。“得”指的是抽象类多了一个能力:抽象类可以包含抽象方法;“失”指的是抽象类失去了一个能力:抽象类不能用于创建实例。
当使用abstract
修饰类
时,表明这个类只能被继承
;当使用abstract
修饰方法
时,表明这个方法必须由子类提供实现(即重写)。而final
修饰的类
不能被继承,final
修饰的方法
不能被重写。因此final和abstract永远不能同时使用。
除此之外,当使用static修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法,但如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误)。因此static和abstract不能同时修饰某个方法,即没有所谓的类抽象方法。
注意:
abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即private和abstract不能同时使用。
1.2 抽象类的作用
抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。
在下面这个范例的抽象父类中,父类的普通方法依赖于一个抽象方法,而抽象方法则推迟到子类中提供实现。
public abstract class SpeedMeter
{
//转速
private double turnRate;
public SpeedMeter()
{
}
//把返回车轮半径的方法定义成抽象方法
public abstract double getRadius();
public void setTurnRate(double turnRate)
{
this.turnRate=turnRate;
}
//定义计算速度的通用算法
public double getSpeed()
{
//速度等于 车轮半径 * 2 * PI * 转速
return java.lang.Math.PI * 2 * getRadius() * turnRate;
}
}
public class CarSpeedMeter extends SpeedMeter
{
public double getRadius()
{
return 0.28;
}
public static void main(String[] args)
{
CarSpeedMeter csm=new CarSpeedMeter();
csm.setTurnRate(15);
System.out.println(csm.getSpeed());
}
}
下面是使用模板模式的一些简单规则。
- 抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
- 父类中可能包含需要调用的其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助。
2 更彻底的抽象:接口
抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(interface),接口里不能包含普通方法,接口里的所有方法都是抽象方法。
2.1 接口的概念
当我们说PCI接口
时,指的是主机板上那个插槽遵守了PCI规范
,而具体的PCI插槽
只是PCI接口的实例。
可见,接口是从多个相似类中抽象出来的规范,接口不提供任何实现。接口体现的是规范和实现分离的设计哲学。
让规范和实现分离正是接口的好处,让软件系统的各组件之间面向接口耦合,是一种松耦合的设计。
2.2 接口的定义
[修饰符] interface 接口名 extends 父接口1, 父接口2...
{
零个到多个常量定义...
零个到多个抽象方法定义...
}
语法的详细说明
- 修饰符可以是
public
或者省略,如果省略了public
访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口。 - 接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵守Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无须任何分隔符。
- 一个接口可以有
多个直接父接口
,但接口只能继承接口,不能继承类
。
由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含Field (只能是常量)、方法(只能是抽象实例方法)、内部类(包括内部接口、枚举)定义。
接口里定义的是多个类共同的公共行为规范,因此接口里的所有成员,包括常量、方法、内部类和枚举类都是public访问权限。定义接口成员时,可以省略访问控制修饰符
,如果指定访问控制修饰符,则只能
使用public
访问控制修饰符。
对于接口里定义的常量Field
而言,它们是接口相关的,而且它们只能是常量
,因此系统会自动为这些Field增加static
和final
两个修饰符。也就是说,在接口中定义Field时,不管是否使用public static final修饰符,接口里的Field总将使用这三个修饰符来修饰。而且,接口里没有构造器和初始化块
,因此接口里定义的Field只能在定义时指定默认值
。
//系统自动为接口里定义的Field增加public static final修饰符
int MAX_SIZE=50;
public static final int MAX_SIZE=50;
对于接口里定义的方法而言,它们只能抽象方法,因此系统会自动为其增加abstract修饰符;由于接口里的方法全部是抽象方法,因此接口里不允许定义静态方法,即不可使用static修饰接口里定义的方法。不管定义接口里方法时是否使用public abstract修饰符,接口里的方法总是使用public abstract来修饰。
注意:
接口里定义的内部类、接口、枚举类默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static对它们进行修饰。
2.3 接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。
2.4 使用接口
接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。
一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。
[修饰符] class 类名 extends 父类 implements 接口1,接口2...
{
类体部分
}
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
注意:
实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用public访问权限。
2.5 接口和抽象类
共同特征:
- 接口和抽象类
都不能被实例化
,它们都位于继承树的顶端,用于被其他类实现和继承。 - 接口和抽象类
都可以包含抽象方法
,实现接口或继承抽象类的普通子类都必须实现这些抽象方法
。
差别:
接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
接口里只能包含抽象方法,不包含已经提供实现的方法;抽象类则完全可以包含普通方法。
接口里不能定义静态方法;抽象类里可以定义静态方法。
接口里只能定义静态常量Field,不能定义普通Field;抽象类里则既可以定义普通Field,也可以定义静态常量Field。
接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
2.6 面向接口编程
1.简单工厂模式
有一个场景:假设程序中有个Computer类需要组合一个输出设备,现在有两个选择:直接让Computer类组合一个Printer,或者让Computer类组合一个Output,那么到底采用哪种方式更好呢?假设让Computer类组合一个Printer对象,如果有一天系统需要重构,需要使用BetterPrinter来代替Printer,于是我们需要打开Computer类源代码进行修改。如果系统中只有一个Computer类组合了Printer还好,但如果系统中有100个类组合了Printer,甚至1000个、10000个……将意味着我们要打开100个、1000个、10000个类进行修改,这是多么大的工作量!
为了避免这个问题,我们让Computer类组合一个Output类型的对象,将Computer类与Printer类完全分离。
public class Computer
{
private Output out;
public Computer(Output out)
{
this.out=out;
}
//定义一个模拟获取字符串输入的方法
public void keyIn(String msg)
{
out.getData(msg);
}
//定义一个模拟打印的方法
public void print()
{
out.out();
}
}
上面的Computer类已经完全与Printer类分离,只是与Output接口耦合。Computer不再负责创建Output对象,系统提供一个Output工厂来负责生成Output对象。这个OutputFactory工厂类代码如下:
public class OutputFactory
{
public Output getOutput()
{
return new Printer();
}
public static void main(String[] args)
{
OutputFactory of=new OutputFactory();
Computer c=new Computer(of.getOutput());
c.keyIn("轻量级Java EE企业应用实战");
c.keyIn("疯狂Java讲义");
c.print();
}
}
在该OutputFactory类中包含了一个getOutput方法,该方法返回一个Output实现类的实例,该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定(具体由该方法中的粗体部分控制,当然也可以增加更复杂的控制逻辑)。如果系统需要将Printer改为BetterPrinter实现类,只需让BetterPrinter实现Output接口,并改变OutputFactory类中的getOutput方法即可。
public class BetterPrinter implements Output
{
private String[] printData
=new String[MAX_CACHE_LINE * 2];
//用以记录当前需打印的作业数
private int dataNum=0;
public void out()
{
//只要还有作业,就继续打印
while(dataNum > 0)
{
System.out.println("高速打印机正在打印:" + printData[0]);
//把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData , 1, printData, 0, --dataNum);
}
}
public void getData(String msg)
{
if (dataNum >=MAX_CACHE_LINE * 2)
{
System.out.println("输出队列已满,添加失败");
}
else
{
//把打印数据添加到队列里,已保存数据的数量加1
printData[dataNum++]=msg;
}
}
}
通过这种方式,我们把所有生成Output对象的逻辑集中在OutputFactory工厂类中管理,而所有需要使用Output对象的类只需与Output接口耦合,而不是与具体的实现类耦合。
2.命令模式
考虑这样一种场景:某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组的数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。
这个要求看起来有点奇怪:这个方法不仅需要普通数据可以变化,甚至还有方法执行体也需要变化,难道我们能把“处理行为”作为一个参数传入该方法?
因为Java不允许代码块单独存在,因此我们使用一个Command接口来定义一个方法,用这个方法来封装“处理行为”。下面是该Command接口的代码。
public interface Command
{
//接口里定义的process方法用于封装“处理行为”
void process(int[] target);
}
下面是需要处理数组的处理类,在这个处理类中包含一个process方法,这个方法无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个Command参数负责对数组的处理行为。该类的程序代码如下。
public class ProcessArray
{
public void process(int[] target , Command cmd)
{
cmd.process(target);
}
}
通过一个Command接口,就实现了让ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没有提供真正的处理,只有等到需要调用ProcessArray对象的process方法时,才真正传入一个Command对象,才确定对数组的处理行为。
public class CommandTest
{
public static void main(String[] args)
{
ProcessArray pa=new ProcessArray();
int[] target={3, -4, 6, 4};
//第一次处理数组,具体处理行为取决于PrintCommand
pa.process(target , new PrintCommand());
System.out.println("------------------");
//第二次处理数组,具体处理行为取决于AddCommand
pa.process(target , new AddCommand());
}
}
运行程序结果:
下面分别是PrintCommand类和AddCommand类的代码。
public class PrintCommand implements Command
{
public void process(int[] target)
{
for (int tmp : target )
{
System.out.println("迭代输出目标数组的元素:" + tmp);
}
}
}
public class AddCommand implements Command
{
public void process(int[] target)
{
int sum=0;
for (int tmp : target )
{
sum +=tmp;
}
System.out.println("数组元素的总和是:" + sum);
}
}
对于PrintCommand和AddCommand两个实现类而言,实际有意义的部分就是process(int[] target)方法,该方法的方法体就是传入ProcessArray类里的process方法的“处理行为”,通过这种方式就可实现process方法和“处理行为”的分离。
3 内部类
- 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
- 匿名内部类适合用于创建那些仅需要一次使用的类。对于前面介绍的命令模式,当需要传入一个Command对象时,重新专门定义PrintCommand和AddCommand两个实现类可能没有太大的意义,因为这两个实现类可能仅需要使用一次。在这种情况下,使用匿名内部类将更方便。
3.1 非静态内部类
定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。
public class OuterClass
{
//此处可以定义内部类
}
注意:
外部类的上一级程序单元是包,所以它只有2个作用域:同一个包内和任何位置。而内部类的上一级程序单元是外部类,它就具有4个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用4种访问控制权限。
下面程序在Cow类里定义了一个CowLeg非静态内部类,并在CowLeg类的实例方法中直接访问Cow的private访问权限的实例Field。
public class Cow
{
private double weight;
//外部类的两个重载的构造器
public Cow(){}
public Cow(double weight)
{
this.weight=weight;
}
//定义一个非静态内部类
private class CowLeg
{
//非静态内部类的两个Field
private double length;
private String color;
//非静态内部类的两个重载的构造器
public CowLeg(){}
public CowLeg(double length , String color)
{
this.length=length;
this.color=color;
}
public void setLength(double length)
{
this.length=length;
}
public double getLength()
{
return this.length;
}
public void setColor(String color)
{
this.color=color;
}
public String getColor()
{
return this.color;
}
//非静态内部类的实例方法
public void info()
{
System.out.println("当前牛腿颜色是:"
+ color + ", 高:" + length);
//直接访问外部类的private修饰的Field
System.out.println("本牛腿所在奶牛重:" + weight); //①
}
}
public void test()
{
CowLeg cl=new CowLeg(1.12 , "黑白相间");
cl.info();
}
public static void main(String[] args)
{
Cow cow=new Cow(378.9);
cow.test();
}
}
如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。
public class DiscernVariable
{
private String prop="外部类的实例变量";
private class InClass
{
private String prop="内部类的实例变量";
public void info()
{
String prop="局部变量";
//通过 外部类类名.this.varName 访问外部类实例Field
System.out.println("外部类的Field值:"
+ DiscernVariable.this.prop);
//通过 this.varName 访问内部类实例的Field
System.out.println("内部类的Field值:" + this.prop);
//直接访问局部变量
System.out.println("局部变量的值:" + prop);
}
}
public void test()
{
InClass in=new InClass();
in.info();
}
public static void main(String[] args)
{
new DiscernVariable().test();
}
}
public class Outer
{
private int outProp=9;
class Inner
{
private int inProp=5;
public void acessOuterProp()
{
//非静态内部类可以直接访问外部类的成员
System.out.println("外部类的outProp值:"
+ outProp);
}
}
public void accessInnerProp()
{
//外部类不能直接访问非静态内部类的实例Field
//下面代码出现编译错误
//System.out.println("内部类的inProp值:" + inProp);
//如需访问内部类的实例Field,则必须显式创建内部类对象
System.out.println("内部类的inProp值:"
+ new Inner().inProp);
}
public static void main(String[] args)
{
//执行下面代码,只创建了外部类对象,还未创建内部类对象
Outer out=new Outer(); //①
out.accessInnerProp();
}
}
非静态内部类对象和外部类对象的关系是怎样的?
答:非静态内部类对象必须寄存在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄存其中。简单地说,如果存在一个非静态内部类对象,则一定存在一个被它寄存的外部类对象。但外部类对象存在时,外部类对象里不一定寄存了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定存在。
根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。如下程序所示。
public class StaticTest
{
//定义一个非静态内部类,是一个空类
private class In{}
//外部类的静态方法
public static void main(String[] args)
{
//下面代码引发编译异常,因为静态成员(main方法)
//无法访问非静态成员(In类)
new In();
}
}
Java不允许在非静态内部类里定义静态成员。
public class InnerNoStatic
{
private class InnerClass
{
/*
下面三个静态声明都将引发如下编译错误:
非静态内部类不能有静态声明
*/
static
{
System.out.println("==========");
}
private static int inProp;
private static void test(){}
}
}
注意:
非静态内部类里不可以有静态初始化块,但可以包含普通初始化块。非静态内部类普通初始化块的作用与外部类初始化块的作用完全相同。
3.2 静态内部类
如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。
静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。下面程序就演示了这条规则。
public class StaticInnerClassTest
{
private int prop1=5;
private static int prop2=9;
static class StaticInnerClass
{
//静态内部类里可以包含静态成员
private static int age;
public void accessOuterProp()
{
//下面代码出现错误
//静态内部类无法访问外部类的实例成员
System.out.println(prop1);
//下面代码正常
System.out.println(prop2);
}
}
}
为什么静态内部类的实例方法也不能访问外部类的实例属性呢?
答:因为静态内部类是外部类的类相关,而不是外部类的对象相关的。也就是说,静态内部类对象不是寄存在外部类对象里的,而是寄存在外部类的类本身中。当静态内部类对象存在时,并不存在一个被它寄存的外部类对象,静态内部类对象里只有外部类的类引用,没有持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被寄存的外部类对象,这将引起错误。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。
public class AccessStaticInnerClass
{
static class StaticInnerClass
{
private static int prop1=5;
private int prop2=9;
}
public void accessInnerProp()
{
//System.out.println(prop1);
//上面代码出现错误,应改为如下形式
//通过类名访问静态内部类的类成员
System.out.println(StaticInnerClass.prop1);
//System.out.println(prop2);
//上面代码出现错误,应改为如下形式
//通过实例访问静态内部类的实例成员
System.out.println(new StaticInnerClass().prop2);
}
}
除此之外,Java还允许在接口里定义内部类,接口里定义的内部类默认使用public static修饰,也就是说,接口内部类只能是静态内部类。
3.3 使用内部类
(1)在外部类内部使用内部类
(2)在外部类以外使用非静态内部类
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
OuterClass.InnerClass varName
从上面语法格式可以看出,在外部类以外的地方使用内部类时,内部类完整的类名应该是OuterClass.InnerClass
。如果外部类有包名,则还应该增加包名前缀。
因为非静态内部类的对象必须寄存在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:
OuterInstance.new InnerConstructor()
下面程序示范了如何在外部类以外的地方创建非静态内部类的对象,并把它赋给非静态内部类类型的变量。
class Out
{
//定义一个内部类,不使用访问控制符
//即只有同一个包中的其他类可访问该内部类
class In
{
public In(String msg)
{
System.out.println(msg);
}
}
}
public class CreateInnerInstance
{
public static void main(String[] args)
{
Out.In in=new Out().new In("测试信息");
/*
上面代码可改为如下三行代码:
使用OutterClass.InnerClass的形式定义内部类变量
Out.In in;
创建外部类实例,非静态内部类实例将寄存在该实例中
Out out=new Out();
通过外部类实例和new来调用内部类构造器创建非静态内部类实例
in=out.new In("测试信息");
*/
}
}
如果需要在外部类以外的地方创建非静态内部类的子类,则尤其要注意上面的规则:非静态内部类的构造器必须通过其外部类对象来调用。
我们知道:当创建一个子类时,子类构造器总会调用父类的构造器,因此在创建非静态内部类的子类时,必须保证让子类构造器可以调用非静态内部类的构造器,调用非静态内部类的构造器时,必须存在一个外部类对象。
下面程序定义了一个子类继承了Out类的非静态内部类In类。
public class SubClass extends Out.In
{
//显示定义SubClass的构造器
public SubClass(Out out)
{
//通过传入的Out对象显式调用In的构造器
out.super("hello");
}
}
非静态内部类In对象和SubClass对象都必须保留有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;当创建SubClass类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。
注意:
非静态内部类的子类不一定是内部类,它可以是一个外部类。但非静态内部类的子类实例一样需要保留一个引用,该引用指向其父类所在外部类的对象。也就是说,如果有一个内部类子类的对象存在,则一定存在与之对应的外部类对象。
(3)在外部类以外使用静态内部类
因为静态内部类是外部类类相关的,因此创建内部类对象时无须创建外部类对象。
new OuterClass.InnerConstructor()
下面程序示范了如何在外部类以外的地方创建静态内部类的实例。
class StaticOut
{
//定义一个静态内部类,不使用访问控制符
//即同一个包中的其他类可访问该内部类
static class StaticIn
{
public StaticIn()
{
System.out.println("静态内部类的构造器");
}
}
}
public class CreateStaticInnerInstance
{
public static void main(String[] args)
{
StaticOut.StaticIn in=new StaticOut.StaticIn();
/*
上面代码可改为如下两行代码:
使用OuterClass.InnerClass的形式定义内部类变量
StaticOut.StaticIn in;
通过new来调用内部类构造器创建静态内部类实例
in=new StaticOut.StaticIn();
*/
}
}
不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。
创建静态内部类的子类
public class StaticSubClass extends StaticOut.StaticIn {}
3.4 局部内部类
如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。
public class LocalInnerClass
{
public static void main(String[] args)
{
//定义局部内部类
class InnerBase
{
int a;
}
//定义局部内部类的子类
class InnerSub extends InnerBase
{
int b;
}
//创建局部内部类的对象
InnerSub is=new InnerSub();
is.a=5;
is.b=8;
System.out.println("InnerSub对象的a和b Field是:"
+ is.a + "," + is.b);
}
}
编译上面程序,看到生成了三个class文件:LocalInnerClass.class、LocalInnerClass$1InnerBase.class和LocalInnerClass 1 I n n e r S u b . c l a s s ,这表明局部内部类的 c l a s s 文件总是遵循如下命名格式: O u t e r C l a s s 1InnerSub.class,这表明局部内部类的class文件总是遵循如下命名格式:OuterClass 1InnerSub.class,这表明局部内部类的class文件总是遵循如下命名格式:OuterClassNInnerClass.class。
3.5 匿名内部类
匿名内部类适合创建那种只需要一次使用的类,例如前面介绍命令模式时所需要的Command对象。匿名内部类的语法有点奇怪,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。
new 父类构造器(实参列表)|实现接口()
{
//匿名内部类的类体部分
}
从上面定义可以看出,匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或实现一个接口。
- 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
- 匿名内部类不能定义构造器,因为匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义实例初始化块,通过实例初始化块来完成构造器需要完成的事情。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象,如下程序所示。
interface Product
{
public double getPrice();
public String getName();
}
public class AnonymousTest
{
public void test(Product p)
{
System.out.println("购买了一个" + p.getName()
+ ",花掉了" + p.getPrice());
}
public static void main(String[] args)
{
AnonymousTest ta=new AnonymousTest();
//调用test方法时,需要传入一个Product参数
//此处传入其匿名实现类的实例
ta.test(new Product()
{
public double getPrice()
{
return 567.8;
}
public String getName()
{
return "AGP显卡";
}
});
}
}
上面程序中的AnonymousTest类定义了一个test方法,该方法需要一个Product对象作为参数,但Product只是一个接口,无法直接创建对象,因此此处考虑创建一个Product接口实现类的对象传入该方法——如果这个Product接口实现类需要重复使用,则应该将该实现类定义成一个独立类;如果这个Product接口实现类只需一次使用,则可采用上面程序中的方式,定义一个匿名内部类。
由于匿名内部类不能是抽象类,所以匿名内部类必须实现它的抽象父类或者接口里包含的所有抽象方法。
- 当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值。
- 但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此处的相似指的是拥有相同的形参列表。
程序清单:codes\06\6.7\AnonymousInner.java
abstract class Device
{
private String name;
public abstract double getPrice();
public Device(){}
public Device(String name)
{
this.name=name;
}
//此处省略了name的setter和getter方法
...
}
public class AnonymousInner
{
public void test(Device d)
{
System.out.println("购买了一个" + d.getName()
+ ",花掉了" + d.getPrice());
}
public static void main(String[] args)
{
AnonymousInner ai=new AnonymousInner();
//调用有参数的构造器创建Device匿名实现类的对象
ai.test(new Device("电子示波器")
{
public double getPrice()
{
return 67.8;
}
});
//调用无参数的构造器创建Device匿名实现类的对象
Devicelt@span b=1> dlt@span b=1>=lt@span b=1> new Device()
{
//初始化块
{
System.out.println("匿名内部类的初始化块...");
}
//实现抽象方法
public double getPrice()
{
return 56.2;
}
//重写父类的实例方法
public String getName()
{
return "键盘";
}
};
ai.test(d);
}
}
当创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法。如果有需要,也可以重写父类中的普通方法。
如果匿名内部类需要访问外部类的局部变量,则必须使用final修饰符来修饰外部类的局部变量,否则系统将报错。
interface A
{
void test();
}
public class ATest
{
public static void main(String[] args)
{
int age=0;
A a=new A()
{
public void test()
{
//下面语句将提示错误:
//匿名内部类内访问局部变量必须使用final修饰
System.out.println(age);
}
};
}
}
参考文献:《疯狂java讲义》李刚