JVM内存区域划分(JDK6/7/8中的变化)
最后更新于:2022-04-01 12:02:05
### 前言
Java程序的运行是通过Java虚拟机来实现的。通过类加载器将class字节码文件加载进JVM,然后根据预定的规则执行。Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些内存区域被统一叫做运行时数据区。Java运行时数据区大致可以划分为5个部分。如下图所示。在这里要特别指出,我们现在说的JVM内存划分是概念模型。具体到每个JVM的具体实现可能会有所不同。具体JVM的实现我只会提到HotSpot虚拟机的实现细节。
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-04-06_5704ab0810200.jpg "")
### 程序计数器
程序计数器是一块较小的内存空间,它可以看成是当前线程所执行的字节码的行号指示器。程序计数器记录线程当前要执行的下一条字节码指令的地址。由于Java是多线程的,所以为了多线程之间的切换与恢复,每一个线程都需要单独的程序计数器,各线程之间互不影响。这类内存区域被称为“线程私有”的内存区域。
由于程序计数器只存储一个字节码指令地址,故此内存区域没有规定任何OutOfMemoryError情况。
### 虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
一个栈帧就代表了一个方法执行的内存模型,虚拟机栈中存储的就是当前执行的所有方法的栈帧(包括正在执行的和等待执行的)。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。我们平时所说的“局部变量存储在栈中”就是指方法中的局部变量存储在代表该方法的栈帧的局部变量表中。而方法的执行正是从局部变量表中获取数据,放至操作数栈上,然后在操作数栈上进行运算,再将运算结果放入局部变量表中,最后将操作数栈顶的数据返回给方法的调用者的过程。(关于栈帧和基于栈的方法执行,我会在之后写两篇文章专门介绍。敬请期待☺)
虚拟机栈可能出现两种异常:由线程请求的栈深度过大超出虚拟机所允许的深度而引起的StackOverflowError异常;以及由虚拟机栈无法提供足够的内存而引起的OutOfMemoryError异常。
### 本地方法栈
本地方法栈与虚拟机栈类似,他们的区别在于:本地方法栈用于执行本地方法(Native方法);虚拟机栈用于执行普通的Java方法。在HotSpot虚拟机中,就将本地方法栈与虚拟机栈做在了一起。
本地方法栈可能抛出的异常同虚拟机栈一样。
### 堆
Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例:所有的对象实例以及数组都要在堆上分配(The heap is the runtime data area from which memory for all class instances and arrays is allocated)。但Class对象比较特殊,它虽然是对象,但是存放在方法区里。在下面的方法区一节会介绍。Java堆是垃圾收集器(GC)管理的主要区域。现在的收集器基本都采用分代收集算法:新生代和老年代。而对于不同的”代“采用的垃圾回收算法也不一样。一般新生代使用复制算法;老年代使用标记整理算法。对于不同的”代“,一般使用不同的垃圾收集器,新生代垃圾收集器和老年代垃圾收集器配合工作。(关于垃圾收集算法、垃圾收集器以及堆中具体的分代等知识,我之后会专门写几篇博客来介绍。再次敬请期待☺)
Java堆可以是物理上不连续的内存空间,只要逻辑上连续即可。Java堆可能抛出OutOfMemoryError异常。
### 方法区
方法区与Java堆一样,是各个线程共享的内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
所有的字节码被加载之后,字节码中的信息:类信息、类中的方法信息、常量信息、类中的静态变量等都会存放在方法区。正如其名字一样:方法区中存放的就是类和方法的所有信息。此外,如果一个类被加载了,就会在方法区生成一个代表该类的Class对象(唯一一种不在堆上生成的对象实例)该对象将作为程序访问方法区中该类的信息的外部接口。有了该对象的存在,才有了反射的实现。
在Java7之前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。但是在之后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经彻底没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫做元空间。
关于元空间的更多信息,请参考:[Java永久代去哪儿了](http://www.infoq.com/cn/articles/Java-PERMGEN-Removed?utm_campaign=infoq_content&)
### 运行时常量池
运行时常量池是方法区的一部分,关于运行时常量池的介绍,请参考我的另一篇博文:[String放入运行时常量池的时机与String.intern()方法解惑](http://blog.csdn.net/rainnnbow/article/details/50461303)。我还是花了些时间在理解运行时常量池上的。
### 直接内存
JDK1.4中引用了NIO,并引用了Channel与Buffer,可以使用Native函数库直接分配堆外内存,并通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
如上文介绍的:Java8以及之后的版本中方法区已经从原来的JVM运行时数据区中被开辟到了一个称作元空间的直接内存区域。
参考:
《深入理解Java虚拟机-JVM高级特性与最佳实践》第二版 周志明著
Java编程思想阅读收获
最后更新于:2022-04-01 12:02:03
15年8月份买了一本Java编程思想第四版中文版。之所以买中文版是因为我试读了同事的英文版发现自己英语水平还是有限,单词虽然认识,但对很多句子把握不准,这样看书太慢了,要理解英文还要理解技术有些hold不住。买了之后断断续续的看到现在,看了400多页了,看到了泛型一章。从15年8月份到元旦,中间还看了本周志明著的《深入理解java虚拟机-JVM高级特性与最佳实践》对JVM有了初步的了解。对于内存布局、垃圾回收、类加载、字节码等概念不再那么模糊。
Java编程思想一书已经读了正好一半了,很有感触。想在这里写下来分享给那些像我这样想看这本书却迟迟不敢开始的人们。
1)Java编程思想这本书给我的最大印象是介绍技术比较详细而深入,不仅告诉你怎么做,而且会说为什么Java是这样做的。作者Bruce Eckel也是C++编程思想的作者,对C++和Java的认识都是大师级的,所以他经常会对比Java和C++,告诉你Java都替程序员避免了C++的哪些坑,某些实现Java比C++好在哪里,又差在什么地方。这样的讲述其实就像在跟大师对话一样,不但能学到Java的知识,还能学习大师的思维。
2)看这本书时,一定要把书中的例子源码下载下来,自己真正的跑例子,有什么疑问直接可以通过修改例子验证自己的想法。这比单纯的看书学习效果是要好非常多的。
3)虽然网上很多人说这本书是入门级的,初学者必读。但我不推荐没有Java基础,想学习Java的人通过这本书入门Java。因为这本书讲得太详细,太厚了,很多的内容不只是基础,拿这本书入门可能会令人感到沮丧。找一本两三百页的书,或者从网上看一些Java入门的视频是比较好的选择。
4)上面也说到了,这本书里一些内容不是入门知识,需要对Java有一定的认识以及足够多的研究之后才能弄懂书中的一些内容。对我而言,本书的泛型一章中的一些内容我看得晕晕乎乎的,只能理解泛型的大概,没能再深入。所以,看这本书时,我的建议是:如果觉得书中一些地方挺难懂,也要硬着头皮读下去,书中例子认真研究,至少都看懂,跑一遍。实在不懂的,查资料也搞不懂的,先跳过去,等读第二遍、第三遍的时候说不定会有恍然大悟的感觉。
5)看这本的中间我还看了一本书《深入理解Java虚拟机-JVM高级特性与最佳实践》,收获非常大。而且我通过读《深入》这本书养成了一个好习惯:有些内容读一遍很多不大懂;但第二天读第二遍的时候轻松了不少,理解了不少;过几天再读第三遍的时候发现都能懂了!所以在读《Java编程思想》的时候我也用了这种读书方法,一遍读不懂的,读两遍,过几天再读一遍。这样读下来,发现虽然书读的比较慢,但理解的比较好,收获很大。
6)还有一点是,读书的时候要相互对照,比如阅读《Java编程思想》的持有对象一章的时候,我会对照《深入理解Java虚拟机》一书中的类加载一章以及JVM内存布局与对象创建的章节。这样理解起来,Java语言层面的东西理解了,JVM层面的知识也知道了。效果非常好。
另外,很多人说技术更新太快,读书都是比较过时的东西,最好是去官网看文档。我同意这个观点,我也会去看文档。但是读书还是很有必要的,为什么呢?在我看来,读书不仅能学到想要的技术,更重要的是书里的文字都是作者精心完成的,是作者思想的精华,读好书就像在跟大师交流,你能学习到大师们的思考方式,获得大师们才有的眼界。这是看文档得不到的。当然,不能买太过时的书。尽量买大师写的、最新版的。吐槽一下:坐落于五道口的国内某一流大学里的一个出版社经常出一些技术书,大家要擦亮眼睛,他们很会抄袭,那种书就是为了挣钱。买书尽量买NB的个人的,那是作者的心血,都是精华。一大堆人写的,算了吧,那是书吗?知识的堆积而已,根本没有思想在里面。当然,我不否认那里有NB的人写了NB的书,我只是吐槽一下鸟大了什么林子都有,买书要擦亮双眼。
Java异常处理机制难点解惑-用代码说话
最后更新于:2022-04-01 12:02:00
### 是否需要看这篇文章?
下面的例子中,如果正常执行返回值多少? 如果出现了ArithmeticException返回值多少? 如果出现非ArithmeticException(如NullPointerException)返回值多少?
如果你了解这个例子说明的问题,并了解例子中三种情况下的执行细节,这篇文章你就不用浪费时间看了。
例子:
~~~
public int testException_finally(){
int x;
try {
x = 1;
//int y = 1/0; //放开此处,出现ArithmeticException。
/*//注释掉 int y = 1/0;处,放开此处,出现NullPointerException
String str = null;
str.substring(0);
*/
return x;
} catch (ArithmeticException e) {
x =2;
return x;
} finally{
x = 3;
}
}
~~~
答案:
**如果正常执行,返回值为1;**
**如果抛出ArithmeticException,返回值为2;**
**如果抛出其他Exception,则抛出该Exception,无返回值。**
一看就知晓的同学们,散了吧。这篇文章你们不需要看了。
不知所云的同学们,请继续往下看。下面要说的正适合你☺
### 1. Java异常的分类
Throwable类是Java语言中所有异常和错误的基类。Throwable有两个直接子类:Error和Exception。Error用来表示编译期或系统错误,一般不用我们程序员关心(现在还是程序猿,但有一颗想做架构师的心☺);Exception是可以被抛出的异常的基本类型,这个才是我们需要关心的基类。
Java异常在设计时的基本理念是用名称代表发生的问题,异常的名称应该可以望文知意。异常并非全是在java.lang包中定义,像IO异常就定义在java.io包中。在Exception中,有一种异常:RuntimeException(运行时异常)不需要我们在程序中专门捕获,Java会自动捕获此种异常,RuntimeException及其子类异常再加上Error异常,被统一叫做unecked Exception(非检查异常);其他的Exception及其子类异常(不包括RuntimeException及其子类异常),被统一叫做checked Exception(检查型异常)。对于检查型异常,才是我们需要捕获并处理的异常。非检查型异常Java会自动捕获并抛出。当然,我们也可以主动捕获RuntimeException型异常。但是Error型异常一般不去捕获处理。
### 2. Java异常处理的基本规则
对于可能发生异常的Java代码块,我们可以将其放入try{}中,然后在try之后使用catch(***Exception e){}处理抛出的具体异常,如果没有匹配的catch处理抛出的异常,则会将该异常向上一层继续抛出,直到抛至Main()方法。
有一些代码,我们希望不管try中的代码是成功还是失败都需要执行,那么这些代码我们就可以放在finally{}中。
Java的异常处理采用的是终止模型,即如果try块中的某处出现了异常,则立刻停止当前程序的运行,在堆中创建对应的异常对象,异常的处理转入到异常处理代码处(即对应的catch块),执行完异常处理代码后,try块中出现异常处之后的程序将不会被执行,程序会跳出try块,执行try块之外的程序。
例子:覆盖知识点:①执行对应的catch;②一定执行finally中代码;③try出异常之后的代码不再执行;
~~~
public String testException_one(){
String str = "aaa";
try {
str += "bbb";
int a = 1/0;
str += "ccc";
} catch (NullPointerException e) {
str += "ddd";
} catch (ArithmeticException e) {
str += "eee";
} finally {
str += "fff";
}
str += "ggg";
return str;
}
~~~
程序执行返回结果:aaabbbeeefffggg
注意:没有输出ccc和ddd。
结果分析:上面的程序进入try块后,连接了bbb,然后遇到1/0抛出ArithmeticException 异常,首先NullPointerException所在的catch块不匹配该异常,然后检查到ArithmeticException 所在的catch块匹配该异常,进入该catch块内进行异常处理。执行完所在的catch块,一定会执行finally块,但是try块报异常行之后的代码不会再执行,直接跳出try块,继续执行try…catch…finally之后的代码。
### 3. 继承和实现接口时的异常限制
~~~
class OneException extends Exception{}
class TwoException extends Exception{}
class OneSonException extends OneException{}
class TwoSonException extends TwoException{}
interface Worker {
void work() throws TwoException;
void say() throws OneException;
}
class Person {
public Person() throws TwoException {
System.out.println("Person Constructor...");
}
public void eat() throws OneException {
System.out.println("Person eat...");
}
public void say() throws TwoException {
System.out.println("Person say...");
}
}
public class Coder extends Person implements Worker {
/**
* 此处的TwoException是必须的,因为Person的构造函数中抛出了TwoException。
* Coder在调用父类构造函数时,也必须抛出次异常,且不能是其子类异常.另外,构造函数可以抛出比父类多的异常。
* @throws TwoException
* @throws OneException
*/
public Coder() throws TwoException, OneException {
super();
}
/**
* 实现的接口的方法或者重写的父类的方法可以抛出原方法的异常或其子类异常或者不抛出异常,
* 但是不能抛出原方法没有声明的异常。这样是为了多态时,当子类向上转型为基类执行方法时,基类的方法依然有效。
*/
public void work() throws TwoSonException {
// TODO Auto-generated method stub
}
/**
* 在接口和父类中都有该方法,且异常声明不是同一个异常,则该方法的声明不能抛出任何异常,
* 因为子类中的该方法在多态时必须同时满足其实现的接口和继承的基类的异常要求。不能抛出比基类或接口方法声明中更多的异常。
*/
public void say(){
}
/**
* 基类中eat方法抛出了异常,在子类中覆盖该方法时,可以不声明抛出异常
*/
public void eat(){
}
}
/**同时还应该注意,如果方法声明抛出的是RunTimeException类型的异常,不受以上的限制;
只有检查型异常才受以上限制。非检查型异常由于系统自动捕获,不受任何限制。
*
*/
~~~
### 4. finally一定会执行
①break/continue/while:如下面例子中所示在循环中遇到continue或break时,finally也会执行。
~~~
public void testException_two(){
for(int i = 0; i < 5; i++){
try {
if(i == 0){
continue;
}
if(i == 1){
throw new Exception();
}
if(i == 3){
break;
}
System.out.println("try..." + i);
} catch (Exception e) {
System.out.println("catch..." + i);
} finally {
System.out.println("finally..." + i);
}
}
}
/*
执行结果:
finally...0
catch...1
finally...1
try...2
finally...2
finally...3
*/
~~~
②return:即使在try块中正常执行了return,finally也在return之前执行了。如下面例子所示:
~~~
public void testException_three(){
int a = 1;
try {
System.out.println("try...");
return;
} catch (Exception e) {
// TODO: handle exception
} finally{
System.out.println("finally...");
}
}
/*
执行结果:
try...
finally...
*/
~~~
③还有一种情况是:当try块抛出异常时,如果没有catch块能捕获到该异常,则该异常会被抛至上一级,在被抛至上一级之前,finally块会被执行,然后异常才会被抛至上一级。这个请有兴趣的同学自己验证吧。
总之,finally中的代码是一定会被执行到的。
### 5. finally中丢失异常
因为finally的特殊性,还会造成异常丢失的情况,如果在finally中抛出异常或者在finally中使用了return,则在try块中抛出的异常将会被系统丢掉。如下面代码所示(OneException和TwoException的定义在上面异常限制一节中已经给出):
~~~
public void testException_finally_one(){
try {
System.out.println("test finally...");
try {
if(1 == 1){
throw new OneException();
}
}finally{
throw new TwoException();
}
} catch (Exception e) {
System.out.println("e.getClass: " + e.getClass());
}
}
/*
*
执行结果输出:
test finally...
e.getClass: class com.synnex.demo.TwoException
*/
public void testException_finally_two(){
try {
System.out.println("test finally...");
try {
if(1 == 1){
throw new OneException();
}
}finally{
return;
}
} catch (Exception e) {
System.out.println("e.getClass: " + e.getClass());
}
}
/*
执行结果输出:
test finally...
*/
~~~
### 6. finally造成的返回值困惑
下面进入到本篇开始的那个例子的解惑。
例子:
~~~
public int testException_finally(){
int x;
try {
x = 1;
//int y = 1/0; //放开此处,出现ArithmeticException。
/*//注释掉 int y = 1/0;处,放开此处,出现NullPointerException
String str = null;
str.substring(0);
*/
return x;
} catch (ArithmeticException e) {
x =2;
return x;
} finally{
x = 3;
}
}
~~~
答案:
**如果正常执行,返回值为1;**
**如果抛出ArithmeticException,返回值为2;**
**如果抛出其他Exception,则抛出该Exception,无返回值。**
解惑:这是我根据《深入理解Java虚拟机-JVM高级特性与最佳实践》第二版书中的例子(P187~P188)做了一些修改。出现这种情况的原因是:在没有出现异常的情况下,先执行了x=1;然后执行return x;时,首先是将x的一个副本保存在了方法栈帧的本地变量表中,执行return之前必须执行finally中的操作:x=3;将x的值设置为了3,但是return时是将本地变量表中保存的x的那个副本拿出来放到栈顶返回。故没出异常时,返回值为1;出ArithmeticException异常或其子类异常时,返回值是2;如果出现非ArithmeticException异常,则执行完x=3之后,将异常抛出至上一层,没有返回值。
对字节码命令熟悉的朋友可以使用javap -verbose等命令反编译出该方法的字节码命令和异常表,从字节码层面上就能清晰的看出执行过程了。我对字节码命令知道得还不够多,只能从大体上解释这种运行过程。以后字节码命令学得自认为可以了的时候,也会写字节码相关的文章出来。希望这篇文章能帮到一些人理解Java的异常处理机制。
参考文章及书籍:
[ Java异常的深入研究与分析](http://blog.csdn.net/rainnnbow/article/details/48497193)
《Java编程思想》第四版中文版第十二章通过异常处理错误
《深入理解Java虚拟机-JVM高级特性与最佳实践》第二版 第六章类文件结构 周志明著
java自动装箱拆箱总结
最后更新于:2022-04-01 12:01:58
对于java1.5引入的自动装箱拆箱,之前只是知道一点点,最近在看一篇博客时发现自己对自动装箱拆箱这个特性了解的太少了,所以今天研究了下这个特性。以下是结合测试代码进行的总结。
### 测试代码:
~~~
int a = 1;
Integer b = 1;
Integer c = 1;
Integer d = 2;
Integer e = 3;
Integer f = 128;
Integer g = 128;
Long h = 3L;
Double m = 4.0;
Double n = 4.0;
Float p = 5f;
Float q = 5f;
System.out.println("a == b : " + (a == b)); //true
System.out.println("b ==c : " + (b == c)); //true
System.out.println("e == (c + d) : " + (e == (c + d))); //true
System.out.println("e.equals(c + d) : " + (e.equals(c + d))); //true
System.out.println("h == (c + d) : " + (h == (c + d))); //true
System.out.println("h.equals(c + d) : " + (h.equals(c + d))); //false
System.out.println("f == g : " + (f == g)); //false
System.out.println("m == n : " + (m == n)); //false
System.out.println("p == q : " + (p == q)); //false
System.out.println("m == d * 2 : " + (m == d * 2)); //true
System.out.println("p == (d + e) : " + (p == (d + e))); //true
~~~
**测试输出结果与说明:**
~~~
1. a == b : true
当基本类型包装类与基本类型值进行==运算时,包装类会自动拆箱。即比较的是基本类型值。
具体实现上,是调用了Integer.intValue()方法实现拆箱。
可以在测试代码处打断点,使用F5快捷键step
into至每一步执行方法,会看到调用了Integer.intValue()方法实现了拆箱。
2. b == c : true
b和c类型均为Integer包装类,故对基本类型进行自动装箱后赋值给b和c。
在进行==运算时不会触发拆箱操作。所以比较的是引用地址,说明b和c是同一个对象。
java中自动装箱调用的是Integer.valueOf()方法实现的,可以像例1一样打断点验证。
为什么b和c 会是同一个对象呢?查看Integer.valueOf()方法实现即可知道,如下:
下面的代码中可以看到,对于-128至127这256个值,直接获取的IntegerCache中的值。
而IntegerCache是Integer中的一个静态内部类,
里面将-128至127(即一个字节所能表示的所有带符号值 -2^7至2^7-1)的包装类存在了一个数组中。
对于-128到127之间的数,直接从数组中获取,其他的数则使用new生成。所以此处输出true。
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
3. e == (c + d) : true
包装类在执行加减乘除求余等运算时,会触发拆箱操作,故c、d拆箱后相加,
结果为基本类型;然后e与基本类型进行==运算,触发拆箱操作。
4. e.equals(c + d) : true
首先,c + d 拆箱运算得到基本类型值;然后当进行equals运算时,
会触发基本类型值的装箱操作,c + d 的结果会自动装箱为包装类;最后与e进行equals运算。
5. h == (c + d) : true
运算顺序与上面例3中一致,唯一区别是h自动拆箱是调用了Long.longValue()实现。
6. h.equals(c + d) : false
运算顺序与上面例4中一致,为false的原因在于c + d的运算结果自动装箱后类型为Integer,
而h的类型为Long,类型不一样,equals的结果为false。
7. f == g : false
请参考上面例2中的解释。
超出了-128至127的缓存范围,故在valueOf()方法中使用new生成了新对象。
8. m == n : false
p == q : false
与上面例子1、2中的结果不同,对于Boolean、Byte、Character、Short、Integer、Long六种基本类型,
对于一个字节以内的值-128到127(Boolean只有true和false)都实现了缓存机制。
不在此范围的数才在对应的valueOf()方法中new出一个新的对象。
但是对于Double和Float类型的浮点数据,在-128到127之间除了256个整数外还有无数的小数,
故java中没有实现Double和Float中一些数的缓存。
所以,对于Double和Float的自动装箱,都是new出新的对象。故此两例均输出false。
10. m == d * 2 : true
p == (d + e) : true
请参考例3和例5。
~~~
### 反编译后代码:
使用java反编译工具,对class字节码文件进行反编译,结果如下所示。从下面的反编译代码,我们可以看到java是如何实现自动装箱、拆箱的。
~~~
int a = 1;
Integer b = Integer.valueOf(1);
Integer c = Integer.valueOf(1);
Integer d = Integer.valueOf(2);
Integer e = Integer.valueOf(3);
Integer f = Integer.valueOf(128);
Integer g = Integer.valueOf(128);
Long h = Long.valueOf(3L);
Double m = Double.valueOf(4.0D);
Double n = Double.valueOf(4.0D);
Float p = Float.valueOf(5.0F);
Float q = Float.valueOf(5.0F);
System.out.println("a == b : " + (a == b.intValue()));
System.out.println("b ==c : " + (b == c));
System.out.println("e == (c + d) : " + (e.intValue() == c.intValue() + d.intValue()));
System.out.println("e.equals(c + d) : " + e.equals(Integer.valueOf(c.intValue() + d.intValue())));
System.out.println("h == (c + d) : " + (h.longValue() == c.intValue() + d.intValue()));
System.out.println("h.equals(c + d) : " + h.equals(Integer.valueOf(c.intValue() + d.intValue())));
System.out.println("f == g : " + (f == g));
System.out.println("m == n : " + (m == n));
System.out.println("p == q : " + (p == q));
System.out.println("m == d * 2 : " + (m.doubleValue() == d.intValue() * 2));
System.out.println("p == (d + e) : " + (p.floatValue() == d.intValue() + e.intValue()));
~~~
参考文章:
[ ①Java 自动装箱和拆箱](http://blog.csdn.net/jackiehff/article/details/8509056)
[②Java自动装箱与拆箱及其陷阱](http://blog.csdn.net/jairuschan/article/details/7513045)
[③深入剖析Java中的装箱和拆箱](http://www.cnblogs.com/dolphin0520/p/3780005.html)
[④四道Java基础题 你能对几道?](http://blog.csdn.net/soul_code/article/details/50369947)
String放入运行时常量池的时机与String.intern()方法解惑
最后更新于:2022-04-01 12:01:56
### 运行时常量池概述
Java运行时常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(包名+类名)
- 字段的名称和描述符
- 方法的名称和描述符
### 运行时常量池位置
运行时常量池在**JDK1.6及之前版本的JVM中是方法区的一部分**,而在HotSpot虚拟机中方法区放在了”永久代(Permanent Generation)”。所以运行时常量池也是在永久代的。
但是**JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池**。
**本文主要解惑String对象(即文本字符串)何时放入常量池,不涉及上述三类符号引用常量和其他非String常量值。而且本文只讨论主流的HotSpot虚拟机。**
### String何时放入常量池
记住一句话:**直接使用双引号声明出来的String对象会直接存储在常量池中。**
### 代码一:
~~~
String a = "计算机软件";
~~~
分析:因为计算机软件五个字直接使用了双引号声明,故JVM会在运行时常量池中首先查找有没有该字符串,有则直接返回该字符串在常量池中的引用;没有则直接在常量池中创建该字符串,然后返回引用。**此时,该句代码已经执行完毕,不会在java Heap(堆)中创建内容相同的字符串。该字符串只在常量池中创建了一个String对象。**
### 代码二:
~~~
String a = new String("计算机软件");
~~~
分析:该行代码生成了两个String对象(Stack(栈)中的对象引用不在讨论范围内):第一步,因为计算机软件五个字直接使用了双引号声明,故JVM会在运行时常量池中首先查找有没有该字符串,有则进入第二步;没有则直接在常量池中创建该字符串,然后进入第二步。第二步:在常量池中创建了一个String对象之后,由于使用了new,JVM会在Heap(堆)中创建一个内容相同的String对象,然后返回堆中String对象的引用。**该行代码分别在常量池和堆中生成了两个内容相同的String对象。**
### 代码三:
~~~
String a = "计算机" + "软件";
~~~
分析:由于JVM存在编译期优化,对于两个直接双引号声明的String的+操作,JVM在编译期会直接优化为“计算机软件”一个字符串,故该行代码同**代码一**。
### 代码四:
~~~
String b = "计算机";
String a = b + "软件";
~~~
分析:由于b是一个String变量,编译期无法确定b的值,故不会优化为一个字符串。即使我们知道b的值,但JVM认为它是个变量,变量的值只能在运行期才能确定,故不会优化。运行期字符串的+连接符相当于new,故该行代码在Heap中创建了一个内容为“计算机软件”的String对象,并返回该对象的引用。**至此,该代码执行完毕,因为没有直接双引号声明计算机软件这5个字的字符串,故常量池中不会生成计算机软件这5个字的字符串。但是会有“计算机”和“软件”这两个String对象,因为他们都用双引号声明了。**
### 代码五:
~~~
String final b = "计算机";
String a = b + "软件";
~~~
分析:该代码与代码四的唯一区别是将b声明为final类型,即为常量。故在编译期JVM能确定b的值,所以对+可以优化为“计算机软件”5个字的字符串。该代码的运行同**代码三和代码一**。
### 代码六:
~~~
String a = new String("计算机") + "软件";
~~~
分析:因为有new,该代码也无法编译期优化,故该行代码只是在Heap中生成了“计算机软件”字符串的String对象,在常量池中没有内容相同的对象生成。
### String.intern方法
### 概述
String.intern()是一个Native方法,它的作用是:如果运行时常量池中已经包含一个等于此String对象内容的字符串,则返回常量池中该字符串的引用;如果没有,则在常量池中创建与此String内容相同的字符串,并返回常量池中创建的字符串的引用。
### JDK1.7改变
当常量池中没有该字符串时,JDK7的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为**在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用**。
**验证代码:**
~~~
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println((str1.intern() == str1));
//JDK1.6:false
//JDK1.7:true
~~~
或
~~~
String b = "计算机";
String a = b + "软件";
System.out.println(a.intern() == a);
//JDK1.6:false
//JDK1.7:true
~~~
### 测试代码
请运行以下的代码看看你分析的结果和真正的运行结果是否一样,JDK1.6和1.7都要跑一遍,如果你都分析对了,那就是理解了。
~~~
//一次放开一个多行注释运行
/*
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
*/
/*
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
*/
/*
//+连接但编译器不优化
String s1=new String("xy") + "z";
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
*/
/*// 一般情况
String s1=new String("xyz") ;
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
*/
/*//编译器优化
String s1 = "xy" + "z";
String s2 = s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
*/
~~~
说明:本文有部分内容摘抄了周志明大神的《深入理解Java虚拟机》一书,部分代码参考了网上多个博客的内容,仅用于学习。无意侵犯版权。
参考:
《深入理解Java虚拟机-JVM高级特性与最佳实践》周志明著
[ JVM常量池和八种基本数据及字符串](http://blog.csdn.net/rainnnbow/article/details/47018241)
[深入解析String#intern](http://www.importnew.com/14142.html)
[Java永久代去哪儿了](http://www.infoq.com/cn/articles/Java-PERMGEN-Removed)
java.util.Arrays类详解(源码总结)
最后更新于:2022-04-01 12:01:54
### 概述
Arrays类位于java.util包下,是一个对数组操作的工具类。今天详细的看了看Arrays类的4千多行源码,现将Arrays类中的方法做一个总结(JDK版本:1.6.0_34)。Arrays类中的方法可以分为八类:
- sort(对数组排序)
- binarySearch(二分法查找数组中的元素)
- equals(比较两个数组是否相等)
- fill(对数组中的指定位置填充相同的内容)
- copyOf(数组拷贝)
- asList(将数组转换为一个固定的List对象)
- hashCode(计算数组的哈希值)
- toString(以特定格式输出数组)
### 举例说明
**说明:以下的代码均为摘抄的java.util.Arrays类中的源码,注释为本人所加。**
### sort
~~~
//对数组a进行排序
public static void sort(long[] a) {
sort1(a, 0, a.length);
}
//对数组a中的从fromIndex(包含)至toIndex(不包含)的值进行排序
public static void sort(long[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
sort1(a, fromIndex, toIndex-fromIndex);
}
/**
对基本类型数组的排序有以上两种方法,这里只摘出了long类型的。sort1方法篇幅原因没有摘出来,在sort1方法中使用的是经过调优的快速排序算法(tuned quicksort)。
**/
..........
..........
..........
//对对象类型进行排序
public static void sort(Object[] a) {
Object[] aux = (Object[])a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
//对对象a中的从fromIndex(包含)至toIndex(不包含)的值进行排序
public static void sort(Object[] a, int fromIndex, int toIndex) {
rangeCheck(a.length, fromIndex, toIndex);
Object[] aux = copyOfRange(a, fromIndex, toIndex);
mergeSort(aux, a, fromIndex, toIndex, -fromIndex);
}
/**
对对象类型数组的排序有以上两种方法,在mergeSort方法中使用的是经过修改的归并排序算法(modified mergesort)。
**/
~~~
### binarySearch
~~~
public static int binarySearch(long[] a, long key) {
return binarySearch0(a, 0, a.length, key);
}
public static int binarySearch(long[] a, int fromIndex, int toIndex,
long key) {
rangeCheck(a.length, fromIndex, toIndex);
return binarySearch0(a, fromIndex, toIndex, key);
}
/**
对数组中元素的查找有以上两种方法,在binarySearch0方法中使用的是二分查找法。并且对基本类型和对象类型的数组查找是同样的操作。
**/
~~~
### equals
~~~
//比较基本类型数组是否相等
public static boolean equals(long[] a, long[] a2) {
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
/**
对于double类型,使用的是:
if (Double.doubleToLongBits(a[i])!=Double.doubleToLongBits(a2[i]))
return false;
对于float类型,使用的是:
if (Float.floatToIntBits(a[i])!=Float.floatToIntBits(a2[i]))
return false;
这样做是为了精确比较。
**/
return true;
}
.....
.....
.....
//比较Object类型数组是否相等
public static boolean equals(Object[] a, Object[] a2) {
if (a==a2)
return true;
if (a==null || a2==null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++) {
Object o1 = a[i];
Object o2 = a2[i];
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return true;
}
.....
.....
.....
//深度比较两个数组是否相等
public static boolean deepEquals(Object[] a1, Object[] a2) {
if (a1 == a2)
return true;
if (a1 == null || a2==null)
return false;
int length = a1.length;
if (a2.length != length)
return false;
for (int i = 0; i < length; i++) {
Object e1 = a1[i];
Object e2 = a2[i];
if (e1 == e2)
continue;
if (e1 == null)
return false;
// Figure out whether the two elements are equal
boolean eq;
if (e1 instanceof Object[] && e2 instanceof Object[])
eq = deepEquals ((Object[]) e1, (Object[]) e2);
else if (e1 instanceof byte[] && e2 instanceof byte[])
eq = equals((byte[]) e1, (byte[]) e2);
else if (e1 instanceof short[] && e2 instanceof short[])
eq = equals((short[]) e1, (short[]) e2);
else if (e1 instanceof int[] && e2 instanceof int[])
eq = equals((int[]) e1, (int[]) e2);
else if (e1 instanceof long[] && e2 instanceof long[])
eq = equals((long[]) e1, (long[]) e2);
else if (e1 instanceof char[] && e2 instanceof char[])
eq = equals((char[]) e1, (char[]) e2);
else if (e1 instanceof float[] && e2 instanceof float[])
eq = equals((float[]) e1, (float[]) e2);
else if (e1 instanceof double[] && e2 instanceof double[])
eq = equals((double[]) e1, (double[]) e2);
else if (e1 instanceof boolean[] && e2 instanceof boolean[])
eq = equals((boolean[]) e1, (boolean[]) e2);
else
eq = e1.equals(e2);
if (!eq)
return false;
}
return true;
}
~~~
### fill
~~~
//使用val对a数组进行数据填充
public static void fill(long[] a, long val) {
fill(a, 0, a.length, val);
}
//使用val对a数组从fromIndex(包含)至toIndex(不包含)位置进行数据填充
public static void fill(long[] a, int fromIndex, int toIndex, long val) {
rangeCheck(a.length, fromIndex, toIndex);
for (int i=fromIndex; i<toIndex; i++)
a[i] = val;
}
~~~
### copyOf
~~~
//拷贝从0开始的newLength个
public static byte[] copyOf(byte[] original, int newLength) {
byte[] copy = new byte[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
.....
.....
.....
//拷贝从from(包含)位置到to(不包含)的元素
public static byte[] copyOfRange(byte[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
byte[] copy = new byte[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
/**
拷贝方法有以上两种,对于基本类型和对象操作是一样的。而System.arraycopy()方法为浅拷贝,故如果是对象数组的拷贝,只是拷贝了对象的引用,没有重新new每一个对象。
**/
~~~
### asList
没有深究。。。。。。
### hashCode
~~~
//对基本数据类型的hashcode的计算
public static int hashCode(long a[]) {
if (a == null)
return 0;
int result = 1;
for (long element : a) {
//对于不同的基本数据类型,计算hashcode的具体操作是不一样的
int elementHash = (int)(element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
....
....
....
//对于Object类型数组的hashcode的计算
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
....
....
....
//对于数组的hashcode值的深度计算
public static int deepHashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a) {
int elementHash = 0;
if (element instanceof Object[])
elementHash = deepHashCode((Object[]) element);
else if (element instanceof byte[])
elementHash = hashCode((byte[]) element);
else if (element instanceof short[])
elementHash = hashCode((short[]) element);
else if (element instanceof int[])
elementHash = hashCode((int[]) element);
else if (element instanceof long[])
elementHash = hashCode((long[]) element);
else if (element instanceof char[])
elementHash = hashCode((char[]) element);
else if (element instanceof float[])
elementHash = hashCode((float[]) element);
else if (element instanceof double[])
elementHash = hashCode((double[]) element);
else if (element instanceof boolean[])
elementHash = hashCode((boolean[]) element);
else if (element != null)
elementHash = element.hashCode();
result = 31 * result + elementHash;
}
return result;
}
~~~
### toString
~~~
//基本数据类型转字符串
public static String toString(long[] a) {
if (a == null)
return "null";
int iMax = a.length - 1;
if (iMax == -1)
return "[]";
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++) {
b.append(a[i]);
if (i == iMax)
return b.append(']').toString();
b.append(", ");
}
}
.....
.....
.....
//Object类型使用valueOf方法转字符串
public static String toString(Object[] a) {
if (a == null)
return "null";
int iMax = a.length - 1;
if (iMax == -1)
return "[]";
StringBuilder b = new StringBuilder();
b.append('[');
for (int i = 0; ; i++) {
b.append(String.valueOf(a[i]));
if (i == iMax)
return b.append(']').toString();
b.append(", ");
}
}
.....
.....
.....
//深度转换字符串
public static String deepToString(Object[] a) {
if (a == null)
return "null";
int bufLen = 20 * a.length;
if (a.length != 0 && bufLen <= 0)
bufLen = Integer.MAX_VALUE;
StringBuilder buf = new StringBuilder(bufLen);
deepToString(a, buf, new HashSet());
return buf.toString();
}
private static void deepToString(Object[] a, StringBuilder buf,
Set<Object[]> dejaVu) {
if (a == null) {
buf.append("null");
return;
}
dejaVu.add(a);
buf.append('[');
for (int i = 0; i < a.length; i++) {
if (i != 0)
buf.append(", ");
Object element = a[i];
if (element == null) {
buf.append("null");
} else {
Class eClass = element.getClass();
if (eClass.isArray()) {
if (eClass == byte[].class)
buf.append(toString((byte[]) element));
else if (eClass == short[].class)
buf.append(toString((short[]) element));
else if (eClass == int[].class)
buf.append(toString((int[]) element));
else if (eClass == long[].class)
buf.append(toString((long[]) element));
else if (eClass == char[].class)
buf.append(toString((char[]) element));
else if (eClass == float[].class)
buf.append(toString((float[]) element));
else if (eClass == double[].class)
buf.append(toString((double[]) element));
else if (eClass == boolean[].class)
buf.append(toString((boolean[]) element));
else { // element is an array of object references
if (dejaVu.contains(element))
buf.append("[...]");
else
deepToString((Object[])element, buf, dejaVu);
}
} else { // element is non-null and not an array
buf.append(element.toString());
}
}
}
buf.append(']');
dejaVu.remove(a);
}
~~~
Java的多态及注意事项
最后更新于:2022-04-01 12:01:51
## 什么是多态:
多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序。在Java中,所有的方法都是通过动态绑定实现多态的。将一个方法调用同一个方法主体关联起来被称作绑定。动态绑定的含义是在运行时根据对象的类型进行绑定。动态绑定也叫作后期绑定或运行时绑定。Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定——它会自动发生。也就是说,在Java中,只有普通的方法是多态的,static方法、private方法、final方法以及成员变量都不是多态的,都属于编译期绑定。
## 注意事项一:“覆盖”私有方法
例子:
~~~
public class PrivateOverride {
private void f(){
System.out.println("private f()");
}
public static void main(String[] args){
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride{
public void f(){
System.out.println("public f()");
}
}
~~~
输出:
~~~
private f()
~~~
说明:对于private方法来说,它默认是final的,不允许被更改,导出类看不到private方法,不能被继承,也就没有“覆盖(override)”这一说。当导出类有一个同名的符合覆盖规则的方法时,其实导出类中的该方法是一个全新的方法。但是当我们试图对私有方法进行覆盖并使用多态时,虽然编译器不会报错,但私有方法不支持多态,最终调用的是基类中的方法。
## 注意事项二:域与静态方法不支持多态
例子:
~~~
class Super{
public int field = 0;
public int getField(){
return field;
}
public static String say(){
return "Super static function.";
}
}
class Sub extends Super{
public int field = 1;
public int getField(){
return field;
}
public int getSuperField(){
return super.field;
}
public static String say(){
return "Sub static function.";
}
}
public class FieldAccess{
public static void main(String[] args){
Super sup = new Sub();
System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
System.out.println("sup.say = " + sup.say());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField());
System.out.println("sub.say = " + sub.say());
}
}
~~~
输出:
~~~
sup.field = 0, sup.getField() = 1
sup.say = Super static function.
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
sub.say = Sub static function.
~~~
说明:对于域和静态方法,是不支持多态的,他们都属于前期绑定,即在编译期实现的绑定。故域和静态方法只能绑定到基类的域和方法,不支持动态绑定。
(以上代码来自Java编程思想第四版,说明来自Java编程思想第四版中关于多态的章节的整理,仅用于学习和交流)
Java继承时的初始化顺序
最后更新于:2022-04-01 12:01:49
Java程序在启动和运行时,需要首先完成初始化的工作。在涉及到继承、static成员变量等因素时,初始化的顺序就复杂起来。下面以一个例子说明继承时的Java初始化顺序。
## 例子:
~~~
class Insect{
private int i = 9;
protected int j;
Insect(){
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 = printInit("static Insect.x1 initialized");
static int printInit(String s){
System.out.println(s);
return 47;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle(){
System.out.println("k = " +k);
System.out.println("j = " + j);
}
private static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args){
System.out.println("Beetle constructor");
Beetle b = new Beetle();
}
}
~~~
输出:
~~~
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
~~~
## 初始化详细说明:
在Beetle上运行Java时,所发生的第一件事情就是试图访问Beetle.main()(一个static方法), 于是加载器开始启动并找出Beetle类的编译代码(在名为Beetle.class的文件中)。在对它进行加载的过程中, 编译器注意到它有一个基类(这是由关键字extends得知的),于是它继续进行加载。不管你是否打算残生一个该基类的对象,这都要发生( 请尝试将对象创建代码注释掉,以证明这一点)。如果该基类还有其自身的基类,那么第二个基类就会被加载,如此类推。
接下来,根基类中的static初始化(在此例中为Insect)即会被执行,然后是下一个导出类,以此类推。
这种方式很重要,因为导出类的static初始化可能会依赖于基类static成员能否被初始化。
至此为止,必要的类都已加载完毕,对象就可以被创建了。首先是基类对象的创建,然后是下一个导出类对象创建,以此类推。
对于每一个对象的创建,首先对象中所有的基本类型都会被设为默认值,对象引用被设为null——这是通过将对象内存设为二进制零值而一举生成的。
然后对象中的成员的显示的初始化被执行。然后构造器会被调用,对象创建完毕。
(以上例子和说明来自于Java编程思想第四版,说明部分有一些是自己修改的,仅用于学习交流之用)
ArrayList、HashMap、HashSet源码总结
最后更新于:2022-04-01 12:01:47
## ArrayList:
1. ArrayList是List接口的大小可变数组的实现,此实现是不同步的。
2. ArrayList内部使用类型为Object[]的数组存储元素。
3. ArrayList默认的数组长度为10, 当需要扩大容量时,扩大后的容量为:newCapacity = (oldCapacity * 3)/2 + 1;
4. ArrayList的clone方法为浅拷贝(shallow copy)
5. ArrayList的remove方法根据参数类型的不同有两种重载:
remove(int index) : 删除指定位置的元素;
remove(Object o) : 删除第一个遇到的元素,如果没有不做改变
6. ArrayList允许null值、允许重复值、不排序,获取快速,增删麻烦。
## HashMap:
HashMap是不同步的。
HashMap内部使用类型为Entry[]的数组存储元素。Entry是HashMap的一个内部类,定义如下所示。
每一个Entry对象其实是一个单向链表,之后的解析可以看到,最后存入的元素在最前面。
备注:下面出现的代码都是HashMap.java中的源码,中文描述是作者加的。
~~~
transient Entry[] table;//HashMap内部定义的数据存储变量
//内部类
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中几个概念:
capacity:容量,即Entry[]数组的长度
loadFactor:负载因子,Entry[]数组中实际数据量/容量的比例达到loadFactor时,HashMap就需要扩大容量了,一般扩大为原来的两倍。
threshold: 当HashMap中的元素个数超过这个数值时,就将扩大容量。
### put方法:
~~~
public V put(K key, V value) {
if (key == null)
//如果key为null,特殊处理,key为null直接存储在table[0]位置。
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);//此处得到的i即为key对应的HashMap中的存储位置table[i]
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//从Entry链表的第一个开始如果找到与key执行equals方法为true的Entry,则修改对应Entry的value值为新值,key不做修改
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++;
//如果没有找到对应的key,则执行增加操作
addEntry(hash, key, value, i);
return null;
}
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)
//如果大小超过了threshold,扩大容量为原来的两倍。扩大容量时,所有的key-value需要重新hash。
resize(2 * table.length);
}
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);//将原来hash表中的数据放入新的hash表中,需要重新hash。
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
/**
* 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;
//此处使用循环,将原来hash链中的所有的key-value都重新获取hash值,重新放置。
//因为放置位置是跟hash表的大小有关的,当hash表容量扩大后,之前放在一个地方的key-value对现在可能hash不到同一个地方了。
do {
Entry<K,V> next = e.next;//记录此处的下一个地址
int i = indexFor(e.hash, newCapacity);//重新计算当前的key-value在新hash表中的位置
e.next = newTable[i];//将之前在同一位置的数据放在e的next位置,没有则为null
newTable[i] = e;//将e作为hash表i位置的第一个元素
e = next;//将next赋值给e, 对原来j位置的所有的元素都执行重新hash,重新放置
} while (e != null);
}
}
}
~~~
get方法:按照put时的逻辑根据key获取value。不再详述。
### keySet与values方法:
这两个方法作用好理解,但需要注意的是,当对keySet()和values()方法获取到的集合执行remove操作的时候就相当于对HashMap集合本身执行remove操作。看源码通过keySet和values获取到的好像是HashMap的迭代器,这里我没有深究。如果谁明白具体原因不吝赐教。
# HashSet:
HashSet的内部是用的HashMap实现的,使用Entry将每一个HashSet元素的引用存储在key位置,value位置使用默认的数据填充。
在此也可以看到,HashMap中的key-value对其实可以看成value是每一个key的附属,只需要找到每一个key的位置,然后把对应的value放入即可。
Java遍历时删除List、Set、Map中的元素(源码分析)
最后更新于:2022-04-01 12:01:45
在对List、Set、Map执行遍历删除或添加等改变集合个数的操作时,不能使用普通的while、for循环或增强for。会抛出ConcurrentModificationException异常或者没有达到删除的需求。在遍历时删除元素,需要使用迭代器的方式。
## ArrayList源码中说明的报异常原因:
~~~
* <p>The iterators returned by this class's <tt>iterator</tt> and
* <tt>listIterator</tt> methods are <i>fail-fast</i>: if the list is
* structurally modified at any time after the iterator is created, in any way
* except through the iterator's own <tt>remove</tt> or <tt>add</tt> methods,
* the iterator will throw a {@link ConcurrentModificationException}. Thus, in
* the face of concurrent modification, the iterator fails quickly and cleanly,
* rather than risking arbitrary, non-deterministic behavior at an undetermined
* time in the future.<p>
~~~
(翻译:通过类的iterator和listiterator方法获取到的迭代器是快速失败迭代器:如果list在迭代器生成之后发生了结构性的改变,迭代器将抛出ConcurrentModificationException,**但是当使用迭代器自己的remove或add方法时,不会抛出此异常。**也就是说,当面对并发修改时,迭代器快速失败,而不是冒在未来不确定的时间发生不确定的行为的危险。)
~~~
* Note that the fail-fast behavior of an iterator cannot be guaranteed
* as it is, generally speaking, impossible to make any hard guarantees in the
* presence of unsynchronized concurrent modification. Fail-fast iterators
* throw <tt>ConcurrentModificationException</tt> on a best-effort basis.
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>the fail-fast behavior of iterators
* should be used only to detect bugs.</i><p>
~~~
(翻译:需要注意的是迭代器**不保证快速失败行为一定发生**,因为一般来说不可能对是否发生了不同步并发修改做任何硬性的保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException异常。因此,写一个通过是否出现这种异常来判断是否正确的程序是错误的。快速失败行为的正确用法是仅用于检测异常。)
## 代码示例:
~~~
public class CollectionRemoveDemo {
public static void main(String[] args) {
ListRemove();
System.out.println("-----------------------------------------------------------------------------------------------");
SetRemove();
System.out.println("-----------------------------------------------------------------------------------------------");
MapRemove();
}
public static void ListRemove(){
List<String> strList = new ArrayList<String>();
strList.add("aaaa");
strList.add("bbbb");
strList.add("cccc");
strList.add("cccc");
strList.add("dddd");
for(String str : strList){
System.out.println(str);
}
System.out.println("init List size:" + strList.size());
Iterator<String> it = strList.iterator();
while(it.hasNext()){
String str = it.next();
if(str.equals("cccc")){
it.remove();
}
}
for(String str : strList){
System.out.println(str);
}
System.out.println("removed List size:" + strList.size());
}
public static void SetRemove(){
Set<String> strSet = new TreeSet<String>();
strSet.add("aaaa");
strSet.add("bbbb");
strSet.add("cccc");
strSet.add("cccc");//重复的数据将不会再次插入
strSet.add("dddd");
for(String str : strSet){
System.out.println(str);
}
System.out.println("Init Set size:" + strSet.size());
Iterator<String> it = strSet.iterator();
while(it.hasNext()){
String str = it.next();
if(str.equals("cccc")){
it.remove();
}
}
for(String str : strSet){
System.out.println(str);
}
System.out.println("removed Set size:" + strSet.size());
}
public static void MapRemove(){
Map<String, String> strMap = new TreeMap<String, String>();
strMap.put("a", "aaaa");
strMap.put("b", "bbbb");
strMap.put("c", "cccc");
strMap.put("d", "dddd");
for(String key : strMap.keySet()){
System.out.println(key + " : " + strMap.get(key));
}
System.out.println("Init Map size:" + strMap.size());
Iterator<Entry<String,String>> it = strMap.entrySet().iterator();
while(it.hasNext()){
Entry<String,String> strEntry = it.next();
if(strEntry.getKey().equals("c")){
it.remove();
}
}
for(String key : strMap.keySet()){
System.out.println(key + " : " + strMap.get(key));
}
System.out.println("removed Map size:" + strMap.size());
}
}
~~~
前言
最后更新于:2022-04-01 12:01:42
> 原文出处:[Java基础知识日常总结](http://blog.csdn.net/column/details/rainnnbow-javabase.html)
> 作者:[rainnnbow](http://blog.csdn.net/rainnnbow)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# Java基础知识日常总结
> 该专栏是本人在日常工作中对Java的基础知识的学习和总结。每篇博客会针对Java中的一个具体点进行分析和总结,这样的点积累多了,对Java的基础知识就慢慢掌握了。随着本人对Java学习的深入,文章涉及的技术深度也会增加。但基础是深入的前提,开通此专栏也是想激励自己不断进步。