昨天初步学习完了类的三个成员并拓展延伸了些其他知识,今天就继续深入学习Java面向对象的基础。
在昨天的最后一个程序中,我们发现,eclipse自动生成的代码里有this这个关键字,我们来看看昨天写的具体例子吧:
public class StandardClass {
private int num;
public void setNum(int num) {
this.num = num;
}
我们可以发现:成员变量的名称叫num,而setNum方法里的形参名称也叫num,这是在方法体中若要使用num这个变量,一定是使用形参的num(变量的就近原则),那么成员变量的num就使用不上了,这可就头疼了,(当然了你可以选择形参不与成员变量重名,问题也就没了),但是我就是要重名呢,哈哈,Java为了解决这个问题,引入了this关键字,在方法中用this.成员变量名的方式就可以访问成员变量了!我们不妨多分析一下,昨天我们学习了成员变量是属于对象的,只有对象.成员变量才能使用,那么我们来对应一下,现在是this.成员变量,这么说this就是对象呗!那么又有问题了,this是这个类的对象没错,但是不同对象之间是调用不同的构造方法来实现的,可是this好像没有调用构造方法啊,那么this为什么还是个对象呢?诶,不妨想想,this是在方法中使用的,而这种方法是需要创建的对象来调用的,哦!原来,this就是创建好的使用该方法的对象!
好,总结一下,this的第一个用法:在程序中产生二义性之处,应使用this来指明当前对象;普通方法中,this总是指向调用该方法的对象。构造方法中,this总是指向正要初始化的对象。
this还有第二个用法: 使用this关键字调用重载的构造方法,避免相同的初始化代码。但只能在构造方法中用,并且必须位于构造方法的第一句。
public class StandardClass {
private int num;
private char ch;
public StandardClass(){
}
public StandardClass(int num) {
this.num = num;
}
public StandardClass(int num,char ch) {
// StandardClass(num); //错误的,没法直接使用
this(num); //相当于使用了StandardClass(int num)构造方法,必须在第一句
this.ch=ch;
}
注意:this关键字不能用于static方法中!
对象创建过程和this的本质:
构造方法是创建Java对象的重要途径,通过new关键字调用构造器时,构造器也确实返回该类的对象,但这个对象并不是完全由构造器负责创建。创建一个对象分为如下四步:
1. 分配对象空间,并将对象成员变量初始化为0或空
2. 执行属性值的显示初始化
(到这里就已经创建好了,有地址值了)
3. 执行构造方法
4. 返回对象的地址给相关的变量
this的本质就是“创建好的对象的地址”! 由于在构造方法调用前,对象已经创建。因此,在构造方法中也可以使用this代表“当前对象” 。
在昨天的学习中,我们已经初步了解了静态变量和静态方法,当时呢只知道它们是属于类的不属于对象,我们现在来深入系统学习下。
在类中,用static声明的成员变量为静态成员变量,也称为类变量。 类变量的生命周期和类相同,在整个应用程序执行期间都有效。它有如下特点:
1. 为该类的公用变量,属于类,被该类的所有实例共享。说简单点,对象可以创建无数个,若没用static修饰的成员变量,不同的对象可以有不一样值的成员变量,但是对于用static修饰的类变量,所有对象的类变量值都是一样的!
2. 对于该类的所有对象来说,static成员变量只有一份(都一样的,不必要占据其他内存来存储了,就保存在类中就行)。被该类的所有对象共享!!
3. 一般用“类名.类属性”来调用。(也可以通过对象引用或类名(不需要实例化)访问静态成员。)
在类中,用static声明的方法是静态方法,它有如下特点:
1. 静态方法不属于对象,属于类。不过可以写成对象.静态方法,但是不推荐,一般都是类名称.静态方法。
2. 在本类方法中调用静态方法可以省略类名称。
3. 在static方法中不可直接访问非static的成员,因为它们是属于对象的(但是在非静态方法中可直接访问静态成员)。
4. 类的构造方法不能用static修饰。(想想也是,static修饰的东西都是属于类的,而构造方法是用来创建对象的,如果用static修饰构造方法这不是矛盾嘛!很好,又来问题了:成员变量使用构造方法初始化,那么类变量的初始化呢,还要在main方法里写类.类变量=啥的?嗯,有办法的,先卖个关子)
总结:静态成员变量和静态方法的本质都是一样的,从属于类,被所有对象共享!
package cn.zjb.test;
/**
* 测试static的使用
* @author 张坚波
*
*/
public class TestStatic {
static int num; //类变量,被所有对象共享
int num2;
/*
* static方法
* 属于类的,被所有对象共享
*/
public static void printNum() {
// System.out.println(num2); //错误,静态方法中无法使用非静态变量,因为没有创建对象
// System.out.println(new TestStatic().num2); //合法
System.out.println(num); //静态方法中可以使用静态变量
}
public void printNum2() {
// System.out.println(num); //合法,非静态方法中可以使用静态变量
// printNum(); //合法,非静态方法中可以使用静态方法
System.out.println(num2); //非静态方法中可以使用非静态变量
}
public static void main(String[] args) {
//测试类变量
TestStatic ts=new TestStatic(); //ts对象
ts.num=1; //ts对象的num赋值
ts.num2=2; //ts对象num2赋值
TestStatic ts2=new TestStatic(); //ts2对象,不赋值
System.out.println(ts.num);
System.out.println(ts2.num); //ts2的静态变量已经有初始值了,虽然没给ts2的num赋值,但是ts赋值了,所以所有对象的num都有值了
System.out.println(ts.num2);
System.out.println(ts2.num2); //ts2的成员变量还是默认值
//其实上面的对象.num可以直接写成TestStatic.num的,后者还更好,强调了类变量从属于类
TestStatic.printNum(); //静态方法可以在静态方法中直接使用
new TestStatic().printNum2(); //非静态方法要借助对象才能在静态方法中调用
}
}
为了更好地学习Java程序的执行过程,咱们来学习一下Java的底层实现,这一章个人觉得非常重要,也看过一些视频和教程,大同小异。整个过程还是比较复杂,而且随着JDK的升级也有改动(方法区的变化最大),现阶段也没必要都会,咱们就先学习有助于理解程序运行的几个部分和关键过程。PS:要知道升级一定是让Java程序运行的更高效流畅,原理是没啥变化的。
JVM内存结构具体划分为如下5个内存空间:
栈(Stack):
JVM栈描述的是Java方法执行的内存模型,JVM为每个线程创建一个栈。
每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表(方法中的所有局部变量)、操作栈(运算的时候要用的,类似于数据结构中用栈来实现运算表达式过程)、动态链接(不了解,目前好像还没涉及)、方法出口(方法会有返回值)等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。
栈是线程私有,不能实现线程之间的共享。
栈由系统自动分配,是一个连续的内存空间,速度快!
堆(Heap):
存放所有new出来的东西,即存放对象信息。堆里面存的数据,不用初始化都有默认值,和成员变量的默认值规则是一致的。
JVM只有一个堆,被所有线程共享。
堆是一个不连续的内存空间,分配灵活,速度慢!
方法区(Method Area):
实质上也是一个堆,存放被虚拟机加载的类信息、常量等。
JVM只有一个方法区,被所有线程共享!
程序计数器(PC Register)
好比与计组中经常接触的PC,存下一条指令的地址,JVM的程序计数器是指示当前线程所执行的字节码的行号指示器。暂时还不需要了解。
本地方法栈(Native Method Stack)
现阶段没必要了解。
总之,要弄清Java程序的运行过程,只要知道前三点即可。
来个实例看看:
第一步:将程序中的所有类的信息存放到方法区。对应的是javac的过程。
第二步:找到类中的main方法,压栈,因为这是程序的入口。将main方法中的局部变量进行默认值的赋值。
第三步:从main方法依次执行语句,正巧第一句是对象的创建,我们发现,在栈中的对象名的值是真实对象的地址,就像指针一样,是种指向关系。(本来String的默认值是null的,写错了)
之后就依次执行语句。
最后:全部执行完语句,main方法出栈,程序结束。(这里还漏了Computer()的进出栈,就不改了,我太菜了)
从这里我们还可以巩固前面学的知识:
this是什么呢?就是我们在堆里面存的那个地址值,所以它才指向的是当前对象;
非静态的变量和方法为什么不能在静态方法里直接用?因为没有对象就找不到。
现在只是粗略学习,在能力更强后会深入学习,并且了解JDK更新的变动。
在运行一个Java程序的时候,JVM会自动拿出一部分固定大小内存空间创建一个堆(也就是把这部分当作是堆),这个堆里要放方法区还要放创建好的对象。那么问题就出现了!当我们的对象创建过多的时候(且系统不做清理无用的对象工作),是不是内存终究会被全部占用,这时候程序就会崩溃啦!所以,我们就需要垃圾回收机制(Garbage Collection),把无用的对象都清理掉,这样就解决问题了!Java的内存管理很大程度指的就是对象的管理,其中包括对象空间的分配和释放。
对象空间的分配:使用new关键字创建对象即可
对象空间的释放:将对象赋值null即可。垃圾回收器将负责回收所有无用对象的内存空间。
垃圾回收相关算法(了解一下吧):
Java通用的垃圾回收机制:分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 、Tenured/Old和Perm 空间,这里的堆就是JVM内存结构的那个堆。
1. 年轻代(Eden、 Survivor1、 Survivor2)
所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
2. 年老代(Tenured/Old)
在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。
3. 持久代(Perm)
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。
三个新概念:
·Minor GC:
用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)
·Major GC:
用于清理老年代区域。
·Full GC:
用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
另外,提一下,Java在开发中容易造成内存泄漏,需要对JVM调优。在对JVM调优的过程中,很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC:
1.年老代(Tenured)被写满;
2.持久代(Perm)被写满;
3.System.gc()被显式调用(程序建议GC启动,不是调用GC)是否调用垃圾回收机制永远都是由系统自己觉得的,程序员只能是建议;
4.上一次GC之后Heap的各域分配策略动态变化。
内存泄漏:程序中已动态分配的堆内存由于某种原因程序未能释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重情况。
在第一天就有了解了,现在来看看具体定义。包机制是Java中管理类的重要手段。 开发中,我们会遇到大量同名的类,通过包我们很容易对解决类重名的问题,也可以实现对类的有效管理。 包对于类,相当于文件夹对于文件的作用。以前说包是对Java程序的管理,其实不太对,准确的是对类的管理!
注意:我定义过的cn.zjb和cn.zjb.test包是没有包含关系的(表面上的逻辑包含关系是不存在的),它们就是两个独立的包!
Java比C语言强的地方有一点就是,C语言的代码几乎都要自己全部实现,但是Java不同,Java通过包机制把很多常用的类都提前写好了,打包起来。要使用它们直接调用就可以了。来看看JDK主要的包吧:
其中,java.lang包是所有Java程序都会自动导入的,不用特别写。
那么,如果我想用其他包中的类呢?其语法是:
import 包名.类名;
注意: .类名 是具体导入哪个类,还可以用 .* 来表示导入该包下所有的类。这种方式会降低编译速度,但不会降低运行速度。
如果导入两个同名的类,只能用包名+类名来显示调用相关类。
总结: