成员内部类就像是在外部类中定义了一个成员变量一样,因此成员内部类可以被public、protected、private或者包访问权限等多种权限修饰。同样的,如果是被public修饰的内部类,则可以在外部创建并调用,如果是被private修饰的内部类,则只能在外部类中创建并调用。
内部类中也可以定义若干字段与方法,它们同样可以被多种权限修饰,这点和普通的类是一样的。需要注意的一点是,如果在内部类中定义的某个字段与外部类的某个字段重名,则外部类的该字段默认会被隐藏。如果需要调用该外部类字段,需要采用 外部类.this.字段名 的形式去访问。
内部类可以无条件的访问外部类中的所有字段与方法,包括私有的。但外部类若想调用内部类的字段与方法,则必须先创建内部类的引用,通过该引用去访问相应字段或者方法。
最后需说明的一点是,内部类必须依托于外部类存在(静态内部类除外),因此创建一个内部类时,必须先创建外部类,然后通过外部类创建内部类。
可参照如下方法:
外部类 外部类变量名 = new 外部类(); 外部类.内部类 内部类变量名 = 外部类变量名.new 内部类();示例代码:
public class Main { public static void main(String[] args) { Outer t = new Outer(); //创建外部类对象 Outer.Inner r = t.new Inner(); //创建内部类对象 r.run(); t.outerdraw(); } } class Outer { public int a = 7; private String s = "gogogo"; public class Inner { //定义一个Inner内部类 public void run(){ //定义公开的run()方法 System.out.println("hhh"); } private void draw(){ //定义私有的draw()方法 System.out.println(a); System.out.println(s); } } public void outerdraw() { Inner r = new Inner(); r.draw(); //在外部类中定义公开的outerdraw()方法调用Inner的私有draw()方法 } }运行结果:
hhh 7 gogogo宽泛的说,局部内部类就是指那些定义在外部类的方法体中的类,这时你可能会想,既然内部类定义在外部类的方法内,那么内部类与该方法的生命周期是同步的么?其实是不同步的,但java通过一些方法解决了这个不同步带来的问题。关于这个下面会再做谈论,这里先关注局部内部类的实现。
另外,由于内部类定义在方法体内,因此内部类不能被public、private、static等权限修饰符修饰。你把它看作是一个方法体中的成员就好。
示例:
public class Main { public static void main(String[] args) { Outer t = new Outer(); t.run(); } } class Outer { private String s = "gogogo"; public void run() { class Inner { //局部内部类 private String s = "biubiubiu"; public void draw(){ System.out.println(Outer.this.s); //输出外部类的s System.out.println(s); //输出内部类的s } } Inner r = new Inner(); //这里创建一个Inner对象 r.draw(); } }运行结果:
gogogo biubiubiu顾名思义,静态内部类相比于一般的内部类只是多了个static修饰而已。静态内部类不依赖于外部类,因此可以被直接创建,另外,静态内部类不使用外部类的字段和方法,这是因为静态内部类可以在不依托于外部类的情况下被创建,如果这时去访问一个并不存在的外部类就会产生矛盾。
示例:
public class Main { public static void main(String[] args) { Outer.Inner r =new Outer.Inner(); //注意静态内部类的类名写法 r.draw(); //biubiubiu } } class Outer { static class Inner { private String s = "biubiubiu"; public void draw(){ System.out.println(s); } } }匿名内部类实际上就是匿名类。
匿名类就是没有名称的类,其名称由java编译器直接给出,一般形如 外部类 + $ + 匿名类顺序 的形式。由于匿名类没有名称因此也就无法被引用,无法被实例化,更不可能有构造方法,匿名类是“一次性用品”。
怎样定义一个匿名类呢?这里关于匿名类有两种实现方式,如下:
#1 利用父类的构造方法定义一个匿名类
public class Main { public static void main(String[] args) { Text2 t = new Text2(); t.run(new Text1(){ public void show(){ System.out.println("我是一个匿名类啊"); //我是一个匿名类啊 } }); } } abstract class Text1 { public abstract void show(); } class Text2 { public void run(Text1 t){ t.show(); } }通过这种方法得到的匿名类相当于父类的子类,本例以抽象类Text1作为父类,通过重写Text1的show()方法得到匿名类,并作为Text2类的run()方法的参数传入。
如果不是抽象类的话,那么调用了父类的构造方法的匿名类也将继承父类的所有非private方法,或者重写父类的方法。
如下:
public class Main { public static void main(String[] args) { Text2 t = new Text2(); t.run(new Text1(){}); //我是父类啊 } } class Text1 { public void show() { System.out.println("我是父类啊"); } } class Text2 { public void run(Text1 t){ t.show(); } }#2 利用接口的实现来定义一个匿名类
public class Main { public static void main(String[] args) { Text t = new Text(); t.run(new Itext(){ public void show(){ System.out.println("我是通过接口实现的匿名类啊"); } }); } } interface Itext { void show(); } class Text { public void run(Itext t){ t.show(); } }这种方法定义的匿名类需不需要实现接口中的所有方法呢?按道理说应该是不需要的,匿名类本来就是一次性用品,是为了编程方便才存在的,如果一个匿名类还有那么多条条框框不就失去了它存在的意义了?但是,我测试了一下,在JDK13环境和2019.3.版本的Eclipse环境下,就算匿名类未实现接口中的所有方法,运行仍然是正常的,但是在编译前IDE会提示那个错误的小叉子。
最后再提一点,因为匿名类是一次性的,因此它的类体内不允许存在静态成员或方法。
1. 内部类的字节码文件命名方式
java编译器对于内部类的字节码文件的命名方式与其它类不同,采用 外部类名$内部类名 的形式,比如:
class Outer { class Inner{ } }编译完如上代码后会产生两个字节码文件,一个是Outer.class,代表外部类,一个是Outer$inner.class,代表内部类。
另外,如果是匿名内部类的话,则因为内部类不存在名字,就采用编号的方式,从1开始,比如 Outer$1 。
2. 深入理解内部类的构造器
构造器就是构造方法,除了匿名内部类之外,其它的内部类都可以定义构造方法,尽管我在前面的所有例子中都没有用到内部类的构造方法,但是你要知道在内部类中定义构造方法是可行的。如果没有定义构造方法,那么java编译器也会自动为内部类生成一个默认的构造方法。这点所有的类几乎都是一样的。
但是我要说的是,不管是默认的构造方法还是有参的构造方法,java编译器都会默认给内部类的构造方法中传入一个参数,这个参数是一个指向外部类对象的引用(当然静态内部类除外,静态内部类基本和普通的类差不多,没什么特殊性),即便是匿名内部类,java编译器照样会在它的构造方法里传入这么一个引用参数,尽管我们不能在匿名内部类中定义构造方法,但匿名类是有构造方法的,而且它的参数是外部类的引用。
这也就不难理解为什么匿名类可以无条件的访问外部类中的字段和成员了,其实它是通过这个引用变量来访问的。
3.局部匿名类和匿名内部类为什么只能访问局部final变量?
先看一个例子:
public class Main { public static void main(String[] args) { } public void text() { final int a = 7; new Thread() { public void run() { System.out.println(a); } }.start(); } }在这个例子中,在方法text中定义了一个匿名内部类,该内部类是Thread的子类并重写了它的run()方法,在这个例子中,你会发现,Thread是有关线程方面的类,这里你不需要过多了解它,你只需要知道,有可能有些匿名类中的方法是需要执行很长时间的。
什么意思呢?如果一个匿名类中的方法在包含它的方法结束之后仍没有结束,这会造成什么影响?如本例,如果在text()方法结束之后,匿名类的run()方法仍在运行,而且该run()方法还需要使用text()方法域内的变量a。但是,按照我们所学,一个方法一旦结束,它的所有内部成员也会被销毁,因此a应该是不存在了的,那么匿名类的run()方法该怎么办呢?
这里,java使用复制副本的方法来解决这个问题,即如果有的方法变量是匿名类所需要使用的,且这些变量在编译前就可以确定,那么java会生成这些变量的副本并把它们放在匿名内部类的常量池中。意即匿名内部类使用的根本就不是原有的外部类方法中的变量,而是它们的副本。
这是对于变量值可以在编译前确定的情况,可是肯定有些变量是在编译前确定不了的,那么该怎么办呢?
此时参照这种方式:
public class Main { public static void main(String[] args) { } public void test(final int a) { new Thread(){ public void run() { System.out.println(a); }; }.start(); } }这种方法通过向外部类方法中传入一个final变量,这同时会在匿名内部类的构造方法传入该变量的副本,这就完成了在运行时动态的给无法确定的值赋值。
以上两种方式可以解决局部内部类和匿名内部类与外部类方法的生命周期不一致的问题,但同时会引入新的问题,因为数据是独立的,因此在内部类中对数据作出的更改不会作用到外部类方法中,这就有可能造成数据不一致的问题,因此java编译器限定匿名内部类和局部内部类只能访问final型的变量,意即禁止在内部类中被更改值。
在旧的java版本中,不加final是无法编译通过的,但新版的java已经做了一些优化,即便我们没有主动给这些将要在局部内部类或者匿名内部类中使用的变量限定为final,java也会自动为它们限定为final。
4.内部类的继承
内部类一般不被用作继承,但如果用作继承,参照如下代码:
public class Main { public static void main(String[] args) { Outer ot = new Outer(7); ExText Et = new ExText(ot); Et.getValue(); } } class Outer { private int a; Outer(int a){ //外部类构造方法 this.a = a; } public int getValue() { return a; } class Inner { int b; Inner(int b){ //内部类构造方法 this.b = ++b; } public void getValue() { System.out.println(b); } } } class ExText extends Outer.Inner { //该类继承自内部类 ExText(Outer ot){ //继承了内部类的类的构造方法中必须有指向外部类对象的引用 ot.super(ot.getValue()); //通过该外部类引用调用super(),注意这里的super()是内部类的构造方法, //而不是外部类,只是通过外部类调用 } }5.内部类的优点
每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。方便编写事件驱动程序。方便编写线程代码。