Android 杂谈——Interview Prepare

https://rainmonth.github.io/posts/A220222.html

Android Daily Interview备用地址

Java方面

Java基础部分

抽象类与接口的区别别

  • 抽象类,采用 修饰符 + abstract class来定义,和普通类一样,可以定义常量、方法、抽象方法、一般方法和构造方法,是类共性的提取
  • 接口, 采用 修饰符 + interface来定义,可以定义常量(默认就是public static的),可以声明方法(java 8以后方法可以有默认实现,不过需要加上default关键字),是一种规范,实现接口的类必须提供接口中定义方法的实现。
  • Java是单继承,多实现的

分别讲讲final,static, synchronized关键字可以修饰什么,以及修饰后的作用

final
  • final修饰基本数据类型,表示数据不能改变;
  • final修饰引用型数据(通常指对象),表示引用型数据的引用地址不变(但对象里面的属性还是可以改变的)
  • final修饰方法,表示该方法不能被重载,不允许子类修改该方法的实现(private修饰的方法隐含为final方法);
  • final修饰类,表示该类不能被继承,该类的所有方法都是final的
    采用final来修饰方法,1是为了安全,这样子类不能改变该方法的实现;2是为了效率(可以将方法转为内嵌调用而不是通过动态绑定来调用)
static
  • static可以修饰方法、变量、代码块、内部类。
    • 静态方法,可以先于类实例的创建而直接通过类名访问,静态方法只能调用静态方法,访问静态变量;
    • 静态变量,用static修饰后,变量在其所属的类加载时就被赋值,该类的所有的对象共有这一个值;
    • 静态代码块,在其声明的类被加载时执行;
      方法声明为static后,有如下限制:只能访问类其他静态方法,只能访问类的静态数据,方法内部不能使用this和super关键字。

