[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. ex3-数字和数字计算

    代码: print("I will now count my chickens:") print("hens", 25+30/6)print("Roo ...

  2. eclipse配置javah命令

    1.找到javah命令所在的目录    我的为 /usr/bin/javah 2.打开eclipse     如图点击第二项 3.配置  如图 ${project_loc}/src -classpat ...

  3. windows下 安装Kali Linux到 U盘的方法

    作者:玄魂工作室 \ 2016年10月20日 把Kali Linux安装到U盘好处很多,可以从U盘启动使用整个电脑的硬件资源, 可以随身携带,减少对自己电脑的影响. 今天要给大家讲的是如何在windo ...

  4. .NET中那些所谓的新语法之一:自动属性、隐式类型、命名参数与自动初始化器

    开篇:在日常的.NET开发学习中,我们往往会接触到一些较新的语法,它们相对以前的老语法相比,做了很多的改进,简化了很多繁杂的代码格式,也大大减少了我们这些菜鸟码农的代码量.但是,在开心欢乐之余,我们也 ...

  5. Atitit wsdl的原理attilax总结

    Atitit wsdl的原理attilax总结 1.1. 在 W3C 的 WSDL 发展史1 1.2. 获取wsdl,可能需要url后面加wsdl,也可能直接url1 1.3. Wsdl的作用2 1. ...

  6. Atitti 大话存储读后感 attilax总结

    Atitti 大话存储读后感 attilax总结 1.1. 大话存储中心思想(主要讲了磁盘文件等存储)1 1.2. 最耐久的存储,莫过于石头了,要想几千万年的存储信息,使用石头是最好的方式了1 1.3 ...

  7. WPF入门教程系列十五——WPF中的数据绑定(一)

    使用Windows Presentation Foundation (WPF) 可以很方便的设计出强大的用户界面,同时 WPF提供了数据绑定功能.WPF的数据绑定跟Winform与ASP.NET中的数 ...

  8. 即时搜索(input框)

    做搜索功能的时候,经常遇到输入框检查的需求,最常见的是即时搜索,今天好好小结一下. 即时搜索的方案: (1)change事件    触发事件必须满足两个条件: a)当前对象属性改变,并且是由键盘或鼠标 ...

  9. XML实体引用

    在 XML 中,一些字符拥有特殊的意义. 如果你把字符 "<" 放在 XML 元素中,会发生错误,这是因为解析器会把它当作新元素的开始. 这样会产生 XML 错误: < ...

  10. PinnedHeaderListView实现删除

    项目中用到四个List集合展示一个页面,并且每个页面都会有一个标题栏.找了半天资料决定用PinnedHeaderListView开源项目.最后需求又来了,需要一个删除的功能,又去网上找资料,发现没有实 ...