数组(Array)
数组是一种顺序存储的线性表,所有元素的内存地址是连续的。
int[] array = new int[]{11, 22, 33};
当创建完这个数组的时候,堆空间只能放3个元素,以后想再放更多的元素是不可能的。
- 在很多编程语言中,数组都有个致命的缺点,无法动态修改容量。
- 但在实际开发中,我们希望数组的容量是可以改变的
那么如何实现动态数组呢?
我们可以自己设计动态数组,首先类名为ArrayList(注意,这不是java官方的ArrayList,但是这种源码设计和官方差不多,而且更简单能够理解源码的设计思想),然后是动态数组的接口设计。
int size();//元素的数量
boolean isEmpty();//是否为空
boolean contains(int element);//是否包含某个元素
void add(int element);//添加元素到最后面
int get(int index);//返回Index位置对应的元素
int set(int index,int element);//设置index位置的元素
void add(int index,int element);//往index位置添加元素
int remove(int index);//删除Index位置对应的元素
int indexOf(int element);//查看元素的位置
void clear();//清楚所有元素
动态数组设计
一个动态数组里面应该有哪些成员变量呢?
private int size; // 元素的数量
private int[] elements; // 所有的元素
设计动态数组的容量
private static final int DEFAULT_CAPACITY = 10; // 初始容量
private static final int ELEMENT_NOT_FOUND = -1;
public ArrayList(int capacity) { // 容量小于10一律扩充为10
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = new int[capacity];
}
public ArrayList(){
this(DEFAULT_CAPACITY);
}
简单接口的实现
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
public boolean contains(int element){
return indexOf(element) != ELEMENT_NOT_FOUND; // 找的到该元素则返回True
}
//获取index位置的元素
public int get(int index){
if(index<0||index>=size){//采用抛出异常更好
throw new IndexOutOfBoundsException("Index:"+index+", size:"+size);
}
return elements[index];
}
}
//设置index位置的元素
public int set(int index){
if(index<0||index>=size){//采用抛出异常更好
throw new IndexOutOfBoundsException("Index:"+index+", size:"+size);
}
int old = elements[index];//把原来的元素取出来
elements[index] = element;//用新的元素覆盖
return old;//返回原来的元素
}
}
//查看元素的索引
public int indexOf(int element){
for (int i = 0; i < size; i++) {
if(elements[i]==element) return i;
}
return ELEMENT_NOT_FOUND;//ELEMENT_NOT_FOUND为常量-1
}
//清楚所有元素
public void clear(){
size = 0;//在使用者看来是清除了,我们设计者看来没有,并且内存没有浪费,因为还要利用
}
//添加元素到数组最后
public void add(int element){
elements[size++] = element;
}
//封装功能函数
public String toString() {
// 打印形式为: size=5, [99, 88, 77, 66, 55]
StringBuilder string = new StringBuilder();
string.append("size=").append(size).append(", [");
for (int i = 0; i < size; i++) {
if(0 != i) string.append(", ");
string.append(elements[i]);
}
string.append("]");
return string.toString();
}
//删除某个元素
public int remove(int index){
if(index<0||index>=size){//采用抛出异常更好
throw new IndexOutOfBoundsException("Index:"+index+", size:"+size);
}
int old = elements[index];
for (int i = index; i < size - 1; i++) {// // 从前往后开始移, 用后面的元素覆盖前面的元素
elements[i-1] = elements[i];
}
size--; // 删除元素后, 将最后一位设置为null
return old;
}
//在index位置插入一个元素
public void add(int index, int element){
if(index<0||index>size){//这里index允许等于size
throw new IndexOutOfBoundsException("Index:"+index+", size:"+size);
}
// 先从后往前开始, 将每个元素往后移一位, 然后再赋值
for (int i = size - 1; i > index; i--) {
elements[i + 1] = elements[i];
}
elements[index] = element; // 复制
size++;
}
到此基本封装完毕,可能有人会疑惑动态数组内部new出来的数组内存几时释放?
new是向堆空间申请内存,一旦使用完毕,java的垃圾回收机制会自动回收。
ArrayList list = new ArrayList();
现在我们来进行接口测试
public static void main(String[] args) {
//new是向堆空间申请内存
ArrayList list = new ArrayList();
list.add(99);
list.add(88);
list.add(77);
list.add(66);
list.add(55);
list.set(3,80);
System.out.println(list);
}
测试结果表明接口功能正常
扩容动态数组
当我们向数组中添加元素时,如果数组已经满了我们需要对数组进行动态扩容。那么
如何扩容呢?难道在数组后面拼接一段内存?这是不允许的,没有这种操作。
因为我们申请内存的时候要用到new,而new申请内存返回的地址是随机的,这就意味着如果你再申请一段内存,地址很有可能不在数组后面,可以在任意处。
如果想要保持一个连续的空间,那就是申请一块更大的内存而且是连续的,将原来的数组挪到新的内存,原来的内存自动回收。如何实现?
写一个确保容量的私有方法ensureCapacity(int capacity)
//保证要有capacity的容量
private void ensureCapacity(int capacity){
int oldCapacity = elements.length;//现在的容量就是数组的长度
if(oldCapacity >= capacity) return;//现在的容量大于等于至少需要的容量,就不需要扩容
// 扩容操作
int newCapacity = oldCapacity + (oldCapacity >> 1);// 新容量为旧容量的1.5倍
int[] newElements = new int[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i]; // 拷贝原数组元素到新数组
}
elements = newElements;
System.out.println("size="+oldCapacity+", 扩容到了"+newCapacity);
}
// System.arraycopy()是系统自身调用的方法,为了便于理解扩容的本质,我这里就不采用这个方法
测试下能否保证容量,为了便于测试,这里初始容量设为2即DEFAULT_CAPACITY = 2,添加5个数据,运行结果如下
泛型数组
前面我们所设计的动态数组只针对int类型的数据进行添加,但是如果想要其它类型也能添加进来,我们可以将相应的数据类型设置为泛型E。相应的类型进行强制转换,例如在ArrayList(int capacity)方法里进行修改
public ArrayList(int capacity) { // 容量小于10一律扩充为10
capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
elements = (E[])new Object[capacity];//所有的类都继承Object,强制转换为泛型
}
假设添加3个对象为Person的数据,测试对象是否添加进来
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person(10,"张三"));
list.add(new Person(20,"李四"));
list.add(new Person(30,"王五"));
System.out.println(list);
}
测试结果如下
注意:使用泛型数组后要注意内存管理。
对象数组
如果new的是一个Object数组,那就是对象数组,对象数组内存里面放的不是对象本身,而是对象的引用(地址),这样可以节省空间。
Object[] objects = new Object[7];
Object[0] = new Person(10,"老李");
如果想销毁数组里的Person对象,该如何实现?
public void clear(){
// 使用泛型数组后要注意内存管理(将元素置null)
for (int i = 0; i < size; i++) {
elements[i] = null;//将数组里指向对象的地址全部清空
}
size = 0;//不要设置成elements=null;
}
对象销毁了,对象的内存自然也就没了,但是数组的内存不能去销毁,将来要放新对象的地址。
正所谓能循环利用的留下,不能循环利用的滚蛋。。。
如何证明Person对象销毁了?在java中有一个方法为finalize(),可以在这个方法里写下对象的遗言,当能调用这个方法说明这个对象要挂了。
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Person - finalize");
}
为了便于看到效果,另外我们需要提醒jvm进行垃圾回收
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person(10,"张三"));
list.add(new Person(20,"李四"));
list.add(new Person(30,"王五"));
list.clear();
//提醒JVM进行垃圾回收
System.gc();
}
运行结果如下,可以看到添加的3个对象均挂了。
总结
对比官方的ArrayList源码,会发现和本文实现的动态数组在增删改查功能上基本一致,除了Iterator(迭代器),这是设计模式的内容。