在本文的开头,先分享一下自己的春招经历吧:
各位掘友大家好,我是练习Android快一年的双非二本大三学生,喜欢看源码,逛掘金,写技术文章......
好了好,不开玩笑了OWO,今年春招投了许多简历的,但是被捞的只有阿里,头条和美团,一路下来个人认为在春招中运气>性格>三观>技术.
这次春招给我最大的感触就是,当你觉得自己像复读机并且对面试官所提的问题能像HashMap一样找到答案的时候,你就离成功很近了.
下面是我在准备面试的时候收集的一些知识点:
volatile
理解,JMM中主存和工作内存到底是啥?和JVM各个部分怎么个对应关系? 参考link
Serializable
在序列化时使用了反射,从而导致GC的频繁调用,参考link
volatile
,一个线程的修改对另外一个线程是马上可见的, CAS
操作,要么都做要么都不做 synchronized
通过进入和退出 Monitor
(观察器)实现, CPU
可能会乱序执行指令,如果在本线程内观察,所有操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的.参考link java锁机制其实是锁总线
字面值是常量,在字节码中使用id索引,equals相等引用不一定相等,Android上String的构造函数会被虚拟机拦截,重定向到StringFactory
数组加链表加红黑树,默认负载因子 0.75
,树化阈值 8
,这部分比较常考,建议专门准备.(打个小广告OWO,你也可以关注我的专栏,里面有一篇文章分析了HashMap和ArrayMap)
CAS的实现如 AtomicInteger
等等
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; } } 复制代码
信号量要与一个锁结合使用,当前线程要先获得这个锁,然后等待与这个锁相关联的信号量,此时该锁会被解锁,其他线程可以抢到这个锁,如果其他线程抢到了这个锁,那他可以通知这个信号量,然后释放该锁,如果此时第一个线程抢到了该锁,那么它将从等待处继续执行(应用场景,将异步回调操作封装为变为同步操作,避免回调地狱)
信号量与锁相比的应用场景不同,锁是服务于共享资源的,而信号量是服务于多个线程间的执行的逻辑顺序的,锁的效率更高一些.
线程上保存着ThreadLocalMap,每个ThreadLocal使用弱引用包装作为Key存入这个Map里,当线程被回收或者没有其他地方引用ThreadLocal时,ThreadLocal也会被回收进而回收其保存的值
ClassLoader
双亲委派机制 简单来说就是先把加载请求转发到父加载器,父加载器失败了,再自己试着加载
通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root:
名称 | 描述 | 优点 | 缺点 |
---|---|---|---|
标记-清除算法 | 暂停除了GC线程以外的所有线程,算法分为“标记”和“清除”两个阶段,首先从GC Root开始标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。 | 标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作 | |
复制算法 | 将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉 | 这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效 | 复制算法的缺点显而易见,可使用的内存降为原来一半 |
标记-整理算法 | 标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的,标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。 | 标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高 | |
分代收集算法 | 是java的虚拟机的垃圾回收算法.基于编程中的一个事实,越新的对象的生存期越短,根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建 |
建议看《Android开发艺术探索》,这玩意三言两语讲不清楚
建议看一下,这个可能会被问,不过我运气好没被问到.
数据报,流模式,TCP可靠,包序不对会要求重传,UDP不管,甚至不能保证送到
这个被问的几率非常的大,几乎等于必问,建议专门花时间去看.
CA证书,中间机构,公钥加密对称秘钥传回服务端,一个明文一个加密,SSL层,中间人攻击,参考link
对于ACM,比较常考链表的题,不常刷算法的同学一定不要对其有抵触心理.
你可能会问为什么要ACM?网上答案说的什么提高代码质量,能够更好地阅读别人的代码这些理由有一定道理,但对于我们去面试的人而言最重要的是ACM是面试官考察你编码能力的最直接的手段,所以不用说这么多废话刷题就够了.
刷题的话,建议去刷leetcode,题号在200以内的,简单和中等难度,不建议刷困难,因为面试的时候基本就不会出,没人愿意在那里等你想一个半个小时的.
在面试官面前现场白板编程时,可以先把思路告诉面试官,写不写得出来是另外一回事,时间复杂度和空间复杂度是怎么来的一定要搞清楚.在编码时也不一定要写出最佳的时间和空间的算法,但推荐你写出代码量最少,思路最清晰的,这样面试官看得舒服,你讲得也舒服.
下面是我在网上收集或者是在实际中遇到过的ACM题,基本上在leetcode上也都有类似的.
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; } 复制代码
public void deleteNode(ListNode node) { ListNode next = node.next; node.val = next.val; node.next = next.next; } 复制代码
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; } 复制代码
public static class Stack<T> { 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; } } 复制代码
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; } 复制代码
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; } 复制代码
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<Integer> list) { if (list == null || list.isEmpty()) { return; } quickSort(list, 0, list.size() - 1); } private static void quickSort(List<Integer> list, int i, int j) { if (i < j) { int mid = partition(list, i, j); partition(list, i, mid); partition(list,mid + 1, j); } } private static int partition(List<Integer> list, int i, int j) { int baseVal = list.get(i); while (i < j) { while (i < j && baseVal <= list.get(j)) { j--; } list.set(i, list.get(j)); while (i < j && baseVal >= list.get(i)) { i++; } list.set(j, list.get(i)); } list.set(i, baseVal); return i; } 复制代码
不要答TCP,答RTMP实时传输协议,RTMP在Github也有很多开源实现,建议面这方面的同学可以去了解一下.
这一块比较抽象,根据你自己的项目来,着重讲你比较熟悉,有把握的模块,一般面试官都会从中抽取一些问题来向你提问.
不要问诸如:
这种问题一点价值都没有,因为你即使问了也不能从他那里获得额外的信息,也不能够影响他对你的判断,要问就要问面试官对你的感受与评价,还要体现出你想要加入的心情以及你问题的深度.
往往有很多同学明明觉得自己已经准备得很好了已经很复读了,可最后还是不没拿到offer,那么你可能需要考虑以下的问题了.
就像我一开始说的,春招是运气>性格>三观>技术的,项目与基础固然很重要,但是想在短短的数小时能完全掌握一个人所有的基本情况实在是太难了,面试官看到的往往只是你的冰山一角,所以在面试的时候,你可能还要在意以下的问题:
最后的最后还是惯例啦,如果喜欢我的文章别忘了给我点个赞,拜托了这对我来说真的很重要.