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; }
常数必须是可以直接取用,并且不可被修改的,所以我们在宣告时加上 static 与 final,事实上,对于接口来说,当中的常数预设就为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、
找我内推: 字节跳动各种岗位
作者:
ZH奶酪(张贺)
邮箱:
cheesezh@qq.com
出处:
http://www.cnblogs.com/CheeseZH/
*
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。