Java编程思想学习笔记(二)
1.7伴随多态的可互换对象(后期绑定)
在处理一个对象时,经常不将其视作其所属的特定类型来看待,而是将它视作其基类的一个对象并且对他发送消息(将无论是什么类型的几何图案都视作几何图案而不是诸如正方形、三角形之类的具体类型),在这种处理方法中不需要考虑接受消息的对象会怎么处理收到的消息,因此不会收到添加新类型的影响(也就是编写程序时,编写的是“打印几何图案”而不是“打印正方形”)。
但是这种编程的思想也存在一种问题,那就是在发送消息给对象时,程序员并不知道会执行怎么样的一段代码(众所周知,正方形的绘制方式肯定和三角形不同、狗和蛇的“行走”方式也是不同的)。
问题的解决办法——后期绑定:向对象发送消息时,被调用的代码直到运行时才能确定。编译器确保被调用方法存在,并且对调用参数和返回值执行类型检查(若不能提供这类保证,那么这类语言被称为弱类型,如python),但是还不知道即将被执行的确切的代码。
Java实现后期绑定的方法是使用一小段特殊的代码来代替绝对地址调用。这段代码使用在对象中存储的信息来计算方法体的地址,根据这一段代码的内容,编译器可以在运行时定位到需要被调用的方法体。这样,每一个对象都可以有不同的行为表现,并且向对象发送消息,对象也可以知道具体要怎么处理这条消息。
书中具体例子:
基类为Shape,衍生出的子类有Circle等,定义一个方法void doSomething(Shape shape),并且在这个方法中调用Shape类中的方法draw()。那么我们可以进行形如doSomething(circle)的方法调用。doSomething接受的消息是基类Shape的对象,但是我们传入的是由这个基类衍生出的子类Circle类型的对象,这其中的处理过程是向上转型。
向上转型换句话说就是将导出类看作他的基类的过程,这样机制下的方法中并不会有形如对传入参数进行类型判断的条件语句(”如果是Circle,就这么做;如果是Line,就那么做“),若是这样的结构,那么每出现一个新的类型对象,就需要在每一个方法中添加关于这个新类型的处理逻辑,非常繁琐,不利于代码的拓展。向上转型的思想将上述的实现方法转化成了”你是一个Shape类导出的子类的对象,我知道你有Shape类中的方法,但是具体怎么去实现,取决于你“。
无论被传入doSomething(Shape shape)方法的是Shape基类的哪一个导出类,这个方法总能做出正确的反应,在Java编译器在编译这部分代码时,并不能确切的知道方法将要处理的是哪种具体类型,所以通常希望他的编译结果是调用基类Shape的draw()函数,而不是具体到某一个子类的draw()函数。
多态使得这种特性下的功能正确运行,编译器只需要知道某个方法将要被调用,但是具体怎么被调用不重要,重点是怎么通过它进行下一步的程序设计。
向一个对象发送消息时,就会涉及到向上转型,同时这个对象也应该知道自己要怎么正确地处理这个消息,怎么做出正确的行为。
1.8单根继承结构
Java中,所有类都继承自单一的唯一基类Object,这就是所谓的单根继承结构。
单根继承结构中所有对象都具有一个共用接口(C++的结构不保证所有的对象都属于同一个基本类型)。
单根继承结构确保所有对象都会具备某些功能,这样在系统中你可以在每个对象上执行一些基本的操作,从底层上看,所有对象都可以很容易地在堆上创建,极大地简化了参数传递。
这种结构也使得垃圾回收变得更加容易(这也是Java相对于C++的重大改进之一),所有对象都保证具有类型信息,所以不会产生无法确定对象类型的局面,这对于系统级操作非常便利,并且也会提高编程的灵活性。
1.9容器
问题:若在解决某个问题时,不知道需要多少个对象,不知道这些对象会存在多久,那么也就不知道如何存储这些对象,需要多大的内存空间来存储这些对象,这些都是在运行时才会得知的。
OOD语言的大多数处理方法是创建一种新的对象类型,或者使用数组来解决(实际上这个数组的概念就是容器)。
容器,在任何需要的时候都可以扩充自己来容纳置于其中的东西,也就不需要在创建的时候知道会有多少对象放于其中。
为什么要创建不同类型的容器?
首先不同容器提供了不同类型的接口和外部行为(栈和队列的接口存在着不同),提供不同类型的容器会为问题的解决带来更加灵活的方案;其次不同容器对于某些操作会有不同的效率(经典的例子:ArrayList和LinkedList,也就是顺序表和链表)
(1)参数化类型
Java SE5之前,容器存储的对象只有Object,单根继承结构使得这种容器可以存储任何类型的对象,进而也很容易被复用。
但是,在将对象放入容器时,这个对象会被向上转型为Object,也就是会暂时失去他本来的身份,在将其从容器中取回时,获得的是一个Object对象的引用,而不是当初放进去的类型。
这里为了使取出的对象类型还是放进去之前的类型,会涉及到向下转型,需要注意的是,向下转型是不安全的(例如:Circle是一种Shape,这是向上转型,但是Shape就一定是Circle吗?这就是向下转型的不安全性)。所以在进行向下转型时,必须确切的知道要处理对象的类型,否则就会出现异常。
上面说的各种坏处都是因为容器不知道自己保存的对象类型,那么能不能直接搞一个能知道自己保存的对象类型的容器呢?
这种解决方案被称为参数化类型机制,参数化类型就是一个编译器可以自动定制作用于特定类型上的类,换句话说就是编译器可以定制一个只存储某种类型对象的容器。
在Java中,参数化类型被称为范型,创建实例:
ArrayList<Shape> shapes = new ArrayList<Shape>();
1.10对象的创建和生命期
任意一个对象的创建和生存都需要资源(尤其是内存)。所以在我们不再需要某一个对象时,他必须被清理掉,并且释放占用的资源。
对象的数据存放于何处?对象的生命周期怎么控制?不同的语言有不同的处理方式:
C++为了追求代码运行的效率,将以上问题交给编写程序的程序员来选择,在进行程序编写时就必须确定对象的存储空间和生命周期,这可以通过将对象置于堆栈或者静态存储区域来实现,但是这样代码的灵活性会大打折扣;
第二种办法是在名为堆(heap)的内存池中动态地创建对象:直到运行的时候才知道需要多少对象,声明周期如何,具体类型为何。这些问题在相关代码被执行时才会被解答。
在Java中创建对象时,可以直接在堆中创建,堆中的存储空间是在运行时被动态管理的,在堆中分配存储空间的时间开销远大于在栈中创建对象(参考数据结构中栈操作和堆调整算法的复杂程度)。
动态内存分配方式在对象越来越复杂的情况下是“赚”的,相比于动态维护堆内存空间,创建新的更加复杂的对象的开销要更大。Java完全采用了动态内存分配方式。
关于生命周期,若是在堆栈上创建,那么编译器就必须知道这个对象存活的具体时间,并且可以自动将这个对象销毁;相对的,在堆上创建对象,编译器就对对象的生命周期一无所知。这也就是为什么C++中需要有free()这样的函数,若不能正确处理,就可能产生内存泄漏的问题,但是Java提供了“垃圾回收器”机制,会自动发现对象什么时候不会再被使用,并将其销毁,垃圾回收器提供的是更高层的避免暗藏的内存泄漏问题的机制。
(1.27上传,读到P47)