Java 基础篇面试题
# 面向对象的特征
面向对象的特征:封装、继承、多态、抽象。
封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。一句话说就是,隐藏内部细节,提供对外接口。
继承:将多个类共有的行为和属性抽取到一个类中,即父类,拥有共同属性和行为的类称为这个类的子类,子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。
多态:**指允许不同的对象对同一消息做出响应。**即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。
抽象表示对问题领域进行分析、设计中得出的抽象的概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。在Java中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的。 抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
# String、StringBuffer与StringBuilder之间区别

简单说:
由于String的内部声明了一个final修饰的byte类型的数组,所以在首次初始化后,也就是创建对象后,该数组的长度就不可变了,所以String不可变的对象, 因此在每次对 String 类型进行改变的时候,其实都等同于生成了一个新的 String 对象,然后将地址指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间,字符串的拼接亦是如此,所以经常改变内容的字符串最好不要用 String 。
并且jdk9之前,String底层使用的char类型的数组,但是从jdk9之后,value的类型由char[]数组变成了byte[]数组。java做这种优化是为了减少字符串在堆内存中的占用(字符串常量池在jdk1.7由方法区移动到了堆内存),节省空间,提高String的性能。
和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。而两者的区别在于StringBuffer给所有的方法都加上了synchronized锁,所以是线程安全的,而StringBuilder没有上锁,所以相对的效率会更高,在多线程也不安全。
我以前有稍微看过ArrayList源码,发现ArrayList类的toString拼接字符串就是使用了StringBuilder来拼接的(在AbstractCollection类中)
# Static关键字有什么用?
修饰共享变量:
被static关键字修饰的内容都是全局共享的,且只会为其分配一次存储空间。被static修饰的内容不是属于对象的,是属于类的。当类加载到JVM内存中,JVM会把静态变量放入方法区并分配内存,也由线程共享。
修饰代码块:
实例代码块会随着对象的创建而执行,即每个对象都会有自己的实例代码块,表现出来就是实例代码块的运行结果会影响当前对象的内容,并随着对象的销毁而消失(内存回收);而静态代码块是当Java类加载到JVM内存中而执行的代码块,由于类的加载在JVM运行期间只会发生一次,所以静态代码块也只会执行一次。
因为静态代码块的主要作用是用来进行一些复杂的初始化工作,所以静态代码块跟随类存储在方法区的表现形式是静态代码块执行的结果存储在方法区,即初始化量存储在方法区并被线程共享。
修饰方法:
被static修饰的方法也是会被加载到方法区,成为全局共享的资源,可以通过类名调用也可以通过对象调用,但是建议的话是通过类名调用,也是规范。
**总结:**static关键字的含义就是共享,只要被static修饰了都会被jvm加载到方法区进行线程共享
**缺点:**由于static会让jvm将修饰的内容加载到方法区共享,所以会破坏类的封装性
# 值传递和引用传递的区别
值传递传递的是一个值的副本,也就是实际数据的一个临时copy,所以方法对这个副本进行的修改不会影响实际参数。
而引用传递传递的是地址的副本,所以方法调用这个对象实际就是去找这个地址指向的存储单元,所以对这个地址指向的存储单元中的数据进行修改是会影响实参的,而仅仅是改变地址副本的指向是不会影响实参的
# JavaBean和POJO的区别
POJO (opens new window)的内在含义是指那些:
有一些private (opens new window)的参数作为对象的属性,然后针对每一个参数定义get和set方法访问的接口。
没有从任何类继承、也没有实现任何接口,更没有被其它框架侵入的java对象。
JavaBean 是一种JAVA语言写成的可重用组件。JavaBean符合一定规范编写的Java类,不是一种技术,而是一种规范。大家针对这种规范,总结了很多开发技巧、工具函数。符合这种规范的类,可以被其它的程序员或者框架使用。它的方法命名,构造及行为必须符合特定的约定:
- 所有属性为private。
- 这个类必须有一个公共的缺省构造函数。即是提供无参数的构造器。
- 这个类的属性使用getter和setter来访问,其他方法遵从标准命名规范。
- 这个类应是可序列化的。实现serializable接口。
两者的区别
- 两者都是一种规范,POJO其实是比javabean更纯净的简单类或接口。POJO严格地遵守简单对象的概念,而一些JavaBean中往往会封装一些简单逻辑。
- POJO主要用于数据的临时传递,它只能装载数据, 作为数据存储的载体,而不具有业务逻辑处理的能力。
- Javabean虽然数据的获取与POJO一样,但是javabean当中可以有其它的方法。
# 泛型常用特点
泛型是Java SE 1.5之后的特性,《Java 核心技术》中对泛型的定义是:“泛型” 意味着编写的代码可以被不同类型的对象所重用。
**使用泛型的好处? **
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
# 什么是抽象类,什么时候要用抽象类?
面试话术
抽象类的特点跟普通Java类很像,但最大的区别就是抽象类中有抽象方法,一般我们如果要抽取几个类中的共有属性以及共有行为时,并且又不写死某个行为,就是这个行为根据子类不同,具体实现也不同,说白了就是个拥有一部分共性属性和行为的模板。
# 你知道java8的新特性吗,请简单介绍一下
- Lambda 表达式 − Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
- 函数式接口 - 针对lambda表达式提出的一个只有一个抽象方法的接口
- 方法引用− 方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。
- Nashorn, JavaScript 引擎 − Java 8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。
- Stream流 - 用于操作集合数据,起到一个集合数据工厂的作用
- **接口 - **中允许定义静态方法和默认方法
- Optional类 - Optional 类是一个可以为 null 的容器对象。如果值存在则 isPresent()方法会返回 true,调用 get()方法会返回该对象。
- 时间API - 这些类都在java.time包下,如LocalDate/LocalTime/LocalDateTime。
# BIO 、NIO 、AIO 的区别
NIO即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO 主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
1、面向流与面向缓冲
Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2、阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
io的各种流是阻塞的,就是当一个线程调用读写方法时,该线程会被阻塞,直到读写完,在这期间该线程不能干其他事,CPU转而去处理其他线程,假如一个线程监听一个端口,一天只会有几次请求进来,但是CPU却不得不为该线程不断的做上下文切换,并且大部分切换以阻塞告终。
NIO通讯是将整个任务切换成许多小任务,由一个线程负责处理所有io事件,并负责分发。它是利用事件驱动机制,而不是监听机制,事件到的时候再触发。NIO线程之间通过wait,notify等方式通讯。保证了每次上下文切换都有意义,减少无谓的进程切换。
3、选择器(Selectors)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
**AIO:**Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
三者的区别:
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
先来个例子理解一下概念,以银行取款为例:
- 同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
- 异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
- 阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
- 非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
Java对BIO、NIO、AIO的支持:
- Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
- Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
- Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
BIO、NIO、AIO适用场景分析:
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
另外,I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
# 重载和重写的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同.个数不同.顺序不同,与方法返回值和访问修饰符无关,发生在编译时。
重写: 发生在父子类中,方法名.参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
**返回值范围小于等于父类:**仅当父类返回值类型是引用类型时,子类才可以修改方法的返回值,并且返回值类型必须是父类返回值类型的子类
# 集合与数组的区别
数组的长度固定,集合的长度可变;
**数组:**数组中存储的数据类型只能是一种类型,可以存储基本数据类型和引用类型;
**集合:**集合中只能存储引用数据类型,且存储的都是对象,数据类型也可不一致,如:map集合中的键与值;在开发中如果对象比较多的情况下,多使用集合来存储对象;
# Java中==和equals的区别
== 的作用:
基本类型:比较的就是值是否相同
引用类型:比较的就是地址值是否相同
**equals 的作用:
**引用类型:默认情况下,比较的是地址值。
特:String、Integer、Date这些类库中equals被重写,比较的是内容而不是地址!
# 为什么重写了equals方法还要重写hashcode方法
equals方法在Object类中默认的实现方式是 : return this == obj 。
就是说,只有this 和 obj引用同一个对象,即两个对象地址值相同,才会返回true。
public boolean equals(Object obj) {
return (this == obj);
}
2
3
而我们往往需要用equals来判断 2个对象是否等价,而非验证他们的唯一性。这样我们在实现自己的类时,就要重写equals。
而hashCode方法在Object类中的实现是计算出对象地址值的散列码
@HotSpotIntrinsicCandidate
public native int hashCode();//本地方法,通过虚拟机调用
2
重写:
public class User {
String name;
int age;
@Override
public boolean equals(Object o) {
//地址值判断
if (this == o) return true;
//非null判断,同类判断
if (o == null || getClass() != o.getClass()) return false;
//能执行到这里,说明obj和this同类且非null。
User user = (User) o;
//内容判断
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
//hashCode计算
return Objects.hash(name, age);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
从上面的代码中我们不难看出,在进行重写equals方法时,就已经能够判断出两个对象是否为同一个对象,内容是否相等了,那么为什么还要重写hashCode方法呢?
关于hashCode方法,一致的约定是:
- 在某个运行时期间,只要对象的(字段的)变化不会影响equals方法的决策结果,那么,在这个期间,无论调用多少次hashCode,都必须返回同一个散列码。
- 如果2个对象通过equals调用后返回是true,那么这个2个对象的hashCode方法也必须返回同样的int型散列码。如果2个对象通过equals返回false,他们的hashCode返回的值允许相同。
从代码中我们也能看出,参与equals方法的字段,也会参与hashCode的计算。
从结论上来讲
重写hashCode方法是为了确保两个对象的内容一定相同。因为**内容不同的对象,hashCode可能相同,而内容相同的对象,其hashCode一定相同。**如果我们只重写hashCode方法的话,就可能会出现hashCode值相等,但对象内容不同的情况;所以重写了equals方法之后,重写hashCode方法,可以认为是一种内容一致性的验证。
但是!!!!!!
上述的情况是在进行普通情况下的对象内容比较。比如:
//此时User只重写了equals方法
User u1 = new User("张三",20);
User u2 = new User("张三",20);
System.out.println(u1.equals(u2)); //true
2
3
4
这是理所当然,因为根本就没有用到hashCode方法。那么这时就会觉得hashCode方法是不是可有可无了?其实并不然,我们都知道HashSet集合不允许存放重复的元素,当需要在HashSet集合中存放自定义类时,单独重写equals方法是没办法做到去重的。比如以下例子:
//此时User只重写了equals方法
User u1 = new User("张三",20);
User u2 = new User("张三",20);
Set list = new HashSet();
list.add(u1);
list.add(u2);
System.out.println(list);
运行结果:
[User{name = 张三, age = 20}, User{name = 张三, age = 20}]
2
3
4
5
6
7
8
9
如上所示,u1和u2两个对象姓名年龄相同,本应该是同一个人,不允许重复存入集合的才对,由于HashSet底层是采用哈希表来存储元素,所以是通过hashCode方法计算出哈希值来确定在哈希表中存储的位置。没有重写hashCode方法,默认用的就是Object类的hashCode方法计算出对象地址的哈希值,既然都是通过地址值计算,那么两个都是new出来的对象,即使内容相同,地址值也不同,所以存储的位置自然也不一样,以此HashSet就认为是不同的对象了,也无法做到去重。
所以,**当需要存储自定义到哈希表,**hashCode方法的作用就体现出来了。为了达到去重的效果,我们必须重写hashCode方法,并且,我们也要意识到,hashCode返回独一无二的散列码,会让存储这个对象的hashtables更好地工作。
# Java中常见数据结构

数组是最常用的数据结构,数组的特点是长度固定,数组的大小固定后就无法扩容了 ,数组只能存储一种类型的数据 ,添加,删除的操作慢,因为要移动其他的元素。像是String、StringBuffer、StringBuilder、ArrayList等底层使用的就是数组,数组在java中的使用还是非常广泛的。
栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。像是JVM内存结构中的本地方法栈使用的就是栈结构。
队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先被读取出来。如线程池中的任务队列,使用的就是队列结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能直接表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结节(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表等。如LinkedList、LinkedHashMap、哈希表结构,链表都充分发挥了其作用。
树是我们计算机中非常重要的一种数据结构,同时使用树这种数据结构,可以描述现实生活中的很多事物,例如家谱、单位的组织架构等等。有二叉树、平衡树、红黑树、B树、B+树。如MySQL、哈希表结构。
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。典型的代表就是set集合和map集合。
堆是计算机学科中一类特殊的数据结构的统称,堆通常可以被看作是一棵完全二叉树的数组对象。如JVM中的堆内存结构。
图的定义:图是由一组顶点和一组能够将两个顶点相连的边组成的
# ArrayList扩容规则
规则:
- 申请集合内存空间时,数组长度为0;
- 当首次添加元素单个元素时,数组扩容为10;
- 当集合中元素个数达到数组容量时,扩容为上一次容量的1.5倍;
- 当调用
addAll()时,会与默认容量做比较取最大值作为下一次扩容的容量;
List.add()的情况:
- 第一次扩容容量为10
- 第二次扩容开始就为当前容量的1.5倍,容量变为15
- 扩容的底层实现是当前容量右移一位加上当前容量
例:
第三次扩容则为15>>1=7→7+15=22,容量变为22
前20次扩容情况
0,10,15,22,33,49,73,109,163,244,366,549,823,1234,1851,2776,4164,6246,9369,14053,21079
**List.addAll()的情况:**方法会将添加的集合大小跟下一次扩容容量之间做比较取最大值
例:
用addAll添加11个元素进集合,则会将集合大小11跟下一次扩容容量做比较,也就是10跟11比较,取最大即11,所以这种情况下扩容为11而不是15,若是先添加10个元素再添加3个元素则是13与15相比取15
# ArrayList与LinkedList的区别
**共同点:**首先两者都实现了List接口,都是存取元素有序、有索引、可以存放重复元素。
**从底层数据结构上比较:**ArrayList是数组,LinkedList是链表。
从操作集合效率上比较:
查询效率上讲,ArrayList要优于LinkedList,因为ArrayList的底层是数组,内存空间是连续的,介于数组的随机访问机制,所以元素的随机访问效率会更高,也就是查询(get)效率会更高,而LinkedList底层是双向链表,内存空间不连续,获取元素时,需要根据索引进行一次二分查找,索引大于集合元素个数的一半则从尾结点开始遍历,否则从头结点开始遍历,无论如何还是需要遍历一半左右的元素,因此查询时间会相对较长,但若是访问首尾元素以及中间元素的话效率与ArrayList没有太大差别。
增删效率上讲,因为ArrayList底层是数组,所以增删元素时,都需要将后续元素进行整体的前移和后移,这里面就会涉及到大量赋值操作,是比较消耗资源的,所以效率也会较低,而LinkedList的底层是链表,增删元素只需要修改前后元素的下一个节点指向地址与上一个节点的指向地址,所以效率会更高。
**从内存占用上比较:**ArrayList底层是数组,内存空间连续,并且每一个存储空间只需要存储元素数据,而LinkedList底层是链表,内存空间不连续,并且每一个节点需要存储上一个节点地址、元素数据、下一个节点地址,所以在内存空间占用上,LinkedList要略高于ArrayList。
**运用场景:**当存入的数据需要经常查询,则放入ArrayList,比如说固定的一系列数据,像题库,不需要经常增删,但需要经常查询的都建议存到ArrayList,以及在使用一些持久层框架中的查询结果返回都是使用ArrayList进行接收,因为我们只需要遍历获取返回的数据,不需要再对这个集合进行增删,而需要经常增删的,如比存放赛晋级的选手名单,会涉及到频繁的增删集合元素,这类信息就可以放到LinkedList,实际开发中,ArrayList的使用率要高于LinkedList。
# lterator数据保护机制
fail-fast 一旦发现遍历的同时其他人来修改,则立即抛异常fail-safe 一旦发现遍历的同时其他人来修改,应当能有对应策略,例如牺牲一致性来让整个遍历运行完成
**注:**ArryList用的就是fail-fast行为来保护数据而CopyOnWriteArrayList用的则是fail-safe行为来保护数据
ArryList底层实现
// modCount记录的是List集合添加元素的次数
// expectedModCount记录的是迭代前List集合添加元素的次数
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
2
3
4
5
6
通过这个方法判断是否被修改,当迭代时发生集合元素修改,madCount != expectedModCount 就会返回true,进入if语句抛出并发修改异常。
# List和Map、Set的区别
List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
- List中存取是有序的,并且值允许重复,有索引;
- Map中存取是无序的,它的键是不允许重复的,但是值是允许重复的,没有索引;
- Set中存取是无序的,并且不允许重复,没有索引,与Map集合相同,元素存储的位置是由hashcode决定的,若是jdk核心类库中的类,是无法人为控制存储位置的,而自定义可以通过重写hashcode来控制元素存储的位置。
# HashSet底层原理
HashSet的底层实现是HashMap,是一个默认长度为16,扩容因子为0.75的哈希数组,即哈希表,与HashMap相同,jdk1.7时是数组+链表的结构,jdk1.8时是数组+链表+红黑树的结构。
需要注意的是,Map是双列集合,是以键值对的形式存储的、Set是单列集合,Set是通过使用一个private static final修饰的Object对象来占用了键值对的值,由于存储的值都是同一个常量,所以值的那一列就可以忽略不计了,以此达到单列的效果。详细原理请看HashMap底层原理。
# HashMap底层原理
** 底层数据结构,1.7、1.8有何不同?**
底层数据结构是哈希表,一个默认长度为16,扩容因子为0.75的哈希数组。这道题相当于在问哈希表1.7和1.8有何不同。哈希表1.7用的是数组+链表的结构,1.8用的是数组+链表 | 红黑树的结构
哈希数组的扩容规则
当哈希数组的链表个数达到扩容因子时,例:首次扩容链表个数达到16*0.75=12时,扩容为原来的两倍
加载因子/扩容因子为何默认是0.75f?
- 在空间占用与查询时间之间取得较好的权衡
- 大于这个值,空间节省了,但链表就会比较长影响性能
- 小于这个值,冲突减少了,但是扩容就会更加频繁,空间占用多
为何要用红黑树?为何一上来不树化?树化阈值为何是8?何时会树化?何时会退化为链表?
- 首先树化并不是一个好的体现,这说明数据存储的不正常。红黑树是用来避免DoS攻击的,防止链表超长时性能下降,树化应当是偶然情况。
- **Dos攻击:**通过发送大量hash冲突的数据使链表超长,从而影响数据查询性能
- hash表的查找,更新的时间复杂度是O(1),而红黑树的查找,更新的时间复杂度是O(log2n),TreeNode占用空间也会比普通Node的大,如非必要,尽量还是使用链表。
- hash值如果足够随机,则在hash表内按泊松分布(呈正三角分布),在扩容因子0.75的情况下,长度超过8的链表出现的概率是0.00000006,选择8就是为了让树化几率足够小。
- 树化两个条件:① 链表长度超过树化阈值 ② 数组容量>=64
- 退化情况1:在扩容时如果拆分树时,树元素个数<=6则会退化链表
- 退化情况2:remove树节点时,若root、root.left、root.right、root.left.left有一个为null,也会退化为链表
介绍一下put方法流程,1.7与1.8有何不同?
put方法流程
- 创建数组(HashMap是懒惰创建数组的,首次put的时候才会创建数组。)
- 计算索引(桶下标)
- 如果桶下标还没人占用,创建Node占位返回
- 如果桶下标已经有人占用
- 已经是TreeNode走红黑树的添加或更新逻辑
- 是普通Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
- 返回前检查容量是否超过阈值
1.7和1.8的区别
- 链表插入节点时,1.7是头插法,1.8是尾插法
- 1.7是大于等于阈值且没有空位时才扩容,而1.8是大于阈值就扩容
源码:if (++size > threshold) resize(); - 1.8在扩容计算Node索引时,会优化(二次哈希)
多线程下会有啥问题?
扩容死链(1.7)
由于1.7在多线程下扩容的数据迁移是头插法导致链表末尾的元素的next指向上一个元素,从而形成死链。
数据错乱(1.7、1.8)
由于多线程在调用HashMap的putVal()方法时出现了变量覆盖问题,当两个线程同时进入putVal()方法,线程一进入判断语句判断桶下标为空,准备进行添加元素时,线程二进入判断语句,由于线程一还未放入元素,导致线程二判断结果也为空,于是在线程一添加元素后,线程二仍会添加元素至同一个位置,导致线程一添加的元素被覆盖,出现数据丢失。
**注:**可以用Collections的synchronizedMap方法使HashMap具有同步的能力。
key能否为null?作为key的对象有什么要求?
- HashMap的key可以为null,但Map的其他实现则不然
- 作为key的对象,必须实现hashCode和equals,并且key的内容不能修改(不可变),因为内容改变的话hashcode的值计算也会改变,导致第二次查找就找不到这个对象了。(存储的是hashcode值,通过hashcode去内存中找对象,hashcode值改了自然也就找不到了)
String对象的hashCode()如何设计的,为啥每次乘的是31
目标是达到较为均匀的散列效果,每个字符串的hashCode足够独特
- 字符串中的每个字符都可以表现为一个数字,称为Si,其中i的范围是0~n-1
- 散列公式为:S031n-1+S131n-2+...Sn-1*310
- 31代入公式有较好的散列特性,并且31h可以被优化为:32h-h、25*h-h、h<<5-h
# 请你比较一下Java和JavaSciprt?
JavaScript 与Java是两个公司开发的不同的两个产品。Java 是原Sun Microsystems公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发;而JavaScript是Netscape公司的产品,为了扩展Netscape浏览器的功能而开发的一种可以嵌入Web页面中运行的基于对象和事件驱动的解释性语言。JavaScript的前身是LiveScript;而Java的前身是Oak语言。
下面对两种语言间的异同作如下比较:
- 基于对象和面向对象:Java是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。
- 解释和编译:Java的源代码在执行之前,必须经过编译。JavaScript是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了JIT(即时编译)技术来提升JavaScript的运行效率)
- 强类型变量和类型弱变量:Java采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript的解释器在运行时检查推断其数据类型。
- 代码格式不一样。
# TCP/IP协议的三次握手和四次挥手
TCP报文格式
三次握手
参数
**SYN=1 **:申请建立连接
seq为序列号,递增,若返回的序列号小于接收的序列号则不合法
ACK=1 :确认发送信息有效,并返回ack包带上序列号seq+1,告诉对方你接下来要给我发的报文要从seq+1开始
握手含义
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
人话
- 第一次握手:客户发送请求
- 第二次握手:服务器接收到请求,向客户确认我这样做是否OK?(在这一次握手后服务器已经知道自己可以收到客户的消息,但不知道客户能不能收到自己消息)
- 第三次握手:客户接收服务器的确认包,回复服务器,这样就可以了
四次挥手
参数
FIN= 1 = 申请断开连接
挥手含义
第一次挥手:客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手:服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
第三次挥手:服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
人话
- 第一次挥手:客户发送断开申请
- 第二次挥手:服务器收到申请,告诉客户我还没准备好,请你等我消息
- 第三次挥手:服务器准备好后,向客户发送断开申请
- 第四次挥手:客户收到,告诉服务器好的,那我们断开吧
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
【问题3】为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
# 请说说网络七层模型?
7 层模型主要包括:
- **物理层:**该层提供网络传输的物理媒体,为网络之间的传输提供最基础的支持。重要的设备有中继器和集线器
- **数据链路层:**主要将从物理层接收的数据进行 MAC 地址(网卡的地址)的封装与解封装。常把这一层的数据叫做帧。在这一层工作的设备是交换机,数据通过交换机来传输。
- **网络层:**主要将从下层接收到的数据进行 IP 地址(例 192.168.0.1)的封装与解封装。在这一层工作的设备是路由器,常把这一层的数据叫做数据包。
- **传输层:**负责将上层数据进行分段提供端到端的可靠或不可靠传输。主要协议:TCP协议,UDP协议。(*)
- **会话层:**该层管理主机之间的会话进程,负责建立,维持,终止进程之间的会话。
- **表示层:**表示层的数据转换包括数据的加密,压缩,格式转换等。
- **应用层:**为网络应用程序提供访问网络服务的接口。 主要协议:HTTP协议(*)
面试话术
网络七层模型啊...我记得从下到上是物理层、数据链路层、网络层、传输层、会话层、表现层、应用层,比如TCP、UDP协议就是传输层的,HTTP协议就是应用层的,越是底层数据传输效率就越高,代码的一个耦合度也就越高,比如RPC通信就是传输层的,建立在TCP协议上,效率传输高,但是通用性差,而HTTP通信则是应用层的,效率略逊于RPC通信,但是通用性强,比如Feign的远程调用用的就是HTTP通信,对语言没有限制,只要是能提供HTTP协议接口就能使用,这也算微服务架构中语言解耦的其中一个原因吧。
# 什么是HTTP协议?
客户端和服务器端之间数据传输的格式规范,格式简称为“超文本传输协议”。是基于TCP/IP的关于数据如何在万维网中如何通信的协议。是一个基于请求与响应模式的、无状态的、应用层的协议,基于 TCP 的连接方式。
# Servlet的生命周期是什么?
生命周期: 对象的生命周期指一个对象从被创建到被销毁的整个过程。 
Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段:
- 加载和实例化:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
默认情况,Servlet会在第一次访问被容器创建,若想在服务器启动时创建可以在@WebServlet注解中添加属性
@WebServlet(urlPatterns = "/demo1",loadOnStartup = 1)
loadOnstartup的取值有两类情况
(1)负整数:第一次访问时创建Servlet对象
(2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高
2
3
4
5
初始化:在Servlet实例化之后,容器将调用Servlet实例的
init()方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化工作。该方法只调用一次调用一次请求处理:每次请求Servlet时,Servlet容器都会调用Servlet实例的
service()方法对请求进行处理服务终止:当需要释放内存或者容器关闭时,容器就会调用Servlet实例的
destroy()方法完成资源的释放。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器回收。
**补充:**Servlet接口还有两个方法
public String getServletInfo()获取Servlet信息
//该方法用来返回Servlet的相关信息,没有什么太大的用处,一般我们返回一个空字符串即可
public String getServletInfo() {
return "";
}
2
3
4
public ServletConfig getServletConfig()获取ServletConfig对象
ServletConfig对象,在init方法的参数中有,而Tomcat Web服务器在创建Servlet对象的时候会调用init方法,必定会传入一个ServletConfig对象,我们只需要将服务器传过来的ServletConfig进行返回即可。
/**
* Servlet方法介绍
*/
@WebServlet(urlPatterns = "/demo3",loadOnStartup = 1)
public class ServletDemo3 implements Servlet {
private ServletConfig servletConfig;
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
System.out.println("init...");
}
public ServletConfig getServletConfig() {
return servletConfig;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
loadOnStartup = 1 :在服务器启动时创建Servlet实例。小于0为不创建,大于0,数字越小优先级越高
# Servlet实现原理
体系结构
因为我们开发的是B/S架构的web项目,都是针对HTTP协议,所以选择使用自定义Servlet,通过继承HttpServlet来实现
Servlet
Servlet是接口,声明了以下方法:
1). 构造器: 只被调用一次. 只有第一次请求 Servlet 时, 创建 Servlet 的实例. 调用构造器. 这说明 Serlvet 的单实例的!
2). init 方法: 只被调用一次. 在创建好实例后立即被调用. 用于初始化当前 Servlet.
3). service: 被多次调用. 每次请求都会调用 service 方法. 实际用于响应请求的.
4). destroy: 只被调用一次. 在当前 Servlet 所在的 WEB 应用被卸载前调用. 用于释放当前 Servlet 所占用的资源.
下面两个方法是开发者调用
1). getServletConfig: 返回一个ServletConfig对象,其中包含这个servlet初始化和启动参数.
2). getServletinfo: 返回有关servlet的信息,如作者、版本和版权.
GenericServlet
- 是一个 Serlvet. 是 Servlet 接口和 ServletConfig 接口的实现类. 但是一个抽象类. 其中的 service 方法为抽象方法
- 如果新建的 Servlet 程序直接继承 GenericSerlvet 会使开发更简洁.
具体实现:
①. 在 GenericServlet 中声明了一个 SerlvetConfig 类型的成员变量, 在 init(ServletConfig) 方法中对其进行了初始化
②. 利用 servletConfig 成员变量的方法实现了 ServletConfig 接口的方法
③. 还定义了一个 init() 方法, 在 init(SerlvetConfig) 方法中对其进行调用, 子类可以直接覆盖 init() 在其中实现对 Servlet 的初始化.
④. 不建议直接覆盖 init(ServletConfig), 因为如果忘记编写 super.init(config); 而还是用了 SerlvetConfig 接口的方法,则会出现空指针异常.
⑤. 新建的 init(){} 并非 Serlvet 的生命周期方法. 而 init(ServletConfig) 是生命周期相关的方法.
HttpServlet!!!
1). 是一个 Servlet, 继承自 GenericServlet. 针对于 HTTP 协议所定制.
2). 在 service() 方法中直接把 ServletReuqest 和 ServletResponse 转为 HttpServletRequest 和HttpServletResponse.并调用了重载的 service(HttpServletRequest, HttpServletResponse)
在 service(HttpServletRequest, HttpServletResponse) 获取了请求方式: request.getMethod(). 根据请求方式有创建了doXxx() 方法(xxx 为具体的请求方式, 比如 doGet, doPost)
**简单来讲:**Servlet是一个接口,定义了一套规范,而GenericServlet则是实现了部分规范的抽象类,也就是实现了Servlet的一些初始化、销毁、参数处理的方法,唯独service没有实现,也就是Servlet的核心方法,根据不同的需求达到不同的实现,而我们经常用的就是针对HTTP协议的实现,也就是HttpServlet,HttpServlet实现了service方法并针对请求方式的不同区分了get和post的方法调用,当然并不限于这两种请求。
# get和post请求的区别
首先Get和Post是Http协议中的两种请求方式,由于HTTP协议对两者的使用做出了规范,以至于出现了以下几种区别:
从安全性来说:
- Get是不安全的,因为数据被放在请求行的URL中;而Post的请求数据放在请求体中。
- GET请求参数会被完整保留在浏览器历史记录里,相当于缓存,可减少浏览器压力,而POST中的参数不会被保留。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。其实从传输的角度上看两者都是不安全的,就算是post提交,只要f12什么都一目了然,比较安全的就是需要加密,比如HTTPS协议
从传输的数据来说:
- Get传送的数据量较小,传输数据大小根据浏览器的不同会略有差异,一般在1k-18k左右,主要是因为服务器处理长 URL 要消耗比较多的资源,为了性能和安全(防止恶意构造长 URL 来攻击)考虑,会给 URL 长度加限制;Post传送的数据量较大,一般被默认为不受限制。
- GET请求只能进行url编码,而POST支持多种编码方式。
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
效率上来说:
- Get执行效率要比Post好,因为POST请求包含更多的请求头,这是其中原因之一。
- post请求的数据在三次握手后,才开始传输数据,而get请求的数据是在第三次握手时就与请求头一起发送到服务器了,所以效率要更高。
实际开发中,一般不敏感的数据,比如分页查询的页码可以放在URL地址中,而一些敏感信息比如交易记录的传递可以放在请求体中,交易记录的多条件分页查询,post请求中在url地址后带上参数,通过ajax传递给后台的数据,当前页码以及每页显示的页码可以放在url中,而查询的条件封装成json对象放在请求体中传递。
# TCP 与 UDP 区别?
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。
UDP是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中的传输层协议,它是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
TCP和UDP都是来自于传输层的协议。传输层位于应用层和网络层之间,负责位于不同主机中进程之间的通信。
TCP协议特点: 传输控制协议 (Transmission Control Protocol)
- 需要连接
- 速度慢
- 没有大小限制
- 不易丢失数据
TCP协议通信场景(速度要求不高,数据完整性要求高)
- 下载
- 扫码支付
- 金融等数据通信。
UDP协议特点: 用户数据报协议(User Datagram Protocol)
- 不需要连接
- 速度快
- 有大小限制一次最多发送64K
- 易丢失数据
UDP协议通信场景(速度要求高,数据完整性要求不高)
- 直播
- 语音通话
- 视频会话
TCP 与 UDP 区别
- TCP基于连接UDP无连接
- TCP速度慢,UDP速度快
- TCP保证数据正确性,UDP可能丢包
- TCP保证数据顺序,UDP不保证
- TCP传输文件没有大小限制,UDP一次最多发送64K
- TCP适用于下载、金融交易等场景,UDP适用于直播、语音通信等场景
# HTTP中重定向和请求转发的区别
请求转发和重定向主要有以下几个区别:
- 请求转发是服务器端的跳转 重定向是浏览器跳转
- 请求转发地址栏是不会发生变化的 重定向地址栏会改变
- 请求转发是1次请求 重定向是2次请求
- 请求转发只能跳转到服务器内部资源,而重定向可以内部和外部资源都可以访问
- 请求转发能做到数据共享重定向不行,若重定向要做到数据共享,则需要将数据存储到session域或者是application域中。
两者的使用场景
- 跳转到下一个页面,不需要传递数据,建议使用重定向
- 跳转到下一个页面,并且要把数据传递给下一个页面,转发
- 跳转到下一个页面,并且将来的数据要给下一个页面及其他很多页面,重定向+session
- 仅仅只需要进行一个内部资源跳转,没有其他要求,建议使用转发,减少服务器访问次数与请求响应次数,从而减少服务器负担
# Jsp和Servlet的区别
Jsp本质上就是一个Servlet,Java虚拟机jvm并不能够识别它,所以web容器像tomcat会将jsp翻译成servlet后执行servlet生命方法service处理客户端请求响应数据。其实就是当你通过 http 请求一个 JSP 页面是,首先 Tomcat 会将JSP翻译并编译成为 Servlet,然后执行 Servlet的生命周期方法处理请求与响应。Servlet是一个接口,也就是一种规范,具体实现由服务器提供。
主要的不同在于:
- Servlet中没有内置对象 。
- JSP中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到。
- jsp是servlet的一种简化,使用jsp程序员只需要编写输出到客户端的内容,至于jsp中的java脚本如何镶嵌到servlet中,由jsp容器完成。
- JSP侧重视图展现数据,Sevlet主要用于控制逻辑获取数据。
# cookie和session的区别
cookie是客户端会话跟踪技术,session是服务端会话跟踪技术
cookie和session的区别主要有以下几点:
1.存储位置不同和隐私策略不同
cookie的数据信息存放在客户端浏览器上,对客户端是可见的,相对来说是不安全的。而session的数据信息存放在服务器上,会相对安全很多。
2.有效期上不同
默认情况下当浏览器关闭,内存释放,则Cookie被销毁,可以通过setMaxAge来设置过期时间,参数为正数:将 Cookie写入浏览器所在电脑的硬盘,持久化存储,到时间自动删除;负数:默认值,Cookie在当前浏览器内存中,当浏览器关闭,则 Cookie被销毁;零:立即删除对应 Cookie。
session的默认存活时间为30分钟,可以通过在web.xml中配置session-timeout来修改,有些人会说会话结束,session也随之失效,其实这并不准确,session数据共享是基于cookie实现的,依赖于名为JSESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,也就是当此次会话结束,该session的JSESSIONID cookie就会失效,并不是这个session会失效,session还存储在服务器中,只是会话结束了已经无法访问到这个session了,只有到30分钟,这个session才会销毁。
3.存储容量不同和存储方式的不同
单个cookie保存的数据<=4KB,一个站点一般保存20~50个Cookie(不同浏览器不一样,Sarafi和Chrome对每个域的Cookie数目没有严格限制)。
对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东 西,并且设置session删除机制(或者采用缓存技术代替session)。
cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。
session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。
4.服务器压力不同
cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。
session是保管在服务器端的,每个用户都会产生一个session。假如并发访问的用户十分多,会产生十分多的session,耗费大量的内存。
在实际开发过程中,如果涉及到账户权限的问题,我会采用session的方式,判断session的值是否存在,如果存在那么表示当前用户登录,不做拦截,如果不存在表示当前用户没有登录则拦截跳转到登录页面。如果是想把信息保存到浏览器,例如记住密码 自动登录等,可以采用cookie的形式并且设置过期时间保存在浏览器上。
# Ajax
概述:AJAX (Asynchronous JavaScript And XML):异步的 JavaScript 和 XML
作用有两方面:
- 与服务器进行数据交换:通过AJAX可以给服务器发送请求,服务器将数据直接响应回给浏览器。
可以通过AJAX和服务器进行通信,已达到使用HTML+AJAX来替换JSP页面的效果。如下图,浏览器发送请求servlet,servlet调用完业务逻辑层后将数据直接响应回给浏览器页面,页面使用HTML来进行数据展示。
- 异步交互技术:可以在不重写加载整个页面的情况下,与服务器交换数据并更新部分网页的技术,如:搜索联想、用户名是否可以校验,等等...
三个特点
- 并行操作:浏览器与服务器是并行操作的,浏览器在工作的时候,服务器也可以工作。
- 后台发送:浏览器的请求是后台js发送给服务器的,js会创建单独的线程发送异步请求,这个线程不会影响浏览器的线程运行。
- 局部刷新:浏览器接收到结果以后进行页面局部刷新
原生的Ajax使用依赖于js的XMLHttpRequest对象,相关方法:
| 属性 | 描述 |
|---|---|
| onreadystatechange | 定义当 readyState 属性发生变化时被调用的函数 |
| readyState | 保存 XMLHttpRequest 的状态。 - 0:请求未初始化 - 1:服务器连接已建立 - 2:请求已收到 - 3:正在处理请求 - 4:请求已完成且响应已就绪 |
| status | 返回请求的状态号 - 200: "OK" - 403: "Forbidden" - 404: "Not Found" |
| responseText | 服务器响应的字符串数据 |
# javaweb三大组件
Servlet: 作为一个中转处理的容器,他连接了客户端和服务器端的信息交互和处理。用于处理请求与响应 servlet中有service方法 用户每次的请求响应都会触发它
**Filter: **可以理解一个一种特殊Servlet,主要用于 用于拦截请求与响应,对用户请求进行预处理,也可以对HttpServletResponse进行后处理是一个典型的处理链过滤请求,无法向用户生成响应。
doFilter():Filter的主要方法,用来完成过滤器主要功能的方法,每次访问目标资源时都会调用。
**Lisenter: **用于监听三大域对象request、session、servletContext的创建与销毁,和域中数据放生变化的时候会调用监听器实现逻辑控制。
启动顺序:监听器>过滤器>Servlet
应用场景:
lisenter:可以来做统计在线人数、加载初始化信息、统计网站访问量、监控访问信息、电商网站定时扫描,服务器启动后每隔两分钟清除超时订单
Filter:权限控制、全局的请求编码设置
Servlet:主要用来在业务处理之前进行控制url传来之后,就对其进行处理。处理完成,返回或转向到某一自己指定的页面,可以向用户生成响应
# Filter的执行流程
- 启动服务器时加载过滤器的实例,并调用init()方法来初始化实例;
- 每一次请求时都只调用方法doFilter()进行处理;
- 停止服务器时调用destroy()方法,销毁实例。
访问过滤器拦截的路径,进入过滤器,首先拦截请求,其次放行给目标资源去执行,目标资源执行后会返回到过滤器中,最后执行拦截响应。
若是过滤器链的情况,注解配置根据Filter类名的字符串自然排序进行执行,xml配置则是根据从上到下的配置顺序。
# 深拷贝和浅拷贝的区别?怎么实现深拷贝?
面试话术
浅拷贝默认情况下会拷贝出一个新对象,其中的基本数据类型则是拷贝值,引用数据类型的话则会拷贝地址,但String类型会重新分配一个内存空间。
深拷贝则是会给所有的引用类型重新分配内存地址。实现深拷贝需要实现Cloneable接口并重写clone() 方法,并且在 clone 方法内部,把该对象引用的其他对象也要 clone 一份,这就要求这个被引用的对象必须也要实现Cloneable 接口并且实现 clone 方法。
# 什么是序列化和反序列化?什么时候需要序列化或反序列化?序列化的底层怎么实现的?
面试话术
java中的序列化就是将对象转为二进制数据,反序列化就是将二进制数据转为对象嘛
需要把Javabean对象写进磁盘的时候就需要序列化,比如使用object流的时候,又或是,呃,我记得开启mybatis二级缓存的时候也是需要将表的实体实现序列化接口。还有让Javabean对象通过网络传输的时候,像RPC远程调用就用的对象传输,就需要序列化。
java实现序列化的底层就是对象流嘛,那个object output inputstream,writeObject readObject。
# 二叉查找树、平衡二叉树、红黑树的区别?
面试话术
二叉查找树,就是左边小右边大这样存储,然后二叉查找树的问题是如果是顺序插入的话,会退化成链表,性能很差,然后就出来了平衡二叉树,就左子树和右子树的高度差如果大于1就会做一个旋转,平衡,但是平衡二叉树的问题在对于频繁插入的场景,会需要频繁的进行旋转,对性能消耗较大,最后就出现了红黑树这种数据结构,红黑树的根节点都是黑色且不存储数据,任何相邻的节点都不能为红色,任何从根节点到达其可达的叶子节点都是相等的,就是经过的黑节点都是一样的,基于这些特点,红黑树就优化了平衡二叉树的频繁旋转问题,红黑树的旋转频率会低很多,所以性能也更好,HashMap在jdk1.8后也引进了这个数据结构来优化hash表。不过红黑树也是存在一些问题的,比如黑节点不存储数据,所以会导致整个结构比较占空间,而且数据量大的话层级会比较深,也是个问题,再优化的话可能要到BTree那边了