文章目录
- JAVA基础
- JVM
- 集合
- 实现 Java list,要求实现 list 的 get(), add(), remove() 三个功能函数,不能直接使用 ArrayList、LinkedList 等 Java 自带高级类(阿里面试题)
- 数据结构与算法精选面试题
- HashMap源码解读
- ArrayList源码解读
- LinkList源码解读
- 自己实现一个arraylsit
- Hash是什么?HashCode是什么?Hash表是什么?为什么要用HashCode?
- hashcode详解
- HashMap是如何存储的?
- HashMap碰撞原理,jdk是如何解决碰撞问题的?
- HashMap的key重复,那么value会被覆盖吗?
- HashMap如何实现相同key存入数据后不被覆盖?
- 什么时候需要重写HashCode、equals?为什么重写equals方法时必须重写hashcode方法?
- Jdk1.6,JDK1.7,jdk1.8中HashMap区别
- 基础知识
- 2个不相等的对象有可能具有相同hashCode吗
- ArrayList和LinkedList有什么区别
- Comparator与Comparable有什么区别
- JDK8提升代码优雅技巧
- Java中变量和常量有什么区别
- Java中止线程的三种方式
- Java中的基本数据类型有哪些?它们的大小是多少?
- Java中的异常处理机制是怎样的
- Java中的集合框架有哪些核心接口
- Java五种文件拷贝方式
- Java创建对象有几种方式
- Java支持多继承么,为什么
- Strings与newString有什么区别
- String类能被继承吗,为什么
- String,Stringbuffer,StringBuilder的区别
- ThreadLocal有哪些应用场景
- char型变量能存贮一个中文汉字吗
- equals与==区别
- for-each与常规for循环的效率区别
- int和Integer的区别
- notify()和notifyAll()有什么区别
- synchronized的实现原理
- synchronized锁优化
- 两个对象hashCode()相同,则equals()否也一定为true?
- 什么是守护线程?与普通线程的区别
- 反射中,Class.forName和ClassLoader的区别
- 如何判断一个对象是否可以被回收
- 如何实现对象克隆
- 如何实现线程的同步
- 工作中最常见的6种OOM问题
- 抽象工厂和工厂方法模式的区别
- 抽象类和接口有什么区别
- 日期格式化用yyyy还是YYYY
- 有哪些常见的运行时异常
- 构造器是否可被重写
- 程序员必懂的权限模型:RBAC
- 缓存淘汰机制LRU和LFU的区别,电商场景下用哪个?
- 讲讲你对CountDownLatch的理解
- 讲讲你对CyclicBarrier的理解
- 讲讲你对ThreadLocal的理解
- 设计模式是如何分类的
- 说说你对lambda表达式的理解
- 说说你对内部类的理解
- 说说你对懒汉模式和饿汉模式的理解
- 说说你对泛型的理解
- 说说你对设计模式的理解
- 谈谈你对Java序列化的理解
- 谈谈你对反射的理解
- 谈谈自定义注解的场景及实现
- 过滤器和拦截器有什么区别?
- 重载和重写的区别
- 金额到底用Long还是Bigdecimal
- 阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别
- 静态内部类与非静态内部类有什么区别
- BIO、NIO、AIO有什么区别
- JDK动态代理与CGLIB实现的区别
- final,finally,finalize的区别
- 什么是值传递和引用传递
- 深拷贝和浅拷贝区别
JAVA基础
JVM
类加载过程
当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现这个类进行初始化。
- 加载
加载,是指Java虚拟机查找字节流(查找.class文件),并且根据字节流创建java.lang.Class对象的过程。这个过程,将类的.class文件中的二进制数据读入内存,放在运行时区域的方法区内。然后在堆中创建java.lang.Class对象,用来封装类在方法区的数据结构。
类加载阶段:
(1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。
(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。
(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等。
Student类加载过程图示:
- 链接
链接包括验证、准备以及解析三个阶段。
(1)验证阶段。主要的目的是确保被加载的类(.class文件的字节流)满足Java虚拟机规范,不会造成安全错误。
(2)准备阶段。负责为类的静态成员分配内存,并设置默认初始值。
(3)解析阶段。将类的二进制数据中的符号引用替换为直接引用。
说明:
符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。
举个例子来说,现在调用方法hello(),这个方法的地址是0xaabbccdd,那么hello就是符号引用,0xaabbccdd就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
- 初始化
初始化,则是为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
- 小结
类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载(垃圾回收)。
- 附录
常见问题:在自己的项目里新建一个java.lang包,里面新建了一个String类,能代替系统String吗?
不能,因为根据类加载的双亲委派机制,会将请求转发给父类加载器,父类加载器发现冲突了String就不会加载了。
类加载机制
类加载器简单来说是用来加载 Java 类到 Java 虚拟机中的。Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
想要真正深入理解Java类加载机制,就要弄懂三个问题:类什么时候加载、类加载的过程是什么、用什么加载。所以本文分为三部分分别介绍Java类加载的时机、类加载的过程、加载器。
一、Java类加载的时机
1.1 类加载的生命周期
类加载的生命周期是从类被加载到内存开始,直到卸载出内存为止的。整个生命周期分为7个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中,验证、准备、解析三部分统称为连接。具体步骤如下图所示:
下面简单介绍下类加载器所执行的生命周期的过程。
(1) 装载:查找和导入Class文件;
(2) 链接:把类的二进制数据合并到JRE中;
(a)校验:检查载入Class文件数据的正确性;
(b)准备:给类的静态变量分配存储空间;
(c)解析:将符号引用转成直接引用;
(3) 初始化:对类的静态变量,静态代码块执行初始化操作。
1.2 类加载的时机
类加载的时机Java虚拟机规范中并没有强制规定,但是对于初始化阶段,有5种场景必须立即执行初始化,也被称为主动引用。
(1) 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3) 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
(5)当使用JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且该方法句柄所对应的类没有初始化过,则先触发初始化。
二、Java类加载的过程
类加载的全过程分为7个阶段,但是主要的过程是加载、验证、准备、解析、初始化这5个阶段。
2.1 加载
在加载阶段,虚拟机需要完成3件事情:
(1) 通过一个类的全限定名来获取定义此类的二进制字节流;
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
2.2 验证
验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。整体来看,验证阶段大致分为4个验证动作。
(1)文件格式验证
第一阶段是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。该阶段是基于二进制字节流验证的,只有通过了这个阶段的验证,字节流才会进入内存的方法去中存储,后面的3个验证都是基于方法区的存储结构进行的。
这一阶段可能的验证点:
a.是否以魔数开头;
b.主、次版本号是否在当前虚拟机处理范围内;
c.常量池的常量数据类型是否被支持;
。。。
(2)元数据验证
元数据验证是对字节码描述信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。这个阶段可能的验证点:
a.是否有父类;
b.是否继承了不被允许继承的类;
c.如果该类不是抽象类,是否实现了其父类或接口要求实现的所有方法;
。。。
(3)字节码验证
字节码验证的主要目的是通过数据流和控制流分析,确定程序语义的合法性和逻辑性。该阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。这个阶段可能的验证点:
a.保证任何时候操作数栈的数据类型与指令代码序列的一致性;
b.跳转指令不会跳转到方法体以外的字节码指令上;
。。。
(4)符号引用验证
符号引用验证的主要目的是保证解析动作能正常执行,如果无法通过符号引用验证,则会抛出异常。这个阶段可能的验证点:
a.符号引用的类、字段、方法的访问性(public、private等)是否可被当前类访问;
b.指定类是否存在符合方法的字段描述符;
。。。
2.3 准备
准备阶段是正式为类变量分配并设置类变量初始值的阶段,这些内存都将在方法区中进行分配,需要说明的是:这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;这里所说的初始值“通常情况”是数据类型的零值,例如:
public static int value = 1;
value在准备阶段过后的初始值为0而不是1,而把value赋值的putstatic指令将在初始化阶段才会被执行。
特殊情况:
public static final int value = 1;//此时准备value赋值为1
2.4 解析
解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程。直接引用是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存有关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用不尽相同。
2.5 初始化
初始化阶段是类加载过程的最后一步,到了该阶段才真正开始执行类定义的Java程序代码,根据程序员通过代码定制的主观计划去初始化类变量和其他资源,是执行类构造器初始化方法的过程。
三、类加载器
类加载器大致可以分为以下3部分:
(1) 启动类加载器: 将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
(2) 扩展类加载器 : 将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。
(3) 应用程序类加载器: 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。
我们的应用程序都是由这三种类加载器相互配合加载的。它们的关系如下图所示,称之为双亲委派模型。
工作过程:如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
好处:java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
双亲委派模型实现起来其实很简单,以下是实现代码,通过以下代码,可以对JVM采用的双亲委派类加载机制有了更感性的认识。
//类加载过程
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//没有被加载,就委托给父类加载器或者委派给启动类加载器加载
try {
if (parent != null) {
// 如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果不存在父类加载器,就检查是否是由启动类加载器加载的类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 如果父类加载器和启动类加载器都不能完成加载任务,调用自身的加载工程
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
总结:
本文从Java类加载的时机、类加载的过程以及类加载的方式三方面对Java类加载机制进行了浅析,希望通过阅读本文可以对Java类加载机制有个大致的了解。
内存模型
由上图可以清楚的看到JVM的内存空间分为3大部分:
堆内存
方法区
栈内存
其中栈内存可以再细分为java虚拟机栈和本地方法栈,堆内存可以划分为新生代和老年代,新生代中还可以再次划分为Eden区、From Survivor区和To Survivor区。
其中一部分是线程共享的,包括 Java 堆和方法区;另一部分是线程私有的,包括虚拟机栈和本地方法栈,以及程序计数器这一小部分内存。
堆内存(Heap)
对于大多数应用来说,Java 堆(Java Heap)是Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆内存是所有线程共有的,可以分为两个部分:年轻代和老年代。
下图中的Perm代表的是永久代,但是注意永久代并不属于堆内存中的一部分,同时jdk1.8之后永久代已经被移除。
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
方法区(Method Area)
方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。
在JDK8之前的HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在JVM启动之前通过在命令行设置参数-XX:MaxPermSize来设定永久代最大可分配的内存空间,默认大小是64M(64位JVM默认是85M)。
随着JDK8的到来,JVM不再有 永久代(PermGen)。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory。
方法区或永生代相关设置
-XX:PermSize=64MB 最小尺寸,初始分配
-XX:MaxPermSize=256MB 最大允许分配尺寸,按需分配
XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 设置垃圾不回收
默认大小
-server选项下默认MaxPermSize为64m
-client选项下默认MaxPermSize为32m
虚拟机栈(JVM Stack)
描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个"栈帧",用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- Java虚拟机栈(JVM Stack)
2.1. 定义
相对于基于寄存器的运行环境来说,JVM是基于栈结构的运行环境
栈结构移植性更好,可控性更强
JVM中的虚拟机栈是描述Java方法执行的内存区域,它是线程私有的
栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程
在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧
正在执行的方法称为当前方法
栈帧是方法运行的基本结构
在执行引擎运行时,所有指令都只能针对当前栈帧进行操作
StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中
JVM能够横扫千军,虚拟机栈就是它的心腹大将,当前方法的栈帧,都是正在战斗的战场,其中的操作栈是参与战斗的士兵
虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上
在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定
栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等
局部变量表
存放方法参数和局部变量
相对于类属性变量的准备阶段和初始化阶段来说,局部变量没有准备阶段,必须显式初始化
如果是非静态方法,则在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量
字节码指令中的STORE指令就是将操作栈中计算完成的局部变量写回局部变量表的存储空间内
操作栈
操作栈是一个初始状态为空的桶式结构栈
在方法执行过程中,会有各种指令往栈中写入和提取信息
JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈
字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的stack属性中
下面用一段简单的代码说明操作栈与局部变量表的交互
详细的字节码操作顺序如下:
第1处说明:局部变量表就像个中药柜,里面有很多抽屉,依次编号为0, 1, 2,3,.,. n
字节码指令istore_ 1就是打开1号抽屉,把栈顶中的数13存进去
栈是一个很深的竖桶,任何时候只能对桶口元素进行操作,所以数据只能在栈顶进行存取
某些指令可以直接在抽屉里进行,比如inc指令,直接对抽屉里的数值进行+1操作
程序员面试过程中,常见的i++和++i的区别,可以从字节码上对比出来
iload_ 1从局部变量表的第1号抽屉里取出一个数,压入栈顶,下一步直接在抽屉里实现+1的操作,而这个操作对栈顶元素的值没有影响
所以istore_ 2只是把栈顶元素赋值给a
表格右列,先在第1号抽屉里执行+1操作,然后通过iload_ 1 把第1号抽屉里的数压入栈顶,所以istore_ 2存入的是+1之后的值
这里延伸一个信息,i++并非原子操作。即使通过volatile关键字进行修饰,多个线程同时写的话,也会产生数据互相覆盖的问题.
动态连接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接
方法返回地址
方法执行时有两种退出情况
正常退出
正常执行到任何方法的返回字节码指令,如RETURN、IRETURN、ARETURN等
异常退出
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧
退出可能有三种方式:
返回值压入,上层调用栈帧
异常信息抛给能够处理的栈帧
PC计数器指向方法调用后的下一条指令
Java虚拟机栈是描述Java方法运行过程的内存模型
Java虚拟机栈会为每一个即将运行的Java方法创建“栈帧”
用于存储该方法在运行过程中所需要的一些信息
局部变量表
存放基本数据类型变量、引用类型的变量、returnAddress类型的变量
操作数栈
动态链接
当前方法的常量池指针
当前方法的返回地址
方法出口等信息
每一个方法从被调用到执行完成的过程,都对应着一个个栈帧在JVM栈中的入栈和出栈过程
注意:人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!这里的“堆”可以这么理解,但这里的“栈”就是现在讲的虚拟机栈,或者说Java虚拟机栈中的局部变量表部分.
真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息.
2.2. 特点
局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建.
而且表的大小在编译期就确定,在创建的时候只需分配事先规定好的大小即可.
在方法运行过程中,表的大小不会改变
Java虚拟机栈会出现两种异常
StackOverFlowError
若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求的栈深度大于虚拟机允许的最大深度时(但内存空间可能还有很多),就抛出此异常
OutOfMemoryError
若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常
Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡.
本地方法栈(Native Stack)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
程序计数器(PC Register)
程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。
当执行java方法时候,计数器中保存的是字节码文件的行号;当执行Native方法时,计数器的值为空。
直接内存
直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
JVM内存参数设置
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-Xmn:设置年轻代大小
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
典型JVM参数配置参考:
java-Xmx3550m-Xms3550m-Xmn2g-Xss128k
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC-XX:+UseParNewGC
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大 小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000 左右。
内存分配与内存回收(GC)
java虚拟机的内存分配与回收机制
分为4个方面来介绍内存分配与回收,分别是内存是如何分配的、哪些内存需要回收、在什么情况下执行回收、如何监控和优化GC机制。
java GC(Garbage Collction)垃圾回收机制,是java与C/C++的主要区别之一。通过对jvm中内存进行标记,自主回收一些无用的内存。目前使用的最多的是sun公司jdk中的HotSpot,所以本文也以该jvm作为介绍的根本。
1.Java内存区域
在java运行时的数据取里,由jvm管理的内存区域分为多个部分:
程序计数器(program counter register):程序计数器是一个比较校的内存单元,用来表示当前程序运行哪里的一个指示器。由于每个线程都由自己的执行顺序,所以程序计数器是线程私有的,每个线程都要由一个自己的程序计数器来指示自己(线程)下一步要执行哪条指令。
如果程序执行的是一个java方法,那么计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地方法(native方法),那么计数器的值为Undefined。由于程序计数器记录的只是当前指令地址,所以不存在内存泄漏的情况,也是jvm内存区域中唯一一个没有OOME(out of memory error)定义的区域。
虚拟机栈(JVM stack):当线程的每个方法在执行的时候都会创建一个栈帧(Stack Frame)用来存储方法中的局部变量、方法出口等,同时会将这个栈帧放入JVM栈中,方法调用完成时,这个栈帧出栈。每个线程都要一个自己的虚拟机栈来保存自己的方法调用时候的数据,因此虚拟机栈也是线程私有的。
虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,抛出StackOverFlowError,不过虚拟机基本上都允许动态扩展虚拟机栈的大小。这样的话线程可以一直申请栈,直到内存不足的时候,会抛出OOME(out of memory error)内存溢出。
本地方法栈(Native Method Stack):本地方法栈与虚拟机栈类似,只是本地方法栈存放的栈帧是在native方法调用的时候产生的。有的虚拟机中会将本地方法栈和虚拟栈放在一起,因此本地方法栈也是线程私有的。
堆(Heap):堆是java GC机制中最重要的区域。堆是为了放置“对象的实例”,对象都是在堆区上分配内存的,堆在逻辑上连续,在物理上不一定连续。所有的线程共用一个堆,堆的大小是可扩展的,如果在执行GC之后,仍没有足够的内存可以分配且堆大小不可再扩展,将会抛出OOME。
方法区(Method Area):又叫静态区,用于存储类的信息、常量池等,逻辑上是堆的一部分,是各个线程共享的区域,为了与堆区分,又叫非堆。在永久代还存在时,方法区被用作永久代。方法区可以选择是否开启垃圾回收。jvm内存不足时会抛出OOME。
直接内存(Direct Memory):直接内存指的是非jvm管理的内存,是机器剩余的内存。用基于通道(Channel)和缓冲区(Buffer)的方式来进行内存分配,用存储在JVM中的DirectByteBuffer来引用,当机器本身内存不足时,也会抛出OOME。
举例说明:Object obj = new Object();
obj表示一个本地引用,存储在jvm栈的本地变量表中,new Object()作为一个对象放在堆中,Object类的类型信息(接口,方法,对象类型等)放在堆中,而这些类型信息的地址放在方法区中。
这里需要知道如何通过引用访问到具体对象,也就是通过obj引用如何找到new出来的这个Object()对象,主要有两种方法,通过句柄和通过直接指针访问。
通过句柄:
在java堆中会专门有一块区域被划分为句柄池,一个引用的背后是一个对象实例数据(java堆中)的指针和对象类型信息(方法区中)的指针,而这两个指针都是在java堆上的。这种方法是优势是较为稳定,但是速度不是很快。
通过直接指针:
一个引用背后是一个对象的实例数据,这个实例数据里面包含了“到对象类型信息的指针”。这种方式的优势是速度快,在HotSpot中用的就是这种方式。
2.内存是如何分配和回收的
内存分配主要是在堆上的分配,如前面new出来的对象,放在堆上,但是现代技术也支持在栈上分配,较为少见,本文不考虑。分配内存与回收内存的标准是八个字:分代分配,分代回收。那么这个代是什么呢?
jvm中将对象根据存活的时间划分为三代:年轻代(Young Generation)、年老代(Old Generation)和永久代(Permannent Generation)。在jdk1.8中已经不再使用永久代,因此这里不再介绍。
年轻代:又叫新生代,所有新生成的对象都是先放在年轻代。年轻代分三个区,一个Eden区,两个Survivor区,一个叫From,一个叫To(这个名字是动态变化的)。当Eden中满时,执行Minor GC将消亡的对象清理掉,仍存活的对象将被复制到Survivor中的From区,清空Eden。当这个From区满的时候,仍存活的对象将被复制到To区,清空From区,并且原From区变为To区,原To区变为From区,这样的目的是保证To区一直为空。当From区满无对象可清理或者From-To区交换的次数超过设定(HotSpot默认为15,通过-XX:MaxTenuringThreashold控制)的时候,仍存活的对象进入老年代。年轻代中Eden和Servivor的比例通过-XX:SerivorRation参数来配置,默认为8,也就时说Eden:From:To=8:1:1。年轻代的回收方式叫做Minor GC,又叫停止-复制清理法。这种方法在回收的时候,需要暂停其他所有线程的执行,导致效率很低,现在虽然有优化,但是仅仅是将停止的时间变短,并没有彻底取消这个停止。
年老代:年老代的空间较大,当年老代内存不足时,将执行Major GC也叫Full GC。如果对象比较大,可能会直接分配到老年代上而不经过年轻代。用-XX:pertenureSizeThreashold来设定这个值,大于这个的对象会直接分配到老年代上。
3.垃圾收集器
在GC机制中,起作用的是垃圾收集器。HotSpot1.6中使用的垃圾收集器如下(有连线表示有联系):
Serial收集器:新生代(年轻代)收集器,使用停止-复制算法,使用一个线程进行GC,其他工作线程暂停。
ParNew收起:新生代收集器,使用停止-复制算法,Serial收集器的多线程版,用多个线程进行GC,其他工作线程暂停,关注缩短垃圾收集时间。
Parallel Scavenge收集器:新生代收集器,使用停止-复制算法,关注CPU吞吐量,即运行用户代码的时间/总时间。
Serial Old收集器:年老代收集器,单线程收集器,使用标记-整理算法(整理的方法包括sweep清理和compact压缩,标记-清理是先标记需要回收的对象,在标记完成后统一清楚标记的对象,这样清理之后空闲的内存是不连续的;标记-压缩是先标记需要回收的对象,把存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这样清理之后空闲的内存是连续的)。
Parallel Old收集器:老年代收集器,多线程收集器,使用标记-整理算法(整理的方法包括summary汇总和compact压缩,标记-压缩与Serial Old一样,标记-汇总是将幸存的对象复制到预先准备好的区域,再清理之前的对象)。
CMS(Concurrent Mark Sweep)收集器:老年老代收集器,多线程收集器,关注最短回收时间停顿,使用标记-清除算法,用户线程可以和GC线程同时工作。
G1收集器:JDK1.7中发布,使用较少,不作介绍。
Java GC是一个非常复杂的机制,想要详细说清楚他需要很多时间,如有错误恳请指正。
JVM线上调优
Azkaban执行多次调度任务之后,就会进入preparation 等待状态,整个服务器就卡住。修改初始堆内存与最大堆内存为2G,
一般和Xmx配置成一样以避免每次gc后JVM重新分配内存。
之后还是不行。后面通过jstat定位到fullGC时间等待太长,通过jstack定位线程问题,最后发现是每次打包etl脚本,执行task之前,Azkaban会进行一个遍历包含etl脚本的压缩文件。直接读取文件到内存之中,文件过大,直接造成频繁fullGC。最后通过执行完task,清空压缩包的方式。解决了问题
一个性能较好的web服务器jvm参数配置:
-server//服务器模式
-Xmx2g //JVM最大允许分配的堆内存,按需分配
-Xms2g //JVM初始分配的堆内存,一般和Xmx配置成一样以避免每次gc后JVM重新分配内存。
-Xmn256m //年轻代内存大小,整个JVM内存=年轻代 + 年老代 + 持久代
-XX:PermSize=128m //持久代内存大小
-Xss256k //设置每个线程的堆栈大小
-XX:+DisableExplicitGC //忽略手动调用GC, System.gc()的调用就会变成一个空调用,完全不触发GC
-XX:+UseConcMarkSweepGC //并发标记清除(CMS)收集器
-XX:+CMSParallelRemarkEnabled //降低标记停顿
-XX:+UseCMSCompactAtFullCollection //在FULL GC的时候对年老代的压缩
-XX:LargePageSizeInBytes=128m //内存页的大小
-XX:+UseFastAccessorMethods //原始类型的快速优化
-XX:+UseCMSInitiatingOccupancyOnly //使用手动定义初始化定义开始CMS收集
-XX:CMSInitiatingOccupancyFraction=70 //使用cms作为垃圾回收使用70%后开始CMS收集
1.jvm调优总结:
https://www.cnblogs.com/andy-zhou/p/5327288.html#_caption_19
2.大型跨境电商JVM调优经历:
https://scholers.iteye.com/blog/2411414
3.一次线上JVM调优实践,FullGC40次/天到10天一次的优化过程:https://blog.youkuaiyun.com/cml_blog/article/details/81057966
JVM调优参数简介、调优目标及调优经验
一、JVM调优参数简介
1、 JVM参数简介
-XX 参数被称为不稳定参数,之所以这么叫是因为此类参数的设置很容易引起JVM 性能上的差异,使JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高JVM 的性能及稳定性。
不稳定参数语法规则:
1.布尔类型参数值
-XX:+ '+'表示启用该选项
-XX:- '-'表示关闭该选项
2.数字类型参数值:
-XX:= 给选项设置一个数字类型值,可跟随单位,例如:'m’或’M’表示兆字节;'k’或’K’千字节;'g’或’G’千兆字节。32K与32768是相同大小的。
3.字符串类型参数值:
-XX:= 给选项设置一个字符串类型值,通常用于指定一个文件、路径或一系列命令列表。
例如:-XX:HeapDumpPath=./dump.core
2、 JVM参数示例
配置: -Xmx4g –Xms4g –Xmn1200m –Xss512k -XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:PermSize=100m
-XX:MaxPermSize=256m -XX:MaxTenuringThreshold=15
解析:
-Xmx4g:堆内存最大值为4GB。
-Xms4g:初始化堆内存大小为4GB 。
-Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
-XX:PermSize=100m:初始化永久代大小为100MB。
-XX:MaxPermSize=256m:设置持久代大小为256MB。
-XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
二、JVM调优目标
-
何时需要做jvm调优?
- heap 内存(老年代)持续上涨达到设置的最大内存值;
- Full GC 次数频繁;
- GC 停顿时间过长(超过1秒);
- 应用出现OutOfMemory 等内存异常;
- 应用中有使用本地缓存且占用大量内存空间;
- 系统吞吐量与响应性能不高或下降。
-
JVM调优原则
1.多数的Java应用不需要在服务器上进行JVM优化;
2.多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
3.在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
4.减少创建对象的数量;
5.减少使用全局变量和大对象;
6.JVM优化是到最后不得已才采用的手段;
7.在实际使用中,分析GC情况优化代码比优化JVM参数更好;
-
JVM调优目标
-
GC低停顿;
-
GC低频率;
-
低内存占用;
-
高吞吐量;
-
JVM调优量化目标(示例):
1. Heap 内存使用率 <= 70%;
2. Old generation内存使用率<= 70%;
3. avgpause <= 1秒;
4. Full gc 次数0 或 avg pause interval >= 24小时 ;
注意:不同应用,其JVM调优量化目标是不一样的。
三、JVM调优经验
1. JVM调优经验总结
JVM调优的一般步骤为:
-
第1步:分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点;
-
第2步:确定JVM调优量化目标;
-
第3步:确定JVM调优参数(根据历史JVM参数来调整);
-
第4步:调优一台服务器,对比观察调优前后的差异;
-
第5步:不断的分析和调整,直到找到合适的JVM参数配置;
-
第6步:找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。
2. JVM调优重要参数解析
注意:不同应用,其JVM最佳稳定参数配置是不一样的。
配置: -server
-Xms12g -Xmx12g -XX:PermSize=500m -XX:MaxPermSize=1000m -Xmn2400m -XX:SurvivorRatio=1 -Xss512k -XX:MaxDirectMemorySize=1G
-XX:+DisableExplicitGC -XX:CompileThreshold=8000 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseCompressedOops -XX:CMSInitiatingOccupancyFraction=60 -XX:ConcGCThreads=4
-XX:MaxTenuringThreshold=10 -XX:ParallelGCThreads=8
-XX:+ParallelRefProcEnabled -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled
-XX:CMSMaxAbortablePrecleanTime=500 -XX:CMSFullGCsBeforeCompaction=4
XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCMSCompactAtFullCollection
-XX:+HeapDumpOnOutOfMemoryError -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/weblogic/gc/gc_$$.log
重要参数(可调优)解析:
-Xms12g:初始化堆内存大小为12GB。
-Xmx12g:堆内存最大值为12GB 。
-Xmn2400m:新生代大小为2400MB,包括 Eden区与2个Survivor区。
-XX:SurvivorRatio=1:Eden区与一个Survivor区比值为1:1。
-XX:MaxDirectMemorySize=1G:直接内存。报java.lang.OutOfMemoryError: Direct buffer memory 异常可以上调这个值。
-XX:+DisableExplicitGC:禁止运行期显式地调用 System.gc() 来触发fulll GC。
注意: Java RMI的定时GC触发机制可通过配置-Dsun.rmi.dgc.server.gcInterval=86400来控制触发的时间。
-XX:CMSInitiatingOccupancyFraction=60:老年代内存回收阈值,默认值为68。
-XX:ConcGCThreads=4:CMS垃圾回收器并行线程线,推荐值为CPU核心数。
-XX:ParallelGCThreads=8:新生代并行收集器的线程数。
-XX:MaxTenuringThreshold=10:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
-XX:CMSFullGCsBeforeCompaction=4:指定进行多少次fullGC之后,进行tenured区 内存空间压缩。
-XX:CMSMaxAbortablePrecleanTime=500:当abortable-preclean预清理阶段执行达到这个时间时就会结束。
3. 触发Full GC的场景及应对策略
年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,对老年代GC称为MajorGC,而Full GC是对整个堆来说的,在最近几个版本的JDK里默认包括了对永生带即方法区的回收(JDK8中无永生带了),出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对的。MajorGC的速度一般会比Minor GC慢10倍以上。
触发Full GC的场景及应对策略:
- 1.System.gc()方法的调用,应对策略:通过-XX:+DisableExplicitGC来禁止调用System.gc ;
- 2.老年代代空间不足,应对策略:让对象在Minor GC阶段被回收,让对象在新生代多存活一段时间,不要创建过大的对象及数组;
- 3.永生区空间不足,应对策略:增大PermGen空间
- 4.GC时出现promotionfailed和concurrent mode failure,应对策略:增大survivor space
- 5.Minor GC后晋升到旧生代的对象大小大于老年代的剩余空间,应对策略:增大Tenured space 或下调CMSInitiatingOccupancyFraction=60
-
- 内存持续增涨达到上限导致Full GC ,应对策略:通过dumpheap 分析是否存在内存泄漏
4. Gc日志分析工具
借助GCViewer日志分析工具,可以非常直观地分析出待调优点。
可从以下几方面来分析:
-
1.Memory,分析Totalheap、Tenuredheap、Youngheap内存占用率及其他指标,理论上内存占用率越小越好;
2.Pause ,分析Gc pause、Fullgc pause、Total pause三个大项中各指标,理论上GC次数越少越好,GC时长越小越好;
5. MAT 堆内存分析工具
EclipseMemory Analysis Tools (MAT) 是一个分析Java堆数据的专业工具,用它可以定位内存泄漏的原因。
JVM内存设置多大合适?Xmx和Xmn如何设置?
问题:
新上线一个java服务,或者是RPC或者是WEB站点, 内存的设置该怎么设置呢?设置成多大比较合适,既不浪费内存,又不影响性能呢?
分析:
依据的原则是根据Java Performance里面的推荐公式来进行设置。
具体来讲:
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍
永久代 PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍。
年轻代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。
BTW:
1、Sun官方建议年轻代的大小为整个堆的3/8左右, 所以按照上述设置的方式,基本符合Sun的建议。
2、堆大小=年轻代大小+年老代大小, 即xmx=xmn+老年代大小 。 Permsize不影响堆大小。
3、为什么要按照上面的来进行设置呢? 没有具体的说明,但应该是根据多种调优之后得出的一个结论。
如何确认老年代存活对象大小?
方式1(推荐/比较稳妥):
JVM参数中添加GC日志,GC日志中会记录每次FullGC之后各代的内存大小,观察老年代GC之后的空间大小。可观察一段时间内(比如2天)的FullGC之后的内存情况,根据多次的FullGC之后的老年代的空间大小数据来预估FullGC之后老年代的存活对象大小(可根据多次FullGC之后的内存大小取平均值)
方式2:(强制触发FullGC, 会影响线上服务,慎用)
方式1的方式比较可行,但需要更改JVM参数,并分析日志。同时,在使用CMS回收器的时候,有可能不能触发FullGC(只发生CMS GC),所以日志中并没有记录FullGC的日志。在分析的时候就比较难处理。
BTW:使用jstat -gcutil工具来看FullGC的时候, CMS GC是会造成2次的FullGC次数增加。 具体可参见之前写的一篇关于jstat使用的文章
所以,有时候需要强制触发一次FullGC,来观察FullGC之后的老年代存活对象大小。
注:强制触发FullGC,会造成线上服务停顿(STW),要谨慎,建议的操作方式为,在强制FullGC前先把服务节点摘除,FullGC之后再将服务挂回可用节点,对外提供服务
在不同时间段触发FullGC,根据多次FullGC之后的老年代内存情况来预估FullGC之后的老年代存活对象大小
如何触发FullGC ?
使用jmap工具可触发FullGC
jmap -dump:live,format=b,file=heap.bin 将当前的存活对象dump到文件,此时会触发FullGC
jmap -histo:live 打印每个class的实例数目,内存占用,类全名信息.live子参数加上后,只统计活的对象数量. 此时会触发FullGC
具体操作实例:
以我司的一个RPC服务为例。
BTW:刚上线的新服务,不知道该设置多大的内存的时候,可以先多设置一点内存,然后根据GC之后的情况来进行分析。
初始JVM内存参数设置为: Xmx=2G Xms=2G xmn=1G
使用jstat 查看当前的GC情况。如下图:
YGC平均耗时: 173.825s/15799=11ms
FGC平均耗时:0.817s/41=19.9ms
平均大约10-20s会产生一次YGC
看起来似乎不错,YGC触发的频率不高,FGC的耗时也不高,但这样的内存设置是不是有些浪费呢?
为了快速看数据,我们使用了方式2,产生了几次FullGC,FullGC之后,使用的jmap -heap 来看的当前的堆内存情况(也可以根据GC日志来看)
heap情况如下图:(命令 : jmap -heap )
上图中的concurrent mark-sweep generation即为老年代的内存描述。
老年代的内存占用为100M左右。 按照整个堆大小是老年代(FullGC)之后的3-4倍计算的话,设置各代的内存情况如下:
Xmx=512m Xms=512m Xmn=128m PermSize=128m 老年代的大小为 (512-128=384m)为老年代存活对象大小的3倍左右
调整之后的,heap情况
GC情况如下:
YGC 差不多在10s左右触发一次。每次YGC平均耗时大约9.41ms。可接受。
FGC平均耗时:0.016s/2=8ms
整体的GC耗时减少。但GC频率比之前的2G时的要多了一些。
注: 看上述GC的时候,发现YGC的次数突然会增多很多个,比如 从1359次到了1364次。具体原因是?
总结:
在内存相对紧张的情况下,可以按照上述的方式来进行内存的调优, 找到一个在GC频率和GC耗时上都可接受的一个内存设置,可以用较小的内存满足当前的服务需要
但当内存相对宽裕的时候,可以相对给服务多增加一点内存,可以减少GC的频率,GC的耗时相应会增加一些。 一般要求低延时的可以考虑多设置一点内存, 对延时要求不高的,可以按照上述方式设置较小内存。
补充:
永久代(方法区)并不在堆内,所以之前有看过一篇文章中描述的 整个堆大小=年轻代+年老代+永久代的描述是不正确的。
问题:
1、GC频率和GC时间多少合适?
2、YGC何时触发,FGC何时触发?
3、内存设置越大,GC的耗时是否就会越长?为什么?
JVM总结(三)Minor GC、Major GC和Full GC
一、Minor GC
Minor GC是指从年轻代空间(包括 Eden 和 Survivor 区域)回收内存。当 JVM 无法为一个新的对象分配空间时会触发
Minor GC,比如当 Eden 区满了。
Eden区满了触发MinorGC,这时会把Eden区存活的对象复制到Survivor区,当对象在Survivor区熬过一定次数的Minor
GC之后,就会晋升到老年代(当然并不是所有的对象都是这样晋升的到老年代的),当老年代满了,就会报OutofMemory异常。
所有的MinorGC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,不过这个过程非常短暂。 执行 Minor GC 操作时,不会影响到永久代。
二、Major GC vs Full GC
在目前的项目中还没有明确的定义,这点需要注意。JVM规范和垃圾收集研究论文都没有提及,但是乍一看,这些建立在我们掌握了Minor GC清理新生代上的定义并非难事:
Major GC清理Tenured区(老年代)。
Full GC清理整个heap区,包括Yong区和Tenured区。
Full GC触发条件
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小 > 老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。即老年代无法存放下新年代过度到老年代的对象的时候,会触发Full GC。
补充
以上的GC总结,只是在非并发GC的触发条件下的大致原理。真正的GC情况跟实际GC器的回收机制有关。不同的GC器对Major GC 和 Full GC 的机制还是有区别的。如JVM中Serial GC, Parallel GC, CMS, G1 GC。会在后续的总结中去总结。
集合
实现 Java list,要求实现 list 的 get(), add(), remove() 三个功能函数,不能直接使用 ArrayList、LinkedList 等 Java 自带高级类(阿里面试题)
数据结构与算法精选面试题
https://www.cnblogs.com/developer_chan/p/11439711.html
HashMap源码解读
https://www.jianshu.com/p/8b6eb2fd15ab
ArrayList源码解读
https://www.cnblogs.com/cocoxu1992/p/10570952.html
LinkList源码解读
https://www.cnblogs.com/developer_chan/p/11439711.html
自己实现一个arraylsit
import java.util.Arrays;
import java.util.Iterator;
public class MyList<T> implements Iterable<T>{
/*
* 设计一种容器:可以不初始化长度,长度可以自动拓展
*/
private int length = 10;//定义初始化容量大小的变量
private Object[] arr = new Object[length];//建立数组用于存储元素
private int index = 0;//建立整数索引,用于记录当前有几个元素
public MyList(int length){
this.length = length;
}
public MyList(){}
//添加元素的方法
public void add(T obj){
//如果元素超出了当前数组的长度,那么需要扩展长度
if(index>=arr.length){
//将原本的元素拷贝到新数组,并且数组长度增加10
Object[] newArr = Arrays.copyOf(arr,arr.length+10);
arr = newArr;
}
arr[index++] = obj;//往数组内添加元素,之后索引自增1
}
//获取元素
public T get(int index){
checkIndex(index);
return (T)arr[index];
}
//定义方法,验证下标
private void checkIndex(int index){
if(index>=this.index){
throw new IndexOutOfBoundsException("下标越界:"+index);
}
}
//删除方法
public void remove(int index){
checkIndex(index);
Object[] newArr = new Object[arr.length];
//现将需要删除的元素的前面的所有元素复制
System.arraycopy(arr,0,newArr,0,index);
//复制要删除元素的后面的所有的元素
System.arraycopy(arr,index+1,newArr,index,this.index-index-1);
this.index--;
arr = newArr;
}
//set方法
public void set(int index,T obj){
checkIndex(index);
arr[index] = obj;
}
//toString
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < index; i++)
{
sb.append(arr[i]);
if(i < index-1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
//length
public int length(){
return index;
}
//addAll
public void addAll(MyList<T> mm){
for (int i = 0; i < mm.length(); i++)
{
this.add(mm.get(i));
}
}
//toArray
public Object[] toArray(){
Object[] res = Arrays.copyOf(arr, index);
return res;
}
//remove
public void remove(T t){
for (int i = 0; i < index; i++)
{
if(arr[i].equals(t)){
this.remove(i);
}
}
}
//removeAll
public void removeAll(MyList<T> mm){
for (int i = 0; i < mm.length(); i++)
{
this.remove(mm.get(i));
}
}
//insert
public void insert(int index,T t){
checkIndex(index);
//建立新数组
Object[] newArr = new Object[arr.length+1];
//复制将要添加的元素索引的之前的所有元素
System.arraycopy(arr, 0, newArr, 0, index);
newArr[index] = t;//将要插入的元素放入新数组的指定位置
System.arraycopy(arr, index, newArr,index+1,arr.length-index);
this.index++;
arr = newArr;
}
@Override
public Iterator<T> iterator()
{
Iterator<T> ite = new Iterator<T>()
{
int index = 0;
@Override
public boolean hasNext()
{
if(index<MyList.this.index){
return true;
}else{
return false;
}
}
@Override
public T next()
{
return (T)arr[index++];
}};
return ite;
}
————————————————
版权声明:本文为优快云博主「静夜思乡」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wanghuiwei888/article/details/78876134
Hash是什么?HashCode是什么?Hash表是什么?为什么要用HashCode?
Hash
百度百科解释:
Hash 又叫 散列函数,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
常用HASH函数
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。常用Hash函数有:
1.直接寻址法。取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)
2. 数字分析法。分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法。取关键字平方后的中间几位作为散列地址。
4. 折叠法。将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
5. 随机数法。选择一随机函数,取关键字作为随机函数的种子生成随机值作为散列地址,通常用于关键字长度不同的场合。
6. 除留余数法。取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。
处理冲突方法
1.开放寻址法;Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),其中H(key)为散列函数,m为散列表长,di为增量序列,可有下列三种取法:
1). di=1,2,3,…,m-1,称线性探测再散列;
2). di=12,-12,22,-22,32,…,±k2,(k<=m/2)称二次探测再散列;
3). di=伪随机数序列,称伪随机探测再散列。
2. 再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数,即在同义词产生地址冲突时计算另一个散列函数地址,直到冲突不再发生,这种方法不易产生“聚集”,但增加了计算时间。
3. 链地址法(拉链法)
4. 建立一个公共溢出区
查找性能分析
散列表的查找过程基本上和造表过程相同。一些关键码可通过散列函数转换的地址直接找到,另一些关键码在散列函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对散列表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
1.散列函数是否均匀;
2. 处理冲突的方法;
3.散列表的装填因子。
散列表的装填因子定义为:α= 填入表中的元素个数/散列表的长度
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。
实际上,散列表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。
了解了hash基本定义,就不能不提到一些著名的hash算法,MD5和SHA-1可以说是应用最广泛的Hash算法,而它们都是以MD4为基础设计的。
常用hash算法的介绍:
(1)MD4
MD4(RFC 1320)是 MIT 的Ronald L. Rivest在 1990 年设计的,MD 是 Message Digest(消息摘要) 的缩写。它适用在32位字长的处理器上用高速软件实现——它是基于 32位操作数的位操作来实现的。
(2)MD5
MD5(RFC 1321)是 Rivest 于1991年对MD4的改进版本。它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 相同。MD5比MD4来得复杂,并且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好。
(3)SHA-1及其他
SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于2^64的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4相同原理,并且模仿了该算法。 [2]
HashCode
百度百科的解释:
hashCode是jdk根据对象的地址或者字符串或者数字,通过hash算法,算出来的int类型的数值 详细了解请 参考 public int hashCode()返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。
hash表
通过hash算法得到的hash值就在这张hash表中,也就是说,hash表就是所有的hash值组成的,有很多种hash函数,也就代表着有很多种算法得到hash值
为什么要用HashCode?
在Java集合中有两类,一类是List,一类是Set
他们之间的区别就在于List集合中的元素师有序的,且可以重复,而Set集合中元素是无序不可重复的。
对于List好处理,但是对于Set而言我们要如何来保证元素不重复呢?
通过迭代来equals()是否相等。数据量小还可以接受,当我们的数据量大的时候效率可想而知(当然我们可以利用算法进行优化)。
比如我们向HashSet插入1000数据,难道我们真的要迭代1000次,调用1000次equals()方法吗?hashCode提供了解决方案。
怎么实现?我们先看hashCode的源码(Object)。
public native int hashCode();
它是一个本地方法,它的实现与本地机器有关,这里我们暂且认为他返回的是对象存储的物理位置(实际上不是,这里写是便于理解)。
当我们向一个集合中添加某个元素,集合会首先调用hashCode方法,这样就可以直接定位它所存储的位置,
若该处没有其他元素,则直接保存。
若该处已经有元素存在,就调用equals方法来匹配这两个元素是否相同,
相同则不存,不同则散列到其他位置。
这样处理,当我们存入大量元素时就可以大大减少调用equals()方法的次数,极大地提高了效率。
所以hashCode在上面扮演的角色为寻域(寻找某个对象在集合中区域位置)。
hashCode可以将集合分成若干个区域,每个对象都可以计算出他们的hash码,可以将hash码分组,每个分组对应着某个存储区域,根据一个对象的hash码就可以确定该对象所存储区域,这样就大大减少查询匹配元素的数量,提高了查询效率。
hashcode详解
一、hashcode是什么?
1、hash和hash表是什么?
想要知道这个hashcode,首先得知道hash,通过百度百科看一下
hash是一个函数,该函数中的实现就是一种算法,就是通过一系列的算法来得到一个hash值,这个时候,我们就需要知道另一个东西,hash表,通过hash算法得到的hash值就在这张hash表中,也就是说,hash表就是所有的hash值组成的,有很多种hash函数,也就代表着有很多种算法得到hash值,如上面截图的三种,等会我们就拿第一种来说。
2、hashcode
有了前面的基础,这里讲解就简单了,hashcode就是通过hash函数得来的,通俗的说,就是通过某一种算法得到的,hashcode就是在hash表中有对应的位置。
每个对象都有hashcode,对象的hashcode怎么得来的呢?
首先一个对象肯定有物理地址,在别的博文中会hashcode说成是代表对象的地址,这里肯定会让读者形成误区,对象的物理地址跟这个hashcode地址不一样,hashcode代表对象的地址说的是对象在hash表中的位置,物理地址说的对象存放在内存中的地址,那么对象如何得到hashcode呢?通过对象的内部地址(也就是物理地址)转换成一个整数,然后该整数通过hash函数的算法就得到了hashcode,所以,hashcode是什么呢?就是在hash表中对应的位置。这里如果还不是很清楚的话,举个例子,hash表中有 hashcode为1、hashcode为2、(…)3、4、5、6、7、8这样八个位置,有一个对象A,A的物理地址转换为一个整数17(这是假如),就通过直接取余算法,17%8=1,那么A的hashcode就为1,且A就在hash表中1的位置。肯定会有其他疑问,接着看下面,这里只是举个例子来让你们知道什么是hashcode的意义。
二、hashcode有什么作用呢?
前面说了这么多关于hash函数,和hashcode是怎么得来的,还有hashcode对应的是hash表中的位置,可能大家就有疑问,为什么hashcode不直接写物理地址呢,还要另外用一张hash表来代表对象的地址?接下来就告诉你hashcode的作用,
1、HashCode的存在主要是为了查找的快捷性,HashCode是用来在散列存储结构中确定对象的存储地址的(后半句说的用hashcode来代表对象就是在hash表中的位置)
为什么hashcode就查找的更快,比如:我们有一个能存放1000个数这样大的内存中,在其中要存放1000个不一样的数字,用最笨的方法,就是存一个数字,就遍历一遍,看有没有相同得数,当存了900个数字,开始存901个数字的时候,就需要跟900个数字进行对比,这样就很麻烦,很是消耗时间,用hashcode来记录对象的位置,来看一下。hash表中有1、2、3、4、5、6、7、8个位置,存第一个数,hashcode为1,该数就放在hash表中1的位置,存到100个数字,hash表中8个位置会有很多数字了,1中可能有20个数字,存101个数字时,他先查hashcode值对应的位置,假设为1,那么就有20个数字和他的hashcode相同,他只需要跟这20个数字相比较(equals),如果每一个相同,那么就放在1这个位置,这样比较的次数就少了很多,实际上hash表中有很多位置,这里只是举例只有8个,所以比较的次数会让你觉得也挺多的,实际上,如果hash表很大,那么比较的次数就很少很少了。 通过对原始方法和使用hashcode方法进行对比,我们就知道了hashcode的作用,并且为什么要使用hashcode了
三、equals方法和hashcode的关系?
通过前面这个例子,大概可以知道,先通过hashcode来比较,如果hashcode相等,那么就用equals方法来比较两个对象是否相等,用个例子说明:上面说的hash表中的8个位置,就好比8个桶,每个桶里能装很多的对象,对象A通过hash函数算法得到将它放到1号桶中,当然肯定有别的对象也会放到1号桶中,如果对象B也通过算法分到了1号桶,那么它如何识别桶中其他对象是否和它一样呢,这时候就需要equals方法来进行筛选了。
1、如果两个对象equals相等,那么这两个对象的HashCode一定也相同
2、如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置
这两条你们就能够理解了。
四、为什么equals方法重写的话,建议也一起重写hashcode方法?
(如果对象的equals方法被重写,那么对象的HashCode方法也尽量重写)
举个例子,其实就明白了这个道理,
比如:有个A类重写了equals方法,但是没有重写hashCode方法,看输出结果,对象a1和对象a2使用equals方法相等,按照上面的hashcode的用法,那么他们两个的hashcode肯定相等,但是这里由于没重写hashcode方法,他们两个hashcode并不一样,所以,我们在重写了equals方法后,尽量也重写了hashcode方法,通过一定的算法,使他们在equals相等时,也会有相同的hashcode值。
实例:现在来看一下String的源码中的equals方法和hashcode方法。这个类就重写了这两个方法,现在为什么需要重写这两个方法了吧?
equals方法:其实跟我上面写的那个例子是一样的原理,所以通过源码又知道了String的equals方法验证的是两个字符串的值是否一样。还有Double类也重写了这些方法。很多类有比较这类的,都重写了这两个方法,因为在所有类的父类Object中。equals的功能就是 “”号的功能。你们还可以比较String对象的equals和的区别啦。这里不再说明。
hashcode方法
HashMap是如何存储的?
HashMap的工作原理是近年来常见的Java面试题。几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之间的区别,那么为何这道面试题如此特殊呢?是因为这道题考察的深度很深。这题经常出现在高级或中高级面试中。投资银行更喜欢问这个问题,甚至会要求你实现HashMap来考察你的编程能力。ConcurrentHashMap和其它同步集合的引入让这道题变得更加复杂。让我们开始探索的旅程吧!
先来些简单的问题
“你用过HashMap吗?” “什么是HashMap?你为什么用到它?”
几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而HashTable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等。这显示出你已经用过HashMap,而且对它相当的熟悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节。面试官可能会问出下面的问题:
“你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”
你也许会回答“我没有详查标准的Java API,你可以看看Java源代码或者Open JDK。”“我可以用Google找到答案。”
但一些面试者可能可以给出答案,“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在bucket中储存键对象和值对象,作为Map.Entry。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及HashMap的工作原理。但是这仅仅是故事的开始,当面试官加入一些Java程序员每天要碰到的实际场景的时候,错误的答案频现。下个问题可能是关于HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:
“当两个对象的hashcode相同会发生什么?” 从这里开始,真正的困惑开始了,一些面试者会回答因为hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。然后面试官可能会提醒他们有equals()和hashCode()两个方法,并告诉他们两个对象就算hashcode相同,但是它们可能并不相等。一些面试者可能就此放弃,而另外一些还能继续挺进,他们回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用LinkedList存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在LinkedList中。”这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最简单的,也正是HashMap的处理方法。但故事还没有完结,面试官会继续问:
“如果两个键的hashcode相同,你如何获取值对象?” 面试者会回答:当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个bucket,他给出答案:将会遍历LinkedList直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到HashMap在LinkedList中存储的是键值对,否则他们不可能回答出这一题。
其中一些记得这个重要知识点的面试者会说,找到bucket位置之后,会调用keys.equals()方法去找到LinkedList中正确的节点,最终找到要找的值对象。完美的答案!
许多情况下,面试者会在这个环节中出错,因为他们混淆了hashCode()和equals()方法。因为在此之前hashCode()屡屡出现,而equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。
如果你认为到这里已经完结了,那么听到下面这个问题的时候,你会大吃一惊。“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”除非你真正知道HashMap的工作原理,否则你将回答不出这道题。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
如果你能够回答这道问题,下面的问题来了:“你了解重新调整HashMap大小存在什么问题吗?”你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞争(race condition)。
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在LinkedList中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在LinkedList的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap呢?:)
热心的读者贡献了更多的关于HashMap的问题:
为什么String, Interger这样的wrapper类适合作为键? String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
我们可以使用自定义的对象作为键吗? 这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
我们可以使用CocurrentHashMap来代替HashTable吗?这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道HashTable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。看看这篇博客查看Hashtable和ConcurrentHashMap的区别。
我个人很喜欢这个问题,因为这个问题的深度和广度,也不直接的涉及到不同的概念。让我们再来看看这些问题设计哪些知识点:
hashing的概念
HashMap中解决碰撞的方法
equals()和hashCode()的应用,以及它们在HashMap中的重要性
不可变对象的好处
HashMap多线程的条件竞争
重新调整HashMap的大小
总结
HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用LinkedList来解决碰撞问题,当发生碰撞了,对象将会储存在LinkedList的下一个节点中。 HashMap在每个LinkedList节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的LinkedList中。键对象的equals()方法用来找到键值对。
因为HashMap的好处非常多,我曾经在电子商务的应用中使用HashMap作为缓存。因为金融领域非常多的运用Java,也出于性能的考虑,我们会经常用到HashMap和ConcurrentHashMap。你可以查看更多的关于HashMap和HashTable的文章。
HashMap碰撞原理,jdk是如何解决碰撞问题的?
hashmap冲突的解决方法以及原理分析:
在Java编程语言中,最基本的结构就是两种,一种是数组,一种是模拟指针(引用),所有的数据结构都可以用这两个基本结构构造,HashMap也一样。当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:
HashMap<String,Object> m=new HashMap<String,Object>();
m.put("a", "rrr1");
m.put("b", "tt9");
m.put("c", "tt8");
m.put("d", "g7");
m.put("e", "d6");
m.put("f", "d4");
m.put("g", "d4");
m.put("h", "d3");
m.put("i", "d2");
m.put("j", "d1");
m.put("k", "1");
m.put("o", "2");
m.put("p", "3");
m.put("q", "4");
m.put("r", "5");
m.put("s", "6");
m.put("t", "7");
m.put("u", "8");
m.put("v", "9");
HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置。当程序执行 map.put(String,Obect)方法 时,系统将调用String的 hashCode() 方法得到其 hashCode 值——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。源码如下:
Java代码 收藏代码
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断当前确定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆盖原来的旧值,并返回旧值。
//如果存在相同的hashcode,那么他们确定的索引位置就相同,这时判断他们的key是否相同,如果不相同,这时就是产生了hash冲突。
//Hash冲突后,那么HashMap的单个bucket里存储的不是一个 Entry,而是一个 Entry 链。
//系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),
//那系统必须循环到最后才能找到该元素。
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可.HashMap程序经过我改造,我故意的构造出了hash冲突现象,因为HashMap的初始大小16,但是我在hashmap里面放了超过16个元素,并且我屏蔽了它的resize()方法。不让它去扩容。这时HashMap的底层数组Entry[] table结构如下:
Hashmap里面的bucket出现了单链表的形式,散列表要解决的一个问题就是散列值的冲突问题,通常是两种方法:链表法和开放地址法。链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。java.util.HashMap采用的链表法的方式,链表是单向链表。形成单链表的核心代码如下:
Java代码 收藏代码
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
bsp;
上面方法的代码很简单,但其中包含了一个设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。
HashMap里面没有出现hash冲突时,没有形成单链表时,hashmap查找元素很快,get()方法能够直接定位到元素,但是出现单链表后,单个bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。 当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap 的 get() 与 put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。
一、HashMap概述
HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
Map map = Collections.synchronizedMap(new HashMap());
二、HashMap的数据结构
HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置。HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。
图中,紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
我们看看HashMap中Entry类的代码:
/** Entry是单向链表。
* 它是 “HashMap链式存储法”对应的链表。
*它实现了Map.Entry 接口,即实现getKey(), getValue(), setValue(V value), equals(Object o), hashCode()这些函数
**/
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 指向下一个节点
Entry<K,V> next;
final int hash;
// 构造函数。
// 输入参数包括"哈希值(h)", "键(k)", "值(v)", "下一节点(n)"
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判断两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 否则,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 实现hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 当向HashMap中添加元素时,绘调用recordAccess()。
// 这里不做任何处理
void recordAccess(HashMap<K,V> m) {
}
// 当从HashMap中删除元素时,绘调用recordRemoval()。
// 这里不做任何处理
void recordRemoval(HashMap<K,V> m) {
}
}
HashMap其实就是一个Entry数组,Entry对象中包含了键和值,其中next也是一个Entry对象,它就是用来处理hash冲突的,形成一个链表。
三、HashMap源码分析
1、关键属性
先看看HashMap类中的一些关键属性:
1 transient Entry[] table;//存储元素的实体数组
2
3 transient int size;//存放元素的个数
4
5 int threshold; //临界值 当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
6
7 final float loadFactor; //加载因子
8
9 transient int modCount;//被修改的次数
其中loadFactor加载因子是表示Hsah表中元素的填满的程度.
若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高.
因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。
2、构造方法
下面看看HashMap的几个构造方法:
public HashMap(int initialCapacity, float loadFactor) {
2 //确保数字合法
3 if (initialCapacity < 0)
4 throw new IllegalArgumentException("Illegal initial capacity: " +
5 initialCapacity);
6 if (initialCapacity > MAXIMUM_CAPACITY)
7 initialCapacity = MAXIMUM_CAPACITY;
8 if (loadFactor <= 0 || Float.isNaN(loadFactor))
9 throw new IllegalArgumentException("Illegal load factor: " +
10 loadFactor);
11
12 // Find a power of 2 >= initialCapacity
13 int capacity = 1; //初始容量
14 while (capacity < initialCapacity) //确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
15 capacity <<= 1;
16
17 this.loadFactor = loadFactor;
18 threshold = (int)(capacity * loadFactor);
19 table = new Entry[capacity];
20 init();
21 }
22
23 public HashMap(int initialCapacity) {
24 this(initialCapacity, DEFAULT_LOAD_FACTOR);
25 }
26
27 public HashMap() {
28 this.loadFactor = DEFAULT_LOAD_FACTOR;
29 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
30 table = new Entry[DEFAULT_INITIAL_CAPACITY];
31 init();
32 }
我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂,至于为什么要把容量设置为2的n次幂,我们等下再看。
重点分析下HashMap中用的最多的两个方法put和get
3、存储数据
下面看看HashMap存储数据的过程是怎样的,首先看看HashMap的put方法:
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
//搜索指定hash值在对应table中的索引
int i = indexFor(hash, table.length);
// 循环遍历Entry数组,若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果key相同则覆盖并返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//修改次数+1
modCount++;
//将key-value添加到table[i]处
addEntry(hash, key, value, i);
return null;
}
上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
我们慢慢的来分析这个函数,第2和3行的作用就是处理key值为null的情况,我们看看putForNullKey(value)方法:
1 private V putForNullKey(V value) {
2 for (Entry<K,V> e = table[0]; e != null; e = e.next) {
3 if (e.key == null) { //如果有key为null的对象存在,则覆盖掉
4 V oldValue = e.value;
5 e.value = value;
6 e.recordAccess(this);
7 return oldValue;
8 }
9 }
10 modCount++;
11 addEntry(0, null, value, 0); //如果键为null的话,则hash值为0
12 return null;
13 }
注意:如果key为null的话,hash值为0,对象存储在数组中索引为0的位置。即table[0]
我们再回去看看put方法中第4行,它是通过key的hashCode值计算hash码,下面是计算hash码的函数:
1 //计算hash值的方法 通过键的hashCode来计算
2 static int hash(int h) {
3 // This function ensures that hashCodes that differ only by
4 // constant multiples at each bit position have a bounded
5 // number of collisions (approximately 8 at default load factor).
6 h ^= (h >>> 20) ^ (h >>> 12);
7 return h ^ (h >>> 7) ^ (h >>> 4);
8 }
得到hash码之后就会通过hash码去计算出应该存储在数组中的索引,计算索引的函数如下:
1 static int indexFor(int h, int length) { //根据hash值和数组长度算出索引值
2 return h & (length-1); //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出
3 }
这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。
接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:
h & (table.length-1) hash table.length-1 8 & (15-1): 0100 & 1110 = 0100 9 & (15-1): 0101 & 1110 = 0100 ----------------------------------------------------------------------------------------------------------------------- 8 & (16-1): 0100 & 1111 = 0100 9 & (16-1): 0101 & 1111 = 0101
从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。
1 void addEntry(int hash, K key, V value, int bucketIndex) {
2 Entry<K,V> e = table[bucketIndex]; //如果要加入的位置有值,将该位置原先的值设置为新entry的next,也就是新entry链表的下一个节点
3 table[bucketIndex] = new Entry<>(hash, key, value, e);
4 if (size++ >= threshold) //如果大于临界值就扩容
5 resize(2 * table.length); //以2的倍数扩容
6 }
参数bucketIndex就是indexFor函数计算出来的索引值,第2行代码是取得数组中索引为bucketIndex的Entry对象,第3行就是用hash、key、value构建一个新的Entry对象放到索引为bucketIndex的位置,并且将该位置原先的对象设置为新对象的next构成链表。
第4行和第5行就是判断put后size是否达到了临界值threshold,如果达到了临界值就要进行扩容,HashMap扩容是扩为原来的两倍。
4、调整大小
resize()方法如下:
重新调整HashMap的大小,newCapacity是调整后的单位
1 void resize(int newCapacity) {
2 Entry[] oldTable = table;
3 int oldCapacity = oldTable.length;
4 if (oldCapacity == MAXIMUM_CAPACITY) {
5 threshold = Integer.MAX_VALUE;
6 return;
7 }
8
9 Entry[] newTable = new Entry[newCapacity];
10 transfer(newTable);//用来将原先table的元素全部移到newTable里面
11 table = newTable; //再将newTable赋值给table
12 threshold = (int)(newCapacity * loadFactor);//重新计算临界值
13 }
新建了一个HashMap的底层数组,上面代码中第10行为调用transfer方法,将HashMap的全部元素添加到新的HashMap中,并重新计算元素在新的数组中的索引位置
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
5、数据读取
1.public V get(Object key) {
2. if (key == null)
3. return getForNullKey();
4. int hash = hash(key.hashCode());
5. for (Entry<K,V> e = table[indexFor(hash, table.length)];
6. e != null;
7. e = e.next) {
8. Object k;
9. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
10. return e.value;
11. }
12. return null;
13.}
有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。
6、HashMap的性能参数:
HashMap 包含如下几个构造器:
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。
HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。
initialCapacity:HashMap的最大容量,即为底层数组的长度。
loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。
负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。
HashMap的实现中,通过threshold字段来判断HashMap的最大容量:
threshold = (int)(capacity * loadFactor);
结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍
Java 8中HashMap冲突解决
在Java 8 之前,HashMap和其他基于map的类都是通过链地址法解决冲突,它们使用单向链表来存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。为了解决在频繁冲突时hashmap性能降低的问题,Java 8中使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)。
在Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引一样时,HashMap会使用树来存储它们。
这一改变是为了继续优化常用类。大家可能还记得在Java 7中为了优化常用类对ArrayList和HashMap采用了延迟加载的机制,在有元素加入之前不会分配内存,这会减少空的链表和HashMap占用的内存。
这一动态的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值时用平衡二叉树替换链表。不过这一特性在所有基于hash table的类中并没有,例如Hashtable和WeakHashMap。
目前,只有ConcurrentHashMap,LinkedHashMap和HashMap会在频繁冲突的情况下使用平衡树。
什么时候会产生冲突
HashMap中调用hashCode()方法来计算hashCode。
由于在Java中两个不同的对象可能有一样的hashCode,所以不同的键可能有一样hashCode,从而导致冲突的产生。
总结
HashMap在处理冲突时使用链表存储相同索引的元素。
从Java 8开始,HashMap,ConcurrentHashMap和LinkedHashMap在处理频繁冲突时将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值便会由链表切换到平衡树,这会将get()方法的性能从O(n)提高到O(logn)。
当从链表切换到平衡树时,HashMap迭代的顺序将会改变。不过这并不会造成什么问题,因为HashMap并没有对迭代的顺序提供任何保证。
从Java 1中就存在的Hashtable类为了保证迭代顺序不变,即便在频繁冲突的情况下也不会使用平衡树。这一决定是为了不破坏某些较老的需要依赖于Hashtable迭代顺序的Java应用。
除了Hashtable之外,WeakHashMap和IdentityHashMap也不会在频繁冲突的情况下使用平衡树。
使用HashMap之所以会产生冲突是因为使用了键对象的hashCode()方法,而equals()和hashCode()方法不保证不同对象的hashCode是不同的。需要记住的是,相同对象的hashCode一定是相同的,但相同的hashCode不一定是相同的对象。
在HashTable和HashMap中,冲突的产生是由于不同对象的hashCode()方法返回了一样的值。
以上就是Java中HashMap如何处理冲突。这种方法被称为链地址法,因为使用链表存储同一桶内的元素。通常情况HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均采用这种方法处理冲突。
从JDK 8开始,HashMap,LinkedHashMap和ConcurrentHashMap为了提升性能,在频繁冲突的时候使用平衡树来替代链表。因为HashSet内部使用了HashMap,LinkedHashSet内部使用了LinkedHashMap,所以他们的性能也会得到提升。
HashMap的快速高效,使其使用非常广泛。其原理是,调用hashCode()和equals()方法,并对hashcode进行一定的哈希运算得到相应value的位置信息,将其分到不同的桶里。桶的数量一般会比所承载的实际键值对多。当通过key进行查找的时候,往往能够在常数时间内找到该value。
但是,当某种针对key的hashcode的哈希运算得到的位置信息重复了之后,就发生了哈希碰撞。这会对HashMap的性能产生灾难性的影响。
在Java 8 之前, 如果发生碰撞往往是将该value直接链接到该位置的其他所有value的末尾,即相互碰撞的所有value形成一个链表。
因此,在最坏情况下,HashMap的查找时间复杂度将退化到O(n).
但是在Java 8 中,该碰撞后的处理进行了改进。当一个位置所在的冲突过多时,存储的value将形成一个排序二叉树,排序依据为key的hashcode。
则,在最坏情况下,HashMap的查找时间复杂度将从O(1)退化到O(logn)。
虽然是一个小小的改进,但意义重大:
1、O(n)到O(logn)的时间开销。
2、如果恶意程序知道我们用的是Hash算法,则在纯链表情况下,它能够发送大量请求导致哈希碰撞,然后不停访问这些key导致HashMap忙于进行线性查找,最终陷入瘫痪,即形成了拒绝服务攻击(DoS)。
原文链接:https://blog.youkuaiyun.com/cpcpcp123/article/details/52744331
HashMap的key重复,那么value会被覆盖吗?
如果key相同,但是hashcode不同,那么value不会被覆盖
如果key相同,并且hashCode相同,那么value会被覆盖
HashMap如何实现相同key存入数据后不被覆盖?
需求:
实现一个在HashMap中存入(任意类型)相同的key值后,key中的value不会被覆盖,而是能够进行叠加!
拿到一个需求的时候,我们要先进行分析,看此需求能否实现,基于已有的知识(经验),然后在通过目前的一些技术看此需求如何实现。
要实现在HashMap中插入相同的key值,内容不被覆盖,那么肯定要了解HashMap的一些机制,首先看一下HashMap的put方法:
从JDK API中看到HashMap的put如何先前存储了一个key(键),在指定相同的key(键)的时候,会用新的值替换旧的值。
如下的代码示例:
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("aflyun", "Java编程技术乐园");
map.put("aflyun", "生活在长沙的延安人");
System.out.println(map.toString());
}
--打印:--
{aflyun=生活在长沙的延安人}
通过上面的示例分析:为什么存入相同的key后,旧值就被新值替换了呢?
要想知道具体原因,那只能去看HashMap的源码实现了。看一下put(K key, V value)方法了,本篇HashMap源码是JDK1.8版本!
/**
- HashMap 的put方法
/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/ - HashMap 的containsKey方法
**/
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
/**
- 将存入的key进行hash操作,也就是使用key.hashCode()!
**/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
判断put和判断key是否是同一个key的时候,使用大概如下判断逻辑:
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
先判断Hash是否一致,然后在判断传入key和当前集合中是否有相同的key。如果key相同,则新值替换旧值。其中在判断中使用了
==
equals
==和 equals 的区别有时候面试会问到,如何你知道这两个的区别不仅看源码能够很好的理解,并且遇到面试也不怕了。
tips:简述==和 equals 的区别>
1)对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;如果作用于引用类型的变量,则比较的是所指向的对象的地址!(确切的说,是堆内存地址)
2)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;诸如String等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
注:对于第二种类型,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。因为每new一次,都会重新开辟堆内存空间。
equals()方法介绍:
JAVA当中所有的类都是继承于Object这个超类的,在Object类中定义了一个equals的方法,equals的源码是这样写的:
public boolean equals(Object obj) {
//this - s1
//obj - s2
return (this == obj);
}
可以看到,这个方法的初始默认行为是比较对象的内存地址值,一般来说,意义不大。所以,在一些类库当中这个方法被重写了,如String、Integer、Date。在这些类当中equals有其自身的实现(一般都是用来比较对象的成员变量值是否相同),而不再是比较类在堆内存中的存放地址了。
所以说,对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是内存中的存放位置的地址值,跟双等号(==)的结果相同;如果被复写,按照复写的要求来。
我们对上面的两段内容做个总结吧:
== 的作用:
基本类型:比较的就是值是否相同
引用类型:比较的就是地址值是否相同
equals 的作用:
引用类型:默认情况下,比较的是地址值。
注:不过,我们可以根据情况自己重写该方法。一般重写都是自动生成,比较对象的成员变量值是否相同
有了上面的分析基础,那针对上面String类型的key的话,那实现起来就比较简单了!因为String中已经实现了HashCode和 equals代码如下:
自定义HashMap
public class MyHashMap<K> extends HashMap<K,String> {
/**
* 使用HashMap中containsKey判断key是否已经存在
* @param key
* @param value
* @return
*/
@Override
public String put(K key, String value) {
String newV = value;
if (containsKey(key)) {
String oldV = get(key);
newV = oldV + "---" + newV;
}
return super.put(key, newV);
}
}
String类型key的进行put操作
public static void main(String[] args) {
MyHashMap<String> map = new MyHashMap<String>();
map.put("aflyun", "Java编程技术乐园");
map.put("aflyun", "生活在长沙的延安人");
map.put("aflyun", "期待你加入乐园");
System.out.println(map.toString());
}
--打印:---
{aflyun=Java编程技术乐园---生活在长沙的延安人---期待你加入乐园}
此时同样的key内容是进行叠加的,不是进行替换!那如何是自定义的类,要当作key,那要怎么做呢?
其实也就是重写了hashCode和equals就可以了。
public class PrettyGirl {
/**
* 姑娘唯一认证ID
*/
private String id;
/**
* 姑娘姓字名谁
*/
private String name;
@Override
public boolean equals(Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
PrettyGirl that = (PrettyGirl) o;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
自定义类型当做key的进行put操作
public static void main(String[] args) {
PrettyGirl prettyGirl = new PrettyGirl();
Map<PrettyGirl,String> map = new HashMap<>();
map.put(prettyGirl, "Java编程技术乐园");
map.put(prettyGirl, "生活在长沙的延安人");
map.put(prettyGirl, "期待和你加入乐园");
System.out.println("map :" + map.toString());
MyHashMap<PrettyGirl> myMap = new MyHashMap<PrettyGirl>();
myMap.put(prettyGirl, "Java编程技术乐园");
myMap.put(prettyGirl, "生活在长沙的延安人");
myMap.put(prettyGirl, "期待和你加入乐园");
System.out.println("myMap :" + myMap.toString());
}
--打印:---
map :{com.happy.PrettyGirl@3c1=期待和你加入乐园}
myMap :{com.happy.PrettyGirl@3c1=Java编程技术乐园---生活在长沙的延安人---期待和你加入乐园}
总结:要实现开头的需求
1、如果是类似String这种,已经重写了hashCode和equals的。则只需要创建一个自己的HashMap类,重写put即可。
2、如果是自定义的类,那就必须重写了hashCode和equals的,然后在使用自定义的HashMap类了。
具体的代码判断逻辑:
判断key是否存在的时候是先比较key的hashCode,再比较相等或equals的,所以重写hashCode()和equals()方法即可实现添加重复元素。重写这两个方法之后就可以覆盖重复的键值对,如果需要对value进行叠加,调用put()方法之前用containsKey()方法判断是否有重复的键值,如果有,则用get()方法获取原有的value,再加上新加入的value即可。
本文涉及的相关知识:
1、HashMap相关源码
2、== 、equals和 hashCode
3、Hash算法
什么时候需要重写HashCode、equals?为什么重写equals方法时必须重写hashcode方法?
什么时候需要重写HashCode、equals
当我们自定义了对象,并且想要将自定义的对象加到Map中时,我们就必须对自定义的对象重写这两个方法,才能正确使用Map。
为什么重写equals方法时必须重写hashcode方法?
总结: 因为不重写hashcode,创建的每个对象都不相等。就没办法找到相等的两个对象
在我们的业务系统中判断对象时有时候需要的不是一种严格意义上的相等,而是一种业务上的对象相等。在这种情况下,原生的equals方法就不能满足我们的需求了
所以这个时候我们需要重写equals方法,来满足我们的业务系统上的需求。那么为什么在重写equals方法的时候需要重写hashCode方法呢?
我们先来看一下Object.hashCode的通用约定(摘自《Effective Java》第45页)
- 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须始终如一地返回
同一个整数。在同一个应用程序的多次执行过程中,这个整数可以不同,即这个应用程序这次执行返回的整数与下一次执行返回的整数可以不一致。 - 如果两个对象根据equals(Object)方法是相等的,那么调用这两个对象中任一个对象的hashCode方法必须产生同样的整数结果。
- 如果两个对象根据equals(Object)方法是不相等的,那么调用这两个对象中任一个对象的hashCode方法,不要求必须产生不同的整数结果。然而,程序员应该意识到这样的事实,对于不相等的对象产生截然不同的整数结果,有可能提高散列表(hash
table)的性能。
常见的误区
看下面这段代码:
import java.util.HashMap;
public class HashCodeEqual {
public static void main(String[] args) {
Apple a1 = new Apple("Blue");
Apple a2 = new Apple("Green");
HashMap<Apple, Integer> map = new HashMap<>();
map.put(a1, 10);
map.put(a2, 20);
System.out.println(map.get(new Apple("Green")));
}
}
class Apple {
public String color;
public Apple(String color) {
this.color = color;
}
@Override
public boolean equals(Object obj) {
if(! (obj instanceof Apple))
return false;
if(obj == this)
return true;
return this.color.equals(((Apple)obj).color);
}
}
我们执行上面这段代码
却发现与我们预想的结果并不一样,我们想取出map中颜色为Green的apple,最后却得到一个null值,这说明map没有我们需要的颜色为green的apple对象,但实际上,我们明明向其中添加了一个颜色为green的apple对象,也重写了equals方法,为什么最后却取不出这个对象呢?
![Upload Paste_Image.png failed. Please try again.]
错误出现的原因
这个问题引起的原因是因为我们没有重写“hashCode”方法,这就需要我们深入理解equals方法和hashCode方法的原理:
如果两个对象是相等的,那么他们必须拥有一样的hashcode,这是第一个前提
如果两个对象有一样的hashcode,但仍不一定相等,因为还需要第二个要求,也就是equals方法的判断。
其实,map判断对象的方法就是先判断hashcode是否相等,如果相等再判断equals方法是否返回true,只有同时满足两个条件,最后才会被认为是相等的。
Map查找元素比线性搜索更快,这是因为map利用hashkey去定位元素,这个定位查找的过程分成两步,内部原理中,map将对象存储在类似数组的数组的区域,所以要经过两个查找,先找到hashcode相等的,然后在再在其中按线性搜索使用equals方法,通过这两部来查找一个对象。
Paste_Image.png
就像上图这个结构,每个hashcode对应一个桶,每个tongli桶里还有多个对象,确定桶的方法是hashCode,在桶中遍历线性查找的方法是equals。
在Object中的默认的hashCode方法的实现是为不同的对象返回不同的hashcode,因此如果我们不重写hashcode方法,那么没有任何两个对象会是相等的,因为object类中的hashcode实现是为不同的对象返回不同的hashcode。
所以,我们就搞清楚了上一段代码出错的原因,由于没有重写hashcode方法,所有的对象都是不一样的,所以我们需要重写hashcode方法,让颜色的对象的hashcode是一样的,比较直接的写法就是直接用color的length作为hashcode。
public int hashCode(){
return this.color.length();
}
** 切记,一定要同时重写hashCode和equals方法 **
Jdk1.6,JDK1.7,jdk1.8中HashMap区别
一、区别
-
最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;
-
jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;
-
插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
-
jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
-
扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;
-
jdk1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
-
扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。
————————————————
版权声明:本文为优快云博主「PANDA」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/qq_38685503/article/details/88430788
HashMap在JDK1.6中的实现方式
put方法:
put方法完成数据的基本写入操作,同时会验证数据的有效性,如key是否为空的处理,计算hash值,hash值 的查找,并根据hash值找出所有的数据,判断是否重复,如果重复,则替换,并返回旧值,如果不重复,则写入数据(addEntry)。
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
对应的addEntry方法: addEntry方法实现了数据的写入以及判断是否需要对HashMap进行扩容。
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
resize方法: resize方法主要是对Map进行扩容的实际操作:构造存储结构,并调用数据转移方法transfer进行数据转移
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
transfer 方法: 完成数据转移,注意,转移的过程就是两个数组进行数据拷贝的过程,数据产生了倒置,新元素其实是插入到了第一个位置。
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {//每次循环都是将第一个元素指向了最新的数据,其他数据后移(链式存储,更改指向)
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
Entry的构造: 链式存储结构。
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
···
}
HashMap在JDK1.7中的实现
JDK1.7中的实现与JDK1.6中的实现总体来说,基本没有改进,区别不大。
put 方法:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
resize方法:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer方法:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
createEntry方法:
/**
* Like addEntry except that this version is used when creating entries
* as part of Map construction or "pseudo-construction" (cloning,
* deserialization). This version needn't worry about resizing the table.
*
* Subclass overrides this to alter the behavior of HashMap(Map),
* clone, and readObject.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
HashMap在JDK1.8中的实现
put方法: put方法中与JDK1.6/1.7相比,新增了判断是否是TreeNode的逻辑,TreeNode即为红黑树, 同时添加了冲突的元素个数是否大于等于7的判断(因为第一个是-1,所以还是8个元素),如果冲突的 元素个数超过8个,则构造红黑树(treeifyBin方法),否则还是按照链表方式存储。
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//取模运算,寻址无冲突,写入数据
tab[i] = newNode(hash, key, value, null);
else {//存在冲突
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//集合中已经是红黑树了
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {//冲突时的数据写入逻辑,判断是否需要构造红黑树
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
treeifyBin方法: treeifyBin方法主要是将链式存储的冲突的数据转换成树状存储结构
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
resize方法: 注意:JDK1.8中resize方法扩容时对链表保持了原有的顺序。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
基础知识
2个不相等的对象有可能具有相同hashCode吗
有可能
**两个不相等的对象有可能具有相同的哈希码。**哈希码是由对象的哈希函数生成的一个整数值,用于支持快速查找和比较对象。
然而,由于哈希码的范围通常比对象的数量小得多,因此不同的对象可能会产生相同的哈希码。这种情况被称为哈希冲突。
哈希算法设计的目标是将不同的输入均匀分布在哈希码空间中,但无法避免完全消除冲突。因此,当发生哈希冲突时,哈希算法会使用特定的策略(例如链表或树结构)来处理这些冲突,以确保不同的对象可以存储在同一个哈希桶中。
综上所述,虽然不同的对象可能具有相同的哈希码,但哈希码仅用于初步判断对象是否可能相等,最终的相等性检查还需要通过 equals() 方法进行。因此,在重写 equals() 方法时,也应该相应地重写 hashCode() 方法,以尽量减少哈希冲突的发生。
ArrayList和LinkedList有什么区别
ArrayList和LinkedList是Java集合框架中List接口的两个常见实现类,它们在底层实现和性能特点上有以下几点区别:
- **底层数据结构:**ArrayList使用数组来存储元素,而LinkedList使用双向链表来存储元素。
- **随机访问性能:**ArrayList支持高效的随机访问(根据索引获取元素),因为它可以通过下标计算元素在数组中的位置。而LinkedList在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
- **插入和删除性能:**ArrayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。
- **内存占用:**ArrayList在每个元素都需要存储一个引用和一个额外的数组空间,因此内存占用比较高。而LinkedList由于需要存储前后节点的引用,相对于ArrayList占用的内存更多。
综上所述,如果需要频繁进行随机访问操作或在尾部进行插入和删除操作,可以选择ArrayList。如果需要频繁进行中间位置的插入和删除操作,或者对内存占用有一定限制,可以选择LinkedList。
Comparator与Comparable有什么区别
Comparator和Comparable都是Java中用于对象排序的接口,它们之间有一些关键的区别。
Comparable接口是在对象自身的类中实现的,它定义了对象的自然排序方式。一个类实现了Comparable接口后,可以使用compareTo方法来比较当前对象和其他对象的大小关系。这个接口只能在对象自身的类中实现,不需要额外的比较器。
Comparator接口是一个独立的比较器,它可以用于对不同类的对象进行排序。Comparator接口允许在对象类之外创建一个单独的比较器类或匿名类,并使用它来定义对象的排序规则。比较器通过实现compare方法来比较两个对象的大小关系。
因此,主要区别如下:
- Comparable接口是在对象自身的类中实现,定义了对象的自然排序方式。
- Comparator接口是一个单独的比较器,定义了用于排序的规则,可以用于不同类的对象排序。
- Comparable是内部排序,对象的类必须实现Comparable接口才能进行排序。
- Comparator是外部排序,可以独立定义排序规则,并与任何类的对象一起使用。
在使用时,如果需要对对象的默认排序进行操作,可以实现Comparable接口。如果需要对不同类的对象进行排序,或者需要定义多种不同的排序规则,可以使用Comparator接口。
JDK8提升代码优雅技巧
在开发过程中经常会使用if…else…进行判断抛出异常、分支处理等操作。这些if…else…充斥在代码中严重影响了代码代码的美观,这时我们可以利用Java 8的Function接口来消灭if…else…
if (...){
throw new RuntimeException("出现异常了");
}
if (...){
doSomething();
} else {
doOther();
}
使用注解@FunctionalInterface标识,并且只包含一个抽象方法的接口是函数式接口。函数式接口主要分为Supplier供给型函数、Consumer消费型函数、Runnable无参无返回型函数和Function有参有返回型函数。
Function可以看作转换型函数
Supplier的表现形式为不接受参数、只返回数据
/**
* Represents a supplier of results.
*
* <p>There is no requirement that a new or distinct result be returned each
* time the supplier is invoked.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #get()}.
*
* @param <T> the type of results supplied by this supplier
*
* @since 1.8
*/
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
表现形式为接收一个参数,并返回一个值。Supplier、Consumer和Runnable可以看作Function的一种特殊表现形式
/**
* Represents a function that accepts one argument and produces a result.
*
* <p>This is a <a href="package-summary.html">functional interface</a>
* whose functional method is {@link #apply(Object)}.
*
* @param <T> the type of the input to the function
* @param <R> the type of the result of the function
*
* @since 1.8
*/
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
/**
* Returns a composed function that first applies the {@code before}
* function to its input, and then applies this function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*
* @param <V> the type of input to the {@code before} function, and to the
* composed function
* @param before the function to apply before this function is applied
* @return a composed function that first applies the {@code before}
* function and then applies this function
* @throws NullPointerException if before is null
*
* @see #andThen(Function)
*/
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
/**
* Returns a composed function that first applies this function to
* its input, and then applies the {@code after} function to the result.
* If evaluation of either function throws an exception, it is relayed to
* the caller of the composed function.
*
* @param <V> the type of output of the {@code after} function, and of the
* composed function
* @param after the function to apply after this function is applied
* @return a composed function that first applies this function and then
* applies the {@code after} function
* @throws NullPointerException if after is null
*
* @see #compose(Function)
*/
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
/**
* Returns a function that always returns its input argument.
*
* @param <T> the type of the input and output objects to the function
* @return a function that always returns its input argument
*/
static <T> Function<T, T> identity() {
return t -> t;
}
}
Consumer消费型函数和Supplier刚好相反。Consumer接收一个参数,没有返回值
/**
* 表示接受单个输入参数但不返回任何结果的操作。
* 与大多数其他功能接口不同, Consumer 它预计会通过副作用进行操作。
* 这是一个功能接口,其 功能 方法是 accept(Object)
*/
@FunctionalInterface
public interface Consumer<T> {
/**
* 对给定参数执行此操作。
* 形参: t – 输入参数
*/
void accept(T t);
/**
* 返回一个组合,该组合 Consumer 按顺序执行此操作,后跟 after 操作。
* 如果执行任一操作引发异常,则会将其中继到组合操作的调用方。
* 如果执行此操作引发异常,则不会执行该 after 操作。
* 形参: after – 此操作后要执行的操作
* 返回值: 按 Consumer 顺序执行此操作后跟 after 操作的组合
* 抛出: NullPointerException – 如果 after 为空
*/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
Runnable的表现形式为即没有参数也没有返回值
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
- 定义函数 定义一个抛出异常的形式的函数式接口, 这个接口只有参数没有返回值是个消费型接口
/**
* 抛异常接口
**/
@FunctionalInterface
public interface ThrowExceptionFunction {
/**
* 抛出异常信息
*
* @param message 异常信息
* @return void
**/
void throwMessage(String message);
}
- 编写判断方法 创建工具类VUtils并创建一个isTure方法,方法的返回值为刚才定义的函数式接口-ThrowExceptionFunction。ThrowExceptionFunction的接口实现逻辑为当参数b为true时抛出异常
/**
* 如果参数为true抛出异常
*
* @param b
* @return com.example.demo.func.ThrowExceptionFunction
**/
public static ThrowExceptionFunction isTure(boolean b){
return (errorMessage) -> {
if (b){
throw new RuntimeException(errorMessage);
}
};
}
- 使用方式 调用工具类参数参数后,调用函数式接口的throwMessage方法传入异常信息。 当出入的参数为false时正常执行
// 报错
BeimingUtil.isTure(true).isthrowMessage("哎呀,一不小心就报错啦");
// 不报错
BeimingUtil.isTure(false).isthrowMessage("哎呀,一不小心就报错啦");
- 定义函数式接口 创建一个名为BranchHandle的函数式接口,接口的参数为两个Runnable接口。这两个两个Runnable接口分别代表了为true或false时要进行的操作
/**
* 分支处理接口
**/
@FunctionalInterface
public interface BranchHandle {
/**
* 分支操作
*
* @param trueHandle 为true时要进行的操作
* @param falseHandle 为false时要进行的操作
* @return void
**/
void trueOrFalseHandle(Runnable trueHandle, Runnable falseHandle);
}
- 编写判断方法 创建一个名为isTureOrFalse的方法,方法的返回值为刚才定义的函数式接口-BranchHandle。
/**
* 参数为true或false时,分别进行不同的操作
**/
public static BranchHandle isTureOrFalse(boolean b){
return (trueHandle, falseHandle) -> {
if (b){
trueHandle.run();
} else {
falseHandle.run();
}
};
}
- 效果
// 参数为true时,执行trueHandle
BeimingUtil.isTureOrFalse(true)
.trueorFalseHandle ( trueHandle: O -> {
System.out.printin("true,没毛病”);
},falseHandle: () -> {
System.out.println("有毛病");
}
- 定义函数 创建一个名为PresentOrElseHandler的函数式接口,接口的参数一个为Consumer接口。一个为Runnable,分别代表值不为空时执行消费操作和值为空时执行的其他操作
/**
* 空值与非空值分支处理
*/
public interface PresentOrElseHandler<T extends Object> {
/**
* 值不为空时执行消费操作
* 值为空时执行其他的操作
*
* @param action 值不为空时,执行的消费操作
* @param emptyAction 值为空时,执行的操作
* @return void
**/
void presentOrElseHandle(Consumer<? super T> action, Runnable emptyAction);
}
- 编写判断方法 创建一个名为isBlankOrNoBlank的方法,方法的返回值为刚才定义的函数式接口-PresentOrElseHandler。
/**
* 参数为true或false时,分别进行不同的操作
*
* @param b
* @return com.example.demo.func.BranchHandle
**/
public static PresentOrElseHandler<?> isBlankOrNoBlank(String str){
return (consumer, runnable) -> {
if (str == null || str.length() == 0){
runnable.run();
} else {
consumer.accept(str);
}
};
}
- 使用方式 调用工具类参数参数后,调用函数式接口的presentOrElseHandle方法传入一个Consumer和Runnable
Function函数式接口是java 8非常重要的特性,利用好Function函数可以极大的简化代码。
在Java中,双冒号"::"是方法引用(Method Reference)的语法。方法引用是一种简化Lambda表达式的语法结构,使代码更加简洁易读。并且在使用方法引用时,会根据上下文推断参数类型,因此特别适用于直接引用已有方法的情况。
方法引用的一般形式是:
ClassName::methodName
其中,ClassName 是包含静态方法 methodName 的类名。根据引用的方法类型,有不同的情况:
假设我们有一个自定义的工具类MathUtil,其中包含一个静态方法square,用于计算一个整数的平方。现在我们想要计算一个整数列表中所有元素的平方和。
import java.util.Arrays;
import java.util.List;
public class MathUtil {
public static int square(int num) {
return num * num;
}
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.mapToInt(MathUtil::square)
.sum();
System.out.println(sum); // 输出55
}
}
在上述代码中,我们通过使用静态方法引用MathUtil::square,将square方法传递给mapToInt方法,以便对列表中的每个元素进行平方运算。
假设我们有一个字符串列表,我们想要按照字符串长度进行排序。我们可以使用Lambda表达式编写比较器,也可以使用实例方法引用简化代码。
import java.util.Arrays;
import java.util.List;
public class StringSorter {
public static void main(String[] args) {
List<String> words = Arrays.asList("apple", "orange", "banana", "pear");
words.sort((a, b) -> a.compareTo(b)); // 使用Lambda表达式
System.out.println(words); // [apple, banana, orange, pear]
// 使用实例方法引用
words.sort(String::compareTo);
System.out.println(words); // [apple, banana, orange, pear]
}
}
在上述代码中,我们首先使用Lambda表达式编写了一个比较器(a, b) -> a.compareTo(b)来进行字符串比较。然后,我们使用实例方法引用String::compareTo来简化比较器的写法。
假设我们有一个自定义的Person类,其中包含姓名和年龄属性。我们想要根据Person对象的年龄进行排序。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class PersonSorter {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 22),
new Person("Bob", 32),
new Person("Charlie", 20),
new Person("David", 26)
);
// 使用Lambda表达式编写比较器
people.sort((p1, p2) -> p1.getAge() - p2.getAge());
System.out.println(people);
// 使用对象方法引用
people.sort(Person::compareByAge);
System.out.println(people);
}
}
static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
public static int compareByAge(Person p1, Person p2) {
return p1.getAge() - p2.getAge();
}
}
在上述代码中,我们首先使用Lambda表达式编写了一个比较器(p1, p2) -> p1.getAge() - p2.getAge()来根据年龄进行排序。然后,我们使用对象方法引用Person::compareByAge来简化比较器的写法。
假设我们需要创建一个空的ArrayList,可以使用构造方法引用来实现。
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class ArrayListCreator {
public static void main(String[] args) {
Supplier<List<String>> supplier = ArrayList::new;
List<String> list = supplier.get();
System.out.println(list instanceof ArrayList); // 输出true
}
}
本文给大家收集了工作常用的 15 种 Java Stream API,可用于进行各种数据处理和操作。具体如下:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 筛选出以字母"A"开头的字符串,并将符合条件的字符串收集到一个新的列表中
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(filteredNames); // Output: [Alice]
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 将给定字符串列表中每个字符串的长度提取出来,并将这些长度存储到一个新的整数列表中
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(nameLengths); // Output: [5, 3, 7, 5]
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 将每个字符串转换为大写形式并收集到新的列表中
List<String> upperCaseNames = new ArrayList<>();
names.forEach(name -> upperCaseNames.add(name.toUpperCase()));
// 输出新的列表
System.out.println(upperCaseNames); // 输出:[ALICE, BOB, CHARLIE, DAVID]
List<List<Integer>> numbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
// 使用 flatMap 方法将多个整数列表合并为一个扁平的列表
List<Integer> flattenedList = numbers.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println(flattenedList); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用流的 reduce 方法计算列表中所有元素的和
int sum = numbers.stream()
.reduce(0, Integer::sum);
System.out.println(sum); // Output: 15
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 4, 3, 5);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(distinctNumbers); // Output: [1, 2, 3, 4, 5]
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNumbers); // Output: [1, 1, 2, 3, 4, 5, 5, 6, 9]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 筛选出大于5的元素,然后限制只获取前3个符合条件的元素
List<Integer> limitedNumbers = numbers.stream()
.filter(num -> num > 5)
.limit(3)
.collect(Collectors.toList());
System.out.println(limitedNumbers); // Output: [6, 7, 8]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 跳过前两个元素
List<Integer> skippedNumbers = numbers.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(skippedNumbers); // Output: [3, 4, 5]
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 anyMatch 方法判断列表中是否有任意一个元素大于3
boolean anyMatch = numbers.stream().anyMatch(n -> n > 3);
// 使用 allMatch 方法判断列表中所有元素是否都大于0
boolean allMatch = numbers.stream().allMatch(n -> n > 0);
// 使用 noneMatch 方法判断列表中是否没有元素小于0
boolean noneMatch = numbers.stream().noneMatch(n -> n < 0);
System.out.println("Any match: " + anyMatch); // Output: true
System.out.println("All match: " + allMatch); // Output: true
System.out.println("None match: " + noneMatch); // Output: true
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 findFirst 方法查找第一个元素
Optional<Integer> first = numbers.stream().findFirst();
// 使用 findAny 方法查找任意一个元素
Optional<Integer> any = numbers.stream().findAny();
System.out.println("First: " + first.orElse(null)); // Output: 1
System.out.println("Any: " + any.orElse(null)); // Output: 1 or any other element
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
// 使用 min 方法找到最小值
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
// 使用 max 方法找到最大值
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
System.out.println("Min: " + min.orElse(null)); // Output: 1
System.out.println("Max: " + max.orElse(null)); // Output: 5
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 使用流进行分组,按照名字的首字母进行分组
Map<Character, List<String>> groupedNames
= names.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
System.out.println(groupedNames); // Output: {A=[Alice], B=[Bob], C=[Charlie], D=[David]}
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 使用流进行分区,根据名字长度是否大于3进行分区
Map<Boolean, List<String>> partitionedNames
= names.stream()
.collect(Collectors.partitioningBy(name -> name.length() > 3));
System.out.println(partitionedNames); // Output: {false=[Bob], true=[Alice, Charlie, David]}
使用 idea 编码时,如果不知道输入什么,请使用 ctrl + shift + 空格
package com.baili.springboot3;
import com.baili.springboot3.entity.Transaction;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.*;
import java.util.stream.Collectors;
@SpringBootTest
public class StreamApiTest {
@Test
void forEachDemo() {
List<String> list = new ArrayList<>(Arrays.asList("apple", "banana", "orange"));
// 使用传统的 for 循环遍历列表并输出每个元素
for (String string : list) {
System.out.println(string);
}
// 移除符合条件的元素
list.removeIf(s -> s.equals("apple"));
// 使用流的 forEach 方法遍历列表并输出每个元素
list.forEach(System.out::println);
list.forEach(s -> {
// TODO
});
}
@Test
void mapDemo() {
List<String> list = Arrays.asList("apple", "banana", "orange");
List<Integer> lengths = new ArrayList<>();
// 使用传统的 for 循环遍历列表,并将每个水果的长度添加到list中
for (String string : list) {
lengths.add(string.length());
}
// map 方法将每个水果映射为其长度,并将收集到list中
lengths = list.stream()
.map(String::length)
.collect(Collectors.toList());
// map 方法将每个水果映射为其长度,并将收集到set中
Set<Integer> collect = list.stream()
.map(String::length)
.collect(Collectors.toSet());
System.out.println(lengths);
System.out.println(collect);
}
@Test
void flatMapDemo() {
List<List<Integer>> numbers = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
// flatMap 将所有元素转换为单独的流,然后连接成一个流
List<Integer> flattenedList = numbers.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
System.out.println(flattenedList); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
@Test
void reduceDemo() {
List<Transaction> transactions = new ArrayList<>();
transactions.add(new Transaction(100.0));
transactions.add(new Transaction(200.0));
transactions.add(new Transaction(300.0));
// reduce 计算金额总和
double totalAmount = transactions.stream()
.mapToDouble(Transaction::getAmount)
.reduce(0.0, Double::sum);
System.out.println("总金额为: " + totalAmount);
}
@Test
void synthesizeDemo() {
List<String> strings = Arrays.asList("apple", "banana", "orange", "apple", "banana", "grape");
// 使用流进行操作
List<String> result = strings.stream()
.filter(string -> string.length() > 5) // 过滤出长度大于5的水果
.distinct() // 去除重复的水果
.sorted() // 按字母顺序排序
.skip(1) //跳过一个元素
.limit(2) // 取前两个水果
.collect(Collectors.toList()); // 收集结果为列表
System.out.println(result); // 输出:[orange]
}
@Test
void matchDemo() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 anyMatch 方法判断列表中是否有任意一个元素大于3
boolean anyMatch = numbers.stream().anyMatch(n -> n > 3);
// 使用 allMatch 方法判断列表中所有元素是否都大于0
boolean allMatch = numbers.stream().allMatch(n -> n > 0);
// 使用 noneMatch 方法判断列表中是否没有元素小于0
boolean noneMatch = numbers.stream().noneMatch(n -> n < 0);
System.out.println("Any match: " + anyMatch); // true
System.out.println("All match: " + allMatch); // true
System.out.println("None match: " + noneMatch); // true
}
@Test
void findDemo() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 findFirst 方法查找第一个元素
Optional<Integer> first = numbers.stream().findFirst();
// 使用 findAny 方法查找任意一个元素
Optional<Integer> any = numbers.stream().findAny();
// 使用 min 方法找到最小值
Optional<Integer> min = numbers.stream().min(Integer::compareTo);
// 使用 max 方法找到最大值
Optional<Integer> max = numbers.stream().max(Integer::compareTo);
System.out.println("First: " + first.orElse(null)); // 1
System.out.println("Any: " + any.orElse(null)); // 1 or any other element
System.out.println("Min: " + min.orElse(null)); // 1
System.out.println("Max: " + max.orElse(null)); // 5
}
@Test
void groupByDemo() {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Adam", "Bella", "Christopher", "Daniel");
// 使用流进行分组,按照名字的首字母进行分组
Map<Character, List<String>> groupedNames = names.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
System.out.println(groupedNames); // {A=[Alice, Adam], B=[Bob, Bella], C=[Charlie, Christopher], D=[David, Daniel]}
}
@Test
void partitioningByDemo() {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 使用流进行分区,根据名字长度是否大于3进行分区
Map<Boolean, List<String>> partitionedNames = names.stream()
.collect(Collectors.partitioningBy(name -> name.length() > 3));
System.out.println(partitionedNames); // {false=[Bob], true=[Alice, Charlie, David]}
}
}
❌ 未使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
for (String fruit : list) {
System.out.println(fruit);
}
✅ 使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
list.forEach(fruit -> System.out.println(fruit));
❌ 未使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
Collections.sort(list, new Comparator() {
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
✅ 使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
Collections.sort(list, (s1, s2) -> s1.compareTo(s2));
❌ 未使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
List filteredList = new ArrayList();
for (String fruit : list) {
if (fruit.startsWith("a")) {
filteredList.add(fruit);
}
}
✅ 使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
List filteredList = list.stream().filter(fruit -> fruit.startsWith("a"))
.collect(Collectors.toList());
❌ 未使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
List lengths = new ArrayList();
for (String fruit : list) {
lengths.add(fruit.length());
}
✅ 使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
List lengths = list.stream()
.map(fruit -> fruit.length())
.collect(Collectors.toList());
❌ 未使用Lambda表达式:
List list = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (int i : list) {
sum += i;
}
✅ 使用Lambda表达式:
List list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.stream().reduce(0, (a, b) -> a + b);
❌ 未使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
Map<Integer, List> grouped = new HashMap<Integer, List>();
for (String fruit : list) {
int length = fruit.length();
if (!grouped.containsKey(length)) {
grouped.put(length, new ArrayList());
}
grouped.get(length).add(fruit);
}
✅ 使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
Map<Integer, List> grouped = list.stream()
.collect(Collectors.groupingBy(fruit -> fruit.length()));
❌ 未使用Lambda表达式:
public interface MyInterface {
public void doSomething(String input);
}
MyInterface myObject = new MyInterface() {
public void doSomething(String input) {
System.out.println(input);
}
};
myObject.doSomething("Hello World");
✅ 使用Lambda表达式:
MyInterface myObject = input -> System.out.println(input);
myObject.doSomething("Hello World");
❌ 未使用Lambda表达式:
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("Thread is running.");
}
});
thread.start();
✅ 使用Lambda表达式:
Thread thread = new Thread(() -> System.out.println("Thread is running."));
thread.start();
❌ 未使用Lambda表达式:
String str = "Hello World";
if (str != null) {
System.out.println(str.toUpperCase());
}
✅ 使用Lambda表达式:
Optional str = Optional.ofNullable("Hello World");
str.map(String::toUpperCase)
.ifPresent(System.out::println);
❌ 未使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
List filteredList = new ArrayList();
for (String fruit : list) {
if (fruit.startsWith("a")) {
filteredList.add(fruit.toUpperCase());
}
}
Collections.sort(filteredList);
✅ 使用Lambda表达式:
List list = Arrays.asList("apple", "banana", "orange");
List filteredList = list.stream().filter(fruit -> fruit.startsWith("a"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
Java中变量和常量有什么区别
在Java中,变量和常量是两个不同的概念,它们有以下 几点 区别:
- 可变性:
- 变量是可以被修改的,其值可以在程序的执行过程中改变。
- 常量是不可被修改的,其值在定义后不能再被改变。
- 声明与赋值:
- 变量需要先声明,并可以在声明后进行赋值。声明时需要指定变量的类型
- 常量在定义时需要使用final关键字进行修饰
- 内存空间:
- 变量在内存中占用一块存储空间,可以改变这个存储空间中的值。
- 常量通常会被编译器在编译时直接替换为对应的值,所以在内存中不会为常量分配额外的存储空间,而是直接使用常量的值。
- 使用场景:
- 变量用于存储会发生变化的数据,例如计数器、临时结果等,在程序的执行过程中可以根据需要改变其值。
- 常量用于表示不可变的数据,例如数学常数、配置项等,在程序中通常希望保持其固定的值,避免误操作导致值的变化。
总结来说,变量是可变的并且需要先声明后赋值,而常量是不可变的并且需要在定义时进行初始化赋值。变量占用内存空间且值可以改变,而常量通常会被编译器直接替换为对应的值,不占用额外的内存空间。变量用于存储会发生变化的数据,常量用于表示不可变的数据。
Java中止线程的三种方式
停止一个线程通常意味着在线程处理任务完成之前停掉正在做的操作,也就是放弃当前的操作。
在 Java 中有以下 3 种方法可以中止正在运行的线程:
- 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
- 使用 stop() 方法强行中止线程,但是不推荐使用这个方法,该方法已被弃用。
- 使用 interrupt 方法中断线程。
在 run() 方法执行完毕后,该线程就中止了。但是在某些特殊的情况下,run() 方法会被一直执行;比如在服务端程序中可能会使用 while(true) { ... }
这样的循环结构来不断的接收来自客户端的请求。此时就可以用修改标志位的方式来结束 run() 方法。
public class ExitFlagTests {
// volatile修饰符用来保证其它线程读取的总是该变量的最新的值
private volatile boolean exitFlag = false; // 退出标志
public void run() {
while (!exitFlag) {
// 执行线程的任务
System.out.println("Thread is running...");
try {
Thread.sleep(1000); // 模拟一些工作
} catch (InterruptedException e) {
// 处理中断(如果需要)
Thread.currentThread().interrupt(); // 重新设置中断状态
}
}
System.out.println("Thread is stopping...");
}
public void stop() {
exitFlag = true; // 设置退出标志为true
}
public static void main(String[] args) throws InterruptedException {
ExitFlagTests exit = new ExitFlagTests();
Thread thread = new Thread(exit::run);
thread.start();
// 让线程运行一段时间
Thread.sleep(5000);
// 请求线程停止
exit.stop();
}
}
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">Thread.stop()</font>
方法可以强行中止线程的执行。然而,这种方法是不安全的,因为它不保证线程资源的正确释放和清理,可能导致数据不一致和资源泄露等问题,因此已被官方弃用。
public class StopMethodTests extends Thread {
public void run() {
while (true) {
System.out.println("Thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) throws InterruptedException {
StopMethodTests thread = new StopMethodTests();
thread.start();
// 让线程运行一段时间
Thread.sleep(5000);
// 强行中止线程(不推荐)
thread.stop();
}
}
通过查看 JDK 的 API,我们会看到 java.lang.Thread 类型提供了一系列的方法如 start()、stop()、resume()、suspend()、destory()等方法来管理线程。但是除了 start() 之外,其它几个方法都被声名为已过时(deprecated)。
虽然 stop() 方法确实可以停止一个正在运行的线程,但是这个方法是不安全的,而且该方法已被弃用,最好不要使用它。
JDK 文档中还引入用一篇文章来解释了弃用这些方法的原因:《Why are Thread.stop, Thread.suspend and Thread.resume Deprecated?》
为什么弃用stop:
- 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
- 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
现在我们知道了使用 stop() 方式停止线程是非常不安全的方式,那么我们应该使用什么方法来停止线程呢?答案就是使用 interrupt() 方法来中断线程。
需要明确的一点的是:interrupt() 方法并不像在 for 循环语句中使用 break 语句那样干脆,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。
也就是说,线程中断并不会立即中止线程,而是通知目标线程,有人希望你中止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定。这一点很重要,如果中断后,线程立即无条件退出,那么我们又会遇到 stop() 方法的老问题。
事实上,如果一个线程不能被 interrupt,那么 stop 方法也不会起作用。
我们来看一个使用 interrupt() 的例子:
public class InterruptTests extends Thread{
public static void main(String[] args) {
try {
InterruptTests t = new InterruptTests();
// 启动线程
t.start();
Thread.sleep(200);
// 中断线程 t
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
// 循环打印 i 的值,从 0 到 200000
for(int i = 0; i <= 200000; i++) {
System.out.println("i=" + i);
}
}
}
i=199993
i=199994
i=199995
i=199996
i=199997
i=199998
i=199999
i=200000
从输出的结果我们会发现 interrupt 方法并没有停止线程 t 中的处理逻辑,也就是说即使 t 线程被设置为了中断状态,但是这个中断并不会起作用,那么该如何停止线程呢?
这就需要使用到另外两个与线程中断有关的方法了:
public boolean Thread.isInterrupted() //判断是否被中断
public static boolean Thread.interrupted() //判断是否被中断,并清除当前中断状态
这两个方法使得当前线程能够感知到是否被中断了(通过检查标志位)。
所以如果希望线程 t 在中断后停止,就必须先判断是否被中断,并为它增加相应的中断处理代码:
@Override
public void run() {
super.run();
for(int i = 0; i <= 200000; i++) {
//判断是否被中断
if(Thread.currentThread().isInterrupted()){
//处理中断逻辑
break;
}
System.out.println("i=" + i);
}
}
在上面这段代码中,我们增加了 Thread.isInterrupted() 来判断当前线程是否被中断了,如果是,则退出 for 循环,结束线程。
这种方式看起来与之前介绍的“使用标志位中止线程”非常类似,但是在遇到 sleep() 或者 wait() 这样的操作,我们只能通过中断来处理了。
public static native void sleep(long millis) throws InterruptedException
Thread.sleep() 方法会抛出一个 InterruptedException 异常,当线程被 sleep() 休眠时,如果被中断,这会就抛出这个异常。
(注意:Thread.sleep() 方法由于中断而抛出的异常,是会清除中断标记的。)
Java中的基本数据类型有哪些?它们的大小是多少?
在Java中,基本数据类型有以下几种:
- 整数类型:
- byte:1字节,在内存中范围为-128到127
- short:2字节,在内存中范围为-32768到32767
- int:4字节,在内存中范围为约-21亿到21亿
- long:8字节,在内存中范围为约-922亿亿到922亿亿
- 浮点数类型:
- float:4字节,在内存中约范围为±3.40282347E+38F(有效位数为6-7位)
- double:8字节,在内存中约范围为±1.79769313486231570E+308(有效位数为15位)
- 字符类型:
- char:2字节,在内存中范围为0到65535,表示一个Unicode字符
- 布尔类型:
- boolean:1位,在内存中只能表示true或false
上述大小是Java语言规范中定义的标准大小,表示它们在内存中占用的字节数。请注意,不同的编译器和平台可能会略有差异,但通常情况下这些标准大小是适用的。
Java中的异常处理机制是怎样的
异常是在程序执行过程中可能出现的错误或意外情况。它们通常表示了程序无法正常处理的情况,如除零错误、空指针引用、文件不存在等。
Java中的异常处理机制通过使用try-catch-finally语句块来捕获和处理异常。具体的处理过程如下:
- 使用try块包裹可能会抛出异常的代码块。一旦在try块中发生了异常,程序的控制流会立即跳转到与之对应的catch块。
- 在catch块中,可以指定捕获特定类型的异常,并提供相应的处理逻辑。如果发生了指定类型的异常,程序会跳转到相应的catch块进行处理。一个try块可以有多个catch块,分别处理不同类型的异常。
- 如果某个catch块成功处理了异常,程序将继续执行catch块之后的代码。
- 在catch块中,可以通过throw语句重新抛出异常,将异常交给上一级的调用者处理。
- 可以使用finally块来定义无论是否发生异常都需要执行的代码。finally块中的代码始终会被执行,无论异常是否被捕获。
通过合理使用异常处理机制,可以使程序更具健壮性和容错性。在处理异常时,应根据具体情况选择是恢复正常执行、报告错误给用户,还是终止程序运行。同时,应避免过度捕获异常和不处理异常导致的问题,以及使用异常替代正常程序流程控制的做法。
Java中的集合框架有哪些核心接口
Java中的集合框架提供了一组接口和类,用于存储和操作数据集合。其中一些核心接口包括:
- **Collection接口:**是集合框架中最通用的接口,用于表示一组对象。它是List、Set和Queue接口的父接口,定义了对集合进行基本操作的方法。
- **List接口:**表示一个有序的、可重复的集合。List接口的实现类可以根据元素的插入顺序访问和操作集合中的元素。常见的List接口的实现类有ArrayList、LinkedList和Vector。
- **Set接口:**表示一个无序的、不可重复的集合。Set接口的实现类不能包含重复的元素。常见的Set接口的实现类有HashSet、TreeSet和LinkedHashSet。
- **Queue接口:**表示一个先进先出的集合。Queue接口的实现类通常用于实现队列数据结构。常见的Queue接口的实现类有LinkedList和PriorityQueue。
- **Map接口:**表示一个键值对的映射集合。Map接口中的每个元素由一个键和一个值组成,并且每个键只能在Map中出现一次。常见的Map接口的实现类有HashMap、TreeMap和LinkedHashMap。
以上是Java集合框架中一些核心接口的介绍。这些接口提供了不同类型和功能的集合,可以根据需求选择合适的接口和实现类来存储和操作数据。
Java五种文件拷贝方式
在 Java 中,文件拷贝主要有以下几种方式,不同场景下效率差异显著。以下从实现方式、效率对比和适用场景三方面详细解析:
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("source.txt");
OutputStream os = new FileOutputStream("target.txt")) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
- 特点:基础方法,直接逐字节或缓冲区读写。
- 效率:最低(适合小文件)。
public static void main(String[] args) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("target.txt"))) {
byte[] buffer = new byte[8192]; // 缓冲区越大,性能越好(通常 8KB~64KB)
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
}
}
- 特点:通过缓冲区减少 I/O 次数。
- 效率:比传统字节流提升 2~5 倍。
public static void main(String[] args) throws IOException {
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
}
- 特点:单行代码完成拷贝,底层自动优化。
- 效率:接近最高效(适合大多数场景)。
public static void main(String[] args) throws IOException {
try (FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
FileChannel targetChannel = new FileOutputStream("target.txt").getChannel()) {
sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
}
}
- 特点:利用通道(Channel)直接传输数据。
- 效率:大文件性能最佳(利用零拷贝技术)。
public static void main(String[] args) throws IOException {
try (RandomAccessFile sourceFile = new RandomAccessFile("source.txt", "r");
RandomAccessFile targetFile = new RandomAccessFile("target.txt", "rw")) {
FileChannel sourceChannel = sourceFile.getChannel();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, sourceChannel.size());
targetFile.getChannel().write(buffer);
}
}
- 特点:将文件映射到内存直接操作。
- 效率:适合超大文件(但实现复杂,需谨慎处理内存)。
方法 | 耗时(毫秒) | 适用场景 |
---|---|---|
传统字节流 | 4500~5000 | 小文件(<10MB) |
缓冲流 | 1200~1500 | 通用场景 |
<font style="background-color:rgb(252, 252, 252);">Files.copy</font> | 800~1000 | 简单代码 + 快速开发 |
<font style="background-color:rgb(252, 252, 252);">FileChannel.transfer</font> | 600~800 | 大文件(>100MB) |
内存映射文件 | 500~700 | 超大文件(>1GB) |
- 小文件(<10MB)
直接使用<font style="background-color:rgb(252, 252, 252);">Files.copy</font>
,代码简洁且性能足够。 - 大文件(100MB~1GB)
优先选择<font style="background-color:rgb(252, 252, 252);">FileChannel.transferTo()</font>
,利用零拷贝减少内核态与用户态数据复制。 - 超大文件(>1GB)
用内存映射文件(<font style="background-color:rgb(252, 252, 252);">MappedByteBuffer</font>
),但需注意:- 避免频繁映射/释放内存(开销大)。
- 处理内存溢出风险(
<font style="background-color:rgb(252, 252, 252);">OutOfMemoryError</font>
)。
- 通用场景
<font style="background-color:rgb(252, 252, 252);">Files.copy</font>
或缓冲流,平衡代码可读性与性能。
- 优先使用****
**<font style="background-color:rgb(252, 252, 252);">Files.copy</font>**
:Java 7+ 自带优化,简单高效。 - 大文件必用****
**<font style="background-color:rgb(252, 252, 252);">FileChannel</font>**
:性能碾压传统流。 - 避免手动逐字节读写:除非处理特殊格式(如加密流)。
Java创建对象有几种方式
在Java中,有以下几种常见的方式来创建对象:
- **使用new关键字:**这是最常见的创建对象的方式。通过调用类的构造函数,使用new关键字可以在内存中分配一个新的对象。
- **使用反射:**Java的反射机制允许在运行时动态地创建对象。通过获取类的Class对象,并调用其构造函数,可以实现对象的创建。
- **使用newInstance()方法:**某些类提供了newInstance()方法来创建对象,这种方式只适用于具有默认无参构造函数的类。
- **使用clone()方法:**如果类实现了Cloneable接口,就可以使用clone()方法创建对象的副本。
- **使用对象的反序列化:**通过将对象序列化到一个字节流中,然后再进行反序列化,可以创建对象的副本。
其中,使用new关键字是最常见和推荐的创建对象的方式。其他方式通常在特定场景下使用,如需要动态创建对象或创建对象的副本等情况。
Java支持多继承么,为什么
**Java不直接支持多继承,即一个类不能同时继承多个父类。**这是由设计上的考虑和语言特性决定的。
Java中选择了单继承的设计,主要出于以下几个原因:
- **继承的复杂性:**多继承会引入菱形继承等复杂性问题。当一个类同时继承自多个父类时,可能会出现命名冲突、方法重复实现等问题,导致代码难以理解和维护。
- **接口的存在:**Java提供了接口(Interface)的概念来解决多继承的问题。接口允许一个类实现多个接口,从而达到类似多继承的效果。接口与类的分离可以降低代码的耦合度,并且使得类的设计更加灵活和可扩展。
- **单一职责原则:**Java鼓励使用组合而非继承的方式,遵循设计原则中的单一职责原则。通过将功能划分为独立的类,然后在需要时进行组合,可以实现更灵活、可复用的代码结构,提高代码的可维护性。
尽管Java不支持直接的多继承,**但可以使用接口或抽象类等方式来模拟部分多继承的功能。**接口提供了一种更灵活、更安全的多继承方式,允许类实现多个接口并获得各个接口的方法声明,同时避免了多继承的复杂性问题。
Strings与newString有什么区别
Java中字符串可以通过两种方式创建:使用字符串字面量直接赋值给变量或使用关键字new创建一个新的String对象。它们之间有以下区别:
**首先,**使用字符串字面量赋值给变量时,Java会使用字符串常量池来管理字符串对象,可以提高性能和节省内存。而使用new String创建的字符串对象则在堆内存中独立分配内存空间,每次调用都会创建一个新的对象,因此内存消耗更大。
**其次,**使用字符串字面量赋值给变量的字符串是不可变的,即不能改变其内容。而使用new String创建的字符串对象是可变的,可以通过调用方法或者使用赋值运算符修改其内容。
**最后,**使用字符串字面量赋值给变量的字符串比较时,如果多个变量引用相同的字符串字面量,则它们实际上引用的是同一个对象,因此比较它们的引用时将返回true。而使用new String创建的字符串对象,即使内容相同,它们也是不同的对象,因此比较它们的引用时将返回false。
String类能被继承吗,为什么
在Java中,String类是被final关键字修饰的,即不可继承。final关键字表示一个类不允许被其他类继承,也就是说,String类不能被任何其他类继承。
这是因为String类具有不可变性和安全性,这些特性可以防止一些潜在的问题,如字符串池中的重用和安全性漏洞。
如果String类能被继承,子类有可能修改原字符串的值,这将破坏字符串对象的不可变性。此外,String类的方法和变量都被设计成private、final和static的,这说明它们不能被重写或隐藏。如果String类可以被继承,这些设计决策将被打破,可能产生更多的问题。
因此,尽管我们不能从String类派生出新的子类,但我们可以使用String类提供的方法来操作和处理字符串。例如,我们可以使用String类的concat()方法连接两个字符串,或使用indexOf()方法查找子串在字符串中的位置等。String类已经包含了大量的方法,可以满足大多数字符串操作的需求。
String,Stringbuffer,StringBuilder的区别
三者均是Java中用来处理字符串的类,它们之间的主要区别如下:
- 可变性:
- String是不可变的类,一旦创建就不能被修改。每次对String进行操作时,都会创建一个新的String对象。
- StringBuffer和StringBuilder是可变的类,可以动态修改字符串内容。
- 线程安全性:
- String是线程安全的,因为它是不可变的。多个线程可以同时访问同一个String对象而无需担心数据的修改问题。
- StringBuffer是线程安全的,它的方法使用了synchronized关键字进行同步,保证在多线程环境下的安全性。
- StringBuilder是非线程安全的,不使用synchronized关键字,所以在多线程环境下使用时需要手动进行同步控制。
- 性能:
- 由于String是不可变的,每次对String进行操作都会创建一个新的String对象,频繁的字符串拼接会导致大量的对象创建和内存消耗。
- StringBuffer是可变的,对字符串的修改是在原有对象上进行,不会创建新的对象,因此在频繁的字符串拼接场景下比String更高效。
- StringBuilder与StringBuffer类似,但不保证线程安全性,因此在单线程环境下性能更高。
综上,如果在单线程环境下进行字符串操作,且不需要频繁修改字符串,推荐使用String;如果在多线程环境下进行字符串操作,或者需要频繁修改字符串,优先考虑使用StringBuffer;如果在单线程环境下进行频繁的字符串拼接和修改,推荐使用StringBuilder以获取更好的性能。
ThreadLocal有哪些应用场景
ThreadLocal是Java中的一个类,它提供了一种在多线程环境下实现线程局部变量存储的机制。
它的应用场景包括线程池、Web开发中的请求上下文信息管理、数据库连接管理和日志记录等等。
**在线程池中,**可以使用ThreadLocal为每个线程维护独立的上下文信息,避免线程间互相干扰。
**在Web开发中,**可以使用ThreadLocal存储当前请求的上下文信息,避免参数传递的复杂性。
**在数据库连接管理中,**ThreadLocal可以为每个线程保持独立的数据库连接,提高并发性能。
**在日志记录中,**ThreadLocal可以将日志记录与当前线程关联起来,方便追踪和排查问题。
**此外,**ThreadLocal还可以用于在线程之间传递全局的上下文信息。
在使用ThreadLocal时需要注意内存泄漏问题和线程安全性,及时清理不再需要的变量副本,并采取适当的同步措施保证线程安全。通过合理使用ThreadLocal,可以简化多线程编程,提高程序的性能和可维护性。
char型变量能存贮一个中文汉字吗
在Java中,char类型是用来表示单个字符的数据类型,它采用Unicode编码,可以存储各种字符,包括中文汉字。
由于Unicode编码使用16位来表示一个字符,char类型占用2个字节的内存空间。而中文汉字通常使用UTF-8编码,一个中文字符占用3个字节的存储空间。因此,将一个中文汉字直接赋值给char类型的变量可能会出现问题,因为无法完整地表示一个中文字符。
如果要在char类型中表示一个中文汉字,可以使用Unicode转义序列。\u后面跟着表示字符的四位十六进制值,通过转义序列可以正确地表示一个中文汉字。例如,字符 ‘中’ 的Unicode编码为’\u4e2d’,我们可以使用char类型变量去存储这个中文汉字:char ch = ‘\u4e2d’;。
需要注意的是,对于一个完整的中文字符,建议使用更适合的数据类型,如String类型来存储。 char类型主要用于表示单个字符,而不是用于存储复杂字符集合。
equals与==区别
在Java中,"=="是一个比较操作符,用于比较两个变量的值是否相等。而"equals()"是Object类中定义的方法,用于比较两个对象是否相等。
具体区别如下:
- "=="用于比较基本数据类型和引用类型变量的地址值是否相等。对于基本数据类型,比较的是它们的实际值;对于引用类型,比较的是它们所引用的对象的地址值。
- "equals()“方法用于比较两个对象的内容是否相等。默认情况下,它与”=="的作用相同,比较的是对象的地址值。但是,可以根据具体的类重写该方法,以实现自定义的比较逻辑。
需要注意以下几点:
- 对于基本数据类型,使用"=="进行比较更加直接和高效。
- 对于引用类型,使用"equals()"进行比较更加准确和灵活,但需要注意重写"equals()"方法,以满足自定义的比较需求。
总结起来,"=="比较的是变量的值或引用的地址值,而"equals()"比较的是对象的内容。
for-each与常规for循环的效率区别
在Java中,for-each循环(也称为增强型for循环)和常规for循环有一些差异,包括它们在执行效率上的区别。下面是它们之间的一些比较:
- **执行效率:**在大多数情况下,常规for循环的执行效率比for-each循环高。这是因为for-each循环需要额外的步骤来获取集合或数组中的元素,而常规for循环可以直接通过索引访问元素,避免了额外的开销。
- **可变性:**常规for循环具有更大的灵活性,可以在循环过程中修改计数器,从而控制循环的行为。而for-each循环是只读的,不能在循环过程中修改集合或数组的元素。
- **代码简洁性:**for-each循环通常比常规for循环更加简洁易读,尤其在遍历集合或数组时。使用for-each循环可以减少迭代器或索引变量的声明和管理,使代码更加清晰。
尽管常规for循环在执行效率上可能更高,但在大多数实际情况下,两者之间的性能差异不会对程序性能产生显著影响。因此,根据具体的使用场景和代码可读性的需求,可以选择使用for-each循环或常规for循环。在只需要遍历集合或数组而不修改其中元素的情况下,for-each循环是一个方便且简洁的选择。
int和Integer的区别
int和Integer之间的区别主要在以下几个方面:
- **数据类型:**int是Java的基本数据类型,而Integer是int的包装类,属于引用类型。
- **可空性:**int是基本数据类型,它不能为null。而Integer是一个对象,可以为null。
- **自动装箱与拆箱:**int可以直接赋值给Integer,这个过程称为自动装箱;而Integer也可以直接赋值给int,这个过程称为自动拆箱。
- **性能和内存开销:**由于int是基本数据类型,它的值直接存储在栈内存中,占用的空间较小且访问速度快。而Integer是对象,它的值存储在堆内存中,占用的空间相对较大,并且访问速度较慢。因此,频繁使用的整数推荐使用int,不需要使用对象特性时可以避免使用Integer。
总的来说,int是基本数据类型,适用于简单的整数运算和存储,没有对象的特性和可空性。而Integer是int的包装类,可以作为对象使用,具有更多的方法和一些方便的功能,如转换、比较等,但相对会带来一些性能和内存开销。
notify()和notifyAll()有什么区别
在Java中,notify()和notifyAll()都属于Object类的方法,用于实现线程间的通信。
notify()方法用于唤醒在当前对象上等待的单个线程。如果有多个线程同时在某个对象上等待(通过调用该对象的wait()方法),则只会唤醒其中一个线程,并使其从等待状态变为可运行状态。具体是哪个线程被唤醒是不确定的,取决于线程调度器的实现。
notifyAll()方法用于唤醒在当前对象上等待的所有线程。如果有多个线程在某个对象上等待,调用notifyAll()方法后,所有等待的线程都会被唤醒并竞争该对象的锁。其中一个线程获得锁后继续执行,其他线程则继续等待。
需要注意的是,notify()和notifyAll()方法只能在同步代码块或同步方法内部调用,并且必须拥有与该对象关联的锁。否则会抛出IllegalMonitorStateException异常。
synchronized的实现原理
synchronized是Java语言中最基本的线程同步机制,它通过互斥锁来控制线程对共享变量的访问。
具体实现原理如下:
- synchronized的实现基础是对象内部的锁(也称为监视器锁或管程),每个锁关联着一个对象实例。
- 当synchronized作用于某个对象时,它就会尝试获取这个对象的锁,如果锁没有被其他线程占用,则当前线程获取到锁,并可以执行同步代码块;如果锁已经被其他线程占用,那么当前线程就会阻塞在同步块之外,直到获取到锁才能进入同步块。
- synchronized还支持作用于类上,此时它锁住的是整个类,而不是类的某个实例。在这种情况下,由于只有一个锁存在,所以所有使用该类的线程都需要等待锁的释放。
- 在JVM内部,每个Java对象都有头信息,其中包含了对象的一些元信息和状态标志。synchronized通过修改头信息的状态标志来实现锁的获取和释放。
- synchronized还支持可重入性,即在同一个线程中可以多次获取同一个锁,这样可以避免死锁问题。
- Java虚拟机会通过锁升级的方式来提升synchronized的效率,比如偏向锁、轻量级锁和重量级锁等机制,使得在竞争不激烈的情况下,synchronized的性能可以达到与非同步代码相当的水平。
synchronized锁优化
synchronized还有一种重要的优化方式,即锁的优化技术。在Java 6及以上版本中,JVM引入了偏向锁、轻量级锁和重量级锁的概念来提高锁的性能。这些优化方式的原理如下:
- **偏向锁:**偏向锁是指当一个线程获取到锁之后,会在对象头中记录下该线程的标识,下次再进入同步块时,无需进行额外的加锁操作,从而提高性能。
- **轻量级锁:**当多个线程对同一个锁进行争夺时,JVM会使用轻量级锁来避免传统的重量级锁带来的性能消耗。它采用自旋的方式,即不放弃CPU的执行时间,尝试快速获取锁,避免线程阻塞和上下文切换的开销。
- **重量级锁:**当多个线程对同一个锁进行强烈争夺时,JVM会升级为重量级锁,此时线程会进入阻塞状态,等待锁的释放。这种方式适用于竞争激烈的情况,但会带来较大的性能开销。
锁优化技术是为了提高synchronized的并发性能,根据锁的竞争程度和持有时间的长短选择相应的锁状态,使得多个线程能够更高效地共享资源。
两个对象hashCode()相同,则equals()否也一定为true?
不一定。
根据Java的规范,如果两个对象的hashCode()返回值相同,那么它们可能相等,但并不保证一定相等。在某些情况下,两个不同的对象可能会产生相同的哈希码,这就是所谓的哈希冲突。因此,在判断两个对象是否相等时,还需要使用equals()方法进行进一步比较。
equals()方法用于比较两个对象的内容是否相等,而hashCode()方法用于获取对象的哈希码。根据Java规范,如果两个对象相等(通过equals()方法比较),它们的哈希码必须相等。但是对于哈希码相等的对象,它们的相等性仍然需要通过equals()方法进行详细比较确认。
为了确保正确的相等性判断,通常需要同时重写equals()和hashCode()方法。在重写equals()方法时,需要定义满足等价关系的比较规则,包括自反性、对称性、传递性和一致性。同时,重写hashCode()方法时,需要保证如果两个对象相等,则它们的哈希码必须相等,以避免哈希冲突。
总结:
两个对象的hashCode()方法返回相同的值,并不能保证它们的equals()方法一定返回true,因此在比较对象的相等性时,需要同时使用equals()方法和hashCode()方法。
什么是守护线程?与普通线程的区别
守护线程是在程序运行时在后台提供一种支持性的线程。与普通线程相比,守护线程有以下几个区别:
- **终止条件:**当所有用户线程结束时,守护线程会自动停止。换句话说,守护线程不会阻止程序的终止,即使它们还没有执行完任务。
- **生命周期:**守护线程的生命周期与主线程或其他用户线程无关。当所有的非守护线程都结束时,JVM 将会退出并停止守护线程的执行。
- **线程优先级:**守护线程的优先级默认与普通线程一样。优先级较高的守护线程也不能够保证在其他线程之前执行。
- **资源回收:**守护线程通常被用于执行一些后台任务,例如垃圾回收、日志记录、定时任务等。当只剩下守护线程时,JVM 会自动退出并且不会等待守护线程执行完毕。
需要注意的是,守护线程与普通线程在编写代码时没有太大的区别。可以通过将线程的setDaemon(true)方法设置为 true,将普通线程转换为守护线程。
总结起来,守护线程在程序运行过程中提供了一种支持性的服务,会在所有的用户线程结束时自动停止。
反射中,Class.forName和ClassLoader的区别
Class.forName和ClassLoader是Java反射中用于加载类的两种不同方式。
Class.forName是一个静态方法,通过提供类的完全限定名,在运行时加载类。此方法还会执行类的静态初始化块。如果类名不存在或无法访问,将抛出ClassNotFoundException异常。
ClassLoader是一个抽象类,用于加载类的工具。每个Java类都有关联的ClassLoader对象,负责将类文件加载到Java虚拟机中。ClassLoader可以动态加载类,从不同来源加载类文件,如本地文件系统、网络等。
两者区别如下:
- Class.forName方法由java.lang.Class类调用,负责根据类名加载类,并执行静态初始化。
- ClassLoader是抽象类,提供了更灵活的类加载机制,可以自定义类加载过程,从不同来源加载类文件。
一般情况下,推荐使用ClassLoader来加载和使用类,因为它更灵活,并避免执行静态初始化的副作用。Class.forName主要用于特定场景,如加载数据库驱动程序。
如何判断一个对象是否可以被回收
在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。
只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段
那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡
判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
- 引用计数算法(ReferenceCounting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况
- 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收
优点:
实现简单,垃圾对象便于辨识:判定效率高,回收没有延退性
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地**解决在引用计数算法中循环引用的问题,防止内存泄漏的发生,**这里的可达性分析就是Java、c#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集
- 可达性分析算法是以根对象集合(GCRoots就是一组必须活跃的引用)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能够被根对象集合直接或者问接连接的对象才是存活对象
在Java 语言中,GC Roots 包括以下几类元素:
- 虚拟机栈中引用的对象,比如:各个线程被调用方法中使用到的参数、局部变量等
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
- 方法区中常量引用的对象,比如:字符串常量池(StringTable)里的引用
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用,基本数据类型对应的class对象,一些常驻的异常对象(如: NullPointerException、OutofMemoryError),系统类加载器
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
- 除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完 整GC Roots集合。比如:分代收集和局部回收(PartialGC)
- 如果只针对了java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GCRoots集合中去考虑,才能保证可达性分析的准确性。
小技巧:
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root
注意
- 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证
- 这点也是导致GC进行时必须Stop The World的一个重要原因,即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
如何实现对象克隆
在Java中,实现对象的克隆有两种方式: 浅拷贝和深拷贝。
- **浅拷贝:**通过创建一个新对象,并将原对象的非静态字段值复制给新对象实现。新对象和原对象共享引用数据。在Java中,可以使用clone()方法实现浅拷贝。要实现一个类的克隆操作,需要满足以下条件:
- 实现Cloneable接口。
- 重写Object类的clone()方法,声明为public访问权限。
- 在clone()方法中调用super.clone(),并处理引用类型字段。
- **深拷贝:**通过创建一个新对象,并将原对象的所有字段值复制给新对象,包括引用类型数据。新对象和原对象拥有独立的引用数据。实现深拷贝有以下方式:
- 使用序列化和反序列化实现深拷贝,要求对象及其引用类型字段实现Serializable接口。
- 自定义拷贝方法,递归拷贝引用类型字段。
如何实现线程的同步
线程的同步是为了保证多个线程按照特定的顺序、协调地访问共享资源,避免数据不一致和竞争条件等问题。
在Java中,常见的线程同步方式有以下几种:
- **使用synchronized关键字:**通过在方法或代码块前加上synchronized关键字,确保同一时间只有一个线程可以执行标记为同步的代码。这样可以避免多个线程同时访问共享资源造成的数据不一致问题。
- **使用ReentrantLock类:**它是一个可重入锁,通过调用lock()和unlock()方法获取和释放锁。与synchronized不同,ReentrantLock提供了更灵活的同步控制,例如可实现公平性和试锁等待时间。
- **使用wait()、notify()和notifyAll()方法:**这些方法是Object类的方法,允许线程间进行协作和通信。通过调用wait()方法使线程进入等待状态,然后其他线程可以通过notify()或notifyAll()方法唤醒等待的线程。
- **使用CountDownLatch和CyclicBarrier:**它们是并发工具类,用于线程之间的同步和等待。CountDownLatch可用于等待一组线程完成操作,而CyclicBarrier用于等待一组线程互相达到屏障位置。
选择适合的同步方式会根据具体需求和场景而定。在使用任何同步机制时,需要注意避免死锁和性能问题,合理设计同步范围和粒度。
工作中最常见的6种OOM问题
今天跟大家一起聊聊线上服务出现OOM问题的6种场景,希望对你会有所帮助。
堆内存OOM是最常见的OOM了。
出现堆内存OOM问题的异常信息如下:
java.lang.OutOfMemoryError: Java heap space
此OOM是由于JVM中heap的最大值,已经不能满足需求了。
举个例子:
@Test
public void test01() {
List<OOMTests> list = Lists.newArrayList();
while (true) {
list.add(new OOMTests());
}
}
这里创建了一个list集合,在一个死循环中不停往里面添加对象。
执行结果:
出现了java.lang.OutOfMemoryError: Java heap space的堆内存溢出。
很多时候,excel一次导出大量的数据,获取在程序中一次性查询的数据太多,都可能会出现这种OOM问题。
我们在日常工作中一定要避免这种情况。
有时候,我们的业务系统创建了太多的线程,可能会导致栈内存OOM。
出现堆内存OOM问题的异常信息如下:
java.lang.OutOfMemoryError: unable to create new native thread
给大家举个例子:
public class StackOOMTest {
public static void main(String[] args) {
while (true) {
new Thread().start();
}
}
}
使用一个死循环不停创建线程,导致系统产生了大量的线程。
如果实际工作中,出现这个问题,一般是由于创建的线程太多,或者设置的单个线程占用内存空间太大导致的。
建议在日常工作中,多用线程池,少自己创建线程,防止出现这个OOM。
我们在业务代码中可能会经常写一些递归
调用,如果递归的深度超过了JVM允许的最大深度,可能会出现栈内存溢出问题。
出现栈内存溢出问题的异常信息如下:
java.lang.StackOverflowError
例如:
@Test
public void test03() {
recursiveMethod();
}
public static void recursiveMethod() {
// 递归调用自身
recursiveMethod();
}
执行结果:
出现了java.lang.StackOverflowError栈溢出的错误。
我们在写递归代码时,一定要考虑递归深度。即使是使用parentId一层层往上找的逻辑,也最好加一个参数控制递归深度。防止因为数据问题导致无限递归的情况,比如:id和parentId的值相等。
直接内存
不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
它来源于NIO
,通过存在堆中的DirectByteBuffer
操作Native内存,是属于堆外内存
,可以直接向系统申请的内存空间。
出现直接内存OOM问题时异常信息如下:
java.lang.OutOfMemoryError: Direct buffer memory
例如下面这样的:
private static final int BUFFER = 1024 * 1024 * 20;
@Test
public void test04() {
ArrayList<ByteBuffer> list = new ArrayList<>();
int count = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
list.add(byteBuffer);
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
System.out.println(count);
}
}
会看到报出来java.lang.OutOfMemoryError: Direct buffer memory直接内存空间不足的异常。
GC OOM
是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略。
出现GC OOM问题时异常信息如下:
java.lang.OutOfMemoryError: GC overhead limit exceeded
为了方便测试,我先将idea中的最大和最小堆大小都设置成10M:
例如下面这个例子:
public class GCOverheadOOM {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
});
}
}
}
出现这个问题是由于JVM在GC的时候,对象太多,就会报这个错误。
我们需要改变GC的策略。
在老代80%时就是开始GC,并且将-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)设置的更合理。
JDK8
之后使用Metaspace
来代替永久代
,Metaspace是方法区在HotSpot
中的实现。
Metaspace不在虚拟机内存中,而是使用本地内存也就是在JDK8中的ClassMetadata
,被存储在叫做Metaspace的native memory。
出现元空间OOM问题时异常信息如下:
java.lang.OutOfMemoryError: Metaspace
为了方便测试,我修改一下idea中的JVM参数,增加下面的配置:
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
指定了元空间和最大元空间都是10M。
接下来,看看下面这个例子:
public class MetaspaceOOMTest {
static class OOM {
}
public static void main(String[] args) {
int i = 0;
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
}
程序最后会报java.lang.OutOfMemoryError: Metaspace的元空间OOM。
这个问题一般是由于加载到内存中的类太多,或者类的体积太大导致的。
抽象工厂和工厂方法模式的区别
抽象工厂模式和工厂方法模式是两种创建型设计模式,都关注对象的创建,但有一些区别。
- 抽象工厂模式提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定具体的类。它适用于需要一次性创建多个相关对象,以形成一个产品族。抽象工厂模式通常由抽象工厂、具体工厂、抽象产品和具体产品组成。通过切换具体工厂实现类,可以改变整个产品族。
- 工厂方法模式将对象的创建延迟到子类中进行。它定义一个用于创建对象的抽象方法,由子类决定具体实例化哪个类。工厂方法模式适用于需要根据不同条件动态地创建不同类型的对象。它通常由抽象工厂、具体工厂、抽象产品和具体产品组成。通过切换具体工厂子类,可以改变单个产品。
总的来说,抽象工厂模式更关注一系列相关对象的创建,用于创建产品族;工厂方法模式更关注单个对象的创建,用于根据不同条件创建不同类型的对象。
抽象类和接口有什么区别
抽象类和接口是Java中的两种机制,用于实现类之间的继承和多态性。它们有以下几点区别:
- **定义和设计:**抽象类是使用abstract关键字定义的类,可以包含抽象方法和非抽象方法,可以有实例变量和构造方法;接口通过interface关键字定义,只能包含抽象方法、默认方法和静态方法,不包含实例变量或构造方法。
- **继承关系:**一个类只能继承自一个抽象类,但可以实现多个接口。继承抽象类体现的是"is-a"关系,而实现接口体现的是"can-do"关系。
- 构造方法:抽象类可以有构造方法,子类可以通过super()调用父类的构造方法;接口没有构造方法。
- **默认实现:**抽象类可以包含非抽象方法,子类可以直接使用;接口可以包含默认方法,提供通用实现,子类可以选择重写或者使用默认实现。
- **设计目的:**抽象类的设计目的是提供类的继承机制,实现代码复用,适用于拥有相似行为和属性的类;接口的设计目的是定义一组规范或契约,实现类遵循特定的行为和功能,适用于不同类之间的解耦和多态性实现。
总之,抽象类和接口是实现继承和多态性的两种机制。抽象类和接口的设计目的、定义和使用方法等方面都有所区别,需要根据实际情况选择合适的方式进行设计和使用。
日期格式化用yyyy还是YYYY
一个工作6年的小伙伴日期时间格式化还在写大写YYYY, 你是真的没踩过雷啊!
哈喽大家好我是徐庶!需要本视频文字版的小伙伴可以在评论区扣666,里面会有更详细的图和代码。
如果你现在还在用大写YYYY格式化日期,那赶紧换成小写yyyy。
有什么区别? 踩过雷的我来告诉你!通常情况下可能没问题,但是**在跨年的时候**大概率就会有问题了。
你可以尝试把“2024-12-31 23:59:59”通过大写Y和小写Y进行格式化, 会发现大写Y的日期2024年变成了2025年,为什么会这样呢?
原因很简单, 大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而小写的yyyy是基于日历的年份。
所以通常情况下,你就发现不了什么问题,但在**跨年的第一周或最后一周**可能会有差异。
这就是一个隐性雷了,赶紧去项目里面封装一个公共的日期处理类规范起来吧, 要不然下次来个小白又写大写Y了。
好, 如果视频对你有帮助可以给徐庶老师一个三连, 我们下期见!/
有哪些常见的运行时异常
运行时异常是在 Java 程序运行过程中才会出现的异常,通常情况下不需要进行 try-catch 处理。以下是 5 个常见的运行时异常:
- 空指针异常:当应用程序尝试使用 null 对象时抛出。
- 数组越界异常:当应用程序尝试访问数组元素的时候,数组下标超出了数组的范围。
- 类转换异常:当应用程序尝试将一个对象强制转换为不是其实例的子类时抛出。
- 非法参数异):当应用程序传递了一个无效或不合法的参数时抛出。
- 非法状态异常:当应用程序调用了一个不合适的方法或处于不正确的状态时抛出。
这些异常是在程序运行过程中出现的,并且多数情况下是由于编程错误造成的。因此,在编写 Java 程序时,应该避免出现这些异常。如果必须出现,也应该在代码设计时加以处理,避免对应用程序的正常运行造成影响。
构造器是否可被重写
**构造器在Java中是一种特殊的方法,用于创建和初始化对象。**与其他普通方法不同,构造器的名称必须与类名一致,并且没有返回类型。
**在Java中,构造器不能被直接重写。**子类无法定义与父类相同名称和参数的构造器。这是因为构造器是用于创建对象并初始化其状态的特殊方法,它与类的实例化密切相关。如果允许子类重写构造器,那么可能会导致对象的创建和初始化过程出现混乱,破坏了类的结构和设计原则。
然而,**子类可以通过调用父类的构造器来完成对继承的父类的初始化操作。**在子类的构造器中可以使用关键字super来调用父类的构造器,并传递相应的参数。这样可以确保父类的构造器得到正确地执行,从而完成对父类属性的初始化。
总结起来,构造器本身不能被重写,但子类可以通过调用父类的构造器来实现对父类的初始化操作。
程序员必懂的权限模型:RBAC
随着软件系统的复杂性和规模的不断增长,权限管理成为了一个至关重要的问题。
在大型多人协作的系统中,如何有效地管理不同用户的访问权限,确保系统的安全性和稳定性,是每一个开发者都需要面对的挑战。为了解决这一问题,业界提出了一种被广泛应用的权限管理模型——基于角色的访问控制(Role-Based Access Control,简称RBAC)。
说起 RBAC 权限模型,我们先看下在“维基”上的定义:
RBAC 其实是一种分析模型,主要分为:基本模型 RBAC0、角色分层模型 RBAC1、角色限制模型 RBAC2 和 统一模型 RBAC3。
RBAC 权限模型是基于角色的权限控制。模型中有几个关键的术语:
- 用户:系统接口及访问的操作者
- 权限:能够访问某接口或者做某操作的授权资格
- 角色:具有一类相同操作权限的用户的总称
RBAC0 是 RBAC 权限模型的核心思想,RBAC1、RBAC2、RBAC3 都是在 RBAC0 上进行扩展的。
RBAC0 是由四部分构成:用户、角色、会话、许可。
- 用户和角色的含义很简单,通过字面意思即可明白,
- **会话:**指用户被赋予角色的过程,称之为会话或者是说激活角色;
- 许可: 就是角色拥有的权限(操作和和被控制的对象),简单的说就是用户可使用的功能或者可查看的数据。
用户与角色是多对多的关系,用户与会话是一对一的关系,会话与角色是一对多的关系,角色与许可是多对多的关系。
RBAC1 是在 RBAC0 权限模型的基础上,在角色中加入了**继承的概念**,添加了继承的概念后,角色就有了上下级或者等级关系。
举例:集团权责清单下包含的角色有:系统管理员、总部权责管理员、区域权责管理员、普通用户,当管理方式向下兼容时,就可以采用 RBAC1 的继承关系来实现权限的设置。
上层角色拥有下层的所有角色的权限,且上层角色可拥有额外的权限
RBAC2 是在 RBAC0 权限模型的基础上,在用户和角色以及会话和角色之间分别加入了**约束的概念**(职责分离),职责分离指的是同一个人不能拥有两种特定的权限(例如财务部的纳入和支出,或者运动员和裁判员等等)。
用户和角色的约束有以下几种形式:
- 互斥角色:同一个用户在两个互斥角色中只能选择一个(也会存在一个用户拥有多个角色情况,但是需要通过切换用户角色来实现对不同业务操作)
- 基数约束:一个用户拥有的角色是有限的,一个角色拥有的许可也是有限的
- 先决条件约束:用户想要获得高级角色,首先必须拥有低级角色
会话和角色之间的约束,可以动态的约束用户拥有的角色,例如一个用户可以拥有两个角色,但是运行时只能激活一个角色。
RBAC3 是 RBAC1 与 RBAC2 的合集,所以 RBAC3 包含**继承和约束**。
RBAC 中具有角色的概念,如果没有角色这个概念,那么在系统中,每个用户都需要单独设置权限,而系统中所涉及到的功能权限和数据权限都非常多,每个用户都单独设置权限对于维护权限的管理员来说无疑是一件繁琐且工作量巨大的任务。
而引入角色这个概念后,我们只需要给系统设置不同的角色,给角色赋予权限,再将用户与角色关联,这样用户所关联的角色就直接拥有了该角色下的所有权限。
例如:用户 1~用户 8 分别拥有以下权限,不同用户具有相同权限的我用不同的颜色做了区分,如下图:
在没有引入 RBAC 权限模型的情况下,用户与权限的关系图可采用下图展示,每个用户分别设置对应的权限,即便是具有相同权限的用户也需要多次设置权限。
引入 RBAC 权限模型及引入了角色的概念,根据上面表格的统计,用户 1、用户 3、用户 5、用户 8 拥有的权限相同,用户 2、用户 6、用户 7 拥有相同的权限,用户 4 是独立的权限,所以我们这里可以根据数据统计,以及实际的需求情况,可以建立三个不同的角色,角色 A、角色 B、角色 C,三个角色分别对应三组用户不同的权限,如下图所示:
对应的上面的案例表格我们就可以调整为含有角色列的数据表,这样便可以清楚的知道每个用户所对应的角色及权限。
通过引用 RBAC 权限模型后,对于系统中大量的用户的权限设置可以更好的建立管理,角色的引入让具有相同权限的用户可以统一关联到相同的角色中,这样只需要在系统中设置一次角色的权限,后续的用户便可以直接关联这些角色,这样就省去了重复设置权限的过程,对于大型平台的应用上,用户的数量成千上万,这样就可避免在设置权限这项工作上浪费大量的时间。
我们依旧拿上面表格案例举例,虽然前面我们应用的 RBAC 权限模型的概念,但是对于大量用户拥有相同权限的用户,我们同样的也需要对每个用户设置对应的角色,如果一个部门上万人,那么我们就需要给这个部门上万人分别设置角色,而这上万其实是具有相同的权限的,如果直接采用基础的 RBAC 权限模型的话,那么面对这样的情况,无疑也是具有一个庞大的重复的工作量,并且也不利于后期用户变更的维护管理,那么针对相同用户具有相同的权限的情况,我们便可以引入用户组的概念。
什么是用户组呢?用户组:把具有相同角色的用户进行分类。
上面我们的数据表格案例中的用户 1、用户 3、用户 5、用户 8 具有相同的角色 A,用户 2、用户 6、用户 7 也拥有相同的角色 B,那么我们就可以将这些具有相同角色的用户建立用户组的关系,拿上面的案例,我们分别对相同角色的用户建立组关系,如下:
- 用户 1、用户 3、用户 5、用户 8 → 建立用户组1
- 用户 2、用户 6、用户 7 → 建立用户组2
因为用户 4 只有一个用户,所以直接还是单独建立用户与角色的关系,不需要建立用户组,尽管只有一个用户也是可以建立用户组的关系,这样有利于后期其他用户与用于 4 具有相同的角色时,就可以直接将其他用户添加到这个用户组下即可,根据业务的实际情况而选择适合的方案即可。
通过案例表格的变化我们就可以直观的看出权限设置变得清晰简洁了,通过第用户组赋予角色,可以减少大量的重复的工作,我们常见的企业组织、部门下经常会出现不同用户具有相同角色的情况,所以采用用户组的方式,便可以很好的解决这个问题,给具有相同权限的用户建立用户组,将用户组关联到对应的角色下,此用户组就拥有了此角色下的所有权限,而用户是属于用户组的,所以用户组下的所有用户也就同样的拥有了此角色下的所有权限。一个用户可以属于多个用户组,一个用户组也可以包括多个用户,所以用户与用户组是多对多的关系。
权限组与用户组的原理差不多,是将一些相对固定的功能或者权限建立组的关系,然后再给此权限组赋予角色,目前我所接触的 后端项目中使用权限组的概念的比较少,可简单的看一下关系图
后端系统中一般产品的权限由页面、操作和数据构成。页面与操作相互关联,必须拥有页面权限,才能分配该页面下对应的操作权限,数据可被增删改查。所以将权限管理分为功能权限管理和数据权限管理。
功能权限管理:指的是用户可看到哪些模块,能操作哪些按钮,因为企业中的用户拥有不同的角色,拥有的职责也是不同的。
数据权限管理:指的是用户可看到哪些模块的哪些数据。
例如:一个系统中包含多个权责清单(清单 1、清单 2、清单 3),系统管理员能对整个系统操作维护,也就可以对系统中的所有清单进行操作(增、删、改、查);假如分配给总部权责管理员的是清单 1,那么他将只能对清单 1 进行操作(增、改、查);普通用户也许只有查看数据的权限,没有数据维操作的权限(查),这里的操作是系统中所有可点击的按钮权限操作,列举的增删改查只是最常见的几种操作而已。
RBAC 权限模型对实际业务需求进行设计分析:
- 不同的区域管理员的权限各不相同(说明会存在不同的用户具有不同的权限,那么我们就可以采用角色对其进行规范)
- 有大量的用户具有相同的权限(例如组织、部门等)(说明存在相同权限的用户,那么我们就可以采用用户组的概念)
- 上级管理员拥有下级人员的所有权限(说明存在继承关系)
- 不同用户所看到的数据和能编辑的数据不同,一些机密性的数据只允许部分人员看或者编辑(说明存在约束)
- 会存在临时性的用户(说明需要支持新建新角色)
- 同一用户会存在多个角色(多角色求合集或者切换用户角色)
主流的权限模型主要分为以下五种:
- ACL 模型:访问控制列表
- DAC 模型:自主访问控制
- MAC 模型:强制访问控制
- ABAC 模型:基于属性的访问控制
- RBAC 模型:基于角色的权限访问控制
Access Control List,ACL 是最早的、最基本的一种访问控制机制,是基于客体进行控制的模型,在其他模型中也有 ACL 的身影。为了解决相同权限的用户挨个配置的问题,后来也采用了用户组的方式。
原理:每一个客体都有一个列表,列表中记录的是哪些主体可以对这个客体做哪些行为,非常简单。
例如:当用户 A 要对一篇文章进行编辑时,ACL 会先检查一下文章编辑功能的控制列表中有没有用户 A,有就可以编辑,无则不能编辑。再例如:不同等级的会员在产品中可使用的功能范围不同。
缺点:当主体的数量较多时,配置和维护工作就会成本大、易出错。
Discretionary Access Control,DAC 是 ACL 的一种拓展。
原理:在 ACL 模型的基础上,允许主体可以将自己拥有的权限自主地授予其他主体,所以权限可以任意传递。
例如:常见于文件系统,LINUX,UNIX、WindowsNT 版本的操作系统都提供 DAC 的支持。
缺点:对权限控制比较分散,例如无法简单地将一组文件设置统一的权限开放给指定的一群用户。主体的权限太大,无意间就可能泄露信息。
Mandatory Access Control,MAC 模型中主要的是双向验证机制。常见于机密机构或者其他等级观念强烈的行业,如军用和市政安全领域的软件。
原理:主体有一个权限标识,客体也有一个权限标识,而主体能否对该客体进行操作取决于双方的权限标识的关系。
例如:将军分为上将>中将>少将,军事文件保密等级分为绝密>机密>秘密,规定不同军衔仅能访问不同保密等级的文件,如少将只能访问秘密文件;当某一账号访问某一文件时,系统会验证账号的军衔,也验证文件的保密等级,当军衔和保密等级相对应时才可以访问。
缺点:控制太严格,实现工作量大,缺乏灵活性。
Attribute-Based Access Control,能很好地解决 RBAC 的缺点,在新增资源时容易维护。
原理:通过动态计算一个或一组属性是否满足某种机制来授权,是一种很灵活的权限模型,可以按需实现不同颗粒度的权限控制。
属性通常有四类:
- 主体属性,如用户年龄、性别等;
- 客体属性,如一篇文章等;
- 环境属性,即空间限制、时间限制、频度限制;
- 操作属性,即行为类型,如读写、只读等。
例如:早上 9:00,11:00 期间 A、B 两个部门一起以考生的身份考试,下午 14:00,17:00 期间 A、B 两个部门相互阅卷。
缺点:规则复杂,不易看出主体与客体之间的关系,实现非常难,现在应用得很少。
缓存淘汰机制LRU和LFU的区别,电商场景下用哪个?
在高并发、高访问量的电商平台中,缓存是提升性能和保障用户体验的关键。然而,受限于物理内存,缓存空间总是有限,因此必须采用合适的淘汰(替换)策略,保证最有价值的数据能长时间驻留缓存。
本文将对比两种主流的缓存淘汰算法:
LRU(最近最少使用)与 LFU(最不经常使用),并结合Java代码实现,最后分析电商场景下如何选择。
思路:优先淘汰最近一段时间最久未被访问的数据。
实现:常用哈希表 + 双向链表;每当访问或新增数据,即把该数据节点移到链表头部。淘汰时直接移除链表尾部。
优点:实现简单,适应访问热点快速变化的场景。
流程讲解:
- 当我们连续插入A、B、C、…Z的时候,此时内存已经
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">插满</font>
了 - 那么当我们再插入一个6,那么此时会将内存存放时间最久的数据A淘汰掉。
- 当我们从外部读取数据C的时候,此时C就会
<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">提到头部</font>
,这时候C就是最晚淘汰的了。
思路:优先淘汰一段时间内访问次数(频率)最低的数据。
实现:需要维护每个数据节点的访问频率,多用哈希表加“频率链表”组合。淘汰最低频率中的最旧节点。
优点:能更好保留长期高频次访问的数据,适合长期热门但访问分布分散的场景。
流程讲解:
- 如果A没有出现过,那么就会放在双向链表的最后,依次类推,就会是Z、Y。。C、B、A的顺序放到频率为1的链表中。
- 当我们新插入 A,B,C 那么A,B,C就会到频率为2的链表中
- 如果再次插入A,B那么A,B会在频率为3中。C依旧在2中
- 如果此时已经满了 ,新插入一个的话,我们会把最后一个D移除,并插入 6
LRU | LFU | |
---|---|---|
原理 | 淘汰最久未被访问的数据 | 淘汰访问次数最少的数据 |
复杂度 | O(1) | O(1) (合理实现时) |
优点 | 实现简单,时间开销小 | 保护长期高频数据,减少冷数据回流 |
缺点 | 容易“误杀”最近高频数据 | 实现较复杂,突发热点响应慢 |
电商常见的数据访问模式:
- 秒杀、大促、首页推荐:短时间部分商品或页面极度火爆,热点变换快。
- 长尾商品、个性化推荐:部分数据长期有较低频率访问。
选择建议:
- 若面向热点突变频繁场景(如秒杀、活动页)——优先选择LRU。 因为持续被访问的热点商品会留在缓存前端,发生访问突变时能快速适应变化,防止缓存穿透。
- 若需保护“常青”商品或内容库(如个性化、长期售卖页面)——可考虑LFU。 能留住虽然访问不集中的长期高频数据,防止被LRU“误杀”。
实际项目中常采用分区或多级缓存,针对不同业务分别设计缓存策略(如:活动页LRU,推荐页LFU)。
- LRU和LFU的根本区别:一个重“新近性”、一个重“访问频率”。
- 电商访问模式以热点突变为主,推荐优先使用LRU缓存机制。
- 综合业务需求时,可以混合使用LRU/LFU,或采用2Q、LRU-K等改进型算法,结合流量特征和数据重要性灵活选型。
import java.util.HashMap;
import java.util.Map;
public class LRUCache<K, V> {
private final int capacity;
private final Map<K, Node<K, V>> map;
private final Node<K, V> head, tail;
static class Node<K, V> {
K key;
V value;
Node<K, V> prev, next;
Node() {}
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>();
head = new Node<>();
tail = new Node<>();
head.next = tail;
tail.prev = head;
}
public V get(K key) {
Node<K, V> node = map.get(key);
if (node == null) return null;
moveToHead(node);
return node.value;
}
public void put(K key, V value) {
Node<K, V> node = map.get(key);
if (node == null) {
node = new Node<>(key, value);
map.put(key, node);
addToHead(node);
if (map.size() > capacity) {
Node<K, V> removed = removeTail();
map.remove(removed.key);
}
} else {
node.value = value;
moveToHead(node);
}
}
// 双向链表操作
private void addToHead(Node<K, V> node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(Node<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(Node<K, V> node) {
removeNode(node);
addToHead(node);
}
private Node<K, V> removeTail() {
Node<K, V> node = tail.prev;
removeNode(node);
return node;
}
}
import java.util.*;
public class LFUCache<K, V> {
private final int capacity;
private int minFreq;
private final Map<K, Node<K, V>> nodeMap;
private final Map<Integer, LinkedHashSet<Node<K, V>>> freqMap;
static class Node<K, V> {
K key;
V value;
int freq;
Node(K key, V value) {
this.key = key;
this.value = value;
this.freq = 1;
}
}
public LFUCache(int capacity) {
this.capacity = capacity;
this.minFreq = 0;
this.nodeMap = new HashMap<>();
this.freqMap = new HashMap<>();
}
public V get(K key) {
Node<K, V> node = nodeMap.get(key);
if (node == null) return null;
increaseFreq(node);
return node.value;
}
public void put(K key, V value) {
if (capacity == 0) return;
if (nodeMap.containsKey(key)) {
Node<K, V> node = nodeMap.get(key);
node.value = value;
increaseFreq(node);
} else {
if (nodeMap.size() == capacity) {
// 淘汰最低频率且最早的数据
LinkedHashSet<Node<K, V>> set = freqMap.get(minFreq);
Node<K, V> toRemove = set.iterator().next();
set.remove(toRemove);
nodeMap.remove(toRemove.key);
}
Node<K, V> newNode = new Node<>(key, value);
nodeMap.put(key, newNode);
freqMap.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(newNode);
minFreq = 1;
}
}
private void increaseFreq(Node<K, V> node) {
int freq = node.freq;
LinkedHashSet<Node<K, V>> set = freqMap.get(freq);
set.remove(node);
if (freq == minFreq && set.isEmpty()) {
minFreq++;
}
node.freq++;
freqMap.computeIfAbsent(node.freq, k -> new LinkedHashSet<>()).add(node);
}
}
public class CacheTest {
public static void main(String[] args) {
// 测试LRU缓存
LRUCache<Integer, String> lruCache = new LRUCache<>(2);
lruCache.put(1, "A");
lruCache.put(2, "B");
System.out.println(lruCache.get(1)); // 输出: A
lruCache.put(3, "C");
System.out.println(lruCache.get(2)); // 输出: null (2被淘汰)
// 测试LFU缓存
LFUCache<Integer, String> lfuCache = new LFUCache<>(2);
lfuCache.put(1, "A");
lfuCache.put(2, "B");
System.out.println(lfuCache.get(1)); // 输出: A
lfuCache.put(3, "C");
System.out.println(lfuCache.get(2)); // 输出: null (2被淘汰,因1频率高)
}
}
讲讲你对CountDownLatch的理解
CountDownLatch是Java中用于多线程协作的辅助类,它可以让一个或多个线程等待其他线程完成某个任务后再继续执行。
CountDownLatch通过一个计数器来实现,计数器的初始值可以设置为等待的线程数量。每个线程在完成任务后都会调用countDown()方法来减少计数器的值。当计数器的值减至0时,等待在CountDownLatch上的线程就会被唤醒,可以继续执行后续的操作。
CountDownLatch的主要作用是协调多个线程的执行顺序,使得某个线程(或多个线程)必须等待其他线程完成后才能继续执行。它常用于以下场景:
- 主线程等待多个子线程完成任务:主线程可以使用await()方法等待所有子线程完成,然后进行结果的汇总或其他操作。
- 多个线程等待外部事件的发生:多个线程可以同时等待某个共同的事件发生,比如等待某个资源准备就绪或者等待某个信号的触发。
- 控制并发任务的同时开始:在某些并发场景中,需要等待所有线程都准备就绪后才能同时开始执行任务,CountDownLatch提供了一种便捷的方式来实现这一需求。
需要注意的是,CountDownLatch的计数器是不能被重置的,也就是说它是一次性的。一旦计数器减至0,它将无法再次使用。如果需要多次使用可重置的计数器,则可以考虑使用CyclicBarrier。
讲讲你对CyclicBarrier的理解
CyclicBarrier是Java中的一个多线程协作工具,它可以让多个线程在一个屏障点等待,并在所有线程都到达后一起继续执行。与CountDownLatch不同,CyclicBarrier可以重复使用,并且可以指定屏障点后执行的额外动作。
CyclicBarrier的主要特点有三个。
- **首先,**它可以重复使用,这意味着当所有线程都到达屏障点后,屏障会自动重置,可以用来处理多次需要等待的任务。
- **其次,**CyclicBarrier可以协调多个线程同时开始执行,这在分阶段任务和并发游戏等场景中非常有用。
- **最后,**CyclicBarrier还提供了可选的动作,在所有线程到达屏障点时执行,可以实现额外的逻辑。
需要注意的是,在创建CyclicBarrier时需要指定参与线程的数量。一旦所有参与线程都到达屏障点后,CyclicBarrier解除阻塞,所有线程可以继续执行后续操作。
讲讲你对ThreadLocal的理解
ThreadLocal是Java中的一个类,用于在多线程环境下实现线程局部变量存储。它提供了一种让每个线程都拥有独立变量副本的机制,从而避免了多线程之间相互干扰和竞争的问题。
在多线程编程中,共享变量的访问往往需要考虑线程安全性和数据隔离问题。ThreadLocal通过为每个线程创建独立的变量副本来解决这些问题。每个线程可以独立地对自己的变量副本进行操作,而不会影响其他线程的副本。
ThreadLocal的核心思想是以"线程"为作用域,在每个线程内部维护一个变量副本。它使用Thread对象作为Key,在内部的数据结构中查找对应的变量副本。当通过ThreadLocal的get()方法获取变量时,实际上是根据当前线程获取其对应的变量副本;当通过set()方法设置变量时,实际上是将该值与当前线程关联,并存储在内部的数据结构中。
使用ThreadLocal时需要注意以下几点:
- **内存泄漏:**在使用完ThreadLocal后,应及时调用remove()方法清理与当前线程相关的变量副本,避免长时间持有引用导致内存泄漏。
- **线程安全性:**ThreadLocal本身并不解决多线程并发访问共享变量的问题,需要额外的同步机制来保证线程安全性。
- **数据隔离:**ThreadLocal适用于多线程环境下需要保持变量独立性的场景,可以避免使用传统的同步方式对共享变量进行操作,提高并发性能。
ThreadLocal常见的应用场景包括线程池、Web开发中的请求上下文信息管理、数据库连接管理和日志记录等。通过合理使用ThreadLocal,可以简化多线程编程,并提高程序的性能和可维护性。
设计模式是如何分类的
根据应用目标,设计模式可以分为创建型、结构型和行为型。
- 创建型模式是关于对象创建过程的总结,包括单例、工厂、抽象工厂、建造者和原型模式。
- 结构型模式是针对软件设计结构的总结,包括桥接、适配器、装饰者、代理、组合、外观和享元模式。
- 行为型模式是从类或对象之间交互、职责划分等角度总结的模式,包括策略、解释器、命令、观察者、迭代器、模板方法和访问者模式。
这些模式各自解决特定问题,并在软件开发中得到广泛应用。比如单例模式确保一个类只有一个实例,适配器模式将一个类的接口转换为客户端所期望的另一个接口。装饰者模式动态地给对象添加额外的职责,命令模式将请求封装成一个对象,从而使得可以用不同的请求对客户进行参数化。观察者模式定义了对象之间的一对多依赖关系,当一个对象改变状态时,其依赖者会收到通知并自动更新。
这些设计模式各自具有明确的应用场景和优缺点,在软件开发中的应用可以提高代码的可维护性和复用性,同时也可以减少出错的可能性并提高软件开发效率。
说说你对lambda表达式的理解
Lambda表达式是Java 8引入的一种简洁的语法形式,用于表示匿名函数。它可以作为参数传递给方法或函数接口,并且可以在需要函数式编程特性的地方使用。
Lambda表达式的语法类似于(参数列表) -> 表达式或代码块。参数列表描述了输入参数,可以省略类型,甚至括号。箭头符号将参数列表与表达式或代码块分隔开来。
Lambda表达式具有以下特点:
- **简洁:**相较于传统的匿名内部类,Lambda表达式更加简洁,能用更少的代码实现相同功能。
- **函数式编程:**支持函数作为一等公民进行传递和操作。
- **闭包:**可以访问周围的变量和参数。
- **方法引用:**可以通过引用已存在的方法进一步简化。
Lambda表达式的应用场景包括:
- **集合操作:**对集合元素进行筛选、映射、排序等操作,使代码简洁和可读。
- **并行编程:**利用Lambda表达式简化并发编程的复杂性。
- **事件驱动模型:**作为回调函数响应用户输入或系统事件。
需要注意,Lambda表达式仅适用于函数式接口(只有一个抽象方法的接口),可直接实现该接口的实例,避免编写传统匿名内部类。Lambda表达式在Java编程中提供了更为灵活和简洁的语法,促进了函数式编程的应用。
Lambda 表达式是 Java 8 引入的一个重要特性,它允许以更简洁的方式编写匿名类和函数式接口的实现。Lambda 表达式的核心思想是将代码块作为方法参数传递,从而简化代码并提高可读性。以下是一些常见的面试问题和答案,帮助你深入理解 Lambda 表达式。
Lambda 表达式是一种匿名函数,可以用来表示一个函数式接口的实现。它允许将代码块作为方法参数传递,从而简化代码。
// 使用匿名类
/**
* @Auth:TianMing
* @Description: 基本应用
*/
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello, Anonymous Class!");
}
}).start();
// 使用 Lambda 表达式
new Thread(() -> System.out.println("Hello, Lambda!")).start();
Lambda 表达式的语法包括三部分:
- 参数列表:可以显式或隐式指定参数类型。
- 箭头符号:
<font style="color:rgba(0, 0, 0, 0.9);">-></font>
,表示参数和函数体之间的分隔符。 - 函数体:可以是一个表达式或代码块。
// 参数类型显式指定
(int a, int b) -> a + b;
// 参数类型隐式推断
(a, b) -> a + b;
// 单个参数时可以省略括号
x -> x * x;
// 函数体为代码块
(x, y) -> {
int result = x + y;
return result;
};
Lambda 表达式必须与函数式接口结合使用。函数式接口是一个只包含一个抽象方法的接口,可以用 <font style="color:rgba(0, 0, 0, 0.9);">@FunctionalInterface</font>
注解标记。
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
/**
* @Auth:TianMing
* @Description: 和函数式接口的关系
*/
public class LambdaExample {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
System.out.println("Addition: " + add.calculate(5, 3)); // 8
Calculator multiply = (a, b) -> a * b;
System.out.println("Multiplication: " + multiply.calculate(5, 3)); // 15
}
}
Lambda 表达式的主要作用是简化代码,提高代码的可读性和可维护性。它特别适合用于以下场景:
- 事件处理:简化事件监听器的实现。
- Stream API:简化集合操作。
- 函数式编程:支持函数式编程风格。
/**
* @Auth:TianMing
* @Description: 作用
*/
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 使用 Lambda 表达式过滤和打印名字
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
Lambda 表达式可以捕获外部变量,但这些变量必须是 有效最终变量(Effectively Final),即它们的值在 Lambda 表达式中不能被修改。
/**
* @Auth:TianMing
* @Description: 捕获变量
*/
public class LambdaExample {
public static void main(String[] args) {
int factor = 2;
Function<Integer, Integer> multiply = x -> x * factor;
System.out.println(multiply.apply(5)); // 10
// 如果 factor 被修改,会导致编译错误
// factor = 3;
}
}
Lambda 表达式在运行时通过动态代理实现,性能开销较小。它通常比匿名类更高效,因为 Lambda 表达式会编译为方法而不是类。
- 单方法接口:Lambda 表达式只能用于实现一个抽象方法的接口。
- 捕获变量:只能捕获有效最终变量。
- 可读性:如果逻辑复杂,Lambda 表达式可能会降低代码的可读性。
Lambda 表达式在实际开发中非常常用,尤其是在以下场景:
- Stream API:简化集合操作。
- 事件处理:简化事件监听器的实现。
- 函数式接口:实现自定义的函数式接口。
JButton button = new JButton("Click Me");
button.addActionListener(event -> System.out.println("Button clicked!"));
Lambda 表达式是 Java 8 的重要特性,它通过简化匿名类的实现,使得代码更简洁、更易读。在面试中,理解 Lambda 表达式的语法、函数式接口的关系以及应用场景是非常重要的。希望这些内容能帮助你在面试中自信地回答 Lambda 表达式相关的问题!如果还有其他疑问,可以继续提问。
说说你对内部类的理解
内部类是Java中一种特殊的类,它定义在其他类或方法中,并且可以访问外部类的成员,包括私有成员。定义在一个类内部的类。它允许类之间共享代码和数据,同时可以提高代码的模块化和封装性。
内部类的主要作用是实现更加灵活和封装的设计。需要注意的是,过度使用内部类会增加代码的复杂性,降低可读性和可维护性。因此,在使用内部类时要考虑其是否真正有必要,并且仔细进行设计和命名。
内部类是 Java 中一个非常重要的特性,它允许在一个类的内部定义另一个类。内部类在实际开发中有很多应用场景,比如事件处理、封装和模块化设计等。以下是一些常见的面试问题和答案,帮助你深入理解内部类。
public class OuterClass {
private int outerField = 10;
// 成员内部类
class InnerClass {
public void display() {
System.out.println("Outer field: " + outerField);
}
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();
}
}
内部类分为如下几种:
- **成员内部类:**定义在一个类的内部,并且不是静态的。成员内部类可以访问外部类的所有成员,包括私有成员。在创建内部类对象时,需要先创建外部类对象,然后通过外部类对象来创建内部类对象。
- **静态内部类:**定义在一个类的内部,使用
<font style="color:rgba(0, 0, 0, 0.9);">static</font>
关键字修饰的内部类。与成员内部类不同,静态内部类不能访问外部类的非静态成员,但可以访问外部类的静态成员。在创建静态内部类对象时,不需要先创建外部类对象,可以直接通过类名来创建。 - **局部内部类:**定义在一个方法或作用域块中的类,它的作用域被限定在方法或作用域块中。局部内部类可以访问外部方法或作用域块中的 final 变量和参数。
- **匿名内部类:**没有定义名称的内部类,通常用于创建实现某个接口或继承某个类的对象。匿名内部类会在定义时立即创建对象,因此通常用于简单的情况,而不用于复杂的类结构。
成员内部类是定义在类内部的类,可以直接访问外部类的成员变量和方法。
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class OuterClass {
private int outerField = 10;
class InnerClass {
public void display() {
System.out.println("Outer field: " + outerField);
}
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();
}
}
局部内部类定义在方法或构造函数中,只能在定义它的作用域内使用。
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class OuterClass {
public void createInnerClass() {
class LocalInnerClass {
public void display() {
System.out.println("Local Inner Class");
}
}
LocalInnerClass localInner = new LocalInnerClass();
localInner.display();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.createInnerClass();
}
}
匿名内部类没有类名,通常用于实现接口或继承类。它常用于简化代码,特别是在事件处理中。
import java.util.function.Consumer;
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class AnonymousInnerClassExample {
public static void main(String[] args) {
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println("Consumed: " + s);
}
};
consumer.accept("Hello, Anonymous Inner Class!");
}
}
静态内部类使用 <font style="color:rgba(0, 0, 0, 0.9);">static</font>
关键字修饰,它不能访问外部类的非静态成员变量和方法。
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class OuterClass {
private static int staticField = 20;
static class StaticInnerClass {
public void display() {
System.out.println("Static field: " + staticField);
}
}
public static void main(String[] args) {
OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
staticInner.display();
}
}
内部类的主要作用包括:
-
封装:内部类可以访问外部类的成员变量和方法,从而实现更紧密的封装。
-
模块化:内部类可以将相关代码组织在一起,提高代码的模块化。
-
事件处理:内部类常用于 GUI 编程中的事件处理。
-
简化代码:匿名内部类可以简化代码,特别是在实现接口时。
-
封装性:内部类可以访问外部类的私有成员。
-
模块化:内部类可以将相关代码组织在一起。
-
代码简化:匿名内部类可以简化代码。
-
复杂性:内部类的生命周期与外部类相关联,可能会导致内存泄漏。
-
性能开销:内部类会增加内存占用,因为每个内部类实例都包含对外部类实例的引用。
内部类的生命周期与外部类的实例相关联。非静态内部类的实例必须依附于外部类的实例,而静态内部类的实例可以独立存在。
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class OuterClass {
class InnerClass {
// 非静态内部类必须依附于外部类的实例
}
static class StaticInnerClass {
// 静态内部类可以独立存在
}
}
内部类在实际开发中非常常用,尤其是在以下场景:
- 事件处理:在 GUI 编程中,内部类常用于事件监听器的实现。
- 封装:内部类可以封装相关代码,提高代码的模块化。
- 简化代码:匿名内部类可以简化代码,特别是在实现接口时。
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class GUIExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Button Example");
JButton button = new JButton("Click Me");
// 使用匿名内部类实现事件监听器
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});
frame.add(button);
frame.setSize(300, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
内部类是 Java 中一个非常强大的特性,它允许在类内部定义另一个类,从而实现更紧密的封装和模块化设计。在面试中,理解内部类的基本概念、类型、优缺点以及应用场景是非常重要的。
说说你对懒汉模式和饿汉模式的理解
懒汉模式和饿汉模式都是单例模式的实现方式,用于确保一个类只有一个实例存在。
- **懒汉模式:**在首次使用时才进行对象的初始化,延迟加载实例。它可以避免不必要的资源消耗,但在多线程环境下需要考虑线程安全和同步开销。
- **饿汉模式:**在类加载时就进行对象的初始化,无论是否需要。它通过类加载机制保证线程安全性,而且获取实例的性能开销较小。但它没有延迟加载的特性,可能浪费一些资源。
选择懒汉模式还是饿汉模式取决于具体需求。如果需要延迟加载且对性能要求不高,可以选择懒汉模式。如果要通过类加载机制保证线程安全且对象创建成本较低,可以选择饿汉模式。也可以结合两种模式的优点,使用双重检查锁、静态内部类等方式实现单例模式,提高线程安全性和性能。
说说你对泛型的理解
泛型是Java中的一个特性,它允许我们在定义类、接口或方法时使用类型参数,以实现代码的通用性和安全性。泛型的目的是在编译时进行类型检查,并提供编译期间的类型安全。
泛型的理解包括以下几个方面:
**首先,**泛型提供了代码重用和通用性。通过使用泛型,我们可以编写可重用的代码,可以在不同的数据类型上执行相同的操作。这样,我们可以避免重复编写类似的代码,提高了开发效率。
**其次,**泛型强调类型安全。编译器可以在编译时进行类型检查,阻止不符合类型约束的操作。这样可以避免在运行时出现类型错误的可能,增加了程序的稳定性和可靠性。
**另外,**使用泛型可以避免大量的类型转换和强制类型转换操作。在使用泛型集合类时,不需要进行强制类型转换,可以直接获取正确的数据类型,提高了代码的可读性和维护性。
**此外,**泛型还可以在编译时进行类型检查,提前发现潜在的类型错误。这种类型检查是在编译时进行的,避免了一些常见的运行时类型异常,减少了错误的可能性。
**最后,**泛型可以增加代码的可读性和可维护性。通过使用泛型,我们可以明确指定数据类型,并在代码中表达清晰,使得其他开发人员更容易理解代码的意图和功能。
应粉丝要求,还得上点强度。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错
那参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class demo1 {
public static void main(String[] args) {
ArrayList<String> list=new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for(String s:list){
System.out.println(s);
}
}
}
//上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。
// 但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(666);
for (int i = 0; i < list.size(); i++) {
System.out.println((String)list.get(i));
}
}
上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。
那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。
使用泛型代码如下:
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public static void main(String[] args) {
ArrayList<String> list = new ArrayList();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//list.add(666);// 在编译阶段,编译器会报错
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。
3.总结
简单总结出泛型的几点好处
(1)统一数据类型,对于后续业务层中取出数据有很强的统一规范性,方便对数据的管理;
(2)把运行时期的问题提前到了编译期,避免了强转类型转换可能出现的异常,降低了程序出错的概率;
泛型的出现就是为了统一集合当中数据类型的
泛型的使用方法非常多,这里来简单说一下泛型类的使用;泛型类,就是把泛型定义在类上。
泛型类的使用场景:当一个类中,某个变量的数据不确定时,就可以定义带有泛型的类。
我们平常所用的ArrayList类,就是一个泛型类,我们看如下源码
ArrayList 源码上显示,在ArrayList类的后面,便是 泛型,定义了这样的泛型,就可以让使用者在创建ArrayList对象时自主定义要存放的数据类型。
这里的 E 可以理解成变量,它不是用来记录数据的,而是记录数据的类型的。可以写成很多字母,T,V,K都可以,通常这些字母都是英文单词的首字母,V表示 value,K表示 key,E表示 element,T表示 type;如果你想,自己练习的时候写成ABCDEFG都可以,但建议养成好习惯,用专业名词的首字母,便于理解。
尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。
泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思,文章后面部分会讲解示意。
自己实现集合
代码如下:
/**
* @Auth:TianMing
* @Description: 自定义泛型类
*/
public class MyArrayList<T> {
// 给出该数组的默认长度为10
Object[] obj = new Object[10];
// 定义一个指针,默认为0
int size;
// 写一个泛型类中添加元素的方法
public boolean add(T t){
// size默认为0,刚好指向数组的第一个位置,添加元素,将要添加的元素t赋值给到obj数组的第一个位置
obj[size] = t;
// size指针加一,指向下一个位置,下次元素添加到size指向的位置
size++;
// 添加完成并size加一之后,操作完成,返回成功true
return true;
}
// 写一个泛型类中取出元素的方法,index索引可以取出指定位置的元素
public T get(int index){
// 取出元素后,强转为我们泛型所指定的类型
return (T)obj[index];
}
}
这里打印出来的是 list 的内存地址,说明我们自定义的 泛型类没有问题。
其实 ArrayList 底层源码就是这样写的,这里我只是简单的写了两个方法,有兴趣的可以把删除方法和修改方法也写出来,动手测试一下。
我们什么时候会用到泛型方法呢?
通常情况下,当一个方法的形参不确定的情况下,我们会使用到泛型方法。
泛型方法其实与泛型类有着紧密的联系,通过上面我写的自定义泛型类不难看出,在泛型类中,所有方法都可以使用类上定义的泛型。
但是,泛型方法却可以脱离泛型类单独存在,泛型方法上定义的泛型只有本方法上可以使用,其他方法不可用。
格式
package tuling.edu;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @Auth:TianMing
* @Description: 集合工具类
*/
public class ListUtil {
private ListUtil() {
}
/*
参数一:集合
参数二: 最后要添加的元素
*/
public static <E> void addAll(ArrayList<E> list, E e1, E e2) {
list.add(e1);
list.add(e2);
}
}
package fangxing;
import java.util.ArrayList;
public class demo4 {
public static void main(String[] args) {
ArrayListlist=new ArrayList<>();
ListUtil.addAll(list,"zhangsan","lisi");
System.out.println(list);//[zhangsan, lisi]
}
}
添加很多元素
public static <E> void addAll(ArrayList<E> list, E ...e1) {
for (E e : e1) {
list.add(e);
}
}
泛型接口与泛型方法相似,当我们的接口中,参数类型不确定的时候,就可以使用泛型。
泛型接口的格式虽然简单,但这不是我们要学习的重点。
我们的重点是:如何使用一个带有泛型的接口?
通常情况下,我们有两种方式
方式一:实现类给出具体的类型。
方式二:实现类延续泛型,在创建对象时再指定泛型类型。
相比于方式一,方式二的扩展性更强。
Java中 List 的实现类 ArrayList 就是采用的第二种方式,延续泛型,我们看源码即可得知
别的不用看,只看我画红线的部分,ArrayList 实现了list接口,但后面还是泛型,延续了泛型,是方式二。
那么我再给各位演示一下方式一,如下我自己定义的一个泛型接口
/**
* @Auth:TianMing
* @Description: 定义一个泛型接口
*/
public interface MyList<E> {
// 定义一个方法做简单测试
public boolean add(E e);
}
//再定义一个类实现该接口,
// 定义MyArrayList类实现MyList接口,并在实现时就指定泛型类型
public class MyArrayList implements MyList<String> {
// 定义一个长度为十的默认数组
Object[] object = new Object[10];
// 定义一个size作为指针
int size;
@Override
public boolean add(String s) {
/**
* size初始化为零,刚好指向数组的第一个位置,添加第一个元素时,我们默认将元素添加到数组的第一个位置
*/
object[size] = s;
// size则合理可以作为指针,当添加第一个元素之后,size++,向后移动一位,下一次就会添加到第二个元素的位置,循环往复
size++;
return true;
}
}
可以看到,在实现类中重写add方法,方法的参数就已经确定,就是我们在实现它时指定的String类型。
然后我们写一个main方法测试是否成功
创建对象,添加元素,打印结果,运行发现成功
但这里是一个内存地址,因为我这里只是简单的定义了一个接口,在Java中ArrayList的源码上千行,里面定义了很多方法,我这里只做简单测试验证一下方式一是如何完成的,很多东西都没有写,大家明白即可。
编译器编译带类型说明的集合时会去掉类型信息
泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。
换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。
其实Java中的泛型本质是伪泛型
当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型, 如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型
当代码编译到class文件的时候,泛型就消失,叫泛型的擦除
看一个例子,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。
/**
* @Auth:TianMing
* @Description: 泛型擦除
*/
public class GenericType {
public static void main(String[] args) {
ArrayList arrayString = new ArrayList()< String >;
ArrayList arrayInteger = new ArrayList()< Integer >;
System.out.println(arrayString.getClass() == arrayInteger.getClass());// true
}
}
在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。
明明我们在 <> 中传入了两种不同的数据类型,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。
那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢?
答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(上界(<font style="color:rgba(0, 0, 0, 0.9);">T extends Something</font>
)或下界(<font style="color:rgba(0, 0, 0, 0.9);">T super Something</font>
))
假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问:
不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢?
换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢?
Java 是如何解决这个问题的?
其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。
可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。
当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;
当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。
在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。
进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。
举例如下:
/**
* @Auth:TianMing
* @Description: 擦除原理
*/
public class GenericType {
public static void main(String[] args) {
ArrayList arrayInteger = new ArrayList();
// 设置验票系统
arrayInteger.add(111);
// 观众进场,验票系统验票,门票会被收走(类型擦除)
Integer n = arrayInteger.get(0);
// 获取观众信息,编译器会进行强制类型转换
System.out.println(n);
}
}
擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令:
对原始方法 get() 的调用,返回的是 Object 类型;
将返回的 Object 类型强制转换为 Integer 类型;
代码如下:
Integer n = arrayInteger.get(0);// 这条代码底层如下:
//(1)get() 方法的返回值返回的是 Object 类型
Object object = arrayInteger.get(0);
//(2)编译器自动插入 Integer 的强制类型转换
Integer n = (Integer) object;
1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析);
2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。
- 泛型的继承
泛型不具备继承性,但是数据具备继承性
此时,泛型里面写的什么类型,那么就传递什么类型的数据
泛型不具备继承性举例
package tuling.edu;
import java.util.ArrayList;
/**
* @Auth:TianMing
* @Description: 泛型通配符
*/
public class demo5 {
public static void main(String[] args) {
/*
泛型不具备继承性,但是数据具备继承性
*/
ArrayList<Ye> list1=new ArrayList<>();
ArrayList<Fu> list2=new ArrayList<>();
ArrayList<Zi> list3=new ArrayList<>();
//调用method方法
method(list1);
//method(list2);//编译错误
//method(list3);//编译错误
}
/*
此时,泛型里面写的什么类型,那么就传递什么类型的数据
*/
public static void method(ArrayList<Ye> list){
}
}
class Ye{
}
class Fu extends Ye{
}
class Zi extends Fu{
}
数据具备继承性
//数据具备继承性
list1.add(new Ye());//添加爷爷的对象等
list1.add(new Fu());
list1.add(new Zi());
定义一个方法,形参是一个集合,但是集合中的数据类型不确定。
应用场景:
-
1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
-
2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符
-
泛型的通配符:
-
关键点:可以限定类型的范围。
测试类
package lx;
import java.util.ArrayList;
/**
* @Auth:TianMing
* @Description: 泛型测试
*/
public class demo1 {
/*
需求:
定义一个继承结构:
动物
| |
猫 狗
| | | |
波斯猫 狸花猫 泰迪 哈士奇
属性:名字,年龄
行为:吃东西
波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干
狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼
泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭
哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家
测试类中定义一个方法用于饲养动物
public static void keepPet(ArrayList list){
//遍历集合,调用动物的eat方法
}
要求1:该方法能养所有品种的猫,但是不能养狗
要求2:该方法能养所有品种的狗,但是不能养猫
要求3:该方法能养所有的动物,但是不能传递其他类型
*/
public static void main(String[] args) {
HuskyDog h = new HuskyDog("哈士奇", 1);
LihuaCat l = new LihuaCat("狸花猫", 2);
PersianCat p = new PersianCat("波斯猫", 3);
TeddyDog t = new TeddyDog("泰迪", 4);
ArrayList list1 = new ArrayList<>(); ArrayList list2 = new ArrayList<>();
// 向列表中添加一些猫的实例
list1.add(l);
list2.add(p);
//调用方法
keepPet1(list1);
keepPet1(list2);
System.out.println("-------------------------------------------");
ArrayList list3 = new ArrayList<>();
ArrayList list4 = new ArrayList<>();
// 向列表中添加一些狗的实例
list3.add(h);
list4.add(t);
//调用方法
keepPet2(list3);
keepPet2(list4);
System.out.println("-------------------------------------------");
list1.add(l);
list2.add(p);
list3.add(h);
list4.add(t);
keepPet3(list1);
keepPet3(list2);
keepPet3(list3);
keepPet3(list4);
}
/*
此时我们就可以使用泛型的通配符:
?也表示不确定的类型
他可以进行类型的限定
? extends E: 表示可以传递E或者E所有的子类类型
? super E:表示可以传递E或者E所有的父类类型
*/
// 要求1:该方法能养所有品种的猫,但是不能养狗
public static void keepPet1(ArrayList<? extends Cat> list) {
//遍历集合,调用动物的eat方法
for (Cat cat : list) {
cat.eat();
}
}
// 要求2:该方法能养所有品种的狗,但是不能养猫
public static void keepPet2(ArrayList<? extends Dog> list) {
//遍历集合,调用动物的eat方法
for (Dog dog : list) {
dog.eat();
}
}
// 要求3:该方法能养所有的动物,但是不能传递其他类型
public static void keepPet3(ArrayList<? extends Animal> list) {
//遍历集合,调用动物的eat方法
for (Animal animal : list) {
animal.eat();
}
}
}
Animal类
package lx;
public abstract class Animal {
private String name;
private int age;
public Animal() {
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "Animal{name = " + name + ", age = " + age + "}";
}
public abstract void eat();
}
cat类型
package lx;
public abstract class Cat extends Animal{
public Cat() {
}
public Cat(String name, int age) {
super(name, age);
}
}
Dog类
package lx;
public abstract class Dog extends Animal{
public Dog() {
}
public Dog(String name, int age) {
super(name, age);
}
}
哈士奇类
package lx;
public class HuskyDog extends Dog{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的哈士奇,正在吃骨头,边吃边拆家");
}
public HuskyDog() {
}
public HuskyDog(String name, int age) {
super(name, age);
}
}
狸花猫类
package lx;
public class LihuaCat extends Cat {
@Override
public void eat() {
System.out.println("一只叫做" + getName() + "的," + getAge() + "岁的狸花猫,正在吃鱼");
}
public LihuaCat() {
}
public LihuaCat(String name, int age) {
super(name, age);
}
}
波斯猫类
package lx;
public class PersianCat extends Cat{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的波斯猫,正在吃小饼干");
}
public PersianCat() {
}
public PersianCat(String name, int age) {
super(name, age);
}
}
泰迪猫类
package lx;
public class TeddyDog extends Dog{
@Override
public void eat() {
System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁泰迪,正在吃骨头,边吃边蹭");
}
public TeddyDog() {
}
public TeddyDog(String name, int age) {
super(name, age);
}
}
说说你对设计模式的理解
设计模式是一套经过验证的、被广泛应用于软件开发中的解决特定问题的重复利用的方案集合。它们是在软件开发领域诸多经验的基础上总结出来的,是具有普适性、可重用性和可扩展性的解决方案。
设计模式通过抽象、封装、继承、多态等特性帮助我们设计出高质量、易扩展、易重构的代码,遵循面向对象的设计原则,如单一职责、开闭原则、依赖倒置、里氏替换等,从而提高代码的可维护性、可测试性和可读性。
设计模式的优点在于它们已经被广泛验证,可以避免一些常见的软件开发问题,同时也提供了一种标准化的方案来解决这些问题。使用设计模式可以提高代码的复用性,减少代码的重复编写,增加代码的灵活性和可扩展性。设计模式还能降低项目的风险,提高系统的稳定性。
不过,设计模式不是万能的,对于简单的问题,可能会使代码变得过于复杂,甚至导致反效果。
在使用设计模式时,需要根据具体的问题需求和实际情况来选择合适的模式,避免滥用模式,并保持代码的简洁、清晰和可读性。
谈谈你对Java序列化的理解
- 序列化:将 Java 对象转换为字节流(如保存到文件或通过网络传输)。
- 反序列化:将字节流还原为 Java 对象。
序列化的核心是让对象能够在非 Java 环境(如文件、数据库、网络)中存储或传输。
Java的序列化是指将Java对象转换为字节流的过程,可以将这些字节流保存到文件中或通过网络传输。反序列化则是指将字节流恢复成对象的过程。
序列化的主要目的是实现对象的持久化存储和传输,让对象可以在不同的计算机或不同的时间点被重建和使用。通过序列化,可以将对象的状态以字节的形式保存下来,并且在需要的时候进行恢复,从而实现了对象的跨平台传输和持久化存储。
在Java中,要使一个类可序列化,需要满足以下条件:
- 实现java.io.Serializable接口,该接口是一个标记接口,没有任何方法。
- 所有的非静态、非瞬态的字段都可以被序列化。
使用Java的序列化机制,可以通过ObjectOutputStream将对象转换为字节流并写入文件或网络流中。反之,通过ObjectInputStream可以从字节流中读取数据并还原为对象。
需要注意的是,在进行序列化和反序列化时,对象的类和字段的定义必须保持一致,否则可能会导致序列化版本不匹配或字段丢失的问题。
应粉丝要求,补充更多追问
面试官通常会从以下几个方面 继续追问:
- 序列化和反序列化的基本概念。
- 序列化和反序列化的应用场景。
- 序列化和反序列化需要注意的细节(如
<font style="color:rgba(0, 0, 0, 0.9);">transient</font>
、<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
等)。 - 序列化和反序列化的安全性问题。
- 序列化和反序列化的性能优化。
Java 提供了 <font style="color:rgba(0, 0, 0, 0.9);">Serializable</font>
接口,用于标记需要序列化的类。
/**
* @Auth:TianMing
* @Description: 基本应用
*/
import java.io.*;
public class SerializationExample implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
private String name;
private transient int age; // transient 关键字表示该字段不参与序列化
// 构造方法、getter 和 setter 略
public static void main(String[] args) {
SerializationExample obj = new SerializationExample();
obj.setName("张三");
obj.setAge(25);
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"))) {
oos.writeObject(obj);
System.out.println("对象已序列化");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.ser"))) {
SerializationExample newObj = (SerializationExample) ois.readObject();
System.out.println("对象已反序列化: " + newObj.getName());
// 注意:transient 字段会被还原为默认值(这里是 0)
System.out.println("年龄: " + newObj.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
序列化底层原理:ObjectStreamClass类中可以看到writeObjectMethod属性,值来自对象writeObject方法
反序列化底层原理:ObjectStreamClass类中可以看到readObjectMethod属性,值来自对象readObject方法
自行编写readObject()函数,用于对象的反序列化构造,从而提供约束性。可以自定义反序列化对象的校验信息
-
<font style="color:rgba(0, 0, 0, 0.9);">transient</font>
修饰的字段不会被序列化。 -
反序列化时,
<font style="color:rgba(0, 0, 0, 0.9);">transient</font>
字段会被还原为默认值(如<font style="color:rgba(0, 0, 0, 0.9);">int</font>
为<font style="color:rgba(0, 0, 0, 0.9);">0</font>
,<font style="color:rgba(0, 0, 0, 0.9);">String</font>
为<font style="color:rgba(0, 0, 0, 0.9);">null</font>
)。 -
<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
是序列化版本号,用于标识类的版本。 -
如果类的结构发生变化(如新增字段或修改字段类型),
<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
不匹配会导致反序列化失败。 -
最好显式声明
<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
,否则编译器会自动生成一个,但容易导致版本不一致。 -
静态字段不会被序列化,因为它们属于类而不是对象。
序列化和反序列化存在安全风险,尤其是反序列化时:
- 反序列化恶意数据可能导致代码执行或数据泄露。
- 面试官可能会问你如何避免这些问题。
- 自定义
**<font style="color:rgba(0, 0, 0, 0.9);">readObject</font>**
和**<font style="color:rgba(0, 0, 0, 0.9);">writeObject</font>**
方法:- 在类中覆盖
<font style="color:rgba(0, 0, 0, 0.9);">readObject</font>
和<font style="color:rgba(0, 0, 0, 0.9);">writeObject</font>
方法,验证数据的合法性。
- 在类中覆盖
/**
* @Auth:TianMing
* @Description: 安全性
*/
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 验证数据
if (name == null) {
throw new InvalidObjectException("name 不能为空");
}
}
- 使用安全的序列化框架:
- 避免使用 Java 默认的序列化机制,改用更安全的框架(如 Protobuf、Kryo)。
- 避免敏感数据参与序列化:
- 使用
<font style="color:rgba(0, 0, 0, 0.9);">transient</font>
修饰敏感字段,确保它们不被序列化。
- 使用
Java 的默认序列化机制效率较低,可以通过以下方式优化:
- 使用
**<font style="color:rgba(0, 0, 0, 0.9);">Externalizable</font>**
接口:<font style="color:rgba(0, 0, 0, 0.9);">Externalizable</font>
是<font style="color:rgba(0, 0, 0, 0.9);">Serializable</font>
的子接口,允许自定义序列化逻辑。- 通过实现
<font style="color:rgba(0, 0, 0, 0.9);">writeExternal</font>
和<font style="color:rgba(0, 0, 0, 0.9);">readExternal</font>
方法,可以减少序列化数据的大小。
- 使用高效的序列化库:
- Protobuf:Google 开发的高效序列化框架,支持跨语言。
- Kryo:轻量级、高性能的序列化框架。
- FST:快速、紧凑的序列化库。
-
答案:序列化可以将对象转换为字节流,方便存储或传输。例如,将对象保存到文件、通过网络发送对象、在分布式系统中传递对象。
-
答案:
<font style="color:rgba(0, 0, 0, 0.9);">transient</font>
表示字段不参与序列化,<font style="color:rgba(0, 0, 0, 0.9);">static</font>
字段属于类而不是对象,因此也不参与序列化。 -
答案:
<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
用于标识类的版本。如果类的结构发生变化,<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
不匹配会导致反序列化失败。 -
答案:可以使用
<font style="color:rgba(0, 0, 0, 0.9);">Externalizable</font>
接口自定义序列化逻辑,或者使用高效的序列化库(如 Protobuf、Kryo)。 -
答案:反序列化恶意数据可能导致代码执行或数据泄露。
-
如何防止反序列化破坏单例模式的对象呢? 可以通过重写
**<font style="color:rgba(0, 0, 0, 0.9);">readObject</font>**
方法 或者 readResolve 方法、使用安全的序列化框架、避免敏感数据参与序列化来解决。 -
**<font style="color:rgba(0, 0, 0, 0.9);">readObject</font>**
方法:适合用来阻止反序列化或验证对象状态。 -
**<font style="color:rgba(0, 0, 0, 0.9);">readResolve()</font>**
方法:适合用来确保反序列化后返回的是单例的唯一实例。
两种方法的区别
-
**<font style="color:rgba(0, 0, 0, 0.9);">readObject</font>**
方法:- 是
<font style="color:rgba(0, 0, 0, 0.9);">ObjectInputStream</font>
的方法,用于自定义反序列化逻辑。 - 适合用来验证对象的状态或直接阻止反序列化。
- 是
-
**<font style="color:rgba(0, 0, 0, 0.9);">readResolve()</font>**
方法:- 是一个特殊的钩子方法,用于在反序列化完成后返回一个对象。
- 适合用来确保返回的是单例的唯一实例。
-
序列化:将对象转换为字节流。
-
反序列化:将字节流还原为对象。
-
注意事项:
<font style="color:rgba(0, 0, 0, 0.9);">transient</font>
、<font style="color:rgba(0, 0, 0, 0.9);">serialVersionUID</font>
、静态字段。 -
安全性:避免反序列化恶意数据,使用安全的序列化框架。
-
性能优化:使用
<font style="color:rgba(0, 0, 0, 0.9);">Externalizable</font>
或高效的序列化库。
掌握这些知识点后,你可以在面试中自信地回答序列化和反序列化相关的问题!如果还有其他疑问,可以继续提问。
import java.io.*;
/**
* @Auth:TianMing
* @Description: readObject
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
// 私有构造方法,防止外部实例化
private Singleton() {
// 防止反射破坏单例
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
// 提供获取实例的方法
public static Singleton getInstance() {
return INSTANCE;
}
// 重写 readObject 方法,阻止反序列化
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
throw new IOException("Singleton cannot be deserialized");
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
// 尝试序列化和反序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"))) {
oos.writeObject(instance1);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"))) {
Singleton instance2 = (Singleton) ois.readObject();
System.out.println(instance1 == instance2); // false,因为 readObject 抛出了异常
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
import java.io.*;
/**
* @Auth:TianMing
* @Description: readResolve
*/
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
// 私有构造方法,防止外部实例化
private Singleton() {
// 防止反射破坏单例
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
// 提供获取实例的方法
public static Singleton getInstance() {
return INSTANCE;
}
// 使用 readResolve 方法,确保返回的是单例的唯一实例
private Object readResolve() {
return INSTANCE;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
// 尝试序列化和反序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"))) {
oos.writeObject(instance1);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"))) {
Singleton instance2 = (Singleton) ois.readObject();
System.out.println(instance1 == instance2); // true,因为 readResolve 返回了 INSTANCE
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
谈谈你对反射的理解
反射是 Java 中一个非常强大的特性,它允许程序在运行时检查和操作类、接口、字段和方法。反射在面试中经常被问到,因为它涉及到 Java 的核心机制,并且在很多框架中都有广泛的应用。以下是一些常见的面试问题和答案,帮助你深入理解反射。当然这个部分也加入了点有强度的追问。
反射是一种允许程序在运行时检查和操作类、接口、字段和方法的机制。通过反射,可以动态地获取类的信息、创建对象、调用方法、访问字段等。
反射的主要用途包括:
-
动态加载类:在运行时加载类,而不需要在编译时知道类的具体名称。
-
访问私有成员:反射可以访问私有字段和方法,这在某些情况下非常有用。
-
实现框架:很多框架(如 Spring、Hibernate)都广泛使用反射来实现动态功能。
-
测试:反射可以用于测试私有方法。
-
灵活性:反射允许在运行时动态地操作类和对象,提供了极大的灵活性。
-
动态性:可以在运行时加载和操作类,而不需要在编译时知道类的具体信息。
-
性能开销:反射操作通常比直接操作慢,因为它需要进行大量的类型检查和安全验证。
-
安全性问题:反射可以破坏封装性,访问私有字段和方法,这可能会导致安全问题。
-
复杂性:反射代码通常比较复杂,难以维护。
通过 <font style="color:rgba(0, 0, 0, 0.9);">Class</font>
类可以获取类的信息。以下是一个示例:
import java.lang.reflect.Method;
/**
* @Auth:TianMing
* @Description: 基本应用
*/
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取类的 Class 对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 获取类名
System.out.println("类名: " + clazz.getName());
// 获取所有公共方法
Method[] methods = clazz.getMethods();
System.out.println("公共方法:");
for (Method method : methods) {
System.out.println(method.getName());
}
// 获取所有声明的方法(包括私有方法)
Method[] declaredMethods = clazz.getDeclaredMethods();
System.out.println("声明的方法:");
for (Method method : declaredMethods) {
System.out.println(method.getName());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
可以通过 <font style="color:rgba(0, 0, 0, 0.9);">Class</font>
对象的 <font style="color:rgba(0, 0, 0, 0.9);">newInstance()</font>
方法或 <font style="color:rgba(0, 0, 0, 0.9);">Constructor</font>
类来创建对象:
import java.lang.reflect.Constructor;
/**
* @Auth:TianMing
* @Description: 创建对象
*/
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取类的 Class 对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 使用 newInstance() 创建对象
Object obj1 = clazz.newInstance();
System.out.println("使用 newInstance() 创建的对象: " + obj1);
// 使用 Constructor 创建对象
Constructor<?> constructor = clazz.getConstructor();
Object obj2 = constructor.newInstance();
System.out.println("使用 Constructor 创建的对象: " + obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
可以通过 <font style="color:rgba(0, 0, 0, 0.9);">Method</font>
类来调用方法:
import java.lang.reflect.Method;
/**
* @Auth:TianMing
* @Description: 方法调用
*/
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取类的 Class 对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 创建对象
Object obj = clazz.newInstance();
// 获取 add 方法
Method addMethod = clazz.getMethod("add", Object.class);
// 调用 add 方法
boolean result = (boolean) addMethod.invoke(obj, "Hello, Reflection!");
System.out.println("add 方法返回值: " + result);
// 获取 size 方法
Method sizeMethod = clazz.getMethod("size");
int size = (int) sizeMethod.invoke(obj);
System.out.println("size 方法返回值: " + size);
} catch (Exception e) {
e.printStackTrace();
}
}
}
可以通过 <font style="color:rgba(0, 0, 0, 0.9);">Field</font>
类来访问私有字段: 注意使用 elementDataField.setAccessible(true);打破封装
import java.lang.reflect.Field;
/**
* @Auth:TianMing
* @Description: 访问私有字段
*/
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取类的 Class 对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 创建对象
Object obj = clazz.newInstance();
// 获取私有字段(例如,ArrayList 的 elementData 字段)
Field elementDataField = clazz.getDeclaredField("elementData");
// 设置可访问(打破封装)
elementDataField.setAccessible(true);
// 获取字段值
Object elementData = elementDataField.get(obj);
System.out.println("elementData 字段值: " + elementData);
// 设置字段值
elementDataField.set(obj, new Object[10]);
System.out.println("修改后的 elementData 字段值: " + elementDataField.get(obj));
} catch (Exception e) {
e.printStackTrace();
}
}
}
注解(Annotations)可以为反射提供元数据。通过反射,可以读取类、方法或字段上的注解信息。例如:
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* @Auth:TianMing
* @Description: 访问注解
*/
public class ReflectionExample {
public static void main(String[] args) {
try {
// 获取类的 Class 对象
Class<?> clazz = Class.forName("com.example.MyClass");
// 获取所有方法
Method[] methods = clazz.getMethods();
// 检查每个方法上的注解
for (Method method : methods) {
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println("方法 " + method.getName() + " 上的注解: " + annotation);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
反射操作通常比直接操作慢,因为它需要进行大量的类型检查和安全验证。以下是一些优化建议:
- 减少反射的使用:尽量避免在性能敏感的代码中使用反射。
- 缓存反射数据:如果需要多次使用反射,可以缓存
<font style="color:rgba(0, 0, 0, 0.9);">Class</font>
、<font style="color:rgba(0, 0, 0, 0.9);">Method</font>
等对象。 - 使用动态代理:在某些情况下,动态代理可以替代反射,提高性能。
反射可以破坏封装性,访问私有字段和方法,这可能会导致安全问题。以下是一些解决方法:
- 1,限制反射的使用:尽量避免使用反射访问私有成员。
- 2,使用安全检查:在使用反射时,进行必要的安全检查。
- 3,使用访问控制:在某些情况下,可以使用
<font style="color:rgba(0, 0, 0, 0.9);">AccessController</font>
来限制反射的访问权限。
实例1:通过接口暴露功能,而不是直接使用反射
/**
* @Auth:TianMing
* @Description: 示例:通过接口暴露功能,而不是直接使用反射
* 依赖注入框架(如 Spring)来管理对象的创建和依赖关系,而不是直接使用反射。
*/
public interface Service {
void execute();
}
public class ServiceImpl implements Service {
@Override
public void execute() {
// 实现逻辑
}
}
// 使用接口调用,而不是反射
Service service = new ServiceImpl();
service.execute();
实例2:在反射调用之前,检查调用者的权限。通过 <font style="color:rgba(0, 0, 0, 0.9);">SecurityManager</font>
或 <font style="color:rgba(0, 0, 0, 0.9);">AccessController</font>
来限制反射的访问权限
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Permission;
import java.security.Permissions;
import java.security.Policy;
import java.security.Principal;
import java.security.ProtectionDomain;
/**
* @Auth:TianMing
* @Description: 权限检查:在反射调用之前,检查调用者的权限
*/
public class SecureReflectionExample {
private static final String SECRET = "superSecretValue";
public static void main(String[] args) {
try {
// 创建一个限制权限的 AccessControlContext
Permissions perms = new Permissions();
perms.add(new RuntimePermission("accessDeclaredMembers"));
AccessControlContext acc = new AccessControlContext(new ProtectionDomain[]{new ProtectionDomain(null, perms)});
// 使用 AccessController 进行权限检查
AccessController.doPrivileged((java.security.PrivilegedExceptionAction<Object>) () -> {
Field field = SecureReflectionExample.class.getDeclaredField("SECRET");
AccessibleObject.setAccessible(new AccessibleObject[]{field}, true);
System.out.println("Secret value: " + field.get(null));
return null;
}, acc);
} catch (Exception e) {
e.printStackTrace();
}
}
}
实例3:通过 <font style="color:rgba(0, 0, 0, 0.9);">SecurityManager</font>
或 <font style="color:rgba(0, 0, 0, 0.9);">Policy</font>
文件限制反射的访问权限
import java.lang.reflect.Field;
import java.security.Permission;
/**
* @Auth:TianMing
* @Description: 通过 SecurityManager 或 Policy 文件限制反射的访问权限
*/
public class SecurityManagerExample {
private static final String SECRET = "superSecretValue";
public static void main(String[] args) {
// 启用 SecurityManager
System.setSecurityManager(new SecurityManager() {
@Override
public void checkPermission(Permission perm) {
if (perm.getName().equals("accessDeclaredMembers")) {
throw new SecurityException("Access denied");
}
}
});
try {
//通过权限检查,禁止未授权的代码访问私有字段或方法
Field field = SecurityManagerExample.class.getDeclaredField("SECRET");
field.setAccessible(true);
System.out.println("Secret value: " + field.get(null));
} catch (Exception e) {
System.out.println("Security exception: " + e.getMessage());
}
}
}
建议
- 代码审查:定期审查代码,确保没有滥用反射。
- 使用安全的库:选择经过安全审计的库,避免使用不安全的反射工具。
- 最小化权限:确保应用程序运行在最小权限的环境中,避免授予不必要的权限。
Spring 的依赖注入功能依赖于反射来动态地创建对象并设置属性值。
假设我们有一个 <font style="color:rgba(0, 0, 0, 0.9);">UserService</font>
类,它依赖于 <font style="color:rgba(0, 0, 0, 0.9);">UserRepository</font>
:
public class UserService {
private UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 其他方法
}
在 Spring 中,通过反射可以动态地创建 <font style="color:rgba(0, 0, 0, 0.9);">UserService</font>
的实例,并将 <font style="color:rgba(0, 0, 0, 0.9);">UserRepository</font>
注入到 <font style="color:rgba(0, 0, 0, 0.9);">UserService</font>
中:
// 假设这是 Spring 容器的一部分
public class SpringContainer {
public <T> T createBean(Class<T> clazz) throws Exception {
// 使用反射创建对象
Constructor<?> constructor = clazz.getDeclaredConstructor(UserRepository.class);
T bean = (T) constructor.newInstance(new UserRepository());
return bean;
}
public static void main(String[] args) {
SpringContainer container = new SpringContainer();
try {
UserService userService = container.createBean(UserService.class);
// 使用 userService
} catch (Exception e) {
e.printStackTrace();
}
}
}
Spring 的 IoC 容器使用反射来管理 Bean 的生命周期,包括创建、初始化和销毁。
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService(userRepository());
}
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
}
在 Spring 中,<font style="color:rgba(0, 0, 0, 0.9);">@Bean</font>
注解的配置类会被解析,通过反射调用方法来创建 Bean。
Spring 的 AOP 功能也依赖于反射来动态地代理方法,实现方法的拦截和增强。
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
}
在 Spring 中,AOP 使用反射来获取方法的签名,并在方法执行前后插入逻辑。
Spring 使用反射来创建动态代理,从而实现 AOP 和事务管理。
public class ProxyFactory {
public static Object createProxy(Object target, InvocationHandler handler) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler
);
}
}
在 Spring 中,事务管理器会使用反射来代理方法,从而在方法执行前后添加事务逻辑。
Spring 使用反射来创建 Bean:
public class BeanFactory {
public <T> T createBean(Class<T> clazz) throws Exception {
Constructor<?> constructor = clazz.getDeclaredConstructor();
return (T) constructor.newInstance();
}
}
Spring 使用反射来设置 Bean 的属性:
public class BeanFactory {
public <T> void setProperty(T bean, String propertyName, Object value) throws Exception {
Field field = bean.getClass().getDeclaredField(propertyName);
field.setAccessible(true);
field.set(bean, value);
}
}
Spring 使用反射来调用初始化和销毁方法:
public class BeanFactory {
public <T> void invokeInitMethod(T bean, String methodName) throws Exception {
Method method = bean.getClass().getDeclaredMethod(methodName);
method.setAccessible(true);
method.invoke(bean);
}
}
Spring 在使用反射时会进行一些优化,以减少性能开销:
- 缓存反射数据:Spring 会缓存
<font style="color:rgba(0, 0, 0, 0.9);">Class</font>
、<font style="color:rgba(0, 0, 0, 0.9);">Method</font>
和<font style="color:rgba(0, 0, 0, 0.9);">Field</font>
对象,避免重复获取。 - 使用 CGLIB 或 Javassist:在某些情况下,Spring 会使用字节码生成库(如 CGLIB 或 Javassist)来代替反射,提高性能。
反射是 Java 中一个非常强大的特性,但也需要谨慎使用。在面试中,理解反射的基本概念、使用场景、优缺点以及安全性和性能问题是关键。希望这些内容能帮助你在面试中自信地回答反射相关的问题!如果还有其他疑问,可以继续提问。
谈谈自定义注解的场景及实现
自定义注解是Java语言的一个强大特性,可以为代码添加元数据信息,提供额外配置或标记。它适用于多种场景。
- **配置和扩展框架:**通过自定义注解,可以为框架提供配置参数或进行扩展。例如,Spring框架中的@Autowired注解用于自动装配依赖项,@RequestMapping注解用于映射请求到控制器方法。
- **运行时检查:**自定义注解可在运行时对代码进行检查,并进行相应处理。例如,JUnit框架的@Test注解标记测试方法,在运行测试时会自动识别并执行这些方法。
- **规范约束:**自定义注解用于规范代码风格和约束。例如,Java代码规范检查工具Checkstyle可使用自定义注解标记违规行为。
实现自定义注解的步骤如下:
- 使用@interface关键字定义注解。
- 可在注解中定义属性,并指定默认值。
- 根据需求,可添加元注解来控制注解的使用方式。
- 在代码中使用自定义注解。
- 使用反射机制解析注解信息。
通过合理运用自定义注解,可提高代码的可读性、可维护性和可扩展性。
过滤器和拦截器有什么区别?
过滤器(Filter)和拦截器(Interceptor)都是用于解决项目中与请求处理、响应管理和业务逻辑控制相关问题的工具,但它们之间存在明显的区别。接下来,我们将详细探讨这两者的不同之处。
首先,我们先来看一下二者在 Spring Boot 项目中的具体实现,这对后续理解二者的区别有很大的帮助。
过滤器可以使用 Servlet 3.0 提供的 @WebFilter 注解,配置过滤的 URL 规则,然后再实现 Filter 接口,重写接口中的 doFilter 方法,具体实现代码如下:
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("执行过滤器 init() 方法。");
}
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
System.out.println("开始执行过滤器 doFilter() 方法。");
// 请求放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("结束执行过滤器 doFilter() 方法。");
}
@Override
public void destroy() {
System.out.println("执行过滤器 destroy() 方法。");
}
}
其中:
- **void init(FilterConfig filterConfig):**容器启动(初始化 Filter)时会被调用,整个程序运行期只会被调用一次。用于实现 Filter 对象的初始化。
- **void doFilter(ServletRequest request, ServletResponse response,FilterChain chain):**具体的过滤功能实现代码,通过此方法对请求进行过滤处理,其中 FilterChain 参数是用来调用下一个过滤器或执行下一个流程。
- ** void destroy():**用于 Filter 销毁前完成相关资源的回收工作。 b) 实现拦截器 拦截器的实现分为两步,第一步,创建一个普通的拦截器,实现 HandlerInterceptor 接口,并重写接口中的相关方法;第二步,将上一步创建的拦截器加入到 Spring Boot 的配置文件中。
实现 HandlerInterceptor 接口并重写 preHandle/postHandle/afterCompletion 方法,具体实现代码如下:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("执行拦截器 preHandle() 方法。");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("执行拦截器 postHandle() 方法。");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("执行拦截器 afterCompletion() 方法。");
}
}
其中:
- **boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle):**在请求方法执行前被调用,也就是调用目标方法之前被调用。比如我们在操作数据之前先要验证用户的登录信息,就可以在此方法中实现,如果验证成功则返回 true,继续执行数据操作业务;否则就返回 false,后续操作数据的业务就不会被执行了。
- **void postHandle(HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView):**调用请求方法之后执行,但它会在 DispatcherServlet 进行渲染视图之前被执行。
- **void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex):**会在整个请求结束之后再执行,也就是在 DispatcherServlet 渲染了对应的视图之后再执行。
最后,我们再将上面的拦截器注入到项目配置文件中,并设置相应拦截规则,具体实现代码如下:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
// 注入拦截器
@Autowired
private TestInterceptor testInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(testInterceptor) // 添加拦截器
.addPathPatterns("/**"); // 拦截所有地址
}
}
了解了二者的使用之后,接下来我们来看二者的区别。
过滤器和拦截器的区别主要体现在以下 5 点:
- 来源不同;
- 触发时机不同;
- 实现不同;
- 支持的项目类型不同;
- 使用的场景不同。
过滤器来自于 Servlet,而拦截器来自于 Spring 框架,从上面代码中我们也可以看出,过滤器在实现时导入的是 Servlet 相关的包,如下图所示:
而拦截器在实现时,导入的是 Spring 相关的包,如下图所示:
请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller),如下图所示:
所以过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。
过滤器是基于方法回调实现的,我们在上面实现过滤器的时候就会发现,当我们要执行下一个过滤器或下一个流程时,需要调用 FilterChain 对象的 doFilter 方法进行回调执行,如下图所示:
而拦截器是基于动态代理(底层是反射)实现的,它的实现如下图所示:
代理调用的效果如下图所示:
过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中。
因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务。 而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。
- 拦截器是基于java反射机制的,而过滤器是基于函数回调。
- 拦截器不依赖于Servlet容器,而过滤器依赖于servlet容器。
- 拦截器只能对action请求起作用,而过滤器可以对几乎所以的请求起作用。
- 拦截器可以访问action上下文,值栈里的对象,而过滤器不能。
- 在Action的生命周期周,拦截器可以被多次调用,而过滤器只能在容器初始化的时候被调用一次。
重载和重写的区别
在Java中,重载和重写是两个不同的概念,它们都用于实现多态性,但是具体的实现方式和作用不同。
- 重载:
- 重载是指在同一个类中,可以有多个方法名相同但参数类型、参数个数或参数顺序不同的方法。
- 重载方法的返回类型可以相同也可以不同,但不足以区分重载方法。
- 重载的作用是增加方法的灵活性和可读性,让同一个方法名可以对不同情况进行处理。
- 重写:
- 重写是指在子类中,可以对父类的方法进行重写。
- 重写方法必须与被重写方法拥有相同的方法名、返回值类型和参数列表,但是可以更改访问修饰符、抛出的异常类型和方法体等。
- 重写的作用是实现多态性,通过父类引用调用子类对象的方法,实现对同一方法名的不同实现。
区别:
- 重载是指在同一个类中对相同方法名的多次定义,而重写是指在继承关系中对父类方法的重新定义。
- 重载的方法签名(方法名、参数类型、个数和顺序)必须不同,而重写的方法签名必须相同。
- 重载的目的是提供更加灵活的方法调用方式,重写的目的是实现多态性。
总之,重载和重写都是Java中多态性的体现,但是它们的实现方式和作用有所不同,需要根据具体的需求进行选择。
金额到底用Long还是Bigdecimal
金额到底用Long还是Bigdecimal, 一直是一个有争议的话题:
我来说说我的观点, 大家也可以评论区说说你的观点:
首先float和double肯定是排除的,因为它们内部使用科学计数法,转换二进制的时候有可能出现无限小数位的问题
那么大家就会选择Long和BigDecimal, Long类型在存储时(比如保留2位小数点)x100, 取出来/100。
其实本质都是一样的,都是避免使用浮点数进行表达,只是Long属于隐式设定小数点,BigDecimal属于显示设定小数点。
那么这2种到底怎么选择呢?
我的建议是: 在代码层面用**BigDecimal ,**数据库层面可视情况定
首先long性能更好:
- 整数类型(如 long)通常在计算机硬件上的性能更好,因为它们的操作可以在硬件层面上更有效地执行。
- BigDecimal 需要额外的空间和计算开销。
阿里的java开发手册是推荐用分存储的,希望大家都能用Long存储分,照顾一下彼此的开发体验。
“8.【强制】任何货币金额,均以最小货币单位且为整型类型进行存储。”
但是对于一些金融系统要求小数点位数要求比较多, 比如精确后六位, 如果每次存x1000000 那long类型的内存开销也荡然无存了也会降低可读性即易用性, 不如用Decimal。
所以数据库在需求阶段能确定小数点位数可以用long, 如果位数不确定,或者要求太精准可以用DECIMAL
阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别
SPI全称为Service Provider Interface,是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。
简单来说SPI是一种非常优秀的设计思想,它的核心就是解耦、方便扩展。
ServiceLoader是Java提供的一种简单的SPI机制的实现,Java的SPI实现约定了以下两件事:
- 文件必须放在
META-INF/services/
目录底下 - 文件名必须为接口的全限定名,内容为接口实现的全限定名
这样就能够通过ServiceLoader加载到文件中接口的实现。
第一步,需要一个接口以及他的实现类
public interface LoadBalance {
}
public class RandomLoadBalance implements LoadBalance{
}
第二步,在META-INF/services/
目录创建一个文件名LoadBalance全限定名的文件,文件内容为RandomLoadBalance的全限定名
测试类:
public class ServiceLoaderDemo {
public static void main(String[] args) {
ServiceLoader<LoadBalance> loadBalanceServiceLoader = ServiceLoader.load(LoadBalance.class);
Iterator<LoadBalance> iterator = loadBalanceServiceLoader.iterator();
while (iterator.hasNext()) {
LoadBalance loadBalance = iterator.next();
System.out.println("获取到负载均衡策略:" + loadBalance);
}
}
}
测试结果:
此时就成功获取到了实现。
在实际的框架设计中,上面这段测试代码其实是框架作者写到框架内部的,而对于框架的使用者来说,要想自定义LoadBalance实现,嵌入到框架,仅仅只需要写接口的实现和spi文件即可。
如下是ServiceLoader中一段核心代码
首先获取一个fullName,其实就是META-INF/services/接口的全限定名
然后通过ClassLoader获取到资源,其实就是接口的全限定名文件对应的资源,然后交给parse
方法解析资源
parse
方法其实就是通过IO流读取文件的内容,这样就可以获取到接口的实现的全限定名
再后面其实就是通过反射实例化对象,这里就不展示了。
所以其实不难发现ServiceLoader实现原理比较简单,总结起来就是通过IO流读取META-INF/services/接口的全限定名
文件的内容,然后反射实例化对象。
由于Java的SPI机制实现的比较简单,所以他也有一些缺点。
第一点就是浪费资源,虽然例子中只有一个实现类,但是实际情况下可能会有很多实现类,而Java的SPI会一股脑全进行实例化,但是这些实现了不一定都用得着,所以就会白白浪费资源。
第二点就是无法对区分具体的实现,也就是这么多实现类,到底该用哪个实现呢?如果要判断具体使用哪个,只能依靠接口本身的设计,比如接口可以设计为一个策略接口,又或者接口可以设计带有优先级的,但是不论怎样设计,框架作者都得写代码进行判断。
所以总得来说就是ServiceLoader无法做到按需加载或者按需获取某个具体的实现。
虽然说ServiceLoader可能有些缺点,但是还是有使用场景的,比如说:
- 不需要选择具体的实现,每个被加载的实现都需要被用到
- 虽然需要选择具体的实现,但是可以通过对接口的设计来解决
Spring我们都不陌生,他也提供了一种SPI的实现SpringFactoriesLoader。
Spring的SPI机制的约定如下:
- 配置文件必须在
META-INF/
目录下,文件名必须为spring.factories - 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。
所以也可以看出,Spring的SPI机制跟Java的不论是文件名还是内容约定都不一样。
在META-INF/
目录下创建spring.factories文件,LoadBalance为键,RandomLoadBalance为值
测试:
public class SpringFactoriesLoaderDemo {
public static void main(String[] args) {
List<LoadBalance> loadBalances = SpringFactoriesLoader.loadFactories(LoadBalance.class, MyEnableAutoConfiguration.class.getClassLoader());
for (LoadBalance loadBalance : loadBalances) {
System.out.println("获取到LoadBalance对象:" + loadBalance);
}
}
}
运行结果:
成功获取到了实现对象。
如下是SpringFactoriesLoader中一段核心代码
其实从这可以看出,跟Java实现的差不多,只不过读的是META-INF/
目录下spring.factories文件内容,然后解析出来键值对。
Spring的SPI机制在内部使用的非常多,尤其在SpringBoot中大量使用,SpringBoot启动过程中很多扩展点都是通过SPI机制来实现的,这里我举两个例子
在SpringBoot3.0之前的版本,自动装配是通过SpringFactoriesLoader来加载的。
但是SpringBoot3.0之后不再使用SpringFactoriesLoader,而是Spring重新从META-INF/spring/
目录下的org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中读取了。
至于如何读取的,其实猜也能猜到就跟上面SPI机制读取的方式大概差不多,就是文件路径和名称不一样。
PropertySourceLoader是用来解析application配置文件的,它是一个接口
SpringBoot默认提供了 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader两个实现,就是对应properties和yaml文件格式的解析。
SpringBoot在加载PropertySourceLoader时就用了SPI机制
首先Spring的SPI机制对Java的SPI机制对进行了一些简化,Java的SPI每个接口都需要对应的文件,而Spring的SPI机制只需要一个spring.factories文件。
其次是内容,Java的SPI机制文件内容必须为接口的实现类,而Spring的SPI并不要求键值对必须有什么关系,更加灵活。
第三点就是Spring的SPI机制提供了获取类限定名的方法loadFactoryNames
,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射。
但是Spring的SPI也同样没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计。
所以不知道你有没有发现,PropertySourceLoader它其实就是一个策略接口,注释也有说,所以当你的配置文件是properties格式的时候,他可以找到解析properties格式的PropertiesPropertySourceLoader对象来解析配置文件。
ExtensionLoader是dubbo的SPI机制的实现类。每一个接口都会有一个自己的ExtensionLoader实例对象,这点跟Java的SPI机制是一样的。
同样地,Dubbo的SPI机制也做了以下几点约定:
- 接口必须要加@SPI注解
- 配置文件可以放在
META-INF/services/
、META-INF/dubbo/internal/
、META-INF/dubbo/
、META-INF/dubbo/external/
这四个目录底下,文件名也是接口的全限定名 - 内容为键值对,键为短名称(可以理解为spring中Bean的名称),值为实现类的全限定名
首先在LoadBalance接口上@SPI注解
@SPI
public interface LoadBalance {
}
然后,修改一下Java的SPI机制测试时配置文件内容,改为键值对,因为Dubbo的SPI机制也可以从META-INF/services/
目录下读取文件,所以这里就没重写文件
random=com.sanyou.spi.demo.RandomLoadBalance
测试类:
public class ExtensionLoaderDemo {
public static void main(String[] args) {
ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
LoadBalance loadBalance = extensionLoader.getExtension("random");
System.out.println("获取到random键对应的实现类对象:" + loadBalance);
}
}
通过ExtensionLoader的getExtension
方法,传入短名称,这样就可以精确地找到短名称对的实现类。
所以从这可以看出Dubbo的SPI机制解决了前面提到的无法获取指定实现类的问题。
测试结果:
dubbo的SPI机制除了解决了无法获取指定实现类的问题,还提供了很多额外的功能,这些功能在dubbo内部用的非常多,接下来就来详细讲讲。
自适应,自适应扩展类的含义是说,基于参数,在运行时动态选择到具体的目标类,然后执行。
每个接口有且只能有一个自适应类,通过ExtensionLoader的getAdaptiveExtension
方法就可以获取到这个类的对象,这个对象可以根据运行时具体的参数找到目标实现类对象,然后调用目标对象的方法。
举个例子,假设上面的LoadBalance有个自适应对象,那么获取到这个自适应对象之后,如果在运行期间传入了random
这个key,那么这个自适应对象就会找到random
这个key对应的实现类,调用那个实现类的方法,如果动态传入了其它的key,就路由到其它的实现类。
自适应类有两种方式产生,第一种就是自己指定,在接口的实现类上加@Adaptive注解,那么这个这个实现类就是自适应实现类。
@Adaptive
public class RandomLoadBalance implements LoadBalance{
}
除了自己代码指定,还有一种就是dubbo会根据一些条件帮你动态生成一个自适应类,生成过程比较复杂,这里就不展开了。
自适应机制在Dubbo中用的非常多,而且很多都是自动生成的,如果你不知道Dubbo的自适应机制,你在读源码的时候可能都不知道为什么代码可以走到那里。。
一提到IOC和AOP,立马想到的都是Spring,但是IOC和AOP并不是Spring特有的概念,Dubbo也实现IOC和AOP的功能,但是是一个轻量级的。
2.1、依赖注入
Dubbo依赖注入是通过setter注入的方式,注入的对象默认就是上面提到的自适应的对象,在Spring环境下可以注入Spring Bean。
public class RoundRobinLoadBalance implements LoadBalance {
private LoadBalance loadBalance;
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
如上代码,RoundRobinLoadBalance中有一个setLoadBalance方法,参数LoadBalance,在创建RoundRobinLoadBalance的时候,在非Spring环境底下,Dubbo就会找到LoadBalance自适应对象然后通过反射注入。
这种方式在Dubbo中也很常见,比如如下的一个场景
RegistryProtocol中会注入一个Protocol,其实这个注入的Protocol就是一个自适应对象。
2.2、接口回调
Dubbo也提供了一些类似于Spring的一些接口的回调功能,比如说,如果你的类实现了Lifecycle接口,那么创建或者销毁的时候就会回调以下几个方法
在dubbo3.x的某个版本之后,dubbo提供了更多接口回调,比如说ExtensionPostProcessor、ExtensionAccessorAware,命名跟Spring的非常相似,作用也差不多。
2.3、自动包装
自动包装其实就是aop的功能实现,对目标对象进行代理,并且这个aop功能在默认情况下就是开启的。
在Dubbo中SPI接口的实现中,有一种特殊的类,被称为Wrapper类,这个类的作用就是来实现AOP的。
判断Wrapper类的唯一标准就是这个类中必须要有这么一个构造参数,这个构造方法的参数只有一个,并且参数类型就是接口的类型,如下代码:
public class RoundRobinLoadBalance implements LoadBalance {
private final LoadBalance loadBalance;
public RoundRobinLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
此时RoundRobinLoadBalance就是一个Wrapper类。
当通过random
获取RandomLoadBalance目标对象时,那么默认情况下就会对RandomLoadBalance进行包装,真正获取到的其实是RoundRobinLoadBalance对象,RoundRobinLoadBalance内部引用的对象是RandomLoadBalance。
测试一下
在配置文件中加入
roundrobin=com.sanyou.spi.demo.RoundRobinLoadBalance
测试结果
从结果可以看出,虽然指定了random
,但是实际获取到的是RoundRobinLoadBalance,而RoundRobinLoadBalance内部引用了RandomLoadBalance。
如果有很多的包装类,那么就会形成一个责任链条,一个套一个。
所以dubbo的aop跟spring的aop实现是不一样的,spring的aop底层是基于动态代理来的,而dubbo的aop其实算是静态代理,dubbo会帮你自动组装这个代理,形成一条责任链。
到这其实我们已经知道,dubbo的spi接口的实现类已经有两种类型了:
- 自适应类
- Wrapper类
除了这两种类型,其实还有一种,叫做默认类,就是@SPI
注解的值对应的实现类,比如
@SPI("random")
public interface LoadBalance {
}
此时random
这个key对应的实现类就是默认实现,通过getDefaultExtension
这个方法就可以获取到默认实现对象。
所谓的自动激活,就是根据你的入参,动态地选择一批实现类返回给你。
自动激活的实现类上需要加上Activate
注解,这里就又学习了一种实现类的分类。
@Activate
public interface RandomLoadBalance {
}
此时RandomLoadBalance就属于可以被自动激活的类。
获取自动激活类的方法是getActivateExtension
,所以根据这个方法的入参,可以动态选择一批实现类。
自动激活这个机制在Dubbo一个核心的使用场景就是Filter过滤器链中。
Filter是dubbo中的一个扩展点,可以在请求发起前或者是响应获取之后就行拦截,作用有点像Spring MVC中的HandlerInterceptor。
Filter的一些实现类
如上Filter有很多实现,所以为了能够区分Filter的实现是作用于provider的还是consumer端,所以就可以用自动激活的机制来根据入参来动态选择一批Filter实现。
比如说ConsumerContextFilter这个Filter就作用于Consumer端。
ConsumerContextFilter
通过以上分析可以看出,实现SPI机制的核心原理就是通过IO流读取指定文件的内容,然后解析,最后加入一些自己的特性。
最后总的来说,Java的SPI实现的比较简单,并没有什么其它功能;Spring得益于自身的ioc和aop的功能,所以也没有实现太复杂的SPI机制,仅仅是对Java做了一点简化和优化;但是dubbo的SPI机制为了满足自身框架的使用要求,实现的功能就比较多,不仅将ioc和aop的功能集成到SPI机制中,还提供注入自适应和自动激活等功能。
静态内部类与非静态内部类有什么区别
在Java中,静态内部类和非静态内部类都是一种嵌套在其他类中的内部类。它们之间有以下几点区别:
- **实例化方式:**静态内部类可以直接通过外部类名来实例化,而非静态内部类必须要通过外部类的实例来实例化。
- **对外部类的引用:**静态内部类不持有对外部类实例的引用,而非静态内部类则会持有对外部类实例的引用。这意味着在静态内部类中不能直接访问外部类的非静态成员(方法或字段),而非静态内部类可以。
- **生命周期:**静态内部类的生命周期与外部类相互独立,即使外部类实例被销毁,静态内部类仍然存在。非静态内部类的生命周期与外部类实例绑定,只有在外部类实例存在时才能创建非静态内部类的实例。
- **访问权限:**静态内部类对外部类的访问权限与其他类一样,根据访问修饰符而定。非静态内部类可以访问外部类的所有成员,包括私有成员。
BIO、NIO、AIO有什么区别
他们三者都是Java中常用的I/O模型,我们从以下三个维度进行对比:
- 阻塞与非阻塞:
- BIO是阻塞式I/O模型,线程会一直被阻塞等待操作完成。
- NIO是非阻塞式I/O模型,线程可以去做其他任务,当I/O操作完成时得到通知。
- AIO也是非阻塞式I/O模型,不需要用户线程关注I/O事件,由操作系统通过回调机制处理。
- 缓冲区:
- BIO使用传统的字节流和字符流,需要为输入输出流分别创建缓冲区。
- NIO引入了基于通道和缓冲区的I/O方式,使用一个缓冲区完成数据读写操作。
- AIO则不需要缓冲区,使用异步回调方式进行操作。
- 线程模型:
- BIO采用一个线程处理一个请求方式,面对高并发时线程数量急剧增加,容易导致系统崩溃。
- NIO采用多路复用器来监听多个客户端请求,使用一个线程处理,减少线程数量,提高系统性能。
- AIO依靠操作系统完成I/O操作,不需要额外的线程池或多路复用器。
综上所述,BIO、NIO、AIO的区别主要在于阻塞与非阻塞、缓冲区和线程模型等方面。根据具体应用场景选择合适的I/O模型可以提高程序的性能和可扩展性。
JDK动态代理与CGLIB实现的区别
JDK动态代理和CGLIB是Java中常用的两种代理技术,它们在实现原理和使用方式上有一些区别。
- JDK动态代理是基于接口的代理技术,要求目标类必须实现一个或多个接口。它使用java.lang.reflect**.Proxy类**和java.lang.reflect.InvocationHandler接口来生成代理类和处理代理方法的调用。在运行时,JDK动态代理会动态生成一个代理类,该代理类实现了目标接口,并在方法调用前后插入额外的代码(即代理逻辑)。然而,JDK动态代理只能代理接口,无法代理普通的类。
- CGLIB是基**于继承的代理技术,可以代理普通的类,不需要目标类实现接口。**它使用字节码生成库,在运行时通过生成目标类的子类来实现代理。CGLIB通过继承目标类创建一个子类,并重写目标方法,以在方法调用前后插入额外的代码(即代理逻辑)。但是,由于继承关系,CGLIB无法代理被标记为final的方法。
总的来说,JDK动态代理适用于基于接口的代理需求,而CGLIB适用于代理普通类的需求。选择使用哪种代理方式取决于具体的需求。如果目标类已经实现了接口且需要基于接口进行代理,可以选择JDK动态代理。而如果目标类没有实现接口,或者需要代理普通类的方法,可以选择CGLIB。
final,finally,finalize的区别
在Java中,final、finally和finalize是三个不同的关键字,它们具有不同的作用和用法。
- final:
- final是一个修饰符,可以用于修饰类、方法和变量。
- 用于修饰类时,表示该类不能被继承,即为最终类。
- 用于修饰方法时,表示该方法不能被子类重写。
- 用于修饰变量时,表示该变量是一个常量,其值不能被修改。
- final是一个修饰符,可以用于修饰类、方法和变量。
- finally:
- finally是一个关键字,用于定义一个代码块,通常与try-catch结构一起使用。
- finally块中的代码无论是否抛出异常,都会被执行。
- finally块通常用于释放资源、关闭连接或执行必要的清理操作。
- finalize:
- finalize是Object类中的一个方法,被用于垃圾回收机制。
- finalize方法在对象被垃圾回收之前被调用,用于进行资源释放或其他清理操作。
- 通常情况下,我们不需要显式地调用finalize方法,而是交由垃圾回收器自动调用。
总结:
- final是修饰符,用于限定类、方法和变量的性质。
- finally是一个关键字,用于定义一个代码块,在异常处理中用于确保特定代码无论如何都会被执行。
- finalize是一个Object类中的方法,用于对象的垃圾回收前的清理操作。
请注意,finalize方法已被废弃,不推荐使用。在现代Java中,可以使用try-with-resources语句或手动释放资源的方式来替代finalize方法的功能。
什么是值传递和引用传递
值传递和引用传递是程序中常用的参数传递方式。
- 值传递是指在函数调用时,将实际参数的值复制一份传递给形式参数,在函数内对形式参数的修改不会影响到实际参数的值。这意味着函数内部对形参的改变不会影响到函数外部的变量。在值传递中,对形参的修改只作用于函数内部。
- 引用传递是指在函数调用时,将实际参数的引用或地址传递给形式参数,函数内部对形参的修改会影响到实际参数。这意味着函数内部对形参的改变会影响到函数外部的变量。在引用传递中,对形参的修改会直接作用于函数外部的变量。
需要注意的是,引用传递实际上传递的是对象的引用或地址,并不是对象本身。对于基本数据类型(如整数、浮点数等),虽然也可以通过指针进行引用传递,但由于基本数据类型的值通常较小,因此通常采用值传递的方式。
深拷贝和浅拷贝区别
浅拷贝是指复制对象时,只复制对象本身,而不复制对象引用的其他对象。浅拷贝后,新对象和原对象共享引用的其他对象。
深拷贝是指复制对象时,不仅复制对象本身,还递归地复制对象引用的其他对象。深拷贝后,新对象和原对象完全独立。
深拷贝和浅拷贝是在进行对象的复制时常用的两种方式,它们有以下区别:
- 拷贝的程度:
- 浅拷贝只拷贝对象的引用,不创建新的对象实例。拷贝后的对象与原始对象共享同一份数据,对其中一个对象的修改会影响到另一个对象。
- 深拷贝创建一个全新的对象实例,并将原始对象的所有属性值复制到新对象中。拷贝后的对象与原始对象是独立的,对任一对象的修改不会影响另一个对象。
- 对象引用:
- 浅拷贝只复制对象引用,新旧对象仍然指向同一块内存空间,修改其中一个对象的属性会影响另一个对象。
- 深拷贝会复制对象本身以及对象引用指向的其他对象,所有对象的引用都将指向全新的内存空间。
- 性能开销:
- 浅拷贝的性能开销较小,因为仅复制对象的引用。
- 深拷贝的性能开销较大,因为需要创建新的对象实例并复制所有属性。
通常情况下,当我们需要复制一个对象并希望新对象与原始对象互不影响时,应使用深拷贝。而浅拷贝更适用于那些对象结构较简单、不包含引用类型成员变量或不需要独立修改的情况。
/**
* @Auth:TianMing
* @Description:浅拷贝
*/
public class ShallowCopyExample implements Cloneable {
private int id;
private String name;
private Address address;
public ShallowCopyExample(int id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) {
Address address = new Address("123 Main St", "City");
ShallowCopyExample original = new ShallowCopyExample(1, "John", address);
try {
ShallowCopyExample copy = (ShallowCopyExample) original.clone();
System.out.println("Original Address: " + original.address);
System.out.println("Copy Address: " + copy.address);
// 修改地址
copy.address.setStreet("456 New St");
System.out.println("After modification:");
System.out.println("Original Address: " + original.address);
System.out.println("Copy Address: " + copy.address);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Address {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address{" + "street='" + street + '\'' + ", city='" + city + '\'' + '}';
}
}
输出:
复制
Original Address: Address{street='123 Main St', city='City'}
Copy Address: Address{street='123 Main St', city='City'}
After modification:
Original Address: Address{street='456 New St', city='City'}
Copy Address: Address{street='456 New St', city='City'}
/**
* @Auth:TianMing
* @Description:深拷贝
*/
public class DeepCopyExample implements Cloneable {
private int id;
private String name;
private Address address;
public DeepCopyExample(int id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
DeepCopyExample copy = (DeepCopyExample) super.clone();
copy.address = (Address) this.address.clone(); // 深拷贝地址
return copy;
}
public static void main(String[] args) {
Address address = new Address("123 Main St", "City");
DeepCopyExample original = new DeepCopyExample(1, "John", address);
try {
DeepCopyExample copy = (DeepCopyExample) original.clone();
System.out.println("Original Address: " + original.address);
System.out.println("Copy Address: " + copy.address);
// 修改地址
copy.address.setStreet("456 New St");
System.out.println("After modification:");
System.out.println("Original Address: " + original.address);
System.out.println("Copy Address: " + copy.address);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Address implements Cloneable {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address{" + "street='" + street + '\'' + ", city='" + city + '\'' + '}';
}
}
输出:
复制
Original Address: Address{street='123 Main St', city='City'}
Copy Address: Address{street='123 Main St', city='City'}
After modification:
Original Address: Address{street='123 Main St', city='City'}
Copy Address: Address{street='456 New St', city='City'}
-
浅拷贝:只复制对象本身,引用的其他对象不复制,新对象和原对象共享引用的其他对象。
-
深拷贝:递归地复制对象及其引用的所有对象,新对象和原对象完全独立。
-
浅拷贝:使用
<font style="color:rgba(0, 0, 0, 0.9);">Object.clone()</font>
方法,或者通过构造方法或手动复制字段 -
深拷贝:手动实现深拷贝,或者使用序列化。
- 手动实现:手动实现深拷贝是最直接的方法,通过逐个复制对象的字段来实现。这种方法需要对对象的内部结构有详细的了解,并且需要递归地复制所有引用的对象。。
- 序列化:序列化是另一种实现深拷贝的方法。通过将对象序列化为字节流,然后再反序列化为新对象,可以实现深拷贝。这种方法适用于所有实现了
<font style="color:rgba(0, 0, 0, 0.9);">Serializable</font>
接口的类。 - 第三方库:一些第三方库(如 Apache Commons Lang)提供了深拷贝的功能,可以简化代码。
/**
* @Auth:TianMing
* @Description: 手动实现深拷贝
*/
public class DeepCopyExample implements Cloneable {
private int id;
private String name;
private Address address;
public DeepCopyExample(int id, String name, Address address) {
this.id = id;
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// 手动实现深拷贝
DeepCopyExample copy = (DeepCopyExample) super.clone();
copy.address = (Address) this.address.clone(); // 深拷贝地址
return copy;
}
public static void main(String[] args) {
Address address = new Address("123 Main St", "City");
DeepCopyExample original = new DeepCopyExample(1, "John", address);
try {
DeepCopyExample copy = (DeepCopyExample) original.clone();
System.out.println("Original Address: " + original.address);
System.out.println("Copy Address: " + copy.address);
// 修改地址
copy.address.setStreet("456 New St");
System.out.println("After modification:");
System.out.println("Original Address: " + original.address);
System.out.println("Copy Address: " + copy.address);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Address implements Cloneable {
private String street;
private String city;
public Address(String street, String city) {
this.street = street;
this.city = city;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
@Override
public String toString() {
return "Address{" + "street='" + street + '\'' + ", city='" + city + '\'' + '}';
}
}