什么是内部类
如果一个类定义在另一个类的内部,那么这个类就是内部类。可以通过public,protected,private修饰符来控制内部类的可见性。同时,内部类也可以被声明为abstract类型,供其他类继承和扩展。
内部类是一个编译时的概念,一旦编译成功,内部类和外部类就是两个完全不同的类。例如,对于一个外部类Outer,在其中定义一个内部类Inner,编译完成后会生成Outer.class
和Outer$Inner.class
两个class文件。所以内部类的成员变量和方法名可以和外部类相同。
内部类用法
按照内部类的用法,可以将内部类分为成员内部类,局部内部类,匿名内部类以及静态内部类。
成员内部类
成员内部类就是在外围类的内部直接定义一个类。可以通过public,protected,private修饰符来控制其可见性。在创建内部类的时候,可以直接创建内部类,也可以让内部类继承某个基类或者接口。如下所示。
1. 直接创建内部类
public class Parcel1 {
private class Contents {
private String content = "goods";
public String contents() {
return content;
}
}
public class Destination {
private String dest;
Destination(String dest) {
this.dest = dest;
}
public String dest() {
return dest;
}
}
public void ship1(String dest) {
Contents c = new Contents();
Destination d = new Destination(dest);
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
public static void ship2(String dest) {
Parcel1 p = new Parcel1();
Contents c = p.new Contents();
Destination d = p.new Destination("dest2");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
public static void main(String[] args) {
Parcel1 p = new Parcel1();
p.ship1("dest1");
Parcel1.ship2("dest2");
}
}
在上面例子中,Contents
和Destination
都定义在Parcel1
类的内部,它们都是public的,所以通过其他类也可以访问到。通过ship1
方法和ship2
方法,我们可以看到创建内部类对象的两种方式。
-
在外部类的非静态方法创建内部类对象
这种方式和使用普通类没有什么区别。可以直接通过
new InnerClass()
的方式来创建内部类对象。如ship1
方法所示。 -
在外部类的静态方法或者其他类中创建内部类对象
在外部类的静态方法或者其他类创建内部类对象时,我们必须首先创建外部类对象
outterObj
,然后通过outterObj.new InnerClass()
的方式来创建内部类对象。如ship2
方法所示。
2.通过实现接口/继承基类创建内部类
除了直接创建内部类,我们还可以让内部类实现某个接口或者继承某个基类。这样,我们在使用内部类对象的时候,可以将其向上转型为对其基类或者接口的引用。这样能够方便地隐藏内部类实现细节并屏蔽类型差异。如下所示。
public interface IContents {
String contents();
}
public interface IDestination {
String dest();
}
public class Parcel2 {
private class Contents implements IContents{
private String content = "goods";
@Override
public String contents() {
return content;
}
}
private class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
public Destination destination(String dest) {
return new Destination(dest);
}
public Contents contents() {
return new Contents();
}
public static void main(String[] args) {
Parcel2 p = new Parcel2();
IContents c = p.contents();
IDestination d = p.destination("dest1");
System.out.println("contents: " + c.contents() + ", destination: " + d.dest());
}
}
这里,内部类Destination
和Contents
分别实现了接口IDestination
和IContents
,我们在访问内部类对象的时候,可以通过IDestination
和IContents
的引用来访问内部类对象,而不再需要关心内部类对象的具体类型。因此,我们可以把Destination
和Contents
设置为private类型。设置为private类型后,在其他类中无法直接创建内部类对象,所以需要在外部类中新增destination
和content
方法来获取内部类对象。
访问外对象成员
内部类可以无缝地访问外部类对象的所有成员,因为当创建一个内部类对象的时候,内部类对象会隐式地持有一个指向外部类对象的引用,通过这个引用来访问外部类的所以成员变量及方法。当然,也可以访问外部类对象本身。关于这点,我们可以直接通过反编译内部类的class文件来验证。我们首先使用javac
命令编译前面的Parcel1
,编译之后,我们看到目录下生成了三个class文件。
Parcel1$1.class
Parcel1$Contents.class
Parcel1$Destination.class
这也验证了内部类是一个编译时的概念,在编译完成后,内部类和外部类就是两个完全独立的类了。通过IntelliJ IDEA我们可以查看反编译后的class文件,我们查看下Parcel1$Contents.class
文件,代码如下所示。
class Parcel1$Contents {
private String content;
private Parcel1$Contents(Parcel1 var1) {
this.this$0 = var1;
this.content = "goods";
}
public String contents() {
return this.content;
}
}
可以看到,编译器为我们做了很多工作,它会自动生成一个内部类的构造方法,这个方法传入的是外部类对象的一个引用,在内部类中,然后在内部类中还会一定一个成员变量this$0
来存储外部类对象的引用。所以,我们在创建一个内部类对象之前,必须首先要创建外部类对象。
持有外部类对象的引用是内部类最有用的一个特性,通过这种方式,我们可以把内部类作为访问外部类的一个窗口。所有内部类(静态内部类除外)在创建的时候都会持有外部类对象的应用,所以在创建内部类对象的时候,我们必须先创建一个外部类对象。而不能直接通过 new InnerClass
的方式来创建内部类对象。例如,下面通过内部类来实现一个迭代器。
public interface Selector {
boolean end();
Object current();
void next();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) {
items = new Object[size];
}
public void add(Object x) {
if (next<items.length) {
items[next++] = x;
}
}
private class SequenceSelector implements Selector {
private int i=0;
@Override
public boolean end() {
return i==items.length;
}
@Override
public Object current() {
return items[i];
}
@Override
public void next() {
if (i < items.length) {
i++;
}
}
}
public Selector selector() {
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(20);
for (int i=0;i<20;i++) {
sequence.add(i);
}
Selector selector = sequence.selector();
while (!selector.end()) {
System.out.println(selector.current());
selector.next();
}
}
}
如果想要在内部类中访问外部类对象本身,可以通过OutterClass.this
来访问。
局部内部类
除了可以定义成员内部类(也就是在外部类中直接定义内部类)外,我们还可以在方法或者局部作用域内定义内部类。
1.在方法内部定义内部类
public class Parcel3 {
public IDestination destination(String dest) {
class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
return new Destination(dest);
}
public IContents contents() {
class Contents implements IContents{
private String content = "goods";
@Override
public String contents() {
return content;
}
}
return new Contents();
}
public static void main(String[] args) {
Parcel3 p = new Parcel3();
IContents c = p.contents();
IDestination d = p.destination("dest1");
System.out.println("contents: " + c.contents() + ", destination: " + d.dest());
}
}
这里的Destination
和Contents
类分别定义在了方法destination
和 content
内部,所以他们只能在所定义的方法内部访问。方法之外的其他地方无法访问该内部类。
2.在代码块定义内部类
除了可以在方法中定义内部类外,我们也可以直接在代码块中定义内部类,如下所示。
public class Parcel4 {
public IDestination destination(String dest) {
class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
return new Destination(dest);
}
public IContents contents(boolean hasContent) {
if (hasContent) {
class Contents implements IContents {
private String content = "goods";
@Override
public String contents() {
return content;
}
}
return new Contents();
}
return null;
}
public static void main(String[] args) {
Parcel4 p = new Parcel4();
IContents c = p.contents(true);
IDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
可以看到Contents
的定义放在了if
语句内部,这就意味着,在if
语句外,我们无法使用该内部类。
虽然Contents
定义在了if
代码块内部,但是这并不代表该类的创建是有条件的,因为内部类是在编译器生成的,编译完成后,该类和外部类没有区别,只是作用域不同罢了。
局部内部类除了作用域不同外,具有和成员内部类一样的特性,局部内部类也可以实现某个接口或者继承某个基类,也可以直接访问外部类对象中的所有成员和方法。但是局部内部类不能有访问说明符,因为他只能在方法或者代码块内部访问。
访问本地变量
当局部内部类引用本地变量时,本地变量必须是final类型或者实际上是final类型。看下面的例子。
public class Parcel7 {
public IDestination destination(String localDest) {
class Destination implements IDestination{
@Override
public String dest() {
return localDest;
}
}
return new Destination();
}
public IContents contents(boolean hasContent) {
if (hasContent) {
class Contents implements IContents {
private String content = "goods";
@Override
public String contents() {
return content;
}
}
return new Contents();
}
return null;
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
IContents c = p.contents(true);
IDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
在上面的例子中,在Destination
是一个局部内部类,并且在Destination
的dest
方法中直接引用了外部类destination
方法中传入的localDest
参数。这Java8之前,这段代码在编译时会直接报错,报错提示如下所示(用Java7编译)。
java: 从内部类中访问本地变量localDest; 需要被声明为最终类型
也就是说localDest
必须声明为final类型,为什么要声明为final类型呢?原因是本地变量的生命周期在方法执行完成的时候就结束了,而内部类的生命周期大多数啊情况下会比本地变量的生命周期要长。如果本地变量被回收,那么在内部类执行到访问本地变量的代码时就会导致空指针问题。所以,为了解决这个问题,在创建内部类的时候,会在内部类中以成员变量的形式对本地变量做一个备份,这个是编译器自动帮我们完成的。关于这点,我们可以通过反编译Parcel7
类来找到答案。
我们通过javac
命令编译Parcel7
类,编译完成后会产生如下三个.class文件。
Parcel7$1Contents.class
Parcel7$1Destination.class
Parcel7.class
然后使用javap
命令对Parcel7$1Destination.class
文件进行反编译。
javap -private Parcel7$1Destination.class
反编译结果如下所示。
class Parcel7$1Destination implements IDestination {
final java.lang.String val$localDest;
final Parcel7 this$0;
Parcel7$1Destination();
public java.lang.String dest();
}
可以看到,在内部类中多了一个val$localDest
的成员变量,这个成员变量就是内部类Destination
中用来对Parcel7.destination
方法中传入的localDest
做备份的。同时也可以看到val$localDest
是final类型的。这意味着,如果我们想在内部类内对localDest
做修改,也是不允许的。那为什么本地变量要定义成final类型呢,先来看看final类型的特点。如果一个变量被声明为final类型,它有如下特点。
- 当final修饰基本数据类型的变量时,这个变量在初始化时就会被赋值,并且初始化完成之后其值就不能再被改变了。
- 当final修饰引用类型的变量时,这个变量在初始化时就会被赋值,并且初始化完成之后其值就不能再被改变,但是该变量指向对象的堆内存的值是可以改变的。
如果当内部类引用本地变量的时候,不把本地变量声明为final类型,在内部类或者外部类的方法中对本地变量做修改后,很容易造成理解上的混淆和代码行为的错乱,所以为了避免混淆和错乱的问题,在内部类引用本地变量的时候,需要把本地变量声明为final类型。这样内部类和方法中的本地变量值就可以始终保持一致。
在Java8之后,不再需要强制将本地变量声明为final类型,所以上面的代码在Java8下编译的时候,不会报错也可以正常执行。但是,如果我们想尝试改变本地变量的值,编译器还是会报错。例如,我们将destination
做如下修改。
public IDestination destination(String localDest) {
class Destination implements IDestination{
@Override
public String dest() {
return localDest;
}
}
localDest = "dest2";
System.out.println(localDest);
return new Destination();
}
我们在方法内重新对localDest
变量进行赋值,然后编译,编译器报错如下所示。
java: 从内部类引用的本地变量必须是最终变量或实际上的最终变量
所以,Java8虽然不再需要将本地变量强制声明为final类型,但是实际上还是要求本地变量时final类型。关于这点,我们可以查看Parcel7.class
文件。
public class Parcel7 {
public Parcel7() {
}
public IDestination destination(final String var1) {
class Destination implements IDestination {
Destination() {
}
public String dest() {
return var1;
}
}
return new Destination();
}
……
}
可以看到,如果在内部类引用了本地变量,编译器会自动将该本地变量转变为final类型。
匿名内部类
匿名内部类本质也属于一种局部内部类,它主要是用于某个类只需要创建一个对象,而不需要再使用的场景。。如下所示。
public class Parcel5 {
public IDestination destination(String dest) {
class Destination implements IDestination{
private String dest;
Destination(String dest) {
this.dest = dest;
}
@Override
public String dest() {
return dest;
}
}
return new Destination(dest);
}
public IContents contents() {
return new IContents() {
private String content = "goods";
@Override
public String contents() {
return content;
}
};
}
public static void main(String[] args) {
Parcel5 p = new Parcel5();
IContents c = p.contents();
IDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
这里在创建Contents对象的时候,就使用到了匿名内部类。之所以叫匿名内部类,就是这个类没有名字,而是直接通过实现某个接口或者基类来创建一个对象,匿名内部类的语法格式如下所示。
new SuperType(construction parameters){
//inner class methods and data
}
其中SuperType可以是像IContents
这样的接口,也可以是一个类。在Java中定义回调函数的时候,经常会用到匿名内部类。例如,在创建一个子线程的时候,我们通常会给Thread的构造方法传递一个实现了Runnable
接口的匿名内部类对象。
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Thread");
}
}).start();
上面的contents
方法通过实现IContents
接口创建了匿名内部类对象,默认使用了匿名内部类的无参构造器。在创建匿名内部类对象的时候,也可以使用有参数的构造器上,不过因为匿名内部类是没有名字的,所以肯定不能在匿名内部类里边定义带参数的构造器。所以,如果想在创建匿名内部类的时候,使用带参数的构造器,必须要匿名内部类的基类含有带参数的构造器。如下所示。
public class BaseDestination {
private String dest = "";
public BaseDestination (String dest) {
this.dest = dest;
}
public String dest() {
return dest;
}
}
public class Parcel6 {
public BaseDestination destination(String dest) {
return new BaseDestination(dest) {
@Override
public String dest() {
return super.dest();
}
};
}
public IContents contents() {
return new IContents() {
private String content = "goods";
@Override
public String contents() {
return content;
}
};
}
public static void main(String[] args) {
Parcel6 p = new Parcel6();
IContents c = p.contents();
BaseDestination d = p.destination("dest1");
System.out.println("content: " + c.contents() + ", destination: " + d.dest());
}
}
上面的例子中,我们通过继承BaseDestination
来创建了一个匿名内部类对象,在创建的时候,我们使用了匿名内部类基类的带参构造器。
由于匿名内部类本质上也是一种局部内部类,它在访问本地变量时也需要将本地变量声明为final类型或者本地变量实际上符合final类型的特点。
静态内部类
前面学习的成员内部类,局部内部类和匿名内部类对象在创建的时候都会隐式地持有外部类对象的引用,如果不想内部类和外部类对象之间有联系,可以将内部类声明为static类型,这就是所谓的静态内部类。静态内部类对象不会持有外部类对象的引用,所以在创建静态内部类对象的时候,并不需要事先创建外部类对象。由于没有持有外部类对象的引用,所以,在静态内部类中只能访问外部类的静态成员变量和静态方法,而无法访问非静态成员变量和非静态方法。
静态内部类由于切断了和外部类的联系,所以它和外部类更加独立。例如,我们在设计模式中经常用到的构建者模式,就是静态内部类的一种典型应用。如下所示。
public class Student {
private String name = "";
private int age;
private String address = "";
private Student(Builder builder) {
name = builder.name;
age = builder.age;
address = builder.address;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public String getAddress() {
return address;
}
public static Builder newBuilder(){
return new Builder();
}
public static class Builder {
private String name = "";
private int age;
private String address = "";
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setAge(int age) {
this.age = age;
return this;
}
public Builder setAddress(String address) {
this.address = address;
return this;
}
public Student build() {
return new Student(this);
}
}
}
public class BuilderTest {
public static void main(String[] args) {
Student.Builder builder = Student.newBuilder();
Student student = builder.setName("xxx")
.setAge(18)
.setAddress("China")
.build();
System.out.println("name: " + student.getName() + ", age: " + student.getAge() + ", address: " +student.getAddress() );
}
}
为什么需要内部类
那么Java为什么要使用内部类呢?通过内部类的特点和用法,我们可以总结出以下原因。
- 内部类定义在一个类的内部,通过内部类我们可以访问外部类的变量和方法,对外部类对象进行操作,作为访问外部类的一个窗口。
- 内部类可以对其他类隐藏可见性和实现细节,保证内部类的扩展性和隔离性。
- 内部类是实现多继承的一种方式,每个内部类都能独立的继承一个类或者实现若干个接口,而不受外围类和其他内部类的限制,所以是实现多继承的一种方案。
- 内部类有一些有用的特性,例如,当想要定义一个回调方法的时候,使用匿名内部类能提供很大的便捷性。