Java进阶2 数组内存和对象的内存管理知识 20131028
前言:
在面试的时候,如果是Java的编程语言,也许你认为没有什么可以问的,只能够说明你对于Java了解的太浅了,几乎就是两个星期的节奏速成,没有在底层掌握Java编程语言。那么面试的时候,你就会发现很多的不会,所以在这个时候切记说你懂Java。
还有有些人面试Java认为就是面试SSH框架,其实个人理解方面,除了那种很小型的公司还有不懂技术的什么什么类型的企业,就会拿SSH器标准你。说一下自己的情况:
我的第一编程语言是C++,同时Java是自己的辅助,可以算的上是本科生中学习Java最好的之一(谦虚点了),但是我自己真的对于SSH没有掌握,因为为了面试去学习SSh框架感觉很不值,自己不喜欢为了学习框架而去学习框架。对于Java中的框架,没有1000也有2000的样子,这么多的框架怎么学啊,所以当有需要的时候才可以去学习。我自己掌握Spring的IOC机制,因为在暑假期间的时候确实需要,还有就是数据接口的框架,我自己掌握的是Mybatis框架技术,所以没有去学习Hibernate框架。其实学习Java的关键不是说你会使用多少的框架,而是对于Java编程语言的真正意义上的掌握,而现在大多数人掌握的Java水平只是出于一种简单的语法,根本不了解Java低层次的更深层次的知识,这样面试的时候,你就会暴露出来,因为面试官问你的问题基本在教材中找不到答案,其实Java是一门轻松入门,但是如果想学懂得话,那就真的需要下点苦功夫了。
Chapter 1 Java数组内存分配
1.Java是一种静态编程语言,对应的Java数组也是静态的即,数组被初始化之后,数组占用的空间和数组的长度是不变的。数组初始化的方式有两种:静态初始化和动态初始化。
静态初始化:程序员显示的指定每一个元素的初始值,有系统决定数组的长度;
动态初始化:程序员指定数组的长度,由系统初始化数组的值,数组还可以使用length访问数组的长度。
数组中的所有元素实质上都保存在内存的堆中,数组的名字保存在栈中。
对于字符串数组的话,其实使用的是string pool 实现的,所以在堆中的内存中存放的知识字符串的地址。
2.数组一定要初始化吗
了解Java中数组的内存分配,其实java数组的名字是保存在栈中的,他本身不是数组对象,而是对数组对象的引用,只要让数组的名字指向有效的数组对象即可使用数组变量。这里的数组变量只是一个引用变量,类似C的指针,数组的初始化其实不是对数组变量执行初始化,而是在堆中创建数组对象,在堆中分配一块连续的内存空间。
int []arr = null;
System.out.println(arr);这一段代码是没有任何问题的,因为访问的是arr变量而不是arr的成员方法或者是属性
arr.length就会报错,抛出NullPointerException,因为通过引用变量访问一个还未引用的有效的对象的时候,就会出现这种异常。
public class TestMain {
public static void main(String[] args) {
Person [] students;
students = new Person[2];
//students[0].printInfo(); // error NullPointerException
//students[1].printInfo(); // error NullPointerException
Person a = new Person(10,12.0);
Person b = new Person(138,24.9);
students[0] = a;
students[1] = b;
System.out.println("before change : ");
students[0].printInfo();
a.age = 100;
a.height = 50.9;
System.out.println("after changed : ");
students[0].printInfo();
/*
* 实际上students[0] 和 a 执行的是同一个对象,当修改了a 的时候,对应的students[0]也会随之修改
* 数组内容同样只是对于对象的一个引用,其中的指向的内容才是实际的对象。
*/
}
}
class Person{
public int age;
public double height;
public Person(int a, double height){
this.age = a; this.height = height;
}
public void printInfo(){
System.out.println("age:" + this.age + ", height:" + this.height);
}
}
Chapter 2 对象及其内存管理
虽然Java是有JVM管理内存的,但是作为程序员,也必须了解Java内存管理机制,我们编写源代码不能够仅仅停留在代码层面上,需要考虑每一行代码对于系统的内存影响。Java的内存管理机制比较那一理解,所以可能会感觉Java内存管理和实际开发距离比较远。这是一种错误的理解,虽然JVM会关心程序的内存回收,但是并不意味着我们程序员可以随意的使用系统的内存。
Java内存管理分为两个方面:内存分配和内存回收。内存分配指的是创建Java对象是JVM为该对象在对内存中分配内存空间;内存回收指的是当该Java对象失去引用的时候,变成垃圾,JVM的垃圾回收机制自动清理该对象,并且回收对象占用的内存空间。
JVM内存回收机制是由一条后台线程维护的,而且该线程也是十分消耗资源的,如果我们在程序中肆无忌惮的创建新对象,让系统分配内存,那么这些分配的内存都将有GVM的垃圾回收机制完成回收,这样做的不好的地方是:
不断的分配内存空间是操作系统的内存空间减少,会降低程序的性能;同时大量已经分配内存的回收是的来及回收的负担加重,降低程序的性能。这一章主要介绍内存管理中的内存分配的知识。
2.1实例变量和类变量
成员变量和局部变量
对于局部变量的话存在三种情况:
形参:在方法签名中定义的局部变量,有放大调用者负责为其赋值,随着方法的结束而消亡。
方法内的局部变量:在方法中定义的局部变量,必须在方法内部显示的初始化,从初始化开始生效,并且随着方法的结束失效;
代码块的局部变量:在代码块中显示的初始化,在代码块结束的时候,变量消亡。
成员变量有两种:静态成员变量和普通的成员变量。静态成员变量也就是说成员属于该类而不是Class中的某一个对象。静态变量的初始化,也就是类变量的初始化是在编译的时候,随着class的初始化而得到初始化的,所以静态变量会造编译阶段的时候就已经完成初始化,所以在普通的成员变量可以使用它,无论是在静态 变量之前还是在静态变量之后。但是对于静态成员变量和静态成员变量就会存在一个先后的问题:
public class ErrorDef{
static int num1 = num2 + 3;
static int num2 = 10;
}
但是对于下面的情况是对的
public class RightDef{
int num1 = num2 + 10;
static int num2 = 10;
}
public class TestMain {
int num1 = num2 + 10;
static int num2; //default 0
public static void main(String[] args) {
TestMain main = new TestMain();
System.out.println(main.num2); //0
System.out.println(main.num1);// 10
}
}
在JVM中每一个Class对应一个对象,每一个Class可以创建多个Java对象。所以静态变量只会有一份。在某种意义上来所其实Class也是一个对象,所以的类都是Class的实例。每一个类初始化之后,系统会为该类创建一个对应的Class实例,程序可以通过反射来获得某个类所对应的Class实例: Person.class ,或者是Class.forName(“Person”)即可。
普通的成员变量的初始化时机:对于实例变量来说,他说与Java对象本身,每一次创建一个Java对象,都会需要为实例变量分配内存空间,并且实例变量执行初始化。在程序中可以在三个地方初始化成员变量:
在声明成员变量的时候初始化;非静态的代码块儿中初始化;构造函数中初始化。前两种方式比后一种方式更早的执行。前两种的话,取决于器在程序中代码的位置。仅限于Java编程语言。我们整理一段Java代码:
public class A {
{
a=2;// 创建A的对象的时候,会执行这一段代码,没创建一个对象都会调用这一段代码,执行当然是在构造函数执行之前,而且可以提前初始化值,但是不可以右值,只可以左值。
}
public int a; //只是一个引用
{
System.out.println("code block a = " +a );
}
static {//静态代码块,在加载类的时候执行,切只会执行一次
System.out.println("static A ");
}
public A(){
System.out.println("A.A()");
System.out.println("in A.A() before change a = " + a);
a = 3;
}
}
public class Base {
public A objA;//不会调用构造函数 当然如果我们在这里显示初始化的话,就会调用,在Base构造函数之前调用A的构造函数,执行一系列操作。
static {
System.out.println("Base static code");
}
{
System.out.println("Base code ");
}
public Base(){
System.out.println("Base.Base()");
objA = new A();
}
}
Main{
Base b= new Base();
}
main start
Base static code
Base code
Base.Base()
static A
code block a = 2
A.A()
in A.A() before change a = 2
在Java中,成员变量在声明的时候初始化的底层实现:
double weight = 23.45;其实是分为两部分实现的,当床架Java对象的时候,根据该语句会为其分配内存空间,但是没有初始化值,weight = 23.45;这一句代码会被提取出来到Java的构造器中执行,但不是构造函数。
对于Java编译的知识我们如果想要了解的更详细的话,可以将源代码编译之后生成class 然后使用 javap –c ClassName输出,查看编译的情况。
对于类的变量初始化时机:定义的时候直接初始化;或者使用静态代码块初始化变量。两种方式的执行顺序按照其在代码中声明的顺序执行。
下面看一段代码:
public class Price {
final static Price INSTANCE = new Price(2.9);
static double initPrice = 20;
public double currPrice;
public Price(double discount){
this.currPrice = this.initPrice - discount;
}
}
public class TestMain {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Price.INSTANCE.currPrice);
Price p = new Price(2.9);
System.out.println(p.currPrice);
}
}
//在第一次使用Price类的时候,静态变量调用类的构造函数进行初始化,但是这个时候声明的initPrice没有进行初始化,默认是0,而不是20,所以在调用构造函数的时候会产生复数。
String a = "yang";
System.out.println(System.identityHashCode(a));
String b = "yang";
System.out.println(System.identityHashCode(b));
String c = new String("yang");
System.out.println(System.identityHashCode(c));
2.2继承的执行顺序
public class Base {
static {
System.out.println("Base static code");
}
{
System.out.println("Base not static code ");
}
public Base(){
System.out.println("Base.Base()");
}
public Base(int a){
System.out.println("Base.Base(int )");
}
}
public class Mid extends Base{
static{
System.out.println("Mid static code");
}
{
System.out.println("Mid not static code");
}
Mid(){
super();
System.out.println("Mid.Mid()");
}
Mid(int a){
super(a);
System.out.println("Mid.Mid(int)");
}
}
public class Sub extends Mid {
static {
System.out.println("Sub static code");
}
{
System.out.println("Sub not static code");
}
Sub(){
super(4);
System.out.println("Sub.Sub() ");
}
}
public static void main(String[] args) {
Sub sub = new Sub();
}
Base static code
Mid static code
Sub static code
Base not static code
Base.Base(int )
Mid not static code
Mid.Mid(int)
Sub not static code
Sub.Sub()
执行顺序的理解,其实首先执行的是父类的非静态代码区域,然后是父类的构造函数,但是super默认会执行默认的构造函数,当我们不显示的super执行父类的构造函数类型的时候,需有默认的构造函数,否则会直接报错。其实在super就是指明执行哪一个父类的构造函数。
只要在程序中创建Java对象,系统总是调用最顶层的父类的初始化操作,包括初始化块和构造函数,然后依次向下调用所有的类的初始化操作,最终执行的是本类的初始化操作,返回本类的实例,至于父类中调用哪一个构造函数,分为如下几种情况:1.子类的构造函数中使用super显式的调用父类中的构造函数,系统会根据super的参数列表匹配父类的构造函数,这个是静态绑定,也就是在编译阶段就已经确定了。注意一点如果使用super的话,必须在构造函数中的第一句使用super指明父类的构造函数。2.子类的构造函数中执行体中的第一行代码使用this关键字现实的调用该类中的重载的构造函数,系统图会根据this调用里传入的实参列表来确定该类中的另一个构造器,执行该类的另一个构造函数。3.既没有super关键字,也没有this关键字调用,系统将会在执行子类的构造器之前,隐式的调用默认的父类构造函数。
2.3 访问子类对象中的实例变量
子类中的方法可以访问父类中的实例变量,这是因为子类继承父类就会获得父类的成员变量和方法;但是父类的方法不能够访问子类的实例变量,因为父类不知道他被那个子类继承,他的子类会增加那些变量。
下面分析一段代码:父类 Base ,子类Sub extends Base , 在main中创建一个子类的对象。
public class Base {
private int val = 2;
public Base(){
System.out.println("Base().val =" + this.val);
System.out.println("Base.Base()");
this.display();
System.out.println(this.getClass());
}
public void display(){
System.out.println("Base.val = " + val);
}
public Base(int a){
System.out.println("Base.Base(int )");
}
}
public class Sub extends Base {
private int val= 22;
Sub(){
System.out.println("Sub.Sub() ");
val = 222;
}
public void display(){
System.out.println("Sub val="+val);
}
}
public static void main(String[] args) throws ClassNotFoundException {
Sub sub = new Sub();
}
输出结果是:
Base().val =2首先调用父类的构造函数,其中使用this输出的变量val是在子类中的声明变量val初始化的2
Base.Base()//首先调用父类的构造函数
Sub val=0//这里我们就有点凌乱了,为什么是0,整理一下,首先是初始化类的构造函数,因为集成,所以首先是执行的父类的初始化,所以在上一条中我们输出的结果是在父类中初始化代码块的2,之后我们在父类中使用this直接调用成员变量的话,那么是调用的Base类的成员变量所以显示的是2,但是我们在后面使用的是调用函数,那么就会有多态问题的出现,这个时候调用函数就是更具具体对象的类型去调用函数。所以这里使用this.display()会根据类的多态调用的是子类中的函数。但是在这个时候,我们子类对象知识分配了内存空间,而没有初始化内存,所以这个时候没有执行到子类的成员变量的初始化,但是我们调用子类的成员变量当然是没有初始化的值0.
class yang.main.Sub//我们在程序父类中输出类的值,会发现this指针实际上是子类。
Sub.Sub()。
在这里我们在整理一个概念:Java对象在内存中的空间并不是有构造代码块实现的内存分配,在构造代码块执行之前,其实对象在内存中的空间已经分配,构造代码块完成的是对内存区域的初始化工作。但是在分配内存空间的时候,没有初始化,默认值都是0.,对于应用类型的变量则是NULL。
总结:当变量编译时的类型和运行时类型是不同的,通过变量访问它的而引用对象的实例变量的时候,该实例变量的值是由声明该变量的类型决定的。但是通过该变量弟阿勇他引用对象的成员函数的时候,则会根据他实际的类型确定的。
2.4父类实例的内存控制
public class Base {
public int val = 2;
public void display(){
System.out.println("Base.val = " + val);
}
}
public class Sub extends Base {
public int val= 22;
public void display(){
System.out.println("Sub val="+val);
}
}
public static void main(String[] args) throws ClassNotFoundException {
Base b = new Base();
System.out.println(b.val); //2
b.display(); // 2
Sub sub = new Sub();
System.out.println(sub.val);//22
sub.display();//22
Base btod = new Sub();
System.out.println(btod.val);//2
btod.display();//22
Base btod2 = sub;
System.out.println(btod2.val);//2
btod2.display();//22
}
总结:不管声明对象是哪一种类型的,只要他们实际指向的是一个子类,那么他调用方法就会将多态体现出来;但是如果调用的是成员变量,那么变量的值总是和声明这些对象的类型一致。
对于继承的话,其实继承了父类中的所有的函数和成员变量,但是因为在访问权限上会做一些限制,其实子类在内存中仅仅有一个对象,但是对于父类中的内容是被隐藏掉。
Java程序中允许出现return this的语句但是不会允许出现 return super,因为在Java中不允许直接将super当成一个引用变量使用。
如果在子类中定义了父类中已经定义的变量,这样在Java是允许的,但是会在子类中隐藏掉,我们可以使用super关键字访问
class Parent{public String tag = “yang”;}
class Derived extends Parent{ public String tag = “teng”;}
Main:
Derived d = new Derived();
System.out.println(d.tag);// complie error
System.out.println(((Parent)d).tag); right yang ;
2.4final 修饰符
2.4.1final 修饰变量
被final修饰的实例变量必须显示的指定初始化值,而且只能够在三个位置指定初始化的值
定义final实例变量的时候指定初始值;在非静态代码块中为final实例变量指定初始化值;在构造函数中初始化值。对于final实例变量JVM无法默认初始化值,因此必须有程序员初始化。
对于final 静态变量,就是是使用static声明的变量的话,那么只能在两个地方进行初始化,一个是静态代码块中,一个是声明的地方。
2.4.2执行宏替换
使用final声明的变量在编译阶段的话会执行宏替换,类似C中的define
再有就是Java会缓存所有的字符串常量,如执行String a = “yang”; String b = “yang”; a==b is true ,因为Java的字符串缓冲池的作用,其实指向的是同一个对象的地址。字符串的话如果可以在编译阶段 就可以确定的字符串,那么就会直接进行编译优化,进行替换,前提是在表达式中不存在变量,全部都是常量。
2.4.3final方法是不可以被重写的
class A{ final void funA(){}}
class B extends A {void funA(){} //error}
追梦的飞飞
于广州中山大学图书馆 20131028
HomePage: http://yangtengfei.duapp.com