数组是最差的数据结构之一。因为不能调整大小,数组总是要面临越界或者内存占用过多的问题。因为其有序的存储方式,插入和删除又有很大不便。
数组列表就很有用了,保留了数组原来的特点(即,可以通过索引直接访问),又可以动态地调整大小,从而避免数组越界或者内存浪费的问题。
“动态”是一个骗局!数组列表的内部,肯定是一个不能调整大小 的数组。 要“调整”数组大小,只能是另外再创建一个数组,替换原来的数组。 动态就是这样实现的。所以,频繁地调整容器的大小会降低效率。
我们给一个普普通通的数组穿上外套之后,就可以骗别人说“这是个动态的容器”了
JVM 会回收已弃用的数组所占用的空间。
我们要在数组列表类的内部创建一个数组来存放元素。这个数组应该是什么类型的呢?我们可以把它声明为 Object [ ] ,这样,数组列表就可以存储任何类型的数据了( 不能存放基本数据类型的数据;应该使用对应的封装类 )。
但是,有时候我们需要一个只能存储特定类型数据 的数组列表。这时候,可以将数组列表类定义为泛型类。泛型就是将数据类型作为可变参数。 下面是泛型类的使用示例,其中的 Generics 代表一种类(不能代表基本数据类型):
class MyClass<Generics> {
private Generics variable;
public setVariable(Generics v) {
// variable = new Generics(); // 不可以
variable = v;
}
public static void main(String[] args) {
// MyClass object = new MyClass(); 也可以
MyClass<Integer> object1 = new MyClass<>(); //1
MyClass object2 = new MyClass<>(); //2
}
可以在实例化对象的时候将对象的泛型指定为实际的类,如语句1,这有点像是传递参数;也可以不指定为实际的类,如语句2,这时候,类中的 Generics 可以表示任意的类。
多数情况下,泛型看起来和类一样。然而, 不能实例化泛型的对象,也不能创建泛型的对象数组。 所以数组列表类中的数组还是只能声明为 Object [ ] 。不过我们可以在类方法中使用泛型对象作为参数,从而限制数组列表中的对象类型。
属性名称 | 属性类型 | 说明 |
---|---|---|
data | Object [ ] | 实际上的 容器 |
size | int | 记录数组列表实际存放了多少元素 |
这些属性都应该是私有的。因为,只有当通过数组列表的方法来进行操作时,数组列表才真正地表现为“动态”。所以,不应该在类外直接访问上述属性。如果把上述属性设为公有,那么这些属性可能会被没有睡醒的人无意中修改掉。
后面再说。 不过我觉得,真正在思考的人,应该早就知道为什么需要 size 。
创建、获取元素数量、在末尾添加元素、在指定位置添加元素、访问指定的元素、更改指定的元素、删除指定的元素 / 元素区间 等等。
在下文中,我把这个类叫做 “MyArrayList
方法名 | 返回值类型 | 参数列表 | 说明 |
---|---|---|---|
构造方法 | 不可用 | — | 初始化 data ,使其大小合适 |
构造方法 | 不可用 | int length |
初始化 data ,将其大小设为 length |
size |
int |
— | 返回 size |
get |
T |
int index |
返回索引为 index 的对象 |
add |
void |
T obj |
在数组末尾增加对象 obj |
add |
boolean |
T obj, int index |
把原来的某些元素向后移动,空出 索引为 index 的位置,然后插入 obj , 插入成功则返回 true |
addList |
boolean |
MyArrayList int index |
把原来的某些元素向后移动,空出足够 的位置,然后在索引为 index 的位置 插入 array ,插入成功则返回 true |
change |
boolean |
int index, T obj |
将指定位置的元素更改为 obj , 更改成功则返回 true |
changeAll |
int |
T old, T intention |
把与 old 相同的元素更改为intention, 返回更改的个数 |
remove |
T |
int index |
删除指定位置的元素, 返回被删除的元素 |
removeAll |
int |
T target |
删除与obj相同的元素, 返回删除的个数 |
remove |
boolean |
int left, int right |
删除闭区间 [left, right] 中的元素, 删除成功则返回 true |
上述方法需要在类外甚至包外使用,应设为公有。
应该设置一个不大不小的值。 如果默认大小太大,会占用不必要的内存;如果默认大小太小,以后可能需要频繁地扩容数组,降低效率。
不是。从默认构造方法就可以看出来,我们还没有调用 add() 方法时,data.length 就大于 0 了;但是这时候 data 数组中已有的数据并不是我们真正想要存放的数据。因此,我们需要一个用来记录“有效数据”数量的变量。
这也就是为什么我们要定义一个 size 属性。我们通过 size 属性而不是 data.length 来确定存放了多少元素; 数组 data 中常常会有几个 “空位”。(此处及下文所说的“空位”,是指 data 数组中多余的、可以删去的空间。这些空间并不是真正的“空”,它们中还存放着一些数据,然而,公有方法不允许从外部访问它们,从类外看来,它们是不存在的。)
我们完全可以 “动态” 地调整容器大小,保证 data 的大小完全等于所需的大小。如果这样做,每次添加元素,我们都要把容器扩大一次;每次删除元素,我们都要缩小容器。每次增删都要改变容器大小,效率太低。所以我们选择让 data 留有少许 “空位” ,虽然会浪费一些空间,但是可以减少改变容器大小的次数,从而保证效率。
有一些代码是要我们重复写数次的。比如说,在删除指定的元素时,我们要判断符合条件的元素是否存在;还有,在添加元素之前,要检查内部的数组是否足够大。
不妨把要重复使用的代码也写成方法,代码重用得越多,我们就越轻松,代码的可读性也会有所提升。
方法名 | 返回值类型 | 参数列表 | 说明 |
---|---|---|---|
accessible |
boolean |
int index |
如果 index 处存放了数据,返回 true |
full |
boolean |
int addition |
如果添加了 addition 个元素之后, data 数组空间不足,返回 true |
space |
boolean |
int index |
如果 data 数组有索引为 index 的 元素,返回 true |
resize |
void |
int size |
将 data 数组大小改为 size |
moveLeft |
void |
int left, int right, int step |
将闭区间 [left, right] 中的元素向左 移动 step 个单位(覆盖原有元素) |
moveRight |
void |
int left, int right, int step |
将闭区间 [left, right] 中的元素向右 移动 step 个单位(覆盖原有元素) |
私有。不应该在类外访问上面这些方法。
可以一个一个地扩大。
不过,在需要大量的元素时,一个一个地扩大效率太低了。因此我在扩大数组的时候,每次扩大都让数组大小增加一倍。这样可能会浪费一些空间,不过不必频繁地扩大了。 要扩大多少取决于喜好。
有必要缩小数组。我们选用数组列表的目的之一就是减少内存空间的浪费,所以如果有机会缩小数组,应该缩小数组。
可以在 data 数组有大量 “空位” 时缩小数组。我的做法是,当 “空位” 数量大于 data.length 的一半时,缩小数组到原来的一半。要缩小多少、什么时候缩小取决于喜好。
因为我不但代码写得差,语文水平还很臭。
public class MyArrayList<T> {
private Object[] data;
private int size;
public MyArrayList() {
data = new Object[10];
}
public MyArrayList(int length) {
data = new Object[length];
}
private boolean accessible(int index) {
return index > -1 && index < size;
}
private boolean full(int addition) {
return size + addition > data.length;
}
private boolean space(int index) {
return index > -1 && index < data.length;
}
private void resize(int size) {
Object[] r = new Object[size];
int right = size < this.size ? size : this.size;
for (int i = 0; i < right; i++) {
r[i] = data[i];
}
data = r;
System.out.println('=');
}
private void moveLeft(int left, int right, int step) {
if (left <= right && space(left - step) && space(right)) {
for (int i = left; i <= right; i++) {
data[i - step] = data[i];
}
}
private void moveRight(int left, int right, int step) {
if (left <= right && space(right + step) && space(left)) {
for (int i = right; i >= left; i--) {
data[i + step] = data[i];
}
}
public int size() {
return size;
}
public T get(int index) {
return accessible(index) ? (T) data[index] : null;
}
public void add(T obj) {
if (full(1)) {
resize(2 * data.length);
}
data[size++] = obj;
}
public String toString() {
StringBuilder r = new StringBuilder();
for (int i = 0; i < size; i++) {
T t = (T) data[i];
r.append(t instanceof MyArrayList ? "A MyArrayList" : t);
r.append('\t');
}
r.append('\n');
return r.toString();
}
public boolean add(T obj, int index) {
if (accessible(index) || index == size) {
if (full(1)) {
resize(2 * data.length);
}
moveRight(index, size - 1, 1);
data[index] = obj;
size++;
return true;
}
return false;
}
public boolean addList(MyArrayList<T> array, int index) {
if (accessible(index) || index == size) {
if (full(array.size)) {
resize(array.size + size);
}
moveRight(index, size - 1, array.size);
for (int i = 0; i < array.size; i++) {
data[i + index] = array.data[i];
}
size += array.size;
return true;
}
return false;
}
public T remove(int index) {
if (accessible(index)) {
T old = (T) data[index];
moveLeft(index + 1, --size, 1);
return old;
}
return null;
}
public int removeAll(T target) {
int counter = 0;
for (int i = 0; i < size; i++) {
if (data[i].equals(target)) {
moveLeft(i + 1, --size, 1);
counter++;
}
}
return counter;
}
public boolean remove(int left, int right) {
if (accessible(left) && accessible(right)) {
int number = right - left + 1;
moveLeft(right + 1, size - 1, number);
size -= number;
if (size < data.length / 2) {
resize(data.length / 2);
}
return true;
}
return false;
}
public boolean change(int index, T obj) {
if (accessible(index)) {
data[index] = obj;
return true;
}
return false;
}
public int changeAll(T old, T intention) {
int counter = 0;
for (int i = 0; i < size; i++) {
if (data[i].equals(old)) {
data[i] = intention;
counter++;
}
}
return counter;
}
}