内部类自救指南

内部类是Java的一个重要组成,参考《Java编程思想》一书后,对内部类进行了一些整理…

初识内部类

内部类是指在类内部定义的类.

内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部类的可视性。

内部类了解外围类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰,尽管并不总是这样。
——摘自《Java编程思想》

内部类与外围类的通信

开始并创建一个内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//代码1.0
public class Outer {
private class Inner {
private String desc="inner class";
public void printDesc() {
system.out.println(desc);
}
}
public Inner getInner() {
return new Inner();
}
public static void main(String[] args) {
Outer outer=new Outer();
Inner inner=outer.getInner();
inner.printDesc();
}
}

代码1.0创建了一个简单的内部类Inner,并对它进行了简单的测试,至少可以得出以下几点:

  • 可以直接为内部类中的成员变量进行初始化
  • 通过外部类可以创建内部类的对象
  • 可以直接使用new 创建内部类(尽管不总是这样)

内部类与外围类的通信

普通的内部类拥有外围类的所有元素的访问权限(嵌套类除外,嵌套类后面会单独讨论);

下图通过代码提示框清除的展示了这一点:
内部类权限

那么内部类如何能访问到外部类的成员,编译器内部帮我们完成了这项艰巨的任务:外围类的对象创建了一个内部类的对象时,内部类对象秘密的包含一个指向外围类对象的引用(编译器帮你做好了这一步);

使用.this与.new

通过.this可以在内部类中获得外部类的引用;
通过.new可以直接创建内部类对象,而之前我们是通过调用方法返回内部类的对象。

代码1.1很好的展示它们的用法与关系(请注意打*的两条语句)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//代码1.1
public class Outer {
private class Inner {
public Outer getOuter() {
return Outer.this; //(*)
}
}

public void doSomething() {
System.out.println("hello outer class");
}
public static void main(String[] args) {
Outer outer=new Outer();
Inner inner=outer.new Inner();//(*)
inner.getOuter().doSomething();
//输出:hello outer class
}
}

注意 内部类的创建的前提是必须有一个外部类,因为内部类需要自动创建一个外部类的引用,所有无法简单的使用Inner inner=new Outer.Inner();来创建一个内部类的对象

还有一点要说明:请注意下面的写法1和写法2是有区别的:

1
2
3
4
5
6
7
8
9
10
11
public class A{
class B{ }
public static void main(String[] args){
//写法1:
A a=new A();
B b1=a.new B();

//写法2:
B b2=new A().new B();
}
}

内部类的继承与转型

内部类继承其他类

内部类可以实现接口或者继承父类,也可以向上转型为基类
代码1.2展示这一概念:
(以下代码来自不同的java文件,如果要放入一个java文件,请保证只有一个public类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//代码1.2
//父接口
public interface BaseOuter {
void print();
}

public class Outer {
private class Inner implements BaseOuter {
@Override
public void print() {
System.out.println("implements BaseOuter...");
}
}
public void doSomething() {
System.out.println("hello outer class");
}
public static void main(String[] args) {
Outer outer = new Outer();
BaseOuter base=outer.new Inner();
base.print();
//输出:implements BaseOuter...
}
}

继承自内部类

内部类之间的继承关系并不简单,原因在于内部类中存在一个指向外部类引用,这个引用需要在继承关系中被初始化。

要对该引用进行初始化,不能使用默认构造函数,必须传入一个外部类的引用然后调用它的构造方法,这种语法再内部类继承时是唯一的,也是必须的。(代码1.3)

1
2
3
4
5
6
7
8
9
//代码1.3
class WithInner{
class Inner{}
}
public class Outer extends WithInner.Inner{
public Outer(WithInner withInner) {
withInner.super();
}
}

内部类的分类

普通内部类

以上所讨论的都是普通的内部类。

方法内部类

方法内部类是指定义在方法之中的内部类,作用范围只在方法中。(代码1.4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//代码1.4
interface FunctionClassBase{
void doSomething();
}

public class Outer {
public FunctionClassBase includeInnerClass() {
class FunctionClass implements FunctionClassBase{
public void doSomething() {
System.out.println("function inner class...");
}
}
return new FunctionClass();
}
public static void main(String[] args) {
Outer outer=new Outer();
outer.includeInnerClass().doSomething();
}
//输出:function inner class...
}

如果FunctionClass不继承自类/实现接口,那么将无法得到它的实例,因为它只能在includeInnerClass方法中使用,所以includeInnerClass方法的返回值无法定义为FunctionClass.

作用域内部类

作用域内部类和方法内部类很相似,它的作用域更有局限,只能在某个作用范围中,比如if语句中。

注意:方法内部类和作用域内部类可以理解为局部内部类,它们不能使用访问修饰符,就像方法中的变量无法使用访问修饰符一样。

匿名内部类

匿名内部类值得引起注意,它是使用最多的内部类,至少我遇到的最多。

代码1.5展示了一个复杂且冗余的例子,如果实际开发中都是这样写,我想程序员会崩溃的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//代码1.5
interface Contents{
void doSomething();
}
public class Outer {
class Inner{
public void setContents(Contents c) {
c.doSomething();
}
}
class SubContents implements Contents{
public void doSomething() {
System.out.println("no name class ...");
}
}
public static void main(String[] args) {
Outer outer=new Outer();
Inner inner=outer.new Inner();
Contents content=outer.new SubContents();
inner.setContents(content);
//输出:no name class ...
}

为了调用setContents方法,我们写了很多冗余的代码,如果使用匿名内部类,将会使上面的例子变得清爽简洁(代码1.6改用匿名内部类,省略了接口定义和内部类定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//代码1.6
public class Outer {
public static void main(String[] args) {
Outer outer=new Outer();
Inner inner=outer.new Inner();
inner.setContents(new Contents() { //*
@Override
public void doSomething() {
System.out.println("no name class...");
}
});
//输出:no name class ...
}
}

带*号的位置开始的一段代码被当作一个Contents对象传入方法,这就是匿名内部类,由于它没有名字,所以使用接口类型代替(由于存在向上转型),接口必须实现方法,因此后面的大括号内重写了doSomething方法,这是匿名内部类的基本定义方法。如果不太理解请仔细对比代码1.5和代码1.6.

匿名内部类的应用非常广泛,比如Android中大量使用了匿名内部类,代码1.7展示了Android中的单击事件如何使用匿名内部类:

无需关系代码的含义,只需要注意setOnClickListener方法中参数是如何使用匿名内部类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//代码1.7
confirmChangePsw=findViewById(R.id.confirm_change_psw);
confirmChangePsw.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!newPsw.getText().toString().equals(checkNewPsw.getText().toString())){
Toast.makeText(ChangePswActivity.this,"修改失败",Toast.LENGTH_SHORT).show();
return;
}
String sql="update user set password=? where u_id="+uid;
noteDatabaseHelper.getReadableDatabase().execSQL(sql,new String[]{newPsw.getText().toString()});
Toast.makeText(ChangePswActivity.this,"修改成功",Toast.LENGTH_SHORT).show();
}
});

