面向对象的一个重要目标是对代码重用的支持。支持这个目标的一个重要的机制就是泛型机制:如果除去对象的基本类型外,实现的方法是相同的,那么我们就可以用泛型实现来描述这种基本的功能。
1.使用Object表示泛型
Java中的基本思想就是可以通过使用像Object这样超类来实现泛型类。
示例一:
(1)编写MemoryCell.java
package cn.pre.example; public class MemoryCell { private Object storedValue; public Object rend() { return storedValue; } public void write(Object x) { storedValue=x; } }
当我们使用这种策略时,有两个细节必须要考虑。第一个细节(2)中阐释,它描述一个main方法,该方法把“37”写到MemoryCell对象中,然后由从MemoryCell对象读出。为了访问这种对象的一个特定方法,必须要强制转换成正确的类型(当然了,在这个例子中可以不必强制转换,因为在程序中可以调用toString()方法。这种调用对任何对象都是能够做到的)。
(2)编写TestMemoryCell并运行该类
package cn.pre.example; import java.lang.management.MemoryNotificationInfo; import javax.swing.event.MenuDragMouseEvent;
/**
* 使用泛型MemoryCell类
* @author youcong
* @date 2018年12月25日20点19分
*/
public class TestMemoryCell { public static void main(String[] args) { MemoryCell m = new MemoryCell(); m.write("37"); String val = (String) m.rend(); System.out.println("Content are:"+val);
// System.out.println("Content are:"+m.rend().toString());
} }
第二个重要细节是不能使用基本类型。只有引用类型能够与Object相容。
说到这,回顾一下Java的基础知识,基本类型有哪些?引用类型有哪些?基本类型和引用类型的区别是?
基本类型:byte、short、int、long、double、float、boolean、char。
引用类型:Object、数组、String和枚举等。
基本类型和引用类型的区别是:基本数据类型和引用类型的区别主要在于基本数据类型是分配在栈上的,而引用类型是分配在堆上的。
那么什么是堆?什么是栈?
堆(heap):是一个可动态申请的内存空间(其记录空闲内存空间的链表由操作系统维护).
在java中,所有使用new xxx()构造出来的对象都在堆中存储,当垃圾回收器检测到某对象未被引用,则自动销毁该对象.所以,理论上说java中对象的生存空间是没有限制的,只要有引用类型指向它,则它就可以在任意地方被使用.
栈(stack):是一个先进后出的数据结构,通常用于保存方法(函数)中的参数,局部变量.
在java中,所有基本类型和引用类型都在栈中存储.栈中数据的生存空间一般在当前scopes内(就是由{...}括起来的区域).
下面的示例二(可以很好的说明这一点):
package cn.pre.example; import java.awt.HeadlessException; public class HeapStackExample { public static void stackTest() {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1==s2);
} public static void heapTest() { String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1==s2);
} public static void main(String[] args) { HeapStackExample.stackTest();
HeapStackExample.heapTest();
} }
在Java中有六个不同的地方可供存数据:
(1) 寄存器(register)。这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部。但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配。你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象。
(2)堆栈(stack)。位于通用RAM中,但通过它的“堆栈指针”可以从处理器哪里获得支持。堆栈指针若向下移动,则分配新的内存;若向上移动,则释放那些
内存。这是一种快速有效的分配存储方法,仅次于寄存器。创建程序时候,JAVA编译器必须知道存储在堆栈内所有数据的确切大小和生命周期,因为它必须生成
相应的代码,以便上下移动堆栈指针。这一约束限制了程序的灵活性,所以虽然某些JAVA数据存储在堆栈中——特别是对象引用,但是JAVA对象不存储其
中。
(3)堆(heap)。一种通用性的内存池(也存在于RAM中),用于存放所以的JAVA对象。堆不同于堆栈的好处是:编译器不需要知道要从堆里分配多少存储区
域,也不必知道存储的数据在堆里存活多长时间。因此,在堆里分配存储有很大的灵活性。当你需要创建一个对象的时候,只需要new写一行简单的代码,当执行
这行代码时,会自动在堆里进行存储分配。当然,为这种灵活性必须要付出相应的代码。用堆进行存储分配比用堆栈进行存储存储需要更多的时间。
(4)静态存储(static storage)。这里的“静态”是指“在固定的位置”。静态存储里存放程序运行时一直存在的数据。你可用关键字static来标识一个对象的特定元素是静态的,但JAVA对象本身从来不会存放在静态存储空间里。
(5)常量存储(constant storage)。常量值通常直接存放在程序代码内部,这样做是安全的,因为它们永远不会被改变。有时,在嵌入式系统中,常量本身会和其他部分分割离开,所以在这种情况下,可以选择将其放在ROM中 。
(6)非RAM存储。如果数据完全存活于程序之外,那么它可以不受程序的任何控制,在程序没有运行时也可以存在。
2.基本类型的包装
当我们实现算法的时候,常常遇到语言定型问题:我们已有一种类型的对象,可是语言的语法却需要一种不同类型的对象。
这种技巧阐释了包装类的基本主题。一种典型的用法是存储一个基本的类型,并添加一些这种基本类型不支持或不能正确支持的操作。
在Java中我们已经看到,虽然每一个引用类型都和Object相容,但是,8种基本类型却不能。于是,Java为这8种基本类型中的每一种都提供了一个包装类。
例如:int类型的包装是Integer。每一个包装对象都是不可变的(就是说它的状态绝不能改变),它存储一种当该对象被构建时所设置的原值,并提供一种方法以重新得到该值。包装类也包含不少的静态使用方法。
示例三(如何能够使用MemoryCell来存储整数):
package cn.pre.example;
/**
* 基本类型包装类
* @author youcong
* @date 2018年12月25日20点19分
*/
public class WrapperDemo { public static void main(String[] args) { MemoryCell m = new MemoryCell();
m.write(new Integer(37));
Integer wrapperVal = (Integer) m.rend();
int val = wrapperVal.intValue();
System.out.println("Contents are:"+val);
} }
基本数据对应的包装类:
byte -> Byte
short ->Short
int -> Integer
long ->Long
double ->Double
float ->Float
char ->Character
boolean -> Boolean
3.使用接口类型表示泛型
只有在使用Object类中已有的那些方法能够表示所执行的操作的时候,才能使用Object作为泛型类型来工作。
例如,考虑在由一些项组成的数组中找出最大项的问题。基本的代码是类型无关的,但是它的确需要一种能力来比较任意两个对象,并确定哪个是大的,哪个是小的。因此,我们不能直接找出Object的数组中的最大元素。最简单的想法是找出Comparable的数组中最大元。要确定顺序,可以使用compareTo()方法,我们知道,它对所有的Comparable都必然是现成可用的。
现在,提出几个忠告很重要。首先,只有实现Comparable接口的那些对象才能够作为Comparable数组的元素被传递。仅有compareTo()方法但并未宣称实现Comparable接口的对象不是Comparable的,它不具有必需的IS-A关系。因为我们也许会比较两个Shape的面积,因此假设Shape实现Comparable接口。
第二,如果Comparable数组有两个不相容的对象(例如一个String和一个Shape),那么Comparable方法将抛出异常ClassCastException(类型转换异常)。
第三,如前所述,基本类型不能作为Comparable传递,但是包装类则可以,因为它们实现了Comparable接口。
第四,接口究竟是不是标准的库接口倒不是必需的。
最后,这个方案不是总能够行得通,因为有时宣称一个类实现所需的接口是不可能的。例如,一个类可能是库中的类,而接口却是用户定义的接口。如果一个类是final类,那么我们就不可能扩展它以创建一个新的类。
4.数组类型的兼容性
语言设计中的困难之一是如何处理集合类型的继承问题。设Employee IS-A Person。那么,这是不是意味着数组Employee[] IS -A Person[]呢?换句话说,如果一个例程接受Person[]作为参数,那么我们能不能把Employee[]作为参数来传递呢?
咋看,该问题不值得一问,似乎Employee[]就应该是和Person[]类型兼容的。然而这个问题比想象中要复杂。假设除Employee外,我们还有Studeng IS-A Person,并设Employee[]是和Person[]类型兼容的。此时考虑下面两条赋值语句:
Person[] arr = new Employee["5];//编译:arrays are compatible arr[0]= new Student(...);//编译: Studeng IS-A Person
两句都编译,而arr[0]实际上是引用一个Employee,可是Student IS-NOT-A Employee。这样就产生了类型混乱。运行时系统(runtime system)不能抛出ClassCastException异常,因为不存在类型转换。
避免这种问题的最容易的方法是指定这些数组不是类型兼容的。可是,在Java中数组却是兼容的。这叫做协变数组类型。每个数组都明了它所允许存储的对象的类型。
如果将一个不兼容的类型插入到数组中,那么虚拟机将抛出ArrayStoreException异常(数组存储异常)。
示例代码我已经提交到我的Github,有需要可克隆下载。
代码地址为:https://github.com/youcong1996/The-Data-structures-and-algorithms/tree/master/Introduction