Java面试题(二)
什么是堆排序
堆排序介绍
堆排序是指利用堆积树这种数据结构所设计的一种排序算法,它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。
堆是一个近完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或大于)它的父节点。
完全二叉树
除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐。
满二叉树
除了叶子结点之外的每一个结点都有两个孩子,每一层(当然包含最后一层)都被完全填充。
完满二叉树
除了叶子结点之外的每一个结点都有两个孩子结点
总得来说,堆排序是将数据看成是完全二叉树、根据完全二叉树的特性来进行排序的一种算法,最大堆要求节点元素都要不小于其孩子,最小堆要求节点元素都不大于其左右孩子,那么处于最大堆的根节点的元素就是这个堆中的最大值。
完全二叉树有个特性:
左边子节点位置 = 当前父节点的两倍 + 1,右边子节点位置 = 当前父节点的两倍 + 2.
堆排序代码实现
1 /**
2 * 建堆
3 *
4 * @param arrays 看作是完全二叉树
5 * @param currentRootNode 当前父节点位置
6 * @param size 节点总数
7 */
8 public static void heapify(int[] arrays, int currentRootNode, int size) {
9
10 if (currentRootNode < size) {
11 //左子树和右字数的位置
12 int left = 2 * currentRootNode + 1;
13 int right = 2 * currentRootNode + 2;
14
15 //把当前父节点位置看成是最大的
16 int max = currentRootNode;
17
18 if (left < size) {
19 //如果比当前根元素要大,记录它的位置
20 if (arrays[max] < arrays[left]) {
21 max = left;
22 }
23 }
24 if (right < size) {
25 //如果比当前根元素要大,记录它的位置
26 if (arrays[max] < arrays[right]) {
27 max = right;
28 }
29 }
30 //如果最大的不是根元素位置,那么就交换
31 if (max != currentRootNode) {
32 int temp = arrays[max];
33 arrays[max] = arrays[currentRootNode];
34 arrays[currentRootNode] = temp;
35
36 //继续比较,直到完成一次建堆
37 heapify(arrays, max, size);
38 }
39 }
40 }
要注意的是:
在上面体验堆排序时,我们是左子树和右子树都是已经有父子这么一个条件的,显然,一个普通的数组并不能有这种条件(父>子),因此,我们往往是从数组最后一个元素来进行建堆。
1 /**
2 * 完成一次建堆,最大值在堆的顶部(根节点)
3 */
4 public static void maxHeapify(int[] arrays, int size) {
5
6 // 从数组的尾部开始,直到第一个元素(角标为0)
7 for (int i = size - 1; i >= 0; i--) {
8 heapify(arrays, i, size);
9 }
10
11 }
接下来不断建堆,然后让数组最后一位与当前堆顶(数组第一位)进行交换即可排序。
1 for (int i = 0; i < arrays.length; i++) {
2
3 //每次建堆就可以排除一个元素了
4 maxHeapify(arrays, arrays.length - i);
5
6 //交换
7 int temp = arrays[0];
8 arrays[0] = arrays[(arrays.length - 1) - i];
9 arrays[(arrays.length - 1) - i] = temp;
10
11 }
用set集合的时候,重写过hashcode()和equal()方法吗?有什么作用?
set集合特点
1.没有重复元素
2.set集合是无序的
3.允许包含值为null的元素,但最多只能有一个null元素
HashSet集合
HashSet集合采用用哈希表结构存储数据,保证元素唯一性的方式依赖于hashCode()和equals()方法。
1.HashSet集合排重时,需要判断两个对象是否相同,对象相同的判断可以通过hashCode值判断,所以需要重写hashCode()方法。
2.HashSet不能为一样的,放入一个值首先判断hashCode(类似下标)是否已经存在,然后用equals()判断是否有一样的。
3.如果只重写其中一个方法的时候,向HashSet集合中添加多个对象时,所有属性都相同时,并没有完成想要的排重效果。HashSet不能为一样的,放入一个值首先判断hashCode(内存中的位置)是否已经存在,然后用equals判断是否有一样的值。
情况一:当我们往HashSet集合中添加 8大基本类型和String类型的时候,不需要重写hashCode()和equals()方法。因为任何对象都是Object类的子类,所以任何对象都拥有这个方法。
情况二:当我们往HashSet集合添加自定义对象的时候,就需要重写hashCode()和equals()方法。建立自己的比较方式,才能保证HashSet集合中的对象唯一。
案列:
public class Student {
private String name;
public Student(String name) {
super();
this.name = name;
}
public Student() {
super();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student [name=" + name + "]";
}
@Override
//重写equals
public boolean equals(Object obj) {
//先判断传入的参数对象是否是Student对象,若不是直接返回false
if(obj instanceof Student) {
//若是,强转成Student对象,并比较属性的值
Student s = (Student) obj;
if(this.name.equals(s.name)) {
//若属性的值相同,则返回true
return true;
}
}
return false;
}
@Override
public int hashCode(){
/*hashCode方法返回值是int类型,所以重写时需要找到int类型的数据返回,还要保证此方法的返回值与对象的所有属性都相关,所以返回姓名属性的字符串的长度*/
return this.name.length();
}
}
多数据源配置情况下的事务管理
可以是分布式事务管理也可以是简单的DataSourceTranctionManager.
但是建议不做分布式事务管理,尽量保证一个事务不只操作一个数据库。
保持服务功能的单一性,如果一个服务会操作到两个数据库中的数据,那设计到的所有表最好放到同一个数据库中。因为分布式事务管理,消耗资源严重,性能下降。
现在我们有两个服务a和b,分别操作一个数据库,如果a中又调用b服务,这时候实际上a服务是操作了多个数据源,如果要保证事务一致性,就需要判断b服务的返回结果。
如果b出错了会抛异常,那我们就try catch b 服务,并且重新在a中把这个异常抛出。如果b服务中始终返回结果,并且结果的状态是错误的,我们就不能try catch了,而是判断返回救过的状态,如果是错误的 就在a中抛出异常。
如果是分布式事务管理,意味着项目里有服务 需要操作多个数据库,并且要做到事务一致性。
spring的org.springframework.transaction.jta.JtaTransactionManager,提供了分布式事务支持。如果使用WAS的JTA支持,把它的属性改为WebSphere对应的TransactionManager。
在tomcat下,是没有分布式事务的,不过可以借助于第三方软件jotm(Java Open Transaction Manager )和AtomikosTransactionsEssentials实现,在spring中分布式事务是通过jta(jotm,atomikos)来进行实现。
1、http://jotm.objectweb.org/
2、http://www.atomikos.com/Main/TransactionsEssentials
前后端分离有什么好处?那前端是如何测试的?
前后端分离的优势
1.可以实现真正的前后端解耦,前段服务器使用nginx,前段/WEB服务器放的是css,js,图片等一系列静态资源(甚至还可以css,js,图片等资源放到特定的文件服务器,例如阿里的oss,并使用cdn加速),前段服务器负责控制页面引用、跳转、路由,前段页面异步调用后端的接口,后端/应用服务器使用tomcat(把tomcat想象成一个数据提供者),加快整体响应速度。(这里需要使用一些前端工程化的框架比如nodejs,react,router,react,redux,webpack)
2、发现bug,可以快速定位是谁的问题,不会出现互相踢皮球的现象。页面逻辑,跳转错误,浏览器兼容性问题,脚本错误,页面样式等问题,全部由前端工程师来负责。接口数据出错,数据没有提交成功,应答超时等问题,全部由后端工程师来解决。双方互不干扰,前端与后端是相亲相爱的一家人。
3、在大并发情况下,我可以同时水平扩展前后端服务器,比如淘宝的一个首页就需要2000+台前端服务器做集群来抗住日均多少亿+的日均pv。(去参加阿里的技术峰会,听他们说他们的web容器都是自己写的,就算他单实例抗10万http并发,2000台是2亿http并发,并且他们还可以根据预知洪峰来无限拓展,很恐怖,就一个首页。。。)
4、减少后端服务器的并发/负载压力。除了接口以外的其他所有http请求全部转移到前端nginx上,接口的请求调用tomcat,参考nginx反向代理tomcat。且除了第一次页面请求外,浏览器会大量调用本地缓存。
5、即使后端服务暂时超时或者宕机了,前端页面也会正常访问,只不过数据刷不出来而已。
6、也许你也需要有微信相关的轻应用,那样你的接口完全可以共用,如果也有app相关的服务,那么只要通过一些代码重构,也可以大量复用接口,提升效率。(多端应用)
7、页面显示的东西再多也不怕,因为是异步加载。
8、nginx支持页面热部署,不用重启服务器,前端升级更无缝。
9、增加代码的维护性&易读性(前后端耦在一起的代码读起来相当费劲)。
10、提升开发效率,因为可以前后端并行开发,而不是像以前的强依赖。
11、在nginx中部署证书,外网使用https访问,并且只开放443和80端口,其他端口一律关闭(防止黑客端口扫描),内网使用http,性能和安全都有保障。
12、前端大量的组件代码得以复用,组件化,提升开发效率,抽出来!
注意事项:
1、在开需求会议的时候,前后端工程师必须全部参加,并且需要制定好接口文档,后端工程师要写好测试用例(2个维度),不要让前端工程师充当你的专职测试,推荐使用chrome的插件postman或soapui或jmeter,service层的测试用例拿junit写。ps:前端也可以玩单元测试吗?
2、上述的接口并不是java里的interface,说白了调用接口就是调用你controler里的方法。
3、加重了前端团队的工作量,减轻了后端团队的工作量,提高了性能和可扩展性。
4、我们需要一些前端的框架来解决类似于页面嵌套,分页,页面跳转控制等功能。(上面提到的那些前端框架)。
5、如果你的项目很小,或者是一个单纯的内网项目,那你大可放心,不用任何架构而言,但是如果你的项目是外网项目,呵呵哒。
6、 以前还有人在使用类似于velocity/freemarker等模板框架来生成静态页面,仁者见仁智者见智。
7、这篇文章主要的目的是说jsp在大型外网java web项目中被淘汰掉,可没说jsp可以完全不学,对于一些学生朋友来说,jsp/servlet等相关的java web基础还是要掌握牢的,不然你以为springmvc这种框架是基于什么来写的?
8、如果页面上有一些权限等等相关的校验,那么这些相关的数据也可以通过ajax从接口里拿。
9、对于既可以前端做也可以后端做的逻辑,我建议是放到前端,为什么?因为你的逻辑需要计算资源进行计算,如果放到后端去run逻辑,则会消耗带宽&内存&cpu等等计算资源,你要记住一点就是服务端的计算资源是有限的,而如果放到前端,使用的是客户端的计算资源,这样你的服务端负载就会下降(高并发场景)。类似于数据校验这种,前后端都需要做!
10、前端需要有机制应对后端请求超时以及后端服务宕机的情况,友好的展示给用户。