您的位置 首页 java

“全栈2019”Java第九十三章:内部类应用场景(迭代器设计模式)

难度

初级

学习时间

30分钟

适合人群

零基础

开发语言

Java

开发环境

  • JDK v11
  • IntelliJ IDEA v2018.3

提示

本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。

1.内部类

前面学习过内部类,这里我们来温故一下。

演示:

请定义一个内部类。

代码:

没有学习过的同学或者是不太清楚内部类的同学也别急,可以点击下面内部类学习资料来进行学习。

附:内部类学习资料

2.一个非常简单的应用场景

让我们通过一个非常简单的应用场景来了解为什么要使用内部类。

在我们日常编写代码的时候,都会写一个Main类,在Main类里面写一个main()方法来测试我们所写的功能。

例如:我们写了一个文字工具类,在这个类里面存放的都是关于如何处理文字的功能。

演示:

请定义一个文字工具类TextUtils,在TextUtils类中定义一个文字转大写的功能。

请在Main类的main()方法中测试TextUtils中的转大写功能。

请观察程序运行结果。

代码:

TextUtils类:

Main类:

结果:

从运行结果来看,符合预期,程序也没有任何错误。

过了一段时间,我又来了一个 数学工具 类,里面存放的都是跟数学有关的功能。

演示:

请定义一个数学工具类MathUtils,在其中定义一个加法功能。

请在Main类的main()方法中测试MathUtils中的加法功能。

请观察程序运行结果。

代码:

MathUtils类:

Main类:

结果:

从运行结果来看,程序没有问题。

上面我们一共写了三个类,一个TextUtils文字工具类,一个MathUtils数学工具类,还有一个测试类Main。

大家有没有发现,如果我想测试TextUtils类的转大写功能,我得来改Main类中的main()方法;如果我想测试MathUtils类的加法功能,我得来改Main类中的main()方法。假如还有其他很多工具类,我都想测一下它们里面功能, 这意味着,我每测一次都得改一次Main类中的main()方法,麻烦的很。而且还不能保留我上次测试用例,很糟糕的体验,意味着每次你想测试功能的时候都得写一遍测试用例。

有没有改进的余地?

有。大家想想,既然每个类里面都可以拥有main()方法( 没有规定只有Main类里面才能拥有main()方法 ),我们为什么不让每个工具类里面都拥有一个main()方法呢?测试直接在本来中进行。

好,接下来我们来试试在工具类中定义main()方法。

演示:

请将上述程序中Main类的main()方法移至TextUtils类中。

请在TextUtils类中执行main()方法。

请观察程序运行结果。

代码:

TextUtils类:

结果:

结果依然是正确的。说明将main()方法从Main类中移至TextUtils类是可以的。

好,请问main()方法就这样放在TextUtils类中妥不妥?

该做法实为不妥。大家想一想, 我们TextUtils类是用来存放处理文字相关功能的,不包括测试方法。 那就难了,也就是说,我们还得把main()方法重新写在Main类里面,现在我不想把main()方法写在Main类里面,只想把测试方法放在我功能方法放在一起,方便随时都可以测试。

针对上述问题,有没有解决的办法?

有。 我们可以采取在工具类里面定义内部类的做法,该内部类是一个测试类,只用于测试。 定义测试内部类的好处有很多,比如,不干扰正常类的使用;能够直接访问外部类中的成员(包括私有),这样就避免了很多麻烦。

接下来,我们就来改进TextUtils类。

演示:

请使用内部类的方式解决上述代码问题。

请观察程序运行结果。

代码:

TextUtils类:

结果:

从运行结果来看,程序没有问题,结果也符合预期。

我们简单的分析一下程序。首先来看测试内部类:

我们发现这个内部类它是一个静态内部类,为什么它会是一个静态内部类呢?

因为它里面的main()方法是静态,我们非静态的内部类是无法拥有静态方法的。

接着来看main()方法:

它就直接调用了外部类中的测试方法。甚至我们把这个TextUtils类名去掉也可以,直接写TextUtils类中的方法即可:

程序经过我们一番改进,现在可以说随时随地测试。测试类跟随着功能类, 当然了,这里只是说用main()方法来测试程序功能时的做法,等后面我们需要单元测试等等一些高级测试后,那又是不一样的体验。

通过此例,我们能够感受到内部类带来的便利,这里小结一下:

内部类是一种逻辑分组仅在一个地方使用的类的方法:如果一个类只对另一个类有用,那么将它嵌入该类并将两者保持在一起是合乎逻辑的。 嵌套这样的“帮助类”使得它们的包更加简化。

测试类就是一个帮助类,它帮助了工具类更好的测试内部的功能。

3. 迭代器 设计模式

我们再来一个稍微复杂一点的例子,该例子很常见,也很实用。

友情提示:该案例从无到有一步一步讲解迭代器设计模式的形成,(本小节很长)希望大家能够有足够的耐性阅读,如果想直接看最终结果请拖至本节末尾。

演示:

请设计一个类,该类是一个专门存储数字的集合类。

请再设计一个类,该类是一个专门存储文字的集合类。

请观察程序代码。

代码:

NumberList类:

TextList类:

从程序代码来看呢,NumberList和TextList类里面各自都有一个数组,数组类型一个是int,存储的是数字;还有一个是String,存储的是文字。它们的初始大小都是10。

存储的容器定义完了,接下来该定义能够添加元素的方法了。

演示:

请将上例中的NumberList和TextList两个类各自新增一个添加元素的方法。

请观察程序代码。

代码:

NumberList类:

TextList类:

我们来解释一下这段代码是什么意思。拿NumberList类来说:

该实例变量position是用来记录当前数字数组已经添加到什么位置了。打个比方来说,初始position的值为0,这意味着第一次调用add()方法时:

条件满足,执行添加元素语句:

注意,此时position值为0,所以是给数组中的第一个位置上添加元素。

接着往下执行,累加position值:

这是因为数组中第一个位置已经添加过元素了,再添加的话就是第二个位置,当第二个位置添加元素时,再往下就是第三个元素,以此类推,直到把数组添加满为止。所以,position要不断累加,以让数组每一个位置都被添加上值。

下面,我们就来演示添加元素这个功能,在演示之前,我们还有一个TextList类没有说,其实这个类和NumberList类一摸一样,只是数组的类型不同,一个int,一个String,所以请大家谅解,这里不再赘述。如果还有不懂的同学,请在评论区留言,我会再详细讲解给你。

好了,我们来测试添加元素功能。

演示:

请保持上例已有代码不变。

请在Main类的main()方法中创建出NumberList和TextList两个集合类。

请使用两个集合类的添加元素方法。

请观察程序运行结果。

代码:

NumberList类:

TextList类:

Main类:

结果:

从运行结果来看,程序没有任何问题。可能有的同学有些疑惑,程序咋啥都没有输出呢?因为我们在程序中没有写任何的输出语句。该程序只要它不报错,就没有问题,为什么这么说呢?因为我在给NumberList添加元素的时候,是循环添加的,而循环次数是20次:

也就是说远远超过了NumberList类里面数组长度,所以,只要程序它不报错,就是正确的。

好,容器也定义了,元素也添加了,我想看看容器里到底存了什么元素,怎么看呢?数组嘛,当然是遍历啦。现在问题是数组定义在类里面,所以要么把数组暴露出来,在Main类里面遍历,要么在自己类里面遍历。选哪种?

第一种,暴露数组,自定义对元素的操作。

演示:

请在上例中NumberList类里添加一个获取数组的方法。

请在Main类main()方法中遍历NumberList中的数组。

请观察程序运行结果。

代码:

NumberList类:

Main类:

结果:

从运行结果来看,程序没有问题。

简单的来分析一个程序,我们给NumberList类中新增了一个可以获取内部数组的方法:

然后我们来看Main类中的main()方法:

获取数字数组,然后对其进行遍历,循环体中的内容由我们任意定义。执行结果:

这是第一种方式,下面我们再来看第二种方式。

第二种,不暴露数组,类中自行遍历,不能自定义对元素对操作。

