目录
- 一.前言
- 二.Java基础面试篇
- 1. 什么是Java?
- 2.Java 和 C++的区别?
- 3.Java中常用的注释以及作用?
- 4.标识符的命名规则
- 5.JVM、JRE和JDK的关系
- 6.Oracle JDK 和 OpenJDK 的对比
- 7.Java中基本数据类型
- 8.int 和 Integer 有什么区别
- 9.switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?
- 10.short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
- 11.&和&&的区别
- 12.break ,continue ,return 的区别及作用
- 13.在 Java 中,如何结束多重嵌套循环呢?
- 14.Java是什么类型的语言?
- 14.1 编译型语言和解释型语言的定义
- 14.2 编译型语言和解释型语言的区别
- 14.3 编译型语言和解释型语言都有哪些?
- 14.4 Java到底是编译型语言还是解释型语言?
- 15.面向对象和面向过程的区别
- 16.面向对象的特征主要有以下几个方面:
- 16.1 继承
- 16.2 封装
- 16.3 多态
- 17.instanceof关键字的作用
- 18.请问下面代码中两个数是否交换成功?
- 19.Java中重载和重写有哪些区别?
- 19.1 方法重载
- 19.2 方法重写
- 20.static都有哪些用法?
- 21.final 有什么用?
- 22.怎样声明一个类不会被其它类继承,什么场景下会用
- 23.Java为什么有单继承,但是可以多实现接口?
- 24.抽象类和接口有哪些区别
- 24.1 抽象类和接口不同之处(JDK8之前)
- 抽象类:
- 接口:
- 拓展补充:(JDK8之后,包含JDK8)
- 24.2 抽象类和接口相同之处
- 25.介绍下内部类
- 26.内部类的优点
- 27.内部类有哪些应用场景
- 28.局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?
- 29.内部类相关,看程序说出运行结果
- 30.Object中的常用方法
- 31.深拷贝和浅拷贝的区别是什么?
- 32.Integer a= 127 与 Integer b = 128相等吗?
- 33.== 和 equals 的区别是什么?
- 34.hashCode的作用
- 35.hashcode和equals如何使用
- 36.解决hash冲突的方法?
- 37.String有哪些特性?
- 38.String真的是不可变的吗?
- 39.String 和StringBuffer、StringBuilder的区别是什么?String 为什么是不可变的?
- 40.String是最基本的数据类型吗?
- 41.是否可以继承 String 类?
- 42.String str="i"与 String str=new String(“i”)一样吗?
- 43.在使用 HashMap 的时候,用 String 做 key 有什么好处?
- 44.String类常用的方法有哪些?
- 45.Java异常关键字
- 46.try-catch-finally 中,如果 catch 中 return 了, finally 还会执行吗?
- 47.异常处理影响性能吗?
- 48.Comparable 和 Comparator的区别?
- 49.Java中集合框架图
- 50.List 和 Set 的区别?
- 51.ArrayList的优缺点?
- 52.ArrayList 和 Vector 的区别是什么?
- 53.ArrayList 和 LinkedList 的区别是什么?
- 54.Java集合的快速失败机制 “fail-fast”?
- 55.Iterator 和 ListIterator 有什么区别?
- 56.为什么 ArrayList 的 elementData 加上 transient 修饰?
- 57.HashMap的底层数据结构?
- 58.HashMap中从获取对象的hash到散列计算规则?
- 59.默认初始化大小是多少?为啥大小都是2的幂?
- 60.HashMap 的长度为什么是2的幂次方?
- 61.为什么String, Interger类适合作为键?
- 62.能否使用任何类作为 Map 的 key?
- 63.HashMap的主要参数都有哪些?
- 64.哈希冲突基本概念以及HashMap解决冲突的方法
- 65.为啥我们重写equals方法的时候需要重写hashCode方法呢?
- 66.HashMap什么时候进行扩容?它是怎么扩容的呢?
- 67.JDK1.7扩容的时候为什么要重新Hash呢,为什么不直接复制过去?
- 68.HashMap和Hashtable的区别是什么?
- 69.HashMap的工作原理?
- 70.HashMap的table的容量如何确定?loadFactor是什么?该容量如何变化?这种变化会带来什么问题?
- 71.HashMap中put方法的过程?
- 72.HashMap数组扩容的过程?
- 73.拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
- 74.说说你对红黑树的见解?
- 75.JDK8中对HashMap做了哪些改变?
- 76.jdk1.8中做了哪些优化优化?
- 77.HashMap的不安全体现在哪里?
- 78.HashMap中容量的初始化
- 79.HashMap是怎么解决哈希冲突的?
- 80.HashMap线程安全的方式?
- 81.介绍一下强引用、软引用、弱引用、虚引用的区别?
- 82.什么是反射机制?
- 83.反射机制优缺点
- 84.反射机制的应用场景有哪些?
- 85.Java获取反射的三种方法
- 86.Java创建对象有几种方式?
- 87.什么是静态代理?
- 88.什么是动态代理?
- 89.JDK动态代理和CGLIB包实现动态代理的区别?
- 89.1 JDK动态代理
- 89.1.1 JDK动态代理步骤
- 89.1.2 概括静态代理和动态代理区别
- 89.2 CGLIB包实现动态代理
- 89.2.1 核心思想
- 89.2.2 CGLIB代理步骤
- 89.2.3 CGLIB在进行代理的时候都进行了哪些工作
- 89.2.4 CGLIB中方法的调用还是通过反射吗?
- 90.JDK动态代理和CGlib动态代理哪一个更快呢?
- 91.Spring如何选择JDK动态代理还是CGLIB动态代理?
- 92.说说Java中实现多线程的几种方法
- 93.线程中的常用方法你知道吗?
- 94.Thread类中yield方法的作用
- 95.为什么wait, notify和notifyAll这些方法不在thread类里面?
- 96.为什么wait和notify方法要在同步块中调用?
- 97.操作系统中进程和线程的关系
- 98.线程安全需要保证几个基本特性?
- 99.什么是线程安全
- 100.说下线程间是如何通信的?
- 101.说下进程间是如何通信的?
- 102.怎么保证多线程的并发安全?
- 103.说说ThreadLocal的原理
- 104.什么是线程同步?线程同步的概念?
- 105.线程同步的方式?
- 106.Java中线程的6种状态(源码中有体现)
- 107.Java中常用的锁以及原理
- 108.Synchronized和ReentrantLock的区别
- 109.线程池的使用规范
- 110.线程池的7个参数你都知道吗?
- 111.线程池拒绝策略
- 112.线程池的5种状态
- 113.线程池的执行流程
- 114.常用的线程池有哪些?
- 115.线程池有哪几种创建方式,能详细的说下吗?
- 115.1 俩种方式、7种方法
- 115.2 通过 ThreadPoolExecutor 创建的线程池
- 115.3 通过 Executors 创建的线程池
- 116.介绍下IO流的分类
- 117.Files的常用方法都有哪些?
- 118.同步、异步、阻塞、非阻塞的概念
- 119.BIO---Blocking IO---阻塞IO
- 119.1 什么是BIO?
- 119.2 BIO中的阻塞是什么意思?
- 119.3 BIO底层是怎么实现的?原理是什么?
- 119.4 BIO的优势以及存在的问题?
- 120.NIO---New IO---非阻塞IO
- 120.1 什么是NIO?
- 120.2 为什么需要NIO?BIO从NIO演进的过程?
- 120.3 NIO的优势之处以及存在的问题?
- 121.什么是AIO?
- 122.Java中JDK必知必会的NIO网络编程
- 123.面试常考的网络编程之Socket
- 124.JDK8新特性之Stream流
- 125.JDK8新特性之Lambda 表达式
- 126.JDK8新特性之方法与构造函数引用
- 127.JDK8新特性之日期时间API-案例实操
- 128.什么jsp?
- 129.什么是Servlet?
- 130.什么是Servlet规范?
- 131.servlet的生命周期
- 132.jsp和servlet的区别
- 133.jsp九大内置对象
- 134.jsp的四大作用域
- 135.说说servlet的原理
- 136.Servlet是线程安全的吗?
- 137.什么是cookie?有什么作用?
- 138.什么是session? 有什么作用?
- 139.cookie与session区别
- 140.禁用客户端cookie,如何实现session?
- 141.HTTP请求/响应的结构是怎么样的?
- 142.HTTP中GET和POST请求的区别?
- 143.Forward和Redirect的区别?
- 144.web.xml在web项目中有什么作用?
- 三.下节预告
- 四.共勉
一.前言
一年中的三月和四月、九月和十月是跳槽涨薪的黄金期,这个时间公司有月很多的空缺岗位,同时也会有很多的求职者想要抓住这个机会寻求一份满意的工作。那么问题来了,怎么才能才面试中表现优异呢?我觉得因素有很多,硬实力和软实力都要具备,首先,我们应该考虑的就是硬实力,那么硬实力中又分为好多的模块知识,这个我觉得是因人而异的,但是万变不离其中的,或者说面试第一道门槛的就是面试必须掌握的八股文
,面试就像闯关一样,你需要一关一关的过,现在第一关就是八股文,你想要拿到心仪的offer,那么就必须先解决掉这个问题。当然有的读者也和作者反映,能不能出一个专题,主要内容就是2023年金三银四面试中常问的问题,做一个总结汇总。好的,既然大家有这样的诉求,那么作为宠粉狂魔的我,必须给大家安排起来。
二.Java基础面试篇
1. 什么是Java?
Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了 C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
2.Java 和 C++的区别?
- 都是面向对象的语言,都支持封装、继承和多态;
- Java 不提供指针来直接访问内存,程序内存更加安全;
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承;
- Java 有自动内存管理机制,不需要程序员手动释放无用内存。
3.Java中常用的注释以及作用?
在程序中,尤其是复杂的程序中,适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果产生任何影响。
注意事项:多行和文档注释都不能嵌套使用。
- 单行注释
// 注释文字
- 多行注释
/* 注释文字 */
- 文档注释
/** 注释文字 */
4.标识符的命名规则
标识符的含义: 是指在程序中,我们自己定义的内容,比如类的名字,方法名称以及变量名称等等,都是标识符。
命名规则(硬性要求): 标识符可以包含英文字母,0-9的数字,$以及_ ,标识符不能以数字开头,标识符不是关键字
命名规范(非硬性要求): 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。 变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。 方法名规范:同变量名。
5.JVM、JRE和JDK的关系
JVM :Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平 台有自己的虚拟机,因此Java语言可以实现跨平台。
JRE:Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核 心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数 据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包。如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK:Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发 工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工 具:编译工具(javac.exe),打包工具(jar.exe)等
6.Oracle JDK 和 OpenJDK 的对比
- Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一 次;
- OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是 OpenJDK的一个实现,并不是完全开源的;
- Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎 相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企 业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳 定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程 序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题;
- 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的 性能;
- Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过 更新到最新版本获得支持来获取最新版本;
- Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。
7.Java中基本数据类型
Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
基本类型 | 字节 | 默认值 | 封装类 |
---|---|---|---|
byte | 1 | (byte)0 | Byte |
short | 2 | (short)0 | Short |
int | 4 | 0 | Integer |
long | 8 | 0l | Long |
float | 4 | 0.0f | Float |
double | 8 | 0.0d | Double |
boolean | - | false | Boolean |
char | 2 | \u0000(null) | Character |
注意:
在Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位。这样我们可以得出boolean类型占了单独使用是4个字节,在数组中又是1个字节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数是32位(这里不是指的是32/64位系统,而是指CPU硬件层面),具有高效存取的特点。
8.int 和 Integer 有什么区别
- Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型,int 的包装类就是Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
- int是基本数据类型,Integer是int的封装类,是引用类型。int默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一旦java看到null,就知道这个引用还没有指向某个对象,再任何引用使用前,必须为其指定一个对象,否则会报错。
- 基本数据类型在声明时系统会自动给它分配空间,而引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。
9.switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?
在 Java 5 以前,switch(expression)中,expression只能是 byte、short、char、int。从Java5 开始,Java 中引入了枚举类型,expression也可以是 enum 类型,从 Java 7开始,expression还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
10.short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
而对于 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1+ 1);其中有隐含的强制类型转换。
11.&和&&的区别
-
&运算符有两种用法:按位与和逻辑与。
-
&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。&&之所以称为短路运算,是因为如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
12.break ,continue ,return 的区别及作用
break 跳出总上一层循环,不再执行循环,结束当前的循环体 ;
continue 跳出本次循环,继续执行下次循环,结束正在执行的循环 进入下一个循环条件 ;
return 程序返回,不再执行下面的代码,结束当前的方法并直接返回;
13.在 Java 中,如何结束多重嵌套循环呢?
在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号/标签,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:
public class Main{
public static void main(String[] args) {
ok:
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
if (i+j==100) {
break ok;
}
}
}
}
}
14.Java是什么类型的语言?
14.1 编译型语言和解释型语言的定义
- 编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。
- 解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束!
14.2 编译型语言和解释型语言的区别
- 编译型语言,执行速度快、效率高;依靠编译器、跨平台性差些。
- 解释型语言,执行速度慢、效率低;依靠解释器、跨平台性好。
14.3 编译型语言和解释型语言都有哪些?
- 编译型的语言包括:C、C++、Delphi、Pascal、Fortran
- 解释型的语言包括:Java、Basic、javascript、python
14.4 Java到底是编译型语言还是解释型语言?
java既是编译型的,也是解释型的
- 编译型:所有的Java代码都是要编译的,.java不经过编译就什么用都没有。
- 解释型:java代码编译后不能直接运行,它是解释运行在JVM上的,所以它是解释运行的,那也就算是解释的了。
15.面向对象和面向过程的区别
面向过程(OOP):是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候调用则可。性能较高,所以单片机、嵌入式开发等一般采用面向过程开发。
面向对象(AOP):是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。 但是性能上来说,比面向过程要低。
16.面向对象的特征主要有以下几个方面:
Java 面向对象编程三大特性:封装 继承 多态
16.1 继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。关于继承需要注意如下事项:
- 子类拥有父类非 private 的属性和方法。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
16.2 封装
封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。关于封装需要注意如下事项:
- 封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们不用提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
- 隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
- 属性的封装:使用者只能通过事先定制好的方法来访问数据,可以方便地加入逻辑控制,限制对属性的 不合理操作;
- 方法的封装:使用者按照既定的方式调用方法,不必关心方法的内部实现,便于使用; 便于修改,增强代码的可维护性;
16.3 多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 关于多态需要注意如下事项:
- 在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
- 方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
- 一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
- 相比于封装和继承,Java多态是三大特性中比较难的一个,封装和继承最后归结于多态, 多态指的是类和类的关系,两个类由继承关系,存在有方法的重写,故而可以在调用时有父类引用指向子类对象。多态必备三个要素:继承,重写,父类引用指向子类对象。
17.instanceof关键字的作用
instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例。具体用法如下:
boolean result = obj instanceof Class;
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定。
int test = 1;
System.out.println(test instanceof Integer);//编译不通过 test 必须是引用类型,不能是基本类型
System.out.println(test instanceof Object);//编译不通过
Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true
//false ,在JavaSE规范中对instanceof 运算符的规定就是:如果 obj 为 null,那么将返回 false。
System.out.println(null instanceof Object); // false
18.请问下面代码中两个数是否交换成功?
考察的点:Java是值传递还是引用传递
public class Test{
public static void main(String[] args){
int a=3;
int b=4;
System.out.println("输出交换前两个数:"+a+"---"+b);
swap(a,b);
System.out.println("输出交换后两个数:"+a+"---"+b);
}
public static void swap(int num1,int num2){
int t;
t=num1;
num1=num2;
num2=t;
}
}
如果你实现了上面的代码,那会你就会知道,交换前后输出a和b的值是相等的,我们就可以知道Java是值传递。
- 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
- 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引 用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说 传递前和传递后都指向同一个引用(也就是同一个内存空间)。
19.Java中重载和重写有哪些区别?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
19.1 方法重载
方法重载:发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同、参数列表的顺序或者三者都不同)则视为重载;方法重载的规则如下:
- 方法名一致,参数列表中参数的顺序,类型,个数不同。
- 重载与方法的返回值无关,存在于父类和子类,同类中。
- 可以抛出不同的异常,可以有不同修饰符
19.2 方法重写
方法重写:发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。方法重写的规则如下:
- 参数列表必须完全与被重写方法的一致。
- 构造方法不能被重写,声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,但是能够被再次声明。
- 访问权限修饰符子类大于等于父类中被重写的方法的访问权限。
- 子类返回异常范围小于等于父类异常范围。
- 子类的返回值类型小于等于父类的返回值类型。
20.static都有哪些用法?
- static关键字修饰静态变量和静态方法.也就是被static所修饰的变量和方法都属于类的静态资源,类实例所共享。
- static也用于静态块,多用于初始化操作:
- static也多用于修饰内部类,此时称之为静态内部类.
- 静态导包,即 import static .import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名。
21.final 有什么用?
用于修饰类、属性和方法
- 被final修饰的类不可以被继承 ;
- 被final修饰的方法不可以被重写 ;
- 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
22.怎样声明一个类不会被其它类继承,什么场景下会用
如果一个类被final修饰,此类不可以有子类,不能被其它类继承,如果一个中的所有方法都没有重写的需要,当前类没有子类也罢,就可以使用final修饰类,最常用的就是Math类。
23.Java为什么有单继承,但是可以多实现接口?
多实现方法不会冲突,多继承方法内容冲突。
24.抽象类和接口有哪些区别
24.1 抽象类和接口不同之处(JDK8之前)
抽象类:
- 抽象类中可以定义构造器;
- 可以有抽象方法和具体方法;
- 抽象类中的成员可以是 private、默认、protected、public;
- 抽象类中可以定义成员变量;
- 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法;
- 抽象类中可以包含静态方法;
- 一个类只能继承一个抽象类;
接口:
- 接口中不能定义构造器;
- 方法全部都是抽象方法;
- 接口中的成员全都是 public 的;
- 接口中定义的成员变量实际上都是常量;
- 接口中不能有静态方法;
- 一个类可以实现多个接口;
拓展补充:(JDK8之后,包含JDK8)
接口中新增加了静态方法、默认方法
24.2 抽象类和接口相同之处
- 不能够实例化;
- 可以将抽象类和接口类型作为引用类型;
- 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
25.介绍下内部类
目的:提高安全性。在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这三种:成员内部类、局部内部类、匿名内部类,如下图所示:
26.内部类的优点
我们为什么要使用内部类呢?因为它有以下优点:
- 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
- 内部类不为同一包的其他类所见,具有很好的封装性;
- 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
- 匿名内部类可以很方便的定义回调。
27.内部类有哪些应用场景
-
一些多算法场合
-
解决一些非面向对象的语句块。
-
适当使用内部类,使得代码更加灵活和富有扩展性。
-
当某个类除了它的外部类,不再被其他的类使用时。
28.局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢? 它内部原理是什么呢? 先看这段代码:
public class Outer {
void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}
以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在 栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变 量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final, 可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。
29.内部类相关,看程序说出运行结果
public class Outer {
private int age = 6;
class Inner {
private int age = 3;
public void print() {
int age = 5;
System.out.println("局部变量:" + age);
System.out.println("内部类变量:" + this.age);
System.out.println("外部类变量:" + Outer.this.age);
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.print();
}
}
运行结果:
1 局部变量:5
2 内部类变量:3
3 外部类变量:6
30.Object中的常用方法
Object 是所有类的根,是所有类的父类,所有对象包括数组都实现了 Object 的方法。
-
clone():实现对象的浅复制,只有实现了 Cloneable 接口才可以调用该方法,否则抛出CloneNotSupportedException 异常,深拷贝也需要实现 Cloneable,同时其成员变量为引用类型的也需要实现 Cloneable,然后重写 clone 方法。
-
equals():一般 equals 和 == 是不一样的,但是在 Object 中两者是一样的。子类一般都要重写这个方法。
-
hashCode():该方法用于哈希查找,重写了 equals 方法一般都要重写 hashCode 方法,这个方法在一些具有哈希功能的集合中用到。一般必须满足 obj1.equals(obj2)==true 。可以推出 obj1.hashCode()==obj2.hashCode() ,但是hashCode 相等不一定就满足 equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
-
wait():配合 synchronized 使用,wait 方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait() 方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。调用该方法后当前线程进入睡眠状态,直到以下事件发生。
- 其他线程调用了该对象的 notify 方法;
- 其他线程调用了该对象的 notifyAll 方法;
- 其他线程调用了 interrupt 中断该线程;
- 时间间隔到了。此时该线程就可以被调度了,如果是被中断的话就抛出一个 InterruptedException 异常。
- notify():配合 synchronized 使用,该方法唤醒在该对象上等待队列中的某个线程,同步队列中的线程是给抢占 CPU 的线程,等待队列中的线程指的是等待唤醒的线程。
- notifyAll():配合 synchronized 使用,该方法唤醒在该对象上等待队列中的所有线程。
- toString():将对象格式化输出为一个字符串。
- getClass():通过getClass()方法可以得到一个Class类的对象。
- finalize():该方法和垃圾收集器有关系,判断一个对象是否可以被回收的最后一步就是判断是否重写了此方法。
31.深拷贝和浅拷贝的区别是什么?
-
浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象.
-
深拷贝:被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他对象的变量将指向被复制过的新对象.而不再是原有的那些被引用的对象.换言之.深拷贝把要复制的对象所引用的对象都复制了一遍.
32.Integer a= 127 与 Integer b = 128相等吗?
对于对象引用类型:==比较的是对象的内存地址。
对于基本数据类型:比较的是值。如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer 对象,而是直接引用常量池中的Integer对象,超过范围 a1b1的结果是false
public class Main{
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true
Integer a1 = 128;
Integer b1 = 128;
System.out.println(a1 == b1); // false
Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}
}
33.== 和 equals 的区别是什么?
equals 和== 最大的区别是一个是方法一个是运算符。
== : 它的作用是判断两个对象的地址是不是相等。也就是判断两个对象是不是同一个对象。如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
equals() : 它的作用也是判断两个对象是否相等。equals()的使用需要注意如下三种情况:
- equals 方法不能用于基本数据类型的变量。
- 类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true 。
34.hashCode的作用
- 当我们在Java中的set集合中插入的时候怎么判断是否已经存在该元素呢?可以通过equals方法。但是如果元素太多,用这样的方法就会比较慢。于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的那个区域。
- hashCode方法它返回的就是根据对象的内存地址换算出的一个值。当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
35.hashcode和equals如何使用
-
equals方法用来简单验证两个对象的相等性。Object类中定义的默认实现只检查两个对象的对象引用,以验证它们的相等性。 通过重写该方法,可以自定义验证对象相等新的规则,如果你使用ORM处理一些对象的话,你要确保在hashCode()和equals()对象中使用getter和setter而不是直接引用成员变量
-
hashcode方法用于获取给定对象的唯一的整数(散列码)。当这个对象需要存储在哈希表这样的数据结构时,这个整数用于确定桶的位置。默认情况下,对象的hashCode()方法返回对象所在内存地址的整数表示。hashCode()是HashTable、HashMap和HashSet使用的。默认的,Object类的hashCode()方法返回这个对象存储的内存地址的编号。
-
hash散列算法,使得在hash表中查找一个记录速度变O(1). 每个记录都有自己的hashcode,散列算法按照hashcode把记录放置在合适的位置. 在查找一个记录,首先先通过hashcode快速定位记录的位置.然后再通过equals来比较是否相等。如果hashcode没找到,则不equal(),元素不存在于哈希表中;即使找到了,也只需执行hashcode相同的几个元素的equal,如果不equal,还是不存在哈希表中。
36.解决hash冲突的方法?
在产生hash冲突时,两个不相等的对象就会有相同的 hashcode 值.当hash冲突产生时,一般有以下几种方式来处理:
- 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储。
- 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
- 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突。
- 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
37.String有哪些特性?
- 不变性:String 是只读字符串,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
- 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
- final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。
38.String真的是不可变的吗?
- String不可变但不代表引用不可以变
实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。
- 通过反射是可以修改所谓的“不可变”对象
用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。
39.String 和StringBuffer、StringBuilder的区别是什么?String 为什么是不可变的?
不可变性
String类中使用字符数组保存字符串,private final char value[]
,所以 string对象是不可变的。
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。
线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
40.String是最基本的数据类型吗?
- 不是,Java 的 8 种基本数据类型中不包括 String。
- Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、 double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
- 但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。
41.是否可以继承 String 类?
String 类是 final 类,不可以被继承。
42.String str="i"与 String str=new String(“i”)一样吗?
不一样,因为内存的分配方式不一样。String str=“i"的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。 String s = new String(“xyz”);创建了几个字符串对象两个对象,一个是静态区的"xyz”,一个是用new创建在堆上的对象。
43.在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
44.String类常用的方法有哪些?
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
45.Java异常关键字
• try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
• catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
• finally – finally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了 return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
• throw – 用于抛出异常。
• throws – 用在方法签名中,用于声明该方法可能抛出的异常。
46.try-catch-finally 中,如果 catch 中 return 了, finally 还会执行吗?
- 会执行,在 return 前执行。在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块, try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块 执行完毕之后再向调用者返回其值,然而如果在 finally 中修改了返回值,就会 返回修改后的值。
- 在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情,Java 中也 可以通过提升编译器的语法检查级别来产生警告或错误。
47.异常处理影响性能吗?
- 异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。
- 仅在异常情况下使用异常;在可恢复的异常情况下使用异常;尽管使用异常有利于 Java 开发,但是在应用中最好不要捕获太多的调用栈,因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此,异常消息应该提供恰到好处的信息。
48.Comparable 和 Comparator的区别?
Comparable接口实际上是出自java.lang包,它有一个 compareTo(Objectobj)方法用来排序。
Comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1,Object obj2)方法。
49.Java中集合框架图
50.List 和 Set 的区别?
-
List , Set 都是继承自Collection 接口
-
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
-
Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是HashSet、LinkedHashSet 以及 TreeSet。
-
List 支持for循环遍历,也可以用迭代器,但是set只能用迭代,因为它是无序的,所以无法用下标来取得想要的值。
-
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
51.ArrayList的优缺点?
- ArrayList的优点如下:
ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。 - ArrayList 的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。 - ArrayList适合使用的场景
ArrayList 比较适合顺序添加、随机访问的场景。
52.ArrayList 和 Vector 的区别是什么?
-
ArrayList和Vector都实现了List 接口,List 接口继承了 Collection 接口,它们都是有序集合
-
线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
-
性能效率:因为是否加锁会影响效率,所有不加锁的ArrayList 在性能方面要优于加锁的Vector。
-
扩容机制:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
-
Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
53.ArrayList 和 LinkedList 的区别是什么?
-
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
-
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
-
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
-
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
-
总结:在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
54.Java集合的快速失败机制 “fail-fast”?
- fail-fast是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
- 实操案例:
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中 的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。 - 原因解析:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount 的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测 modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出 异常,终止遍历。
- 解决办法:
方法1:在遍历过程中,所有涉及到改变modCount值得地方全部加上 synchronized。
方法2:使用CopyOnWriteArrayList来替换ArrayList
55.Iterator 和 ListIterator 有什么区别?
-
Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
-
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
-
ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元 素、替换一个元素、获取前面或后面元素的索引位置。
56.为什么 ArrayList 的 elementData 加上 transient 修饰?
- ArrayList 中的数组定义如下:
transient 的作用是说不希望 elementData 数组被序列化,重写了 writeObject 实现
private transient Object[] elementData;
- 再看一下 ArrayList 的定义:
可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
- 每次序列化时,先调用 defaultWriteObject() 方法序列化 ArrayList 中的非transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。
- 底层实操代码:
ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;
/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData
/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
总结:为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
57.HashMap的底层数据结构?
JDK7中HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。HashMap通过key的HashCode经过扰动函数处理过后得到Hash值,然后通过位运算判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。当Map中的元素总数超过Entry数组的0.75时,触发扩容操作,为了减少链表长度,元素分配更均匀。
58.HashMap中从获取对象的hash到散列计算规则?
JDK1.8中,是通过hashCode()的高16位异或低16位实现的:(h=k.hashCode())^(h>>>16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
static final int hash(Object key){
int h;
return (key==null)?0:(h=key.hashCode())^(h>>>16);
}
计算规则说明:
key.hashCode();返回散列值也就是hashcode,假设随便生成的一个值。
n表示数组初始化的长度是16。
&(按位与运算):运算规则:相同的二进制数位上,都是1的时候,结果为1,否则为零。
^(按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为0,不同为1。
计算过程如下所示:
问:为什么这里把key的hashcode取出来,把它右移16位,然后取异或?
答:因为int是4个字节,也就是32位,让高16位向右移动16位后参与到位运算中,较少了hash冲突。
59.默认初始化大小是多少?为啥大小都是2的幂?
- 默认初始化大小是16,hash运算的过程其实就是对目标元素的Key进行hashcode,再对Map的容量进行取模,而JDK 的工程师为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。
- HashMap的容量为什么是2的n次幂,和这个(n - 1) & hash的计算方法有关系,符号&是按位与的计算,这是位运算,计算机能直接运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。
60.HashMap 的长度为什么是2的幂次方?
-
为什么HashMap的长度是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。 -
这个算法应该如何设计呢?或者说这个算法该怎么实现呢?
我们首先可能会想到采用%取余的操作来实现。重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 最后我们HashMap底层采用的就是通过二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。 -
那为什么是两次扰动呢?(jdk1.8源码)
加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性, 最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。
在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动)
61.为什么String, Interger类适合作为键?
- 因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。
- String类型的变量存储到了字符串常量池中,会被缓存到常量池中,下次直接取出来即可;
62.能否使用任何类作为 Map 的 key?
-
可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点: 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
-
用户自定义 Key 类的最佳实践是使之为不可变的,也就是需要通过final关键字来修饰,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。
63.HashMap的主要参数都有哪些?
-
DEFAULT_INITIAL_CAPACITY:默认的初始化容量,1<<4位运算的结果是16,也就是默认的初始化容量为16。当然如果对要存储的数据有一个估计值,最好在初始化的时候显示的指定容量大小,减少扩容时的数据搬移等带来的效率消耗。同时,容量大小需要是2的整数倍。
-
MAXIMUM_CAPACITY:容量的最大值,1 << 30位,2的30次幂。
-
DEFAULT_LOAD_FACTOR:默认的加载因子,0.75 设计者认为这个数值是基于时间和空间消耗上最好的数值。这个值和容量的乘积是一个很重要的数值,也就是阈值,当达到这个值时候会产生扩容,扩容的大小大约为原来的二倍。
-
TREEIFY_THRESHOLD:因为jdk8以后,HashMap底层的存储结构改为了数组+链表+红黑树的存储结构(之前是数组+链表),刚开始存储元素产生碰撞时会在碰撞的数组后面挂上一个链表,当链表长度大于这个参数时,链表就可能会转化为红黑树,为什么可能后面还有一个参数,需要他们两个都满足的时候才会转化。
-
UNTREEIFY_THRESHOLD:介绍上面的参数时,我们知道当长度过大时可能会产生从链表到红黑树的转化,但是,元素不仅仅只能添加还可以删除,或者另一种情况,扩容后该数组槽位置上的元素数据不是很多了,还使用红黑树的结构就会很浪费,所以这时就可以把红黑树结构变回链表结构,什么时候变,就是元素数量等于这个值也就是6的时候变回来(元素数量指的是一个数组槽内的数量,不是HashMap中所有元素的数量)。
-
MIN_TREEIFY_CAPACITY:链表树化的一个标准,前面说过当数组槽内的元素数量大于8时可能会转化为红黑树,之所以说是可能就是因为这个值,当数组的长度小于这个值的时候,会先去进行扩容,扩容之后就有很大的可能让数组槽内的数据可以更分散一些了,也就不用转化数组槽后的存储结构了。当然,长度大于这个值并且槽内数据大于8时,那就转化为红黑树吧。
64.哈希冲突基本概念以及HashMap解决冲突的方法
- 哈希冲突基本概念:如果两个不同对象的hashCode相同,这种现象称为hash冲突。
- HashMap解决冲突的方法:
- 开放定址法:开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
- 链地址法:链地址法将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
- 再哈希法:当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
- 建立公共溢出区:将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。
65.为啥我们重写equals方法的时候需要重写hashCode方法呢?
- HashMap中value的查找是通过 key 的 hashcode 来查找,所以对自己的对象必须重写 hashcode 方法通过 hashcode 找到对象地址后会用 equals 比较你传入的对象和 hashmap 中的 key 对象是否相同,因此还要重写 equals。
66.HashMap什么时候进行扩容?它是怎么扩容的呢?
- HashMap进行扩容取决于以下两个元素:
Capacity:HashMap当前长度。
LoadFactor:负载因子,默认值0.75f。
当Map中的元素个数(包括数组,链表和红黑树中)超过了16*0.75=12之后开始扩容。 - 具体怎么进行扩容呢?将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing ,因为它将会调用hash方法找到新的bucket位置。
67.JDK1.7扩容的时候为什么要重新Hash呢,为什么不直接复制过去?
是因为长度扩大以后,Hash的规则也随之改变。比如原来长度(Length)是8,位运算出来的值是2 ,新的长度是16,位运算出来的值明显不一样了。
68.HashMap和Hashtable的区别是什么?
HashMap是线程不安全的,是允许key和value的值为null的,速度更慢,效率更快
Hashtable是线程安全的,是不允许key和value的值为null的,速度更快,效率更慢
-
HashMap是线程不安全的,HashTable是线程安全的;
-
由于线程安全,所以HashTable的效率比不上HashMap;
-
HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许;
-
HashMap默认初始化数组的大小为16,HashTable为11,前者扩容时,扩大两倍,后者扩大两倍+1;
-
HashMap需要重新计算hash值,而HashTable直接使用对象的hashCode;
69.HashMap的工作原理?
HashMap底层是hash数组和单向链表实现,数组中的每个元素都是链表,由Node内部类(实现Map.Entry<K,V>接口)实现,HashMap通过put&get方法存储和获取。
存储对象时,将K/V键值传给put()方法:
-
调用hash(K)方法计算K的hash值,然后结合数组长度,计算得数组下标;
-
调整数组大小(当容器中的元素个数大于capacity*loadfactor时,容器会进行扩容resize为2n);
-
第三个步骤中包含三个小的步骤:
-
如果K的hash值在HashMap中不存在,则执行插入,若存在,则发生碰撞;
-
如果K的hash值在HashMap中存在,且它们两者equals返回true,则更新键值对;
-
如果K的hash值在HashMap中存在,且它们两者equals返回false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。
(JDK1.7之前使用头插法、JDK1.8使用尾插法)
(注意:当碰撞导致链表大于TREEIFY_THRESHOLD=8时,就把链表转换成红黑树)
70.HashMap的table的容量如何确定?loadFactor是什么?该容量如何变化?这种变化会带来什么问题?
-
table数组大小是由capacity这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;
-
loadFactor是装载因子,主要目的是用来确认table数组是否需要动态扩展,默认值是0.75,比如table数组大小为16,装载因子为0.75时,threshold就是12,当table的实际大小超过12时,table就需要动态扩容;
-
扩容时,调用resize()方法,将table长度变为原来的两倍(注意是table长度,而不是threshold)
-
如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。
71.HashMap中put方法的过程?
- HashMap中put方法的过程步骤如下所示:
- 调用哈希函数获取Key对应的hash值,再计算其数组下标;
- 如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;
- 如果链表长度超过阀值(TREEIFYTHRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
- 如果结点的key已经存在,则替换其value即可;
- 如果集合中的键值对大于12,调用resize方法进行数组扩容。
- HashMap中put方法的过程图解如图所示:
72.HashMap数组扩容的过程?
-
数组扩容的算法
创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。 -
什么时候才需要扩容
当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。 -
链表什么时候转换为红黑树
当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,结点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的结点个数低于6,也会再把树转换为链表。 -
HashMap的扩容是什么
进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以结点要么就在原来的位置,要么就被分配到”原位置+旧容量”这个位置。
73.拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
- 选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持”平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
- 查找红黑树,由于之前添加的已经保证这个树是有序的了,所以查找的时候就是二分查找,效率更加高效。
- 如果为树结构,则在树中通过key.equals(key)进行查找,时间复杂度是O(logn)
如果是链表结构,则在链表中通过key.equals(key)进行查找,时间复杂度是O(n)
74.说说你对红黑树的见解?
- 每个节点非红即黑
- 根节点总是黑色的
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
- 每个叶子节点都是黑色的空节点(NIL节点)
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
75.JDK8中对HashMap做了哪些改变?
-
在java1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)
-
发生hash碰撞时,java1.7会在链表的头部插入,而java1.8会在链表的尾部插入
-
在java1.8中,Entry被Node替代
76.jdk1.8中做了哪些优化优化?
- 数组+链表改成了数组+链表或红黑树
- 链表的插入方式从头插法改成了尾插法
- 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
77.HashMap的不安全体现在哪里?
hashMap是线程不安全的,其主要体现:
-
在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
现在我们要在容量为2的容器里面用不同线程插入A,B,C,假如我们在resize之前打个短点,那意味着数据都插入了但是还没resize那扩容前可能是这样的。
我们可以看到链表的指向A->B->C
Tip:A的下一个指针是指向B的
因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
就可能出现下面的情况,大家发现问题没有?
B的下一个指针指向了A
一旦几个线程都调整完成,就可能出现环形链表
如果这个时候去取值,悲剧就出现了——Infinite Loop。 -
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
jdk1.8中HashMap中put操作的主函数,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入接下来的逻辑中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
78.HashMap中容量的初始化
当我们使用HashMap(int initialCapacity)来初始化容量的时候,jdk会默认帮我们计算一个相对合理的值当做初始容量。那么,是不是我们只需要把已知的HashMap中即将存放的元素个数直接传给initialCapacity就可以了呢?
关于这个值的设置,在《阿里巴巴Java开发手册》有以下建议:
initialCapacity=(需要存储的元素个数/负载因子)+1,注意负载因子默认是0.75
也就是说,如果我们设置的默认值是7,经过Jdk处理之后,会被设置成8,但是,这个HashMap在元素个数达到 8*0.75 = 6的时候就会进行一次扩容,这明显是我们不希望见到的。我们应该尽量减少扩容。原因也已经分析过。
如果我们通过initialCapacity/ 0.75F + 1.0F计算,7/0.75 + 1 = 10 ,10经过Jdk处理之后,会被设置成16,这就大大的减少了扩容的几率。
当HashMap内部维护的哈希表的容量达到75%时(默认情况下),会触发rehash,而rehash的过程是比较耗费时间的。所以初始化容量要设置成initialCapacity/0.75 + 1的话,可以有效的减少冲突也可以减小误差。
所以,我可以认为,当我们明确知道HashMap中元素的个数的时候,把默认容量设置成initialCapacity/ 0.75F + 1.0F是一个在性能上相对好的选择,但是,同时也会牺牲些内存。
我们想要在代码中创建一个HashMap的时候,如果我们已知这个Map中即将存放的元素个数,给HashMap设置初始容量可以在一定程度上提升效率。
但是,JDK并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运算,最终得到一个2的幂。原因也已经分析过。
所以为了最大程度的避免扩容带来的性能消耗,我们建议可以把默认容量的数字设置成initialCapacity/ 0.75F + 1.0F。
79.HashMap是怎么解决哈希冲突的?
答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;
什么是哈希?
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。
什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
HashMap的数据结构
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:
这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化
hash()函数
上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);
总结
简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
- 使用链地址法(使用散列表)来链接拥有相同hash值的数据;
- 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
- 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
80.HashMap线程安全的方式?
HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全.
方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现.
通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装. 封装的关键点有2处,1)使用了经典的synchronized来进行互斥, 2)使用了代理模式new了一个新的类,这个类同样实现了Map接口.在Hashmap上面,synchronized锁住的是对象,所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒. 优点:代码实现十分简单,一看就懂.缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差.
方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.
重新写了HashMap,比较大的改变有如下几点.使用了新的锁机制,把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能,使用的是NonfairSync. 这个特性调用CAS指令来确保原子性与互斥性.当如果多个线程恰好操作到同一个segment上面,那么只会有一个线程得到运行.
关于ConcurrentHashMap大家可以先看一下这篇文章
【Java中JDK1.8版本并发集合ConcurrentHashMap】
81.介绍一下强引用、软引用、弱引用、虚引用的区别?
- 强引用
JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用
- 软引用
2.1 基本概念:软引用是用来描述一些还有用但是非必须的对象。对于软引用关联的对象,在系统将于发生内存溢出异常之前,将会把这些对象列进回收范围中进行二次回收。(当你去处理占用内存较大的对象 并且生命周期比较长的,不是频繁使用的)
2.2 存在的问题:软引用可能会降低应用的运行效率与性能。比如:软引用指向的对象如果初始化很耗时,或者这个对象在进行使用的时候被第三方施加了我们未知的操作。
2.3 使用场景: 软引用在实际中有重要的应用,例如浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。
- 如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建
- 如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出
-
弱引用
弱引用(Weak Reference)对象与软引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收 -
虚引用
也叫幽灵引用和幻影引用,为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。也就是说,如果一个对象被设置上了一个虚引用,实际上跟没有设置引用没有任何的区别 一般不用,辅助咱们的Finaliza函数的使用
82.什么是反射机制?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
静态编译和动态编译
**静态编译:**在编译时确定类型,绑定对象
**动态编译:**运行时确定类型,绑定对象
83.反射机制优缺点
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
84.反射机制的应用场景有哪些?
反射是框架设计的灵魂。
在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。
举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制, 经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性
85.Java获取反射的三种方法
1.通过new对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制
86.Java创建对象有几种方式?
new 关键字
平时使用的最多的创建对象方式
User user=new User();
反射方式
使用 newInstance(),但是得处理两个异常 InstantiationException、IllegalAccessException:
User user=User.class.newInstance();
Object object=(Object)Class.forName("java.lang.Object").newInstance()
clone方法
Object对象中的clone方法来完成这个操作
反序列化操作
调用 ObjectInputStream 类的 readObject() 方法。我们反序列化一个对象,JVM 会给我们创建一个单独的对象。JVM 创建对象并不会调用任何构造函数。一个对象实现了 Serializable 接口,就可以把对象写入到文中,并通过读取文件来创建对象。
总结
创建对象的方式关键字:new、反射、clone 拷贝、反序列化。
87.什么是静态代理?
可以看一下我的这篇文章
【什么是静态代理?什么是动态代理?JDK动态代理和CGLIB包实现动态代理的区别】
- JVM层面:在编译时就已经实现,编译完成后代理类是一个实际的class文件。
- 灵活性:静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,非常麻烦的。
使用JDK静态代理很容易就完成了对一个类的代理操作。但是JDK静态代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较麻烦。
88.什么是动态代理?
- JVM层面:在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。
89.JDK动态代理和CGLIB包实现动态代理的区别?
89.1 JDK动态代理
89.1.1 JDK动态代理步骤
- 通过实现InvocationHandler接口来自定义自己的InvocationHandler;
- 通过Proxy.getProxyClass获得动态代理类;
- 通过反射机制获得代理类的构造方法,方法签名为getConstructor(InvocationHandler.class);
- 通过构造函数获得代理对象并将自定义的InvocationHandler实例对象传为参数传入;
- 通过代理对象调用目标方法;
89.1.2 概括静态代理和动态代理区别
- JDK静态代理是通过直接编码创建的,JDK动态代理是利用反射机制在运行时创建代理类的。
- 具体体现在上面我们从灵活性和JVM层面分析静态代理和动态代理的区别。
89.2 CGLIB包实现动态代理
89.2.1 核心思想
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。
89.2.2 CGLIB代理步骤
- 实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
- 然后在需要使用的时候,通过CGLIB动态代理获取代理对象。
89.2.3 CGLIB在进行代理的时候都进行了哪些工作
- 生成的代理类继承被代理类。在这里我们需要注意一点:如果委托类被final修饰,那么它不可被继承,即不可被代理;同样,如果委托类中存在final修饰的方法,那么该方法也不可被代理
- 代理类会为委托方法生成两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法
- 当执行代理对象的方法时,会首先判断一下是否存在实现了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,则将调用MethodInterceptor中的intercept方法,在intercept方法中,我们除了会调用委托方法,还会进行一些增强操作。在Spring AOP中,典型的应用场景就是在某些敏感方法执行前后进行操作日志记录
89.2.4 CGLIB中方法的调用还是通过反射吗?
不是,在CGLIB中,方法的调用并不是通过反射来完成的,而是直接对方法进行调用:通过FastClass机制对Class对象进行特别的处理,CGLIB采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引index来直接调用相应的方法
90.JDK动态代理和CGlib动态代理哪一个更快呢?
- 在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率。只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理就有些慢了。
- 使用CGLIB实现动态代理,CGLIB底层采用ASM字节码生成框架,使用字节码技术生成代理类, 在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLIB不能对声明为final的方法进行代理, 因为CGLIB原理是动态生成被代理类的子类。
91.Spring如何选择JDK动态代理还是CGLIB动态代理?
- 当Bean实现接口时,Spring就会用JDK的动态代理。
- 当Bean没有实现接口时,Spring使用CGlib实现。
92.说说Java中实现多线程的几种方法
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(JDK1.5以后)
- 线程池方式创建
93.线程中的常用方法你知道吗?
- start方法:start方法是我们开启一个新的线程的方法,但是并不是直接开启,而是告诉CPU我已经准备好了,快点运行我,这是启动一个线程的唯一入口。
- run方法:线程的线程体,当一个线程开始运行后,执行的就是run方法里面的代码,我们不能直接通过线程对象来调用run方法。因为这并没有产生一个新的线程。仅仅只是一个普通对象的方法调用。
- getName方法:获取线程名称的方法
- sleep方法:将当前线程暂定指定的时间
- isAlive:获取线程的状态。
- join:调用某线程的该方法,将当前线程和该线程合并,即等待该线程结束,在恢复当前线程的运行
- yield让出CPU,当前线程进入就绪状态
- wait和notify/notifyAll:阻塞和唤醒的方法
- 设置优先级
我们创建的多个线程的执行顺序是由CPU决定的。Java中提供了一个线程调度器来监控程序中启动后进入就绪状态的所有的线程,优先级高的线程会获取到比较多
运行机会
/**
* 最小的优先级是 1
*/
public final static int MIN_PRIORITY = 1;
/**
* 默认的优先级都是5
*/
public final static int NORM_PRIORITY = 5;
/**
* 最大的优先级是10
*/
public final static int MAX_PRIORITY = 10;
94.Thread类中yield方法的作用
yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
95.为什么wait, notify和notifyAll这些方法不在thread类里面?
明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
96.为什么wait和notify方法要在同步块中调用?
1.只有在调用线程拥有某个对象的独占锁时,才能够调用该对象的wait(),notify()和notifyAll()方法。
2.如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。
3.还有一个原因是为了避免wait和notify之间产生竞态条件。
wait()方法强制当前线程释放对象锁。这意味着在调用某对象的wait()方法之前,当前线程必须已经获得该对象的锁。因此,线程必须在某个对象的同步方法或同步代码块中才能调用该对象的wait()方法。
在调用对象的notify()和notifyAll()方法之前,调用线程必须已经得到该对象的锁。因此,必须在某个对象的同步方法或同步代码块中才能调用该对象的notify()或notifyAll()方法。
调用wait()方法的原因通常是,调用线程希望某个特殊的状态(或变量)被设置之后再继续执行。调用notify() 或notifyAll()方法的原因通常是,调用线程希望告诉其他等待中的线程:“特殊状态已经被设置”。这个状态作为线程间通信的通道,它必须是一个可变的共享状态(或变量)。
97.操作系统中进程和线程的关系
线程是操作系统调度的最小单元,它可以让一个进程并发地处理多个任务,也叫轻量级进程。所以,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、局部变量,并且能够共享进程内的资源。由于共享资源,处理器便可以在这些线程之间快速切换,从而让使用者感觉这些线程在同时执行。 总的来说,操作系统可以同时执行多个任务,每个任务就是一个进程。进程可以同时执行多个任务,每个任务就是一个线程。一个程序运行之后至少有一个进程,而一个进程可以包含多个线程,但至少要包含一个线程。 具体关系如下图所示:
98.线程安全需要保证几个基本特性?
原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。
可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile就是负责保证可见性的。
有序性,是保证线程内串行语义,避免指令重排等。
99.什么是线程安全
线程安全就是说多线程访问同一段代码,不会产生不确定的结果。
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。这个问题有值得一提的地方,就是线程安全也是有几个级别的:
- 不可变:像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
- 绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
- 相对线程安全:相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
- 线程非安全:这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类
100.说下线程间是如何通信的?
线程之间的通信有两种方式:共享内存和消息传递。
共享内存
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。典型的共享内存通信方式,就是通过共享对象进行通信。
例如线程A与线程B之间如果要通信的话,那么就必须经历下面两个步骤:
1.线程A把本地内存A更新过得共享变量刷新到主内存中去。
2.线程B到主内存中去读取线程A之前更新过的共享变量。
消息传递
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行
通信。在Java中典型的消息传递方式,就是wait()和notify(),或者BlockingQueue
101.说下进程间是如何通信的?
- 共享存储(Shared-memory)
- 消息传递(message-passing)
- 管道通信(Pipe)
- 信号
- 信号量
- Socket通信
102.怎么保证多线程的并发安全?
Java保证线程安全的方式有很多,共有7种,其中较为常用的有三种,按照资源占用情况由轻到重排列,这三种保证线程安全的方式分别是原子类、volatile、锁。
-
JDK从1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,按功能可以归纳为4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。无论原子更新哪种类型,都要遵循“比较和替换”规则(CAS的方式),即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败。
-
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全。可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步。volatile具有如下的内存语义:当写一个volatile变量时,该线程本地内存中的共享变量的值会被立刻刷新到主内存;当读一个volatile变量时,该线程本地内存会被置为无效,迫使线程直接从主内存中读取共享变量。
-
原子类和volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全,Java中加锁的方式有两种,分别是synchronized关键字和Lock接口。synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式,以及多个条件变量。若想通过升级的方式让它支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。因此,JDK的开发团队在1.5新增了Lock接口,并通过Lock支持了上述的功能,即:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。
实现线程安全的方式有很多,除了上述三种方式之外,还有如下几种方式:
-
无状态设计 线程安全问题是由多线程并发修改共享变量引起的,如果在并发环境中没有设计共享变量,则自然就不会出现线程安全问题了。这种代码实现可以称作“无状态实现”,所谓状态就是指共享变量。
-
不可变设计 如果在并发环境中不得不设计共享变量,则应该优先考虑共享变量是否为只读的,如果是只读场景就可以将共享变量设计为不可变的,这样自然也不会出现线程安全问题了。具体来说,就是在变量前加final修饰符,使其不可被修改,如果变量是引用类型,则将其设计为不可变类型(参考String类)。
-
并发工具 java.util.concurrent包提供了几个有用的并发工具类,一样可以保证线程安全: - Semaphore:就是信号量,可以控制同时访问特定资源的线程数量。 - CountDownLatch:允许一个或多个线程等待其他线程完成操作。 - CyclicBarrier:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。
-
本地存储 我们也可以考虑使用ThreadLocal存储变量,ThreadLocal可以很方便地为每一个线程单独存一份数据,也就是将需要并发访问的资源复制成多份。这样一来,就可以避免多线程访问共享变量了,它们访问的是自己独占的资源,它从根本上隔离了多个线程之间的数据共享。
103.说说ThreadLocal的原理
ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。
但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。
104.什么是线程同步?线程同步的概念?
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。
105.线程同步的方式?
Java主要通过加锁的方式实现线程同步,而锁有两类,分别是synchronized和Lock。 synchronized可以加在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同:
- 加在普通方法上,则锁是当前的实例(this)。
- 加在静态方法上,则锁是当前类的Class对象。
- 加在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。 不同的锁对象,意味着不同的锁粒度,所以我们不应该无脑地将它加在方法前了事,尽管通常这可以解决问题。而是应该根据要锁定的范围,准确的选择锁对象,从而准确地确定锁的粒度,降低锁带来的性能开销。
synchronized和Lock一些功能的异同以及技术演进的过程:
synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式,以及多个条件变量。若想通过升级的方式让synchronized支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。因此,JDK的开发团队在1.5引入了Lock接口,并通过Lock支持了上述的功能。Lock支持的功能包括:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)
synchronized和Lock底层实现的原理:
synchronized采用“CAS+Mark Word”实现,为了性能的考虑,并通过锁升级机制降低锁的开销。在并发环境中,synchronized会随着多线程竞争的加剧,按照如下步骤逐步升级:无锁、偏向锁、轻量级锁、重量级锁。 Lock则采用“CAS+volatile”实现,其实现的核心是AQS。AQS是线程同步器,是一个线程同步的基础框架,它基于模板方法模式。在具体的Lock实例中,锁的实现是通过继承AQS来实现的,并且可以根据锁的使用场景,派生出公平锁、不公平锁、读锁、写锁等具体的实现。
106.Java中线程的6种状态(源码中有体现)
Java线程在运行的生命周期中,在任意给定的时刻,只能处于下列6种状态之一:
-
NEW :初始状态,线程被创建,但是还没有调用start方法。
-
RUNNABLE:可运行状态,线程正在JVM中执行,但是有可能在等待操作系统的调度。
-
BLOCKED :阻塞状态,线程正在等待获取监视器锁。
-
WTING :等待状态,线程正在等待其他线程的通知或中断。
-
TIMED_WTING:超时等待状态,在WTING的基础上增加了超时时间,即超出时间自动返回。
-
TERMINATED:终止状态,线程已经执行完毕。
线程整个过程的转移:
线程在创建之后默认为初始状态,在调用start方法之后进入可运行状态,可运行状态不代表线程正在运行,它有可能正在等待操作系统的调度。进入等待状态的线程需要其他线程的通知才能返回到可运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,除了他线程的唤醒,在超时时间到达时也会返回运行状态。此外,线程在执行同步方法时,在没有获取到锁的情况下,会进入到阻塞状态。线程在执行完run方法之后,会进入到终止状态。
线程状态的注意事项:
Java将操作系统中的就绪和运行两个状态合并为可运行状态(RUNNABLE)。线程阻塞于synchronized的监视器锁时会进入阻塞状态,而线程阻塞于Lock锁时进入的却是等待状态,这是因为Lock接口实现类对于阻塞的实现均使用了LockSupport类中的相关方法。
107.Java中常用的锁以及原理
- Java中加锁有两种方式,分别是synchronized关键字和Lock接口,而Lock接口的经典实现是ReentrantLock。另外还有ReadWriteLock接口,它的内部设计了两把锁分别用于读写,这两把锁都是Lock类型,它的经典实现是ReentrantReadWriteLock。
- synchronized的实现依赖于对象头,Lock接口的实现则依赖于AQS。
- synchronized的底层是采用Java对象头来存储锁信息的,对象头包含三部分,分别是Mark Word、Class Metadata Address、Array length。其中,Mark Word用来存储对象的hashCode及锁信息,Class Metadata Address用来存储对象类型的指针,而Array length则用来存储数组对象的长度。
- AQS是队列同步器,是用来构建锁的基础框架,Lock实现类都是基于AQS实现的。AQS是基于模板方法模式进行设计的,所以锁的实现需要继承AQS并重写它指定的方法。AQS内部定义了一个FIFO的队列来实现线程的同步,同时还定义了同步状态来记录锁的信息。 ReentrantLock通过内部类Sync定义了锁,它还定义了Sync的两个子类FrSync和NonfrSync,这两个子类分别代表公平锁和非公平锁。Sync继承自AQS,它不仅仅使用AQS的同步状态记录锁的信息,还利用同步状态记录了重入次数。同步状态是一个整数,当它为0时代表无锁,当它为N时则代表线程持有锁并重入了N次。 ReentrantReadWriteLock支持重入的方式与ReentrantLock一致,它也定义了内部类Sync,并定义出两个子类FrSync和NonfrSync来实现公平锁和非公平锁。此外,ReentrantReadWriteLock内部包含读和写两把锁,这两把锁都是由Sync来实现的。区别在于读锁支持共享,即多个线程可以同时加读锁成功,而写锁是互斥的,即只能有一个线程加锁成功。
108.Synchronized和ReentrantLock的区别
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
-
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
-
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
-
锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象
109.线程池的使用规范
需要的同学,大家可以看一下我总结的这篇文章。
【线程池有哪几种创建方式,能详细的说下吗?俩种方式、7种方法?以及案例演示过程】
阿里巴巴开发手册规定,线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让开发人员更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 - CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
110.线程池的7个参数你都知道吗?
构造方法中的参数信息
查看源代码
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。任务提交到线程池后,首先会检查当前线程数是否达到了corePoolSize,如果没有达到的话,则会创建一个新线程来处理这个任务。
- maximumPoolSize 线程池最大线程数量
当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
- keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
- unit 空闲线程存活时间单位
keepAliveTime 空闲线程存活时间计量单位
- workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
1.基于数组的有界阻塞队列,按FIFO排序。
2.新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。
3.当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。
4.如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
1.基于链表的无界阻塞队列(最大容量为Interger.MAX),按照FIFO排序。
2.由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数)。
3.使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
1.一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。
2.新任务进来时,不会缓存,直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
1.具有优先级的无界阻塞队列。
2.优先级通过参数Comparator实现。
- threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等操作。
111.线程池拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,我们通过拒绝策略来解决这个问题,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
112.线程池的5种状态
-
RUNNING
- 线程池被创建,线程池的状态就是RUNNING状态;
- 线程池处于RUNNING状态时,线程池能够接收新任务,也能够对已经添加的任务进行处理;
-
SHUTDOWN
- 线程池已经被关闭了,不再接收新任务;但是,其还是会处理队列中的剩余的任务;
- 调用线程池的shutdown()方法后,线程池的状态就会由RUNNING转为SHUTDOWN;
-
STOP
- 线程池处于STOP状态,此时线程池不再接收新任务,不处理已经添加进来的任务,并且会中断正在处理的任务;
- 调用线程池的shutdownNow()方法后,线程池的状态就会由RUNNING或SHUTDOWN转为STOP;
-
TIDYING
- 线程池被下达关闭命令后,如果当前所有的任务都已经终止了(这个终止可以表示执行结束,也可以表示强制中断,也可以表示被丢弃) ,那么线程就会进入TIDYING状态;当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
- 如果线程状态已经是SHUTDOWN了,并且线程中以及队列中都没有任务时,线程池就会由SHUTDOWN转为TIDYING;如果线程池状态为STOP,那么当线程池把所有的任务都给清理干净时,线程池就会由STOP转为TIDYING;
-
TERMINATED;
- 线程池结束,不能重新启动了;
- 如果线程池处于TIDYING状态,那么当线程池执行完terminated()方法后,线程池状态就会由TIDYING转为TERMINTED;
113.线程池的执行流程
-
提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker() 方法创建一个核心线程去执行任务;
-
如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;
-
如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;
-
如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略
114.常用的线程池有哪些?
new SingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
new FixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。
new CachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
new ScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的求。
115.线程池有哪几种创建方式,能详细的说下吗?
【线程池有哪几种创建方式,能详细的说下吗?俩种方式、7种方法?以及案例演示过程】
115.1 俩种方式、7种方法
线程池的创建方法总共有 7 种(其中 6 种是通过 Executors 创建的,1 种是通过ThreadPoolExecutor 创建的,但总体来说可分为 2 类):
- 通过 ThreadPoolExecutor 创建的线程池;
- 通过 Executors 创建的线程池(不推荐使用)。
115.2 通过 ThreadPoolExecutor 创建的线程池
- ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置,上篇文章我讲到了一些基本概念,地址在这里,不会的同学先补一下相关的知识。
【线程池的使用规范、线程池的7个参数、4种拒绝策略、线程池的5种状态、线程池的执行流程】
115.3 通过 Executors 创建的线程池
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池,任务执行顺序不确定。
JDK 8 添加
。
116.介绍下IO流的分类
117.Files的常用方法都有哪些?
- Files. exists():检测文件路径是否存在。
- Files. createFile():创建文件。
- Files. createDirectory():创建文件夹。
- Files. delete():删除一个文件或目录。
- Files. copy():复制文件。
- Files. move():移动文件。
- Files. size():查看文件个数。
- Files. read():读取文件。
- Files. write():写入文件。
118.同步、异步、阻塞、非阻塞的概念
同步和异步指的是:当前线程是否需要等待方法调用执行完毕。
阻塞和非阻塞指的是:当前接口数据还未准备就绪时,线程是否被阻塞挂起
同步&异步其实是处于框架这种高层次维度来看待的,而阻塞&非阻塞往往针对底层的系统调用方面来抉择,也就是说两者是从不同维度来考虑的。
这四个概念两两组合,会形成4个新的概念,如下:
同步阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,则客户端则被服务端堵塞了,所以客户端会一直等待服务端的响应,此时客户端不能做其他任何事,服务端也不会接受其他客户端的请求。这种通信机制比较简单粗暴,但是效率不高。
同步非阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候虽然客户端会一直等待响应,但是服务端可以处理其他的请求,过一会回来处理原先的。这种方式很高效,一个服务端可以处理很多请求,不会在因为任务没有处理完而堵着,所以这是非阻塞的。
异步阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,但是客户端不会等待服务器响应,它可以做其他的任务,等服务器处理完毕后再把结果响应给客户端,客户端得到回调后再处理服务端的响应。这种方式可以避免客户端一直处于等待的状态,优化了用户体验,其实就是类似于网页里发起的ajax异步请求。
异步非阻塞:客户端发送请求给服务端,此时服务端处理任务时间很久,这个时候的任务虽然处理时间会很久,但是客户端可以做其他的任务,因为他是异步的,可以在回调函数里处理响应;同时服务端是非阻塞的,所以服务端可以去处理其他的任务,如此,这个模式就显得非常的高效了。
119.BIO—Blocking IO—阻塞IO
119.1 什么是BIO?
BIO为阻塞型
IO模型,服务器在接收客户端连接(accept)以及服务器读取客户端发送数据(recv)时会发生阻塞
。
119.2 BIO中的阻塞是什么意思?
当服务端执行了accept函数调用后,当前线程就会进入“等待”状态释放cpu的占用,直到有客户端连接服务器为止;服务端调用recv读取客户端发来的数据包时,当前线程就会进入“等待”状态释放cpu的占用,直到有客户端发来消息为止;
119.3 BIO底层是怎么实现的?原理是什么?
socket主要有三部分组成发送缓冲区+接收缓冲区+等待列表,当服务器socket调用accept函数,就回将当前线程的引用挂在等待列表中,如果socket接收到了客户端连接则唤醒等待列表中的线程。
119.4 BIO的优势以及存在的问题?
优点:
- 每个连接创建一个线程,实现了一个服务器连接多个客户端。
缺点:
- 当来了一个客户端创建连接后,如果不给客户端新分配一个线程执行服务器逻辑,那么服务端将很难再和第二个客户端建立连接。但是每处理一个请求就要重新new一个线程,而且我们的CPU是不会停止的,操作系统需要不断的调用我们的进程,进行进程的切换,用户态到内核态的切换,这个过程会大量的浪费我们CPU的资源以及一些内存的资源,是不可取的。
- 很多线程其实是在等数据(阻塞),是一个无用的线程
120.NIO—New IO—非阻塞IO
120.1 什么是NIO?
标记了非阻塞后的socket在调用acept和recv函数时无论有无连接或数据都会返回不会阻塞。
120.2 为什么需要NIO?BIO从NIO演进的过程?
- 如果说服务器只有很少的人用,那么BIO其实就可以满足我们的用户,但问题在于互联网蓬勃发展,随着服务器访问人数的增加,这样的服务器模型将会成为瓶颈。
- 我们以一种
C10K
的思想去看待BIO服务器代码。如果我们客户端的连接数增加了10K倍,那么就意味着要创建10K个线程,单单创建线程就是一项不小的开销了,再加上线程之间要来回切换,单机服务器根本就扛不住这么大的连接数。 - 那既然瓶颈是出在线程上,我们就考虑能不能把服务器的模型变为单线程模型,思路其实和之前说的差不多,用集合保存每个连接的客户端,通过while循环来对每个连接进行操作。
- BIO操作的瓶颈在于accept客户端的时候会阻塞,以及进行读写操作的时候会阻塞,导致单线程执行效率低。为了突破这个瓶颈,操作系统发展出了NIO,这里的NIO指的是非阻塞IO。
- 也就是说在accept客户端连接的时候,不需要阻塞,如果没有客户端连接就返回-1(java-NULL),在读写操作的时候,也不阻塞,有数据就读,没数据就直接返回,这样就解决了单线程服务器的瓶颈问题。
120.3 NIO的优势之处以及存在的问题?
优点:
- NIO是同步非阻塞的,客户端的accept和客户端读取都是非阻塞的。
缺点:
- 客户端与服务器建立连接后,后续会进行一系列的读写操作。虽然这些读写操作是非阻塞的,但是每调一次读写操作在操作系统层面都要进行一次用户态和内核态的切换,这个也是一项巨大的开销(读写等系统调用都是在内核态完成的)。
- 虽然可以单线程完成支持多连接的socket服务端,但是如果有1万个连接但是只有一个发送消息,还是会调用函数recv读取一万遍,其中有9999遍是无效调用,要知道应用程序是不能直接调用内核函数的,应用程序调用内核函数时会触发软件中断,效果类似线程上下文切换,会暂停当前应用程序让出CPU切换至内核函数执行,执行完后再切换回应用程序,这是会有进程的现场保护和现场恢复过程会占用CUP的时间和资源。
- 解决方案:如果可以吧socket集合交给内核去管理,让内核帮我们去遍历socket集合,返回给我们有数据可读的客户端,然后我们只进行有效读取。
121.什么是AIO?
AIO : 异步非阻塞 ,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。
AIO属于NIO包中的类实现,其实 IO主要分为BIO和NIO ,AIO只是附加品,解决IO不能异步的实现在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作。
122.Java中JDK必知必会的NIO网络编程
【Java中JDK必知必会的NIO网络编程、BIO、NIO、AIO、同步异步,阻塞非阻塞、NIO的三大核心组件、Selector、Channel、Buffer,单线程、多线程、主从Reactor模型】
123.面试常考的网络编程之Socket
【面试常考的网络编程之Socket、短连接与长连接、客户端与服务端网络通讯流程、Java网络编程之BIO、JDK网络编程BIO案例实战演练】
124.JDK8新特性之Stream流
需要的同学,大家可以看一下我总结的这篇文章。
【JDK8新特性之Stream流-Stream流常用的API以及案例实操】
125.JDK8新特性之Lambda 表达式
需要的同学,大家可以看一下我总结的这篇文章。
【JDK8新特性之Lambda表达式-案例实操】
126.JDK8新特性之方法与构造函数引用
需要的同学,大家可以看一下我总结的这篇文章。
【JDK8新特性之方法引用-案例实操】
127.JDK8新特性之日期时间API-案例实操
需要的同学,大家可以看一下我总结的这篇文章。
【JDK8新特性之日期时间API-案例实操】
128.什么jsp?
JSP的全称是Java Server Pages,即Java的服务器页面,JSP的主要作用是代替Servlet程序回传HTML页面的数据。JSP页面本质上是一个Servlet程序,第一次访问JSP页面时,Tomcat服务器会将此JSP页面翻译成为一个Java源文件,并对其进行编译成为.class字节码文件。
129.什么是Servlet?
Servlet是JavaEE规范的一种,主要是为了扩展Java作为Web服务的功能,统一接口。由其他内部厂商如tomcat,jetty内部实现web的功能。如一个http请求到来:容器将请求封装为servlet中的HttpServletRequest对象,调用init(),service()等方法输出response,由容器包装为httpresponse返回给客户端的过程。
130.什么是Servlet规范?
- 从Jar包上来说,Servlet规范就是两个Jar文件。servlet-api.jar和jsp-api.jar,Jsp也是一种Servlet。
- 从package上来说,就是javax.servlet和javax.servlet.http两个包。
- 从接口来说,就是规范了Servlet接口、Filter接口、Listener接口、ServletRequest接口、ServletResponse接口等。类图如下:
131.servlet的生命周期
- init()初始化方法:servlet容器加载servlet,加载完成后servlet容器会创建一个servlet容器并调用init()方法,只会调用一次。
- service()处理客户端请求阶段:收到一个客户端请求,服务器会产生一个线程去处理,容器会创建一个特定的servletRequest、servletResponse对象,通过servlet的service方法中的servletRequest解析用户请求的信息,通过servletResponse返回数据。
- destroy()终止阶段:当web应用终止或者servlet容器终止、或servlet容器会调用servlet对象的destory方法时,在destory方法中可以释放资源。
132.jsp和servlet的区别
- 本质都是servlet
- servlet侧重于逻辑处理
- jsp侧重于视图显示
133.jsp九大内置对象
- page页面对象
- config配置对象
- request请求对象
- response响应对象
- session会话对象
- application全局对象
- out输出对象
- pageContext页面上下文对象
- exception异常对象
134.jsp的四大作用域
page:只在当前页面有效
request:它在当前请求中有效
session:它在当前会话中有效
application:他在所有的应用程序中都有效
注意:当4个作用域对象都有相同的name属性时,默认按照最小的顺序查找
135.说说servlet的原理
-
首先简单解释一下Servlet接收和响应客户请求的过程,首先客户发送一个请求,Servlet是调用service()方法对请求进行响应的,通过源代码可见,service()方法中对请求的方式进行了匹配,选择调用doGet,doPost等这些方法,然后再进入对应的方法中调用逻辑层的方法,实现对客户的响应。在Servlet接口和GenericServlet中是没有doGet()、doPost()等等这些方法的,HttpServlet中定义了这些方法,但是都是返回error信息,所以,我们每次定义一个Servlet的时候,都必须实现doGet或doPost等这些方法。
-
每一个自定义的Servlet都必须实现Servlet的接口,Servlet接口中定义了五个方法,其中比较重要的三个方法涉及到Servlet的生命周期,分别是上文提到的init(),service(),destroy()方法。GenericServlet是一个通用的,不特定于任何协议的Servlet,它实现了Servlet接口。而HttpServlet继承于GenericServlet,因此HttpServlet也实现了Servlet接口。所以我们定义Servlet的时候只需要继承HttpServlet即可。
-
Servlet接口和GenericServlet是不特定于任何协议的,而HttpServlet是特定于HTTP协议的类,所以HttpServlet中实现了service()方法,并将请求ServletRequest、ServletResponse 强转为HttpRequest 和 HttpResponse。
136.Servlet是线程安全的吗?
Servlet与线程安全Servlet不是线程安全的,多线程并发的读写会导致数据不同步的问题。 解决的办法是尽量不要定义name属性,而是要把name变量分别定义在doGet()和doPost()方法内。
137.什么是cookie?有什么作用?
cookie是由Web服务器保存在用户浏览器上的小文件(key-value格式),包含用户相关的信息。客户端向服务器发起请求,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户身份。
138.什么是session? 有什么作用?
session是依赖Cookie实现的。session是服务器端对象session 是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置 sessionid,浏览器在向服务器请求过程中传输cookie 包含 sessionid ,服务器根据 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。
139.cookie与session区别
-
存储位置与安全性:cookie数据存放在客户端上,安全性较差,session数据放在服务器上,安全性相对更高;
-
存储空间:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点多保存20个cookie,session无此限制
-
占用服务器资源:session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。
140.禁用客户端cookie,如何实现session?
一般是通过 Cookie 来保存 SessionID ,假如你使用了 Cookie 保存 SessionID 的方案的话, 如果客户端禁用了 Cookie,那么 Session 就无法正常工作。但是,并不是没有 Cookie 之后就不能用 Session 了,比如你可以将 SessionID 放在请求的 url 里面https://javaguide.cn/?Session_id=xxx 。这种方案的话可行,但是安全性和用户体验感降低。当然,为了你也可以对 SessionID 进行一次加密之后再传入后端。
141.HTTP请求/响应的结构是怎么样的?
大家可以看一下我总结的这篇文章,非常详细。
【强烈建议收藏:计算机网络面试专题:HTTP协议基本概念以及通信过程、HTTPS基本概念、SSL加密原理、通信过程、中间人攻击问题、HTTP协议和HTTPS协议区别】
142.HTTP中GET和POST请求的区别?
【让面试官吃惊的回答:HTTP中GET和POST请求的区别你知道吗?】
143.Forward和Redirect的区别?
- 浏览器URL地址:Forward是服务器内部的重定向,服务器内部请求某个servlet,然后获取响应的内容,浏览器的URL地址是不会变化的;Redirect是客户端请求服务器,然后服务器给客户端返回了一个302状态码和新的location,客户端重新发起HTTP请求,服务器给客户端响应location对应的URL地址,浏览器的URL地址发生了变化。
- 数据的共享:Forward是服务器内部的重定向,request在整个重定向过程中是不变的,request中的信息在servlet间是共享的。Redirect发起了两次HTTP请求分别使用不同的request。
- 请求的次数:Forward只有一次请求;Redirect有两次请求。
144.web.xml在web项目中有什么作用?
一个web中可以没有web.xml文件,也就是说,web.xml文件并不是web工程必须的。
web.xml文件是用来初始化配置信息:比如Welcome页面、servlet、servlet-mapping、filter、listener、启动加载级别等。
web.xml常用的一些功能:
- 指定欢迎页面。一般情况下,我们会在web.xml中指定欢迎页。但 web.xml并不是一个Web的必要文件,没有web.xml,网站仍然是可以正常工作的。只不过网站的功能复杂起来后,web.xml的确有非常大用处,所以,默认创建的动态web工程在WEB-INF文件夹下面都有一个web.xml文件。
- 命名与定制URL。我们可以为Servlet和JSP文件命名并定制URL,其中定制URL是依赖命名的,命名必须在定制URL前。下面拿serlet来举例:
- 定制初始化参数。可以定制servlet、JSP、Context的初始化参数,然后可以再servlet、JSP、Context中获取这些参数值。
- 指定错误处理页面。可以通过“异常类型”或“错误码”来指定错误处理页面。
- 设置过滤器。比如设置一个编码过滤器,过滤所有资源
- 设置监听器。
- 设置会话(Session)过期时间。其中时间以分钟为单位,假如设置60分钟超时:
三.下节预告
能看到这的人,一定是人中龙凤,给你点个赞。都说到这里了,不点个赞是不是都不好意思走了,哈哈,那就赶快点个赞收藏起来我,创作不易,感谢关注!
如果关于Java基础的面试知识你已经很好的掌握了,那么接下来给你安排的就是【2023金三银四面试小抄之JVM篇】,敬请期待!
四.共勉
最后,我想送给大家一句一直激励我的座右铭,希望可以与大家共勉!