JAVA程序设计基础-第6版陈国君2006-学习笔记2

JAVA程序设计基础-第6版陈国君2006-学习笔记2

[TOC]

JAVA程序设计基础-第6版陈国君2006-学习笔记2

第六章 类与对象

面向对象的编程思想是力图使在计算机语言中对事物的描述与现实世界中该事物的本来面目尽可能地一致。

类的基本概念

是对某一类事物的描述,是抽象的、概念上的定义。
对象是实际存在的属该类事物的具体个体,因而也成为实例(instance)。

数据成员:data member
域:field
函数成员:function member
方法:method
函数:function
面向对象程序设计:Object Oriented Programming,OOP
封装:encapsulate

数据成员称为域变量、属性、成员变量。
函数成员称为成员方法、方法。

定义类

类是将数据和方法封装在一起的一种数据结构,数据表示类的属性,方法表示类的行为,定义类实际上是定义类的属性与方法。

类的一般结构

1
2
3
4
5
6
7
8
[类修饰符] class 类名称 {	
[修饰符] 数据类型 成员变量名称; //声明成员变量

[修饰符] 返回值的数据类型 方法名(参数1, 参数2, ... , 参数n) { //声明成员方法
语句序列;
return [表达式];
}
}

类修饰符分为公共访问控制符、抽象类说明符、最终类说明符合缺省访问控制符。

类修饰符 含义
public 将一个类声明为公众类,可以被任何对象访问
abstract 将一个类声明为抽象类,没有实现方法,需要子类提供方法的实现,所以不能创建该类的实例
final 将一个类声明为最终类即非继承类,表示它不能被其他类所继承
缺省 缺省修饰符时,则表示只有在相同包中的对象才能使用这样的类

成员变量

1
[修饰符] 变量类型 变量名[ = 初值];

成员变量的修饰符有访问控制符、静态修饰符、最新修饰符、过渡修饰符和易失修饰符

成员变量修饰符 含义
public 公众访问控制符。指定该变量为公共的,它可以被任何对象的方法访问
private 私有访问控制符。指定该变量只允许自己类的方法访问,其他任何类(包括子类)中的方法均不能访问此变量
protected 保护访问控制符。指定该变量只可以被它自己的类及其子类或同一包中的其他类访问,在子类中可以覆盖此变量
缺省 缺省访问控制符时,则表示在同一个包中的类可以访问此成员变量,而其他包中的类不能访问该成员变量
final 最终修饰符。指定此变量的值不能改变
static 静态修饰符。指定该变量被所有对象共享,即所有的实例都可使用该变量
transient 过渡修饰符。指定该变量是一个系统保留、暂无特别作用的临时变量
volatile 易失修饰符。指定该变量可以同时被几个线程控制和修改

成员方法

1
2
3
4
[修饰符] 返回值的数据类型 方法名(参数1, 参数2, ..., 参数n) {
语句序列; //方法的主体
return [表达式]; //方法的主体
}

方法定义修饰符是可选项。
方法修饰符较多,包括访问控制符、静态修饰符、抽象修饰符、最终修饰符、同步修饰符和本地修饰符。

成员方法修饰符 含义
public 公共访问控制符。指定该方法为公共的,他可以被任何对象的方法访问
private 私有访问控制符。指定该方法只允许自己类的方法访问,其他任何类(包括子类)中的方法均不能访问此方法
protected 保护访问控制符。指定该方法只可以被它的类及其子类或同一包中的其他类访问
缺省 缺省访问控制符时,则表示在同一个包中的类可以访问此成员方法,而其他包中的类不能访问该成员方法
final 最终修饰符。指定该方法不能被覆盖
static 静态修饰符。指定不需要实例化一个对象就可以调用的方法
abstract 抽象修饰符。指定该方法只声明方法头,而没有方法体,抽象方法需在子类中被实现
synchronized 同步修饰符。在多线程程序中,该修饰符用于对同步资源加锁,以防止其他线程访问,运行结束后解锁
native 本地修饰符。指定此方法的方法体是用其他语言(如C语言)在程序外部编写的

成员变量与局部变量的区别

类中定义的变量是成员变量,二方法中定义的变量是局部变量。

  • 从语法形式上看,成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public、private、static 等修饰符所修饰,而局部变量则不能被访问控制修饰符及 static 所修饰;成员变量和局部变量都可以被 final 所修饰。
  • 从变量在内存中的存储方式上看,成员变量是对象的一部分,而对象是存在于堆内存的,而局部变量是存在于栈内存的。
  • 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而产生,随着方法调用的结束而自动消失。
  • 成员变量如果没有被赋初值,则会自动以类型的默认值赋值(有一种情况例外,被final 修饰但没有被static修饰的成员变量必须显式地赋值);而局部变量则不会自动赋值,必须显式地赋值后才能使用。

对象的创建与使用

对象的声明周期:创建 -> 使用 -> 销毁

创建对象

1
2
3
4
Cylinder volu;
volu = new Cylinder();

Cylinder volu = new Cylinder();

一个方法内部的变量必须进行初始化,否则编译无法通过。

成员变量的类型 初始值
byte 0
short 0
int 0
long 0L
flaot 0.0F
double 0.0D
char \u0000 (表示为空)
boolean false
所有引用类型 null

对象的使用

1
对象名.对象成员

如果Java程序中有多个类,经编译之后便会产生与类相等数目的.class文件。

如果再类声明的内部使用这些成员,则可直接使用成员名称,而不需要调用对象名称。

在类定义内调用方法

类定义的内部方法与方法之间也可以相互调用。

1
2
3
4
5
this.成员名

double volume() {
return this.area() * this.height;
}

参数的传递

以变量为参数调用方法

调用方法传参放在括号中,可以使数值型、字符串型、引用类型。

以数组作为参数或返回值的方法调用

基本数据类型传递的是该数据的值本身;引用数据类型传递的是独享的引用变量。

方法中的可变参数

1
2
3
返回值类型 方法名 (固定参数列表, 数据类型 ... 可变参数名) {
方法体
}

如果方法中有多个参数,可变参数必须位于最后一项,及可变参数只能出现在参数列表的最后。
可变参数符号”…“要位于数据类型和数组名之间,其前后有无空格都可以。
调用可变参数的方法时,编译器为该可变参数隐含创建一个数组,在方法体中以数组的形式访问可变参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class APP6_7 {
public static void display(int x, String ... arg) {
System.out.print(x + " ");
for (int i=0; i<arg.length; i++) {
System.out.print(arg[i] + " ");
}
System.out.println();
}

public static void main(String[] args) {
display(5);
display(6, "a", "b");
display(7, "AA", "BB", "CC", "DD");
}
}

//程序运行结果
5
6 a b
7 AA BB CC DD

匿名对象

当一个对象被创建之后,在调用改对象的方法时,也可以不定义对象的引用变量,而直接调用这个对象的方法,这样的对象成为匿名对象。

1
2
3
4
5
Cylinder volu = new Cylinder();
volu.setCylinder(2.5, 5, 3.14);

new Cylinder().setCylinder(2.5, 5, 3.14);
//方法执行完成后,该对象成为垃圾被回收

匿名对象使用情况:

  1. 如果对一个对象只需要进行一次方法调用,那么就可以使用匿名对象。
  2. 将匿名对象作为实参传递给一个方法调用。
1
2
3
4
5
publci static void getSomeOne(MyClass c) {
...
}

getSomeOne(new MyClass());

本章小结

  • 类是把事物的数据与相关的功能封装在一起,形成的一种特殊结构,用以表达现实世界的一种抽象概念。
  • 同一个Java 程序内,若定义了多个类,则最多只能有一个类声明为 public,在这种情况下,文件名称必须与声明成 public 的类名称相同。
  • Java 语言把数据成员称为成员变量,把函数成员称为成员方法,成员方法简称为方法。
  • 封装是指把变量和方法包装在一个类内,以限定成员的访问,从而达到保护数据的一种技术。
  • 由类所创建的对象称为实例。
  • 创建属于某类的对象,可以通过下面两个步骤来完成:①声明指向”由类所创建的对象”的变量;②利用 new 运算符创建新的对象,并用步骤①所创建的变量来指向它。
  • 要访问对象里的某个成员变量时,可以通过”对象名.成员变量名”的形式来达到;若要调用封装在类内的方法时,则可以使用“对象名.方法名()”的语法形式来完成。
  • 如果要强调”对象本身的成员”,可以在成员名前加上”this”关键宇。即”this. 成员名”,此时的 this 即代表调用该成员的对象。
  • 若方法本身没有返回值,则必须在方法定义的前面加上关键宇 void。
  • 在类外部可访问到类内部的公共成员。
  • 方法的参数可以是任意类型的数据,其返回值也可是任意类型。
  • 具有可变参数的方法所接收参数的个数可以不是固定的,而是根据需要传递参数的个数。方法中接收不固定个数的参数称为可变参数。其”可变参数名”就是接收可变实参的数组名,数组的长度由可变实参的个数决定。
  • 当一个对象被创建之后,在调用该对象的方法时,不定义对象的引用变量,而直接调用这个对象的方法,这样的对象称为匿名对象。

课后习题

  • 类与对象的区别是什么?
  • 如何定义一个类?类的结构是怎样的?
  • 定义一个类时所使用的修饰符有哪几个?每个修饰符的作用是什么?是否可以混用?
  • 成员变量的修饰符有哪些?各修饰符的功能是什么?是否可以混用?
  • 成员方法的修饰符有哪些?各修饰符的功能是什么?是否可以混用?
  • 成员变量与局部变量的区别有哪些?
  • 创建一个对象使用什么运算符?对象实体与对象引用有何不同?
  • 对象的成员如何表示?
  • 在成员变量或成员方法前加上关键宇this 表示什么含义?
  • 什么是方法的返回值?返回值在类的方法里的作用是什么?
  • 在方法调用中,使用对象作为参数进行传递时,是”传值”还是”传址”?对象作参数起到什么作用?
  • 什么叫匿名对象?一般在什么情况下使用匿名对象?
  • 以m行n列二维数组为参数进行方法调用,分别计算二维数组各列元素之和,返回并输出所计算的结果。

第七章 Java语言类的特性

类的私有成员与公共成员

私有成员

在类的成员声明前面加上修饰符private,就无法从类的外部访问到类的内部成员,只能被该类自身访问和修改,不能被任何其他类(包括该类的子类)获取或引用,

公共成员

在类的成员声明前面加上修饰符public,则表示该成员你可以被所有其他的类所访问。

缺省访问控制符

若在类成员的前面不加任何访问控制符,则该成员具有缺省的访问控制特性,该成员只能被同一个包(类库)中的类所访问和调用,如果一个子类与父类位于不同的包中,子类也不能访问父类中的缺省访问控制成员,其他包中任何类都不能缺省访问控制成员。

如果一个类没有访问控制符,只能被同一个包中的类访问和引用,而不可以被其他包中的类所使用。

方法的重载(overloading)

同一个类内具有相同名称的多个方法,这多个同名方法如果参数个数不同,或者是参数个数相同但类型不同,或参数的顺序不同则这些同名的方法就具有不同的功能。

Java不允许参数个数或参数类型完全相同,而只有返回值类型不同的重载。

构造方法(constructor)

构造方法的作用与定义

  • 构造方法是在对象被创建时初始化对象成员的方法。
  • 名称必须要与它所在的类名完全相同。
  • 构造方法没有返回值。不能用void来修饰。
  • 创建对象时会自动调用它。
  • 不能被显式直接调用,而是用new来调用。
  • 创建一个类的对象的同时,会自动调用该类的构造方法为新对象初始化。

默认的构造方法

如果省略构造方法,Java编译器会自动为该类生成一个默认的构造方法(default constructor),一旦用户为该类定义了构造方法,系统就不再提供默认的构造方法。

构造方法的重载

当一个类有多个构造方法时,这多个构造反方可以重载。

从一个构造方法内调用另一个构造方法

  • 在一个构造方法内调用另一个构造方法时,必须使用 this() 语句来调用,否则编译时将出现错误。
  • this() 语句必须写在构造方法内的第一行位置。

公共的构造方法与私有的构造方法

构造方法一般为公共的,因为在创建对象时,是在类的外部被自动调用。

如果构造方法被声明为private,则无法在该构造方法所在的类以外的地方调用,但在该类的内部还是可以被调用的。

静态成员

被 static 修饰的成员称为静态成员,也称为类成员,而不用 static 修饰的成员称为实例成员。

实例成员

没有被static修饰的成员。

静态变量

被static修饰的成员变量,也称为 类变量。

静态变量是属于类的变量而不是属于任何一个类的具体对象。

静态变量是一个公共的存储单元,不是保存在某个对象实例的内存空间,而是保存在类的内存空间的公共存储单元中。

类的任何一个对象访问它,取到的都是一个相同的数值。
类的任何一个对象去修改它,都是在对同一个内存单元做操作。

静态变量不需要实例化就可以使用。也可以通过对象来访问静态变量。

1
2
类名.静态变量名;	//建议访问方式
对象名.静态变量名;

静态变量必须独立于方法之外,必须在函数外声明。
可以节省内存。

静态方法

用static修饰符修饰的方法。

  • 非static的方法是属于某个对象的方法,在创建这个对象时,对象的方法在内存中拥有属于自己专用的代码段。而static 的方法是属于整个类的,它在内存中的代码段将被所有的对象所共用,而不被任何一个对象所专用。
  • 由于static 方法是属于整个类的,所以它不能直接操纵和处理属于某个对象的成员,而只能处理属于整个类的成员,即 static 方法只能访问 static 成员变量或调用 static 成员方法,或者说在静态方法中不能直接访问实例变量与实例方法。静态方法中虽不能直接访问非静态的成员,但可以通过创建对象的方法间接地访问非静态成员。
  • 在静态方法中不能使用 this 或super。
  • 调用静态方法时,可以使用类名直接调用,也可以用某一个具体的对象名来调用。
