提交 ba2e2de7 authored 作者: Thomas Mueller's avatar Thomas Mueller

MVTableEngine (WIP)

上级 a7fbd0b9
...@@ -9,8 +9,9 @@ package org.h2.engine; ...@@ -9,8 +9,9 @@ package org.h2.engine;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.Set;
import org.h2.command.Command; import org.h2.command.Command;
import org.h2.command.CommandInterface; import org.h2.command.CommandInterface;
...@@ -26,6 +27,7 @@ import org.h2.message.DbException; ...@@ -26,6 +27,7 @@ import org.h2.message.DbException;
import org.h2.message.Trace; import org.h2.message.Trace;
import org.h2.message.TraceSystem; import org.h2.message.TraceSystem;
import org.h2.mvstore.db.MVTable; import org.h2.mvstore.db.MVTable;
import org.h2.mvstore.db.TransactionStore.Change;
import org.h2.mvstore.db.TransactionStore.Transaction; import org.h2.mvstore.db.TransactionStore.Transaction;
import org.h2.result.ResultInterface; import org.h2.result.ResultInterface;
import org.h2.result.Row; import org.h2.result.Row;
...@@ -38,6 +40,7 @@ import org.h2.table.Table; ...@@ -38,6 +40,7 @@ import org.h2.table.Table;
import org.h2.util.New; import org.h2.util.New;
import org.h2.util.SmallLRUCache; import org.h2.util.SmallLRUCache;
import org.h2.value.Value; import org.h2.value.Value;
import org.h2.value.ValueArray;
import org.h2.value.ValueLong; import org.h2.value.ValueLong;
import org.h2.value.ValueNull; import org.h2.value.ValueNull;
import org.h2.value.ValueString; import org.h2.value.ValueString;
...@@ -517,22 +520,20 @@ public class Session extends SessionWithState { ...@@ -517,22 +520,20 @@ public class Session extends SessionWithState {
*/ */
public void rollback() { public void rollback() {
checkCommitRollback(); checkCommitRollback();
if (transaction != null) {
Set<String> changed = transaction.getChangedMaps(0);
for (MVTable t : database.getMvStore().getTables()) {
if (changed.contains(t.getMapName())) {
t.setModified();
}
}
transaction.rollback();
transaction = null;
}
currentTransactionName = null; currentTransactionName = null;
boolean needCommit = false; boolean needCommit = false;
if (undoLog.size() > 0) { if (undoLog.size() > 0) {
rollbackTo(null, false); rollbackTo(null, false);
needCommit = true; needCommit = true;
} }
if (transaction != null) {
rollbackTo(null, false);
needCommit = true;
// rollback stored the undo operations in the transaction
// committing will end the transaction
transaction.commit();
transaction = null;
}
if (locks.size() > 0 || needCommit) { if (locks.size() > 0 || needCommit) {
database.commit(this); database.commit(this);
} }
...@@ -558,13 +559,33 @@ public class Session extends SessionWithState { ...@@ -558,13 +559,33 @@ public class Session extends SessionWithState {
undoLog.removeLast(trimToSize); undoLog.removeLast(trimToSize);
} }
if (transaction != null) { if (transaction != null) {
Set<String> changed = transaction.getChangedMaps(savepoint.transactionSavepoint); long savepointId = savepoint == null ? 0 : savepoint.transactionSavepoint;
for (MVTable t : database.getMvStore().getTables()) { List<MVTable> tables = database.getMvStore().getTables();
if (changed.contains(t.getMapName())) { HashMap<String, MVTable> tableMap = New.hashMap();
t.setModified(); for (MVTable t : tables) {
tableMap.put(t.getMapName(), t);
}
Iterator<Change> it = transaction.getChanges(savepointId);
while (it.hasNext()) {
Change c = it.next();
MVTable t = tableMap.get(c.mapName);
if (t != null) {
long key = ((ValueLong) c.key).getLong();
ValueArray value = (ValueArray) c.value;
short op;
Row row;
if (value == null) {
op = UndoLogRecord.INSERT;
row = t.getRow(this, key);
} else {
op = UndoLogRecord.DELETE;
row = new Row(value.getList(), Row.MEMORY_CALCULATE);
}
row.setKey(key);
UndoLogRecord log = new UndoLogRecord(t, op, row);
log.undo(this);
} }
} }
transaction.rollbackToSavepoint(savepoint.transactionSavepoint);
} }
if (savepoints != null) { if (savepoints != null) {
String[] names = new String[savepoints.size()]; String[] names = new String[savepoints.size()];
......
...@@ -74,7 +74,7 @@ public class MVDelegateIndex extends BaseIndex { ...@@ -74,7 +74,7 @@ public class MVDelegateIndex extends BaseIndex {
@Override @Override
public double getCost(Session session, int[] masks, SortOrder sortOrder) { public double getCost(Session session, int[] masks, SortOrder sortOrder) {
return 10 * getCostRangeIndex(masks, mainIndex.getRowCount(session), sortOrder); return 10 * getCostRangeIndex(masks, mainIndex.getRowCountApproximation(), sortOrder);
} }
@Override @Override
......
...@@ -36,7 +36,7 @@ import org.h2.value.ValueNull; ...@@ -36,7 +36,7 @@ import org.h2.value.ValueNull;
public class MVPrimaryIndex extends BaseIndex { public class MVPrimaryIndex extends BaseIndex {
private final MVTable mvTable; private final MVTable mvTable;
private String mapName; private final String mapName;
private TransactionMap<Value, Value> dataMap; private TransactionMap<Value, Value> dataMap;
private long lastKey; private long lastKey;
private int mainIndexColumn = -1; private int mainIndexColumn = -1;
...@@ -53,7 +53,7 @@ public class MVPrimaryIndex extends BaseIndex { ...@@ -53,7 +53,7 @@ public class MVPrimaryIndex extends BaseIndex {
null, null, null); null, null, null);
ValueDataType valueType = new ValueDataType( ValueDataType valueType = new ValueDataType(
db.getCompareMode(), db, sortTypes); db.getCompareMode(), db, sortTypes);
mapName = getName() + "_" + getId(); mapName = "table." + getId();
MVMap.Builder<Value, Value> mapBuilder = new MVMap.Builder<Value, Value>(). MVMap.Builder<Value, Value> mapBuilder = new MVMap.Builder<Value, Value>().
keyType(keyType). keyType(keyType).
valueType(valueType); valueType(valueType);
...@@ -62,19 +62,6 @@ public class MVPrimaryIndex extends BaseIndex { ...@@ -62,19 +62,6 @@ public class MVPrimaryIndex extends BaseIndex {
lastKey = k == null ? 0 : k.getLong(); lastKey = k == null ? 0 : k.getLong();
} }
/**
* Rename the index.
*
* @param newName the new name
*/
public void renameTable(String newName) {
TransactionMap<Value, Value> map = getMap(null);
rename(newName + "_DATA");
String newMapName = newName + "_DATA_" + getId();
map.renameMap(newMapName);
mapName = newMapName;
}
@Override @Override
public String getCreateSQL() { public String getCreateSQL() {
return null; return null;
...@@ -134,7 +121,11 @@ public class MVPrimaryIndex extends BaseIndex { ...@@ -134,7 +121,11 @@ public class MVPrimaryIndex extends BaseIndex {
e.setSource(this); e.setSource(this);
throw e; throw e;
} }
try {
map.put(key, ValueArray.get(row.getValueList())); map.put(key, ValueArray.get(row.getValueList()));
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.CONCURRENT_UPDATE_1, table.getName());
}
lastKey = Math.max(lastKey, row.getKey()); lastKey = Math.max(lastKey, row.getKey());
} }
...@@ -149,11 +140,15 @@ public class MVPrimaryIndex extends BaseIndex { ...@@ -149,11 +140,15 @@ public class MVPrimaryIndex extends BaseIndex {
} }
} }
TransactionMap<Value, Value> map = getMap(session); TransactionMap<Value, Value> map = getMap(session);
try {
Value old = map.remove(ValueLong.get(row.getKey())); Value old = map.remove(ValueLong.get(row.getKey()));
if (old == null) { if (old == null) {
throw DbException.get(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1, throw DbException.get(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1,
getSQL() + ": " + row.getKey()); getSQL() + ": " + row.getKey());
} }
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.CONCURRENT_UPDATE_1, table.getName());
}
} }
@Override @Override
...@@ -201,8 +196,12 @@ public class MVPrimaryIndex extends BaseIndex { ...@@ -201,8 +196,12 @@ public class MVPrimaryIndex extends BaseIndex {
@Override @Override
public double getCost(Session session, int[] masks, SortOrder sortOrder) { public double getCost(Session session, int[] masks, SortOrder sortOrder) {
try {
long cost = 10 * (dataMap.map.getSize() + Constants.COST_ROW_OFFSET); long cost = 10 * (dataMap.map.getSize() + Constants.COST_ROW_OFFSET);
return cost; return cost;
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.OBJECT_CLOSED);
}
} }
@Override @Override
...@@ -261,7 +260,11 @@ public class MVPrimaryIndex extends BaseIndex { ...@@ -261,7 +260,11 @@ public class MVPrimaryIndex extends BaseIndex {
@Override @Override
public long getRowCountApproximation() { public long getRowCountApproximation() {
try {
return dataMap.map.getSize(); return dataMap.map.getSize();
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.OBJECT_CLOSED);
}
} }
@Override @Override
......
...@@ -41,7 +41,7 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -41,7 +41,7 @@ public class MVSecondaryIndex extends BaseIndex {
final MVTable mvTable; final MVTable mvTable;
private final int keyColumns; private final int keyColumns;
private String mapName; private final String mapName;
private TransactionMap<Value, Value> dataMap; private TransactionMap<Value, Value> dataMap;
public MVSecondaryIndex(Database db, MVTable table, int id, String indexName, public MVSecondaryIndex(Database db, MVTable table, int id, String indexName,
...@@ -59,7 +59,7 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -59,7 +59,7 @@ public class MVSecondaryIndex extends BaseIndex {
sortTypes[i] = columns[i].sortType; sortTypes[i] = columns[i].sortType;
} }
sortTypes[keyColumns - 1] = SortOrder.ASCENDING; sortTypes[keyColumns - 1] = SortOrder.ASCENDING;
mapName = getName() + "_" + getId(); mapName = "index." + getId();
ValueDataType keyType = new ValueDataType( ValueDataType keyType = new ValueDataType(
db.getCompareMode(), db, sortTypes); db.getCompareMode(), db, sortTypes);
ValueDataType valueType = new ValueDataType(null, null, null); ValueDataType valueType = new ValueDataType(null, null, null);
...@@ -74,15 +74,6 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -74,15 +74,6 @@ public class MVSecondaryIndex extends BaseIndex {
// ok // ok
} }
@Override
public void rename(String newName) {
TransactionMap<Value, Value> map = getMap(null);
String newMapName = newName + "_" + getId();
map.renameMap(newMapName);
mapName = newMapName;
super.rename(newName);
}
@Override @Override
public void add(Session session, Row row) { public void add(Session session, Row row) {
TransactionMap<Value, Value> map = getMap(session); TransactionMap<Value, Value> map = getMap(session);
...@@ -100,18 +91,26 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -100,18 +91,26 @@ public class MVSecondaryIndex extends BaseIndex {
} }
} }
array.getList()[keyColumns - 1] = ValueLong.get(row.getKey()); array.getList()[keyColumns - 1] = ValueLong.get(row.getKey());
try {
map.put(array, ValueLong.get(0)); map.put(array, ValueLong.get(0));
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.CONCURRENT_UPDATE_1, table.getName());
}
} }
@Override @Override
public void remove(Session session, Row row) { public void remove(Session session, Row row) {
ValueArray array = getKey(row); ValueArray array = getKey(row);
TransactionMap<Value, Value> map = getMap(session); TransactionMap<Value, Value> map = getMap(session);
try {
Value old = map.remove(array); Value old = map.remove(array);
if (old == null) { if (old == null) {
throw DbException.get(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1, throw DbException.get(ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1,
getSQL() + ": " + row.getKey()); getSQL() + ": " + row.getKey());
} }
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.CONCURRENT_UPDATE_1, table.getName());
}
} }
@Override @Override
...@@ -164,7 +163,11 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -164,7 +163,11 @@ public class MVSecondaryIndex extends BaseIndex {
@Override @Override
public double getCost(Session session, int[] masks, SortOrder sortOrder) { public double getCost(Session session, int[] masks, SortOrder sortOrder) {
try {
return 10 * getCostRangeIndex(masks, dataMap.map.getSize(), sortOrder); return 10 * getCostRangeIndex(masks, dataMap.map.getSize(), sortOrder);
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.OBJECT_CLOSED);
}
} }
@Override @Override
...@@ -208,7 +211,11 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -208,7 +211,11 @@ public class MVSecondaryIndex extends BaseIndex {
@Override @Override
public boolean needRebuild() { public boolean needRebuild() {
try {
return dataMap.map.getSize() == 0; return dataMap.map.getSize() == 0;
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.OBJECT_CLOSED);
}
} }
@Override @Override
...@@ -219,7 +226,11 @@ public class MVSecondaryIndex extends BaseIndex { ...@@ -219,7 +226,11 @@ public class MVSecondaryIndex extends BaseIndex {
@Override @Override
public long getRowCountApproximation() { public long getRowCountApproximation() {
try {
return dataMap.map.getSize(); return dataMap.map.getSize();
} catch (IllegalStateException e) {
throw DbException.get(ErrorCode.OBJECT_CLOSED);
}
} }
@Override @Override
......
...@@ -131,12 +131,6 @@ public class MVTable extends TableBase { ...@@ -131,12 +131,6 @@ public class MVTable extends TableBase {
} }
} }
@Override
public void rename(String newName) {
super.rename(newName);
primaryIndex.renameTable(newName);
}
private void doLock(Session session, int lockMode, boolean exclusive) { private void doLock(Session session, int lockMode, boolean exclusive) {
traceLock(session, exclusive, "requesting for"); traceLock(session, exclusive, "requesting for");
// don't get the current time unless necessary // don't get the current time unless necessary
...@@ -367,7 +361,7 @@ public class MVTable extends TableBase { ...@@ -367,7 +361,7 @@ public class MVTable extends TableBase {
* @param key the primary key * @param key the primary key
* @return the row * @return the row
*/ */
Row getRow(Session session, long key) { public Row getRow(Session session, long key) {
return primaryIndex.getRow(session, key); return primaryIndex.getRow(session, key);
} }
...@@ -393,7 +387,14 @@ public class MVTable extends TableBase { ...@@ -393,7 +387,14 @@ public class MVTable extends TableBase {
// if (isPersistIndexes() && indexType.isPersistent()) { // if (isPersistIndexes() && indexType.isPersistent()) {
int mainIndexColumn; int mainIndexColumn;
mainIndexColumn = getMainIndexColumn(indexType, cols); mainIndexColumn = getMainIndexColumn(indexType, cols);
if (!database.isStarting() && primaryIndex.getRowCount(session) != 0) { if (database.isStarting()) {
index = new MVSecondaryIndex(session.getDatabase(),
this, indexId,
indexName, cols, indexType);
if (index.getRowCountApproximation() != 0) {
mainIndexColumn = -1;
}
} else if (primaryIndex.getRowCount(session) != 0) {
mainIndexColumn = -1; mainIndexColumn = -1;
} }
if (mainIndexColumn != -1) { if (mainIndexColumn != -1) {
...@@ -417,10 +418,10 @@ public class MVTable extends TableBase { ...@@ -417,10 +418,10 @@ public class MVTable extends TableBase {
String n = getName() + ":" + index.getName(); String n = getName() + ":" + index.getName();
int t = MathUtils.convertLongToInt(total); int t = MathUtils.convertLongToInt(total);
while (cursor.next()) { while (cursor.next()) {
database.setProgress(DatabaseEventListener.STATE_CREATE_INDEX, n,
MathUtils.convertLongToInt(i++), t);
Row row = cursor.get(); Row row = cursor.get();
buffer.add(row); buffer.add(row);
database.setProgress(DatabaseEventListener.STATE_CREATE_INDEX, n,
MathUtils.convertLongToInt(i++), t);
if (buffer.size() >= bufferSize) { if (buffer.size() >= bufferSize) {
addRowsToIndex(session, buffer, index); addRowsToIndex(session, buffer, index);
} }
......
...@@ -149,6 +149,9 @@ public class MVTableEngine implements TableEngine { ...@@ -149,6 +149,9 @@ public class MVTableEngine implements TableEngine {
store.setWriteDelay(value); store.setWriteDelay(value);
} }
/**
* Rollback all open transactions.
*/
public void rollback() { public void rollback() {
List<Transaction> list = transactionStore.getOpenTransactions(); List<Transaction> list = transactionStore.getOpenTransactions();
for (Transaction t : list) { for (Transaction t : list) {
......
...@@ -8,11 +8,9 @@ package org.h2.mvstore.db; ...@@ -8,11 +8,9 @@ package org.h2.mvstore.db;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.h2.mvstore.Cursor; import org.h2.mvstore.Cursor;
import org.h2.mvstore.DataUtils; import org.h2.mvstore.DataUtils;
...@@ -237,7 +235,9 @@ public class TransactionStore { ...@@ -237,7 +235,9 @@ public class TransactionStore {
VersionedValue value = map.get(key); VersionedValue value = map.get(key);
// possibly the entry was added later on // possibly the entry was added later on
// so we have to check // so we have to check
if (value.value == null) { if (value == null) {
// nothing to do
} else if (value.value == null) {
// remove the value // remove the value
map.remove(key); map.remove(key);
} }
...@@ -280,6 +280,10 @@ public class TransactionStore { ...@@ -280,6 +280,10 @@ public class TransactionStore {
return false; return false;
} }
long[] key = undoLog.firstKey(); long[] key = undoLog.firstKey();
if (key == null) {
// unusual, but can happen
return false;
}
firstOpenTransaction = key[0]; firstOpenTransaction = key[0];
} }
if (firstOpenTransaction == transactionId) { if (firstOpenTransaction == transactionId) {
...@@ -338,30 +342,89 @@ public class TransactionStore { ...@@ -338,30 +342,89 @@ public class TransactionStore {
} }
/** /**
* Get the set of changed maps. * Get the changes of the given transaction, starting from the latest log id
* back to the given log id.
* *
* @param t the transaction * @param t the transaction
* @param maxLogId the maximum log id * @param maxLogId the maximum log id
* @param toLogId the minimum log id * @param toLogId the minimum log id
* @return the set of changed maps * @return the changes
*/ */
HashSet<String> getChangedMaps(Transaction t, long maxLogId, long toLogId) { Iterator<Change> getChanges(final Transaction t, final long maxLogId, final long toLogId) {
HashSet<String> set = New.hashSet(); return new Iterator<Change>() {
for (long logId = maxLogId - 1; logId >= toLogId; logId--) {
private long logId = maxLogId - 1;
private Change current;
{
fetchNext();
}
private void fetchNext() {
while (logId >= toLogId) {
Object[] op = undoLog.get(new long[] { Object[] op = undoLog.get(new long[] {
t.getId(), logId }); t.getId(), logId });
int mapId = ((Integer) op[1]).intValue(); int mapId = ((Integer) op[1]).intValue();
// TODO open map by id if possible // TODO open map by id if possible
Map<String, String> meta = store.getMetaMap(); Map<String, String> meta = store.getMetaMap();
String m = meta.get("map." + mapId); String m = meta.get("map." + mapId);
logId--;
if (m == null) { if (m == null) {
// map was removed later on // map was removed later on
} else { } else {
String mapName = DataUtils.parseMap(m).get("name"); current = new Change();
set.add(mapName); current.mapName = DataUtils.parseMap(m).get("name");
current.key = op[2];
VersionedValue oldValue = (VersionedValue) op[3];
current.value = oldValue == null ? null : oldValue.value;
return;
}
}
current = null;
}
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Change next() {
if (current == null) {
throw DataUtils.newUnsupportedOperationException("no data");
}
Change result = current;
fetchNext();
return result;
} }
@Override
public void remove() {
throw DataUtils.newUnsupportedOperationException("remove");
} }
return set;
};
}
/**
* A change in a map.
*/
public static class Change {
/**
* The name of the map where the change occurred.
*/
public String mapName;
/**
* The key.
*/
public Object key;
/**
* The value.
*/
public Object value;
} }
/** /**
...@@ -536,15 +599,16 @@ public class TransactionStore { ...@@ -536,15 +599,16 @@ public class TransactionStore {
} }
/** /**
* Get the set of changed maps starting at the given savepoint up to * Get the list of changes, starting with the latest change, up to the
* now. * given savepoint (in reverse order than they occurred). The value of
* the change is the value before the change was applied.
* *
* @param savepointId the savepoint id, 0 meaning the beginning of the * @param savepointId the savepoint id, 0 meaning the beginning of the
* transaction * transaction
* @return the set of changed maps * @return the changes
*/ */
public Set<String> getChangedMaps(long savepointId) { public Iterator<Change> getChanges(long savepointId) {
return store.getChangedMaps(this, logId, savepointId); return store.getChanges(this, logId, savepointId);
} }
/** /**
...@@ -945,8 +1009,8 @@ public class TransactionStore { ...@@ -945,8 +1009,8 @@ public class TransactionStore {
* @return the first key, or null if empty * @return the first key, or null if empty
*/ */
public K firstKey() { public K firstKey() {
// TODO transactional firstKey Iterator<K> it = keyIterator(null);
return map.firstKey(); return it.hasNext() ? it.next() : null;
} }
/** /**
...@@ -955,8 +1019,16 @@ public class TransactionStore { ...@@ -955,8 +1019,16 @@ public class TransactionStore {
* @return the last key, or null if empty * @return the last key, or null if empty
*/ */
public K lastKey() { public K lastKey() {
// TODO transactional lastKey K k = map.lastKey();
return map.lastKey(); while (true) {
if (k == null) {
return null;
}
if (get(k) != null) {
return k;
}
k = map.lowerKey(k);
}
} }
/** /**
...@@ -983,7 +1055,6 @@ public class TransactionStore { ...@@ -983,7 +1055,6 @@ public class TransactionStore {
* @return the result * @return the result
*/ */
public K ceilingKey(K key) { public K ceilingKey(K key) {
int test;
// TODO this method is slow // TODO this method is slow
Cursor<K> cursor = map.keyIterator(key); Cursor<K> cursor = map.keyIterator(key);
while (cursor.hasNext()) { while (cursor.hasNext()) {
...@@ -993,8 +1064,6 @@ public class TransactionStore { ...@@ -993,8 +1064,6 @@ public class TransactionStore {
} }
} }
return null; return null;
// TODO transactional ceilingKey
// return map.ceilingKey(key);
} }
/** /**
......
...@@ -83,7 +83,7 @@ public class ValueDataType implements DataType { ...@@ -83,7 +83,7 @@ public class ValueDataType implements DataType {
final CompareMode compareMode; final CompareMode compareMode;
final int[] sortTypes; final int[] sortTypes;
ValueDataType(CompareMode compareMode, DataHandler handler, int[] sortTypes) { public ValueDataType(CompareMode compareMode, DataHandler handler, int[] sortTypes) {
this.compareMode = compareMode; this.compareMode = compareMode;
this.handler = handler; this.handler = handler;
this.sortTypes = sortTypes; this.sortTypes = sortTypes;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论