JAVA面试问题回答个人总结

JAVA面试问题回答个人总结

在此先感谢博客上的各位大佬,基本上都是看了他们的博文提炼出来的答案,下面也有跳转到他们博文的路径,这里做一个总结
如有侵权,请联系我,必定删除

1.数据库的优化方法

1.选取最适用的字段属性
数据库中的表越小,在它上面执行的查询也就会越快,为了获得更好的性能,在创建表的时候可以将表中字段的宽度设得尽可能小,
例如,在定义邮政编码这个字段的时候,如果将其设为char(255),显然给数据库增加了不必要的空间,甚至使用VARCHAR也是多余的,因为CHAR(6)就可以很好得完成任务了

2.使用连接(join)来代替子查询
连接之所以更有效率一些,是因为MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作

3.使用联合(UNION)来代替手动创建的临时表
它可以把需要使用临时表的两条或更多的select查询合并到一个查询中,在客户端的查询会话结束的时候,临时表会被自动删除,从而保证数据库整齐、高效

4.事务
设想一下,可能会出现这种情况:第一个表更新成功后,数据库出现异常,导致第二个表更新失败,这样就会造成数据的不完整,甚至会破坏数据库中的数据
要避免这种情况,可以使用事务,它的作用是,要么语句块中的每条语句都成功,要么都失败
如下

BEGIN; 
INSERT INTO salesinfo 
SET CustomerID=14;
UPDAT Einventory
SET Quantity=11
WHERE item='book';
COMMIT;

5.锁定表

LOCKTABL Einventory 
WRITE
SELECT Quantity 
FROMinventory WHEREItem='book';
...
UPDATE inventory 
SET Quantity=11 WHERE Item='book';
UNLOCKTABLES

这里,我们用一个select语句取出初始数据,通过一些计算,用update语句将新值更新到表中。包含有WRITE关键字的LOCKTABLE语句可以保证在UNLOCKTABLES命令被执行之前,不会有其它的访问来对inventory进行插入、更新或者删除的操作。

6.使用外键
保证数据的关联性
7.使用索引
join、where、orderby
8.优化的查询语句

详细内容出处

2.数据库的三种引擎

1.InnoDB:支持事物处理,支持外键,支持崩溃修复能力和并发控制。如果需要对事务的完整性要求比较高(如银行),要求实现并发控制(如售票),那选择InnoDB有很大的优势。如果需要频发的更新、删除操作的数据库,也可以选择InnoDB,因为支持事务的提交和回滚

2.MyISAM:插入数据快,空间和内存使用比较低。如果表主要是用于插入新记录和读出记录,那么选择MyISAM能实现处理高效率。如果应用的完整性、并发性要求比较低,也可以使用

3.MEMORY:所有的数据都在内存中,数据的处理速度很快,但是安全性不高。如果需要很快的读写速度,对数据的安全性要求较低,可以选择MEMORY。它对表的大小有要求,不能建立太大的表。

性能对比

详细内容出处

3.数据库设计的原则:

第一范式(确保每列保持原子性);
第二范式(确保表中的每列都和主键相关);
第三范式(确保每列都和主键列直接相关,而不是间接相关)。

详细内容出处

4.数据库索引原理

主流的RDBMS都是把平衡树(b tree或者 b+ tree)当做数据表默认的索引数据结构的

事实上, 一个加了主键的表,并不能被称之为「表」。一个没加主键的表,它的数据无序的放置在磁盘存储器上,一行一行的排列的很整齐, 跟我认知中的「表」很接近。

如果给表上了主键,那么表在磁盘上的存储结构就由整齐排列的结构转变成了树状结构,也就是上面说的「平衡树」结构

一个带有主键的表即为聚集索引

非聚集索引和聚集索引一样, 同样是采用平衡树作为索引的数据结构。

非聚集索引和聚集索引的区别在于, 通过聚集索引可以查到需要查找的数据, 而通过非聚集索引可以查到记录对应的主键值 , 再使用主键的值通过聚集索引查找到需要的数据

不管以任何方式查询表, 最终都会利用主键通过聚集索引来定位到数据, 聚集索引(主键)是通往真实数据所在的唯一路径。

然而, 有一种例外可以不使用聚集索引就能查询出所需要的数据, 这种非主流的方法 称之为「覆盖索引」查询, 也就是平时所说的复合索引或者多字段索引查询。

//建立索引
create index index_birthday on user_info(birthday);
//查询生日在1991年11月1日出生用户的用户名
select user_name from user_info where birthday = '1991-11-1'

这是一个非聚集索引
首先,通过非聚集索引index_birthday查找birthday等于1991-11-1的所有记录的主键ID值

然后,通过得到的主键ID值执行聚集索引查找,找到主键ID值对就的真实数据(数据行)存储的位置

最后,从得到的真实数据中取得user_name字段的值返回, 也就是取得最终的结果

create index index_birthday_and_user_name on user_info(birthday, user_name);

这是一个双字段的覆盖索引
通过非聚集索引index_birthday_and_user_name查找birthday等于1991-11-1的叶节点的内容,然而, 叶节点中除了有user_name表主键ID的值以外, user_name字段的值也在里面, 因此不需要通过主键ID值的查找数据行的真实所在, 直接取得叶节点中user_name的值返回即可。

关于SQL查询语句在聚集索引、非聚集索引平衡树中怎么工作,可以在以下链接中查看
详细内容出处

5.TCP粘包与拆包

什么是粘包和拆包,这里举个简单的例子:
首先要知道,TCP的过程是 1.服务器与客户端建立连接; 2.客户端向服务器发送包; 3.服务器与客户端断开连接
那么假设现在有两种情况
1.客户端与服务器建立连接,客户端向服务器发送了一个包,客户端与服务器断开连接;
2.客户端与服务器建立连接,客户端向服务器发送了两个包,客户端与服务器断开连接;

我们先看第一种情况,这种情况下,只要断开了连接,那么服务器就知道客户端像服务器发送包的过程已经完成了,接下来只要对包进行拆分解析就可以得出自己想要得到的信息

问题在于第二种情况,这种情况下,又分为三种情况

1.服务器端收到两个包裹,第一个包裹包含客户端发送出去的第一个包的全部信息,第二个包裹包含客户端发送出去的第二个包的全部信息,这种情况比较好处理,服务器只需要简单地从缓冲区读就可以了

2.服务器端收到一个包裹,这个包裹包含了客户端发送出去的第一个包和第二个包的全部信息,这也就是所谓的粘包,这种情况下服务器就蒙了,它完全不知道哪里到哪里是第一个包裹的内容,哪里到哪里是第二个包裹的内容

3.服务器端收到两个包裹,第一个包裹包含了客户端发送出去的第一个包的部分信息,第二个包裹包含了客户端发送出去的第一个包的余下的信息以及第二个包的全部信息,这就是所谓的拆包

发生TCP粘包、拆包主要是由于下面一些原因:
1.应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包
2.应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包
3.进行mss(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>mss的时候将发生拆包
4.接受方法不及时读取套接字缓冲区数据,发生粘包

解决方法:
1.使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
2.设置定长消息,服务端每次读取既定长度的内容作为一条完整消息。
3.设置消息边界,服务端从网络流中按消息编辑分离出消息内容。

详细内容出处

6.Hashmap

Hashmap结构如下
Hashmap结构图
hashmap的结构是数组+链表,它的主体是数组,链表的存在是为了解决哈希冲突,如果定位到的位置没有链表,那么查找和添加的时间复杂度仅为O(1);如果定位到的位置含有链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找,时间复杂度也为O(n)

关于Hashmap:
(1) HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。
(2) HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
  table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
  size是HashMap的大小,它是HashMap保存的键值对的数量。
  threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=“容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
  loadFactor就是加载因子。
  modCount是用来实现fail-fast机制的。

详细内容出处

7.类加载过程

类加载过程:
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载,
其中类加载过程包括:加载、验证、整备、解析、初始化五个阶段

双亲委托模型:
一个类加载器收到一个类加载的请求时,不会立刻自己去尝试加载这个类,而是把这个请求委托给它的父类加载器去完成,每一个层次的加载器都是如此,直至委托给最顶层的启动类加载器为止,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载

双亲委托模型的好处:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中一个类

详细内容出处

8.JVM

JVM是基于栈执行的虚拟机,除基本类型外的所有JAVA类型(类和接口)都是通过符号引用取得关联的,而非显式的基于内存地址的引用。类的实例通过用户代码进行显式创建,但却通过垃圾回收机制自动销毁。JVM通过明确清晰基本类型确保了平台无关性,在JAVA class中的二进制使用的是网络字节序

常见错误:
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

Java内存泄露根本原因
长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收

详细内容出处

9.java高并发简单处理

1、尽量使用缓存技术来做。用户缓存,页面缓存等一切缓存,使用特定的机制进行刷新。利用消耗内存空间来换取用户的效率,同时减少数据库的访问次数。

2、把数据库的查询语句进行优化,一般复杂的SQL语句就不要使用ORM框架自带的做法来写,采用自己来写SQL,例如hibernate的hql中的复杂语句就会很耗时。

3、优化数据库的表结构,在关键字、主键、访问率极高的字段中加入索引。但尽量只是在数字类型上面加,因为使用字段is null 的时候,索引的效果就会失效。

4、报表统计的模块,尽量使用定时任务执行,如果非要实时进行刷新,那么就可以采用缓存来做数据。

5、可以使用静态页面的地方,尽量使用静态页面,减少页面的解析时间。同时页面中的图片过多时,可以考虑把图片单独做成一个服务器,这样可以减少业务服务器的压力。

6、使用集群的方式来解决单台服务器的性能问题。

7、把项目拆分成多个应用小型服务器的形式来进行部署。采用数据同步机制(可以使用数据库同步形式来做)达到数据一致性。

8、使用负载均衡模式来让每一个服务器资源进行合理的利用。

9、缓存机制中,可以使用redis来做内存数据库缓存起来。也可以使用镜像分担,这样可以让两台服务器进行访问,提高服务器的访问量。

详细内容出处

10.post和get的区别

1.get参数通过url传递,而post通过request body传递
2.get传递的参数长度有限制,而post没有
3.get只能进行url编码,而post可以进行多种编码
4.get产生的url可以被bookmark,而post不可以
5.get请求会被浏览器主动cache,而post不会
6.get请求的参数会被完整地保留在浏览记录里,而post不会
7.get的参数类型只支持ASCII字符,而post没有限制
8.get回退时是无害的,而post会再次提交请求
最本质的是
对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);

而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

详细内容出处

11.四次挥手最后服务端没收到客户端的ack怎么办

四次挥手,客户端最后一次发送ack给服务端后,不会立刻关闭连接,而是进入TIME_WAIT(等待)状态,过了2MSL(报文最长寿命)后,才会断开连接,如果过程中服务端没有收到ack,客户端将会重发

12.hashmap是线程安全的吗?

不是的。
1.在hashmap做put操作的时候,会执行addEntry方法,如果线程A和线程B对同一个数组同时addEntry,会同时产生新的头结点,那么他们其中一个的写入就会使另一个的写入丢失
2.在hashmap做删除键值对的时候,多个线程同时操作同一个数组位置,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改
3. 当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

详细内容出处

13.如何实现hashmap线程安全

1.替换成Hashtable,Hashtable通过对整个表上锁实现线程安全,因此效率比较低

2.使用Collections类的synchronizedMap方法包装一下。方法如下:

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) 返回由指定映射支持的同步(线程安全的)映射

3.使用ConcurrentHashMap,它使用分段锁来保证线程安全

14.单向链表

单链表的初始化

public class Node{
	Object element; //数据域
	Node next;

	//结点构造
	public Node(Object element){
		this.element = element;
	}
	
}
public class linkList{
	Node head;

	//初始化
	public static void initLink(){
		this.head = new Node(null);
	}
}

单链表中增加结点

