应广大编程爱好者的需求,现本版推出[JAVA初学教程进阶],希望对初学者有一些帮助.
另外:关于JAVA运行环境的配置,在此不赘述,请大家参考本版精华.
注:本版所贴出所有学习内容均为CU所有,如转载,请注明来自ChinaUnix_JAVA专栏,谢谢合作.
鉴于时间缘故,本贴在发表过程中难免有疏漏/错误之处,敬请广大网友批评指正.
本贴参考资料如下:1>;Thinking in Java(3rd)
2>;JAVA大全(电子工业出版社)
第一次:Java的语言基础
Java语言是目前最受欢迎的程序语言之一,与传统的程序语言相比(比如C、C++),Java语言简单易学,使用它编程时间短、功能强,人们接受起来也更快、更简便。Java主要具有以下三个特点:
第一, 跨平台,不管Windows还是Unix或者其他平台,它都可以使用;
第二, 面向对象;
第三, 动态域的控制。
1.1 初识Java语言
像其它编程语言一样,Java编程语言也被用来创建应用程序。那,首先我们还是从一个最普遍的小应用程序范例来切入——在屏幕上显示字串“Hello World!”。下列代码给出了这个Java应用程序。
1.1.1 HelloWorldApp
1.//
2.// Sample HelloWorld application
3.//
4.public class HelloWorldApp{
5.public static void main (String args[]) {
6.System.out.println ("Hello World!");
7.}
8.}
以上程序行是在你的屏幕上打印“Hello World!”所需的最少组件。接下来,我们就针对上面给出的这段程序来简单解释一下,并且来实际编译、运行它。
1.1.2 描述HelloWorldApp
第1-3行
程序中的1-3行是注释行
1 //
2 // Sample HelloWorld application
3 //
第4行
第4行声明类名为HelloWorldApp。类名(Classname)是在源文件中指明的,它可在与源代码相同的目录上创建一个classname.class文件。在本例题中,编译器创建了一个称为HelloWorldApp.class的文件, 它包含了公共类HelloWorldApp的编译代码。
4 public class HelloWorldApp{
第5行
第5行是程序执行的起始点。Java技术解释器必须发现这一严格定义的点,否则将拒绝运行程序。
其它程序语言(特别是C和C++)也采用main ()声明作为程序执行的起始点。此声明的不同部分将在本课程的后几部分介绍。
如果在程序的命令行中给出了任何自变量,它们将被传递给main()方法中被称作args的String数组。在本例题中,未使用自变量。
5 public static void main (String args[]) {
- public-方法main()可被任何程序访问,包括Java技术解释器。
- static- 是一个告知编译器main()是用于类HelloWorldApp中的函数的关键字。为使main()在程序做其它事之前就开始运行,这一关键字是必要的。
- void- 表明main()不返回任何信息。这一点是重要的,因为Java编程语言要进行谨慎的类型检查,包括检查调用的方法确实返回了这些方法所声明的类型。
- String args [] - 是一个String数组的声明,它将包含位于类名之后的命令行中的自变量。
java HelloWorldApp args[0]args[1]····
第6行
第6行声明如何使用类名、对象名和方法调用。它使用由System类的out成员引用的PrintStreamout对象的println()方法,将字串“Hello World!”打印到标准输出上。
6 System.out.println (“Hello World!”);
在这个例子中,println()方法被输入了一个字串自变量并将其写在了标准输出流上。
第7-8行
本程序的7-8行分别是方法main()和类HelloWorldApp的下括号。
7 }
8 }
1.1.3 编译并运行HelloWorldApp
编译
当你创建了HelloWorldApp.java源文件后,用下列程序行进行编译:
c:/student/javac HelloWorldApp.java
如果编译器未返回任何提示信息,新文件HelloWorldApp.class则被存储在与源文件相同的目录中,除非另有指定。
运行
为运行你的HelloWorldApp应用程序,需使用Java解释器和位于bin目录下的java:
c:/student/ java HelloWorldApp
Hello World!
1.1.4 严格的约定
通过上面实际编写、编译和运行这个简单的例子,我们会发现Java是一种限制很严格的语言。我们在编写Java程序的时候,一定要严格遵从它的约定,以后你会发现这些约定对维护和组织程序,使你养成一种优良的编程风格都很有帮助。
->; 源文件的类型
在Java中,一个源程序文件被称为一个编译单元。它是一个包含一个或多个类定义的文本文件。Java编译器要求源程序文件必须使用.java文件扩展名。这里可以注意到这个文件的扩展名是4个字符,所以,你的操作系统必须要有支持长文件名的能力。
->; 源文件的命名
如果.java文件包括一个公共类,那么它必须使用与那个公共类相同的文件名。例如在前例中的类的定义是
public class HelloWorldapp
源文件名则必须是HelloWorldapp.java
->; 类的数量
在源文件中,可以定义多个类,但每次只能定义一个公共类。
->; 标识符
Java中使用的标识符可以是大写和小写字母、数字、下划线(_)、美元符号($)的任意组合。以下是一些有效的标识符:
Year99 count this_is_ok $some
以下是一些无效的标识符:
99year hello# not/ok
这里需要提醒大家注意的是,Java是一种大小写敏感的语言,所以大家在书写代码的时候一定要注意大小写的运用。
1.2 结构与构件
现在,我们从一个典型的Java源文件来了解一下Java程序涉及到的一些重要的构件,以及这些构件的结构。
1 package trades;
2 import java.util.Vector;
3 public class Quote extends Thread implements Tradable {
4 // Code goes in here
5 }
那我们从上面的例子中可以发现源文件的结构主要包含三个要素:
->; 一个包声明(可选)
程序的第1行就是声明了一个包(package),把该程序放在这个包中间。
->; 任意数量的包引入语句(可选)
程序的第2行是要求该程序引入系统提供的一个包java.util.Vector。
->; 任意数量的类的声明
程序的3-5行则是声明了一个public的类Quote,它是从它的超类Thread扩展得到,并且实现Tradable的接口。
该三要素必须以上述顺序出现。即,任何包引入语句出现在所有类定义之前;如果使用包声明,则包声明必须出现在类和引入语句之前。
结合上面的例子,我们接下来简单的讨论一下涉及到的这些构件,包package、包引入import、类class、方法method和变量variable。
1.2.1 包package
包实际上就是一个组织类class和接口interface的集合。通过包的这种机制,来把类名空间划分成更多易于管理的块。这样做其主要目的就是可以避免类名的冲突,在不同的包里的类,其名字相同也不会发生冲突。包的主要功能如下:
->; 依照功能来组织和划分类。例如:java.awt包含了构成抽象窗口工具包(AWT)的类,这个包被用来构建和管理应用程序的图形用户界面。
->; 有助于定义变量和方法的访问控制。
->; 通过这种包和包引入的机制,也可以保持程序的相对独立性,有利于模块化程序设计的实现。
声明包的定义语句必须放在源文件的顶端。如果你没有显式的声明一个包,那么Java会把你的类放入缺省包(default package)里面,这个包没有名字。
Java使用文件系统目录来存储包,目录名必须和包严格的匹配。另外,这里还要简单谈论一下类路径(CLASSPATH)环境变量的问题。Java编译器使用参考的包层次的根是由类路径决定的。例如,你可以在DOS系统环境下使用如下的命令来指定类路径:
set CLASSPATH=C:/Mysource;C:/Mypackages
如果我们在源文件类的声明前有以下语句:
Package trades.quotes
那么,这个源文件就放在下面这个目录里面:
c:/Mypackages/trades/quotes
最后,需要提醒的是,如果你定义的类已经指定到了某个包中,那么在运行的时候你也需要指定相应的包名和类名一起使用。
同学们可以结合实验二来加深对包概念的理解。
1.2.2 类class
类是面向程序设计中最小的抽象单元,它就像是一个数据和代码的容器。它也是Java里面最重要的一个要件。它通过实例化转变为对象,在实例化的时候自动调用构造函数来初始化对象。
对于类的声明比较复杂,它可以使用很多的修饰符,我们以下面的例子来简单谈论一下这些修饰符。
public abstract class Myclass extends Parent implements A,B {}
首先我们看到的public修饰符,是定义的类的访问控制属性,这个我们在下一章会有详尽的介绍。
然后我们看到的abstract修饰符是表示该类是个抽象类,需要其他类来扩展。这里再介绍一个final修饰符,它与abstract修饰符的意义正好相反,它表示该类不能扩展。所以abstract和final不能在声明类的时候同时使用。
在Java中所有的类都扩展一个超类,如果我们没有指定一个超类,那么系统会把Object类作为它要扩展的超类,Object类就是所有类的超类。这里需要注意的是,在C++中我们使用基类(base class)和继承类(derived class)的称谓,而在Java中我们一般称为超类(super class)和子类(subclass)。
另外,需要提一下的是一个特殊的关键字this,它用来引用正在被调用方法的所属当前对象。
1.2.3 接口interface
接口是一个非功能性、抽象的类,它包含常量和没有功能性的方法声明。也就是说,用interface,你可以指定一个类必须做什么,而不是规定它如何去做。接口在语句的构成上与类十分相似,但是它们缺少实例变量,而且它们定义的方法是不含有方法体的。
接口中声明的变量,一般是final和static型的,意思是它们的值不能通过实现类而改变,也就是说它们被看作是常量,而且它们还必须以常量值初始化。
对于接口来说,它们定义以后都是需要类来实现implement的。也就是说,使用具体的类来实现上面的“如何做”。一个类可以实现多个接口,一个接口也可以被多个类来实现。
另外,接口也可以扩展其他接口来实现继承。如:
interface MyInterface extends YourInterface, Runnable {}
1.2.4 方法method
方法声明的是被调用执行的代码,它可以使用传递的参数并且返回一定类型的返回值。对于方法的修饰符也比较复杂和繁多。我们仍然以一个例子来简单谈论以下其中的一些修饰符。
public static final synchronized double getPrice ( final int itemCode, Color itemColor ) throw Exception { /* code here */}
首先,Public是定义的方法的访问控制属性,这个我们在下一章会有详细介绍。
然后,static、final和synchronized是特殊的修饰符。同样需要注意的是其中final和abstract修饰符不能同时使用的。
最后,我们可以看到该方法getPrice返回一个double数据类型的返回值,而且里面定义了两个参数(itemCode和itemColor)。
另外,我们还注意它通过throw修饰符,表示它会抛出一个Exception的异常,关于异常我们在后面的章节会介绍到。
接下来我们会介绍两种比较特殊的方法。
->; Main方法
Main()方法是Java程序的入口起点,它和其他方法的定义类似,只是它包含一个字符串数组用来传递命令行方式执行程序时所跟的参数。如下的例子它把命令行方式所跟的参数全部显示出来:
public class MainTest {
public static void main ( String [] args ) {
for ( int i = 0 ; i < args.lenth; i++ ) {
System.out.println(“Argument ” + i + “: ” + args[i] );
}
}
}
执行如下:
c:/java project/Main>; java MainTest Philip K Dick
Argumet 0: Philip
Argumet 1: K
Argumet 2: Dick
这里需要注意的是,如果Main()方法没有使用static修饰符,那么编译不会出错,但是如果你试图执行该程序将会报错,提示main()方法不存在。这是因为你如果这样使用命令行的形式直接执行该程序,MainTest类并没有实例化,所以其main()方法也不会存在,而使用static修饰符则表示该方法是静态的,不需要实例化即可使用。
->; 构造函数Constructor Method
当类被实例化的时候,第一个被调用的方法就是构造函数。构造函数的主要作用就是初始化变量。如果没有定义构造函数,那么Java会使用其超类的默认构造函数。
构造函数与其他方法相比,主要具有以下的特点:
->; 构造函数的名字和其类名相同。
->; 没有返回值。
->; 构造函数不能像其他超类的方法那样被继承。
->; 不能使用final、abstract、synchronized、native或者static修饰符。
有时候,我们在编写构造函数的时候,可能需要首先调用其超类的构造函数,这里我们使用super的关键字,实际上它同this关键字的作用类似,只是它指的是其超类。如下的例子:
class DataServer extends Server {
public String serverName;
public DataServer ( int startCode ) {
super ( startCode );
serverName = “Customer Service”;
}
}
需要注意的是,调用超类构造函数super()的语句必须放在其构造函数定义的前面,否则编译器会报错。
1.2.5 变量variable
变量是Java程序的一个基本存储单元。变量由一个标识符、类型及一个可选初始值的组合定义。此外,如同定义方法一样,它也有各种的修饰符。比如访问控制属性的修饰符public、private、protected和final、static等。这里需要注意的是,变量不能使用synchronized、abstract和native修饰符。
在定义变量的时候需要指明其类型,除了常用的基本类型(比如int型、Boolean型等)以外,也可以使用对象类型。
1.2.6 引入语句import
使用import引入语句是允许你选择某个包中的某个类或者所有类,使之能在你当前的代码中能方便的使用。例如:
import java.util.Dictionary; //引入java.util包中的Dictionary类
import javax.swing.*; //引入javax.swing包中的所有的类
当然如果你不使用引入语句也可以使用其他包中的其他的类,只要你指定引用对象的全名。比如:
java.util.Date now = new java.util.Date();
另外,java.lang包是会被自动引入到源程序中的。
1.3 关键字和标识符
Java语言一共使用了48个保留关键字,他们主要可以分为如下几类。
->; 访问控制
private , protected , public
->; 类、方法和变量修饰符
abstract , class , extends , final , implements , interface , native , new , static , strictfp , synchronized , transient , volatile
->; 程序控制语句
break , case , continue , default , do , else , for , if , instanceof , return , switch , while
->; 错误处理
catch , finally , throw , throws , try
->; 包相关
import , package
->; 基本类型
boolean , byte , char , double , float , int , long , short
->; 变量引用
super , this , void
->; 未使用的关键字
const , goto
这些关键字的具体意义可以参考语法书,这里就不再详细阐述了。
另外,除了这48个关键字以外,还有3个语法保留字,即null、true和false。
1.4 变量的初始化
变量从声明的位置来看可以分为两种类型,一是实例变量instance variable,即声明在类一级;另一种是局部变量local variable,它声明在方法一级。这两类变量除了声明的位置不同,它们最主要的区别在于是否需要初始化的问题。下面我们具体来讨论一下这个问题。
->; 实例变量
类的成员就是定义在类一级的变量。它们实际上是可以不需要初始化的,系统一般会自动给它们赋一个默认值。
->; 基本类型
对于8种基本类型来说,Boolean型默认赋值false,char型会默认为Unicode字符集的/u0000,而其余几种类型都默认为0。
->; 对象类型
对于对象类型,系统不会赋予任何默认值,但会表示为null。
->; 数组类型
数组类型于对象类型类似,当你不初始化它的时候,它也是等于null。但如果你初始化它,则有些不同,它与基本类型相似会默认为0。
->; 局部变量
对于局部变量,一般来说它必须初始化。因为无论是基本类型还是对象类型,系统都不会自动赋于任何默认值,所以你必须指定一定的值。当然,如果你定义了局部变量而不去使用它,编译也是可以通过的。
第二次:修饰符
2.1 访问控制
封装将数据和处理数据的代码连接起来。同时,封装也提供了另外一个重要属性:访问控制。通过封装你可以控制程序的某个部分可以访问类的成员,防止对象的滥用,从而保护对象中数据的完整性。对于所有的面向对象的语言,比如C++,访问控制都是一个很重要的方面。由于Java语言使用了包的概念,使它的访问控制相对来说更复杂一些。我们把控制访问控制权限的修饰符主要分为两类,类和它的方法及变量,下面我们分别简单介绍。
-类的访问控制
->; Default:当类不使用任何访问控制修饰符时,即采用的默认的访问控制权限。它允许同一个包内的类访问,而对于它所在包以外的类则不能访问。
->; Public:允许任何包中的任何类访问,对Java里面的所有类开放。
-方法和变量的访问控制
->; Public:所有类均可以访问。
->; Private:只能被它所在的类中的成员访问,使该定义的成员对外在的类不可见。
->; Protected:可以被同一个包的类访问,另外其所有子类也可以访问。
->; Default:当成员不使用任何访问控制修饰符时,即采用默认的访问控制权限。它和Protected类似,唯一的区别在于子类访问权限,它仅允许同一个包的子类访问,而其他包中的子类则不可以访问。
2.2 其他修饰符
除了访问控制修饰符,Java还有其他繁多的修饰符来声明类、方法和变量,下面分别针对所修饰的对象来简单介绍一下主要的修饰符。
-类修饰符
->; final:用来指定该类不能被其他类扩展,从而阻止继承。
->; abstract:表示该类是不允许被实例化的类,也就是说该类需要被扩展继承。被这样声明的类也称为抽象类。
显而易见,final和abstract不能同时使用。
-方法修饰符
->; abstract:被声明的方法称为抽象方法,不含任何代码,需要其继承的子类的相应方法覆盖重载。这里需要注意的是被声明有abstract方法的类必须被声明为abstract。
->; final:声明的方法不允许被覆盖重载。
->; static:声明的方法被成为类方法,不依赖于任何的对象,不需要实例化对象即可直接使用类名来调用该方法。注意的是在该方法体内不可访问实例变量。
->; 变量修饰符
->; static:被声明为static的变量实际可以看作就是全局变量,同样不需要实例化对象即可直接使用类名来引用之。
->; final:被声明的变量的内容不可以被修改,实际可以被看作是一个常量,类似于C或者C++中的const。
2.3 缺省构造函数
我们都知道当对象被实例化的时候,构造函数总是被调用。如果我们在定义类的时候不指定一个构造函数,Java会自行创建一个不带参数的缺省构造函数。而如果我们定义有了一个构造函数,则Java不会再创建缺省构造函数。
更值得注意的是,如果子类的超类不含有不带参数的构造函数,那么子类在使用缺省构造函数就会出错,Java不会为子类创建不带参数的缺省构造函数。因此,我们在使用缺省构造函数的时候要比较小心。我们可以看如下的例子:
class Fruit {
public Fruit ( String color ) {
System.out.print ( “color = ” + color ) ;
}
}
class Apple extends Fruit {
public static void main ( String [ ] args ) {
Apple m = new Apple () ;
}
}
运行结果出错:
Fruit.java:6: No constructor matching Fruit ( ) found in class Fruit .
Class Apple extends Fruit {
1 error
2.4 合法的返回类型
由于在方法调用的时候,方法返回的类型有可能与实际声明的类型不同,因此我们需要关心什么样的返回类型才是合法的。实际上,系统采用了隐式的类型转换来处理类型的返回。以下几种情况的是合法的:
->; 如果声明的是浮点类型,那么可返回整型类型。
->; 如果声明的是整型类型,那么只要返回的整型类型范围小于或等于声明的类型,返回合法。
->; 如果声明的是对象类型,那么只要返回的是该对象类型,或者是其子类的对象类型,合法。
第三次:JAVA运算符
3 运算符
同大多数的编程语言一样,Java语言也包含了许多的运算符。如果大家学习过C或者C++,会发现下面介绍的各种Java的运算符都与之类似。
3.1.1 赋值运算符 =
这是任何编程语言的最基本的运算符,它用来给变量指定一个值。对于基本类型来说,赋值都便于理解,将新的值赋给变量并保存在变量中供使用。但对于对象类型来说,这里就有一点区别,特别需要提醒大家注意。
对象类型并不是把实际的值(这里是实例)赋给了对象类型的变量,而是赋给的一个参考指针。这样,源对象类型的变量和新的这个变量实际上是指向的同一个实例,如果使用其中一个让实例改变,那么相应的另一个所指向的实例也会改变。这里我们可以借用C里面的指针的概念来方便理解,但实际上Java是不具有指针的概念和定义的。
我们通过下面的例子可以进一步来理解这个概念。
import java.awt.Dimension;
class ReferenceTest {
Dimension a = new Dimension ( 5,10 );
System.out.println (“a.height = ” + a.height ) ;
Dimension b = a ;
b.height = 30 ;
System.out,println (“a.height = ” + a.height + “after change to b ”);
}
}
运行结果:
c:/java Project/Reference>;java ReferenceTest
a.height = 10
a. height = 30 afer change to b
另外,赋值运算符还可以和其他的运算符,联合组成新的赋值符。如*=、/=、+=、-=等等,这于C或者C++类似。
3.1.2 比较运算符
比较运算符是用来对相同数据类型的变量进行大小或者是否相等、相同的比较,返回的是Boolean类型的值。因此也就大概分为两类。
n >;、>;=、<、<=
这是比较变量的大小关系,与我们学过的任何编程语言相同,就不再介绍了。
n = = 、! =
这是比较变量是否相等或相同。这对于平常的比较基本类型的变量容易理解,只是我们要强调一下对对象类型的比较。与我们前面介绍的赋值运算符类似的是,它也是进行的对其参考指针的比较,而并不是比较两个内容上的差别。我们可以借助下面的例子来理解。
import java.awt.Button
class CompareRefernce {
public static void main ( String [ ] args ) {
Button a = new Button ( “Exit”);
Button b = new Button ( “Exit”);
Button c = a;
System.out.println ( “Is refernce a = = b ? ” + ( a = = b) ) ;
System.out.println ( “Is refernce a = = c ? ” + ( a = = c) ) ;
}
}
运行结果:
Is refernce a = = b ? false
Is refernce a = = c ? true
3.1.3 instanceof运算符
这个是Java语言特殊的一个运算符,它是用来测试其对象是否属于某类或其超类。但是这里需要提醒大家的是,如果你使用instanceof来比较不是一个继承关系树上的类,Java能够编译通过,但运行的时候会报错。另外,你可以对null对象使用这个运算符,只是无论对于什么类测试的结果都是false。
3.1.4 算术运算符
加+、减-、乘*、除/和取模%运算,这与其他的编程语言类似,不再详述。
3.1.5 自增++、自减--运算符
Java的自增和自减运算符,与C语言类似,同样需要注意的是其放置的位置不同,可能的结果也不同。如果放置在变量的前面,表示先自增(减)再参与下步运算,而如果放置在后面则表示先参与运算再自增(减)。如下的例子说明了自增运算符的使用:
class IncDec{
public static void main ( String [ ] args ) {
int a = 1;
int b = 1;
int c;
int d;
c = ++b;
d = a++;
c++;
System.out.println ( “a = ” + a );
System.out.println ( “b = ” + b );
System.out.println ( “c = ” + c );
System.out.println ( “d = ” + d );
}
}
运行结果:
a = 2
b = 2
c = 3
d = 1
3.1.6 字符串连接运算符 +
Java语言与C语言类似,也使用+作为连接字符串的运算符,这实际是对String类重载了+运算符。
3.1.7 位运算符
包括位移运算和位逻辑运算,这也与C语言相似。
->; 位移运算
>;>;右移、<<左移、>;>;>;无符号右移。
->; 位逻辑运算
&与、|或、^异或、~非运算,这于其他的编程语言类似,不再详述。
3.1.8 逻辑运算符
与&&、或||,这于其他的编程语言类似,不再详述。只是需要提醒大家不要把它们和位逻辑运算符混淆,这也是初学者最容易犯的错误。
3.1.9 条件运算符 ?:
这与C语言完全相同,具体不再解释。
3.1.10 类型转换
我们在编写程序的时候经常需要对变量的类型进行转换,Java语言与其他的编程语言类似,也提供两种类型转换方式,即显式转换和隐式转换。转换的对象可分为两类,一是基本类型,二是对象类型。
这里需要掌握这样一个要点。对于基本类型来说,凡是大转小(以类型的宽度考虑)需要使用显式转换,也就是需要在变量前面强制给出需要转换成的类型。而对小转大来说,系统会自行进行隐式转换。
对于对象类型来说,也与之类似。凡是超类转子类则需要使用显式强制转换,而子类转超类系统可自行进行隐式转换。另外还需要注意的一点是,对于不在一个继承关系树上的类要进行强制转换,Java编译可通过,但实际运行会出错。
3.2 equals()方法
equals()方法实际与= =运算符作用相同,也是用来比较相同类型的两个变量是否相同或相等。只是有点区别的是,对于String类来说,它重载equals()方法,使用它不是比较两个参考指针的区别,而是实际对所指向的具体内容进行比较,这也满足了平时我们对比较字符串的实际需求。当然,对其他类来说,你也可以重载equals()方法,使其满足你的实际需要,来比较两个对象类型的变量。
3.3 优先级
与其他编程语言类似的,Java语言的运算符同样涉及到优先级别的问题,书上130页从高到低给出了所有运算符的优先级。建议大家,如果对某些运算符之间的优先级不是很清楚的时候,可以使用()来改变它们的优先级关系。
3.4 方法的参数传递
最后,简单讨论一下方法的传递的问题。Java语言的参数传递类型主要可以分为两种,值传递和引用传递。借助C语言的概念,我们知道,第一种就是把参数值直接复制成方法体的参数,这样对方法体中的参数的改变不会影响到调用它的参数。而对于第二种,参数的引用(或者说是个指针)被传递给了方法体的参数,该引用用来访问调用中指定的实际参数。这样,对方法体参数的改变将会影响到调用方法体的参数。
由于没有指针的概念,Java的参数传递相对比较简单。对于一般的基本类型来说,都采用的是值传递;而对于对象类型则是使用的引用传递。
第四次:JAVA程序控制语句
4.程序控制语句
我们在使用任何编程语言都使用过程序控制语句,Java语言的程序控制语句基本与C语言完全相同,它主要分为以下几种:选择、循环和跳转。
4.1 选择语句
4.1.1 if语句
if语句是最基本的条件判断语句,用于根据条件来控制程序的执行路径。其完整格式如下:
if (condition) statement1;
else statement2;
if和else的对象statement可以是单个语句,也可以是个程序块。else子句是可选的。
另外if-else可以嵌套。所以这里需要提醒大家注意的是,如果你没有使用{}来保证每个程序块是具体所属那个if或者else子句,那么一定要注意if和else的匹配关系。一个else子句总是对应着它的同一个块中的最近的if语句,而且该语句没有于其他的else语句相关联。
4.1.2 switch语句
switch语句是Java的多路分支语句。它提供了一种基于一个表达式的值来使程序执行不同部分的简单方法。它比使用一系列if-else-if语句效率显得更高,也更方便。其通用格式如下:
switch ( expression ) {
case value1:
// statement sequence
break;
case value2:
// statement sequence
break;
.
.
.
case valueN:
// statement sequence
break;
default:
// default statement sequence
}
表达式expression必须为byte、short、int或char类型。每个case子句后的值必须是于表达式类型兼容的特定的一个常量(注意必须是常量,而不是变量)。Default子句是可选的。重复的default子句或者重复的case值是不允许的。
4.2 循环语句
4.2.1 while循环
while语句是Java最基本的循环语句。当它的控制表达式为真的时候,while语句重复执行单个语句或语句块。通用格式如下:
while ( condition ) {
// body of loop
}
注意的是,如果循环体只有单个语句,可以不使用{}。后面的do-while、for也类似。
同学们可以结合实验一来加深对while循环的理解。
4.2.2 do-while循环
与while循环类似,只是循环控制表达式是在循环体的尾部进行测试。这意味着即使表达式为假,循环体也至少要被执行一次。通用格式如下:
do {
// body of loop
} while ( condition ) ;
4.2.3 for循环
for循环是一个功能强大而且形式灵活的结构。通用格式如下:
for ( initialization ; condition ; iteration ) {
// body
}
它与C语言完全相同,按它分为的三段,for循环执行的过程也可以看作是三步。首先循环启动时先执行初始化initialization部分,通常这里是设置循环控制变量值的一个表达式,作为控制循环的计数器。然后,计算条件condition的值,来判断是否满足该表达式,执行循环体。最后执行循环体的反复部分iteration,一般来说这里通常是增加或者减少循环控制变量的一个表达式。
需要注意的是,对于initialization和iteration两个部分而言,可以使用多个变量表达式,中间用逗号隔开。另外,for循环也可以不含任何的部分,这样做就是一个死循环。总之,对于for循环语句的使用相当的灵活,大家可以在具体的应用中感受到。
4.3 跳转语句
4.3.1 break语句
break语句有三种作用。首先,可以在switch语句中使用,用来终止一个子句序列,跳出switch语句。另外,它能用来退出一个循环,这种使用与C语言也完全一致。第三种,它能作为一种“先进”的goto语句来使用。这是Java的一个特点,我们特别讨论一下。
我们知道,对于一个比较优秀的语言来讲,都限定使用goto语句。Java语言虽然把goto作为了保留关键字,但并不使用这个语句。它提供了break语句来扩展的实现它的一些功能。通过使用带有标签的break语句,来指定执行从何处重新开始。标签break语句的通用格式如下:
break label ;
这里,标签label是标识代码块的标签。当这种形式的break执行时,控制被传递处指定的代码块。但需要注意的是,如果一个标签不在包围break的块中定义,你就不能break到该标签。一般来说,在实际应用中,带标签的break语句可以用来跳转到多重循环体之外。
4.3.2 continue语句
用法与C语言类似,用来强迫一个循环提早反复。也就是说,你可以使用continue来继续运行循环,但忽略这次重复剩余的循环体语句。
第五次:异常处理
同大多数的编程语言一样,Java语言也包含了许多的运算符。如果大家学习过C或者C++,会发现下面介绍的各种Java的运算符都与之类似。
5.1 try-catch-finally
首先我们介绍一下异常的一些基本概念。Java异常是一个描述在代码段中发生的异常(也就是出错)情况的对象。当异常情况发生,一个代表该异常的对象被创建并在导致该错误的方法中被引发throw。另外,你需要定义一段代码来处理这一异常,首先它需要捕获catch被引发的异常。
如果我们不提供任何异常处理程序,那么异常会被Java运行时系统的默认处理程序捕获。任何不是被你程序捕获的异常最终都会被该默认处理程序处理。默认处理程序会显示一个描述异常的字符串,打印异常发生处的堆栈轨迹并终止程序。
堆栈轨迹实际显示了导致错误产生的方法调用序列。Java在运行的时候会维护一个堆栈来保存程序运行调用的方法序列。实际上,异常发生以后就是按照这个堆栈的方法序列依次呈递给这些方法,如果这些方法里面有异常处理程序就可以捕获它加以处理,否则会一直呈递到系统默认的处理程序。
我们通过下面的例子能更容易理解这个调用方法堆栈和异常的呈递机制。
Class Exc {
static void subroutine () {
int d = 0 ;
int a = 10 / d ;
}
public static void main ( String [ ] args ) {
Exc.subroutine ();
}
}
运行结果:
java.lang.ArithmeticException: / by zero
at Exc.subroutine(Exc.java:4)
at Exc.main(Exc.java:7)
尽管由Java运行时系统提供的默认异常处理程序对程序的调试很有用,但通常还是需要自己来处理异常。因为,这样做一可以修正错误,二来可以防止程序的自动终止。
接下来,我们来看一下异常处理块的通用格式:
try {
// block of code to monitor for errors
}
catch ( ExceptionType1 exOb ) {
// exception handler for ExceptionType1
}
catch ( ExceptionType2 exOb ) {
// exception handler for ExceptionType2
}
// …
finally {
// block of code to be executed before try block ends
}
☆ try子句
用来监控可能产生错误的代码
☆ catch子句
用来捕获你指定的异常类型,并进行错误处理。它必须紧跟try块,并且可以定义多个catch块。
☆ finally子句
用来处理善后清理的工作,因为无论异常是否引发,它里面的代码都会执行,甚至你在try块中定义了return子句,它也一样会被执行。这是可选的。
5.2 Exception类
前面我们讨论的所有异常都来自于Exception类。
在Java编程语言中,异常类可以大致分为三种。Java.lang.Throwable类充当所有对象的父类。它有Error和Exception两个基本子类,如下图所示:
Throwable类不能使用,而使用子类异常中的一个来描述任何特殊异常。
☆ Error表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。
☆ RuntimeException表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。比如,如果数组索引扩展不超出数组界限,那么,ArrayIndexOutOfBoundsException异常从不会抛出。比如,这也适用于取消引用一个空值对象变量。因为一个正确设计和实现的程序从不出现这种异常,通常对它不做处理。这会导致一个运行时信息,应确保能采取措施更正问题,而不是将它藏到谁也不注意的地方。
☆ 其它异常表示一种运行时的困难,它通常由环境效果引起,可以进行处理。例子包括文件未找到或无效URL异常(用户打了一个错误的URL),如果用户误打了什么东西,两者都容易出现。这两者都可能因为用户错误而出现,这就鼓励程序员去处理它们。
在捕获异常的时候,一定要注意异常类型的匹配。只有catch块中定义的捕获异常的类型匹配引发的异常的类型或者其超类,才捕获并处理。所以,一定要注意你处理的异常类型的继承关系。另外,对于多个catch块来说,如果你定义的带捕获处理的异常有子类和相应的其超类,一定要注意放置的顺序,捕获超类的catch块一定要放置捕获其子类的catch块后面。
5.3 throws子句
如果一个方法可以导致一个异常但不处理它,它必须指定这种行为以使方法的调用者可以保护它们自己而不发生异常。你可以在方法声明中使用throws子句来实现这一目的。一个throws子句列举了一个方法可能引发的所有异常类型。这对于除了Error或者RuntimeException及它们的子类以外类型的所有异常是必要的。一个方法可以引发的所有其他类型的异常必须在throws子句中声明。
针对上面的情况,这里简单介绍一下异常的检查类型的概念。RuntimeException异常类及其子类就是非检查类型,Error类虽然不属于Exception类也是非检查类型的。对于非检查类型的异常,即使你在方法声明中没有指定它为可能引发的异常类型,它也会自动正确的被引发,系统保证它们的运行而不会出错。而除此以外的其他异常类型都是检查的,也就是如果你要引发它就必须在方法声明中指定,而且还必须使用catch块来捕获处理。
另外,对于throws子句还需要提一下的是其方法的重载问题。对于子类重载超类方法的这种覆盖重载override来说,要注意子类方法所引发的异常不能比超类的多,你引发的异常类型可以是超类引发异常类型的子类或者根本不引发任何异常,反正不可以超出其超类的范围。
5.4 引发和创建自定义异常
5.4.1 throw子句
前面我们都是获取的Java运行时系统引发的异常,然而,我们也可以在程序中用throw语句来引发明确的异常。Throw子句通用格式如下:
throw ThrowableInstance ;
ThrowableInstance一定是Throwable类类型或者其子类的一个对象,所以这里又涉及到了一个实例化异常类的问题。可以使用new来实例化异常类,需要注意的是,异常的有两个构造函数:一个没有参数,一个带有一个字符串参数。使用后者时,参数指定的是一个描述异常的字符串。如下:
throw new NullpointerException (“demo”) ;
5.4.2 创建自定义异常类
尽管Java内置异常可以处理大多数常见错误,你还是可以建立你自己的异常类型来满足你的特殊要求。建立的方法很简单,只要定义一个Exception 的一个子类就可以了。
第六次 垃圾回收机制
这里讨论Java的垃圾回收机制,也就是Java管理使用内存的机制。当对象在内存中创建以后,如果你的程序使用完毕了该对象,通常都应该回收这个对象所占用的内存空间。在C 和C++语言中,一般都靠程序员在代码中手工编写代码来处理这个过程。而Java语言则通过它的JVM(Java虚拟机)使用一种称作mark-sweep的垃圾回收机制来自动处理这一过程。
6.1 垃圾回收机制
内存空间是计算机最基本、最重要的资源。对内存空间的耗费最主要的就是创建对象。当我们不再需要使用创建的对象时,就需要释放分配给对象的内存,回收资源。Java使用一种称为“堆heap”的结构来管理内存,它使用垃圾回收机制来保证始终有足够的内存分配给创建的对象。这一内存管理机制通常是系统自动进行,不受程序员干预的。这也就减轻了程序员的工作负担。
在回收内存空间中,系统主要使用一种称作标记-清理(mark-sweep)的机制。首先,系统遍历所有对象的引用指针,如果发现有对象没有被引用,那么就把它标记为未使用。然后,系统把所有的凡是标记了未使用状态的对象进行清理回收。即使是对于那种两个未使用的对象互相引用的情况,系统通过的遍历算法也可以发现,所以这一机制是十分有效的。
另外需要注意的是,回收处理是周期性的运行,它由系统自行控制。它可能延迟任意长的时间来运行回收器,也可能在任意时候运行。所以这就可能在运行的时候出现一些问题,比如程序会暂停或者运行速度减慢。
针对这样的回收机制,我们在程序中可以显式的使用一些方法来告诉系统回收我们不需要的对象,提高回收执行的效率。比如,把不再需要使用的对象置为null,或者赋给其他对象类型变量,使其不引用指向原对象。
6.2 手工干预回收
对于回收机制,都是由系统自行完成,通常我们并不需要关心。但有时候也可以使用一些手段来手工干预内存空间的回收。
Java提供一个java.lang包中的Runtime类来做这样的工作。我们使用Runtime类的gc()方法来运行垃圾回收器,强制内存空间回收。需要注意的是使用这个方法只是给系统发出一个要求,而不是命令,所以并不能保证一定完成你指定的工作,达到你满意的效果。 另外还需要注意的是Runtime类没有构造函数,你需要使用其getRuntime()来实例化它。
有时候当撤消一个对象的时候,需要完成一些操作,来回收一些非Java的资源。对处理这样的情况,Java提供了一种被称为收尾(finalization)的机制,你可以采用与C语言类似的方式来手工回收一些资源。
要实现这一机制,只需要在你需要回收的类中定义finalize()方法即可。Java回收该类的一个对象时就会调用这个方法。在finalize()方法中你需要指定一个对象被撤消前必须执行的操作。
finalize()方法的通用格式如下:
protected void finalize ()
{
// finalization code here
}
第七次 重载
重载对于面向对象编程来说是一个难点,相对C++来说,Java这个方面较为简单。这里我们主要讨论方法的两种形式的重载和构造函数的重载。通过本次学习,大家会对Java面向对象编程的思想有更一步的理解。
7.1 方法的重载
通过方法重载可以实现多态,多态性是面向对象编程的一个重要的特点。Java语言中对方法的重载分为两类,我们在下面依次具体介绍。
7.1.1 Overload名称重载
指同一个类中的多个方法可以享用相同的名字,只要它们声明的参数不同。这种不同体现在或者参数的个数不同,或者其参数类型不同。编译器根据参数的个数和类型来决定当前所使用的方法。需要注意的是多个方法必须返回相同的数据类型,它们的不同只在于参数。
我们可以通过如下的例子来了解名称重载。
public class Mytest {
public void printNumbers ( int a, int b ) {
System.out.println(a);
System.out.println(b);
}
public void printNumbers ( int a, int b, int c ) {
System.out.println(a);
System.out.println(b);
System.out.println(c);
}
7.1.2 Override覆盖重载
在类层次结构中,如果子类中的一个方法与它超类中的方法用相同的方法名、返回类型和参数表,就称子类中的方法覆盖重载超类中的方法。从子类中调用重载方法时,它总是引用子类定义的方法,而超类中定义的方法将被隐藏。也就是说,通过覆盖重载,子类修改了从超类继承下来的行为。
需要注意的是方法覆盖仅载两个方法的名称和类型都相同时才发生。如果仅有名称相同,那两个方法只是覆盖。
另外使用方法覆盖还需要注意要保证以下几点:
☆ 覆盖方法的返回类型必须与它所覆盖的方法相同。
☆ 覆盖方法不能比它所覆盖的方法访问控制范围差。
☆ 覆盖方法不能比它所覆盖的方法抛出更多的异常。
☆ synchronized修饰符对超类和子类的作用是独立的。
我们可以通过如下的例子来了解覆盖重载。
Myparent.java
public class Myparent {
public void printClassName () {
System.out.println ( “Myparent” );
}
}
Mychild.java
public class Mychild {
public void printClassName () {
System.out.println ( “Mychild” );
}
}
7.2 构造函数的重载
构造函数和其他的方法一样也可以被重载,由于它的特殊性,所以它的重载也与其他的方法重载有些不同。主要包括以下几个方面。
☆ 使用this调用自身的其余构造函数实现构造函数的名称覆盖。如下的示例:
public class Myclass {
String title ;
//Calling constructor
public Myclass () {
this ( “Default Title ” );
}
//Called constructor
public Myclass ( String t ) {
title = t ;
}
}
☆ 使用super调用超类的构造函数。需要注意的是,如果不显式的使用super调用超类的构造函数,系统会调用超类的不带参数的缺省构造函数。另外要使用this()或者super()则必须把它放在其构造函数的首行。如下的示例:
public class MySuper {
//first constructor
public MySuper () {
System.out.println (“Super1”) ;
}
//second constructor
public MySuper ( String title ) {
System.out.println (“Super2”) ;
}
}
public class MySub extends MySuper {
//first constructor
public MySub () {
super(“the title”)
System.out.println (“Sub1”) ;
}
//second constructor
public MySub ( String title ) {
System.out.println (“Sub2”) ;
}
}
7.3 所属Is和拥有Has关系
这里主要是提醒大家明白了一个我们在平时程序设计中经常混淆的概念,即所属Is和拥有Has关系的概念和区别。我们需要注意的是只有对于所属的这种关系,我们才可以使用子类和超类的程序设计思想。
第八次 内部类
内部类就是定义在其他类、方法、甚至表达式中的类,它的特点与一般的类并没有什么大的不同。Java语言有四种类型的内部类,下面分别介绍。
8.1 静态Static内部类
静态内部类是定义在类内部的类,所以也称为嵌套类,并且使用static修饰符来声明。需要注意的是凡是内部类,其名字都不能和封装它的类名字相同。对于静态内部类来说,它只能访问其封装类中的静态成员(包括方法和变量)。
静态内部类示例如下:
package mypackage;
public class EnclosiongClass {
private static int staticvar = 0;
public int instancevar = 0 ;
public static class StaticInnerClass {
}
}
8.2 成员Member内部类
成员内部类与静态内部类相似,也是定义在类内部这一级,只是它不使用static修饰符来声明。所以它和类中的其他实例变量一样,都可以看作是其封装类中的成员。它可以访问其封装类中的任何成员。需要注意的是,在成员内部类中不能定义有static变量,但是可以使用局部变量。
由于成员内部类是非static的,所以在创建内部类的实例时,必须有一个其封装类的实例引用。可见下面的例子:
public class EnclosingClass {
private int instVar = 1;
public class MemberClass {
public void innerMethod {
// it is okay to do something with instVar here
}
}
public MemberClass createMember () {
return this.new MemberClass ();
}
}
这里是在封装类内部创建内部类的实例,实际这个this可以不显式使用,系统会自行隐式使用来引用封装类的实例。如果对于在封装类外部来创建内部类的实例,依然必须要有一个封装类的实例引用。其语法有两种,可分别见下面的两个示例:
EnclosingClass ec = new EnclosingClass ();
EnclosingClass.MemberClass mc = ec.new MemberClass();
或者
EnclosingClass.MemberClass mc = new EnclosingClass ().new MemberClass ();
另外还需要提一下的是this和super在内部类中使用所代表的意义,this是指的该内部类的实例引用,而super是指的其封装类的实例引用。所以大家在使用这两个关键字的时候一定要注意它所处的位置。
8.3 局部Local内部类
局部内部类是定义在方法体或更小的语句块中的类。它的使用如同方法体中的局部变量,所以你不可以为它声明访问控制修饰符。同成员内部类相同,也不可以含有static成员。这里需要注意的是,在局部内部类和后面将介绍的匿名内部类中可以引用方法中声明的变量,但这些变量必须是final的。
8.4 匿名Anonymous内部类
在某些时候,你可能需要定义一个类,但在程序中却从来不会定义它的对象,只是用来把这个类的对作为自变量直接传递给方法,这时就使用匿名类。如下的例子:
pickButton.AddActionListener ( new ActionListener ( )
{
//Code to define the class that implements
//the ActionListener interface
}
)
匿名类总是用来扩展一个现有的类,或者实现一个接口。它没有名字,所以也没有构造函数,并且也没有任何修饰符来声明它。
一般来说,匿名类经常用于AWT和Swing中的事件处理。
第九次 线程
Java语言中一个十分重要的特点就是支持多线程编程。多线程程序包含两条或两条以上并发运行的部分。程序中每个这样的部分都叫做一个线程thread,每个线程都有独立的执行路径。因此,多线程是多任务处理的一个特殊形式。
9.1 创建进程
通常,通过实例化一个Thread对象来创建一个线程,Java对此定义了两种方式。下面我们分别介绍一下。
☆ 实现Runnable接口
创建线程的最简单的方法是创建一个实现Runnable接口的类。创建过程大致分以下的几步。
首先,你通过实现Runnable接口的run()方法来创建每一个对象的线程。run()方法能够像主线程那样调用其他方法,引用其他类,声明变量。唯一不同的是,run()在程序中确定另一个并发的线程执行入口。当run()方法返回时,该线程即结束。
其次,你要在你创建的实现Runnable接口的类中,实例化一个Thread类的对象。
最后,实例化了Thread类创建了一个线程后,线程并没有运行。你要运行你创建的这个线程还要调用它的start()方法。实际上,start()执行的就是一个对run()的调用。
☆ 扩展Thread类
创建线程的另一个方法是创建一个新类来扩展继承Thread类,然后再创建这个类的实例。当这个子类继承Thread类时,它必须重载run()方法,这个run()方法就是新线程的入口。另外它同样也必须调用start()方法去启动新线程的执行。
9.2 线程的同步
当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一时刻仅被一个线程占用。这个过程就成为同步(synchronization)。
与其他语言不同的是,Java提供了一种语言上对同步的支持,极大的简化了管理线程同步的复杂性。有两种方法来同步化代码,两者都包括了synchronized关键字的运用,下面分别说明。
☆ 使用同步方法
在多线程的情况下,你如果有一个或多个方法操纵对象的内部状态,都必须使用synchronized关键字来声明方法为同步方法,防止状态出现竞争。一旦线程进入实例的同步方法,就没有其他线程可以进入相同实例的同步方法。
☆ 使用同步语句
当我们在类的定义中没有用到同步方法的时候,你可能也需要对一些操纵对象内部状态的代码同步化。这时你可以使用synchronized来声明一个同步语句块。
下面给出了synchronized语句的普通格式:
synchronized(object) {
// statement to be synchronized
}
9.3 线程间通信
对于多线程的管理,一般采用一种轮询的方式。轮询通常由重复监测条件的循环实现。一旦条件成立,就采用适当的动作。这种方式相对简单,但却很大程度上浪费了CPU的时间。Java语言则采用了另外一种机制,避免了轮询。它通过wait()、notify()和notifyAll()方法实现一个进程间通信机制。这些方法是在Object类中用final声明定义了的,所以所有的类都包含它们。它们的意义分别如下:
☆ wait():通知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并调用notify()把它唤醒。
☆ notify():恢复相同对象中第一个调用wait()的线程。
☆ notifyAll():恢复相同对象中所有调用wait()的线程。
这里需要提醒的是,这三个方法仅在synchronized方法中才能被调用。
9.4 死锁
这里,我们介绍一个死锁的概念。这是我们需要避免在多任务处理中出现的情况。
死锁发生在当两个线程对一对有依赖循环时。例如,假设一个线程进入了对象A的管程而另一个线程则进入了对象B的管程。如果A的线程试图调用Y的同步方法,它将被锁定。这时,如果B的线程希望调用A的一些同步方法,线程就会永远等待。系统陷入死锁的情况。因为为到达A,B必须释放自己的锁定以使第一个线程可以完成。
9.5 优先级问题
线程优先级被线程调度用来判断何时每个线程允许运行。理论上,优先级高的线程比优先级低的线程获得更多的CPU时间。但实际上,线程获得CPU时间通常由包括优先级在内的多个因素决定的。
我们可以使用setPriority()方法来设置线程的优先级,该方法是Thread类的成员。它的通常格式如下:
final void setPriority(int level)
level指定了对调用线程的新的优先级的设置。Level的值必须在MIN_PRIORITY 到MAX_PRIORITY之间。通常它们的值是1和10。如果要指定默认的优先级使用NORM_PRIORITY,通常值为5。
另外你还可以使用getPriority()方法来获得当前的优先级设置。