static方法可以被继承,但是不能被覆盖,知识被隐藏了(因为static变量是静态绑定的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Father {
public static void print() {
System.out.println("Father");
}
}

public class Son extends Father {
public static void print() {
System.out.println("Son");
}

public static void main(String[] args) {
Son son = new Son();
son.print();// 输出Son

Father son = new Son();
son.print();// 输出Father
}
}
synchronized

synchronized 是java语言关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。修饰代码块的时候,传递的可以是对象、也可以是类

简述下String、StringBuffer和StringBuilder的区别

  • String 是不可变的常量字符串,类采用final修饰,说明不能被继承,用来承载char字符的数据采用final修饰,说明在构造的创建的时候就已经确定了,任何String操作都是在新的String对象上产生的;
  • StringBuffer和StringBuilder都继承自AbstractStringBuilder,都可以表示可变的字符串(表现就是用来承载char字符的数组没有用final修饰),但StringBuffer是线程安全的,而StringBuilder不是线程全的(表现就是StringBuffer相关的操作都加synchronized关键字(加锁了)

“equals”与”==”、”hashCode()”的区别

  • 如果没有重写,equals方法和==是相同的(equals内部调用的时==);
  • 关于hashCode,是一个散列值,Java有一个如下约定,如果两个对象相等,那么要求他的hashCode相等,但hashCode相等,equals方法却可以不等。
  • hashCode表示的对象的散列值,通常在散列存储接口中更具hashCode来定位到存储位置,然后在配合equals方法类具体定位到对象的具体位置;

Java中深拷贝与浅拷贝的区别

  • 如果对象中都是基本类型,深拷贝与浅拷贝是一样的;
  • 如果对象中存在应用的成员变量,此时深拷贝和浅拷贝就有所不同:
    • 浅拷贝只是拷贝该引用类型成员变量的地址,拷贝后的对象与原来的对象共用改引用成员;
    • 深拷贝则会在拷贝引用类型时会新建一个对象,并将原引用对象的值赋给新创建的对象。

深拷贝可以通过让对象实现Serializable接口然后序列化和反序列化实现,也可以通过Clonable接口,然后对对象的引用类型新建一个相同的对象然后为其赋值即可。

Error与Exception的区别

首先,二者都是Throwable的子类。Exception是异常,Java中可以预料的异常情况,可以进行捕获,并在捕获后做额外的处理(比如IO流读写是的IOException);Error是错误,是Java中不可预料的,发生后可能导致虚拟机不可处理或不可恢复,Java不建议捕获处理,比如Android中常见的OutOfMemoryError。关于Error这里说一下OutOfMerroryError:

  1. oom异常一般是java程序在做内存分配时,发现没有足够的剩余空间可用而抛出的异常;
    1. 此时的分配空间可能是出于代码的new操作(用户主动),可能是出于内存的复制操作(语言自动),也可能是出于内存数据的重振操作(语言自动),可能是出jvm检测到外部信号(jvm自动);
    2. oom只是被建议为不要捕获的异常,因为通常你对这种情况是无能为力的!但你如果实在要捕获,why not ?
    3. oom一般只会影响当前线程,而jvm中只要存在一个非daemon线程在运行,jvm就不会退出;
    4. 如果是线程池运行环境,一般需要一个统一管理oom的程序,否则不能及时统一处理oom;

什么是反射机制,反射机制的应用场景

在运行时状态中,对于任意一个类,都能知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性,这种动态获取类信息及动态调用对象方法的能力称为Java的反射机制。反射机制通常和Java注解一起,用来动态的生产类代码,动态调用类方法,常见的应用有:

  1. 逆向代码,如反编译;
  2. 与注解结合的框架,如Retrofit;
  3. EventBus、Gson等;

谈谈如何重写equals()方法,以及为什么要重写hashCode()方法

重写equals方法:先比价地址,然后比较类型,在比较需要比较的字段值。
直接重写Object类的equals()方法即可,没有重写则equals()相当于”==”,具体重写可以参照IDE的模板生成(可以过滤掉不使用的字段)。之所以重写equals还要重写hashCode(),是因为在散列存储数据结构中(HashMap、Hashtable、HashSet等),他们先通过hashCode来定位元素插入未知的。如果两个对象equals相等,但没有重写hashCode方法,就会导致两个对象都被存进了集合

Java中IO流分为几种?BIO、NIO、AIO有什么区别

参考说明:https://blog.csdn.net/m0_38109046/article/details/89449305
Java中的IO流主要包括三种BIO(传统的Block)NIO

谈谈对Java中泛型擦除的理解,并说说其局限性

泛型擦除讲的就是定义的泛型类型变量在被编译器编译成字节码后,最终都会被替换成实实在在的数据类型的过程。

String为什么要设计成不可变的

首先明确,String类时final修饰的,且起真正承载字符的数据也是final的,这就保证String对象在创建完成后就不可变。那么为什么这样设计呢?

  1. 字符串常量池,其存在就是为了性能优化,发现要创建的字符串常量已经存在,就无需再次分配内存了,而是直接引用已经存在于常量池中的字符串对象,提高了效率;

    1
    2
    s1 = "abc";
    s2 = "abc";// 此时只是把对重字符串常量池中的abc对应的地址赋值给s2,并没有再额外开劈内存空间
1
2
2. hashCode,String对象重写了hashCode方法,并且缓存了hashCode,String不可变就可以报纸String对象创建后hashCode就不可变且不需要重新计算,使得String和适合作为HashMap的key
3. 线程安全

对Java注解的理解

  • Java注解

    Java成员变量、局部变量和静态变量的创建和回收时机

  • 成员变量,对象被创建时创建,当对象不在被引用时,在下一次GC被回收;

  • 局突变量,方法执行时创建,方法只想完成后被回收;

  • 静态变量,在类被加载时创建,当类不在使用时被回收;

Java中String.length()的运作原理

Java集合

谈谈List、Set、Map的区别

  • List 逻辑上有序,可以有重复的元素,可以有多个null元素;

  • Set 无序,不能有重复元素,至多能有一个null元素;

  • Map 键值对集合,无序,最多允许一个null键,可以多个空值;

    谈谈ArrayList和LinkedList的区别

  • ArrayList,基于数组,读的效率高;

  • LinkedList,基于链表,插入效率高;

    HashMap和HashTable的区别

    HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。下面多HashMap和Hashtable做一些说明:

  • 继承的父类不同:HashMap和Hashtable不仅作者不同,而且连父类也是不一样的。HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口

  • 迭代器区别:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。

  • 内部实现与操作上的区别

    • HashTable
      1. 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
      2. 初始size为11,扩容:newsize = olesize*2+1
      3. 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
    • HashMap
      1. 底层数组+链表实现,可以存储null键和null值,线程不安全
      2. 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
      3. 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
      4. 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
      5. 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
      6. 计算index方法:index = hash & (tab.length – 1)

HashMap加载因子额外考虑

  1. 一些常用概念
    谈到Hash类的集合,一般都会有如下几个概念:

    • 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
    • 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
    • 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
  2. Hash表的属性

    HashMap和HashTable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:

    • 容量(capacity):hash表中桶的数量
    • 初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量
    • 尺寸(size):当前hash表中记录的数量
    • 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)
  3. 负载因子说明

    hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。
    HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。

“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:

  • 较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查> 询是最频繁的操作(HashMap的get()与put()方法都要用到查询)
  • 较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销程序猿可以根> 据实际情况来调整“负载极限”值。

ArrayList的扩容机制

ArrayList默认的容量是10:

1
private static final int DEFAULT_CAPACITY = 10;

扩容的代码在grow(int minCapacity)方法中:

1
2
3
4
5
6
7
8
9
10
11
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 计算新的容量值:为老的的1.5倍
if (newCapacity - minCapacity < 0)// 新的值不符合预期(不能满足minCapacity,就扩容为minCapacity的容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) // 新的值太大,通过hugeCapacity来进行判断
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

方法的参数minCapacity,表示的时期望分配的最小容量

1
2
3
4
5
6
7
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;// 返回最大值
}

