forked from mozilla/rhino
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathHashtable.java
More file actions
262 lines (231 loc) · 8.24 KB
/
Copy pathHashtable.java
File metadata and controls
262 lines (231 loc) · 8.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package org.mozilla.javascript;
import java.io.Serializable;
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import java.util.HashMap;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* This generic hash table class is used by Set and Map. It uses
* a standard HashMap for storing keys and values so that we can handle
* lots of hash collisions if necessary, and a doubly-linked list to support the iterator
* capability.
* <p>
* This second one is important because JavaScript handling of
* the iterator is completely different from the way that Java does it. In Java
* an attempt to modify a collection on a HashMap or LinkedHashMap while iterating
* through it (except by using the "remove" method on the Iterator object itself) results in a
* ConcurrentModificationException. JavaScript Maps and Sets explicitly allow
* the collection to be modified, or even cleared completely, while iterators
* exist, and even lets an iterator keep on iterating on a collection that was
* empty when it was created..
*/
public class Hashtable implements Serializable, Iterable<Hashtable.Entry> {
private static final long serialVersionUID = -7151554912419543747L;
private final HashMap<Object, Entry> map = new HashMap<>();
private Entry first = null;
private Entry last = null;
/**
* One entry in the hash table. Override equals and hashcode because this is
* another area in which JavaScript and Java differ. This entry also becomes a
* node in the linked list.
*/
public static final class Entry implements Serializable {
private static final long serialVersionUID = 4086572107122965503L;
protected Object key;
protected Object value;
protected boolean deleted;
protected Entry next;
protected Entry prev;
private final int hashCode;
Entry() {
hashCode = 0;
}
Entry(Object k, Object value) {
if ((k instanceof Number) && ( ! ( k instanceof Double))) {
// Hash comparison won't work if we don't do this
this.key = ((Number)k).doubleValue();
} else if (k instanceof ConsString) {
this.key = k.toString();
} else {
this.key = k;
}
if (key == null) {
hashCode = 0;
} else if (k.equals(ScriptRuntime.negativeZero)) {
hashCode = 0;
} else {
hashCode = key.hashCode();
}
this.value = value;
}
public Object key() {
return key;
}
public Object value() {
return value;
}
/**
* Zero out key and value and return old value.
*/
Object clear() {
final Object ret = value;
key = Undefined.instance;
value = Undefined.instance;
deleted = true;
return ret;
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
try {
return ScriptRuntime.sameZero(key, ((Entry)o).key);
} catch (ClassCastException cce) {
return false;
}
}
}
private Entry makeDummy() {
final Entry d = new Entry();
d.clear();
return d;
}
public int size() {
return map.size();
}
public void put(Object key, Object value) {
final Entry nv = new Entry(key, value);
final Entry ev = map.putIfAbsent(nv, nv);
if (ev == null) {
// New value -- insert to end of doubly-linked list
if (first == null) {
first = last = nv;
} else {
last.next = nv;
nv.prev = last;
last = nv;
}
} else {
// Update the existing value and keep it in the same place in the list
ev.value = value;
}
}
public Object get(Object key) {
final Entry e = new Entry(key, null);
final Entry v = map.get(e);
if (v == null) {
return null;
}
return v.value;
}
public boolean has(Object key) {
final Entry e = new Entry(key, null);
return map.containsKey(e);
}
public Object delete(Object key) {
final Entry e = new Entry(key, null);
final Entry v = map.remove(e);
if (v == null) {
return null;
}
// To keep existing iterators moving forward as specified in EC262,
// we will remove the "prev" pointers from the list but leave the "next"
// pointers intact. Once we do that, then the only things pointing to
// the deleted nodes are existing iterators. Once those are gone, then
// these objects will be GCed.
// This way, new iterators will not "see" the deleted elements, and
// existing iterators will continue from wherever they left off to
// continue iterating in insertion order.
if (v == first) {
if (v == last) {
// Removing the only element. Leave it as a dummy or existing iterators
// will never stop.
v.clear();
v.prev = null;
} else {
first = v.next;
first.prev = null;
if (first.next != null) {
first.next.prev = first;
}
}
} else {
final Entry prev = v.prev;
prev.next = v.next;
v.prev = null;
if (v.next != null) {
v.next.prev = prev;
} else {
assert(v == last);
last = prev;
}
}
// Still clear the node in case it is in the chain of some iterator
return v.clear();
}
public void clear() {
// Zero out all the entries so that existing iterators will skip them all
Iterator<Entry> it = iterator();
it.forEachRemaining(Entry::clear);
// Replace the existing list with a dummy, and make it the last node
// of the current list. If new nodes are added now, existing iterators
// will drive forward right into the new list. If they are not, then
// nothing is referencing the old list and it'll get GCed.
if (first != null) {
Entry dummy = new Entry();
dummy.clear();
last.next = dummy;
first = last = dummy;
}
// Now we can clear the actual hashtable!
map.clear();
}
public Iterator<Entry> iterator() {
return new Iter(first);
}
// The iterator for this class works directly on the linked list so that it implements
// the specified iteration behavior, which is very different from Java.
private final class Iter
implements Iterator<Entry>
{
private Entry pos;
Iter(Entry start) {
// Keep the logic simpler by having a dummy at the start
Entry dummy = makeDummy();
dummy.next = start;
this.pos = dummy;
}
private void skipDeleted() {
// Skip forward past deleted elements, which could appear due to
// "delete" or a "clear" operation after this iterator was created.
// End up just before the next non-deleted node.
while ((pos.next != null) && pos.next.deleted) {
pos = pos.next;
}
}
@Override
public boolean hasNext() {
skipDeleted();
return ((pos != null) && (pos.next != null));
}
@Override
public Entry next() {
skipDeleted();
if ((pos == null) || (pos.next == null)) {
throw new NoSuchElementException();
}
final Entry e = pos.next;
pos = pos.next;
return e;
}
}
}