https://mp.weixin.qq.com/s/IDKUmQB5Na-dSFwDag0pmA
Java内部类你真的会吗?
Java架构研究室 5天前
一、四种内部类
1.1、成员内部类
成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:
1 public class OuterAndInnerClass { 2 public static void main(String[] args) { 3 Outer outer = new Outer(); 4 //创建内部类的两种方式 (1)在外部 5 //Outer.Inner inner = outer.new Inner(); 6 //创建内部类的两种方式 (1)在内部类所依附的外部类中创建 7 Outer.Inner inner =outer.getInnerClass(); 8 outer.out(); 9 inner.in(); 10 inner.testInner(); 11 } 12 } 13 class Outer{//外部类 14 public Inner getInnerClass(){ 15 return new Inner(); 16 } 17 String outName = "外部类"; 18 String sameName = "同名外部"; 19 public void out(){ 20 System.out.println("外部方法"); 21 } 22 class Inner{//内部类 23 String inName = "内部类"; 24 String sameName = "同名内部"; 25 String name = "内部类变量"; 26 public void in(){ 27 System.out.println("内部方法"); 28 } 29 public void testInner(){ 30 String name = "局部变量"; 31 System.out.println(name);//#内部类变量 32 System.out.println(this.name);//#局部变量 33 System.out.println("outName:" + outName);//#outName:外部类 34 System.out.println("inName:" + inName);//#inName:内部类 35 System.out.println("sameName:" + sameName);//#sameName:同名内部 36 System.out.println("sameName:" + this.sameName);//#sameName:同名内部,this指向Inner 37 System.out.println("sameName:" + Outer.this.sameName);//#sameName:同名外部 38 } 39 } 40 }
1.1.1,创建成员内部类的方法有两种
虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:
Outer outer = new Outer(); 第一种方式:Outer.Inner inner= outer.new Inner(); 第二种方式:Outer.Inner inner= outer.getInnerClass();
1.1.2,成员内部类的访问控制修饰符
内部类就如同外部类的成员变量一样。四种访问控制符都是可以的,public,default,protected,private。
内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。
1.1.3,成员内部类调用外部类的成员变量或者方法
调用内部类的成员变量:
System.out.println("inName:" + inName);//#inName:内部类
调用外部类的成员变量:(同调用内部类的成员变量)
System.out.println("outName:" + outName);//#outName:外部类
TIPS: 特殊情况:当外部类和内部类的成员变量同名的情况 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问: 外部类.this.成员变量 外部类.this.成员方法
1.2、局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
1 //局部内部类 2 public class LocalInnerClass { 3 public static void main(String[] args) { 4 People people = new People(); 5 people.getWoman("形参变量"); 6 } 7 } 8 class People { 9 String peopleName = "people"; 10 String sameName = "外部同名变量"; 11 public People getWoman(final String methodName){ 12 final String localName = "局部变量"; 13 class Woman extends People{ 14 String womanName = "woman"; 15 String sameName = "局部内部类同名变量"; 16 public Woman(){ 17 //methodName = "";//编译错误:Cannot assign a value to final variable 'methodName' 18 //localName = "";//编译错误:Variable 'localName' is accessed from within inner class, needs to be final or effectively final 19 System.out.println(methodName);//#形参变量 20 System.out.println(localName);//#局部变量 21 System.out.println(peopleName);//#people 22 System.out.println(womanName);//#woman 23 System.out.println(sameName);//#局部内部类同名变量 24 System.out.println(this.sameName);//#局部内部类同名变量 25 System.out.println(People.this.sameName);//#外部同名变量 26 } 27 } 28 return new Woman(); 29 } 30 }
在局部内部类中调用外部类的变量或者方法的方式和规则是一样的。
TIPS 值得注意的是,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。
1.3、匿名内部类
匿名内部类由于没有名字,所以它的创建方式有点儿奇怪。创建格式如下:
new 父类构造器(参数列表)|实现接口() { //匿名内部类的类体部分 }
在这里我们看到使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用。当然这个引用是隐式的。
1 //匿名内部类 2 public class AnonInnerClass { 3 public static void useRunnable(MyRunnable runnable){ 4 runnable.run(); 5 } 6 public static void main(String[] args) { 7 AnonInnerClass.useRunnable(new MyRunnable() { 8 @Override 9 public void run() { 10 System.out.println("重写run方法"); 11 } 12 }); 13 AnonInnerClass.useRunnable(new MyRunnable("name") { 14 @Override 15 public void run() { 16 System.out.println("重写run方法"); 17 } 18 }); 19 } 20 } 21 abstract class MyRunnable { 22 public MyRunnable(){ 23 System.out.println("调用匿名内部类的无参构造器"); 24 } 25 public MyRunnable(String name){ 26 System.out.println("调用匿名内部类的有参构造器,参数为:" + name); 27 } 28 //抽象方法 29 public abstract void run(); 30 }
这里我们能够看到,useRunnable 方法要接受一个MyRunnable的实例参数,但是,MyRunnable是一个抽象的类,不能被实例化,所以只能创建一个新的类继承这个MyRunnable类,然后指向MyRunnable,从而拿到MyRunnable的实例参数。
在这里JVM会创建一个继承自MyRunnable类的匿名类的对象,该对象转型为对MyRunnable类型的引用。
对于匿名内部类的使用它是存在一个缺陷的,就是它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失,所以匿名内部类是不能够被重复使用。对于上面的实例,如果我们需要对test()方法里面内部类进行多次使用,建议重新定义类,而不是使用匿名内部类。
TIPS:
1、使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
2、匿名内部类中是不能定义构造函数的。(类都是匿名的,没法定义构造方法)
3、匿名内部类中不能存在任何的静态成员变量和静态方法。(类是匿名的,当然没有类方法或类变量)
4、匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
5、匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
6、我们给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须要为final。也就是说:当所在的方法的形参需要被内部类里面使用时,该形参必须为final。
7、匿名内部类的初始化(使用构造代码块)
我们一般都是利用构造器来完成某个实例的初始化工作的,但是匿名内部类是没有构造器的!那怎么来初始化匿名内部类呢?使用构造代码块!利用构造代码块能够达到为匿名内部类创建一个构造器的效果。
1 public class InitAnonInnerClass { 2 public static void main(String[] args) { 3 OuterClass outer = new OuterClass(); 4 InnerClass inner1 = outer.getInnerClass(15, "变了"); 5 System.out.println(inner1.getStr()); 6 InnerClass inner2 = outer.getInnerClass(20, "变了"); 7 System.out.println(inner2.getStr()); 8 } 9 } 10 11 class OuterClass { 12 public InnerClass getInnerClass(final int num, final String str){ 13 return new InnerClass() { 14 int num_ ; 15 String str_ ; 16 //使用构造代码块完成初始化 17 { 18 if(0 < num && num < 18){ 19 //str = "";//编译错误Variable 'str' is accessed from within inner class, needs to be final or effectively final 20 str_ = str; 21 }else { 22 str_ = "没变啊"; 23 } 24 } 25 public String getStr(){ 26 return str_; 27 } 28 }; 29 } 30 } 31 32 abstract class InnerClass { 33 public abstract String getStr(); 34 } out:
变了
没变啊
1.4、静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
1 //静态内部类 2 public class StaticInnerClass { 3 public static void main(String[] args) { 4 //初始化静态内部类,注意和其它内部类的初始化方式的区别 5 OuterClass1.InnerClass inner = new OuterClass1.InnerClass(); 6 inner.test(); 7 } 8 } 9 class OuterClass1 { 10 String outName = "我是外部类"; 11 static String outType = "外部类"; 12 static class InnerClass { 13 String innerName = "我是内部类"; 14 static String innerType = "静态内部类"; 15 public InnerClass (){ 16 //System.out.println(outName);//编译错误:Non-static field 'outName' cannot be referenced from a static context 17 System.out.println(outType); 18 } 19 public void test(){ 20 System.out.println("调用内部类方法"); 21 } 22 } 23 }
如上所示创建静态内部类对象的一般形式为:
外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
二、深入理解内部类
2.1.为什么成员内部类可以无条件访问外部类的成员?
在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是OuterAndInnerClass.java的代码:
1 public class OuterAndInnerClass { 2 public static void main(String[] args) { 3 Outer outer = new Outer(); 4 Outer.Inner inner = outer.getInnerClass(); 5 outer.out(); 6 } 7 } 8 9 class Outer{ 10 public Inner getInnerClass(){ 11 return new Inner(); 12 } 13 public void out(){ 14 System.out.println("外部方法"); 15 } 16 class Inner{ 17 public void in(){ 18 System.out.println("内部方法"); 19 } 20 } 21 }
编译之后,出现了两个字节码文件:
编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?
虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。
2.2、为什么局部内部类和匿名内部类只能访问局部final变量?
1 public class Test { 2 public static void main(String[] args) { 3 Test test = new Test(); 4 test.test(1); 5 } 6 7 public void test(final int b) { 8 final int a = 10; 9 new Thread(){ 10 public void run() { 11 // a = 2;//编译错误:Cannot assign a value to final variable 'b' 12 // b = 3;//编译错误:Cannot assign a value to final variable 'b' 13 System.out.println(a); 14 System.out.println(b); 15 }; 16 }.start(); 17 } 18 }
当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。
也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?
对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。
文章到此就结束了,如果你觉得文章还不错,欢迎大家关注下方公众号查看更多干货好文,感谢你的阅读。