Java基础之六:关键字this、super、static

来源:互联网 发布:装修手机设计软件 编辑:程序博客网 时间:2024/06/05 12:01

一、this关键字

  

this是一个引用,它指向自身的这个对象。

看内存分析图:

  假设我们在堆内存new了一个对象,在这个对象里面你想象着他有一个引用this,this指向这个对象自己,所以这就是this,这个new出来的对象名字是什么,我们不知道,不知道也没关系,因为这并不影响这个对象在内存里面的存在,这个对象只要在内存中存在,他就一定有一个引用this。

看下面的例子分析:

复制代码
 1 package cn.galc.test; 2  3 public class Leaf { 4  5     int i = 0; 6  7     public Leaf(int i) { 8         this.i = i; 9     }10 11     Leaf increament() {12         i++;13         return this;14     }15 16     void print() {17         System.out.println("i = " + i);18     }19 20     public static void main(String[] args) {21         Leaf leaf = new Leaf(100);22         leaf.increament().increament().print();23     }24 }
复制代码

在内存中分析main方法的执行过程

首先分析第一句话:Leaf leaf = new Leaf(100);

  程序执行到这里的时候,栈空间里面有一个变量leaf,它指向了我们new出来的在堆空间里面的Leaf对象。new这个Leaf对象的时候,调用了构造方法Leaf(),这个构造方法里面有一个形参i,所以在栈空间里面给构造方法分配有一小块内存,名字叫i用来装传递过来的实参。这里传过来的实参是100,所以i里面装的值就是100。得到这个值之后,构造方法继续执行,执行this.i = i;这里就是把栈空间里面的i的值通过值传递给Leaf对象里面的成员变量i,所以成员变量i的值也变成了100。内存中的布局如下图所示:

  

  构造方法执行完之后,为这个构造方法分配的内存消失,所以栈里面的i所标记的那一小块内存会消失。因此第一句话执行完之后,内存中的布局如下图所示:

  

接下来分析第二句话:leaf.increament().increament().print();

  首先逐个分析:leaf.increament(),这里是调用increament()方法,是对new出来的那个Leaf对象调用的,leafLeaf对象的引用对象,因此通过这个引用对象来调用increament()方法,即相当于是Leaf对象自己调用了increament()方法。increament()方法的定义如下:

Leaf increament(){  i++;  return this;}

  因此Leaf对象调用increament()方法时,首先执行方法体里面的第一句话i++;这样就把Leaf对象的成员变量i的值由原来的100变成了101。此时的内存布局如下图所示。

  接下来执行方法体里面的第二句话:return this;

  这里把this作为返回值,当有返回值的时候,首先会在栈里面给这个返回值分配一小块临时的存储空间。这块存储空间里面的内容是this里面的内容。this指向它自身,所以栈内存里面的那块临时存储空间里面装的this也是指向堆内存里面的Leaf对象。

所以leaf.increament().increament().print();这句话里面的left.increament()这一小句话执行完之后,内存中的布局如下图所示。

  

  leaf.increament().increament().print();这句话里面的left.increament()这一小句话执行完之后,返回一个this,此时leaf.increament().increament().print();就相当于是this.increament().print();

  接着栈里面的存储在临时空间里面的this调用increament()方法,而this指的就是Leaf对象,所以又是Leaf对象调用increament()方法。Leaf对象调用increament()方法时,又会执行方法体里面的i++,所以此时i又由原来的101变成了102。然后又执行return this,所以栈内存里面又多了一块临时存储空间,里面装的值也是this,这个this又是指向堆内存里面的Leaf对象。因此此时这个Leaf对象有了四个指向他自己的引用对象。

  leaf.increament().increament().print();这句话里面的leaf.increament().increament()这一小句话执行完之后,都返回了一个this,所以此时的leaf.increament().increament().print();就相当于是这样子的:this.this.print();

  接下来又是栈里面的那个新的this调用print()方法,使用this来调用,那就相当于是Leaf对象来调用,Leaf对象自己调用print()方法将自己的i属性的值打印出来,所以打印出来的结果应该是102

  因此main方法里面的整个程序执行完之后,内存中的布局如下图所示:

  

  this的总结:this一般出现在方法里面,当这个方法还没有调用的时候,this指的是谁并不知道。但是实际当中,你如果new了一个对象出来,那么this指的就是当前这个对象。对哪个对象调用方法,this指的就是调用方法的这个对象(你对哪个对象调用这个方法,this指的就是谁)。如果再new一个对象,这个对象他也有自己的this,他自己的this就当然指的是他自己了。

一、super关键字

  

  在JAVA类中使用super来引用父类的成分,用this来引用当前对象,如果一个类从另外一个类继承,我们new这个子类的实例对象的时候,这个子类对象里面会有一个父类对象。怎么去引用里面的父类对象呢?使用super来引用,this指的是当前对象的引用,super是当前对象里面的父对象的引用。

1.1.super关键字测试

复制代码
 1 package cn.galc.test; 2  3 /** 4  * 父类 5  * @author gacl 6  * 7  */ 8 class FatherClass { 9     public int value;10     public void f() {11         value=100;12         System.out.println("父类的value属性值="+value);13     }14 }15 16 /**17  * 子类ChildClass从父类FatherClass继承18  * @author gacl19  *20  */21 class ChildClass extends FatherClass {22     /**23      * 子类除了继承父类所具有的valu属性外,自己又另外声明了一个value属性,24      * 也就是说,此时的子类拥有两个value属性。25      */26     public int value;27     /**28      * 在子类ChildClass里面重写了从父类继承下来的f()方法里面的实现,即重写了f()方法的方法体。29      */30     public void f() {31         super.f();//使用super作为父类对象的引用对象来调用父类对象里面的f()方法32         value=200;//这个value是子类自己定义的那个valu,不是从父类继承下来的那个value33         System.out.println("子类的value属性值="+value);34         System.out.println(value);//打印出来的是子类自定义的那个value的值,这个值是20035         /**36          * 打印出来的是父类里面的value值,由于子类在重写从父类继承下来的f()方法时,37          * 第一句话“super.f();”是让父类对象的引用对象调用父类对象的f()方法,38          * 即相当于是这个父类对象自己调用f()方法去改变自己的value属性的值,由0变了100。39          * 所以这里打印出来的value值是100。40          */41         System.out.println(super.value);42     }43 }44 45 /**46  * 测试类47  * @author gacl48  *49  */50 public class TestInherit {51     public static void main(String[] args) {52         ChildClass cc = new ChildClass();53         cc.f();54     }55 }
复制代码

运行结果:

  

1.2. 画内存分析图了解程序执行的整个过程

  分析任何程序都是从main方法的第一句开始分析的,所以首先分析main方法里面的第一句话:

ChlidClass cc = new ChlidClass();

  程序执行到这里时,首先在栈空间里面会产生一个变量cccc里面的值是什么这不好说,总而言之,通过这个值我们可以找到new出来的ChlidClass对象。由于子类ChlidClass是从父类FatherClass继承下来的,所以当我们new一个子类对象的时候,这个子类对象里面会包含有一个父类对象,而这个父类对象拥有他自身的属性value。这个value成员变量在FatherClass类里面声明的时候并没有对他进行初始化,所以系统默认给它初始化为0,成员变量(在类里面声明)在声明时可以不给它初始化,编译器会自动给这个成员变量初始化,但局部变量(在方法里面声明)在声明时一定要给它初始化,因为编译器不会自动给局部变量初始化,任何变量在使用之前必须对它进行初始化。

  子类在继承父类value属性的同时,自己也单独定义了一个value属性,所以当我们new出一个子类对象的时候,这个对象会有两个value属性,一个是从父类继承下来的value,另一个是自己的value。在子类里定义的成员变量value在声明时也没有给它初始化,所以编译器默认给它初始化为0因此,执行完第一句话以后,系统内存的布局如下图所示:

 

接下来执行第二句话:

1 cc.f();

  当new一个对象出来的时候,这个对象会产生一个this的引用,这个this引用指向对象自身。如果new出来的对象是一个子类对象的话,那么这个子类对象里面还会有一个super引用,这个super指向当前对象里面的父对象。所以相当于程序里面有一个this,this指向对象自己,还有一个super,super指向当前对象里面的父对象。

  这里调用重写之后的f()方法,方法体内的第一句话:“super.f();”是让这个子类对象里面的父对象自己调用自己的f()方法去改变自己value属性的值,父对象通过指向他的引用super来调用自己的f()方法,所以执行完这一句以后,父对象里面的value的值变成了100。接着执行“value=200;”这里的vaule是子类对象自己声明的value,不是从父类继承下来的那个value。所以这句话执行完毕后,子类对象自己本身的value值变成了200。此时的内存布局如下图所示:

  方法体内的最后三句话都是执行打印value值的命令,前两句打印出来的是子类对象自己的那个value值,因此打印出来的结果为200,最后一句话打印的是这个子类对象里面的父类对象自己的value值,打印出来的结果为100

  到此,整个内存分析就结束了,最终内存显示的结果如上面所示。

一、static关键字

  

  原来一个类里面的成员变量,每new一个对象,这个对象就有一份自己的成员变量,因为这些成员变量都不是静态成员变量。对于static成员变量来说,这个成员变量只有一份,而且这一份是这个类所有的对象共享。

1.1.静态成员变量与非静态成员变量的区别

  以下面的例子为例说明

复制代码
 1 package cn.galc.test; 2  3 public class Cat { 4  5     /** 6      * 静态成员变量 7      */ 8     private static int sid = 0; 9 10     private String name;11 12     int id;13 14     Cat(String name) {15         this.name = name;16         id = sid++;17     }18 19     public void info() {20         System.out.println("My Name is " + name + ",NO." + id);21     }22 23     public static void main(String[] args) {24         Cat.sid = 100;25         Cat mimi = new Cat("mimi");26         Cat pipi = new Cat("pipi");27         mimi.info();28         pipi.info();29     }30 }
复制代码

通过画内存分析图了解整个程序的执行过程

  执行程序的第一句话:Cat.sid = 100;时,这里的sid是一个静态成员变量,静态变量存放在数据区(data seg),所以首先在数据区里面分配一小块空间sid,第一句话执行完后,sid里面装着一个值就是100

  此时的内存布局示意图如下所示

  

  接下来程序执行到:

    Cat  mimi = new Cat(“mimi”);

  这里,调用Cat类的构造方法Cat(String name),构造方法的定义如下:

    Cat ( String name){

      this.name = name;

      id=sid++;

    }

  调用时首先在栈内存里面分配一小块内存mm,里面装着可以找到在堆内存里面的Cat类的实例对象的地址,mm就是堆内存里面Cat类对象的引用对象。这个构造方法声明有字符串类型的形参变量,所以这里把“mimi”作为实参传递到构造方法里面,由于字符串常量是分配在数据区存储的,所以数据区里面多了一小块内存用来存储字符串“mimi”。此时的内存分布如下图所示:

  

  当调用构造方法时,首先在栈内存里面给形参name分配一小块空间,名字叫name,接下来把”mimi”这个字符串作为实参传递给name,字符串也是一种引用类型,除了那四类8种基础数据类型之外,其他所有的都是引用类型,所以可以认为字符串也是一个对象。所以这里相当于把”mimi”这个对象的引用传递给了name,所以现在name指向的是”mimi”。所以此时内存的布局如下图所示:

  接下来执行构造方法体里面的代码:

    this.name=name;

  这里的this指的是当前的对象,指的是堆内存里面的那只猫。这里把栈里面的name里面装着的值传递给堆内存里面的cat对象的name属性,所以此时这个name里面装着的值也是可以找到位于数据区里面的字符串对象“mimi”的,此时这个name也是字符串对象“mimi”的一个引用对象,通过它的属性值就可以找到位于数据区里面的字符串对象“mimi”。此时的内存分布如下图所示:

  

  接下来执行方法体内的另一句代码:

    id=sid++;

  这里是把sid的值传递给id,所以id的值是100sid传递完以后,自己再加1,此时sid变成了101。此时的内存布局如下图所示。

  

  到此,构造方法调用完毕,给这个构造方法分配的局部变量所占的内存空间全部都要消失,所以位于栈空间里面的name这块内存消失了。栈内存里面指向数据区里面的字符串对象“mimi”的引用也消失了,此时只剩下堆内存里面的指向字符串对象“mimi”的引用没有消失。此时的内存布局如下图所示:

  

  接下来执行:Cat  pipi = new Cat(“pipi”);

  这里是第二次调用构造方法Cat(),整个调用过程与第一次一样,调用结束后,此时的内存布局如下图所示:

  

  最后两句代码是调用info()方法打印出来,打印结果如下:

  

  通过这个程序,看出来了这个静态成员变量sid的作用,它可以计数。每当有一只猫new出来的时候,就给它记一个数。让它自己往上加1

  程序执行完后,内存中的整个布局就如上图所示了。一直持续到main方法调用完成的前一刻。

  这里调用构造方法Cat(String name) 创建出两只猫,首先在栈内存里面分配两小块空间mimipipi,里面分别装着可以找到这两只猫的地址,mimipipi对应着堆内存里面的两只猫的引用。这里的构造方法声明有字符串类型的变量,字符串常量是分配在数据区里面的,所以这里会把传过来的字符串mimipipi都存储到数据区里面。所以数据区里面分配有存储字符串mimipipi的两小块内存,里面装着字符串“mimi”和“pipi”,字符串也是引用类型,除了那四类8种的基础数据类型之外,其他所有的数据类型都是引用类型。所以可以认为字符串也是一个对象。

  这里是new了两只猫出来,这两只猫都有自己的idname属性,所以这里的idname都是非静态成员变量,即没有static修饰。所以每new出一只新猫,这只新猫都有属于它自己的idname,即非静态成员变量idname是每一个对象都有单独的一份。但对于静态成员变量来说,只有一份,不管new了多少个对象,哪怕不new对象,静态成员变量在数据区也会保留一份。如这里的sid一样,sid存放在数据区,无论new出来了多少只猫在堆内存里面,sid都只有一份,只在数据区保留一份。

  静态成员变量是属于整个类的,它不属于专门的某个对象。那么如何访问这个静态成员变量的值呢?首先第一点,任何一个对象都可以访问这个静态的值,访问的时候访问的都是同一块内存。第二点,即便是没有对象也可以访问这个静态的值,通过“类名.静态成员变量名”来访问这个静态的值,所以以后看到某一个类名加上“.”再加上后面有一个东西,那么后面这个东西一定是静态的,如”System.out”,这里就是通过类名(System类)再加上“.”来访问这个out的,所以这个out一定是静态的。

再看下面的这段代码

复制代码
 1 package cn.galc.test; 2  3 public class Cat { 4  5     /** 6      * 这里面的sid不再是静态成员变量了,因为没有static修饰符, 7      * 此时它就是类里面一个普通的非静态成员变量,和id,name一样, 8      * 成为每一个new出来的对象都具有的属性。 9      */10     private  int sid = 0;11 12     private String name;13 14     int id;15 16     Cat(String name) {17         this.name = name;18         id = sid++;19     }20 21     public void info() {22         System.out.println("My Name is " + name + ",NO." + id);23     }24 25     public static void main(String[] args) {26         //Cat.sid = 100;这里不能再使用“类.静态成员变量”的格式来访问sid了,因为sid现在变成了非静态的成员变量了。所以必须要把这句话注释掉,否则无法编译通过。27         Cat mimi = new Cat("mimi");28         Cat pipi = new Cat("pipi");29         mimi.info();30         pipi.info();31     }32 }
复制代码

  这段代码与上一段代码唯一的区别是把声明sid变量的static修饰符给去掉了,此时的sid就不再是静态成员变量,而是非静态成员变量了,此时每一个new出来的cat对象都会有自己单独的sid属性。所以这段代码执行完成后,内存中的布局如下图所示:

  

  由于sid变成了非静态成员变量,所以不再有计数的功能了。sididname属性一样,成为每一个new出来的对象都具有的属性,所以每一个new出来的cat都加上了一个sid属性。由于不能再使用”类名.静态成员对象名”的格式访问sid,所以代码的第一句”Cat.sid =100;”不能这样使用,否则编译会出错,必须把这句话注释掉才能编译成功。既然无法访问得到sid的值,所以sid的值就一直都是初始化时赋给的值0。直到调用构造方法时,执行到方法体内的代码id=sid++;时,sid首先把自身的值0赋值给id,所以id的值是0,然后sid自己加1,所以sid变成了1

  所以静态变量和非静态变量的区别就在于静态变量可以用来计数,而非静态变量则不行。

  理解了内存,就理解了一切,就理解了各种各样的语言。所有的语言无非都是这样:局部变量分配内存永远在栈里面,new出来的东西分配内存永远是在堆里,静态的东西分配内存永远是在数据区。剩下的代码肯定是在代码区。所有的语言都是这样。

  在一个静态方法里,如果想访问一个非静态的成员变量,是不能直接访问的,必须在静态方法里new一个对象出来才能访问。如果是加了static的成员变量,那么这个成员变量就是一个静态的成员变量,就可以在main方法里面直接访问了。

  main方法是一个静态的方法,main方法要执行的时候不需要new一个对象出来。

  动态方法是针对于某一个对象调用的,静态方法不会针对某一个对象来调用,没有对象照样可以用。所以可以使用”classname.method()”.的形式来调用静态方法。所以想在main方法里面访问非静态成员变量是不可以的,想在main方法里面访问非静态方法也是不可以的,因为非静态方法只能针对于某个对象来调用,没有对象,就找不到方法的执行者了。

  成员变量只有在new出一个对象来的时候才在堆内存里面分配存储空间。局部变量在栈内存里面分配存储空间。

  静态方法不再是针对某一个对象来调用,所以不能访问非静态的成员。

  非静态成员专属于某一个对象,想访问非静态成员必须new一个对象出来才能访问。

  静态的变量可以通过对象名去访问,也可以通过类名去访问,两者访问的都是同一块内存。


0 0
原创粉丝点击