Android面试小技巧:随机应变,知己知彼,别太诚实,手握筹码

  • 5月16日 然后戏剧性的一幕出现了!!!早上10点过了没多久,头条的HR打电话过来叫我准备下一轮面试,时间安排在下星期一,而且语气一股没得商量的感觉(内心OS:这是什么意思???)
  • 5月20日 我收到了美团的offer,自然就没继续去面头条了.
  • 5月21日 下午HR打电话问我还考不考虑,我感觉头条对我不够感兴趣,就拒绝了,虽然真的很感谢当时该我白金码的那位大佬,但总感觉我并不是头条要找的人.

1.3 美团

  • 5月初 之前就一直想投美团,但一直觉得挺迷的,牛客和实习僧这两个网站都被头条刷屏了,为什么TMD中的MD还一点动静都没有?就算有也只有iOS岗,所以就很郁闷,在想可能公司不缺人吧.
  • 5月5日 突然反应过来还可以去官网投递啊,然后去了美团招聘的官网.当时已经非常佛系了,心想今年美团肯定不缺人吧,要不然怎么动静这么小?而且在官网投简历,还没有内推不是等于买彩票吗?但最后还是抱着试一试的态度投递了简历,而且上面基本啥都没写.
  • 5月13日 简历居然被捞了???!!!老大打电话给我约了面试时间.
  • 5月15日 面了40分钟,差不多也是照着简历来问,还是Java并发三大特性,还问了Activity的启动流程,再有就是我简历上写的那些多进程,MVVM之类的了.所以大概就是把之前面试的内容再复读一次,不过感觉现在复读得已经比较熟练了,面试完后我问面试官对我的评价如何,面试官貌似很高兴,问我简历上面怎么什么都不写,还告诉我我很OK,进的概率还是很大的(内心OS:有点受宠若惊?)
  • 5月15日下午 老大又给我来了电话,约了下一轮的面试时间,还顺便做了一个15分钟的小面试,问了一些技术问题,基本上是一些开源项目的源码,期间问到了Glide,我刚好没看过,吓得我当天晚上赶紧去把Glide的源码扫了一遍,最后老大还加了我的微信.
  • 5月16日 没错,效率高到甚至没有隔天,老大面我,面了一个小时.有一道ACM,直接秒了,然后继续复读,让我详细讲了Handler,Looper,MessageQueue,Message的源码,还有调试追踪内存泄漏,还有计网TCP三次握手,还有操作系统的死锁,还有AsyncTask底层和在不同API等级之间的区别等等,多多少少都答了,不过有一些还是缺页中断了(忘了或者不会QAQ),但是直到最后都没问Glide(QAQ跳来跳去的,我看了一晚上啊),面完以后老大貌似对我挺满意的,当场给我过了,然后帮跟我和大老板约了面试.
  • 5月17日 没错,效率高到甚至没有隔天,大老板面我,聊了大概20到30分钟,大老版问的都是一些比较开放性的问题,比如我们的美团APP启动白屏了让你列出20种可能,最后我从各方面瞎编乱造了大概10种实在编不下去了,感觉是想考察我的发散思维能力,然后问了我为什么想去北京,我就说了我的世界观想去见识更大的世界,感觉大老板挺开心的.面试结束后我然后私下去问老大大老板觉得我怎么样?然后老大告诉我可以等HR电话了(内心OS:说实话有点小开心)
  • 5月20日 跟HR确认了offer,至此我的春招结束.感觉自己性格缺陷挺多的,真的很感谢老大对我的帮助和包容.

1.4 小结

  • 不要轻易放弃和认为春招已经结束了,机会其实还有很多.
  • 说句扎心的话,个人觉得其实觉得在面试中运气真的很重要,如果你和我一样遇到了超nice的领导,就会一路绿灯,要不然的话真的可能会把你面到自闭.
  • 从我个人主观此次面试经历来说,感觉头条并没有外界传闻中的那么效率高缺人.从我个人的这种要强的性格出发,是无法接受这种你告诉我挂了之后不告诉我原因,然后又再打电话约下一轮面试的操作的.
  • PS: 个人猜测应该是原本的候选人把头条给鸽了,然后资格顺位继承到我身上,然后我也鸽了,最后实在没人了,所以才会再打电话给我问我考不考虑.但是人是情感动物啊,而情感往往是建立在反馈的基础上的,如果别人给我的反馈太消极了,那么我给别人的表现也肯定不会积极到哪里去.

2 把自己训练成HashMap和复读机

这次春招给我最大的感触就是,当你觉得自己像复读机能把面试题给复读出来并且对面试官所提的问题能像HashMap一样在常数时间内找到答案的时候,你就离成功很近了.

下面是我在准备面试的时候收集的一些知识点:

2.1 Java

2.1.1 volatile理解,JMM中主存和工作内存到底是啥?和JVM各个部分怎么个对应关系?

参考link

2.1.2 序列化

Serializable在序列化时使用了反射,从而导致GC的频繁调用,参考link

2.1.3 可见性,原子性,有序性(必考)

  • 可见性volatile,一个线程的修改对另外一个线程是马上可见的,
  • 原子性CAS操作,要么都做要么都不做
  • 有序性synchronized通过进入和退出Monitor(观察器)实现,CPU可能会乱序执行指令,如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的.参考link

2.1.4 Java锁机制

java锁机制其实是锁总线,同步关键字和Lock接口的优劣.

2.1.5 Java的常量池?不同String赋值方法,引用是否相等?

字面值是常量,在字节码中使用id索引,equals相等引用不一定相等,Android上String的构造函数会被虚拟机拦截,重定向到StringFactory

2.1.6 HashMap的实现?树化阈值?负载因子?

数组加链表加红黑树,默认负载因子0.75,树化阈值8,这部分比较常考,建议专门准备.(打个小广告OWO,你也可以关注我的专栏,里面有一篇文章分析了HashMap和ArrayMap)

2.1.7 Java实现无锁同步

CAS的实现如AtomicInteger等等

2.1.8 单例模式

  • 双重检查

public class Singleton {

private static volatile Singleton singleton;

private Singleton() {}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
复制代码

  • 反序列化安全,反射安全 枚举级单例,类加载时由JVM保证单例,反序列化不会生成新对象,另外一种反射安全是在构造函数中对单例进行检查如果存在则抛出异常.

2.1.9 锁的条件变量

信号量要与一个锁结合使用,当前线程要先获得这个锁,然后等待与这个锁相关联的信号量,此时该锁会被解锁,其他线程可以抢到这个锁,如果其他线程抢到了这个锁,那他可以通知这个信号量,然后释放该锁,如果此时第一个线程抢到了该锁,那么它将从等待处继续执行(应用场景,将异步回调操作封装为变为同步操作,避免回调地狱)

信号量与锁相比的应用场景不同,锁是服务于共享资源的,而信号量是服务于多个线程间的执行的逻辑顺序的,锁的效率更高一些.

2.1.10 ThreadLocal原理

线程上保存着ThreadLocalMap,每个ThreadLocal使用弱引用包装作为Key存入这个Map里,当线程被回收或者没有其他地方引用ThreadLocal时,ThreadLocal也会被回收进而回收其保存的值

2.1.11 软引用,弱引用,虚引用

  • 软引用内存不够的时候会释放
  • 弱引用GC时释放
  • 虚引用,需要和一个引用队列联系在一起使用,引用了跟没引用一样,主要是用来跟GC做一些交互.

2.1.12 ClassLoader双亲委派机制

简单来说就是先把加载请求转发到父加载器,父加载器失败了,再自己试着加载

2.1.13 GC Roots有这些

通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root:

  • 处于激活状态的线程
  • 栈中的对象
  • JNI栈中的对象
  • JNI中的全局对象
  • 正在被用于同步的各种锁对象
  • JVM自身持有的对象,比如系统类加载器等。

2.1.14 GC算法

名称描述优点缺点
标记-清除算法暂停除了GC线程以外的所有线程,算法分为“标记”和“清除”两个阶段,首先从GC Root开始标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
复制算法将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效复制算法的缺点显而易见,可使用的内存降为原来一半
标记-整理算法标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的,标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高
分代收集算法是java的虚拟机的垃圾回收算法.基于编程中的一个事实,越新的对象的生存期越短,根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建

2.2 Android

2.2.1 Handler、MessageQueue等一套东西讲一下,详细说了下源码。为什么主线程loop不会ANR?

  • Android线程模型就是消息循环,Looper关联MessageQueue,不断尝试从MessageQueue取出Message来消费,这个过程可能会被它自己阻塞.
  • 而Handler最终都调用enqueueMessage(Message,when)入队的,延迟的实现时当前是时间加上延迟时间给消息指定一个执行的时间点,然后在MessageQueue找到插入位置,此时会判断是否需要唤醒线程来消费消息以及更新下次需要暂停的时间.
  • Message知道要发到哪个Handler是因为Message把Handler保存到了target.
  • Message内部使用链表进行回收复用

2.2.2 View事件以及View体系相关知识

建议看《Android开发艺术探索》,这玩意三言两语讲不清楚

2.2.3 Android中使用多线程的方法

