Java是一种计算机编程语言,它是并发的、基于类的、面向对象的,并被特别设计为具有尽可能少的实现依赖性(依赖性)。它的目的是让应用程序开发人员 “一次编写,随处运行”(WORA),也就是说,在一个平台上运行的代码不需要重新编译就可以在另一个平台上运行。
Ch-00
Buzzwords
- 面向对象类似C++
- 简单:无指针运算
- 网络支持(Network-savvy):例如,通过URL访问互联网上的对象,与访问本地文件系统相似
- 支持多线程
- 鲁棒性强(Robust):Java有一个指针模型(pointer model),消除了覆盖内存和破坏数据的可能性
- 安全:Java能够构建无病毒、无篡改(tamper)的系统,从而避免了攻击
- 动态(dynamic):例如,找出运行时的信息是直接的
- 可移植的:字符串(Strings)以标准的Unicode格式存储
- 结构体系中立(architectural neutral):java编译器(编译器)生成一种结构体系中立的对象文件格式–字节码
- 解释型(interpreted):java解释器(解释器)可以直接在解释器被移植到的任何机器上执行Java字节码(bytecode),具有平台无关性
Ch-01
Java的出现标志这真正的分布式系统的到来。
Java配备了解释器(interpreter,即jave.exe)和Java运行环境(JRE),其中JRE包括了Java虚拟机(JVM)、类库及一些核心文件。
JVM负责将字节码翻译成JVM所在平台的机器码(machine code),并让当前平台运行该机器码。
Java的每种数据类型都分配固定长度(跨平台性)。
Java运行平台
- Java SE(Java Standard Edition):桌面级应用程序和低端的服务器应用程序
- Java EE(Java Enterprise Edition):企业级应用服务
- Java ME(Java Micro Edition):嵌入式设备开发平台
- javac Hello.java —— 编译源文件
- java Hello —— 运行主类
一个文件只能包含一个public类(也可以没有public类),文件名与public类名相同,如果没有public类,需与某个类的名字相同
- JVM,Java虚拟机,Java Virtual Machine
- JDK,Java开发工具包,Java Development Kit
- JRE,Java运行环境,Java Runtime Environment
- IDE,集成开发环境,Integrated Development Environment
Ch-02
- bytecode:字节码,JVM能够识别的二进制代码
- API:Application Programming Interface,应用程序编程接口/类库
- JVM:能够隔离程序与底层硬件
2.1 标识符和关键字
标识符(identifiers)
用于标识类名、变量名、方法名、数组名、文件名等有效字符序列
Java语言规定标识符由字母、下划线、美元符号和数字组成,其中首字符不能是数字,字母区分大小写
标识符不能是true、false、null(尽管他们并不是关键字)
关键字
abstract, continue, for, new, switch, assert***, default, goto*, package, synchronized, boolean, do, if, private, this, break, double, implements, protected, throw, byte, else, import, public, throws, case, enum****, instanceof, return, transient, catch, extends, int, short, try, char, final, interface, static, void, class, finally, long, strictfp**, volatile, const*, float, native, super, while
* not used
** added in 1.2
*** added in 1.4
**** added in 5.0
2.2 基本数据类型(primitive data types or fundamental types)
boolean, char, byte, short, int, long, float, double,Java摆脱平台依赖,在不同设备上格式均一样,可以通过类似”Byte.SIZE”查看大小
- boolean:true,false,默认值false
- byte:1个字节,8位;取值范围:-2^7~2^7-1,默认值0
- short:2个字节,16位;取值范围:-2^15~ 2^15-1,默认值0
- int:4个字节,32位;取值范围:-2^31~2^31-1,默认值0
-
long:8个字节,64位;取值范围:-2^63~2^63-1,默认值0
- char:2个字节,无负数概念,取值0-65535,如果要看Unicode的顺序位置,至少需要转成int(short有一位符号位),默认值’\u0000’
- float:4个字节,具有7-8个有效数位,取值范围为10^-38~10^38和-10^38~-10^-38,常量结尾用“f”或者“F”,默认值0.0f
- double:8个字节,为默认浮点数类型,或者可以在尾部添加“d”,取值范围为10^-308~10^308和-10^308~-10^-308,具有15-17个有效数位,默认值0.0d
如果需要减少精度丢失问题,可以使用BigDecimal类, String(or any object)默认值为null
基本数据类型不是类创建的对象
2.3 基本数据类型转换(casting)
精度从低到高排列:byte, short, int, long, float, double
- 当把级别低的变量赋值给级别高的变量时,系统会自动完成数据类型转换
- 当把级别高的变量赋值给级别低的变量时,必须使用显式类型转换
2.4 数据的输入和输出
System.out.printf()
- %d输出整型
- %c输出字符
- %f输出浮点类型数据,小数部分最多保留6位
- %s输出字符串
- %md输出的整型类型数据占m列
- %m.nf输出的float数据占m列,小数点保留n位
Scanner
Scanner是SDK1.5新增的一个类,需要import java.util.Scanner(也可以使用*进行wildcard import),可以用该类创建一个对象:Scanner reader = new Scanner(System.in);
然后用reader对象调用下列方法,读取用户在命令行输入的各种数据类型:
- nextByte()
- nextShort
- nextInt()
- nextLong()
- nextFloat()
- nextDouble()
- nextLine()
上述方法执行的时候都会引起堵塞,等待在命令行输入数据回车确认。
2.5 数组
index从0开始,声明数组仅仅是给出数组名和元素的数据类型,只有创建数组的时候才分配内存空间,分配内存空间的时候必须指明数组的长度。
如果访问越界会抛出ArrayIndexOutOfBoundsException异常。
数组属于引用型变量!!!
arraycopy方法:调用System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
Ch-03
-
Java按运算符两边的操作元的最高精度保留结果精度(byte, short, int, long, float, double)
-
运算结果是boolean型值,优先级比较低
- 对于byte和short类型数据,a«n的运算结果是int型精度(a在运算时会被升级成int)
- 如果a是byte、short或int型数据,系统总是先计算出n%32的结果m,然后再进行a«m运算,如果a是long型数据,系统总是先计算出n%64的结果m,然后再进行a«m运算
- 移位运算符右操作符一定要是整型类型
- 正数不断右移位的结果是0,负数不断右移位的结果是-1(负数右移高位填1,是补码移动)
instanceof运算符
左操作元是一个对象,右操作元是类,返回boolean值,判断该对象是否是由该类创建的对象
语句概述
- 方法调用语句,如
reader.nextInt()
-
表达式语句,如
x = 23
- 复合语句,也可以成为代码块
- 控制语句
- package语句和import语句
分支语句
switch语句中表达式的值必须是整型或字符型,常量值也必须是整型或者字符型,default
语句可以省略
在同一个switch语句中,case后的常量值必须互不相同
branching语句
包括break和continue(我还以为是啥…)
Ch-04
4.1 面向对象编程
三个特性:封装(Encapsulation),继承(Inheritance),多态(Polymorphism)
- 封装:将数据(属性)和对数据的操作(功能)封装在一起,例如“类(class)”的概念
- 继承:子类可以继承父类的属性和功能,同时可以增加子类独有的属性和功能
- 多态:(1)操作名称的多态:多个操作具有相同的名字,但这些操作所接收的消息类型不同;(2)与继承相关的多态:同一操作被不同类型的对象调用时可能产生不同的行为
4.2 类声明和类体
-
类(class)是组成Java程序的基本要素
-
类封装了一种类型的对象(object)的变量和方法
-
类是用来定义(define)对象的模板
-
可以用类创建对象,当使用一个类创建(create)一个对象时,我们也说给出了这个类的一个实例(instance)
4.3 类体的构成
类体内容可以有两种类型的成员:
- 成员变量(member variable):通过变量声明来定义的变量,称作成员变量或域(data field),用来刻画类创建的对象的属性、状态
- 方法(method):方法是类体的重要成员之一。其中的构造方法是具有特殊地位的方法,供类创建对象时使用,用来给出类所创建的对象的初始状态;另一种方法,可以由类所创建的对象调用,对象调用这些方法来操作成员变量,进而形成一定的算法
成员变量的类型可以是Java中的任意数据类型,包括对象和接口,其在整个类内都有效,与其在类体中书写的先后位置无关
类的成员类型中可以有数据和方法,即数据的定义和方法的定义,但没有语句,语句必须放在方法中
4.4 构造方法与对象的创建
类中有一部分方法称为构造方法(constructor),类创建对象时需要使用构造方法,以便给类所创建的对象一个合理的初始状态
- 名字必须与类名相同
- 不返回任何数据类型,省略void
- 多个构造方法之间,或参数个数不同,或参数类型不同
创建一个对象包括:
- 对象的声明(declare)
- 为对象分配成员变量
- 对象的声明:类名+对象名
- 为对象分配成员变量:使用new运算符和类的构造方法,如果类里定义了一个或多个构造方法,那么Java不提供默认的构造方法
4.5 对象的引用与实体
创建对象就是指为其分配成员变量,并获得一个引用(reference),两个对象如果有着相同的引用,就有着相同的实体。
使用.
操作符实现对变量的访问(access)和对方法的调用(invoke)
Java具有“垃圾收集”(garbage collection)机制,Java的运行环境周期性地检测某个实体是否已不再被任何对象所引用,如果发现这样的实体,就释放该实体占有的内存。因此,Java编程人员不必像C++程序员那样,要自己时刻检查哪些对象应该释放内存。
没有实体的对象称作空对象。空对象不能使用,即不能让一个空对象去调用方法产生行为。假如程序中使用了空对象,程序在运行时会出现异常,即NullPointerException。由于对象是动态地分配实体,所以Java的编译器对空对象不做检查。因此,在编写程序时要避免使用空对象。
4.6 成员变量
static静态变量
用关键字static修饰的成员变量称作静态变量(static variable)或类变量(class variable), 而没有使用static修饰的成员变量称作实例变量(instance variable)。
静态变量是与类相关联的数据变量,也就是说,静态变量是和该类所创建的所有对象相关联的变量,改变其中一个对象的这个静态变量就同时改变了其它对象的这个静态变量。
因此,静态变量不仅可以通过某个对象访问也可以直接通过类名访问。
注:通过类名访问静态变量是一个好的编程习惯。
final常量(constant)
如果一个成员变量修饰为final,就是常量,不能更改,常量的名字习惯用大写字母
final修饰的成员变量不占用内存,这意味着在声明final成员变量时,必须要初始化。
对于final修饰的成员变量,可以通过对象访问,但不能通过类名访问。
注:在实际开发中,常量通常是不同对象间共享的静态变量,因此会同时用final和static来修饰(通常用类名来访问)。
4.7 方法method
- instance method实例方法
- static method静态方法
- 构造方法constructor
方法的名字必须符合标识符规定。在给方法起名字时应遵守习惯。名字如果使用拉丁字母,首字母要小写。如果由多个单词组成,从第2个单词开始的首字母使用大写
类中的方法必须要有方法体,如果方法的类型是void类型,方法体中也可以不书写任何语句。
-
实例方法(instance method)可以调用该类中的实例方法、静态方法
实例方法:不用static修饰的方法
可以操作实例变量、静态变量,必须通过对象来调用
-
静态方法(static method)只能调用该类的静态方法,不能调用实例方法 静态方法:又称类方法,方法声明中用关键字static修饰的方法
静态方法可以通过类名调用,也可以通过对象来调用
静态方法只能操作静态变量,不能操作实例变量
注:通过类名调用静态方法是一个好的编程习惯。
无论静态方法或实例方法,当被调用执行时,方法中的局部变量才被分配内存空间,方法调用完毕,局部变量即刻释放所占的内存
当方法被调用时,如果方法有参数,参数必须要实例化,即参数变量必须有具体的值。
在Java中,方法的所有参数都是“传值”的(pass by value),也就是说,方法中参数变量的值是调用者指定的值的拷贝。方法如果改变参数的值,不会影响向参数“传值”的变量的值。
Java的引用类型数据包括对象(object)、数组(array)、接口(interface),当参数是引用类型时,“传值”传递的是变量的引用(reference)而不是变量所引用的实体(entity)
4.8 方法重载
方法重载(overload)是指一个类中可以有多个方法具有相同的名字,但这些方法的参数必须不同,即或者是参数的个数不同,或者是参数的类型不同。方法的返回类型和参数的名字不参与比较,也就是说,如果两个方法的名字相同,即使返回类型不同,也必须保证参数不同。
4.9 this关键字
this是Java的一个关键字,可以出现在实例方法(instance method)和构造方法(constructor)中,但不可以出现在静态方法(static method)中。
静态方法(static method)中不可以使用this:因为静态方法可以通过类名直接调用,这时可能还没有创建任何对象。
- 使用this来区分成员变量和局部变量:如果局部变量的名字与成员变量的名字相同,则成员变量被隐藏,即这个成员变量在这个方法内暂时失效。这时,如果想在该方法内使用成员变量,成员变量前面的
this.
就不可以省略。
4.10 package包
通过关键字package声明包语句。package语句作为Java源文件中的第一条语句,指明该源文件定义的类所在的包。
package语句的一般格式为:package 包名;
如果源程序中省略了package语句,源文件中所定义命名的类被隐含地认为是无名包(default package)的一部分,即源文件中定义命名的类在同一个包中,但该包没有名字,也无法被引用。注:不使用无名包是一个好的编程习惯。
包名可以是一个合法的标识符,也可以是若干个标识符加.
分割而成
4.11 import语句
在一个Java源程序中可以有多个import语句,它们必须写在package语句(假如有package语句)和源文件中类的定义之间。
如果使用import语句引入了整个包中的类,那么可能会增加编译时间,但不会影响程序运行的性能。
为避免类名混淆,Java运行环境总是先到程序所在的目录中寻找程序所使用的类,然后加载到内存
-
如果在当前目录中寻找到了要加载的类,那么程序就不会再加载import语句引入的同名类
-
如果在当前目录没有发现所需要的类,就到import语句所指的包中查找
4.12 访问权限
类有两种重要的成员:成员变量和方法。
类创建的对象可以通过.
运算符访问分配给自己的变量,也可以通过.
运算符调用类中的实例方法和静态方法。
类在定义声明成员变量和方法时,可以用关键字private、protected和public来说明成员变量和方法的访问权限(又称为可见性,visibility),使得对象访问自己的变量和使用方法受到一定的限制。
private
如果这些成员不打算供类以外的类使用,就把它们变成私有的。
用关键字private修饰的成员变量和方法被称为私有成员变量和私有方法。 对于私有成员变量或私有方法,只有在本类中创建该类的对象时,这个对象才能访问自己的私有成员变量和类中的私有方法。
public
如果这些成员是为该类的users准备的,那么就把它们变成公共的(没有限制)。
用public修饰的成员变量和方法被称为共有成员变量和共有方法。
当我们在任何一个类中用类A创建了一个对象a后,该对象a能访问自己的public成员变量和类中的public方法。
friendly
不用private, public, protected修饰的成员变量和方法被称为友好成员变量和友好方法。
同一个包中的类可以访问。
protected
如果这些字段或方法是为类的扩展者准备的(继承),而不是为类的users准备的,那么就把它们变成保护的。
用protected修饰的成员变量和方法被称为受保护的成员变量和受保护的方法(子类能访问)。
当类在同一个包中时,受保护的变量 和 受保护的方法 不仅可以在自己类中调用,也可以在另外一个类中调用 。
类声明时,如果关键字class前面加上public关键字,就称这样的类是一个public类。 不能用protected修饰类。 不能用private来修饰外部类,只能修饰内部类(这种情况也很少)
关于构造方法
private, public, protected修饰符的意义也同样适合于构造方法(constructor)。
如果一个类没有明确地声明构造方法,那么public类的默认构造方法是public的,友好类的默认构造方法是友好的。
需要注意的是,如果一个public类定义声明的构造方法中没有public的构造方法,那么在另外一个类中使用该类创建对象时,使用的构造方法就不是 public的,创建对象就受到一定的限制(例如,要求是否在同一个package中)。
更进一步,如果构造方法是private,则意味着不允许用户创建对象,例如java.lang.Math类的构造函数。注:Math类中的方法都是静态方法,因为需要通过类名来访问。
As example:
- 在类A中,可以访问对象a的以下成员: private, friendly (or default), protected, public
- 在与类A同package的另外一个类B中,可以访问对象a的以下成员: Friendly (or default), protected, public
- 在类A的子类B中(不同package),可以访问对象a的以下成员: protected, public
- 在与类A不同package的另外一个类C中,可以访问对象a的以下成员: public
4.13 对象的组合
一个类可以把对象作为自己的成员变量
4.14 基本类型数据的类包装
Java同时也提供了与基本类型数据相关的类,实现了对基本类型数据的封装。这些类在java.lang包中(java.lang.Number)
– Byte, Short, Integer, Long
– Float, Double
– Character
4.15 对象数组
没啥好说的
4.16 反编译器和文档生成器
使用javap.exe
可以将字节码反编译为源码,查看源码类中的方法的名字和成员变量的名字
4.17 jar文件
没啥好说的
Ch-05
5.1 子类与父类
继承(inheritance)是一种由已有的类创建新类的机制。
利用继承,我们可以先创建一个共有属性的一般类,根据该一般类再创建具有特殊属性的新类。
新类继承一般类的属性(状态)和功能(行为),并根据需要增加它自己的新的属性(状态)和功能(行为)。
由继承而得到的类称为子类(subclass, child class, or extended class),被继承的类称为父类(superclass, parent class, or base class)。
父类可以是自己编写的类也可以是Java类库中的类。 利用继承有利于实现代码的重用,子类只需要添加新的属性、功能。 Java不支持多重继承,即子类只能有一个父类。
使用关键字extends来声明一个类是另外一个类的子类:
class 子类名 extends 父类名
{
...
}
Every class in Java is descended from java.lang.Object class. If no inheritance is specified when a class is defined, its superclass is Object.
5.2 子类的继承性
继承的定义
所谓类继承就是子类继承父类的成员变量和方法作为自己的成员变量和方法,就好象它们是在子类中直接声明的一样。当然,子类能否继承父类的成员变量和方法还有一定的限制。
子类和父类在同一包中的继承性
如果子类和父类在同一包中,那么子类自然地继承了父类中不是private的成员变量(即:friendly, protected, public)作为自己的成员变量,并且也自然地继承了父类中不是private的方法(即:friendly, protected, public)作为自己的方法。继承的成员变量和方法的访问权限保持不变。
子类和父类不在同一包中的继承性
如果子类和父类不在同一个包中,那么子类只能继承父类的protected, public成员变量和方法,继承的成员变量和方法的访问权限保持不变。即如果子类和父类不在同一个包里,子类不能继承父类的friendly成员变量和friendly方法。
5.3 子类对象的构造过程
- 当用子类的构造方法创建一个子类的对象时,子类的构造方法总是先调用父类的某个构造方法。
- 如果子类的构造方法没有指明使用父类的哪个构造方法,子类就调用父类的不带参数的构造方法。
注:父类的构造方法(constructor)不会被子类继承。
子类如何创建对象
将子类中声明的成员变量作为子类对象的成员变量。
父类的成员变量也都分配了内存空间,但只将其中一部分(继承的那部分)作为子类对象的成员变量。 父类的private成员变量尽管分配了内存空间, 但它不作为子类的成员变量,即父类的private成员变量不归子类管理。
方法的继承性与成员变量的继承性相同。
如果子类和父类不在同一包中,尽管父类的friendly成员变量分配了内存空间,也不作为子类的成员变量。
子类创建对象时似乎浪费了一些内存,因为当用子类创建对象时,父类的成员变量也都分配了内存空间,但只将其中一部分作为子类对象的成员变量。
实际情况并非如此,子类中还有一部分方法是从父类继承的,这部分方法却可以操作这部分没有继承的变量。
5.4 成员变量隐藏和方法重写
成员变量隐藏
子类可以隐藏继承的成员变量,当在子类中定义和父类中同名的成员变量时,子类就隐藏了继承的成员变量,可以通过super
关键字访问。
方法重写
子类可以通过方法重写(overriding)来隐藏继承的方法。
方法重写:子类中定义一个方法,并且这个方法的名字、返回类型、参数个数和类型与从父类继承的方法完全相同。
如果子类在准备隐藏继承的方法时,参数个数或参数类型与父类的方法不尽相同,那实际上也没有隐藏继承的方法,这时子类就出现两个方法具有相同的名字,即重载(overloading) 。
- Overloading (重载) means to define multiple methods with the same name but different signatures.
- Overriding (重写) means to provide a new implementation for a method in the subclass.
访问修饰符protected的进一步说明
一个类A中的protected成员变量和方法可以被它的直接子类和间接子类继承,比如B是A的子类,C是B的子类 ,D又是C的子类,那么B、C和D类都继承了A的protected成员变量和方法。
如果用D类在D本身中创建了一个对象,那么该对象总是可以通过“.”运算符访问继承的或自己定义的protected变量和protected方法的。 但是,如果在另外一个类中,比如E类,用D类创建了一个对象d,该对象通过“.”运算符访问protected成员变量和protected方法的权限如下:
- 子类D的protected成员变量和方法,如果不是从父类继承来的,对象d访问这些protected成员变量和方法时,只要E类和D类在同一个包中就可以了。–如果子类D的对象的protected成员变量或protected方法是从父类继承的,那么就要一直追溯到该protected成员变量或方法的“祖先”类,即A类,如果E类和A类在同一个包中,对象能访问继承的protected成员变量和protected方法。
- 如果子类D的对象的protected成员变量或protected方法是从父类继承的,那么就要一直追溯到该protected成员变量或方法的“祖先”类,即A类,如果E类和A类在同一个包中,对象能访问继承的protected成员变量和protected方法。
5.5 super关键字
super关键字有两种用法:
- 在子类中使用super调用父类的构造方法
- 在子类中使用super调用被子类隐藏的成员变量和方法
使用super调用父类的构造方法
子类不继承父类的构造方法,因此,子类如果想使用父类的构造方法,必须在子类的构造方法中使用关键字super来表示,而且super必须是子类构造方法中的第一条语句。
Example:
class A {
int x;
A(int x_val) {x = x_val;}
}
class B extands A {
int z;
B(int x_val) {
super(x_val); // 必须是第一条语句
z = 30;
}
}
使用super操作被隐藏的成员变量和方法
class A {
int m = 0;
int cal() {
return m;
}
}
class B {
int m = 1;
int getA_m() {
return super.m;
}
int cal() {
return super.cal()+1;
}
}
5.6 final类和final方法
final类不能被继承,即不能有子类
final class A {...}
将一个类声明为final类一般是由于安全性考虑。因为一旦一个方法被修饰为final方法,则这个方法不能被重写(overriding),即不允许子类通过重写来隐藏继承的final方法。
5.7 对象的上转型对象
对象的上转型
A subclass (子类) is a specialization of its superclass (父类); every instance of a subclass is also an instance of its superclass, but not vice versa.
假设B是A的子类或间接子类,我们用子类B创建一个对象,可以把这个对象的引用放到类A声明的对象中:
1
2
3
4
A a; // B是A的子类
B b;
b = new B();
a = b; // implicit casting,又称upcasting, 与之对应的是explicit casting(又称downcasting)
对象a是对象b的上转型对象,对象的上转型对象的实体是子类负责创建的,但上转型对象会失去原对象的一些属性和功能。
对象a、b共有的:
A被子类继承或隐藏的成员变量
B被子类继承或重写的方法
对象b有而a没有的:
子类新增的成员变量
子类新增的方法
上转型对象不能操作子类新增的成员变量和方法
上转型对象可以访问被子类继承或隐藏的成员变量(即父类中的变量),也可以调用被子类继承的方法(即父类中的方法)或重写的方法(即被子类重写的方法),用上转型对象访问重写方法会调用重写的方法。
可以将对象的上转型对象再强制转换到一个子类对象,这时,该子类对象又具备了子类所有的属性和功能 。
不要将父类创建的对象和子类对象的上转型对象混淆,对象的上转型对象的实体是由子类负责创建的,只不过失掉了一些属性和功能而已。
5.8 继承与多态
和继承有关的多态是指父类的某个方法被其子类重写(overriding)时,可以产生自己的功能行为。
当一个类有多个子类时,并且这些子类都重写(overriding)了父类中的某个方法,我们把子类创建的对象的引用放到该父类的对象中时,就得到了该对象的一个上转型对象,那么这个上转型对象在调用这个方法时就可能具有多种形态(polymorphism, from a Greek word meaning ”many forms”) 。
5.9 abstract类
用关键字abstract修饰的类称为abstract类(抽象类) 。
-
abstract类中如果自己提供constructor(构造方法),则用protected修饰为好,因为是给子类用的。
-
abstract类不能用new运算符创建对象,必须产生其子类,由子类创建对象。
-
如果abstract类的类体中有abstract方法,只允许声明,而不允许实现;而该类的非abstract子类必须实现abstract方法,即重写(override)父类中的abstract方法。
-
一个abstract类只关心子类是否具有某种功能,不关心功能的具体实现。
抽象方法必须是非静态,抽象类可以有静态非抽象方法
-
抽象类不能使用new操作符进行实例化,但是仍可以定义constructor,这些构造函数在其子类的构造函数中被调用。
-
一个包含抽象方法的类必须是抽象的。然而,我们有可能定义一个不包含任何抽象方法的抽象类。在这种情况下,你不能使用new操作符创建该类的实例。这个类被用来作为定义子类的基类。
- 一个子类可以覆盖其父类中的一个方法,将其定义为抽象的。当超类中的方法的实现在子类中变得无效时,这就很有用。在这种情况下,子类必须被定义为抽象的。
- 一个子类可以是抽象的,即使它的父类是具体的。例如,Object类是具体的,但它的子类可以是抽象的。
- 你不能使用new操作符从一个抽象类中创建一个实例,但是抽象类可以作为一个数据类型使用。
5.10 面向抽象
- 面向抽象的第一步就是将经常需要变化的细节分割出来,将其作为abstract类中的abstract方法,不让设计者去关心实现的细节,避免所设计的类依赖这些细节。
- 面向抽象编程的第二步就是继承抽象类,进而设计一个新类。
5.11 接口interface
抽象类和接口都用于抽象化具体对象的,都不能直接实例化,但是两者的侧重点不同:抽象类主要用来抽象类型,表示这个对象是什么;接口主要用来抽象功能,表示这个对象能做什么
- 如果一个类实现某个接口,那么这个类可以实现该接口中的所有(或部分)方法,如果只实现部分方法,则为抽象类。
- 接口中的常量用public static final来修饰,但可以省略。
- 接口中的方法用public abstract来修饰,但可以省略。
- 在实现接口中的方法时,一定要用public来修饰,不可以省略。
- 如果父类实现了某个接口,则其子类也就自然实现了这个接口。
- 接口也可以被继承,即可以通过关键字extends声明一个接口是另一个接口的子接口。
接口与多态
- 如果允许多继承,轿车类想具有“调节温度”的功能,轿车类可以是机动车的子类,同时也是另外一个具有“调节温度”功能的类的子类。多继承有可能增加子类的负担,因为轿车可能从它的多个父类继承了一些并不需要的功能。
- Java不支持多继承,即一个类只能有一个父类。单继承使得程序更加容易维护和健壮,多继承使得编程更加灵活,但却增加了子类的负担,使用不当会引起混乱。
- 为了使程序容易维护和健壮,且不失灵活性,Java使用了接口,一个类可以实现多个接口,接口可以增加很多类都需要实现的功能,不同的类可以实现相同的接口,同一个类也可以实现多个接口。
- 接口的思想在于它可以增加很多类都需要实现的功能。
5.12 接口回调
在讲述继承与多态时,我们通过子类对象的上转型体现了继承的多态性,即把子类创建的对象的引用放到一个父类的对象中时,得到该对象的一个上转型对象,那么这个上转型对象在调用方法时就可能具有多种形态,不同对象的上转型对象调用同一方法可能产生不同的行为。
接口回调
接口回调是多态的另一种体现。
接口回调:把实现某一接口的类创建的对象的引用赋给该接口声明的接口变量,那么该接口变量就可以调用被类实现的接口中的方法,当接口变量调用被类实现的接口中的方法时,就是通知相应的对象调用接口的方法,这一过程称作对象的接口回调。
不同的类在实现同一接口时,可能具有不同的功能体现,即接口的方法体不必相同,因此,接口回调可能产生不同的行为。
接口作参数
当一个方法的参数是一个接口类型时(i.e., 接口作为一个data type),如果一个类实现了该接口,那么,就可以把该类的实例的引用传值给该参数,参数可以回调类实现的接口中的方法。
Example:
interface Show {
void show();
}
class A implements Show {
public void show() {
System.out.println(1);
}
}
class B implements Show {
public void show() {
System.out.println(2);
}
}
class C {
public void f(Show s) {
s.show();
}
}
public class Run {
public static void main(String[] args) {
C c = new C();
c.f(new A());
c.f(new B());
}
}
// ========================
Output:
1
2
5.14 抽象类与接口对比
- 抽象类中可以有abstract方法、非abstract方法;接口中只可以有abstract方法【注:Java 8之后可以有静态方法】
- 抽象类中可以有常量、变量;接口中只可以有常量
抽象类和接口:让设计忽略细节,将重心放在整个系统的设计上。
- 如果某个问题需要使用继承才能更好的解决,如子类除了需要实现父类的抽象方法,还需要从父类继承一些变量或继承一些重要的非抽象方法,可以考虑用抽象类。
- 如果某个问题不需要继承,只是需要给出某些重要的抽象方法的实现细节,就可以考虑使用接口。
一个superclass定义了相关子类的相关行为
一个接口定义了类的共同行为(包括不相关类)
一般来说,明确描述父子关系的强大is-a关系应该使用类来建模。
弱的is-a关系,也被称为is-kind-of关系,表示一个对象拥有某种属性。一个弱的is-a关系可以用接口来建模。
- 接口比抽象类更灵活,因为一个子类只能扩展一个父类,但可以实现任意数量的接口。
- 然而,接口不能包含具体的方法。
- 接口和抽象类的优点可以通过创建一个接口和一个实现它的抽象类来结合。然后,你可以使用接口或抽象类,以方便为准。
对比
-
抽象类和接口都用于抽象化具体对象的,都不能直接实例化,但是两者的侧重点不同:抽象类主要用来抽象类型,表示这个对象是什么;接口主要用来抽象功能,表示这个对象能做什么;
-
接口可以看成抽象类的变体,所有方法都是抽象的,因此接口只能做方法的声明,不能有方法的实现;而抽象类可以有默认的方法实现,既可以做方法的声明,也可以做方法的实现;
如果往抽象类中添加新的方法,可以给他提供默认的实现,因此可以不需要改变子类的代码;如果往接口中添加方法,那么必须改变实现该接口的类(JDK8 之后,接口也可以有默认的实现)
-
接口可以继承多个接口,抽象类不可以多继承类,但可以单继承类或多实现接口。
-
抽象方法和接口函数都不能使用static修饰。抽象方法的访问修饰符可以是public、protected和default,不能是private;接口的默认访问修饰符为public,不能使用其他修饰符。
-
接口的变量只能是不可变常量,默认修饰符都是public static final;但是抽象类的变量可以是普通变量
-
抽象类可以有构造器,接口不能有构造器。
-
接口实现类必须实现接口中的所有声明的方法,但抽象类的子类可以部分实现父类的抽象方法,但如果子类不能全部实现抽象方法,那么该子类只能是抽象类;
-
与正常Java类的相比,抽象类除了不能实例化之外,和正常Java类没有任何区别,但接口和正常Java类是完全不同的类型。
抽象类和接口是Java语言中两种不同的抽象概念,他们的存在对多态提供了非常好的支持,虽然他们之间存在很大的相似性。抽象类的功能远远超过接口,但是定义抽象类的代价比较高。因为每个类只能继承一个类。因此,在这个抽象类中,你必须编写出其子类的所有共性。虽然接口在功能上会弱化很多,但是他只是针对一组动作的描述,而且可以在一个类中同时实现多个接口,因此在设计阶段会降低难度。
5.15 内部类
类中可以有一种成员:内部类
Java支持在一个类中声明另一个类,这样的类称作内部类,而包含内部类的类称为内部类的外嵌类
外嵌类把内部类看作是自己的成员,**外嵌类**的成员变量在**内部类**中仍然有效,**内部类**中的方法可以调用**外嵌类**中的方法
内部类的类体中不可以声明静态变量(类变量)和静态方法(类方法),如果这个内部类被声明为static,则可以有静态变量和静态方法
外嵌类可以用内部类声明对象,作为外嵌类的成员
5.16 匿名类
当使用类创建对象时,程序允许我们把类体与对象的创建组合在一起,此类体被认为是该类的一个子类去掉类声明后的类体,称作匿名类。匿名类就是一个子类,由于无名可用,所以不可能用匿名类声明对象,但却可以直接用匿名类创建一个对象。
abstract class Student
{
abstract void speak();
}
class Teacher
{
void look(Student stu)
{
stu.speak();
}
}
public class Example5_16
{
public static void main(String args[])
{
Teacher zhang = new Teacher();
zhang.look(
new Student() // 以下为匿名类的类体
{
void speak()
{
System.out.println("这是匿名类中的方法");
}
}
);
}
}
-
匿名类可以继承类的方法也可以重写类的方法
- 我们使用匿名类时,必然是在某个类中直接用匿名类创建对象,因此匿名类一定是内部类
- 匿名类可以访问外嵌类中的成员变量和方法
- 匿名类不可以声明静态成员变量和静态方法
- 匿名类的主要用途就是向方法的参数传值
与接口有关的匿名类
假设Computable是一个接口,那么,Java允许直接用接口名和一个类体创建一个匿名对象,此类体被认为是实现了Computable接口的类去掉类声明后的类体,称作匿名类。
如果某个方法的参数是接口类型,那么我们可以使用接口名和类体组合创建一个匿名对象传递给方法的参数,类体必须要实现接口中的全部方法。
interface Show
{
public void show();
}
class A
{
void f(Show s)
{
s.show();
}
}
public class Example5_17
{
public static void main(String args[])
{
A a=new A();
a.f(
new Show(){
public void show()
{
System.out.println("这是实现了接口的匿名类");
}
});
}
}
5.17 异常类
所谓异常就是程序运行时可能出现一些错误,比如试图打开一个根本不存在的文件等,异常处理将会改变程序的控制流程,让程序有机会对错误作出处理。
当程序运行出现异常时,Java运行环境就用异常类Exception的相应子类创建一个异常对象,并等待处理。
Java使用try-catch语句来处理异常,将可能出现的异常操作放在try-catch语句的try部分,当try部分中的某个语句发生异常后,try部分将立刻结束执行,而转向执行相应的catch部分。
try
{
包含可能发生异常的语句
}
catch(ExceptionSubClass1 e)
{
}
catch(ExceptionSubClass2 e)
{
}
各个catch参数中的异常类都是Exception的某个子类,表明try部分可能发生的异常,这些子类之间不能有父子关系,否则保留一个含有父类参数的catch即可。在try中发生异常后的语句不会执行。
自定义异常类
我们也可以继承Exception类,定义自己的异常类,然后规定哪些方法产生这样的异常。
一个方法在声明时可以使用throw关键字声明抛出所要产生的若干个异常,并在该方法的方法体中具体给出产生异常的操作,即用相应的异常类创建对象,这将导致该方法结束执行并抛出所创建的异常对象。
程序必须在try-catch语句块中调用抛出异常的方法。
class MyException extends Exception
{
String message;
MyException(int n)
{
message = n + ": not a positive number";
}
public String getMessage()
{
return message;
}
}
class A
{
public void f(int n) throws MyException
{
if(n<0)
{
MyException ex = new MyException(n);
throw(ex); // 抛出异常,结束方法f的执行
}
double number = Math.sqrt(n);
System.out.println("square root of "+ n + ": " + number);
}
}
public class Example5_19
{
public static void main(String args[])
{
A a=new A();
try{
a.f(28);
a.f(-8);
}
catch(MyException e)
{
System.out.println(e.getMessage());
}
}
}
5.18 泛型类
泛型(generics)是Sun公司在SDK1.5中推出的,其主要目的是可以建立具有类型安全的集合框架,如链表、散列映射等数据结构。
泛型类声明
可以使用“class 名称<泛型列表>”声明一个类,为了和普通的类有所区别,这样声明的类称作**泛型类**,如:` class A
其中A是泛型类的名称,E是其中的泛型,也就是说我们并没有指定E是何种类型的数据,它可以是任何对象或接口,但不能是基本类型数据。
泛型类的类体和普通类的类体完全类似,由成员变量和方法构成:
class Chorus<E,F>
{
void makeChorus(E person, F instrument)
{
person.toString();
instrument.toString();
}
}
使用泛型类声明对象
使用泛型类声明变量、创建对象时,必须要指定类中使用的泛型的实际类型。
class Chorus<E,F>
{
void makeChorus(E person, F instrument)
{
person.toString();
instrument.toString();
}
}
class Singer
{
public String toString()
{
System.out.println("好一朵美丽的茉莉花");
return "";
}
}
class MusicalInstrument
{
public String toString()
{
System.out.println("|3 35 6116|5 56 5-|");
return "";
}
}
public class Example5_20
{
public static void main(String args[])
{
Chorus<Singer, MusicalInstrument> model = new Chorus<Singer, MusicalInstrument>();
Singer singer = new Singer();
MusicalInstrument piano = new MusicalInstrument();
model.makeChorus(singer, piano);
}
}
Java中的泛型类和C++的类模板有很大的不同,在上述例子中,泛型类中的泛型数据person和instrument只能调用Object类中的方法,因此Singer和MusicalInstrument两个类都重写了Object类的toString()方法。
下面我们再看一个例子,我们声明了一个泛型类Cone,一个Cone对象计算体积时,只关心它的底是否能计算面积,并不关心底的类型。
class Cone<E>
{
double height;
E bottom;
public Cone(E b)
{
bottom = b;
}
public void computeVolume()
{
String s = bottom.toString();
double area = Double.parseDouble(s);
System.out.println("Volume:" + 1.0/3.0*area*height);
}
}
class Rectangle
{
double sideA,sideB,area;
Rectangle(double a,double b)
{
sideA=a;
sideB=b;
}
public String toString()
{
area = sideA*sideB;
return ""+area;
}
}
class Circle
{
double area,radius;
Circle(double r)
{
radius = r;
}
public String toString()
{
area = radius*radius*Math.PI;
return "" + area;
}
}
public class Example5_21
{
public static void main(String args[])
{
Circle circle = new Circle(1);
Cone<Circle> coneCircle = new Cone<Circle>(circle);
coneCircle.height=1;
coneCircle.computeVolume();
Rectangle rect = new Rectangle(1,1);
Cone<Rectangle> coneRectangle = new Cone<Rectangle>(rect);
coneRectangle.height = 1;
coneRectangle.computeVolume();
}
}
泛型接口
可以使用“interface 名称<泛型列表>”声明一个接口,这样声明的接口称作**泛型接口**,`interface COmputer
interface Computer<E,F>
{
void makeChorus(E x, F y);
}
class Chorus<E,F> implements Computer<E,F>
{
public void makeChorus(E x, F y)
{
x.toString();
y.toString();
}
}
class MusicalInstrument
{
public String toString()
{
System.out.println("|5 6 3-|5 17 56|");
return "";
}
}
class Singer
{
public String toString()
{
System.out.println("美丽的草原,我可爱的家乡");
return "";
}
}
- Java泛型的主要目的是可以建立具有类型安全的数据结构,如链表(LinkedList)、散列映射(HashMap)等数据结构。
-
SDK1.5是支持泛型的编译器,它将运行时类型检查提前到编译时执行,使代码更安全。
- 泛型是对类型进行参数化的能力。有了它,你可以用通用类型定义一个类或一个方法,这些类型可以被编译器替换成具体类型。
- 泛型类或方法允许你指定该类或方法可以处理的对象的允许类型。如果你试图用一个不兼容的对象来使用该类或方法,编译器可以检测到这个错误。
Ch-06
6.1 String类
Java使用java.lang包中的String类来创建一个字符串变量,因此字符串变量是类类型的变量,是一个对象(object)。
字符串类String表示一个UTF-16格式(16位/两个字节)的字符串,其代码单元是char。
创建字符串对象
String s = new String("Well done Mario");
String a = new String(s);
String类还有两个比较常用的构造方法:
- String (char a[]):用一个字符数组a创建一个String对象
char[] a = {'b','o','y'};
String s = new String(a);
- String(char a[], int startIndex,int count):提取字符数组a中的一部分字符创建一个String对象,参数startIndex和count分别指定在a中提取字符的起始位置和从该位置开始截取的字符个数
char[] a = {'s','t','b','u','s','n'};
String s = new String(a,2,3);
引用字符串常量对象
字符串常量(string literal)被当作是String对象,因此可以把字符串常量的引用赋值给一个字符串变量(String variable)
String s1, s2;
s1 = "How are you";
s2 = "How are you";
- s1, s2具有相同的引用(reference),因而具有相同的实体(string value or content)。
- 由于字符串是不可变的,并且在编程中无处不在,因此JVM对具有相同字符序列的字符串字元使用唯一的实例,以提高效率和节省内存。
- 一个String变量持有对String对象的引用,该对象存储了一个字符串值。
注:大多数情况下,三者之间的差异可以忽略
String类的常用方法
public int length()
:获取长度
public boolean equals(String s)
:比较当前字符串对象的内容是否与参数指定的字符串s的内容相同
public boolean startsWith(String s)
:判断当前字符串对象的前缀是否是参数指定的字符串s
public boolean endsWith(String s)
:判断当前字符串对象的后缀是否是参数指定的字符串s
public int compareTo(String s)
:按字典序与参数s指定的字符串比较大小:
- 如果当前字符串与s相同,该方法返回值0
- 如果当前字符串对象大于s,该方法返回正值
- 如果小于s,该方法返回负值。
Example:
public class Example6_1
{
public static void main(String args[])
{
String s1,s2;
s1 = new String("we are students");
s2 = new String("we are students");
System.out.println(s1.equals(s2)); // true, same content?
System.out.println(s1==s2); // false, same reference?
System.out.println(s1.compareTo(s2)); // 0
}
}
public int indexOf(String s)
:从当前字符串的头开始检索字符串s,并返回首次出现s的位置。如果没有检索到字符串s,该方法返回的值是-1
public String substring(int startPoint)
:获得一个当前字符串的子串,该子串是从当前字符串的startPoint处截取到最后所得到的字符串
public String replaceAll(String s1, String s2)
:获得一个新的字符串对象,该字符串对象是通过用参数s2指定的字符串替换原字符串中由s1(regex)指定的所有字符串而得到的字符串
public String trim()
:获得一个新的字符串对象,该字符串对象是去掉前后空格后的字符串
字符串与基本数据的转化
Integer类中public static int parse(String s)
可以将字符串转化成int,在double等中也有类似静态方法。
对象的字符串表示
所有的类都默认是java.lang包中Object类的子类或间接子类。Object类有一个public方法toString(),一个对象通过调用该方法可以获得该对象的字符串表示。
字符串与字符数组、字节数组
-
字符串与字符数组
String类提供了将字符串存放到数组中的方法
public void getChars(int start, int end, char c[], int offset)
,字符串调用该方法将当前字符串中的一部分字符复制到参数c指定的数组中,将字符串中从位置start到end-1位置上的字符复制到数组c中,并从数组c的offset处开始存放这些字符。需要注意的是,必须保证数组c能容纳要被复制的字符。public char[] toCharArray()
:字符串对象调用该方法可以初始化一个字符数组,该数组的长度与字符串的长度相等,并将字符串对象的全部字符复制到该数组中。 -
字符串与字节数组
String(byte[])
:用指定的字节数组构造一个字符串对象String(byte[], int offset, int length)
:用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象
public byte[] getBytes()
:使用平台默认的字符编码,将当前字符串转化为一个字节数组
6.2 StringBuffer类
String类创建的字符串对象是不可修改的(不能修改、删除或替换字符串中的某个字符),即String对象一旦创建,那么实体是不可以再发生变化的。
StringBuffer类能创建可修改的字符串序列,也就是说,该类的对象的实体的内存空间可以自动改变大小,便于存放一个可变的字符串。
StringBuffer类的构造方法
- StringBuffer():分配给该对象的实体的初始容量(capacity)可以容纳16个字符,当该对象的实体存放的字符序列的长度大于16时,实体的容量自动增加,以便存放所增加的字符。
- StringBuffer(int size):指定分配给该对象的实体的初始容量为参数size指定的字符个数,当该对象的实体存放的字符序列的长度大于size个字符时,实体的容量自动增加,以便存放所增加的字符。
- StringBuffer(String s):指定分配给该对象的实体的初始容量为参数字符串s的长度额外再加16个字符。
StringBuffer对象可以通过:
-
length()
方法获取实体中存放的字符序列的长度(length) -
capacity()
方法获取当前实体的实际容量(capacity)
StringBuffer的常用方法
- append():可以将其它Java类型数据转化为字符串后,再追加到StringBuffer对象中。
- char charAt(int index):得到参数index指定的位置上的单个字符。当前对象实体中的字符串序列的第一个位置为0,第二个位置为1,依次类推。index的值必须是非负的,并且小于当前对象实体中字符串序列的长度。
-
void setCharAt(int index, char ch):将当前StringBuffer对象实体中的字符串位置index处的字符用参数ch指定的字符替换。index的值必须是非负的,并且小于当前对象实体中字符串序列的长度。
- StringBuffer insert(int index, String str):将一个字符串插入另一个字符串中,并返回当前对象的引用。
- public StringBuffer reverse():将该对象实体中的字符串翻转,并返回当前对象的引用。
- StringBuffer delete(int startIndex, int endIndex):从当前StringBuffer对象实体中的字符串中删除一个子字符串,并返回当前对象的引用。这里startIndex指定了需删除的第一个字符的下标,而endIndex指定了需删除的最后一个字符的前一个字符的下标。因此要删除的子字符串从startIndex到endIndex-1。
- StringBuffer replace(int startIndex, int endIndex, String str):将当前StringBuffer对象实体中的字符串的一个子字符串用参数str指定的字符串替换。被替换的子字符串由下标startIndex和endIndex指定,即从startIndex到endIndex-1的字符串被替换。该方法返回当前StringBuffer对象的引用。
StringBuffer与StringBuilder
- 功能几乎完全相同
- StringBuffer是线程安全的,StringBuilder不是线程安全的(线程安全是指多个线程操作同一个对象不会出现问题)
- 如果字符串缓冲区被单个线程使用(这种情况很普遍),建议优先采用StringBuilder,因为效率高(而线程同步需要时间开销)
- 如果需要多线程同步,则建议使用StringBuffer
6.3 StringTokenizer类
当我们需要分析一个字符串并将字符串分解成可被独立使用的单词时,可以使用java.util包中的StringTokenizer类,该类有两个常用的构造方法:
- StringTokenizer(String s):为字符串s构造一个分析器。使用默认的分隔符集合,即空格符(多个空格被看做一个空格)、换行符’\n’、回车符’\r’、tab符’\t’、进纸符’\f’
- StringTokenizer(String s, String delim):为字符串s构造一个分析器,参数delim中的字符被作为分隔符
我们把一个StringTokenizer对象称作一个字符串分析器,字符串分析器封装了语言符号和对其进行操作的方法。
字符串分析器可以使用nextToken()
方法逐个获取字符串分析器中的语言符号(单词),每当获取到一个语言符号,字符串分析器中的负责计数的变量的值就自动减一,该计数变量的初始值等于字符串中的单词数目,字符串分析器调用countTokens()
方法可以得到计数变量的值。
字符串分析器通常用while循环来逐个获取语言符号,为了控制循环,我们可以使用StringTokenizer类中的hasMoreTokens()
方法,只要计数的变量的值大于0,该方法就返回true,否则返回false。
6.5 Scanner类
Scanner类不仅可以创建出用于读取用户从键盘输入的数据的对象,而且还可以创建出用于解析字符串的对象。
- 使用默认分隔标记解析字符串:
import java.util.*;
public class Example_Scanner1
{
public static void main (String args[])
{
String cost = " TV cost 877 dollar, Computer cost 2398";
Scanner scanner = new Scanner(cost);
double sum = 0;
while(scanner.hasNext())
{
try{
double price = scanner.nextDouble();
sum = sum + price;
System.out.println(price);
}
catch(InputMismatchException exp)
{
String t = scanner.next();
}
}
System.out.println("Sum: " + sum);
}
}
// ================
Output:
877.0
2398.0
Sum: 3275.0
- 正则表达式作为分隔标记解析字符串
Scanner对象可以调用useDelimiter()
方法将一个正则表达式作为分隔标记,即和正则表达式匹配的字符串都是分隔标记。
import java.util.*;
public class Example_Scanner2
{
public static void main (String args[])
{
String cost = "市话费: 176.89元, 长途费: 187.98元, 网络费: 928.66元";
Scanner scanner = new Scanner(cost);
scanner.useDelimiter("[^0123456789.]+");
while(scanner.hasNext())
{
try{
double price = scanner.nextDouble();
System.out.println(price);
}
catch(InputMismatchException exp)
{
String t = scanner.next();
}
}
}
}
// ======================
Output:
176.89
187.98
928.66
6.6 模式匹配
模式匹配就是检索和指定模式匹配的字符串。Java提供了专门用来进行模式匹配的类,这些类在java.util.regex包中。
建立模式对象
进行模式匹配的第一步就是使用Pattern类创建一个对象,称作模式对象。Pattern类调用静态方法compile(String pattern)
来完成这一任务,其中的参数pattern是一个正则表达式,称作模式对象使用的模式。
例如,我们使用正则表达式A\\d
”建立一个模式对象p
Pattern p = Pattern.compile("A\\d");
Pattern类也可以调用静态方法compile(String regex, int flags)
返回一个Pattern对象,例如参数flags可以取有效值Pattern.CASE_INSENSITIVE
来表示模式匹配的时候忽略大小写
建立匹配对象
模式对象p调用matcher(CharSequence input)
方法返回一个Matcher对象m(称作匹配对象),参数input可以是任何一个实现了CharSequence接口的类创建的对象,我们前面学习的String类和StringBuffer类都实现了CharSequence接口。
一个Matcher对象m可以使用下列3个方法寻找参数input指定的字符序列中是否有和pattern匹配的子序列(pattern是创建模式对象p时使用的正则表达式):
public boolean find()
:在input中寻找和pattern匹配的下一子序列public boolean matches()
:判断input是否完全和pattern匹配public boolean lookingAt()
:判断从input的开始位置是否有和pattern匹配的子序列public boolean find(int start)
:判断input从参数start指定位置开始是否有和pattern匹配的子序列,参数start取值0时,该方法和lookingAt()的功能相同public String replaceAll(String replacement)
:Matcher对象m调用该方法可以返回一个字符串对象,该字符串是通过把input中与pattern匹配的子字符串全部替换为参数replacement指定的字符串得到的(input本身没有发生变化)-
public String replaceFirst(String replacement)
:Matcher对象m调用该方法可以返回一个字符串对象,该字符串是通过把input中第一个与pattern匹配的子字符串替换为参数replacement指定的字符串得到的(input本身没有发生变化) public String group()
:返回匹配的字符串
Example:
import java.util.regex.*;
public class Example6_8
{
public static void main(String args[])
{
Pattern p;
Matcher m;
String input = "0A1A2A3A4A5A6A7A8A9";
p = Pattern.compile("\\dA\\d"); // 生成模式对象
m = p.matcher(input); // 建立匹配对象
while(m.find())
{
String str = m.group();
System.out.print("From " + m.start() + " To " + m.end() + ": ");
System.out.println(str);
}
}
}
6.4 正则表达式及字符串的替换与分解
正则表达式
一个正则表达式是一些含有特殊意义字符的字符串,这些特殊字符称作正则表达式中的元字符。比如,“\\dok”中的\\d就是有特殊意义的元字符,代表0到9中的任何一个。
一个正则表达式也称作一个模式,字符串“9ok”和“1ok”都是和模式“\\dok”匹配的字符串之一。
和一个模式匹配的字符串称作匹配模式字符串,也称作模式匹配字符串。
元字符
.
:代表任何一个字符\d
:代表0~9的任意一个数组\D
:代表任意一个非数字字符\s
:代表空格类字符,’\t’、’\n’、’\x0B’等\S
:代表非空格类字符\w
:代表可用于标识符的字符(不包括美元符号)\W
:代表不能用与标识符的字符
限定修饰符
X?
:X出现0次或1次X*
:X出现0次或多次X+
:X出现1次或多次X{n}
:X恰好出现n次X{n,}
:X至少出现n次X{n, m}
:X出现n至m次
括号
在正则表达式(模式)中可以使用一对方括号括起若干个字符,代表方括号中的任何一个字符。例如:
pattern = "[159]ABC" // "1ABC"、"5ABC"、"9ABC"都是和模式pattern匹配的字符序列
[abc]
:代表a, b, c中的任何一个[^abc]
:代表除了a, b, c以外的任何字符[a-d]
:代表a至d中的任何一个
另外,方括号里允许嵌套方括号,可以进行并、交、差运算
[a-d[m-p]]
:代表a至d,或m至p中的任何字符(并)[a-z&&[def]]
:代表d, e或f中的任何一个(交)[a-f&&[^bc]]
:代表a, d, e, f(差)
pattern的或运算
模式可以使用**“ | ”**位运算符进行逻辑“或”运算得到一个新模式。例如,pattern1、pattern2是两个模式,即两个正则表达式。那么,一下表达式就是两个模式的“或”。一个字符串如果匹配模式pattren1或匹配模式pattern2,那么就匹配模式pattern。 |
pattern=pattern1|pattern2;
字符串的替换
public String replaceAll(String regex, String replacement)方法返回一个字符串,该字符串是当前字符串中所有与参数regex指定的正则表达式匹配的字符串被参数replacement指定的字符串替换后的字符串。
\p{Alpha}
表示字母字符
字符串的分解
public String[] split(String regex):使用参数指定的正则表达式regex作为为分隔标记分解出其中的单词,并将分解出的单词存放在字符串数组中 。
\\p{Punct} 表示标点符号 *!”#$%&’()+,-./:;<=>?@[]^_`{ |
}~** |
import java.util.Scanner;
public class Example6_11
{
public static void main (String args[])
{
Scanner reader = new Scanner(System.in);
String str = reader.nextLine();
String regex = "[\\s\\d\\p{Punct}]+"; // 空格字符、数字和符号(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)组成的正则表达式
String words[] = str.split(regex); // 以regex为格式进行分割
for(int i=0; i<words.length; i++)
{
int m = i+1;
System.out.println("Word" + m + ":" + words[i]);
}
}
}
// Input:Shenzhen university @china
// Output:
// Word1:shenzhen
// Word2:university
// Word3:china
Ch-07
7.1 Date 类
Date对象
Date类在java.util包中
使用Date类的无参数构造方法创建的对象可以获取本地当前时间。
用Date的构造方法Date(long time)
创建的Date对象表示相对1970年1月1日0点(Greenwich Mean Time, GMT, 格林威治标准时间)的时间。例如,参数time取值60*60*1000毫秒表示Thu Jan 01 01:00:00 GMT 1970。
由于北京时间,China Standard Time,中国标准时间在时区划分上,属东八区,比协调世界时早8小时,记为UTC+8,所以输出时间会多8h。
可以用System类的静态方法public long currentTimeMillis()
获取系统当前时间,这个时间是从1970年1月1日0点(GMT)到目前时刻所走过的毫秒数。
格式化时间
Date对象表示时间的默认顺序:星期 月 日 小时 分 秒 年
Sat Apr 28 21:59:38 CST 2001
如果我们可能希望按照某种习惯来输出时间
-
年 月 星期 日
-
年 月 星期 日 小时 分 秒
可以使用DateFormat的子类SimpleDateFormat
来实现日期的格式化
SimpleDateFormat构造方法:public SimpleDateFormat(String pattern)
// 用参数pattern指定的格式创建一个对象sdf
String pattern = "yyyy-MM-dd";
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
// 用public String format(Date date)方法格式化时间对象
Date currentTime = new Date();
String currenTime2 = sdf.format(currentTime);
常用时间元字符
y, yy
:2位数字年份,如14
yyyy
:4位数字年份,如2014
M, MM
:2位数字月份,如08
MMM
:汉字月份,如八月
d, dd
:2位数字日期,如09, 22
a
:上午或下午
H, HH
:2位数字小时(00-23)
h, hh
:2位数字小时(am/pm,01-12)
m, mm
:2位数字分
s, ss
:2位数字秒
E, EE
:星期
注:关于pattern中的普通字符(非时间元字符),如果是ASCII字符集中的字符,必须用”'”转义符:” ‘Time’ yyyy-MM-dd”
7.2 Calendar类
Calendar类在java.util包中。
使用Calendar类的static方法getInstance()
可以初始化一个日历对象
然后,calendar对象可以调用方法,将日历翻到任何一个时间,当参数year取负数时表示公元前。:
public final void set(int year, int month, int date)
public final void set(int year, int month, int date, int hour, int minute)
public final void set(int year, int month, int date, int hour, int minute, int second)
calendar对象调用方法public int get(int field)
可以获取有关年份、月份、小时、星期等信息,参数field的有效值由Calendar的静态常量指定,如calendar.get(Calendar.MONTH);
,返回一个整数,0表示一月,1表示2月等
7.3 Math类与BigInteger类
Math类
在编写程序时,可能需要计算一个数的平方根、绝对值、获取一个随机数等。java.lang包中的类包含许多用来进行科学计算的静态方法(static methods,又称类方法),这些方法可以直接通过类名调用。
另外,Math类还有两个静态常量,E和PI,它们的值分别是:
- 2.7182828284590452354
- 3.14159265358979323846
Math类常用方法
public static long abs(double a)
:返回a的绝对值
public static double max(double a, double b)
:返回a, b的最大值
public static double min(double a, double b)
:返回a, b的最小值
public static double random()
:产生一个0到1之间的随机数,范围是[0,1)
public static double pow(double a, double b)
:返回a的b次幂
public static double sqrt(double a)
:返回a的平方根
public static double log(double a)
:返回a的对数
public static double sin(double a)
:返回正弦值
public static double asin(double a)
:返回反正弦值
BigInteger类
程序有时需要处理大整数,java.math包中的BigInteger类提供任意精度的整数运算。可以使用如下构造方法创建一个十进制的BigInteger对象public BigInteger(String val)
,参数val中如果含有非数字字符就会发生NumberFormatException异常。
BigInteger类的常用方法
public BigInteger add(BigInteger val)
:返回当前大整数对象与参数指定的大整数对象的和
public BigInteger subtract(BigInteger val)
:返回当前大整数对象与参数指定的大整数对象的差
public BigInteger multiply(BigInteger val)
:返回当前大整数对象与参数指定的大整数对象的积
public BigInteger divide(BigInteger val)
:返回当前大整数对象与参数指定的大整数对象的商
public BigInteger remainder(BigInteger val)
:返回当前大整数对象与参数指定的大整数对象的余
public int compareTo(BigInteger val)
:返回当前大整数对象与参数指定的大整数的比较结果,返回值是1、-1或0,分别表示当前大整数对象大于、小于或等于参数指定的大整数
public BigInteger abs()
:返回当前大整数对象的绝对值
public BigInteger pow(int exponent)
:返回当前大整数对象的exponent次幂
public String toString()
:返回当前大整数对象十进制的字符串表示
public String toString(int p)
:返回当前大整数对象p进制的字符串表示
7.4 数字格式化
有时我们可能需要对输出的数字结果进行必要的格式化,例如,对于3.14356789,我们希望保留小数位为3位、整数部分至少要显示3位,即将3.14356789格式化为003.144。
可以使用java.text包中的NumberFormat类,该类调用如下静态方法来实例化一个NumberFormat对象:
// public static final NumberFormat getInstance()
NumberFormat f = NumberFormat.getInstance();
NumberFormat常用方法:
public void setMaximumFractionDigits(int newValue)
public void setMinimumFractionDigits(int newValue)
public void setMaximumIntegerDigits(int newValue)
public void setMinimumIntegerDigits(int newValue)
对象f可调用public final String format(double number)方法来格式化数字number。
Example:
import java.text.NumberFormat;
public class Example_Format
{
public static void main(String args[])
{
double a = Math.sqrt(10);
System.out.println("Before: " + a);
NumberFormat f = NumberFormat.getInstance();
f.setMaximumFractionDigits(5);
f.setMinimumIntegerDigits(3);
String s = f.format(a);
System.out.println("After: " + s);
}
}
7.5 LinkedList<E>泛型类
使用LinkedList<E>泛型类可以创建链表结构的数据对象。
链表是由若干个节点组成的一种数据结构,每个节点含有一个数据和下一个节点的引用(单链表),或含有一个数据并含有上一个节点的引用和下一个节点的引用(双链表),节点的索引从0开始。
链表适合动态地改变存储的数据,如,增加、删除节点等操作。
LinkedList<E>对象
java.util包中的LinkedList<E>泛型类创建的对象以链表结构存储数据,习惯上称LinkedList类创建的对象为链表对象。例如,LinkedList<String> mylist = new LinkedList<String>();
,创建一个空双链表。然后mylist可以使用add(String obj)
方法向链表依次增加节点,节点中的数据是参数obj指定对象的引用。
mylist.add("How");
mylist.add("Are");
mylist.add("You");
mylist.add("Java");
// 这时,双链表mylist就有了有4个节点,节点是自动连接在一起的,不需要我们去做连接,也就是说,不需要我们去操作安排节点中所存放的下一个或上一个节点的引用。
常用方法
public boolean add(E element)
:向链表末尾添加一个新的节点,该节点中的数据是参数element指定的对象。
public void add(int index, E element)
:向链表的指定位置添加一个新的节点,该节点中的数据是参数element指定的对象。
public void addFirst(E element)
:向链表的头添加新节点,该节点中的数据是参数element指定的对象。
public E removeFirst()
:删除第一个节点,并返回这个节点中的对象。
public E removeLast()
:删除最后一个节点,并返回这个节点中的对象。
public E get(int index)
:得到链表中指定位置处节点中的对象。
public E getFirst()
:得到链表中第一个节点中的对象。
public E getLast()
:得到链表中最后一个节点中的对象。
public int indexOf(E element)
:返回含有数据element的节点在链表中首次出现的位置,如果链表中无此节点则返回-1。
public int lastIndexOf(E element)
:返回含有数据element的节点在链表中最后出现的位置,如果链表中无此节点则返回-1。
public E set(int index, E element)
:将当前链表index位置节点中的对象替换为参数element指定的对象,并返回被替换的对象。
public int size()
:返回链表的长度,即节点的个数。
public boolean contains(Object element)
:判断链表节点中是否有节点包含对象element。
public Object clone()
:得到当前链表的一个克隆链表,该克隆链表中节点数据的改变不会影响到当前链表中节点的数据,反之亦然。
迭代器访问:
public class Example7_7
{
public static void main(String args[])
{
LinkedList<Student> mylist = new LinkedList<Student>();
Student stu1 = new Student("S1",78);
Student stu2 = new Student("S2",98);
mylist.add(stu1);
mylist.add(stu2);
Iterator<Student> iter = mylist.iterator();
while(iter.hasNext())
{
Student temp = iter.next();
System.out.printf("%s:%d\n",temp.name,temp.score);
}
}
}
LinkedList<E>泛型类实现的接口
LinkedList<E>泛型类实现了泛型接口List<E>,而List<E>接口是Collection<E>接口的子接口。
LinkedList<E>类中的绝大部分方法都是接口方法的实现。
编程时,可以使用接口回调技术,即把LinkedList<E>对象的引用赋值给Collection<E>接口变量或List<E>接口变量,那么接口就可以调用类实现的接口方法。
JDK1.5之前的LinkedList类
JDK1.5之前没有泛型的LinkedList类,可以用普通的LinkedList创建一个链表对象,例如:LinkedList mylist = new LinkedList();
创建了一个空双链表。然后mylist链表可以使用add(Object obj)方法向这个链表依次添加节点。由于任何类都是Object类的子类,因此可以把任何一个对象作为链表节点中的对象。
需要注意的是当使用get()
获取一个节点中的对象,要用类型转换运算符转换回原来的类型。
Java泛型的主要目的是可以建立具有类型安全的集合框架(Java Collections Framework),如链表、散列表等数据结构,最重要的一个优点就是:在使用这些泛型类建立的数据结构时,不必进行强制类型转换,即不要求进行运行时类型检查(在编译阶段已经完成检查)。
JDK1.5是支持泛型的编译器,它将运行时类型检查提前到编译时执行,使代码更安全。如果你使用旧版本的LinkedList类,1.5编译器会给出警告信息,但程序仍能正确运行。
以下为旧版本的LinkedList:
import java.util.*;
public class Example7_9
{
public static void main(String args[])
{
LinkedList mylist = new LinkedList();
mylist.add("A");
mylist.add(1);
String str = (String) mylist.get(0); // 必须强制转换取出的数据,否则报错
System.out.println(str);
int num = (int) mylist.get(1); // 必须强制转换取出的数据,否则报错
System.out.println(num);
}
}
7.6 HashSet<E>泛型类
HashSet<E>泛型类在数据组织上类似数学上的集合,可以进行”交”、”并”、”差”等运算。
HashSet<E>对象
HashSet<E>泛型类创建的对象称作集合,例如HashSet<String> set = new HashSet<String>();
,对象set是一个可以存储String类型数据的集合,可以调用add(String s)
方法将String类型的数据添加到集合中,添加到集合中的数据称做集合的元素。
集合不允许有相同的元素,也就是说,如果b已经是集合中的元素,那么再执行set.add(b)
操作是无效的。
集合对象的初始容量(capacity)是16个字节,装载因子(load factor)是0.75,也 就是说,如果集合添加的元素超过总容量的75%时,集合的容量将增加一倍。
常用方法
public boolean add(E o)
:向集合添加参数指定的元素。
public void clear()
:清空集合,使集合不含有任何元素。
public boolean contains(Object o)
:判断集合是否包含参数指定的数据。
public boolean isEmpty()
:判断集合是否为空。
public boolean remove(Object o)
:删除参数指定的元素。
public int size()
:返回集合中元素的个数。
Object[] toArray()
:将集合元素存放到数组中,并返回这个数组。
boolean containsAll(HanshSet set)
:判断当前集合是否包含参数指定的集合。
public Object clone()
:得到当前集合的一个克隆对象,该对象中元素的改变不会影响到当前集合中的元素,反之亦然。
集合的交、并与差
集合对象调用boolean addAll(HashSet set)
方法可以和参数指定的集合求并运算,使得当前集合成为两个集合的并。
集合对象调用boolean retainAll(HashSet set)
方法可以和参数指定的集合求交运算,使得当前集合成为两个集合的交。
集合对象调用boolean removeAll(HashSet set)
方法可以和参数指定的集合求差运算,使得当前集合成为两个集合的差。
参数指定的集合和当前集合必须是同种类型的集合,否则上述方法返回false。
HashSet<E>泛型类实现的接口
HashSet<E>泛型类实现了泛型接口Set<E>,而 Set<E>接口是Collection<E>接口的子接口。
HashSet<E>类中的绝大部分方法都是接口方法的实现。
编程时,可以使用接口回调技术,即把HashSet<E>对象的引用赋值给Collection<E>接口变量或Set<E>接口变量,那么接口就可以调用类实现的接口方法。
7.7 HashMap<K, V>泛型类
HashMap<K,V>也是一个很实用的类,HashMap<K,V>对象采用散列表这种数据结构存储数据,习惯上称HashMap<K,V>对象为散列映射对象。
散列映射用于存储键/值数据对,允许把任何数量的键/值数据对存储在一起。
键(Key)不可以发生逻辑冲突,即不要对两个数据项使用相同的键,如果出现两个数据项使用相同的键,那么,先前散列映射中的键/值对将被替换。
散列映射在它需要更多的存储空间时会自动增大容量:
例如,如果散列映射的装载因子是0.75,那么当散列映射的容量被使用了75%时,它就把容量增加到原始容量的2倍。
对于数组和链表这两种数据结构,如果要查找它们存储的某个特定的元素却不知道它的位置,就需要从头开始访问元素直到找到匹配的为止;如果数据结构中包含很多的元素,就会浪费时间。 这时最好使用散列映射来存储要查找的数据,使用散列映射可以减少检索的开销。
HashMap<K, V>泛型类
HashMap<K,V>泛型类创建的对象称作散列映射,例如:HashMap<String, Student> hashtable = new HashMap<String, Student>();
那么,hashtable就可以存储”键/值”对数据,其中的键必须是一个String对象,键对应的值必须是Student对象。hashtable可以调用public V put(K key, V value)将键/值对数据存放到散列映射中,该方法同时返回键所对应的值。
常用方法
public void clear()
:清空散列映射。
public Object clone()
:返回当前散列映射的一个克隆。
public boolean containsKey(Object key)
:如果散列映射有键/值对使用了参数指定的键,方法返回true,否则返回false。
public boolean containsValue(Object value)
:如果散列映射有键/值对的值是参数指定的值,方法返回true,否则返回false。
public V get(Object key)
:返回散列映射中使用key做键的键/值对中的值。
public boolean isEmpty()
:如果散列映射不含任何键/值对,方法返回true,否则返回false。
public V remove(Object key)
:删除散列映射中键为参数指定的键/值对,并返回键对应的值。
public int size()
:返回散列映射的大小,即键/值对的数目。
遍历散列映射
如果想获得散列映射中所有键/值对中的值,首先使用public Collection<V> values()
该方法返回一个实现Collection<V>接口的类创建的对象的引用,并要求将该对象的引用返回到Collection<V>接口变量中。values()方法返回的对象中存储了散列映射中所有”键/值”对中的”值”,这样接口变量就可以调用类实现的方法,比如获取Iterator对象,然后输出所有的值。
Example:
import java.util.*;
public class Example7_12
{
public static void main(String args[])
{
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("a", 1);
map.put("b", 2);
Collection<Integer> collection = map.values();
Iterator<Integer> iter = collection.iterator();
while(iter.hasNext())
{
Integer temp = iter.next();
System.out.println(temp.toString());
}
}
}
HashMap<K,V>泛型类实现的接口
HashMap<K,V>泛型类实现了泛型接口Map<K,V>,HashMap<K,V>类中的绝大部分方法都是Map<K,V>接口方法的实现。
编程时,可以使用接口回调技术,即把HashMap<K,V>对象的引用赋值给Map<K,V>接口变量,那么接口就可以调用类实现的接口方法。
7.8 TreeSet<E>泛型类
TreeSet<E>类是实现Set接口的类,它的大部分方法都是接口方法的实现。TreeSet<E>泛型类创建的对象称作树集,例如:TreeSet<Student> tree = new TreeSet<Student>();
,那么,tree就是一个可以存储Student类型数据的集合,tree可以调用add()
方法将Student类型的数据添加到树集中,存放到树集中的对象按对象的字符串表示升序排列。
TreeSet<E>类的常用方法
public boolean add(E o)
:向树集添加对象,添加成功返回true,否则返回false。
public void clear()
:删除树集中的所有对象。
public void contains(Object o)
:如果包含对象o,方法返回true,否则返回false 。
public E first()
:返回树集中的第一个对象(最小的对象)。
public E last()
:返回最后一个对象(最大的对象)。
public isEmpty()
:判断是否是空树集,如果树集不含对象返回true 。
public boolean remove(Object o)
:删除树集中的对象o。
public int size()
:返回树集中对象的数目。
对象调用toString()方法就可以获得自己的字符串表示,但很多对象不适合按照字符串排列大小,我们在创建树集时可自己规定树集中的对象按着什么样的”大小”顺序排列:
import java.util.*;
class Student implements Comparable
{
String name;
int score;
Student(String name, int score)
{
this.name = name;
this.score = score;
}
public int compareTo(Object o)
{
Student stu = (Student)o;
return (this.score - stu.score);
}
}
public class Example7_13
{
public static void main(String args[])
{
TreeSet<Student> mytree = new TreeSet<Student>();
Student stu1 = new Student("S1",78);
Student stu2 = new Student("S2",98);
mytree.add(stu1);
mytree.add(stu2);
Iterator<Student> iter = mytree.iterator();
while(iter.hasNext())
{
Student temp = iter.next();
System.out.printf("%s:%d\n",temp.name,temp.score);
}
}
}
注:树集中不容许出现大小相等的两个节点,例如,在上述例子中如果再添加语句:
Student stu3 = new Student("S3",98);
mytree.add(stu3);
是无效的。如果允许成绩相同,可把上述例子中Student类中的compareTo方法更改为:
public int compareTo(Object o)
{
Student stu = (Student)o;
if(this.score==stu.score)
return 1;
else
return (this.score - stu.score);
}
Comparator接口
Comparator是java.util包中的一个接口,compare(Object o1,Object o2)
是接口中的方法。
import java.util.*;
class Student
{
String name;
int score;
Student(String name, int score)
{
this.name = name;
this.score = score;
}
}
class StudentComparator implements Comparator // 实现Comparator接口
{
public int compare(Object o1, Object o2)
{
return ( ((Student)o1).score - ((Student)o2).score);
}
}
public class ExampleComparator
{
public static void main(String args[])
{
Student [] students = new Student[]{new Student("S1",78), new Student("S2",98)};
Arrays.sort(students, new StudentComparator()); // 参数中传入StudentComparator接口对象
for(int i=0; i<students.length; i++)
{
Student temp = students[i];
System.out.printf("%s:%d\n",temp.name,temp.score);
}
}
}
Comparator与Comparable接口的区别
一个类实现了Comparable接口,表明这个类的对象之间是可以相互比较的(i.e., it is comparable),这个类对象组成的集合就可以直接使用sort方法排序。
Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:
- 类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
- 可以使用多种排序标准,比如升序、降序等
7.9 TreeMap<K, V>泛型类
TreeMap类实现了Map接口。TreeMap提供了按排序顺序存储”关键字/值”对的有效手段。
应该注意的是,不像散列映射(HashMap),树映射(TreeMap)保证它的元素按照关键字升序排列。下面是TreeMap构造方法:
- 第一种形式构造的树映射,按关键字的大小顺序来排序树映射中的”键/值”对,键的大小顺序是按其字符串表示的字典顺序。
- 第二种形式构造的树映射,键的大小顺序按comp接口规定的大小顺序来排序树映射中的”键/值”对。
TreeMap类的常用方法与HashMap<K,V>类相似。
7.10 Stack<E>泛型类
栈是一种“后进先出”的数据结构,只能在一端进行输入或输出数据的操作。栈把第一个放入该栈的数据放在最底下,而把后续放入的数据放在已有数据的顶上。
向栈中输入数据的操作称为”压栈”,从栈中输出数据的操作称为”弹栈”。由于栈总是在顶端进行数据的输入输出操作,所以弹栈总是输出(删除)最后压入堆栈中的数据,这就是”后进先出”的来历。
使用java.util包中的Stack类创建一个堆栈对象
常用方法:
public E push(E item)
:压栈
public E pop()
:弹栈
public boolean empty()
:判断栈是否还有数据
public E peek()
:获取栈顶端的数据,但不删除该数据
public int search(Object data)
:获取数据在栈中的位置,最顶端的位置是1,向下依次增加,如果栈不含此数据,则返回-1
栈是很灵活的数据结构,使用栈可以节省内存的开销。 比如,递归是一种很消耗内存的算法,我们可以借助栈消除大部分递归,达到和递归算法同样的目的。 Example:斐波那契整数序列( Fibonacci sequence)是我们熟悉的一个递归序列,它的第n项是前两项的和,第一项和第二项都是1。
import java.util.*;
public class Example7_15
{
public static void main(String args[])
{
Stack<Integer> stack=new Stack<Integer>();
stack.push(new Integer(1));
stack.push(new Integer(1));
int k=1;
while(k<=5)
{
Integer F1 = stack.pop();
int f1 = F1.intValue();
Integer F2 = stack.pop();
int f2 = F2.intValue();
Integer temp = new Integer(f1+f2);
System.out.println(temp.toString());
stack.push(temp);
stack.push(F2);
k++;
}
}
}
Ch-08
8.1 Java中的线程
程序是一段静态的代码,它是应用软件执行的蓝本。
进程是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡(完成)的过程。
线程是比进程更小的执行单位。一个进程在其执行过程中,可以产生多个线程,形成多条执行线索,每条线索(即每个线程)也有它自身的产生、存在和消亡的过程,也是一个动态的概念。
Java应用程序总是从主类(main class)的main()
方法开始执行。
当JVM加载代码,发现main()方法之后,就会启动一个线程,这个线程称作“主线程”,该线程负责执行main()方法。 那么,在main()方法中再创建的线程,就称为主线程中的线程。
如果main()方法中没有创建其他线程,那么当main()方法执行完最后一条语句,即main()方法返回时,JVM就会结束我们的Java应用程序。 如果main()方法中又创建了其他线程,那么JVM就要在主线程和其他线程之间轮流切换,保证每个线程都有机会使用CPU资源,main()方法即使执行完最后的语句,JVM也不会结束我们的程序,JVM一直要等到主线程中的所有线程都结束之后,才结束我们的Java应用程序。
8.2 线程的生命周期
线程的4种状态 在Java语言中,Thread类或其子类创建的对象称作线程,新建的线程在它的一个完整的生命周期中通常要经历4种状态。
-
新建 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。此时,已经有了相应的内存空间和其他资源。
-
运行 线程创建后仅仅是占有了内存资源,在JVM管理的线程中还没有这个线程,此线程必须调用
start()
方法(是一个从父类继承的方法)通知JVM,这样JVM就知道又有一个新的线程排队等候切换了。当JVM将CPU使用权切换给线程时,如果线程是Thread类的子类创建的,该类中的
run()
方法就立刻执行。所以我们必须在子类中重写(override)父类的run()方法。Thread类中的run()方法没有具体内容,程序要在Thread类的子类中重写run()方法来覆盖父类的run()方法。run()方法规定了该线程的具体使命。在线程没有结束run()方法之前,不要让线程再调用start()方法,否则将发生ILLegalThreadStateException异常。
-
中断 有4种原因的中断:
-
JVM将CPU资源从当前线程切换给其他线程,使本线程让出CPU的使 用权,进而处于中断状态。
-
线程使用CPU资源期间,执行了
sleep(int millsecond)
方法,使当前线程进入休眠状态。sleep(int millsecond)方法是Thread类中的一个类方法/静态方法(static method),线程一旦执行了sleep(int millsecond)方法,就立刻让出CPU的使用权,使当前线程处于中断状态。经过参数millsecond指定的毫秒之后,该线程就重新进到线程队列中排队等待CPU资源,以便从中断处继续运行。
- 线程使用CPU资源期间,执行了
wait()
方法,使得当前线程进入中断(等待)状态。等待状态的线程不会主动进到线程队列中排队等待CPU资源,必须由其他线程调用notify()
方法通知它,使得它重新进到线程队列中排队等待CPU资源,以便从中断处继续运行。有关wait(), notify()和notifyAll()方法将在第8节详细讨论。
- 线程使用CPU资源期间,执行了
-
线程使用CPU资源期间,执行某个操作进入中断(阻塞)状态,比如执行读/写操作引起阻塞。进入阻塞状态时线程不能进入排队队列,只有当引起阻塞的原因消除时,线程才重新进到线程队列中排队等待CPU资源,以便从原来中断处开始继续运行。
-
-
死亡 处于死亡状态的线程不具有继续运行的能力。线程死亡的原因:
- 正常运行的线程完成了它的全部工作,即执行完run()方法中的全部语句,结束了run()方法。
- 线程被提前强制终止,即强制run()方法结束。
所谓死亡状态就是线程释放了实体,即释放了分配给线程对象的内存。
Example(多次运行结果不同):
class WriteWordThread extends Thread
{
WriteWordThread(String s)
{
setName(s);
}
public void run()
{
for(int i=1; i<=3;i++)
System.out.println("Thread: " + getName());
}
}
public class Example8_1
{
public static void main(String args[])
{
WriteWordThread zhang, wang;
zhang = new WriteWordThread("Zhang"); //新建线程
wang = new WriteWordThread("Wang"); //新建线程
zhang.start(); //启动线程
for(int i=1; i<=3; i++)
{
System.out.println("Main Thread");
}
wang.start(); //启动线程
}
}
上述程序在不同的计算机运行或在同一台计算机反复运行的结果不尽相同,输出结果依赖当前CPU资源的使用情况。
为了使结果尽量不依赖当前CPU资源的使用情况,我们应当让线程主动调用sleep方法让出CPU的使用权进入中断状态。
8.3 线程的优先级与调度管理
JVM中的线程调度器负责管理线程,调度器把线程的优先级分为10个级别,分别用Thread类中的类常量(即由static final修饰的变量)表示。每个Java线程的优先级都在常数1(Thread.MIN_PRIORITY)到常数10(Thread.MAX_PRIORITY)的范围内。
如果没有明确设置线程的优先级,每个线程的优先级都为常数5(Thread.NORM_PRIORITY)。
线程的优先级可以通过setPriority(int grade)方法调整,这一方法需要一个int类型参数。如果此参数不在1-10的范围内,那么setPriority便产生一个lllegalArgumentException异常。 getPriority方法返回线程的优先级。需要注意是,有些操作系统只能识别3个级别:1、5和10。
JVM中的线程调度器使高优先级的线程能始终运行:
如果有A,B,C,D四个线程,A和B的优先级高于C和D,那么调度器首先以轮流的方式执行A和B,一直等到A和B都执行完毕进入死亡状态,才会在C和D之间轮流切换。
8.4 Thread的子类创建线程
在Java语言中,用Thread类或其子类创建线程对象。这一节将讲述怎样用Thread子类创建对象。 用户可以继承Thread类,但需要重写父类的run()方法,其目的是规定线程的具体操作,否则线程就什么也不做,因为父类的run()方法中没有任何操作语句。
下面例子3中除主线程外还有两个线程,这两个线程分别在命令行窗口的左侧和右侧顺序地一行一行地输出字符串。主线程负责判断输出的行数,当其中任何一个线程输出8行后,就结束进程。本例题中用到了System类中的类方法exit(int n)
,主线程使用该方法结束整个程序。
Example:
public class Example8_3
{
public static void main(String args[])
{
Left left = new Left();
Right right = new Right();
left.start();
right.start();
while(true)
{
System.out.println(left.n + "," + right.n);
if(left.n>=8 || right.n>=8)
System.exit(0);
}
}
}
8.5 Runnable接口
使用Thread子类创建线程的优点是:我们可以在子类中增加新的成员变量,使线程具有某种属性,也可以在子类中增加新的方法,使线程具有某种功能。但是,Java不支持多继承,Thread类的子类不能再扩展/继承其他的类。
Runnable接口与目标对象
创建线程的另一个途径就是用Thread类直接创建线程对象。使用Thread类创建线程对象时,通常使用的构造方法是Thread(Runnable target)
该构造方法中的参数是一个Runnable类型的接口,因此,在创建线程对象时必须向构造方法的参数传递一个实现Runnable接口的类的实例,该实例对象称作所创建线程的目标对象(又称为目标任务task),当线程调用start()方法后,一旦轮到它来享用CPU资源,目标对象就会自动调用接口中的run()方法(接口回调),这一过程是自动实现的,用户程序只需要让线程调用start()方法即可。
下面的例子4中,两个线程:zhang和cheng,使用同一目标对象。两个线程共享目标对象的money。当money的值小于100时,线程zhang结束自己的run()方法进入死亡状态;当money的值小于60时,线程cheng结束自己的run()方法进入死亡状态。
class Bank implements Runnable
{
private int money = 0; String name1,name2; // 全局变量,同一目标的不同线程共享
Bank(String s1, String s2){ name1 = s1; name2 = s2; }
public void setMoney(int mount){ money = mount; }
public void run()
{
while(true)
{
money = money-10;
if(Thread.currentThread().getName().equals(name1)){
System.out.println(name1 + ": " + money);
if(money<=100){
System.out.println(name1 + ": Finished");
return;
}
}
else
if(Thread.currentThread().getName().equals(name2)){
System.out.println(name2 + ": " + money);
if(money<=60){
System.out.println(name2 + ": Finished");
return;
}
}
try{ Thread.sleep(800); }
catch(InterruptedException e) {}
}
}
}
public class Example8_4
{
public static void main(String args[])
{
String s1="treasurer"; // 会计
String s2="cashier"; // 出纳
Bank bank = new Bank(s1,s2);
bank.setMoney(120);
Thread zhang;
Thread cheng;
zhang = new Thread(bank); // 目标对象bank
cheng = new Thread(bank); // 目标对象bank
zhang.setName(s1);
cheng.setName(s2);
zhang.start();
cheng.start();
}
}
下面的例子5中共有4个线程:threadA、threadB、threadC和threadD threadA和threadB的目标对象a1 threadA和threadB共享a1的成员number
threadC和threadD的目标对象是a2 threadC和threadD共享a2的成员number
class TargetObject implements Runnable
{
private int number = 0;
public void setNumber(int n)
{
number = n;
}
public void run()
{
while(true)
{
if(Thread.currentThread().getName().equals("add"))
{
number++;
System.out.printf("%d\n",number);
}
if(Thread.currentThread().getName().equals("sub"))
{
number--;
System.out.printf("%12d\n",number);
}
try{ Thread.sleep(1000); }
catch(InterruptedException e) {}
}
}
}
public class Example8_5
{
public static void main(String args[])
{
TargetObject a1 = new TargetObject();
a1.setNumber(10);
TargetObject a2 = new TargetObject();
a2.setNumber(-10);
Thread threadA,threadB,threadC,threadD;
threadA = new Thread(a1); // 目标对象a1
threadB = new Thread(a1); // 目标对象a1
threadA.setName("add");
threadB.setName("add");
threadC = new Thread(a2); // 目标对象a2
threadD = new Thread(a2); // 目标对象a2
threadC.setName("sub");
threadD.setName("sub");
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}
关于run()方法中的局部变量
对于具有相同目标对象的线程,当其中一个线程享用CPU资源时,目标对象自动调用接口中的run()方法,当轮到另一个线程享用CPU资源时,目标对象会再次调用接口中的run()方法。不同线程的run()方法中的局部变量互不干扰,一个线程改变了自己的run()方法中的局部变量的值不会影响其他线程的run()方法中的局部变量。
8.6 线程的常用方法
start()
线程调用该方法将启动线程,使之从新建状态进入就绪队列排队,一旦轮到它来享用CPU资源时,就可以脱离创建它的主线程独立开始自己的生命周期了。
run()
Thread类的run()方法与Runnable接口中的run()方法的功能和作用相同,都用来定义线程对象被调度之后所执行的操作,都是系统自动调用而用户程序不得调用的方法。
sleep(int millsecond)
优先级高的线程可以在它的run()方法中调用sleep()方法来使自己放弃CPU资源,休眠一段时间。
isAlive()
在线程的run()方法结束之前,即没有进入死亡状态之前,线程调用isAlive()方法返回true。当线程进入死亡状态后(实体内存被释放),线程仍可以调用方法isAlive(),这时返回的值是false。线程未调用start()方法之前,调用isAlive()方法也返回false。
需要注意的是,一个已经运行的线程在没有进入死亡状态前,不要再给线程分配实体,由于线程只能引用最后分配的实体,先前的实体就会成为“垃圾”,并且不会被垃圾收集机制收集(请看例子7)。
现在让我们看一个例子(例子7),一个线程每隔1秒钟在命令行窗口输出机器的当前时间,在输出3次之后,该线程又被分配了实体,新实体又开始运行。这时,我们在命令行每秒钟能看见两行当前时间,因为垃圾实体仍然在工作 。
class A implements Runnable
{
Thread thread;
int n=0;
A(){ thread=new Thread(this); }
public void run()
{
while(true)
{
n++;
System.out.println(new java.util.Date());
try{ Thread.sleep(1000); }
catch(InterruptedException e) {}
if(n==3)
{
thread = new Thread(this);
thread.start();
}
}
}
}
public class Example8_7
{
public static void main(String args[])
{
A a = new A();
a.thread.start();
}
}
currentThread()
currentThread()方法是Thread类中的静态方法,可以用类名调用,该方法返回当前正在使用CPU资源的线程。
interrupt()
interrupt()方法经常用来“吵醒”休眠的线程。当一些线程调用sleep()方法处于休眠状态时,一个使用CPU资源的其它线程在执行过程中,可以让休眠的线程分别调用interrupt()方法“吵醒”自己,即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。
在下面的例子8中,有3个线程:zhang、li和teacher
- zhang和li准备休眠10秒钟后,再输出“Good morning”
- teacher线程在输出3句“Let’s start…”后,“吵醒”休眠的线程
class ClassRoom implements Runnable{
Thread teacher, zhang, li;
ClassRoom(){
teacher = new Thread(this); zhang = new Thread(this); li = new Thread(this);
zhang.setName("Zhang"); li.setName("Li"); teacher.setName("Pan");
}
public void run(){
Thread thread = Thread.currentThread();
if(thread==zhang || thread==li){
try{ System.out.println(thread.getName() + ": Sleep for 10s");
Thread.sleep(10000); }
catch(InterruptedException e){
System.out.println(thread.getName() + ": been wake up"); }
System.out.println(thread.getName() + ": Good morning");
}
else if(thread==teacher){
for(int i=1;i<=3;i++){
System.out.println(thread.getName() + ": Let's start ...");
try{ Thread.sleep(500); }
catch(InterruptedException e) {}
}
zhang.interrupt();
li.interrupt();
}
}
}
public class Example8_8
{
public static void main(String args[])
{
ClassRoom room = new ClassRoom();
room.zhang.start();
room.li.start();
room.teacher.start();
}
}
8.7 线程同步
线程同步是指多个线程要执行一个synchronized修饰的方法,如果一个线程A在占有CPU资源期间,使得synchronized方法被调用执行,那么在该synchronized方法返回之前(即synchronized方法调用执行完毕之前),其他占有CPU资源的线程一旦调用这个synchronized方法就会引起堵塞,堵塞的线程一直要等到堵塞的原因消除( 即synchronized方法返回),再排队等待CPU资源,以便使用这个同步方法。
在下面的例子9中有两个线程:treasurer和cashier,他们共同拥有一个帐本。他们都可以使用saveOrTake(int number)对帐本进行访问, treasurer使用saveOrTake方法时,向帐本写入存钱记录;cashier使用saveOrTake方法时,向帐本写入取钱记录。因此,当treasurer正在使用saveOrTake方法时,cashier被禁止使用,反之也是这样。
class Bank implements Runnable{
int money = 300;
String treasurerName, cashierName;
public Bank(String s1,String s2){ treasurerName=s1; cashierName=s2; }
public void run(){ saveOrTake(30); }
public synchronized void saveOrTake(int number){
if(Thread.currentThread().getName().equals(treasurerName)){
for(int i=1;i<=3;i++){
money = money + number;
try { Thread.sleep(1000); }
catch(InterruptedException e) {}
System.out.println(treasurerName + " : " + money);
}
}
else if(Thread.currentThread().getName().equals(cashierName)){
for(int i=1;i<=2;i++){
money = money-number/2;
try{ Thread.sleep(1000);}
catch(InterruptedException e){}
System.out.println(cashierName + " : " + money);
}
}
}
}
public class Example8_9
{
public static void main(String args[])
{
String treasurerName = "Treaurer", cashierName = "Cashier";
Bank bank = new Bank(treasurerName, cashierName);
Thread treasurer, cashier;
treasurer = new Thread(bank); // 目标对象bank
cashier = new Thread(bank); // 目标对象bank
treasurer.setName(treasurerName);
cashier.setName(cashierName);
treasurer.start();
cashier.start();
}
}
8.8 使用wait(),notify(),notifyAll()协调同步线程
wait(), notify()和notifyAll()
都是Object类中的final方法,是被所有的类继承、且不允许重写的方法。
当一个线程使用同步方法中的某个变量,而此变量又需要其它线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()
方法。使用wait()方法可以中断方法的执行,使本线程等待,暂时让出CPU的使用权,并允许其它线程使用这个同步方法。其它线程如果在使用这个同步方法时不需要等待,那么它使用完这个同步方法的同时,应当用notifyAll()
方法通知所有由于使用这个同步方法而处于等待的线程结束等待。
Example:
在下面的例子10中,模拟3个人排队买票,每人买1张票。售票员只有1张五元的钱,电影票五元钱一张。 Zhang拿1张二十元的人民币排在Sun前面买票,Sun拿1张十元的人民币排在Zhao的前面买票,Zhao拿1张五元的人民币排在最后。那么,最终的卖票次序应当是Sun、Zhao、Zhang 。
class TicketSeller
{
int fiveNumber=1, tenNumber=0, twentyNumber=0;
public synchronized void sellTicket(int receiveMoney)
{
String s=Thread.currentThread().getName();
if(receiveMoney==5)
{
fiveNumber = fiveNumber+1;
System.out.println(s + " gives $5 to seller, seller gives " + s + " a ticket");
}
else if(receiveMoney==10){
while(fiveNumber<1){
try{ System.out.println(s + " gives $10 to seller");
System.out.println("seller asks " + s + " to wait");
wait();
System.out.println(s + " stops waiting and starts to buy...");
}
catch(InterruptedException e){}
}
fiveNumber=fiveNumber-1;
tenNumber=tenNumber+1;
System.out.println(s + " gives $10 to seller, seller gives " + s + " a ticket and $5");
}
else if(receiveMoney==20){
while(fiveNumber<1||tenNumber<1){
try{ System.out.println(s + " gives $20 to seller");
System.out.println("seller asks " + s + " to wait");
wait();
System.out.println(s+" stops waiting and starts to buy ...");
}
catch(InterruptedException e){}
}
fiveNumber = fiveNumber-1;
tenNumber = tenNumber-1;
twentyNumber = twentyNumber+1;
System.out.println(s + " gives $20 to seller, seller gives " + s + " a ticket and $15");
}
notifyAll();
}
}
class Cinema implements Runnable
{
TicketSeller seller;
String name1, name2, name3;
Cinema(String s1,String s2,String s3)
{
seller = new TicketSeller();
name1 = s1;
name2 = s2;
name3 = s3;
}
public void run()
{
if(Thread.currentThread().getName().equals(name1))
{
seller.sellTicket(20);
}
else if(Thread.currentThread().getName().equals(name2))
{
seller.sellTicket(10);
}
else if(Thread.currentThread().getName().equals(name3))
{
seller.sellTicket(5);
}
}
}
public class Example8_10
{
public static void main(String args[])
{
String s1="Zhang", s2="Sun", s3="Zhao";
Cinema cinema = new Cinema(s1,s2,s3);
Thread zhang, sun, zhao;
zhang = new Thread(cinema); // 目标对象cinema
sun = new Thread(cinema); // 目标对象cinema
zhao = new Thread(cinema); // 目标对象cinema
zhang.setName(s1);
sun.setName(s2);
zhao.setName(s3);
zhang.start();
sun.start();
zhao.start();
}
}
8.9 挂起、恢复和终止线程
在下面的例子11中,线程thread每隔一秒钟输出一个整数,输出3个整数后,该线程挂起;主线程负责恢复thread线程继续执行。
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
class A implements Runnable{
int number = 0;
boolean stop = false;
boolean getStop(){ return stop; }
public void run(){
while(true){
number++;
System.out.println(Thread.currentThread().getName() + " : " + number);
if(number==3){
try{ System.out.println(Thread.currentThread().getName() + " : hang up");
stop = true;
hangUP();
System.out.println(Thread.currentThread().getName() + " : resumed");
}
catch(Exception e){}
}
try{ Thread.sleep(1000); }
catch(Exception e){}
}
}
public synchronized void hangUP() throws InterruptedException{ wait(); }
public synchronized void restart(){ notifyAll(); }
}
public class Example8_11
{
public static void main(String args[])
{
A target = new A();
Thread thread = new Thread(target);
thread.setName("Zhang San");
thread.start();
while(target.getStop()==false){}
System.out.println("Main Thread");
target.restart();
}
}
8.10 线程联合
一个线程A在占有CPU资源期间,可以让线程B调用join()
方法和本线程联合,如:B.join(); 我们称A在运行期间联合了B。
如果线程A在占有CPU资源期间一旦联合B线程,那么A线程将立刻中断执行,一直等到它联合的线程B执行完毕,A线程再重新排队等待CPU资源,以便恢复执行。如果A线程准备联合的B线程已经结束,那么B.join()不会产生任何效果。 在下面的例子13中,一个线程在运行期间联合了另外一个线程。
class JoinThread implements Runnable
{
Thread threadA, threadB;
String content[] = {"今天晚上,", "大家不要", "回去的太早,", "还有工作", "需要大家做!"};
JoinThread()
{
threadA=new Thread(this);
threadB=new Thread(this);
threadB.setName("经理");
}
public void run()
{
if(Thread.currentThread()==threadA){
System.out.println("我等"+threadB.getName()+"说完再说话");
threadB.start();
while(threadB.isAlive()==false){}
try{ threadB.join(); }
catch(InterruptedException e){}
System.out.printf("\n我开始说话:\"我明白你的意思了,谢谢\"");
}
else if(Thread.currentThread()==threadB){
System.out.println(threadB.getName()+"说:");
for(int i=0;i<content.length;i++){
System.out.print(content[i]);
try { threadB.sleep(1000); }
catch(InterruptedException e){}
}
}
}
}
public class Example8_13
{
public static void main(String args[])
{
JoinThread a = new JoinThread();
a.threadA.start();
}
}
8.11 守护线程
一个线程调用void setDaemon(boolean on)
方法可以将自己设置成一个守护(daemon)线程,例如:thread.setDaemon(true);
线程默认是非守护线程,非守护线程也称作用户(user)线程。
当程序中的所有用户线程都已结束运行时,即使守护线程的run()方法中还有需要执行的语句,守护线程也立刻结束运行。一般地,用守护线程做一些不是很严格的工作,线程的随时结束不会产生什么不良的后果。一个线程必须在运行之前设置自己是否是守护线程。
class Daemon implements Runnable{
Thread A,B;
Daemon(){
A = new Thread(this);
B = new Thread(this);
}
public void run(){
if(Thread.currentThread()==A){
for(int i=0;i<3;i++){
System.out.println("i="+i) ;
try { Thread.sleep(1000); }
catch(InterruptedException e){}
}
}
else if(Thread.currentThread()==B){
while(true){
System.out.println("线程B是守护线程 ");
try{ Thread.sleep(1000); }
catch(InterruptedException e){}
}
}
}
}
public class Example8_14
{
public static void main(String args[])
{
Daemon a=new Daemon ();
a.A.start();
a.B.setDaemon(true); // 设置为守护线程
a.B.start();
}
}
补充
线程池
对于每个任务都创建线程这种方法对于单个任务的执行是很方便的,但对于大量的任务来说,它并不高效,因为你必须为每个任务创建一个线程。为每个任务启动一个新的线程可能会限制吞吐量,导致性能不佳。
使用线程池是管理并发执行的任务数量的一种理想方式。
Java提供了用于在线程池中执行任务的Executor接口和用于管理和控制任务的ExecutorService接口。ExecutorService是Executor的一个子接口,可以使用execute()
执行单个线程,方法如果使用Executors.newCachedThreadPool();
则能实现多个task并行执行
提示。如果你只需要为一个任务创建一个线程,请使用Thread类。如果你需要为多个任务创建线程,最好使用一个线程池。
使用Locks进行同步
静态内部类使用场景:一般是当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建对象。
一个同步的实例方法在执行该方法之前隐含地获得了一个实例上的锁。
Java使你能够显式地获取锁,这使你在协调线程时有更多的控制权。锁是锁接口的一个实例,它定义了获取和释放锁的方法。 ReentrantLock是Lock的一个具体实现,用于创建互斥锁。
一般来说,使用同步方法或语句比使用显式锁进行互斥更简单。然而,使用显式锁来同步有条件的线程更加直观和灵活。
线程间协作
线程同步通过确保关键区域内多个线程的相互排斥,足以避免冲突条件,但有时你也需要一种线程合作的方式。
Conditions可以用来促进线程之间的通信。一个线程可以指定在某个条件下要做什么。 Conditions是通过调用Lock对象上的newCondition()方法来创建的对象(对象)。 一旦一个条件被创建,你可以使用它的await()、signal()和signalAll()方法进行线程通信。
Ch-09
读写文件时可以使用输入/输出流,简称I/O流
输入流(input stream or input object)的指向称作“源” 程序从输入流中读取“源”中的数据
输出流(output stream or output object)的指向称作“目的地” 程序通过向输出流中写入数据,把信息传递到“目的地”
程序的“源”和“目的地”可以是文件、键盘、鼠标、内存或显示器窗口
显式地关闭任何打开的流是一个好的编程习惯
java.io中有4个重要的abstract class:
- InputStream(字节输入流)
- OutputStream(字节输出流)
- Reader(字符输入流)
- Writer(字符输出流)
9.1 文件
File类
Java使用File类创建的对象来获取文件本身的一些信息,如文件所在的目录、文件的长度、文件读写权限等,文件对象并不涉及对文件的读写操作。
构造方法
File(String filename);
File(String directoryPath, String filename);
……
文件属性
public String getName()
:获取文件的名字
public boolean canRead()
:判断文件是否可读
public boolean canWrite()
:判断文件是否可被写入
public boolean exits()
:判断文件是否存在
public long length()
:获取文件的长度(单位是字节)
public String getAbsolutePath()
:获取文件的绝对路径
public String getParent()
:获取文件的父目录
public boolean isFile()
:判断文件是否是一个正常的文件
public boolean isDirectory()
:判断文件是否是一个目录
public boolean isHidden()
:判断文件是否是隐藏文件
public long lastModified()
:获取文件最后修改的时间
目录
创建目录
File类的对象可以调用public boolean mkdir()
:创建一个目录
列出目录中的文件(如果File对象是一个目录)
public String[] list()
:用字符串数组形式返回目录下的全部文件
public String[] list(FilenameFilter obj)
:用字符串数组形式返回目录下指定类型的全部文件
public File[] listFiles()
:用File对象数组形式返回目录下的全部文件
public File[] listFiles(FilenameFilter obj)
:用File对象数组形式返回目录下指定类型的全部文件
文件的创建与删除
当使用File类创建一个文件对象后,例如:
File file = new File("c:\\myletter","letter.txt");
如果c:\myletter目录中没有名字为letter.txt的文件,文件对象file需要调用public boolean createNewFile(),即file.createNewFile();
,从而在c:\myletter目录中建立一个名字为letter.txt的文件。
如果c:\myletter目录中已有名字为letter.txt的文件,则打开这个文件。
文件对象调用方法public boolean delete()可以删除当前文件,例如:
file.delete();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
class FileAccept implements FilenameFilter
{
String str = null;
FileAccept(String s)
{
str = "." + s;
}
public boolean accept(File dir, String name)
{
return name.endsWith(str);
}
}
运行可执行文件
使用Runtime类声明一个对象
使用静态方法getRuntime()创建这个对象:Runtime ec = Runtime.getRuntime();
ec可以调用exec(String command)方法打开本地机器的可执行文件或执行一个操作:
import java.io.*;
public class Example9_2
{
public static void main(String args[])
{
try
{
Runtime ec = Runtime.getRuntime();
File file = new File("C:\\windows", "Notepad.exe");
ec.exec(file.getAbsolutePath());
}
catch(Exception e){}
}
}
9.12 使用Scanner解析文件
应用程序可能需要解析文件中的特殊数据,此时,应用程序可以把文件的内容全部读入内存后,再使用第6章的有关知识解析所需要的内容,其优点是处理速度快,但如果读入的内容较大,将消耗较多的内存,这就是所谓的“以空间换时间”。
本节介绍怎样借助Scanner类和正则表达式来解析文件,比如,要解析出文件中的特殊单词、数字等信息。使用Scanner类和正则表达式来解析文件的特点是“以时间换空间”,解析的速度相对较慢,但节省内存。
使用默认分隔符标记解析文件
创建Scanner
对象,并指向要解析的文件,例如:
那么scanner将空格作为分隔标记、调用next()
方法依次返回file中的单词,如果file最后一个单词已被next()方法返回,scanner调用hasNext()将返回false,否则返回true。
对于数字型的单词,比如108,167.92等可以用nextInt()或nextDouble()方法来代替next()方法。但需要特别注意的是,如果单词不是数字型单词,调用nextInt()或nextDouble()方法将发生InputMismatchException异常。在处理异常时可以调用next()方法返回该非数字化单词。
import java.io.*;
import java.util.*;
public class Demo
{
public static void main(String args[])
{
File file = new File("D:\\chp09\\cost.txt");
Scanner scanner = null;
int sum=0;
try{
scanner = new Scanner(file);
while(scanner.hasNext()){
try{
int price = scanner.nextInt();
sum = sum + price;
System.out.println(price);
}
catch(InputMismatchException exp){
String t = scanner.next();
}
}
System.out.println("Total Cost:"+sum+" dollar");
}
catch(Exception exp){ System.out.println(exp); }
}
}
使用正则表达式作为分隔标记解析文件
创建Scanner对象,指向要解析的文件,并使用useDelimiter()方法指定正则表达式作为分隔标记,例如:
File file = new File("hello.java");
Scanner scanner = new Scanner(file);
scanner.useDelimiter(正则表达式);
那么,scanner将正则表达式作为分隔标记。
使用正则表达式(匹配所有非数字字符串): String regex=”[^0123456789.]+” 作为分隔标记解析communicate.txt文件中的通信费用。
communicate.txt的内容如下:市话费:176.89元,长途费:187.98元,网络费:928.66元
import java.io.*;
import java.util.*;
public class Demo{
public static void main(String args[]){
File file = new File("D:\\chp09\\communicate.txt");
Scanner scanner = null;
double sum = 0;
try {
double fare=0;
scanner = new Scanner(file);
scanner.useDelimiter("[^0123456789.]+");
while(scanner.hasNextDouble()){
fare = scanner.nextDouble();
sum = sum+fare;
System.out.println(fare);
}
System.out.println("Total: " + sum);
}
catch(Exception exp){
System.out.println(exp);
}
}
}
单词记忆训练
基于文本文件的英文单词训练程序,具体内容如下:
- 文本文件D:/chp09/word.txt中的内容由英文单词所构成,单词之间用空格分隔,例如:first boy girl hello well。
- 使用Scanner解析word.txt中的单词,并显示在屏幕上,然后要求用户输入该单词。
- 当用户输入单词时,程序将从屏幕上隐藏掉刚刚显示的单词,以便考核用户是否清晰地记住了这个单词。
- 程序读取了word.txt的全部内容后,将统计出用户背单词的正确率。
9.3 文件字符流
FileReader类
构造方法
FileReader(String name)
FileReader(File file)
常用方法
int read()
:读取一个字符(即2个字节),返回0~65535之间的一个整数(Unicode字符值),如果未读出字符就返回-1。
int read(char b[])
:读取b.length个字符到字符数组b中,返回实际读取的字符数目;如果到达文件的末尾,则返回-1。
int read(char b[ ], int off, int len)
:读取len个字符并存放到字符数组b中,返回实际读取的字符数目;如果到达文件的末尾,则返回-1。其中,off参数指定read方法从字符数组b中的什么地方存放数据。
FileWriter类
构造方法
FileWriter(String name)
FileWriter(File file)
常用方法
void write(char b[])
:写b.length个字符到输出流
void write(char b[], int off, int len)
:从给定字符数组中起始于偏移量off处写len个字符到输出流,参数b是存放了数据的字符数组
void write(String str)
:把字符串中的全部字符写入到输出流
void write(String str, int off, int len)
:从字符串str中起始于偏移量off处写len个字符到输出流
Example:
import java.io.*;
public class Example9_4
{
public static void main(String args[])
{
File file = new File("hello.txt");
char b[] = "深圳大学".toCharArray();
try{
FileWriter output = new FileWriter(file);
output.write(b); // 字符数组
output.write("脚踏实地!"); // 字符串
output.close();
FileReader input = new FileReader(file);
int n=0;
while((n=input.read(b,0,2))!=-1){ // 最多读2个字符
String str = new String(b,0,n); // 转换为字符串
System.out.println(str);
}
input.close();
}
catch(IOException e){
System.out.println(e);
}
}
}
9.5 缓冲流
BufferedReader类
BufferedReader(Reader in)
BufferedReader流能够读取文本行,方法是readLine()
通过向BufferedReader传递一个Reader对象(如FileReader对象),来创建一个BufferedReader对象,如:
FileReader fr = new FileReader("Student.txt");
BufferedReader input = new BufferedReader(fr);
然后,input调用readLine()
顺序读取文件Student.txt的一行。
BufferedWriter类
类似地,可以将BufferedWriter流和FileWriter流连接在一起,然后使用BufferedWriter流将数据写到目的地,例如:
FileWriter fw = new FileWriter("hello.txt");
BufferedWriter output = new BufferedWriter(fw);
BufferedWritter流调用如下方法,把字符串s或s的一部分写入到目的地
write(String s)
write(String s, int off, int len)
Example:
import java.io.*;
public class Example9_5{
public static void main(String args[]){
try{
FileReader fr = new FileReader("input.txt");
BufferedReader input = new BufferedReader(fr);
FileWriter fw = new FileWriter("output.txt");
BufferedWriter output = new BufferedWriter(fw);
String s=null;
int i=0;
while((s = input.readLine())!=null){
i++;
output.write(i + ": " + s);
output.newLine();
}
output.flush(); output.close(); fw.close();
input.close(); fr.close();
}
catch(IOException e){
System.out.println(e);
}
}
}
9.2 文件字节流
FileInputStream类
为了创建FileInputStream类的对象,可以使用下列构造方法:
FileInputStream(String name)
FileInputStream(File file)
输入流的唯一目的是提供通往数据的通道,程序可以通过这个通道读取数据,read()方法给程序提供一个从输入流中读取数据的基本方法。
read()
方法从输入流中顺序读取单个字节的数据。该方法返回字节值(0~255之间的一个整数),读取位置到达文件末尾,则返回-1。
read()
方法还有其它一些形式。这些形式能使程序把多个字节读到一个字节数组中:
int read(byte b[]);
int read(byte b[], int off, int len);
其中,off参数指定read()
方法把数据存放在字节数组b中的什么地方,len参数指定该方法将要读取的最大字节数。上面所示的这两个read()方法都返回实际读取的字节数,如果它们到达输入流的末尾,则返回-1。
FileOutputStream类
构造方法
FileOutputStream(String name)
FileOutputStream(File file)
输出流通过使用write()方法把数据写入输出流到达目的地
public void write(byte b[])
:写b.length个字节到输出流
public void write(byte b[], int off, int len)
:从给定字节数组中起始于偏移量off处写len个字节到输出流,参数b是存放了数据的字节数组
Example:
import java.io.*;
public class Example9_3
{
public static void main(String args[])
{
File file = new File("hello.txt");
byte b[] = "深圳大学".getBytes();
try{
FileOutputStream output = new FileOutputStream(file);
output.write(b); // 字节数组
output.close();
FileInputStream input = new FileInputStream(file);
int n=0;
while( (n=input.read(b,0,2))!=-1 ) // 最多读2个字节
{
String str = new String(b,0,n); // 转换为字符串
System.out.println(str);
}
}
catch(IOException e){
System.out.println(e);
}
}
}
9.8 数据流
DataInputStream类和DataOutputStream类
DataInputStream类创建的对象称为数据输入流 DataOutputStream类创建的对象称为数据输出流
DataInputStream类和DataOutputStream类的构造方法
DataInputStream(InputStream is)
DataOutputStream(OutputStream os)
Example:
import java.io.*;
public class Example9_8
{
public static void main(String args[])
{
try{
FileOutputStream fos = new FileOutputStream("jerry.dat");
DataOutputStream output = new DataOutputStream(fos);
output.writeInt(100);
output.writeChars("I am ok");
}
catch(IOException e){}
try{
FileInputStream fis = new FileInputStream("jerry.dat");
DataInputStream input = new DataInputStream(fis);
System.out.println(input.readInt());
char c;
while((c=input.readChar())!='\0') //'\0'表示空字符
System.out.print(c);
}
catch(IOException e){}
}
}
9.9 对象流
ObjectInputStream类和ObjectOutputStream类
ObjectInputStream类创建的对象被称为对象输入流 ObjectOutputStream类创建的对象被称为对象输出流
对象输出流使用writeObject(Object obj)方法将一个对象obj写入输出流 对象输入流使用readObject()从源中读取一个对象到程序中
构造方法 ObjectInputStream(InputStream in) ObjectOutputStream(OutputStream out)
Java提供给我们的绝大多数对象都是序列化的,比如组件等。
一个类如果实现了Serializable接口,那么这个类创建的对象就是所谓的序列化的对象(a serializable object)。
Serializable接口中的方法对程序是不可见的,因此实现该接口的类不需要实现额外的方法,当把一个序列化的对象写入到对象输出流时,JVM就会实现Serializable接口中的方法,进而按一定格式的文本将对象写入到目的地。
Example:
import java.io.*;
class Goods implements Serializable
{
String name = null;
double unitPrice;
Goods(String name, double unitPrice){
this.name=name;
this.unitPrice=unitPrice;
}
public void setUnitPrice(double unitPrice){
this.unitPrice=unitPrice;
}
public double getUnitPrice(){
return unitPrice;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
}
public class Example9_9
{
public static void main(String args[])
{
Goods TV1 = new Goods("HaierTV",3468);
try{
FileOutputStream fileOut = new FileOutputStream("a.txt");
ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
objectOut.writeObject(TV1);
FileInputStream fileIn = new FileInputStream("a.txt");
ObjectInputStream objectIn = new ObjectInputStream(fileIn);
Goods TV2 = (Goods)objectIn.readObject();
TV2.setUnitPrice(8888);
TV2.setName("GreatWall");
System.out.printf("\nTv1:%s,%f",TV1.getName(),TV1.getUnitPrice());
System.out.printf("\nTv2:%s,%f",TV2.getName(),TV2.getUnitPrice());
}
catch(Exception event){
System.out.println(event);
}
}
}
9.10 序列化和对象克隆
使用对象流很容易获取一个序列化对象的深度克隆(原对象有引用型变量的时候)。
我们只需将该对象写入到对象输出流,然后用对象输入流读回的对象就是原对象的一个深度克隆。
Exmaple:
import java.io.*;
class Goods implements Serializable{
String name=null;
Goods(String name){
this.name=name;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
}
class Shop implements Serializable{
Goods goods[];
public void setGoods(Goods[] s){
goods=s;
}
public Goods[] getGoods(){
return goods;
}
}
public class Example9_10{
public static void main(String args[]){
Shop shop1 = new Shop();
Goods s1[] = {new Goods("TV"), new Goods("PC")};
shop1.setGoods(s1);
try{
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objectOut = new ObjectOutputStream(out);
objectOut.writeObject(shop1);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream objectIn = new ObjectInputStream(in);
Shop shop2 = (Shop)objectIn.readObject();
Goods goods2[] = shop2.getGoods();
System.out.println("shop2:");
for(int i=0;i<goods2.length;i++)
System.out.println(goods2[i].getName());
}
catch(Exception event){
System.out.println(event);
}
}
}
9.11 随机读写流
RandomAccessFile类的构造方法
RandomAccessFile(String name, String mode)
:参数name用来确定一个文件名,给出创建的流的源,也是流的目的地。参数mode取r(只读)或rw(可读写),决定创建的流对文件的访问权限。
RandomAccessFile(File file, String mode)
:参数file是一个File对象,给出创建的流的源,也是流的目的地。参数mode取r(只读)或rw(可读写),决定创建的流对文件的访问权利。
一个随机存取的文件由一连串的字节组成。
一个叫做文件指针的特殊标记被定位在这些字节中的一个。 读或写的操作发生在文件指针的位置。 当一个文件被打开时,文件指针被设置在文件的开头。 当你向文件读或写数据时,文件指针向前移动到下一个数据项。
Example:
import java.io.*;
public class Example9_11
{
public static void main(String args[])
{
RandomAccessFile inAndOut=null;
int [] data = {20,30,40,50,60};
try{
inAndOut = new RandomAccessFile("a.dat","rw");
}
catch(Exception e){}
try{
for(int i=0;i<data.length;i++)
inAndOut.writeInt(data[i]);
for(long i=data.length-1;i>=0;i--){
inAndOut.seek(i*4);
System.out.println(inAndOut.readInt());
}
inAndOut.close();
}
catch(IOException e){}
}
}
import java.io.*;
public class Example9_12
{
public static void main(String args[])
{
RandomAccessFile input = null;
try{
input = new RandomAccessFile("test.txt","rw");
long length = input.length();
long position = 0;
input.seek(position);
while(position<length)
{
String str = input.readLine();
position = input.getFilePointer();
System.out.println(str);
}
}
catch(IOException e){}
}
}
9.13 文件锁
JDK1.4增加了一个FileLock类,该类的对象称做文件锁。
RondomAccessFile创建的流在读写文件时可以使用文件锁,那么只要不解除该锁,其它线程无法操作被锁定的文件。
使用文件锁的步骤如下: Step 1:使用RondomAccessFile流建立指向文件的流对象,该对象的读写属性必须是”rw”
RandomAccessFile input = new RandomAccessFile("Example.java","rw");
Step 2:流对象input调用getChannel()方法获得一个连接到底层文件的FileChannel 对象(信道)
FileChannel channel = input.getChannel();
Step 3:信道调用tryLock()或lock()方法获得一个FileLock(文件锁)对象,这一过程也称作对文件加锁
FileLock lock = channel.tryLock();
另外,FileInputStream以及FileOutputStream在读/写文件时都可以获得文件锁。
在下面的例子13中Java程序在读取文件test.txt时,使用了文件锁,这时你无法用其它程序来操作文件test.txt,比如在Java程序结束前,你用Windows下的”记事本”(Notepad.exe)也无法修改、保存test.txt。
Example:
import java.io.*;
import java.nio.channels.*;
public class Example9_13
{
public static void main(String args[]){
int b;
byte tom[] = new byte[12];
try{
RandomAccessFile input = new RandomAccessFile("test.txt","rw");
FileChannel channel = input.getChannel();
while((b=input.read(tom,0,10))!=-1){
FileLock lock = channel.tryLock();
String s = new String (tom, 0, b);
System.out.print(s);
try {
Thread.sleep(1000);
lock.release();
}
catch(Exception eee){
System.out.println(eee);
}
}
input.close();
}
catch(Exception ee){
System.out.println(ee);
}
}
}
9.6 数组流
字节输入流:ByteArrayInputStream 字节输出流:ByteArrayOutputStream 分别使用字节数组作为流的源和目的地
构造方法
-
ByteArrayInputStream(byte[] buf)
-
ByteArrayInputStream(byte[] buf, int offset, int length)
第一个构造方法构造的数组字节流的源是参数buf指定的数组的全部字节单元。 第二个构造方法构造的数组字节流的源是参数buf指定的数组从offset处取的length个字节单元。
public byte[] toByteArray()
:
在程序Example9_10中有用到(返回输出流写入到缓冲区的全部字节)
数组字节流读写操作不会发生IOException异常。
在下面的例子6中,我们向内存(输出流的缓冲区)写入ASCII表,然后再读出这些字节和字节对应的字符。
import java.io.*;
public class Example9_6
{
public static void main(String args[])
{
int n=-1;
ByteArrayOutputStream output = new ByteArrayOutputStream();
for(int i=0;i<5;i++)
{
output.write('A'+i);
}
ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
while((n=input.read())!=-1)
{
System.out.println(n + ":" + (char)n);
}
}
}
与数组字节流对应的是数组字符流
- CharArrayReader
- CharArrayWriter
与数组字节流不同的是,数组字符流的读操作可能发生IOException异常。
在下面的例子7中,我们将Unicode表中的一些字符写入内存,然后再读出。
import java.io.*;
public class Example9_7
{
public static void main(String args[])
{
int n=-1;
CharArrayWriter output = new CharArrayWriter();
for(int i=65;i<=69;i++)
{
output.write(i);
}
CharArrayReader input = new CharArrayReader(output.toCharArray());
try
{
while((n=input.read())!=-1)
{
System.out.println(n + ":" + (char)n);
}
}
catch(IOException e){}
}
}
9.7 字符串流
StringReader使用字符串作为流的源。
构造方法:public StringReader(String s) 该构造方法构造的输入流指向参数s指定的字符串
public int read():顺序读出源中的一个字符,并返回字符在Unicode表中的位置。 public int read(char[] buf, int off, int len):顺序地从源中读出参数len指定的字符个数,并将读出的字符存放到参数buf指定的数组中,参数off指定数组buf存放读出字符的起始位置,该方法返回实际读出的字符个数。
StringWritter将内存作为流的目的地。 构造方法:StringWritter(); StringWritter(int size);
字符串输出流调用下列方法可以向缓冲区写入字符 public void write(int b) public void write(char[] buf, int off, int len) public void write(String str) public void write(String str, int off, int len)
字符串输出流调用public String toString()方法,可以返回输出流写入到缓冲区的全部字符;调用public void flush()方法可以刷新缓冲区。
Ch-10
10.1 AWT组件与SWING组件概述
awt包
Java早期进行用户界面设计时,使用java.awt包(package)中提供的类。
AWT是Abstract Window Toolkit(抽象窗口工具包)的缩写。
java.awt包中的类创建的组件习惯上称为重组件(heavyweight components)。例如,当用java.awt包中的Button类创建一个按钮组件时,都有一个相应的本地组件在为它工作,即显示组件和处理组件事件,该本地组件称为它的同位体。Java 2(JDK 1.2)推出之后,增加了一个新的javax.swing包(package),该包提供了功能更为强大的用来设计GUI界面的类。
swing包
javax.swing包为我们提供了更加丰富的、功能强大的组件,称为Swing组件,其中大部分组件是轻组件(lightweight components),没有同位体,而是把与显示组件有关的许多工作和处理组件事件的工作交给相应的UI代表来完成。
这些UI代表是用Java语言编写的类,这些类被增加到Java的运行环境中,因此组件的外观不依赖平台,不仅在不同平台上的外观是相同的,而且与重组件相比有更高的性能。
如果Java运行环境低于1.2版本,就不能运行含有Swing组件的程序。
轻量级组件是用JAVA代码画出来的,这样具有平台移植性
轻组件,以下为JComponent子类 JComponent JButton JTextField JTextArea JTree JTable JPanel 重组件,以下为Window子类 Frame - JFrame Dialog - JDialog
Java把由Component类的子类或间接子类创建的对象称为组件;把由Container类的子类或间接子类创建的对象称为容器。
可以向容器添加组件。Container类提供了一个public方法add(),一个容器可以调用这个方法将组件添加到该容器中。
调用removeAll()
方法可以移掉容器中的全部组件,调用remove(Component com)
方法可以移掉容器中参数指定的组件。
每当容器添加新的组件或移掉组件时,应该让容器调用validate()
方法,以保证容器中的组件能正确显示出来。
容器本身也是一个组件,因此可以把一个容器添加到另一个容器中实现容器的嵌套。
javax.swing包中有4个最重要的类JComponent, JFrame, JApplet和JDialog。 JComponent类的子类都是轻组件,JComponent类是java.awt包中Container类的子类,因此所有的轻组件也都是容器。
JFrame, JApplet, JDialog都是重组件,即有同位体的组件。这样,窗口(JFrame)、小应用程序(Java Applet)、对话框(JDialog)可以和操作系统交互信息。轻组件必须在这些容器中绘制自己,习惯上称这些容器为Swing的顶层/底层容器。
10.2 JFrame窗体
javax.swing包中的JFrame类是java.awt包中Frame类的子类。
JFrame类的常用方法
JFrame()
:创建一个无标题的窗口。
JFrame(String s)
:创建一个标题为s的窗口。
public void setBounds(int a, int b, int width, int height)
:设置出现在屏幕上时的初始位置为(a, b),即距屏幕左面a个像素、距屏幕上方b个像素;窗口的宽是width,高是height。
public void setSize(int width, int height)
:设置窗口的大小,窗口在屏幕出现时默认位置是(0, 0)。
public void setVisible(boolean b)
:设置窗口是可见还是不可见,窗口默认是不可见的。
JDK 1.4或之前的版本要求如下:
- 不可以把组件直接添加到JFrame窗体中。
- JFrame窗体含有一个称为内容面板(内容窗格,content pane)的容器,应当把组件添加到内容面板中(内容面板也是重容器)。
- 不能为JFrame窗体设置布局,而应当为JFrame窗体的内容面板(内容窗格)设置布局。内容面板(内容窗格)的默认布局是BorderLayout布局。
- JFrame窗体通过调用方法getContentPane()方法得到它的内容面板(内容窗格)。
10.5 中间容器
JPanel面板
我们会经常使用JPanel创建一个面板,再向这个面板添加组件,然后把这个面板添加到底层容器或其他中间容器中。
JPanel面板的默认布局是FlowLayout布局。
可以使用JPanel类构造方法JPanel()
构造一个面板容器对象。
JScrollPane滚动窗格
我们可以把一个组件放到一个滚动窗格中,然后通过滚动条来观察这个组件。
例如,JTextArea不自带滚动条,因此我们就需要把文本区放到一个滚动窗格中。可以使用JScrollPane的构造方法JScrollPane(component com)构造一个滚动窗格。
JSplitPane拆分窗格
拆分窗格是被分成两部分的容器。拆分窗格有两种类型:水平拆分和垂直拆分。
水平拆分窗格用一条拆分线把容器分成左右两部分,左面放一个组件,右面放一个组件,拆分线可以水平移动。
垂直拆分窗格由一条拆分线分成上下两部分,上面放一个组件,下面放一个组件,拆分线可以垂直移动。
可以使用JSplitPane的构造方法JSplitPane(int a, Component b, Component c)
构造一个拆分窗格,参数a取JSplitPane的静态常量 HORIZONTAL_SPLIT或VERTICAL_SPLIT,以决定是水平拆分还是垂直拆分。后两个参数决定要放置的组件。
拆分窗格调用setDividerLocation(double position)
设置拆分线的位置。
JLayeredPane分层窗格
如果添加到容器中的组件经常需要处理重叠问题,就可以考虑将组件添加到JLayeredPane容器。
JLayeredPane容器将容器分为5层,容器使用add(JComponent com, int layer)
添加组件com,并指定com所在的层,其中参数layer取值JLayeredPane类中的类常量:DEFAULT_LAYER 、PALETTE_LAYER、 MODAL_LAYER、POPUP_LAYER、DRAG_LAYER。
DEFAULT_LAYER是最底层,添加到DEFAULT_LAYER层的组件如果和其它层的组件发生重叠,将被其它组件遮挡。
DRAG_LAYER层是最上面的层,如果JLayeredPane中添加了许多组件,当你用鼠标移动一组件时,可以把移动的组件放到DRAG_LAYER层,这样,组件在移动过程中,就不会被其它组件遮挡。
添加到同一层上的组件,如果发生重叠,后添加的会遮挡先前添加的组件。
JLayeredPane对象调用public void setLayer(Component com, int layer)可以重新设置组件com所在的层,调用public int getLayer(Component com)可以获取组件com所在的层数。
Example:我们在JLayeredPane容器中添加5个组件,分别位于不同的层上。
import javax.swing.*;
import java.awt.*;
public class Example10_6
{
public static void main(String args[])
{
new WindowLayered();
}
}
class WindowLayered extends JFrame
{
WindowLayered()
{
// ---
setBounds(100,100,300,300);
setVisible(true);
// ---
JButton b1 = new JButton("DEFAULT_LAYER");
JButton b2 = new JButton("PALETTE_LAYER");
JButton b3 = new JButton("MODAL_LAYER");
JButton b4 = new JButton("POPUP_LAYER");
JButton b5 = new JButton("DRAG_LAYER");
// ---
b5.setBounds(50,50,200,100);
b4.setBounds(40,40,200,100);
b3.setBounds(30,30,200,100);
b2.setBounds(20,20,200,100);
b1.setBounds(10,10,200,100);
// ---
JLayeredPane pane = new JLayeredPane();
pane.setLayout(null);
// ---
pane.add(b5,JLayeredPane.DRAG_LAYER);
pane.add(b4,JLayeredPane.POPUP_LAYER);
pane.add(b3,JLayeredPane.MODAL_LAYER);
pane.add(b2,JLayeredPane.PALETTE_LAYER);
pane.add(b1,JLayeredPane.DEFAULT_LAYER);
// ---
add(pane, BorderLayout.CENTER);
// ---
validate();
// ---
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
}
}
10.23 对话框
JDialog类
JDialog类和JFrame类都是Window类的子类,二者有相似之处也有不同的地方,比如对话框必须要依赖于某个窗口或组件,当它所依赖的窗口或组件消失时,对话框也将消失;而当它所依赖的窗口或组件可见时,对话框又会自动恢复。
对话框的模式
对话框分为无模式(modaless)和有模式(modal)两种,我个人理解就是有模式需要强制执行对话框操作。
-
无模式对话框处于激活状态时,程序仍能激活它所依赖的窗口或组件,它也不堵塞线程的执行。
-
有模式对话框处于激活状态时,只让程序响应对话框内部的事件,程序不能再激活它所依赖的窗口或组件,而且它将堵塞当前线程的执行,直到该对话框消失不可见。
Example:当对话框处于激活状态时,命令行无法输出信息,当对话框消失时,再根据对话框消失的原因,命令行输出信息:“Button: Yes”或“Button: No”。
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class Example10_30
{
public static void main(String args[])
{
MyDialog dialog = new MyDialog(null, "我有模式", true);
dialog.setVisible(true);
if(dialog.getMessage() == MyDialog.YES)
{
System.out.println("你单击了对话框的yes按钮");
}
else
{
if(dialog.getMessage() == MyDialog.NO)
{
System.out.println("你单击了对话框的No按钮");
}
else
{
if(dialog.getMessage() == MyDialog.CLOSE)
{
System.out.println("你单击了对话框的关闭图标");
}
}
}
System.exit(0);
}
}
class MyDialog extends JDialog implements ActionListener
{
static final int YES=1, NO=0, CLOSE=-1;
int message=10;
Button yes,no;
MyDialog(JFrame f, String s, boolean b)
{
super(f,s,b);
// ---
setLayout(new FlowLayout());
setBounds(60,60,100,100);
// ---
yes = new Button("Yes");
yes.addActionListener(this);
// ---
no = new Button("No");
no.addActionListener(this);
// ---
add(yes);
// ---
add(no);
// --- 匿名类
addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
message = CLOSE;
setVisible(false);
}
});
}
// ---
public void actionPerformed(ActionEvent e)
{
if(e.getSource()==yes)
{
message = YES;
setVisible(false);
}
else if(e.getSource()==no)
{
message = NO;
setVisible(false);
}
}
// ---
public int getMessage()
{
return message;
}
}
输入对话框
javax.swing包中的JOptionPane类的静态方法public static String showInputDialog(Component parentComponent, Object message, String title, int messageType)
可以创建一个输入对话框。
消息对话框
javax.swing包中的JOptionPane类的静态方法public static void showMessageDialog(Component parentComponent, String message, String title, int messageType)
可以创建一个消息对话框。
确认对话框
确认对话框是有模式对话框,可以用javax.swing包中的JOptionPane类的静态方法public static int showConfirmDialog(Component parentComponent, Object message, String title, int optionType)
创建一个确认对话框。参数分别是对话框所依赖的组件、对话框上显示的消息、对话框的标题和对话框的外观。
Example:用户在输入对话框中输入数字字符,如果输入的字符中有非数字字符,将弹出一个消息对话框,提示用户输入了非法字符,该消息对话框消失后,将清除用户输入的非法字符;如果用户的输入没有非法字符,将弹出一个确认对话框,让用户确认,如果单击确认对话框上的“是(Y)”按钮,就把数字放入文本区。
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
import java.util.regex.*;
public class Example10_31
{
public static void main(String args[])
{
new Dwindow();
}
}
class Dwindow extends JFrame implements ActionListener
{
JButton inputNumber;
JTextArea save;
Pattern p; //模式对象
Matcher m; //匹配对象
Dwindow()
{
// ---
inputNumber = new JButton("单击按钮打开输入对话框");
inputNumber.addActionListener(this);
add(inputNumber, BorderLayout.NORTH);
// ---
save = new JTextArea(12,16);
add(new JScrollPane(save), BorderLayout.CENTER);
// ---
setBounds(60,60,300,300);
setVisible(true);
// ---
p = Pattern.compile("\\D+"); //创建模式对象(含有非数字字符的模式)
// ---
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void actionPerformed(ActionEvent e)
{
String str = JOptionPane.showInputDialog(null, "请输入数字字符序列", "输入对话框", JOptionPane.INFORMATION_MESSAGE);
if(str!=null)
{
m = p.matcher(str);
while(m.find())
{
JOptionPane.showMessageDialog(this, "您输入了非法字符", "消息对话框", JOptionPane.WARNING_MESSAGE);
str = JOptionPane.showInputDialog(null, "请输入数字字符序列");
m = p.matcher(str);
}
// ---
int n = JOptionPane.showConfirmDialog(this, "确认正确吗?", "确认对话框", JOptionPane.YES_NO_OPTION);
if(n == JOptionPane.YES_OPTION)
{
save.append("\n"+str);
}
}
}
}
颜色对话框
可以用javax.swing包中的JColorChooser类的静态方法public static Color showDialog(Component com, String title, Color initialColor)
创建一个颜色对话框,其中参数com指定对话框所依赖的组件,title指定对话框的标题,initialColor 指定对话框返回的初始颜色,即对话框消失后返回的默认值。
颜色对话框可根据用户在颜色对话框中选择的颜色返回一个颜色对象。
Example:当用户单击buttonOpen按钮时,弹出一个颜色对话框,然后根据用户选择的颜色来改变按钮showColor的颜色 。
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class Example10_32
{
public static void main(String args[])
{
new ColorWin("带颜色对话框的窗口");
}
}
class ColorWin extends JFrame implements ActionListener
{
JButton buttonOpen,showColor;
ColorWin(String s)
{
// ---
setTitle(s);
// ---
buttonOpen = new JButton("打开颜色对话框");
buttonOpen.addActionListener(this);
add(buttonOpen,BorderLayout.NORTH);
// ---
showColor = new JButton();
add(showColor,BorderLayout.CENTER);
// ---
setBounds(60,60,300,300);
setVisible(true);
// ---
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void actionPerformed(ActionEvent e)
{
Color newColor = JColorChooser.showDialog(this, "调色板", showColor.getBackground());
if(newColor!=null)
{
showColor.setBackground(newColor);
}
}
}
文件对话框
文件对话框提供从文件系统中进行文件选择的界面。 JFileChooser对象调用下列方法可以使得一个有模式对话框显示在桌面上,该对话框称作文件对话框,文件对话框将在参数指定的组件parentComponent的正前方显示,如果parentComponent为null,则在系统桌面的正前方显示。
- showDialog(Component parentComponent, String s)
- showOpenDialog(Component parentComponent)
- showSaveDialog(Component parentComponent)
当文件对话框消失后,上述方法返回下面的整型常量之一,返回的值依赖于单击了对话框上的“确认”按钮还是“取消”按钮。
- JFileChooser.APPROVE_OPTION
- JFileChooser.CANCEL_OPTION
Example:当用户单击“打开文件”按钮,将弹出一个文件对话框,用户可以把选择的文件的内容显示在一个文本区中。
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
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
import java.io.*;
public class Example10_33
{
public static void main(String args[])
{
new FileWindow();
}
}
class FileWindow extends JFrame implements ActionListener
{
JButton buttonFile;
JTextArea text;
JFileChooser fileChooser;
FileWindow()
{
fileChooser = new JFileChooser("c:/");
// ---
buttonFile = new JButton("打开文件");
buttonFile.addActionListener(this);
add(buttonFile, BorderLayout.NORTH);
// ---
text = new JTextArea("显示文件内容");
add(new JScrollPane(text), BorderLayout.CENTER);
// ---
setBounds(60,60,300,300);
setVisible(true);
// ---
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public void actionPerformed(ActionEvent e)
{
text.setText(null);
int n = fileChooser.showOpenDialog(null);
if(n == JFileChooser.APPROVE_OPTION)
{
File file = fileChooser.getSelectedFile();
// ---
try
{
FileReader readfile = new FileReader(file);
BufferedReader in = new BufferedReader(readfile);
String s = null;
while( (s=in.readLine()) != null )
{
text.append(s+"\n");
}
}
catch(IOException ee){}
}
}
}
10.24 多文档界面
Java实现多文档界面(MDI)常用的方式是在一个JFrame窗口中添加若干个内部窗体,内部窗体由JInternalFrame类负责创建。这些内部窗体被限制在JFrame窗口中。
在使用内部窗体时,需要将内部窗体事先添加到JDesktopPane桌面容器中,一个桌面容器可以添加若干个内部窗体,这些内部窗体将被限制在该桌面容器中,然后把桌面容器添加到JFrame窗口即可。
桌面容器使用方法add(JInternalFrame e, int layer)
添加内部窗体,并指定内部窗体所在的层次。
10.4 布局设计
希望控制组件在容器中的位置,这就需要学习布局设计的知识。我们将分别介绍java.awt包中的FlowLayout、BorderLayout、CardLayout、GridLayout布局类和java.swing.border包中的BoxLayout布局类。
对于JFrame窗口,默认布局是BorderLayout布局。
容器可以使用方法setLayout(布局对象);
来设置自己的布局。
FlowLayout布局
FlowLayout类创建的对象称做FlowLayout型布局。FlowLayout类的一个常用构造方法为FlowLayout()
该构造方法可以创建一个居中对齐的布局对象。例如:
FlowLayout flow = new FlowLayout();
如果一个容器con使用这个布局对象,即con.setLayout(flow);
,那么,con可以使用Container类提供的add方法将组件顺序地添加到容器中,组件按照加入的先后顺序从左向右排列,一行排满之后就转到下一行继续从左至右排列。
FlowLayout布局对象调用setHgap(int hgap)
方法和setVgap(int vgap)
方法可以设置布局的水平间隙和垂直间隙。
BorderLayout布局
BorderLayout布局是Window型容器的默认布局,例如JFrame和JDialog都是Window类的间接子类。
如果容器使用BorderLayout布局,那么容器空间简单地划分为东、西、南、北、中五个区域。每加入一个组件都应该指明把这个组件添加在哪个区域中,区域由BorderLayout中的静态常量EAST, WEST, SOUTH, NORTH, CENTER表示。
添加到某个区域的组件将占据这个区域。每个区域只能放置一个组件,如果向某个已放置了组件的区域再放置一个组件,那么先前的组件将被后者替换掉。
CardLayout布局
使用CardLayout的容器可以容纳多个组件,但是实际上同一时刻容器只能从这些组件中选出一个来显示,这个被显示的组件将占据所有的容器空间。 JTabbedPane创建的对象是一个轻容器,称作选项卡窗格。选项卡窗格的默认布局是CardLayout布局。 选项卡窗格可以使用add(String text, Component com);方法将组件com添加到容器当中,并指定和该组件com对应的选项卡的文本提示是text。
GridLayout布局
GridLayout是使用较多的布局编辑器,其基本布局策略是把容器划分成若干行乘若干列的网格区域,组件就位于这些划分出来的小格中。
-
使用GridLayout的构造方法GridLayout(int m, int n)创建布局对象,指定划分网格的行数m和列数n,例如: GridLayout grid = new GridLayout(10,8);
-
使用GridLayout布局的容器调用方法add将组件加入容器,组件进入容器的顺序将按照第一行第一个、第一行第二个…第一行最后一个、第二行第一个、…最后一行第一个…最后一行最后一个。
BoxLayout布局
用BoxLayout类可以创建一个布局对象,称为盒式布局。BoxLayout在java.swing.border包中。java swing包提供了Box类,该类也是Container类的一个子类,创建的容器称作一个盒式容器,盒式容器的默认布局是盒式布局,而且不允许更改盒式容器的布局。因此,在策划程序的布局时,可以利用容器的嵌套,在某个容器中嵌入几个盒式容器,达到布局的目的。使用盒式布局的容器将组件排列在一行或一列,这取决于创建盒式布局对象时,指定了行排列还是列排列。
在行型盒式布局容器中添加的组件的上沿在同一水平线上。在列型盒式布局容器中添加的组件的左沿在同一垂直线上。 使用Box类的静态方法 createHorizontalBox()可以获得一个具有行型盒式布局的盒式容器;使用Box类的静态方法 createVerticalBox()可以获得一个具有列型盒式布局的盒式容器。
如果想控制盒式布局容器中组件之间的距离,就需要使用水平支撑或垂直支撑。Box类调用静态方法createHorizontalStrut(int width)可以得到一个不可见的水平Strut类型对象,称做水平支撑。该水平支撑的高度为width ,宽度是width。Box类调用静态方法createVertialStrut(int height)可以得到一个不可见的垂直Strut类型对象,称做垂直支撑。参数height决定垂直支撑的高度,垂直支撑的高度为height 。
null布局
我们可以把一个容器的布局设置为null布局(空布局)。空布局容器可以准确地定位组件在容器的位置和大小。setBounds(int a, int b, int width, int height)方法是所有组件都拥有的一个方法,组件调用该方法可以设置组件本身的大小和在容器中的位置。例如,p是某个容器,p.setLayout(null);会把p的布局设置为空布局。
向空布局的容器p添加一个组件com需要两个步骤,首先使用add(com)方法向容器添加组件,然后组件com再调用setBounds(int a, int b, int width, int height)方法设置该组件在容器中的位置和本身的大小,组件都是一个矩形结构,方法中的参数a,b是被添加的组件com的左上角在容器中的位置坐标,即该组件距容器左面a个像素,距容器上方b个像素;width和height是组件com的宽和高。
10.3 菜单组件
窗口中的菜单条(menu bar)、菜单(menu)、菜单项(menu item)是我们所熟悉的界面,菜单放在菜单条里,菜单项放在菜单里。
JMenuBar菜单条
JComponent类的子类JMenuBar是负责创建菜单条的,即JMenuBar的一个实例就是一个菜单条。JFrame类有一个将菜单条放置到窗口中的方法public void setJMenuBar(JMenuBar menubar); 需要注意的是,只能向窗口添加一个菜单条。
JMenu菜单
JComponent类的子类JMenu类是负责创建菜单的,JMenu类的主要方法有以下几种: JMenu(String s):建立一个指定标题的菜单,标题由参数s确定。 public void add(MenuItem item):向菜单增加由参数item指定的菜单项对象。 public void add(String s):向菜单增加指定的选项。 public JMenuItem getItem(int n):得到指定索引处的菜单项。 public int getItemCount():得到菜单项数目。
JMenuItem菜单项
JMenuItem是JMenu的父类,该类是负责创建菜单项的,即JMenuItem的一个实例就是一个菜单项。菜单项将被放在菜单里。JMenuItem类的主要方法有以下几种:
- JMenuItem(String s):构造有标题的菜单项。
- JMenuItem(String s, Icon icon):构造有标题和图标的菜单项。
- public void setEnabled(boolean b):设置当前菜单项是否可被选择。
- public String getLabel():得到菜单项的名字。
public void setAccelerator(KeyStroke keyStroke)
:为菜单项设置快捷键。为了向该方法的参数传递一个KeyStroke对象,可以使用KeyStroke类的类方法:public static KeyStroke getKeyStroke(char keyChar)返回一个KeyStroke对象。也可以使用KeyStroke类的静态方法public static KeyStroke getKeyStroke(int keyCode, int modifiers)返回一个KeyStroke对象- 参数keyCode取值范围:KeyEvent.VK_A~KeyEvent.VK_Z
- 参数modifiers取值范围:InputEvent.ALT_MASK, InputEvent.CTRL_MASK和InputEvent.SHIFT_MASK
JMenu
嵌入子菜单JMenu是JMenuItem的子类,因此菜单项本身也可以是一个菜单,称这样的菜单项为子菜单。为了使得菜单项有一个图标,可以用图标类Icon声明一个图标,然后使用其子类ImageIcon类创建一个图标,如Icon icon = new ImageIcon(“open.gif”);
Example:一个含有彩蛋的窗口
import javax.swing.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
public class Example10_1
{
public static void main(String args[])
{
FirstWindow win = new FirstWindow("一个简单的窗口");
}
}
class FirstWindow extends JFrame
{
JMenuBar menubar;
JMenu menu;
JMenuItem item1,item2;
FirstWindow(String s)
{
setTitle(s);
// ---
item1 = new JMenuItem("打开", new ImageIcon("open.gif"));
item2 = new JMenuItem("保存", new ImageIcon("save.gif"));
item1.setAccelerator(KeyStroke.getKeyStroke('O'));
item2.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_MASK));
// ---
menu = new JMenu("文件");
menu.add(item1);
menu.addSeparator();
menu.add(item2);
// ---
menubar = new JMenuBar();
menubar.add(menu);
setJMenuBar(menubar);
// ---
validate();
// ---
setSize(160,170);
setLocation(120,120);
setVisible(true);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
}
}
10.6 文本组件
JTextField文本框
JTextField创建的一个对象就是一个文本框。用户可以在文本框输入单行的文本。
JTextField类的主要方法:
- JTextField(int x):如果使用这个构造方法创建文本框对象 ,文本框的可见字符个数由参数x指定。
- JTextField(String s):如果使用这个构造方法创建文本框对象,则文本框的初始字符串为s。
- public void setText(String s):文本框对象调用该方法可以设置文本框中的文本为参数s指定的文本。
- public String getText():文本框对象调用该方法可以获取文本框中的文本。
- public void setEditable(boolean b):文本框对象调用该方法可以指定文本框的可编辑性。
- public void setHorizontalAlignment(int alignment):设文本在文本框中的对齐方式,其中alignment的有效值确定对齐方式。
JPasswordField
密码框可以使用setEchoChar(char c)设置回显字符(默认的回显字符是‘*’);使用char[] getPassword()方法返回密码框中的密码。
ActionEvent事件
学习组件除了需要了解组件的属性和功能外,一个更重要的方面是学习怎样处理组件上发生的界面事件。当用户在有输入焦点的文本框(JTextField)中按回车键、单击按钮、在一个下拉式列表中选择一个条目等操作时,都会发生界面事件。程序有时需要对发生的事件作出反应,来实现特定的任务。 在学习处理事件时,必须很好地掌握事件源、监视/监听器和处理事件的接口这三个概念。
事件源
能够产生事件的对象都可以成为事件源,如文本框、按钮、下拉式列表等。也就是说,事件源必须是一个对象,而且这个对象必须是Java认为能够发生事件的对象。
监视/监听器。
我们需要一个对象对事件源进行监视/监听,以便对发生的事件作出处理。事件源通过调用相应的方法将某个对象作为自己的监视器。
例如,对于文本框,这个方法是addActionListener(ActionListener listener),对于获取了监视器的文本框对象,在文本框(JTextField)获得输入焦点之后,如果用户按回车键,Java运行系统就自动用ActionEvent类创建一个对象,即发生了ActionEvent事件。
处理事件的接口
注意到发生ActionEvent事件的事件源对象获得监视器的方法是: addActionListener(ActioListener listener);
该方法中的参数是ActionListener类型的接口,因此必须将一个实现ActionListener接口的类创建的对象作为传递给该方法的参数,使得该对象成为事件源的监视器。监视器负责调用特定的方法来处理事件,也就是说创建监视器的类必须提供处理事件的特定方法,即实现接口中的方法。Java采用接口回调技术来处理事件,当事件源发生事件时,接口立刻通知监视器自动调用实现的某个接口方法,该接口方法规定了怎样处理事件的操作。接口回调这一过程对程序是不可见的,Java在设计组件事件时已经设置好了这一回调过程,程序只需让事件源获得正确的监视器,即将实现了正确接口的对象的引用传递给方法。
ActionEvent类中的方法
ActionEvent事件对象调用方法public Object getSource()
可以返回发生ActionEvent事件的对象的引用。
菜单项上的ActionEvent事件
单击某个菜单项可以发生ActionEvent事件。菜单项使用addActionListener(ActionListener listener)方法获得监视器。
JTextArea文本区
(1)使用JTextArea(int rows, int columns)构造一个可见行和可见列分别是rows、columns的文本区。 使用setLineWrap(boolean b)方法决定输入的文本能否在文本区的右边界自动换行; 使用setWrapStyleWord(boolean b)决定是以单词为界(b取true时)或以字符为界(b 取false时)进行换行。 使用append(String s)尾加文本。 使用insert(String s, int x)方法在文本区的指定位置处插入文本。 使用getCaretPosition()获取文本区中输入光标的位置。 使用copy()和cut()方法将文本区中选中的内容拷贝或剪切到系统的剪贴板。 使用paste()方法将系统剪贴板上的文本数据粘贴在文本区中。
(2) 文本区上的DocumentEvent事件 文本区可以触发DocumentEvent事件,DocumentEvent类在javax.swing.event包中。用户在文本区组件的UI代表的视图中进行文本编辑操作,使得文本区中的文本内容发生变化,将导致该组件所维护的文档模型中的数据发生变化,从而导致DocumentEvent事件的发生。需要使用addDocumentListener()
方法向组件维护的文档注册监视器。
监视器需实现DocumentListener接口,该接口中有三个方法:
public void changedUpdate(DocumentEvent e)
public void removeUpdate(DocumentEvent e)
public void insertUpdate(DocumentEvent e)
文本区调用 getDocument()方法返回维护的文档,该文档是实现了Document接口类的一个实例。
10.13 组件常用方法
JComponent类是所有组件的父类,这一节介绍JComponent类的几个常用方法。 组件都是矩形形状,组件本身有一个默认的坐标系,组件的左上角的坐标值是(0,0)。如果一个组件的宽是20,高是10,那么,该坐标系中,x坐标的最大值是20;y坐标的最大值是10。
组件的颜色
public void setBackground(Color c):设置组件的背景色。 public void setForeground(Color c):设置组件的前景色。 public Color getBackground(Color c):获取组件的背景色。 public Color getForeground(Color c):获取组件的前景色。
上述方法中都涉及到Color类,Color类是java.awt包中的类,该类创建的对象称为颜色对象。 用Color类的构造方法public Color(int red, int green, int blue)可以创建一个颜色对象,其中red、green和blue的取值在0到255之间。
组件透明
组件默认是不透明的。public void setOpaque(boolean isOpaque)设置组件是否不透明,当参数isOpaque取false时组件被设置为透明,取值true时组件被设置为不透明。 public boolean isOpaque() 当组件不透明时该方法返回true,否则返回false。
组件的边框
组件默认的边框是一个黑边的矩形。 public void setBorder(Border border):设置组件的边框。 public Border getBorder():返回边框。 组件调用setBorder方法来设置边框,该方法的参数是一个接口,因此必须向该参数传递一个实现接口Border类的实例,如果传递一个null,组件将取消边框。
组件的字体
public void setFont(Font f):组件调用该方法设置组件上的字体。例如,文本组件调用该方法可以设置文本组件中的字体。 public Font getFont(Font f):组件调用该方法获取组件上的字体。上述方法中用到了java.awt包中的Font类,该类创建的对象称为字体对象。 Font类的构造方法是public Font(String name, int style, int size);使用该构造方法可以创建字体对象。其中,name是字体的名字,如果系统不支持字体的名字,将取默认的名字创建字体对象。style决定字体的样式,取值是一个整数。在创建字体对象时,应当给出一个合理的字体名字,也就是说,程序所在的计算机系统上有这样的字体名字。如果在创建字体对象时,没有给出一个合理的字体名字,那么该字体在特定平台的字体系统名称为默认名称。
组件的大小与位置
public void setSize(int width, int height):组件调用该方法设置组件的大小,通过参数width和height指定组件的宽度和高度。 public void setLocation(int x, int y):组件调用该方法设置组件在容器中的位置,参数x, y是组件距容器的左边界x个像素,距容器的上边界 y 个像素。 public Dimension getSize():组件调用该方法返回一个Dimension对象的引用,该对象实体中含有名字是width和height的成员变量,也就是当前组件的宽度和高度。
public Point getLocation(int x, int y):组件调用该方法返回一个Point对象的引用,该对象实体中含有名字是x 和y的成员变量,就是组件的左上角在容器的坐标系中的x坐标和y坐标。 public void setBounds(int x, int y, int width, int height):组件调用该方法设置组件在容器中的位置和组件的大小。 public Rectangle getBounds():组件调用该方法返回一个Rectangle对象的引用,该对象实体中含有名字是x、y、width和height的成员变量,分别是当前组件左上角在容器坐标系中的x坐标和y坐标,宽度和高度。
组件的激活与可见性
public void setEnabled(boolean b):组件调用该方法可以设置组件是否可被激活,当参数b取值true时,组件可以被激活,当参数b取值false 时,组件不可激活。默认情况下,组件是可以被激活的。 public void setVisible(boolean):设置组件在该容器中的可见性,当参数b取值true时,组件在容器中可见,当参数b取值false 时,组件在容器中不可见。除了Window型组件外,其它类型组件默认是可见的。
以下有关组件的内容请同学们自学 10.7 按钮与标签组件 10.8 复选框与单选按钮组件 10.9 列表组件 10.10 表格组件 10.11 树组件 10.12 进度条组件
10.14 窗口事件
WindowListener接口
JFrame类是Window类的子类,Window型对象都能触发WindowEvent事件。当一个 JFrame窗口被激活、撤销激活、打开、关闭、图标化或撤销图标化时,会引发窗口事件,即WindowEvent创建一个窗口事件对象。 窗口使用addWindowListener()方法获得监视器,创建监视器对象的类必须实现WindowListener接口,该接口中有7个不同的方法,分别是: public void WindowActivated(WindowEvent e):当窗口从非激活状态到激活状态时,窗口的监视器调用该方法。 public void WindowDeactivated(WindowEvent e):当窗口从激活状态到非激活状态时,窗口的监视器调用该方法。 public void WindowClosing(WindowEvent e):窗口正在被关闭时,窗口监视器调用该方法。
public void WindowClosed(WindowEvent e):当窗口关闭时,窗口的监视器调用该方法。 public void WindowIconified(WindowEvent e):窗口图标化时,窗口的监视器调用该方法。 public void WindowDeiconified(WindowEvent e):当窗口撤销图标化时,窗口的监视器调用该方法。 public void WindowOpened(WindowEvent e):当窗口打开时,窗口的监视器调用该方法。
WindowEvent创建的事件对象调用getWindow()方法可以获取发生窗口事件的窗口。 当单击窗口上的关闭图标时,监视器首先调用WindowClosing()方法,然后执行窗口初始化时用setDefaultCloseOperation(int n)方法设定的关闭操作,最后再执行WindowClosed()方法。 如果在WindowClosing()方法执行了System.exit(0);或setDefaultCloseOperation(int n)设定的关闭操作是EXIT_ON_CLOSE或DO_NOTHING_ON_CLOSE,那么监视器就没有机会再调用WindowClosed()方法了。 当单击窗口的图标化按钮时,监视器调用WindowIconified()方法后,还将调用WindowDeactivated()方法。 当撤销窗口图标化时,监视器调用WindowDeiconified()方法后还会调用WindowActivated()方法。
WindowAdapter适配器
接口中如果有多个方法会给使用者带来诸多不便,因为实现这个接口的类必须实现该接口中的全部方法,否则这个类必须是一个abstract类。为了给编程人员提供方便,对于Java提供的接口,如果其中的方法多于一个,就提供一个相关的称为适配器(adapter)的类,这个适配器是已经实现了相应接口的类,只是相应方法的实现内容为空。
例如,Java在提供WindowListener接口的同时,又提供了WindowAdapter类,WindowAdapter类实现了WindowListener接口。因此,可以使用WindowAdapter类的子类创建的对象作为监视器,在子类中重写所需要的接口方法即可。
10.15 鼠标事件
鼠标事件的触发
组件是可以触发鼠标事件的事件源。用户的下列7种操作都可以使得组件触发鼠标事件: 鼠标指针从组件外进入 鼠标指针从组件内退出 鼠标指针停留在组件上时,按下鼠标 鼠标指针停留在组件上时,释放鼠标 鼠标指针停留在组件上时,单击鼠标 在组件上拖动鼠标指针 在组件上移动鼠标指针 鼠标事件的类型是MouseEvent,即组件触发鼠标事件时,MouseEvent类自动创建一个事件对象。
MouseListener接口与MouseMotionListener接口
Java使用两个接口来处理鼠标事件 MouseListener接口 如果事件源使用addMouseListener(MouseListener listener)获取监视器,那么用户的下列5种操作可以使得事件源触发鼠标事件: 鼠标指针从组件外进入 鼠标指针从组件内退出 鼠标指针停留在组件上时,按下鼠标 鼠标指针停留在组件上时,释放鼠标 鼠标指针停留在组件上时,单击或连续单击鼠标
创建监视器的类必须要实现MouseListener接口,该接口有5个方法: mouseEntered(MouseEvent e):负责处理鼠标进入组件触发的鼠标事件。 mouseExited(MouseEvent e):负责处理鼠标退出组件触发的鼠标事件。 mousePressed(MouseEvent e):负责处理鼠标按下触发的鼠标事件。 mouseReleased(MouseEvent e):负责处理鼠标释放触发的鼠标事件。 mouseClicked(MouseEvent e):负责处理鼠标单击或连击触发的鼠标事件。
MouseMotionListener接口 如果事件源使用addMouseMotionListener(MouseMotionListener listener)获取监视器,那么用户的下列两种操作可使得事件源触发鼠标事件: 在组件上拖动鼠标指针 在组件上移动鼠标指针
相应的有两个处理接口的方法 mouseDragged(MouseEvent e):负责处理鼠标拖动事件,即在事件源上拖动鼠标时,监视器将自动调用接口中的这个方法对事件做出处理。 mouseMoved(MouseEvent e):负责处理鼠标移动事件,即在事件源上移动鼠标时,监视器将自动调用接口中的这个方法对事件做出处理。
由于处理鼠标事件的接口中的方法多于一个,Java提供了相应的适配器(adapter)类,分别是MouseAdapter和MouseMotionAdapter,这两个类分别实现了MouseListener接口和MouseMotionListener接口。
MouseEvent类
在处理鼠标事件时,程序经常关心鼠标在当前组件坐标系中的位置,以及触发鼠标事件使用的是鼠标的左键或右键等信息。 MouseEvent类中有下列几个重要的方法: getX():鼠标事件调用该方法返回触发当前鼠标事件时,鼠标指针在事件源坐标系中的x-坐标。 getY():鼠标事件调用该方法返回触发当前鼠标事件时,鼠标指针在事件源坐标系中的y-坐标。 getClickCount():鼠标事件调用该方法返回鼠标被连续点击的次数。 getModifiers():鼠标事件调用该方法返回一个整数值,如果是通过鼠标左键触发的鼠标事件,该方法返回的值等于InputEvent类中的类常量BUTTON1_MASK;如果是右键返回的是InputEvent类中的类常量BUTTON3_MASK来表示。 getSource():鼠标事件调用该方法返回触发当前鼠标事件的事件源。
用鼠标拖动组件
可以使用坐标变换来实现组件的拖动。当我们用鼠标拖动容器中的组件时,可以先获取鼠标指针在组件坐标系中的坐标x,y,以及组件的左上角在容器坐标系中的坐标a,b;如果在拖动组件时,想让鼠标指针的位置相对于拖动的组件保持静止,那么,组件左上角在容器坐标系中的位置应当是a+x-x0和a+y-y0,其中x0和y0是最初在组件上按下鼠标时鼠标指针在组件坐标系中的位置坐标。
弹出式菜单
单击鼠标右键出现“弹出式菜单”是用户熟悉和常用的操作。弹出式菜单由JPopupMenu类负责创建,可以用下列构造方法创建弹出式菜单: public JPopupMenu():构造无标题弹出式菜单。 public JPopupMenu(String label):构造由参数label指定标题的弹出式菜单。 通过调用public void show(Component invoker, int x, int y)方法设置弹出式菜单在组件invoker上的弹出位置。
10.16 焦点事件
组件可以触发焦点事件。组件可以使用public void addFocusListener(FocusListener listener)增加焦点事件监视器。
创建监视器的类必须要实现FocusListener接口,该接口有两个方法: public void focusGained(FocusEvent e) public void focusLost(FocusEvent e)
当组件从无输入焦点变成有输入焦点触发FocusEvent事件时,监视器调用类实现的接口方法focusGained(FocusEvent e);当组件从有输入焦点变成无输入焦点触发FocusEvent事件时,监视器调用类实现的接口方法focusLost(FocusEvent e)。 一个组件可以调用public boolean requestFocusInWindow()方法获得输入焦点。
当一个组件处于激活状态时,组件可以成为触发KeyEvent事件的事件源。当某个组件处于激活状态时,如果用户敲击了键盘上一个键就会导致这个组件触发KeyEvent事件。
使用KeyListener接口处理键盘事件 组件使用addKeyListener方法获得监视器。监视器是一个对象,创建该对象的类必须实现接口KeyListener。接口KeyListener中有3个方法: public void keyPressed(KeyEvent e) public void keyTyped(KeyEvent e) public void KeyReleased(KeyEvent e)
当按下键盘上某个键时,监视器就会发现,然后方法keyPressed(KeyEvent e)会自动执行,并且KeyEvent类自动创建一个对象传递给方法keyPressed(KeyEvent e)中的参数e。方法keyTyped(KeyEvent e)是keyPressed(KeyEvent e)和keyReleased(KeyEvent e)方法的组合。
用KeyEvent类的public int getKeyCode()方法可以判断哪个键被按下、敲击或释放,该方法返回一个键码值(如表10.1所示)。 KeyEvent类的public char getKeyChar()判断哪个键被按下、敲击或释放,该方法返回键的字符。
处理复合键 键盘事件KeyEvent对象调用getModifiers()方法,可以返回下列整数值,它们分别是InputEvent类的类常量:ALT_MASK、CTRL_MASK 、SHIFT_MASK
小结: 事件类型:WindowEvent, MouseEvent, FocusEvent, KeyEvent
监视器接口:WindowListener, MouseListener/MouseMotionListener, FocusListener, KeyListener
适配器:WindowAdapter, MouseAdapter/MouseMotionAdapter
要求:理解事件触发和响应的流程(与10.6节中ActionEvent, DocumentEvent的内容类似);能查阅各种方法等的细节
10.18 AWT线程
AWT线程
当Java程序包含图形用户界面(GUI)时,Java虚拟机在运行应用程序时会自动启动更多的线程,其中有两个重要的线程:AWT-EventQueue和AWT-Windows。 AWT-EventQueue线程负责处理GUI事件,AWT-Windows线程负责将窗体或组件绘制到桌面。
挂起、恢复和终止线程
我们可以通过GUI界面事件,即在AWT-EventQueue线程中,通知其它线程开始运行、挂起、恢复或死亡。
所谓挂起一个线程就是让线程暂时让出CPU的使用权限,暂时停止执行 ,挂起一个线程可以使用wait方法。 恢复线程就是让曾挂起的线程恢复执行过程,即从曾中断处继续线程的执行。为了恢复线程,可以让主线程或其它线程执行notifyAll()方法,通知挂起的线程继续执行。 终止线程就是让线程结束run方法的执行进入死亡状态。
使用Timer类的构造方法:Timer(int a, Object b)创建一个计时器,其中的参数a的单位是豪秒,确定计时器每隔a毫秒“震铃”一次,参数b是计时器的监视器。 计时器发生的震铃事件是ActionEvent类型事件。当震铃事件发生时,监视器就会监视到这个事件,监视器就回调ActionListener接口中的actionPerformed方法。
10.20 MVC设计模式
MVC是一种通过三个不同部分构造一个软件或组件的理想办法: 模型(model):用于存储数据的对象。 视图(view):为模型提供数据显示的对象。 控制器(controller):处理用户的交互操作,对用户的操作作出响应;让模型和视图进行必要的交互,即通过视图修改获取模型中的数据;当模型中的数据变化时,让视图更新显示。
10.21 播放音频
使用Applet的一个静态的方法(类方法):newAudioClip(java.net.URL url) 根据参数url封装的音频获得一个可用于播放的音频对象clip,clip对象可以使用下列方法来处理声音文件: play():开始播放, loop():循环播放, stop():停止播放。
10.22 按钮绑定到键盘
按钮绑定到键盘通常被理解为用户直接敲击某个键代替用鼠标单击该按钮所产生的效果 。
1.AbstractAction类与特殊的监视器 如果按钮通过addActionListener()方法注册的监视器和程序为按钮的键盘操作指定的监视器是同一个监视器,用户直接敲击某个键(按钮的键盘操作)就可代替用鼠标单击该按钮所产生的效果,这也就是人们通常理解的按钮的键盘绑定。
抽象类javax.swing.AbstractAction类已经实现了Action接口,因为大部分应用不需要实现Action中的其他方法,因此编写AbstractAction类的子类时,只要重写public void actionPerformed(ActionEvent e)即可,该方法是ActionListener接口中的方法。
为按钮的键盘操作指定了监视器后,用户只要敲击相应的键,监视器就执行actionPerformed()方法。
2.指定监视器的步骤
以下假设按钮是button,listener是AbstractAction类的子类的实例。
(1) 获取输入映射:按钮首先调用public final InputMap getInputMap(int condition)方法返回一个InputMap对象,其中参数condition取值JComponent类的下列static常量:WHEN_FOCUSED, WHEN_IN_FOCUSED_WINDOW, WHEN_ANCESTOR_OF_FOCUSED_COMPONENT。
例如:InputMap inputmap = button.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
(2) 绑定按钮的键盘操作:步骤(1)返回的输入映射首先调用方法public void put(KeyStroke keyStroke, Object actionMapKey)将敲击键盘上的某键指定为按钮的键盘操作,并为该操作指定一个Object类型的映射关键字,例如:inputmap.put(KeyStroke.getKeyStroke(“A”), “abcdefg”);
(3) 为按钮的键盘操作指定监视器:按钮调用方法public final ActionMap getActionMap()返回一个ActionMap对象:ActionMap actionmap = button.getActionMap(); 然后,该对象actionmap调用方法public void put(Object key, Action action) 为按钮的键盘操作指定监视器(实现单击键盘上的键通知监视器的过程)。例如:actionmap.put(“abcdefg”, listener);
10.25 发布应用程序
可以使用jar.exe把一些文件压缩成一个JAR文件,来发布我们的应用程序。可以把Java应用程序中涉及到的类压缩成一个JAR文件,如Tom.jar,然后使用Java解释器(使用参数-jar)执行这个压缩文件:java -jar Tom.jar或用鼠标双击该文件,执行这个压缩文件。
Comments powered by Disqus.