title:Java内部类
date:2017年11月11日18:57:15
对内部类其实也接触了挺多了的,在学习回调方法的时候我们接触了匿名内部类,在创建线程的时候我们也习惯在new Thread的时候直接创建一个Runnable对象实例。
Thread t1=new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"start");
try {
Thread.sleep(2000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName()+"finish");
}
},"t1");
还有在学习单例模式的时候,为了保证线程安全,我们通常会选用饿汉式单例,当然,我们要明白,最好的单例模式是使用枚举,因为枚举是天然的单例,具体的在之前的单例模式中有介绍。
public class Singleton3 {
private Singleton3() {
}
private static class SingleHolder {
public static Singleton3 holder = new Singleton3();
}
public static final Singleton3 getInstance() {
return SingleHolder.holder;
}
}
这些内部类的使用场景足以证明内部类的应用是十分广泛的,我们有必要深入了解一下内部类相关知识。
一.内部类的特性
在《Thinking in java》中,提到了内部类有如下特性:
内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立。
在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类。
这个在我们马上要讲的锁(Lock)的实现中就会出现,在独享锁ReentrantLock的实现中有两种实现,分别为公平锁和非公平锁,分别依赖于ReentrantLock中的两个内部类实现(这两个类都继承了Sync类,而Sync又继承了AbstractQueuedSynchronizer类(进行线程安全原子操作的类))。部分代码如下:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
- 创建内部类对象的时刻并不依赖于外围类对象的创建。
- 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 内部类提供了更好的封装,除了该外围类,其他类都不能访问。
二.四种内部类介绍
成员内部类
顾名思义,他和普通的类成员变量一样,
他是可以无限制的访问外围类的所有 成员属性和方法,尽管是private的,但是外围类要访问内部类的成员属性和方法则需要通过内部类实例来访问。
package com.wangcc.MyJavaSE.innerClass; public class OutClass { private String name; private int age; public OutClass(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "OutClass [name=" + name + ", age=" + age + "]"; } public class InnerClass { public void innerPrint() { age = 23; System.out.println(OutClass.this.toString());//(1) } } public InnerClass getInnerClass() { return new InnerClass(); } public static void main(String[] args) { OutClass outClass = new OutClass("kobe", 39); InnerClass innerClass = outClass.getInnerClass(); innerClass.innerPrint(); } }
我们运行程序,得到的输出:
OutClass [name=kobe, age=23]
注意,这个时候,如果我们将(1)改成: System.out.println(this.toString());
就会输出:
com.wangcc.MyJavaSE.innerClass.OutClass$InnerClass@15db9742
所以证明成员内部类是一个独立的类,他有着自己的this指针。
之前我们说到内部类可以访问到外部类的所有变量,那么为什么呢?下面就来探究一下
我们知道一个包含内部类的类编译之后会生出两个class文件。这里则是:OutClassInnerClass.class和OutClass.class,这里我们对OutClassInnerClass.class进行反编译:
javap -v OutClass$InnerClass.class >>C:\Coding\temp\inner.txt (输出cmd命令结果到文本中)
得到的文本如下:
Classfile /C:/Coding/WorkSpace/MyJavaSE/target/classes/com/wangcc/MyJavaSE/innerClass/OutClass$InnerClass.class Last modified 2017-11-11; size 886 bytes MD5 checksum 2abdd0575f95b91c87e476d303b5c05e Compiled from "OutClass.java" public class com.wangcc.MyJavaSE.innerClass.OutClass$InnerClass minor version: 0 major version: 49 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Class #2 // com/wangcc/MyJavaSE/innerClass/OutClass$InnerClass #2 = Utf8 com/wangcc/MyJavaSE/innerClass/OutClass$InnerClass #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 this$0 #6 = Utf8 Lcom/wangcc/MyJavaSE/innerClass/OutClass; #7 = Utf8 <init> #8 = Utf8 (Lcom/wangcc/MyJavaSE/innerClass/OutClass;)V #9 = Utf8 Code #10 = Fieldref #1.#11 // com/wangcc/MyJavaSE/innerClass/OutClass$InnerClass.this$0:Lcom/wangcc/MyJavaSE/innerClass/OutClass; #11 = NameAndType #5:#6 // this$0:Lcom/wangcc/MyJavaSE/innerClass/OutClass; #12 = Methodref #3.#13 // java/lang/Object."<init>":()V #13 = NameAndType #7:#14 // "<init>":()V #14 = Utf8 ()V #15 = Utf8 LineNumberTable #16 = Utf8 LocalVariableTable #17 = Utf8 this #18 = Utf8 Lcom/wangcc/MyJavaSE/innerClass/OutClass$InnerClass; #19 = Utf8 innerPrint #20 = Methodref #21.#23 // com/wangcc/MyJavaSE/innerClass/OutClass.access$0:(Lcom/wangcc/MyJavaSE/innerClass/OutClass;I)V #21 = Class #22 // com/wangcc/MyJavaSE/innerClass/OutClass #22 = Utf8 com/wangcc/MyJavaSE/innerClass/OutClass #23 = NameAndType #24:#25 // access$0:(Lcom/wangcc/MyJavaSE/innerClass/OutClass;I)V #24 = Utf8 access$0 #25 = Utf8 (Lcom/wangcc/MyJavaSE/innerClass/OutClass;I)V #26 = Fieldref #27.#29 // java/lang/System.out:Ljava/io/PrintStream; #27 = Class #28 // java/lang/System #28 = Utf8 java/lang/System #29 = NameAndType #30:#31 // out:Ljava/io/PrintStream; #30 = Utf8 out #31 = Utf8 Ljava/io/PrintStream; #32 = Methodref #3.#33 // java/lang/Object.toString:()Ljava/lang/String; #33 = NameAndType #34:#35 // toString:()Ljava/lang/String; #34 = Utf8 toString #35 = Utf8 ()Ljava/lang/String; #36 = Methodref #37.#39 // java/io/PrintStream.println:(Ljava/lang/String;)V #37 = Class #38 // java/io/PrintStream #38 = Utf8 java/io/PrintStream #39 = NameAndType #40:#41 // println:(Ljava/lang/String;)V #40 = Utf8 println #41 = Utf8 (Ljava/lang/String;)V #42 = Utf8 SourceFile #43 = Utf8 OutClass.java #44 = Utf8 InnerClasses #45 = Utf8 InnerClass { final com.wangcc.MyJavaSE.innerClass.OutClass this$0; descriptor: Lcom/wangcc/MyJavaSE/innerClass/OutClass; flags: ACC_FINAL, ACC_SYNTHETIC public com.wangcc.MyJavaSE.innerClass.OutClass$InnerClass(com.wangcc.MyJavaSE.innerClass.OutClass); descriptor: (Lcom/wangcc/MyJavaSE/innerClass/OutClass;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #10 // Field this$0:Lcom/wangcc/MyJavaSE/innerClass/OutClass; 5: aload_0 6: invokespecial #12 // Method java/lang/Object."<init>":()V 9: return LineNumberTable: line 18: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/wangcc/MyJavaSE/innerClass/OutClass$InnerClass; public void innerPrint(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #10 // Field this$0:Lcom/wangcc/MyJavaSE/innerClass/OutClass; 4: bipush 23 6: invokestatic #20 // Method com/wangcc/MyJavaSE/innerClass/OutClass.access$0:(Lcom/wangcc/MyJavaSE/innerClass/OutClass;I)V 9: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream; 12: aload_0 13: invokevirtual #32 // Method java/lang/Object.toString:()Ljava/lang/String; 16: invokevirtual #36 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 19: return LineNumberTable: line 20: 0 line 21: 9 line 22: 19 LocalVariableTable: Start Length Slot Name Signature 0 20 0 this Lcom/wangcc/MyJavaSE/innerClass/OutClass$InnerClass; } SourceFile: "OutClass.java" InnerClasses: public #45= #1 of #21; //InnerClass=class com/wangcc/MyJavaSE/innerClass/OutClass$InnerClass of class com/wangcc/MyJavaSE/innerClass/OutClass
我们注意看这一段:
final com.wangcc.MyJavaSE.innerClass.OutClass this$0; descriptor: Lcom/wangcc/MyJavaSE/innerClass/OutClass; flags: ACC_FINAL, ACC_SYNTHETIC public com.wangcc.MyJavaSE.innerClass.OutClass$InnerClass(com.wangcc.MyJavaSE.innerClass.OutClass);
我们看第一行:定义了指向外部对象的一个指针。
编译器会默认为成员内部类添加了一个指向外部类对象的引用,然后再上面片段的最后一行进行赋值。我们观察Java类,发现我们并没显示的实现一个带有外部对象作为参数的构造方法,所以默认应该是只有一个无参的构造方法才对呀,但是编译器还是会默认会为构造方法添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的OutClass this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对OutClass this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。所以也就有:
第一:成员内部类中不能存在任何static的变量和方法;第二:成员内部类是依附于外围类的,所以只有先创建了外围类才能够创建内部类。
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。我们看一个例子。
package com.wangcc.MyJavaSE.innerClass; class People { private int age; public People(int age) { this.age = age; } public int getAge() { return age; } public People() { } } class Man { public People getWoman() { class Woman extends People { // 局部内部类 public Woman(int age) { super(10); } } return new Woman(24); } } public class PartClass { public static void main(String[] args) { Man man = new Man(); People woman = man.getWoman(); System.out.println(woman.getAge()); } }
匿名内部类
匿名类一般用来做监听器(回调方法)使用,这个在我们之前讲的MQ API中已经说过了,匿名类再实现回调方法的时候是经常使用,可以看看之前的MQ系列。
对于匿名内部类,有一些他独特的地方:
它是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。匿名内部类也是不能有访问修饰符和static修饰符的。
我们之前在使用匿名内部类的时候,对于内部类中的方法里的参数的修饰和局部变量的修饰都需要使用
final,大家有没有想过这到底是为什么呢。我们来通过一个例子来分析一下,依然是通过反编译:
我们之前说过了,我们在开启线程的时候经常就会使用内部类。
package com.wangcc.MyJavaSE.innerClass;
public class AnonymousInnerClassTest2 {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Main Thread");
AnonymousInnerClassTest2 test = new AnonymousInnerClassTest2();
test.test(20);
System.out.println("Main End");
}
public void test(final int b) {
final int a = 10;
new Thread(new Runnable() {
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName());
System.out.println(a);
System.out.println(b);
}
}, "t1").start();
;
}
}
我们运行这个类,发现编译后生成的class文件也有两个,AnonymousInnerClassTest21.class和AnonymousInnerClassTest2.class,很明显我们的匿名内部类是AnonymousInnerClassTest21.class我们反编译一下
Classfile /C:/Coding/WorkSpace/MyJavaSE/target/classes/com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1.class
Last modified 2017-11-11; size 1083 bytes
MD5 checksum f380d6128102e0eaa6244a78a84d9dfc
Compiled from "AnonymousInnerClassTest2.java"
class com.wangcc.MyJavaSE.innerClass.AnonymousInnerClassTest2$1 implements java.lang.Runnable
minor version: 0
major version: 49
flags: ACC_SUPER
Constant pool:
#1 = Class #2 // com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1
#2 = Utf8 com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Class #6 // java/lang/Runnable
#6 = Utf8 java/lang/Runnable
#7 = Utf8 this$0
#8 = Utf8 Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;
#9 = Utf8 val$b
#10 = Utf8 I
#11 = Utf8 <init>
#12 = Utf8 (Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;I)V
#13 = Utf8 Code
#14 = Fieldref #1.#15 // com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1.this$0:Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;
#15 = NameAndType #7:#8 // this$0:Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;
#16 = Fieldref #1.#17 // com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1.val$b:I
#17 = NameAndType #9:#10 // val$b:I
#18 = Methodref #3.#19 // java/lang/Object."<init>":()V
#19 = NameAndType #11:#20 // "<init>":()V
#20 = Utf8 ()V
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1;
#25 = Utf8 run
#26 = Fieldref #27.#29 // java/lang/System.out:Ljava/io/PrintStream;
#27 = Class #28 // java/lang/System
#28 = Utf8 java/lang/System
#29 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Methodref #33.#35 // java/lang/Thread.currentThread:()Ljava/lang/Thread;
#33 = Class #34 // java/lang/Thread
#34 = Utf8 java/lang/Thread
#35 = NameAndType #36:#37 // currentThread:()Ljava/lang/Thread;
#36 = Utf8 currentThread
#37 = Utf8 ()Ljava/lang/Thread;
#38 = Methodref #33.#39 // java/lang/Thread.getName:()Ljava/lang/String;
#39 = NameAndType #40:#41 // getName:()Ljava/lang/String;
#40 = Utf8 getName
#41 = Utf8 ()Ljava/lang/String;
#42 = Methodref #43.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#43 = Class #44 // java/io/PrintStream
#44 = Utf8 java/io/PrintStream
#45 = NameAndType #46:#47 // println:(Ljava/lang/String;)V
#46 = Utf8 println
#47 = Utf8 (Ljava/lang/String;)V
#48 = Methodref #43.#49 // java/io/PrintStream.println:(I)V
#49 = NameAndType #46:#50 // println:(I)V
#50 = Utf8 (I)V
#51 = Utf8 SourceFile
#52 = Utf8 AnonymousInnerClassTest2.java
#53 = Utf8 EnclosingMethod
#54 = Class #55 // com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2
#55 = Utf8 com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2
#56 = NameAndType #57:#50 // test:(I)V
#57 = Utf8 test
#58 = Utf8 InnerClasses
{
final com.wangcc.MyJavaSE.innerClass.AnonymousInnerClassTest2 this$0;
descriptor: Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;
flags: ACC_FINAL, ACC_SYNTHETIC
com.wangcc.MyJavaSE.innerClass.AnonymousInnerClassTest2$1(com.wangcc.MyJavaSE.innerClass.AnonymousInnerClassTest2, int);
descriptor: (Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;I)V
flags:
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: aload_1
2: putfield #14 // Field this$0:Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2;
5: aload_0
6: iload_2
7: putfield #16 // Field val$b:I
10: aload_0
11: invokespecial #18 // Method java/lang/Object."<init>":()V
14: return
LineNumberTable:
line 1: 0
line 15: 10
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1;
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokestatic #32 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
6: invokevirtual #38 // Method java/lang/Thread.getName:()Ljava/lang/String;
9: invokevirtual #42 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
15: bipush 10
17: invokevirtual #48 // Method java/io/PrintStream.println:(I)V
20: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_0
24: getfield #16 // Field val$b:I
27: invokevirtual #48 // Method java/io/PrintStream.println:(I)V
30: return
LineNumberTable:
line 19: 0
line 20: 12
line 21: 20
line 22: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this Lcom/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1;
}
SourceFile: "AnonymousInnerClassTest2.java"
EnclosingMethod: #54.#56 // com.wangcc.MyJavaSE.innerClass.AnonymousInnerClassTest2.test
InnerClasses:
#1; //class com/wangcc/MyJavaSE/innerClass/AnonymousInnerClassTest2$1
我们注意看run方法那一段:有一条指令
bipush 10
这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。
然后我们注意看构造方法:构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量test方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。
也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?
对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
静态内部类
静态内部类我们见的非常多了,在学习HashMap源码时,我们就有说到他的底层实现是散列数组加链表(JDK8在链表长度大于8之后会变成红黑树)其中的散列数组的元素Node就是用HashMap中的一个静态内部类实现的。
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。