上篇文章讲述了Java中两种特殊的”类”—接口和对象,本篇文章将讲述一下Java中另一种特殊的类—内部类的相关细节。内部类是相对于普通类来讲的,是指定义在一个类内部的类,而包含它的类则称为外部类。一般来讲,内部类和外部类都有着比较密切的联系,同时和其它类的关系并不是很大,定义在类内部,可以实现对外的隐藏,可以更好地封装。但是内部类只是Java编译器的概念,对于Java虚拟机而言,它并不知道内部类的存在, 每个内部类在编译以后都会生成一个独立的字节码文件。Java中内部类主要有以下四种:
- 静态内部类
- 成员内部类
- 局部内部类
- 匿名内部类
1. 静态内部类
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 useStaticInnerClass() {
StaticInner si = new StaticInner();
si.innerMethod();
}
public static void main(String[] args) {
Outer.StaticInner staticInner = new Outer.StaticInner();
staticInner.innerMethod();
}
}
外部类为Outer,静态内部类为StaticInner,静态内部类除了位置放在别的类内部,其它的跟普通的类几乎没什么区别,也可以定义静态变量、实例变量、静态方法、实例方法。另外,静态内部类可以访问外部类的静态变量、静态方法和构造函数,而不能访问外部类的实例变量和实例方法。在外部类中可以直接访问静态内部类,如useStaticInnerClass中所示,否则要使用main方法所示的初始化方式(前提是静态内部类外部可访问,非private修饰)。这里留下一个问题,为什么静态内部类不能访问外部类的实例变量和实例方法?
1.2 静态内部类原理
上面讲过内部类只是Java编译器的概念,对于Java虚拟机而言,它并不知道内部类的存在, 每个内部类在编译以后都会生成一个独立的字节码文件。上述静态内部类的java文件编译后会发现生成两个class文件,分别是Outer.class、Outer$StaticInner.class。两个class的代码大概如下(非反编译class文件得到,反编译后会完全转化成java代码,看不到内部类的实现细节。如果想了解内部类实现细节的同学,可以通过javap查看class文件,根据Java虚拟机指令还原代码)。
//外部类
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());
}
}
可以看到,静态内部类和外部类是完全独立的(静态内部类不依赖于外部类,在未实例化外部类的前提下,也可以实例化内部类)。回到之前提出的问题,为什么静态内部类不能访问外部类的实例变量和实例方法?查看上述还原的代码可以发现,静态内部类中并不持有外部类的实例,而一个类中的实例变量和实例方法是跟具体对象绑定的,要通过对象才能访问,所以可以讲静态内部类的设计就是无法访问外部类的实例变量和实例方法,如果内部类要访问外部类的实例变量和实例方法(跟外部类有紧密的联系),就要使用下面要讲的成员内部类。这也印证了对象和类那篇文章讲的,静态内部类可以完全独立存在,它只是想借用外部类的壳用一下,隐藏一下自己。是一个独立的类,完全是形式上的“内部”,和外部类没有本质上“内外”的关系。
然后关注以下外部类中生成的静态方法access$0(),通过之前的对象和类那篇文章讲的权限修饰符,我们知道private修饰的成员变量只能在本类中访问,所以Java编译器为其生成了一个静态的access$0方法,以便在内部类中访问外部类的私有静态成员。
1.3 静态内部类使用场景
静态内部类使用场景很多,当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建的时候会考虑采用静态内部类的设计。Java中就有很多使用静态内部类的例子,比如:
- Integer类内部有一个私有静态内部类IntegerCache,用于管理Integer常量池数据缓存
- 链表的LinkedList类内部有一个私有静态内部类Node,表示链表中的每个节点
1.4 静态内部类加载时机
熟悉单例模式的话,应该清楚有一种单例就是基于静态内部类实现的,据说可以实现延迟加载,会在调用getInstance方法时才去初始化生成Singleton对象,如下:
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
下面看一下静态内部类的加载时机,如下:
public class Outer1 {
static {
System.out.println("load outer class...");
}
//静态内部类
static class StaticInner {
static {
System.out.println("load static inner class...");
}
static void staticInnerMethod() {
System.out.println("static inner method...");
}
}
public static void main(String[] args) {
Outer1 outer = new Outer1();
System.out.println("======================");
Outer1.StaticInner.staticInnerMethod();
}
}
static静态代码块会在加载类的时候执行,这个后面的文章会给予说明。也就是讲,如果静态内部类随着外部类加载,那么静态内部类static代码块打印的内容肯定在”=============”上。首先看一下运行结果:
Connected to the target VM, address: '127.0.0.1:6072', transport: 'socket'
load outer class...
======================
load static inner class...
static inner method...
Disconnected from the target VM, address: '127.0.0.1:6072', transport: 'socket'
说明静态内部类并没有随着外部类的加载而加载,而是在调用静态内部类的静态方法是加载的。所以加载一个类时,其静态内部类不会同时被加载。一个静态内部类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时或静态内部类对象初始化时发生。
2. 成员内部类
2.1 成员内部类定义
成员内部类是指没有static修饰的内部类,成员内部类跟成员变量和成员方法一样,要通过外部类实例才能访问,定义如下:
@Getter
@Setter
public class Outer {
private int a = 100;
public class Inner {
public void innerMethod() {
System.out.println("outer a " + a);
action();
}
}
private void action() {
System.out.println("action");
}
public void useInnerClass() {
Inner inner = new Inner();
inner.innerMethod();
}
public static void main(String[] args) {
Outer outer = new Outer();
outer.setA(99);
Outer.Inner inner = outer.new Inner();
inner.innerMethod();
}
}
外部类为Outer,成员内部类Inner,与静态内部类不同,除了静态变量和方法,成员内部类还可以直接访问外部类的实例变量和方法,比如innerMethod直接访问外部类私有实例变量a、可以访问外部类的示实例方法action()。但是要注意的是,如果内部类中也有重名方法action,如果要访问外部类action方法就要通过Outer.this.action()访问。另外成员内部类中不能定义static对象类型成员变量(static final基本类型成员可以),也不可以定义静态方法。最后观察一下外部类方法useInnerClass可以发现,外部类中可以直接使用内部类。如果在外部类以外要使用成员内部类,只能通过main方法里的方式访问。
2.2 成员内部类原理
跟静态内部类一样,上述成员内部类的java文件编译后会发现生成两个class文件,分别是Outer.class、Outer$Inner.class,两个class的代码大概如下:
//外部类
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);
}
}
可以看到,内部类Outer$Inner持有外部类Outer对象的引用,也就是讲成员内部类是跟具体的某一个外部类绑定的。同时因为无法直接访问外部类的private成员,所以外部类为私有成员变量a,私有成员方法生成了access函数。
2.3 成员内部类使用场景
如果内部类与外部类关系密切,且操作或依赖外部类实例变量和方法,则可以考虑定义为成员内部类。比如Java API LinkedList中,listIterator方法的返回值是ListIterator接口类型,用于返回链表的Iterator遍历链表,所以LinkedList中定义了一个实现了ListIterator接口的内部类ListItr,listIterator方法中通过调用内部类ListItr的构造函数返回一个ListItr对象,获得ListIterator。同时成员内部类ListItr为private,对外完全隐藏。
3. 局部内部类
3.1 局部内部类定义
局部内部类是指定义在方法体内的内部类,只能在定义的方法内被使用。
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);
a = 20;
}
}
Inner inner = new Inner();
inner.innerMethod();
System.out.println("==============");
System.out.println(a);
}
public static void main(String[] args){
Outer outer = new Outer();
outer.test(999);
System.out.println(outer.a);
}
}
跟局部变量一样,局部内部类不能有权限修饰符。如果方法是实例方法,局部内部类可以访问外部类的静态变量和静态方法,还可以直接访问外部类的实例变量和方法,如innerMethod直接访问了外部私有实例变量a。如果方法是静态方法,则局部内部类只能访问外部类的静态变量和方法。局部内部类还可以直接访问方法的参数和方法中的局部变量,不过,这些变量必须被声明为final(Java8后在定义是不再要求显示指明为final,但是其实还是final类型的),如innerMethod直接访问了方法参数param和局部变量str。
3.2 局部内部类原理
局部内部类编译后,也会生成一个新的class,所以上述Java文件编译后会产生两个class文件,class表示的代码大致如下:
//外部类
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");
}
}
生成的局部内部类OuterInner有一个实例变量outer指向外部对象,用于访问外部类的实例变量和实例方法,特殊的对于外部类私有变量的访问也是通过生成静态access方法实现的。另外,局部内部类可以访问方法的参数和方法中定义的局部变量,是通过局部内部类构造函数传参实现的,比如上述实例的param。
下面试着理解以下为什么局部内部类为什么只能访问外部方法的final参数及局部变量。局部内部类访问方法的局部变量或者参数,代码上看是一个东西,直接传递给内部类。但是从底层实现上看,局部变量和参数传递给内部类是通过局部内部类的构造函数实现的,所以局部变量和方法参数跟局部内部类使用的其实根本就不是一个变量,在内部类对于这些变量的修改对方法中的变量完全没影响,会产生不一致的现象。所以为了避免混淆,干脆定义为final不允许修改。
3.3 局部内部类使用场景
局部内部类使用的比较少,并且能使用局部内部类完成的,用成员内部类肯定也能完成。
4. 匿名内部类
4.1 匿名内部类定义
匿名内部类,也就是没有名字的内部类,是一种特殊的局部内部类。使用匿名内部类有个前提条件,必须继承一个父类或实现一个接口。
new 父类(参数列表) {
//匿名内部类实现部分
}
//或者
new 父接口() {
//匿名内部类实现部分
}
匿名内部类我们必须要继承一个父类或者实现一个接口,也仅能继承一个父类或者实现一个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用,当然这个引用是隐式的。假如存在一个抽象类Person,在Person类中定义了一个抽象方法eat,所以继承了Person类的子类都要实现该方法,如下:
public abstract class Person {
public abstract void eat();
}
public class Worker extends Person{
public void eat() {
System.out.println("eat work meal!");
}
public static void main(String[] args) {
Person p = new Worker();
p.eat();
}
}
假如Worker对象只在这个地方使用一次,那么单独定义一个Worker类必然是比较麻烦的。这时候匿名内部类的好处就体现出来了:
public abstract class Person {
public abstract void eat();
}
public class Outer {
public void test(Date date) {
Person p = new Person() {
public void eat() {
System.out.println(date + " :eat work meal!");
}
};
p.eat();
}
}
对于匿名内部类的使用存在一个缺陷,它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失,所以匿名内部类是不能够被重复使用。对于上面的实例,如果我们需要对test()方法里面内部类进行多次使用,建议重新定义类,而不是使用匿名内部类。
4.2 匿名内部类原理
对于局部内部类,跟上述所讲的其他内部类一样,编译后也会生成新的class。所以上述Java文件编译后也会生成两个class文件,分别为Outer.class,Outer$1.class。两个class文件内容大致如下:
//外部类
public class Outer {
public void test(final Date date){
Person p = new Outer$1(this, date);
p.eat();
}
}
//内部类
public class Outer$1 extends Person {
Outer outer;
Date date;
Outer$1(Outer outer, Date date){
this.outer = outer;
this.date = date;
}
@Override
public void eat() {
System.out.println(date + " :eat work meal!");
}
}
与局部内部类类似,外部实例this,方法date都作为参数传递给了内部类构造方法。另外匿名内部类用来创建一个对象,所以匿名内部类中不能定义构造函数。匿名内部类是一种特殊的局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
4.3 匿名内部类使用场景
如果对象只会创建一次,且不需要构造方法来接受参数,则可以使用匿名内部类,代码书写上更为简洁。在调用方法时,很多方法需要一个接口参数,比如说Arrays.sort方法,它可以接受一个数组,以及一个Comparator接口参数。比如说,我们要对一个字符串数组不区分大小写排序,可以使用Arrays.sort方法,但需要传递一个实现了Comparator接口的对象,这时就可以使用匿名内部类,代码如下所示:
public void sortIgnoreCase(String[] strs){
Arrays.sort(strs, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
});
}