Java学习笔记(3)——资源管理

46、包/套件/package

Java提供package机制,它就像是一个管理容器,可以将您所 定义的名称区隔管理在package下,而不会有相互冲突的发生,例如您定义了一个 dimension2d与dimension3d的package,在它们之下都有一个Point类别,但由于属于不同的package,所以这两个名称 并不会有所冲突。

Java的package被设计为与档案系统结构相对应,如果您的package设定是only.caterpillar,则该类别应该在指定目录(或jar)的onlyfun/caterpillar下可以找到,为了要能建立与package相对应的档案系统结构,您在编译时可以加入-d 参数,并指定要建立在哪一个目录之下。

下面这个程序使用"package"关键词来建立package以管理我们所定义的类别:

package onlyfun.caterpillar;
public class UsePackage {
    public static void main(String[] args) {
        System.out.println("Hello! World!");
    }
}

在编译时要使用以下的指令:

$ javac -d . UsePackage.java 

在编译时使用 "-d" 参数,并指定在现行目录 "."中建立档案与系统结构,则编译完成之后,在现行目录中会出现onlyfun/caterpillar目录,而当中有一个UsePackage.class 档案,在编译完成之后,package的指定就成为class名称的一部份了,在执行时可以这么下指令:

$ java onlyfun.caterpillar.UsePackage 
Hello! World! 

可以为类别建立package管理,举下面的例子来说:

package onlyfun.caterpillar;
public class Point2D {
    private int x, y;
    public Point2D() {x = 0; y = 0;}
    public Point2D(int x, int y) {this.x = x; this.y = y;}
    public int getX() {return x;}
    public int getY() {return y;}
}

这个类别建立在Point2D.java档案中,可以先用以下的指令来编译它:

$ javac -d .  Point2D.java

之前说过,package名称为类别名称的一部份,除非您重新编译类别,否则的话无法改变这个名称,为了要使用这个类别,方法之一是使用完全描述(Fully qualified)名称,也就是完整的指出package与类别名称,例如:

UsePackage.java
public class UsePackage {
	public static void main(String[] args) {
		onlyfun.caterpillar.Point2D p1 = new
		onlyfun.caterpillar.Point2D(10, 20);
		System.out.printf("p1: (x, y) = (%d, %d)%n",
		p1.getX(), p1.getY());
	}
}

当然这个方法在使用上不是很方便,您可以使用"import"关键词,告知编译器要使用的类别是位于哪一个package之下,如此可以少打一些 字,让编译器多作一些事,例如:

UsePackage.java
import onlyfun.caterpillar.Point2D;
public class UsePackage {
	public static void main(String[] args) {
		Point2D p1 = new Point2D(10, 20);
		System.out.printf("p1: (x, y) = (%d, %d)%n",
		p1.getX(), p1.getY());
	}
}

您在使用"import"时可以指定类别的完整描述,如果您会使用到某个package下的许多类别,您可以使用 '*',表示您可能使用到某个package下的某些类别,再让编译器作更多事,例如:

UsePackage.java
import onlyfun.caterpillar.*;
public class UsePackage {
	public static void main(String[] args) {
		Point2D p1 = new Point2D(10, 20);
		System.out.printf("p1: (x, y) = (%d, %d)%n",
		p1.getX(), p1.getY());
	}
}


但要注意的是,如果您import之后,出现类别名称有同名冲突时,编译器就不知道如何处理了,例如:

import java.util.Arrays;
import onlyfun.caterpillar.Arrays;
public class SomeClass {
	....
}

在这个例子中,编译器发现有两个Arrays类别,它不确定若遇到Arrays时您要使用的是java.util.Arrays,或是 onlyfun.caterpillar.Arrays,它只好回报以下讯息:

java.util.Arrays is already defined in a single-type import
import onlyfun.caterpillar.Arrays;
^
1 error

这个时候您就要考虑换一下类别名称了(如果您有权更动那些类别的话),或者是不使用"import",直接使用完整描述在"import"时尽量不使用 '*' 也可以减少这种情况发生

注意如果您提供的类别若不位于相同的package中,您的类别必须宣告为"public",若不宣告则类别预设只能于同一个package中被存取,例如将之前Point2D类别的public拿掉,则编译UsePackage.java档案时,会出现以下的错误:

UsePackage.java:5: caterpillar.demo.Point2D is not public in caterpillar.demo;cannot be accessed from outside package

如果定义类别成员时没有指定public、protected或private的存取修饰,则为预设(Default)权 限,成员将只能于同一个package中被直接存取,通常称之为package-friendly或package-private,且该成员将无法于子 类别中被直接存取。

要存取package的class也与CLASSPATH的设定有关,建议您也看看官方网站上的文章,您对package的了解会更深入:

  • Setting the class path (Windows)
  • Setting the class path (Solaris and Linux) 

Java平台的classes是被储存在Java安装目录的jre/lib/下的rt.jar,另外额外的第三组件(Third- party)可以放在/jre/lib/ext/中,在之前的例子中,使用"import"就是在告知编译器我们的类别位于哪一个package下,这些类别必须设定好CLASSPATH才可以被编译器找到,预设上是jre/lib/下的rt.jar、jre/lib/ext/中相关扩充组件与现行工作目录

47、预设建构子

当您在Java中定义一个类别,但没有定义建构子时,编译器会自动帮您产生一个预设建构子。

在继承时,如果您没有使用super()指定要使用父类别的哪个建构子,则预设会寻找无参数的建构子。

预设建构子的存取权限是跟随着类别的存取权限。

48、存取权限与修饰

在这边整理一下private、protected、public与default与类别及套件的存取关系:

存取修饰

同一类别

同一套件

子类别

全域

private

OK

     

default

OK

OK

   

protected

OK

OK

OK

 

public

OK

OK

OK

OK

49、常数设置

有时候您会需要定义一些常数供程序使用,您可以使用接口或类别来定义,例如定义操作常数:

OpConstants.java
public interface OpConstants {
	public static final int TURN_LEFT = 1;
	public static final int TURN_RIGHT = 2;
	public static final int SHOOT = 3;
}

常数必须是可以直接取用,并且不可被修改的,所以我们在宣告时加上 staticfinal,事实上,对于接口来说,当中的常数预设就为public、static、final,即使您没有明确指定修饰,例如下例与上例是一样的:

OpConstants.java
public interface OpConstants { 
int TURN_LEFT = 1; 
int TURN_RIGHT = 2; 
int SHOOT = 3; 
} 

这意谓着如果您实作的接口中有定义常数,您不可以重新指定该常数值,例如下例是错误的…

interface ISome {
	int const = 10;
	...
}
public class Some implements ISome {
	public Some() {
		const = 20; // 这行是错的
	}
}

宣告常数之后,就可以在程序中直接使用 OperateConstants.TURN_LEFT之类的名称来取代常数值,例如:

public void someMethod() {
	....
	doOp(OpConstants.TURN_RIGHT);
	....
}
public void doOp(int op) {
	switch(op) {
		case OpConstants.TURN_LEFT:
		System.out.println("向左转");
		break;
		case OpConstants.TURN_RIGHT:
		System.out.println("向右转");
		break;
		case OpConstants.SHOOT:
		System.out.println("射击");
		break;
	}
}

如果使用类别来宣告的话,方法也是类似,例如:

OpConstants.java
public class OpConstants {
	public static final int TURN_LEFT = 1;
	public static final int TURN_RIGHT = 2;
	public static final SHOOT = 3;
}

对于简单的常数设置,上面的作法已经足够了,不过在 J2SE 5.0 中新增了 列举型态(Enumerated Types),使用列举型态,除常数设定的功能之外,您还可以获得像编译时期型态检查等的更多好处。

50、Static import

J2SE 5.0 后新增了"import static" ,它的作用与 套件 (package) 中介绍的"import"类似,都是为了让您可以省一些打字功夫,让编译器多作一点事而存在的。

"import static"是使用时的语法,国外网站上的文章或原文书中介绍这个功能时,大都用static import描述这个功能,编译器讯息也这么写,这边就还是用static import来作为原文时的描述,但为了比较彰显这个功能的作用,我称之为「import 静态成员」。

使用"import static"语法,您可以import类别或接口中的静态成员,例如来看看这个Hello! World!程序:

