| 常见的Error | | |

| — | — | — |

| OutOfMemoryError | StackOverflowError | NoClassDeffoundError |

| 常见的Exception | | |

| — | — | — |

| 常见的非检查性异常 | | |

| ArithmeticException | ArrayIndexOutOfBoundsException | ClassCastException |

| IllegalArgumentException | IndexOutOfBoundsException | NullPointerException |

| NumberFormatException | SecurityException | UnsupportedOperationException |

| 常见的检查性异常 | | |

| IOException | CloneNotSupportedException | IllegalAccessException |

| NoSuchFieldException | NoSuchMethodException | FileNotFoundException |

内部类

===

  • 非静态内部类没法在外部类的静态方法中实例化。

  • 非静态内部类的方法可以直接访问外部类的所有数据,包括私有的数据。

  • 在静态内部类中调用外部类成员,成员也要求用 static 修饰。

  • 创建静态内部类的对象可以直接通过外部类调用静态内部类的构造器;创建非静态的内部类的对象必须先创建外部类的对象,通过外部类的对象调用内部类的构造器。

匿名内部类

  • 匿名内部类不能定义任何静态成员、方法

  • 匿名内部类中的方法不能是抽象的

  • 匿名内部类必须实现接口或抽象父类的所有抽象方法

  • 匿名内部类不能定义构造器

  • 匿名内部类访问的外部类成员变量或成员方法必须用 final 修饰

多态

==

  • 父类的引用可以指向子类的对象

  • 创建子类对象时,调用的方法为子类重写的方法或者继承的方法

  • 如果我们在子类中编写一个独有的方法,此时就不能通过父类的引用创建的子类对象来调用该方法

抽象和接口

=====

  • 抽象类不能有对象(不能用 new 关键字来创建抽象类的对象)

  • 抽象类中的抽象方法必须在子类中被重写

  • 接口中的所有属性默认为:public static final;

  • 接口中的所有方法默认为:public abstract;

集合框架

====

  • List接口存储一组不唯一,有序(插入顺序)的对象, Set接口存储一组唯一,无序的对象。

HashMap

结构图

  • JDK 1.7 HashMap 结构图

  • JDK 1.8 HashMap 结构图

HashMap 的工作原理

HashMap 基于 hashing 原理,我们通过 put() 和 get() 方法储存和获取对象。当我们将键值对传递给 put() 方法时,它调用键对象的 hashCode() 方法来计算 hashcode,让后找到 bucket 位置来储存 Entry 对象。当两个对象的 hashcode 相同时,它们的 bucket 位置相同,‘碰撞’会发生。因为 HashMap 使用链表存储对象,这个 Entry 会存储在链表中,当获取对象时,通过键对象的 equals() 方法找到正确的键值对,然后返回值对象。

如果 HashMap 的大小超过了负载因子(load factor)定义的容量,怎么办?

默认的负载因子大小为 0.75,也就是说,当一个 map 填满了 75% 的 bucket 时候,和其它集合类(如 ArrayList 等)一样,将会创建原来 HashMap 大小的两倍的 bucket 数组,来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing,因为它调用 hash 方法找到新的 bucket 位置。

为什么 String, Interger 这样的 wrapper 类适合作为键?

因为 String 是不可变的,也是 final 的,而且已经重写了 equals() 和 hashCode() 方法了。其他的 wrapper 类也有这个特点。不可变性是必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的 hashcode 的话,那么就不能从 HashMap 中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个 field 声明成 final 就能保证 hashCode 是不变的,那么请这么做吧。因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会小些,这样就能提高 HashMap 的性能。

HashMap 与 HashTable 对比

HashMap 是非 synchronized 的,性能更好,HashMap 可以接受为 null 的 key-value,而 Hashtable 是线程安全的,比 HashMap 要慢,不接受 null 的 key-value。

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>

implements Map<K,V>, Cloneable, Serializable {

···

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

···

public V get(Object key) {

Node<K,V> e;

return (e = getNode(hash(key), key)) == null ? null : e.value;

}

···

}

HashTable.java

public class Hashtable<K,V>

extends Dictionary<K,V>

implements Map<K,V>, Cloneable, java.io.Serializable {

···

public synchronized V put(K key, V value) {

// Make sure the value is not null

if (value == null) {

throw new NullPointerException();

}

···

addEntry(hash, key, value, index);

return null;

}

···

public synchronized V get(Object key) {

HashtableEntry<?,?> tab[] = table;

int hash = key.hashCode();

int index = (hash & 0x7FFFFFFF) % tab.length;

for (HashtableEntry<?,?> e = tab[index] ; e != null ; e = e.next) {

if ((e.hash == hash) && e.key.equals(key)) {

return (V)e.value;

}

}

return null;

}

···

}

ConcurrentHashMap

Base 1.7

ConcurrentHashMap 最外层不是一个大的数组,而是一个 Segment 的数组。每个 Segment 包含一个与 HashMap 数据结构差不多的链表数组。

在读写某个 Key 时,先取该 Key 的哈希值。并将哈希值的高 N 位对 Segment 个数取模从而得到该 Key 应该属于哪个Segment,接着如同操作 HashMap 一样操作这个 Segment。

Segment 继承自 ReentrantLock,可以很方便的对每一个 Segmen 上锁。

对于读操作,获取 Key 所在的 Segment 时,需要保证可见性。具体实现上可以使用volatile关键字,也可使用锁。但使用锁开销太大,而使用volatile时每次写操作都会让所有CPU内缓存无效,也有一定开销。ConcurrentHashMap 使用如下方法保证可见性,取得最新的Segment:

Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)

获取 Segment 中的 HashEntry 时也使用了类似方法:

HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile

(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)

对于写操作,并不要求同时获取所有 Segment 的锁,因为那样相当于锁住了整个Map。它会先获取该 Key-Value 对所在的 Segment 的锁,获取成功后就可以像操作一个普通的 HashMap 一样操作该 Segment,并保证该 Segment 的安全性。同时由于其它 Segment 的锁并未被获取,因此理论上可支持 concurrencyLevel(等于Segment的个数)个线程安全的并发读写。

获取锁时,并不直接使用 lock 来获取,因为该方法获取锁失败时会挂起。事实上,它使用了自旋锁,如果 tryLock 获取锁失败,说明锁被其它线程占用,此时通过循环再次以 tryLock 的方式申请锁。如果在循环过程中该 Key 所对应的链表头被修改,则重置 retry 次数。如果 retry 次数超过一定值,则使用 lock 方法申请锁。

这里使用自旋锁是因为自旋锁的效率比较高,但是它消耗 CPU 资源比较多,因此在自旋次数超过阈值时切换为互斥锁。

Base 1.8

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:查询遍历链表效率太低。因此 1.8 做了一些数据结构上的调整。

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

ConcurrentHashMap.java

final V putVal(K key, V value, boolean onlyIfAbsent) {

if (key == null || value == null) throw new NullPointerException();

int hash = spread(key.hashCode());

int binCount = 0;

for (Node<K,V>[] tab = table;😉 {

Node<K,V> f; int n, i, fh;

if (tab == null || (n = tab.length) == 0)

tab = initTable();

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

if (casTabAt(tab, i, null,

new Node<K,V>(hash, key, value, null)))

break; // no lock when adding to empty bin

}

else if ((fh = f.hash) == MOVED)

tab = helpTransfer(tab, f);

else {

V oldVal = null;

synchronized (f) {

if (tabAt(tab, i) == f) {

if (fh >= 0) {

binCount = 1;

···

}

else if (f instanceof TreeBin) {

···

}

else if (f instanceof ReservationNode)

throw new IllegalStateException(“Recursive update”);

}

}

···

}

addCount(1L, binCount);

return null;

}

ArrayList

ArrayList.java

public class ArrayList extends AbstractList

implements List, RandomAccess, Cloneable, java.io.Serializable

···

// 增加元素

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

···

// 删除元素

public E remove(int index) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

modCount++;

E oldValue = (E) elementData[index];

int numMoved = size - index - 1;

if (numMoved > 0)

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[–size] = null; // clear to let GC do its work

return oldValue;

}

···

// 查找元素

public E get(int index) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

return (E) elementData[index];

}

···

}

LinkedList

LinkedList 本质上是一个双向链表的存储结构。

LinkedList.java

public class LinkedList

extends AbstractSequentialList

implements List, Deque, Cloneable, java.io.Serializable

