Java—内部类详解
1.内部类简介
在Java中,可以将一个类定义在另一个类或者方法里面,这样的类称之为内部类。广泛意义上的内部类一般包括这四种:成员内部类,局部内部类,匿名内部类和静态内部类。我们就先来了解一下这四种内部类的用法。
内部类是一种编译器现象,与虚拟机无关。编译器会把内部类翻译成用$(美元符号)符号分隔外部类名与内部类名的常规类文件,而虚拟机对此一无所知
2.四种内部类
1>.成员内部类
成员内部类是最普通的内部类,它的定义为:位于一个类的内部,如下面的例子
public class InnerClass {
private int test;
public class Hello{
static final int a = 5; //内部类中的静态域必须被final修饰
public void change(){
test = 2;
}
}
public static void main(String[] args) {
InnerClass ic = new InnerClass();
ic.new Hello().change();
System.out.println(ic.test);
}
}
上面的例子中,我们先不看主方法里的内容,外部类中声明了一个私有域,然后还有一个名为Hello的成员内部类,成员内部类中有一个被static final 声明的变量,在内部类中存在一个方法,将外部类中的test成员变量修改为5。
这里就体现了内部类的几个特性:
- 1.可以无条件访问外部类中的其他域或者方法,无论是共有还是私有属性(你可以将成员内部类当作外部类的一个成员,这也是它叫做成员内部类的原因)。
- 2.内部类中的静态域必须被final修饰,要理解这一点就必须要知道:成员内部类只能由其外部类创建或访问。也就是说,我们想要得到一个内部类的实例,就必须先创建一个外部类。外部类可能有多个,所以内部类的实例也可能有多个,如果一个变量被声明为static变量,那么这个时候它的值可能会被多个不同的内部类实例修改,所以必须内部类的静态域必须被final修饰
- 3.内部类中不能存在static方法。前面说过,内部类可以访问外部类中的域和成员方法,但如果存在这种情况:外部内并没有创建实例,但是直接调用了内部类中的静态方法,该去访问谁呢?其实也可以去访问静态域和方法,但是这样太过于复杂。
另外,内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
2>.局部内部类
- 局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法或者该作用域中。
public class OutClass {
private int a;
public void test(int b){
class Inner{
public void change(){
a = 7;
// b = 5; //局部内部类只能访问声明为final的局部变量
System.out.println(b);
}
}
Inner i = new Inner(); //只在该方法内可见
i.change();
}
public static void main(String[] args) {
OutClass oc = new OutClass();
oc.test(7);
System.out.println(oc.a);
}
}
注意:局部内部类就像是方法中的局部变量一样,是不能有public,protected,private以及static修饰符的。
另外:在局部内部类中是不能访问局部变量的,除非这个变量被final修饰(即对基本类型不可修改,引用类型不可改变引用)。
但是上面的例子中,变量b并没有被final修饰啊,但是还可以被输出,这其实是jdk1.8之后,取消了这个限定。但如果把上面代码中的b=5的注释去掉,编译器就会报错,为什么呢? 其实还是不能在局部内部类中去修改局部变量的值,那既然不修改,效果其实和加上了final是一样的。
3>.匿名内部类
在平时写代码时最常用到的就是匿名内部类了,在java8引进lambda表达式之后,使匿名内部类的使用更进一步方便了,为什么这么说呢?我们先来研究以下匿名内部类存在的意义:
我们用我们常见的一种情况作为例子:
interface Test{
int change();
}
public class AnonymousInner {
private int a;
public void test(Change c){
a = c.change();
}
public static void main(String[] args) {
AnonymousInner ai = new AnonymousInner();
ai.test(new Change());
System.out.println(ai.a);
}
}
class Change implements Test{
@Override
public int change() {
int a = 5;
return a;
}
}
我们用上面的代码来实现改变成员变量a的值,我们发现,Change类实现了Test接口,重写了change()方法,并且在test()方法中被调用,我们可不可以不去定义那个实现Test接口的Change类,而是在使用时现场重写方法呢?答案当然是肯定的,这里就要用到我们的匿名内部类了
public class AnonymousInnerClass {
private int a;
void test(Test t){
a = t.change();
}
public static void main(String[] args) {
AnonymousInnerClass aic = new AnonymousInnerClass();
aic.test(new Test() {
@Override
public int change() {
int a = 5;
return a;
}
});
System.out.println(aic.a);
}
}
我们在调用test()方法时,为其传入了一个实现了Test接口的实例,但是这个实例并不是用自己的类名创建的,而是用接口名创建实例,同时重写了接口中的change()方法,没有出现继承Test接口的类名,所以我们称之为匿名内部类。
在引入了lambda表达式之后,上述代码我们可以简化为:
public static void main(String[] args) {
AnonymousInnerClass aic = new AnonymousInnerClass();
aic.test(() -> {
int a = 5;
return a;
});
System.out.println(aic.a);
}
在使用匿名内部类时要注意:
- 匿名内部类是唯一一种没有构造器的类。所以匿名内部类的使用范围非常有限,大部分时候都是为了实现函数式接口。
- 同时,使用匿名内部类时,只能创建一个实例,且只能被使用一次。
- 匿名内部类实际上属于局部内部类,所以局部内部类的限制都对其生效
4>.静态内部类
静态内部类也是定义在一个类中的类,不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点像,并且它不能使用外部类的非static成员变量,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
public class StaticClass {
static class InnerClass{
public static void test(){
System.out.println("静态内部类测试");
}
}
public static void main(String[] args) {
InnerClass.test();
}
}
3.深入理解内部类
1>.为什么成员内部类可以无条件访问外部类的成员?
在编译器进行编译的时候,会将成员内部类单独编译成一个字节码文件,即编译后有一个 Outer.class文件,同时有一个Outer$Inner.class文件,对这个文件反编译之后,我们从中获取到: 虽然在定义时内部类是默认的无参构造器,但是编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以在成员内部类中可以随意访问外部类的成员。这也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类实例,则无法对该默认参数初始化赋值,就无法创建内部类对象了
2>.为什么局部内部类和匿名内部类只能访问局部final变量
若使用了匿名内部类,编译后会产生Outer.class和Outer$x.class(x为正整数)两个class文件,考虑一种情况,局部变量在该方法结束后就自动销毁了,但如果匿名内部类中的代码还在运行怎么办呢? 为了能够实现这样的效果,java采用了复制的手段来解决。也就是说,编译器默认会在匿名内部类(局部内部类)的常量池中太难嫁一个内容相等的字面量。这样,匿名内部类使用的其实是另一个局部变量,只不过值和局部变量的值相等,因此和方法中的局部变量完全独立开。
也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
那么既然匿名内部类中的局部变量与真正传入的局部变量不是同一个变量,那么如果在匿名内部类中对其进行修改,就会出现数据的不一致性,也就是说,真正的局部变量就不会改变。所以java编译器就限定必须将变量a限制为final变量(java8之后可以不这么做),但局部变量的值不可以改变(对于引用型变量,不允许指向新的对象)。
4.使用内部类的好处:
- 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整
- 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