Java002 【Java编程思想】第1章对象导论

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010425839/article/details/84261650

目录

Java002 【Java编程思想】第1章对象导论

出版者的话

读者书评

译者序

前言

封面的故事

致谢

绪论

1.1抽象过程

1.2每个对象都有一个接口

1.3每个对象都提供服务

1.4 被隐藏的具体实现

1.5复用现有实现

1.6继承

1.7伴随多态的可互换对象

Java前期(静态)绑定和后期(动态)绑定

1.8单根继承结构

1.9容器

1.10对象的创建和生命周期

1.11异常处理:处理错误

1.12并发编程

1.13Java与Internet

1.14总结


Java002 【Java编程思想】第1章对象导论

出版者的话

文艺复兴以来,源远流长的科学精神和逐步形成的学术规范,使西方国家在自然科学的各个领域取得了垄断性的优势;也正是这样的传统,使美国在信息技术发展的六十多年间名家辈出、独领风骚。在商业化的进程中,美国的产业界与教育界越来越紧密结合,计算机学科中的许多泰山北斗同时身处科研和教学的最前线,由此而产生的经典科学著作,不仅擘画了研究的范畴,还揭示了学术的源变,既遵循学术规范,又自有学者个性,其价值并不会因年月的流逝而衰退。

“出版要为教育服务”

读者书评

“睿智而不呆板的解说 ” “一本有思想的书”  “您的书中涵盖了Java开发工作中最重要,却很少被提及的概念-“原理””  ““您打破了“天下没有白吃的午餐”这句谚语。不是那种施舍性质的午餐,而是连美食家都觉得美味的午餐。”您一定掌握了艺术的精髓,使我们得以循序渐进地成功掌握细节知识。您也让学习过程变得非常简单,同时令人愉快。感谢您这本真正精彩的指南。” “我想将《Thinking in Java》作为我们每月聚会讨论的主要内容。这样我们可以在聚会中对书中的章节进行复习和讨论。” “阅读您的书令人如沐春风。” “您总能保持高水准的写作水平。感谢您!” “《Thinking in Java》撼动了整个自由世界!”

译者序

这是Java语言本身不断发展和完善的必然要求,也是本书作者Bruce Eckel孜孜不倦的创作精神和灵感所结出的硕果。他不但向我们展示了什么样的书才是经典书籍,而且还展示了经典书籍怎样才能精益求精、长盛不衰。掌握好Java语言并不是一件可以轻松完成的任务,如何真正掌握Java语言,从而编写出健壮、高效的以及灵活的程序是Java程序员们面临的重大挑战。本书深入浅出、循序渐进地把我们领入Java的世界,让我们在不知不觉中就学会了用Java的思想去考虑问题、解决问题。全书由陈昊鹏翻译,郭嘉也参与了部分翻译工作。

前言

程序设计其实是对复杂性的管理:待解决问题的复杂性,以及用来将解决该问题的工具的复杂性。几乎没有哪个语言将自己的设计目标专注于克服开发与维护程序的复杂性。(不过我相信Phyton语言已经非常接近该目标了)当然,有些编程语言在设计决策时也曾考虑到复杂性的问题,然后,总是会有其他议题被认为更有必要加入到该语言中。于是不可避免地,也正是这些所谓更必要的议题导致程序员最终“头撞南墙”。在设计C++/VB/Perl/Smalltalk之类的程序设计语言时,设计师也都为解决复杂性问题做了某种程度的工作。并且,正是解决某类特定问题的能力,成就了它们的成功。

随着对Java的了解,Sun对Java的设计目标:为程序员减少复杂性。用他们的话说:我们关心的是,减少开发健壮代码所需的时间以及困难。在早期,这个目标使得代码运行的并不快(Java程序的运行效率已经改善了),但它确实显著缩短了代码的开发时间。与用C++开发相同的程序相比Java只需一半甚至更少时间。仅此一项,就已节约了无法估量的时间与金钱。然而Java并未止步于此。它开始着手解决日渐变得重要的各种复杂任务,如多线程、网络编程,并将其作为语言特性或工具库的形式纳入Java,使得开发此类应用变得倍加简单。最终,Java解决了一些相当大的复杂性问题:跨平台编程、动态代码修改、甚至是安全的议题。它让你在面对其中任何一个问题时,都能从“举步维艰”到“起立鼓掌”。抛去我们都能看到的性能问题,Java确实非常精彩地履行了它的诺言:极大地提升程序员的生产率。同时,Java正从各个方面提升人们相互通讯的带宽。使得一切都变的更容易:编程、团队合作,创建与用户交互的用户界面,在不同类型的机器上运行程序,以及编写通过因特网通信的程序。

我认为,通讯变革的成功并不见得就是传输巨量的比特。我们所看到的真正的变革是人与人之间的通讯变得更容易了:无论是一对一、群体间,甚至是整个星球间的通讯。我曾听闻,在足够多的人之间的相互联系之上,下一次变革将是一种全球意识的形成。Java说不定就是促进该变革的工具,至少,它所具备的可能性是我觉得,教授这门语言是非常有意义的一件事情。

为一本书写新版时,作者最满意的是:把事情做得“恰如其分”。这是我从本书上一个版本发布以来所学到的东西。通常而言,这种见识正如谚语所言:学习就是从失败中汲取教训。并且,我也借机进行了一些修订。与往常一样,一个新的版本必将带来引人入胜的新思想。此时,新发现带来的喜悦,采用比以往更好的形式表达思想的能力,已经远远超多了可能引入的小错误。这也是对不断的在我脑中盘旋低语着的一种挑战,那就是让持有本书老版本的读者也愿意购买新的版本。这些促使着我尽可能改进、重写,以及重新组织内容,为热忱的读者们献上一本全新的、值得拥有的书。

我同样也认识到代码测试的重要性,必须要有一个内建的测试框架,并且每次开发系统时都必须进行测试。否则,根本没办法知道代码可靠与否。

封面的故事

封面创作灵感来自于美国的Arts&Crafts运动。该运动始于世纪之交,并在1900到1920年间达到顶峰。它起源于英格兰,是对工业革命带来的机器产品和维多利亚时代高度装饰化风格的回应。Arts&Crafts强调简洁设计,而回归自然是整个运动的核心,注重手工制造及推崇个性化设计,可是它并不回避使用现代工具。这和我们现今的情形有很多相似之处:世纪之交,从计算机革命的最初起源到对个人来说更精简、更意味深长的事物演变,以及对软件开发技能而不仅是生产程序代码的强调。

我以同样的眼光看待Java:尝试将程序员从操作系统机制中解放出来,朝着“软件艺师”的方向发展。

我和封面设计者自孩提时代就是朋友,我们从这次运动中获得灵感,并且都拥有源自那个时期的(或受那个时期启发而创作的)家具、台灯和其他作品。

封面暗示的另一个主题是一个收集盒,博物学家可以用它来展示昆虫标本。这些昆虫可以看作是对象,并放置到盒这个对象当中,而盒对象又放置到封面对象中,这形象说明了面向对象程序设计中最基本的集合概念。当然,程序员可能会不禁联想到程序缺陷(bug):这些昆虫被捕获,并假设在标本罐中被杀死,最后禁闭于一个展示盒中,似乎暗示Java有能力发现、显示和制服程序缺陷(事实上,这也是Java最为强大的属性之一)。

致谢

你们的耐心让我感激不已 在我尽力学习更好地与其他人相处的过程中,你们都对我很有帮助,并且我希望继续学习怎样使我的工作能够通过借鉴他人的成功而变得更出色。感谢你在我懈怠时对我的鞭笞。详细审视了我公司的组织结构,推翻了每一块可能隐藏祸害的石头,并且使得事情都条理化和合法化了,这让我心服口服。感谢你的细心和耐心。感谢你在面临难于处理的计算机问题时的坚定不移。Ervin发现的错误和对本书所作的完善对本书来说价值连城。他对细节的投入和关注程度令人惊异,他是我所见过的远远超过其他人的最好的技术读者。weblog已经成为了当我需要交流思想时的一种解决之道。感谢那些通过提交评论帮助我澄清概念的人们。Evan Cofsky一如既往地提供了有力的支持,他埋头处理了大量晦涩的细节,从而建立和维护了基于Linux的web服务器,并保持MindView服务器始终处于协调和安全的状态。一份特别的感谢要送给我的新朋友,咖啡,它为本项目产生了几乎无穷无尽的热情。感谢人们不断为我提供我所需要的一切,并容忍我所有的特殊需要,而且不厌其烦地帮我把所有事情都搞定。在我开发过程中,有些工具已经被证明是无价的了,但每次使用时仍会非常感激创建者。Cygwin(http://www.cygwin.com)为我解决了无数Windows不能解决的问题,并且每天我都会变得更加依赖它。IBM的Eclipse(http://www.eclipse.org)对开发社区做了真正杰出的贡献,并且随着它的不断升级,我期望能看到它的更伟大之处(IBM是怎样成为了潮流所向,我肯定错过了一份备忘录)。而JetBrains Intellij Idea则继续开阔者开发工具的创新之路。当然,如果我在其他地方强调得还不够的话,我得再次重申,我经藏使用Python(www.Python.org)解决问题。感谢整个Python社区,他们是一帮令人吃惊的群体。

绪论

上帝赋予人类说话的能力,而语言又创造了思想,思想是人类对宇宙的量度。

人类及其受那些已经成为社会表达工具的特定语言的支配。真实世界在很大程度上是不知不觉地基于群体的语言习惯形成的。

如同任何人类语言一样,Java提供了一种表达概念的方式。如果使用得当,随着问题变得更庞大复杂,这种表达工具将会比别的可供选择的语言更为简单、灵活。

我们不应该将Java仅仅看作是一些特性的集合,有一些特性在孤立状态下没有任何意义。只有在考虑到设计,而不仅仅是编码时,才能完整地运用Java的各部分。而且,要按照这种方式来理解Java,必须理解在语言和编程中经常碰到的问题。这本书讨论的是编程问题,它们为什么成为问题,以及Java已经采取了什么样的方案来解决它们。因此,每章所阐述的特性集,都是基于我所看到的这一语言在解决特定类型问题的方式。

自始至终,我一直持这样的观点:你需要在头脑中创建一个模型,以加强对这种语言的深入理解,如果遇到问题,就将它反馈到头脑中的模型并推断出答案。 

向读者提供我认为对理解这种程序设计语言来说很重要的部分,而不是提供我所知道的所有事情。我相信信息在重要性上存在差异性,有一些事实对于95%的程序员来说永远不必知道

"我们之所以将自然界分解,组织成各种概念,并按其含义分类,主要是因为我们是整个口语交流社会共同遵守的协定的参与者,这个协定以语言的形式固定下来......除非赞成这个协定中规定的有关语言信息的组织和分类,否则我们根本无法交谈。"

-Benjamin Lee Whorf(1897-1941)

计算机革命起源于机器,因此,编程语言的产生也始于对机器的模仿。

但是,计算机并非只是机器那么简单。计算机是头脑的延伸工具,同时还是一种不同类型的表达媒体。因此,这种工具看起来已经越来越不像机器,而更像我们头脑的一部分,以及一种写作、绘画、雕刻、动画、电影等一样的表达形式。面向对象程序设计(OOP Object-oriented Programming)便是这种以计算机作为表达媒体的大趋势中的组成部分。

1.1抽象过程

所有编程语言都提供抽象机制。可以认为,人们所能够解决问题的复杂性直接取决于抽象的类型和质量。所谓的“类型”是指“所抽象的是什么?”汇编语言是对底层机器的轻微抽象。接着出现了许多所谓“命令式”语言(如Fortran/Basic/C等)都是对汇编语言的抽象。这些语言在汇编语言基础上有了大幅的改进,但是它们所作的主要抽象仍要求在解决问题时要基于计算机的结构,而不是基于所要解决的问题的结构来考虑。程序员必须建立起在机器模型(位于"解空间"内,这是你对问题建模的地方,例如计算机)和实际待解决问题的模型(位于“问题空间”内,这是问题存在的地方,例如一项业务)之间的关联。建立这种映射是费力的,而且这不属于编程语言所固有的功能,这使得程序难以编写,并且维护代价高昂,同时也产生了作为副产物的整个“编程方法”行业。

另一种对机器建模的方式就是只针对待解问题建模。早期的编程语言,如Lisp和Apl,都选择考虑世界的某些特定视图(分别对应于“所有问题最终都是列表”或者“所有问题都是算法形式的”)。Prolog则将所有问题都转换成决策链。此外还产生了基于约束条件编程的语言和专门通过对图形符号操作来实现编程的语言(后者被证明限制性过强)。这些方式对于它们所要解决的特定类型的问题都是不错的解决方案,但是一旦超出其特定领域,它们就力不从心了。

面向对象方式通过向程序员提供表示问题空间中的元素的工具而更进了一步。这种表示方式非常通用,使得程序员不会受限于任何特定类型的问题。我们将问题空间中的元素及其在解空间中的表示成为“对象”。(还需要一些无法类比为问题空间元素的对象。)这种思想的实质是:程序可以通过添加新类型的对象使自身适用于某个特定问题。因此,当在阅读描述解决方案的代码的同时,也是在阅读关于问题的表述。相比以前所使用的语言,这是一种更灵活和更强有力的语言抽象。所以,OOP允许根据问题来描述问题,而不是根据运行解决方案的计算机来描述问题。但是它仍然与计算机有联系:每个对象看起来都有点像一台微型计算机-具有状态及操作,用户可以要求对象执行这些操作。如果要对现实世界中的对象作类比,那么说它们都具有特性和行为似乎不错。

Alan Kay曾经总结了第一个成功的面向对象语言,同时也是java所基于的语言之一的Smalltalk的五个基本特性,这些特性表现了一种纯粹的面向对象程序设计方式:

1)万事万物皆对象。理论上讲,可以抽取待解决问题的任何构件(狗、建筑物、服务等),将其表示为程序中的对象。

2)程序是对象的集合,它们通过发送消息来告知彼此所要做的。要想请求一个对象,就必须对该对象发送一条消息。更具体地说,可以把消息想象为对某个特定对象的方法的调用请求。

3)每个对象都有自己的由其他对象所构成的存储。换句话说,可以通过创建包含现有对象的包的方式来创建新类型对象。因此,可以在程序中构件复杂的体系,同时将其复杂性隐藏在对象的简单性背后。

4)每个对象都拥有其类型。每个对象都是某个类型一个实例。每个类区别于其他类的特性就是可以发送什么样的消息给它。

5)某一特定类型的所有对象都可以接收同样的消息。

Booch对对象提出了一个更加简洁的描述:对象具有状态、行为和标识。这意味着每一个对象都可以拥有其内部数据(它们给出了该对象的状态)和方法(它们产生行为),并且每一个对象都可以唯一地与其他对象区分开来(具体说来,就是每一个对象在计算机中都有一个唯一的地址)。

1.2每个对象都有一个接口

所有的对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。本节中的接口实际指的是对象所具有的功能/行为/方法,或者说是对某一特定对象所能发出的请求。在类型中,每一个可能的请求都有一个方法与之相关联,当向对象发送请求时,与之相关联的方法就会被调用。此过程通常被概括为:向某个对象“发送消息”(产生请求),这个对象便知道此消息的目的,然后执行对应的程序代码。

1.3每个对象都提供服务

