JAVA基础

JAVA基础 - 内部类

2019-01-29  本文已影响0人  HRocky

一、内部类是什么

将一个类A的定义放在另一个类B的内部,这个类A就是内部类。

二、内部类的分类

内部类有两种分类:静态内部类和非静态内部类。

1. 静态内部类

将内部类声明为static,那么这个类就是静态内部类,也称为嵌套类。

public class Outer {
    private static class  Inner {   
    }
}

类Inner就是一个静态内部类。

2. 非静态内部类

非静态内部类又可以分为如下几类:成员内部类(也可以称为普通内部类)、局部内部类、匿名内部类。

成员内部类(普通内部类)

把内部类当做外部类的一个成员,与属性和方法平级,这样的内部类就是成员内部类。

public class Outer {
     private class Inner {
     }
}

局部内部类

在代码块里创建的内部类,就称为局部内部类。典型的方式是在一个方法体的里面创建。

可以跟局部变量做一个类比来理解。

interface Counter {
    int next();
}

public class LocalInnerClass {
    private int count = 0;
    // 局部内部类
    class LocalCounter implements Counter {
        public int next() {
            return count++;
        }
    }

    return new LocalCounter();
}

上述代码中LocalCounter就是一个定义在方法体中的类,是一个局部内部类。

匿名内部类

匿名内部类从字面理解就是没有名字的内部类。什么叫没有名字呢?想想我们在Java中是如何定义一个类的?形如下面的形式:

class A {}

这个A就是我们定义的这个类的名字。既然叫做匿名,所以我们就可以大胆地猜测匿名类定义形式跟普通的这样定义是不一样的。

事实确实是这样,匿名类的定义语法有些奇怪,而且通常伴随着类实例的创建。

匿名内部类定义和实例化形式如下:

new 父类构造方法(参数){ 
      // 注:该方法名必须在父类中已经存在 
     修饰符 返回参数类型 方法名(参数列表){ 
     } 
}

看看下面的一个示例:

interface Contents {
    int value();
}

public class Outer {
    public Contents contents() {
        return new Contents() {// 插入了一个类定义
            private int i = 11;
            public int value() {
                return i;
            }
        }; // 注意这里需要分号
    }
}

上面的代码中我们定义了一个实现Contents接口的匿名内部类,同时伴随着这个匿名内部类的实例的返回。

三、内部类标识符

我们知道知道每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息,同样的,那么内部类也必须生成一个.class文件以包含它们的Class对象信息。

编译后内部类的命名的规则是这样的:
外围类的名字,加上"$",再加上内部类的名字。对于匿名内部类由于没有名字,编译器会简单地产生一个数字作为其标识符。

下面来看看,不同内部类编译后生产的类文件的命名:

1.实例一(成员内部类):

public class Parcel1 {  
    private class A {
    }
}

编译后生成的类文件为:


成员内部类.png

2.实例二(局部内部类):


interface A {
    int count();
}

public class Parcel2 {
    
    public A getA() {
        class A1 implements A {
            public int count() {
                return 10;
            }
        }
        
        return new A1();
    }
    
}

编译后生成的类文件为:


局部内部类.png

3.实例三(匿名内部类):


interface A {
    int count();
}

public class Parcel3 {
    
    public A getA() {
        
        return new A() {
            
            public int count() {
                return 10;
            }
            
        };

    }
    
}

编译后生成的类文件为:


匿名内部类.png

四、创建内部类的对象

想创建内部类的实例应该怎么操作?

局部内部类和匿名内部类这里就不用说怎么创建其实例了,主要来说明成员内部类和静态内部类类实例的创建。

关键点说明:

对于非静态内部类,在拥有外部类对象之前是不可能创建内部类对象的。因为内部类对象必须拥有一个外部类对象的引用。也就是说必须使用外部类的对象来创建该内部类对象。

对于静态内部类,它类似于一个静态方法,它跟外部类对象没有强制的关联关系,不需要拥有一个外部类对象的引用。

1. 使用.new创建普通内部类对象

public class Outer {
    private class Inner {
    }
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
    }
}

2.创建静态内部类对象

public class Outer {
    private static class Inner {
    }
    public static void main(String[] args) {
        Inner inner = new Inner();
    }
}

五、外部类和内部类的通信

将一个类定义在另一类的内部,稍微想一下就可以知道它们之间可能会有某种不平凡的关系。 没有特殊性,跟单独定义两个类一样,那么Java语言就不会推出这样的语法了。

对一个类的对象的操作无法就是访问其属性,操纵其方法,而且我们知道Java有访问修饰符来修饰属性和方法,这样来控制数据的可见性。我们举一个简单的例子:A和B是单独定义的两个类,A中定义了一个private类型的属性,我们知道B是无法访问到这个属性的。然后我们想想,有内部类这个概念,那么如果B定义在A的内部,是否可以访问到A的这个private属性呢?答案是肯定的。B在A内部定义,那么B就了解A的一切。

也就是说:内部类拥有其外围类的所有元素的访问权。

1. 成员内部类与外部类的通信

我们上面说到过成员内部类对象创建的前提是外部类对象先被创建了,然后用外部类的这个对象来创建内部类对象。也提到过为什么要这样,是因为内部类对象必须持有一个外部类对象的引用,那为什么有这样的规定了。这就涉及到了我们这里所说的对外围类元素访问的问题。

因为只有内部类对象拥有了外部类对象的引用,所以我们才拥有了对外部类对象所有元素的访问权。

当某个外围类的对象创建一个内部类对象时,此内部类对象必定会秘密得捕获一个指向那个外围类对象的引用。然后,在你访问此外围类的成员时,就是用那个引用来选择外围类的成员。

而这个外围类对象的引用的捕获是编译器为我们做的。

通过字节码文件看一下,有如下的代码:

public class Outer {
    private class Inner {
    }
}

编译后生成了Outer.class和Outer$Inner.class两个类文件,通过javap命令查看Outer$Inner.class的字节码内容,如下:

D:\>javap -verbose Outer$Inner.class
Classfile /D:/Outer$Inner.class
  Last modified 2019-1-28; size 295 bytes
  MD5 checksum 857baa55430362992ad8e2bb6b32fa84
  Compiled from "Outer.java"
class Outer$Inner
  SourceFile: "Outer.java"
  minor version: 0
  major version: 51
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#13         //  Outer$Inner.this$0:LOuter;
   #2 = Methodref          #4.#14         //  java/lang/Object."<init>":()V
   #3 = Class              #16            //  Outer$Inner
   #4 = Class              #19            //  java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               LOuter;
   #7 = Utf8               <init>
   #8 = Utf8               (LOuter;)V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               Outer.java
  #13 = NameAndType        #5:#6          //  this$0:LOuter;
  #14 = NameAndType        #7:#20         //  "<init>":()V
  #15 = Class              #21            //  Outer
  #16 = Utf8               Outer$Inner
  #17 = Utf8               Inner
  #18 = Utf8               InnerClasses
  #19 = Utf8               java/lang/Object
  #20 = Utf8               ()V
  #21 = Utf8               Outer
{
    final Outer this$0;
    flags: ACC_FINAL, ACC_SYNTHETIC
}

可以看到,这里有个内容:

final Outer this$0;

这个属性保存对外围类对象的引用。

2. 静态内部类与外部类的通信

上面我们已经提到过了,当内部类为静态的,要创建嵌套类的对象,并不需要其外围类的对象,那么静态内部类的对象中就不需要保存有指向外围类对象的引用。没有了这个引用,对外围类元素的访问权有所限制吗?答案是有的,像静态方法一样,静态内部类对象只能访问外围类对象的静态元素。

类比静态方法。不需要对象的引用,也能访问的就只有静态元素了。

静态内部类访问非静态外部类元素.png

六、非静态内部类和static

我们先来说规则:

非静态内部类不能有static的成员,除非它们是编译器常量。

再来说,为什么?

1. 以类加载角度说明

二进制类文件什么时候加载到JVM中呢?JVM没有明确规定加载的时机,但是规定了类初始化的时机,而类加载在类初始化之前,所以也就可以间接地知道类什么时候被加载。

初始化触发的时机有很多,这里我们只说跟我们要讨论的问题相关的几个动作:

类加载在初始化之前也就是说new和访问类静态字段的时候,类未加载要先加载。

我们上面已经提到过了,成员内部类对象是外部类的一个实例成员,只能通过外部类对象.new的方法进行创建,也就是说只有这个时候如果内部类未加载的话就进行加载。

但是如果可以定义成static,根据static访问的规则可以使用Outer
.Inner.静态字段的方式访问,这样也是可以加载类的,这样的话就不是和上面的规定互相矛盾了吗?

2. 从数据共享来说

内部类对象是外部类对象的一个实例属性,每一个内部类的实例彼此是独立的,没有任何的关系,假设可以定义静态的成员,那么这个静态成员就是多个内部类对象共享的。共享数据这违背了内部类对象彼此独立的原则。

3.从设计角度来说

从类设计的角度看也完全没有必要允许内部类定义静态成员。因为我们知道内部类拥有外部类成员的访问权。真的有这个需要,直接在外部类中定义静态成员就可以了。

七、局部内部类、匿名内部类和final

有这样的规定:

局部内部类、匿名内部类访问的局部变量必须由final修饰。

JDK8中不需要显示使用final修饰,稍后说明。

1. 局部内部类和final

interface Destination {
    String readLabel();
}

public class Parcel4 {
    public Destination destination(final String dest) {
        return new Destination() {
            private String label = dest;
            public String readLabel() {
                return label;
            }
        };
    }
}

如上代码,匿名内部类内部要使用方法的参数dest,那么这个dest必须是final修饰的。

在JDK7中进行编译,如果未用final修饰,会提示如下错误:

D:\>javac Parcel4.java
Parcel4.java:12: 错误: 从内部类中访问本地变量dest; 需要被声明为最终类型
                        private String label = dest;
                                               ^
1 个错误

2. 局部内部类和final

interface Counter {
    int next();
}

public class LocalInnerClass {
    
    Counter getCounter(final String name) { 
        class LocalCounter implements Counter {
            public int next() {
                System.out.println(name);
                return 0;
            }
        }
        
        return new LocalCounter();
    }   
}

在JDK7中进行编译,如果未用final修饰,会提示如下错误:

D:\>javac LocalInnerClass.java
LocalInnerClass.java:12: 错误: 从内部类中访问本地变量name; 需要被声明为最终类型
                                System.out.println(name);
                                                   ^
1 个错误

3. Java8 Effectively final 功能

Effectively final规则如下:

对于一个变量,如果没有给它加final修饰,而且没有对它的二次赋值,那么这个变量就是effectively final(有效的不会变的)。
A variable or parameter whose value is never changed after it is initialized is effectively final。

由于Java8新增的这个规则,所以上面的代码中,不需要显示的使用到final。

看下面的例子,使用jdk1.8但是进行了二次赋值,那么这个参数就不是effectively final的,编译器就会报错:

二次赋值.png

4. 为什么必须是final?

继续使用上面出现过的例子:

interface Counter {
    int next();
}

public class LocalInnerClass {  
    Counter getCounter(final String name) { 
        class LocalCounter implements Counter {
            public int next() {
                System.out.println(name);
                return 0;
            }   
        }
        return new LocalCounter();
    }
}

通过反编译查看生成的内部类字节码内容如下:

D:\>javap -verbose LocalInnerClass$1LocalCounter.class
Classfile /D:/LocalInnerClass$1LocalCounter.class
  Last modified 2019-1-29; size 744 bytes
  MD5 checksum b85fe0e51f962a9478db8f58e6df7273
  Compiled from "LocalInnerClass.java"
class LocalInnerClass$1LocalCounter implements Counter
  SourceFile: "LocalInnerClass.java"
  EnclosingMethod: #24.#25                // LocalInnerClass.getCounter
  InnerClasses:
       #34= #6; //LocalCounter=class LocalInnerClass$1LocalCounter
  minor version: 0
  major version: 51
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #6.#26         //  LocalInnerClass$1LocalCounter.this$0:LLocalInnerClass;
   #2 = Fieldref           #6.#27         //  LocalInnerClass$1LocalCounter.val$name:Ljava/lang/String;
   #3 = Methodref          #7.#28         //  java/lang/Object."<init>":()V
   #4 = Fieldref           #29.#30        //  java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #31.#32        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #33            //  LocalInnerClass$1LocalCounter
   #7 = Class              #36            //  java/lang/Object
   #8 = Class              #37            //  Counter
   #9 = Utf8               val$name
  #10 = Utf8               Ljava/lang/String;
  #11 = Utf8               this$0
  #12 = Utf8               LLocalInnerClass;
  #13 = Utf8               <init>
  #14 = Utf8               (LLocalInnerClass;Ljava/lang/String;)V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               Signature
  #18 = Utf8               ()V
  #19 = Utf8               next
  #20 = Utf8               ()I
  #21 = Utf8               SourceFile
  #22 = Utf8               LocalInnerClass.java
  #23 = Utf8               EnclosingMethod
  #24 = Class              #38            //  LocalInnerClass
  #25 = NameAndType        #39:#40        //  getCounter:(Ljava/lang/String;)LCounter;
  #26 = NameAndType        #11:#12        //  this$0:LLocalInnerClass;
  #27 = NameAndType        #9:#10         //  val$name:Ljava/lang/String;
  #28 = NameAndType        #13:#18        //  "<init>":()V
  #29 = Class              #41            //  java/lang/System
  #30 = NameAndType        #42:#43        //  out:Ljava/io/PrintStream;
  #31 = Class              #44            //  java/io/PrintStream
  #32 = NameAndType        #45:#46        //  println:(Ljava/lang/String;)V
  #33 = Utf8               LocalInnerClass$1LocalCounter
  #34 = Utf8               LocalCounter
  #35 = Utf8               InnerClasses
  #36 = Utf8               java/lang/Object
  #37 = Utf8               Counter
  #38 = Utf8               LocalInnerClass
  #39 = Utf8               getCounter
  #40 = Utf8               (Ljava/lang/String;)LCounter;
  #41 = Utf8               java/lang/System
  #42 = Utf8               out
  #43 = Utf8               Ljava/io/PrintStream;
  #44 = Utf8               java/io/PrintStream
  #45 = Utf8               println
  #46 = Utf8               (Ljava/lang/String;)V
{
  final java.lang.String val$name;
    flags: ACC_FINAL, ACC_SYNTHETIC

  final LocalInnerClass this$0;
    flags: ACC_FINAL, ACC_SYNTHETIC

  LocalInnerClass$1LocalCounter();
    flags:
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:LLocalInnerClass;
         5: aload_0
         6: aload_2
         7: putfield      #2                  // Field val$name:Ljava/lang/String;
        10: aload_0
        11: invokespecial #3                  // Method java/lang/Object."<init>":()V
        14: return
      LineNumberTable:
        line 9: 0
    Signature: #18                          // ()V

  public int next();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #2                  // Field val$name:Ljava/lang/String;
         7: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: iconst_0
        11: ireturn
      LineNumberTable:
        line 12: 0
        line 13: 10
}

注意下图中红框标记的内容:


字节码.png

看到没有,实际上内部类重新定义了一个跟参数有关联的内部字段来保存对形参引用的拷贝。

为什么要重新定义内部字段,其实也很好理解,内部类编译后是单独的一个类文件,上面我们提到过,内部类对象对外部类对象是非常熟知的,对于外部类的成员的访问,是通过定义一个字段来保存外部类对象的引用,通过这个引用来访问外部类成员。那么对于局部变量和形参了,同样也需要类似的处理。在内部类中定义外部这些对象的一个拷贝属性,然后在构造函数中进行初始化,这样当内部对象创建出来之后,就已经对外部环境了如指掌。

也就是:内部类的class文件的构造函数参数中会显示传入外部类对象以及方法内局部变量和形参。

现在问题就来了,假如没有规定必须final的而你又有在内部类中对传入的参数进行重新赋值的要求,而你在内部类中确实这么做了,但是因为内部类内部操作的其实是另外一个变量,你发现你根本就做不到这个需求。从代码角度看,会相当地疑惑,我操作的就是我想操作的变量,为什么操作不起效。

