Java基础复习笔记 数组,内存形式,父子,内存控制01

来源:互联网 发布:哦淘宝网 编辑:程序博客网 时间:2024/06/05 00:51

1.       前言

写这基础复习系列是觉得工作中自己的基础太差了,很多东西都没想透彻,没研究明白。看了《Java基础16课》总结出其中的一些知识点,用于以后自己复习用,以前的一些知识盲点也明白了。当然,基础这东西很难说,什么是基础?有人认为将Java的SDK源码中重要的类研究一遍,并且能按其规范(接口)实现了自己的类才算是真正掌握了基础。其实一点都没错,只有通过去看微观的实现,才能提升自己的认识。

2.       数组在内存中的存储状态

先看看数组,数组咱们平时经常用,从用法来看,数组相当于普通变量,只不过它可以状态多个相同类的多个对象容器而已。在内存中,数组向内存申请的空间是一段连续的物理空间。

Java代码  收藏代码
  1. public class ArrayTest {  
  2.     public static void main(String[] args) {  
  3.         String[] array = new String[] { "1""2""3" };  
  4.         for (String str : array) {  
  5.             System.out.println(str.hashCode());  
  6.         }  
  7.     }  
  8. }  

 这3个字符串实际上占用的是一段连续的内存空间地址。需要说明的一点就是数组是引用型变量,数组中的元素仅仅是指向内存地址的指针,而指针指向的目的地才是实际的数据对象。内存中的情况如下:

 所谓的数组声明,实际上就是按照指定长度,为数组在内存开辟了一段连续的空间,如果不是Java基本原型数据则附给这些内存空间的指针与默认初始地址null,如果是原型数据,则这些空间不再是指针,而是实实在在的原型值(例如int是0)。

而实际上多维数组的实现也是基于上面一维数组实现的,所以二维数组在我们来看,逻辑上可以当成矩阵,而在实实在在物理内存上则是如下:

例如

Java代码  收藏代码
  1. public static void main(String[] args) {  
  2.         String[][] str2 = new String[3][4];  
  3.         for (int i = 0; i < str2.length; i++) {  
  4.             int numOuter = i + 1;  
  5.             System.out.println("外层执行了" + numOuter + "次");  
  6.             for (int j = 0; j < str2[i].length; j++) {  
  7.                 int numIuter = j + 1;  
  8.                 System.out.println("内层执行了" + numIuter + "次");  
  9.                 str2[i][j] = "素" + i + j;  
  10.             }  
  11.         }  
  12.   
  13.         for (String[] strArray1 : str2) {  
  14.             for (String str : strArray1) {  
  15.                 System.out.print(str + " ");  
  16.             }  
  17.             System.out.println();  
  18.         }  
  19.     }  

 看似二维数组存储的元素是这样的矩阵形式

Java代码  收藏代码
  1. 00 素01 素02 素03   
  2. 10 素11 素12 素13   
  3. 20 素21 素22 素23  

 实际上这个二维数组的分配如下图



 再复杂的三维数组、多维数组同样以此类推,呈现出一个倒树结构,以根扩展,叶子节点才是真正存储数据(原型)或者是真正指向应用数据对象的指针(复杂对象)。

3.       对象的产生

对象的产生和JVM的运行机制息息相关,我们使用一个对象为我们服务实际上归根结底最后都是得用new出来的对象为我们所用,而这个对象是通过类对象产生的,这就是Java思想中的万事万物接对象的概念。首先得有一个模板对象,这个模板对象就是类对象,每一个new出来的实例对象实际上都是由这个模板对象而产生出来的,所以我们定义类的时候如果具有类变量,那么所有因它而创建的实例对象中的static变量都会因为类变量的改变而改变。因为static本身就是类对象所拥有的,模板都变了,你实例对象中的相关变量当然要变喽。

无论是通过哪个实例对象去访问类变量,底层都是用类对象直接访问该类变量,所以大家使用static变量时得出来的值都是一样的。

还要说明的一点就是final变量,如果在编译时就能确定该变量的值,则此值在程序运行时不再是个变量,而是一个定值常量。至于实例变量的初始化时机以及JVM的一些初始化内幕,请参考blog:http://suhuanzheng7784877.iteye.com/blog/964784

4.       父子对象

使用Java不可能不使用继承机制,现在来看看new一个子类的时候是如何初始化父类的。假如有如下的类结构

 所有类如果不指定父类那么就都是Object的子类,如果指定了父类,则间接地会继承Object的,可能是它的孙子,也可能是它的曾孙子,也可能是它的孙子的孙子。如下例

Java代码  收藏代码
  1. class Parent{  
  2.     static{  
  3.         System.out.println("老子的静态块");  
  4.     }  
  5.     {  
  6.         System.out.println("老子的非静态块");  
  7.     }  
  8.     public Parent(){  
  9.         System.out.println("老子的无参构造函数");  
  10.     }  
  11. }  
  12.   
  13. class Sub extends Parent{  
  14.     static{  
  15.         System.out.println("儿子的静态块");  
  16.     }  
  17.     {  
  18.         System.out.println("儿子的非静态块");  
  19.     }  
  20.     public Sub(){  
  21.         System.out.println("儿子的无参构造函数");  
  22.     }  
  23. }  
  24.   
  25. public class ParSubTest {  
  26.   
  27.     /** 
  28.      * @param args 
  29.      */  
  30.     public static void main(String[] args) {  
  31.         new Sub();  
  32.     }  
  33. }  

 执行之后的结果是

Java代码  收藏代码
  1. 老子的静态块  
  2. 儿子的静态块  
  3. 老子的非静态块  
  4. 老子的无参构造函数  
  5. 儿子的非静态块  
  6. 儿子的无参构造函数  

 由此可以得出结论:

0.静态代码块总会在实例对象创建之前执行,因为它是属于类对象级别的代码块,JVM先在内存中分配好了类对象的空间,执行完静态块后再去理会实例对象作用域的东西。

1.总是执行父类的非静态块

2.隐式调用父类的无参构造函数,或者现实调用父类的有参构造函数

3.执行子类的非静态块

4.根据程序需要(就是new后面的构造器函数)调用子类的构造函数

下面来看看一个不太规范的父子程序引发的问题。

Java代码  收藏代码
  1. package se01;  
  2.   
  3. class Par1 {  
  4.   
  5.     private int num = 20;  
  6.   
  7.     public Par1() {  
  8.         System.out.println("par-num:" + num);  
  9.         this.display();  
  10.     }  
  11.   
  12.     public void display() {  
  13.         System.out.println("num:" + num + "   class:"  
  14.                 + this.getClass().getName());  
  15.     }  
  16.   
  17. }  
  18.   
  19. class Sub1 extends Par1 {  
  20.     private int num = 40;  
  21.   
  22.     public Sub1() {  
  23.         num = 4000;  
  24.     }  
  25.   
  26.     public void display() {  
  27.         System.out.println("sub-num:" + num + "   class:"  
  28.                 + this.getClass().getName());  
  29.     }  
  30. }  
  31.   
  32. public class ParSubErrorTest {  
  33.   
  34.     public static void main(String[] args) {  
  35.         new Sub1();  
  36.     }  
  37. }  

 当然,一般在实际项目开发中也不会这么写代码,不过这代码给咱们的启示是揭示了JVM的一些内幕。执行结果是

Java代码  收藏代码
  1. par-num:20  
  2. sub-num:0   class:se01.Sub1  

 就像刚刚得出的5条结论一样,在new Sub1();的时候先要对父类进行构造函数的调用,而父类的构造函数又调用了方法display(),这个时候问题就出现了,父类究竟调用的是谁的构造方法?是父类自己的,还是子类重写的?结论很简单了,就是子类若重写了该方法,那么直接调用子类的重写方法,如果没有重写该方法,那么直接由父类对象直接调用自己的方法即可。由上面程序可以看出子类重写了该display()方法,那么在调用子类的构造函数之前是先调用了父类的无参构造函数,之后在父类无参构造函数中调用了子类重写后的display()方法,而此时,子类对象还没实例化完毕呢,仅仅在内存中分配了相应的空间而已,实例变量仅仅有系统默认值而已,并没有完成赋值的过程,所以,此时子类的实例变量num是默认值0,导致调用子类方法时显示num也是0。而父类的实例变量当然此时已经初始化完毕了,实例对象也有了,自然它的num是赋予初始值后的20喽。

而这程序的问题,或者说不规范的地方在哪里呢?就是它将构造函数用于了其他用途,构造函数实际上就是为了初始化数据用的,而不是用于调用其他方法用的,此程序在构造函数中调用了自己声明的一个public方法,无异于扭曲了构造函数本身的作用,虽然说这么写编译器不会报错,但是无异于给继承机制带来了隐患。