当正在试图开发或理解一个程序设计时,最好的方法之一就是将对象想象为“服务提供者”。程序本身将向用户提供服务,它将通过调用其他对象提供的服务来实现这一目的。你的目标就是去创建(或者最好是在现有代码库中寻找)能够提供理想的服务来解决问题的一系列对象。将问题分解为对象集合。将对象看着服务提供者有助于提高对象的内聚性。高内聚是软件设计的基本质量要求之一:这意味着一个软件构件(例如一个对象、一个方法或一个对象库)的各个方面组合得很好。不仅能满足现有问题的需要,而且还可以随时作为独立的功能模块供其他问题调用。当理解代码或重用某个对象时,若看出该对象所能提供的服务的价值,则会使得调整对象以适应其设计的过程变得简单得多。

1.4 被隐藏的具体实现

类创建者只允许类消费者访问其所必需的类的那部分内容,而隐藏类的其他部分。这样类创建者可以任意修改被隐藏的部分,而不用担心对其他任何人造成影响。被隐藏的部分通常代表对象内部脆弱的部分,它们很容易被粗心或不知内情的类消费者所毁坏,因此将实现隐藏起来可以减少程序bug。

在任何相互关系中,具有各方都遵守的边界是十分重要的事情。

访问控制存在的第一个原因就是让类消费者无法触及他们不应该触及的部分(这部分对数据类型的内部操作来说是必需的,但并非用户解决特定问题所需的接口的那一部分)。这对类消费者而言是其实是一项服务,因为据此可以很容易看出哪些东西对他们是很重要的,而哪些东西是可以忽略的。

访问控制存在的第二个原因就是允许库设计者可以改变类内部的工作方式而不用担心会影响到类消费者。例如:为减轻开发认为而以某种简单的方式实现了某个特定类,但稍后发现必须改写它才能使其运行得更快。如果接口和实现可以清晰地分离并得以保护,则可轻而易举完成这项工作。(比如需要修改一个方法,但是此方法被很多地方引用了,若此方法一开始抽象的并不好(未考虑业务后续扩展的可能),可能这次修改会导致所有引用到此方法的地方都要跟着修改,反之,则只需要修改这一个方法即可,其他调用此方法的地方即类消费者则不必做任何改动,不过呢,测试验证还是需要的)

访问指定词(access specifier)决定了紧跟其后被定义的东西可以被谁使用。

  • public 任何人都能访问的元素
  • private 只有类创建者和类的内部方法可以访问
  • protected 只有类创建者和类的内部方法以及继承的类可以访问
  • 默认(无任何访问限定词) 只有同一包(库构件)内可以访问

1.5复用现有实现

一旦类被创建并测试完,则它在理想情况下代表一个有用的代码单元。事实证明,这种复用性并不易达到预期。设计一个可复用的对象需要丰富经验和敏锐洞察力。但是一旦你有了这样的设计,它就可供复用。

https://zhidao.baidu.com/question/717044793623066285.html

组合(Composition part-of)和聚合(Aggregation has-a)是有很大区别的,这个区别不是在形式上,而是在本质上:
比如A类中包含B类的一个引用b,当A类的一个对象消亡时,b这个引用所指向的对象也同时消亡(没有任何一个引用指向它,成了垃圾对象),这种情况叫做组合,反之b所指向的对象还会有另外的引用指向它,这种情况叫聚合。
在实际写代码时组合方式一般会这样写:
A类的构造方法里创建B类的对象,也就是说,当A类的一个对象产生时,B类的对象随之产生,当A类的这个对象消亡时,它所包含的B类的对象也随之消亡。
聚合方式则是这样:
A类的对象在创建时不会立即创建B类的对象,而是等待一个外界的对象传给它
传给它的这个对象不是A类创建的。

//实体类:人
class People{
  String name;
  String identityId;  //身份属性的ID
  Body body;         //身体对象属性
  //省略getter、setter方法
}
//实体类:身体
class Body{
   String 内脏;
   String 手;
   String 脚;
}
//实体类:身份信息
class Identity{
   Integer id;
   String number;   //身份证号
   String name;    //姓名
   ....
}

https://blog.csdn.net/luokai_586524/article/details/80164860

由人(People)、身体(Body)、身份信息(Identity)之间的关系可以看出。人由身体所组成,应该由身体来组合为一个人,身体作为人的组合的一部分。体现在数据存储时,即不必将身体(Body)存储在数据库中,而是直接作为人的属性存在。而Identity,记录着一个人的各种信息,脱离People之后,可能会有别的地方依旧会使用到该属性。因此这个就是聚合,在设计实体类时,直接保存其ID即可。 

组合带来的了极大的灵活性。新类的成员对象通常被声明为private,使得类消费者不能访问它们。这也使得你可以在不干扰类消费者的情况下修改这些成员,也可以在运行时修改这些成员对象,以实现动态修改程序的行为。继承则不具备这样的灵活性,因为编译器必须对通过继承而创建的类施加编译时的限制。

建立新类时,应该首先考虑组合,因为更简单灵活,设计会变得更加清晰。一旦有了一些经验之后,便能够看出必须使用继承的场合了。

https://blog.csdn.net/houwc/article/details/52460348

继承所带来的多态性虽然是面向对象的一个重要特性,但这种特性不能在所有的场合中滥用。继承应该被当做设计架构的有用补充,而不是全部。组合不能用于多态,但组合使用的频率却要远远高于继承。从设计的角度来看,继承代表的是“Is a”,组合代表的是“Has a”。这是最重要的区别,任何时候,设计理念上的因素总是排在第一位。继承不仅仅是指继承自某个类型(class),也可以指继承自某个接口(interface)。继承最大的优点就是多态,这也奠定了面向抽象编程的基础。继承提高了代码的复用性。组合显然不具备这种特性。从语法角度来看,继承易于扩展。基类一旦扩展一个具有public、internal、protected访问修饰符的接口,所有的子类都会自动拥有其接口,组合则不能。组合要拥有任何对象的行为,必须手动编码。

到目前为止,似乎一直在说继承的优点。事实上,继承的以上优点,正好又是它的缺点。子类天然具有基类的公开接口,而这正好破换了面向对象的“封装性”。我们显然不需要每一层的类型都具有上层的所有接口。一个类,如果其继承体系达到3层(当然,凡事都有例外,WPF体系中的控件集成体系,以Shape为例,多达7层),就可以考虑停止了。如果不停止,对调用者来说,最底层的类型会有多少公开的方法和属性呢?答案是最底层的类型会拥有所有上层类型的开放接口。随着项目的发展,组合的优势会逐渐体现出来,它良好的封装性使类型可以对外宣布:我只做一件事。

组合的另一个优势是,它可以组合多个其他类型。如果组合太多的类型,就意味着当前的类很可能做了太多的事情,它就需要拆分成两个类了。继承不具有这样的特性,在C#中,子类只能有一个基类(接口则放开这种限制,子类可以继承自多个接口)。

应当根据实际情况考虑是使用继承还是组合。一般来讲,组合更能满足大部分的应用场景。不要为了让代码看起来像“面向对象”,而滥用继承

1.6继承

一个基类型包含其所有导出类型所共享的特性和行为。可以创建一个基类型来表示系统中某些对象的核心概念,从基类型中导出其他类型,来表示此核心可以被实现的各种不同方式。可以通过继承构件一个类型层次结构,以此来表示待求解的某种类型的问题。通过使用对象,类型层次结构成为了主要模型,因此,可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。导出类与基类具有相同类型,可以接收相同类型的消息。

由于基类和导出类具有相同的基础接口,所以伴随此接口的必定有某些具体实现。即当对象接收到特定消息时,必须有某些代码去执行。如果只是简单地继承一个类而并不做其他任何事,那么在基类接口中的方法将会被直接继承到导出类中。这意味着导出类的对象不仅与基类拥有相同的类型,而且还具有相同行为,这样没意义。

有两种方法可以使得基类与导出类产生差异:1直接在导出类中添加新方法2覆盖(overriding)基类方法,此时导出类与基类使用相同的接口方法,但是在导出类中的此方法中做了些不同的事情。

1.7伴随多态的可互换对象

在处理类型层次结构时,经常想把一个对象不当作它所属的特定类型来对待,而是将其当作其基类的对象来对待。这使得人们可以编写出不依赖于特定类型的代码。基类的所有对象都具有某些共性的属性和行为,都可以接收相同的消息,而不必担心对象将如何具体处理消息。

这样的代码是不会受到添加新类型的影响的,而且添加新类型是扩展一个面向对象程序以便处理新情况的最常用的方式。例如,可以从几何形中导出一个新的子类型五角形,而并不需要修改处理泛化几何形状的方法。通过导出新的子类型而轻松扩展设计的能力是对改动进行封装的基本方式之一。可以改善设计,降低软件维护成本。

但是,在试图将导出类的对象看作其泛化基类对象时,仍然存在一个问题。如果某个方法要让泛化几何形状绘制自己,让泛化交通工具行驶,让泛化鸟类移动,那么编译器在编译时是不可能知道应该执行哪一段代码的。这就是关键所作在:当发送这样的消息时,程序员并不想知道哪一段代码将被执行,对象会依据自身具体类型来执行恰当的代码。这是如何做到的呢?这就涉及到后期/动态/运行时绑定概念了。

Java前期(静态)绑定和后期(动态)绑定

https://www.cnblogs.com/jstarseven/articles/4631586.html

程序绑定的概念:
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。对java来说,绑定分为静态绑定和动态绑定;或者叫做前期绑定和后期绑定.

静态绑定:
在程序执行前方法已经被绑定(也就是说在编译过程中就已经知道这个方法到底是哪个类中的方法),此时由编译器或其它连接程序实现。例如:C。
针对java简单的可以理解为程序编译期的绑定;这里特别说明一点,java当中的方法只有final,static,private和构造方法是前期绑定

动态绑定:
后期绑定:在运行时根据具体对象的类型进行绑定
若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
动态绑定的过程:

  1. 虚拟机提取对象的实际类型的方法表;
  2. 虚拟机搜索方法签名;
  3. 调用方法。

关于final,static,private和构造方法是前期绑定的理解
对于private的方法,首先一点它不能被继承,既然不能被继承那么就没办法通过它子类的对象来调用,而只能通过这个类自身的对象来调用。因此就可以说private方法和定义这个方法的类绑定在了一起。
final方法虽然可以被继承,但不能被重写(覆盖),虽然子类对象可以调用,但是调用的都是父类中所定义的那个final方法,(由此我们可以知道将方法声明为final类型,一是为了防止方法被覆盖,二是为了有效地关闭java中的动态绑定)。
构造方法也是不能被继承的(网上也有说子类无条件地继承父类的无参数构造函数作为自己的构造函数,不过个人认为这个说法不太恰当,因为我们知道子类是通过super()来调用父类的无参构造方法,来完成对父类的初始化, 而我们使用从父类继承过来的方法是不用这样做的,因此不应该说子类继承了父类的构造方法),因此编译时也可以知道这个构造方法到底是属于哪个类。
对于static方法,具体的原理我也说不太清。不过根据网上的资料和我自己做的实验可以得出结论:static方法可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个static方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法。而如果子类中定义了相同的方法,则会调用子类的中定义的方法。唯一的不同就是,当子类对象上转型为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法。因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法)
由上面我们可以得出结论,如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。

java的编译与运行
java的编译过程是将java源文件编译成字节码(jvm可执行代码,即.class文件)的过程,在这个过程中java是不与内存打交道的,在这个过程中编译器会进行语法的分析,如果语法不正确就会报错。
Java的运行过程是指jvm(java虚拟机)装载字节码文件并解释执行。在这个过程才是真正的创立内存布局,执行java程序。
java字节码的执行有两种方式: (1)即时编译方式:解释器先将字节编译成机器码,然后再执行该机器码;(2)解释执行方式:解释器通过每次解释并执行一小段代码来完成java字节码程序的所有操作。(这里我们可以看出java程序在执行过程中其实是进行了两次转换,先转成字节码再转换成机器码。这也正是java能一次编译,到处运行的原因。在不同的平台上装上对应的java虚拟机,就可以实现相同的字节码转换成不同平台上的机器码,从而在不同的平台上运行)

前面已经说了对于java当中的方法而言,除了final,static,private
和构造方法是前期绑定外,其他的方法全部为动态绑定。
而动态绑定的典型发生在父类和子类的转换声明之下:
比如:Parent p = new Children();
其具体过程细节如下:
1:编译器检查对象的声明类型和方法名。
假设我们调用x.f(args)方法,并且x已经被声明为C类的对象,那么编译器会列举出C 类中所有的名称为f 的方法和从C 类的超类继承过来的f 方法。
2:接下来编译器检查方法调用中提供的参数类型。
如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就调用这个方法,这个过程叫做“重载解析”。

3:当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。

假设实际类型为D(C的子类),如果D类定义了f(String)那么该方法被调用,否则就在D的超类中搜寻方法f(String),依次类推。

JAVA 虚拟机调用一个类方法时(静态方法),它会基于对象引用的类型(通常在编译时可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定,是多态的一种。动态绑定为解决实际的业务问题提供了很大的灵活性,是一种非常优美的机制。

与方法不同,在处理java类中的成员变量(实例变量和类变量)时,并不是采用运行时绑定,而是一般意义上的静态绑定。所以在向上转型的情况下,对象的方法可以找到子类,而对象的属性(成员变量)还是父类的属性(子类对父类成员变量的隐藏)。
Java代码

public class Father {  
    protected String name = "父亲属性";  
}  
    
  
public class Son extends Father {  
    protected String name = "儿子属性";  
  
    public static void main(String[] args) {  
        Father sample = new Son();  
        System.out.println("调用的属性:" + sample.name);  
    }  
}  

结论,调用的成员为父亲的属性。

这个结果表明,子类的对象(由父类的引用handle)调用到的是父类的成员变量。所以必须明确,运行时(动态)绑定针对的范畴只是对象的方法
现在试图调用子类的成员变量name,该怎么做?最简单的办法是将该成员变量封装成方法getter形式
代码如下:
Java代码

public class Father {  
    protected String name = "父亲属性";  
  
    public String getName() {  
        return name;  
    }  
}    
  
public class Son extends Father {  
    protected String name = "儿子属性";  
  
    public String getName() {  
        return name;  
    }  
  
    public static void main(String[] args) {  
        Father sample = new Son();  
        System.out.println("调用的属性:" + sample.getName());  
    }  
}  

结果:调用的是儿子的属性
java因为什么对属性要采取静态的绑定方法。这是因为静态绑定是有很多的好处,它可以让我们在编译期就发现程序中的错误,而不是在运行期。这样就可以提高程序的运行效率!而对方法采取动态绑定是为了实现多态,多态是java的一大特色。多态也是面向对象的关键技术之一,所以java是以效率为代价来实现多态这是很值得的。

当向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法的存在,并对调用参数和返回值执行类型检查(无法提供此类保证的语言被称为弱类型),但并不知道将被执行的确切代码。为了执行后期绑定,Java使用一小段特殊代码来替代绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址。这样,根据这一小段代码的内容,每一个对象都可以具有不同的行为表现。当向一个对象发送消息时,该对象就能够知道这条消息应该做些什么。

如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。

正式因为多态才使得事情总是能够被正确处理。编译器和运行系统会处理相关的细节。更重要的是怎么利用多态进行设计。

1.8单根继承结构

终极基类Object。单根继承保证所有对象都具备某些功能,单根继承结构使得垃圾回收器的实现变得容易得多。由于所有对象都具有其类型信息,因此不会因无法确定对象的类型而陷入僵局。这对于系统级操作(异常处理)显得尤为重要,并且给编程带来了更大的灵活性。

1.9容器

通常若不知道在解决问题时需要多少个对象,或者它们将存活多久,就不可能知道如何存储这些对象。如何才能知道需要多少空间来创建这些对象呢?答案是你不可能知道,因为这类信息只有在运行时才能获得。

对于面向对象设计中的大多数问题而言,这个问题的解决方案似乎过于轻率:创建一种持有对其他对象的引用的新的对象类型,称之为容器/集合。在任何需要的时候都可以扩充自己以容纳你置于其中的所有东西。因此不需要知道将来会把多少个对象置于容器中,只需要创建一个容器对象,然后让它处理所有细节。

对容器要有所选择,因为:

  • 不同容器提供了不同类型的接口和外部行为。某种容器提供的解决方案可能比其他容器要灵活的多。
  • 不同容器对于某些操作具有不同的效率。比如ArrayList和LinkedList,都是具有相同接口和外部行为的简单序列,但它们对某些操作所花费的代价却有天壤之别。ArrayList中随机访问元素是一个花费固定时间的操作,而LinkedList中随机选取元素需要在列表中移动,这种代价是高昂的,访问越靠近表尾的元素花费的时间越长;另一方面,若想在列表中插入一个元素,LinkedList的开销却比ArrayList要小。上述操作及其他操作的效率,依序列底层结构的不同而存在很大差异。我们可以在一开始使用LinkedList构件程序,而在优化系统性能时改用ArrayList。

Java SE5的重大变化之一就是增加了参数化类型/泛型的向下转型,向下转型为更具体的类型,向下转型是安全的。ArrayList<Shape> shapes = new ArrayList<Shape>();使用泛型可以记住放入容器中的对象具体是什么类型。就不会像以前一样,因为容器中只能存放Object的类型,导致当将对象置入容器时必须向上转型为Object而丢失自己的身份。

1.10对象的创建和生命周期

使用对象时最关键的问题之一便是它们的生成和销毁方式。每个对象为了生存都需要资源,尤其是内存。当我们不再需要一个对象时,它必须被清理掉,使其占用的资源得以释放和重用。

C++认为效率控制是最重要的议题,所以给程序员提供了选择的权利。为了追求最大的执行速度,对象的存储空间和生命周期可以在编写程序时确定,这可以通过将对象至于堆栈(它们有时被称为自动变量(automatic variable)或限域变量(scoped variable))或静态存储区域来实现。这种方式将存储空间分配和释放置于优先考虑的位置,某些情况下这样控制非常有价值。但是,也牺牲了灵活性,因为必须在编写程序时知道对象的确切的数量、生命周期和类型。如果试图解决更一般化的问题,这种方式就显得过于受限了。第二种方式是在被称为堆(heap)的内存池中动态地创建对象。在这种方式中,直到运行时才知道需要多少对象,它们的生命周期如何,以及它们的具体类型是什么。这些问题的答案只能在程序运行时相关代码被执行到的那一刻才能确定。如果需要一个新对象,可以在需要的时刻直接在队中创建。因为存储空间是在运行时被动态管理的,所以需要大量的时间在堆中分配存储空间,这可能要远远大于在堆栈中创建存储空间的时间。在堆栈中创建存储空间和释放存储空间通常只需要一条汇编指令即可,分别对应将栈顶指针向下移动和将栈顶指针向上移动。创建堆存储空间的时间依赖于存储机制的设计。动态方式有这样的一个一般性的逻辑假设:对象趋于变得复杂,所以查找和释放存储空间的开销不会对对象的创建造成更大的冲击。动态方式所带来的更大的灵活性正式解决一般化编程问题的要点所在。Java完全采用了动态内存分配方式(基本类型只是一种特例)。

对于允许在堆栈上创建对象的语言,编译器可以确定对象存活的时间,并可以自动销毁它,然而在堆上创建的对象,编译器对其生命周期一无所知。像C++必须通过编程方式来确定何时销毁对象,可能因为未正确处理而导致内存泄露。Java提供了垃圾回收机制,可以自动发现对象何时不再被使用,并继而销毁它。这样就减少了所必须考虑的议题和必须编写的代码。更重要的是这样有更高层的保障避免暗藏的内存泄露问题。这个问题已经使许多C++项目折戟沉沙。垃圾回收器的实现得益于所有对象都是继承自单根鸡肋Object以及只能以一种方式创建对象(在堆上创建)这两个特性。使得用Java编程的过程较之用C++编程要简单得多,所要做出的决策和要克服的障碍也要少得多。

http://www.2cto.com/kf/201302/190704.html

简单的说: Java把内存划分成两种:一种是栈内存,一种是堆内存。

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

     堆内存用来存放由new创建的对象和数组。

     在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

1. 栈(stack)与堆(heap)都是Java用来在Ram【ROM是只读内存(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。通常用在不需经常变更资料的电子或电脑系统中,资料并且不会因为电源关闭而消失。RAM(random access memory)随机存储器。存储单元的内容可按需随意取出或存入,且存取的速度与存储单元的位置无关的存储器。这种存储器在断电时将丢失其存储内容,故主要用于存储短时间使用的程序。 按照存储信息的不同,随机存储器又分为静态随机存储器(Static RAM,SRAM)和动态随机存储器(Dynamic RAM,DRAM)。】中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

2. 栈的优势是,存取速度比堆要快,仅次于直接位于CPU中的寄存器。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。另外,栈数据可以共享,详见第3点。堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。

3. Java中的数据类型有两种。 一种是基本类型(primitive types), 共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的,称为自动变量。值得注意的是,自动变量存的是字面值,不是类的实例,即不是类的引用,这里并没有类的存在。如int a = 3; 这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值固定定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义

 int a = 3; int b = 3;

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

特别注意的是,这种字面值的引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变的情况。如上例,我们定义完a与 b的值后,再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。

另一种是包装类数据,如Integer, String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于堆中,Java用new()语句来显式地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

4. String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建(作为对比,在JDK 5.0之前,你从未见过Integer i = 3;的表达式,因为类与字面值是不能通用的,除了String。而在JDK 5.0中,这种表达式是可以的!因为编译器在后台进行Integer i = new Integer(3)的转换)。前者是规范的类的创建过程,即在Java中,一切都是对象,而对象是类的实例,全部通过new()的形式来创建。Java 中的有些类,如DateFormat类,可以通过该类的getInstance()方法来返回一个新创建的类,似乎违反了此原则。其实不然。该类运用了单例模式来返回类的实例,只不过这个实例是在该类内部通过new()来创建的,而getInstance()向外部隐藏了此细节。那为什么在String str = "abc";中,并没有通过new()来创建实例,

是不是违反了上述原则?其实没有。

5. 关于String str = "abc"的内部工作。Java内部将此语句转化为以下几个步骤:

(1)先定义一个名为str的对String类的对象引用变量:String str;

(2)在栈中查找有没有存放值为"abc"的地址,如果没有,则开辟一个存放字面值为"abc"的地址,接着创建一个新的String类的对象o,并将o 的字符串值指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。

(3)将str指向对象o的地址。 值得注意的是,一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用! 为了更好地说明这个问题,我们可以通过以下的几个代码进行验证。

String str1 = "abc"; String str2 = "abc";

System.out.println(str1==str2); //true

 注意,我们这里并不用str1.equals(str2);的方式,因为这将比较两个字符串的值是否相等。==号,根据JDK的说明,只有在两个引用都指向了同一个对象时才返回真值。而我们在这里要看的是,str1与str2是否都指向了同一个对象。 结果说明,JVM创建了两个引用str1和str2,但只创建了一个对象,而且两个引用都指向了这个对象。 我们再来更进一步,将以上代码改成:

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

System.out.println(str1 + "," + str2); //bcd, abc

System.out.println(str1==str2); //false

这就是说,赋值的变化导致了类对象引用的变化,str1指向了另外一个新对象!而str2仍旧指向原来的对象。上例中,当我们将str1的值改为"bcd"时,JVM发现在栈中没有存

放该值的地址,便开辟了这个地址,并创建了一个新的对象,其字符串的值指向这个地址。

事实上,String类被设计成为不可改变(immutable)的类。如果你要改变其值,可以,但JVM在运行时根据新值悄悄创建了一个新对象,然后将这个对象的地址返回给原来类的引用。这个创建过程虽说是完全自动进行的,但它毕竟占用了更多的时间。在对时间要求比较敏感的环境中,会带有一定的不良影响。 再修改原来代码:

String str1 = "abc";

String str2 = "abc";

str1 = "bcd";

String str3 = str1;

System.out.println(str3); //bcd

String str4 = "bcd";

System.out.println(str1 == str4); //true

str3 这个对象的引用直接指向str1所指向的对象(注意,str3并没有创建新对象)。当str1改完其值后,再创建一个String的引用str4,并指向因str1修改值而创建的新的对象。可以发现,这回str4也没有创建新的对象,从而再次实现栈中数据的共享。 我们再接着看以下的代码。

String str1 = new String("abc");

String str2 = "abc";

System.out.println(str1==str2); //false

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。

String str1 = "abc";

String str2 = new String("abc");

 System.out.println(str1==str2); //false

创建了两个引用。创建了两个对象。两个引用分别指向不同的两个对象。 以上两段代码说明,只要是用new()来新建对象的,都会在堆中创建,而且其字符串是单独存值的,即使与栈中的数据相同,也不会与栈中的数据共享。

6. 数据类型包装类的值不可修改。不仅仅是String类的值不可修改,所有的数据类型

包装类都不能更改其内部的值。

7. 结论与建议:

(1)我们在使用诸如String str = "abc";的格式定义类时,总是想当然地认为,我们创建了String类的对象str。担心陷阱!对象可能并没有被创建!唯一可以肯定的是,指向 String类的引用被创建了。至于这个引用到底是否指向了一个新的对象,必须根据上下文来考虑,除非你通过new()方法来显要地创建一个新的对象。因此,更为准确的说法是,我们创建了一个指向String类的对象的引用变量str,这个对象引用变量指向了某个值为"abc"的String类。清醒地认识到这一点对排除程序中难以发现的bug是很有帮助的。

(2)使用String str = "abc";的方式,可以在一定程度上提高程序的运行速度,因为JVM会自动根据栈中数据的实际情况来决定是否有必要创建新对象。而对于String str = new String("abc");的代码,则一概在堆中创建新对象,而不管其字符串值是否相等,是否有必要创建新对象,从而加重了程序的负担。这个思想应该是享元模式的思想,但JDK的内部在这里实现是否应用了这个模式,不得而知。

(3)当比较包装类里面的数值是否相等时,用equals()方法;当测试两个包装类的引用是否指向同一个对象时,用==。

(4)由于String类的immutable性质,当String变量需要经常变换其值时,应该考虑使用StringBuffer类,以提高程序效率。

java中内存分配策略及堆和栈的比较

内存分配策略

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的.

静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在

编译时就可以给他们分配固定的内存空间.这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求.

栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的.和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存.和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例.堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放.

堆和栈的比较

上面的定义从编译原理的教材中总结而来,除静态存储分配之外,都显得很呆板和难以理解,下面撇开静态存储分配,集中比较堆和栈:

从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的.而这种不同又主要是由于堆和栈的特点决定的:

编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行.退出函数的时候,修改栈指针就可以把栈中的内容销毁.这样的模式速度最快,当然要用来运行程序了.需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时.

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低.但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定.在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存.当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因,看来列宁同志说的好,人的优点往往也是人的缺点,人的缺点往往也是人的优点(晕~).

JVM中的堆和栈

JVM是基于堆栈的虚拟机.JVM为每个新创建的线程都分配一个堆栈.也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。 我们知道,某个线程正在执行的方法称为此线程的当前方法.我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据.这个帧在这里和编译原理中的活动纪录的概念是差不多的. 从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。 每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程 共享.跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

GC的思考

Java为什么慢?JVM的存在当然是一个原因,但有人说,在Java中,除了简单类型(int,char等)的数据结构,其它都是在堆中分配内存(所以说Java的一切都是对象),这也是程序慢的原因之一。我的想法是(应该说代表TIJ的观点),如果没有Garbage Collector(GC),上面的说法就是成立的.堆不象栈是连续的空间,没有办法指望堆本身的内存分配能够象堆栈一样拥有传送带般的速度,因为,谁会 为你整理庞大的堆空间,让你几乎没有延迟的从堆中获取新的空间呢?

这个时候,GC站出来解决问题.我们都知道GC用来清除内存垃圾,为堆腾出空间供程序使用,但GC同时也担负了另外一个重要的任务,就是要让Java中堆的内存分配和其他语言中堆栈的内存分配一样快,因为速度的问题几乎是众口一词的对Java的诟病.要达到这样的目的,就必须使堆的分配也能够做到象传送带一样,不用自己操心去找空闲空间.这样,GC除了负责清除Garbage外,还要负责整理堆中的对象,把它们转移到一个远离Garbage的纯净空间中无间隔的排列起来,就象堆栈中一样紧凑,这样Heap Pointer就可以方便的指向传送带的起始位置,或者说一个未使用的空间,为下一个需要分配内存的对象"指引方向".因此可以这样说,垃圾收集影响了对象的创建速度,听起来很怪,对不对?

 那GC怎样在堆中找到所有存活的对象呢?前面说了,在建立一个对象时,在堆中分配实际建立这个对象的内存,而在堆栈中分配一个指向这个堆对象的指针(引 用),那么只要在堆栈(也有可能在静态存储区)找到这个引用,就可以跟踪到所有存活的对象.找到之后,GC将它们从一个堆的块中移到另外一个堆的块中,并 将它们一个挨一个的排列起来,就象我们上面说的那样,模拟出了一个栈的结构,但又不是先进后出的分配,而是可以任意分配的,在速度可以保证的情况下, Isn't it great?

 但是,列宁同志说了,人的优点往往也是人的缺点,人的缺点往往也是人的优点(再晕~~).GC()的运行要占用一个线程,这本身就是一个降低程序运行性能 的缺陷,更何况这个线程还要在堆中把内存翻来覆去的折腾.不仅如此,如上面所说,堆中存活的对象被搬移了位置,那么所有对这些对象的引用都要重新赋值.这 些开销都会导致性能的降低.

基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间 分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空 间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈 空间分配。字符串常量在DATA区域分配,this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小!

JVM中的堆和栈

JVM是基于堆栈的虚拟机。JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。 我们知道,某个线程正在执行的方法称为此线程的当前方法。我们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在 线程的Java堆栈里新压入一个帧。这个帧自然成为了当前帧.在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据。这个帧在这里 和编译原理中的活动纪录的概念是差不多的。 从Java的这种分配机制来看,堆栈又可以这样理解:堆栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有先进后出的特性。

每一个Java应用都唯一对应一个JVM实例,每一个实例唯一对应一个堆。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有 的线程共享。跟C/C++不同,Java中分配堆内存是自动初始化的。Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分 配,也就是说在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

1.11异常处理:处理错误

自编程语言问世以来,错误处理就始终是最困难的问题之一。因为设计一个良好的错误处理机制非常困难,所以许多语言直接略去这个问题,将其交给程序库设计者处理,而这些设计者也只是提出一些不彻底的方法,这些方法可用于许多很容易就绕过此问题的场合,而且其解决方式通常也只是忽略此问题。大多数错误处理机制的主要问题在于,它们都依赖于程序员自身的警惕性,这种警惕性来源于一种共同约定,而不是编程语言所强制的。如果程序员不够警惕-通常是因为她们太忙,这些机制就很容易被忽视。

异常处理将错误处理直接置于编程语言中,有时甚至置于操作系统中。异常是一种对象,它从出错地点被抛出,并被专门设计用来出来处理特定类型错误的相应的异常处理器捕获。异常处理就像是与程序正常执行路径并行的,在错误发生时执行另一条完全分离的路径,所以不会干扰正常的执行代码。这往往使得代码编写变得简单,因为不需要被迫定期检查错误。此外,被抛出的异常不像方法返回的错误值和方法设置的用来表示错误条件的标志位那样可以被忽略。异常不能被忽略,所以它保证一定会在某处得到处理。最后需要指出的是:异常提供了一种从错误状况进行可靠恢复的路径。现在不再是只能退出程序,你可以经常进行校正,并恢复程序的执行,这些都有助于编写出更健壮的程序。

Java的异常处理在众多编程语言中格外引人注目,因为Java一开始就内置了异常处理,而且强制你必须使用它。它是唯一可接受的错误报告方式。如果没有编写正确的处理异常的代码,那么就会得到一条编译时出错消息。这种有保障的一致性有时会使得错误处理非常容易。

值得注意的是,异常处理不是面向对象的特征,尽管在面向对象语言中异常常被表示成为一个对象。异常处理在面向对象语言出现之前就已经存在了。

1.12并发编程

在计算机编程中有一个基本概念,就是在同一时刻处理多个任务的思想,许多程序设计问题都要求,程序能够停下正在做的工作,转而处理某个其他问题,然后再返回主进程,有许多方法可以实现这个目的。最初,程序员们用所掌握的有关机器底层的知识来编写中断服务程序,主进程的挂起是通过硬件中断来触发的。尽管这样可行,但难度太大,而且不能移植。有时中断对于处理时间性强的任务是必需的,但是对于大量的其他问题,我们只是想把问题切分成多个可独立运行的部分(任务),从而提高程序的相应能力,在程序中,这些彼此独立运行的部分称之为线程,上述概念被称为并发。并发最常见的例子就是用户界面,通过使用任务,用户可以在按下按钮后快速得到一个响应,而不用被迫等到程序完成当前任务为止。

通常,线程只是一种为单一处理器分配执行时间的手段,但是如果操作系统支持多处理器,那么每个任务可以被指派给不同处理器,并且它们是真正地并行执行。在语言级别上,多线程所带来的便利之一便是程序员不再操心机器上是有多个处理器还是只有一个处理器,由于程序在逻辑上被分为线程,所以如果机器拥有多个处理器,那么程序不需要特殊调整也能执行得更快。

所有这些都是的并发看起来相当简单,但是有一个隐患:共享资源。如果多个并发任务访问同一个资源就会出问题。为了解决此问题,共享的资源在使用期间必须被锁定。因此,整个过程是,某个任务锁定某项资源,完成其任务,然后释放资源锁,使其他任务可以使用这项资源。

Java的并发是内置于语言中的,Java SE5已经增添了大量额外的库支持。

1.13Java与Internet

1.13.1 web是什么

  • 客户/服务器计算技术 客户/服务器系统的核心思想是:系统的数据集中存储,根据需要分发给某些人员或机器集群。数据信息存储池集中于中央,对数据信息的修改将被传播给信息消费者。信息存储池、用于分发消息的软件以及信息与软件所驻留的机器或机群被总称为服务器。驻留在用户机器上的软件与服务器通信以获取/处理信息,然后将它们显示在被称为客户机的用户机器上。如果客户端软件发生变化,则必需被重新编译/调试/并更新到客户端机器上,事实证明这比想象的更加复杂和费力。如果想支持多种不同类型计算机和操作系统,问题将更麻烦。最后还有一个最重要的性能问题:可能在任意时刻都有成百上千的客户向服务器发出请求,所以任何很小的延迟都会产生重大影响。为了将延迟最小化,程序员努力减轻处理任务的负载,通常是分散给客户机处理,但有时也会使用所谓的中间件将负载分散给在服务器端的其他机器。(中间件也被用来提高可维护性。)
  • Web就是一台巨型服务器,所有服务器和客户机都同时共存于同一个网络中。

1.13.2客户端编程

引入在客户端浏览器中运行程序的能力,即为客户端编程。大多数运行Web浏览器的机器都是能够执行大型任务的强有力的引擎。客户端编程意味着Web浏览器能用来执行任何它能完成的任务,使得返回给用户的结果更加迅捷,而且使得网站更具交互性。不必把用户所有的请求都发给服务器去处理。

  • 插件(plug-in) 程序员将下载的一段代码插入浏览器适当位置,以此来为浏览器添加新功能。下载一次插件即可。某些更快更强大的行为都是通过插件添加到服务器中的。但编写插件并不轻松,也不是构件某特定网站时所要做到事情。插件对于客户端编程的价值在于:它允许专家级程序员不需经过浏览器生产厂商的许可,就可以开发某种语言扩展,并将它们添加到服务器中。因此,插件提供了一个后门,使得可以创建新的客户端编程语言(但并不是所有的客户端编程语言都是以插件的形式实现的)。
  • 脚本语言 插件引发了浏览器脚本语言的开发。通过使用某种脚本语言,可以将客户端程序的源代码直接嵌入到HTML页面中,解释这种语言的插件在HTML页面被显示时自动激活。脚本语言先天就相当易于理解,因为它们只是作为HTML页面的一分部的简单文本,当服务器收到要获取该页面的请求时,它们可以被快速加载。此方法缺点是代码会暴露给任何人去浏览。但是,通常也不会用脚本语言去做相当复杂的事情,所以这个缺点并不太严重。如果你期望有一种脚本语言在Web浏览器不需要任何插件的情况下就可以得到支持,非JavaScript莫属(它与Java之间只存在表面上的相似,需额外的学习曲线,这样命名只是学赶上Java潮流)。遗憾的是,大多数Web浏览器最初都是以彼此相异的方式来实现对JavaScript的支持的,这种差异甚至存在于同一浏览器的不同版本之间。以ECMAScript形式实现的JavaScript的标准化有助于此问题的解决,但是不同浏览器为了跟上这一标准化趋势已花费了相当长的时间(并且这种努力由于微软一直在推进它自己的VBScript形式的标准化而显得无所帮助,VBScript与JavaScript之间也存在暧昧的相似性)。通常,你必须以JavaScript的某个最小公分母形式来编程,以使得你的程序可以在所有的浏览器上运行。JavaScript的错误处理的调试只能用一团糟来形容。作为其使用艰难的证据,我们可以看到直到最近才有人创建了真正复杂的JavaScript脚本片段,并且编写这样的脚本需要超然的奉献精神和超高的专业技巧。这也表明在Web浏览器内部使用脚本语言实际上总被用来解决特定类型的问题,主要用来创建更丰富具有交互性的图形化用户界面(Graphic user interface,GUI)。但是,脚本语言可以解决客户端编程所遇到的百分之八十的问题。你的问题恰好可能正好落在这百分之八十的范围内,由于脚本语言提供了更容易更快捷的开发方式,因此你应该在考虑诸如Java这样更复杂的解决方案之前,优先考虑脚本语言。
  • Java 剩下百分之二十难啃的骨头怎么办呢?Java是处理它们最流行的解决方案。如并发、数据库访问、网络编程和分布式计算。Java是通过applet以及使用Java Web Start来进行客户端编程的。applet是只在Web浏览器中运行的小程序,它是作为网页的一部分而自动下载的(就像网页中图片被自动下载一样)。当applet被激活时,它便开始执行一个程序,这正是它的优雅之处:它提供一种分发软件的方法,一旦用户需要客户端软件时,就自动从服务器把客户端软件分发给用户。用户获取最新版本的客户端软件时不会产生错误,而且也不需要很麻烦的重新安装过程。Java这种设计方式,使得程序员只需创建单一的程序,而且只需要一台有浏览器的计算机,且浏览器内置Java解释器(大多数机器都如此),那么改程序就能自动在这台计算机上运行。由于Java是一种程序的语言,所以在提出对服务器的请求之前和之后,可以在客户端尽可能多地多做些事情。例如,不必跨网络发送一张请求表单来检查自己是否填写了错误的日期或其他参数,客户端计算机就可以快速标出错误数据,而不同等待服务器做出标记并给你传回图片。这不仅立即就获得了高速的响应能力,而且也降低了网络流量和服务器负载,从而不会使整个Internet的速度都慢下来。
  • 备选方案 Java applet并未达到所吹嘘的境界。当Java首度出现时,似乎大家最欢欣鼓舞的莫过于applet了,因为它们最终将解决严峻的客户端可编程性问题,从而提高基于互联网应用的可响应性,同时降低对宽带的需求。人们展望到了大量的可能性。实际上,Web上确实存在一些非常灵巧的applet,但压倒性的向applet的迁移却始终未发生。这其中最大问题可能在于安装JRE所必需的10MB带宽对于一般用户来说过于恐怖了,而微软未选择在IE中包含JRE这一事实也许就此已经封杀了applet的命运。无论怎样,applet始终没有得到大规模应用。尽管如此,applet和Java Web Start应用在某些情况下仍旧很有价值。无论何时,只要你想控制用户的机器,例如在一个公司内部,使用这些技术来发布和更新客户端应用就显得非常恰当,并且可以节省大量时间、人力和财力,特别是需频繁更新时。在图形化用户章节,有一种折中的技术,Macromedia的Flex,它允许创建基于Flash的与applet相当的应用。因为Flash Player在超过90%的Web浏览器上都可用(包含Windows/Linux/Mac),因此被认为是事实上以及被接受的标准。安装和更新Flash Player都十分快捷。ActionScript语言是基于ECMScript的,因此我们对它应该是很熟悉的。Flex使得我们在编程时无需担心浏览器相关性,远比JavaScript要更加吸引人得多。对于客户端编程而言,这是一种值得考虑的备选方案。
  • .NET和C# 曾几何时,Java applet的最大竞争对手是微软的ActiveX-尽管它要求客户端必需运行在Windows平台。之后,微软以.Net平台和C#编程语言的形式推出了与Java全面竞争的对手。.Net平台大致相当于Java虚拟机(JVM,即执行Java程序的软件平台)和Java类库,而C#毫无疑问与Java有类似之处。目前.Net主要受攻击的地方在于其平台可移植性,微软宣传这么做没问题,并且Mono项目(www.go-mono.com)已经有了一个在Linux上运行的.Net的部分实现;但是在该实现完成以及微软不会排斥其中的任何部分之前,.Net作为一种跨平台的解决方案仍旧是一场高风险的赌博。
  • Inernet与Intranet Web是最常用解决客户/服务器问题的方案,因此,即便是解决这个问题的一个子集,特别是公司内部的典型的客户/服务器问题,也一样可以使用这项技术。当Web技术仅限定于特定公司的信息网络时,称之为Intranet(企业内部网)。Intranet比Internet提供了更高的安全性,因为可以物理控制对公司内部服务器的访问。从培训的角度来看,似乎一旦人们理解了浏览器的基本概念之后,对他们来说,处理网页和apple的外观差异就会容易很多,因此对新型系统的学习曲线也就减缓了。

客户端编程要考虑安全问题,程序运行在Internet上时不可能直到它将运行在什么样的平台上,因此,要格外小心,不要传播有bug的代码。你需要跨平台、安全的语言,如脚本语言和Java。传统的物理安装升级程序所浪费的时间是迁移到浏览器上的最主要的原因,因为在浏览器方式下,升级是透明自动的(Java Web Start也是解决此问题的方式之一)。

当面对各种令人眼花缭乱的解决客户端编程问题的解决方案时,最好的方法就是进行性价比分析。认真考虑问题的各种限制,然后思考哪种解决方案可以称为最短的捷径。既然客户端编程仍然需要编程,那么针对自己的特殊应用选取最快的开发方式总是最好的做法。为那些在程序开发中不可避免的问题提早做准备是一种积极的态度。 

1.13.3服务器端编程

服务器端编程是Java取得巨大成功的因素之一。当提出对服务器的请求后,会发生什么?大部分时间,请求只是要求“给我发送一个文件”,之后浏览器会以某种适当的形式解释该文件,例如将其作为HTML页面、图片、Java applet或脚本程序等来解释。更复杂的通常涉及数据库事务。常见的是复杂的数据库搜索请求,服务器对结果进行格式编排,使其称为一个HTML页面返回给客户端。(当然,如果客户端通过Java或脚本程序具备了更多的智能,那么服务器可以只返回原始数据,由客户端进行格式编排,这样更快也降低了对服务器的负载)。另一常见情形是涉及对数据库的修改。这些数据库请求必需通过服务器端处理,这就是所谓的服务器端编程。服务器编程可使用Perl/Python/C++或其他编程语言编写CGI(CGI 是Web 服务器运行时外部程序的规范,按CGI 编写的程序可以扩展服务器功能。CGI 应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI,包括流行的C、C ++、VB 和Delphi 等。CGI 分为标准CGI 和间接CGI两种。标准CGI 使用命令行参数或环境变量表示服务器的详细请求,服务器与浏览器通信采用标准输入输出方式。间接CGI 又称缓冲CGI,在CGI 程序和CGI 接口之间插入一个缓冲程序,缓冲程序与CGI 接口间用标准输入输出进行通信。

CGI(Common Gateway Interface) 是WWW技术中最重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序【CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。CGI规范允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器,CGI将Web的一组简单的静态超媒体文档变成一个完整的新的交互式媒体。CGI在物理上是一段程序,运行在服务器上,提供同客户端HTML页面的接口。这样说大概还不好理解。那么我们看一个实际例子:现在的个人主页上大部分都有一个留言本。留言本的工作是这样的:先由用户在客户端输入一些信息,如评论之类的东西。接着用户按一下“发布或提交”(到目前为止工作都在客户端),浏览器把这些信息传送到服务器的CGI目录下特定的CGI程序中,于是CGI程序在服务器上按照预定的方法进行处理。在本例中就是把用户提交的信息存入指定的文件中。然后CGI程序将执行结果返回给服务器(webServer),然后服务器将结果返回给客户端,表示请求的任务已经结束。此时用户在浏览器里将看到“留言结束”的字样。整个过程结束】程序而实现。其中还包括基于Java的Web服务器,它让你用Java编写的servlet的程序来实现服务器端编程。servlet及其衍生物JSP,是许多开发网站的公司迁移到Java上的两个主要原因,尤其是它们消除了处理具有不同能力的浏览器时所遇到的问题。服务器编程的话题在《企业Java编程思想Thinking in Enterprise Java》一书中有所论述。

1.14总结

了解过程型语言(数据定义和函数调用)的含义,需要通读函数调用和底层概念,以在脑海中建立一个模型。这正是在涉及过程式程序时需要中间表示形式的原因。容易把人搞糊涂,其使用的术语面向的是计算机而非要解决的问题。良好的Java程序定义了两部分内容:用来表示问题空间概念的对象(而不是计算机表示方式的相关内容),以及发送给这些对象的用来表示在此空间内的行为的消息。通过阅读设计良好的面向对象的程序,很容易理解其代码。

OOP和Java也许并不适合所有人。重要的是要正确评估自己的需求,并决定Java是否能够最好地满足这些需求,还是使用其他编程系统(包括你当前正在使用的)才是更好的选择。如果直到自己的需求在可预见的未来会变得非常特殊化,并且Java可能不能满足你的具体限制,那么就应该去考虑其他选择(我特别推荐读者看看Python www.Python.org)。即使最终仍旧选择Java,至少也要理解还有哪些选项可供选择,并且对为什么选择这个方向要有清楚的认识。

猜你喜欢

转载自blog.csdn.net/u010425839/article/details/84261650