内部类的这种特殊处理我们没法从代码角度看出来,所有会造成让人疑惑的情况发生,而这种发生不深入底层字节码,是无法得到答案的。所以Java从设计上就屏蔽了允许在内部类中改变外部变量的这种需求。从而避免疑惑发生。

八、匿名内部类和构造器

我们知道匿名内部类是没有名字的,所以在匿名内部类中不可能有命名构造器,那么我们又需要拥有类似构造器这样的进行实例初始化的构造器行为,该怎么做呢?

答案是:通过实例初始化,就能够达到匿名内部类创建一个构造器的效果。

示例代码:

interface Destination {
    String readLabel();
}

public class Parcel5 {
    
    public Destination destination(final String dest, final float price) {
        return new Destination() {
            
            private int cost;
            private String label = dest;
            
            // Instance initialization for each object:
            {
                cost = Math.round(price);
                if (cost > 1000)
                    System.out.println("Over budget!");
            }
            
            public String readLabel() {
                return label;
            }
            
        };
    }

}

九、内部类的继承

先来回想一下在上面描述的章节中我们是如何创建内部类的对象的。在上面描述中我们说到,创建成员内部类对象之前先创建外部类对象,然后通过外部类对象.new内部类这样的方式来创建。为什么要这样?因为内部类和外部类要通信,必须保持有一个指向外部类对象的引用,而这个引用的赋值操作是在内部类的构造器中JVM为我们隐式地设置的。

外部类对象引用赋值.png

那现在如果我们继承了内部类定义了一个新类,这个类不是外部类的成员变量,而这个新类是内部类的子类,它当然也要对它父类的外部类熟知,那么它也必须保持有一个对外部类对象的引用,但是因为我们定义的这个新类并不是外部类的实例成员也就无法通过外部类对象.new内部类这样的语法创建,这个外部类引用就无法隐式地设置,所以解决的办法就是我们必须显示地设置。

示例如下:

class WithInner {
    class Inner {}
}

public class InheritInner extends WithInner.Inner {
    
    InheritInner(WithInner wi) {
        wi.super();
    }
    
    public static void main(String[] args) {
        WithInner wi = new WithInner();
        InheritInner ii = new InheritInner(wi);
    }

}

不能有无参构造器。

构造器类使用的语法为:

enclosingClassReference.super();

十、内部类的覆盖

先来看下面的示例代码:

class Egg {
    private Yolk y;
    protected class Yolk {
        public Yolk() {
            System.out.println("Egg.Yolk()");
        }
    }
    public Egg() {
        System.out.println("New Egg()");
        y = new Yolk();
    }
}

public class BigEgg extends Egg {
    
    public class Yolk {
        public Yolk() {
            System.out.println("BigEgg.Yolk()");
        }
    }
    
    public static void main(String[] args) {
        new BigEgg();
    }

}

输出是什么?

我们通过字节码文件来进行分析。

编译之后生成了如下四个class文件。

class文件.png

javap查看BigEgg.class,截取与我们研究相关的字节码部分如下:

BigEgg.png javap查看 Egg.png

Egg.class,截取与我们研究相关的字节码部分如下:

经过分析,输出的结果为:

New Egg()
Egg.Yolk()

这个例子说明:当继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间中。

十一、为什么要用内部类(待补充)

使用内部类会带来什么好处,也就是说它能帮我们解决什么问题。

1. 多重继承

Java语法规定了一个类不能继承多个类,也就是无法实现像C++那样的多重继承功能,但是如果我们确实需要,那我们可以通过内部类来实现。

2.解决设计问题

JDK的集合类大部分都有迭代器的功能,通过分析源码可以看到,都是在集合类的内部使用内部类来实现,定义实现迭代器接口的内部类来操作集合类中的数据。假设没有内部类这个功能,集合类的迭代器功能实现起来就非常地麻烦,使得API很难看。

题外话:这节的内容因为知识面还不够广,写出来的自己都感觉不怎么满意,暂且如此,待以后补充。

上一篇 下一篇

猜你喜欢

热点阅读