1、内部类的本质
之前我们所说的类都对应于一个独立的Java源文件,但是一个类还可以放在另一个类的内部,称之为内部类。相对而言,包含它的类称之为外部类。
一般而言,内部类与包含它的外部类有比较密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上也更为简洁。
不过,内部类只是Java编译器的概念,对于Java虚拟机而言,它是不知道内部类的这回事的。每个内部类最后都会被编译成一个独立的类,生成一个独立的字节码文件。
也就是说,每个内部类其实都可以被替换成一个独立的类。当然,这是单纯就技术实现而言。内部类可以方便的访问外部类的私有变量,可以声明为private从而实现对外完全隐藏,相关代码写在一起,写法也更为简洁,这些都是内部类的好处。
在Java中,根据定义的位置和方式不同,主要有4中内部类。
- 静态内部类
- 成员内部类
- 方法内部类
- 匿名内部类
其中,方法内部类是在一个方法内定义和使用;匿名内部类的使用范围更小,它们都不能再外部使用;成员内部类和静态内部类都可以被外部使用,不够它们都可以被声明为private,这样,外部就不能使用了。
- 1.1 静态内部类
静态内部类和静态变量,静态方法定义的位置一样,也带有static关键字,只是它定义的是类。
public class Outer {
private static int shared = 100;
public static class StaticInner {
public void innerMethod(){
System.out.println("inner " + shared);
}
}
public void test(){
StaticInner si = new StaticInner();
si.innerMethod();
}
}
静态内部类与外部类的联系不大(与其他内部类相比)。它可以访问外部类的静态变量和方法,如innerMethod直接访问share变量,但是不可以访问实例变量和方法。在外部类,可以直接使用静态内部类。
public静态内部类可以直接被外部使用,只是需要通过外部类.静态内部类的方式使用,如下所示:
Outer.StaticInner si = new Outer.StaticInner();
si.innerMethod();
静态内部类内部实现:
public class Outer {
private static int shared = 100;
public void test(){
Outer$StaticInner si = new Outer$StaticInner();
si.innerMethod();
}
static int access$0(){
return shared;
}
}
public class Outer$StaticInner {
public void innerMethod() {
System.out.println("inner " + Outer.access$0());
}
}
内部类访问外部类的一个私有静态变量shared,而我们知道私有变量是不能被外部类访问的,Java的解决方式是:自动为外部类生成非私有的访问方法,它返回这个私有静态变量。
- 1.2 成员内部类
与静态内部类相比,成员内部类没有static修饰符,含义有很大不同。
public class Outer {
private int a = 100;
public class Inner {
public void innerMethod(){
System.out.println("outer a " +a);
Outer.this.action();
}
}
private void action(){
System.out.println("action");
}
public void test(){
Inner inner = new Inner();
inner.innerMethod();
}
}
与静态内部类不同,除了静态变量和方法,成员内部类还可以直接访问外部类的实例变量和方法。
成员内部类还可以通过外部类.this.xxx的方式引用外部类的实例变量和方法,如Outer.this.action()。这些写法一般在重名情况下使用,如果没有重名,那么外部类.this.是多余的。
在外部类内,使用成员变量和方法与静态内部类是一样的,直接使用即可。与静态内部类不同的是,成员内部类对象总是与一个外部类对象相连的。在外部使用时,它不能直接通过new Outer.Inner()方式创建对象,而是要先创建Outer类对象。
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.innerMethod();
与静态内部类不同,成员内部类中不可以定义静态变量和方法(final变量例外,它等同于常量)。Java为什么有这种规定呢?可以这么理解:这些内部类与外部类是相连的,不应该独立使用,而静态变量和方法一般都是独立使用的,在内部类的意义不大,如果内部类确实需要静态变量和方法,可以挪到外部类中。
成员内部类的内部实现:
public class Outer {
private int a = 100;
private void action() {
System.out.println("action");
}
public void test() {
Outer$Inner inner = new Outer$Inner(this);
inner.innerMethod();
}
static int access$0(Outer outer) {
return outer.a;
}
static void access$1(Outer outer) {
outer.action();
}
}
public class Outer$Inner {
final Outer outer;
public Outer$Inner(Outer outer){
ths.outer = outer;
}
public void innerMethod() {
System.out.println("outer a " + Outer.access$0(outer));
Outer.access$1(outer);
}
}
内部类有个实例变量指向外部类对象,它在构造方法中被初始化。外部类在创建内部类对象时给它传递当前对象。由于内部类访问了外部类的private变量和方法,外部类生成了非私有静态方法,供内部类访问。
成员内部类使用场景:如果内部类与外部类关系密切,需要访问外部类的变量和方法,则可以考虑定义为成员内部类。外部类的一些方法的返回值可能是某个接口,为了返回这个接口,外部类方法可能使用内部类实现这个接口,这个内部类可以设置为private,对外完全隐藏。
- 1.3 方法内部类
内部类还可以定义在方法中。
public class Outer {
private int a = 100;
public void test(final int param){
final String str = "hello";
class Inner {
public void innerMethod(){
System.out.println("outer a " +a);
System.out.println("param " +param);
System.out.println("local var " +str);
}
}
Inner inner = new Inner();
inner.innerMethod();
}
}
类Inner定义在方法test中,方法内部类只能在定义的方法内被使用。如果方法是实例方法,则除了静态变量和方法,内部类还可以直接访问外部类的实例方法和变量。如果是静态方法,则方法内部类只能访问外部类的静态变量和方法。方法内部类还可以直接访问方法的参数和方法内的局部变量,不过,这些变量必须被声明为final。
方法内部类的内部实现;
public class Outer {
private int a = 100;
public void test(final int param) {
final String str = "hello";
OuterInner inner = new OuterInner(this, param);
inner.innerMethod();
}
static int access$0(Outer outer){
return outer.a;
}
}
public class OuterInner {
Outer outer;
int param;
OuterInner(Outer outer, int param){
this.outer = outer;
this.param = param;
}
public void innerMethod() {
System.out.println("outer a " + Outer.access$0(this.outer));
System.out.println("param " + param);
System.out.println("local var " + "hello");
}
}
跟成员内部类类似,方法内部类有个实例变量指向外部类对象,它在构造方法中被初始化。外部类在创建内部类对象时给它传递当前对象,对外部私有实例变量访问也是通过非私有静态方法。
方法内部类可以访问方法中的参数和局部变量,其中方法中的参数是通过构造方法传递参数实现的,而局部变量没有作为参数传递,这是以为它被定义为常量,在生成的代码中,可以直接使用它的值。
这也解释了为什么方法内部类访问方法的参数和方法局部变量时,这些变量为什么必须被声明为final。因为方法内部类操作的并不是这些变量,而是它自己的实例变量,只是这些实例变量的值与外部一样,对这些变量的赋值,并不会改变外部的值,为避免混淆,所以干脆强制规定必须声明为final。
如果确实需要修改外部的变量,那么可以将变量改成只包含该变量的数组,修改数组中的值。
public class Outer {
public void test(){
final String[] str = new String[]{"hello"};
class Inner {
public void innerMethod(){
str[0] = "hello world";
}
}
Inner inner = new Inner();
inner.innerMethod();
System.out.println(str[0]);
}
}
通过前面的语法介绍和原理可以看出,方法内部类可以用成员内部类代替,至于方法参数,也可以作为参数传递给成员内部类。不过,如果类只在某个方法中使用,使用方法内部类,可以实现更好的封装。
- 1.4 匿名内部类
匿名内部类实例:
public class Outer {
public void test(final int x, final int y){
Point p = new Point(2,3){
@Override
public double distance() {
return distance(new Point(x,y));
}
};
System.out.println(p.distance());
}
}
匿名内部类只能被使用一次,用来创建一个对象。它没有名字,没有构造方法,但是可以根据参数列表,调用对应父类的构造方法。它可以定义实例变量和方法,可以有初始化代码块,初始化代码块可以起到构造方法的作用,只是构造方法可以有多个,而初始化代码块只能有一份。因为没有构造方法,它自己无法接收参数,需要需要参数,则应该使用其他内部类。与方法内部类一样,匿名内部类也可以访问外部类的所有变量和方法,可以访问方法的final参数和局部变量。
匿名内部类内部实现:
public class Outer {
public void test(final int x, final int y){
Point p = new Outer$1(this,2,3,x,y);
System.out.println(p.distance());
}
}
public class Outer$1 extends Point {
int x2;
int y2;
Outer outer;
Outer$1(Outer outer, int x1, int y1, int x2, int y2){
super(x1,y1);
this.outer = outer;
this.x2 = x2;
this.y2 = y2;
}
@Override
public double distance() {
return distance(new Point(this.x2,y2));
}
}
与方法内部类类似,外部实例this、方法参数都作为参数传递给了内部类的构造方法。此外new的参数也传递给了构造方法,内部类构造方法又将它们传递给了父类的构造方法。
匿名内部类能做的,方法内部类都能做。但是如果对象只会创建一次,且不需要构造方法来接受参数,则可以使用匿名内部类,这样代码书写上更为简洁。
回调
public void sortIgnoreCase(String[] strs){
Arrays.sort(strs, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
}
所谓回调是相对于一般正向调用而言的,平时一般调用都是正向调用。但是Arrays.sort中传递过来的Comparator对象,它的compare方法并不是在写代码的时候被调用,而是在Arrays.sort内部某个地方回过头来调用的。
将程序分为保持不变的主题框架,和针对具体情况的可变逻辑,通过回调方式进行协作,是计算机程序的一种常用实践。匿名内部类是实现回调的一种简便方式。
2、参考资料
Java编程的逻辑 第5章 类的扩展 5.3 内部类