前言

整理了一下内部类的相关知识,算是比较全,比较基础的,希望大家一起学习进步。

一、什么是内部类?

在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性 定义方式一致。

一个内部类的例子:

public class Outer {

    private int radius = 1;
public static int count = 2; public Outer() {
} class inner{
public void visitOuter() {
System.out.println("visit outer private member variable:" + radius);
System.out.println("visit outer static variable:" + count);
}
}
}

二、内部类的种类

内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类

静态内部类

定义在类内部的静态类,就是静态内部类。

public class Outer {

    private static int radius = 1;

    static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}

静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,new 外部类.静态内部类(),如下:

Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();

成员内部类

定义在类内部,成员位置上的非静态类,就是成员内部类。

public class Outer {

    private static  int radius = 1;
private int count =2; class Inner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}

成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类() ,如下:

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();

局部内部类

定义在方法中的内部类,就是局部内部类。

public class Outer {

    private  int out_a = 1;
private static int STATIC_b = 2; public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}

定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类(),如下:

 public static void testStaticFunctionClass(){
class Inner {
}
Inner inner = new Inner();
}

匿名内部类

匿名内部类就是没有名字的内部类,日常开发中使用的比较多。

public class Outer {

    private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
//匿名内部类必须继承或实现一个已有的接口
interface Service{
void method();
}

除了没有名字,匿名内部类还有以下特点:

  • 匿名内部类必须继承一个抽象类或者实现一个接口。
  • 匿名内部类不能定义任何静态成员和静态方法。
  • 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
  • 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。

匿名内部类创建方式:

new 类/接口{
//匿名内部类实现部分
}

三、内部类的优点

我们为什么要使用内部类呢?因为它有以下优点:

  • 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
  • 内部类不为同一包的其他类所见,具有很好的封装性;
  • 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
  • 匿名内部类可以很方便的定义回调。

一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!

public class Outer {

    private  int radius = 1;

    protected void test(){
System.out.println("我是外部类方法");
} class Inner {
public void visit() {
System.out.println("访问外部类变量" + radius);
test();
}
}
}

我们可以看到,内部类Inner是可以访问外部类Outer的私有变量radius或者方法test的。

内部类不为同一包的其他类所见,具有很好的封装性

当内部类使用 private修饰时,这个类就对外隐藏了。当内部类实现某个接口,并且进行向上转型,对外部来说,接口的实现已经隐藏起来了,很好体现了封装性。

//提供的接口
interface IContent{
String getContents();
} public class Outer {
//私有内部类屏蔽实现细节
private class PContents implements IContent{
@Override
public String getContents() {
System.out.println("获取内部类内容");
return "内部类内容";
}
} //对外提供方法
public IContent getIContent() {
return new PContents();
} public static void main(String[] args) {
Outer outer=new Outer();
IContent a1=outer.getIContent();
a1.getContents();
}
}

我们可以发现,Outer外部类对外提供方法getIContent,用内部类实现细节,再用private修饰内部类,屏蔽起来,把Java的封装性表现的淋漓尽致。

内部类有效实现了“多重继承”,优化 java 单继承的缺陷。

我们知道Java世界中,一个类只能有一个直接父类,即以单继承方式存在。但是内部类让“多继承”成为可能:

  • 一般来说,内部类继承某个类或者实现某个接口,内部类的代码操作创建它的外围类的对象。内部类提供了某种进入其外围类的窗口。
  • 每个内部类都可以队里的继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类没有影响
  • 接口解决了部分问题,一个类可以实现多个接口,内部类允许继承多个非接口类型(类或抽象类)。

一份来自Java编程思想,内部类实现“多继承”的温暖如下:

class D {}
abstract class E{}
class Z extends D {
E makeE(){ return new E() {}; }
} public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args){
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}

代码中出现了一个类D,一个抽象类E。然后,用类Z继承D,内部类构造返回E。因此,当你不管要的是D还是E,Z都可以应付,“多继承”的特点完美表现出来。

匿名内部类可以很方便的定义回调。

什么是回调?假设有两个类A和B,在A中调用B的一个方法b,而b在执行又调用了A的方法c,则c就称为回调函数。



当然,回调函数也可以是a函数,这就是同步回调,最简单的回调方式。

回调应用场景挺多的,如android中的事件监听器。匿名内部类可以很方便的定义回调,看个例子

//定义一个CallBack接口
public interface CallBack {
void execute();
} public class TimeTools { /**
* 测试函数调用时长,通过定义CallBack接口的execute方法
* @param callBack
*/
public void testTime(CallBack callBack) {
long beginTime = System.currentTimeMillis(); //记录起始时间
callBack.execute(); ///进行回调操作
long endTime = System.currentTimeMillis(); //记录结束时间
System.out.println("[use time]:" + (endTime - beginTime)); //打印使用时间
} public static void main(String[] args) {
TimeTools tool = new TimeTools();
tool.testTime(new CallBack(){
//匿名内部类,定义execute方法
public void execute(){
TestTimeObject testTimeObject = new TestTimeObject();
testTimeObject.testMethod();
}
});
}
}

