第六章 访问权限控制
-
包:库单元
包:包内包含有一组类,它们在单一的名字空间之下被组织在一起。
编译:当编译一个.java文件时,在.java文件中的每个类都会有一个输出文件,而该输出文件的名称与.java文件中的每个类的名称相同,只是多了一个.class后缀名。 Java可运行程序是一组可以打包并压缩为一个Java文档文件(JAR,使用Java的jar文档生成器)的.class文件。Java解释器负责这些文件的查找,装载和解释。
代码组织:类库实际上是一组类文件,其中每个文件都有一个public类(此类必须与文件名相同),以及任意数量的非public类,因此每个文件都有一个构件。如果希望这些构件(每一个都有它们自己的独立的.java和.class文件)从属于同一个群组,就可以使用关键字package。如果使用package语句,它必须是文件中除注释以外的第一句程序代码。在文件起始处写://这表示你在声明该编译单元是名为access的类库的一部分 package access;
牢记 package和import关键字允许你做的,是将单一的全局名字空间分开,使得无论多少人使用Internet以及Java开始编写类,都不会出现名称冲突问题。
独一无二的包名:package名称的第一部分是类的创建者的反顺序的Internet域名,第二部分是把package名称分解为你的机器上的一个目录。
路径:放置在环境变量CLASSPATH可以找到的地方。 -
import static
对于自制工具库,可以使用静态import语句import static来导入,以在你的系统上使用静态的自己定义的方法。静态import语句简化了方法的使用:import static java.lang.System.out; import static java.lang.Integer.*; public class TestStaticImport { public static void main(String[] args) { out.println(MAX_VALUE); out.println(toHexString(42)); } } /*Output: 2147483647 2a */
-
Java访问权限修饰词
在Thinking in Java读书笔记(一)的第一章的第1条笔记就提到了public、protected、private这几个修饰词。修饰词 本类 同一个包的类 子类 其他类 public √ √ √ √ protected √ √ √ × default √ √ × × private √ × × × 默认权限是包访问权限。
第七章 复用类
-
复用类
通过创建新类来复用代码,而不必从头开始编写。有两种方法:组合语法和继承语法。
组合语法通常用于想在新类中使用现有类的功能而非它的接口这种情形。
继承语法是使用某个现有类,并开发一个它的特殊版本,通常这意味着你在使用一个通用类,并为了某种特殊需要而将其特殊化。 -
组合语法
只需将对象引用置于新类中即可。对于非基本类型的对象,必须将其引用置于新类中,但可以直接定义基本类型数据。class WaterSource { private String s; WaterSource() { System.out.println("WaterSource()"); s = "Constructed"; } public String toString() {return s;} } public class SprinklerSystem { private String value1, value2, value3, value4; private WaterSource source = new WaterSource(); private int i; private float f; public String toString() { return "value1 = " + value1 + " " + "value2 = " + value2 + " " + "value3 = " + value3 + " " + "value4 = " + value4 + "\n" + "i = " + i + " " + "f = " + f + " " + "source = " + source; } public static void main(String[] args) { SprinklerSystem sprinklers = new SprinklerSystem(); System.out.println(sprinklers); } } /*Output: WaterSource() value1 = null value2 = null value3 = null value4 = null i = 0 f = 0.0 source = Constructed */
这里的toString()方法很特别,每一个非基本类型的对象都有一个toString()方法,而且当编译器需要一个String而你却只有一个对象时,该方法会被自动调用。
-
初始化引用
下列位置可进行引用的初始化:
①在定义对象的地方,这意味着他们总是能够在构造器被调用之前被初始化。
②在类的构造器中。
③就在正要使用这些对象之前,这种方法称为惰性初始化。在生成对象不值得及不必每次都生成对象的情况下,这种方式可以减少额外的负担。
④使用实例初始化。
示例:class Soap { private String s; Soap() { System.out.println("Soap()"); s = "Constructed"; } public String toString() {return s;} } public class Bath { //定义的同时初始化 private String s1 = "Happy", s2 = "Happy", s3, s4; private Soap castille; private int i; private float toy; public Bath() { System.out.println("Inside Bath()"); //构造器内初始化 s3 = "Joy"; toy = 3.14f; castille = new Soap(); } { i = 47; } //实例初始化 public String toString() { if(s4 == null) //惰性初始化 s4 = "Joy"; return "s1 = " + s1 + "\n" + "s2 = " + s2 + "\n" + "s3 = " + s3 + "\n" + "s4 = " + s4 + "\n" + "i = " + i + "\n" + "toy" + toy + "\n" + "castlle = " + castille; } public static void main(String[] args) { Bath b = new Bath(); System.out.print(b); } } /*Output: Inside Bath() Soap() s1 = Happy s2 = Happy s3 = Joy s4 = Joy i = 47 toy3.14 castlle = Constructed */
-
继承语法
声明语法:class SubClass extends SuperClass {/*...*/}
一般情况下,为了方便继承,SuperClass的所有数据成员都指定为private,所有的方法指定为public。
super关键字表示超类,可以帮助子类调用父类的方法。 -
main()
可以为每个类都创建一个main()方法。即使是一个程序中含有多个类,也只有命令行所调用的那个类的main()方法会被调用(即使被调用的类不是public类)。 -
初始化基类
子类会在构造器中调用基类构造器来执行初始化,而基类构造器具有执行基类初始化所需要的所有知识和能力。
不带参数的构造器:即使你不为子类创建构造器,编译器也会为你合成一个默认构造器,该构造器将调用基类的构造器。
带参数的构造器:如果没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须使用关键字super显式地编写调用基类构造器的语句,并且配以适当的参数列表。不然编译器会抱怨找不到这个基类构造器。 -
代理
组合和继承的中庸之道。我们将一个成员对象置于所要构造的类中(就像组合),但与此同时我们在新类中暴露了该成员对象的所有方法(就像继承)。示例://: SpaceShipControls.java public class SpaceShipControls { void up(int velocity) {} void down(int velocity) {} void left(int velocity) {} void right(int velocity) {} void forward(int velocity) {} void back(int velocity) {} void turboBoost() {} } =================================== //: SpaceShipDelegation.java public class SpaceShipDelegation { private String name; private SpaceShipControls controls = new SpaceShipControls(); //这里像是组合 public SpaceShipDelegation(String name) { this.name = name; } //Delegated methods 就像继承一样暴露所有的方法 public void back(int velocity) { controls.back(velocity); } public void down(int velocity) { controls.down(velocity); } public void forward(int velocity) { controls.forward(velocity); } public void left(int velocity) { controls.left(velocity); } public void right(int velocity) { controls.right(velocity); } public void up(int velocity) { controls.up(velocity); } public void turboBoost() { controls.turboBoost(); } public static void main(String[] args) { SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector"); protector.forward(100); } }
-
@Override注解
Java SE5新增加@Override注解,它并不是关键字,但是可以把它当作关键字使用。当你想要覆写(重写)某个方法时,可以选择添加这个注解,在你不留心重载而并非覆写了该方法时,编译器就会生成一条错误信息。当子类重写父类方法时,编译器会检查该注解下方法是否正确。 -
向上转型
子类和父类的关系:子类是父类的一种类型。子类可能比父类含有更多的方法,但它必须至少具备父类中所含有的方法。在向上转型的过程中,类接口中唯一可能发生的事情是丢失方法,而不是获取它们。向上转型是安全的,因为基类不会具有大于导出类的接口。 -
final关键字
使用final关键字的三种情况:数据,方法,类。(1)final数据
常用情况:一个永不改变的编译时常量或一个在运行时被初始化的而并不想被改变的值。
针对编译期常量,可以在编译时执行计算式,减轻运行时的负担。在Java中,这类常量必须是基本数据类型,并且以final关键字表示。
一个既是static又是final的域(即编译时常量)只占据一段不能改变的储存空间。 按照惯例,既是static又是final的域将用大写表示,并使用下划线分隔各个单词。
当对引用使用final时,只确保该引用指向这个对象,但对象本身可以改变。
空白final:被声明为final但又未给定初值的域。只要在使用前初始化即可。
final参数:Java允许在参数列表中以声明的方式将参数指明为final,这意味着你无法在方法中更改参数引用所指向的对象。对于基本数据类型参数,可以理解为只可读参数不可改参数。(2)final方法
使用final方法的原因有两个:①锁定方法,以防任何继承类修改它的含义。②效率(Java早期版本的final会同意编译器针对该方法的所有调用都转为内嵌调用,不过在Java SE5/6时,虚拟机(特别是Hotspot技术)会优化这个问题,所以其实不用考虑②)。
类中所有的private方法都隐式地指定为是final的。(3)final类
当将某个类的整体定义为final时(通过将关键字final置于它的定义之前),就表明你不打算继承该类。 -
类的加载
每个类的编译代码都在它自己的独立、文件中,该文件只在需要使用代码程序时才会被加载。
第八章 多态
-
方法调用绑定
绑定:将一个方法调用同一个方法主体关联起来。
前期绑定:在程序执行前进行绑定(由编译器和连接程序实现)。
后期绑定:在运行时根据对象的类型进行绑定,后期绑定也叫动态绑定或运行时绑定。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
Java中所有方法都是通过动态绑定实现多态。 -
多态
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。 -
多态的缺陷
(1) "覆盖"私有方法public class PrivateOverride { private void f() { System.out.println("private f()"); } public static void main(String[] args) { PrivateOverride po = new Derived(); po.f(); } } class Derived extends PrivateOverride { public void f() { System.out.println("public f()"); } } /*Output: private f() */ /*由于继承也会把基类的main方法继承下来,所以在eclipse中运行时编译器会询问执行 哪一个main,这里选择PrivateOverride即可。*/
上例中我们想输出的是public f(),但是由于private方法被自动认为是final方法,而且对子类是屏蔽的,所以Derived类中的f()是一个全新的方法。这里可得出结论:只有非private方法才能被覆盖,但是还需要密切注意覆盖private方法的现象。在导出类中,对于基类中的private方法,最好采用不同的名字。
(2) 域和静态方法
如果某个方法是静态的,它的行为就不具有多态性。静态方法是与类相关联的而非对象。 -
构造器和多态
构造器本身是static方法,只不过该static声明是隐式的,因此构造器并不具有多态性。
构造器的调用顺序:
(1) 调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层的导出类,等等,直到最低层的导出类。
(2) 按声明顺序调用成员的初始化方法。
(3) 调用导出类构造器的主体。
例子:class Meal { Meal() {System.out.println("Meal()");} } class Bread { Bread() {System.out.println("Bread()");} } class Cheese { Cheese() {System.out.println("Cheese()");} } class Lettuce { Lettuce() {System.out.println("Lettuce()");} } class Lunch extends Meal { Lunch() {System.out.println("Lunch()");} } class PortableLunch extends Lunch { PortableLunch() {System.out.println("PortableLunch()");} } public class Sandwich extends PortableLunch { private Bread b = new Bread(); private Cheese c = new Cheese(); private Lettuce l = new Lettuce(); public Sandwich() {System.out.println("Sandwich()");} public static void main(String[] args) { new Sandwich(); } } /*Output: Meal() Lunch() PortableLunch() Bread() Cheese() Lettuce() Sandwich() */
由于构造器负责创建对象,这里会遇到一个细节问题,就是子类的成员变量还没初始化就被父类构造器调用。例子:
class Glyph { void draw() {System.out.println("Glyph.draw()");} Glyph() { System.out.println("Glyph() before draw()"); draw();//由于动态绑定为RoundGlyph对象,所以这里执行的是RoundGlyph的draw方法 System.out.println("Glyph() after draw()"); } } class RoundGlyph extends Glyph { private int radius = 1; RoundGlyph(int r) { radius = r; System.out.println("RoundGlyph.RoundGlyph().radius = " + radius); } void draw() { System.out.println("RoundGlyph.RoundGlyph().radius = " + radius); } } public class PolyConstructors { public static void main(String[] args) { new RoundGlyph(5); } } /*Output: Glyph() before draw() RoundGlyph.RoundGlyph().radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph().radius = 5 */
所以在上述的构造器调用顺序之前,编译器会在其他任何事物发生之前,将分配给对象的存储空间初始化为二进制的零。优点是所有的东西都至少初始化为零(或是某些特殊数据类型中与“零”等价的值),而不仅仅留作垃圾。为避免错误,在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,它们自动属于final方法)。
-
协变返回类型
Java SE5中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:class Grain { public String toString() { return "Grain"; } } class Wheat extends Grain { public String toString() { return "Wheat"; } } class Mill { Grain process() { return new Grain(); } } class WheatMill extends Mill { Wheat process() { return new Wheat(); } } public class CovariantReturn { public static void main(String[] args) { Mill m = new Mill(); Grain g = m.process(); System.out.println(g); m = new WheatMill(); g = m.process(); System.out.println(g); } } /*Output: Grain Wheat */
Java SE5之前会强制process()的覆盖版本必须返回Grain而不能返回Wheat,而协变会允许返回更加具体的Wheat。
-
向下转型
向下转型:在继承层次中向下移动。 -
运行时类型识别
运行时类型识别(RTTI):在Java语言中,所有的转型都会得到检查。 在进入运行期时会对类型转换进行检查,保证它的确是我们希望的那种类型。如果不是,就会返回一个ClassCastException(类转型异常)。
第九章 接口
-
抽象方法
抽象方法是不完整的方法,仅有声明而没有方法体。abstract void f();
-
抽象类
抽象类:包含抽象方法的类。
如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。(否则。编译器会报错。)
我们也可能创建一个没有抽象方法的抽象类 。当一个类包含任何abstract方法都没有实际意义而且我们想阻止产生这个类的任何对象,这样做就很有用。
抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继承层次结构向上移动。 -
接口
interface关键字比abstract更加抽象。一个接口表示:所有实现了该特定接口的类看起来都像这样。 接口被用来建立类与类之间的协议。interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特性。
创建接口:用interface关键字代替class关键字。
遵循接口:使用implements关键字。
接口中定义的方法默认为public,也必须是public。
接口没有任何具体实现,即没有任何与接口相关的存储。 -
Java中的多重继承
当要表示“1个x是一个a和一个b以及一个c”时,Java可以组合多个接口并只有一个类具体实现,减少负担(比较于c++中的多重继承)。在导出类中,Java中不强制要求必须有一个是抽象的或“具体的”(没有任何抽象方法)基类。如果从一个非接口的类继承,那么只能从一个类去继承,其余基元素都必须是接口。需要将所有的接口置于implements关键字之后,用逗号将它们一一隔开。 可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。下例展示一个具体类组合多个接口之后产生一个新类:interface CanFight { void fight(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } class ActionCharacter { public void fight() {System.out.println("fight() in ActionCharacter");} } class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() {System.out.println("swim() in Hero");} public void fly() {System.out.println("fly() in Hero");} } public class Adventure { public static void t(CanFight x) { x.fight(); } public static void u(CanSwim x) { x.swim(); } public static void v(CanFly x) { x.fly(); } public static void w(ActionCharacter x) { x.fight(); } public static void main(String[] args) { Hero h = new Hero(); t(h); //Treat it as a CanFight u(h); //Treat it as a CanSwim v(h); //Treat it as a CanFly w(h); //Treat it as an ActionCharacter } } /*Output: fight() in ActionCharacter swim() in Hero fly() in Hero fight() in ActionCharacter */
当一个类组合了具体类和多个接口时,具体类必须放在前面,后面跟着的才是接口。
使用接口的原因:
(1) 为了能够向上转型为多个基类型(以及由此而带来的灵活性)。
(2) 与抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。 -
通过继承来扩展接口
通过继承,可以很容易地在接口中添加新的方法声明。interface Monster { void menace(); } interface DangerousMonster extends Monster { void destory(); } interface Lethal { void kill(); } class DragonZilla implements DangerousMonster { public void menace() {} public void destory() {} } interface Vampire extends DangerousMonster, Lethal { void drinkBlood(); } class VeryBadVampire implements Vampire { public void menace() {} public void destory() {} public void kill() {} public void drinkBlood() {} } public class HorrorShow { static void u(Monster b) { b.menace(); } static void v(DangerousMonster d) { d.menace(); d.destory(); } static void w(Lethal l) { l.kill(); } public static void main(String[] args) { DangerousMonster barney = new DragonZilla(); u(barney); v(barney); Vampire vlad = new VeryBadVampire(); v(vlad); w(vlad); } } /* DangerousMonster是Monster的直接扩展,它产生了一个新接口。 DragonZilla中实现了这个接口。在Vampire中使用的语法仅适用于接口继承。 一般情况下,只可以将extends用于单一类,但可以引用多个基类接口,并用逗号一一隔开。*/
在打算组合的不同接口中使用相同的方法名通常会造成混乱,所以要尽量避免这种情况的发生。
-
接口中的域
接口中的任何域都自动是static和final的,所以接口就成为了一种很快捷用来创建常量组的工具。这在Java SE5之前,是实现enum关键字的主要方法,通过在接口中定义常量来实现枚举功能。当然Java SE5之后,直接用enum就行了。 -
初始化接口中的域
在接口中定义的域不能是“空final”,但可以被非常量表达式初始化。例如://: RandVals.java import java.util.Random; public interface RandVals { Random RAND = new Random(47); int RAND_INT = RAND.nextInt(10); long RAND_LONG = RAND.nextLong() * 10; float RAND_FLOAT = RAND.nextLong() * 10; double RAND_DOUBLE = RAND.nextDouble() * 10; }
既然域是static的,它们就可以在类第一次被加载时初始化,这发生在任何域首次被访问之前。当然,这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内。 所以直接通过接口名.常量名的方法就可以访问这些域内常量的值:
//: TestRandVals.java public class TestRandVals { public static void main(String[] args) { System.out.println(RandVals.RAND_INT); System.out.println(RandVals.RAND_LONG); System.out.println(RandVals.RAND_FLOAT); System.out.println(RandVals.RAND_DOUBLE); } }
第十章 内部类
-
内部类
将一个类的定义放在另一个类的定义内部。
如果想从外部类的非静态方法之外的任意位置建立某个内部类的对象,需要具体指明这个对象的类型:OuterClassName.InnerClassName。
使用内部类的最主要原因:每个内部类都能独立地继承自一个接口的实现,所以无论外围类是否已经继承了某个接口的实现,对于内部类都没有影响。
内部类的一些特性:
(1) 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立。
(2) 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。
(3) 创建内部类对象的时刻并不依赖于外围类对象的创建。
(4) 内部类并没有令人迷惑的“is-a”关系,它就是一个独立的实体。 -
链接到外部类
当生成一个内部类的对象时,此对象与制造它的外围对象(enclosing object)之间就有一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外围类的所有元素的访问权。 -
.this和.new
当需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟.this://: DotThis.java public class DotThis { void f() { System.out.println("DotThis.f()"); } public class Inner { public DotThis outer() { return DotThis.this; // A plain "this" would be Inner's "this" } } public Inner inner() { return new Inner(); } public static void main(String[] args) { DotThis dt = new DotThis(); DotThis.Inner dti = dt.inner(); dti.outer().f(); } } /*Output: DotThis.f() */
当要告知某些其他对象去创建其某个内部类的对象时,可以使用.new语法:
public class DotNew { public class Inner {} public static void main(String[] args) { DotNew dn = new DotNew(); /* Must use instance of outer class to create an instance of the inner class.*/ DotNew.Inner dni = dn.new Inner(); } }
要想直接创建内部类的对象不能直接去引用外部类的名字DotNew,而是必须使用外部类的对象来创建内部类对象。在拥有外部类对象之前是不可能创建内部类对象的。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用:
public class Parcel3 { static class Contents { private int i = 11; public int value() { return i; } } static class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public static void main(String[] args) { Parcel3.Contents c = new Contents(); Parcel3.Destination d = new Destination("Tasmania"); } }
-
在方法和作用域内的内部类
使用理由:
(1) 在实现了某类型的接口后,可以创建并返回对其的引用。(向上转型)
(2) 当解决一个复杂问题时,想创建一个类来辅助解决,但是又不希望这个类是公共可用的。
局部内部类:在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。
在任意的作用域中都可以嵌入内部类。 -
匿名内部类
将返回值得生成与表示这个返回值的类的定义结合在一起,注意这个类是匿名的,没有名字://: Contents.java public interface Contents { int value(); } //: Parcel7.java public class Parcel7 { public Contents contents() { return new Contents() { private int i = 11; public int value() { return i; } }; //Semicolon required in this case } public static void main(String[] args) { Parcel7 p = new Parcel7(); Contents c = p.contents(); } } /*这里有个小插曲,因为Contents接口的定义是在前几页书,就很容易漏了写。漏写的话就会报错, 上网一查原来不止我一个人遇到这个错误。。。看来看书有点不认真了。。。*/
当基类需要一个有参数的构造器:
//: Wrapping.java public class Wrapping { private int i; public Wrapping(int x) { i = x; } public int value() { return i; } } //: Parcel8.java public class Parcel8 { public Wrapping wrapping(int x) { // Base constructor call: return new Wrapping(x) { public int value() { return super.value() * 47; } }; // Semicolon required } public static void main(String[] args) { Parcel8 p = new Parcel8(); Wrapping w = p.wrapping(10); } }
如果定义一个匿名内部类,并希望它使用一个在其外部定义的对象,那么编译器会要求其参数引用是final的:
其实这里的final不写也行,编译器会帮你加一个,但是就算不写你也不能在内部类里更改它的值,否则会报错。//: Destination.java public interface Destination { String readLabel(); } //: Parcel9.java public class Parcel9 { // Argument must be final to use inside anonymous inner class: public Destination destination(final String dest) { return new Destination() { private String label = dest; public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel9 p = new Parcel9(); Destination d = p.destination("Tanzania"); } }
那如果想做一些类似构造器的工作怎么办呢?因为它不可能有命名构造器(因为它根本就没有名字。。),但是通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果:
abstract class Base { public Base(int i) { System.out.println("Base constructor, i = " + i); } public abstract void f(); } public class AnonymousConstructor { // 这里不要求final,因为i被传递给匿名类的基类的构造器,它并不会在匿名内部类被直接使用。 public static Base getBase(int i) { return new Base(i) { { System.out.println("Inside instance initializer"); } public void f() { System.out.println("In anonymous f()"); } }; } public static void main(String[] args) { Base base = getBase(47); base.f(); } } /*Output: Base constructor, i = 47 Inside instance initializer In anonymous f() */
匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是二者不可兼备。而且如果是实现接口,也只能实现一个接口。
-
嵌套类
如果不需要内部类对象与其外围对象之间有联系,那么可以将内部类声明为static,这通常被称为嵌套类。普通的内部类对象隐式地保存了一个指向它的外围类对象的引用,而嵌套类有所不同。
嵌套类意味着:
(1) 要创建嵌套类的对象,并不需要其外围类的对象。
(2) 不能从嵌套类的对象中访问非静态的外围类对象。
另外,嵌套类与普通的内部类还有一个区别。普通内部类的字段与方法只能放在类的外部层次上,所以普通内部类不能有static数据和static字段,也不能包含嵌套类。但是嵌套类可以包含所有这些东西:public class Parcel11 { private static class ParcelContents implements Contents { private int i = 11; public int value() { return i; } } protected static class ParcelDestination implements Destination { private String label; private ParcelDestination(String whereTo) { label = whereTo; } public String readLabel() { return label; } // Nested classes can contain other static elements: public static void f() {} static int x = 10; static class AnotherLevel { public static void f() {} static int x = 10; } } public static Destination destination(String s) { return new ParcelDestination(s); } public static Contents contents() { return new ParcelContents(); } public static void main(String[] args) { Contents c = contents(); Destination d = destination("Tasmania"); } }
嵌套类没有特殊的this引用,这有点像一个static方法。
接口内部的类:接口一般不能放置任何代码,嵌套类可以作为接口的一部分,你放到接口中的任何类都自动地是public和static的。
一个内部类被嵌套多少层并重要,它能透明地访问所有它所嵌入的外围类的所有成员:class MNA { private void f() {} class A { private void g() {} public class B { void h() { g(); f(); } } } } public class MultiNestingAccess { public static void main(String[] args) { MNA mna = new MNA(); MNA.A mnaa = mna.new A(); MNA.A.B mnaab = mnaa.new B(); } }
-
闭包
闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。
内部类是面向对象的闭包,因为它不仅包含外围类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外围类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。 -
内部类的继承
由于内部类的构造器必须连接到指向其外围类对象的引用,所以在继承内部类时,这个引用必须被初始化,而在导出类中不再存在可连接的默认对象,使用enclosingClassReference.super()语法来解决这个问题:class WithInner { class Inner {} } public class InheritInner extends WithInner.Inner { InheritInner(WithInner wi) { //enclosingClassReference.super(); wi.super(); } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } }
-
内部类标识符
由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个“meta-class”,叫做Class对象),内部类同样遵守这个规则。这些类文件有严格的命名规则:
(1) 外围类的名字加上“ $ ”再加上内部类的名字。
(2) 如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。
(3) 如果内部类嵌套在别的内部类之中,只需要直接将它们的名字加在其外围类标识符与“ $ ”的后面。