Java开发笔记(五十四)内部类和嵌套类
通常情况下,一个Java代码文件只定义一个类,即使两个类是父类与子类的关系,也要把它们拆成两个代码文件分别定义。可是有些事物相互之间密切联系,又不同于父子类的继承关系,比如一棵树会开很多花朵,这些花儿作为树木的一份子,它们依附于树木,却不是树木的后代。花朵不但拥有独特的形态,包括花瓣、花蕊、花萼等,而且拥有完整的生命周期,从含苞欲放到盛开绽放再到凋谢枯萎。这样一来,倘若把花朵抽象为花朵类,那么花朵类将囊括花瓣、花蕊、花萼等成员属性,以及含苞、盛开、凋谢等成员方法。既然花朵类如此规整,完全可以定义为一个class,但是花朵类又依附于树木类,说明它不适合从树木类独立出来。
为了解决这种依附关系的表达问题,自然就得打破常规思维,其实Java支持类中有类,在一个类的内部再定义另外一个类,仿佛新类是已有类的成员一般。一个类的成员包括成员属性和成员方法,还包括刚才说的成员类,不过“成员类”的叫法不常见,大家约定俗成的叫法是“内部类”,与内部类相对应,外层的类也可称作“外部类”。仍旧以前述的树木类和花朵类为例,如今可在树木类的内部增加定义花儿类,就像下面代码那样:
//演示内部类的简单定义 public class Tree { private String tree_name; public Tree(String tree_name) { this.tree_name = tree_name; } public void sprout() { System.out.println(tree_name+"发芽啦"); // 外部类访问它的内部类,就像访问其它类一样,都要先创建类的实例,再访问它的成员 Flower flower = new Flower("花朵"); flower.bloom(); } // Flower类位于Tree类的内部,它是个内部类 public class Flower { private String flower_name; public Flower(String flower_name) { this.flower_name = flower_name; } public void bloom() { System.out.println(flower_name+"开花啦"); } } }
从以上代码可见,外部类里面访问内部类Flower,就像访问其它类一样,都要先创建类的实例,再访问它的成员。至于在外面别的地方访问这里的外部类Tree,自然也跟先前的用法没什么区别。可是如果别的地方也想调用内部类Flower,那就没这么容易了,因为直接通过new关键字是无法创建内部类实例的。只有先创建外部类的实例,才能基于该实例去new内部类的实例,内部实例的创建代码格式形如“外部类的实例名.new 内部类的名称(...)”。下面是外部调用内部类的具体代码例子:
// 先创建外部类的实例,再基于该实例去创建内部类的实例 TreeInner inner = new TreeInner("桃树"); // 创建一个内部类的实例,需要在new之前添加“外层类的实例名.” TreeInner.Flower flower = inner.new Flower("桃花"); flower.bloom(); // 调用内部类实例的bloom方法
所谓好事多磨,引入内部类造成的麻烦不仅仅一个,还有另一个问题也挺棘手的。由于内部类是外部类的一个成员类,因此二者不可避免存在理论上的资源冲突。假设外部类与内部类同时拥有某个同名属性,比如它俩都定义了名叫tree_name的树木名称字段,那么在内部类里面,tree_name到底指的是内部类自身的同名属性,还是指外部类的同名属性呢?
从前面的类继承文章了解到,一旦遇到同名的父类属性、子类属性、输入参数,则编译器采取的是就近原则,例如在方法内部优先表示同名的输入参数,在子类内部优先表示同名的子类属性等等。同理,对于同名的内部类属性和外部类属性来说,tree_name在内部类里面优先表示内部类的同名属性。考虑到避免混淆的缘故,也可以在内部类里面使用“this.属性名”来表达内部类的自身属性。但如此一来,内部类又该怎样访问外部类的同名属性,确切地说,内部类Flower的定义代码应当如何调用外部类TreeInner的tree_name字段?显然这个问题足以让关键字this人格分裂,明明身在TreeInner里面,却代表不了TreeInner。为了拯救可怜的this,Java允许在this之前补充类名,从而限定此处的this究竟代表哪个类。譬如“TreeInner.this”表示的是外部类TreeInner自身,而“TreeInner.this.tree_name”则表示TreeInner的成员属性tree_name。于是在内部类里面终于能够区分内部类和外部类的同名属性了,详细的区分代码如下所示:
// 该方法访问内部类自身的tree_name字段 public void bloomInnerTree() { // 内部类里面的this关键字指代内部类自身 System.out.println(this.tree_name+"的"+flower_name+"开花啦"); } // 该方法访问外部类TreeInner的tree_name字段 public void bloomOuterTree() { // 要想在内部类里面访问外部类的成员,就必须在this之前添加“外部类的名称.” System.out.println(TreeInner.this.tree_name+"的"+flower_name+"开花啦"); }
当然多数场合没有这种外部与内部属性命名冲突的情况,故而在this前面添加类名纯属多此一举,只有定义了内部类,并且内部类又要访问外部类成员的时候,才需要显式指定this的归属类名。
苦口婆心地啰嗦了许久,内部类的小脾气总算搞定了。不料一波三折,之前说到其它地方调用内部类的时候,必须先创建外部类的实例,然后才能创建并访问内部类的实例。这个流程实在繁琐,好比我想泡一杯茉莉花茶,难道非得到田里种一株茉莉才行?很明显这么搞费时又费力,理想的做法是:只要属于对茉莉花的人为加工,而非紧密依赖于茉莉植株的自然生长,那么这个茉莉花类理应削弱与茉莉类的耦合关系。为了把新的类与类关系同外部类与内部类区分开来,Java允许在内部类的定义代码前面添加关键字static,表示这是一种静态的内部类,它无需强制绑定外部类的实例即可正常使用。
静态内部类的正式称呼叫“嵌套类”,外层类于它而言仿佛一层外套,有套没套不会对嵌套类的功能运用产生实质性影响,套一套的目的仅仅表示二者比较熟悉而已。下面是把Flower类改写为嵌套类的代码定义例子,表面上只加了一个static:
//演示嵌套类的定义 public class TreeNest { private String tree_name; public TreeNest(String tree_name) { this.tree_name = tree_name; } public void sprout() { System.out.println(tree_name+"发芽啦"); } // Flower类虽然位于TreeNest类的里面,但是它被static修饰,故而与TreeNest类的关系比起一般的内部类要弱。 // 为了与一般的内部类区别开来,这里的Flower类被叫做嵌套类。 public static class Flower { private String flower_name; public Flower(String flower_name) { this.flower_name = flower_name; } public void bloom() { System.out.println(flower_name+"开花啦"); } public void bloomOuterTree() { // 注意下面的写法是错误的,嵌套类不能直接访问外层类的成员 //System.out.println(TreeNest.this.tree_name+"的"+flower_name+"开花啦"); } } }
现在Flower类变成了嵌套类,别的地方访问它就会省点事,按照格式“new 外层类的名称.嵌套类的名称(...)”即可直接创建嵌套类的实例,不必画蛇添足先创建外层类的实例。完整的调用代码如下所示:
// 演示嵌套类的调用方法 private static void testNest() { // 创建一个嵌套类的实例,格式为“new 外层类的名称.嵌套类的名称(...)” TreeNest.Flower flower = new TreeNest.Flower("桃花"); flower.bloom(); }
正所谓有利必有弊,外部调用嵌套类倒是省事,嵌套类自身若要访问外层类就不能随心所欲了。原先花朵类作为内部类之时,通过前缀“外部类的名称.this”便可访问外部类的各项成员;现今花朵类摇身一变嵌套类,要访问外层的树木类不再容易了,对嵌套类而言,外层类犹如一个熟悉的陌生人,想跟它打招呼就像跟路人打招呼一样无甚区别,都得先创建对方的实例,然后才能通过实例访问它的每个成员。
迄今为止,这里已经介绍了好几种的类,它们相互之间的关系各异,通俗地说,子类与父类之间是继承关系,内部类与外部类之间是共存关系,嵌套类与外层类之间是同居关系。
更多Java技术文章参见《Java开发笔记(序)章节目录》