java基础阶段几个必会面试题
在我理解,面向对象是向现实世界模型的自然延伸,这是一种“万物皆对象”的编程思想。在现实生活中的任何物体都可以归为一类事物,而每一个个体都是一类事物的实例。面向对象的编程是以对象为中心,以消息为驱动,所以程序=对象+消息。
面向对象有三大特性,封装、继承和多态。
封装就是将一类事物的属性和行为抽象成一个类,使其属性私有化,行为公开化,提高了数据的隐秘性的同时,使代码模块化。这样做使得代码的复用性更高。
继承则是进一步将一类事物共有的属性和行为抽象成一个父类,而每一个子类是一个特殊的父类--有父类的行为和属性,也有自己特有的行为和属性。这样做扩展了已存在的代码块,进一步提高了代码的复用性。
如果说封装和继承是为了使代码重用,那么多态则是为了实现接口重用。多态的一大作用就是为了解耦--为了解除父子类继承的耦合度。如果说继承中父子类的关系式IS-A的关系,那么接口和实现类之之间的关系式HAS-A。简单来说,多态就是允许父类引用(或接口)指向子类(或实现类)对象。很多的设计模式都是基于面向对象的多态性设计的。
2.JVM的内存区及其GC算法
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,结束这一步