5.       继承机制在处理成员变量和方法时的区别

Java代码  收藏代码
  1. package se01;  
  2.   
  3. class Parent2 {  
  4.     int a = 1;  
  5.     public void test01() {  
  6.         System.out.println(a);  
  7.     }  
  8. }  
  9.   
  10. class Sub2 extends Parent2 {  
  11.     int a = 2;  
  12.     public void test01() {  
  13.         System.out.println(a);  
  14.     }  
  15. }  
  16.   
  17. public class ParSubPMTest {  
  18.   
  19.     public static void main(String[] args) {  
  20.         Parent2 sub2 = new Sub2();  
  21.         Sub2 sub3 = (Sub2)sub2;  
  22.         System.out.println(sub2.a);  
  23.         sub2.test01();  
  24.         System.out.println(sub3.a);  
  25.         sub3.test01();  
  26.     }  
  27. }  

 

输出结果是

Java代码  收藏代码
  1. 1  
  2. 2  
  3. 2  
  4. 2  

 也就是说通过直接访问实例变量的时候是显示父类特性的,当使用方法的时候则显示运行时特性。实际上父子关系在内存中存储是这样的

 

就是说实例对象虽然都是同一个,但是这个实例实际上既存储了自己的变量,也存储了父类的变量,当使用父类声明的对象访问变量时呈现父亲的变量值,使用子类的对象直接访问变量时呈现子类的值。也就是说当我们初始化一个子类对象时,会将它所有的父类(这里是单继承的意思,所有的父类就是说父亲、爷爷、曾祖、曾曾祖父……)的实例变量分配内存空间。如果子类定义的实例变量与父类同名,那么会隐藏父类的变量,并不是完全覆盖,通过父类.变量依然能够获得父类的实例变量。

6.       Java内存管理技巧

1:尽量使用直接量,而尽量不要用new的方式建立这些对象,比如

Java代码  收藏代码
  1. String string = "1";  
  2. Long longlong = 1L;  
  3. Byte bytebyte = 1;  
  4. Short shortshort = 1;  
  5. Integer integer = 22;  
  6. Float floatfloat = 2.2F;  
  7. Double doubledouble = 0.333333;  
  8. Boolean booleanboolean = false;  
  9. Character character = 'm';  

 2:尽量使用StringBuffer和StringBuilder来进行字符串的的链接和使用,这个就不用解释了吧,很常用,尤其是拼接SQL的时候。

3:养成习惯,尽早释放无用对象

例如如下程序:

Java代码  收藏代码
  1. public void test(){  
  2.         StringBuilder stringBuilder = new StringBuilder();  
  3.           
  4.         stringBuilder = null;  
  5.         //很消耗时间………………………………  
  6.     }  

 在很消耗时间的程序执行前将变量就尽量释放掉,让JVM垃圾回收期去回收去。

4:不到万不得以,不要轻易使用static变量,虽然static变量很常用,不过这个类变量会常驻内存,从对象复用的角度讲,倒是省了资源了,但是如果不是经常复用的对象而声明了static变量就会常驻内存,只要程序还在运行就永不会回收。

5:避免创建重复对象变量

Java代码  收藏代码
  1. for(int i=0;i<10;i++){  
  2.     Use use = new Use();  
  3. }  

 如上代码创建了很多个临时对象变量use,实际上可以改进成

Java代码  收藏代码
  1. Use use = null;  
  2. for(int i=0;i<10;i++){  
  3.     use = new Use();  
  4.     use = null;  
  5. }  

 6:尽量不要自己使用对象的finalize方法

不到万不得以,千万不要在此方法中进行变量回收等等操作。

7:如果运行时环境要求空间资源很严格,那么可以考虑使用软引用SoftReference对象进行引用。当内存不够时,它会牺牲自己,释放软引用对象。软引用对象适用于比较瞬时的处理程序,处理完了就完了,内存不够会先将此对象控件腾出来而不回内存溢出的报错误。(关于垃圾回收和对象各种方式的引用会在之后学习笔记中体现)

7.       总结

主要复习了数组的内存形式、父子对象的一些调用陷阱、父子关系在内存中的形式、内存的使用技巧。


原创粉丝点击