Android面试八股合集

686 阅读18分钟

持续更新

Java

基础

  1. 访问修饰符

    image-20240718222734480.png

  2. 多态

    定义:允许不同类的对象对同一消息做出响应。即同一消息可以根据发 送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

    实现多态的技术称为:动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

    实现方式:接口实现、重写、重载

  3. 重写和重载的区别

    重写只能修改父类函数的访问修饰权限和函数体,并且子类函数的访问修饰权限不能少于父类的。

    而重载只要求函数参数列表不同(个数、类型或者顺序不同),访问权限或者返回类型不做要求。

    重写和重载都是多态的表现,重载是编译时的多态表现,重写是运行时多态的表现。

  4. 接口和抽象类的区别

    • 抽象类可以提供抽象成员函数的实现细节,接口只能提供public的抽象函数
    • 抽象类中的成员变量可以是各种类型与普通类相同,接口中的成员变量只能是public static final类型
    • 接口无构造器和静态代码块
    • 一个类可实现多个接口,但只能继承一个抽象类
    • 接口侧重于约束行为,抽象类侧重于代码的复用
  5. equals、hashcode、==

    equals比较两个对象内容是否相等,默认比较地址,可重写定义,==比较两个对象对象地址是否相等,基本数据类型比较值是否相等。

    hashcode是对象在哈希表中的位置,在哈希表中有用,其他地方没用,常用于集合,常配合equals使用,二者的关系:

    ①若两个对象相等(equals),那么这两个对象一定有相同的哈希值(hashCode);②若两个对象的哈希值相同,但这两个对象并不一定相等。

  6. java异常

    Java异常全部继承于Throwable,分为Error和Exception两类,Error属于系统的异常例如StackOverflowError,ClassNotFoundError,Exception属于程序上的错误,例如NullPointException、ArrayIndexOutOfBoundsException等,Exception是我们在程序中能try-catch到的,而Error是不能try-catch的。Exception又分为检查型异常和非检查型异常,检查型异常一般是除了RuntimeException以外的异常。

  7. 为什么局部类访问局部变量必须final修饰局部变量

    因为局部变量与局部内部类生命周期不一致的问题,当你访问不被修改的局部变量不会报错,而访问被修改的局部变量会强制给局部变量加final,代码如下: image-20240718234411256.png 修改:

    image-20240718234456755.png

    原因匿名内部类是被编译为单独的一个类的名字前缀是外部类,传入的局部变量会当做函数参数传入,因此匿名内部类改变局部变量值的外部无法感知,会导致值不同步的问题。

  8. java泛型

    泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。泛型可分为泛型类 、泛型接口、泛型方法。Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除 。泛型擦除是为了兼容jdk1.4往前的版本。编译器会在编译期间动态将泛型 T 擦除为 Object 或将 T extends xxx 擦除为其限定类型 xxx

    既然编译器要把泛型擦除,那为什么还要用泛型呢?用Object代替不行吗?

    • 可在编译期间进行类型检测。
    • 使用 Object 类型需要手动添加强制类型转换,降低代码可读性,提高出错概率
    • 泛型可以使用自限定类型。如 T extends Comparable 还能调用 compareTo(T o) 方法 ,Object则没有此功能

    桥方法(Bridge Method) 用于继承泛型类时保证多态。注意桥方法为编译器自动生成,非手写。

    class Node<T> {
    
        public T data;
    
        public Node(T data) {
        this.data = data; }
    
        public void setData(T data) {
    
            System.out.println("Node.setData");
            this.data = data;
        }
    }
    
    class MyNode extends Node<Integer> {
    
        public MyNode(Integer data) {
            super(data); 
        }
        //Node<T> 泛型擦除后为 setData(Object data),而子类 MyNode 中并没有重写该方法,所以编译器会加入该桥方法保证多态
        public void setData(Object data) {
    
            setData((Integer) data);
        }
    
        public void setData(Integer data) {
    
            System.out.println("MyNode.setData");
            super.setData(data);
        }
    }
    

    泛型的一些限制:

    • 只能声明不能实例化 T 类型变量
    • 泛型参数不能是基本类型。因为基本类型不是 Object 子类,应该用基本类型对应的引用类型代替
    • 泛型无法使用 Instance of 和 getClass() 进行类型判断
    • 不能抛出和捕获 T 类型的异常。可以声明
    • 不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突
    • 不能使用static修饰泛型变量

    通配符?和T的区别:

    T代表一种类型,而?泛指所有类型,?一般用于限定泛型参数类型的范围或者声明类的时候使用,T可用于类、接口方法上面。

    public class Utils {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
            list.add(1);
            //test 和 test1两个方法是等价的
            test(list);
            test1(list);
    
            List<? extends Number> list1 = null;
            list1 = new ArrayList<Integer>();
            //list1 = new ArrayList<Double>();
        }
        static<T extends Number> void test(List<T> list){
            System.out.println(list.get(0).intValue());
        }
        static void test1(List<? extends Number> list){
            System.out.println(list.get(0).intValue());
        }
    }
    
  9. 深拷贝、浅拷贝

    浅拷贝:在拷贝一个对象时,对对象的基本数据类型的成员变量进行拷贝,但对引用类型的成员变量只进行引用的传递,并没有创建一个新的对象,当对引用类型的内容修改会影响被拷贝的对象。 深拷贝:在拷贝一个对象时,除了对基本数据类型的成员变量进行拷贝,对引用类型的成员变量进行拷贝时,创建一个新的对象来保存引用类型的成员变量。

    java中默认是浅拷贝,可重写类的clone方法实现深拷贝或者序列化反序列化对象实现深拷贝。

  10. java反射

    Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。

  11. String、StringBuilder、StringBuffer

    String 在 Java 中是不可变的,每次变化一个值就会开辟一个新的内存空间。

    StringBuilder:线程非安全的,每次变化一个值不会开辟一个新的内存空间

    StringBuffer:线程安全的,每次变化一个值不会开辟一个新的内存空间

    三者在执行速度方面的比较:StringBuilder > StringBuffer > String

    对于三者使用的总结:

    1.如果要操作少量的数据用 String。

    2.单线程操作大量数据用 StringBuilder。

    3.多线程操作大量数据用 StringBuffer。

