教学目标: i面向对象基础 i掌握对象的三大特性 i掌握Java类的构建 i掌握如何使用Java类 i理解引用类型 i理解按值传递和按引用传递 i深入理解变量 i掌握包装类 i理解类型转换 i理解Java类的基本运行顺序
|
一、面向对象初步
1.什么是对象?
对象是真实世界中的物体在人脑中的映象,包括实体对象和逻辑对象。实体对象指的是我们能在现实生活中能看得见、摸得着,实际存在的东西,比如:人,桌子,椅子等。逻辑对象是针对非具体物体,但是在逻辑上存在的东西的反映,比如:人与人的关系。为了简单,这里讨论的对象都是实体对象。
2.对象的基本构成
初次接触对象,我们从实体对象入手,因为看得见、摸得着会比较容易理解。
分析实体对象的构成,发现有这样一些共同点,这些实体对象都有自己的属性,这些属性用来决定了对象的具体表现,比如:人有身高、体重等。
除了这些静态的,用于描述实体对象的基本情况外,实体对象还有自己的动作,通过这些动作能够完成一定的功能,我们称之为方法,比如:人的手能动,能够写字,能够刷牙等。
对象同时具备这些静态属性和动态的功能。
3.如何进行对象抽象?
抽象是在思想上把各种对象或现象之间的共同的本质属性抽取出来而舍去个别的非本质的属性的思维方法。也就是说把一系列相同或类似的实体对象的特点抽取出来,采用一个统一的表达方式,这就是抽象。
比如:张三这个人身高180cm,体重75kg,会打篮球,会跑步
李四这个人身高170cm,体重70kg,会踢足球
现在想要采用一个统一的对象来描述张三和李四,那么我们就可以采用如下的表述方法来表述:
人{
静态属性:
姓名;
身高;
体重;
动态动作:
打篮球();
跑步();
踢足球();
}
这个“人”这个对象就是对张三和李四的抽象,那么如何表述张三这个具体的个体呢:
人{
静态属性:
姓名=张三;
身高 = 180cm;
体重 = 75kg;
动态动作:
打篮球(); //相应的打篮球的功能实现
跑步();//相应的跑步的功能实现
踢足球();
}
如何表述李四这个具体的个体呢:
人{
静态属性:
姓名=李四;
身高 = 170cm;
体重 = 70kg;
动态动作:
打篮球();
跑步();
踢足球();//相应的踢足球的功能实现
}
对实体对象的抽象一定要很好的练习,可以把你所看到的任何物体都拿来抽象,“一切皆对象”。要练习到,你看到的没有物体,全是对象就好了。
4.抽象对象和实体对象的关系
仔细观察上面的抽象对象——“人”,和具体的实体对象:“张三”、“李四”。你会发现,抽象对象只有一个,实体对象却是无数个,通过对抽象对象设置不同的属性,赋予不同的功能,那么就能够表示不同的实体对象。
这样就大大简化了对象的描述工作,使用一个对象就可以统一地描述某一类实体了,在需要具体的实体的时候,分别设置不同的值就可以表示具体对象了。
5.Java中的类和对象
5.1 Java中的类
把抽象出来的对象使用Java表达出来,那就是类class。类在Java编程语言中作为定义新类型的一种途径,类声明可定义新类型并描述这些类型是如何实现的。接下来将会学习许多关于类的特性。
比如前面讨论过的“人”使用Java表达出来就是一个类。
5.2 Java中的对象
Java中的对象是在Java中一个类的实例,也称实例对象。实例就是实际例子。
类可被认为是一个模板------你正在描述的一个对象模型。一个对象就是你每次使用的时候创建的一个类的实例的结果。
比如前面讨论的张三和李四,他们就是“人”这个类的实例。
二、面向对象三大特征
1.封装
封装这个词听起来好象是将什么东西包裹起来不要别人看见一样,就好象是把东西装进箱子里面,这样别人就不知道箱子里面装的是什么东西了。其实JAVA中的封装这个概念也就和这个是差不多的意思。
封装是JAVA面向对象的特点的表现,封装是一种信息隐蔽技术。它有两个含义:即把对象的全部属性和全部服务结合在一起,形成一个不可分割的独立单位;以及尽可能隐藏对象的内部结构。也就是说,如果我们使用了封装技术的话,别人就只能用我们做出来的东西而看不见我们做的这个东西的内部结构了。
封装的功能
- 隐藏对象的实现细节
- 迫使用户去使用一个界面访问数据
- 使代码更好维护
封装迫使用户通过方法访问数据能保护对象的数据不被误修改,还能使对象的重用变得更简单。数据隐藏通常指的就是封装。它将对象的外部界面与对象的实现区分开来,隐藏实现细节。迫使用户去使用外部界面,即使实现细节改变,还可通过界面承担其功能而保留原样,确保调用它的代码还继续工作。封装使代码维护更简单。
2.继承
is a 关系 —— 子对象
在面向对象世界里面,常常要创建某对象(如:一个职员对象),然后需要一个该基本对象的更专业化的版本,比如,可能需要一个经理的对象。显然经理实际上是一个职员,经理和职员具有is a的关系,经理只是一个带有附加特征的职员。因此,需要有一种办法从现有对象来创建一个新对象。这个方式就是继承。
“继承”是面向对象软件技术当中的一个概念。如果一个对象A继承自另一个对象B,就把这个A称为"B的子对象",而把B称为"A的父对象"。继承可以使得子对象具有父对象的各种属性和方法,而不需要再次编写相同的代码。在令子对象继承父对象的同时,可以重新定义某些属性,并重写某些方法,即覆盖父对象的原有属性和方法,使其获得与父对象不同的功能。
3.多态
同一行为的多种不同表达,或者同一行为的多种不同实现就叫做多态。
还是用刚才经理和职员这个例子来举例:人事部门需要对公司所有职员统一制作胸卡(一般也就是门禁卡,进出公司证明身份使用),制作的师傅说,只要告诉我一个人员的信息,就可以制作出一份胸卡,简化一下就是:一位职员的信息对应一份胸卡。
这个时候,对胸卡制作的师傅而言,所有的人都是职员,无所谓是经理还是普通职员。也就是说,对于传递职员信息这样一个行为,存在多种不同的实现,既可以传递经理的信息,也可以传递普通职员的信息。这就是多态的表现。
再举一个例子:比如我们说“笔”这个对象,它就有很多不同的表达或实现,比如有钢笔、铅笔、圆珠笔等等。那么我说“请给我一支笔”,你给我钢笔、铅笔或者圆珠笔都可以,这里的“笔”这个对象就具备多态。
三、Java类的基本构成
1.Java类的定义形式
一个完整的Java类通常由下面六个部分组成:
包定义语句 (一句)
import语句 (若干句)
类定义{
成员变量
构造方法
成员方法
}
其中:只有类定义和“{}”是不可或缺的,其余部分都可以根据需要来定义。
下面分别来学习各个部分的基本规则,看看如何写Java的类。
2.包
2.1 包是什么?
在Java中,包是类、接口或其它包的集合,包主要用来将类组织起来成为组,从而对类进行管理。
2.2 包能干什么?
包对于下列工作非常有用:
(1):包允许您将包含类代码的文件组织起来,易于查找和使用适当的类。
(2):包不止是包含类和接口,还能够包含其它包。形成层次的包空间。
(3):它有助于避免命名冲突。当您使用很多类时,确保类和方法名称的唯一性是非常困难的。包能够形成层次命名空间,缩小了名称冲突的范围,易于管理名称。
为便于管理数目众多的类,Java语言中引入了“包”的概念,可以说是对定义的Java类进行“分组”,将多个功能相关的类定义到一个“包”中,以解决命名冲突、引用不方便、安全性等问题。
就好似当今的户籍制度,每个公民除有自己的名字“张三”、“李四”外还被规定了他的户籍地。假定有两个人都叫张三,只称呼名字就无法区分他们,但如果事先登记他们的户籍分别在北京和上海,就可以很容易的用“北京的张三”、“上海的张三”将他们区分开来。如果北京市仍有多个张三,还可以细分为“北京市.海淀区的张三”、“北京市.西城区.平安大街的张三”等等,直到能惟一标识每个“张三”为止。
JDK中定义的类就采用了“包”机制进行层次式管理,下图显示了其组织结构的一部分:
|
lang包 |
java包 |
io 包 |
System类 |
String类 |
Object类 |
从图中可以看出,一个名为java的包中又包含了两个子包:io包和lang包。lang包中包含了System, String, Object三个类的定义。事实上,Java包中既可以包含类的定义,也可以包含子包,或同时包含两者。
简而言之:从逻辑上讲,包是一组相关类的集合;从物理上讲,同包即同目录。
但是千万注意:不能将一般的目录看作是包!
2.1 JDK中常用的包
java.lang —— 包含一些Java语言的核心类,包含构成Java语言设计基础的类。在此包中定义的最重要的一个类是“Object”,代表类层次的根,Java是一个单根系统,最终的根就是“Object”,这个类会在后面讲到。
Java并不具有“自由”的方法,例如,不属于任何类的方法,Java中的所有方法必须始终属于某个类。经常需要使用数据类型转换方法。Java在Java.lang包中定义了“包装对象”类,使我们能够实现数据类型转换。如Boolean、Character、Integer、Long、Float和Double,这些在后面会讲到。
此包中的其它类包括:
l Math —— 封装最常用的数学方法,如正弦、余弦和平方根。
l String,StringBuffer —— 封装最常用的字符串操作。
你不必显示导入该包,该Java包通常已经导入。
java.awt —— 包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。
javax.swing —— 完全Java版的图形用户界面(GUI)解决方案,提供了很多完备的组件,可以应对复杂的桌面系统构建。
java.net —— 包含执行与网络相关的操作的类,如URL, Socket, ServerSocket等。
java.io —— 包含能提供多种输入/输出功能的类。
java.util —— 包含一些实用工具类,如定义系统特性、使用与日期日历相关的方法。还有重要的集合框架。
2.2 Java中如何表达包 —
package语句
Java语言使用package语句来实现包的定义。package语句必须作为Java源文件的第一条语句,指明该文件中定义的类所在的包。若缺省该语句,则指定为无名包,其语法格式为:
package pkg1[.pkg2[.pkg3…]]; //“[]”表示可选
Java编译器把包对应于文件系统的目录管理,因此包也可以嵌套使用,即一个包中可以含有类的定义也可以含有子包,其嵌套层数没有限制。package语句中,用‘.’来指明包的层次;
程序package的使用:Test.java
package p1;
public class Test{
public void display(){
System.out.println("in method display()");
}
}
Java语言要求包声明的层次和实际保存类的字节码文件的目录结构存在对应关系,以便将来使用该类时能通过包名(也就是目录名)查找到所需要的类文件。简单地说就是包的层次结构需要和文件夹的层次对应。
注意:每个源文件只有一个包的声明,而且包名应该全部小写。
具体来说,程序员要做以下工作:
2.3 编译和生成包
如果在程序Test.java中已定义了包p1,就必须将编译生成的字节码文件Test.class保存在与包名同名的子目录中,可以选用下述两种方式之一:
采用下述命令编译:
javac Test.java
则编译器会在当前目录下生成Test.class文件,再在适合位置手动创建一个名为p1的子目录,将Test.class复制到该p1目录下。
采用简化的编译命令,就是可以带包编译
javac -d destpath Test.java
归入该包的类的字节代码文件应放在java的类库所在路径的destpath子目录下。现在包的相对位置已经决定了,但java类库的路径还是不定的。事实上,java可以有多个存放类库的目录,其中的缺省路径为java目录下的lib子目录,你可以通过使用-classpath选项来确定你当前想选择的类库路径。除此之外,你还可以在CLASSPATH环境变量中设置类库路径。destpath为目标路径,可以是本地的任何绝对或相对路径。则编译器会自动在destpath目录下建立一个子目录p1,并将生成的.class文件自动保存到destpath/p1下。例如:
javac -d .\ Test.java
javac -d C:\test\ Test.java
2.4 带包运行
运行带包的程序,需要使用类的全路径,也就是带包的路径,比如上面的那个程序,就使用如下的代码进行运行:
java p1.Test
3.import 语句
为了能够使用某一个包的成员,我们需要在Java程序中明确导入该包。使用“import”语句可完成此功能。在java源文件中import语句应位于package语句之后,所有类的定义之前,可以有0~多条,其语法格式为:
import package1[.package2…].(classname|*);
java运行时环境将到CLASSPATH + package1.[package2…]路径下寻找并载入相应的字节码文件classname.class。“*”号为通配符,代表所有的类。也就是说import语句为编译器指明了寻找类的途径。
例,使用import语句引入类程序:TestPackage.java
import p1.Test; //或者import p1.*;
public class TestPackage{
public static void main(String args[]){
Test t = new Test(); //Test类在p1包中定义
t.display();
}
}
java编译器默认为所有的java程序引入了JDK的java.lang包中所有的类(import java.lang.*;),其中定义了一些常用类:System、String、Object、Math等。因此我们可以直接使用这些类而不必显式引入。但使用其它非无名包中的类则必须先引入、后使用。
3.1 Java类搜寻方式
程序中的import语句标明要引入p1包中的Test类,假定环境变量CLASSPATH的值为“.;C:\jdk6\lib;D:\ex”,java运行环境将依次到下述可能的位置寻找并载入该字节码文件Test.class:
.\p1\Test.class
C:\jdk6\lib\p1\Test.class
D:\ex\p1\Test.class
其中,“.”代表当前路径,如果在第一个路径下就找到了所需的类文件,则停止搜索。否则依次搜索后续路径,如果在所有的路径中都未找到所需的类文件,则编译或运行出错。
4.访问修饰符
Java语言允许对类中定义的各种属性和方法进行访问控制,即规定不同的保护等级来限制对它们的使用。为什么要这样做?Java语言引入类似访问控制机制的目的在于实现信息的封装和隐藏。Java语言为对类中的属性和方法进行有效地访问控制,将它们分为四个等级:private, 无修饰符, protected, public,具体规则如下:
表 Java类成员的访问控制
变量和方法可以处于四个访问级别中的一个:公共,受保护,无修饰符或私有。类可以在公共或无修饰级别。
变量、方法或类有缺省(无修饰符)访问性,如果它没有显式受保护修饰符作为它的声明的一部分的话。这种访问性意味着,访问可以来自任何方法,当然这些方法只能在作为对象的同一个包中的成员类当中。
以修饰符protected标记的变量或方法实际上比以缺省访问控制标记的更易访问。一个protected方法或变量可以从同一个包中的类当中的任何方法进行访问,也可以是从任何子类中的任何方法进行访问。当它适合于一个类的子类但不是不相关的类时,就可以使用这种受保护访问来访问成员。
5.类定义
Java程序的基本单位是类,你建立类之后,就可用它来建立许多你需要的对象。Java把每一个可执行的成分都变成类。
类的定义形式如下:
[一般修饰符] class {
[]
[]
[]
}
这里,类名要是合法的标识符。在类定义的开始与结束处必须使用花括号。你也许想建立一个矩形类,那么可以用如下代码:
public class Rectangle{
......//矩形具体的属性和方法
}
6.构造方法
6.1 什么是构造方法?
类有一个特殊的成员方法叫作构造方法,它的作用是创建对象并初始化成员变量。在创建对象时,会自动调用类的构造方法。
6.2 构造方法定义规则
Java中的构造方法必须与该类具有相同的名字,并且没有方法的返回类型(包括没有void)。另外,构造方法一般都应用public类型来说明,这样才能在程序任意的位置创建类的实例--对象。
6.3 示例
下面是一个Rectangle类的构造方法,它带有两个参数,分别表示矩形的长和宽:
public class Rectangle{
int width;
int height;
public Rectangle(int w,int h) {
width=w;
height=h;
}
}
6.4 说明
每个类至少有一个构造方法。如果不写一个构造方法,Java编程语言将提供一个默认的,该构造方法没有参数,而且方法体为空。
注意:如果一个类中已经定义了构造方法则系统不再提供默认的构造方法。
7.析构方法
析构方法finalize的功能是:当对象被从内存中删除时,该成员方法将会被自动调用。通常,在析构方法内,你可以填写用来回收对象内部的动态空间的代码。
特别注意:当我们去调用析构方法的时候,并不会引起该对象实例从内存中删除,而是不会起到任何作用。
在Java编程里面,一般不需要我们去写析构方法,这里只是了解一下就可以了。
8.属性
8.1 属性是什么?
简单点说,属性就是对象所具有的静态属性。
8.2 定义规则
Java类中属性的声明采用如下格式:
访问修饰符 修饰符 类型 属性名称=初始值;
访问修饰符:可以使用四种不同的访问修饰符中的一种,包括public(公共的)、protected(受保护的),无修饰符和private(私有的)。public访问修饰符表示属性可以从任何其它代码调用。private表示属性只可以由该类中的其它方法来调用。protected将在以后的课程中讨论。
修饰符:是对属性特性的描述,例如后面会学习到的:static、final等等。
类型:属性的数据类型,可以是任意的类型。
属性名称:任何合法标识符
初始值:赋值给属性的初始值。如果不设置,那么会自动进行初始化,基本类型使用缺省值,对象类型自动初始化为null。
8.3 说明
属性有时候也被称为成员变量、实例变量、域,它们经常被互换使用。
9.方法
9.1 方法是什么?
方法就是对象所具有的动态功能。
9.2 定义规则
Java类中方法的声明采用以下格式:
访问修饰符 修饰符 返回值类型 方法名称 (参数列表) throws 异常列表 {方法体}
访问修饰符:可以使用四种不同的访问修饰符中的一种,包括public(公共的)、protected(受保护的),无修饰符和private(私有的)。public访问修饰符表示方法可以从任何其它代码调用。private表示方法只可以由该类中的其它方法来调用。protected将在以后的课程中讨论。
修饰符:是对方法特性的描述,例如后面会学习到的:static、final、abstract、synchronized等等。
返回值类型:表示方法返回值的类型。如果方法不返回任何值,它必须声明为void(空)。Java技术对返回值是很严格的,例如,如果声明某方法返回一个int值,那么方法必须从所有可能的返回路径中返回一个int值(只能在等待返回该int值的上下文中被调用。)
方法名称:可以是任何合法标识符,并带有用已经使用的名称为基础的某些限制条件。
参数列表:允许将参数值传递到方法中。列举的元素由逗号分开,而每一个元素包含一个类型和一个标识符。在下面的方法中只有一个形式参数,用int类型和标识符days来声明:public void test(int days){}
throws 异常列表:子句导致一个运行时错误(异常)被报告到调用的方法中,以便以合适的方式处理它。异常在后面的课程中介绍。
花括号内是方法体,即方法的具体语句序列。
9.3 示例
比如现在有一个“车”的类Car,“车”具有一些基本的属性,比如四个轮子,一个方向盘,车的品牌等等。当然,车也具有自己的功能,也就是方法,比如车能够“开动”——run。要想车子能够开动,需要给车子添加汽油,也就是说,需要为run方法传递一些参数“油”进去。车子跑起来过后,我们需要知道当前车辆运行的速度,就需要run方法具有返回值“当前的速度”。
package cn.javass.javatest;
public class Car {// 车这个类
private String make;// 一个车的品牌
private int tyre;// 一个车具有轮胎的个数
private int wheel;// 一个车具有方向盘的个数
public Car() {
// 初始化属性
make = "BMW";// 车的品牌是宝马
tyre = 4;// 一个车具有4个轮胎
wheel = 1;// 一个车具有一个方向盘
}
public double run(int oil) {
// 进行具体的功能处理
return 200.0;
}
}
9.4 形参和实参
形参:就是形式参数的意思。是在定义方法名的时候使用的参数,用来标识方法接收的参数类型,在调用该方法时传入。
实参:就是实际参数的意思。是在调用方法时传递给该方法的实际参数。
比如:上面的例子中“int oil”就是个形式参数,这里只是表示需要加入汽油,这个方法才能正常运行,但具体加入多少,要到真正使用的时候,也就是调用这个方法的时候才具体确定,加入调用的时候传入“80”,这就是个实际参数。
形参和实参有如下基本规则:
(1)形参和实参的类型必须要一致,或者要符合隐含转换规则
(2)形参类型不是引用类型时,在调用该方法时,是按值传递的。在该方法运行时,形参和实参是不同的变量,它们在内存中位于不同的位置,形参将实参的值复制一份,在该方法运行结束的时候形参被释放,而实参内容不会改变。
(3)形参类型是引用类型时,在调用该方法时,是按引用传递的。运行时,传给方法的是实参的地址,在方法体内部使用的也是实参的地址,即使用的就是实参本身对应的内存空间。所以在函数体内部可以改变实参的值。
9.5 参数可变的方法 (学习了数组和包装类后再学习此内容)
从JDK5.0开始,提供了参数可变的方法。
当不能确定一个方法的入口参数的个数时,5.0以前版本的Java中,通常的做法是将多个参数放在一个数组或者对象集合中作为参数来传递,5.0版本以前的写法是:
int sum(Integer[] numbers){…}
//在别处调用该方法
sum(new Integer[] {12,13,20});
而在5.0版本中可以写为:
int sum(Integer... numbers){//方法内的操作}
注意:方法定义中是三个点
//在别处调用该方法
sum(12,13,20);//正确
sum(10,11); //正确
也就是说,传入参数的个数并不确定。但请注意:传入参数的类型必须是一致的,究其本质,就是一个数组。
显然,JDK5.0版本的写法更为简易,也更为直观,尤其是方法的调用语句,不仅简化很多,而且更符合通常的思维方式,更易于理解。
四、如何使用一个Java类
前面学习了如何定义一个类,下面来学习如何使用一个类。
1.new关键字
假如定义了一个表示日期的类,有三个整数变量;日、月和年的意义即由这些整数变量给出。如下所示:
class MyDate {
int day;
int month;
int year;
}
名称MyDate按照大小写的有关约定处理,而不是由语意要求来定。
那么怎么来使用这个类呢:在你可以使用变量之前,实际内存必须被分配。这个工作是通过使用关键字new来实现的。如下所示:
在一个方法体中,声明
MyDate today;
today = new MyDate();
第一个语句(声明)仅为引用分配了足够的空间,而第二个语句则通过调用对象的构造方法为构成MyDate的三个整数分配了空间。对象的赋值使变量today重新正确地引用新的对象。这两个操作被完成后,MyDate对象的内容则可通过today进行访问。
关键字new意味着内存的分配和初始化,new 调用的方法就是类的构造方法。
使用一个语句同时为引用today和由引用today所指的对象分配空间也是可能的。
MyDate today = new MyDate();
2.如何使用对象中的属性和方法
要调用对象中的属性和方法,使用“.”操作符。
例如:
today.day = 26;
today.month = 7;
today.year = 2008;
3.this关键字
关键字this是用来指向当前对象或类实例的,功能说明如下:
3.1 点取成员
this.day指的是调用当前对象的day字段,示例如下:
public class MyDate {
private int day, month, year;
public void tomorrow() {
this.day = this.day + 1;
//其他代码
}
}
Java编程语言自动将所有实例变量和方法引用与this关键字联系在一起,因此,使用关键字在某些情况下是多余的。下面的代码与前面的代码是等同的。
public class MyDate {
private int day, month, year;
public void tomorrow() {
day = day + 1; // 在day前面没有使用this
//其他代码
}
}
3.2 区分同名变量
也有关键字this使用不多余的情况。如,需要在某些完全分离的类中调用一个方法,并将当前对象的一个引用作为参数传递时。例如:
Birthday bDay = new Birthday (this);
还有一种情况,就是在类属性上定义的变量和方法内部定义的变量相同的时候,到底是调用谁呢?例如:
public class Test{
int i = 2;
public void t(){
int i = 3; //跟属性的变量名称是相同的
System.out.println(“实例变量i=”+ this.i);
System.out.println(“方法内部的变量i=”+ i);
}
}
也就是说:“this.变量”调用的是当前属性的变量值,直接使用变量名称调用的是相对距离最近的变量的值。
3.3 作为方法名来初始化对象
也就是相当于调用本类的其它构造方法,它必须作为构造方法的第一句。示例如下:
public class Test {
public Test(){
this(3);//在这里调用本类的另外的构造方法
}
public Test(int a){
}
public static void main(String[] args) {
Test t = new Test();
}
}
五、引用类型
1.引用类型是什么?
一般引用类型(reference type)指向一个对象,不是原始值,指向对象的变量是引用变量。
在Java里面除去基本数据类型的其它类型都是引用数据类型。Java程序运行时,会为引用类型分配一定量的存储空间并解释该存储空间的内容。
示例如下:
public class MyDate{
private int day=8;
private int month=8;
private int year=2008;
public MyDate(int day, int month, int year){…}
public void print(){…}
}
public class TestMyDate{
public static void main(String args[]){
MyDate today = new MyDate(23,7,2008);//这个today变量
//就是一个引用类型的变量
}
}
2.引用类型的赋值
在Java编程语言中,用类的一个类型声明的变量被指定为引用类型,这是因为它正在引用一个非原始类型,这对赋值具有重要的意义。请看下列代码片段:
int x = 7;
int y = x;
String s = “Hello”;
String t = s;
四个变量被创建:两个原始类型 int 和两个引用类型String。x的值是7,而这个值被复制到y;x 和 y是两个独立的变量且其中任何一个的进一步的变化都不对另外一个构成影响。
至于变量 s 和 t,只有一个String 对象存在, 它包含了文本“Hello”,s和 t均引用这个单一的对象。
将变量t 重新定义为:t=”World”; 则新的对象World被创建,而 t 引用这个对象。上述过程被描述如下
3.按值传递还是按引用传递
这个在Java里面是经常被提起的问题,也有一些争论,似乎最后还有一个所谓的结论:“在Java里面参数传递都是按值传递”。事实上,这很容易让人迷惑,下面先分别看看什么是按值传递,什么是按引用传递,只要能正确理解,至于称作按什么传递就不是个大问题了。
3.1 按值传递是什么?
指的是在方法调用时,传递的参数是按值的拷贝传递。示例如下:
public class TempTest {
private void test1(int a){
//做点事情
}
public static void main(String[] args) {
TempTest t = new TempTest();
int a = 3;
t.test1(a);//这里传递的参数a就是按值传递
}
}
按值传递重要特点:传递的是值的拷贝,也就是说传递后就互不相关了。
示例如下:
public class TempTest {
private void test1(int a){
a = 5;
System.out.println("test1方法中的a==="+a);
}
public static void main(String[] args) {
TempTest t = new TempTest();
int a = 3;
t.test1(a);//传递后,test1方法对变量值的改变不影响这里的a
System.out.println("main方法中的a==="+a);
}
}
运行结果是:
test1方法中的a===5
main方法中的a===3
3.2 按引用传递是什么?
指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。
示例如下:
public class TempTest {
private void test1(A a){
}
public static void main(String[] args) {
TempTest t = new TempTest();
A a = new A();
t.test1(a); //这里传递的参数a就是按引用传递
}
}
class A{
public int age = 0;
}
3.3 按引用传递的重要特点
传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
示例如下:
第1行 public class TempTest {
第2行 private void test1(A a){
第3行 a.age = 20;
第4行 System.out.println("test1方法中的age="+a.age);
第5行 }
第6行 public static void main(String[] args) {
第7行 TempTest t = new TempTest();
第8行 A a = new A();
第9行 a.age = 10;
第10行 t.test1(a);
第11行 System.out.println("main方法中的age="+a.age);
第12行 }
第13行 }
第14行 class A{
第15行 public int age = 0;
第16行 }
运行结果如下:
test1方法中的age=20
main方法中的age=20
3.4 理解按引用传递的过程——内存分配示意图
要想正确理解按引用传递的过程,就必须学会理解内存分配的过程,内存分配示意图可以辅助我们去理解这个过程。
用上面的例子来进行分析:
(1):运行开始,运行第8行,创建了一个A的实例,内存分配示意如下:
这是一个A的实例 此时age = 0; |
main方法中的变量a
(2):运行第9行,是修改A实例里面的age的值,运行后内存分配示意如下:
这是一个A的实例 此时age = 10; |
(3):运行第10行,是把main方法中的变量a所引用的内存空间地址,按引用传递给test1方法中的a变量。请注意:这两个a变量是完全不同的,不要被名称相同所蒙蔽。
内存分配示意如下:
main方法中的变量a
赋值给 (按引用传递)
test1方法中的变量a
由于是按引用传递,也就是传递的是内存空间的地址,所以传递完成后形成的新的内存示意图如下:
main方法中的变量a
test1方法中的变量a
也就是说:是两个变量都指向同一个空间。
(4):运行第3行,为test1方法中的变量a指向的A实例的age进行赋值,完成后形成的新的内存示意图如下:
main方法中的变量a
test1方法中的变量a
此时A实例的age值的变化是由test1方法引起的
(5):运行第4行,根据此时的内存示意图,输出test1方法中的age=20
(6):运行第11行,根据此时的内存示意图,输出main方法中的age=20
3.5 对上述例子的改变
理解了上面的例子,可能有人会问,那么能不能让按照引用传递的值,相互不影响呢?就是test1方法里面的修改不影响到main方法里面呢?
方法是在test1方法里面新new一个实例就可以了。改变成下面的例子,其中第3行为新加的:
第1行 public class TempTest {
第2行 private void test1(A a){
第3行 a = new A();//新加的一行
第4行 a.age = 20;
第5行 System.out.println("test1方法中的age="+a.age);
第6行 }
第7行 public static void main(String[] args) {
第8行 TempTest t = new TempTest();
第9行 A a = new A();
第10行 a.age = 10;
第11行 t.test1(a);
第12行 System.out.println("main方法中的age="+a.age);
第13行 }
第14行}
第15行class A{
第16行 public int age = 0;
第17行}
运行结果为:
test1方法中的age=20
main方法中的age=10
为什么这次的运行结果和前面的例子不一样呢,还是使用内存示意图来理解一下
3.6 再次理解按引用传递
(1) 运行开始,运行第9行,创建了一个A的实例,内存分配示意如下:
main方法中的变量a
(2) 运行第10行,是修改A实例里面的age的值,运行后内存分配示意如下:
main方法中的变量a
(3)运行第11行,是把main方法中的变量a所引用的内存空间地址,按引用传递给test1方法中的a变量。请注意:这两个a变量是完全不同的,不要被名称相同所蒙蔽。
内存分配示意如下:
这是一个A的实例 此时age = 10; |
赋值给 (按引用传递)
test1方法中的变量a
由于是按引用传递,也就是传递的是内存空间的地址,所以传递完成后形成的新的内存示意图如下:
main方法中的变量a
test1方法中的变量a
也就是说:是两个变量都指向同一个空间。
(4)运行第3行,为test1方法中的变量a重新生成了新的A实例的,完成后形成的新的内存示意图如下:
这是一个A的实例 此时age = 10; |
main方法中的变量a
test1方法中的变量a
(5)运行第4行,为test1方法中的变量a指向的新的A实例的age进行赋值,完成后形成的新的内存示意图如下:
main方法中的变量a
test1方法中的变量a
注意:这个时候test1方法中的变量a的age被改变,而main方法中的是没有改变的。
(6) 运行第5行,根据此时的内存示意图,输出test1方法中的age=20
(7) 运行第12行,根据此时的内存示意图,输出main方法中的age=10
3.7 说明
(1)“在Java里面参数传递都是按值传递”这句话的意思是:按值传递是传递的值的拷贝,按引用传递其实传递的是引用的地址值,所以统称按值传递。
(2)在Java里面只有基本类型和按照下面这种定义方式的String是按值传递,其它的都是按引用传递。就是直接使用双引号定义字符串方式:String str = “Java私塾”;
六、再谈变量
1.实例变量和局部变量
在方法外定义的变量主要是实例变量(不带static 修饰的),它们是在使用new Xxxx ()创建一个对象时被分配内存空间的。每当创建一个对象时,系统就为该类的所有实例变量分配存储空间;创建多个对象就有多份实例变量。通过对象的引用就可以访问实例变量。
在方法内定义的变量或方法的参数被称为局部(local)变量,有时也被用为自动(automatic)、临时(temporary)或栈(stack)变量。
方法参数变量定义在一个方法调用中传送的自变量,每次当方法被调用时,一个新的变量就被创建并且一直存在到程序的运行跳离了该方法。
当执行进入一个方法遇到局部变量的声明语句时,局部变量被创建,当执行离开该方法时,局部变量被取消,也就是该方法结束时局部变量的生命周期也就结束了。
因而,局部变量有时也被引用为“临时或自动”变量。在成员方法内定义的变量对该成员变量是“局部的”,因而,你可以在几个成员方法中使用相同的变量名而代表不同的变量。该方法的应用如下所示:
public class Test {
private int i; // Test类的实例变量
public int firstMethod() {
int j = 1; // 局部变量
// 这里能够访问i和j
System.out.println("firstMethod 中 i="+i+",j="+j);
return 1;
} // firstMethod()方法结束
public int secondMethod(float f) { //method parameter
int j = 2; //局部变量,跟firstMethod()方法中的j是不同的
// 这个j的范围是限制在secondMethod()中的
// 在这个地方,可以同时访问i,j,f
System.out.println("secondMethod 中 i="+i+",j="+j+",f="+f);
return 2;
}
public static void main(String[] args) {
Test t = new Test();
t.firstMethod();
t.secondMethod(3);
}
}
2.变量初始化
在Java程序中,任何变量都必须经初始化后才能被使用。当一个对象被创建时,实例变量在分配内存空间时按程序员指定的初始化值赋值,否则系统将按下列默认值进行初始化:
byte |
0 |
short |
0 |
int |
0 |
long |
0L |
float |
0.0f |
double |
0.0d |
char |
'\u0000' |
boolean |
false |
所有引用类型 |
null |
注意── 一个具有空值null的引用不引用任何对象。试图使用它引用的对象将会引起一个异常。异常是出现在运行时的错误,这将在模块“异常”中讨论。
在方法外定义的变量被自动初始化。局部变量必须在使用之前做“手工”(由程序员进行)初始化。如果编译器能够确认一个变量在初始化之前可能被使用的情形,编译器将报错。
public class Test {
private int i; //Test类的实例变量
public void test1() {
int x = (int) (Math.random() * 100);
int y;
int z;
if (x > 50) {
y = 9;
}
z = y + x; // 将会引起错误,因为y可能还没有被初始化就使用了
}
public static void main(String[] args) {
Test t = new Test();
t.test1();
}
}
3.变量的范围(scope)
Java变量的范围有四个级别:类级、对象实例级、方法级、块级。
(1)类级变量又称全局级变量,在对象产生之前就已经存在,就是后面会学到的static变量。
(2)对象实例级,就是前面学到的属性变量
(3)方法级:就是在方法内部定义的变量,就是前面学到的局部变量。
(4)块级:就是定义在一个块内部的变量,变量的生存周期就是这个块,出了这个块就消失了。
示例如下:
public class Test {
private static String name="Java私塾";//类级
private int i; // 对象实例级,Test类的实例变量
{//属性块,在类初始化属性时候运行
int j = 2;//块级
}
public void test1() {
int j = 3;//方法级
if(j==3){
int k = 5;//块级
}
//这里不能访问块级的变量,块级变量只能在块内部访问
System.out.println("name="+name+",i="+i+",j="+j);
}
public static void main(String[] args) {
Test t = new Test();
t.test1();
}
}
运行结果:
name=Java私塾,i=0,j=3
3.1 访问说明
(1)方法内部除了能访问方法级的变量,还可以访问类级和实例级的变量
(2)块内部能够访问类级、实例级变量,如果块被包含在方法内部,它还可以访问方法级的变量。
(3)变量当然是要在被访问前被定义和初始化,不能访问后面才定义的变量。
七、包装类
虽然Java语言是典型的面向对象编程语言,但其中的8种基本数据类型并不支持面向对象的编程机制,基本类型的数据不具备“对象”的特性——不携带属性、没有方法可调用。沿用它们只是为了迎合人类根深蒂固的习惯,并的确能简单、有效地进行常规数据处理。
这种借助于非面向对象技术的做法有时也会带来不便,比如引用类型数据均继承了Object类的特性,要转换为String类型(经常有这种需要)时只要简单调用Object类中定义的toString()即可,而基本数据类型转换为String类型则要麻烦得多。为解决此类问题,Java语言引入了封装类的概念,在JDK中针对各种基本数据类型分别定义相应的引用类型,并称之为包装类(Wrapper Classes)。
下表描述了基本数据类型及对应的包装类
基本数据类型 |
对应的包装类 |
boolean |
Boolean |
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
char |
Character |
float |
Float |
double |
Double |
每个包装类的对象可以封装一个相应的基本类型的数据,并提供了其它一些有用的功能。包装类对象一经创建,其内容(所封装的基本类型数据值)不可改变。
例,包装类用法程序:Wrapper.java
public class Wrapper{
public static void main(String args[]){
int i = 500;
Integer t = new Integer(i);
int j = t.intValue(); // j = 500
String s = t.toString(); // s = "500"
System.out.println(t);
Integer t1 = new Integer(500);
System.out.println(t.equals(t1));
}
}
程序运行结果为:
500
true
包装类一个常用的功能就是把字符串类型的数据造型成为对应的基本数据类型,如下示例:
String str = "123";
int a = Integer.parseInt(str);
更过的功能还请查看JDK文档。
八、类型转换
在赋值的信息可能丢失的地方,编译器需要程序员用类型转换(type cast)的方法确认赋值。Java中的类型转换分成:强制类型转换、自动升级类型转换和后面将会学习到的向上造型。
1.强制类型转换
把某种类型强制转换成另外一种类型就叫做强制类型转换。
例如,可以将一个long值“挤压”到一个int变量中。显式转型做法如下:
long bigValue = 99L;
int squashed = (int)(bigValue);
在上述程序中,期待的目标类型被放置在圆括号中,并被当作表达式的前缀,该表达式必须被更改。一般来讲,建议用圆括号将需要转型的全部表达式封闭。否则,转型操作的优先级可能引起问题。
注意:强制类型转换只能用在原本就是某个类型,但是被表示成了另外一种类型的时候,可以把它强制转换回来。强制转换并不能在任意的类型间进行转换。
比如上面的例子:99这个数本来就是一个int的数,但是它通过在后面添加L来表示成了一个long型的值,所以它才能够通过强制转换来转换回int类型。
2.升级和表达式的类型转换
当没有信息丢失时,变量可被自动升级为一个较长的形式(如:int至long的升级)
long bigval = 6; // 6 是int 类型, OK
int smallval = 99L; // 99L 是 long 型, 非法
double z = 12.414F; // 12.414F 是 float型, OK
float z1 = 12.414; // 12.414 是 double型, 非法
一般来讲,如果变量类型至少和表达式类型一样大(位数相同),则你可认为表达式是赋值兼容的。
3.表达式的升级类型转换
对 + 运算符来说,当两个操作数是原始数据类型时,其结果至少有一个int,并且有一个通过提升操作数到结果类型,或通过提升结果至一个较宽类型操作数而计算的值,这可能会导致溢出或精度丢失。例如:
short a,b,c;
a=1;
b=2;
c= a+b;
上述程序会出错是因为在执行“+”操作前,a和b会从short提升至int,两个int相加的结果也是int,然后把一个int的值赋值给c,但是c是short型的,所以出错。如果c被声明为一个int,或按如下操作进行类型转换:
c = (short)(a+b);
则上述代码将会成功通过。
尤其在四则运算表达式里面,如果不强制进行类型转换,那么运算最后的结果就是精度最高的那个操作数决定的。比如:
3*5.0的结果就是double型的,应该定义成为:double a = 3 * 5.0;
4.自动包装(装箱)和解包(拆箱)
自动包装:就是把基础数据类型自动封装并转换成对应的包装类的对象。
自动解包:就是把包装类的对象自动解包并转换成对应的基础数据类型。
示例如下:
public class Test {
public static void main(String args[]) {
Integer a1 = 5;//自动包装
int a2 = new Integer(5);//自动解包
System.out.println("a1="+a1+",a2="+a2);
}
}
运行结果:a1=5,a2=5
九、Java类的基本运行顺序
作为程序员,应该对自己写的程序具备充分的掌控能力,应该清楚程序的基本运行过程,否则糊里糊涂的,不利于对程序的理解和控制,也不利于技术上的发展。
我们以下面的类来说明一个基本的Java类的运行顺序:
第1行 public class Test {
第2行 private String name = "Java私塾";
第3行 private int age = 2;
第4行 public Test(){
第5行 age = 1000;//期望能到1000年,呵呵
第6行 }
第7行 public static void main(String[] args) {
第8行 Test t = new Test();
第9行 System.out.println(t.name+"的年龄是"+t.age+"年");
第10行 }
第11行 }
运行的基本顺序是:
(1):先运行到第7行,这是程序的入口
(2):然后运行到第8行,这里要new一个Test,就要调用Test的构造方法
(3):就运行到第4行,注意:可能很多人觉得接下来就应该运行第5行了,错!初始化一个类,必须先初始化它的属性
(4):因此运行到第2行,然后是第3行
(5):属性初始化完过后,才回到构造方法,执行里面的代码,也就是第5行
(6):然后是第6行,表示new一个Test实例完成
(7):然后回到main方法中执行第9行
(8):然后是第10行
运行的结果是:Java私塾的年龄是1000年
说明:这里只是说明一个基本的运行过程,没有考虑更多复杂的情况。
作业
1.当你试图编译和执行下面的程序时会发生什么?
class Mystery{
String s;
public static void main(String[] args){
Mystery m=new Mystery();
m.go();
}
void Mystery(){
s="constructor";
}
void go(){
System.out.println(s);
}
}
选择下面的正确答案:
A 编译不通过
B 编译通过但运行时产生异常
C 代码运行但屏幕上看不到任何东西
D 代码运行,屏幕上看到 constructor
E 代码运行,屏幕上看到 null
以下是编程题:
1.编写一个名为MyPoint类,其中含有int类型的x 和y 属性,并写出有参和无参的两个构造方法,以及getX和setX、getY 和setY 方法,再重写toString方法用来显示对象的x、y的值,如显示(1,2),然后进行调试。
2.将上面的MyPoint类对象x和y属性的值由命令行输入,根据命令行参数个数,若不带任何命令行参数,则显示(0,0);若传一个参数,则打印(此参数值,0);若传两个参数,则打印(第一个参数值,第二个参数值)。
3.设计一个银行帐户类(Account),具有户名(accountName)、帐号(accountNo)、余额(balance)等属性,以及存款(deposit)、取款(withdraw)等方法,并对此类进行测试。
4.设计个Circle类,其属性为圆心点(类型为前面设计的类MyPoint)和半径,并为此类编写以下三个方法:
一是计算圆面积的calArea()方法;
二是计算周长的calLength();
三是boolean inCircle(MyPoint mp)方法,功能是测试作为参数的某个点是否在当前对象圆内(圆内,包括圆上返回true;在圆外,返回false)。
5.创建一个桌子Table类,该类中与桌子名称、重量、桌面宽度、长度和桌子高度属性,
以及以下几个方法。
1) area(): 计算桌面的面积。
2) display(): 在屏幕上输出所有成员变量的值。
3) changeWeight(int w): 改变桌子重量。
然后在main放中实现创建一个桌子对象,计算桌面的面积,改变桌子重量,并在屏幕上输出所有桌子属性的值。
上面任务完成后,再为table类添加几个构造方法,并在main方法中调用不同的构造方法,创建几张桌子。然后进行调试。
6.编写一个MyArray 类,其中添加一个属性为整型数组,再添加一个构造方法对数组赋初值,以及为该类添加数组求和方法,返回求和得到的值。最后写一个测试类利用MyArray类计算数组的求和值并输出。
Java私塾跟我学系列——JAVA篇 网址:http://www.javass.cn 电话:010-68434236
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/26660100/viewspace-715589/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/26660100/viewspace-715589/