1.Java的数学运算都是在栈中进行的
2.基本类型和基本类型的包装类。基本类型有:byte、short、char、int、long、boolean。基本类型的包装类分别是:Byte、Short、Character、Integer、Long、Boolean。注意区分大小写。二者的区别是:基本类型体现在程序中是普通变量,基本类型的包装类是类,体现在程序中是引用变量。因此二者在内存中的存储位置不同:基本类型存储在栈中,而基本类型包装类存储在堆中。上边提到的这些包装类都实现了常量池技术,另外两种浮点数类型的包装类则没有实现。另外,String类型也实现了常量池技术。(常量池里如果有需要的string,则直接用,否则往常量池里加新的)
3. 以上提到的几种基本类型包装类均实现了常量池技术,但他们维护的常量仅仅是【-128至127】这个范围内的常量,如果常量值超过这个范围,就会从堆中创建对象,不再从常量池中取。
4. String类型也实现了常量池技术,但是稍微有点不同。String型是先检测常量池中有没有对应字符串,如果有,则取出来;如果没有,则把当前的添加进去。
5. 冲突类处理:如果你import的2个以上的包都有同一个名称的类,在你使用这个名称的类时,你应该明确指定它来自哪里。如:java.util.Vector v = new Java.util.Vector();
6. privata关键字: 除了包含该成员的类之外,其他任何类都无法访问这个成员。任何可以肯定只是该类的助手的方法,都可以指定它为private的。
7. protected关键字:主要用于继承,但是它也实现了包访问权限,即包内的其他类也可以访问protected元素。一般都是把域作为private的,而把父类相关方法设置为protected的,这是一种机制。
8.封装的概念:访问权限的控制常被称为具体实现的隐藏。把数据和方法包装进类中,加上具体实现的隐藏,常共同被称作封装。
9.类访问权限:
a.)类只有public和包访问权限,无private和protected.
b.)一个class文件只能有1个public class,且文件名必须和这个public class完全相同。
c.)如果你不想别人生成这个类对象,你可以把这个类的构造器指定为private的,这样别人就无法应用了。
d.)如果你不想别人随意生成这个类对象,又可以使用这个类的话,有2中方法:
1.)class A{
private A(){}
public static A makeA(){
return new A();
}
}
2.)class A{
private static A a = new A();
private A(){}
public static A getA(){
return a;
}
}
这是一种单例模式。
没有public修饰的类都是包访问权限的类。
10.Java中初始化的地方 :
A.)定义对象的时候;
B.)构造器里;
C.)惰性初始化 例如if(string ==“Test”);
D.)实例初始化(初始化块);
11.关于继承的规则:
在Java中,如果父类成员没有生命访问权限,那么只有在同1个包中的子类可以使用父类 成员,而其他包中的子类只能使用父类中生命了public的成员,因此,有一个很有用的规则如下。
通常情况下:我们应该做的是父类里的数据成员都指定为private的,而方法应该都指定public的(具体看你的应用场景。)
12.关于生成子类对象时,父类对象的问题:
当你生成1个子类对象的时候,这个子类对象其实包含了1个父类的子对象,这个子对象和你直接创建的对象是一样的。二者区别在于后者来自于外部,而前者的自对象被包装在子类对象的内部。
13.带参数的父类构造器:
如果你的父类构造器没有默认无参数的构造器,而是有参构造器,那么你的子类必须用super关键字显式的调用基类构造器,否则,编译器会显示找不到父类构造器,
并且你在子类构造器中第一件应该做的事就是调用父类带参构造器。
14.复用代码的几种形式:
a) 组合;
b) 继承;
c) 代理; 例如 class playerControl{
Public void play(String what){}
Public void start(String what){}
}
Class playerProxy{
playerControl control = new playerControl();
Public void play(String what){
Control.play(what);
}
Public void start(String what){
Control.start(what);
}
}
代理的优势可以提供想要的方法子集,而不用全盘接受。
比如说我们有1个类是Player,它有玩足球的一系列相关方法,它有玩篮球、羽毛球、游泳的一系列相关方法。但是我们可能只是需要1个BasketBallPlayer,它不应该有关于玩足球、羽毛球的方法。因此,如果通过继承来构造我们的BasketBallPlayer.class,这显得很不合理,我们应该利用代理,让我们的新类只拥有某个方法子集就好。
15.finally的子句的意思是无论如何,这部分代码一定要执行。
16.@Override注解可以防止我们在不想重载的时候进行了重载。
17.正常情况下,大部分采用组合的形势,但是如果要使用继承,应该考虑一下:
自己是否需要新的子类向基类进行向上转型,如果需要向上转型,则继承是必要的。
final关键字相关:
18.final修饰的四元素: a.) 成员变量; b.)方法; c.)类; d.)本地变量(方法中的或者代码块中的变量)。
19.使用final修饰数据的2种可能原因:
a) 我们不希望这个数据被改变,
b) 一个永不改变的编译时常量:编译器可以将编译时常量带入任何它需要的计算式,这就意味着在编译时就可以进行一些计算(执行计算式),可以减少运行时负担。
20.final关键字修饰对象引用:final关键字修饰的”引用”,即
final Object o1 = new Object(1);
那么就不能 o1 = new Object(2);了 ,因为它不能再指向另一个对象了。
然而对象的值是可以变的,比如:
Object里有1个int i ;
o1.i++;是可以的,因为它改变的是对象的属性。 而不是引用。
而数组也是引用,比如int[] a = new int[]{1,2,3}
我们可以改变a[i],比如a[1] = 10;因为数组也是引用。
所以一般情况下final修饰应用没有修饰基本类型作用大。
21.空白final:所谓空白final,就是指被声明是final的但是没有初始化的域。
22.空白final的作用:它可以使得一个类中的final域做到因对象不同而值不同,具体做法是在不同的构造器中对final域进行初始化。使得不同构造器中的final域是不同的
23.final一些原则:必须在域的定义处初始化或者在构造器中进行初始化,这正是final域在使用前总是被初始化的原因所在。
24.final参数(本地变量):引用------->无法改变引用指向新的对象。
基本数据类型------>无法改变值。
25.final方法使用的2点原因:
a) 明确禁止覆盖。(其实更深层次的是禁止动态绑定)
b) 以前是为了效率,不过在JAVA SE5/6时,让编译器和JVM来处理效率的问题,所以该条省略。
26.private和final:private方法因为它是外部用不到的,所以它隐含是final的, 对于private方法,可以理解为它不是基类的接口的一部分,而是隐藏于类中的程序代码,如果我们在子类当中有相同名称的方法的话,它并不是覆写基类方法,因为基类方式是private的,而是新生成了1个方法,所以即使名字相同,也不会报错。
27.final类:出于设计或者安全,你不希望它有子类,不可以继承它。它的所有方法都是final的,无法覆写。
28.类的代码(可以理解为static代码)在初次使用时才加载:类的编译代码保存在它自己的独立文件中,该文件只在需要的时候才会被加载。
一个很好的关于final总结地址:
http://www.importnew.com/7553.html
Java中的final关键字非常重要,它可以应用于类、方法以及变量。这篇文章中我将带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使用final关键字的实例。final经常和static一起使用来声明常量,你也会看到final是如何改善应用性能的。
final关键字的含义?
final在Java中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。
什么是final变量?
凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为final的都叫作final变量。final变量经常和static关键字一起使用,作为常量。下面是final变量的例子:
1 2 | public static final String LOAN = "loan"; LOAN = new String("loan") //invalid compilation error |
final变量是只读的。
什么是final方法?
final也可以声明方法。方法前面加上final关键字,代表这个方法不可以被子类的方法重写。如果你认为一个方法的功能已经足够完整了,子类中不需要改变的话,你可以声明此方法为final。final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。下面是final方法的例子:
1 2 3 4 5 6 7 8 9 10 11 12 | class PersonalLoan{ public final String getName(){ return "personal loan"; } }
class CheapPersonalLoan extends PersonalLoan{ @Override public final String getName(){ return "cheap personal loan"; //compilation error: overridden method is final } } |
什么是final类?
使用final来修饰的类叫作final类。final类通常功能是完整的,它们不能被继承。Java中有许多类是final的,譬如String, Interger以及其他包装类。下面是final类的实例:
1 2 3 4 5 6 7 | final class PersonalLoan{
}
class CheapPersonalLoan extends PersonalLoan{ //compilation error: cannot inherit from final class
} |
final关键字的好处
下面总结了一些使用final关键字的好处
1. final关键字提高了性能。JVM和Java应用都会缓存final变量。
2. final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
3. 使用final关键字,JVM会对方法、变量及类进行优化。
不可变类
创建不可变类要使用final关键字。不可变类是指它的对象一旦被创建了就不能被更改了。String是不可变类的代表。不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等。
相关阅读:为什么String是不可变的以及如何写一个不可变类。
关于final的重要知识点
1. final关键字可以用于成员变量、本地变量、方法以及类。
2. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。
3. 你不能够对final变量再次赋值。
4. 本地变量必须在声明时赋值。
5. 在匿名类中所有变量都必须是final变量。
6. final方法不能被重写。
7. final类不能被继承。
8. final关键字不同于finally关键字,后者用于异常处理。
9. final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
10. 接口中声明的所有变量本身是final的。
11. final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
12. final方法在编译阶段绑定,称为静态绑定(static binding)。
13. 没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。
14. 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
15. 按照Java代码惯例,final变量就是常量,而且通常常量名要大写:
1 | private final int COUNT = 10; |
1. 对于集合对象声明为final指的是引用不能被更改,但是你可以向其中增加,删除或者改变内容。譬如:
1 2 3 4 | private final List Loans = new ArrayList(); list.add(“home loan”); //valid list.add("personal loan"); //valid loans = new Vector(); //not valid |
我们已经知道final变量、final方法以及final类是什么了。必要的时候使用final,能写出更快、更好的代码的。
多态:
1.将1个方法调用和方法体关联起来---被称作绑定:绑定分为前期绑定和后期绑定。Java中除了static方法和final方法之外,其他所有方法都是后期绑定。
2.后期绑定机制:就是在运行时根据对象的类型进行绑定,又叫运行时绑定。
3.final方法其实就是旨在关闭动态绑定。
4.方法向上转型: class Circle extends Shape
public Shape next(){
return new Circle();
}
5.多态是一像“将改变的事物与未变的事物分离开来”的重要事物。
-------------------------------------------------------------------------------------------------------------------------------
其他知识点:
1.一个对象创建的过程:
http://blog.csdn.net/lingzhou1/article/details/8476709
如果是Person p = new Person();
1.首先在硬盘指定位置找到Person.class文件,然后将该文件加载进内存。
2.执行main方法时,在内存的栈区中为main()方法开辟1个空间,然后再main方法的栈区中分配了1个变量p 。
3.在堆内存中开辟了一段实体空间,分配了1个内存首地址。
4.在该空间中进行属性的空间分配,并进行默认初始化。
5.对空间的属性进行显示初始化。
6.进行实体的构造代码块初始化。
7.构造函数初始化。
8.将首地址赋值给p,p变量就引用了该实体。
http://blog.csdn.net/anjayxc/article/details/6063210
这是没有涉及继承的情况,对于涉及了继承的情况总结如下:
假设有个名为Mouse4j的类,继承自Mouse,Mouse又继承自Animal
1.当首次创建型为Mouse4j的对象时,Java解释器查找类路径,定位Mouse4j.class文件。
2.Java解释器会根据Mouse4j.class定位其基类Mouse.class、再根据Mouse.class定位到基类Animal.class文件,有关静态初始化的动作从基类到子类依次执行。
3.当你用new Mouse4j()创建对象的时候,首先将在堆上为Mouse4j对象(包括其基类Mouse和Animal中的域)分配足够的存储空间。 (这里要注意:在堆上的子类对象是拥有其基类的域的!)
4.这块存储空间会被清零,这就自动地将Mouse4j中的所有基本类型数据(包括其基类Mouse和Animal中的)设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用(包括其基类Mouse和Animal中的)则被置成了null。
5.执行基类Animal中所有出现于域定义处的初始化动作。
6.执行基类Animal构造器。
7.执行基类Mouse中所有出现于域定义处的初始化动作。
8.执行基类Mouse构造器。
9.执行子类Mouse4j中所有出现于域定义处的初始化动作。
10.执行子类Mouse4j构造器。
即:class是从子类到基类依次查找,有关静态初始化的动作从基类到子类依次执行。
在为所创建对象的存储空间清零后,找到继承链中最上层的基类,执行a、b两步:
a.执行其出现在域定义处的初始化动作
b.然后再执行其构造器
然后从基类到子类依次执行这两步操作。
2.关于静态块什么时候初始化的问题:
博客地址:
http://www.cnblogs.com/ivanfu/archive/2012/02/12/2347817.html
java的static块执行时机
之前一直认为static块是在class load的时候执行,今天在验证Spring初始化Context loader的时候,发现bean的static块并没有执行。
Java代码:
1 Class A{
2 static{
3 System.out.println("static block invoked!")
4 }
5 }
那么static块到底在什么时候运行的呢?如果了解JVM原理,我们知道,一个类的运行分为以下步骤:
1. 装载
2. 连接
3. 初始化
其中装载阶段又三个基本动作组成:
1. 通过类型的完全限定名,产生一个代表该类型的二进制数据流
2. 解析这个二进制数据流为方法区内的内部数据结
3. 构创建一个表示该类型的java.lang.Class类的实例
另外如果一个类装载器在预先装载的时遇到缺失或错误的class文件,它需要等到程序首次主动使用该类时才报告错误。
连接阶段又分为三部分:
1. 验证,确认类型符合Java语言的语义,检查各个类之间的二进制兼容性(比如final的类不用拥有子类等),另外还需要进行符号引用的验证。
2. 准备,Java虚拟机为类变量分配内存,设置默认初始值。
3. 解析(可选的) ,在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
当一个类被主动使用时,Java虚拟就会对其初始化,如下六种情况为主动使用:
1. 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
2. 当调用某个类的静态方法时
3. 当使用某个类或接口的静态字段时
4. 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
5. 当初始化某个子类时
6. 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit。
实际上,static块的执行发生在“初始化”的阶段。初始化阶段,jvm主要完成对静态变量的初始化,静态块执行等工作。
下面我们看看执行static块的几种情况:
1、第一次new A()的过程会打印"";因为这个过程包括了初始化
2、第一次Class.forName("A")的过程会打印"";因为这个过程相当于Class.forName("A",true,this.getClass().getClassLoader());
3、第一次Class.forName("A",false,this.getClass().getClassLoader())的过程则不会打印""。因为false指明了装载类的过程中,不进行初始化。不初始化则不会执行static块。
参考资料:深入Java虚拟机
3.static的优点和缺点:
http://blog.csdn.net/lingzhou1/article/details/8476709
static:关键字是一个修饰符。用于修饰成员(成员变量 和成员函数)。
特点:
1.想要实现对象中的共性数据的对象共享。可以讲这个数据进行静态修饰。
2.被静态修饰的成员,可以直接被类名所调用。也就是说,静态的成员多了一种调用方式。类名.静态方式。
3.静态随着类的加载而加载。而且优先于对象存在。
弊端:
1.有些数据是对象特有的数据,是不可以被静态修饰的。因为那样的话,特有数据会变成对象的共享数据。这样对事物的描述就出了问题。所以,在定义静态时,必须要明确,这个数据是否是被对象所共享的。
2.静态方法只能访问静态成员,不可以访问非静态成员。
因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。
3.静态方法中不能使用this,super关键字。
因为this代表对象,而静态在时,有可能没有对象,所以this无法使用。
4.主函数是静态的。
什么时候定义静态成员呢?
成员分两种:
1.成员变量。(数据共享时静态化)
该成员变量的数据是否是所有对象都一样;
如果是,那么该变量需要被静态修饰,因为是共享数据。如果不是,那么就说这是对象的特有数据,要存储到对象中。
2.成员函数。(方法中没有调用特有数据时就定义成静态)
如何判断成员函数是否需要被静态修饰呢?
只要参考,该函数内是否访问了对象中的特有数据。如果有访问特有数据,那方法不能被静态修饰。如果没有访问过特有数据,那么这个方法需要被静态修饰。
静态的生命周期很长。静态代码块就是一个有静态关键字标示的一个代码区域。定义在类中。完成类的初始化。静态代码块随着类的加载而执行,而且只执行一次(new多个对象就只执行一次)。如果和主函数在同一个类中,优先于主函数执行。
主函数的解释:保证所在类的独立运行。是程序的入口。被JVM调用。
静态代码块、构造代码块、构造函数同时存在时的执行顺序:
静态代码块--构造代码块--构造函数。
4.Java内存分配的问题:
http://blog.csdn.net/yangyuankp/article/details/7651251
l 寄存器:JVM内部虚拟寄存器,存取速度非常快,程序不可控制。
l 栈:保存局部变量的值,包括:1.用来保存基本数据类型的值;2.保存类的实例,即堆区对象的引用(指针)。也可以用来保存加载方法时的帧。
l 堆:用来存放动态产生的数据,比如new出来的对象。注意创建出来的对象只包含属于各自的成员变量,并不包括成员方法。因为同一个类的对象拥有各自的成员变量,存储在各自的堆中,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。
l 常量池:JVM为每个已加载的类型维护一个常量池,常量池就是这个类型用到的常量的一个有序集合。包括直接常量(基本类型,String)和对其他类型、方法、字段的符号引用(1)。池中的数据和数组一样通过索引访问。由于常量池包含了一个类型所有的对其他类型、方法、字段的符号引用,所以常量池在Java的动态链接中起了核心作用。常量池存在于堆中。
总结常量值:在堆中,保存了除float之外的基本类型和String类型,所有的对其他类型、方法、字段的符号引用。
l 代码段:用来存放从硬盘上读取的源程序代码。
l 数据段:用来存放static定义的静态成员。
下面是内存表示图:
上图中大致描述了Java内存分配,接下来通过实例详细讲解Java程序是如何在内存中运行的(注:以下图片引用自尚学堂马士兵老师的J2SE课件,图右侧是程序代码,左侧是内存分配示意图,我会一一加上注释)。
预备知识:
1.一个Java文件,只要有main入口方法,我们就认为这是一个Java程序,可以单独编译运行。
2.无论是普通类型的变量还是引用类型的变量(俗称实例),都可以作为局部变量,他们都可以出现在栈中。只不过普通类型的变量在栈中直接保存它所对应的值,而引用类型的变量保存的是一个指向堆区的指针,通过这个指针,就可以找到这个实例在堆区对应的对象。因此,普通类型变量只在栈区占用一块内存,而引用类型变量要在栈区和堆区各占一块内存。
示例:
1.JVM自动寻找main方法,执行第一句代码,创建一个Test类的实例,在栈中分配一块内存,存放一个指向堆区对象的指针110925。
2.创建一个int型的变量date,由于是基本类型,直接在栈中存放date对应的值9。
3.创建两个BirthDate类的实例d1、d2,在栈中分别存放了对应的指针指向各自的对象。他们在实例化时调用了有参数的构造方法,因此对象中有自定义初始值。
调用test对象的change1方法,并且以date为参数。JVM读到这段代码时,检测到i是局部变量,因此会把i放在栈中,并且把date的值赋给i。
把1234赋给i。很简单的一步。
change1方法执行完毕,立即释放局部变量i所占用的栈空间。
调用test对象的change2方法,以实例d1为参数。JVM检测到change2方法中的b参数为局部变量,立即加入到栈中,由于是引用类型的变量,所以b中保存的是d1中的指针,此时b和d1指向同一个堆中的对象。在b和d1之间传递是指针。
change2方法中又实例化了一个BirthDate对象,并且赋给b。在内部执行过程是:在堆区new了一个对象,并且把该对象的指针保存在栈中的b对应空间,此时实例b不再指向实例d1所指向的对象,但是实例d1所指向的对象并无变化,这样无法对d1造成任何影响。
change2方法执行完毕,立即释放局部引用变量b所占的栈空间,注意只是释放了栈空间,堆空间要等待自动回收。
调用test实例的change3方法,以实例d2为参数。同理,JVM会在栈中为局部引用变量b分配空间,并且把d2中的指针存放在b中,此时d2和b指向同一个对象。再调用实例b的setDay方法,其实就是调用d2指向的对象的setDay方法。
调用实例b的setDay方法会影响d2,因为二者指向的是同一个对象。
change3方法执行完毕,立即释放局部引用变量b。
以上就是Java程序运行时内存分配的大致情况。其实也没什么,掌握了思想就很简单了。无非就是两种类型的变量:基本类型和引用类型。二者作为局部变量,都放在栈中,基本类型直接在栈中保存值,引用类型只保存一个指向堆区的指针,真正的对象在堆里。作为参数时基本类型就直接传值,引用类型传指针。
小结:
1.分清什么是实例什么是对象。Class a= new Class();此时a叫实例,而不能说a是对象。实例在栈中,对象在堆中,操作实例实际上是通过实例的指针间接操作对象。多个实例可以指向同一个对象。
2.栈中的数据和堆中的数据销毁并不是同步的。方法一旦结束,栈中的局部变量立即销毁,但是堆中对象不一定销毁。因为可能有其他变量也指向了这个对象,直到栈中没有变量指向堆中的对象时,它才销毁,而且还不是马上销毁,要等垃圾回收扫描时才可以被销毁。
3.以上的栈、堆、代码段、数据段等等都是相对于应用程序而言的。每一个应用程序都对应唯一的一个JVM实例,每一个JVM实例都有自己的内存区域,互不影响。并且这些内存区域是所有线程共享的。这里提到的栈和堆都是整体上的概念,这些堆栈还可以细分。
4.类的成员变量在不同对象中各不相同,都有自己的存储空间(成员变量在堆中的对象中)。而类的方法却是该类的所有对象共享的,只有一套,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。
以上分析只涉及了栈和堆,还有一个非常重要的内存区域:常量池,这个地方往往出现一些莫名其妙的问题。常量池是干嘛的上边已经说明了,也没必要理解多么深刻,只要记住它维护了一个已加载类的常量就可以了。接下来结合一些例子说明常量池的特性。
预备知识:
基本类型和基本类型的包装类。基本类型有:byte、short、char、int、long、boolean。基本类型的包装类分别是:Byte、Short、Character、Integer、Long、Boolean。注意区分大小写。二者的区别是:基本类型体现在程序中是普通变量,基本类型的包装类是类,体现在程序中是引用变量。因此二者在内存中的存储位置不同:基本类型存储在栈中,而基本类型包装类存储在堆中。上边提到的这些包装类都实现了常量池技术,另外两种浮点数类型的包装类则没有实现。另外,String类型也实现了常量池技术。
实例:
[java] view plain copy print?
1. public class test {
2. public static void main(String[] args) {
3. objPoolTest();
4. }
5.
6. public static void objPoolTest() {
7. int i = 40;
8. int i0 = 40;
9. Integer i1 = 40;
10. Integer i2 = 40;
11. Integer i3 = 0;
12. Integer i4 = new Integer(40);
13. Integer i5 = new Integer(40);
14. Integer i6 = new Integer(0);
15. Double d1=1.0;
16. Double d2=1.0;
17.
18. System.out.println("i=i0\t" + (i == i0));
19. System.out.println("i1=i2\t" + (i1 == i2));
20. System.out.println("i1=i2+i3\t" + (i1 == i2 + i3));
21. System.out.println("i4=i5\t" + (i4 == i5));
22. System.out.println("i4=i5+i6\t" + (i4 == i5 + i6));
23. System.out.println("d1=d2\t" + (d1==d2));
24.
25. System.out.println();
26. }
27. }
public class test {
public static void main(String[] args) {
objPoolTest();
}
public static void objPoolTest() {
int i = 40;
int i0 = 40;
Integer i1 = 40;
Integer i2 = 40;
Integer i3 = 0;
Integer i4 = new Integer(40);
Integer i5 = new Integer(40);
Integer i6 = new Integer(0);
Double d1=1.0;
Double d2=1.0;
System.out.println("i=i0\t" + (i == i0));
System.out.println("i1=i2\t" + (i1 == i2));
System.out.println("i1=i2+i3\t" + (i1 == i2 + i3));
System.out.println("i4=i5\t" + (i4 == i5));
System.out.println("i4=i5+i6\t" + (i4 == i5 + i6));
System.out.println("d1=d2\t" + (d1==d2));
System.out.println();
}
}
结果:
[plain] view plain copy print?
1. i=i0 true
2. i1=i2 true
3. i1=i2+i3 true
4. i4=i5 false
5. i4=i5+i6 true
6. d1=d2 false
i=i0 true
i1=i2 true
i1=i2+i3 true
i4=i5 false
i4=i5+i6 true
d1=d2 false
结果分析:
1.i和i0均是普通类型(int)的变量,所以数据直接存储在栈中,而栈有一个很重要的特性:栈中的数据可以共享。当我们定义了int i = 40;,再定义int i0 = 40;这时候会自动检查栈中是否有40这个数据,如果有,i0会直接指向i的40,不会再添加一个新的40。
2.i1和i2均是引用类型,在栈中存储指针,因为Integer是包装类。由于Integer 包装类实现了常量池技术,因此i1、i2的40均是从常量池中获取的,均指向同一个地址,因此i1=12。
3.很明显这是一个加法运算,Java的数学运算都是在栈中进行的,Java会自动对i1、i2进行拆箱操作转化成整型,因此i1在数值上等于i2+i3。
4.i4和i5 均是引用类型,在栈中存储指针,因为Integer是包装类。但是由于他们各自都是new出来的,因此不再从常量池寻找数据,而是从堆中各自new一个对象,然后各自保存指向对象的指针,所以i4和i5不相等,因为他们所存指针不同,所指向对象不同。
5.这也是一个加法运算,和3同理。
6.d1和d2均是引用类型,在栈中存储指针,因为Double是包装类。但Double包装类没有实现常量池技术,因此Doubled1=1.0;相当于Double d1=new Double(1.0);,是从堆new一个对象,d2同理。因此d1和d2存放的指针不同,指向的对象不同,所以不相等。
小结:
1.以上提到的几种基本类型包装类均实现了常量池技术,但他们维护的常量仅仅是【-128至127】这个范围内的常量,如果常量值超过这个范围,就会从堆中创建对象,不再从常量池中取。比如,把上边例子改成Integer i1 = 400; Integer i2 = 400;,很明显超过了127,无法从常量池获取常量,就要从堆中new新的Integer对象,这时i1和i2就不相等了。
2.String类型也实现了常量池技术,但是稍微有点不同。String型是先检测常量池中有没有对应字符串,如果有,则取出来;如果没有,则把当前的添加进去。
凡是涉及内存原理,一般都是博大精深的领域,切勿听信一家之言,多读些文章。我在这只是浅析,里边还有很多猫腻,就留给读者探索思考了。希望本文能对大家有所帮助!