因为这不是一套笔试题,而是我正在找工作的朋友们零零碎碎提供的,所以叫做杂记(主要来源于一个对我帮助贼大的朋友还有一些狗群友)。
背景的话:两到三年的经验,属于中级开发,然后坐标上海,广东。
我都是听到说面试问了就记下来,今天正好闲着所以整理了一下,顺序也是随缘写的。
短信限制功能怎么做?(这个是面试题,问答形式的)
首先,大多数时候现在一个app发送短信是很正常的功能,起码注册百分之九十都会有这个功能。但是如果不做限制,一个多线程跑下去平台里有多少短信费用都能刷没了(这句话是我朋友说的)。所以这个短信限制要怎么做呢?
这个问题问的好,其实我还真做过这块的功能,之前也写了一个帖子具体实现每个手机号每天只发送20条短信的限制。java发送短信验证码限制
这个用到了redis缓存,然后当一个号码一天发送短信到20次则直接报错。
不过!上述解决办法还有一个bug:那就是如果人家随机生成手机号码呢?这样多线程跑着,中国现有的手机号这么多,还是有多少钱都不够跑的!所以如果想继续维护安全,还有一个设置:
如果是手机端,是肯定有设备码的,这个设备码对于一个手机唯一的。
如果是web端,一个电脑有一个IP和MAC。相比于ip,MAC更加可靠。
大不了就是调用短信接口的时候获取设备标识(设备码和MAC),然后设置每个每天只能发送多少多少次短信。
问:如果是随机生成的虚拟ip,设备码,mac码呢?
答:那就没救了,等着被攻击吧,毕竟如果设备码虚拟生成烂大街,美团早就黄了。判断新用户就是根据手机号和设备码的(笑脸)。
String a = "hello";String b = "he"+new String("llo");System.out.println(a==b);
这个其实是很基础的题,但是我心目中那个贼厉害的朋友居然答错了(兴奋ing),这个输出结果肯定是false啦。
要从String说起,在java中String一直是个很特殊的存在,例如a这种直接等于的,是存放在常量池中的,而不是堆中,但是b中的new String("llo"),就是存放在堆中的,同样由字符串拼接的b相当于创建了一个新对象,所以也是存在堆中。就这样一个常量区中的字符串和一个堆中的字符串,怎么可能地址一样?所以自然结果是false啦。
然后这个题稍微变一下,变成String a = "hello";String b = "he"+"llo"; System.out.println(a==b);
这样的话,结果是true,源于java的优化(具体哪里优化我忘记了)反正字符串拼接的时候两边都是常量会直接运算结果。所以"he"+"llo"直接就变成了"hello",于是两个常量池中的hello自然地址相同。
case问题
这个题我觉得是非常简单的,但是因为有人说遇到了,所以也还是写了出来(因为这个需要代码判断,所以直接贴代码题目)
public static int getValue(int i) {
int result = 0;
switch (i) {
case 1:
result = result+i;
case 2:
result = result+i*2;
case 3:
result = result+i*3;
}
return result;
}
如上代码片段,i为2时结果是多少?
其实这个考的就是case选中后继续往下走的问题,正常i为2,进入到第二个分支,走完继续走第三个分支,出来的结果是10.只要知道这个机制就可以答出来了。
其实我们在用switch-case时,通常要搭配一个关键字break。在具体是case下面配上这个字,选择分支走完自动退出,不再继续往下执行。
上面的题目因为没有这个关键字所以顺序往下执行。
list问题
list问题:一个list初始化为{5,3,1},执行以下代码后结果是多少?
list.add(6);
list.add(0,4);
list.remove(1);
反正从良心的角度出发,我觉得这个题应该也是很基础的,然后我居然不会!!对的,第一眼看到题有点蒙,尤其是第二个代码句,不过还好面试的不是我,看到题目的时候正好闲着就.了一下list,然后才知道add如果有两个参数,第一个参数是下标。
然后list的remove方法,如果是integer类型 参数,也会被认为是下标(我不知道这个题是真出的好还是碰巧,list中元素是int类型,这个remove(1)如果你误解为是移除1这个元素就大错特错了,因为实际代码跑了一遍,这里的remove被认为是移除下标为1的元素)
三行代码,两行知识盲区,深感自己基础不足。
闲话少叙,说正经的,这个答案是{4,3,1,6},我觉得我说到这已经解释的很明白啦,再有问题的自己跑代码吧。
排序
其实这个问题真的,腻腻歪歪由不可避免。当年被要求手写冒泡还有点小情绪,现在看来是正常操作了。
不过既然问的频率这么高,所以我觉得还是整理几种排序的写法,毕竟多会点总没错的。
冒泡排序
这个是最基本的操作,原理也稍微了解就能明白。主要核心就是双循环,两数互换(追尾且绕圈),这个是我当时学习的时候老师的讲解,觉得蛮形象的记到现在。
最简版冒泡排序:
public static void BubbleSort(int [] arr){
int temp;//临时变量
for(int i=0; ii; j--){
if(arr[j] < arr[j-1]){
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
}
}
这个从大到小或者从小到大排序在于比较时候arr[j] 和 arr[j-1]的关系,想要大到小就是大于,这样后一个比前一个大则会交换,否则不会交换。大的往前挪,小的往后,最后结果才是大到小的排序。
同理小到大就如上文的代码一样小于,大的往后挪,小的往前,最终才能达到排序的目的。
冒泡优化:
public static void BubbleSort1(int [] arr){
int temp;//临时变量
boolean flag;//是否交换的标志
for(int i=0; ii; j--){
if(arr[j] < arr[j-1]){
temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
flag = true;
}
}
if(!flag) break;
}
}
这个就是设置一个flag,如果已经排序完毕,但是遍历没走完,正常的冒泡还是会继续挨个比对。而使用这个flag可以保证在已经排序完毕后立刻跳出循环,不用做无畏的比对。
选择排序
基本思想就是在一个数组中,找到最小的元素,放到第一个元素。然后在除了第一个元素其余中选出最小的,放到第二个元素位置。然后在剩下的元素中选择最小的,放在第三个元素的位置,以此类推。
选择排序代码实现:
public static void select_sort(int array[],int lenth){
for(int i=0;i
剩下还有归并,快排等,不过我其实也不咋熟悉,这里就不献丑了。
String StringBuilder和StringBuffer的区别
好吧,又涉及到我知识盲区了(基础真的渣)。
以前我只知道String是定长的,SB是变长的,如果经常更改的字符串用SB。但是这两个StringBuilder和StringBuffer就不是很清晰了。
因为看到这个问题,所以就直接看了下源码:
其实看到这就很显而易见了,二者的区别就是线程安全问题。StringBuilder是非线程安全的,StringBuffer是线程安全的。
然后一个我看来比较奇怪的点:StringBuffer比StringBuilder要出现的早。
StringBuilder是jdk1.5才出现的。一个非线程安全的对象后出现,并且占据主流。
原因就是因为非线程安全,所以执行速度快,效率高。
抽象类和接口的区别
这个有点太基础了,属于每个能都能说点,但是准不准确全不全面又不好定义。
下面是我看到的说的不错的几点,说的很深刻,而且看着高大上。
语法层面上的区别:
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口;
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
- 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
- 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
- 抽象类的抽象方法可以是public,protected,default类型,而接口的方法只能是public。
设计层面上的区别:
- 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。 - 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
for循环里的执行顺序(最小白的一个问题)
for循环的表达式一般如下:
for(表达式1;表达式2;表达式3){
表达式4;
}
执行顺序124324324324324...循环下去,知道2条件不符则退出循环。
线程锁问题
下面哪些操作会使线程释放锁资源?
sleep(),wait(),join(),yield()。
这个题怎么说呢,应试题吧,反正我觉得我项目中使用的不多,但是既然有人问,还是要学学的。
sleep()的作用是是当前线程陷入阻塞状态,并不会释放线程锁。关于这个sleep是否会让出cpu,一直是个争论点(ps:这是一个简友给我留言指出的问题)。
从反证法的角度来讲,循环跑线程然后sleep,并不会使cpu使用率变高,所以由此可见sleep是不占用cpu的。
但是另一种说法:
反正具体到底占用不占用我至今没有找到很权威的解释和说法。起码这道题里知道肯定不会释放线程锁就ok了!
wait()这个方法差不多就是专门用来释放锁的了我觉得,当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时释放了锁对象,让出cpu。
当一个线程执行了wait方法后,必须持有同一个锁的线程调用notify或者notifyAll方法才能唤醒。
注意:当前线程没有拥有锁会报错,唤醒的线程没有这个锁也会报错,线程不是synchronized的调用wait也会报错。反正这个是object的方法,感觉挺不常用的,我记得是不推荐使用。
yield()这个方法是暂时让出cpu,而不会释放锁。同时这个暂时让出也是有公平竞争的,就是让cpu自己选把时间给谁,极有可能cpu下次还选择这个线程。
join()最后说说join(),因为之前看了join源码,发现在join方法内,也调用了wait方法。所以这个释放锁让出cpu好像没有什么疑问了吧?
然后这个会释放锁的方法:join(),wait()。
ArrayList,LinkedList,HashMap,HashTable
这几个集合真的真的是逃不过。我还觉得这次的题目说的不够全,起码没再把什么vector,ConcurrentHashMap什么的都放上。这个题目据说是谈谈对他们的了解?
我个人感觉反正是从实现方式,线程安全,增删效率,查找效率,有序性几方面来说吧。
然后这个问题我真的觉得可说性太多,而且也不是几句话甚至几十句话能说完的,主要点就是上面几个,拆开来都能长篇大论。
我就不打字了,反正大家知道这种问题面试贼容易问到就行了。
初始化问题
这些面试题收集了快一个星期吧,果然问啥的都有,现在自己看看还觉得挺全面。这个问题是群友重点强调的一个问题。因为他就因为这道题错了直接被pass的,说是基础不扎实。
题目:
题目代码:
public class P {
public static int a = 123;
static {
System.out.println("P is init");
}
}
public class S extends P{
static {
System.out.println("S is init");
}
}
public class Test {
public static void main(String[] args) {
System.out.println(S.a);
}
}
现在输出S.a.结果是什么?
我反正当时想也没相对,估计我去面试也会这一题就pass
不卖关子了,结果是P is init 123
我觉得按照类初始化的顺序:父静态->子静态->父构造>子构造来想,怎么也想不出为什么会错啊?
后来查了资料才发现错误原因:
首先,JVM五种初始化的场景:
- new 关键字实例化对象
- 实用reflect对类反射调用的时候。
- 初始化一个类,发现其父类没有被初始化的时候会先初始化其父类
- 虚拟机启动时,包含main方法的执行主类,虚拟机会先初始化这个类
- 实用jdk1.7语言动态支持的时候一些情况。
除了这五种情况,其他所有的引用类的方式都不会触发初始化,称为被动引用。有三个典型的例子:
- 子类引用父类的静态字段,不会导致子类初始化。
- 通过数组引用来引用类,不会触发此类的初始化
-
常量在编译阶段会被存入调用类的常量池中,本质上并没有引用到定义常量类类,所以自然不会触发定义常量的类的初始化。
首先,咱们这道题的情况就是第一种情况。然後出于求真的心态,我也试了下后两种情况:
至此,这次的面试题总结到此为止,如果稍微有帮到你,麻烦点个喜欢点个关注支持下哟。也祝大家找工作顺顺利利!全文手打,希望帮到你~