1
2
类名.静态方法名();
对象名.静态方法名();

静态方法可以在不产生对象的情况下直接以类名来调用。

main程序入口方法的理解:由于 Java 虚拟机需要在类外调用main()方法,所以该方法的访问权限必领是public;又因为 Java 虚拟机运行时系统在开始执行一个程序前,并没有创建main()方法所在类的一个实例对象,所以它只能通过类名来调用 main()方法作为程序的人口,即调用main()方法的是类名,而不是由类所创建的对象,因而该方法必须是static 的。

静态初始化器

关键字static修饰的一对花括号”{}”括起来的语句组。

1
2
3
static {	//类初始化器
...
}

用于初始化工作。

  • 构造方法是对每个新创建的对象进行初始化,而静态初始化器是对类自身进行初始化。
  • 构造方法是在用new运算符创建新对象时由系统自动执行,而静态初始化器一般不能由程序调用,它是在所属的类被加载入内存时由系统调用执行的。
  • 用new运算符创建多少个新对象,构造方法就被调用多少次,但静态初始化器则在类被加载入内存时只执行一次,与创建多少个对象无关。
  • 不同于构造方法,静态初始化器不是方法,因而没有方法名、返回值和参数。

多个静态初始化器,则在类的初始化时会依次执行。

对象的应用

对象的赋值与比较

当参数是基本数据类型时,是传值方式调用,而当参数是引用变量时,则是传址方式调用。

== 比较的是对象的引用地址是否相同。

引用变量作为方法的返回值

类类型的数组

  • 声明类类型的数组变量,并用new运算符分配内存空间给数组;
  • 用new创建新的对象,分配内存空间给它,并让数组元素指向它。

以对象数组为参数进行方法调用

Java语言的垃圾回收

在Java 程序的生命周期中,Java 运行环境提供丁一个系统的垃圾回收器线程,负责自动回收那些没有被引用的对象所占用的内存,这种清除无用对象进行内存回收的过程就叫作垃圾回收(garbase-collection)。垃圾回收是Java 语言提供的一种自动内存回收功能,可以让程序员减轻许多内存管理的负担,也减少程序员犯错的机会。
一个对象被创建时。JVM 会为该对象分配一定的内存、调用该对象的构造方法并开始跟踪该对象。当该对象停止使用时,JVM 将通过垃圾回收器回收该对象所占用的内存。
那么Java 是如何知道一个对象是无用的呢?这是因为系统中的任何对象都有一个引用计数器,一个对象被引用1次,则该对象的引用计数器为 1,被引用2次,则引用计数器为 2相反,若对一个对象减少1 次引用,则该对象的引用计数器就诚 1,依次类推,当一个对象的引用计数器诚到0时,说明该对象可以回收。

垃圾回收的两个好处

  • (1)把程序员从复杂的内存追踪、监测、释放等工作中解放出来。
  • (2)防止了系统内存被非法释放,从而使系统更加稳定。

垃圾回收的三个特点

  • (1)只有当一个对象不被任何引用类型的变量使用时,它占用的内存才可能被垃圾回
    收器回收。

如下面的程序段:

1
2
3
String strl = "This is a string";
String str2 = strl;
str2 = new String ("This is another string");

当程序执行到第 3行时,”This is a string”对象仍然被str2 引用,因此,此时不能被垃圾回收器回收。当程序执行完第4行,str2引用了一个新的字符串对象,此时”This is string”对象不在被任何引用类型的变量(str1 和 str2)引用,因此,此时该对象可以被当作垃圾回收。

  • (2)不能通过程序强迫垃圾回收器立即执行。

垃圾回收器负责释放没有引用与之关联的对象所占用的内存,但是回收的时间对程序员是透明的,在任何时候,程序员都不能通过程序强迫垃圾回收器立即执行,但可以通过调用System.gc()或者Runtime.sc()方法提示垃圾回器进行内存回收操作,不过这也不能保证调用该方法后,垃圾回收器立即执行。

  • (3)当拉圾回收器将要释放无用对象占用的内存时,先调用该对象的 finalize()方法。

在Java 语言中对象的回收是由系统进行的,但有一些任务需要在回收时进行,如清理一些非内存资源、关闭打开的文件等。这可通过覆盖对象中的finalize()方法来实现,因为系统在回收时会自动调用对象的finalize()方法。

finalize()方法的形式如下:

1
protected void Finalize() throws Throwable

由于只有当拉圾回收器将要释放该对象的内存时,才会执行该对象的finalize()方法,如果在小程序或应用程序退出之前,垃圾回收器始终没有执行释放内存的操作,那么垃圾回收器将不会调用无用对象的 finalize()方法。换句话说,以下情况是完全可能的:一个小程序或应用程序只占用了少量的内存,没有造成严重的内存需求,于是垃圾回收器没有释放这些对象的内存就退出了。显然,如果程序员为某个对象定义了 finalize()方法,JVM 可能不会调用它,因为垃圾回收器不曾释放过这个对象的内存,调用 System.gc()也不会起作用,因为它仅仅是给JVM一个建议而不是命令。当一个对象将要退出生命周期时,可以通过finalize()方法来释放对象所占的其他相关资源,但是,JVM有很大的可能不调用对象的finalize()方法,因此很难保证使用该方法来释放资源是安全有效的。

本章小结

  • 用修饰符 private 修饰的类成员称为类的私有成员(private member)。私有成员无法从该类的外部访问到,而只能被该类自身访问和修改,而不能被任何其他类(包括该类的子类)获取或引用;如果在类的成员声明的前面加上修饰符public,则该成员为公共成员,表示该成员可以被所有其他的类所访问。
  • 所谓重载是指在同一个类内定义相同名称的多个方法。这些同名的方法或者参数的个数不同或者参数的个数相同但类型不同,这些同名的方法便可以具有不同的功能。
  • 构造方法可视为一种特殊的方法,它的主要功能是帮助创建的对象赋初值。
  • 构造方法的名称必须与其所属的类名称相同,且不能有返回值。
  • 从某一构造方法内调用另一构造方法,必须通过 this()语句来调用。
  • 构造方法有公共(public) 与私有 (private)之分,公共构造方法可以在程序的任何地方被调用,所以新创建的对象均可自动调用它,而私有构造方法则无法在该构造方法所在的类以外的地方被调用。
  • 如果一个类没有定义构造方法,则 Java 编译系统会自动为其生成默认的构造方法。默认的构造方法是没有任何参数,方法体内也没有任何语句的构造方法。
  • 实例变量与实例方法、静态变量与静态方法是不同的成员变量与成员方法。
  • 基本类型的变量是指由 int.double 等关键字所声明而得到的变量,而由类声明而得到的变量称为类类型的变量,它是属于引用类型变量的一种。
  • 对象也可以用数组来存放,但必须有下面两个步骤:①声明类类型的数组变量,并用new 运算符分配内存空间给数组;②用new 运算符产生新的对象,并分配内存空间给它,并让数组元素指向它。
  • Java 语言具有垃圾自动回收的功能。

课后习题

  • 一个类的公共成员与私有成员有何区别?
  • 什么是方法的重载?
  • 一个类的构造方法的作用足什么?若一个类没有声明构造方法。该程序能正殖执行吗?为什么?
  • 构造方法有哪些特性?
  • 在在一个构造方法内可以调用另一个构造方法吗?如果可以,如何调用?
  • 静态变量与实例变量有哪些不同?
  • 静态方法与实例方法有哪些不同?
  • 在一个静态方法内调用一个非静态成员为什么是非法的?
  • 对象的相等与指向它们的引用相等有什么不同?
  • 什么是静态初始化器?其作用是什么?静态初始化器由谁在何时执行?它与构造方法有何不同?
  • Java语言中怎样清除没有被引用的对象?能否控制 Java 系统中垃圾的回收时间?

第八章 继承、抽象类、接口和枚举

类的继承是使用已有的类作为基础派生出新的类。
抽象类与接口都是类概念的扩展

类的继承

父类/超类(superclass):被继承的类;
子类(subclass):由继承而得到的类。

一个类只能有一个直接父类。所有类都是直接或间接继承该类

子类的创建

1
2
3
class SubClass extends SpuerClass {
...
}

子类每个对象也是其父类的对象,任何可以使用父类实例的地方,都可以使用子类实例。

子类的构建方法

执行子类的构造方法之前会先调用父类中没有参数的构造方法。

extends关键字可将父类中的非私有成员继承给子类。

调用父类中特定的构造方法

在子类的构造方法中通过super()语句来调用父类特定的构造方法。(该语句必须写在第一行)

在子类中访问父类的成员

子类中使用super不但可以访问父类的构造方法,还可以访问父类的成员变量和成员方法,但super不能访问在子类中添加的成员。

1
2
super.变量名();
super.方法名();

父类protected 成员,子类可以访问。
父类private 成员,子类不可继承

覆盖

java多态polymorphism

在子类中定义名称、参数个数与类型均与父类中完全相同的方法,用以重写父类中同名方法的功能。

覆盖父类的方法

子类不能覆盖父类中声明为final或static的方法。

在子类中覆盖父类的方法时,可以扩大父类中的方法权限,但不可以缩小父类方法权限。

用父类的对象访问子类的成员

只有覆盖情况发生才可以使用。

向上转型:父类引用指向子类对象。
向下转型:子类引用指向父类对象,需要显示声明强转。

不可被继承的成员与最终类

final来修饰成员变量/类,不可以被修改/继承。

final成员变量和final局部变量都是只读量,只能被赋值一次,创建时候赋值或者构造方法赋值。
static+final,指定为常量,只能在定义时候赋值。

Object类

java.lang.Object

Object常用方法 功能说明
public boolean equals(Obect obj) 判断两个对象变量所指向的是否为同一个对象
public String toString() 将调用toString()方法的对象转换成字符串
public final Class getClass() 返回运行时对象所属的类
protected Object clone() 返回调用该方法的对象的一个副本

equals()方法

对于字符串的操作,Java程序在执行时会维护一个字符串池(String poll),对于一些可共享的字符串对象,会现在字符串池中查找是否有相同的字符串内容(字符相同),如果有就直接返回,而不是直接创建一个新的字符串对象,以减少内存的占用。

toString()方法

getClass()方法

返回运行时的对象所属的类。

对象运算符instanceof

测试一个指定对象是否是指定类或它的子类的实例,返回true/false

getName() 得到this类名字符串
getSuperclass() 得到父类

抽象类

抽象类(abstract):专门的类作为父类

抽象类不能用new运算符来创建实例对象的类

抽象类与抽象方法

1
2
3
4
5
6
7
8
abstract class 类名 {
声明成员变量;
返回值的数据类型 方法名(参数表) {
...
}
...
abstract 返回值的数据类型 方法名(参数表);
}

抽象方法声明中static和abstract不能同时用。
abstract不能和final合用。
abstract不能与private、static、final或native并列修饰同一个方法。

抽象类不一定有抽象方法,但有抽象方法的类一定要声明为抽象类。

抽象类可以定义构造方法,但需要用protected修饰,只能被子类构造调用。
如果抽象类没有定义构造方法,则系统为其添加默认的构造方法。

抽象类的应用

接口

interface

接口本身也具有数据成员、抽象放、默认方法和静态方法。

接口的数据成员都是静态的且必须初始化,数据成员都是静态常量。
接口中除了声明抽象方法外,还可以定义静态方法和默认方法,但是不能定义一般方法。

接口的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
[public] interface 接口名称 [extends 父接口名列表] {
[public] [static] [final] 数据类型 常量名 = 常量; //常量

[public] [abstract] 返回值的数据类型 方法名(参数表); //抽象方法

[public] static 返回值的数据类型 方法名(参数表) { //静态方法
...
}

[public] default 返回值的数据类型 方法名(参数表) { //静态方法
...
}
}

接口与一般的类一样,本身也具有数据成员与成员方法,但数据成员必须是静态的且一定要赋初值,且此值不能再被修改,若省略数据成员的修饰符,系统默认为public static final;对抽象方法,若方法名前即使省略修饰符,系统仍然默认为public abstract;接口中的静态方法是用public static修饰的;而默认方法是用public default修饰的。

  • 接口中的“抽象方法”只需做声明,不用定义其处理数据的方法体;
  • 数据成员都是静态的且必须赋初值,即数据成员必须是静态常量;
  • 接口中的成员都是公共的,所以在定义接口时若省略了public修饰符,在实现抽象方法时,则不能省略该修饰符;
  • 接口实际上就是一种特殊的抽象类。

接口的实现与引用

implement

1
2
3
class 类名称 implements 接口名表 {
...
}
  • 如果实现某接口的类不是abstract的抽象类,则在类的定义部分必须实现指定接口的所有抽象方法,即非抽象类中不能存在抽象方法。
  • 一个类在实现某接口的抽象方法时,必须使用完全相同的方法头,否则只是在定义一个新方法,而不是实现已有的抽象方法。
  • 接口中抽象方法的访问控制修饰符都已指定为public,所以类在实现方法时,必须显式地使用public修饰符,否则将被系统警告为缩小了接口中定义的方法的访问控制范围。
  • 与类一样,每个接口都被编译成独立的扩展名为.class的字节码文件。

接口的继承

一个接口可以有一个以上的父接口,它们之间用逗号分隔,形成父接口列表。
新接口将继承所有父接口中的常量、抽象方法和默认方法,但不能继承父接口中的静态方法,也不能被实现类所继承。

利用接口实现类的多重继承

1
2
3
public class A implements B, C {
...
}

接口中静态方法和默认方法

接口中的静态方法不能被子接口继承,也不能被实现类继承。默认方法可以。
默认方法虽然有方法体,但是必须通过对象来调用,不能通过接口调用。