演示:

请将NumberList获取数组方法替换为遍历数组方法。

请在Main类main()方法中调用遍历数组方法。

请观察程序运行结果。

代码:

NumberList类:

Main类:

结果:

从运行结果来看,程序没有问题。

我们简单的分析一下此程序,首先是NumberList的遍历数组方法:

一旦遍历方法写死,那就真的无法改变了,也就是说你只有遍历数字数组,那就是打印里面的数字,无法对数组中的元素进行操作,例如我想给遍历每一个元素时,打印该元素加100的结果,第一种方式很好就实现了:

而第二种方式又得去改NumberList的iteration()方法,很麻烦,而且我们对元素的操作千奇百怪,你是不可能在一个方法里面写完的,所以第二种方式不推荐大家使用。

好了,经过两个遍历定义在类里的数组的方式,发现第一种方式还是要好很多。

对了,我们除了NumberList需要遍历以外,TextList同样需要遍历,那么这两个类都需要将各自的数组提供出去。

演示:

请将NumberList类和TextList类都提供获取数组的方法。

请在Main类main()方法中遍历它们的元素。

请观察程序运行结果。

代码:

NumberList类:

TextList类:

Main类:

结果:

从运行结果来看,程序没有问题。至于大家看见结果中有7个null,那是String类型的默认值。因为我们只添加了三个文字元素,所以后面七个都是null。

刚刚说的不是重点,重点是我们遍历的操作有点重复:

该写一个公共方法,然后遍历的时候只需要调这个公共方法即可,而且方法的参数还得是Object类型的,因为设置为int类型,String不兼容;设置为String类型,int不兼容。该方法定义在Main类中:

看似很美好,我们来运行程序:

错误信息:

文字版:

完了,这下无法抽取公共遍历方法。

那有没有解决的办法呢?

有。大家想想,NumberList和TextList里面都有数组,而且都需要遍历数组,我们为什么不抽取它们公共部分呢?

有人估计想问公共部分是什么呢?

是遍历数组啊。让NumberList和TextList两个容器类之间产生一点联系,前面我们学过连接类与类用接口。问题是,接口里面定义一些什么东西呢?

首先,我们是一个一个取出来的,所以要有一个记录当前取到第几个的位置变量,还得有一个获取当前元素的方法,最后还得来一个询问还能继续往下取的方法。

根据描述,大概这样一个接口就定义好了。有人要问那个记录当前获取元素的位置变量怎么没有?因为接口中只能定义常量,所以不能定义在接口中,那么自然而然就得定义在实现类中。

好了,那就实现吧。

演示:

请定义一个接口Iterator,里面定义两个方法,一个是用于判断是否还有下一个元素,还有一个是获取下一个元素。

请NumberList类和TextList类实现Iterator接口,并在各自类中定义一个用于记录当前取到元素的位置。

请在Main类main()方法中使用Iterator方式遍历元素。

请观察程序运行结果。

代码:

Iterator接口:

NumberList类:

TextList类:

Main类:

结果:

从运行结果来看,程序没有问题。看似也很完美,简单的来分析一下程序,首先来看Iterator接口:

定义这两个方法,是因为它们是必须的。hasNext()方法是为了防止数组下标越界,next()方法是为了获取当前遍历的元素。

再来看看实现类,NumberList和TextList两个类,我们只看其中一个即可,因为它们只是类型不同,其他都一样:

除了实现接口中的两个抽象方法以外,我们还定义了一个用于记录当前遍历元素的位置变量,该变量很重要,它能让我们知道遍历到什么位置了,这样我们可以根据它来获取相对应的元素。

最后来看看Main类:

遍历方式改变了,使用while循环是因为类中判断条件很符合这种方式。我们通过next()方法获取当前元素。大家也看到了,这两处还能再抽取一个公共方法来:

运行程序,执行结果:

从运行结果来看,和之前的一样。

一切都看起来很美好,但是这里有一个小小的BUG,是什么呢?那就是NumberList和TextList迭代多次只有第一次迭代有效,后面迭代无效。来试试。

结果:

