类的成员-内部类
类的成员之五:内部类
# 概述
- 当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。
- 在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。
Inner class
一般用在定义它的类或语句块之内,在外部引用它时必须给出完整的名称。Inner class
的名字不能与包含它的外部类类名相同;
- 分类
- 成员内部类(
static
成员内部类和非static
成员内部类) - 局部内部类(不谈修饰符)、匿名内部类
- 成员内部类(
# 成员内部类
成员内部类作为类的角色
- 可以在内部定义属性、方法、构造器等结构
- 可以声明为
abstract
类,因此可以被其它的内部类继承 - 可以声明为
final
的,言外之意没有final
修饰的,可以被继承 - 可以调用外部类的结构
- 可以被4种权限修饰
- 编译以后生成
OuterClass$InnerClass.class
字节码文件(也适用于局部内部类)
class Person{
public void eat(){
……
}
//抽象
abstract static Bird{
}
//静态成员内部类
static class Dog{
//定义属性
String name;
int age;
//定义方法
public void show(){
……
}
}
//非静态成员内部类
class Cat{
//定义属性
double weight;
//定义构造器
public Cat(){
……
}
//定义方法
public void miao(){
//调用外部类的方法
eat();//实际上是Person.this.eat();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
成员内部类作为类的成员的角色
- 和外部类不同,
Inner class
还可以声明为private
或protected
; - 可以调用外部类的结构
- Innerclass可以声明为static的,但此时就不能再使用外层类的非static的成员变量;
# 局部内部类
class Person{
public void method(){
//方法内的局部内部类
class AA{
}
}
{
//代码块内的局部内部类
class BB{
}
}
public Person(){
//构造器内的局部内部类
class CC{
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 区别
如何实例化成员内部类的对象
创建静态的成员内部类:
Person.dog dog = new Person.dog();
dog.show();
2
创建非静态的成员内部类
Person p = new Person();
Person.Cat cat = p.new Cat();
cat.miao();
2
3
如何在成员内部类区分外部类的结构
在内部类调用:
- 形参直接使用
- 内部类的属性或方法:
this.属性/方法
- 内部类调用外部类的属性或方法:
外部类名.this.属性/方法
# 为什么需要内部类 (opens new window)
在某些业务场景下,需要重写某个类的方法,然后在这个方法体内使用,就可以使用局部内部类。
至此,我们已经看到了许多描述内部类的语法和语义,但是这并不能同答“为什么需要内部类”这个问题。那么,Java 设计者们为什么会如此费心地增加这项基本的语言特性呢?
一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外部类的对象。所以可以认为内部类提供了某种进入其外部类的窗口。
内部类必须要回答的一个问题是:如果只是需要一个对接口的引用,为什么不通过外部类实现那个接口呢?答案是:“如果这能满足需求,那么就应该这样做。”那么内部类实现一个接口与外部类实现这个接口有什么区别呢?答案是:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:
每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(译注:类或抽象类)。
# 匿名内部类
下面的例子看起来有点奇怪:
// innerclasses/Parcel7.java
// Returning an instance of an anonymous inner class
public class Parcel7 {
public Contents contents() {
return new Contents() { // Insert class definition
private int i = 11;
@Override
public int value() {
return i;
}
}; // Semicolon required
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个匿名内部类中,使用了默认的构造器来生成 Contents。下面的代码展示的是,如果你的基类需要一个有参数的构造器,应该怎么办:
// innerclasses/Parcel8.java
// Calling the base-class constructor
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- [1] 将合适的参数传递给基类的构造器。
- [2] 在匿名内部类末尾的分号,并不是用来标记此内部类结束的。实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。
尽管 Wrapping 只是一个具有具体实现的普通类,但它还是被导出类当作公共“接口”来使用。
// innerclasses/Wrapping.java
public class Wrapping {
private int i;
public Wrapping(int x) { i = x; }
public int value() { return i; }
}
2
3
4
5
6
为了多样性,Wrapping 拥有一个要求传递一个参数的构造器。
在匿名类中定义字段时,还能够对其执行初始化操作:
// innerclasses/Parcel9.java
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");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果在定义一个匿名内部类时,它要使用一个外部环境(在本匿名内部类之外定义)对象,那么编译器会要求其(该对象)参数引用是 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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输出为:
Base constructor, i = 47
Inside instance initializer
In anonymous f()
2
3
在此例中,不要求变量 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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
输出为:
Over budget!
在实例初始化操作的内部,可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是 if 语句)。所以对于匿名类而言,实例初始化的实际效果就是构造器。当然它受到了限制-你不能重载实例初始化方法,所以你仅有一个这样的构造器。
匿名内部类与正规的继承相比有些受限,因为匿名内部类要么继承类,要么实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。