java基础阶段几个必会面试题
1.说出你对面向对象的理解
在我理解,面向对象是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动,所以程序=对象+消息。
面向对象有三大特性,封装、继承和多态。
封装就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化。这样做使得代码的复用性更高。
继承则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类--有父类的行为和属性,也有自己特有的行为和属性。这样做扩展了已存在的代码块,进一步提高了代码的复用性。
如果说封装和继承是为了使代码重用,那么多态则是为了实现接口重用。多态的一大作用就是为了解耦--为了解除父子类继承的耦合度。如果说继承中父子类的关系式IS-A的关系,那么接口和实现类之之间的关系式HAS-A。简单来说,多态就是允许父类引用(或接口)指向子类(或实现类)对象。很多的设计模式都是基于面向对象的多态性设计的。
2.JVM的内存区及其GC算法
参考:https://blog.csdn.net/anjoyandroid/article/details/78609971
元空间:jdk1.8取消了持久代新增了元空间,并将方法区放在元空间中
3.集合框架下的各种接口和实现类有哪些,分别有啥特点
参考:
https://blog.csdn.net/sdgihshdv/article/details/72566485
https://blog.csdn.net/suifeng629/article/details/82179996
https://blog.csdn.net/C18298182575/article/details/87167323
4.string类有啥特点,有哪些常用的API
1.String类对象的相等判断使用equals()方法完成,“==”实现的是地址数值的比较
2.字符串内容一旦声明则不可改变,String类对象内容的改变是依靠引用关系的变更实现的。
3.String类有两种实例化方式,使用直接赋值可以不产生垃圾空间,并且可以自动入池,不要使用构造方法赋值。
一些常见API:
indexOf():检索字符串中某个字符或某段字符的下标。
lastIndexOf():和indexOf类似,不过是查找最后一个出现的位置。
str.lastIndexOf(str,index):从下标index往前查找最后一个出现的位置
substring():返回一个字符串的子字符串
charAt(index):返回下标对应的字符
trim():去掉字符串前后的空格
startsWith()/endsWith():检测字符串是否已制定字符串开头或结尾,返回值是boolean
split()/根据括号内的字符串分离字符串,返回值是一个字符串数组
....
5.stringBuilder和stringBuffer的区别?
运行速度:StringBuilder >StringBuffer >String
线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
为什么StringBuilder是不安全的?
下面是StringBuilder 的append方法源码
char[] value; int count; public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
对于count + =len;不是一个原子操作 两个线程同时执行假设都是 计数器为 10 执行完后 就会变成11 而不是12
什么是原子操作:
简单的例子:
转账,A转给B100,因为停电,导致A转出了100,B却没收到100,所以要把100回滚给A。
原子操作就是多线程下各线程同时执行失败且同时成功,在两个线程下,由于count继承于父类AbstractStringBuilder,当
其中一个线程对coun执行+len后,另一线程取到的count值仍为原来的count值,故+len后和上一个线程得到的结果一样,
故线程不安全
而stringBuffer中源码:
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
当一个线程访问append后会立即上锁,从而另一个线程无法访问append方法,故是线程安全的
在多线程下,stringBuffer下各线程需要频繁的加锁解锁操作,从而需要运行更长的时间,虽然stringBuilder不需要加锁解锁,
但由于线程不安全性,更适用于单线程。
6.线程创建的3种方式,线程阻塞的API有哪些及其之间的区别?
Runnable,Thread,通过 Callable 和 Future 创建线程三种方式。
1. 继承Thread类来创建一个线程, 并实现run方法(线程需要完成的功能); 构建子类对象,start()启动线程
2. 实现Runnable接口来创建一个线程, 实现Runnable,实现run()方法; 将Runnable接口类的对象作为参数传递给Thread类对象, 并调用start()方法;
3. 实现Callable接口来创建一个线程, 先定义一个Callable的实现类, 重写call()方法, call()有返回值; 两种执行方式:
1). 借助FutureTask执行, 创建Callable实现类的对象, 并作为参数传递给FutureTask, FutureTask作为参数传递给Thread类的对象, 并执行start()方法;
2). 借助线程池来执行, 先创建线程池, 然后调用线程池的submit方法, 并将Callable实现列作为参数传入
方法二的好处:
1. 可以将一个Runnable实现类传递给多个线程对象, 适合用多个相同程序代码的编程处理同一个资源
2. Thread类创建线程是采用继承的方式, 而Java中只能单继承, 如果某个子类的需要创建线程只能采用实现Runnable接口或者实现Callable接口的方式.
方法三的好处:
1. 有返回值
2. call()可以抛出异常
3. 运行Callable任务可以得到一个Future兑现,表示异步计算的结果. 它提供了检测计算是否完成的方法(isDone())以等待计算的完成,并检索计算的结果.
线程阻塞api:
sleep()方法;:该方法允许指定以ms为单位的一段时间作为参数, 它使得线程在指定的时间内进入阻塞状态,不能得到CPU时间, 指定时间已过,线程重新进入可执行状态.
suspend()和resume()方法:配套使用, suspend()使得线程进入阻塞状态,且不会自动恢复, 必须将其对应的resume()调用, 才可以使线程进入可执行状态.
yield();:使得线程放弃当前分得的CPU时间, 但是不使线程阻塞, 即线程仍然处于可执行状态;
wait()和notify()方法:配套使用,若wait()有参数,相当于sleep(但可以通过notify强行唤醒), wait()没有参数,相当于suspend(), 需要通过notify唤醒
sleep(0)和sleep(1)和不要sleep的区别:
sleep(0),如果线程调度器的可运行队列中有大于或等于当前线程优先级的就绪线程存在,操作系统会将当前线程从处理器上移除,调度其他优先级高的就绪线程运行;如果可运行队列中的没有就绪线程或所有就绪线程的优先级均低于当前线程优先级,那么当前线程会继续执行,就像没有调用 Sleep(0)一样。
Sleep(1),会引发线程上下文切换:调用线程会从线程调度器的可运行队列中被移除一段时间,这个时间段约等于 timeout 所指定的时间长度。为什么说约等于呢?是因为睡眠时间单位为毫秒,这与系统的时间精度有关。通常情况下,系统的时间精度为 10 ms,那么指定任意少于 10 ms但大于 0 ms 的睡眠时间,均会向上求值为 10 ms。
7.抽象类和接口的区别?有了抽象类为啥还要接口?
1.一类可以实现多个接口但只能继承自一个抽象类,从抽象类派生出的子类同样可以实现接口,从而,我们能得出一个结论:接口是为Java实现多继承而存在的
2.抽象类中可以存在非抽象的方法,可接口不能存在非抽象的方法,并且接口里面的方法只是一个声明,必须用 public abstract来修饰,没有具体的实现
3.抽象方法中的成员变量可以被不同的修饰符修饰,而接口中的成员变量默认都是静态常量
4.抽象类是对对象进行的抽象,而接口是一种行为规范,这一点是比较重要的.
(所以为什么有了接口还要有抽象类)
8.冒泡排序,选择排序,快速排序(了解)
冒泡排序:什么是冒泡?比如说水底随机产生一些气泡,一起往上冒泡,越轻的气泡往上冒的越快
具体:12 34 10 78 67
如果从小到大排序:先将67和78比较,67比78小,依次往前比较,小的放前面,打的放后面,以此为一轮排序,然后再将新的数组重复上述过程,共需要n轮排序(n为元素个数);
选择排序:从一个数组里选出最小的元素放在数组第一位并交换位置,然后再将去掉第一位的数组找出最小元素并放在这个新数组第一位,
重复此操作。
12 34 10 78 67
第一轮:10| 34 12 78 67
第二轮:10 12| 34 78 67
第三轮:10 12 34| 78 67
第四轮:10 12 34 67| 78
排序结束
快速排序:基于基数排序。先取任意一基数,一般为数组第一个元素(由于当第一个元素为最小值(最大值)时会使排序出现错误,故有时候也取中间的元素),然后将比基数小的数作为一个数组,比基数大的数作为一个数组,再将新的两个数组分别递归排序。
通过基数分成两个数组的过程:12 34 10 78 67 8 假设数组为arr
取一基数temp=12 取low=0(数组第一位),high=5(数组最后一位)
第一轮:第一步:先从后往前比较:arr[high]=8<12=temp,结束这一步操作,high与low不变。如果这里arr[high]>12,则令high-1得到新的high将arr[high]与temp比较,依此下去直到arr[high]<temp,这种情况high发生改变,low不变。
第二步:再从前往后将arr[low]与temp比较,原理与第一步相同,因为arr[1]>temp,此时low=1,结束这一步操作。
第三步:交换arr[low]与arr[high]的值
第一轮结果:12 8 10 78 67 34(low=1,high=5)
第二轮:与第一轮一样,第一步,从arr[high]往前,直到arr[2]=10<12,此时high=2,结束这一步
第二步,从arr[low]往后,12,8,10都不大于12,到这里的时候,因为low=2=high,故比较,得到索引index=low=high=2
第二轮结果:12 8 10 78 67 34
因为index得到了值3,将arr[index]作为分界点将最后一轮结果数组[12 8 10 78 67 34]分为两个数组[12 8 10]和[78 67 34]
将新的到的两个数组重复进行上述操作
[12 8 10]->因为12为最大值,故取中间值8->[8]和[10 12]->[8]、[10]、[12]
[78 67 34]->取67,->[34]、[67 78]->[34]、[67]、[78]->[8]、[10]、[12]、[34]、[67]、[78]
(拓展:希尔排序、插入排序)
9.什么是死锁?如何避免死锁
死锁的定义:所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
产生原因:
1) 系统资源的竞争
通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
2) 进程推进顺序非法
进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
四个产生死锁的条件:
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。
避免死锁:
1.加锁顺序(线程按照一定的顺序加锁)
2.加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
3.死锁检测
参考:https://blog.csdn.net/ls5718/article/details/51896159