Java通篇复习

题型

  1. 填空题(10个1分)
    例子:
    Java里实现多任务的机制:线程;

实现线程的类:thread,runnable

  1. 单选(10个1分)基本上是结合代码(基础,使用,基本概念丰富一点)例子:各种函数

  2. 简答(30) 阐述一个概念
    例子:
    Java异常处理有几种方式?
    声明抛出异常,捕获异常

类变量与实例变量?
实例变量
没有static修饰的变量称为实例变量(Instance Variables)
用来存储所有实例都需要的属性信息,不同实例的属性值可能会不同
可通过下面的表达式访问实例属性的值
<实例名>.<实例变量名>
类变量
也称为静态变量,声明时需加static修饰符
不管类的对象有多少,类变量只存在一份,在整个类中只有一个值
类初始化的同时就被赋值
适用情况

  • 类中所有对象都相同的属性
  • 经常需要共享的数据
  • 系统中用到的一些常量值
    引用格式
    <类名 | 实例名>.<类变量名>
  1. 程序阅读 (4个5分) 问我结果是什么(接口),线程,多态,接口,二次分发

  2. 编程 (3个10分)
    知识点:
    1.Java的特点,特征,编译型的程序,基本数据类型与表达式(看哪个不对),输入输出,数组

    2.什么是抽象封装继承多态的概念?类和对象的关系,区别?public,abstract晓得意思?类变量,成员变量(方法)定义方式有什么区别?
    Heap and stack保存在哪里?
    Heap and stack
    Heap memory 全局使用 stack memory 被某个线程使用
    Object在Heap中创建,在stack中包含对其的引用.
    Stack memory只存储基础数据类型和对对象的引用
    Stack中是后入先出LIFO,heap中更为复杂Java Garbage Collection.
    Stack memory生命周期较短 heap memory生命周期是整个程序运行时间.
    Stack memory的大小较Heap memory较小.
    Stack memory速度较快相较于heap memory.

    final关键字的作用是什么?
    final关键字可以用来修饰引用、方法和类。
    如果引用为基本数据类型,则该引用为常量,该值无法修改;
    如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改。
    如果引用时类的成员变量,则必须当场赋值,否则编译会报错。
    当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。
    当用final修改类时,该类成为最终类,无法被继承
    实例变量和类变量都可被声明为final
    final实例变量必须在每个构造方法结束之前赋初值,以保证使用之前会被初始化
    final类变量必须在声明的同时初始化

    包的作用?
    将相关的源代码文件组织在一起
    类名的空间管理,利用包来划分名字空间,便可以避免类名冲突
    提供包一级的封装及存取权限

    UML的图,掌握基本的关系,如何表示一个类(代码与图之间转化?1对1?1对多?)

    3.接口与多态:语法,什么叫塑性?什么叫多态?什么叫动态绑定(程序阅读,小汽车问题)?构造方法的调用顺序?重载,覆盖?
    4.集合对象:底层的接口(Map等),List,Array…的对比,什么时候用哪一个?基于list实现的queue和arraydequeue
    5.Java IO:节点流处理流概念;接口逻辑;文本输入输出基本方法;
    6.异常处理:3-7,3-8,3-9.会单独有PPT
    7.多线程:基本编程你要知道,thread类和runnable接口,所有类要知道作用,数据共享(程序阅读),8.2.1

概述

  • 面向对象技术给软件发展带来的益处
    可重用性
    可靠性

  • 面向对象语言的基本特征
    抽象和封装
    继承性:对已有类增加属性和功能或进行部分修改来建立新的类,实现代码重用
    多态性:同一个消息被不同的对象接收后可以导致不同的行为

Java特点:

  • 面向对象
  • 安全性
    Java不支持指针
    Java的内部安全措施
  • 平台无关性
    编译后的字节码对应于Java虚拟机,因此可在不同平台上运行
  • 多线程
    Java是第一个在语言级提供内置多线程支持的高级语言
  • 内存管理
    Java对内存自动进行管理并进行垃圾回收

变量与常量

变量

  • 一个由标识符命名的项
  • 每个变量都有类型, 例如 int 类型或 Object类型, 变量还有作用域.
  • 变量的值可以被改变.

常量

  • 常量一旦被初始化以后就不可改变。

标识符

  1. 标识符是一个名称,与内存中的某个位置(地址)相对应
  2. 标识符的第一个字符必须是下列字符之一:
    大写字母 (A-Z)
    小写字母 (a-z)
    下划线(_)
    美元符号 ($)
  3. 标识符的第二个字符及后继字符必须是:
    上述列表中的任意字符
    数字字符 (0-9)

数组的声明

声明(Declaration)

  • 声明数组时无需指明数组元素的个数,也不为数组元素分配内存空间
  • 不能直接使用,必须经过初始化分配内存后才能使用

Type[ ] arrayName;
Type arrayName[ ];

public class MyArray {
    public static void main(String[] args){
        int myArray[];                  //声明数组
        myArray=new int[10];            //创建数组
        System.out.println("Index\t\tValue");
        for(int i=0; i<myArray.length;i++)
            System.out.println(i+"\t\t"+myArray[i]);
        //证明数组元素默认初始化为0
        //myArray[10]=100;      //将产生数组越界异常
    } 
}

数组名是一个引用:
例子

public class Arrays 
{   public static void main(String[] args) 
    {  int[] a1 = { 1, 2, 3, 4, 5 }; 
        int[] a2; 
        a2 = a1; 
        for(int i = 0; i < a2.length; i++)   a2[i]++; 
        for(int i = 0; i < a1.length; i++) 
            System.out.println( "a1[" + i + "] = " + a1[i]); 
    } 
} 
运行结果:
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6

字符串引用构成的数组:
String[] strArray;

strArray = new String[8] ;

strArray[0]= "Hello" ;
public class ArrayOfStringsDemo 
{ public static void main(String[] args) 
   { String[] anArray =
         { "String One", "String Two", "String Three"};
      for (int i = 0; i < anArray.length; i++)
      { System.out.println(anArray[i].toLowerCase());
      } 
   } 
} 

运行结果:
string one
string two
string three

数组的复制:

public class ArrayCopyDemo { 
    public static void main(String[] args) { 
        char[] copyFrom = { 'd', 'e', 'c', 'a', 'f', 'f', 'e', 'i', 'n', 'a', 't', 'e', 'd'}; 
        char[] copyTo = new char[7]; 
        System.arraycopy(copyFrom, 2, copyTo, 0, 7); 
        System.out.println(new String(copyTo));
   } 
} 
结果:
caffein

多维数组:

二维数组的长度
	class UnevenExample2
	{ public static void main( String[ ] arg )
 	   { int[ ][ ] uneven = 
	        { { 1, 9, 4 },
	          { 0, 2},
	          { 0, 1, 2, 3, 4 } };
   	      System.out.println("Length is: " + uneven.length );
  	    }
	}

运行结果:
Length is: 3

每行的长度:
	class UnevenExample3
	{
 	    public static void main( String[] arg )
 	   {
   	    // 声明并构造一个2维数组
      	int[ ][ ] uneven = 	
	          { { 1, 9, 4 },
	          { 0, 2},
         	  { 0, 1, 2, 3, 4 } };
        // 数组的长度 (行数)
        System.out.println("Length of array is: " + uneven.length );
        // 数组每一行的长度(列数)
        System.out.println("Length of row[0] is: " + uneven[0].length );
        System.out.println("Length of row[1] is: " + uneven[1].length );
        System.out.println("Length of row[2] is: " + uneven[2].length );
  }
}

运行结果:
Length of array is: 3
Length of row[0] is: 3
Length of row[1] is: 2
Length of row[2] is: 5

Object-Oriented 面向对象

class 类

将属性及行为相同或相似的对象归为一类
类可以看成是对象的抽象,代表了此类对象所具有的共有属性和行为
在面向对象的程序设计中,每一个对象都属于某个特定的类

抽象

忽略问题中与当前目标无关的方面,以便更充分地注意与当前目标有关的方面

封装

利用抽象数据类型将数据和基于数据的操作封装在一起
用户只能看到对象的封装界面信息,对象的内部细节对用户是隐蔽的
封装的目的在于将对象的使用者和设计者分开,使用者不必知道行为实现的细节,只需使用设计者提供的消息来访问对象

封装定义:

  • 清楚的边界
    所有对象的内部信息被限定在这个边界内
  • 接口
    对象向外界提供的方法,外界可以通过这些方法与对象进行交互
  • 受保护的内部实现
    功能的实现细节,不能从类外访问。

继承

是指新的类可以获得已有类(称为超类、基类或父类)的属性和行为,称新类为已有类的派生类(也称为子类)

在继承过程中派生类继承了基类的特性,包括方法和实例变量

派生类也可修改继承的方法或增加新的方法,使之更适合特殊的需要

有助于解决软件的可重用性问题,使程序结构清晰,降低了编码和维护的工作量

单继承

多态

一个程序中同名的不同方法共存
主要通过子类对父类方法的覆盖来实现
不同类的对象可以响应同名的消息(方法) ,具体的实现方法却不同
使语言具有灵活、抽象、行为共享、代码共享的优势,很好地解决了应用程序方法同名问题

类和对象

类是构造对象的模板,对象是类的具体实例

[public] [abstract | final] class 类名称 
  	[extends 父类名称]  
    [implements 接口名称列表]
	{  
		变量成员声明及初始化;
     	方法声明及方法体;
	}

基本语法

关键字

class
表明其后声明的是一个类。
extends
如果所声明的类是从某一父类派生而来,那么,父类的名字应写在extends之后
implements
如果所声明的类要实现某些接口,那么,接口的名字应写在implements之后

修饰符
可以有多个,用来限定类的使用方式

  • public
    表明此类为公有类
  • abstract
    指明此类为抽象类
  • final
    指明此类为终结类

类声明体
变量成员声明及初始化,可以有多个
方法声明及方法体,可以有多个

变量和对象
变量除了存储基本数据类型的数据,还能存储对象的引用,用来存储对象引用的变量称为引用变量
类的对象也称为类的实例

比如int a = 1;
a就是变量的命名,1就是变量的值。
而当变量指向一个对象时,这个变量就被称为引用变量
比如A a =new A();
a就是引用变量,它指向了一个A对象,也可以说它引用了一个A对象。我们通过操纵这个a来操作A对象。
此时,变量a的值为它所引用对象的地址。

对象的声明
格式
类名 变量名
例如Clock是已经声明的类名,则下面语句声明的变量aclock将用于存储该类对象的引用:
Clock aclock;
声明一个引用变量时并没有对象生成

对象的创建
生成实例的格式:
new <类名>()
例如: aclock=new Clock()
其作用是:
在内存中为此对象分配内存空间
返回对象的引用(reference ,相当于对象的存储地址)
引用变量可以被赋以空值
例如:aclock=null;

实例变量
没有static修饰的变量称为实例变量(Instance Variables)
用来存储所有实例都需要的属性信息,不同实例的属性值可能会不同
可通过下面的表达式访问实例属性的值
<实例名>.<实例变量名>

类变量
也称为静态变量,声明时需加static修饰符
不管类的对象有多少,类变量只存在一份,在整个类中只有一个值
类初始化的同时就被赋值
适用情况
类中所有对象都相同的属性
经常需要共享的数据
系统中用到的一些常量值
引用格式
<类名 | 实例名>.<类变量名>

public class Circle { 
   static double PI = 3.14159265; 
   int radius; 
}
public class ClassVariableTester { 
   public static void main(String args[]) { 
     Circle x = new Circle();  
     System.out.println(x.PI); 
     System.out.println(Circle.PI); 
     Circle.PI = 3.14; 
     System.out.println(x.PI); 
     System.out.println(Circle.PI); 
   } 
}
测试结果
3.14159265
3.14159265
3.14
3.14

public class Point 
{
   private int x;
   private int y;
   public static int pointCount=0;
   public Point(int x, int y) 
   { this.x = x; this.y = y; pointCount++;}
}

class ex2_4 
{
	public static void main(String[] args)
    {
		  Point p = new Point(1,1);
          System.out.println(p.pointCount);
		  Point q = new Point(2,2);
		  System.out.println(q.pointCount);
		  System.out.println(q.pointCount == Point.pointCount);
		  System.out.println(Point.pointCount);
	}
}

测试结果
1
2
true
2 

Heap and stack
Heap memory 全局使用 stack memory 被某个线程使用
Object在Heap中创建,在stack中包含对其的引用.

Stack memory只存储基础数据类型和对对象的引用
Stack中是后入先出LIFO,heap中更为复杂Java Garbage Collection.
Stack memory生命周期较短heap memory生命周期是整个程序运行时间.
Stack memory的大小较Heap memory较小. stack memory速度较快相较于heap memory.

final修饰符
实例变量和类变量都可被声明为final
final实例变量必须在每个构造方法结束之前赋初值,以保证使用之前会被初始化
final类变量必须在声明的同时初始化

方法成员
定义类的行为
一个对象能够做的事情
我们能够从一个对象取得的信息
可以没有,也可以有多个;一旦在类中声明了方法,它就成为了类声明的一部分
分为实例方法和类方法

声明格式
[public | protected | private]
[static][ final][abstract] [native] [synchronized]
返回类型 方法名([参数列表]) [throws exceptionList]
{
方法体
}

  • 方法修饰
    public、protected、private 为存取控制符
    static指明方法是一个类方法
    final指明方法是一个终结方法
    abstract指明方法是一个抽象方法
    native用来集成java代码和其它语言的代码
    synchronized用来控制多个并发线程对共享数据的访问

  • 返回类型
    方法返回值的类型,可以是任意的Java数据类型
    当不需要返回值时,返回类型为void

  • 参数类型
    简单数据类型,
    引用类型(数组、类或接口)
    可以有多个参数,也可以没有参数,方法声明时的参数称为形式参数

  • 方法体
    方法的实现
    包括局部变量的声明以及所有合法的Java指令
    局部变量的作用域只在该方法内部

  • throws exceptionList
    用来处理异常

方法调用
给对象发消息意味着调用对象的某个方法
从对象中取得信息
修改对象的状态或进行某种操作
进行计算及取得结果等
调用格式
<对象名>.<方法名>([参数列表])
称点操作符“.”前面的<对象名>为消息的接收者(receiver)

  • 参数传递
    值传递:参数类型为基本数据类型时
    引用传递:参数类型为对象类型或数组时

实例方法
表示特定对象的行为
声明时前面不加static修饰符
使用时需要发送给一个类实例

例如:
在Circle类中声明计算周长的方法

public class Circle { 
     static double PI = 3.14159265; 
     int radius;     
		public double circumference() { 
          return 2 * PI * radius; 
     } 
 }
    由于radius是实例变量,在程序运行时,Java会自动取其接收者对象的属性值
    也可将circumference方法体改为:
		return 2 * PI * this.radius; 
		关键字this代表此方法的接收者对象

public class CircumferenceTester { 
      public static void main(String args[]) { 
           Circle c1 = new Circle(); 
           c1.radius = 50; 
           Circle c2 = new Circle(); 
           c2.radius = 10;         
	       double circum1 = c1.circumference(); 
           double circum2 = c2.circumference(); 
           System.out.println("Circle 1 has circumference " + circum1); 
           System.out.println("Circle 2 has circumference " + circum2); 
     } 
 }
运行结果
Circle 1 has circumference 314.159265
Circle 2 has circumference 62.831853

说明
在使用实例方法时,需要将其发送给一个实例对象(也称给对象发送一条消息),radius的值即是接收者对象的值
在执行c1.circumference()时,radius的值为c1的radius属性值;在执行c2.circumference()时,radius的值为c2的radius属性值

以对象作为参数的方法举例:
在Circle类中增加fitsInside方法判断一个圆是否在一个长方形内,需要以Rectangle类的对象作为参数
public class Circle { 
     static double PI = 3.14159265; 
     int radius;     
     public double circumference() { 
          return 2 * PI * radius; 
     } 
    public void enlarge(int factor) { 
          radius = radius * factor; 
     }
	public boolean fitsInside (Rectangle r) {     
      return (2 * radius < r.width) && (2 * radius < r.height);      
	}
  }

public class InsideTester {
    public static void main(String args[]) { 
	 Circle c1 = new Circle(); 
	 c1.radius = 8; 
	 Circle c2 = new Circle(); 
	 c2.radius = 15; 
	 Rectangle r = new Rectangle(); 
	 r.width = 20; 
	 r.height = 30; 
	 System.out.println("Circle 1 fits inside Rectangle:" + c1.fitsInside(r)); 
	 System.out.println("Circle 2 fits inside Rectangle:" + c2.fitsInside(r)); 
 } 
}
运行结果
Circle 1 fits inside Rectangle: true
Circle 2 fits inside Rectangle: false

类方法
也称为静态方法,表示类中对象的共有行为
声明时前面需加static修饰符
不能被声明为抽象的
类方法可以在不建立对象的情况下用类名直接调用,也可用类实例调用

将摄氏温度(centigrade)转换成华氏温度(fahrenheit)
转换公式为 fahrenheit = centigrade * 9 / 5 + 32
除了摄氏温度值及公式中需要的常量值,此功能不依赖于具体的类实例的属性值,因此可声明为类方法
转换方法centigradeToFahrenheit放在类Converter中

public class Converter { 
  public static int centigradeToFahrenheit(int cent)
  { return (cent * 9 / 5 + 32);   
  }  
 }
方法调用
Converter.centigradeToFahrenheit(40)

可变长参数
可变长参数使用省略号表示,其实质是数组
例如,“String … s”表示“String[] s”
对于可变长参数的方法,传递给可变长参数的实际参数可以是多个对象,也可以是一个对象或者是没有对象


是一组类的集合
一个包可以包含若干个类文件,还可包含若干个包

  • 包的作用
    将相关的源代码文件组织在一起
    类名的空间管理,利用包来划分名字空间,便可以避免类名冲突
    提供包一级的封装及存取权限
  • 包的命名
    每个包的名称必须是“独一无二”的
    Java中包名使用小写字母表示
  • 命名方式建议
    将机构的Internet域名反序,作为包名的前导
    若包名中有任何不可用于标识符的字符,用下划线替代
    若包名中的任何部分与关键字冲突,后缀下划线
    若包名中的任何部分以数字或其他不能用作标识符起始的字符开头,前缀下划线
  • 编译单元与类空间
    一个Java源代码文件称为一个编译单元,由三部分组成
    所属包的声明(省略,则属于默认包)
    Import (引入)包的声明,用于导入外部的类
  • 类和接口的声明
    一个编译单元中只能有一个public类,该类名与文件名相同,编译单元中的其他类往往是public类的辅助类,经过编译,每个类都会产一个class文件
    利用包来划分名字空间,便可以避免类名冲突

类的访问控制

类的访问控制
类的访问控制只有public(公共类)及无修饰符(缺省类)两种
不同包中的类若无修饰,则不能相互访问

类成员的访问控制

  • 公有(public)
    可以被其他任何对象访问(前提是对类成员所在的类有访问权限)
  • 保护(protected)
    只可被同一类及其子类的实例对象访问
  • 私有(private)
    只能被这个类本身访问,在类外不可见
  • 默认(default)
    仅允许同一个包内的访问;又被称为“包(package)访问权限”
public class Circle { 
        static double PI = 3.14159265; 
        private int radius;     
  		   public double circumference() { 
            return 2 * PI * radius; 
        } 
  }
public class CircumferenceTester { 
      public static void main(String args[]) { 
           Circle c1 = new Circle(); 
           c1.radius = 50; 
           Circle c2 = new Circle(); 
           c2.radius = 10;         
	       double circum1 = c1.circumference(); 
           double circum2 = c2.circumference(); 
           System.out.println("Circle 1 has circumference " + circum1); 
           System.out.println("Circle 2 has circumference " + circum2); 
     } 
 }

对象初始化和回收

  • 对象初始化
    系统在生成对象时,会为对象分配内存空间,并自动调用构造方法对实例变量进行初始化
  • 对象回收
    对象不再使用时,系统会调用垃圾回收程序将其占用的内存回收
  • 构造方法
    一种和类同名的特殊方法
    用来初始化对象
    Java中的每个类都有构造方法,用来初始化该类的一个新的对象
    没有定义构造方法的类,系统自动提供默认的构造方法

构造方法的特点
方法名与类名相同
没有返回类型,修饰符void也不能有
通常被声明为公有的(public)
可以有任意多个参数
主要作用是完成对象的初始化工作
不能在程序中显式的调用
在生成一个对象时,系统会自动调用该类的构造方法为新生成的对象初始化

系统提供的默认构造方法
如果在类的声明中没有声明构造方法,则Java编译器会提供一个默认的构造方法
默认的构造方法没有参数,其方法体为空
使用默认的构造方法初始化对象时,如果在类声明中没有给实例变量赋初值,则对象的属性值为零或空

声明一个银行帐号类及测试代码
public class BankAccount{
	String  ownerName;
    int     accountNumber;
    float       balance;
}
public class BankTester{
     public static void main(String args[]){		
       BankAccount myAccount = new BankAccount(); 	
       System.out.println("ownerName=" + myAccount.ownerName);
       System.out.println("accountNumber=" + myAccount.accountNumber);
	   System.out.println("balance=" + myAccount.balance);
	}
}
运行结果
ownerName=null
accountNumber=0
balance=0.0

自定义构造方法

自定义构造方法与方法重载
可在生成对象时给构造方法传送初始值,使用希望的值给对象初始化
构造方法可以被重载,构造方法的重载和方法的重载一致
一个类中有两个及以上同名的方法,但参数表不同,这种情况就被称为方法重载
在方法调用时,Java可以通过参数列表的不同来辨别应调用哪一个方法

自定义无参的构造方法
无参的构造方法对其子类的声明很重要。如果在一个类中不存在无参的构造方法,则要求其子类声明时必须声明构造方法,否则在子类对象的初始化时会出错
在声明构造方法时,好的声明习惯是
不声明构造方法
如果声明,至少声明一个无参构造方法

构建一个Bush类,有两个有参数的构造方法
class Bush {
Bush(int i) {}
Bush(double d) {}
}
如果写:new Bush();
编译器将要告诉你找不到对应的构造方法
说明
用户在进行类声明时,如果没有声明任何构造方法,系统会赋给此类一个默认(无参)的构造方法。
但是,只要用户声明了构造方法,即使没有声明无参的构造方法,系统也不再赋默认的构造方法

创建一个拥有两个构造方法的Tree类,一个有参,一个无参。

import java.util.*;
class Tree 
{ 
  int height;
  Tree()   { prt("Planting a seedling"); height = 0;  }
  Tree(int i) { 
	  prt("Creating new Tree that is "+ i + " feet tall");
      height = i;
  }
  void info()   { prt("Tree is " + height + " feet tall");  }
  void info(String s)  { prt(s + ": Tree is " + height + " feet tall"); }
  static void prt(String s)  { System.out.println(s);   }
} 

public class Overloading 
{
  public static void main(String[] args) 
  {
    for(int i = 0; i < 5; i++) 
    {
      Tree t = new Tree(i);
      t.info();
      t.info("overloaded method");
    }
    new Tree();
  }
} 
测试结果
Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling

this关键字的使用
可以使用this关键字在一个构造方法中调用另外的构造方法
代码更简洁,维护起来也更容易
通常用参数个数比较少的构造方法调用参数个数最多的构造方法

使用this关键字,修改BankAccout类中无参数和二参数的构造方法

public BankAccount() { 
        this("", 999999, 0.0f); 
} 
public BankAccount(String initName, int initAccountNumber) { 
	  this(initName, initAccountNumber, 0.0f);    
}
public BankAccount(String initName, int initAccountNumber,
 float initBalance) { 
         ownerName = initName; 
         accountNumber = initAccountNumber; 
         balance = initBalance; 
 }

内存回收技术
当一个对象在程序中不再被使用时,就成为一个无用对象
当前的代码段不属于对象的作用域
把对象的引用赋值为空
Java运行时系统通过垃圾收集器周期性地释放无用对象所使用的内存
Java运行时系统会在对对象进行自动垃圾回收前,自动调用对象的finalize()方法

垃圾收集器
自动扫描对象的动态内存区,对不再使用的对象做上标记以进行垃圾回收
作为一个线程运行
通常在系统空闲时异步地执行
当系统的内存用尽或程序中调用System.gc()要求进行垃圾收集时,与系统同步运行

举例

toString()方法
将对象的内容转换为字符串
Java的所有类都有一个默认的toString()方法,其方法体如下:
getClass().getName() + ‘@’ +
Integer.toHexString(hashCode())
下面的两行代码等价
System.out.println(anAccount);
System.out.println(anAccount.toString());
如果需要特殊的转换功能,则需要自己重写toString()方法

toString()方法的几点说明
必须被声明为public
返回类型为String
方法的名称必须为toString,且没有参数
在方法体中不要使用输出方法System.out.println()

为BankAccount类添加自己的toString()方法
public String toString() { 
   return("Account #" + accountNumber + " with balance $" + balance); 
}

DecimalFormat类
在java.text包中
在toString()方法中使用DecimalFormat类的实例方法format对数据进行格式化
修改后的toString()方法如下

public String toString() { 
    return("Account #" + accountNumber + " with balance " +
     new java.text.DecimalFormat("$0.00").format(balance)); 
}

使用类方法生成特殊的实例
声明类方法返回特殊的BankAccount实例,作用是生成特定的几种账号样例

example1()方法
public static BankAccount example1() { 
        BankAccount ba = new BankAccount(); 
       	ba.setOwnerName("LiHong"); 
       	ba.setAccountNumber(554000); 
      	ba.deposit(1000); 
       	return ba; 
}  

example2()
public static BankAccount example2() { 
       BankAccount ba = new BankAccount(); 
       ba.setOwnerName("ZhaoWei"); 
       ba.setAccountNumber(554001); 
       ba.deposit(1000); 
       ba.deposit(2000); 
       return ba; 
 } 

emptyAccountExample()
public static BankAccount emptyAccountExample()
{ 
    BankAccount ba = new BankAccount(); 
    ba.setOwnerName("HeLi"); 
    ba.setAccountNumber(554002); 
    return ba; 
}

声明类变量
修改BankAccount类
增加类变量LAST_ACCOUNT_NUMBER,初始值为0,当生成一个新的BankAccount对象时,其帐号为LAST_ACCOUNT_NUMBER的值累加1
自动产生对象的accountNumber,且不允许直接修改其值
修改构造方法,取消帐号参数
取消setAccountNumber方法
取消setBalance方法

UML简介

UML(统一建模语言)
面向对象建模的标准语言
图形化(即可视化)的建模语言

  • 组成
    视图(view)、
    图(diagram)、
    模型元素(model element)
    通用机制(general mechanism)

类图
由类和它们之间的关系组成
定义系统中的类,表示类的内部结构(类的属性和操作)
表示类与类之间的关系(如继承关系)
是构建其他图的基础

由三部分组成

  • 类的名字
  • 类的属性
  • 类的操作(方法)

在实际使用时可以

  • 省掉操作部分
  • 省略属性及操作部分

关联
描述了系统中对象或实例间的离散连接

  • 包括
    普通关联
    聚集
    组成

泛化(继承)
就是通常所说的继承关系
是通用元素和具体元素之间的一种分类关系。具体元素完全拥有通用元素的信息,并且还可以附加一些其他信息
用从子类指向父类(也称为基类或超类)的箭头表示,指向父类的是一个空三角形

继承、抽象、组合

继承

类的继承
一种由已有的类创建新类的机制,是面向对象程序设计的基石之一
通过继承,可以根据已有类来定义新类,新类拥有已有类的所有功能
Java只支持类的单继承,每个子类只能有一个直接父类
父类是所有子类的公共属性及方法的集合,子类则是父类的特殊化
继承机制可以提高程序的抽象程度,提高代码的可重用性

  • 基类(base class)也称超类(superclass)
    是被直接或间接继承的类

  • 派生类(derived-class)也称子类 (subclass)
    继承其他类而得到的类
    继承所有祖先的状态和行为
    派生类可以增加变量和方法
    派生类也可以覆盖(override)继承的方法

派生类产生的对象,从外部来看,它应该包括

  • 与基类相同的接口
  • 可以具有更多的方法和数据成员
  • 其内包含着一个基类类型的子对象

继承的语法
class childClass extends parentClass
{
//类体
}

子类不能直接访问从父类中继承的私有属性及方法,但可使用公有(及保护)方法进行访问

public class B { 
   public int a = 10; 
   private int b = 20; 
   protected int c = 30; 
   public int getB()  { return b; } 
} 
public class A extends B { 
   public int d; 
   public void tryVariables() { 
      System.out.println(a);             //允许 
      System.out.println(b);             //不允许
      System.out.println(getB());        //允许 
      System.out.println(c);             //允许 
    } 
}

隐藏和覆盖
子类对从父类继承来的属性变量及方法可以重新定义

属性的隐藏
子类中声明了与父类中相同的成员变量名,则从父类继承的变量将被隐藏
子类拥有了两个相同名字的变量,一个继承自父类,另一个由自己声明
当子类执行继承自父类的操作时,处理的是继承自父类的变量,而当子类执行它自己声明的方法时,所操作的就是它自己声明的变量

如何访问被隐藏的父类属性
调用从父类继承的方法,则操作的是从父类继承的属性 使用super.属性

class A1
{   int x = 2;    
    public void setx(int i) 
    {   x = i;    }
    void printa()
    {System.out.println(x);}  
}
class B1 extends A1
{   int x=100;
    void printb() {
        super.x = super.x +10 ;
        System.out.println("super.x=" +super.x + "  x= " + x);
    }  
}
public class Exam4_4Test 
{  public static void main(String[] args)
    {   A1 a1 = new A1();  
         a1.setx(4);   
         a1.printa();   //4

         B1 b1 = new B1();     
         b1.printb();  //super.x=12 x=100   
         b1.printa();  //12
         //这里注意:在子类中修改父类属性,之后再调用父类属性的方法时,操作的是修改后的父类属性
  
         b1.setx(6);    // 将继承x值设置为6
         b1.printb();   //super.x=16 x=100   
         b1.printa();   //16
         a1.printa();   //4
         //这里是a1对象,其值为之前修改过的4
    }  
}

运行结果:
4
super.x= 12  x= 100
12
super.x= 16  x= 100
16
4

子类不能继承父类中的静态属性,但可以对父类中的静态属性进行操作。
如在上面的例子中,将“int x = 2;”改为“static int x = 2;”,再编译及运行程序,会得到下面的结果

4
super.x= 14 x= 100
14
super.x= 16 x= 100
16
16

方法覆盖
如果子类不需使用从父类继承来的方法的功能,则可以声明自己的同名方法,称为方法覆盖
覆盖方法的返回类型,方法名称,参数的个数及类型必须和被覆盖的方法一模一样
只需在方法名前面使用不同的类名或不同类的对象名即可区分覆盖方法和被覆盖方法
覆盖方法的访问权限可以比被覆盖的宽松,但是不能更为严格

应用场合
子类中实现与父类相同的功能,但采用不同的算法或公式
在名字相同的方法中,要做比父类更多的事情
在子类中需要取消从父类继承的方法

  • 必须覆盖的方法
    派生类必须覆盖基类中的抽象的方法,否则派生类自身也成为抽象类.
  • 不能覆盖的方法
    基类中声明为final的终结方法
    基类中声明为static 的静态方法
  • 调用被覆盖的方法
    super.overriddenMethodName();

有继承时的构造方法遵循以下的原则

  1. 子类不能从父类继承构造方法
  2. 好的程序设计方法是在子类的构造方法中调用某一个父类构造方法,调用语句必须出现在子类构造方法的第一行,可使用super关键字
  3. 如子类构造方法的声明中没有明确调用父类构造方法,则系统在执行子类的构造方法时会自动调用父类的默认构造方法(即无参的构造方法)
public class Person {
    protected String name, phoneNumber, address;
    public Person() {
        this("", "", "");
    }
    public Person(String aName, String aPhoneNumber, String anAddress) {
        name = aName;
        phoneNumber = aPhoneNumber;
        address = anAddress;
    }
}
public class Employee extends Person {
    protected int employeeNumber;
    protected String workPhoneNumber;
    public Employee() {
        //此处隐含调用构造方法 Person()
        this(0, "");
    }
    public Employee(int aNumber, String aPhoneNumber) {
        //此处隐含调用构造方法 Person()
        employeeNumber = aNumber;
        workPhoneNumber = aPhoneNumber;
    }
}
public class Professor extends Employee {
    protected String research;
    public Professor() {
        super();
        research = "";
    }
    public Professor(int aNumber, String aPhoneNumber, String aResearch) {
        super(aNumber, aPhoneNumber);
        research = aResearch;
    }
}

object类

Object类
Java程序中所有类的直接或间接父类,类库中所有类的父类,处在类层次最高点
包含了所有Java类的公共属性,其构造方法是

Object( )

Object类定义了所有对象必须具有的状态和行为,较主要的方法如下

public final Class getClass()  
获取当前对象所属的类信息,返回Class对象
public String toString() 
返回当前对象本身的有关信息,按字符串对象返回
public boolean equals(Object obj)  
比较两个对象是否是同一对象,是则返回true
protected Object clone( )  
生成当前对象的一个拷贝,并返回这个复制对象
Public int hashCode()   
返回该对象的哈希代码值
protected void finalize() throws Throwable 
定义回收当前对象时所需完成的资源释放工作
你的类不可以覆盖终结方法,即有final修饰的方法

相等和同一的概念
两个对象具有相同的类型,及相同的属性值,则称二者相等(equal)
如果两个引用变量指向的是同一个对象,则称这两个变量(对象)同一(identical)
两个对象同一,则肯定相等
两个对象相等,不一定同一
比较运算符“==” 判断的是这两个对象是否同一

public class Tester {
    public static void main(String args[]) {
        BankAccount a = new BankAccount("Bob", 123456, 100.00f);
        BankAccount b = new BankAccount("Bob", 123456, 100.00f);
        if (a == b) {
            System.out.println("YES");
        } else {
            System.out.println("NO");
        }
    }
}
此程序运行的结果为“NO”,原因是使用等号“==”判断的是两个对象是否同一,显然a和b是两个对象

public class Tester1 {
    public static void main(String args[]) {
        BankAccount a = new BankAccount("Bob", 123456, 100.00f, Grade.General);
        BankAccount b = a;
        if (a == b) {
            System.out.println("YES");
        } else {
            System.out.println("NO");
        }
    }
}
将a所指对象的引用赋给b,因此a与b指向的是同一个对象,a与b同一。输出结果为“YES”

主要方法

equals 方法
由于Object是类层次结构中的树根节点,因此所有其他类都继承了equals()方法
Object类中的 equals() 方法的定义如下,可见,也是判断两个对象是否同一

public boolean equals(Object x) { 
    return this == x; 
}
public class EqualsTest{
	public static void main(String args[]){
	BankAccount a = new BankAccount("Bob", 123456, 100.00f); 
	BankAccount b = new BankAccount("Bob", 123456, 100.00f); 
        if (a.equals(b))
           System.out.println("YES"); 
        else 
           System.out.println("NO");
    }
}
由于不是同一对象,运行结果仍然是“NO”

equlas方法的重写
要判断两个对象各个属性域的值是否相同,则不能使用从Object类继承来的equals方法,而需要在类声明中对equals方法进行重写
String类中已经重写了Object类的equals方法,可以判别两个字符串是否内容相同

在BankAccount类中增加equals方法,由于是对Object类中的equals方法进行重写,
因此方法定义头必须与Object类中的equals方法完全相同

public boolean equals(Object x) { 
    if (this.getClass() != x.getClass()) 
	return false;      
    BankAccount b = (BankAccount) x;     
    return 
       ((this.getOwnerName().equals(b.getOwnerName())) 
       &&(this.getAccountNumber() == b.getAccountNumber())
       &&(this.getBalance() == b.getBalance())); 
}
equals方法的应用举例
public class Apple { 
  private String color; 
  private boolean ripe;  
  public Apple(String aColor, boolean isRipe) { 
     color = aColor; 
     ripe = isRipe; 
  }      
  public void setColor(String aColor) { color = aColor;  }      
  public void setRipe(boolean isRipe) { ripe = isRipe; }     
  public String getColor() { return color; }     
  public boolean getRipe() { return ripe; }
  public String toString() { 
     if (ripe)  return("A ripe " + color + " apple"); 
     else  return("A not so ripe " + color + " apple"); 
  } 

  public boolean equals(Object obj) { 
     if (obj instanceof Apple) { 
        Apple a = (Apple) obj; 
        return (color.equals(a.getColor()) && (ripe == a.getRipe())); 
     } 
     return false; 
  }
  //instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例
}


public class AppleTester { 
   public static void main(String args[]) { 
     Apple a = new Apple("red", true); 
     Apple b = new Apple("red", true); 
     System.out.println(a + " is equal to " + b + ": " + a.equals(b)); 
     System.out.println("a is identical to b: " + (a == b));
     Apple c = a; 
     System.out.println(a + " is equal to " + c + ": " + a.equals(c)); 
     System.out.println("a is identical to c: " + (a == c)); 
     }
}

运行结果
A ripe red apple is equal to A ripe red apple: true
a is identical to b: false
A ripe red apple is equal to A ripe red apple: true
a is identical to c: true

clone方法
根据已存在的对象构造一个新的对象
在根类Object 中被定义为protected,所以需要覆盖为public
实现Cloneable 接口,赋予一个对象被克隆的能力(cloneability)
class MyObject implements Cloneable
{ //…
}

finalize方法
在对象被垃圾回收器回收之前,系统自动调用对象的finalize方法
如果要覆盖finalize方法,覆盖方法的最后必须调用super.finalize

getClass方法
final 方法,返回一个Class对象,用来代表对象隶属的类
通过Class 对象,你可以查询Class对象的各种信息:比如它的名字,它的基类,它所实现接口的名字等。
void PrintClassName(Object obj) {
System.out.println("The Object’s class is " +
obj.getClass().getName());
}

终结类与终结方法

被final修饰符修饰的类和方法
终结类不能被继承
终结方法不能被当前类的子类重写
终结类

  • 终结类的特点
    不能有派生类

  • 终结类存在的理由
    安全: 黑客用来搅乱系统的一个手法是建立一个类的派生类,然后用他们的类代替原来的类
    设计: 你认为你的类是最好的或从概念上你的类不应该有任何派生类
    终结方法

  • 终结方法的特点
    不能被派生类覆盖

  • 终结方法存在的理由
    对于一些比较重要且不希望子类进行更改的方法,可以声明为终结方法。可防止子类对父类关键方法的错误重写,增加了代码的安全性和正确性
    提高运行效率。通常,当java运行环境(如java解释器)运行方法时,它将首先在当前类中查找该方法,接下来在其超类中查找,并一直沿类层次向上查找,直到找到该方法为止

final 方法举例

class Parent
{
   public Parent() {   } //构造方法
   final int getPI() { return Math.PI; } //终结方法
}
说明
getPI()是用final修饰符声明的终结方法,不能在子类中对该方法进行重载,因而如下声明是错的
Class Child extends Parent
{
   public Child() {   }  //构造方法
   int getPI() { return 3.14; } //重写父类中的终结方法,不允许
}

抽象类

代表一个抽象概念的类
没有具体实例对象的类,不能使用new方法进行实例化
类前需加修饰符abstract
可包含常规类能够包含的任何东西,例如构造方法,非抽象方法
也可包含抽象方法,这种方法只有方法的声明,而没有方法的实现

  • 存在意义
    抽象类是类层次中较高层次的概括,抽象类的作用是让其他类来继承它的抽象化的特征
    抽象类中可以包括被它的所有子类共享的公共行为
    抽象类可以包括被它的所有子类共享的公共属性
    在程序中不能用抽象类作为模板来创建对象
    在用户生成实例时强迫用户生成更具体的实例,保证代码的安全性

抽象类声明的语法形式为

abstract class Number {
    . . .
}

如果写:
new Number();
编译器将显示错误

抽象方法
声明的语法形式为
public abstract (…);
仅有方法头,而没有方法体和操作实现
具体实现由当前类的不同子类在它们各自的类声明中完成
抽象类可以包含抽象方法

需注意的问题
一个抽象类的子类如果不是抽象类,则它必须为父类中的所有抽象方法书写方法体,即重写父类中的所有抽象方法
只有抽象类才能具有抽象方法,即如果一个类中含有抽象方法,则必须将这个类声明为抽象类
除了抽象方法,抽象类中还可以包括非抽象方法

抽象方法的优点
隐藏具体的细节信息,所有的子类使用的都是相同的方法头,其中包含了调用该方法时需要了解的全部信息
强迫子类完成指定的行为,规定其子类需要用到的“标准”行为
举例:

画图:
abstract class GraphicObject {
    int x, y;
    void moveTo(int newX, int newY) { . . . }
    abstract void draw();
}

class Circle extends GraphicObject {
    void draw() {  . . .  }
}
class Rectangle extends GraphicObject {
    void draw() {  . . .  }
}

贷款(Loan)分为许多种类,如租借(Lease)、抵押(Mortgage)、房屋贷款(HouseLoan)、汽车贷款(CarLoan)等
将Loan声明为抽象类,并指定所有的子类对象都应具有的行为,如计算月还款值(calculateMonthlyPayment),还款(makePayment),取得客户信息(getClientInfo),其中前两个因贷款种类不同计算方法也不同,可声明为抽象方法,Loan的所有子类都必须对这两个抽象方法进行重写

public abstract class Loan { 
    public abstract float calculateMonthlyPayment(); 
    public abstract void makePayment(float amount); 
    public Client getClientInfo() {  } 
}

泛型

泛型可以使用在类、接口以及方法的创建中,分别称为泛型类、泛型方法和泛型接口
泛型类:在类名后面加上“”
泛型方法:在方法名前加上“”

使用泛型的优点:使Java语言变得更加简单、安全。

通配符泛型
为了了解通配符泛型的作用,先尝试一下下面的代码是否合法:

class ShowType {
    public void showType(GeneralType<Object> o) {
        System.out.println(o.getObj().getClass().getName());
    }
}       
public class Test {   
    public static void main(String args[]){
        ShowType st = new ShowType();
        GeneralType <Integer> i = new GeneralType <Integer> (2);
        st.showType(i); //这行语句是否合法?
    }
}

上面的代码并不能通过编译,这是因为,不能将General类型的变量当做参数传递给General
事实上,这里能传递的类型只能是General。因此,在使用泛型时应该注意和继承类的区别。
使用通配符泛型 可以让showType函数发挥应有的作用
“?”代表任意一种类型,它被称为通配符

class GeneralType <Type> {
    Type object;
    public GeneralType(Type object) {
        this.object = object;
    }
    public Type getObj() {
        return object;
    }
}
class ShowType {
    public void showType(GeneralType<?> o) {
        System.out.println(o.getObj().getClass().getName());
    }
}

public class Test {
    public static void main(String args[]){
        ShowType st = new ShowType();
        GeneralType<Integer> i = new GeneralType<Integer> (2);
        GeneralType<String> s = new GeneralType<String> ("hello");
        st.showType(i);
        st.showType(s);
    }
} 
程序的运行结果如下:
java.lang.Integer
java.lang.String

有限制的泛型
有时候需要将泛型中参数代表的类型做限制,此时就可以使用有限制的泛型。有限制的泛型是指,在参数“Type”后面使用“extends”关键字并加上类名或接口名,表明参数所代表的类型必须是改类的关键字或者实现了该接口。注意,对于实现了某接口的有限制泛型,也是使用extends关键字,而不是implements关键字

有限制的泛型
class GeneralType <Type extends Number> {
    Type object;
    public GeneralType(Type object) {
        this.object = object;
    }
    public Type getObj() {
        return object;
    }
}

public class Test {
    public static void main(String args[]){
        GeneralType<Integer> i = new GeneralType<Integer> (2);
        //GeneralType<String> s = new GeneralType<String> ("hello");//非法,
        //T只能是Number或Number的子类
    }
}

组合

类的组合
面向对象编程的一个重要思想就是用软件对象来模仿现实世界的对象
现实世界中,大多数对象由更小的对象组成
与现实世界的对象一样,软件中的对象也常常是由更小的对象组成
Java的类中可以有其他类的对象作为成员,这便是类的组合

组合的语法很简单,只要把已存在类的对象放到新类中即可
可以使用“has a”语句来描述这种关系
例如,考虑Kitchen类提供烹饪和冷藏食品的功能,很自然的说“my kitchen ‘has a’ cooker/refrigerator”。所以,可简单的把对象myCooker和myRefrigerator放在类Kitchen中。格式如下
class Cooker{ // 类的语句 }
class Refrigerator{ // 类的语句}
class Kitchen{
Cooker myCooker;
Refrigerator myRefrigerator;
}

组合与继承的比较
“包含”关系用组合来表达
如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择组合,我们需在新类里嵌入现有类的private对象
如果想让类用户直接访问新类的组合成分,需要将成员对象的属性变为public
“属于”关系用继承来表达
取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规用途的类,并根据特定需求对其进行定制

为了解决类名冲突,Java提供包来管理类名空间
Java利用包来组织相关的类,并控制访问权限
包是一种松散的类的集合,利用包来管理类,可实现类的共享与复用
同一包中的类在默认情况下可以互相访问,通常把需要在一起工作的类放在一个包里

数据包裹类
对应Java的每一个基本数据类型(primitive data type)都有一个数据包裹类
每个包裹类都只有一个类型为对应的基本数据类型的属性域

一些函数

生成数据类型包裹类对象的方法
从基本数据类型的变量或常量生成包裹类对象
double x = 1.2;
Double a = new Double(x);
Double b = new Double(-5.25);
从字符串生成包裹类对象
Double c = new Double("-2.34");
Integer i = new Integer(“1234”);
已知字符串,可使用valueOf方法将其转换成包裹类对象:
Integer.valueOf(“125”);
Double.valueOf(“5.15”);
自动装箱
Integer i =3; Double d = -5.25

得到基本数据类型数据的方法
每一个包裹类都提供相应的方法将包裹类对象转换回基本数据类型的数据
anIntegerObject.intValue() // 返回 int类
aCharacterObject.charValue() // 返回 char类型的数据
Integer、Float、Double、Long、Byte 及Short 类提供了特殊的方法能够将字符串类型的对象直接转换成对应的int、float、double、long、byte或short类型的数据
Integer.parseInt(“234”) // 返回int类型的数据
Float.parseFloat(“234.78”) // 返回float类型的数据
自动拆箱
Integer a = new Integer(3); int i = a;

int length( )
char charAt(int index)
int indexOf(String s)
String substring(int begin, int end)
public String[] split(String regex)
String concat(String s)
String replace(char oldChar, char newChar);
int compareTo(String s);
boolean equals(String s);
String trim( );
String toLowerCase()
String toUpperCase()

StringBuffer类
其对象是可以修改的字符串
字符的个数称为对象的长度(length)
分配的存储空间称为对象的容量(capacity)
与String类的对象相比,执行效率要低一些
该类的方法不能被用于String类的对象

int length ( )
int capacity( )
void ensureCapacity(int size)
void setLength(int len)
char charAt(int index)
void setCharAt(int index, char c)
void getChars(int start, int end, char [ ] charArray, int newStart)
StringBuffer reverse( )
StringBuffer insert(int index, Object ob)
StringBuffer append(Object ob)

已知一个字符串,返回将字符串中的非字母字符都删除后的字符串
public class StringEditor { 
    public static String removeNonLetters(String original) {    
        StringBuffer aBuffer = new StringBuffer(original.length()); 
        char aCharacter;        
        for (int i=0; i<original.length(); i++) { 
            aCharacter = original.charAt(i); 
            if (Character.isLetter(aCharacter)) 
                aBuffer.append(new Character(aCharacter)); 
    } 
    return new String(aBuffer); 
   } 
}

public class StringEditorTester { 
   public static void main(String args[]) { 
       String original = "Hello123, My Name is Mark,234I think you are my classmate?!!";            
       System.out.println(StringEditor.removeNonLetters(original)); 
    } 
 }
运行结果
HelloMyNameisMarkIthinkyouaremyclassmate

System类
访问系统资源
arraycopy() 复制一个数组
exit() 结束当前运行的程序
currentTimeMillis() 获得系统当前日期和时间等
访问标准输入输出流
System.in 标准输入,表示键盘
System.out 标准输出,表示显示器
Runtime类
可直接访问运行时资源
totalMemory() 返回系统内存总量
freeMemory() 返回内存的剩余空间
Class类
提供运行时信息,如名字、类型以及父类
Object类中的getClass方法返回当前对象所在的类,返回类型是Class
它的getName方法返回一个类的名称,返回值是String
它的getSuperclass方法可以获得当前对象的父类
ClassLoader类
提供把类装入运行时环境的方法

Class类应用举例。

public class ClassTest { 
    public static void main(String args[]) {   
        BankAccount anAccount = new BankAccount(); 
        Class aClass = anAccount.getClass();       
        System.out.println(aClass); 
        System.out.println(aClass.getName()); 
    } 
}
运行结果
class BankAccount
BankAccount

接口与多态

• 与抽象类一样都是定义多个类的共同属性
• 使抽象的概念更深入了一层,是一个“纯”抽象类,它只提供一种形式,并不提供实现
• 允许创建者规定方法的基本形式:方法名、参数列表以及返回类型,但不规定方法主体
• 也可以包含基本数据类型的数据成员,但它们都默认为static和final

接口的作用
• 是面向对象的一个重要机制
• 实现多继承,同时免除C++中的多继承那样的复杂性
• 建立类和类之间的“协议”
• 把类根据其实现的功能来分别代表,而不必顾虑它所在的类继承层次;这样可以最大限度地利用动态绑定,隐藏实现细节
• 实现不同类之间的常量共享

接口的作用及语法:

  • 与抽象类不同
    接口允许我们在看起来不相干的对象之间定义共同行为

在UML图中,实现接口用带有空三角形的虚线表示

• 声明格式为

[接口修饰符] interface 接口名称 [extends 父接口名]{//方法的原型声明或静态常量
}

• 接口的数据成员一定要赋初值,且此值将不能再更改,允许省略final关键字
• 接口中的方法必须是“抽象方法”,不能有方法体,允许省略

public及abstract关键字

//其中的方法都是抽象方法
public interface Insurable {
    public int getNumber();
    public int getCoverageAmount();
    public double calculatePremium();
    public Date getExpiryDate();
}

接口的实现
• 接口不能用new运算符直接产生对象,必须利用其特性设计新的类,再用新类来创建对象
• 利用接口设计类的过程,称为接口的实现,使用implements关键字
• 语法如下

public class 类名称 implements 接口名称 {
/* Bodies for the interface methods */
/* Own data and methods. */
}

• 必须实现接口中的所有方法
• 来自接口的方法必须声明成public

对象可以被转型为其所属类实现的接口类型
• getPolicyNumber、calculatePremium是Insurable接口中声明的方法
• getMileage是Car类新添加的方法,Insurable接口中没有声明此方法


Car jetta = new Car();
Insurable item = (Insurable)jetta; //对象转型为接口类型
item.getPolicyNumber();
item.calculatePremium();
item.getMileage(); // 接口中没有声明此方法,不可以
jetta.getMileage(); // 类中有此方法,可以
((Car)item).getMileage(); // 转型回原类,可调用此方法了

public class InterfaceTester {
    public static void main(String args[]){
        Rectangle rect=new Rectangle(5,6);
        System.out.println("Area of rect = " + rect.area());
        Circle cir=new Circle(2.0);
        System.out.println("Area of cir = " + cir.area());
    }
}
• 运行结果
Area of rect = 30.0
Area of cir = 12.56

声明接口类型的变量,并用它来访问对象

public class VariableTester {
public static void main(String []args)
{
    Shape2D var1,var2;
    var1=new Rectangle(5,6);
    System.out.println("Area of var1 = " + var1.area());
    var2=new Circle(2.0);
    System.out.println("Area of var2 = " + var2.area());
}
}
• 输出结果
Area of var1 = 30.0
Area of var2 = 12.56

多重继承
• Java的设计以简单实用为导向,不允许一个类有多个父类
• 但允许一个类可以实现多个接口,通过这种机制可实现多重继承
• 一个类实现多个接口的语法如下

[类修饰符] class 类名称 implements 接口1,接口2,{
… …
}

接口的扩展
• 接口可通过扩展的技术派生出新的接口
• 原来的接口称为基接口(base interface)或父接口(super interface)
• 派生出的接口称为派生接口(derived interface)或子接口(sub interface)
• 派生接口不仅可以保有父接口的成员,同时也可加入新成员以满足实际问题的需要
实现接口的类也必须实现此接口的父接口
• 接口扩展的语法
interface 子接口的名称 extends 父接口的名称1,父接口的名称2,…{
… …
}

塑性

塑性的概念

又称为类型转换
• 方式
• 隐式(自动)的类型转换
• 显式(强制)的类型转换

塑型的对象包括
基本数据类型
• 将值从一种形式转换成另一种形式
引用变量
• 将对象暂时当成更一般的对象来对待,并不改变其类型
• 只能被塑型为
• 任何一个父类类型
• 对象所属的类实现的一个接口
• 被塑型为父类或接口后,再被塑型回其本身所在的类

Manager对象
• 可以被塑型为Employee、Person、Object或Insurable,
• 不能被塑型为Customer、Company或Car

基本数据类型
• 相容类型之间存储容量低的自动向存储容量高的类型转换
引用变量
• 被塑型成更一般的类
Employee emp;
emp = new Manager(); //将Manager类型的对象直接赋给
//Employee类的引用变量,系统会
//自动将Manage对象塑型为Employee类
• 被塑型为对象所属类实现的接口类型
Car jetta = new Car();
Insurable item = jetta;

塑性的应用

塑型应用的场合包括
• 赋值转换。赋值号右边的表达式类型或对象转换为左边的类型
• 方法调用转换。实参的类型转换为形参的类型
• 算术表达式转换。算数混合运算时,不同类型的项转换为相同的类型再进行运算
• 字符串转换。字符串连接运算时,如果一个操作数为字符串,一个操作数为数值型,则会自动将数值型转换为字符串

如果在塑型前和塑型后的类中都提供了相同的方法,如果将此方法发送给塑型后的对象,那么系统将会调用哪一个类中的方法?

实例方法的查找
从对象创建时的类开始,沿类层次向上查找

Manager man = new Manager();
Employee emp1 = new Employee();
Employee emp2 = (Employee)man;
emp1.computePay(); // 调用Employee类中的computePay()方法
man.computePay(); // 调用Manager类中的computePay()方法
emp2.computePay(); // 调用Manager类中的computePay()方法

类方法的查找
总是在引用变量声明时所属的类中进行查找

Manager man = new Manager();
Employee emp1 = new Employee();
Employee emp2 = (Employee)man;
man.expenseAllowance(); //in Manager
emp1.expenseAllowance(); //in Employee
emp2.expenseAllowance(); //in Employee!!! 

多态

是指不同类型的对象可以响应相同的消息
• 从相同的基类派生出来的多个类型可被当作同一种类型对待,可对这些不同的类型进行同样的处理,
由于多态性,这些不同派生类对象响应同一方法时的行为是有所差别的
例如
• 所有的Object类的对象都响应toString()方法
• 所有的BankAccount类的对象都响应deposit()方法

多态的目的
• 所有的对象都可被塑型为相同的类型,响应相同的消息
• 使代码变得简单且容易理解
• 使程序具有很好的“扩展性”

绑定
• 指将一个方法调用同一个方法主体连接到一起
• 根据绑定时期的不同,可分为
早期绑定:程序运行之前执行绑定
晚期绑定:也叫作“动态绑定”或“运行期绑定,基于对象的类别,在程序运行时执行绑定

• 对动态绑定进行测试如下

public class BindingTester{
    public static void main(String[] args) {
        Shape[] s = new Shape[9];
        int n;
        for(int i = 0; i < s.length; i++) {
            n = (int)(Math.random() * 3);
            switch(n) {
                case 0: s[i] = new Circle(); break;
                case 1: s[i] = new Square(); break;
                case 2: s[i] = new Triangle();
                }
        }
        for(int i = 0; i < s.length; i++) s[i].draw();
    }
}

编译时无法知道s数组元素的具体类型,运行时才能确定类型,所以是动态绑定

所有类都在music包中
• Note类中定义了三个音符
• Instrument类中声明并实现了一个play方法
• Wind类继承了Instrument类,重载了play方法
• Music类中包含了main方法

class Note {
    private int value;
    private Note(int val) { value = val; }
    public static final Note
        MIDDLE_C = new Note(0),
        C_SHARP = new Note(1),
        B_FLAT = new Note(2);
} 
class Instrument {
    public void play(Note n) {
    System.out.println("Instrument.play()");
    }
    }

class Wind extends Instrument {
    public void play(Note n) {
    System.out.println("Wind.play()");
    }
}
public class Music {
    public static void tune(Instrument i) {
        i.play(Note.MIDDLE_C);
    }
    public static void main(String[] args) {
        Wind flute = new Wind();
        tune(flute);
    }
}
• 运行结果
Wind.play()

• 说明
• 运行中,Instrument类的对象实际是Wind类的,所以调用
了Wind类中的play方法

多态的应用
技术基础
• 向上塑型技术:一个父类的引用变量可以指向不同的子类对象
• 动态绑定技术:运行时根据父类引用变量所指对象的实际类型执行相应的子类方法,从而实现多态性

public abstract class Vehicle
{
    private String type;
    public Vehicle( ) { }
    public Vehicle(String s) { type = s; }
    public abstract void drivedByFemaleDriver();
    public abstract void drivedByMaleDriver();
}

public class Bus extends Vehicle {
    public Bus( ) { }
    public void drivedByFemaleDriver()
    { System.out.println("A female driver drives a bus."); }
    public void drivedByMaleDriver()
    { System.out.println("A male driver drives a bus."); }
}
public class Car extends Vehicle {
    public Car( ) { }
    public void drivedByFemaleDriver()
    { System.out.println("A Female driver drives a car."); }
    public void drivedByMaleDriver()
    { System.out.println("A Male driver drives a car."); }
}

public abstract class Driver {
    public Driver() { }
    public abstract void drives(Vehicle v );
}
public class FemaleDriver extends Driver{
    public FemaleDriver( ) { }
    public void drives(Vehicle v){ v.drivedByFemaleDriver(); }
}
public class MaleDriver extends Driver{
    public MaleDriver( ) { }
    public void drives(Vehicle v){ v.drivedByMaleDriver(); }
}

public class DriverTest {
    static public void main(String [ ] args) {
        Driver a = new FemaleDriver( );
        Driver b = new MaleDriver( );
        Vehicle x = new Car( );
        Vehicle y = new Bus( );
        a.drives(x);
        b.drives(y);
    }
}
结果:
A Female driver drives a car.
A male driver drives a bus.

• 这种技术称为二次分发(“double dispatching”),即对输出消息的请求被分发两次
• 首先根据驾驶员的类型被发送给一个类
• 之后根据交通工具的类型被发送给另一个类

构造方法与多态

• 构造方法与其他方法是有区别的
• 构造方法并不具有多态性,但仍然非常有必要理解构造方法如何在复杂的分级结构中随同多态性一同使用的情况

构造方法的调用顺序
• 调用基类的构造方法。这个步骤会不断重复下去,首先被初始化的是分级结构的根部,然后是下一个派生类,等等。直到抵达最深一层的派生类
• 按声明顺序调用成员初始化模块
• 调用派生构造方法

当我们在构造派生类的时候,必须能假定基类的所有成员都是有效的。在构造方法内部,必须保证使用的所有成员都已初始化。因此唯一的办法就是首先调用基类构造方法,然后在进入派生类构造方法之前,初始化所有能够访问的成员

构造方法中的多态方法:
• 在构造方法内调用准备构造的那个对象的动态绑定方法
• 会调用位于派生类里的一个方法
• 被调用方法要操纵的成员可能尚未得到正确的初始化
• 可能造成一些难于发现的程序错误

abstract class Glyph {
    abstract void draw();
    Glyph() {
    System.out.println("Glyph() before draw()");
    draw();
    System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
    void draw() { 
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}
public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}

Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5

内部类
• 在另一个类或方法的定义中定义的类
• 可访问其外部类中的所有数据成员和方法成员
• 可对逻辑上相互联系的类进行分组
• 对于同一个包中的其他类来说,能够隐藏
• 可非常方便地编写事件驱动程序
• 声明方式
• 命名的内部类:可在类的内部多次使用
• 匿名内部类:可在new关键字后声明内部类,并立即创建一个对象
• 假设外层类名为Myclass,则该类的内部类名为
• Myclass$c1.class (c1为命名的内部类名)
• Myclass$1.class (表示类中声明的第一个匿名内部类)

在方法内定义一个内部类
• 为实现某个接口,产生并返回一个引用
• 为解决一个复杂问题,需要建立一个类,而又不想它为外界所用

对象群体的组织

对象数组

数组
在Java提供的存储及随机访问对象序列的各种方法中,数组是效率最高的一种
类型检查
边界检查

优点
数组知道其元素的类型
编译时的类型检查
大小已知

代价
数组对象的大小是固定的,在生存期内大小不可变

对象数组
数组元素是类的对象
所有元素具有相同的类型
每个元素都是一个对象的引用

数组的优点
是Java提供的随机访问对象序列的最有效方法
是一个简单的线性序列,访问元素的速度较快
数组的缺点
大小自创建以后就固定了,在其整个生存期内其大小不可改变
数组元素只能是同一类型

集合

可动态改变其大小
可在序列中存储不同类型的数据

把具有相同性质的一类东西,汇聚成一个整体
在Java2中有很多与集合有关的接口及类,它们被组织在以Collection及Map接口为根的层次结构中,称为集合框架
在Java2之前,在Java 1.0/1.1中,没有完整的集合框架。只有一些简单的可以自扩展的容器类:Vector,Hashtable

集合框架(Java Collections Framework)
为表示和操作集合而规定的一种统一的标准的体系结构
提供了一些现成的数据结构可供使用,程序员可以利用集合框架快速编写代码,并获得优良性能

包含三大块内容
对外的接口:表示集合的抽象数据类型,使集合的操作与表示分开
接口的实现:指实现集合接口的Java类,是可重用的数据结构
对集合运算的算法:是指执行运算的方法,例如在集合上进行查找和排序

接口与实现

Iterator

提供了遍历容器中元素的方法。只有容器本身清楚容器里元素的组织方式,因此迭代器只能通过容器本身得到
每个容器都会通过内部类的形式实现自己的迭代器

ListIterator

允许我们向前、向后两个方向遍历 List;
在遍历时修改 List 的元素;
遍历时获取迭代器当前游标所在位置。

Collection

AbstractCollection
List

除了继承 Collection 的一些方法,还提供以下操作 :
位置相关:List 和 数组一样,都是从 0 开始,我们可以根据元素在 list 中的位置进行操作,比如说 get, set, add, addAll, remove;
搜索:从 list 中查找某个对象的位置,比如 indexOf, lastIndexOf;
迭代:使用 Iterator 的拓展版迭代器 ListIterator 进行迭代操作;
范围性操作:使用 subList 方法对 list 进行任意范围的操作。

实现类是 ArrayList,LinkedList 和 Vector
以 ArrayList 作为默认选择
当插入、删除频繁时,使用 LinkedList,
Vector 总是比 ArrayList 慢,

List vs. Array
相同:
都可以表示一组同类型的对象
都使用下标进行索引
不同:
数组可以存任何类型元素
List 不可以存基本数据类型,必须要包装
数组容量固定不可改变;List 容量可动态增长
数组效率高; List 由于要维护额外内容,效率相对低一些

AbstractList

实现了 List 的一些位置相关操作(比如 get,set,add,remove)
还创建了内部的迭代器 Itr, ListItr

ArrayList

容量不固定
有序的(元素输出顺序与输入顺序一致)
元素可以为 null
效率高
size(), isEmpty(), get(), set() iterator(), ListIterator() 方法的时间复杂度都是 O(1)
add() 添加操作的时间复杂度平均为 O(n)其他所有操作的时间复杂度几乎都是 O(n)
占用空间更小
对比 LinkedList,不用占用额外空间维护链表结构

AbstractSequentialList

AbstractSequentialList 只支持按次序访问,而不像 AbstractList 那样支持随机访问

LinkedList

双向链表实现
元素时有序的,输出顺序与输入顺序一致
允许元素为 null
所有指定位置的操作都是从头开始遍历进行的
和 ArrayList 一样,不是同步容器

ArrayList VS. LinkedList
ArrayList
基于数组,在数组中搜索和读取数据是很快的。因此 ArrayList 获取数据的时间复杂度是O(1);
但是添加、删除时该元素后面的所有元素都要移动,所以添加/删除数据效率不高;
另外其实还是有容量的,每次达到阈值需要扩容,这个操作比较影响效率。

LinkedList
基于双端链表,添加/删除元素只会影响周围的两个节点,开销很低;
只能顺序遍历,无法按照索引获得元素,因此查询效率不高;
没有固定容量,不需要扩容;
需要更多的内存,LinkedList 每个节点中需要多存储前后节点的信息,占用空间更多些。

Vector

没有线程安全的需求,一般推荐使用 ArrayList,而不是 Vector,因为每次都要获取锁

Vector vs. ArrayList
相同:
都是基于数组
都支持随机访问
默认容量都是 10
都有扩容机制
不同:
Vector 出生的比较早,JDK 1.0 就出生了
ArrayList JDK 1.2 才出来Vector 比 ArrayList 多一种迭代器
EnumerationVector 是线程安全的,ArrayList 不是
Vector 默认扩容 2 倍,ArrayList 是 1.5

stack

push 入栈
pop 栈顶元素出栈,并返回
peek 获取栈顶元素,并不删除

queue
deque
ArrayDeque

Map

Hashtable
古老,线程安全
HashMap
速度很快,但没有顺序
LinkedHashMap
结合 HashMap 和 TreeMap 的有点,有序的同时效率也不错,仅比 HashMap 慢一点
TreeMap
有序的,效率比 HashMap 低

Collection接口
声明时可以使用一个参数类型,即Collection
声明了一组操作成批对象的抽象方法:查询方法、修改方法
查询方法
int size() – 返回集合对象中包含的元素个数
boolean isEmpty() – 判断集合对象中是否还包含元素,如果没有任何元素,则返回true
boolean contains(Object obj) – 判断对象是否在集合中
boolean containsAll(Collection c) – 判断方法的接收者对象是否包含集合中的所有元素
修改方法包括
boolean add(Object obj) – 向集合中增加对象
boolean addAll(Collection<?> c) – 将参数集合中的所有元素增加到接收者集合中
boolean remove(Object obj) –从集合中删除对象
boolean removeAll(Collection c) -将参数集合中的所有元素从接收者集合中删除
boolean retainAll(Collection c) – 在接收者集合中保留参数集合中的所有元素,其它元素都删除
void clear() – 删除集合中的所有元素

Set接口
扩展了Collection
禁止重复的元素,是数学中“集合”的抽象
对equals和hashCode操作有了更强的约定,如果两个Set对象包含同样的元素,二者便是相等的
实现它的两个主要类是哈希集合(HashSet)及树集合(TreeSet)

SortedSet接口
一种特殊的Set
其中的元素是升序排列的,还增加了与次序相关的操作
通常用于存放词汇表这样的内容

List接口
扩展了Collection
可包含重复元素
元素是有顺序的,每个元素都有一个index值(从0开始)标明元素在列表中的位置
在声明时可以带有一个参数,即List
实现它的四个主要类是
Vector
ArrayList:一种类似数组的形式进行存储,因此它的随机访问速度极快
LinkedList:内部实现是链表,适合于在链表中间需要频繁进行插入和删除操作
栈Stack

Map接口
不是Collection接口的继承
用于维护键/值对(key/value pairs)
描述了从不重复的键到值的映射,是一个从关键字到值的映射对象
其中不能有重复的关键字,每个关键字最多能够映射到一个值
声明时可以带有两个参数,即Map<K, V>,其中K表示关键字的类型,V表示值的类型

SortedMap接口
一种特殊的Map,其中的关键字是升序排列的
与SortedSet对等的Map,通常用于词典和电话目录等
在声明时可以带有两个参数,即SortedMap<K, V>,其中K表示关键字的类型,V表示值的类型

接口的实现
Collection没有直接的实现,只是作为其他集合接口的最小公分母
除Collection 以外,其余五个接口都有实现
主要的实现有
Set->HashSet
SortedSet->TreeSet
List->Vector / ArrayList / LinkedList
Map->HashMap
SortedMap->TreeMap

import java.util.Vector; 
import java.util.Iterator; 
public class IteratorTester { 
  public static void main(String args[]) { 
    String[]  num = {"one", "two", "three", "four", "five","six", "seven", "eight", "nine", "ten"}; 
    Vector<String>   aVector = new Vector<String> (java.util.Arrays.asList(num)); 
    System.out.println("Before Vector: " + aVector); 
    Iterator<String> nums = aVector.iterator(); 
    while(nums.hasNext()) { 
      String aString = (String)nums.next(); 
      System.out.println(aString); 
      if (aString.length() > 4)    nums.remove();
    } 
    System.out.println("After Vector: " + aVector);
  }
}
运行结果
Before Vector: [one, two, three, four, five, six, seven, eight, nine, ten]
one
two
three
four
five
six
seven
eight
nine
ten
After Vector: [one, two, four, five, six, nine, ten]

IO

处理流
不直接与数据源或目标相连,而是基于另一个流来构造
从流读写数据的同时对数据进行处理
InputStreamReader和BufferedReader都属于处理流
InputStreamReader读取字节并转换为字符
BufferedReader对另一个流产生的数据进行缓冲

BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in) ); 

 

文件读写

写入文本文件(Writer)
常用类:FileWriter(节点流)、BufferedWriter

//写入文件中
import java.io.*;   
class FileWriterTester {
  public static void main ( String[] args ) throws IOException {  
     //main方法中声明抛出IO异常
     String fileName = "C:\\Hello.txt"; 
     //每次运行都将删除原有的文件,创建同名文件,重新写入
     FileWriter writer = new FileWriter( fileName );   		//一共有五种构造方法
     //FileWriter writer = new FileWriter( fileName ,true ); 
     //运行此句程序,会发现在原文件内容后面又追加了重复的内容,这就是将构造方法的第二个参数设为true的效果
     writer.write( "Hello!\n"); 
     writer.write( "This is my first text file,\n"  );  
     writer.write( "You can see how this is done.\n" ); 
     writer.write("输入一行中文也可以\n");
     writer.close(); 
  }
}

BufferedWriter类
当需要写入的内容很多时,利用更高效的缓冲器流类BufferedWriter

import java.io.*; 
class BufferedWriterTester {
	public static void main ( String[] args ) throws IOException	{
		String fileName = "C:/newHello.txt" ;
		BufferedWriter out = new BufferedWriter( new  FileWriter( fileName ) );
        out.write( "Hello!"  );
    	out.newLine() ; //换行
 		out.write( "This is another text file using BufferedWriter,"  );   
        out.newLine(); ;
        out.write( "So I can use a common way to start a newline" ); 
        out.close();
    }
}

读出文本文件
常用类:FileReader(节点流)、BufferedReader、InputStreamReader

FileReader类
继承自Reader抽象类的子类InputStreamReader,从文本文件中读取字符

BufferedReader类
读文本文件的缓冲器类,继承自Reader

//文件读入
import java.io.*;
class BufferedReaderTester {
    public static void main ( String[] args ) {
        String fileName = "C:/Hello.txt" , line;
        try {
             BufferedReader in = new BufferedReader(new FileReader( fileName  ) );//若文件不存在会抛出IOException
             line = in.readLine();   //读取一行内容
             while ( line != null ) { 
       			System.out.println( line );
      			line = in.readLine();
     	      }
              in.close(); //一定要记得关闭文件
              //int c;
			  //while((c=in.read())!= -1)  System.out.print((char)c);
			  //Reader类的read()方法也可用来判别文件结束。该方法返回的一个表示某个字符的int型整数,如果读到文件末尾,返回 -1
        }
       catch ( IOException iox ) { 
            System.out.println("Problem reading " + fileName );  
        }
    }
}

写二进制文件(OutputStream、InputStream)
常用类:OutputStream、FileOutputStream、BufferedOutputStream、DataOutputStream


import java.io.*; 
class FileOutputstreamTester {
  public static void main ( String[] args ) { 
    String fileName = "c:/data1.dat" ;
     int value0  = 255, value1  = 0, value2 = -1;
     try {
        DataOutputStream out = new DataOutputStream(new FileOutputStream( fileName  ) );
        out.writeInt( value0 );
        out.writeInt( value1 );
        out.writeInt( value2 );
        out.close();
      }
      catch ( IOException iox ){
     	 System.out.println("Problem writing " + fileName );   }
      }
 }
 //写完后查看二进制信息,发现内容为00 00 00 FF 00 00 00 00 FF FF FF FF

BufferedOutputStream类
类似于BufferedWriter,对于大量数据的写入可以提高效率

//向文件中写入各种数据类型的数,并统计写入的字节数
import java.io.*; 
class BufferedOutputStreamTester {
  public static void main ( String[] args ) throws IOException {
    	String fileName = "mixedTypes.dat" ;
    	DataOutputStream dataOut = new DataOutputStream(
                             new BufferedOutputStream(
                               new FileOutputStream( fileName  ) ) );
    	dataOut.writeInt( 0 );
    	System.out.println( dataOut.size()  + " bytes have been written.");//4
    	dataOut.writeDouble( 31.2 );
    	System.out.println( dataOut.size()  + " bytes have been written.");//12
    	dataOut.writeBytes("JAVA");
    	System.out.println( dataOut.size()  + " bytes have been written.");//16
    	dataOut.close();
  }
}

读二进制文件
常用类:FileInputStream、DataInputStream、BufferedInputStream

import java.io.*;
class DataInputStreamTester {
  public static void main ( String[] args ) {
    	String fileName = "c:/data1.dat" ;   long sum = 0;
    	try {
    	    DataInputStream instr = new DataInputStream(
           new BufferedInputStream(new FileInputStream(fileName)));
         try {
            while ( true )
            sum += instr.readInt();
         }
         catch ( EOFException  eof ) {//遇到文件结尾会抛出此异常
           System.out.println( "The sum is: " + sum );
           instr.close();
         }
    }
    catch ( IOException iox ) {
    	    System.out.println("IO Problems with " + fileName ); }
    }
  }
}

//文本文件的存储方式也是二进制代码,也可以用InputStream类的方法读取
import java.io.*;
public class InputStreamTester {
	public static void main(String[] args) throws IOException {
	    FileInputStream s=new FileInputStream("c:/Hello.txt");
        int c;
        while ((c = s.read()) != -1) //读取1字节,结束返回-1
            System.out.write(c);     
        s.close();
    }
}

文件复制

import java.io.*; 
class CopyBytes {
  public static void main ( String[] args ) {
    DataInputStream  instr;
    DataOutputStream outstr;
    if ( args.length != 2 ) {
        System.out.println("Please enter file names");
        return;
    }
    try { 
         instr = new DataInputStream(new   
            BufferedInputStream(new FileInputStream( args[0] )));
         outstr = new DataOutputStream(new  
            BufferedOutputStream(new FileOutputStream( args[1] )));       
        try { 
                int data;
                while ( true ) { 
                    data = instr.readUnsignedByte() ;
                    outstr.writeByte( data ) ;
                    }
                }
                catch ( EOFException  eof ) { 
                    outstr.close();   
                    instr.close(); 
                    return;     
                }
        }
        catch ( FileNotFoundException nfx )
        {   System.out.println("Problem opening files" );    }
        catch ( IOException iox )
        {   System.out.println("IO Problems" );    }
    }
    }

异常

异常是异常类的对象
生成异常对象并提交的过程称为**抛出(throw)**一个异常

错误的分类

Syntax errors 语法错误,编译器进行处理
Runtime errors 运行时错误,运行过程中出现无法执行的操作
Logic errors 逻辑错误,运行程序未能按照意图执行

根据错误的严重程度不同可以分为以下两类:

  • 错误:致命性的
  • 异常:非致命性

System errors 由JVM捕获。Error class 系统内部错误。程序无法处理,只能告知用户。
Exception 由于程序或程序的外部环境引起。可以捕获并处理。
exception下的异常又分为两类:

unchecked exceptions
•RuntimeException, Error and their subclasses
通常是程序的逻辑错误
•NullPointerException引用了空对象
•IndexOutOfBoundsException数组异常
•Java不要求必须捕获未检测异常

checked exceptions
•编译器强制检查和处理异常

异常的处理

  1. 声明抛出异常
    使用throws子句声明将异常抛出到调用方法中
public void setRadius(double newRadius) throws IllegalArgumentException {
    if (newRadius >= 0)
        radius =  newRadius;
    else 
        throw new IllegalArgumentException("Radius cannot be negative");
}
  1. 捕获异常
    使用try{}catch(){}块,捕获到所发生的异常
try {  
    statements;
}catch(TheException ex) { 
    handling ex; 
}finally { 
    finalStatements; 
}

异常处理的优点:将错误处理和普通代码进行了分离
异常处理try-catch需要大量的时间和资源

什么时候处理异常:
在调用的方法中处理->抛出
在方法内部处理->无需抛出

Thread

多线程编程基础

一个独立程序的每一次运行称为一个进程,两个独立的程序在同时运行,称为两个进程

进程要占用相当一部分处理器时间和内存资源
• 进程具有独立的内存空间
• 通信很不方便,编程模型比较复杂

多线程
• 一个程序中多段代码同时并发执行,称为多线程
• 线程比进程开销小,协作和数据交换容易
• Java是第一个支持内置线程操作的主流编程语言
• 多数程序设计语言支持多线程要借助于操作系统“原语(primitives)”

Thread类
直接继承了Object类,并实现了Runnable接口。位于java.lang包中
• 封装了线程对象需要的属性和方法
• 继承Thread类——创建多线程的方法之一
• 从Thread类派生一个子类,并创建子类的对象
• 子类应该重写Thread类的run方法,写入需要在新线程中执行的语句段。
• 调用start方法来启动新线程,自动进入run方法。

在新线程中完成计算某个整数的阶乘
public class FactorialThreadTester{
    public static void main( String [] args )
    {
        System.out.println("main thread starts");
        FactorialThread thread=new FactorialThread(10);
        thread.start(); //将自动进入run()方法
        System.out.println("main thread ends " );
    }
}

class FactorialThread extends Thread
{
    private int num;
    public FactorialThread( int num ){ this.num=num; }
    public void run(){
        int i=num;
        nt result=1;
        System.out.println("new thread started" );
    while(i>0){ 
        result=result*i; i=i-1; 
    }
    System.out.println("The factorial of "+num+" is "+result);
    System.out.println("new thread ends");
    }
}
运行结果
main thread starts
main thread ends
new thread started
The factorial of 10 is 3628800
new thread ends

main函数调用thread.start()方法启动新线程后并不等待其
run方法返回就继续运行,thread.run函数在一边独自运行,
不影响原来的main函数的运行

创建3个新线程,每个线程睡眠一段时间(06秒),然后结束
public class ThreadSleepTester {
public static void main( String [] args ) {
//创建并命名每个线程
TestThread thread1 = new TestThread( "thread1" );
TestThread thread2 = new TestThread( "thread2" );
TestThread thread3 = new TestThread( "thread3" );
System.out.println( "Starting threads" );
thread1.start(); // 启动线程1
thread2.start(); // 启动线程2
thread3.start(); // 启动线程3
System.out.println( "Threads started, main ends\n" );
}
}
class TestThread extends Thread {
    private int sleepTime;
    public TestThread( String name ) {
        super( name );
        sleepTime = ( int ) ( Math.random() * 6000 );
    }
    public void run() {
    try {
        System.out.println(
        getName() + " going to sleep for " + sleepTime );
        Thread.sleep( sleepTime ); //线程休眠
    }catch ( InterruptedException exception ) {};
    System.out.println( getName() + " finished" )
    }
}

Runnable接口
• Thread类实现了Runnable接口
• 只有一个run()方法
• 更便于多个线程共享资源
• Java不支持多继承,如果已经继承了某个基类,便需要实现Runnable接口来生成多线程
• 以实现runnable的对象为参数建立新的线程
• start方法启动线程就会运行run()方法

public class FactorialThreadTester {
    public static void main( String [] args ) {
        System.out.println("main thread starts");
        FactorialThread t=new FactorialThread(10);//实现了Runnable的
        new Thread(t).start(); //运行FactorialThread的run
        System.out.println("new thread started,main thread ends " );
    }
}

class FactorialThread implements Runnable {
    private int num;
    public FactorialThread( int num ) {
        this.num=num;
    }
    public void run() {
        int i=num;
        int result=1;
    while(i>0) {
        result=result*i;
        i=i-1;
    }
    System.out.println("The factorial of "+num+" is "+result);
    System.out.println("new thread ends");
    }
} 

public class ThreadSleepTester {
    public static void main( String [] args ) {
        TestThread thread1 = new TestThread();
        TestThread thread2 = new TestThread();
        TestThread thread3 = new TestThread();
        System.out.println( "Starting threads" );
        new Thread(thread1,"Thread1").start();
        new Thread(thread2,"Thread2").start();
        new Thread(thread3,"Thread3").start();
    System.out.println( "Threads started, main ends\n" );
    }
}
class TestThread implements Runnable {
    private int sleepTime;
    public TestThread() {
        sleepTime = ( int ) ( Math.random() * 6000 );
    }
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " going to sleep for "+ sleepTime );
            Thread.sleep( sleepTime );
        }catch ( InterruptedException exception ) {};
        System.out.println( Thread.currentThread().getName()+ "finished" );
    }
}

用同一个实现了Runnable接口的对象作为参数创建多个线程
多个线程共享同一对象中的相同的数据

独立的同时运行的线程有时需要共享一些数据并且考虑到彼此的状态和动作

此为多线程之间的数据共享。比如卖票这个行为,需要用到数据共享,因为一共就那么多票,几个窗口同时开放相当于多个线程

有时线程之间彼此不独立、需要同步

  • 线程间的互斥
    同时运行的几个线程需要共享一个(些)数据
    共享的数据,在某一时刻只允许一个线程对其进行操作

“生产者/消费者” 问题
• 假设有一个线程负责往数据区写数据,另一个线程从同一数据
区中读数据,两个线程可以并行执行
• 如果数据区已满,生产者要等消费者取走一些数据后才能再写
• 当数据区空时,消费者要等生产者写入一些数据后再取

用两个线程模拟存票、售票过程
• 假定开始售票处并没有票,一个线程往里存票,另外一个线程则往出卖票
• 我们新建一个票类对象,让存票和售票线程都访问它。本例采用两个线程共享同一个数据对象来实现对同一份数据的操作

public class ProducerAndConsumer {
    public static void main(String[] args) {
        Tickets t=new Tickets(10); //建立票对象,票总数10
        new Consumer(t).start(); //开始卖票线程
        new Producer(t).start(); //开始存票线程
    }
}

class Tickets {
    int number=0; //票号
    int size; //总票数
    boolean available=false; //表示目前是否有票可售
    public Tickets(int size) //构造函数,传入总票数参数
    {
    this.size=size;
    }
}

class Producer extends Thread{
    Tickets t=null;
    public Producer(Tickets t)
    { this.t=t; }
    public void run(){
        while( t.number < t.size){
            System.out.println("Producer puts ticket "+(++t.number));
            t.available=true;
        }
    }
}

class Consumer extends Thread {//售票线程
    Tickets t=null;
    int i=0;
    public Consumer(Tickets t)
    { this.t=t; }
    public void run(){
        while(i<t.size){
            if(t.available==true && i<=t.number)
                System.out.println("Consumer buys ticket "+(++i));
            if(i==t.number) //现有的票号卖完了
                t.available=false;
        }
    }
}

运行结果:
Producer puts ticket 1
Producer puts ticket 2
Producer puts ticket 3
Producer puts ticket 4
Producer puts ticket 5
Producer puts ticket 6
Producer puts ticket 7
Producer puts ticket 8
Consumer buys ticket 1
Consumer buys ticket 2
Consumer buys ticket 3
Consumer buys ticket 4
Consumer buys ticket 5
Consumer buys ticket 6
Consumer buys ticket 7
Consumer buys ticket 8
Producer puts ticket 9
Producer puts ticket 10
Consumer buys ticket 9
Consumer buys ticket 10.

线程同步
• 互斥:许多线程在同一个共享数据上操作而互不干扰,同一时刻只能有一个线程访问该共享数据。因此有些方法或程序段在同一时刻只能被一个线程执行,称之为监视区
• 协作:多个线程可以有条件地同时操作共享数据。执行监视区代码的线程在条件满足的情况下可以允许其它线程进入监视区

synchronized ——线程同步关键字,实现互斥
• 用于指定需要同步的代码段或方法,也就是监视区
• 可实现与一个锁旗标的交互。例如:
synchronized(对象){ 代码段 }
• synchronized的功能是:首先判断对象的锁旗标是否在,如果在就获得锁旗标,然后就可以执行紧随其后的代码段;如果对象的锁旗标不在(已被其他线程拿走),就进入等待状态,直到获得锁旗标
• 当被synchronized限定的代码段执行完,就释放锁旗标

互斥
• 存票线程和售票线程应保持互斥关系。即售票线程执行时不进入存票线程、存票线程执行时不进入售票线程
• Java 使用监视器机制
• 每个对象只有一个“锁旗标” ,利用多线程对“锁旗标”的争夺实现线程间的互斥
• 当线程A获得了一个对象的锁旗标后,线程B必须等待线程A完成规定的操作、并释放出锁旗标后,才能获得该对象的锁旗标,并执行线程B中的操作

将需要互斥的语句段放入synchronized(object){}语句中,且两处的object是相同的

class Producer extends Thread {
    Tickets t=null;
    public Producer(Tickets t) { this.t=t;}
    public void run() {
        while((t.number)<t.size) {
            synchronized(t) { // 申请对象t的锁旗标
                System.out.println("Producer puts ticket"+(++t.number));
                t.available=true;
            } // 释放对象t的锁旗标
        }
        System.out.println("Producer ends!");
    }
}
class Consumer extends Thread {
    Tickets t=null;
    int i=0;
    public Consumer(Tickets t) { this.t=t; }
    public void run() {
        while(i<t.size) {
            synchronized(t) { //申请对象t的锁旗标
                if(t.available==true && i<=t.number)
                    System.out.println("Consumer buys ticket "+(++i));
                if(i==t.number) {
                try{ Thread.sleep(1); } catch(Exception e){}
                t.available=false;
                }
            } //释放对象t的锁旗标
        }
        System.out.println("Consumer ends");
    }
} 

存票程序段和售票程序段为获得同一对象的锁旗标而实现互斥操作
• 当线程执行到synchronized的时候,检查传入的实参对象,并申请得到该对象的锁旗标。
如果得不到,那么线程就被放到一个与该对象锁旗标相对应的等待线程池中。
直到该对象的锁旗标被归还,池中的等待线程才能重新去获得锁旗标,然后继续执行下去.
除了可以对指定的代码段进行同步控制之外,还可以定义整个方法在同步控制下执行,只要在方法定义前加上synchronized关键字即可

后台线程
• 也叫守护线程,通常是为了辅助其它线程而运行的线程
• 它不妨碍程序终止
• 一个进程中只要还有一个前台线程在运行,这个进程就不会结束;如果一个进程中的所有前台线程都已经结束,那么无论是否还有未结束的后台线程,这个进程都会结束
• “垃圾回收”便是一个后台线程
• 如果对某个线程对象在启动(调用start方法)之前调用了setDaemon(true)方法,这个线程就变成了后台线程

• 创建一个无限循环的后台线程,验证主线程结束后,程序即结束
public class Ex8_10 {
    public static void main(String[] args) {
        ThreadTest t=new ThreadTest();
        t.setDaemon(true);
        t.start();
    }
}
class ThreadTest extends Thread {
    public void run() { while(true){} }
}
• 运行程序,则发现整个程序在主线程结束时就随之中止运行了,如果注释掉t.setDaemon(true)语句,则程序永远不会结束

线程的生命周期

线程从产生到消亡的过程:一个线程在任何时刻都处于某种线程状态(thread state)

死锁
• 线程在运行过程中,其中某个步骤往往需要满足一些条件才能继续进行下去,如果这个条件不能满足,线程将在这个步骤上出现阻塞
• 线程A可能会陷于对线程B的等待,而线程B同样陷于对线程C的等待,依次类推,整个等待链最后又可能回到线程A。如此一来便陷入一个彼此等待的轮回中,任何线程都动弹不得,此即所谓死锁(deadlock)
• 对于死锁问题,关键不在于出现问题后调试,而是在于预防

设想一个游戏,规则为3个人站在三角形的三个顶点的位置上,三个边上放着三个球,如图所示。每个人都必须先拿到自己左手边的球,才能再拿到右手边的球,两手都有球之后,才能够把两个球都放下

创建3个线程模拟3个游戏者的行为。

public class Ex8_11{
    public static void main(String[] args){
        Balls ball=new Balls(); //新建一个球类对象
        Player0 p0=new Player0(ball); //创建0号游戏者
        Player1 p1=new Player1(ball); //创建1号游戏者
        Player2 p2=new Player2(ball); //创建2号游戏者
        p0.start(); //启动0号游戏者
        p1.start(); //启动1号游戏者
        p2.start(); //启动2号游戏者
    }
}
class Balls { //球类
    boolean flag0=false; //0号球的标志变量,true表示已被人拿,false表示未被任何人拿
    boolean flag1=false; //1号球的标志变量
    boolean flag2=false; //2号球的标志变量
}

class Player0 extends Thread { //0号游戏者的类
    private Balls ball;
    public Player0(Balls b) { this.ball=b; }
    public void run() {
        while(true) {
            while(ball.flag1==true){}; //如果1号球已被拿走,则等待
            ball.flag1=true; //拿起1号球
            while(ball.flag0==true){}; //如果0号球已被拿走,则等待
            if(ball.flag1==true && ball.flag0==false) {
            ball.flag0=true; //拿起0号球
            System.out.println("Player0 has got two balls!");
            ball.flag1=false; //放下1号球
            ball.flag0=false; //放下0号球
            try{ sleep(1);}catch(Exception e){}; //放下后休息1ms
            }
        }
    }
}

class Player1 extends Thread //1号游戏者的类
{ private Balls ball;
    public Player1(Balls b) { this.ball=b; }
    public void run(){ 
        while(true){ 
            while(ball.flag0==true){};
            ball.flag0=true;
            while(ball.flag2==true){};
            if(ball.flag0==true && ball.flag2==false)
            { ball.flag2=true;
            System.out.println("Player1 has got two balls!");
            ball.flag0=false;
            ball.flag2=false;
            try{ sleep(1);}catch(Exception e){};
            }
        }
    }
}

class Player2 extends Thread //2号游戏者的类
{ private Balls ball;
public Player2(Balls b) { this.ball=b; }
public void run()
{ while(true)
{ while(ball.flag2==true){};
ball.flag2=true;
while(ball.flag1==true){};
if(ball.flag2==true && ball.flag1==false)
{ ball.flag1=true;
System.out.println("Player2 has got two balls!");
ball.flag1=false;
ball.flag2=false;
try{ sleep(1);}catch(Exception e){};
}
}
}
}

运行结果
• 若干次后将陷入死锁,不再有输出信息,即任何人都不能再
同时拥有两侧的球
• 程序说明
• 如果刚好3个人都拿到了左手边的球,都等待那右手边的球,
则因为谁都不能放手,则这3个线程都将陷入无止尽的等待
当中,这就构成了死锁
• 为了便于观察死锁发生的条件,我们在每个游戏者放下两边
的球后增加了sleep语句
• 为了避免死锁,需要修改游戏规则,使每个人都只能先抢到
两侧中号比较小的球,才能拿另一只球,这样就不会再出现
死锁现象

结束线程的生命
• 用stop方法可以结束线程的生命
• 但如果一个线程正在操作共享数据段,操作过程没有完成就用stop结束的话,将会导致数据的不完整,因此并不提倡使用此方法
• 通常,可通过控制run方法中循环条件的方式来结束一个线程

线程调度
• 在单CPU的系统中,多个线程需要共享CPU,在任何时间点上实际只能有一个线程在运行
• 控制多个线程在同一个CPU上以某种顺序运行称为线程调度
• Java虚拟机支持一种非常简单的、确定的调度算法,叫做固定优先级算法。这个算法基于线程的优先级对其进行调度

线程的优先级

• 每个Java线程都有一个优先级,其范围都在1和10之间。默认情况下,每个线程的优先级都设置为5
• 在线程A运行过程中创建的新的线程对象B,初始状态具有和线程A相同的优先级
• 如果A是个后台线程,则B也是个后台线程
• 可在线程创建之后的任何时候,通过setPriority(int priority)方法改变其原来的优先级
1最低,10最高

假设某线程正在运行,则只有出现以下情况之一,才会使其暂停运行
• 一个具有更高优先级的线程变为就绪状态(Ready);
• 由于输入/输出(或其他一些原因)、调用sleep、wait、yield方法使其发生阻塞;
• 对于支持时间分片的系统,时间片的时间期满

创建两个具有不同优先级的线程,都从1递增到400000,每增加
50000显示一次
public class Ex8_13{
    public static void main(String[] args) {
        TestThread[] runners = new TestThread[2];
        for (int i = 0; i < 2; i++) runners[i] = new TestThread(i);
        runners[0].setPriority(2); //设置第一个线程优先级为2
        runners[1].setPriority(3); //设置第二个线程优先级为3
        for (int i = 0; i < 2; i++) runners[i].start();
    }
}
class TestThread extends Thread{
    private int tick = 1;
    private int num;
    public TestThread(int i) { this.num=i; }
    public void run() {
        while (tick < 400000) {
            tick++;
            if ((tick % 50000) == 0) { //每隔5000进行显示
                System.out.println("Thread #" + num + ", tick = " + tick);
                yield(); //放弃执行权
            }
        }
    }
}

Thread #1, tick = 50000
Thread #1, tick = 100000
Thread #1, tick = 150000
Thread #1, tick = 200000
Thread #1, tick = 250000
Thread #1, tick = 300000
Thread #1, tick = 350000
Thread #1, tick = 400000
Thread #0, tick = 50000
Thread #0, tick = 100000
Thread #0, tick = 150000
Thread #0, tick = 200000
Thread #0, tick = 250000
Thread #0, tick = 300000
Thread #0, tick = 350000
Thread #0, tick = 400000

结果说明
• 具有较高优先级的线程1一直运行到结束,具有较低优先级的线程0才开始运行
• 虽然具有较高优先级的线程1调用了yield方法放弃CPU资源,允许线程0进行争夺,但马上又被线程1抢夺了回去,所以有没有yield方法都没什么区别

通常,我们在一个线程内部插入yield()语句,这个方法会使正在运行的线程暂时放弃执行,这是具有同样优先级的线程就有机会获得调度开始运行,但较低优先级的线程仍将被忽略不参加调度

并发控制

并发控制概述

多事务执行方式:
(1)事务串行执行
每个时刻只有一个事务运行,其他事务必须等到这个事务结束以后方能运行。不能充分利用系统资源,发挥数据库共享资源的特点
(2)交叉并发方式
在单处理机系统中,事务的并行执行是这些并行事务的并行操作轮流交叉运行。单处理机系统中的并行事务并没有真正地并行运行,
但能够减少处理机的空闲时间,提高系统的效率
(3)同时并发方式
多处理机系统中,每个处理机可以运行一个事务,多个处理机可以
同时运行多个事务,实现多个事务真正的并行运行
最理想的并发方式,但受制于硬件环境
更复杂的并发方式机制

事务是并发控制的基本单位

事务并发执行带来的问题——可能会存取不正确的数据,破坏事务的隔离性和数据库的一致性,有以下几种情形:

丢失修改(lost update)

两个并发执行的事务T1和T2对同一数据做修改,T2提交的结果破坏了T1提交的结果,从而造成了T1对数据的修改被丢失。
称“丢失更新”

不可重复读(non-repeatable read)

不可重复读包括三种情况:
(1)事务T1读取某一数据后,事务T2对其做了修改,当事务
T1再次读该数据时,得到与前一次不同的值。
(2)事务T1按一定条件从数据库中读取了某些数据记录后,事务T2删除了其中部分记录,当T1再次按相同条件读取数据时,发现某些记录神秘地消失了。
(3)事务T1按一定条件从数据库中读取某些数据记录后,事务T2插入了一些记录,当T1再次按相同条件读取数据时,发现多了一些记录。
后两种不可重复读有时也称为幻影现象(Phantom Row)

读“脏”数据(dirty read)

读“脏”数据是指:
▪ 事务T1修改某一数据,并将其写回磁盘
▪ 事务T2读取同一数据后,T1由于某种原因被撤销
▪ 这时T1已修改过的数据恢复原值,T2读到的数据就与数据库中数据不一致
▪ T2读到的数据就为“脏”数据,即不正
确的数据

产生上述三类数据不一致性的主要原因就是并发操作破坏了事
务的隔离性

并发控制的类型
悲观并发控制:用户执行的操作导致应用了某个锁,只有这个锁的所有者释放该锁,其他用户才能执行与该锁冲突的操作。
乐观并发控制
用户读取数据时不锁定数据。当一个用户更新数据
时,系统将进行检查,查看该用户读取数据后其他用户是否又更改了该数据。如果其他用户更新了数据,将产生一个错误。一般情况下,收到错误信息的用户将回滚事务并重新开始。
它主要用于数据争用较少的环境中

封锁

封锁就是事务T在对某个数据对象(例如表、记录等)操作之
前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象。封锁是实现并发控制的一个非常重要的技术

DBMS通常提供了多种类型的封锁。一个事务对某个数据对象加锁后究竟拥有什么样的控制是由封锁的类型决定的。
基本封锁类型

排它锁(exclusive lock,简记为X锁)

排它锁又称为写锁,其采用的原理是禁止并发操作。
若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任
何事务都不能再对A加任何类型的锁,直到T释放A上的锁。从而保证了其它事务在T释放A上的锁之前不能再读取和修改A

这里要注意一个概念:**如果其他事务无需修改数据,只是纯粹读数据,是不加锁。**上面指的不能读取和修改A指的是要求加锁的操作而言

共享锁(share lock,简记为S锁)

共享锁又称为读锁,其采用的原理是允许其他用户对同一数据对象进行查询,但不能对该数据对象进行修改。
若事务T对数据对象A加上S锁,则其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。

锁的相容矩阵

封锁协议

▪在运用X锁和S锁对数据对象加锁时,需要约定一些规则:封锁协议(Locking Protocol)
何时申请X锁或S锁
持锁时间
何时释放
▪常用的封锁协议:三级封锁协议

一级封锁协议

事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。
正常结束(COMMIT)
非正常结束(ROLLBACK)
一级封锁协议可防止丢失修改,并保证事务T是可恢复的。
在一级封锁协议中,如果仅仅是读数据,则不需要加锁,所以它不能保证可重复读不读“脏”数据
使用封锁机制,解决丢失修改问题

二级封锁协议

一级封锁协议+事务T在读取数据R前必须先加S锁,读完后即可释放S锁。
二级封锁协议可以防止丢失修改和读“脏”数据
在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读

三级封锁协议

一级封锁协议 + 事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放
三级封锁协议可防止丢失修改、读脏数据和不可重复读。

三级协议的主要区别
▪ 什么操作需要申请封锁以及何时释放锁(即持锁时间)。不同的封锁协议使事务达到的一致性级别不同
▪ 封锁协议级别越高,一致性程度越高

活锁和死锁

封锁技术可以有效地解决并发操作的一致性问题,但也带来一些新的问题:

活锁

当某个事务请求对某一数据的排它性封锁时,由于其他事务对该数据的操作而使这个事务处于永久等待状态,这种状态称为活锁。
避免活锁:采用先来先服务的策略

死锁

在同时处于等待状态的两个或多个事务中,其中的每一个事务在它能够进行之前,都等待着某个数据、而这个数据已被它们中的某个事务所封锁,这种状态称为死锁(Deadlock)

▪ 发生死锁的必要条件有以下四条:
互斥条件:一个数据对象一次只能被一个事务所使用,即对数据的封锁采用排它式;
不可抢占条件:一个数据对象只能被占有它的事务所释放,而不能被别的事务强行抢占;
部分分配条件:一个事务已经封锁分给它的数据对象,但仍然要求封锁其他数据;
循环等待条件:允许等待其他事务释放数据对象,系统处于加锁请求相互等待的状态。

  • 预防死锁
    ▪一次加锁法
    一次加锁法是每个事务必须将所有要使用的数据对象全部依次加锁,并要求加锁成功,只要一个加锁不成功,表示本次加锁失败,则应该立即释放所有已加锁成功的数据对象,然后重新开始从头加锁。
    问题:
    扩大封锁范围,降低了系统的并发度。难于事先精确确定封锁对象。
    只能将事务在执行过程中可能要封锁的数据对象全部加锁,这就
    进一步降低了并发度。

▪顺序封锁法
顺序加锁法是预先对所有可加锁的数据对象规定一个加锁顺序,每个事务都需要按此顺序加锁,在释放时,按逆序进行。

维护成本高(对象众多,需求不断变化)
难于实现(很难事先确定封锁对象)

DBMS在解决死锁的问题上更普遍采用的是诊断并解除死锁的
方法

  • 死锁的诊断与解除

▪允许死锁发生
▪解除死锁
▪由DBMS的并发控制子系统定期检测系统中是否存在死锁
▪一旦检测到死锁,就要设法解除。

▪超时法
如果一个事务的等待时间超过了规定的时限,就认为发生了死锁。
▪优点:
▪实现简单
▪缺点:
▪有可能误判死锁
▪时限若设置得太长,死锁发生后不能及时发现。

▪等待图法
用事务等待图动态反映所有事务的等待情况
▪ 事务等待图是一个有向图G=(T,U)
▪ T为结点的集合,每个结点表示正运行的事务
▪ U为边的集合,每条边表示事务等待的情况
▪ 若T1等待T2,则T1,T2之间划一条有向边,从T1指向T2
▪ 并发控制子系统周期性地(比如每隔数秒)检测事务等待图,如果发现
图中存在回路,则表示系统中出现了死锁。

▪ DBMS的并发控制子系统一旦检测到系统中存在死锁,就要设法解除。
▪ 选择一个处理死锁代价最小的事务,将其撤消,释放此事务持有的所有的锁,使其它事务能继续运行下去。
▪ 作为“牺牲品”的事务通常为:
▪ 最迟交付的事务
▪ 获得锁最少的事务
▪ 恢复代价最小的事务

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值