关于java中堆内存与栈内存的详细分析
一、概述
在Java中,内存分为两种,一种是栈内存,另一种就是堆内存。
二、堆内存
1、什么是堆内存?
堆内存是Java内存中的一种,它的作用是用于存储Java中的对象和数组,当我们new一个对象或者创建一个数组的时候,就会在堆内存中开辟一段空间给它,用于存放。
2、堆内存的特点是什么?
第一点:堆其实可以类似的看做是管道,或者说是平时去排队买票的的情况差不多,所以堆内存的特点就是:先进先出,后进后出,也就是你先排队,好,你先买票。
第二点:堆可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,但缺点是,由于要在运行时动态分配内存,存取速度较慢。
3、new对象在堆中如何分配?
由Java虚拟机的自动垃圾回收器来管理。
三、栈内存
1、什么是栈内存
栈内存是Java的另一种内存,主要是用来执行程序用的,比如:基本类型的变量和对象的引用变量。
2、栈内存的特点
第一点:栈内存就好像一个矿泉水瓶,像里面放入东西,那么先放入的沉入底部,所以它的特点是:先进后出,后进先出
第二点:存取速度比堆要快,仅次于寄存器,栈数据可以共享,但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性
3、栈内存分配机制
栈内存可以称为一级缓存,由垃圾回收器自动回收
4、数据共享
例子:
int a = 3;
int b = 3;
第一步处理:
1.编译器先处理int a = 3;
2.创建变量a的引用
3.在栈中查找是否有3这个值
4.没有找到,将3存放,a指向3
第二步处理:
1.处理b=3
2.创建变量b的引用
3.找到,直接赋值
第三步改变:
接下来
a = 4;
同上方法
a的值改变,a指向4,b的值是不会发生改变的。
PS:如果是两个对象的话,那就不一样了,对象指向的是同一个引用,一个发生改变,另一个也会发生改变。
四、栈和堆的区别
JVM是基于堆栈的虚拟机,JVM为每个新创建的线程都分配一个堆栈。也就是说,对于一个Java程序来说,它的运行就是通过对堆栈的操作来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操作:以帧为单位的压栈和出栈操作。
差异点:
1.堆内存用来存放由new创建的对象和数组。
2.栈内存用来存放方法或者局部变量等。
3.堆是先进先出,后进后出。
4.栈是后进先出,先进后出。
相同点:
1.都是属于Java内存的一种。
2.系统都会自动去回收它,但是对于堆内存一般开发人员会自动回收它。
基础知识
1、 JVM、JRE、JDK、J2EE等相关概念
JVM(Java Virtual Machine)
java虚拟机,用于保证java的跨平台的特性。
JRE(Java Runtime Environment)
java运行环境,包括jvm + java的核心类库。
JDK(Java Development Kit)
java开发工具,包括jre + 开发工具(比如javac、jconsole)。
J2EE(企业版)
是为开发企业环境下的应用程序提供的一套解决方案。该技术体系中包含的技术如 Servlet、Jsp等,主要针对于Web应用程序开发。
J2SE(标准版)
是为开发普通桌面和商务应用程序提供的解决方案。该技术体系是其他两者的基础,可以完成一些桌面应用程序的开发。比如Java版的扫雷。
J2ME(小型版)
是为开发电子消费产品和嵌入式设备提供的解决方案。该技术体系主要应用于小型电子消费类产品,如手机中的应用程序等,但已被淘汰,手机开发都用android和object-c了。
Java语言是跨平台,jvm不是跨平台的,jvm实现java程序跨平台。
我们利用JDK(调用JAVA API)开发了属于我们自己的JAVA程序后,通过JDK中的编译程序(javac)将我们的文本java文件编译成JAVA字节码,在JRE上运行这些JAVA字节码,JVM解析这些字节码,映射到CPU指令集或OS的系统调用。
2、 classpath的作用是什么?
classpath是配置class文件所在的目录,用于指定类搜索路径,JVM就是通过它来寻找java类的class类文件的。
3、 变量有什么用?为什么要定义变量?
? 变量的作用:用来存储数据。
? 为什么要定义变量:用来不断的存放同一类型的数据,并可以重复使用 。
4、 标示符命名规则
? 由数字(0-9),大小写英文字母,以及_和$组成。
? 不能以数字开头。
? 不能使用关键字来自定义命名。
5、 基本类型和包装类型
? 基本数据类型(4类8种):
整数类型:byte、short、int、long
浮点数类型:float、double
字符类型:char
布尔类型:boolean
? 包装类型
整数类型:Byte、Short、Integer、Long
浮点数类型:Float、Double
字符类型:Character
布尔类型:Boolean
byte
基本类型:byte 二进制位数:8(1字节)
包装类:java.lang.Byte
最小值:Byte.MIN_VALUE=-128
最大值:Byte.MAX_VALUE=127
short
基本类型:short 二进制位数:16(2字节)
包装类:java.lang.Short
最小值:Short.MIN_VALUE=-32768
最大值:Short.MAX_VALUE=32767
int
基本类型:int 二进制位数:32(4字节)
包装类:java.lang.Integer
最小值:Integer.MIN_VALUE=-2147483648
最大值:Integer.MAX_VALUE=2147483647
long
基本类型:long 二进制位数:64(8字节)
包装类:java.lang.Long
最小值:Long.MIN_VALUE=-9223372036854775808
最大值:Long.MAX_VALUE=9223372036854775807
float
基本类型:float 二进制位数:32(4字节)
包装类:java.lang.Float
最小值:Float.MIN_VALUE=1.4E-45
最大值:Float.MAX_VALUE=3.4028235E38
double
基本类型:double 二进制位数:64(8字节)
包装类:java.lang.Double
最小值:Double.MIN_VALUE=4.9E-324
最大值:Double.MAX_VALUE=1.7976931348623157E308
char
基本类型:char 二进制位数:16(2字节)
包装类:java.lang.Character
最小值:Character.MIN_VALUE=0
最大值:Character.MAX_VALUE=65535
boolean
基本类型:boolean二进制位数:8(1字节)
包装类:java.lang.Boolean
boolean与Boolean的区别:
1.boolean是基础数据类型,而Boolean是一个包装类;
2.boolean一般存在于—栈空间中,而Boolean对象存在堆空间中;
3.boolean有true和false俩种值,Boolean除了true和false外,还有null。
6、 基本类型和引用类型
Java提供了两类数据类型,一种是基本类型(原始类型),一种是引用类型。参数传递的时候,基本数据类型是值传递,引用数据类型是地址传递(没有重写equals()情况下)。大多数Java类库都实现了比较对象内容的equals()方法。
数据类型图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r96qMEfS-1645175624408)(/Users/laowang/Desktop/求职/20180607221353561.png)]
引用类型常见的有:String,StringBuffer,ArrayList,HashSet,HashMap等。
7、 类型转换
精度从高到低 double ? float ? long ? int ? short(或者char) ? byte
? 自动类型转换:将一个低精度—>高精度
? 强制类型转换:将一个高精度—>低精度(精度会下降)
8、 Integer缓存机制
java在申请一个大于-128小于127的数时,其实是从cache中直接取出来用的,如果不在这个范围则是new了一个Integer对象。Character、Byte、Short、Long原理也是一样。Boolean也有缓存,Boolean.TRUE或者Boolean.FALSE获取的是同一个对象。Float和Double没有缓存。
9、 什么是函数
定义:
函数就是定义在类中的具有特定功能的一段独立小程序。
特点:
? 定义函数可以将功能代码进行封装
? 提高了代码的复用性
? 函数只有被调用才会被执行
? 函数没有具体返回值,返回值类型用关键字void表示, 函数中的return语句如果在最后一行可以省略不写。
10、 什么是数组
数组是同一种数据类型的集合。可以自动给数组中的元素从0开始编号,方便操作这些元素。
11、 &和&&的区别
? &&会出现短路,逻辑与运算符,如果可以通过第一个表达式判断出整个表达式的结果,则不继续后面表达式的运算,只能操作boolean类型数据。
? &不会出现短路,位运算符,将整个表达式都运算。既可以操作boolean数据还可以操作数字。位运算符包括:与(&)、非(~)、或(|)、异或(^)。
12、 字符编码的类型和演变
ASCII
ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符 [1] 。
GB2312、GBK、GB18030
是国家标准GB2312基础上扩容后兼容GB2312的标准。GBK的文字编码是用双字节来表示的,即不论中、英文字符均使用二个字节来表示,为了区分中文,将其最高位都设定成1。GBK包含全部中文字符,是国家编码,通用性比UTF8差,不过UTF8占用的数据库比GBK大。GB2312 支持简体中文的码,GBK 支持简体中文及繁体中文,GB18030支持各个民族语言。
Unicode
Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储,互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。Unicode 是一本很厚的字典,她将全世界所有的字符定义在一个集合里。
UTF-8、UTF-16
Unicode TransformationFormat-8bit,允许含BOM,但通常不含BOM。是用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文使用24位(三个字节)来编码。UTF-8包含全世界所有国家需要用到的字符,是国际编码,通用性强。UTF-8编码的文字可以在各国支持UTF8字符集的浏览器上显示。如果是UTF8编码,则在外国人的英文IE上也能显示中文,他们无需下载IE的中文语言支持包。
演变:
-
中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。
-
汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
-
中国的各个少数民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。
-
每个国家都像中国一样,把自己的语言编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 UNICODE ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。所以只要电脑上有 UNICODE 这种编码系统,无论是全球哪种文字,只需要保存文件的时候,保存成 UNICODE 编码就可以被其他电脑正常解释。
-
UNICODE 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。
-
GBK、GB2312等与UTF8之间都必须通过Unicode编码才能相互转换。
13、 Java特点
- 跨平台性
跨平台性是指软件可以不受计算机硬件和操作系统的约束而在任意计算机环境下正常运行。 Java自带的虚拟机很好地实现了跨平台性。 Java源程序代码经过编译后生成二进制的字节码是与平台无关的,但是可被Java虚拟机识别的一种机器码指令,Java虚拟机提供了一个字节码到底层硬件平台及操作系统的屏障,使得Java语言具备跨平台性。
- 面向对象
面向对象技术使得应用程序的开发变得简单易用,节省代码。Java是一种面向对象的语言,也继承了面向对象的诸多好处,如代码扩展、代码复用等。
- 简单
Java语言是一种相当简洁的“面向对象”程序设计语言。Java语言省略了C++语言中所有的难以理解、容易混淆的特性,例如头文件、指针、结构、单元、运算符重载、虚拟基础类等。它更加严谨、简洁。
- 安全性
Java编译时要进行Java语言和语义的检查,保证每个变量对应一个相应的值,编译后生成Java类。运行时Java类需要类加载器载入,并经由字节码校验器校验之后才可以运行。 Java类在网络上使用时,对它的权限进行了设置,保证了被访问用户的安全性。
14、 Java5与6、7、8、9、10的特性
java5
-
自动装箱与拆箱
-
枚举(常用来设计单例模式)
-
静态导入
-
可变参数
-
内省
java 6
-
Web服务元数据
-
脚本语言支持
-
JTable的排序和过滤
-
更简单,更强大的JAX-WS
-
轻量级Http Server
-
嵌入式数据库 Derby
java 7
-
switch中可以使用字串了
-
运用List tempList = new ArrayList<>(); 即泛型实例化类型自动推断
-
语法上支持集合,而不一定是数组
-
新增一些取环境信息的工具方法
-
Boolean类型反转,空指针安全,参与位运算
-
两个char间的equals
-
安全的加减乘除
-
map集合支持并发请求,且可以写成 Map map = {name:“xxx”,age:18};
java 8
-
允许在接口中有默认方法实现
-
Lambda表达式
-
函数式接口
-
方法和构造函数引用
-
Lambda的范围
-
内置函数式接口
-
Streams
-
Parallel Streams
-
Map
-
时间日期API
-
Annotations
java 9
-
Jigsaw 项目;模块化源码
-
简化进程API
-
轻量级 JSON API
-
钱和货币的API
-
改善锁争用机制
-
代码分段缓存
-
智能Java编译, 第二阶段
-
HTTP 2.0客户端
-
Kulla计划: Java的REPL实现
Java10
-
本地变量类型推断
-
统一JDK仓库
-
垃圾回收器接口
-
G1的并行Full GC
-
应用程序类数据共享
-
ThreadLocal握手机制
15、 内存结构(java7&java8)
- 程序计数器(又称PC寄存器)
是线程私有区域,记录着当前线程所执行的字节码的行号指示器(即偏移量),是内存中唯一一块没有规定任何OutOfMemoryError(内存溢出)情况的区域,为什么?因为我们不需要操作该区域,该区域是内部维护的。
- 虚拟机栈
是线程私有区域,用于执行方法时存储局部变量(入栈),当方法执行完毕后,所占空间会自动释放(出栈)。
- 本地方法栈
是为本地的native方法服务的,其他的都和虚拟机栈一样。
- Java堆
是Java虚拟机所管理的内存中最大的内存块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,存放对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区(java8称为元数据空间)
也是线程共享的内存区城,它用于存储已被虚拟机加载的类信息(比如类的版本,字段,方法,接口)、常量、静态变量、即时编译器编译后的代码等数据。
16、 Java反射原理(重点)
Java反射是可以在运行时,通过一个类的class对象来获取类的属性、方法、构造函数、父类、接口等内部信息。
反射机制主要提供了以下功能:
-
在运行时判断任意一个对象所属的类。
-
在运行时构造任意一个类的对象。
-
在运行时判断任意一个类所具有的成员变量和方法。
-
在运行时调用任意一个对象的方法。
-
生成动态代理。
反射的原理:
JAVA语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类中的方法以及属性等信息。
17、 switch(expr)在各个java版本的用法
在Java 5以前,switch(expr)中,expr只能是byte、short、char、int。从Java 5开始,引入了枚举类型,expr也可以是enum类型,从Java 7开始,expr还可以是字符串(String)。
18、 能否自己定义String
不行,因为jvm在加载类的时候会执行双亲委派。
19、 运行时类型识别(RTTI)
RTTI(Run Time Type Identification,运行时类型识别),它假定我们在编译时已经知道了所有的类型信息。Java RTTI这个说法是源自《Thinking in Java》,此前RTTI一直是C++的概念。
RTTI和反射的区别:
-
RTTI对应C++体系,反射对应OO面向对象体系;
-
RTTI在编译器编译时打开和检查.class文件,反射在运行时打开和检查.class文件。
20、 即时编译器技术(JIT)
JIT(Just In Time Compiler,即时编译技术),是JVM的重要一部分。JIT的核心是分析代码,优化运行效率。在运行时JIT会把编译过的机器码缓存起来,已备下次使用。JIT不是一定可以加快执行速度,有可能降低执行速度,这取决于你的代码结构,当然大多数情况下我们还是能够如愿以偿的。
JIT常见的优化:
-
代码可能写的不够最优,由JIT代替程序员做优化;
-
程序代码本身没问题,但是cpu和内存的操作可以进一步优化,这些程序员并不知道,由JIT来帮程序员做优化。
面向对象
1、 面向对象思想
面向对象是相对于面向过程而言的,面向过程强调的是功能,面向对象强调的是将功能封装进对象,强调具备功能的对象。
我要达到某种结果,我就寻找能帮我达到该结果的功能的对象,比如我要洗衣服我就买洗衣机,至于怎么洗我无需管。
特点:
? 是符合人们思考习惯的一种思想;
? 将复杂的事情简单化了;
? 将程序员从执行者变成了指挥者。
特征:
- 封装:
隐藏对象的属性和实现细节,仅对外提供公共访问方式。
- 继承:
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可。
- 多态:
一个对象在程序不同运行时刻代表的多种状态,父类或者接口的引用指向子类对象。(Service和Impl就是这么回事)
2、 类和对象
类:对现实世界中某类事物的描述,是抽象的,概念上的定义。
对象:事物具体存在的个体。
3、 成员变量和局部变量(重点)
- 作用域
成员变量:针对整个类有效。
局部变量:只在某个范围内有效(一般指的就是方法或者语句体内) 。
- 存储位置
成员变量:随着对象的创建而存在,随着对象的消失而消失,存储在堆内存中。
局部变量:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。 当方法调用完,或者语句结束后,就自动释放。
- 初始值
成员变量:有默认初始值。
局部变量:没有默认初始值,使用前必须赋值。
4、 封装(重点)
- 定义:
隐藏对象的属性和实现细节,仅对外提供公共访问方式。
- 特点:
? 隐藏实现,调用者只需调用方法即可,不知道具体实现过程;
? 提高数据的安全性,不能通过“变量名.属性名”的方式来修改某个私有的成员属性;
? 便于修改,提高代码的可维护性。
- 步骤:
? 对需要封装的属性使用访问权限修饰符private修饰;
? 在类的内部提供对外访问的set和get方法。
5、 继承(重点)
- 定义:
把很多类的相同特征和行为进行抽取,用一个类来描述。让多个类和这个类产生一个关系。这样的话,多个类就可以省略很多代码,这个关系就是继承。java中用extends关键字表示。
- 继承的特点:
? java中只能单继承,没有多继承;
? java可以有多重(层)继承,内部类可以实现。
- 继承的好处:
? 继承的出现提高了代码的复用性;
? 继承的出现让类与类之间产生了关系,提供了多态的前提。
- 子父类中的成员关系:
- 成员变量的使用,在子类方法中使用一个变量时:
? 首先,在方法的局部变量中找这个变量,有则使用。
? 否则,在本类中找成员变量,有则使用。
? 否则,在父类中找成员变量,有则使用。
? 否则,报错。
- 成员方法的使用,用子类对象使用一个方法时:
? 首先,在子类中找这个方法,有则使用。
? 否则,在父类中找这个方法,有则使用。
? 否则,报错。
- 重写和重载的区别:
重载:在同一类中,方法名相同,参数列表不同,重载可以改变返回类型。
重写:表现在不同类中(子父类中)。
- 重写需要注意:
? 子类方法的访问权限要大于等于父类方法的访问权限。
? 静态只能重写静态,但是这种情况一般不会出现。
- this和super的区别
this:代表本类对象的引用。
super:代表父类的存储空间。
- 子类调用父类构造方法的顺序:
-
子类的构造方法总是先调用父类的构造方法;
-
如果子类的构造方法没有明显地指明使用父类的哪个构造方法,子类会默认调用父类不带参数的构造方法;
-
如果父类没有无参的构造函数,则子类需要在自己的构造方法中显示的调用父类的构造函数;
-
如果父类没有无参的构造函数,并且在子类的构造方法中也没有显示地调用父类的其他构造方法,这时Java编译器将报告错误。使用super调用父类构造方法必须是子类构造方法的第一条语句。
- 注意事项:
-
当父类没有无参构造方法时,必须使用super调用其他的构造方法
-
子类创建对象时,会先去创建父类的对象。
-
类的构造器即构造函数不能被继承,因此不能被重写,但可以被重载。
6、 多态(重点)
- 定义:
同一个对象,在程序不同时刻的多种运行状态。
- 举例:
动物:狗是动物,猫也是动物。用法:Animala1 = new Dog();Animala2 = new Cat();
食物:猪肉是食物,鱼肉也是食物 。用法:同上。
- 多态前提:
? 存在着继承或者实现关系
? 有方法的重写
? 父类(接口)引用指向子类(实现)对象
- 多态的优点和缺点:
优点:多态的存在提高了程序的扩展性和后期可维护性。
缺点:虽然可以预先使用,但是只能访问父类中已有的功能,运行的是后期子类的功能内容,不能预先使用子类中定义的特有功能。
- 多态中对象调用成员的特点:
Fu f = new Zi();
? A:成员变量
编译看左边,运行看左边
? B:成员方法
编译看左边,运行看右边
? C:静态方法
编译看左边,运行看左边
- 多态的思想:
指挥同一批对象做事情。举例:带兵打仗,下课等。
- 类的构造器即构造函数不能被继承,因此不能被重写,但可以被重载。
7、 权限修饰符
? private:类内部
? default(缺省):类内部、同一个包
? protected:类内部、同一个包、子类
? public:任何地方
8、 构造方法
- 特点:
? 方法名与类名相同
? 没有返回类型
? 没有返回值
- 作用:
构造函数是用于创建对象,并对其进行初始化赋值,对象一建立就自动调用相对应的构造函数。
- 构造方法的注意事项:
? 如果一个自定义类没有构造方法,系统会默认给出一个无参构造方法。
? 如果一个自定义类提供了构造方法,系统将不再给出无参构造方法。
? 如果你想使用无参构造方法,则必须手动给出无参构造方法。
? 一般情况下,我们的自定义类都要手动给出无参构造方法。
- 构造方法和成员方法的区别:
- 格式区别:
? 构造方法和类名相同,并且没有返回类型,也没有返回值。
? 普通成员方法可以任意起名,必须有返回类型,可以没有返回值。
- 作用区别:
? 构造方法用于创建对象,并进行初始化值。
? 普通成员方法是用于完成特定功能的。
- 调用区别:
? 构造方法是在创建对象时被调用的,一个对象建立,只调用一次构造方法。
? 普通成员方法是由创建好的对象调用,可以调用多次。
9、 构造代码块
- 作用:
给对象进行初始化,对象一建立就执行,而且优先于构造方法执行。
- 构造代码块和构造方法的区别:
? 构造代码块是给所有不同对象的共性进行统一初始化
? 构造方法是给对应的对象进行初始化
- 执行顺序:
静态代码块 ? 构造代码块 ? 构造方法
10、 静态代码块
静态代码块作用于类,主要用于类的初始化操作,并且只执行一次,执行优先级高于非静态的初始化块。
11、 static关键字(重点)
- 定义:
用来修饰成员变量、成员方法、类、块(比如静态代码块)。
- 特点:
? 随着类的加载而加载
? 优先于对象存在
? 对所有对象共享
? 可以被类名直接调用
- 注意事项:
? 静态方法只能访问静态成员,因为静态的内容是随着类的加载而加载,它是先进内存。
? 静态方法中不能使用this,super关键字。
? main方法是静态的,public static void main(String[] args),详细介绍如下:
? public:公共的意思,是最大权限修饰符。
? static:由于jvm调用main方法的时候,没有创建对象。只能通过类名调用。所以,main必须用static修饰。
? void:由于main方法是被jvm调用,不需要返回值,用void修饰。
? main:main是主要的意思,所以jvm采用了这个名字,是程序的入口。
? string[]:字符串数组
? args:数组名
? 在运行的时候,使用java命令给args数组赋值。 格式:java MainTest hello world
- 静态变量和成员变量的区别:
? 调用方式:
静态变量也称为类变量,可以直接通过类名调用,也可以通过对象名调用。
这个变量属于类。
成员变量也称为实例变量,只能通过对象名调用,这个变量属于对象。
? 存储位置:
静态变量存储在方法区/元数据空间中的静态区。
成员变量存储在堆内存,局部变量存储在栈内存。
? 生命周期:
静态变量随着类的加载而存在,随着类的消失而消失,生命周期长。
成员变量随着对象的创建而存在,随着对象的消失而消失。
? 与对象的相关性:
静态变量是所有对象共享的数据。
成员变量是每个对象所特有的数据。
- 静态的优点和缺点 :
优点:
对对象的共享数据进行单独空间的存储,节省内存,没有必要每个对象都存储一份,可直接被类名调用。
缺点:
生命周期过长,一般不推荐使用,静态虽好但只能访问静态 。
- 什么场景使用使用静态:
? 当所有对象共享某个数据的时候,就把这个成员变量定义为静态修饰的。
? 当某个方法没有访问该类中的非静态成员,就可以把这个方法定义为静态修饰。
- 静态代码块:
? 它只执行一次,它比main方法还先执行。
? 执行顺序:静态代码块 ? 构造代码块 ? 构造方法
静态代码块 – >构造代码块 --> 构造方法
- static final和final static的区别:
static final和final static没什么区别,一般static写在前面,伪命题小心掉坑。
12、 final关键字(重点)
- 定义:
最终的意思,可以用于修饰类,方法,变量。
- 用法:
final修饰的类不能被继承。
final修饰的方法不能被重写。
final修饰的变量是一个常量,只能被赋值一次。
- 注意事项:
内部类只能访问被final修饰的局部变量。
13、 this关键字
- 定义:
this关键字代表本类对象的一个引用,谁调用this所在的方法,this就代表谁。
- 使用场景:
? 用于区分同名成员变量和局部变量;
? 在定义方法时,该方法内部要用到调用该方法的对象时,因为此时对象还没建立,故this代表此对象;
? 构造方法调用 ,this(参数)必须作为第一条语句存在。
14、 instanceof关键字
用于判断某个对象是否是某种类型。格式:任意对象 instanceof 类名。
15、 抽象类(重点)
- 定义:
当多个类都有相同的方法声明,但是方法体不一样,此时我们考虑把方法声明进行抽取到父类中,让子类继承后,子类去实现方法体。没有方法体的方法,我们需要用抽abstract来修饰。
- 特点:
? 抽象类和抽象方法都要用abstract进行修饰
? 抽象类不能被实例化
? 抽象类中不一定有抽象方法,但是,有抽象方法的类一定是抽象类。
- 数据的特点:
? 成员变量
抽象类中可以有变量,也可以有常量。
? 成员方法
抽象类中可以有抽象方法,也可以有非抽象方法。
? 构造方法
抽象类是一个类,所以它有构造方法。虽然本身不能实例化,但是可以给子类实例化使用。
- 相关问题:
? 抽象类中可不可以没有抽象方法?
答:抽象类可以没有抽象方法。
? 抽象类中是否有构造方法?能不能被实例化?如果不能,为什么有构造方法?
答:抽象类有构造方法。抽象类不能被实例化。抽象类中的构造方法供子类实例化调用。
? 抽象关键字abstract不可以和哪些关键字共存?
**private:
私有内容子类继承不到,所以不能重写。 但是abstract修饰的方法,要求被重写,两者冲突。
**final
final修饰的方法不能被重写。但是abstract修饰的方法,要求被重写,两者冲突。
**static
假如一个抽象方法能通过static修饰,那么这个方法,就可以直接通过类名调用。而抽象方法是没有方法体的,这样的调用无意义。所以不能用static修饰。
16、 接口(重点)
- 定义:
当一个类中的方法都是抽象的时候,java提供了另一种表示方式,叫接口。
用interface关键字表示,类与接口关系用implements表示。
- 成员特点:
? 成员变量:是常量,默认修饰 public static final
? 成员方法:都是抽象的,默认修饰 public abstract
- 接口的特点:
? 是对外暴露的规则 。
? 是功能的扩展 。
? 接口的出现降低耦合性。
? 耦合(类与类之间的关系)
? 内聚(类完成功能的能力)
? 编程规范:低耦合,高内聚。
? 类可以实现多个接口,接口可以继承多个接口。
- 接口和抽象类的区别:
? 抽象类只能被单继承,接口可以多实现。
? 抽象类中定义的是继承体系中的共性功能,接口中定义的是实现体系中的扩展功能。
? 数据特点不一样,如下:
抽象类中的数据特点:
成员变量:可以是变量,也可以是常量
成员方法:可以是抽象方法,也可以是非抽象方法
构造方法:有构造方法
接口中的数据特点:
成员变量:是常量。默认修饰 public static final
成员方法:都是抽象方法。都有默认修饰 public abstract
构造方法:没有构造方法
17、 Object类
- 定义
是所有类的根类或者超类。java中提供的类以及我们自定义的类都直接或者间接的继承自Object类。
- Object类中的方法,如下:
? Object clone()
实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
? 浅拷贝:拷贝的是引用。
? 深拷贝:新开辟内存空间,进行值拷贝。
? void finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
? Class getClass()
获取对象在运行时类的所有信息的集合。
例子:String name = s.getClass().getName();
? int hashCode()
获取对象的哈希值,其实就是对象的内存地址值十进制表示 。
? String toString()
返回对象的字符串表示。
表示格式: getClass().getName()+”@”+Integer.toHexString(hashCode());
一般我们输出对象名的时候,其实底层调用的就是该对象的toString()方法。这种返回没有意义,所以我们会重写这个方法,显示类的成员变量信息。
? boolean equals(Object obj)
用于比较两个对象的地址值是否相同。我们获取对象后,比较它的地址值意义不大,所以也会对这个方法进行重写。重写要完成什么功能,是根据需求定的。
? void wait(long timeout)
让当前线程释放所持有对象的锁(线程暂停执行),进入在对象监视器上等待的状态,直到达到最长等待时间(timeout)或其他线程调用notify()或notifyAll()方法才被唤醒。
? void notify
唤醒在该对象上等待的某个线程。
? void notifyAll
唤醒在该对象上等待的所有线程。
18、 ==和equals的用法:
? ==的用法:
? 可以用于比较基本数据类型,比较的就是基本数据类型的值是否相等。
? 可以用于比较引用数据类型,比较的是对象的地址值是否相等。
? equals的用法:
? equals只能用于比较引用数据类型的。
? Object提供的equals是用于比较对象地址值是否相同。
? 自定义类中,如果重写了equals方法,那么就是按照你自己的需求来比较。比如:Integer重写了equals方法,是值比较。
19、 Person p = new Person(); 在内存中做了哪些事情?
-
将Person.class文件加载进内存中,即操作方法区;
-
在栈空间开辟一个变量空间p;
-
在堆内存给对象分配空间;
-
对对象中的成员进行默认初始化,比如:设置为int赋值0,boolean赋值false等等;
-
对对象中的成员进行显示初始化,比如:int i = 10,则赋值为10;
-
调用静态代码块对对象进行初始化(如果没有就不执行);
-
调用构造代码块对对象进行初始化(如果没有就不执行);
-
调用构造函数对对象进行初始化,此时对象初始化完毕;
-
将对象的内存地址赋值给p变量,让p变量指向该对象。
-
方法执行完毕后,p变量出栈释放内存,Person对象的内存释放是由GC来处理
默认初始化:系统在堆内存创建一个新的对象时,进行的默认初始化,如null和0。
显示初始化:在类定义时,直接在各个成员变量的定义时,优先进行赋值。
20、 对象的初始化流程如下:
-
初始化父类的静态成员
-
初始化父类的静态代码块
-
初始化子类的静态成员
-
初始化子类的静态代码块
-
初始化父类的非静态成员
-
初始化父类的非静态代码块
-
初始化父类的构造方法
-
初始化子类的非静态成员
-
初始化子类的非静态代码块
-
初始化子类的构造方法
-
加载class类,如果继承了父类,则先加载父类,又如果父类继承了祖父类,则先加载祖父类,递归执行下面的步骤,按辈分从大到小执行。
? 初始化类(祖父类/父类/子类)的静态成员
? 执行类(祖父类/父类/子类)的静态代码块
- 创建对象,如果继承了父类,则先创建父类,又如果父类继承了祖父类,则先创建祖父类,递归执行下面的步骤,按辈分从大到小执行。
? 初始化类的成员变量
? 执行类的构造代码块
? 调用类的构造函数
温馨提示:
\1. 静态成员变量和静态代码块都是存在方法区,生命周期跟随类
\2. 成员变量是跟随着类的实例存放在堆当中,生命周期跟随对象
\3. 总是最先初始化父类,总是最先初始化java.lang.Object类。
21、 匿名对象
- 定义:
匿名对象就是没有名字的对象,是对象的一种简写形式。
- 应用场景:
? 只调用一次类中的方法,
? 可以作为实际参数在方法传递中使用
- 使用方式:
new Car().run(); 即调用run()方法。
- 注意事项:
匿名对象执行完毕后,由于不再被引用,Java的自动回收机制会视作垃圾处理。
22、 内部类(次重点)
- 定义:
把一个类定义在某个类中的,这个类就被称为内部类,内置类,嵌套类。
- 访问特点:
? 内部类可以直接访问外部类中的成员,因为内部类持有外部类的引用。可以实现多继承。
? 外部类要想访问内部类的成员,必须创建对象访问。
- 什么时候使用内部类:
假如有A类和B类,A类想直接访问B类的成员,B类想通过创建A类对象来进行访问A类成员,这个时候,就可以把A类定义为B类的内部类。
23、 匿名内部类(局部内部类的简写) (重点)
- 定义:
是没有名字的内部类。正因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码的编写。
- 规则:
? 匿名内部类不能有构造方法;
? 匿名内部类不能定义任何静态成员、方法和类;
? 匿名内部类不能是public,protected,private,static;
? 只能创建匿名内部类的一个实例;
? 一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类;
? 因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。
- 使用场景:
匿名内部类适合创建那种只需要一次使用的类。
- 匿名内部类的优点和缺点:
优点:简化代码书写
缺点:
? 不能直接调用自己的特有方法
? 不能执行强转换动作
? 如果该类里面方法较多,不允许使用匿名内部类
- 注意事项:
? 在Java 8之前,Java要求被局部内部类、匿名内部类访问的局部变量必须使用final修饰。
? 从Java 8开始这个限制取消了,Java 8更加智能,如果局部变量被匿名内部类访问,那么该局部变量相对于自动使用了final修饰。
- 格式:
new 父类名或者接口名()
{
重写父类方法或者实现接口中的方法。
也可以自定义其他方法。
};
- demo代码:
interface Fish{
public String getFishName();
public int getPrice();
}
public class Test{
public void test(Fish f){
System.out.println(“买一条” + f.getFishName() + “,花费了” + f.getPrice());
}
public static void main(String[] args){
Test t = new Test ();
t.test(new Fish(){
public int getPrice(){
return 1000;
}
public String getFishName(){
return “银龙”;
}
});
}
}
输出结果:买一条银龙,花费了1000
24、 异常
- 定义:
程序运行过程中的不正常现象就叫异常。异常体系的根类是Throwable,所有的异常都是从Throwable继承而来的,是所有异常的共同祖先。
- 异常的体系结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lI70YhnF-1645175624412)(/Users/laowang/Desktop/求职/img/20150107221255554.png)]
Throwable:
|CError:重大的问题,我们处理不了,也不需要编写代码处理,比如说内存溢出。
|CException:一般性的错误,是需要我们编写代码进行处理。
|CRuntimeException:运行时异常,不受检查异常,也不需处理。
|C非RuntimeException:受检查异常,必须处理,比如IOException。
- Throwable:
它包括Error和Exception两个子类。有以下方法:
? getMessage():获取异常信息,返回字符串。
? toString():获取异常类名和异常信息,返回字符串。
? printStackTrace():获取异常类名和异常信息,以及异常出现的位置,返回值void。
- Error:
表示出现错误,编译时期的错误以及系统错误都是通过Error抛出,这些型错误是程序处理不了的,也不需要编写代码处理,比如说内存溢出。
- Exception:
表示出现异常,它包括非检查异常(unckecked exception)和检查异常(checked exception)两种异常。
- 非检查异常(unckecked exception):
包括Error和RuntimeException以及他们的子类。javac在编译时不会提示这样的异常,也不需要处理这些异常,对于这些异常,我们应该修正代码,而不是捕捉异常。比如:NullPointerException、ArrayIndexOutOfBoundsException等等。
- 检查异常(checked exception):
除了Error和RuntimeException以及他们的子类外,都是检查异常。javac在编译时会提示这样的异常,必须处理这些异常。比如:IOException、SQLException、ClassNotFoundException等等。
- 异常的处理:
捕捉异常格式try⋯catch⋯finally。可以写多个catch,即捕捉多个异常,原则是先捕捉小的再捕捉大的异常。finally表示永远被执行,除非退出jvm或者执行了System.exit(0)才会不执行。
- 自定义异常:
除了使用Java内置的异常类以外,还可以自定义异常类,只需继承Exception或者RuntimeException。在项目开发过程中,大多数情况都是继承RuntimeException。
- Error和Exception区别:
? Error表示总是不可控制的(unchecked),Exception可以是可被控制(checked)或者不可控制的(unchecked);
? Error表示系统错误或低层资源的错误,Exception表示程序代码的错误;
? Error应该在系统级被捕捉,Exception应该在应用程序级被捕捉。
- throws和throw的区别:
throws用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给谁。throw是用来抛出一个具体的异常类型。
? throws用在方法声明后面,throw用在方法体内;
? throws可以抛出多个异常类名,用逗号隔开,throw只能抛出一个异常对象名;
? throws表示抛出异常,由该方法的调用者来处理,throw表示抛出异常,由方法体内的语句处理;
? throws表示出现异常的一种可能性,并不一定会发生这些异常,throw表示一定抛出了某种异常。
25、 final,finally,finalize区别
? final是最终的意思。它可以用于修饰类、成员变量、成员方法。修饰的类不能被继承,修饰的变量是常量,修饰的方法不能被重写;
? finally是异常处理里面的关键字。表示永远被执行,除非退出jvm或者执行了System.exit(0)才会不执行;
? finalize是Object类中的一个方法。它是于垃圾回收器调用的方式。
26、 假如catch中有return语句, finally里中的代码会执行吗?是在return前,还是在return后呢?
答:会,在return前执行。
数组和链表的区别:
1、 数组是将元素在内存中连续存放。
链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。
2、 数组必须事先定义固定的长度,不能适应数据动态的增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;
链表动态地进行存储分配,可以适应数据动态地增减的情况。
3、(静态)数组从栈中分配空间,对于程序员方便快速,但是自由度小;
链表从堆中分配空间,自由度大但是申请管理比较麻烦。
数组和链表在存储数据方面到底谁好?根据数组和链表的特性,分两种情况讨论:
1、当进行数据查询时,数组可以直接通过下标迅速访问数组中的元素。
而链表则需要从第一个元素开始一直找到需要的元素位置,
显然,数组的查询效率会比链表的高。
2、当进行增加或删除元素时,在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。
同样,如果想删除一个元素,需要移动大量去填掉被移动的元素,而链表只需改动元素中的指针即可实现增加或删除元素。
那么哈希表,是既能具备数组的快速查询的优点,又能融合链表方便快捷的增加删除元素的优势。
所谓的hash,简单的说就是散列,即将输入的数据通过hash函数得到一个key值,输入的数据存储到数组中下标的key值的数组单元中去。
Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表。
数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。
而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。
有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。
哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:
哈希表。如拿HashMap来说。
从上图中,我们可以发现哈希表是由数组 + 链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它的内部其实是用一个Entity数组来实现的,属性有key、value、next。
13、集合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JsibymZB-1645175624414)(/Users/laowang/Desktop/求职/img/图片 1.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TKMJd5ih-1645175624415)(/Users/laowang/Desktop/求职/img/20140801190156823.jpg)]
1、ArrayList、LinkedList、Vector、Stack、CopyOnWriteArrayList、ArrayList和Vector区别、LinkedList与ArrayList的区别:
1.LinkedList
是一个双链表,在**添加和删除元素时具有比ArrayList更好的性能.**但在get与set方面弱于ArrayList.不同步(就是线程不安全):
同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。
由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。
与ArrayList一样,LinkedList也是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list = Collections.synchronizedList(new LinkedList(…));
2.*ArrayList*
是一个可改变大小的数组.当更多的元素加入到ArrayList中时,其大小将会动态地增长.内部的元素可以直接通过get与set方法进行访问,因为ArrayList本质上就是一个数组.不同步(就是线程不安全)
ArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。
size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。
ArrayList擅长于随机访问。同时ArrayList是非同步的。
**3.**Vector
Vector是矢量队列,它继承了AbstractList,实现了List、 RandomAccess, Cloneable, java.io.Serializable接口。
和ArrayList类似,但属于强同步类。如果你的程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用ArrayList是更好的选择。同步(线程安全)
有句话叫越安全,效率就越低。
4.stack
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。线程安全
ArrayList和LinkeLlist
1.ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2.对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList要移动数据。
这一点要看实际情况的。**若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。**但若是批量随机的插入删除数 据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。
Vector和ArrayList
1,Vector : 是线程同步的,所以它也是线程安全的
Arraylist : 是线程异步的,是不安全的。如果不考虑到线程的安全因素,一般用
arraylist效率比较高。
2,如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%. 如果在集合中使用数据量比较大的数据,用vector有一定的优势。
5、CopyOnWriteArrayList
CopyOnWriteArrayList : 在读的频率大于写的并发应用程序中,一般用 CopyOnWriteArrayList
类替代ArrayList
,实际上CopyOnWriteArrayList内部维护的就是一个数组。是一个ArrayList的线程安全的变体。
COW的设计思想
回到上面所说的,如果简单的使用读写锁的话,在写锁被获取之后,读写线程被阻塞,只有当写锁被释放后读线程才有机会获取到锁从而读到最新的数据,站在读线程的角度来看,即读线程任何时候都是获取到最新的数据,满足数据实时性。既然我们说到要进行优化,必然有trade-off,我们就可以牺牲数据实时性满足数据的最终一致性即可。而CopyOnWriteArrayList就是通过Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略来实现数据的最终一致性,并且能够保证读线程间不阻塞。
COW通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。对CopyOnWrite容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
- 内存占用问题:因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对 象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对 象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比 如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的minor GC和major GC。
- 数据实时一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
2、HashMap、HashTable、ConcurrentHashMap、TreeMap、LinkedHashMap、WeakHashMap、HashMap与Hashtable的区别、HashMap与TreeMap的区别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4eFQwNjs-1645175624416)(/Users/laowang/Desktop/20150927125918227.png)]
可以看到Map集合没有继承Collection接口,其提供的是键到值的映射。Map不能包含相同的键,每个键只能映射一个值。键还决定了储存对象在映射中的储存位置。
HashMap与HashTable:
HashMap:
1、继承Dictionary类,但都实现了Map接口
2、线程不安全
3、键和值,两个都可以为null
4、保留了contains方法
5、重新计算hash值
6、默认大小是16,扩容方式:2的N次方
HashTable :
1、继承AbstracMap类,但都实现了Map接口
2、线程安全
3、不能用null作键值,一个都不行!
4、没有contains
5、直接使用对象的hashCode
6、默认大小是11,增加的方式是 old*2+1。
(1)继承的父类不同
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
(2)线程安全性不同
Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,所以是线程安全的。
HashMap时就必须要自己增加同步处理,本身是线程不安全。
*(3)是否提供contains方法*
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。
Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。
*(4)key和value是否允许null值*
其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。
Hashtable中,key和value都不允许出现null值。
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
*(5)两个遍历方式的内部实现上不同*
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
*(6)hash值不同*
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
*(7)内部实现使用的数组初始化和扩容方式不同*
Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。
HashMap中hash数组的默认大小是16,而且一定是2的指数。
HashMap与CurrentHashMap区别
先回到Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操作;
而ConcurrentHashMap中则是一次锁住一个桶:
**ConcurrentHashMap : **
(1)ConcurrentHashMap对整个桶数组进行了分段,而HashMap则没有
(2)ConcurrentHashMap在每一个分段上都用锁进行保护,从而让锁的粒度更精细一些,并发性能更好
HashMap:没有锁机制,不是线程安全的,在多线程环境中,需要手动实现同步机制。
Hashmap与Linkedhashmap区别
LinkedHashMap是HashMap子类,而且还多了after和behind方法。
LinkedHashMap比HashMap多维护了一个链表。
TreeMap
TreeMap的底层使用了红黑树来实现,像TreeMap对象中放入一个key-value 键值对时,就会生成一个Entry对象,这个对象就是红黑树的一个节点,其实这个和HashMap是一样的,一个Entry对象作为一个节点,只是这些节点存放的方式不同。
存放每一个Entry对象时都会按照key键的大小按照二叉树的规范进行存放,所以TreeMap中的数据是按照key从小到大排序的。
TreeMap和HashMap的区别:
TreeMap:
1、线程不安全
2、继承SortedMap类,保持键的有序顺序,所有的元素都是有某一固定顺序
3、基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
HashMap:
1、线程不安全
2、继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;
3、基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals()(可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;
(虽然HashMap的数据结构是数组加链表,但是当某个链表下的数据量达到一定阈值时他会触发树化操作,树化是新版本中的HashMap引入的优化手段,基于红黑树的数据结构重新结构化Map以得到进一步的性能提升,而hashtable没有相应的机制。)
- HashMap是通过hashcode()对其内容进行快速查找的;HashMap中的元素是没有顺序的;
- TreeMap中所有的元素都是有某一固定顺序的,如果需要得到一个有序的结果,就应该使用TreeMap;
- HashMap和TreeMap都不是线程安全的;
- HashMap继承AbstractMap类;覆盖了hashcode() 和equals() 方法,以确保两个相等的映射返回相同的哈希值;
- TreeMap继承SortedMap类;他保持键的有序顺序;
- HashMap:基于hash表实现的;使用HashMap要求添加的键类明确定义了hashcode() 和equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子;
- TreeMap:基于红黑树实现的;TreeMap就没有调优选项,因为红黑树总是处于平衡的状态;
- HashMap:适用于Map插入,删除,定位元素;
- TreeMap:适用于按自然顺序或自定义顺序遍历键(key);
补充:
为什么使用hashmap需要重写hashcodes和equals方法?
1、解决散列冲突(hashcodes)
2、在大量数据的情况下,提高效率(equals)
冲突主要取决于:
(1)散列函数,一个好的散列函数的值应尽可能平均分布。
(2)处理冲突方法。
(3)负载因子的大小。太大不一定就好,而且浪费空间严重,负载因子和散列函数是联动的。
散列冲突的解决办法:
(1)线性探查法:冲突后,线性向前试探,找到最近的一个空位置。缺点是会出现堆积现象。存取时,可能不是同义词的词也位于探查序列,影响效率。
(2)双散列函数法(拉链法):在位置d冲突后,再次使用另一个散列函数产生一个与散列表桶容量m互质的数c,依次试探(d+n*c)%m,使探查序列跳跃式分布。
1. HashMap
虽然HashMap的数据结构是数组加链表,但是当某个链表下的数据量达到一定阈值时他会触发树化操作
标准链地址法实现(下图)。数组方式存储key/value
,线程非安全,允许null
作为key
和value
,key
不可以重复,value
允许重复,不保证元素迭代顺序是按照插入时的顺序,key
的hash
值是先计算key
的hashcode
值,然后再进行计算,每次容量扩容会重新计算所以key
的hash
值,会消耗资源,要求key
必须重写equals
和hashcode
方法。默认初始容量16
,加载因子0.75
,扩容为旧容量乘2
,查找元素快,如果key
一样则比较value
,如果value
不一样,则按照链表结构存储value
。如果需要同步,可以用 Collections
的synchronizedMap
方法(Map m = Collections.synchronizedMap(new HashMap(…));
)。使HashMap
具有同步的能力,或者使用ConcurrentHashMap
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4VuvAKiP-1645175624418)(https://segmentfault.com/img/remote/1460000013577737?w=583&h=280)]
JDK 1.8之后,加入了static final int TREEIFY_THRESHOLD = 8;
,当同一桶内元素个数超过8个,就会将链表结构进行树化。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
2. HashTable
线程安全,不允许有null
的键和值,线程安全的,它在所有涉及到多线程操作的都加上了synchronized
关键字来锁住整个table
,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。
3. ConcurrentHashMap
HashTable
有很多的优化空间,锁住整个table
这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash
值,不会因为rehash
造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table
,这就是ConcurrentHashMap
JDK1.7版本的核心思想。
JDK1.7的实现
在JDK1.7版本中,ConcurrentHashMap
的数据结构是由一个Segment
数组和多个HashEntry
组成,如下图所示:
Segment
数组的意义就是将一个大的table
分割成多个小的table
来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment
元素存储的是**HashEntry
数组+链表**,这个和HashMap
的数据存储结构一样
JDK1.8的实现
JDK1.8的实现已经摒弃了Segment
的概念,而是直接用**Node
数组+链表+红黑树**的数据结构来实现,并发控制使用Synchronized
和CAS
来操作,整个看起来就像是优化过且线程安全的HashMap
,虽然在JDK1.8中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本。
4. TreeMap
TreeMap
实现SortMap
接口,基于红黑二叉树的NavigableMap
的实现,线程非安全,不允许null
,key
不可以重复,value
允许重复,存入TreeMap
的元素应当实现Comparable
接口或者实现Comparator
接口,会按照排序后的顺序迭代元素,两个相比较的key
不得抛出classCastException
。主要用于存入元素的时候对元素进行自动排序,迭代输出的时候就按排序顺序输出
5. LinkedHashMap
LinkedHashMap
是HashMap
的一个子类,保存了记录的插入顺序,在用Iterator
遍历LinkedHashMap
时,先得到的记录肯定是先插入的。也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap
慢,不过有种情况例外,当HashMap
容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap
慢,因为LinkedHashMap
的遍历速度只和实际数据有关,和容量无关,而HashMap
的遍历速度和他的容量有关。
6. WeakHashMap
WeakHashMap
,从名字可以看出它是某种 Map
,允许null
作为key
和value
(Both null values and the null key are supported.),非线程安全(this class is not synchronized)。它的特殊之处在于 WeakHashMap
里的entry
可能会被GC
自动删除,即使程序员没有调用remove()
或者clear()
方法。
更直观的说,当使用 WeakHashMap
时,即使没有显示的添加或删除任何元素,也可能发生如下情况:
- 调用两次
size()
方法返回不同的值;- 两次调用
isEmpty()
方法,第一次返回false
,第二次返回true
;- 两次调用
containsKey()
方法,第一次返回true
,第二次返回false
,尽管两次使用的是同一个key
;- 两次调用
get()
方法,第一次返回一个value
,第二次返回null
,尽管两次使用的是同一个对象。
WeekHashMap
的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
要明白WeekHashMap
的工作原理,还需要引入一个概念:弱引用(WeakReference)。Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的**“有效引用”并不包括弱引用**。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。
WeakHashMap
内部是通过弱引用来管理entry
的,弱引用的特性对应到WeakHashMap
上意味:将一对key, value
放入到WeakHashMap
里并不能避免该key
值被GC
回收,除非在WeakHashMap
之外还有对该key
的强引用。
3、HashSet、TreeSet、LinkedHashSet、HashSet、TreeSet、LinkedHashSet的区别
一.HashSet
特点:
1.HashSet中不能有相同的元素,可以有一个Null元素,存入的元素是无序的。
2.HashSet如何保证唯一性?
1).HashSet底层数据结构是哈希表,哈希表就是存储唯一系列的表,而哈希值是由对象的hashCode()方法生成。
2).确保唯一性的两个方法:**hashCode()和equals()**方法。
3.添加、删除操作时间复杂度都是O(1)。
4.非线程安全
二.LinkedHashSet
特点:
1.LinkedHashSet中不能有相同元素,可以有一个Null元素,元素严格按照放入的顺序排列。
2.LinkedHashSet如何保证有序和唯一性?
1).底层数据结构由哈希表和链表组成。
2).链表保证了元素的有序即存储和取出一致,哈希表保证了元素的唯一性。
3.添加、删除操作时间复杂度都是O(1)。
4.非线程安全
三.TreeSet
特点:
1.TreeSet是中不能有相同元素,不可以有Null元素,根据元素的自然顺序进行排序。
2.TreeSet如何保证元素的排序和唯一性?
底层的数据结构是红黑树(一种自平衡二叉查找树)自然排序
3.添加、删除操作时间复杂度都是O(log(n))
4.非线程安全
HashSet、TreeSet和LinkedHashSet比较
HashSet是用hash table 实现的,它其中的元素是无序的,add、remove和contains方法的时间复杂度都是O(1)。
TreeSet是使用tree 结构实现的(算法书中的红黑树)。它其中的元素是有序的,但是add、remove和contains方法的时间复杂度是 O(log (n)),TreeSet提供了frist()、last()、headset()和tailset()等方法来处理这个有序的set。
LinkedHashSet是介于TreeSet和HashSet之间的,它利用hash table 实现的,同时使用了Link List,所以它提供了插入的顺序,基本方法的时间复杂度是O(1)。
四.总结:
通过以上特点可以分析出,三者都保证了元素的唯一性,如果无排序要求可以选用HashSet;如果想取出元素的顺序和放入元素的顺序相同,那么可以选用LinkedHashSet。如果想插入、删除立即排序或者按照一定规则排序可以选用TreeSet。
4、阻塞队列、非阻塞队列、双端队列(Deque)
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列.
1.ArrayDeque, (数组双端队列)
2.PriorityQueue, (优先级队列)
3.ConcurrentLinkedQueue, (基于链表的并发队列)
4.DelayQueue, (延期阻塞队列)(阻塞队列实现了BlockingQueue接口)
5.ArrayBlockingQueue, (基于数组的并发阻塞队列)
6.LinkedBlockingQueue, (基于链表的FIFO阻塞队列)
7.LinkedBlockingDeque, (基于链表的FIFO双端阻塞队列)
8.PriorityBlockingQueue, (带优先级的无界阻塞队列)
9.SynchronousQueue (并发同步阻塞队列)
阻塞队列和生产者-消费者模式
阻塞队列(Blocking queue)提供了可阻塞的put和take方法,它们与可定时的offer和poll是等价的。如果Queue已经满了,put方法会被阻塞直到有空间可用;如果Queue是空的,那么take方法会被阻塞,直到有元素可用。Queue的长度可以有限,也可以无限;无限的Queue永远不会充满,所以它的put方法永远不会阻塞。
阻塞队列支持生产者-消费者设计模式。一个生产者-消费者设计分离了“生产产品”和“消费产品”。该模式不会发现一个工作便立即处理,而是把工作置于一个任务(“to do”)清单中,以备后期处理。生产者-消费者模式简化了开发,因为它解除了生产者和消费者之间相互依赖的代码。生产者和消费者以不同的或者变化的速度生产和消费数据,生产者-消费者模式将这些活动解耦,因而简化了工作负荷的管理。
生产者-消费者设计是围绕阻塞队列展开的,生产者把数据放入队列,并使数据可用,当消费者为适当的行为做准备时会从队列中获取数据。生产者不需要知道消费者的省份或者数量,甚至根本没有消费者—它们只负责把数据放入队列。类似地,消费者也不需要知道生产者是谁,以及是谁给它们安排的工作。BlockingQueue可以使用任意数量的生产者和消费者,从而简化了生产者-消费者设计的实现。最常见的生产者-消费者设计是将线程池与工作队列相结合。
阻塞队列简化了消费者的编码,因为take会保持阻塞直到可用数据出现。如果生产者不能足够快地产生工作,让消费者忙碌起来,那么消费者只能一直等待,直到有工作可做。同时,put方法的阻塞特性也大大地简化了生产者的编码;如果使用一个有界队列,那么当队列充满的时候,生产者就会阻塞,暂不能生成更多的工作,从而给消费者时间来赶进进度。
有界队列是强大的资源管理工具,用来建立可靠的应用程序:它们遏制那些可以产生过多工作量、具有威胁的活动,从而让你的程序在面对超负荷工作时更加健壮。
虽然生产者-消费者模式可以把生产者和消费者的代码相互解耦合,但是它们的行为还是间接地通过共享队列耦合在一起了
类库中包含一些BlockingQueue的实现,其中LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列,与 LinkedList和ArrayList相似,但是却拥有比同步List更好的并发性能。PriorityBlockingQueue是一个按优先级顺序排序的队列,当你不希望按照FIFO的属性处理元素时,这个PriorityBolckingQueue是非常有用的。正如其他排序的容器一样,PriorityBlockingQueue可以比较元素本身的自然顺序(如果它们实现了Comparable),也可以使用一个 Comparator进行排序。
最后一个BlockingQueue的实现是SynchronousQueue,它根本上不是一个真正的队列,因为它不会为队列元素维护任何存储空间。不过,它维护一个排队的线程清单,这些线程等待把元素加入(enqueue)队列或者移出(dequeue)队列。因为SynchronousQueue没有存储能力,所以除非另一个线程已经准备好参与移交工作,否则put和take会一直阻止。SynchronousQueue这类队列只有在消费者充足的时候比较合适,它们总能为下一个任务作好准备。
非阻塞算法
基于锁的算法会带来一些活跃度失败的风险。如果线程在持有锁的时候因为阻塞I/O,页面错误,或其他原因发生延迟,很可能所有的线程都不能前进了。
一个线程的失败或挂起不应该影响其他线程的失败或挂起,这样的算法成为非阻塞(nonblocking)算法;如果算法的每一个步骤中都有一些线程能够继续执行,那么这样的算法称为锁自由(lock-free)算法。在线程间使用CAS进行协调,这样的算法如果能构建正确的话,它既是非阻塞的,又是锁自由的。非竞争的CAS总是能够成功,如果多个线程以一个CAS竞争,总会有一个胜出并前进。非阻塞算法堆死锁和优先级倒置有“免疫性”(但它们可能会出现饥饿和活锁,因为它们允许重进入)。
非阻塞算法通过使用低层次的并发原语,比如比较交换,取代了锁。原子变量类向用户提供了这些底层级原语,也能够当做“更佳的volatile变量”使用,同时提供了整数类和对象引用的原子化更新操作。
双端队列:
BlockingDeque
Java.util.concruuent包中的BlockingDeque接口是一种双端队列,向其中加入元素或从中取出元素都是线程安全的。这里展示如何使用BlockingDeque。
BlockingDeque是一个双端队列,如果完全不可能对双端队列进行插入或者删除元素,它将会阻塞线程。
deque 是 “Double Ended Queue”的简称。因此一个deque可以从两端插入和取出元素的。
BlockingDeque使用说明
BlockingDeque用于一个线程对同个一个双端队列进行生产和消费元素。也常用于生产线程需要在队列两端插入元素,消费线程需要从队列两端移除元素,下面是一个图示:
一个线程生产元素并将元素插入到队列的两端。如果当前队列是满的,插入线程将会被阻塞直到一个移除元素的线程从队列中取出一个元素。同样,如果队列当前是空的,移除元素的线程会被阻塞直到一个插入元素的线程向队列中插入了一个元素。
BlockingDeque的方法
对于在队列中插入、删除和检查元素操作BlockingQueue有4类不同的方法。如果操作不能立即执行,每一类方法的行为将不同。下面是方法列表:
operation | Throws Exception | Special Value | Blocks | Times Out |
---|---|---|---|---|
Insert | addFirst(o) | offerFirst(o) | putFirst(o) | offerFirst(o, timeout, timeunit) |
Remove | removeFirst(o) | pollFirst(o) | takeFirst(o) | pollFirst(timeout, timeunit) |
Examine | getFirst(o) | peekFirst(o) |
对于4种不同行为的说明参见BlockingQueuez中的说明。
BlockingDeque继承自BlockingQueue
BlockingDeque接口继承自BlockingQueue接口。这意味着你可以把BlockingDeque作为BlockingQueue来使用。如果你这样做,那么各种插入元素的方法将会把元素增加到队列的尾部,并且将会从队列的头部开始移除元素。
下面的列表列出了BlockingQueue方法在BlockingDeque中的实现。
BlockingDeque | BlockingQueue |
---|---|
add() | addLast() |
offer() x 2 | offerLast() x 2 |
put() | putLast() |
remove() | removeFirst() |
poll() x 2 | pollFirst() |
take() | takeFirst() |
element() | getFirst() |
peek() | peekFirst() |
BlockingDeque的具体实现
由于BlockingDeque是一个接口类,使用时需要使用它的实现类。Java.util.concurrent包中有以下关于BlockingDeque接口的实现类:
- LinkedBlockingDeque
例子
下面是如何实现BlockingDeque的小例子:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst();1234567
LinkedBlockingDeque
LinkedBlockingDeque类实现了BlockingDeque接口。
LinkedBlockingDeque是一个双端队列,如果一个线程试图从一个空的队列中取出元素,它将被阻塞住,不管线程是从哪一端去获取元素。
下面是展示如何实例化和使用LinkedBlockingDeque:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst();
StringBuffer和StringBuilder的区别
StringBuffer 是线程安全的
StringBuilder 是非线程安全的
所以当进行大量字符串拼接操作的时候,如果是单线程就用StringBuilder会更快些,如果是多线程,就需要用StringBuffer 保证数据的安全性
非线程安全的为什么会比线程安全的 快? 因为不需要同步嘛,省略了些时间
数据结构、线程、锁、JVM、IO、NIO、网络编程