HelloWorld.java
import static java.lang.System.out;
public class HelloWorld {
	public static void main(String[] args) {
		out.println("Hello! World!");
	}
}

在这边您将java.lang.System类别中的out静态成员import至程序中,编译时编译器遇到out名称,就会自动展开为System.out,所以这还是编译器给的蜜糖(Compiler suger)。

再来看一个例子,Arrays 类别 中有很多的静态方法,为了使用方便,可使用"import static"将这些静态方法import至程序中,例如:

UseImportStatic.java
import static java.lang.System.out;
import static java.util.Arrays.sort;
public class UseImportStatic {
	public static void main(String[] args) {
		int[] array = {2, 5, 3, 1, 7, 6, 8};
		sort(array);
		for(int i : array) {
			out.print(i + " ");
		}
	}
}

如果您想要import类别下所有的静态成员,可以使用 '*' 字符,例如:

UseImportStatic.java
import static java.lang.System.*;
import static java.util.Arrays.*;
public class UseImportStatic {
	public static void main(String[] args) {
		int[] array = {2, 5, 3, 1, 7, 6, 8};
		sort(array);
		for(int i : array) {
			out.print(i + " ");
		}
	}
}

与import一样,import 静态成员(static import)这个功能是为了方便,可以让您少打一些字,您把少打的字交给编译器来判断并自动为您补上,但是您要注意名称冲突问题,有些名称冲突编译器可 能透过以下的几个方法来解决:

  • 成员覆盖
    如果类别中有同名的field或方法名称,则优先选用它们。
  • 区域变量覆盖
    如果方法中有同名的变量名或自变量名,则选用它们。
  • 重载(Overload)方法 上的比对
    对于被使用import static的各个静态成员,若有同名冲突,尝试透用重载机制判断,也就是透过方法名称及自变量列的比对来选择适当的方法。

如果编译器无法判断,则会回报错误,例如若您定义的sort()方法与Arrays的sort()方法冲突,且编译器也无法判别时,会出现以下的讯息:

 reference to sort is ambiguous,
 both method sort(float[]) in onlyfun.caterpillar.Arrays and
 method sort(float[]) in java.util.Arrays match

总之,package与类别等可以用于管理一些资源,避免同名冲突发生,而"import"与"import staic"则是反其道而行,让您可以获得一些方便,如果同名冲突发生了,这种方便性的使用就有考虑的必要了。

51、异常处理

Java的例外处理藉由"try"、"catch"、"finally"三个关键词组合的语言来达到,其语法基本结构如下:

try {
	// 陈述句
}
catch(例外型态 名称) {
	// 例外处理
}
finally {
	// 一定会处理的区块
}

一个"try"所包括的区块,必须有对应的"catch"区块,它可以有多个"catch"区域,而"finally"可有可无,如果没有定义"catch"区块,则一定要有"finally"区块

先来看个实例,了解如何使用try...catch来处理使用者输入的错误:

UseException.java
import java.io.*;
public class UseException {
	public static void main(String[] args) {
		try {
			int input;
			BufferedReader buf = new BufferedReader(
			new InputStreamReader(System.in));
			System.out.print("请输入整数: ");
			input = Integer.parseInt(buf.readLine());
			System.out.println("input x 10 = " + (input*10));
		}
		catch(IOException e) {
			System.out.println("I/O错误");
		}
		catch(NumberFormatException e) {
			System.out.println("输入格式有误");
		}
	}
}

例外处理是程序在执行但发生错误并无法处理时,会丢出一个例外对象,在这个程序中,您特意 使用 BufferedReader 取得输入,当使用 BufferedReader类别时,若发生I/O错误会丢出IOException例外,这个例外您必须处理。

您试着从使用者输入取得一个整数值,由BufferedReader对象所读取到的输入是个字符串,您使用Integer类别的 parseInt()方法试着剖析该字符串为整数,如果无法剖析,则会发生错误并丢出一个NumberFormatException例外对象,当这个例外丢出后,程序会离开目前执行的位置,而如果设定的"catch"有捕捉这个例外,则会执行对应区块中的陈述句,注意当例外一但丢出,就不会再回到例外的丢出点了。

来看看这个程序的执行范例:

$ java UseException 
请输入整数: 10 
input x 10 = 100 
$ java UseException 
请输入整数: XX 
输入格式有误

如果程序中设定有"finally"区块,则无论例外是否有发生,则一定会执行"finally"区块中所定义的陈述句,"finally"区块使用时机 的例子之一,就是当您开启了某个档案时,在读/写的过程中发生错误,在使用"catch"区块处理相对应的例外之后,最后在"finally"区块中定义 一些关闭档案的动作,让关闭档案的动作一定会被执行。

使用例外处理的好处是您可以将程序逻辑与错误处理分开,使得程序易于撰写、阅读与维护,由于例外处理是在程序执行时发生错误,而没有办法处理之时才产生例外对象,所以与使用判断式来避免例外的方式比起来,例外处理会有比较好的执行效能。

52、throw、throws

当程序发生错误而无法处理的时候,会丢出对应的例外对象,除此之外,在某些时刻,您可能会想要自行丢出例外,例如在例外处理结束后,再将例外丢出,让下一层例外处理区块来捕捉,若想要自行丢出例外,您可以使用"throw"关键词,并生成指定的例外对象,例如:

throw new ArithmeticException();

举个例子来说明,在Java的除法中,允许除数为浮点数0.0,所得到的是Infinity,即无穷数,如果您想要自行检验除零错误,可以自行丢出例外,最接近这个条件的是ArithmeticException,当除数为整数且为0时,就会引发这个例外,您可以如下丢出例外:

UseThrow.java
public class UseThrow {
	public static void main(String[] args) {
		double dblzero = 0.0;
		try {
			System.out.println("浮点数除以零: "
			+ (100 / dblzero));
			if(dblzero == 0)
			throw new ArithmeticException();
		}
		catch(ArithmeticException e) {
			System.out.println("发生除零例外");
		}
	}
}

执行结果:

浮点数除以零: Infinity 
发生除零例外

每个例外都必须有一个"catch"区块来捕捉,在巢状的try...catch时,必须注意该例外是由何者引发并由何者捕捉,例如:

UseThrow.java
public class UseThrow {
	public static void main(String[] args) {
		try {
			try {
				throw new ArrayIndexOutOfBoundsException();
			}
			catch(ArrayIndexOutOfBoundsException e) {
				System.out.println(
				"ArrayIndexOutOfBoundsException/内层try-catch");
			}
			throw new ArithmeticException();
		}
		catch(ArithmeticException e) {
			System.out.println("发生ArithmeticException");
		}
		catch(ArrayIndexOutOfBoundsException e) {
			System.out.println(
			"ArrayIndexOutOfBoundsException/外层try-catch");
		}
	}
}

执行结果:

ArrayIndexOutOfBoundsException/内层try-catch 
发生ArithmeticException?

在这个程序中,ArrayIndexOutOfBoundsException由内层try-catch丢出并捕捉,由于内层 已经捕捉了例外,所以外层的try-catch中之ArrayIndexOutOfBoundsException并不会捕捉到内层所丢出的例外,但如果 内层的try-catch并没有捕捉到这个例外,则外层try-catch就有机会捕捉这个例外,例如:

UseThrow.java
public class UseThrow {
	public static void main(String[] args) {
		try {
			try {
				throw new ArrayIndexOutOfBoundsException();
			}
			catch(ArithmeticException e) {
				System.out.println(
				"ArrayIndexOutOfBoundsException/内层try-catch");
			}
			throw new ArithmeticException();
		}
		catch(ArithmeticException e) {
			System.out.println("发生ArithmeticException");
		}
		catch(ArrayIndexOutOfBoundsException e) {
			System.out.println(
			"ArrayIndexOutOfBoundsException/外层try-catch");
		}
	}
}

执行结果:

ArrayIndexOutOfBoundsException/外层try-catch

程序中会订定许多方法(Method),这些方法中可能会因某些错误而引发例外,但您不希望直接在这个方法中处理这些例外,而希望呼叫这个它的方法来统一处理,这时候您可以使用"throws"关键词来宣告这个方法将会丢出例外,例如:

