《OnJava》——11内部类
内部类
利用内部类,可以将逻辑上存在关联的类组织在一起,而且可以控制一个类在另一个类中的可见性。
内部类和组合不同,内部类是一种代码隐藏机制:将代码放在其他类的内部。
11.1 创建内部类
创建内部类的方式就是把类定义放在一个包围它的类之中。
/** * 创建内部类 */ public class Parcel1 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } // 在Parcel1内,使用内部类看上去,就和使用任何其他类一样 public void ship(String dest) { Contents c = new Contents(); Destination d = new Destination(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel1 p = new Parcel1(); p.ship("Tasmania"); } } /* Output: Tasmania */
返回一个指向内部类的引用
public class Parcel2 { class Contents { private int i = 11; public int value() { return i; } } class Destination { private String label; Destination(String whereTo) { label = whereTo; } String readLabel() { return label; } } public Destination to(String s) { return new Destination(s); } public Contents contents() { return new Contents(); } public void ship(String dest) { Contents c = contents(); Destination d = to(dest); System.out.println(d.readLabel()); } public static void main(String[] args) { Parcel2 p = new Parcel2(); p.ship("Tasmania"); Parcel2 q = new Parcel2(); // Defining references to inner classes: //要在内部类的非静态方法之外的任何地方创建内部类对象 //必须将对象的类型指定为OutClassName.InnerClassName //即,将实例指定到详细的类 Parcel2.Contents c = q.contents(); Parcel2.Destination d = q.to("Borneo"); System.out.println("c = " + c.value()); System.out.println("d = " + d.readLabel()); } } /* Output: Tasmania c = 11 d = Borneo */
11.2 到外部类的链接
内部类看上去像是一种名称隐藏和代码组织机制。
当创建一个内部类时,这个内部类的对象中会隐含一个链接,指向用于创建该对象的外围对象。
通过这个链接,无须任何特殊条件,内部类对象就可以访问外围对象的成员。
内部类拥有对外围对象所有元素的访问权。
//保存一个对象序列 //迭代器 interface Selector { boolean end(); Object current(); void next(); } //Sequence是以类的形式包装起来的定长Object数组。 public class Sequence { private Object[] items; private int next = 0; public Sequence(int size) { items = new Object[size]; } //使用add()方法向序列末尾增加一个新的Object(有空间的前提下) public void add(Object x) { if(next < items.length) items[next++] = x; } //实现“迭代器”接口,重写方法 private class SequenceSelector implements Selector { //在SequenceSelector中使用到了外围items(private) //内部类可以访问外围对象的所有方法和字段,就好像拥有它们一样 private int i = 0; @Override public boolean end() { return i == items.length; } @Override public Object current() { return items[i]; } @Override public void next() { if(i < items.length) i++; } } public Selector selector() { return new SequenceSelector(); } public static void main(String[] args) { Sequence sequence = new Sequence(10); for(int i = 0; i < 10; i++) sequence.add(Integer.toString(i)); Selector selector = sequence.selector(); while(!selector.end()) { System.out.print(selector.current() + " "); selector.next(); } } } /* Output: 0 1 2 3 4 5 6 7 8 9 */
内部类可以自动访问外围类的所有成员。
这是怎么做到的呢?
对于负责创建内部类对象的特定外围类对象而言,内部类对象偷偷地获取了一个指向它的引用。(内部得外部的属性)
该引用会被用于选择相应的成员
内部类的对象只能与其外部类的对象关联创建(当内部类为非static时)
11.3 使用 .this和.new
若要生成外部类对象的引用,可以使用外部类的名字,后面加上句点和this。
即 Inner.this
这个就是外部类了
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" //如果直接写this,引用的是Inner的this,当前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语法,在new表达式中提供指向其他外部类对象的引用:
public class DotNew { public class Inner {} public static void main(String[] args) { DotNew dn = new DotNew(); DotNew.Inner dni = dn.new Inner(); } } //除非已经有了一个外部类的对象,否则创建内部类对象是不可能的 //因为内部类的对象会暗中连接到用于创建它的外部类对象 //如果你创建的是嵌套类(static修饰的内部类),就不需要指向外部类对象的引用
11.4 内部类和向上转型
当需要向上转型为基类,特别是接口时,内部类就更有吸引力了。(从实现某个接口的对象生成一个该接口类型的引用,其效果和向上转型为某个基类在本质上是一样的)
这是因为,内部类(接口的实现)对外部而言可以时不可见的、不可用的,这便于隐藏实现。
外部获得的只是一个指向基类或接口的引用。
创建接口:接口会自动将其所有成员设置为public的。
public interface Destination { String readLabel(); }
public interface Contents { int value(); }
当得到一个指向基类或接口的引用时,无法找到其确切的类型:
class Parcel4 { private class PContents implements Contents { private int i = 11; @Override public int value() { return i; } } protected final class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } @Override public String readLabel() { return label; } } public Destination destination(String s) { return new PDestination(s); } public Contents contents() { return new PContents(); } } public class TestParcel { public static void main(String[] args) { Parcel4 p = new Parcel4(); Contents c = p.contents(); Destination d = p.destination("Tasmania"); // Illegal -- can't access private class: //非法————不能访问private类 //- Parcel4.PContents pc = p.new PContents(); } }
在Parcel4中,内部类PContents时private的,所以只有Parcel4能访问它。
普通类(非内部类)无法声明为private的或protected的,它们只能被给予public或包访问权限。
PDestination是protected的,所以它只能被Parcel4、相同包中的类(因为protected也给予了包访问权限),以及Parcel4的子类访问。
这意味着客户程序员对这些成员的了解是有限的,对它们的访问权限也是有限的。
private内部类为类的设计者提供了一种方式,可以完全组织任何与类型相关的编码依赖,并且可以完全隐藏实现细节。
11.5 在方法和作用域中的内部类
内部类可以在一个方法内或者任何一个作用域内创建。
- 在方法中定义的类
- 在方法中的某个作用域内定义的类
- 实现某个接口的匿名类
- 这样的匿名类——它继承了拥有非默认构造器的类
- 执行字段初始化的匿名类
- 它通过实例初始化来执行构造(匿名内部类不可能有构造器)的匿名类。
局部内部类:在一个方法的作用域内(而不是在另一个类的作用域内)创建一个完整的类。
public class Parcel5 { public Destination destination(String s) { /** * 局部内部类 -- PDestination * PDestination在destination()外无法访问。 */ final class PDestination implements Destination { private String label; private PDestination(String whereTo) { label = whereTo; } @Override public String readLabel() { return label; } } return new PDestination(s); } public static void main(String[] args) { Parcel5 p = new Parcel5(); Destination d = p.destination("Tasmania"); } }
PDestination是destination()的一部分,而不是parcel5的一部分。因此,PDestination在destination()外无法访问。
在return语句中的向上转型意味着destination()中只传出了一个指向Destination接口的引用。
PDestination类的名字被放在了destination()中这一事实,并不意味着一旦destination()方法返回,得到的PDestination就不是一个合法的对象了。
在同一子目录下的每个类中,你都可以使用类标识符PDestination来命名内部类,而不会产生名字冲突。
如何在任何作用域嵌入一个内部类:
public class Parcel6 { private void internalTracking(boolean b) { if(b) { /** * 这个类被嵌入了if语句的作用域内 * 在作用域之外是不可用的。 */ class TrackingSlip { private String id; TrackingSlip(String s) { id = s; } String getSlip() { return id; } } TrackingSlip ts = new TrackingSlip("slip"); String s = ts.getSlip(); } // Can't use it here! Out of scope: //- TrackingSlip ts = new TrackingSlip("x"); } public void track() { internalTracking(true); } public static void main(String[] args) { Parcel6 p = new Parcel6(); p.track(); } }
11.6 匿名内部类
/** * 返回匿名内部类的一个实例 */ public class Parcel7 { public Contents contents() { return new Contents() { // 插入类定义 private int i = 11; @Override public int value() { return i; } }; // 分号是必须的 } public static void main(String[] args) { Parcel7 p = new Parcel7(); Contents c = p.contents(); } }
contents()方法将返回值的创建和用于表示该返回值的类的定义结合了起来。此外,这个类没有名字——它是匿名的。
看起来你正在创建一个Contents对象,但是在到达分号之前,你说:“等等,我想插入一个类定义。”
“创建一个继承自Contents的匿名类的对象”,通过new表达式返回的引用会被自动地向上转型为一个Contents引用。
匿名内部类的语法如下:
public class Parcel7b { class MyContents implements Contents { private int i = 11; @Override public int value() { return i; } } public Contents contents() { return new MyContents(); } public static void main(String[] args) { Parcel7b p = new Parcel7b(); Contents c = p.contents(); } }
在这个匿名内部类中,Contents是用无参构造器创建的。
如果基类需要带一个参数的构造器,应该这样:
/** * 基类需要带一个参数的构造器 */ public class Parcel8 { public Wrapping wrapping(int x) { // Base constructor call: return new Wrapping(x) { // [1] @Override public int value() { return super.value() * 47; } }; // [2] } public static void main(String[] args) { Parcel8 p = new Parcel8(); Wrapping w = p.wrapping(10); } }
【1】将适当的参数传给基类构造器
【2】匿名内部类末尾的分号并不是用来标记类体的结束。相反,它标记表达式的结束,而该表达式恰好包含了这个匿名类。因此,它和分号在其他地方的用法没什么不同。
尽管,Wrapping是一个带有实现的普通类,但它也被用作其子类的公共“接口”:
public class Wrapping { private int i; public Wrapping(int x) { i = x; } public int value() { return i; } }
也可以在定义匿名类中的字段时执行初始化:
public class Parcel9 { // Argument must be final or "effectively final" //要在匿名内部类中使用 // to use within the anonymous inner class: // 参数必须是最终变量,或者"实际上的最终变量" public Destination destination(final String dest) { return new Destination() { private String label = dest; @Override public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel9 p = new Parcel9(); Destination d = p.destination("Tasmania"); System.out.println("d = " + d.readLabel()); } }
如果你正在定义一个匿名类,而且一定要用到一个在该匿名类之外定义的对象,编译器要求参数引用用final修饰,或者是“实际上的最终变量”(也就是说,在初始化之后它永远不会改变,所以它可以被视为final)
就像你在destination()的参数中看到的那样。这里不写final也没有任何问题,但把它写上当作提醒通常最好。
如果只是要给一个字段赋值,这个实例中的方法就很好。
但是如果必须执行某个蕾丝构造器的动作,该怎么办呢?因为匿名类没有名字,所以不可能有命名的构造器。
借助实例初始化,我们可以在效果上为匿名内部类创建一个构造器:
/** * Creating a constructor for an anonymous inner class * 为匿名内部类创建一个构造器 */ abstract class Base { Base(int i) { System.out.println("Base constructor, i = " + i); } public abstract void f(); } public class AnonymousConstructor { public static Base getBase(int i) { return new Base(i) { { System.out.println("Inside instance initializer"); } @Override 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() */
这里变量i并不是必须为最终变量。尽管i被传给了匿名类的基类构造器,但是在该匿名类内部,它并没有被直接使用到。
对比一下,当参数在匿名类内部被用到:
** * 使用"实例初始化",来执行匿名内部类的构造 */ public class Parcel10 { public Destination destination(final String dest, final float price) { return new Destination() { private int cost; //为每个对象执行实例初始化 // Instance initialization for each object: { cost = Math.round(price); if(cost > 100) System.out.println("Over budget!"); } private String label = dest; @Override public String readLabel() { return label; } }; } public static void main(String[] args) { Parcel10 p = new Parcel10(); Destination d = p.destination("Tasmania", 101.395F); } } /* Output: Over budget! */
我们在实例初始化操作内可用看到一段代码,也就是if语句,它们不能作为字段初始化的一部分来执行。
所以在效果上,实例初始化就是匿名内部类的构造器。不过它也有局限性——我们无法重载实例初始化部分,所以只能有一个这样的构造器。
与普通的继承相比,匿名内部类有些局限性,因为它们要么是扩展一个类,要么是实现一个接口,但是两者不可兼得。
而且就算要实现接口,也只能实现一个。
11.7 嵌套类
如果不需要内部类对象和外部类对象之间的连接,可以将内部类设置为static的。这就是嵌套类。
普通内部类对象中隐式地保留了一个引用,指向创建该对象的外部类对象。
对于static的内部类(嵌套类)来说:
- 不需要一个外部类对象来创建嵌套类对象
- 无法从嵌套类对象内部访问非static的外部类对象
嵌套类和普通内部类之间的不同:
普通内部类的字段和方法,只能放在类的外部层次中,所以普通内部类中不能有static数据、static字段,也不能包含嵌套类
但是嵌套类中可以包含:其他的静态元素
/** * 嵌套类(static的内部类) */ public class Parcel11 { private static class ParcelContents implements Contents { private int i = 11; @Override public int value() { return i; } } protected static final class ParcelDestination implements Destination { private String label; private ParcelDestination(String whereTo) { label = whereTo; } @Override 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(); System.out.println("c.value() = " + c.value()); Destination d = destination("Tasmania"); System.out.println("d.readLabel() = " + d.readLabel()); } }
这样在main()中并不需要Paecel11对象;
静态方法可以直接调用,类名调用和对象调用
非静态方法只能通过对象调用
相反,我们使用选择static成员的普通语法来调用方法,这些方法指向Contents和Destination类型的引用。
普通内部类(非static的)可以使用特殊的this(内部类.this)引用来创建指向外部类对象的连接。而嵌套类没有特殊的this引用,这使它和static方法类似。
11.7.1 接口中的类
嵌套类可以是接口的一部分。
放到接口中的任何类都会自动成为public和static的。
因为类是static的,所以被嵌套的类知识放在了这个接口的命名空间内。
甚至可以在内部类内实现包围它的这个接口:
public interface ClassInInterface { void howdy(); /** * 在内部类实现这个接口,套娃呢在? */ class Test implements ClassInInterface { @Override public void howdy() { System.out.println("Howdy!"); } public static void main(String[] args) { new Test().howdy(); } } } /* Output: Howdy! */
当你要创建某个接口的所有不同实现使用的公用代码时,将一个类嵌入这个接口中会非常方便。
11.7.2 从多层嵌套的内部类中访问外部成员
一个内部类被嵌套多少册并不重要。
它可以透明地访问它的所有类的所有成员,如下面的代码所示:
/** * 被嵌套的类可以访问各层外部类中的所有成员 */ class MNA { private void f() {} class A { private void g() {} public class B { public void h() { System.out.println("B::h()"); g(); { System.out.println("B::CodeBlock"); } 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(); mnaab.h(); } } /** * B::h() * B::CodeBlock * /
可以注意到,private方法g()和f()无须任何条件就可以调用。
这个例子也演示了当你在一个不同的类中创建对象时,创建多层嵌套的内部类对象的基本语法。
.new 语法会得到正确的作用域,所以不必在调用构造器时限定类的名字。
11.8 为什么需要内部类
到底为什么需要内部类呢?
通常情况下,内部类继承自某个类或实现某个接口,内部类中的代码会操作用以创建该内部类对象的外部类对象。内部类提供了进入其外部类的某种窗口。
引入内部类的主要原因:
-
每个内部类都可以独立地继承自一个实现
-
外部类是否已经继承了某个实现,对内部类并没有限制
-
内部类完善了多重继承问题的解决方案
-
内部类实际上支持我们继承多个非接口类型
例如,在一个类内必须以某种形式实现两个接口。
由于接口的灵活性,有两个选择:一个单独的类(同时实现两个接口)或一个内部类(分离实现两接口)
interface A {} interface B {} //通过一个单独的类,同时实现两个接口 class X implements A, B {} //通过内部类,分离实现两个接口 class Y implements A { B makeB() { // Anonymous inner class: // 匿名内部类 return new B() {}; } } public class MultiInterfaces { static void takesA(A a) {} static void takesB(B b) {} public static void main(String[] args) { X x = new X(); Y y = new Y(); takesA(x); takesA(y); takesB(x); takesB(y.makeB()); } }
如果使用的是抽象类或具体类,而不是接口的话,而且你的类必须以某种方式实现这两者,那就只能使用内部类来实现了:
// 对于具体类或抽象类,内部类可以产生“多重实现继承”的效果 class D {} abstract class E {} class Z extends D { //通过内部类实现抽象类 E makeE() { return new E() {}; } } public class MultiImplementation { static void takesD(D d) {} static void takesE(E e) {} public static void main(String[] args) { Z z = new Z(); takesD(z); takesE(z.makeE()); } }
有了内部类,可以得到如下这些额外的功能:
- 内部类可以有多个实例,每个实例都有自己的状态信息,独立于外围类对象的信息。
- 一个外围类中可以有多个内部类,它们可以以不同方式实现同一接口,或者继承同一类。
- 内部类对象的创建时机不与外围类对象的创建捆绑到一起。
- 内部类不存在可能引起混淆的“is-a”关系;它是独立的实体。
11.8.1 闭包与回调
**闭包(closure)**是一个可调用的对象,它保留了自它被创建时所在的作用域的信息
**回调(callback)**Java中的指针机制,通过回调可以给其他对象提供一段信息,以支持它在之后的某个时间点调用会原始的对象中
// Very simple to just implement the interface: // 只实现这个接口非常简单 class Callee1 implements Incrementable { private int i = 0; @Override public void increment() { i++; System.out.println(i); } } class MyIncrement { public void increment() { System.out.println("Other operation"); } static void f(MyIncrement mi) { mi.increment(); } } // If your class must implement increment() in // some other way, you must use an inner class: //如果我们的类必须以其他某种方式实现increment(),则必须使用内部类: class Callee2 extends MyIncrement { private int i = 0; @Override public void increment() { super.increment(); i++; System.out.println(i); } private class Closure implements Incrementable { //留个指回Callee2的钩子 @Override public void increment() { // Specify outer-class method, otherwise // you'll get an infinite recursion: //调用外围类方法,否则会无限递归下去(跳出递归) Callee2.this.increment(); } } Incrementable getCallbackReference() { return new Closure(); } } class Caller { private Incrementable callbackReference; Caller(Incrementable cbh) { callbackReference = cbh; } void go() { callbackReference.increment(); } } public class Callbacks { public static void main(String[] args) { Callee1 c1 = new Callee1(); Callee2 c2 = new Callee2(); MyIncrement.f(c2); Caller caller1 = new Caller(c1); Caller caller2 = new Caller(c2.getCallbackReference()); caller1.go(); caller1.go(); caller2.go(); caller2.go(); } } /* Output: Other operation 1 1 2 Other operation 2 Other operation 3 */
通过使用内部类来提供单独的实现,满足更多的需求。
当创建内部类时,并没有增加或修改外围类的接口。
参考《OnJava》Visit http://OnJava8.com for more book information.
posted on 2022-06-23 15:54 JavaCoderPan 阅读(25) 评论(0) 编辑 收藏 举报 来源
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南