相关下载:
JDK: http://www.oracle.com/technetwork/java/javase/downloads/index.html
Eclips: http://www.eclipse.org/downloads/
废话少说,仅讨论Java与C++的区别。
基本类型
Java中的所有对象均是Object的子类且内存都是在堆中分配的,但基本类型(boolean、char、byte、short、int、long、float、double)是在栈中分配的。
boolean:与C++的bool类似。不同的是C++中bool实际上是一个整型,但Java中写不出这样的代码“boolean b = !0”。
每个基本类型都有对应的一个包装器类型:Boolean、Character、Byte、Short、Integer、Long、Float、Double、Void。这些包装类提供更丰富的方法,比如Integer.ParseInt。
操作符
Java是不支持操作符重载的。
Java中没有sizeof操作符,因为Java中所有的数据类型在所有机器中的大小都是相同的。
"=="仅比较对象的引用,但比较基本类型时是进行值的比较。看下面代码:
- Integer x = new Integer(7);
- Integer y = new Integer(7);
- boolean b = x == y;
-
- int i = 7;
- int j = 7;
- b = i == j;
如果要自定义比较,就需要覆盖Object类中的equal方法。
控制执行流程
Foreach 语法:
- int a[] = {1, 2, 3, 4, 5};
- for (int x: a)
- System.out.print(x);
break/continue label
语法(适用于嵌套循环中):
- int x = 10;
- int y = 5;
- outer:
- while (x-- > 0)
- {
- while (y++ < 10)
- {
- if (x + y == 9)
- break outer;
- }
- }
初始化与清理
所有局部变量都必须显式给一个初始值,使用未初始化的局部变量会得到编译错误。(C++中未初始的局部变量会是一个未定义的值。)
类的成员变量在未显式初始化时会得到默认的初始值,如int会初始化为0,而boolean初始化为false。
同样你也可以显式地初始化成员变量:
- public class MyClass
- {
- int a = 5;
- bool b = true;
- char c = 'c';
- SomeClass sc = new SomeClass();
- }
更有甚者你可以通过把一个方法的返回值作为一个成员变量的初始值:
- public class MyClass
- {
- int a = f();
- int b = g(a);
- int f() { return 10; }
- int g(int n) { return n * 3; }
- }
成员变量初始化的顺序取决于它们定义的顺序。上述代码中,a会先于b初始化。如果改变它们的顺序会出现编译错误:
- public class MyClass
- {
- int b = g(a);
- int a = f();
- }
当然成员变量也可以在构造器中初始化。
static成员变量的初始方法与非静态成员变量的初始化相同。
- public class MyClass
- {
- int a = 5;
- bool b = true;
- static char c = 'c';
- }
以上述代码为例,一个Java类的初始化顺序为:
1. 当MyClass对象被创建(new MyClass)或MyClass的静态方法被调用时,Java解释器会定位MyClass.class文件;
2.载入MyClass.class,初始化所有静态成员变量;
3.在new MyClass调用时,在堆上分配足够存储空间;
4.分配出的存储空间全部清零,这也是为什么成员变量在未显式初始化时会有默认的初始值;
5.执行所有成员变量的显式初始化;
6.执行构造器。
多个静态初始化可以组织成“静态子句”
- public class MyClass
- {
- int a = 5;
- bool b = true;
- static char c = 'c';
- static {
- double d = 4.0;
- int e = 7;
- }
- }
实例初始化
和静态子句相似,用来初始化非静态成员,并且可以保证在构造器之前调用,这在有多个构造器的时候是非常方便的:
- public class MyClass
- {
- int a;
- boolean b;
- {
- a = 5;
- b = true;
- System.out.println("look!");
- }
- }
数组
的定义可以是"int a[]"也可以是"int[] a"。你可以用“int a[] = new int[6];”的方式来初始化一个数组。
- Integer[] i = {
- new Integer(1),
- new Integer(2),
- 3
- };
等价于:
- Integer[] i = new Integer{
- new Integer(1),
- new Integer(2),
- 3,
- };
所以花括号的定义仍然是在堆中分配内存。
可变参数列表
是非常有趣的特性:
- void f(Object... args)
- {
- for (Object o: args)
- System.out.println(o);
- }
-
-
- f(1, 'c', false);
枚举不是一些简单的常数,它是可以用打印的。
- public enum E { A, B, C };
-
- E e = E.C;
- System.out.println(e);
Java中的对象是不需要手动清理的,垃圾回收器会(在适当的时候)清理所有没被引用的对象。当然,你也可以强制让垃圾回收器进行清理:
- void f()
- {
- MyClass mc = new MyClass();
- mc = null;
- System.gc();
- }
垃圾回收器的机制:
有一种所谓的“引用计数机制”,当对象被引用时计数器加一,当取消引用时计数器减一。当计数器为0时则释放该对象。这种模式非常慢,JVM实际上用一些更快的模式。
根据当前所有的引用(指针),肯定可以能找到它在堆中所指的对象。通过这种方法可以在堆中找到所有存活的对象。在清理时把这些“活”的对象复制到新堆中并紧密排列,并把所有的引用更新到新堆的地址,再把旧堆弃置。在处理过程中,当前的程序会停止,所以这种操作称为“停止-复制”(Stop-and-copy)。由于分配新内存时,堆里的对象是紧密排列的、连续的,所以只需要移动堆顶指针就可以得到一块新的内存,这样和在栈中分配内存的相似,所在java中的new操作非常高效。
由于stop-and-copy需要额外一个堆,所以要维护比实际需要大一倍的内存空间,此外复制操作也是很耗时的,当程序进入稳定状态时,只有少量的垃圾产生,但仍然复制所有的对象无疑是种浪费。所以有另一种模式“标记-清扫”(mark-and-sweep)。在这种模式下,仍然通过引用找到所有存活的对象,然后把所有死掉的对象释放掉,再把剩下的堆空间整理成连续的空间。这种方式仍然需要中止当前程序。
JVM综合上述两种模式,按需在堆中分配几块较大的内存,复制操作就发生在这些内存块之间。每个块都有表示它存活时间的“代数(generation count)”,当一个块被清理过一遍后它的代数会增加,一般清理会从代数最低的块开始,因为里面可能包含大量的临时变量。当“停止-复制”变得低效时,JVM会切换到“标记-清扫”模式。当后一个模式产生大量内存碎片,就切换回前一个模式。这就是“自适应技术”。
Java的对象可以覆写一个叫finalize的方法。这个方法会在GC释放一个对象之前被调用,且最多只能调一次。但是,“但是”,这个方法并不保证何时被调用,也不保证一定会调用。不能把它当成C++里的析构函数使用。如果需要做一些析构操作,需要定义其它方法,并且进行显式调用。finalize这种东西基本靠不住,不靠谱的,最好别用。
访问权限控制
Java中的“包(Package)”是一个类的集合。它是Java中的类库。包里的所有类都属于同一个命名空间。与C++的namespace类似,在java文件除注释外的首行使用语句“package name”定义一个命名空间name。使用语句“import name.className”来导入某类或“import name.*”为导入该空间中的所有类。静态导入“import static name.className.*"用来导入一个类中的所有静态方法与静态成员变量。如果没有用package语句声明的话,类会放入默认包(default package)中。
Java代码写在一个.java文件里,编译后每个类会生成一个.class文件。JVM可以解释运行这个.class文件。多个.class文件也可以打包成.jar文件,JVM同样这以解释运行这个.jar文件。在一个Java文件中,只能有一个public类,且该类名必须与文件名相同。其余定义在该文件中的类必须是只能是包可见,否则编译器会报错。
类成员的访问权限有四种:public、protected、包访问权限(没有关键字)和private。类本身的访问权限有两种:public和包访问权限(没有关键字)。
复用类
Java的继承语法是"class Derived extends Base {}"。未显式声明父类的类都会隐式继承Object类。在构造器中通过调用super()来初始化父类:“Derived() { super(); }”。如果不显式初始化父类,编译器会默认调用无参数的父类构造器。
C++中重载父类方法会隐藏重载的方法,而Java中不会。为了防止此类事件发生,可以在方法定义前加入“@Override“来进行修饰。
final修饰成员变量或局部变量时类似于C++中的const,表示只读。final成员变量可以在声明时初始化,也可以在构造器中初始化。static final表示全局只读变量。Java中所有的非静态方法都是虚函数,用final方法不能再被覆写。final类不能被继承。
接口
用abstract修饰一个类说明它是一个抽象类。只有在抽象类中才可以定义抽象方法(纯虚虚数),用“abstract void f();”表示。
用interface定义一个接口,接口里所有的方法都是public的抽象方法,但不需要abstract或public修饰。类实现一个接口的以这种形式:“class Implementorimplements Interface {}”。接口可以继承自另一个接口:“interface DerivedInterfaceextends Interface”。
接口里是可以定义数据的,这些数据隐式声明为public static final:
- public interface Interface {
- int ONE = 1, TWO = 2, Three = 3;
- }
接口里可以定义嵌套接口:
- public interface Outer {
- public interface Inner {
- void f();
- }
- }
-
- class Implementor implements Outer.Inner {
- pubilc void f() {}
- }
内部类
Java的内部类很有趣。你可以这样定义一个内部类:
- class Outer {
- class Inner {}
- }
Inner是必须与某一个Outer对象绑定的,通过外部创建内部类的方法就可以得知:
- Outer oo = new Outer();
- Outer::Inner ii = oo.new Inner();
所以每一个Inner都有对应的一个外部类对象,而且内部类可以访问外部类对象中的所有属性和方法,包括private权限的。内部类可以用Outer.this来得到外部类对象的引用:
- class Outer {
- class Inner {
- Outer GetOuter()
- {
- return Outer.this;
- }
- }
- }
在方法中或一个作用域“{}”中也可以定义内部类,用一段比较变态的代码来说明问题:
- class Outer {
- int a = 6
- class Inner {
- int b = 3;
- void f()
- {
- class FunInner {
- int sum()
- {
- return a + b;
- }
- }
- FunInner fi = new FunInner();
- System.out.print(fi.sum());
- }
- }
- }
还有一种东西叫“匿名内部类”:
- interface I {
- void f();
- }
-
- class Outer {
- I getI()
- {
- return new I() {
- public void f() {}
- };
- }
- }
如果不需要和外部类绑定,可以定义一个嵌套类:
- class Outer {
- static class Nested {}
- }
嵌套类和C++中的嵌套类基本相同,只能访问外部类的静态数据。它的创建方法为:
- Outer.Nested nested = new Outer.Nested();
接口里面也可以定义嵌套类,但不需要static修饰,因为接口里定义的变量都是默认为public static的:
- interface I {
- void f();
- class MyI implements I {
- void void f() {}
- }
- }
这样做的好处是接口的定义同时还能提供默认的实现。
内部类的作用:
内部类可以继承别的类,同时又可以完全访问外部类的成员,这就等同于多重继承的效果。
Java里没有指针,所以不能像C++一样把函数指针传递给出去作为回调函数(callback)。通过内部类可以实现:
- interface Callback { void call(); }
-
- class MyClass {
- void f() {}
- Callback getCallback()
- {
- return new Callback() {
- void call() { f(); }
- };
- }
- }
-
- class Caller {
- void operation(Callback callback)
- {
- callback.call();
- }
-
- static void main(String[] args)
- {
- Caller caller = new Caller();
- MyClass mc = new MyClass();
- caller.operation(mc.getCallback());
- }
- }
Java中的这样的回调机制叫做
闭包(closure)
。
内部类也可以被继承。因为外部类和内部类有着千丝万缕的联系,所以继承的类同样需要某种机制建立起这种联系:
- class Outer { class Inner{} }
- public class InheritInner extends Outer.Inner {
-
- InheritInner(Outer oo) {
- oo.super();
- }
- static void main(String[] args)
- {
- Outer oo = new Outer();
- InheritInner ii = new InheritInner(oo);
- }
- }
内部类同样会生成.class文件,它的文件名会是这样的形式:Outer$Inner.class。如果是匿名内部类,在$后会简单加入一个数字作为标识符。
持有对象
这里仅概述Java中的容器。Java也是支持范型的,比如“ArrayList a = new ArrayList”。不带范型参数的容器默认为Object对象的集合。
容器包括两大类:Collection和Map,它们包括:
Collection:List(ArrayList、LinkedList)、Set(HashSet、TreeSet、LinkedHashSet)、Queue(PriorityQueue)。
Map:HashMap、TreeMap、LinkedHashMap。
Collection提供一个iterator()方法来返回一个迭代器(Iterator),一个Iterator要包含以下方法:hasNext()、next()和remove()(可选)。
为了支持Foreach的语法,一个类必须实现Iterable接口。这个接口有一个方法iterator()来得到一个迭代器。Collection实现了这个接口。下面的代码定义了支持Foreach的类:
- class MyCollection implements Iterable {
- public Iterator iterator()
- {
- return new Iterator() {
- int i = 0;
- public boolean hasNext() { return i < 10; }
- public Integer next() { return i++; }
- public void remove() { throw new UnsupportedOperationException(); }
- };
- }
- public static void main(String[] args)
- {
- MyCollection mc = new MyCollection();
- for (int i: mc)
- System.out.print(i);
- }
- }
通过异常处理错误
Java中可以通过throw抛出所有继承Throwable的类。任何抛出的异常(RuntimeException除外)被编译器强制需要catch,如果不catch会出现编译错误。
如果需要把异常抛到方法外,则
需要在方法定义中声明该异常
:
- void makeException() throws MyException
- {
- throw new MyException();
- }
这样编译器才会让你通过,但是
所有调用了这个方法的地方必须catch这个被声明的异常
,否则编译不通过,(当然也可以通过上述声明异常的方式把异常抛到更外层)。相比之下,C++的异常声明更像一个提示,不带有强制性质。
Throwable分为两种类型:Error和Exception。Error用来表示编译时和系统错误,所以我们一般不用关心。Exception类有一个方法printStackTrace可以打印出异常抛出点的调用栈,另一个函数fillStackTrace可以在catch后更新调用栈信息再抛出。
RuntimeException是Exeption的子类,用来表示运行时发现的编程错误,比如访问空指针(NullPointerException)和数组越界(ArrayIndexOfBoundsException)。这种类型的异常是唯一不被编译器强制需要捕获的。如果一个RuntimeException抛出后没有被catch,程序会退出并调用printStackTrace方法。
使用finally块可以保证不管是否有异常发生,该块的代码都会被运行:
- try
- {
-
- }
- catch (Exception e)
- {
- }
- finally
- {
-
- }
finally也可以用来保证有多个return点的函数可以执行必要的清理工作,这也弥补了没有析构的不足。
有时候你可能需要把一些异常串成一个异常链。Exception.initCause可以支持这个需求:
-
- class LevelOneException extends Exception {}
- class LevelTwoException extends Exception {}
-
- class ExceptionMaker {
- void makeLevelOne() throws LevelOneException
- {
- throw new LevelOneException();
- }
- void makeLevelTwo() throws LevelTwoException
- {
- try {
- makeLevelOne();
- }
- catch (LevelOneException loe)
- {
- LevelTwoException lte = new LevelTwoException();
- lte.initCause(loe);
- throw lte;
- }
- }
- void Test()
- {
- try {
- makeLevelTwo();
- }
- catch (LevelTwoException lte)
- {
- Throwable t = lte.getCause();
- }
- }
- }
在C++里,当一个异常没有被Catch的情况下,再抛出一个新的异常,比如析构函数里的异常,这个程序基本就over了。但在Java中,这种情况会导致旧的异常丢失,新的异常被传递出去:
- void loseException() {
- try {
- try {
- throw new LevelOneException();
- } finally {
- throw new LevelTwoException();
- }
- } catch (LevelTwoException lte) {
- lte.printStackTrace();
- }
- }
这应该算是Java的一个缺陷吧。
在覆写父类方法时,子类方法中的异常声明必须是父类声明的异常的子集。就是说子类的方法只能抛出比父类更少的异常,而不能抛出父类不会抛出的异常。
字符串
Java中的String类方便的操作字符串。它是唯一重载操作符(+、+=)的类。可以这样拼接字符串:String s = new String("ADD"); s += "More"; s += "More" + "And" + "More";可以注意到"More"+"And"+"More"看起来可能会产生很多临时变量,但Java对它进行了优化。上面的加号操等价于"StringBuilder sb = new StringBuilder(); sb.append("More"); sb.append("And"); sb.append("More"); s = sb.toString();
Object类定义了一个toString方法,它的实现是返回对象类名以及内存地址。所以所有的类都可以转换成String而且也可以实现自己的toString。
有一个类Fomatter(java.util.Formatter),它是专门用来格式化字符串的,和C里面的printf相似:
- void format()
- {
- PrintStream o = System.out;
- Formatter f = new Formatter(o);
- f.format("%d + %d = %d", 1, 1, 2);
- }
PrintStream
类(如System.out)里也定义了一个
format
方法和一个
printf
方法。它们是等价的。它们的内部实现也是借助了Formatter类。
String
的
format
方法也同样利用了Formatter类。
String支持正则表达式。String的matches、split、replaceFirst和replaceAll等方法都和正则表达式有很好的交互。java.lang.regex包中的Pattern类更好地支持正则表达式。这里不多做研究,或许将来单独开一篇博客来探讨正则表达式。
类型信息
Java中的每一个类都对应着一个Class对象(java.lang.Class)。通过这个Class对象你可以在运行时得到很多类中的有用的信息。用Class.forName来得到一个Class对象。
- try {
- Class c = Class.forName("MyClass");
- String name = c.getName();
- name = c.getSimpleName();
- name = c.getCanonicalName();
- Class superClass = c.getSuper();
- if (superClass != null)
- superClass.newInstance();
- boolean b = c.isInterface();
- for(java.lang.Class face: c.getInterfaces())
- name = face.getName();
- } catch (ClassNotFoundException) {}
除了Class.forName,还可以直接用MyClass.class来得到一个Class对象。这种方式不会抛出异常。所有的类,包括基本类型,都可以使用“.class”。比如int.class。基本类型的包装类有TYPE成员,比如Integer.TYPE与int.class是一样的。
不像C++在程序启动时就把所有的静态数据与执行代码载入到内存中,java根据需要在运行时把字节码载入到内存,它分三个步骤:1、加载:类加载器查找到字节码(.class文件)并根据这些字节码创建一个Class对象;2、链接:验证类中的字节码,为静态域分配存储空间,需要的话同时解析这个类其它类的所有引用;3、初始化:当类的静态方法(构造器是特殊的静态方法)或者非常数静态域(即不是编译器常量)被首次引用时,执行静态初始化块和初始化静态数据。
Class类是支持范型的:
Class范型的作用是可以得到编译器的支持,比如类型检查:
除了类型检查,Class范型的new Instance会返回相应类型的对象,不仅仅是个简单的Object:
- Class c = int.class;
- Class cc = int.class;
- Object o = c.newInstance();
- Integer i = cc.newInstance();
这种范型强制性太大,如果希望一个Class范型的引用接受别的Class类型的对象,可以使用
通配符
:
- Class> c = int.class;
- c = double.class;
Class>其实与普通的Class类是一样的,只不过显式声明你确实需要一个接受任何类型的Class对象,而不是忘了加范型参数。
如果你想让一个Class范型类既能接受int.class,又能接受double.class,但是不想接受其它非数值的类型,可以这样:
- Class extends Number> c = int.class;
- c = double.class;
- Number n = c.newInstance();
?extends
通配符可以让编译器确保接受的类型是某个类型的子类。另一个通配符
?super
可以得保证接受的类型是某个类的超类:
- class Base {}
- class Derived extends Base {}
-
- Class super Derived> c = Derived.class.getSuperclass();
- Object o = c.newInstance();
看到上例的Class不能接受子类的getSuperClass的返回值,还是挺奇怪的,毕竟Base是Derived的基类是在编译时就确定的。不过既然编译器规定Class super Derived>到Class的转换,那也只能遵从了。
Class范型提供一个cast方法:
- Base b = new Derived;
- Class c = Derived.class;
- Derived d = c.cast(b);
这样的cast其实与Derived d = (Derived)b;是完全等价的,所以这样的cast基本不怎么用。如果被转换的类不能被cast到目标类型的话,会抛出一个ClassCastException异常。(C++里cast是不会使用RTTI的。)另一个基本没用的方法是Class.asSubClass:
- Class extends Base> c = Derived.class.asSubclass(Base.class);
- Derived d = (Derived)c.newInstance();
另一个在运行时得到类型信息的方法是关键字
instanceof
它与
Class.isInstance
是等价的:
- Base o = new Derived();
- boolean b = o instanceof Derived;
- b = Derived.class.isInstance(o);
以上介绍的都是java的
RTTI机制
。Java还有一套
反射机制
。RTTI能够维护的类型都是编译时已知的类型,而反射可以使用一些在编译时完全不可知的类型。比如在进行一个远程调用时,类信息是通过网络传输过来的,编译器在编译时并不知道这个类的存在。下面演示如何使用反射:
- import java.lang.reflect.*;
-
- class SomeClass {
- public SomeClass() {}
- public int SomeMethod(double d, char c) { return 2; }
- public int a;
- }
-
- public class ReflectTest {
- public static void main(String[] args) {
- Class c = Class.forName("SomeClass");
- for (Constructor> constructor: c.getConstructors())
- System.out.println(constructor);
- for (Method method: c.getMethods())
- System.out.println(method);
- for (Field field: c.getFields())
- System.out.println(field);
-
- SomeClass sc = new SomeClass();
- Method method = c.getMethod("SomeMethod", double.class, char.class);
- Integer returnedValue = (Integer)method.invoke(sc, 3, '4');
- Field field = c.getField("a");
- int value = field.getInt(sc);
- System.out.println(value);
- }
- }
其实反射和RTTI并没有什么本质的区别,因为java的类都是在运行是加载并解析的,而且两者通过Class对象来获取类型信息。不同的地方就是RTTI可以直接使用方法名来调用一个方法,而不必用字符串去执行一个方法。
设计模式里有个"代理模式"(点击链接进入博客:设计模式 —— 《Head First》)。代理模式里会定义一个接口,真正工作的类和代理类都会实现这个接口,但用户只会看到代理类,而不知道真正工作的类。这个模式的好处就是可以隐藏实现细节,经常改动的地方对用户是不可见的。Java里提供了一个自动生成代理类的机制,主要使用java.lang.reflect包里的Proxy类和InvocationHandler接口:
- import java.lang.reflect.*;
-
- interface MyInterface {
- void doSomething();
- }
-
- class RealWorker implements MyInterface {
- public void doSomething() { System.out.println("RealWorker"); }
- }
-
- class MyHandler implements InvocationHandler {
- public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- return method.invoke(worker, args);
- }
- private MyInterface worker = new RealWorker();
- }
-
- public class ProxyTest {
- public static void main(String[] args) {
- MyInterface myProxy = (MyInterface)Proxy.newProxyInstance(
- MyInterface.class.getClassLoader(),
- new Class[] {MyInterface.class}, new MyHandler());
-
- myProxy.doSomething();
- }
- }
可以看到我们只定义了一个接口和实现这个接口的类,但没有直接定义一个代理类,而是通过实现
InvocationHandler
和使用
Proxy.newProxyInstance
让编译器自动生成一个
Proxy
类。
通过反射机制可以做一些很违反规定的事情。
你可以使用一个某个包里不对外开放的类,以及它的私有方法
:
-
- package hidden;
-
- class HiddenClass {
- private void invisible() { System.out.println("invisible"); }
- }
-
-
- import java.lang.reflect.*;
- import hidden.*;
-
- public class HiddenClassTest {
- public static void main(String[] args) {
- Class c = Class.forName("hidden.HiddenClass");
-
-
- Constructor constructor = c.getDeclaredConstructor();
- constructor.setAccessible(true);
- Object obj = constructor.newInstance();
-
- Method method = c.getDeclaredMethod("invisible");
- method.setAccessible(true);
- method.invoke(obj);
- }
- }
当然这样的做法是很不值得提倡的。除了普通的类,同样可以用
Class.forName("OuterClass$InnerClass")
的方式来访问内部类。访问匿名类则用
Class.forName("OuterClass$1")
的方式。有一点值得注意的是,内部类和匿名类的构造函数的第一个参数是外部类的引用,所以getDeclaredConstructor方法的以外部类的类型作为第一个参数。这里就不再列出代码了。
范型
java里也有范型的概念,可以自定义范型类、范型接口、范型内部类和范型方法等:
- class MyGeneric { public T value; }
- class MyOtherGeneric { void f(A a, B b) {} }
- interface MyGenericInterface { T getT(); }
- class NormalOuter {
- class GenericInner {}
- public void GenericMethod(T value) {}
- }
注意范型不能接受基本类型作为参数,但是可以使用包装类。比如不能使用MyGeneric但可以使用MyGeneric。
java中的范型使用了一种“擦除”的机制,即范型只在编译期存在,编译器去除了(不是替代)范型参数,而java虚拟机根本不知道范型的存在。带有不同的范型参数的范型类在运行时都是同一个类:
- MyGeneric mg1 = new MyGeneric();
- MyGeneric mg2 = new MyGeneric();
- boolean b = mg1.getClass() == mg2.getClass();
因为范型类在运行时最终都会被擦除成普通的类,所以不能定义两个类名相同,而范型参数不同的类:
- class MyGeneric { public T value; }
-
更悲惨的是,在运行时是无法确切知道范型参数的实际类型。接着上面的代码:
- MyOtherGeneric mog = new MyOtherGeneric();
- TypeVariable>[] types = mog.getClass().getTypeParameters();
- System.out.println(Arrays.toString(types));
getTypeParameters这个方法只是返回范型参数的占位符,不是具体的某个类型。
java的范型在定义时不是假定范型参数支持哪些方法,尽管C++是可以做到的。看下面的代码:
如果要让上面的代码编译通过,则需要使用到边界:
- class SupportT { void f() {} }
- class MyGenericextends SupportT> {
- public void GuessT(T t) { t.f(); }
- }
java中之所以用擦除的方式来实现范型,而不是像C++一样根据不同的范型参数具现化不同的类,是由于java语言在发布初期并不支持范型,在发布很长时间后才开始引入范型,为了很原来的类库兼容才使用了擦除。擦除会把参数T换成Object。
因为擦除,在范型类里是不能创建范型参数类型的对象,因为编译器不能保证T拥有默认构造器:
- class ClassMaker {
- T create() {
-
- }
- }
要创建一个范型对象,可以利用Class类型:
- class ClassMaker {
- ClassMaker(Class c) { type = c; }
- T create() {
- T t = null;
- try { t = type.newInstance(); }
- catch (InstantiationException e) { e.printStackTrace(); }
- catch (IllegalAccessException e) { e.printStackTrace(); }
- return t;
- }
- private Class type;
- }
同样,如果要创建范型数组的话:
- class ClassMaker {
- ClassMaker(Class c) { type = c; }
- T[] createArray(int size) {
-
-
- return (T[])Array.newInstance(type, size);
- }
- private Class type;
- }
范型的边界(extends)可以加入多个限定,但只能是一个类和多个接口:
- interface face1 {}
- interface face2 {}
- class class1 {}
- class Boundextends class1 & face1 & face2> {}
在子类还能加入更多的限定:
- interface face3 {}
- class BoundDerivedextends class1 & face1 & face2 & face3> extends Bound {}
试试把一个子类的数组赋给父类数组的引用:
- class Base1 {}
- class Derived1 extends Base1 {}
- Base1[] base = new Derived1[3];
- base[0] = new Base1();
父类数组的引用实际指向一个子类的数组,当给元素赋值时,传入一个父类对象对得到一个异常,因为它期望的是一个子类的类型。
如果你希望在编译期时就发现这样的错误,而不是等到异常发生时才发现,可以使用范型:
这样你在编译时就会发现这个错误。
有时候你可能就是希望一个合法的向上转型,这时可以使用通配符:
- ArrayList extends Base1> aleb = new ArrayList();
-
-
-
- aleb.add(null);
- Base1 b = aleb.get(0);
可以发现带有通配符参数的范型类,它的所有带范型参数的方法调用都不能通过编译,比如上例中的ArrayList.add(T)。甚至连Object类型都不能接受,只能接受null。不带范型参数的方法可以调用,如ArrayList.get。这是因为编译器不知道应该接受什么类型,所以干脆就什么类型都不接受。如果你希望你的范型类在参数是通配符的时候,它的某些方法仍然能被调用,则定义方法的参数类型为Object,而非范型类型T。
与通配符相对的是
超类型通配符
,即
super T>
- ArrayList super Derived1> alsb = new ArrayList();
- alsb.add(new Derived1());
-
- Object d = alsb.get(0);
可以看到在接受参数时限制放宽了,因为编译器知道范型的下界,只要是Derived类或它的子类都是合法的。但是在返回时,它只能返回Object类型,因为它不能确定它的上界。
无界通配符
,即
>
,与
原生类型
(非范型类)大体相似,但仍有少许不同:
- ArrayList> al_any = new ArrayList();
-
- Object obj = al_any.get(0);
-
- ArrayList al_raw = new ArrayList();
- al_raw.add(new Object());
- Object obj2 = al_raw.get(0);
调用add时,无界通配符范型不接受任何类型的参数,而原生类型可以接受Object类型但会发出一个警告。
有一种看起来很诡异的范型参数定义,即自限定的类型。它是这样的形式:
- class SelfBoundedextends SelfBounded> {
- void f(T t) {}
- }
看起来很复杂,先把范型参数提出来研究下:T extends SelfBounded,它表明了一种类型,这种类型是SelfBounded的子类,而SelfBounded的范型参数恰恰就是这个类型本身。举个例子:
- class Derived2 extends SelfBounded {}
Derived2这个类就是以Derived2为参数的SelfBounded范型的子类,而SelfBounded类只能接受如Derived2这样特殊的类型作为范型参数。T extends SelfBounded其实是限定了一个继承关系,下面的定义是不合法的:
- class OtherType {}
- class Derived3 extends SelfBounded {}
因为OtherType并不是SelfBounded的子类,所以不能作为SelfBounded的合法范型参数。
自限定类型一般用于参数协变。Java是支持返回值协变的:
- class BaseFace { Base1 getObject() { System.out.println("BaseFace"); return new Base1(); } }
- class DerivedFace extends BaseFace { Derived1 getObject() { System.out.println("DerivedFace"); return new Derive1(); } }
子类的同名方法覆盖了父类的方法,不是重载。下面的代码可以证明:
- void test(DerivedFace df) {
- Derived1 d1 = df.getObject();
- BaseFace bf = df;
- Base1 b1 = bf.getObject();
- }
有趣的是,你可以调用DerivedFace.class.getMethods()看看,Derived类一共定义了两个方法,和普通的覆盖又有不同。
尽管支持返回值协变,Java并不支持参数协变。改写父类和子类的定义:
- class BaseFace { setObject(Base1 b) { System.out.println("BaseFace"); } }
- class DerivedFace extends BaseFace { void setObject(Derived1 d) { System.out.println("DerivedFace"); } }
- void test(DerivedFace df) {
- Derived1 d1 = new Derived1();
- df.setObject(d1);
- df.setObject((Base1)d1);
- BaseFace bf = df;
- bf.setObject(d1);
- }
自限定类型很好的支持了参数协变。
- class BaseFace2extends BaseFace2> { void setObject(T t) {} }
- class DerivedFace2 extends BaseFace2 {}
- void test(DerivedFace2 df, BaseFace2 bf) {
- df.setObject(df);
-
- bf.setObject(bf);
- bf.setObject(df);
- }
可以看到自限定类型的子类只能接受子类作为参数,而且方法是被覆盖的,而不是重载。
Java容器的插入方法并不检查类型,所以很可能会插入错误的类型:
- List l_raw = new ArrayList();
- l_raw.add(new Double(3));
Collections提供了检查类型的方法,比如checkedList返回一个在插入时检查类型的List:
- List l = Collections.checkedList(new ArrayList(), Integer.class);
- List l_raw = l;
- l_raw.add(new Double(3));
在checked容器里试图插入不合法的类型会抛出异常。
数组
Java中的数组是在堆中分配的连续内存,它可以用来存放基本类型的值或对象类型的引用。除了操作符“[]”外,数组同样还有一个length属性,这和C++里的数组不同。
我们还可以使用多维数组:
- int a[][][] = new int[2][3][2];
- a[1][2][1] = 5;
数组内所有元素都会被赋一个默认初值(null、0、false等)。因为数组是在堆中初始化的,所以可变长度的维度是可行的。这样的数组被称为
粗糙数组
:
- int a[][][] = new int[2][][];
- a[0] = new int[3][];
- a[1] = new int[5][];
- a[0][1] = new int[6];
- a[1][2] = new int[8];
- System.out.println(Arrays.deepToString(a));
Arrays.deepToString
方法可以将多维数组转成字符串。同样你也可以用数组初始化的方式来定义粗糙数组:
- int a[][][] = new int[][][] {
- { {1,2,3,4}, {5,6} },
- { {7}, {8}, {9,10,11}, {12} },
- { {13,14}, {15,16}, {17,18} }
- };
Java提供了一些对数组的操作,比如fill、equal、copy等。下面演示下:
Arrays.fill:
- void fillArray() {
- int a[] = new int[5];
- Arrays.fill(a, 3);
- System.out.println(Arrays.toString(a));
- int b[] = new int[8];
- Arrays.fill(b, 2, 6, 3);
- System.out.println(Arrays.toString(b));
- }
System.arraycopy
:
- void copyArray() {
- int a[] = {1, 2, 3, 4, 5};
- int b[] = new int[a.length+1];
- System.arraycopy(a, 0, b, 1, a.length);
- System.out.println(Arrays.toString(b));
- }
Arrays.equals
:
- void equalsArray() {
- int a[] = {1, 2, 3, 4, 5};
- int b[] = {1, 2, 3, 4, 5};
- Arrays.equals(a, b);
- }
还有数组的排序,对象类型并须实现
Comparable
接口才可以用
Arrays.sort
排序,否则在会得到异常。示例代码:
- class MyElement implements Comparable {
- public int compareTo(MyElement other) {
- return this.value - other.value;
- }
- static MyElement createNext() {
- MyElement me = new MyElement();
- me.value = r.nextInt(20);
- return me;
- }
- public String toString() {
- return String.valueOf(value);
- }
- private int value;
- private static Random r = new Random();
- }
-
- static void sortArray() {
- MyElement meArray[] = new MyElement[8];
- for (int i = 0; i < meArray.length; ++i) {
- meArray[i] = MyElement.createNext();
- }
- System.out.println(Arrays.toString(meArray));
- Arrays.sort(meArray);
- System.out.println(Arrays.toString(meArray));
- }
如果你对Comparable的排序不满意,你可以自己定义一个
Comparator
:
- class MyElementComparator implements Comparator {
- @Override
- public int compare(MyElement left, MyElement right) {
- return right.compareTo(left);
- }
- }
- void reverseSortArray() {
- MyElement meArray[] = new MyElement[8];
- for (int i = 0; i < meArray.length; ++i) {
- meArray[i] = MyElement.createNext();
- }
- System.out.println(Arrays.toString(meArray));
- Arrays.sort(meArray, new MyElementComparator());
- System.out.println(Arrays.toString(meArray));
- }
对于排好序的数组,可以使用二分法来查找元素。
Arrays.binarySearch
:
- void binarySearchArray() {
- int a[] = { 3, 5, 2, 4, 9, 7, 1 };
- Arrays.sort(a);
- int index = Arrays.binarySearch(a, 7);
- }
编译器不允许你创建一个范型数组,但是允许你使用范型数组的引用:
-
- List liArray[] = new List[8];
-
容器深入研究
Java容器分为两大类Collection和Map,它们分别对应着两个接口。我们先来研究Collection这一大类。
Collection又分为三类List、Set和Queue,它们三个同样对应着三个接口。看一下Collection接口的定义:
- interface Collection {
- boolean add(T e);
- boolean addAll(Collection extends T> c);
- void clear();
- boolean contains(Object o);
- boolean containsAll(Collection> c);
- boolean isEmpty();
- Iterator iterator();
- boolean remove(Object o);
- boolean removeAll(Collection> c);
- boolean retainAll(Collection> c);
- int size();
- Object[] toArray();
- T[] toArray(T[] a);
- }
这些方法从字面上看都很理解。其中retainAll与removeAll相对,只保留参数中的元素。同时它还可以返回一个迭代器
Iterator
。
再看看List接口的定义:
- interface List extends Collection {
- void add(int index, T element);
- boolean addAll(int index, Collection extends T> c);
- T get(int index);
- int indexOf(Object o);
- int lastIndexOf(Object o);
- ListIterator listIterator();
- ListIterator listIterator(int index);
- T remove(int index);
- T set(int index, T element);
- List subList(int fromIndex, int toIndex);
- }
List加入了对index的支持。同时它还可以另一个迭代器类型
ListIterator
。
接下来是Set接口的定义:
- interface Set extends Collection {
- }
它没有加入任何新的方法。
还有Queue接口的定义:
- interface Queue extends Collection {
- T element();
- boolean offer(T e);
- T peek();
- T poll();
- T remove();
- }
element
方法和
peek
方法都是获取队首元素,不同的是当队列为空是element抛出异常(NoSuchElementException)而peek返回null。
poll
和
remove
都是返回并删除队首元素,当队列为空时remove抛出异常而poll返回null。offer方法类似于add。它在不违反队列容量限制的情况下试图插入一个元素,不成功的话返回false而不抛出异常(add方法会抛出异常IllegalStateException)。
虽然这些接口定义了各种方法,但实现它们的类并不必支持这些方法。实现接口的类在方法的实现中抛出UnSupportedOperationException来表示它不支持该方法。比如一个ArrayList的长度是可以变化的,但是如果由Arrays.asList()得到的话,那它就不再支持改变长度的方法了:
- List arrayAsList = Arrays.asList(1, 3, 5);
- List modifiableList = new ArrayList(arrayAsList);
- modifiableList.clear();
- arrayAsList.set(2, 3);
- arrayAsList.clear();
UnSupportedOperationException是一个RuntimeException,它表示一个编程错误,其实和C++里的Assert一样。
Collections.unmodifiableList
方法会得到一个只读的List:
- List unmodifiableList = Collections.unmodifiableList(modifiableList);
- unmodifiableList.set(2, 3);
Collections.unmodifiableList返回的其实是一个内部类Collections$UnmodifiableRandomAccessList的一个实例。
相应的,Collections.synchronizedCollection、Collections.synchronizedList、Collections.synchronizedMap、Collections.synchronizedSet、Collections.synchronizedSortedMap和Collections.synchronizedSortedSet会返回支持线程同步的容器。它们也都是Collections的内部类。
每种类型的接口都有一个Abstract类的实现,比如Collection接口被AbstractCollection实现。AbstractCollection实现了Collection里绝大多数的方法,除了iterator()方法和size()方法。那些被实现的方法仅仅是抛出一个UnSupportedOperationException。Abstract容器类存在的价值在于,当你需要写一个容器类,可以继承自这个Abstract容器类并覆盖你仅需要支持的方法,其它的不需要支持的方法可以继续抛出异常。这样你就不必耗费大量精力去实现容器接口的所有方法。相应的,你还可以发现AbstractList、AbstractSet、AbstractMap等Abstract容器类。Java类库里所有的具体容器类都继承自某个Abstract容器类。
现在来看看Iterator类。一个Collection都会返回一个Iterator用于遍历这个容器。看看Iterator的定义:
- interface Iterator {
- boolean hasNext();
- T next();
- void remove();
- }
当要遍历一个容器时,用hasNext判断是否有下一个元素,用next取得下一个元素,如果没有下一个元素next方法会抛出NoSuchElementException。通过remove删除当前元素,删除后当前的元素不再可用。有些Iterator不支持remove,这种情况下这个方法会简单抛出一个UnSupportedOperationException。
一旦容器被修改,之前获得的Iterator就会失效。如果这时还试图访问这个Iterator就会得到异常:
- Collection c = new ArrayList();
- c.add(1);
- Iterator itor = c.iterator();
- c.add(3);
- if (itor.hasNext())
- itor.next();
List可以返回一个
ListIterator
类型。看看它的定义:
- interface ListIterator {
- void add(T e);
- boolean hasPrevious();
- int nextIndex();
- T previous();
- int previousIndex();
- void set(T e);
- }
可以看到ListIterator支持了向前和向后遍历,同时还支持添加元素和修改当前元素的操作。
实现List接口的类有ArrayList和LinkedList。一个用数组实现,另一个用双向链表实现。除了这两个类外,还有早期开发的不再推荐使用的Vector和Stack类。
实现Set接口的类有HashSet、TreeSet和LinkedHashSet。
HashSet用散列的方式存储集合元素。它支持快速查找。放入HashSet的元素必须支持hashCode和equals方法。
LinkedHashSet可以达到HashSet一样的查找速度,同时内部维护了一个链表。当使用跌代器遍历LinkedHashSet时,遍历的顺序和元素插入的顺序一样。放入LinkedHashSet的元素同样需要支持hashCode。
TreeSet使用红黑树实现集合,它对内部元素排序。在遍历元素时按照由小到大的顺序。放入TreeSet的元素必须实现Comparable接口。TreeSet实现了SortedSet接口:
- interface SortedSet {
- Comparator super T> comparator();
- T first();
- SortedSet headSet(T toElement);
- T last();
- SortedSet subSet(T fromElement, T toElement);
- SortedSet tailSet(T fromElement);
- }
first方法返回集合中最小的元素。last返回最大的元素。headSet、subSet和tailSet返回子集。
实现Queue接口的类有LinkedList和PriorityQueue。
其实LinkedList同时实现了List接口和Deque接口。Deque是双向队列:
- interface Deque extends Queue {
- void addFirst(T e);
- void addLast(T e);
- Iterator descendingIterator();
- T getFirst();
- T getLast();
- boolean offerFirst(T e);
- boolean offerLast(T e);
- T peekFirst();
- T peekLast();
- T pollFirst();
- T pollLast();
- T pop();
- void push(T e);
- T removeFirst();
- boolean removeFirstOccurrence(Object o);
- T removeLast();
- boolean removeLastOccurrence(Object o);
- }
PriorityQueue
根据优先级确定下一个移出对列的元素。放入PriorityQueue的元素需要实现Comparable接口,最小的元素会先在队首。看下面的示例代码:
- PriorityQueue pq = new PriorityQueue();
- pq.offer(5);
- pq.offer(13);
- pq.offer(9);
- pq.offer(12);
- pq.offer(6);
- while (!pq.isEmpty()) {
- System.out.println(pq.poll());
- }
接下来看看Map接口的定义:
- interface Map {
- void clear();
- boolean containsKey(Object key);
- boolean containsValue(Object value);
- Set> entrySet();
- V get(Object key);
- boolean isEmpty();
- Set keySet();
- V put(K key, V value);
- void putAll(Map extends K, ? extends V> m);
- V remove(Object key);
- int size();
- Collection values();
- }
注意到
entrySet
方法返回一个
Map.Enry
的集合。Map.Entry也是一个接口:
- Map.Entry {
- K getKey();
- V getValue();
- V setValue(V value);
- }
还可以看到Map通过
values
方法转成一个Collection类型。
实现Map接口的类有
HashMap
、
LinkedHashMap
、
TreeMap
、
WeakHashMap
、
ConcurrentHashMap
和
IdentityHashMap
等。
其实可以把Map看成特殊的Collection。Collection里的元素是一个value而Map里的元素是一个键值对(Entry)。从这种角度看
HashMap
、
LinkedHashMap
和
TreeMap
与HashSet、LinkedHashSet和TreeSet其实大同小异。其中TreeMap同样也实现了一个
SortedMap
的接口,它和SortedSet很相似:
- interface SortedMap extends Map {
- Comparator super K> comparator();
- K firstKey();
- SortedMap headMap(K toKey);
- K lastKey();
- SortedMap subMap(K fromKey, K toKey);
- SortedMap tailMap(K fromKey);
- }
WeakHashMap
里的某个键如果没有在Map外被引用时,这个键会被垃圾回收器回收并且该键的条目会从Map中删除。这个键其实是一个弱引用,即它不会增加对所引用对象的计数。
ConcurrentHashMap
是一种线程安全的Map,它不涉及同步加锁。
IdentityHashMap
在比较键值时是调用“==”操作符,而不是对象的equals方法。
说到WeakHashMap里的弱引用,我们来研究下java里的引用对象(reference object)。在java.lang.ref包里有一个Reference对象。它有三个子类SoftReference,WeakReference和PhantomReference。
通常情况一 下,我们用new创建一个对象并把它赋给一个引用,这个引用是一个强引用(Strong Reference)。比如“Object o = new Integer(3);”里的“o”就是一个强引用。垃圾回收器是绝对不会释放一个强引用所引用的对象的。
软引用(soft reference)比强引用的程度稍弱些。一般情况下,垃圾回收器是不会清理软引用所引用的对象的,看这个例子:
- class MyClass {
- protected void finalize() {
- System.out.println("MyClass.finalize");
- }
- }
-
-
- MyClass mc = new MyClass();
- SoftReference sr = new SoftReference(mc, rq);
- mc = null;
- System.gc();
当使用System.gc强行清理垃圾的时候,软引用所引用的对象并不会被清理。但如果内存不够的情况下,被软引用的对象就会被清理掉:
-
- MyClass mc = new MyClass();
- SoftReference sr = new SoftReference(mc, rq);
- mc = null;
-
-
- ArrayList al = new ArrayList();
- while (true)
- al.add(new int[204800]);
-
-
可以看到在抛出异常前,虚拟机把软引用所指的对象清理掉了。软引用的这样的特性特别适合作为内存的Cache。
弱引用(Weak reference)要比软引用的程度还弱些。如果某对象没有被强引用或软引用,而只是被弱引用,垃圾回收器会清理掉这个对象:
- MyClass mc = new MyClass();
- WeakReference wr = new WeakReference(mc);
- mc = null;
- System.gc();
虚引用(Phantom reference)不干涉对象的生命周期,它只保证当它所引用的对象被清理时,虚引用本身被放进一个ReferenceQueue里。
- ReferenceQueue rq = new ReferenceQueue();
- MyClass mc = new MyClass();
- PhantomReference pr = new PhantomReference(mc, rq);
- mc = null;
- System.gc();
-
- Reference r = null;
- try {
- r = rq.remove();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- if (r != null)
- System.out.println("You die!!!");
当被虚引用的对象被gc回收时,gc会把PhantomReference放入ReferenceQueue里。由于finalize方法的不可靠,虚引用可以更好地保证一些清理工作。
最后介绍一个很节省存储空间的容器BitSet。它的每个元素是一个位,你可以设置它的各个位,并且可以进行与或操作等。BitSet的最小size是64位。下面给出一个例子:
- void print(BitSet bs) {
- for (int i = 0; i < bs.size(); ++i)
- System.out.print(bs.get(i) ? '1' : '0');
- System.out.println();
- }
-
- BitSet bs1 = new BitSet();
- bs1.set(2, 15);
- print(bs1);
- bs1.set(4, false);
- print(bs1);
- BitSet bs2 = new BitSet();
- bs2.set(8, 27);
- print(bs2);
- bs2.and(bs1);
- print(bs2);
Java I/O系统
先看一下java.io.File类。它其实应该命名为FileInfo会好一些。看看它能做什么:
它能用来创建一个文件:
- File file = new File("MyFile");
- if (!file.exists()) {
- try {
- file.createNewFile();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
查看一个文件的属性
:
- boolean canRead = file.canRead();
- long lastModified = file.lastModified();
- String absolutePath = file.getAbsolutePath();
- long freeSpace = file.getFreeSpace();
改变一个文件的属性
:
- file.setExecutable(false);
- file.setWritable(true);
- file.setLastModified(System.currentTimeMillis());
移动/重命名一个文件
:
- File otherFile = new File("MyOtherFile");
- file.renameTo(otherFile);
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- boolean exists = file.exists();
创建一个文件夹
:
- File folder = new File("MyFolder");
- if (!folder.exists())
- folder.mkdir();
列出一个文件夹下的所有文件和文件夹
:
- String fileNamesInFolder[] = folder.list();
- File filesInFolder[] = folder.listFiles();
当然你也可以
列出一个文件夹下你感兴趣的文件和文件夹
:
- String interestedfileNames[] = folder.list(new FilenameFilter() {
- public boolean accept(File dir, String name) {
- return name.endsWith(".txt");
- }
- });
用File的静态方法
得到系统所有的根目录
:
- File roots[] = File.listRoots();
- System.out.println(Arrays.toString(roots));
得到一个文件或文件夹的父文件夹
(只对绝对路径有效):
- System.out.println(file.getParent());
- File absFile = file.getAbsoluteFile();
- System.out.println(absFile.getParent());
最后是
删除一个文件或文件夹
:
- boolean deleted = file.delete();
- deleted = folder.delete();
输入输出(I/O)的对象并不仅仅是文件,可以是任何形式的设备,比如屏幕或是网络。下面介绍四个I/O最最基本的类,它们是InputStream、OutputStream、Reader和Writer。
InputStream是所有“输入流”的基类,它是一个纯虚类并定义了一个读取字节的纯虚方法:“public int read() throws IOException”,你可以这样定义InputStream的子类:
- class MyInputStream extends InputStream {
- @Override
- public int read() throws IOException {
- if (itor >= data.length)
- return -1;
- return data[itor++];
- }
-
- private int itor = 0;
- private byte data[] = { 2, 5, 9, 8, 3, 4 };
- }
在定义好这个纯虚read方法只读取一个字节,但返回一个int值。这个int值如果为-1说明读取已经完毕,但如果这个值作为有效值的话,它的前三个字节会被忽略。read方法定义好后,InputStream的
read(byte[] b)
和
read(byte[] b, int off, int len)
方法会调用read()方法来实现更复杂的功能。
OutputStream是所有“输出流”的基类,它也是一个纯虚类,定义了两个写入字节的纯虚方法:“public void write(int b) throws IOException”。定义子类:
- class MyOutputStream extends OutputStream {
- @Override
- public void write(int b) throws IOException {
- data.add((byte)b);
- }
-
- private ArrayList data = new ArrayList();
- }
Write方法写入传入int值的最后一个字节。同样的,OutputStream的
write(byte[] b)
和
write(byte[] b, int off, int len)
方法会调用write()方法来完成更复杂的功能。
Reader是所有“字符读取器”的纯虚基类,它有两个纯虚方法:“public void close() throws IOException”、“public int read(char[] cbuf, int off, int len) throws IOException”。定义子类:
- class MyReader extends Reader {
- @Override
- public void close() throws IOException {
- closed = true;
- }
-
- @Override
- public int read(char[] cbuf, int off, int len) throws IOException {
- if (closed)
- throw new IOException();
- if (index >= data.length())
- return -1;
- int count = 0;
- for (int i = 0; i < len && index < data.length(); ++i) {
- cbuf[i+off] = data.charAt(index++);
- ++count;
- }
- return count;
- }
-
- private boolean closed = false;
- private String data = "This is the data. You are happy~";
- private int index = 0;
- }
Reader是InputStream的补充,它提供了读取字符的功能,而不仅仅是字节。在定义好read(char[] cbuf, int off, int len)方法后,Reader的“read()”、“read(char[] cbuf)”和“read(CharBuffer target)”方法就可以利用定义好的方法来提供更简单的读取字符的方法。
Writer是所有“写入字符器”的纯虚基类,它定义了三个纯虚方法:“public void close() throws IOException”、“public void flush() throws IOException”和“public void write(char[] cbuf, int off, int len) throws IOException”。定义子类:
- class MyWriter extends Writer {
- @Override
- public void close() throws IOException {
- closed = true;
- }
-
- @Override
- public void flush() throws IOException {
- if (closed)
- throw new IOException();
- System.out.println(data);
- }
-
- @Override
- public void write(char[] cbuf, int off, int len) throws IOException {
- if (closed)
- throw new IOException();
- for (int i = 0; i < len; ++i)
- data += cbuf[i+off];
- }
-
- private boolean closed = false;
- private String data = new String();
- }
定义好这个纯虚方法后,Writer类的“
append(char c)
”、“
append(CharSequence csq)
”、“
append(CharSequence csq, int start, int end)
”、“
write(char[] cbuf)”、“writer.write(int c)
”、“
write(String str)
”和“
write(String str, int off, int len)
”方法也都可以用了。
现在回到一个比较基本的问题,怎么从读写一个文件的数据?java提供了两个类:FileInputStream和FileOutputStream。我们可以用它们基类里定义的方法:InputStream.read(byte[] bytes)和OutputStream.write(byte[] bytes)。提起精神来,下面的代码有点长,虽然不复杂:
- void testFileIOStream() throws IOException {
- long ldata = -328910192;
- int idata = 2305910;
- short sdata = 4652;
- char cdata = 'A';
- double ddata = 98323.8253221;
- float fdata = 2382.784f;
-
-
- FileOutputStream fos = new FileOutputStream("MyFile");
- fos.write(DataConvertor.longToBytes(ldata));
- fos.write(DataConvertor.intToBytes(idata));
- fos.write(DataConvertor.shortToBytes(sdata));
- fos.write(DataConvertor.charToBytes(cdata));
- fos.write(DataConvertor.doubleToBytes(ddata));
- fos.write(DataConvertor.floatToBytes(fdata));
- fos.flush();
- fos.close();
-
- byte[] lBytes = new byte[Long.SIZE/8];
- byte[] iBytes = new byte[Integer.SIZE/8];
- byte[] sBytes = new byte[Short.SIZE/8];
- byte[] cBytes = new byte[Character.SIZE/8];
- byte[] dBytes = new byte[Double.SIZE/8];
- byte[] fBytes = new byte[Float.SIZE/8];
-
-
- FileInputStream fis = new FileInputStream("MyFile");
- fis.read(lBytes);
- fis.read(iBytes);
- fis.read(sBytes);
- fis.read(cBytes);
- fis.read(dBytes);
- fis.read(fBytes);
- fis.close();
-
-
- System.out.println("Long data: " + DataConvertor.bytesToLong(lBytes));
- System.out.println("Int data: " + DataConvertor.bytesToInt(iBytes));
- System.out.println("Short data: " + DataConvertor.bytesToShort(sBytes));
- System.out.println("Char data: " + DataConvertor.bytesToChar(cBytes));
- System.out.println("Double data: " + DataConvertor.bytesToDouble(dBytes));
- System.out.println("Float data: " + DataConvertor.bytesToFloat(fBytes));
- }
看到上面的代码里有个
DataConvertor
的类,它可以把基本类型和字节数组进行转换。它可不是java里自带的,我们得自己实现它:
- class DataConvertor {
- public static byte[] longToBytes(long l) {
- return numberToBytes(l, Long.SIZE/8);
- }
-
- public static long bytesToLong(byte[] bytes) {
- return bytesToNumber(bytes, Long.SIZE/8).longValue();
- }
-
- public static byte[] intToBytes(int n) {
- return numberToBytes(n, Integer.SIZE/8);
- }
-
- public static int bytesToInt(byte[] bytes) {
- return bytesToNumber(bytes, Integer.SIZE/8).intValue();
- }
-
- public static byte[] shortToBytes(short s) {
- return numberToBytes(s, Short.SIZE/8);
- }
-
- public static short bytesToShort(byte[] bytes) {
- return bytesToNumber(bytes, Short.SIZE/8).shortValue();
- }
-
- public static byte[] doubleToBytes(double d) {
- return longToBytes(Double.doubleToLongBits(d));
- }
-
- public static double bytesToDouble(byte[] bytes) {
- return Double.longBitsToDouble(bytesToLong(bytes));
- }
-
- public static byte[] floatToBytes(float f) {
- return intToBytes(Float.floatToRawIntBits(f));
- }
-
- public static float bytesToFloat(byte[] bytes) {
- return Float.intBitsToFloat(bytesToInt(bytes));
- }
-
- public static byte[] charToBytes(char c) {
- return numberToBytes((int)c, Character.SIZE/8);
- }
-
- public static char bytesToChar(byte[] bytes) {
- return (char)(bytesToNumber(bytes, Character.SIZE/8).intValue());
- }
-
- private static byte[] numberToBytes(Number n, final int size) {
- byte[] bytes = new byte[size];
- long l = n.longValue();
- for (int i = 0; i < size; i++)
- bytes[i] = (byte)((l >> 8*i) & 0xff);
- return bytes;
- }
-
- private static Number bytesToNumber(byte[] bytes, final int size) {
- long l = 0;
- for (int i = 0; i < size; i++)
- l |= ((long)(bytes[i] & 0xff) << (8*i));
- return l;
- }
- }
是不是有点复杂?如果我们每次写文件总要和这么底层的字节打交道的话,那样总是显得比较繁琐。java提供了另一组I/O流:
DataInputStream
和
DataOutputStream
,它可以外嵌在其它的I/O流对象外,比如FileInputStream。用这两个类重写上面的读写文件的功能:
- void testDataIOStream() throws IOException {
- DataOutputStream dos = new DataOutputStream(new FileOutputStream("MyFile"));
- dos.writeLong(ldata);
- dos.writeInt(idata);
- dos.writeShort(sdata);
- dos.writeChar(cdata);
- dos.writeDouble(ddata);
- dos.writeFloat(fdata);
- dos.flush();
- dos.close();
-
- DataInputStream dis = new DataInputStream(new FileInputStream("MyFile"));
- long l = dis.readLong();
- int n = dis.readInt();
- short s = dis.readShort();
- char c = dis.readChar();
- double d = dis.readDouble();
- float f = dis.readFloat();
- dis.close();
- }
java的I/O类库用的是
装饰者模式
,比如DataInputStream可以是任何一个InputStream的装饰者。通过这种模式,你可以组合出各种功能的I/O对象。
如果需要向文件存取字符串,而不是字节,就可以使用FileReader和FileWriter了: