Java探索之旅(16)——异常处理
1.异常与异常处理
在《java编程思想》中这样定义 异常:阻止当前方法或作用域继续执行的问题。虽然java中有异常处理机制,但是要明确一点,决不应该用"正常"的态度来看待异常。绝对一点说异常就是某种意义上的错误,就是问题,它可能会导致程序失败。之所以java要提出异常处理机制,就是要告诉开发人员,你的程序出现了不正常的情况,请注意。
异常就是一个表示组织执行正常进行的错误(情况)。异常没有处理,程序将非正常终止。这是Java鲁棒性的一个体现。异常处理最根本的优势或者目的:即将检测错误和处理错误分离,被调用的方法检测错误而调用此方法的程序(方法)处理错误。
常见的异常有2种构造方法,默认或者字符串参数。
如下:两个数除法的运算,实现了被除数为0的异常处理
import java.util.*;
public class Exception {
public static void main(String args[]){
Scanner input=new Scanner(System.in);
int a,b;
boolean flag=true;
do{
try{
System.out.println("input a,b");
a=input.nextInt();
b=input.nextInt();
int result=division(a,b);
System.out.println("a/b = "+result);
flag=false;
}
catch(ArithmeticException ex){//处理错误
System.out.println(ex);
input.nextLine();//清行,等待输入
}
}
while(flag);
input.close();
}
public static int division(int a,int b)//检错错误
{
if(b!=0)
return a/b;
else
throw new ArithmeticException(" b cannot be zero ,input again");
} }
2.异常类型与体系
Throwable是所有异常的根。所有的异常类直接或者间接继承于它,体系如下。
异常类一般分为如下3类
Excepiton、Error及其子类。一般为程序设计上存在不可恢复的逻辑错误。对免检异常可以不做处理,JVM会处理。
3.异常的捕获和处理 常捕yi获和处理框架:
3.1处理和捕获框架
❶try语句块:尝试运行代码,try语句块中代码受异常监控,其中代码(或者调用的方法)发生异常时,会抛出异常对象。
❷catch语句块:会捕获try代码块中发生的异常并在其代码块中做异常处理,catch语句带一个Throwable类型的参数表示可捕获异常类型。当try中出现异常时,catch会捕获到发生的异常并匹配。catch语句可以有多个,用来匹配多个中的一个异常。一旦匹配上后,就不再尝试匹配别的catch块了。由于catch可以捕获某个父类异常及其所有派生子类。因此应该保证子类catch块在父类的catch块之前,否则产生编译错误。
❸finally语句块:这一语句块可以省略。 是紧跟catch语句后的语句块,这个语句块总是会在方法返回前执行, 而不管是否try语句块是否发生异常。并且这个语句块总是在方法返回前执行。 目的是给程序一个补救的机会。这样做也体现了Java语言的健壮性,具体见4.3节
try{
//(尝试运行的)程序代码
}catch(异常类型 异常的变量名){
//异常处理代码
}finally{
//异常发生,方法返回之前,总是要执行的代码
}
三个语句块均不能单独使用,可以组成形如:
try...catch...finally
try...catch
try...finally
3种结构,catch语句可以有一个或多个,finally语句最多一个。局部变量独立而不能相互访问。多个catch块时候,由上到下匹配,一旦成功某个异常类即执行catch块代码,且不再执行其他catch块。
3.2throw和throws关键字
❶throw关键字:用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了某种检查异常, 则还在方法头部用throws对应声明。该方法的调用者必须检查处理抛出的异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理也很简单,就是打印异常消息和堆栈信息。如果抛出免检异常(Error或RuntimeException),则该方法的调用者可选择是否处理该异常。
❷throws关键字:用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者没有处理该异常的时候,应该继续抛出。注意,如果方法没有在父类中声明异常,那么不能在子类中对其进行覆盖来声明异常。
public static void test3() throws Exception{
//抛出一个检查异常
throw new Exception("方法test3中的Exception");
}
3.3 异常说明
常见异常类的成员函数(继承自Throwable)如下:
❶String getCause():返回引起异常的原因(如引起该异常的异常)。如果cause不存在或未知则返回 null。
❷String getMessage():返回异常的消息信息,即新建异常时候的String参数。
❸String toString(); 返回“异常类全名:getMessage()方法返回字符串”
❹void printStackTrace():对象的堆栈跟踪输出至错误输出流
❺StackTraceElement[] getStackTrace():返回栈跟踪元素数组来表示可抛出的栈跟踪信息。可以调用子函数确定异常发生的类(getClassName()),发生异常的方法(getMeholdName()),发生的行号(getLineNumber())等信息。对于嵌套调用,元素0为栈顶元素,表示调用序列的最后一个调用(异常被抛出的地方),最后一个栈元素为调用序列的最后一个调用。如下面的非嵌套调用,则倒序输出所有的异常。
public class NeverCaught {
public static void main(String[] args) throws Exception {
try
{ function1();}
catch (Exception ex) {
System.out.println("getCause()----"+ex.getCause());//此处异常是由“exception from function2”引起
System.out.println("getMessage()----"+ex.getMessage());
System.out.println("toString()----"+ex.toString()+"\n"); ex.printStackTrace();//打印调用栈的跟踪信息 StackTraceElement[] ste=ex.getStackTrace();
for(int i=0;i<ste.length;i++)
{System.out.print("\n"+ste[i].getMethodName()+":");//出现异常的方法名
System.out.println(ste[i].getLineNumber());//出现异常的所属类名
System.out.println(ste[i].getClassName());}//出现异常的行号
}
}
static void function1() throws Exception {
try {function2();}
catch (Exception ex) {
Exception C= new Exception("exception from function1",ex);
throw C;}
} static void function2() throws Exception{
throw new Exception("exception from function2");
输出:
4.常见的异常处理机制
4.1异常处理器及执行的语句
如果try中语句抛出异常,而本函数没有catch处理,则将传递异常给调用此函数的方法,且本函数的其他代码将不会执行。如果上一级函数仍旧没有catch处理,则这一级函数的其他代码不会执行的同时,传递该异常给再上一级调用函数。即所谓的反向传播检查。如果至main函数仍旧未有处理异常,JVM会进行处理。
一旦某一级处理器捕获到这个异常,则仅仅执行该函数catch的代码和try-catch结构之外的代码。调用该方法的上一级函数的try和try-catch之外的代码将被执行。
例如:
①function3抛出3中异常Exception3,Exception2,Exception1之一
②调用关系main<--function1<---function2<-----function3
③main,function1(),function2()分别处理异常Exception3,Exception2,Exception1
public class MyException { public static void main(String[] args) throws Exception3, Exception2 {//处理异常 Exception1
try{
function1(3);//此处定义1抛出异常1,定义2抛出异常2,定义3抛出异常3
/*statement1 code block*/
System.out.println("执行 statement1 code block");
}
catch(Exception1 ex1){
System.out.println("检测到 Exception1");
}
/*statement2 code block*/
System.out.println("执行 statement2 code block");
} public static void function1(int a) throws Exception3,Exception2,Exception1//处理异常 Exception2
{
try{
function2(a);
/*statement3 code block*/
System.out.println("执行 statement3 code block");
}
catch(Exception2 ex2){
System.out.println("检测到 Exception2");
}
/*statement4 code block*/
System.out.println("执行 statement4 code block");
} public static void function2(int a) throws Exception3,Exception2,Exception1//处理异常 Exception3
{
try{
function3(a);
/*statement5 code block*/
System.out.println("执行 statement5 code block");
}
catch(Exception3 ex3){
System.out.println("检测到 Exception3");
}
/*statement6 code block*/
System.out.println("执行 statement6 code block");
} public static void function3(int a) throws Exception3,Exception2,Exception1//抛出异常
{
if(a==1)
throw new Exception1("Exception1 appears");
else if(a==2)
throw new Exception2("Exception2 appears");
else
throw new Exception3("Exception3 appears");
}
} class Exception1 extends Exception
{
Exception1(String str)
{super(str);}
}
class Exception2 extends Exception
{
Exception2(String str)
{super(str);}
}
class Exception3 extends Exception
{
Exception3(String str)
{super(str);}
}
倘若function3()抛出异常Exception1。输出
检测到 Exception1
执行 statement2 code block
倘若function3()抛出异常Exception2。输出
检测到 Exception2
执行 statement4 code block
执行 statement1 code block
执行 statement2 code block
倘若function3()抛出异常Exception3。输出
检测到 Exception3
执行 statement6 code block
执行 statement3 code block
执行 statement4 code block
执行 statement1 code block
执行 statement2 code block
4.2 异常链的使用及异常丢失
参考2中提到了异常处理的丢失情况。假设这样的场景:倘若某个异常B被捕捉到,且继而抛出其他异常C,则异常B的信息将不会出现在C的跟踪栈StackTrace上,不利于检查。因此使用从Throwable继承的initCause()方法,即异常链特性。
❶先定义异常类ExceptionB,ExceptionC
public class ExceptionB extends Exception {
public ExceptionB(String str) {super(str);}
} public class ExceptionC extends Exception<strong>B</strong> {
public ExceptionC(String str) {super(str);}
}
使用异常链
public class NeverCaught {
public static void main(String[] args) {//处理异常C
try
{ function1();}
catch (ExceptionC ex) {
ex.printStackTrace();}
}
static void function1() throws ExceptionC {//处理异常ExceptionB,抛出异常ExceptionC
try {function2();}
catch (ExceptionB ex) {
ExceptionC C= new ExceptionC("exception C");
//异常链
C.initCause(ex);//初始化引起C异常的异常
throw C;}
}
倘若没有使用异常链C.initCause(ex);则main()中的ex.printStackTrace()仅仅显示出现异常ExceptionC,而添加之后,ExceptionC,ExceptionB二者先后显示。
❷异常链同原始异常一起抛出新的异常,也称为链式异常。还有一种利用构造法,如:
public class NeverCaught {
public static void main(String[] args) throws Exception {
try
{ function1();}
catch (Exception ex) {
ex.printStackTrace();}
}
static void function1() throws Exception {
try {function2();}
catch (Exception ex) {
Exception C= new Exception("exception from function1",ex);//包装成新的异常
throw C;}
} static void function2() throws Exception{
throw new Exception("exception from function2");
}
}
第11行代码利用构造法包装新的异常。控制台中先显示"exception from function1"后显示原始异常"exception from function2",2次异常都被检测到。
4.3异常转译
所谓的异常转译就是将一种异常转换另一种新的异常,也许这种新的异常更能准确表达程序发生异常。在Java中有个概念就是异常原因,异常原因导致当前抛出异常的那个异常对象,几乎所有带异常原因的异常构造方法都使用Throwable类型做参数,这也就为异常的转译提供了直接的支持,因为任何形式的异常和错误都是Throwable的子类。
比如将SQLException转换为另外一个新的异常DAOException,先自定义异常DAOException。
public class DAOException extends RuntimeException {
/(省略了部分代码)
public DAOException(String message, Throwable cause) {
super(message, cause);
}
有一个SQLException类型的异常对象sqle,要转换为DAOException,可以这么写
DAOException daoEx = new DAOException ( "SQL异常", sqle);
异常转译是针对所有继承Throwable超类的类而言的,从编程的语法角度讲,其子类之间都可以相互转换。但是,从合理性和系统设计角度考虑,可将异常分为三类:Error、Exception、RuntimeException。参考1认为,合理的转译关系图应该如图:
参考1的作者认为:对于一个应用系统来说, 系统所发生的任何异常或者错误对操作用户来说都是系统"运行时"异常,都是这个应用系统内部的异常。这也是异常转译和应用系统异常框架设计的指导原则。在系统中大量处理非检查异常的负面影响很多, 最重要的一个方面就是代码可读性降低,程序编写复杂,异常处理的代码也很苍白无力。 因此,很有必要将这些检查异常Exception和错误Error转换为RuntimeException异常让程序员根据情况来决定是否捕获和处理所发生的异常。
图中的三条线标识转换的方向,分三种情况:
①:Error到Exception:将错误转换为异常,并继续抛出。例如Spring WEB框架中将org.springframework.web.servlet.DispatcherServlet的doDispatch()方法中, 将捕获的错误转译为一个NestedServletException异常。这样做的目的是为了最大限度挽回因错误发生带来的负面影响。 因为一个Error常常是很严重的错误,可能会引起系统挂起。
②:Exception到RuntimeException:将必检异常转换为RuntimeException可以让程序代码变得更优雅, 让开发人员集中经理设计更合理的程序代码,反过来也增加了系统发生异常的可能性。
③:Error到RuntimeException:目的还是一样的。把所有的异常和错误转译为免检异常, 这样可以让代码更为简洁,还有利于对错误和异常信息的统一处理。
4.4 finally子句和文件读写清理
不论异常是否出现或者捕获与否,finally块子句都会被执行。甚至在finally之前出现return语句,它也会被执行。当有除内存之外的资源回复初始状态时使用该子句。如已经打开的文件等。例如修改4.1中的代码如下:
public class ExceptionList { public static void main(String[] args) throws Exception3, Exception2 {//处理异常 Exception1
.............
finally {System.out.println("执行 statement2 code block");}
} public static void function1(int a) throws Exception3,Exception2,Exception1//处理异常 Exception2
{
............
/*statement4 code block*/
finally {System.out.println("执行 statement4 code block");}
} public static void function2(int a) throws Exception3,Exception2,Exception1//处理异常 Exception3
{
............
/*statement6 code block*/
finally {System.out.println("执行 statement6 code block");}
} public static void function3(int a) throws Exception3,Exception2,Exception1//抛出异常
{...........}
}
倘若function3()抛出Exception1。输出:
执行 statement6 code block
执行 statement4 code block
检测到 Exception1
执行 statement2 code block
finally子句常见于I/O程序设计。为了确保文件在任何情况下得到关闭,建议在其中放入文件关闭语句。参考2中提到:Try...finally结构是保证资源正确关闭的一个手段。如果你不清楚代码执行过程中会发生什么异常情况会导致资源不能得到清理,那么你就用try对这段"可疑"代码进行包装,然后在finally中进行资源的清理。
例如:原始代码如下
public void readFile() {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream("file")));
// do some other work //close reader
reader.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
因为程序会在异常出现的地方跳出,故而后面的代码将不能执行。所以以上虽然及早的关闭reader,但是reader.close()以前异常随时可能发生,因此不能预防任何异常的出现。这时我们就可以用try...finally来改造成为:
public void readFile() {
BufferedReader reader = null;
try {
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream("file")));
// do some other work // close reader
} finally {
reader.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
异常缺失:finally子句会导致异常丢失。例如,在try.....finally模型中:finally中也抛出的新异常会是try中抛出的异常被取代。又例如,finally中出现retrun语句会导致导致try中抛出的异常被屏蔽
4.5多个catch块和catch排列规则
单个方法的catch检查中,从上到下检查各个catch块。若某个catch块父类异常的,则也能catch子类异常。因此应该保证子类catch块出现在父类catch块之前。否则出现编译错误。
public class Sequence {
public static void main(String[] args){
try{method(2);}
catch(ExceptionB b)
{System.out.println("getMessage()----"+b.getMessage());}
catch(ExceptionA a)
{System.out.println("getMessage()----"+a.getMessage());}
}
public static void method(int a) throws ExceptionA//即抛出父类异常又抛出子类异常,声明抛出父类异常即可
{
if(a==1)
throw new ExceptionA("ExceptionA appears");
else
throw new ExceptionB("ExceptionB appears");
}
}
class ExceptionA extends Exception {
public ExceptionA(String str) {super(str);}
} class ExceptionB extends ExceptionA {
public ExceptionB(String str) {super(str);}
}
如上代码。子类catch块在前,父类catch块在后。父类catch可以捕获子类异常处理。同样,倘若某个函数即抛出父类异常又抛出子类异常,声明抛出父类异常即可。
4.6异常的限制
❶对于某一类。不同参数的构造函数可以抛出不同的异常。但是必须声明抛出基类异常。
❷基类函数未声明抛出异常,则子类重载函数不能声明抛出。
❷单若基类抛出异常,则可以重载不抛出异常或者抛出继承于基类异常之异常的函数。
❹接口同名函数不能改变基类已经存在的函数的异常类型。
简言之:在继承和覆盖中,“异常说明”的接口不是变大而是变小了。例如出现在基类的异常可以不一定出现在子类。这样从侧面说明子类的功能更加细化。
4.7构造器与异常
使用构造器时,如打开文件。应使用规则:在最外层,使用try...catch块。try中创建成功后,立即进入再一级try..finally(或try...catch--finally)块,在finally语句中再使用释放功能。创建失败,由外层catch块处理。
5.总结
5.1何时使用异常
❶try包含正常情况执行的代码,catch包含异常情况下执行的代码。异常处理将错误处理代码从正常的程序设计分离出来,因此更易读和修改。但是由于异常处理需要初始化异常对象,需要从调用栈返回,而且还要沿方法调用链传播异常以便找到其他异常处理器,因此需要很多医院和时间。
❷尽量在发生异常的地方处理异常。共同的异常应当考虑将其包装成同一异常类。
❸异常是程序设计的一部分,不要为了使用异常而使用异常。特别是不要将异常处理当简单的逻辑测试使用,例如NullPointException某种情况下可以直接使用if-else判断。
5.2异常处理的原则
❶能处理就早处理,抛出不去还不能处理的就想法消化掉或者转换为RuntimeException处理。 因为对于一个应用系统来说,抛出大量异常是有问题的,应该从程序开发角度尽可能的控制异常发生的可能。
❷对于检查异常,如果不能行之有效的处理,还不如转换为RuntimeException抛出。这样也让上层的代码有选择的余地――可处理也可不处理。 异常可以传播,也可以相互转译,但应该根据需要选择合理的异常转译的方向
❸对于一个应用系统来说,应该有自己的一套异常处理框架,这样当异常发生时,也能得到统一的处理风格,将优雅的异常信息反馈给用户。
5.3异常的误用
参考2中提到:
❶避免用一个Exception(Throwable)来捕捉所有的异常,
❷异常是程序处理意外情况的机制,当程序发生意外时,我们需要尽可能多的得到意外的信息,包括发生的位置,描述,原因等等。这些都是我们解决问题的线索。但是上面的例子都只是简单的printStackTrace()。如果我们自己写代码,就要尽可能多的对这个异常进行描述。比如说为什么会出现这个异常,什么情况下会发生这个异常。如果传入方法的参数不正确,告知什么样的参数是合法的参数,或者给出一个sample。
❸将try block写的简短,不要所有的东西都扔在这里,我们尽可能的分析出到底哪几行程序可能出现异常,只是对可能出现异常的代码进行try。尽量为每一个不同的异常写一个try...catch,避免异常丢失。在IO操作中,一个IOException也具有"一夫当关万夫莫开"的气魄。
5.4 其它
❶在捕获某种异常后,直接抛出另外一类异常。则垃圾回收机制会将栈中的异常对象清理干净,printStackTrace()显示从异常由此抛出。见《Think in Java》p260
❷相较于C和C++,异常能够容许程序在发生错误后强制停止并且换一条路径走,并告诉我们出了什么问题增加程序的稳健性,C则不行。
❸new在堆上创建异常对象,并返回该对象的引用。当前路径终止并在开始查找异常处理程序。
❹可以声明某个方法抛出异常,但实际不抛出。定义抽象基类和接口时往往需要实现抛出这些预声明的异常。
❺C++无finally子句,依赖析构函数达到同样的目的。
❻可以直接在main函数中声明抛出异常,但不做处理,异常信息自动传递给控制台。
❼可以使用RuntimeException(e)的形式包装必检异常,然后重新抛出。再使用.getCause()的方式取出。具体见《Thinking in Java》P280
本文参考:
1.Java异常体系结构
Java探索之旅(16)——异常处理的更多相关文章
- Java探索之旅(15)——包装类和字符类
1.包装类 ❶出于对性能的考虑,并不把基本数据类型作为对象使用,因为适用对象需要额外的系统花销.但是某些Java方法,需要对象作为参数,例如数组线性表ArrayList.add(Object).Jav ...
- Java探索之旅(13)——字符串类String
1.初始化 String类是Java预定义类,非基本类型而是引用类型. public class StudyString { public static void main(String[] args ...
- Java探索之旅(10)——数组线性表ArrayList和字符串生成器StringBuffer/StringBuilder
1.数组线性表ArrayList 数组一旦定义则不可改变大小.ArrayList可以不限定个数的存储对象.添加,插入,删除,查找比较数组更加容易.可以直接使用引用类型变量名输出,相当于toString ...
- Java探索之旅(8)——继承与多态
1父类和子类: ❶父类又称基类和超类(super class)子类又称次类和扩展类.同一个package的子类可以直接(不通过对象)访问父类中的(public,缺省,protected)数据和方法. ...
- Java探索之旅(4)——方法和Random&Math类
1.基本知识点 ❶方法在C++里面称为函数.调用方法时,应该类型兼容--即不需显式类型转换即可将形参传递给实参. ❷形参的改变不影响实参的值. ❸Java注重模块化设计和自顶向下的设 ...
- Java探索之旅(18)——多线程(2)
1 线程协调 目的对各线程进行控制,保证各自执行的任务有条不紊且有序并行计算.尤其是在共享资源或者数据情况下. 1.1 易变volatile cache技术虽然提高了访问数据的效率,但是有可能导致主存 ...
- Java探索之旅(17)——多线程(1)
1.多线程 1.1线程 线程是程序运行的基本执行单元.指的是一段相对独立的代码,执行指定的计算或操作.多操作系统执行一个程序时会在系统中建立一个进程,而在这个进程中,必须至少建立一个线程(这个线程被 ...
- Java探索之旅(14)——文本I/O与读写
1文件类File ❶封装文件或路径的属性.不包括创建和读写文件操作.File实例并不会实际创建文件.不论文件存在与否,可以创建任意文件名的实例.两种实例创建方式如下: ...
- Java探索之旅(12)——equals方法及其覆盖
1.Object中的equals方法 java中的的基本数据类型:byte,short,char,int,long,float,double,boolean.==比较的是值. ❶作用:对于复合类型来说 ...
随机推荐
- ARDUINO W5100 WebServer测试
1.直接下载官方的enternet->WebServer代码 /* Web Server A simple web server that shows the value of the anal ...
- rails跨域请求配置
gem 'rack-cors', '~> 0.3.1'application.rb config.middleware.insert_before 0, "Rack::Cors&quo ...
- iOS 发大招 otherButtonTitles:(nullable NSString *)otherButtonTitles, ... 写法 && 编写通用类的时候关于可变参数的处理
开始 我 以为 这个 alertView 里面 ...的写法 应该 是一个 普通的数组 然 并没有 分享一篇好文 http://www.tekuba.net/program/290/ IOS实现 ...
- python 3 并发编程之多进程 multiprocessing模块
一 .multiprocessing模块介绍 python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情况需要使用多进程. ...
- 【Flask】Sqlalchemy 常用数据类型
### SQLAlchemy常用数据类型:1. Integer:整形,映射到数据库中是int类型.2. Float:浮点类型,映射到数据库中是float类型.他占据的32位.3. Double:双精度 ...
- 最小生成树prim算法 POJ2031
#include<iostream> #include<algorithm> #include<string.h> #include<ctype.h> ...
- Symbol Table(符号表)
一.定义 符号表是一种存储键值对的数据结构并且支持两种操作:将新的键值对插入符号表中(insert):根据给定的键值查找对应的值(search). 二.API 1.无序符号表 几个设计决策: A.泛型 ...
- Python- Anacoda环境使用Selenium+ChromeDriver报错
我的系统是win10,python是用Anacoda安装的,通过pip安装了selenium 后使用Chromedriver发现报错,pip安装selenium如下: pip install sele ...
- linux 下载rpm包到本地,createrepo:创建本地YUM源
如何下载rpm包到本地 设置yum安装时,保留rpm包. 1.编辑 /etc/yum.conf 将keepcache的值设置为1; 这样就可以将yum安装时的rpm包保存在 /var/cache/yu ...
- HTML5 canvas save()和restore()方法讲解
我们尝试用这个连续矩形的例子来描述 canvas 的状态堆是如何工作的.第一步是用默认设置画一个大四方形,然后保存一下状态.改变填充颜色画第二个小一点的白色四方形,然后再保存一下状态.再次改变填充颜色 ...