0-9数字只打印了一遍,按理说,应该打印两遍。

问题出在哪里呢?

问题就出在next变量在第一遍迭代完之后,没有被重置为0,它的值一直为9,导致第二遍开始遍历的时候,hasNext()方法一直返回false:

那有同学就说,我们在第二次遍历之前重置一下next不就好了吗,等等,这种操作什么时候做?在遍历前吗?你会永远都记得做这个操作吗?有可能一不留神就没有做这个操作,导致后面无法估量的损失,试想一下,你正在操作的是银行里的金额,稍不注意就会造成损失。所以这种在遍历前重置的操作不可取。

那怎么办?没有解决办法了吗?

不,还是有的,刚刚我们想到重置next变量,如果我们每一次遍历都是去获取一个迭代器对象,将next遍历封装到迭代器里面,那么每一次获取迭代器对象next都是初始值。

问题还没解决完,这个迭代器对象肯定是一个类,而且需要需要去实现Iterator接口。

那么这个类该定义在哪?外部类吗?不行,因为Iterator接口中的两个方法都用到了集合类中的数组,意味着迭代器类要能直接操作数组成员。而能直接操作类中成员的类只有内部类可以,所以迭代器是一个内部类。

演示:

请使用内部类迭代器Itr改写NumberList和TextList类。

请在Main类main()方法中使用新方式遍历元素。

请观察程序运行结果。

代码:

Iterator接口:

NumberList类:

TextList类:

Main类:

结果:

从运行结果来看,程序没有任何问题。结果也符合预期。之前说的两次迭代集合的问题已经得到解决。我们来分析一个该程序。

首先,我们得来看看新改写的NumberList类,TextList和NumberList是一摸一样的,就不再重复看,看其中一个即可:

内部类Itr里面有一个next变量,用于记录当前遍历元素的位置,每一次创建新的迭代器对象时,next变量都是新的,又从初始值开始。那我们就去看看创建迭代器对象的地方:

有三处,都是通过外部类对象来创建内部类对象的方式。第一次迭代结果:

第二次迭代结果:

第三次迭代结果:

三次迭代结果都是正确的。

大家有没有觉得我们创建迭代器的方式有点不妥,如果我们在外部类里面直接定义一个获取内部类对象的方法,岂不是更好?我们来优化NumberList类和TextList类:

NumberList类:

TextList类:

Main类:

结果:

从运行结果来看,程序没有任何问题。而且我们将内部类设为私有成员,提供了获取迭代器对象的方法。

到这里,迭代器设计模式案例讲解完毕!后面还会有设计模式专题,里面也会讲解到迭代器设计模式,敬请关注!

总结

  • 内部类是一种逻辑分组仅在一个地方使用的类的方法:如果一个类只对另一个类有用,那么将它嵌入该类并将两者保持在一起是合乎逻辑的。 嵌套这样的“帮助类”使得它们的包更加简化。
  • 内部类增强了封装:考虑两个顶级类A和B,其中B需要访问A的成员,否则这些成员将被声明为私有。 通过将类B隐藏在类A中,可以将A的成员声明为私有,并且B可以访问它们。 另外,B本身可以隐藏在外面。
  • 内部类可以带来更易读和可维护的代码:在顶级类中嵌套小类会使代码更接近于使用它的位置。

至此,Java中内部类应用场景相关内容讲解先告一段落,更多内容请持续关注。

答疑

如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。

上一章

下一章

“全栈2019”Java第九十四章:局部内部类详解

学习小组

加入同步学习小组,共同交流与进步。

  • 方式一:关注头条号Gorhaf,私信“Java学习小组”。
  • 方式二:关注公众号Gorhaf,回复“Java学习小组”。

全栈工程师学习计划

关注我们,加入“全栈工程师学习计划”。

版权声明

原创不易,未经允许不得转载!

文章来源:智云一二三科技

文章标题:“全栈2019”Java第九十三章:内部类应用场景(迭代器设计模式)

文章地址:https://www.zhihuclub.com/186789.shtml

关于作者: 智云科技

热门文章

网站地图