9.3 子父类中成员变量特征体现

最后更新于:2022-04-01 14:11:48

这一节我们来说一说继承中子父类中的成员变量有什么特征。 我们先看从一个例子说起: ~~~ class Father//父类 { int age1 = 48; } class Son extends Father//子类 { int age2 = 22; public void printInfo() { System.out.println("父亲的年龄:"+age1+"\n儿子的年龄:"+age2); } } class ExtendTest2 { public static void main(String[] args) { Son son = new Son(); son.printInfo(); } } ~~~ 我们上面的例子中有一个儿子类继承了一个父亲类,我们看到子类中访问了父类的年龄这个成员变量,那么我们看下结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422b0c6b5.jpg) 这时输出了正确的结果。 但是我们会发现这两个成员变量的名称是不一样的,那么我们来看一种特殊情况,当两个成员变量名一样时,会是什么样子呢? ~~~ class Father//父类 { int age = 48; } class Son extends Father//子类 { int age = 22; public void printInfo() { System.out.println("父亲的年龄:"+age+"\n儿子的年龄:"+age); } } ~~~ 此时我们再看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422b1eeb3.jpg) 我们发现父亲和儿子的年龄都是儿子的年龄,这就出现了问题,当子父类中的成员变量名相同时,我们发现会直接使用子类中的成员变量。 那么怎么解决这个问题呢? 我们很自然想起前面学习时当一个类中成员变量和局部变量同名时,我们使用了this关键字进行了区分,那么这里有没有一个标记让我们区分子父类中的同名成员变量呢? 答案当然是肯定的,这个关键字就是super关键字。 我们来看例子: ~~~ class Father//父类 { int age = 48; } class Son extends Father//子类 { int age = 22; public void printInfo() { System.out.println("父亲的年龄:"+super.age+"\n儿子的年龄:"+this.age); } } ~~~ 这时我们再看一下运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422b308e3.jpg) 很显然,结果就是我们想要的。所以我们看到一个新关键字super,当子父类中的成员变量同名时,我们用super关键字来区分。 同时,我们会发现,this关键字和super关键字的用法很相似,不过我们一定要明确的是,它们只是相似,而不是相同哦! 为什么这么说呢?我们来说一下两个关键字的不同之处: this关键字:代表一个本类对象的引用,这我们之前就已经学习了。 super关键字:并不代表父类对象,而代表一个父类的空间。因为我们这里根本就没创建父类的对象,只有子类的对象。 下面我们再来看一个比较容易产生疑问的地方,就是父类中的私有成员,子类具有访问权限吗? 我们直接看例子: ~~~ class Father//父类 { private int age = 48; } class Son extends Father//子类 { int age = 22; public void printInfo() { System.out.println("父亲的年龄:"+super.age+"\n儿子的年龄:"+this.age); } } ~~~ 我们把父类中的成员变量私有化,然后看运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422b41467.jpg) 我们会发现在编译时就已经报了错,很明显我们在子类中是不能直接访问父类中的私有内容。 那么我们要怎么访问呢,我们可以用以下方法: ~~~ class Father//父类 { private int age = 48; public int getAge() { return this.age; } } class Son extends Father//子类 { int age = 22; public void printInfo() { System.out.println("父亲的年龄:"+super.getAge()+"\n儿子的年龄:"+this.age); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422b5532f.jpg) 我们可以通过上面的方法去访问一个类中的私有成员哦。
';

9.2 单继承与多重继承

最后更新于:2022-04-01 14:11:46

这一节我们来看java中的单继承和多重继承。 **在java语言中,支持的是单继承,不直接支持多继承**,但是对C++中的多继承进行了改良。 那么什么是单继承和多继承呢? 单继承:一个子类只能有一个直接父类。 多继承:一个子类可以有多个直接父类。这个在java中是不允许的。 为什么不允许呢?我们看一个例子: ~~~ class A { void method() { System.out.println("a"); } } class B { void method() { System.out.println("b"); } } class C extends A,B { } ~~~ 对于上面的情况,如果当我们调用C对象的method方法时,就会出现不确定性错误了,java也是以这个问题进行改良,从而不直接支持多继承。 所以我们可以看到,java中不直接支持多继承的原因就是:**当多个类中有相同成员时会产生调用的不确定性。** java语言中还支持多重继承。 比如下面的例子就是一个多重继承: ~~~ class A { } class B extends A { } class C extends B { } ~~~ 这就是多重继承的形式,对于上面的继承,就出现了继承体系,而在这个继承体系中,A是最顶层类,C是对子类。 通过上面的继承体系我们不难看出,A实现的是最基本最基础的功能。 那么我们在实际开发中应该如何使用这个继承体系呢? **第一步:查看该体系中的顶层类(这里是A),了解该体系的基本功能。** **第二步:创建体系中的最子类对象(这里是C),完成功能的使用。** 通过上面的两步,我们就可以按需使用一个继承体系了。 这两节我们一直在说继承,在9.1中我们也说到继承可以提高代码的复用性,但是我们一定要**注意:千万不要为了提高代码复用性去定义继承**。 这是为什么呢?我们看下面例子: ~~~ class A { void method1(){} void method2(){} } class B { void method1(){} void method3(){} } ~~~ 我们发现两个类中都有method1方法,那么我们为了提高复用性而去继承: ~~~ class A { void method1(){} void method2(){} } class B extends A { void method3(){} } ~~~ B类中确实有了method1方法,可以也同时继承了method2方法,但method2方法其实并不是B中的方法,这就出现了问题。 这就让我们必须明确什么时候定义继承:**当类与类之间存着所属关系时,就定义继承**。 那么什么是所属关系呢?比如X是Y中的一种,就是说X属于Y,就是一种所属关系。 我们这样表示所属关系:**is a 关系,就是所属关系。** 所以当我们发现两个类是一种is a 关系时,我们就可以定义继承。
';

9.1 继承

最后更新于:2022-04-01 14:11:44

这一节我们学习面向对象的第二个特征——继承。 那么什么是继承呢?继承有作用是什么? 我们先通过一个例子来说明: ~~~ class Student//学生类 { String name;//姓名 int age;//年龄 void printInfo()//打印信息 { System.out.println("姓名:"+name+"\t年龄:"+age); } void study()//学生在学习 { System.out.println(name+"正在学习。"); } } class Worker//工人类 { String name;//姓名 int age;//年龄 void printInfo()//打印信息 { System.out.println("姓名:"+name+"\t年龄:"+age); } void work()//工作在工作 { System.out.println(name+"正在工作。"); } } class ExtendTest { public static void main(String[] args) { Student student = new Student(); student.name = "小强"; student.age = 15; student.printInfo(); student.study(); Worker worker = new Worker(); worker.name = "光头强"; worker.age = 28; worker.printInfo(); worker.work(); } } ~~~ 我们看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422ad4bd5.jpg) 这里我们定义了两个类,一个是学生类,一个是工人类,但是我们会发现,上面的代码重复的内容太多了,与我们要实现的代码复用相悖,那么我们可以看到,对于学生和工作,它们有的共同之处就是它们都是一个人,有人的所有特征,那么我们在这里就可以重新定义一个Person类,如下: ~~~ class Person { String name; int age; void printInfo() { System.out.println("姓名:"+name+"\t年龄:"+age); } } ~~~ 在Person类中,我们封装了人的共性特征,姓名和年龄,以及一个打印信息的方法。 那么我们该怎么做呢?当然就是要建立Student类和Worker类与Person类之间的关系了,这就是我们这一节要说的继承,让Student类和Worker类都继承于Person类,就可以直接继承这些共有成员了。 我们可以extends关键字来实现继承: ~~~ class Student extends Person//学生类 { void study()//学生在学习 { System.out.println(name+"正在学习。"); } } class Worker extends Person//工人类 { void work()//工作在工作 { System.out.println(name+"正在工作。"); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422ad4bd5.jpg) 这时我们会发现,运行结果正常,也是我们所要看到的,但是比较之前的代码,我们真正意义上提高了代码的复用性。 由此我们也可以总结出继承的优点: 1.提高了代码的复用性。 2.让类与类之间产生的关系,为面向对象的第三个特征“多态”提供了前提。
';

8.8 构造代码块

最后更新于:2022-04-01 14:11:41

这一节我们再看一个特殊的代码块,那就是构造代码块。 这里我们简单的通过例子来说明一下: ~~~ class Person { private String name; { System.out.println("Person类的第一个代码块被执行"); } Person() { System.out.println("无参数构造函数被执行"); this.name = "小宝宝"; } Person(String name) { System.out.println("有name参数构造函数被执行"); this.name = name; } public void speak() { System.out.println("名字:"+name); } } class ConBlockTest { public static void main(String[] args) { Person p1 = new Person(); p1.speak(); Person p2 = new Person("小科比"); p2.speak(); } } ~~~ 我们在这个例子中看到了Person类中有一个代码块,它没有被static关键字修饰,这就是我们这一节所说的构造代码块,为什么这么说呢,我们看运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a9c286.jpg) 我们很显然就看到了在我们创建两个对象时,该代码块都被执行了,而构造函数只是当创建对应对象时被调用。 所以构造代码块的作用就是:**给所有对象进行相同部分的初始化。** 而我们的构造方法是对对应的对象进行有针对性的独特的初始化。 那么构造代码块的构造函数哪个先执行呢?我们看代码: ~~~ class Person { private String name; {//第一个构造代码块 System.out.println("Person类的第1个代码块被执行"); } Person() { System.out.println("无参数构造函数被执行"); this.name = "小宝宝"; } Person(String name) { System.out.println("有name参数构造函数被执行"); this.name = name; } public void speak() { System.out.println("名字:"+name); } {//第二个构造代码块 System.out.println("Person类的第2个代码块被执行"); } } ~~~ 我们看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422aae4e6.jpg) 我们看到两个不同位置的构造代码块都在构造函数被执行之前就已经执行了,所以说**构造代码块优先于构造函数执行。** ** ** 所以,当我们需要把所有对象都有相同的初始化时,我们可以使用构造代码块来实现,比如上面的例子中,人一出生都会哭,那么我们就可以用构造代码块来初始哭这个功能: ~~~ class Person { private String name; { cry(); } Person() { this.name = "小宝宝"; } Person(String name) { this.name = name; } public void cry() { System.out.println("哇哇"); } public void speak() { System.out.println("名字:"+name); } } ~~~ 这样我们就把所有对象哭的功能封装到了一个构造代码块中,在创新对象是会优先执行,很好的实现了我们想要的功能。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422abf8e0.jpg)
';

8.7 静态代码块

最后更新于:2022-04-01 14:11:39

这一节我们看一个比较特殊的概念,那就是静态代码块。 前面我们也提到过代码块,就是一段独立的代码空间,那么什么是静态代码块呢?说白了,**就是用static关键字修饰的代码块。** 我们来看一个例子: ~~~ class StaticBlock { static { System.out.println("静态代码块被执行"); } void myPrint() { System.out.println("myPrint方法执行"); } } class StaticBlockTest { public static void main(String[] args) { new StaticBlock().myPrint(); } } ~~~ 我们来看一看运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a62405.jpg) 从结果我们看到了我们调用的myPrint函数被调用执行了,并且在此函数被调用之前,静态代码块就已经被执行。 这就是我们要说的特别之处,静态代码块是特殊的代码块,它被static关键字修饰,并且拥有静态的所有特征,最主要的是它有一个比较自然独特的特点:我们之前说,**静态随着类的加载而加载,而静态代码块随着着类的加载而执行,只要类被加载,那么该静态代码块就会被执行,并且只执行一次。** 我们看下面的测试: ~~~ class StaticBlockTest { public static void main(String[] args) { new StaticBlock().myPrint(); new StaticBlock().myPrint(); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a7351b.jpg) 我们看到,静态代码块只执行了一次,而我们的myPrint方法被调用了两次执行了两次。所以当类加载时,静态方法就已经加载并且执行一次。 通过上面的例子让我们明确了静态代码块的实际作用:**用于给类进行初始化。** 这就相当于我们之前学习的构造函数,构造函数是用于给对象进行初始化,而静态代码块是用来给类进行初始化。 这里我们也许会有疑问,既然构造函数能够进行初始化,那么我们为什么还要用静态代码块来初始化呢,其实不是所有的类都能创建对象,因为有些类有可能不需要被创建对象的,比如我们在一个类中定义的全部是静态成员,那么创建对象就没有意义。 我们再看一个静态代码块的用法,我们看下面的代码: ~~~ class StaticBlock { static int num; static { num = 10; num = num * 3; } void myPrint() { System.out.println("num = "+num); } } class StaticBlockTest { public static void main(String[] args) { new StaticBlock().myPrint(); } } ~~~ 我们来看运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a86ac0.jpg) 从结果我们直接可以看到,我们在静态代码块中对静态变量进行了多次运算和赋值,所以当我们需要对静态变量进行多次运算时我们可以运用静态代码块。 不过这个在开发中用的并不多,在一些底层的框架开发中会专门用到。 这一节我们就简单的学到这里。
';

8.6 静态的内存加载

最后更新于:2022-04-01 14:11:37

之前几节我们一直在说static关键字所修饰的静态,那么在程序运行时,内存中static是如何体现的呢?这一节我们就来看一看。 我们还是先看一个例子,希望我们通过对这个例子的分析让我们初学者们对static所修饰的静态在内存中的具体体现有一个深刻的理解。 ~~~ class Person { String name;//姓名,这是一个成员变量 int age; static String country = "美国";//国籍,这是一个静态变量 Person(String name,int age)//构造函数 { this.name = name; this.age = age; } public void printInfo()//非静态函数 { System.out.println(name+":"+age); } public static void printCoun()//静态函数,打印静态变量country { System.out.println(Person.country); } } class StaticTest { public static void main(String[] args) { Person.printCoun(); Person p = new Person("科比",37); p.printInfo(); } } ~~~ 我们先来看运行结果,然后再一步步分析: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a4c4ec.jpg) 例子很简单,结果也很明显,我们之前在7.9中谈成员变量与静态变量的区别时提到了一个区别就是成员变量是存储在堆内存中的对象中,而静态变量则存储在方法区中的静态区中。 这里,我们就引入了内存的一个新区域,那就是方法区,对于方法,当程序运行时,都会被存储在这个区域。 那么我们就对上面的代码运行过程和内存变化进行分析,当然在我们分析之前,我们必须明确一个常识,那就是**当我们执行类时,类就会进入内存。** ** ** 那么对于上面的代码,分析过程就会很清晰了: 1.当我们运行程序时,StaticTest类进入内存,虚拟机会在方法区的非静态区中分配空间存储StaticTest(){}默认构造函数,同时在方法区的静态中分配空间存储static main(){……}主函数,当然包括主函数的所有代码的字节码。 2.静态区的main函数进栈内存,main方法中有一个对象变量p。 3.执行Person.printCoun(){}方法,Person类进入内存,方法区的非表态区分配空间存放构造函数Person(name,age){……}和非静态函数void printInfo(){……},在方法区的静态区中分配空间存储静态变量country="美国"和静态方法printCoun(){……}。 4.静态区的printCoun()方法进栈内存,并从静态区找到静态变量country并打印,控制台输出:“美国”。 5.printCoun()方法执行结束,跳出方法,printCoun()方法出栈内存。 6.执行Person p = new Person("科比",37),此时堆内存中创建空间存储对象,这里假设地址为0x0056,则所属this=0x0056,并有成员变量name和age。 7.非静态区的构造函数Person(name,age)进栈内存,对对象进行初始化,为堆内存中的对象进行初始化,name=科比,age=37。 8.初始化完成,把地址0x0056赋值给对象p,p=0x0056。 9.构造函数出栈内存,释放参数name和age。 10.执行p.printInfo()语句,非表态区的printInfo()方法进栈内存,this=0x0056。 11.打印this所指向的成员变量this.name和this.age,控制台输出:科比:37。 12.printInfo()方法执行结束,跳出方法,方法出栈内存。 13.main()函数执行结束,跳出,函数出栈内存。 14.程序运行结束。 上面我们对例子中的代码进行了逐步分析,基本上明晰了static关键字所修饰的静态在程序运行时在内存中的具体变化,希望在以后的实际开发过程中有所帮助。 最后我们再说一个小知识点:**存储在方法区中的变量和方法都会对象所共享,所以方法区又称为共享区。** ** **
';

8.5 static关键字的使用场景

最后更新于:2022-04-01 14:11:35

这一节我们来看一下在我们开发的过程中,在什么时候我们要用到static关键字进行静态修饰。 我们这里所说的静态,无非就是两种,一种是静态变量,一种是静态函数,我们分这两种情况进行说明static关键字的使用场合。 **一、静态变量** 我们在7.8节中对static关键字特点和7.9节中成员变量与静态变量区别进行学习的时候就已经很明确的指出了静态变量是共享数据,而对象中封装的特有数据。 因此我们可以这样解释:当分析对象中所具备成员变量的值是相同的,且不需要对象做修改,这时这个成员变量就可以被static关键字修饰为静态。 从另一个角度也就是说,只要数据在对象中都是不同的,也就是对象的特有数据,必须存在在对象中,则就是非静态的,不能被static关键字修饰为静态。 因此用static关键字定义静态变量的使用场合:**如果是相同数据,且对象不需要做修改,只需要使用,且不需要存储在对象中,则使用static关键字定义成静态。** **二、静态函数** 我们在7.10static关键字使用细节时说到了静态方法中不能调用非静态成员,且不能使用this关键字,所以对于函数是否使用static关键字修饰,只需要参考一点,就是该函数功能是否有访问到对象中的特有数据。 为了帮助我们理解,我们可以通俗的说,从源码看,该功能是否需要访问非静态成员变量,如果需要,该功能就是非静态的,如果不需要,我们就可以将该功能用static关键字定义成静态函数。 当然,我们也可以定义成非静态,但是非静态需要对象去调用,而当创建了对象却没有访问特有数据的方法,该对象创建是没有意义的。因为我们之前说过对象是用于封装特有数据的,当我们没有访问特有数据,那么我们为什么要创建对象呢?创建的对象只是浪费了堆内存中的空间,再什么也没有做,所以说此时创建对象是没有意义的。 因此用static关键字定义静态函数的使用场合:**如果一个函数不需要访问非静态成员变量,就可以用static关键字定义成静态。** ** ** 这一节我们就简单的说这些吧。
';

8.4 main函数的解析与细节

最后更新于:2022-04-01 14:11:32

上一节我们说了静态使用细节时提到了main函数是静态的,其实,对于我们初学者,真是对main函数是什么样认识还很模糊,那么这一节我们就解析一下main函数. main函数的固定格式:**public static void main(String[] args)** java语言中主函数有两个特殊之处: **1.格式是固定的.** **2.被jvm所识别和调用.** ** ** 那么它们第一个单词都有什么意义呢,我们下面就学习一下: public:因为权限必须是最大的. static:虚拟机对运行main函数是不需要对象的,直接用主函数所属类名调用即可. void:主函数没有具体的返回值.虚拟机调用主函数. main:函数名,不是关键字,只是一个jvm识别的固定名字. String[] args:这是主函数的参数列表,是一个数组类型的参数,而且元素都是字条串类型. 那么我们关键点来了,我们知道,任何一个函数被调用,如果它有参数,那么就要传递相对应的实参. 那么对于主函数,传递的实参是什么?谁来调用主函数,虚拟机调用,只要有参数,我们就必须传递对应实参. 那么到底传的实参是什么呢? 我们看下面的例子: ~~~ class MainTest { public static void main(String[] args) //其实虚拟机传的参数是 new String[0] { System.out.println(args); } } ~~~ 我们看一下运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41edaf7e5.jpg) 我们运行并看到了  [Ljava.lang.String;@139a55,说明确实有个字符串数组的实体,那么这个字符串数组有长度是多少呢?我们再来测试: ~~~ class MainTest { public static void main(String[] args) //其实虚拟机传的参数是 new String[0] { System.out.println(args); System.out.println(args.length); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41edc30e5.jpg) 我们看实体的长度,结果为0. 所以主函数实际传递的参数是创建了一个String类型的数组对象实体,即 new String[0] 这就是java语言中main函数为我们传递参数提供了可能,我们可以把我们想要传递的参数传递进去.为我们留下了余地. 那么我们又会问,为什么参数是字符串类型呢? 我们知道,字符串类型可以与其他数据类型进行转换,所以,无论我们传什么,传进去的都是字符串.那为什么是字符串呢,这是因为字符串具有通用性,我们只需要根据实际进行字符串转换. 那么什么时候传值呢?当然是虚拟机调用主函数时传值.我们接着看下面例子: ~~~ class MainTest { public static void main(String[] args) //其实虚拟机传的参数是 new String[0] { System.out.println(args); System.out.println(args.length); System.out.println(args[0]); } } ~~~ 这个例子中是打印出实参数组中的第一个元素. 我们可以这样传值:java MainTest haha xixi hehe ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41edd7a9c.jpg) 我们看到了打印出字符中数组参数的长度为3,第一个元素内容是haha,既然我们能够把我们想要传入的参数传入main函数,那么我们把所有的参数打印出来: ~~~ class MainTest { public static void main(String[] args) //其实虚拟机传的参数是 new String[0] { System.out.println(args); System.out.println(args.length); for(int x=0;x<args.length;x++) System.out.println(args[x]); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41edef338.jpg) 这样就很清晰了,我们可以在jvm调用main函数的时候传入我们想要运行的参数并进行处理. 我们再来看一个小细节,那就形式参数args,其实我们args就是main函数的形式参数变量名,那么它可不可以用其它的名字呢?我们看一例子: ~~~ class MainTest { public static void main(String[] x) //其实虚拟机传的参数是 new String[0] { System.out.println("运行正常!"); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a0b724.jpg) 我们可以看到,对于这个参数名是可以换成其他名字,这也是主函数中唯一一个可以变化的地方.并且,其实在早期时,这个名字其实是arguments,意思是参数,由于是字符串的数组,所以是复数. 同时我们再来看一个小细节: ~~~ class MainTest { public static void main(String[] x) //其实虚拟机传的参数是 new String[0] { System.out.println("运行正常!"); } public static void main(int[] x) {} } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a0b724.jpg) 我们会发现程序正常运行,这更加说了main就是一个函数名,并且两个main方法之间是重载关系,而jvm只会找格式固定的main函数. 所以当我们这样做时: ~~~ class MainTest { public static void main(String[] args) //其实虚拟机传的参数是 new String[0] { System.out.println("运行正常!"); } public static void main(String[] x) {} } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c422a28789.jpg) 这时编译直接抛出异常,其实这两个函数,只是参数名不相同,实际是同一函数. 我们在以后的编程过程中多注意这两个小细节。
';

8.3 static关键字使用的注意细节

最后更新于:2022-04-01 14:11:30

这一节我们看静态在使用过程中应注意的几个细节。 上一节我们在学习成员变量与静态变量的区别时,对于两种变量的调用方式不同时出现了“无法从静态上下文中访问非静态变量”的错误,这个问题我们在以后会经常遇到。那么这是什么问题呢?我们从例子说起,我们把上一节中的Person类中方法printInfo进行静态修饰: ~~~ class Person { String name;//姓名,这是一个成员变量 static String country = "中国";//国籍,这是一个静态变量 public static void printInfo() { System.out.println(name+":"+country); } } class StaticTest { public static void main(String[] args) { Person p = new Person(); p.name = "小强"; p.printInfo(); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed38250.jpg) 我们看到了一个和上一节完全相同的错误,而当我们把方法进行修改: ~~~ public static void printInfo() { System.out.println(":"+country); } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed4d5c7.jpg) 我们看到当把printInfo方法中的非静态变量name去掉之后,程序正常运行,因此我们可以总结出static关键字使用中的一个常见问题,即: **第一个细节:静态方法中不能调用非静态成员,只能调用静态成员.**这是由于我们知道静态成员是随着类的加载而存在,是先于对象存在的,而非静态的成员变量是随着对象的创建而存在,所以当我们在静态方法中调用非静态成员时,还没有创建对象,当然就不存在非静态变量.比如我们上面的例子,静态方法printInfo和静态变量country都会随着Person类的加载而加载,而成员变量name只有随着Person对象的创建才会存在,当我们在静态方法中直接调用name时,此时根本就不存在,当然会出错. 当然对于这个细节,也可以说是静态方法的一个局限性.非静态方法既可以访问静态成员,也可以访问非静态成员. 我们再看下一个比较常见的问题. **第二个细节:静态方法中不可以使用this或者super关键字.**原因当然可以用静态的特点解释,super我们还没有学习,这里暂不说明,就拿this关键字来说,我们说this代表对象,那么当我们在静态方法中使用this时,this代表的就是当前类的实例,而静态方法和类是一起存在的,而该类的对象并不存在,所以会出错.我们看一个例子: ~~~ class Person { String name; static String country = "中国"; public static void printInfo() { System.out.println(this.name+":"+country); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed5e64e.jpg) 我们很清楚的就看到这是不合法的. **第三个细节:主函数是静态的.**这个我们大家肯定都不陌生,我们从开始学习就在搞main函数,它的正确格式是: ~~~ public static void main(String[] args) ~~~ 那么我们再来看一个例子: ~~~ class StaticTest { int num = 4; public static void main(String[] args) { printNum(); } public void printNum() { System.out.println(num); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed731ce.jpg) 我们会发现我们没有用好第一个细节,所以我们进一步修改: ~~~ class StaticTest { int num = 4; public static void main(String[] args) { printNum(); } public static void printNum() { System.out.println(num); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed85af2.jpg) 哦,还有一个非静态成员变量,再一次修改: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed9a5e2.jpg) 这个过程真麻烦哦,那么我们是不是要在主函数中调用的成员都是静态的呢?如果真是这样,那么要对象干什么呢?我们知道主函数是程序的入口,对于面向对象语言,那么主函数也就对象访问的入口,是指挥对象工作的,如果没有对象不知道有多么可怕啊. 我们之前学习了两种调用方式,一种是通过类名调用,而另一种方式就是通过对象调用,那么我们为了规避静态的局限性,我们把上面的方法进行非静态化,并且通过对象去调用: ~~~ class StaticTest { int num = 4; public static void main(String[] args) { new StaticTest().printNum(); } public void printNum() { System.out.println(num); } } ~~~ 并且程序正常运行了. **第四个细节:在本类中,非静态成员前面省略的是this,而在静态成员前面省略的却是类名.**比如上面的代码,实际上在Person类中,成员变量name前面省略的this,即this.name,而在静态变量country前面省略的类名Person,即Person.country,这一点我们在以后的使用多注意就可以了.
';

8.2 成员变量与静态变量的区别

最后更新于:2022-04-01 14:11:28

这一节我们看一看成员变量与静态变量的区别所在。 什么是静态变量呢?我们上节用static关键字时就提到了静态变量。也就是说用static关键字修饰的变量就是静态变量。 我们在6.4节学习了成员变量与局部变量的区别,这一节我们重点看看成员变量与静态变量之间的区别。 我们看代码: ~~~ class Person { String name;//姓名,这是一个成员变量 static String country = "中国";//国籍,这是一个静态变量 public void printInfo() { System.out.println(name+":"+country); } } ~~~ 上面的代码中我们可以看到变量name是一个成员变量,而country是一个静态变量,用static关键字修饰。 这里我们通过上一节static关键字的特点和成员变量的特点我们总结四个方面的区别: **1.两个变量的生命周期不同:** 成员变量:随着对象的创建而存在,随着对象的被回收而释放。 静态变量:随着类的加载而存在,随着类的消失而消失。 那么类什么时候消失呢?一般情况下,虚拟机结束了,类就结束了,当然这只是说一般情况下。 **2.两个变量的调用方式不同:** 我们来看一个上面代码的测试: ~~~ class StaticTest { public static void main(String[] args) { Person p = new Person(); p.name = "小强"; String name = Person.name; System.out.println(p.country+"--"+Person.country); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed0e6e3.jpg) 我们可以看到直接用类名来调用成员变量就会出现“无法从静态上下文中访问非静态变量”的错误。 当我们把这名话注释之后再来运行: ~~~ class StaticTest { public static void main(String[] args) { Person p = new Person(); p.name = "小强"; //String name = Person.name; System.out.println(p.country+"--"+Person.country); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ed20159.jpg) 程序正常运行并合法访问,所以我们可以总结出下面的区别: 成员变量:只能被对象调用。 静态变量:既可被对象调用,也可被类调用。上一节我们就已经学习了static修饰的成员可以直接用类名调用。 **3.两个变量的别名不同:** 成员变量:又称为实例变量。 静态变量:又称为类变量。 **4.两个变量的存储位置不同:** 成员变量:存储在堆内存中的对象中,所以也叫对象的特有数据。 静态变量:存储在方法区(共享数据区)的静态区中,所以也叫类的共享数据。
';

8.1 static关键字之特点

最后更新于:2022-04-01 14:11:26

这一节我们来学习java语言中一个新的关键字就是static关键字。 当然对于每一个关键字,我们都要明确的是它有什么作用,怎么用。我们先来看一个例子: ~~~ class Person { String name;//姓名 String country = "中国";//国籍 public void printInfo() { System.out.println(name+":"+country); } } class StaticTest { public static void main(String[] args) { Person p = new Person(); p.name = "小强"; p.printInfo(); Person p1 = new Person(); p1.name = "小明"; p1.printInfo(); Person p2 = new Person(); p2.name = "小红"; p2.printInfo(); } } ~~~ 我们看看这个例子的运行结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ecdb674.jpg) 我们会发现创建的三个对象中,姓名不同,但是国籍是相同的,那么对于很多个对象呢,国籍是相同的,那么我们就会想能不能把国籍独立起来放到一个空间中供所有对象直接引用就可以了,而在对象中只需要有姓名这个属性即可。 java语言给我了我们解决这个问题的途径,那就是我们现在要说的static关键字。 很明显,我们也就明确了static关键字的作用:就是把特有数据谜面所有对象都能共享的数据,也称为对象的共享数据。 那么上面的代码用static关键字进行优化: ~~~ class Person { String name;//姓名 static String country = "中国";//国籍 public void printInfo() { System.out.println(name+":"+country); } } ~~~ 当然结果还是我们上面看到的结果,但是实际上,内存中会把country专门存放在一个独立的空间中,而每个对象中都只有name属性,这样就节省了很多不必须的空间。 我们刚才看了static关键字的作用,那么下面我们再来看static关键字有哪些特点呢? 我们通过上面的例子,引申总结出以下四条特点: **1.static既是关键字,也是修饰符,用于修饰成员。**这里的成员就是成员变量和成员函数。 **2.static关键字修饰的数据是共享数据。**我们前面看到了static的作用就是修饰共享数据,而对象中存储的数据就是每个对象的特有数据。 **3.static关键字修饰的成员随着类的加载而加载。**这是由于static修饰的数据要被所有对象共享,所以要在对象共享之前就存在,因此java语言就赋予了static这样的功能。 **4.static关键字修饰的成员优先于对象存在,**因为static成员是随着类的加载面存在的。 **5.static关键字修饰的成员多了一种调用方法,可以直接被类名调用,即:类名.静态成员 的调用方式。** 比如上面的例子中,我们可以这样调用: ~~~ class StaticTest { public static void main(String[] args) { Person p = new Person(); //p.name = "小强"; //p.printInfo(); System.out.println(Person.country); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41eceed4b.jpg) 我们看到staitc修饰的成员我们可以直接用类名去调用,也就是说我们不需要创建对象就可以直接调用,因为static成员随着类的加载而存在,实现了相同数据被不多个对象的共享。 当然,既然我们的java是面向对象语言,如果我们把所有的成员都用static修饰,也就是说没有对象了,那是一件多么可怕的事情呢。但是,不管怎么说,共享数据个人认为确实还是比较爽的。
';

7.7 this关键字的细节与应用

最后更新于:2022-04-01 14:11:23

这一节我们接着上一节来继续学习this关键字. 我们之前在7.5节中的构造函数应注意的细节中提到过一个细节就是构造函数可以调用一般函数,但一般函数不能直接调用构造函数.但是我们没有深究构造函数能不能调用构造函数,那么现在我们就来看一看这个问题. 那么构造函数与构造函数之间是怎么访问呢? 为了掌握这个问题,我们必须要明确一个概念,那就是在一个类中的成员如果要被执行,就必须由对象调用.而每个对象都有一个所属的this. java语言给了关键字this这个功能,那就是用this调用构造函数,而且也是通过参数不同选择调用不同的构造函数. 我们来看一个例子: ~~~ class Person { private String name; private int age; Person()//构造方法1 { System.out.println("person run"); } Person(String name)//构造方法2 { this.name = name; } Person(String name,int age)//构造方法3 { this.name = name; this.age = age; } public void speak() { System.out.println(this.name+":"+this.age); } } ~~~ ~~~ class ThisTest { public static void main(String[] args) { Person kobe = new Person("科比",37); kobe.speak(); } } ~~~ 对于上面的例子我们可以看到,构造函数3中的第一句语句其实我们已经通过构造函数2实现了,那么为了提高代码的复用性,我们为什么不调用构造函数2呢,java语言中用下面的语句来调用构造函数: ~~~ Person(String name,int age)//构造方法3 { this(name);//用this关键字调用了构造函数Person(String name) this.age = age; } ~~~ 我们看到了this关键字可以用于在构造函数中调用其他构造函数,当然对于调用那个构造函数,还是通过参数来确定. 那么我们就明确了构造函数与构造函数之间的调用形式. 下面我们来看两个this关键字用法中需要注意的两个细节: **第一个细节:构造函数中调用构造函数只能定义在构造函数的第一行.** 这是为什么呢,因为初始化动作一定要先执行.这就是java语言定义的一个规则,如果不是定义在第一行,编译直接通不过. 我们看例子,把上面的构造函数3的语句交换位置: ~~~ Person(String name,int age)//构造方法3 { this.age = age; this(name);//用this关键字调用了构造函数Person(String name) } ~~~ 我们看编译情况: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ec9f032.jpg) 我们看到编译错误:对this的调用必须是构造器中的第一个语句,这就是我们在使用this关键字时的第一个细节. **第二个细节:注意构造函数的调用出现递归循环而导致栈内在溢出.** 我们看个例子: ~~~ Person()//构造方法1 { this("KOBE"); System.out.println("person run"); } Person(String name)//构造方法2 { this(); this.name = name; } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ecb1da1.jpg) 我们很容易发现两个构造函数相互调用,形成了递归,使得两个构造函数不断进栈,最后栈内存溢出,程序终止. 上面我们看了两个在使用this关键字时需要注意的细节,下面我们再简单的看看this关键的应用情况. 我们一般什么时候使用this呢,我们上一节中说过this的概念:this就代表对象,就是所在函数所在对象的引用.那么我们不难理解,当我们在一个类中用到了本类的对象,我们就通常会用this来引用. 那么我们来实现一个功能:判断两个人是否是同龄人. 分析一下:要判断两个人是否同龄,我们只需要比较这两个人(也就是两个Person对象)的年龄是否相等即可,那么也就是说一个对象可以直接调用Person类中的方法来与另一个对象做比较就可以了. 我们可以这样实现: ~~~ //判断两个人是否同龄 public boolean compare(Person p) { return this.age == p.age; } ~~~ 我们来测试一下: ~~~ class ThisTest { public static void main(String[] args) { Person kobe = new Person("科比",37); Person james = new Person("詹姆斯",31); kobe.speak(); james.speak(); System.out.println("这两个人相等吗:"+kobe.compare(james)); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ecc70ad.jpg) 我们看到this的主要应用就是代表对象,那个对象调用了this所有的函数,我们就通俗的认为this就代表那个对象.
';

7.6 this关键字的原理

最后更新于:2022-04-01 14:11:21

这一节我们来讲一个关键字,就是this关键字。 我们还是通过例子来看吧: ~~~ class Person { private String name; private int age; Person(String n,int a) { name = n; age = a; } public void speak() { System.out.println(name+":"+age); } } ~~~ ~~~ class ThisTest { public static void main(String[] args) { Person kobe = new Person("KOBE",37); kobe.speak(); } } ~~~ 这个例子我们应该很熟悉了,前面几节都在用这个例子,我们再来看一看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41b0680db.jpg) 很显然,构造函数对对象kobe进行了初始化。 但是我们发现,虽然结果是我们想要的,但是,我们单独看这个构造函数,从可读性的角度我们分析,我们根本就不知道函数传过来的是什么内容,我们可以说一无所知,可阅读性太差了,那么我们再对这个构造函数进行改造: ~~~ class Person { private String name; private int age; Person(String name,int age) { name = name; age = age; } public void speak() { System.out.println(name+":"+age); } } ~~~ 唉,这样不是很清晰了吗,我们一眼就看出这个构造函数要告诉我们初始化对象的姓名和年龄了,我们看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ec76dd2.jpg) 嗯???这是什嘛情况,KOBE人呢? 我们7.4节谈过了构造函数的内存加载过程,但是我们没有提到这里遇到的这种情况,这种情况我们可以说是:成员变量和局部变量重名的问题,此时对于栈内存和堆内存中都会变量name和age,而调用的构造函数会自动到栈内存中寻找这两个变量,而此时,恰好都有,所以系统就会做一个非常有趣的事情,就是把栈内存中的name原赋给本身,而对象的name和age的值其实在堆内存中,所以结果就是我们刚才看到的结果喽。 那么对于这个问题,我们该怎么解决呢? java给我们解决方案,那就是用一个关键字this来区分成员变量和局部变量。我们再来改造: ~~~ class Person { private String name; private int age; Person(String name,int age) { this.name = name; this.age = age; } public void speak() { System.out.println(name+":"+age); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41ec8a72c.jpg) 很好,KOBE同志又回来了。 所以我们可以说,当成员变量与局部变量重名时,我们可以用this来区分。 那么我们就想明白,this到底代表什么呢?java语言说,this代表的是对象。我们还想明白,this代表的是哪个对象?java语言又说,代表的就是当前对象。 专业点的术语是这样定义this的:this就是所在函数所在对象的引用。说简单点就是:this代表本类对象的引用。 我们自己用通俗点的语言来定义:就是哪个对象调用了this所在的哪个函数,this就代表哪个对象,也就是说this就是这个对象的引用。 比如上面的例子中的kobe调用了构造函数Person(String name,int age),那么我们可说this就可以代表kobe这个对象。 那么我们再来对this在内存中的体现过程分析一下,我们继续7.4的过程,只有小的变动。 1.main方法进栈内存,main方法中有一个Person类类型变量kobe; 2.new创建Person对象,在堆内存中创建空间(假如地址为0x0045),该空间中有两个成员变量name和age; 3.对对象的两个成员变量进行初始化,此时会自动选择调用构造函数Person(String n,int a); 4.构造函数Person(String name,int age)进栈内存,参数name="KOBE",age=0也加载入栈。而此时系统会自动为该栈内存中加载一个对象的引用,也就是this,并且把kobe的堆内存地址赋给this; 5.然后在把this.name和this.age的初始化为栈内存中name和age,这样就很清晰了,this.name和this.age我们可以理解为就this所指堆内存中对象的成员变量,此时对象的初始化完成; 6.把地址0x0045赋给main方法中的实例变量kobe; 7.构造函数Person(String name,int age)出栈,释放参数name和age和this引用; 8.执行kobe.speak()语句,调用Person类中的speak()方法,则speak方法进栈,此时系统也会为speak方法加载一个this引用,指向堆内存中的对象地址(0x0045); 9.执行打印语句,跳出speak方法,speak方法出栈,释放this引用; 10.跳出main方法,main方法出栈,程序运行结束。 通过上面的过程分析,我们可以简单的总结出这样一个结论:当在函数中需要用到调用函数的对象时,就用this关键字。 为了更好的理解这个结论,我们把上面的例子可以标准的写成下面这样: ~~~ class Person { private String name; private int age; Person(String name,int age) { this.name = name; this.age = age; } public void speak() { System.out.println(this.name+":"+this.age); } } ~~~ 我们对这个类方法中的所有成员变量都标准的写成了this.成员变量的格式。 那么我们刚开始看的第一个例子为什么没有this,而结果是也是正确的呢? 很显然,当然对于成员变量与局部变量不重名时,this是可以省略的,但请注意,不是没有,而是省略。
';

7.5 构造函数需要注意的几个细节

最后更新于:2022-04-01 14:11:19

这一节我们简单的说几个在使用构造函数时需要注意的细节。 通过我们前几节的学习,我们对构造函数有了一个比较清楚的认识,当我们在创建对象时,我们会调用构造函数。那么我们在定义和调用构造函数时,需要注意哪些细节呢? ** ** **第一个需要注意的细节:构造函数与set方法** 看两个函数代码: ~~~ class Person { private String name; private int age; //构造函数,初始化name Person(String n) { name = n; System.out.println(name+"age"+age); } //一般函数,设置name public void setName(String n) { name = n; } } ~~~ 我们看到上面有两个函数,第一个是构造函数,第二个是一般方法,它们两个都是为设置name的内容,那么我们能不能用构造函数代替set方法呢,很显然,这是不行了,因为我们之前已经学习了构造函数和一般函数的区别,而set方法就是一个一般函数。构造函数在这里只是对name进行了一次初始化,之后就不再作用了,而set方法当我们需要更改名字的任何时候都可以调用。 **第二个需要注意的细节:构造函数可以调用一般函数,但是一般函数不可以直接调用构造函数。** 我们来看例子: ~~~ class Person { private String name; private int age; //构造函数,初始化name Person(String n) { setName(n); System.out.println(name+":"+age); } //一般函数,设置name public void setName(String n) { name = n; } } ~~~ 编译通过,我们来测试一下 ~~~ class PersonTest { public static void main(String[] args) { Person p = new Person("小强"); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41b01664b.jpg) 很显然构造函数中调用一般函数是可以的。 当我们在set方法中调用构造函数: ~~~ class Person { private String name; private int age; //构造函数,初始化name Person(String n) { name = n; System.out.println(name+":"+age); } //一般函数,设置name public void setName(String n) { Person(n); name = n; } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41b02ac7a.jpg) 我们看到编译直接通不过,所以说一般函数中是不能直接调用构造函数的。 **第三个需要注意的细节:与类名同名的一般方法** 我们看这样一个函数: ~~~ class Person { private String name; private int age; void Person(String n) { name = n; System.out.println(name+":"+age); } } ~~~ 测试: ~~~ class PersonTest { public static void main(String[] args) { Person p = new Person("小强"); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41b040591.jpg) 我们看到提示说无法将构造函数应用到给定类型,实际没有参数,但我们的代码中有参数“小强”,也就是说我们创建对象用的函数不是构造函数,而是一个一般函数,程序没有在Person类中找到相应的构造函数,因为我们的代码中没有定义构造函数,那么就只有默认构造函数,而默认构造函数是没有任何参数的。所以我们在以后的编程过程中一定要注意这种现象。 **第四个需要注意的细节:构造函数中有return语句。** 我们看代码说明问题 ~~~ class Person { private String name; private int age; Person(String n,int a) { if(a<0) { System.out.println("初始化不合法!"); return; } name = n; age = a System.out.println(name+":"+age); return ;//我们在函数那里学习了,没有返回值的函数中的return语句是可以省略的 } } ~~~ 我们来测试一下 ~~~ class PersonTest { public static void main(String[] args) { Person p = new Person("小强",-1); } } ~~~ 结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41b05389e.jpg) 我们发现了程序编译通过并且运行正常,return语句跳出构造函数,那么也就是说构造函数中有return语句。 这一节我们学习了四个构造函数中经常需要注意的细节,在以后的编程过程中我们多多注意。
';

7.4 构造函数的内存加载

最后更新于:2022-04-01 14:11:17

这一节我们来说说构造函数在内存中是如何被加载的。 我们之前说过,构造函数只有创建对象时才会被调用并且只调用一次。那么在创建对象的过程中内存中的变化是什么样的呢? 我们接着上一节的Person类,我们分析 ~~~ class Person { private String name; private int age; Person()//构造函数,分别为成员变量name和age赋固定的值 { name = "baby"; age = 1; System.out.println("person run"); } Person(String n)//构造函数,有一个初始name参数 { name = n; } Person(String n,int a) { name = n; age = a; } public void speak() { System.out.println(name+":"+age); } } ~~~ 我们用下面的两行代码来分析一下构造函数在内存中的加载过程 ~~~ class ConsDemo { public static void main(String[] args) { Person p = new Person("小强",10); p.speak(); } } ~~~ 对于上面的测试,我们分析它的运行过程: 1.main方法进栈内存,main方法中有一个Person类类型变量p; 2.new创建Person对象,在堆内存中创建空间(假如地址为0x0045),该空间中有两个成员变量name和age; 3.对对象的两个成员变量进行初始化,此时会自动选择调用构造函数Person(String n,int a); 4.构造函数Person(String n,int a)进栈内存,并且有参数n="小强",a=0; 5.然后在堆内存中把参数n和a的数值初始化name和age变量,此时对象的初始化完成; 6.把地址0x0045赋给main方法中的变量p; 7.构造函数Person()出栈,释放参数n和a; 8.执行p.speak()语句,调用Person类中的speak()方法,则speak方法进栈; 9.执行打印语句,跳出speak方法,speak方法出栈; 10.跳出main方法,main方法出栈,程序运行结束。 我们在上面通过对一个简单的对象创建过程进行了分析,简单的学习了构造函数在内存中的加载和运行过程,这里也就是突出了对象的初始化,如果类中没有定义构造函数,那么我们在创建对象时会调用默认的构造函数,而当我们定义了构造函数,则会通过参数类型选择不同的构造函数进行对象的初始化,而且我们知道对象都必须被初始化,初始化就会调用相应的构造函数,所以说,构造函数是必须会进栈内存的。
';

7.3 构造函数的重载

最后更新于:2022-04-01 14:11:14

这一节我们来学习一下构造函数的重载。 说到重载,我们在函数那一章已经学习过了,为了更好的理解构造函数的重载,我们先要弄明白一个问题: 那就是什么时候定义构造函数呢? 在描述事物时,该事物已存在就具备一些内容,这些内容都定义在构造函数中。 我们来看例子: ~~~ class Person { private String name; private int age; Person() { System.out.println("person run"); } public void speak() { System.out.println(name+":"+age); } } ~~~ 我们来测试一下: ~~~ class ConsDemo { public static void main(String[] args) { Person p = new Person();//此时调用了构造函数 p.speak(); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41afb95ea.jpg) 此时打印结果为null和0,就是说我们从来没有对成员变量赋过值。 那么当我们想要在创建Person对象时成员变量就有值,可以定义这样的构造函数: ~~~ class Person { private String name; private int age; Person()//构造函数,分别为成员变量name和age赋值 { name = "baby"; age = 1; System.out.println("person run"); } public void speak() { System.out.println(name+":"+age); } } ~~~ 测试 ~~~ class ConsDemo { public static void main(String[] args) { Person p = new Person(); p.speak(); } } ~~~ 我们看到结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41afcd8c2.jpg) 那么如果我们要使一个Person对象一出生就有名字,那么我们可以定义如下一个构造函数: ~~~ class Person { private String name; private int age; Person()//构造函数,分别为成员变量name和age赋固定的值 { name = "baby"; age = 1; System.out.println("person run"); } Person(String n)//构造函数,有一个初始name参数 { name = n; } public void speak() { System.out.println(name+":"+age); } } ~~~ 我们来测试一下: ~~~ class ConsDemo { public static void main(String[] args) { Person p = new Person(); p.speak(); Person p1 = new Person("旺财"); p1.speak(); } } ~~~ 看一下结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41afe10d9.jpg) 同理,我们可以定义另外一个构造函数,把name和age两个参数同时初始化 ~~~ class Person { private String name; private int age; Person()//构造函数,分别为成员变量name和age赋固定的值 { name = "baby"; age = 1; System.out.println("person run"); } Person(String n)//构造函数,有一个初始name参数 { name = n; } Person(String n,int a) { name = n; age = a; } public void speak() { System.out.println(name+":"+age); } } ~~~ 测试: ~~~ class ConsDemo { public static void main(String[] args) { Person p = new Person(); p.speak(); Person p1 = new Person("旺财"); p1.speak(); Person p2 = new Person("小强",10); p2.speak(); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41aff3aab.jpg) 从上面我们看到,一个类中我们定义了多个构造函数,它们的参数各不相同,这种现象就是构造函数的重载。 通过构造函数的重载,我们可以通过不同的构造函数初始化不同的对象。
';

7.2 构造函数与一般函数的区别

最后更新于:2022-04-01 14:11:12

这一节我们简单学习一下构造函数与一般函数之间的区别所在。 那么它们有什么区别呢,结合上一节,我们可以总结出以下两点区别: 第一个区别: 构造函数:对象创建时,就会调用与之对应的构造函数,对对象进行初始化。 一般函数:对象创建后,需要函数功能时,才调用函数。 这也就是说创建对象时,构造函数一定被调用,而一般函数不一定被调用。 第二个区别: 构造函数:对象创建时,会调用且只调用一次。 一般函数:对象创建后,可以被调用多次。 简单看看示例: ~~~ class Person { private String name; private int age; Person() { System.out.println("person run"); } public void speak() { System.out.println(name+":"+age); } } ~~~ ~~~ class ConsDemo { public static void main(String[] args) { Person p = new Person();//此时调用了构造函数 p.speak();//调用两次speak方法 p.speak(); } } ~~~ 我们可以看到当创建一个对象p是,调用了Person类中的构造函数,而且是只调用了一次。而对于一般方法speak方法,是我们在创建了对象之后,才调用,并且我们可以调用任意次娄。
';

7.1 构造函数概述与默认构造函数

最后更新于:2022-04-01 14:11:10

这一节我们来学习一个特殊的函数,那就是构造函数。 那么什么是构造函数呢? 既然是函数,那它应该是被定义在类中,同时有函数名,要明确函数返回的结果和参数类型。 我们这里先看一下**构造函数的特点:** 1.函数名与类名相同; 2.不用定义返回值类型 3.没有具体的返回值。 从这些特点来说,这确实是一个很特殊的函数哦。 我们来看一个定义构造函数的例子: ~~~ class Person { private String name; private int age; //定义一个Person类的构造函数 Person()//构造函数,而且是空参数的。 { System.out.println("person run"); } public void speak() { System.out.println(name+":"+age); } } //测试类 class ConsDemo { public static void main(String[] args) { Person p = new Person(); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41af9e905.jpg) 结果我们看到了,当我们创建对象是,执行了我们定义的构造方法。 那么我们可以这样通俗的定义构造函数: **构造函数:构建创造对象时调用的函数。这样是不是很好理解了。** 那么我们就可以清楚的知道构造函数的主要作用: **构造函数的作用:可以给对象进行初始化。** 我们必须清楚,创建的每一个对象都必须要通过构造函数初始化。 那么当我们一个类中没有构造函数时,为什么我们还是可以创建对象呢?那是因为有默认构造函数。 什么是默认构造函数呢? **当一个类中如果没有定义过构造函数,那么该类中会有一个默认的空参数构造函数。** ~~~ class Person(){}//构造函数,而且是空参数的,这其实就是类中的默认构造函数 ~~~ 但我们更要注意的是:**如果在类中定义了指定的构造函数,那么类中的默认构造函数就没有了。** 比如说我们定义一个类: ~~~ class Demo{}//一个什么代码都没有的类 ~~~ 我们说这个类中有没有内容? 答案当然是肯定的,虽然这个类是空的,但是我们可以创建它的对象,也就是说这个类中有一个默认的构造函数: ~~~ class Demo { Demo(){} } ~~~ 很显然,这个类中确实有内容,就是我们说的默认构造函数。
';

6.7 封装

最后更新于:2022-04-01 14:11:08

这一节我们学习面向对象中的第一个特性,封装(encapsulation) 封装:是指隐藏对象的发生和实现细节,仅对外提供公共访问方式. 那么什么是隐藏对象的实现细节?我们来举例学习. 比如我们来定义一个人的类,简单点说,人的属性有年龄,行为有说话. 我们看下面的代码: ~~~ class Person//定义一个人的类 { int age; void speak() { System.out.println("age = "+age); } } class PersonDemo//定义测试类 { public static void main(String[] args) { Person p = new Person(); p.age = -20;//这时代码仍然正常运行了,这就造成了安全隐患 p.speak(); } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41af62be0.jpg) 我们看到存在安全隐患,当我们为人的年龄属性赋值为负数时,程序是正常运行的. 那么现在把Person类修改一下 ~~~ <pre name="code" class="java">class Person { private int age; void speak() { System.out.println("age = "+age); } } ~~~ ~~~ ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41af73230.jpg) 我们看到报错了,因为我们对age进行了私有,在PersonDemo类中无法访问. 为了进一步解决问题,我们再对两个类进行修改: ~~~ class Person { private int age;//把该成员属性设置为私有 public void setAge(int a) { if(a>0 && a<130) { age = a; speak(); } else System.out.println("错误的数据"); } void speak() { System.out.println("age = "+age); } } class PersonDemo { public static void main(String[] args) { Person p = new Person(); p.setAge(-20);//调用方法设置年龄,但是我们不知道该方法的内部实现 } } ~~~ 结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41af86870.jpg) 我们看到当age>0 && age<130时,才合法. 从这个过程我们看到,当我们要操作Person中的age属性时,是通过setAge方法来实现的,但是setAge方法是怎么实现的,外部类是无法知道的,这就是我们所要说的隐藏对象的属性和实现细节,也就是封装. 其实对于属性的封装,为了外部访问,我们一般有规范的get和set方法,我们来看一下两个规范且常用的方法: ~~~ class Person { private int age;//把该成员属性设置为私有 public void setAge(int a)//设置年龄 { age = a; } public int getAge()//获取年龄 { return age; } } ~~~ 下面我们再来说说封装的优点和原则: 优点: 1.提高代码安全性; 2.实现对内部属性的可控性; 3.提高代码的复用性; 4.隔离变化; 原则: 1.将不需要对外提供的内容都隐藏起来; 2.把属性都隐藏起来,提供公共方法供外部访问(当然不是所有方法都必须是公共方法). 最后我们再看一下刚才我们用到的一个关键字:private. **private关键字**:指私有,是一个权限修饰符,用于修饰成员.私有的内容只在本类中有效. 它的常见用法:就是将成员变量私有化,对外提供对应的set,get方法对其进行访问.提高对数据访问的安全性. 注意:私有仅仅是封装的一种体现而已. 对于java语言来讲,最小的封装体就是函数.类也是封装体.框架本身也是封装体.这些都一个共性,就是我们可以不知道实现细节,直接拿来用就可以了.
';

6.6 基本数据类型参数与引用数据类型参数的传递过程

最后更新于:2022-04-01 14:11:05

这一节基本数据类型参数和引用数据类型参数的传递过程。 数据类型参数和引用参数我们在前面章节中都已涉及到了,那么我们来看看下面的两段代码: ~~~ //基本数据类型参数传递 class Demo { public static void main(String[] args) { int x = 3; change(x);//调用方法 System.out.println("x = " + x);// } public static void change(int x) { x = 4; } } //引用类型数据参数传递 class Demo { int x = 3; public static void main(String[] args) { Demo d = new Demo(); d.x = 9; change(d); System.out.println("d.x = " + d.x); } public static void change(Demo d) { d.x = 4; } } ~~~ 现在我们来分别对这两对代码的运行程分析一下。 **一、对于基本数据类型参数传递代码的运行过程分析:** 1.main方法进栈内存,main方法中有基本数据类型变量int x; 2.为main方法中的变量x赋值为3; 3.调用change(x)方法,则change方法进栈; 4.为change方法变量x赋值为4; 5.跳出change方法,同时change方法出栈,释放所有change方法和change方法中的x,即把x=4释放掉; 6.执行打印语句,些时的栈中只有main方法中的x,那么打印出的x=3; 7.跳出main方法,结束程序。 我们来看一下打印结果与我们的分析是不是一致的? ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41af379b3.jpg) **二、对于引用数据类型参数传递代码的运行过程分析:** 1.main方法进栈内存,main方法中有一个类类型变量Demo d; 2.new创建Demo对象,在堆内存中开辟一个空间,并把空间地址传给d(我们这里假设为0x0078),并为该地址中的x初始化为0,然后把3赋给x; 3.把d所指堆内存(0x0078)中的x赋为9; 4.调用change(d)方法,change方法进栈,change方法中的对象d就是main方法中的d,指向之前的堆内存地址(0x0078); 5.把d所指堆内存(0x0078)中的x赋为4; 6.跳出change方法,同时change方法出栈,释放change方法和方法中的对象d; 7.执行打印语句,些时的栈中也是只有main方法,并且d指向堆内存(0x0078),该地址中的x就是步骤5中的值4; 8.跳出main方法,结束程序。 我们看看结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-18_573c41af4e28c.jpg) 我们从两个代码打印出的结果可以看出结果与我们的分析是完全一致的。 那么,基本数据类型参数和引用数据类型参数的过程就是我们上面分析的过程。
';