//尾插
public void addNodeT(Node node){

	//判断是否为第一次增加
	if(head.next == null)
		head.next = node;
	else{
		Node tmp = head;
		//遍历链表
		while(tmp.next!=null)
			tmp = tmp.next
		tmp.next = node;
	}
}
//头插
public void addNodeH(Node node){
	if(head.next == null)
		head.next = node;
	else{
		node.next = head.next;
		head.next = node;
	}
}

插入结点到指定位置

public void insert(int index,Node node){
	//判断
	if(index<1||indext>length()+1){
		System.out.println("位置不合理");
		return;
	}
	//判断插入位置是否是第一个位置
	else if(index==1){
		//判断此时链表是否为空
		if(head.next == null)
			head.next = node;
		else
			node.next = head.next;
			head.next = node;
	}
	else{
		int length = 1;
		Node tmp = head;
		while(tmp.next!=null){
			if(index == length++){
				//此时tmp为位置上一个结点
				node.next = tmp.next;
				tmp.next = node;
				return;
			}
			tmp = tmp.next;
		}
		
	}
}

删除结点

public static void delete(int index){
	if(index<1||index>length()){
		System.out.println("输入位置不合理");
		return;
	}
	else{
		int length = 1;
		Node tmp = head;
		while(tmp.next!=null){
			if(index == length++){
				tmp.next = tmp.next.next;
				return;
			}
			tmp = tmp.next;
		}
	}
}

15.二分查找

public static int search(int[] arr,int key,int low,int high){
	if(key<arr[low]||key>arr[high]||low>high){
		return -1;
	}
	int middle = (low+high)/2;
	if(key == arr[middle])
		return middle;
	else if(key>arr[middle])
		return search(arr,key,middle+1,high);
	else
		return search(arr,key,low,middle);
}

16.查找和排序时间复杂度比较

查找方法 时间复杂度
顺序查找 O(n)
二分查找(折半查找) O( l o g 2 log_{2} n)
二叉排序树查找 O( l o g 2 log_{2} n)
分块查找 O( l o g 2 log_{2} n)
哈希表查找 O(1)
排序方法 最差时间 平均时间复杂度 稳定性
冒泡排序 O( n 2 n^2 ) O( n 2 n^2 ) 稳定
插入排序 O( n 2 n^2 ) O( n 2 n^2 ) 稳定
选择排序 O( n 2 n^2 ) O( n 2 n^2 ) 稳定
二叉树排序 O( n 2 n^2 ) O(n* l o g 2 n log_{2}n ) 不一定
快速排序 O( n 2 n^2 ) O(n* l o g 2 n log_{2}n ) 不稳定
堆排序 O( n 2 n^2 ) O(n* l o g 2 n log_{2}n ) 不稳定
希尔排序 O( n 2 n^2 ) O(n* l o g 2 n log_{2}n )~ O( n 2 n^2 ) 不稳定

17.冒泡排序

举例说明:要排序数组:int[] arr={6,3,8,2,9,1};

第一趟排序:

第一次排序:6和3比较,6大于3,交换位置: 3 6 8 2 9 1

第二次排序:6和8比较,6小于8,不交换位置:3 6 8 2 9 1

第三次排序:8和2比较,8大于2,交换位置: 3 6 2 8 9 1

第四次排序:8和9比较,8小于9,不交换位置:3 6 2 8 9 1

第五次排序:9和1比较:9大于1,交换位置: 3 6 2 8 1 9

第一趟总共进行了5次比较, 排序结果: 3 6 2 8 1 9


第二趟排序:

第一次排序:3和6比较,3小于6,不交换位置:3 6 2 8 1 9

第二次排序:6和2比较,6大于2,交换位置: 3 2 6 8 1 9

第三次排序:6和8比较,6大于8,不交换位置:3 2 6 8 1 9

第四次排序:8和1比较,8大于1,交换位置: 3 2 6 1 8 9

第二趟总共进行了4次比较, 排序结果: 3 2 6 1 8 9


第三趟排序:

第一次排序:3和2比较,3大于2,交换位置: 2 3 6 1 8 9

第二次排序:3和6比较,3小于6,不交换位置:2 3 6 1 8 9

第三次排序:6和1比较,6大于1,交换位置: 2 3 1 6 8 9

第二趟总共进行了3次比较, 排序结果: 2 3 1 6 8 9


第四趟排序:

第一次排序:2和3比较,2小于3,不交换位置:2 3 1 6 8 9

第二次排序:3和1比较,3大于1,交换位置: 2 1 3 6 8 9

第二趟总共进行了2次比较, 排序结果: 2 1 3 6 8 9


第五趟排序:

第一次排序:2和1比较,2大于1,交换位置: 1 2 3 6 8 9

第二趟总共进行了1次比较, 排序结果: 1 2 3 6 8 9


最终结果:1 2 3 6 8 9


由此可见:N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次,所以可以用双重循环语句,外层控制循环多少趟,内层控制每一趟的循环次数,即

代码:

public static void main(String[] args){
	int[] arr = {6,3,8,2,9,1};
	for(int i=0;i<arr.length()-1;i++)
		for(int j=0;j<arr.length()-1-i;j++){
			if(arr[j]>arr[j+1]){
				int tmp = arr[j];
				arr[j] = arr[j+1];
				arr[j+1] = tmp;
			}	
		}
}

详细内容出处

18.插入排序

0.初始状态 3,1,5,7,2,4,9,6(共8个数)

有序表:3;无序表:1,5,7,2,4,9,6

1.第一次循环,从无序表中取出第一个数 1,把它插入到有序表中,使新的数列依旧有序

有序表:1,3;无序表:5,7,2,4,9,6

2.第二次循环,从无序表中取出第一个数 5,把它插入到有序表中,使新的数列依旧有序

有序表:1,3,5;无序表:7,2,4,9,6

3.第三次循环,从无序表中取出第一个数 7,把它插入到有序表中,使新的数列依旧有序

有序表:1,3,5,7;无序表:2,4,9,6

4.第四次循环,从无序表中取出第一个数 2,把它插入到有序表中,使新的数列依旧有序

有序表:1,2,3,5,7;无序表:4,9,6

5.第五次循环,从无序表中取出第一个数 4,把它插入到有序表中,使新的数列依旧有序

有序表:1,2,3,4,5,7;无序表:9,6

6.第六次循环,从无序表中取出第一个数 9,把它插入到有序表中,使新的数列依旧有序

有序表:1,2,3,4,5,7,9;无序表:6

7.第七次循环,从无序表中取出第一个数 6,把它插入到有序表中,使新的数列依旧有序

有序表:1,2,3,4,5,6,7,9;无序表:(空)

核心代码

for(int i=1;i<arr.length();i++){
	int tmp = arr[i];
	for(int j = i-1;j>=0&&arr[i]>arr[j];j--)
		arr[j+1] = arr[j];
	arr[j+1] = tmp;
}

详细内容出处

19.选择排序

举例:数组 int[] arr={5,2,8,4,9,1};


第一趟排序: 原始数据:5 2 8 4 9 1

最小数据1,把1放在首位,也就是1和5互换位置,

排序结果:1 2 8 4 9 5


第二趟排序:

第1以外的数据{2 8 4 9 5}进行比较,2最小,

排序结果:1 2 8 4 9 5


第三趟排序:

除1、2以外的数据{8 4 9 5}进行比较,4最小,8和4交换

排序结果:1 2 4 8 9 5


第四趟排序:

除第1、2、4以外的其他数据{8 9 5}进行比较,5最小,8和5交换

排序结果:1 2 4 5 9 8


第五趟排序:

除第1、2、4、5以外的其他数据{9 8}进行比较,8最小,8和9交换

排序结果:1 2 4 5 8 9


核心代码

for(int i=0;i<arr.length()-1;i++){
	int k = i;
	for(int j=k+1;j<arr.length();j++)
		if(arr[j]<arr[k])
			k = j;
	if(k!=i){
		int tmp = arr[k];
		arr[k] = arr[j];
		arr[j] = tmp;
	}
}

详细内容出处

猜你喜欢

转载自blog.csdn.net/bigbigChopper/article/details/84339614