内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。然而必须要了解,内部类与组合是完全不同的概念,这一点很重要。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但是,你将会了解到,内部类远不止如此,它了解外部类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样(而且 Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求)。
创建内部类的简单方式如下所示,把类定义到外部类里面:
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; }
}
// 在外部类中使用内部类,直接创建对象并使用即可
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");
}
}
输出结果:
Tasmania
更典型的情况是,外部类将有一个方法,该方法返回一个指向内部类的引用,就像在 to()
和 contents()
方法中看到的那样:
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();
Contents c = q.contents();
Destination d = q.to("Borneo");
}
}
也许你会说,哎。这不是多次一举么?直接new
就完事咯,何必这么麻烦呢?如果可以在main
方法或者其它类中直接创建,大可试试。这也是后面我会说到的 内部类
的创建。
到目前为止,内部类似乎还只是一种名字隐藏和组织代码的模式。这些是很有用,但还不是最引人注目的,它还有其他的用途。当生成一个内部类的对象时,此对象与制造它的外部对象(enclosing object)之间就有了一种联系,所以它能访问其外部对象的所有成员,而不需要任何特殊条件。此外,内部类还拥有其外部类的所有元素的访问权。
// 简单的迭代器
interface Selector {
//判断是否结束
boolean end();
//当前元素
Object current();
//指针后移
void next();
}
// 简单的对象数组,增加额外的 add 方法
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int size) {
items = new Object[size];
}
// 添加元素方法
public void add(Object x) {
if(next < items.length) {
items[next++] = x;
}
}
// 可以理解为 Sequence 的内置迭代器
private class SequenceSelector implements Selector {
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();
}
}
}
我们需要关心的主要是 SequenceSelector
中的方法,很明显,在SequenceSelector
中,可以直接调用 Sequence
的私有对象,这可能超出我们的认知。搞不懂就Debug呗!
开始Debug,在上述程序中标记处打上断点,进行调试,可以发现在sequence.selector()
执行之后,selector
中存在sequence
中私有元素的引用,所以能够访问其中的私有元素,内部类自动拥有对其外部类所有成员的访问权。
这是如何做到的呢?当某个外部类的对象创建了一个内部类对象时,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用。然后,在你访问此外部类的成员时,就是用那个引用来选择外部类的成员。幸运的是,编译器会帮你处理所有的细节,但你现在可以看到:内部类的对象只能在与其外部类的对象相关联的情况下才能被创建(就像你应该看到的,内部类是非 static 类时)。构建内部类对象时,需要一个指向其外部类对象的引用,如果编译器访问不到这个引用就会报错。不过绝大多数时候这都无需程序员操心。
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟 .this。这样产生的引用自动地具有正确的类型,这一点在编译期就被知晓并受到检查,因此没有任何运行时开销。下面的示例展示了如何使用 .this:
public class DotThis {
void f() { System.out.println("DotThis.f()"); }
public class Inner {
public DotThis outer() {
// 如果只是 this,指的就是 Inner
return DotThis.this;
}
}
public Inner inner() { return new Inner(); }
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
// 返回的 DotThis 对象与dt相同。
dti.outer().f();
}
}
输出为:
DotThis.f()
我们以前创建内部类都是在非静态方法之中进行创建,当我们在外部类中的静态方法中直接创建时,编译器会提示不允许这么做(除非为内部类添加static关键字,但这样会失去对其外部类所有成员的访问权)。这个时候就需要在外部类的静态方法中使用.new进行创建。
如下所示:
public class DotNew {
class Inner {}
public static void main(String[] args) {
// 前提是存在外部类对象
DotNew dn = new DotNew();
// 调用一次就是创建新的对象,但是使用 .this 获取到的外部类为同一个
Inner dni = dn.new Inner();
}
}
有人就要问了,在其他类中创建内部类怎么办?请看以下代码:
public class Test {
public static void main(String[] args) {
//以下操作显然时错误的
//DotNew.Inner inner = new DotNew.Inner();
DotNew dn = new DotNew();
//在其他类中需要指明 OuterClassName.InnerClassName 如DotNew.Inner
DotNew.Inner dni = dn.new Inner();
}
}
不管是在其他类中的静态方法还是非静态方法,都需要先创建外部类,再使用.new创建内部类对象。当然,如果外部类提供方法返回内部类对象的引用,也可以不使用.new,但依然需要使用 OuterClassName.InnerClassName
格式用于接收。
当将内部类向上转型为其基类,尤其是转型为一个接口的时候,内部类就有了用武之地。(从实现了某个接口的对象,得到对此接口的引用,与向上转型为这个对象的基类,实质上效果是一样的。)这是因为此内部类-某个接口的实现-能够完全不可见,并且不可用。所得到的只是指向基类或接口的引用,所以能够很方便地隐藏实现细节。
我们可以创建前一个示例的接口:
public interface Destination {
String readLabel();
}
public interface Contents {
int value();
}
现在 Contents 和 Destination 表示客户端程序员可用的接口。记住,接口的所有成员自动被设置为 public。
当取得了一个指向基类或接口的引用时,甚至可能无法找出它确切的类型,看下面的例子:
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");
// 不允许这么做,因为PContents的访问权限为private
// 同样也不能创建PDestination,因为PDestination的构造方法私有
//- Parcel4.PContents pc = p.new PContents();
}
}
在 Parcel4 中,内部类 PContents 是 private,所以除了 Parcel4,没有人能访问它。普通(非内部)类的访问权限不能被设为 private 或者 protected;他们只能设置为 public 或 package 访问权限。
PDestination 是 protected,所以只有 Parcel4 及其子类、还有与 Parcel4 同一个包中的类(因为 protected 也给予了包访问权)能访问 PDestination,其他类都不能访问 PDestination,这意味着,如果客户端程序员想了解或访问这些成员,那是要受到限制的。实际上,甚至不能向下转型成 private 内部类(或 protected 内部类,除非是继承自它的子类),因为不能访问其名字,就像在 TestParcel 类中看到的那样。
private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节。此外,从客户端程序员的角度来看,由于不能访问任何新增加的、原本不属于公共接口的方法,所以扩展接口是没有价值的。这也给 Java 编译器提供了生成高效代码的机会。
到目前为止,读者所看到的只是内部类的典型用途。通常,如果所读、写的代码包含了内部类,那么它们都是“平凡的”内部类,简单并且容易理解。然而,内部类的语法重写了大量其他的更加难以理解的技术。例如,可以在一个方法里面或者在任意的作用域内定义内部类。
第一个例子展示了在方法的作用域内(而不是在其他类的作用域内)创建一个完整的类。这被称作局部内部类:
public class Parcel5 {
public Destination destination(String s) {
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 的一部分。所以,在 destination()
之外不能访问 PDestination,注意出现在 return 语句中的向上转型-返回的是 Destination 的引用,它是 PDestination 的基类。当然,在 destination()
中定义了内部类 PDestination,并不意味着一旦 destination()
方法执行完毕,PDestination 就不可用了。
你可以在同一个子目录下的任意类中对某个内部类使用类标识符 PDestination,这并不会有命名冲突。
下面的例子展示了如何在任意的作用域内嵌入一个内部类:
public class Parcel6 {
private void internalTracking(boolean b) {
if(b) {
class TrackingSlip {
private String id;
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
// 跟局部变量一样,程序块外无法访问
//- TrackingSlip ts = new TrackingSlip("x");
}
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
}
}
TrackingSlip 类被嵌入在 if 语句的作用域内,这并不是说该类的创建是有条件的,它其实与别的类一起编译过了。然而,在定义 Trackingslip 的作用域之外,它是不可用的,除此之外,它与普通的类一样。
下面的例子看起来有点奇怪:
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 的引用。上述匿名内部类的语法是下述形式的简化形式:
// innerclasses/Parcel7b.java
// Expanded version of Parcel7.java
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) {
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);
}
}
尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被导出类当作公共“接口”来使用。
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
}
在匿名类中定义字段时,还能够对其执行初始化操作:
public class Parcel9 {
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");
}
}
如果在定义一个匿名内部类时,它要使用一个外部环境(在本匿名内部类之外定义)对象,那么编译器会要求其(该对象)参数引用是 final 或者是 “effectively final”(也就是说,该参数在初始化后不能被重新赋值,所以可以当作 final)的,就像你在 destination()
的参数中看到的那样。这里省略掉 final 也没问题,但通常加上 final 作为提醒比较好。
如果只是简单地给一个字段赋值,那么此例中的方法是很好的。但是,如果想做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果,就像这样:
// innerclasses/AnonymousConstructor.java
// 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();
}
}
输出为:
Base constructor, i = 47
Inside instance initializer
In anonymous f()
在此例中,不要求变量 i 一定是 final 的。因为 i 被传递给匿名类的基类的构造器,它并不会在匿名类内部被直接使用。
下例是带实例初始化的"parcel"形式。注意 destination()
的参数必须是 final 的,因为它们是在匿名类内部使用的(译者注:即使不加 final, Java 8 的编译器也会为我们自动加上 final,以保证数据的一致性)。
// innerclasses/Parcel10.java
// Using "instance initialization" to perform
// construction on an anonymous inner class
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);
}
}
输出为:
Over budget!
在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 if 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。
匿名内部类与正规的继承相比有些受限,因为匿名内部类要么继承类,要么实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
原文:https://www.cnblogs.com/kaoshanyao/p/15227252.html