Android 高级面试-3:语言相关

主要内容:Kotlin, Java, RxJava, 多线程/并发, 集合

1、Java 相关

1.1 缓存相关

  • LruCache 的原理
  • DiskLruCache 的原理

LruCache 用来实现基于内存的缓存,LRU 就是最近最少使用的意思,LruCache 基于 LinkedHashMap 实现。LinkedHashMap 是在 HashMap 的基础之上进行了封装,除了具有哈希功能,还将数据插入到双向链表中维护。每次读取的数据会被移动到链表的尾部,当达到了缓存的最大的容量的时候就将链表的首部移出。使用 LruCache 的时候需要注意的是单位的问题,因为该 API 并不清楚要存储的数据是如何计算大小的,所以它提供了方法供我们实现大小的计算方式。(《Android 内存缓存框架 LruCache 的源码分析》)

DiskLruCache 与 LruCache 类似,也是用来实现缓存的,并且也是基于 LinkedHashMap 实现的。不同的是,它是基于磁盘缓存的,LruCache 是基于内存缓存的。所以,LinkedHashMap 能够存储的空间更大,但是读写的速率也更慢。使用 DiskLruCache 的时候需要到 Github 上面去下载。OkHttp 和 Glide 的磁盘缓存都是基于 DiskLruCache 开发的。DiskLruCahce 内部维护了一个日志文件,记录了读写的记录的信息。其他的基本都是基础的磁盘 IO 操作。

  • Glide 缓存的实现原理

1.2 List 相关

  • ArrayList 与 LinkedList 区别
  1. ArrayList 是基于动态数组,底层使用 System.arrayCopy() 实现数组扩容;查找值的复杂度为 O(1),增删的时候可能扩容,复杂度也比 LinkedList 高;如果能够大概估出列表的长度,可以通过在 new 出实例的时候指定一个大小来指定数组的初始大小,以减少扩容的次数;适合应用到查找多于增删的情形,比如作为 Adapter 的数据的容器
  2. LinkedList 是基于双向链表;增删的复杂度为 O(1),查找的复杂度为 O(n);适合应用到增删比较多的情形
  3. 两种列表都不是线程安全的,Vector 是线程安全的,但是它的线程安全的实现方式是通过对每个方法进行加锁,所以性能比较低。

如果想线程安全地使用这列表类(可以参考下面的问题)

  • 如何实现线程间安全地操作 List?

我们有几种方式可以线程间安全地操作 List. 具体使用哪种方式,可以根据具体的业务逻辑进行选择。通常有以下几种方式:

  1. 第一是在操作 List 的时候使用 sychronized 进行控制。我们可以在我们自己的业务方法上面进行加锁来保证线程安全。
  2. 第二种方式是使用 Collections.synchronizedList() 进行包装。这个方法内部使用了私有锁来实现线程安全,就是通过对一个全局变量进行加锁。调用我们的 List 的方法之前需要先获取该私有锁。私有锁可以降低锁粒度。
  3. 第三种是使用并发包中的类,比如在读多写少的情况下,为了提升效率可以使用 CopyOnWriteArrayList 代替 ArrayList,使用 ConcurrentLinkedQueue 代替 LinkedList. 并发容器中的 CopyOnWriteArrayList 在读的时候不加锁,写的时候使用 Lock 加锁。ConcurrentLinkedQueue 则是基于 CAS 的思想,在增删数据之前会先进行比较。

1.3 Map 相关

  • SparseArray 的原理

SparseArray 主要用来替换 Java 中的 HashMap,因为 HashMap 将整数类型的键默认装箱成 Integer (效率比较低). 而 SparseArray 通过内部维护两个数组来进行映射,并且使用二分查找寻找指定的键,所以它的键对应的数组无需是包装类型。SparseArray 用于当 HashMap 的键是 Integer 的情况,它会在内部维护一个 int 类型的数组来存储键。同理,还有 LongSparseArray, BooleanSparseArray 等,都是用来通过减少装箱操作来节省内存空间的。但是,因为它内部使用二分查找寻找键,所以其效率不如 HashMap 高,所以当要存储的键值对的数量比较大的时候,考虑使用 HashMap.

  • HashMap、ConcurrentHashMap 以及 HashTable
  • hashmap 如何 put 数据(从 hashmap 源码角度讲解)?(掌握 put 元素的逻辑)