{

····

private static class Node {

E item;

Node next;

Node prev;

Node(Node prev, E element, Node next) {

this.item = element;

this.next = next;

this.prev = prev;

}

}

···

// 增加元素

void linkLast(E e) {

final Node l = last;

final Node newNode = new Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

···

// 删除元素

E unlink(Node x) {

final E element = x.item;

final Node next = x.next;

final Node prev = x.prev;

if (prev == null) {

first = next;

} else {

prev.next = next;

x.prev = null;

}

if (next == null) {

last = prev;

} else {

next.prev = prev;

x.next = null;

}

x.item = null;

size–;

modCount++;

return element;

}

···

// 查找元素

Node node(int index) {

// assert isElementIndex(index);

if (index < (size >> 1)) {

Node x = first;

for (int i = 0; i < index; i++)

x = x.next;

return x;

} else {

Node x = last;

for (int i = size - 1; i > index; i–)

x = x.prev;

return x;

}

}

···

}

对于元素查询来说,ArrayList 优于 LinkedList,因为 LinkedList 要移动指针。对于新增和删除操作,LinedList 比较占优势,因为 ArrayList 要移动数据。

CopyOnWriteArrayList

CopyOnWriteArrayList 是线程安全容器(相对于 ArrayList),增加删除等写操作通过加锁的形式保证数据一致性,通过复制新集合的方式解决遍历迭代的问题。

CopyOnWriteArrayList.java

public class CopyOnWriteArrayList

implements List, RandomAccess, Cloneable, java.io.Serializable {

final transient Object lock = new Object();

···

// 增加元素

public boolean add(E e) {

synchronized (lock) {

Object[] elements = getArray();

int len = elements.length;

Object[] newElements = Arrays.copyOf(elements, len + 1);

newElements[len] = e;

setArray(newElements);

return true;

}

}

···

// 删除元素

public E remove(int index) {

synchronized (lock) {

Object[] elements = getArray();

int len = elements.length;

E oldValue = get(elements, index);

int numMoved = len - index - 1;

if (numMoved == 0)

setArray(Arrays.copyOf(elements, len - 1));

else {

Object[] newElements = new Object[len - 1];

System.arraycopy(elements, 0, newElements, 0, index);

System.arraycopy(elements, index + 1, newElements, index,

numMoved);

setArray(newElements);

}

return oldValue;

}

}

···

// 查找元素

private E get(Object[] a, int index) {

return (E) a[index];

}

}

反射

==

try {

Class cls = Class.forName(“com.jasonwu.Test”);

//获取构造方法

Constructor[] publicConstructors = cls.getConstructors();

//获取全部构造方法

Constructor[] declaredConstructors = cls.getDeclaredConstructors();

//获取公开方法

Method[] methods = cls.getMethods();

//获取全部方法

Method[] declaredMethods = cls.getDeclaredMethods();

//获取公开属性

Field[] publicFields = cls.getFields();

//获取全部属性

Field[] declaredFields = cls.getDeclaredFields();

Object clsObject = cls.newInstance();

Method method = cls.getDeclaredMethod(“getModule1Functionality”);

Object object = method.invoke(null);

} catch (ClassNotFoundException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

} catch (InstantiationException e) {

e.printStackTrace();

} catch (NoSuchMethodException e) {

e.printStackTrace();

} catch (InvocationTargetException e) {

e.printStackTrace();

}

单例

==

饿汉式

public class CustomManager {

private Context mContext;

private static final Object mLock = new Object();

private static CustomManager mInstance;

public static CustomManager getInstance(Context context) {

synchronized (mLock) {

if (mInstance == null) {

mInstance = new CustomManager(context);

}

return mInstance;

}

}

private CustomManager(Context context) {

this.mContext = context.getApplicationContext();

}

}

双重检查模式

public class CustomManager {

private Context mContext;

private volatile static CustomManager mInstance;

public static CustomManager getInstance(Context context) {

// 避免非必要加锁

if (mInstance == null) {

synchronized (CustomManger.class) {

if (mInstance == null) {

mInstacne = new CustomManager(context);

}

}

}

return mInstacne;

}

private CustomManager(Context context) {

this.mContext = context.getApplicationContext();

}

}

静态内部类模式

public class CustomManager{

private CustomManager(){}

private static class CustomManagerHolder {

private static final CustomManager INSTANCE = new CustomManager();

}

public static CustomManager getInstance() {

return CustomManagerHolder.INSTANCE;

}

}

静态内部类的原理是: 当 SingleTon 第一次被加载时,并不需要去加载 SingleTonHoler,只有当 getInstance() 方法第一次被调用时,才会去初始化 INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance 方法并没有多次去 new 对象,取的都是同一个 INSTANCE 对象。

()()()

缺点在于无法传递参数,如Context等

线程

==

线程是进程中可独立执行的最小单位,也是 CPU 资源(时间片)分配的基本单位。同一个进程中的线程可以共享进程中的资源,如内存空间和文件句柄。

属性

| 属性 | 说明 |

| — | — |

| id | 线程 id 用于标识不同的线程。编号可能被后续创建的线程使用。编号是只读属性,不能修改 |

| name | 名字的默认值是 Thread-(id) |

| daemon | 分为守护线程和用户线程,我们可以通过 setDaemon(true) 把线程设置为守护线程。守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC 线程就是一个守护线程。setDaemon() 要在线程启动前设置,否则 JVM 会抛出非法线程状态异常,可被继承。 |

| priority | 线程调度器会根据这个值来决定优先运行哪个线程(不保证),优先级的取值范围为 1~10,默认值是 5,可被继承。Thread 中定义了下面三个优先级常量: - 最低优先级:MIN_PRIORITY = 1 - 默认优先级:NORM_PRIORITY = 5 - 最高优先级:MAX_PRIORITY = 10 |

状态

| 状态 | 说明 |

| — | — |

| New | 新创建了一个线程对象,但还没有调用start()方法。 |

| Runnable | Ready 状态 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取 cpu 的使用权。Running 绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。 |

| Blocked | 线程因为某种原因放弃了cpu 使用权(等待锁),暂时停止运行 |

| Waiting | 线程进入等待状态因为以下几个方法: - Object#wait() - Thread#join() - LockSupport#park() |

| Timed Waiting | 有等待时间的等待状态。 |

| Terminated | 表示该线程已经执行完毕。 |

状态控制

  • wait() / notify() / notifyAll()
wait()notify()notifyAll()java.lang.IllegalMonitorStateException

| 方法 | 说明 |

| — | — |

wait()
notify()
notifyAll()

被移动的线程状态由 Running 变为 Blocked,notifyAll 方法调用后,等待线程依旧不会从 wait() 返回,需要调用 notify() 或者 notifyAll() 的线程释放掉锁后,等待线程才有机会从 wait() 返回。

  • join() / sleep() / yield()
join()
sleep(long)join()
yield()

volatile

========

当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,因此在读取 volatile 类型的变量时总会返回最新写入的值。

当一个变量定义为 volatile 之后,将具备以下特性:

  • 保证此变量对所有的线程的可见性,不能保证它具有原子性(可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的)

  • 禁止指令重排序优化

  • volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行

AtomicInteger 中主要实现了整型的原子操作,防止并发情况下出现异常结果,其内部主要依靠 JDK 中的 unsafe 类操作内存中的数据来实现的。volatile 修饰符保证了 value 在内存中其他线程可以看到其值得改变。CAS(Compare and Swap)操作保证了 AtomicInteger 可以安全的修改value 的值。

synchronized

============

当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。Monitor 是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的线程同步。

根据获取的锁分类

获取对象锁

  • synchronized(this|object) {}

  • 修饰非静态方法

获取类锁

  • synchronized(类.class) {}

  • 修饰静态方法

原理

同步代码块:

  • monitorenter 和 monitorexit 指令实现的

同步方法

  • 方法修饰符上的 ACC_SYNCHRONIZED 实现

Lock

====

public interface Lock {

void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

void unlock();

Condition newCondition();

}

| 方法 | 说明 |

| — | — |

lock()
lockInterruptibly()
tryLock()
tryLock(long,TimeUnit)

锁的分类

悲观锁、乐观锁

悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在 Java 中是通过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

自旋锁、适应性自旋锁

阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是 10 次,可以使用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

死锁

当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。

引用类型

====

强引用 > 软引用 > 弱引用

| 引用类型 | 说明 |

| — | — |

| StrongReferenc(强引用) | 当一个对象具有强引用,那么垃圾回收器是绝对不会的回收和销毁它的,非静态内部类会在其整个生命周期中持有对它外部类的强引用 |

| WeakReference (弱引用) | 在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用的话,该对象会被回收 |

| SoftReference(软引用) | 如果一个对象只具有软引用,若内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,才会回收这些对象的内存 |

| PhantomReference(虚引用) | 一个只被虚引用持有的对象可能会在任何时候被 GC 回收。虚引用对对象的生存周期完全没有影响,也无法通过虚引用来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到 ReferenceQueue 来判断是否被GC,这也是唯一判断对象是否被 GC 的途径)。 |

动态代理

====

示例:

// 定义相关接口

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

【Android高级架构视频学习资源】

用 -XX:PreBlockSpin 来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用 unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

死锁

当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。

引用类型

====

强引用 > 软引用 > 弱引用

| 引用类型 | 说明 |

| — | — |

| StrongReferenc(强引用) | 当一个对象具有强引用,那么垃圾回收器是绝对不会的回收和销毁它的,非静态内部类会在其整个生命周期中持有对它外部类的强引用 |

| WeakReference (弱引用) | 在垃圾回收器运行的时候,如果对一个对象的所有引用都是弱引用的话,该对象会被回收 |

| SoftReference(软引用) | 如果一个对象只具有软引用,若内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,才会回收这些对象的内存 |

| PhantomReference(虚引用) | 一个只被虚引用持有的对象可能会在任何时候被 GC 回收。虚引用对对象的生存周期完全没有影响,也无法通过虚引用来获取对象实例,仅仅能在对象被回收时,得到一个系统通知(只能通过是否被加入到 ReferenceQueue 来判断是否被GC,这也是唯一判断对象是否被 GC 的途径)。 |

动态代理

====

示例:

// 定义相关接口

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-iRqNUHgr-1647442356488)]

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!