给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
# @author:leacoder
# @des: 暴力循环 两数之和 O(N*N)
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for index1,num1 in enumerate(nums): #两层循环
for index2,num2 in enumerate(nums[index1+1:]):
if num1 + num2 == target:
return [index1,index1+index2+1]
return []
# @author:leacoder
# @des: 和固定利用差值diff 两数之和 O(N)
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)-1):#循环 O(N)
diff = target - nums[i]
if diff in nums[i+1:]: #O(1)
return [i,nums[i+1:].index(diff)+i+1]
return []
GitHub链接: https://github.com/lichangke/LeetCode
个人Blog: https://lichangke.github.io/
欢迎大家来一起交流学习
原文: 深度学习框架的来龙去脉——史上最全面最新的深度学习框架对比分析 原作者: 神奇博士
这是一篇很长的文章,为了看完这篇文章,单单从头到尾鼠标的滑轮就要滚动了30多次 -_-!。但我还是认认真真从头到尾读了一遍。 为什么呢?也许是曾经与深度学习擦肩而过过。还记得2016年底,公司项目组PL组织内部的深度学习自学习,当时自己挺感兴趣,除了组织的学习外自己从网上找资料,实现了使用Python语言 基于TensorFlow处理图片风格转换(将A图片转换为B图片的风格),受限于电脑设备现在还对当时处理速度之慢记忆犹新,也许当时能继续研究下去就好了……,可是没有也许,不久PL调走了新的PL对着压根就不关注,慢慢地自己也就没有投入了,当时收集的资料笔记代码现在还躺在公司电脑中。 现在我依旧对深度学习感兴趣,所以分享了这篇文章给同样有这爱好的人,同时也是分享给自己。告诫自己有兴趣就要坚持,哪怕无人支持。
人工智能从学术理论研究到生产应用的产品化开发过程中通常会涉及到多个不同的步骤和工具,这使得人工智能开发依赖的环境安装、部署、测试以及不断迭代改进准确性和性能调优的工作变得非常繁琐耗时也非常复杂。为了简化、加速和优化这个过程,学界和业界都作了很多的努力,开发并完善了多个基础的平台和通用工具,也被称会机器学习框架或深度学习框架。有了这些基础的平台和工具,我们就可以避免重复发明轮子,而专注于技术研究和产品创新。这些框架有早期从学术界走出的Caffe、 Torch和Theano,到现在产业界由Google领导的TensorFlow,Amazon选择押注的MXNet,Facebook倾力打造的PyTorch,Microsoft内部开源的CNTK等等。
当前主流的深度学习框架列表:
看起来我们好像有很多很多选择,但其实如果我们进一步进行细分,就会发现我们的选择也并不是很多,没有巨头背书的框架就只能面临被淘汰和边缘化的命运了,其实顶级深度学习框架只有四大阵营,或者说是四大门派,Google领导的TensorFlow,Amazon选择的MXnet,Facebook倾力打造的PyTorch,Microsoft把内部核心技术开源的CNTK。虽然Keras等框架在深度学习框架中排名很高,但它却不是一个独立框架,而是作为前端对底层引擎进行上层封装的高级API层,提升易用性,此类深度学习框架的目标是只需几行代码就能让你构建一个神经网络,这类框架还有FastAI和Gluon。好在每一个前端上层轻量级框架又都对应一个最适合的基础底层框架,这样就出现了深度学习框架的四大技术方向,每一个技术方向背后又都有一个巨头在背书和推动。
深度学习框架的四大阵营与其技术方向分别为: (1)TensorFlow,前端框架Keras,背后巨头Google; (2)PyTorch,前端框架FastAI,背后巨头Facebook; (3)MXNet,前端框架Gluon,背后巨头Amazon; (4)Cognitive Toolkit (CNTK),前端框架Keras或Gluon,背后巨头Microsoft。
原作者依次就少了以下主流开源深度学习框架的来龙去脉: TensorFlow、Keras、PyTorch、Theano、Caffe、Caffe2、Torch、FastAI、MXNet、Gluon、CNTK、DL4J、Chainer 、ONNX 点击这里读原文
进入深度学习领域,基础是学习Python。可以说现在进入深度学习领域是相对容易的,在5年前,研究深度学习需要用C++或Matlab来编写大量的低级算法,这需要研究生教育甚至是博士的教育。现在不一样了,你只需要学习Python,就很容易上手,虽然深度学习正在支持越来越多的编程语言,但Python最简单而且应用最广泛的一个,Python最厉害的地方在于其生态系统非常好,有社区的强大支持,比如要装Python,有方便的Anaconda;要用Python visualization,有Matplotlib可以用;要Numerical computation有NumPy和SciPy可以选择,要做图像处理,还有Scikit-image。有很多现成的工具可以使用,可以节省自己大量的时间,这正是工程师所需要的。
在对所有主流深度学习框架有一个了解后,我想是时候舍弃开发语言(基本都支持Python和C++,Java和Lua面向特定社区)、接口简易、文档完善、运算速度、性能、安装部署方便等方面的纯技术比较了,可能在这些框架诞生的初期我们更看重这些方面,但是随着各个框架的不断的完善与大企业的支持与不断的投入,各个框架之间也在不断的相互借鉴,最后的结果就是大家都差不多,各有千秋,我们现在要进入深一层维度的比拼,应该至少考虑下面几个维度:
深度学习框架是否支持分布式计算,是不是分布式框架? 分布式:TensorFlow、MXNet、PyTorch、CNTK、Caffe2、DL4J 不支持分布式:Caffe、Theano、Torch
深度学习框架是否支持移动端部署? 支持:PyTorch、MXNet、TensorFlow、Caffe2 不支持:CNTK
编程接口的设计是命令式编程(imperative programming)还是声明式语言(declarative programing)? 命令式:简单易懂的编程接口PyTorch,NumPy和Torch、Theano MXNet通过NDarray模块和Gluon高级接口提供了非常类似PyTorch的编程接口。 声明式:TensorFlow、Theano、Caffe
深度学习框架是基于动态计算图还是静态计算图? 目前使用动态计算图的框架有PyTorch、MXNet、Chainer。 目前使用静态计算图框架有TensorFlow、Keras、CNTK、Caffe/Caffe2、Theano等,其中TensorFlow主要使用了静态计算图,TensorFlow在2018年10月宣布了一个动态计算选项Eager Execution,但该特性还比较新颖可能并不是很成熟,并且 TensorFlow 的文档和项目依然以静态计算图为主。MXNet同时具有动态计算图和静态计算图两种机制。
深度学习框架是否有强大的社区和生态支持? 重金打造的TensorFlow,多方押注的MXNet,正在崛起的PyTorh,技术稳重的CNTK, 这四大开源深度学习框架都满足这一点。
深度学习框架背后是否有巨头支持? Google领导的TensorFlow,Amazon选择的MXNet,Facebook倾力打造的PyTorch,Microsoft把内部核心技术开源的CNTK,这四大开源深度学习框架都满足这一点。
通过对上面六个维度的思考,我想大家应该知道该如何作选择了:首先,静态计算图很好,但是动态图是未来和趋势,对于大多数开发者来说,Python是基础,Python的成熟可用的库、工具和生态与社区的支持太重要了;对于深度学习的商业应用而非纯粹的实验室研究来说,支持分布式和移动端运行平台是必选的,将来一定会用到的;前端的编程接口越灵活超好,我们需要考虑不同的应用场景,因此前端编程接口的设计需要兼容简单高效的命令式和逻辑清晰的声明式;深度学习框架一定要有背后巨头的大力支持和强大的社区,有专业的团队不断的更新并完善代码库。这样来看,只有下面的四大顶级深度学习框架阵营才能够满足我们的要求。 深度学习框架的四大阵营与其技术方向分别为: (1)TensorFlow,前端框架Keras,背后巨头Google; (2)PyTorch,前端框架FastAI,背后巨头Facebook; (3)MXNet,前端框架Gluon,背后巨头Amazon; (4)Cognitive Toolkit (CNTK),前端框架Keras或Gluon,背后巨头Microsoft。
那么在这四大阵营中又如何选择呢?这就要看具体项目的需要了,看重Google无与伦比的巨大影响力的开发者并不需要太过纠结,TensorFlow会支持最广泛的开发语言与最多的运行平台,开发者很难逃出Google的覆盖范围,更多的开发者会被收编,AlphaGo已经帮助Google证明了Google在人工智能上技术领先地位,Keras+TensorFlow的方案已经被Google官方认可,Google的TensorFlow2.0将带来的新技术与突破;喜欢学习新事物和追求完美的开发者一定不能错过Facebook的PyTorch,PyTorch正在强势崛起,是动态图技术的最佳代表,是当前最活跃最有生命力的深度学习框架,这一次Google遇到了真正的对手;Amazon在云计算和云服务上的领先地位带给开发者更大的信心,选择Amazon人工智能背后的技术一定没有错;微软的技术正在不断挑战人类语音识别和图像识别的极限,长期受益于微软阵营的开发人员对于微软开源其核心技术是非常兴奋的,Cognitive Toolkit (CNTK)可以被Keras和Gluon同时支持,这太棒了,确实带给开发者更多的选择。
最后,我们会发现深度学习框架其实只是一个工具和平台,虽然分为四大阵营和四大技术路线,但是得益于这些主流框架之间的不停的比拼与互相借鉴,最后会发现其实大家都差不多,最棒的是这些主流的深度学习框架都是基于Python的,只要掌握了Python和深度学习算法的设计思想,每一种框架都是一个可用的库或工具集,我们是工程师,工程师需要善于学习并善于选择使用最优的工具。初学者可以从上层高级API框架开始学习,如Keras、Gluon和FastAI,但是不能依赖这些层层封装高级API,不然是无法真正掌握深度学习的技术本质的。深入学习并熟练掌握一种顶级深度学习框架是非常重要的,比如PyTorch,然后再跑一跑TensorFlow和MXNet,我们可以在对比中学习,在深度学习领域,可以深刻理解什么是“纸上得来终觉浅”,我觉得学习深度学习及人工智能技术,一定要动手实践,只有动手做过了才是自己的,不然,一切都还是书本上的。
GitHub链接: https://github.com/lichangke/LeetCode
个人Blog: https://lichangke.github.io/
欢迎大家来一起交流学习
Flask和Django是Python最流行的两个Web框架,在这篇文章中,作者讨论在Flask和Django之间进行选择时应该考虑的一些要点。作者长期使用Flask并是Flask by Example这本书的作者,所以作者个人可能对Flask有点偏见。 为什么我需要一个Web框架,以及那是什么。 Web框架旨在实现大多数Web应用程序常用的所有功能,例如将URL映射到Python代码块。 确切地说,框架中实现的内容以及应用程序开发人员编写的内容因框架而异。 Flask和Django之间的最大区别是: Flask实现了最低限度的功能,并为附加组件或开发人员留下接口 Django遵循“batteries included”的理念,为您提供更多开箱即用的功能。 快速比较 主要对比: Flask提供简单,灵活和细粒度的控制。它可以让你决定如何实现它。 Django提供全面的体验:您可以获得开箱即用的应用程序和项目的管理面板,数据库界面,ORM和目录结构。 你应该选择: Flask,如果您专注于体验和学习机会,或者您想要更多地控制使用哪些组件(例如您想要使用哪些数据库以及如何与它们进行交互)。 Django,如果你专注于最终产品。特别是如果你正在开发一个直接的应用程序,如新闻网站,电子商店或博客,你希望总是有一种明显的做事方式。 更多信息: Django已经存在了很长时间 - 它于2005年首次发布,而Flask于2010年首次亮相 Hello, World! 作者分别就使用Flask和Django来实现一个“Hello World”应用程序,按步骤进行了具体说明。可以从中体会Flask和Django的差别。 最后的评论 在这篇文章中,作者介绍了Flask和Django,然后对两者进行了简短的比较,接着展示了如何使用每个框架构建一个“Hello World”应用程序。 Django是一个比Flask更重的框架 - 如果你正在学习Web编程,那么要弄清楚哪些部分负责什么功能,以及你需要改变什么来获得你想要的结果可能更难。但是,一旦你习惯了Django,它所做的额外工作可能非常有用,可以节省你设置Web应用程序重复,枯燥的组件的时间。 有时很难在两个框架之间进行选择 - 好的是,即使你进入更高级的功能,例如模板,这两个在许多方面仍然非常相似。 因此,如果您需要或想要,可以轻松地从一个切换到另一个。
为什么学习更多并不总是更好? Knowledge is power.这是一个流行的短语 - 在人与人之间重复,很少考虑其真正的意义。 也许知识本身并不是我们所追求的,也许我们追求的是获取足够的知识来能够采取明智的行动。 1.知道不是学习的同义词 大目标通常需要的不仅仅是死记硬背;他们要求更深入地了解核心原则。但是我们中的许多人仍然将学习与记忆事实,公式和概念联系在一起。过多的没有实践的知识会导致压力,沮丧和误导。最终,我们将意识到我们已经被欺骗了:没有实践,知识就没有力量。对于信息的保留率,仅通过阅读,信息保留了10%;付诸练习,信息保留了75%,而能向他人传授,信息保留了90%.获取知识的过程看起来应该这样:读/听/观看→执行→评估→重复 2.强迫学习会削弱创造力。 孩子们不会因小挫折感到沮丧,因为他们没有失败的概念;他们一直在尝试,因为那很有趣。我们都是曾是孩子。我们的天性引导我们学习语言,学习社交,研究我们认为重要的事情。直到上学才开始让学习成为一件苦差事。换句话说,学习是自然而然的,直到我们强迫它。最大限度地学习我们喜欢的感兴趣的知识,而不是强迫自己去学习,要让学习知识回到我们牙牙学语时,那样的自然而然而非强迫学习。 3.有时候我们需要忘掉。 可以理解的是,我们大多数人认为学习就是增加知识和技能。然而,有时我们必须删除旧的思维方式才能开始新事物。经验之谈是有必要的,但是我们得承认过多的依赖已学习的知识,有时并不利于学习新的知识。当新知识与旧知识有不同时,我们应该问自己为什么会有不同之处时,而不是简单地套用旧知识的方法去学习新知识。 4.有时委托是个更好的选择 每天的时间有限人的精力也是有限的,我们不可能也没必要了解所有的知识,术业有专攻,将事情委托给更专业技能的人事是明智的选择,我们就能将时间和精力放在真正需要学习的知识上。 成功的关键似乎在于调整我们的知识获取,同时将我们获得的信息付诸实践。
文章介绍了C编程语言的特点以及优缺点
作者在文中解决了三个关键概念。1. “I don’t have enough time”,第一个是那些认为他们没有足够时间的人。2. The benefits of keeping your job, 第二是强调坚持工作的好处。3. Restructuring your way of thinking,我们可以在思考方法中做出的一些改进。 大多数人高估了他们一天所能做的事情,但低估了他们一年可以做些什么。 做事要有重点,如果一切都是优先事项,那么没有什么是优先事项。保持想法新鲜,保持头脑清晰。优化你的生活。不要抱有一夜成名的神话。不要把工作之外的时间“me time”,理所当然地去放松去挥霍,如果使用得当,每年将近3000小时可以去实现很多东西
XY问题是询问您尝试的解决方案而不是实际问题。通常是这个样子的,我们想做X,但是我们不知道如何做X,我们认为如果能够做到Y,那么我们可以摸索X的解决方案。 可是很多时候我们也不知道如何做Y. 然后我们去寻求做Y的帮助。无论是寻求帮助的人还是提供帮助的人,这会导致大量的时间和精力浪费。所以问问题要问X求X,不要问的Y实际上我是想解决X
阅读了耗子叔推荐的一篇自学界神文 中英对照版 建议还是看原版虽然是英文但是不难。编程(不只是编程)没有捷径,不要想着市面上标题着《7天搞定……》《24小时自学……》这些书籍能给你多大提高,这种书籍最多也就让你有个粗浅的印象,但是绝对不可能有深入的理解,那种浅尝辄止是很危险的。要深入要精通要成为专家,你得花时间耗精力,加上科学正确的方法之外没有捷径可言。
作者通过拐点(The Inflection Point)一说,在学习编程中存在最消磨人意志的阶段,那就是拐点阶段,从各种角度来说,也是唯一重要的阶段。当你不再依靠教程,开始解决没人提供解决方案的问题时,这就是拐点阶段。 拐点阶段,你编程的速度可能只有接受指导阶段的十分之一到二十分之一,这时你会产生疑问:我是否能成为一个程序员。在拐点阶段产生不安和疑惑是正常的。尽管你发现学习新东西和敲代码的速度大幅下降,但实际上,你正在完成最重要的事情。在特定领域的知识足够丰富的时候,你所学的东西实际上是过程性知识(https://en.wikipedia.org/wiki/Procedural_knowledge)。过程性知识指的是教会自己原本不知道的东西的能力。 高效率征服拐点,需要在接受指导阶段,除了学习结构性的指导材料,还要全程给自己一些挑战,尝试尽可能少的依赖教学材料,了解越过拐点是一个困难的过程,别给自己太大压力,拐点过程的最后阶段是接受。接受软件开发是一个持续学习的过程。如果你感觉已经学习了所有东西,只意味着你应该想一想如何解决更复杂的问题。 https://www.jianshu.com/p/a140c8475933
GitHub链接: https://github.com/lichangke/LeetCode
个人Blog: https://lichangke.github.io/
欢迎大家来一起交流学习
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口 k 内的数字。滑动窗口每次只向右移动一位。
返回滑动窗口最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
注意:
你可以假设 k 总是有效的,1 ≤ k ≤ 输入数组的大小,且输入数组不为空。
进阶:
你能在线性时间复杂度内解决此题吗?
# @author:leacoder
# @des: 双端队列 滑动窗口最大值
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums: return []
window ,result = [],[] #window 存在窗口中的数的 下标 result用于存最后的结果
for i, numx in enumerate(nums):
"""
1.判断元素是否超出滑动窗口范围
i > k 说明滑动窗口不为空,window[0] < i - k说明最大元素超出了窗口,这时候必须舍弃
队首元素:window.pop(0)
"""
if i>=k and window[0]<=i-k: #新数据来时每次将window最左边的pop掉window[0]放的是最大而不是最左边界所以需要判断
window.pop(0) #不满足 window内条件
while window and nums[window[-1]]<=numx: #维护window 保持k的范围内window最大数始终在windowp[0]
window.pop() #window中如果有比新进numx小的 pop掉(我们要的是窗口内最大)
window.append(i)
#将新进numx 下标加入window window中最大数 始终是window[0]
#因为上方while循环已经保证在append新进数时 window中要么为空,新进入数最大
#要么比新进数入小的已pop掉留下比新进入数大的数放在头部
if i>=k-1: #下标从0开始 顾 i=k-1时 window中已处理过k个数了
result.append(nums[window[0]])
return result
'''
巧妙运用了window大小固定,并且 新进入数 如果比之前window中已有数都大的话,那么之前的数永远不可能是我们需要的数(滑动窗口最大值)
1 3 -1 -3 5 3 6 7 为nums k=3为例
假设已到新数进入前 窗口为 1 [3 -1 -3] 5 3 6 7, 现在新数 5 下标为四进入,按着上面代码逻辑
1、pop掉窗口最最左边数 3 下标为一 被pop
2、此时window中为[ -1 -3 ]的下标,循环比较 新数 5 大于 -1 -3 顾pop掉。此时window为空 跳出循环
3、将新数 5下标为四 append入window (存放下标) 此时window为[四] 1 3 [-1 -3 5] 3 6 7
4、这时窗口中最大值 为window[0]为下标的数
5、新进数3下标五 进入,-1 已不在window内了不需pop,window中只有5下标为四 不需要pop任何数据,将3 下标五append入window(存放下标) 此时window为[四 五] 1 3 -1 []-3 5 3] 6 7 窗口中最大值 依旧为window[0]为下标的数
'''
/*
@author:leacoder
@des: 优先队列 滑动窗口最大值
PriorityQueue 默认是一个小顶堆
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(k==0){
return nums;
}
PriorityQueue<Integer> queue = new PriorityQueue<>(k, (a, b) -> {return b-a;});//优先队列 顶为最大
for (int i = 0; i < k; i++) {
queue.add(nums[i]);
}
int[] res = new int[nums.length - k + 1];//存放结果
for (int i = 0; i < res.length; i++) {
res[i] = queue.peek(); //从取优先队列取出最大
queue.remove(nums[i]);//删除 优先队列中nums[i]
if (i + k < nums.length) {
queue.add(nums[i + k]); //将新进入数 加入优先队列中
}
}
return res;
}
}
/*
PriorityQueue 默认是一个小顶堆,如何实现大顶堆
1、
PriorityQueue<Integer> pq = new PriorityQueue<>(n,(Integer a,Integer b)->{return b-a;});
2、
PriorityQueue<Integer> pq = new PriorityQueue<>(n, new Comparator<Integer>() {
@Override
public int compare(Integer integer, Integer t1) {
return t1-integer;
}
});
*/
GitHub链接: https://github.com/lichangke/LeetCode
个人Blog: https://lichangke.github.io/
欢迎大家来一起交流学习
原文:Goodbye, Object Oriented Programming 原作者:Charles Scalfani
作为使用面向对象编程已有十几年的作者,曾经也非常热衷于继承、封装、多态的优点。它们是范式的三大支柱。但是随着对面向对象编程的深入理解,结合实际遇到的问题,带着挑剔的眼光,作者看到了面向对象编程的一些问题。
你想要一个香蕉,但是你得到的却是一只拿着香蕉的大猩猩和整个丛林。
由于继承原因,当你想要复用其他项目某一个已经存在的类时,你不得不需要这个类的父类,然后可能它父类的父类……最后会发现我需要它的祖宗十八代=。=。你以为解决了这个就可以了吗?少年你还是太天真了,你会发现,它编译不过,为什么? 这个对象包含了这个其他对象。 所以你也需要它,这没问题,但问题是你不只是需要那个对象,你需要对象的父对象及其父对象的父对象,依此类推,每个包含的对象以及包含父对象,父对象,父对象的所有父对象……
我们可以通过不创建太深的层次结构来解决这个问题。哦……似乎没什么不对,但如果继承是重用的关键,那么我对该机制的任何限制肯定会限制重用的好处。
早晚你会遇到下面这种恶心的问题,有些语言甚至根本解决不了。 大多数面向对象语言都不支持这种情况,尽管看上去似乎很符合逻辑。为什么面向对象语言支持这种情况如此困难?
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
function start() {
}
}
Class Printer inherits from PoweredDevice {
function start() {
}
}
Class Copier inherits from Scanner, Printer {
}
请注意,Scanner类和Printer类都实现了一个名为start的函数。那么Copier类继承了哪个启动函数? 扫描仪在? 打印机一个? 它不可能两者兼而有之。
解决方案很简单:不要这样做。没错。大多数面向对象都不让你这么干。但是,但是……要是必须这样建模该怎么办?我需要重用!那就必须使用包含和委托。
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
function start() {
}
}
Class Printer inherits from PoweredDevice {
function start() {
}
}
Class Copier {
Scanner scanner
Printer printer
function start() {
printer.start()
}
}
这里注意Copier类现在包含了Printer类和Scanner类的实例。Copier委派Printer类的start方法去实现自己的start方法,它也可以简单的委派给Scanner类的start方法。 在C++我们可以采用虚继承,这个网上搜索下很多讲解。
我们尽量使用较浅的类层次结构,并保证里面没有环,这样就不会出现菱形继承了。 似乎一切都解决了。直到我们发现…… 我前一天工作得好好的代码今天出错了!关键是,我没有改任何代码! 嗯也许是个 bug……但等等……的确有些改动…… 但改动的不是我的代码。似乎改动来自我继承的那个类。 基类的改动怎么让我的代码挂了呢? 看看下面这个基类
import java.util.ArrayList;
public class Array
{
private ArrayList<Object> a = new ArrayList<Object>();
public void add(Object element)
{
a.add(element);
}
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
a.add(elements[i]); // this line is going to be changed
}
}
注意被注释的那一行,那一行改动会让代码挂掉。 下面是派生类
public class ArrayCount extends Array
{
private int count = 0;
@Override
public void add(Object element)
{
super.add(element);
++count;
}
@Override
public void addAll(Object elements[])
{
super.addAll(elements);
count += elements.length;
}
}
Array的add()添加一个元素到本地的ArrayList Array的addAll()调用本地的ArrayList的add()方法添加循环的每一个元素。 ArrayCount的add()方法调用了父类的add()然后count变量+1。 ArrayCount的addAll()方法调用父类的addAll()然后加上元素数组的长度。 基类中加注释的那行代码现在改成这样,问题就出现了:
public void addAll(Object elements[])
{
for (int i = 0; i < elements.length; ++i)
add(elements[i]); // this line was changed
}
从基类的作者的角度来看,这个类实现的功能完全没有变化。而且所有自动化测试也都通过来了。但是基类的作者忘记了继承的类。而继承类的作者就被坑了T_T。
现在ArrayCount的addAll()调用父类的addAll(),后者在内部调用add(),而add()被继承类重载了。 因此,每次继承类的add()被调用时,count都会增加,然后在继承类的addAll()被调用时再次增加。 count被增加了两次。
这个问题还得要包含和委托来解决。使用包含和委托,可以从白盒编程转到黑盒编程。白盒编程的意思是说,写继承类时必须要了解基类的实现。而黑盒编程可以完全无视基类的实现,因为不可能通过重载函数的方式向基类注入代码。只需要关注接口即可。
封装似乎是面向对象编程的第二大好处。对象状态变量被保护起来防止外部访问,即它们被封装在对象内部。我们不需要再操心那些可能被不知道谁访问的全局变量。但是:
为了提高效率,对象传递给函数时传递的是引用,而不是值。也就是说,函数不会传递对象本身,而是传递指向对象的一个引用或指针。 如果一个对象的引用被传递给另一个对象的构造函数,构造函数就能将这个对象引用放到私有变量中,用封装保护起来。 但这个传递的对象不是安全的!因为其他代码也可能拥有指向该对象的指针,比如调用构造函数的那段代码。它必须有指向对象的引用,否则没办法传递给构造函数。
构造函数必须要复制传递过来的对象。而且不能是浅复制,必须是深复制,即传入的对象内包含的所有对象和所有对象中包含的所有对象……都必须要复制。但这却没有效率。更糟糕的是,并非所有对象都能复制的。一些拥有操作系统资源的对象,最好的情况是复制无效,最糟糕的情况是根本不可能复制。
并不是因为多态不好,而是因为实现多态并不需要面向对象语言。接口也能实现多态,而且不需要面向对象的负担。而且,接口也不会限制你能混入的不同行为的数目。可以告别面向对象的多态,使用基于接口的多态。
作者罗列了面向对象编程的多个问题:香蕉 猴 丛林问题、钻石问题(菱形继承问题)、脆弱的基类问题、引用问题。并且可以使用基于接口的多态而非面向对象的多态。这是作者几十年面向对象编程的经验之谈,但是 说 再见,面向对象编程 肯定是夸大了,不过其问题与不足我们也该谨慎处理避免掉坑里。也许随着历史的发展会出现更好的更NB的不同理念的编程语言,即使那时我相信面向对象编程也会有一席之地。
GitHub链接: https://github.com/lichangke/LeetCode
个人Blog: https://lichangke.github.io/
欢迎大家来一起交流学习