Java-代码面试完全指南(一)

25 阅读1小时+

Java 代码面试完全指南(一)

原文:zh.annas-archive.org/md5/2AD78A4D85DC7F13AC021B920EE60C36

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Java 是一种非常流行的语言,在各种领域和行业的 IT 工作岗位中都有很多需求。由于 Java 赋予全球数十亿设备动力,它已成为一种非常吸引人的学习技术。然而,学习 Java 是一回事,开始在 Java 领域发展职业是另一回事。本书专门为那些想要发展 Java 职业并希望在 Java 中心面试中脱颖而出的人而写。

通过本书,您将学会如何做到以下几点:

  • 以一种对抗时尚解决 220 多个最受欢迎的 Java 编码面试问题,这些问题在包括谷歌、亚马逊、微软、Adobe 和 Flipkart 在内的众多公司中都会遇到。

  • 收集解决各种 Java 编码问题的最佳技术。

  • 解决旨在培养强大和快速逻辑能力的耐人寻味的算法。

  • 重点介绍了可以决定成功与失败之间差异的常见非技术面试问题。

  • 全面了解雇主对 Java 开发人员的要求。

通过本书,您将建立起解决 Java 编码面试问题的坚实信息基础。从本书中获得的知识将使您对自己充满信心,从而获得您的以 Java 为中心的梦想工作。

本书适合的读者是谁

《Java 完整编码面试指南》是一个全面的资源,适用于那些正在寻找 Java 开发人员(或相关)工作并需要以对抗时尚的方式解决编码问题的人。它专门为初级和中级候选人而设计。

本书涵盖了什么

第一章从哪里开始以及如何为面试做准备,是一本全面指南,解决了从零到聘用的 Java 面试准备过程。更确切地说,我们想要强调可以确保未来职业道路顺利成功的主要检查点。

第二章大公司的面试是什么样子,讨论了在谷歌、亚马逊、微软、Facebook 和 Crossover 等主要大型科技公司进行面试的方式。

第三章常见非技术问题及如何回答,解决了非技术问题的主要方面。面试的这一部分通常由招聘经理甚至人力资源部门负责。

第四章如何处理失败,讨论了面试的一个微妙方面 - 处理失败。本章的主要目的是向您展示如何识别失败的原因以及如何在将来减轻它们。

第五章如何应对编码挑战,涵盖了通常被称为技术面试的技术测验和编码挑战主题。

第六章面向对象编程,解释了在 Java 面试中遇到的面向对象编程的最受欢迎的问题和问题,包括 SOLID 原则和编码挑战,如点唱机、停车场和哈希表。

第七章算法的大 O 分析,提供了分析算法效率和可伸缩性的最流行指标,即大 O 符号,在技术面试的背景下。

第八章递归和动态规划,涵盖了面试官最喜欢的话题之一 - 递归和动态规划。这两个主题彼此紧密合作,因此您必须能够同时涵盖两者。

第九章位操作,解释了您在技术面试中应该了解的位操作的最重要方面。这类问题在面试中经常遇到,而且并不容易。在本章中,您将遇到 25 个这样的编码挑战。

第十章数组和字符串,涵盖了涉及字符串和数组的 29 个热门问题。

第十一章链表和映射,教授您在面试中遇到的与映射和链表相关的 17 个最著名的编码挑战。

第十二章栈和队列,解释了涉及栈和队列的 11 个最受欢迎的面试编码挑战。主要是要学习如何从头开始提供栈/队列实现,以及如何通过 Java 内置实现解决编码挑战。

第十三章树和图,涵盖了面试中最棘手的话题之一——树和图。虽然与这两个话题相关的问题有很多,但实际面试中只有少数问题会遇到。因此,非常重要的是高度重视涉及树和图的最受欢迎的问题。

第十四章排序和搜索,涵盖了技术面试中遇到的最受欢迎的排序和搜索算法。我们将涵盖诸如归并排序、快速排序、基数排序、堆排序和桶排序等排序算法,以及二分搜索等搜索算法。通过本章结束时,您应该能够解决涉及排序和搜索算法的各种问题。

第十五章数学和谜题,讨论了面试中的一个有争议的话题:数学和谜题问题。许多公司认为这类问题不应该成为技术面试的一部分,而其他公司仍然认为这个话题对面试很重要。

第十六章并发,涵盖了一般面试中涉及 Java 并发(多线程)的最受欢迎的问题。

第十七章函数式编程,探讨了 Java 函数式编程的最受欢迎的问题。我们涵盖了关键概念、lambda 和流。

第十八章单元测试,讨论了您在申请开发人员或软件工程师等职位时可能遇到的单元测试面试问题。当然,如果您正在寻找测试人员(手动/自动化)职位,那么本章可能只是测试的另一个视角。因此,请不要期望在这里看到特定于手动/自动化测试人员职位的问题。

第十九章系统可扩展性,提供了在初中级面试中可能会被问到的最广泛的可扩展性面试问题,比如 Web 应用软件架构师、Java 架构师或软件工程师。

充分利用本书

您只需要 Java(最好是 Java 8+)和您喜欢的 IDE(NetBeans、IntelliJ IDEA、Eclipse 等)。

我还强烈建议读者参考 Packt 出版的Java 编码问题书,以进一步提高您的技能。

下载示例代码文件

您可以从www.packt.com的帐户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载

  4. 搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有其他代码包,来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/上找到。来看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:

static.packt-cdn.com/downloads/9781839212062_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

文本中的代码:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:'Triangle,Rectangle 和 Circle类实现Shape接口并重写draw()`方法以绘制相应的形状。"

代码块设置如下:

public static void main(String[] args) {
 Shape triangle = new Triangle();
 Shape rectangle = new Rectangle();
 Shape circle = new Circle();
 triangle.draw();
 rectangle.draw();
 circle.draw();
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

public static void main(String[] args) {
 Shape triangle = new Triangle();
Shape rectangle = new Rectangle();
 Shape circle = new Circle();
 triangle.draw();
 rectangle.draw();
 circle.draw();
}

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中出现。例如:"然而,这种方法对于第三种情况 339809(1010010111101100001)不起作用。"

提示或重要说明

看起来像这样。

联系我们

我们始终欢迎读者的反馈。

customercare@packtpub.com

勘误:尽管我们已经尽一切努力确保内容的准确性,但错误确实会发生。如果您在本书中发现错误,我们将不胜感激地向我们报告。请访问www.packtpub.com/support/err…,选择您的书,点击勘误提交表单链接,然后输入详细信息。

copyright@packt.com,附有材料的链接。

如果您有兴趣成为作者:如果您在某个专题上有专业知识,并且有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。阅读并使用本书后,为什么不在购买书籍的网站上留下评论呢?潜在读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packt.com

第一部分:面试的非技术部分

本节的目标是涵盖面试的非技术部分。这包括面试惯用语和大公司的模式,如亚马逊、微软、谷歌等。您将熟悉主要的非技术面试问题及其含义(面试官如何解释答案)。

本节包括以下章节:

  • [第一章],从哪里开始以及如何准备面试

  • [第二章],大公司面试的模式

  • [第三章],常见的非技术问题及如何回答

  • [第四章],如何处理失败

  • [第五章],如何应对编程挑战

第一章:从哪里开始,如何为面试做准备

本章是一个全面的指南,涵盖了从最开始到被聘用的 Java 面试准备过程。更确切地说,我们想要强调可以确保未来职业道路顺利和成功的主要检查点。当然,在你阅读本书的时候,你可能会发现自己处于这些检查点中的任何一个:

  • 尽早开始面试准备

  • 获得正确的经验

  • 向世界展示你的工作

  • 准备你的简历

  • 参加面试

在本章结束时,你将清楚地了解如何根据你目前的状态实现前述检查点。因此,让我们从覆盖第一个检查点开始,看看新手面试路线图。

新手面试路线图

让我们从一个基本的真理开始,这是绝对必要的,但不足以成为成功的开发者:最优秀的 Java 开发者对他们的工作充满激情,而且,随着时间的推移,真正的激情会变成职业。长期来看,激情是无价的,它会让你脱颖而出,远远超过那些技术娴熟但缺乏激情的人。

既然你买了这本书,你想要在 Java 软件开发职业中投入一些时间和金钱。主要是,你想成为令人惊叹的 Java 生态系统的一部分!你已经感受到了专注于使用 Java 工作所带来的力量和能量,因此,即使你还没有积极地考虑过,你已经开始为 Java 面试做准备。

很可能,你是一名学生,或者刚刚获得了 IT、计算机科学学士学位,或者你只是发现自己对 Java 语言有天赋。然而,既然你在这里,你一定有很多关于如何在 Java 生态系统中找到梦想工作的问题和疑虑。

是时候制定成功的计划了!以下流程图代表了一个学生或 Java 新手的面试路线图,他们想成为 Java 生态系统的一部分:

图 1.1 - 新手面试路线图

图 1.1 - 新手面试路线图

在这一章中,我们将涵盖前面图表中的每一项。让我们从第一项开始,了解自己

了解自己

在寻找工作之前,了解自己是很重要的。这意味着你应该知道自己是什么样的开发者,想要什么样的工作。

这对于获得正确的经验、发展你的技能包和找到合适的雇主至关重要。很可能,你可以涵盖各种 Java 编程任务,但你是否觉得它们都同样吸引人?做一些自己不喜欢的事情短期内是可以的,但长期来看是行不通的。

理想情况下,长期来看,你必须专注于自己最喜欢做的事情!这样,你最大程度地提高了成为顶尖 Java 开发者的机会。但是,做自己最喜欢的事情应该考虑到 IT 市场提供的内容(无论是短期还是长期)。一些 Java 技术在工作机会中得到了广泛覆盖,而其他一些可能需要很长时间才能找到工作,或者必须做一些非常不愉快的权衡(例如,搬迁)。强烈建议定期参考并参与(每一票都很重要)由网站如 blogs.oracle.com、snyk.io、jaxenter.com、codeburst.io、jetbrains.com 和 dzone.com 进行的最相关的 Java 调查。有很多公司可供选择,从统计学上来看,最大程度地提高了找到适合你的公司的机会。这是问题的一半,另一半是为确保你想要的工作的公司也想要你而做好准备。

现在,让我们来看看 10 个问题,这些问题将帮助你确定你计划成为什么样的开发者。审视自己,尝试将你的个性和技能与以下问题和解释相重叠:

  1. 你对开发用户界面或在幕后执行的重要业务逻辑感兴趣?开发出色的用户界面是图形界面的一个极其重要的方面。毕竟,图形界面是最终用户看到和与之交互的内容。它需要创造力、创新、远见和心理学(例如,开发多设备界面是相当具有挑战性的)。它需要对 Java AWT、Swing、JavaFX、Vaadin 等有所了解。另一方面,在幕后执行并回应最终用户操作的业务逻辑是界面背后的引擎,但对于最终用户来说,它大部分时间是一个黑匣子。业务逻辑需要强大的编码技能和对算法、数据结构、框架(如 Spring Boot、Jakarta EE 和 Hibernate)、数据库等的扎实知识。大多数 Java 开发人员选择编写幕后的业务逻辑(用于桌面和网络应用程序)。

  2. 你觉得哪种应用程序最吸引人(桌面、移动、网络或其他)?每种类型的应用程序都有特定的挑战和专用的工具套件。如今,公司的目标是尽可能多地吸引消费者,因此现代应用程序应该适用于多平台设备。最重要的是,你应该能够在编码时知道该应用程序将在不同的设备上公开,并与其他系统进行交互。

  3. 你是否特别感兴趣测试、调试和/或代码审查?具有编写有价值的测试、发现错误和审查代码的强大技能是保证高质量最终产品的最重要的技能。在这三个领域中,我们应该专注于测试,因为几乎任何 Java 开发人员的工作描述都要求候选人具有编写单元测试和集成测试的强大技能(最常用的工具是 JUnit、TestNG、Mockito 和 Cucumber-JVM)。然而,试图找到一个专门的 Java 测试工作或 Java 代码审查员是相当具有挑战性的,通常在大公司(特别是在提供远程工作的公司,如 Upstack 或 Crossover)中遇到。大多数公司更喜欢成对代码审查,每个 Java 开发人员都应该编写有意义的测试,为他们编写的代码提供高覆盖率。因此,你必须能够做到两者:编写令人惊讶的代码,并为该代码编写测试。

  4. 你对与数据库交互的应用程序感兴趣,还是试图避免这样的应用程序?大多数 Java 应用程序使用数据库(关系数据库或 NoSQL 数据库)。广泛的 Java 开发人员工作将必然要求你对通过对象关系映射框架(如 Hibernate)、JPA 实现(如 Hibernate JPA 或 Eclipse Link)或 SQL 中心库(如 jOOQ)编码具有强大的知识。大多数 Java 应用程序与关系数据库交互,如 MySQL、PostgreSQL、Oracle 或 SQL Server。但在相当数量的应用程序中也会遇到 NoSQL 数据库,如 MongoDB、Redis 或 Cassandra。试图避免开发与数据库交互的应用程序可能会严重限制提供的工作范围。如果这是你的情况,那么你应该从今天开始重新考虑这一方面。

  5. 你是否偏爱代码优化和性能?关心代码性能是一项非常受赞赏的技能。这样的行为会让你被归类为一个注重细节的完美主义者。拥有优化代码并提高性能的解决方案将很快让你参与设计和架构功能需求的解决方案。但在面试时(编码挑战阶段),不要专注于代码优化和性能!只需专注于提供一个可行的解决方案,并尽可能地编写清晰的代码。

  6. 对你来说,更吸引人的是专注于编码的工作还是成为软件架构师?作为一名 Java 开发人员,你在职业生涯的开始阶段将专注于编码并在代码层面上做出实现设计决策。随着时间的推移,一些开发人员发现自己在设计大型应用程序方面有能力和兴趣。这意味着是时候从 Java 开发人员进化为 Java 架构师,甚至是 Java 首席架构师。虽然编码仍然是你工作的一部分,但作为架构师,你将在同一天戴上不同的帽子。你需要在会议、架构设计和编码之间分配时间。如果你觉得自己有设计和架构项目不同部分的能力,那么建议你考虑一些软件架构方面的培训。此外,在专注于编码的工作期间,挑战自己看看你能找到什么解决方案,并将其与应用程序当前架构师实施的解决方案进行比较。

  7. 你更倾向于小公司还是大公司?选择小公司还是大公司是一种权衡。理想情况下,大公司(品牌)会提供稳定性、职业发展路径和良好的薪酬计划。但你可能会感到受到官僚主义、缺乏沟通和部门之间的竞争以及冷漠僵化的环境的限制。在小公司,你有机会更加强烈地感到自己是成功的一部分,并且会感受到成为一个小社区(甚至是一个家庭)的温暖愉悦感。然而,小公司可能会很快失败,你可能会在一两年内被解雇,很可能没有任何补偿计划。

  8. 你是针对软件公司(从事各种项目)还是特定行业(例如石油工业、医药、汽车工业等)?软件公司管理来自各个领域的项目(例如,软件公司可能同时为好莱坞明星开发网站、开发金融应用程序和航空交通管制应用程序)。从开发者的角度来看,这意味着你需要多方面思考,能够快速适应并理解不同业务领域的要求,而不需要深入了解这些领域。另一方面,大型行业(例如石油工业)更倾向于创建自己的 IT 部门,开发和维护特定于该公司领域的应用程序。在这种情况下,你很可能也会接受一些关于该公司领域的培训。你将有机会成为开发特定领域应用程序的专家。

  9. 你更喜欢远程工作吗?在过去几年中,大量公司决定雇佣远程开发人员。此外,像 Upwork、Remote|OK、X-Team 和 Crossover 这样的新公司是 100%远程公司,只招聘远程职位。在世界的任何一个角落工作并拥有灵活的工作时间是非常吸引人的。这些公司为初级、中级和高级开发人员提供工作机会,其中一些公司(例如 Crossover)还提供远程管理职位。但是,你也必须意识到这种安排的一些其他方面:可能会通过网络摄像头进行监控(例如,每 10 分钟拍摄一次快照);你需要在完全远程的团队中工作,团队成员来自不同的时区(例如,参加夜间会议可能会有挑战);你需要熟悉 JIRA、GitHub、Zoom、Slack、Meetup 和内部市场平台等工具;你可能会面临大量摩擦(大量电子邮件)和缺乏沟通;你需要支付税款,最后但同样重要的是,你可能需要实现不切实际的指标以牺牲质量来保持你的职位。

  10. **管理是否吸引您?**通常,达到管理职位是需要领导能力的目标。换句话说,您应该能够在技术和人类层面上做出重要决策。从这个角度来看,您需要避免那些提供扎实技术职业道路但不提供晋升到管理层的机会的公司。

重要提示

了解自己是生活中做出最佳决定所需的最困难的部分之一。有时,询问其他人的意见是消除您对自己的主观看法的最佳方式。大多数时候,询问您的老师、父母和朋友将帮助您更好地了解您的技能和最适合您的位置。独自做出重要决定是有风险的。

一旦了解自己,就是了解市场的时候了。

了解市场

知道自己想要什么很好,但还不够。作为下一步,您应该研究市场对您的需求。目标是获得您想要的和市场提供的完美结合。

重要提示

发展市场技能是在不久的将来找工作的重要方面。

首先,您必须检查过去几年中最受欢迎的 Java 技术以及未来趋势。随着时间的推移,保持相对稳定受欢迎度的技术是公司中最常用的技术。

花时间阅读来自重要网站的过去 2-3 年的几项调查,如 blogs.oracle.com,snyk.io,jaxenter.com,codeburst.io,jetbrains.com 和 dzone.com。主要可以在 Google 上搜索java technologies survey 2019或类似的关键词组合。同时,不要忽视财务部分,确保搜索java salaries survey 2019

您将找到许多调查,它们很好地总结了最受欢迎的技术,正如您在以下两个图中所看到的。第一个显示了应用服务器的受欢迎程度:

图 1.2 - 使用的应用服务器

图 1.2 - 使用的应用服务器

以下图显示了开发人员更喜欢使用的框架:

图 1.3 - 开发人员更喜欢使用的框架

图 1.3 - 开发人员更喜欢使用的框架

阅读时,列出并记录哪些 Java 技术最受欢迎,哪些技术目前不值得关注。这将是一个类似于以下的列表:

图 1.4 - 按受欢迎程度分割技术

图 1.4 - 按受欢迎程度分割技术

通过这种方式,您可以快速过滤市场上最需要的技术。学习热门技术最大化了您在不久的将来找工作的机会。

此外,通过以下方式了解市场对您添加到Popular列的技术的态度:

  • 社交网络:大量社交网络包含有关技术和 IT 行业趋势的帖子。一些主要参与者是 LinkedIn,Stack Overflow,Twitter,Reddit 和 Facebook。

  • 书店:图书出版商努力满足编程社区对最受欢迎技术的兴趣。他们通过对值得在他们的书中涵盖的主题进行严肃的研究活动来进行过滤。一本新书或某一主题或技术的大量书籍是编程社区对该主题的兴趣的良好指标。然而,要注意突然变得流行的技术。大多数时候,这样的技术不会立即被公司采用。可能需要多年才能被采用,或者它们可能永远留在阴影中。

  • 课程和培训:除了学院和大学外,大量网站致力于提供热门和热门话题的课程和培训。

一切都是关于获得正确的经验

你知道自己想要什么和市场提供了什么。这很酷!现在是时候获得正确的经验了!没有经验就没有简历,没有简历就没有面试,因此,这是一个重要且费力的步骤。接下来的小节将帮助你实现两个主要目标:

  • 积累大量的技术知识和技能。

  • 在 Java 生态系统中赢得信任和可见度。

注意 - 这两个目标不会一夜之间实现!这需要时间和毅力,但有一个明确且有保证的结果 - 你将成为顶尖的 Java 开发者。所以,让我们开始吧!

开始做些什么

对于学生或应届毕业生来说,很难决定从哪里开始获得经验并写简历。你知道你应该开始做些什么,但你无法决定那个什么应该是什么。嗯,那个什么应该是代码。在你有任何正式工作之前,参与学校项目、实习、编程、志愿工作和任何形式的实践经验。

是时候在网上大放异彩了

尽快上网展示你的能力是必不可少的(例如从学校开始)。公司和编程社区都希望看到你在网上的成长。但在你跳入之前,确保你遵循下面的两条黄金规则:

  • **非常重要的是要注意在网上展示你的工作时使用的身份。**不要使用虚假的凭据、头像、昵称、电子邮件、密码等。你现在创建的账户(例如 GitHub、Stack Overflow、LinkedIn、YouTube、Twitter 等)很可能会在整个互联网上共享,并让你出名。始终使用你的全名(例如 Mark Janel,Joana Nimar),为你的个人资料使用一张相关的照片(如下图所示),并在账户(例如@markjanel,joananimar)和电子邮件地址(例如 mark.janel@gmail.com)中使用你的名字。虚假的名字、电子邮件和昵称很难与你和你的工作联系在一起:

图 1.5-使用相关照片

图 1.5-使用相关照片

  • **始终接受批评并保持礼貌。**在网上展示你的工作会吸引批评。你收到的极少部分评论是毫无逻辑的恶意评论。在这种情况下,最好的做法是忽略这样的评论。但大多数评论都是积极和建设性的。始终用论据回应这样的评论,并始终保持礼貌。**常识是最重要的技能!**要开放并保持对其他观点的开放!

不要感到失望或沮丧。永远不要放弃!

为开源项目做贡献

为开源项目做贡献是衡量你的技能并迅速获得经验和向寻找候选人的公司展示自己的超音速途径。不要低估自己!小小的贡献也同样重要。甚至阅读和理解开源项目的代码也是获得编码经验和学习编码技巧的绝佳机会。

许多开源项目鼓励和支持开发者做出贡献。例如,查看以下截图中的 Hibernate ORM 开源项目:

图 1.6-为开源项目做贡献

图 1.6-为开源项目做贡献

你有机会在你以后的日常工作中使用的代码上留下自己的印记!而且它也被数百万的开发者使用。多酷啊!?

开始你自己的 GitHub 账户

除了为开源项目做贡献外,建议开设自己的 GitHub 账户。雇主会在见到你之前评估你的 GitHub 个人资料内容。不要忽视任何方面!花时间整理你的 GitHub 个人资料,让它展现你最好的代码。请记住,最糟糕的 GitHub 账户是空账户或者长期低活跃度的账户,如下图左侧所示:

图 1.7 - 四个月的 GitHub 贡献

图 1.7 - 四个月的 GitHub 贡献

表现出对清晰代码和有意义的README.md文件的偏好,并避免长期低活跃度,如前一张截图所示。

开始你自己的 Stack Overflow 账户

Stack Overflow 是评估你工作的下一个站点。你在 Stack Overflow 上的问题和答案将出现在谷歌搜索中,因此,你必须特别注意你发布的内容(问题和答案)。一般来说,你的问题可能会显示你的知识水平,因此不要发布简单的问题、在文档中有简单答案的问题、隐藏在琐碎编程挑战背后的问题等。另一方面,确保提供有价值的答案,不要重复别人的答案。提供能为你带来徽章的内容,而不是负面评价。将你的 GitHub 个人资料链接到你的答案,以提供完整的解决方案。

开设自己的 YouTube 频道

除了娱乐,YouTube 也是一个巨大的技术知识来源。在 YouTube 上,你可以发布完整的编程解决方案,向人们展示如何编程,如何成为更好的程序员。如果你做到以下几点,你可以迅速增加你的 YouTube 订阅者:

  • 不要制作过长的视频(保持在 10-20 分钟的课程)!

  • 确保你有一个好的网络摄像头和麦克风。一个好的网络摄像头至少有 1080p 的分辨率,一个好的麦克风是 Snowball ICE;录制时使用免费或低成本的工具,比如 Free2X Webcam Recorder(free2x.com/webcam-recorder)和 Loom(loom.com);Camtasia Studio 也很棒(techsmith.com/video-editor.html)。

  • 展现出优秀的英语能力(英语在 YouTube 上最常用)。

  • 介绍自己(但要快速)。

  • 热情(向人们展示你享受你的工作,但不要夸大其词)。

  • 务实(人们喜欢现场编码)。

  • 抓住机会证明你的演讲技巧(这将为你打开参加技术会议的大门)。

  • 推广你的工作(添加链接和提示,以获取更多视频、源代码等)。

  • 回应人们的反馈/问题(不要忽视人们对你视频的评论)。

  • 接受批评并保持礼貌。

将你的 GitHub 和 Stack Overflow 账户链接到你的 YouTube 视频,以获得更多曝光和粉丝。

开始你的技术博客

你在 GitHub、Stack Overflow 和 YouTube 上的出色工作可以很容易地在技术博客的故事中进行推广。写有关编程主题的文章,特别是你解决的编程问题,并写教程、技巧等。持续发布高质量内容将增加你的流量,并将你的博客索引到搜索引擎上。有一天,这些有价值的内容可以被利用来写一本惊人的书,或者开发一部优秀的 Udemy(udemy.com)或 PluralSight(learn.pluralsight.com)视频。

有很多博客平台,比如 Blogger(blogger.com)、WordPress(wordpress.org)和 Medium(medium.com)。选择你喜欢的平台并开始。

写文章并吸引大量流量和/或获得报酬

如果你想发布技术文章并赚钱,或者吸引大量流量到你的作品,那么个人博客在一段时间内可能不会很有用(1-2 年)。但你可以为那些每天注册大量流量的网站写技术文章。例如,DZone(dzone.com)是一个很棒的技术平台,你可以免费写作,或者加入不同的计划,按照你的工作获得报酬。通过简单地创建一个免费的 DZone 账户,你可以立即开始通过他们的在线编辑器发布技术文章。1-5 天内,他们将审查你的作品并在网上发布。几乎立即,成千上万的人会阅读你的文章。除了 DZone,其他很棒的技术平台也会支付你为他们写作(通常每篇文章 10-150 美元,取决于长度、主题、内部政策等)。其中一些平台包括 InformIT(informit.com)、InfoQ(infoq.com)、Mkyong(mkyong.com)、developer.com(developer.com)、Java Code Geeks(javacodegeeks.com)、GeeksForGeeks(geeksforgeeks.org)和 SitePoint(sitepoint.com)。

推广自己和自己的作品(作品集)

工作很重要,但展示你所做的事情并获得他人的反馈也很重要。

重要提示

*管理你的在线资料非常重要。*招聘人员使用在线资料来寻找理想的候选人,更好地了解你,并准备深入或定制的面试问题。

除了 GitHub、Stack Overflow 等,招聘人员还会在 Google 上搜索你的名字,并查看你的个人网站和社交网络资料。

个人网站

个人网站(或作品集)是展示你工作的网站。只需添加你制作/贡献的应用程序的截图,并简要描述你的工作。解释你在每个项目中的角色,并提供项目的链接。注意不要暴露私人和专有公司信息。你可以从互联网上快速获得灵感(例如,codeburst.io/10-awesome-web-developer-portfolios-d266b32e6154)

在建立个人网站时,你可以依赖免费或低成本的网站构建工具,如 Google Sites(sites.google.com)和 Wix(wix.com)。

社交网络资料

最重要的社交网络之一是 Twitter。在 Twitter 上,你可以在全世界最优秀的 Java 开发者面前推广你的工作。从第一天开始,搜索并关注最优秀的 Java 开发者,很快他们也会关注你!作为一个提示,开始关注你能找到的尽可能多的 Java Champions(全球最优秀的 Java 开发者的独家社区)。Twitter 上有一个庞大而有价值的 Java 开发者社区。尽快认识他们!

其他社交网络,如 Facebook 和 Instagram,也会被招聘人员扫描。注意你的帖子内容。显然,激进主义、种族主义、狂热主义、琐碎或性内容、政治内容、口号和煽动暴力、诽谤和冒犯性内容等都会让招聘人员退后一步。

CodersRank 很重要

CodersRank(codersrank.io/)是一个收集关于你工作的信息的平台(例如,它从 GitHub、Stack Overflow、Bitbucket、HakerRank 等收集信息),并试图将你与全球数百万其他开发者进行排名。在下面的截图中,你可以看到一个开发者的个人资料页面:

图 1.8 - CodersRank 个人资料摘要

图 1.8 - CodersRank 个人资料摘要

这也是招聘人员的另一个重要指标。

学习,编码,学习,编码...

一旦你成为开发者,你必须遵循“学习->编码”实践,以便跻身顶尖并保持在那里。永远不要停止学习,永远不要停止编码!作为一个经验法则,“学习->编码”实践可以通过“以例学习”或“教学是我的学习方式”等方法来应用,或者任何其他适合你的方法。

认证怎么样?

一旦你访问 education.oracle.com/certification,你会发现 Oracle 提供了一套 Java 认证。虽然获得认证(来自 Oracle 或其他方)没有错,但在职位描述中并不要求。获得这些认证需要大量的金钱和时间,而且大多数时候并不值得。你可以更明智地利用这段时间参与项目(副业项目,学校项目,开源项目等)。这是给雇主留下更好印象的方法。因此,证书的价值有限,获得它们需要大量资源。此外,证书是有限期的。想想在 2020 年,成为 Java 6 认证的价值有多大,或者在 2030 年成为 Java 12 认证的价值有多大!

但是如果你真的想考虑认证,那么以下是提供的顶级认证(有关更多信息,请在 Google 上搜索,因为链接可能会随时间而中断):

  • OCAJP(Oracle 认证助理,Java 程序员 1)和 OCPJP(Oracle 认证专业人员,Java 程序员 2)

  • Spring 专业认证

  • OCEWCD(Oracle 认证专家,Java EE 6 Web 组件开发人员)

  • Apache Spark Cert HDPCD(HDP 认证开发人员)

  • 专业 Scrum 大师

  • 项目管理(PMP)

  • AWS 解决方案架构师

  • Oracle 认证大师

在互联网上拥有经验和知名度(粉丝)对你的职业生涯是一个巨大的加分。但是你仍然需要一份有用的简历来申请 Java 工作。所以,现在是写简历的时候了。

写简历的时间

写一份令人印象深刻的简历并不容易。有很多平台承诺如果你让他们为你做,你的简历会很棒。也有很多简历模板,大多数都相当复杂和繁琐。另一方面,简历是个人的东西,最好自己做。记住以下几点就足以为招聘人员制作一份吸引人的简历。让我们看看这些要点以及如何处理它们。

简历筛选者在寻找什么

首先,简历筛选者想要找出你是否是一个优秀的编码人员,是否聪明。其次,他们想要找出你是否适合某个可用职位(他们会检查你的经验是否符合该职位所需的特定技术和工具)。

努力突出你是一个优秀的编码人员,聪明。这意味着尽可能在一个集中的形式中尽可能技术化。注意:太多的字会稀释你简历的本质,导致失去焦点。要技术化,清晰,简洁

简历应该有多长

要回答简历应该有多长,你必须回答另一个问题:你认为招聘人员花多长时间阅读一份简历?很可能是 10-20 秒。换句话说,招聘人员在字里行间阅读,试图快速确定他们感兴趣的内容。

一般来说,简历不应超过一页。如果你有 10 年以上的经验,那么可以用 2 页。

你可能认为在 1-2 页内概括你的丰富经验是不可能的,但这并不是真的。首先,优先考虑内容,其次,添加这些内容直到覆盖 1-2 页。跳过剩余的内容。不要担心招聘人员不会知道你做过的一切!他们会对你的简历亮点印象深刻,并乐于在面试中发现你的其余经验。

写一份适合一页的简历。

如果你有 10 年以上的经验,那么考虑两页。请记住,一些招聘人员可能会在没有阅读一行的情况下跳过长篇简历。他们想要立即找到最令人印象深刻的项目。添加不太重要的项目和/或太多的字会分散招聘人员的注意力,让他们浪费时间。

如何列出你的工作经历

如果你的就业历史很短(2-4 个角色),那么把所有的都加到简历中。如果你有很长的角色列表(4 个以上的角色),那就不要列出你的完整就业历史。只选择 4 个最令人印象深刻的角色(在重要公司的角色、领导角色、取得了巨大成就和/或做出了重大贡献的角色)。

对于每个角色,遵循成就->行动->效果模型。始终从成就开始!这将成为招聘人员的磁铁。一旦他们读到成就,你就吸引了他们继续阅读。

例如,假设你在公司Foo工作,通过调整参数,你成功将连接池的性能提高了 30%。现在应用程序可以额外容纳 15%的交易吞吐量。将这一成就以单一陈述方式添加到简历中:

通过调整其参数,将连接池的性能提高了 30%,从而提高了 15%的交易吞吐量。

通过成就->行动->效果陈述列出最相关的角色。始终尝试衡量你创造的收益。不要说,“通过压缩...,我减少了内存占用”,而是说,“通过压缩...,我减少了内存占用 5%”。

列出最相关的项目(前五个)

一些招聘人员更喜欢直接跳到你的简历的我的项目部分。他们遵循不要废话,只谈实质的原则。你不必列出所有的项目!列出前五个并只添加那些。不要从同一类别中添加所有五个。选择一个或两个独立项目,一个或两个开源贡献等。一个拥有高 GitHub 星级评分的独立项目才是真正会给招聘人员留下深刻印象的。

列出顶级项目及其相关细节。这是失去谦逊、尽力给人留下深刻印象的正确地方。

提名你的技术技能

技术技能部分是必须的。在这里,你必须列出你所了解的编程语言、软件和工具。它不必像命名那样冗长,但也不必是一个简短的部分。它必须与列出的项目相关并协调一致。以下列表明了编写技术技能部分时要遵循的主要标准:

  • 不要列出所有的 Java 变种:不要添加 Spring MVC、Spring Data、Spring Data REST、Spring Security 等列表。只说 Spring。或者,如果你是 Java EE 的人,不要列出 JPA、EJB、JSF、JAX-RX、JSON-B、JSON-P、JASPIC 等列表。只说 Java EE、Jakarta EE。或者,如果在职位描述中以这种方式列出它们,那么你可以在括号中添加它们。例如:“Spring(MVC、Data 包括 Data REST、Security)”或“Java EE(JPA、EJB、JSF、JAX-RX、JSON-B、JSON-B、JASPIC)”。

  • 不要添加软件版本:避免添加 Java 8、Spring Boot 2 或 Hibernate 5 等内容。如果这些细节是必要的,面试官会问你。

  • 不要列出实用技术:避免列出项目中常用的实用库。例如,不要添加 Apache Commons、Google Guava、Eclipse Collections 等。可能招聘人员没有听说过它们。或者,如果他们听说过,他们会带着讽刺的微笑。

  • 不要列出你只是轻微接触过的技术:列出你只是偶尔和/或肤浅地使用过的技术是相当冒险的。在面试中,你可能会被问到这些技术的问题,这会让你陷入困境。

  • 对于每种技术,添加你的经验:例如,写上Java(专家)、Spring Boot(高级)、Jakarta EE(熟练)、Hibernate(专家)

  • 不要用技术的使用年限来衡量你的经验:大多数时候,这并不相关。这个度量标准对招聘人员来说并没有太多意义。你的经验是通过你的项目展现出来的。

  • 避免常见技术:不要列出操作系统、Microsoft Office、Gmail、Slack 等。列出这些东西只会给招聘者带来干扰。

  • 仔细检查您的英语:如果简历有拼写错误,招聘者可能会将其丢弃。如果您不是以英语为母语的人,那么请找一个以英语为母语的人来校对您的简历。

  • 不要列出单一的编程语言:理想情况下,您应该列出两到三种编程语言(例如,Java(专家)、C++(中级)、Python(有经验)),但不要说您在所有这些语言中都是专家。没有人会相信你!另一方面,单一的编程语言可能被解释为您不愿意学习新技术。

  • 将技术分成类别:不要将技术列为一个长长的、逗号分隔的列表。例如,避免类似于Java、Ruby、C++、Java EE、Spring Boot、Hibernate、JMeter、JUnit、MySQL、PostgreSQL、AWS、Ocean 和 Vue.js这样的列表。将它们分成类别,并按经验排序,如下例所示:

a. 编程语言:Java(专家)、Ruby(中级)和 C++(初学者)

b. 框架:Java EE(专家)、Spring Boot(高级)

c. 对象关系映射ORM):Hibernate(专家)

d. 测试:JMeter(专家)、JUnit(高级)

e. 数据库:MySQL(专家)、PostgreSQL(中级)

f. :AWS(专家)、Ocean(初学者)

g. JavaScript 框架:Vue.js(中级)

LinkedIn 简历

很可能,您的 LinkedIn 个人资料将是招聘者的第一站。此外,大量的电子工作平台在您尝试申请工作时都要求您的 LinkedIn 账户。甚至有些情况下,这个账户是强制性的。

LinkedIn 是一个专门用于跟踪专业联系的社交网络。本质上,LinkedIn 是一个在线简历的增强版。在 LinkedIn 上,您可以创建工作提醒,同事、客户和朋友可以为您或您的工作背书,这可能非常有价值。

重要提示

注意保持您的 LinkedIn 简历与纸质简历同步。此外,如果您通过 LinkedIn 寻找工作,请注意,所有您的联系人都会收到关于您更新的通知。这些联系人包括您当前公司的人,而且很可能您不希望他们知道您在找新工作。解决方案是在更新之前禁用这些通知。

现在,我们可以讨论求职流程了。

求职流程

技术公司更喜欢多阶段面试。但是,在被邀请参加面试之前,您必须找到正在招聘的公司,申请他们的工作,然后最终与他们见面。

寻找正在招聘的公司

过去几年(2017 年以后)的调查估计,70%-85%的工作都是通过人际网络填补的(linkedin.com/pulse/new-survey-reveals-85-all-jobs-filled-via-networking-lou-adler/)。技术工作(尤其是 IT 领域)代表了利用人际网络的主要领域。

在几乎任何国家,都有几个电子工作平台。我们称它们为本地电子工作平台。通常,本地电子工作平台列出了在该国活跃的公司或全球招聘的公司的工作机会。

全球范围内,我们有全球性的电子工作平台。这些平台包括一些主要的参与者(所有这些网站都允许您上传简历或在线创建简历):

  • LinkedIn(linkedin.com):拥有超过 6.1 亿用户,覆盖全球 200 多个国家,这是全球最大的专业社交网络和社交招聘平台。

  • Indeed(indeed.com):这是一个领先的职位网站,收集了来自数千个网站的数百万个工作机会。

  • CareerBuilder(careerbuilder.com):这是另一个发布来自全球各地的大量工作机会的巨大平台。

  • Stack Overflow(stackoverflow.com/jobs):这是开发人员学习、分享编程知识和发展职业的最大、最值得信赖的在线社区。

  • FlexJobs(flexjobs.com)和Upwork(upwork.com):这些是专门为自由职业者提供高级、灵活的远程工作的平台。

提供寻找工作有用的服务的其他平台包括以下内容:

  • Dice(dice.com):这是每个阶段的技术专家的领先职业目的地。

  • Glassdoor(glassdoor.com):这是一个包括公司特定评级和评论的复杂平台。

除了这些平台,还有许多其他平台,你可以自己发现。

提交简历

一旦你找到想申请的公司,就是提交你的简历的时候了。

首先,看看公司的网站。这可以帮助你找到以下内容:

  • 看看是否可以直接通过公司网站申请(通过绕过就业机构,你可以加快流程,公司可以直接雇佣你,而不必向就业机构支付佣金)。

  • 你可以在公司数据库中注册,以便在合适的职位开放时联系你。

  • 你有机会了解公司的历史、愿景、项目、文化等等。

  • 你可以找到公司相关人员的联系方式(例如,你可以找到详细和支持的电话号码)。

第二,仔细检查你的简历和在线资料。很可能,如果你的简历给招聘人员留下了深刻印象,他们会在谷歌上搜索你的名字,并检查你的社交网络活动。从技术内容到社交媒体,一切都将在发送面试邀请之前进行扫描。

第三,不要向所有公司发送完全相同的简历!对于每家公司,都要对简历进行调整,使其尽可能与职位描述相关。

我得到了面试!现在怎么办?

如果你迄今为止都按照路线图进行,那么只是几天的事情,你就会收到一封电子邮件或电话邀请你参加面试。哦,等等...你是说你已经有了面试?太棒了!是时候准备好自己了!

电话筛选阶段

大多数 IT 公司更喜欢从电话筛选开始多步面试流程。电话筛选通常是通过 Skype、Zoom 或 Meetup(或类似平台)完成的,你需要分享你的网络摄像头。还需要麦克风和一副耳机。如果你选择远程职位,电话筛选非常受欢迎,但最近,它们被用于各种职位。

通常,公司使用两种方法:

  • 与人力资源或就业机构人员进行电话筛选:这是一个可选的、非技术性的 15-30 分钟面试,旨在详细说明提供条款,展示你的个性、关注点,你和他们的期望等等。这可能在技术电话筛选之前或之后进行。

  • 首先是技术电话筛选:有些公司会直接邀请你参加技术电话筛选。在这种情况下,你可以期待几个技术问题,也许是一场测验,以及一个或多个编码挑战环节(解决编码挑战是本书的主要重点)。如果你通过了技术电话筛选,那么很可能会有一个非技术的电话筛选。

参加面试

除非你选择远程职位,下一步将是面对面的面试。有些情况下,没有电话筛选,这是面试的第一步。在这种情况下,你可能会先接受人力资源部门的面试,然后是技术面试。但是,如果你经历了电话筛选,那么可能会联系你,也可能不会。这取决于公司如何评估电话筛选。如果他们决定不继续进行下一阶段的面试,那么你可能会收到一些反馈,涵盖了你电话筛选表现的优点和不足之处。不要忽视反馈,仔细阅读并客观地看待。这可能会帮助你避免重复同样的错误。说到错误…

避免常见的错误

注意以下可能导致面试失败的常见错误:

  • 忽视信息的力量: 有时面试失败后,我们会找朋友谈谈面试情况。这时,你的朋友可能会说:“我的朋友,我认识一个人两个月前在这家公司成功面试过!为什么你之前不告诉我?我肯定他可以给你一些建议!”显然,现在已经太迟了!避免这种情况,尽量获取尽可能多的信息。**看看你或你的朋友在公司是否有联系,在社交媒体上问问等。**这样可以获取非常有用的信息。

  • 回答缺乏清晰和连贯性: 你的回答应该是技术性的、清晰的、有意义的、表达力强的,并且始终与话题相关。认真回答问题。口吃、回答不完整、插话等都不受面试官欢迎。

  • 认为形象不重要: 不要忽视你的形象!着装要专业,去理发店,保持干净整洁!所有这些都是第一印象的一部分。**如果你看起来邋遢,也许你的代码也是如此。如果你穿着得体,那么面试官会把你视为高人一等。**然而,着装得体并不意味着你应该奢华。

  • 没有充分展示自己: 面试官必须看到你的价值。没有人比你更能向他们传达你的价值。告诉他们你曾经遇到的问题(在以前的公司,某个项目中等),并解释你是如何与团队或独立解决的。雇主希望找到既是优秀团队合作者又能独立工作的人。采用*SAR(Situation|Action|Result)*方法。首先描述情况,然后解释你采取的行动,最后描述结果。

  • 不练习编码挑战: 在某个时候,你将被安排至少一次编码挑战。大多数情况下,一般的编码技能是不够的!这些挑战是特定于面试的,你必须在面试前练习。一般来说,解决编码挑战(问题)遵循方法->分解->构建的解决模式。显然,你不能记住解决方案,因此你需要尽可能多地练习。在本书的后面,我们将讨论解决编码挑战的最佳方法。

面试结束后,就是等待回复的时候了。大多数公司会告诉你他们需要多少时间来提供最终答复,并通常会提供一个代表着录取、拒绝、下一轮面试或者申请状态的答复。祝你好运!

摘要

本章总结了在 Java 生态系统中获得工作应遵循的最佳实践。我们谈到了选择适当的工作和我们的资格,获取经验,制作简历等等。大部分建议都是针对学生或刚刚毕业的人。当然,不要把这些建议看作是一个详尽的清单或者应该完全应用的清单。这些实践将帮助你收获你认为有吸引力的果实,并允许你在过程中加入自己的触摸。

接下来,让我们看看大公司是如何进行面试的。

第二章:大公司的面试是什么样子的

大公司的面试过程通常比较长,技术问题和编码挑战的复杂性逐渐增加(这样的面试过程可能需要一个月甚至更长时间)。大多数公司在提供职位之前更倾向于进行一次或多次技术电话筛选、现场技术挑战和面对面面试。通常,其中一次面试将是非技术性的(被称为“午餐面试”)。

让我们来了解一下几家领先的 IT 公司是如何进行面试的。一般来说,所有这些公司都在寻找聪明、热情和优秀的程序员。

我们将讨论以下公司的面试是如何进行的:

  • 谷歌

  • 亚马逊

  • 微软

  • Facebook

  • Crossover

让我们开始吧!

谷歌的面试

谷歌的面试从技术电话筛选开始(技术问题和编码挑战)。这些技术电话筛选将涉及 4-5 个人。其中一个电话筛选将是非技术性的。在这个时候,你可以自由地问任何你想问的问题。

在这些面试阶段,你将根据你的分析能力、编码能力、经验和沟通技巧得分。

面试官将他们的反馈提交给招聘委员会HC)。HC 负责提供职位或拒绝你。如果 HC 认为你是合适的人选,那么他们会将提供提案转发给其他委员会。最终决定由执行管理委员会做出。

主要的技术重点是分析算法、脑力算法、系统设计和可扩展性。

很可能你需要等待几周才能得到回复。

建议在 YouTube 上搜索“在谷歌面试”并观看最相关的证言和路线图视频。还要搜索“谷歌最常问的面试问题”。

亚马逊的面试

亚马逊的面试从一个由亚马逊团队进行的技术电话筛选开始。如果一些面试官在电话筛选后仍然不满意,那么他们可能会要求进行另一轮以澄清问题。

如果你通过了技术电话筛选,那么你将被邀请参加几次面对面的面试。来自业务不同领域的面试官团队将分别进行面试并评估你的技术能力(包括编码)。其中一个人也被称为“提高标准者”。通常情况下,这个人经验最丰富,他的问题和编码挑战会更难。他们还会将你与其他候选人进行比较,并决定是否提供职位。

主要关注面向对象编程OOP)和可扩展性。

如果一周后没有收到任何反馈,那么你应该给亚马逊的联系人发送一封友好的跟进电子邮件。很可能他们会很快回复你的邮件,并解释你的面试当前状态。

