---------android培训、java培训、期待与您交流! ---------
1. 前言
写这基础复习系列是总结出其中的一些知识点,用于以后自己复习用,以前的一些知识盲点也明白了。当然,基础这东西很难说,什么是基础?有人认为将Java的SDK源码中重要的类研究一遍,并且能按其规范(接口)实现了自己的类才算是真正掌握了基础。其实一点都没错,只有通过去看微观的实现,才能提升自己的认识。
数据类型在计算机语言里面,是对内存位置的一个抽象表达方式,可以理解为针对内存的一种抽象的表达方式。接触每种语言的时候,都会存在数据类型的认识,有复杂的、简单的,各种数据类型都需要在学习初期去了解,Java是强类型语言,所以Java对于数据类型的规范会相对严格。数据类型是语言的抽象原子概念,可以说是语言中最基本的单元定义,在Java里面,本质上讲将数据类型分为两种:基本类型和引用数据类型。
基本类型:简单数据类型是不能简化的、内置的数据类型、由编程语言本身定义,它表示了真实的数字、字符和整数。java的基本数据类型共有8种,即int, short, long, byte, float, double, boolean, char(注意,并没有string的基本类型)。这种类型的定义是通过诸如int a = 3; long b = 255L;的形式来定义的。如int a = 5;这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中。
就是存在栈中的数据可以共享。。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不同的,如
in a=3;int b=3;at和b都指向了栈中字面值为3的地址。
这种情况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另一个对象引用变量。
引用数据类型:Java语言本身不支持C++中的结构(struct)或联合(union)数据类型,它的复合数据类型一般都是通过类或接口进行构造,类提供了捆绑数据和方法的方式,同时可以针对程序外部进行信息隐藏。
2)引用类型的存储原理:引用类型继承于Object类(也是引用类型)都是按照Java里面存储对象的内存模型来进行数据存储的,使用Java内存堆和内存栈来进行这种类型的数据存储,简单地讲,“引用”是存储在有序的内存栈上的,而对象本身的值存储在内存堆上的;
下面介绍几种引用数据类型
对象
在Java中,创建一个对象包括对象的声明和实例化两步,下面用一个例题来说明对象的内存模型。
假设有类Rectangle定义如下:
Java代码
class Rectangle{
double width,height;
Rectangle(double w,double h){
width=w;
height=h;
}
}
(1)声明对象时的内存模型
用Rectangle rect;声明一个对象rect时,将在栈内存为对象的引用变量rect分配内存空间,但Rectangle的值为空,称rect是一个空对象。空对象不能使用,因为它还没有引用任何“实体”。
(2)对象实例化时的内存模型
当执行rect=new Rectangle(3,5);时,会做两件事:
在堆内存中为类的成员变量width,height分配内存,并将其初始化为各数据类型的默认值;接着进行显式初始化(类定义时的初始化值);最后调用构造方法,为成员变量赋值。
返回堆内存中对象的引用(相当于首地址)给栈内存中的引用变量rect,以后就可以通过rect来引用堆内存中的对象了。
包装类
基本型别都有对应的包装类:如int对应Integer类,double对应Double类等,基本类型的定义都是直接在栈中,如果用包装类来创建对象,就和普通对象一样了。例如:int i=0;i直接存储在栈中。 (栈)Integer i(i此时是对象) = (堆)new Integer(5);这样,i对象数据存储在堆中,i的引用存储在栈中,通过栈中的引用来操作对象。
3 数组在内存中的存储状态
先看看数组,数组咱们平时经常用,从用法来看,数组相当于普通变量,只不过它可以状态多个相同类的多个对象容器而已。在内存中,数组向内存申请的空间是一段连续的物理空间。
- public class ArrayTest {
- public static void main(String[] args) {
- String[] array = new String[] { "1", "2", "3" };
- for (String str : array) {
- System.out.println(str.hashCode());
- }
- }
- }
这3个字符串实际上占用的是一段连续的内存空间地址。需要说明的一点就是数组是引用型变量,数组中的元素仅仅是指向内存地址的指针,而指针指向的目的地才是实际的数据对象。内存中的情况如下:
所谓的数组声明,实际上就是按照指定长度,为数组在内存开辟了一段连续的空间,如果不是Java基本原型数据则附给这些内存空间的指针与默认初始地址null,如果是原型数据,则这些空间不再是指针,而是实实在在的原型值(例如int是0)。
数组引用表示的含义:例如int[] arr = null
这个定义中,arr表示一个可以参考引用自一维数组对象的变量名称,但是目前将这个名称参考引用自null,表示还没有指定这个名称参考引用自实际的对象。在Java中,=运算用于基本数据类型时,是将值复制给变量,但当它用于对象时,则是将对象指定给参考引用名称来参考引用。也可以将同一个对象指定给两个参考引用名称,当对象的值由其中一个参考引用名称进行操作而变更时,另一个参考引用名称所参考引用到的值也会变动。
多维数组变量也是引用类型数组变量的一种。对于数组int array[2][],变量array储存在栈内存中,它指向堆内存中的一个包含两个元素的数组。其中每个元素的类型是一个引用类型(一维数组类型),并指向一个实实在在的数组.
Java中的数组作为对象带来的好处
1)越界检查
2)length field:与传统的C++中的数组相比,length字段可以方便的得到数组的大小;但要注意,仅仅可以得到数组的大小,不能得到数组中实际包含多少个元素,因为length 只会告诉我们最多可将多少元素置入那个数组。
3) 初始化:对象数组在创建之初会自动初始化成null,由原始数据类型构成的数组会自动初始化成零(针对数值类型),(Char)0 (针对字符类型)或者false (针对布尔类型)。
4) 数组作为返回值:首先,既然数组是对象,那么就可以把这个对象作为返回值;而且,不必担心那个数组的
3. 对象的产生
对象的产生和JVM的运行机制息息相关,我们使用一个对象为我们服务实际上归根结底最后都是得用new出来的对象为我们所用,而这个对象是通过类对象产生的,这就是Java思想中的万事万物接对象的概念。首先得有一个模板对象,这个模板对象就是类对象,每一个new出来的实例对象实际上都是由这个模板对象而产生出来的,所以我们定义类的时候如果具有类变量,那么所有因它而创建的实例对象中的static变量都会因为类变量的改变而改变。因为static本身就是类对象所拥有的,模板都变了,你实例对象中的相关变量当然要变喽。
无论是通过哪个实例对象去访问类变量,底层都是用类对象直接访问该类变量,所以大家使用static变量时得出来的值都是一样的。
还要说明的一点就是final变量,如果在编译时就能确定该变量的值,则此值在程序运行时不再是个变量,而是一个定值常量。至于实例变量的初始化时机以及JVM的一些初始化内幕,请参考blog:http://suhuanzheng7784877.iteye.com/blog/964784。
4. 父子对象
使用Java不可能不使用继承机制,现在来看看new一个子类的时候是如何初始化父类的。假如有如下的类结构
所有类如果不指定父类那么就都是Object的子类,如果指定了父类,则间接地会继承Object的,可能是它的孙子,也可能是它的曾孙子,也可能是它的孙子的孙子。如下例
- class Parent{
- static{
- System.out.println("老子的静态块");
- }
- {
- System.out.println("老子的非静态块");
- }
- public Parent(){
- System.out.println("老子的无参构造函数");
- }
- }
- class Sub extends Parent{
- static{
- System.out.println("儿子的静态块");
- }
- {
- System.out.println("儿子的非静态块");
- }
- public Sub(){
- System.out.println("儿子的无参构造函数");
- }
- }
- public class ParSubTest {
- /**
- * @param args
- */
- public static void main(String[] args) {
- new Sub();
- }
- }
执行之后的结果是
由此可以得出结论:
0.静态代码块总会在实例对象创建之前执行,因为它是属于类对象级别的代码块,JVM先在内存中分配好了类对象的空间,执行完静态块后再去理会实例对象作用域的东西。
1.总是执行父类的非静态块
2.隐式调用父类的无参构造函数,或者现实调用父类的有参构造函数
3.执行子类的非静态块
4.根据程序需要(就是new后面的构造器函数)调用子类的构造函数
下面来看看一个不太规范的父子程序引发的问题。
- package se01;
- class Par1 {
- private int num = 20;
- public Par1() {
- System.out.println("par-num:" + num);
- this.display();
- }
- public void display() {
- System.out.println("num:" + num + " class:"
- + this.getClass().getName());
- }
- }
- class Sub1 extends Par1 {
- private int num = 40;
- public Sub1() {
- num = 4000;
- }
- public void display() {
- System.out.println("sub-num:" + num + " class:"
- + this.getClass().getName());
- }
- }
- public class ParSubErrorTest {
- public static void main(String[] args) {
- new Sub1();
- }
- }
当然,一般在实际项目开发中也不会这么写代码,不过这代码给咱们的启示是揭示了JVM的一些内幕。执行结果是
就像刚刚得出的5条结论一样,在new Sub1();的时候先要对父类进行构造函数的调用,而父类的构造函数又调用了方法display(),这个时候问题就出现了,父类究竟调用的是谁的构造方法?是父类自己的,还是子类重写的?结论很简单了,就是子类若重写了该方法,那么直接调用子类的重写方法,如果没有重写该方法,那么直接由父类对象直接调用自己的方法即可。由上面程序可以看出子类重写了该display()方法,那么在调用子类的构造函数之前是先调用了父类的无参构造函数,之后在父类无参构造函数中调用了子类重写后的display()方法,而此时,子类对象还没实例化完毕呢,仅仅在内存中分配了相应的空间而已,实例变量仅仅有系统默认值而已,并没有完成赋值的过程,所以,此时子类的实例变量num是默认值0,导致调用子类方法时显示num也是0。而父类的实例变量当然此时已经初始化完毕了,实例对象也有了,自然它的num是赋予初始值后的20喽。
而这程序的问题,或者说不规范的地方在哪里呢?就是它将构造函数用于了其他用途,构造函数实际上就是为了初始化数据用的,而不是用于调用其他方法用的,此程序在构造函数中调用了自己声明的一个public方法,无异于扭曲了构造函数本身的作用,虽然说这么写编译器不会报错,但是无异于给继承机制带来了隐患。
5. 继承机制在处理成员变量和方法时的区别
- package se01;
- class Parent2 {
- int a = 1;
- public void test01() {
- System.out.println(a);
- }
- }
- class Sub2 extends Parent2 {
- int a = 2;
- public void test01() {
- System.out.println(a);
- }
- }
- public class ParSubPMTest {
- public static void main(String[] args) {
- Parent2 sub2 = new Sub2();
- Sub2 sub3 = (Sub2)sub2;
- System.out.println(sub2.a);
- sub2.test01();
- System.out.println(sub3.a);
- sub3.test01();
- }
- }
输出结果是
也就是说通过直接访问实例变量的时候是显示父类特性的,当使用方法的时候则显示运行时特性。实际上父子关系在内存中存储是这样的
就是说实例对象虽然都是同一个,但是这个实例实际上既存储了自己的变量,也存储了父类的变量,当使用父类声明的对象访问变量时呈现父亲的变量值,使用子类的对象直接访问变量时呈现子类的值。也就是说当我们初始化一个子类对象时,会将它所有的父类(这里是单继承的意思,所有的父类就是说父亲、爷爷、曾祖、曾曾祖父……)的实例变量分配内存空间。如果子类定义的实例变量与父类同名,那么会隐藏父类的变量,并不是完全覆盖,通过父类.变量依然能够获得父类的实例变量。
6. Java内存管理技巧
1:尽量使用直接量,而尽量不要用new的方式建立这些对象,比如
- String string = "1";
- Long longlong = 1L;
- Byte bytebyte = 1;
- Short shortshort = 1;
- Integer integer = 22;
- Float floatfloat = 2.2F;
- Double doubledouble = 0.333333;
- Boolean booleanboolean = false;
- Character character = 'm';
2:尽量使用StringBuffer和StringBuilder来进行字符串的的链接和使用,这个就不用解释了吧,很常用,尤其是拼接SQL的时候。
3:养成习惯,尽早释放无用对象
例如如下程序:
- public void test(){
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder = null;
- //很消耗时间………………………………
- }
在很消耗时间的程序执行前将变量就尽量释放掉,让JVM垃圾回收期去回收去。
4:不到万不得以,不要轻易使用static变量,虽然static变量很常用,不过这个类变量会常驻内存,从对象复用的角度讲,倒是省了资源了,但是如果不是经常复用的对象而声明了static变量就会常驻内存,只要程序还在运行就永不会回收。
5:避免创建重复对象变量
如上代码创建了很多个临时对象变量use,实际上可以改进成
6:尽量不要自己使用对象的finalize方法
不到万不得以,千万不要在此方法中进行变量回收等等操作。
7:如果运行时环境要求空间资源很严格,那么可以考虑使用软引用SoftReference对象进行引用。当内存不够时,它会牺牲自己,释放软引用对象。软引用对象适用于比较瞬时的处理程序,处理完了就完了,内存不够会先将此对象控件腾出来而不回内存溢出的报错误。(关于垃圾回收和对象各种方式的引用会在之后学习笔记中体现)
7. 总结
主要复习了基本数据类型 ,引用数据类型的内存形式、父子对象的一些调用陷阱、父子关系在内存中的形式、内存的使用技巧。