我写了一个小词法分析器,将字符缓冲区转换成令牌流。令牌的一种类型是标识符,可以是“原始”或关键字。为了测试后者,我有一张包含所有关键字的地图。
Map<String, MyType> lookup = new HashMap<>();
lookup.put("RETURN", KEYWORD_RETURN);
[...]
该地图填充了所有大写字符串。
现在,我从输入字符缓冲区中得到的只是一个偏移量和一个长度,可以在其中找到我的标识符(不必在这里大写)。
显而易见的解决方案看起来像这样。
bool lookupIdentifier(CharBuffer buffer, int offset, int length, Map<String, MyType> lookupTable) {
int current = buffer.position();
buffer.rewind();
String toCheck = buffer.subSequence(offset, offset + length).toString().toUpperCase();
buffer.position(current);
return lookupTable.containsKey(toCheck);
}
地图上大约有50个条目。带有不区分大小写的比较器的TreeMap
是O(1)HashMap
查找的一个很好的选择吗?
我对我的方法不满意的是toCheck
字符串的创建是分配的。有没有一种方法可以重用CharBuffer
中的子字符串进行查找?
答案 0 :(得分:5)
通过使用CharBuffer
作为键类型,可以避免昂贵的字符串构造:
Map<CharBuffer, MyType> lookup = new TreeMap<>(Comparator
.comparingInt(CharBuffer::remaining)
.thenComparing((cb1,cb2) -> {
for(int p1 = cb1.position(), p2 = cb2.position(); p1 < cb1.limit(); p1++, p2++) {
char c1 = cb1.get(p1), c2 = cb2.get(p2);
if(c1 == c2) continue;
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if(c1 != c2) return Integer.compare(c1, c2);
}
return 0;
}));
lookup.put(CharBuffer.wrap("RETURN"), MyType.KEYWORD_RETURN);
boolean lookupIdentifier(
CharBuffer buffer, int offset, int length, Map<CharBuffer, MyType> lookupTable) {
int currentPos = buffer.position(), currLimit = buffer.limit();
buffer.clear().position(offset).limit(offset + length);
boolean result = lookupTable.containsKey(buffer);
buffer.clear().position(currentPos).limit(currLimit);
return result;
}
比较器在执行不区分大小写的字符比较之前使用便宜的长度比较。假设您使用RETURN
之类的关键字,它们具有简单的大小写映射。
对于具有50个关键字的地图,使用log²比较进行查找可能仍会产生合理的性能。请注意,每次比较都会在第一个不匹配处停止。
您可以将哈希与专用包装对象一起使用:
final class LookupKey {
final CharBuffer cb;
LookupKey(CharBuffer cb) {
this.cb = cb;
}
@Override public int hashCode() {
int code = 1;
for(int p = cb.position(); p < cb.limit(); p++) {
code = Character.toUpperCase(cb.get(p)) + code * 31;
}
return code;
}
@Override public boolean equals(Object obj) {
if(!(obj instanceof LookupKey)) return false;
final LookupKey other = (LookupKey)obj;
CharBuffer cb1 = this.cb, cb2 = other.cb;
if(cb1.remaining() != cb2.remaining()) return false;
for(int p1 = cb1.position(), p2 = cb2.position(); p1 < cb1.limit(); p1++, p2++) {
char c1 = cb1.get(p1), c2 = cb2.get(p2);
if(c1 == c2) continue;
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if(c1 != c2) return false;
}
return true;
}
}
Map<LookupKey, MyType> lookup = new HashMap<>();
lookup.put(new LookupKey(CharBuffer.wrap("RETURN")), MyType.KEYWORD_RETURN);
boolean lookupIdentifier(
CharBuffer buffer, int offset, int length, Map<LookupKey, MyType> lookupTable) {
int currentPos = buffer.position(), currLimit = buffer.limit();
buffer.clear().position(offset).limit(offset + length);
boolean result = lookupTable.containsKey(new LookupKey(buffer));
buffer.clear().position(currentPos).limit(currLimit);
return result;
}
像LookupKey
这样的轻量级对象的构造与String
不同,它不需要复制字符内容,因此可以忽略不计。但是请注意,与比较器不同,散列必须预先处理所有字符,这可能比小型TreeMap
的log2比较要昂贵。
如果这些关键字不太可能更改,则在关键字字符串的不变属性上使用显式查找代码(即switch
)可能会更加有效。例如。首先切换length
(如果大多数关键字的长度不同),然后切换一个对于大多数关键字都不同的字符(包括大写和小写变体的case
标签)。另一种选择是对这些属性进行分层查找。