ArrayList implemented using JavaScript sparse arrays for good insertion/deletion performance at either end.  This code also makes ArrayList the primary list type, with Vector delegating to list.

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@379 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/super/com/google/gwt/emul/java/util/ArrayList.java b/user/super/com/google/gwt/emul/java/util/ArrayList.java
index 62c923c..18e6415 100644
--- a/user/super/com/google/gwt/emul/java/util/ArrayList.java
+++ b/user/super/com/google/gwt/emul/java/util/ArrayList.java
@@ -15,88 +15,261 @@
  */
 package java.util;
 
-/**
- * Implements a list backed by an array.
- */
-public class ArrayList extends AbstractList implements List, RandomAccess,
-    Cloneable {
+import com.google.gwt.core.client.JavaScriptObject;
 
-  private Vector vec;
+/**
+ * See Sun's JDK 1.4 documentation for documentation.   
+ * 
+ * Differences between this implementation and JDK 1.4 <code>ArrayList</code>
+ * include capacity management and range checking.
+ * <p>
+ * <b>Capacity</b> There is no speed advantage to pre-allocating array sizes in
+ * JavaScript, so this implementation does not include any of the capacity and
+ * "growth increment" concepts in the standard ArrayList class.
+ * </p>
+ * <p>
+ * <b>Dual endedness</b> For increased performance, this implementation supports
+ * constant time insertion and deletion from either end.
+ * </p>
+ */
+public class ArrayList extends AbstractList implements List, Cloneable,
+    RandomAccess {
+  /*
+   * Implementation notes:  
+   *   Currently if one uses an ArrayList as a ring buffer, adding from one end,
+   *   and deleting from the other, the indexes will increase (or decrease) 
+   *   without ever being normalized.  Back of the envelope calculations 
+   *   indicate that at 30 indexes per second, it will take a year of solid run
+   *   time to chew through the billion indexes needed to get into trouble.
+   *   Given that it seemed better not to rebalance than to charge everyone the
+   *   extra code size the rebalancing code would represent. 
+   */
+
+  protected static boolean equals(Object a, Object b) {
+    return (a == null ? b == null : a.equals(b));
+  } 
+  /** 
+   * This field holds the javascript array, and is not private to avoid Eclipse
+   * warnings. 
+   */
+  JavaScriptObject array;
+  /** 
+   * This field holds the last populated index of the array and is not private
+   * to avoid Eclipse warnings.
+   */
+  int endIndex;
+  /** 
+   * This field holds the first populated index of the array and is not private
+   * to avoid Eclipse warnings.
+   */
+  int startIndex;
 
   public ArrayList() {
-    vec = new Vector();
+    initArray();
   }
 
   public ArrayList(Collection c) {
-    vec = new Vector();
+    initArray();
     addAll(c);
   }
 
-  public void add(int index, Object o) {
-    vec.add(index, o);
-  }
+  public native void add(int index, Object o) /*-{
+    var array  = this.@java.util.ArrayList::array;
+    var endIndex = this.@java.util.ArrayList::endIndex;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    // This if is not an else if to call attention to the early return.
+    if (index + startIndex == endIndex) {
+      // If we are at the end simply set the next element to hold the value.
+      array[endIndex] = o;
+      this.@java.util.ArrayList::endIndex++;
+      return;
+    }
+    if (index == 0) {
+      // If we are adding at the beginning, simply set the new element, and 
+      // move the beginning back.
+      array[--this.@java.util.ArrayList::startIndex] = o;
+      return;
+    }
+    
+    // Somewhere in the middle, so do range checking and the splice.
+    // Range checking, must be more permissive since one can add off the end.
+    this.@java.util.ArrayList::verifyIndexOneExtra(I)(index);
+    array.splice(index + startIndex, 0, o);
+    // The end of the array moved forward if we got here so record that.
+    this.@java.util.ArrayList::endIndex++;
+  }-*/;
 
   public boolean add(Object o) {
-    return vec.add(o);
+    add(size(),o);
+    return true;
   }
-
-  public boolean addAll(Collection c) {
-    return vec.addAll(c);
-  }
-
-  public boolean addAll(int index, Collection c) {
-    return vec.addAll(index, c);
-  }
-
+ 
   public void clear() {
-    vec.clear();
+    setSize(0);
   }
-
+  
   public Object clone() {
     return new ArrayList(this);
   }
 
-  public boolean contains(Object elem) {
-    return vec.contains(elem);
+  public boolean contains(Object o) {
+    return (indexOf(o) != -1);
+  }
+ 
+  public native Object get(int index) /*-{
+    this.@java.util.ArrayList::verifyIndex(I)(index);
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    return this.@java.util.ArrayList::array[index + startIndex];
+  }-*/;
+  
+  public int indexOf(Object o) {
+    return indexOf(o, 0);
   }
 
-  public Object get(int index) {
-    return vec.get(index);
-  }
-
-  public int indexOf(Object elem) {
-    return vec.indexOf(elem);
-  }
-
-  public boolean isEmpty() {
-    return (vec.size() == 0);
-  }
-
-  public Iterator iterator() {
-    return vec.iterator();
-  }
+  public native boolean isEmpty() /*-{
+    return (this.@java.util.ArrayList::endIndex == this.@java.util.ArrayList::startIndex);
+  }-*/;
 
   public int lastIndexOf(Object o) {
-    return vec.lastIndexOf(o);
+     return lastIndexOf(o, size() - 1);
   }
 
   public Object remove(int index) {
-    return vec.remove(index);
+    Object old = get(index);
+    removeRange(index,index + 1);
+    return old;
+  } 
+
+  public boolean remove(Object o) {
+    int i = indexOf(o);
+    if (i == -1) {
+      return false;
+    }
+    remove(i);
+    return true;
   }
 
-  public Object set(int index, Object elem) {
-    return vec.set(index, elem);
-  }
+  public native Object set(int index, Object o) /*-{
+    this.@java.util.ArrayList::verifyIndex(I)(index);
+    var array = this.@java.util.ArrayList::array;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    var old = array[index + startIndex];
+    array[index + startIndex] = o;
+    return old;
+  }-*/;
 
-  public int size() {
-    return vec.size();
+  public native int size() /*-{
+    return this.@java.util.ArrayList::endIndex - this.@java.util.ArrayList::startIndex; 
+  }-*/;
+  
+  protected native void removeRange(int fromIndex, int toIndex) /*-{
+    this.@java.util.ArrayList::verifyIndexOneExtra(I)(fromIndex);
+    this.@java.util.ArrayList::verifyIndexOneExtra(I)(toIndex);
+    var array = this.@java.util.ArrayList::array;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    var endIndex = this.@java.util.ArrayList::endIndex;
+    if (fromIndex == 0) {
+      // Chop off the beginning.
+      for (var i = startIndex; i < (toIndex + startIndex); i++) {
+        delete array[i];
+      }
+      this.@java.util.ArrayList::startIndex += (toIndex - fromIndex);
+    } else if (toIndex + startIndex == endIndex) {
+      // Chop off the end.
+      for (var i = (fromIndex + startIndex); i < endIndex; i++) {
+        delete array[i];
+      }
+      this.@java.util.ArrayList::endIndex = (fromIndex + startIndex);
+    } else {
+      // Splice from the middle.
+      var numToRemove = toIndex - fromIndex;
+      array.splice(fromIndex + startIndex, numToRemove);
+      this.@java.util.ArrayList::endIndex -= numToRemove;
+    }
+  }-*/;
+  
+  native int indexOf(Object o, int index) /*-{
+    var array = this.@java.util.ArrayList::array;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    var i = index + startIndex;
+    var endIndex = this.@java.util.ArrayList::endIndex;            
+    while (i < endIndex) {
+      if (@java.util.ArrayList::equals(Ljava/lang/Object;Ljava/lang/Object;)(array[i],o)) {
+        return i - startIndex;
+      }
+      ++i;
+    }
+    return -1;
+  }-*/;
+  
+  /**
+   * Throws an <code>indexOutOfBoundsException</code>, and is not 
+   * private to avoid eclipse warnings.
+   */
+  void indexOutOfBounds(int i) {
+    throw new IndexOutOfBoundsException("Size: " + this.size() + " Index: " + i);
   }
+  
+  /**
+   * Computes the last index of an element given an offset, and is
+   * not private to avoid eclipse warnings.
+   */
+  native int lastIndexOf(Object o, int index) /*-{
+    var array = this.@java.util.ArrayList::array;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    var i = index + startIndex;
+    while (i >= startIndex) {
+      if (@java.util.ArrayList::equals(Ljava/lang/Object;Ljava/lang/Object;)(array[i],o)) {
+        return i - startIndex;
+      }
+      --i;
+    }
+    return -1;
+  }-*/;
 
-  public Object[] toArray() {
-    return vec.toArray();
-  }
+  /**
+   * This function sets the size of the array, and is used in Vector.
+   */
+  native void setSize(int newSize) /*-{
+    // Make sure to null fill any newly created slots (otherwise,
+    // get() can return 'undefined').
+    var endIndex = this.@java.util.ArrayList::endIndex;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    var array = this.@java.util.ArrayList::array;
+    var newEnd = newSize + startIndex;
+    for (var i = endIndex; i < newEnd; ++i) {
+      array[i] = null;
+    }
 
-  protected void removeRange(int fromIndex, int endIndex) {
-    vec.removeRange(fromIndex, endIndex);
-  }
-}
+    // Also make sure to clean up orphaned slots (or we'll end up
+    // leaving garbage uncollected).
+    for (var i = endIndex - 1; i >= newEnd; --i) {
+      delete array[i];
+    }
+    this.@java.util.ArrayList::endIndex = newEnd;
+  }-*/;
+  
+  native void verifyIndex(int index) /*-{
+    var endIndex = this.@java.util.ArrayList::endIndex;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    if (index < 0 || index + startIndex >= endIndex) {
+      this.@java.util.ArrayList::indexOutOfBounds(I)(index);
+    }
+  }-*/;
+  
+  native void verifyIndexOneExtra(int index) /*-{
+    var endIndex = this.@java.util.ArrayList::endIndex;
+    var startIndex = this.@java.util.ArrayList::startIndex;
+    if (index < 0 || index + startIndex > endIndex) {
+      this.@java.util.ArrayList::indexOutOfBounds(I)(index);
+    }
+  }-*/;
+ 
+  private native void initArray() /*-{
+    this.@java.util.ArrayList::array = new Array();
+    var HALFWAY_INDEX = 1000000000; // Halfway through the address space
+    // Javascript arrays are sparse, so this wastes no space
+    this.@java.util.ArrayList::startIndex = HALFWAY_INDEX;
+    this.@java.util.ArrayList::endIndex = HALFWAY_INDEX;
+  }-*/;
+}
\ No newline at end of file
diff --git a/user/super/com/google/gwt/emul/java/util/Vector.java b/user/super/com/google/gwt/emul/java/util/Vector.java
index cc0e218..774ad4f 100644
--- a/user/super/com/google/gwt/emul/java/util/Vector.java
+++ b/user/super/com/google/gwt/emul/java/util/Vector.java
@@ -16,70 +16,54 @@
 package java.util;
 
 /**
- * Differences include capacity management and range checking.
- * <p>
- * <b>Capacity</b> There is no speed advantage to pre-allocating array sizes in
- * JavaScript, so this implementation does not include any of the capacity and
- * "growth increment" concepts in the standard Vector class.
- * </p>
- * <p>
- * <b>Range checking</b> For increased performance, this implementation does
- * not check for index validity.
- * </p>
+ * To keep performance characteristics in line with Java community expectations, 
+ * <code>Vector</code> is a wrapper around <code>ArrayList</code>.  
+ *
  */
-public class Vector extends AbstractList implements List, Cloneable,
-    RandomAccess {
+public class Vector extends AbstractList implements List, RandomAccess,
+    Cloneable {
 
-  protected static boolean equals(Object a, Object b) {
-    return (a == null ? b == null : a.equals(b));
-  }
+  private ArrayList arrayList;
 
   public Vector() {
-    initArray();
+    arrayList = new ArrayList();
   }
 
   public Vector(Collection c) {
-    initArray();
+    arrayList = new ArrayList();
     addAll(c);
   }
 
-  public native void add(int index, Object o) /*-{
-    var a = this.array;
-    this.array = a.slice(0, index).concat(o, a.slice(index)); 
-  }-*/;
+  public void add(int index, Object o) {
+    arrayList.add(index, o);
+  }
 
-  public native boolean add(Object o) /*-{
-    var a = this.array;
-    a[a.length] = o;
-    return true; 
-  }-*/;
+  public boolean add(Object o) {
+    return arrayList.add(o);
+  }
 
   public boolean addAll(Collection c) {
-    return super.addAll(c);
+    return arrayList.addAll(c);
   }
 
   public boolean addAll(int index, Collection c) {
-    return super.addAll(index, c);
+    return arrayList.addAll(index, c);
   }
 
   public void addElement(Object o) {
     add(o);
   }
 
-  public native void clear() /*-{
-    this.array.length = 0;
-  }-*/;
+  public void clear() {
+    arrayList.clear();
+  }
 
   public Object clone() {
     return new Vector(this);
   }
 
-  public boolean contains(Object o) {
-    return (indexOf(o) != -1);
-  }
-
-  public boolean containsAll(Collection c) {
-    return super.containsAll(c);
+  public boolean contains(Object elem) {
+    return arrayList.contains(elem);
   }
 
   public void copyInto(Object[] objs) {
@@ -88,95 +72,58 @@
     while (++i < n) {
       objs[i] = get(i);
     }
-  }
-
+  } 
+  
   public Object elementAt(int index) {
     return get(index);
   }
 
-  public boolean equals(Object o) {
-    return super.equals(o);
-  }
-
   public Object firstElement() {
     return get(0);
   }
 
   public Object get(int index) {
-    if ((index < 0) || (index >= size())) {
-      throw new NoSuchElementException();
-    }
-    return _get(index);
+    return arrayList.get(index);
   }
 
-  public int hashCode() {
-    return super.hashCode();
+  public int indexOf(Object elem) {
+    return arrayList.indexOf(elem);
   }
 
-  public int indexOf(Object o) {
-    return indexOf(o, 0);
+  public int indexOf(Object elem, int index) {
+    return arrayList.indexOf(elem, index);
   }
 
-  public native int indexOf(Object o, int startIndex) /*-{
-    var a = this.array;
-    var i = startIndex-1;
-    var n = a.length;
-    while (++i < n) {
-      if (@java.util.Vector::equals(Ljava/lang/Object;Ljava/lang/Object;)(a[i],o))
-        return i;
-    }
-    return -1;
-  }-*/;
-
   public void insertElementAt(Object o, int index) {
     add(index, o);
   }
 
-  public native boolean isEmpty() /*-{
-    return (this.array.length == 0);
-  }-*/;
+  public boolean isEmpty() {
+    return (arrayList.size() == 0);
+  }
+
+  public Iterator iterator() {
+    return arrayList.iterator();
+  }
 
   public Object lastElement() {
     if (isEmpty()) {
-      throw new NoSuchElementException("last");
+      throw new IndexOutOfBoundsException("last");
     } else {
       return get(size() - 1);
     }
   }
 
   public int lastIndexOf(Object o) {
-    return lastIndexOf(o, size() - 1);
+    return arrayList.lastIndexOf(o);
   }
 
-  public native int lastIndexOf(Object o, int startIndex) /*-{
-    var a = this.array;
-    var i = startIndex;
-    while (i >= 0) {
-      if (@java.util.Vector::equals(Ljava/lang/Object;Ljava/lang/Object;)(a[i],o))
-        return i;
-      --i;
-    }
-    return -1;
-  }-*/;
-
-  public native Object remove(int index) /*-{
-    var a = this.array;
-    var old = a[index];
-    this.array = a.slice(0, index).concat(a.slice(index+1));
-    return old; 
-  }-*/;
-
-  public boolean remove(Object o) {
-    int i = indexOf(o);
-    if (i == -1) {
-      return false;
-    }
-    remove(i);
-    return true;
+  public int lastIndexOf(Object o, int index) {
+    return arrayList.lastIndexOf(o, index);
   }
 
-  public boolean removeAll(Collection c) {
-    return super.removeAll(c);
+  public Object remove(int index) {
+    return arrayList.remove(index);
   }
 
   public void removeAllElements() {
@@ -191,60 +138,27 @@
     remove(index);
   }
 
-  public boolean retainAll(Collection c) {
-    return super.retainAll(c);
+  public Object set(int index, Object elem) {
+    return arrayList.set(index, elem);
   }
 
-  public native Object set(int index, Object o) /*-{
-    var a = this.array;
-    var old = a[index];
-    a[index] = o;
-    return old;
-  }-*/;
-
   public void setElementAt(Object o, int index) {
     set(index, o);
   }
 
-  public native void setSize(int newSize) /*-{
-    // Make sure to null-fill any newly created slots (otherwise,
-    // get() can return 'undefined').
-    for (var i = this.array.length; i < newSize; ++i)
-      this.array[i] = null;
+  public void setSize(int size) {
+    arrayList.setSize(size);
+  }
 
-    // Also make sure to clean up orphaned slots (or we'll end up
-    // leaving garbage uncollected).
-    for (var i = this.array.length - 1; i >= newSize; --i)
-      delete this.array[i];
-
-    this.array.length = newSize;
-  }-*/;
-
-  public native int size() /*-{
-    return this.array.length; 
-  }-*/;
+  public int size() {
+    return arrayList.size();
+  }
 
   public Object[] toArray() {
-    return super.toArray();
+    return arrayList.toArray();
   }
 
-  public String toString() {
-    return super.toString();
+  protected void removeRange(int fromIndex, int endIndex) {
+    arrayList.removeRange(fromIndex, endIndex);
   }
-
-  protected native void removeRange(int fromIndex, int endIndex) /*-{
-    var a = this.array;
-    this.array = a.slice(0, fromIndex).concat(a.slice(endIndex));
-  }-*/;
-
-  // CHECKSTYLE_OFF: Underscore prefix is an old convention that could be cleaned up.
-  private native Object _get(int index) /*-{
-    return this.array[index];
-  }-*/;
-  // CHECKSTYLE_ON
-
-  private native void initArray() /*-{
-    this.array = new Array(); 
-  }-*/;
-
 }
diff --git a/user/test/com/google/gwt/emultest/java/util/ArrayListTest.java b/user/test/com/google/gwt/emultest/java/util/ArrayListTest.java
index b99b7ae..328456d 100644
--- a/user/test/com/google/gwt/emultest/java/util/ArrayListTest.java
+++ b/user/test/com/google/gwt/emultest/java/util/ArrayListTest.java
@@ -1,12 +1,30 @@
-// Copyright 2006 Google Inc. All Rights Reserved.
+/*
+ * Copyright 2006 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
 package com.google.gwt.emultest.java.util;
 
+import com.google.gwt.core.client.GWT;
+
 import org.apache.commons.collections.TestArrayList;
 
 import java.util.ArrayList;
 import java.util.List;
 
-/** Tests ArrayList, and, by extention AbstractList */
+/** Tests ArrayList, and, by extension AbstractList.  Uses inheritance to 
+ * inherit all of Apache's TestList and TestCollection. */
 public class ArrayListTest extends TestArrayList {
   public ArrayListTest() {
   }
@@ -15,4 +33,47 @@
     return new ArrayList();
   }
 
+  public void testAddWatch() {
+    ArrayList s = new ArrayList();
+    s.add("watch");
+    assertEquals(s.get(0), "watch");
+  }
+
+
+  public void testRemoveRange() {
+    if (GWT.isScript()) {
+      ArrayList l = new ArrayList();
+      for (int i = 0; i < 10; i++) {
+        l.add(new Integer(i));
+      }
+      verifyRemoveRangeWorks(l);
+    }
+  }
+
+  private native void verifyRemoveRangeWorks(ArrayList l) /*-{
+    var startIndex = l.@java.util.ArrayList::startIndex;
+    var endIndex = l.@java.util.ArrayList::endIndex;
+    var array = l.@java.util.ArrayList::array;
+    l.@java.util.ArrayList::removeRange(II)(0,2);
+    if (array[startIndex] !== undefined) {
+      @junit.framework.Assert::fail(Ljava/lang/String;)("startIndex should be empty");
+    }
+    if (array[startIndex + 1] !== undefined) {
+      @junit.framework.Assert::fail(Ljava/lang/String;)("startIndex + 1 should be empty");
+    }
+    if (array[startIndex + 2] === undefined) {
+      @junit.framework.Assert::fail(Ljava/lang/String;)("startIndex + 2 should not be empty");
+    }
+    l.@java.util.ArrayList::removeRange(II)(6,8);
+    if (array[endIndex - 3] === undefined) {
+      @junit.framework.Assert::fail(Ljava/lang/String;)("endIndex - 3 should not be empty");
+    }
+    if (array[endIndex - 2] !== undefined) {
+      @junit.framework.Assert::fail(Ljava/lang/String;)("endIndex - 2 should be empty");
+    }
+    if (array[endIndex - 1] !== undefined) {
+      @junit.framework.Assert::fail(Ljava/lang/String;)("endIndex - 1 should be empty");
+    }
+  }-*/;
+  
 }