小白学Java:内部类

内部类是封装的一种形式,是定义在类或接口中的类。

内部类的分类

成员内部类

即定义的内部类作为外部类的一个普通成员(非static),就像下面这样:

public class Outer {
class Inner{
private String id = "夏天"; public String getId() {
return id;
}
} public Inner returnInner(){
return new Inner();
}
public void show(){
Inner in = new Inner();
System.out.println(in.id);
}
}

我们通过以上一个简单的示例,可以得出以下几点:

  • Inner类就是内部类,它的定义在Outer类的内部。
  • Outer类中的returnInner方法返回一个Inner类型的对象
  • Outer类中的show方法通过我们熟悉的方式创建了Inner示例并访问了其私有属性。

可以看到,我们像使用正常类一样使用内部类,但实际上,内部类有许多奥妙,值得我们去学习。至于内部类的用处,我们暂且不谈,先学习它的语法也不迟。我们在另外一个类中再试着创建一下这个Inner对象吧:

class OuterTest{
public static void main(String[] args) {
//!false:Inner in = new Inner();
Outer o = new Outer();
o.show();
Outer.Inner in = o.returnInner();
//!false: can't access --System.out.println(in.id);
System.out.println(in.getId());
}
}

哦呦,有意思了,我们在另一个类OuterTest中再次测试我们之前定义的内部类,结果出现了非常明显的变化,我们陷入了沉思:

  • 我们不能够像之前一样,用Inner in = new Inner();创建内部类实例。
  • 没关系,我们可以通过Outer对象的returnInner方法,来创建一个实例,成功!
  • 需要注意的是:我们如果需要一个内部类类型的变量指向这个实例,我们需要明确指明类型为:Outer.Inner,即外部类名.内部类名
  • 好啦,得到的内部类对象,我们试着直接去访问它的私有属性!失败!
  • 那就老老实实地通过getId方法访问吧,成功!

说到这,我们大概就能猜测到:内部类的存在可以很好地隐藏一部分具有联系代码,实现了那句话:我想让你看到的东西你随便看,不想让你看的东西你想看,门都没有。

链接到外部类

其实我们之前在分析ArrayList源码的时候,曾经接触过内部类。我们在学习迭代器设计模式的时候,也曾领略过内部类带了的奥妙之处。下面我通过《Java编程思想》上:通过一个内部类实现迭代器模式的简单案例做相应的分析与学习:

首先呢,定义一个“选择器”接口:

interface Selector {
boolean end();//判断是否到达终点
void next();//移到下一个元素
Object current();//访问当前元素
}

然后,定义一个序列类Sequence:

public class Sequence {
private Object[] items;
private int next = 0;
//构造器
public Sequence(int size) {
items = new Object[size];
}
public void add(Object x) {
if (next < items.length) {
items[next++] = x;
}
}
//该内部类可以访问外部类所有成员(包括私有成员)
private class SequenceSelector implements Selector {
private int i = 0;
@Override
public boolean end() {
return i == items.length;
}
@Override
public void next() {
if (i < items.length) {
i++;
}
}
@Override
public Object current() {
return items[i];
}
}
//向上转型为接口,隐藏实现的细节
public Selector selector() {
return new SequenceSelector();
}
}
  • 内部类SequenceSelector以private修饰,实现了Selector接口,提供了方法的具体实现。
  • 内部类访问外部类的私有成员items,可以得出结论:内部类自动拥有对其外部类所有成员的访问权。

当内部类是非static时,当外部类对象创建了一个内部类对象时,内部类对象会产生一个指向外部类的对象的引用,所以非static内部类可以看到外部类的一切。

  • 外部类Sequenceselector方法返回了一个内部类实例,意思就是用接口类型接收实现类的实例,实现向上转型,既隐藏了实现细节,又利于扩展。

我们看一下具体的测试方法:

    public static void main(String[] args) {
Sequence sq = new Sequence(10);
for (int i = 0; i < 10; i++) {
sq.add(Integer.toString(i));
}
//产生我们设计的选择器
Selector sl = sq.selector(); while (!sl.end()) {
System.out.print(sl.current() + " ");
sl.next();
}
}
  • 隐藏实现细节:使用Sequence序列存储对象时,不需要关心内部迭代的具体实现,用就完事了,这正是内部类配合迭代器设计模式体现的高度隐藏。
  • 利于扩展:我们如果要设计一个反向迭代,可以在Sequence内部再定义一个内部类,并提供Selector接口的实现细节,及其利于扩展,妙啊。

.new和.this

我们稍微修改一下最初的Outer:

public class Outer {
String id = "乔巴";
class Inner{
private String id = "夏天"; public String getId() {
return id;
}
public String getOuterId(){
return Outer.this.id;
}
public Outer returnOuter(){
return Outer.this;
}
}
public static void main(String[] args) {
Outer o = new Outer();
System.out.println(o.new Inner().getId());//夏天
System.out.println(o.new Inner().getOuterId());//乔巴
}
}
  • 在内部类Inner体内添加了returnOuter的引用,return Outer.this;,即外部类名.this
  • 我们可以发现,内部类内外具有同名的属性,我们在内部类中,不加任何修饰的情况下默认调用内部类里的属性,我们可以通过引用的形式访问外部类的id属性,即Outer.this.id

我们来测试一波:

    public static void main(String[] args) {
Outer.Inner oi = new Outer().new Inner();
System.out.println(oi.getId());//夏天
Outer o = oi.returnOuter();
System.out.println(o.id);//乔巴
}
  • 外部类产生内部类对象的方法已经被我们删除了,这时我们如果想要通过外部类对象创建一个内部类对象:Outer.Inner oi = new Outer().new Inner();,即在外部类对象后面用.new 内部类构造器

我们对内部类指向外部类对象的引用进行更加深入的理解与体会,我们会发现,上面的代码在编译之后,会产生两个字节码文件:Outer$Inner.classOuter.class。我们对Outer$Inner.class进行反编译:



确实,内部类在创建的过程中,依靠外部类对象,而且会产生一个指向外部类对象的引用

局部内部类

方法作用域内部类

即在方法作用域内创建一个完整的类。

public class Outer {
public TestOuter test(final String s){
class Inner implements TestOuter{
@Override
public void testM() {
//!false: s+="g";
System.out.println(s);
}
}
return new Inner();
}
public static void main(String[] args) {
Outer o = new Outer();
o.test("天乔巴夏").testM();//天乔巴夏
}
}
interface TestOuter{
void testM();
}

需要注意两点:

  • 此时Inner类是test方法的一部分,Outer不能在该方法之外访问Inner。
  • 方法传入的参数s和方法内本身的局部变量都需要以final修饰,不能被改变!!!

JDK1.8之后可以不用final显式修饰传入参数和局部变量,但其本身还是相当于final修饰的,不可改变。我们去掉final,进行反编译:

任意作用域内的内部类

可以将内部类定义在任意的作用域内:

public class Outer {
public void test(final String s,final int value){
final int a = value;
if(value>2){
class Inner{
public void testM() {
//!false: s+="g";
//!false: a+=1;
System.out.println(s+", "+a);
}
}
Inner in = new Inner();
in.testM();
}
//!false:Inner i = new Inner();
}
public static void main(String[] args) {
Outer o = new Outer();
o.test("天乔巴夏",3);
}
}

同样需要注意的是:

  • 内部类定义在if条件代码块中,并不意味着创建该内部类有相应的条件。内部类一开始就会被创建,if条件只决定能不能用里头的东西。
  • 如上所示,if作用域之外,编译器就不认识内部类了,因为它藏起来了。

静态内部类

即用static修饰的成员内部类,归属于类,即它不存在指向外部类的引用

public class Outer {
static int a = 5;
int b = 6;
static class Inner{
static int value;
public void show(){
//!false System.out.println(b);
System.out.println(a);
}
}
}
class OuterTest {
public static void main(String[] args) {
Outer.Inner oi = new Outer.Inner();
oi.show();
}
}

需要注意的是:

  • 静态内部类也可以定义非静态的成员属性和方法。
  • 静态内部类对象的创建不依靠外部类的对象,可以直接通过:new Outer.Inner()创建内部类对象。
  • 静态内部类中可以包含静态属性和方法,而除了静态内部类之外,即我们上面所说的所有的内部类内部都不能有(但是可以有静态常量static final修饰)。
  • 静态内部类不能访问非静态的外部类成员。
  • 最后,我们反编译验证一下:

匿名内部类

这个类型的内部类,看着名字就怪怪的,我们先看看一段违反我们认知的代码:

public class Outer {
public InterfaceInner inner(){
//创建一个实现InterfaceInner接口的是实现类对象
return new InterfaceInner() {
@Override
public void show() {
System.out.println("Outer.show");
}
};
}
public static void main(String[] args) {
Outer o = new Outer();
o.inner().show();
}
}
interface InterfaceInner{
void show();
}

真的非常奇怪,乍一看,InterfaceInner是个接口,而Outer类的inner方法怎么出现了new InterfaceInner()的字眼呢?接口不是不能创建实例对象的么?

确实,这就是匿名内部类的一个使用,其实inner方法返回的是实现了接口方法的实现类对象,我们可以看到分号结尾,代表一个完整的表达式,只不过表达式包含着接口实现,有点长罢了。所以上面匿名内部类的语法其实就是下面这种形式的简化形式:

public class Outer {
class Inner implements InterfaceInner{
@Override
public void show(){
System.out.println("Outer.show");
}
}
public InterfaceInner inner(){
return new Inner();
}
public static void main(String[] args) {
Outer o = new Outer();
o.inner().show();
}
}
interface InterfaceInner{
void show();
}

不仅仅是接口,普通的类也可以被当作“接口”来使用:

public class Outer {
public OuterTest outerTest(int value) {
//参数传给匿名类的基类构造器
return new OuterTest(value) { @Override
public int getValue() {
return super.getValue() * 10;
}
};
}
public static void main(String[] args) {
Outer o = new Outer();
System.out.println(o.outerTest(10).getValue());//100
}
}
class OuterTest {
public int value;
OuterTest(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}

需要注意的是:

  • 匿名类既可以扩展类,也可以实现接口,当然抽象类就不再赘述了,普通类都可以,抽象类就更可以了。但不能同时做这两件事,且每次最多实现一个接口。
  • 匿名内部类没有名字,所以自身没有构造器。
  • 针对类而言,上述匿名内部类的语法就表明:创建一个继承OuterTest类的子类实例。所以可以在匿名内部类定义中调用父类方法与父类构造器。
  • 传入的参数传递给构造器,没有在类中直接使用,可以不用在参数前加final。

内部类的继承

内部类可以被继承,但是和我们普通的类继承有些出处。具体来看一下:

public class Outer {
class Inner{
private int value = 100;
Inner(){
}
Inner(int value){
this.value = value;
}
public void f(){
System.out.println("Inner.f "+value);
}
}
}
class TestOuter extends Outer.Inner{
TestOuter(Outer o){
o.super();
}
TestOuter(Outer o,int value){
o.super(value);
} public static void main(String[] args) {
Outer o = new Outer();
TestOuter tt = new TestOuter(o);
TestOuter t = new TestOuter(o,10);
tt.f();
t.f();
}
}

我们可以发现的是:

  • 一个类继承内部类的形式:class A extends Outer.Inner{}
  • 内部类的构造器必须链接到指向外部类对象的引用上,o.super();,即都需要传入外部类对象作为参数。

内部类有啥用

可以看到的一点就是,内部类内部的实现细节可以被很好地进行封装。而且Java中存在接口的多实现,虽然一定程度上弥补了Java“不支持多继承”的特点,但内部类的存在使其更加优秀,可以看看下面这个例子:

//假设A、B是两个接口
class First implements A{
B makeB(){
return new B() {
};
}
}

这是一个通过匿名内部类实现接口功能的简单的例子。对于接口而言,我们完全可以通过下面这样进行,因为Java中一个类可以实现多个接口:

class First implements A,B{
}

但是除了接口之外,像普通的类,像抽象类,都可以定义独立的内部类去单独继承并实现,使用内部类使“多重继承”更加完善


由于后面的许多内容还没有涉及到,学习到,所以总结的比较浅显,并没有做特别深入,特别真实的场景模拟,之后有时间会再做系统性的总结。如果有叙述错误的地方,还望评论区批评指针,共同进步。

参考:

《Java 编程思想》

https://stackoverflow.com/questions/70324/java-inner-class-and-static-nested-class?r=SearchResults

小白学Java:内部类的更多相关文章

  1. 小白学Java:包装类

    目录 小白学Java:包装类 包装类的继承关系 创建包装类实例 自动装箱与拆箱 自动装箱 自动拆箱 包装类型的比较 "=="比较 equals比较 自动装箱与拆箱引发的弊端 自动装 ...

  2. 小白学Java:迭代器原来是这么回事

    目录 小白学Java:迭代器原来是这么回事 迭代器概述 迭代器设计模式 Iterator定义的方法 迭代器:统一方式 Iterator的总结 小白学Java:迭代器原来是这么回事 前文传送门:Enum ...

  3. 小白学Java:老师!泛型我懂了!

    目录 小白学Java:老师!泛型我懂了! 泛型概述 定义泛型 泛型类的定义 泛型方法的定义 类型变量的限定 原生类型与向后兼容 通配泛型 非受限通配 受限通配 下限通配 泛型的擦除和限制 类型擦除 类 ...

  4. 小白学Java:奇怪的RandomAccess

    目录 小白学Java:奇怪的RandomAccess RandomAccess是个啥 forLoop与Iterator的区别 判断是否为RandomAccess 小白学Java:奇怪的RandomAc ...

  5. 小白学Java:File类

    目录 小白学Java:File类 不同风格的分隔符 绝对与相对路径 File类常用方法 常用构造器 创建方法 判断方法 获取方法 命名方法 删除方法 小白学Java:File类 我们可以知道,存储在程 ...

  6. 小白学Java:I/O流

    目录 小白学Java:I/O流 基本分类 发展史 文件字符流 输出的基本结构 流中的异常处理 异常处理新方式 读取的基本结构 运用输入与输出 文件字节流 缓冲流 字符缓冲流 装饰设计模式 转换流(适配 ...

  7. 小白学Java:RandomAccessFile

    目录 小白学Java:RandomAccessFile 概述 继承与实现 构造器 模式设置 文件指针 操作数据 读取数据 read(byte b[])与read() 追加数据 插入数据 小白学Java ...

  8. 【aliyun】学java,看这里,不迷茫!1460道Java热门问题

    阿里极客公益活动: 或许你挑灯夜战只为一道难题 或许你百思不解只求一个答案 或许你绞尽脑汁只因一种未知 那么他们来了,阿里系技术专家来云栖问答为你解答技术难题了 他们用户自己手中的技术来帮助用户成长 ...

  9. 学Java必看!零基础小白再也不用退缩了

    程序员们!请往这儿看 对于JAVA的学习,可能你还会有许多的顾虑 不要担心 接着往下看吧 学Java前 一.数学差,英语也不好是不是学不好Java? 答案是:是~ 因为你在问这个问题的时候说明你对自己 ...

随机推荐

  1. dotnet 使用 MessagePack 序列化对象

    和很多序列化库一样,可以通过 MessagePack 序列化和反序列化,和 json 相比这个库提供了二进制的序列化,序列化之后的内容长度比 json 小很多 这个库能序列的内容不多,大多数时候建议使 ...

  2. Unable to preventDefault inside passive event listener due to target being treated as passive 怎么办?

    本篇为转载,原文链接:https://blog.csdn.net/lijingshan34/article/details/88350456 翻译一下:chrome 监听touch类事件报错:无法被动 ...

  3. docker(整理中

    docker镜像默认的下载地址就是docker的官网,而他们的官网在国内没有节点,时不时就被国家防火墙隔绝,会出现DNS解析不到,或者找不到镜像等狗血提示. 解决的方法有三个: 第一,就是不断尝试,因 ...

  4. ELK学习实验004:Elasticsearch的简单介绍和操作

    一 集群节点 Elstaicsearch的集群是由多个节点组成都,通过cluster.name设置集权名称,比能切用与区分其他的集群,每个节点通过node.name指定节点 在Elasticsearc ...

  5. 洛谷$P3302$ 森林 $[SDOI2013]$ 主席树

    正解:主席树 解题报告: 传送门! 口胡一时爽代码火葬场 这题想法不难,,,但显然的是代码应该还挺难打的 但反正我也不放代码,就写下题解趴$QwQ$ 第一问就是个$Count\ on\ a\ tree ...

  6. 详解js的bind、call、apply

    详解js的bind.call.apply 说明 虽然bind.call.apply都是js很基础的一块知识,但是我从未认真总结过这三者的区别. 由于公司后端是用的微服务架构,又没有中间层对接,导致前端 ...

  7. FPGA基础入门程序代码

    module flow_led( input sys_clk , //系统时钟,外部时钟50M input sys_rst_n, //系统复位,低电平有效 :] led //4个LED灯 ); //r ...

  8. webpack4的配置你都掌握了么?

    webpack5都出了,webpack4的的基本配置,解析ES6,引入CSS,编译Less,设置image等等,你都会了么? ​解析ES6 了解Babel Babel是一个JavaScript编译器, ...

  9. 在EasyUI项目中使用FileBox控件实现文件上传处理

    我在较早之前的随笔<基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用>Web框架介绍中介绍了基于Uploadify的文件上传操作,免费版本用的是J ...

  10. Q&A系列一:DataPipeline常见问题回答

    不知不觉中,大家已经陪伴DataPipeline走过了3年时间.在这期间,得益于客户们的积极反馈和沟通,我们总结了一些日常工作中比较常见的问题,并基于这些问题进行了总结. 为避免突兀,我们会先从比较基 ...