在调用testTime()测时间的时候,用匿名内部类实现一个方法execute(),在该方法内搞事情(执行目标函数),执行完后,又回到testTime方法,很好了实现测试函数调用时长的功能。显然,匿名内部类让回调实现变得简单

四、内部类的底层

内部类标志符

每个内部类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息。内部类也必须生成一个.class文件以包含它们的Class对象信息。内部类文件的命名有严格规则:外围类的名字+$+内部类的名字。

一个简单例子:

public class Outer {
class Inner{
}
}

javac Outer.java编译完成后, 生成的class文件如下:

如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中(静态内部类),只需直接将它们的名字加在其外围类标志符与“$”的后面。

为什么内部类可以访问外部类的成员,包括私有数据?

由上一小节,我们知道内部类可以访问外部类的成员,包括私有数据。那么它是怎么做到的呢?接下来揭晓答案。

先看这个简单地例子:

public class Outer {

    private int i = 0;

    class Inner{
void method(){
System.out.println(i);
}
}
}

一个外部类Outer,一个外部类私有属性i,一个内部类Inner,一个内部类方法method。内部类方法访问了外部类属性i。

先编译,javac Outer.java,生成.class文件,如下:

javap -classpath . -v Outer$Inner,反编译Outter$Inner.class文件得到以下信息:

我们可以看到这一行,它是一个指向外部类对象的指针:

final innerclass.Outer this$0;

虽然编译器在创建内部类时为它加上了一个指向外部类的引用, 但是这个引用是怎样赋值的呢?编译器会为内部类的构造方法添加一个参数,进行初始化, 参数的类型就是外部类的类型,如下:

innerclass.Outer$Inner(innerclass.Outer);

成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。

局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?

局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?

先看这段代码:

public class Outer {

    void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
} }
}
}

反编译(Outer$1Inner)得到以下信息

我们在内部类innerMethod方法中,可以看到以下这条指令:

3: bipush   10
  • 它表示将常量10压入栈中,表示使用的是一个本地局部变量。
  • 其实,如果一个变量的值在编译期间可以确定(demo中确定是10了),则编译器会默认在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。
  • 酱紫可以确保局部内部类使用的变量与外层的局部变量区分开,它们只是值相等而已。

以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。

我们再来看一段代码,其实就是把变量a挪到传参方式进来

public class Outer {

    void outMethod(final int a){
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}

反编译可得



我们看到匿名内部类Outer$1Inner的构造器含有两个参数,一个是指向外部类对象的引用,一个是int型变量,很显然,这里是将变量innerMethod方法中的形参a以参数的形式传进来对匿名内部类中的拷贝(变量a的拷贝)进行赋值初始化。

那么,新的问题又来了,既然在innerMethod方法中访问的变量a和outMethod方法中的变量a不是同一个变量,当在innerMethod方法中修改a会怎样?那就会造成数据不一致的问题了。

怎么解决呢?使用final修饰符,final修饰的引用类型变量,不允许指向新的对象,这就解决数据不一致问题。注意: 在Java8 中,被局部内部类引用的局部变量,默认添加final,所以不需要添加final关键词。

五、内部类的应用场景。

一般我们在哪些场景下使用内部类呢?

场景之一:一些多算法场合

一些算法多的场合,也可以借助内部类,如:

Arrays.sort(emps,new Comparator(){
Public int compare(Object o1,Object o2)
{
return ((Employee)o1).getServedYears()-((Employee)o2).getServedYears();
}
});

场景二:解决一些非面向对象的语句块。

如果一些语句块,包括if…else语句,case语句等等比较多,不好维护扩展,那么就可以借助内部类+设计模式解决。

场景之三:适当使用内部类,使得代码更加灵活和富有扩展性。

适当的使用内部类,可以使得你的代码更加灵活和富有扩展性。如JDK的lamda表达式,用内部类非常多,代码优雅很多。如下

// JDK8 Lambda表达式写法
new Thread(() -> System.out.println("Thread run()")).start();

场景四:当某个类除了它的外部类,不再被其他的类使用时。

如果一个类,不能为其他的类使用;或者出于某种原因,不能被其他类引用。那我们就可以考虑把它实现为内部类。数据库连接池就是这样一个典型例子。

六、内部类常见面试题

最后,我们来看一道经典内部类面试题吧。

public class Outer {
private int age = 12; class Inner {
private int age = 13;
public void print() {
int age = 14;
System.out.println("局部变量:" + age);
System.out.println("内部类变量:" + this.age);
System.out.println("外部类变量:" + Outer.this.age);
}
} public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.print();
} }

运行结果:

参考与感谢

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。

Java程序员必备基础:内部类解析的更多相关文章

  1. Java程序员必备基础结构图

    前言 最近看了深入理解Java虚拟机第三版,整理了一些基础结构图,算是比较全的了,做一下笔记,大家一起学习. 1.Java虚拟机运行时数据区图 JVM内存结构是Java程序员必须掌握的基础. 程序计数 ...

  2. Java程序员必备基础:JDK 5-15都有哪些经典新特性

    前言 JDK 15发布啦~ 我们一起回顾JDK 5-15 的新特性吧,大家一起学习哈~ 本文已经收录到github ❝ https://github.com/whx123/JavaHome ❞ 「公众 ...

  3. Java程序员必备的一些流程图

    Java程序员必备的一些流程图 转自https://juejin.im/post/5d214639e51d4550bf1ae8df 前言: 整理了一些Java基础流程图/架构图,做一下笔记,大家一起学 ...

  4. Java程序员必备的 15框开发工具

    15款Java程序员必备的开发工具 如果你是一名Web开发人员,那么用膝盖想也知道你的职业生涯大部分将使用Java而度过.这是一款商业级的编程语言,我们没有办法不接触它. 对于Java,有两种截然不同 ...

  5. Java 程序员必备的 15 个框架,前 3 个地位无可动摇!

    Java 程序员方向太多,且不说移动开发.大数据.区块链.人工智能这些,大部分 Java 程序员都是 Java Web/后端开发.那作为一名 Java Web 开发程序员必须需要熟悉哪些框架呢? 今天 ...

  6. Android java程序员必备技能,集合与数组中遍历元素,增强for循环的使用详解及代码

    Android java程序员必备技能,集合与数组中遍历元素, 增强for循环的使用详解及代码 作者:程序员小冰,CSDN博客:http://blog.csdn.net/qq_21376985 For ...

  7. 干货收藏 | Java 程序员必备的一些流程图

    阅读本文大概需要 6 分钟. 转载自:https://juejin.im/post/5d214639e51d4550bf1ae8df 1.Spring 的生命周期 Spring 作为当前 Java 最 ...

  8. Java 程序员必备的一些流程图

    1.spring的生命周期 2.TCP三次握手,四次挥手 3.线程池执行流程图 4.JVM内存结构 5.Java内存模型 6.springMVC执行流程图 7.JDBC执行流程 8.spring cl ...

  9. 程序员必备基础:Git 命令全方位学习

    前言 掌握Git命令是每位程序员必备的基础,之前一直是用smartGit工具,直到看到大佬们都是在用Git命令操作的,回想一下,发现有些Git命令我都忘记了,于是写了这篇博文,复习一下~ https: ...

随机推荐

  1. H3C LMI协议标准

  2. 714 - Copying Books——[贪心、二分查找]

    Before the invention of book-printing, it was very hard to make a copy of a book. All the contents h ...

  3. 2018-8-10-win10-uwp-获取文件夹出错

    title author date CreateTime categories win10 uwp 获取文件夹出错 lindexi 2018-08-10 19:16:50 +0800 2018-2-1 ...

  4. 使用Python内置的smtplib包和email包来实现邮件的构造和发送。

    此文章github地址:https://github.com/GhostCNZ/Python_sendEmail Python_sendEmail 使用Python内置的smtplib包和email包 ...

  5. tf.squeeze()

    转载自:https://www.cnblogs.com/mdumpling/p/8053376.html 原型 tf.squeeze(input, squeeze_dims=None, name=No ...

  6. linux /proc 接口

    无论何时一个硬件中断到达处理器, 一个内部的计数器递增, 提供了一个方法来检查设备 是否如希望地工作. 报告的中断显示在 /proc/interrupts. 下面的快照取自一个双处理 器 Pentiu ...

  7. vue 实例未加载完成显示 花括号解决方案

    css [v - cloak] { display: none!important; } html < div id = "app" v-cloak >

  8. dotnet 通过 WMI 获取系统启动的服务

    本文告诉大家如何通过 WMI 获取系统启动的服务 通过 Win32_Service 可以获取系统启动的服务 获取的时候只需要拿Caption和State就可以 var mc = "Win32 ...

  9. 洛谷——P1111修复公路(并查集)

    题目背景 AA地区在地震过后,连接所有村庄的公路都造成了损坏而无法通车.政府派人修复这些公路. 题目描述 给出A地区的村庄数NN,和公路数MM,公路是双向的.并告诉你每条公路的连着哪两个村庄,并告诉你 ...

  10. javascript拷贝

    function copy(obj){ //浅拷贝 var result = {}; for(var attr in obj){ result[attr] = obj[attr]; } return ...