写点什么

Android 面试指南(三),kotlin 匿名内部类写法

用户头像
Android架构
关注
发布于: 刚刚

SparseArray 理解

原理

装箱,int 数据类型---->Integer 对象,拆箱,Integer 对象---->int 数据类型


默认容量是 10


  • key 是 int 值(避免装箱问题),使用二分查找寻找 key,同样也是用二分插入,从小到大排列好的

  • 两个数组,一组存放 key(int []),一组存放 value(object [])


mKeys[i] = key;mValues[i] = value;


  • 如果冲突,直接替换 value 的值


二分插入:


while (lo <= hi) {//二分法一分而二,数组中间下标 final int mid = (lo + hi) >>> 1;//二分法一分而二,数组中间下标处的值 final int midVal = array[mid];


if (midVal < value) {/**如果数组中间处的值比要找的值小,代表要找的值在数组的中后部部分,所以当前下标取值为 mid + 1/lo = mid + 1;} else if (midVal > value) {/*如果数组中间处的值比要找的值大,代表要找的值在数组的前中部部分,所以当前下标取值为 mid - 1*/hi = mid - 1;} else {//数组中间处的值与要找的值相等,直接返回数组中部的下标 midreturn mid; // value found}}


第一个值放到最中间位置


第二个值如果大于中间的值放置在左边的中间位置


………….


put 方法中,容量充足,计算 key 值所需存放的 index,如果 key 相同,就直接替换 value,如果不同,就 insert 数组,后续 index 元素后移,新 key 放置在 index 上

较 HashMap 的优点
  • 节省内存

  • 性能更好,避免装箱问题

  • 数据量不达到千级,key 为 int 值,可以用 SparseArray 替换 HashMap

SparseArray 与 HashMap 的比较,应用场景是?

  1. SparseArray 采用的不是哈希算法,HashMap 采用的是哈希算法

  2. SparseArray 采用的是两个一维数组分别用于存储键和值,HashMap 采用的是一维数组+单向链表/红黑树

  3. SparseArray key 只能是 int 类型,而 HashMap 可以任何类型

  4. SparseArray key 是有序存储(升序),而 HashMap 不是

  5. SparseArray 默认容量是 10,而 HashMap 默认容量是 16

  6. SparseArray 内存使用要优于 HashMap,因为:


  • SparseArray key 是 int 类型,而 HashMap 是 Object

  • SparseArray value 的存储被不像 HashMap 一样需要额外的需要一个实体类(Node)进行包装


  1. SparseArray 查找元素总体而言比 HashMap 要逊色,因为 SparseArray 查找是需要经过二分法的过程,而 HashMap 不存在冲突的情况其技术处的 hash 对应的下标直接就可以取到值


针对上面与 HashMap 的比较,采用 SparseArray 还是 HashMap,建议根据如下需求选取:


  1. 如果对内存要求比较高,而对查询效率没什么大的要求,可以是使用 SparseArray

  2. 数量在百级别的 S


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


parseArray 比 HashMap 有更好的优势 3. 要求 key 是 int 类型的,因为 HashMap 会对 int 自定装箱变成 Integer 类型 4. 要求 key 是有序的且是升序

ArrayMap 的理解

内部也使用二分算法进行存储和查找,设计上更多考虑了内存中的优化


  • int []存储 hash 值,array[index]存储 key,array[index+1]存储 value


数据量最好在千级以内

ArrayMap 和 SparseArray 怎么进行选取?

  1. 如果 key 为 int,那么选取 SparseArray 进行存储, 不存在封/拆箱问题

  2. 如果 key 不为 int,则使用 ArrayMap

TreeMap 的理解

TreeMap 是一个二叉树的结构,红黑树


不允许重复的 key


TreeMap 没有调优选项,因为其红黑树总保持在平衡状态

TreeMap 和 HashMap 的区别?

  1. TreeMap 由红黑树构成,HashMap 由数组+链表/红黑树构成

  2. HashMap 元素没有顺序,TreeMap 元素会根据可以进行升序排序

  3. HashMap 进行插入,查找,删除最好,TreeMap 进行自然顺序便利或者自定义顺序便利比较好

ThreadLocal 的理解

面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)


线程隔离,数据不交叉


  • ThreadLocalMap,每个 thread 都存在一个变量 ThreadLocalMap threadLocals

  • threadLocalMap 中存在 Entry,同 ThreadLocal 之间为弱引用关系

  • ThreadLocalMap 中 key 为 ThreadLocal 的弱引用,value 为 Entry,内部为一个 object 对象

  • table 默认大小为 16,存在初始容量(16)和阈值(16*2/3)

  • 在 ThreadLocal 中使用 get()和 set()方法初始化 threadLocals

  • get、set、remove 方法将 key==null 的数据清除

  • table 是环形数组


线性探测法避免哈希冲突,增量查找没有被占用的地方


通过 hashcode 计算索引位置,如果 key 值相同,则替换,不同就 nextIndex,继续判断,直到插入数据


ThreadLocal 就是管理每个线程中的 ThreadLocalMap,所以线程隔离了。

ThreadLocalMap 的理解

新建 ThreadLcoal 的时候,创建一个 ThreadLocalMap 对象,计算 hash 的时候使用 0x61c88647 这个值,他是黄金分割数,导致计算出来的 hash 值比较均匀,这样回大大减少 hash 冲突,内部在采用线性探测法解决冲突 set:


  1. 根据 key 计算出数组索引值

  2. 遍历该索引值的链表,如果为空,直接将 value 赋值,如果 key 相等,直接更新 value,如果 key 不相等,使用线性探测法再次检测。

ThreadLocal 使用弱引用的原因

key 使用了弱引用,如果 key 使用强引用,那么当 ThreadLocal 的对象被回收了,但 ThreadLocalMap 还持有 ThreadLocal 的强引用,回导致 ThreadLocal 不会被回收,导致内存泄漏

ThreadLocal 的内存泄漏

  • 避免使用 static 修饰 ThreadLocal:延长生命周期,可能造成内存泄漏

  • ThreadLocal 弱引用被 gc 回收后,则 key 为 null,object 对象没有被回收,只有当再次调用 set,get,remove 方法的时候才会清楚 key 为 null 的对象

ThreadLocalMap 清理过期 key 的方式

  1. 探测式清理 本该放在 4 的位置上的值,放到了 7 的位置上,当 5 过时后,将 7 的数据挪到 5 的位置上

  2. 启发式清理 遍历数组,清理数据

ConcurrentHashMap 和 HashMap 的区别

jdk 1.7 ReentrantLock+segments + hashEntry(不可变)



  • 线程安全,分段线程锁,hashtable 是整段锁,所以性能有所提高

  • 默认分配 16 个锁,比 Hashtable 效率高 16 倍

  • hashEnty 是 final 的,不能被修改,只要被修改,该节点之前的链就要重新创建,采用头插插入,所以顺序反转

  • 获取 size,因为是多线程访问,所以 size 会获取三遍,如果前后两个相等就返回,假设不相等,就将 Segment 加锁后计算。


jdk 1.8 : synchronized +node+volatile+红黑树


put:


  1. 根据 key 的 hash 值算出 Node 数组的相应位置

  2. 如果该 Node 不为空,且当前该节点不处于移动状态,则对节点加 synchronized 锁,进行遍历节点插入操作

  3. 如果是红黑树节点,向红黑树插入操作

  4. 如果大于 8 个,拓展为红黑树


get:


  1. 计算 hash 值,定位到该 table 索引位置,如果是首节点符合就返回

  2. 如果遇到扩容的时候,会调用标志正在扩容节点 ForwardingNode 的 find 方法,通知在新表中查找该节点,匹配就返回

  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回 null


1.7 和 1.8 的区别:


  1. 1.7:ReentrantLock+segments + hashEntry(不可变)


1.8:synchronized +node+volatile+红黑树


  1. 1.8 的锁的粒度更低,锁的是一个链表(table[i]),而 1.7 锁的是一个小的 hashmap(segement)

  2. ReentrantLock 性能比 synchronized 差


扩容:


1.7 下进行小 HashMap(segement)扩容操作


1.8 下使用 synchrozied 节点加锁,所以可以通过多个线程扩容处理。一个线程创建新的 ConcurrentHashMap,并设置大小,多个线程将旧的内容添加到新的 map 中,如果添加过的内容就会设置标记,其他线程就不会处理

为什么只有 hashmap 可以存储 null 值和 null 键

因为 hashmap 是线程不安全的,而在其他中都是线程安全的,在多线程访问时,无法判断 key 为 null 是没有找到,还是 key 为 null

常见锁

锁的分类

  1. 公平锁/非公平锁


  • 公平锁:多个线程按照申请锁的顺序获取锁。

  • 非公平锁:多个线程申请锁并不是按照顺序获取锁,有可能先申请后获取锁。(Synchronized)


ReentrantLock 默认是非公平锁,通过构造传参可设置为公平锁。非公平锁的优点在于吞吐量比公平锁大


  1. 可重入锁:又名递归锁,指在外层方法获取锁以后,在进入内层方法也会自动获取锁。


synchronized void setA() throws Exception(){Thread.sleep(1000);setB();}


synchronized void setB() throws Exception(){Thread.sleep(1000);}


如果不是可重入锁,那么 setB 方法不会被当前线程执行,容易造成死锁


synchronized 是可重入锁


  1. 独享锁/共享锁


  • 独享锁:一个锁一次只能被一个线程所持有(ReentrantLock,synchronized)

  • 共享锁:一个锁被多个线程所持有。(ReadWriteLock)


  1. 互斥锁/读写锁 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

  2. 互斥锁在 Java 中的具体实现就是 ReentrantLock

  3. 读写锁在 Java 中的具体实现就是 ReadWriteLock

  4. 乐观锁/悲观锁


  • 悲观锁:对同一数据的并发操作,一定会发生修改的。(利用各种锁实现)

  • 乐观锁:对同一数据的并发操作,一定不会发生修改的。(无锁编程,CAS 算法,自旋实现原子操作的更新)


  1. 分段锁

  2. 是一种锁的设计,并不是具体的锁,在 1.7 版本的 ConcurrentHashMap 中,使用分段锁设计,该分段锁又称为 Segment,map 中每一个链表由 ReentrantLock 修饰

  3. 偏向锁/轻量级锁/重量级锁 这三种锁是描述 synchronized 的三种状态。


  • 偏向锁:一段同步代码一直被一个线程访问,那么会自动获取锁,降低获取锁的代价

  • 轻量级锁:当锁是偏向锁的时候,被另一个线程访问,偏向锁会升级为轻量级锁,其他线程通过自旋的方式获取锁,不会阻塞,提高性能

  • 重量级锁:在轻量级锁的基础上,自旋达到上限就会阻塞,升级为重量级锁,会让其他线程进入阻塞,影响性能。


锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后无法降为偏向锁,这种升级无法降级的策略目的就是为了提高获得锁和释放锁的效率。


  1. 自旋锁

  2. 获取锁的过程中,不会立即阻塞,会采用循环的方式获取锁,减少线程切换上下文的消耗,缺点是循环会消耗 cpu

java 中常用锁的类型

  1. synchronized:非公平,悲观,独享,互斥,可重入,重量级锁

  2. ReentrantLock:默认非公平(可公平),悲观,独享,互斥,可重入,重量级锁


CAS,全称为 Compare-And-Swap,是一条 CPU 的原子指令,其作用是让 CPU 比较后原子地更新某个位置的值,实现方式是基于硬件平台的汇编指令,就是说 CAS 是靠硬件实现的,JVM 只是封装了汇编调用,那些 AtomicInteger 类便是使用了这些封装后的接口。

synchronized 和 volatile

简述 synchronized 的原理

可见性:表示 A 修改的值对于 B 执行时可以看见 A 修改后的值


  • 内部使用 monitorenter 指令,同时只有一个线程可以获取 monitor

  • 未获取 monitor 的线程会被阻塞,等待获取 monitor

  • 线程 A 获取主内存值后加锁,在本地内存更新值(临时区)后,推送到主内存,通过 synchronized 隐式通知线程 B 访问主存获取值,在 B 的把本地内存更新值后推送到主存,重复以上操作。


通过 Monitor 对象来实现方法和代码块的同步,存在 monitorEnter 和 monitorExit 指令,插入程序中,在一个线程访问时,通过 Monitor 进行线程阻塞

synchronized 修饰静态方法、?静态方法区别

静态方法:该类的对象,new 出来的多个实例对象是被一个锁锁住的,多线程访问需要等待


非静态方法:实例对象

volatile

修饰成员变量,保证可见性,下一个操作再上一个操作之上。++操作不保证和原子性,


将本地缓存同步到主存中,使其他本地缓存失效,本地缓存通过嗅探检查自己的缓存是否过期。(下一次访问,主存不会主动通知)


volatile 无法保证原子性,可以使用乐观锁的重试机制进行优化

synchronized 和 volatile 区别

  • Synchronized 引起线程阻塞,而 volatile 不会

  • 区别在于,synchronized 是隐式通知 B 去主存获取值,volatile 是 B 主动通过嗅探的方法发现自己的内存过期后去主存做同步

  • synchronized:先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。

  • 都存在可见性,但是 volatile 不具备原子性,所以不会造成线程阻塞


假设某一时刻 i=10,线程 A 读取 10 到自己的工作内存,A 对该值进行加一操作,但正准备将 11 赋给 i 时,由于此时 i 的值并未改变,B 读取了主存的值仍为 10 到自己的工作内存,并执行了加一操作,正准备将 11 赋给 i 时,A 将 11 赋给了 i,由于 volatile 的影响,立即同步到主存,主存中的值为 11,并使得 B 工作内存中的 i 失效,B 执行第三步,虽然此时 B 工作内存中的 i 失效了,但是第三步是将 11 赋给 i,对 B 来说,我只是赋值操作,并没有使用 i 这个动作,所以这一步并不会去刷新主存,B 将 11 赋值给 i,并立即同步到主存,主存中的值仍为 11。虽然 A/B 都执行了加一操作,但主存却为 11,这就是最终结果不是 10000 的原因。


  • synchronized 修饰方法,类,变量,代码块,volatile 只能修饰变量

synchronized 修饰不同对象的区别

  1. 修饰类:作用的对象是这个类的所有对象

  2. 方法:作用对象是这个方法的对象

  3. 静态方法:作用对象是这个类的对象

  4. 代码块:作用对象是这个代码块的对象

悲观锁和乐观锁(CAS)

悲观锁:当前线程获得锁会阻塞其他线程(sychronized)


乐观锁:不会添加锁,会存在三个值内存实际值,内存的旧值,更新的新值,如果内存实际值和旧值相等,则没有线程修改该值,将更新的新值直接赋值给内存,如果不相等,就重新尝试赋值操作(volatile)


CAS 的缺点:


  1. ABA 问题,A->B->A,乐观锁认为没有变化,都是 A,所以直接赋值

  2. 重新赋值的话,会导致时间过长。

ReentrantLock

CAS+AQS 实现,乐观锁


AQS(单链表队列)维护一个等待队列,将获取不到锁的线程放入到队列中进行等待,当当前线程执行结束后,进行出队操作,使用一个 volatile 的 int 成员变量(state)来表示同步状态


通过 ReentrantLock 的 Lock 方法进行加锁


通过 ReentrantLock 的 unLock 方法进行解锁

线程

新建线程有几种方式?

  1. new Thread

  2. 新建 Runnable 对象

  3. 新建 Callable 或者 Future 对象

  4. 线程池使用

new Thread 的弊端

执行一个异步任务你还只是如下 new Thread 吗? new Thread 的弊端如下:


  1. 每次 new Thread 新建对象性能差。

  2. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或 oom。

  3. 缺乏更多功能,如定时执行、定期执行、线程中断。


相比 new Thread,Java 提供的四种线程池的好处在于:


  1. 重用存在的线程,减少对象创建、消亡的开销,性能佳。

  2. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

  3. 提供定时执行、定期执行、单线程、并发数控制等功能。

线程池

简述线程池

线程的 5 种状态

  • NEW:创建一个新线程

  • RUNNABLE:可运行

  • BLOCKED:阻塞

  • WAITING:进入等待状态

  • TIMED_WAITING:等待结束,重新获取锁

  • TERMINATED:结束

  • RUNNING:运行中

  • READY:就绪



一般来说分为五大状态:



  1. 新建(New):

  2. 创建线程对象,进入新建状态。eg:Thread thread = new Thread();

  3. 就绪(Runnable):

  4. 调用 thread.start()方法,随时可被 cpu 执行

  5. 运行(Runnable):

  6. CPU 执行线程

  7. 阻塞(Blocked): 出于某些原因,cpu 放弃线程执行,线程进入暂停状态


  • 等待阻塞:调用 wait 方法,进行阻塞,线程等待某工作完成

  • 同步阻塞:在获取 Synchronized 同步锁时,进行等待

  • 其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。


  1. 死亡(Dead):

  2. 堪称执行完毕或者因异常退出,线程死亡,回收

start 和 run 的区别?sleep 和 wait 的区别?join,yield,interrupt

  • start 是启动一个线程

  • run 只是 Thread 的实现方法,主要实现是 Runnable 的接口回调 run 方法

  • sleep 不会释放对象锁,只是暂停了线程的运行,当指定时间到了,就恢复运行状态

  • wait 方法放弃对象锁,只有调用了 notify()方法,才会重新获取锁,进入运行状态

  • join 方法是规定线程的执行顺序,如果在 B 线程中调用了 A 的 join 方法,那么,直到 A 执行完毕,才会执行 B,按照顺序串行执行。实际内部方法是调用了 wait 方法,让 B 处于等待状态,A 执行完成后,启动 B


注意:wait 方法是调用 u 哦在线程放弃对象锁,所以在 B 线程调用 A 的 join 方法,只是让 B 等待了。


  • yield 方法,通知 cpu 该线程任务不紧急,可以被暂停让其他线程运行

  • interrupt 方法,中断通知线程,具体操作由线程执行,根据不同状态,执行不同逻辑

线程 t1、t2、t3,如何保证他们顺序执行?

t3 开始中调用 t2.join(),t2 开始中调用 t1.join()。


t1 执行完毕后,t2 中 t1.join()方法不阻塞,即 t1 执行完,执行 t2 中的方法,后续类似


使用 CountDownLacth,进行计数


public static void main(String[] args) {


final Thread t1 = new Thread(new Runnable() {


@Overridepublic void run() {System.out.println("t1");}});final Thread t2 = new Thread(new Runnable() {


@Overridepublic void run() {try {//引用 t1 线程,等待 t1 线程执行完 t1.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2");}});Thread t3 = new Thread(new Runnable() {


@Overridepublic void run() {try {//引用 t2 线程,等待 t2 线程执行完 t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t3");}});t3.start();t2.start();t1.start();}

什么是死锁

资源竞争互相等待


假设线程 A,线程 B,资源 A,资源 B


线程 A 访问资源 A,持有资源 A 锁,线程 B 访问资源 B,持有资源 B 锁,而后线程 A 要访问资源 B,但是线程 B 持有资源 B 锁,线程 A 等待,线程 B 要访问资源 A,但是线程 A 持有资源 A 锁。所以 B 等待。


结果就是 A、B 相互等待对方释放资源,造成死锁。

一个线程崩溃会影响其他线程吗?

不一定。


如果崩溃发生在堆区(线程共享区域),会导致其他线程崩溃。


如果崩溃发生在栈区(线程私有区域),不会导致其他线程的崩溃

java 反射

  1. 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;

  2. 每个类都会有一个与之对应的 Class 实例,从而每个类都可以获取 method 反射方法,并作用到其他实例身上;

  3. 反射也是考虑了线程安全的,放心使用;

  4. 反射使用软引用 relectionData 缓存 class 信息,避免每次重新从 jvm 获取带来的开销;

  5. 反射调用多次生成新代理 Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;

  6. 当找到需要的方法,都会 copy 一份出来,而不是使用原来的实例,从而保证数据隔离;

  7. 调度反射方法,最终是由 jvm 执行 invoke0()执行;


使用反射从 jvm 中的二进制码文件中读取数据

反射原理

.java-->.class-->java.lang.Class 对象


编译过程:


  • 将.java 文件编译成机器可以识别的二进制文件.class

  • .class 文件中存储着类文件的各种信息。

  • 比如版本号、类的名字、字段的描述和描述符、方法名称和描述、是不是 public、类索引、字段表集合,方法集合等等数据

  • JVM 从二进制文件.class 中取出并拿到内存解析

  • 类加载器获取类的二进制信息,并在内存中生成 java.lang.Class 对象

  • 最后开始类的生命周期并初始化(先静态后非静态和构造,先父类在子类)


而反射操作的就是内存中的 java.lang.Class 对象。


总结来说.class 是一种有顺序的结构文件,而 Class 对象就是对这种文件的一种表示,所以我们能从 Class 对象中获取关于类的所有信息,这就是反射的原理。

为什么反射耗时?

  1. 校验时间长

  2. 基本类型的封箱和拆箱

  3. 方法内联

什么是内联函数?

方法调用过多会进行内敛优化,减少方法的嵌套层级,加快执行,缓解栈的空间存储

反射可以修改 final 类型的成员变量吗?

已知 final 修饰后不会被修改,所以获取这个变量的时候就直接帮你在编译阶段就给赋值了


编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。


所以上述的 getName 方法经过 JVM 编译内联优化后会变成:


public String getName() {return "Bob";}


//打印出来也是 BobSystem.out.println(user.name)//经过内联优化 System.out.println("Bob")


反射是可以修改 final 变量的,但是如果是基本数据类型或者 String 类型的时候,无法通过对象获取修改后的值,因为 JVM 对其进行了内联优化。

反射可以修改 static 值吗?

Field.get(null) 可以获取静态变量。Field.set(null,object) 可以修改静态变量。

Java 异常

简析

java 中的异常分为 2 大类,Error 和 Exception。Error 中有 StackOverFlowError 和 OutOfMemoryError。Exception 分为 IOException 和 RuntimeException。


Java 中检查型异常和非检查型异常有什么区别?

检查型异常 extends Exception(编译时异常):需要使用 try catch 进行捕获,否则会出错,继承自 Exception


非检查型异常 extends RuntimeException(运行时异常):不需要捕获,在必要时才会报错,

try-catch-finally-return 执行顺序?

  1. 不管是否有异常产生,finally 块中代码都会执行

  2. 当 try 和 catch 中有 return 语句时,finally 块仍然会执行

  3. finally 是在 return 后面的表达式运算执行的,所以函数返回值在 finally 执行前确定的,无论 finally 中的代码怎么样,返回的值都不会改变,仍然是之前 return 语句中保存的值

  4. finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值

throw 和 throws 的区别

throw 用在方法内部,抛出异常


throws 用在方法外部,在方法中抛出异常

栈溢出 StackOverFlowError 发生的几种情况?

递归,栈内存存满,函数调用栈太深

Java 常见异常有哪些

java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。


java.lang.InstantiationError:实例化错误。当一个应用试图通过 Java 的 new 操作符构造一个抽象类或者接口时抛出该异常.


java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让 Java 虚拟机分配给一个对象时抛出该错误。


java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。


java.lang.ClassCastException:类造型异常。假设有类 A 和 B(A 不是 B 的父类或子类),O 是 A 的实例,那么当强制将 O 构造为类 B 的实例时抛出该异常。该异常经常被称为强制类型转换异常。


java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历 CLASSPAH 之后找不到对应名称的 class 文件时,抛出该异常。


java.lang.ArithmeticException:算术条


件异常。譬如:整数除零等。


java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。


java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于 0 或大于等于序列大小时,抛出该异常。


java.lang.InstantiationException:实例化异常。当试图通过 newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。


java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。


java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。


java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了 null 时,抛出该异常。譬如:调用 null 对象的实例方法、访问 null 对象的属性、计算 null 对象的长度、使用 throw 语句抛出 null 等等。


java.lang.NumberFormatException:数字格式异常。当试图将一个 String 转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。


java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于 0 或大于等于序列大小时,抛出该异常。

linux 进程通信有几种

Linux 中的进程间通信有哪些?解释 Binder 通信为什么高效?Binder 通信有什么限制?


Linux 中的进程间通信有如下几种:


  • 信号(signal)

  • 消息队列

  • 共享内存(Shared Memory)

  • 共享内存允许两个或多个进程进程共享同一块内存(这块内存会映射到各个进程自己独立的地址空间)从而使得这些进程可以相互通信。

  • 管道/命名管道(Pipe)

  • Pipe 这个词很形象地描述了通信双方的行为,即进程 A 与进程 B。一根管道同时具有读取端和写入端。比如进程 A 从 write end 写入,那么进程 B 就可以从 read end 读取数据。

  • Socket

  • 本地和服务端各自维护一个“文件”,在建立连接打开后,向自己的文件中写入数据,供对方读取


Binder 通信是 Android 系统特有的 IPC 机制,Binder 的优点有以下几个:


  1. 性能:Binder 的效率高,只需要一次内存拷贝;而 Linux 中的管道、消息队列、套接字都需要 2 次;共享内存的方式不需要拷贝数据,但是有多进程同步的问题。

  2. 稳定性:Binder 的架构是基于 C/S 结构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。

  3. 安全性:传统的 IPC 接收方无法获得对方可靠的进程用户 ID/进程 ID(UID/PID),从而无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。Android 系统中对外只暴露 Client 端,Client 端将任务发送给 Server 端,Server 端会根据权限控制策略,判断 UID/PID 是否满足访问权限。从安全角度,Binder 的安全性更高。


Binder 通信的另外一个限制是最多 16 个线程。最多只能传输 1M 的数据,否则会有 TransactionTooLarge 的 Exception。

CountDownLatch 原理

存在 4 个线程,想在 4 个线程都执行完毕后执行另一个线程,


countDownLatch 是采用计数器的原理,存在两个方法:


countDown:计数-1


await:线程挂起,当计数为 0 时,执行其后的逻辑

Java 泛型

泛型简述

java 中泛型即是“参数化类型”,即该泛型类型是一个参数传入


只在程序的源代码中存在,在编译后的字节码中已经替换为原生类型,这种方法称为伪泛型。


java 中的泛型只在编译时期有效,正确检验泛型的结果后,会将泛型相关的信息擦出,并在对象进入和离开的方法边界上添加类型检查类型转化的方法。


List<String> stringArrayList = new ArrayList<String>();List<Integer> integerArrayList = new ArrayList<Integer>();


Class classStringArrayList = stringArrayList.getClass();Class classIntegerArrayList = integerArrayList.getClass();


if(classStringArrayList==classIntegerArrayList){ //返回 trueSystem.out.println("类型相同");}


泛型有泛型类泛型方法泛型接口


泛型类:


//此处 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型//在实例化泛型类时,必须指定 T 的具体类型 public class Generic<T>{//key 这个成员变量的类型为 T,T 的类型由外部指定 private T key;


public Generic(T key) { //泛型构造方法形参 key 的类型也为 T,T 的类型由外部指定 this.key = key;}


public T getKey(){ //泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定 return key;}}


泛型接口:


//定义一个泛型接口 public interface Generator<T> {public T next();}/**


  • 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中

  • 即:class FruitGenerator<T> implements Generator<T>{

  • 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"/class FruitGenerator<T> implements Generator<T>{@Overridepublic T next() {return null;}}/*

  • 传入泛型实参时:

  • 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口 Generator<T>

  • 但是我们可以为 T 传入无数个实参,形成无数种类型的 Generator 接口。

  • 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型

  • 即:Generator<T>,public T next();中的的 T 都要替换成传入的 String 类型。*/public class FruitGenerator implements Generator<String> {


private String[] fruits = new String[]{"Apple", "Banana", "Pear"};


@Overridepublic String next() {Random rand = new Random();return fruits[rand.nextInt(3)];}}


泛型方法:


/**


  • 泛型方法的基本介绍

  • @param tClass 传入的泛型实参

  • @return T 返回值为 T 类型

  • 说明:


*/public <T> T genericMethod(Class<T> tClass){T instance = tClass.newInstance();return instance;}

泛型对方法重载的影响?

方法不能进行重载,会报错,两种方法都有相同的擦除,在编译期间进行泛型擦除的,会导致擦出后都一样


public class MyMethod {public void listMethod(List<String> list1){}public void listMethod(List<Integer> list2){}}

类加载

java 类的初始化流程

父类到子类,静态到(非静态,构造),变量----->代码块


父类静态变量----父类静态代码块----子类静态变量----子类静态代码块----父类非静态----父类构造----子类非静态----子类构造

jvm 类加载机制的 7 个流程

加载-----验证------准备------解析------初始化-------使用------卸载 JVM 将.java 文件加载成二进制文件.class


加载:


  1. 获取二进制流 class 文件

  2. 将静态存储结构转换为方法区中运行时的数据结构,存储到方法区中

  3. 在堆中生成一个 java 对象,作为方法区的引用


获取.class 文件并在堆中生成一个 class 对象,将加载的类结构信息存储在方法区




验证:JVM 规范校验,代码逻辑校验


准备:为类变量分配内存并设置类变量的初始化,如果变量被 final 修饰,会直接放入对应的常量池中,并赋值


解析:常量池符号引用替换为内存的直接引用


(上述三种统称为连接)




初始化:执行代码逻辑,对静态变量,静态代码块和类对象进行初始化


使用:使用初始化好的 class 对象


卸载:销毁创建 class 对象,负责运行的 jvm 退出内存

全局变量和局部变量的区别
  1. 全局变量应用于整个类文件。局部变量只在方法执行期间存在,之后被回收。静态局部变量对本函数体始终可见

  2. 全局变量,全局静态变量,局部静态变量都在静态存储空间。局部变量在栈(虚拟机栈)中分配空间

  3. 全局变量初始化需要赋值,局部变量不需要赋值

  4. 一个中不能声明同名全局变量,一个方法中不能声明同名局部变量。若全局变量和局部变量同名,则在方法中全局变量不生效。

大致流程

当 JVM 碰到 new 字节码的时候,会先判断类是否已经初始化,如果没有初始化(有可能类还没有加载,如果是隐式装载,此时应该还没有类加载,就会先进行装载、验证、准备、解析四个阶段),然后进行类初始化。 如果已经初始化过了,就直接开始类对象的实例化工作,这时候会调用类对象的方法。

类初始化的时机
  1. 初始化 main 方法的主类

  2. new 关键字触发,如果类还没有被初始化

  3. 访问静态方法和静态字段时,目标对象类没有被初始化,则进行初始化操作

  4. 子类初始化过程中,如果发现父类没有初始化,则先初始化父类

  5. 通过反射 API 调用时,如果类没有初始化,则进行初始化操作

  6. 第一次调用 java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。

类的实例化触发时机
  1. new 触发实例化,创建对象

  2. 反射,class.newnIstance()和 constructor.newnIstance()方法触发创建对象

  3. Clone 方法创建对象

  4. 使用序列化和反序列化的机制创建对象

类的初始化和类的实例化的区别

类的初始化:为静态成员赋值,执行静态代码块 类的实例化:执行非静态方法和构造方法


  1. 类的初始化只会执行一次,静态代码块只会执行一次

  2. 类的实例化会执行多次,每次实例化执行一次

在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?

正常情况下是先类初始化,再类实例化


在非正常情况下,比如在静态变量中


public class Run {public static void main(String[] args) {new Person2();}}


public class Person2 {public static int value1 = 100;public static final int value2 = 200;


public static Person2 p = new Person2();public int value4 = 400;


static{value1 = 101;System.out.println("1");}


{value1 = 102;System.out.println("2");}


public Person2(){value1 = 103;System.out.println("3");}}

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android面试指南(三),kotlin匿名内部类写法