HashMap (下称 HM) 是哈希表,ConcurrentHashMap (下称 CHM) 也是哈希表,它们之间的区别是 HM 不是线程安全的,CHM 线程安全,并且对锁进行了优化。对应 HM 的还有 HashTable (下称 HT),它通过对内部的每个方法加锁来实现线程安全,效率较低。

HashMap 的实现原理:HashMap 使用拉链法来解决哈希冲突,即当两个元素的哈希值相等的时候,它们会被方进一个桶当中。当一个桶中的数据量比较多的时候,此时 HashMap 会采取两个措施,要么扩容,要么将桶中元素的数据结构从链表转换成红黑树。因此存在几个常量会决定 HashMap 的表现。在默认的情况下,当 HashMap 中的已经被占用的桶的数量达到了 3/4 的时候,会对 HashMap 进行扩容。当一个桶中的元素的数量达到了 8 个的时候,如果桶的数量达到了 64 个,那么会将该桶中的元素的数据结构从链表转换成红黑树。如果桶的数量还没有达到 64 个,那么此时会对 HashMap 进行扩容,而不是转换数据结构。

从数据结构上,HashMap 中的桶中的元素的数据结构从链表转换成红黑树的时候,仍然可以保留其链表关系。因为 HashMap 中的 TreeNode 继承了 LinkedHashMap 中的 Entry,因此它存在两种数据结构。

HashMap 在实现的时候对性能进行了很多的优化,比如使用截取后面几位而不是取余的方式计算元素在数组中的索引。使用哈希值的高 16 位与低 16 进行异或运算来提升哈希值的随机性。

因为每个桶的元素的数据结构有两种可能,因此,当对 HashMap 进行增删该查的时候都会根据结点的类型分成两种情况来进行处理。当数据结构是链表的时候处理起来都非常容易,使用一个循环对链表进行遍历即可。当数据结构是红黑树的时候处理起来比较复杂。红黑树的查找可以沿用二叉树的查找的逻辑。

下面是 HashMap 的插入的逻辑,所有的插入操作最终都会调用到内部的 putVal() 方法来最终完成。

    public V put(K key, V value) {
   
        return putVal(hash(key), key, value, false, true);
    }

    private V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
   
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        if ((tab = table) == null || (n = tab.length) == 0) {
    // 原来的数组不存在
            n = (tab = resize()).length;
        }

        i = (n - 1) & hash; // 取哈希码的后 n-1 位,以得到桶的索引
        p = tab[i]; // 找到桶
        if (p == null) {
    
            // 如果指定的桶不存在就创建一个新的,直接new 出一个 Node 来完成
            tab[i] = newNode(hash, key, value, null);
        } else {
    
            // 指定的桶已经存在
            Node<K,V> e; K k;

            if (p.hash == hash // 哈希码相同
                && ((k = p.key) == key || (key != null && key.equals(k))) // 键的值相同
            ) {
   
                // 第一个结点与我们要插入的键值对的键相等
                e = p;
            } else if (p instanceof TreeNode) {
   
                // 桶的数据结构是红黑树,调用红黑树的方法继续插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            } else {
   
                // 桶的数据结构是链表,使用链表的处理方式继续插入
                for (int binCount = 0; ; ++binCount) {
   
                    if ((e = p.next) == null) {
   
                        // 已经遍历到了链表的结尾,还没有找到,需要新建一个结点
                        p.next = newNode(hash, key, value, null);
                        // 插入新结点之后,如果某个链表的长度 >= 8,则要把链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) {
   
                            treeifyBin(tab, hash);
                        }
                        break;
                    }
                    if (e.hash == hash // 哈希码相同 
                        && ((k = e.key) == key || (key != null && key.equals(k))) // 键的值相同
                    ) {
   
                        // 说明要插入的键值对的键是存在的,需要更新之前的结点的数据
                        break;
                    }
                    p = e;
                }
            }

            if (e != null) {
    
                // 说明指定的键是存在的,需要更新结点的值
                V oldValue = e.value;
                if 

你可能感兴趣的:(Android,基础,进阶,Android,面试,Java,Kotlin,sychronized)