JAVA程序设计2——面向对象程序设计(上)
1.面向对象的概念
重点一:面向对象的三大特征:封装性、继承性、多态性
重点二:类的结构与对象的定义及使用
重点三:构造方法的定义、重载及私有化
重点四:匿名对象
1.1 面向对象的三大特征
介绍封装性、继承性、多态性
封装性:
封装性有两层意思:
一、把对象的属性和行为封装成一个密不可分的单位即对象。
二、对象的属性和行为可以定义不同级别的访问性,这样就可以对某些对象隐身,对某些可见,能够可控。
继承性:
类是很多对象的抽象,包含属性和方法,当一个类A的属性和方法是另一个类的子集B时,A叫父类,子类比父类有更多的特有属性,也叫派生类。java支持单继承,并通过interface实现多继承。
多态性:
JAVA多态实现有两种,一种是方法重载一种是方法覆写。方法重载是函数名可以相同,但参数类型或个数不同,而方法重写是函数名和参数类型、个数均相同,但封装级别有条件。
2.类与对象
所谓面向对象程序设计,是指程序设计时候主要的工作是设计对象的。类和对象面向对象程序设计的两个重要概念,类(class)和对象(object,也叫做实例instance),类是对象的抽象,或者说是把一群对象的属性和方法抽取出来,形成一个模板,这个模板就是类。"人类"就是一个类,它是很多人的特征、行为的抽象。再如:类是对很多对象的抽象。是所有抽象的代表,就好像元素周期表中,元素与原子的对应关系一样。
使用面向对象观点来进行编程的好处就是:把一个系统看成一个个模块组成,这些模块之间联动(相当于方法之间的调用),让系统运行。类可以实例化。比如变形金刚,一个大黄蜂机器人,设计这个大黄蜂的机器人的设计图就相当于一个类,把设计图转换成具体的变形金刚就是实例化。当然变形金刚设计图里面的每一个模块就相当于一个类,这些类组合在一起就是一个设计。可以把大脑当作一个public类的主函数,因为一切的行为都是由它发起的。使用面向对象的观点来编程可以使程序设计时比较容易扩展,比较容易掌控规模。
备注:java编程语言中方法一般其他语言都称为函数,但是因为java面向对象中的“对象”这个形象的称呼,比如一个变形金刚,发射导弹是一种行为,方法,而用函数称呼好像抽象了点。因此在java中一般称函数为方法。同理变量与属性、参数都是等价的。方法的返回值就是函数的返回值。我们对比下数学中的函数,例如y = f(x1,x2,x3),f是函数名,x1,x2,x3就是函数的参数,y就是返回值
每一个方法或者功能都可以抽象出一个类,可以从结构化编程向面向对象编程这样转变思考方式
2.1 类的设计
一个类最常见有三种成员:构造器(用于构造类的对象或者说实例)、属性、方法。三种成员都可以有一个到多个。如果都为0个,没有多少实际意义。另外内部类、注释也属于类的一部分,这会在其他章节介绍。
2.1.1创建类语法
1 修饰符 class 类名{ 2 3 零到多个构造器定义 4 5 零到多个变量 6 7 零到多个方法 8 9 }
构造器
构造器是创建类创建实例对象的唯一方法,如果设计类时候没有给出创建实例的构造器,系统会默认提供一个,如果已经给出了,则系统不提供构造器。
2.1.2 属性设计
属性是对象拥有的特征,比如人的身高、体重都是属性,属性有类型和值,类型就是基本数据类型和引用数据类型两种,值也对应数据类型
语法:
修饰符 属性类型 属性名 = 属性值
属性语法格式详细说明如下
修饰符:包括三类:封装级别、外加static、final。也可以省略。封装级别有private、忽略、protected、public,需要注意的是有的书把忽略这种情况说成是default,也是默认的意思,"默认"这种说法源自eclipse创建源文件时候的一种选择项是default,但实际上是没有default这种封装级别的关键字的,当然default在别的语句中会有使用,比如switch语句,但在default不是封装级别关键字。
属性类型:属性类型可以是Java语言允许的任何数据类型、包括基本类型和引用类型
属性名:属性名也是标识符的一种了,字母数字下划线美元符,首字母小写,余下单词首字母大写,驼峰写法。
2.1.3 方法设计
方法是对象拥有的行为,比如人有吃饭行为,吃饭就是一种方法。
定义方法的语法格式如下: 修饰符 方法返回值类型 方法名(形参列表){ //由零条到多条可执行性语句组成的方法体 }
方法语法格式说明:
修饰符:private、忽略、protected、public、abstract、static final,封装级别只能出现一种,abstract和final只能出现其中一个,可以与static组合起来修饰方法
方法返回值类型:任何数据类型,如果声明了方法返回值类型,则方法体内必须有一个有效的return语句,该语句返回一个变量或一个表达式,这个变量或者表达式类型必须与此处声明的类型匹配。如果没有返回值则必须使用void来声明没有返回值。
方法名:与属性名命名方法一致。区别是方法后面有括号,括号里面可能会有参数列表
形参列表:形参列表用于定义该方法可以接受的参数,形参列表由零组到多组"参数类型 形参名"组合而成,多组参数之间以英文逗号隔开。形参类型和形参名以英文空格隔开。一旦在定义方法时指定了形参列表,则调用该方法时必须传入对应它的参数值——谁调用方法,谁负责为形参赋值。
2.1.4 构造器
构造器是类属性的一个特殊的方法,定义构造器的语法格式与定义方法的语法格式很像,定义构造器的语法格式如下:
修饰符 构造器名/类名 (形参列表){ //由零条到多条可执行性语句组成的构造器执行体 }
注意:构造器没有返回值类型,也不能有void类型,如果定义了返回值类型或使用void,编译不会出错,但是系统会把它当做一个普通的方法来处理,也就是不会实例化对象。构造器返回的是类的实例,是隐式的
对象的产生和使用
通过new关键字来调用某个类的构造器来创建这个实例,然后把实例赋值给实例变量。
java常用内存区
1、栈内存空间:保存所有的对象引用的堆内存地址
2、堆内存空间:保存每个对象属性的具体内容
3、全局数据区:保存static类型属性
4、全局代码区:保存所有方法的定义
2.1.5 对象、引用和指针
我们知道创建对象的代码是:Person p = new Person();这行代码实际产生了两个实体(存储空间有实际的存储数据):一个是p变量,一个是Person对象
从Person类定义来看,Person对象应包含两个属性,而属性是需要内存来存储的。因此,当创建Person对象时,必然需要有对应的内存来存储Person对象的属性。有多个属性就需要用多个内存块存储这些属性。当把这个Person对象赋值给一个实例变量(引用变量)时,系统并不会把Person对象在内存里重新复制一份,而是让这个实例变量指向这个对象即可,也就是实例变量里存放的仅仅是对象的地址,用实例变量指向实际的对象。
2.1.6 类变量、实例变量
类变量是比较容易容易引起歧义的,一种是类的变量,一个类有类方法,也有类的变量,这个变量可以直接调用。另一种理解有些牵强,比如说int a = 3, 我们称a为整型变量,比较类似的是Person p = null 那这样的话很容易理解把Person当作和int相似作用,因此p也可以被称为类变量,当然这种说法很牵强,因为这里的类Person是一种具体的类,真正比较一般化的类应该是class这个关键字 比如说class Person{...} 这里的class就是和int对等的一种类型,而Person的身份却相当于一个类变量。其实这里讨论Person是类变量或者p是类变量都没有必要的,因为这并不是重点。但是一般情况下类变量是指类的变量,而实例变量指的是p
int a = 3 ;这个代码的本质含义是3和a是属于int类型的,并且把3赋值给了a。而Person p = new Person();这个代码的含义是通过new Person()创建一个实例和一个变量,它们都属于Person类型的,并且把实例赋给实例变量p。
下图是实例变量和实例的关系
实例变量是放在栈内存的,存储的是实例对象的地址,栈内存里放的是实例变量,而对象的数据包括方法、属性都放在堆内存里。因此实例变量与C语言的指针很像,它们都是存储一个地址值,通过这个地址来引用到实际对象。实际上Java里的实例变量就是C里的指针,只是Java封装了这个指针,避免开发者进行繁琐的指针操作。
当一个对象被创建成功后,这个对象被保存在堆内存中,Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象。也就是说,不管是数组还是对象,都只能通过引用来访问它们。
对象的地址显然可以赋给别的实例变量,只要这个实例变量和对象的类型是一致的。Person p1 = p;
如果堆内存里的对象没有任何变量指向该对象,那么程序将无法再访问该对象,这个对象也就变成了垃圾,Java的垃圾回收机制将会回收该对象,释放该对象所占的内存区。要想回收所占的内存,只需要将引用变量和对象之间的关系切断,也就是把这些引用变量赋值为null即可。
2.2 方法设计
方法是类或对象的行为特征的抽象,方法是类或对象最重要的组成部分。但从功能上看,方法完全类似于传统结构化程序设计里的函数。但需要说明的是,Java里的方法不能独立存在,所有的方法都必须定义在类里。方法在逻辑上要么属于类,要么属于对象。
2.2.1方法的特点
从方法的定义语法上看,方法和函数是一致的。但是在使用上,Java里面的方法与传统的函数显著不同:在结构化编程语言里,函数是一等公民,整个软件由一个个的函数组成;在面向对象编程语言里,类才是一等公民,整个系统由一个个的类组成。因此在Java语言里,方法不能独立存在,方法必须属于类或对象。所以定义方法时候,不能单独定义一个方法,必须在类内定义。并且如果方法用了static修饰,那么可以由类来直接调用或者通过创建对象调用,如果没有static修饰,那么只能通过创建对象调用,也就是说用static修饰的方法是属于类和对象的,而没有用static修饰的方法只是属于对象。另外与结构化编程语言里不同的是,Java里的方法允许重载、覆写。
Java语言是静态的:一个类定义完成后,只要不再重新编译这个类文件,该类和该类的对象所拥有的方法是固定的,不会再改变。由于方法不能独立存在,它必须属于一个类或一个对象,因此方法不能像函数那样独立执行,执行方法时必须使用类或对象来作为调用者。即所有方法都必须使用"类.方法"或"对象.方法"形式来调用。同一个类里不同方法之间相互调用时,如果被调用的是普通方法,则默认使用this作为调用者,如果是静态方法即static方法,默认使用类作为调用者。
总之Java里的方法特点主要体现在下面几个方面:
1. 方法定义于类中
2. 方法要么属于类要么属于一个对象
3. 永远不能独立执行方法,执行方法必须使用类或对象作为调用者。
2.2.2 方法的参数传递机制
提到方法就必须提到参数传递,否则这个方法就是死方法了。参数分为两种,实际参数和形式参数。实际参数就是调用方法时候传递给形式参数的值。Java方法传递形式有两种:
1.基本数据类型的变量值传递,此时调用方法时候,方法的参数是一个变量,传递参数时候传递的是变量的值,就是将实际参数值的副本传入方法内,而实际参数本身不会受到影响,就像西游记里面的孙悟空一样,孙悟空的元神复制了一个假的孙悟空,这个孙悟空与原有的孙悟空有相同的能力,但是不管假的孙悟空会怎样,真的孙悟空不会变的。
2.引用类型的地址值传递,这种情况下调用方法时候,方法的参数是一个对象,传递的参数对象的地址值,而不是把对象给复制过去。
例子:交互数值的代码
package chapter6; public class TestSwap { public static void main(String[] args){ int a = 3; int b = 4; System.out.println("交换前a:" + a + " b:" + b); swap(a,b); System.out.println("交换后a:" + x + " b:" + y); } public static void swap(int x,int y){ int temp = x; x = y; y = temp; System.out.println("swap方法里交换后a:" + x + " b:" + y); } }
从上面的运行结果来看,swap方法完成了实参值的交换,但是这并没有影响原先的值。因为实际传递的值是只是值的复制,并非地址传递。也就是说在swap方法里面也创建了两个变量,总计4个变量。系统会为main方法和swap方法分配两块栈区,分别用于保存main方法和swap方法的局部变量。也就是main方法中的ab变量作为参数传入swap方法,在swap方法中重新产生了两个变量x,y
引用数据类型的参数传递,同样传递的也是值,只不过这个只是地址值,也就是主方法把对象的共享权(也就是地址赋值的方式)给调用的方法,从而来直接操纵对象。地址操纵的方式就是会对对象的属性进行直接更改。而基本类型的复制方式并不修改原本的数据。
2.2.3 形参长度可变的方法
从JDK1.5 之后,Java允许定义形参长度可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点...,则表明该形参可以接收多个参数值,多个参数值被当成数组传入,下面定义形参长度可变的方法。
package chapter6; public class VarArgsTest { public static void main(String[] args){ //调用一个包含长度可变的方法 test(3,"English","Castellano","Deutsch"); } public static void test(int x,String...language){ System.out.println(x); for(String var:language){ System.out.println(var); } } }
当然也可以使用数组形参的方法,也就是String[] language,这种形式就是很直接的把数组当作形参,因此调用方法传递参数的时候也必须是一个数组。不过,形参是可变长度,而调用时候是数组也是可以的。
test(3,new String[]{"English","Castellano","Deutsch"}) 或者String[] lang = new String[]{"English","Castellano","Deutsch"} test(3,lang)
package chapter6; public class VarArgsTest { public static void main(String[] args){ //调用一个包含长度可变的方法 String[] language = new String[]{"English","Castellano","Deutsch"}; test(3,language); } public static void test(int x,String[] language){ System.out.println(x); for(String var:language){ System.out.println(var); } } }
必须注意的是:如果采用长度可变的形参,这样的形参只能位于参数列表最后,也就是说一个形参列表里面只能有一个长度可变的形参,而数组参数则可以位于任何位置,显然采用数组形式的形参方式更加灵活。需要提醒的是:大部分时间,我们不推荐重载具有形参长度可变的方法,因为这样做没有多大意义,而且容易引起程序的可读性降低。
一个源代码文件.java文件相当于一个小的功能,这个功能里面可以包含多个类,一个类里面可以包含多个方法。
2.2.4 方法递归
方法递归就是一种反算,当前的表达式值并不能直接计算出来,而是需要反复调用自身进行表达式变换,一直变换到可以计算的状态,如果是无穷递归,也就是说没法变换到一种可以计算的状态,那么这就是一个死循环。
因此定义递归方法时有一条最重要的规定:递归一定要向已知方向递归,并且能够递归到一个可计算的状态。小的方向已知,往小的方向递归,大的方向已知,往大的方向递归。
总之,只要一个方法的方法体实现里再次调用了方法本身就是递归方法,递归一定要向已知方向递归。
package chapter6; public class Recursive { public static void main(String[] args){ System.out.println(fn(10)); } public static int fn(int n){ if(n == 0){ return 1; }else if(n == 1){ return 2; }else{ return 2*fn(n - 1) + fn(n - 2); } } }
2.2.5 方法重载
方法重载就是可以定义同名的方法,但是参数不同,这样调用方法时候,赋予不同的参数,就可以得到不同的结果。注意关键点是形参不同,与修饰符以及返回值类型没有任何关系。
从前面的介绍来看:
调用一个方法时候有三个部分(和定义方法是不同的):
1. 调用者,也就是方法的所属者,既可以是类,也可以是对象。
2. 方法名,方法的标识
3. 形参列表,当调用方法时,系统将会根据传入的实参列表匹配。
package chapter6; public class ReloadTest { public static void reload(){ System.out.println("这是一个零参数函数"); } public static void reload(int x){ System.out.println("传入的参数值是:" + x); } public static void main(String[] args){ ReloadTest.reload(); ReloadTest.reload(3); } }
为什么不能用返回值类型来区分重载(同名)的方法呢?
原因是这样的:对于int f(){}和void f(){}两个方法,如果调用后赋值给一个变量,比如int result = f();系统可以识别是想调用返回值类型为int的方法;但Java调用方法时可以忽略方法返回值,如果采用如下方法来调用:f(),或者System.out.println(f()),谁能判断是调用哪个方法呢?如果程序设计者都不能明确,都不知道自己写的是什么,Java系统当然也会糊涂。在设计程序时候,有一条重要规定:不要让系统糊涂,系统如果茫然了,那肯定是你设计的问题。因此不能使用返回值类型作为区分重载方法的依据。
简而言之:自己都说不清楚不能理解的事情,就别指望别人会明白。
2.3 成员变量和局部变量
在Java语言中,根据定义变量的位置的不同,可以将变量分成两类:成员变量和局部变量。成员变量和局部变量运行机制有较大差异,这里介绍两者的差异。 成员变量指的是在类范围里定义的变量,也就是前面所述的属性;局部变量指的是在一个方法内定义的变量。 不管是成员变量还是局部变量,都需要遵守相同的命名规则,虽然从语法角度来看只要遵守命名规则即可,但是从程序的可读性来讲,应该是有多个意义的单词连缀而成,其中第一个单词字母小写,后面每个单词首字母大写。 细分变量可以发现,变量总计有5种。
类变量 :即类属性,以static修饰,对象共用,都可以修改
成员变量
实例变量 :即实例属性,没有static修饰符,对象自有
所有变量
形参 :方法签名中定义的变量
局部变量 方法局部变量 :在方法内部定义
代码块局部变量 :在代码块中定义
2.3.1 成员变量
static修饰的是类属性,这个类属性从这个类的准备阶段起开始存在,直到系统完全销毁这个类,类属性的作用域与这个类的生成范围相同;而实例属性则从这个实例被创建开始起存在,直到系统完全销毁这个实例,实例属性的作用域与对应实例生存范围相同.
类成员变量访问语法:类.类属性或实例.类属性 注意:通过实例访问类的属性,并不是访问实例自己的属性,而是类的属性,如果通过一个实例修改了类的属性,会导致该类的其他实例访问这个类属性时候获得的是修改后的值。 实例成员变量访问语法:实例.实例属性 成员变量无须显式初始化,定义了之后,系统会自动为这个成员变量赋值
package chapter1; class Person{ public static String name; public int age; }; public class VarTest { public static void main(String[] args){ //创建一个Person类对象 Person p1 = new Person(); //static 变量的两种输出方式 System.out.println("name: " + Person.name); System.out.println("name: " + p1.name); //实例变量输出,只能通过实例访问 //System.out.println("name: " + Person.age); System.out.println("name: " + p1.age); //给成员变量赋值,以类访问 Person.name = "朗道二级相变"; System.out.println("Person.name: " + Person.name); //给成员变量赋值,以实例访问 p1.name = "朗道二级相变2"; System.out.println("p1.name: " + p1.name); //创建新的实例,并访问 Person p2 = new Person(); System.out.println("p2.name: " + p2.name); //修改实例变量 System.out.println("分别给不同的实例赋值"); p1.age = 13; p2.age = 23; System.out.println("p1.age: " + p1.age); System.out.println("p1.age: " + p2.age); } } 输出结果: name: null name: null name: 0 Person.name: 朗道二级相变 p1.name: 朗道二级相变2 p2.name: 朗道二级相变2 分别给不同的实例赋值 p1.age: 13 p1.age: 23
2.3.2 局部变量
形参:在定义方法签名时定义的变量,形参的作用域在整个方法内有效 方法局部变量:在方法体内定义的局部变量,作用域是从定义该变量的地方生效,到该方法结束时失效。 代码块局部变量:在代码块中定义的局部变量,这个局部变量的作用域从定义该变量的地方生效,到该代码块结束时失效。 注意:与成员变量不同的是,局部变量除了形参之外,都必须显式初始化,也就是说先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。会报这样的错误:The local variable a may not have been initialized。
形参的作用域是整个方法体内有效,而且形参也无须显式初始化,形参的初始化在调用该方法时由系统完成。当通过类或对象调用某个方法时,系统会在该方法栈区内为所有的形参分配内存空间,并将实参的值赋给对应的形参,这就完成了形参的初始化。
在同一个类中,成员变量的作用范围是整个类内有效,一个类里不能定义两个同名的成员变量,即使一个是类属性,一个是实例属性也不行。一个方法里面也不呢鞥定义两个同名的局部变量,即使一个是方法局部变量,一个是代码块局部变量或形参也不行。 但java允许局部变量和成员变量同名。如果同名,局部变量覆盖成员变量,如果需要在方法里面引用成员变量,用this关键字(对应实例属性)或类名(对应类属性)。 成员变量是定义在类中的,类变量有static,而实例变量没有static,而局部变量是定义在方法内的或者代码块里面,代码块用{}包起来。
2.4 成员变量的初始化和内存中的运行机制
当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。 对于类成员变量来说,该变量是属于类的,只需要分配一次内存,当创建其他对象时候,并不为对象再次创建类成员变量,而是共用类的成员变量,如果有类的实例变量,那么会为实例变量分配内存空间。
从上图可以看出,修改一个实例变量时候,修改结果与其他实例变量(哪怕是同名的)没有任何关系,而修改类成员变量时候则会影响到其他的实例变量,因为类成员变量大家共同使用的。记住:类属性是属于类的,不属于对象,创建对象时候,当然也不会为对象创建类型,而是使用类初始化后的类属性,对象只可以使用,不能自己创建。
2.5 局部变量的初始化和内存中的运行机制
局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量指向初始化。这意味着定义局部变量后,系统并未这个变量分配内存空间,知道等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。注意:定义局部变量时候可以不初始化,但是如果还需要访问它就必须初始化。 与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中的。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过地址引用到该变量实际引用的对象或数组。 栈内存中的变量无须系统垃圾回收,栈内存的变量往往是随着方法或代码块的运行结束而结束。因此,局部变量的作用域从初始化该变量开始,直到该方法或该代码块运行完成而结束。因此局部变量只保持基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。
2.6 栈内存与堆内存
栈内存:某一个函数被调用时,这个函数会在栈内存里面申请一片空间,以后在这个函数内部定义的变量,都会分配到这个函数所申请到的栈。当函数运行结束时,分配给函数的栈空间被收回,在这个函数中被定义的变量也随之被释放和消失。
堆内存:通过new产生的数组和对象分配在堆内存中。堆内存中分配的内存,由JVM提供的GC(垃圾回收机制)来管理。在堆内存中产生了一个数组对象后,我们还可以在栈中定义一个变量,这个栈中变量的取值等于堆中对象的首地址。栈内存中的变量就成了堆内存中数组或者对象的引用变量。我们以后就可以在程序中直接使用栈中的这个变量来访问我们在堆中分配的数组或者对象,引用变量相当于数组或者对象起的一个别名,或者代号。
引用变量是一个普通的变量,定义时在栈中分配;引用变量在被运行到它的作用域之外时就被释放,而我们的数组和对象本身是在堆中分配的,即使程序运行到使用new产生对象的语句所在的函数或者代码之后,我们刚才被产生的数组和对象也不会被释放。数组和对象只是在没有引用变量指向它,也就是没有任何引用变量的值等于它的首地址,它才会变成垃圾不会被使用,但是它任然占据着内存空间不放(这也就是我们Java比较吃内存的一个原因),在随后一个不确定的时间被垃圾回收器收走。 栈内存放的是引用,堆内存放数据,A a = new A();a是引用,new A();成员变量在new A();也就是在对象或类中,因而成员变量放在堆内存中。 成员变量是放在堆内存中的。
2.7 变量的使用规则
代码块局部变量指的是一些语句比如条件语句、循环语句里面所涉及的变量,与方法里面的局部变量是有明显区别的。 在程序设计时候应该怎样选择类属性、实例属性、方法局部变量、代码块局部变量呢? 如果就程序的运行结果来看,大部分情况下都可以直接使用类属性或者实例属性来解决问题,无须使用局部变量。但实际上这种做法相当错误,因为当我们定义一个成员变量时,成员变量将被放置到堆内存中,成员变量的作用域将扩大到类存在范围或对象存在范围内,这种范围扩大有两个害处。 1. 增大了变量的生存时间,这将导致更大的系统开销。 2. 扩大了变量的作用域,这不利于提高程序的内聚性。 可以对比下面三个程序:
程序1: public class TestScope1 { //定义一个成员变量作为循环变量 static int i; public static void main(String[] args) { for ( i = 0 ; i < 10 ; i++) { System.out.println("Hello"); } } } 程序2 public class TestScope2 { public static void main(String[] args) { //定义一个方法局部变量作为循环变量 int i; for ( i = 0 ; i < 10 ; i++) { System.out.println("Hello"); } } } 程序3: public class TestScope3 { public static void main(String[] args) { //定义一个代码块局部变量作为循环变量 for (int i = 0 ; i < 10 ; i++) { System.out.println("Hello"); } } }
上面三个程序只有第三个最符合程序设计规范。 对于一个循环变量来说,只需要在循环体内有效就可以了。
如果有如下几种情形,应该考虑使用成员变量: 1. 如果需要定义的变量是用于描述某个类或者对象的固有信息的,例如人的身高、体重等信息,他们是人对象的固有信息,每个人都有,这些变量需要定义为成员变量。如果这种信息对这个类的所有实例完全相同或者说是类相关的,例如人类眼睛的数量,目前来说都是2个。这种变量就是需要定义成类变量,如果这种信息是实例相关的,例如人的身高、体重,每个人的身高体重都是可能互不相同,这种变量需要定义成实例变量。
2. 如果在某个类中需要以一个变量来保存该类或者实例运行时的状态信息,例如五子棋程序的棋盘数组,它用以保存五子棋实例运行时的状态信息。这种用于保存某个类,或某个实例状态信息的变量通常使用成员变量。 3. 如果某个信息需要在某个类的多个方法之间进行共享,则这个信息应该使用成员变量来保存。 记住:使用成员变量时候一定是要与类或实例相关的。
即使在程序中使用局部变量,也应该尽可能地缩小局部变量的作用范围,局部变量的作用范围越小,它在内存里停留的时间就越短,程序运行性能越好。因此,能使用代码块局部变量的地方,就坚决不要使用方法局部变量。
2.8构造方法的定义及重载
构造方法又称为构造器。我们说对象一定要实例化,其本质就是用类的构造方法来创造一个具体的对象来使用。我们知道之前实例化时候,需要调用setter和getter方法才行。这样其实不是很方便。如果能在对象实例化时候就一步实现初始化就最好了。这个想法其实在我们没有封装对象之前一直是这样做的,实例化对象时候就初始化。但因为封装性的缘故,所以又无法实现这一点。通过将构造方法重载,可以实现对象初始化时候,直接将对象的值赋给属性。
构造方法的定义和普通方法是一样的。只不过是类用来构造对象的方法而已。构造方法的名称和类的名称是一样的。
构造器是特殊的方法,这个特殊方法用于创建类的实例。构造器就像一个模具,你把这个模具做成什么样子(构造方法/函数写成什么样),做出来的实际产品就是什么样(构造出来的对象就是什么样)。 Java语言里构造器是创建对象的根本途径(即使使用工厂模式、反射等方式创建对象,其实质依然是依赖于构造器),因此Java类必须包含一个或一个以上的构造器。
2.8.1 构造方法的定义语法
class 类名称 访问权限 构造方法名/类名 (参数类型 参数名,...,){ 程序语句;//构造方法没有返回值 }
注意:定义构造器时,只有封装级别,没有返回值类型、static等修饰符,只有封装级别加上方法签名(名称和形参),没有返回值类型。
其实通过构造方法来实例化对象是很直接也是很自然的事情,构造方法的重载本质上是将setter和getter方法集成起来。其实和分别调用setter和getter方法没什么区别。但是从代码形式上看更直接也更容易理解。
构造方法如果没有重载就使用默认的,默认的构造方法是 class 类名{},也就是构造方法里面代码块是空的。构造方法被定义后,系统默认的构造方法将不再生成。
2.8.2 使用构造器执行初始化
构造器最大的用处是在创建对象时执行初始化,之前说明在创建一个对象时,系统为这个对象的属性进行默认初始化,这种默认初始化把基本属性类型的属性设为0(数值型属性)或false(对布尔类型)或null(对引用类型)。 如果想改变这种默认的初始化,在系统创建对象时就为该对象各属性显示指定初始值,就可以通过构造器实现。 如果没有为Java类提供任何构造器,系统会为这个类提供一个无参数的构造器,这个构造器的执行体为空,不做任何事情。 自定义一个构造器
package chapter5; public class TestConstructor { private String name; private int age; public TestConstructor(String name,int age){ this.name = name; this.age = age; }; public static void main(String[] args){ TestConstructor tc = new TestConstructor("Jack",23); System.out.println("姓名:" + tc.name); System.out.println("年龄:" + tc.age); }; }
不仅如此还可以在构造器里面通过像getter和setter方法那样,对初始化的对象进行控制。
package chapter5; import java.util.Scanner; public class TestConstructor { private String name; private int age; public TestConstructor(String name,int age){ //对输入的姓名进行控制 if((name.length() > 0)&&(name.length() < 9)){ this.name = name; }else{ System.out.println("名字长度不符合"); return; } //对输入的年龄进行控制 if((age > 0)&&(age < 150)){ this.age = age; }else{ System.out.println("请输入正确的年龄"); return; } }; public static void main(String[] args){ //定义一个扫描器,扫描输入流 Scanner scannerName = new Scanner(System.in); System.out.println("请输入您的姓名:"); String name = scannerName.nextLine(); Scanner scannerAge = new Scanner(System.in); System.out.println("请输入您的年龄:"); int age = scannerAge.nextInt(); //使用自定义构造器创建对象 TestConstructor tc = new TestConstructor(name,age); System.out.println("您的姓名:" + tc.name); System.out.println("您的年龄:" + tc.age); }; } 请输入您的姓名: 朗道二级相变朗道二级相变 请输入您的年龄: 160 名字长度不符合 您的姓名:null 您的年龄:0
对于上面的结果或许很多人会奇怪,咦?不是控制输入不满足条件吗?为什么最后还能输出对象的信息呢? 需要明确的一点是构造器的作用。构造器是创建Java对象的途径,是不是说构造器就完全负责创建对象呢? 不是的!构造器是创建Java对象的重要途径,通过new 关键字调用构造器时,构造器也确实返回了该类的对象,但这个对象并不是完全由构造器负责创建的。实际上,当程序设计者调用构造器时,系统会先为该对象分配内存空间,并为这个对象执行默认初始化,这个对象已经产生了——这些操作都在构造器执行之前就完成了。也就是说,当系统开始执行构造器的执行体之前,系统已经创建了一个对象,只是这个对象还不能被外部程序访问(需要引用变量),只能在该构造器中通过this来引用它。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回,通常还会赋给另一个引用类型的变量,从而让外部程序可以访问该对象。 一旦程序设计者设计了自定义的构造器,系统不再提供没有参数的构造器,也就是new TestConstructor(),来创建实例,因为该类不再包含无参的构造器。如果程序设计者希望该类报留无参数的构造器,或者希望有多个初始化过程,则可以为该类提供多个构造器,如果一个类里提供了多个构造器,就形成了构造器的重载。通常建议为Java类保留无参默认构造器,因此如果为一个类编写了有参数的构造器,通常建议为该类额外编写一个无参数的构造器。 因为构造器主要用于被其他方法来调用,用以返回该类的实例,因而通常把构造器设置成public访问权限,从而允许系统中任何位置的类来创建该类的对象,除非一些极端情况下,需要限制该类创建对象。
2.8.3 构造器的重载
重载关注的是方法名称相同而形参列表不同。同一个类里具有多个构造器,多个构造器的形参列表,即被称为构造器重载。
有一点需要注意的是:我们说重载关注的是方法名称和形参列表不同(不考虑封装级别)。那么形参列表不同具体指什么呢?形参列表不同指的是参数个数和参数类型不同,对于参数个数来说很好理解,那么对于参数类型呢?比如下面两个构造器
构造器1: public Constructor(String name,int age){ } 构造器2: public Constructor(String name,String age){ }
上面两个构造器理论上是不同的,看起来定义了一个重载的方法,但其实只是名称相同而已,因为使用构造器的时候需要对参数赋值,虽然两个都是age,但是使用this进行调用时候,显然不能调用同一个属性,最终还是需要定义三个属性,而对于同一种属性,我们显然一般不能定义成两种类型(只需要一种,如果有需要,进行基本类型转换就可以了),所以形参列表参数个数相同,并且参数名称相同,但是类型不同的重载理论上是没有问题的,实际用处不大,有点伪重载的感觉(但依然是符合Java规范的重载)。
如果系统定义了多个构造器,其中一个构造器里面可以完全包含另一个构造器的执行体。 对于这种完全包含情况,可以在构造器B中调用构造器A。但是构造器不能直接被调用,必须使用new关键字来调用,但一旦使用了new关键字来调用构造器,将会导致系统重新创建一个对象。为了在构造器B中调用构造器A中的初始化代码,又不会重新创建一个Java对象,可以使用this关键字来调用相应构造器。这一点在前面说过了 举个例子:
public class Apple{ public String name; public String color; public double weight; public Apple(){ } //两个参数的构造器 public Apple(String name , String color){ this.name = name; this.color = color; } //三个参数的构造器 public Apple(String name , String color , double weight){ //通过this调用另一个重载的构造器的初始化代码 this(name , color); //下面this引用该构造器正在初始化的Java对象 this.weight = weight; } }
注意:1. 一个构造器重载其他构造器时候必须放在首行,因此最多只能重载调用一个其他的构造器,如果有两个了,必然有一个放在第一行后面。 使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器。