java基础之内部类
一、内部类详解
在java中,一个类可以定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义的内部类一般包括:成员内部类,局部内部类,匿名内部类,静态内部类。
1.成员内部类
成员内部类是最普通的内部类,它定义在另一个类的内部(非static),以下代码就是一个成员内部类:
class Outer{
private int age = 1;
public Outer(int age) {
this.age = age;
}
class Inner{
public void doSomething(String name) {
System.out.println(name);
}
}
}
成员内部类可以访问其外部类对象的所有成员而不需要任何特殊条件,即内部类还拥有其外部类的所有元素的访问权,可以无条件访问外部类的所有成员属性和成员方法(也包括private成员和静态成员)。可以通过上面的代码的字节码证明:
Compiled from "InnerClassStu.java"
class com.ran.basic.Outer$Inner {
final com.ran.basic.Outer this$0; //外部类对象的指针
com.ran.basic.Outer$Inner(com.ran.basic.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/ran/basic/Outer;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void doSomething(java.lang.String);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1
4: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
}
可以看到文件中有一个指向外部类对象的指针,这意味着在构建成员内部类对象时,有一个指向其外围类对象的引用,如果编译器访问不到这个引用就会报错。
通过以上证明,还可以说明成员内部类是依附于外部类而存在的,所以假如要创建成员内部类的对象,必须存在一个外部类的对象,创建成员内部类对象的一般方式为:
Outer.Inner inner = new Outer().new Inner();
当然也可以通过调用在外部类里面提供创建成员内部类对象的方法:
public class InnerClassStu {
public static void main(String[] args) {
//第一种方式,必须通过Outter对象来创建
Outer.Inner inner = new Outer().new Inner();
//第二种方式,通过outer类提供的创建inner对象的方法创建
Outer.Inner inner1 = new Outer().getInnerInstance();
}
}
class Outer{
private Inner inner = null;
public Outer() {
}
public Inner getInnerInstance() {
if (inner == null) inner = new Inner();
return inner;
}
class Inner{
public Inner() {
}
private String address = "cq";
}
}
假如当成员内部类拥有和外部类同名的成员变量或者方法的时候,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要在内部类里访问外部类的同名成员,需要以以下的形式进行访问:
外部类.this.成员变量(成员方法)
虽然成员内部类可以无条件的访问外部类的成员,但是外部类不能无条件访问成员内部类的成员。如果外部类想要访问成员内部类的成员,那么需要在外部类里创建一个成员内部类的对象,再通过指向这个对象的引用来访问。以下为一个没有在外部类里面创建成员内部类对象的字节码文件反编译得到如下内容:
Compiled from "InnerClassStu.java"
class com.ran.basic.Outer {
public com.ran.basic.Outer(int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field age:I
9: aload_0
10: iload_1
11: putfield #2 // Field age:I
14: return
}
说明在外部类里面没有创建成员内部类对象时是没有指向内部类的指针。
另,内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。
2.局部内部类
定义在方法或者作用域内的内部类,局部内部类的访问权限仅限于方法内或者该作用域内。
interface Destination{
String read();
}
class Outer{
public Destination destination(String s) {
class Inner implements Destination{
private String text;
private Inner(String text) {
this.text = text;
}
@Override
public String read() {
return text;
}
}
return new Inner(s);
}
}
局部内部类就像是方法里面的一个局部变量一样,是不能有public/protected/private以及static修饰符的。
3.匿名内部类
创建一个继承自某个类(实现接口)的匿名类对象。比如:
abstract class Destination{
private String text;
public Destination(String text){
this.text = text;
}
abstract String read();
}
class Outer{
public Destination destination(String t) {
//匿名内部类
return new Destination(t) {
@Override
public String read() {
return t;
}
};
}
}
如果定义一个匿名内部类,并且希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的。为什么需要是final的呢? 比如以下代码:
public class Test {
public static void main(String[] args) {
new Test().test(2);
}
public void test(final int b) {
//如果在java8之前,不加final会编译不通过,但是在java8可以不需要用final修饰符修饰。
//这实际上就是一个语法糖(底层还是帮你加了final)。
//但通过反编译没有看到底层为我们加上final,我们无法改变这个局部变量的值,如果改变就会编译报错
final int c = 3;
new Thread(){
public void run() {
System.out.println(b);
System.out.println(c);
};
}.start();
}
}
上面的匿名内部类例子的代码,会被编译成两个class文件:
Test.class
Test$1.class //默认情况下,编译器会为匿名内部类和局部内部类起名:外部类名$X.class(X为正整数)。
通过字节码Test$1.class反编译可以得到如下的内容:
Compiled from "Test.java"
class com.ran.basic.Test$1 extends java.lang.Thread {
final int val$b;
final com.ran.basic.Test this$0;
com.xr.basic.Test$1(com.ran.basic.Test, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/ran/basic/Test;
5: aload_0
6: iload_2
7: putfield #2 // Field val$b:I
10: aload_0
11: invokespecial #3 // Method java/lang/Thread."<init>":()V
14: return
public void run();
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #2 // Field val$b:I
7: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iconst_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
}
其中,在run方法里面可以看到iconst_3,表示当int取值3时,JVM采用iconst指令将常量压入栈中。也就是说明使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。如果匿名内部类使用的变量值在方法中被改变,那么就会出现数据的不一致性。为了解决这种可能出现数据不一致的问题,java编译器就要求其参数引用是final的。在java8及以后的版本,在此处新增了一个语法糖,即使你不加final也可以编译通过,因为底层会帮你加上final。
4.静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字(static)。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或方法。因为外部类的非static成员必须依赖于具体的对象,而在没有外部类对象的情况下,可以创建静态内部类的对象。
class Outer{
String fail = "静态内部类调用该属性会在编译期报错";
static String success = "静态内部类调用该静态属性";
public Outer(){}
static class Inner{
public void print(){
// System.out.println(fail);编译不会通过
System.out.println(success);
}
}
}
普通内部类和静态内部类的区别:
1.普通内部类对象隐式的保存了一个指向创建它的外部类对象的引用;而静态内部类就不是这样的。
2.普通内部类不能有static数据和static字段,也不能再包含静态内部类;但是静态内部类可以包含这些所有的东西。 3.普通的内部类通过一个特殊的this引用可以链接到其外围类对象;静态内部类就没有这个特殊的this引用(就是1的另一种说法)。
静态类意味着:
1.要创建静态内部类(嵌套类)的对象,并不需要其外围类对象
2.不能从静态内部类(嵌套类)类的对象中访问非静态的外围类对象(摘自《java 编程思想》)
创建静态内部类对象的一般形式为:
外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
创建成员内部类对象的一般形式为:
外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()
二、内部类的好处
1.内部类有效的实现“多重继承”
每个内部类都能独立的继承一个接口实现,所以无论外部类是否已经继承了某个(接口的)实现,对内部类没有影响。内部类使得多继承的解决方案变得完整。