文章目录
- java面向对象重点总结
- 类与实例
- 构造方法
- 方法重载
- 属性与修饰符
- 封装
- 继承
- 多态
- 重构
- 抽象类
- 接口
- 抽象类和接口的区别:
- 集合
- 泛型
java面向对象重点总结
对象是一个自包含的实体,用一组可识别的特性和行为来标识。
面向对象编程,英文叫Object-Oriented Programming(OOP),其实就是针对对象来进行编程的意思。
类与实例
类就是具有相同的属性和功能的对象的抽象的集合。我们来看代码:
这里’class’是表示定义类的关键字,'Cat’就是类的名称,'shout’就是类的方法。
这里有两点要注意:
(1)类名称首字母记着要大写。多个单词则各个首字母大写;
(2)对外公开的方法需要用’public’修饰符。
应用一个类,只要将类实例化一下就可以了。
实例,就是一个真实的对象。比如我们都是’人’,而你和我其实就是’人’类的实例了。而实例化就是创建对象的过程,使用new关键字来创
建。
注意,'Cat cat = new Cat();'其实做了两件事:
Cat 实 例 化 后 , 等 同 于 出 生 了 一 只 小 猫 cat , 此 时 就 可 以 让 小 猫cat.shout()了。在任何需要小猫叫的地方都可以实例化它。
构造方法
构造方法,又叫构造函数,其实就是对类进行初始化。构造方法与类同名,无返回值,也不需要void,在new的时候调用。
所有类都有构造方法,如果你不编码则系统默认生成空的构造方法,若你有定义的构造方法,那么默认的构造方法就会
失效了。
也就是说,由于你没有在Cat类中定义过构造方法,所以Java语言会生成一个空的构造方法Cat()。当然,这个空的方法什么也不做,只是为了让你能顺利地实例化而已。
构造方法是为了对类进行初始化。比如我们希望每个小猫一诞生就有姓名,那么就应该写一个有参数的构造方法:
这样一来,我们在客户端要生成小猫时,就必须要给小猫起名字了:
结果显示:
方法重载
方法重载提供了创建同名的多个方法的能力,但这些方法需使用不同的参数类型。注意:并不是只有构造方法可以重载,普通方法也是可以重载的。
注意方法重载时,两个方法必须要方法名相同,但参数类型或个数必须要有所不同,否则重载就没有意义了
方法重载可在不改变原方法的基础上,新增功能。
方法重载算是提供了函数可扩展的能力。比如刚才这个例子,有的小猫起好名字了,就用带string参数的构造方法,有的没有名字,
就用不带参数的,这样就达到了扩展的目的。
如果我需要分清楚猫的姓和名,还可以再重载一个public Cat(string firstName,string lastName)
属性与修饰符
属性是一个方法或一对方法,即属性适合于以字段的方式使用方法调用的场合。这里还需要解释一下字段的意思,字段是存储类要满足其设计所需要的数据,字段是与类相关的变量。比如刚才的Cat类中的’private string name = "";'name其实就是一个字段,它通常是私有的类变量。那么属性是什么样呢?我们现在增加一个猫叫次数ShoutNum的属性。
Java中的访问修饰符用于控制类、变量、方法和构造方法的访问权限。Java支持四种不同的访问修饰符:
public
:对所有类可见。protected
:对同一包内的类可见,对子类可见,对其他包的类不可见。default
(即缺省,无修饰符):对同一包内的类可见,对其他包的类不可见。private
:只对同一类内可见。
以下是一些使用这些修饰符的示例:
public class MyClass {public int publicField; // 公开访问protected int protectedField; // 保护访问int defaultField; // 默认(包)访问private int privateField; // 私有访问public void myMethod() { // 公开访问// 方法体}protected void myProtectedMethod() { // 保护访问// 方法体}void myDefaultMethod() { // 默认(包)访问// 方法体}private void myPrivateMethod() { // 私有访问// 方法体}
}
通常字段都是private,即私有的变量,而属性都是public,即公有的变量。那么在这里shoutNum就是私有的字段,而ShoutNum
就是公有的对外属性。由于是对外的,所以属性的名称一般首字母大写,而字段则一般首字母小写或前加’_'。
属性有两个方法get和set。get返回与声明的属性相同的数据类型,表示的意思是调用时可以得到内部字段的值或引用;set有一个参数,用关键字value表示,它的作用是调用属性时可以给内部的字段或引用赋值.
这就好像我们的房子,我们并不希望房子是全透明的,那样你在家里的所有活动全部都被看得清清楚楚,毫无隐私可言。通常我们的房子有门有窗,但更多的是不透明的墙。这门和窗其实就是public,而房子内的东西,其实就是private。而对于这个房子来说,门窗是可以控制的,我们并不是让所有的人都可以从门随意进出,也不希望蚊子苍蝇来回出入。这就是属性的作用了,如果你把字段声明为public,那就意味着不设防的门窗,任何时候,调用者都可以读取或写入,这是非常糟糕的一件事。如果把对外的数据写成属性,那情况就会好
很多。
这就好比给窗子安装了纱窗,只让阳光和空气进入,而蚊子苍蝇就隔离。多了层控制就多了层保护。
由于有了’叫声次数’的属性,于是我们的shout方法就需要改进了:
“此时调用的时候只需要给属性赋值就可以了”
结果显示:
如果我们不给属性赋值,小猫会叫’喵’应该是三声,因为字段shoutNum的初始值是3。
另外需要强调的是,变量私有的叫字段,公有的是属性,那么对于方法而言,同样也就有私有方法和公有方法了,一般不需要对外界公开的方法都应该设置其修饰符为private(私有)。这才有利于’封装’。
封装
现在我们可以讲面向对象的三大特性之一’封装’了。每个对象都包含它能进行操作所需要的所有信息,这个特性称为封装,因此对象不必依赖其他对象来完成自己的操作。这样方法和属性包装在类中,通过类的实例来实现。
封装有很多好处。第一,良好的封装能够减少耦合,至少我们让Cat和Form1的耦合分离了。第二,类内部的实现可以自由地修改,这也是显而易见的,我们已经对Cat做了很大的改动。第三,类具有清晰的对外接口,这其实指的就是定义为public的ShoutNum属性和shout方法。
封装的好处很好理解,比如刚才举的例子。我们的房子就是一个类的实例,室内的装饰与摆设只能被室内的居住者欣赏与使用,如果
没有四面墙的遮挡,室内的所有活动在外人面前一览无遗。由于有了封装,房屋内的所有摆设都可以随意地改变而不用影响他人。然而,
如果没有门窗,一个包裹得严严实实的黑箱子,即使它的空间再宽阔,也没有实用价值。房屋的门窗,就是封装对象暴露在外的属性和方法,专门供人进出,以及流通空气、带来阳光。
现在我需要增加一个狗叫的功能,就是加一个按钮’狗叫’,单击后会弹出’我的名字叫XX汪汪汪’如何做?"
很简单,仿造Cat加一个Dog类。然后再用类似代码调用就好了:
但你有没有发现,Cat和Dog有非常类似的代码?90%的代码是一样的,代码有大量重复不会是什么好事情。我们要用到面向
对象的第二大特性’继承’。
继承
我们还是先离开软件编程,来想想我们的动物常识,猫和狗都是动物,准确地说,他们都是哺乳动物。哺乳动物有什么特征?
哺乳动物是胎生、哺乳、恒温的动物,因为猫和狗是哺乳动物,所以猫和狗就同样具备胎生、哺乳、恒温的特征。所以我们可以这样说,由于猫和狗是哺乳动物,所以猫和狗与哺乳动物是继承关系。
回到编程上,对象的继承代表了一种’is-a’的关系,如果两个对象A和B,可以描述为’B是A’,则表明B可以继承A。‘猫是哺乳动物’,就说
明了猫与哺乳动物之间是继承与被继承的关系。实际上,继承者还可以理解为是对被继承者的特殊化,因为它除了具备被继承者的特性外,还具备自己独有的个性。例如,猫就可能拥有抓老鼠、爬树等’哺乳动物’对象所不具备的属性。因而在继承关系中,继承者可以完全替换被继承者,反之则不成立。所以,我们在描述继承的’is-a’关系时,是不能相互颠倒的。说’哺乳动物是猫’显然有些莫名其妙。继承定义了类如何相互关联,共享特性。继承的工作方式是,定义父类和子类,或叫作基类和派生类,其中子类继承父类的所有特性。子类不但继承了父类的所有特性,还可以定义新的特性。
学习继承最好是记住三句话,如果子类继承于父类,第一,子类拥有父类非private的属性和功能;第二,子类具有自己的属性和功能,即子类可以扩展父类没有的属性和功能;第三,子类还可以以自己的方式实现父类的功能(方法重写)。
对比观察Cat和Dog类:
我们会发现大部分代码都是相同的,所以我们现在建立一个父类,动物Animal类,显然猫和狗都是动物。我们把相同的代码尽量放到动物类中。
然后我们需要写Cat和Dog的代码。让它们继承Animal。这样重复的部分都可以不用写了,不过在Java中,子类从它的父类中继承的成员
有方法、属性等,但对于构造方法,有一些特殊,它不能被继承,只能被调用。对于调用父类的成员,可以用super关键字。
此时客户端的代码完全一样,没有受到影响,但重复的代码却因此减少了。
如果现在需要加牛、羊、猪等多个类似的类,按你以前的写法就需要再复制三遍,也就是有五个类。如果我们需要改动起始的叫声次数,也就是让shoutNum由3改为5,那需要改5个类,现在有了Animal,就只要改一个类就行了,继承可以使得重复减少。
不用继承的话,如果要修改功能,就必须在所有重复的方法中修改,代码越多,出错的可能就越大,而继承的优点是,继承使得所有
子类公共的部分都放在了父类,使得代码得到了共享,这就避免了重复,另外,继承可使得修改或扩展继承而来的实现都较为容易。
继承是有缺点的,那就是父类变,则子类不得不变。另外,继承会破坏包装,父类实现细节暴露给子类,这其实是增大了两个类之间的耦合性。
耦合性简单理解就是藕断丝连,两个类尽管分开,但如果关系密切,一方的变化都会影响到另一方,这就是耦合性高的表现,继
承显然是一种类与类之间强耦合的关系。当两个类之间具备**‘is-a’**的关系时,就可以考虑用继承了。
多态
下面我们再来增加需求,如果我们要举办一个动物运动会,来参加的有各种各样的动物,其中有一项是’叫声比赛’。界面就是放两个按钮,一个是’动物报名’,就是确定动物的种类和报名的顺序;另一个是’叫声比赛’,就是报名的动物挨个地叫出声音来比赛。注意来报名的都是什么动物,我们并不知道。可能是猫、可能是狗,也可能是其他的动物,当然它们都需要会叫才行。
先分析一下,来报名的都是动物,参加叫声比赛必须会叫。这说明什么?说明都有叫的方法,也就是Animal类中有shout方法。
所谓的’动物报名’,其实就是建立一个动物对象数组,让不同的动物对象加入其中。所谓的’叫声比赛’,其实就是遍历这个数组来让动物
们’shout()'就可以了。
就之前讲到的知识,是不足以解决这个问题的,所以我们引入面向对象的第三大特性——多态
同样是鸟,同样展开翅膀的动作,但老鹰、鸵鸟和企鹅之间,是完全不同的作用。老鹰展开翅膀用来更高更远地飞翔,鸵鸟用来更快更稳地奔跑,而企鹅则是更急更流畅地游泳。这就是生物多态性表现。在面向对象中,多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行
看定义显然不太明白,我再来举个例子。我们的国粹’京剧’以前都是子承父业,代代相传的艺术。假设有这样一对父子,父亲是非常有名的京剧艺术家,儿子长大成人,模仿父亲的戏也惟妙惟肖。有一天,父亲突然发高烧,上不了台表演,而票都早就卖出,退票显然会大大影响声誉。怎么办呢?由于京戏都是需要化妆才可以上台的,于是就决定让儿子代父亲上台表演。
化妆后谁认识谁呀,只要唱得好就可以糊弄过去了。
这里面有几点注意,第一,子类以父类的身份出现,儿子代表老子表演,化妆后就是以父亲身份出现了。第二,子类在工作时以自己的方式来实现,儿子模仿得再好,那也是模仿,儿子只能用自己理解的表现方式去模仿父亲的作品;第三,子类以父类的身份出现时,子类特有的属性和方法不可以使用,儿子经过多年学习,其实已经有了自己的创作,自己的绝活,但在此时,代表父亲表演时,绝活是不能表现出来的。当然,如果父亲还有别的儿子会表演,也可以在此时代表父亲上场,道理也是一样的。这就是多态。
子类可以选择使用override关键字,将父类实现替换为它自己的实现,这就是方法重写
Override,或者叫作方法覆写。我们来看一下例子:
由于Cat和Dog都有shout的方法,只是叫的声音不同,所以我们可以让Animal有一个shout的方法,然后Cat和Dog去重写这个shout,用的时候,就可以用猫或狗代替Animal叫唤,来达到多态的目的。
再回到刚才写的客户端代码上:
结果显示,先单击"动物报名",然后"叫声比赛",将有五个对话列出:
Animal相当于京剧表演的老爸,Cat和Dog相当于儿子,儿子代表父亲表演shout,但Cat叫出来的是’喵’,Dog叫出来的是’汪’,这就是所
谓的不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行。
不过一定要注意了,这个对象的声明必须是父类,而不是子类,实例化的对象是子类,这才能实现多态。多态的原理是当方法被调用时,无论对象是否被转换为其父类,都只有位于对象继承链最末端的方法实现会被调用。也就是说,虚方法是按照其运行时类型而非编译时类型进行动态绑定调用的。
重构
现在又来了小牛和小羊来报名,需要参加’叫声比赛’,如何做?我现在再实现牛Cattle和羊Sheep的类,让它们都继承
Animal就可以了。
有没有发现,猫狗牛羊四个类,除了构造方法之外,shout里除了四种动物叫的声音不同外,几乎没有任何差异。
所以这里有重复,我们还是应该要改造它,
我先把重复的这个shout的方法体放到Animal类中,把叫的声音部分改成另一个方法getShoutSound。
此时的子类就极其简单了。除了叫声和构造方法不同,所有的重复都转移到了父类,真是漂亮之极。
子类拥有所有父类非private的属性和方法。由于子类继承父类,所以是public的Shout方法是一定可以为所有子类所用的。这里其实就是在用一个设计模式,叫模板方法
抽象类
我们再来观察,你会发现,Animal类其实根本就不可能实例化的,你想呀,说一只猫长什么样,可以想象,说new Animal();即
实例化一个动物。一个动物长什么样?动物是一个抽象的名词,没有具体对象与之对应。
所以我们完全可以考虑把实例化没有任何意义的父类,改成抽象类,同样地,对于Animal类的getShoutSound方法,其实方法体没有任何意义,所以可以将修饰符改为abstract,使之成为抽象方法。Java允许把类和方法声明为abstract,即抽象类和抽象方法。
这样一来,Animal就成了抽象类。抽象类需要注意几点,第一,抽象类不能实例化,刚才就说过,'动物’实例化是没有意义的;第二,抽象方法是必须被子类重写的方法,不重写的话,它的存在又有什么意义呢?其实抽象方法可以被看成是没有实现体的虚方法;第三,如果类中包含抽象方法,那么类就必须定义为抽象类,不论是否还包含其他一般方法。
这么说的话,一开始就可以把Animal类设成抽象类了,根本没有必要存在虚方法的父类。我们应该考虑让抽象类拥有尽可能多的共同代码,拥有尽可能少的数据。
抽象类通常代表一个抽象概念,它提供一个继承的出发点,当设计一个新的抽象类时,一定是用来继承的,所以,在一个以继承关系形成的等级结构里面,树叶节点应当是具体类,而树枝节点均应当是抽象类。也就是说,具体类不是用来继承的。我们作为编程设计者,应该要努力做到这一点。比如,若猫、狗、牛、羊是最后一级,那么它们就是具体类,但如果还有更下面一级的金丝猫继承于猫、哈巴狗继承于狗,就需要考虑把猫和狗改成抽象类了,当然这也是需要具体情况具体分析的。
接口
在动物运动会里还有一项非常特殊的比赛是为了给有特异功能的动物展示其特殊才能的。可以来比赛的比如有机器猫叮当 、石猴孙悟空、肥猪猪八戒,再比如蜘蛛人、蝙蝠侠等。其实我的目的只是为了让两个动物尽量不相干而已。现在叮当会从肚皮的口袋里变出东西,而孙悟空可以拔根毫毛变出东西,且有七十二般变化,八戒有三十六般变化。它们各属于猫、猴、猪,现在需要让它们比赛谁变东西的本领大。来分析一下如何做?
变出东西’应该是叮当 、孙悟空、猪八戒的行为方法,要想用多态,就得让猫、猴、猪有’变出东西’的能力,而为了更具有普遍意义,干脆让动物具有’变出东西’的行为,这样就可以使用多态了。
这犯了几乎所有学面向对象的人都会犯的错误,'变出东西’它是动物的方法吗?如果是,那是不是所有的动物都必须具备’变出东西’的能力呢?这其实只是三个特殊动物具备的方法。
这时候我们就需要新的知识了,那就是接口interface。通常我们理解的接口可能更多的是像电脑、音响等设备的硬件接口,比如用来传输电力、音视频、数据等接线的插口。而今天我们要提的,是面向对象编程里的接口概念。
接口是把隐式公共方法和属性组合起来,以封装特定功能的一个集合。一旦类实现了接口,类就可以支持接口所指定的所有属性和成员。声明接口在语法上与声明抽象类完全相同,但不允许提供接口中任何成员的执行方式。所以接口不能实例化,不能有构造方法和字段;不能有修饰符,比如public、private等;不能声明为虚拟的或静态的等。还有实现接口的类就必须要实现接口中的所有方法和属性。
一个类可以支持多个接口,多个类也可以支持相同的接口。所以接口的概念可以让用户和其他开发人员更容易理解其他人的代
码。接口的命名,有些语言前面要加一个大写字母’I’,这是一种规范。
我们先创建一个接口,它是用来’变东西’的。注意接口用interface声明,而不是class(接口名称前加’I’会更容易识别),接口中的方法或属性前面不能有修饰符、方法没有方法体。
猴子的类Monkey和孙悟空的类StoneMonkey与上面非常类似,在此省略。此时我们的客户端,可以加一个’变出东西’按钮,并实现下面的代码:
结果显示:
其实这和抽象类是很类似的,由于我现在要让两个完全不相干的对象,叮当和孙悟空来做同样的事情’变出东西’,所以我不得不让
他们去实现做这件’变出东西’的接口,这样的话,当我调用接口的’变出东西’的方法时,程序就会根据我实现接口的对象来做出反应,如果是叮当,就是用万能口袋,如果是孙悟空,就是七十二变,利用了多态性完成了两个不同的对象本不可能完成的任务。
同样是飞,鸟用翅膀飞,飞机用引擎加机翼飞,而超人呢?举起两手,握紧拳头就能飞,它们是完全不同的对象,但是,如果硬要
把它们放在一起的话,用一个飞行行为的接口,比如命名为IFly的接口来处理就是非常好的办法。
抽象类和接口的区别:
从表象上来说,抽象类可以给出一些成员的实现,接口却不包含成员的实现,抽象类的抽象成员可被子类部分实现,接口的成员需要实现类完全实现,一个类只能继承一个抽象类,但可实现多个接口等。但这些都是从两者的形态上去区分的。我觉得还有三点是能帮助我们去区分抽象类和接口。第一,类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象。接口是对类的局部(行为)进行的抽象,而抽象类是对类整体(字段、属性、方法)的抽象。如果只关注行为抽象,那么也可以认为接口就是抽象类。总之,不论是接口、抽象类、类甚至对象,都是在不同层次、不同角度进行抽象的结果,它们的共性就是抽象。第二,如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。比如猫呀狗呀,它们其实都是动物,它们之间有很多相似的地方,所以我们应该让它们去继承动物这个抽象类,而飞机、麻雀、超人是完全不相关的类,叮当是动漫角色,孙悟空是古代神话人物,这也是不相关的类,但它们又是有共同点的,前三个都会飞,而后两个都会变出东西,所以此时让它们去实现相同的接口来达到我们的设计目的就很合适了。其实实现接口和继承抽象类并不冲突,我完全可以让超人继承人类,再实现飞行接口。
超人除了内裤外穿以外,基本就是一个正常人的样子,让他继承人类是对的,但他本事很大,除了飞天,他还具有刀枪不入、力大无穷等非常人的能力,而这些能力也可能是其他对象具备的,所以就让超人去实现飞行、力大无穷等行为接口,这就可以让超人和飞机比飞行,和大象比力气了,这就是一个类只能继承一个抽象类,却可以实现多个接口的做法。第三,从设计角度讲,抽象类是从子类中发现了公共的东西,泛化出父类,然后子类继承父类,而接口是根本不知子类的存在,方法如何实现还不确认,预先定义。这里其实说明的是抽象类和接口设计的思维过程。回想一下我们今天刚开始讲的时候,先是有一个Cat类,然后再有一个Dog类,观察后发现它们有类似之处,于是泛化出Animal类,这也体现了敏捷开发的思想,通过重构改善既有代码的设计。事实上,只有小猫的时候,你就去设计一个动物类,这就极有可能会成为过度设计了。所以说,抽象类往往都是通过重构得来的,当然,如果你事先意识到多种分类的可能,那么事先就设计出抽象类也是完全可以的。
而接口就完全不是一回事。我们很早已经设计好了电源插座的接口,但在几十年前是无法想象未来会有什么样的电器需要电源插座的。只要事先把接口设计好,剩下的事就慢慢再说不着急了。
再比如我们是动物运动会的主办方,在策划时,大家坐在一起考虑需要组织什么样的比赛。大家商议后,觉得应该设置如跑得最快、跳得最高、飞得最远、叫得最响、力气最大等比赛项目。此时,主办方其实还不太清楚会有什么样的动物来参加运动会,所有的这些比赛项目都可能是完全不相同的动物在比,它们将如何去实现这些行为也不得而知,此时,能做的事就是事先定义这些比赛项目的行为接口。
抽象类是自底而上抽象出来的,而接口则是自顶向下设计出来的。
集合
下面我们再来看看,客户端的代码中,‘动物报名’用的是Animal类的对象数组,你设置了数组的长度为5,也就是说,最多只能有五个动物可以报名参加’叫声比赛’,多了就不行了。这显然是非常不合理的,应该考虑改进。
数组的优点,比如说数组在内存中连续存储,因此可以快速而容易地从头到尾遍历元素,可以快速修改元素等。数组的缺点,应该是创建时必须要指定数组变量的大小,还有在两个元素之间添加元素也比较困难。
这就可能使得数组长度设置过大,造成内存空间浪费,长度设置过小造成溢出。所以Java提供了用于数据存储和检索的专用类,这些类统称集合。这些类提供对堆栈、队列、列表和哈希表的支持。大多数集合类实现相同的接口。
我们现在介绍当中最常用的一种,ArrayList。首先ArrayList是程序包java.util.ArrayList下的一部分,它是使用大小可按需动态增加的数组实现Collection接口。
**ArrayList的容量是ArrayList可以保存的元素数。ArrayList的默认初始容量为0。随着元素添加到ArrayList中,容量会根据需要通过重新分配自动增加。使用整数索引可以访问此集合中的元素。此集合中的索引从零开始。**可以这样理解,数组的容量是固定的,而ArrayList的容量可根据需要自动扩充。由于实现了Collection,所以ArrayList提供添加、插入或移除某一范围元素的方法。下面我们来看看如何做:
如果有动物报完名后,由于某种原因(如政治、宗教、兴奋剂、健康等)放弃比赛,此时应该需要将其从名单中移除。例如,在报了名后,两只小狗需要退出比赛。我们查了一下它们的报名索引次序为1和2(从0开始计算),所以可以应用集合的remove方法,它的作用是移除指定索引处的集合项。
集 合 与 数 组 的 不 同 就 在 于 此 , 程 序 在 执 行RemoveAt(1)的时候,也就是叫’阿毛’的Dog被移除了集合,此时’小黑’的索引次序还是原来的2吗?等于整个后序对象都向前移一位了。应该是这样才对。也就是说,集合的变化是影响全局的,它始终都保证元素的连续性。
总结一下,集合ArrayList相比数组有什么好处:
主要就是它可以根据使用大小按需动态增加,不用受事先设置其大小的控制。还有就是可以随意地添加、插入或移除某一范围元素,比数组要方便。
这是ArrayList的优势,但它也有不足,ArrayList不管你是什么对象 都 是 接 受 的 , 因 为 在 它 眼 里 所 有 元 素 都 是 Object , 这 就 使 得 如 果你 ‘arrayAnimal.add ( 123 ) ;’ 或 者 'arrayAnimal.add ( " HelloWorld");'在编译时都是没有问题的,但在执行时,'for(Animal item:arrayAnimal)'需要明确集合中的元素是Animal类型,而123是整型,HelloWorld是字符串型,这就会在运行到此处时报错,显然,这是典型的类型不匹配错误,换句话说,ArrayList不是类型安全的。还有就是ArrayList对于存放值类型的数据,比如int、string型(string是一种拥有值类型特点的特殊引用类型)或者结构struct的数据,用ArrayList就意味着都需要将值类型装箱为Object对象,使用集合元素时,还需要执行拆箱操作,这就带来了很大的性能损耗。
所谓装箱就是把值类型打包到Object引用类型的一个实例中。比如整型变量i被’装箱’并赋值给对象o。
所谓拆箱就是指从对象中提取值类型。此例中对象o拆箱并将其赋值给整型变量i。
相对于简单的赋值而言,装箱和拆箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个全新的对象。其次,拆箱所需的强制转换也需要进行大量的计算[MSDN]。总之,装箱拆箱是耗资源和时间的。而ArrayList集合在使用值类型数据时,其实就是在不断地做装箱和拆箱的工作,这显然是非常糟糕的事情。
Java在5.0版之前的确也没什么好办法,但5.0出来后,就推出了新的技术来解决这个问题,那就是泛型。
泛型
泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。泛型集合类可以将类型参数用作它所存储的对象的类型的占位符;类型参数作为其字段的类型和其方法的参数类型出现。我读给你的是泛型定义的原话,听起有些抽象,我们直接来看例子。在Java 5.0后有ArrayList类的泛型等效类,该类使用大小可按需动态增加的数组实现Collection泛型接口。其实用法上关键就是在ArrayList后面加’',这个’T’就是你需要指定的集合的数据或对象类型。
此 时 , 如 果 你 再 写 ‘arrayAnimal.add ( 123 ) ;’ 或者’arrayAnimal.add("HelloWorld");'结果将是编译报错,因为add的参数必须是要Animal或者Animal的子类型才行。
我是这样想的,其实ArrayList和ArrayList在功能上是一样的,不同点就在于,它在声明和实例化时都需要指定其内部项的数据或对象类型,这就避免了刚才讲的类型安全问题和装箱拆箱的性能问题了。
不过显然Java语言的设计者也并不是一开始就明白这一点,也是通过实践和用户的反馈才在Java 5.0版中改进过来的。巨人也会有走弯路的时候,何况我们常人。通常情况下,都建议使用泛型集合,因为这样可以获得类型安全的直接优点而不需要从基集合类型派生并实现类型特定的成员。此外,如果集合元素为值类型,泛型集合类型的性能通常优于对应的非泛型集合类型(并优于从非泛型基集合类型派生的类型),因为使用泛型时不必对元素进行装箱。