【Java】成员内部类

一、书写格式

1、成员内部类 是写在成员位置(类中,方法外)的,没有用 static 修饰的。它是属于外部类的成员之一。

例如我们刚刚写的 Car类 中的 Engine类,这里的 Engine类 就是 成员内部类

image-20240418195548704

2、这个 Engine内部类 跟外面的 carName、carAge等这些成员变量 / 成员方法的地位是一模一样的。

因此成员内部类也是可以被修饰符给修饰的:private,默认,protected,public,static等,只要是用来修饰成员变量的,都可以用来修饰成员内部类,并且它们的规则,跟以前也是一样的。

3、成员内部类 中,在 JDK16 之前是不能定义静态变量的,在 JDK16 开始的时候才可以定义静态变量。

我们现在使用的是最新版本的JDK,是完全可以在 成员内部类 中去定义静态变量的。

image-20240418203313655

但是在很多课程中,关于第三点都没有讲,因为在当时JDK16还没出来。


二、用不同的权限修饰符修饰

1)使用 private 修饰

例如下图,如果使用 private 去修饰成员内部类,那么在外界就不能直接创建成员内部类的对象,只能在外部类的里面去创建内部类的对象。

image-20240418202023571

如果你不太理解,那么你可以换成成员变量 carName 去思考,如果将 carName 也用私有去修饰,那么在外界也不能直接去使用 carName,只能在本来中使用。

成员内部类其实是跟它一样的道理,一旦用 private 去修饰,只能在本类中使用,在外界就不能用了。


2)空着不写(默认)

如果内部类前面权限修饰符空着不写,那就跟 Car类 中的成员变量是一样的了,即 默认权限

默认权限 只能在本包中使用,在其他的包中就用不了了。


3)protected 修饰

protected 可以在本包中的其他类使用,也可以在 其他包的子类 中使用。


4)public 修饰

public 表示公共的,表示在所有的地方都可以直接创建 成员内部类的对象


5)static 修饰

如果使用 static 修饰,那就表示 静态的内部类

静态的内部类 一会会单独的再说,所以说我们现在先达成一个共识:在以后,凡是我们说 成员内部类,一定不是用 static 修饰的。如果用 static 修饰,那这个时候就不叫 成员内部类 了,会叫做 静态内部类


三、创建 成员内部类 的对象

1)方式一:在外部类中编写方法,对外提供内部类的对象

这种方式并不是所有的情况都用,一般来讲我们会用在:使用 private 修饰 内部类 的情况下。

但是如果 内部类 没有用 private 修饰,而是其他的修饰符去修饰的,那么一般我们会使用第二种方式去获取它的对象。

下面来带大家看一下 ArrayList 的源码:ctrl + N 找到 java.util 包下的 ArrayList

image-20240418192640218

ctrl + F12 找到 Itr内部类

image-20240419071051314

在以后,我们学习高级的遍历方式——迭代器的时候,就需要获取到这个迭代器的对象,但是发现,这个 内部类private 修饰了。

此时可以在外部类中编写方法,对外提供内部类的对象。

仔细看,就在 Itr内部类 的上面,Java就提供了该方法!

并且仔细看它返回的类型,不是 Itr类型,而是 Itr类 实现的 Iterator<E>接口 的类型!

因此在外界我们也用这个接口的类型去接收就行了。相当于就形成了 多态 的形式。

image-20240419071517476


2)方式二:在外部类直接创建内部类对象

当成员内部类被非私有修饰时,我们一般都是直接创建对象。

// 格式
外部类名.内部类名 对象名 = 外部类对象.内部类对象;

3)在外部类直接创建内部类对象的格式详解

Outer.java

package com.itheima.a02innerclassdemo2;
public class Outer {
    private class Inner{
    }
}

在以前创建对象的方式:

// 类名 对象名 = new 类名();
Student s = new Student();

接下来我们就一起来推导一下创建内部类的方式 外部类名.内部类名 对象名 = 外部类对象.内部类对象;

我们需要思考:我要创建的是谁的对象?当然是 内部类的对象,这个代码中的 内部类 就叫做 Inner

但在之前我们又说了:Inner 单独存在是没有意义的,因此我们还需要给它加一个外部类的标识:Outer.

合起来 Outer.Inner 就表示:现在我创建的这个对象是 Outer 这个外部类中的 Inner 这个内部类的对象,这个就是等号左边的类名。

这个类名我们也叫做 数据类型

后面我们再去写这个对象的名字,这里取名为 oi,即在创建对象的时候,使用 oi 这个变量去记录对象的地址值。


在看等号的右边之前,先思考,在之前,如果我们想要使用 Outer类 中的 name属性,都是先创建 Outer 的对象,然后再把这个对象的地址值去赋值给一个变量。然后再用这个变量去调用 name

Outer o = new Outer();
System.out.println(o.name);

但是你有没有想过,这个 o,假设我不写,直接用对象去调用 name,也是完全可以的。

这样写的话就是我们之前所讲的 链式编程,这样直接让我们少定义一个变量。

System.out.println(new Outer().name);

此时要思考为什么要这么写?

这是因为 name 是一个 成员变量,如果想要调用,那么只能通过对象去调用。


此时再来反过来推导 成员内部类,在刚刚我们曾经讲过,成员内部类 其实就跟 成员变量 / 成员方法 是一模一样的。

那现在想要调用 成员内部类,那是不是也得用外部类的对象去调用?

因此在等号右边,我需要先去创建一个外部类的对象,再去调用里面的成员 Inner

Outer.Inner oi = new Outer().Inner

但是此时我调用的 Inner 它并不是一个变量,我要获取的时它的对象,因此就变成了最终写法。

Outer.Inner oi = new Outer().new Inner();

因此通过这种方式,我们就能获取到 成员内部类的对象,其中等号的右边非常的难以理解,但是我们可以把它分成两部分:左边部分 new Outer() 就可以把它理解为:外部类的对象,它是一个 调用者,用来调用后面成员内部类的对象 new Inner() ,获取到这个对象后,再去把这个对象的地址值赋值给 变量oi


知道获取对象的方式之后,接下来我们再反过来去研究上面的细节就非常的轻松了。

四、细说方法一:在外部类中编写方法,对外提供内部类的对象

1)引出问题

当我使用 private 去修饰 Inner 内部类后,IDEA立马爆红报错,在外界直接获取不了了!

image-20240418211640318

我们可以用鼠标点击红色,然后 alt + 回车,这时就可以看见IDEA给我们的一些解决方案,其中最简单的解决方案就是:Make 'Inner' public —— 将 'inner内部类' 变成 'public' 的

image-20240418211803403

但此时我就是想要创建内部类的对象,有没有办法呢?

此时就要说到 方法一 了:在外部类中编写方法,对外提供内部类的对象

Outer.java

package com.itheima.a02innerclassdemo2;

public class Outer {
    String name;

    private class Inner{
    }

    public Inner getInstance(){
        return new Inner();
    }
}

此时在外界我们就不需要直接创建内部类的对象了,而是先创建外部类的对象,然后使用这个对象去调用刚刚写的 getInstance() 方法,这样就可以获取到内部类的对象了。

// Outer.Inner oi = new Outer().new Inner();

Outer o = new Outer();
Outer.Inner inner = o.getInstance();

在等号的左边其实还有个小细节:在等号的左边,如果 数据类型 还是写 Outer.Inner 的话,代码就会报错:'com.itheima.a02innerclassdemo2.Outer.Inner' 在 'com.itheima.a02innerclassdemo2.Outer' 中具有私有访问权限

报错的原因是因为,Inner类 现在是一个私有的,那么在外界,根本就不知道 Outer类 中含有 Inner类,因此我们不能直接这么去表示。

image-20240418212735651


2)解决问题

那怎么去写呢?有两种方式。

1、方法一:使用 Inner 的父类类型形成一个多态

看代码,可以发现 Inner类 没有父类。

image-20240418213155475

既然没有父类,那就是默认继承 Object类,因此在这里我们只需要使用 Object 去进行接收就行了

Outer o = new Outer();
Object inner = o.getInstance();

2、方法二:等号的左边直接不使用变量接收,而是直接使用方法获取到的对象

Outer o = new Outer();
System.out.println(o.getInstance());

此时打印出来的是对象的地址值。

注意这里有个小细节:在Java中,内部类在表示类型的时候,在底层,是通过 $ 的形式进行区分的。

$左边外部类的类名$右边内部类的类名

image-20240418213614161

因此,之前大家在给变量起名字的时候,曾经给过大家一个小建议:最好不要使用下划线和美元$,因为 下划线 是给 常量 使用的,$ 是给 内部类 使用的。

image-20240418213838474

3)总结

在编写 成员内部类的时候,如果 成员内部类private 去修饰,那么在外界我们是不能直接创建 Inner 的对象的。

如果说,你直接创建,代码就会报错。此时有两种解决方案。

  • 权限修饰符 改为 public

  • 在外部类提供一个方法,在方法中返回内部类的对象

    此时在外界,直接通过方法去获取内部类的对象就可以了


五、在 JDK16 之前是不能定义静态变量的,在 JDK16 开始的时候才可以定义静态变量

我们现在安装的是最新版,但是我们也是可以切换到低版本的JDK的。

在切换之前,我们现在 Inner内部类 中定义一个静态变量。

Outer.java

package com.itheima.a02innerclassdemo2;

public class Outer {
    String name;

    private class Inner{
        static int a = 10;
    }

    public Inner getInstance(){
        return new Inner();
    }
}

此时并没有任何问题,因为现在我们使用的版本是比 JDK16 要高的。

image-20240418214728754

那怎么去切换 JDK 版本呢?

File ——> Project Structure…(项目结构)

image-20240418214837615

先点击左边的 Project,然后在右边的界面中是可以去切换版本的。

红框框起来的序号1 就是说明:你当前项目安装的是哪个版本的JDK,现在我们安装的是17,没有任何问题。

红框框起来的序号2 就是说明:你用哪个版本去编译、去运行当前的项目。

image-20240418215137696

此时你编译 / 运行的版本,一定要小于等于你安装的版本。

我们可以用鼠标点击看,由于我现在安装的是 17,此时我就可以使用 SDK default 默认的 17。

还可以使用比 17 低的所有的版本。

image-20240418215419473

随便选一个就行了,这里就选择 15 了,最后点击 OK 即可。

回到代码中看,此时 static 就已经爆红报错了,用鼠标放在红色波浪线上,可以看见报错信息:在语言级别 '15' 不支持内部类中的静态声明

image-20240418215628510

现在我们再切换回来

image-20240418215801973

此时发现这里的 static 也不报错了。

image-20240418215841812


六、成员内部类获取外部类的成员变量

1)需求

这里通过一个小案例的形式带着大家一起来分析。

在外部类中,我有一个成员变量 a,记录的值是 10。内部类中也有一个 a,值为 20

内部类中还有一个方法 show()show() 中有一个局部变量,名字也为 a,值为 30

需求:让下面的三个输出语句打印的结果分别是 102030,括号中该怎么去写呢?

image-20240419072711116

2)分析

这个问题的难点就是 局部变量成员变量外部类的成员变量 这三者之间重名了。

如果没有重名就很简单,直接调用即可,前面不需要加任何的关键词。

但是现在有重名,该怎么调呢?

之前针对于这种情况我们曾经讲过一个 就近原则,下面代码如果直接打印 a,它就会先在 本方法 中找。

package com.itheima.a03innerclassdemo3;

public class Outer {
    private int a = 10;

    class Inner {
        private int a = 20;

        public void show() {
            int a = 30;
            System.out.println(a);//30
        }
    }
}

但如果我不想在方法中找,我想在本类中的成员位置中找,这时就可以使用 this.a

package com.itheima.a03innerclassdemo3;

public class Outer {
    private int a = 10;

    class Inner {
        private int a = 20;

        public void show() {
            int a = 30;
            System.out.println(a);//30
            System.out.println(this.a);//20
        }
    }
}

外部类第4行中的 a 怎么打印呢?

有同学会说:用 super 调用不就行了吗?但是InnerOuter` 不是继承关系。

正确答案是:Outer.this.a,这种方式获取的就是外部类的成员变量。

package com.itheima.a03innerclassdemo3;

public class Outer {
    private int a = 10;

    class Inner {
        private int a = 20;

        public void show() {
            int a = 30;
            //Outer.this 获取了外部类对象的地址值
            System.out.println(Outer.this.a);//10
            System.out.println(this.a); //20
            System.out.println(a); //30
        }
    }
}

3)内部类的内存图

接下来我们一起来看看上面代码在内存中的过程是什么样的。

image-20240419073745822

首先一开始,Test类 的字节码文件加载到内存,然后虚拟机会自动调用 main方法,此时 main方法 加载进栈。

开始执行 main方法 中的第一行代码:Outer.Inner oi = new Outer().new Inner();。这个时候发现,这句话中用到了 Outer类Inner类,因此它会把 外部类内部类 的字节码文件加载到 内存 当中。

这里要注意,外部类内部类 在内存中是两个独立的字节码文件。

image-20240419074436679

如果你不相信,我可以先带着你去看一看。


打开IDEA,右键 ——> 选择 Open In ——> Explore

image-20240419074709107

此时打开的是本地的Java文件。

然后回到项目的根文件,这里的 basic-code 就是我项目的名称。

image-20240419074806508

basic-code 统计的目录下去找一个 out 文件夹,因为所有的字节码文件都在 out文件夹 中。

image-20240419074919553

双击打开 —— production —— 找到当前的模块,这里是 oop-innerclass

image-20240419075131726

点击进去,找到 a03innerclassdemo3,此时就能很明显的看见 外部类内部类 在内存中是两个独立的字节码文件。

image-20240419075232863


此时我们继续来看内存图。

在刚刚我们已经将 Outer.classOuter$Inner.class 都已经加载到内存当中了,接下来就要来真正的执行这行代码了。

image-20240419074436679

先来看等号的左边,等号的左边相当于就是在栈中开辟了一个空间,这个空间的名字就是这个变量,变量的名字就叫做 oi,变量的类型是 Outer.Inner,那就表示,在这个变量当中,以后能记录 Outer.Inner 这个类型对象的地址值。

image-20240419075631197

然后再来看等号的右边,等号的右边相当于有两个部分,我们先来看第一部分:new Outer()

image-20240419075711098

此时看见 new 关键字了,并且 new 的是 外部类的对象,所以说,在堆内存当中,它会创建一个 外部类的对象,在这个对象里面,它会记录 成员变量a的值:10

image-20240419075942457

再往下,第一部分执行完毕了,再来看第二部分:new Inner()

image-20240419080020824

第2部分又是 new,因此它在堆里面,会再次开辟一个空间,这个空间就是 内部类的对象

内部类的对象 中,它也要存储成员变量的信息,因此它里面就会有一个 int a = 20;,还没完!

与此同时,Java还会给 内部类的对象 去加一个隐藏的成员变量 this,这个 this 就是用来记录 外部类对象的地址值

image-20240419080356176

对象创建完毕了,就需要将右边的地址赋值给左边的变量 oi,此时赋值给 oi 的是 002

反过来想,如果 oi 中记录的是 001 的话,那么 oi 的类型就应该为 Outer,而不是 Outer.Inner

但是现在既然写的是 Outer.inner,那就表示这个变量现在记录的是内部类的地址值 002

到目前为止,第一行代码才算是执行完毕。

image-20240419080655269

接下来再来走第2行,用 oi 调用 show() 方法。

这个时候 show() 方法就会被加载到栈中。

那我们说,方法它是有调用者的,现在 show() 方法是 oi 调的,因此这里的 this 记录的就是调用者的地址值 002

image-20240419080948587

再往下,方法里面我定义了一个局部变量 a,它记录的是 30,所以这个 a 就是在 show() 方法中。

image-20240419081051197

接下来看输出语句,我们先来看最简单的 sout(a),直接输出 a,它就会触发 就近原则,先到本方法中找,此时找到了,所以它直接打印 aa 的值就为 30

image-20240419081255431

再往下,打印 this.athis 记录的是调用者的地址值,当前这个方法是 002 调用的。

所以说,sout(this.a) 就可以把它理解为:我要打印 002 里面的 a002 就是右边内部类的对象,所以它打印的结果就是 20

image-20240419081503255

最后再来看这个最长的 sout(Outer.this.a)

此时要注意,前面有一个 Outer 前缀,此时它在找的时候,就不会找自己类(Innter类)中的20了,而是先找自己类(Innter类)中的 this(Outer),通过这个 this 来找到 Inner类 中的外部类对象,即左边的 001,这个时候再去打印 001 里面的 a,因此打印的结果就是 10

image-20240419081822573

此时我们就发现了,在整个内存中,跟以前不一样的是:内部类对象中有一个隐藏的 this,用来记录外部类对象的地址值。


4)内存分析工具

接下来我们就回到IDEA中,用内存分析工具去验证一下我们的结论,去看一下它到底有没有这个this。

首先在测试类中,还是要来先写一个键盘录入,因为使用内存分析工具的时候,需要让程序不停止。

然后直接右键 Run 运行。

image-20240419082221731

打开 Terminal,输入 jps,能获取到当前 TestID

再往下,输入内存分析工具 jhsdb hsdb,然后回车。

image-20240419082655816

File ——> Attach to HotSpot process...Attach:连接;HotSpot:虚拟机的名字)

image-20240418083731552

输入刚刚测试类的ID:4300,然后点击 OK 即可

image-20240419083159010

我们的目的是看:Java有没有给内部类添加一个隐含的 this变量

Tools ——> Class Browser

在出来的弹窗中输入 Outer,然后回车。

在下面关于 OuterOuter的内部类Inner 的字节码文件信息都已经罗列出来了。

我们点击第二个 Outer$Inner

image-20240419083518118

进来后就是 Inner内部类 的字节码文件信息。

找到 Fields,它表示是内部类中所有的成员变量。

第一个 private int a,这个是我们自己写的。

与此同时,第二个,它有一个隐藏的成员变量,这个成员变量不是我们自己写的,是虚拟机帮我们自动添加的,它是用 final 去修饰的。它的 数据类型com.itheima.a03innerclassdemo3.Outer,就是 Outer外部类。这个变量的名字叫 this,只不过这里的 this 的名字并不是叫做 this,而是叫做 this$0,因为它需要跟 Inner 中的关键字 this 区分开。

因此,用 this$0 记录的就是外部类对象的地址值。

image-20240419083718994

回到代码中:System.out.println(Outer.this.a); 中的 Outer.this 表示的就是获取的是 外部类对象的地址值,简单来说 Outer.this 表示的就是 外部类的对象

那你说,用 外部类的对象去调用a,获取到的就是外部类成员变量 a 的值:10


七、总结

1、内部类的分类?

  • 写在成员位置的 成员内部类

成员内部类 我们一般是不会使用 static静态关键字 去修饰的。如果说你用 static静态关键字 去修饰了,那就不是 成员内部类 了,而是叫做 静态内部类

  • static关键字 修饰的 静态内部类

  • 写在方法里面的 局部内部类

  • 没有名字的 匿名内部类

2、什么是成员内部类?

成员内部类 是写在成员位置(类中,方法外)的,没有用 static 修饰的。它是属于外部类的成员之一。

3、获取成员内部类对象的两种方式?

  • 方式一:在外部类中编写方法,对外提供内部类的对象

这种方式并不是所有的情况都用,一般来讲我们会用在:使用 private 修饰 内部类 的情况下。

但是如果 内部类 没有用 private 修饰,而是其他的修饰符去修饰的,那么一般我们会使用第二种方式去获取它的对象。

  • 在外部类直接创建内部类对象

当成员内部类被非私有修饰时,我们一般都是直接创建对象。

// 格式
外部类名.内部类名 对象名 = 外部类对象.内部类对象;

4、外部类成员变量和内部类成员变量重名时,在内部类如何访问?

System.out.println(Outer.this.变量名) // 这种方式可以直接获取到外部类的成员变量
  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值