[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:

#studentcounting

Student.class:

#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://stackoverflow.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

[java] 深入理解内部类: inner-classes的更多相关文章

  1. Java 深入理解内部类

    摘自海子:Java内部类详解 深入理解内部类 1.为什么成员内部类可以无条件访问外部类的成员? 在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译 ...

  2. java 深入理解内部类以及之间的调用关系

    什么是内部类 内部类是指在一个外部类的内部再定义一个类.内部类作为外部类的一个成员,并且依附于外部类而存在的.内部类可为静态,可用protected和private修饰(而外部类只能使用public和 ...

  3. Java抽象类和内部类

    类(class) 类是相似对象中共同属性和方法的集合体 在面向对象中定义类,就是在描述事物,就是在定义属性(变量)和行为(方法).属性和行为共同成为类中的成员(成员变量和成员方法). 封装.继承和多态 ...

  4. Java基础(53):内部类(转)

    java中的内部类总结 内部类不是很好理解,但说白了其实也就是一个类中还包含着另外一个类 如同一个人是由大脑.肢体.器官等身体结果组成,而内部类相当于其中的某个器官之一,例如心脏:它也有自己的属性和行 ...

  5. Effective Java通俗理解(持续更新)

    这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...

  6. Java 中的内部类

    前言 在第一次把Java 编程思想中的内部类这一章撸完后,有点印象.大概知道了什么时内部类,局部内部类,匿名内部类,嵌套内部类.随着时间的推移,自己慢慢的就忘记了,总感觉自己思考的东西不多,于是 看了 ...

  7. Effective Java通俗理解(上)

    这篇博客是Java经典书籍<Effective Java(第二版)>的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约 ...

  8. Java Inner Class 内部类

    内部类  Inner Class 一个内部类可以定义在另一个类里,可以定义在函数里,甚至可以作为一个表达式的一部分. Java中的内部类共分为四种: 静态内部类static inner class ( ...

  9. Java基础(五)--内部类

    内部类简单来说就是把一个类的定义放到另一个类的定义内部 内部类分为:成员内部类.局部内部类.匿名内部类.静态内部类 成员内部类:最常见的内部类 public class Outter { privat ...

随机推荐

  1. android 之HttpURLConnection的post,get方式请求数据

    get方式和post方式的区别: 1.请求的URL地址不同: post:"http://xx:8081//servlet/LoginServlet" get:http://xxx: ...

  2. 回忆那些我们曾今铭记过的.NET重点知识

    正如标题所说的那样,到底是那些.NET的知识点呢?     接下来就让我带着你们去了解这些知识点吧! 1.接口 2.索引器 3.FOREACH的本质 4.匿名内部类 5.运算符的重载 一.什么是接口? ...

  3. USACO翻译:USACO 2012 FEB Silver三题

    USACO 2012 FEB SILVER 一.题目概览 中文题目名称 矩形草地 奶牛IDs 搬家 英文题目名称 planting cowids relocate 可执行文件名 planting co ...

  4. “眉毛导航”——SiteMapPath控件的使用(ASP.NET)

    今天做网站的时候,用到了SiteMapPath控件,我把使用方法记录下来,以便日后查阅以及帮助新手朋友们. SiteMapPath”会显示一个导航路径(也称为痕迹导航或眉毛导航),此路径为用户显示当前 ...

  5. fwrite写入文件不成功bug

    文件写入了,只是从头覆盖了!因为在fwrite():前面 文件位置指针是是SEEK_SET,即首位置.在fwrite(fileHeader, 1, 10, file); 前面加上一行fseek(fil ...

  6. window.location.href 中文乱码问题。。。。

    window.location.href 中文乱码问题.... 要解决此问题需要两次解码, 第一次解码: 是在页面中的js脚本中解码:window.location.href = "save ...

  7. 用jdbc访问二进制类型的数据

    package it.cast.jdbc; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; impor ...

  8. [学习笔记] Inten

  9. (源码下载)高灵活度,高适用性,高性能,轻量级的 ORM 实现

    我在上一篇博客中简单说明了一个面向内存数据集的“ORM”的实现方法,也提到我的设计实现或许不能称之为“ORM”,姑且称之为 S-ORM吧. 可能有些小伙伴没有理解我的思路和目的,与传统ORM框架做了简 ...

  10. java中文乱码解决之道(五)-----java是如何编码解码的

    在上篇博客中LZ阐述了java各个渠道转码的过程,阐述了java在运行过程中那些步骤在进行转码,在这些转码过程中如果一处出现问题就很有可能会产生乱码!下面LZ就讲述java在转码过程中是如何来进行编码 ...