牛刀小试 – 详解Java中的接口与内部类的使用
最后更新于:2022-04-01 20:08:42
**一、接口**
- 接口的理解
Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现;
也就是说,接口自身自提供方法的基本声明,而不提供方法体;接口中声明的方法只能被实现该接口的子类所具体实现。
接口是Java中另一种非常重要的结构。因为Java不支持多继承,某种程度来说这也造成了一定的局限性。
所以接口允许多实现的特点弥补了类不能多继承的缺点。通常通过继承和接口的双重设计,可以既保持类的数据安全也变相实现了多继承。
- 接口的特点
1. 使用关键字"interface"声明一个接口;使用关键字"implements"声明一个类实现一个接口。
1. 与类的权限限制相同,接口的也只能被声明为“public”或默认修饰符。
1. 接口当中声明的变量被自动的设置为public、static、final,也就是说在接口声明的变量实际都会被隐式的提升为"公有的静态常量"
1. 接口中声明的方法都是抽象的,并且都被自动的设置为public。
1. 接口自身不能被构造实例化,但可以通过实现该接口的类进行实例化。
1. 实现接口的类如果不是抽象类,那么该类就必须对接口中的方法进行实现。
1. 接口与接口之间也可以实现继承关系。子接口除拥有父接口的所有方法声明外,还可以定义新的方法声明。
~~~
package com.tsr.j2seoverstudy.interface_demo;
//访问修饰符只能为public或默认访问修饰符
public interface InterfaceDemo {
// 会被隐式的提升为:public static final int VAR = 50;
int VAR = 50;
// public void methodDemo();
void methodDemo();//方法必须是抽象的
}
class Test implements InterfaceDemo{
public static void main(String[] args) {
InterfaceDemo in = new InterfaceDemo();//compile exception
InterfaceDemo in = new Test();//但可以声明接口类型的变量,并通过实现该接口的类来进行实例化
}
@Override
public void methodDemo() {
}
}
~~~
根据接口的特点,实际上我们可以看到:接口实际上更像是在声明一种规范,相当于实现定义了程序的一种框架。例如,作为一种遥控汽车的设计者。你可能需要提供一些规范给这些遥控汽车的生产厂商们,让他们按照你的设计规范来生产遥控汽车。
~~~
package com.tsr.j2seoverstudy.interface_demo;
public interface Moveable {
void turnLeft();
void turnRight();
void stop();
}
~~~
这是你提供的让玩具汽车具备可移动性(moveable)的接口,生产厂商必须按照该接口的规范进行实现,才能然小汽车成功的move起来。
- 接口与抽象类的区别
1. 一个类可以实现多个接口,但只能继承一个抽象类。
1. 抽象类中可以存在非抽象方法,但接口中声明的方法必须是抽象的。
1. 抽象类中的方法可以是任何的访问权限,但接口中的方法都是public权限的。
1. 抽象类中可以存在自己定义的任何类型的实例域(变量等),但接口中的域都是公有、静态、最终的。
而抽象类和接口最大的相同之处,可能就在于:都是对其体系中的对象,不断的进行向上抽取而来的共有特性。它们都可以用于多态的实现。
**二、内部类**
1、什么是内部类?
顾名思义,内部类就指定义在另一个类的内部当中的类。我们知道一个Java所编写的基本体现形式就是一个类,而一个类的结构通常是由域(静态域、实例域)和方法构成的。而有时候一个类中还有另一种构成部分,就是内部类。
2、为什么使用内部类?
关于这点,《Java2学习指南》中这样说:你是一个OO程序猿,因此知道为了代码的重用性和灵活性(可扩展性),需要将类保持足够的专用性。也就是说,一个类只应该具有该类对象需要执行的代码;而任何其它操作,都应该放在更适合这些工作的其它的类当中。但是!有时候会出现当前类当中需要的某个操作,应当放在另一个单独的特殊类中更为合适,因为要保持类足够的专用性;但不巧的是,这些操作又与当前的类有着密切联系(例如会使用到当前类当中的成员(包括私有成员)等等)。正是这一类的情况,促使了内部类的诞生。
而更具体的来说,之所以使用内部类的原因,通常主要在于:
- 在内部类当中可以访问该类定义所在的作用域当中的任何数据,包括私有数据。(这是因为内部类会隐式的持有所在外部类的对象引用:“外部类名.this”)
~~~
package com.tsr.j2seoverstudy.base;
public class Outer {
private int num = 5;
private class Inner {
void printOuterNum() {
/*
* 1.验证了内部类可以访问其定义所在的作用域当中的任何属性,包括被声明为私有的属性。
* 2.之所以内部类能访问外部类的实例属性,是因为其隐式的持有了外部类的对象:外部类类名.this
*/
System.out.println(num);
System.out.println(Outer.this.num);
}
}
}
~~~
可以看到上面例子中虽然外部类“Outer”中的变量“num”被声明为私有的,但定义在“Outer”当中的内部类仍然可以访问到该成员变量。
- 内部类能够针对于存在同一个包下的其他类,将自身隐藏起来。简单的来说,该好处就是带来更完善的类的封装性。
~~~
package com.tsr.j2seoverstudy.base;
class Outer {
private int num = 5;
// 暴露给别人的方法接口
public void exposeMethod() {
Inner in = new Inner();
System.out.println(in.doSomeThingSecret());
}
private class Inner {
int doSomeThingSecret() {
// 封装一些你不想暴露给其它人任何细节的方法
System.out.println("隐蔽的方法,叼!");
return num;
}
}
}
/*
* 程序输出结果为:
* 隐蔽的方法,叼!
* 5
*/
public class Test{
public static void main(String[] args) {
Outer out = new Outer();
out.exposeMethod();
}
}
~~~
通过该例子我们可以看到通过内部类实现带来的严谨的封装性。我们通过内部类“Inner”的方法“doSomeThingSecret”完成了一系列“秘密的操作”。
但我们提供给它人使用时,暴露给使用者的细节只有外部类当中的一个方法“exposeMethod”。这就很好的达到了我们的目的,隐藏不想让别人知道的操作。
我们知道通常来讲,类的访问权限只能被修饰为public 或 默认的。这就意味着即使我们选择相对较小的访问权限:默认的包访问权限。
那么我们定义的该类当中的实现细节,也会暴露给位于同一个包中的其它类。
而内部类允许被声明为pirvate。这意味着:其它类甚至连我们定义了这样的一个类都不知道,就更不用提该类当中的实现细节了。
- 通过匿名内部类能够更为便捷的定义回调函数。以Java中的多线程机制为例:
如果不使用内部类,那么我们的实现方式就应该如同:
~~~
package com.tsr.j2seoverstudy.base;
public class InnerDemo {
public static void main(String[] args) {
Assignment assignment = new Assignment();
Thread t = new Thread(assignment);
t.start();
}
}
class Assignment implements Runnable{
@Override
public void run() {
System.out.println("线程任务");
}
}
~~~
而通过匿名内部类,我们可以将实现简化为:
~~~
package com.tsr.j2seoverstudy.base;
public class InnerDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程任务");
}
});
t.start();
}
}
~~~
与我在之前的回顾中说到的一样,“匿名”其实很好理解,直接的理解就是没有名字。Java中的标示符就是名字。
所以,在第一种实现方式里,定义线程任务类的类名标示符“Assignment ”就是该线程任务类的类名。
而当我们将线程的任务通过第二种方式实现时,我们发现该其直接被作为参数传递给Thread类的构造函数当中。
没有相关的类名标示符,那么这个线程任务类就是没有名字的,所以被称为“匿名”。
注:第三种使用方式应该是实际开发中最常用的。前两种情况我个人在工作里很少用到,但很多知名的书中都介绍了,所以不妨也作为一种了解,总会有用得上的时候。
**创建内部类对象的方式**
在上面我们说过了,内部类当中是隐式的持有一个其所属外部类的对象引用的。
由此就不能想象,一个内部类的对象创建肯定是依赖于其所属的外部类的。
换句话说,要像创建使用一个内部类的对象,前提是必须先获取到其所属外部类的对象。
内部类的创建方式大概也就是两种情况:
- 在其所属外部类当中,创建该内部类对象:这种情况与平常创建对象的方式没有任何不同,也就是ClassName clazz = new ClassName();这样的方式。
- 在其所属外部类之外的类中创建内部类对象:因为我们说过了内部类对象的创建依赖于其所属外部类,所以这是的创建方式为:Outer.Inner in = new Outer().new Inner()。
我们还是通过一道以前看见过的面试题,来更直观的了解内部类的对象创建:
~~~
/*
* 题目:
* 1. public class Outer{
* 2. public void someOuterMethod() {
* 3. // Line 3
* 4. }
* 5. public class Inner{}
* 6. public static void main( String[] args ) {
* 7. Outer o = new Outer();
* 8. // Line 8
* 9. }
* 10. }
*
* Which instantiates an instance of Inner?
* A. new Inner(); // At line 3
* B. new Inner(); // At line 8
* C. new o.Inner(); // At line 8
* D. new Outer.Inner(); // At line 8
*/
~~~
其实很简单,只要牢记我们上面说的两种创建情况就OK了。归纳来讲:在创建内部类对象之前,必须先构造其外部类的对象。
所以当在外部类中创建内部类对象,因为外部类自身持有对象引用:this。所以可以直接创建内部类。
而在外部类之外创建内部类对象,则就需要先new Outer()创建得到外部类对象,再创建内部类对象。
由此我们来分别看该题目当中的4个答案:
- A答案放在程序的第三行。是在其所属外部类当中的实例方法中创建内部类对象,因为实例方法持有外部类对象引用this,所以可以直接创建。则A答案合法。
- B答案放在程序的第八行。虽然是在内部类本身创建内部类对象,但因为代码是位于静态方法当中,所以并不持有其外部类对象。所以B是非法的。
- C答案放在程序的第八行。代码虽然位于静态方法中,但因为之前已经创建了外部类对象“o”,所以再通过“o”创建内部类对象的方式是行得通的的。但要注意的是这种方式的正确使用形式应当是“o.Inner()”而非“new o.Inner()”。所以C也是非法的。
- D答案放在程序的第八行。乍看之下,十分完美。但要注意的是其使用的是“new Outer.”,而非通过new关键字调用类构造器创建对象的正确方式“new Outer().”。所以D自然也是非法的。
由此可以得出结论,合法的内部类实例声明方式只有:A。
**局部内部类**
局部内部类是内部类之中一种稍显特殊的使用方式。顾名思义,与“局部变量”相同,也就是被定义在方法或代码块当中的内部类。
关于局部内部类的使用,我觉得需要掌握的主要只有三点:
第一、与其它的局部成员一样,局部类的有效范围被限定在包含的代码块中,一旦超出该范围,该局部内部类就不能被访问了。
第二、局部内部类不能被访问修饰符修饰,也就是说不能使用private、protected、public任一修饰符。因为作用域已经被限定在了当前所属的局部块中。
第三、这通常也是使用局部内部类的最常见原因。你可能也注意到了,普通的内部类虽然能访问任何其所属外部类的成员;但其所属外部类定义的方法当中的局部变量是访问不到的,使用局部内部类就可以解决这一问题。但必须谨记的是:被局部内部类访问的变量必须被修饰为final。
~~~
public class PartInnerDemo {
int num_1 = 10;
public void method(){
final int num_2 = 5;
class PartInner{
private void InnerMethod(){
System.out.println(num_1+num_2);
}
}
}
}
~~~
**
**
**静态内部类(嵌套类)**
静态内部类可以说是内部类当中的一朵奇葩。开个玩笑,之所以这样说是因为静态内部类是比较特殊的一种内部类。
它的特性更像是一种嵌套类,而非内部类。因为我们前面说过了一般内部类当中,都会隐式的持有一个其所属外部类的对象引用。而静态内部类则不会。
除此之外,在任何非静态内部类当中,都不能存在静态数据。所以,如果想在内部类中声明静态数据,那么这个内部类也必须被声明为静态的。
当然,静态类中除了静态数据,也可以声明实例数据。不同之处在于:
如果要在之外使用静态内部类当中的静态数据,那么可以直接通过该内部类的:类名.静态成员名的方式。
而如果要只用该静态内部类当中的实例成员,那么就必须如同其他非静态内部类一样,先创建该内部类的对象。
但同时需要注意,静态内部类的对象创建与一般的内部类又有所不同,因为我们知道静态内部类自身是不持有外部类的对象引用的,所以它不依赖于外部类的对象。简单的说,我们可以认为静态内部类自身也就是所属外部类的一个静态成员,所以其对象创建的方式为:Outer.Inner in = new Outer.Inner();
~~~
public class StaticInner {
void test() {
int num_1 = Inner.num_1;
//
Inner in = new Inner();
int num = in.num;
}
private static class Inner {
int num = 5;
static int num_1 = 10;
}
public static void main(String[] args) {
StaticInner.Inner in = new StaticInner.Inner();
}
}
~~~
说到这里,想起另外一个问题。这个问题在初学Java时一直没能想清楚原因:
~~~
public class Test {
public static void main(String[] args) {
class Inner{
String name;
Inner(String s){
name = s;
}
}
Inner o = new Inner("test");
}
}
~~~
我们在前面说过,所有非静态的内部类的实例化工作,都依赖于其外部类的对象实例化。
但在这段代码中,我们对于定义的局部内部类“Inner”的对象创建却没有依赖于其所在外部类。
这应当是因为:因为该局部内部类被定义在一个静态方法中,静态方法中不会持有其所属的类的对象引用this。
也就是说,定义在静态方法当中的局部内部类遭受与静态方法同样的限制:不能访问任何其所属外部类的非静态成员。
而内部类之所以持有其外部类对象引用的目的在于:可以访问其所属外部类的所有实例成员。
那么既然现在我已经被限制成不能访问实例成员了,自然也就不必依赖于外部类的对象了。
**匿名内部类**
关于匿名内部类,其实在上面说为什么使用内部类的三种原因时,已经说过了。
匿名内部类的定义格式通常为:
~~~
new SuperType(constuction parameters){
//inner class method and data
}
~~~
对于匿名内部类,简单来说就是一种内部类的简写形式。
而必须注意的是,对于匿名内部类的使用,其前提是:该内部类必须是继承于一个外部类或者实现一个外部接口。
正如我们在上面说到的关于Java多线程。之所以能使用匿名内部类,是因为我们定义的匿名内部类实现了Runnable接口。
';