建议在 YouTube 上搜索“在亚马逊面试”并观看最相关的证言和路线图视频。还要搜索“亚马逊最常问的面试问题”。

微软的面试

微软的面试从几轮技术电话筛选开始,或者他们可能要求你前往他们的工作分部之一。你将与不同团队进行 4-5 轮技术面试。

最终决定属于招聘经理。通常情况下,只有在你通过了所有技术面试阶段后,才会联系这位招聘经理。

主要关注算法和数据结构。

如果一周后没有收到任何反馈,那么你应该给微软的联系人发送一封友好的跟进电子邮件。有时,他们可能在一天内就做出决定,但也可能需要一周、一个月甚至更长时间。

建议在 YouTube 上搜索“在微软面试”并观看最相关的证言和路线图视频。还要搜索“微软最常问的面试问题”。

Facebook 的面试

Facebook 的面试从几轮技术和非技术电话筛选开始,涉及问题(技术和非技术)和编码挑战。通常,面试由一组软件工程师和招聘经理进行。

Facebook 使用三种类型的面试,涵盖以下领域:

  • 你适应 Facebook 文化的能力,以及一些技术技能 - 被称为行为绝地面试

  • 你的编码和算法技能(这些是我们稍后会涵盖的常见问题,从第六章开始,面向对象编程)- 被称为忍者面试

  • 你的设计和架构技能 - 被称为海盗面试

你可以期待这些类型的面试的组合。通常,一个绝地和两个忍者就足够了。对于需要更高经验的职位,还会有海盗面试。

如果你通过了这些技术电话筛选,那么你将收到一些家庭作业,包括技术问题和编码挑战。这一次,你必须提供优雅而干净的编码解决方案。

主要关注你在任何语言中快速构建东西的能力。你可以期待在 PHP、Java、C++、Python、厄朗等语言中编码。

面试团队将决定是否雇佣你。

建议在 YouTube 上搜索Facebook 面试,观看最相关的证词和路线图视频。还要搜索Facebook 最常问的面试问题

Crossover 的面试

Crossover 是一家远程公司。他们通过他们的平台远程招聘,并且有独家的现场面试流程。他们的现场面试遵循以下路线图:

图 2.1 - 跨界面试路线图

图 2.1 - 跨界面试路线图

所有步骤都很重要,这意味着你在每一步的回答必须通过他们的内部规则。如果一步没有通过他们的内部规则,那么可能会导致面试突然关闭。但是,最重要的步骤是第 3、5、6 和 7 步。第 3 步代表淘汰性的标准认知能力测试CCAT)。例如,你必须在 15 分钟内回答 50 个问题。你必须正确回答 25 个以上的问题才有机会进入下一步。如果你不熟悉 CCAT 测试,那么强烈建议练习(有专门的书籍和网站致力于 CCAT 测试)。没有认真的练习,要通过它将会相当具有挑战性。如果你不是母语为英语的人,那么你必须特别注意练习需要严肃英语技能的问题。

在第 5 步,你将接受一个技术问题的测验。有 30 个以上的问题,有 5 个答案变体(一个或多个答案是正确的)。在这一步不需要编码。

如果你达到第 6 步,那么你将收到需要在 3 小时内完成并提交(上传)到平台的技术家庭作业。这个家庭作业可以由一个或多个从提供的存根应用程序开始的 Java 应用程序组成。

在第 7 步,你最终会通过电话与一个人见面。这通常是技术和非技术问题的混合。

技术问题将涵盖各种 Java 主题(集合、并发、I/O、异常等)。

通常,你会在一周内通过电子邮件收到最终答复。根据职位不同,提供将以 1 个月的有薪实习班经验开始。请注意,在实习班结束后,你仍然可能被拒绝或需要重新申请。在实习班期间和之后,你必须通过每周衡量你的表现的指标来保持你的职位。你必须每周工作 40 小时,每 10 分钟进行一次网络摄像头截图。而且,你有责任安排支付自己的税款。薪水是固定的,并且在他们的网站上公开。

建议仔细阅读他们网站上的职位描述和推荐信。他们还有品牌大使,您可以联系他们了解公司文化、期望、面试流程等信息。

其他远程公司遵循三步面试流程。例如,Upstack 遵循以下模式:

  1. 初试面试:非技术电话筛选

  2. 技术面试:包含编程挑战的技术电话筛选

  3. 提供:发送给您一个聘用意向并签署协议

当然,这里没有列出许多其他大公司。但作为一个经验法则,这里概述的公司和他们的流程应该给您一些重要的见解,让您了解您应该从 IT 行业的大公司中期望什么。

总结

在本章中,我们概述了几家领先的 IT 公司如何进行面试。大多数 IT 公司都遵循本章介绍的相同做法,但有着不同的组合和特色。

接下来,让我们看看最常见的非技术问题是什么,以及如何回答它们。

第三章:常见的非技术问题及如何回答

在这一章中,我们将解决非技术面试问题的主要方面。面试的这部分通常由招聘经理或甚至是人力资源部门负责。为了准备这次面试,意味着熟悉以下问题:

  • 非技术问题的目的是什么?

  • 你的经验是什么?

  • 你最喜欢的编程语言是什么?

  • 你想做什么?

  • 你的职业目标是什么?

  • 你的工作风格是什么?

  • 你为什么想要换工作?

  • 你的薪资历史是什么?

  • 为什么我们应该雇佣你?

  • 你想要赚多少钱?

  • 你有问题要问我吗?

我们将在各自的具体部分讨论每个问题。让我们开始吧。

非技术问题的目的是什么?

非技术面试问题的目的是衡量你的经验、性格和个性与其他员工和团队的匹配程度,以及你是否能够与其他员工和团队融洽相处。成为现有团队的一员是必须的。这些问题也有助于在你和公司之间建立人际关系,并看看他们理想的候选人与你的教育、信仰、想法、期望、文化等是否有任何兼容性或化学反应。此外,非技术问题也涵盖了工作的实际和务实方面,如薪资、搬迁、医疗保险、工作时间安排、是否愿意加班等等。

有些公司会根据这个非技术面试拒绝候选人,即使他们最初打算提供工作机会。

有些公司在技术面试之前进行这个面试。这些公司试图从一开始就确定你的经验和目标是否使你成为该职位的合适候选人。这就像说人际部分比技术部分更重要。

其他公司会在技术面试之后进行这个面试。这些公司试图确定对你来说什么是最好的工作机会。这就像说技术部分比人际部分更重要。

非技术问题没有对或错的答案!在这些情况下,最好的答案是真诚的答案。作为一个经验法则,回答时要表达真实的感受;不要试图说面试官想听到的话。这就像一场谈判 - 会有取舍。不要忘记要有礼貌和尊重。

接下来,让我们看看最常见的非技术问题以及一些答案建议。不要学习/抄袭这些答案!试着想出你自己的答案,并专注于你想要突出的内容。在家里塑造和重复答案,并在面试官面前做好准备。不要依赖你的自发性;依赖真诚并平衡取舍。

你的经验是什么?

很可能,在正式介绍之后,你会被问及你的经验。如果你对这个问题没有准备好答案,那么你就麻烦了。让我们强调几个重要方面,帮助你准备一个合适的答案:

  • 不要把你的经验详细描述成无聊的时间线清单:选择最具代表性的项目和成就,并充满热情地谈论它们。充满热情地谈论你的工作(但不要显得绝望,也不要夸大),并将你的成就放在团队/项目的背景下。例如,避免说... 我独自完成了这个和那个! 最好说,...我通过做这个和那个来帮助我的团队。 不要说,...我是唯一一个能够做到那个的人。 更好的说法是*...我被团队提名来完成这个棘手的任务*。如果你是第一份工作,那么谈谈你的学校项目(把你的同事看作你的团队)和你的独立项目。如果你参加过编程比赛,那么谈谈你的成绩和经验。

  • 不要只强调积极的事情:经历可能是积极的也可能是消极的。谈谈发生了什么是对的,但也要谈谈发生了什么是错的。大多数时候,真正有价值的教训来自于消极的经历。这些经历迫使我们超越自己的极限去寻找解决方案。此外,这样的经历证明了对压力的抵抗力,坚韧不拔和专注力。当然,要平衡积极和消极的经历,并强调你从双方学到了什么。

  • 不要提供太短或太长的答案:调整你的回答,使其在 1-2 分钟内完成。

你最喜欢的编程语言是什么?

既然我们在谈论 Java 职位,显然你最喜欢的语言是 Java。但如果出现这样的问题,那么它的目的是要揭示你是 Java 迷还是一个开放思想的人。换句话说,面试官认为与固执的、沉迷于一种编程语言并希望在所有情况下都专门使用它的人一起工作是很困难的。成为一名 Java 开发人员并不意味着你应该考虑 Java 来解决你所有的任务,并忽略其他一切。因此,一个好的答案可能是,“显然,我是 Java 的忠实粉丝,但我也认为选择最适合工作的工具很重要。认为 Java 是所有问题的答案是荒谬的。”

你想做什么?

这是一个难题,你的答案可能有很多解释。要真诚地告诉面试官你想做什么。你已经阅读了工作描述,因此你知道你想要这份工作。向面试官解释你决定的主要原因。例如,你可以说,“我想成为一名优秀的 Java 后端开发人员,而你们的项目在这个领域非常具有挑战性。我想成为参与这些项目的团队的一部分。”或者,你可以说,“我想成为一家重要公司的一家重要初创公司的一部分,这对我来说是一个很好的机会。我听说正在组建一个新团队,我会非常兴奋成为其中的一部分。”不要忽略说一些关于在一个伟大团队工作的事情!很可能你不会独自工作,成为一个团队合作者是几乎在任何公司工作中的一个重要方面。

你的职业目标是什么?

通过这个问题(或它的姊妹问题,“你在五年内看到自己在哪里?”),面试官试图了解这个职位是否符合你的职业目标。他们试图了解你是否把这个职位看作你职业道路的一部分,或者你是否有其他原因(除了金钱)来做这件事。描述一个详细的职业道路很难,但你可以给出一个显示你承诺和动力去做好工作的答案。例如,你可以说,“我目前的目标是在具有挑战性的项目上担任 Java 后端开发人员,这将帮助我积累更多的经验。几年后,我希望自己参与设计复杂的 Java 应用程序。再往后的事情现在想太远了。”

你的工作风格是什么?

这种问题应该让你警惕。大多数时候,这个问题是特定于那些有不寻常工作风格的公司。例如,他们经常加班或周末工作。也许他们工作时间很长,或者他们有难以实现的指标或截止日期。或者,他们在这个职位上施加了很大的压力和责任。向面试官解释你的工作风格,并间接地强调你不同意的事情。例如,你可以指出你不愿意做夜班,说,“我喜欢在早上开始工作,处理最困难的任务,而在一天的后半部分,我会处理下一天的计划。”或者,你可以指出你不愿意周末工作,说,“我喜欢每周努力工作 40 小时,从周一到周五。我喜欢和朋友们度过周末。”

如果你被直接问及特定方面,那么提供一个明确的答案。例如,面试官可能会说:“你知道,如果你周末工作,你会得到双倍的报酬。你对此有什么看法?”好吧,三思而后行,根据你的感觉回答,但不要给解释留下空间。

你为什么想换工作?

当然,如果你是第一份工作,那么你不会被问到这样的问题(或者它的姊妹问题,“你为什么离开上一份工作?”)。但如果你之前有过工作(或者你计划改变你目前的工作),面试官会想知道你为什么做出这个决定。关键在于详细说明清晰而有力的论点,而不要说任何有关你之前的公司、老板、同事等不好或冒犯的话——遵循“如果你不能说出任何好话,就不要说任何话”的原则。

以下是一些可以帮助你回答这个问题的建议(注意这个问题如何与前一个问题交织在一起——如果这家公司的工作方式与你目前或之前的公司的风格相关,那么离开那份工作的原因很可能也适用于避免这份工作):

  • 不要把钱作为第一个论点:钱通常是换工作的一个很好的理由,但把它作为第一个论点是一条危险的路。面试官可能会认为你只关心钱。或者,他们可能会认为你现在的雇主没有给你加薪是因为你不够有价值。迟早,他们可能会认为,你会想要更多的钱,如果他们不能给你想要的加薪,你会寻找其他地方。

  • 引发你无法控制的因素:引发你无法控制的因素会让你处于安全区域。例如,你可以说:“我的团队被分配到一个需要搬迁的项目。”或者,你可以说:“我被调到了夜班,我无法适应这个时间表。”

  • 引发环境的重大变化:例如,你可以说:“我的公司大规模裁员,我不想冒这个风险。”或者,你可以说:“我在一家小公司工作了 5 年,现在我想把我的经验用在一家大公司。”

  • 引发你不喜欢的并且面试官知道的方面:你可以说:“我被聘为 Java 后端程序员,但我花了很多时间帮助前端的人。正如你在我的简历中看到的,我的经验根植于后端技术。”

你的薪资历史是什么?

显然,这个问题旨在确定新报价的基准。如果你对目前的薪水满意,那么你可以给出一个数字。否则,最好礼貌地说“我不想搞砸事情,我期待的补偿应该适合新职位及其要求。”

为什么我们应该雇佣你?

这是一个相关的并且稍微冒犯的问题。在大多数情况下,这是一个陷阱问题,旨在揭示你对批评的反应。如果这个问题出现在面试的开始,那么你应该把它看作是一个问题的误导性表述,“你的经验是什么?”

如果这个问题出现在面试的最后,那么很明显面试官非常清楚为什么公司应该雇佣你,因此,他不希望听到基于你的简历或经验的强有力的论点。在这种情况下,保持冷静和积极,并提到你为什么喜欢这家公司,为什么想在这家公司工作,以及你对它的了解。表现出你的兴趣(例如,表明你已经研究过这家公司并访问过他们的网站)应该会让面试官感到受宠若惊,然后他可以迅速转到下一个问题。

你想赚多少钱?

这个问题出现在面试的一开始(例如,在非技术电话面试)或者在结束时,当公司准备给你准备一个报价。如果出现在开始时,这意味着面试是否继续将取决于你的回答。如果你的期望超出了可能的报价,那么面试很可能会在这里结束。最好尽可能推迟明确的回答,比如说,我脑海中没有一个明确的数字。当然,钱很重要,但还有其他重要的事情。首先让我们看看我的价值是否符合你的期望,然后我们可以谈判。 或者,如果你必须给出一个答案,最好给出一个薪水范围。你应该知道这个职位的普遍薪水范围(因为你在面试前已经做了功课,在网上做了调查),因此,提供一个符合你期望并尊重你的研究的范围。

理想情况下,这个问题出现在面试过程的最后阶段。这清楚地表明公司想要你,并准备给你一个报价。

现在,你开始了谈判的艺术!

不要急着说数字!此时,你应该相当清楚自己在面试中的表现以及自己有多想要这份工作。首先问面试官报价范围,其他奖金有哪些,总薪酬包括哪些。你还需要考虑几种可能的情况:

  • 在一个非常愉快的情况下,报价会高于你的期望:接受它!

  • 更有可能的是,这个报价接近你的期望值:试着再挤一点。例如,如果你得到的范围在60,00060,000 - 65,000 之间,那么可以说一些类似的话,我心里想的差不多是这个 - 更确切地说,如果我们能达成65,00065,000 - 70,000,我会非常满意。 这可能会帮助你得到大约63,00063,000 - 68,000。

  • 得到含糊的答复:与其得到一个报价范围,你可能会得到一个含糊的答复,比如说,我们根据申请者定制薪水,因此,我需要知道你的期望。 在这种情况下,说出你心中的更高数字。很可能你不会得到这个报价,但这给了你谈判的空间。要简短直接;例如,说,我期望年薪 65,000 美元。 你可能会得到大约 60,000 美元的报价,或者一个让你失望的答复,比如说,抱歉,但我们心目中的数字要低得多。 这将导致下一节。

  • 得到令人失望的报价:在这种情况下,要迅速表达你的失望,比如说,我不得不说我对这个报价非常失望。 然后重申你的强大技能和经验。试着提出明确的支持所要求数字的论点,并强调你不想要什么离谱的东西。如果你不愿意接受这份工作的这些条件,那么在回答结束时加上一个最后通牒,比如说,如果这是你们的最终答复,我无法接受这样的报价。 如果公司对你印象深刻,他们可能需要更多时间,然后会给你另一个报价。如果你考虑接受这个报价,那么要求书面协议,在六个月后重新谈判。此外,试着从谈判中挤出其他好处,比如灵活的工作时间、奖金等等。

重要提示

作为一个经验法则,要记住以下几个方面:- 谈论薪水时不要害羞或尴尬(新手们经常会这样)。- 不要从不给你谈判空间的低数字开始。- 不要低估自己,卖自己短。- 不要浪费时间谈判不可谈判的事情。

你有问题要问我吗?

几乎任何面试都以这个问题结束。面试官想要澄清你可能有的任何疑虑。你可以问任何你想问的问题,但要注意不要问一些愚蠢的问题或需要长篇回答的问题。你可以询问面试官说过但不太清楚的事情的细节,或者你可以询问他们对你的个人看法。或者,你可以问一些类似“你是怎么来到这家公司的?对你来说最具挑战性的是什么?”的问题。如果你没有问题要问,那就不要问。简单地说一些像“嗯,我必须说你已经回答了我所有重要的问题。谢谢你的时间!”的话。

总结

在本章中,我们涵盖了面试中你可能面对的最常见的非技术问题。这些问题在面试前应该认真训练,因为它们是成功面试的重要组成部分。的确,对这些问题的出色回答单独并不能带给你一个 offer,没有必要的技术知识的充分展示,但它们可以影响你的薪水待遇、日常工作期望、工作风格和职业目标。因此,不要在这样的面试中毫无准备。

在下一章中,我们将看到如何面对当我们无法获得理想工作时的微妙情况。

第四章:如何处理失败

本章讨论了面试中一个微妙的方面——处理失败。本章的主要目的是向你展示如何识别失败的原因,并如何在将来减轻它们。

然而,在讨论如何处理失败之前,让我们快速解决接受或拒绝提议的正确方式。在面试结束时,或者在面试过程中的某个时候,你可能会发现自己处于接受或拒绝提议的位置。这不是简单地给出一个干脆的是或否的答案。

本章的议程包括以下内容:

  • 接受或拒绝一个提议

  • 考虑到失败是一个选择

  • 理解一个公司可能因为很多原因拒绝你

  • 客观地识别和消除不匹配

  • 不要对一个公司形成固执的迷恋

让我们开始第一个话题。

接受或拒绝一个提议

接受一个提议是相当简单的。你需要通知公司你接受了这个提议,并讨论细节,比如开始日期(特别是如果你需要在目前的工作场所工作一个通知期),文件工作,重新分配(如果有的话),等等。

拒绝一个提议是一个更微妙的情况。必须以一种让你能够与每个人保持良好关系的方式来做。公司在面试中投入了时间和资源,所以你必须礼貌地拒绝他们的提议。过一段时间后,你也可以考虑再次申请该公司。例如,你可以说类似于“我想感谢你的提议。我对你的公司印象深刻,我很喜欢面试的过程,但我决定现在不是适合我的选择。再次感谢你,也许有一天我们会再见面。”

有些情况下,你需要处理多个提议。当你接受一个提议时,你必须拒绝另一个。在 IT 行业,建立联系并随时保持联系非常重要。人们经常换工作和职位,在这个动态的环境中,不要浪费任何联系是很重要的。因此,不要忘记打电话给招聘经理(或联系人)告诉他们你的决定。你可以使用之前给出的同样的短语。如果你不能打电话,那就发一封电子邮件或亲自去办公室见他们。

失败是一个选择

在电影中,我们经常听到“失败不是一个选择”的说法。但那只是电影!面试总是以一个提议或拒绝结束,所以失败是一个选择。我们的任务是减轻失败。

处理失败并不容易,特别是当它们一个接一个地出现时。我们每个人对失败的反应都是不同的,也是人性的。从感到失望和顺从到紧张反应或说出后悔的话,这些都是正常的人类反应。然而,重要的是要控制这些反应并以专业的方式行事。这意味着应用一系列步骤来减轻将来的失败。首先,重要的是要理解为什么你被拒绝了。

一个公司可能因为很多原因拒绝你

嗯,也许问题正是从这个强大的词开始:拒绝。说或者想公司 X 拒绝了你是正确的吗?我会说这种表述是有毒的,听起来就像公司对你有什么私人恩怨一样。这种思维方式应该从一开始就被切断。相反,你应该试图找出问题出在哪里。

怎么样说或者想到你和公司之间的技能和/或期望不匹配?很可能,这更接近现实。面试中有两个参与方(你和面试官),双方都试图找到允许他们以主观的方式合作的匹配或兼容性。一旦你这样想,你就不会责怪自己,而会试图找出问题出在哪里。

面试后获得反馈

如果公司通知你没有被录用,那么现在是时候给他们打电话并要求他们的反馈了。你可以说类似于“谢谢你给我面试的机会。我正在努力提高我的面试技能,所以如果你能提供任何对我有用的反馈,那将是太棒了。”

获得适当的反馈非常重要。它代表了修复和消除不匹配的起点,因此你可以开始减轻失败。不匹配通常如下:

  • 表现: 候选人在面试过程中未达到或保持预期的表现。

  • 期望: 候选人不符合面试官的期望(例如,他们的薪资期望超出了公司的期望)。

  • 缺乏技能/经验: 候选人不符合工作的技能水平(例如,缺乏经验)。

  • 沟通: 候选人具有技术技能,但未能正确表达。

  • 面试官的偏见: 候选人的行为不适合该工作/公司。

现在让我们来看看如何识别和消除不匹配之处。

客观地识别和消除不匹配之处

虽然反馈代表了修复和消除不匹配的起点,但你必须意识到它可能相当主观。重要的是仔细阅读反馈,并在回忆面试的阶段时,以客观的方式将他们的反馈与你的记忆重叠。

一旦你确定了客观的不匹配之处,就是时候消除它们了。

不要对一家公司形成固执的迷恋

有些人很难被某家公司录用。即使经过两三次尝试,他们也不会停止。继续尝试是毅力还是固执?他们的梦想工作已经变成了固执,还是他们应该继续尝试?这些都是非常个人的问题,但作为一个经验法则,固执总是有害的,不会带来任何好处。如果你发现自己处于这种情况,或者你认识有人处于这种情况,那么现在是时候改变你的态度,认为也许以下是正确的思考方式。

不要失去对自己的信心 - 有时,他们不配拥有你!

这个标题听起来像是一句鼓励的空洞口号,旨在让你感觉更好。然而,这并不是真的!这种情况经常发生,在许多情境中都有。例如,一位刚开始职业生涯的歌手参加了一档著名的歌唱比赛,并没有赢得任何奖项;她甚至没有被认为是优秀的人之一。她没有再次参加比赛(就像章节标题中的情况),但几年后,她赢得了她的第一个格莱美奖。

现实生活中有很多这样的例子。这位歌手没有失去她的技能自信,她是对的!那个著名的歌唱比赛并不配拥有她。多年后,比赛的组织者邀请这位歌手再次演唱(这次作为嘉宾),并为之前发生的事情道歉。

所以,不要失去对自己的信心 - 有时,他们不配拥有你!

总结

本章简要概述了我们在求职过程中必须明智处理的一个重要方面 - 失败。它们是生活的一部分,我们必须知道如何以健康和专业的方式处理它们。不要过于情绪化,尝试以专业、冷静、现实和客观的方式对待每次失败。

在下一章中,我们将介绍技术面试的高潮:编程挑战。

第五章:如何应对编码挑战

本章涵盖了技术测验和编码挑战,这在技术面试中常见。

编码挑战是面试中最重要的部分。这部分可以由单个会话或多个会话组成。一些公司更喜欢将技术面试分为两部分:第一部分包括技术测验,而第二部分包括一个或多个编码挑战。在本章中,我们将详细讨论这两个主题:

  • 技术测验

  • 编码挑战

通过本章结束时,你应该能够规划自己的技术面试方法。你将知道如何处理面试中的关键时刻,面试官期望从你那里看到和听到什么,以及如何处理当你对答案/解决方案一无所知时的阻塞时刻。

技术测验

技术测验可以采用技术面试官问答的形式,也可以是现场测验。通常包含 20-40 个问题,耗时不到一小时。

当技术面试官进行这个过程时,你将需要提供自由回答,持续时间可能会有所不同(例如,30-45 分钟之间)。清晰、简洁、并且始终保持话题相关是很重要的。

通常,当技术面试官进行面试时,问题会被构建成需要你做出决定或选择的场景。例如,一个问题可能听起来像这样:我们需要一个能够以极快的速度搜索数百万条记录并具有相当数量的误报的高效算法。你会为我们推荐什么? 很可能,期望的答案是类似于,我会考虑 Bloom 过滤器家族的算法。如果你在以前的项目中遇到过类似的情况,那么你可以这样说:我们在一个关于流数据的项目中遇到了相同的情况,我们决定采用 Bloom 过滤器算法

另一类问题旨在简单检查你的技术知识。这些问题不是在场景或项目的背景下提出的;例如,你能告诉我 Java 中线程的生命周期状态是什么吗? 期望的答案是,在任何时刻,Java 线程可以处于以下状态之一: NEW, RUNNABLE, RUNNING, BLOCKED, SLEEP, WAITING/TIMED/WAITING, TERMINATED

通常,回答技术问题是一个三步方法,如下图所示。首先,你应该理解问题。如果有任何疑问,就要求澄清。其次,你必须知道面试官希望你在回答中识别出几个关键词或要点。这就像一个清单。这意味着你必须了解应该在答案中突出的关键内容。第三,你只需要用逻辑和有意义的方式包装关键词/要点:

图 5.1 - 处理技术测验的过程

图 5.1 - 处理技术测验的过程

你将在第六章**,面向对象编程中看到大量例子。

一般来说,你的答案应该是技术性的,简洁但全面,并且自信地表达出来。害羞的人常见的错误是提供一个听起来像问题的答案。他们的语气就像他们对每个词都在询问确认。当你的答案听起来像一个问题时,面试官可能会告诉你直接给出答案而不要问他。

重要提示

当你只能部分回答一个问题时,不要急于回答或者说你不知道。尝试向面试官询问更多细节和/或 20 秒的思考时间。有时,这会帮助你提供一个不完整但还不错的答案。例如,面试官可能会问你,“Java 中检查异常和未检查异常的主要区别是什么?”如果你不知道区别,那么你可以给出一个答案,比如,“检查异常是 Exception 的子类,而未检查异常是 RuntimeException 的子类”。你实际上没有回答问题,但这比说“我不知道”要好!或者,你可以提出一个问题,比如,“您是指我们被迫捕获的异常吗?”通过这样做,你可能会从面试官那里得到更多细节。注意不要问得像,“您是指我们被迫捕获的异常和我们不被迫捕获的异常吗?”你可能会得到一个简短的答复,比如“是”。这对你没有帮助!另一方面,如果你真的不知道答案/解决方案,那么最好说“我不知道”。这不一定会对你不利,而试图用太多的废话来迷惑面试官肯定会对你不利。

有些公司更喜欢进行现场的多项选择测验。在这种情况下,没有人的帮助,你必须在固定的时间内完成测验(例如,30 分钟)。重要的是要尽量回答尽可能多的问题。如果你不知道一个问题,那就继续下一个。时间在流逝!在最后(最后的 2-3 分钟),你可以回过头来尝试回答那些你放弃的问题。

然而,有些平台不允许你在问题之间来回跳转。在这种情况下,当你不知道问题的答案时,你被迫冒险猜测答案。花费大量时间回答一个问题最终会导致得分不佳。理想情况下,你应该尽量在每个问题上花相同的时间。例如,如果你有 20 个问题要在 30 分钟内回答,那么你可以为每个问题分配 30/20 = 1.5 分钟。

接近技术测验(无论是什么类型的测验)的最佳技巧之一是进行几次“模拟”面试。找个朋友,让他扮演面试官的角色。把问题放在一个碗里,让他随机挑选一个一个来回答。回答问题,表现得就像你真的在面对真正的面试官一样。

编码挑战

编码挑战是任何技术面试的高潮。这是你展示所有编码技能的时刻。是时候证明你能胜任这份工作了。有工作和整洁的代码可以帮助你留下良好的印象。一个良好的印象可能弥补你在面试的其他阶段留下的空白。

编码挑战是一把双刃剑,可能会从计划中将你剔除,另一方面可能会让你尽管有其他缺点,但还是得到一个工作机会。

然而,这些编码挑战所特有的问题因为各种原因而非常困难。这些将在下一节中介绍。

编码挑战所特有的问题意在困难

你是否曾经见过一个特定于编码挑战阶段的问题,觉得奇怪、愚蠢,或者可能毫无意义,与真正的问题毫无关联?如果是的话,那么你见到了一个特别好的编码挑战阶段的问题。

为了更好地了解如何为这些问题做准备,了解它们的特点和要求是很重要的。所以,让我们来看一下它们:

  • 它们不是现实世界的问题:通常,现实世界的问题需要大量时间来编码,因此它们不适合编码挑战。面试官会要求您解决可以在合理时间内解释和编码的问题,而这些问题通常不是现实世界的问题。

  • 它们可能相当愚蠢:看到相当愚蠢的问题并不罕见,看起来就像它们是为了使您的生活变得更加复杂而创造的。它们似乎对某事没有用或没有目标。这是正常的,因为它们大多数时候不是现实世界的问题。

  • 它们相当复杂:即使它们可以很快解决,它们也不容易!很可能,您将被要求编写一个方法或一个类,但这并不意味着它会很容易。通常,它们需要各种技巧,它们是令人费解的,和/或它们利用编程语言的不太知名的特性(例如,使用位操作)。

  • 解决方案并不明显:由于它们相当复杂,这些问题的解决方案并不明显。不要指望立即找到解决方案!几乎没有人能做到!这些问题是特别设计的,以查看您如何处理无法立即看到解决方案的情况。这就是为什么您可能需要几个小时来解决它(通常是 1 到 3 个小时之间)。

  • 禁止常见的解决路径:大多数时候,这样的问题有明确的条款,禁止使用常见的解决路径。例如,您可能会收到一个听起来像这样的问题:*编写一个方法,它可以在给定位置之间提取字符串的子字符串,而不使用 String#substring()这样的内置方法。*就像这个例子一样,有无数的例子。只需选择一个或多个内置的 Java 方法(例如,实用方法),可以在相对短的时间内实现,并加以阐述;例如,编写一个方法,它可以做 X 而不使用 Y 这样的内置解决方案。探索 API 源代码,参与开源项目,并练习这样的问题对于解决这样的问题非常有用。

  • 他们的目的是将您置于一组接受录用的候选人中:这些编码挑战的难度被校准,以使您成为一组独特百分比的候选人。一些公司只向不到 5%的候选人提供工作机会。如果大多数候选人可以轻松解决某个特定问题,那么它将被替换。

重要提示

编码挑战特有的问题旨在具有挑战性,并通常按难度递增的顺序提出。很可能,要通过这些编码挑战,您的经验和编码技能是不够的。因此,如果尽管您的知识,您无法立即看到解决方案,不要感到沮丧。许多这样的问题旨在测试您解决不寻常情况的能力和测试您的编码技能。它们可能具有荒谬的条款和/或模糊的解决方案,利用编程语言的不常见特性。它们可能包含愚蠢的要求和/或虚假案例。只专注于如何解决它们,并始终遵守规则。

通常,一次编码挑战会足够面试官。然而,也有一些情况下,您可能需要通过两次甚至三次这样的挑战。关键是尽可能多地练习。下一节将向您展示如何处理一般的编码挑战问题。

解决编码挑战问题

在讨论解决编码挑战问题的过程之前,让我们快速为编码挑战设置一个可能的环境。主要有两个坐标定义了这个环境:面试官在编码挑战期间的存在和纸笔对电脑的方法。

面试官在编码挑战过程中的存在

最常见的情况是,在编码挑战期间面试官会在场(通过电话或者面对面)。他们会评估你的最终结果(代码),但他们不仅仅是为了这个原因在场。仅仅衡量你的编码能力并不需要他们的存在,通常在编程比赛中会遇到。面试编码挑战不是一个编程比赛。面试官想要在整个过程中看到你,以分析你的行为和沟通能力。他们想要看到你是否有解决问题的计划,你是以有组织还是混乱的方式行动,你是否写了丑陋的代码,你是否愿意沟通你的行动,或者你是否内向。此外,他们想要协助和指导你。当然,你需要努力不需要指导或尽可能少的指导,但对指导的适当反应也是受欢迎的。然而,努力不需要指导并不意味着你不应该与面试官互动。

继续交流!

与面试官的互动是一个重要因素。以下列表解释了互动计划的几个方面:

  • 在编码之前解释你的解决方案:在开始编码之前,从面试官那里挤出一些有价值的信息是很重要的。向他们描述你想要如何解决问题,你要遵循什么步骤,以及你会使用什么。例如,你可以说,“我认为在这里使用 HashSet 是合适的选择,因为插入的顺序不重要,而且我们不需要重复的值”。你会得到一个赞成的手势或一些建议,这将帮助你获得预期的结果。

  • 在编码时解释你在做什么:在编码时,向面试官解释。例如,你可以说,首先,我会创建一个 ArrayList 的实例,或者,在这里,我将文件从本地文件夹加载到内存中

  • 提出适当的问题:只要你知道并尊重限制,你可以提出可以节省时间的问题。例如,问,“我记不得了 - 默认的 MySQL 端口是 3308 还是 3306?”然而,不要过分夸大这些问题!

  • 提及重要方面:如果你知道与问题相关的其他信息,那么与面试官分享。这是一个展示你的编程知识、思想和围绕问题的想法的好机会。

如果你遇到一个你已经知道的问题(也许你在练习中解决过这样的问题),那么不要马上说出来。这不会给面试官留下好印象,你可能会得到另一个编码挑战。最好遵循你对任何其他问题的处理过程。在我们讨论这个过程之前,让我们解决面试环境的另一个方面。

纸笔与电脑的方法

如果编码挑战是通过电话屏幕进行的,那么面试官会要求你分享屏幕并在你喜欢的集成开发环境IDE)中编码。这样,面试官可以看到你如何利用 IDE 的帮助(例如,他们可以看到你是否使用 IDE 生成 getter 和 setter,还是手动编写它们)。

重要提示

避免在每行代码后运行应用程序。相反,在每个逻辑代码块后运行应用程序。进行更正并再次运行。利用 IDE 调试工具。

如果你与面试官面对面,那么可能会被要求使用纸张或白板进行编码。这时,编码可以使用 Java 甚至伪代码。由于你的代码无法编译和执行,你必须手动测试它。通过拿一个例子并将其通过你的代码来展示你的代码是有效的是很重要的。

重要提示

在混乱的方法中避免过多的写入-删除代码循环。三思而后行!否则,你会让面试官头疼。

现在,让我们看一下旨在提供解决问题的方法论和逻辑方法的一般步骤。

处理编码挑战问题的过程

处理编码挑战问题的过程可以通过一系列顺序应用的步骤来完成。以下图表显示了这些步骤:

图 5.2 - 处理编码挑战问题的过程

图 5.2 - 处理编码挑战问题的过程

现在,让我们详细说明每个步骤。在应用这个解决问题的过程中,不要忘记交互组件。

理解问题

理解问题非常重要。不要基于假设或对问题的部分理解开始解决问题。至少要读两遍问题!不要依赖于一次阅读,因为在大多数情况下,这些问题包含隐藏和模糊的要求或细节,很容易被忽略。

不要犹豫向面试官询问关于问题的问题。有些情况下,故意忽略细节,以测试你发现潜在问题的能力。

重要提示

只有你理解了问题,你才有解决它的机会。

接下来,是时候建立一个例子了。如果你能建立一个例子,那么这清楚地表明你已经理解了问题。

建立一个例子

据说,“一幅图值千言”,但我们也可以用同样的方式来描述一个例子。

勾画问题并建立一个例子将澄清任何剩下的误解。这将给你一个通过逐步方法详细了解问题的机会。一旦你有一个可行的例子,你应该开始看到整体解决方案。这对于测试你的最终代码也是有用的。

重要提示

草图和例子对于巩固你对问题的理解是有用的。

现在,是时候考虑整体解决方案,并决定要使用的算法了。

决定要使用的算法并解释它们

在这一点上,你已经理解了问题,甚至建立了一个例子。现在,是时候形成一个整体解决方案,并将其分解为步骤和算法了。

这是一个耗时的过程。在这一点上,重要的是应用“表达你的想法”的方法。如果你什么都不说,面试官就不知道你是一无所知还是在头脑风暴。例如,你可以说,“我觉得我可以用一个列表来存储邮件,...嗯...不,这不行,因为列表接受重复项。”当你在说话时(即使看起来像是在自言自语),面试官可以判断你推理的正确性,看到你的知识水平,并给你一些建议。面试官可能会回答说,“是的,这是一个很好的观点”,“但是不要忘记你需要保持插入的顺序”。

大多数情况下,问题需要对数据(字符串、数字、位、对象等)进行某种形式的操作,比如排序、排序、过滤、反转、展平、搜索、计算等。有数据的地方,也有数据结构(数组、列表、集合、映射、树等)。关键是找到你需要的数据操作和数据结构之间的适当匹配。通常,适当的匹配意味着以下内容:

  • 你可以轻松地对数据结构应用某些操作。

  • 你可以获得良好的性能(大 O - 见[第七章](B15403_07_Final_JM_ePub.xhtml#_idTextAnchor135),算法的大 O 分析)。

  • 你可以在使用的数据结构之间保持和谐。这意味着你不需要复杂或繁重的算法,也不需要进行数据结构之间的转换或利用数据。

这些是拼图的大块。成功识别正确的匹配是工作的一半。另一半是将这些块组合在一起形成解决方案。换句话说,你需要在方程中引入逻辑。

在阅读问题或理解问题并在脑海中构思解决方案的大局之后,立即开始编码是非常诱人的。*不要这样做!*通常,这会导致一连串的失败,让你失去耐心。很快,你所有的想法都会被浓雾所包围,你会开始匆忙编码,甚至出现荒谬的错误。

重要提示

在开始编码之前,花时间深思熟虑解决方案。

现在,是时候开始编写你的解决方案,并用你的编码技能给面试官留下深刻印象了。

编写骨架

用一个骨架开始编写解决方案。更准确地说,定义你的类、方法和接口,但不实现(行为/动作)。你将在下一步中填充它们。这样,你向面试官展示你的编码阶段遵循了一条清晰的道路。不要过于匆忙地跳入代码中。此外,尊重编程的基本原则,如单一职责、开闭原则、里氏替换原则、接口隔离原则、依赖反转SOLID)和不要重复自己DRY)。面试官很可能会关注这些原则。

重要提示

编写解决方案的骨架有助于面试官更容易地跟随你,并更好地理解你的推理。

此时,你已经吸引了面试官的注意。现在,是时候让你的骨架活起来了。

编写解决方案

现在,是时候编写解决方案了。在这个过程中,向面试官解释你编写的主要代码行。注意并遵守著名的 Java 编码风格(例如,遵循 google.github.io/styleguide/javaguide.html 上的Google Java Style Guide)。

重要提示

遵循著名的 Java 编码风格并向面试官解释你的行动将对最终结果有很大帮助。

一旦你完成了解决方案的核心实现,就是增加代码的健壮性的时候了。因此,作为最后的一步,不要忽视异常处理和验证(例如,验证方法的参数)。同时,确保你满足了问题的所有要求,并且使用了正确的数据类型。最后,是时候祈祷你的代码能够通过测试了。

测试解决方案是这个过程的最后一步。

测试解决方案

在这个过程的第二步中,你建立了一个例子。现在,是时候向面试官展示你的代码通过了例子的测试。非常重要的是要证明你的代码至少对这个例子有效。它可能会出现一些小错误,但最终,重要的是它能够运行。

不要放松!你赢得了当前的战斗,但并没有赢得战争!通常,面试官还想看到你的代码对边界情况或特殊情况的处理。通常,这些特殊情况涉及虚拟值、边界值、不当输入、强制异常的操作等。如果你的代码不够健壮,无法通过这些尝试,那么面试官会认为你在生产应用中也会这样编码。另一方面,如果你的代码有效,那么面试官会完全印象深刻。

重要提示

有效的代码应该让面试官满意。至少,你会感到他们对你更友好和放松一些。

如果你给面试官留下了良好的印象,那么面试官可能会想要问你一些额外的问题。你应该期待被问及代码的性能和替代解决方案。当然,你可以在没有被问及的情况下提供这样的信息。面试官会很高兴看到你能够以多种方式解决问题,并且你理解每种解决方案和决策的利弊。

卡住会让你僵住

首先,卡住是正常的。不要惊慌!不要沮丧!不要放弃!

如果你卡住了,那么面试官可能也会卡住。主要问题是如何处理这种障碍,而不是障碍本身。你必须保持冷静,并尝试做以下事情:

  • 回到你的示例:有时,详细说明你的示例或查看另一个示例会有所帮助。有两个示例可以帮助你在脑海中塑造出一般情况,并理解问题的支柱。

  • 在示例中孤立问题:每个示例都有一系列步骤。确定你卡住的步骤,并将其作为一个单独的问题专注解决。有时,将问题从上下文中分离出来可以让你更好地理解并解决它。

  • 尝试不同的方法:有时,解决问题的方法是从不同的角度来解决问题。不同的视角可以给你一个新的视野。也许另一个数据结构,Java 的隐藏功能,蛮力方法等等可以帮助你。一个丑陋的解决方案总比没有解决方案好!

  • 模拟或推迟问题:长时间挣扎解决一个步骤可能会导致你无法及时完成问题的不愉快情况。有时,最好是模拟或推迟导致你困扰的步骤,并继续进行其他步骤。可能最后当你回到这一步时,你会对它有更清晰的认识,并知道如何编码。

  • 寻求指导:这应该是你的最后手段,但在危机中,你必须采取拼命的解决方案。你可以询问类似于“我对这个方面感到困惑,因为…”(并解释;尝试证明你的困惑)。“你能否给我一些关于我在这里错过了什么的提示?”

面试官意识到这一步(步骤)的困难,所以他们不会对你卡住感到惊讶。他们会欣赏你的毅力、分析能力和在寻找解决方案时的冷静,即使你找不到解决方案。面试官知道你在日常工作中会遇到类似的情况,而在这种情况下最重要的是保持冷静并寻找解决方案。

总结

在本章中,我们谈到了解决编程挑战问题的过程。除了我们之前列举的步骤 - 理解问题,构建示例,决定和解释算法,编写框架代码,编写和测试解决方案 - 还有一个步骤将成为接下来章节的目标:大量练习问题!在下一章中,我们将从编程的基本概念开始。

第二部分:概念

本节涵盖有关概念的问题。在这个领域提供优秀的知识是一个很好的指标,表明你具备所需的基本技能,这意味着你有一个坚实和健康的技术基础,可以在面试阶段回答问题。公司寻找这样的人作为可能的候选人,可以接受培训来解决非常具体和复杂的任务。

本节包括以下章节:

  • 第六章,面向对象编程

  • 第七章,算法的大 O 分析

  • 第八章,递归和动态规划

  • 第九章,位操作

第六章:面向对象编程

本章涵盖了在 Java 面试中遇到的与面向对象编程OOP)相关的最流行的问题和问题。

请记住,我的目标不是教你面向对象编程,或者更一般地说,这本书的目标不是教你 Java。我的目标是教你如何在面试的情境下回答问题和解决问题。在这样的情境下,面试官希望得到一个清晰而简洁的答案;你没有时间写论文和教程。你必须能够清晰而有力地表达你的想法。你的答案应该是有意义的,你必须说服面试官你真正理解你在说什么,而不只是背诵一些空洞的定义。大多数情况下,你应该能够用一两个关键段落表达一篇文章或一本书的一章。

通过本章结束时,你将知道如何回答 40 多个涵盖面向对象编程基本方面的问题和问题。作为基本方面,你必须详细了解它们。如果你不知道这些问题的正确和简洁的答案,那么在面试中成功的机会将受到严重影响。

因此,让我们总结我们的议程如下:

  • 面向对象编程概念

  • SOLID 原则

  • GOF 设计模式

  • 编码挑战

让我们从与面向对象编程概念相关的问题开始。

技术要求

你可以在 GitHub 上找到本章中的所有代码。请访问以下链接:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/tree/master/Chapter06

理解面向对象编程概念

面向对象模型基于几个概念。这些概念对于计划设计和编写依赖于对象的应用程序的任何开发人员来说都必须熟悉。因此,让我们从以下列举它们开始:

  • 对象

  • 抽象

  • 封装

  • 继承

  • 多态

  • 协会

  • 聚合

  • 组合

通常,当这些概念被包含在问题中时,它们会以什么是...?为前缀。例如,什么是对象?,或者什么是多态?

重要说明

这些问题的正确答案是技术知识和现实世界的类比或例子的结合。避免冷冰冰的答案,没有超级技术细节和没有例子(例如,不要谈论对象的内部表示)。注意你说的话,因为面试官可能直接从你的答案中提取问题。如果你的答案中提到了一个概念,那么下一个问题可能会涉及到那个概念。换句话说,不要在你的答案中添加任何你不熟悉的方面。

因此,让我们在面试情境下回答与面向对象编程概念相关的问题。请注意,我们应用了第五章中学到的内容,如何应对编码挑战。更确切地说,我们遵循理解问题|确定关键词/关键点|给出答案的技巧。首先,为了熟悉这种技巧,我将提取关键点作为一个项目列表,并在答案中用斜体标出它们。

什么是对象?

你的答案中应该包含以下关键点:

  • 对象是面向对象编程的核心概念之一。

  • 对象是一个现实世界的实体。

  • 对象具有状态(字段)和行为(方法)。

  • 对象表示类的一个实例。

  • 对象占据内存中的一些空间。

  • 对象可以与其他对象通信。

现在,我们可以按照以下方式提出答案:

*对象是面向对象编程的核心概念之一。对象是现实世界的实体,比如汽车、桌子或猫。在其生命周期中,对象具有状态和行为。例如,猫的状态可以是颜色、名字和品种,而其行为可以是玩耍、吃饭、睡觉和喵喵叫。在 Java 中,对象通常是通过new关键字构建的类的实例,它的状态存储在字段中,并通过方法公开其行为。每个实例在内存中占据一些空间,并且可以与其他对象通信。例如,另一个对象男孩可以抚摸一只猫,然后它就会睡觉。

如果需要进一步的细节,那么你可能想谈论对象可以具有不同的访问修饰符和可见性范围,可以是可变的、不可变的或不可变的,并且可以通过垃圾收集器进行收集。

什么是类?

你应该在你的答案中封装的关键点是:

  • 类是面向对象编程的核心概念之一。

  • 类是创建对象的模板或蓝图。

  • 类不会占用内存。

  • 一个类可以被实例化多次。

  • 一个类只做一件事。

现在,我们可以这样提出一个答案:

*类是面向对象编程的核心概念之一。类是构建特定类型对象所需的一组指令。我们可以把类想象成一个模板、蓝图或配方,告诉我们如何创建该类的对象。创建该类的对象是一个称为实例化的过程,通常通过new关键字完成。我们可以实例化任意多个对象。类定义不会占用内存,而是保存在硬盘上的文件中。一个类应该遵循的最佳实践之一是单一职责原则(SRP)。在遵循这个原则的同时,一个类应该被设计和编写成只做一件事。

如果需要进一步的细节,那么你可能想谈论类可以具有不同的访问修饰符和可见性范围,支持不同类型的变量(局部、类和实例变量),并且可以声明为abstractfinalprivate,嵌套在另一个类中(内部类),等等。

什么是抽象?

你应该在你的答案中封装的关键点是:

  • 抽象是面向对象编程的核心概念之一。

  • 抽象是将对用户有意义的东西暴露给他们,隐藏其余的细节。

  • 抽象允许用户专注于应用程序的功能,而不是它是如何实现的。

  • 在 Java 中,通过抽象类和接口实现抽象。

现在,我们可以这样提出一个答案:

爱因斯坦声称一切都应该尽可能简单,但不要过于简单抽象是面向对象编程的主要概念之一,旨在尽可能简化用户的操作。换句话说,抽象只向用户展示对他们有意义的东西,隐藏其余的细节。在面向对象编程的术语中,我们说一个对象应该向其用户只公开一组高级操作,而这些操作的内部实现是隐藏的。因此,抽象允许用户专注于应用程序的功能,而不是它是如何实现的。这样,抽象减少了暴露事物的复杂性,增加了代码的可重用性,避免了代码重复,并保持了低耦合和高内聚。此外,它通过只暴露重要细节来维护应用程序的安全性和保密性。

让我们考虑一个现实生活的例子:一个人开车。这个人知道每个踏板的作用,以及方向盘的作用,但他不知道这些事情是车内部是如何完成的。他不知道赋予这些事情力量的内部机制。这就是抽象。在 Java 中,可以通过抽象类和接口实现抽象

如果需要更多细节,你可以分享屏幕或使用纸和笔编写你的例子。

所以,我们说一个人在开车。这个人可以通过相应的踏板加速或减速汽车。他还可以通过方向盘左转和右转。所有这些操作都被分组在一个名为Car的接口中:

public interface Car {
    public void speedUp();
    public void slowDown();
    public void turnRight();
    public void turnLeft();
    public String getCarType();
}

接下来,每种类型的汽车都应该实现Car接口,并重写这些方法来提供这些操作的实现。这个实现对用户(驾驶汽车的人)是隐藏的。例如,ElectricCar类如下所示(实际上,我们有复杂的业务逻辑代替了System.out.println):

public class ElectricCar implements Car {
    private final String carType;
    public ElectricCar(String carType) {
        this.carType = carType;
    }        
    @Override
    public void speedUp() {
        System.out.println("Speed up the electric car");
    }
    @Override
    public void slowDown() {
        System.out.println("Slow down the electric car");
    }
    @Override
    public void turnRight() {
        System.out.println("Turn right the electric car");
    }
    @Override
    public void turnLeft() {
        System.out.println("Turn left the electric car");
    }
    @Override
    public String getCarType() {
        return this.carType;
    }        
}

这个类的用户可以访问这些公共方法,而不需要了解具体的实现:

public class Main {
    public static void main(String[] args) {
        Car electricCar = new ElectricCar("BMW");
        System.out.println("Driving the electric car: " 
		  + electricCar.getCarType() + "\n");
        electricCar.speedUp();
        electricCar.turnLeft();
        electricCar.slowDown();
    }
}

输出列举如下:

Driving the electric car: BMW
Speed up the electric car
Turn left the electric car
Slow down the electric car

所以,这是一个通过接口进行抽象的例子。完整的应用程序名为Abstraction/AbstractionViaInterface。在本书附带的代码中,你可以找到通过抽象类实现相同场景的代码。完整的应用程序名为Abstraction/AbstractionViaAbstractClass

接下来,让我们谈谈封装。

什么是封装?

你应该在你的答案中封装的关键点如下:

  • 封装是面向对象编程的核心概念之一。

  • 封装是一种技术,通过它,对象状态被隐藏,同时提供了一组公共方法来访问这个状态。

  • 当每个对象将其状态私有化在一个类内部时,封装就实现了。

  • 封装被称为数据隐藏机制。

  • 封装有许多重要的优点,比如松散耦合、可重用、安全和易于测试的代码。

  • 在 Java 中,封装是通过访问修饰符(publicprivateprotected)实现的。

现在,我们可以这样呈现一个答案:

封装是面向对象编程的核心概念之一。主要来说,封装将代码和数据绑定在一个单元(类)中,并充当一个防御屏障,不允许外部代码直接访问这些数据。主要来说,它是隐藏对象状态,向外部提供一组公共方法来访问这个状态的技术。当每个对象将其状态私有化在一个类内部时,我们可以说封装已经实现。这就是为什么封装也被称为公共、私有和受保护。通常,当一个对象管理自己的状态时,其状态通过私有变量声明,并通过公共方法访问和/或修改。让我们举个例子:一个Cat类可以通过moodhungryenergy等字段来表示其状态。虽然Cat类外部的代码不能直接修改这些字段中的任何一个,但它可以调用play()feed()sleep()等公共方法来在内部修改Cat的状态。Cat类也可能有私有方法,外部无法访问,比如meow()。这就是封装。

如果需要更多细节,你可以分享屏幕或使用纸和笔编写你的例子。

所以,我们的例子中的Cat类可以按照下面的代码块进行编码。注意,这个类的状态是通过私有字段封装的,因此不能直接从类外部访问:

public class Cat {
    private int mood = 50;
    private int hungry = 50;
    private int energy = 50;
    public void sleep() {
        System.out.println("Sleep ...");
        energy++;
        hungry++;
    }
    public void play() {
        System.out.println("Play ...");
        mood++;
        energy--;
        meow();
    }
    public void feed() {
        System.out.println("Feed ...");
        hungry--;
        mood++;
        meow();
    }
    private void meow() {
        System.out.println("Meow!");
    }
    public int getMood() {
        return mood;
    }
    public int getHungry() {
        return hungry;
    }
    public int getEnergy() {
        return energy;
    }
}

修改状态的唯一方式是通过play()feed()sleep()这些公共方法,就像下面的例子一样:

public static void main(String[] args) {
    Cat cat = new Cat();
    cat.feed();
    cat.play();
    cat.feed();
    cat.sleep();
    System.out.println("Energy: " + cat.getEnergy());
    System.out.println("Mood: " + cat.getMood());
    System.out.println("Hungry: " + cat.getHungry());
}

输出将如下所示:

Feed ...Meow!Play ...Meow!Feed ...Meow!Sleep ...
Energy: 50
Mood: 53
Hungry: 49

完整的应用程序名为Encapsulation。现在,让我们来了解一下继承。

什么是继承?

你应该在你的答案中封装的关键点如下:

  • 继承是面向对象编程的核心概念之一。

  • 继承允许一个对象基于另一个对象。

  • 继承通过允许一个对象重用另一个对象的代码并添加自己的逻辑来实现代码的可重用性。

  • 继承被称为IS-A关系,也被称为父子关系。

  • 在 Java 中,继承是通过extends关键字实现的。

  • 继承的对象被称为超类,继承超类的对象被称为子类。

  • 在 Java 中,不能继承多个类。

现在,我们可以这样呈现一个答案:

“继承是面向对象编程的核心概念之一。它允许一个对象基于另一个对象”,当不同的对象非常相似并共享一些公共逻辑时,这是很有用的,但它们并不完全相同。“继承通过允许一个对象重用另一个对象的代码来实现代码的可重用性,同时它也添加了自己的逻辑”。因此,为了实现继承,我们重用公共逻辑并将独特的逻辑提取到另一个类中。“这被称为 IS-A 关系,也被称为父子关系”。就像说FooBuzz类型的东西一样。例如,猫是猫科动物,火车是车辆。IS-A 关系是用来定义类层次结构的工作单元。“在 Java 中,继承是通过extends关键字实现的,通过从父类派生子类”。子类可以重用其父类的字段和方法,并添加自己的字段和方法。“继承的对象被称为超类,或者父类,继承超类的对象被称为子类,或者子类。在 Java 中,继承不能是多重的;因此,子类或子类不能继承多于一个超类或父类的字段和方法。例如,Employee类(父类)可以定义软件公司任何员工的公共逻辑,而另一个类(子类),名为Programmer,可以扩展Employee以使用这个公共逻辑并添加特定于程序员的逻辑。其他类也可以扩展ProgrammerEmployee类。”

如果需要更多细节,你可以分享屏幕或使用纸和笔编写你的例子。

Employee类非常简单。它包装了员工的名字:

public class Employee {
    private String name;
    public Employee(String name) {
        this.name = name;
    }
    // getters and setters omitted for brevity
}

然后,Programmer类扩展了Employee。像任何员工一样,程序员有一个名字,但他们也被分配到一个团队中:

public class Programmer extends Employee {
    private String team;
    public Programmer(String name, String team) {
        super(name);
        this.team = team;
    }
    // getters and setters omitted for brevity
}

现在,让我们通过创建一个Programmer并调用从Employee类继承的getName()和从Programmer类继承的getTeam()来测试继承:

public static void main(String[] args) {
    Programmer p = new Programmer("Joana Nimar", "Toronto");
    String name = p.getName();
    String team = p.getTeam();
    System.out.println(name + " is assigned to the " 
          + team + " team");
}

输出将如下所示:

Joana Nimar is assigned to the Toronto team

完整的应用程序被命名为继承。接下来,让我们谈谈多态。

什么是多态?

你应该在你的答案中包含的关键点是:

  • 多态是面向对象编程的核心概念之一。

  • 多态在希腊语中意味着“多种形式”。

  • 多态允许对象在某些情况下表现得不同。

  • 多态可以通过方法重载(称为编译时多态)或通过方法重写来实现 IS-A 关系(称为运行时多态)。

现在,我们可以这样呈现一个答案:

多态是面向对象编程的核心概念之一。多态是由两个希腊单词组成的:poly,意思是morph,意思是形式。因此,多态意味着多种形式

更准确地说,在面向对象编程的上下文中,多态性允许对象在某些情况下表现不同,或者换句话说,允许以不同的方式(方法)完成某个动作。实现多态性的一种方式是通过方法重载。这被称为编译时多态性,因为编译器可以在编译时识别调用重载方法的形式(具有相同名称但不同参数的多个方法)。因此,根据调用的重载方法的形式,对象的行为会有所不同。例如,名为Triangle的类可以定义多个带有不同参数的draw()方法。

另一种实现多态性的方法是通过方法重写,当我们有一个 IS-A 关系时,这是常见的方法。这被称为运行时多态性,或动态方法分派。通常,我们从一个包含一堆方法的接口开始。接下来,每个类实现这个接口并重写这些方法以提供特定的行为。这次,多态性允许我们像使用其父类(接口)一样使用这些类中的任何一个,而不会混淆它们的类型。这是可能的,因为在运行时,Java 可以区分这些类并知道使用哪一个。例如,一个名为Shape的接口可以声明一个名为draw()的方法,而TriangleRectangleCircle类实现了Shape接口并重写了draw()方法来绘制相应的形状。

如果需要进一步的细节,那么你可以分享屏幕或使用纸和笔编写你的例子。

通过方法重载实现多态性(编译时)

Triangle类包含三个draw()方法,如下所示:

public class Triangle {
    public void draw() {
        System.out.println("Draw default triangle ...");
    }
    public void draw(String color) {
        System.out.println("Draw a triangle of color " 
            + color);
    }
    public void draw(int size, String color) {
        System.out.println("Draw a triangle of color " + color
           + " and scale it up with the new size of " + size);
    }
}

接下来,注意相应的draw()方法是如何被调用的:

public static void main(String[] args) {
    Triangle triangle = new Triangle();
    triangle.draw();
    triangle.draw("red");
    triangle.draw(10, "blue");
}

输出将如下所示:

Draw default triangle ...
Draw a triangle of color red
Draw a triangle of color blue and scale it up 
with the new size of 10

完整的应用程序名为多态性/编译时。接下来,让我们看一个实现运行时多态性的例子。

通过方法重写实现多态性(运行时)

这次,draw()方法是在一个接口中声明的,如下所示:

public interface Shape {
    public void draw();
}

TriangleRectangleCircle类实现了Shape接口并重写了draw()方法来绘制相应的形状:

public class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw a triangle ...");
    }
}
public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw a rectangle ...");
    }
}
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Draw a circle ...");
    }
}

接下来,我们创建一个三角形、一个矩形和一个圆。对于这些实例中的每一个,让我们调用draw()方法:

public static void main(String[] args) {
    Shape triangle = new Triangle();
    Shape rectangle = new Rectangle();
    Shape circle = new Circle();
    triangle.draw();
    rectangle.draw();
    circle.draw();
}

输出显示,在运行时,Java 调用了正确的draw()方法:

Draw a triangle ...
Draw a rectangle ...
Draw a circle ...

完整的应用程序名为多态性/运行时。接下来,让我们谈谈关联。

重要提示

有人认为多态性是面向对象编程中最重要的概念。此外,也有声音认为运行时多态性是唯一真正的多态性,而编译时多态性实际上并不是一种多态性形式。在面试中引发这样的辩论是不建议的。最好是充当调解人,提出事情的两面。我们很快将讨论如何处理这种情况。

什么是关联?

你的答案中应该包含的关键点是:

  • 关联是面向对象编程的核心概念之一。

  • 关联定义了两个相互独立的类之间的关系。

  • 关联没有所有者。

  • 关联可以是一对一、一对多、多对一和多对多。

现在,我们可以给出如下答案:

关联是面向对象编程的核心概念之一。关联的目标是定义两个类之间独立于彼此的关系,也被称为对象之间的多重性关系。没有关联的所有者。参与关联的对象可以互相使用(双向关联),或者只有一个使用另一个(单向关联),但它们有自己的生命周期。关联可以是单向/双向,一对一,一对多,多对一和多对多。例如,在PersonAddress对象之间,我们可能有一个双向多对多的关系。换句话说,一个人可以与多个地址相关联,而一个地址可以属于多个人。然而,人可以存在而没有地址,反之亦然。

如果需要进一步的细节,那么你可以分享屏幕或使用纸和笔编写你的例子。

PersonAddress类非常简单:

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    // getters and setters omitted for brevity
}
public class Address {
    private String city;
    private String zip;
    public Address(String city, String zip) {
        this.city = city;
        this.zip = zip;
    }
    // getters and setters omitted for brevity
}

PersonAddress之间的关联是在main()方法中完成的,如下面的代码块所示:

public static void main(String[] args) {
    Person p1 = new Person("Andrei");
    Person p2 = new Person("Marin");
    Address a1 = new Address("Banesti", "107050");
    Address a2 = new Address("Bucuresti", "229344");
    // Association between classes in the main method 
    System.out.println(p1.getName() + " lives at address "
            + a2.getCity() + ", " + a2.getZip()
            + " but it also has an address at "
            + a1.getCity() + ", " + a1.getZip());
    System.out.println(p2.getName() + " lives at address "
            + a1.getCity() + ", " + a1.getZip()
            + " but it also has an address at "
            + a2.getCity() + ", " + a2.getZip());
}

输出如下所示:

Andrei lives at address Bucuresti, 229344 but it also has an address at Banesti, 107050
Marin lives at address Banesti, 107050 but it also has an address at Bucuresti, 229344

完整的应用程序被命名为Association。接下来,让我们谈谈聚合。

什么是聚合?

你的答案中应该包含的关键点如下:

  • 聚合是面向对象编程的核心概念之一。

  • 聚合是单向关联的特殊情况。

  • 聚合代表一个 HAS-A 关系。

  • 两个聚合对象有各自的生命周期,但其中一个对象是 HAS-A 关系的所有者。

现在,我们可以这样呈现一个答案:

聚合是面向对象编程的核心概念之一。主要是,聚合是单向关联的特殊情况。当一个关联定义了两个类之间独立于彼此的关系时,聚合代表这两个类之间的 HAS-A 关系。换句话说,两个聚合对象有各自的生命周期,但其中一个对象是 HAS-A 关系的所有者。有自己的生命周期意味着结束一个对象不会影响另一个对象。例如,一个TennisPlayer有一个Racket。这是一个单向关联,因为一个Racket不能拥有一个TennisPlayer。即使TennisPlayer死亡,Racket也不会受到影响。

重要提示

请注意,当我们定义聚合的概念时,我们也对关联有了一个陈述。每当两个概念紧密相关且其中一个是另一个的特殊情况时,都要遵循这种方法。下一步,同样的做法被应用于将组合定义为聚合的特殊情况。面试官会注意到并赞赏你对事物的概览,并且你能够提供一个有意义的答案,没有忽视上下文。

如果需要进一步的细节,那么你可以分享屏幕或使用纸和笔编写你的例子。

我们从Rocket类开始。这是网球拍的简单表示:

public class Racket {
    private String type;
    private int size;
    private int weight;
    public Racket(String type, int size, int weight) {
        this.type = type;
        this.size = size;
        this.weight = weight;
    }
    // getters and setters omitted for brevity
}

一个TennisPlayer拥有一个Racket。因此,TennisPlayer类必须能够接收一个Racket,如下所示:

public class TennisPlayer {
    private String name;
    private Racket racket;
    public TennisPlayer(String name, Racket racket) {
        this.name = name;
        this.racket = racket;
    }
    // getters and setters omitted for brevity
}

接下来,我们创建一个Racket和一个使用这个RacketTennisPlayer

public static void main(String[] args) {
    Racket racket = new Racket("Babolat Pure Aero", 100, 300);
    TennisPlayer player = new TennisPlayer("Rafael Nadal", 
        racket);
    System.out.println("Player " + player.getName() 
        + " plays with " + player.getRacket().getType());
}

输出如下:

Player Rafael Nadal plays with Babolat Pure Aero

完整的应用程序被命名为Aggregation。接下来,让我们谈谈组合。

什么是组合?

你的答案中应该包含的关键点如下:

  • 组合是面向对象编程的核心概念之一。

  • 组合是聚合的一种更为严格的情况。

  • 组合代表一个包含一个不能独立存在的对象的 HAS-A 关系。

  • 组合支持对象的代码重用和可见性控制。

现在,我们可以这样呈现一个答案:

组合是面向对象编程的核心概念之一主要来说,组合是聚合的一种更严格的情况。聚合表示两个对象之间具有自己的生命周期的 HAS-A 关系,组合表示包含一个不能独立存在的对象的 HAS-A 关系。为了突出这种耦合,HAS-A 关系也可以被称为 PART-OF。例如,一个Car有一个Engine。换句话说,发动机是汽车的一部分。如果汽车被销毁,那么发动机也会被销毁。组合被认为比继承更好,因为它维护了对象的代码重用和可见性控制

如果需要进一步的细节,那么你可以分享屏幕或使用纸和笔编写你的例子。

Engine类非常简单:

public class Engine {
    private String type;
    private int horsepower;
    public Engine(String type, int horsepower) {
        this.type = type;
        this.horsepower = horsepower;
    }
    // getters and setters omitted for brevity
}

接下来,我们有Car类。查看这个类的构造函数。由于EngineCar的一部分,我们用Car创建它:

public class Car {
    private final String name;
    private final Engine engine;
    public Car(String name) {
        this.name = name;
        Engine engine = new Engine("petrol", 300);
        this.engine=engine;
    }
    public int getHorsepower() {
        return engine.getHorsepower();
    }
    public String getName() {
        return name;
   }    
}

接下来,我们可以从main()方法中测试组合如下:

public static void main(String[] args) {
    Car car = new Car("MyCar");
    System.out.println("Horsepower: " + car.getHorsepower());
}

输出如下:

Horsepower: 300

完整的应用程序被命名为组合**。**

到目前为止,我们已经涵盖了关于面向对象编程概念的基本问题。请记住,这些问题几乎可以在涉及编码或架构应用程序的任何职位的 Java 技术面试中出现。特别是如果你有大约 2-4 年的经验,那么你被问到上述问题的机会很高,你必须知道答案,否则这将成为你的一个污点。

现在,让我们继续讨论 SOLID 原则。这是另一个基本领域,与面向对象编程概念并列的必须知道的主题。在这个领域缺乏知识将在最终决定你的面试时证明是有害的。

了解 SOLID 原则

在这一部分,我们将对与编写类的五个著名设计模式对应的问题进行回答 - SOLID 原则。简而言之,SOLID 是以下内容的首字母缩写:

  • S:单一责任原则

  • O:开闭原则

  • L:里氏替换原则

  • I:接口隔离原则

  • D:依赖反转原则

在面试中,与 SOLID 相关的最常见的问题是*什么是...?*类型的。例如,*S 是什么?或者 D 是什么?*通常,与面向对象编程相关的问题是故意模糊的。这样,面试官测试你的知识水平,并希望看到你是否需要进一步的澄清。因此,让我们依次解决这些问题,并提供一个令面试官印象深刻的答案。

S 是什么?

你应该在你的答案中概括的关键点如下:

  • S 代表单一责任原则(SRP)。

  • S 代表一个类应该只有一个责任

  • S 告诉我们为了一个目标编写一个类。

  • S 维护了整个应用程序模块的高可维护性和可见性控制。

现在,我们可以给出以下答案:

首先,SOLID 是 Robert C. Martin(也被称为 Uncle Bob)阐述的前五个面向对象设计(OOD)原则的首字母缩写。S是 SOLID 的第一个原则,被称为单一责任原则SRP)。这个原则意味着一个类应该只有一个责任。这是一个非常重要的原则,应该在任何类型的项目中遵循,无论是任何类型的类(模型、服务、控制器、管理类等)。只要我们为一个目标编写一个类,我们就能在整个应用程序模块中保持高可维护性和可见性控制。换句话说,通过保持高可维护性,这个原则对业务有重大影响,通过提供应用程序模块的可见性控制,这个原则维护了封装性。

如果需要更多细节,那么你可以分享屏幕或使用纸和笔编写一个像这里呈现的例子一样的例子。

例如,你想计算一个矩形的面积。矩形的尺寸最初以米为单位给出,面积也以米为单位计算,但我们希望能够将计算出的面积转换为其他单位,比如英寸。让我们看一下违反 SRP 的方法。

违反 SRP

在单个类RectangleAreaCalculator中实现前面的问题可以这样做。但是这个类做了不止一件事:它违反了 SRP。请记住,通常当你用“和”这个词来表达一个类做了什么时,这是 SRP 被违反的迹象。例如,下面的类计算面积并将其转换为英寸:

public class RectangleAreaCalculator {
    private static final double INCH_TERM = 0.0254d;
    private final int width;
    private final int height;
    public RectangleAreaCalculator(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public int area() {
        return width * height;
    }
    // this method breaks SRP
    public double metersToInches(int area) {
        return area / INCH_TERM;
    }    
}

由于这段代码违反了 SRP,我们必须修复它以遵循 SRP。

遵循 SRP

通过从RectangleAreaCalculator中删除“metersToInches()”方法来解决这种情况,如下所示:

public class RectangleAreaCalculator {
    private final int width;
    private final int height;
    public RectangleAreaCalculator(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public int area() {
        return width * height;
    }       
}

现在,RectangleAreaCalculator只做一件事(计算矩形面积),从而遵守 SRP。

接下来,可以将“metersToInches()”提取到一个单独的类中。此外,我们还可以添加一个新的方法来将米转换为英尺:

public class AreaConverter {
    private static final double INCH_TERM = 0.0254d;
    private static final double FEET_TERM = 0.3048d;
    public double metersToInches(int area) {
        return area / INCH_TERM;
    }
    public double metersToFeet(int area) {
        return area / FEET_TERM;
    }
}

这个类也遵循了 SRP,因此我们的工作完成了。完整的应用程序被命名为“SingleResponsabilityPrinciple”。接下来,让我们谈谈第二个 SOLID 原则,即开闭原则。

O 是什么?

你应该在你的答案中包含的关键点是:

  • O 代表开闭原则(OCP)。

  • O 代表“软件组件应该对扩展开放,但对修改关闭”。

  • O 维持了这样一个事实,即我们的类不应该包含需要其他开发人员修改我们的类才能完成工作的约束条件-其他开发人员应该只能扩展我们的类来完成他们的工作。

  • O 以一种多才多艺、直观且无害的方式维持软件的可扩展性。

现在,我们可以这样回答:

首先,SOLID 是 Robert C. Martin 提出的前五个面向对象设计(OOD)原则的首字母缩写,也被称为 Uncle Bob(可选短语)。 O 是 SOLID 中的第二个原则,被称为开闭原则(OCP)。这个原则代表“软件组件应该对扩展开放,但对修改关闭”。这意味着我们的类应该被设计和编写成其他开发人员可以通过简单地扩展它们来改变这些类的行为。因此,“我们的类不应该包含需要其他开发人员修改我们的类才能完成工作的约束条件-其他开发人员应该只能扩展我们的类来完成工作”。

虽然我们“必须以一种多才多艺、直观且无害的方式维持软件的可扩展性”,但我们不必认为其他开发人员会想要改变我们的类的整个逻辑或核心逻辑。主要是,如果我们遵循这个原则,那么我们的代码将作为一个良好的框架,不会让我们修改它们的核心逻辑,但我们可以通过扩展一些类、传递初始化参数、重写方法、传递不同的选项等来修改它们的流程和/或行为。

如果需要更多细节,那么你可以分享屏幕或使用纸和笔编写一个像这里呈现的例子一样的例子。

现在,例如,你有不同的形状(例如矩形、圆)并且我们想要求它们的面积之和。首先,让我们看一下违反 OCP 的实现。

违反 OCP

每个形状都将实现Shape接口。因此,代码非常简单:

public interface Shape {    
}
public class Rectangle implements Shape {
    private final int width;
    private final int height;
    // constructor and getters omitted for brevity
}
public class Circle implements Shape {
    private final int radius;
    // constructor and getter omitted for brevity
}

在这一点上,我们可以很容易地使用这些类的构造函数来创建不同尺寸的矩形和圆。一旦我们有了几种形状,我们想要求它们的面积之和。为此,我们可以定义一个AreaCalculator类,如下所示:

public class AreaCalculator {
    private final List<Shape> shapes;
    public AreaCalculator(List<Shape> shapes) {
        this.shapes = shapes;
    }
    // adding more shapes requires us to modify this class
    // this code is not OCP compliant
    public double sum() {
        int sum = 0;
        for (Shape shape : shapes) {
            if (shape.getClass().equals(Circle.class)) {
                sum += Math.PI * Math.pow(((Circle) shape)
                    .getRadius(), 2);
            } else 
            if(shape.getClass().equals(Rectangle.class)) {
                sum += ((Rectangle) shape).getHeight() 
                    * ((Rectangle) shape).getWidth();
            }
        }
        return sum;
    }
}

由于每种形状都有自己的面积公式,我们需要一个if-else(或switch)结构来确定形状的类型。此外,如果我们想要添加一个新的形状(例如三角形),我们必须修改AreaCalculator类以添加一个新的if情况。这意味着前面的代码违反了 OCP。修复这段代码以遵守 OCP 会对所有类进行多处修改。因此,请注意,即使是简单的例子,修复不遵循 OCP 的代码可能会非常棘手。

遵循 OCP

主要思想是从AreaCalculator中提取每种形状的面积公式,并将其放入相应的Shape类中。因此,矩形将计算其面积,圆形也是如此,依此类推。为了强制每种形状必须计算其面积,我们将area()方法添加到Shape合同中:

public interface Shape { 
    public double area();
}

接下来,RectangleCircle实现Shape如下:

public class Rectangle implements Shape {
    private final int width;
    private final int height;
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public double area() {
        return width * height;
    }
}
public class Circle implements Shape {
    private final int radius;
    public Circle(int radius) {
        this.radius = radius;
    }
    @Override
    public double area() {
        return Math.PI * Math.pow(radius, 2);
    }
}

现在,AreaCalculator可以循环遍历形状列表,并通过调用适当的area()方法来计算面积。

public class AreaCalculator {
    private final List<Shape> shapes;
    public AreaCalculator(List<Shape> shapes) {
        this.shapes = shapes;
    }
    public double sum() {
        int sum = 0;
        for (Shape shape : shapes) {
            sum += shape.area();
        }
        return sum;
    }
}

代码符合 OCP。我们可以添加一个新的形状,而不需要修改AreaCalculator。因此,AreaCalculator对于修改是封闭的,当然,对于扩展是开放的。完整的应用程序被命名为开闭原则。接下来,让我们谈谈第三个 SOLID 原则,Liskov 替换原则。

什么是 L?

您应该在您的答案中封装以下关键点:

  • L 代表Liskov 替换原则 (LSP)

  • L 代表派生类型必须完全可替换其基类型

  • L 支持子类的对象必须与超类的对象以相同的方式行为。

  • L 对于运行时类型识别后跟随转换是有用的。

现在,我们可以如下呈现一个答案:

首先,SOLID 是前五个foo(p)的首字母缩写,其中p是类型T。然后,如果q是类型S,并且ST的子类型,那么foo(q)应该正常工作。

如果需要进一步的细节,那么您可以共享屏幕或使用纸张和笔来编写一个像这里呈现的例子一样的例子。

我们有一个接受三种类型会员的国际象棋俱乐部:高级会员、VIP 会员和免费会员。我们有一个名为Member的抽象类,它充当基类,以及三个子类-PremiumMemberVipMemberFreeMember。让我们看看这些会员类型是否可以替代基类。

违反 LSP

Member类是抽象的,它代表了我们国际象棋俱乐部所有成员的基类。

public abstract class Member {
    private final String name;
    public Member(String name) {
        this.name = name;
    }
    public abstract void joinTournament();
    public abstract void organizeTournament();
}

PremiumMember类可以加入国际象棋比赛,也可以组织这样的比赛。因此,它的实现非常简单。

public class PremiumMember extends Member {
    public PremiumMember(String name) {
        super(name);
    }
    @Override
    public void joinTournament() {
        System.out.println("Premium member joins tournament");         
    }
    @Override
    public void organizeTournament() {
        System.out.println("Premium member organize 
            tournament");        
     }
}

VipMember类与PremiumMember类大致相同,因此我们可以跳过它,专注于FreeMember类。FreeMember类可以参加比赛,但不能组织比赛。这是我们需要在organizeTournament()方法中解决的问题。我们可以抛出一个带有有意义消息的异常,或者我们可以显示一条消息,如下所示:

public class FreeMember extends Member {
    public FreeMember(String name) {
        super(name);
    }
    @Override
    public void joinTournament() {
        System.out.println("Classic member joins tournament 
            ...");

    }
    // this method breaks Liskov's Substitution Principle
    @Override
    public void organizeTournament() {
        System.out.println("A free member cannot organize 
            tournaments");

    }
}

但是抛出异常或显示消息并不意味着我们遵循 LSP。由于免费会员无法组织比赛,因此它不能替代基类,因此它违反了 LSP。请查看以下会员列表:

List<Member> members = List.of(
    new PremiumMember("Jack Hores"),
    new VipMember("Tom Johns"),
    new FreeMember("Martin Vilop")
); 

以下循环显示了我们的代码不符合 LSP,因为当FreeMember类必须替换Member类时,它无法完成其工作,因为FreeMember无法组织国际象棋比赛。

for (Member member : members) {
    member.organizeTournament();
}

这种情况是一个停滞不前的问题。我们无法继续实现我们的应用程序。我们必须重新设计我们的解决方案,以获得符合 LSP 的代码。所以让我们这样做!

遵循 LSP

重构过程从定义两个接口开始,这两个接口用于分离两个操作,即加入和组织国际象棋比赛:

public interface TournamentJoiner {
    public void joinTournament();
}
public interface TournamentOrganizer {
    public void organizeTournament();
}

接下来,抽象基类实现这两个接口如下:

public abstract class Member 
    implements TournamentJoiner, TournamentOrganizer {
    private final String name;
    public Member(String name) {
        this.name = name;
    }  
}

PremiumMemberVipMember保持不变。它们扩展了Member基类。然而,FreeMember类不能组织比赛,因此不会扩展Member基类。它只会实现TournamentJoiner接口:

public class FreeMember implements TournamentJoiner {
    private final String name;
    public FreeMember(String name) {
        this.name = name;
    }
    @Override
    public void joinTournament() {
        System.out.println("Free member joins tournament ...");
    }
}

现在,我们可以定义一个能够参加国际象棋比赛的成员列表如下:

List<TournamentJoiner> members = List.of(
    new PremiumMember("Jack Hores"),
    new PremiumMember("Tom Johns"),
    new FreeMember("Martin Vilop")
);

循环此列表,并用每种类型的成员替换TournamentJoiner接口,可以正常工作并遵守 LSP:

// this code respects LSP
for (TournamentJoiner member : members) {
    member.joinTournament();
}   

按照相同的逻辑,可以将能够组织国际象棋比赛的成员列表编写如下:

List<TournamentOrganizer> members = List.of(
    new PremiumMember("Jack Hores"),
    new VipMember("Tom Johns")
);

FreeMember没有实现TournamentOrganizer接口。因此,它不能添加到此列表中。循环此列表,并用每种类型的成员替换TournamentOrganizer接口可以正常工作并遵守 LSP:

// this code respects LSP
for (TournamentOrganizer member : members) {
    member.organizeTournament();
}

完成!现在我们有一个符合 LSP 的代码。完整的应用程序命名为LiskovSubstitutionPrinciple。接下来,让我们谈谈第四个 SOLID 原则,接口隔离原则。

什么是 I?

您应该在答案中封装的关键点如下:

  • I 代表接口隔离原则(ISP)。

  • I 代表客户端不应被强制实现他们不会使用的不必要的方法

  • 我将一个接口分割成两个或更多个接口,直到客户端不被强制实现他们不会使用的方法。

现在,我们可以如下呈现一个答案:

首先,SOLID 是前五个Connection接口的首字母缩写,它有三种方法:connect()socket()http()。客户端可能只想为通过 HTTP 的连接实现此接口。因此,他们不需要socket()方法。大多数情况下,客户端会将此方法留空,这是一个糟糕的设计。为了避免这种情况,只需将Connection接口拆分为两个接口;SocketConnection具有socket()方法,HttpConnection具有http()方法。这两个接口将扩展保留有共同方法connect()Connection接口。

如果需要进一步的细节,那么您可以共享屏幕或使用纸和笔编写一个像这里呈现的示例。由于我们已经描述了前面的例子,让我们跳到关于违反 ISP 的部分。

违反 ISP

Connection接口定义了三种方法如下:

public interface Connection {
    public void socket();
    public void http();
    public void connect();
}

WwwPingConnection是一个通过 HTTP 对不同网站进行 ping 的类;因此,它需要http()方法,但不需要socket()方法。请注意虚拟的socket()实现-由于WwwPingConnection实现了Connection,它被强制提供socket()方法的实现:

public class WwwPingConnection implements Connection {
    private final String www;
    public WwwPingConnection(String www) {
        this.www = www;
    }
    @Override
    public void http() {
        System.out.println("Setup an HTTP connection to " 
            + www);
    }
    @Override
    public void connect() {
        System.out.println("Connect to " + www);
    }
    // this method breaks Interface Segregation Principle
    @Override
    public void socket() {
    }
}

在不需要的方法中具有空实现或抛出有意义的异常,比如socket(),是一个非常丑陋的解决方案。检查以下代码:

WwwPingConnection www 
    = new WwwPingConnection 'www.yahoo.com');
www.socket(); // we can call this method!
www.connect();

我们期望从这段代码中获得什么?一个什么都不做的工作代码,或者由于没有 HTTP 端点而导致connect()方法引发的异常?或者,我们可以从socket()中抛出类型为*Socket is not supported!*的异常。那么,它为什么在这里?!因此,现在是时候重构代码以遵循 ISP 了。

遵循 ISP

为了遵守 ISP,我们需要分隔Connection接口。由于任何客户端都需要connect()方法,我们将其留在这个接口中:

public interface Connection {
    public void connect();
}

http()socket()方法分布在扩展Connection接口的两个单独的接口中,如下所示:

public interface HttpConnection extends Connection {
    public void http();
}
public interface SocketConnection extends Connection {
    public void socket();
}

这次,WwwPingConnection类只能实现HttpConnection接口并使用http()方法:

public class WwwPingConnection implements HttpConnection {
    private final String www;
    public WwwPingConnection(String www) {
        this.www = www;
    }
    @Override
    public void http() {
        System.out.println("Setup an HTTP connection to "
            + www);
    }
    @Override
    public void connect() {
        System.out.println("Connect to " + www);
    } 
}

完成!现在,代码遵循 ISP。完整的应用程序命名为InterfaceSegregationPrinciple。接下来,让我们谈谈最后一个 SOLID 原则,依赖倒置原则。

什么是 D?

您应该在答案中封装的关键点如下:

  • D代表依赖倒置原则(DIP)。

  • D代表依赖于抽象,而不是具体实现

  • D支持使用抽象层来绑定具体模块,而不是依赖于其他具体模块。

