首页 > 代码库 > 牛刀小试 - 详解Java中的接口与内部类的使用

牛刀小试 - 详解Java中的接口与内部类的使用

一、接口

  • 接口的理解

    Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现;

    也就是说,接口自身自提供方法的基本声明,而不提供方法体;接口中声明的方法只能被实现该接口的子类所具体实现。


    接口是Java中另一种非常重要的结构。因为Java不支持多继承,某种程度来说这也造成了一定的局限性。

    所以接口允许多实现的特点弥补了类不能多继承的缺点。通常通过继承和接口的双重设计,可以既保持类的数据安全也变相实现了多继承。


  • 接口的特点
  1. 使用关键字"interface"声明一个接口;使用关键字"implements"声明一个类实现一个接口。
  2. 与类的权限限制相同,接口的也只能被声明为“public”或默认修饰符。
  3. 接口当中声明的变量被自动的设置为public、static、final,也就是说在接口声明的变量实际都会被隐式的提升为"公有的静态常量"
  4. 接口中声明的方法都是抽象的,并且都被自动的设置为public。
  5. 接口自身不能被构造实例化,但可以通过实现该接口的类进行实例化。
  6. 实现接口的类如果不是抽象类,那么该类就必须对接口中的方法进行实现。
  7. 接口与接口之间也可以实现继承关系。子接口除拥有父接口的所有方法声明外,还可以定义新的方法声明。
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. 一个类可以实现多个接口,但只能继承一个抽象类。
  2. 抽象类中可以存在非抽象方法,但接口中声明的方法必须是抽象的。
  3. 抽象类中的方法可以是任何的访问权限,但接口中的方法都是public权限的。
  4. 抽象类中可以存在自己定义的任何类型的实例域(变量等),但接口中的域都是公有、静态、最终的。

    而抽象类和接口最大的相同之处,可能就在于:都是对其体系中的对象,不断的进行向上抽取而来的共有特性。它们都可以用于多态的实现。


二、内部类


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接口。


牛刀小试 - 详解Java中的接口与内部类的使用