内类有哪些
内类(inner class)是相对于外部类(outer class)来说的,包括:内部类/接口,内部匿名类/接口。
内部类和外部类的关系
内部类是可以访问外部类的成员变量的,包括私有变量(private)、静态变量(static):
public class OuterClass {
private int privateMem = 1;
static String staticMem = "staticMem";
class InnerClass {
void print() {
System.out.println("innerClass access " + privateMem + "and " + staticMem);
}
}
}
代码成功访问到了“privateMem” 和 “staticMem”,编译器没有报错。
但是外部类却访问不了内部类:
它是怎么做到呢?
好,当然我们在刚接触java的时候,课本上就已经很清楚的告诉我们这样的关系了。but,why?大家都是类,凭什么内部类就能访问外部类的属性,反过来却不行?它们内在究竟靠什么维护这个关系?
我们把OuterClass.java 用javac 做编译:
发现编译后生成了两个.class文件,OuterClass 和 OuterClass$InnerClass 从文件名来看,OuterClass$InnerClass 描述了两者的关系,我们看看里面是啥:
OuterClass.class:
public class OuterClass {
private int privateMem = 1;
static String staticMem = "staticMem";
public OuterClass() {
}
class InnerClass {
InnerClass() {
}
void print() {
System.out.println("innerClass access " + OuterClass.this.privateMem + "and " + OuterClass.staticMem);
}
}
}
和我们最初的代码,就是多了两个空构造方法。
OuterClass$InnerClass:
class OuterClass$InnerClass {
OuterClass$InnerClass(OuterClass var1) {
this.this$0 = var1;
}
void print() {
System.out.println("innerClass access " + OuterClass.access$000(this.this$0) + "and " + OuterClass.staticMem);
}
}
OuterClass$InnerClass有了很大不同!
首先,OuterClass$InnerClass的构造方法变成了有参的,注意这个参数是个OuterClass的实例var1。并在构造方法里出现了“this.this$0 = var1”。大名鼎鼎的“this$0”出现了,做android的同学在学习内存泄漏的时候经常听到,“this$0”指向了外部类的引用,这句话说的就是这里。“this$0” 是虚拟机为每个类预留的“0”号指针(引用)。
再看print函数,对于变量privateMem 和 staticMem的访问变成了“ OuterClass.access$000(this.this$0) “ 和”OuterClass.staticMem“。
怎么会这样?privateMem哪去了?为啥要这样访问?
OK,这时候就要回到一个问题:private 的限制域
这里明确说了内部类是可以访问的,其他统统不行。对于OuterClass$InnerClass 来说,OuterClass 是同包下的,OuterClass$InnerClass当然访问不到OuterClass中的任何一个private。OuterClass$InnerClass 是 InnerClass 在jvm运行时的实际替换者,从这个角度,private并不是对内部类可见的,只是jvm用了个小手段“欺骗”了我们,通过了其他方式拿到了private变量。
然而,OuterClass.access$000(this.this$0) 为什么能代表privateMem这个变量呢?
static Type access$xyz(Outer); 是JAVA编译器自动生成的十分重要的方法(该方法的个数由你的内部类要访问的外部类的变量个数相关),目的是:用于内部类访问外部类的数据成员时使用。XYZ为数字。X是按照私有成员在内部类出现的顺序递增的。YZ为02的话,标明是基本变量成员;YZ为00的话标明是对象成员或者函数。
privateMem是OuterClass的第一个private属性的变量,所以OuterClass.access$000(this.this$0)。
而对于staticMem,对于一个同包下的类来讲,访问OuterClass的static变量,当然只需要OuterClass.staticMem就可以。
题外话:
1、OuterClass$InnerClass 的构造函数参数是个实例而不是class,这说明,如果要实例它,必须要有指定的一个外部类的实例,所以,你不能new OuterClass.InnerClass()这样写,因为这个时候不知道它外部类的具体实例是哪个,只能
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
这样实例InnerClass。
2、access$XYZ 是虚拟机生成的,开发人员并不能在语言层面调用它,但是可以通过ASM等字节码工具生成类似的代码达到访问的目的,这个角度讲,是不安全的。
3、凭什么说OuterClass$InnerClass 就是InnerClass的替代者?
public class OuterClass {
private int privateMem = 1;
private static String staticMem = "staticMem";
void visit(){
new InnerClass().print();
}
class InnerClass {
void print() {
System.out.println("innerClass access " + privateMem +
"and " + staticMem);
}
}
}
加一个访问方法,javac一下,然后对OuterClass.class javap -v一下:
Classfile /Users/yueshaojun/IdeaProjects/learn/src/main/java/com/media/learn/service/OuterClass.class
Last modified 2019-5-19; size 774 bytes
MD5 checksum 3785ea3206188c250d045ba1a200a3a3
Compiled from "OuterClass.java"
public class com.media.learn.service.OuterClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Fieldref #8.#28 // com/media/learn/service/OuterClass.staticMem:Ljava/lang/String;
#2 = Fieldref #8.#29 // com/media/learn/service/OuterClass.privateMem:I
#3 = Methodref #9.#30 // java/lang/Object."<init>":()V
#4 = Class #31 // com/media/learn/service/OuterClass$InnerClass
#5 = Methodref #4.#32 // com/media/learn/service/OuterClass$InnerClass."<init>":(Lcom/media/learn/service/OuterClass;)V
#6 = Methodref #4.#33 // com/media/learn/service/OuterClass$InnerClass.print:()V
#7 = String #14 // staticMem
#8 = Class #34 // com/media/learn/service/OuterClass
#9 = Class #35 // java/lang/Object
#10 = Utf8 InnerClass
#11 = Utf8 InnerClasses
#12 = Utf8 privateMem
#13 = Utf8 I
#14 = Utf8 staticMem
#15 = Utf8 Ljava/lang/String;
#16 = Utf8 <init>
#17 = Utf8 ()V
#18 = Utf8 Code
#19 = Utf8 LineNumberTable
#20 = Utf8 visit
#21 = Utf8 access$000
#22 = Utf8 (Lcom/media/learn/service/OuterClass;)I
#23 = Utf8 access$100
#24 = Utf8 ()Ljava/lang/String;
#25 = Utf8 <clinit>
#26 = Utf8 SourceFile
#27 = Utf8 OuterClass.java
#28 = NameAndType #14:#15 // staticMem:Ljava/lang/String;
#29 = NameAndType #12:#13 // privateMem:I
#30 = NameAndType #16:#17 // "<init>":()V
#31 = Utf8 com/media/learn/service/OuterClass$InnerClass
#32 = NameAndType #16:#36 // "<init>":(Lcom/media/learn/service/OuterClass;)V
#33 = NameAndType #37:#17 // print:()V
#34 = Utf8 com/media/learn/service/OuterClass
#35 = Utf8 java/lang/Object
#36 = Utf8 (Lcom/media/learn/service/OuterClass;)V
#37 = Utf8 print
{
public com.media.learn.service.OuterClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #3 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field privateMem:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
void visit();
descriptor: ()V
flags:
Code:
stack=3, locals=1, args_size=1
0: new #4 // class com/media/learn/service/OuterClass$InnerClass
3: dup
4: aload_0
5: invokespecial #5 // Method com/media/learn/service/OuterClass$InnerClass."<init>":(Lcom/media/learn/service/OuterClass;)V
8: invokevirtual #6 // Method com/media/learn/service/OuterClass$InnerClass.print:()V
11: return
LineNumberTable:
line 8: 0
line 9: 11
static int access$000(com.media.learn.service.OuterClass);
descriptor: (Lcom/media/learn/service/OuterClass;)I
flags: ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field privateMem:I
4: ireturn
LineNumberTable:
line 3: 0
static java.lang.String access$100();
descriptor: ()Ljava/lang/String;
flags: ACC_STATIC, ACC_SYNTHETIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #1 // Field staticMem:Ljava/lang/String;
3: areturn
LineNumberTable:
line 3: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #7 // String staticMem
2: putstatic #1 // Field staticMem:Ljava/lang/String;
5: return
LineNumberTable:
line 5: 0
}
SourceFile: "OuterClass.java"
InnerClasses:
#10= #4 of #8; //InnerClass=class com/media/learn/service/OuterClass$InnerClass of class com/media/learn/service/OuterClass
这里就是jvm运行时的指令表。只看visit方法的:
void visit();
descriptor: ()V
flags:
Code:
stack=3, locals=1, args_size=1
0: new #4 // class com/media/learn/service/OuterClass$InnerClass
3: dup
4: aload_0
5: invokespecial #5 // Method com/media/learn/service/OuterClass$InnerClass."<init>":(Lcom/media/learn/service/OuterClass;)V
发现根本没有InnerClass什么事,都是OuterClass$InnerClass在跑,这就是jvm的小手段。
变一变
把InnerClass 加上static修饰,发现访问不了OuterClass的private变量了。把标红的去掉查看编译结果
class OuterClass$InnerClass {
OuterClass$InnerClass() {
}
void print() {
System.out.println("innerClass access and " + OuterClass.access$000());
}
}
构造方法没有入参了!这意味着,OuterClass$InnerClass 和 OuterClass完全没有了“联系”,InnerClass写在OuterClass内部和单独写成一个文件,效果是一摸一样的。
所以,android的同鞋是不是有种莫名的熟悉?解决内存泄漏常用的手段之一,声明为静态内部类。
内部匿名类和外部类的关系
非静态的方法实例匿名内部类
public class OuterClass {
private int privateMem = 1;
static String staticMem = "staticMem";
void visit() {
new Runnable() {
@Override
public void run() {
System.out.println("i have accessed " + privateMem + "and " + staticMem);
}
}.run();
}
}
在visit内部跑一个匿名Runnable。编译后
发现变成了OuterClass${num}这样的形式,jvm按顺序增加为匿名内部类生成class。
OuterClass$1.class
class OuterClass$1 implements Runnable {
OuterClass$1(OuterClass var1) {
this.this$0 = var1;
}
public void run() {
System.out.println("i have accessed " + OuterClass.access$000(this.this$0) + "and " + OuterClass.staticMem);
}
}
这个和内部类的情况一样。
静态的方法实例匿名内部类
发现privateMem也不给访问了。去掉标红的部分编译后
OuterClass$1.class
final class OuterClass$1 implements Runnable {
OuterClass$1() {
}
public void run() {
System.out.println("i have accessed and " + OuterClass.staticMem);
}
}
OuterClass$1 加了final修饰符,并且也不给构造方法加参了。所以访问不到privateMem。
So,Why?
static 的方法是属于类级别的,内部匿名类初始化的时候,如果需要外部类的实例,这个时候外部类的实例是没有的。所以构造方法不会有外部类的实例作为入参。