HashMap的实现原理

参考文章:Java集合HashMap

请简述LinkedHashMap的工作原理和使用方式

LinkedHashMap 继承自HashMap,并实现了Map接口,也就是说HashMap有的一些方法,LinkedHashMap都有,通过维护一个双向链表来保证Map数据的有序输出。

谈谈对于ConcurrentHashMap的理解

分段锁,线程安全,效率高

Java多线程

谈谈happens-before

在Java内存模型中,happens-before 应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。

  • 程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
  • 管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
  • volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  • 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
  • 线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
  • 传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
  • 对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

3.1 Java 中使用多线程的方式有哪些?

  • 使用Thread或继承Thread类;
  • 实现Runnable的类(Thread本身也是实现Runnable接口的);
  • 实现Callable接口的类
  • 采用线程池

3.2 说一下线程的几种状态?

  • new,线程创建了,但尚未调用其start方法;

  • runnable,运行状态(包含就绪状态和运行状态,start,线程创建了,调用start方法了,但线程调度程序尚未执行这个线程,这个状态持续的时间会很短)

  • blocked,阻塞状态,等待其他线程释放锁时进入该状态(即等待monitor lock进入同步块或同步方法或者调用wait后等待重新进入同步块或同步方法

  • waiting,等待状态,调用wait方法进入等待状态,需要等待notify

  • timed_waiting,限时等待(调用sleep、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil()

  • terminated,终止状态

  • Thread.yield() : 当前线程,不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

  • Thread.sleep(millis): 当前线程,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式

  • Thread.join()。当前线程调用其他线程t的join方法,当前线程进入WAITTING/TIMED_WAITTING 状态。不释放已持有的对象锁。等待线程t执行完成或者时间结束。可能会进入 Blocked状态。基于wait实现

  • Object.wait():当前线程调用对象的wait方法,当前线程进入等待队列。释放当前对象锁。

  • Object.notify(): 唤醒等待队列中的任意一个线程。jdk 1.8 唤醒的是头节点(等待时间最长的那个)

  • Object.notifyAll(): 唤醒等待队列中的全部线程。

3.3 如何实现多线程中的同步?

线程间的同步实质是保证线程中共享变量的数据同步

  • volatile关键字,在get和set的场景下是可以的,由于get和set的时候都加了读写内存屏障,在数据可见性上保证数据同步。但是对于++这种非原子性操作,数据会出现不同步
  • synchronized对代码块或方法加锁,结合wait,notify调度保证数据同步
  • reentrantLock加锁结合Condition条件设置,在线程调度上保障数据同步
  • CountDownLatch简化版的条件锁,在线程调度上保障数据同步
  • cas=compare and swap(set), 在保证操作原子性上,确保数据同步
  • 参照UI线程更新UI的思路,使用handler把多线程的数据更新都集中在一个线程上,避免多线程出现脏读

3.4 谈谈线程死锁,如何有效的避免线程死锁?

定义:

所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都将无法向前推进。

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。

死锁示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/** 
* 一个简单的死锁类
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
public class DeadLock implements Runnable {
public int flag = 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}

public static void main(String[] args) {

DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();

}
}

避免线程死锁,只要破会以上四个条件中的一个就可以了

  • 保证枷锁顺序,相当于给获取下一个锁增加限制条件,只有获取到前一个锁才有可能获取到下一个
  • 加锁实现,当线程获取一个锁时,给它一个获取锁的时限,超过这个时间还没获取到,就采取一定措施了,不在等待了,不去请求该所,并释放自己已经获取到的锁;
  • 死锁检测,利用map、图等数据结构将线程和其获取到的锁进行记录,然后在线程请求锁失败时遍历存储的数据结构(通常是图)来看看是否有死锁发生,如果有,则让线程释放自己的锁,并放弃其他锁的请求,在一定时间后进行重试;

3.5 谈谈线程阻塞的原因?

主要分三种:

  • 等待阻塞;调用Object.wait,是线程放弃了cpu的使用权,并且释放对象锁,线程就被加入等待队列了,等地啊Object.notify()(或者Object.notifyAll())来唤醒
  • 同步阻塞;synchronized关键字同步锁、或者其他形式的锁造成的线程进入等待池(锁的等待池)
  • 其他阻塞(I/O、网络阻塞)
    • I/O 操作
    • 网络连接;
    • socket连接
  1. 竞争锁需要等待,此时就阻塞了;
  2. 线程自己调用sleep方法、
  3. 线程调用对象的wait方法阻塞;
  4. 线程中发起了I/O操作(System.in.read())或者进行了远程通信,如发送同步的网络请求,在I/O输入之前或网络请求结果返回之前,是阻塞的
  5. 请求与服务器建立连接时,即当线程执行Socket带参构造函数,或者执行Socket的connect方法,会进入阻塞,直到连接成功才从socket的构造方法或connect方法返回;
  6. 线程从Socket的输入流读取数据时,如果没有足够的数据,就会进入阻塞状态,直到读到了足够的数据,或者到达输入流的末尾,或者出现了异常,才从输入流的read()方法返回或异常中断。输入流中有多少数据才算足够呢?这要看线程执行的read()方法的类型。
    • int read(); 只要输入流中有一个字节,就算足够。
    • int read(byte[] buff); 只要输入流中的字节数目与参数buff数组的长度相同,就算足够。
    • String readLine(); 只要输入流中有一行字符串,就算足够。值得注意的是,InputStream类并没有readLine方法,在过滤流BufferedReader类中才有此方法。
  7. 线程向Socket的输出流写一批数据时,可能会进入阻塞状态,等到输出了所有的数据,或者出现异常,才从输出流的write()方法返回或异常中断。
  8. 调用Socket的setSoLinger()方法设置了关闭Socket的延迟时间,那么当线程执行Socket的close方法时,会进入阻塞状态,直到底层Socket发送完所有剩余数据,或者超过了setSoLinger()方法设置的延迟时间,才从close()方法返回。

3.6 请谈谈 Thread 中 run() 与 start() 方法的区别?

run()方法其实是Runnable接口定意思的方法,其实就是一个普通的接口调用;
start()方法时线程真正开始的地方,调用start方法后,线程在获取到CPU时间片后,就开始真正的运行了;

start()方法时同步的,有synchronized关键字修饰,内部调用start0这个native方法,主要用来通知JVM创建线程,设置线程组用的。

如果没有调用start,而直接调用run,此时run方法就是一次简单的对象方法调用。

3.7 说一下 synchronized 和 volatile 关键字的区别?

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

3.8 如何保证线程安全?

首先要理解什么是线程安全,在多个线程访问共同资源时,在某一个线程对资源进行写操作中途(写入已经开始,还没结束),其他线程对这个写了一般资源进行了读操作,或者基于这个写了一半操作进行写操作,导致数据问题。(多个线程的情况下发生还没写完就读,还没读完就写,导致程序运行的结果不如预期)

要保证线程安全
前提: 需要遵循happens-before原则,即前一个操作的结果能被后面一个操作获取,例如一个线程将一个变量a赋值为1了,另一个线程知道这个结果。

  1. 采用同步机制synchronized关键字、或者CountDownLatch、ReentrantLock、CyclicBarrier等;
  2. 采用volatile关键字(注意他只能保证可见性,不能保证原子性,能用来保存线程安全的场景有限,通常是单写多度)
  3. 采用Atomic包(本质上是乐观锁)
  4. 采用ThreadLocal进行线程隔离

保证线程安全需要保证:

  1. 原子性:一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
  2. 可见性,一个线程对值做出更改,其他线程需要能看到这个值以及被改变
  3. 有序性,程序的执行要和代码的顺序一致。

3.9 谈谈 ThreadLocal 用法和原理?

用法示例:两个线程,共享一个变量 ThreadLocal a,线程t1将a赋值为0到5并顺序打印;线程t2将a赋值为6到10并顺序打印。
气质这个a中,存储了多个指定变量的副本,放在了ThreadLocalMap的value中,见下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap的key是ThreadLocal对象,它是根据线程获取到的。

ThreadLocal用完要及时remove,否则也是会造成内存泄漏的,虽然ThreadLocal中的ThreadLocalMap的Value是个弱引用,但是它的key,不是,如果一个外层资源运行完毕,触发了GC,但此时他还被作为key的Thread所引用,就会导致外层资源不能被回收。

3.10 谈谈 Java 线程中 notify 和 notifyAll 方法有什么区别?

都是对象Object类的方法,前者通知等待该对象锁队列中的一个(JDK1.8是最久的一个即第一个),后者是通知等待该对象锁队列的所有

3.11 什么是线程池?如何创建一个线程池?

在会打之前,先要想想使用线程的方式:

1
2
3
4
5
6
7
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// do somthing
}
});
t.start();

线程池,就是一组线程的集合。可以用来进行线程复用。

线程池可以通过 Executors方法创建 一些android为我们提供的线程池。比如 Executors.newFixedThreadPool(int)

但最好是还要舒心线程池创建的参数
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
   允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool:
  允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

让我们用这种方法创建自己的线程池

1
2
3
4
5
6
7
new ThreadPoolExecutor(
int corePoolSize, //核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, //保活时间
TimeUnit unit, //时间单位
BlockingQueue workQueue, //任务队列
RejectedExecutionHandler handler) //最后一个参数, 当前线程数超过。 任务队列数+最大线程数时的具体操作。比如抛出异常或者丢掉线程任务等等。

再有就是其他具体细节问题了。比如shutdown(),和shutdownNow()区别啦。看名字应该都懂,一个等待 队列里 没完事的继续执行再关,另一个直接关取消任务。

再有就是ScheduledThreadPool这种用于执行周期性任务的。scheduleAtFixedRate方法和scheduleWithFixedDelay方法区别啦?

任务耗时 小于 线程周期时间 scheduleAtFixedRate(勤快) 在周期时间到达时,开始新一轮任务。 scheduleWithFixedDelay(懒惰)在任务结束后,才开始计算周期时间,周期时间结束后,再开始新的任务。

任务耗时 大于 线程周期时间 scheduleAtFixedRate(勤快) 任务结束后,马上开始新一轮任务。但是由于 任务耗时 > 线程周期时间%% 实际上这个周期时间已经被耗时时间影响到了。
scheduleWithFixedDelay (懒惰)任务结束后,开始计算周期时间,然后开始执行新一轮任务。

ps:就好比耗时的任务是每天上班。 线程周期24h(每天11点来git答题)。
scheduleAtFixedRate(勤快,下班了,马上答题。提前下班。11点答题。加班超过11点,下班了立刻答题)
scheduleWithFixedDelay(懒惰,必须休息。即下班了,必须休息固定的时间,在去答题。无论早下班还是晚下班。)

3.12 谈一谈 Java 中几种常见锁以及它们的使用场景?

  1. synchronized
  2. ReentrantLock
  3. Semaphore
  4. AtomicInteger(内部利用CAS)

悲观锁与乐观锁
悲观锁:不管怎样都枷锁,即使其他线程根本没有对共享数据进行修改,实现通常synchronized关键字、ReentrantLock等
乐观锁:认为其他线程没有修改数据,不加锁,但是在更新共享数据的时候,回去判断有没有人修改这个数据。实现方式有版本号控制、CAS算法

公平锁与偏向锁

3.13 谈一谈线程 sleep() 和 wait() 方法的区别?

sleep是Thread的方法,调用sleep后,线程进入time_wating状态,但不释放锁,等待时间结束后再获取到cpu时间片后又会重新运行
而wait是对象的方法,调用线程内调用对象的wait方法,当前线程进入阻塞状态,需要等待

3.14 什么是悲观锁和乐观锁?

3.15 什么是 BlockingQueue?请分析一下其内部原理并谈谈它的使用场景?

3.16 谈一谈 Java 线程安全的集合有哪些?各有什么特点?

Hashtable,数组+链表结构
Vector实现List接口,现行接口
ConcurrentHashMap HashMap的线程安全版本,较Hashtable在多线程的情况下性能更高

3.17 Java 中为什么会出现 Atomic 类?试分析它的原理和缺点?

Atomic类采用的自旋锁

3.18 说说 ThreadLocal 的使用场景?与 Synchronized 相比有什么特性?

ThreadLocal是通过线程隔离来保证多线程下使用安全,而Synchronized是通过线程同步来保证线程安全。

Java虚拟机

4.1 请简要谈一谈 Java 中的垃圾回收机制?

JVM维护的一套内存回收机制,将那些不在被引用的对象占用内存回收在利用的机制;内存一般都是分代回收的。所以Java中内存分为以下三代:

  • 新生代
  • 老年代,新生代尽力多次Minor GC后就进入了老年代,一些大的对象也会直接在老年代分配;
  • 永久代,
    回收算法
  • 标记清除算法,先找到要清除的对象,然后进行清除,效率低,容易产生内存碎片
  • 复制算法,将内存划分两个区域,将还在使用的对象复制到新的区域,然后直接回收原来区域;
  • 标记整理算法,标记起来,然后将要清楚和不要清楚的飞凯,然后将要清除的清除(有点放舱医院的感觉)

4.2 回答一下什么是强、软、弱、虚引用以及它们之间的区别?

强:最普遍的引用,如通过new创建的对象,只要引用连没断开,就不会被回收,而且如果申请时内存不足,会直接OOM
软:内存不足时,下一次GC会被回收;
弱:GC时被回收;
虚:不会决定对象的生命周期,任何时候都可能被回收,通常和ReferenceQueue配合使用,用来判断对象是否要被回收了。

4.3 简述 JVM 中类的加载机制与加载过程?

JVM将class文件加载到内存,并对数据进行校验、转换解析和初始化,最终生成可以被虚拟机识别的Class 对象的过程,这既是类加载机制

过程:加载->链接(校验文件、准备内存、解析文件)-> 初始化-> 使用

4.4 JVM、Dalvik、ART 三者的原理和区别?

4.5 请谈谈 Java 的内存回收机制?

4.6 什么是 JMM?它存在哪些问题?该如何解决?

Java内存模型:定义了共享内存系统中多线程程序读写操作行为的规范,Java内存模型也就是为了解决这个并发编程问题而存在的
怎么解决:内存模型解决并发问题主要采取两种方式,分别是限制处理器优化,另一种是使用了内存屏障。
而对于这两种方式,Java底层其实已经封装好了一些关键字,我们这边只需要用起来就可以了。
关于解决并发编程中的原子性问题,Java底层封装了Synchronized的方式,来保证方法和代码块内的操作都是原子性的;
而至于可见性问题,Java底层则封装了Volatile的方式,将被修饰的变量在修改后立即同步到主内存中。
至于有序性问题,其实也就是我们所说的重排序问题,Volatile关键字也会禁止指令的重排序,而Synchroinzed关键字由于保证了同一时刻只允许一条线程操作,自然也就保证了有序性。

Android方面

Android 四大组件相关

Activity 与 Fragment 之间常见的几种通信方式?

  1. 对象建的简单调用;

  2. 通过接口来进行;

  3. 通过广播;

  4. 通过EventBus;

    谈谈 Android 中几种 LaunchMode 的特点和应用场景?

    四种
    标准
    singleTop,栈顶复用,在栈顶的话直接onNewIntent,不在栈顶,则新建一个
    singleTask,同一个应用一个栈只能有一个,如果不在栈顶,会将位于其上的finish,让其至于栈顶;不是同一个易用,则会新建一个栈;
    singleIntance 一个栈,一个实例并且只能有一个;

BroadcastReceiver 与 LocalBroadcastReceiver 有什么区别?

前者是全局广播,8.0之前可以静态注册页可以动态注册,8.0后只能动态注册了;后者只能动态注册,是应用内广播

对于 Context,你了解多少?

IntentFilter 是什么?有哪些使用场景?匹配机制是怎样的?

谈一谈 startService 和 bindService 方法的区别,生命周期以及使用场景?

startService启动
使用:

  1. 定义Service;
  2. AndroidManifest.xml中配置Service
  3. 调用Context.startService 启动(可以通过Intent来传递参数)
  4. 不在使用时调用stopService来停止该服务;
    生命周期(生命周期和Activity不同,Activity结束了Service不一定结束)
    onCreate->onStartCommand->onDestroy

bindService启动

  1. 定义Service
  2. AndroidManifest.xml中配置Service
  3. 调用Context.bindService 启动(可以通过Intent来传递参数)
  4. 不再使用时调用unbindServie解除绑定

生命周期(生命周期和Activity相同,Activity结束Service就结束了)
onCreate->onBind->onUnbind->onDestroy

Activity和Service的通信方式

  1. Intent
  2. 通过广播、通过EventBus;
  3. bind方式启动时,可以通过实现ServiceConnection完成二者交互

Service 如何进行保活?

Service 保活方案

  1. 不再白名单的进入后台运行的Service,Android 8.0以上,将在在1分钟后被秒;
  2. Android 8.0后不允许后台运行的Service在调用startService了

所以需要通过提高Service优先级来提交Service的存活了
前台进程->可见进程->服务进程->后台进程->空进程

所以常见多发应该是通过startForground配合Notification的形式来保活,比如音乐播放器

  1. 提高Server进程所在优先级;
  2. 类似双进程守护;
  3. 引导用户将其加入到系统或厂商加入白名单

简单介绍下 ContentProvider 是如何实现数据共享的?

说下切换横竖屏时 Activity 的生命周期变化?

Activity 中 onNewIntent 方法的调用时机和使用场景?

Intent 传输数据的大小有限制吗?如何解决?

define BINDER_VM_SIZE ((110241024) - (4096 *2)),通过共享内存的形式来进行传值

说说 ContentProvider、ContentResolver、ContentObserver 之间的关系?

说说 Activity 加载的流程?

Android 异步任务和消息机制

HandlerThread 的使用场景和实现原理?

IntentService 的应用场景和内部实现原理?

AsyncTask 的优点和缺点?内部实现原理是怎样的?

内部是Handler+线程池
几个回调的调用线程

谈谈你对 Activity.runOnUiThread 的理解?

如果当前在UI线程,则Runnable立即运行;如果非UI线程,则提交到UI线程的消息队列,等待执行的时机;

Android 的子线程能否做到更新 UI?

谈谈 Android 中消息机制和原理?

为什么在子线程中创建 Handler 会抛异常?

试从源码角度分析 Handler 的 post 和 sendMessage 方法的区别和应用场景?

最终调用的都是sendMessageAtTime

Handler 中有 Loop 死循环,为什么没有阻塞主线程,原理是什么?

主线程的死循环一直运行是不是特别消耗CPU资源呢? 其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce() 方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。 Gityuan–Handler(Native层)

Android UI 绘制相关

此类主要涵盖 Android 的 View 绘制过程、常见 UI 组件、自定义 View、动画等。

Android 补间动画和属性动画的区别?

Window 和 DecorView 是什么?DecorView 又是如何和 Window 建立联系的?

简述一下 Android 中 UI 的刷新机制?

你认为 LinearLayout、FrameLayout 和 RelativeLayout 哪个效率高, 为什么?

说一下 Android 中的事件分发机制?

谈谈自定义 View 的流程?

有针对 RecyclerView 做过哪些优化?

谈谈你是如何优化 ListView 的?

谈一谈自定义 RecyclerView.LayoutManager 的流程?

什么是 RemoteViews?使用场景有哪些?

谈一谈获取View宽高的几种方法?

View.post() 为什么可以获取到宽高信息?

谈一谈属性动画的插值器和估值器?

getDimension、getDimensionPixelOffset 和 getDimensionPixelSize 三者的区别?

请谈谈源码中 StaticLayout 的用法和应用场景?

有用过ConstraintLayout吗?它有哪些特点?

关于LayoutInflater,它是如何通过 inflate 方法获取到具体View的?

谈一谈如何实现 Fragment 懒加载?

谈谈 RecyclerView的缓存机制?

请说说 View.inflate 和 LayoutInflater.inflate 的区别?

View.inflate

请谈谈 invalidate() 和 postInvalidate() 方法的区别和应用场景?

一个在UI线程中调用,另一个可以在非UI线程中调用

谈一谈自定义View和ViewGroup的流程以及区别?

要做的事情不同

谈一谈 SurfaceView 与 TextureView 的使用场景和用法?

谈一谈 RecyclerView.Adapter 的几种数据刷新方式有何不同?

说说你对 Window 和 WindowManager 的理解?

谈一谈 Activity、View 和 Window 三者的关系?

有了解过WindowInsets吗?它有哪些应用场景?

Android 中 View 的几种位移方式的区别?

为什么 ViewPager 嵌套 ViewPager,内部的 ViewPager 滚动没有被拦截?

请谈谈 Fragment 的生命周期?

请谈谈什么是同步屏障?

有了解过 ViewDragHelper 的工作原理吗?

谈一谈Android的屏幕刷新机制?

Android 性能调优相关

谈谈你对Android性能优化方面的了解?

一般什么情况下会导致内存泄漏问题?如何解决

自定义 Handler 时如何有效地避免内存泄漏问题?

哪些情况下会导致OOM问题?如何解决?

ANR 出现的场景以及解决方案?

谈谈 Android 中内存优化的方式?

谈谈布局优化的技巧?

对于 Android 中图片资源的优化方案你知道哪些?

Android Native Crash 问题如何分析定位?

该如何给 Apk 瘦身?

说一下你是如何优化 App 启动过程的?

谈谈代码混淆的步骤?

说说 App 的电量优化?

谈谈如何对 WebView 进行优化?

如何处理大图的加载?

谈谈如何对网络请求进行优化?

请谈谈如何加载Bitmap并防止内存溢出?

Android 中的 IPC

请回答一下 Android 中进程间通信有哪些方式?

请谈谈你对 Binder 机制的理解?

根本上来说是一个CS结构。

什么是 AIDL?它的使用场景是什么?

Android 系统 SDK 相关

请简要谈谈 Android 系统的架构组成?

SharedPreferences 是线程安全的吗?它的 commit 和 apply 方法有什么区别?

Serializable 和 Parcelable 有哪些区别?

请说一下 Android 7.0 的新特性?

谈谈 ArrayMap 和 HashMap 的区别?

简要说说 LruCache 的原理?

Android 中为什么推荐用 SparseArray 代替 HashMap?

PathClassLoader 和 DexClassLoader 有何区别?

说说 HttpClient 与 HttpUrlConnection 的区别?为何前者会被替代?

什么是Lifecycle?请分析其内部原理和使用场景?

谈一谈 Android 的签名机制?不同版本下的签名有什么不同?

谈谈安卓 Apk 构建的流程?

简述一下 Android 8.0、9.0 分别增加了哪些新特性?

谈谈 Android 10 更新了哪些内容?如何进行适配?

请简述 Apk 的安装过程?

Java 与 JS 代码如何互调?有做过相关优化吗?

什么是 JNI?具体说说如何实现 Java 与 C++ 的互调?

请谈谈 App 的启动流程?

第三方框架分析

谈一谈 LeakCanray 的工作原理?

说说 EventBus 的实现原理?

谈谈网络请求中的拦截器 - Interceptor 的实现原理和使用场景?

谈一谈 Glide 中的缓存机制?

ViewModel 的出现是为了解决什么问题?并简要说说它的内部原理?

请说说依赖注入框架 ButterKnife 的实现原理?

谈一谈 RxJava 背压原理?

综合技术

请谈谈你对 MVC 和 MVP 的理解?

分别介绍下你所知道的 Android 中几种存储方式?

简述下热修复的原理?

谈谈你是如何适配更多机型的?

请谈谈你是如何进行多渠道打包的?

MVP 中你是如何处理 Presenter 层以防止内存泄漏的?

如何计算一张图片所占的内存空间大小?

有没有遇到 64k 问题,应该如何解决?

如何优化 Gradle 的构建速度?

如何获取 Android 设备唯一 ID?

谈一谈 Android P 禁用 HTTP 协议对我们开发有什么影响?

什么是 AOP?在 Android 中它有哪些应用场景?

什么是 MVVM?你是如何将其应用于具体项目中的?

请谈谈你会如何实现数据埋点?

假如让你实现断点上传功能,你认为应该怎样去做?

webp 和 svg 格式的图片各自有什么特点?应该如何在 Android 中使用?

说说你是如何进行单元测试的?以及如何应用在 MVP 和 MVVM 中?

如何绕过 Android 9.0 针对反射的限制?

对于 GIF 格式的图片加载有什么思路和建议?

为什么要将项目迁移到 AndroidX?如何进行迁移?

你了解过哪些Android屏幕适配方面的技巧?

数据结构方面

什么是冒泡排序?如何去优化?

请用 Java 实现一个简单的单链表?

如何反转一个单链表?

谈谈你对时间复杂度和空间复杂度的理解?

谈一谈如何判断一个链表有环?

手写二叉树结构?

什么是红黑树?为什么要用红黑树?

什么是快速排序?如何优化?

说说循环队列?它有哪些应用场景?

如何判断单链表交叉?

设计模式

请简要谈一谈单例模式?

对于面向对象的六大基本原则了解多少?

请列出几种常见的工厂模式并说明它们的用法?

说说项目中用到的设计模式和使用场景?

什么是代理模式?如何使用?Android源码中的代理模式?

谈一谈单例模式,建造者模式,工厂模式的使用场景?如何合理选择?

谈谈你对原型模式的理解?

请谈谈策略模式原理及其应用场景?

静态代理和动态代理的区别,什么场景使用?

谈一谈责任链模式的使用场景?

计算机网络方面

请简述 Http 与 Https 的区别?

说一说 HTTPS、UDP、Socket 之间的区别?

请简述一次 HTTP 网络请求的过程?

谈一谈 TCP/IP 三次握手、四次挥手过程?

为什么说Http是可靠的数据传输协议?

TCP/IP 协议分为哪几层?TCP 和 HTTP 分别属于哪一层?

Post 中请求参数放在了哪个位置?

Kotlin 方面

请简述一下什么是 Kotlin?它有哪些特性?

Kotlin 中注解 @JvmOverloads 的作用?

Kotlin 中 List 与 MutableList 的区别?

Kotlin 中实现单例的几种常见方式?

谈谈你对 Kotlin 中的 data 关键字的理解?相比于普通类有哪些特点?

什么是委托属性?请简要说说其使用场景和原理?

请举例说明 Kotlin 中 with 与 apply 函数的应用场景和区别?

Kotlin中 Unit 类型的作用以及与Java中 Void 的区别?

Kotlin 中 infix 关键字的原理和使用场景?

Kotlin中的可见性修饰符有哪些?相比于 Java 有什么区别?

你觉得 Kotlin 与 Java 混合开发时需要注意哪些问题?

在 Kotlin 中,何为解构?该如何使用?

在 Kotlin 中,什么是内联函数?有什么作用?

谈谈Kotlin中的构造方法?有哪些注意事项?

谈谈 Kotlin 中的 Sequence,为什么它处理集合操作更加高效?

请谈谈 Kotlin 中的 Coroutines,它与线程有什么区别?有哪些优点?

Kotlin中可见型修饰符有哪些?相比于Java有什么区别?

谈谈Kotlin中的Unit?它和Java中的void有什么区别?z’x’l

Kotlin中该如何安全地处理可空类型?

说说 Kotlin中 的 Any 与Java中的 Object 有何异同?

Kotlin中的数据类型有隐式转换吗?为什么?

分别通过对象表达式 object 和 lambda 表达式实现的函数式接口内部有何不同?

Kotlin 中集合遍历有哪几种方式?

为什么协程比线程要轻量?

开放性问题

你知道哪些提升开发效率的骚操作?

在开发过程中你遇到过的最大的难题是什么?如何解决的?

说说你未来的职业规划是怎样的?

你是如何看待 Flutter,React Native 与 Android 关系的?