8

I checked out official Android documentation for LRUCache which says : Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection. I suppose this is the doubly linked list which is maintained by linkedhashmap which is used by the cache. To check this behavior, I checked out the source code for LruCache, and checked the get(K key) method. It further calls upon map's get method which gets the value from the underlying hashmap and calls upon recordAccess method.

public V get(Object key) {
    LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
    if (e == null)
        return null;
    e.recordAccess(this);
    return e.value;
}

recordAccess method in turn moves the accessed entry to the end of the list in case accessOrder is set to true (for my problem let's assume it is), else it does nothing.

/**
     * This method is invoked by the superclass whenever the value
     * of a pre-existing entry is read by Map.get or modified by Map.set.
     * If the enclosing Map is access-ordered, it moves the entry
     * to the end of the list; otherwise, it does nothing.
     */
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }

This sounds contradictory to the above statement where it's said that the element is moved to the head of the queue. Instead it's moved to the last element of the list (using head.before). Surely, I'm missing something here, any help?

gaurav jain
  • 3,119
  • 3
  • 31
  • 48
  • i have no idea what sources you are checking, i can only see [this](https://android.googlesource.com/platform/frameworks/support.git/+/795b97d901e1793dac5c3e67d43c96a758fec388/v4/java/android/support/v4/util/LruCache.java#63) – pskink Jul 13 '17 at 07:40
  • I'm checking the same sources, and the actual re-ordering takes place in LinkedHashMap class (because that's where list is maintained), so you need to go into map.get() method. – gaurav jain Jul 13 '17 at 07:42
  • 1
    ok, so they refer to some virtual `"queue"`, not to implementation details of `LinkedHashMap` (the mapping is reversed) – pskink Jul 13 '17 at 08:36
  • Uhm... `add**Before**(lm.**head**er)`... the element will become the new head... head is the first in the list... "Instead it's moved to the last element of the list (using head.before)." is an incorrect statement from you. – D. Kovács Jul 13 '17 at 08:44
  • @D.Kovács Please check the implementation from source. private transient LinkedHashMapEntry header; is the header which is not modified in the addBefore method. Only the before field of header is modified to point to the new last element. The new element is basically inserted between the last element and the header, since it's a circular list. Please provide any source for your statement, as I couldn't find the same in code. – gaurav jain Jul 13 '17 at 08:50

2 Answers2

2

You are not missing anything, just you are reading LruCache documentation for LinkedHashMap. LinkedHashMap has its own documentation, in particular about its accessOrder. (Same on Java docs).

[...when accessOrder=true...] order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order)

So LinkedHashMap puts the most recently used entries at the end, and it is documented.

Practically LruCache describes how such cache works in theory, but LinkedHashMap shows how to implement it without adding separate backward-moving iterators: by putting the recent elements at the end, trimming can use the already available (forward-moving) iterator to access (and remove) old elements efficiently.

Though here and now I could not tell what was wrong with removeEldestEntry. Perhaps it did not exist in the past.

tevemadar
  • 12,389
  • 3
  • 21
  • 49
1

From the javadoc of LinkedHashMap:

If the three argument constructor is used, and accessOrder is specified as true, the iteration will be in the order that entries were accessed. The access order is affected by put, get, and putAll operations, but not by operations on the collection views.

Exactly the case, that LruCache has.

public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}

Let's see what recordAccess() does:

    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) { // true, because `LruCache` instantiated this 
                              // map with `accessOrder = true`
            lm.modCount++;
            remove(); // remove this `LinkedHashMapEntry` from the map
            addBefore(lm.header); // adds this entry before the current header of
                                  // the map, thus this entry becomes the header
        }
    }

Instead it's moved to the last element of the list (using head.before).

I cannot see, how your statement is valid.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • For the entry to become a header, lm.header must be modified in addBefore method. I don't see any assignments to lm.header field which would cause the header to change. – gaurav jain Jul 16 '17 at 08:02
  • In the [addBefore() implementation](https://android.googlesource.com/platform/libcore/+/0976dc2/ojluni/src/main/java/java/util/LinkedHashMap.java#356) you can see how references are being changed: the input entry is being added as current header item's `before` item, and input entry's `after` points to previous header item. So, just references are being tossed around. – azizbekian Jul 16 '17 at 08:46
  • agreed that the item is added as header.before and it's after pointer points to header. But header itself is not updated. It's a way of adding something before the header. But I don't see any fields of lm.header (passed to the method) being modified, which forms the basis of my problem. – gaurav jain Jul 16 '17 at 15:13
  • As far as I understand, you expect to see how actual values are being changed for the old header item. That's not how linked list works. Imagine objects (`foo1 - foo2 - foo3`): here `foo1.after = foo2`, `foo2.before = foo1`: each item references the item before and after it. Now `foo4.addBefore(foo1)` would do following: `foo4.after = foo1`, `foo4.before = foo1.before` (let's name this item `x`), `x.after = foo4`, `foo1.before = foo4`. This will result in following sequence: `foo4 - foo1 - foo2 - foo3`. We just manipulated references, haven't changed actual object data. – azizbekian Jul 16 '17 at 15:58
  • `But I don't see any fields of lm.header being modified` `foo1.before = foo4` modifies the field of `foo1`, pointing to currently added header item. – azizbekian Jul 16 '17 at 16:05
  • I think we're not on the same page here. Let's go by the method parameter. lm.header is the header passed on to the addBefore() method. In this method, existingEntry copies the reference of header, and I then expect it to be manipulated. So in the above example, foo1 basically replicates existingEntry from the source, and as you quoted, foo1.before = foo4 would modify it. But in the source, there is no assignment to existingEntry, then how it's reference can be changed. Also, docs for record access say: If the enclosing Map is access-ordered, it moves the entry to the END OF THE LIST. – gaurav jain Jul 16 '17 at 18:06
  • `But in the source, there is no assignment to existingEntry` The sequence of assignments in the abovementioned `foo` examples is exactly depicted from `addBefore()` implementation. See first assignment: `after = existingEntry` - here `after` points to the `lm.header`. Now see the last assignment: `after.before = this;` - which is `lm.header.before = this`, where `this` may be considered `foo4` from the example. Thus, you can see, that there is a field from `lm.header` that is being changed. – azizbekian Jul 17 '17 at 06:44
  • Yes the header's before field is indeed modified, but lm.header points to the same node throughout. It is never modified once initialized. Had the node been added in front, lm.header would have been modified at every insertion. Modifying it's before fields doesn't update the header, it just updates the before field of the node. For ex : If lm.header printed 00abc234 before insertion, it would still print 00abc234 after insertion as it is never modified and insertions are made at end. So, I still cannot understand your logic, despite such a lengthy discussion. – gaurav jain Jul 21 '17 at 17:44
  • For reference, you can check out this post where head pointer is modified as a node is added at front of DLL : http://www.geeksforgeeks.org/doubly-linked-list/. In case I consider head as a sentinel node here, wherein head.next gives me the first actual node of the list, even head.next isn't modified in the code. So, it makes my belief stronger, that this has something to do with bad documentation. – gaurav jain Jul 21 '17 at 18:05