11.1 ADTs
所以今天我们要开始谈论现实世界了,好的,我们要从我们自己创建的类中走出来,开始使用真正的工具,为此我们将开始讨论Java库。你们中很多人如果高中学过Java,可能已经见过大部分内容,但现在我们将开始为那些还没有接触过的同学讲解。
到目前为止,课程的一半内容都是关于构建数据结构的,我们基本上给出了一些本学期剩余十周内对我们很重要的概念示例。特别是,我们在课堂上构建了一个List61B及其接口,并开发了AList和SList类,虽然它们还不错,但并不是所有操作都很高效。例如,SList在执行某些操作时效率不高,对吧?因此,我们给了你一个机会去构建更好的东西,比如双端队列,它比List61B有更多的操作。
在你的案例中,你能够高效地支持使用数组双端队列和链表双端队列接口(抱歉,是实现)来添加和删除元素。在这两种情况下,我们最终都有一个抽象数据类型,就像洞穴中人们相信的东西一样,然后我们有僧侣的秘密行动,我做了一个版本,你也做了一个版本。当然,这里有一个现实世界,我们稍后会看到,但在我们继续之前,我要说,除了这一点,这一直是课程的主要故事线,还有一些旁支,那就是实现继承的另一种很好的用途,用于比较。我们看到了接口继承的情况,这是去年春季2016年Josh课上的内容,无论如何,这是接口继承的例子。
我们还看到了接口的另外两个用途,除了捕捉这个美好的故事之外,它还给我们提供了一个契约,我们可以这样说:如果你是一个可比较的对象,你肯定有一个比较方法。在这种情况下,我们使用接口继承来说明你具备哪些能力,比如,狗有能力进行比较,同时这也给了我们一种绕过Java中不能传递函数的方法,即不能将函数作为参数传递给其他函数。因此,我们能够使用实现或接口继承来创建像字符比较器这样的对象,这是你在项目1C中做的事情。实际上,你构建了一个类,可以像函数生成器一样工作,可以选择任意的m值。所以,总的来说,我们以两种方式使用了接口:首先,我们捕捉到了一个非常美好的抽象宇宙的概念,即生活在洞穴中的人们和真实宇宙中僧侣们在火焰前摆弄东西之间的界限;其次,我们也用它来表达对象的能力。这就是我们到目前为止所做的事情。
现在,我们开始看到现实世界是如何工作的。所以,我们不再使用为了这个课程目的而编造的双端队列或你自己发明的东西,而是将转向Java会做什么。这个抽象数据类型和实际实现之间的区别是我们将反复强调的一个概念。所以,我要开始介绍其他抽象数据类型,我们在课堂上已经看到了双端队列和包装器,所以下半节课我们将看到其他你可能已经见过的数据类型。这里给你介绍一个你可能听说过也可能没听说过的新的抽象数据类型,称为栈。
那么什么是栈呢?栈是一个抽象数据类型,有两个操作:push和pop。在这种情况下,栈中只包含整数。例如,这个栈目前有数字4,如果我开始push东西,它们会堆在栈顶。所以,如果我push数字6,它会出现,如果我现在再push数字2,它也会出现。现在我在栈上唯一能做的另一件事就是pop。pop是什么意思呢?当栈的状态是2时,2会被移除。所以,栈有点像双端队列,但它是单端的。我们只能在栈顶添加和移除元素。这就是栈的抽象数据类型。
实现这个抽象数据类型的方法有很多种,但我想让你考虑一下你已知的方法。如果我们尝试实现这个新的抽象数据类型,这两个操作中哪一个你认为会更快?来试试看,思考一下这个问题。
(学生讨论)
我们可以进行一次投票,4比1支持链表。有没有人认为数组是更好的答案?有人愿意说说为什么吗?好的,不太受欢迎的答案,我提前警告你,为什么你认为数组可能会更好?是的,为什么它更好?是的,当我们从数组末尾pop一个元素时,我们只需要将size减1。如果是链表,你需要做什么?你可能需要遍历整个列表,对吗?为什么在链表中pop时不需遍历整个列表?是的,你是链表的支持者。你们现在在争论,为什么不需遍历整个链表?另外,你提出指针始终在栈顶,对吗?如果你有一个指向栈顶的指针,你就无需遍历整个列表。所以,我认为,如果你在链表中保持一个指向4的指针,那么它确实会很慢,但如果你保持一个指向栈顶的指针,那就没问题了。你可以说,每次从链表移动时可能会稍微多一点工作,但真的会有很大差别吗?下一个问题是,为什么数组可能会更慢?有时候你需要重新调整大小,但这在栈中实际上并没有多大区别,无论你使用哪种方法,只要你确保在链表中跟踪最后一个或栈顶元素,它们的表现差不多。但是,链表有一个轻微的优势,因为它永远不会进行重新调整大小的操作。再来一个例子。
还有一个抽象数据类型,你可能从未听说过,因为这是我编造的,叫做抓袋。在这种情况下,它支持四个操作:insert,将X插入抓袋;这个袋子就是一个袋子,没有任何特定的顺序。当你插入东西时,就像把它扔进袋子里。当我执行remove时,我取出一些东西,然后我有一个已经在里面的东西,我得到了一个被移除的东西,你得到一个随机的东西。sample的意思是基本上查看袋子里的东西,但不取出来。size告诉你袋子里有多少东西。所以,这是一个抽象数据类型,你可以在不知道链表或数组的情况下使用它,但假设你尝试实现它,你觉得哪一种整体上会是更好的实现?只是凭直觉,我的手机在袋子里。
(学生讨论)
好的,房间里的意见几乎是统一的,这次我直接问数组派的人,为什么你认为它会更快?对于sample操作,使用数组时,你可以在0到元素数量之间随机选择一个数字,直接访问你想访问的元素。而使用链表时,显然会更复杂,因为进行采样需要扫描整个列表,即使你保持一个随机指针,仍然需要不断生成随机指针,这必然涉及扫描列表。所以,数组在这种情况下可能会更好,实际上会是一个更好的解决方案。你也可以在链表中做一些巧妙的事情,比如同时维护一个包含所有元素的数组,但那样会增加复杂性。
11.2 List and Set Demo
但那样你就有点作弊了。在Java中,有许多内置的抽象数据类型,比如列表(List)、集合(Set)和映射(Map),这些是最常用的三种抽象数据类型,位于java.util库中。在这个意义上,我们使用的列表类似于我们在课堂上构建的列表,集合类似于你在其他语言中见过的集合,而映射类似于Python字典。例如,Python字典允许你做类似这样的事情:J hugs grade is 88.4,它让你可以将值与键关联起来,正如我所说,映射也被称为关联数组、关联列表、符号表或字典等。
我们将在稍后尝试使用这些抽象数据类型,但在我们开始之前,我想提到的是,在继承层次结构中,Java的接口为你提供了这些功能。Collection是一个非常模糊的抽象数据类型,它只能做很少的事情,而List、Set和Map则更加具体。列表是一种集合,集合有一个contains方法,你可以用它来检查某个元素是否在集合中,但列表有获取特定项的能力,而集合不一定有序,集合没有get me item 5这样的方法,但列表有。
在列表抽象数据类型中,最流行的两种实现是链表(LinkedList)和数组列表(ArrayList)。对于集合,有哈希集合(HashSet)和树集合(TreeSet)。对于映射,有哈希映射(HashMap)和树映射(TreeMap)。还有更多的抽象数据类型和实现,但这些都是最常用的六种实现和三种最常用的抽象数据类型。让我们试着使用它们。
假设我们有一本书或故事的文本,我们想做几件事情:第一,也许我们想创建一个包含书中所有单词的列表;第二,也许我们想统计所有单词的数量,特别是独特的单词数量;第三,记录某些我们关心的单词出现的次数。让我们看看在Java中如何实现这些任务,这些是你在其他语言中可能已经做过的事情,特别是在我们学习了61A之后,这些是你在课程早期就会做的事情,我们会发现这样做有点麻烦。
这段代码将演示所有基本的集合,我已经在这里导入了它们,稍后我会解释不需要强制导入的原因,但为了代码能够运行,我们现在先导入它们。当我这样做时,我从一个免费方法开始,该方法接受一个字符串并移除所有非字母字符,这对我们提取单词很有帮助。我给自己准备了一个文件库,名为《巴别图书馆》(The Library of Babel),但文件名似乎有些混淆了,没关系,它实际上是一个叫《巴比伦的彩票》(The Lottery in Babylon)的故事,但我和另一个故事混在一起了。我想要从这个文件中提取单词,但在此之前我没有检查它是否能正确加载,所以我们来看看会发生什么。
我将使用这个getWords方法,我们的最终目标是返回一个字符串列表。通常,Java程序可能看起来像这样:List<String> words = new ArrayList<String>();。这表示我想创建一个抽象列表,我不在乎是什么类型的列表,但我必须选择某种类型的列表,所以我选择了ArrayList。如果我试图说new List,它会告诉我不能这样做,因为这不是一个具体的东西,我在编写程序时必须选择一个具体的实现。
接下来,我将做一点类似于我们在项目零中做的事情。首先,我将创建一个输入流,读取文件中的内容。这只是一个我们在项目零中做过的事情,我不会再次详细解释,但我会说:Scanner in = new Scanner(inputFileName);。在这种情况下,我需要导入Scanner,这是很酷的。
为什么你不导入这些库?也许这是一个问题,稍等一下。你们都做过几次项目结构设置,对吧?库,让我们添加一些库。好的,大家可能有点打哈欠,我知道这不是很有趣,但没关系。
现在我们已经创建了一个输入流,只要输入流不为空,我们将获取下一个字符串,然后对其进行清理,使其更规范。如果它包含破折号或大写字母,这将修复它。我将说nextWord = cleanString,然后我们打印出nextWord,我编程时有时会这样做,只是为了确保一切按预期工作。当然,我们还没有完成,但这至少能让程序运行起来,并输出所有的单词。理想情况下,它应该输出所有单词,比如“所有的巴比伦男人……”等等。如果我的环境设置不正确,我们来看看是否会出错。哦,它工作了,很好,这里有我们故事中的所有单词,比如“所有的巴比伦男人……”等等。这是一篇很棒的故事,谁读过博尔赫斯的短篇小说?哦,有人知道,博尔赫斯有很多粉丝,你们错过了一位伟大的作家,他的故事只有几页长,但非常密集和精彩。你喜欢它们吗?好的,学期结束后可以读一读,这比这个更好。好了,现在我们已经收集了所有的单词,每遇到一个单词,我们就将其存储在一个变量nextWord中,然后简单地说words.add(nextWord),最后返回words。这样,我们应该能收集到故事中的所有单词,并生成一个列表,这个列表可以无限扩展,不像数组,如果使用数组会显得更混乱。关于这个方法有什么问题吗?如果你在高中学过Java,你对这个应该已经很熟悉了。
让我们打印出来,确保它能正常工作。我们再次运行程序,打印出w,得到一个漂亮的字符串表示形式。显然,列表打印出来时会显示为逗号分隔的列表,带有标点符号。好的,Java做得不错。接下来,让我们看看如何使用集合来统计文件中的单词数量。在countUniqueWords方法中,我们有一个单词列表,我们刚刚构建了这个列表,现在我们想知道有多少不同的单词。在这种情况下,如何使用集合来实现?谁知道这个技巧?对,集合只保留唯一的单词,所以我们可以怎么做?如果你有一个集合,集合中只能有一个副本,你可以将所有东西都放入集合,然后查看集合中有多少个元素。例如,如果我添加dog, dog, dog, dog, dog, cat, dog, dog, dog, cat, cat, dog, pig,集合的大小将是3,因为每个元素只能有一个副本。我们将创建一个字符串集合,称为wordSet,然后我们说new,但我们不能说new Set,所以我需要某种具体的实现,随便选一个。现在我有了一个单词集合,然后我将懒一点,稍后我们会改进。我将说for (int i = 0; i < words.size(); i++),我通常不用i++,因为我觉得那种语法有点奇怪,所以我尽量避免使用++,这只是我个人的观点。我觉得代码应该一次只做一件事,同时检查和修改某件事让我感到不安,但不管怎样,我们将逐个取出这些单词,然后说currentWord = words.get(i),这是个好名字,就像我们为双端队列接口命名一样。然后我们把currentWord加入集合,最后返回集合的大小。这样,我们就能得到故事中独特单词的数量。让我们来做这件事。
好的,这里统计出了2964个不同的单词。好极了,现在这个语法有点笨拙,大约在2000年或2001年左右,Java已经推出一段时间了,人们注意到这种语法有点多余,每次都要写这么长的循环,看起来很丑。显式生成索引并迭代它,获取索引处的元素,这样做并不好。通常的做法是使用所谓的增强型for循环,看起来像这样。我将写出来,如果你没见过这种循环,我会解释更多,但其实它很简单,我们说for (String word : words),这样我们就不需要显式获取索引处的元素或使用索引了。在这种情况下,currentWord会随着列表中的每个单词不断重生。有谁见过这种循环?有谁没见过这种循环?你们是在G7还是其他课程中学到的?
你们在61A中做过这个吗?没有。好的,我明白了,或者也许有人做过,有分歧吗?是在61A中吗?哦,对于字符串F,是的,明白了。在Python中,它看起来像`for s in words`,对吧?你们可能见过这个。
哦,我们能不能直接这样做,为什么我没有直接这样做?有人想猜猜为什么我没有这样做吗?
记住,我是老师,对,哦,对,这让我感觉不舒服。我只是想说,如果你是班上的学生,你可能在刷Facebook,或者在想其他事情,通过这样做,每个东西都有一个名字,对吧?所以基本上,这是一种单调的技巧,在实际编程中可能是有用的,它会占用一点内存,但也会使你的代码更清晰,尽管我觉得这有点傻,但它确实更有说服力,实际上我有时会在代码中这样做,把一些东西放在一个带有名称的变量中,因为命名的东西更容易阅读,所以这是唯一的原因,好问题。
现在我们将去掉这部分代码,基本上,对于那些来自Python背景的同学,你们见过类似的东西,比如`for currentWord in words`,对吧?有谁没见过这个?好的,我们了解了基本思路,如果你太害羞了,那也没办法。好的,所以我们现在确保代码仍然能运行。
11.3 Map Demo
好的,顺便说一句,今天我想要让课程不要太密集,因为你们下周有中期考试,所以我希望不要太慢,不幸的是,你们现在不能2倍速听我讲课。好的,我们要做的最后一件事,哦,对,嗯哼,我可能会这样做:`Set<L>` 或者类似的,它会生成一个列表版本,这意味着存在一个集合的构造函数,它可以接受一个集合,让我们试试看,我这样做可能有点冗长,所以这是一个很好的问题,让我们猜测一下语法可能是什么样的,也许我们可以完全绕过这一点,我们在这里试一试。
`Set<String> W = new HashSet<String>();` 然后,我实际上不需要这个字符串,对不起,几年前这是可能的,所以如果这个字符串`words`可以,好的,那么我们该在哪里尝试这样做呢?就在这里,让我们试一试,所以它是`W`,然后这是一个很好的问题,我们将`words`的大小赋给`W`,看起来编译没有问题,我敢打赌它会工作,让我们看看,964,所以确实可以工作。现在,如果我们在这里看一下,如果我们这样做`new HashSet<>`,我不会这样做,好的,列表,好的,它不会显示签名,但基本上,它确实有类似的东西,所以这是一个非常好的问题,好的。
所以,比这个循环更好的方法,接下来我们要构建一个映射。在这种情况下,我们要做的是找出单词“lottery”、“V”和“Babylon”出现的次数,所以我们要创建一个我们关心的所有单词的列表,“lottery”、“V”和“Babylon”,并且让它打印出这三个单词各出现了多少次。这又是一个你应该在做Python时见过的常见模式,如果你在CS 7中,希望你至少会觉得有些相似。
现在我们会注意到这个方法的签名就像一个怪物,看看屏幕上的样子,这对Java来说是非常正常的,虽然没有什么比这更糟糕的了,但我们得习惯。为了做到这一点,我们要构建一个映射,告诉我们给定一个字符串,它出现了多少次。
所以我们说 `Map<String, Integer> counts = new HashMap<String, Integer>();`,我们称这个东西为`counts`,等于新的`HashMap`,因为它们看起来比其他的稍微流行一些。所以我们说 `new HashMap<String, Integer>()`,现在我们将遍历每个字符串,对于字符串`W`和我们的单词列表,实际上我现在要称它们为目标单词,所以对于每个字符串和目标单词,有其他方法可以这样做,但这是最清楚的写法。我要在我的字典中放入一个从该字符串到计数的映射,所以对于我所有的目标单词,我们目前在我们的单词列表中知道有多少个?没有。
所以这基本上说的是记下我们还没有看到任何单词,好的,然后我可以这样做,对于字符串`F`在单词列表中,现在我将从`counts`中获取每个单词的计数,`get(F)`,这是旧的计数,好的,然后我将这样做,你也可以把它写成一行,但我只是写得更清晰,所以我将说`counts.put(F, oldCount + 1)`,所以这是说,当前我们正在查看的单词,比如说单词是“potato”,好的。
所以我们将说,对于字符串`F`在单词列表中,我们将获取计数,然后增加计数。然而,这段代码有一个小问题,那就是它实际上在检查,比如说获取的单词是“potato”,但“potato”不是“Babylon”或“Lottery”。所以这段代码将以一种奇怪的方式行为,事实上,让我们看看会发生什么,我将返回`counts`,是的。
`get`接受左边的一个东西,一个字符串,它是怎么工作的?它是如何实现的?这是一个很好的问题,所以抽象数据类型实际上我们不在乎,对吧?所以在映射中,`get`之前接受一个索引,但既然这是一个映射,它使用的是不同的语义,所以现在我们只是信任它的工作方式,我们将在大约四周后讨论它是如何工作的,好的,这就是抽象数据类型的荣耀。所以当我们尝试运行这段代码时,我们遇到了某种问题,看看是什么问题,没有指针异常,所以我们得到了一个空指针异常,因为它试图获取像“horse”这样的单词,但“horse”不存在,所以如果我们真的想要详细一些,我们会这样做,如果`counts.containsKey(s)`,那么做所有这些事情,所以基本上我们只有在单词实际存在的情况下才会真正增加计数,好的,所以这比实际的Java代码稍微冗长一些,但实际的Java代码也差不多这么长,所以当你运行它时,最终你会得到“D”出现了172次,“Babylon”出现了6次,“Lottery”出现了12次,好的,所以关于这个问题有任何疑问吗?在我们继续之前,有人问了一个更简洁的方法,但我不想用Arena,是的,嘿,更简洁的方法不会短很多,我警告你。
是的,为什么我们需要一个循环来设置所有的计数,然后再一个循环来遍历单词?是的,所以你实际上不需要预先设置,你可以做类似之前的事情,对吧?所以比较一下我们在这里做的,这只是一个老方法,找到独特的单词,我们不需要再讲这个,但只是指出我们在使用这个新的循环,但这是我刚写的版本,或多或少,我想将它与Python的等价版本进行比较,好的,这里有一些有趣的差异。
所以这是Java版本,Python版本当然没有声明类型,它还有一点非常有趣的地方,即使你从未见过Python,事实上,如果你从未做过Python,这可能也很有趣,我们注意到这一行`counts = {}`,这是怎么回事?这就像我们神奇地创建了一个Python字典,而在Java中我们必须说一大堆东西,比如这是我想用的抽象数据类型,这是我想用的具体实现,这里只是给我一个字典,好的,然后我们遍历这个字典,我说对于每个目标单词,我将它们设置为零,当然在Python中你也可以更简短地做这件事,然后我基本上说对于每个单词,如果它实际上在单词计数中,就增加它的值,所以我们注意到索引字典的方式很像数组,而在Java中我们必须调用这些`get`和`put`方法,对吧?
11.4 Java vs. Python
好的,所以Python版本显然更加简洁,所以我想在这里对比一下Java和Python,是的,每种语言都会有某些被认为是该语言核心特性的功能,这些功能旨在塑造你如何思考在该语言中解决问题的方式。当涉及到数据结构时,作为第一类公民的数据结构通常有一种特殊的语法用于创建和使用这些对象,所以实际上情况会变得更复杂,你将在项目中看到这一点,比如在Python中,如果你想创建一个字典来告诉你不同生物有多少条腿,你可以在一行中完成,比如说 `num_legs = {'horse': 4, 'dog': 4, 'human': 2, 'fish': 0}`,但在Java中,如果你想创建这样一个集合,你需要做类似这样的事情,你可以编写一个辅助方法来缩短代码,但默认的方法是你需要创建一个映射,然后一次一个地放入每个元素,是的,这只是Java的一个特性,你没有字典或关联数组或我们称之为的东西,这曾经引起了一些争议,即是否应该引入类似的功能到Java中,但这最终在两年前或两年半前被取消了。
也许有一天它会回归,但他们发现这太混乱了,因为在Java中,不仅仅有集合,不仅仅是列表、集合和映射,还有其他人们使用的东西,所以他们想找出是否有通用的语法对任何人可能编写的任何集合都适用,而Python中的这个符号是用来表示字典或映射的,所以Java希望使其具有可扩展性,但他们没有找到正确的方法来实现这一点,所以编程语言是由人创造的,也许这就是教训之一,有时候真的很难解决这些问题,好的。
所以关于Java的一件有趣的事情是,它实际上给了你控制分离实现和数据的能力,即抽象数据类型,因此高级用户可以选择我想要一个哈希映射或树映射,在这两种情况下,都是相同的抽象数据类型(字典),但这里是基于哈希表的,这里是基于平衡二叉搜索树的,因此实际上这样做可以让真正需要调整性能的用户做出选择,而在Python中,你得到一个字典,那就是你得到的字典,除非你自己从头开始实现。
另外,例如,如果你迭代这个树映射,也就是你只是依次遍历每个键,你会按字母顺序得到它们,但在哈希映射中,你不会按字母顺序得到它们,实际上你没有任何保证的顺序,所以哈希映射可能更快,但在大多数情况下,树映射会按字母顺序返回键,所以这给了你一些更细粒度的控制,好的。
顺便说一下,我在61B的任务似乎是让内容更冗长,好吧,所以有些事情是这样的,我认为即使是Java程序肯定更加冗长,毫无疑问,但实际上它们可以更快,其中一个原因是静态类型,这很好,因为你得到了类型检查,但我也绝对喜欢Java的一点是,当你查看一个程序并看到一个如`collect_word_count`这样的方法时,只要它不是太复杂,好的一点是当我看到这个时,我知道某人在给我两个列表,而我返回一个映射,这在Python中并不是这种情况,除非你使用可选的类型指定功能,但我从未真正使用过,所以我喜欢Java中存在静态类型的事实。
另一个是语言中的偏见,它具有强制接口继承的特殊特性,我认为接口更好,所以你得到更干净的继承,另一个问题是像`private`关键字这样的东西,它们使得抽象边界坚固,并防止大型项目因自身重量而崩溃,因为如果你不能编辑某个东西,如果你不能访问我决定为私有的变量,那就防止你破坏某些东西,这也使代码更高效,例如,你可以选择哈希映射或树映射,你有单值数组,这使它们比Python稍微快一些,还有一个有趣的话题是,Java中的基本数据结构更接近计算机内部的实际结构,所以如果我们做了类似于数组队列的事情,例如在Python中,这会有点尴尬,因为没有必要进行数组调整,Python中最像数组的东西是列表,实际上已经基本上是这样了,它们可以按照需要自动扩展,实际上有趣的是,Python中的列表实际上就是一个ArrayList,就像我们在课堂上做的那样,但你不会真的知道这一点,因为良好的语义工作得很好,好的,所以关于这一点有什么想法吗?对于一些琐事,是的。
Java的第一类公民是类,是的,所以实际上让我们看看在Java中真正容易做什么类型,我认为类型是最大的一个,因为你可以进行类型检查的黑客攻击。
11.5 Abstract Classes
将各种概念融入到Paesan中,但就是这样,类型是一个大话题,这是一个有趣的问题,还有什么其他的呢?现在,这是一个明显不性感的话题,所以我想提一下接口作为一种抽象类,所以在过去的几讲中,我们看到了`implements`和`extends`可以允许我们进行接口继承和实现继承,作为提醒,接口继承是你能做什么,实现继承是你如何做它。现在事实证明,在现代时代,接口最初仅用于接口继承,而在现代时代,它们既可以有抽象方法,即只有分号结尾的方法,也可以有默认方法,这些方法实际上是有所作为的,它们有具体的实现。现在我不确定我是否在幻灯片上说过,但我肯定口头提到过,在接口中每个方法都必须是公共的,所以任何时候我在这里加上`public`都是多余的,IntelliJ会提示我们不需要这样做,另一点是,除非你使用`default`关键字,否则方法就是所谓的抽象方法,也就是说它以分号结尾,它是抽象的,意味着它实际上什么都不做,它只是你必须能够做的事情,好的。
所以,关于接口我还有一件事要说,我们要谈谈它们的近亲,也就是说你实际上可以在接口中放置变量,在这种情况下,它们将是`public static final`的,这意味着它们是公共的,是静态的,你知道这些是什么意思,而`final`只是意味着你永远不能改变这个值,所以你可以用它来定义常量,比如重力。顺便说一句,类可以实现多个接口,所以我可以实现`Universe`和`Illusion`,并制作一个既具有宇宙特征又具有幻象特征的网站,也许会有一些默认方法。正如我们知道的,接口不能被实例化,它们可以提供抽象方法或具体方法,这些是默认方法,你可以获得`public static final`变量,而且我们只能拥有公共方法,它们必须以分号结束,好的。
实际上还有一个密切相关的想法,称为抽象类,我们在本课程中不会经常使用它们,但你偶尔会在Java库中看到它们,所以我想让你知道它们是什么。抽象类有点介于接口和类之间,所以它们不能被实例化,你不能说`new List`,同样地,在Java教程中也有一个例子叫做图形对象,你不能创建一个新的图形对象。虽然抽象类不能被实例化,但它们可以做一些接口不能做的事情,所以就像接口一样,它们可以提供抽象方法或具体方法,所以它们可以给你分号结尾的方法或实际的方法,但语义略有不同,这有点像语言中的关键字问题,变得比应该的更复杂了,但在抽象类中,默认情况下任何你指定的方法都是具体的,任何你想说成抽象的方法都必须使用`abstract`关键字,对不起,就是这样,而且这些正好与接口中的默认选择相反,在抽象类中默认是具体的。
你还可以提供任何类型的变量和方法,比如我可以有`public int x, y`,这意味着每个图形对象都会得到一个`x`和`y`,它们是公共的,好的,同样的,对于方法,你可以提供任何你想要的类型,甚至可以有私有方法来继承,好的,就像我们之前看到的,任何时候你有一个抽象方法,如果你有一个具体的类,而它实际上没有覆盖任何抽象方法,它就会报错,所以例如,如果图形对象提供了`draw`和`resize`方法,那么圆类必须实现`draw`和`resize`,它不需要实现`move`——为什么它不需要实现这个?因为它已经有了一个实现,好的,它会继承这个方法。
有人问这个看得清不清楚,哦,它在屏幕上比这更清楚,所以这种浅蓝色表示抽象类和具体类,实际上你可以构建一个层次结构,顶部是一个接口,中间是一个抽象类,底部是一个具体类,所以我把这丢给你是因为听我谈论这个可能会有点无聊,所以你可以思考我最近说了什么。在这个例子中,我有一个纸张粉碎机接口、豪华型号抽象类和一个特定的具体类未显示,问题是当我们沿着继承层次向下走时,DCX 9000复印机(我编造的)还需要实现多少个方法?好了,我收回房间的控制权,有些分歧,好的,让我们谈谈这个接口。
这个接口提供了多少个抽象方法?你怎么知道它们是抽象的?它没有说“我是抽象的”,这就是接口的特点,如果你不说,那就是抽象的,好的,现在我们来看豪华型号,这些方法中有多少个是抽象方法?一个,这个连接Wi-Fi的方法,好的,所以到目前为止,你会说这里有三个抽象方法,但是这个类实际上具体实现了多少个方法?一个,哦,其实是两个,一个是计数,一个是粉碎,这两个都是从上面继承来的,所以基本上这个类提供了两个抽象方法,它实现了一个,但又增加了一个,所以最终的答案是两个,还有两个需要实现,它们是连接Wi-Fi和粉碎,好的,为什么不需要说`public abstract shred`?你不需要显式地说它,因为它是从接口继承来的,在这个类中你不需要实现它,抽象类不需要实现接口方法,你甚至不需要列出它们,我有可能错了,但我相当确定我是对的,好的,我们现在可以尝试,但我害怕打开它,是的,它是公共的,所以在这个类中我写`public abstract void shred`,`shred`是个好问题,所以如果我实际写`public abstract void shred`,就像我刚才重复的一样,这是一个很好的问题,它不会有语义影响,我不知道它会怎么做,我会在IntelliJ中打开它,但我没有准备好文件,不想输入所有这些东西,所以我会把这个话题留待以后讨论,但这是一个有趣的问题,如果你实际再次写下这个抽象类会发生什么?
不过最终还是有两个任务留给你,你需要写`connect to Wi-Fi`和`shred`,`shred all`已经完成了,所以总结一下,这基本上就是我要说的关于它们的所有内容,我会再多说一点,但对于接口,主要目标是接口继承,你可以做一点实现继承,类可以实现多个接口,而抽象类基本上可以做接口能做的所有事情,但它们有更多的能力,比如可以提供非公共方法,子类当然也只能扩展一个抽象类,因为Java有一个规则,你只能扩展一个类,在我看来,如果你不确定应该用什么,你应该使用接口,抽象类有时会出现,但我基本上觉得你在语言中使用的工具越复杂,你就越可能给自己找麻烦,所以你应该尽可能限制自己的选择,Oracle有一个完整的教程,说明何时使用每种工具,这不是软件工程课程,我们不会深入讨论太多,但我确实想向你展示一点Java为何使用抽象类的例子,这是Java中列表的工作原理,这并不是全部,但大部分是这样,有一个列表接口和一个抽象列表抽象类,名字起得很好,然后有两个具体的实现扩展了这个类,抽象列表做了什么?它提供了例如一个添加方法,用于在列表末尾添加一个元素,它是如何做到的?它使用了自己的`size`方法和将由具体类实现的`add`方法,所以数组列表和链表它们是具体的实现,这是在到达底层之前仍然抽象的方法,每个类都必须能够实现在列表任何位置添加元素的方法,但如果你不想实现将元素添加到列表末尾的方法,那就是`add`的作用,好的。
现在你可能会问自己一个问题,为什么这不能只是列表接口中的默认方法?我认为原因在于接口直到两年前才有默认方法,所以这可能是为什么,好的,抽象列表是公共的,我们不能简单地丢弃它,所以这是一个抽象列表的例子,现在实际上更复杂一点,这是真实的列表层次结构的样子,链表扩展了抽象序列列表,而抽象序列列表又扩展了确切列表,确切列表扩展了抽象集合,抽象集合实现了集合接口,好的,如果你感兴趣,你可以浏览源代码,我们不会太在意这些,但事实上我撒谎了,这是真正的层次结构,布局链接列表昨天是件有趣的事,它真的很奇怪,比如抽象集合实现了列表,但列表也扩展了集合,抽象集合扩展了集合,你知道的,如果你对这些感到兴奋,你可以稍后关于它们的信息,好的,但我只是想让你知道,基本上你会在查看文档时遇到抽象类,但你可能不需要自己写抽象类。
11.6 Packages
好的,最后是我自己,好吧,关于包的快速问答,但不会很难,好的,包是我最后想谈的一个小话题,因为它会出现在期中考试后的第一次作业中,在这种情况下,我将再次转向Python,我认为Python有一套很好的行为准则。
其中一条被称为Python的罪恶,是一些美好的东西,比如可读性很重要等等,但有两条在这里将非常重要,一是显式优于隐式,这也是为什么在讲座中我倾向于写出那些只命名为`and`的小傻变量的原因之一,另一个是命名空间是一个非常好的主意,我们应该多用一些,所以包解决了这两个问题,让我们弄清楚什么是命名空间,好吧,包的目标是我们真的想给每件事一个规范的名字,事实上,这个规范表示的概念是一个美妙的数学概念,应该刻在你的脑子里,它基本上只是一个事物的独特表示,所以非规范的是车牌号码,你可以重复使用一个车牌,比如如果我卖掉了一辆车,其他人可以买下我的车牌,同样,如果我买了一辆车,我可能会得到一个新的车牌,然而汽车都有编号,所以这里指的是非常特定的一辆摩托车,我觉得那可能是我被盗的摩托车,也可能记不清了,所以这是一个规范的名字,它与那辆摩托车相关联,好的,所以在Java中,我们可以通过给每个类一个我们称之为包名的东西来尝试赋予规范性,好的,这将是一些术语上的乱炖,但包是一个所谓的命名空间,用于组织类和接口,在作业1中,我们将创建我们自己的,但让我给你举一个小例子,好的。
所以包基本上解决了类名可能冲突的问题,你可能有多个名为Dog的类,也许五个人各自写了一个Dog类,所以我可以做的是说,这不仅是一个Dog,它将是一个名为`ugh.Josh.eh.animal.Dog`的类,好的,现在那将是一个非常笨拙的名字,所以我不会把类名弄得特别长以确保没有人有相同的名字,所以我们将在文件顶部放上`package ugh.Josh.eh.animal;`,实际上,谁在这里曾经被IntelliJ自动插入过包名?有人可能在自动评分器出问题的时候遇到过,可能是这样,所以那是为什么他们改变了整个命名空间的情况,好的,所以正如我们将在期中考试后看到的,我们会想这样做,我们将在所有文件中开始放入包,因为这意味着现在如果你这样做,比如包名我选了`Oh.Josh.H`,原因是我们的包名通常以网站地址开头,所以如果我的网站地址是`Joshhqg`,我会把我写的所有东西放在`uhg.Josh.H.whatever`包中,好的,所以这样做的好处是,当我真正想创建一个Dog时,我可以非常具体地指出我想要哪个Dog,我不只是要内置的Dog,也不要Frank的Dog,我想要的是`oh.Josh.H.animal.Dog`,现在正如我们所见,在JUnit讲座的超速轮中,输入整个名称可能会很烦人,所以我们如何在Java中解决这个问题?通过导入,所以当我导入时,它基本上说的是从现在开始,任何时候你看到Dog,你想到的就是`josh.h.animal.Dog`,也可以进行通配符导入,这基本上说的是我不仅想要Dog类,我还想要某个包中的所有东西,所以在那种情况下,我会这样做`import Josh.H.animal.*;`,所以这里有一个星号语法,我觉得这里缺少一个词,这将确保我得到那个包中的所有东西,所以这可能不好,有人想猜猜为什么吗?它污染了你的命名空间,你得到了所有这些东西,你不知道自己到底得到了什么,你是隐式的而不是显式的,如果你从多个包中导入,你可能会有冲突,比如你从三个包中获取所有东西,而这三个包碰巧都有一个Dog类,那么你就会有冲突,现在这与Python有什么不同?Python中的导入做了什么?它们让你使用来自其他文件的东西,这在Java中不是必要的,好的,我只是想澄清一下,Java中的导入并不是让你去我的电脑的其他地方寻找库,你们已经知道你们必须做整个IntelliJ的事情,而是从现在开始,语言请将Dogs语义上视为来自这个包的Dogs,好的,所以我只想简要介绍一下包,让它留在我们的脑海中,我们将在期中考试后回到这个话题,今天就到这里。