BinaryDictionary.java revision e137ec0a91cf93b0a99fd1e1556ee835d026f731
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.text.TextUtils; 20import android.util.Log; 21import android.util.SparseArray; 22 23import com.android.inputmethod.annotations.UsedForTesting; 24import com.android.inputmethod.keyboard.ProximityInfo; 25import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 26import com.android.inputmethod.latin.makedict.DictionaryHeader; 27import com.android.inputmethod.latin.makedict.FormatSpec; 28import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; 29import com.android.inputmethod.latin.makedict.UnsupportedFormatException; 30import com.android.inputmethod.latin.makedict.WordProperty; 31import com.android.inputmethod.latin.personalization.PersonalizationHelper; 32import com.android.inputmethod.latin.settings.NativeSuggestOptions; 33import com.android.inputmethod.latin.utils.CollectionUtils; 34import com.android.inputmethod.latin.utils.JniUtils; 35import com.android.inputmethod.latin.utils.LanguageModelParam; 36import com.android.inputmethod.latin.utils.StringUtils; 37 38import java.io.File; 39import java.util.ArrayList; 40import java.util.Arrays; 41import java.util.HashMap; 42import java.util.Locale; 43 44/** 45 * Implements a static, compacted, binary dictionary of standard words. 46 */ 47// TODO: All methods which should be locked need to have a suffix "Locked". 48public final class BinaryDictionary extends Dictionary { 49 private static final String TAG = BinaryDictionary.class.getSimpleName(); 50 51 // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h 52 private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; 53 // Must be equal to MAX_RESULTS in native/jni/src/defines.h 54 private static final int MAX_RESULTS = 18; 55 // The cutoff returned by native for auto-commit confidence. 56 // Must be equal to CONFIDENCE_TO_AUTO_COMMIT in native/jni/src/defines.h 57 private static final int CONFIDENCE_TO_AUTO_COMMIT = 1000000; 58 59 @UsedForTesting 60 public static final String UNIGRAM_COUNT_QUERY = "UNIGRAM_COUNT"; 61 @UsedForTesting 62 public static final String BIGRAM_COUNT_QUERY = "BIGRAM_COUNT"; 63 @UsedForTesting 64 public static final String MAX_UNIGRAM_COUNT_QUERY = "MAX_UNIGRAM_COUNT"; 65 @UsedForTesting 66 public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT"; 67 68 public static final int NOT_A_VALID_TIMESTAMP = -1; 69 70 // Format to get unigram flags from native side via getWordPropertyNative(). 71 private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 4; 72 private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0; 73 private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1; 74 private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2; 75 private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3; 76 77 // Format to get probability and historical info from native side via getWordPropertyNative(). 78 public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4; 79 public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0; 80 public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1; 81 public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2; 82 public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3; 83 84 private long mNativeDict; 85 private final Locale mLocale; 86 private final long mDictSize; 87 private final String mDictFilePath; 88 private final boolean mIsUpdatable; 89 private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH]; 90 private final int[] mOutputSuggestionCount = new int[1]; 91 private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS]; 92 private final int[] mSpaceIndices = new int[MAX_RESULTS]; 93 private final int[] mOutputScores = new int[MAX_RESULTS]; 94 private final int[] mOutputTypes = new int[MAX_RESULTS]; 95 // Only one result is ever used 96 private final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; 97 98 private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); 99 100 private final SparseArray<DicTraverseSession> mDicTraverseSessions = 101 CollectionUtils.newSparseArray(); 102 103 // TODO: There should be a way to remove used DicTraverseSession objects from 104 // {@code mDicTraverseSessions}. 105 private DicTraverseSession getTraverseSession(final int traverseSessionId) { 106 synchronized(mDicTraverseSessions) { 107 DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId); 108 if (traverseSession == null) { 109 traverseSession = mDicTraverseSessions.get(traverseSessionId); 110 if (traverseSession == null) { 111 traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize); 112 mDicTraverseSessions.put(traverseSessionId, traverseSession); 113 } 114 } 115 return traverseSession; 116 } 117 } 118 119 /** 120 * Constructor for the binary dictionary. This is supposed to be called from the 121 * dictionary factory. 122 * @param filename the name of the file to read through native code. 123 * @param offset the offset of the dictionary data within the file. 124 * @param length the length of the binary data. 125 * @param useFullEditDistance whether to use the full edit distance in suggestions 126 * @param dictType the dictionary type, as a human-readable string 127 * @param isUpdatable whether to open the dictionary file in writable mode. 128 */ 129 public BinaryDictionary(final String filename, final long offset, final long length, 130 final boolean useFullEditDistance, final Locale locale, final String dictType, 131 final boolean isUpdatable) { 132 super(dictType); 133 mLocale = locale; 134 mDictSize = length; 135 mDictFilePath = filename; 136 mIsUpdatable = isUpdatable; 137 mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance); 138 loadDictionary(filename, offset, length, isUpdatable); 139 } 140 141 static { 142 JniUtils.loadNativeLibrary(); 143 } 144 145 private static native long openNative(String sourceDir, long dictOffset, long dictSize, 146 boolean isUpdatable); 147 private static native void getHeaderInfoNative(long dict, int[] outHeaderSize, 148 int[] outFormatVersion, ArrayList<int[]> outAttributeKeys, 149 ArrayList<int[]> outAttributeValues); 150 private static native void flushNative(long dict, String filePath); 151 private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC); 152 private static native void flushWithGCNative(long dict, String filePath); 153 private static native void closeNative(long dict); 154 private static native int getFormatVersionNative(long dict); 155 private static native int getProbabilityNative(long dict, int[] word); 156 private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1); 157 private static native void getWordPropertyNative(long dict, int[] word, 158 int[] outCodePoints, boolean[] outFlags, int[] outProbabilityInfo, 159 ArrayList<int[]> outBigramTargets, ArrayList<int[]> outBigramProbabilityInfo, 160 ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities); 161 private static native int getNextWordNative(long dict, int token, int[] outCodePoints); 162 private static native void getSuggestionsNative(long dict, long proximityInfo, 163 long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, 164 int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint, 165 int[] suggestOptions, int[] prevWordCodePointArray, int[] outputSuggestionCount, 166 int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes, 167 int[] outputAutoCommitFirstWordConfidence); 168 private static native void addUnigramWordNative(long dict, int[] word, int probability, 169 int[] shortcutTarget, int shortcutProbability, boolean isNotAWord, 170 boolean isBlacklisted, int timestamp); 171 private static native void addBigramWordsNative(long dict, int[] word0, int[] word1, 172 int probability, int timestamp); 173 private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1); 174 private static native int addMultipleDictionaryEntriesNative(long dict, 175 LanguageModelParam[] languageModelParams, int startIndex); 176 private static native int calculateProbabilityNative(long dict, int unigramProbability, 177 int bigramProbability); 178 private static native String getPropertyNative(long dict, String query); 179 private static native boolean isCorruptedNative(long dict); 180 181 // TODO: Move native dict into session 182 private final void loadDictionary(final String path, final long startOffset, 183 final long length, final boolean isUpdatable) { 184 mNativeDict = openNative(path, startOffset, length, isUpdatable); 185 } 186 187 // TODO: Check isCorrupted() for main dictionaries. 188 public boolean isCorrupted() { 189 if (!isValidDictionary()) { 190 return false; 191 } 192 if (!isCorruptedNative(mNativeDict)) { 193 return false; 194 } 195 // TODO: Record the corruption. 196 Log.e(TAG, "BinaryDictionary (" + mDictFilePath + ") is corrupted."); 197 Log.e(TAG, "locale: " + mLocale); 198 Log.e(TAG, "dict size: " + mDictSize); 199 Log.e(TAG, "updatable: " + mIsUpdatable); 200 return true; 201 } 202 203 public DictionaryHeader getHeader() throws UnsupportedFormatException { 204 if (mNativeDict == 0) { 205 return null; 206 } 207 final int[] outHeaderSize = new int[1]; 208 final int[] outFormatVersion = new int[1]; 209 final ArrayList<int[]> outAttributeKeys = CollectionUtils.newArrayList(); 210 final ArrayList<int[]> outAttributeValues = CollectionUtils.newArrayList(); 211 getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys, 212 outAttributeValues); 213 final HashMap<String, String> attributes = new HashMap<String, String>(); 214 for (int i = 0; i < outAttributeKeys.size(); i++) { 215 final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray( 216 outAttributeKeys.get(i)); 217 final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray( 218 outAttributeValues.get(i)); 219 attributes.put(attributeKey, attributeValue); 220 } 221 final boolean hasHistoricalInfo = DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals( 222 attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY)); 223 return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes), 224 new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo)); 225 } 226 227 228 @Override 229 public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, 230 final String prevWord, final ProximityInfo proximityInfo, 231 final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { 232 return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, 233 additionalFeaturesOptions, 0 /* sessionId */); 234 } 235 236 @Override 237 public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, 238 final String prevWord, final ProximityInfo proximityInfo, 239 final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, 240 final int sessionId) { 241 if (!isValidDictionary()) return null; 242 243 Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE); 244 // TODO: toLowerCase in the native code 245 final int[] prevWordCodePointArray = (null == prevWord) 246 ? null : StringUtils.toCodePointArray(prevWord); 247 final int composerSize = composer.size(); 248 249 final boolean isGesture = composer.isBatchMode(); 250 if (composerSize <= 1 || !isGesture) { 251 if (composerSize > MAX_WORD_LENGTH - 1) return null; 252 for (int i = 0; i < composerSize; i++) { 253 mInputCodePoints[i] = composer.getCodeAt(i); 254 } 255 } 256 257 final InputPointers ips = composer.getInputPointers(); 258 final int inputSize = isGesture ? ips.getPointerSize() : composerSize; 259 mNativeSuggestOptions.setIsGesture(isGesture); 260 mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions); 261 // proximityInfo and/or prevWordForBigrams may not be null. 262 getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), 263 getTraverseSession(sessionId).getSession(), ips.getXCoordinates(), 264 ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints, 265 inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(), 266 prevWordCodePointArray, mOutputSuggestionCount, mOutputCodePoints, mOutputScores, 267 mSpaceIndices, mOutputTypes, mOutputAutoCommitFirstWordConfidence); 268 final int count = mOutputSuggestionCount[0]; 269 final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); 270 for (int j = 0; j < count; ++j) { 271 final int start = j * MAX_WORD_LENGTH; 272 int len = 0; 273 while (len < MAX_WORD_LENGTH && mOutputCodePoints[start + len] != 0) { 274 ++len; 275 } 276 if (len > 0) { 277 final int flags = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_FLAGS; 278 if (blockOffensiveWords 279 && 0 != (flags & SuggestedWordInfo.KIND_FLAG_POSSIBLY_OFFENSIVE) 280 && 0 == (flags & SuggestedWordInfo.KIND_FLAG_EXACT_MATCH)) { 281 // If we block potentially offensive words, and if the word is possibly 282 // offensive, then we don't output it unless it's also an exact match. 283 continue; 284 } 285 final int kind = mOutputTypes[j] & SuggestedWordInfo.KIND_MASK_KIND; 286 final int score = SuggestedWordInfo.KIND_WHITELIST == kind 287 ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j]; 288 // TODO: check that all users of the `kind' parameter are ready to accept 289 // flags too and pass mOutputTypes[j] instead of kind 290 suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len), 291 score, kind, this /* sourceDict */, 292 mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */, 293 mOutputAutoCommitFirstWordConfidence[0])); 294 } 295 } 296 return suggestions; 297 } 298 299 public boolean isValidDictionary() { 300 return mNativeDict != 0; 301 } 302 303 public int getFormatVersion() { 304 return getFormatVersionNative(mNativeDict); 305 } 306 307 @Override 308 public boolean isValidWord(final String word) { 309 return getFrequency(word) != NOT_A_PROBABILITY; 310 } 311 312 @Override 313 public int getFrequency(final String word) { 314 if (word == null) return NOT_A_PROBABILITY; 315 int[] codePoints = StringUtils.toCodePointArray(word); 316 return getProbabilityNative(mNativeDict, codePoints); 317 } 318 319 // TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni 320 // calls when checking for changes in an entire dictionary. 321 public boolean isValidBigram(final String word0, final String word1) { 322 return getBigramProbability(word0, word1) != NOT_A_PROBABILITY; 323 } 324 325 public int getBigramProbability(final String word0, final String word1) { 326 if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) return NOT_A_PROBABILITY; 327 final int[] codePoints0 = StringUtils.toCodePointArray(word0); 328 final int[] codePoints1 = StringUtils.toCodePointArray(word1); 329 return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1); 330 } 331 332 public WordProperty getWordProperty(final String word) { 333 if (TextUtils.isEmpty(word)) { 334 return null; 335 } 336 final int[] codePoints = StringUtils.toCodePointArray(word); 337 final int[] outCodePoints = new int[MAX_WORD_LENGTH]; 338 final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT]; 339 final int[] outProbabilityInfo = 340 new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT]; 341 final ArrayList<int[]> outBigramTargets = CollectionUtils.newArrayList(); 342 final ArrayList<int[]> outBigramProbabilityInfo = CollectionUtils.newArrayList(); 343 final ArrayList<int[]> outShortcutTargets = CollectionUtils.newArrayList(); 344 final ArrayList<Integer> outShortcutProbabilities = CollectionUtils.newArrayList(); 345 getWordPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbabilityInfo, 346 outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, 347 outShortcutProbabilities); 348 return new WordProperty(codePoints, 349 outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX], 350 outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX], 351 outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX], 352 outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], outProbabilityInfo, 353 outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, 354 outShortcutProbabilities); 355 } 356 357 public static class GetNextWordPropertyResult { 358 public WordProperty mWordProperty; 359 public int mNextToken; 360 361 public GetNextWordPropertyResult(final WordProperty wordPreperty, final int nextToken) { 362 mWordProperty = wordPreperty; 363 mNextToken = nextToken; 364 } 365 } 366 367 /** 368 * Method to iterate all words in the dictionary for makedict. 369 * If token is 0, this method newly starts iterating the dictionary. 370 */ 371 public GetNextWordPropertyResult getNextWordProperty(final int token) { 372 final int[] codePoints = new int[MAX_WORD_LENGTH]; 373 final int nextToken = getNextWordNative(mNativeDict, token, codePoints); 374 final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); 375 return new GetNextWordPropertyResult(getWordProperty(word), nextToken); 376 } 377 378 // Add a unigram entry to binary dictionary with unigram attributes in native code. 379 public void addUnigramWord(final String word, final int probability, 380 final String shortcutTarget, final int shortcutProbability, final boolean isNotAWord, 381 final boolean isBlacklisted, final int timestamp) { 382 if (TextUtils.isEmpty(word)) { 383 return; 384 } 385 final int[] codePoints = StringUtils.toCodePointArray(word); 386 final int[] shortcutTargetCodePoints = (shortcutTarget != null) ? 387 StringUtils.toCodePointArray(shortcutTarget) : null; 388 addUnigramWordNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints, 389 shortcutProbability, isNotAWord, isBlacklisted, timestamp); 390 } 391 392 // Add a bigram entry to binary dictionary with timestamp in native code. 393 public void addBigramWords(final String word0, final String word1, final int probability, 394 final int timestamp) { 395 if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { 396 return; 397 } 398 final int[] codePoints0 = StringUtils.toCodePointArray(word0); 399 final int[] codePoints1 = StringUtils.toCodePointArray(word1); 400 addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability, timestamp); 401 } 402 403 // Remove a bigram entry form binary dictionary in native code. 404 public void removeBigramWords(final String word0, final String word1) { 405 if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { 406 return; 407 } 408 final int[] codePoints0 = StringUtils.toCodePointArray(word0); 409 final int[] codePoints1 = StringUtils.toCodePointArray(word1); 410 removeBigramWordsNative(mNativeDict, codePoints0, codePoints1); 411 } 412 413 public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) { 414 if (!isValidDictionary()) return; 415 int processedParamCount = 0; 416 while (processedParamCount < languageModelParams.length) { 417 if (needsToRunGC(true /* mindsBlockByGC */)) { 418 flushWithGC(); 419 } 420 processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict, 421 languageModelParams, processedParamCount); 422 if (processedParamCount <= 0) { 423 return; 424 } 425 } 426 } 427 428 private void reopen() { 429 close(); 430 final File dictFile = new File(mDictFilePath); 431 // WARNING: Because we pass 0 as the offset and file.length() as the length, this can 432 // only be called for actual files. Right now it's only called by the flush() family of 433 // functions, which require an updatable dictionary, so it's okay. But beware. 434 loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */, 435 dictFile.length(), mIsUpdatable); 436 } 437 438 public void flush() { 439 if (!isValidDictionary()) return; 440 flushNative(mNativeDict, mDictFilePath); 441 reopen(); 442 } 443 444 public void flushWithGC() { 445 if (!isValidDictionary()) return; 446 flushWithGCNative(mNativeDict, mDictFilePath); 447 reopen(); 448 } 449 450 /** 451 * Checks whether GC is needed to run or not. 452 * @param mindsBlockByGC Whether to mind operations blocked by GC. We don't need to care about 453 * the blocking in some situations such as in idle time or just before closing. 454 * @return whether GC is needed to run or not. 455 */ 456 public boolean needsToRunGC(final boolean mindsBlockByGC) { 457 if (!isValidDictionary()) return false; 458 return needsToRunGCNative(mNativeDict, mindsBlockByGC); 459 } 460 461 @UsedForTesting 462 public int calculateProbability(final int unigramProbability, final int bigramProbability) { 463 if (!isValidDictionary()) return NOT_A_PROBABILITY; 464 return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability); 465 } 466 467 @UsedForTesting 468 public String getPropertyForTest(final String query) { 469 if (!isValidDictionary()) return ""; 470 return getPropertyNative(mNativeDict, query); 471 } 472 473 @Override 474 public boolean shouldAutoCommit(final SuggestedWordInfo candidate) { 475 return candidate.mAutoCommitFirstWordConfidence > CONFIDENCE_TO_AUTO_COMMIT; 476 } 477 478 @Override 479 public void close() { 480 synchronized (mDicTraverseSessions) { 481 final int sessionsSize = mDicTraverseSessions.size(); 482 for (int index = 0; index < sessionsSize; ++index) { 483 final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index); 484 if (traverseSession != null) { 485 traverseSession.close(); 486 } 487 } 488 mDicTraverseSessions.clear(); 489 } 490 closeInternalLocked(); 491 } 492 493 private synchronized void closeInternalLocked() { 494 if (mNativeDict != 0) { 495 closeNative(mNativeDict); 496 mNativeDict = 0; 497 } 498 } 499 500 // TODO: Manage BinaryDictionary instances without using WeakReference or something. 501 @Override 502 protected void finalize() throws Throwable { 503 try { 504 closeInternalLocked(); 505 } finally { 506 super.finalize(); 507 } 508 } 509} 510