2019年为android开发准备的面试题(含答案)

Posted 潇潇凤儿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2019年为android开发准备的面试题(含答案)相关的知识,希望对你有一定的参考价值。

2018年算是互联网的寒冬,一大波公司宣扬裁员,也确实裁掉一部分,有的拿到了高额的补偿,可以慢慢拿着工资、喝着小酒、找着工作,甚至找个一年半载也不是问题。虽然自己公司还没扬言裁员,且自己还茫目自信,再怎么裁也不会裁到我这等优秀员工身上啊。但寒冬归寒冬,饭还是要吃、酒还是要喝,做技术的不管需不需要面试,也要时不时抽空更新下自己,让自己涨涨知识,一些基本知识点重新拾起,也为自己不久的将来面试做准备吧。

2019,加油〜〜

下面的题目全是从别人那copy过来的,答案全部是自己总结的,希望自己能温故而知新〜〜

(一) java基础面试知识点

1、java中==和equals和hashCode的区别

1)==若是基本数据类型比较,是比较值,若是引用类型,则比较的是他们在内存中的存放地址。对象是存放在堆中,栈中存放的对象的引用,所以==是对栈中的值进行比较,若返回true代表变量的内存地址相等;

2)equals是Object类中的方法,Object类的equals方法用于判断对象的内存地址引用是不是同一个地址(是不是同一个对象)。若是类中覆盖了equals方法,就要根据具体代码来确定,一般覆盖后都是通过对象的内容是否相等来判断对象是否相等。

3)hashCode()计算出对象实例的哈希码,在对象进行散列时作为key存入。之所以有hashCode方法,因为在批量的对象比较中,hashCode比较要比equals快。在添加新元素时,先调用这个元素的hashCode方法,一下子能定位到它应该旋转的物理位置,若该位置没有元素,可直接存储;若该位置有元素,就调用它的equals方法与新元素进行比较,若相同则不存,不相同,就放到该位置的链表末端。

4)equals与hashCode方法关系:

hashCode()是一个本地方法,实现是根据本地机器上关的。equals()相等的对象,hashCode()也一定相等;hashCode()不等,equals()一定也不等;hashCode()相等,equals()可能相等,也可能不等。

所以在重写equals(Object obj)方法,有必要重写hashCode()方法,确保通过equals(Object obj)方法判断结果为true的两个对象具备相等的hashCode()返回值。

5)equals与==的关系:

Integer b1 = 127;在java编译时被编译成Integer b1 = Integer.valueOf(127);对于-128到127之间的Integer值,用的是原生数据类型int,会在内存里供重用,也就是这之间的Integer值进行==比较时,只是进行int原生数据类型的数值进行比较。而超出-128〜127的范围,进行==比较时是进行地址及数值比较。

 

2、int、char、long各占多少字节数

int\\float占用4个字节,short\\char占用2个字节,long\\double占用8个字节,byte占用1个字节 boolean占一位

基本数据类型存放在栈里,包装类栈里存放的是对象的引用,即值的地址,而值存放在堆里。

3、int与integer的区别

Integer是int的包装类,int则是java的一种基本数据类型,Integer变量必须实例化才能使用,当new一个Integer时,实际是生成一个指向此对象的引用,而int是直接存储数据的值,Integer默认值是null,而int默认值是0

4、谈谈对java多态的理解

同一个消息可以根据发送对象的不同而采用多种不同的行为方式,在执行期间判断所引用的对象的实际类型,根据其实际的类型调用其相应的方法。

作用:消除类型之间的耦合关系。实现多态的必要条件:继承、重写(因为必须调用父类中存在的方法)、父类引用指向子类对象

5、String、StringBuffer、StringBuilder区别

都是字符串类,String类中使用字符数组保存字符串,因有final修饰符,String对象是不可变的,每次对String操作都会生成新的String对象,这样效率低,且浪费内存空间。但线程安全。

StringBuilder和StringBuffer也是使用字符数组保存字符,但这两种对象都是可变的,即对字符串进行append操作,不会产生新的对象。它们的区别是:StringBuffer对方法加了同步锁,是线程安全的,StringBuilder非线程安全。

6、什么是内部类?内部类的作用

内部类指在类的内部再定义另一个类。

内部类的作用:1)实现多重继承,因为java中类的继承只能单继承,使用内部类可达到多重继承;2)内部类可以很好的实现隐藏,一般非内部类,不允许有private或protected权限的,但内部类可以;3)减少了类文件编译后产生的字节码文件大小;

内部类在编译完后也会产生.class文件,但文件名称是:外部类名称$内部类名称.class。分为以下几种:

1)成员内部类,作为外部类的一个成员存在,与外部类的属性、方法并列,成员内部类持有外部类的引用,成员内部类不能定义static变量和方法。应用场合:每一个外部类都需要一个内部类实例,内部类离不开外部类存在。

2)静态内部类,内部类以static声明,其他类可通过外部类.内部类来访问。特点:不会持有外部类的引用,可以访问外部类的静态变量,若要访问成员变量须通过外部类的实例访问。应用场合:内部类不需要外部类的实例,仅为外部类提供或逻辑上属于外部类,逻辑上可单独存在。设计的意义:加强了类的封装性(静态内部类是外部类的子行为或子属性,两者保持着一定关系),提高了代码的可读性(相关联的代码放在一起)。

3)匿名内部类,在整个操作中只使用一次,没有名字,使用new创建,没有具体位置。

4)局部内部类,在方法内或是代码块中定义类,

7、抽象类和接口区别

抽象类在类前面须用abstract关键字修饰,一般至少包含一个抽象方法,抽象方法指只有声明,用关键字abstract修饰,没有具体的实现的方法。因抽象类中含有无具体实现的方法,固不能用抽象类创建对象。当然如果只是用abstract修饰类而无具体实现,也是抽象类。抽象类也可以有成员变量和普通的成员方法。抽象方法必须为public或protected(若为private,不能被子类继承,子类无法实现该方法)。若一个类继承一个抽象类,则必须实现父类中所有的抽象方法,若子类没有实现父类的抽象方法,则也应该定义为抽象类。

接口用关键字interface修饰,接口也可以含有变量和方法,接口中的变量会被隐式指定为public static final变量。方法会被隐式的指定为public abstract,接口中的所有方法均不能有具体的实现,即接口中的方法都必须为抽象方法。若一个非抽象类实现某个接口,必须实现该接口中所有的方法。

区别:1)抽象类可以提供成员方法实现的细节,而接口只能存在抽象方法;

2)抽象类的成员变量可以是各种类型,而接口中成员变量只能是public static final类型;

3)接口中不能含有静态方法及静态代码块,而抽象类可以有静态方法和静态代码块;

4)一个类只能继承一个抽象类,用extends来继承,却可以实现多个接口,用implements来实现接口。

7.1、抽象类的意义

抽象类是用来提供子类的通用性,用来创建继承层级里子类的模板,减少代码编写,有利于代码规范化。

7.2、抽象类与接口的应用场景

抽象类的应用场景:1)规范了一组公共的方法,与状态无关,可以共享的,无需子类分别实现;而另一些方法却需要各个子类根据自己特定状态来实现特定功能;

2)定义一组接口,但不强迫每个实现类都必须实现所有的方法,可用抽象类定义一组方法体可以是空方法体,由子类选择自己感兴趣的方法来覆盖;

7.3、抽象类是否可以没有方法和属性?

可以

7.4、接口的意义

1)有利于代码的规范,对于大型项目,对一些接口进行定义,可以给开发人员一个清晰的指示,防止开发人员随意命名和代码混乱,影响开发效率。

2)有利于代码维护和扩展,当前类不能满足要求时,不需要重新设计类,只需要重新写了个类实现对应的方法。

3)解耦作用,全局变量的定义,当发生需求变化时,只需改变接口中的值即可。

4)直接看接口,就可以清楚知道具体实现类间的关系,代码交给别人看,别人也能立马明白。

8、泛型中extends和super的区别

<? extends T>限定参数类型的上界,参数类型必须是T或T的子类型,但对于List<? extends T>,不能通过add()来加入元素,因为不知道<? extends T>是T的哪一种子类;

<? super T>限定参数类型的下界,参数类型必须是T或T的父类型,不能能过get()获取元素,因为不知道哪个超类;

9、父类的静态方法能否被子类重写?静态属性和静态方法是否可以被继承?

父类的静态方法和属性不能被子类重写,但子类可以继承父类静态方法和属性,如父类和子类都有同名同参同返回值的静态方法show(),声明的实例Father father = new Son(); (Son extends Father),会调用father对象的静态方法。静态是指在编译时就会分配内存且一直存在,跟对象实例无关。

10、进程和线程的区别

进程:具有一定独立功能的程序,是系统进行资源分配和调度运行的基本单位。每一个android应用可以理解为一个进程,但一个应用也可以设置多个进程。

线程:进程的一个实体,是CPU调度的苯单位,也是进程中执行运算的最小单位,即执行处理机调度的基本单位,如果把进程理解为逻辑上操作系统所完成的任务,线程则表示完成该任务的许多可能的子任务之一。Android中分主线程和子线程,UI更新只能在主线程中进行。

关系:一个进程可有多个线程,至少一个;一个线程只能属于一个进程。同一进程的所有线程共享该进程的所有资源。不同进程的线程间要利用消息通信方式实现同步。

区别:进程有独立的地址空间,而多个线程共享内存;进程具有一个独立功能的程序,线程不能独立运行,必须依存于应用程序中;

11、final,finally,finalize的区别

final:变量、类、方法的修饰符,被final修饰的类不能被继承,变量或方法被final修饰则不能被修改和重写。

finally:异常处理时提供finally块来执行清除操作,不管有没有异常抛出,此处代码都会被执行。如果try语句块中包含return语句,finally语句块是在return之后运行;

finalize:Object类中定义的方法,若子类覆盖了finalize()方法,在在垃圾收集器将对象从内存中清除前,会执行该方法,确定对象是否会被回收。

12、序列化Serializable 和Parcelable 的区别

序列化:将一个对象转换成可存储或可传输的状态,序列化后的对象可以在网络上传输,也可以存储到本地,或实现跨进程传输;

为什么要进行序列化:开发过程中,我们需要将对象的引用传给其他activity或fragment使用时,需要将这些对象放到一个Intent或Bundle中,再进行传递,而Intent或Bundle只能识别基本数据类型和被序列化的类型。

Serializable:表示将一个对象转换成可存储或可传输的状态。

Parcelable:与Serializable实现的效果相同,也是将一个对象转换成可传输的状态,但它的实现原理是将一个完整的对象进行分解,分解后的每一部分都是Intent所支持的数据类型,这样实现传递对象的功能。

Parcelable实现序列化的重要方法:序列化功能是由writeToParcel完成,通过Parcel中的write方法来完成;反序列化由CREATOR完成,内部标明了如何创建序列化对象及数级,通过Parcel的read方法完成;内容描述功能由describeContents方法完成,一般直接返回0。

区别:Serializable在序列化时会产生大量临时变量,引起频繁GC。Serializable本质上使用了反射,序列化过程慢。Parcelable不能将数据存储在磁盘上,在外界变化时,它不能很好的保证数据的持续性。

选择原则:若仅在内存中使用,如activity\\service间传递对象,优先使用Parcelable,它性能高。若是持久化操作,优先使用Serializable

注意:静态成员变量属于类,不属于对象,固不会参与序列化的过程;用transient关键字编辑的成员变量不会参与序列化过程;可以通过重写writeObject()和readObject()方法来重写系统默认的序列化和反序列化。

13、谈谈对kotlin的理解

特点:1)代码量少且代码末尾没有分号;2)空类型安全(编译期处理了各种null情况,避免执行时异常);3)函数式的,可使用lambda表达式;4)可扩展方法(可扩展任意类的的属性);5)互操作性强,可以在一个项目中使用kotlin和java两种语言混合开发;

14、string 转换成 integer的方式及原理

1)parseInt(String s)内部调用parseInt(s, 10)默认为10进制 。2)正常判断null\\进制范围,length等。3)判断第一个字符是否是符号位。4)循环遍历确定每个字符的十进制值。5)通过*=和-=进行计算拼接。6)判断是否为负值返回结果。

(二) java深入源码级的面试题(有难度)

1、哪些情况下的对象会被垃圾回收机制处理掉?

利用可达性分析算法,虚拟机会将一些对象定义为GC Roots,从GC Roots出发沿着引用链向下寻找,如果某个对象不能通过GC Roots寻找到,虚拟机就认为该对象可以被回收掉。

1.1 哪些对象可以被看做是GC Roots呢?

1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

2)方法区中的类静态属性引用的对象,常量引用的对象;

3)本地方法栈中JNI(Native方法)引用的对象;

1.2 对象不可达,一定会被垃圾收集器回收么?

即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行finalize()方法,对象必须重写finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个队列中,JVM会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。

2、讲一下常见编码方式?

编码的意义:计算机中存储的最小单元是一个字节即8bit,所能表示的字符范围是255个,而人类要表示的符号太多,无法用一个字节来完全表示,固需要将符号编码,将各种语言翻译成计算机能懂的语言。

1)ASCII码:总共128个,用一个字节的低7位表示,0〜31控制字符如换回车删除等;32~126是打印字符,可通过键盘输入并显示出来;

2)ISO-8859-1,用来扩展ASCII编码,256个字符,涵盖了大多数西欧语言字符。

3)GB2312:双字节编码,总编码范围是A1-A7,A1-A9是符号区,包含682个字符,B0-B7是汉字区,包含6763个汉字;

4)GBK为了扩展GB2312,加入了更多的汉字,编码范围是8140~FEFE,有23940个码位,能表示21003个汉字。

5)UTF-16: ISO试图想创建一个全新的超语言字典,世界上所有语言都可通过这本字典Unicode来相互翻译,而UTF-16定义了Unicode字符在计算机中存取方法,用两个字节来表示Unicode转化格式。不论什么字符都可用两字节表示,即16bit,固叫UTF-16。

6)UTF-8:UTF-16统一采用两字节表示一个字符,但有些字符只用一个字节就可表示,浪费存储空间,而UTF-8采用一种变长技术,每个编码区域有不同的字码长度。  不同类型的字符可以由1~6个字节组成。                                                                                                                                                                                                                       

3、utf-8编码中的中文占几个字节;int型几个字节?

utf-8是一种变长编码技术,utf-8编码中的中文占用的字节不确定,可能2个、3个、4个,int型占4个字节。

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

代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类的方法。

区别:1)静态代理:由程序员创建或是由特定工具生成,在代码编译时就确定了被代理的类是哪一个是静态代理。静态代理通常只代理一个类;

2)动态代理:在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类;

实现步骤:a.实现InvocationHandler接口创建自己的调用处理器;b.给Proxy类提供ClassLoader和代理接口类型数组创建动态代理类;c.利用反射机制得到动态代理类的构造函数;d.利用动态代理类的构造函数创建动态代理类对象;

使用场景:Retrofit中直接调用接口的方法;Spring的AOP机制;

5、Java的异常体系

Java中Throwable是所有异常和错误的超类,两个直接子类是Error(错误)和Exception(异常):

1)Error是程序无法处理的错误,由JVM产生和抛出,如OOM、ThreadDeath等。这些异常发生时,JVM一般会选择终止程序。

2)Exception是程序本身可以处理的异常,又分为运行时异常(RuntimeException)(也叫Checked Eception)和非运行时异常(不检查异常Unchecked Exception)。运行时异常有NullPointerException\\IndexOutOfBoundsException等,这些异常一般是由程序逻辑错误引起的,应尽可能避免。非运行时异常有IOException\\SQLException\\FileNotFoundException以及由用户自定义的Exception异常等。

6、谈谈你对解析与分派的认识。

解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。

分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。

7、修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?

会调用对象的equals方法,如果对象的equals方法没有被重写,equals方法和==都是比较栈内局部变量表中指向堆内存地址值是否相等。

8、Java中实现多态的机制是什么?

多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。Java实现多态有三个必要条件:继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。

9、如何将一个Java对象序列化到文件里?

ObjectOutputStream.writeObject()负责将指定的流写入,ObjectInputStream.readObject()从指定流读取序列化数据。

//写入
try 
    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:/student.txt"));
    os.writeObject(studentList);
    os.close();
 catch(FileNotFoundException e) 
    e.printStackTrace();
 catch(IOException e) 
    e.printStackTrace();

10、说说你对Java反射的理解

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

反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只对系统应用开放,这里就可以利用java的反射机制通过反射来获取所需的私有成员或是方法。

1) 获取类的Class对象实例 Class clz = Class.forName("com.zhenai.api.Apple");

2) 根据Class对象实例获取Constructor对象  Constructor appConstructor = clz.getConstructor();

3) 使用Constructor对象的newInstance方法获取反射类对象 Object appleObj = appConstructor.newInstance();

4) 获取方法的Method对象  Method setPriceMethod = clz.getMethod("setPrice", int.class);

5) 利用invoke方法调用方法  setPriceMethod.invoke(appleObj, 14);

6) 通过getFields()可以获取Class类的属性,但无法获取私有属性,而getDeclaredFields()可以获取到包括私有属性在内的所有属性。带有Declared修饰的方法可以反射到私有的方法,没有Declared修饰的只能用来反射公有的方法,其他如Annotation\\Field\\Constructor也是如此。

11、说说你对Java注解的理解

注解是通过@interface关键字来进行定义的,形式和接口差不多,只是前面多了一个@

public @interface TestAnnotation

使用时@TestAnnotation来引用,要使注解能正常工作,还需要使用元注解,它是可以注解到注解上的注解。元标签有@Retention @Documented @Target @Inherited @Repeatable五种

@Retention说明注解的存活时间,取值有RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到JVM中。RetentionPolicy.RUNTIME可以留到程序运行的时候,它会被加载进入到JVM中,所以在程序运行时可以获取到它们。

@Documented 注解中的元素包含到javadoc中去

@Target  限定注解的应用场景,ElementType.FIELD给属性进行注解;ElementType.LOCAL_VARIABLE可以给局部变量进行注解;ElementType.METHOD可以给方法进行注解;ElementType.PACKAGE可以给一个包进行注解 ElementType.TYPE可以给一个类型进行注解,如类、接口、枚举

@Inherited 若一个超类被@Inherited注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;

注解的作用:

1)提供信息给编译器:编译器可利用注解来探测错误和警告信息

2)编译阶段:软件工具可以利用注解信息来生成代码、html文档或做其它相应处理;

3)运行阶段:程序运行时可利用注解提取代码

注解是通过反射获取的,可以通过Class对象的isAnnotationPresent()方法判断它是否应用了某个注解,再通过getAnnotation()方法获取Annotation对象

12、说一下泛型原理,并举例说明

泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。Java泛型是在Java1.5以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在一定程度无视类型参数T,直接从T所在的类开始向上T的父类去擦除,如调用泛型方法,传入类型参数T进入方法内部,若没在声明时做类似public T methodName(T extends Father t),Java就进行了向上类型的擦除,直接把参数t当做Object类来处理,而不是传进去的T。即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如new T(),或者T.play()(play为某子类的方法而不是擦除后的类的方法)

13、Java中String的了解

1)String类是final型,固String类不能被继承,它的成员方法也都默认为final方法。String对象一旦创建就固定不变了,对String对象的任何改变都不影响到原对象,相关的任何改变操作都会生成新的String对象。

2)String类是通过char数组来保存字符串的,String对equals方法进行了重定,比较的是值相等。

String a = "test"; String b = "test"; String c = new String("test");

a、b和字面上的test都是指向JVM字符串常量池中的"test"对象,他们指向同一个对象。而new关键字一定会产生一个对象test,该对象存储在堆中。所以new String("test")产生了两个对象,保存在栈中的c和保存在堆中的test。而在java中根本就不存在两个完全一模一样的字符串对象,故在堆中的test应该是引用字符串常量池中的test。

例:

String str1 = "abc"; //栈中开辟一块空间存放引用str1,str1指向池中String常量"abc"
String str2 = "def"; //栈中开辟一块空间存放引用str2,str2指向池中String常量"def"
String str3 = str1 + str2;//栈中开辟一块空间存放引用str3
//str1+str2通过StringBuilder的最后一步toString()方法返回一个新的String对象"abcdef"
//会在堆中开辟一块空间存放此对象,引用str3指向堆中的(str1+str2)所返回的新String对象。
System.out.println(str3 == "abcdef");//返回false
因为str3指向堆中的"abcdef"对象,而"abcdef"是字符池中的对象,所以结果为false。JVM对String str="abc"对象放在常量池是在编译时做的,而String str3=str1+str2是在运行时才知道的,new对象也是在运行时才做的。

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

1)字符串常量池需要String不可变。因为String设计成不可变,当创建一个String对象时,若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。

2)String对象可以缓存hashCode。字符串的不可变性保证了hash码的唯一性,因此可以缓存String的hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较hashCode,提高了比较性能;

3)安全性。String被许多java类用来当作参数,如url地址,文件path路径,反射机制所需的Strign参数等,若String可变,将会引起各种安全隐患。

(三) 数据结构

1、常用数据结构简介

数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素间的关系组成。常用的数据有:数组、栈、队列、链表、树、图、堆、散列表。

1)数组:在内存中连续存储多个元素的结构。数组元素通过下标访问,下标从0开始。优点:访问速度快;缺点:数组大小固定后无法扩容,只能存储一种类型的数据,添加删除操作慢。适用场景:适用于需频繁查找,对存储空间要求不高,很少添加删除。

2)栈:一种特殊的线性表,只可以在栈顶操作,先进后出,从栈顶放入元素叫入栈,从栈顶取出元素叫出栈。应用场景:用于实现递归功能,如斐波那契数列。

3)队列:一种线性表,在列表一端添加元素,另一端取出,先进先出。使用场景:多线程阻塞队列管理中。

4)链表:物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域,一个是指向下一个结点地址的指针域。有单链表、双向链表、循环链表。优点:可以任意加减元素,不需要初始化容量,添加删除元素只需改变前后两个元素结点的指针域即可。缺点:因为含有大量指针域,固占用空间大,查找耗时。适用场景:数据量小,需频繁增加删除操作。

5)树:由n个有限节点组成一种具有层次关系的集合。二叉树(每个结点最多有两个子树,结点的度最大为2,左子树和右子树有顺序)、红黑树(HashMap底层源码)、B+树(mysql的数据库索引结构)

6)散列表(哈希表):根据键值对来存储访问。

7)堆:堆中某个节点的值总是不大于或不小于其父节点的值,堆总是一棵完全二叉树。

8)图:由结点的有穷集合V和边的集合E组成。

2、并发集合了解哪些?

1)并发List,包括Vector和CopyOnWriteArrayList是两个线程安全的List,Vector读写操作都用了同步,CopyOnWriteArrayList在写的时候会复制一个副本,对副本写,写完用副本替换原值,读时不需要同步。

2)并发Set,CopyOnWriteArraySet基于CopyOnWriteArrayList来实现的,不允许存在重复的对象。

3)并发Map,ConcurrentHashMap,内部实现了锁分离,get操作是无锁的。

4)并发Queue,ConcurrentLinkedQueue适用于高并发场景下的队列,通过无锁方式实现。 BlockingQueue阻塞队列,应用场景,生产者-消费者模式,若生产快于消费,生产队列装满时会阻塞,等待消费。

5)并发Deque, LinkedBlockingDueue没有进行读写锁分离,同一时间只能有一个线程对其操作。

6)并发锁重入锁ReentrantLock,互斥锁,一次最多只能一个线程拿到锁。

7)读写锁ReadWriteLock,有读取和写入锁两种,读取允许多个读取线程同时持有,而写入只能有一个线程持有。

3、列举java的集合以及集合之间的继承关系

 

5、容器类介绍以及之间的区别

1)Collection接口:集合框架的根接口,它是集合类框架中最具一般性的顶层接口。

2)Map接口:提供了键值对的映射关系的集合,关键字不能有重复值,每个关键字至多可映射一个值。HashMap(通过散列机制,用于快速访问),TreeMap(保持key处于排序状态,访问速度不如hashmap), LinkedHashMap(保持元素的插入顺序)

3)Set接口:可包含重复的元素,LinkedHashSet TreeSet(用红黑树来存储元素) HashSet

4)List接口:可通过索引对元素进行精准的插入和查找,实现类有ArrayList LinkedList

5)Queue接口:继承自Collection接口,LinkedList实现了Queue接口,提供了支持队列的行为。

6)Iterator接口:为了迭代集合

7)Comparable接口:用于比较

6、List,Set,Map的区别

Set是一个无序的集合,不能包含重复的元素;

list是一个有序的集合可以包含重复的元素,提供了按索引访问的方式;

map包含了key-value对,map中key必须唯一,value可以重复。

7、HashMap的实现原理

1)数据结构

jdk1.7及以前,HashMap由数组+链表组成,数组Entry是HashMap的主体,Entry是HashMap中的一个静态内部类,每一个Entry包含一个key-value键值对,链表是为解决哈希冲突而存在。

从jdk1.8起,HashMap是由数组+链表/红黑树组成,当某个bucket位置的链表长度达到阀值8时,这个链表就转变成红黑树。

2)HashMap是线程不安全的,存储比较快,能接受null值,HashMap通过put(key, value)来储存元素,通过get(key)来得到value值,通过hash算法来计算hashcode值,用hashcode标识Entry在bucket中存储的位置。

3)HashMap中为什么要使用加载因子,为什么要进行扩容

加载因子是指当HashMap中存储的元素/最大空间值的阀值,如果超过这个值,就会进行扩容。加载因子是为了让空间得到充分利用,如果加载因子太大,虽对空间利用更充分,但查找效率会降低;如果加载因子太小,表中的数据过于稀疏,很多空间还没用就开始扩容,就会对空间造成浪费。

至于为什么要扩容,如果不扩容,HashMap中数组处的链表会越来越长,这样查找效率就会大大降低。

7.1 HashMap如何put数据(从HashMap源码角度讲解)?

当我们使用put(key, value)存储对象到HashMap中时,具体实现步骤如下:

1)先判断table数组是否为空,为空以默认大小构建table,table默认空间大小为16

2)计算key的hash值,并计算hash&(n-1)值得到在数组中的位置index,如果该位置没值即table[index]为空,则直接将该键值对存放在table[index]处。

3)如果table[index]处不为空,说明发生了hash冲突,判断table[index]处结点是否是TreeNode(红黑树结点)类型数据,如果是则执行putTreeVal方法,按红黑树规则将键值对存入;

4)如果table[index]是链表形式,遍历该链表上的数据,将该键值对放在table[index]处,并将其指向原index处的链表。判断链表上的结点数是否大于链表最大结点限制(默认为8),如果超过了需执行treeifyBin()操作,则要将该链表转换成红黑树结构。

5)判断HashMap中数据个数是否超过了(最大容量*装载因子),如果超过了,还需要对其进行扩容操作。

7.2 HashMap如何get数据?

get(key)方法获取key的hash值,计算hash&(n-1)得到在链表数组中的位置first=table[hash&(n-1)],先判断first(即数组中的那个)的key是否与参数key相等,不等的话,判断结点是否是TreeNode类型,是则调用getTreeNode(hash, key)从二叉树中查找结点,不是TreeNode类型说明还是链表型,就遍历链表找到相同的key值返回对应的value值即可。

7.3 当两个对象的hashcode相同,即发生碰撞时,HashMap如何处理

当两个对象的hashcode相同,它们的bucket位置相同,hashMap会用链表或是红黑树来存储对象。Entry类里有一个next属性,作用是指向下一个Entry。第一个键值对A进来,通过计算其key的hash得到index,记做Entry[index]=A。一会又进来一个键值对B,通过计算其key的hash也是index,HashMap会将B.next=A, Entry[index]=B.如果又进来C,其key的hash也是index,会将C.next=B, Entry[index]=C.这样bucket为index的地方存放了A\\B\\C三个键值对,它们能过next属性链在一起。数组中存储的是最后插入的元素,其他元素都在后面的链表里。

7.4 如果两个键的hashcode相同,如何获取值对象?

当调用get方法时,hashmap会使用键对象的hashcode找到bucket位置,找到bucket位置后,会调用key.equals()方法去找到链表中正确的节点,最终找到值对象。

7.5 hashMap如何扩容

HashMap默认负载因为是0.75,当一个map填满了75%的bucket时,和其他集合类一样,将会创建原来HashMap大小两倍的bucket数组,来重新调整HashMap的大小,并将原来的对象放入新的bucket数组中。

在jdk1.7及以前,多线程扩容可能出现死循环。因为在调整大小过程中,存储在某个bucket位置中的链表元素次序会反过来,而多线程情况下可能某个线程翻转完链表,另外一个线程又开始翻转,条件竞争发生了,那么就死循环了。

而在jdk1.8中,会将原来链表结构保存至节点e中,将原来数组中的位置设为null,然后依次遍历e,根据hash&n是否为0分成两条支链,保存在新数组中。如果多线程情况可能会取到null值造成数据丢失。

8、ConcurrentHashMap的实现原理

1)jdk1.7及以前:一个ConcurrentHashMap由一个segment数组和多个HashEntry组成,每一个segment都包含一个HashEntry数组, Segment继承ReentrantLock用来充当锁角色,每一个segment包含了对自己的HashEntry的操作,如get\\put\\replace操作,这些操作发生时,对自己的HashEntry进行锁定。由于每一个segment写操作只锁定自己的HashEntry,可以存在多个线程同时写的情况。

jdk1.8以后:ConcurrentHashMap取消了segments字段,采用transient volatile HashEntry<K, V> table保存数据,采用table数组元素作为锁,实现对每一个数组数据进行加锁,进一小减少并发冲突概率。ConcurrentHashMap是用Node数组+链表+红黑树数据结构来实现的,并发制定用synchronized和CAS操作。

2)Segment实现了ReentrantLock重入锁,当执行put操作,会进行第一次key的hash来定位Segment的位置,若该Segment还没有初始化,会通过CAS操作进行赋值,再进行第二次hash操作,找到相应的HashEntry位置。

9、ArrayMap和HashMap的对比

1)存储方式不一样,HashMap内部有一个Node<K,V>[]对象,每个键值对都会存储到这个对象里,当用put方法添加键值对时,会new一个Node对象,tab[i] = newNode(hash, key, value, next);

ArrayMap存储则是由两个数组来维护,int[] mHashes; Object[] mArray; mHashes数组中保存的是每一项的HashCode值,mArray存的是键值对,每两个元素代表一个键值对,前面保存key,后面保存value。mHashes[index]=hash; mArray[index<<1]=key; mArray[(index<<1)+1]=value;

ArrayMap相对于HashMap,无需为每个键值对创建Node对象,且在数组中连续存放,更省空间。

2)添加数据时扩容处理不一样,进行了new操作,重新创建对象,开销很大;而ArrayMap用的是copy数据,所有效率相对高些;

3)ArrayMap提供了数组收缩功能,在clear或remove后,会重新收缩数组,释放空间;

4)ArrayMap采用二分法查找,mHashes中的hash值是按照从小到大的顺序连续存放的,通过二分查找来获取对应hash下标index,去mArray中查找键值对。mHashes中的index*2是mArray中的key下标,index*2+1为value的下标,由于存在hash碰撞情况,二分查找到的下标可能是多个连续相同的hash值中的任意一个,此时需要用equals比对命中的key对象是否相等,不相等,应当从当前index先向后再向前遍历所有相同hash值。

5)sparseArray比ArrayMap进一步优化空间,SparseArray专门对基本类型做了优化,Key只能是可排序的基本类型,如int\\long,对value,除了泛型Value,还对每种基本类型有单独实现,如SparseBooleanArray\\SparseLongArray等。无需包装,直接使用基本类型值,无需hash,直接使用基本类型值索引和判断相等,无碰撞,无需调用hashCode方法,无需equals比较。SparseArray延迟删除。

10、HashTable实现原理

Hashtable中的无参构造方法Hashtable()中调用了this(11, 0.75f),说明它默认容量是11,加载因子是0.75,在构造方法上会new HashtableEntry<?, ?>[initialCapacity]; 会新建一个容量是初始容量的HashtableEntry数组。HashtableEntry数组中包含hash\\Key\\Value\\next变量,链表形式,重写了hashCode和equals方法。Hashtable所有public方法都在方法体上加上了synchronized锁操作,说明它是线程安全的。它还实现了Serializable接口中的writeObject和readObject方法,分别实现了逐行读取和写入的功能,并且加了synchronized锁操作。

(1) put(Key, Value)方法

1)先判断value是否为空,为空抛出空指针异常;

2)根据key的hashCode()值,计算table表中的位置索引(hash&0x7FFFFFFF)%tab.length值index,如果该索引处有值,再判断该索引处链表中是否包含相同的key,如果key值相同则替换旧值。

3)如果没有相同的key值,调用addEntry方法,在addEntry中判断count大小是否超过了最大容量限制,如果超过了需要重新rehash(),容量变成原来容量*2+1,将原表中的值都重新计算hash值放入新表中。再构造一个HashtableEntry对象放入相应的table表头,如果原索引处有值,则将table[index].next指向原索引处的链表。

(2)get方法

根所key.hashCode(),计算它在table表中的位置,(hash&0x7FFFFFFF)%tab.length,遍历该索引处表的位置中是否有值,是否存在链表,再判断是key值和hash值是否相等,相等则返回对应的value值。

11、HashMap和HashTable的区别

1)Hashtable是个线程安全的类,在对外方法都添加了synchronized方法,序列化方法上也添加了synchronized同步锁方法,而HashMap非线程安全。这也导致Hashtable的读写等操作比HashMap慢。

2)Hashtable不允许值和键为空,若为空会抛出空指针。而HashMap允许键和值为空;

3)Hashtable根据key值的hashCode计算索引,(hash&0x7FFFFFFF)%tab.length,保证hash值始终为正数且不超过表的长度。而HashMap中计算索引值是通过hash(key)&(tab.length-1),是通过与操作,计算出在表中的位置会比Hashtable快。

4)Hashtable容量能为任意大于等于1的正数,而HashMap的容量必须为2^n,Hashtable默认容量为11,HashMap初始容量为16

5)Hashtable每次扩容,新容量为旧容量的2倍+1,而HashMap为旧容量的2倍。

12、HashMap与HashSet的区别

HashSet底层实现是HashMap,内部包含一个HashMap<E, Ojbect> map变量

private transient HashMap<E,Object> map;

一个Object PRESENT变量(当成插入map中的value值)

private static final Object PRESENT = new Object();

HashSet中元素都存到HashMap键值对的Key上面。具体可以查看HashSet的add方法,直接调用了HashMap的put方法,将值作为HashMap的键,值用一个固定的PRESENT值。

public boolean add(E e) 
        return map.put(e, PRESENT)==null;
    

HashSet没有单独的get方法,用的是HashMap的。HashSet实现了Set接口,不允许集合中出现重复元素,将对象存储进HashSet前,要先确保对象重写了hashCode()和equals方法,以保证放入set对象是唯一的。

13、HashSet与HashMap怎么判断集合元素重复?

HashMap在放入key-value键值对是,先通过key计算其hashCode()值,再与tab.length-1做与操作,确定下标index处是否有值,如果有值,再调用key对象的equals方法,对象不同则插入到表头,相同则覆盖;

HashSet是将数据存放到HashMap的key中,HashMap是key-value形式的数据结构,它的key是唯一的,HashSet利用此原理保证放入的对象唯一性。

14、集合Set实现Hash怎么防止碰撞

HashSet底层实现是HashMap,HashMap如果两个不同Key对象的hashCode()值相等,会用链表存储,HashSet也一样。

15、ArrayList和LinkedList的区别,以及应用场景

ArrayList底层是用数组实现的,随着元素添加,其大小是动态增大的;在内存中是连续存放的;如果在集合末尾添加或删除元素,所用时间是一致的,如果在列表中间添加或删除元素,所用时间会大大增加。通过索引查找元素速度很快。适合场合:查询比较多的场景

LinkedList底层是通过双向链表实现的,LinkedList和ArrayList相比,增删速度快,但查询和修改值速度慢。在内存中不是连续内存。场景:增删操作比较多的场景。

16、二叉树的深度优先遍历和广度优先遍历的具体实现

public class Tree 
    Tree left, right;
    int data;
    public Tree(int data) 
        this.data = data;
    


//深度优先遍历(其实和前序遍历实现一样)
public void queryByDeepth(Tree root) 
    if(root != null) 
        print(root.data);
    
    if(root.left != null) queryByDeepth(root.left);
    if(root.right != null) queryByDeepth(root.right);


//广度优先遍历(用队列辅助实现)
public void queryByDeepth(Tree root) 
    if(root == null) return;
    Queue<Tree> queue = new LinkedList<Tree>();
    queue.offer(root);
    while(root != null || !queue.isEmpty()) 
        root = queue.poll();
        print(root.data);
        if(root.left != null) queue.offer(root.left);
        if(root.right != null) queue.offer(root.right);
    
    

17、堆的结构

java的堆用于存放对象实例的,通过new一个对象实例,在堆上给实例分配内存空间。而数据结构中堆是一种树,实现了优先队列的插入和删除的时间复杂度都为o(logn),常用数组来表示。

18、堆和树的区别

堆是一棵除最底层外被完全填满的二叉树,底层上的元素从左到右填入,即完全二叉树,它可用一个数组表示,对于数组任一个位置上的i个元素,左儿子位置为2*i+1,右儿子在2*i+2上,父亲则在位置i/2上。

树又分二叉树(每个节点不能有多于两个的子节点)、二叉查找树(在二叉树基础上,对树中每个节点X,左子树中所有项的值小于X中的项,右子树中所有项的值大于X中的项)、AVL树(深度必须是O(logN),平衡二叉查找树的每个节点的左子树和右子树最多差1的二叉查找树,对于插入后破坏平衡性,通过旋转调节)、伸展树。

19、堆和栈在内存中的区别是什么(解答提示:可以从数据结构方面以及实际实现方面两个方面去回答)?

(1)数据结构方面:堆是一种二叉树;

栈是一种后进先出的存储结构,有压栈和出栈两种操作。

(2)内存方面:

栈内存:静态变量、局部变量是以压栈出栈的方式分配内存,系统会在一个代码段中分配和回收局部变量,实际上每个代码段、函数都是一个或多个嵌套的栈,不需要手动管理栈区内存。

堆内存:在Java运行时被用为对象分配内存,GC在堆内存上释放没有任何引用的对象所占的内存,任何在堆上被创建的对象都有一个全局的访问。

20、什么是深拷贝和浅拷贝

1)深拷贝:不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。即对象进行深拷贝要对整个对象图进行拷贝。

实现方式:a)重写clone方法,要为对象图的每一层的每个对象都实现Cloneable接口并重写clone方法。b)将对象序列化为字节序列后,默认会将该对象整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

2)浅拷贝:对于基本数据类型,直接进行值传递,即直接复制一个值给新对象,对一个对象的值改变不会影响到拷贝的数据;对于引用数据类型的成员变量,如成员变量是某个数组或类对象,它会进行引用传递,只是将该成员变量的引用值(地址)复制一份给新对象,因为两个对象都指向同一个实例,故改变该成员变量会影响到另一个对象的该成员变量值。

实现方式:a)通过拷贝构造方法实现,指该类的构造方法的参数为该类的对象。b)重写clone方法,使用clone方法的类必须实现Cloneable接口。

21、手写链表逆序代码

//假设LinkedList的结点是Node
//1、遍历法
public Node reverseLinkedList(LinkedList head) 
    if(head == null || head.next == null) return head;
    Node pre = null, next = null;
    while(head != null) 
        next = head.next;
        next.next = pre;
        pre = head;
        head = next;
    
    return pre;

//2、递归法
public Node reverseLinkedList(LinkedList head) 
    if(head == null || head.next == null) return head;
    Node next = head.next;
    Node newNode = reverseLinkedList(next);
    next.next = head;
    head.next = null;
    return newNode;

22、讲一下对树,B+树的理解

具体参考这篇文章吧:B+树

B树一个节点可以拥有多于2个子节点的二叉查找树,每个节点有M-1个key,且以升序排列,位于M-1和M key的子节点的值位于M-1和M key对应的value之间,其它节点至少有M/2个子节点,普遍运用在数据库和文件系统中。

 

23、讲一下对图的理解

图就是一些顶点的集合,这些顶点通过一系列边结对,顶点用圆圈表示,边就是这些圆圈之间的连线,顶点之间通过边连接。

它的物理存储结构有:

1)邻接矩阵:用两个数组存储图的信息,1个以数组存储顶点,一个二维数组存储边的信息。对于顶点多而边数少的稀疏图造成存储空间大量浪费。

2)邻接表:数组+链表,用数组存储每个节点,数组中每个节点的所有邻接点组成一个链表,邻接表关心了出度,但查找入度就要遍历整个图。

遍历图:从图中某一个顶点出发遍历途中其余顶点,每一个顶点仅被访问一次。

1)深度优先遍历

int[] visited = new int[g.vnum];
public void dfsVisitGraph(Graph g, int i) 
    visited[i] = 1;
    print(g.vex[i]);
    for(int j = 0; j < g.vnum; j++) 
        if(g.arc[i][j] != 0 && g.arc[i][j] != IUNFINITY && !visited[j]) 
            dfsVisitGraph(g, j);
        
    

public void dfsTraverse(Graph g) 
    for(int i = 0; i < g.vnum; i++) 
        if(!visited[i]) 
            dfsVisitGraph(g, i);
        
    

        

2)广度优先遍历(队列)

public void bfsTraverse(Graph g) 
    Queue<Integer> queue = new LinkedList<>();
    int[] visited = new int[g.vnum];
    for(int i = 0; i < g.vnum; i++) 
        if(visited[i] == 0) 
            visited[i] = 1;
            queue.offer(g.vexs[i]);   
            print(g.vexs[i]);
            while(!queue.isEmpty()) 
                int link = queue.poll();
                for(int j = 0; j < g.vnum; j++) 
                    if(g.arc[link][j] == 1 && visited[j] == 0) 
                        visited[j] = 1;
                        print(g.vexs[j];
                        queue.offer(g.vexs[i]);   
                    
                
            
        
    

24、判断单链表成环与否?

public boolean linkHasCircle(LinkedList node) 
    if(node == null || node.next == null) return false;
    Node first = node, second = node;//first慢指针一次走一步;second快指针一次走两步
    while(second.next != null && second.next.next != null) 
        first = node.next;
        second = node.next.next;
        if(first == second) 
            return true;
        
    
    return false;

25、合并多个单有序链表(假设都是递增的)

这个主要遍历链表,比较值大小,如果需要返回链表头节点,则需要先把头结点保存好

public Node merge(LinkedList node1, LinkedList node2) 
    if(node1 == null) return node2;
    if(node2 == null) return node1;
    if(node1 == null && node2 == null) return null;
    int data1 = node1.data;
    int data2 = node2.data;
    Node newNode, head;
    int data = data2;
    if(data1 <= data2) 
        data = data1;
        node1 = node1.next;
     else 
        node2 = node2.next;
    
    newNode = new Node(data);
    head = newNode;

    while(node1 != null && node2 != null) 
        if(node1.data <= node2.data) 
            newNode.next.data = node1.data;
            node1 = node1.next;
         else 
            newNode.next.data = node2.data;
            node2 = node2.next;
        
        newNode = newNode.next;
    
    while(node1 != null) 
            newNode.next.data = node1.data;
            node1 = node1.next;
            newNode = newNode.next;
    

    while(node2 != null) 
            newNode.next.data = node2.data;
            node2 = node2.next;
            newNode = newNode.next;
    
    return head;

            

(四) 线程、多线程和线程池

1、开启线程的三种方式?

1)继承Thread类,重写run()方法,在run()方法体中编写要完成的任务 new Thread().start();

2)实现Runnable接口,实现run()方法 new Thread(new MyRunnable()).start();

3)实现Callable接口MyCallable类,实现call()方法,使用FutureTask类来包装Callable对象,使用FutureTask对象作为Thread对象的target创建并启动线程;调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable());

new Thread(ft).start();

2、run()和start()方法区别

run()方法只是线程的主体方法,和普通方法一样,不会创建新的线程。只有调用start()方法,才会启动一个新的线程,新线程才会调用run()方法,线程才会开始执行。

3、如何控制某个方法允许并发访问线程的个数?

创建Semaphore变量,Semaphore semaphore = new Semaphore(5, true); 当方法进入时,请求一个信号,如果信号被用完则等待,方法运行完,释放一个信号,释放的信号新的线程就可以使用。

4、在Java中wait和seelp方法的不同

wait()方法属于Object类,调用该方法时,线程会放弃对象锁,只有该对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

sleep()方法属于Thread类,sleep()导致程序暂停执行指定的时间,让出CPU,但它的监控状态依然保存着,当指定时间到了又会回到运行状态,sleep()方法中线程不会释放对象锁。

5、谈谈wait/notify关键字的理解

notify: 唤醒在此对象监视器上等待的单个线程

notifyAll(): 通知所有等待该竞争资源的线程

wait: 释放obj的锁,导致当前的线程等待,直接其他线程调用此对象的notify()或notifyAll()方法

当要调用wait()或notify()/notifyAll()方法时,一定要对竞争资源进行加锁,一般放到synchronized(obj)代码中。当调用obj.notify/notifyAll后,调用线程依旧持有obj锁,因此等待线程虽被唤醒,但仍无法获得obj锁,直到调用线程退出synchronized块,释放obj锁后,其他等待线程才有机会获得锁继续执行。

6、什么导致线程阻塞?

(1)一般线程阻塞

1)线程执行了Thread.sleep(int millsecond)方法,放弃CPU,睡眠一段时间,一段时间过后恢复执行;

2)线程执行一段同步代码,但无法获得相关的同步锁,只能进入阻塞状态,等到获取到同步锁,才能恢复执行;

3)线程执行了一个对象的wait()方法,直接进入阻塞态,等待其他线程执行notify()/notifyAll()操作;

4)线程执行

以上是关于2019年为android开发准备的面试题(含答案)的主要内容,如果未能解决你的问题,请参考以下文章

2022最新 Android 中高级面试题汇总(含答案解析)

2022最新 Android 中高级面试题汇总(含答案解析)

2019年3月springboot最新面试题(含详细答案)

刚参加完阿里Android开发岗面试:一面+二面+三面+HR四面,定级P6,面试经验分享总结!(含必考题答案)

Android岗高频面试题合集(含答案)

java程序员面试题大全含答案(2018--2019)