[java] 深入理解内部类: inner-classes

时间:2023-07-30 18:22:56

[java] 深入理解内部类: inner-classes

[java] 深入理解内部类: inner-classes

1 简介

内部类就是定义在其他类中的类, 那么为什么要具有这样的特性呢?

  1. 内部类能够访问该类外部类的所有成员变量, 以及所有方法, 包括私有的成员.
  2. 内部类将它的可见性进行了一定的隐藏, 使得同一个package中的其他类不能直接的对其进行访问.

下面将通过一个个案例来对内部类进行深入的解释, 欢迎看客的各种建议.

2 案例

一个小学教室, 数学老师在教小朋友们数数字, 老师说一个数, 如n, 那么小朋友就从1开始以n为间隔进行数数, 如1, 1+n, 1+n+n, …, 老师说停, 那么小朋友便停止数数 (这里假设小朋友的数数速度是均匀的, 1 number/s).

2.1 不使用内部类的实现

import java.awt.event.*;
import javax.swing.*; public class Demo {
public static void main(String[] args) {
// 为了示例的简洁
// 这里直接默认学生的名字为: xiao ming
// 数数的步长为10
Student s = new Student("xiao ming");
s.startCount(10); JOptionPane.showMessageDialog(null, "停止数数.");
System.exit(0);
}
} /**
学生类, 仅包含学生的名字信息
*/
class Student {
public Student(String name) {
this.name = name;
} public void startCount(int step) {
// 为了能够知道谁在计数
// 这里需要将学生的名字信息传递给计数类
ActionListener count = new Counting(step, name);
Timer t = new Timer(1000, count);
t.start();
} private String name;
} /**
用来完成每隔一定时间实施数数功能
*/
class Counting implements ActionListener {
public Counting(int step, String name) {
this.step = step;
this.name = name;
num = 1;
} // 进行数数
public void actionPerformed(ActionEvent event) {
System.out.println(name + " count: " + num);
num = num + step;
} private int step;
private int num;
private String name;
}

从上面的示例中, 我们发现, 如果使用外部类进行实现该功能, 那么该计数类将能够被包中其他的类使用, 降低了该功能的封装性. 这正好可以通过使用内部类的方法进行解决.

2.2 内部类的实现

import java.awt.event.*;
import javax.swing.*; public class Demo {
public static void main(String[] args) {
// 为了示例的简洁
// 这里直接默认学生的名字为: xiao ming
// 数数的步长为10
Student s = new Student("xiao ming");
s.startCount(10); JOptionPane.showMessageDialog(null, "停止数数.");
System.exit(0);
}
} /**
学生类, 仅包含学生的名字信息
*/
class Student {
public Student(String name) {
this.name = name;
} public void startCount(int step) {
// 为了能够知道谁在计数
// 这里需要将学生的名字信息传递给计数类
ActionListener count = new Counting(step);
Timer t = new Timer(1000, count);
t.start();
} private String name; /**
用来完成每隔一定时间实施数数功能
内部类中可以直接使用外部类中的成员变量 - name
*/
private class Counting implements ActionListener {
public Counting(int step) {
this.step = step;
num = 1;
} // 进行数数
public void actionPerformed(ActionEvent event) {
System.out.println(name + " count: " + num);
num = num + step;
} private int step;
private int num;
}
}

这样包内的其他类就不能直接调用Counting类了.

3 有趣的事情开始发生了

用外部类实现的示例编译后产生的类文件有:

Demo.class, Student.class, Counting.class

而内部类示例编译后产生的类文件为:

Demo.class, Student.class, Student$Counting.class

单单从编译后的结果来看, java虚拟机在具体类的载入以及实现过程中, 应该和"外部类实现","内部类实现"没有关系. 那么"Student$Counting.class"这个神奇的类中到底包含了什么呢?

下面就来揭示这个秘密吧, 采用的工具为java decompiler.

反编译后的结果如下, Student$Counting.class:

[java] 深入理解内部类: inner-classes #studentcounting

Student.class:

[java] 深入理解内部类: inner-classes #student

从Student$Counting.class中发现, 它的构造函数的参数列表中多了一个变量:

// before
public Counting(int paramInt) {
this.step = paramInt;
this.num = 1;
} // after
public Student$Counting(Student paramStudent, int paramInt) {
// ...
}

但是仍然没有可视化的给出, 到底将这个变量赋值给了谁? 尝试的寻找更合适的java反向编译的软件, 花了近2个多小时的时间, 没有找到合适的, 哎, 算了, 反正不是为了得到反向的所有的代码, 只是专注于类内部的成员变量以及成员函数, 就采用java.lang.reflect中的类实现了从一个class文件中提取类名, 成员变量以及函数的.

3.1 从类文件中提取成员函数, 变量以及构造函数

在reflect库中包含了一下三个类Field, Method, Constructor用来分别获得和成员变量, 成员函数, 以及构造函数相关的内容. 具体实现如下1:

import java.lang.reflect.*;
import javax.swing.*; public class ReflectionTest {
public static void main(String[] args) {
// 从命令行中输入待处理文件, 或是由用户输入
String name;
if (args.length > 0)
name = args[0];
else
name = JOptionPane.showInputDialog
("Class name (e.g. java.util.Date): "); try {
// 输出类名以及父类
Class cl = Class.forName(name);
Class supercl = cl.getSuperclass();
System.out.print("class " + name);
if (supercl != null && supercl != Object.class) {
System.out.print(" extends " + supercl.getName());
} System.out.print(" {\n");
printConstructors(cl);
System.out.println();
printMethods(cl);
System.out.println();
printFields(cl);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} /**
输出一个类的所有构造函数
*/
public static void printConstructors(Class cl) {
Constructor[] constructors = cl.getDeclaredConstructors(); for (int i = 0; i < constructors.length; i++) {
Constructor c = constructors[i];
String name = c.getName();
System.out.print("\t" + Modifier.toString(c.getModifiers()));
System.out.print(" " + name + "("); // 输出参数类型
Class[] paramTypes = c.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
} /**
输出一个类的所有成员方法
*/
public static void printMethods(Class cl) {
Method[] methods = cl.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
Class retType = m.getReturnType();
String name = m.getName();
System.out.print("\t" + Modifier.toString(m.getModifiers()));
System.out.print(" " + retType.getName() + " " + name + "("); // 输出参数类型
Class[] paramTypes = m.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
if (j > 0) System.out.print(", ");
System.out.print(paramTypes[j].getName());
}
System.out.println(");");
}
} /**
输出所有的成员变量
*/
public static void printFields(Class cl) {
Field[] fields = cl.getDeclaredFields(); for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
Class type = f.getType();
String name = f.getName();
System.out.print("\t" + Modifier.toString(f.getModifiers()));
System.out.println(" " + type.getName() + " " + name + ";");
}
}
}

3.2 解释为什么能够在内部类直接访问外部类的成员变量

运行:

javac ReflectionTest.java
java ReflectionTest 'Student$Counting'

输出为:

class Student$Counting {
public Student$Counting(Student, int); public void actionPerformed(java.awt.event.ActionEvent); private int step;
private int num;
final Student this$0;
}

然后再结合该类的反编译的结果图, 就可以发现通过在构造函数中增加Student变量, 并且赋值给了 final Student this$0, 这就解释了 为什么在内部类中能够访问外部类的成员变量 , 但是又为什么在内部类中能够访问 外部类的私有变量 呢?

让我们来看一下Student$Counting中另一处神奇的地方.

System.out.println(Student.access$000(this.this$0) + " count: " + this.num);

可见, Student类中又多了一个成员函数, 但是在Java Decompiler软件的结果中并没有发现, 于是再次采用了ReflectionTest, 其结果为:

class Student {
public Student(java.lang.String); public void startCount(int);
static java.lang.String access$000(Student); private java.lang.String name;
}

可见编译后的Student中出现了一个新的静态方法, 我们可以很容易的猜测到该静态方法的实现类似如下:

static String access$000(Student s) {
return name;
}

编译器通过这样的方式实现了从一个类中访问另一个类的私有变量, 那么通过直接修改class文件, 使得获取或改变类中的私有变量成为了可能, 当然要进行实现需要更强大的能力了.

4 局部内部类实现

使用局部内部类在代码编写的过程中其他的类均失去了对它的调用能力, 此时Student如下:

/**
学生类, 仅包含学生的名字信息
*/
class Student {
public Student(String name) {
this.name = name;
} public void startCount(final int step) {
/**
用来完成每隔一定时间实施数数功能
局部内部类中可以直接使用外部类中的成员变量 - name
还可以直接调用final的局部变量
*/
class Counting implements ActionListener {
// 进行数数
public void actionPerformed(ActionEvent event) {
System.out.println(name + " count: " + num);
num = num + step;
} private int num = 1;
} // 为了能够知道谁在计数
// 这里需要将学生的名字信息传递给计数类
ActionListener count = new Counting();
Timer t = new Timer(1000, count);
t.start();
} private String name; }

此时编译后新生成的类有:

Demo.class  Student$1Counting.class  Student.class

继续使用ReflectionTest, 分别对后两个类文件进行处理, 结果如下:

// Student$1Counting.class
class Student$1Counting {
Student$1Counting(Student, int); public void actionPerformed(java.awt.event.ActionEvent); private int num;
final int val$step;
final Student this$0;
} // Student.class
class Student {
public Student(java.lang.String); public void startCount(int);
static java.lang.String access$000(Student); private java.lang.String name;
}

可见通过编译后, 局部变量和外部类, 都成为了内部类的成员变量. 局部变量的可用性, 使得整个程序更加的简单明了.

5 匿名的内部类

匿名内部类的实现也可以采用类似的方式实现.

匿名内部类的实现格式如下:

new SuperType(construction parameters) {
inner class methods and data
}

其实, 可以通过一下方式使得对匿名内部类有一个更清楚的了解:

Person count = new class extends Person("") {...}; // 该形式并不是真正的java格式,只是为了帮助理解
Person count = new Person("") {...}; // 这才是正确的格式

匿名内部类, 还有一个比较常用的方法就是利用初始化块对一个变量进行初始化:

 private List<String> maleList = new ArrayList<String>() {
// 初始化块
{
add("Machael");
add("Scorfield");
add("Other");
}
};

想要更清楚的理解上述的初始化块, 可以继续看下面的示例:

class Person {
private String name; {
System.out.println("初始化块 in class Person");
} public Person() {
this.name = null;
} public Person(String name) {
setName(name);
} public void setName(String name) {
this.name = name;
} public String getName() {
return name;
}
} public class Demo {
public static void main(String[] args) {
Person p1 = new Person("xiao ming");
System.out.println("直接初始化: " + p1.getName()); System.out.println(); Person p2 = new Person() {
// 初始化块
{
System.out.println("初始化块 in 匿名类");
setName("xiao ming");
}
};
System.out.println("双括号初始化: " + p2.getName());
}
}

其结果为:

初始化块 in class Person
直接初始化: xiao ming 初始化块 in class Person
初始化块 in 匿名类
双括号初始化: xiao ming

Footnotes:

1 book Core Java, 下载地址: download

补充:

匿名类初始化与直接操作之间的比较, 可以参见 http://*.com/questions/924285/efficiency-of-java-double-brace-initialization

Date: 2014-05-19 Mon

Author: Zhong Xiewei

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0