集合

Set、Map、List区别:

Set不允许重复是无序的,List允许重复是有序的,Map是键值对集合,键是不允许重复,值允许重复,是无序的。

LinkedHashSet、LinkedHashMap能保证有序性是其内部维护了一个双向链表。

List

ArrayList 底层原理是数组,默认容量是10,扩容是乘以1.5,适用于查询效率频繁的数据集合,LinkedList底层原理是双向链表,适用于增删频繁的数据集合。

CopyOnWriteArrayList是线程安全的ArrayList,CopyOnWriteArrayList对写(add,remove,set)的相关操作都是对原数组结构进行了复制一份,在复制的数据上面修改再将原数组结构同步到复制数组上面,加锁是为了安全性。复制是防止多线程中一个线程写一个线程for遍历发生ConcurrentModificationException错误。

Set:

HashSet内部使用的是HashMap,value是Object对象,使用containsKey判断元素是否重复。

Map

  1. HashMap

    数组+链表/红黑树,数组用于快速查询,链表/红黑树用于解决hash冲突。

    put过程

    首先会判断数组是否需要初始化,对key的hashCode()做hash运算,计算index,如果没碰撞直接放到bucket里,如果碰撞了,以链表的形式存在buckets后(1.8存在链表尾部,1.8以前存在链表头部),如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(JDK1.8中的改动),如果节点已经存在就替换old value(保证key的唯一性),如果bucket满了(超过load factorcurrent capacity),就要resize。resize过程:1.8以前再次hash计算index,1.8以后index等于扩容之前的index+扩容量

    get过程

    对key的hashCode()做hash运算,计算index;

    如果在bucket里的第一个节点和key匹配,则直接返回;

    如果有冲突,则通过key.equals(k)去查找对应的节点,当前节点是树节点,查找红黑树,否则,遍历链表查找

    为什么扩容是2倍,负载因子默认是0.75

    这是实践出来性能最好的数据。

  2. HashTable与HashMap

    Hashtable是线程安全,HashMap是非线程安全。

    HashMap可以使用null作为key,且以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key和value

    HashMap继承了AbstractMap,HashTable继承Dictionary抽象类,两者均实现Map接口

    HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。HashMap扩容时是当前容量翻倍,Hashtable扩容时是容量翻倍+1

    Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模 HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模

  3. ConcurrentHashMap

    HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个table数组,粒度比较大;

    ConcurrentHashMap 在jdk1.7中使用分段锁,在jdk1.8中使用CAS+synchronized。

    从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承于ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。

    实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。

    1.7 put/get流程

    根据hashcode计算hash,定位到segment,segment如果是空就先初始化

    使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功

    通过hash值找到HashEntry,遍历HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value,不存在就再插入链表。为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。

    get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。

    1.8 put/get流程

    根据hashcode计算hash,然后判断是否需要初始化,再利用hash值定位出Node,如果为空则CAS尝试写入,失败则自旋保证成功,如果当前位置的hashcode == MOVED == -1 则需要扩容,如果还没有put成功,就syncronized对当前node加锁,接下来put的操作就和hashmap 1.8一样了

  4. SparseArray,ArrayMap

    SparseArray,ArrayMap是Android中独有的集合类。

    SparseArray内部是通过两个数组来进行数据存储的,一个存储 key,另外一个存储 value,避免了对 key 的自动装箱

    对数据还采取了压缩的方式,从而节约内存空间。

    同时,SparseArray 在存储和读取数据时候,使用的是二分查找法。也就是说SparseArray 存储的元素都是按元素的 key 值从小到大排列好的

    ArrayMap 利用两个数组,第一个数组用来保存每一个 key 的 hash 值,第二个数组大小为 第一个数组大小 的 2 倍,依次保存 key 和 value。

    当插入时,根据 key 的 hashcode得到 hash 值,计算出在 mArrays 的 index位置,然后利用二分查找找到对应的位置进行插入,当出现哈希冲突时,会在index 的相邻位置插入。

    假设数据量都在千级以内的情况下:

    1、如果 key 的类型已经确定为 int 类型,那么使用 SparseArray,因为它避免了自动装箱的过程,如果 key 为 long 类型,它还提供了一个 LongSparseArray 来确保 key 为 long 类型时的使用

    2、如果 key 类型为其它的类型,则使用 ArrayMap。

