1、谈谈对面向对象思想的理解
首先,谈谈“面向过程”vs“面向对象”
我觉得这两者是思考角度的差异,面向过程更多是以“执行者”的角度来思考问题,而面向对象更多是以“组织者”的角度来思考问题,举个例子,比如我要产生一个0-10之间的随机数,如果以“面向过程”的思维,那我更多是关注如何去设计一个算法,然后保证比较均衡产生0-10的随机数,而面向对象的思维会更多关注,我找谁来帮我们做这件事,比如Random类,调用其中提供的方法即可。
所以,面向对象的思维更多的是考虑如何去选择合适的工具,然后组织到一起干一件事。
好比一个导演,要拍一场电影,那么首先要有男猪脚和女猪脚,然后还有其他等等,最后把这些资源组织起来,拍成一场电影。
再说回我们的程序世界,这个组织者的思维无处不在,比如,我们要开发项目,以三层架构的模式来开发,那么这个时候,我们不需要重复造轮子,只需要选择市面上主流的框架即可,比如SpringMVC,Spring,MyBatis,这些都是各层的主流框架。
2、JDK、JRE和JVM有什么区别?
JDK:Java Development Kit,Java开发工具包,提供了Java的开发环境和运行环境。
包含了编译Java源文件的编译器Javac,还有调试和分析的工具。
JRE:Java Runtime Environment,Java运行环境,包含Java虚拟机及一些基础类库
JVM:Java Virtual Machine,Java虚拟机,提供执行字节码文件的能力
所以,如果只是运行Java程序,只需要安装JRE即可。
另外注意,JVM是实现Java跨平台的核心,但JVM本身并不是跨平台的,不同的平台需要安装不同的JVM
3、Java的基本数据类型
boolean,byte,char,short,int,long,float,double
注意:String是引用类型
4、==和equals的区别
== 比较的是值
比较基本的数据类型,比较的是数值
比较引用类型:比较引用指向的值(地址)
equals
默认比较也是地址,因为这个方法的最初定义在Object上,默认的实现就是比较地址
自定义的类,如果需要比较的是内容,那么就要学String,重写equals方法
代码案例:测试以下的每道题,你是否能够正确得到答案?
String s1 = new String("zs");
String s2 = new String("zs");
System.out.println(s1 == s2);//false
String s3 = "zs";
String s4 = "zs";
System.out.println(s3 == s4);//true
System.out.println(s3 == s1);//false
String s5 = "zszs";
String s6 = s3+s4;
System.out.println(s5 == s6);//false
final String s7 = "zs";
final String s8 = "zs";
String s9 = s7+s8;
System.out.println(s5 == s9);//true
final String s10 = s3+s4;
System.out.println(s5 == s10);//false
5、final的作用
final修饰类,表示类不可变,不可继承。比如,String,不可变性
final修饰方法,表示该方法不可重写。比如模板方法,可以固定我们的算法
final修饰变量,这个变量就是常量
注意:
修饰的是基本数据类型,这个值本身不能修改
修饰的是引用类型,引用的指向不能修改
比如下面的代码是可以的
final Student student = new Student(1,"Andy");
student.setAge(18);//注意,这个是可以的!
6、String s = "java"与String s = new String("java")
String s = "java";
String s = new String("java");
这两者的内存分配方式是不一样的。
第一种方式,JVM会将其分配到常量池,而第二种方式是分配到堆内存
7、String,StringBuffer,StringBuilder区别
String 跟其他两个类的区别是
String是final类型,每次声明的都是不可变的对象,
所以每次操作都会产生新的String对象,然后将指针指向新的String对象。
StringBuffer,StringBuilder都是在原有对象上进行操作
所以,如果需要经常改变字符串内容,则建议采用这两者。
StringBuffer vs StringBuilder
前者是线程安全的,后者是线程不安全的。
线程不安全性能更高,所以在开发中,优先采用StringBuilder.
StringBuilder > StringBuffer > String
8、接口和抽象类的区别
这个问题,要分JDK版本来区分回答:
- JDK1.8之前:
- 语法:
- 抽象类:方法可以有抽象的,也可以有非抽象, 有构造器接口:方法都是抽象,属性都是常量,默认有public static final修饰
- 设计:
- 抽象类:同一类事物的抽取,比如针对Dao层操作的封装,如,BaseDao,BaseServiceImpl接口:通常更像是一种标准的制定,定制系统之间对接的标准例子:1,单体项目,分层开发,interface作为各层之间的纽带,在controller中注入IUserService,在Service注入IUserDao2,分布式项目,面向服务的开发,抽取服务service,这个时候,就会产生服务的提供者和服务的消费者两个角色这两个角色之间的纽带,依然是接口
- JDK1.8之后:
- 接口里面可以有实现的方法,注意要在方法的声明上加上default或者static
最后区分几个概念:
- 多继承,多重继承,多实现
- 多重继承:A->B->C(爷孙三代的关系)多实现:Person implements IRunable,IEatable(符合多项国际化标准)多继承:接口可以多继承,类只支持单继承
9、算法题-求N的阶乘(手写)
这道算法题一般考查的递归的编程技能,那么我们回顾下递归程序的特点:
1,什么是递归?
递归,就是方法内部调用方法自身
递归的注意事项:
找到规律,编写递归公式
找到出口(边界值),让递归有结束边界
注意:如果递归太多层,或者没有正确结束递归,则会出现“栈内存溢出Error”!
问题:为什么会出现栈内存溢出,而不是堆内存溢出?
2,这道题该怎么写?
规律:N !=(n-1)!*n;
出口:n == 1或n == 0 return 1;
public static int getResult(int n){
if(n<0){
throw new ValidateException("非法参数");
}
if(n==1 || n==0){
return 1;
}
return getResult(n-1)*n;
}
10、算法题-求解斐波那切数列的第N个数是几?(手写)
如何实现递归求斐波那切数列第N个数字的值(传说中的不死神兔就是这个问题)
数字的规律:1,1,2,3,5,8,13,21....
所以,我们可以分析编写如下:
/**
* 规律:每个数等于前两个数之和
* 出口:第1项和第2项都等于1
*/
public static int getFeiBo(int n) {
if (n < 0){
return -1;
}
if (n == 1 || n == 2) {
return 1;
} else {
return getFeiBo(n - 1) + getFeiBo(n - 2);
}
}
11、什么是向上转型?什么是向下转型?
这道题目一般出现在(笔试-选择题)
举例说明即可:
向上转型:Person person = new Student(); 安全的
向下转型:Teacher teacher = (Teacher)person; 不安全的
12、Int和Integer的区别(重点)
1,来,先来一道考题,你看做对了吗?
Integer i1 = new Integer(12);
Integer i2 = new Integer(12);
System.out.println(i1 == i2);//false
Integer i3 = 126;
Integer i4 = 126;
int i5 = 126;
System.out.println(i3 == i4);//true
System.out.println(i3 == i5);//true
Integer i6 = 128;
Integer i7 = 128;
int i8 = 128;
System.out.println(i6 == i7);//false
System.out.println(i6 == i8);//true
以上这些输出的答案是什么?true or false? why?
你可以自己先思考,再看后面的答案分析。
答案揭晓
分情况来比较
- 都定义为Integer的比较:
new:一旦new,就是开辟一块新内存,结果肯定是false
不new:
看范围
Integer做了缓存,-128至127,当你取值在这个范围的时候,会采用缓存的对象,所以会相等
当不在这个范围,内部创建新的对象,此时不相等
- Integer和int的比较:
实际比较的是数值,Integer会做拆箱的动作,来跟基本数据类型做比较
此时跟是否在缓存范围内或是否new都没关系
源码分析:
当我们写Integer i = 126,实际上做了自动装箱:Integer i = Integer.valueOf(126);
分析这段源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//IntegerCache是Integer的内部类
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
13、方法的重写和重载的区别
一般出现在(笔试题-选择题),下面我们说下重点
- 重载:发生在一个类里面,方法名相同,参数列表不同(混淆点:跟返回类型没关系)
以下不构成重载
public double add(int a,int b)
public int add(int a,int b)
- 重写:发生在父类子类之间的,方法名相同,参数列表相同
14、算法题-冒泡排序
冒泡排序原理:
- 比较相邻的两个元素,如果前者大于后者则交换位置;
- 这样对数组第0个数据到N-1个数据进行遍历比较一次后,最大的数据会移动到最后一位。
- N=N-1,如果N=0则排序完成;
public void bubbleSort(int[] array){
if(array.length <= 1){
return;
}
for(int i=0;i<array.length;i++){
for(int j=0;j<array.length-i-1;j++){
if(array[j] > array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
关于上面的优化思考
其实,当某次冒泡操作没有数据交换时,说明已经达到了完全有序,
不用再继续后续的冒泡操作。
public void bubbleSort(int[] array){
if(array.length <= 1){
return;
}
//重复n次冒泡
for(int i=0;i<array.length;i++){
//是否可以提交退出冒泡的标记
boolean flag = false;
//相邻之间两两比较,并且每次减少一位参与比较
for(int j=0;j<array.length-i-1;j++){
if(array[j] > array[j+1]){
//需要交换
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
//
flag = true;//有数据交换,不能提前退出
}
}
if(!flag){
//没有数据交换,提前退出冒泡比较
break;
}
}
}
15、List和Set的区别
- List(有序,可重复)
- Set(无序,不可重复)
16、谈谈ArrayList和LinkedList的区别
1,底层数据结构的差异
ArrayList,数组,连续一块内存空间
LinkedList,双向链表,不是连续的内存空间
2,一个常规的结论
虽然不严谨,但也可以应付很多面试了
ArrayList,查找快,因为是连续的内存空间,方便寻址,但删除,插入慢,因为需要发生数据迁移
LinkedList,查找慢,因为需要通过指针一个个寻找,但删除,插入块,因为只要改变前后节点的指针指向即可。
3,ArrayList细节分析
1,增加
-
- 添加到末尾,正常不需要做特别的处理,除非现有的数组空间不够了,需要扩容
- 数组初始化容量多大?10,当你知道需要存储多少数据时,建议在创建的时候,直接设置初始化大小
- 怎么扩容?
- 当发现容量不够之后,就进行扩容按原先数组容量的1.5倍进行扩容,位运算,下面是关键的源码
- 添加到末尾,正常不需要做特别的处理,除非现有的数组空间不够了,需要扩容
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
- 再将原先数组的元素复制到新数组,Arrays
elementData = Arrays.copyOf(elementData, newCapacity)
- 添加到其他位置,这个时候需要做整体的搬迁
- 2,删除
- 删除末尾,并不需要迁移删除其他的位置,这个时候也需要搬迁
- 3,修改
- 修改之前,必须先定位定位-查找-ArrayList(数组是一段连续的内存空间,定位会特别快)
- 4,查找
- 如上所述
4,LinkedList细节分析
1,提供了的两个引用(first,last)
2,增加
添加到末尾,创建一个新的节点,将之前的last节点设置为新节点的pre,新节点设置为last
我们看下源码:
void linkLast(E e) {
//获取到最后一个节点
final Node<E> l = last;
//构建一个新节点,将当前的last作为这个新节点的pre
final Node<E> newNode = new Node<>(l, e, null);
//把last指向新节点
last = newNode;
//如果原先没有最后一个节点
if (l == null)
//将first指向新节点
first = newNode;
else
//否则,将原先的last的next指向新节点
l.next = newNode;
size++;
modCount++;
}
Node节点的定义:内部类
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
添加到其他位置,这个时候,就需要调整前后节点的引用指向
3,如何去定义一个双向链表的节点,如上述的源码所示
4,修改
修改最后一个节点或者第一个节点,那么就很快(first,last)
修改其他位置,如果是按坐标来定位节点,则会按照二分查找法,源码如下:
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
5,一个思考题,假如我们可以确定要存储1000个元素,那么采用ArrayList和LinkedList,
哪个更耗内存,为什么?
6,LinkedList,要实现在A和B之间插入C,该如何实现,编写伪代码即可
17、如何在双向链表A和B之间插入C?
可以使用伪代码的方式来实现,你的答案是什么?
假设我们定位到了A节点,那么A.next就是B节点,这个是前提。
你的答案是?可以思考过后,再看答案
C.pre = A;
C.next = A.next;
A.next.pre = C;
A.next = C;
18、谈谈HashSet的存储原理
HashSet的存储原理或者工作原理,主要是从如何保证唯一性来说起。
这里面主要有3个问题,需要回答?
第一,为什么要采用Hash算法?有什么优势,解决了什么问题?
第二,所谓哈希表是一张什么表?
第三,HashSet如何保证保存对象的唯一性?会经历一个什么样的运算过程?
大家可以先思考,晚些再补充答案!
首先,我们要明确一点,HashSet底层采用的是HashMap来实现存储,其值作为HashMap的key
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
具体关于hashmap的细节再说
第一,为什么要采用Hash算法?有什么优势,解决了什么问题?
解决的问题是唯一性
存储数据,底层采用的是数组
当我们往数组放数据的时候,你如何判断是否唯一?
可以采用遍历的方式,逐个比较,但是这种效率低,尤其是数据很多的情况下
所以,为了解决这个效率低的问题,我们采用新的方式
采用hash算法,通过计算存储对象的hashcode,然后再跟数组长度-1做位运算,得到我们要存储在数组的哪个下标下,如果此时计算的位置没有其他元素,直接存储,不用比较。
此处,我们只会用到hashCode
但是随着元素的不断添加,就可能出现“哈希冲突”,不同的对象计算出来的hash值是相同的,这个时候,我们就需要比较,才需要用到equals方法
如果equals相同,则不插入,不相等,则形成链表
第二,所谓哈希表是一张什么表?
本质是一个数组,而且数组的元素是链表
JDK1.7的版本实现
JDK1.8做了优化
随着元素不断添加,链表可能会越来越长,会优化红黑树
19、谈谈LinkedHashMap和HashMap的区别(重点)
此处,我们好好谈谈HashMap
主要关注几个点:
1,初始化大小是16,如果事先知道数据量的大小,建议修改默认初始化大小。 减少扩容次数,提高性能 ,这是我一直会强调的点
2,最大的装载因子默认是0.75,当HashMap中元素个数达到容量的0.75时,就会扩容。 容量是原先的两倍
3,HashMap底层采用链表法来解决冲突。 但是存在一个问题,就是链表也可能会过长,影响性能
于是JDK1.8,对HashMap做了进一步的优化,引入了红黑树。
当链表长度超过8,且数组容量大于64时,链表就会转换为红黑树
当红黑树的节点数量小于6时,会将红黑树转换为链表。
因为在数据量较小的情况下,红黑树要维护自身平衡,比链表性能没有优势。
这3点非常重要!
其次,LinkedHashMap就是链表+散列表的结构,其底层采用了Linked双向链表来保存节点的访问顺序,所以保证了有序性。
20、谈谈ConcurrentHashMap,HashMap,Hashtable的区别
1,首先,来看看其他几个相关的类
Hashtable是线程安全的,但效率低
HashMap是线程不安全的,但效率高
Collections.synchronizedMap(),工具类提供了同步包装器的方法,来返回具有线程安全的集合对象
性能依然有问题
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
//在这个类的内部方法实现上,也只是单纯加上了锁
public V put(K key, V value) {
synchronized (mutex) {
return m.put(key, value);
}
}
为解决这样的矛盾问题,所以JDK提供了并发包,来平衡这样的问题(java.util.concurrent)
2,ConcurrentHashMap(重点)
- 兼顾了线程安全和效率的问题
分析:HashTable锁了整段数据(用户操作是不同的数据段,依然需要等待)
解决方案:把数据分段,执行分段锁(分离锁),核心把锁的范围变小,这样出现并发冲突的概率就变小
在保存的时候,计算所存储的数据是属于哪一段,只锁当前这一段
- 注意:分段锁(分离锁)是JDK1.8之前的一种的方案,JDK1.8之后做了优化。
JDK1.7跟JDK1.8在ConcurrentHashMap的实现上存在以下区别:
1,数据结构
JDK1.7采用链表的方式,而JDK1.8则采用链表+红黑树的方式
2,发生hash碰撞之后
JDK1.7发生碰撞之后,会采用链表的方式来解决
JDK1.8发生碰撞之后,默认采用链表,但当链表的长度超过8,且数组容量超过64时,会转换为红黑树存储
3,保证并发安全
JDK1.7采用分段锁的方式,而JDK1.8采用CAS和synchronized的组合模式
4,查询复杂度
JDK1.7采用链表的方式,时间复杂度为O(n),而JDK1.8在采用红黑树的方式时,时间复杂度为O(log(n))
题外话:
不过红黑树其实是一种兜底方案,因为当链表数量达到8个的时候,其发生的概率是千万分之几,所以作者考虑到这种极端情况下,需要用红黑树的方式来优化
说明:本文限于篇幅,故而只展示20题的面试内容,完整的Java面试学习文档小编已经帮你整理好了,有需要的朋友点赞+关注私信我777免费领取Java、大厂面试学习资料哦!