  • D维持了具体模块的解耦。

现在,我们可以提出一个答案如下:

首先,SOLID 是 Robert C. Martin 提出的前五个面向对象设计(OOD)原则的首字母缩写,也被称为 Uncle Bob(可选短语)。D是 SOLID 原则中的最后一个原则,被称为依赖倒置原则(DIP)。这个原则代表依赖于抽象,而不是具体实现。这意味着我们应该依赖于抽象层来绑定具体模块,而不是依赖于具体模块。为了实现这一点,所有具体模块应该只暴露抽象。这样,具体模块允许扩展功能或在另一个具体模块中插入,同时保持具体模块的解耦。通常,高级具体模块和低级具体模块之间存在高耦合。

如果需要更多细节,你可以分享屏幕或使用纸和笔编写一个例子。

数据库 JDBC URL,PostgreSQLJdbcUrl,可以是一个低级模块,而连接到数据库的类可能代表一个高级模块,比如ConnectToDatabase#connect()

打破 DIP

如果我们向connect()方法传递PostgreSQLJdbcUrl类型的参数,那么我们就违反了 DIP。让我们来看看PostgreSQLJdbcUrlConnectToDatabase的代码:

public class PostgreSQLJdbcUrl {
    private final String dbName;
    public PostgreSQLJdbcUrl(String dbName) {
        this.dbName = dbName;
    }
    public String get() {
        return "jdbc:// ... " + this.dbName;
    }
}
public class ConnectToDatabase {
    public void connect(PostgreSQLJdbcUrl postgresql) {
        System.out.println("Connecting to "
            + postgresql.get());
    }
}

如果我们创建另一种类型的 JDBC URL(例如MySQLJdbcUrl),那么我们就不能使用之前的connect(PostgreSQLJdbcUrl postgreSQL)方法。因此,我们必须放弃对具体的依赖,创建对抽象的依赖。

遵循 DIP

抽象可以由一个接口表示,每种类型的 JDBC URL 都应该实现该接口:

public interface JdbcUrl {
    public String get();
}

接下来,PostgreSQLJdbcUrl实现了JdbcUrl以返回特定于 PostgreSQL 数据库的 JDBC URL:

public class PostgreSQLJdbcUrl implements JdbcUrl {
    private final String dbName;
    public PostgreSQLJdbcUrl(String dbName) {
        this.dbName = dbName;
    }
    @Override
    public String get() {
        return "jdbc:// ... " + this.dbName;
    }
}

以完全相同的方式,我们可以编写MySQLJdbcUrlOracleJdbcUrl等。最后,ConnectToDatabase#connect()方法依赖于JdbcUrl抽象,因此它可以连接到实现了这个抽象的任何 JDBC URL:

public class ConnectToDatabase {
    public void connect(JdbcUrl jdbcUrl) {
        System.out.println("Connecting to " + jdbcUrl.get());
    }
}

完成!完整的应用程序命名为DependencyInversionPrinciple

到目前为止,我们已经涵盖了 OOP 的基本概念和流行的 SOLID 原则。如果你计划申请一个包括应用程序设计和架构的 Java 职位,那么建议你看看通用责任分配软件原则(GRASP)([en.wikipedia.org/wiki/GRASP_(object-oriented_design](en.wikipedia.org/wiki/GRASP_…

接下来,我们将扫描一系列结合了这些概念的热门问题。现在你已经熟悉了理解问题 | 提名关键点 | 回答的技巧,我将只突出回答中的关键点,而不是事先提取它们作为一个列表。

与 OOP、SOLID 和 GOF 设计模式相关的热门问题

在这一部分,我们将解决一些更难的问题,这些问题需要对 OOP 概念、SOLID 设计原则和四人帮(GOF)设计模式有真正的理解。请注意,本书不涵盖 GOF 设计模式,但有很多专门讨论这个主题的优秀书籍和视频。我建议你尝试 Aseem Jain 的《用 Java 学习设计模式》(www.packtpub.com/application-development/learn-design-patterns-java-video)。

面向对象编程(Java)中的方法重写是什么?

方法重写是一种面向对象的编程技术,允许开发人员编写两个具有相同名称和签名但具有不同行为的方法(非静态,非私有和非最终)。在继承运行时多态的情况下,可以使用方法重写。

在继承的情况下,我们在超类中有一个方法(称为被重写方法),并且我们在子类中重写它(称为重写方法)。在运行时多态中,我们在一个接口中有一个方法,实现这个接口的类正在重写这个方法。

Java 在运行时决定应该调用的实际方法,取决于对象的类型。方法重写支持灵活和可扩展的代码,换句话说,它支持以最小的代码更改添加新功能。

如果需要更多细节,那么可以列出管理方法重写的主要规则:

  • 方法的名称和签名(包括相同的返回类型或子类型)在超类和子类中,或在接口和实现中是相同的。

  • 我们不能在同一个类中重写一个方法(但我们可以在同一个类中重载它)。

  • 我们不能重写privatestaticfinal方法。

  • 重写方法不能降低被重写方法的可访问性,但相反是可能的。

  • 重写方法不能抛出比被重写方法抛出的检查异常更高的检查异常。

  • 始终为重写方法使用@Override注解。

Java 中重写方法的示例可在本书附带的代码中找到,名称为 MethodOverriding。

在面向对象编程(Java)中,什么是方法重载?

方法重载是一种面向对象的编程技术,允许开发人员编写两个具有相同名称但不同签名和不同功能的方法(静态或非静态)。通过不同的签名,我们理解为不同数量的参数,不同类型的参数和/或参数列表的不同顺序。返回类型不是方法签名的一部分。因此,当两个方法具有相同的签名但不同的返回类型时,这不是方法重载的有效情况。因此,这是一种强大的技术,允许我们编写具有相同名称但具有不同输入的方法(静态或非静态)。编译器将重载的方法调用绑定到实际方法;因此,在运行时不进行绑定。方法重载的一个著名例子是System.out.println()println()方法有几种重载的版本。

因此,有四条主要规则来管理方法重载:

  • 通过更改方法签名来实现重载。

  • 返回类型不是方法签名的一部分。

  • 我们可以重载privatestaticfinal方法。

  • 我们可以在同一个类中重载一个方法(但不能在同一个类中重写它)。

如果需要更多细节,可以尝试编写一个示例。Java 中重载方法的示例可在本书附带的代码中找到,名称为 MethodOverloading。

重要提示

除了前面提到的两个问题,您可能需要回答一些其他相关的问题,包括什么规则管理方法的重载和重写(见上文)?方法重载和重写的主要区别是什么(见上文)?我们可以重写静态或私有方法吗(简短的答案是不可以,见上文)?我们可以重写 final 方法吗(简短的答案是不可以,见上文)?我们可以重载静态方法吗(简短的答案是可以,见上文)?我们可以改变重写方法的参数列表吗(简短的答案是不可以,见上文)?因此,建议提取和准备这些问题的答案。所有所需的信息都可以在前面的部分找到。此外,注意诸如只有通过 final 修饰符才能防止重写方法这样的问题。这种措辞旨在混淆候选人,因为答案需要概述所涉及的概念。这里的答案可以表述为这是不正确的,因为我们也可以通过将其标记为私有或静态来防止重写方法。这样的方法不能被重写

接下来,让我们检查几个与重写和重载方法相关的其他问题。

在 Java 中,协变方法重写是什么?

协变方法重写是 Java 5 引入的一个不太知名的特性。通过这个特性,重写方法可以返回其实际返回类型的子类型。这意味着重写方法的客户端不需要对返回类型进行显式类型转换。例如,Java 的clone()方法返回Object。这意味着,当我们重写这个方法返回一个克隆时,我们得到一个Object,必须显式转换为我们需要的Object的实际子类。然而,如果我们利用 Java 5 的协变方法重写特性,那么重写的clone()方法可以直接返回所需的子类,而不是Object

几乎总是,这样的问题需要一个示例作为答案的一部分,因此让我们考虑实现Cloneable接口的Rectangle类。clone()方法可以返回Rectangle而不是Object,如下所示:

public class Rectangle implements Cloneable {
    ...  
    @Override
    protected Rectangle clone() 
            throws CloneNotSupportedException {
        Rectangle clone = (Rectangle) super.clone();
        return clone;
    }
}

调用clone()方法不需要显式转换:

Rectangle r = new Rectangle(4, 3);
Rectangle clone = r.clone();

完整的应用程序名为CovariantMethodOverriding。注意一些关于协变方法重写的间接问题。例如,可以这样表述:我们可以在重写时修改方法的返回类型为子类吗? 对于这个问题的答案与*Java 中的协变方法重写是什么?*相同,在这里讨论过。

重要提示

了解针对 Java 的一些不太知名特性的问题的答案可能是面试中的一个重要加分项。这向面试官表明您具有深入的知识水平,并且您对 Java 的发展了如指掌。如果您需要通过大量示例和最少理论来快速了解所有 JDK 8 到 JDK 13 的功能,那么您一定会喜欢我出版的名为Java 编程问题的书,由 Packt 出版(packtpub.com/au/programm…)。

在重写和重载方法方面,主要的限制是什么?

首先,让我们讨论重写方法。如果我们谈论未经检查的异常,那么我们必须说在重写方法中使用它们没有限制。这样的方法可以抛出未经检查的异常,因此任何RuntimeException。另一方面,在检查异常的情况下,重写方法只能抛出被重写方法的检查异常或该检查异常的子类。换句话说,重写方法不能抛出比被重写方法抛出的检查异常范围更广的检查异常。例如,如果被重写的方法抛出SQLException,那么重写方法可以抛出子类,如BatchUpdateException,但不能抛出超类,如Exception

其次,让我们讨论重载方法。这样的方法不会施加任何限制。这意味着我们可以根据需要修改throw子句。

重要提示

注意那些以主要是什么...?你能列举某些...吗?你能提名...吗?你能强调...吗?等方式措辞的问题。通常,当问题包含主要,某些,提名强调等词时,面试官期望得到一个清晰简洁的答案,应该听起来像一个项目列表。回答这类问题的最佳实践是直接进入回答并将每个项目列举为一个简洁而有意义的陈述。在给出预期答案之前,不要犯常见错误,即着手讲述所涉及的概念的故事或论文。面试官希望看到你的综合和整理能力,并在检查你的知识水平的同时提取本质。

如果需要更多细节,那么你可以编写一个示例,就像这本书中捆绑的代码一样。考虑检查OverridingExceptionOverloadingException应用程序。现在,让我们继续看一些更多的问题。

如何从子类重写的方法中调用超类重写的方法?

我们可以通过 Java 的 super 关键字从子类重写的方法中调用超类重写的方法。例如,考虑一个包含方法foo()的超类A,以及一个名为BA子类。如果我们在子类B中重写foo()方法,并且我们从重写方法B#foo()中调用super.foo(),那么我们调用被重写的方法A#foo()

我们能重写或重载 main()方法吗?

我们必须记住main()方法是静态的。这意味着我们可以对其进行重载。但是,我们不能对其进行重写,因为静态方法在编译时解析,而我们可以重写的方法在运行时根据对象类型解析。

我们能将非静态方法重写为静态方法吗?

不。我们不能将非静态方法重写为静态方法。此外,反之亦然也不可能。两者都会导致编译错误。

重要提示

像前面提到的最后两个问题一样直截了当的问题,值得一个简短而简洁的答案。面试官触发这样的闪光灯问题来衡量你分析情况并做出决定的能力。主要是,答案是简短的,但你需要一些时间来说。这类问题并不具有很高的分数,但如果你不知道答案,可能会产生重大的负面影响。如果你知道答案,面试官可能会在心里说好吧,这本来就是一个容易的问题!但是,如果你不知道答案,他可能会说他错过了一个简单的问题!她/他的基础知识有严重缺陷

接下来,让我们看一些与其他面向对象编程概念相关的更多问题。

我们能在 Java 接口中有一个非抽象方法吗?

直到 Java 8,我们不能在 Java 接口中有非抽象方法。接口中的所有方法都是隐式公共和抽象的。然而,从 Java 8 开始,我们可以向接口添加新类型的方法。从实际角度来看,从 Java 8 开始,我们可以直接在接口中添加具体实现的方法。这可以通过使用 default static 关键字来实现。 default 关键字是在 Java 8 中引入的,用于在接口中包含称为 static 方法的方法,接口中的 static 方法与默认方法非常相似,唯一的区别是我们不能在实现这些接口的类中重写 static 方法。由于static方法不绑定到对象,因此可以通过使用接口名称加上点和方法名称来调用它们。此外,static方法可以在其他defaultstatic方法中调用。

如果需要更多细节,那么您可以尝试编写一个示例。考虑到我们有一个用于塑造蒸汽车辆的接口(这是一种旧的汽车类型,与旧代码完全相同):

public interface Vehicle {
    public void speedUp();
    public void slowDown();    
}

显然,通过以下SteamCar类已经建造了不同种类的蒸汽车:

public class SteamCar implements Vehicle {
    private String name;
    // constructor and getter omitted for brevity
    @Override
    public void speedUp() {
        System.out.println("Speed up the steam car ...");
    }
    @Override
    public void slowDown() {
        System.out.println("Slow down the steam car ...");
    }
}

由于SteamCar类实现了Vehicle接口,它重写了speedUp()slowDown()方法。过了一段时间,汽油车被发明出来,人们开始关心马力和燃油消耗。因此,我们的代码必须发展以支持汽油车。为了计算消耗水平,我们可以通过添加computeConsumption()默认方法来发展Vehicle接口,如下所示:

public interface Vehicle {
    public void speedUp();
    public void slowDown();
    default double computeConsumption(int fuel, 
            int distance, int horsePower) {        
        // simulate the computation 
        return Math.random() * 10d;
    }        
}

发展Vehicle接口不会破坏SteamCar的兼容性。此外,电动汽车已经被发明。计算电动汽车的消耗与汽油汽车的情况不同,但公式依赖于相同的术语:燃料、距离和马力。这意味着ElectricCar将重写computeConsumption(),如下所示:

public class ElectricCar implements Vehicle {
    private String name;
    private int horsePower;
    // constructor and getters omitted for brevity
    @Override
    public void speedUp() {
        System.out.println("Speed up the electric car ...");
    }
    @Override
    public void slowDown() {
        System.out.println("Slow down the electric car ...");
    }
    @Override
    public double computeConsumption(int fuel, 
            int distance, int horsePower) {
        // simulate the computation
        return Math.random()*60d / Math.pow(Math.random(), 3);
    }     
}

因此,我们可以重写default方法,或者我们可以使用隐式实现。最后,我们必须为我们的接口添加描述,因为现在它服务于蒸汽、汽油和电动汽车。我们可以通过为Vehicle添加一个名为description()static方法来实现这一点,如下所示:

public interface Vehicle {
    public void speedUp();
    public void slowDown();
    default double computeConsumption(int fuel, 
        int distance, int horsePower) {        
        return Math.random() * 10d;
    }
    static void description() {
        System.out.println("This interface control
            steam, petrol and electric cars");
    }
}

这个static方法不绑定到任何类型的汽车,可以直接通过Vehicle.description()调用。完整的代码名为Java8DefaultStaticMethods

接下来,让我们继续其他问题。到目前为止,您应该对“理解问题”|“提名关键点”|“回答”技术非常熟悉,所以我将停止突出显示关键点。从现在开始,找到它们就是你的工作了。

接口和抽象类之间的主要区别是什么?

在 Java 8 接口和抽象类之间的差异中,我们可以提到抽象类可以有构造函数,而接口不支持构造函数。因此,抽象类可以有状态,而接口不能有状态。此外,接口仍然是完全抽象的第一公民,其主要目的是被实现,而抽象类是为了部分抽象。接口仍然被设计为针对完全抽象的事物,它们本身不做任何事情,但是指定了如何在实现中工作的合同。默认方法代表了一种方法,可以在不影响客户端代码和不改变状态的情况下向接口添加附加功能。它们不应该用于其他目的。换句话说,另一个差异在于,拥有没有抽象方法的抽象类是完全可以的,但是只有默认方法的接口是一种反模式。这意味着我们已经创建了接口作为实用类的替代品。这样,我们就打败了接口的主要目的,即被实现。

重要说明

当你不得不列举两个概念之间的许多差异或相似之处时,注意限制你的答案在问题确定的坐标内。例如,在前面的问题中,不要说接口支持多重继承而抽象类不支持这一点。这是接口和类之间的一般变化,而不是特别是 Java 8 接口和抽象类之间的变化。

抽象类和接口之间的主要区别是什么?

直到 Java 8,抽象类和接口的主要区别在于抽象类可以包含非抽象方法,而接口不能包含这样的方法。从 Java 8 开始,主要区别在于抽象类可以有构造函数和状态,而接口两者都不能有。

可以有一个没有抽象方法的抽象类吗?

是的,我们可以。通过向类添加abstract关键字,它变成了抽象类。它不能被实例化,但可以有构造函数和只有非抽象方法。

我们可以同时拥有一个既是抽象又是最终的类吗?

最终类不能被子类化或继承。抽象类意味着要被扩展才能使用。因此,最终和抽象是相反的概念。这意味着它们不能同时应用于同一个类。编译器会报错。

多态、重写和重载之间有什么区别?

在这个问题的背景下,重载技术被称为编译时多态,而重写技术被称为运行时多态。重载涉及使用静态(或早期)绑定,而重写使用动态(或晚期)绑定。

接下来的两个问题构成了这个问题的附加部分,但它们也可以作为独立的问题来表述。

什么是绑定操作?

绑定操作确定由于代码行中的引用而调用的方法(或变量)。换句话说,将方法调用与方法体关联的过程称为绑定操作。一些引用在编译时解析,而其他引用在运行时解析。在运行时解析的引用取决于对象的类型。在编译时解析的引用称为静态绑定操作,而在运行时解析的引用称为动态绑定操作。

静态绑定和动态绑定之间的主要区别是什么?

首先,静态绑定发生在编译时,而动态绑定发生在运行时。要考虑的第二件事是,私有、静态和最终成员(方法和变量)使用静态绑定,而虚方法根据对象类型在运行时进行绑定。换句话说,静态绑定是通过Type(Java 中的类)信息实现的,而动态绑定是通过Object实现的,这意味着依赖静态绑定的方法与对象无关,而是在Type(Java 中的类)上调用的,而依赖动态绑定的方法与Object相关。依赖静态绑定的方法的执行速度比依赖动态绑定的方法的执行速度稍快。静态和动态绑定也用于多态。静态绑定用于编译时多态(重载方法),而动态绑定用于运行时多态(重写方法)。静态绑定在编译时增加了性能开销,而动态绑定在运行时增加了性能开销,这意味着静态绑定更可取。

在 Java 中什么是方法隐藏?

方法隐藏是特定于静态方法的。更确切地说,如果我们在超类和子类中声明具有相同签名和名称的两个静态方法,那么它们将互相隐藏。从超类调用方法将调用超类的静态方法,从子类调用相同的方法将调用子类的静态方法。隐藏与覆盖不同,因为静态方法不能是多态的。

如果需要更多细节,你可以写一个例子。考虑Vehicle超类具有move()静态方法:

public class Vehicle {
    public static void move() {
        System.out.println("Moving a vehicle");
    }
}

现在,考虑Car子类具有相同的静态方法:

public class Car extends Vehicle {
    // this method hides Vehicle#move()
    public static void move() {
        System.out.println("Moving a car");
    }
}

现在,让我们从main()方法中调用这两个静态方法:

public static void main(String[] args) {
    Vehicle.move(); // call Vehicle#move()
    Car.move();     // call Car#move()
}

输出显示这两个静态方法互相隐藏:

Moving a vehicle
Moving a car

注意我们通过类名调用静态方法。在实例上调用静态方法是非常糟糕的做法,所以在面试中要避免这样做!

我们可以在 Java 中编写虚方法吗?

是的,我们可以!实际上,在 Java 中,所有非静态方法默认都是虚方法。我们可以通过使用private和/或final关键字标记来编写非虚方法。换句话说,可以继承以实现多态行为的方法是虚方法。或者,如果我们颠倒这个说法的逻辑,那些不能被继承(标记为private)和不能被覆盖(标记为final)的方法是非虚方法。

多态和抽象之间有什么区别?

抽象和多态代表两个相互依存的基本面向对象的概念。抽象允许开发人员设计可重用和可定制的通用解决方案,而多态允许开发人员推迟在运行时选择应该执行的代码。虽然抽象是通过接口和抽象类实现的,多态依赖于覆盖和重载技术。

你认为重载是实现多态的一种方法吗?

这是一个有争议的话题。有些人不认为重载是多态;因此,他们不接受编译时多态的概念。这些声音认为,唯一的覆盖方法才是真正的多态。这种说法背后的论点是,只有覆盖才允许代码根据运行时条件而表现出不同的行为。换句话说,表现多态行为是方法覆盖的特权。我认为只要我们理解重载和覆盖的前提条件,我们也就理解了这两种变体如何维持多态行为。

重要提示

处理有争议的话题的问题是微妙且难以正确处理的。因此,最好直接跳入答案,陈述这是一个有争议的话题。当然,面试官也对听到你的观点感兴趣,但他会很高兴看到你了解事情的两面。作为一个经验法则,尽量客观地回答问题,不要以激进的方式或者缺乏论据的方式处理问题的一面。有争议的事情毕竟还是有争议的,这不是揭开它们的神秘面纱的合适时间和地点。

好的,现在让我们继续一些基于 SOLID 原则和著名且不可或缺的四人帮(GOF)设计模式的问题。请注意,本书不涵盖 GOF 设计模式,但有很多专门讨论这个话题的优秀书籍和视频。我建议你尝试Aseem JainLearn Design Patterns with Javawww.packtpub.com/application-development/learn-design-patterns-java-video))。

哪个面向对象的概念服务于装饰者设计模式?

服务装饰者设计模式的面向对象编程概念是组合。通过这个面向对象编程概念,装饰者设计模式在不修改原始类的情况下提供新功能。

单例设计模式应该在什么时候使用?

单例设计模式似乎是在我们只需要一个类的应用级(全局)实例时的正确选择。然而,应该谨慎使用单例,因为它增加了类之间的耦合,并且在开发、测试和调试过程中可能成为瓶颈。正如著名的《Effective Java》所指出的,使用 Java 枚举是实现这种模式的最佳方式。在全局配置(例如日志记录器、java.lang.Runtime)、硬件访问、数据库连接等方面,依赖单例模式是一种常见情况。

重要提示

每当可以引用或提及著名参考资料时,请这样做。

策略和状态设计模式之间有什么区别?

状态设计模式旨在根据状态执行某些操作(在不更改类的情况下,在不同状态下展示某些行为)。另一方面,策略设计模式旨在用于在不修改使用它的代码的情况下在一系列算法之间进行切换(客户端通过组合和运行时委托可互换地使用算法)。此外,在状态中,我们有清晰的状态转换顺序(流程是通过将每个状态链接到另一个状态来创建的),而在策略中,客户端可以以任何顺序选择它想要的算法。例如,状态模式可以定义发送包裹给客户的状态

包裹从有序状态开始,然后继续到已交付状态,依此类推,直到通过每个状态并在客户端接收包裹时达到最终状态。另一方面,策略模式定义了完成每个状态的不同策略(例如,我们可能有不同的交付包裹策略)。

代理和装饰者模式之间有什么区别?

代理设计模式对于提供对某物的访问控制网关非常有用。通常,该模式创建代理对象,代替真实对象。对真实对象的每个请求都必须通过代理对象,代理对象决定如何何时将其转发给真实对象。装饰者设计模式从不创建对象,它只是在运行时用新功能装饰现有对象。虽然链接代理不是一个可取的做法,但以一定顺序链接装饰者可以以正确的方式利用这种模式。例如,代理模式可以表示互联网的代理服务器,而装饰者模式可以用于用不同的自定义设置装饰代理服务器。

外观和装饰者模式之间有什么区别?

装饰者设计模式旨在为对象添加新功能(换句话说,装饰对象),而外观设计模式根本不添加新功能。它只是外观现有功能(隐藏系统的复杂性),并通过向客户端暴露的“友好界面”在幕后调用它们。外观模式可以暴露一个简单的接口,调用各个组件来完成复杂的任务。例如,装饰者模式可以用来通过用发动机、变速箱等装饰底盘来建造汽车,而外观模式可以通过暴露一个简单的接口来隐藏建造汽车的复杂性,以便命令了解建造过程细节的工业机器人。

模板方法和策略模式之间的关键区别是什么?

模板方法和策略模式将特定领域的算法集合封装成对象,但它们的实现方式并不相同。关键区别在于策略模式旨在根据需求在运行时在不同策略(算法)之间做出决定,而模板方法模式旨在遵循算法的固定骨架(预定义的步骤序列)实现。一些步骤是固定的,而其他步骤可以根据不同的用途进行修改。例如,策略模式可以在不同的支付策略之间做出决定(例如信用卡或 PayPal),而模板方法可以描述使用特定策略进行支付的预定义步骤序列(例如,通过 PayPal 进行支付需要固定的步骤序列)。

生成器和工厂模式之间的关键区别是什么?

工厂模式在单个方法调用中创建对象。我们必须在此调用中传递所有必要的参数,工厂将返回对象(通常通过调用构造函数)。另一方面,生成器模式旨在通过一系列 setter 方法构建复杂对象,允许我们塑造任何组合的参数。在链的末尾,生成器方法公开了一个build()方法,表示参数列表已设置,现在是构建对象的时候了。换句话说,工厂充当构造函数的包装器,而生成器更加精细,充当您可能想要传递到构造函数的所有可能参数的包装器。通过生成器,我们避免了望远镜构造函数用于公开所有可能的参数组合。例如,回想一下Book对象。一本书由一些固定参数来描述,例如作者、标题、ISBN 和格式。在创建书籍时,您很可能不会在这些参数的数量上纠结,因此工厂模式将是适合创建书籍的选择。但是Server对象呢?嗯,服务器是一个具有大量可选参数的复杂对象,因此生成器模式在这里更加合适,甚至是工厂在内部依赖于生成器的这些模式的组合。

适配器和桥接模式之间的关键区别是什么?

适配器模式致力于提供现有代码(例如第三方代码)与新系统或接口之间的兼容性。另一方面,桥接模式是提前实现的,旨在将抽象与实现解耦,以避免大量的类。因此,适配器致力于在设计后提供事物之间的兼容性(可以想象为A 来自 After),而桥接是提前构建的,以使抽象和实现可以独立变化(可以想象为B 来自 Before)。适配器充当ReadJsonRequestReadXmlRequest,它们能够从多个设备读取,例如D1D2D3D1D2只产生 JSON 请求,而D3只产生 XML 请求。通过适配器,我们可以在 JSON 和 XML 之间进行转换,这意味着这两个类可以与所有三个设备进行通信。另一方面,通过桥接模式,我们可以避免最终产生许多类,例如ReadXMLRequestD1ReadXMLRequestD2ReadXMLRequestD3ReadJsonRequestD1ReadJsonRequestD2ReadJsonRequestD3

我们可以继续比较设计模式,直到完成所有可能的组合。最后几个问题涵盖了类型设计模式 1 与设计模式 2的最受欢迎的问题。强烈建议您挑战自己,尝试识别两种或更多给定设计模式之间的相似之处和不同之处。大多数情况下,这些问题使用来自同一类别的两种设计模式(例如,两种结构或两种创建模式),但它们也可以来自不同的类别。在这种情况下,这是面试官期望听到的第一句话。因此,在这种情况下,首先说出每个涉及的设计模式属于哪个类别。

请注意,我们跳过了所有简单问题,比如什么是接口?什么是抽象类?等等。通常,这类问题是要避免的,因为它们并不能说明您的理解水平,更多的是背诵一些定义。面试官可以问抽象类和接口的主要区别是什么?,他可以从您的回答中推断出您是否知道接口和抽象类是什么。始终要准备好举例。无法举例说明严重缺乏对事物本质的理解。

拥有 OOP 知识只是问题的一半。另一半是具有将这些知识转化为设计应用程序的愿景和灵活性。这就是我们将在接下来的 10 个示例中做的事情。请记住,我们专注于设计,而不是实现。

编码挑战

接下来,我们将解决关于面向对象编程的几个编码挑战。对于每个问题,我们将遵循第五章**中的图 5.2如何处理编码挑战。主要是,我们将首先向面试官提出一个问题,比如*设计约束是什么?*通常,围绕 OOD 的编码挑战是以一种一般的方式由面试官表达的。这是故意这样做的,以便让您询问有关设计约束的细节。

一旦我们清楚地了解了约束条件,我们可以尝试一个示例(可以是草图、逐步运行时可视化、项目列表等)。然后,我们找出算法/解决方案,最后,我们提供设计骨架。

示例 1:自动唱机

亚马逊谷歌

问题:设计自动唱机音乐机的主要类。

要问的问题:自动唱机播放什么-CD、MP3?我应该设计什么-自动唱机建造过程,它是如何工作的,还是其他什么?是免费的自动唱机还是需要钱?

面试官:免费的自动唱机只播放 CD 吗?设计它的主要功能,因此设计它是如何工作的。

解决方案:为了理解我们的设计应该涉及哪些类,我们可以尝试想象一台自动唱机并确定其主要部分和功能。沿着这里的线条画一个图表也有助于面试官了解您的思维方式。我建议您始终采取以书面形式将问题可视化的方法-草图是一个完美的开始:

图 6.1 – 自动唱机

图 6.1 – 自动唱机

因此,我们可以确定自动唱机的两个主要部分:CD 播放器(或特定的自动唱机播放机制)和用户命令的接口。CD 播放器能够管理播放列表并播放这些歌曲。我们可以将命令接口想象为一个由自动唱机实现的 Java 接口,如下面的代码所示。除了以下代码,您还可以使用这里的 UML 图:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/Jukebox/JukeboxUML.png

public interface Selector {
    public void nextSongBtn();
    public void prevSongBtn();
    public void addSongToPlaylistBtn(Song song);
    public void removeSongFromPlaylistBtn(Song song);
    public void shuffleBtn();
}
public class Jukebox implements Selector {
    private final CDPlayer cdPlayer;
    public Jukebox(CDPlayer cdPlayer) {
        this.cdPlayer = cdPlayer;        
    }            
    @Override
    public void nextSongBtn() {...}
    // rest of Selector methods omitted for brevity
}

CDPlayer是点唱机的核心。通过Selector,我们控制CDPlayer的行为。CDPlayer必须能够访问可用的 CD 和播放列表:

public class CDPlayer {
    private CD cd;
    private final Set<CD> cds;
    private final Playlist playlist;
    public CDPlayer(Playlist playlist, Set<CD> cds) {
        this.playlist = playlist;
        this.cds = cds;
    }                
    protected void playNextSong() {...}
    protected void playPrevSong() {...}   
    protected void addCD(CD cd) {...}
    protected void removeCD(CD cd) {...}
    // getters omitted for brevity
}

接下来,Playlist管理一个Song列表:

public class Playlist {
    private Song song;
    private final List<Song> songs; // or Queue
    public Playlist(List<Song> songs) {
        this.songs = songs;
    }   
    public Playlist(Song song, List<Song> songs) {
        this.song = song;
        this.songs = songs;
    }        
    protected void addSong(Song song) {...}
    protected void removeSong(Song song) {...}
    protected void shuffle() {...}    
    protected Song getNextSong() {...};
    protected Song getPrevSong() {...};
    // setters and getters omitted for brevity
}

UserCDSong类暂时被跳过,但你可以在名为点唱机的完整应用程序中找到它们。这种问题可以以多种方式实现,所以也可以尝试你自己的设计。

示例 2:自动售货机

亚马逊谷歌Adobe

问题:设计支持典型自动售货机功能实现的主要类。

要问的问题:这是一个带有不同类型硬币和物品的自动售货机吗?它暴露了功能,比如检查物品价格、购买物品、退款和重置吗?

面试官:是的,确实!对于硬币,你可以考虑一分硬币、五分硬币、一角硬币和一美元硬币。

解决方案:为了理解我们的设计应该涉及哪些类,我们可以尝试勾画一个自动售货机。有各种各样的自动售货机类型。简单地勾画一个你知道的(比如下图中的那种):

图 6.2 – 自动售货机

图 6.2 – 自动售货机

首先,我们立即注意到物品和硬币是 Java 枚举的良好候选。我们有四种硬币和几种物品,所以我们可以编写两个 Java 枚举如下。除了以下代码,你还可以使用这里的 UML 图表:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/VendingMachine/VendingMachineUML.png

public enum Coin {
    PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
    ...
}
public enum Item {
    SKITTLES("Skittles", 15), TWIX("Twix", 35) ...
    ...
}

自动售货机需要一个内部库存来跟踪物品和硬币的状态。我们可以将其通用地塑造如下:

public final class Inventory<T> {
    private Map<T, Integer> inventory = new HashMap<>();
    protected int getQuantity(T item) {...}
    protected boolean hasItem(T item) {...}
    protected void clear() {...}    
    protected void add(T item) {...}
    protected void put(T item, int quantity) {...}
    protected void deduct(T item) {...}
}

接下来,我们可以关注客户用来与自动售货机交互的按钮。正如你在前面的例子中看到的,将这些按钮提取到一个接口中是常见做法,如下所示:

public interface Selector {
    public int checkPriceBtn(Item item);
    public void insertCoinBtn(Coin coin);
    public Map<Item, List<Coin>> buyBtn();
    public List<Coin> refundBtn();
    public void resetBtn();    
}

最后,自动售货机可以被塑造成实现Selector接口并提供一堆用于完成内部任务的私有方法:

public class VendingMachine implements Selector {
    private final Inventory<Coin> coinInventory
        = new Inventory<>();
    private final Inventory<Item> itemInventory
        = new Inventory<>();
  private int totalSales;
    private int currentBalance;
    private Item currentItem;
    public VendingMachine() {
        initMachine();
    }   
    private void initMachine() {
        System.out.println("Initializing the
            vending machine with coins and items ...");
    }
    // override Selector methods omitted for brevity
}

完整的应用程序名为自动售货机。通过遵循前面提到的两个例子,你可以尝试设计一个 ATM、洗衣机和类似的东西。

示例 3:一副卡牌

亚马逊谷歌Adobe微软

问题:设计一个通用卡牌组的主要类。

要问的问题:由于卡可以是几乎任何东西,你能定义通用吗?

面试官:一张卡由一个符号(花色)和一个点数来描述。例如,想象一副标准的 52 张卡牌组。

解决方案:为了理解我们的设计应该涉及哪些类,我们可以快速勾画一个标准 52 张卡牌组的卡牌和一副卡牌,如图 6.3 所示:

图 6.3 – 一副卡牌

图 6.3 – 一副卡牌

由于每张卡都有花色和点数,我们将需要一个封装这些字段的类。让我们称这个类为StandardCardStandardCard的花色包括黑桃,红心,方块梅花,因此这个花色是 Java 枚举的一个很好的候选。StandardCard的点数可以在 1 到 13 之间。

一张卡可以独立存在,也可以是一副卡牌的一部分。多张卡组成一副卡牌(例如,一副标准的 52 张卡牌组形成一副卡牌)。一副卡牌中的卡的数量通常是可能的花色和点数的笛卡尔积(例如,4 种花色 x 13 个点数 = 52 张卡)。因此,52 个StandardCard对象形成了StandardPack

最后,一副牌应该是一个能够执行一些与这个“标准包”相关的操作的类。例如,一副牌可以洗牌,可以发牌或发一张牌,等等。这意味着还需要一个Deck类。

到目前为止,我们已经确定了一个 Java 的enumStandardCardStandardPackDeck类。如果我们添加了所需的抽象层,以避免这些具体层之间的高耦合,那么我们就得到了以下的实现。除了以下代码,您还可以使用这里的 UML 图:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/DeckOfCards/DeckOfCardsUML.png

  • 对于标准牌实现:
public enum StandardSuit {   
    SPADES, HEARTS, DIAMONDS, CLUBS;
}
public abstract class Card {
    private final Enum suit;
    private final int value;
    private boolean available = Boolean.TRUE;
    public Card(Enum suit, int value) {
        this.suit = suit;
        this.value = value;
    }
    // code omitted for brevity
}
public class StandardCard extends Card {
    private static final int MIN_VALUE = 1;
    private static final int MAX_VALUE = 13;
    public StandardCard(StandardSuit suit, int value) {
        super(suit, value);
    }
    // code omitted for brevity
}
  • 标准牌组实现提供以下代码:
public abstract class Pack<T extends Card> {
    private List<T> cards;
    protected abstract List<T> build();
  public int packSize() {
        return cards.size();
    }
    public List<T> getCards() {
        return new ArrayList<>(cards);
    }
    protected void setCards(List<T> cards) {
        this.cards = cards;
    }
}
public final class StandardPack extends Pack {
    public StandardPack() {
        super.setCards(build());
    }
    @Override
    protected List<StandardCard> build() {
        List<StandardCard> cards = new ArrayList<>();
        // code omitted for brevity        
        return cards;
    }
}
  • 牌组实现提供以下内容:
public class Deck<T extends Card> implements Iterable<T> {
    private final List<T> cards;
    public Deck(Pack pack) {
        this.cards = pack.getCards();
    }
    public void shuffle() {...}
    public List<T> dealHand(int numberOfCards) {...}
    public T dealCard() {...}
    public int remainingCards() {...}
    public void removeCards(List<T> cards) {...}
    @Override
    public Iterator<T> iterator() {...}
}

代码的演示可以快速写成如下:

// create a single classical card
Card sevenHeart = new StandardCard(StandardSuit.HEARTS, 7);       
// create a complete deck of standards cards      
Pack cp = new StandardPack();                   
Deck deck = new Deck(cp);
System.out.println("Remaining cards: " 
    + deck.remainingCards());

此外,您可以通过扩展CardPack类轻松添加更多类型的卡。完整的代码名为DeckOfCards

示例 4:停车场

亚马逊谷歌Adobe微软

问题:设计停车场的主要类。

需要询问的问题:这是单层停车场还是多层停车场?所有停车位是否相同?我们应该停放什么类型的车辆?这是免费停车吗?我们使用停车票吗?

面试官:这是一个同步自动多层免费停车场。所有停车位大小相同,但我们期望有汽车(需要 1 个停车位)、货车(需要 2 个停车位)和卡车(需要 5 个停车位)。其他类型的车辆应该可以在不修改代码的情况下添加。系统会释放一个停车票,以便以后用于取车。但是,如果司机只提供车辆信息(假设丢失了停车票),系统仍然应该能够工作并在停车场中找到车辆并将其取出。

解决方案:为了了解我们的设计应该涉及哪些类,我们可以快速勾画一个停车场,以识别主要的参与者和行为,如图 6.4 所示:

图 6.4 - 停车场

图 6.4 - 停车场

该图表显示了两个主要的参与者:停车场和自动停车系统。

首先,让我们专注于停车场。停车场的主要目的是停放车辆;因此,我们需要确定可接受的车辆(汽车、货车和卡车)。这看起来像是一个抽象类(Vehicle)和三个子类(CarVanTruck)的典型情况。但这并不是真的!司机提供有关他们的车辆的信息。他们并没有真正将车辆(对象)推入停车系统,因此我们的系统不需要为汽车、货车、卡车等专门的对象。从停车场的角度来看。它需要车辆牌照和停车所需的空闲车位。它不关心货车或卡车的特征。因此,我们可以将Vehicle塑造如下。除了以下代码,您还可以使用这里的 UML 图:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/ParkingLot/ParkingLotUML.png

public enum VehicleType {
    CAR(1), VAN(2), TRUCK(5);
}
public class Vehicle {
    private final String licensePlate;
    private final int spotsNeeded;
    private final VehicleType type;
    public Vehicle(String licensePlate, 
            int spotsNeeded, VehicleType type) {
        this.licensePlate = licensePlate;
        this.spotsNeeded = spotsNeeded;
        this.type = type;
    }
    // getters omitted for brevity    
    // equals() and hashCode() omitted for brevity
}

接下来,我们需要设计停车场。主要是,停车场有几层(或级别),每层都有停车位。除其他外,停车场应该暴露出停车/取车的方法。这些方法将把停车/取车的任务委托给每一层(或特定的一层),直到成功或没有要扫描的层为止。

public class ParkingLot {
    private String name;
    private Map<String, ParkingFloor> floors;
    public ParkingLot(String name) {
        this.name = name;
    }
    public ParkingLot(String name, 
            Map<String, ParkingFloor> floors) {
        this.name = name;
        this.floors = floors;
    }    
    // delegate to the proper ParkingFloor
    public ParkingTicket parkVehicle(Vehicle vehicle) {...}
    // we have to find the vehicle by looping floors  
    public boolean unparkVehicle(Vehicle vehicle) {...} 
    // we have the ticket, so we have the needed information
    public boolean unparkVehicle
        ParkingTicket parkingTicket) {...} 
    public boolean isFull() {...}
    protected boolean isFull(VehicleType type) {...}
    // getters and setters omitted for brevity
}

停车楼控制某一楼层的停车/取车过程。它有自己的停车票注册表,并能够管理其停车位。主要上,每个停车楼都充当独立的停车场。这样,我们可以关闭一个完整的楼层,而其余楼层不受影响:

public class ParkingFloor{
    private final String name;
    private final int totalSpots;
    private final Map<String, ParkingSpot>
        parkingSpots = new LinkedHashMap<>();
    // here, I use a Set, but you may want to hold the parking 
    // tickets in a certain order to optimize search
    private final Set<ParkingTicket>
        parkingTickets = new HashSet<>();
private int totalFreeSpots;
    public ParkingFloor(String name, int totalSpots) {
        this.name = name;
        this.totalSpots = totalSpots;
        initialize(); // create the parking spots
    }
    protected ParkingTicket parkVehicle(Vehicle vehicle) {...}     
    //we have to find the vehicle by looping the parking spots  
    protected boolean unparkVehicle(Vehicle vehicle) {...} 
    // we have the ticket, so we have the needed information
    protected boolean unparkVehicle(
        ParkingTicket parkingTicket) {...} 
    protected boolean isFull(VehicleType type) {...}
    protected int countFreeSpots(
        VehicleType vehicleType) {...}
    // getters omitted for brevity
    private List<ParkingSpot> findSpotsToFitVehicle(
        Vehicle vehicle) {...}    
    private void assignVehicleToParkingSpots(
        List<ParkingSpot> spots, Vehicle vehicle) {...}    
    private ParkingTicket releaseParkingTicket(
        Vehicle vehicle) {...}    
    private ParkingTicket findParkingTicket(
        Vehicle vehicle) {...}    
    private void registerParkingTicket(
        ParkingTicket parkingTicket) {...}           
    private boolean unregisterParkingTicket(
        ParkingTicket parkingTicket) {...}                    
    private void initialize() {...}
}

最后,停车位是一个对象,它保存有关其名称(标签或编号)、可用性(是否空闲)和车辆(是否停放在该位置的车辆)的信息。它还具有分配/移除车辆到/从此位置的方法:

public class ParkingSpot {
    private boolean free = true;
    private Vehicle vehicle;
    private final String label;
    private final ParkingFloor parkingFloor;
    protected ParkingSpot(ParkingFloor parkingFloor, 
            String label) {
        this.parkingFloor = parkingFloor;
        this.label = label;
    }
    protected boolean assignVehicle(Vehicle vehicle) {...}
    protected boolean removeVehicle() {...}
    // getters omitted for brevity
}

此刻,我们已经拥有了停车场的所有主要类。接下来,我们将专注于自动停车系统。这可以被塑造为一个作为停车场调度员的单一类:

public class ParkingSystem implements Parking {

    private final String id;
    private final ParkingLot parkingLot;
    public ParkingSystem(String id, ParkingLot parkingLot) {
        this.id = id;
 this.parkingLot = parkingLot;
    }
    @Override
    public ParkingTicket parkVehicleBtn(
        String licensePlate, VehicleType type) {...}
    @Override
    public boolean unparkVehicleBtn(
        String licensePlate, VehicleType type) {...}
    @Override
    public boolean unparkVehicleBtn(
        ParkingTicket parkingTicket) {...}     
    // getters omitted for brevity
}

包含部分实现的完整应用程序被命名为ParkingLot

示例 5:在线阅读系统

问题:设计在线阅读系统的主要类。

需要询问的问题:需要哪些功能?可以同时阅读多少本书?

面试官:系统应该能够管理读者和书籍。您的代码应该能够添加/移除读者/书籍并显示读者/书籍。系统一次只能为一个读者和一本书提供服务。

解决方案:为了理解我们的设计应该涉及哪些类,我们可以考虑草绘一些东西,如图 6.5 所示:

图 6.5 – 一个在线阅读系统

图 6.5 – 一个在线阅读系统

为了管理读者和书籍,我们需要拥有这样的对象。这是一个小而简单的部分,在面试中从这样的部分开始对打破僵局和适应手头的问题非常有帮助。当我们在面试中设计对象时,没有必要提出一个对象的完整版本。例如,一个读者有姓名和电子邮件,一本书有作者、标题和 ISBN 就足够了。让我们在下面的代码中看到它们。除了下面的代码,您还可以使用这里的 UML 图:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/OnlineReaderSystem/OnlineReaderSystemUML.png

public class Reader {
    private String name;
    private String email;
    // constructor omitted for brevity
    // getters, equals() and hashCode() omitted for brevity
}
public class Book {
    private final String author;
    private final String title;
    private final String isbn;
    // constructor omitted for brevity
    public String fetchPage(int pageNr) {...}
    // getters, equals() and hashCode() omitted for brevity
}

接下来,如果我们考虑到书籍通常由图书馆管理,那么我们可以将添加、查找和移除书籍等多个功能封装在一个类中,如下所示:

public class Library {
    private final Map<String, Book> books = new HashMap<>();
    protected void addBook(Book book) {
       books.putIfAbsent(book.getIsbn(), book);
    }
    protected boolean remove(Book book) {
       return books.remove(book.getIsbn(), book);
    }
    protected Book find(String isbn) {
       return books.get(isbn);
    }
}

读者可以由一个名为ReaderManager的类来管理。您可以在完整的应用程序中找到这个类。为了阅读一本书,我们需要一个显示器。Displayer应该显示读者和书籍的详细信息,并且应该能够浏览书籍的页面:

public class Displayer {
    private Book book;
    private Reader reader;
    private String page;
    private int pageNumber;
    protected void displayReader(Reader reader) {
        this.reader = reader;
        refreshReader();
    }
    protected void displayBook(Book book) {
        this.book = book;
        refreshBook();
    }
    protected void nextPage() {
        page = book.fetchPage(++pageNumber);
        refreshPage();
    }
    protected void previousPage() {
        page = book.fetchPage(--pageNumber);        
        refreshPage();
    }        
    private void refreshReader() {...}    
    private void refreshBook() {...}    
    private void refreshPage() {...}
}

最后,我们所要做的就是将LibraryReaderManagerDisplayer封装在OnlineReaderSystem类中。这个类在这里列出:

public class OnlineReaderSystem {
    private final Displayer displayer;
    private final Library library;
    private final ReaderManager readerManager;
    private Reader reader;
    private Book book;
    public OnlineReaderSystem() {
        displayer = new Displayer();
        library = new Library();
        readerManager = new ReaderManager();
    }
    public void displayReader(Reader reader) {
        this.reader = reader;
        displayer.displayReader(reader);
    }
    public void displayReader(String email) {
        this.reader = readerManager.find(email);
        if (this.reader != null) {
            displayer.displayReader(reader);
        }
    }
    public void displayBook(Book book) {
        this.book = book;
        displayer.displayBook(book);
    }
    public void displayBook(String isbn) {
        this.book = library.find(isbn);
        if (this.book != null) {
            displayer.displayBook(book);
        }
    }
    public void nextPage() {
        displayer.nextPage();
    }
    public void previousPage() {
        displayer.previousPage();
    }
    public void addBook(Book book) {
        library.addBook(book);
    }
    public boolean deleteBook(Book book) {
        if (!book.equals(this.book)) {
            return library.remove(book);
        }
        return false;
    }
    public void addReader(Reader reader) {
        readerManager.addReader(reader);
    }
    public boolean deleteReader(Reader reader) {
        if (!reader.equals(this.reader)) {
            return readerManager.remove(reader);
        }
        return false;
    }
    public Reader getReader() {
        return reader;
    }
    public Book getBook() {
        return book;
    }
}

完整的应用程序名为OnlineReaderSystem

示例 6:哈希表

亚马逊谷歌Adobe微软

问题:设计一个哈希表(这是面试中非常流行的问题)。

需要询问的问题:需要哪些功能?应该应用什么技术来解决索引冲突?键值对的数据类型是什么?

add()get()操作。为了解决索引冲突,我建议您使用链接技术。键值对应该是通用的。

哈希表的简要概述:哈希表是一种存储键值对的数据结构。通常,数组保存表中所有键值条目,该数组的大小设置为容纳预期数据量。每个键值的键通过哈希函数(或多个哈希函数)传递,输出哈希值或哈希。主要,哈希值表示哈希表中键值对的索引(例如,如果我们使用数组存储所有键值对,则哈希函数返回应该保存当前键值对的数组的索引)。通过哈希函数传递相同的键应该每次产生相同的索引 - 这对于通过其键查找值很有用。

当哈希函数为不同的键生成两个相同的索引时,我们面临索引冲突。解决索引冲突问题最常用的技术是线性探测(这种技术在表中线性搜索下一个空槽位 - 尝试在数组中找到一个不包含键值对的槽位(索引))和chaining(这种技术表示作为链表数组实现的哈希表 - 冲突存储在与链表节点相同的数组索引中)。下图是用于存储名称-电话对的哈希表。它具有chaining功能(检查马里乌斯-0838234条目,它被链接到卡琳娜-0727928,因为它们的键马里乌斯卡琳娜导致相同的数组索引126):

图 6.6 - 哈希表

图 6.6 - 哈希表

HashEntry)。正如您在前面的图中所看到的,键值对有三个主要部分:键、值和指向下一个键值对的链接(这样,我们实现chaining)。由于哈希表条目应该只能通过专用方法(如get()put())访问,因此我们将其封装如下:

public class HashTable<K, V> {
    private static final int SIZE = 10;
    private static class HashEntry<K, V> {
        K key;
        V value;
        HashEntry <K, V> next;
        HashEntry(K k, V v) {
            this.key = k;
            this.value = v;
            this.next = null;
        }        
    }
    ...

接下来,我们定义包含HashEntry的数组。为了测试目的,大小为10的元素足够了,并且可以轻松测试chaining(大小较小容易发生碰撞)。实际上,这样的数组要大得多:

private final HashEntry[] entries 
        = new HashEntry[SIZE];
    ...

接下来,我们添加get()put()方法。它们的代码非常直观:

    public void put(K key, V value) {
        int hash = getHash(key);
        final HashEntry hashEntry = new HashEntry(key, value);
        if (entries[hash] == null) {
            entries[hash] = hashEntry;
        } else { // collision => chaining
            HashEntry currentEntry = entries[hash];
            while (currentEntry.next != null) {
                currentEntry = currentEntry.next;
            }
            currentEntry.next = hashEntry;
        }
    }
    public V get(K key) {
        int hash = getHash(key);
        if (entries[hash] != null) {
            HashEntry currentEntry = entries[hash];
            // Loop the entry linked list for matching 
            // the given 'key'
            while (currentEntry != null) {                
                if (currentEntry.key.equals(key)) {
                    return (V) currentEntry.value;
                }
                currentEntry = currentEntry.next;
            }
        }
        return null;
    }

最后,我们添加一个虚拟哈希函数(实际上,我们使用诸如 Murmur 3 之类的哈希函数 - en.wikipedia.org/wiki/MurmurHash):

    private int getHash(K key) {        
        return Math.abs(key.hashCode() % SIZE);
    }    
}

完成!完整的应用程序名为HashTable

对于以下四个示例,我们跳过了书中的源代码。花点时间分析每个示例。能够理解现有设计是塑造设计技能的另一个工具。当然,您可以在查看书中代码之前尝试自己的方法,并最终比较结果。

示例 7:文件系统

问题:设计文件系统的主要类。

要问的问题:需要哪些功能?文件系统的组成部分是什么?

面试官:您的设计应支持目录和文件的添加、删除和重命名。我们谈论的是目录和文件的分层结构,就像大多数操作系统一样。

解决方案:完整的应用程序名为FileSystem。请访问以下链接以查看 UML:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/FileSystem/FileSystemUML.png

示例 8:元组

亚马逊谷歌

问题:设计一个元组数据结构。

要问的问题:元组可以有 1 到n个元素。那么,您期望什么样的元组?元组中应存储什么数据类型?

面试官:我期望一个包含两个通用元素的元组。元组也被称为pair

解决方案:完整的应用程序名为Tuple

示例 9:带有电影票预订系统的电影院

亚马逊,谷歌,Adobe,微软

问题:设计一个带有电影票预订系统的电影院。

要问什么:电影院的主要结构是什么?它有多个影厅吗?我们有哪些类型的票?我们如何播放电影(只在一个房间,每天只播放一次)?

面试官:我期望一个有多个相同房间的电影院。一部电影可以同时在多个房间播放,同一部电影一天内可以在同一个房间播放多次。有三种类型的票,简单、白银和黄金,根据座位类型。电影可以以非常灵活的方式添加/移除(例如,我们可以在特定的开始时间从某些房间中移除一部电影,或者我们可以将一部电影添加到所有房间)。

解决方案:完整的应用程序名为 MovieTicketBooking。请访问以下链接查看 UML:github.com/PacktPublishing/The-Complete-Coding-Interview-Guide-in-Java/blob/master/Chapter06/MovieTicketBooking/MovieTicketBookingUML.png

示例 10:循环字节缓冲区

亚马逊,谷歌,Adobe

问题:设计一个循环字节缓冲区。

要问什么:它应该是可调整大小的?

面试官:是的,它应该是可调整大小的。主要是,我希望你设计所有你认为必要的方法的签名。

解决方案:完整的应用程序名为 CircularByteBuffer。

目前为止一切顺利!我建议你也尝试为前面的 10 个问题设计你自己的解决方案。不要认为所提供的解决方案是唯一正确的。尽可能多地练习,通过改变问题的背景来挑战自己,也尝试其他问题。

本章的源代码包名称为 Chapter06。

总结

本章涵盖了关于面向对象编程基础知识的最受欢迎的问题和 10 个设计编码挑战,在面试中非常受欢迎。在第一部分,我们从面向对象的概念(对象、类、抽象、封装、继承、多态、关联、聚合和组合)开始,继续讲解了 SOLID 原则,并以结合了面向对象编程概念、SOLID 原则和设计模式知识的问题为结束。在第二部分,我们解决了 10 个精心设计的设计编码挑战,包括设计点唱机、自动售货机和著名的哈希表。

练习这些问题和问题将使你有能力解决面试中遇到的任何面向对象编程问题。

在下一章中,我们将解决大 O 符号和时间的问题。