多线程

  • 原子性:一个操作或多个操作要么全部执行且执行过程不被中断,要么不执行。
  • 可见性:多个线程修改同一个共享变量时,一个线程修改后,其他线程能马上获得修改后的值。
  • 有序性:程序执行的顺序按照代码的先后顺序执行。*9
  1. synchronized和ReetrantLock

    synchronized可给对象加锁,可给类加锁,synchronized配合Object的wait和notify函数阻塞唤醒线程。ReetrantLock只能用作代码块加锁,ReetrantLock配合newCondition获取的Condition的await和signal函数阻塞唤醒线程。他们都是可重入锁,但是synchronized是自动获取释放锁,而ReentrantLock需要显示获取释放锁,并且synchronized是不公平锁,ReentrantLock默认是非公平锁,利用AQS+CAS可以实现公平锁。而synchronized 底层原理是监视器。

  2. volatile

    volatile能保证可见性和有序性,但是不能保证原子性,synchronized能保证原子性,volatile通过jvm内存屏障保证变量的有序性,防止指令重排序。

    什么是指令重排序?

    编译器为了优化程序性能,在不影响程序结果的前提下将程序指令重排序。

    int a = 0;//1
    int b = 1;//2
    int c = a + b;//3
    

    1,2代码不存在互相依赖,3代码依赖1和2,这时候发生指令重排序,2就有可能发生在1前面。

    在单线程下,这并不会产生影响,但在多线程下就有可能产生影响,例如:

    class ReorderExample {
        int a = 0;
        boolean flag = false;
        public void writer() {
            a = 1;                   //1
            flag = true;             //2
        }
        public void reader() {
            if (flag) {                //3
                int i =  a * a;        //4
                ……
            }
        }
    }
    

    线程A执行writer,线程B执行reader,如果A发生重排序,2就会在1前面,此时B就会进入3的if条件语句,但此时a还没有赋值,a的值仍然为0,此时i计算出的结果就不是预期。

    volatile不能保证原子性的表现

    初始i=0;Thread A执行i++操作,Thread B执行i++操作,i++分三个操作读取i,i+1,写入i+1。 因为不能保证原子性,A 执行到i+1时还没有写入到i变量的内存,此时B读取i的值为0,因此线程A和B都执行完一次 i++之后i的值可能还是1。使用synchronized可保证i++的三个操作要么都发生要么都不发生。

  3. 线程池

    创建线程可以通过new Thread、new Runnable创建常用于IO等,通过Callable和Future可以获取线程执行返回的结果。通过FeatureTask或者ThreadPoolExcutor.submit可构建Feature对象,常用于分段计算。

    各种线程池的区别:

    1. newCacheThreadPool:为每一个任务创建一个线程,并且也可以重用已有的线程,无核心线程数量,超过60s的空闲线程将弃用,但是会受到系统实际内存的线程。
    2. newFixedThreadPool:核心线程数和最大线程数是相同的(都是核心线程),并且无空闲线程,核心线程数无限时要求,就是可以创建固定大小的线程数量,同时阻塞队列是没有大小限制的
    3. newScheduledThreadPool:具有延迟和周期执行线程的线程池,固定的核心线程数,最大线程数没有限制,空闲的线程会立即启用。
    4. newSingleThreadExecutor:创建只有一个的核心线程,只处理一个线程,其实并不能处理并发

    线程池参数区别

    corePoolSize:核心线程数,线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。 线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务。

    maximumPoolSize:最大线程数。线程池允许创建的最大线程数量。 当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程并执行。

    keepAliveTime:空闲线程存活时间 当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。

    可被回收的线程:

    设置allowCoreThreadTimeout=true的核心线程。 大于核心线程数的线程(非核心线程)。

    unit:时间单位,keepAliveTime的时间单位

    workQueue:工作队列,存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现了BlockingQueue接口。

    JDK默认的工作队列有五种:

    • ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
    • LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。
    • SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
    • PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
    • DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。

    threadFactory:线程工厂,创建线程的工厂,可以设定线程名、线程编号等

    handler:拒绝策略 当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。

    JDK默认的拒绝策略有四种:

    • AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    • DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
    • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
    • CallerRunsPolicy:由调用线程处理该任务。

