万物皆对象
一、抽象过程:
1,万物皆为对象。
狗、房子这种具体的事物是对象,“服务”这种抽象的概念也是对象。
你可以用对象来存储东西。狗对象可以存储狗头,狗腿等。“服务”对象可以存储服务类型、服务员、顾客等。
你可以要求对象执行某种操作。比如,让狗叫一声,让“服务”对象做一个“送货上门”的动作。
2,程序是对象的集合
程序中的各个对象通过发送消息来告诉彼此要做什么,来合作完成一项任务。比如你调用某个对象的某个方法,“调用”的过程就是“发消息”。
3,一个对象可以由许多其他对象组成。
4,每一个对象都有自己的类型。
比如,A a=new A();那么,a的类型就是A,“类”就是“类型”。你创建了一个狗对象,那么,狗对象的类型就是狗。
类与类区分开来的一个重要因素是:“可以发送什么样的消息给它”,即:每个类都有自己的方法。
5,某一特定类型的对象可以接收同样的消息。
接收消息即调用方法。泰迪类、博美类都属于狗类。上面这句话的意思就是:因为泰迪和博美都是狗,所以,它们都能够调用狗的“叫”、“跑”等方法。比如:
BoMei和TaiDi继承了Dog,所以,它们创建的对象可以调用Dog的方法。
这就意味着,我们要编写BoMei或者TaiDi相关的代码时,只需要编写Dog相关代码就可以,这就是OOP(面向对象设计)中最强有力的概念之一。如下:
我给Dog定义了legs属性。在test(Dog dog)方法中,我传入的参数是“Dog”,操作的也是Dog类的属性。但是当我在16行调用这个方法时,我传的参数是“BoMei”对象,此时,操作的就是BoMei。
同理,当我们要操作的是TaiDi对象时,给test传入TaiDi对象就行。我们就不必分别为TaiDi和BoMei再定义一个test(TaiDi ta)、test(BoMei bo)方法。
6,每个对象都是唯一的
对象都有状态、行为、标识、类型。比如上面的BoMei类,“状态”就是“变量”,就是“属性”,上面的BoMei类,legs就是它的“状态”。行为就是“方法”、“函数”,对应上面的bark()、eat()等。行为可以改变状态,比如上面的test(Dog dog)改变了legs属性的值。类型就是对象所属的类,而“标识”就相当于身份证一样,这在java中表现为:每个对象在内存中都有唯一的地址。
二、每个对象都有一个接口
苹果有皮、有核,是甜的,长在树上。梨也有皮,也有核,也是甜的,也长在树上。那么苹果和梨有很多相似性,被划分到同一类型:水果类。这个“水果类”就是一个抽象数据类型(所谓的抽象,就是抽取了一些类似的表象东西,比如皮、核、味道、生长环境等)。
抽象数据类型的运作方式与基本数据类型是一致的。比如对于基本数据类型,可以这样使用:
对于抽象数据类型,可以这样使用:
创建某一类型的变量(创建对象或者实例),并调用其方法(发送消息或请求,让它知道该做什么)。
每个具体的对象又有自己特有的状态,比如,苹果的皮是红的,梨子的皮是绿的。苹果水分少,梨子水分多等。所以,苹果、梨都是水果类,但是它们又是水果类中独立的个体,这些个体就是一个个唯一的对象,那么对象与类的关系就明了了:每一个对象都属于定义了特性和行为的某个特定的类。编程系统对待抽象数据类型与对待一般数据类型是一视同仁的,也会作类型检查等:
苹果和梨子都能吃,能榨果汁,能制作苹果罐头等。吃、榨果汁、制作罐头这属于行为(也就是方法,也就是请求),苹果和梨子都满足这些特定的请求,那么这些特定的请求就定义在接口中,而接口也是一种类型:
接口只是定义了可以向某一特定对象发出的请求,那么具体到苹果、梨子该发出怎样的请求,剥皮吃还是不剥皮,榨果汁时要注意什么,这都是具体请求该有的细节,在苹果类、梨子类中必须有这些具体的细节方法,这些代码就构成了“实现”。
三、每个对象都提供服务
不要试图把所有功能都拥挤在一个对象里,完成一项服务项目需要各个对象的配合,比如实现一个打印模块,我们可以定义一个对象专门检查配置是否正常,另一个对象定义怎样打印一张4A图纸,再定义一个对象集合,调用前两个对象,再加之自己的方法最终把图纸打印出来。每个对象都可以很好的完成一个任务,但并不试图做更多的事情。然后这些对象齐心协力去完成一项服务。
上述的“齐心协力”其实就是软件设计的基本质量要求之一:高内聚。
四、访问控制
Java用三个关键字控制了变量及方法的访问:public、private、protected。public其他类都可以访问,private只有本类及类的内部方法能够访问,其他类不可见。protected表示只有本类及继承本类的子类可见,其他不可见。除此之外,Java还有一种默认的访问权限:包访问权限,类可以访问同一个包中的其他类成员。
访问控制的原因1:让客户端程序员(类的调用者)无法触及他们不该触及的部分。调用者只需要调用有用的方法,有些变量等是设计者为了实现内部逻辑而使用的,不需要让客户端程序员知道。如果把那些私有变量公开化,既会干扰到客户端程序员的调用思路,也可能被客户端程序员误操作而修改了状态值。
访问控制的原因2:类库的设计者可以改变内部的工作方式而不会影响到外部客户端程序员的调用。
五、组合,聚合,代码复用
代码复用是面向对象程序设计语言所提供的最了不起的优点之一。
直接用某个类创建一个对象也属于复用,下面第6行就是在复用Apple类。
将某个类的对象置于某个新类中(创建一个成员对象),也属于复用:
上例中是用Fruit、Dog合成了Test类,所以,这个概念称为:组合(composition)。如果组合是动态发生的,则称为“聚合(aggregation)”。
什么是动态发生呢?看:
第7行并没有创建Apple实例,等到11行调用时,才实例化了Apple,这就是动态发生。
组合的关系是:has-a,即:汽车拥有引擎、公司拥有员工、Test拥有Fruit、Dog。
六、继承
复制现有的类,然后添加和修改这个复制品来创建新类,这就是继承。当源类(又叫:父类、超类、基类)发生变动时,被修改的子类也会发生这些变动。
上面Circle继承了Shape,自然也就继承了Shape的color属性,以及getColor()、setColor()方法、draw()方法。在继承过来的同时,Circle修改了draw()方法,表现出自己与父类不同的地方(这一行为叫做override,即:覆盖)。当然,Circle也可以添加自己的新方法。
如果我把父类的getColor方法修改一下:
那么子类的这个方法就跟着发生了改变。
父类含有所有子类所共享的特性和行为。比如上例,color变量是共享的(color属于特性),draw()方法也是共享的(draw()属于行为)。
父类与子类有着相同的类型。
从上面代码可以看出,Circle类型同时也是Shape类型,这个很容易理解,博美是狗,泰迪是狗,圆是图形,没毛病。
理解面向对象设计的重要门槛是理解:通过继承而产生的类型等价性。
使父类与子类产生差异的两种方法:
1, 添加新方法。(此时,子类与父类的关系是:is like a)
2, 覆盖。(只覆盖而不添加新方法的话,子类与父类的关系是:is a)。
七、向上转型
上面代码中,test()里面的参数为父类Shape,调用的也是父类Shape的draw()方法。当在main方法中调用test时,却传入了子类Circle对象,test方法也可以正常调用,且这时,调用的是Circle的draw()方法。
经常需要这样,把一个子类对象当做它的父类型来对待,这样做的好处是,不依赖特定类型的代码,也不受添加新类型的影响。比如,下一次你传入一个正方形子类,那么test内部就会自动调用正方形的draw()方法。你如果添加一个新类:六角形,然后把六角形对象传入test,它也能够正常调用。
面向对象设计语言采用的是“后期绑定”。当test()方法具体调用时,才能确定参数所对应的具体类型。
上述把子类当做父类型的过程叫做“向上转型,upcasting”。
八、容器
我们有时需要管理很多对象,我们不知道需要多少个对象,不知道这些对象能够活多久,不知道存储这些对象需要多大空间,一问三不知。
容器(集合)帮助我们解决了上述问题。我们把对象放入容器中,在任何时候都可以去扩充它。
Java中具有满足各种需要的容器。比如:List(有序的对象集合),Map(建立对象之间的关联,也叫映射),Set(每种对象只有一个,不会重复,类似于数学中的集合),当然,还有队列(先放进去的对象先出来,FIFO)、树(以任意顺序把许多对象放进该容器,当你遍历时每个值已经排好序)、堆栈(最后放进去的对象先出来,LIFO队列)等。
对象的关联比如:
把”张三”与”NAME”关联,把”17”与”AGE”关联,这样,在获取”张三”时,只需要如下:
1,不同的容器作用不同(接口不同、方法不同)。
比如,Stack和Queue二者不在一个继承树下,且各自的方法功能不相同。实际上,Stack(堆栈)是一种后进先出的模式,只能在栈头进行插入与删除操作。Queue(队列)是一种先进先出的模式,只能在队尾进行插入,在队头进行删除。
2,不同的容器性能不同
比如:ArrayList和LinkedList。如果是随机访问一个元素,对于ArrayList来说时间是固定的,而LinkedList需要从第一个元素开始查找,直到找到目标元素,如果目标元素在容器的末端,那么就要花费更多时间。
如果是插入一个元素,对于ArrayList,插入位置后面的元素统统要往后移动一位(想想实际生活中的插队),而对于LinkedList来说,只需要把插入位置的两个对象拆开,然后把目标元素加入进来即可(想想实际生活中小朋友们手拉手的情况)。下图是LinkedList的插入与删除:
九、泛型,向下转型
Java中,所有对象都继承自Object,那么由向上转型规律可知,能存放Object的容器,就能存放任何Java对象。
其实容器里放置的并不是对象本身,而只是对象的”引用”,指向对象的地址。
当你把一个非Object对象(比如String)的引用放进一个容器时,由于该容器只能存放Object引用,所以,它将会强制转成Object引用。那么当你再次取出该引用时,就变成了Object引用,而不是你想要的String。如下:
虽然存入List中的是dog的引用,而把它赋值给dog2时,却报错,因为dog.get(0)取出来的是Object类型的引用。这时,就要用到向下转型。
向上转型是把子类当作父类来用,而向下转型是把父类型转换为一个更具体的类型:
比如上图,把Object引用强制转换为Dog。
向上转型是安全的,比如你可以大胆的说:苹果是水果,梨子是水果,所以,你可以大胆的把任何对象强制转成Object:
向下转型却是不安全的,你只知道它是一条狗,但是你不知它具体是什么种类:
上例把泰迪放入List,取出来时是Object,却错误的把它转换成博美,结果发生了错误。
为了避免上面种种风险的发生,Java中引入了泛型:
如上图,明确说明List狗窝中只能放泰迪,那么你放入博美就会直接报错。
如上图,我已经知道List狗窝里面睡的是泰迪,你取出来把它喊成博美,那么也会报错。
十、对象的生命周期
出生:
当你每次new一个对象时,Java就动态地在一个称为“堆”的内存池中创建一个对象。由于是动态的,所以,直到运行时才能知道要创建多少个对象,对象的生命周期是什么,以及对象的具体类型是什么。再搬出前面出现过的一段代码来加深对“动态”的了解:
上例中,直到运行test()时才会创建Apple实例。
死亡:
Java的垃圾回收机制会自动发现对象什么时候不用了,然后去销毁它,以达到释放内存的目的。
Java判断某个对象是否可以回收是一件复杂的事情。比如,一般情况下,创建一个对象,会在堆中生成一个对象,而会在栈中放一个该对象的引用(一个数字,指向这个对象在堆中的地址),垃圾回收器有一种早期策略,每生成一个引用,计数器就会+1,而每减少一个引用,计数器就会-1,当发现计数器为0时,说明该对象可以回收了。如下图:
图中第四步,per2指向了per1引用的地址,那么就没有引用指向age=20的那个对象了,于是,那个对象会被垃圾回收器回收。
十一、异常处理
异常是一种对象,当程序发生错误时,它从那个错误点“抛出”,然后由该错误对应的专门的处理器“抓住”。所以,如果代码是正常的,异常代码将不会发生。
有些异常是在运行的时候才能发现的,主要是由于程序员的失误造成的,这类异常叫做“运行时异常”,比如:
你明知道test不能转成整数,还非要转,运行时就会报错。
还有一种异常,叫“非运行时异常”,就是运行前就该有所防范的,比如想从某个路径下加载一个文件,这种情况下,就有找不到这个文件的可能性,那么,你必须做出防患于未然,可以抛出异常:
也可以捕获异常:
十二、并发编程
为了提高响应能力,我们想把一个任务分成多个子任务独立的运行,让他们一起干活。这些独立运行的子任务就是”线程”,而“一起干活”就是“并发”。
实际上,在单一处理器环境中,线程之间是轮番交叉运行的,由处理器来分给每个线程时间。而在多处理器上才存在真正的”一起干活”,即“并行”。
多线程并发完成一项工作的过程中,资源共享就带来隐患,比如桌子上有一个粉笔(资源),两个小朋友(线程)同时伸手去拿粉笔(抢占资源),那么就会打起来。所以,就要有一种机制,当一个小朋友要去拿粉笔时,先把粉笔保护起来(加锁),等这个小朋友不用了,再释放锁,另一个小朋友才可以用粉笔。
更多内容请关注: