Android 面试指南
October 9, 2017 · View on GitHub
Android 面试指南
关于
受 android-interview-questions 项目启发,这里想发挥众多 Android 中国开发者的力量,整理一份高质量、范围全的 Android 面试指南,旨在帮助更多的 Android 开发者提升技术,找到工作。
现在还是项目初期,项目背景见这里:想跟大家一起做件小事,也欢迎关注微信公众号 stormzhang,后续有任何进展我都会在公号进行通知的。
目前该项目有如下初步的计划:
-
翻译 android-interview-questions 项目,不过翻译仅是我们的第一步而已,这个比较简单,目前第一步翻译工作已经被认领完毕;
-
因为原项目很多只是列了一些问题,但是都没有答案的,所以第二步我们就是认领问题,整理答案,务必保证高质量、易理解,因为问题很多,所以这一步需要花费不少精力,也需要更多的同学参与进来,目前还在第一步阶段;
-
第三步是补充与完善,原项目虽然列了不少领域,但是总归有些遗漏的,比如 Android 安全、插件化、Kotlin 等等,第三步是找在某一领域研究比较深的同学加入进来,对一些领域进行补充、完善,甚至会推出一些专题等;
Contents
数据结构与算法
数据结构与算法问题的难度完全取决于你所申请的公司
-
数组
-
数组由一组相同的数据类型组成。它存储在连续的内存空间内,使用索引可以找到元素的地址。数组包括一维数组和多维数组,一维数组是最简单的数据结构,也是最常用的。
算法 平均 最坏 空间(Space) O(n) O(n) 查找(Search) O(n) O(n) 插入(Insert) O(n) O(n) 删除(Delete) O(n) O(n)
-
-
链表
-
链表看起来更像树,而不是数组,它使用一组结点来表示一个序列。每一个结点都包含数据和一个指针。在链表中,结点中的数据可以为任意类型,而指针则是指向下一结点的引用。链表包含一个头结点和一个尾结点。头结点是链表中的第一个结点,尾结点是最后一个结点。链表不是一个循环数据结构,所以尾结点没有指向头结点的指针,指针为空。一些基础方法的时间复杂度如下:
算法 平均 最坏 空间 (Space) O(n) O(n) 查找 (Search) O(n) O(n) 插入 (Insert) O(1) O(1) 删除 (Delete) O(1) O(1)
-
-
双向链表
-
一个双向链表首先是一个链表,但是在每个结点中有两个指针,前驱指针指向前驱结点,后继指针指向后继结点。双向链表也有一个头结点,头结点的后继指针指向第一个结点。最后一个结点的后继指针指向空,但是如果最后一个结点的后继指针指向第一个结点,这时称这个链表为双向循环链表。双向循环链表能非常方便地从每个结点查找它的前驱结点和后继结点。

算法 平均 最坏 空间 (Space) O(n) O(n) 查找 (Search) O(n) O(n) 插入 (Insert) O(1) O(1) 删除 (Delete) O(1) O(1)
-
-
栈
-
栈是一个有着「后进先出」特性的基础数据结构,这就意味着最后一个入栈的元素,也是第一个出栈的。栈就像是一堆书,想要得到书堆中的第一本书(最下面一本),必须把其他的书都先拿走。向栈中添加一个元素的操作被称为 Push(入栈),删除一个元素的操作被称为 Pop(出栈),查看且不删除最后一个入栈的元素的操作被称为 Top 。实现栈的常用方法是使用链表(LinkedList),也可以使用不允许空值的 StackArray(使用数组实现),还有允许空值的 Vector
算法 平均 最坏 图形表示 空间 (Space) O(n) O(n)
查找 (Search) O(n) O(n) 入栈 (Push) O(1) O(1) 出栈 (Pop) O(1) O(1) 查看栈顶 (Top) O(1) O(1)
-
-
队列
-
优先队列
-
动态编程
-
字符串操作
-
二叉树
-
二叉搜索树
-
排序算法
-
哈希表与哈希图
-
广度优先搜索
-
深度优先搜索
-
贪心算法
所谓贪心算法(贪婪算法),是在求解一个问题时,作出当前最好的选择,而不考虑大局。也就是说,这种算法的每一种实现,只是在作出一个某种方面上的局部最优解。
我们可以说贪心算法是「短视的」, 「不可恢复」的
此算法没有固定框架,只有每个人贪心策略的选择,而导致的不同姿态的代码实现。
来自wikipedia:
贪心法,又称貪心演算法、貪婪演算法、或稱貪婪法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。[1]比如在旅行推销员问题中,如果旅行员每次都选择最近的城市,那这就是一种贪心算法。
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。
Java 核心
- 抽象
在面向对象的概念中,所有对象都是由类来描述,但是反过来,并不是所有类都是用来描述对象的。如果一个类中没有包含足够信息来描绘一个具体的对象,这样的类就是抽象类。
- 继承
继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别A“继承自”另一个类别B,就把这个A称为“B的子类别”,而把B称为“A的父类别”也可以称“B是A的超类”。继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类别的行为在编译期就已经决定,无法在执行期扩充。 有些编程语言支持多重继承,即一个子类别可以同时有多个父类别,比如C++编程语言;而在有些编程语言中,一个子类别只能继承自一个父类别,比如Java编程语言,这时可以利用接口来实现与多重继承相似的效果。 现今面向对象程式设计技巧中,继承并非以继承类别的“行为”为主,而是继承类别的“型态”,使得元件的型态一致。另外在设计模式中提到一个守则,“多用合成,少用继承”,此守则也是用来处理继承无法在执行期动态扩充行为的遗憾。
- 封装
从字面上理解就是包装的意思,是指利用抽象数据类型,将数据和关于数据的操作封装起来,使其成为一个不可分割的独立实体。数据将会被保护在抽象数据类型的内部,仅能够通过暴露在表面的操作(public方法,比如setter和getter)来与这个对象进行交流和交互。用户不知道对象的内部细节,但是通过该对象提供的接口来访问对象。其好处是:减少耦合,方便地在未来修改调整自己,更加有把握地(精确地)控制成员,隐藏信息,实现细节。
- 多态
使用相同的消息,使得类作出不同的反应(继承为我们使用多态打下了基础)。Java实现多态有三个必要条件:继承、重写、向上转型。
- 面向对象的三个基本元素和五个原则
-
三个基本元素:
封装: 封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。
继承: 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
多态: 多态性是指允许不同类的对象对同一消息作出 响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
-
五个基本原则:
单一职责原则(Single-Resposibility Principle):一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
开放封闭原则(Open-Closed principle):软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
Liskov替换原则(Liskov-Substituion Principle):子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
依赖倒置原则(Dependecy-Inversion Principle):依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
接口隔离原则(Interface-Segregation Principle):使用多个小的专门的接口,而不要使用一个大的总接口。
-
抽象类和接口的区别?Link
-
抽象类是一个可同时包含具体方法和抽象方法(方法未被实现)的类。抽象方法必须被该抽象类的子类实现。抽象类是可以继承的。
-
接口像是描述类的一张蓝图或者说是类的一种契约,它包含了许多空方法,这代表着它的所有的子类都应该拥有共同点。它的子类应该提供这些空方法的具体实现。一 个类需要用
implements来实现接口,接口可以用extends来继承其他接口。 -
在设计层级理解他们的不同:
- 抽象的层次不同:抽象类对类的整体(包括属性,行为)都可以进行抽象,接口对类的局部进行抽象,具体来说接口仅仅是对类的行为进行抽象。
- 跨域不同:抽象类是 从各种子类中提取相似的部分,然后泛化成抽象类,子类可以继承这样的抽象类。 实现接口是 不存在
is-a的关系的类们,你不可以称同样可以飞行的飞机和鸟为同一个抽象类,但是他们可以有同样的接口fly-able。抽象类的父类和派生类在概念上一致,接口的原生类和派生类在仅仅在局部行为上一致。 - 设计层次不同:抽象类是从一堆在底层的子类们来进行抽象提取,从下往上,从而产生抽象类;接口是在直接定义的高度来声明的,然后从这个高度上往下实现此接口。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。
-
-
序列化是什么?如何实现它?
- 序列化是一种将对象转换为字节流的过程,目的是为了将该对象存储到内存中,等后面再次构建该对象时可以获取到该对象先前的状态及数据信息。Java中,有两种方式可以实现序列化,既可以实现Serializable接口,也可以实现Parcelable接口。然而,在Android中,我们不应该使用Serializable接口。因为Serializable接口使用了反射机制,这个过程相对缓慢,而且往往会产生出很多临时对象,这样可能会触发垃圾回收器频繁地进行垃圾回收。相比而言,Parcelable接口比Serializable接口效率更高,性能方面要高出10x多倍。
-
什么是单例?
- 单例模式指的是一个类只能被初始化一次,即只有一个实例。单例模式限定一个类只能拥有一个实例。这在系统中只需要一个实例来和其他模块协调工作时是很实用的。单例普遍使用在只需要一个或是限制一定数量实例的系统中。(wikipedia)
-
什么是匿名内部类?
普通的类可以自然地实例化他自己,相反地,内部类是这样的类: 一定要绑定上一个外部类才能进行实例化的类。
内部类提供了一种模拟车和车轮的机制,车是外部类,车轮是内部类
而匿名内部类也就是没有名字的内部类,因为没有名字,所以只能使用一次。
使用匿名内部类还有个前提条件:必须继承一个父类或实现一个接口
详情可见:
http://www.cnblogs.com/nerxious/archive/2013/01/25/2876489.html
-
对字符串进行
==和equals()操作时有什么区别?==比较两个字符串的地址,初学者很经常拿来比较其内容,将会导致出现不等的情况。equals()是String这个类重写的一个方法,平常的类的equals()也仅仅是比较两个变量的地址,而String类的equals()重写后,将依次比较其串中的字符。 -
hashCode()和equals()何时使用?一般是在想要人性化地(而不是计算机式地,比较地址那样)比较两个对象的时候,我们需要使用这两个方法,或者说我们要重写这两个方法,而且有如下的原则:
- hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;
- 如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
- 如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
- 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
hashCode请参考:http://blog.csdn.net/fenglibing/article/details/8905007
-
Java 中的
final,finally和finalize?- final: 修饰变量,方法,类; 修饰变量时表明这对象的值不可变,你不能为这个变量赋一个新的值,或者这样说:对基本类型而言,你不能改变其数值,对于引用,你不能将其指向一个新的引用(而引用自身是可以改变的)。 修饰方法时表明我们希望把这个方法锁定,以防止任何继承类修改它的含义,这样会确保在继承中,我们的final方法的行为不会改变,并且不会被覆盖。使用final方法的另一个考虑是效率问题:在Java早期的时候,遇到final方法,编译器会将此方法调用转为内嵌调用,如此一来以减小方法调用产生的开销。 修饰类的时候表明你不打算继承该类,而且也不允许别人这样做。
- finally:是异常处理中进行收场处理的代码块,比如关闭一个数据库连接,清理一些资源占用的问题。不管有没有异常被捕获,finally子句中的代码都会被执行。
- finalize:finalize出现的原因在于: 我们一定需要进行清理动作。Java没有用于释放对象的,如同C++里的delete调用,的方法,而是使用垃圾回收器(GC)帮助我们释放空间。当垃圾回收器准备释放对象占用的存储空间的时候,将首先调用其finalize()方法。
-
什么是内存泄露,Java 是如何处理它的?
总的来说是:保留下来却永远不再使用的对象引用 它包括: 1. 静态集合类引起内存泄漏, 2. 当集合里面的对象属性被修改后,再调用remove()方法时不起作用, 3. 监听器, 4. 各种连接 5. 内部类和外部模块的引用 6. 单例模式 详情可见:http://note.youdao.com/noteshare?id=376631d4640128dc55646c8e577cc3ab
而关于Java 如何处理它,我还不能给出准确答案,应该是使用各种检测泄漏的工具检测到之后,使用GC处理它们
-
垃圾收集器是什么?它是如何工作的
-
所有的对象实例都在JVM管理的堆区域分配内存
只要对象被引用,JVM就会认为它还存活于进程中。
一旦对象不再被引用,就不能被应用程序所访问,
垃圾收集器将删除它并重新声明未使用的内存。
-
-
比较
Arrays和ArrayLists。Arrays:一个包含许多和操纵数组有关方法的类,比如排序和查找,它继承自Object类。
ArraysList:是一个容器,它可以实现数组的大小可变,方便地增加和删除元素。它实现了List接口的类。
详细的笔记在:http://note.youdao.com/noteshare?id=659562b6beba34649d769d09cd2e0533
- 比较
HashSet和TreeSet。
TreeSet是基于二叉树实现的,其中的数据是自动排序好的。不允许放入null值。
HashSet是基于Hash表实现的,其中的数据是无序的,允许放入null值。
详细的笔记在:http://note.youdao.com/noteshare?id=4a3e44e90105d9906eb308317bc816bb
- Java 中的类型转换。
1. 基本数据的类型转换
在Java中,整数类型(byte/short/int/long)中,对于未声明数据类型的整形,其默认类型为int型。在浮点类型(float/double)中,对于未声明数据类型的浮点型,默认为double型。而当我们将一个数值范围较小的类型赋给一个数值范围较大的数值型变量,jvm在编译过程中会将此数值的类型进行自动提升。在数值类型的自动类型提升过程中,数值精度至少不应该降低(整型保持不变,float->double精度将变高)。而当我们需要将数值范围较大的数值类型赋给数值范围较小的数值类型变量时,我们需要手动地转换,称为强制类型转换。
2.引用数据类型的类型转换
由于继承和向上转型,子类向父类的转换是很自然的。但是当把父类转换为子类时,强制类型转换会在运行时检查父类的真实类型:如果引用的父类对象的真实身份是子类类型,只不过这个子类类型的对象经历了向上转型变成父类对象,这个时候我们再向下转回来子类,是可以的;而如果真的是父类的类型,就会抛出ClassCastException的异常。
详细的笔记在:http://note.youdao.com/noteshare?id=7e85432e5d781ef8ad86ddabfc964885
-
方法重载和重写的区别
- 重载发生在编译时,重写发生在运行时。重载方法调用与其定义的绑定发生在编译时,但是重写方法调用与其定义的绑定在运行时发生。
- 静态方法可以重载,意味着一个类可以有多个同名的静态方法。静态方法不能被重写,即使在子类中声明了一个相同的静态方法,它与父类的相同方法无关。
- 最基本的区别是重载是在同一个类中完成的,重写父类需要有子类。重写是给父类的继承方法一个具体的实现。
- 静态绑定用于方法重载,动态绑定用于方法重写。性能:重载比重写更有效率,原因是方法重写的绑定是在运行时完成的。
- 私有方法和用
final修饰的方法可以重载但不可重写。这意味着一个类可以有多个同名的private/final方法,子类不能重写父类的private/final方法。 - 方法重载的情况下不关心返回值类型, 它可以相同,也可以不同。但是方法重写的情况下可以有多个具体的返回值类型。 - 方法重载时参数列表必须不同,方法重写时参数列表必须相同。
-
什么是访问修饰符?它们能做什么?
诸如public,protected,private这几个关键词叫做访问修饰符。
其作用是控制它所定义的域或者方法的访问权。
-
接口可以继承另一个接口吗?
可以,通过继承,你可以
-
在接口中添加新的方法声明
-
还可以通过继承,在新的接口中组合数个接口,这时只需要用逗号将接口名一一分开来即可。
-
-
Java 中
static关键字是什么意思?static是Java里的非访问修饰符,它可以用来创建类方法和类变量。
当修饰一个变量的时候,此变量就成了独立于对象的静态变量,无论一个类实例化了多少个对象,这个类只有一份这个静态变量的拷贝,所以static修饰的变量,即静态变量,也被叫做类变量。一个局部变量不能被声明为static变量。
当修饰一个方法的时候,此方法就成了独立于对象的静态方法,静态方法不能使用类的非静态变量,因为静态方法和静态变量先于非静态的其他成员初始化,静态方法先出来,然后才是非静态的,所以明白这个顺序很重要。静态方法从参数列表得到数据,然后计算这些数据。
有些人认为static方法不是面向对象的,因为它们确实具有全局函数的语义;使用static方法时,由于不存在this,所以不是通过「向对象发送消息」的方式来完成的。当代码中出现很多static方法时,你应该反思自己的设计的,然而,static确有其用。
总结一下,当static:
-
修饰方法时,此方法能够不用在初始化对象的前提下直接调用,即,可以直接通过类名.static方法()这样来访问。
-
修饰代码块时,这部分代码块只会被执行一次。
-
修饰变量时,这个变量只在内存中有一个副本。
-
-
Java 中静态方法可以被重写吗?
严格来说,不存在静态方法的重写,当一个子类继承父类时,写同样的方法时,只是将父类的静态方法隐藏。
[静态方法能否被重写](http://xm-king.iteye.com/blog/745787)
所谓静态,就是在运行时,虚拟机已经认定此方法属于哪个类。 专业术语有严格的含义,用语要准确."重写"只能适用于实例方法.不能用于静态方法.对于静态方法,只能隐藏. 静态方法的调用不需要实例化.. 不实例化也就不能用多态了,也就没有所谓的父类引用指向子类实例.因为不能实例化 也就没有机会去指向子类的实例。所以也就不存在多态了。
-
什么是多态?什么是继承?
- 多态是:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘自“Delphi4 编程技术内幕”)
- 继承是:子类继承父类的特征和行为,使得子类具有父类的各种属性和方法。
-
Integer和int之间的区别- Integer是int提供的封装类,而int是Java的基本数据类型; Integer默认值是null,而int默认值是0; 声明为Integer的变量需要实例化,而声明为int的变量不需要实例化; Integer是对象,用一个引用指向这个对象,而int是基本类型,直接存储数值
- 两个new 出来的Integer总是不一样的。当使用
==时,发现其内存地址不同,所以进行==返回false - 两个不是new出来的Integer,而是诸如Integer i = x,如果x的范围在-128~127,因为如下语句:
Integer i5 = 127;//java在编译的时候,被翻译成-> Integer i5 = Integer.valueOf(127);调用了Integer.valueOf (), 这会将127缓存,下次我们写:Integer i6 = 127;时,i6指向缓存中的同一个对象。所以此时i5==i6是true; 当x范围在-128~127之外,没有缓存存在,即使他们包裹的数值相等,他们也不能使用==的到true。 - int和Integer的比较,无论Integer是否使用new,其值和将和平常的预料的一样:Integer自动拆箱,然后和int比较数值.这里就不会被内存地址的不同所影响,该相等时就相等。
-
Java 中的对象是否会以引用传递或者值传递?详细说明。
Java中的对象总是以值传递的。
private void init(MyClass objVar) {
objVar = new MyClass();
}
public void test() {
MyClass obj = null;
init(obj);
//在调用init方法后,obj仍旧指向空
//这是因为,obj是值传递,而不是引用传递。
}
- 什么是 ThreadPoolExecutor? Link
中文翻译请参考这里:http://note.youdao.com/noteshare?id=84c9bc5c8b01defcb9db9ca963bf6f67
- 本地变量、实例变量以及类变量之间的区别?
本地变量就是局部变量,它在方法或者代码块里被声明并使用,其内存中的位置是栈里,没有默认初始化值,生命周期很短。 实例变量是没有被static修饰的成员变量,它属于一个类的一个实例。每次new一个实例,这样的变量也同时new一遍,其位置在堆区中,有默认初始化的值,生命周期和它所在的实例一样长。 类变量,又称静态变量,它是被static修饰的成员变量,它属于一个类,被所有实例共享。每次new一个实例,这样的变量并不会被new一遍,其内存位置在方法区内。可以通过类名直接访问。有默认的初始化值,生命周期很长。
-
什么是反射? Link
-
在 Java 中什么是强引用、软引用、弱引用以及虚引用?
强引用:不会被GC轻易清理,只要引用存在,垃圾回收器永远不会回收。
Object obj = new Object();
软引用: 非必须引用,内存溢出之前进行回收
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
弱引用: 第二次垃圾回收时回收
``` Object obj = new Object(); WeakReference