1.内部类的存在意义是什么?
如果有两个类A、B,关系紧密,且 B 类的设计初衷只是为了帮助 A 类的实现。那么类 B 可以作为类 A 的成员,也就是内部类。
2.有几种类型
我们知道,类的成员分为两种,静态成员和实例成员(非静态成员)。
那么内部类也就自然分为两种,静态内部类,成员内部类。
静态内部类是为了帮助外部类静态部分的实现,成员内部类是为了帮助外部类实例部分的实现。
我们还知道,静态成员与类关联,实例成员依赖于对象。
静态成员由类调用,实例成员由对象调用。
那么同样静态内部类与外部类关联,成员内部类与外部类的对象关联。
静态内部类通过外部类调用,成员内部类通过外部类对象调用。
还有两种内部类 1.局部内部类 2.匿名内部类,存在于方法中。本文不做讨论。
我们平时用的最多的应该就是匿名内部类了,有关匿名内部类,我在 https://blog.csdn.net/qq_44707077/article/details/117334860 中的 3.2.2 节有讨论,感兴趣的同学可以点进去看看。
3.成员内部类如何帮助外部类完成实现
3.1成员内部类可以访问外部类中所有方法及属性
先记住
当我们使用 new 关键字创建对象时,对应的几个字节码指令: new 、dup 、invokespecial
其中 invokespecial 指令是通过调用构造方法来创建对象。
现在我们来创建一个成员内部类,并通过反编译查看对应的字节码指令:
成员内部类的创建依赖于外部类的对象
StaticInnerTest.Inner inner = new StaticInnerTest().new Inner();
0: new #2 // class keyword/wordStatic/StaticInnerTest$Inner
3: dup
4: new #3 // class keyword/wordStatic/StaticInnerTest
7: dup
8: invokespecial #4 // Method keyword/wordStatic/StaticInnerTest."<init>":()V
11: dup
12: invokevirtual #5 判断非空,NPE // Method java/lang/Object.getClass:()Ljava/lang/Class;
15: pop 弹出
16: invokespecial #6 //Methodkeyword/wordStatic/StaticInnerTest$Inner."<init>":(Lkeyword/wordStatic/StaticInnerTest;)V
19: astore_1
20: return
解读反编译结果:
8: invokespecial #4:调用 StaticInnerTest 无参构造方法
16: invokespecial #6 :调用 StaticInnerTest$Inner 的有参构造方法,传入 StaticInnerTest 对象
可以看到,jvm 需要先创建一个外部类对象,并通过调用成员内部类有参构造方法,传入外部类对象的引用来创建成员内部类对象。成员内部类保存这个外部类引用。并通过这个外部类引用,直接访问外部类中所有的方法及属性,包括私有的。
成员内部类与外部类的关系为聚合
反编译结果中:12: 调用 getClass() 获取外部类的 class 对象,15: 又马上从操作栈中弹出。目的是为了检查外部类是否为空,防止以下的情况发生。
StaticInnerTest staticInnerTest = null;
StaticInnerTest.Inner inner = staticInnerTest.new Inner(); //成功编译。抛出NPE
3.2内部类如何访问外部类的私有属性/方法
public class StaticInnerTest {
private int i = 1;
private static int s = 2;
static class StaticInner{
public static void staticFunc(){
s = 3;
aVoid();
}
}
class Inner{
public void innerFunc(){
i = 3;
aVoid();
}
}
private static void aVoid(){
}
}
现在有外部类 StaticInnerTest、静态内部类 StaticInner 、成员内部类 Inner
其实对于 jvm 来说,没有什么内部类外部类的说法。为什么这么说呢?当我试着去对外部类进行 javac 编译,我得到了三个 .class 文件
因为 jvm 加载的是 .class 文件而不是 .java 文件。对于 jvm,所有类都是顶层类 top-level。在 class 文件中内部类与外部类是通过类文件属性表中的 InnerClasses(记录内部类) 、SourceFile(记录源文件) 属性进行绑定的。
内部类的初始化时机
既然对于 JVM 来说,三个类都是顶层类,那么 Inner 类和 StaticInner 类是如何访问 StaticInnerTest 的私有方法和私有属性呢。打开两个类的 .class 文件
Inner.class 文件:
class StaticInnerTest$Inner {
StaticInnerTest$Inner(StaticInnerTest var1) {
this.this$0 = var1;
}
public void innerFunc() {
StaticInnerTest.access$202(this.this$0, 3);
StaticInnerTest.access$100();
}
}
反编译结果
public void innerFunc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #1 // Field this$0:Lkeyword/wordStatic/StaticInnerTest;
4: iconst_3
5: invokestatic #3 // Method keyword/wordStatic/StaticInnerTest.access$202:(Lkeyword/wordStatic/StaticInnerTest;I)I
8: pop
9: invokestatic #4 // Method keyword/wordStatic/StaticInnerTest.access$100:()V
12: return
可以看出来,javac 编译器为 StaticInnerTest 动态创建了名为 access$xx 的静态方法,让内部类可以"突破"顶层类间不能直接访问对方的私有属性及方法的界限。
StaticInner.class 文件:
class StaticInnerTest$StaticInner {
StaticInnerTest$StaticInner() {
}
public static void staticFunc() {
StaticInnerTest.access$002(3);
StaticInnerTest.access$100();
}
}
4.静态内部类如何帮助外部类完成实现
静态内部类,或者叫它 静态嵌套类。它与外部类的关系不像成员内部类与外部类呈聚合关系,是一种更加松散,耦合度更低的关系。
静态嵌套类不保存外部类的引用,但外部类知晓静态嵌套类中所有的静态方法及属性,同样的静态内部类也可以访问外部类中所有静态的方法及属性。
那么静态嵌套类与外部类的关系应该是关联关系。关联的耦合度比聚合更低。
4.1 静态内部类如何访问外部类私有静态成员
与成员内部类一样,javac 为外部类动态生成 access$xxx 静态方法,由静态内部类调用。
4.2外部类如何访问静态内部类的私有静态成员
直接通过静态内部类名访问
5. JDK 中的内部类
5.1 JDK 中的静态内部类
例如 IntegerCache ,作为 Integer 的静态内部类,帮助 Integer 完成缓存池的功能。
Integer 访问静态内部类 IntegerCache 的私有静态属性
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache 源码,没有访问外部类的私有静态属性及方法
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
5.2 JDK 中的成员内部类
例如 ArrayList 中的 Itr 成员内部类,帮助 ArrayList 完成迭代器的功能。
ArrayList list = new ArrayList();
for (Object o : list) {
System.out.println(o);
}
成员内部类 Itr,访问 ArrayList 中的私有属性 modCount,size,elementData…
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
6.内部类的初始化时机
我们来了解类的初始化时机:
参考 《深入理解Java虚拟机》,当类被主动引用时,会触发类的初始化(加载、验证、准备阶段在此之前完成)
- new 字节码指令:javac 将 new 关键字编译为三个字节码指令 1.new 2.dup 3.invokespecial
- putstatic 字节码指令:修改类中的静态属性
- getstatic 字节码指令:访问类中的静态属性
- invokestatic 字节码指令:调用类中的静态方法
- 当初始化一个类时,如果发现其父类还没有被初始化,则先初始化其父类。
- 包含 main 方法的类
- 当使用java反射包中的方法对类型进行反射调用时:Class.forName(“xxx”)
- jdk8 后,当类实现的接口中含有 default 方法,这个类初始化时,包含 default 方法的接口也要初始化
- jdk7 后加入动态语言支持 invokedynamic ,第一次访问 MethodHandle 对应的类,且 MethodHandle 解析结果为 REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial 时,触发初始化
以上类的初始化时机,对于成员内部类来说,需要删掉 2-4 点,因为 java 规定成员内部类中不能存在静态属性 / 静态方法。
7.为什么成员内部类中不能有静态属性 / 静态方法呢?
参考:https://blog.csdn.net/bzq9012/article/details/17638207
java核心技术中提到:
内部类中声明的所有静态域都必须是final。原因很简单。我们希望一个静态域只有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不是final , 它可能就不是唯一的。
正常情况下,外部类与内部类关系如下。
我们使用 static 修饰一个成员,就是想让这个成员与类进行绑定,外部只能通过类名来访问这个成员。
如果成员内部类含有静态成员,那么就要让静态成员与成员内部类进行绑定。
我们知道成员内部类与外部类的对象关联。
但外部类对象是无法通过成员内部类的类名去访问其静态属性的。如果这个静态属性对于成员内部类来说确有意义,那 java 建议你使用 static 修饰内部类,成为静态内部类,然后通过外部类来访问静态内部类。