解决接口多重继承中名字冲突问题

如果子接口中定义了与父接口同名的常量或者相同名称的方法,则父接口中的常量被隐藏,方法被覆盖。

发生冲突:

  • 同名方法新实现。
  • 委托声明 接口名.super.默认方法名()

在多个父接口的实现类中解决同名默认方法的名字冲突问题,有两种办法:一种是提供同名方法的一个新实现;另一种是委托一个父接口的默认方法。
如果两个父接口中有一个提供的不是默认方法,而是抽象方法,则只需要在接口的实现类中提供同名方法的一个新实现即可。
如果两个父接口中的同名方法都是抽象方法,则不会发生名字冲突,实现接口的类可以实现该同名方法即可,或者不实现该方法而将自己也声明为抽象类。
如果一个类继承一个父类并实现了一个接口,而从父类和接口中继承了同名的方法,此时采用“类比接口优先”的原则,即只继承父类的方法,而忽略来自接口的默认方法。

枚举

有些数据的取值被限定在几个确定的值之间,可以被一一列举出来。

对于类似这种当一个变量有几种固定取值时,将其声明为枚举类型。

枚举类型的定义

枚举是一种特殊的类,也称为枚举类,是一种引用类型。

1
2
3
4
[修饰符] enum 枚举类型名 {	//修饰符可以使public/private/internal
枚举成员
方法
}

枚举类型名:作为枚举名使用;表示枚举成员的数据类型。

枚举成员:一一列出的枚举常量,任何两个枚举成员之间不能重名,用逗号分割。

enum、class、interface地位相同。

  • 枚举可以实现一个或多个接口,使用enum关键字声明的枚举默认继承了java.lang.Enum类,而不是继承java.lang.Object类,因此枚举不能显式地继承其他父类。
  • 使用enum定义非抽象的枚举类时默认使用final修饰,因此枚举类不能派生子类。
  • 创建枚举类型的对象时不能使用new运算符,而是直接将枚举成员赋值给枚举对象。
  • 因为枚举是类,所以它可以有自己的构造方法和其他方法。但构造方法只能用private访问修饰符, 如果省略,则默认使用private修饰符,如果强制使用访问修饰符,则只能使用private。
  • 枚举的所有枚举成员必须在枚举体的第一行显式列出,否则该枚举不能产生枚举成员。枚举成员默认使用public static final进行修饰。
  • 可以通过==!=比较两个枚举是否相等。
1
2
3
public enum Direction {
EAST, SOUTH, WEST, NORTH; //默认使用public static final 修饰
}

所有枚举类型都包含values()和valueOf()两个预定义方法。

方法 功能说明
public static enumtype[] values() 返回枚举类型的数组,该数组包含枚举的所有枚举成员,并按它们的声明顺序存储
public static enumtype valueOf(String str) 返回名称为str的枚举成员

所有枚举对象都继承自抽象类java.lang.Enum<E>,该类定义了枚举公用的方法以方便用户使用。java.lang.Enum<E>实现了java.lang.Comparable<E>java.lang.Serializable 两个接口,所以枚举类型时可以使用比较器和遍历操作的。
| Eunm < E >常用方法 | 功能说明 |
| :-: | :-: |
| public final int compareTo(E o) | 返回当前枚举成员与参数枚举成员o在定义时顺序的比较结果 |
| public final String name() | 返回枚举常量的名称 |
| public final int ordinal() | 返回枚举成员在枚举中的序号(枚举成员的序号从0开始) |
| publie final boolean equals(Object other) | 比较两个枚举引用的对象是否相等 |
| public String toString() | 返回枚举成员的名称 |
| public static < TextendsEnum < T > > T valueOf(Class< T > enumType, String name) | 返回指定枚举类型和指定名称的枚举成员 |

不包含方法的枚举

当访问枚举类型的成员时,直接使用枚举名嗲用枚举成员即可,枚举名.枚举成员

枚举名.valueOf() 形式调用获取枚举类的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Direction {
EAST, SOUTH, WEST, NORTH;
}

public class D8_1 {
public static void mian(String[] args) {
Direction dir = Direction.EAST;
Direction dir1 = Direction.valueOf("NORTH");
System.out.pritln(dir);
System.out.pritln(dir1);
for (Direction d : Direction.values()) {
System.out.pritln(d.ordoal() + " " + d.name());
}
}
}
1
2
3
4
5
6
EAST
NORTH
0 EAST
1 SOUTH
2 WEST
3 NORTH

包含属性和方法的枚举

枚举的构造方法只是在构造枚举成员时候被调用。每一个枚举成员都是枚举的一个对象,因此创建每个枚举成员时都需要调用该构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Direciton {
EAST("东"), SOUTH("南"), WEST("西"), NORTH("北");
private String name;
private Direction(String name) {
this.name = name;
}
public String toString() {
return name;
}
}

public class D8_2 {
public static void mian(String[] args) {
Direction dir = Enum.valueOf(Direction.class, "NORTH");
System.out.pritln(dir);
for (Direction d : Direction.values()) {
System.out.pritln(d.name() + " " +d.toString());
}
}
}
1
2
3
4
5

EAST 东
SOUTH 南
WEST 西
NORTH 北

包的概念

Java语言提供的一种区别类名空间的机制,是类的组织方式。

使用package语句创建包

1
package 包名1[.包名2[.包名3]...];

包层次的根文件夹是由环境变量ClassPath来确定的。
默认包的路径是当前文件夹,即无名包(unnamed package)

Java语言中的常用包

API :应用程序接口(Application Programming Interface)

  • java.lang:语言包;
  • java.io:输入输出流的文件包;
  • java.util:实用包;
  • java.net:网络功能包;
  • java.sql:数据库连接包;
  • java.text:文本包。

语言包java.lang

  • Object类;
  • 数据类型包装类( The Data Type Wrapper ) ;
  • 字符串类( String) ;
  • 数学类( Math) ;
  • 系统和运行时类( System、Runtime ) ;
  • 类操作类( Class ) ;
  • 错误和异常处理类( Throwable, Exception和Error ) ;
  • 线程类( Thread ) ;
  • 过程类( Process )。

输入输出流的文件包java.io

  • 基本输入输出流类;
  • 文件输入输出流类;
  • 过滤输入输出流类;
  • 管道输入输出流类;
  • 随机输入输出流类。

实用包java.util

  • 数据输入类(Scanner) ;
  • 日期类(Date、Calendar等) ;
  • 链表类(LinkedList) ;
  • 向量类(Vector) ;
  • 哈希表类(Hashtable) ;
  • 栈类(Stack) ;
  • 树类(TreeSet)。

网络功能包java.net

  • 访问网络资源类(URL) ;
  • 套接字类(Socket) ;
  • 服务器端套接字类(ServerSocket) ;
  • 数据报打包类(DatagramPacket) ;
  • 数据报通信类(DatagramSocket)。

数据库连接包java.sql

实现JDBC(Java DataBase Conection)java数据库链接的类库。

文本包java.text

Java文本包java.text中的Format、DataFormat、SimpleDateFormat等类提供各种文本或日期格式。

Java语言中几个常用的类

Date类(java.util)

Date构造方法 功能说明
public Date() 用系统日期时间数据创建Date对象
public Date(long date) 用长整型数date创建Date对象,date表示从1970年1月1日00:00:00时开始到该日期时刻的微秒数
Date常用方法 功能说明
public long getTime() 返回从1970年1月1日00:00:00时开始到目前的微秒数
public boolean after(Date when) 日期比较,日期在when之后返回true,否则返回false
public boolean before(Date when) 日期比较,日期在when之前返回true,否则返回false

Date对象表示时间的默认顺序是:星期、月、日、小时、分、秒、年。如果希望按年、月、日、时、分、秒、星期的顺序显示其时间,这时可以使用java.text.DateFormat类的子类java.text.SimpleDateFormat来实现日期的格式化。

SimpleDateFormat类有一个常用的构造方法:public SimpleDateFormat(String pattern)。该构造方法可以用参数pattern指定格式创建一个对象,该对象调用format(Date date)方法来格式化时间对象date。需要注意的是,pattern中应当含有如下一些有效的字符序列:

  • y或yy表示用2位数字输出的年份,yyyy表示用4位数字输出年份;
  • M或MM表示用2位数字或文本输出月份,若要用汉字输出月份,pattern中应连续包含至少3个M;
  • d或dd表示用2位数字输出日;
  • H或HH表示用2位数字输出小时;
  • m或mm表示用2位数字输出分;
  • s或ss表示用2位数字输出秒;
  • E表示用字符串输出星期;
  • a表示输出上、下午。

Calender类(java.util)

描述日期时间的抽象类。

Calendar类通常用于需要将日期值分解的情况,Calendar类中声明了YEAR等多个常量,分别表示年、月、日等日期中的单个部分值。

Calender类中常用的常量 意义
public static final int YEAR 表示对象日期的年
public static final int MONTH 表示对象日期的月,0 ~ 11分别表示1 ~ 12月
public static final int DAY_OF_MONTH 表示对象日期的日
public static final int DATE 与public static final int DAY_OF_MONTH意义相同
public static final int DAY_OF_YEAR 表示对象日期是该年的第几天
public static final int WEEK_OF_YEAR 表示对象日期是该年的第几周
public static final int HOUR 表示对象日期的时
public static final int MINUTE 表示对象日期的分
public static final int SECOND 表示对象日期的秒
Calender类常用方法 功能说明
public int get(int field) 返回对象属性field 的值,属性是上表描述的静态常量
public void set(int field,int value) 设置对象属性field的值为value
public boolean after(Object when) 日期比较,日期在when之后返回true,否则返回false
public boolean before(Object when) 日期比较,日期在when之前返回true,否则返回false
public static Calendar getInstance() 获取Calendar对象
public final Date getTime() 由Calendar对象创建Date对象
public long getTimeInMillis() 返回从1970年1月1日00:00:00时开始到目前的微秒数
public void setTimeInMillis(long millis) 以长整型数millis设置对象日期,millis表示从1970年1月1日00:00:00时开始到该日期时刻的微秒数
1
2
Calendar now = Calendar.getInstance();	//创建日历对象
int month = now.get(Calendar.MONTH); //获得日历对象的月份值

也可利用now对象调用相应的set()方法将日历翻到任何一个时间。

Random类(java.util)

Random类构造方法 功能说明
public Random() 用系统时间作为种子创建Random对象
public Random(long seed) 用seed作为种子创建Random对象
Random类常用方法 功能说明
public int nextInt() 返回一个整型随机数
public int nextInt(int n) 返回一个大小为0~n的整型随机数
public long nextLong() 返回一个长整型随机数
public float nextFloat() 返回一个0.0~1.0的单精度随机数
public double nextDouble() 返回一个0.0~1.0的双精度随机数

Math类(java.lang)

Calender类中常用的常量 意义
public static final double PI 圆周率π=3.141592653589793
public static final double E 自然对数率e=2.718281828459045
Calender类中常用方法 意义
public static double abs(double a) 返回数a的绝对值
public static double sin(double a) 返回a的正弦值,a的单位为弧度
public statice double cos(double a) 返回a的余弦值,a的单位为弧度
public static double tan(double a) 返回a的正切值,a的单位为弧度
public static double asin(double a) 返回a的反正弦值
public static double acos(double a) 返回a的反余弦值
public static double atan(double a) 返回a的反正切值
public static double sqrt(double a) 返回数a的平方根,a必须是正数
public static double ceil(double a) 返回大于或等于a的最小实型整数值
public static double floor(double a) 返回小于或等于a的最大实型整数值
public static double random() 返回取值在[0.0,1.0)区间的随机数
public static double pow(double a, double b) 返回以a为底,以b为指数的幂值

利用import语句引用Java定义的包

1
import包名1[.包名2[.包名3…]].类名|∗

Java编译器为所有程序自动隐含地导入java.lang包

使用*只能表示本层次的所有类,不包括子层次下的类。

1
2
3
4
5
6
7
8
import java.util.*;
class myDate extends Date {

}

class myDate extends java.util.Date {

}

Java程序结构

  • package,声明包,0或1个;
  • import,导入包,0或多个;
  • public class,声明公有类,0个或1个,文件名与该类名相同;
  • class,声明类,0或多个;
  • interface,声明接口,0或多个。

本章小结

  • 通过extends关键字,可将父类的非私有成员(成员变量和成员方法)继承给子类。
  • 父类有多个构造方法时,如果要调用特定的构造方法,则可在子类的构造方法中,通过super()语句来调用。
  • Java程序在执行子类的构造方法之前,如果没有用super()语句来调用父类中特定的构造方法,则会先调用父类中没有参数的构造方法。其目的是为了帮助继承自父类的成员做初始化操作。
  • 在构造方法内调用同一类内的其他构造方法使用this()语句,而从子类的构造方法调用其父类的构造方法则使用super()语句。
  • this())除了可以用来调用同一类的构造方法之外,如果同一类内的成员变量与局部变量的名称相同时,也可以利用“this.成员变量名”来调用同一类内的成员变量。
  • this()与super()的相似之处:(1)当构造方法有重载时,两者均会根据所给予的参数的类型与个数,正确地选择执行相对应的构造方法;(2)两者均必须编写在构造方法内的第一行,也就是因为这个原因,this()与super()无法同时存在于同一个构造方法内。
  • 除了利用super()来调用父类的构造方法外,还利用“super.成员名”的形式来调用父类中的成员变量或成员方法。
  • 把成员声明成protected最大的好处是可同时兼顾到成员的安全与便利性,因为它只能供父类、子类及同一包中的类来访问,而其他类则无法更改或读取它。
  • 重载是指在同一个类内,定义名称相同但参数个数或类型不同的多个方法。Java系统可根据参数的个数或类型,调用相对应的方法。
  • 覆盖是在子类当中,定义名称、参数个数与类型均与父类相同的方法,用以覆盖父类中方法的功能。
  • 如果父类的方法不希望子类的方法来覆盖它,可以在父类的方法之前加上final关键字,这样该方法就不会被覆盖。
  • final关键字的另一作用是把它放在成员变量前面,这样该变量就变成一个常量,因而便无法在程序中的任何地方再做修改。
  • 无论是自定义的类,还是Java内置的类,所有的类均继承自Object类。
  • Java语言的抽象类是专门用来当作父类的,所以抽象类不能直接用来创建对象。抽象类的目的是要用户根据它的格式来修改并创建新的类。
  • 抽象类中的方法可分为两种:一种是一般的方法;另一种是以关键字abstract开头的抽象方法。抽象方法是没有定义方法体的方法,是要保留给由抽象类派生出的子类来定义。
  • 接口的结构和抽象类非常相似,它也具有数据成员、抽象方法、默认方法和静态方法,但它与抽象类有两点不同:(1)接口的数据成员都是静态的且必须初始化;(2)接口中的抽象方法必须全部声明为public abstract。
  • Java语言并不允许类的多重继承,但利用接口可实现多重继承。
  • 接口与一般类一样,均可通过扩展技术来派生出新的接口。原来的接口称为基本接口或父接口;派生出的接口称为派生接口或子接口。通过这种机制,子接口不仅可以拥有父接口的成员,同时也可以添加新的成员以满足实际问题的需要。
  • 枚举是一种特殊的类,所以它是一种引用类型。
  • 枚举类型名有两层含义:一是作为枚举名使用;二是表示枚举成员的数据类型。正因为如此,枚举成员也称为枚举实例或枚举对象。
  • Java语言的package是存放类与接口的地方,因此我们把package译为“类库”。它是在使用多个类或接口时,避免名称重复而采用的一种措施。
  • 在源文件内若没有指明package,则Java把它视为“没有名称的package”。
  • 如果多个类分别属于不同的package,若某个类要访问到其他类的成员时,必须做下列修改:①若某个类需要被访问时,则必须把这个类声明为public的;②若要访问不同package内某个public类的成员时,在程序代码内必须明确地指明“被访问package的名称.类名称”。
  • 在类之前加上public修饰符是为了让其他包里的类也可以访问该类里的成员。如果省略了类的修饰符,则只能让同一个包里的类来访问。
  • 导入包里的某个类,其格式为“import包名.类名”。
  • String类放置在java.lang类库内。在java.lang类库里所有的类均会自动加载,因此当使用到String类时,无须利用import命令来加载它。

课后习题

  • 子类将继承父类的所有成员吗?为什么?
  • 在子类中可以调用父类的构造方法吗?若可以,如何调用?
  • 在调用子类的构造方法之前,若没有指定调用父类的特定构 造方法,则会先自动调用父类中没有参数的构造方法,其目的是什么?
  • 在子类中可以访问父类的成员吗?若可以,用什么方式访 问?
  • 用父类对象变量可以访问子类的成员方法吗?若可以,则只限于什么情况?
  • 什么是多态机制?Java语言中是如何实现多态的?
  • 方法的覆盖与方法的重载有何不同?
  • this和super分别有什么特殊的含义?
  • 什么是最终类与最终方法?它们的作用是什么?
  • 什么是抽象类与抽象方法?使用时应注意哪些问题?
  • 什么是接口?为什么要定义接口?
  • 如何定义接口?接口与抽象类有哪些异同?
  • 在多个父接口的实现类中,多个接口中的方法名冲突问题有几种形式?如何解决?
  • 编程题。定义一个表示一周七天的枚举,并在主方法 main()中遍历枚举所有成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class WeekDaysList {
public static void main(String[] args) {
for(DaysOfTheWeek day:DaysOfTheWeek.values())
System.out.println(day);
}
}

enum DaysOfTheWeek
{
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,STAURDAY,SUNDAY;
public String toString()
{
String s = super.toString();
return s.substring(0,1) + s.substring(1).toLowerCase();
}
}
  • 什么是包?它的作用是什么?如何创建包?如何引用包中的类?

第九章 异常处理

异常处理的基本概念

异常(exception)是指在程序运行中由代码产生的一种错误。

错误与异常

错误分为语法错误语义错误逻辑错误

语法错误:语法错是由于违反程序设计语言的语法规则而产生的错误,如标识符未声明、表达式中运算符与操作数类型不兼容、括号不匹配、语句末尾缺少分号等。语法错误是由语言的编译系统负责检测和报告。

语义错误:如果程序在语法上正确,但在语义上存在错误,如输入数据格式错、除数为0错、给变量赋值超出其允许范围等,这类错误称为语义错。还有一些语义错误不能被程序事先处理,如待打开的文件不存在、网络连接中断等,这类错误的发生不由程序本身所控制,因此必须进行异常处理。

逻辑错误:如果程序编译通过,也可运行,但运行结果与预期结果不符,如由于循环条件不正确而没有结果、循环次数不对等因素导致的计算结果不正确等,这类错误称为逻辑错。

虽然程序有三种性质的错误,但Java系统中根据错误严重程度的不同,而将程序运行时出的错分为两类:错误异常

错误是指程序在执行过程中所遇到的硬件或操作系统的错误,如内存溢出、虚拟机错等。

异常是指在硬件和操作系统正常时,程序遇到的运行错。

Java语言的异常处理机制

Java语言提供的异常处理机制是通过面向对象的方法来处理异常的。

异常抛出后,运行系统从生成异常对象的代码开始,沿方法的调用栈逐层回溯查找,直到找到包含相应异常处理的方法,并把异常对象提交给该方法为止,这个过程称为捕获异常

每当Java程序运行过程中发生一个可识别的运行错误时,即该错误有一个异常类与之对应时,系统都会产生一个相应的该异常类的对象。

异常处理类

由于Java语言中定义了很多异常类,而每个异常类都代表一种运行错误,所以说,Java语言的异常类是处理运行时错误的特殊类,类中包含了该运行错误的信息和处理错误的方法等内容。

Throwable继承类有java.lang.Error和java.lang.Exception

即Error类及子类的对象是由Java虚拟机生成并抛出给系统,这种错误有内存溢出错栈溢出错动态链接错等。

  • 异常类的构造方法
1
2
public Exception();
public Exception(String s);
  • 异常类的常用方法
1
2
public String toString();	//返回描述当前Excrption类信息的字符串
public void printStackTrace(); //该方法没有返回值,它的功能是完成一个输出操作,在当前的标准输出设备(一般是屏幕显示器)上输出当前异常对象的堆栈使用轨迹,即程序先后调用并执行了哪些对象或类的哪些方法,使得运行过程中产生了这个异常对象。

RuntimeException可以不编写异常处理的程序代码,依然可以成功编译,因为它是在程序运行时才有可能产生,如除数为0异常、数组下标越界异常、空指针异常等。这类异常应通过程序调试尽量避免而不是使用try-catch-finally语句去捕获它。
除RuntimeException之外,其他则是非运行时异常,这种异常经常是在程序运行过程中由环境原因造成的异常,如输入输出异常、网络地址不能打开、文件未找到等。这类异常必须在程序中使用try-catch-finally语句去捕获它并进行相应的处理,否则编译不能通过。

程序对错误与异常的处理方式有三种:一是程序不能处理的错误;二是程序应避免而可以不去捕获的运行时异常;三是必须捕获的非运行时异常。

异常的处理

在Java语言中,异常处理是通过trycatchfinallythrowthrows五个关键字来实现的。

try-catch-finally语句来捕获和处理一个或多个异常

1
2
3
4
5
6
7
try {
要检查的语句序列
} catch(异常类名 形参对象名) {
异常发生时的处理语句序列
} finally {
一定会运行的语句序列
}

多异常处理是通过在一个try块后面定义若干个catch块来实现的,每个catch块用来接收和处理一种特定的异常对象。

一般地,将处理较具体、较常见异常的catch块应放在前面。
当catch块中含有System.exit(0)语句时,则不执行finally块中的语句,程序直接终止;当catch块中含有return语句时则执行完finally块中的语句后再终止程序。

抛出异常

系统自动抛出的异常
指定方法抛出异常

抛出异常的方法

方式一:在方法体内使用throw语句抛出异常对象。

1
throw 由异常类所产生的对象;

方式二:在方法头部添加throws子句表示方法将抛出异常。

1
[修饰符]返回值类型方法名([参数列表])throws异常类列表

处理异常的方法

由一个方法抛出异常后,该方法内又没有处理异常的语句,则系统就会将异常向上传递,由调用它的方法来处理这些异常,若上层调用方法中仍没有处理异常的语句,则可以再往上追溯到更上层,这样可以一层一层地向上追溯,一直可追溯到main()方法,这时JVM肯定要处理的,这样编译就可以通过了。也就是说,如果某个方法声明抛出异常,则调用它的方法必须捕获并处理异常,否则会出现错误。

由方法抛出异常交系统处理

对于程序需要处理的异常,一般编写try-catch-finally语句捕获并处理,而对于程序中无法处理必须交由系统处理的异常,由于系统直接调用的是主方法main(),所以可以在主方法头使用throws子句声明抛出异常交由系统处理。如下面的程序,编译能通过,运行也没问题。

针对IOException类的异常处理,编写的方式也有三种:

  • 直接由主方法main()抛出异常,让Java默认的异常处理机制来处理,即若在主方法main()内没有使用try-catch语句捕获异常 , 则 必 须 在 声 明 主 方 法 main()头 部 的 后 面 加 上 throws IOException子句;
  • 在程序代码内编写try-catch语句来捕获由系统抛出的异常,如此则不用指定main()throws IOException抛出异常了;
  • 既在main()方法头的后面使用throws IOException抛出异常,也可以在程序中使用try-catch语句来捕获由系统抛出的异常。

自动关闭资源的try语句

try-with-resources语句,也称为自动资源管理语句。

自动关闭资源的try语句相当于包含了隐式的finally语句块,该finally语句块会自动调用res.close()方法关闭前面所访问的资源。
如果在try-with-resources语句中含有catch和finally子句,则catch和finally子句将会在try-with-resources语句中打开的资源被关闭之后得到调用。

java.io.Closeable接口继承AutoCloseable接口,这两个接口被所有的I/O流类实现。因此,在使用I/O流时,可以使用try-with-resources语句。

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Scanner;
public class APP9_7 {
public static void main(String[] args) throws IOException {
try(Scanner in = new Scanner(Path.get("chapter9\\t.txt"))) {
while(in.hasNext()) {
System.out.println(in.nextLine());
}
}
}
}

自定义异常类

(1)用户自定义的异常类必须是Throwable类的直接或间接子类。
(2)为用户自定义的异常类定义属性和方法,或覆盖父类的属性和方法,使这些属性和方法能够体现该类所对应的错误信息。习惯上是在自定义异常类中加入两个构造方法,分别是没有参数的构造方法和含有字符串型参数的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class CircelException extends Exception {
double radius;
CircleException(double r) {
radius = r;
}
public String toString() {
return "半径 r = " + radius + "不是一个正数";
}
}

class Circle {
private double radius;
public void setRadius(double r) throws CircelException {
if(r < 0)
throw new CircelException(r);
else
radius = r;
}

public void show() {
System.out.println("圆面积 = " + 3.14 * radius * radius);
}
}

public class App9_8 {
public static void main(String[] args) {
Circle cir = new Circle();

try {
cir.setRadius(-2.0);
} catch (CircleException e) {
System.out.println("自定义异常:" + e.toString() + "");
}

cir.show();
}
}

对异常处理的两种方式:

  • 在方法内使用try-catch语句来处理方法本身所产生的异常。
  • 如果不想再当前方法中使用try-catch语句来处理异常,可以在方法声明的头部使用throws语句或者在方法内部使用throw语句将它送往上一层调用机构去处理。

本章小结

  • 异常类可分为两大类,分别为java.lang.Exception与java.lang.Error类。
  • 程序代码没有编写处理异常时,Java语言的默认异常处理机制是:(1)抛出异常;(2)停止程序的执行。
  • 当异常发生时,有两种处理方式:(1)交由Java语言默认的异常处理机制做处理;(2)自行编写try-catch-finally语句块来捕获异常。
  • try语句块若有异常发生时,程序的运行便会中断,抛出”由异常类所产生的对象”,并按下列步骤来运行:
    (1)抛出的对象如果属catch()括号内所欲捕获的异常类,catch会捕获此异常,然后进到catch语句块内继续运行;
    (2)无论try语句块是否捕获到异常,或者捕获到的异常是否与catch()括号里的异常类相匹配,最后一定会运行finally语句块里的程序代码;
    (3)finally块运行结束后,程序转到try-catch-finally语句之后的语句继续运行。
  • RuntimeException不编写异常处理的程序代码,仍然可以编译成功,它是在程序运行时才有可能发生;而IOException一定要进行捕获处理才可以,它通常用来处理与输入输出有关的操作。
  • catch()括号内只接收由Throwable类的子类所产生的对象,其他的类均不接收。
  • 抛出异常有下列两种方式:(1)系统自动抛出异常;(2)指定方法抛出异常。
  • 方法中没有使用try-catch语句来处理异常,可在方法声明的头部使用throws语句或在方法内部使用throw语句将它送往上一层调用机构去处理。即如果一个方法可能会抛出异常,则可将处理此异常的try-catch-finally语句写在调用此方法的程序块内。
  • 自动关闭资源语句try-with-resources,只能关闭实现了java.lang.AutoCloseable接口的资源。

课后习题

  • 什么是异常?简述Java语言的异常处理机制。
  • Throwable类的两个直接子类Error和Exception的功能各是什么?用户可以捕获到的异常是哪个类的异常?
  • Exception类有何作用?Exception类的每个子类对象代表了什么?
  • 什么是运行时异常?什么是非运行时异常?
  • 抛出异常有哪两种方式?
  • 在捕获异常时,为什么要在catch()括号内有一个变量e?
  • 在异常处理机制中,用catch()括号内的变量e接收异常类对象的步骤有哪些?
  • 在什么情况下,方法的头部必须列出可能抛出的异常?
  • 若try语句结构中有多个catch()子句,这些子句的排列顺序与程序执行效果是否有关?为什么?
  • 什么是抛出异常?系统定义的异常如何抛出?用户自定义的异常又如何抛出?
  • 自动关闭资源语句,为什么只能关闭实现java.lang.AutoCloseable接口的资源?
  • 系统定义的异常与用户自定义的异常有何不同?如何使用这两类异常?

第十章 Java语言的输入输出与文件处理

Java语言的输入和输出

流的概念

流(stream)是指计算机各部件之间的数据流动。
按照传输方向:输入流、输出流。
按照流的内容:字节流(8byte)、字符流(16Unicode)。

输入输出流

数据流(datastream):把不同类型的输入输出源(键盘、屏幕、文件、网络等)抽象为流,而其中输入或输出的数据。

输入流(input stream):将数据从外设或外存(如键盘、鼠标、文件等)传递到应用程序的流称为输入流。

输出流(output stream):将数据从应用程序传递到外设或外存(如屏幕、打印机、文件等)的流称为输出流。

流式输入输出的最大特点是数据的获取和发送是沿着数据序列顺序进行,每一个数据都必须等待排在它前面的数据读入或送出之后才能被读写,每次读写操作处理的都是序列中剩余的未读写数据中的第一个,而不能随意选择输入输出的位置。

缓冲流

为了提高数据的传输效率,通常使用缓冲流(bufferedstream),即为一个流配有一个缓冲区(buffer),这个缓冲区就是专门用于传送数据的一块内存。

当向一个缓冲流写入数据时,系统将数据发送到缓冲区,而不是直接发送到外部设备。

缓冲流提高了内存与外部设备之间的数据传输效率。

输入输出流类库

根据输入输出数据类型的不同,输入输出流按处理数据的类型分为两种:一种是字节流(byte stream);另一种是字符流(character stream)

字节流:字节流每次读写8位二进制数,由于它只能将数据以二进制的原始方式读写,而不能分解、重组和理解这些数据,所以可以使之变换、恢复到原来的有意义的状态,因此字节流又被称为二进制字节流(binary byte stream)或位流(bits stream)。

字符流:而字符流一次读写16位二进制数,并将其作为一个字符而不是二进制位来处理。

在java.io包中有四个基本类:InputStream、OutputStream及Reader、Writer类,它们分别处理字节流和字符流。

其中InputStream、OutputStream、Reader与Writer是抽象类,用于数据流的输入输出;File是文件类,用于对磁盘文件与文件夹的管理;RandomAccessFile是随机访问文件类,用于实现对磁盘文件的随机读写操作。

在流的输入输出操作中InputStream和OutputStream流类通常用来处理”位流”(bit stream)即字节流,这种流通常被用来读写诸如图片、音频、视频之类的二进制数据,也就是二进制文件,但也可以处理文本文件;而Reader与Writer类则是用来处理”字符流”(character stream),也就是文本文件。

使用InputStream和OutputStream流类

InputStream和OutputStream流类是Java语言中用来处理以位(bit)为单位的流,它除了可用来处理二进制文件(binary file)的数据之外,也可用来处理文本文件。

基本的输入输出流类

InputStream流类

常用方法 功能说明
public in read() 从输入流中的当前位置读入一个字节(8b)的二进制数据,然后以此数据为低位字节,配上8个全0的高位字节合成一个16位的整型量(0~255)返回给调用此方法的语句,若输人流中的当前位置没有数据,则返回-1
public int read(byte[] b) 从输人流中的当前位置连续读入多个字节保存在数组b中,同时返回所读到的字节数
public int read(byte[] b, int off, int len) 从输入流中的当前位置连续读入len个字节,从数组b的第off+1个元素位置处开始存放,同时返回所读到的字节数
public int available() 返回输入流中可以读取的字节数
public long skip(long n) 使位置指针从当前位置向后跳过n个字节
public void mark(int readlimit) 在当前位置处做一个标记,并且在输人流中读取readlimit个字节数后该标记失效
public void reset() 将位置指针返回到标记的位置
public void close() 关闭输人流与外设的连接并释放所占用的系统资源

当Java程序需要从外设如键盘、磁盘文件等读入数据时,应该创建一个适当类型的输入流对象来完成与该外设的连接 。由于InputStream是抽象类 ,所以程序中创建的输入流对象一 般是InputStream某个子类的对象,通过调用该对象继承的read()方法就可实现对相应外设的输入操作。

OutputStream流类

常用方法 功能说明
public void write(int b) 将参数b的低位字节写人到输出流
public void write(byte[] b) 将字节数组b中的全部字节按顺序写人到输出流
public void write(byte[] b, int off, int len) 将字节数组b中第off+1个元素开始的len个数据,顺序地写人到输出流
public void flush() 强制清空缓冲区并执行向外设写操作
public void close() 关闭输出流与外设的连接并释放所占用的系统资源

当Java程序需要向外设如屏幕、磁盘文件等输出数据时,应该创建一个适当类型的输出流的对象来完成与该外设的连接。

输入输出流的应用

文件输入输出流

FileInputStreamFileOutputStream分别是InputStreamOutputStream的直接子类,这两个子类主要是负责完成对本地磁盘文件的顺序输入与输出操作的流。

  • FileInputStream类的构造方法
常用方法 功能说明
public FileInputStream(String name) 以名为name的文件为数据源建立文件输入流
public FileInputStream(File file) 以文件对象file为数据源建立文件输入流
public FileInputStream(FileDescriptor fdObj) 以文件描述符对象fdObj为输人端建立一个文件输人流
  • FileOutputStream类的构造方法
常用方法 功能说明
public FileOutputStream(String name) 以指定名字的文件为接收端建立文件输出流
public FileOutputStream(String name, boolean append) 以指定名字的文件为接收端建立文件输出流,并指定写人方式,append为true 时输出字节被写到文件的末尾
public FileOutputStream(File file) 以文件对象file为接收端建立文件输出流
public FileOutputStream(FileDescriptor fdObj) 以文件描述符对象fdObj建立一个文件输出流

FileDescriptor是java.io包中定义的另一个类,该类不能实例化,该类中有三个静态成员:in、out和err,分别对应于标准输入流、标准输出流和标准错误流,利用它们可以在标准输入流和标准输出流上建立文件输入输出流,实现键盘输入或屏幕输出操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//创建文本文文件myfile.txt,从键盘输入一串字符,然后再读取该文件并将文本文件内容显示在屏幕上。

import java.io.*;
class App10_1 {
public static void main(String[] args) {
char ch;
int data;

try(
FileInputStream fin = new FileInputStream(FileDescriptor.in);
FileOutputStream fout = new FileOutputStream("D:/myfil.txt");
){
System.out.println("请输入一串字符,并以 # 结束");
while ((ch = (char)fin.read()) != '#') {
fout.write(ch);
}
} catch (FileNotFoundException e) {
System.out.println("文件没有找到!");
} catch (IOException e) {
}

try (
FileInputStream fin = new FileInputStream("D:/myfile.txt");
FileOutputStream fout = new FileOutputStream(FileDescriptor.out);
){
while (fin.avilable() > 0) {
data = fin.rad();
fout.writ(data);
}
} catch (IOException e) {
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//FileInputStream和FileOutputStream实现二进制图像复制
import java.io.*;
public class APP10_2 {
try(
FileInputStream fi = new FileInputStream("test.jpg");
FileOutputStream fo = new FileOutputStream("test_copy.jpg");
) {
System.out.println("文件大小 = " + fi.available());
byte[] b = new byte[fi.available()];
fi.read(b);
fo.write(b);
System.out.println("文件已经被复制");
}
}

顺序输入流

顺序输入流类SequenceInputStream是InputStream的直接子类,其功能是将多个输入流顺序连接在一起,形成单一的输入数据流,没有对应的输出数据流存在。
在进行输入时,顺序输入流依次打开每个输入流并读取数据,在读取完毕后将该流关闭,然后自动切换到下一个输入流。也就是说,由多个输入流构成的顺序输入流,当从一个流中读取数据遇到EOF时,SequenceInputStream将自动转向下一个输入流,直到构成SequenceInputStream类的最后一个输入流读取到EOF时为止。

  • SequenceInputStream类的构造方法
构造方法 功能说明
public SequenceInputStream(Enumeration e) 创建一个串行输入流,链接枚举对象e中的所有输入流
public SequenceInputStream(InputStream s1, InputStream s2) 创建一个串行输入流,链接输入流s1和s2
  • SequenceInputStream类的常用方法
常用方法 功能说明
public int available() 返回流中的可读取的字节数
public void close() 关闭输入流
public int read() 从输入流中读取字节,遇到EOF就转向下一输入流
public int read(byte[] b, int off, int len) 将len个数据读到一个字节数组从off开始的位置

管道输入输出流

管道字节输入流PipedInputStream和管道字节输出流PipedOutputStream类提供了利用管道方式进行数据输入输出管理的类。

管道流用来将一个程序或线程的输出连接到另外一个程序或线程作为输入,使得相连线程能够通过PipedInputStream和PipedOutputStream流进行数据交换,从而可以实现程序内部线程间的通信或不同程序间的通信。

PipedInputStream是一个通信管道的接收端,它必须与一个作为发送端的PipedOutputStream对象相连;PipedOutputStream是一个通信管道的发送端,它必须与一个作为接收端的PipedInputStream对象相连。

  • 构造方法

PipedInputStream(PipedOutputStream src),创建一个管道字节输入流,并将其连接到src指定的管道字节输出流。
PipedOutputStream(PipedInputStream src),创建一个管道字节输出流,并将其连接到src的管道字节输入流。

  • PipedInputStream常用方法
常用方法 功能说明
public int available() 返回可以读取的字节数
public void close() 关闭管道输入流并释放系统资源
public int read() 从管道输入流中读取下一字节数据
public int read(byte[] b, int off, int len) 从管道输入流读取len字节数据到数组
protected coid receive(int b) 从管道中接受1字节数据
public void connect(PipedOutputStream src) 连接到指定输出流,管道输入流将从该输出流接受数据
  • PipedOutputStream常用方法
常用方法 功能说明
public void close() 关闭管道输出流并释放系统资源
public void connect(PipedInputStream snk) 连接到指定输入流,管道输出流将从该输入流读取数据
public void write(int b) 写指定字节数据到管道输出流
public void wirte(byte[] b, int off, int len) 从数组off偏移处写len字节数据到管道输出流
public void flush() 刷新输出流并使缓冲区数据全部写出

过滤输入输出流

过滤字节输入流类FilterInputStream和过滤字节输出流类FilterOutputStream,分别实现了在数据的读、写操作的同时进行数据处理,它们是InputStream和OutputStream类的直接子类 。

FilterInputStream和FilterOutputStream也是两个抽象类,它们又分别派生出数据输入流类DataInputStream和数据输出流类DataOutputStream等子类。可以按照基本数据理性进行数据读写。

过滤字节输入输出流的主要特点是,过滤字节输入输出流是建立在基本输入输出流之上,并在输入输出数据的同时能对所传输的数据做指定类型或格式的转换,即可实现对二进制字节数据的理解和编码转换。

FileInputStream类的对象是1字节输入流,每次输入1字节。与DataInputStream类的对象串接后每次可直接读取一个int(4字节)型数据。

  • 构造方法

DataInputStream(InputStream in),建立一个新的数据输入流,从指定的输入流in读数据。
DataOutputStream(OutputStream out),建立一个新的数据输出流,向指定的输出流out写数据。

  • DataInputStream常用方法
常用方法 功能说明
public boolean readBoolean() 从流中读1字节,若字节值非0返回true,否则返回false
public byte readByte() 从流中读1字节,返回该字节值
public char readChar() 从流中读取a、b2字节,形成Unicode字符(char)((a<<8) | (b &.0xff))
public short readShort() 从流中读人2字节的short值并返回
public int readInt() 从流中读人4字节的int值并返回
public float readFloat() 从流中读人4字节的float值并返回
public long readLong() 从流中读入8字节的long值并返回
public double readDouble() 从流中读人8字节的double值并返回
  • DataOutputStream常用方法
常用方法 功能说明
public boolean writeBoolean(boollean v) 若v的值为true,则向流中写人(字节)1,否则写入(字节)0
public byte writeByte(int v) 向流中写人1字节。写人v的最低1字节,其他字节丢弃
public char writeChar(int v) 向流中写人v的最低2字节,其他字节丢弃
public short writeShort(int v) 向流中写人v的最低2字节,其他字节丢弃
public int writeInt(int v) 向流中写人参数v的4字节
public float writeFloat(float v) 向流中写入参数v的4字节
public long writeLong(long v) 向流中写人参数v的8字节
public double writeDouble(double v) 向流中写人参数v的8字节

标准输入输出流

当Java程序与外设进行数据交换时,需要先创建一个输入或输出流类的对象,完成与外设的连接。
当程序对标准输入输出设备进行操作时,则不需要如此。

为了方便程序对键盘输入和屏幕输出进行操作,Java系统事先在System类中定义了静态流对象System.in和System.out和System.err 。
System.in对应于输入流,通常指键盘输入设备 ;
System.out对应于输出流,指显示器等信息输出设备;
System.err对应于标准错误输出设备,使得程序的运行错误可以有固定的输出位置,通常该对象对应于显示器。

  • 标准输入
1
2
3
4
System.out.println("按任一键继续");
try {
char test = (char)System.in.read();
} catch(IOException e) {}
  • 标准输出

PrintStream类是过滤字节输出流类FilterOutputStream的一个子类,其中定义了向屏幕输送不同类型数据的方法print()和println()。这两个方法的区别是前者输出数据后不换行,后者换行。System.out对应的输出流通常指显示器、打印机或磁盘文件等信息输出设备。

  • 标准错误输出

由PrintStream类派生的错误流。err使用与out同样的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//数据流的应用
import java.io.*;
public class App10_4 {
public static void main(String[] artg) {
try {
byte[] b = new byte[128];

System.out.print("请输入字符:");
int count = System.in.read(b);

System.out.println("输入的是:");
for (int i=0; i<count; i++) {
System.out.print(b[i] + " ");
}
System.out.println();

for (int i=0; i<count-2; i++) {
System.out.print((char)b[i] + " ");
}
System.out.println();

System.out.println("输入的字符个数为" + count);

Class InClass = System.in.getClass();
Class OutClass = System.out.getClass();

System.out.println("in所在的类是:" + InClass.toString());
System.out.println("out所在的类是:" + OutClass.toString());
} catch (IOException e) { }
}
}
1
2
3
4
5
6
7
请输入字符:abc↙
输入的是:
97 98 99 13 10
a b c
输入的字符个数为5
in所在的类是:class java.io.BufferedInputStream
out所在的类是:class java.io.PrintStream

使用Reader和Writer流类

而Reader和Write类则是用来处理“字符流”的,也就是文本文件。为抽象类。

  • Reader类常用方法
常用方法 功能说明
public int read() 从输人流中读一个字符
public int read(char[] cbuf) 从输入流中读最多cbuf.length个字符,存人字符数组cbuf中
public int read(char[] cbuf, int off, int len) 从输入流中读最多len个字符,存入字符数组cbuffer中,从off开始的位置
public long skip(long n) 从输人流中最多向后跳n个字符
public boolean ready() 判断流是否做好读的准备
public void mark(int readAheadLimit) 标记输人流的当前位置
public boolean markSupported() 测试输人流是否支持mark
public void reset() 重定位输人流
public void close() 关闭输人流
  • Writer类常用方法
常用方法 功能说明
public void write(int c) 将单一字符c输出到流中
public void wirte(String str) 将字符串str输出到流中
public void write(char[] cbuf) 将字符数组cbuf输出到流
public void write(char[] cbuf, int off, int len) 将字符数组按指定的格式输出(off表示索引,len表示写入的字符数)到流中
public void flush() 将缓冲区中的数据写到文件中
public void close() 关闭输出流

使用FileReader类读取文件

Reader -> InputStreamReader -> FileReader

  • 构造方法
构造方法 功能说明
public FileReadeer(String name) 根据文件名称创建一个可读取的输入流对象
  • 使用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
public class App10_5 {
public static void main(String[] args) throws IOException {
char[] c = new char[500];
try (
FileReader fr = new FileReader("D:/test.txt");
){
int num = fr.read();
String srt = new String(c, 0, num);
System.out.println("读取的字符个数为:" + num + ",其内容如下:");
System.out.println(str);
}
}
}

使用FileWriter类写入文件

Writer -> OutputStreamWriter -> FileWriter

  • 构造方法
构造方法 功能说明
public FileWriter(String filename) 根据所给文件名创建一个可供写入字符数据的输出流对象,原先的文件会被覆盖
public FileWriter(String filename, boolean a) 同上,但如果a设置为true,则会将数据追加在原先文件的后边
  • 使用案例
1
2
3
4
5
6
7
8
9
10
11
import java.io.*;
public class App10_6 {
public static void main(String[] args) throws IOException {
FileWirter fw = new FileWriter("d:\\test.txt");
char[] c = {'H', 'e', 'l', 'l', 'o', '\r', '\n'};
String str = "欢迎使用Java!";
fw.write(c);
fw.wirte(str);
fw.close();
}
}

使用BufferedReader类读取文件

使用BufferedReader类来读取缓冲区中的数据之前,必须先创建FileReader类对象,再以该对象为参数来创建BufferedReader类的对象,然后才可以利用此对象来读取缓冲区中的数据。

  • 构造方法
构造方法 功能说明
public BufferReader(Reader in) 创建缓冲区字符输入流
public BufferReader(Reader in, int size) 创建缓冲区字符输入流,并设置缓冲区大小
  • 常用方法
常用方法 功能说明
public int read() 读取单一字符
public int read(char[] cbuf) 从流中读取字符并写入到字符数组cbuf中
public int read(char[] cbuf, int off, int len) 从流中读取字符存放到字符数组cbuf中(off表示数组下标,len表示读取的字符数)
public long skip(long n) 跳过n个字符不读取
public String readLine() 读取一行字符串
public void close() 关闭流
  • 使用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
public class App10_7 {
public static void main(String[] args) throws IOException {
String thisLine;
int count = 0;
try(
FileReader fr = new FileReader("D:/test.txt");
BufferReader bfr = new BufferReader(fr);
) {
while ((thisLine = bfr.readLine()) != null) {
count++;
Sytem.out.println(thisLine);
}
System.out.println("共读取了" + count + "行");
} catch (IOException ioe) {
System.out.println("错误!" + ioe);
}
}
}

使用BufferedWriter类写入文件

缓冲字符输出流类BufferedWriter继承自Writer类,BufferedWriter类是用来将数据写入到缓冲区中。

缓冲区内的数据最后必须要用flush()方法将缓冲区清空,也就是将缓冲区中的数据全部写到文件内。

  • 构造方法
构造方法 功能说明
public BufferWriter(Writer out) 创建缓冲区字符输出流
public BufferWriter(Writer out, int size) 创建缓冲区字符输出流,并设置缓冲区大小
  • 常用方法
常用方法 功能说明
public void write(int c) 将单一字符写入缓冲区中
public void write(char[] cbuf, int off, int len) 将字符数组cbuf按指定的格式写入到输出缓冲区中(off表示数组下标,len表示写入的字符数)
public void write(String str, int off, int len) 写入字符串(off表示下标,len表示写入的字符数)
public void newLine() 写入回车换行字符
public void flush() 将缓冲区中的数据写到文件中
public void close() 关闭流
  • 使用案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//文件复制
import java.io.*;
public class App10_8 {
public static void main(String[] args) throws IOException {
String str = new String();
try (
BufferReader in = new BufferReader(new FileReader("d:/tset.txt"));
BufferWriter out = new BufferWriter(new FileWriter("d:/test_copy.txt"));
) {
while ((str=in.readLine()) != null) {
System.out.println(str);
out.writer(str);
out.newLine();
}
out.flush();
} catch (IOException ioe) {
System.out.println("错误!" + ioe);
}
}
}

文件的管理与随机访问

Java语言对文件与文件夹的管理

在java.io包中定义了一个File类专门用来管理磁盘文件和文件夹,而不负责数据的输入输出。

创建File类的对象

  • 构造方法
构造方法 功能说明
public File(String path) 用Path参数创建File对象对应的磁盘文件夹或文件夹名及其路径
public File(String path, String name) 以path为路径,以name为文件或文件夹名创建File对象
public File(File dir, String name) 用一个已经存在代表某磁盘文件夹的File对象dir作为文件夹,以name作为文件或文件夹名来创建File对象

path参数可以是绝对路径,如“d:\java\myfile\sample.java”,也可相对路径,如“myfile\sample.java”, path参数还可以是磁盘上的某个文件夹。
由于不同的操作系统使用的文件夹分隔符不同,如Windows操作系统使用反斜线“\”,UNIX操作系统使用正斜线“/”。
File类的一个静态变量File.separator代表不同操作系统下通用路径。"d:"+File.separator+"java"+File.separator+"myfile"

获取文件或文件夹属性

  • 获取文件或文件夹属性常用方法
常用方法 功能说明
public boolean exists() 判断文件或文件夹是否存在
public boolean isFile() 判断对象是否代表有效文件
public boolean isDirectory() 判断对象是否代表有效文件夹
public String getName() 返回文件名或文件夹名
public String getPath() 返回文件或文件夹的路径
public long length() 返回文件的字节数
public boolean canRead() 判断文件是否可读
public boolean canWrite() 判断文件是否可写
public String[] list() 将文件夹中所有文件名保存在字符串数组中返回
public boolean equals(File f) 比较两个文件或文件夹是否相同

文件或文件夹操作

  • 文件或文件夹操作的常用方法
常用方法 功能说明
public boolean renameTo(File newFile) 将文件重命名成newFile对应的文件名
public boolean delete() 将当前文件删除,若删除成功返回true,否则返回false
public boolean mkdir() 创建当前文件夹的子文件夹,若成功返回ture,否则返回false

对文件的随机访问

随机访问文件类RandomAccessFile,它可以实现对文件的随机读写。

RandomAccessFile是有关文件处理中功能齐全、文件访问方法众多的类。RandomAccessFile类用于进行随意位置、任意类型的文件访问,并且在文件的读取方式中支持文件的任意读取而不只是顺序读取。

  • RandomAccessFile构造方法
构造方法 功能说明
public RandomAccessFile(String name, String mode) 以name来指定随机文件流对象所对应的文件名,以mode表示对文件的访问模式
public RandomAccessFile(File file, String mode) 以file来指定随机文件流对象所对应的文件名,以mode表示对文件的访问模式

r:只读方式打开文件。
rw:读写方式打开文件。

  • RandomAccessFile中用于读取操作的常用方法
常用方法 功能说明
public void close() 关闭随机访问文件流并释放系统资源
public final FileDescriptorgetFD() 获取文件描述符
public long getFilePointer() 返回文件指针的当前位置
public long length() 返回文件长度
public int skipBytes(int n) 跳过输人流中n个字符,并返回跳过实际的字节数
public int read() 从文件输入流中读取一个字节的数据
public int read(byte[] b, int off. int len) 从文件输人流的当前指针位置开始读取长度为len字节的数据存放到字节数组b中,存放的偏移位置为off。若遇文件结束符,则返回值为-1
public final void readFully(byte[] b) 从文件输入流的当前指针位置开始读取b.length字节的数据存放到字节数组b中。若遇文件结束符,则抛出EOFException类异常
public final void readFully(byte[] b, int off, int len) 从文件输入流的当前指针位置开始读取长度为len字节的数据存放到字节数组b中,存放的偏移位置为off。若遇文件结束符,则抛出EOFException类异常
public final boolean readBoolean() 读取文件中的逻辑值
public final byte readByte() 从文件中读取带符号的字节值
public final char readChar() 从文件中读取一个Unicode字符
public final String readLine() 从文本文件中读取一行
public void seek(long pos) 设置文件指针位置
  • RandomAccessFile类用于写入操作的常用方法
常用方法 功能说明
public void write(int b) 在文件指针的当前位置写人一个int型数据b
public void writeBoolean(boolean v) 在文件指针的当前位置写人一个boolean型数据v
public void writeByte(int v) 在文件指针的当前位置写人一个字节值,只写v的最低1字节,其他字节丢弃
public void writeBytes(String s) 以字节形式写一个字符串到文件
public void writeChar(int v) 在文件指针的当前位置写入v的最低2字节,其他丢弃
public void writeChars(String s) 以字符形式写一个字符串到文件
public void writeDouble(double v) 在文件当前指针位置写人8字节数据v
public void writeFloat(float v) 在文件当前指针位置写人4字节数据v
public void writeInt(int v) 把整型数作为4字节写人文件
public void writeLong(long v) 把长整型数作为8字节写人文件
public void writeShort(int v) 在文件指针的当前位置写人2字节,只写v的最低2字节,其他字节丢弃
public void writeUTF(String str) 作为UTF格式向文件写入一个字符串

本章小结

  • Java语言是以流的方式来处理输入输出的,其好处是:无论是什么形式的输入输出,只要针对流做处理就可以了。
  • Java语言中的流是由字符或位组合而成的,可以通过它来读写数据,甚至可以通过它连接数据源,并可以将数据以字符或位组合的形式保存。
  • 以数据的读取或写入而言,流可分为输入流与输出流两种。
  • 可以通过InputStream、OutputStream、Reader与Writer类来处理流的输入输出。
  • InputStream与OutputStream类及其子类既可用于处理二进制文件也可用于处理文本文件,但主要以处理二进制位流的字节文件为主。
  • Reader与Writer类是用来处理文本文件的读取和写入操作,通常是以它们的派生类来创建实体对象,再利用它们来处理文本文件读写操作。
  • BufferedWriter类中的newLine()方法可写入回车换行字符,而且与操作系统无关,使用它可确保程序可跨平台运行。
  • 文件流类File的对象对应系统的磁盘文件或文件夹。
  • 随机访问文件类RandomAccessFile,可以实现对文件的随机读写。
  • 在关闭流对象时,若流对象是在try语句块之前定义的,则流对象的关闭最好是放在finally语句块中;但若流对象是在try语句块中定义,那么关闭流对象的语句可放在try语句块的最后面。

课后习题

  • 什么是数据的输入与输出?
  • 什么是流?Java语言中分为哪两种流?这两种流有何差异?
  • InputStream、OutputStream、Reader和Writer四个类在功能上有何异同?
  • 利用基本输入输出流实现从键盘上读入一个字符,然后显示在屏幕上。
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;
public class D10_4 {
public static void main(String[] args) {
try (
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
) {
String str = br.readLine();
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 顺序流与管道流的区别是什么?
  • Java语言中定义的三个标准输入输出流是什么?它们对应什么设备?
  • 利用文件输出流创建一个文件file1.txt,写入字符”文件已被成功创建!”,然后用记事本打开该文件,看一下是否正确写入。
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;
public class D10_7 {
public static void main(String[] args) {
try (
BufferedWriter writer = new BufferedWriter(new FileWriter("c:" + File.separator + "file1.txt"));
) {
writer.write("文件已被成功创建!");
writer.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 利用文件输入流打开10.7题中创建的文件file1.txt,读出其内容并显示在屏幕上。
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;
public class D10_8 {
public static void main(String[] args) {
try (
BufferedReader br = new BufferedReader(new FileReader("c:" + File.separator + "file1.txt"));
) {
String str = br.readLine();
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 利用文件输入输出流打开10.7题创建的文件file1.txt,然后在文件的末尾追加一行字符串”又添加了一行文字!”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
public class D10_9 {
public static void main(String[] args) {
try (
BufferedWriter writer = new BufferedWriter(new FileWriter("c:" + File.separator + "file1.txt", true));
) {
writer.newLine();
writer.write("又添加了一行文字!");
writer.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
  • 产生15个20~9999的随机整数,然后利用BufferedWriter类将其写入文件file2.txt中之后再读取该文件中的数据并将它们按升序排序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.*;
import java.util.*;

public class D10_10{
public static void main(String[] args) {
int[] input = new int[15];
Random rad = new Random();
System.out.println("随机生成数组:");
for (int i=0; i<15; i++) {
input[i] = rad.nextInt(9979) + 20;
System.out.print(input[i] + " ");
}

System.out.println("---------");

try(
BufferedWriter bw = new BufferedWriter(new FileWriter("c:" + File.separator + "file2.txt"));
) {
for (int i=0; i<15; i++) {
bw.write(String.valueOf(input[i]));
bw.newLine();
}
bw.flush();
bw.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("随机数组写入文件。");

System.out.println("---------");

int[] output = new int[15];
try(
BufferedReader br = new BufferedReader(new FileReader("c:" + File.separator + "file2.txt"));
) {
System.out.println("从文件中读出数组:");
for (int i=0; i<15; i++) {
output[i] = Integer.parseInt(br.readLine());
System.out.print(output[i] + " ");
}
br.close();
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("---------");

Arrays.sort(output);

System.out.println("排序后的数组:");
for (int i=0; i<15; i++) {
System.out.print(output[i] + " ");
}
}
}
  • Java语言中使用什么类来对文件与文件夹进行管理?

第十一章 多线程

多任务(multitasking)
分时(timesharing)

线程的概念

多线程(multithread)是指在同一个进程中同时存在几个执行体,按几条不同的执行路径同时工作的情况。
多线程编程的含义就是可将一个程序任务分成几个可以同时并发执行的子任务。

程序、进程、多任务与多线程

程序-program

程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

进程-porcess

进程是程序的一次执行过程,是系统运行程序的基本单位。

系统运行一个程序即是一个进程从创建、运行到消亡的过程。

每个进程还占有某些系统资源,如CPU时间、内存空间、文件、输入输出设备的使用权等。

每个进程之间是独立的,除非利用某些通信管道来进行通信,或是通过操作系统产生交互作用,否则基本上各进程不知道(不需要,也不应该知道)彼此的存在。

多任务-multi task

多任务是指在一个系统中可以同时运行多个进程,即有多个独立运行的任务,每一个任务对应一个进程。

所谓同时运行的进程,其实是指由操作系统将系统资源分配给各个进程,每个进程在CPU上交替运行。

多线程-thread

对于完全不相关的程序而言,在同时执行时,彼此的进程也不会做数据交换的工作,而可以完全独立地运行。

进程只是资源分配的单位,线程是处理器调度的基本单位。

一个进程包含一个以上线程,一个进程中的线程只能使用该进程的资源和环境。

线程不能独立存在,必须存在于进程中。

线程也被称为负担轻的进程(light-weight process)

CPU在同一时间段内执行一个程序中的多个程序段来完成工作。

多线程就是同时执行一个以上的线程,执行一个线程不必等待另一个线程执行完后才进行,所有线程都可以发生在同一时刻。但操作系统并没有将多个线程看作多个独立的应用去实现线程的调度和管理以及资源分配。

多任务是针对操作系统而言的,表示操作系统可以同时运行多个应用程序。
多线程是针对一个进程而言的,表示在一个进程内部可以同时执行多个线程。

线程的状态与生命周期

新建状态-newbron

线程对象已经被分配了内存空间和其他资源,并已被初始化,但是该线程尚未被调度。此时的线程可以被调度,变成就绪状态。

就绪状态-runnable

就绪状态也称为可运行状态。

处于新建状态的线程被启动后,将进入线程队列排队等待CPU资源,此时它已具备了运行的条件,也就是处于就绪状态。

原来处于阻塞状态的线程被解除阻塞后也将进入就绪状态。

执行状态-running

每一个Thread类及其子类的对象都有一个重要的run()方法,该方法定义了这一类线程的操作和功能。当线程对象被调度执行时,它将自动调用本对象的run()方法,从该方法的第一条语句开始执行,一直到执行完毕。

处于执行状态的线程在下列情况下将让出CPU的控制权:

  • 线程执行完毕
  • 有比当前线程优先级更高的线程处于就绪状态
  • 线程主动睡眠一段时间
  • 线程在等待某一资源

阻塞状态-blocked

一个正在执行的线程如果在某些特殊情况下,将让出CPU并暂时中止自己的执行,线程处于这种不可执行的状态被称为阻塞状态。

下边两种情况可以使得一个线程进入阻塞状态:

  • 调用sleep()或yield()方法
  • 二是为等待一个条件变量,线程调用wait()方法;
  • 三是该线程与另一线程join()在一起。

一个线程被阻塞时它不能进入排队队列,只有当引起阻塞的原因被消除时,线程才可以转入就绪状态,重新进到线程队列中排队等待CPU资源,以便从原来的暂停处继续执行。
处于阻塞状态的线程通常需要由某些事件才能唤醒,至于由什么事件唤醒该线程,则取决于其阻塞的原因。
处于睡眠状态的线程必须被阻塞一段固定的时间,当睡眠时间结束时就变成就绪状态;因等待资源或信息而被阻塞的线程则需要由一个外来事件唤醒。

消亡状态-dead

处于消亡状态的线程不具有继续执行的能力。

线程消亡的原因:

  • 正常运行的线程完成了它的全部工作,即执行完了run()方法的最后一条语句并退出;
  • 当进程因故停止运行时,该进程中的所有线程将被强行终止。

当线程处于消亡状态、并且没有该线程对象的引用时,垃圾回收器会从内存中删除该线程对象。

线程的优先级与调度

优先级

在多线程系统中,每个线程都被赋予一个执行优先级。优先级决定了线程被CPU执行的优先顺序。

Java语言中线程的优先级从低到高以整数1~10表示,共分为10级。
MIN_PRIORITY:最小优先级,1。
MAX_PRIORITY:最高优先级,10。
NORM_PRIORITY:普通优先级,5。

对应一个新建的线程,系统会遵循如下的原则为其指定优先级。
(1)新建线程将继承创建它的父线程的优先级。父线程是指执行创建新线程对象语句所在的线程,它可能是程序的主线程,也可能是某一个用户自定义的线程。
(2)一般情况下,主线程具有普通优先级。

setPriority()方法可以设置改变线程的优先级。

调度

调度就是指在各个线程之间分配CPU资源。
线程调度有两种模型:分时模型和抢占模型。

分时模型

CPU资源是按照时间片来分配的,获得CPU资源的线程只能在指定的时间片内执行,一旦时间片使用完毕,就必须把CPU让给另一个处于就绪状态的线程,线程本身不会让出CPU。

强占模型

当前活动的线程一旦获得执行权,将一直执行下去,直到执行完或由于某种原因主动放弃执行权。
高优先级的线程应该不时地主动进入”睡眠”状态。

Java的Thread线程类与Runnable接口

实现多线程的方法有两种:继承java.lang包中的Thread类;用户在定义自己的类中实现Runnable接口。

利用Thread类的子类来创建线程

  • Thread类的构造方法
构造方法 功能说明
public Thread() 创建一个线程对象,此线程对象的名称是”Thread-n”的形式,其中n是一个整数。使用这个构造方法,必须创建Thread类的一个子类并覆盖其run()方法
public Thread(String name) 创建一个线程对象,参数name指定了线程的名称
public Thread(Runnable target) 创建一个线程对象,此线程对象的名称是”Thread-n”的形式,其中n是一个整数。参数target的run()方法将被线程对象调用,作为其执行代码
public Thread(Runnable target, String name) 功能同上,参数target的run()方法将被线程对象调用,作为其执行代码。参数name指定了新创建线程的名称
  • Thread类的常用方法
构造方法 功能说明
public static Thread currentThread() 返回当前正在执行的线程对象
public final String getName() 返回线程的名称
public void start() 使该线程由新建状态变为就绪状态。如果该线程已经是就绪状态,则产生IllegalStateException异常
public void run() 线程应执行的任务
public final boolean isAlive() 如果线程处于就绪、阻塞或运行状态,则返回true;如果线程处于新建且没有启动的状态,或已经结束,则返回false
public void interrupt() 当线程处于就绪状态或执行状态时,给该线程设置中断标志;一个正在执行的线程让睡眠线程调用该方法,则可导致睡眠线程发生InterruptedException异常而唤醒自己,从而进入就绪状态
public static boolean isInterrupted() 判断该线程是否被中断,若是返回true,否则返回false
public final void join() 暂停当前线程的执行,等待调用该方法的线程结束后再继续执行本线程
public final int getPriority() 返回线程的优先级
public final void setPriority(int newPriority) 设置线程优先级。如果当前线程不能修改这个线程,则产生SecurityException异常。如果参数不在所要求的优先级范围内,则产生llegalArgumentException异常
public static void sleep(long millis) 为当前执行的线程指定睡眠时间。参数millis是线程睡眠的毫秒数。如果这个线程已经被别的线程中断,则产生InterruptedException异常
public static void yield() 暂停当前线程的执行,但该线程仍处于就绪状态,不转为阻塞状态。该方法只给同优先级线程以执行的机会

要在Thread的子类里激活线程,必须做好以下两件事情:

  • 此类必须是继承自Thread类;
  • 线程所要执行的代码必须写在run()方法内。

run()方法是线程执行的起点,必须通过定义run()方法来为线程提供代码。

1
2
3
4
5
6
7
class 类名 extends Thread {
类里的成员变量;
类里的成员方法;
修饰符 run() {
线程的代码
}
}

run()方法规定了线程要执行的任务,但一般不是直接调用run()方法,而是通过线程的start()方法来启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//利用Thread类的子类来创建线程
class MyThread extends Thread {
private String who;
public MyThread(String str) {
who = str;
}
public void run() {
for(int i=0; i<5; i++) {
try {
sleep((int)(1000*Math.random()));
} catch(InterruptedException e) {
System.out.println(who + "正在运行!");
}
}
}
}
public class App11_1 {
public static void main(String[] args) {
MyThread you = new MyThread("你");
MyThread she = new MyThread("她");
you.start();
she.start();
System.out.println("主方法main()运行结束!");
}
}
1
2
3
4
5
6
7
8
9
10
11
主方法main()运行结束!
你正在运行!
她正在运行!
你正在运行!
你正在运行!
她正在运行!
她正在运行!
你正在运行!
她正在运行!
她正在运行!
你正在运行!

main()方法本身也是一个线程,

用Runnable接口来创建线程

Runnable接口是Java语言中实现线程的接口,定义在java.lang包中,其中只提供了一个抽象方法run()的声明。

但是Runnable接口并没有任何对线程的支持,还必须创建Thread类的实例,这一点通过Thread(Runnable target)类的构造方法来实现。

除了利用Thread类的子类创建线程外,另一种就是直接利用Runnable接口和线程的构造方法Thread(Runnable target)来创建线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//利用Runnable接口来创建线程
class MyThread implements Runnable {
private String who;
public MyThread(String str) {
who = str;
}
public void run() {
for(int i=0; i<5; i++) {
try{
Thread.sleep((int)(1000*Math.random())); //sleep要声明Thread
} catch(InterruptedException e) {
System.out.println(who + "正在运行!");
}
System.out.println(who + "正在运行!");
}
}
}
public class App11_2 {
public static void main(String[] args) {
MyThread you = new MyThread("你");
MyThread she = new MyThread("她");
Thread t1 = new Thread(you);
Thread t2 = new Thread(she);
t1.start();
t2.start();
}
}
public class App11_3 {
public static void main(String[] args) {
MyThread you = new MyThread("你");
MyThread she = new MyThread("她");
Thread t1 = new Thread(you);
Thread t2 = new Thread(she);

t1.start();
try{
you.join();
} catch(InterruptedException e) {
}

t2.start();
try{
she.join();
} catch(InterruptedException e) {
}

System.out.println("主方法main()运行结束!");
}
}

当某一线程调用join()方法时,则其他线程会等到该线程结束后才开始执行。

1
2
3
4
5
6
7
8
9
10
11
你正在运行!
你正在运行!
你正在运行!
你正在运行!
你正在运行!
她正在运行!
她正在运行!
她正在运行!
她正在运行!
她正在运行!
主方法main()运行结束!

直接继承Thread类的优点是编写简单,可以直接操纵线程;缺点是若继承Thread类,就不能再继承其他类。
使用Runnable接口的特点是:可以将Thread类与所要处理的任务的类分开,形成清晰的模型;还可以从其他类继承,从而实现多重继承的功能。

  • 获取线程中的名字

若直接使用继承Thread类的子类:在类中this即指当前线程;
若是使用实现Runnable接口的类:要在此类中获得当前线程的引用,必须使用Thread.currentThread()方法。

当可运行对象包含线程对象时,即线程对象是可运行对象的成员时,则在run()方法中可以通过调用Thread.currentThread()方法来获得正在运行的线程的引用。
当可运行对象不包含线程对象时,在可运行对象run()方法中需要使用语句Thread.currentThread().getName()来返回当前正在运行线程的名字。

线程间的数据共享

同一进程的多个线程间可以共享相同的内存单元,并可利用这些共享单元来实现数据交换、实时通信和必要的同步操作。

对于同一可运行对象的多个线程,可运行对象的成员变量自然就是这些线程共享的数据单元。

使用Runnable接口可以轻松实现多个线程共享相同数据,只要用同一可运行对象作为参数创建多个线程就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//用Thread子类程序来模拟航班售票系统,实现3个售票窗口发售某次航班的10张机票,一个售票窗口用一个线程来表示。
class TrheadSale extends Thread {
private int tickets = 10;
public void run() {
while(true) {
if(tickets>0) {
System.out.println(this.getNmae()+" 售机票第" + tickets-- + "号");
} else {
System.exit(0);
}
}
}
}
public class App11_4 {
public static void main(String[] args) {
ThreadSale t1 = new ThreadSale();
ThreadSale t2 = new ThreadSale();
ThreadSale t3 = new ThreadSale();
t1.start();
t2.start();
t3.start();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Thread-0 售机票第10号
Thread-0 售机票第9号
Thread-2 售机票第10号
Thread-2 售机票第9号
Thread-2 售机票第8号
Thread-2 售机票第7号
Thread-2 售机票第6号
Thread-2 售机票第5号
Thread-0 售机票第8号
Thread-1 售机票第10号
Thread-2 售机票第4号
Thread-0 售机票第7号
Thread-0 售机票第6号
Thread-0 售机票第5号
Thread-2 售机票第3号
Thread-1 售机票第9号
Thread-0 售机票第4号
Thread-0 售机票第3号
Thread-0 售机票第2号
Thread-0 售机票第1号
Thread-2 售机票第2号
Thread-1 售机票第8号
Thread-1 售机票第7号
Thread-1 售机票第6号
Thread-2 售机票第1号
Thread-1 售机票第5号
Thread-1 售机票第4号
Thread-1 售机票第3号
Thread-1 售机票第2号
Thread-1 售机票第1号

创建了三个售票口对象分开分线程卖票,tickets并不共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//用Runnable接口程序来模拟航班售票系统,利用同一可运行对象实现3个售票窗口发售某次航班的10张机票,一个售票窗口用一个线程来表示。
class ThreadSale implements Runnable {
private int tickets = 10;
public void run() {
while(true) {
if(tickets > 0) {
System.out.println(Thread.currentThread().getName()+" 售机票第" + tickets-- + "号");
} else {
System.exit(0);
}
}
}
}
public class App11_4 {
public static void main(String[] args) {
ThreadSale t = new ThreadSale();
Thread t1 = new Thread(t, "第1售票窗口");
Thread t2 = new Thread(t, "第2售票窗口");
Thread t3 = new Thread(t, "第3售票窗口");
t1.start();
t2.start();
t3.start();
}
}
1
2
3
4
5
6
7
8
9
10
第1售票窗口售机票第10号
第3售票窗口售机票第9号
第2售票窗口售机票第8号
第1售票窗口售机票第7号
第3售票窗口售机票第6号
第3售票窗口售机票第3号
第3售票窗口售机票第2号
第2售票窗口售机票第5号
第1售票窗口售机票第4号
第3售票窗口售机票第1号

即每个线程调用的是同一个ThreadSale对象中的run()方法,访问的是同一个对象中的变量tickets,这种情况下变量tickets才是共享的资源。

Runnable接口适合处理多线程访问同一资源的情况,并且可以避免由于Java语言的单继承性带来的局限。

多线程的同步控制

当一个线程对共享的数据进行操作时,应使之成为一个”原子操作”,即在没有完成相关操作之前,
不允许其他线程打断它,否则就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。

线程间互斥:所以说被多个线程共享的数据在同一时刻只允许一个线程处于操作之中。

线程执行过程中,在执行有关的若干个动作时,没有能够保证独占相关的资源,而是在对该资源进行处理时又被其他线程的操作打断或干扰而引起的。

线程同步:必须保证线程在一个完整的操作所有动作的执行过程中,都占有相关资源而不被打断,这就是线程同步的概念。

临界资源同步资源:多线程共享的资源或数据。
临界代码临界区:每个线程中访问临界资源的那一段代码。
临界资源:在一个时刻只能被一个线程访问的资源
临界区:访问临界资源的那段代码

在Java语言中每个对象都有一个”互斥锁”与之相连。当线程A获得了一个对象的互斥锁后,线程B若也想获得该对象的互斥锁,就必须等待线程A完成规定的操作并释放出互斥锁后,才能获得该对象的互斥锁,并执行线程B中的操作。

为了保证互斥,Java语言使用synchronized关键字来标识同步的资源,这里的资源可以是一种类型的数据,也就是对象,也可以是一个方法,还可以是一段代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//同步语句
Synchronized(对象) {
临界代码段
}

//同步方法1
public synchronized 返回类型 方法名() {
方法体
}

//同步方法2
public 返回类型 方法名() {
synchronized(this) {
方法体
}
}

synchronized的功能是:首先判断对象或方法的互斥锁是否在,若在就获得互斥锁,然后就可以执行紧随其后的临界代码段或方法体;如果对象或方法的互斥锁不在(已被其他线程拿走),就进入等待状态,直到获得互斥锁。

(1)synchronized锁定的通常是临界代码。由于所有锁定同一个临界代码的线程之间在synchronized代码块上是互斥的,也就是说,这些线程的synchronized代码块之间是串行执行的,不再是互相交替穿插并发执行,因而保证了synchronized代码块操作的原子性。
(2)synchronized代码块中的代码数量越少越好,包含的范围越小越好,否则就会失去多线程并发执行的很多优势。
(3)若两个或多个线程锁定的不是同一个对象,则它们的synchronized代码块可以互相交替穿插并发执行。
(4)所有的非synchronized代码块或方法,都可自由调用。如线程A获得了对象的互斥锁,调用对象的synchronized代码块,其他线程仍然可以自由调用该对象的所有非synchronized方法和代码。
(5)任何时刻,一个对象的互斥锁只能被一个线程所拥有。
(6)只有当一个线程执行完它所调用对象的所有synchronized代码块或方法时,该线程才会释放这个对象的互斥锁。
(7)临界代码中的共享变量应定义为private型。否则,其他类的方法可能直接访问和操作该共享变量,这样synchronized的保护就失去了意义。
(8)由于(7)的原因,只能用临界代码中的方法访问共享变量。故锁定的对象通常是this,即通常格式都是:

1
synchronized(this){…}

(9)一定要保证,所有对临界代码中共享变量的访问与操作均在synchronized代码块中进行。
(10)对于一个static型的方法,即类方法,要么整个方法是synchronized,要么整个方法不是synchronized。
(11)如果synchronized用在类声明中,则表示该类中的所有方法都是synchronized的。

线程之间的通信

java.lang.Object类的wait()、notify()和notifyAll()等方法为线程间的通信提供了有效手段。

  • Object类中用于线程间通信的常用方法
常用方法 功能说明
public final void wait() 如果一个正在执行同步代码(synehronized)的线程A执行了wait()调用(在对象x上),该线程暂停执行而进入对象x的等待队列,并释放已获得的对象x的互斥锁。线程A要一直等到其他线程在对象x上调用notify( )或notifyAll()方法,才能够在重新获得对象x的互斥锁后继续执行(从wait()语句后继续执行)
public void notify() 唤醒正在等待该对象互斥锁的第一个线程
public void notifyAll() 唤醒正在等待该对象互斥锁的所有线程,具有最高优先级的线程首先被唤醒并执行

wait()、notify()和notifyAll()只能在同步代码块里调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//两个线程模拟存票、售票古城,但要求每存入一张票,就售出一张票,售出后,再存入,直至售完为止。

class Tickets {
protected int size; //总票数
int number = 0; //票号
boolean available = false; //是否有票可售

public Tickets(int size) {
this.size = size;
}

public synchronized void put() { //同步方法存票
if(available) {
try {
wait();
} catch (Exception e) {
}
}

System.out.println("存入第【" + (++number) + "】号票");
available = true;
notify();
}

public synchronized void sell() { //同步方法取票
if(available) {
try {
wait();
} catch (Exception e) {
}
}

System.out.println("售出第【" + (++number) + "】号票");
available = false;
notify();

if (number == size) { //售完最后一张票后,设置一个结束标志
number = size + 1;
}
}
}

class Producer extends Thread { //存票
Tickets t = null;
public Producer(Tickets t) {
this.t = t;
}
public void run() {
while(t.number < t.size) {
t.put();
}
}
}

class Consumer extends Thread { //售票
Tickets t = null;
public Consumer(Tickets t) {
this.t = t;
}
public void run() {
while(t.number <= t.size) {
t.sell();
}
}
}

public class App11_8 {
public static void main(String[] args) {
Tickets t = new Tickets(10);
new Producer(t).start();
new Consumer(t).start();
}
}

本章小结

  • 线程(thread)是指程序的运行流程。多线程机制可以同时运行多个程序块,使程序运行的效率变得更高,也可以克服传统程序语言所无法设计的问题。
  • 多任务与多线程是两个不同的概念。多任务是针对操作系统而言的,表示操作系统可以同时运行多个应用程序;而多线程是针对一个程序而言的,表示在一个程序内部可以同时执行多个线程。
  • 创建线程有两种方法:一种是继承java.lang包中的Thread类;另一种是用户在定义自己的类中实现Runnable接口。
  • run()方法给出了线程要执行的任务。若是派生自Thread类,必须把线程的程序代码编写在run()方法内,实现覆盖操作;若是实现Runnable接口,必须在实现Runnable接口的类中定义run()方法。
  • 如果在类中要激活线程,必须先做好下列两件事情:①此类必须是派生自Thread类或实现Runnable接口,使自己成为它的子类;②线程的任务必须写在run()方法内。
  • 每一个线程,在其创建和消亡之前,均会处于下列五种状态之一:新建状态、就绪状态、运行状态、阻塞状态和消亡状态。
  • 阻塞状态的线程一般情况下可由下列情况所产生:(1)该线程调用对象的wait()方法;(2)该线程本身调用了sleep()方法;(3)该线程和另一个线程join()在一起;(4)有优先级更高的线程处于就绪状态。
  • 解除阻塞的原因有:(1)如果线程是由调用对象的wait()方法所阻塞的,则该对象的notify()方法被调用时可解除阻塞;(2)线程进入睡眠(sleep)状态,但指定的睡眠时间到了。
  • Thread类中的sleep()方法可以用来控制线程睡眠时间,睡眠时间的长短全由sleep()方法中的参数而定,单位为1/1000s。
  • 线程在运行时,因不需要外部的数据或方法,就不必关心其他线程的状态或行为,这样的线程称为独立、不同步的或是异步执行的。
  • 被多个线程共享的数据在同一时刻只允许一个线程处于操作之中,这就是同步控制。
  • 当一个线程对共享的数据进行操作时,在没有完成相关操作之前,应使之成为一个”原子操作”,即不允许其他线程打断它,否则可能会破坏数据的完整性,而得到错误的处理结果。
  • synchronized锁定的是一个具体对象,通常是临界区对象。所有锁定同一个对象的线程之间,在synchronized代码块上是互斥的,也就是说,这些线程的synchronized代码块之间是串行执行的,不再是互相交替穿插并发执行,因而保证了synchronized代码块操作的原子性。
  • 由于所有锁定同一个对象的线程之间,在synchronized代码块上是互斥的,这些线程的synchronized代码块之间是串行执行的,故synchronized代码块中的代码数量越少越好,包含的范围越小越好,否则多线程就会失去很多并发执行的优势。
  • 任何时刻,一个对象的互斥锁只能被一个线程所拥有。
  • 只有当一个线程执行完它所调用对象的所有synchronized代码块或方法时,该线程才会自动释放这个对象的互斥锁。
  • 一定要保证,所有对临界区共享变量的访问与操作均在synchronized代码块中进行。

课后习题

  • 简述线程的基本概念。程序、进程、线程的关系是什么?
  • 什么是多线程?为什么程序的多线程功能是必要的?
  • 多线程与多任务的差异是什么?
  • 线程有哪些基本状态?这些状态是如何定义的?
  • Java程序实现多线程有哪两个途径?
  • 在什么情况下,必须以类实现Runnable接口来创建线程?
  • 什么是线程的同步?程序中为什么要实现线程的同步?是如何实现同步的?
  • 假设某家银行可接受顾客的存款,每进行一次存款,便可计算出存款的总额。现有两名顾客,每人分三次,每次存入100元钱。试编程来模拟顾客的存款操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Bank {
int money = 0;

public Bank(int money) {
this.money = money;
}

public synchronized void put(int number) {

System.out.println("存入" + number + "元");
this.money += number;
System.out.println("银行剩余" + this.money + "元");

notify();
}

public synchronized void get(int number) {
if (number <= this.money) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("取出" + number + "元");
this.money -= number;
System.out.println("银行剩余" + this.money + "元");

notify();
}
}
}

class Consumer extends Thread{
Bank b = null;
String name = "";
public Consumer(String name, Bank b) {
this.name = name;
this.b = b;
}

public void run() {
for (int i=0; i<3; i++) {
System.out.println("顾客" + name);
b.put(100);
}
}
}

public class D11_8 {
public static void main(String[] args) {
Bank b = new Bank(0);
new Consumer("No1", b).start();
new Consumer("No2", b).start();
}
}
文章作者: HibisciDai
文章链接: http://hibiscidai.com/2021/12/25/JAVA程序设计基础-第6版陈国君2006-学习笔记2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 HibisciDai
支付宝打赏
微信打赏