private void arrayMethod(int[] arr)
			throws ArrayIndexOutOfBoundsException,ArithmeticException {
	// 实作
}

 注意如果会丢出多种可能的例外时,中间使用逗点分隔;当有方法上使用"throws"宣告例外时,意味着呼叫该方法的呼叫者必须处理这些例外,而被呼叫方法可以保持程序逻辑的简洁,下面这个范例是"throws"的一个简单示范:

UseThrows.java
public class UseThrows {
	public static void main(String[] args) {
		try {
			throwsTest();
		}
		catch(ArithmeticException e) {
			System.out.println("捕捉例外");
		}
	}
	private static void throwsTest()
	throws ArithmeticException {
		System.out.println("这只是一个测试");
		// 程序处理过程假设发生例外
		throw new ArithmeticException();
	}
}

执行结果:

这只是一个测试
捕捉例外

简单的说,您要不就在方法中直接处理例外,要不就在方法上宣告该方法会丢回例外,由呼叫它的呼叫者来处理例外,另一方面,在方法上使用 "throws"宣告可丢出的例外,也表示了您只能丢出所宣告类型的例外,其它的例外您必须在方法中处理完,或是重新包装为所宣告的例外再丢出。

如果使用继承时,在父类别的某个方法上宣告了throws某些例外,而在子类别中重新定义该方法时,您可以:

  • 不处理例外(重新定义时不设定throws)
  • 可仅throws父类别中被重新定义的方法上之某些例外
  • 可throws被重新定义的方法上之例外之子类别

但是您不可以:

  • throws出额外的例外
  • throws被重新定义的方法上之例外之父类别

51、断言(Assertion)

例外是程序中非预期的错误,例外处理是在这些错误发生时所采取的措施。

有些时候,您预期程序中应该会处于何种状态,例如某些情况下某个值必然是多少,这称之为一种断言(Assertion),断言有两种情况:成立或不成立。当预期结果与实际执行相同时,断言成立,否则断言失败。

Java在JDK 1.4之后提供断言陈述,有两种使用的语法:

assert <BOOLEAN_EXPRESSION>;
assert <BOOLEAN_EXPRESSION> : <DETAIL_EXPRESSION>;

boolean_expression如果为true,则什么事都不会发生,如果为false,则会发生 java.lang.AssertionError,此时若采取的是第二个语法,则会将detail_expression的结果显示出来,如果是个物 件,则呼叫它的toString()显示文字描述结果。

一个使用断言的时机是内部不变量(Internal invarant)的判断,例如在某个时间点上,或某个状况发生时,您判断某个变量必然要是某个值,举个例子来说:

AssertionDemo.java
public class AssertionDemo {
	public static void main(String[] args) {
		if(args.length > 0) {
			System.out.println(args[0]);
		}
		else {
			assert args.length == 0;
			System.out.println("没有输入自变量");
		}
	}
}

在正常的预期中,数组长度是不会小于0的,所以一但执行至else区块,数组长度必然只有一个可能,就是等于0,您断言args.length==0结果 必然成立,else之中的程序代码也只有在断言成立的状况下才能执行,如果不成立,表示程序运行存在错误,else区块不应被执行,您要停下来检查程序的错 误,事实上断言主要的目的通常是在开发时期才使用。

另一个使用断言的时机为控制流程不变量(Control flow invariant)的判断,例如在使用switch时,假设您已经列出了所有的可能常数:

...
switch(var) {
	case Constants.Con1:
	...
	break;
	case Constants.Con2:
	...
	break;
	case Constants.Con3:
	...
	break;
	default:
	assert false : "非定义的常数";
}
...

假设您已经在switch中列出了所有的常数,即var不该出现Constants.Con1、Constants.Con2、 Constants.Con3以外的常数,则如果发生default被执行的情况,表示程序的状态与预期不符,此时由于assert false必然断言失败。

总结就是,断言是判定程序中的某个执行点必然是某个状态,所以它不能当作像if之类的判断式使用,简单的说它不应是程序执行流程的一部份

52、

53、

54、

55、

56、

57、

58、

59、

 

posted @ 2012-11-24 12:15  ZH奶酪  阅读(2041)  评论(0编辑  收藏  举报