  • 裸new一个Thread(失控线程,不推荐)
  • RxJava的调度器(io(优先级比低),密集计算线程(优先级比高,用于执行密集计算任务),安卓主线程, Looper创建(实际上内部也是创建了Handler))
  • Java Executor框架的Executors#newCachedThreadPool(),不会造成资源浪费,60秒没有被使用的线程会被释放
  • AsyncTask,内部使用FutureTask实现,通过Handler将结果转发到主线程,默认的Executor是共用的,如果同时执行多个AsyncTask,就可能需要排队,但是可以手动指定Executor解决这个问题,直接new匿名内部类会保存外部类的引用,可能会导致内存泄漏
  • Android线程模型提供的Handler和HandlerThread
  • 使用IntentService
  • IntentService和Service的区别——没什么区别,其实就是开了个HandlerThread,让它不要在主线程跑耗时任务

2.2.4 RecyclerView复用缓存

建议看一下,这个可能会被问,不过我运气好没被问到.

2.2.5 Activity启动流程

网上有很多相关的文章,可以自己结合源码去看一下,如果能讲个大概的话也是很加分的.

2.2.6 JNI(除非你自己说你会,否则不是很常考)

  • 可避免的内存拷贝,直接传递对象,到C层是一个jobject的指针,可以使用jmethodID和jfiledID访问方法和字段,无需进行内存拷贝,使用直接缓冲区也可以避免内存拷贝.
  • 无法避免的内存拷贝,基本类型数组,无法避免拷贝,因为JVM不信任C层的任何内存操作,特别是字符串操作,因为Java的字符串与C/C++的字符串所使用的数据类型是不一样的C/C++使用char一个字节(1字节=8位)或wchar_t是四字节.而jstring和jchar使用的是UTF-16编码使用双字节.(Unicode是兼容ASCII,但不兼容GBK,需要自己转换)
  • 自己创建的局部引用一定要释放,否则一直持有内存泄漏
  • 非局部引用方法返回后就会失效,除非创建全局引用,jclass是一个jobject,方法外围使用时需要创建全局引用,jmethodID和jfiledID不需要.
  • JNI是通过Java方法映射到C函数实现的,如果使用这种方法,函数必须以C式接口导出(因为C++会对名字做修饰处理),当然也可以在JNI_OnLoad方法中注册.
  • JNIEnv是线程独立的,JNI中使用pthread创建的线程没有JNIEnv,需要AttachCurrentThread来获取JNIEnv,不用时要DetachCurrentThread

2.3专业课

2.3.1 TCP和UDP的根本区别?

数据报,流模式,TCP可靠,包序不对会要求重传,UDP不管,甚至不能保证送到

2.3.2 TCP三次握手

这个被问的几率非常的大,几乎等于必问,建议专门花时间去看.

2.3.3 Http和Https

CA证书,中间机构,公钥加密对称秘钥传回服务端,一个明文一个加密,SSL层,中间人攻击,参考link

2.4 ACM

对于ACM,比较常考链表的题,不常刷算法的同学一定不要对其有抵触心理.

你可能会问为什么要ACM?网上答案说的什么提高代码质量,能够更好地阅读别人的代码这些理由有一定道理,但对于我们去面试的人而言最重要的是ACM是面试官考察你编码能力的最直接的手段,所以不用说这么多废话刷题就够了.

刷题的话,建议去刷leetcode,题号在200以内的,简单和中等难度,不建议刷困难,因为面试的时候基本就不会出,没人愿意在那里等你想一个半个小时的.

在面试官面前现场白板编程时,可以先把思路告诉面试官,写不写得出来是另外一回事,时间复杂度和空间复杂度是怎么来的一定要搞清楚.在编码时也不一定要写出最佳的时间和空间的算法,但推荐你写出代码量最少,思路最清晰的,这样面试官看得舒服,你讲得也舒服.

下面是我在网上收集或者是在实际中遇到过的ACM题,基本上在leetcode上也都有类似的.

2.4.1 数组、链表