关于匿名内部类还有两点需要说明:

如果你有一个匿名内部类,它要使用一个宰他外部定义的对象,编译器会要求其参数引用是final的;如果你忘记了,会得到一个编辑期错误信息。

匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
——摘自《Java编程思想》

嵌套类

嵌套类是指static修饰的类,它不会与外围类建立联系,所以也不能访问外围类的非静态成员,创建嵌套类也不需要依赖于外围类,这是与之前的普通内部类最大的区别。

嵌套类的另一个特点在于:在嵌套类内部允许包含static成员属性/方法,并且可以再包含嵌套类。嵌套类甚至可以定义在接口中。普通内部类无法做到。
之前提到,普通内部类无法使用new OuterClassName.InnerClassName()创建,而对于嵌套类这样是可以的,可以理解为一个静态方法的调用。(代码1.8)

1
2
3
4
5
6
7
8
9
10
11
12
13
//代码1.8
public class Outer {
static class Inner{
private static String desc="static inner class";
public static void showDesc() {
System.out.println(desc);
}
}
public static void main(String[] args) {
Outer.Inner inner=new Outer.Inner();
inner.showDesc();
}
}

代码1.8还可以再简化一点,由于是静态方法与嵌套类,不需要任何对象也可以直接调用。(代码1.9)

1
2
3
4
5
6
7
8
9
10
11
12
//代码1.9
public class Outer {
static class Inner{
private static String desc="static inner class";
public static void showDesc() {
System.out.println(desc);
}
}
public static void main(String[] args) {
Outer.Inner.showDesc();
}
}

内部类标识符

Java中,每一个类都会产生一个class文件,内部类也不例外,普通类的class文件名即为类名,内部类略有不同,它的命名为OuterClassName$InnerClassName.class

例如代码1.10的简单结构编译后会产生如下两个文件:

  • Outer.class
  • Outer$Inner.class
1
2
3
4
5
6
7
8
9
10
//代码1.10
public class Outer{

class Inner{

}

public static void main(String[] args) {
}
}

匿名内部类则会以一个数字代替内部类的名字。

为什么选择内部类

使用内部类实现多重继承

使用内部类可以实现多重继承,如果是实现多个接口,你可以选择直接使用外围类实现多个接口;如果面对多个非接口(类/抽象类)要继承,那么一个外围类是无法做到的(Java只允许单继承),这时可以使用内部类完成,使用匿名内部类是常见的。

代码1.11定义了两个抽象类,如果要同时再一个类中重写这两个抽象类的方法,使用匿名内部类(见代码1.12)

1
2
3
4
5
6
7
//代码1.11
abstract class A{
public void doA() {}
}
abstract class B{
public void doB() {}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//代码1.12
public class Outer extends A{
@Override
public void doA() {
super.doA();
System.out.println("extends A");
}
public void doB(B b) {
b.doB();
}
public static void main(String[] args) {
Outer outer=new Outer();
outer.doA();
outer.doB(new B() {
@Override
public void doB() {
super.doB();
System.out.println("extends B");
}
});
}
}

《Java编程思想》中对使用内部类的好处做了如下说明:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外围类对象的信息相互独立
  2. 单个外围类中,你可以让多个内部类以不同的方式实现同一个接口/继承同一个类
  3. 创建内部类对象的时刻并不依赖于外部类对象的创建
  4. 内部类没有令人迷惑的“is-a”关系:它就是一个独立的实体
  5. 内部类可以更加完美的实现回调机制
smartpig wechat
扫一扫关注微信公众号
0%