|
2007年4月8日
引子 Web 2.0,在过去的一年里也许还是一个新的名词,曾几何时它像网上核武一样爆发了,并以不可阻挡之势燃烧了整个互联网,其热度不压于当年的超女,又曾几何时它悄悄地走进了我们的生活,从陌生走向了熟悉,从概念走向了应用。今天,Web 2.0构成了我们网络生活不可缺少的一部分,今天,你Web 2.0了吗?
什么是Web2.0? 如果你是一只老网虫但还不知道什么是Web2.0,赶紧去跳海吧,不怕死的就去跳(记得带个救生圈,出事了我不负责哈!),怕死的还不赶紧补补课,免得被人看穿了让人笑话。如果你是个善于表现自己的人,多学点东西吧,学了Web 2.0你可以不分对象地大谈特谈地讲述你的Web 2.0观点, 嘿嘿,想象一下别人对你崇拜的表情吧。
Web2.0是以 Flickr、Craigslist、Linkedin、Tribes、Ryze、 Friendster、Del.icio.us、43Things.com等网站为代表,以Blog、TAG、SNS、RSS、wiki等应用为核心,依据六度分隔、xml、ajax等新理论和技术实现的互联网新一代模式。
1. 什么是Wiki
WIKI的来源
WIKI概念的发明人是Ward Cunningham,该词来源于夏威夷语的“wee kee wee kee”,原本是“快点快点” (quick)的意思。
Wiki--一种多人协作的写作工具。Wiki站点可以有多人(甚至任何访问者)维护,每个人都可以发表自己的意见,或者对共同的主题进行扩展或者探讨。
Wiki指一种超文本系统。这种超文本系统支持面向社群的协作式写作,同时也包括一组支持这种写作的辅助工具。有人认为,Wiki系统属于一种人类知识网格系统,我们可以在Web的基础上对Wiki文本进行浏览、创建、更改,而且创建、更改、发布的代价远比HTML文本小;同时Wiki系统还支持面向社群的协作式写作,为协作式写作提供必要帮助;最后,Wiki的写作者自然构成了一个社群,Wiki系统为这个社群提供简单的交流工具。与其它超文本系统相比,Wiki有使用方便及开放的特点,所以Wiki系统可以帮助我们在一个社群内共享某领域的知识。
WIKI可以做什么
WIKI最适合做百科全书、知识库、整理某一个领域的知识等知识型站点,几个分在不同地区的人利用wiki协同工作共同写一本书等等。Wiki技术已经被较好的用在百科全书、手册/FAQ编写、专题知识库方面。
Wiki的特点
使用方便
维护快捷:快速创建、存取、更改超文本页面(这也是为什幺叫作“wiki wiki”的原因)。
格式简单:用简单的格式标记来取代 HTML 的复杂格式标记。(类似所见即所得的风格)
链接方便:通过简单标记,直接以关键字名来建立链接(页面、外部连接、图像等)。
命名平易:关键字名就是页面名称,并且被置于一个单层、平直的名空间中。
有组织
自组织的:同页面的内容一样,整个超文本的组织结构也是可以修改、演化的。
可汇聚的:系统内多个内容重复的页面可以被汇聚于其中的某个,相应的链接结构也随之改变。
可增长
可增长:页面的链接目标可以尚未存在,通过点击链接,我们可以创建这些页面,从而使系统得到增长。
修订历史:记录页面的修订历史,页面的各个版本都可以被获取。
开放性
开放的:社群的成员可以任意创建、修改、删除页面。
可观察:系统内页面的变动可以被访问者观察到。
2.
什么是RSS
RSS是站点用来和其他站点之间共享内容的一种简易方式(也叫聚合内容)的技术。最初源自浏览器“新闻频道”的技术,现在通常被用于新闻和其他按顺序排列的网站,例如Blog。
RSS可以干什么?
1、订阅BLOG(BLOG上,你可以订阅你工作中所需的技术文章;也可以订阅与你有共同爱好的作者的日志,总之,BLOG上你对什么感兴趣你就可以订什么)
2、订阅新闻(无论是奇闻怪事、明星消息、体坛风云,只要你想知道的,都可以订阅)
如何使用RSS
·下载和安装一个RSS新闻阅读器
·从网站提供的聚合新闻目录列表中订阅您感兴趣的新闻栏目的内容
·订阅后您将会及时获得所订阅新闻频道的最新内容
RSS的几个缩写来源
1、Really Simple Syndication(真正简易的聚合)
2、Rich Site Summary(丰富的站点摘要)
3、RDF Site Summary(RDF站点摘要)
RSS新闻特点
对网民而言:对网站而言:
1.没有广告或者图片来影响标题或者文章概要的阅读。
2.RSS阅读器自动更新你定制的网站内容,保持新闻的及时性。
3.用户可以加入多个定制的RSS提要,从多个来源搜集新闻整合到单个数据流中。 1.扩大了网站内容的传播面,也增加了网站访问量,因为访问者调阅的RSS文件和浏览的网页,都是从网站服务器上下载的。
2.RSS文件的网址是固定不变的,网站可以随时改变其中的内容。RSS内容一旦更新,浏览者看到的内容也随即更新了
3.什么是Tag?
Tag(标签)是一种更为灵活、有趣的分类方式,您可以为每篇日志、每个帖子或者每张图片等添加一个或多个Tag(标签),你可以看到网站上所有和您使用了相同Tag的内容,由此和他人产生更多的联系。Tag体现了群体的力量,使得内容之间的相关性和用户之间的交互性大大增强。
比如,你在一篇日志上添加了“读书”和“Tag”两个标签,就能通过这两个tag看到和你有相同兴趣的其他日志。同样,如果你给自己的网络书签贴上不同标签,那么,在下一次去寻找时,会轻易找到自己想要的信息。
那么,如果我贴了Tag,能产生什么效果呢?首先,信息将会条理化。其次,当你积累了一定数量的Tag之后,你会发现自己最关心的话题。GOOGLE的"我的搜索历史"功能就是采用了标签,你的每次搜索关键词都可以成为tag,之后,你会了解自己这一天在关心什么。
当然,你也可以看到有哪些人和自己使用了一样的Tag(标签),进而找到和您志趣相投的人。
2、Tag究竟有哪些不同?
Tag不是关键词,因为,一个机器就没有办法提取一张照片的关键字,但人可以给它设定一个或多个Tag。而Tag真正不同的地方在于,你可以随意用任何词来标记一件事物,只要方便你找到它。因此,这一标志是活跃的、无序的、个人化、相当自我的一种标记方式。
当我可以为我自己的言论作出自己想要的标志,而不是别人给予我的分类,那么,我将说些什么呢?我又会通过这种标志找到什么样的人什么样的文章、图片呢?Tag创造了一个新的无序但充满生机的网络联合体,通过这个联合,人们找到和自己最接近的内容。
3、如何使用Tag?
现在很多网站都使用了Tag模式,只要使用者自身打开了界限,随心所欲地给自己注释标签,不被旧有思维局限住,就对了。简单地说,Tag是一种随心所欲的标签,当我读一篇文章或者看一张图片的时候想什么就写什么,不受原有分类的束缚,怎么想就怎么使用。
Abraham Lincoln
Delivered on the 19th Day of November, 1863
Cemetery Hill, Gettysburg, Pennsylvania
Fourscore and seven years ago, our fathers brought forth upon this continent a new Nation, conceived in Liberty, and dedicated to the proposition that all men are created equal. Now, we are engaged in a great Civil War,testing whether that Nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battlefield of that war. We have come to dedicate a portion of that field as a final resting-place for those who gave their lives that Nation might live. It is altogether fitting and proper that we should do this.
But, in a larger sense, we cannot dedicate, we cannot consecrate, we cannot hallow this ground. The brave men, living and dead, who struggled here, have consecrated it far above our power to add or detract. The world will little note nor long remember what we say here, but it can never forget what they did here. It is for us, the living, rather to be dedicated to the great task remaining before us; that from these honored dead, we take increased devotion to that cause for which they gave the last full measure of devotion; that this Nation, under GOD, shall have a new birth of freedom; and that government of the People by the People and for the People shall not perish from the earth.
葛底斯堡演说
亚伯拉罕·林肯,1963年11月19日
87年前,我们的先辈们在这个大陆上创立了一个新国家,它孕育于自由之中,奉行一切人生来平等的原则。现在我们正从事一场伟大的内战,以考验这个国家,或者任何一个孕育于自由和奉行上述原则的国家是否能够长久存在下去。我们在这场战争中的一个伟大战场上集会。烈士们为使这个国家能够生存下去而献出了自己的生命,我们来到这里,是要把这个战场的一部分奉献给他们作为最后安息之所。我们这样做是完全应该而且是非常恰当的。
但是,从更广泛的意义上来说,这块土地我们不能够奉献,不能够圣化,不能够神化。那些曾在这里战斗过的勇士们,活着的和去世的,已经把这块土地圣化了,这远不是我们微薄的力量所能增减的。我们今天在这里所说的话,全世界不大会注意,也不会长久地记住,但勇士们在这里所做过的事,全世界却永远不会忘记。毋宁说,倒是我们这些还活着的人,应该在这里把自己奉献于勇士们已经如此崇高地向前推进但尚未完成的事业。倒是我们应该在这里把自己奉献于仍然留在我们面前的伟大任务??我们要从这些光荣的死者身上汲取更多的献身精神,来完成他们已经完全彻底为之献身的事业;我们要在这里下定最大的决心,不让这些死者白白牺牲;我们要使国家在上帝福佑下得到自由的新生,要使这个民有、民治、民享的政府永世长存。
Inaugural Address of John F. Kennedy 肯尼迪总统就职演说 January 20, 1961
Vice President Johnson, Mr. Speaker, Mr. Chief Justice, President Eisenhower, Vice President Nixon, President Truman, Reverend Clergy, fellow citizens:
We observe today not a victory of party but a celebration of freedom, symbolizing an end as well as a beginning, signifying renewal as well as change. For I have sworn before you and Almighty God the same solemn oath our forebears prescribed nearly a century and three-quarters ago.
我们今天所看到的,并非是某一党派的胜利,而是自由的庆典。它象征着结束,亦象征着开始;意味着更新,亦意味着变化。因为我已在你们及万能的上帝面前,依着我们先辈175年前写下的誓言宣誓。
The world is very different now. For man holds in his mortal hands the power to abolish all forms of human poverty and all forms of human life. And yet the same revolutionary beliefs for which our forebears fought are still at issue around the globe -- the belief that the rights of man come not from the generosity of the state but from the hand of God.
世界已然今非昔比,因为人类手中已经掌握了巨大的力量,既可以用来消除各种形式的贫困,亦可用以毁灭人类社会。然而,我们先辈曾为之战斗的那些革命性的信念还依然在世界上受人争议——那就是,每个人享有的各项权利决非来自国家政权的慷慨赐予,而是出自上帝之手。
We dare not forget today that we are the heirs of that first revolution. Let the word go forth from this time and place, to friend and foe alike, that the torch has been passed to a new generation of Americans -- born in this century, tempered by war, disciplined by a hard and bitter peace, proud of our ancient heritage -- and unwilling to witness or permit the slow undoing of those human rights to which this nation has always been committed, and to which we are committed today at home and around the world.
今天,我们不敢有忘,我们乃是那第一次革命的后裔。此时,让这个声音从这里同时向我们的朋友和敌人传达:火炬现已传递到新一代美国人手中——他们生于本世纪,既经受过战火的锤炼,又经历过艰难严峻的和平岁月的考验。他们深为我们古老的遗产所自豪——决不愿目睹或听任诸项人权受到无形的侵蚀,这些权利不仅为这个国家始终信守不渝,亦是我们正在国内和世界上誓死捍卫的东西。
Let every nation know, whether it wishes us well or ill, that we shall pay any price, bear any burden, meet any hardship, support any friend, oppose any foe to assure the survival and the success of liberty.
让每一个国家都知道,无论它们对我们抱有善意还是恶意,我们都准备付出任何代价、承受任何重任、迎战任何艰险、支持任何朋友、反对任何敌人,以使自由得以维系和胜利。
This much we pledge -- and more.
这是我们矢志不移的承诺,且远不止此!
To those old allies whose cultural and spiritual origins we share, we pledge the loyalty of faithful friends. United there is little we cannot do in a host of cooperative ventures. Divided there is little we can do, for we dare not meet a powerful challenge at odds and split asunder.
对于那些与我们共享同一文化和精神源头的老朋友,我们许以朋友的忠诚。在许许多多的合作事业中,我们会尽己所能以促进我们的团结,而决不故意制造分裂,因为我们不敢轻易面对由分歧或体系崩溃而导致的巨大挑战。
To those new states whom we welcome to the ranks of the free, we pledge our word that one form of colonial control shall not have passed away merely to be replaced by a far more iron tyranny. We shall not always expect to find them supporting our view. But we shall always hope to find them strongly supporting their own freedom -- and to remember that, in the past, those who foolishly sought power by riding the back of the tiger ended up inside.
对于那些新成立的国家,我们欢迎它们加入自由阵营,并在此许以忠告:某种形式的殖民控制决不会仅仅因为被另一种更为残酷的霸权所取代就消声匿迹。我们不会期待他们始终支持我们的观点,但我们希望他们能始终坚定地维护他们自己的自由——并且牢记,在过去,那些愚蠢地骑上独~裁的虎背以谋求权力的人最终都以葬身虎腹而告终。
To those people in the huts and villages of half the globe struggling to break the bonds of mass misery, we pledge our best efforts to help them help themselves, for whatever period is required -- not because the communists may be doing it, not because we seek their votes, but because it is right.
对于那些寄居于大半个地球上的草舍村落、为着挣脱无尽苦难的枷锁而奋斗的人民,我们承诺将尽我们最大的努力,以使他们获得自助的能力。因为这是时代对我们提出的要求——不是因为共~产~党人可能如此行事、不是因为我们需要他们的选票,仅仅是因为这样做是正当的。
If a free society cannot help the many who are poor, it cannot save the few who are rich.
如果一个自由的社会不能帮助贫穷的多数,它就不能拯救那富裕的少数。
To our sister republics south of our border, we offer a special pledge: to convert our good words into good deeds, in a new alliance for progress, to assist free men and free governments in casting off the chains of poverty. But this peaceful revolution of hope cannot become the prey of hostile powers. Let all our neighbors know that we shall join with them to oppose aggression or subversion anywhere in the Americas.
对于我们的南部邻邦共和国,我们许以特殊的承诺:将我们的良言转为善行,在为了进步而结成的新盟邦里,帮助自由的人民和自由的政府摆脱贫困。但这一希翼中的和平革命不能成为敌对势力的牺牲品,让我们所有的邻邦都知道,我们将与他们一道,反对发生在美洲任何地区的侵略和颠覆。
And let every other power know that this hemisphere intends to remain the master of its own house.
让所有其他势力都知道,这一半球的人民致力于维护他们作为自己家园主人的地位。
To that world assembly of sovereign states, the United Nations, our last best hope in an age where the instruments of war have far outpaced the instruments of peace, we renew our pledge of support -- to prevent it from becoming merely a forum for invective, to strengthen its shield of the new and the weak, and to enlarge the area in which its writ may run.
对于那个主权国家的世界性会议组织——联合国,我们最后一次良好祝愿是发生在战争机器远远超过和平机器的时代。为了防止它沦为仅仅用来谩骂攻讦的论坛,为了加强它对新成立国家及弱小国家的保障功能、为了扩展其权力涵盖的领域,我们现在重申对它的支持承诺。
Finally, to those nations who would make themselves our adversary, we offer not a pledge but a request: that both sides begin anew the quest for peace -- before the dark powers of destruction unleashed by science engulf all humanity in planned or accidental self-destruction.
最后,对于那些主动站到我们敌对面的国家,我们提出的不是许诺,而是恳求:在被科学释放出的、黑暗的破坏力量以有计划的或偶然性的自我毁灭方式吞噬全人类之前,恳求双方再一次地开始谋求和平的努力。
We dare not tempt them with weakness. For only when our arms are sufficient beyond doubt can we be certain beyond doubt that they will never be employed. But neither can two great and powerful groups of nations take comfort from our present course -- both sides overburdened by the cost of modern weapons, both rightly alarmed by the steady spread of the deadly atom, yet both racing to alter that uncertain balance of terror that stays the hand of mankind's final war. So let us begin anew -- remembering on both sides that civility is not a sign of weakness, and sincerity is always subject to proof.
我们不敢以软弱诱惑它们,因为只有当我们的军备充足到确切无疑的程度时,我们才能确切无疑地肯定它们永远不会被投入使用。但这两个强大的国家集团都无法从彼此当前的做法中得到安慰——双方都背负了过高的现代武器系统的成本、双方都理所当然地对致死性原子武器的持续扩散感到惊恐不安,但双方都竞相改变不确定的恐怖均衡,这种均衡恰恰抑制了人类最后摊牌的冲动。
Let us never negotiate out of fear. But let us never fear to negotiate.
让我们永远不要因为惧怕而谈判,让我们永远不要惧怕谈判。
Let both sides explore what problems unite us instead of belaboring those problems which divide us.
让双方探寻那些能将我们团结在一起的因素,而不是那些刻意挑出那些分裂我们的因素。
Let both sides, for the first time, formulate serious and precise proposals for the inspection and control of arms, and bring the absolute power to destroy other nations under the absolute control of all nations.
让双方首先提出认真细致的方案来核查及控制军备,并将毁灭其他国家的绝对力量置于所有国家的绝对控制之下。
Let both sides seek to invoke the wonders of science instead of its terrors. Together let us explore the stars, conquer the deserts, eradicate disease, tap the ocean depths, and encourage the arts and commerce.
让双方努力去激发科学的奇迹,而非科学的恐怖。让我们一同探索星空、征服沙漠、消除疾病、开发海洋深处,鼓励艺术和商业。
Let both sides unite to heed, in all corners of the earth, the command of Isaiah -- to "undo the heavy burdens... [and] let the oppressed go free."
让双方在世界每一个角落,都共同信守《圣经.以赛亚书》中的教诲——“卸下重负……让被压迫者自由。”
And if a beachhead of cooperation may push back the jungle of suspicion, let both sides join in creating a new endeavor -- not a new balance of power, but a new world of law -- where the strong are just, and the weak secure, and the peace preserved.
如果合作的滩头堡能够遏制重重猜疑,让双方携手进行新的努力——不是为了建立新的势力均衡,而是为了建立新的规则体系——以使强者正义,弱者安全,和平维系。
All this will not be finished in the first one hundred days. Nor will it be finished in the first one thousand days; nor in the life of this Administration; nor even perhaps in our lifetime on this planet. But let us begin.
所有这些工作将不会在从现在起的一百天、一千天内完成,也不会在本届行政分支任期内完成,甚至可能不会在我们的有生之年完成,但是,请让我们现在开始工作。
In your hands, my fellow citizens, more than mine, will rest the final success or failure of our course. Since this country was founded, each generation of Americans has been summoned to give testimony to its national loyalty. The graves of young Americans who answered the call to service surround the globe.
我的同胞们,我们事业的最终成败将掌握在你们的手中而不仅仅是我的手中。从这个国家被创建那天起,每一代美国人都被召唤去证实自己对国家的忠诚。那些响应号召献身国家的年轻美国人的安息之所遍布全球。
Now the trumpet summons us again -- not as a call to bear arms, though arms we need -- not as a call to battle, though embattled we are -- but a call to bear the burden of a long twilight struggle, year in and year out, rejoicing in hope, patient in tribulation, a struggle against the common enemies of man: tyranny, poverty, disease, and war itself.
现在,召唤的号角又一次吹响——不是号召我们扛起武器,虽然武器是我们所需要的——也不是号召我们去参加战斗,虽然我们准备战斗——而是号召我们年复一年地去进行一场漫长而未分胜负的搏斗,在希望中欢乐,而患难中忍耐,以反对人类共同的敌人:暴政、贫困、疾病以及战争本身。
Can we forge against these enemies a grand and global alliance, North and South, East and West, that can assure a more fruitful life for all mankind? Will you join in that historic effort?
为了反对这些敌人,我们能够将南方与北方、东方与西方团结起来,熔铸成一个伟大的和全球性的联盟,以确保全人类得享更为成果累累的生活吗?你们愿意参与这项历史性的努力吗?
In the long history of the world, only a few generations have been granted the role of defending freedom in its hour of maximum danger. I do not shrink from this responsibility -- I welcome it. I do not believe that any of us would exchange places with any other people or any other generation. The energy, the faith, the devotion which we bring to this endeavor will light our country and all who serve it. And the glow from that fire can truly light the world.
在世界历史的长河里,只有少数几代人被赋予了在自由面临最大危机时捍卫自由的使命,我不会畏缩于这一责任——我欢迎它!我也不相信我们中的任何人会愿意与其他国家的人民或其他世代的人民易地而处。我们在这场努力中所倾注的精力、信念和奉献将照耀我们的国家以及所有为之献身的人,火焰所放射出的光芒必将普照全世界。
And so, my fellow Americans, ask not what your country can do for you; ask what you can do for your country.
所以,我的美国同胞们,不要问你的国家为你做了什么,而应问你能为你的国家做些什么。
My fellow citizens of the world, ask not what America will do for you, but what together we can do for the freedom of man.
我的世界同胞们,不要问美国将为你做些什么,而应问我们应该一起为了全人类的自由做些什么。
Finally, whether you are citizens of America or citizens of the world, ask of us here the same high standards of strength and sacrifice which we ask of you. With a good conscience our only sure reward, with history the final judge of our deeds, let us go forth to lead the land we love, asking His blessing and His help, but knowing that here on earth God's work must truly be our own.
最后,无论是美国公民还是世界其他国家的公民,请用我们要求于你们的关于力量和牺牲的高标准来要求我们,本着我们唯一可以指望有所回报的善意良知,依着能最终裁决我们功业的历史,让我们着手领导我们所热爱的国家,在祈求神的赐福和神的帮助的同时,也能深切体认,在这片土地上,神的工作必定也是我们自己所应承担的使命。
摘要:
虽然session机制在web应用程序中被采用已经很长时间了,但是仍然有很多人不清楚session机制的本质,以至不能正确的应用这一技术。本文将详细讨论session的工作机制并且对在Java web application中应用session机制时常见的问题作出解答。
一、术语session在我的经验里,session这个词被滥用的程度大概仅次于transaction,更加有趣的是transaction与session在某些语境下的含义是相同的。 session,中文经常翻译为会话,其本来的含义是指有始有终的一系列动作/消息,比如打电话时从拿起电话拨号到挂断电话这中间的一系列过程可以称之为一个 session。有时候我们可以看到这样的话“在一个浏览器会话期间,...”,这里的会话一词用的就是其本义,是指从一个浏览器窗口打开到关闭这个期间 ①。最混乱的是“用户(客户端)在一次会话期间”这样一句话,它可能指用户的一系列动作(一般情况下是同某个具体目的相关的一系列动作,比如从登录到选购商品到结账登出这样一个网上购物的过程,有时候也被称为一个transaction),然而有时候也可能仅仅是指一次连接,也有可能是指含义①,其中的差别只能靠上下文来推断②。 然而当session一词与网络协议相关联时,它又往往隐含了“面向连接”和/或“保持状态”这样两个含义, “面向连接”指的是在通信双方在通信之前要先建立一个通信的渠道,比如打电话,直到对方接了电话通信才能开始,与此相对的是写信,在你把信发出去的时候你并不能确认对方的地址是否正确,通信渠道不一定能建立,但对发信人来说,通信已经开始了。“保持状态”则是指通信的一方能够把一系列的消息关联起来,使得消息之间可以互相依赖,比如一个服务员能够认出再次光临的老顾客并且记得上次这个顾客还欠店里一块钱。这一类的例子有“一个TCP session”或者 “一个POP3 session”③。 而到了web服务器蓬勃发展的时代,session在web开发语境下的语义又有了新的扩展,它的含义是指一类用来在客户端与服务器之间保持状态的解决方案④。有时候session也用来指这种解决方案的存储结构,如“把xxx保存在session 里”⑤。由于各种用于web开发的语言在一定程度上都提供了对这种解决方案的支持,所以在某种特定语言的语境下,session也被用来指代该语言的解决方案,比如经常把Java里提供的javax.servlet.http.HttpSession简称为session⑥。 鉴于这种混乱已不可改变,本文中session一词的运用也会根据上下文有不同的含义,请大家注意分辨。 在本文中,使用中文“浏览器会话期间”来表达含义①,使用“session机制”来表达含义④,使用“session”表达含义⑤,使用具体的“HttpSession”来表达含义⑥ 二、HTTP协议与状态保持HTTP 协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。 然而聪明(或者贪心?)的人们很快发现如果能够提供一些按需生成的动态信息会使web变得更加有用,就像给有线电视加上点播功能一样。这种需求一方面迫使HTML逐步添加了表单、脚本、DOM等客户端行为,另一方面在服务器端则出现了CGI规范以响应客户端的动态请求,作为传输载体的HTTP协议也添加了文件上载、 cookie这些特性。其中cookie的作用就是为了解决HTTP协议无状态的缺陷所作出的努力。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。 让我们用几个例子来描述一下cookie和session机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠,然而一次性消费5杯咖啡的机会微乎其微,这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案: 1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。 2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。 3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。 由于HTTP协议是无状态的,而出于种种考虑也不希望使之成为有状态的,因此,后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。 三、理解cookie机制cookie机制的基本原理就如上面的例子一样简单,但是还有几个问题需要解决:“会员卡”如何分发;“会员卡”的内容;以及客户如何使用“会员卡”。 正统的cookie分发是通过扩展HTTP协议来实现的,服务器通过在HTTP的响应头中加上一行特殊的指示以提示浏览器按照指示生成相应的cookie。然而纯粹的客户端脚本如JavaScript或者VBScript也可以生成cookie。 而cookie 的使用是由浏览器按照一定的原则在后台自动发送给服务器的。浏览器检查所有存储的cookie,如果某个cookie所声明的作用范围大于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。意思是麦当劳的会员卡只能在麦当劳的店里出示,如果某家分店还发行了自己的会员卡,那么进这家店的时候除了要出示麦当劳的会员卡,还要出示这家店的会员卡。 cookie的内容主要包括:名字,值,过期时间,路径和域。 其中域可以指定某一个域比如.google.com,相当于总店招牌,比如宝洁公司,也可以指定一个域下的具体某台机器比如www.google.com或者froogle.google.com,可以用飘柔来做比。 路径就是跟在域名后面的URL路径,比如/或者/foo等等,可以用某飘柔专柜做比。 路径与域合在一起就构成了cookie的作用范围。 如果不设置过期时间,则表示这个cookie的生命期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览器会话期的 cookie被称为会话cookie。会话cookie一般不存储在硬盘上而是保存在内存里,当然这种行为并不是规范规定的。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。 存储在硬盘上的cookie 可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存里的cookie,不同的浏览器有不同的处理方式。对于IE,在一个打开的窗口上按 Ctrl-N(或者从文件菜单)打开的窗口可以与原窗口共享,而使用其他方式新开的IE进程则不能共享已经打开的窗口的内存cookie;对于 Mozilla Firefox0.8,所有的进程和标签页都可以共享同样的cookie。一般来说是用javascript的window.open打开的窗口会与原窗口共享内存cookie。浏览器对于会话cookie的这种只认cookie不认人的处理方式经常给采用session机制的web应用程序开发者造成很大的困扰。 下面就是一个goolge设置cookie的响应头的例子
HTTP/1.1 302 Found
Location: http://www.google.com/intl/zh-CN/
Set-Cookie: PREF=ID=0565f77e132de138:NW=1:TM=1098082649:LM=1098082649:S=KaeaCFPo49RiA_d8; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com
Content-Type: text/html
这是使用HTTPLook这个HTTP Sniffer软件来俘获的HTTP通讯纪录的一部分 浏览器在再次访问goolge的资源时自动向外发送cookie 用Firefox可以很容易的观察现有的cookie的值 使用HTTPLook配合Firefox可以很容易的理解cookie的工作原理。 IE也可以设置在接受cookie前询问 四、理解session机制session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。 保存这个session id的方式可以采用cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个cookie的名字都是类似于SEEESIONID,而。比如weblogic对于web应用程序生成的cookie,JSESSIONID= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764,它的名字就是 JSESSIONID。 由于cookie可以被人为的禁止,必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器。经常被使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,一种是作为URL路径的附加信息,表现形式为http://...../xxx;jsessionid= ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764 另一种是作为查询字符串附加在URL后面,表现形式为http://...../xxx?jsessionid=ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764 这两种方式对于用户来说是没有区别的,只是服务器在解析的时候处理的方式不同,采用第一种方式也有利于把session id的信息和正常程序参数区分开来。 为了在整个交互过程中始终保持状态,就必须在每个客户端可能请求的路径后面都包含这个session id。 另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。比如下面的表单
<form name="testform" action="/xxx">
<input type="text">
</form>
在被传递给客户端之前将被改写成
<form name="testform" action="/xxx">
<input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764">
<input type="text">
</form>
这种技术现在已较少应用,笔者接触过的很古老的iPlanet6(SunONE应用服务器的前身)就使用了这种技术。 实际上这种技术可以简单的用对action应用URL重写来代替。 在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览器,session就消失了”。其实可以想象一下会员卡的例子,除非顾客主动对店家提出销卡,否则店家绝对不会轻易删除顾客的资料。对session来说也是一样的,除非程序通知服务器删除一个session,否则服务器会一直保留,程序一般都是在用户做log off的时候发个指令去删除session。然而浏览器从来不会主动在关闭之前通知服务器它将要关闭,因此服务器根本不会有机会知道浏览器已经关闭,之所以会有这种错觉,是大部分session机制都使用会话cookie来保存session id,而关闭浏览器后这个 session id就消失了,再次连接服务器时也就无法找到原来的session。如果服务器设置的cookie被保存到硬盘上,或者使用某种手段改写浏览器发出的HTTP请求头,把原来的session id发送给服务器,则再次打开浏览器仍然能够找到原来的session。 恰恰是由于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间,当距离客户端上一次使用session的时间超过这个失效时间时,服务器就可以认为客户端已经停止了活动,才会把session删除以节省存储空间。 五、理解javax.servlet.http.HttpSessionHttpSession是Java平台对session机制的实现规范,因为它仅仅是个接口,具体到每个web应用服务器的提供商,除了对规范支持之外,仍然会有一些规范里没有规定的细微差异。这里我们以BEA的Weblogic Server8.1作为例子来演示。 首先,Weblogic Server提供了一系列的参数来控制它的HttpSession的实现,包括使用cookie的开关选项,使用URL重写的开关选项,session持久化的设置,session失效时间的设置,以及针对cookie的各种设置,比如设置cookie的名字、路径、域, cookie的生存时间等。 一般情况下,session都是存储在内存里,当服务器进程被停止或者重启的时候,内存里的session也会被清空,如果设置了session的持久化特性,服务器就会把session保存到硬盘上,当服务器进程重新启动或这些信息将能够被再次使用, Weblogic Server支持的持久性方式包括文件、数据库、客户端cookie保存和复制。 复制严格说来不算持久化保存,因为session实际上还是保存在内存里,不过同样的信息被复制到各个cluster内的服务器进程中,这样即使某个服务器进程停止工作也仍然可以从其他进程中取得session。 cookie生存时间的设置则会影响浏览器生成的cookie是否是一个会话cookie。默认是使用会话cookie。有兴趣的可以用它来试验我们在第四节里提到的那个误解。 cookie的路径对于web应用程序来说是一个非常重要的选项,Weblogic Server对这个选项的默认处理方式使得它与其他服务器有明显的区别。后面我们会专题讨论。 关于session的设置参考[5] http://e-docs.bea.com/wls/docs70/webapp/weblogic_xml.html#1036869 六、HttpSession常见问题(在本小节中session的含义为⑤和⑥的混合) 1、session在何时被创建 一个常见的误解是以为session在有客户端访问时就被创建,然而事实是直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建,注意如果JSP没有显示的使用 <% @page session="false"%> 关闭session,则JSP文件在编译成Servlet时将会自动加上这样一条语句 HttpSession session = HttpServletRequest.getSession(true);这也是JSP中隐含的 session对象的来历。 由于session会消耗内存资源,因此,如果不打算使用session,应该在所有的JSP中关闭它。 2、session何时被删除 综合前面的讨论,session在下列情况下被删除a.程序调用HttpSession.invalidate();或b.距离上一次收到客户端发送的session id时间间隔超过了session的超时设置;或c.服务器进程被停止(非持久session) 3、如何做到在浏览器关闭时删除session 严格的讲,做不到这一点。可以做一点努力的办法是在所有的客户端页面里使用javascript代码window.oncolose来监视浏览器的关闭动作,然后向服务器发送一个请求来删除session。但是对于浏览器崩溃或者强行杀死进程这些非常规手段仍然无能为力。 4、有个HttpSessionListener是怎么回事 你可以创建这样的listener去监控session的创建和销毁事件,使得在发生这样的事件时你可以做一些相应的工作。注意是session的创建和销毁动作触发listener,而不是相反。类似的与HttpSession有关的listener还有 HttpSessionBindingListener,HttpSessionActivationListener和 HttpSessionAttributeListener。 5、存放在session中的对象必须是可序列化的吗 不是必需的。要求对象可序列化只是为了session能够在集群中被复制或者能够持久保存或者在必要时server能够暂时把session交换出内存。在 Weblogic Server的session中放置一个不可序列化的对象在控制台上会收到一个警告。我所用过的某个iPlanet版本如果 session中有不可序列化的对象,在session销毁时会有一个Exception,很奇怪。 6、如何才能正确的应付客户端禁止cookie的可能性 对所有的URL使用URL重写,包括超链接,form的action,和重定向的URL,具体做法参见[6] http://e-docs.bea.com/wls/docs70/webapp/sessions.html#100770 7、开两个浏览器窗口访问应用程序会使用同一个session还是不同的session 参见第三小节对cookie的讨论,对session来说是只认id不认人,因此不同的浏览器,不同的窗口打开方式以及不同的cookie存储方式都会对这个问题的答案有影响。 8、如何防止用户打开两个浏览器窗口操作导致的session混乱 这个问题与防止表单多次提交是类似的,可以通过设置客户端的令牌来解决。就是在服务器每次生成一个不同的id返回给客户端,同时保存在session里,客户端提交表单时必须把这个id也返回服务器,程序首先比较返回的id与保存在session里的值是否一致,如果不一致则说明本次操作已经被提交过了。可以参看《J2EE核心模式》关于表示层模式的部分。需要注意的是对于使用javascript window.open打开的窗口,一般不设置这个id,或者使用单独的id,以防主窗口无法操作,建议不要再window.open打开的窗口里做修改操作,这样就可以不用设置。 9、为什么在Weblogic Server中改变session的值后要重新调用一次session.setValue 做这个动作主要是为了在集群环境中提示Weblogic Server session中的值发生了改变,需要向其他服务器进程复制新的session值。 10、为什么session不见了 排除session正常失效的因素之外,服务器本身的可能性应该是微乎其微的,虽然笔者在iPlanet6SP1加若干补丁的Solaris版本上倒也遇到过;浏览器插件的可能性次之,笔者也遇到过3721插件造成的问题;理论上防火墙或者代理服务器在cookie处理上也有可能会出现问题。 出现这一问题的大部分原因都是程序的错误,最常见的就是在一个应用程序中去访问另外一个应用程序。我们在下一节讨论这个问题。 七、跨应用程序的session共享常常有这样的情况,一个大项目被分割成若干小项目开发,为了能够互不干扰,要求每个小项目作为一个单独的web应用程序开发,可是到了最后突然发现某几个小项目之间需要共享一些信息,或者想使用session来实现SSO(single sign on),在session中保存login的用户信息,最自然的要求是应用程序间能够访问彼此的session。 然而按照Servlet规范,session的作用范围应该仅仅限于当前应用程序下,不同的应用程序之间是不能够互相访问对方的session的。各个应用服务器从实际效果上都遵守了这一规范,但是实现的细节却可能各有不同,因此解决跨应用程序session共享的方法也各不相同。 首先来看一下Tomcat是如何实现web应用程序之间session的隔离的,从 Tomcat设置的cookie路径来看,它对不同的应用程序设置的cookie路径是不同的,这样不同的应用程序所用的session id是不同的,因此即使在同一个浏览器窗口里访问不同的应用程序,发送给服务器的session id也可以是不同的。 根据这个特性,我们可以推测Tomcat中session的内存结构大致如下。 笔者以前用过的iPlanet也采用的是同样的方式,估计SunONE与iPlanet之间不会有太大的差别。对于这种方式的服务器,解决的思路很简单,实际实行起来也不难。要么让所有的应用程序共享一个session id,要么让应用程序能够获得其他应用程序的session id。 iPlanet中有一种很简单的方法来实现共享一个session id,那就是把各个应用程序的cookie路径都设为/(实际上应该是/NASApp,对于应用程序来讲它的作用相当于根)。
<session-info>
<path>/NASApp</path>
</session-info>
需要注意的是,操作共享的session应该遵循一些编程约定,比如在session attribute名字的前面加上应用程序的前缀,使得 setAttribute("name", "neo")变成setAttribute("app1.name", "neo"),以防止命名空间冲突,导致互相覆盖。 在Tomcat中则没有这么方便的选择。在Tomcat版本3上,我们还可以有一些手段来共享session。对于版本4以上的Tomcat,目前笔者尚未发现简单的办法。只能借助于第三方的力量,比如使用文件、数据库、JMS或者客户端cookie,URL参数或者隐藏字段等手段。 我们再看一下Weblogic Server是如何处理session的。 从截屏画面上可以看到Weblogic Server对所有的应用程序设置的cookie的路径都是/,这是不是意味着在Weblogic Server中默认的就可以共享session了呢?然而一个小实验即可证明即使不同的应用程序使用的是同一个session,各个应用程序仍然只能访问自己所设置的那些属性。这说明Weblogic Server中的session的内存结构可能如下 对于这样一种结构,在 session机制本身上来解决session共享的问题应该是不可能的了。除了借助于第三方的力量,比如使用文件、数据库、JMS或者客户端 cookie,URL参数或者隐藏字段等手段,还有一种较为方便的做法,就是把一个应用程序的session放到ServletContext中,这样另外一个应用程序就可以从ServletContext中取得前一个应用程序的引用。示例代码如下, 应用程序A
context.setAttribute("appA", session);
应用程序B
contextA = context.getContext("/appA");
HttpSession sessionA = (HttpSession)contextA.getAttribute("appA");
值得注意的是这种用法不可移植,因为根据ServletContext的JavaDoc,应用服务器可以处于安全的原因对于context.getContext("/appA");返回空值,以上做法在Weblogic Server 8.1中通过。 那么Weblogic Server为什么要把所有的应用程序的cookie路径都设为/呢?原来是为了SSO,凡是共享这个session的应用程序都可以共享认证的信息。一个简单的实验就可以证明这一点,修改首先登录的那个应用程序的描述符weblogic.xml,把cookie路径修改为/appA 访问另外一个应用程序会重新要求登录,即使是反过来,先访问cookie路径为/的应用程序,再访问修改过路径的这个,虽然不再提示登录,但是登录的用户信息也会丢失。注意做这个实验时认证方式应该使用FORM,因为浏览器和web服务器对basic认证方式有其他的处理方式,第二次请求的认证不是通过 session来实现的。具体请参看[7] secion 14.8 Authorization,你可以修改所附的示例程序来做这些试验。 八、总结session机制本身并不复杂,然而其实现和配置上的灵活性却使得具体情况复杂多变。这也要求我们不能把仅仅某一次的经验或者某一个浏览器,服务器的经验当作普遍适用的经验,而是始终需要具体情况具体分析。 摘要:虽然session机制在web应用程序中被采用已经很长时间了,但是仍然有很多人不清楚session机制的本质,以至不能正确的应用这一技术。本文将详细讨论session的工作机制并且对在Java web application中应用session机制时常见的问题作出解答。
counting 1 bits C implementations
Here are C implementations of all the methods for counting 1 bits mentioned in that node. (Go read that first, if you haven't already.) All of the statistical information is purely anecdotal, but for what it's worth, it's based on my testing the code on a Pentium 3 and a Celeron 2, using the cl compiler of Microsoft Visual C++, and on a Sun Ultra 5, using gcc and Sun's own cc. For testing 64-bit code, I used __int64 on the Intel machines, and long long on the Sparc. It's worth noting that while Sun's compiler outputs faster executables than gcc, it doesn't change the relative performance of the different methods.
Table Lookup
Use a pre-built lookup table of all the 1-bit counts for every possibly byte, then index into that for each byte that comprises the word. This is the fastest method (slightly) for 32 bits on both Intel and Sparc, and (even more slightly) the fastest for 64 bits on Sparc, falling to second fastest on 64 bits on Intel. Changing the lookup table from anything but unsigned or int makes it a little slower (what with the extra casting and byte-loading the compiler is forced to add.)
unsigned numbits_lookup_table[256] = {
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2,
3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3,
3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3,
4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4,
3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5,
6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4,
4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5,
3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3,
4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6,
6, 7, 6, 7, 7, 8
};
unsigned numbits_lookup(unsigned i)
{
unsigned n;
n = numbits_lookup_table[i & 0xff];
n += numbits_lookup_table[i>>8 & 0xff];
n += numbits_lookup_table[i>>16 & 0xff];
n += numbits_lookup_table[i>>24 & 0xff];
return n;
}
Counters
If you want a full explanation of how this works, read my writeup at counting 1 bits, but suffice it to say that you are essentially partitioning the word into groups, and combining the groups by adding them together in pairs until you are left with only one group, which is the answer. (performance notes in the next section.)
unsigned numbits(unsigned int i)
{
unsigned int const MASK1 = 0x55555555;
unsigned int const MASK2 = 0x33333333;
unsigned int const MASK4 = 0x0f0f0f0f;
unsigned int const MASK8 = 0x00ff00ff;
unsigned int const MASK16 = 0x0000ffff;
i = (i&MASK1 ) + (i>>1 &MASK1 );
i = (i&MASK2 ) + (i>>2 &MASK2 );
i = (i&MASK4 ) + (i>>4 &MASK4 );
i = (i&MASK8 ) + (i>>8 &MASK8 );
i = (i&MASK16) + (i>>16&MASK16);
return i;
}
Optimized Counters
call pointed out in counting 1 bits that you could optimize the Counters method further if you pay attention to which bits you care about and which you don't, which allows you to skip applying some of the masks.
Some symbols that I'll use to represent what's going on:
0 : bits we know are zero from the previous step
o : bits we know are zero due to masking
- : bits we know are zero due to shifting
X : bits that might be 1 and we care about their values
x : bits that might be 1 but we don't care about their values
So a 0 plus a 0 is still a 0 , obviously; the tricky ones are the others, but they're not even so bad. 0 plus X is X , since if the X is a 0 or a 1 , added to 0 it will pass through unchanged. However, X plus X is XX , because the sum can range from 0 (0+0 ), to 10 (1+1 ). The same holds true with x s, once those show up.
Step 1:
oXoXoXoXoXoXoXoXoXoXoXoXoXoXoXoX
+ -XoXoXoXoXoXoXoXoXoXoXoXoXoXoXoX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 2:
ooXXooXXooXXooXXooXXooXXooXXooXX
+ --XXooXXooXXooXXooXXooXXooXXooXX
0XXX0XXX0XXX0XXX0XXX0XXX0XXX0XXX
Step 3:
oooo0XXXoooo0XXXoooo0XXXoooo0XXX
+ ----0XXXoooo0XXXoooo0XXXoooo0XXX
0000XXXX0000XXXX0000XXXX0000XXXX
Step 4:
oooooooo0000XXXXoooooooo0000XXXX
+ --------0000XXXXoooooooo0000XXXX
00000000000XXXXX00000000000XXXXX
Step 5:
oooooooooooooooo00000000000XXXXX
+ ----------------00000000000XXXXX
00000000000000000000000000XXXXXX
You'll notice that the higher the step, the more known zeros (0 ) there are. call's suggestion was to change step 5 to this:
Step 5:
ooooooooooooxxxx00000000000XXXXX
+ ----------------00000000000XXXXX
000000000000xxxx0000000000XXXXXX
(mask) ooooooooooooooooooooooooooXXXXXX
(where "(mask) " means "after adding, apply a mask".)
However, you can go back even further and apply the same technique - all the way to step 3, in fact. The best I can think to optimize this changes the last three steps into the following: Step 3:
0xxx0XXX0xxx0XXX0xxx0XXX0xxx0XXX
+ ----0XXX0xxx0XXX0xxx0XXX0xxx0XXX
0xxxXXXX0xxxXXXX0xxxXXXX0xxxXXXX
(mask) 0000XXXX0000XXXX0000XXXX0000XXXX
Step 4:
0000xxxx0000XXXX0000xxxx0000XXXX
+ --------0000XXXX0000xxxx0000XXXX
0000xxxx000XXXXX000xxxxx000XXXXX
Step 5:
0000xxxx000xxxxx000xxxxx000XXXXX
+ ----------------000xxxxx000XXXXX
0000xxxx000xxxxx00xxxxxx00XXXXXX
(mask) ooooooooooooooooooooooooooXXXXXX
Anyway, that's all very lovely, but here's the C to do it:
unsigned numbits(unsigned int i)
{
unsigned int const MASK1 = 0x55555555;
unsigned int const MASK2 = 0x33333333;
unsigned int const MASK4 = 0x0f0f0f0f;
unsigned int const MASK6 = 0x0000003f;
unsigned int const w = (v & MASK1) + ((v >> 1) & MASK1);
unsigned int const x = (w & MASK2) + ((w >> 2) & MASK2);
unsigned int const y = (x + (x >> 4) & MASK4);
unsigned int const z = (y + (y >> 8));
unsigned int const c = (z + (z >> 16)) & MASK6;
return c;
}
The performance on this method is marginally worse than the lookup method in the 32 bit cases, slightly better than lookup on 64 bit Intel, and right about the same on 64 bit Sparc. Of note is the fact that loading one of these bitmasks into a register actually takes two instructions on RISC machines, and a longer-than-32-bit instruction on the Intel, because it's impossible to pack an instruction and 32 bits worth of data into a single 32 bit instruction. See the bottom of jamesc's writeup at MIPS for more details on that...
Mind-bending "best" method (even more optimized counters)
A slightly-modified version of the code on this page: http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel, which in turn stole the code from the "Software Optimization Guide for AMD AthlonTM 64 and OpteronTM Processors":
unsigned numbits(unsigned int i)
{
unsigned int const MASK1 = 0x55555555;
unsigned int const MASK2 = 0x33333333;
unsigned int const MASK4 = 0x0f0f0f0f;
unsigned int const w = v - ((v >> 1) & MASK1);
unsigned int const x = (w & MASK2) + ((w >> 2) & MASK2);
unsigned int const y = (x + (x >> 4) & MASK4);
unsigned int const c = (y * 0x01010101) >> 24;
return c;
}
This method is identical to the "Optimized Counters" method, with two tricks applied:
- To get rid of an AND in the first line: instead of adding adjacent bits, it subtracts the high bit by itself of a pair of the bits from the pair together, because the results are the same.
00 - 0 = 0 , 01 - 0 = 01 , 10 - 1 = 01 , 11 - 1 = 10
- To merge the last two lines into one, it uses a multiply and a shift, which adds the four remaining byte-sized "counters" together in one step.
Subtract 1 and AND
See counting 1 bits SPOILER for a fuller explanation of this one, but basically the lowest 1-bit gets zeroed out every iteration, so when you run out of 1s to zero, you've iterated to the number of bits in the word. Clever. Unfortunately, not that terribly fast; it's roughly two to three times slower than the lookup and counters methods on both architectures.
unsigned numbits_subtraction(unsigned i)
{
unsigned n;
for(n=0; i; n++)
i &= i-1;
return n;
}
Straightforwardly Examine Each Bit
The most easily understandable and slowest method: iterate over all the bits in the word; if the current bit is a 1, then increment the counter, otherwise, do nothing. That's actually done here by looking at the least-significant bit on each iteration, then shifting to the right one, and iterating until there are no more 1 bits in the word. There's a little optimization in the #ifndef here: instead of doing if (i & 1) n++; , which uses a branch instruction, just add the actual value of the least-significant bit to the counter ( n += (i & 1); ), as it will be a 1 when you want to add 1, and 0 when you don't. (We're just twiddling bits anyway, so why not?) This actually makes the processor do more adds, but adding is fast, and branching is slow, on modern processors, so it turns out to be about twice as fast. However, even "twice as fast" is still four to five times slower than the lookup method, again, on all architectures.
unsigned numbits(unsigned int i)
{
unsigned n;
for(n=0; i; i >>= 1)
#ifndef MORE_OPTIMAL
if (i & 1) n++;
#else
n += (i & 1);
#endif
return n;
}
Now, why does this all matter? It doesn't, really, but it was sure a good way to waste some time, and maybe someone learned some optimizing tricks from it... (Well, I did, actually - so I hope someone else did as well.) |
步一:定义标签
步二:创建标签库描述器TLD diagnostics.tld, 将它放在WEB-INF目录下的tlds文件夹下,diagnostics.tld如下:
<?xml version="1.0"?> <taglib> <tlibversion>1.0</tlibversion> <jspversion>1.1</jspversion> <shortname>diag</shortname> <tag> <name>getWebServer</name> <tagclass>servlet.GetWebServerTag</tagclass> <bodycontent>empty</bodycontent> </tag> </taglib>
步三:编写标签处理器 GetServerTag.java
package servlet;
import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection;
import javax.servlet.http.HttpServletRequest; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.TagSupport;
public class GetWebServerTag extends TagSupport {
@Override public int doStartTag() throws JspException { try{ // get the request object from the page context HttpServletRequest request = (HttpServletRequest)pageContext.getRequest(); // Request information form the web server URL url = new URL("http", request.getServerName(), request.getServerPort(), "/"); URLConnection con = url.openConnection(); ((HttpURLConnection)con).setRequestMethod("OPTIONS"); String webserver = con.getHeaderField("server"); // write it to the output stream JspWriter out = pageContext.getOut(); out.print(webserver); }catch (IOException e) { throw new JspException(e.getMessage()); } return SKIP_BODY; } }
步四: 编写WEB.xml文件
加入: <jsp-config> <taglib> <taglib-uri>diagnostics</taglib-uri> <taglib-location>/WEB-INF/tlds/diagnostics.tld</taglib-location> </taglib> </jsp-config>
步五:编写jsp页面,将标签并入该JSP页面
<%@ taglib prefix="diag" uri="diagnostics"%> <html> <head> <title>Basci Example of a Custom Tag</title> </head> <body> <H3>Basci Example of a Custom Tag</H3> The web server is <diag:getWebServer/> </body> </html>
OK , so much so this ,
wish you success
<注: 注意红色的地方要一致哦>
想要在 JavaServer Pages (JSP) 应用程序中添加自定义标签吗?本教程将为您展示如何用这些标签编写类似于 JSP 技术自带操作 —— 如 jsp:useBean 、jsp:getProperty 和 jsp:forward —— 的自定义操作。介绍如何用特定于自已的域的表示逻辑的自定义操作来扩展 JSP 语法。
在 JSP 应用程序中添加 自定义标签 的能力可以使您将工作重点放到以文档为中心的开发方式上。可以使 Java 代码不出现在 JSP 页中,从而使这些页面更容易维护。(我从经验中学到,在 JSP 页中放入过多的 Java 代码时,代码维护就会成为可怕的任务)。本教程将使您可以立即开发出自定义标签。了解了 JSP 自定义标签开发的好处后,您可能会对程序员没有更多地使用它而感到意外。
在本教程中,我将讨论使用自定义标签的基本内容。将介绍如何用自定义标签创建可重用的表示组件并避免在 JSP 页加入 Java scriptlet。
在本教程中,我们将:
- 定义一个 JSP 自定义标签体系结构。
- 解释简单标签。
- 定义嵌套标签。
- 用
BodyContent 解释标签。
- 在标签中添加属性。
- 在标签中添加 scriptlet 变量。
- 用自定义标签实现控制流程。
- 用 Struts 简化标签部署。
如果发现自己在 JSP 应用程序中加入了大量 Java scriptlet,那么本教程就是为您准备的。 阅读本教程后,就会掌握将 Java 代码从 JSP 页面中清除出去所需要的信息。
本教程假定读者熟悉 Java 平台、JavaServer Pages (JSP) 技术、MVC 模式、Reflection API、Model 2,最好还有 Struts 框架。此外,要从本教程中得到最大的收获,还需要很好的使用标签库的经验
Rick Hightower 是一位 J2EE 开发人员和顾问,他热衷于使用 J2EE、Ant、Hibernate、Struts、IMB 的 ETTK 和 Xdoclet。 Rick 是 Trivera Technologies 的前任 CTO,这是一家全球培训、指导和咨询公司,其重点是企业开发。他经常在 IBM developerWorks 上发表文章,并编写了 10 多篇 developerWorks 教程,内容从 EJB 技术到 Web 服务到 XDoclet。 Rick 不久前与别人共同开办了另一家名为 ArcMind 的公司,它专门研究 agile 方法,还从事 Struts/JavaServer Faces 开发、咨询和指导。
在为 eBlox 工作时,Rick 和 eBlox 小组远在 1.0 版本之前就已使用 Struts 为电子商务站点构建了两个框架和一个 ASP (应用程序服务提供者)。这个框架目前正在为 2000 多个在线商店店面提供支持。
Rick 最近完成了一本名为 Professional Jakarta Struts 的书。在周游全国对 J2EE 和 Struts 项目提供咨询,或者在大会上发表关于 J2EE 和极端编程 (extreme programing)的讲演之余,Rick 喜欢在通宵咖啡店喝咖啡,写一些有关 Struts、J2EE 和其他内容的文章,并以第三人称描写他自己。
在创建自定义标签之前,需要创建一个 标签处理程序。标签处理程序是一个执行自定义标签操作的 Java 对象。在使用自定义标签时,要导入一个 标签库 —— 即一组标签/标签处理程序对。通过在 Web 部署描述符中声明库导入它,然后用指令 taglib 将它导入 JSP 页。
如果 JSP 容器在转换时遇到了自定义标签,那么它就检查 标签库描述符(tag library descriptor) (TLD) 文件以查询相应的标签处理程序。TLD 文件对于自定义标签处理程序,就像 Web 部署描述符对于 servlet 一样。
在运行时,JSP 页生成的 servlet 得到对应于这一页面所使用的标签的标签处理程序的一个实例。生成的 servlet 用传递给它的属性初始化标签处理程序。
标签处理程序实现了 生存周期 方法。生成的 servlet 用这些方法通知标签处理程序应当启动、停止或者重复自定义标签操作。生成的 servlet 调用这些生存周期方法执行标签的功能。
可以定义两种类型的标签:
javax.servlet.jsp.tagext.Tag
javax.servlet.jsp.tagext.BodyTag
对 正文 进行操作 —— 即对在开始和结束标签之间的内容进行操作的 —— 标签必须实现 BodyTag 接口。在这个教程中,我们将称这些标签为 正文标签。我们将不对其正文操作的标签称为 简单标签。简单标签可以实现 Tag 接口,尽管不要求它们这样做。要记住不对其正文操作的标签仍然 有 正文,只不过,它的标签处理程序不能读取这个正文。
上一页 下一页
Struts 框架带有几个自定义标签库(有关 Struts 的更多信息的链接请参阅 参考资料 )。这些库中的一个标签可以创建一个支持改写 URL 的链接并用 jsessionid 对改写的连接编码。
不过有一个问题:如果希望传递一组请求参数(如查询字符串),也许必须为此创建一个 Java scriptlet。真是乱!下面的清单 (search_results.jap) 展示了一个 JSP 页,它被迫加入了这样一个 scriptlet。
<%@ taglib uri="struts-html" prefix="html" %> <jsp:useBean class="java.util.HashMap" id="deleteParams" /> <% deleteParams.put("id", cd.getId()); deleteParams.put("method","delete"); %> <!-- Pass the map named deleteParams to html:link to generate the request parameters--> <html:link action="/deleteCD" name="deleteParams">delete </html:link> </font></td>
search_results.jsp 创建一个 hashmap 并向这个 map 传递两个属性。在下面几小节,我们将创建一个不用 Java 代码完成这项工作的自定义标签。我们的标签将定义如下的一个 hashmap:
<map:mapDefine id="deleteParams"> <map:mapEntry id="id" name="cd" property="id"/> <map:mapEntry id="method" value="delete"/> </map:mapDefine> <!-- Pass the map named deleteParams to html:link to generate the request parameters--> <html:link action="/deleteCD" name="deleteParams">delete </html:link> </font></td>
这将使我们可以容易地创建小型 map。
这个例子将展示几个关键概念,包括使用嵌套标签和定义 scriplet 变量。首先我将解释这个标签是如何工作的。然后在以后的几节中建立这些概念,并介绍如何编写这个标签的不同形式,使它们处理其正文并控制执行流程。
让我们创建一个定义一个 HashMap scriptlet 变量的标签。为此,需要实现标签处理程序接口 (javax.servlet.jsp.tagext.Tag)。因此,我们要创建的第一个标签将是一个简单标签。
这个标签将实例化一个 map。使用这个标签的开发人员可以指定要实例化的 map 的类型 —— HashMap 、TreeMap 、FastHashMap 或者 FastTreeMap 。FastHashMap 和 FastTreeMap 来自 Jakarta Commons Collection library (有关链接请参阅 参考资料)。开发人员还可以指定标签所在的范围 —— 页、请求、会话还是应用程序范围。
要构建这个简单标签,我们需要完成以下步骤:
- 创建实现了
Tag 接口(准确地说是 javax.servlet.jsp.tagext.Tag)的标签处理程序类。
- 创建一个 TLD 文件。
- 在标签处理程序 Java 类中创建属性。
- 在 TLD 文件中定义与标签处理程序 Java 类中定义的属性对应的属性。
- 在 TLD 文件中声明 scriptlet 变量。
- 实现
doStartTag() 方法。在标签处理程序类中,根据属性将值设置到 scriptlet 变量中。
如果您像我一样,可能会提前阅读书的结尾,所以请查看 附录 中标签处理程序类的完整列表以了解这个过程是如何结束的。
在下面几小节中,我们将分析 MapDefineTag 的实现,并分析如何到达这一步。
第 1 步:创建一个实现了 Tag 接口的标签处理程序 |
第 3 页(共9 页) |
为了编写标签处理程序,必须实现 Tag 接口。如前所述,这个接口用于不操纵其标签正文的简单标签处理程序。就像 J2EE API 文档 (有关链接请参阅 参考资料)所说的:Tag 接口定义了标签处理程序和 JSP 页实现类之间的基本协议。它定义了在标签开始和结束时调用的生存周期和方法。
标签处理程序接口有以下方法:
方法 |
作用 |
int doStartTag() throws JspException |
处理开始标签 |
int doEndTag() throws JspException |
处理结束标签 |
Tag getParent() /void setParent(Tag t) |
获得/设置标签的父标签 |
void setPageContext(PageContext pc) |
pageContext 属性的 setter 方法 |
void release() |
释放获得的所有资源 |
TagSupport
现在,不必直接实现 Tag 接口,相反,用 map 定义的(map-defining)标签将继承 TagSupport 类。这个类以有意义的默认方法实现 Tag 接口,因而使开发自定义标签更容易 (有关 TagSupport 的 API 文档的链接请参阅 参考资料)。 例如,TagSupport 类定义了 get /setParent() 和 setPageContext() ,这与所有标签处理程序几乎相同。 get /setParent() 方法允许标签嵌套。TagSupport 类还定义了一个可以被子类使用的 pageContext 实例变量 (protected PageContext pageContext ),这个变量是由 setPageContext() 方法设置的。
在默认情况下,TagSupport 实现了 doStartTag() 以使它返回 SKIP_BODY 常量,表示将不对标签正文进行判断。 此外,在默认情况下,doEndTag() 方法返回 EVAL_PAGE ,它表示 JSP 运行时引擎应当对页面的其余部分进行判断。 最后,TagSupport 实现了 release() ,它设置 pageContext 及其父元素为 null 。
TagSupport 类还实现了 IterationTag 接口和 doAfterBody() ,这样它就返回 SKIP_BODY 。 在后面讨论进行迭代的标签时我将对此加以更详细的解释(请参阅 用自定义标签控制流程)。
好了,现在让我们通过继承 TagSupport 来实现 Tag 接口:
... import javax.servlet.jsp.tagext.TagSupport; ... public class MapDefineTag extends TagSupport { ...
我们已经定义了标签处理程序,现在需要增加从处理程序到 TLD 文件中的标签的映射。我们将在下一小节中对此进行处理。然后,将完成 MapDefineTag 中剩余的代码。
第 2 步:创建一个 TLD 文件 |
第 4 页(共9 页) |
TLD 文件对自定义标签处理程序的作用就像 Web 部署描述符对 servlet 的作用。 TLD 文件列出了从标签名到标签处理程序的映射。 这个文件中的大多数数据都是在 JSP 页转换时使用的。 TLD 文件通常保存在 Web 应用程序的 WEB-INF 目录,并在 web.xml 文件中声明。它们一般用 .tld 扩展名结束。
TLD 文件有一个 导言(preamble),在这里标识 JSP 技术的版本和使用的标签库。这个导言通常看起来像这样:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"> <taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>map</short-name>
让我们更详细地分析一下这些标签:
- TLD 文件的根元素是
taglib 。taglib 描述了一个 标签库 —— 即一组标签/标签处理程序对。
- 因为我们使用的是 JSP 版本 1.2,所以在这个例子中需要
tlib-version 和 short-name 元素。
tlib-version 元素对应于标签库版本。
jsp-version 对应于标签库所依赖的 JSP 技术的版本。
short-name 元素定义了 IDE 和其他开发工具可以使用的标签库的简单名。
taglib 元素包含许多 tag 元素,标签库中每一个标签有一个 tag 元素。
因为我们刚创建了自己的类,所以我们将继续往下进行,在 TLD 文件中声明这个类,如下所示:
<taglib> ... <tag> <name>mapDefine</name> <tag-class>trivera.tags.map.MapDefineTag</tag-class> <body-content>JSP</body-content> ...
tag 元素用于将自定义标签映射到它们的自定义标签处理程序。上述清单中的 tag 元素将自定义标签 mapDefine 映射到处理程序 trivera.tags.map.MapDefineTag 。 因此,不论在 mapDefine 上运行的是什么转换引擎,都会调用 trivera.tags.map.MapDefineTag 。
已经在 TLD 中定义了标签,接下来要在标签处理程序类中定义这个标签的一些属性了。
第 3 步:在标签处理程序 Java 类中创建属性 |
第 5 页(共9 页) |
上一页 下一页
我们希望为 mapDefine 标签指定三个属性,如下所示:
属性说明 |
id |
新 scriptlet 变量的名字。 |
scope |
新 scriptlet 变量所在的范围。 |
type |
新 scriptlet 变量的类型 (HashMap 、FastHashMap 、TreeMap 或者 FastTreeMap )。 如果 type 设置为 hash ,那么就会创建一个 HashMap 。如果 type 设置为 fasthash ,那么将创建 FastHashMap 。 |
在 JSP 页中使用这个标签时,它看起来将像下面这样:
<map:mapDefine id="editParams" scope="session" type="hash"> ... </map:mapDefine>
这个标签将在会话范围内创建一个名为 editParams 的 HashMap 。
为了在标签处理程序中创建属性,需要定义相应的 JavaBean 属性。 因此,每一个属性在标签处理程序中都有对应的 setter 方法,如下所示:
public class MapDefineTag extends TagSupport { ... private String type = FASTTREE; private String id; private String scope;
public void setType(String string) { type = string; }
public void setId(String string) { id = string; } public void setScope(String string) { scope = string; }
转换引擎将用硬编码的配置数据或者运行时表达式设置这个标签的属性。 我们将在 第 4 步:在 TLD 文件中定义属性 中对此做更详细的讨论。
在 第 5 步:实现 doStartTag() 方法 中,我们将在标签处理程序的 doStartTag() 方法中使用这些属性。
第 4 步:在 TLD 文件中定义属性 |
第 6 页(共9 页) |
就 像上一小节中所做的那样,通过声明 JavaBean 属性定义自定义属性,然后在 TLD 文件中声明这些属性。 每一个 JavaBean 属性都必须与相应的自定义标签属性相匹配。 在 TLD 中定义的属性必须匹配 JavaBean 属性,不过却可以有与标签属性不匹配的 JavaBean 属性。
下面是 MapDefineTag 的属性声明:
<tag> <name>mapDefine</name> <tag-class>trivera.tags.map.MapDefineTag</tag-class> <body-content>JSP</body-content> ... <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> <description>The id attribute</description> </attribute> <attribute> <name>scope</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <description>The scope attribute</description> </attribute> <attribute> <name>type</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <description> Specifies the type of map valid values are fasttree, fasthash, hash, tree </description> </attribute> </tag>
name 元素指定属性的名字。required 元素指定属性是否是必需的(默认值是 false )。rtexprvalue 元素表明属性是硬编码了转换时的值还是允许使用运行时 scriptlet 表达式。
记住,MapDefineTag 类必须为前面描述的每一个属性定义一个 JavaBean 属性,我们在 第 3 步:在标签处理程序 Java 类中创建属性 中完成这个任务。
第 5 步:实现 doStartTag() 方法 |
第 7 页(共9 页) |
标签开始时调用 doStartTag() 方法 —— 从开发人员的角度看,这是当引擎遇到 <map:mapDefine> 时发生的。如果 doStartTag() 返回 SKIP_BODY ,那么将不处理标签正文。 如果它返回一个 EVAL_BODY_INCLUDE ,那么将处理正文。
MapDefine 类的 doStartTag() 方法完成以下工作:
- 根据
type 属性确定要创建的 map 的属性。
- 根据
scope 属性确定新的 map 对象放在什么范围内。
- 根据
id 属性确定新 map 对象要放入的范围的名字。
让我们更详细地分析这个过程。MapDefine 类检查 type 属性是设置为 FASTTREE 、HASH 、TREE 还是 FASTHASH 。然后创建相应的 map,如下所示:
/* String constants for the different types of maps we support */ public static final String FASTHASH = "FASTHASH"; public static final String FASTTREE = "FASTTREE"; public static final String HASH = "HASH"; public static final String TREE = "TREE"; /** The map we are going to create */ private Map map = null; /** The member variable that holds the type attribute */ private String type = FASTTREE; ... public int doStartTag() throws JspException { /** Based on the type attribute, determines which type of Map to create */ if (type.equalsIgnoreCase(FASTTREE)) { map = new FastTreeMap(); } else if (type.equalsIgnoreCase(HASH)) { map = new HashMap(); } else if (type.equalsIgnoreCase(TREE)) { map = new TreeMap(); } else if (type.equalsIgnoreCase(FASTHASH)) { map = new FastHashMap(); }
然后,用 id 和 scope 属性将 hashmap 以一个给定的名字设置到一个给定范围中:
private String id; private String scope; public int doStartTag() throws JspException { ... if (scope == null){ pageContext.setAttribute(id, map); }else if("page".equalsIgnoreCase(scope)){ pageContext.setAttribute(id, map); }else if("request".equalsIgnoreCase(scope)){ pageContext.getRequest().setAttribute(id, map); }else if("session".equalsIgnoreCase(scope)){ pageContext.getSession().setAttribute(id, map); }else if("application".equalsIgnoreCase(scope)){ pageContext.getServletContext().setAttribute(id, map); } return EVAL_BODY_INCLUDE; }
如果范围属性是 null ,那么 map 将放入页范围。否则,参数将放入通过 scope 属性传递的范围名所对应的范围中。
到目前为止,我们已经有一个非常简单的标签,它有三个属性:id 、scope 和 type 。 我们将用给定的名字将 map 放到一个范围中。但是,我们还有一件事没做,就是声明 scriptlet 变量。 为了做到这一点,需要向 TLD 再添加一项,这就是我们在下一小节中所要做的事。
最近因为比较闲,而且有几个师弟来上海找工作,交流后有点感受,所以发了一个贴子.那是第一次在CSDN上发贴,最近几天又想了想,所以再发一贴,可能也是最后一贴,我只谈谈我在上海做了6年软件开发所了解到的一些东西,本人能力一般,所以我说的也只针对能力一般的人,特别厉害的牛人不需要理会我说的话. 希望能对有些人有一点帮助. 对于快要毕业的大学生来上海找工作,我觉得最重要的是基础知识一定要好.我推荐看JAVA编程思想这本书,很多公司笔试的JAVA方面的题目出自这本书.另外JSP,SERVLET,JDBC一定要有所了解,这个能说明你在大学的时候实习过,做过一些小项目的.HIBERNATE,STRUTS,SPRING这些东西我不推荐你去学习,我觉得你应该学习如何处理异常,学会用JDBC访问数据库,学会如何释放资源,如何控制数据库事务,学会如何写自定义标签,这样能够显著的提高你的编程水平. 工作一年的后我觉得应该回过去看看大学的数据结构,算法,操作系统,然后应该对面象对象要有更深刻的理解,设计模式也应该慢慢开始涉及到(JAVA变编程思想这本书建议多看几次).我不建议工作一年就开始换工作,因为找工作要花时间,新工作就要适应新的框架和新的业务,这样你就会乱费很多时间的.我还是不建议你去看HIBERNATE,STRUTS,SPRING这样的东西,虽然你工作中可能要用到它.对于英语我觉得应该早做准备,强迫自己看一些英文方面的书籍,提高自己的口语水平,另外我还要强调一点,适当的参加培训是很有必要的,上海有很多的外语培训机构(主要是培训商务交流),效果很不错的,不过也很贵的,所以去前一定要计划好,不要象大学那样交了钱不去上课或者不认真听课,那样就最好先不要去. 工作两年后,如果你算法,数据结构,设计模式等等有比较深的了解的时候 我觉得HIBERNATE,STRUTS,SPRING的实现方法你应该能够很快理解. 我还是不推荐你学习这些,我推荐学习EJB.推荐书籍精通EJB,ED ROMAN写的那本.就象他说的这本书不是宣传EJB,不是写关于EJB世界如何精彩,不是为了消灭某个其他对手等等,学习这本书是希望你能构建出健壮,安全,可扩展的服务器部署.他里面很多的概念你可能在工作中碰不到,但是不表示你可以不需要理会,而应该去思考如果碰到这样的问题我应该怎样解决,然后可以去网上找资料解决它.(这本书是写EJB2.0的,但是我还是推荐,看完后你绝对不会觉得是乱费时间,我也觉得没有做过项目也很难理解这本书的) 我觉得一个程序员要到一定档次,英语是必须的,所以我觉得如果能有一个全英文的工作环境工作一段时间是很应该的.现在基本上的技术规范全部是英文写的,很多技术论文也是英文的,如果英语不行,水平很难提高.其实对于在上海工作三年的程序员基本上知道自己应该如何发展的.
再说说上海JAVA程序员的大概工资吧.一般应届毕业生3K左右,三年工作经验5K以上,5年工作经验8K以上.这个工资是指很多公司全部能够给出,而且也比较容易找到的.对于很厉害的人肯定不合适.大公司和外企在我说的基础上乘1.5倍.(其实上海找软件开发大专就可以了,只不过要牺牲一部分福利,和需要更高的能力,本科就一点问题也没有)
对于从外地来上海找工作的学生,我建议做好长期打算,毕竟找工作是需要开销的,上海的交通费用很高,而且是省不掉的.但是心态一定要摆正,首先要有决心,工作一定可以找到,另外不要浮躁,其实找工作应该是一个不段学习的过程, 当你面试一家公司,那么笔试的题目在你面试完后一定要花时间把你你不会的搞明白,笔试十家后你会发现其实题目差不多的.态度一定要谦虚,待遇可以适当降低 但是不要低于2.5K(不过一般你转正都能要求到这个数目的,只要你好好和老板谈).要相信工作可以找到也要做好长期打算(就是万一短时间找不到工作的打算,不要轻易退场,挺一挺就过去了),经济上要准备充足.而且面试不要担心什么的,上海软件公司多得很,你今天面试不过,三个月后这个公司可能又要你去面试的.他不会记得你的.在投简历的时候,简单点的两页就可以突出你在学校期间做过哪些项目.只要是软件工程师就可以投,最多乱费两页纸,不要做太好的简历,是乱费钱.网上投简历就应该全部投,最多是不要你去面试,不会少什么的.找工作能力是一方面,技巧也有的,只有自己慢慢体会了. 再说说为什么我不推荐学习HIBERNATE,STRUTS,SPRING,因为我一直就在用这个,现在突然发现自己好象无法前进了.所以现在又重新学习基础,但是问题是我年龄大了,事情多了,可以供学习的时间不多了.所以我才极力推荐在工作的前三年一定要把基本功打扎实.现在有很多程序员会用HIBERNATE,STRUTS,SPRING来写项目,可是确不会写一个JDBC连接,对JAVA的多线程一点不知道,SERVLET不会的也很多.这个是很不好的,也是现在国内很多程序员的通病.为什么很多人说程序员到30就写不动代码,学不会新东西了,你基础那么差还能学会就真的怪了. 不要把时间放到讨论EJB和SPRING哪个好哪个差上面,不过我还是倾向EJB的,很多人说EJB能实现的SPRING也能实现(好象有EJB能实现的但SPRING不能实现,但是没有完全了解不敢说),但是为什么不想想SPRING能实现的难道EJB不能实现,实现麻烦一点有什么关系呢,学习一个东西不是要看到它的优点,而更应该看到它的缺点.呵呵,不要骂我,我认为SOA将成为潮流,把骂我的时间放到了解SOA上.而且想去大的外企,那么社会招聘百分之八十会问EJB的.进去大的外企有两个方式,一个是社会招聘,但是这个面试特别严格,基础不好外语不好的基本上没有戏.(社会招聘指招聘有工作经验的,应届毕业生叫校园招聘)另外一个就是通过外包进去,这样进去要求要低,但是你必须乱费几年,等机会直接进你外包出去的公司或者跳巢到和他同一档次的外企.
很多人说待遇不重要,重要的是学到东西,这个是不对的.待遇是你价值的体现,学东西就为了更好的待遇,所以要工资的时候不要觉得不好意思,想要多少就直接说出来.只有生活有保障才能更好的学习,更好的工作.工资高,心情就好,做什么也快呀.其实应该这么说,能学到什么不重要,重要的是工资要高.
所谓程序员只能到30岁的说法是明显不对的,程序员也不是一个跳板,不要总是想去做系统分析,去搞管理,而应该扎扎实实的学习.做程序员有什么不好,从程序员开始,往中级程序员,高级程序员,职深程序员,专家程序员一直走下去.
再次强调,外语很重要,请把它放在第一位置.最近好象软件行业也开始好转起来了,对日的外包多不说,欧美的外包也多起来了,而且外包的性质也发生变化,从最初的编码和测试发展到分析,设计,编码,测试全部外包了.这样和做国内的项目区别不大,所以还是很有发展前景的.
好象还有很多想说的但是说不出来,发现在CSDN上发贴很费时间,所以这个也是我最后一次发贴,项目马上开启了,也没有时间来论坛了.希望想来上海发展的和在上海做JAVA的程序员全部能找到满意的工作,生活更开心点,毕竟做程序还是很辛苦的.以上只是个人观点,没有任何强加给任何人的意思.
面向对象的特征有哪些方面 1. 抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象。
继承:继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
封装:封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。
多态性:多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
2、String是最基本的数据类型吗? 基本数据类型包括byte、int、char、long、float、double、boolean和short。 java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类。
3、int 和 Integer 有什么区别 Java 提供两种不同的类型:引用类型和原始类型(或内置类型)。Int是java的原始数据类型,Integer是java为int提供的封装类。Java为每个原始类型提供了封装类。 原始类型封装类boolean Boolean char Character byte Byte short Short int Integer long Long float Float double Double 引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关。
4、String 和StringBuffer的区别 JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个String类提供了数值不可改变的字符串。而这个StringBuffer类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用StringBuffer。典型地,你可以使用StringBuffers来动态构造字符数据。
5、运行时异常与一般异常有何异同? 异常表示程序运行过程中可能出现的非正常状态,运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。
6、说出Servlet的生命周期,并说出Servlet和CGI的区别。 Servlet被服务器实例化后,容器运行其init方法,请求到达时运行其service方法,service方法自动派遣运行与请求对应的doXXX方法(doGet,doPost)等,当服务器决定将实例销毁的时候调用其destroy方法。 与cgi的区别在于servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁,而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于servlet。
7、说出ArrayList,Vector, LinkedList的存储性能和特性 ArrayList和Vector都是使用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢,Vector由于使用了synchronized方法(线程安全),通常性能上较ArrayList差,而LinkedList使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
8、EJB是基于哪些技术实现的?并说出SessionBean和EntityBean的区别,StatefulBean和StatelessBean的区别。 EJB包括Session Bean、Entity Bean、Message Driven Bean,基于JNDI、RMI、JAT等技术实现。 SessionBean在J2EE应用程序中被用来完成一些服务器端的业务操作,例如访问数据库、调用其他EJB组件。EntityBean被用来代表应用系统中用到的数据。 对于客户机,SessionBean是一种非持久性对象,它实现某些在服务器上运行的业务逻辑。 对于客户机,EntityBean是一种持久性对象,它代表一个存储在持久性存储器中的实体的对象视图,或是一个由现有企业应用程序实现的实体。 Session Bean 还可以再细分为 Stateful Session Bean 与 Stateless Session Bean ,这两种的 Session Bean都可以将系统逻辑放在 method之中执行,不同的是 Stateful Session Bean 可以记录呼叫者的状态,因此通常来说,一个使用者会有一个相对应的 Stateful Session Bean 的实体。Stateless Session Bean 虽然也是逻辑组件,但是他却不负责记录使用者状态,也就是说当使用者呼叫 Stateless Session Bean 的时候,EJB Container 并不会找寻特定的 Stateless Session Bean 的实体来执行这个 method。换言之,很可能数个使用者在执行某个 Stateless Session Bean 的 methods 时,会是同一个 Bean 的 Instance 在执行。从内存方面来看, Stateful Session Bean 与 Stateless Session Bean 比较, Stateful Session Bean 会消耗 J2EE Server 较多的内存,然而 Stateful Session Bean 的优势却在于他可以维持使用者的状态。
9、Collection 和 Collections的区别。 Collection是集合类的上级接口,继承与他的接口主要有Set 和List. Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。
10、&和&&的区别。 &是位运算符,表示按位与运算,&&是逻辑运算符,表示逻辑与(and)。
11、HashMap和Hashtable的区别。 HashMap是Hashtable的轻量级实现(非线程安全 的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable。 HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。 HashMap把Hashtable的contains方法去掉了,改成containsvalue和containsKey。因为contains方法容易让人引起误解。 Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。 最大的不同是,Hashtable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap 就必须为之提供外同步。 Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。
12、final, finally, finalize的区别。 final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。finally是异常处理语句结构的一部分,表示总是执行。finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。
13、sleep() 和 wait() 有什么区别? sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,将执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
14、Overload和Override的区别。Overloaded的方法是否可以改变返回值的类型? 方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被“屏蔽”了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloaded的方法是可以改变返回值的类型。
15、error和exception有什么区别? error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。 exception 表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。
16、同步和异步有何异同,在什么情况下分别使用他们?举例说明。 如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
17、abstract class和interface有什么区别?声明方法的存在而不去实现它的类被叫做抽象类(abstract class),它用于要创建一个体现某些基本行为的类,并为该类声明方法,但不能在该类中实现该类的情况。不能创建abstract 类的实例。然而可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。Abstract 类的子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类。取而代之,在子类中实现该方法。知道其行为的其它类可以在类中实现这些方法。接口(interface)是抽象类的变体。在接口中,所有方法都是抽象的。多继承性可通过实现这样的接口而获得。接口中的所有方法都是抽象的,没有一个有程序体。接口只可以定义static final成员变量。接口的实现与子类相似,除了该实现类不能从接口定义中继承行为。当类实现特殊接口时,它定义(即将程序体给予)所有这种接口的方法。然后,它可以在实现了该接口的类的任何对象上调用接口的方法。由于有抽象类,它允许使用接口名作为引用变量的类型。通常的动态联编将生效。引用可以转换到接口类型或从接口类型转换,instanceof 运算符可以用来决定某对象的类是否实现了接口。
18、heap和stack有什么区别。栈是一种线形集合,其添加和删除元素的操作应在同一段完成。栈按照后进先出的方式进行处理。堆是栈的一个组成元素
19、forward 和redirect的区别forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容是从哪儿来的,所以它的地址栏中还是原来的地址。 redirect就是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,一般来说浏览器会用刚才请求的所有参数重新请求,所以session,request参数都可以获取。
20、EJB与JAVA BEAN的区别?Java Bean 是可复用的组件,对Java Bean并没有严格的规范,理论上讲,任何一个Java类都可以是一个Bean。但通常情况下,由于Java Bean是被容器所创建(如Tomcat)的,所以Java Bean应具有一个无参的构造器,另外,通常Java Bean还要实现Serializable接口用于实现Bean的持久性。Java Bean实际上相当于微软COM模型中的本地进程内COM组件,它是不能被跨进程访问的。Enterprise Java Bean 相当于DCOM,即分布式组件。它是基于Java的远程方法调用(RMI)技术的,所以EJB可以被远程访问(跨进程、跨计算机)。但EJB必须被布署在诸如Webspere、WebLogic这样的容器中,EJB客户从不直接访问真正的EJB组件,而是通过其容器访问。EJB容器是EJB组件的代理,EJB组件由容器所创建和管理。客户通过容器来访问真正的EJB组件。
21、Static Nested Class 和 Inner Class的不同。 Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化。
22、JSP中动态INCLUDE与静态INCLUDE的区别?动态INCLUDE用jsp:include动作实现 <jsp:include page="included.jsp" flush="true" />它总是会检查所含文件中的变化,适合用于包含动态页面,并且可以带参数。静态INCLUDE用include伪码实现,定不会检查所含文件的变化,适用于包含静态页面<%@ include file="included.htm" %>
23、什么时候用assert。 assertion(断言)在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。在实现中,assertion就是在程序中的一条语句,它对一个boolean表达式进行检查,一个正确程序必须保证这个boolean表达式的值为true;如果该值为false,说明程序已经处于不正确的状态下,系统将给出警告或退出。一般来说,assertion用于保证程序最基本、关键的正确性。assertion检查通常在开发和测试时开启。为了提高性能,在软件发布后,assertion检查通常是关闭的。
24、GC是什么? 为什么要有GC? GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。
25、short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1;有什么错? short s1 = 1; s1 = s1 + 1; (s1+1运算结果是int型,需要强制转换类型) short s1 = 1; s1 += 1;(可以正确编译)
26、Math.round(11.5)等於多少? Math.round(-11.5)等於多少? Math.round(11.5)==12 Math.round(-11.5)==-11 round方法返回与参数最接近的长整数,参数加1/2后求其floor.
27、String s = new String("xyz");创建了几个String Object? 两个
28、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 以下程序使用内部类实现线程,对j增减的时候没有考虑顺序问题。 public class ThreadTest1{ private int j; public static void main(String args[]) { ThreadTest1 tt=new ThreadTest1(); Inc inc=tt.new Inc(); Dec dec=tt.new Dec(); for(int i=0;i<2;i++){ Thread t=new Thread(inc); t.start(); t=new Thread(dec); t.start(); } } private synchronized void inc(){ j++; System.out.println(Thread.currentThread().getName()+"-inc:"+j); }
private synchronized void dec(){ j--; System.out.println(Thread.currentThread().getName()+"-dec:"+j); }
class Inc implements Runnable{ public void run(){ for(int i=0;i<100;i++){ inc(); } } } class Dec implements Runnable{ public void run(){ for(int i=0;i<100;i++){ dec(); } } }
}
29、Java有没有goto? java中的保留字,现在没有在java中使用。
30、启动一个线程是用run()还是start()?启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。
31、EJB包括(SessionBean,EntityBean)说出他们的生命周期,及如何管理事务的? SessionBean:Stateless Session Bean 的生命周期是由容器决定的,当客户机发出请求要建立一个Bean的实例时,EJB容器不一定要创建一个新的Bean的实例供客户机调用,而是随便找一个现有的实例提供给客户机。当客户机第一次调用一个Stateful Session Bean 时,容器必须立即在服务器中创建一个新的Bean实例,并关联到客户机上,以后此客户机调用Stateful Session Bean 的方法时容器会把调用分派到与此客户机相关联的Bean实例。EntityBean:Entity Beans能存活相对较长的时间,并且状态是持续的。只要数据库中的数据存在,Entity beans就一直存活。而不是按照应用程序或者服务进程来说的。即使EJB容器崩溃了,Entity beans也是存活的。Entity Beans生命周期能够被容器或者 Beans自己管理。EJB通过以下技术管理实务:对象管理组织(OMG)的对象实务服务(OTS),Sun Microsystems的Transaction Service(JTS)、Java Transaction API(JTA),开发组(X/Open)的XA接口。
32、应用服务器有那些? BEA WebLogic Server,IBM WebSphere Application Server,Oracle9i Application Server,jBoss,Tomcat
33、给我一个你最常见到的runtime exception。 ArithmeticException, ArrayStoreException, BufferOverflowException, BufferUnderflowException, CannotRedoException, CannotUndoException, ClassCastException, CMMException, ConcurrentModificationException, DOMException, EmptyStackException, IllegalArgumentException, IllegalMonitorStateException, IllegalPathStateException, IllegalStateException, ImagingOpException, IndexOutOfBoundsException, MissingResourceException, NegativeArraySizeException, NoSuchElementException, NullPointerException, ProfileDataException, ProviderException, RasterFormatException, SecurityException, SystemException, UndeclaredThrowableException, UnmodifiableSetException, UnsupportedOperationException
34、接口是否可继承接口? 抽象类是否可实现(implements)接口? 抽象类是否可继承实体类(concrete class)? 接口可以继承接口。抽象类可以实现(implements)接口,抽象类是否可继承实体类,但前提是实体类必须有明确的构造函数。
35、List, Set, Map是否继承自Collection接口? List,Set是,Map不是
36、说出数据连接池的工作机制是什么? J2EE服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接。客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其表记为忙。如果当前没有空闲连接,池驱动程序就新建一定数量的连接,新建连接的数量有配置参数决定。当使用的池连接调用完成后,池驱动程序将此连接表记为空闲,其他调用就可以使用这个连接。
37、abstract的method是否可同时是static,是否可同时是native,是否可同时是synchronized? 都不能
38、数组有没有length()这个方法? String有没有length()这个方法?数组没有length()这个方法,有length的属性。String有有length()这个方法。
39、Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别? Set里的元素是不能重复的,那么用iterator()方法来区分重复与否。equals()是判读两个Set是否相等。equals()和==方法决定引用值是否指向同一对象equals()在类中被覆盖,为的是当两个分离的对象的内容和类型相配的话,返回真值。
40、构造器Constructor是否可被override?构造器Constructor不能被继承,因此不能重写Overriding,但可以被重载Overloading。
41、是否可以继承String类?String类是final类故不可以继承。
42、swtich是否能作用在byte上,是否能作用在long上,是否能作用在String上?switch(expr1)中,expr1是一个整数表达式。因此传递给 switch 和 case 语句的参数应该是 int、 short、 char 或者 byte。long,string 都不能作用于swtich。
43、try {}里有一个return语句,那么紧跟在这个try后的finally {}里的code会不会被执行,什么时候被执行,在return前还是后?会执行,在return前执行。
44、编程题: 用最有效率的方法算出2乘以8等於几? 2 << 3
45、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?不对,有相同的hash code。
46、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 是值传递。Java 编程语言只有值传递参数。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的内容可以在被调用的方法中改变,但对象的引用是永远不会改变的。
47、当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法? 不能,一个对象的一个synchronized方法只能由一个线程访问。
48、编程题: 写一个Singleton出来。 Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。一般Singleton模式通常有几种种形式:第一种形式: 定义一个类,它的构造函数为private的,它有一个static的private的该类变量,在类初始化时实例话,通过一个public的getInstance方法获取对它的引用,继而调用其中的方法。public class Singleton {private Singleton(){} private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } } 第二种形式: public class Singleton { private static Singleton instance = null; public static synchronized Singleton getInstance() { if (instance==null) instance=new Singleton(); return instance; } } 其他形式: 定义一个类,它的构造函数为private的,所有方法为static的。一般认为第一种形式要更加安全些
49、Java的接口和C++的虚类的相同和不同处。 由于Java不支持多继承,而有可能某个类或对象要使用分别在几个类或对象里面的方法或属性,现有的单继承机制就不能满足要求。与继承相比,接口有更高的灵活性,因为接口中没有任何实现代码。当一个类实现了接口以后,该类要实现接口里面所有的方法和属性,并且接口里面的属性在默认状态下面都是public static,所有方法默认情况下是public.一个类可以实现多个接口。
50、Java中的异常处理机制的简单原理和应用。 当JAVA程序违反了JAVA的语义规则时,JAVA虚拟机就会将发生的错误表示为一个异常。违反语义规则包括2种情况。一种是JAVA类库内置的语义检查。例如数组下标越界,会引发IndexOutOfBoundsException;访问null的对象时会引发NullPointerException。另一种情况就是JAVA允许程序员扩展这种语义检查,程序员可以创建自己的异常,并自由选择在何时用throw关键字引发异常。所有的异常都是java.lang.Thowable的子类。
51、垃圾回收的优点和原理。并考虑2种回收机制。 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低级别的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收
52、请说出你所知道的线程同步的方法。 wait():使一个线程处于等待状态,并且释放所持有的对象的lock。sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
53、你所知道的集合类都有哪些?主要方法?最常用的集合类是 List 和 Map。 List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象的元素列表。 List 适用于按数值索引访问元素的情形。 Map 提供了一个更通用的元素存储方法。 Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。
54、描述一下JVM加载class文件的原理机制?JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader 是一个重要的Java运行时系统组件。它负责在运行时查找和装入类文件的类。
55、char型变量中能不能存贮一个中文汉字?为什么? 能够定义成为一个中文的,因为java中以unicode编码,一个char占16个字节,所以放一个中文是没问题的
56、多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么? 多线程有两种实现方法,分别是继承Thread类与实现Runnable接口 ,同步的实现方面有两种,分别是synchronized,wait与notify
57、JSP的内置对象及方法。 request表示HttpServletRequest对象。它包含了有关浏览器请求的信息,并且提供了几个用于获取cookie, header, 和session数据的有用的方法,response表示HttpServletResponse对象,并提供了几个用于设置送回 浏览器的响应的方法(如cookies,头信息等) out对象是javax.jsp.JspWriter的一个实例,并提供了几个方法使你能用于向浏览器回送输出结果。 pageContext表示一个javax.servlet.jsp.PageContext对象。它是用于方便存取各种范围的名字空间、servlet相关的对象的API,并且包装了通用的servlet相关功能的方法。 session表示一个请求的javax.servlet.http.HttpSession对象。Session可以存贮用户的状态信息 applicaton 表示一个javax.servle.ServletContext对象。这有助于查找有关servlet引擎和servlet环境的信息 config表示一个javax.servlet.ServletConfig对象。该对象用于存取servlet实例的初始化参数。 page表示从该页面产生的一个servlet实例
58、线程的基本概念、线程的基本状态以及状态之间的关系线程指在程序执行过程中,能够执行程序代码的一个执行单位,每个程序至少都有一个线程,也就是程序本身。Java中的线程有四种状态分别是:运行、就绪、挂起、结束。
59、JSP的常用指令<%@page language=”java” contenType=”text/html;charset=gb2312” session=”true” buffer=”64kb” autoFlush=”true” isThreadSafe=”true” info=”text” errorPage=”error.jsp” isErrorPage=”true” isELIgnored=”true” pageEncoding=”gb2312” import=”java.sql.*”%>isErrorPage(是否能使用Exception对象),isELIgnored(是否忽略表达式) <%@include file=”filename”%><%@taglib prefix=”c”uri=”http://……”%>
60、什么情况下调用doGet()和doPost()?Jsp页面中的form标签里的method属性为get时调用doGet(),为post时调用doPost()。
61、servlet的生命周期web容器加载servlet,生命周期开始。通过调用servlet的init()方法进行servlet的初始化。通过调用service()方法实现,根据请求的不同调用不同的do***()方法。结束服务,web容器调用servlet的destroy()方法。
62、如何现实servlet的单线程模式 <%@ page isThreadSafe=”false”%>
63、页面间对象传递的方法 request,session,application,cookie等
64、JSP和Servlet有哪些相同点和不同点,他们之间的联系是什么? JSP是Servlet技术的扩展,本质上是Servlet的简易方式,更强调应用的外表表达。JSP编译后是"类servlet"。Servlet和JSP最主要的不同点在于,Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML里分离开来。而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。JSP侧重于视图,Servlet主要用于控制逻辑。
65、四种会话跟踪技术 cookie,url重写,session,隐藏域
65,jsp的四种范围 page否是代表与一个页面相关的对象和属性。一个页面由一个编译好的 Java servlet 类(可以带有任何的 include 指令,但是没有 include 动作)表示。这既包括 servlet 又包括被编译成 servlet 的 JSP 页面 request是是代表与 Web 客户机发出的一个请求相关的对象和属性。一个请求可能跨越多个页面,涉及多个 Web 组件(由于 forward 指令和 include 动作的关系) session是是代表与用于某个 Web 客户机的一个用户体验相关的对象和属性。一个 Web 会话可以也经常会跨越多个客户机请求 application是是代表与整个 Web 应用程序相关的对象和属性。这实质上是跨越整个 Web 应用程序,包括多个页面、请求和会话的一个全局作用域
66、Request对象的主要方法: setAttribute(String name,Object):设置名字为name的request的参数值 getAttribute(String name):返回由name指定的属性值 getAttributeNames():返回request对象所有属性的名字集合,结果是一个枚举的实例 getCookies():返回客户端的所有Cookie对象,结果是一个Cookie数组 getCharacterEncoding():返回请求中的字符编码方式 getContentLength():返回请求的Body的长度 getHeader(String name):获得HTTP协议定义的文件头信息 getHeaders(String name):返回指定名字的request Header的所有值,结果是一个枚举的实例 getHeaderNames():返回所以request Header的名字,结果是一个枚举的实例 getInputStream():返回请求的输入流,用于获得请求中的数据 getMethod():获得客户端向服务器端传送数据的方法 getParameter(String name):获得客户端传送给服务器端的有name指定的参数值 getParameterNames():获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 getParameterValues(String name):获得有name指定的参数的所有值 getProtocol():获取客户端向服务器端传送数据所依据的协议名称 getQueryString():获得查询字符串 getRequestURI():获取发出请求字符串的客户端地址 getRemoteAddr():获取客户端的IP地址 getRemoteHost():获取客户端的名字 getSession([Boolean create]):返回和请求相关Session getServerName():获取服务器的名字 getServletPath():获取客户端所请求的脚本文件的路径 getServerPort():获取服务器的端口号 removeAttribute(String name):删除请求中的一个属性
67、J2EE是技术还是平台还是框架?J2EE本身是一个标准,一个为企业分布式应用的开发提供的标准平台。 J2EE也是一个框架,包括JDBC、JNDI、RMI、JMS、EJB、JTA等技术。
68、我们在web应用开发过程中经常遇到输出某种编码的字符,如iso8859-1等,如何输出一个某种编码的字符串? Public String translate (String str) { String tempStr = ""; try { tempStr = new String(str.getBytes("ISO-8859-1"), "GBK"); tempStr = tempStr.trim(); } catch (Exception e) { System.err.println(e.getMessage()); } return tempStr; }
69、简述逻辑操作(&,|,^)与条件操作(&&,||)的区别。区别主要答两点:a.条件操作只能操作布尔型的,而逻辑操作不仅可以操作布尔型,而且可以操作数值型b.逻辑操作不会产生短路
70、XML文档定义有几种形式?它们之间有何本质区别?解析XML文档有哪几种方式? a: 两种形式 dtd schema,b: 本质区别:schema本身是xml的,可以被XML解析器解析(这也是从DTD上发展schema的根本目的),c:有DOM,SAX,STAX等 DOM:处理大型文件时其性能下降的非常厉害。这个问题是由DOM的树结构所造成的,这种结构占用的内存较多,而且DOM必须在解析文件之前把整个文档装入内存,适合对XML的随机访问 SAX:不现于DOM,SAX是事件驱动型的XML解析方式。它顺序读取XML文件,不需要一次全部装载整个文件。当遇到像文件开头,文档结束,或者标签开头与标签结束时,它会触发一个事件,用户通过在其回调事件中写入处理代码来处理XML文件,适合对XML的顺序访问 STAX:Streaming API for XML (StAX)
71、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 主要相同点:Lock能完成synchronized所实现的所有功能主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。
72、EJB的角色和三个对象 一个完整的基于EJB的分布式计算结构由六个角色组成,这六个角色可以由不同的开发商提供,每个角色所作的工作必须遵循Sun公司提供的EJB规范,以保证彼此之间的兼容性。这六个角色分别是EJB组件开发者(Enterprise Bean Provider) 、应用组合者(Application Assembler)、部署者(Deployer)、EJB 服务器提供者(EJB Server Provider)、EJB 容器提供者(EJB Container Provider)、系统管理员(System Administrator)三个对象是Remote(Local)接口、Home(LocalHome)接口,Bean类
73、EJB容器提供的服务主要提供声明周期管理、代码产生、持续性管理、安全、事务管理、锁和并发行管理等服务。
74、EJB规范规定EJB中禁止的操作有哪些? 1.不能操作线程和线程API(线程API指非线程对象的方法如notify,wait等),2.不能操作awt,3.不能实现服务器功能,4.不能对静态属生存取,5.不能使用IO操作直接存取文件系统,6.不能加载本地库.,7.不能将this作为变量和返回,8.不能循环调用。
75、remote接口和home接口主要作用remote接口定义了业务方法,用于EJB客户端调用业务方法。home接口是EJB工厂用于创建和移除查找EJB实例
76、bean 实例的生命周期对于Stateless Session Bean、Entity Bean、Message Driven Bean一般存在缓冲池管理,而对于Entity Bean和Statefull Session Bean存在Cache管理,通常包含创建实例,设置上下文、创建EJB Object(create)、业务方法调用、remove等过程,对于存在缓冲池管理的Bean,在create之后实例并不从内存清除,而是采用缓冲池调度机制不断重用实例,而对于存在Cache管理的Bean则通过激活和去激活机制保持Bean的状态并限制内存中实例数量。
77、EJB的激活机制 以Stateful Session Bean 为例:其Cache大小决定了内存中可以同时存在的Bean实例的数量,根据MRU或NRU算法,实例在激活和去激活状态之间迁移,激活机制是当客户端调用某个EJB实例业务方法时,如果对应EJB Object发现自己没有绑定对应的Bean实例则从其去激活Bean存储中(通过序列化机制存储实例)回复(激活)此实例。状态变迁前会调用对应的ejbActive和ejbPassivate方法。
78、EJB的几种类型会话(Session)Bean ,实体(Entity)Bean 消息驱动的(Message Driven)Bean ;会话Bean又可分为有状态(Stateful)和无状态(Stateless)两种;实体Bean可分为Bean管理的持续性(BMP)和容器管理的持续性(CMP)两种
79、客服端调用EJB对象的几个基本步骤设置JNDI服务工厂以及JNDI服务地址系统属性,查找Home接口,从Home接口调用Create方法创建Remote接口,通过Remote接口调用其业务方法。
80、如何给weblogic指定大小的内存? 在启动Weblogic的脚本中(位于所在Domian对应服务器目录下的startServerName),增加set MEM_ARGS=-Xms32m -Xmx200m,可以调整最小内存为32M,最大200M
81、如何设定的weblogic的热启动模式(开发模式)与产品发布模式?可以在管理控制台中修改对应服务器的启动模式为开发或产品模式之一。或者修改服务的启动文件或者commenv文件,增加set PRODUCTION_MODE=true。
82、如何启动时不需输入用户名与密码?修改服务启动文件,增加 WLS_USER和WLS_PW项。也可以在boot.properties文件中增加加密过的用户名和密码.
83、在weblogic管理制台中对一个应用域(或者说是一个网站,Domain)进行jms及ejb或连接池等相关信息进行配置后,实际保存在什么文件中?保存在此Domain的config.xml文件中,它是服务器的核心配置文件。
84、说说weblogic中一个Domain的缺省目录结构?比如要将一个简单的helloWorld.jsp放入何目录下,然的在浏览器上就可打入http://主机:端口号//helloword.jsp就可以看到运行结果了? 又比如这其中用到了一个自己写的javaBean该如何办? Domain目录\服务器目录\applications,将应用目录放在此目录下将可以作为应用访问,如果是Web应用,应用目录需要满足Web应用目录要求,jsp文件可以直接放在应用目录中,Javabean需要放在应用目录的WEB-INF目录的classes目录中,设置服务器的缺省应用将可以实现在浏览器上无需输入应用名。
85、在weblogic中发布ejb需涉及到哪些配置文件不同类型的EJB涉及的配置文件不同,都涉及到的配置文件包括ejb-jar.xml,weblogic-ejb-jar.xmlCMP实体Bean一般还需要weblogic-cmp-rdbms-jar.xml
86、如何在weblogic中进行ssl配置与客户端的认证配置或说说j2ee(标准)进行ssl的配置缺省安装中使用DemoIdentity.jks和DemoTrust.jks KeyStore实现SSL,需要配置服务器使用Enable SSL,配置其端口,在产品模式下需要从CA获取私有密钥和数字证书,创建identity和trust keystore,装载获得的密钥和数字证书。可以配置此SSL连接是单向还是双向的。
87、如何查看在weblogic中已经发布的EJB?可以使用管理控制台,在它的Deployment中可以查看所有已发布的EJB
88、CORBA是什么?用途是什么? CORBA 标准是公共对象请求代理结构(Common Object Request Broker Architecture),由对象管理组织 (Object Management Group,缩写为 OMG)标准化。它的组成是接口定义语言(IDL), 语言绑定(binding:也译为联编)和允许应用程序间互操作的协议。 其目的为:用不同的程序设计语言书写在不同的进程中运行,为不同的操作系统开发。
89、说说你所熟悉或听说过的j2ee中的几种常用模式?及对设计模式的一些看法 Session Facade Pattern:使用SessionBean访问EntityBean;Message Facade Pattern:实现异步调用;EJB Command Pattern:使用Command JavaBeans取代SessionBean,实现轻量级访问;Data Transfer Object Factory:通过DTO Factory简化EntityBean数据提供特性;Generic Attribute Access:通过AttibuteAccess接口简化EntityBean数据提供特性;Business Interface:通过远程(本地)接口和Bean类实现相同接口规范业务逻辑一致性;EJB架构的设计好坏将直接影响系统的性能、可扩展性、可维护性、组件可重用性及开发效率。项目越复杂,项目队伍越庞大则越能体现良好设计的重要性。
90、说说在weblogic中开发消息Bean时的persistent与non-persisten的差别persistent方式的MDB可以保证消息传递的可靠性,也就是如果EJB容器出现问题而JMS服务器依然会将消息在此MDB可用的时候发送过来,而non-persistent方式的消息将被丢弃。
91、Servlet执行时一般实现哪几个方法?public void init(ServletConfig config);public ServletConfig getServletConfig();public String getServletInfo();public void service(ServletRequest request,ServletResponse response);public void destroy()
92、常用的设计模式?说明工厂模式。 Java中的23种设计模式:Factory(工厂模式),Builder(建造模式), Factory Method(工厂方法模式),Prototype(原始模型模式),Singleton(单例模式), Facade(门面模式),Adapter(适配器模式), Bridge(桥梁模式), Composite(合成模式),Decorator(装饰模式), Flyweight(享元模式), Proxy(代理模式),Command(命令模式), Interpreter(解释器模式), Visitor(访问者模式),Iterator(迭代子模式), Mediator(调停者模式), Memento(备忘录模式),Observer(观察者模式),State(状态模式),Strategy(策略模式),Template Method(模板方法模式), Chain Of Responsibleity(责任链模式)。工厂模式:工厂模式是一种经常被使用到的模式,根据工厂模式实现的类可以根据提供的数据生成一组类中某一个类的实例,通常这一组类有一个公共的抽象父类并且实现了相同的方法,但是这些方法针对不同的数据进行了不同的操作。首先需要定义一个基类,该类的子类通过不同的方法实现了基类中的方法。然后需要定义一个工厂类,工厂类可以根据条件生成不同的子类实例。当得到子类的实例后,开发人员可以调用基类中的方法而不必考虑到底返回的是哪一个子类的实例。
93、EJB需直接实现它的业务接口或Home接口吗,请简述理由。远程接口和Home接口不需要直接实现,他们的实现代码是由服务器产生的,程序运行中对应实现类会作为对应接口类型的实例被使用。
94、排序都有哪几种方法?请列举。用JAVA实现一个快速排序。排序的方法有:插入排序(直接插入排序、希尔排序),交换排序(冒泡排序、快速排序),选择排序(直接选择排序、堆排序),归并排序,分配排序(箱排序、基数排序) 快速排序的伪代码。/ /使用快速排序方法对a[ 0 :n- 1 ]排序,从a[ 0 :n- 1 ]中选择一个元素作为m i d d l e,该元素为支点, 把余下的元素分割为两段left 和r i g h t,使得l e f t中的元素都小于等于支点,而right 中的元素都大于等于支点,递归地使用快速排序方法对left 进行排序,递归地使用快速排序方法对right 进行排序,所得结果为l e f t + m i d d l e + r i g h t。
95、请对以下在J2EE中常用的名词进行解释(或简单描述)web容器:给处于其中的应用程序组件(JSP,SERVLET)提供一个环境,使JSP,SERVLET直接更容器中的环境变量接口交互,不必关注其它系统问题。主要有WEB服务器来实现。例如:TOMCAT,WEBLOGIC,WEBSPHERE等。该容器提供的接口严格遵守J2EE规范中的WEB APPLICATION 标准。我们把遵守以上标准的WEB服务器就叫做J2EE中的WEB容器。EJB容器:Enterprise java bean 容器。更具有行业领域特色。他提供给运行在其中的组件EJB各种管理功能。只要满足J2EE规范的EJB放入该容器,马上就会被容器进行高效率的管理。并且可以通过现成的接口来获得系统级别的服务。例如邮件服务、事务管理。JNDI:(Java Naming & Directory Interface)JAVA命名目录服务。主要提供的功能是:提供一个目录系统,让其它各地的应用程序在其上面留下自己的索引,从而满足快速查找和定位分布式应用程序的功能。JMS:(Java Message Service)JAVA消息服务。主要实现各个应用程序之间的通讯。包括点对点和广播。JTA:(Java Transaction API)JAVA事务服务。提供各种分布式事务服务。应用程序只需调用其提供的接口即可。JAF:(Java Action FrameWork)JAVA安全认证框架。提供一些安全控制方面的框架。让开发者通过各种部署和自定义实现自己的个性安全控制策略。RMI/IIOP:(Remote Method Invocation /internet对象请求中介协议)他们主要用于通过远程调用服务。例如,远程有一台计算机上运行一个程序,它提供股票分析服务,我们可以在本地计算机上实现对其直接调用。当然这是要通过一定的规范才能在异构的系统之间进行通信。RMI是JAVA特有的。
96、JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗? Java通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。在Java中,每个异常都是一个对象,它是Throwable类或其它子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throws)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理。用try来指定一块预防所有“异常”的程序。紧跟在try程序后面,应包含一个catch子句来指定你想要捕捉的“异常”的类型。throw语句用来明确地抛出一个“异常”。throws用来标明一个成员函数可能抛出的各种“异常”。Finally为确保一段代码不管发生什么“异常”都被执行一段代码。可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try语句,“异常”的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种“异常”进行处理,堆栈就会展开,直到遇到有处理这种“异常”的try语句。
97、一个“.java”源文件中是否可以包括多个类(不是内部类)?有什么限制?可以。必须只有一个类名与文件名相同。
98、MVC的各个部分都有那些技术来实现?如何实现? MVC是Model-View-Controller的简写。"Model" 代表的是应用的业务逻辑(通过JavaBean,EJB组件实现), "View" 是应用的表示面(由JSP页面产生),"Controller" 是提供应用的处理过程控制(一般是一个Servlet),通过这种设计模型把应用逻辑,处理过程和显示逻辑分成不同的组件实现。这些组件可以进行交互和重用。
99、java中有几种方法可以实现一个线程?用什么关键字修饰同步方法? stop()和suspend()方法为何不推荐使用?有两种实现方法,分别是继承Thread类与实现Runnable接口用synchronized关键字修饰同步方法反对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。
100、java中有几种类型的流?JDK为每种类型的流提供了一些抽象类以供继承,请说出他们分别是哪些类? 字节流,字符流。字节流继承于InputStream \ OutputStream,字符流继承于InputStreamReader \ OutputStreamWriter。在java.io包中还有许多其他的流,主要是为了提高性能和使用方便。
101、java中会存在内存泄漏吗,请简单描述。会。如:int i,i2; return (i-i2); //when i为足够大的正数,i2为足够大的负数。结果会造成溢位,导致错误。
102、java中实现多态的机制是什么?方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。
103、垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
104、静态变量和实例变量的区别?static i = 10; //常量; class A a; a.i =10;//可变
105、什么是java序列化,如何实现java序列化? 序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。序列化的实现:将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。
106、是否可以从一个static方法内部发出对非static方法的调用?不可以,如果其中包含对象的method();不能保证对象初始化.
107、写clone()方法时,通常都有一行代码,是什么?Clone 有缺省行为,super.clone();他负责产生正确大小的空间,并逐位复制。
108、在JAVA中,如何跳出当前的多重嵌套循环?用break; return 方法。
109、List、Map、Set三个接口,存取元素时,各有什么特点?List 以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。
110、J2EE是什么?J2EE是Sun公司提出的多层(multi-diered),分布式(distributed),基于组件(component-base)的企业级应用模型(enterpriese application model).在这样的一个应用系统中,可按照功能划分为不同的组件,这些组件又可在不同计算机上,并且处于相应的层次(tier)中。所属层次包括客户层(clietn tier)组件,web层和组件,Business层和组件,企业信息系统(EIS)层。
111、UML方面 标准建模语言UML。用例图,静态图(包括类图、对象图和包图),行为图,交互图(顺序图,合作图),实现图。
112、说出一些常用的类,包,接口,请各举5个常用的类:BufferedReader BufferedWriter FileReader FileWirter String Integer;常用的包:java.lang java.awt java.io java.util java.sql;常用的接口:Remote List Map Document NodeList
113、开发中都用到了那些设计模式?用在什么场合? 每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心。通过这种方式,你可以无数次地使用那些已有的解决方案,无需在重复相同的工作。主要用到了MVC的设计模式。用来开发JSP/Servlet或者J2EE的相关应用。简单工厂模式等。
114、jsp有哪些动作?作用分别是什么? JSP共有以下6种基本动作 jsp:include:在页面被请求的时候引入一个文件。 jsp:useBean:寻找或者实例化一个JavaBean。 jsp:setProperty:设置JavaBean的属性。 jsp:getProperty:输出某个JavaBean的属性。 jsp:forward:把请求转到一个新的页面。 jsp:plugin:根据浏览器类型为Java插件生成OBJECT或EMBED标记。
115、Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以implements(实现)interface(接口)? 可以继承其他类或完成其他接口,在swing编程中常用此方式。
116、应用服务器与WEB SERVER的区别?应用服务器:Weblogic、Tomcat、Jboss; WEB SERVER:IIS、 Apache
117、BS与CS的联系与区别。C/S是Client/Server的缩写。服务器通常采用高性能的PC、工作站或小型机,并采用大型数据库系统,如Oracle、Sybase、Informix或 SQL Server。客户端需要安装专用的客户端软件。B/S是Brower/Server的缩写,客户机上只要安装一个浏览器(Browser),如Netscape Navigator或Internet Explorer,服务器安装Oracle、Sybase、Informix或 SQL Server等数据库。在这种结构下,用户界面完全通过WWW浏览器实现,一部分事务逻辑在前端实现,但是主要事务逻辑在服务器端实现。浏览器通过Web Server 同数据库进行数据交互。C/S 与 B/S 区别: 1.硬件环境不同: C/S 一般建立在专用的网络上, 小范围里的网络环境, 局域网之间再通过专门服务器提供连接和数据交换服务; B/S 建立在广域网之上的, 不必是专门的网络硬件环境,例与电话上网, 租用设备. 信息自己管理. 有比C/S更强的适应范围, 一般只要有操作系统和浏览器就行 2.对安全要求不同 :C/S 一般面向相对固定的用户群, 对信息安全的控制能力很强. 一般高度机密的信息系统采用C/S 结构适宜. 可以通过B/S发布部分可公开信息.B/S 建立在广域网之上, 对安全的控制能力相对弱, 可能面向不可知的用户。3.对程序架构不同 : C/S 程序可以更加注重流程, 可以对权限多层次校验, 对系统运行速度可以较少考虑. B/S 对安全以及访问速度的多重的考虑, 建立在需要更加优化的基础之上. 比C/S有更高的要求 B/S结构的程序架构是发展的趋势, 从MS的.Net系列的BizTalk 2000 Exchange 2000等, 全面支持网络的构件搭建的系统. SUN 和IBM推的JavaBean 构件技术等,使 B/S更加成熟. 4.软件重用不同: C/S 程序可以不可避免的整体性考虑, 构件的重用性不如在B/S要求下的构件的重用性好. B/S 对的多重结构,要求构件相对独立的功能. 能够相对较好的重用.就入买来的餐桌可以再利用,而不是做在墙上的石头桌子 。5.系统维护不同 :C/S 程序由于整体性, 必须整体考察, 处理出现的问题以及系统升级. 升级难. 可能是再做一个全新的系统, B/S 构件组成,方面构件个别的更换,实现系统的无缝升级. 系统维护开销减到最小.用户从网上自己下载安装就可以实现升级. 6.处理问题不同 :C/S 程序可以处理用户面固定, 并且在相同区域, 安全要求高需求, 与操作系统相关. 应该都是相同的系统,B/S 建立在广域网上, 面向不同的用户群, 分散地域, 这是C/S无法作到的. 与操作系统平台关系最小. 7.用户接口不同: C/S 多是建立的Window平台上,表现方法有限,对程序员普遍要求较高,B/S 建立在浏览器上, 有更加丰富和生动的表现方式与用户交流. 并且大部分难度减低,减低开发成本. 8.信息流不同 : C/S 程序一般是典型的中央集权的机械式处理, 交互性相对低,B/S 信息流向可变化, B-B B-C B-G等信息、流向的变化, 更像交易中心。
一、什么是Java虚拟机 当你谈到Java虚拟机时,你可能是指: 1、抽象的Java虚拟机规范 2、一个具体的Java虚拟机实现 3、一个运行的Java虚拟机实例 二、Java虚拟机的生命周期 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。 Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包含main()方法的类名。 Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可以把自己的程序设置为守护线程。包含Main()方法的初始线程不是守护线程。 只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。 三、Java虚拟机的体系结构 在Java虚拟机的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了Java虚拟机的内部结构,他们不仅仅为Java虚拟机的实现提供了清晰的内部结构,更是严格规定了Java虚拟机实现的外部行为。 每一个Java虚拟机都由一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。 程序的执行需要一定的内存空间,如字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等。Java虚拟机将这些信息统统保存在数据区(data areas)中。虽然每个Java虚拟机的实现中都包含数据区,但是Java虚拟机规范对数据区的规定却非常的抽象。许多结构上的细节部分都留给了 Java虚拟机实现者自己发挥。不同Java虚拟机实现上的内存结构千差万别。一部分实现可能占用很多内存,而其他以下可能只占用很少的内存;一些实现可能会使用虚拟内存,而其他的则不使用。这种比较精炼的Java虚拟机内存规约,可以使得Java虚拟机可以在广泛的平台上被实现。 数据区中的一部分是整个程序共有,其他部分被单独的线程控制。每一个Java虚拟机都包含方法区(method area)和堆(heap),他们都被整个程序共享。Java虚拟机加载并解析一个类以后,将从类文件中解析出来的信息保存与方法区中。程序执行时创建的 对象都保存在堆中。 当一个线程被创建时,会被分配只属于他自己的PC寄存器“pc register”(程序计数器)和Java堆栈(Java stack)。当线程不掉用本地方法时,PC寄存器中保存线程执行的下一条指令。Java堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的 参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中(native method stacks),可能再寄存器或者其他非平台独立的内存中。 Java堆栈有堆栈块(stack frames (or frames))组成。堆栈块包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的块压到Java堆栈中,当这个方法运行结束时,Java虚拟机会将对应的块弹出并抛弃。 Java虚拟机不使用寄存器保存计算的中间结果,而是用Java堆栈在存放中间结果。这是的Java虚拟机的指令更紧凑,也更容易在一个没有寄存器的设备上实现Java虚拟机。 图中的Java堆栈中向下增长的,PC寄存器中线程三为灰色,是因为它正在执行本地方法,他的下一条执行指令不保存在PC寄存器中。 四、数据类型(Data Types) 所有Java虚拟机中使用的数据都有确定的数据类型,数据类型和操作都在Java虚拟机规范中严格定义。Java中的数据类型分为原始数据类型 (primitive types)和引用数据类型(reference type)。引用类型依赖于实际的对象,但不是对象本身。原始数据类型不依赖于任何东西,他们就是本身表示的数据。 所有Java程序语言中的原始 数据类型,都是Java虚拟机的原始数据类型,除了布尔型(boolean)。当编译器将Java源代码编译为自己码时,使用整型(int)或者字节型 (byte)去表示布尔型。在Java虚拟机中使用整数0表示布尔型的false,使用非零整数表示布尔型的true,布尔数组被表示为字节数组,虽然他 们可能会以字节数组或者字节块(bit fields)保存在堆中。 除了布尔型,其他Java语言中的原始类型都是Java虚拟机中的数据类型。在Java中数据类型被分为:整形的byte,short,int,long;char和浮点型的float,double。Java语言中的数据类型在任何主机上都有同样的范围。 在Java虚拟机中还存在一个Java语言中不能使用的原始数据类型返回值类型(returnValue)。这种类型被用来实现Java程序中的“finally clauses”,具体的参见18章的“Finally Clauses”。 引用类型可能被创建为:类类型(class type),接口类型(interface type),数组类型(array type)。他们都引用被动态创建的对象。当引用类型引用null时,说明没有引用任何对象。 Java虚拟机规范只定义了每一种数据类型表示的范围,没有定义在存储时每种类型占用的空间。他们如何存储由Java虚拟机的实现者自己决定。
TypeRange byte8-bit signed two's complement integer (-27 to 27 - 1, inclusive) short16-bit signed two's complement integer (-215 to 215 - 1, inclusive) int32-bit signed two's complement integer (-231 to 231 - 1, inclusive) long64-bit signed two's complement integer (-263 to 263 - 1, inclusive) char16-bit unsigned Unicode character (0 to 216 - 1, inclusive) float32-bit IEEE 754 single-precision float double64-bit IEEE 754 double-precision float returnValueaddress of an opcode within the same method referencereference to an object on the heap, or null
五、字节长度 Java虚拟机中最小的数据单元式字(word),其大小由Java虚拟机的实现者定义。但是一个字的大小必须足够容纳byte,short,int, char,float,returnValue,reference;两个字必须足够容纳long,double。所以虚拟机的实现者至少提供的字不能小 于31bits的字,但是最好选择特定平台上最有效率的字长。 在运行时,Java程序不能决定所运行机器的字长。字长也不会影响程序的行为,他只是在Java虚拟机中的一种表现方式。 六、类加载器子系统 Java虚拟机中的类加载器分为两种:原始类加载器(primordial class loader)和类加载器对象(class loader objects)。原始类加载器是Java虚拟机实现的一部分,类加载器对象是运行中的程序的一部分。不同类加载器加载的类被不同的命名空间所分割。 类加载器调用了许多Java虚拟机中其他的部分和java.lang包中的很多类。比如,类加载对象就是java.lang.ClassLoader子类 的实例,ClassLoader类中的方法可以访问虚拟机中的类加载机制;每一个被Java虚拟机加载的类都会被表示为一个 java.lang.Class类的实例。像其他对象一样,类加载器对象和Class对象都保存在堆中,被加载的信息被保存在方法区中。 1、加载、连接、初始化(Loading, Linking and Initialization) 类加载子系统不仅仅负责定位并加载类文件,他按照以下严格的步骤作了很多其他的事情:(具体的信息参见第七章的“类的生命周期”) 1)、加载:寻找并导入指定类型(类和接口)的二进制信息 2)、连接:进行验证、准备和解析 ①验证:确保导入类型的正确性 ②准备:为类型分配内存并初始化为默认值 ③解析:将字符引用解析为直接饮用 3)、初始化:调用Java代码,初始化类变量为合适的值 2、原始类加载器(The Primordial Class Loader) 每个Java虚拟机都必须实现一个原始类加载器,他能够加载那些遵守类文件格式并且被信任的类。但是,Java虚拟机的规范并没有定义如何加载类,这由 Java虚拟机实现者自己决定。对于给定类型名的类型,原始莱加载器必须找到那个类型名加“.class”的文件并加载入虚拟机中。 3、类加载器对象 虽然类加载器对象是Java程序的一部分,但是ClassLoader类中的三个方法可以访问Java虚拟机中的类加载子系统。 1)、protected final Class defineClass(…):使用这个方法可以出入一个字节数组,定义一个新的类型。 2)、protected Class findSystemClass(String name):加载指定的类,如果已经加载,就直接返回。 3)、protected final void resolveClass(Class c):defineClass()方法只是加载一个类,这个方法负责后续的动态连接和初始化。 具体的信息,参见第八章“连接模型”( The Linking Model)。 4、命名空间 当多个类加载器加载了同一个类时,为了保证他们名字的唯一性,需要在类名前加上加载该类的类加载器的标识。具体的信息, 七、方法区(The Method Area) 在Java虚拟机中,被加载类型的信息都保存在方法区中。这写信息在内存中的组织形式由虚拟机的实现者定义,比如,虚拟机工作在一个“little- endian”的处理器上,他就可以将信息保存为“little-endian”格式的,虽然在Java类文件中他们是以“big-endian”格式保 存的。设计者可以用最适合并地机器的表示格式来存储数据,以保证程序能够以最快的速度执行。但是,在一个只有很小内存的设备上,虚拟机的实现者就不会占用 很大的内存。 程序中的所有线程共享一个方法区,所以访问方法区信息的方法必须是线程安全的。如果你有两个线程都去加载一个叫Lava的类,那只能由一个线程被容许去加载这个类,另一个必须等待。 在程序运行时,方法区的大小是可变的,程序在运行时可以扩展。有些Java虚拟机的实现也可以通过参数也订制方法区的初始大小,最小值和最大值。 方法区也可以被垃圾收集。因为程序中的内由类加载器动态加载,所有类可能变成没有被引用(unreferenced)的状态。当类变成这种状态时,他就可 能被垃圾收集掉。没有加载的类包括两种状态,一种是真正的没有加载,另一个种是“unreferenced”的状态。 1、类型信息(Type Information) 每一个被加载的类型,在Java虚拟机中都会在方法区中保存如下信息: 1)、类型的全名(The fully qualified name of the type) 2)、类型的父类型的全名(除非没有父类型,或者弗雷形式java.lang.Object)(The fully qualified name of the typeís direct superclass) 3)、给类型是一个类还是接口(class or an interface)(Whether or not the type is a class ) 4)、类型的修饰符(public,private,protected,static,final,volatile,transient等)(The typeís modifiers) 5)、所有父接口全名的列表(An ordered list of the fully qualified names of any direct superinterfaces) 类型全名保存的数据结构由虚拟机实现者定义。除此之外,Java虚拟机还要为每个类型保存如下信息: 1)、类型的常量池(The constant pool for the type) 2)、类型字段的信息(Field information) 3)、类型方法的信息(Method information) 4)、所有的静态类变量(非常量)信息(All class (static) variables declared in the type, except constants) 5)、一个指向类加载器的引用(A reference to class ClassLoader) 6)、一个指向Class类的引用(A reference to class Class)
1)、类型的常量池(The constant pool for the type) 常量池中保存中所有类型是用的有序的常量集合,包含直接常量(literals)如字符串、整数、浮点数的常量,和对类型、字段、方法的符号引用。常量池 中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对 象。 2)、类型字段的信息(Field information) 字段名、字段类型、字段的修饰符(public,private,protected,static,final,volatile,transient等)、字段在类中定义的顺序。 3)、类型方法的信息(Method information) 方法名、方法的返回值类型(或者是void)、方法参数的个数、类型和他们的顺序、字段的修饰符(public,private,protected,static,final,volatile,transient等)、方法在类中定义的顺序 如果不是抽象和本地本法还需要保存 方法的字节码、方法的操作数堆栈的大小和本地变量区的大小(稍候有详细信息)、异常列表 4)、类(静态)变量(Class Variables) 类变量被所有类的实例共享,即使不通过类的实例也可以访问。这些变量绑定在类上(而不是类的实例上),所以他们是类的逻辑数据的一部分。在Java虚拟机使用这个类之前就需要为类变量(non-final)分配内存 常量(final)的处理方式于这种类变量(non-final)不一样。每一个类型在用到一个常量的时候,都会复制一份到自己的常量池中。常量也像类变 量一样保存在方法区中,只不过他保存在常量池中。(可能是,类变量被所有实例共享,而常量池是每个实例独有的)。Non-final类变量保存为定义他的 类型数据(data for the type that declares them)的一部分,而final常量保存为使用他的类型数据(data for any type that uses them)的一部分。 5)、指向类加载器的引用(A reference to class ClassLoader) 每一个被Java虚拟机加载的类型,虚拟机必须保存这个类型是否由原始类加载器或者类加载器加载。那些被类加载器加载的类型必须保存一个指向类加载器的引 用。当类加载器动态连接时,会使用这条信息。当一个类引用另一个类时,虚拟机必须保存那个被引用的类型是被同一个类加载器加载的,这也是虚拟机维护不同命 名空间的过程。 6)、指向Class类的引用(A reference to class Class) Java虚拟机为每一个加载的类型创建一个java.lang.Class类的实例。你也可以通过Class类的方法: public static Class forName(String className)来查找或者加载一个类,并取得相应的Class类的实例。通过这个Class类的实例,我们可以访问Java虚拟机方法区中的信息。具体参照Class类的JavaDoc。 2、方法列表(Method Tables) 为了更有效的访问所有保存在方法区中的数据,这些数据的存储结构必须经过仔细的设计。所有方法区中,除了保存了上边的那些原始信息外,还有一个为了加快存 取速度而设计的数据结构,比如方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例 方法的引用,报错那些父类中调用的方法。
八、堆 当Java程序创建一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,所有的线程都共享他。 1、垃圾收集(Garbage Collection) 垃圾收集是释放没有被引用的对象的主要方法。它也可能会为了减少堆的碎片,而移动对象。在Java虚拟机的规范中没有严格定义垃圾收集,只是定义一个Java虚拟机的实现必须通过某种方式管理自己的堆。 2、对象存储结构(Object Representation) Java虚拟机的规范中没有定义对象怎样在堆中存储。每一个对象主要存储的是他的类和父类中定义的对象变量。对于给定的对象的引用,虚拟机必须嫩耨很快的 定位到这个对象的数据。另为,必须提供一种通过对象的引用方法对象数据的方法,比如方法区中的对象的引用,所以一个对象保存的数据中往往含有一个某种形式 指向方法区的指针。 一个可能的堆的设计是将堆分为两个部分:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每一个引用池中的条目都包含两个部分:指向对象池中对 象数据的指针和方法区中对象类数据的指针。这种设计能够方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只需要修改对应引用池中 的指针地址。但是每次访问对象的数据都需要处理两次指针。 另一种堆的设计是:一个对象的引用就是一个指向一堆数据和指向相应对象的偏移指针。这种设计方便了对象的访问,可是对象的移动要变的异常复杂。 当程序试图将一个对象转换为另一种类型时,虚拟机需要判断这种转换是否是这个对象的类型,或者是他的父类型。当程序适用instanceof语句的时候也 会做类似的事情。当程序调用一个对象的方法时,虚拟机需要进行动态绑定,他必须判断调用哪一个类型的方法。这也需要做上面的判断。 无论虚拟机实现者使用哪一种设计,他都可能为每一个对象保存一个类似方法列表的信息。因为他可以提升对象方法调用的速度,对提升虚拟机的性能非常重要,但 是虚拟机的规范中比没有要求必须实现类似的数据结构。 每一个Java虚拟机中的对象必须关联一个用于同步多线程的lock(mutex)。同一时刻,只能有一个对象拥有这个对象的锁。当一个拥有这个这个对象 的锁,他就可以多次申请这个锁,但是也必须释放相应次数的锁才能真正释放这个对象锁。很多对象在整个生命周期中都不会被锁,所以这个信息只有在需要时才需 要添加。很多Java虚拟机的实现都没有在对象的数据中包含“锁定数据”,只是在需要时才生成相应的数据。除了实现对象的锁定,每一个对象还逻辑关联到一 个“wait set”的实现。锁定帮组线程独立处理共享的数据,不需要妨碍其他的线程。“wait set”帮组线程协作完成同一个目标。“wait set”往往通过Object类的wait()和notify()方法来实现。 垃圾收集也需要堆中的对象是否被关联的信息。Java虚拟机规范中指出垃圾收集一个运行一个对象的finalizer方法一次,但是容许 finalizer方法重新引用这个对象,当这个对象再次不被引用时,就不需要再次调用finalize方法。所以虚拟机也需要保存finalize方法 是否运行过的信息。更多信息参见第九章的“垃圾收集” 3、数组的保存(Array Representation) 在Java 中,数组是一种完全意义上的对象,他和对象一样保存在堆中、有一个指向Class类实例的引用。所有同一维度和类型的数组拥有同样的Class,数组的长 度不做考虑。对应Class的名字表示为维度和类型。比如一个整型数据的Class为“[I”,字节型三维数组Class名为“[[[B”,两维对象数据 Class名为“[[Ljava.lang.Object”。 多维数组被表示为数组的数组 , 数组必须在堆中保存数组的长度,数组的数据和一些对象数组类型数据的引用。通过一个数组引用的,虚拟机应该能够取得一个数组的长度,通过索引能够访问特定 的数据,能够调用Object定义的方法。Object是所有数据类的直接父类。
Historical Collection Classes(JDK1.1 之前) 提供的容器有Arrays,Vector,Stack,Hashtable,Properties,BitSet。其中定义出一种走访群集内各元素的标准方式,称为Enumeration(列举器)接口,用法如下:
Vector v=new Vector(); for (Enumeration enum =v.elements(); enum.hasMoreElements();) { Object o = enum.nextElement(); processObject(o); }
而在JDK1.2版本中引入了Iterator接口,新版本的集合对象(HashSet,HashMap,WeakHeahMap,ArrayList,TreeSet,TreeMap, LinkedList)是通过Iterator接口访问集合元素的。 例如:
List list=new ArrayList(); for(Iterator it=list.iterator();it.hasNext();) { System.out.println(it.next()); }
这样,如果将老版本的程序运行在新的Java编译器上就会出错。因为List接口中已经没有elements(),而只有iterator()了。那么如何可以使老版本的程序运行在新的Java编译器上呢?如果不加修改,是肯定不行的,但是修改要遵循“开-闭”原则。 这时候我想到了Java设计模式中的适配器模式。
package net.blogjava.lzqdiy;
import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List;
public class NewEnumeration implements Enumeration {
Iterator it; public NewEnumeration(Iterator it) { this.it=it; // TODO Auto-generated constructor stub }
public boolean hasMoreElements() { // TODO Auto-generated method stub return it.hasNext(); }
public Object nextElement() { // TODO Auto-generated method stub return it.next(); } public static void main(String[] args) { List list=new ArrayList(); list.add("a"); list.add("b"); list.add("C"); for(Enumeration e=new NewEnumeration(list.iterator());e.hasMoreElements();) { System.out.println(e.nextElement()); } } }
NewEnumeration是一个适配器类,通过它实现了从Iterator接口到Enumeration接口的适配,这样我们就可以使用老版本的代码来使用新的集合对象了。
一、反射的概念 : 反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在Lisp和面向对象方面取得了成绩。其中LEAD/LEAD++ 、OpenC++ 、MetaXa和OpenJava等就是基于反射机制的语言。最近,反射机制也被应用到了视窗系统、操作系统和文件系统中。
反射本身并不是一个新概念,它可能会使我们联想到光学中的反射概念,尽管计算机科学赋予了反射概念新的含义,但是,从现象上来说,它们确实有某些相通之处,这些有助于我们的理解。在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。可以看出,同一般的反射概念相比,计算机科学领域的反射不单单指反射本身,还包括对反射结果所采取的措施。所有采用反射机制的系统(即反射系统)都希望使系统的实现更开放。可以说,实现了反射机制的系统都具有开放性,但具有开放性的系统并不一定采用了反射机制,开放性是反射系统的必要条件。一般来说,反射系统除了满足开放性条件外还必须满足原因连接(Causally-connected)。所谓原因连接是指对反射系统自描述的改变能够立即反映到系统底层的实际状态和行为上的情况,反之亦然。开放性和原因连接是反射系统的两大基本要素.
Java中,反射是一种强大的工具。它使您能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代表链接。反射允许我们在编写与执行时,使我们的程序代码能够接入装载到JVM中的类的内部信息,而不是源代码中选定的类协作的代码。这使反射成为构建灵活的应用的主要工具。但需注意的是:如果使用不当,反射的成本很高。
二、Java中的类反射: Reflection 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。Java 的这一能力在实际应用中也许用得不是很多,但是在其它的程序设计语言中根本就不存在这一特性。例如,Pascal、C 或者 C++ 中就没有办法在程序中获得函数定义相关的信息。
1.检测类:
1.1 reflection的工作机制
考虑下面这个简单的例子,让我们看看 reflection 是如何工作的。
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { Class c = Class.forName(args[0]); Method m[] = c.getDeclaredMethods(); for (int i = 0; i < m.length; i++) System.out.println(m[i].toString()); } catch (Throwable e) { System.err.println(e); } } }
按如下语句执行:
java DumpMethods java.util.Stack
它的结果输出为:
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object)
这样就列出了java.util.Stack 类的各方法名以及它们的限制符和返回类型。
这个程序使用 Class.forName 载入指定的类,然后调用 getDeclaredMethods 来获取这个类中定义了的方法列表。java.lang.reflect.Methods 是用来描述某个类中单个方法的一个类。
1.2 Java类反射中的主要方法
对于以下三类组件中的任何一类来说 -- 构造函数、字段和方法 -- java.lang.Class 提供四种独立的反射调用,以不同的方式来获得信息。调用都遵循一种标准格式。以下是用于查找构造函数的一组反射调用:
Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的公共构造函数,
Constructor[] getConstructors() -- 获得类的所有公共构造函数
Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(与接入级别无关)
Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)
获得字段信息的Class 反射调用不同于那些用于接入构造函数的调用,在参数类型数组中使用了字段名:
l Field getField(String name) -- 获得命名的公共字段
l Field[] getFields() -- 获得类的所有公共字段
l Field getDeclaredField(String name) -- 获得类声明的命名的字段
l Field[] getDeclaredFields() -- 获得类声明的所有字段
用于获得方法信息函数:
l Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法
l Method[] getMethods() -- 获得类的所有公共方法
l Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法
l Method[] getDeclaredMethods() -- 获得类声明的所有方法
1.3开始使用 Reflection:
用于 reflection 的类,如 Method,可以在 java.lang.relfect 包中找到。使用这些类的时候必须要遵循三个步骤:第一步是获得你想操作的类的 java.lang.Class 对象。在运行中的 Java 程序中,用 java.lang.Class 类来描述类和接口等。
下面就是获得一个 Class 对象的方法之一:
Class c = Class.forName("java.lang.String");
这条语句得到一个 String 类的类对象。还有另一种方法,如下面的语句:
Class c = int.class;
或者
Class c = Integer.TYPE;
它们可获得基本类型的类信息。其中后一种方法中访问的是基本类型的封装类 (如 Integer) 中预先定义好的 TYPE 字段。
第二步是调用诸如 getDeclaredMethods 的方法,以取得该类中定义的所有方法的列表。
一旦取得这个信息,就可以进行第三步了——使用 reflection API 来操作这些信息,如下面这段代码:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());
它将以文本方式打印出 String 中定义的第一个方法的原型。
2.处理对象:
如果要作一个开发工具像debugger之类的,你必须能发现filed values,以下是三个步骤:
a.创建一个Class对象 b.通过getField 创建一个Field对象 c.调用Field.getXXX(Object)方法(XXX是Int,Float等,如果是对象就省略;Object是指实例).
例如: import java.lang.reflect.*; import java.awt.*;
class SampleGet {
public static void main(String[] args) { Rectangle r = new Rectangle(100, 325); printHeight(r);
}
static void printHeight(Rectangle r) { Field heightField; Integer heightValue; Class c = r.getClass(); try { heightField = c.getField("height"); heightValue = (Integer) heightField.get(r); System.out.println("Height: " + heightValue.toString()); } catch (NoSuchFieldException e) { System.out.println(e); } catch (SecurityException e) { System.out.println(e); } catch (IllegalAcces***ception e) { System.out.println(e); } } }
三、安全性和反射: 在处理反射时安全性是一个较复杂的问题。反射经常由框架型代码使用,由于这一点,我们可能希望框架能够全面接入代码,无需考虑常规的接入限制。但是,在其它情况下,不受控制的接入会带来严重的安全性风险,例如当代码在不值得信任的代码共享的环境中运行时。
由于这些互相矛盾的需求,Java编程语言定义一种多级别方法来处理反射的安全性。基本模式是对反射实施与应用于源代码接入相同的限制:
n 从任意位置到类公共组件的接入
n 类自身外部无任何到私有组件的接入
n 受保护和打包(缺省接入)组件的有限接入
不过至少有些时候,围绕这些限制还有一种简单的方法。我们可以在我们所写的类中,扩展一个普通的基本类java.lang.reflect.AccessibleObject 类。这个类定义了一种setAccessible方法,使我们能够启动或关闭对这些类中其中一个类的实例的接入检测。唯一的问题在于如果使用了安全性管理器,它将检测正在关闭接入检测的代码是否许可了这样做。如果未许可,安全性管理器抛出一个例外。
下面是一段程序,在TwoString 类的一个实例上使用反射来显示安全性正在运行:
public class ReflectSecurity {
public static void main(String[] args) {
try {
TwoString ts = new TwoString("a", "b");
Field field = clas.getDeclaredField("m_s1");
// field.setAccessible(true);
System.out.println("Retrieved value is " +
field.get(inst));
} catch (Exception ex) {
ex.printStackTrace(System.out);
}
}
}
如果我们编译这一程序时,不使用任何特定参数直接从命令行运行,它将在field .get(inst)调用中抛出一个IllegalAcces***ception异常。如果我们不注释field.setAccessible(true)代码行,那么重新编译并重新运行该代码,它将编译成功。最后,如果我们在命令行添加了JVM参数-Djava.security.manager以实现安全性管理器,它仍然将不能通过编译,除非我们定义了ReflectSecurity类的许可权限。
四、反射性能: 反射是一种强大的工具,但也存在一些不足。一个主要的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。
下面的程序是字段接入性能测试的一个例子,包括基本的测试方法。每种方法测试字段接入的一种形式 -- accessSame 与同一对象的成员字段协作,accessOther 使用可直接接入的另一对象的字段,accessReflection 使用可通过反射接入的另一对象的字段。在每种情况下,方法执行相同的计算 -- 循环中简单的加/乘顺序。
程序如下:
public int accessSame(int loops) {
m_value = 0;
for (int index = 0; index < loops; index++) {
m_value = (m_value + ADDITIVE_VALUE) *
MULTIPLIER_VALUE;
}
return m_value;
}
public int accessReference(int loops) {
TimingClass timing = new TimingClass();
for (int index = 0; index < loops; index++) {
timing.m_value = (timing.m_value + ADDITIVE_VALUE) *
MULTIPLIER_VALUE;
}
return timing.m_value;
}
public int accessReflection(int loops) throw* **ception {
TimingClass timing = new TimingClass();
try {
Field field = TimingClass.class.
getDeclaredField("m_value");
for (int index = 0; index < loops; index++) {
int value = (field.getInt(timing) +
ADDITIVE_VALUE) * MULTIPLIER_VALUE;
field.setInt(timing, value);
}
return timing.m_value;
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}
}
在上面的例子中,测试程序重复调用每种方法,使用一个大循环数,从而平均多次调用的时间衡量结果。平均值中不包括每种方法第一次调用的时间,因此初始化时间不是结果中的一个因素。下面的图清楚的向我们展示了每种方法字段接入的时间:
图 1:字段接入时间 :
我们可以看出:在前两副图中(Sun JVM),使用反射的执行时间超过使用直接接入的1000倍以上。通过比较,IBM JVM可能稍好一些,但反射方法仍旧需要比其它方法长700倍以上的时间。任何JVM上其它两种方法之间时间方面无任何显著差异,但IBM JVM几乎比Sun JVM快一倍。最有可能的是这种差异反映了Sun Hot Spot JVM的专业优化,它在简单基准方面表现得很糟糕。反射性能是Sun开发1.4 JVM时关注的一个方面,它在反射方法调用结果中显示。在这类操作的性能方面,Sun 1.4.1 JVM显示了比1.3.1版本很大的改进。
如果为为创建使用反射的对象编写了类似的计时测试程序,我们会发现这种情况下的差异不象字段和方法调用情况下那么显著。使用newInstance()调用创建一个简单的java.lang.Object实例耗用的时间大约是在Sun 1.3.1 JVM上使用new Object()的12倍,是在IBM 1.4.0 JVM的四倍,只是Sun 1.4.1 JVM上的两部。使用Array.newInstance(type, size)创建一个数组耗用的时间是任何测试的JVM上使用new type[size]的两倍,随着数组大小的增加,差异逐步缩小。
结束语: Java语言反射提供一种动态链接程序组件的多功能方法。它允许程序创建和控制任何类的对象(根据安全性限制),无需提前硬编码目标类。这些特性使得反射特别适用于创建以非常普通的方式与对象协作的库。例如,反射经常在持续存储对象为数据库、XML或其它外部格式的框架中使用。Java reflection 非常有用,它使类和数据结构能按名称动态检索相关信息,并允许在运行着的程序中操作这些信息。Java 的这一特性非常强大,并且是其它一些常用语言,如 C、C++、Fortran 或者 Pascal 等都不具备的。
但反射有两个缺点。第一个是性能问题。用于字段和方法接入时反射要远慢于直接代码。性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。即使测试中最坏情况下的计时图显示的反射操作只耗用几微秒。仅反射在性能关键的应用的核心逻辑中使用时性能问题才变得至关重要。
许多应用中更严重的一个缺点是使用反射会模糊程序内部实际要发生的事情。程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术会带来维护问题。反射代码比相应的直接代码更复杂,正如性能比较的代码实例中看到的一样。解决这些问题的最佳方案是保守地使用反射——仅在它可以真正增加灵活性的地方——记录其在目标类中的使用。
-
public class ServerSocket - extends Object
此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
服务器套接字的实际工作由 SocketImpl 类的实例执行。应用程序可以更改创建套接字实现的套接字工厂来配置它自身,从而创建适合本地防火墙的套接字。
-
public class Socket - extends Object
此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器之间的通信端点。
套接字的实际工作由 SocketImpl 类的实例执行。应用程序通过更改创建套接字实现的套接字工厂可以配置它自身,以创建适合本地防火墙的套接字。
/** * 为了验证我们的服务器程序能否正常工作,还必须有一个客户端程序与之通信。 * 我们也没必要每次都要编写客户端程序来测试。其实, Windows提供的telnet * 程序,就是一个TCP客户端程序,我们只要使用telnet程序对我们的服务器程序 * 进行测试,我们只要在运行telnet程序时指定所要连接的服务器程序的IP地址 * 和端口号,telnet程序就会按照指定的参数去与服务器程序进行连接。建立连接 * 后,在telnet程序窗口中键入的内容会发送到服务器,从服务器端接收到的数据 * 会显示在窗口中。 * 先运行TcpServer程序,然后在命令行中输入 telnet 10.214.16.80 8001 * 就会执行 */
// TCP 服务端程序 public class TcpServer {
public static void main(String[] args){ try{ //建立一个在8001端口上等待连接的ServerSocket对象 ServerSocket ss = new ServerSocket(8001); //侦听并接受到此套接字的连接。此方法在进行连接之前一直阻塞,返回客户端套接字 Socket s = ss.accept(); InputStream ips = s.getInputStream();//返回此套接字的输入流 OutputStream ops = s.getOutputStream();//返回此套接字的输出流 //将字节从指定的字节数组写入此输出流 ops.write("chenweicai...".getBytes()); // byte[] buf = new byte[1024]; // // 从输入流中读取一定数量的字节并将其存储在缓冲区数组buf中 // int len = ips.read(buf); // System.out.println(new String(buf, 0, len)); //由于telent只要输入就发送,而不管有没有回车,所以只有第一个字符被发送。 //java提供了一个BufferedReader类,可以帮助我们按行处理输入流。 BufferedReader br = new BufferedReader (new InputStreamReader(ips)); System.out.println("The message comes form client: " + br.readLine());//输出客户端输入的内容 br.close();//关闭包装类,会自动关闭包装类中所包装的底层类,所以不用调用ips.close(). // ips.close(); ops.close(); s.close(); ss.close(); }catch (Exception e){ e.printStackTrace(); } } }
给个具体的例子
/** * 实现服务器和客户端之间的对话功能 */ public class Servicer implements Runnable {
Socket s; public Servicer (Socket s){ this.s = s; } public void run(){ try { InputStream ips = s.getInputStream(); OutputStream ops = s.getOutputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(ips)); DataOutputStream dos = new DataOutputStream(ops); while(true){ String strWord = br.readLine(); System.out.println("From Client StrWord: " + strWord + strWord.length()); if(strWord.equalsIgnoreCase("quit")) break; String strEcho = (new StringBuffer(strWord).reverse()).toString(); dos.writeBytes(strWord + "---->" + strEcho + System.getProperty("line.separator")); } br.close(); dos.close(); s.close(); } catch (IOException e) { e.printStackTrace(); } } }
import java.net.ServerSocket; import java.net.Socket;
public class TcpServer {
public static void main(String[] args){ try{ ServerSocket ss = new ServerSocket(8001); while(true){ Socket s = ss.accept(); new Thread(new Servicer(s)).start(); } }catch(Exception e){ } } }
public class TcpClient {
public static void main(String[] args){ try{ if(args.length < 2){ System.out.println("Usage: java TcpClient ServerIP ServerPort"); return; } Socket s = new Socket(InetAddress.getByName(args[0]), Integer.parseInt(args[1])); InputStream ips = s.getInputStream(); OutputStream ops = s.getOutputStream(); BufferedReader brKey = new BufferedReader(new InputStreamReader(System.in)); DataOutputStream dos = new DataOutputStream(ops); BufferedReader brNet = new BufferedReader(new InputStreamReader(ips)); while(true){ String strWord = brKey.readLine(); dos.writeBytes(strWord + System.getProperty("ling.separator")); if(strWord.equalsIgnoreCase("quit")) break; else System.out.println(brNet.readLine()); } dos.close(); brNet.close(); brKey.close(); s.close(); }catch (Exception e){ } } }
-
public class DatagramSocket - extends Object
此类表示用来发送和接收数据报包的套接字。
数据报套接字是包投递服务的发送或接收点。每个在数据报套接字上发送或接收的包都是单独编址和路由的。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。
在 DatagramSocket 上总是启用 UDP 广播发送。为了接收广播包,应该将 DatagramSocket 绑定到通配符地址。在某些实现中,将 DatagramSocket 绑定到一个更加具体的地址时广播包也可以被接收。
示例:DatagramSocket s = new DatagramSocket(null); s.bind(new InetSocketAddress(8888)); 这等价于:DatagramSocket s = new DatagramSocket(8888); 两个例子都能创建能够在 UDP 8888 端口上接收广播的 DatagramSocket。
-
public final class DatagramPacket - extends Object
此类表示数据报包。
数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达。不对包投递做出保证。
给个具体实现的例子
//UDP数据的发送类似发送寻呼一样的道理。就像寻呼机必须先处于开机接收状态才能接收寻呼一样的道理 //我们先要运行UDP接收程序,再运行UDP发送程序。UDP数据包的接收是过期作废的。 public class UdpSend {
public static void main(String[] args) throws SocketException, UnknownHostException{ DatagramSocket ds = new DatagramSocket();//创建用来发送数据报包的套接字 String str = "Hello World,陈伟才"; DatagramPacket dp = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("10.214.16.80"), 3000); //构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号 try { ds.send(dp); } catch (IOException e) { e.printStackTrace(); } ds.close(); } }
public class UdpRecv {
public static void main(String[] args) throws Exception{ DatagramSocket ds = new DatagramSocket(3000);//创建接收数据报套接字并将其绑定到本地主机上的指定端口 byte[] buf = new byte[1024]; DatagramPacket dp = new DatagramPacket(buf, 1024); ds.receive(dp); String strRecv = new String (dp.getData(), 0, dp.getLength()) + " from " + dp.getAddress().getHostAddress() + ":" + dp.getPort(); System.out.println(strRecv); ds.close(); } }
再给个例子Chat
public class Chat {
Frame f = new Frame("我的聊天室"); /* * tfIp 是用于输入IP地址的文本框,在发送数据时, 要取出其中的IP地址, * 所以将其定义成员变量,以便发送消息的程序代码能访问。 */ TextField tfIP = new TextField(15); /** * lst时用于显示接收消息的列表框, 在接收到数据时,要向其中增加新的记录项 * 所以将其定义成员变量,以便发送消息的程序代码能访问 */ List lst = new List(6); DatagramSocket ds; public Chat(){ try{ ds = new DatagramSocket(3000); }catch(Exception e){ e.printStackTrace(); } new Thread (new Runnable(){ public void run(){ byte[] buf = new byte[1024]; DatagramPacket dp = new DatagramPacket(buf, 1024); while (true){ try{ ds.receive(dp); lst.add(new String(buf, 0 , dp.getLength()) + " : form " + dp.getAddress().getHostAddress(), 0); }catch(Exception ee){ ee.printStackTrace(); } } } }).start(); } public void init(){ f.setSize(300, 300); f.add(lst); Panel p = new Panel(); p.setLayout(new BorderLayout()); p.add("West", tfIP); TextField tfData = new TextField (20); p.add("East", tfData); f.add("South", p); f.setVisible(true); f.setResizable(false);//不能改变窗口大小 //增加关闭窗口的事件处理代码 f.addWindowListener(new WindowAdapter(){ public void windowClosing(WindowEvent e){ ds.close();//程序退出时,关闭Socket,释放相关的资源 f.setVisible(false); f.dispose(); System.exit(0); } }); //增加在消息文本框中按下回车键的事件处理代码 tfData.addActionListener(new ActionListener(){ public void actionPerformed (ActionEvent e){ //要在这里增加网络消息发送相关程序代码 //下面的语句用于数据发送后,清空文本框中原来的内容 // 取出文本框的消息字符串,并将其转换成字节数组 byte[] buf; buf = e.getActionCommand().getBytes(); try { DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName(tfIP.getText()), 3000); try { ds.send(dp); } catch (IOException e1) { e1.printStackTrace(); } } catch (UnknownHostException e1) { e1.printStackTrace(); } ((TextField)e.getSource()).setText(""); } }); } public static void main(String[] args){ Chat chat = new Chat(); chat.init(); } }
课前思考 1. 什么是TCP/ IP协议? 2. TCP/IP有哪两种传输协议,各有什么特点? 3. 什么是URL? 4. URL和IP地址有什么样的关系? 5. 什么叫套接字(Socket)? 6. 套接字(Socket)和TCP/IP协议的关系? 7. URL和套接字(Socket)的关系? 8.1 网络编程基本概念,TCP/IP协议简介
8.1.1 网络基础知识
网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。网络编程中有两个主要的问题,一个是如何准确的定位网络上一台或多台主机,另一个就是找到主机后如何可靠高效的进行数据传输。在TCP/IP协议中IP层主要负责网络主机的定位,数据传输的路由,由IP地址可以唯一地确定Internet上的一台主机。而TCP层则提供面向应用的可靠的或非可靠的数据传输机制,这是网络编程的主要对象,一般不需要关心IP层是如何处理数据的。 目前较为流行的网络编程模型是客户机/服务器(C/S)结构。即通信双方一方作为服务器等待客户提出请求并予以响应。客户则在需要服务时向服务器提出申请。服务器一般作为守护进程始终运行,监听网络端口,一旦有客户请求,就会启动一个服务进程来响应该客户,同时自己继续监听服务端口,使后来的客户也能及时得到服务。
8.1.3两类传输协议:TCP;UDP 尽管TCP/IP协议的名称中只有TCP这个协议名,但是在TCP/IP的传输层同时存在TCP和UDP两个协议。
TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。 UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。 下面我们对这两种协议做简单比较: 使用UDP时,每个数据报中都给出了完整的地址信息,因此无需要建立发送方和接收方的连接。对于TCP协议,由于它是一个面向连接的协议,在socket之间进行数据传输之前必然要建立连接,所以在TCP中多了一个连接建立的时间。 使用UDP传输数据时是有大小限制的,每个被传输的数据报必须限定在64KB之内。而TCP没有这方面的限制,一旦连接建立起来,双方的socket就可以按统一的格式传输大量的数据。UDP是一个不可靠的协议,发送方所发送的数据报并不一定以相同的次序到达接收方。而TCP是一个可靠的协议,它确保接收方完全正确地获取发送方所发送的全部数据。 总之,TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输。相比之下UDP操作简单,而且仅需要较少的监护,因此通常用于局域网高可靠性的分散系统中client/server应用程序。 读者可能要问,既然有了保证可靠传输的TCP协议,为什么还要非可靠传输的UDP协议呢?主要的原因有两个。一是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。二是在许多应用中并不需要保证严格的传输可靠性,比如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。
8.2 基于URL的高层次Java网络编程
8.2.1一致资源定位器URL
URL(Uniform Resource Locator)是一致资源定位器的简称,它表示Internet上某一资源的地址。通过URL我们可以访问Internet上的各种网络资源,比如最常见的WWW,FTP站点。浏览器通过解析给定的URL可以在网络上查找相应的文件或其他资源。
8.2.2 URL的组成
protocol://resourceName 协议名(protocol)指明获取资源所使用的传输协议,如http、ftp、gopher、file等,资源名(resourceName)则应该是资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。例如: http://www.sun.com/ 协议名://主机名 http://home.netscape.com/home/welcome.html 协议名://机器名+文件名 http://www.gamelan.com:80/Gamelan/network.html#BOTTOM 协议名://机器名+端口号+文件名+内部引用.
8.2.3 创建一个URL
为了表示URL, java.net中实现了类URL。我们可以通过下面的构造方法来初始化一个URL对象: (1) public URL (String spec); 通过一个表示URL地址的字符串可以构造一个URL对象。 URL urlBase=new URL("http://www. 263.net/") (2) public URL(URL context, String spec); 通过基URL和相对URL构造一个URL对象。 URL net263=new URL ("http://www.263.net/"); URL index263=new URL(net263, "index.html") (3) public URL(String protocol, String host, String file); new URL("http", "www.gamelan.com", "/pages/Gamelan.net. html"); (4) public URL(String protocol, String host, int port, String file); URL gamelan=new URL("http", "www.gamelan.com", 80, "Pages/Gamelan.network.html");
注意:类URL的构造方法都声明抛弃非运行时例外(MalformedURLException),因此生成URL对象时,我们必须要对这一例外进行处理,通常是用try-catch语句进行捕获。格式如下:
try{ URL myURL= new URL(…) }catch (MalformedURLException e){ … }
8.2.4 解析一个URL
一个URL对象生成后,其属性是不能被改变的,但是我们可以通过类URL所提供的方法来获取这些属性: public String getProtocol() 获取该URL的协议名。 public String getHost() 获取该URL的主机名。 public int getPort() 获取该URL的端口号,如果没有设置端口,返回-1。 public String getFile() 获取该URL的文件名。 public String getRef() 获取该URL在文件中的相对位置。 public String getQuery() 获取该URL的查询信息。 public String getPath() 获取该URL的路径 public String getAuthority() 获取该URL的权限信息 public String getUserInfo() 获得使用者的信息 public String getRef() 获得该URL的锚
8.2.5 从URL读取WWW网络资源
当我们得到一个URL对象后,就可以通过它读取指定的WWW资源。这时我们将使用URL的方法openStream(),其定义为: InputStream openStream(); 方法openSteam()与指定的URL建立连接并返回InputStream类的对象以从这一连接中读取数据。 public class URLReader { public static void main(String[] args) throws Exception { //声明抛出所有例外 URL tirc = new URL("http://www.tirc1.cs.tsinghua.edu.cn/"); //构建一URL对象 BufferedReader in = new BufferedReader(new InputStreamReader(tirc.openStream())); //使用openStream得到一输入流并由此构造一个BufferedReader对象 String inputLine; while ((inputLine = in.readLine()) != null) //从输入流不断的读数据,直到读完为止 System.out.println(inputLine); //把读入的数据打印到屏幕上 in.close(); //关闭输入流 } }
8.2.6 通过URLConnetction连接WWW
通过URL的方法openStream(),我们只能从网络上读取数据,如果我们同时还想输出数据,例如向服务器端的CGI程序发送一些数据,我们必须先与URL建立连接,然后才能对其进行读写,这时就要用到类URLConnection了。CGI是公共网关接口(Common Gateway Interface)的简称,它是用户浏览器和服务器端的应用程序进行连接的接口,有关CGI程序设计,请读者参考有关书籍。 类URLConnection也在包java.net中定义,它表示Java程序和URL在网络上的通信连接。当与一个URL建立连接时,首先要在一个URL对象上通过方法openConnection()生成对应的URLConnection对象。例如下面的程序段首先生成一个指向地址http://edu.chinaren.com/index.shtml的对象,然后用openConnection()打开该URL对象上的一个连接,返回一个URLConnection对象。如果连接过程失败,将产生IOException. Try{ URL netchinaren = new URL ("http://edu.chinaren.com/index.shtml"); URLConnectonn tc = netchinaren.openConnection(); }catch(MalformedURLException e){ //创建URL()对象失败 … }catch (IOException e){ //openConnection()失败 … } 类URLConnection提供了很多方法来设置或获取连接参数,程序设计时最常使用的是getInputStream()和getOurputStream(),其定义为: InputSteram getInputSteram(); OutputSteram getOutputStream(); 通过返回的输入/输出流我们可以与远程对象进行通信。看下面的例子: URL url =new URL ("http://www.javasoft.com/cgi-bin/backwards"); //创建一URL对象 URLConnectin con=url.openConnection(); //由URL对象获取URLConnection对象 DataInputStream dis=new DataInputStream (con.getInputSteam()); //由URLConnection获取输入流,并构造DataInputStream对象 PrintStream ps=new PrintSteam(con.getOutupSteam()); //由URLConnection获取输出流,并构造PrintStream对象 String line=dis.readLine(); //从服务器读入一行 ps.println("client…"); //向服务器写出字符串 "client…" 其中backwards为服务器端的CGI程序。实际上,类URL的方法openSteam()是通过URLConnection来实现的。它等价于 openConnection().getInputStream(); 基于URL的网络编程在底层其实还是基于下面要讲的Socket接口的。WWW,FTP等标准化的网络服务都是基于TCP协议的,所以本质上讲URL编程也是基于TCP的一种应用.
8.3 基于Socket的低层次Java网络编程
8.3.1 Socket通讯
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。 在传统的UNIX环境下可以操作TCP/IP协议的接口不止Socket一个,Socket所支持的协议种类也不光TCP/IP一种,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。
8.3.2 Socket通讯的一般过程
使用Socket进行Client/Server程序设计的一般连接过程是这样的:Server端Listen(监听)某个端口是否有连接请求,Client端向Server端发出Connect(连接)请求,Server端向Client端发回Accept(接受)消息。一个连接就建立起来了。Server端和Client端都可以通过Send,Write等方法与对方通信。
对于一个功能齐全的Socket,都要包含以下基本结构,其工作过程包含以下四个基本的步骤: (1) 创建Socket; (2) 打开连接到Socket的输入/出流; (3) 按照一定的协议对Socket进行读/写操作; (4) 关闭Socket.
8.3.3 创建Socket
java在包java.net中提供了两个类Socket和ServerSocket,分别用来表示双向连接的客户端和服务端。这是两个封装得非常好的类,使用很方便。其构造方法如下: Socket(InetAddress address, int port); Socket(InetAddress address, int port, boolean stream); Socket(String host, int prot); Socket(String host, int prot, boolean stream); Socket(SocketImpl impl) Socket(String host, int port, InetAddress localAddr, int localPort) Socket(InetAddress address, int port, InetAddress localAddr, int localPort) ServerSocket(int port); ServerSocket(int port, int backlog); ServerSocket(int port, int backlog, InetAddress bindAddr) 其中address、host和port分别是双向连接中另一方的IP地址、主机名和端口号,stream指明socket是流socket还是数据报socket,localPort表示本地主机的端口号,localAddr和bindAddr是本地机器的地址(ServerSocket的主机地址),impl是socket的父类,既可以用来创建serverSocket又可以用来创建Socket。count则表示服务端所能支持的最大连接数。例如: Socket client = new Socket("127.0.01.", 80); ServerSocket server = new ServerSocket(80); 注意,在选择端口时,必须小心。每一个端口提供一种特定的服务,只有给出正确的端口,才能获得相应的服务。0~1023的端口号为系统所保留,例如http服务的端口号为80,telnet服务的端口号为21,ftp服务的端口号为23, 所以我们在选择端口号时,最好选择一个大于1023的数以防止发生冲突。 在创建socket时如果发生错误,将产生IOException,在程序中必须对之作出处理。所以在创建Socket或ServerSocket是必须捕获或抛出例外。
8.3.8 简单的Client/Server程序设计
下面我们给出一个用Socket实现的客户和服务器交互的典型的C/S结构的演示程序,读者通过仔细阅读该程序,会对前面所讨论的各个概念有更深刻的认识。程序的意义请参考注释。
1. 客户端程序 import java.io.*; import java.net.*; public class TalkClient { public static void main(String args[]) { try{ Socket socket=new Socket("127.0.0.1",4700); //向本机的4700端口发出客户请求 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //由系统标准输入设备构造BufferedReader对象 PrintWriter os=new PrintWriter(socket.getOutputStream()); //由Socket对象得到输出流,并构造PrintWriter对象 BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); //由Socket对象得到输入流,并构造相应的BufferedReader对象 String readline; readline=sin.readLine(); //从系统标准输入读入一字符串 while(!readline.equals("bye")){ //若从标准输入读入的字符串为 "bye"则停止循环 os.println(readline); //将从系统标准输入读入的字符串输出到Server os.flush(); //刷新输出流,使Server马上收到该字符串 System.out.println("Client:"+readline); //在系统标准输出上打印读入的字符串 System.out.println("Server:"+is.readLine()); //从Server读入一字符串,并打印到标准输出上 readline=sin.readLine(); //从系统标准输入读入一字符串 } //继续循环 os.close(); //关闭Socket输出流 is.close(); //关闭Socket输入流 socket.close(); //关闭Socket }catch(Exception e) { System.out.println("Error"+e); //出错,则打印出错信息 } } }
2. 服务器端程序 import java.io.*; import java.net.*; import java.applet.Applet; public class TalkServer{ public static void main(String args[]) { try{ ServerSocket server=null; try{ server=new ServerSocket(4700); //创建一个ServerSocket在端口4700监听客户请求 }catch(Exception e) { System.out.println("can not listen to:"+e); //出错,打印出错信息 }
Socket socket=null; try{ socket=server.accept(); //使用accept()阻塞等待客户请求,有客户 //请求到来则产生一个Socket对象,并继续执行 }catch(Exception e) { System.out.println("Error."+e); //出错,打印出错信息 } String line; BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream())); //由Socket对象得到输入流,并构造相应的BufferedReader对象 PrintWriter os=newPrintWriter(socket.getOutputStream()); //由Socket对象得到输出流,并构造PrintWriter对象 BufferedReader sin=new BufferedReader(new InputStreamReader(System.in)); //由系统标准输入设备构造BufferedReader对象
System.out.println("Client:"+is.readLine()); //在标准输出上打印从客户端读入的字符串 line=sin.readLine(); //从标准输入读入一字符串 while(!line.equals("bye")){ //如果该字符串为 "bye",则停止循环 os.println(line); //向客户端输出该字符串 os.flush(); //刷新输出流,使Client马上收到该字符串 System.out.println("Server:"+line); //在系统标准输出上打印读入的字符串 System.out.println("Client:"+is.readLine()); //从Client读入一字符串,并打印到标准输出上 line=sin.readLine(); //从系统标准输入读入一字符串 } //继续循环 os.close(); //关闭Socket输出流 is.close(); //关闭Socket输入流 socket.close(); //关闭Socket server.close(); //关闭ServerSocket }catch(Exception e){ System.out.println("Error:"+e); //出错,打印出错信息 } } }
8.3.9 支持多客户的client/server程序设计
前面提供的Client/Server程序只能实现Server和一个客户的对话。在实际应用中,往往是在服务器上运行一个永久的程序,它可以接收来自其他多个客户端的请求,提供相应的服务。为了实现在服务器方给多个客户提供服务的功能,需要对上面的程序进行改造,利用多线程实现多客户机制。服务器总是在指定的端口上监听是否有客户请求,一旦监听到客户请求,服务器就会启动一个专门的服务线程来响应该客户的请求,而服务器本身在启动完线程之后马上又进入监听状态,等待下一个客户的到来。
ServerSocket serverSocket=null; boolean listening=true; try{ serverSocket=new ServerSocket(4700); //创建一个ServerSocket在端口4700监听客户请求 }catch(IOException e) { } while(listening){ //永远循环监听 new ServerThread(serverSocket.accept(),clientnum).start(); //监听到客户请求,根据得到的Socket对象和 客户计数创建服务线程,并启动之 clientnum++; //增加客户计数 } serverSocket.close(); //关闭ServerSocket
设计ServerThread类
public class ServerThread extends Thread{ Socket socket=null; //保存与本线程相关的Socket对象 int clientnum; //保存本进程的客户计数 public ServerThread(Socket socket,int num) { //构造函数 this.socket=socket; //初始化socket变量 clientnum=num+1; //初始化clientnum变量 } public void run() { //线程主体 try{//在这里实现数据的接受和发送}
8.3.10 据报Datagram通讯
前面在介绍TCP/IP协议的时候,我们已经提到,在TCP/IP协议的传输层除了TCP协议之外还有一个UDP协议,相比而言UDP的应用不如TCP广泛,几个标准的应用层协议HTTP,FTP,SMTP…使用的都是TCP协议。但是,随着计算机网络的发展,UDP协议正越来越来显示出其威力,尤其是在需要很强的实时交互性的场合,如网络游戏,视频会议等,UDP更是显示出极强的威力,下面我们就介绍一下Java环境下如何实现UDP网络传输。
8.3.11 什么是Datagram
所谓数据报(Datagram)就跟日常生活中的邮件系统一样,是不能保证可靠的寄到的,而面向链接的TCP就好比电话,双方能肯定对方接受到了信息。在本章前面,我们已经对UDP和TCP进行了比较,在这里再稍作小节: TCP,可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大。 UDP,不可靠,差错控制开销较小,传输大小限制在64K以下,不需要建立连接。
8.3.12 Datagram通讯的表示方法:DatagramSocket;DatagramPacket
包java.net中提供了两个类DatagramSocket和DatagramPacket用来支持数据报通信,DatagramSocket用于在程序之间建立传送数据报的通信连接, DatagramPacket则用来表示一个数据报。先来看一下DatagramSocket的构造方法: DatagramSocket(); DatagramSocket(int prot); DatagramSocket(int port, InetAddress laddr) 其中,port指明socket所使用的端口号,如果未指明端口号,则把socket连接到本地主机上一个可用的端口。laddr指明一个可用的本地地址。给出端口号时要保证不发生端口冲突,否则会生成SocketException类例外。注意:上述的两个构造方法都声明抛弃非运行时例外SocketException,程序中必须进行处理,或者捕获、或者声明抛弃。 用数据报方式编写client/server程序时,无论在客户方还是服务方,首先都要建立一个DatagramSocket对象,用来接收或发送数据报,然后使用DatagramPacket类对象作为传输数据的载体。下面看一下DatagramPacket的构造方法 : DatagramPacket(byte buf[],int length); DatagramPacket(byte buf[], int length, InetAddress addr, int port); DatagramPacket(byte[] buf, int offset, int length); DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);
其中,buf中存放数据报数据,length为数据报中数据的长度,addr和port旨明目的地址,offset指明了数据报的位移量。 在接收数据前,应该采用上面的第一种方法生成一个DatagramPacket对象,给出接收数据的缓冲区及其长度。然后调用DatagramSocket 的方法receive()等待数据报的到来,receive()将一直等待,直到收到一个数据报为止。 DatagramPacket packet=new DatagramPacket(buf, 256); Socket.receive (packet); 发送数据前,也要先生成一个新的DatagramPacket对象,这时要使用上面的第二种构造方法,在给出存放发送数据的缓冲区的同时,还要给出完整的目的地址,包括IP地址和端口号。发送数据是通过DatagramSocket的方法send()实现的,send()根据数据报的目的地址来寻径,以传递数据报。 DatagramPacket packet=new DatagramPacket(buf, length, address, port); Socket.send(packet); 在构造数据报时,要给出InetAddress类参数。类InetAddress在包java.net中定义,用来表示一个Internet地址,我们可以通过它提供的类方法getByName()从一个表示主机名的字符串获取该主机的IP地址,然后再获取相应的地址信息。
8.3.14 用数据报进行广播通讯
DatagramSocket只允许数据报发送一个目的地址,java.net包中提供了一个类MulticastSocket,允许数据报以广播方式发送到该端口的所有客户。MulticastSocket用在客户端,监听服务器广播来的数据。
1. 客户方程序:MulticastClient.java import java.io.*; import java.net.*; import java.util.*; public class MulticastClient { public static void main(String args[]) throws IOException { MulticastSocket socket=new MulticastSocket(4446); //创建4446端口的广播套接字 InetAddress address=InetAddress.getByName("230.0.0.1"); //得到230.0.0.1的地址信息 socket.joinGroup(address); //使用joinGroup()将广播套接字绑定到地址上 DatagramPacket packet;
for(int i=0;i<5;i++) { byte[] buf=new byte[256]; //创建缓冲区 packet=new DatagramPacket(buf,buf.length); //创建接收数据报 socket.receive(packet); //接收 String received=new String(packet.getData()); //由接收到的数据报得到字节数组, //并由此构造一个String对象 System.out.println("Quote of theMoment:"+received); //打印得到的字符串 } //循环5次 socket.leaveGroup(address); //把广播套接字从地址上解除绑定 socket.close(); //关闭广播套接字 } }
2. 服务器方程序:MulticastServer.java public class MulticastServer{ public static void main(String args[]) throws java.io.IOException { new MulticastServerThread().start(); //启动一个服务器线程 } }
3. 程序MulticastServerThread.java import java.io.*; import java.net.*; import java.util.*; public class MulticastServerThread extends QuoteServerThread //从QuoteServerThread继承得到新的服务器线程类MulticastServerThread { Private long FIVE_SECOND=5000; //定义常量,5秒钟 public MulticastServerThread(String name) throws IOException { super("MulticastServerThread"); //调用父类,也就是QuoteServerThread的构造函数 }
public void run() //重写父类的线程主体 { while(moreQuotes) { //根据标志变量判断是否继续循环 try{ byte[] buf=new byte[256]; //创建缓冲区 String dString=null; if(in==null) dString=new Date().toString(); //如果初始化的时候打开文件失败了, //则使用日期作为要传送的字符串 else dString=getNextQuote(); //否则调用成员函数从文件中读出字符串 buf=dString.getByte(); //把String转换成字节数组,以便传送send it InetAddress group=InetAddress.getByName("230.0.0.1"); //得到230.0.0.1的地址信息 DatagramPacket packet=new DatagramPacket(buf,buf.length,group,4446); //根据缓冲区,广播地址,和端口号创建DatagramPacket对象 socket.send(packet); //发送该Packet try{ sleep((long)(Math.random()*FIVE_SECONDS)); //随机等待一段时间,0~5秒之间 }catch(InterruptedException e) { } //异常处理 }catch(IOException e){ //异常处理 e.printStackTrace( ); //打印错误栈
moreQuotes=false; //置结束循环标志 } } socket.close( ); //关闭广播套接口 } }
【本讲小结】
本讲主要讲解了Java环境下的网络编程。因为TCP/IP协议是Java网络编程的基础知识,本讲开篇重点介绍了TCP/IP协议中的一些概念,TCP/IP协议本身是一个十分庞大的系统,用几个小节是不可能讲清楚的。所以我们只是联系实际,讲解了一些最基本的概念,帮助学生理解后面的相关内容。重点有一下几个概念:主机名,IP,端口,服务类型,TCP,UDP。 后续的内容分为两大块,一块是以URL为主线,讲解如何通过URL类和URLConnection类访问WWW网络资源,由于使用URL十分方便直观,尽管功能不是很强,还是值得推荐的一种网络编程方法,尤其是对于初学者特别容易接受。本质上讲,URL网络编程在传输层使用的还是TCP协议。 另一块是以Socket接口和C/S网络编程模型为主线,依次讲解了如何用Java实现基于TCP的C/S结构,主要用到的类有Socket,ServerSocket。以及如何用Java实现基于UDP的C/S结构,还讨论了一种特殊的传输方式,广播方式,这种方式是UDP所特有的,主要用到的类有DatagramSocket , DatagramPacket, MulticastSocket。这一块在Java网络编程中相对而言是最难的(尽管Java在网络编程这方面已经做的够"傻瓜"了,但是网络编程在其他环境下的却是一件极为头痛的事情,再"傻瓜"还是有一定的难度),也是功能最为强大的一部分,读者应该好好研究,领悟其中的思想。 最后要强调的是要学好Java网络编程,Java语言,最重要的还是在于多多练习!
java中的事件机制
Java做的图形界面软件通过事件响应机制实现用户与程序的交互,原理大概是这样:
首先,在java控件对象(比如文本框)上添加一个监控对象,方法是one.addXXXListenner(two),这就相当于你要对某人进行监听,先要在他身上绑定一个窃听器一样,这里“one”就是你要监听的那个家伙,two就是你自己造的一个窃听器。
第二步就是要考虑怎样造这个窃听器了,我们首先要搞清楚它要实现的功能:它不仅能够监听one的一举一动,还能够把监听到的事件告诉系统,并让系统对这个事件做出相应的处理。Java中是通过接口实现这样的功能的。这些接口请参见jdk中java.awt.event包,里面那几个XXXListener就是(不是很多,常用的更少)。在这些接口中定义了一些方法,这些方法就是相应的事件处理方式,它们只由系统调用并且都有一个事件类的对象作为参数,这个参数正是联系发生事件主体one和操作系统的纽带。当然接口是虚的,不能产生对象的;所以想必你也猜到,上面的“窃听器”two的类型肯定是某个实现了XXXListener接口的类。
好了,现在在回顾一下这个过程:
(1)用户通过鼠标、键盘等在one对象上做动作(比如点击鼠标),
(2)这个动作被two监听到并产生事件对象e(即XXXEvent的对象),
(3)two通过事件e对象向系统打报告,
(4)系统调用two中实现的方法对事件进行处理,并向方法传送事件e。
如果你清楚了这个过程,再来细看一下这个XXXListener接口。我们必须用类来实现接口的方法(这是java基础知识吧◎),再用这个类产生two这样的对象。类实现接口必须实现接口中的每个方法,而实际上我们需要的也许只是该接口中的某一个方法(比如处理鼠标右键点击的那个),那么每个方法还得去实现一个空的,真是浪费。Sun为了让程序员省点事,在JDK中已经为我们把这些接口实现好了,这些类就是我们所谓的“适配器”(XXXAdapter),我们只需要继承(extends)这些类并重写里面我们需要的方法就OK了,所以,其实适配器就是类,只不过这些类中只有一些空的方法,别无所有。
到此,你大概明白程序员在事件处理过程中该做哪些事了吧:
(1)制造“窃听器”,即:实现事件监听接口,重写相关方法,
(2)安装窃听器,即:为监控对象添加监听器。
下面,我们通过程序再来仔细看看事件响应的实现方式(待续):
(以下内容参考陈刚《eclipse从入门到精通》)
(1)、 匿名内部类写法
例程:
text.addMouseListener(new MouseAdapter() {
public void mouseDoubleClick(MouseEvent e) {//鼠标双击事件的方法
//打开一个信息框
MessageDialog.openInformation (null,"","Hello World");
}
});
new MouseAdapter()就是一个匿名内部类。其实就是在实现类的同时用new构造一个该类的对象,并把它作为addMouseListener的参数,它的效果和下面的一样,只不过代码比较集中。
(2)、命名内部类写法
public class HelloWorld {
public static void main(String[] args) {
……
Text text = new Text(shell, SWT.BORDER);
//加入鼠标事件监听器,并用下面代码所定义的内部类生成一个对象
text.addMouseListener(new MyMouseDoubleClick());
……
}
//定义一个名为MyMouseDoubleClick的内部类
private static final class MyMouseDoubleClick extends MouseAdapter {
public void mouseDoubleClick(MouseEvent e) {
MessageDialog.openInformation(null, "", "Hello World");
}
}
}
(3)、外部类写法
这种写法和命名内部类有些相似,只不过是将MyMouseDoubleClick类从HelloWorld.java中拿出去,单独写成一个类文件。实现代码如下
//文件1: HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
……
Text text = new Text(shell, SWT.BORDER);
//加入鼠标事件监听器,并用下面代码所定义的内部类生成一个对象
text.addMouseListener(new MyMouseDoubleClick());
……
}
}
//文件2:MyMouseDoubleClick.java
public class MyMouseDoubleClick extends MouseAdapter {
public void mouseDoubleClick(MouseEvent e) {
MessageDialog.openInformation(null, "", "Hello World");
}
}
(4)、实现监听接口的写法
将HelloWorld类实现MouseListener接口,这样类本身就成了一个监听器,使得加入监听器的代码可以更简洁。这种方式适合加入监听器的组件较多,且要求监听器的事件处理代码可以被组件共用。这种方式还有一个要注意的地方:事件方法和其他方法混合在了一起,容易引起误读,所以应该在事件方法前加入详细的注释说明。
实现MouseListener接口要写的事件方法多一些,当然没用的事件方法可以空实现。如果继承MouseListener接口的适配器MouseAdapter,则只写需要的方法就行了。另外要注意:只有接口才能有多继承的特性,所以如果HelloWorld已经是某个类的子类,就只能用实现接口的方式,而不能继承接口的适配器了。
给出示例代码如下:
public class HelloWorld extends MouseAdapter{//或implements MouseListener
public static void main(String[] args) {
……
Text text1 = new Text(shell, SWT.BORDER);
Text text2 = new Text(shell, SWT.BORDER);
text1.addMouseListener(this);
text2.addMouseListener(this);
……
}
public void mouseDoubleClick(MouseEvent e) {
MessageDialog.openInformation(null, "", "Hello World");
}
}
java中的事件机制的参与者有3种角色:
1.event object:就是事件产生时具体的“事件”,用于listener的相应的方法之中,作为参数,一般存在与listerner的方法之中
2.event source:具体的接受事件的实体,比如说,你点击一个button,那么button就是event source,这样你必须使button对某些事件进行相应,你就需要注册特定的listener,比如说MouseEvent之中的MouseClicked方法,这是他就必须有了add方法
3.event listener:具体的对监听的事件类,当有其对应的event object产生的时候,它就调用相应的方法,进行处理。在windows程序设计里边这种相应使用callback机制来实现的
-
先看看jdk提供的event包:
-
public interface EventListener:所有事件侦听器接口必须扩展的标记接口。
-
public class EventObject extends Object implements Serializable
所有事件状态对象都将从其派生的根类。 所有 Event 在构造时都引用了对象 "source",在逻辑上认为该对象是最初发生有关 Event 的对象。
在Java2处理事件时,没有采用dispatchEvent()-postEvent()-handleEvent()方式,采用了监听器类,每个事件类都有相关联的监听器接口。事件从事件源到监听者的传递是通过对目标监听者对象的Java方法调用进行的。
对每个明确的事件的发生,都相应地定义一个明确的Java方法。这些方法都集中定义在事件监听者(EventListener)接口中,这个接口要继承 java.util.EventListener。 实现了事件监听者接口中一些或全部方法的类就是事件监听者。
伴随着事件的发生,相应的状态通常都封装在事件状态对象中,该对象必须继承自java.util.EventObject。事件状态对象作为单参传递给应响应该事件的监听者方法中。发出某种特定事件的事件源的标识是:遵从规定的设计格式为事件监听者定义注册方法,并接受对指定事件监听者接口实例的引用。
开始之前首先问个问题:您熟悉java.util.EventObject 和java.util.EventListener两个类以及他们已有的子类吗?
如果你已经能够熟练使用jdk为我们提供的事件监听器,并且很熟悉MouseEvent, KeyEvent, WindowEvent等等这些jdk为我们准备好的事件,那么想必你对java的事件机制已经有所理解。但是也许你还是觉得虽然用起来没什么问题,但是原理还是有些糊涂,那么下面我们再进一步自己实现这些事件和监听器,我们把这个取名为自定义事件。
其实自定义事件在java中很有用处,我们有的时候想让自己的程序产生一个事件,但有不希望(或者不可能)用鼠标,键盘之类的输入设备进行操作,比如你写一个应用程序,在这个程序中一旦收到邮件就对邮件进行相关处理,对于“收到邮件”这个事件,jdk中就没有定义。对于这样的事件,以及对于这样的事件的监听器,我们只能自己动手完成了。
那么下面就以实例开始我们这个“创新”的过程:首先,我们要明确jdk中需要的资源:类EventObject作为父类用来生成我们自己的事件类,接口EventListener用来实现我们自己的监听器;剩下的事情就是如何注册这些事件以及测试他们了。
(1) 通过DemoEvent.java文件创建DemoEvent类,这个类继承EventObject。这个类的构造函数的参数传递了产生这个事件的事件源(比如各种控件),方法getSource用来获得这个事件源的引用。
DemoEvent.java
package demo.listener;
import java.util.EventObject;
public class DemoEvent extends EventObject
{
Object obj;
public DemoEvent(Object source)
{
super(source);
obj = source;
}
public Object getSource()
{
return obj;
}
public void say()
{
System.out.println("This is say method...");
}
}
|
(2) 定义新的事件监听接口,该接口继承自EventListener;该接口包含对DemeEvent事件的处理程序:
DemoListener.java
package demo.listener;
import java.util.EventListener;
public interface DemoListener extends EventListener
{
public void demoEvent(DemoEvent dm);
}
|
通过上面的接口我们再定义事件监听类,这些类具体实现了监听功能和事件处理功能。回想一下上文中那四种实现方式,我们这里不正是使用了其中的第三种——外部类写法的方式吗?
Listener1.java
package demo.listener;
public class Listener1 implements DemoListener
{
public void demoEvent(DemoEvent de)
{
System.out.println("Inside listener1...");
}
}
|
Listener2.java
package demo.listener;
public class Listener2 implements DemoListener
{
public void demoEvent(DemoEvent de)
{
System.out.println("Inside listener2...");
}
}
|
Listener3.java
package demo.listener;
public class Listener3 implements DemoListener
{
public void demoEvent(DemoEvent de)
{
System.out.println("Inside listener3...");
}
}
|
(3) 通过DemeSource..ava文件创造一个事件源类,它用一个java.utile.Vector对象来存储所有的事件监听器对象,存储方式是通过addListener(..)这样的方法。notifyDemeEvent(..)是触发事件的方法,用来通知系统:事件发生了,你调用相应的处理函数(回调函数)吧。
DemoSource.java
package demo.listener;
import java.util.*;
public class DemoSource
{
private Vector repository = new Vector();
DemoListener dl;
public DemoSource()
{
}
public void addDemoListener(DemoListener dl)
{
repository.addElement(dl);
}
public void notifyDemoEvent()
{
Enumeration enum = repository.elements();
while(enum.hasMoreElements())
{
dl = (DemoListener)enum.nextElement();
dl.demoEvent(new DemoEvent(this));
}
}
}
|
(4) 好了,最后写一个测试程序测试一下我们自定义的事件吧,这段程序应该不难理解吧:)
TestDemo.java
package demo.listener;
public class TestDemo
{
DemoSource ds;
public TestDemo()
{
try{
ds = new DemoSource();
Listener1 l1 = new Listener1();
Listener2 l2 = new Listener2();
Listener3 l3 = new Listener3();
ds.addDemoListener(l1);
ds.addDemoListener(l2);
ds.addDemoListener(l3);
ds.addDemoListener(new DemoListener(){ public void demoEvent(DemoEvent event){ System.out.println("Method come from 匿名类..."); } });
ds.notifyDemoEvent();
}catch(Exception ex)
{ex.printStackTrace();}
}
public static void main(String args[])
{
new TestDemo();
}
}
|
1. import java.net.InetAddress; import java.net.UnknownHostException;
public class NetWork {
//static fields and static block //InetAddress of the local host private static InetAddress local = null; //integer version of the local host private static int packedLocal = 0; static { try{ local = InetAddress.getLocalHost(); }catch(UnknownHostException e){ e.printStackTrace(); }
System.out.println("localhost : " + local); packedLocal = translate(local); } public static InetAddress getLocalHost(){ return local; } public static String getLocalHostName(){ return local.getHostName(); } public static String getLocalMachineName(){ String hostname = local.getHostName(); int machEndIndex = hostname.indexOf('.'); if(machEndIndex == -1) return hostname; else return hostname.substring(0, machEndIndex); } public static boolean isLocal(InetAddress address){ return local.equals(address); } public static boolean isLocal(int packedAddress){ return packedAddress == packedLocal; } /** * return an integer representation of the specified * InetAddress. This is used for efficiency */ public static int translate(InetAddress address){ byte[] ad = address.getAddress();//原始IP地址 if(ad.length != 4) throw new IllegalArgumentException("only accept 32-byte IP address"); int packedAddress = ((ad[0] << 24) & 0xFF000000) | ((ad[1] << 16) & 0xFF0000) | ((ad[2] << 8) & 0xFF00) | (ad[3] & 0xFF); return packedAddress; } /** * return an InetAddress representation of the specified integer. * This is used for performance improvements in serivalization. */ public static InetAddress translate (int packed) throws UnknownHostException{ int[] ad = new int[4]; ad[0] = (packed >>> 24); ad[1] = ((packed >>> 16) & 0x000000FF); ad[2] = ((packed >>> 8) & 0x000000FF); ad[3] = (packed & 0x000000FF); StringBuffer buf = new StringBuffer(); buf.append(ad[0]); buf.append("."); buf.append(ad[1]); buf.append("."); buf.append(ad[2]); buf.append("."); buf.append(ad[3]); return InetAddress.getByName(buf.toString()); } public static String translateToString(int packed){ int[] ad = new int[4]; ad[0] = (packed >>> 24); ad[1] = ((packed >>> 16) & 0x000000FF); ad[2] = ((packed >>> 8) & 0x000000FF); ad[3] = (packed & 0x000000FF); StringBuffer buf = new StringBuffer(); buf.append(ad[0]); buf.append("."); buf.append(ad[1]); buf.append("."); buf.append(ad[2]); buf.append("."); buf.append(ad[3]); return buf.toString(); } public static void main(String[] args){ InetAddress localhost = getLocalHost(); System.out.println("Local Host Name is : " + getLocalHostName()); System.out.println("Local Host Machine Name is : " + getLocalMachineName()); if(isLocal(localhost)) System.out.println(localhost.getHostName() + " is Local Host."); int intforaddress = translate(localhost); System.out.println("localhost integer representation is " + intforaddress); System.out.println("localhost string representation is " + translateToString(intforaddress)); System.out.println(translateToString(intforaddress).toString()); } }
2. public interfece Externalizable extendx Serializable Externalizable 实例类的惟一特性是可以被写入序列化流中,该类负责保存和恢复实例内容。 若某个要完全控制某一对象及其超类型的流格式和内容,则它要实现 Externalizable 接口的 writeExternal 和 readExternal 方法。这些方法必须显式与超类型进行协调以保存其状态。这些方法将代替自定义的 writeObject 和 readObject 方法实现。 Serialization 对象将使用 Serializable 和 Externalizable 接口。对象持久性机制也可以使用它们。要存储的每个对象都需要检测是否支持 Externalizable 接口。如果对象支持 Externalizable,则调用 writeExternal 方法。如果对象不支持 Externalizable 但实现了 Serializable,则使用 ObjectOutputStream 保存该对象。 在重构 Externalizable 对象时,先使用无参数的公共构造方法创建一个实例,然后调用 readExternal 方法。通过从 ObjectInputStream 中读取 Serializable 对象可以恢复这些对象。 Externalizable 实例可以通过 Serializable 接口中记录的 writeReplace 和 readResolve 方法来指派一个替代对象。
writeExternal
void writeExternal(ObjectOutput out)
throws IOException
- 该对象可实现 writeExternal 方法来保存其内容,它可以通过调用 DataOutput 的方法来保存其基本值,或调用 ObjectOutput 的 writeObject 方法来保存对象、字符串和数组。
-
-
- 参数:
out - 要写入对象的流
- 抛出:
IOException - 包含可能发生的所有 I/O 异常
readExternal
void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
- 对象实现 readExternal 方法来恢复其内容,它通过调用 DataInput 的方法来恢复其基础类型,调用 readObject 来恢复对象、字符串和数组。readExternal 方法必须按照与 writeExternal 方法写入值时使用的相同顺序和类型来读取这些值。
-
-
- 参数:
in - 为了恢复对象而从中读取数据的流
- 抛出:
IOException - 如果发生 I/O 错误
ClassNotFoundException - 如果无法找到需要恢复的某个对象的类。
import java.io.Externalizable; import java.net.InetAddress;
public interface EndPoint extends Externalizable {
/** * * @return Returns the IP address contained in this endpoint. */ public InetAddress getAddress(); /** * returns the IP address contained in this endpoint. * enclosed in an integer value. */ public int getIntAddress(); /** * Returns the port number contained in this endpoint. */ public int getPort(); /** * Return true if this endpoint is the local endpoint. */ public boolean isLocal(); /** * Returns true if this endpoint is a multicast endpoint. */ public boolean isMulticastEndPoint(); }
import internet.network.NetWork;
import java.io.Externalizable; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectInputStream; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.net.InetAddress; import java.net.UnknownHostException;
public class EndPointImpl implements EndPoint, Comparable, Externalizable{
//Constants /* Size of this object (in bytes) when marshalled. */ public static final int SIZE = 6; //Fields protected InetAddress address; private int packedAddress; private int port; protected boolean local; //Constructors /** * Default constructor for externalization */ public EndPointImpl(){ } public EndPointImpl(InetAddress address, int port){ this.address = address; this.port = port; packedAddress = NetWork.translate(address); local = NetWork.isLocal(packedAddress); } public EndPointImpl(int packedAddress, int port){ this.packedAddress = packedAddress; this.port = port; this.local = NetWork.isLocal(packedAddress); //The InetAddress version of the address is computed //only when needed, to avoid uselesss DNS lookups. address = null; } public InetAddress getAddress() { if(address == null) try{ address = NetWork.translate(packedAddress); }catch(UnknownHostException e){ System.out.println(e.getMessage()); } return address; }
public int getIntAddress() { return this.packedAddress; }
public int getPort() { return this.port; }
public boolean isLocal() { return this.local; }
public boolean isMulticastEndPoint() { return address.isMulticastAddress(); } //////////////////////////////////////////////////////////// // Methods from Externalizable ////////////////////////////////////////////////////////////
/** * Restores the content of this object form the marshaled data contained * in the specified input stream. * * @param in the stream to be read. */ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { packedAddress = in.readInt(); port = (int) in.readUnsignedShort(); local = NetWork.isLocal(packedAddress); address = null; }
/** * Marshals the content of this object to the specified output stream. * * @param out the stream to be written */ public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(packedAddress); out.writeShort(port); }
//////////////////////////////////////////////////////////// // Methods from Object and Comparable //////////////////////////////////////////////////////////// /** * Compares this object with the specified object for order. */ public int compareTo(Object obj) { if(this == obj){ return 0; } else{ EndPointImpl endpoint = (EndPointImpl) obj; if(packedAddress < endpoint.packedAddress){ return -1; } else if(packedAddress == endpoint.packedAddress){ if(port < endpoint.port) return -1; else if(port == endpoint.port) return 0; else return 1; } else{ return 1; } } } public int hashcode(){ return packedAddress ^ port; }
/** * Compares two EndPointImpl objects for content equality. */ public boolean equals (Object obj){ if(!(obj instanceof EndPointImpl)) return false; EndPointImpl endpoint = (EndPointImpl) obj; return (endpoint.packedAddress == packedAddress && endpoint.port == port); } /** * Returns a string representation of this object. */ public String toString(){ StringBuffer buf = new StringBuffer(); buf.append("["); buf.append(NetWork.translateToString(packedAddress)); buf.append(":"); buf.append(port); buf.append("]"); return buf.toString(); } public static void main(String[] args) throws FileNotFoundException, IOException{ InetAddress localhost = NetWork.getLocalHost(); EndPointImpl endpoint = new EndPointImpl(localhost, 100); System.out.println("This EndPoint is : " + endpoint.toString()); EndPointImpl endpoint2 = new EndPointImpl(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("endpoint.txt")); try { endpoint.writeExternal(oos); } catch (IOException e) { e.printStackTrace(); } oos.close(); ObjectInputStream ios = new ObjectInputStream(new FileInputStream("endpoint.txt")); try { endpoint2.readExternal(ios); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } ios.close(); System.out.println("This EndPoint is : " + endpoint2.toString()); } }
类Deflater 和 类Inflater:此类使用流行的 ZLIB 压缩程序库为通用压缩提供支持。ZLIB 压缩程序库最初是作为 PNG 图形标准的一部分开发的,不受专利的保护。
//encoding a string into bytes String inputString = "chenweicai"; byte[] input = inputString.getBytes("UTF-8"); System.out.println("before compressed, the bytes length is :" + input.length); //compress the bytes byte[] output = new byte[100]; Deflater compresser = new Deflater(); compresser.setInput(input); compresser.finish(); int compressedDataLength = compresser.deflate(output); System.out.println("compressed bytes length is :" + compressedDataLength); //decompress the bytes Inflater decompresser = new Inflater(); decompresser.setInput(output, 0, compressedDataLength); byte[] result = new byte[100]; int resultLength = decompresser.inflate(result); decompresser.end(); System.out.println("after decompressed, the bytes length is :" + resultLength); //decode the bytes into a string String outputString = new String(result, 0, resultLength, "UTF-8"); System.out.println("after compress and decompress the string is :" + outputString);
2.
import java.io.Serializable;
public class Employee implements Serializable {
private String name; private double salary;
public Employee(String name, double salary) { super(); // TODO Auto-generated constructor stub this.name = name; this.salary = salary; } public void raiseSalary(double byPercent){ double temp = salary * byPercent / 100; salary += temp; }
public String toString() { // TODO Auto-generated method stub return getClass().getName() + "[ Name = " + name + ", salary = " + salary +"]"; } }
public class Test2 {
protected byte[] deflate(Object object) throws IOException{ ByteArrayOutputStream baos = new ByteArrayOutputStream(); Deflater def = new Deflater (Deflater.BEST_COMPRESSION); DeflaterOutputStream dos = new DeflaterOutputStream(baos, def); ObjectOutputStream out = new ObjectOutputStream(dos); out.writeObject(object); out.close(); dos.finish(); dos.close(); return baos.toByteArray(); } protected Object inflate(byte[] compressedContent) throws IOException{ if(compressedContent == null) return null; try{ ObjectInputStream in = new ObjectInputStream( new InflaterInputStream(new ByteArrayInputStream(compressedContent))); Object object = in.readObject(); in.close(); return object; }catch(Exception e){ throw new IOException(e.toString()); } } public static void main(String[] args) throws IOException{ Employee employee = new Employee("LiLei", 1000); Test2 test2 = new Test2(); byte[] bytes = new byte[1000]; bytes = test2.deflate(employee); System.out.println(bytes); System.out.println(test2.inflate(bytes)); } }
JDK为程序员提供了大量的类库,而为了保持类库的可重用性,可扩展性和灵活性,其中使用到了大量的设计模式,本文将介绍JDK的I/O包中使用到的Decorator模式,并运用此模式,实现一个新的输出流类。 Decorator模式简介 Decorator模式又名包装器(Wrapper),它的主要用途在于给一个对象动态的添加一些额外的职责。与生成子类相比,它更具有灵活性。 有时候,我们需要为一个对象而不是整个类添加一些新的功能,比如,给一个文本区添加一个滚动条的功能。我们可以使用继承机制来实现这一功能,但是这种方法不够灵活,我们无法控制文本区加滚动条的方式和时机。而且当文本区需要添加更多的功能时,比如边框等,需要创建新的类,而当需要组合使用这些功能时无疑将会引起类的爆炸。 我们可以使用一种更为灵活的方法,就是把文本区嵌入到滚动条中。而这个滚动条的类就相当于对文本区的一个 装饰。这个装饰(滚动条)必须与被装饰的组件(文本区)继承自同一个接口,这样,用户就不必关心装饰的实现,因为这对他们来说是透明的。装饰会将用户的请求转发给相应的组件(即调用相关的方法),并可能在转发的前后做一些额外的动作(如添加滚动条)。通过这种方法,我们可以根据组合对文本区嵌套不同的装饰,从而添加任意多的功能。这种动态的对对象添加功能的方法不会引起类的爆炸,也具有了更多的灵活性。 以上的方法就是 Decorator模式,它通过给对象添加装饰来动态的添加新的功能。如下是Decorator模式的UML图: Component为组件和装饰的公共父类,它定义了子类必须实现的方法。 ConcreteComponent是一个具体的组件类,可以通过给它添加装饰来增加新的功能。 Decorator是所有装饰的公共父类,它定义了所有装饰必须实现的方法,同时,它还保存了一个对于Component的引用,以便将用户的请求转发给Component,并可能在转发请求前后执行一些附加的动作。 ConcreteDecoratorA和ConcreteDecoratorB是具体的装饰,可以使用它们来装饰具体的Component。 Java IO包中的Decorator模式 JDK提供的java.io包中使用了Decorator模式来实现对各种输入输出流的封装。以下将以java.io.OutputStream及其子类为例,讨论一下Decorator模式在IO中的使用。 首先来看一段用来创建IO流的代码:
以下是代码片段: try { OutputStream out = new DataOutputStream(new FileOutputStream("test.txt")); } catch (FileNotFoundException e) { e.printStackTrace(); } |
这段代码对于使用过JAVA输入输出流的人来说再熟悉不过了,我们使用DataOutputStream封装了一个FileOutputStream。这是一个典型的Decorator模式的使用,FileOutputStream相当于Component,DataOutputStream就是一个Decorator。将代码改成如下,将会更容易理解:
以下是代码片段: try { OutputStream out = new FileOutputStream("test.txt"); out = new DataOutputStream(out); } catch(FileNotFoundException e) { e.printStatckTrace(); } |
由于FileOutputStream和DataOutputStream有公共的父类OutputStream,因此对对象的装饰对于用户来说几乎是透明的。下面就来看看OutputStream及其子类是如何构成Decorator模式的: OutputStream是一个抽象类,它是所有输出流的公共父类,其源代码如下:
以下是代码片段: public abstract class OutputStream implements Closeable, Flushable { public abstract void write(int b) throws IOException; ... } |
它定义了write(int b)的抽象方法。这相当于Decorator模式中的Component类。 ByteArrayOutputStream,FileOutputStream 和 PipedOutputStream 三个类都直接从OutputStream继承,以ByteArrayOutputStream为例:
以下是代码片段: public class ByteArrayOutputStream extends OutputStream { protected byte buf[]; protected int count; public ByteArrayOutputStream() { this(32); } public ByteArrayOutputStream(int size) { if (size 〈 0) { throw new IllegalArgumentException("Negative initial size: " + size); } buf = new byte[size]; } public synchronized void write(int b) { int newcount = count + 1; if (newcount 〉 buf.length) { byte newbuf[] = new byte[Math.max(buf.length 〈〈 1, newcount)]; System.arraycopy(buf, 0, newbuf, 0, count); buf = newbuf; } buf[count] = (byte)b; count = newcount; } ... } |
它实现了OutputStream中的write(int b)方法,因此我们可以用来创建输出流的对象,并完成特定格式的输出。它相当于Decorator模式中的ConcreteComponent类。 接着来看一下FilterOutputStream,代码如下:
以下是代码片段: public class FilterOutputStream extends OutputStream { protected OutputStream out; public FilterOutputStream(OutputStream out) { this.out = out; } public void write(int b) throws IOException { out.write(b); } ... } |
同样,它也是从OutputStream继承。但是,它的构造函数很特别,需要传递一个OutputStream的引用给它,并且它将保存对此对象的引用。而如果没有具体的OutputStream对象存在,我们将无法创建FilterOutputStream。由于out既可以是指向FilterOutputStream类型的引用,也可以是指向ByteArrayOutputStream等具体输出流类的引用,因此使用多层嵌套的方式,我们可以为ByteArrayOutputStream添加多种装饰。这个FilterOutputStream类相当于Decorator模式中的Decorator类,它的write(int b)方法只是简单的调用了传入的流的write(int b)方法,而没有做更多的处理,因此它本质上没有对流进行装饰,所以继承它的子类必须覆盖此方法,以达到装饰的目的。 BufferedOutputStream 和 DataOutputStream是FilterOutputStream的两个子类,它们相当于Decorator模式中的ConcreteDecorator,并对传入的输出流做了不同的装饰。以BufferedOutputStream类为例:
以下是代码片段: public class BufferedOutputStream extends FilterOutputStream { ... private void flushBuffer() throws IOException { if (count 〉 0) { out.write(buf, 0, count); count = 0; } } public synchronized void write(int b) throws IOException { if (count 〉= buf.length) { flushBuffer(); } buf[count++] = (byte)b; } ... } |
这个类提供了一个缓存机制,等到缓存的容量达到一定的字节数时才写入输出流。首先它继承了FilterOutputStream,并且覆盖了父类的write(int b)方法,在调用输出流写出数据前都会检查缓存是否已满,如果未满,则不写。这样就实现了对输出流对象动态的添加新功能的目的。 下面,将使用Decorator模式,为IO写一个新的输出流。 JDK为程序员提供了大量的类库,而为了保持类库的可重用性,可扩展性和灵活性,其中使用到了大量的设计模式,本文将介绍JDK的I/O包中使用到的Decorator模式,并运用此模式,实现一个新的输出流类。 Decorator模式简介 Decorator模式又名包装器(Wrapper),它的主要用途在于给一个对象动态的添加一些额外的职责。与生成子类相比,它更具有灵活性。 有时候,我们需要为一个对象而不是整个类添加一些新的功能,比如,给一个文本区添加一个滚动条的功能。我们可以使用继承机制来实现这一功能,但是这种方法不够灵活,我们无法控制文本区加滚动条的方式和时机。而且当文本区需要添加更多的功能时,比如边框等,需要创建新的类,而当需要组合使用这些功能时无疑将会引起类的爆炸。 我们可以使用一种更为灵活的方法,就是把文本区嵌入到滚动条中。而这个滚动条的类就相当于对文本区的一个 装饰。这个装饰(滚动条)必须与被装饰的组件(文本区)继承自同一个接口,这样,用户就不必关心装饰的实现,因为这对他们来说是透明的。装饰会将用户的请求转发给相应的组件(即调用相关的方法),并可能在转发的前后做一些额外的动作(如添加滚动条)。通过这种方法,我们可以根据组合对文本区嵌套不同的装饰,从而添加任意多的功能。这种动态的对对象添加功能的方法不会引起类的爆炸,也具有了更多的灵活性。 以上的方法就是 Decorator模式,它通过给对象添加装饰来动态的添加新的功能。如下是Decorator模式的UML图: Component为组件和装饰的公共父类,它定义了子类必须实现的方法。 ConcreteComponent是一个具体的组件类,可以通过给它添加装饰来增加新的功能。 Decorator是所有装饰的公共父类,它定义了所有装饰必须实现的方法,同时,它还保存了一个对于Component的引用,以便将用户的请求转发给Component,并可能在转发请求前后执行一些附加的动作。 ConcreteDecoratorA和ConcreteDecoratorB是具体的装饰,可以使用它们来装饰具体的Component。 Java IO包中的Decorator模式 JDK提供的java.io包中使用了Decorator模式来实现对各种输入输出流的封装。以下将以java.io.OutputStream及其子类为例,讨论一下Decorator模式在IO中的使用。 首先来看一段用来创建IO流的代码:
以下是代码片段: try { OutputStream out = new DataOutputStream(new FileOutputStream("test.txt")); } catch (FileNotFoundException e) { e.printStackTrace(); } |
这段代码对于使用过JAVA输入输出流的人来说再熟悉不过了,我们使用DataOutputStream封装了一个FileOutputStream。这是一个典型的Decorator模式的使用,FileOutputStream相当于Component,DataOutputStream就是一个Decorator。将代码改成如下,将会更容易理解:
以下是代码片段: try { OutputStream out = new FileOutputStream("test.txt"); out = new DataOutputStream(out); } catch(FileNotFoundException e) { e.printStatckTrace(); } |
由于FileOutputStream和DataOutputStream有公共的父类OutputStream,因此对对象的装饰对于用户来说几乎是透明的。下面就来看看OutputStream及其子类是如何构成Decorator模式的: OutputStream是一个抽象类,它是所有输出流的公共父类,其源代码如下:
以下是代码片段: public abstract class OutputStream implements Closeable, Flushable { public abstract void write(int b) throws IOException; ... } |
它定义了write(int b)的抽象方法。这相当于Decorator模式中的Component类。 ByteArrayOutputStream,FileOutputStream 和 PipedOutputStream 三个类都直接从OutputStream继承,以ByteArrayOutputStream为例:
以下是代码片段: public class ByteArrayOutputStream extends OutputStream { protected byte buf[]; protected int count; public ByteArrayOutputStream() { this(32); } public ByteArrayOutputStream(int size) { if (size 〈 0) { throw new IllegalArgumentException("Negative initial size: " + size); } buf = new byte[size]; } public synchronized void write(int b) { int newcount = count + 1; if (newcount 〉 buf.length) { byte newbuf[] = new byte[Math.max(buf.length 〈〈 1, newcount)]; System.arraycopy(buf, 0, newbuf, 0, count); buf = newbuf; } buf[count] = (byte)b; count = newcount; } ... } |
它实现了OutputStream中的write(int b)方法,因此我们可以用来创建输出流的对象,并完成特定格式的输出。它相当于Decorator模式中的ConcreteComponent类。 接着来看一下FilterOutputStream,代码如下:
以下是代码片段: public class FilterOutputStream extends OutputStream { protected OutputStream out; public FilterOutputStream(OutputStream out) { this.out = out; } public void write(int b) throws IOException { out.write(b); } ... } |
同样,它也是从OutputStream继承。但是,它的构造函数很特别,需要传递一个OutputStream的引用给它,并且它将保存对此对象的引用。而如果没有具体的OutputStream对象存在,我们将无法创建FilterOutputStream。由于out既可以是指向FilterOutputStream类型的引用,也可以是指向ByteArrayOutputStream等具体输出流类的引用,因此使用多层嵌套的方式,我们可以为ByteArrayOutputStream添加多种装饰。这个FilterOutputStream类相当于Decorator模式中的Decorator类,它的write(int b)方法只是简单的调用了传入的流的write(int b)方法,而没有做更多的处理,因此它本质上没有对流进行装饰,所以继承它的子类必须覆盖此方法,以达到装饰的目的。 BufferedOutputStream 和 DataOutputStream是FilterOutputStream的两个子类,它们相当于Decorator模式中的ConcreteDecorator,并对传入的输出流做了不同的装饰。以BufferedOutputStream类为例:
以下是代码片段: public class BufferedOutputStream extends FilterOutputStream { ... private void flushBuffer() throws IOException { if (count 〉 0) { out.write(buf, 0, count); count = 0; } } public synchronized void write(int b) throws IOException { if (count 〉= buf.length) { flushBuffer(); } buf[count++] = (byte)b; } ... } |
这个类提供了一个缓存机制,等到缓存的容量达到一定的字节数时才写入输出流。首先它继承了FilterOutputStream,并且覆盖了父类的write(int b)方法,在调用输出流写出数据前都会检查缓存是否已满,如果未满,则不写。这样就实现了对输出流对象动态的添加新功能的目的。 下面,将使用Decorator模式,为IO写一个新的输出流。 自己写一个新的输出流
了解了OutputStream及其子类的结构原理后,我们可以写一个新的输出流,来添加新的功能。这部分中将给出一个新的输出流的例子,它将过滤待输出语句中的空格符号。比如需要输出"java io OutputStream",则过滤后的输出为"javaioOutputStream"。以下为SkipSpaceOutputStream类的代码:
以下是代码片段: import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; /** * A new output stream, which will check the space character * and won’t write it to the output stream. * @author Magic * */ public class SkipSpaceOutputStream extends FilterOutputStream { public SkipSpaceOutputStream(OutputStream out) { super(out); } /** * Rewrite the method in the parent class, and * skip the space character. */ public void write(int b) throws IOException{ if(b!=’ ’){ super.write(b); } } } |
它从FilterOutputStream继承,并且重写了它的write(int b)方法。在write(int b)方法中首先对输入字符进行了检查,如果不是空格,则输出。 以下是一个测试程序:
以下是代码片段: import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Test the SkipSpaceOutputStream. * @author Magic * */ public class Test { public static void main(String[] args){ byte[] buffer = new byte[1024];
/** * Create input stream from the standard input. */ InputStream in = new BufferedInputStream(new DataInputStream(System.in));
/** * write to the standard output. */ OutputStream out = new SkipSpaceOutputStream(new DataOutputStream(System.out));
try { System.out.println("Please input your words: "); int n = in.read(buffer,0,buffer.length); for(int i=0;i〈n;i++){ out.write(buffer[i]); } } catch (IOException e) { e.printStackTrace(); } } } |
执行以上测试程序,将要求用户在console窗口中输入信息,程序将过滤掉信息中的空格,并将最后的结果输出到console窗口。比如:
以下是引用片段: Please input your words: a b c d e f abcdef |
总 结 在java.io包中,不仅OutputStream用到了Decorator设计模式,InputStream,Reader,Writer等都用到了此模式。而作为一个灵活的,可扩展的类库,JDK中使用了大量的设计模式,比如在Swing包中的MVC模式,RMI中的Proxy模式等等。对于JDK中模式的研究不仅能加深对于模式的理解,而且还有利于更透彻的了解类库的结构和组成。
synchronized的一个简单例子
public class TextThread {
/** * @param args */ public static void main(String[] args) { // TODO 自动生成方法存根 TxtThread tt = new TxtThread(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); }
} class TxtThread implements Runnable { int num = 100; String str = new String(); public void run() { while (true) { synchronized(str) { if (num>0) { try { Thread.sleep(10); } catch(Exception e) { e.getMessage(); } System.out.println(Thread.currentThread().getName()+ "this is "+ num--); } } } } }
上面的例子中为了制造一个时间差,也就是出错的机会,使用了Thread.sleep(10)
Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。
总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
接着来讨论synchronized用到不同地方对代码产生的影响:
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。
1. 把synchronized当作函数修饰符时,示例代码如下:
Public synchronized void methodAAA()
{
//….
}
这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。
上边的示例代码等同于如下代码:
public void methodAAA()
{
synchronized (this) // (1)
{
//…..
}
}
(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(
2.同步块,示例代码如下:
public void method3(SomeObject so)
{
synchronized(so)
{
//…..
}
}
这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:
Class Foo
{
public synchronized static void methodAAA() // 同步的static 函数
{
//….
}
public void methodBBB()
{
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
小结如下:
搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。
还有一些技巧可以让我们对共享资源的同步访问更加安全:
1. 定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
2. 如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全蛭蓖饨缍韵笸ü齡et方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了。
|
synchronized的一个简单例子
public class TextThread {
/** * @param args */ public static void main(String[] args) { // TODO 自动生成方法存根 TxtThread tt = new TxtThread(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); new Thread(tt).start(); }
} class TxtThread implements Runnable { int num = 100; String str = new String(); public void run() { while (true) { synchronized(str) { if (num>0) { try { Thread.sleep(10); } catch(Exception e) { e.getMessage(); } System.out.println(Thread.currentThread().getName()+ "this is "+ num--); } } } } }
上面的例子中为了制造一个时间差,也就是出错的机会,使用了Thread.sleep(10)
Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。
总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
接着来讨论synchronized用到不同地方对代码产生的影响:
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。
1. 把synchronized当作函数修饰符时,示例代码如下:
Public synchronized void methodAAA()
{
//….
}
这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。
上边的示例代码等同于如下代码:
public void methodAAA()
{
synchronized (this) // (1)
{
//…..
}
}
(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(
2.同步块,示例代码如下:
public void method3(SomeObject so)
{
synchronized(so)
{
//…..
}
}
这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:
Class Foo
{
public synchronized static void methodAAA() // 同步的static 函数
{
//….
}
public void methodBBB()
{
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。
小结如下:
搞清楚synchronized锁定的是哪个对象,就能帮助我们设计更安全的多线程程序。
还有一些技巧可以让我们对共享资源的同步访问更加安全:
1. 定义private 的instance变量+它的 get方法,而不要定义public/protected的instance变量。如果将变量定义为public,对象在外界可以绕过同步方法的控制而直接取得它,并改动它。这也是JavaBean的标准实现方式之一。
2. 如果instance变量是一个对象,如数组或ArrayList什么的,那上述方法仍然不安全蛭蓖饨缍韵笸ü齡et方法拿到这个instance对象的引用后,又将其指向另一个对象,那么这个private变量也就变了,岂不是很危险。 这个时候就需要将get方法也加上synchronized同步,并且,只返回这个private对象的clone()――这样,调用端得到的就是对象副本的引用了。
|
1. public class Thread1 implements Runnable{
//当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时, //一个时间内只能有一个线程得到执行。 //另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块 public void run(){ synchronized (this){ for(int i=0; i<5; i++){ System.out.println(Thread.currentThread().getName() + " synchronize loop " + i); } } } public static void main(String[] args) {
Thread1 thread1 = new Thread1(); Thread thread2 = new Thread(thread1, "A"); Thread thread3 = new Thread(thread1, "B"); thread2.start(); thread3.start();//thead3必须等待thread2的执行完毕就可立即执行 }
}
2.
public class Thread2 {
public void method1(){ synchronized(this){ int i = 5; while(i-- > 0){ System.out.println(Thread.currentThread().getName() + " : " + i); try{ Thread.sleep(500); } catch(InterruptedException ie){ ie.printStackTrace(); } } } } //当一个线程访问object的一个synchronized(this)同步代码块时, //另一个线程仍然可以访问该object中的非synchronized(this)同步代码块 public void method2(){ int i = 5; while(i-- > 0){ System.out.println(Thread.currentThread().getName() + " : " + i); try{ Thread.sleep(500); }catch(InterruptedException ie){ ie.printStackTrace(); } } } public static void main(String[] args) {
final Thread2 thread2 = new Thread2(); Thread thread1 = new Thread(new Runnable(){ public void run(){ thread2.method1(); } }, "Thread1"); Thread thread3 = new Thread(new Runnable(){ public void run(){ thread2.method2(); } }, "Thread2"); thread1.start(); thread3.start();//thead3不用等待thread1的执行完毕就可立即执行 }
3.
public class Thread3 {
class Inner{ private void method1(){ int i = 5; while(i-- > 0){ System.out.println(Thread.currentThread().getName() + " : Inner.method1() = " + i); try{ Thread.sleep(500); }catch(InterruptedException ie){ ie.printStackTrace(); } } } //尽管线程thread1获得了对Inner的对象锁,但由于线程thread2访问的是同一个Inner中的非同步部分。 //所以两个线程互不干扰。 private void method2(){ int i = 5; while(i-- > 0){ System.out.println(Thread.currentThread().getName() + " : Inner.method2() = " + i); try{ Thread.sleep(500); }catch(InterruptedException ie){ ie.printStackTrace(); } } } }
public void method1(Inner inner){ synchronized (inner){//使用对象锁 inner.method1(); } } public void method2(Inner inner){ inner.method2(); } public static void main(String[] args){ final Thread3 thread3 = new Thread3(); final Inner inner = thread3.new Inner(); Thread thread1 = new Thread(new Runnable(){ public void run(){ thread3.method1(inner); } }, "Thread1"); Thread thread2 = new Thread(new Runnable(){ public void run(){ thread3.method2(inner); } }, "Thread2"); thread1.start(); thread2.start();//thread2无需等待thread1运行完毕,就可执行, } }
4.
public class Thread4 {
class Inner{ private void method1(){ int i = 5; while(i-- > 0){ System.out.println(Thread.currentThread().getName() + " : Inner.method1() = " + i); try{ Thread.sleep(500); }catch(InterruptedException ie){ ie.printStackTrace(); } } } //尽管线程thread1与thread2访问了同一个Inner对象中两个毫不相关的部分,但因为thread1先获得了对Inner的对象锁, //所以thread2对Inner.mmethod2()的访问也被阻塞,因为method2()是Inner中的一个同步方法。 private synchronized void method2(){ int i = 5; while(i-- > 0){ System.out.println(Thread.currentThread().getName() + " : Inner.method2() = " + i); try{ Thread.sleep(500); }catch(InterruptedException ie){ ie.printStackTrace(); } } } }
public void method1(Inner inner){ synchronized (inner){//使用对象锁 inner.method1(); } } public void method2(Inner inner){ inner.method2(); } public static void main(String[] args){ final Thread4 thread4 = new Thread4(); final Inner inner = thread4.new Inner(); Thread thread1 = new Thread(new Runnable(){ public void run(){ thread4.method1(inner); } }, "Thread1"); Thread thread2 = new Thread(new Runnable(){ public void run(){ thread4.method2(inner); } }, "Thread2"); thread1.start(); thread2.start();//thread2要等待thread1运行完才能执行,因为method2是一个同步方法 } }
5.
import java.text.SimpleDateFormat; import java.util.Date;
public class TestThread extends Thread{
private static Integer threadCounterLock;//用于同步,防止数据被写乱,可以是任意的对象 private static int threadCount;//静态变量用于本类的线程计数器 static{ threadCount = 0; threadCounterLock = new Integer(0); } public TestThread(){ super(); } public synchronized static void incThreadCount(){ threadCount++; System.out.println("thread count after enter: " + threadCount); } public synchronized static void decThreadCount(){ threadCount--; System.out.println("thread count after leave: " + threadCount); } public void run(){ // synchronized(threadCounterLock){//同步 // threadCount++; // System.out.println("thread count after enter: " + threadCount); // } incThreadCount();//此语句作用同上面注释掉的同步代码块 final long nSleepMilliSecs = 1000; //循环中的休眠时间
long nCurrentTime = System.currentTimeMillis(); long nEndTime = nCurrentTime + 10000;//运行截止时间
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try{ while(nCurrentTime < nEndTime){ nCurrentTime = System.currentTimeMillis(); System.out.println("Thread " + this.hashCode() + ", current time: " + simpleDateFormat.format(new Date(nCurrentTime))); try{ sleep(nSleepMilliSecs); }catch(InterruptedException ie){ ie.printStackTrace(); } } }finally{ // synchronized(threadCounterLock){ // threadCount--; // System.out.println("thread count after leave: " + threadCount); // } decThreadCount();//此语句作用同上面注释掉的同步代码块 } } public static void main(String[] args){ TestThread[] testThread = new TestThread[2]; for(int i=0; i<2; i++){ testThread[i] = new TestThread(); testThread[i].start(); } } }
1。类中定义内部类
public class Outer {
private int outer_i = 100;//内部类可以访问外部类的变量 void test(){ Inner inner = new Inner(); inner.display(); } //当一个类中的程序代码要用到另外一个类的实例对象, //而另外一个类的程序代码又要访问第一个类的成员时 // 将另外一个类作成第一个类的内部类。 class Inner{ private int inner_i; //内部类的成员只有在内部类的范围之内是可知的, //并不能被外部类使用 void display(){ System.out.println("display: outer_i = " + outer_i); } } }
public static void main(String[] args) {
Outer outer = new Outer(); outer.test(); }
2.
public class Outer {
private int size = 1; public void print(){ Inner inner = new Inner(); inner.method(11); } public class Inner{ private int size = 3; public void method(int size){ size++;//method的形参 this.size++;//Inner类中的成员变量 Outer.this.size++;//Outer类中的成员变量 System.out.println("method size is :" +size); System.out.println("Inner size is :" + this.size); System.out.println("Outer size is :" + Outer.this.size); } } public static void main(String[] args){ Outer outer = new Outer(); outer.print(); } }
3.内部类被外部引用
public class Outer {
private int size = 10; //内部类声明为public 可从外部类之外被调用 public class Inner{ public void print(){ System.out.println(++size); } } }
public class test {
public static void main(String[] args) {
Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); inner.print(); } }
4.方法中定义内部类
public class Outer {
int outer_i = 100; String str = new String("Between"); void test(){ for(int i=0; i<5; i++){ class Inner{ void display(){ System.out.println("display: outer_i = " + outer_i); } } Inner inner = new Inner(); inner.display(); } } //在方法中定义的内部类只能访问方法中的final类型的局部变量 //因为用final定义的局部变量相当于一个常量,她的生命周期超出了方法运行的生命周期 public void amethod(final int iArgs){ int it315;//此变量内部类不能访问 class Bicycle{ public void sayhello(){ System.out.println(str); System.out.println(iArgs); } } Bicycle bicycle = new Bicycle(); bicycle.sayhello(); } }
public static void main(String[] args) {
Outer outer = new Outer(); outer.test(); outer.amethod(18); }
1.静态变量
public class Chinese {
static String country = "中国"; String name; int age; void singOurCountry(){ System.out.println("啊, 亲爱的" + country); } }
public static void main(String[] args) {
System.out.println("Chinese country is :" + Chinese.country);//类.成员 Chinese ch1 = new Chinese(); System.out.println("Chinese country is :" + ch1.country);//对象.成员 ch1.singOurCountry(); }
注:1. 不能把任何方法内的变量声明为static类型 2. 静态成员变量为该类的各个实例所共享,所以在内存中该变量只有一份, 经常用来计数统计 如: class A{ private static int count = 0;
public A(){ count = count + 1; }
public void finalize(){//在垃圾回收时,finalize方法被调用 count = count - 1; } }
2.静态方法 public class Chinese {
//在静态方法里,只能直接调用同类中的其他静态成员,不能直接访问类中的非静态成员和方法 //因为对如非静态成员和方法,需先创建类的实例对象后才可使用。 //静态方法不能使用关键字this和super,道理同上 //main方法是静态方法,不能直接访问该类中的非静态成员,必须先创建该类的一个实例 static void sing(){ System.out.println("啊..."); } void singOurCountry(){ sing();//类中的成员方法可以直接访问静态成员方法 System.out.println("啊...祖国..."); } }
public static void main(String[] args) {
Chinese.sing();//类.方法 Chinese ch1 = new Chinese(); ch1.sing();//对象名.方法 ch1.singOurCountry(); }
3. 静态代码块 public class StaticCode {
static String country; static{ country = "Chine"; System.out.println("Static code is loading..."); } }
public class TestStatic {
static{ System.out.println("TestStatic code is loading..."); } public static void main(String[] args){ System.out.println("begin executing main method..."); //类中的静态代码将会自动执行,尽管产生了类StaticCode的两个实例对象, //但其中的静态代码块只被执行了一次。同时也反过来说明:类是在第一次被使用时 //才被加载,而不是在程序启动时就加载程序中所有可能要用到的类 new StaticCode(); new StaticCode(); } }
java实现多线程有两种方法: 一种是继承Thread类
如下面例子: public class TestThread extends Thread {
//实现多线程方法一:继承Thread类,并重载run方法 public void run(){ //要将一段代码在一个新的线程上运行,该代码应写在run函数中 while(true){ System.out.println(Thread.currentThread().getName() + "is running..."); } } }
public class ThreadDemo2 {
public static void main(String[] args) { //启动一个新的线程,不是直接调用Thread类子类对象的run方法, //而是调用Thread类子类对象的start方法,Thread类对象的start方法 //将产生新的线程,并在该线程上运行该Thread类对象中的run方法,根据 //多态性 实际上运行的是Thread子类的run方法 TestThread testThread = new TestThread(); testThread.start(); while(true){ System.out.println("main thread is running..."); } }
}
方法二是:实现接口Runnable中的run函数
如下列所示
//实现多线程方法二:实现Runnable接口,重载run函数, //大多数情况下,如果只想重写 run() 方法,而不重写其他 Thread 方法, //那么应使用 Runnable 接口。这很重要, //因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。 public class TestThread implements Runnable {
public void run() { // TODO Auto-generated method stub System.out.println(Thread.currentThread().getName() + "is running......"); } }
public static void main(String[] args) { // TODO Auto-generated method stub TestThread testThread = new TestThread();//创建TestThread类的一个实例 Thread thread = new Thread(testThread);//创建一个Thread类的实例 thread.start();//使线程进入runnable状态 while(true){ System.out.println("main thread is running..."); } }
2.这两种方法的区别:实现Runnable接口适合多个相同的程序代码访问处理同一资源
如下列所示: //四个售票窗口售同一中车票,总共有100张,要求这四个售票多线程进行 public class TicketRunnable implements Runnable {
private int tickets = 100; public void run() { while(true){ if(tickets > 0){ System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } } }
}
public class TicketRunnableDemo {
public static void main(String[] args){ //创建了四个线程,每个线程调用了同一各TicketRunnable对象中run方法, //访问的是同一个对象中的变量tickets,从而满足了我们的要求。 //实现Runnable接口适合多个相同的程序代码访问处理同一资源 TicketRunnable ticketrunnable = new TicketRunnable(); new Thread(ticketrunnable).start(); new Thread(ticketrunnable).start(); new Thread(ticketrunnable).start(); new Thread(ticketrunnable).start(); } }
而继承继承Thread类,并重载run方法不能达到此目的 如下所示: public class TicketThread extends Thread {
private int tickets = 100; public void run(){ while(true){ if(tickets>0){ System.out.println(Thread.currentThread().getName() + "is saling ticket " + tickets--); } } } }
public class TicketThreadDemo {
public static void main(String[] args){ //我们希望的是四个线程共售100,我们创建四个线程 //但结果与我们希望的相反,各个线程各售100张票 new TicketThread().start(); new TicketThread().start(); new TicketThread().start(); new TicketThread().start(); // 如果我们象下面这样写的话,只有一个线程在售票,没有实现四个线程并发售票 // TicketThread ticketThread = new TicketThread(); // ticketThread.start(); // ticketThread.start(); // ticketThread.start(); // ticketThread.start(); } }
3.后台线程
public class DaemonTest { public static void main(String[] args){ TicketRunnable ticketRunnable = new TicketRunnable(); Thread thread = new Thread(ticketRunnable); thread.setDaemon(true);//将此线程设置为后台线程,当进程中只有后台线程时,进程就会结束 thread.start(); } }
4.联合线程 (还没搞懂)
5. 线程同步
在上面的售票程序中,线程可能是不安全的,
比如我们修改一下:
public class ThreadRunnable implements Runnable{ private int tickets = 100; public void run() { while(true){ if(tickets > 0){ try{ Thread.sleep(10);//使该线程暂停, }catch(Exception e){ System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() + " is saling ticket " + tickets--); } } } }
再次测试时,发现可能会打印-1 -2 这样的ticket
所以我们要修改一下,使该程序是线程安全的,
public class TicketSynch implements Runnable{ private int tickets = 100; String str = new String("");//此对象放在run函数之外 public void run(){ while(true){ synchronized(str){//同步代码块 if(tickets > 0){ try{ Thread.sleep(10); } catch(Exception e){ System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() +" is saling ticket " + tickets--); } } } }
}
但是 同步以牺牲程序的性能为代价
5. 同步函数
public class SynchMethod implements Runnable{
private int tickets = 100; public void run(){ while(true){ sale(); } } public synchronized void sale(){ if(tickets > 0){ try{ Thread.sleep(10); }catch(Exception e){ System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() + "is saling ticket " + tickets--); } } }
public static void main(String[] args){ //创建了四个线程,每个线程调用了同一各TicketRunnable对象中run方法, //访问的是同一个对象中的变量tickets,从而满足了我们的要求。 //实现Runnable接口适合多个相同的程序代码访问处理同一资源 SynchMethod synchMethod = new SynchMethod(); new Thread(synchMethod).start(); new Thread(synchMethod).start(); new Thread(synchMethod).start(); new Thread(synchMethod).start(); }
6.代码块和函数间的同步 public class MethodAndBlockSyn implements Runnable{
private int tickets = 100; String str = new String(""); public void run(){ if(str.equals("method")){ while(true){ sale(); } } else{ synchronized(str){//同步代码块 if(tickets > 0){ try{ Thread.sleep(10); } catch(Exception e){ System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() +" is saling ticket " + tickets--); } } } } public synchronized void sale(){ if(tickets > 0){ try{ Thread.sleep(10); }catch(Exception e){ System.out.println(e.getMessage()); } System.out.println(Thread.currentThread().getName() + "is saling ticket " + tickets--); } } }
public static void main(String[] args){ MethodAndBlockSyn test = new MethodAndBlockSyn(); new Thread(test).start();// 这个线程调用同步代码块 try{ Thread.sleep(1);//让线程等待100ms 是为了让该线程得到CPU //避免该线程未得到CPU就将str设置为method,从而使此线程也是调用同步函数 }catch(Exception e) { } test.str = new String("method"); new Thread(test).start();//这个线程调用同步函数 }
7. 线程间通讯
wait: 告诉当前线程放弃监视器并进入睡眠状态,知道有其他线程进入同一监视器并调用notify为止。
notify:唤醒同一对象监视器中调用wait的第一个线程。类似饭馆有一个空位置后通知所有等待就餐顾客中的第一位可以入座的情况
notifyAll:唤醒同一对象监视器中调用wait的所有线程,具有最高优先级的程序先被唤醒并执行。
以上三个方法只能在synchronized方法中调用
public class Record {
private String name = "陈世美"; private String sex = "女"; boolean bFull = false; public synchronized void put(String name, String sex) { if(bFull) try { wait(); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } this.name = name; try{ Thread.sleep(10); } catch(Exception e){ System.out.println(e.getMessage()); } this.sex = sex; bFull = true; notify(); } public synchronized void get(){ if(!bFull) try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(name + "--->" + sex); bFull = false; notify(); } }
public class Productor implements Runnable {
Record record = null; public Productor(Record record){ this.record = record; } public void run(){ int i = 0; while(true){ if(i==0) record.put("陈陈陈", "男"); else record.put("陈世美", "女"); i = (i+1)%2; } } }
public class Consumer implements Runnable {
Record record = null; public Consumer(Record record){ this.record = record; } public void run(){ while(true){ record.get(); } } }
public static void main(String[] args) { // TODO Auto-generated method stub
Record record = new Record(); new Thread(new Productor(record)).start(); new Thread(new Consumer(record)).start(); }
|