  • 链表逆序(头条几乎是必考的)

public ListNode reverseList(ListNode head)
{
if (head == null)
{
return null;
}
if (head.next == null)
{
return head;
}
ListNode prev = null;
ListNode current = head;
while (current != null)
{
ListNode next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
复制代码

  • 删除排序数组中的重复项

public int removeDuplicates(int[] nums)
{
int length = nums.length;
if (length == 0 || length == 1)
{
return length;
}
int size = 1;
int pre = nums[0];
for (int i = 1; i < length; )
{
if (nums[i] == pre)
{
i++;
} else
{
pre = nums[size++] = nums[i++];
}
}
return size;
}
复制代码

  • 数组中找到重复元素
  • n个长为n的有序数组,求最大的n个数
  • 用O(1)的时间复杂度删除单链表中的某个节点 把后一个元素赋值给待删除节点,这样也就相当于是删除了当前元素,只有删除最后一个元素的时间为o(N)平均时间复杂度仍然为O(1)

public void deleteNode(ListNode node) {
ListNode next = node.next;
node.val = next.val;
node.next = next.next;
}
复制代码

  • 删除单链表的倒数第N个元素 两个指针,第一个先走N步第二个再走,时间复杂度为O(N),参考link

public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null)
{
return null;
}
if (head.next == null)
{
return n == 1 ? null : head;
}
int size = 0;
ListNode point = head;
ListNode node = head;
do
{
if (size >= n + 1)
{
point = point.next;
}
node = node.next;
size++;
} while (node != null);
if (size == n)
{
return head.next;
}
node = point.next;
point.next = node == null ? null : node.next;
return head;
}
复制代码

  • 从长序列中找出前K大的数字
  • 用数组实现双头栈

public static class Stack
{

public Stack(int cap)
{
if (cap <= 0)
{
throw new IllegalArgumentException();
}
array = new Object[cap];
left = 0;
right = cap - 1;
}

private Object[] array;
private int left;
private int right;

public void push1(T val)
{
int index = left + 1;
if (index < right)
{
array[index] = val;
}
left = index;
}

@SuppressWarnings(“unchecked”)
public T pop1()
{
if (left > 0)
{
return (T)array[left–];
}
return null;
}

public void push2(T val)
{
int index = right - 1;
if (index > left)
{
array[index] = val;
}
right = index;
}

@SuppressWarnings(“unchecked”)
public T pop2()
{
if (right < array.length)
{
return (T)array[right++];
}
return null;
}
}
复制代码

  • 两个链表求和,返回结果也用链表表示 1 -> 2 -> 3 + 2 -> 3 -> 4 = 3 -> 5 -> 7

public ListNode addTwoNumbers(ListNode node1, ListNode node2)
{
ListNode head = null;
ListNode tail = null;
boolean upAdd = false;
while (!(node1 == null && node2 == null))
{
ListNode midResult = null;
if (node1 != null)
{
midResult = node1;
node1 = node1.next;
}
if (node2 != null)
{
if (midResult == null)
{
midResult = node2;
} else
{
midResult.val += node2.val;
}
node2 = node2.next;
}
if (upAdd)
{
midResult.val += 1;
}
if (midResult.val >= 10)
{
upAdd = true;
midResult.val %= 10;
}
else
{
upAdd = false;
}
if (head == null)
{
head = midResult;
tail = midResult;
} else
{
tail.next = midResult;
tail = midResult;
}
}
if (upAdd)
{
tail.next = new ListNode(1);
}
return head;
}
复制代码

  • 交换链表两两节点

public ListNode swapPairs(ListNode head)
{
if (head == null)
{
return null;
}
if (head.next == null)
{
return head;
}
ListNode current = head;
ListNode after = current.next;
ListNode nextCurrent;
head = after;
do
{
nextCurrent = after.next;
after.next = current;
if (nextCurrent == null)
{
current.next = null;
break;
}
current.next = nextCurrent.next;
after = nextCurrent.next;
if (after == null)
{
current.next = nextCurrent;
break;
}
current = nextCurrent;
} while (true);
return head;
}
复制代码

  • 找出数组中和为给定值的两个元素,如:[1, 2, 3, 4, 5]中找出和为6的两个元素。

public int[] twoSum(int[]mun,int target)
{
Map<Integer, Integer> table = new HashMap<>();
for (int i = 0; i < mun.length; ++i)
{
Integer value = table.get(target - mun[i]);
if (value != null)
{
return new int[]{i, value};
}
table.put(mun[i], i);
}
return null;
}
复制代码

2.4.2 树

  • 二叉树某一层有多少个节点

2.4.3 排序

  • 双向链表排序(这个就比较过分了,遇到了就自求多福吧)

public static void quickSort(Node head, Node tail) {
if (head == null || tail == null || head == tail || head.next == tail) {
return;
}

if (head != tail) {
Node mid = getMid(head, tail);
quickSort(head, mid);
quickSort(mid.next, tail);
}
}

public static Node getMid(Node start, Node end) {
int base = start.value;
while (start != end) {
while(start != end && base <= end.value) {
end = end.pre;
}
start.value = end.value;
while(start != end && base >= start.value) {
start = start.next;
}
end.value = start.value;
}
start.value = base;
return start;
}

/**

  • 使用如内部实现使用双向链表的LinkedList容器实现的快排
    */
    public static void quickSort(List list) {
    if (list == null || list.isEmpty()) {
    return;
    }
    quickSort(list, 0, list.size() - 1);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值