JVM

  1. JMM内存模型

    JMM(Java 内存模型)详解 | JavaGuide

  2. JAVA内存结构

    Java内存区域详解(重点) | JavaGuide

  3. 垃圾回收

    JVM垃圾回收详解(重点) | JavaGuide

  4. 类加载

    类加载器详解(重点) | JavaGuide

设计模式

重学设计模式 - 掘金 (juejin.cn)

Android

Handler

Handler原理解析

app启动流程(activity启动流程)

view绘制流程

屏幕刷新机制

recyclerview

三种动画

Activity

启动模式

生命周期

Fragment

Service

ContentProvider

Broadcast

MVC/MVVM/MVP/MVI

事件分发

进程间通信(aidl binder socket)

三方框架

retrofit、jetpcak、okhttp、rxjava、Glide、Arouter、LeakCanary等原理分析。

计算机网络

计算机网络常见面试题(上)

计算机网络常见面试题(下)

操作系统

操作系统常见面试题(上)

操作系统常见面试题(下)

代码手撕

双重检验单例模式

生产消费模式

双亲委托模型

两个线程交替打印

责任链模式

快排/归并

循环队列实现

算法

算法基础:数据结构与算法基础笔记 - 掘金 (juejin.cn)

leetcode hot100

常见面试算法:

01背包、完全背包

大数相乘/相加

快速幂

rand5实现ran7

版本号

还原ip